mirror of
https://github.com/hwchase17/langchain.git
synced 2026-04-14 16:39:11 +00:00
Compare commits
224 Commits
langchain=
...
mdrxy/post
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69f36578d3 | ||
|
|
d899681040 | ||
|
|
608d8cf99e | ||
|
|
689ce96016 | ||
|
|
a1df299123 | ||
|
|
1d7a2690a2 | ||
|
|
5fa708fb14 | ||
|
|
66038386d4 | ||
|
|
7dc2c777ea | ||
|
|
9b8c211f98 | ||
|
|
a6e8c83878 | ||
|
|
0d3c4e9817 | ||
|
|
89e1594196 | ||
|
|
a84722e2d7 | ||
|
|
7e40de7800 | ||
|
|
97b3d6dae1 | ||
|
|
624799838c | ||
|
|
cb2b85bb1d | ||
|
|
5581600e9e | ||
|
|
28eceabd8b | ||
|
|
ca00e4fed9 | ||
|
|
57279c7b81 | ||
|
|
09c3c52fd0 | ||
|
|
8a257e777b | ||
|
|
73ebaddcf0 | ||
|
|
ee6fce5586 | ||
|
|
13301a779e | ||
|
|
331d57b429 | ||
|
|
d4663be53d | ||
|
|
0ab5010bcf | ||
|
|
3899154daf | ||
|
|
1d60235b1b | ||
|
|
b522ce7b31 | ||
|
|
3356d05557 | ||
|
|
1ead03c79d | ||
|
|
2ff1d23bba | ||
|
|
3289ee20ed | ||
|
|
3d687ea8fb | ||
|
|
5b401fa414 | ||
|
|
381f0a3971 | ||
|
|
34e867e92b | ||
|
|
0b99ca4fcd | ||
|
|
5799aa1045 | ||
|
|
cf5b011055 | ||
|
|
2ab225769d | ||
|
|
1dc2600cd4 | ||
|
|
6bcc4a1af1 | ||
|
|
725d204b95 | ||
|
|
2ef23882d2 | ||
|
|
e261924030 | ||
|
|
d22cfaf7c6 | ||
|
|
3bd8c0c4a3 | ||
|
|
a7b943bbe3 | ||
|
|
5fbf270c9d | ||
|
|
e73b027686 | ||
|
|
ecd19ff71f | ||
|
|
cb0d227d8a | ||
|
|
b688e36e38 | ||
|
|
606ef38e74 | ||
|
|
36e590ca5f | ||
|
|
fc417aaf17 | ||
|
|
5dc8ba3c99 | ||
|
|
f1ab8c5c80 | ||
|
|
bfe0a26547 | ||
|
|
bb5bd1181f | ||
|
|
9093c6effe | ||
|
|
8cb7dbd37b | ||
|
|
2a2a4067ca | ||
|
|
5e9765d811 | ||
|
|
703736a1e3 | ||
|
|
61fd703e5f | ||
|
|
4e40c2766a | ||
|
|
9ce73a73f8 | ||
|
|
b4cd67ac15 | ||
|
|
8e3c6b109f | ||
|
|
fd69425439 | ||
|
|
e6dde3267a | ||
|
|
23c4c506d3 | ||
|
|
d1404e63bb | ||
|
|
18c25e9f10 | ||
|
|
8e824d9ec4 | ||
|
|
fbe9babb34 | ||
|
|
9bd028d04a | ||
|
|
2e8744559d | ||
|
|
19edaa8acb | ||
|
|
b500244250 | ||
|
|
d972d00b3a | ||
|
|
384158daec | ||
|
|
c080296bed | ||
|
|
323c76504a | ||
|
|
ed2aa9f747 | ||
|
|
76da99e022 | ||
|
|
2847814c70 | ||
|
|
d383f00489 | ||
|
|
50c5bb5607 | ||
|
|
2b6911d9af | ||
|
|
f805ea9601 | ||
|
|
0276cc0290 | ||
|
|
ceca38d3fe | ||
|
|
5554a36ad5 | ||
|
|
bda22aa1d9 | ||
|
|
48cd13114f | ||
|
|
e6a9694f5d | ||
|
|
25bb36de81 | ||
|
|
92afcaae60 | ||
|
|
7ad1c19d9c | ||
|
|
f10225184d | ||
|
|
0c7b7e045d | ||
|
|
4c86e8ba39 | ||
|
|
048de6dfb6 | ||
|
|
557eddfd51 | ||
|
|
aa9c63b96a | ||
|
|
8aeff95341 | ||
|
|
0438f8c277 | ||
|
|
7f4f130479 | ||
|
|
6537939f53 | ||
|
|
a2529cd805 | ||
|
|
c1f1641018 | ||
|
|
225e0fa8c9 | ||
|
|
f021e899dc | ||
|
|
578cef9622 | ||
|
|
7979fd3d9f | ||
|
|
3b65985551 | ||
|
|
c4babed5c6 | ||
|
|
5ae53fdfb3 | ||
|
|
901690ceec | ||
|
|
be2c7f1aa8 | ||
|
|
b5c5ba0a5f | ||
|
|
944b43dd25 | ||
|
|
730a3676f8 | ||
|
|
cd5b36456a | ||
|
|
13cfdf1676 | ||
|
|
c25f3847d0 | ||
|
|
7ca0efde04 | ||
|
|
9495eb348d | ||
|
|
e5d4acf681 | ||
|
|
659eab2607 | ||
|
|
458a186540 | ||
|
|
a7aad60989 | ||
|
|
9da28bac86 | ||
|
|
0b91774263 | ||
|
|
5517ef37fb | ||
|
|
2bbe4216e0 | ||
|
|
fcc02f78e4 | ||
|
|
721bf15430 | ||
|
|
dcfd9c0e04 | ||
|
|
e03d6b80d5 | ||
|
|
33378f16fb | ||
|
|
ea25f5ebdd | ||
|
|
04c0c1bdc3 | ||
|
|
c1f5d0963d | ||
|
|
e81f00fb29 | ||
|
|
9ecf6360af | ||
|
|
7ce68f27da | ||
|
|
03ae39747b | ||
|
|
10de0a5364 | ||
|
|
30ac1da0de | ||
|
|
6d447f89d9 | ||
|
|
5ef9f6e036 | ||
|
|
e3939ade5a | ||
|
|
b0e4ef3158 | ||
|
|
ca7790f895 | ||
|
|
5884fb9523 | ||
|
|
0bd862b814 | ||
|
|
85f1ba2351 | ||
|
|
d46187201d | ||
|
|
3d78cc69f1 | ||
|
|
a92c032ff6 | ||
|
|
88b5f22f1c | ||
|
|
78b2d51edc | ||
|
|
294dda8df2 | ||
|
|
21c7cf1fa0 | ||
|
|
2212137931 | ||
|
|
e99ccbc126 | ||
|
|
75e237643a | ||
|
|
1f403cf612 | ||
|
|
451e8496e7 | ||
|
|
d4b7a6542e | ||
|
|
75b07b3d4e | ||
|
|
2e0bed6a21 | ||
|
|
5ec0fa69de | ||
|
|
6a416c6186 | ||
|
|
3dcafac79b | ||
|
|
d3e9c4d29d | ||
|
|
1cc4dc7cc9 | ||
|
|
398c067f30 | ||
|
|
d84eef667a | ||
|
|
8d93720c70 | ||
|
|
85c401f648 | ||
|
|
04ec6cacaf | ||
|
|
ed9bd6e3ad | ||
|
|
c739afd45b | ||
|
|
4fbeffcfee | ||
|
|
72f1d79022 | ||
|
|
f6297ced67 | ||
|
|
4804bd6ec2 | ||
|
|
10087ac024 | ||
|
|
f752c1a07f | ||
|
|
7902fa3238 | ||
|
|
4be9407b09 | ||
|
|
9225bff326 | ||
|
|
d4cb740e0c | ||
|
|
e5c9912a89 | ||
|
|
8bca31f8c4 | ||
|
|
c5baa3ac27 | ||
|
|
795e746ca7 | ||
|
|
6519a5675b | ||
|
|
e9f7cd3e0e | ||
|
|
5c94e47d14 | ||
|
|
e0950f29b7 | ||
|
|
71778cb721 | ||
|
|
37d8666276 | ||
|
|
c286c06f16 | ||
|
|
b83e9b1056 | ||
|
|
c1f66611fc | ||
|
|
f93bc48915 | ||
|
|
516d74b6df | ||
|
|
c85f7b6061 | ||
|
|
f167c35243 | ||
|
|
b8a76cb6e9 | ||
|
|
dbcdf0b702 | ||
|
|
beb2ee6edf | ||
|
|
bc06782b1a | ||
|
|
48ea9f104b |
@@ -26,7 +26,7 @@
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
// Run commands after the container is created
|
||||
"postCreateCommand": "uv sync && echo 'LangChain (Python) dev environment ready!'",
|
||||
"postCreateCommand": "cd libs/langchain_v1 && uv sync && echo 'LangChain (Python) dev environment ready!'",
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"GitHub.copilot-chat"
|
||||
],
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": ".venv/bin/python",
|
||||
"python.defaultInterpreterPath": "libs/langchain_v1/.venv/bin/python",
|
||||
"python.formatting.provider": "none",
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
34
.dockerignore
Normal file
34
.dockerignore
Normal file
@@ -0,0 +1,34 @@
|
||||
# Git
|
||||
.git
|
||||
.github
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
.venv
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
*.egg-info
|
||||
.tox
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Worktree
|
||||
worktree
|
||||
|
||||
# Test artifacts
|
||||
.coverage
|
||||
htmlcov
|
||||
coverage.xml
|
||||
|
||||
# Build artifacts
|
||||
dist
|
||||
build
|
||||
|
||||
# Misc
|
||||
*.log
|
||||
.DS_Store
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: "\U0001F41B Bug Report"
|
||||
description: Report a bug in LangChain. To report a security issue, please instead use the security option below. For questions, please use the LangChain forum.
|
||||
description: Report a bug in LangChain. To report a security issue, please instead use the security option (below). For questions, please use the LangChain forum (below).
|
||||
labels: ["bug"]
|
||||
type: bug
|
||||
body:
|
||||
@@ -76,7 +76,7 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Example Code (Python)
|
||||
label: Reproduction Steps / Example Code (Python)
|
||||
description: |
|
||||
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case.
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,9 +1,6 @@
|
||||
blank_issues_enabled: false
|
||||
version: 2.1
|
||||
contact_links:
|
||||
- name: 📚 Documentation issue
|
||||
url: https://github.com/langchain-ai/docs/issues/new?template=01-langchain.yml
|
||||
about: Report an issue related to the LangChain documentation
|
||||
- name: 💬 LangChain Forum
|
||||
url: https://forum.langchain.com/
|
||||
about: General community discussions and support
|
||||
@@ -13,3 +10,6 @@ contact_links:
|
||||
- name: 📚 API Reference Documentation
|
||||
url: https://reference.langchain.com/python/
|
||||
about: View the official LangChain API reference documentation
|
||||
- name: 📚 Documentation issue
|
||||
url: https://github.com/langchain-ai/docs/issues/new?template=01-langchain.yml
|
||||
about: Report an issue related to the LangChain documentation
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: "✨ Feature Request"
|
||||
description: Request a new feature or enhancement for LangChain. For questions, please use the LangChain forum.
|
||||
description: Request a new feature or enhancement for LangChain. For questions, please use the LangChain forum (below).
|
||||
labels: ["feature request"]
|
||||
type: feature
|
||||
body:
|
||||
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -17,7 +17,7 @@ Thank you for contributing to LangChain! Follow these steps to have your pull re
|
||||
- Write 1-2 sentences summarizing the change.
|
||||
- If this PR addresses a specific issue, please include "Fixes #ISSUE_NUMBER" in the description to automatically close the issue when the PR is merged.
|
||||
- If there are any breaking changes, please clearly describe them.
|
||||
- If this PR depends on another PR being merged first, please include "Depends on #PR_NUMBER" inthe description.
|
||||
- If this PR depends on another PR being merged first, please include "Depends on #PR_NUMBER" in the description.
|
||||
|
||||
3. Run `make format`, `make lint` and `make test` from the root of the package(s) you've modified.
|
||||
|
||||
@@ -27,4 +27,4 @@ Additional guidelines:
|
||||
|
||||
- We ask that if you use generative AI for your contribution, you include a disclaimer.
|
||||
- PRs should not touch more than one package unless absolutely necessary.
|
||||
- Do not update the `uv.lock` files unless or add dependencies to `pyproject.toml` files (even optional ones) unless you have explicit permission to do so by a maintainer.
|
||||
- Do not update the `uv.lock` files or add dependencies to `pyproject.toml` files (even optional ones) unless you have explicit permission to do so by a maintainer.
|
||||
|
||||
2
.github/actions/uv_setup/action.yml
vendored
2
.github/actions/uv_setup/action.yml
vendored
@@ -27,7 +27,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install uv and set the python version
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: ${{ env.UV_VERSION }}
|
||||
python-version: ${{ inputs.python-version }}
|
||||
|
||||
19
.github/pr-file-labeler.yml
vendored
19
.github/pr-file-labeler.yml
vendored
@@ -118,17 +118,6 @@ xai:
|
||||
- any-glob-to-any-file:
|
||||
- "libs/partners/xai/**/*"
|
||||
|
||||
# Infrastructure and DevOps
|
||||
infra:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- ".github/**/*"
|
||||
- "Makefile"
|
||||
- ".pre-commit-config.yaml"
|
||||
- "scripts/**/*"
|
||||
- "docker/**/*"
|
||||
- "Dockerfile*"
|
||||
|
||||
github_actions:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -142,11 +131,3 @@ dependencies:
|
||||
- "uv.lock"
|
||||
- "**/requirements*.txt"
|
||||
- "**/poetry.lock"
|
||||
|
||||
# Documentation
|
||||
documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "**/*.md"
|
||||
- "**/README*"
|
||||
|
||||
|
||||
6
.github/workflows/_lint.yml
vendored
6
.github/workflows/_lint.yml
vendored
@@ -47,6 +47,12 @@ jobs:
|
||||
cache-suffix: lint-${{ inputs.working-directory }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
# - name: "🔒 Verify Lockfile is Up-to-Date"
|
||||
# working-directory: ${{ inputs.working-directory }}
|
||||
# run: |
|
||||
# unset UV_FROZEN
|
||||
# uv lock --check
|
||||
|
||||
- name: "📦 Install Lint & Typing Dependencies"
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
|
||||
81
.github/workflows/_release.yml
vendored
81
.github/workflows/_release.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -394,9 +394,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
if: false # temporarily skip
|
||||
strategy:
|
||||
matrix:
|
||||
partner: [anthropic]
|
||||
partner: [openai, anthropic]
|
||||
fail-fast: false # Continue testing other partners if one fails
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
@@ -430,7 +431,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
if: startsWith(inputs.working-directory, 'libs/core')
|
||||
with:
|
||||
name: dist
|
||||
@@ -470,6 +471,67 @@ jobs:
|
||||
uv pip install ../../core/dist/*.whl
|
||||
make integration_tests
|
||||
|
||||
# Test external packages that depend on langchain-core/langchain against the new release
|
||||
# Only runs for core and langchain_v1 releases to catch breaking changes before publish
|
||||
test-dependents:
|
||||
name: "🐍 Python ${{ matrix.python-version }}: ${{ matrix.package.path }}"
|
||||
needs:
|
||||
- build
|
||||
- release-notes
|
||||
- test-pypi-publish
|
||||
- pre-release-checks
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Only run for core or langchain_v1 releases
|
||||
if: startsWith(inputs.working-directory, 'libs/core') || startsWith(inputs.working-directory, 'libs/langchain_v1')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11", "3.13"]
|
||||
package:
|
||||
- name: deepagents
|
||||
repo: langchain-ai/deepagents
|
||||
path: libs/deepagents
|
||||
# No API keys needed for now - deepagents `make test` only runs unit tests
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
path: langchain
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ matrix.package.repo }}
|
||||
path: ${{ matrix.package.name }}
|
||||
|
||||
- name: Set up Python + uv
|
||||
uses: "./langchain/.github/actions/uv_setup"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Install ${{ matrix.package.name }} with local packages
|
||||
# External dependents don't have [tool.uv.sources] pointing to this repo,
|
||||
# so we install the package normally then override with the built wheel.
|
||||
run: |
|
||||
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
|
||||
|
||||
# Install the package with test dependencies
|
||||
uv sync --group test
|
||||
|
||||
# Override with the built wheel from this release
|
||||
uv pip install $GITHUB_WORKSPACE/dist/*.whl
|
||||
|
||||
- name: Run ${{ matrix.package.name }} tests
|
||||
run: |
|
||||
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
|
||||
make test
|
||||
|
||||
publish:
|
||||
# Publishes the package to PyPI
|
||||
needs:
|
||||
@@ -477,7 +539,10 @@ jobs:
|
||||
- release-notes
|
||||
- test-pypi-publish
|
||||
- pre-release-checks
|
||||
- test-prior-published-packages-against-new-core
|
||||
- test-dependents
|
||||
# - test-prior-published-packages-against-new-core
|
||||
# Run if all needed jobs succeeded or were skipped (test-dependents only runs for core/langchain_v1)
|
||||
if: ${{ !cancelled() && !failure() }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# This permission is used for trusted publishing:
|
||||
@@ -499,7 +564,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -539,7 +604,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
|
||||
4
.github/workflows/auto-label-by-package.yml
vendored
4
.github/workflows/auto-label-by-package.yml
vendored
@@ -17,8 +17,8 @@ jobs:
|
||||
script: |
|
||||
const body = context.payload.issue.body || "";
|
||||
|
||||
// Extract text under "### Package"
|
||||
const match = body.match(/### Package\s+([\s\S]*?)\n###/i);
|
||||
// Extract text under "### Package" (handles " (Required)" suffix and being last section)
|
||||
const match = body.match(/### Package[^\n]*\n([\s\S]*?)(?:\n###|$)/i);
|
||||
if (!match) return;
|
||||
|
||||
const packageSection = match[1].trim();
|
||||
|
||||
126
.github/workflows/integration_tests.yml
vendored
126
.github/workflows/integration_tests.yml
vendored
@@ -1,8 +1,8 @@
|
||||
# Routine integration tests against partner libraries with live API credentials.
|
||||
#
|
||||
# Uses `make integration_tests` for each library in the matrix.
|
||||
# Uses `make integration_tests` within each library being tested.
|
||||
#
|
||||
# Runs daily. Can also be triggered manually for immediate updates.
|
||||
# Runs daily with the option to trigger manually.
|
||||
|
||||
name: "⏰ Integration Tests"
|
||||
run-name: "Run Integration Tests - ${{ inputs.working-directory-force || 'all libs' }} (Python ${{ inputs.python-version-force || '3.10, 3.13' }})"
|
||||
@@ -24,17 +24,29 @@ permissions:
|
||||
|
||||
env:
|
||||
UV_FROZEN: "true"
|
||||
DEFAULT_LIBS: '["libs/partners/openai", "libs/partners/anthropic", "libs/partners/fireworks", "libs/partners/groq", "libs/partners/mistralai", "libs/partners/xai", "libs/partners/google-vertexai", "libs/partners/google-genai", "libs/partners/aws"]'
|
||||
DEFAULT_LIBS: >-
|
||||
["libs/partners/openai",
|
||||
"libs/partners/anthropic",
|
||||
"libs/partners/fireworks",
|
||||
"libs/partners/groq",
|
||||
"libs/partners/mistralai",
|
||||
"libs/partners/xai",
|
||||
"libs/partners/google-vertexai",
|
||||
"libs/partners/google-genai",
|
||||
"libs/partners/aws"]
|
||||
|
||||
jobs:
|
||||
# Generate dynamic test matrix based on input parameters or defaults
|
||||
# Only runs on the main repo (for scheduled runs) or when manually triggered
|
||||
compute-matrix:
|
||||
# Defend against forks running scheduled jobs, but allow manual runs from forks
|
||||
if: github.repository_owner == 'langchain-ai' || github.event_name != 'schedule'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
name: "📋 Compute Test Matrix"
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
python-version-min-3-11: ${{ steps.set-matrix.outputs.python-version-min-3-11 }}
|
||||
steps:
|
||||
- name: "🔢 Generate Python & Library Matrix"
|
||||
id: set-matrix
|
||||
@@ -47,9 +59,16 @@ jobs:
|
||||
# python-version should default to 3.10 and 3.13, but is overridden to [PYTHON_VERSION_FORCE] if set
|
||||
# working-directory should default to DEFAULT_LIBS, but is overridden to [WORKING_DIRECTORY_FORCE] if set
|
||||
python_version='["3.10", "3.13"]'
|
||||
python_version_min_3_11='["3.11", "3.13"]'
|
||||
working_directory="$DEFAULT_LIBS"
|
||||
if [ -n "$PYTHON_VERSION_FORCE" ]; then
|
||||
python_version="[\"$PYTHON_VERSION_FORCE\"]"
|
||||
# Bound forced version to >= 3.11 for packages requiring it
|
||||
if [ "$(echo "$PYTHON_VERSION_FORCE >= 3.11" | bc -l)" -eq 1 ]; then
|
||||
python_version_min_3_11="[\"$PYTHON_VERSION_FORCE\"]"
|
||||
else
|
||||
python_version_min_3_11='["3.11"]'
|
||||
fi
|
||||
fi
|
||||
if [ -n "$WORKING_DIRECTORY_FORCE" ]; then
|
||||
working_directory="[\"$WORKING_DIRECTORY_FORCE\"]"
|
||||
@@ -57,8 +76,10 @@ jobs:
|
||||
matrix="{\"python-version\": $python_version, \"working-directory\": $working_directory}"
|
||||
echo $matrix
|
||||
echo "matrix=$matrix" >> $GITHUB_OUTPUT
|
||||
echo "python-version-min-3-11=$python_version_min_3_11" >> $GITHUB_OUTPUT
|
||||
|
||||
# Run integration tests against partner libraries with live API credentials
|
||||
build:
|
||||
integration-tests:
|
||||
if: github.repository_owner == 'langchain-ai' || github.event_name != 'schedule'
|
||||
name: "🐍 Python ${{ matrix.python-version }}: ${{ matrix.working-directory }}"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -74,15 +95,27 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
path: langchain
|
||||
|
||||
# These libraries exist outside of the monorepo and need to be checked out separately
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: langchain-ai/langchain-google
|
||||
path: langchain-google
|
||||
- name: "🔐 Authenticate to Google Cloud"
|
||||
id: "auth"
|
||||
uses: google-github-actions/auth@v3
|
||||
with:
|
||||
credentials_json: "${{ secrets.GOOGLE_CREDENTIALS }}"
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: langchain-ai/langchain-aws
|
||||
path: langchain-aws
|
||||
|
||||
- name: "🔐 Configure AWS Credentials"
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
- name: "📦 Organize External Libraries"
|
||||
run: |
|
||||
rm -rf \
|
||||
@@ -97,27 +130,27 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: "🔐 Authenticate to Google Cloud"
|
||||
id: "auth"
|
||||
uses: google-github-actions/auth@v3
|
||||
with:
|
||||
credentials_json: "${{ secrets.GOOGLE_CREDENTIALS }}"
|
||||
|
||||
- name: "🔐 Configure AWS Credentials"
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: "📦 Install Dependencies"
|
||||
# Partner packages use [tool.uv.sources] in their pyproject.toml to resolve
|
||||
# langchain-core/langchain to local editable installs, so `uv sync` automatically
|
||||
# tests against the versions from the current branch (not published releases).
|
||||
|
||||
# TODO: external google/aws don't have local resolution since they live in
|
||||
# separate repos, so they pull `core`/`langchain_v1` from PyPI. We should update
|
||||
# their dev groups to use git source dependencies pointing to the current
|
||||
# branch's latest commit SHA to fully test against local langchain changes.
|
||||
run: |
|
||||
echo "Running scheduled tests, installing dependencies with uv..."
|
||||
cd langchain/${{ matrix.working-directory }}
|
||||
uv sync --group test --group test_integration
|
||||
|
||||
- name: "🚀 Run Integration Tests"
|
||||
# WARNING: All secrets below are available to every matrix job regardless of
|
||||
# which package is being tested. This is intentional for simplicity, but means
|
||||
# any test file could technically access any key. Only use for trusted code.
|
||||
env:
|
||||
LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }}
|
||||
|
||||
AI21_API_KEY: ${{ secrets.AI21_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }}
|
||||
@@ -155,7 +188,6 @@ jobs:
|
||||
WATSONX_APIKEY: ${{ secrets.WATSONX_APIKEY }}
|
||||
WATSONX_PROJECT_ID: ${{ secrets.WATSONX_PROJECT_ID }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }}
|
||||
run: |
|
||||
cd langchain/${{ matrix.working-directory }}
|
||||
make integration_tests
|
||||
@@ -179,3 +211,59 @@ jobs:
|
||||
# grep will exit non-zero if the target message isn't found,
|
||||
# and `set -e` above will cause the step to fail.
|
||||
echo "$STATUS" | grep 'nothing to commit, working tree clean'
|
||||
|
||||
# Test dependent packages against local packages to catch breaking changes
|
||||
test-dependents:
|
||||
# Defend against forks running scheduled jobs, but allow manual runs from forks
|
||||
if: github.repository_owner == 'langchain-ai' || github.event_name != 'schedule'
|
||||
|
||||
name: "🐍 Python ${{ matrix.python-version }}: ${{ matrix.package.path }}"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [compute-matrix]
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# deepagents requires Python >= 3.11, use bounded version from compute-matrix
|
||||
python-version: ${{ fromJSON(needs.compute-matrix.outputs.python-version-min-3-11) }}
|
||||
package:
|
||||
- name: deepagents
|
||||
repo: langchain-ai/deepagents
|
||||
path: libs/deepagents
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
path: langchain
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ matrix.package.repo }}
|
||||
path: ${{ matrix.package.name }}
|
||||
|
||||
- name: "🐍 Set up Python ${{ matrix.python-version }} + UV"
|
||||
uses: "./langchain/.github/actions/uv_setup"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: "📦 Install ${{ matrix.package.name }} with Local"
|
||||
# Unlike partner packages (which use [tool.uv.sources] for local resolution),
|
||||
# external dependents live in separate repos and need explicit overrides to
|
||||
# test against the langchain versions from the current branch, as their
|
||||
# pyproject.toml files point to released versions.
|
||||
run: |
|
||||
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
|
||||
|
||||
# Install the package with test dependencies
|
||||
uv sync --group test
|
||||
|
||||
# Override langchain packages with local versions
|
||||
uv pip install \
|
||||
-e $GITHUB_WORKSPACE/langchain/libs/core \
|
||||
-e $GITHUB_WORKSPACE/langchain/libs/langchain_v1
|
||||
|
||||
# No API keys needed for now - deepagents `make test` only runs unit tests
|
||||
- name: "🚀 Run ${{ matrix.package.name }} Tests"
|
||||
run: |
|
||||
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
|
||||
make test
|
||||
|
||||
2
.github/workflows/pr_labeler_file.yml
vendored
2
.github/workflows/pr_labeler_file.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
# Safe since we're not checking out or running the PR's code
|
||||
# Never check out the PR's head in a pull_request_target job
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
|
||||
2
.github/workflows/pr_lint.yml
vendored
2
.github/workflows/pr_lint.yml
vendored
@@ -27,7 +27,7 @@
|
||||
# * release — prepare a new release
|
||||
#
|
||||
# Allowed Scope(s) (optional):
|
||||
# core, cli, langchain, langchain_v1, langchain-classic, model-profiles,
|
||||
# core, cli, langchain, langchain-classic, model-profiles,
|
||||
# standard-tests, text-splitters, docs, anthropic, chroma, deepseek, exa,
|
||||
# fireworks, groq, huggingface, mistralai, nomic, ollama, openai,
|
||||
# perplexity, prompty, qdrant, xai, infra, deps
|
||||
|
||||
148
.github/workflows/tag-external-contributions.yml
vendored
Normal file
148
.github/workflows/tag-external-contributions.yml
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
# Automatically tag issues and pull requests as "external" or "internal"
|
||||
# based on whether the author is a member of the langchain-ai
|
||||
# GitHub organization.
|
||||
#
|
||||
# Setup Requirements:
|
||||
# 1. Create a GitHub App with permissions:
|
||||
# - Repository: Issues (write), Pull requests (write)
|
||||
# - Organization: Members (read)
|
||||
# 2. Install the app on your organization and this repository
|
||||
# 3. Add these repository secrets:
|
||||
# - ORG_MEMBERSHIP_APP_ID: Your app's ID
|
||||
# - ORG_MEMBERSHIP_APP_PRIVATE_KEY: Your app's private key
|
||||
#
|
||||
# The GitHub App token is required to check private organization membership.
|
||||
# Without it, the workflow will fail.
|
||||
|
||||
name: Tag External Contributions
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
tag-external:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.ORG_MEMBERSHIP_APP_ID }}
|
||||
private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Check if contributor is external
|
||||
id: check-membership
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const author = context.payload.sender.login;
|
||||
|
||||
try {
|
||||
// Check if the author is a member of the langchain-ai organization
|
||||
// This requires org:read permissions to see private memberships
|
||||
const membership = await github.rest.orgs.getMembershipForUser({
|
||||
org: 'langchain-ai',
|
||||
username: author
|
||||
});
|
||||
|
||||
// Check if membership is active (not just pending invitation)
|
||||
if (membership.data.state === 'active') {
|
||||
console.log(`User ${author} is an active member of langchain-ai organization`);
|
||||
core.setOutput('is-external', 'false');
|
||||
} else {
|
||||
console.log(`User ${author} has pending membership in langchain-ai organization`);
|
||||
core.setOutput('is-external', 'true');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log(`User ${author} is not a member of langchain-ai organization`);
|
||||
core.setOutput('is-external', 'true');
|
||||
} else {
|
||||
console.error('Error checking membership:', error);
|
||||
console.log('Status:', error.status);
|
||||
console.log('Message:', error.message);
|
||||
// If we can't determine membership due to API error, assume external for safety
|
||||
core.setOutput('is-external', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
- name: Add external label to issue
|
||||
if: steps.check-membership.outputs.is-external == 'true' && github.event_name == 'issues'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.issue.number;
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
labels: ['external']
|
||||
});
|
||||
|
||||
console.log(`Added 'external' label to issue #${issue_number}`);
|
||||
|
||||
- name: Add external label to pull request
|
||||
if: steps.check-membership.outputs.is-external == 'true' && github.event_name == 'pull_request_target'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pull_number = context.payload.pull_request.number;
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pull_number,
|
||||
labels: ['external']
|
||||
});
|
||||
|
||||
console.log(`Added 'external' label to pull request #${pull_number}`);
|
||||
|
||||
- name: Add internal label to issue
|
||||
if: steps.check-membership.outputs.is-external == 'false' && github.event_name == 'issues'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.issue.number;
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
labels: ['internal']
|
||||
});
|
||||
|
||||
console.log(`Added 'internal' label to issue #${issue_number}`);
|
||||
|
||||
- name: Add internal label to pull request
|
||||
if: steps.check-membership.outputs.is-external == 'false' && github.event_name == 'pull_request_target'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pull_number = context.payload.pull_request.number;
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pull_number,
|
||||
labels: ['internal']
|
||||
});
|
||||
|
||||
console.log(`Added 'internal' label to pull request #${pull_number}`);
|
||||
@@ -1,4 +1,24 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: no-commit-to-branch # prevent direct commits to protected branches
|
||||
args: ["--branch", "master"]
|
||||
- id: check-yaml # validate YAML syntax
|
||||
args: ["--unsafe"] # allow custom tags
|
||||
- id: check-toml # validate TOML syntax
|
||||
- id: end-of-file-fixer # ensure files end with a newline
|
||||
- id: trailing-whitespace # remove trailing whitespace from lines
|
||||
exclude: \.ambr$
|
||||
|
||||
# Text normalization hooks for consistent formatting
|
||||
- repo: https://github.com/sirosen/texthooks
|
||||
rev: 0.6.8
|
||||
hooks:
|
||||
- id: fix-smartquotes # replace curly quotes with straight quotes
|
||||
- id: fix-spaces # replace non-standard spaces (e.g., non-breaking) with regular spaces
|
||||
|
||||
# Per-package format and lint hooks for the monorepo
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: core
|
||||
@@ -97,3 +117,15 @@ repos:
|
||||
entry: make -C libs/partners/qdrant format lint
|
||||
files: ^libs/partners/qdrant/
|
||||
pass_filenames: false
|
||||
- id: core-version
|
||||
name: check core version consistency
|
||||
language: system
|
||||
entry: make -C libs/core check_version
|
||||
files: ^libs/core/(pyproject\.toml|langchain_core/version\.py)$
|
||||
pass_filenames: false
|
||||
- id: langchain-v1-version
|
||||
name: check langchain version consistency
|
||||
language: system
|
||||
entry: make -C libs/langchain_v1 check_version
|
||||
files: ^libs/langchain_v1/(pyproject\.toml|langchain/__init__\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -6,8 +6,6 @@
|
||||
"ms-toolsai.jupyter",
|
||||
"ms-toolsai.jupyter-keymap",
|
||||
"ms-toolsai.jupyter-renderers",
|
||||
"ms-toolsai.vscode-jupyter-cell-tags",
|
||||
"ms-toolsai.vscode-jupyter-slideshow",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"bierner.markdown-mermaid",
|
||||
|
||||
@@ -85,6 +85,7 @@ Suggest PR titles that follow Conventional Commits format. Refer to .github/work
|
||||
### Maintain stable public interfaces
|
||||
|
||||
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
|
||||
You should warn the developer for any function signature changes, regardless of whether they look breaking or not.
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ Suggest PR titles that follow Conventional Commits format. Refer to .github/work
|
||||
### Maintain stable public interfaces
|
||||
|
||||
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
|
||||
You should warn the developer for any function signature changes, regardless of whether they look breaking or not.
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/langchain-ai/langchain" target="_blank"><img src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode" alt="Open in Dev Containers"></a>
|
||||
<a href="https://codespaces.new/langchain-ai/langchain" target="_blank"><img src="https://github.com/codespaces/badge.svg" alt="Open in Github Codespace" title="Open in Github Codespace" width="150" height="20"></a>
|
||||
<a href="https://codspeed.io/langchain-ai/langchain" target="_blank"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed Badge"></a>
|
||||
<a href="https://twitter.com/langchainai" target="_blank"><img src="https://img.shields.io/twitter/url/https/twitter.com/langchainai.svg?style=social&label=Follow%20%40LangChainAI" alt="Twitter / X"></a>
|
||||
<a href="https://x.com/langchain" target="_blank"><img src="https://img.shields.io/twitter/url/https/twitter.com/langchain.svg?style=social&label=Follow%20%40LangChain" alt="Twitter / X"></a>
|
||||
</div>
|
||||
|
||||
LangChain is a framework for building agents and LLM-powered applications. It helps you chain together interoperable components and third-party integrations to simplify AI application development – all while future-proofing decisions as the underlying technology evolves.
|
||||
@@ -71,4 +71,5 @@ To improve your LLM application development, pair LangChain with:
|
||||
|
||||
- [API Reference](https://reference.langchain.com/python) – Detailed reference on navigating base packages and integrations for LangChain.
|
||||
- [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview) – Learn how to contribute to LangChain projects and find good first issues.
|
||||
- [Code of Conduct](https://github.com/langchain-ai/langchain/blob/master/.github/CODE_OF_CONDUCT.md) – Our community guidelines and standards for participation.
|
||||
- [Code of Conduct](https://github.com/langchain-ai/langchain/?tab=coc-ov-file) – Our community guidelines and standards for participation.
|
||||
- [LangChain Academy](https://academy.langchain.com/) – Comprehensive, free courses on LangChain libraries and products, made by the LangChain team.
|
||||
|
||||
20
libs/Makefile
Normal file
20
libs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Makefile for libs/ directory
|
||||
# Contains targets that operate across multiple packages
|
||||
|
||||
LANGCHAIN_DIRS = core text-splitters langchain langchain_v1 model-profiles
|
||||
|
||||
.PHONY: lock check-lock
|
||||
|
||||
# Regenerate lockfiles for all core packages
|
||||
lock:
|
||||
@for dir in $(LANGCHAIN_DIRS); do \
|
||||
echo "=== Locking $$dir ==="; \
|
||||
(cd $$dir && uv lock); \
|
||||
done
|
||||
|
||||
# Verify all lockfiles are up-to-date
|
||||
check-lock:
|
||||
@for dir in $(LANGCHAIN_DIRS); do \
|
||||
echo "=== Checking $$dir ==="; \
|
||||
(cd $$dir && uv lock --check) || exit 1; \
|
||||
done
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://pypi.org/project/langchain-cli/#history)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://pypistats.org/packages/langchain-cli)
|
||||
[](https://twitter.com/langchainai)
|
||||
[](https://x.com/langchain)
|
||||
|
||||
## Quick Install
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ dev-dependencies = [
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "T201"]
|
||||
|
||||
[tool.ruff.lint.flake8-tidy-imports]
|
||||
ban-relative-imports = "all"
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"docs/**" = [ "ALL",]
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Homepage = "https://docs.langchain.com/"
|
||||
Documentation = "https://docs.langchain.com/"
|
||||
Source = "https://github.com/langchain-ai/langchain/tree/master/libs/cli"
|
||||
Changelog = "https://github.com/langchain-ai/langchain/releases?q=%22langchain-cli%3D%3D1%22"
|
||||
Twitter = "https://x.com/LangChainAI"
|
||||
Twitter = "https://x.com/LangChain"
|
||||
Slack = "https://www.langchain.com/join-community"
|
||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
|
||||
@@ -38,14 +38,16 @@ dev = [
|
||||
"pytest-watcher>=0.3.4,<1.0.0"
|
||||
]
|
||||
lint = [
|
||||
"ruff>=0.13.1,<0.14",
|
||||
"mypy>=1.18.1,<1.19"
|
||||
"ruff>=0.14.11,<0.15.0"
|
||||
]
|
||||
test = [
|
||||
"langchain-core",
|
||||
"langchain-classic"
|
||||
]
|
||||
typing = ["langchain-classic"]
|
||||
typing = [
|
||||
"mypy>=1.19.1,<1.20",
|
||||
"langchain-classic"
|
||||
]
|
||||
test_integration = []
|
||||
|
||||
[tool.uv.sources]
|
||||
@@ -64,10 +66,6 @@ ignore = [
|
||||
"FIX002", # Line contains TODO
|
||||
"PERF203", # Rarely useful
|
||||
"PLR09", # Too many something (arg, statements, etc)
|
||||
"RUF012", # Doesn't play well with Pydantic
|
||||
"TC001", # Doesn't play well with Pydantic
|
||||
"TC002", # Doesn't play well with Pydantic
|
||||
"TC003", # Doesn't play well with Pydantic
|
||||
"TD002", # Missing author in TODO
|
||||
"TD003", # Missing issue link in TODO
|
||||
|
||||
@@ -76,7 +74,6 @@ ignore = [
|
||||
]
|
||||
unfixable = [
|
||||
"B028", # People should intentionally tune the stacklevel
|
||||
"PLW1510", # People should intentionally set the check argument
|
||||
]
|
||||
|
||||
flake8-annotations.allow-star-arg-any = true
|
||||
@@ -89,6 +86,9 @@ pyupgrade.keep-runtime-typing = true
|
||||
convention = "google"
|
||||
ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters
|
||||
|
||||
[tool.ruff.lint.flake8-tidy-imports]
|
||||
ban-relative-imports = "all"
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**" = [ "D1", "S", "SLF",]
|
||||
"scripts/**" = [ "INP", "S",]
|
||||
|
||||
@@ -4,8 +4,8 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .file import File
|
||||
from .folder import Folder
|
||||
from tests.unit_tests.migrate.cli_runner.file import File
|
||||
from tests.unit_tests.migrate.cli_runner.folder import Folder
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .file import File
|
||||
from tests.unit_tests.migrate.cli_runner.file import File
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
308
libs/cli/uv.lock
generated
308
libs/cli/uv.lock
generated
@@ -2,6 +2,15 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10.0, <4.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
@@ -143,16 +152,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.118.0"
|
||||
version = "0.128.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/28/3c/2b9345a6504e4055eaa490e0b41c10e338ad61d9aeaae41d97807873cdf2/fastapi-0.118.0.tar.gz", hash = "sha256:5e81654d98c4d2f53790a7d32d25a7353b30c81441be7d0958a26b5d761fa1c8", size = 310536, upload-time = "2025-09-29T03:37:23.126Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/54e2bdaad22ca91a59455251998d43094d5c3d3567c52c7c04774b3f43f2/fastapi-0.118.0-py3-none-any.whl", hash = "sha256:705137a61e2ef71019d2445b123aa8845bd97273c395b744d5a7dfe559056855", size = 97694, upload-time = "2025-09-29T03:37:21.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -193,6 +203,8 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" },
|
||||
@@ -202,6 +214,8 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
|
||||
@@ -211,6 +225,8 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||
@@ -220,6 +236,8 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||
@@ -227,6 +245,8 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
||||
]
|
||||
|
||||
@@ -341,7 +361,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-classic"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = { editable = "../langchain" }
|
||||
dependencies = [
|
||||
{ name = "async-timeout", marker = "python_full_version < '3.11'" },
|
||||
@@ -387,15 +407,14 @@ dev = [
|
||||
{ name = "langchain-core", editable = "../core" },
|
||||
{ name = "langchain-text-splitters", editable = "../text-splitters" },
|
||||
{ name = "playwright", specifier = ">=1.28.0,<2.0.0" },
|
||||
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
|
||||
{ name = "setuptools", specifier = ">=67.6.1,<79.0.0" },
|
||||
]
|
||||
lint = [
|
||||
{ name = "cffi", marker = "python_full_version < '3.10'", specifier = "<1.17.1" },
|
||||
{ name = "cffi", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "ruff", specifier = ">=0.13.1,<0.14.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.11,<0.15.0" },
|
||||
]
|
||||
test = [
|
||||
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
|
||||
{ name = "cffi", marker = "python_full_version < '3.10'", specifier = "<1.17.1" },
|
||||
{ name = "cffi", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
|
||||
@@ -435,7 +454,7 @@ typing = [
|
||||
{ name = "fastapi", specifier = ">=0.116.1,<1.0.0" },
|
||||
{ name = "langchain-core", editable = "../core" },
|
||||
{ name = "langchain-text-splitters", editable = "../text-splitters" },
|
||||
{ name = "mypy", specifier = ">=1.18.2,<1.19.0" },
|
||||
{ name = "mypy", specifier = ">=1.19.1,<1.20.0" },
|
||||
{ name = "mypy-protobuf", specifier = ">=3.0.0,<4.0.0" },
|
||||
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
|
||||
@@ -466,7 +485,6 @@ dev = [
|
||||
{ name = "pytest-watcher" },
|
||||
]
|
||||
lint = [
|
||||
{ name = "mypy" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
test = [
|
||||
@@ -475,6 +493,7 @@ test = [
|
||||
]
|
||||
typing = [
|
||||
{ name = "langchain-classic" },
|
||||
{ name = "mypy" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -492,20 +511,20 @@ dev = [
|
||||
{ name = "pytest", specifier = ">=7.4.2,<9.0.0" },
|
||||
{ name = "pytest-watcher", specifier = ">=0.3.4,<1.0.0" },
|
||||
]
|
||||
lint = [
|
||||
{ name = "mypy", specifier = ">=1.18.1,<1.19" },
|
||||
{ name = "ruff", specifier = ">=0.13.1,<0.14" },
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = ">=0.14.11,<0.15.0" }]
|
||||
test = [
|
||||
{ name = "langchain-classic", editable = "../langchain" },
|
||||
{ name = "langchain-core", editable = "../core" },
|
||||
]
|
||||
test-integration = []
|
||||
typing = [{ name = "langchain-classic", editable = "../langchain" }]
|
||||
typing = [
|
||||
{ name = "langchain-classic", editable = "../langchain" },
|
||||
{ name = "mypy", specifier = ">=1.19.1,<1.20" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.0.0"
|
||||
version = "1.2.7"
|
||||
source = { editable = "../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -515,6 +534,7 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "uuid-utils" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -526,6 +546,7 @@ requires-dist = [
|
||||
{ name = "pyyaml", specifier = ">=5.3.0,<7.0.0" },
|
||||
{ name = "tenacity", specifier = ">=8.1.0,!=8.4.0,<10.0.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.7.0,<5.0.0" },
|
||||
{ name = "uuid-utils", specifier = ">=0.12.0,<1.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -534,7 +555,7 @@ dev = [
|
||||
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
|
||||
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }]
|
||||
lint = [{ name = "ruff", specifier = ">=0.14.11,<0.15.0" }]
|
||||
test = [
|
||||
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
|
||||
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
|
||||
@@ -556,14 +577,14 @@ test = [
|
||||
test-integration = []
|
||||
typing = [
|
||||
{ name = "langchain-text-splitters", directory = "../text-splitters" },
|
||||
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
|
||||
{ name = "mypy", specifier = ">=1.19.1,<1.20.0" },
|
||||
{ name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" },
|
||||
{ name = "types-requests", specifier = ">=2.28.11.5,<3.0.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-text-splitters"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
source = { editable = "../text-splitters" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -579,7 +600,7 @@ dev = [
|
||||
]
|
||||
lint = [
|
||||
{ name = "langchain-core", editable = "../core" },
|
||||
{ name = "ruff", specifier = ">=0.13.1,<0.14.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.11,<0.15.0" },
|
||||
]
|
||||
test = [
|
||||
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
|
||||
@@ -596,7 +617,7 @@ test-integration = [
|
||||
{ name = "nltk", specifier = ">=3.9.1,<4.0.0" },
|
||||
{ name = "scipy", marker = "python_full_version == '3.12.*'", specifier = ">=1.7.0,<2.0.0" },
|
||||
{ name = "scipy", marker = "python_full_version >= '3.13'", specifier = ">=1.14.1,<2.0.0" },
|
||||
{ name = "sentence-transformers", marker = "python_full_version < '3.14'", specifier = ">=3.0.1,<4.0.0" },
|
||||
{ name = "sentence-transformers", specifier = ">=3.0.1,<4.0.0" },
|
||||
{ name = "spacy", marker = "python_full_version < '3.14'", specifier = ">=3.8.7,<4.0.0" },
|
||||
{ name = "thinc", specifier = ">=8.3.6,<9.0.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.8.0,<1.0.0" },
|
||||
@@ -605,14 +626,14 @@ test-integration = [
|
||||
typing = [
|
||||
{ name = "beautifulsoup4", specifier = ">=4.13.5,<5.0.0" },
|
||||
{ name = "lxml-stubs", specifier = ">=0.5.1,<1.0.0" },
|
||||
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
|
||||
{ name = "mypy", specifier = ">=1.19.1,<1.20.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.8.0,<1.0.0" },
|
||||
{ name = "types-requests", specifier = ">=2.31.0.20240218,<3.0.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.0"
|
||||
version = "1.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -622,48 +643,48 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "xxhash" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/f7/7ae10f1832ab1a6a402f451e54d6dab277e28e7d4e4204e070c7897ca71c/langgraph-1.0.0.tar.gz", hash = "sha256:5f83ed0e9bbcc37635bc49cbc9b3d9306605fa07504f955b7a871ed715f9964c", size = 472835, upload-time = "2025-10-17T20:23:38.263Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/9c/dac99ab1732e9fb2d3b673482ac28f02bee222c0319a3b8f8f73d90727e6/langgraph-1.0.6.tar.gz", hash = "sha256:dd8e754c76d34a07485308d7117221acf63990e7de8f46ddf5fe256b0a22e6c5", size = 495092, upload-time = "2026-01-12T20:33:30.778Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/42/6f6d0fe4eb661b06da8e6c59e58044e9e4221fdbffdcacae864557de961e/langgraph-1.0.0-py3-none-any.whl", hash = "sha256:4d478781832a1bc67e06c3eb571412ec47d7c57a5467d1f3775adf0e9dd4042c", size = 155416, upload-time = "2025-10-17T20:23:36.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/45/9960747781416bed4e531ed0c6b2f2c739bc7b5397d8e92155463735a40e/langgraph-1.0.6-py3-none-any.whl", hash = "sha256:bcfce190974519c72e29f6e5b17f0023914fd6f936bfab8894083215b271eb89", size = 157356, upload-time = "2026-01-12T20:33:29.191Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "ormsgpack" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/83/6404f6ed23a91d7bc63d7df902d144548434237d017820ceaa8d014035f2/langgraph_checkpoint-2.1.2.tar.gz", hash = "sha256:112e9d067a6eff8937caf198421b1ffba8d9207193f14ac6f89930c1260c06f9", size = 142420, upload-time = "2025-10-07T17:45:17.129Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/cb/2a6dad2f0a14317580cc122e2a60e7f0ecabb50aaa6dc5b7a6a2c94cead7/langgraph_checkpoint-3.0.0.tar.gz", hash = "sha256:f738695ad938878d8f4775d907d9629e9fcd345b1950196effb08f088c52369e", size = 132132, upload-time = "2025-10-20T18:35:49.132Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f2/06bf5addf8ee664291e1b9ffa1f28fc9d97e59806dc7de5aea9844cbf335/langgraph_checkpoint-2.1.2-py3-none-any.whl", hash = "sha256:911ebffb069fd01775d4b5184c04aaafc2962fcdf50cf49d524cd4367c4d0c60", size = 45763, upload-time = "2025-10-07T17:45:16.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/2a/2efe0b5a72c41e3a936c81c5f5d8693987a1b260287ff1bbebaae1b7b888/langgraph_checkpoint-3.0.0-py3-none-any.whl", hash = "sha256:560beb83e629784ab689212a3d60834fb3196b4bbe1d6ac18e5cad5d85d46010", size = 46060, upload-time = "2025-10-20T18:35:48.255Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.0"
|
||||
version = "1.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langgraph-checkpoint" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/2d/934b1129e217216a0dfaf0f7df0a10cedf2dfafe6cc8e1ee238cafaaa4a7/langgraph_prebuilt-1.0.0.tar.gz", hash = "sha256:eb75dad9aca0137451ca0395aa8541a665b3f60979480b0431d626fd195dcda2", size = 119927, upload-time = "2025-10-17T20:15:21.429Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/f5/8c75dace0d729561dce2966e630c5e312193df7e5df41a7e10cd7378c3a7/langgraph_prebuilt-1.0.6.tar.gz", hash = "sha256:c5f6cf0f5a0ac47643d2e26ae6faa38cb28885ecde67911190df9e30c4f72361", size = 162623, upload-time = "2026-01-12T20:31:28.425Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2e/ffa698eedc4c355168a9207ee598b2cc74ede92ce2b55c3469ea06978b6e/langgraph_prebuilt-1.0.0-py3-none-any.whl", hash = "sha256:ceaae4c5cee8c1f9b6468f76c114cafebb748aed0c93483b7c450e5a89de9c61", size = 28455, upload-time = "2025-10-17T20:15:20.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6c/4045822b0630cfc0f8624c4499ceaf90644142143c063a8dc385a7424fc3/langgraph_prebuilt-1.0.6-py3-none-any.whl", hash = "sha256:9fdc35048ff4ac985a55bd2a019a86d45b8184551504aff6780d096c678b39ae", size = 35322, upload-time = "2026-01-12T20:31:27.161Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.2.9"
|
||||
version = "0.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "orjson" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/d8/40e01190a73c564a4744e29a6c902f78d34d43dad9b652a363a92a67059c/langgraph_sdk-0.2.9.tar.gz", hash = "sha256:b3bd04c6be4fa382996cd2be8fbc1e7cc94857d2bc6b6f4599a7f2a245975303", size = 99802, upload-time = "2025-09-20T18:49:14.734Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/0f/ed0634c222eed48a31ba48eab6881f94ad690d65e44fe7ca838240a260c1/langgraph_sdk-0.3.3.tar.gz", hash = "sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26", size = 130589, upload-time = "2026-01-13T00:30:43.894Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/66/05/b2d34e16638241e6f27a6946d28160d4b8b641383787646d41a3727e0896/langgraph_sdk-0.2.9-py3-none-any.whl", hash = "sha256:fbf302edadbf0fb343596f91c597794e936ef68eebc0d3e1d358b6f9f72a1429", size = 56752, upload-time = "2025-09-20T18:49:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl", hash = "sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b", size = 67021, upload-time = "2026-01-13T00:30:42.264Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -706,6 +727,79 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/8e/e7a43d907a147e1f87eebdd6737483f9feba52a5d4b20f69d0bd6f2fa22f/langsmith-0.4.31-py3-none-any.whl", hash = "sha256:64f340bdead21defe5f4a6ca330c11073e35444989169f669508edf45a19025f", size = 386347, upload-time = "2025-09-25T04:18:16.69Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librt"
|
||||
version = "0.7.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/8a/071f6628363d83e803d4783e0cd24fb9c5b798164300fcfaaa47c30659c0/librt-0.7.5.tar.gz", hash = "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa", size = 145868, upload-time = "2025-12-25T03:53:16.039Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f2/3248d8419db99ab80bb36266735d1241f766ad5fd993071211f789b618a5/librt-0.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81056e01bba1394f1d92904ec61a4078f66df785316275edbaf51d90da8c6e26", size = 54703, upload-time = "2025-12-25T03:51:48.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/30/7e179543dbcb1311f84b7e797658ad85cf2d4474c468f5dbafa13f2a98a5/librt-0.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7c72c8756eeb3aefb1b9e3dac7c37a4a25db63640cac0ab6fc18e91a0edf05a", size = 56660, upload-time = "2025-12-25T03:51:49.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/91/3ba03ac1ac1abd66757a134b3bd56d9674928b163d0e686ea065a2bbb92d/librt-0.7.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddc4a16207f88f9597b397fc1f60781266d13b13de922ff61c206547a29e4bbd", size = 161026, upload-time = "2025-12-25T03:51:51.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/6e/b8365f547817d37b44c4be2ffa02630be995ef18be52d72698cecc3640c5/librt-0.7.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63055d3dda433ebb314c9f1819942f16a19203c454508fdb2d167613f7017169", size = 169530, upload-time = "2025-12-25T03:51:52.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/6a/8442eb0b6933c651a06e1888f863971f3391cc11338fdaa6ab969f7d1eac/librt-0.7.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f85f9b5db87b0f52e53c68ad2a0c5a53e00afa439bd54a1723742a2b1021276", size = 183272, upload-time = "2025-12-25T03:51:53.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/c4/b1166df6ef8e1f68d309f50bf69e8e750a5ea12fe7e2cf202c771ff359fc/librt-0.7.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c566a4672564c5d54d8ab65cdaae5a87ee14c1564c1a2ddc7a9f5811c750f023", size = 179040, upload-time = "2025-12-25T03:51:55.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/30/8f3fd9fd975b16c37832d6c248b976d2a0e33f155063781e064f249b37f1/librt-0.7.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee15c2a190ef389f14928135c6fb2d25cd3fdb7887bfd9a7b444bbdc8c06b96", size = 173506, upload-time = "2025-12-25T03:51:56.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/71/c3d4d5658f9849bf8e07ffba99f892d49a0c9a4001323ed610db72aedc82/librt-0.7.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:584cb3e605ec45ba350962cec853e17be0a25a772f21f09f1e422f7044ae2a7d", size = 193573, upload-time = "2025-12-25T03:51:57.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/7c/c1c8a0116a2eed3d58c8946c589a8f9e1354b9b825cc92eba58bb15f6fb1/librt-0.7.5-cp310-cp310-win32.whl", hash = "sha256:9c08527055fbb03c641c15bbc5b79dd2942fb6a3bd8dabf141dd7e97eeea4904", size = 42603, upload-time = "2025-12-25T03:51:59.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/00/b52c77ca294247420020b829b70465c6e6f2b9d59ab21d8051aac20432da/librt-0.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:dd810f2d39c526c42ea205e0addad5dc08ef853c625387806a29d07f9d150d9b", size = 48977, upload-time = "2025-12-25T03:52:00.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/89/42b3ccb702a7e5f7a4cf2afc8a0a8f8c5e7d4b4d3a7c3de6357673dddddb/librt-0.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f952e1a78c480edee8fb43aa2bf2e84dcd46c917d44f8065b883079d3893e8fc", size = 54705, upload-time = "2025-12-25T03:52:01.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/90/c16970b509c3c448c365041d326eeef5aeb2abaed81eb3187b26a3cd13f8/librt-0.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75965c1f4efb7234ff52a58b729d245a21e87e4b6a26a0ec08052f02b16274e4", size = 56667, upload-time = "2025-12-25T03:52:02.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/2f/da4bdf6c190503f4663fbb781dfae5564a2b1c3f39a2da8e1ac7536ac7bd/librt-0.7.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:732e0aa0385b59a1b2545159e781c792cc58ce9c134249233a7c7250a44684c4", size = 161705, upload-time = "2025-12-25T03:52:03.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/88/c5da8e1f5f22b23d56e1fbd87266799dcf32828d47bf69fabc6f9673c6eb/librt-0.7.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdde31759bd8888f3ef0eebda80394a48961328a17c264dce8cc35f4b9cde35d", size = 171029, upload-time = "2025-12-25T03:52:04.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/8a/8dfc00a6f1febc094ed9a55a448fc0b3a591b5dfd83be6cfd76d0910b1f0/librt-0.7.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3146d52465b3b6397d25d513f428cb421c18df65b7378667bb5f1e3cc45805", size = 184704, upload-time = "2025-12-25T03:52:05.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/57/65dec835ff235f431801064a3b41268f2f5ee0d224dc3bbf46d911af5c1a/librt-0.7.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29c8d2fae11d4379ea207ba7fc69d43237e42cf8a9f90ec6e05993687e6d648b", size = 180720, upload-time = "2025-12-25T03:52:06.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/27/92033d169bbcaa0d9a2dd476c179e5171ec22ed574b1b135a3c6104fb7d4/librt-0.7.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb41f04046b4f22b1e7ba5ef513402cd2e3477ec610e5f92d38fe2bba383d419", size = 174538, upload-time = "2025-12-25T03:52:08.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/5c/0127098743575d5340624d8d4ec508d4d5ff0877dcee6f55f54bf03e5ed0/librt-0.7.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bb7883c1e94ceb87c2bf81385266f032da09cd040e804cc002f2c9d6b842e2f", size = 195240, upload-time = "2025-12-25T03:52:09.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/0f/be028c3e906a8ee6d29a42fd362e6d57d4143057f2bc0c454d489a0f898b/librt-0.7.5-cp311-cp311-win32.whl", hash = "sha256:84d4a6b9efd6124f728558a18e79e7cc5c5d4efc09b2b846c910de7e564f5bad", size = 42941, upload-time = "2025-12-25T03:52:10.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/3a/2f0ed57f4c3ae3c841780a95dfbea4cd811c6842d9ee66171ce1af606d25/librt-0.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab4b0d3bee6f6ff7017e18e576ac7e41a06697d8dea4b8f3ab9e0c8e1300c409", size = 49244, upload-time = "2025-12-25T03:52:11.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7c/d7932aedfa5a87771f9e2799e7185ec3a322f4a1f4aa87c234159b75c8c8/librt-0.7.5-cp311-cp311-win_arm64.whl", hash = "sha256:730be847daad773a3c898943cf67fb9845a3961d06fb79672ceb0a8cd8624cfa", size = 42614, upload-time = "2025-12-25T03:52:12.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/9d/cb0a296cee177c0fee7999ada1c1af7eee0e2191372058814a4ca6d2baf0/librt-0.7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba1077c562a046208a2dc6366227b3eeae8f2c2ab4b41eaf4fd2fa28cece4203", size = 55689, upload-time = "2025-12-25T03:52:14.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/5c/d7de4d4228b74c5b81a3fbada157754bb29f0e1f8c38229c669a7f90422a/librt-0.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:654fdc971c76348a73af5240d8e2529265b9a7ba6321e38dd5bae7b0d4ab3abe", size = 57142, upload-time = "2025-12-25T03:52:15.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/b2/5da779184aae369b69f4ae84225f63741662a0fe422e91616c533895d7a4/librt-0.7.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6b7b58913d475911f6f33e8082f19dd9b120c4f4a5c911d07e395d67b81c6982", size = 165323, upload-time = "2025-12-25T03:52:16.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/40/6d5abc15ab6cc70e04c4d201bb28baffff4cfb46ab950b8e90935b162d58/librt-0.7.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e0fd344bad57026a8f4ccfaf406486c2fc991838050c2fef156170edc3b775", size = 174218, upload-time = "2025-12-25T03:52:17.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/d0/5239a8507e6117a3cb59ce0095bdd258bd2a93d8d4b819a506da06d8d645/librt-0.7.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46aa91813c267c3f60db75d56419b42c0c0b9748ec2c568a0e3588e543fb4233", size = 189007, upload-time = "2025-12-25T03:52:18.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/a4/8eed1166ffddbb01c25363e4c4e655f4bac298debe9e5a2dcfaf942438a1/librt-0.7.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ddc0ab9dbc5f9ceaf2bf7a367bf01f2697660e908f6534800e88f43590b271db", size = 183962, upload-time = "2025-12-25T03:52:19.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/83/260e60aab2f5ccba04579c5c46eb3b855e51196fde6e2bcf6742d89140a8/librt-0.7.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a488908a470451338607650f1c064175094aedebf4a4fa37890682e30ce0b57", size = 177611, upload-time = "2025-12-25T03:52:21.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/36/6dcfed0df41e9695665462bab59af15b7ed2b9c668d85c7ebadd022cbb76/librt-0.7.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47fc52602ffc374e69bf1b76536dc99f7f6dd876bd786c8213eaa3598be030a", size = 199273, upload-time = "2025-12-25T03:52:22.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/b7/157149c8cffae6bc4293a52e0267860cee2398cb270798d94f1c8a69b9ae/librt-0.7.5-cp312-cp312-win32.whl", hash = "sha256:cda8b025875946ffff5a9a7590bf9acde3eb02cb6200f06a2d3e691ef3d9955b", size = 43191, upload-time = "2025-12-25T03:52:23.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/91/197dfeb8d3bdeb0a5344d0d8b3077f183ba5e76c03f158126f6072730998/librt-0.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:b591c094afd0ffda820e931148c9e48dc31a556dc5b2b9b3cc552fa710d858e4", size = 49462, upload-time = "2025-12-25T03:52:24.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/ea/052a79454cc52081dfaa9a1c4c10a529f7a6a6805b2fac5805fea5b25975/librt-0.7.5-cp312-cp312-win_arm64.whl", hash = "sha256:532ddc6a8a6ca341b1cd7f4d999043e4c71a212b26fe9fd2e7f1e8bb4e873544", size = 42830, upload-time = "2025-12-25T03:52:25.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/9a/8f61e16de0ff76590af893cfb5b1aa5fa8b13e5e54433d0809c7033f59ed/librt-0.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a", size = 55750, upload-time = "2025-12-25T03:52:26.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/7c/a8a883804851a066f301e0bad22b462260b965d5c9e7fe3c5de04e6f91f8/librt-0.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0", size = 57170, upload-time = "2025-12-25T03:52:27.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/5d/b3b47facf5945be294cf8a835b03589f70ee0e791522f99ec6782ed738b3/librt-0.7.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5", size = 165834, upload-time = "2025-12-25T03:52:29.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/b6/b26910cd0a4e43e5d02aacaaea0db0d2a52e87660dca08293067ee05601a/librt-0.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325", size = 174820, upload-time = "2025-12-25T03:52:30.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/a3/81feddd345d4c869b7a693135a462ae275f964fcbbe793d01ea56a84c2ee/librt-0.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec", size = 189609, upload-time = "2025-12-25T03:52:31.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/a9/31310796ef4157d1d37648bf4a3b84555319f14cee3e9bad7bdd7bfd9a35/librt-0.7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89", size = 184589, upload-time = "2025-12-25T03:52:32.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/22/da3900544cb0ac6ab7a2857850158a0a093b86f92b264aa6c4a4f2355ff3/librt-0.7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25", size = 178251, upload-time = "2025-12-25T03:52:33.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/77/78e02609846e78b9b8c8e361753b3dbac9a07e6d5b567fe518de9e074ab0/librt-0.7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b", size = 199852, upload-time = "2025-12-25T03:52:34.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/25/05706f6b346429c951582f1b3561f4d5e1418d0d7ba1a0c181237cd77b3b/librt-0.7.5-cp313-cp313-win32.whl", hash = "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee", size = 43250, upload-time = "2025-12-25T03:52:35.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/59/c38677278ac0b9ae1afc611382ef6c9ea87f52ad257bd3d8d65f0eacdc6a/librt-0.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e", size = 49421, upload-time = "2025-12-25T03:52:36.895Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/47/1d71113df4a81de5fdfbd3d7244e05d3d67e89f25455c3380ca50b92741e/librt-0.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45", size = 42827, upload-time = "2025-12-25T03:52:37.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/ae/8635b4efdc784220f1378be640d8b1a794332f7f6ea81bb4859bf9d18aa7/librt-0.7.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2", size = 55191, upload-time = "2025-12-25T03:52:38.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/11/ed7ef6955dc2032af37db9b0b31cd5486a138aa792e1bb9e64f0f4950e27/librt-0.7.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f", size = 56894, upload-time = "2025-12-25T03:52:39.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/f1/02921d4a66a1b5dcd0493b89ce76e2762b98c459fe2ad04b67b2ea6fdd39/librt-0.7.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6", size = 163726, upload-time = "2025-12-25T03:52:40.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/87/27df46d2756fcb7a82fa7f6ca038a0c6064c3e93ba65b0b86fbf6a4f76a2/librt-0.7.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361", size = 172470, upload-time = "2025-12-25T03:52:42.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/a9/e65a35e5d423639f4f3d8e17301ff13cc41c2ff97677fe9c361c26dbfbb7/librt-0.7.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e", size = 186807, upload-time = "2025-12-25T03:52:43.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/b0/ac68aa582a996b1241773bd419823290c42a13dc9f494704a12a17ddd7b6/librt-0.7.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2", size = 181810, upload-time = "2025-12-25T03:52:45.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/c1/03f6717677f20acd2d690813ec2bbe12a2de305f32c61479c53f7b9413bc/librt-0.7.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760", size = 175599, upload-time = "2025-12-25T03:52:46.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/d7/f976ff4c07c59b69bb5eec7e5886d43243075bbef834428124b073471c86/librt-0.7.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2", size = 196506, upload-time = "2025-12-25T03:52:47.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/74/004f068b8888e61b454568b5479f88018fceb14e511ac0609cccee7dd227/librt-0.7.5-cp314-cp314-win32.whl", hash = "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8", size = 39747, upload-time = "2025-12-25T03:52:48.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/b1/ea3ec8fcf5f0a00df21f08972af77ad799604a306db58587308067d27af8/librt-0.7.5-cp314-cp314-win_amd64.whl", hash = "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e", size = 45970, upload-time = "2025-12-25T03:52:49.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/30/5e3fb7ac4614a50fc67e6954926137d50ebc27f36419c9963a94f931f649/librt-0.7.5-cp314-cp314-win_arm64.whl", hash = "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d", size = 39075, upload-time = "2025-12-25T03:52:50.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7f/0af0a9306a06c2aabee3a790f5aa560c50ec0a486ab818a572dd3db6c851/librt-0.7.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802", size = 57375, upload-time = "2025-12-25T03:52:51.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/1f/c85e510baf6572a3d6ef40c742eacedc02973ed2acdb5dba2658751d9af8/librt-0.7.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4", size = 59234, upload-time = "2025-12-25T03:52:52.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b1/bb6535e4250cd18b88d6b18257575a0239fa1609ebba925f55f51ae08e8e/librt-0.7.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2", size = 183873, upload-time = "2025-12-25T03:52:53.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/49/ad4a138cca46cdaa7f0e15fa912ce3ccb4cc0d4090bfeb8ccc35766fa6d5/librt-0.7.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5", size = 194609, upload-time = "2025-12-25T03:52:54.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/2d/3b3cb933092d94bb2c1d3c9b503d8775f08d806588c19a91ee4d1495c2a8/librt-0.7.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416", size = 206777, upload-time = "2025-12-25T03:52:55.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/52/6e7611d3d1347812233dabc44abca4c8065ee97b83c9790d7ecc3f782bc8/librt-0.7.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899", size = 203208, upload-time = "2025-12-25T03:52:57.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/aa/466ae4654bd2d45903fbf180815d41e3ae8903e5a1861f319f73c960a843/librt-0.7.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7", size = 196698, upload-time = "2025-12-25T03:52:58.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/8f/424f7e4525bb26fe0d3e984d1c0810ced95e53be4fd867ad5916776e18a3/librt-0.7.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf", size = 217194, upload-time = "2025-12-25T03:52:59.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/33/13a4cb798a171b173f3c94db23adaf13a417130e1493933dc0df0d7fb439/librt-0.7.5-cp314-cp314t-win32.whl", hash = "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d", size = 40282, upload-time = "2025-12-25T03:53:01.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/f1/62b136301796399d65dad73b580f4509bcbd347dff885a450bff08e80cb6/librt-0.7.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d", size = 46764, upload-time = "2025-12-25T03:53:02.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/cb/940431d9410fda74f941f5cd7f0e5a22c63be7b0c10fa98b2b7022b48cb1/librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1", size = 39728, upload-time = "2025-12-25T03:53:03.306Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
@@ -729,47 +823,48 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.18.2"
|
||||
version = "1.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1215,28 +1310,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.13.3"
|
||||
version = "0.14.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1328,15 +1423,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.48.0"
|
||||
version = "0.49.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1434,11 +1529,40 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-utils"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/0e/512fb221e4970c2f75ca9dae412d320b7d9ddc9f2b15e04ea8e44710396c/uuid_utils-0.12.0.tar.gz", hash = "sha256:252bd3d311b5d6b7f5dfce7a5857e27bb4458f222586bb439463231e5a9cbd64", size = 20889, upload-time = "2025-12-01T17:29:55.494Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/43/de5cd49a57b6293b911b6a9a62fc03e55db9f964da7d5882d9edbee1e9d2/uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3b9b30707659292f207b98f294b0e081f6d77e1fbc760ba5b41331a39045f514", size = 603197, upload-time = "2025-12-01T17:29:30.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/fa/5fd1d8c9234e44f0c223910808cde0de43bb69f7df1349e49b1afa7f2baa/uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:add3d820c7ec14ed37317375bea30249699c5d08ff4ae4dbee9fc9bce3bfbf65", size = 305168, upload-time = "2025-12-01T17:29:31.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c6/8633ac9942bf9dc97a897b5154e5dcffa58816ec4dd780b3b12b559ff05c/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8fce83ecb3b16af29c7809669056c4b6e7cc912cab8c6d07361645de12dd79", size = 340580, upload-time = "2025-12-01T17:29:32.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/88/8a61307b04b4da1c576373003e6d857a04dade52ab035151d62cb84d5cb5/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec921769afcb905035d785582b0791d02304a7850fbd6ce924c1a8976380dfc6", size = 346771, upload-time = "2025-12-01T17:29:33.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/fb/aab2dcf94b991e62aa167457c7825b9b01055b884b888af926562864398c/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f3b060330f5899a92d5c723547dc6a95adef42433e9748f14c66859a7396664", size = 474781, upload-time = "2025-12-01T17:29:35.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/7a/dbd5e49c91d6c86dba57158bbfa0e559e1ddf377bb46dcfd58aea4f0d567/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:908dfef7f0bfcf98d406e5dc570c25d2f2473e49b376de41792b6e96c1d5d291", size = 343685, upload-time = "2025-12-01T17:29:36.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/19/8c4b1d9f450159733b8be421a4e1fb03533709b80ed3546800102d085572/uuid_utils-0.12.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c6a24148926bd0ca63e8a2dabf4cc9dc329a62325b3ad6578ecd60fbf926506", size = 366482, upload-time = "2025-12-01T17:29:37.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/43/c79a6e45687647f80a159c8ba34346f287b065452cc419d07d2212d38420/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:64a91e632669f059ef605f1771d28490b1d310c26198e46f754e8846dddf12f4", size = 523132, upload-time = "2025-12-01T17:29:39.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/a2/b2d75a621260a40c438aa88593827dfea596d18316520a99e839f7a5fb9d/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:93c082212470bb4603ca3975916c205a9d7ef1443c0acde8fbd1e0f5b36673c7", size = 614218, upload-time = "2025-12-01T17:29:40.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/6b/ba071101626edd5a6dabf8525c9a1537ff3d885dbc210540574a03901fef/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:431b1fb7283ba974811b22abd365f2726f8f821ab33f0f715be389640e18d039", size = 546241, upload-time = "2025-12-01T17:29:41.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/12/9a942b81c0923268e6d85bf98d8f0a61fcbcd5e432fef94fdf4ce2ef8748/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd7838c40149100299fa37cbd8bab5ee382372e8e65a148002a37d380df7c8", size = 511842, upload-time = "2025-12-01T17:29:43.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/a7/c326f5163dd48b79368b87d8a05f5da4668dd228a3f5ca9d79d5fee2fc40/uuid_utils-0.12.0-cp39-abi3-win32.whl", hash = "sha256:487f17c0fee6cbc1d8b90fe811874174a9b1b5683bf2251549e302906a50fed3", size = 179088, upload-time = "2025-12-01T17:29:44.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/92/41c8734dd97213ee1d5ae435cf4499705dc4f2751e3b957fd12376f61784/uuid_utils-0.12.0-cp39-abi3-win_amd64.whl", hash = "sha256:9598e7c9da40357ae8fffc5d6938b1a7017f09a1acbcc95e14af8c65d48c655a", size = 183003, upload-time = "2025-12-01T17:29:45.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/f9/52ab0359618987331a1f739af837d26168a4b16281c9c3ab46519940c628/uuid_utils-0.12.0-cp39-abi3-win_arm64.whl", hash = "sha256:c9bea7c5b2aa6f57937ebebeee4d4ef2baad10f86f1b97b58a3f6f34c14b4e84", size = 182975, upload-time = "2025-12-01T17:29:46.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/f7/6c55b7722cede3b424df02ed5cddb25c19543abda2f95fa4cfc34a892ae5/uuid_utils-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e2209d361f2996966ab7114f49919eb6aaeabc6041672abbbbf4fdbb8ec1acc0", size = 593065, upload-time = "2025-12-01T17:29:47.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/40/ce5fe8e9137dbd5570e0016c2584fca43ad81b11a1cef809a1a1b4952ab7/uuid_utils-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d9636bcdbd6cfcad2b549c352b669412d0d1eb09be72044a2f13e498974863cd", size = 300047, upload-time = "2025-12-01T17:29:48.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/9b/31c5d0736d7b118f302c50214e581f40e904305d8872eb0f0c921d50e138/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd8543a3419251fb78e703ce3b15fdfafe1b7c542cf40caf0775e01db7e7674", size = 335165, upload-time = "2025-12-01T17:29:49.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/5c/d80b4d08691c9d7446d0ad58fd41503081a662cfd2c7640faf68c64d8098/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e98db2d8977c052cb307ae1cb5cc37a21715e8d415dbc65863b039397495a013", size = 341437, upload-time = "2025-12-01T17:29:51.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b3/9dccdc6f3c22f6ef5bd381ae559173f8a1ae185ae89ed1f39f499d9d8b02/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8f2bdf5e4ffeb259ef6d15edae92aed60a1d6f07cbfab465d836f6b12b48da8", size = 469123, upload-time = "2025-12-01T17:29:52.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/90/6c35ef65fbc49f8189729839b793a4a74a7dd8c5aa5eb56caa93f8c97732/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c3ec53c0cb15e1835870c139317cc5ec06e35aa22843e3ed7d9c74f23f23898", size = 335892, upload-time = "2025-12-01T17:29:53.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/c7/e3f3ce05c5af2bf86a0938d22165affe635f4dcbfd5687b1dacc042d3e0e/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:84e5c0eba209356f7f389946a3a47b2cc2effd711b3fc7c7f155ad9f7d45e8a3", size = 360693, upload-time = "2025-12-01T17:29:54.558Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: all format lint test tests test_watch integration_tests help extended_tests
|
||||
.PHONY: all format lint test tests test_watch integration_tests help extended_tests check_version
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
all: help
|
||||
@@ -31,6 +31,9 @@ test_profile:
|
||||
check_imports: $(shell find langchain_core -name '*.py')
|
||||
uv run --group test python ./scripts/check_imports.py $^
|
||||
|
||||
check_version:
|
||||
uv run python ./scripts/check_version.py
|
||||
|
||||
extended_tests:
|
||||
uv run --group test pytest --only-extended --disable-socket --allow-unix-socket $(TEST_FILE)
|
||||
|
||||
@@ -69,6 +72,7 @@ help:
|
||||
@echo '----'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'check_version - validate version consistency'
|
||||
@echo 'test - run unit tests'
|
||||
@echo 'tests - run unit tests'
|
||||
@echo 'test TEST_FILE=<test_file> - run all tests in file'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://pypi.org/project/langchain-core/#history)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://pypistats.org/packages/langchain-core)
|
||||
[](https://twitter.com/langchainai)
|
||||
[](https://x.com/langchain)
|
||||
|
||||
Looking for the JS/TS version? Check out [LangChain.js](https://github.com/langchain-ai/langchainjs).
|
||||
|
||||
|
||||
@@ -13,20 +13,20 @@ from typing import TYPE_CHECKING
|
||||
from langchain_core._import_utils import import_attr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .beta_decorator import (
|
||||
from langchain_core._api.beta_decorator import (
|
||||
LangChainBetaWarning,
|
||||
beta,
|
||||
suppress_langchain_beta_warning,
|
||||
surface_langchain_beta_warnings,
|
||||
)
|
||||
from .deprecation import (
|
||||
from langchain_core._api.deprecation import (
|
||||
LangChainDeprecationWarning,
|
||||
deprecated,
|
||||
suppress_langchain_deprecation_warning,
|
||||
surface_langchain_deprecation_warnings,
|
||||
warn_deprecated,
|
||||
)
|
||||
from .path import as_import_path, get_relative_path
|
||||
from langchain_core._api.path import as_import_path, get_relative_path
|
||||
|
||||
__all__ = (
|
||||
"LangChainBetaWarning",
|
||||
@@ -58,6 +58,20 @@ _dynamic_imports = {
|
||||
|
||||
|
||||
def __getattr__(attr_name: str) -> object:
|
||||
"""Dynamically import and return an attribute from a submodule.
|
||||
|
||||
This function enables lazy loading of API functions from submodules, reducing
|
||||
initial import time and circular dependency issues.
|
||||
|
||||
Args:
|
||||
attr_name: Name of the attribute to import.
|
||||
|
||||
Returns:
|
||||
The imported attribute object.
|
||||
|
||||
Raises:
|
||||
AttributeError: If the attribute is not a valid dynamic import.
|
||||
"""
|
||||
module_name = _dynamic_imports.get(attr_name)
|
||||
result = import_attr(attr_name, module_name, __spec__.parent)
|
||||
globals()[attr_name] = result
|
||||
@@ -65,4 +79,9 @@ def __getattr__(attr_name: str) -> object:
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
"""Return a list of available attributes for this module.
|
||||
|
||||
Returns:
|
||||
List of attribute names that can be imported from this module.
|
||||
"""
|
||||
return list(__all__)
|
||||
|
||||
@@ -125,7 +125,7 @@ def beta(
|
||||
_name = _name or obj.__qualname__
|
||||
old_doc = obj.__doc__
|
||||
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
|
||||
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
|
||||
"""Finalize the annotation of a class."""
|
||||
# Can't set new_doc on some extension objects.
|
||||
with contextlib.suppress(AttributeError):
|
||||
@@ -168,7 +168,7 @@ def beta(
|
||||
emit_warning()
|
||||
obj.fdel(instance)
|
||||
|
||||
def finalize(_wrapper: Callable[..., Any], new_doc: str) -> Any:
|
||||
def finalize(_: Callable[..., Any], new_doc: str, /) -> Any:
|
||||
"""Finalize the property."""
|
||||
return property(fget=_fget, fset=_fset, fdel=_fdel, doc=new_doc)
|
||||
|
||||
@@ -181,7 +181,7 @@ def beta(
|
||||
wrapped = obj
|
||||
old_doc = wrapped.__doc__
|
||||
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T:
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str, /) -> T:
|
||||
"""Wrap the wrapped function using the wrapper and update the docstring.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -28,6 +28,27 @@ from pydantic.v1.fields import FieldInfo as FieldInfoV1
|
||||
from langchain_core._api.internal import is_caller_internal
|
||||
|
||||
|
||||
def _build_deprecation_message(
|
||||
*,
|
||||
alternative: str = "",
|
||||
alternative_import: str = "",
|
||||
) -> str:
|
||||
"""Build a simple deprecation message for `__deprecated__` attribute.
|
||||
|
||||
Args:
|
||||
alternative: An alternative API name.
|
||||
alternative_import: A fully qualified import path for the alternative.
|
||||
|
||||
Returns:
|
||||
A deprecation message string for IDE/type checker display.
|
||||
"""
|
||||
if alternative_import:
|
||||
return f"Use {alternative_import} instead."
|
||||
if alternative:
|
||||
return f"Use {alternative} instead."
|
||||
return "Deprecated."
|
||||
|
||||
|
||||
class LangChainDeprecationWarning(DeprecationWarning):
|
||||
"""A class for issuing deprecation warnings for LangChain users."""
|
||||
|
||||
@@ -81,60 +102,57 @@ def deprecated(
|
||||
) -> Callable[[T], T]:
|
||||
"""Decorator to mark a function, a class, or a property as deprecated.
|
||||
|
||||
When deprecating a classmethod, a staticmethod, or a property, the
|
||||
`@deprecated` decorator should go *under* `@classmethod` and
|
||||
`@staticmethod` (i.e., `deprecated` should directly decorate the
|
||||
underlying callable), but *over* `@property`.
|
||||
When deprecating a classmethod, a staticmethod, or a property, the `@deprecated`
|
||||
decorator should go *under* `@classmethod` and `@staticmethod` (i.e., `deprecated`
|
||||
should directly decorate the underlying callable), but *over* `@property`.
|
||||
|
||||
When deprecating a class `C` intended to be used as a base class in a
|
||||
multiple inheritance hierarchy, `C` *must* define an `__init__` method
|
||||
(if `C` instead inherited its `__init__` from its own base class, then
|
||||
`@deprecated` would mess up `__init__` inheritance when installing its
|
||||
own (deprecation-emitting) `C.__init__`).
|
||||
When deprecating a class `C` intended to be used as a base class in a multiple
|
||||
inheritance hierarchy, `C` *must* define an `__init__` method (if `C` instead
|
||||
inherited its `__init__` from its own base class, then `@deprecated` would mess up
|
||||
`__init__` inheritance when installing its own (deprecation-emitting) `C.__init__`).
|
||||
|
||||
Parameters are the same as for `warn_deprecated`, except that *obj_type*
|
||||
defaults to 'class' if decorating a class, 'attribute' if decorating a
|
||||
property, and 'function' otherwise.
|
||||
Parameters are the same as for `warn_deprecated`, except that *obj_type* defaults to
|
||||
'class' if decorating a class, 'attribute' if decorating a property, and 'function'
|
||||
otherwise.
|
||||
|
||||
Args:
|
||||
since:
|
||||
The release at which this API became deprecated.
|
||||
message:
|
||||
Override the default deprecation message. The %(since)s,
|
||||
%(name)s, %(alternative)s, %(obj_type)s, %(addendum)s,
|
||||
and %(removal)s format specifiers will be replaced by the
|
||||
since: The release at which this API became deprecated.
|
||||
message: Override the default deprecation message.
|
||||
|
||||
The `%(since)s`, `%(name)s`, `%(alternative)s`, `%(obj_type)s`,
|
||||
`%(addendum)s`, and `%(removal)s` format specifiers will be replaced by the
|
||||
values of the respective arguments passed to this function.
|
||||
name:
|
||||
The name of the deprecated object.
|
||||
alternative:
|
||||
An alternative API that the user may use in place of the
|
||||
deprecated API. The deprecation warning will tell the user
|
||||
about this alternative if provided.
|
||||
alternative_import:
|
||||
An alternative import that the user may use instead.
|
||||
pending:
|
||||
If `True`, uses a `PendingDeprecationWarning` instead of a
|
||||
DeprecationWarning. Cannot be used together with removal.
|
||||
obj_type:
|
||||
The object type being deprecated.
|
||||
addendum:
|
||||
Additional text appended directly to the final message.
|
||||
removal:
|
||||
The expected removal version. With the default (an empty
|
||||
string), a removal version is automatically computed from
|
||||
since. Set to other Falsy values to not schedule a removal
|
||||
date. Cannot be used together with pending.
|
||||
package:
|
||||
The package of the deprecated object.
|
||||
name: The name of the deprecated object.
|
||||
alternative: An alternative API that the user may use in place of the deprecated
|
||||
API.
|
||||
|
||||
The deprecation warning will tell the user about this alternative if
|
||||
provided.
|
||||
alternative_import: An alternative import that the user may use instead.
|
||||
pending: If `True`, uses a `PendingDeprecationWarning` instead of a
|
||||
`DeprecationWarning`.
|
||||
|
||||
Cannot be used together with removal.
|
||||
obj_type: The object type being deprecated.
|
||||
addendum: Additional text appended directly to the final message.
|
||||
removal: The expected removal version.
|
||||
|
||||
With the default (an empty string), a removal version is automatically
|
||||
computed from since. Set to other Falsy values to not schedule a removal
|
||||
date.
|
||||
|
||||
Cannot be used together with pending.
|
||||
package: The package of the deprecated object.
|
||||
|
||||
Returns:
|
||||
A decorator to mark a function or class as deprecated.
|
||||
|
||||
```python
|
||||
@deprecated("1.4.0")
|
||||
def the_function_to_deprecate():
|
||||
pass
|
||||
```
|
||||
Example:
|
||||
```python
|
||||
@deprecated("1.4.0")
|
||||
def the_function_to_deprecate():
|
||||
pass
|
||||
```
|
||||
"""
|
||||
_validate_deprecation_params(
|
||||
removal, alternative, alternative_import, pending=pending
|
||||
@@ -204,7 +222,7 @@ def deprecated(
|
||||
_name = _name or obj.__qualname__
|
||||
old_doc = obj.__doc__
|
||||
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
|
||||
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
|
||||
"""Finalize the deprecation of a class."""
|
||||
# Can't set new_doc on some extension objects.
|
||||
with contextlib.suppress(AttributeError):
|
||||
@@ -223,6 +241,11 @@ def deprecated(
|
||||
obj.__init__ = functools.wraps(obj.__init__)( # type: ignore[misc]
|
||||
warn_if_direct_instance
|
||||
)
|
||||
# Set __deprecated__ for PEP 702 (IDE/type checker support)
|
||||
obj.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined]
|
||||
alternative=alternative,
|
||||
alternative_import=alternative_import,
|
||||
)
|
||||
return obj
|
||||
|
||||
elif isinstance(obj, FieldInfoV1):
|
||||
@@ -234,7 +257,7 @@ def deprecated(
|
||||
raise ValueError(msg)
|
||||
old_doc = obj.description
|
||||
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
|
||||
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
|
||||
return cast(
|
||||
"T",
|
||||
FieldInfoV1(
|
||||
@@ -255,7 +278,7 @@ def deprecated(
|
||||
raise ValueError(msg)
|
||||
old_doc = obj.description
|
||||
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
|
||||
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
|
||||
return cast(
|
||||
"T",
|
||||
FieldInfo(
|
||||
@@ -313,14 +336,17 @@ def deprecated(
|
||||
if _name == "<lambda>":
|
||||
_name = set_name
|
||||
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
|
||||
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
|
||||
"""Finalize the property."""
|
||||
return cast(
|
||||
"T",
|
||||
_DeprecatedProperty(
|
||||
fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc
|
||||
),
|
||||
prop = _DeprecatedProperty(
|
||||
fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc
|
||||
)
|
||||
# Set __deprecated__ for PEP 702 (IDE/type checker support)
|
||||
prop.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined]
|
||||
alternative=alternative,
|
||||
alternative_import=alternative_import,
|
||||
)
|
||||
return cast("T", prop)
|
||||
|
||||
else:
|
||||
_name = _name or cast("type | Callable", obj).__qualname__
|
||||
@@ -331,7 +357,7 @@ def deprecated(
|
||||
wrapped = obj
|
||||
old_doc = wrapped.__doc__
|
||||
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T:
|
||||
def finalize(wrapper: Callable[..., Any], new_doc: str, /) -> T:
|
||||
"""Wrap the wrapped function using the wrapper and update the docstring.
|
||||
|
||||
Args:
|
||||
@@ -343,6 +369,11 @@ def deprecated(
|
||||
"""
|
||||
wrapper = functools.wraps(wrapped)(wrapper)
|
||||
wrapper.__doc__ = new_doc
|
||||
# Set __deprecated__ for PEP 702 (IDE/type checker support)
|
||||
wrapper.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined]
|
||||
alternative=alternative,
|
||||
alternative_import=alternative_import,
|
||||
)
|
||||
return cast("T", wrapper)
|
||||
|
||||
old_doc = inspect.cleandoc(old_doc or "").strip("\n")
|
||||
@@ -398,7 +429,7 @@ def deprecated(
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_langchain_deprecation_warning() -> Generator[None, None, None]:
|
||||
"""Context manager to suppress LangChainDeprecationWarning."""
|
||||
"""Context manager to suppress `LangChainDeprecationWarning`."""
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", LangChainDeprecationWarning)
|
||||
warnings.simplefilter("ignore", LangChainPendingDeprecationWarning)
|
||||
@@ -421,35 +452,33 @@ def warn_deprecated(
|
||||
"""Display a standardized deprecation.
|
||||
|
||||
Args:
|
||||
since:
|
||||
The release at which this API became deprecated.
|
||||
message:
|
||||
Override the default deprecation message. The %(since)s,
|
||||
%(name)s, %(alternative)s, %(obj_type)s, %(addendum)s,
|
||||
and %(removal)s format specifiers will be replaced by the
|
||||
since: The release at which this API became deprecated.
|
||||
message: Override the default deprecation message.
|
||||
|
||||
The `%(since)s`, `%(name)s`, `%(alternative)s`, `%(obj_type)s`,
|
||||
`%(addendum)s`, and `%(removal)s` format specifiers will be replaced by the
|
||||
values of the respective arguments passed to this function.
|
||||
name:
|
||||
The name of the deprecated object.
|
||||
alternative:
|
||||
An alternative API that the user may use in place of the
|
||||
deprecated API. The deprecation warning will tell the user
|
||||
about this alternative if provided.
|
||||
alternative_import:
|
||||
An alternative import that the user may use instead.
|
||||
pending:
|
||||
If `True`, uses a `PendingDeprecationWarning` instead of a
|
||||
DeprecationWarning. Cannot be used together with removal.
|
||||
obj_type:
|
||||
The object type being deprecated.
|
||||
addendum:
|
||||
Additional text appended directly to the final message.
|
||||
removal:
|
||||
The expected removal version. With the default (an empty
|
||||
string), a removal version is automatically computed from
|
||||
since. Set to other Falsy values to not schedule a removal
|
||||
date. Cannot be used together with pending.
|
||||
package:
|
||||
The package of the deprecated object.
|
||||
name: The name of the deprecated object.
|
||||
alternative: An alternative API that the user may use in place of the
|
||||
deprecated API.
|
||||
|
||||
The deprecation warning will tell the user about this alternative if
|
||||
provided.
|
||||
alternative_import: An alternative import that the user may use instead.
|
||||
pending: If `True`, uses a `PendingDeprecationWarning` instead of a
|
||||
`DeprecationWarning`.
|
||||
|
||||
Cannot be used together with removal.
|
||||
obj_type: The object type being deprecated.
|
||||
addendum: Additional text appended directly to the final message.
|
||||
removal: The expected removal version.
|
||||
|
||||
With the default (an empty string), a removal version is automatically
|
||||
computed from since. Set to other Falsy values to not schedule a removal
|
||||
date.
|
||||
|
||||
Cannot be used together with pending.
|
||||
package: The package of the deprecated object.
|
||||
"""
|
||||
if not pending:
|
||||
if not removal:
|
||||
@@ -534,8 +563,8 @@ def rename_parameter(
|
||||
"""Decorator indicating that parameter *old* of *func* is renamed to *new*.
|
||||
|
||||
The actual implementation of *func* should use *new*, not *old*. If *old* is passed
|
||||
to *func*, a DeprecationWarning is emitted, and its value is used, even if *new* is
|
||||
also passed by keyword.
|
||||
to *func*, a `DeprecationWarning` is emitted, and its value is used, even if *new*
|
||||
is also passed by keyword.
|
||||
|
||||
Args:
|
||||
since: The version in which the parameter was renamed.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import inspect
|
||||
from typing import cast
|
||||
|
||||
|
||||
def is_caller_internal(depth: int = 2) -> bool:
|
||||
@@ -16,7 +17,7 @@ def is_caller_internal(depth: int = 2) -> bool:
|
||||
return False
|
||||
# Directly access the module name from the frame's global variables
|
||||
module_globals = frame.f_globals
|
||||
caller_module_name = module_globals.get("__name__", "")
|
||||
caller_module_name = cast("str", module_globals.get("__name__", ""))
|
||||
return caller_module_name.startswith("langchain")
|
||||
finally:
|
||||
del frame
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Distinct from provider-based [prompt caching](https://docs.langchain.com/oss/python/langchain/models#prompt-caching).
|
||||
|
||||
!!! warning "Beta feature"
|
||||
|
||||
This is a beta feature. Please be wary of deploying experimental code to production
|
||||
unless you've taken appropriate precautions.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RetrieverManagerMixin:
|
||||
"""Mixin for Retriever callbacks."""
|
||||
"""Mixin for `Retriever` callbacks."""
|
||||
|
||||
def on_retriever_error(
|
||||
self,
|
||||
@@ -31,12 +31,12 @@ class RetrieverManagerMixin:
|
||||
parent_run_id: UUID | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run when Retriever errors.
|
||||
"""Run when `Retriever` errors.
|
||||
|
||||
Args:
|
||||
error: The error that occurred.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -48,12 +48,12 @@ class RetrieverManagerMixin:
|
||||
parent_run_id: UUID | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run when Retriever ends running.
|
||||
"""Run when `Retriever` ends running.
|
||||
|
||||
Args:
|
||||
documents: The documents retrieved.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -68,6 +68,7 @@ class LLMManagerMixin:
|
||||
chunk: GenerationChunk | ChatGenerationChunk | None = None,
|
||||
run_id: UUID,
|
||||
parent_run_id: UUID | None = None,
|
||||
tags: list[str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run on new output token. Only available when streaming is enabled.
|
||||
@@ -77,8 +78,9 @@ class LLMManagerMixin:
|
||||
Args:
|
||||
token: The new token.
|
||||
chunk: The new generated chunk, containing content and other information.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -88,14 +90,16 @@ class LLMManagerMixin:
|
||||
*,
|
||||
run_id: UUID,
|
||||
parent_run_id: UUID | None = None,
|
||||
tags: list[str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run when LLM ends running.
|
||||
|
||||
Args:
|
||||
response: The response which was generated.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -105,14 +109,16 @@ class LLMManagerMixin:
|
||||
*,
|
||||
run_id: UUID,
|
||||
parent_run_id: UUID | None = None,
|
||||
tags: list[str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run when LLM errors.
|
||||
|
||||
Args:
|
||||
error: The error that occurred.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -132,8 +138,8 @@ class ChainManagerMixin:
|
||||
|
||||
Args:
|
||||
outputs: The outputs of the chain.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -149,8 +155,8 @@ class ChainManagerMixin:
|
||||
|
||||
Args:
|
||||
error: The error that occurred.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -166,8 +172,8 @@ class ChainManagerMixin:
|
||||
|
||||
Args:
|
||||
action: The agent action.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -183,8 +189,8 @@ class ChainManagerMixin:
|
||||
|
||||
Args:
|
||||
finish: The agent finish.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -204,8 +210,8 @@ class ToolManagerMixin:
|
||||
|
||||
Args:
|
||||
output: The output of the tool.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -221,8 +227,8 @@ class ToolManagerMixin:
|
||||
|
||||
Args:
|
||||
error: The error that occurred.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -251,8 +257,8 @@ class CallbackManagerMixin:
|
||||
Args:
|
||||
serialized: The serialized LLM.
|
||||
prompts: The prompts.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
**kwargs: Additional keyword arguments.
|
||||
@@ -278,8 +284,8 @@ class CallbackManagerMixin:
|
||||
Args:
|
||||
serialized: The serialized chat model.
|
||||
messages: The messages.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
**kwargs: Additional keyword arguments.
|
||||
@@ -300,13 +306,13 @@ class CallbackManagerMixin:
|
||||
metadata: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run when the Retriever starts running.
|
||||
"""Run when the `Retriever` starts running.
|
||||
|
||||
Args:
|
||||
serialized: The serialized Retriever.
|
||||
serialized: The serialized `Retriever`.
|
||||
query: The query.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
**kwargs: Additional keyword arguments.
|
||||
@@ -328,8 +334,8 @@ class CallbackManagerMixin:
|
||||
Args:
|
||||
serialized: The serialized chain.
|
||||
inputs: The inputs.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
**kwargs: Additional keyword arguments.
|
||||
@@ -352,8 +358,8 @@ class CallbackManagerMixin:
|
||||
Args:
|
||||
serialized: The serialized chain.
|
||||
input_str: The input string.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
inputs: The inputs.
|
||||
@@ -376,8 +382,8 @@ class RunManagerMixin:
|
||||
|
||||
Args:
|
||||
text: The text.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -393,8 +399,8 @@ class RunManagerMixin:
|
||||
|
||||
Args:
|
||||
retry_state: The retry state.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -412,13 +418,12 @@ class RunManagerMixin:
|
||||
|
||||
Args:
|
||||
name: The name of the custom event.
|
||||
data: The data for the custom event. Format will match
|
||||
the format specified by the user.
|
||||
data: The data for the custom event. Format will match the format specified
|
||||
by the user.
|
||||
run_id: The ID of the run.
|
||||
tags: The tags associated with the custom event
|
||||
(includes inherited tags).
|
||||
metadata: The metadata associated with the custom event
|
||||
(includes inherited metadata).
|
||||
tags: The tags associated with the custom event (includes inherited tags).
|
||||
metadata: The metadata associated with the custom event (includes inherited
|
||||
metadata).
|
||||
"""
|
||||
|
||||
|
||||
@@ -430,7 +435,7 @@ class BaseCallbackHandler(
|
||||
CallbackManagerMixin,
|
||||
RunManagerMixin,
|
||||
):
|
||||
"""Base callback handler for LangChain."""
|
||||
"""Base callback handler."""
|
||||
|
||||
raise_error: bool = False
|
||||
"""Whether to raise an error if an exception occurs."""
|
||||
@@ -475,7 +480,7 @@ class BaseCallbackHandler(
|
||||
|
||||
|
||||
class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
"""Async callback handler for LangChain."""
|
||||
"""Base async callback handler."""
|
||||
|
||||
async def on_llm_start(
|
||||
self,
|
||||
@@ -498,8 +503,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
Args:
|
||||
serialized: The serialized LLM.
|
||||
prompts: The prompts.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
**kwargs: Additional keyword arguments.
|
||||
@@ -525,8 +530,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
Args:
|
||||
serialized: The serialized chat model.
|
||||
messages: The messages.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
**kwargs: Additional keyword arguments.
|
||||
@@ -553,8 +558,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
Args:
|
||||
token: The new token.
|
||||
chunk: The new generated chunk, containing content and other information.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -572,8 +577,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
response: The response which was generated.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -591,10 +596,11 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
error: The error that occurred.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
- response (LLMResult): The response which was generated before
|
||||
the error occurred.
|
||||
"""
|
||||
@@ -615,8 +621,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
Args:
|
||||
serialized: The serialized chain.
|
||||
inputs: The inputs.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
**kwargs: Additional keyword arguments.
|
||||
@@ -635,8 +641,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
outputs: The outputs of the chain.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -654,8 +660,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
error: The error that occurred.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -677,8 +683,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
Args:
|
||||
serialized: The serialized tool.
|
||||
input_str: The input string.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
inputs: The inputs.
|
||||
@@ -698,8 +704,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
output: The output of the tool.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -717,8 +723,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
error: The error that occurred.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -736,8 +742,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
text: The text.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -754,8 +760,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
retry_state: The retry state.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@@ -772,8 +778,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
action: The agent action.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -791,8 +797,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
finish: The agent finish.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -813,8 +819,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
Args:
|
||||
serialized: The serialized retriever.
|
||||
query: The query.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
metadata: The metadata.
|
||||
**kwargs: Additional keyword arguments.
|
||||
@@ -833,8 +839,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
documents: The documents retrieved.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -852,8 +858,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
Args:
|
||||
error: The error that occurred.
|
||||
run_id: The run ID. This is the ID of the current run.
|
||||
parent_run_id: The parent run ID. This is the ID of the parent run.
|
||||
run_id: The ID of the current run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
tags: The tags.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
@@ -883,7 +889,7 @@ class AsyncCallbackHandler(BaseCallbackHandler):
|
||||
|
||||
|
||||
class BaseCallbackManager(CallbackManagerMixin):
|
||||
"""Base callback manager for LangChain."""
|
||||
"""Base callback manager."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -932,8 +938,9 @@ class BaseCallbackManager(CallbackManagerMixin):
|
||||
def merge(self, other: BaseCallbackManager) -> Self:
|
||||
"""Merge the callback manager with another callback manager.
|
||||
|
||||
May be overwritten in subclasses. Primarily used internally
|
||||
within merge_configs.
|
||||
May be overwritten in subclasses.
|
||||
|
||||
Primarily used internally within `merge_configs`.
|
||||
|
||||
Returns:
|
||||
The merged callback manager of the same type as the current object.
|
||||
@@ -960,28 +967,29 @@ class BaseCallbackManager(CallbackManagerMixin):
|
||||
# ['tag2', 'tag1']
|
||||
```
|
||||
""" # noqa: E501
|
||||
manager = self.__class__(
|
||||
# Combine handlers and inheritable_handlers separately, using sets
|
||||
# to deduplicate (order not preserved)
|
||||
combined_handlers = list(set(self.handlers) | set(other.handlers))
|
||||
combined_inheritable = list(
|
||||
set(self.inheritable_handlers) | set(other.inheritable_handlers)
|
||||
)
|
||||
|
||||
return self.__class__(
|
||||
parent_run_id=self.parent_run_id or other.parent_run_id,
|
||||
handlers=[],
|
||||
inheritable_handlers=[],
|
||||
handlers=combined_handlers,
|
||||
inheritable_handlers=combined_inheritable,
|
||||
tags=list(set(self.tags + other.tags)),
|
||||
inheritable_tags=list(set(self.inheritable_tags + other.inheritable_tags)),
|
||||
metadata={
|
||||
**self.metadata,
|
||||
**other.metadata,
|
||||
},
|
||||
inheritable_metadata={
|
||||
**self.inheritable_metadata,
|
||||
**other.inheritable_metadata,
|
||||
},
|
||||
)
|
||||
|
||||
handlers = self.handlers + other.handlers
|
||||
inheritable_handlers = self.inheritable_handlers + other.inheritable_handlers
|
||||
|
||||
for handler in handlers:
|
||||
manager.add_handler(handler)
|
||||
|
||||
for handler in inheritable_handlers:
|
||||
manager.add_handler(handler, inherit=True)
|
||||
return manager
|
||||
|
||||
@property
|
||||
def is_async(self) -> bool:
|
||||
"""Whether the callback manager is async."""
|
||||
|
||||
@@ -12,7 +12,6 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from contextvars import copy_context
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||
from uuid import UUID
|
||||
|
||||
from langsmith.run_helpers import get_tracing_context
|
||||
from typing_extensions import Self, override
|
||||
@@ -44,6 +43,7 @@ from langchain_core.utils.uuid import uuid7
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncGenerator, Coroutine, Generator, Sequence
|
||||
from uuid import UUID
|
||||
|
||||
from tenacity import RetryCallState
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing_extensions import override
|
||||
|
||||
from langchain_core.document_loaders.base import BaseLoader
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.tracers._compat import pydantic_to_dict
|
||||
|
||||
|
||||
class LangSmithLoader(BaseLoader):
|
||||
@@ -118,14 +119,14 @@ class LangSmithLoader(BaseLoader):
|
||||
for key in self.content_key:
|
||||
content = content[key]
|
||||
content_str = self.format_content(content)
|
||||
metadata = example.dict()
|
||||
metadata = pydantic_to_dict(example)
|
||||
# Stringify datetime and UUID types.
|
||||
for k in ("dataset_id", "created_at", "modified_at", "source_run_id", "id"):
|
||||
metadata[k] = str(metadata[k]) if metadata[k] else metadata[k]
|
||||
yield Document(content_str, metadata=metadata)
|
||||
|
||||
|
||||
def _stringify(x: str | dict) -> str:
|
||||
def _stringify(x: str | dict[str, Any]) -> str:
|
||||
if isinstance(x, str):
|
||||
return x
|
||||
try:
|
||||
|
||||
@@ -30,9 +30,9 @@ from typing import TYPE_CHECKING
|
||||
from langchain_core._import_utils import import_attr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import Document
|
||||
from .compressor import BaseDocumentCompressor
|
||||
from .transformers import BaseDocumentTransformer
|
||||
from langchain_core.documents.base import Document
|
||||
from langchain_core.documents.compressor import BaseDocumentCompressor
|
||||
from langchain_core.documents.transformers import BaseDocumentTransformer
|
||||
|
||||
__all__ = ("BaseDocumentCompressor", "BaseDocumentTransformer", "Document")
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from langchain_core.prompts.prompt import PromptTemplate
|
||||
|
||||
|
||||
def _get_length_based(text: str) -> int:
|
||||
return len(re.split("\n| ", text))
|
||||
return len(re.split(r"\n| ", text))
|
||||
|
||||
|
||||
class LengthBasedExampleSelector(BaseExampleSelector, BaseModel):
|
||||
|
||||
@@ -242,6 +242,17 @@ def _delete(
|
||||
vector_store: VectorStore | DocumentIndex,
|
||||
ids: list[str],
|
||||
) -> None:
|
||||
"""Delete documents from a vector store or document index by their IDs.
|
||||
|
||||
Args:
|
||||
vector_store: The vector store or document index to delete from.
|
||||
ids: List of document IDs to delete.
|
||||
|
||||
Raises:
|
||||
IndexingException: If the delete operation fails.
|
||||
TypeError: If the `vector_store` is neither a `VectorStore` nor a
|
||||
`DocumentIndex`.
|
||||
"""
|
||||
if isinstance(vector_store, VectorStore):
|
||||
delete_ok = vector_store.delete(ids)
|
||||
if delete_ok is not None and delete_ok is False:
|
||||
|
||||
@@ -12,13 +12,14 @@ from typing import (
|
||||
Literal,
|
||||
TypeAlias,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from typing_extensions import TypedDict, override
|
||||
|
||||
from langchain_core.caches import BaseCache
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.caches import BaseCache # noqa: TC001
|
||||
from langchain_core.callbacks import Callbacks # noqa: TC001
|
||||
from langchain_core.globals import get_verbose
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
@@ -86,13 +87,28 @@ def get_tokenizer() -> Any:
|
||||
return GPT2TokenizerFast.from_pretrained("gpt2")
|
||||
|
||||
|
||||
_GPT2_TOKENIZER_WARNED = False
|
||||
|
||||
|
||||
def _get_token_ids_default_method(text: str) -> list[int]:
|
||||
"""Encode the text into token IDs."""
|
||||
# get the cached tokenizer
|
||||
"""Encode the text into token IDs using the fallback GPT-2 tokenizer."""
|
||||
global _GPT2_TOKENIZER_WARNED # noqa: PLW0603
|
||||
if not _GPT2_TOKENIZER_WARNED:
|
||||
warnings.warn(
|
||||
"Using fallback GPT-2 tokenizer for token counting. "
|
||||
"Token counts may be inaccurate for non-GPT-2 models. "
|
||||
"For accurate counts, use a model-specific method if available.",
|
||||
stacklevel=3,
|
||||
)
|
||||
_GPT2_TOKENIZER_WARNED = True
|
||||
|
||||
tokenizer = get_tokenizer()
|
||||
|
||||
# tokenize the text using the GPT-2 tokenizer
|
||||
return tokenizer.encode(text)
|
||||
# Pass verbose=False to suppress the "Token indices sequence length is longer than
|
||||
# the specified maximum sequence length" warning from HuggingFace. This warning is
|
||||
# about GPT-2's 1024 token context limit, but we're only using the tokenizer for
|
||||
# counting, not for model input.
|
||||
return cast("list[int]", tokenizer.encode(text, verbose=False))
|
||||
|
||||
|
||||
LanguageModelInput = PromptValue | str | Sequence[MessageLikeRepresentation]
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
import typing
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
|
||||
from functools import cached_property
|
||||
@@ -74,6 +73,7 @@ from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass
|
||||
from langchain_core.utils.utils import LC_ID_PREFIX, from_env
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import builtins
|
||||
import uuid
|
||||
|
||||
from langchain_core.output_parsers.base import OutputParserLike
|
||||
@@ -341,6 +341,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
"""Profile detailing model capabilities.
|
||||
|
||||
!!! warning "Beta feature"
|
||||
|
||||
This is a beta feature. The format of model profiles is subject to change.
|
||||
|
||||
If not specified, automatically loaded from the provider package on initialization
|
||||
@@ -358,7 +359,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
|
||||
@cached_property
|
||||
def _serialized(self) -> dict[str, Any]:
|
||||
return dumpd(self)
|
||||
# self is always a Serializable object in this case, thus the result is
|
||||
# guaranteed to be a dict since dumps uses the default callback, which uses
|
||||
# obj.to_json which always returns TypedDict subclasses
|
||||
return cast("dict[str, Any]", dumpd(self))
|
||||
|
||||
# --- Runnable methods ---
|
||||
|
||||
@@ -461,7 +465,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
|
||||
# Check if a runtime streaming flag has been passed in.
|
||||
if "stream" in kwargs:
|
||||
return kwargs["stream"]
|
||||
return bool(kwargs["stream"])
|
||||
|
||||
if "streaming" in self.model_fields_set:
|
||||
streaming_value = getattr(self, "streaming", None)
|
||||
@@ -547,7 +551,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
):
|
||||
if block["type"] != index_type:
|
||||
index_type = block["type"]
|
||||
index = index + 1
|
||||
index += 1
|
||||
if "index" not in block:
|
||||
block["index"] = index
|
||||
run_manager.on_llm_new_token(
|
||||
@@ -679,7 +683,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
):
|
||||
if block["type"] != index_type:
|
||||
index_type = block["type"]
|
||||
index = index + 1
|
||||
index += 1
|
||||
if "index" not in block:
|
||||
block["index"] = index
|
||||
await run_manager.on_llm_new_token(
|
||||
@@ -730,7 +734,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
|
||||
# --- Custom methods ---
|
||||
|
||||
def _combine_llm_outputs(self, llm_outputs: list[dict | None]) -> dict: # noqa: ARG002
|
||||
def _combine_llm_outputs(self, _llm_outputs: list[dict | None], /) -> dict:
|
||||
return {}
|
||||
|
||||
def _convert_cached_generations(self, cache_val: list) -> list[ChatGeneration]:
|
||||
@@ -1144,7 +1148,15 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
if check_cache:
|
||||
if llm_cache:
|
||||
llm_string = self._get_llm_string(stop=stop, **kwargs)
|
||||
prompt = dumps(messages)
|
||||
normalized_messages = [
|
||||
(
|
||||
msg.model_copy(update={"id": None})
|
||||
if getattr(msg, "id", None) is not None
|
||||
else msg
|
||||
)
|
||||
for msg in messages
|
||||
]
|
||||
prompt = dumps(normalized_messages)
|
||||
cache_val = llm_cache.lookup(prompt, llm_string)
|
||||
if isinstance(cache_val, list):
|
||||
converted_generations = self._convert_cached_generations(cache_val)
|
||||
@@ -1187,7 +1199,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
):
|
||||
if block["type"] != index_type:
|
||||
index_type = block["type"]
|
||||
index = index + 1
|
||||
index += 1
|
||||
if "index" not in block:
|
||||
block["index"] = index
|
||||
if run_manager:
|
||||
@@ -1262,7 +1274,15 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
if check_cache:
|
||||
if llm_cache:
|
||||
llm_string = self._get_llm_string(stop=stop, **kwargs)
|
||||
prompt = dumps(messages)
|
||||
normalized_messages = [
|
||||
(
|
||||
msg.model_copy(update={"id": None})
|
||||
if getattr(msg, "id", None) is not None
|
||||
else msg
|
||||
)
|
||||
for msg in messages
|
||||
]
|
||||
prompt = dumps(normalized_messages)
|
||||
cache_val = await llm_cache.alookup(prompt, llm_string)
|
||||
if isinstance(cache_val, list):
|
||||
converted_generations = self._convert_cached_generations(cache_val)
|
||||
@@ -1305,7 +1325,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
):
|
||||
if block["type"] != index_type:
|
||||
index_type = block["type"]
|
||||
index = index + 1
|
||||
index += 1
|
||||
if "index" not in block:
|
||||
block["index"] = index
|
||||
if run_manager:
|
||||
@@ -1500,9 +1520,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
|
||||
def bind_tools(
|
||||
self,
|
||||
tools: Sequence[
|
||||
typing.Dict[str, Any] | type | Callable | BaseTool # noqa: UP006
|
||||
],
|
||||
tools: Sequence[builtins.dict[str, Any] | type | Callable | BaseTool],
|
||||
*,
|
||||
tool_choice: str | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -1521,11 +1539,11 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
|
||||
def with_structured_output(
|
||||
self,
|
||||
schema: typing.Dict | type, # noqa: UP006
|
||||
schema: builtins.dict[str, Any] | type,
|
||||
*,
|
||||
include_raw: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, typing.Dict | BaseModel]: # noqa: UP006
|
||||
) -> Runnable[LanguageModelInput, builtins.dict[str, Any] | BaseModel]:
|
||||
"""Model wrapper that returns outputs formatted to match the given schema.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -301,7 +301,10 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
|
||||
@functools.cached_property
|
||||
def _serialized(self) -> dict[str, Any]:
|
||||
return dumpd(self)
|
||||
# self is always a Serializable object in this case, thus the result is
|
||||
# guaranteed to be a dict since dumps uses the default callback, which uses
|
||||
# obj.to_json which always returns TypedDict subclasses
|
||||
return cast("dict[str, Any]", dumpd(self))
|
||||
|
||||
# --- Runnable methods ---
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ class ModelProfile(TypedDict, total=False):
|
||||
"""Model profile.
|
||||
|
||||
!!! warning "Beta feature"
|
||||
|
||||
This is a beta feature. The format of model profiles is subject to change.
|
||||
|
||||
Provides information about chat model capabilities, such as context window sizes
|
||||
|
||||
@@ -6,7 +6,7 @@ from langchain_core._import_utils import import_attr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.load.dump import dumpd, dumps
|
||||
from langchain_core.load.load import loads
|
||||
from langchain_core.load.load import InitValidator, loads
|
||||
from langchain_core.load.serializable import Serializable
|
||||
|
||||
# Unfortunately, we have to eagerly import load from langchain_core/load/load.py
|
||||
@@ -15,11 +15,19 @@ if TYPE_CHECKING:
|
||||
# the `from langchain_core.load.load import load` absolute import should also work.
|
||||
from langchain_core.load.load import load
|
||||
|
||||
__all__ = ("Serializable", "dumpd", "dumps", "load", "loads")
|
||||
__all__ = (
|
||||
"InitValidator",
|
||||
"Serializable",
|
||||
"dumpd",
|
||||
"dumps",
|
||||
"load",
|
||||
"loads",
|
||||
)
|
||||
|
||||
_dynamic_imports = {
|
||||
"dumpd": "dump",
|
||||
"dumps": "dump",
|
||||
"InitValidator": "load",
|
||||
"loads": "load",
|
||||
"Serializable": "serializable",
|
||||
}
|
||||
|
||||
174
libs/core/langchain_core/load/_validation.py
Normal file
174
libs/core/langchain_core/load/_validation.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""Validation utilities for LangChain serialization.
|
||||
|
||||
Provides escape-based protection against injection attacks in serialized objects. The
|
||||
approach uses an allowlist design: only dicts explicitly produced by
|
||||
`Serializable.to_json()` are treated as LC objects during deserialization.
|
||||
|
||||
## How escaping works
|
||||
|
||||
During serialization, plain dicts (user data) that contain an `'lc'` key are wrapped:
|
||||
|
||||
```python
|
||||
{"lc": 1, ...} # user data that looks like LC object
|
||||
# becomes:
|
||||
{"__lc_escaped__": {"lc": 1, ...}}
|
||||
```
|
||||
|
||||
During deserialization, escaped dicts are unwrapped and returned as plain dicts,
|
||||
NOT instantiated as LC objects.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.load.serializable import (
|
||||
Serializable,
|
||||
to_json_not_implemented,
|
||||
)
|
||||
|
||||
_LC_ESCAPED_KEY = "__lc_escaped__"
|
||||
"""Sentinel key used to mark escaped user dicts during serialization.
|
||||
|
||||
When a plain dict contains 'lc' key (which could be confused with LC objects),
|
||||
we wrap it as {"__lc_escaped__": {...original...}}.
|
||||
"""
|
||||
|
||||
|
||||
def _needs_escaping(obj: dict[str, Any]) -> bool:
|
||||
"""Check if a dict needs escaping to prevent confusion with LC objects.
|
||||
|
||||
A dict needs escaping if:
|
||||
|
||||
1. It has an `'lc'` key (could be confused with LC serialization format)
|
||||
2. It has only the escape key (would be mistaken for an escaped dict)
|
||||
"""
|
||||
return "lc" in obj or (len(obj) == 1 and _LC_ESCAPED_KEY in obj)
|
||||
|
||||
|
||||
def _escape_dict(obj: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Wrap a dict in the escape marker.
|
||||
|
||||
Example:
|
||||
```python
|
||||
{"key": "value"} # becomes {"__lc_escaped__": {"key": "value"}}
|
||||
```
|
||||
"""
|
||||
return {_LC_ESCAPED_KEY: obj}
|
||||
|
||||
|
||||
def _is_escaped_dict(obj: dict[str, Any]) -> bool:
|
||||
"""Check if a dict is an escaped user dict.
|
||||
|
||||
Example:
|
||||
```python
|
||||
{"__lc_escaped__": {...}} # is an escaped dict
|
||||
```
|
||||
"""
|
||||
return len(obj) == 1 and _LC_ESCAPED_KEY in obj
|
||||
|
||||
|
||||
def _serialize_value(obj: Any) -> Any:
|
||||
"""Serialize a value with escaping of user dicts.
|
||||
|
||||
Called recursively on kwarg values to escape any plain dicts that could be confused
|
||||
with LC objects.
|
||||
|
||||
Args:
|
||||
obj: The value to serialize.
|
||||
|
||||
Returns:
|
||||
The serialized value with user dicts escaped as needed.
|
||||
"""
|
||||
if isinstance(obj, Serializable):
|
||||
# This is an LC object - serialize it properly (not escaped)
|
||||
return _serialize_lc_object(obj)
|
||||
if isinstance(obj, dict):
|
||||
if not all(isinstance(k, (str, int, float, bool, type(None))) for k in obj):
|
||||
# if keys are not json serializable
|
||||
return to_json_not_implemented(obj)
|
||||
# Check if dict needs escaping BEFORE recursing into values.
|
||||
# If it needs escaping, wrap it as-is - the contents are user data that
|
||||
# will be returned as-is during deserialization (no instantiation).
|
||||
# This prevents re-escaping of already-escaped nested content.
|
||||
if _needs_escaping(obj):
|
||||
return _escape_dict(obj)
|
||||
# Safe dict (no 'lc' key) - recurse into values
|
||||
return {k: _serialize_value(v) for k, v in obj.items()}
|
||||
if isinstance(obj, (list, tuple)):
|
||||
return [_serialize_value(item) for item in obj]
|
||||
if isinstance(obj, (str, int, float, bool, type(None))):
|
||||
return obj
|
||||
|
||||
# Non-JSON-serializable object (datetime, custom objects, etc.)
|
||||
return to_json_not_implemented(obj)
|
||||
|
||||
|
||||
def _is_lc_secret(obj: Any) -> bool:
|
||||
"""Check if an object is a LangChain secret marker."""
|
||||
expected_num_keys = 3
|
||||
return (
|
||||
isinstance(obj, dict)
|
||||
and obj.get("lc") == 1
|
||||
and obj.get("type") == "secret"
|
||||
and "id" in obj
|
||||
and len(obj) == expected_num_keys
|
||||
)
|
||||
|
||||
|
||||
def _serialize_lc_object(obj: Any) -> dict[str, Any]:
|
||||
"""Serialize a `Serializable` object with escaping of user data in kwargs.
|
||||
|
||||
Args:
|
||||
obj: The `Serializable` object to serialize.
|
||||
|
||||
Returns:
|
||||
The serialized dict with user data in kwargs escaped as needed.
|
||||
|
||||
Note:
|
||||
Kwargs values are processed with `_serialize_value` to escape user data (like
|
||||
metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
|
||||
skipped because `to_json()` replaces their values with secret markers.
|
||||
"""
|
||||
if not isinstance(obj, Serializable):
|
||||
msg = f"Expected Serializable, got {type(obj)}"
|
||||
raise TypeError(msg)
|
||||
|
||||
serialized: dict[str, Any] = dict(obj.to_json())
|
||||
|
||||
# Process kwargs to escape user data that could be confused with LC objects
|
||||
# Skip secret fields - to_json() already converted them to secret markers
|
||||
if serialized.get("type") == "constructor" and "kwargs" in serialized:
|
||||
serialized["kwargs"] = {
|
||||
k: v if _is_lc_secret(v) else _serialize_value(v)
|
||||
for k, v in serialized["kwargs"].items()
|
||||
}
|
||||
|
||||
return serialized
|
||||
|
||||
|
||||
def _unescape_value(obj: Any) -> Any:
|
||||
"""Unescape a value, processing escape markers in dict values and lists.
|
||||
|
||||
When an escaped dict is encountered (`{"__lc_escaped__": ...}`), it's
|
||||
unwrapped and the contents are returned AS-IS (no further processing).
|
||||
The contents represent user data that should not be modified.
|
||||
|
||||
For regular dicts and lists, we recurse to find any nested escape markers.
|
||||
|
||||
Args:
|
||||
obj: The value to unescape.
|
||||
|
||||
Returns:
|
||||
The unescaped value.
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
if _is_escaped_dict(obj):
|
||||
# Unwrap and return the user data as-is (no further unescaping).
|
||||
# The contents are user data that may contain more escape keys,
|
||||
# but those are part of the user's actual data.
|
||||
return obj[_LC_ESCAPED_KEY]
|
||||
|
||||
# Regular dict - recurse into values to find nested escape markers
|
||||
return {k: _unescape_value(v) for k, v in obj.items()}
|
||||
if isinstance(obj, list):
|
||||
return [_unescape_value(item) for item in obj]
|
||||
return obj
|
||||
@@ -1,10 +1,26 @@
|
||||
"""Dump objects to json."""
|
||||
"""Serialize LangChain objects to JSON.
|
||||
|
||||
Provides `dumps` (to JSON string) and `dumpd` (to dict) for serializing
|
||||
`Serializable` objects.
|
||||
|
||||
## Escaping
|
||||
|
||||
During serialization, plain dicts (user data) that contain an `'lc'` key are escaped
|
||||
by wrapping them: `{"__lc_escaped__": {...original...}}`. This prevents injection
|
||||
attacks where malicious data could trick the deserializer into instantiating
|
||||
arbitrary classes. The escape marker is removed during deserialization.
|
||||
|
||||
This is an allowlist approach: only dicts explicitly produced by
|
||||
`Serializable.to_json()` are treated as LC objects; everything else is escaped if it
|
||||
could be confused with the LC format.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langchain_core.load._validation import _serialize_value
|
||||
from langchain_core.load.serializable import Serializable, to_json_not_implemented
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.outputs import ChatGeneration
|
||||
@@ -25,6 +41,20 @@ def default(obj: Any) -> Any:
|
||||
|
||||
|
||||
def _dump_pydantic_models(obj: Any) -> Any:
|
||||
"""Convert nested Pydantic models to dicts for JSON serialization.
|
||||
|
||||
Handles the special case where a `ChatGeneration` contains an `AIMessage`
|
||||
with a parsed Pydantic model in `additional_kwargs["parsed"]`. Since
|
||||
Pydantic models aren't directly JSON serializable, this converts them to
|
||||
dicts.
|
||||
|
||||
Args:
|
||||
obj: The object to process.
|
||||
|
||||
Returns:
|
||||
A copy of the object with nested Pydantic models converted to dicts, or
|
||||
the original object unchanged if no conversion was needed.
|
||||
"""
|
||||
if (
|
||||
isinstance(obj, ChatGeneration)
|
||||
and isinstance(obj.message, AIMessage)
|
||||
@@ -40,10 +70,17 @@ def _dump_pydantic_models(obj: Any) -> Any:
|
||||
def dumps(obj: Any, *, pretty: bool = False, **kwargs: Any) -> str:
|
||||
"""Return a JSON string representation of an object.
|
||||
|
||||
Note:
|
||||
Plain dicts containing an `'lc'` key are automatically escaped to prevent
|
||||
confusion with LC serialization format. The escape marker is removed during
|
||||
deserialization.
|
||||
|
||||
Args:
|
||||
obj: The object to dump.
|
||||
pretty: Whether to pretty print the json. If `True`, the json will be
|
||||
indented with 2 spaces (if no indent is provided as part of `kwargs`).
|
||||
pretty: Whether to pretty print the json.
|
||||
|
||||
If `True`, the json will be indented by either 2 spaces or the amount
|
||||
provided in the `indent` kwarg.
|
||||
**kwargs: Additional arguments to pass to `json.dumps`
|
||||
|
||||
Returns:
|
||||
@@ -55,28 +92,29 @@ def dumps(obj: Any, *, pretty: bool = False, **kwargs: Any) -> str:
|
||||
if "default" in kwargs:
|
||||
msg = "`default` should not be passed to dumps"
|
||||
raise ValueError(msg)
|
||||
try:
|
||||
obj = _dump_pydantic_models(obj)
|
||||
if pretty:
|
||||
indent = kwargs.pop("indent", 2)
|
||||
return json.dumps(obj, default=default, indent=indent, **kwargs)
|
||||
return json.dumps(obj, default=default, **kwargs)
|
||||
except TypeError:
|
||||
if pretty:
|
||||
indent = kwargs.pop("indent", 2)
|
||||
return json.dumps(to_json_not_implemented(obj), indent=indent, **kwargs)
|
||||
return json.dumps(to_json_not_implemented(obj), **kwargs)
|
||||
|
||||
obj = _dump_pydantic_models(obj)
|
||||
serialized = _serialize_value(obj)
|
||||
|
||||
if pretty:
|
||||
indent = kwargs.pop("indent", 2)
|
||||
return json.dumps(serialized, indent=indent, **kwargs)
|
||||
return json.dumps(serialized, **kwargs)
|
||||
|
||||
|
||||
def dumpd(obj: Any) -> Any:
|
||||
"""Return a dict representation of an object.
|
||||
|
||||
Note:
|
||||
Plain dicts containing an `'lc'` key are automatically escaped to prevent
|
||||
confusion with LC serialization format. The escape marker is removed during
|
||||
deserialization.
|
||||
|
||||
Args:
|
||||
obj: The object to dump.
|
||||
|
||||
Returns:
|
||||
Dictionary that can be serialized to json using `json.dumps`.
|
||||
"""
|
||||
# Unfortunately this function is not as efficient as it could be because it first
|
||||
# dumps the object to a json string and then loads it back into a dictionary.
|
||||
return json.loads(dumps(obj))
|
||||
obj = _dump_pydantic_models(obj)
|
||||
return _serialize_value(obj)
|
||||
|
||||
@@ -1,16 +1,83 @@
|
||||
"""Load LangChain objects from JSON strings or objects.
|
||||
|
||||
!!! warning
|
||||
`load` and `loads` are vulnerable to remote code execution. Never use with untrusted
|
||||
input.
|
||||
## How it works
|
||||
|
||||
Each `Serializable` LangChain object has a unique identifier (its "class path"), which
|
||||
is a list of strings representing the module path and class name. For example:
|
||||
|
||||
- `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
|
||||
- `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
|
||||
|
||||
When deserializing, the class path from the JSON `'id'` field is checked against an
|
||||
allowlist. If the class is not in the allowlist, deserialization raises a `ValueError`.
|
||||
|
||||
## Security model
|
||||
|
||||
The `allowed_objects` parameter controls which classes can be deserialized:
|
||||
|
||||
- **`'core'` (default)**: Allow classes defined in the serialization mappings for
|
||||
langchain_core.
|
||||
- **`'all'`**: Allow classes defined in the serialization mappings. This
|
||||
includes core LangChain types (messages, prompts, documents, etc.) and trusted
|
||||
partner integrations. See `langchain_core.load.mapping` for the full list.
|
||||
- **Explicit list of classes**: Only those specific classes are allowed.
|
||||
|
||||
For simple data types like messages and documents, the default allowlist is safe to use.
|
||||
These classes do not perform side effects during initialization.
|
||||
|
||||
!!! note "Side effects in allowed classes"
|
||||
|
||||
Deserialization calls `__init__` on allowed classes. If those classes perform side
|
||||
effects during initialization (network calls, file operations, etc.), those side
|
||||
effects will occur. The allowlist prevents instantiation of classes outside the
|
||||
allowlist, but does not sandbox the allowed classes themselves.
|
||||
|
||||
Import paths are also validated against trusted namespaces before any module is
|
||||
imported.
|
||||
|
||||
### Injection protection (escape-based)
|
||||
|
||||
During serialization, plain dicts that contain an `'lc'` key are escaped by wrapping
|
||||
them: `{"__lc_escaped__": {...}}`. During deserialization, escaped dicts are unwrapped
|
||||
and returned as plain dicts, NOT instantiated as LC objects.
|
||||
|
||||
This is an allowlist approach: only dicts explicitly produced by
|
||||
`Serializable.to_json()` (which are NOT escaped) are treated as LC objects;
|
||||
everything else is user data.
|
||||
|
||||
Even if an attacker's payload includes `__lc_escaped__` wrappers, it will be unwrapped
|
||||
to plain dicts and NOT instantiated as malicious objects.
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
from langchain_core.load import load
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
# Use default allowlist (classes from mappings) - recommended
|
||||
obj = load(data)
|
||||
|
||||
# Allow only specific classes (most restrictive)
|
||||
obj = load(
|
||||
data,
|
||||
allowed_objects=[
|
||||
ChatPromptTemplate,
|
||||
AIMessage,
|
||||
HumanMessage,
|
||||
],
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
from langchain_core._api import beta
|
||||
from langchain_core.load._validation import _is_escaped_dict, _unescape_value
|
||||
from langchain_core.load.mapping import (
|
||||
_JS_SERIALIZABLE_MAPPING,
|
||||
_OG_SERIALIZABLE_MAPPING,
|
||||
@@ -49,34 +116,209 @@ ALL_SERIALIZABLE_MAPPINGS = {
|
||||
**_JS_SERIALIZABLE_MAPPING,
|
||||
}
|
||||
|
||||
# Cache for the default allowed class paths computed from mappings
|
||||
# Maps mode ("all" or "core") to the cached set of paths
|
||||
_default_class_paths_cache: dict[str, set[tuple[str, ...]]] = {}
|
||||
|
||||
|
||||
def _get_default_allowed_class_paths(
|
||||
allowed_object_mode: Literal["all", "core"],
|
||||
) -> set[tuple[str, ...]]:
|
||||
"""Get the default allowed class paths from the serialization mappings.
|
||||
|
||||
This uses the mappings as the source of truth for what classes are allowed
|
||||
by default. Both the legacy paths (keys) and current paths (values) are included.
|
||||
|
||||
Args:
|
||||
allowed_object_mode: either `'all'` or `'core'`.
|
||||
|
||||
Returns:
|
||||
Set of class path tuples that are allowed by default.
|
||||
"""
|
||||
if allowed_object_mode in _default_class_paths_cache:
|
||||
return _default_class_paths_cache[allowed_object_mode]
|
||||
|
||||
allowed_paths: set[tuple[str, ...]] = set()
|
||||
for key, value in ALL_SERIALIZABLE_MAPPINGS.items():
|
||||
if allowed_object_mode == "core" and value[0] != "langchain_core":
|
||||
continue
|
||||
allowed_paths.add(key)
|
||||
allowed_paths.add(value)
|
||||
|
||||
_default_class_paths_cache[allowed_object_mode] = allowed_paths
|
||||
return _default_class_paths_cache[allowed_object_mode]
|
||||
|
||||
|
||||
def _block_jinja2_templates(
|
||||
class_path: tuple[str, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Block jinja2 templates during deserialization for security.
|
||||
|
||||
Jinja2 templates can execute arbitrary code, so they are blocked by default when
|
||||
deserializing objects with `template_format='jinja2'`.
|
||||
|
||||
Note:
|
||||
We intentionally do NOT check the `class_path` here to keep this simple and
|
||||
future-proof. If any new class is added that accepts `template_format='jinja2'`,
|
||||
it will be automatically blocked without needing to update this function.
|
||||
|
||||
Args:
|
||||
class_path: The class path tuple being deserialized (unused).
|
||||
kwargs: The kwargs dict for the class constructor.
|
||||
|
||||
Raises:
|
||||
ValueError: If `template_format` is `'jinja2'`.
|
||||
"""
|
||||
_ = class_path # Unused - see docstring for rationale. Kept to satisfy signature.
|
||||
if kwargs.get("template_format") == "jinja2":
|
||||
msg = (
|
||||
"Jinja2 templates are not allowed during deserialization for security "
|
||||
"reasons. Use 'f-string' template format instead, or explicitly allow "
|
||||
"jinja2 by providing a custom init_validator."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def default_init_validator(
|
||||
class_path: tuple[str, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Default init validator that blocks jinja2 templates.
|
||||
|
||||
This is the default validator used by `load()` and `loads()` when no custom
|
||||
validator is provided.
|
||||
|
||||
Args:
|
||||
class_path: The class path tuple being deserialized.
|
||||
kwargs: The kwargs dict for the class constructor.
|
||||
|
||||
Raises:
|
||||
ValueError: If template_format is `'jinja2'`.
|
||||
"""
|
||||
_block_jinja2_templates(class_path, kwargs)
|
||||
|
||||
|
||||
AllowedObject = type[Serializable]
|
||||
"""Type alias for classes that can be included in the `allowed_objects` parameter.
|
||||
|
||||
Must be a `Serializable` subclass (the class itself, not an instance).
|
||||
"""
|
||||
|
||||
InitValidator = Callable[[tuple[str, ...], dict[str, Any]], None]
|
||||
"""Type alias for a callable that validates kwargs during deserialization.
|
||||
|
||||
The callable receives:
|
||||
|
||||
- `class_path`: A tuple of strings identifying the class being instantiated
|
||||
(e.g., `('langchain', 'schema', 'messages', 'AIMessage')`).
|
||||
- `kwargs`: The kwargs dict that will be passed to the constructor.
|
||||
|
||||
The validator should raise an exception if the object should not be deserialized.
|
||||
"""
|
||||
|
||||
|
||||
def _compute_allowed_class_paths(
|
||||
allowed_objects: Iterable[AllowedObject],
|
||||
import_mappings: dict[tuple[str, ...], tuple[str, ...]],
|
||||
) -> set[tuple[str, ...]]:
|
||||
"""Return allowed class paths from an explicit list of classes.
|
||||
|
||||
A class path is a tuple of strings identifying a serializable class, derived from
|
||||
`Serializable.lc_id()`. For example: `('langchain_core', 'messages', 'AIMessage')`.
|
||||
|
||||
Args:
|
||||
allowed_objects: Iterable of `Serializable` subclasses to allow.
|
||||
import_mappings: Mapping of legacy class paths to current class paths.
|
||||
|
||||
Returns:
|
||||
Set of allowed class paths.
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Allow a specific class
|
||||
_compute_allowed_class_paths([MyPrompt], {}) ->
|
||||
{("langchain_core", "prompts", "MyPrompt")}
|
||||
|
||||
# Include legacy paths that map to the same class
|
||||
import_mappings = {("old", "Prompt"): ("langchain_core", "prompts", "MyPrompt")}
|
||||
_compute_allowed_class_paths([MyPrompt], import_mappings) ->
|
||||
{("langchain_core", "prompts", "MyPrompt"), ("old", "Prompt")}
|
||||
```
|
||||
"""
|
||||
allowed_objects_list = list(allowed_objects)
|
||||
|
||||
allowed_class_paths: set[tuple[str, ...]] = set()
|
||||
for allowed_obj in allowed_objects_list:
|
||||
if not isinstance(allowed_obj, type) or not issubclass(
|
||||
allowed_obj, Serializable
|
||||
):
|
||||
msg = "allowed_objects must contain Serializable subclasses."
|
||||
raise TypeError(msg)
|
||||
|
||||
class_path = tuple(allowed_obj.lc_id())
|
||||
allowed_class_paths.add(class_path)
|
||||
# Add legacy paths that map to the same class.
|
||||
for mapping_key, mapping_value in import_mappings.items():
|
||||
if tuple(mapping_value) == class_path:
|
||||
allowed_class_paths.add(mapping_key)
|
||||
return allowed_class_paths
|
||||
|
||||
|
||||
class Reviver:
|
||||
"""Reviver for JSON objects."""
|
||||
"""Reviver for JSON objects.
|
||||
|
||||
Used as the `object_hook` for `json.loads` to reconstruct LangChain objects from
|
||||
their serialized JSON representation.
|
||||
|
||||
Only classes in the allowlist can be instantiated.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: dict[str, str] | None = None,
|
||||
valid_namespaces: list[str] | None = None,
|
||||
secrets_from_env: bool = True, # noqa: FBT001,FBT002
|
||||
secrets_from_env: bool = False, # noqa: FBT001,FBT002
|
||||
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]]
|
||||
| None = None,
|
||||
*,
|
||||
ignore_unserializable_fields: bool = False,
|
||||
init_validator: InitValidator | None = default_init_validator,
|
||||
) -> None:
|
||||
"""Initialize the reviver.
|
||||
|
||||
Args:
|
||||
secrets_map: A map of secrets to load.
|
||||
allowed_objects: Allowlist of classes that can be deserialized.
|
||||
- `'core'` (default): Allow classes defined in the serialization
|
||||
mappings for `langchain_core`.
|
||||
- `'all'`: Allow classes defined in the serialization mappings.
|
||||
|
||||
This includes core LangChain types (messages, prompts, documents,
|
||||
etc.) and trusted partner integrations. See
|
||||
`langchain_core.load.mapping` for the full list.
|
||||
- Explicit list of classes: Only those specific classes are allowed.
|
||||
secrets_map: A map of secrets to load.
|
||||
If a secret is not found in the map, it will be loaded from the
|
||||
environment if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
additional_import_mappings: A dictionary of additional namespace mappings.
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
|
||||
When `allowed_objects` is `None` (using defaults), paths from these
|
||||
mappings are also added to the allowed class paths.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
init_validator: Optional callable to validate kwargs before instantiation.
|
||||
|
||||
If provided, this function is called with `(class_path, kwargs)` where
|
||||
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
||||
The validator should raise an exception if the object should not be
|
||||
deserialized, otherwise return `None`.
|
||||
|
||||
Defaults to `default_init_validator` which blocks jinja2 templates.
|
||||
"""
|
||||
self.secrets_from_env = secrets_from_env
|
||||
self.secrets_map = secrets_map or {}
|
||||
@@ -95,7 +337,26 @@ class Reviver:
|
||||
if self.additional_import_mappings
|
||||
else ALL_SERIALIZABLE_MAPPINGS
|
||||
)
|
||||
# Compute allowed class paths:
|
||||
# - "all" -> use default paths from mappings (+ additional_import_mappings)
|
||||
# - Explicit list -> compute from those classes
|
||||
if allowed_objects in ("all", "core"):
|
||||
self.allowed_class_paths: set[tuple[str, ...]] | None = (
|
||||
_get_default_allowed_class_paths(
|
||||
cast("Literal['all', 'core']", allowed_objects)
|
||||
).copy()
|
||||
)
|
||||
# Add paths from additional_import_mappings to the defaults
|
||||
if self.additional_import_mappings:
|
||||
for key, value in self.additional_import_mappings.items():
|
||||
self.allowed_class_paths.add(key)
|
||||
self.allowed_class_paths.add(value)
|
||||
else:
|
||||
self.allowed_class_paths = _compute_allowed_class_paths(
|
||||
cast("Iterable[AllowedObject]", allowed_objects), self.import_mappings
|
||||
)
|
||||
self.ignore_unserializable_fields = ignore_unserializable_fields
|
||||
self.init_validator = init_validator
|
||||
|
||||
def __call__(self, value: dict[str, Any]) -> Any:
|
||||
"""Revive the value.
|
||||
@@ -146,6 +407,20 @@ class Reviver:
|
||||
[*namespace, name] = value["id"]
|
||||
mapping_key = tuple(value["id"])
|
||||
|
||||
if (
|
||||
self.allowed_class_paths is not None
|
||||
and mapping_key not in self.allowed_class_paths
|
||||
):
|
||||
msg = (
|
||||
f"Deserialization of {mapping_key!r} is not allowed. "
|
||||
"The default (allowed_objects='core') only permits core "
|
||||
"langchain-core classes. To allow trusted partner integrations, "
|
||||
"use allowed_objects='all'. Alternatively, pass an explicit list "
|
||||
"of allowed classes via allowed_objects=[...]. "
|
||||
"See langchain_core.load.mapping for the full allowlist."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
if (
|
||||
namespace[0] not in self.valid_namespaces
|
||||
# The root namespace ["langchain"] is not a valid identifier.
|
||||
@@ -153,13 +428,11 @@ class Reviver:
|
||||
):
|
||||
msg = f"Invalid namespace: {value}"
|
||||
raise ValueError(msg)
|
||||
# Has explicit import path.
|
||||
# Determine explicit import path
|
||||
if mapping_key in self.import_mappings:
|
||||
import_path = self.import_mappings[mapping_key]
|
||||
# Split into module and name
|
||||
import_dir, name = import_path[:-1], import_path[-1]
|
||||
# Import module
|
||||
mod = importlib.import_module(".".join(import_dir))
|
||||
elif namespace[0] in DISALLOW_LOAD_FROM_PATH:
|
||||
msg = (
|
||||
"Trying to deserialize something that cannot "
|
||||
@@ -167,9 +440,16 @@ class Reviver:
|
||||
f"{mapping_key}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
# Otherwise, treat namespace as path.
|
||||
else:
|
||||
mod = importlib.import_module(".".join(namespace))
|
||||
# Otherwise, treat namespace as path.
|
||||
import_dir = namespace
|
||||
|
||||
# Validate import path is in trusted namespaces before importing
|
||||
if import_dir[0] not in self.valid_namespaces:
|
||||
msg = f"Invalid namespace: {value}"
|
||||
raise ValueError(msg)
|
||||
|
||||
mod = importlib.import_module(".".join(import_dir))
|
||||
|
||||
cls = getattr(mod, name)
|
||||
|
||||
@@ -181,6 +461,10 @@ class Reviver:
|
||||
# We don't need to recurse on kwargs
|
||||
# as json.loads will do that for us.
|
||||
kwargs = value.get("kwargs", {})
|
||||
|
||||
if self.init_validator is not None:
|
||||
self.init_validator(mapping_key, kwargs)
|
||||
|
||||
return cls(**kwargs)
|
||||
|
||||
return value
|
||||
@@ -190,46 +474,81 @@ class Reviver:
|
||||
def loads(
|
||||
text: str,
|
||||
*,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: dict[str, str] | None = None,
|
||||
valid_namespaces: list[str] | None = None,
|
||||
secrets_from_env: bool = True,
|
||||
secrets_from_env: bool = False,
|
||||
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]] | None = None,
|
||||
ignore_unserializable_fields: bool = False,
|
||||
init_validator: InitValidator | None = default_init_validator,
|
||||
) -> Any:
|
||||
"""Revive a LangChain class from a JSON string.
|
||||
|
||||
!!! warning
|
||||
This function is vulnerable to remote code execution. Never use with untrusted
|
||||
input.
|
||||
|
||||
Equivalent to `load(json.loads(text))`.
|
||||
|
||||
Only classes in the allowlist can be instantiated. The default allowlist includes
|
||||
core LangChain types (messages, prompts, documents, etc.). See
|
||||
`langchain_core.load.mapping` for the full list.
|
||||
|
||||
!!! warning "Beta feature"
|
||||
|
||||
This is a beta feature. Please be wary of deploying experimental code to
|
||||
production unless you've taken appropriate precautions.
|
||||
|
||||
Args:
|
||||
text: The string to load.
|
||||
allowed_objects: Allowlist of classes that can be deserialized.
|
||||
|
||||
- `'core'` (default): Allow classes defined in the serialization mappings
|
||||
for `langchain_core`.
|
||||
- `'all'`: Allow classes defined in the serialization mappings.
|
||||
|
||||
This includes core LangChain types (messages, prompts, documents, etc.)
|
||||
and trusted partner integrations. See `langchain_core.load.mapping` for
|
||||
the full list.
|
||||
|
||||
- Explicit list of classes: Only those specific classes are allowed.
|
||||
- `[]`: Disallow all deserialization (will raise on any object).
|
||||
secrets_map: A map of secrets to load.
|
||||
|
||||
If a secret is not found in the map, it will be loaded from the environment
|
||||
if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
additional_import_mappings: A dictionary of additional namespace mappings.
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
|
||||
When `allowed_objects` is `None` (using defaults), paths from these
|
||||
mappings are also added to the allowed class paths.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
init_validator: Optional callable to validate kwargs before instantiation.
|
||||
|
||||
If provided, this function is called with `(class_path, kwargs)` where
|
||||
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
||||
The validator should raise an exception if the object should not be
|
||||
deserialized, otherwise return `None`.
|
||||
|
||||
Defaults to `default_init_validator` which blocks jinja2 templates.
|
||||
|
||||
Returns:
|
||||
Revived LangChain objects.
|
||||
|
||||
Raises:
|
||||
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
|
||||
"""
|
||||
return json.loads(
|
||||
text,
|
||||
object_hook=Reviver(
|
||||
secrets_map,
|
||||
valid_namespaces,
|
||||
secrets_from_env,
|
||||
additional_import_mappings,
|
||||
ignore_unserializable_fields=ignore_unserializable_fields,
|
||||
),
|
||||
# Parse JSON and delegate to load() for proper escape handling
|
||||
raw_obj = json.loads(text)
|
||||
return load(
|
||||
raw_obj,
|
||||
allowed_objects=allowed_objects,
|
||||
secrets_map=secrets_map,
|
||||
valid_namespaces=valid_namespaces,
|
||||
secrets_from_env=secrets_from_env,
|
||||
additional_import_mappings=additional_import_mappings,
|
||||
ignore_unserializable_fields=ignore_unserializable_fields,
|
||||
init_validator=init_validator,
|
||||
)
|
||||
|
||||
|
||||
@@ -237,49 +556,112 @@ def loads(
|
||||
def load(
|
||||
obj: Any,
|
||||
*,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: dict[str, str] | None = None,
|
||||
valid_namespaces: list[str] | None = None,
|
||||
secrets_from_env: bool = True,
|
||||
secrets_from_env: bool = False,
|
||||
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]] | None = None,
|
||||
ignore_unserializable_fields: bool = False,
|
||||
init_validator: InitValidator | None = default_init_validator,
|
||||
) -> Any:
|
||||
"""Revive a LangChain class from a JSON object.
|
||||
|
||||
!!! warning
|
||||
This function is vulnerable to remote code execution. Never use with untrusted
|
||||
input.
|
||||
Use this if you already have a parsed JSON object, eg. from `json.load` or
|
||||
`orjson.loads`.
|
||||
|
||||
Use this if you already have a parsed JSON object,
|
||||
eg. from `json.load` or `orjson.loads`.
|
||||
Only classes in the allowlist can be instantiated. The default allowlist includes
|
||||
core LangChain types (messages, prompts, documents, etc.). See
|
||||
`langchain_core.load.mapping` for the full list.
|
||||
|
||||
!!! warning "Beta feature"
|
||||
|
||||
This is a beta feature. Please be wary of deploying experimental code to
|
||||
production unless you've taken appropriate precautions.
|
||||
|
||||
Args:
|
||||
obj: The object to load.
|
||||
allowed_objects: Allowlist of classes that can be deserialized.
|
||||
|
||||
- `'core'` (default): Allow classes defined in the serialization mappings
|
||||
for `langchain_core`.
|
||||
- `'all'`: Allow classes defined in the serialization mappings.
|
||||
|
||||
This includes core LangChain types (messages, prompts, documents, etc.)
|
||||
and trusted partner integrations. See `langchain_core.load.mapping` for
|
||||
the full list.
|
||||
|
||||
- Explicit list of classes: Only those specific classes are allowed.
|
||||
- `[]`: Disallow all deserialization (will raise on any object).
|
||||
secrets_map: A map of secrets to load.
|
||||
|
||||
If a secret is not found in the map, it will be loaded from the environment
|
||||
if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
additional_import_mappings: A dictionary of additional namespace mappings.
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
|
||||
When `allowed_objects` is `None` (using defaults), paths from these
|
||||
mappings are also added to the allowed class paths.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
init_validator: Optional callable to validate kwargs before instantiation.
|
||||
|
||||
If provided, this function is called with `(class_path, kwargs)` where
|
||||
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
||||
The validator should raise an exception if the object should not be
|
||||
deserialized, otherwise return `None`.
|
||||
|
||||
Defaults to `default_init_validator` which blocks jinja2 templates.
|
||||
|
||||
Returns:
|
||||
Revived LangChain objects.
|
||||
|
||||
Raises:
|
||||
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from langchain_core.load import load, dumpd
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
msg = AIMessage(content="Hello")
|
||||
data = dumpd(msg)
|
||||
|
||||
# Deserialize using default allowlist
|
||||
loaded = load(data)
|
||||
|
||||
# Or with explicit allowlist
|
||||
loaded = load(data, allowed_objects=[AIMessage])
|
||||
|
||||
# Or extend defaults with additional mappings
|
||||
loaded = load(
|
||||
data,
|
||||
additional_import_mappings={
|
||||
("my_pkg", "MyClass"): ("my_pkg", "module", "MyClass"),
|
||||
},
|
||||
)
|
||||
```
|
||||
"""
|
||||
reviver = Reviver(
|
||||
allowed_objects,
|
||||
secrets_map,
|
||||
valid_namespaces,
|
||||
secrets_from_env,
|
||||
additional_import_mappings,
|
||||
ignore_unserializable_fields=ignore_unserializable_fields,
|
||||
init_validator=init_validator,
|
||||
)
|
||||
|
||||
def _load(obj: Any) -> Any:
|
||||
if isinstance(obj, dict):
|
||||
# Need to revive leaf nodes before reviving this node
|
||||
# Check for escaped dict FIRST (before recursing).
|
||||
# Escaped dicts are user data that should NOT be processed as LC objects.
|
||||
if _is_escaped_dict(obj):
|
||||
return _unescape_value(obj)
|
||||
|
||||
# Not escaped - recurse into children then apply reviver
|
||||
loaded_obj = {k: _load(v) for k, v in obj.items()}
|
||||
return reviver(loaded_obj)
|
||||
if isinstance(obj, list):
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
"""Serialization mapping.
|
||||
|
||||
This file contains a mapping between the lc_namespace path for a given
|
||||
subclass that implements from Serializable to the namespace
|
||||
This file contains a mapping between the `lc_namespace` path for a given
|
||||
subclass that implements from `Serializable` to the namespace
|
||||
where that class is actually located.
|
||||
|
||||
This mapping helps maintain the ability to serialize and deserialize
|
||||
well-known LangChain objects even if they are moved around in the codebase
|
||||
across different LangChain versions.
|
||||
|
||||
For example,
|
||||
For example, the code for the `AIMessage` class is located in
|
||||
`langchain_core.messages.ai.AIMessage`. This message is associated with the
|
||||
`lc_namespace` of `["langchain", "schema", "messages", "AIMessage"]`,
|
||||
because this code was originally in `langchain.schema.messages.AIMessage`.
|
||||
|
||||
The code for AIMessage class is located in langchain_core.messages.ai.AIMessage,
|
||||
This message is associated with the lc_namespace
|
||||
["langchain", "schema", "messages", "AIMessage"],
|
||||
because this code was originally in langchain.schema.messages.AIMessage.
|
||||
|
||||
The mapping allows us to deserialize an AIMessage created with an older
|
||||
The mapping allows us to deserialize an `AIMessage` created with an older
|
||||
version of LangChain where the code was in a different location.
|
||||
"""
|
||||
|
||||
@@ -275,6 +273,11 @@ SERIALIZABLE_MAPPING: dict[tuple[str, ...], tuple[str, ...]] = {
|
||||
"chat_models",
|
||||
"ChatGroq",
|
||||
),
|
||||
("langchain_xai", "chat_models", "ChatXAI"): (
|
||||
"langchain_xai",
|
||||
"chat_models",
|
||||
"ChatXAI",
|
||||
),
|
||||
("langchain", "chat_models", "fireworks", "ChatFireworks"): (
|
||||
"langchain_fireworks",
|
||||
"chat_models",
|
||||
@@ -529,16 +532,6 @@ SERIALIZABLE_MAPPING: dict[tuple[str, ...], tuple[str, ...]] = {
|
||||
"structured",
|
||||
"StructuredPrompt",
|
||||
),
|
||||
("langchain_sambanova", "chat_models", "ChatSambaNovaCloud"): (
|
||||
"langchain_sambanova",
|
||||
"chat_models",
|
||||
"ChatSambaNovaCloud",
|
||||
),
|
||||
("langchain_sambanova", "chat_models", "ChatSambaStudio"): (
|
||||
"langchain_sambanova",
|
||||
"chat_models",
|
||||
"ChatSambaStudio",
|
||||
),
|
||||
("langchain_core", "prompts", "message", "_DictMessagePromptTemplate"): (
|
||||
"langchain_core",
|
||||
"prompts",
|
||||
|
||||
@@ -92,11 +92,12 @@ class Serializable(BaseModel, ABC):
|
||||
|
||||
It relies on the following methods and properties:
|
||||
|
||||
- `is_lc_serializable`: Is this class serializable?
|
||||
- [`is_lc_serializable`][langchain_core.load.serializable.Serializable.is_lc_serializable]: Is this class serializable?
|
||||
|
||||
By design, even if a class inherits from `Serializable`, it is not serializable
|
||||
by default. This is to prevent accidental serialization of objects that should
|
||||
not be serialized.
|
||||
- `get_lc_namespace`: Get the namespace of the LangChain object.
|
||||
- [`get_lc_namespace`][langchain_core.load.serializable.Serializable.get_lc_namespace]: Get the namespace of the LangChain object.
|
||||
|
||||
During deserialization, this namespace is used to identify
|
||||
the correct class to instantiate.
|
||||
@@ -105,10 +106,10 @@ class Serializable(BaseModel, ABC):
|
||||
During deserialization an additional mapping is handle classes that have moved
|
||||
or been renamed across package versions.
|
||||
|
||||
- `lc_secrets`: A map of constructor argument names to secret ids.
|
||||
- `lc_attributes`: List of additional attribute names that should be included
|
||||
- [`lc_secrets`][langchain_core.load.serializable.Serializable.lc_secrets]: A map of constructor argument names to secret ids.
|
||||
- [`lc_attributes`][langchain_core.load.serializable.Serializable.lc_attributes]: List of additional attribute names that should be included
|
||||
as part of the serialized representation.
|
||||
"""
|
||||
""" # noqa: E501
|
||||
|
||||
# Remove default BaseModel init docstring.
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
@@ -132,8 +133,9 @@ class Serializable(BaseModel, ABC):
|
||||
def get_lc_namespace(cls) -> list[str]:
|
||||
"""Get the namespace of the LangChain object.
|
||||
|
||||
For example, if the class is `langchain.llms.openai.OpenAI`, then the
|
||||
namespace is `["langchain", "llms", "openai"]`
|
||||
For example, if the class is
|
||||
[`langchain.llms.openai.OpenAI`][langchain_openai.OpenAI], then the namespace is
|
||||
`["langchain", "llms", "openai"]`
|
||||
|
||||
Returns:
|
||||
The namespace.
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""AI message."""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Literal, cast, overload
|
||||
|
||||
from pydantic import model_validator
|
||||
from pydantic import Field, model_validator
|
||||
from typing_extensions import NotRequired, Self, TypedDict, override
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
@@ -166,10 +167,10 @@ class AIMessage(BaseMessage):
|
||||
(e.g., tool calls, usage metadata) added by the LangChain framework.
|
||||
"""
|
||||
|
||||
tool_calls: list[ToolCall] = []
|
||||
tool_calls: list[ToolCall] = Field(default_factory=list)
|
||||
"""If present, tool calls associated with the message."""
|
||||
|
||||
invalid_tool_calls: list[InvalidToolCall] = []
|
||||
invalid_tool_calls: list[InvalidToolCall] = Field(default_factory=list)
|
||||
"""If present, tool calls with parsing errors associated with the message."""
|
||||
|
||||
usage_metadata: UsageMetadata | None = None
|
||||
@@ -326,7 +327,7 @@ class AIMessage(BaseMessage):
|
||||
if tool_calls := values.get("tool_calls"):
|
||||
values["tool_calls"] = [
|
||||
create_tool_call(
|
||||
**{k: v for k, v in tc.items() if k not in ("type", "extras")}
|
||||
**{k: v for k, v in tc.items() if k not in {"type", "extras"}}
|
||||
)
|
||||
for tc in tool_calls
|
||||
]
|
||||
@@ -394,7 +395,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
type: Literal["AIMessageChunk"] = "AIMessageChunk" # type: ignore[assignment]
|
||||
"""The type of the message (used for deserialization)."""
|
||||
|
||||
tool_call_chunks: list[ToolCallChunk] = []
|
||||
tool_call_chunks: list[ToolCallChunk] = Field(default_factory=list)
|
||||
"""If provided, tool call chunks associated with the message."""
|
||||
|
||||
chunk_position: Literal["last"] | None = None
|
||||
@@ -405,8 +406,8 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
"""
|
||||
|
||||
@property
|
||||
@override
|
||||
def lc_attributes(self) -> dict:
|
||||
"""Attributes to be serialized, even if they are derived from other initialization args.""" # noqa: E501
|
||||
return {
|
||||
"tool_calls": self.tool_calls,
|
||||
"invalid_tool_calls": self.invalid_tool_calls,
|
||||
@@ -442,7 +443,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
blocks = [
|
||||
block
|
||||
for block in blocks
|
||||
if block["type"] not in ("tool_call", "invalid_tool_call")
|
||||
if block["type"] not in {"tool_call", "invalid_tool_call"}
|
||||
]
|
||||
for tool_call_chunk in self.tool_call_chunks:
|
||||
tc: types.ToolCallChunk = {
|
||||
@@ -563,7 +564,11 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def init_server_tool_calls(self) -> Self:
|
||||
"""Parse `server_tool_call_chunks` from [`ServerToolCallChunk`][langchain.messages.ServerToolCallChunk] objects.""" # noqa: E501
|
||||
"""Initialize server tool calls.
|
||||
|
||||
Parse `server_tool_call_chunks` from
|
||||
[`ServerToolCallChunk`][langchain.messages.ServerToolCallChunk] objects.
|
||||
"""
|
||||
if (
|
||||
self.chunk_position == "last"
|
||||
and self.response_metadata.get("output_version") == "v1"
|
||||
@@ -573,7 +578,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and block.get("type")
|
||||
in ("server_tool_call", "server_tool_call_chunk")
|
||||
in {"server_tool_call", "server_tool_call_chunk"}
|
||||
and (args_str := block.get("args"))
|
||||
and isinstance(args_str, str)
|
||||
):
|
||||
@@ -651,29 +656,28 @@ def add_ai_message_chunks(
|
||||
else:
|
||||
usage_metadata = None
|
||||
|
||||
# Ranks are defined by the order of preference. Higher is better:
|
||||
# 2. Provider-assigned IDs (non lc_* and non lc_run-*)
|
||||
# 1. lc_run-* IDs
|
||||
# 0. lc_* and other remaining IDs
|
||||
best_rank = -1
|
||||
chunk_id = None
|
||||
candidates = [left.id] + [o.id for o in others]
|
||||
# first pass: pick the first provider-assigned id (non-run-* and non-lc_*)
|
||||
candidates = itertools.chain([left.id], (o.id for o in others))
|
||||
|
||||
for id_ in candidates:
|
||||
if (
|
||||
id_
|
||||
and not id_.startswith(LC_ID_PREFIX)
|
||||
and not id_.startswith(LC_AUTO_PREFIX)
|
||||
):
|
||||
if not id_:
|
||||
continue
|
||||
|
||||
if not id_.startswith(LC_ID_PREFIX) and not id_.startswith(LC_AUTO_PREFIX):
|
||||
chunk_id = id_
|
||||
# Highest rank, return instantly
|
||||
break
|
||||
else:
|
||||
# second pass: prefer lc_run-* IDs over lc_* IDs
|
||||
for id_ in candidates:
|
||||
if id_ and id_.startswith(LC_ID_PREFIX):
|
||||
chunk_id = id_
|
||||
break
|
||||
else:
|
||||
# third pass: take any remaining ID (auto-generated lc_* IDs)
|
||||
for id_ in candidates:
|
||||
if id_:
|
||||
chunk_id = id_
|
||||
break
|
||||
|
||||
rank = 1 if id_.startswith(LC_ID_PREFIX) else 0
|
||||
|
||||
if rank > best_rank:
|
||||
best_rank = rank
|
||||
chunk_id = id_
|
||||
|
||||
chunk_position: Literal["last"] | None = (
|
||||
"last" if any(x.chunk_position == "last" for x in [left, *others]) else None
|
||||
|
||||
@@ -8,6 +8,7 @@ from pydantic import ConfigDict, Field
|
||||
|
||||
from langchain_core._api.deprecation import warn_deprecated
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.utils import get_bolded_text
|
||||
from langchain_core.utils._merge import merge_dicts, merge_lists
|
||||
from langchain_core.utils.interactive_env import is_interactive_env
|
||||
@@ -17,7 +18,6 @@ if TYPE_CHECKING:
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate
|
||||
|
||||
|
||||
@@ -204,7 +204,6 @@ class BaseMessage(Serializable):
|
||||
|
||||
"""
|
||||
# Needed here to avoid circular import, as these classes import BaseMessages
|
||||
from langchain_core.messages import content as types # noqa: PLC0415
|
||||
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
|
||||
_convert_to_v1_from_anthropic_input,
|
||||
)
|
||||
@@ -266,6 +265,9 @@ class BaseMessage(Serializable):
|
||||
|
||||
Can be used as both property (`message.text`) and method (`message.text()`).
|
||||
|
||||
Handles both string and list content types (e.g. for content blocks). Only
|
||||
extracts blocks with `type: 'text'`; other block types are ignored.
|
||||
|
||||
!!! deprecated
|
||||
As of `langchain-core` 1.0.0, calling `.text()` as a method is deprecated.
|
||||
Use `.text` as a property instead. This method will be removed in 2.0.0.
|
||||
@@ -277,7 +279,7 @@ class BaseMessage(Serializable):
|
||||
if isinstance(self.content, str):
|
||||
text_value = self.content
|
||||
else:
|
||||
# must be a list
|
||||
# Must be a list
|
||||
blocks = [
|
||||
block
|
||||
for block in self.content
|
||||
@@ -302,7 +304,7 @@ class BaseMessage(Serializable):
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate # noqa: PLC0415
|
||||
|
||||
prompt = ChatPromptTemplate(messages=[self])
|
||||
return prompt + other
|
||||
return prompt.__add__(other)
|
||||
|
||||
def pretty_repr(
|
||||
self,
|
||||
|
||||
@@ -159,12 +159,12 @@ def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
|
||||
|
||||
return url_citation
|
||||
|
||||
if citation_type in (
|
||||
if citation_type in {
|
||||
"char_location",
|
||||
"content_block_location",
|
||||
"page_location",
|
||||
"search_result_location",
|
||||
):
|
||||
}:
|
||||
document_citation: types.Citation = {
|
||||
"type": "citation",
|
||||
"cited_text": citation["cited_text"],
|
||||
@@ -173,8 +173,6 @@ def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
|
||||
document_citation["title"] = citation["document_title"]
|
||||
elif title := citation.get("title"):
|
||||
document_citation["title"] = title
|
||||
else:
|
||||
pass
|
||||
known_fields = {
|
||||
"type",
|
||||
"cited_text",
|
||||
@@ -280,8 +278,6 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
|
||||
"id": tc.get("id"),
|
||||
}
|
||||
break
|
||||
else:
|
||||
pass
|
||||
if not tool_call_block:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
@@ -465,12 +461,26 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Anthropic content."""
|
||||
"""Derive standard content blocks from a message with Anthropic content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_anthropic(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message chunk with Anthropic content."""
|
||||
"""Derive standard content blocks from a message chunk with Anthropic content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_anthropic(message)
|
||||
|
||||
|
||||
|
||||
@@ -65,14 +65,28 @@ def _convert_to_v1_from_bedrock_chunk(
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Bedrock content."""
|
||||
"""Derive standard content blocks from a message with Bedrock content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
if "claude" not in message.response_metadata.get("model_name", "").lower():
|
||||
raise NotImplementedError # fall back to best-effort parsing
|
||||
return _convert_to_v1_from_bedrock(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message chunk with Bedrock content."""
|
||||
"""Derive standard content blocks from a message chunk with Bedrock content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
# TODO: add model_name to all Bedrock chunks and update core merging logic
|
||||
# to not append during aggregation. Then raise NotImplementedError here if
|
||||
# not an Anthropic model to fall back to best-effort parsing.
|
||||
|
||||
@@ -240,8 +240,6 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
|
||||
"id": tc.get("id"),
|
||||
}
|
||||
break
|
||||
else:
|
||||
pass
|
||||
if not tool_call_block:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
@@ -283,12 +281,26 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Bedrock Converse content."""
|
||||
"""Derive standard content blocks from a message with Bedrock Converse content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_converse(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a chunk with Bedrock Converse content."""
|
||||
"""Derive standard content blocks from a chunk with Bedrock Converse content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_converse(message)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.content import Citation, create_citation
|
||||
|
||||
try:
|
||||
import filetype # type: ignore[import-not-found]
|
||||
|
||||
_HAS_FILETYPE = True
|
||||
except ImportError:
|
||||
_HAS_FILETYPE = False
|
||||
|
||||
|
||||
def _bytes_to_b64_str(bytes_: bytes) -> str:
|
||||
"""Convert bytes to base64 encoded string."""
|
||||
@@ -391,9 +398,7 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"base64": url,
|
||||
}
|
||||
|
||||
try:
|
||||
import filetype # type: ignore[import-not-found] # noqa: PLC0415
|
||||
|
||||
if _HAS_FILETYPE:
|
||||
# Guess MIME type based on file bytes
|
||||
mime_type = None
|
||||
kind = filetype.guess(decoded_bytes)
|
||||
@@ -401,9 +406,6 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
mime_type = kind.mime
|
||||
if mime_type:
|
||||
image_url_b64_block["mime_type"] = mime_type
|
||||
except ImportError:
|
||||
# filetype library not available, skip type detection
|
||||
pass
|
||||
|
||||
converted_blocks.append(
|
||||
cast("types.ImageContentBlock", image_url_b64_block)
|
||||
@@ -411,7 +413,10 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
except Exception:
|
||||
# Not valid base64, treat as non-standard
|
||||
converted_blocks.append(
|
||||
{"type": "non_standard", "value": item}
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": item,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# This likely won't be reached according to previous implementations
|
||||
@@ -523,12 +528,26 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Google (GenAI) content."""
|
||||
"""Derive standard content blocks from a message with Google (GenAI) content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_genai(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a chunk with Google (GenAI) content."""
|
||||
"""Derive standard content blocks from a chunk with Google (GenAI) content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_genai(message)
|
||||
|
||||
|
||||
|
||||
@@ -105,26 +105,40 @@ def _convert_to_v1_from_groq(message: AIMessage) -> list[types.ContentBlock]:
|
||||
if isinstance(message.content, str) and message.content:
|
||||
content_blocks.append({"type": "text", "text": message.content})
|
||||
|
||||
for tool_call in message.tool_calls:
|
||||
content_blocks.append( # noqa: PERF401
|
||||
{
|
||||
"type": "tool_call",
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
"id": tool_call.get("id"),
|
||||
}
|
||||
)
|
||||
content_blocks.extend(
|
||||
{
|
||||
"type": "tool_call",
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
"id": tool_call.get("id"),
|
||||
}
|
||||
for tool_call in message.tool_calls
|
||||
)
|
||||
|
||||
return content_blocks
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with groq content."""
|
||||
"""Derive standard content blocks from a message with groq content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_groq(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message chunk with groq content."""
|
||||
"""Derive standard content blocks from a message chunk with groq content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_groq(message)
|
||||
|
||||
|
||||
|
||||
@@ -10,16 +10,28 @@ from langchain_core.language_models._utils import (
|
||||
_parse_data_uri,
|
||||
is_openai_data_block,
|
||||
)
|
||||
from langchain_core.messages import AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
|
||||
def convert_to_openai_image_block(block: dict[str, Any]) -> dict:
|
||||
"""Convert `ImageContentBlock` to format expected by OpenAI Chat Completions."""
|
||||
"""Convert `ImageContentBlock` to format expected by OpenAI Chat Completions.
|
||||
|
||||
Args:
|
||||
block: The image content block to convert.
|
||||
|
||||
Raises:
|
||||
ValueError: If required keys are missing.
|
||||
ValueError: If source type is unsupported.
|
||||
|
||||
Returns:
|
||||
The formatted image content block.
|
||||
"""
|
||||
if "url" in block:
|
||||
return {
|
||||
"type": "image_url",
|
||||
@@ -50,6 +62,18 @@ def convert_to_openai_data_block(
|
||||
|
||||
"Standard data content block" can include old-style LangChain v0 blocks
|
||||
(URLContentBlock, Base64ContentBlock, IDContentBlock) or new ones.
|
||||
|
||||
Args:
|
||||
block: The content block to convert.
|
||||
api: The OpenAI API being targeted. Either "chat/completions" or "responses".
|
||||
|
||||
Raises:
|
||||
ValueError: If required keys are missing.
|
||||
ValueError: If file URLs are used with Chat Completions API.
|
||||
ValueError: If block type is unsupported.
|
||||
|
||||
Returns:
|
||||
The formatted content block.
|
||||
"""
|
||||
if block["type"] == "image":
|
||||
chat_completions_block = convert_to_openai_image_block(block)
|
||||
@@ -169,8 +193,6 @@ def _convert_to_v1_from_chat_completions_input(
|
||||
Returns:
|
||||
Updated list with OpenAI blocks converted to v1 format.
|
||||
"""
|
||||
from langchain_core.messages import content as types # noqa: PLC0415
|
||||
|
||||
converted_blocks = []
|
||||
unpacked_blocks: list[dict[str, Any]] = [
|
||||
cast("dict[str, Any]", block)
|
||||
@@ -248,7 +270,7 @@ def _convert_from_v1_to_chat_completions(message: AIMessage) -> AIMessage:
|
||||
if block_type == "text":
|
||||
# Strip annotations
|
||||
new_content.append({"type": "text", "text": block["text"]})
|
||||
elif block_type in ("reasoning", "tool_call"):
|
||||
elif block_type in {"reasoning", "tool_call"}:
|
||||
pass
|
||||
else:
|
||||
new_content.append(block)
|
||||
@@ -265,8 +287,6 @@ _FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
|
||||
|
||||
def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
|
||||
"""Convert v0 AIMessage into `output_version="responses/v1"` format."""
|
||||
from langchain_core.messages import AIMessageChunk # noqa: PLC0415
|
||||
|
||||
# Only update ChatOpenAI v0.3 AIMessages
|
||||
is_chatopenai_v03 = (
|
||||
isinstance(message.content, list)
|
||||
@@ -683,8 +703,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
||||
) = None
|
||||
call_id = block.get("call_id", "")
|
||||
|
||||
from langchain_core.messages import AIMessageChunk # noqa: PLC0415
|
||||
|
||||
if (
|
||||
isinstance(message, AIMessageChunk)
|
||||
and len(message.tool_call_chunks) == 1
|
||||
@@ -706,8 +724,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
||||
if invalid_tool_call.get("id") == call_id:
|
||||
tool_call_block = invalid_tool_call.copy()
|
||||
break
|
||||
else:
|
||||
pass
|
||||
if tool_call_block:
|
||||
if "id" in block:
|
||||
if "extras" not in tool_call_block:
|
||||
@@ -735,7 +751,7 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
||||
k: v for k, v in block["action"].items() if k != "sources"
|
||||
}
|
||||
for key in block:
|
||||
if key not in ("type", "id", "action", "status", "index"):
|
||||
if key not in {"type", "id", "action", "status", "index"}:
|
||||
web_search_call[key] = block[key]
|
||||
|
||||
yield cast("types.ServerToolCall", web_search_call)
|
||||
@@ -761,8 +777,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
||||
web_search_result["status"] = "success"
|
||||
elif status:
|
||||
web_search_result["extras"] = {"status": status}
|
||||
else:
|
||||
pass
|
||||
if "index" in block and isinstance(block["index"], int):
|
||||
web_search_result["index"] = f"lc_wsr_{block['index'] + 1}"
|
||||
yield cast("types.ServerToolResult", web_search_result)
|
||||
@@ -778,14 +792,14 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
||||
file_search_call["index"] = f"lc_fsc_{block['index']}"
|
||||
|
||||
for key in block:
|
||||
if key not in (
|
||||
if key not in {
|
||||
"type",
|
||||
"id",
|
||||
"queries",
|
||||
"results",
|
||||
"status",
|
||||
"index",
|
||||
):
|
||||
}:
|
||||
file_search_call[key] = block[key]
|
||||
|
||||
yield cast("types.ServerToolCall", file_search_call)
|
||||
@@ -804,8 +818,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
||||
file_search_result["status"] = "success"
|
||||
elif status:
|
||||
file_search_result["extras"] = {"status": status}
|
||||
else:
|
||||
pass
|
||||
if "index" in block and isinstance(block["index"], int):
|
||||
file_search_result["index"] = f"lc_fsr_{block['index'] + 1}"
|
||||
yield cast("types.ServerToolResult", file_search_result)
|
||||
@@ -849,8 +861,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
||||
code_interpreter_result["status"] = "success"
|
||||
elif status:
|
||||
code_interpreter_result["extras"] = {"status": status}
|
||||
else:
|
||||
pass
|
||||
if "index" in block and isinstance(block["index"], int):
|
||||
code_interpreter_result["index"] = f"lc_cir_{block['index'] + 1}"
|
||||
|
||||
@@ -981,7 +991,14 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with OpenAI content."""
|
||||
"""Derive standard content blocks from a message with OpenAI content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
if isinstance(message.content, str):
|
||||
return _convert_to_v1_from_chat_completions(message)
|
||||
message = _convert_from_v03_ai_message(message)
|
||||
@@ -989,7 +1006,14 @@ def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message chunk with OpenAI content."""
|
||||
"""Derive standard content blocks from a message chunk with OpenAI content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
if isinstance(message.content, str):
|
||||
return _convert_to_v1_from_chat_completions_chunk(message)
|
||||
message = _convert_from_v03_ai_message(message) # type: ignore[assignment]
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
"""Standard, multimodal content blocks for Large Language Model I/O.
|
||||
|
||||
!!! warning
|
||||
This module is under active development. The API is unstable and subject to
|
||||
change in future releases.
|
||||
|
||||
This module provides standardized data structures for representing inputs to and
|
||||
outputs from LLMs. The core abstraction is the **Content Block**, a `TypedDict`.
|
||||
This module provides standardized data structures for representing inputs to and outputs
|
||||
from LLMs. The core abstraction is the **Content Block**, a `TypedDict`.
|
||||
|
||||
**Rationale**
|
||||
|
||||
Different LLM providers use distinct and incompatible API schemas. This module
|
||||
provides a unified, provider-agnostic format to facilitate these interactions. A
|
||||
message to or from a model is simply a list of content blocks, allowing for the natural
|
||||
interleaving of text, images, and other content in a single ordered sequence.
|
||||
Different LLM providers use distinct and incompatible API schemas. This module provides
|
||||
a unified, provider-agnostic format to facilitate these interactions. A message to or
|
||||
from a model is simply a list of content blocks, allowing for the natural interleaving
|
||||
of text, images, and other content in a single ordered sequence.
|
||||
|
||||
An adapter for a specific provider is responsible for translating this standard list of
|
||||
blocks into the format required by its API.
|
||||
@@ -25,16 +21,27 @@ without losing the benefits of type checking and validation.
|
||||
|
||||
Furthermore, provider-specific fields **within** a standard block are fully supported
|
||||
by default in the `extras` field of each block. This allows for additional metadata
|
||||
to be included without breaking the standard structure.
|
||||
to be included without breaking the standard structure. For example, Google's thought
|
||||
signature:
|
||||
|
||||
```python
|
||||
AIMessage(
|
||||
content=[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "J'adore la programmation.",
|
||||
"extras": {"signature": "EpoWCpc..."}, # Thought signature
|
||||
}
|
||||
], ...
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Do not heavily rely on the `extras` field for provider-specific data! This field
|
||||
is subject to deprecation in future releases as we move towards PEP 728.
|
||||
|
||||
!!! note
|
||||
|
||||
Following widespread adoption of [PEP 728](https://peps.python.org/pep-0728/), we
|
||||
will add `extra_items=Any` as a param to Content Blocks. This will signify to type
|
||||
checkers that additional provider-specific fields are allowed outside of the
|
||||
intend to add `extra_items=Any` as a param to Content Blocks. This will signify to
|
||||
type checkers that additional provider-specific fields are allowed outside of the
|
||||
`extras` field, and that will become the new standard approach to adding
|
||||
provider-specific metadata.
|
||||
|
||||
@@ -72,30 +79,10 @@ to be included without breaking the standard structure.
|
||||
openai_data = my_block["openai_metadata"] # Type: Any
|
||||
```
|
||||
|
||||
PEP 728 is enabled with `# type: ignore[call-arg]` comments to suppress
|
||||
warnings from type checkers that don't yet support it. The functionality works
|
||||
correctly in Python 3.13+ and will be fully supported as the ecosystem catches
|
||||
up.
|
||||
|
||||
**Key Block Types**
|
||||
|
||||
The module defines several types of content blocks, including:
|
||||
|
||||
- `TextContentBlock`: Standard text output.
|
||||
- `Citation`: For annotations that link text output to a source document.
|
||||
- `ToolCall`: For function calling.
|
||||
- `ReasoningContentBlock`: To capture a model's thought process.
|
||||
- Multimodal data:
|
||||
- `ImageContentBlock`
|
||||
- `AudioContentBlock`
|
||||
- `VideoContentBlock`
|
||||
- `PlainTextContentBlock` (e.g. .txt or .md files)
|
||||
- `FileContentBlock` (e.g. PDFs, etc.)
|
||||
|
||||
**Example Usage**
|
||||
|
||||
```python
|
||||
# Direct construction:
|
||||
# Direct construction
|
||||
from langchain_core.messages.content import TextContentBlock, ImageContentBlock
|
||||
|
||||
multimodal_message: AIMessage(
|
||||
@@ -109,7 +96,7 @@ multimodal_message: AIMessage(
|
||||
]
|
||||
)
|
||||
|
||||
# Using factories:
|
||||
# Using factories
|
||||
from langchain_core.messages.content import create_text_block, create_image_block
|
||||
|
||||
multimodal_message: AIMessage(
|
||||
@@ -124,6 +111,7 @@ multimodal_message: AIMessage(
|
||||
```
|
||||
|
||||
Factory functions offer benefits such as:
|
||||
|
||||
- Automatic ID generation (when not provided)
|
||||
- No need to manually specify the `type` field
|
||||
"""
|
||||
@@ -139,30 +127,30 @@ class Citation(TypedDict):
|
||||
"""Annotation for citing data from a document.
|
||||
|
||||
!!! note
|
||||
|
||||
`start`/`end` indices refer to the **response text**,
|
||||
not the source text. This means that the indices are relative to the model's
|
||||
response, not the original document (as specified in the `url`).
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_citation` may also be used as a factory to create a `Citation`.
|
||||
Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["citation"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
url: NotRequired[str]
|
||||
@@ -200,13 +188,12 @@ class NonStandardAnnotation(TypedDict):
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
value: dict[str, Any]
|
||||
@@ -224,25 +211,24 @@ class TextContentBlock(TypedDict):
|
||||
from a language model or the text of a user message.
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_text_block` may also be used as a factory to create a
|
||||
`TextContentBlock`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["text"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
text: str
|
||||
@@ -270,12 +256,12 @@ class ToolCall(TypedDict):
|
||||
and an identifier of "123".
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_tool_call` may also be used as a factory to create a
|
||||
`ToolCall`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["tool_call"]
|
||||
@@ -286,7 +272,6 @@ class ToolCall(TypedDict):
|
||||
|
||||
An identifier is needed to associate a tool call request with a tool
|
||||
call result in events when multiple concurrent tool calls are made.
|
||||
|
||||
"""
|
||||
# TODO: Consider making this NotRequired[str] in the future.
|
||||
|
||||
@@ -332,8 +317,8 @@ class ToolCallChunk(TypedDict):
|
||||
|
||||
An identifier is needed to associate a tool call request with a tool
|
||||
call result in events when multiple concurrent tool calls are made.
|
||||
|
||||
"""
|
||||
# TODO: Consider making this NotRequired[str] in the future.
|
||||
|
||||
name: str | None
|
||||
"""The name of the tool to be called."""
|
||||
@@ -353,7 +338,6 @@ class InvalidToolCall(TypedDict):
|
||||
|
||||
Here we add an `error` key to surface errors made during generation
|
||||
(e.g., invalid JSON arguments.)
|
||||
|
||||
"""
|
||||
|
||||
# TODO: Consider making fields NotRequired[str] in the future.
|
||||
@@ -366,8 +350,8 @@ class InvalidToolCall(TypedDict):
|
||||
|
||||
An identifier is needed to associate a tool call request with a tool
|
||||
call result in events when multiple concurrent tool calls are made.
|
||||
|
||||
"""
|
||||
# TODO: Consider making this NotRequired[str] in the future.
|
||||
|
||||
name: str | None
|
||||
"""The name of the tool to be called."""
|
||||
@@ -423,7 +407,13 @@ class ServerToolCallChunk(TypedDict):
|
||||
"""JSON substring of the arguments to the tool call."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""An identifier associated with the tool call."""
|
||||
"""Unique identifier for this server tool call chunk.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
"""
|
||||
|
||||
index: NotRequired[int | str]
|
||||
"""Index of block in aggregate response. Used during streaming."""
|
||||
@@ -439,7 +429,13 @@ class ServerToolResult(TypedDict):
|
||||
"""Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""An identifier associated with the server tool result."""
|
||||
"""Unique identifier for this server tool result.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
"""
|
||||
|
||||
tool_call_id: str
|
||||
"""ID of the corresponding server tool call."""
|
||||
@@ -461,25 +457,24 @@ class ReasoningContentBlock(TypedDict):
|
||||
"""Reasoning output from a LLM.
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_reasoning_block` may also be used as a factory to create a
|
||||
`ReasoningContentBlock`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["reasoning"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
reasoning: NotRequired[str]
|
||||
@@ -487,7 +482,6 @@ class ReasoningContentBlock(TypedDict):
|
||||
|
||||
Either the thought summary or the raw reasoning text itself. This is often parsed
|
||||
from `<think>` tags in the model's response.
|
||||
|
||||
"""
|
||||
|
||||
index: NotRequired[int | str]
|
||||
@@ -504,35 +498,38 @@ class ImageContentBlock(TypedDict):
|
||||
"""Image data.
|
||||
|
||||
!!! note "Factory function"
|
||||
`create_image_block` may also be used as a factory to create a
|
||||
|
||||
`create_image_block` may also be used as a factory to create an
|
||||
`ImageContentBlock`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["image"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
file_id: NotRequired[str]
|
||||
"""ID of the image file, e.g., from a file storage system."""
|
||||
"""Reference to the image in an external file storage system.
|
||||
|
||||
For example, OpenAI or Anthropic's Files API.
|
||||
"""
|
||||
|
||||
mime_type: NotRequired[str]
|
||||
"""MIME type of the image. Required for base64.
|
||||
"""MIME type of the image.
|
||||
|
||||
Required for base64 data.
|
||||
|
||||
[Examples from IANA](https://www.iana.org/assignments/media-types/media-types.xhtml#image)
|
||||
|
||||
"""
|
||||
|
||||
index: NotRequired[int | str]
|
||||
@@ -552,35 +549,38 @@ class VideoContentBlock(TypedDict):
|
||||
"""Video data.
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_video_block` may also be used as a factory to create a
|
||||
`VideoContentBlock`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["video"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
file_id: NotRequired[str]
|
||||
"""ID of the video file, e.g., from a file storage system."""
|
||||
"""Reference to the video in an external file storage system.
|
||||
|
||||
For example, OpenAI or Anthropic's Files API.
|
||||
"""
|
||||
|
||||
mime_type: NotRequired[str]
|
||||
"""MIME type of the video. Required for base64.
|
||||
"""MIME type of the video.
|
||||
|
||||
Required for base64 data.
|
||||
|
||||
[Examples from IANA](https://www.iana.org/assignments/media-types/media-types.xhtml#video)
|
||||
|
||||
"""
|
||||
|
||||
index: NotRequired[int | str]
|
||||
@@ -600,34 +600,38 @@ class AudioContentBlock(TypedDict):
|
||||
"""Audio data.
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_audio_block` may also be used as a factory to create an
|
||||
`AudioContentBlock`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["audio"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
file_id: NotRequired[str]
|
||||
"""ID of the audio file, e.g., from a file storage system."""
|
||||
"""Reference to the audio file in an external file storage system.
|
||||
|
||||
For example, OpenAI or Anthropic's Files API.
|
||||
"""
|
||||
|
||||
mime_type: NotRequired[str]
|
||||
"""MIME type of the audio. Required for base64.
|
||||
"""MIME type of the audio.
|
||||
|
||||
Required for base64 data.
|
||||
|
||||
[Examples from IANA](https://www.iana.org/assignments/media-types/media-types.xhtml#audio)
|
||||
|
||||
"""
|
||||
|
||||
index: NotRequired[int | str]
|
||||
@@ -647,42 +651,49 @@ class PlainTextContentBlock(TypedDict):
|
||||
"""Plaintext data (e.g., from a `.txt` or `.md` document).
|
||||
|
||||
!!! note
|
||||
|
||||
A `PlainTextContentBlock` existed in `langchain-core<1.0.0`. Although the
|
||||
name has carried over, the structure has changed significantly. The only shared
|
||||
keys between the old and new versions are `type` and `text`, though the
|
||||
`type` value has changed from `'text'` to `'text-plain'`.
|
||||
|
||||
!!! note
|
||||
|
||||
Title and context are optional fields that may be passed to the model. See
|
||||
Anthropic [example](https://platform.claude.com/docs/en/build-with-claude/citations#citable-vs-non-citable-content).
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_plaintext_block` may also be used as a factory to create a
|
||||
`PlainTextContentBlock`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["text-plain"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
file_id: NotRequired[str]
|
||||
"""ID of the plaintext file, e.g., from a file storage system."""
|
||||
"""Reference to the plaintext file in an external file storage system.
|
||||
|
||||
For example, OpenAI or Anthropic's Files API.
|
||||
"""
|
||||
|
||||
mime_type: Literal["text/plain"]
|
||||
"""MIME type of the file. Required for base64."""
|
||||
"""MIME type of the file.
|
||||
|
||||
Required for base64 data.
|
||||
"""
|
||||
|
||||
index: NotRequired[int | str]
|
||||
"""Index of block in aggregate response. Used during streaming."""
|
||||
@@ -717,35 +728,44 @@ class FileContentBlock(TypedDict):
|
||||
`PlainTextContentBlock`).
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_file_block` may also be used as a factory to create a
|
||||
`FileContentBlock`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["file"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Used for tracking and referencing specific blocks (e.g., during streaming).
|
||||
|
||||
Not to be confused with `file_id`, which references an external file in a
|
||||
storage system.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
file_id: NotRequired[str]
|
||||
"""ID of the file, e.g., from a file storage system."""
|
||||
"""Reference to the file in an external file storage system.
|
||||
|
||||
For example, a file ID from OpenAI's Files API or another cloud storage provider.
|
||||
This is distinct from `id`, which identifies the content block itself.
|
||||
"""
|
||||
|
||||
mime_type: NotRequired[str]
|
||||
"""MIME type of the file. Required for base64.
|
||||
"""MIME type of the file.
|
||||
|
||||
Required for base64 data.
|
||||
|
||||
[Examples from IANA](https://www.iana.org/assignments/media-types/media-types.xhtml)
|
||||
|
||||
"""
|
||||
|
||||
index: NotRequired[int | str]
|
||||
@@ -780,25 +800,24 @@ class NonStandardContentBlock(TypedDict):
|
||||
`value` field.
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`create_non_standard_block` may also be used as a factory to create a
|
||||
`NonStandardContentBlock`. Benefits include:
|
||||
|
||||
* Automatic ID generation (when not provided)
|
||||
* Required arguments strictly validated at creation time
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["non_standard"]
|
||||
"""Type of the content block. Used for discrimination."""
|
||||
|
||||
id: NotRequired[str]
|
||||
"""Content block identifier.
|
||||
"""Unique identifier for this content block.
|
||||
|
||||
Either:
|
||||
|
||||
- Generated by the provider (e.g., OpenAI's file ID)
|
||||
- Generated by the provider
|
||||
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
|
||||
|
||||
"""
|
||||
|
||||
value: dict[str, Any]
|
||||
@@ -855,7 +874,7 @@ KNOWN_BLOCK_TYPES = {
|
||||
"non_standard",
|
||||
# citation and non_standard_annotation intentionally omitted
|
||||
}
|
||||
"""These are block types known to `langchain-core>=1.0.0`.
|
||||
"""These are block types known to `langchain-core >= 1.0.0`.
|
||||
|
||||
If a block has a type not in this set, it is considered to be provider-specific.
|
||||
"""
|
||||
@@ -895,7 +914,6 @@ def is_data_content_block(block: dict) -> bool:
|
||||
|
||||
Returns:
|
||||
`True` if the content block is a data content block, `False` otherwise.
|
||||
|
||||
"""
|
||||
if block.get("type") not in _get_data_content_block_types():
|
||||
return False
|
||||
@@ -940,17 +958,21 @@ def create_text_block(
|
||||
|
||||
Args:
|
||||
text: The text content of the block.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
annotations: `Citation`s and other annotations for the text.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `TextContentBlock`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
block = TextContentBlock(
|
||||
type="text",
|
||||
@@ -985,9 +1007,15 @@ def create_image_block(
|
||||
url: URL of the image.
|
||||
base64: Base64-encoded image data.
|
||||
file_id: ID of the image file from a file storage system.
|
||||
mime_type: MIME type of the image. Required for base64 data.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
mime_type: MIME type of the image.
|
||||
|
||||
Required for base64 data.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `ImageContentBlock`.
|
||||
@@ -997,9 +1025,9 @@ def create_image_block(
|
||||
`mime_type`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
if not any([url, base64, file_id]):
|
||||
msg = "Must provide one of: url, base64, or file_id"
|
||||
@@ -1041,9 +1069,15 @@ def create_video_block(
|
||||
url: URL of the video.
|
||||
base64: Base64-encoded video data.
|
||||
file_id: ID of the video file from a file storage system.
|
||||
mime_type: MIME type of the video. Required for base64 data.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
mime_type: MIME type of the video.
|
||||
|
||||
Required for base64 data.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `VideoContentBlock`.
|
||||
@@ -1053,9 +1087,9 @@ def create_video_block(
|
||||
`mime_type`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
if not any([url, base64, file_id]):
|
||||
msg = "Must provide one of: url, base64, or file_id"
|
||||
@@ -1101,9 +1135,15 @@ def create_audio_block(
|
||||
url: URL of the audio.
|
||||
base64: Base64-encoded audio data.
|
||||
file_id: ID of the audio file from a file storage system.
|
||||
mime_type: MIME type of the audio. Required for base64 data.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
mime_type: MIME type of the audio.
|
||||
|
||||
Required for base64 data.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `AudioContentBlock`.
|
||||
@@ -1113,9 +1153,9 @@ def create_audio_block(
|
||||
`mime_type`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
if not any([url, base64, file_id]):
|
||||
msg = "Must provide one of: url, base64, or file_id"
|
||||
@@ -1161,9 +1201,15 @@ def create_file_block(
|
||||
url: URL of the file.
|
||||
base64: Base64-encoded file data.
|
||||
file_id: ID of the file from a file storage system.
|
||||
mime_type: MIME type of the file. Required for base64 data.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
mime_type: MIME type of the file.
|
||||
|
||||
Required for base64 data.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `FileContentBlock`.
|
||||
@@ -1173,9 +1219,9 @@ def create_file_block(
|
||||
`mime_type`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
if not any([url, base64, file_id]):
|
||||
msg = "Must provide one of: url, base64, or file_id"
|
||||
@@ -1225,16 +1271,20 @@ def create_plaintext_block(
|
||||
file_id: ID of the plaintext file from a file storage system.
|
||||
title: Title of the text data.
|
||||
context: Context or description of the text content.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `PlainTextContentBlock`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
block = PlainTextContentBlock(
|
||||
type="text-plain",
|
||||
@@ -1277,16 +1327,20 @@ def create_tool_call(
|
||||
Args:
|
||||
name: The name of the tool to be called.
|
||||
args: The arguments to the tool call.
|
||||
id: An identifier for the tool call. Generated automatically if not provided.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
id: An identifier for the tool call.
|
||||
|
||||
Generated automatically if not provided.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `ToolCall`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
block = ToolCall(
|
||||
type="tool_call",
|
||||
@@ -1315,16 +1369,20 @@ def create_reasoning_block(
|
||||
|
||||
Args:
|
||||
reasoning: The reasoning text or thought summary.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `ReasoningContentBlock`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
block = ReasoningContentBlock(
|
||||
type="reasoning",
|
||||
@@ -1360,15 +1418,17 @@ def create_citation(
|
||||
start_index: Start index in the response text where citation applies.
|
||||
end_index: End index in the response text where citation applies.
|
||||
cited_text: Excerpt of source text being cited.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
|
||||
Returns:
|
||||
A properly formatted `Citation`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
block = Citation(type="citation", id=ensure_id(id))
|
||||
|
||||
@@ -1400,16 +1460,20 @@ def create_non_standard_block(
|
||||
|
||||
Args:
|
||||
value: Provider-specific content data.
|
||||
id: Content block identifier. Generated automatically if not provided.
|
||||
index: Index of block in aggregate response. Used during streaming.
|
||||
id: Content block identifier.
|
||||
|
||||
Generated automatically if not provided.
|
||||
index: Index of block in aggregate response.
|
||||
|
||||
Used during streaming.
|
||||
|
||||
Returns:
|
||||
A properly formatted `NonStandardContentBlock`.
|
||||
|
||||
!!! note
|
||||
|
||||
The `id` is generated automatically if not provided, using a UUID4 format
|
||||
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
|
||||
|
||||
"""
|
||||
block = NonStandardContentBlock(
|
||||
type="non_standard",
|
||||
|
||||
@@ -214,20 +214,29 @@ class ToolCall(TypedDict):
|
||||
This represents a request to call the tool named `'foo'` with arguments
|
||||
`{"a": 1}` and an identifier of `'123'`.
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`tool_call` may also be used as a factory to create a `ToolCall`. Benefits
|
||||
include:
|
||||
|
||||
* Required arguments strictly validated at creation time
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""The name of the tool to be called."""
|
||||
|
||||
args: dict[str, Any]
|
||||
"""The arguments to the tool call."""
|
||||
"""The arguments to the tool call as a dictionary."""
|
||||
|
||||
id: str | None
|
||||
"""An identifier associated with the tool call.
|
||||
|
||||
An identifier is needed to associate a tool call request with a tool
|
||||
call result in events when multiple concurrent tool calls are made.
|
||||
|
||||
"""
|
||||
|
||||
type: NotRequired[Literal["tool_call"]]
|
||||
"""Used for discrimination."""
|
||||
|
||||
|
||||
def tool_call(
|
||||
@@ -240,7 +249,7 @@ def tool_call(
|
||||
|
||||
Args:
|
||||
name: The name of the tool to be called.
|
||||
args: The arguments to the tool call.
|
||||
args: The arguments to the tool call as a dictionary.
|
||||
id: An identifier associated with the tool call.
|
||||
|
||||
Returns:
|
||||
@@ -252,9 +261,9 @@ def tool_call(
|
||||
class ToolCallChunk(TypedDict):
|
||||
"""A chunk of a tool call (yielded when streaming).
|
||||
|
||||
When merging `ToolCallChunk`s (e.g., via `AIMessageChunk.__add__`),
|
||||
all string attributes are concatenated. Chunks are only merged if their
|
||||
values of `index` are equal and not None.
|
||||
When merging `ToolCallChunk` objects (e.g., via `AIMessageChunk.__add__`), all
|
||||
string attributes are concatenated. Chunks are only merged if their values of
|
||||
`index` are equal and not `None`.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -270,13 +279,25 @@ class ToolCallChunk(TypedDict):
|
||||
|
||||
name: str | None
|
||||
"""The name of the tool to be called."""
|
||||
|
||||
args: str | None
|
||||
"""The arguments to the tool call."""
|
||||
"""The arguments to the tool call as a JSON-parseable string."""
|
||||
|
||||
id: str | None
|
||||
"""An identifier associated with the tool call."""
|
||||
"""An identifier associated with the tool call.
|
||||
|
||||
An identifier is needed to associate a tool call request with a tool
|
||||
call result in events when multiple concurrent tool calls are made.
|
||||
"""
|
||||
|
||||
index: int | None
|
||||
"""The index of the tool call in a sequence."""
|
||||
"""The index of the tool call in a sequence.
|
||||
|
||||
Used for merging chunks.
|
||||
"""
|
||||
|
||||
type: NotRequired[Literal["tool_call_chunk"]]
|
||||
"""Used for discrimination."""
|
||||
|
||||
|
||||
def tool_call_chunk(
|
||||
@@ -290,7 +311,7 @@ def tool_call_chunk(
|
||||
|
||||
Args:
|
||||
name: The name of the tool to be called.
|
||||
args: The arguments to the tool call.
|
||||
args: The arguments to the tool call as a JSON string.
|
||||
id: An identifier associated with the tool call.
|
||||
index: The index of the tool call in a sequence.
|
||||
|
||||
@@ -313,7 +334,7 @@ def invalid_tool_call(
|
||||
|
||||
Args:
|
||||
name: The name of the tool to be called.
|
||||
args: The arguments to the tool call.
|
||||
args: The arguments to the tool call as a JSON string.
|
||||
id: An identifier associated with the tool call.
|
||||
error: An error message associated with the tool call.
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from typing import (
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from xml.sax.saxutils import escape, quoteattr
|
||||
|
||||
from pydantic import Discriminator, Field, Tag
|
||||
|
||||
@@ -65,14 +66,19 @@ logger = logging.getLogger(__name__)
|
||||
def _get_type(v: Any) -> str:
|
||||
"""Get the type associated with the object for serialization purposes."""
|
||||
if isinstance(v, dict) and "type" in v:
|
||||
return v["type"]
|
||||
if hasattr(v, "type"):
|
||||
return v.type
|
||||
msg = (
|
||||
f"Expected either a dictionary with a 'type' key or an object "
|
||||
f"with a 'type' attribute. Instead got type {type(v)}."
|
||||
)
|
||||
raise TypeError(msg)
|
||||
result = v["type"]
|
||||
elif hasattr(v, "type"):
|
||||
result = v.type
|
||||
else:
|
||||
msg = (
|
||||
f"Expected either a dictionary with a 'type' key or an object "
|
||||
f"with a 'type' attribute. Instead got type {type(v)}."
|
||||
)
|
||||
raise TypeError(msg)
|
||||
if not isinstance(result, str):
|
||||
msg = f"Expected 'type' to be a str, got {type(result).__name__}"
|
||||
raise TypeError(msg)
|
||||
return result
|
||||
|
||||
|
||||
AnyMessage = Annotated[
|
||||
@@ -93,8 +99,199 @@ AnyMessage = Annotated[
|
||||
"""A type representing any defined `Message` or `MessageChunk` type."""
|
||||
|
||||
|
||||
def _has_base64_data(block: dict) -> bool:
|
||||
"""Check if a content block contains base64 encoded data.
|
||||
|
||||
Args:
|
||||
block: A content block dictionary.
|
||||
|
||||
Returns:
|
||||
Whether the block contains base64 data.
|
||||
"""
|
||||
# Check for explicit base64 field (standard content blocks)
|
||||
if block.get("base64"):
|
||||
return True
|
||||
|
||||
# Check for data: URL in url field
|
||||
url = block.get("url", "")
|
||||
if isinstance(url, str) and url.startswith("data:"):
|
||||
return True
|
||||
|
||||
# Check for OpenAI-style image_url with data: URL
|
||||
image_url = block.get("image_url", {})
|
||||
if isinstance(image_url, dict):
|
||||
url = image_url.get("url", "")
|
||||
if isinstance(url, str) and url.startswith("data:"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
_XML_CONTENT_BLOCK_MAX_LEN = 500
|
||||
|
||||
|
||||
def _truncate(text: str, max_len: int = _XML_CONTENT_BLOCK_MAX_LEN) -> str:
|
||||
"""Truncate text to `max_len` characters, adding ellipsis if truncated."""
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return text[:max_len] + "..."
|
||||
|
||||
|
||||
def _format_content_block_xml(block: dict) -> str | None:
|
||||
"""Format a content block as XML.
|
||||
|
||||
Args:
|
||||
block: A LangChain content block.
|
||||
|
||||
Returns:
|
||||
XML string representation of the block, or `None` if the block should be
|
||||
skipped.
|
||||
|
||||
Note:
|
||||
Plain text document content, server tool call arguments, and server tool
|
||||
result outputs are truncated to 500 characters.
|
||||
"""
|
||||
block_type = block.get("type", "")
|
||||
|
||||
# Skip blocks with base64 encoded data
|
||||
if _has_base64_data(block):
|
||||
return None
|
||||
|
||||
# Text blocks
|
||||
if block_type == "text":
|
||||
text = block.get("text", "")
|
||||
return escape(text) if text else None
|
||||
|
||||
# Reasoning blocks
|
||||
if block_type == "reasoning":
|
||||
reasoning = block.get("reasoning", "")
|
||||
if reasoning:
|
||||
return f"<reasoning>{escape(reasoning)}</reasoning>"
|
||||
return None
|
||||
|
||||
# Image blocks (URL only, base64 already filtered)
|
||||
if block_type == "image":
|
||||
url = block.get("url")
|
||||
file_id = block.get("file_id")
|
||||
if url:
|
||||
return f"<image url={quoteattr(url)} />"
|
||||
if file_id:
|
||||
return f"<image file_id={quoteattr(file_id)} />"
|
||||
return None
|
||||
|
||||
# OpenAI-style image_url blocks
|
||||
if block_type == "image_url":
|
||||
image_url = block.get("image_url", {})
|
||||
if isinstance(image_url, dict):
|
||||
url = image_url.get("url", "")
|
||||
if url and not url.startswith("data:"):
|
||||
return f"<image url={quoteattr(url)} />"
|
||||
return None
|
||||
|
||||
# Audio blocks (URL only)
|
||||
if block_type == "audio":
|
||||
url = block.get("url")
|
||||
file_id = block.get("file_id")
|
||||
if url:
|
||||
return f"<audio url={quoteattr(url)} />"
|
||||
if file_id:
|
||||
return f"<audio file_id={quoteattr(file_id)} />"
|
||||
return None
|
||||
|
||||
# Video blocks (URL only)
|
||||
if block_type == "video":
|
||||
url = block.get("url")
|
||||
file_id = block.get("file_id")
|
||||
if url:
|
||||
return f"<video url={quoteattr(url)} />"
|
||||
if file_id:
|
||||
return f"<video file_id={quoteattr(file_id)} />"
|
||||
return None
|
||||
|
||||
# Plain text document blocks
|
||||
if block_type == "text-plain":
|
||||
text = block.get("text", "")
|
||||
return escape(_truncate(text)) if text else None
|
||||
|
||||
# Server tool call blocks (from AI messages)
|
||||
if block_type == "server_tool_call":
|
||||
tc_id = quoteattr(str(block.get("id") or ""))
|
||||
tc_name = quoteattr(str(block.get("name") or ""))
|
||||
tc_args_json = json.dumps(block.get("args", {}), ensure_ascii=False)
|
||||
tc_args = escape(_truncate(tc_args_json))
|
||||
return (
|
||||
f"<server_tool_call id={tc_id} name={tc_name}>{tc_args}</server_tool_call>"
|
||||
)
|
||||
|
||||
# Server tool result blocks
|
||||
if block_type == "server_tool_result":
|
||||
tool_call_id = quoteattr(str(block.get("tool_call_id") or ""))
|
||||
status = quoteattr(str(block.get("status") or ""))
|
||||
output = block.get("output")
|
||||
if output:
|
||||
output_json = json.dumps(output, ensure_ascii=False)
|
||||
output_str = escape(_truncate(output_json))
|
||||
else:
|
||||
output_str = ""
|
||||
return (
|
||||
f"<server_tool_result tool_call_id={tool_call_id} status={status}>"
|
||||
f"{output_str}</server_tool_result>"
|
||||
)
|
||||
|
||||
# Unknown block type - skip silently
|
||||
return None
|
||||
|
||||
|
||||
def _get_message_type_str(
|
||||
m: BaseMessage,
|
||||
human_prefix: str,
|
||||
ai_prefix: str,
|
||||
system_prefix: str,
|
||||
function_prefix: str,
|
||||
tool_prefix: str,
|
||||
) -> str:
|
||||
"""Get the type string for XML message element.
|
||||
|
||||
Args:
|
||||
m: The message to get the type string for.
|
||||
human_prefix: The prefix to use for `HumanMessage`.
|
||||
ai_prefix: The prefix to use for `AIMessage`.
|
||||
system_prefix: The prefix to use for `SystemMessage`.
|
||||
function_prefix: The prefix to use for `FunctionMessage`.
|
||||
tool_prefix: The prefix to use for `ToolMessage`.
|
||||
|
||||
Returns:
|
||||
The type string for the message element.
|
||||
|
||||
Raises:
|
||||
ValueError: If an unsupported message type is encountered.
|
||||
"""
|
||||
if isinstance(m, HumanMessage):
|
||||
return human_prefix.lower()
|
||||
if isinstance(m, AIMessage):
|
||||
return ai_prefix.lower()
|
||||
if isinstance(m, SystemMessage):
|
||||
return system_prefix.lower()
|
||||
if isinstance(m, FunctionMessage):
|
||||
return function_prefix.lower()
|
||||
if isinstance(m, ToolMessage):
|
||||
return tool_prefix.lower()
|
||||
if isinstance(m, ChatMessage):
|
||||
return m.role
|
||||
msg = f"Got unsupported message type: {m}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def get_buffer_string(
|
||||
messages: Sequence[BaseMessage], human_prefix: str = "Human", ai_prefix: str = "AI"
|
||||
messages: Sequence[BaseMessage],
|
||||
human_prefix: str = "Human",
|
||||
ai_prefix: str = "AI",
|
||||
*,
|
||||
system_prefix: str = "System",
|
||||
function_prefix: str = "Function",
|
||||
tool_prefix: str = "Tool",
|
||||
message_separator: str = "\n",
|
||||
format: Literal["prefix", "xml"] = "prefix", # noqa: A002
|
||||
) -> str:
|
||||
r"""Convert a sequence of messages to strings and concatenate them into one string.
|
||||
|
||||
@@ -102,6 +299,15 @@ def get_buffer_string(
|
||||
messages: Messages to be converted to strings.
|
||||
human_prefix: The prefix to prepend to contents of `HumanMessage`s.
|
||||
ai_prefix: The prefix to prepend to contents of `AIMessage`.
|
||||
system_prefix: The prefix to prepend to contents of `SystemMessage`s.
|
||||
function_prefix: The prefix to prepend to contents of `FunctionMessage`s.
|
||||
tool_prefix: The prefix to prepend to contents of `ToolMessage`s.
|
||||
message_separator: The separator to use between messages.
|
||||
format: The output format. `'prefix'` uses `Role: content` format (default).
|
||||
|
||||
`'xml'` uses XML-style `<message type='role'>` format with proper character
|
||||
escaping, which is useful when message content may contain role-like
|
||||
prefixes that could cause ambiguity.
|
||||
|
||||
Returns:
|
||||
A single string concatenation of all input messages.
|
||||
@@ -109,9 +315,38 @@ def get_buffer_string(
|
||||
Raises:
|
||||
ValueError: If an unsupported message type is encountered.
|
||||
|
||||
Note:
|
||||
If a message is an `AIMessage` and contains both tool calls under `tool_calls`
|
||||
and a function call under `additional_kwargs["function_call"]`, only the tool
|
||||
calls will be appended to the string representation.
|
||||
|
||||
When using `format='xml'`:
|
||||
|
||||
- All messages use uniform `<message type="role">content</message>` format.
|
||||
- The `type` attribute uses `human_prefix` (lowercased) for `HumanMessage`,
|
||||
`ai_prefix` (lowercased) for `AIMessage`, `system_prefix` (lowercased)
|
||||
for `SystemMessage`, `function_prefix` (lowercased) for `FunctionMessage`,
|
||||
`tool_prefix` (lowercased) for `ToolMessage`, and the original role
|
||||
(unchanged) for `ChatMessage`.
|
||||
- Message content is escaped using `xml.sax.saxutils.escape()`.
|
||||
- Attribute values are escaped using `xml.sax.saxutils.quoteattr()`.
|
||||
- AI messages with tool calls use nested structure with `<content>` and
|
||||
`<tool_call>` elements.
|
||||
- For multi-modal content (list of content blocks), supported block types
|
||||
are: `text`, `reasoning`, `image` (URL/file_id only), `image_url`
|
||||
(OpenAI-style, URL only), `audio` (URL/file_id only), `video` (URL/file_id
|
||||
only), `text-plain`, `server_tool_call`, and `server_tool_result`.
|
||||
- Content blocks with base64-encoded data are skipped (including blocks
|
||||
with `base64` field or `data:` URLs).
|
||||
- Unknown block types are skipped.
|
||||
- Plain text document content (`text-plain`), server tool call arguments,
|
||||
and server tool result outputs are truncated to 500 characters.
|
||||
|
||||
Example:
|
||||
Default prefix format:
|
||||
|
||||
```python
|
||||
from langchain_core import AIMessage, HumanMessage
|
||||
from langchain_core.messages import AIMessage, HumanMessage, get_buffer_string
|
||||
|
||||
messages = [
|
||||
HumanMessage(content="Hi, how are you?"),
|
||||
@@ -120,7 +355,54 @@ def get_buffer_string(
|
||||
get_buffer_string(messages)
|
||||
# -> "Human: Hi, how are you?\nAI: Good, how are you?"
|
||||
```
|
||||
|
||||
XML format (useful when content contains role-like prefixes):
|
||||
|
||||
```python
|
||||
messages = [
|
||||
HumanMessage(content="Example: Human: some text"),
|
||||
AIMessage(content="I see the example."),
|
||||
]
|
||||
get_buffer_string(messages, format="xml")
|
||||
# -> '<message type="human">Example: Human: some text</message>\\n'
|
||||
# -> '<message type="ai">I see the example.</message>'
|
||||
```
|
||||
|
||||
XML format with special characters (automatically escaped):
|
||||
|
||||
```python
|
||||
messages = [
|
||||
HumanMessage(content="Is 5 < 10 & 10 > 5?"),
|
||||
]
|
||||
get_buffer_string(messages, format="xml")
|
||||
# -> '<message type="human">Is 5 < 10 & 10 > 5?</message>'
|
||||
```
|
||||
|
||||
XML format with tool calls:
|
||||
|
||||
```python
|
||||
messages = [
|
||||
AIMessage(
|
||||
content="I'll search for that.",
|
||||
tool_calls=[
|
||||
{"id": "call_123", "name": "search", "args": {"query": "weather"}}
|
||||
],
|
||||
),
|
||||
]
|
||||
get_buffer_string(messages, format="xml")
|
||||
# -> '<message type="ai">\\n'
|
||||
# -> ' <content>I\\'ll search for that.</content>\\n'
|
||||
# -> ' <tool_call id="call_123" name="search">'
|
||||
# -> '{"query": "weather"}</tool_call>\\n'
|
||||
# -> '</message>'
|
||||
```
|
||||
"""
|
||||
if format not in ("prefix", "xml"):
|
||||
msg = (
|
||||
f"Unrecognized format={format!r}. Supported formats are 'prefix' and 'xml'."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
string_messages = []
|
||||
for m in messages:
|
||||
if isinstance(m, HumanMessage):
|
||||
@@ -128,22 +410,96 @@ def get_buffer_string(
|
||||
elif isinstance(m, AIMessage):
|
||||
role = ai_prefix
|
||||
elif isinstance(m, SystemMessage):
|
||||
role = "System"
|
||||
role = system_prefix
|
||||
elif isinstance(m, FunctionMessage):
|
||||
role = "Function"
|
||||
role = function_prefix
|
||||
elif isinstance(m, ToolMessage):
|
||||
role = "Tool"
|
||||
role = tool_prefix
|
||||
elif isinstance(m, ChatMessage):
|
||||
role = m.role
|
||||
else:
|
||||
msg = f"Got unsupported message type: {m}"
|
||||
raise ValueError(msg) # noqa: TRY004
|
||||
message = f"{role}: {m.text}"
|
||||
if isinstance(m, AIMessage) and "function_call" in m.additional_kwargs:
|
||||
message += f"{m.additional_kwargs['function_call']}"
|
||||
|
||||
if format == "xml":
|
||||
msg_type = _get_message_type_str(
|
||||
m, human_prefix, ai_prefix, system_prefix, function_prefix, tool_prefix
|
||||
)
|
||||
|
||||
# Format content blocks
|
||||
if isinstance(m.content, str):
|
||||
content_parts = [escape(m.content)] if m.content else []
|
||||
else:
|
||||
# List of content blocks
|
||||
content_parts = []
|
||||
for block in m.content:
|
||||
if isinstance(block, str):
|
||||
if block:
|
||||
content_parts.append(escape(block))
|
||||
else:
|
||||
formatted = _format_content_block_xml(block)
|
||||
if formatted:
|
||||
content_parts.append(formatted)
|
||||
|
||||
# Check if this is an AIMessage with tool calls
|
||||
has_tool_calls = isinstance(m, AIMessage) and m.tool_calls
|
||||
has_function_call = (
|
||||
isinstance(m, AIMessage)
|
||||
and not m.tool_calls
|
||||
and "function_call" in m.additional_kwargs
|
||||
)
|
||||
|
||||
if has_tool_calls or has_function_call:
|
||||
# Use nested structure for AI messages with tool calls
|
||||
# Type narrowing: at this point m is AIMessage (verified above)
|
||||
ai_msg = cast("AIMessage", m)
|
||||
parts = [f"<message type={quoteattr(msg_type)}>"]
|
||||
if content_parts:
|
||||
parts.append(f" <content>{' '.join(content_parts)}</content>")
|
||||
|
||||
if has_tool_calls:
|
||||
for tc in ai_msg.tool_calls:
|
||||
tc_id = quoteattr(str(tc.get("id") or ""))
|
||||
tc_name = quoteattr(str(tc.get("name") or ""))
|
||||
tc_args = escape(
|
||||
json.dumps(tc.get("args", {}), ensure_ascii=False)
|
||||
)
|
||||
parts.append(
|
||||
f" <tool_call id={tc_id} name={tc_name}>"
|
||||
f"{tc_args}</tool_call>"
|
||||
)
|
||||
elif has_function_call:
|
||||
fc = ai_msg.additional_kwargs["function_call"]
|
||||
fc_name = quoteattr(str(fc.get("name") or ""))
|
||||
fc_args = escape(str(fc.get("arguments") or "{}"))
|
||||
parts.append(
|
||||
f" <function_call name={fc_name}>{fc_args}</function_call>"
|
||||
)
|
||||
|
||||
parts.append("</message>")
|
||||
message = "\n".join(parts)
|
||||
else:
|
||||
# Simple structure for messages without tool calls
|
||||
joined_content = " ".join(content_parts)
|
||||
message = (
|
||||
f"<message type={quoteattr(msg_type)}>{joined_content}</message>"
|
||||
)
|
||||
else: # format == "prefix"
|
||||
content = m.text
|
||||
message = f"{role}: {content}"
|
||||
tool_info = ""
|
||||
if isinstance(m, AIMessage):
|
||||
if m.tool_calls:
|
||||
tool_info = str(m.tool_calls)
|
||||
elif "function_call" in m.additional_kwargs:
|
||||
# Legacy behavior assumes only one function call per message
|
||||
tool_info = str(m.additional_kwargs["function_call"])
|
||||
if tool_info:
|
||||
message += tool_info # Preserve original behavior
|
||||
|
||||
string_messages.append(message)
|
||||
|
||||
return "\n".join(string_messages)
|
||||
return message_separator.join(string_messages)
|
||||
|
||||
|
||||
def _message_from_dict(message: dict) -> BaseMessage:
|
||||
@@ -206,8 +562,11 @@ def message_chunk_to_message(chunk: BaseMessage) -> BaseMessage:
|
||||
ignore_keys = ["type"]
|
||||
if isinstance(chunk, AIMessageChunk):
|
||||
ignore_keys.extend(["tool_call_chunks", "chunk_position"])
|
||||
return chunk.__class__.__mro__[1](
|
||||
**{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}
|
||||
return cast(
|
||||
"BaseMessage",
|
||||
chunk.__class__.__mro__[1](
|
||||
**{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -229,13 +588,13 @@ def _create_message_from_message_type(
|
||||
"""Create a message from a `Message` type and content string.
|
||||
|
||||
Args:
|
||||
message_type: (str) the type of the message (e.g., `'human'`, `'ai'`, etc.).
|
||||
content: (str) the content string.
|
||||
name: (str) the name of the message.
|
||||
tool_call_id: (str) the tool call id.
|
||||
tool_calls: (list[dict[str, Any]]) the tool calls.
|
||||
id: (str) the id of the message.
|
||||
additional_kwargs: (dict[str, Any]) additional keyword arguments.
|
||||
message_type: the type of the message (e.g., `'human'`, `'ai'`, etc.).
|
||||
content: the content string.
|
||||
name: the name of the message.
|
||||
tool_call_id: the tool call id.
|
||||
tool_calls: the tool calls.
|
||||
id: the id of the message.
|
||||
additional_kwargs: additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
a message of the appropriate type.
|
||||
@@ -539,6 +898,7 @@ def filter_messages(
|
||||
):
|
||||
continue
|
||||
|
||||
new_msg = msg
|
||||
if isinstance(exclude_tool_calls, (list, tuple, set)):
|
||||
if isinstance(msg, AIMessage) and msg.tool_calls:
|
||||
tool_calls = [
|
||||
@@ -562,7 +922,7 @@ def filter_messages(
|
||||
)
|
||||
]
|
||||
|
||||
msg = msg.model_copy( # noqa: PLW2901
|
||||
new_msg = msg.model_copy(
|
||||
update={"tool_calls": tool_calls, "content": content}
|
||||
)
|
||||
elif (
|
||||
@@ -573,11 +933,11 @@ def filter_messages(
|
||||
# default to inclusion when no inclusion criteria given.
|
||||
if (
|
||||
not (include_types or include_ids or include_names)
|
||||
or (include_names and msg.name in include_names)
|
||||
or (include_types and _is_message_type(msg, include_types))
|
||||
or (include_ids and msg.id in include_ids)
|
||||
or (include_names and new_msg.name in include_names)
|
||||
or (include_types and _is_message_type(new_msg, include_types))
|
||||
or (include_ids and new_msg.id in include_ids)
|
||||
):
|
||||
filtered.append(msg)
|
||||
filtered.append(new_msg)
|
||||
|
||||
return filtered
|
||||
|
||||
@@ -720,7 +1080,8 @@ def trim_messages(
|
||||
max_tokens: int,
|
||||
token_counter: Callable[[list[BaseMessage]], int]
|
||||
| Callable[[BaseMessage], int]
|
||||
| BaseLanguageModel,
|
||||
| BaseLanguageModel
|
||||
| Literal["approximate"],
|
||||
strategy: Literal["first", "last"] = "last",
|
||||
allow_partial: bool = False,
|
||||
end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,
|
||||
@@ -758,53 +1119,65 @@ def trim_messages(
|
||||
messages: Sequence of Message-like objects to trim.
|
||||
max_tokens: Max token count of trimmed messages.
|
||||
token_counter: Function or llm for counting tokens in a `BaseMessage` or a
|
||||
list of `BaseMessage`. If a `BaseLanguageModel` is passed in then
|
||||
`BaseLanguageModel.get_num_tokens_from_messages()` will be used.
|
||||
Set to `len` to count the number of **messages** in the chat history.
|
||||
list of `BaseMessage`.
|
||||
|
||||
If a `BaseLanguageModel` is passed in then
|
||||
`BaseLanguageModel.get_num_tokens_from_messages()` will be used. Set to
|
||||
`len` to count the number of **messages** in the chat history.
|
||||
|
||||
You can also use string shortcuts for convenience:
|
||||
|
||||
- `'approximate'`: Uses `count_tokens_approximately` for fast, approximate
|
||||
token counts.
|
||||
|
||||
!!! note
|
||||
|
||||
Use `count_tokens_approximately` to get fast, approximate token
|
||||
counts.
|
||||
|
||||
This is recommended for using `trim_messages` on the hot path, where
|
||||
exact token counting is not necessary.
|
||||
`count_tokens_approximately` (or the shortcut `'approximate'`) is
|
||||
recommended for using `trim_messages` on the hot path, where exact token
|
||||
counting is not necessary.
|
||||
|
||||
strategy: Strategy for trimming.
|
||||
|
||||
- `'first'`: Keep the first `<= n_count` tokens of the messages.
|
||||
- `'last'`: Keep the last `<= n_count` tokens of the messages.
|
||||
allow_partial: Whether to split a message if only part of the message can be
|
||||
included. If `strategy='last'` then the last partial contents of a message
|
||||
are included. If `strategy='first'` then the first partial contents of a
|
||||
message are included.
|
||||
end_on: The message type to end on. If specified then every message after the
|
||||
last occurrence of this type is ignored. If `strategy='last'` then this
|
||||
is done before we attempt to get the last `max_tokens`. If
|
||||
`strategy='first'` then this is done after we get the first
|
||||
`max_tokens`. Can be specified as string names (e.g. `'system'`,
|
||||
`'human'`, `'ai'`, ...) or as `BaseMessage` classes (e.g.
|
||||
`SystemMessage`, `HumanMessage`, `AIMessage`, ...). Can be a single
|
||||
type or a list of types.
|
||||
included.
|
||||
|
||||
start_on: The message type to start on. Should only be specified if
|
||||
`strategy='last'`. If specified then every message before
|
||||
the first occurrence of this type is ignored. This is done after we trim
|
||||
the initial messages to the last `max_tokens`. Does not
|
||||
apply to a `SystemMessage` at index 0 if `include_system=True`. Can be
|
||||
specified as string names (e.g. `'system'`, `'human'`, `'ai'`, ...) or
|
||||
as `BaseMessage` classes (e.g. `SystemMessage`, `HumanMessage`,
|
||||
`AIMessage`, ...). Can be a single type or a list of types.
|
||||
If `strategy='last'` then the last partial contents of a message are
|
||||
included. If `strategy='first'` then the first partial contents of a
|
||||
message are included.
|
||||
end_on: The message type to end on.
|
||||
|
||||
If specified then every message after the last occurrence of this type is
|
||||
ignored. If `strategy='last'` then this is done before we attempt to get the
|
||||
last `max_tokens`. If `strategy='first'` then this is done after we get the
|
||||
first `max_tokens`. Can be specified as string names (e.g. `'system'`,
|
||||
`'human'`, `'ai'`, ...) or as `BaseMessage` classes (e.g. `SystemMessage`,
|
||||
`HumanMessage`, `AIMessage`, ...). Can be a single type or a list of types.
|
||||
|
||||
start_on: The message type to start on.
|
||||
|
||||
Should only be specified if `strategy='last'`. If specified then every
|
||||
message before the first occurrence of this type is ignored. This is done
|
||||
after we trim the initial messages to the last `max_tokens`. Does not apply
|
||||
to a `SystemMessage` at index 0 if `include_system=True`. Can be specified
|
||||
as string names (e.g. `'system'`, `'human'`, `'ai'`, ...) or as
|
||||
`BaseMessage` classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`,
|
||||
...). Can be a single type or a list of types.
|
||||
|
||||
include_system: Whether to keep the `SystemMessage` if there is one at index
|
||||
`0`. Should only be specified if `strategy="last"`.
|
||||
`0`.
|
||||
|
||||
Should only be specified if `strategy="last"`.
|
||||
text_splitter: Function or `langchain_text_splitters.TextSplitter` for
|
||||
splitting the string contents of a message. Only used if
|
||||
`allow_partial=True`. If `strategy='last'` then the last split tokens
|
||||
from a partial message will be included. if `strategy='first'` then the
|
||||
first split tokens from a partial message will be included. Token splitter
|
||||
assumes that separators are kept, so that split contents can be directly
|
||||
concatenated to recreate the original text. Defaults to splitting on
|
||||
newlines.
|
||||
splitting the string contents of a message.
|
||||
|
||||
Only used if `allow_partial=True`. If `strategy='last'` then the last split
|
||||
tokens from a partial message will be included. if `strategy='first'` then
|
||||
the first split tokens from a partial message will be included. Token
|
||||
splitter assumes that separators are kept, so that split contents can be
|
||||
directly concatenated to recreate the original text. Defaults to splitting
|
||||
on newlines.
|
||||
|
||||
Returns:
|
||||
List of trimmed `BaseMessage`.
|
||||
@@ -815,8 +1188,8 @@ def trim_messages(
|
||||
|
||||
Example:
|
||||
Trim chat history based on token count, keeping the `SystemMessage` if
|
||||
present, and ensuring that the chat history starts with a `HumanMessage` (
|
||||
or a `SystemMessage` followed by a `HumanMessage`).
|
||||
present, and ensuring that the chat history starts with a `HumanMessage` (or a
|
||||
`SystemMessage` followed by a `HumanMessage`).
|
||||
|
||||
```python
|
||||
from langchain_core.messages import (
|
||||
@@ -869,8 +1242,34 @@ def trim_messages(
|
||||
]
|
||||
```
|
||||
|
||||
Trim chat history using approximate token counting with `'approximate'`:
|
||||
|
||||
```python
|
||||
trim_messages(
|
||||
messages,
|
||||
max_tokens=45,
|
||||
strategy="last",
|
||||
# Using the "approximate" shortcut for fast token counting
|
||||
token_counter="approximate",
|
||||
start_on="human",
|
||||
include_system=True,
|
||||
)
|
||||
|
||||
# This is equivalent to using `count_tokens_approximately` directly
|
||||
from langchain_core.messages.utils import count_tokens_approximately
|
||||
|
||||
trim_messages(
|
||||
messages,
|
||||
max_tokens=45,
|
||||
strategy="last",
|
||||
token_counter=count_tokens_approximately,
|
||||
start_on="human",
|
||||
include_system=True,
|
||||
)
|
||||
```
|
||||
|
||||
Trim chat history based on the message count, keeping the `SystemMessage` if
|
||||
present, and ensuring that the chat history starts with a `HumanMessage` (
|
||||
present, and ensuring that the chat history starts with a HumanMessage (
|
||||
or a `SystemMessage` followed by a `HumanMessage`).
|
||||
|
||||
trim_messages(
|
||||
@@ -992,24 +1391,44 @@ def trim_messages(
|
||||
raise ValueError(msg)
|
||||
|
||||
messages = convert_to_messages(messages)
|
||||
if hasattr(token_counter, "get_num_tokens_from_messages"):
|
||||
list_token_counter = token_counter.get_num_tokens_from_messages
|
||||
elif callable(token_counter):
|
||||
|
||||
# Handle string shortcuts for token counter
|
||||
if isinstance(token_counter, str):
|
||||
if token_counter in _TOKEN_COUNTER_SHORTCUTS:
|
||||
actual_token_counter = _TOKEN_COUNTER_SHORTCUTS[token_counter]
|
||||
else:
|
||||
available_shortcuts = ", ".join(
|
||||
f"'{key}'" for key in _TOKEN_COUNTER_SHORTCUTS
|
||||
)
|
||||
msg = (
|
||||
f"Invalid token_counter shortcut '{token_counter}'. "
|
||||
f"Available shortcuts: {available_shortcuts}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
# Type narrowing: at this point token_counter is not a str
|
||||
actual_token_counter = token_counter # type: ignore[assignment]
|
||||
|
||||
if hasattr(actual_token_counter, "get_num_tokens_from_messages"):
|
||||
list_token_counter = actual_token_counter.get_num_tokens_from_messages
|
||||
elif callable(actual_token_counter):
|
||||
if (
|
||||
next(iter(inspect.signature(token_counter).parameters.values())).annotation
|
||||
next(
|
||||
iter(inspect.signature(actual_token_counter).parameters.values())
|
||||
).annotation
|
||||
is BaseMessage
|
||||
):
|
||||
|
||||
def list_token_counter(messages: Sequence[BaseMessage]) -> int:
|
||||
return sum(token_counter(msg) for msg in messages) # type: ignore[arg-type, misc]
|
||||
return sum(actual_token_counter(msg) for msg in messages) # type: ignore[arg-type, misc]
|
||||
|
||||
else:
|
||||
list_token_counter = token_counter
|
||||
list_token_counter = actual_token_counter
|
||||
else:
|
||||
msg = (
|
||||
f"'token_counter' expected to be a model that implements "
|
||||
f"'get_num_tokens_from_messages()' or a function. Received object of type "
|
||||
f"{type(token_counter)}."
|
||||
f"{type(actual_token_counter)}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
@@ -1044,11 +1463,38 @@ def trim_messages(
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
_SingleMessage = BaseMessage | str | dict[str, Any]
|
||||
_T = TypeVar("_T", bound=_SingleMessage)
|
||||
# A sequence of _SingleMessage that is NOT a bare str
|
||||
_MultipleMessages = Sequence[_T]
|
||||
|
||||
|
||||
@overload
|
||||
def convert_to_openai_messages(
|
||||
messages: _SingleMessage,
|
||||
*,
|
||||
text_format: Literal["string", "block"] = "string",
|
||||
include_id: bool = False,
|
||||
pass_through_unknown_blocks: bool = True,
|
||||
) -> dict: ...
|
||||
|
||||
|
||||
@overload
|
||||
def convert_to_openai_messages(
|
||||
messages: _MultipleMessages,
|
||||
*,
|
||||
text_format: Literal["string", "block"] = "string",
|
||||
include_id: bool = False,
|
||||
pass_through_unknown_blocks: bool = True,
|
||||
) -> list[dict]: ...
|
||||
|
||||
|
||||
def convert_to_openai_messages(
|
||||
messages: MessageLikeRepresentation | Sequence[MessageLikeRepresentation],
|
||||
*,
|
||||
text_format: Literal["string", "block"] = "string",
|
||||
include_id: bool = False,
|
||||
pass_through_unknown_blocks: bool = True,
|
||||
) -> dict | list[dict]:
|
||||
"""Convert LangChain messages into OpenAI message dicts.
|
||||
|
||||
@@ -1068,6 +1514,9 @@ def convert_to_openai_messages(
|
||||
content blocks these are left as is.
|
||||
include_id: Whether to include message IDs in the openai messages, if they
|
||||
are present in the source messages.
|
||||
pass_through_unknown_blocks: Whether to include content blocks with unknown
|
||||
formats in the output. If `False`, an error is raised if an unknown
|
||||
content block is encountered.
|
||||
|
||||
Raises:
|
||||
ValueError: if an unrecognized `text_format` is specified, or if a message
|
||||
@@ -1135,7 +1584,7 @@ def convert_to_openai_messages(
|
||||
err = f"Unrecognized {text_format=}, expected one of 'string' or 'block'."
|
||||
raise ValueError(err)
|
||||
|
||||
oai_messages: list = []
|
||||
oai_messages: list[dict] = []
|
||||
|
||||
if is_single := isinstance(messages, (BaseMessage, dict, str)):
|
||||
messages = [messages]
|
||||
@@ -1317,6 +1766,36 @@ def convert_to_openai_messages(
|
||||
},
|
||||
}
|
||||
)
|
||||
elif block.get("type") == "function_call": # OpenAI Responses
|
||||
if not any(
|
||||
tool_call["id"] == block.get("call_id")
|
||||
for tool_call in cast("AIMessage", message).tool_calls
|
||||
):
|
||||
if missing := [
|
||||
k
|
||||
for k in ("call_id", "name", "arguments")
|
||||
if k not in block
|
||||
]:
|
||||
err = (
|
||||
f"Unrecognized content block at "
|
||||
f"messages[{i}].content[{j}] has 'type': "
|
||||
f"'tool_use', but is missing expected key(s) "
|
||||
f"{missing}. Full content block:\n\n{block}"
|
||||
)
|
||||
raise ValueError(err)
|
||||
oai_msg["tool_calls"] = oai_msg.get("tool_calls", [])
|
||||
oai_msg["tool_calls"].append(
|
||||
{
|
||||
"type": "function",
|
||||
"id": block.get("call_id"),
|
||||
"function": {
|
||||
"name": block.get("name"),
|
||||
"arguments": block.get("arguments"),
|
||||
},
|
||||
}
|
||||
)
|
||||
if pass_through_unknown_blocks:
|
||||
content.append(block)
|
||||
elif block.get("type") == "tool_result":
|
||||
if missing := [
|
||||
k for k in ("content", "tool_use_id") if k not in block
|
||||
@@ -1397,7 +1876,10 @@ def convert_to_openai_messages(
|
||||
},
|
||||
}
|
||||
)
|
||||
elif block.get("type") in ["thinking", "reasoning"]:
|
||||
elif (
|
||||
block.get("type") in {"thinking", "reasoning"}
|
||||
or pass_through_unknown_blocks
|
||||
):
|
||||
content.append(block)
|
||||
else:
|
||||
err = (
|
||||
@@ -1669,7 +2151,11 @@ def _get_message_openai_role(message: BaseMessage) -> str:
|
||||
if isinstance(message, ToolMessage):
|
||||
return "tool"
|
||||
if isinstance(message, SystemMessage):
|
||||
return message.additional_kwargs.get("__openai_role__", "system")
|
||||
role = message.additional_kwargs.get("__openai_role__", "system")
|
||||
if not isinstance(role, str):
|
||||
msg = f"Expected '__openai_role__' to be a str, got {type(role).__name__}"
|
||||
raise TypeError(msg)
|
||||
return role
|
||||
if isinstance(message, FunctionMessage):
|
||||
return "function"
|
||||
if isinstance(message, ChatMessage):
|
||||
@@ -1702,26 +2188,29 @@ def count_tokens_approximately(
|
||||
"""Approximate the total number of tokens in messages.
|
||||
|
||||
The token count includes stringified message content, role, and (optionally) name.
|
||||
|
||||
- For AI messages, the token count also includes stringified tool calls.
|
||||
- For tool messages, the token count also includes the tool call ID.
|
||||
|
||||
Args:
|
||||
messages: List of messages to count tokens for.
|
||||
chars_per_token: Number of characters per token to use for the approximation.
|
||||
|
||||
One token corresponds to ~4 chars for common English text.
|
||||
|
||||
You can also specify `float` values for more fine-grained control.
|
||||
[See more here](https://platform.openai.com/tokenizer).
|
||||
extra_tokens_per_message: Number of extra tokens to add per message, e.g.
|
||||
special tokens, including beginning/end of message.
|
||||
|
||||
You can also specify `float` values for more fine-grained control.
|
||||
[See more here](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb).
|
||||
count_name: Whether to include message names in the count.
|
||||
Enabled by default.
|
||||
|
||||
Returns:
|
||||
Approximate number of tokens in the messages.
|
||||
|
||||
!!! note
|
||||
Note:
|
||||
This is a simple approximation that may not match the exact token count used by
|
||||
specific models. For accurate counts, use model-specific tokenizers.
|
||||
|
||||
@@ -1729,7 +2218,6 @@ def count_tokens_approximately(
|
||||
This function does not currently support counting image tokens.
|
||||
|
||||
!!! version-added "Added in `langchain-core` 0.3.46"
|
||||
|
||||
"""
|
||||
token_count = 0.0
|
||||
for message in convert_to_messages(messages):
|
||||
@@ -1770,3 +2258,14 @@ def count_tokens_approximately(
|
||||
|
||||
# round up once more time in case extra_tokens_per_message is a float
|
||||
return math.ceil(token_count)
|
||||
|
||||
|
||||
# Mapping from string shortcuts to token counter functions
|
||||
def _approximate_token_counter(messages: Sequence[BaseMessage]) -> int:
|
||||
"""Wrapper for `count_tokens_approximately` that matches expected signature."""
|
||||
return count_tokens_approximately(messages)
|
||||
|
||||
|
||||
_TOKEN_COUNTER_SHORTCUTS = {
|
||||
"approximate": _approximate_token_counter,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import (
|
||||
Any,
|
||||
Generic,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
from typing_extensions import override
|
||||
@@ -46,7 +47,7 @@ class BaseLLMOutputParser(ABC, Generic[T]):
|
||||
async def aparse_result(
|
||||
self, result: list[Generation], *, partial: bool = False
|
||||
) -> T:
|
||||
"""Async parse a list of candidate model `Generation` objects into a specific format.
|
||||
"""Parse a list of candidate model `Generation` objects into a specific format.
|
||||
|
||||
Args:
|
||||
result: A list of `Generation` to be parsed. The Generations are assumed
|
||||
@@ -56,7 +57,7 @@ class BaseLLMOutputParser(ABC, Generic[T]):
|
||||
|
||||
Returns:
|
||||
Structured output.
|
||||
""" # noqa: E501
|
||||
"""
|
||||
return await run_in_executor(None, self.parse_result, result, partial=partial)
|
||||
|
||||
|
||||
@@ -77,7 +78,7 @@ class BaseGenerationOutputParser(
|
||||
"""Return the output type for the parser."""
|
||||
# even though mypy complains this isn't valid,
|
||||
# it is good enough for pydantic to build the schema from
|
||||
return T # type: ignore[misc]
|
||||
return cast("type[T]", T) # type: ignore[misc]
|
||||
|
||||
@override
|
||||
def invoke(
|
||||
@@ -181,7 +182,7 @@ class BaseOutputParser(
|
||||
if hasattr(base, "__pydantic_generic_metadata__"):
|
||||
metadata = base.__pydantic_generic_metadata__
|
||||
if "args" in metadata and len(metadata["args"]) > 0:
|
||||
return metadata["args"][0]
|
||||
return cast("type[T]", metadata["args"][0])
|
||||
|
||||
msg = (
|
||||
f"Runnable {self.__class__.__name__} doesn't have an inferable OutputType. "
|
||||
@@ -267,7 +268,7 @@ class BaseOutputParser(
|
||||
async def aparse_result(
|
||||
self, result: list[Generation], *, partial: bool = False
|
||||
) -> T:
|
||||
"""Async parse a list of candidate model `Generation` objects into a specific format.
|
||||
"""Parse a list of candidate model `Generation` objects into a specific format.
|
||||
|
||||
The return value is parsed from only the first `Generation` in the result, which
|
||||
is assumed to be the highest-likelihood `Generation`.
|
||||
@@ -280,7 +281,7 @@ class BaseOutputParser(
|
||||
|
||||
Returns:
|
||||
Structured output.
|
||||
""" # noqa: E501
|
||||
"""
|
||||
return await run_in_executor(None, self.parse_result, result, partial=partial)
|
||||
|
||||
async def aparse(self, text: str) -> T:
|
||||
|
||||
@@ -37,7 +37,7 @@ class OutputFunctionsParser(BaseGenerationOutputParser[Any]):
|
||||
The parsed JSON object.
|
||||
|
||||
Raises:
|
||||
`OutputParserException`: If the output is not valid JSON.
|
||||
OutputParserException: If the output is not valid JSON.
|
||||
"""
|
||||
generation = result[0]
|
||||
if not isinstance(generation, ChatGeneration):
|
||||
@@ -88,7 +88,7 @@ class JsonOutputFunctionsParser(BaseCumulativeTransformOutputParser[Any]):
|
||||
The parsed JSON object.
|
||||
|
||||
Raises:
|
||||
OutputParserExcept`ion: If the output is not valid JSON.
|
||||
OutputParserException: If the output is not valid JSON.
|
||||
"""
|
||||
if len(result) != 1:
|
||||
msg = f"Expected exactly one result, but got {len(result)}"
|
||||
@@ -228,7 +228,7 @@ class PydanticOutputFunctionsParser(OutputFunctionsParser):
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_schema(cls, values: dict) -> Any:
|
||||
def validate_schema(cls, values: dict[str, Any]) -> Any:
|
||||
"""Validate the Pydantic schema.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Output parsers using Pydantic."""
|
||||
|
||||
import json
|
||||
from typing import Annotated, Generic
|
||||
from typing import Annotated, Generic, Literal, overload
|
||||
|
||||
import pydantic
|
||||
from pydantic import SkipValidation
|
||||
@@ -42,6 +42,16 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
|
||||
msg = f"Failed to parse {name} from completion {json_string}. Got: {e}"
|
||||
return OutputParserException(msg, llm_output=json_string)
|
||||
|
||||
@overload
|
||||
def parse_result(
|
||||
self, result: list[Generation], *, partial: Literal[False] = False
|
||||
) -> TBaseModel: ...
|
||||
|
||||
@overload
|
||||
def parse_result(
|
||||
self, result: list[Generation], *, partial: bool = False
|
||||
) -> TBaseModel | None: ...
|
||||
|
||||
def parse_result(
|
||||
self, result: list[Generation], *, partial: bool = False
|
||||
) -> TBaseModel | None:
|
||||
@@ -54,7 +64,7 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
|
||||
all the keys that have been returned so far.
|
||||
|
||||
Raises:
|
||||
`OutputParserException`: If the result is not valid JSON
|
||||
OutputParserException: If the result is not valid JSON
|
||||
or does not conform to the Pydantic model.
|
||||
|
||||
Returns:
|
||||
@@ -77,7 +87,7 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
|
||||
Returns:
|
||||
The parsed Pydantic object.
|
||||
"""
|
||||
return super().parse(text)
|
||||
return self.parse_result([Generation(text=text)])
|
||||
|
||||
def get_format_instructions(self) -> str:
|
||||
"""Return the format instructions for the JSON output.
|
||||
|
||||
@@ -57,16 +57,18 @@ class ChatGeneration(Generation):
|
||||
text = ""
|
||||
if isinstance(self.message.content, str):
|
||||
text = self.message.content
|
||||
# Assumes text in content blocks in OpenAI format.
|
||||
# Uses first text block.
|
||||
# Extracts first text block from content blocks.
|
||||
# Skips blocks with explicit non-text type (e.g., thinking, reasoning).
|
||||
elif isinstance(self.message.content, list):
|
||||
for block in self.message.content:
|
||||
if isinstance(block, str):
|
||||
text = block
|
||||
break
|
||||
if isinstance(block, dict) and "text" in block:
|
||||
text = block["text"]
|
||||
break
|
||||
block_type = block.get("type")
|
||||
if block_type is None or block_type == "text":
|
||||
text = block["text"]
|
||||
break
|
||||
self.text = text
|
||||
return self
|
||||
|
||||
|
||||
@@ -104,18 +104,31 @@ class ChatPromptValue(PromptValue):
|
||||
|
||||
|
||||
class ImageURL(TypedDict, total=False):
|
||||
"""Image URL."""
|
||||
"""Image URL for multimodal model inputs (OpenAI format).
|
||||
|
||||
Represents the inner `image_url` object in OpenAI's Chat Completion API format. This
|
||||
is used by `ImagePromptTemplate` and `ChatPromptTemplate`.
|
||||
|
||||
See Also:
|
||||
`ImageContentBlock`: LangChain's provider-agnostic image format used in message
|
||||
content blocks. Use `ImageContentBlock` when working with the standardized
|
||||
message format across different providers.
|
||||
|
||||
Note:
|
||||
The `detail` field values are not validated locally. Invalid values
|
||||
will be rejected by the downstream API, allowing new valid values to
|
||||
be used without requiring a LangChain update.
|
||||
"""
|
||||
|
||||
detail: Literal["auto", "low", "high"]
|
||||
"""Specifies the detail level of the image.
|
||||
|
||||
Can be `'auto'`, `'low'`, or `'high'`.
|
||||
|
||||
This follows OpenAI's Chat Completion API's image URL format.
|
||||
Defaults to ``'auto'`` if not specified. Higher detail levels consume
|
||||
more tokens but provide better image understanding.
|
||||
"""
|
||||
|
||||
url: str
|
||||
"""Either a URL of the image or the base64 encoded image data."""
|
||||
"""URL of the image or base64-encoded image data."""
|
||||
|
||||
|
||||
class ImagePromptValue(PromptValue):
|
||||
|
||||
@@ -2,19 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins # noqa: TC003
|
||||
import contextlib
|
||||
import json
|
||||
import typing
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping # noqa: TC003
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Generic,
|
||||
TypeVar,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
@@ -22,7 +17,7 @@ from typing_extensions import Self, override
|
||||
|
||||
from langchain_core.exceptions import ErrorCode, create_message
|
||||
from langchain_core.load import dumpd
|
||||
from langchain_core.output_parsers.base import BaseOutputParser
|
||||
from langchain_core.output_parsers.base import BaseOutputParser # noqa: TC001
|
||||
from langchain_core.prompt_values import (
|
||||
ChatPromptValueConcrete,
|
||||
PromptValue,
|
||||
@@ -56,7 +51,7 @@ class BasePromptTemplate(
|
||||
|
||||
These variables are auto inferred from the prompt and user need not provide them.
|
||||
"""
|
||||
input_types: typing.Dict[str, Any] = Field(default_factory=dict, exclude=True) # noqa: UP006
|
||||
input_types: builtins.dict[str, Any] = Field(default_factory=dict, exclude=True)
|
||||
"""A dictionary of the types of the variables the prompt template expects.
|
||||
|
||||
If not provided, all variables are assumed to be strings.
|
||||
@@ -69,7 +64,7 @@ class BasePromptTemplate(
|
||||
Partial variables populate the template so that you don't need to pass them in every
|
||||
time you call the prompt.
|
||||
"""
|
||||
metadata: typing.Dict[str, Any] | None = None # noqa: UP006
|
||||
metadata: builtins.dict[str, Any] | None = None
|
||||
"""Metadata to be used for tracing."""
|
||||
tags: list[str] | None = None
|
||||
"""Tags to be used for tracing."""
|
||||
@@ -122,7 +117,10 @@ class BasePromptTemplate(
|
||||
|
||||
@cached_property
|
||||
def _serialized(self) -> dict[str, Any]:
|
||||
return dumpd(self)
|
||||
# self is always a Serializable object in this case, thus the result is
|
||||
# guaranteed to be a dict since dumpd uses the default callback, which uses
|
||||
# obj.to_json which always returns TypedDict subclasses
|
||||
return cast("dict[str, Any]", dumpd(self))
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -156,7 +154,7 @@ class BasePromptTemplate(
|
||||
if not isinstance(inner_input, dict):
|
||||
if len(self.input_variables) == 1:
|
||||
var_name = self.input_variables[0]
|
||||
inner_input = {var_name: inner_input}
|
||||
inner_input_ = {var_name: inner_input}
|
||||
|
||||
else:
|
||||
msg = (
|
||||
@@ -168,12 +166,14 @@ class BasePromptTemplate(
|
||||
message=msg, error_code=ErrorCode.INVALID_PROMPT_INPUT
|
||||
)
|
||||
)
|
||||
missing = set(self.input_variables).difference(inner_input)
|
||||
else:
|
||||
inner_input_ = inner_input
|
||||
missing = set(self.input_variables).difference(inner_input_)
|
||||
if missing:
|
||||
msg = (
|
||||
f"Input to {self.__class__.__name__} is missing variables {missing}. "
|
||||
f" Expected: {self.input_variables}"
|
||||
f" Received: {list(inner_input.keys())}"
|
||||
f" Received: {list(inner_input_.keys())}"
|
||||
)
|
||||
example_key = missing.pop()
|
||||
msg += (
|
||||
@@ -184,7 +184,7 @@ class BasePromptTemplate(
|
||||
raise KeyError(
|
||||
create_message(message=msg, error_code=ErrorCode.INVALID_PROMPT_INPUT)
|
||||
)
|
||||
return inner_input
|
||||
return inner_input_
|
||||
|
||||
def _format_prompt_with_error_handling(self, inner_input: dict) -> PromptValue:
|
||||
inner_input_ = self._validate_input(inner_input)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
TypedDict,
|
||||
@@ -48,9 +48,6 @@ from langchain_core.prompts.string import (
|
||||
from langchain_core.utils import get_colored_text
|
||||
from langchain_core.utils.interactive_env import is_interactive_env
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
class MessagesPlaceholder(BaseMessagePromptTemplate):
|
||||
"""Prompt template that assumes variable is already list of messages.
|
||||
@@ -765,7 +762,7 @@ MessageLike = BaseMessagePromptTemplate | BaseMessage | BaseChatPromptTemplate
|
||||
|
||||
MessageLikeRepresentation = (
|
||||
MessageLike
|
||||
| tuple[str | type, str | list[dict] | list[object]]
|
||||
| tuple[str | type, str | Sequence[dict] | Sequence[object]]
|
||||
| str
|
||||
| dict[str, Any]
|
||||
)
|
||||
@@ -848,9 +845,9 @@ class ChatPromptTemplate(BaseChatPromptTemplate):
|
||||
|
||||
!!! note "Single-variable template"
|
||||
|
||||
If your prompt has only a single input variable (i.e., 1 instance of "{variable_nams}"),
|
||||
and you invoke the template with a non-dict object, the prompt template will
|
||||
inject the provided argument into that variable location.
|
||||
If your prompt has only a single input variable (i.e., 1 instance of
|
||||
"{variable_nams}"), and you invoke the template with a non-dict object, the
|
||||
prompt template will inject the provided argument into that variable location.
|
||||
|
||||
```python
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
@@ -874,7 +871,7 @@ class ChatPromptTemplate(BaseChatPromptTemplate):
|
||||
# ]
|
||||
# )
|
||||
```
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
messages: Annotated[list[MessageLike], SkipValidation()]
|
||||
"""List of messages consisting of either message prompt templates or messages."""
|
||||
@@ -1428,16 +1425,26 @@ def _convert_to_message_template(
|
||||
f" Got: {message}"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
message = (message["role"], message["content"])
|
||||
try:
|
||||
message_type_str = message["role"]
|
||||
template = message["content"]
|
||||
else:
|
||||
if len(message) != 2: # noqa: PLR2004
|
||||
msg = f"Expected 2-tuple of (role, template), got {message}"
|
||||
raise ValueError(msg)
|
||||
message_type_str, template = message
|
||||
except ValueError as e:
|
||||
msg = f"Expected 2-tuple of (role, template), got {message}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
if isinstance(message_type_str, str):
|
||||
message_ = _create_template_from_message_type(
|
||||
message_type_str, template, template_format=template_format
|
||||
)
|
||||
elif (
|
||||
hasattr(message_type_str, "model_fields")
|
||||
and "type" in message_type_str.model_fields
|
||||
):
|
||||
message_type = message_type_str.model_fields["type"].default
|
||||
message_ = _create_template_from_message_type(
|
||||
message_type, template, template_format=template_format
|
||||
)
|
||||
else:
|
||||
message_ = message_type_str(
|
||||
prompt=PromptTemplate.from_template(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import warnings
|
||||
from functools import cached_property
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
@@ -65,7 +65,10 @@ class DictPromptTemplate(RunnableSerializable[dict, dict]):
|
||||
|
||||
@cached_property
|
||||
def _serialized(self) -> dict[str, Any]:
|
||||
return dumpd(self)
|
||||
# self is always a Serializable object in this case, thus the result is
|
||||
# guaranteed to be a dict since dumpd uses the default callback, which uses
|
||||
# obj.to_json which always returns TypedDict subclasses
|
||||
return cast("dict[str, Any]", dumpd(self))
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
@@ -116,7 +119,7 @@ def _insert_input_variables(
|
||||
inputs: dict[str, Any],
|
||||
template_format: Literal["f-string", "mustache"],
|
||||
) -> dict[str, Any]:
|
||||
formatted = {}
|
||||
formatted: dict[str, Any] = {}
|
||||
formatter = DEFAULT_FORMATTER_MAPPING[template_format]
|
||||
for k, v in template.items():
|
||||
if isinstance(v, str):
|
||||
@@ -132,7 +135,7 @@ def _insert_input_variables(
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
formatted[k] = _insert_input_variables(v, inputs, template_format)
|
||||
elif isinstance(v, (list, tuple)):
|
||||
formatted_v = []
|
||||
formatted_v: list[str | dict[str, Any]] = []
|
||||
for x in v:
|
||||
if isinstance(x, str):
|
||||
formatted_v.append(formatter(x, **inputs))
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
from pydantic import ConfigDict, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core.example_selectors import BaseExampleSelector
|
||||
from langchain_core.prompts.prompt import PromptTemplate
|
||||
from langchain_core.prompts.string import (
|
||||
DEFAULT_FORMATTER_MAPPING,
|
||||
@@ -21,7 +22,7 @@ class FewShotPromptWithTemplates(StringPromptTemplate):
|
||||
"""Examples to format into the prompt.
|
||||
Either this or example_selector should be provided."""
|
||||
|
||||
example_selector: Any = None
|
||||
example_selector: BaseExampleSelector | None = None
|
||||
"""ExampleSelector to choose the examples to format into the prompt.
|
||||
Either this or examples should be provided."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Image prompt template for a multimodal model."""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -125,7 +125,7 @@ class ImagePromptTemplate(BasePromptTemplate[ImageURL]):
|
||||
output: ImageURL = {"url": url}
|
||||
if detail:
|
||||
# Don't check literal values here: let the API check them
|
||||
output["detail"] = detail
|
||||
output["detail"] = cast("Literal['auto', 'low', 'high']", detail)
|
||||
return output
|
||||
|
||||
async def aformat(self, **kwargs: Any) -> ImageURL:
|
||||
|
||||
@@ -92,4 +92,4 @@ class BaseMessagePromptTemplate(Serializable, ABC):
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate # noqa: PLC0415
|
||||
|
||||
prompt = ChatPromptTemplate(messages=[self])
|
||||
return prompt + other
|
||||
return prompt.__add__(other)
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from abc import ABC
|
||||
from abc import ABC, abstractmethod
|
||||
from string import Formatter
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from pydantic import BaseModel, create_model
|
||||
from typing_extensions import override
|
||||
|
||||
from langchain_core.prompt_values import PromptValue, StringPromptValue
|
||||
from langchain_core.prompts.base import BasePromptTemplate
|
||||
@@ -189,17 +190,20 @@ def mustache_schema(template: str) -> type[BaseModel]:
|
||||
return _create_model_recursive("PromptInput", defs)
|
||||
|
||||
|
||||
def _create_model_recursive(name: str, defs: Defs) -> type:
|
||||
return create_model( # type: ignore[call-overload]
|
||||
name,
|
||||
**{
|
||||
k: (_create_model_recursive(k, v), None) if v else (type(v), None)
|
||||
for k, v in defs.items()
|
||||
},
|
||||
def _create_model_recursive(name: str, defs: Defs) -> type[BaseModel]:
|
||||
return cast(
|
||||
"type[BaseModel]",
|
||||
create_model( # type: ignore[call-overload]
|
||||
name,
|
||||
**{
|
||||
k: (_create_model_recursive(k, v), None) if v else (type(v), None)
|
||||
for k, v in defs.items()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_FORMATTER_MAPPING: dict[str, Callable] = {
|
||||
DEFAULT_FORMATTER_MAPPING: dict[str, Callable[..., str]] = {
|
||||
"f-string": formatter.format,
|
||||
"mustache": mustache_formatter,
|
||||
"jinja2": jinja2_formatter,
|
||||
@@ -330,6 +334,10 @@ class StringPromptTemplate(BasePromptTemplate, ABC):
|
||||
"""
|
||||
return StringPromptValue(text=await self.aformat(**kwargs))
|
||||
|
||||
@override
|
||||
@abstractmethod
|
||||
def format(self, **kwargs: Any) -> str: ...
|
||||
|
||||
def pretty_repr(
|
||||
self,
|
||||
html: bool = False, # noqa: FBT001,FBT002
|
||||
|
||||
@@ -48,6 +48,9 @@ class StructuredPrompt(ChatPromptTemplate):
|
||||
schema_: schema for the structured prompt.
|
||||
structured_output_kwargs: additional kwargs for structured output.
|
||||
template_format: template format for the prompt.
|
||||
|
||||
Raises:
|
||||
ValueError: if schema is not provided.
|
||||
"""
|
||||
schema_ = schema_ or kwargs.pop("schema", None)
|
||||
if not schema_:
|
||||
|
||||
@@ -315,7 +315,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
"args" in metadata
|
||||
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
|
||||
):
|
||||
return metadata["args"][0]
|
||||
return cast("type[Input]", metadata["args"][0])
|
||||
|
||||
# If we didn't find a Pydantic model in the parent classes,
|
||||
# then loop through __orig_bases__. This corresponds to
|
||||
@@ -323,7 +323,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
|
||||
type_args = get_args(cls)
|
||||
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
|
||||
return type_args[0]
|
||||
return cast("type[Input]", type_args[0])
|
||||
|
||||
msg = (
|
||||
f"Runnable {self.get_name()} doesn't have an inferable InputType. "
|
||||
@@ -349,12 +349,12 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
"args" in metadata
|
||||
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
|
||||
):
|
||||
return metadata["args"][1]
|
||||
return cast("type[Output]", metadata["args"][1])
|
||||
|
||||
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
|
||||
type_args = get_args(cls)
|
||||
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
|
||||
return type_args[1]
|
||||
return cast("type[Output]", type_args[1])
|
||||
|
||||
msg = (
|
||||
f"Runnable {self.get_name()} doesn't have an inferable OutputType. "
|
||||
@@ -369,7 +369,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
|
||||
def get_input_schema(
|
||||
self,
|
||||
config: RunnableConfig | None = None, # noqa: ARG002
|
||||
config: RunnableConfig | None = None,
|
||||
) -> type[BaseModel]:
|
||||
"""Get a Pydantic model that can be used to validate input to the `Runnable`.
|
||||
|
||||
@@ -385,6 +385,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Returns:
|
||||
A Pydantic model that can be used to validate input.
|
||||
"""
|
||||
_ = config
|
||||
root_type = self.InputType
|
||||
|
||||
if (
|
||||
@@ -447,7 +448,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
|
||||
def get_output_schema(
|
||||
self,
|
||||
config: RunnableConfig | None = None, # noqa: ARG002
|
||||
config: RunnableConfig | None = None,
|
||||
) -> type[BaseModel]:
|
||||
"""Get a Pydantic model that can be used to validate output to the `Runnable`.
|
||||
|
||||
@@ -463,6 +464,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Returns:
|
||||
A Pydantic model that can be used to validate output.
|
||||
"""
|
||||
_ = config
|
||||
root_type = self.OutputType
|
||||
|
||||
if (
|
||||
@@ -2277,6 +2279,9 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Use this to implement `stream` or `transform` in `Runnable` subclasses.
|
||||
|
||||
"""
|
||||
# Extract defers_inputs from kwargs if present
|
||||
defers_inputs = kwargs.pop("defers_inputs", False)
|
||||
|
||||
# tee the input so we can iterate over it twice
|
||||
input_for_tracing, input_for_transform = tee(inputs, 2)
|
||||
# Start the input iterator to ensure the input Runnable starts before this one
|
||||
@@ -2293,6 +2298,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
run_type=run_type,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
defers_inputs=defers_inputs,
|
||||
)
|
||||
try:
|
||||
child_config = patch_config(config, callbacks=run_manager.get_child())
|
||||
@@ -2374,6 +2380,9 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Use this to implement `astream` or `atransform` in `Runnable` subclasses.
|
||||
|
||||
"""
|
||||
# Extract defers_inputs from kwargs if present
|
||||
defers_inputs = kwargs.pop("defers_inputs", False)
|
||||
|
||||
# tee the input so we can iterate over it twice
|
||||
input_for_tracing, input_for_transform = atee(inputs, 2)
|
||||
# Start the input iterator to ensure the input Runnable starts before this one
|
||||
@@ -2390,6 +2399,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
run_type=run_type,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
defers_inputs=defers_inputs,
|
||||
)
|
||||
try:
|
||||
child_config = patch_config(config, callbacks=run_manager.get_child())
|
||||
@@ -4323,6 +4333,7 @@ class RunnableGenerator(Runnable[Input, Output]):
|
||||
input,
|
||||
self._transform, # type: ignore[arg-type]
|
||||
config,
|
||||
defers_inputs=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -4356,7 +4367,7 @@ class RunnableGenerator(Runnable[Input, Output]):
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
return self._atransform_stream_with_config(
|
||||
input, self._atransform, config, **kwargs
|
||||
input, self._atransform, config, defers_inputs=True, **kwargs
|
||||
)
|
||||
|
||||
@override
|
||||
@@ -4429,6 +4440,138 @@ class RunnableLambda(Runnable[Input, Output]):
|
||||
```
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, RunnableConfig], Awaitable[Output]],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Awaitable[Output]],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], AsyncIterator[Output]],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, RunnableConfig], Output],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Iterator[Output]],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Runnable[Input, Output]],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, CallbackManagerForChainRun], Output],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, CallbackManagerForChainRun, RunnableConfig], Output],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Output],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Iterator[Output]]
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
# Cannot move uuid to TYPE_CHECKING as RunnableConfig is used in Pydantic models
|
||||
import uuid # noqa: TC003
|
||||
import warnings
|
||||
from collections.abc import Awaitable, Callable, Generator, Iterable, Iterator, Sequence
|
||||
from concurrent.futures import Executor, Future, ThreadPoolExecutor
|
||||
@@ -49,8 +51,24 @@ class EmptyDict(TypedDict, total=False):
|
||||
class RunnableConfig(TypedDict, total=False):
|
||||
"""Configuration for a `Runnable`.
|
||||
|
||||
See the [reference docs](https://reference.langchain.com/python/langchain_core/runnables/#langchain_core.runnables.RunnableConfig)
|
||||
for more details.
|
||||
!!! note Custom values
|
||||
|
||||
The `TypedDict` has `total=False` set intentionally to:
|
||||
|
||||
- Allow partial configs to be created and merged together via `merge_configs`
|
||||
- Support config propagation from parent to child runnables via
|
||||
`var_child_runnable_config` (a `ContextVar` that automatically passes
|
||||
config down the call stack without explicit parameter passing), where
|
||||
configs are merged rather than replaced
|
||||
|
||||
!!! example
|
||||
|
||||
```python
|
||||
# Parent sets tags
|
||||
chain.invoke(input, config={"tags": ["parent"]})
|
||||
# Child automatically inherits and can add:
|
||||
# ensure_config({"tags": ["child"]}) -> {"tags": ["parent", "child"]}
|
||||
```
|
||||
"""
|
||||
|
||||
tags: list[str]
|
||||
@@ -90,7 +108,8 @@ class RunnableConfig(TypedDict, total=False):
|
||||
|
||||
configurable: dict[str, Any]
|
||||
"""Runtime values for attributes previously made configurable on this `Runnable`,
|
||||
or sub-Runnables, through `configurable_fields` or `configurable_alternatives`.
|
||||
or sub-`Runnable` objects, through `configurable_fields` or
|
||||
`configurable_alternatives`.
|
||||
|
||||
Check `output_schema` for a description of the attributes that have been made
|
||||
configurable.
|
||||
|
||||
@@ -165,6 +165,9 @@ class AsciiCanvas:
|
||||
y0: y coordinate of the box corner.
|
||||
width: box width.
|
||||
height: box height.
|
||||
|
||||
Raises:
|
||||
ValueError: if box dimensions are invalid.
|
||||
"""
|
||||
if width <= 1 or height <= 1:
|
||||
msg = "Box dimensions should be > 1"
|
||||
|
||||
@@ -8,9 +8,10 @@ import random
|
||||
import re
|
||||
import string
|
||||
import time
|
||||
import urllib.parse
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -40,6 +41,8 @@ except ImportError:
|
||||
|
||||
MARKDOWN_SPECIAL_CHARS = "*_`"
|
||||
|
||||
_HEX_COLOR_PATTERN = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
|
||||
|
||||
|
||||
def draw_mermaid(
|
||||
nodes: dict[str, Node],
|
||||
@@ -81,6 +84,7 @@ def draw_mermaid(
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Returns:
|
||||
Mermaid graph syntax.
|
||||
|
||||
@@ -389,7 +393,7 @@ async def _render_mermaid_using_pyppeteer(
|
||||
}
|
||||
)
|
||||
|
||||
img_bytes = await page.screenshot({"fullPage": False})
|
||||
img_bytes = cast("bytes", await page.screenshot({"fullPage": False}))
|
||||
await browser.close()
|
||||
|
||||
if output_file_path is not None:
|
||||
@@ -428,14 +432,14 @@ def _render_mermaid_using_api(
|
||||
)
|
||||
|
||||
# Check if the background color is a hexadecimal color code using regex
|
||||
if background_color is not None:
|
||||
hex_color_pattern = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
|
||||
if not hex_color_pattern.match(background_color):
|
||||
background_color = f"!{background_color}"
|
||||
if background_color is not None and not _HEX_COLOR_PATTERN.match(background_color):
|
||||
background_color = f"!{background_color}"
|
||||
|
||||
# URL-encode the background_color to handle special characters like '!'
|
||||
encoded_bg_color = urllib.parse.quote(str(background_color), safe="")
|
||||
image_url = (
|
||||
f"{base_url}/img/{mermaid_syntax_encoded}"
|
||||
f"?type={file_type}&bgColor={background_color}"
|
||||
f"?type={file_type}&bgColor={encoded_bg_color}"
|
||||
)
|
||||
|
||||
error_msg_suffix = (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Helper class to draw a state graph into a PNG file."""
|
||||
|
||||
from itertools import groupby
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from langchain_core.runnables.graph import Graph, LabelsDict
|
||||
|
||||
@@ -149,7 +149,7 @@ class PngDrawer:
|
||||
|
||||
# Save the graph as PNG
|
||||
try:
|
||||
return viz.draw(output_path, format="png", prog="dot")
|
||||
return cast("bytes | None", viz.draw(output_path, format="png", prog="dot"))
|
||||
finally:
|
||||
viz.close()
|
||||
|
||||
@@ -201,7 +201,8 @@ class PngDrawer:
|
||||
viz, start, end, str(data) if data is not None else None, cond
|
||||
)
|
||||
|
||||
def update_styles(self, viz: Any, graph: Graph) -> None:
|
||||
@staticmethod
|
||||
def update_styles(viz: Any, graph: Graph) -> None:
|
||||
"""Update the styles of the entrypoint and END nodes.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -320,7 +320,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
`RunnableBindingBase` init.
|
||||
|
||||
"""
|
||||
history_chain: Runnable = RunnableLambda(
|
||||
history_chain: Runnable[Any, Any] = RunnableLambda(
|
||||
self._enter_history, self._aenter_history
|
||||
).with_config(run_name="load_history")
|
||||
messages_key = history_messages_key or input_messages_key
|
||||
@@ -329,16 +329,16 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
**{messages_key: history_chain}
|
||||
).with_config(run_name="insert_history")
|
||||
|
||||
runnable_sync: Runnable = runnable.with_listeners(on_end=self._exit_history)
|
||||
runnable_async: Runnable = runnable.with_alisteners(on_end=self._aexit_history)
|
||||
runnable_sync = runnable.with_listeners(on_end=self._exit_history)
|
||||
runnable_async = runnable.with_alisteners(on_end=self._aexit_history)
|
||||
|
||||
def _call_runnable_sync(_input: Any) -> Runnable:
|
||||
def _call_runnable_sync(_input: Any) -> Runnable[Any, Any]:
|
||||
return runnable_sync
|
||||
|
||||
async def _call_runnable_async(_input: Any) -> Runnable:
|
||||
async def _call_runnable_async(_input: Any) -> Runnable[Any, Any]:
|
||||
return runnable_async
|
||||
|
||||
bound: Runnable = (
|
||||
bound = (
|
||||
history_chain
|
||||
| RunnableLambda(
|
||||
_call_runnable_sync,
|
||||
@@ -539,7 +539,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
hist: BaseChatMessageHistory = config["configurable"]["message_history"]
|
||||
|
||||
# Get the input messages
|
||||
inputs = load(run.inputs)
|
||||
inputs = load(run.inputs, allowed_objects="all")
|
||||
input_messages = self._get_input_messages(inputs)
|
||||
# If historic messages were prepended to the input messages, remove them to
|
||||
# avoid adding duplicate messages to history.
|
||||
@@ -548,7 +548,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
input_messages = input_messages[len(historic_messages) :]
|
||||
|
||||
# Get the output messages
|
||||
output_val = load(run.outputs)
|
||||
output_val = load(run.outputs, allowed_objects="all")
|
||||
output_messages = self._get_output_messages(output_val)
|
||||
hist.add_messages(input_messages + output_messages)
|
||||
|
||||
@@ -556,7 +556,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
hist: BaseChatMessageHistory = config["configurable"]["message_history"]
|
||||
|
||||
# Get the input messages
|
||||
inputs = load(run.inputs)
|
||||
inputs = load(run.inputs, allowed_objects="all")
|
||||
input_messages = self._get_input_messages(inputs)
|
||||
# If historic messages were prepended to the input messages, remove them to
|
||||
# avoid adding duplicate messages to history.
|
||||
@@ -565,7 +565,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
input_messages = input_messages[len(historic_messages) :]
|
||||
|
||||
# Get the output messages
|
||||
output_val = load(run.outputs)
|
||||
output_val = load(run.outputs, allowed_objects="all")
|
||||
output_messages = self._get_output_messages(output_val)
|
||||
await hist.aadd_messages(input_messages + output_messages)
|
||||
|
||||
|
||||
@@ -753,25 +753,19 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
return AddableDict(picked)
|
||||
return None
|
||||
|
||||
def _invoke(
|
||||
self,
|
||||
value: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return self._pick(value)
|
||||
|
||||
@override
|
||||
def invoke(
|
||||
self,
|
||||
input: dict[str, Any],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
return self._call_with_config(self._invoke, input, config, **kwargs)
|
||||
) -> Any:
|
||||
return self._call_with_config(self._pick, input, config, **kwargs)
|
||||
|
||||
async def _ainvoke(
|
||||
self,
|
||||
value: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
) -> Any:
|
||||
return self._pick(value)
|
||||
|
||||
@override
|
||||
@@ -780,13 +774,13 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: dict[str, Any],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
) -> Any:
|
||||
return await self._acall_with_config(self._ainvoke, input, config, **kwargs)
|
||||
|
||||
def _transform(
|
||||
self,
|
||||
chunks: Iterator[dict[str, Any]],
|
||||
) -> Iterator[dict[str, Any]]:
|
||||
) -> Iterator[Any]:
|
||||
for chunk in chunks:
|
||||
picked = self._pick(chunk)
|
||||
if picked is not None:
|
||||
@@ -798,7 +792,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: Iterator[dict[str, Any]],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[dict[str, Any]]:
|
||||
) -> Iterator[Any]:
|
||||
yield from self._transform_stream_with_config(
|
||||
input, self._transform, config, **kwargs
|
||||
)
|
||||
@@ -806,7 +800,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
async def _atransform(
|
||||
self,
|
||||
chunks: AsyncIterator[dict[str, Any]],
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
) -> AsyncIterator[Any]:
|
||||
async for chunk in chunks:
|
||||
picked = self._pick(chunk)
|
||||
if picked is not None:
|
||||
@@ -818,7 +812,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: AsyncIterator[dict[str, Any]],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
) -> AsyncIterator[Any]:
|
||||
async for chunk in self._atransform_stream_with_config(
|
||||
input, self._atransform, config, **kwargs
|
||||
):
|
||||
@@ -830,7 +824,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: dict[str, Any],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[dict[str, Any]]:
|
||||
) -> Iterator[Any]:
|
||||
return self.transform(iter([input]), config, **kwargs)
|
||||
|
||||
@override
|
||||
@@ -839,7 +833,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: dict[str, Any],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
) -> AsyncIterator[Any]:
|
||||
async def input_aiter() -> AsyncIterator[dict[str, Any]]:
|
||||
yield input
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -31,7 +31,7 @@ from langchain_core.runnables.utils import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from collections.abc import AsyncIterator, Callable, Iterator
|
||||
|
||||
|
||||
class RouterInput(TypedDict):
|
||||
@@ -151,7 +151,7 @@ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
|
||||
raise ValueError(msg)
|
||||
|
||||
def invoke(
|
||||
runnable: Runnable, input_: Input, config: RunnableConfig
|
||||
runnable: Runnable[Input, Output], input_: Input, config: RunnableConfig
|
||||
) -> Output | Exception:
|
||||
if return_exceptions:
|
||||
try:
|
||||
@@ -188,7 +188,7 @@ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
|
||||
raise ValueError(msg)
|
||||
|
||||
async def ainvoke(
|
||||
runnable: Runnable, input_: Input, config: RunnableConfig
|
||||
runnable: Runnable[Input, Output], input_: Input, config: RunnableConfig
|
||||
) -> Output | Exception:
|
||||
if return_exceptions:
|
||||
try:
|
||||
|
||||
@@ -45,6 +45,12 @@ class EventData(TypedDict, total=False):
|
||||
chunks support addition in general, and adding them up should result
|
||||
in the output of the `Runnable` that generated the event.
|
||||
"""
|
||||
tool_call_id: NotRequired[str | None]
|
||||
"""The tool call ID associated with the tool execution.
|
||||
|
||||
This field is available for the `on_tool_error` event and can be used to
|
||||
link errors to specific tool calls in stateless agent implementations.
|
||||
"""
|
||||
|
||||
|
||||
class BaseStreamEvent(TypedDict):
|
||||
|
||||
@@ -7,7 +7,10 @@ import asyncio
|
||||
import inspect
|
||||
import sys
|
||||
import textwrap
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
# Cannot move to TYPE_CHECKING as Mapping and Sequence are needed at runtime by
|
||||
# RunnableConfigurableFields.
|
||||
from collections.abc import Mapping, Sequence # noqa: TC003
|
||||
from functools import lru_cache
|
||||
from inspect import signature
|
||||
from itertools import groupby
|
||||
@@ -129,9 +132,12 @@ def asyncio_accepts_context() -> bool:
|
||||
return sys.version_info >= (3, 11)
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def coro_with_context(
|
||||
coro: Awaitable[Any], context: Context, *, create_task: bool = False
|
||||
) -> Awaitable[Any]:
|
||||
coro: Awaitable[_T], context: Context, *, create_task: bool = False
|
||||
) -> Awaitable[_T]:
|
||||
"""Await a coroutine with a context.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -205,7 +205,7 @@ class InMemoryBaseStore(BaseStore[str, V], Generic[V]):
|
||||
async def amdelete(self, keys: Sequence[str]) -> None:
|
||||
self.mdelete(keys)
|
||||
|
||||
def yield_keys(self, prefix: str | None = None) -> Iterator[str]:
|
||||
def yield_keys(self, *, prefix: str | None = None) -> Iterator[str]:
|
||||
"""Get an iterator over keys that match the given prefix.
|
||||
|
||||
Args:
|
||||
@@ -221,7 +221,7 @@ class InMemoryBaseStore(BaseStore[str, V], Generic[V]):
|
||||
if key.startswith(prefix):
|
||||
yield key
|
||||
|
||||
async def ayield_keys(self, prefix: str | None = None) -> AsyncIterator[str]:
|
||||
async def ayield_keys(self, *, prefix: str | None = None) -> AsyncIterator[str]:
|
||||
"""Async get an async iterator over keys that match the given prefix.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -5,10 +5,11 @@ from __future__ import annotations
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import typing
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable # noqa: TC003
|
||||
from inspect import signature
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -22,6 +23,7 @@ from typing import (
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
import typing_extensions
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
@@ -31,6 +33,7 @@ from pydantic import (
|
||||
ValidationError,
|
||||
validate_arguments,
|
||||
)
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import ValidationError as ValidationErrorV1
|
||||
from pydantic.v1 import validate_arguments as validate_arguments_v1
|
||||
@@ -80,6 +83,8 @@ TOOL_MESSAGE_BLOCK_TYPES = (
|
||||
"file",
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SchemaAnnotationError(TypeError):
|
||||
"""Raised when args_schema is missing or has an incorrect type annotation."""
|
||||
@@ -94,12 +99,14 @@ def _is_annotated_type(typ: type[Any]) -> bool:
|
||||
Returns:
|
||||
`True` if the type is an Annotated type, `False` otherwise.
|
||||
"""
|
||||
return get_origin(typ) is typing.Annotated
|
||||
return get_origin(typ) in {typing.Annotated, typing_extensions.Annotated}
|
||||
|
||||
|
||||
def _get_annotation_description(arg_type: type) -> str | None:
|
||||
"""Extract description from an Annotated type.
|
||||
|
||||
Checks for string annotations and `FieldInfo` objects with descriptions.
|
||||
|
||||
Args:
|
||||
arg_type: The type to extract description from.
|
||||
|
||||
@@ -111,6 +118,8 @@ def _get_annotation_description(arg_type: type) -> str | None:
|
||||
for annotation in annotated_args[1:]:
|
||||
if isinstance(annotation, str):
|
||||
return annotation
|
||||
if isinstance(annotation, FieldInfo) and annotation.description:
|
||||
return annotation.description
|
||||
return None
|
||||
|
||||
|
||||
@@ -560,9 +569,12 @@ class ChildTool(BaseTool):
|
||||
elif self.args_schema and issubclass(self.args_schema, BaseModelV1):
|
||||
json_schema = self.args_schema.schema()
|
||||
else:
|
||||
input_schema = self.get_input_schema()
|
||||
json_schema = input_schema.model_json_schema()
|
||||
return json_schema["properties"]
|
||||
input_schema = self.tool_call_schema
|
||||
if isinstance(input_schema, dict):
|
||||
json_schema = input_schema
|
||||
else:
|
||||
json_schema = input_schema.model_json_schema()
|
||||
return cast("dict", json_schema["properties"])
|
||||
|
||||
@property
|
||||
def tool_call_schema(self) -> ArgsSchema:
|
||||
@@ -653,6 +665,7 @@ class ChildTool(BaseTool):
|
||||
TypeError: If `args_schema` is not a Pydantic `BaseModel` or dict.
|
||||
"""
|
||||
input_args = self.args_schema
|
||||
|
||||
if isinstance(tool_input, str):
|
||||
if input_args is not None:
|
||||
if isinstance(input_args, dict):
|
||||
@@ -670,6 +683,7 @@ class ChildTool(BaseTool):
|
||||
msg = f"args_schema must be a Pydantic BaseModel, got {input_args}"
|
||||
raise TypeError(msg)
|
||||
return tool_input
|
||||
|
||||
if input_args is not None:
|
||||
if isinstance(input_args, dict):
|
||||
return tool_input
|
||||
@@ -710,9 +724,30 @@ class ChildTool(BaseTool):
|
||||
f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"
|
||||
)
|
||||
raise NotImplementedError(msg)
|
||||
validated_input = {
|
||||
k: getattr(result, k) for k in result_dict if k in tool_input
|
||||
}
|
||||
|
||||
# Include fields from tool_input, plus fields with explicit defaults.
|
||||
# This applies Pydantic defaults (like Field(default=1)) while excluding
|
||||
# synthetic "args"/"kwargs" fields that Pydantic creates for *args/**kwargs.
|
||||
field_info = get_fields(input_args)
|
||||
validated_input = {}
|
||||
for k in result_dict:
|
||||
if k in tool_input:
|
||||
# Field was provided in input - include it (validated)
|
||||
validated_input[k] = getattr(result, k)
|
||||
elif k in field_info and k not in ("args", "kwargs"):
|
||||
# Check if field has an explicit default defined in the schema.
|
||||
# Exclude "args"/"kwargs" as these are synthetic fields for variadic
|
||||
# parameters that should not be passed as keyword arguments.
|
||||
fi = field_info[k]
|
||||
# Pydantic v2 uses is_required() method, v1 uses required attribute
|
||||
has_default = (
|
||||
not fi.is_required()
|
||||
if hasattr(fi, "is_required")
|
||||
else not getattr(fi, "required", True)
|
||||
)
|
||||
if has_default:
|
||||
validated_input[k] = getattr(result, k)
|
||||
|
||||
for k in self._injected_args_keys:
|
||||
if k in tool_input:
|
||||
validated_input[k] = tool_input[k]
|
||||
@@ -727,7 +762,9 @@ class ChildTool(BaseTool):
|
||||
)
|
||||
raise ValueError(msg)
|
||||
validated_input[k] = tool_call_id
|
||||
|
||||
return validated_input
|
||||
|
||||
return tool_input
|
||||
|
||||
@abstractmethod
|
||||
@@ -771,6 +808,9 @@ class ChildTool(BaseTool):
|
||||
# Start with filtered args from the constant
|
||||
filtered_keys = set[str](FILTERED_ARGS)
|
||||
|
||||
# Add injected args from function signature (e.g., ToolRuntime parameters)
|
||||
filtered_keys.update(self._injected_args_keys)
|
||||
|
||||
# If we have an args_schema, use it to identify injected args
|
||||
if self.args_schema is not None:
|
||||
try:
|
||||
@@ -778,9 +818,12 @@ class ChildTool(BaseTool):
|
||||
for field_name, field_type in annotations.items():
|
||||
if _is_injected_arg_type(field_type):
|
||||
filtered_keys.add(field_name)
|
||||
except Exception: # noqa: S110
|
||||
except Exception:
|
||||
# If we can't get annotations, just use FILTERED_ARGS
|
||||
pass
|
||||
_logger.debug(
|
||||
"Failed to get args_schema annotations for filtering.",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Filter out the injected keys from tool_input
|
||||
return {k: v for k, v in tool_input.items() if k not in filtered_keys}
|
||||
@@ -946,7 +989,7 @@ class ChildTool(BaseTool):
|
||||
error_to_raise = e
|
||||
|
||||
if error_to_raise:
|
||||
run_manager.on_tool_error(error_to_raise)
|
||||
run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
|
||||
raise error_to_raise
|
||||
output = _format_output(content, artifact, tool_call_id, self.name, status)
|
||||
run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
|
||||
@@ -1076,7 +1119,7 @@ class ChildTool(BaseTool):
|
||||
error_to_raise = e
|
||||
|
||||
if error_to_raise:
|
||||
await run_manager.on_tool_error(error_to_raise)
|
||||
await run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
|
||||
raise error_to_raise
|
||||
|
||||
output = _format_output(content, artifact, tool_call_id, self.name, status)
|
||||
@@ -1514,7 +1557,7 @@ def _replace_type_vars(
|
||||
_replace_type_vars(arg, generic_map, default_to_bound=default_to_bound)
|
||||
for arg in args
|
||||
)
|
||||
return _py_38_safe_origin(origin)[new_args] # type: ignore[index]
|
||||
return cast("type", _py_38_safe_origin(origin)[new_args]) # type: ignore[index]
|
||||
return type_
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Literal, get_type_hints, overload
|
||||
from typing import Any, Literal, cast, get_type_hints, overload
|
||||
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
@@ -407,7 +407,7 @@ def _get_schema_from_runnable_and_arg_types(
|
||||
)
|
||||
raise TypeError(msg) from e
|
||||
fields = {key: (key_type, Field(...)) for key, key_type in arg_types.items()}
|
||||
return create_model(name, **fields) # type: ignore[call-overload]
|
||||
return cast("type[BaseModel]", create_model(name, **fields)) # type: ignore[call-overload]
|
||||
|
||||
|
||||
def convert_runnable_to_tool(
|
||||
|
||||
@@ -6,8 +6,10 @@ from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.documents import Document
|
||||
# Cannot move Callbacks and Document to TYPE_CHECKING as StructuredTool's
|
||||
# func/coroutine parameter annotations are evaluated at runtime.
|
||||
from langchain_core.callbacks import Callbacks # noqa: TC001
|
||||
from langchain_core.documents import Document # noqa: TC001
|
||||
from langchain_core.prompts import (
|
||||
BasePromptTemplate,
|
||||
PromptTemplate,
|
||||
|
||||
@@ -11,9 +11,10 @@ from typing import (
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
# Cannot move to TYPE_CHECKING as _run/_arun parameter annotations are needed at runtime
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManagerForToolRun,
|
||||
CallbackManagerForToolRun,
|
||||
AsyncCallbackManagerForToolRun, # noqa: TC001
|
||||
CallbackManagerForToolRun, # noqa: TC001
|
||||
)
|
||||
from langchain_core.runnables import RunnableConfig, run_in_executor
|
||||
from langchain_core.tools.base import (
|
||||
|
||||
@@ -16,9 +16,10 @@ from typing import (
|
||||
from pydantic import Field, SkipValidation
|
||||
from typing_extensions import override
|
||||
|
||||
# Cannot move to TYPE_CHECKING as _run/_arun parameter annotations are needed at runtime
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManagerForToolRun,
|
||||
CallbackManagerForToolRun,
|
||||
AsyncCallbackManagerForToolRun, # noqa: TC001
|
||||
CallbackManagerForToolRun, # noqa: TC001
|
||||
)
|
||||
from langchain_core.runnables import RunnableConfig, run_in_executor
|
||||
from langchain_core.tools.base import (
|
||||
|
||||
93
libs/core/langchain_core/tracers/_compat.py
Normal file
93
libs/core/langchain_core/tracers/_compat.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Compatibility helpers for Pydantic v1/v2 with langsmith Run objects.
|
||||
|
||||
Note: The generic helpers (`pydantic_to_dict`, `pydantic_copy`) detect Pydantic
|
||||
version based on the langsmith `Run` model. They're intended for langsmith objects
|
||||
(`Run`, `Example`) which migrate together.
|
||||
|
||||
For general Pydantic v1/v2 handling, see `langchain_core.utils.pydantic`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from langchain_core.tracers.schemas import Run
|
||||
|
||||
# Detect Pydantic version once at import time based on Run model
|
||||
_RUN_IS_PYDANTIC_V2 = hasattr(Run, "model_dump")
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def run_to_dict(run: Run, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Convert run to dict, compatible with both Pydantic v1 and v2.
|
||||
|
||||
Args:
|
||||
run: The run to convert.
|
||||
**kwargs: Additional arguments passed to model_dump/dict.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of the run.
|
||||
"""
|
||||
if _RUN_IS_PYDANTIC_V2:
|
||||
return run.model_dump(**kwargs)
|
||||
return run.dict(**kwargs) # type: ignore[deprecated]
|
||||
|
||||
|
||||
def run_copy(run: Run, **kwargs: Any) -> Run:
|
||||
"""Copy run, compatible with both Pydantic v1 and v2.
|
||||
|
||||
Args:
|
||||
run: The run to copy.
|
||||
**kwargs: Additional arguments passed to model_copy/copy.
|
||||
|
||||
Returns:
|
||||
A copy of the run.
|
||||
"""
|
||||
if _RUN_IS_PYDANTIC_V2:
|
||||
return run.model_copy(**kwargs)
|
||||
return run.copy(**kwargs) # type: ignore[deprecated]
|
||||
|
||||
|
||||
def run_construct(**kwargs: Any) -> Run:
|
||||
"""Construct run without validation, compatible with both Pydantic v1 and v2.
|
||||
|
||||
Args:
|
||||
**kwargs: Fields to set on the run.
|
||||
|
||||
Returns:
|
||||
A new Run instance constructed without validation.
|
||||
"""
|
||||
if _RUN_IS_PYDANTIC_V2:
|
||||
return Run.model_construct(**kwargs)
|
||||
return Run.construct(**kwargs) # type: ignore[deprecated]
|
||||
|
||||
|
||||
def pydantic_to_dict(obj: Any, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Convert any Pydantic model to dict, compatible with both v1 and v2.
|
||||
|
||||
Args:
|
||||
obj: The Pydantic model to convert.
|
||||
**kwargs: Additional arguments passed to model_dump/dict.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of the model.
|
||||
"""
|
||||
if _RUN_IS_PYDANTIC_V2:
|
||||
return obj.model_dump(**kwargs) # type: ignore[no-any-return]
|
||||
return obj.dict(**kwargs) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def pydantic_copy(obj: T, **kwargs: Any) -> T:
|
||||
"""Copy any Pydantic model, compatible with both v1 and v2.
|
||||
|
||||
Args:
|
||||
obj: The Pydantic model to copy.
|
||||
**kwargs: Additional arguments passed to model_copy/copy.
|
||||
|
||||
Returns:
|
||||
A copy of the model.
|
||||
"""
|
||||
if _RUN_IS_PYDANTIC_V2:
|
||||
return obj.model_copy(**kwargs) # type: ignore[attr-defined,no-any-return]
|
||||
return obj.copy(**kwargs) # type: ignore[attr-defined,no-any-return]
|
||||
@@ -137,7 +137,7 @@ def _get_tracer_project() -> str:
|
||||
tracing_context = ls_rh.get_tracing_context()
|
||||
run_tree = tracing_context["parent"]
|
||||
if run_tree is None and tracing_context["project_name"] is not None:
|
||||
return tracing_context["project_name"]
|
||||
return cast("str", tracing_context["project_name"])
|
||||
return getattr(
|
||||
run_tree,
|
||||
"session_name",
|
||||
|
||||
@@ -184,7 +184,7 @@ class _TracerCore(ABC):
|
||||
# Changing this to "chat_model" may break triggering on_llm_start
|
||||
run_type="chat_model",
|
||||
tags=tags,
|
||||
name=name, # type: ignore[arg-type]
|
||||
name=name,
|
||||
)
|
||||
|
||||
def _create_llm_run(
|
||||
@@ -213,7 +213,7 @@ class _TracerCore(ABC):
|
||||
start_time=start_time,
|
||||
run_type="llm",
|
||||
tags=tags or [],
|
||||
name=name, # type: ignore[arg-type]
|
||||
name=name,
|
||||
)
|
||||
|
||||
def _llm_run_with_token_event(
|
||||
@@ -221,9 +221,10 @@ class _TracerCore(ABC):
|
||||
token: str,
|
||||
run_id: UUID,
|
||||
chunk: GenerationChunk | ChatGenerationChunk | None = None,
|
||||
parent_run_id: UUID | None = None, # noqa: ARG002
|
||||
parent_run_id: UUID | None = None,
|
||||
) -> Run:
|
||||
"""Append token event to LLM run and return the run."""
|
||||
_ = parent_run_id
|
||||
llm_run = self._get_run(run_id, run_type={"llm", "chat_model"})
|
||||
event_kwargs: dict[str, Any] = {"token": token}
|
||||
if chunk:
|
||||
@@ -284,6 +285,16 @@ class _TracerCore(ABC):
|
||||
llm_run.end_time = datetime.now(timezone.utc)
|
||||
llm_run.events.append({"name": "end", "time": llm_run.end_time})
|
||||
|
||||
tool_call_count = 0
|
||||
for generations in response.generations:
|
||||
for generation in generations:
|
||||
if hasattr(generation, "message"):
|
||||
msg = generation.message
|
||||
if hasattr(msg, "tool_calls") and msg.tool_calls:
|
||||
tool_call_count += len(msg.tool_calls)
|
||||
if tool_call_count > 0:
|
||||
llm_run.extra["tool_call_count"] = tool_call_count
|
||||
|
||||
return llm_run
|
||||
|
||||
def _errored_llm_run(
|
||||
@@ -336,7 +347,7 @@ class _TracerCore(ABC):
|
||||
start_time=start_time,
|
||||
child_runs=[],
|
||||
run_type=run_type or "chain",
|
||||
name=name, # type: ignore[arg-type]
|
||||
name=name,
|
||||
tags=tags or [],
|
||||
)
|
||||
|
||||
@@ -433,7 +444,7 @@ class _TracerCore(ABC):
|
||||
child_runs=[],
|
||||
run_type="tool",
|
||||
tags=tags or [],
|
||||
name=name, # type: ignore[arg-type]
|
||||
name=name,
|
||||
)
|
||||
|
||||
def _complete_tool_run(
|
||||
@@ -528,43 +539,47 @@ class _TracerCore(ABC):
|
||||
"""Return self copied."""
|
||||
return self
|
||||
|
||||
def _end_trace(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _end_trace(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""End a trace for a run.
|
||||
|
||||
Args:
|
||||
run: The run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_run_create(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_run_create(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process a run upon creation.
|
||||
|
||||
Args:
|
||||
run: The created run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_run_update(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_run_update(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process a run upon update.
|
||||
|
||||
Args:
|
||||
run: The updated run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_llm_start(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_llm_start(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the LLM Run upon start.
|
||||
|
||||
Args:
|
||||
run: The LLM run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_llm_new_token(
|
||||
self,
|
||||
run: Run, # noqa: ARG002
|
||||
token: str, # noqa: ARG002
|
||||
chunk: GenerationChunk | ChatGenerationChunk | None, # noqa: ARG002
|
||||
run: Run,
|
||||
token: str,
|
||||
chunk: GenerationChunk | ChatGenerationChunk | None,
|
||||
) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process new LLM token.
|
||||
|
||||
@@ -573,100 +588,113 @@ class _TracerCore(ABC):
|
||||
token: The new token.
|
||||
chunk: Optional chunk.
|
||||
"""
|
||||
_ = (run, token, chunk)
|
||||
return None
|
||||
|
||||
def _on_llm_end(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_llm_end(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the LLM Run.
|
||||
|
||||
Args:
|
||||
run: The LLM run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_llm_error(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_llm_error(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the LLM Run upon error.
|
||||
|
||||
Args:
|
||||
run: The LLM run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_chain_start(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_chain_start(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Chain Run upon start.
|
||||
|
||||
Args:
|
||||
run: The chain run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_chain_end(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_chain_end(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Chain Run.
|
||||
|
||||
Args:
|
||||
run: The chain run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_chain_error(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_chain_error(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Chain Run upon error.
|
||||
|
||||
Args:
|
||||
run: The chain run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_tool_start(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_tool_start(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Tool Run upon start.
|
||||
|
||||
Args:
|
||||
run: The tool run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_tool_end(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_tool_end(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Tool Run.
|
||||
|
||||
Args:
|
||||
run: The tool run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_tool_error(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_tool_error(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Tool Run upon error.
|
||||
|
||||
Args:
|
||||
run: The tool run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_chat_model_start(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_chat_model_start(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Chat Model Run upon start.
|
||||
|
||||
Args:
|
||||
run: The chat model run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_retriever_start(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_retriever_start(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Retriever Run upon start.
|
||||
|
||||
Args:
|
||||
run: The retriever run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_retriever_end(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_retriever_end(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Retriever Run.
|
||||
|
||||
Args:
|
||||
run: The retriever run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
def _on_retriever_error(self, run: Run) -> Coroutine[Any, Any, None] | None: # noqa: ARG002
|
||||
def _on_retriever_error(self, run: Run) -> Coroutine[Any, Any, None] | None:
|
||||
"""Process the Retriever Run upon error.
|
||||
|
||||
Args:
|
||||
run: The retriever run.
|
||||
"""
|
||||
_ = run
|
||||
return None
|
||||
|
||||
@@ -13,6 +13,7 @@ import langsmith
|
||||
from langsmith.evaluation.evaluator import EvaluationResult, EvaluationResults
|
||||
|
||||
from langchain_core.tracers import langchain as langchain_tracer
|
||||
from langchain_core.tracers._compat import run_copy
|
||||
from langchain_core.tracers.base import BaseTracer
|
||||
from langchain_core.tracers.context import tracing_v2_enabled
|
||||
from langchain_core.tracers.langchain import _get_executor
|
||||
@@ -103,7 +104,7 @@ class EvaluatorCallbackHandler(BaseTracer):
|
||||
)
|
||||
else:
|
||||
self.executor = None
|
||||
self.futures = weakref.WeakSet()
|
||||
self.futures = weakref.WeakSet[Future[None]]()
|
||||
self.skip_unfinished = skip_unfinished
|
||||
self.project_name = project_name
|
||||
self.logged_eval_results = {}
|
||||
@@ -154,8 +155,8 @@ class EvaluatorCallbackHandler(BaseTracer):
|
||||
res
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _select_eval_results(
|
||||
self,
|
||||
results: EvaluationResult | EvaluationResults,
|
||||
) -> list[EvaluationResult]:
|
||||
if isinstance(results, EvaluationResult):
|
||||
@@ -206,7 +207,7 @@ class EvaluatorCallbackHandler(BaseTracer):
|
||||
if self.skip_unfinished and not run.outputs:
|
||||
logger.debug("Skipping unfinished run %s", run.id)
|
||||
return
|
||||
run_ = run.copy()
|
||||
run_ = run_copy(run)
|
||||
run_.reference_example_id = self.example_id
|
||||
for evaluator in self.evaluators:
|
||||
if self.executor is None:
|
||||
|
||||
@@ -12,7 +12,6 @@ from typing import (
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
from typing_extensions import NotRequired, override
|
||||
|
||||
@@ -47,6 +46,7 @@ from langchain_core.utils.uuid import uuid7
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
@@ -73,6 +73,8 @@ class RunInfo(TypedDict):
|
||||
"""The inputs to the run."""
|
||||
parent_run_id: UUID | None
|
||||
"""The ID of the parent run."""
|
||||
tool_call_id: NotRequired[str | None]
|
||||
"""The tool call ID associated with the run."""
|
||||
|
||||
|
||||
def _assign_name(name: str | None, serialized: dict[str, Any] | None) -> str:
|
||||
@@ -81,9 +83,9 @@ def _assign_name(name: str | None, serialized: dict[str, Any] | None) -> str:
|
||||
return name
|
||||
if serialized is not None:
|
||||
if "name" in serialized:
|
||||
return serialized["name"]
|
||||
return cast("str", serialized["name"])
|
||||
if "id" in serialized:
|
||||
return serialized["id"][-1]
|
||||
return cast("str", serialized["id"][-1])
|
||||
return "Unnamed"
|
||||
|
||||
|
||||
@@ -301,6 +303,10 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
# vs. None value.
|
||||
info["inputs"] = kwargs["inputs"]
|
||||
|
||||
if "tool_call_id" in kwargs:
|
||||
# Store tool_call_id in run info for linking errors to tool calls
|
||||
info["tool_call_id"] = kwargs["tool_call_id"]
|
||||
|
||||
self.run_map[run_id] = info
|
||||
self.parent_map[run_id] = parent_run_id
|
||||
|
||||
@@ -426,6 +432,10 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
"""Run on new output token. Only available when streaming is enabled.
|
||||
|
||||
For both chat models and non-chat models (legacy LLMs).
|
||||
|
||||
Raises:
|
||||
ValueError: If the run type is not `llm` or `chat_model`.
|
||||
AssertionError: If the run ID is not found in the run map.
|
||||
"""
|
||||
run_info = self.run_map.get(run_id)
|
||||
chunk_: GenerationChunk | BaseMessageChunk
|
||||
@@ -659,6 +669,7 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
name_=name_,
|
||||
run_type="tool",
|
||||
inputs=inputs,
|
||||
tool_call_id=kwargs.get("tool_call_id"),
|
||||
)
|
||||
|
||||
self._send(
|
||||
@@ -687,31 +698,31 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Run when tool errors."""
|
||||
# Extract tool_call_id from kwargs if passed directly, or from run_info
|
||||
# (which was stored during on_tool_start) as a fallback
|
||||
tool_call_id = kwargs.get("tool_call_id")
|
||||
run_info, inputs = self._get_tool_run_info_with_inputs(run_id)
|
||||
if tool_call_id is None:
|
||||
tool_call_id = run_info.get("tool_call_id")
|
||||
|
||||
self._send(
|
||||
{
|
||||
"event": "on_tool_error",
|
||||
"data": {
|
||||
"error": error,
|
||||
"input": inputs,
|
||||
},
|
||||
"run_id": str(run_id),
|
||||
"name": run_info["name"],
|
||||
"tags": run_info["tags"],
|
||||
"metadata": run_info["metadata"],
|
||||
"parent_ids": self._get_parent_ids(run_id),
|
||||
event: StandardStreamEvent = {
|
||||
"event": "on_tool_error",
|
||||
"data": {
|
||||
"error": error,
|
||||
"input": inputs,
|
||||
"tool_call_id": tool_call_id,
|
||||
},
|
||||
"tool",
|
||||
)
|
||||
"run_id": str(run_id),
|
||||
"name": run_info["name"],
|
||||
"tags": run_info["tags"],
|
||||
"metadata": run_info["metadata"],
|
||||
"parent_ids": self._get_parent_ids(run_id),
|
||||
}
|
||||
self._send(event, "tool")
|
||||
|
||||
@override
|
||||
async def on_tool_end(self, output: Any, *, run_id: UUID, **kwargs: Any) -> None:
|
||||
"""End a trace for a tool run.
|
||||
|
||||
Raises:
|
||||
AssertionError: If the run ID is a tool call and does not have inputs
|
||||
"""
|
||||
"""End a trace for a tool run."""
|
||||
run_info, inputs = self._get_tool_run_info_with_inputs(run_id)
|
||||
|
||||
self._send(
|
||||
|
||||
@@ -21,6 +21,8 @@ from typing_extensions import override
|
||||
|
||||
from langchain_core.env import get_runtime_environment
|
||||
from langchain_core.load import dumpd
|
||||
from langchain_core.messages.ai import UsageMetadata, add_usage
|
||||
from langchain_core.tracers._compat import run_construct, run_to_dict
|
||||
from langchain_core.tracers.base import BaseTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
|
||||
@@ -69,6 +71,32 @@ def _get_executor() -> ThreadPoolExecutor:
|
||||
return _EXECUTOR
|
||||
|
||||
|
||||
def _get_usage_metadata_from_generations(
|
||||
generations: list[list[dict[str, Any]]],
|
||||
) -> UsageMetadata | None:
|
||||
"""Extract and aggregate `usage_metadata` from generations.
|
||||
|
||||
Iterates through generations to find and aggregate all `usage_metadata` found in
|
||||
messages. This is typically present in chat model outputs.
|
||||
|
||||
Args:
|
||||
generations: List of generation batches, where each batch is a list
|
||||
of generation dicts that may contain a `'message'` key with
|
||||
`'usage_metadata'`.
|
||||
|
||||
Returns:
|
||||
The aggregated `usage_metadata` dict if found, otherwise `None`.
|
||||
"""
|
||||
output: UsageMetadata | None = None
|
||||
for generation_batch in generations:
|
||||
for generation in generation_batch:
|
||||
if isinstance(generation, dict) and "message" in generation:
|
||||
message = generation["message"]
|
||||
if isinstance(message, dict) and "usage_metadata" in message:
|
||||
output = add_usage(output, message["usage_metadata"])
|
||||
return output
|
||||
|
||||
|
||||
class LangChainTracer(BaseTracer):
|
||||
"""Implementation of the SharedTracer that POSTS to the LangChain endpoint."""
|
||||
|
||||
@@ -156,7 +184,7 @@ class LangChainTracer(BaseTracer):
|
||||
start_time=start_time,
|
||||
run_type="llm",
|
||||
tags=tags,
|
||||
name=name, # type: ignore[arg-type]
|
||||
name=name,
|
||||
)
|
||||
self._start_trace(chat_model_run)
|
||||
self._on_chat_model_start(chat_model_run)
|
||||
@@ -165,8 +193,9 @@ class LangChainTracer(BaseTracer):
|
||||
def _persist_run(self, run: Run) -> None:
|
||||
# We want to free up more memory by avoiding keeping a reference to the
|
||||
# whole nested run tree.
|
||||
self.latest_run = Run.construct(
|
||||
**run.dict(exclude={"child_runs", "inputs", "outputs"}),
|
||||
run_data = run_to_dict(run, exclude={"child_runs", "inputs", "outputs"})
|
||||
self.latest_run = run_construct(
|
||||
**run_data,
|
||||
inputs=run.inputs,
|
||||
outputs=run.outputs,
|
||||
)
|
||||
@@ -214,13 +243,20 @@ class LangChainTracer(BaseTracer):
|
||||
run.tags = self._get_tags(run)
|
||||
if run.ls_client is not self.client:
|
||||
run.ls_client = self.client
|
||||
# Mark whether inputs are real (not placeholder) so we can exclude them
|
||||
# from PATCH for normal invocations.
|
||||
#
|
||||
# Streaming invocations use {"input": ""} as placeholder in POST and send
|
||||
# real inputs in PATCH.
|
||||
run.extra["inputs_is_truthy"] = run.inputs != {"input": ""}
|
||||
run.post()
|
||||
except Exception as e:
|
||||
# Errors are swallowed by the thread executor so we need to log them here
|
||||
log_error_once("post", e)
|
||||
raise
|
||||
|
||||
def _update_run_single(self, run: Run) -> None:
|
||||
@staticmethod
|
||||
def _update_run_single(run: Run) -> None:
|
||||
"""Update a run."""
|
||||
if run.extra.get("__disabled"):
|
||||
return
|
||||
@@ -266,6 +302,15 @@ class LangChainTracer(BaseTracer):
|
||||
|
||||
def _on_llm_end(self, run: Run) -> None:
|
||||
"""Process the LLM Run."""
|
||||
# Extract usage_metadata from outputs and store in extra.metadata
|
||||
if run.outputs and "generations" in run.outputs:
|
||||
usage_metadata = _get_usage_metadata_from_generations(
|
||||
run.outputs["generations"]
|
||||
)
|
||||
if usage_metadata is not None:
|
||||
if "metadata" not in run.extra:
|
||||
run.extra["metadata"] = {}
|
||||
run.extra["metadata"]["usage_metadata"] = usage_metadata
|
||||
self._update_run_single(run)
|
||||
|
||||
def _on_llm_error(self, run: Run) -> None:
|
||||
@@ -276,15 +321,28 @@ class LangChainTracer(BaseTracer):
|
||||
"""Process the Chain Run upon start."""
|
||||
if run.parent_run_id is None:
|
||||
run.reference_example_id = self.example_id
|
||||
self._persist_run_single(run)
|
||||
# Skip persisting if inputs are deferred (e.g., iterator/generator inputs).
|
||||
# The run will be posted when _on_chain_end is called with realized inputs.
|
||||
if not run.extra.get("defers_inputs"):
|
||||
self._persist_run_single(run)
|
||||
|
||||
def _on_chain_end(self, run: Run) -> None:
|
||||
"""Process the Chain Run."""
|
||||
self._update_run_single(run)
|
||||
# If inputs were deferred, persist (POST) the run now that inputs are realized.
|
||||
# Otherwise, update (PATCH) the existing run.
|
||||
if run.extra.get("defers_inputs"):
|
||||
self._persist_run_single(run)
|
||||
else:
|
||||
self._update_run_single(run)
|
||||
|
||||
def _on_chain_error(self, run: Run) -> None:
|
||||
"""Process the Chain Run upon error."""
|
||||
self._update_run_single(run)
|
||||
# If inputs were deferred, persist (POST) the run now that inputs are realized.
|
||||
# Otherwise, update (PATCH) the existing run.
|
||||
if run.extra.get("defers_inputs"):
|
||||
self._persist_run_single(run)
|
||||
else:
|
||||
self._update_run_single(run)
|
||||
|
||||
def _on_tool_start(self, run: Run) -> None:
|
||||
"""Process the Tool Run upon start."""
|
||||
|
||||
@@ -541,7 +541,7 @@ class LogStreamCallbackHandler(BaseTracer, _StreamingCallbackHandler):
|
||||
|
||||
def _get_standardized_inputs(
|
||||
run: Run, schema_format: Literal["original", "streaming_events"]
|
||||
) -> dict[str, Any] | None:
|
||||
) -> Any:
|
||||
"""Extract standardized inputs from a run.
|
||||
|
||||
Standardizes the inputs based on the type of the runnable used.
|
||||
@@ -563,14 +563,14 @@ def _get_standardized_inputs(
|
||||
)
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
inputs = load(run.inputs)
|
||||
inputs = load(run.inputs, allowed_objects="all")
|
||||
|
||||
if run.run_type in {"retriever", "llm", "chat_model"}:
|
||||
return inputs
|
||||
|
||||
# new style chains
|
||||
# These nest an additional 'input' key inside the 'inputs' to make sure
|
||||
# the input is always a dict. We need to unpack and user the inner value.
|
||||
# the input is always a dict. We need to unpack and use the inner value.
|
||||
inputs = inputs["input"]
|
||||
# We should try to fix this in Runnables and callbacks/tracers
|
||||
# Runnables should be using a None type here not a placeholder
|
||||
@@ -595,7 +595,7 @@ def _get_standardized_outputs(
|
||||
Returns:
|
||||
An output if returned, otherwise a None
|
||||
"""
|
||||
outputs = load(run.outputs)
|
||||
outputs = load(run.outputs, allowed_objects="all")
|
||||
if schema_format == "original":
|
||||
if run.run_type == "prompt" and "output" in outputs:
|
||||
# These were previously dumped before the tracer.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.tracers._compat import run_copy
|
||||
from langchain_core.tracers.base import BaseTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
|
||||
@@ -35,6 +36,6 @@ class RunCollectorCallbackHandler(BaseTracer):
|
||||
Args:
|
||||
run: The run to be persisted.
|
||||
"""
|
||||
run_ = run.copy()
|
||||
run_ = run_copy(run)
|
||||
run_.reference_example_id = self.example_id
|
||||
self.traced_runs.append(run_)
|
||||
|
||||
@@ -7,8 +7,8 @@ def merge_dicts(left: dict[str, Any], *others: dict[str, Any]) -> dict[str, Any]
|
||||
r"""Merge dictionaries.
|
||||
|
||||
Merge many dicts, handling specific scenarios where a key exists in both
|
||||
dictionaries but has a value of None in 'left'. In such cases, the method uses the
|
||||
value from 'right' for that key in the merged dictionary.
|
||||
dictionaries but has a value of `None` in `'left'`. In such cases, the method uses
|
||||
the value from `'right'` for that key in the merged dictionary.
|
||||
|
||||
Args:
|
||||
left: The first dictionary to merge.
|
||||
@@ -22,11 +22,10 @@ def merge_dicts(left: dict[str, Any], *others: dict[str, Any]) -> dict[str, Any]
|
||||
TypeError: If the value has an unsupported type.
|
||||
|
||||
Example:
|
||||
If left = {"function_call": {"arguments": None}} and
|
||||
right = {"function_call": {"arguments": "{\n"}}
|
||||
then, after merging, for the key "function_call",
|
||||
the value from 'right' is used,
|
||||
resulting in merged = {"function_call": {"arguments": "{\n"}}.
|
||||
If `left = {"function_call": {"arguments": None}}` and
|
||||
`right = {"function_call": {"arguments": "{\n"}}`, then, after merging, for the
|
||||
key `'function_call'`, the value from `'right'` is used, resulting in
|
||||
`merged = {"function_call": {"arguments": "{\n"}}`.
|
||||
"""
|
||||
merged = left.copy()
|
||||
for right in others:
|
||||
@@ -58,7 +57,7 @@ def merge_dicts(left: dict[str, Any], *others: dict[str, Any]) -> dict[str, Any]
|
||||
# "all dicts."
|
||||
# )
|
||||
if (right_k == "index" and merged[right_k].startswith("lc_")) or (
|
||||
right_k in ("id", "output_version", "model_provider")
|
||||
right_k in {"id", "output_version", "model_provider"}
|
||||
and merged[right_k] == right_v
|
||||
):
|
||||
continue
|
||||
@@ -81,7 +80,7 @@ def merge_dicts(left: dict[str, Any], *others: dict[str, Any]) -> dict[str, Any]
|
||||
|
||||
|
||||
def merge_lists(left: list | None, *others: list | None) -> list | None:
|
||||
"""Add many lists, handling None.
|
||||
"""Add many lists, handling `None`.
|
||||
|
||||
Args:
|
||||
left: The first list to merge.
|
||||
@@ -156,9 +155,9 @@ def merge_lists(left: list | None, *others: list | None) -> list | None:
|
||||
def merge_obj(left: Any, right: Any) -> Any:
|
||||
"""Merge two objects.
|
||||
|
||||
It handles specific scenarios where a key exists in both
|
||||
dictionaries but has a value of None in 'left'. In such cases, the method uses the
|
||||
value from 'right' for that key in the merged dictionary.
|
||||
It handles specific scenarios where a key exists in both dictionaries but has a
|
||||
value of `None` in `'left'`. In such cases, the method uses the value from `'right'`
|
||||
for that key in the merged dictionary.
|
||||
|
||||
Args:
|
||||
left: The first object to merge.
|
||||
|
||||
@@ -130,7 +130,7 @@ async def tee_peer(
|
||||
if buffer:
|
||||
continue
|
||||
try:
|
||||
item = await iterator.__anext__()
|
||||
item = await anext(iterator)
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
else:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user