mirror of
https://github.com/hwchase17/langchain.git
synced 2026-02-05 00:30:18 +00:00
Compare commits
318 Commits
langchain=
...
langchain=
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc72a8c45a | ||
|
|
bc8620189c | ||
|
|
5a956b745f | ||
|
|
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 | ||
|
|
9f61ed8b81 | ||
|
|
6cff82d02e | ||
|
|
0cd72b50fb | ||
|
|
1a3cd46d88 | ||
|
|
470160cf81 | ||
|
|
20b8342fdf | ||
|
|
2f8af61218 | ||
|
|
81758e22f3 | ||
|
|
54241f4d06 | ||
|
|
7c9223d2b2 | ||
|
|
3342e4d62d | ||
|
|
5842110dbc | ||
|
|
62db04c43a | ||
|
|
fb892ee50a | ||
|
|
8ad0e9f267 | ||
|
|
d0b13e926d | ||
|
|
6fa4a45311 | ||
|
|
97dd5f2cb8 | ||
|
|
2a82fbc0ff | ||
|
|
0e5e33ba03 | ||
|
|
fc35544e0d | ||
|
|
15cc090e52 | ||
|
|
0f940d74b2 | ||
|
|
7829b722b1 | ||
|
|
914730cf8d | ||
|
|
c3738ea376 | ||
|
|
cd124a0949 | ||
|
|
57ff48e62e | ||
|
|
bc232e6d03 | ||
|
|
be32382d92 | ||
|
|
16c984ef0a | ||
|
|
13dd115d1d | ||
|
|
75d365418b | ||
|
|
2cff369cdc | ||
|
|
f5b6eecf72 | ||
|
|
a528ea1796 | ||
|
|
bf6a5eb122 | ||
|
|
5720dea41b | ||
|
|
087107557f | ||
|
|
05ba853548 | ||
|
|
3fb90666be | ||
|
|
6a2a149f89 | ||
|
|
bbc1d46efe | ||
|
|
d6b5f05f33 | ||
|
|
10377a7373 | ||
|
|
373ad8ac2c | ||
|
|
5eec11e2db | ||
|
|
badc0cf1b6 | ||
|
|
3b7abdff96 | ||
|
|
4aebfbad59 | ||
|
|
ae1f03fbe0 | ||
|
|
46dbb3967e | ||
|
|
dd0b990ba5 | ||
|
|
5aa46501cf | ||
|
|
92df109dd5 | ||
|
|
d27fb0c432 | ||
|
|
69dd39c461 | ||
|
|
41cebfe4fb | ||
|
|
5350967ddc | ||
|
|
7542278997 | ||
|
|
ff6e3558d7 | ||
|
|
585e12e53b | ||
|
|
73ba156a7d | ||
|
|
395c8d0bd4 | ||
|
|
34d31b8394 | ||
|
|
2aa0555941 | ||
|
|
dff229d018 | ||
|
|
b009ca4d23 | ||
|
|
0254c12cb0 | ||
|
|
2faed37ff1 | ||
|
|
d886dcfba5 | ||
|
|
91d5ca275d | ||
|
|
dcb670f395 | ||
|
|
85012ae601 | ||
|
|
aa0f4fb927 | ||
|
|
d18cdc6f32 | ||
|
|
8a5f46322b | ||
|
|
a0e86b18bf | ||
|
|
6affec92ce | ||
|
|
a64aee310c | ||
|
|
ba6c2590ae | ||
|
|
bb71f53585 | ||
|
|
9875ffbabc | ||
|
|
b5efafe80c | ||
|
|
ff3353f02f | ||
|
|
3ace4e3680 | ||
|
|
80c397019f | ||
|
|
4a42158e6c | ||
|
|
7ba3e80057 | ||
|
|
50e27a447b | ||
|
|
78c10f8790 | ||
|
|
ccfc9f795a | ||
|
|
b21926fe6c | ||
|
|
f1ad0da8f5 |
@@ -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
|
||||
132
.github/CODE_OF_CONDUCT.md
vendored
132
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,132 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
conduct@langchain.dev.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@@ -1,6 +0,0 @@
|
||||
# Contributing to LangChain
|
||||
|
||||
Hi there! Thank you for even being interested in contributing to LangChain.
|
||||
As an open-source project in a rapidly developing field, we are extremely open to contributions, whether they involve new features, improved infrastructure, better documentation, or bug fixes.
|
||||
|
||||
To learn how to contribute to LangChain, please follow the [contribution guide here](https://docs.langchain.com/oss/python/contributing).
|
||||
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.
|
||||
|
||||
|
||||
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.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,6 +10,6 @@ contact_links:
|
||||
- name: 📚 API Reference Documentation
|
||||
url: https://reference.langchain.com/python/
|
||||
about: View the official LangChain API reference documentation
|
||||
- name: 💬 LangChain Forum
|
||||
url: https://forum.langchain.com/
|
||||
about: Ask questions and get help from the community
|
||||
- name: 📚 Documentation issue
|
||||
url: https://github.com/langchain-ai/docs/issues/new?template=01-langchain.yml
|
||||
about: Report an issue related to the LangChain documentation
|
||||
|
||||
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 }}
|
||||
|
||||
330
.github/copilot-instructions.md
vendored
330
.github/copilot-instructions.md
vendored
@@ -1,330 +0,0 @@
|
||||
# Global Development Guidelines for LangChain Projects
|
||||
|
||||
## Core Development Principles
|
||||
|
||||
### 1. Maintain Stable Public Interfaces ⚠️ CRITICAL
|
||||
|
||||
**Always attempt to preserve function signatures, argument positions, and names for exported/public methods.**
|
||||
|
||||
❌ **Bad - Breaking Change:**
|
||||
|
||||
```python
|
||||
def get_user(id, verbose=False): # Changed from `user_id`
|
||||
pass
|
||||
```
|
||||
|
||||
✅ **Good - Stable Interface:**
|
||||
|
||||
```python
|
||||
def get_user(user_id: str, verbose: bool = False) -> User:
|
||||
"""Retrieve user by ID with optional verbose output."""
|
||||
pass
|
||||
```
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
|
||||
- Check if the function/class is exported in `__init__.py`
|
||||
- Look for existing usage patterns in tests and examples
|
||||
- Use keyword-only arguments for new parameters: `*, new_param: str = "default"`
|
||||
- Mark experimental features clearly with docstring admonitions (using MkDocs Material, like `!!! warning`)
|
||||
|
||||
🧠 *Ask yourself:* "Would this change break someone's code if they used it last week?"
|
||||
|
||||
### 2. Code Quality Standards
|
||||
|
||||
**All Python code MUST include type hints and return types.**
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```python
|
||||
def p(u, d):
|
||||
return [x for x in u if x not in d]
|
||||
```
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```python
|
||||
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
|
||||
"""Filter out users that are not in the known users set.
|
||||
|
||||
Args:
|
||||
users: List of user identifiers to filter.
|
||||
known_users: Set of known/valid user identifiers.
|
||||
|
||||
Returns:
|
||||
List of users that are not in the known_users set.
|
||||
"""
|
||||
return [user for user in users if user not in known_users]
|
||||
```
|
||||
|
||||
**Style Requirements:**
|
||||
|
||||
- Use descriptive, **self-explanatory variable names**. Avoid overly short or cryptic identifiers.
|
||||
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
|
||||
- Avoid unnecessary abstraction or premature optimization
|
||||
- Follow existing patterns in the codebase you're modifying
|
||||
|
||||
### 3. Testing Requirements
|
||||
|
||||
**Every new feature or bugfix MUST be covered by unit tests.**
|
||||
|
||||
**Test Organization:**
|
||||
|
||||
- Unit tests: `tests/unit_tests/` (no network calls allowed)
|
||||
- Integration tests: `tests/integration_tests/` (network calls permitted)
|
||||
- Use `pytest` as the testing framework
|
||||
|
||||
**Test Quality Checklist:**
|
||||
|
||||
- [ ] Tests fail when your new logic is broken
|
||||
- [ ] Happy path is covered
|
||||
- [ ] Edge cases and error conditions are tested
|
||||
- [ ] Use fixtures/mocks for external dependencies
|
||||
- [ ] Tests are deterministic (no flaky tests)
|
||||
|
||||
Checklist questions:
|
||||
|
||||
- [ ] Does the test suite fail if your new logic is broken?
|
||||
- [ ] Are all expected behaviors exercised (happy path, invalid input, etc)?
|
||||
- [ ] Do tests use fixtures or mocks where needed?
|
||||
|
||||
```python
|
||||
def test_filter_unknown_users():
|
||||
"""Test filtering unknown users from a list."""
|
||||
users = ["alice", "bob", "charlie"]
|
||||
known_users = {"alice", "bob"}
|
||||
|
||||
result = filter_unknown_users(users, known_users)
|
||||
|
||||
assert result == ["charlie"]
|
||||
assert len(result) == 1
|
||||
```
|
||||
|
||||
### 4. Security and Risk Assessment
|
||||
|
||||
**Security Checklist:**
|
||||
|
||||
- No `eval()`, `exec()`, or `pickle` on user-controlled input
|
||||
- Proper exception handling (no bare `except:`) and use a `msg` variable for error messages
|
||||
- Remove unreachable/commented code before committing
|
||||
- Race conditions or resource leaks (file handles, sockets, threads).
|
||||
- Ensure proper resource cleanup (file handles, connections)
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```python
|
||||
def load_config(path):
|
||||
with open(path) as f:
|
||||
return eval(f.read()) # ⚠️ Never eval config
|
||||
```
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
def load_config(path: str) -> dict:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
```
|
||||
|
||||
### 5. Documentation Standards
|
||||
|
||||
**Use Google-style docstrings with Args and Returns sections for all public functions.**
|
||||
|
||||
❌ **Insufficient Documentation:**
|
||||
|
||||
```python
|
||||
def send_email(to, msg):
|
||||
"""Send an email to a recipient."""
|
||||
```
|
||||
|
||||
✅ **Complete Documentation:**
|
||||
|
||||
```python
|
||||
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
|
||||
"""
|
||||
Send an email to a recipient with specified priority.
|
||||
|
||||
Args:
|
||||
to: The email address of the recipient.
|
||||
msg: The message body to send.
|
||||
priority: Email priority level.
|
||||
|
||||
Returns:
|
||||
True if email was sent successfully, False otherwise.
|
||||
|
||||
Raises:
|
||||
InvalidEmailError: If the email address format is invalid.
|
||||
SMTPConnectionError: If unable to connect to email server.
|
||||
"""
|
||||
```
|
||||
|
||||
**Documentation Guidelines:**
|
||||
|
||||
- Types go in function signatures, NOT in docstrings
|
||||
- Focus on "why" rather than "what" in descriptions
|
||||
- Document all parameters, return values, and exceptions
|
||||
- Keep descriptions concise but clear
|
||||
|
||||
📌 *Tip:* Keep descriptions concise but clear. Only document return values if non-obvious.
|
||||
|
||||
### 6. Architectural Improvements
|
||||
|
||||
**When you encounter code that could be improved, suggest better designs:**
|
||||
|
||||
❌ **Poor Design:**
|
||||
|
||||
```python
|
||||
def process_data(data, db_conn, email_client, logger):
|
||||
# Function doing too many things
|
||||
validated = validate_data(data)
|
||||
result = db_conn.save(validated)
|
||||
email_client.send_notification(result)
|
||||
logger.log(f"Processed {len(data)} items")
|
||||
return result
|
||||
```
|
||||
|
||||
✅ **Better Design:**
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ProcessingResult:
|
||||
"""Result of data processing operation."""
|
||||
items_processed: int
|
||||
success: bool
|
||||
errors: List[str] = field(default_factory=list)
|
||||
|
||||
class DataProcessor:
|
||||
"""Handles data validation, storage, and notification."""
|
||||
|
||||
def __init__(self, db_conn: Database, email_client: EmailClient):
|
||||
self.db = db_conn
|
||||
self.email = email_client
|
||||
|
||||
def process(self, data: List[dict]) -> ProcessingResult:
|
||||
"""Process and store data with notifications.
|
||||
|
||||
Args:
|
||||
data: List of data items to process.
|
||||
|
||||
Returns:
|
||||
ProcessingResult with details of the operation.
|
||||
"""
|
||||
validated = self._validate_data(data)
|
||||
result = self.db.save(validated)
|
||||
self._notify_completion(result)
|
||||
return result
|
||||
```
|
||||
|
||||
**Design Improvement Areas:**
|
||||
|
||||
If there's a **cleaner**, **more scalable**, or **simpler** design, highlight it and suggest improvements that would:
|
||||
|
||||
- Reduce code duplication through shared utilities
|
||||
- Make unit testing easier
|
||||
- Improve separation of concerns (single responsibility)
|
||||
- Make unit testing easier through dependency injection
|
||||
- Add clarity without adding complexity
|
||||
- Prefer dataclasses for structured data
|
||||
|
||||
## Development Tools & Commands
|
||||
|
||||
### Package Management
|
||||
|
||||
```bash
|
||||
# Add package
|
||||
uv add package-name
|
||||
|
||||
# Sync project dependencies
|
||||
uv sync
|
||||
uv lock
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run unit tests (no network)
|
||||
make test
|
||||
|
||||
# Don't run integration tests, as API keys must be set
|
||||
|
||||
# Run specific test file
|
||||
uv run --group test pytest tests/unit_tests/test_specific.py
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Lint code
|
||||
make lint
|
||||
|
||||
# Format code
|
||||
make format
|
||||
|
||||
# Type checking
|
||||
uv run --group lint mypy .
|
||||
```
|
||||
|
||||
### Dependency Management Patterns
|
||||
|
||||
**Local Development Dependencies:**
|
||||
|
||||
```toml
|
||||
[tool.uv.sources]
|
||||
langchain-core = { path = "../core", editable = true }
|
||||
langchain-tests = { path = "../standard-tests", editable = true }
|
||||
```
|
||||
|
||||
**For tools, use the `@tool` decorator from `langchain_core.tools`:**
|
||||
|
||||
```python
|
||||
from langchain_core.tools import tool
|
||||
|
||||
@tool
|
||||
def search_database(query: str) -> str:
|
||||
"""Search the database for relevant information.
|
||||
|
||||
Args:
|
||||
query: The search query string.
|
||||
"""
|
||||
# Implementation here
|
||||
return results
|
||||
```
|
||||
|
||||
## Commit Standards
|
||||
|
||||
**Use Conventional Commits format for PR titles:**
|
||||
|
||||
- `feat(core): add multi-tenant support`
|
||||
- `!fix(cli): resolve flag parsing error` (breaking change uses exclamation mark)
|
||||
- `docs: update API usage examples`
|
||||
- `docs(openai): update API usage examples`
|
||||
|
||||
## Framework-Specific Guidelines
|
||||
|
||||
- Follow the existing patterns in `langchain_core` for base abstractions
|
||||
- Implement proper streaming support where applicable
|
||||
- Avoid deprecated components
|
||||
|
||||
### Partner Integrations
|
||||
|
||||
- Follow the established patterns in existing partner libraries
|
||||
- Implement standard interfaces (`BaseChatModel`, `BaseEmbeddings`, etc.)
|
||||
- Include comprehensive integration tests
|
||||
- Document API key requirements and authentication
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Checklist
|
||||
|
||||
Before submitting code changes:
|
||||
|
||||
- [ ] **Breaking Changes**: Verified no public API changes
|
||||
- [ ] **Type Hints**: All functions have complete type annotations
|
||||
- [ ] **Tests**: New functionality is fully tested
|
||||
- [ ] **Security**: No dangerous patterns (eval, silent failures, etc.)
|
||||
- [ ] **Documentation**: Google-style docstrings for public functions
|
||||
- [ ] **Code Quality**: `make lint` and `make format` pass
|
||||
- [ ] **Architecture**: Suggested improvements where applicable
|
||||
- [ ] **Commit Message**: Follows Conventional Commits format
|
||||
30
.github/pr-file-labeler.yml
vendored
30
.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,22 +131,3 @@ dependencies:
|
||||
- "uv.lock"
|
||||
- "**/requirements*.txt"
|
||||
- "**/poetry.lock"
|
||||
|
||||
# Documentation
|
||||
documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "**/*.md"
|
||||
- "**/*.rst"
|
||||
- "**/README*"
|
||||
|
||||
# Security related changes
|
||||
security:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "**/*security*"
|
||||
- "**/*auth*"
|
||||
- "**/*credential*"
|
||||
- "**/*secret*"
|
||||
- "**/*token*"
|
||||
- ".github/workflows/security*"
|
||||
|
||||
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: |
|
||||
|
||||
83
.github/workflows/_release.yml
vendored
83
.github/workflows/_release.yml
vendored
@@ -19,7 +19,7 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
description: "From which folder this pipeline executes"
|
||||
default: "libs/langchain"
|
||||
default: "libs/langchain_v1"
|
||||
release-version:
|
||||
required: true
|
||||
type: string
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -394,9 +394,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
if: false # temporarily skip
|
||||
strategy:
|
||||
matrix:
|
||||
partner: [anthropic]
|
||||
partner: [openai, anthropic]
|
||||
fail-fast: false # Continue testing other partners if one fails
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
@@ -430,7 +431,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
if: startsWith(inputs.working-directory, 'libs/core')
|
||||
with:
|
||||
name: dist
|
||||
@@ -470,6 +471,67 @@ jobs:
|
||||
uv pip install ../../core/dist/*.whl
|
||||
make integration_tests
|
||||
|
||||
# Test external packages that depend on langchain-core/langchain against the new release
|
||||
# Only runs for core and langchain_v1 releases to catch breaking changes before publish
|
||||
test-dependents:
|
||||
name: "🐍 Python ${{ matrix.python-version }}: ${{ matrix.package.path }}"
|
||||
needs:
|
||||
- build
|
||||
- release-notes
|
||||
- test-pypi-publish
|
||||
- pre-release-checks
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
# Only run for core or langchain_v1 releases
|
||||
if: startsWith(inputs.working-directory, 'libs/core') || startsWith(inputs.working-directory, 'libs/langchain_v1')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11", "3.13"]
|
||||
package:
|
||||
- name: deepagents
|
||||
repo: langchain-ai/deepagents
|
||||
path: libs/deepagents
|
||||
# No API keys needed for now - deepagents `make test` only runs unit tests
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
path: langchain
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ matrix.package.repo }}
|
||||
path: ${{ matrix.package.name }}
|
||||
|
||||
- name: Set up Python + uv
|
||||
uses: "./langchain/.github/actions/uv_setup"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Install ${{ matrix.package.name }} with local packages
|
||||
# External dependents don't have [tool.uv.sources] pointing to this repo,
|
||||
# so we install the package normally then override with the built wheel.
|
||||
run: |
|
||||
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
|
||||
|
||||
# Install the package with test dependencies
|
||||
uv sync --group test
|
||||
|
||||
# Override with the built wheel from this release
|
||||
uv pip install $GITHUB_WORKSPACE/dist/*.whl
|
||||
|
||||
- name: Run ${{ matrix.package.name }} tests
|
||||
run: |
|
||||
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
|
||||
make test
|
||||
|
||||
publish:
|
||||
# Publishes the package to PyPI
|
||||
needs:
|
||||
@@ -477,7 +539,10 @@ jobs:
|
||||
- release-notes
|
||||
- test-pypi-publish
|
||||
- pre-release-checks
|
||||
- test-prior-published-packages-against-new-core
|
||||
- test-dependents
|
||||
# - test-prior-published-packages-against-new-core
|
||||
# Run if all needed jobs succeeded or were skipped (test-dependents only runs for core/langchain_v1)
|
||||
if: ${{ !cancelled() && !failure() }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# This permission is used for trusted publishing:
|
||||
@@ -499,7 +564,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
@@ -539,7 +604,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: ${{ inputs.working-directory }}/dist/
|
||||
|
||||
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:
|
||||
|
||||
10
.github/workflows/pr_lint.yml
vendored
10
.github/workflows/pr_lint.yml
vendored
@@ -27,12 +27,18 @@
|
||||
# * release — prepare a new release
|
||||
#
|
||||
# Allowed Scope(s) (optional):
|
||||
# core, cli, langchain, langchain_v1, langchain-classic, model-profiles,
|
||||
# core, cli, langchain, langchain-classic, model-profiles,
|
||||
# standard-tests, text-splitters, docs, anthropic, chroma, deepseek, exa,
|
||||
# fireworks, groq, huggingface, mistralai, nomic, ollama, openai,
|
||||
# perplexity, prompty, qdrant, xai, infra, deps
|
||||
#
|
||||
# Multiple scopes can be used by separating them with a comma.
|
||||
# Multiple scopes can be used by separating them with a comma. For example:
|
||||
#
|
||||
# feat(core,cli): add multi‐tenant support to core and cli
|
||||
#
|
||||
# Note: PRs touching the langchain package should use the 'langchain' scope. It is not
|
||||
# acceptable to omit the scope for changes to the langchain package, despite it being
|
||||
# the main package & name of the repo.
|
||||
#
|
||||
# Rules:
|
||||
# 1. The 'Type' must start with a lowercase letter.
|
||||
|
||||
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}`);
|
||||
8
.github/workflows/v1_changes.md
vendored
8
.github/workflows/v1_changes.md
vendored
@@ -1,8 +0,0 @@
|
||||
With the deprecation of v0 docs, the following files will need to be migrated/supported
|
||||
in the new docs repo:
|
||||
|
||||
- run_notebooks.yml: New repo should run Integration tests on code snippets?
|
||||
- people.yml: Need to fix and somehow display on the new docs site
|
||||
- Subsequently, `.github/actions/people/`
|
||||
- _test_doc_imports.yml
|
||||
- check-broken-links.yml
|
||||
@@ -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",
|
||||
|
||||
11
AGENTS.md
11
AGENTS.md
@@ -72,7 +72,15 @@ uv run --group lint mypy .
|
||||
|
||||
#### Commit standards
|
||||
|
||||
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes.
|
||||
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes. Note that all commit/PR titles should be in lowercase with the exception of proper nouns/named entities. All PR titles should include a scope with no exceptions. For example:
|
||||
|
||||
```txt
|
||||
feat(langchain): add new chat completion feature
|
||||
fix(core): resolve type hinting issue in vector store
|
||||
chore(anthropic): update infrastructure dependencies
|
||||
```
|
||||
|
||||
Note how `feat(langchain)` includes a scope even though it is the main package and name of the repo.
|
||||
|
||||
#### Pull request guidelines
|
||||
|
||||
@@ -85,6 +93,7 @@ Suggest PR titles that follow Conventional Commits format. Refer to .github/work
|
||||
### Maintain stable public interfaces
|
||||
|
||||
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
|
||||
You should warn the developer for any function signature changes, regardless of whether they look breaking or not.
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
|
||||
|
||||
11
CLAUDE.md
11
CLAUDE.md
@@ -72,7 +72,15 @@ uv run --group lint mypy .
|
||||
|
||||
#### Commit standards
|
||||
|
||||
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes.
|
||||
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes. Note that all commit/PR titles should be in lowercase with the exception of proper nouns/named entities. All PR titles should include a scope with no exceptions. For example:
|
||||
|
||||
```txt
|
||||
feat(langchain): add new chat completion feature
|
||||
fix(core): resolve type hinting issue in vector store
|
||||
chore(anthropic): update infrastructure dependencies
|
||||
```
|
||||
|
||||
Note how `feat(langchain)` includes a scope even though it is the main package and name of the repo.
|
||||
|
||||
#### Pull request guidelines
|
||||
|
||||
@@ -85,6 +93,7 @@ Suggest PR titles that follow Conventional Commits format. Refer to .github/work
|
||||
### Maintain stable public interfaces
|
||||
|
||||
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
|
||||
You should warn the developer for any function signature changes, regardless of whether they look breaking or not.
|
||||
|
||||
**Before making ANY changes to public APIs:**
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
80
SECURITY.md
80
SECURITY.md
@@ -1,80 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
LangChain has a large ecosystem of integrations with various external resources like local and remote file systems, APIs and databases. These integrations allow developers to create versatile applications that combine the power of LLMs with the ability to access, interact with and manipulate external resources.
|
||||
|
||||
## Best practices
|
||||
|
||||
When building such applications, developers should remember to follow good security practices:
|
||||
|
||||
* [**Limit Permissions**](https://en.wikipedia.org/wiki/Principle_of_least_privilege): Scope permissions specifically to the application's need. Granting broad or excessive permissions can introduce significant security vulnerabilities. To avoid such vulnerabilities, consider using read-only credentials, disallowing access to sensitive resources, using sandboxing techniques (such as running inside a container), specifying proxy configurations to control external requests, etc., as appropriate for your application.
|
||||
* **Anticipate Potential Misuse**: Just as humans can err, so can Large Language Models (LLMs). Always assume that any system access or credentials may be used in any way allowed by the permissions they are assigned. For example, if a pair of database credentials allows deleting data, it's safest to assume that any LLM able to use those credentials may in fact delete data.
|
||||
* [**Defense in Depth**](https://en.wikipedia.org/wiki/Defense_in_depth_(computing)): No security technique is perfect. Fine-tuning and good chain design can reduce, but not eliminate, the odds that a Large Language Model (LLM) may make a mistake. It's best to combine multiple layered security approaches rather than relying on any single layer of defense to ensure security. For example: use both read-only permissions and sandboxing to ensure that LLMs are only able to access data that is explicitly meant for them to use.
|
||||
|
||||
Risks of not doing so include, but are not limited to:
|
||||
|
||||
* Data corruption or loss.
|
||||
* Unauthorized access to confidential information.
|
||||
* Compromised performance or availability of critical resources.
|
||||
|
||||
Example scenarios with mitigation strategies:
|
||||
|
||||
* A user may ask an agent with access to the file system to delete files that should not be deleted or read the content of files that contain sensitive information. To mitigate, limit the agent to only use a specific directory and only allow it to read or write files that are safe to read or write. Consider further sandboxing the agent by running it in a container.
|
||||
* A user may ask an agent with write access to an external API to write malicious data to the API, or delete data from that API. To mitigate, give the agent read-only API keys, or limit it to only use endpoints that are already resistant to such misuse.
|
||||
* A user may ask an agent with access to a database to drop a table or mutate the schema. To mitigate, scope the credentials to only the tables that the agent needs to access and consider issuing READ-ONLY credentials.
|
||||
|
||||
If you're building applications that access external resources like file systems, APIs or databases, consider speaking with your company's security team to determine how to best design and secure your applications.
|
||||
|
||||
## Reporting OSS Vulnerabilities
|
||||
|
||||
LangChain is partnered with [huntr by Protect AI](https://huntr.com/) to provide
|
||||
a bounty program for our open source projects.
|
||||
|
||||
Please report security vulnerabilities associated with the LangChain
|
||||
open source projects at [huntr](https://huntr.com/bounties/disclose/?target=https%3A%2F%2Fgithub.com%2Flangchain-ai%2Flangchain&validSearch=true).
|
||||
|
||||
Before reporting a vulnerability, please review:
|
||||
|
||||
1) In-Scope Targets and Out-of-Scope Targets below.
|
||||
2) The [langchain-ai/langchain](https://docs.langchain.com/oss/python/contributing/code#repository-structure) monorepo structure.
|
||||
3) The [Best Practices](#best-practices) above to understand what we consider to be a security vulnerability vs. developer responsibility.
|
||||
|
||||
### In-Scope Targets
|
||||
|
||||
The following packages and repositories are eligible for bug bounties:
|
||||
|
||||
* langchain-core
|
||||
* langchain (see exceptions)
|
||||
* langchain-community (see exceptions)
|
||||
* langgraph
|
||||
* langserve
|
||||
|
||||
### Out of Scope Targets
|
||||
|
||||
All out of scope targets defined by huntr as well as:
|
||||
|
||||
* **langchain-experimental**: This repository is for experimental code and is not
|
||||
eligible for bug bounties (see [package warning](https://pypi.org/project/langchain-experimental/)), bug reports to it will be marked as interesting or waste of
|
||||
time and published with no bounty attached.
|
||||
* **tools**: Tools in either `langchain` or `langchain-community` are not eligible for bug
|
||||
bounties. This includes the following directories
|
||||
* `libs/langchain/langchain/tools`
|
||||
* `libs/community/langchain_community/tools`
|
||||
* Please review the [Best Practices](#best-practices)
|
||||
for more details, but generally tools interact with the real world. Developers are
|
||||
expected to understand the security implications of their code and are responsible
|
||||
for the security of their tools.
|
||||
* Code documented with security notices. This will be decided on a case-by-case basis, but likely will not be eligible for a bounty as the code is already
|
||||
documented with guidelines for developers that should be followed for making their
|
||||
application secure.
|
||||
* Any LangSmith related repositories or APIs (see [Reporting LangSmith Vulnerabilities](#reporting-langsmith-vulnerabilities)).
|
||||
|
||||
## Reporting LangSmith Vulnerabilities
|
||||
|
||||
Please report security vulnerabilities associated with LangSmith by email to `security@langchain.dev`.
|
||||
|
||||
* LangSmith site: [https://smith.langchain.com](https://smith.langchain.com)
|
||||
* SDK client: [https://github.com/langchain-ai/langsmith-sdk](https://github.com/langchain-ai/langsmith-sdk)
|
||||
|
||||
### Other Security Concerns
|
||||
|
||||
For any other security concerns, please contact us at `security@langchain.dev`.
|
||||
20
libs/Makefile
Normal file
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
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ def get_usage_metadata_callback(
|
||||
"""Get usage metadata callback.
|
||||
|
||||
Get context manager for tracking usage metadata across chat model calls using
|
||||
`AIMessage.usage_metadata`.
|
||||
[`AIMessage.usage_metadata`][langchain.messages.AIMessage.usage_metadata].
|
||||
|
||||
Args:
|
||||
name: The name of the context variable.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Language models.
|
||||
"""Core language model abstractions.
|
||||
|
||||
LangChain has two main classes to work with language models: chat models and
|
||||
"old-fashioned" LLMs.
|
||||
"old-fashioned" LLMs (string-in, string-out).
|
||||
|
||||
**Chat models**
|
||||
|
||||
@@ -11,14 +11,16 @@ as outputs (as opposed to using plain text).
|
||||
Chat models support the assignment of distinct roles to conversation messages, helping
|
||||
to distinguish messages from the AI, users, and instructions such as system messages.
|
||||
|
||||
The key abstraction for chat models is `BaseChatModel`. Implementations should inherit
|
||||
from this class.
|
||||
The key abstraction for chat models is
|
||||
[`BaseChatModel`][langchain_core.language_models.BaseChatModel]. Implementations should
|
||||
inherit from this class.
|
||||
|
||||
See existing [chat model integrations](https://docs.langchain.com/oss/python/integrations/chat).
|
||||
|
||||
**LLMs**
|
||||
**LLMs (legacy)**
|
||||
|
||||
Language models that takes a string as input and returns a string.
|
||||
|
||||
These are traditionally older models (newer models generally are chat models).
|
||||
|
||||
Although the underlying models are string in, string out, the LangChain wrappers also
|
||||
|
||||
@@ -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:
|
||||
@@ -1578,86 +1596,86 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
depends on the `schema` as described above.
|
||||
- `'parsing_error'`: `BaseException | None`
|
||||
|
||||
Example: Pydantic schema (`include_raw=False`):
|
||||
???+ example "Pydantic schema (`include_raw=False`)"
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AnswerWithJustification(BaseModel):
|
||||
'''An answer to the user question along with justification for the answer.'''
|
||||
class AnswerWithJustification(BaseModel):
|
||||
'''An answer to the user question along with justification for the answer.'''
|
||||
|
||||
answer: str
|
||||
justification: str
|
||||
answer: str
|
||||
justification: str
|
||||
|
||||
|
||||
model = ChatModel(model="model-name", temperature=0)
|
||||
structured_model = model.with_structured_output(AnswerWithJustification)
|
||||
model = ChatModel(model="model-name", temperature=0)
|
||||
structured_model = model.with_structured_output(AnswerWithJustification)
|
||||
|
||||
structured_model.invoke(
|
||||
"What weighs more a pound of bricks or a pound of feathers"
|
||||
)
|
||||
structured_model.invoke(
|
||||
"What weighs more a pound of bricks or a pound of feathers"
|
||||
)
|
||||
|
||||
# -> AnswerWithJustification(
|
||||
# answer='They weigh the same',
|
||||
# justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'
|
||||
# )
|
||||
```
|
||||
# -> AnswerWithJustification(
|
||||
# answer='They weigh the same',
|
||||
# justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'
|
||||
# )
|
||||
```
|
||||
|
||||
Example: Pydantic schema (`include_raw=True`):
|
||||
??? example "Pydantic schema (`include_raw=True`)"
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AnswerWithJustification(BaseModel):
|
||||
'''An answer to the user question along with justification for the answer.'''
|
||||
class AnswerWithJustification(BaseModel):
|
||||
'''An answer to the user question along with justification for the answer.'''
|
||||
|
||||
answer: str
|
||||
justification: str
|
||||
answer: str
|
||||
justification: str
|
||||
|
||||
|
||||
model = ChatModel(model="model-name", temperature=0)
|
||||
structured_model = model.with_structured_output(
|
||||
AnswerWithJustification, include_raw=True
|
||||
)
|
||||
model = ChatModel(model="model-name", temperature=0)
|
||||
structured_model = model.with_structured_output(
|
||||
AnswerWithJustification, include_raw=True
|
||||
)
|
||||
|
||||
structured_model.invoke(
|
||||
"What weighs more a pound of bricks or a pound of feathers"
|
||||
)
|
||||
# -> {
|
||||
# 'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Ao02pnFYXD6GN1yzc0uXPsvF', 'function': {'arguments': '{"answer":"They weigh the same.","justification":"Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ."}', 'name': 'AnswerWithJustification'}, 'type': 'function'}]}),
|
||||
# 'parsed': AnswerWithJustification(answer='They weigh the same.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'),
|
||||
# 'parsing_error': None
|
||||
# }
|
||||
```
|
||||
structured_model.invoke(
|
||||
"What weighs more a pound of bricks or a pound of feathers"
|
||||
)
|
||||
# -> {
|
||||
# 'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Ao02pnFYXD6GN1yzc0uXPsvF', 'function': {'arguments': '{"answer":"They weigh the same.","justification":"Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ."}', 'name': 'AnswerWithJustification'}, 'type': 'function'}]}),
|
||||
# 'parsed': AnswerWithJustification(answer='They weigh the same.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'),
|
||||
# 'parsing_error': None
|
||||
# }
|
||||
```
|
||||
|
||||
Example: Dictionary schema (`include_raw=False`):
|
||||
??? example "Dictionary schema (`include_raw=False`)"
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from langchain_core.utils.function_calling import convert_to_openai_tool
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from langchain_core.utils.function_calling import convert_to_openai_tool
|
||||
|
||||
|
||||
class AnswerWithJustification(BaseModel):
|
||||
'''An answer to the user question along with justification for the answer.'''
|
||||
class AnswerWithJustification(BaseModel):
|
||||
'''An answer to the user question along with justification for the answer.'''
|
||||
|
||||
answer: str
|
||||
justification: str
|
||||
answer: str
|
||||
justification: str
|
||||
|
||||
|
||||
dict_schema = convert_to_openai_tool(AnswerWithJustification)
|
||||
model = ChatModel(model="model-name", temperature=0)
|
||||
structured_model = model.with_structured_output(dict_schema)
|
||||
dict_schema = convert_to_openai_tool(AnswerWithJustification)
|
||||
model = ChatModel(model="model-name", temperature=0)
|
||||
structured_model = model.with_structured_output(dict_schema)
|
||||
|
||||
structured_model.invoke(
|
||||
"What weighs more a pound of bricks or a pound of feathers"
|
||||
)
|
||||
# -> {
|
||||
# 'answer': 'They weigh the same',
|
||||
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'
|
||||
# }
|
||||
```
|
||||
structured_model.invoke(
|
||||
"What weighs more a pound of bricks or a pound of feathers"
|
||||
)
|
||||
# -> {
|
||||
# 'answer': 'They weigh the same',
|
||||
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'
|
||||
# }
|
||||
```
|
||||
|
||||
!!! warning "Behavior changed in `langchain-core` 0.2.26"
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_background_tasks: set[asyncio.Task] = set()
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _log_error_once(msg: str) -> None:
|
||||
@@ -100,9 +102,9 @@ def create_base_retry_decorator(
|
||||
asyncio.run(coro)
|
||||
else:
|
||||
if loop.is_running():
|
||||
# TODO: Fix RUF006 - this task should have a reference
|
||||
# and be awaited somewhere
|
||||
loop.create_task(coro) # noqa: RUF006
|
||||
task = loop.create_task(coro)
|
||||
_background_tasks.add(task)
|
||||
task.add_done_callback(_background_tasks.discard)
|
||||
else:
|
||||
asyncio.run(coro)
|
||||
except Exception as e:
|
||||
@@ -299,7 +301,10 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
|
||||
@functools.cached_property
|
||||
def _serialized(self) -> dict[str, Any]:
|
||||
return dumpd(self)
|
||||
# self is always a Serializable object in this case, thus the result is
|
||||
# guaranteed to be a dict since dumps uses the default callback, which uses
|
||||
# obj.to_json which always returns TypedDict subclasses
|
||||
return cast("dict[str, Any]", dumpd(self))
|
||||
|
||||
# --- Runnable methods ---
|
||||
|
||||
|
||||
@@ -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,11 +1,83 @@
|
||||
"""Load LangChain objects from JSON strings or objects."""
|
||||
"""Load LangChain objects from JSON strings or objects.
|
||||
|
||||
## How it works
|
||||
|
||||
Each `Serializable` LangChain object has a unique identifier (its "class path"), which
|
||||
is a list of strings representing the module path and class name. For example:
|
||||
|
||||
- `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
|
||||
- `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
|
||||
|
||||
When deserializing, the class path from the JSON `'id'` field is checked against an
|
||||
allowlist. If the class is not in the allowlist, deserialization raises a `ValueError`.
|
||||
|
||||
## Security model
|
||||
|
||||
The `allowed_objects` parameter controls which classes can be deserialized:
|
||||
|
||||
- **`'core'` (default)**: Allow classes defined in the serialization mappings for
|
||||
langchain_core.
|
||||
- **`'all'`**: Allow classes defined in the serialization mappings. This
|
||||
includes core LangChain types (messages, prompts, documents, etc.) and trusted
|
||||
partner integrations. See `langchain_core.load.mapping` for the full list.
|
||||
- **Explicit list of classes**: Only those specific classes are allowed.
|
||||
|
||||
For simple data types like messages and documents, the default allowlist is safe to use.
|
||||
These classes do not perform side effects during initialization.
|
||||
|
||||
!!! note "Side effects in allowed classes"
|
||||
|
||||
Deserialization calls `__init__` on allowed classes. If those classes perform side
|
||||
effects during initialization (network calls, file operations, etc.), those side
|
||||
effects will occur. The allowlist prevents instantiation of classes outside the
|
||||
allowlist, but does not sandbox the allowed classes themselves.
|
||||
|
||||
Import paths are also validated against trusted namespaces before any module is
|
||||
imported.
|
||||
|
||||
### Injection protection (escape-based)
|
||||
|
||||
During serialization, plain dicts that contain an `'lc'` key are escaped by wrapping
|
||||
them: `{"__lc_escaped__": {...}}`. During deserialization, escaped dicts are unwrapped
|
||||
and returned as plain dicts, NOT instantiated as LC objects.
|
||||
|
||||
This is an allowlist approach: only dicts explicitly produced by
|
||||
`Serializable.to_json()` (which are NOT escaped) are treated as LC objects;
|
||||
everything else is user data.
|
||||
|
||||
Even if an attacker's payload includes `__lc_escaped__` wrappers, it will be unwrapped
|
||||
to plain dicts and NOT instantiated as malicious objects.
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
from langchain_core.load import load
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
# Use default allowlist (classes from mappings) - recommended
|
||||
obj = load(data)
|
||||
|
||||
# Allow only specific classes (most restrictive)
|
||||
obj = load(
|
||||
data,
|
||||
allowed_objects=[
|
||||
ChatPromptTemplate,
|
||||
AIMessage,
|
||||
HumanMessage,
|
||||
],
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
from langchain_core._api import beta
|
||||
from langchain_core.load._validation import _is_escaped_dict, _unescape_value
|
||||
from langchain_core.load.mapping import (
|
||||
_JS_SERIALIZABLE_MAPPING,
|
||||
_OG_SERIALIZABLE_MAPPING,
|
||||
@@ -44,34 +116,209 @@ ALL_SERIALIZABLE_MAPPINGS = {
|
||||
**_JS_SERIALIZABLE_MAPPING,
|
||||
}
|
||||
|
||||
# Cache for the default allowed class paths computed from mappings
|
||||
# Maps mode ("all" or "core") to the cached set of paths
|
||||
_default_class_paths_cache: dict[str, set[tuple[str, ...]]] = {}
|
||||
|
||||
|
||||
def _get_default_allowed_class_paths(
|
||||
allowed_object_mode: Literal["all", "core"],
|
||||
) -> set[tuple[str, ...]]:
|
||||
"""Get the default allowed class paths from the serialization mappings.
|
||||
|
||||
This uses the mappings as the source of truth for what classes are allowed
|
||||
by default. Both the legacy paths (keys) and current paths (values) are included.
|
||||
|
||||
Args:
|
||||
allowed_object_mode: either `'all'` or `'core'`.
|
||||
|
||||
Returns:
|
||||
Set of class path tuples that are allowed by default.
|
||||
"""
|
||||
if allowed_object_mode in _default_class_paths_cache:
|
||||
return _default_class_paths_cache[allowed_object_mode]
|
||||
|
||||
allowed_paths: set[tuple[str, ...]] = set()
|
||||
for key, value in ALL_SERIALIZABLE_MAPPINGS.items():
|
||||
if allowed_object_mode == "core" and value[0] != "langchain_core":
|
||||
continue
|
||||
allowed_paths.add(key)
|
||||
allowed_paths.add(value)
|
||||
|
||||
_default_class_paths_cache[allowed_object_mode] = allowed_paths
|
||||
return _default_class_paths_cache[allowed_object_mode]
|
||||
|
||||
|
||||
def _block_jinja2_templates(
|
||||
class_path: tuple[str, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Block jinja2 templates during deserialization for security.
|
||||
|
||||
Jinja2 templates can execute arbitrary code, so they are blocked by default when
|
||||
deserializing objects with `template_format='jinja2'`.
|
||||
|
||||
Note:
|
||||
We intentionally do NOT check the `class_path` here to keep this simple and
|
||||
future-proof. If any new class is added that accepts `template_format='jinja2'`,
|
||||
it will be automatically blocked without needing to update this function.
|
||||
|
||||
Args:
|
||||
class_path: The class path tuple being deserialized (unused).
|
||||
kwargs: The kwargs dict for the class constructor.
|
||||
|
||||
Raises:
|
||||
ValueError: If `template_format` is `'jinja2'`.
|
||||
"""
|
||||
_ = class_path # Unused - see docstring for rationale. Kept to satisfy signature.
|
||||
if kwargs.get("template_format") == "jinja2":
|
||||
msg = (
|
||||
"Jinja2 templates are not allowed during deserialization for security "
|
||||
"reasons. Use 'f-string' template format instead, or explicitly allow "
|
||||
"jinja2 by providing a custom init_validator."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def default_init_validator(
|
||||
class_path: tuple[str, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Default init validator that blocks jinja2 templates.
|
||||
|
||||
This is the default validator used by `load()` and `loads()` when no custom
|
||||
validator is provided.
|
||||
|
||||
Args:
|
||||
class_path: The class path tuple being deserialized.
|
||||
kwargs: The kwargs dict for the class constructor.
|
||||
|
||||
Raises:
|
||||
ValueError: If template_format is `'jinja2'`.
|
||||
"""
|
||||
_block_jinja2_templates(class_path, kwargs)
|
||||
|
||||
|
||||
AllowedObject = type[Serializable]
|
||||
"""Type alias for classes that can be included in the `allowed_objects` parameter.
|
||||
|
||||
Must be a `Serializable` subclass (the class itself, not an instance).
|
||||
"""
|
||||
|
||||
InitValidator = Callable[[tuple[str, ...], dict[str, Any]], None]
|
||||
"""Type alias for a callable that validates kwargs during deserialization.
|
||||
|
||||
The callable receives:
|
||||
|
||||
- `class_path`: A tuple of strings identifying the class being instantiated
|
||||
(e.g., `('langchain', 'schema', 'messages', 'AIMessage')`).
|
||||
- `kwargs`: The kwargs dict that will be passed to the constructor.
|
||||
|
||||
The validator should raise an exception if the object should not be deserialized.
|
||||
"""
|
||||
|
||||
|
||||
def _compute_allowed_class_paths(
|
||||
allowed_objects: Iterable[AllowedObject],
|
||||
import_mappings: dict[tuple[str, ...], tuple[str, ...]],
|
||||
) -> set[tuple[str, ...]]:
|
||||
"""Return allowed class paths from an explicit list of classes.
|
||||
|
||||
A class path is a tuple of strings identifying a serializable class, derived from
|
||||
`Serializable.lc_id()`. For example: `('langchain_core', 'messages', 'AIMessage')`.
|
||||
|
||||
Args:
|
||||
allowed_objects: Iterable of `Serializable` subclasses to allow.
|
||||
import_mappings: Mapping of legacy class paths to current class paths.
|
||||
|
||||
Returns:
|
||||
Set of allowed class paths.
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Allow a specific class
|
||||
_compute_allowed_class_paths([MyPrompt], {}) ->
|
||||
{("langchain_core", "prompts", "MyPrompt")}
|
||||
|
||||
# Include legacy paths that map to the same class
|
||||
import_mappings = {("old", "Prompt"): ("langchain_core", "prompts", "MyPrompt")}
|
||||
_compute_allowed_class_paths([MyPrompt], import_mappings) ->
|
||||
{("langchain_core", "prompts", "MyPrompt"), ("old", "Prompt")}
|
||||
```
|
||||
"""
|
||||
allowed_objects_list = list(allowed_objects)
|
||||
|
||||
allowed_class_paths: set[tuple[str, ...]] = set()
|
||||
for allowed_obj in allowed_objects_list:
|
||||
if not isinstance(allowed_obj, type) or not issubclass(
|
||||
allowed_obj, Serializable
|
||||
):
|
||||
msg = "allowed_objects must contain Serializable subclasses."
|
||||
raise TypeError(msg)
|
||||
|
||||
class_path = tuple(allowed_obj.lc_id())
|
||||
allowed_class_paths.add(class_path)
|
||||
# Add legacy paths that map to the same class.
|
||||
for mapping_key, mapping_value in import_mappings.items():
|
||||
if tuple(mapping_value) == class_path:
|
||||
allowed_class_paths.add(mapping_key)
|
||||
return allowed_class_paths
|
||||
|
||||
|
||||
class Reviver:
|
||||
"""Reviver for JSON objects."""
|
||||
"""Reviver for JSON objects.
|
||||
|
||||
Used as the `object_hook` for `json.loads` to reconstruct LangChain objects from
|
||||
their serialized JSON representation.
|
||||
|
||||
Only classes in the allowlist can be instantiated.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: dict[str, str] | None = None,
|
||||
valid_namespaces: list[str] | None = None,
|
||||
secrets_from_env: bool = True, # noqa: FBT001,FBT002
|
||||
secrets_from_env: bool = False, # noqa: FBT001,FBT002
|
||||
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]]
|
||||
| None = None,
|
||||
*,
|
||||
ignore_unserializable_fields: bool = False,
|
||||
init_validator: InitValidator | None = default_init_validator,
|
||||
) -> None:
|
||||
"""Initialize the reviver.
|
||||
|
||||
Args:
|
||||
secrets_map: A map of secrets to load.
|
||||
allowed_objects: Allowlist of classes that can be deserialized.
|
||||
- `'core'` (default): Allow classes defined in the serialization
|
||||
mappings for `langchain_core`.
|
||||
- `'all'`: Allow classes defined in the serialization mappings.
|
||||
|
||||
This includes core LangChain types (messages, prompts, documents,
|
||||
etc.) and trusted partner integrations. See
|
||||
`langchain_core.load.mapping` for the full list.
|
||||
- Explicit list of classes: Only those specific classes are allowed.
|
||||
secrets_map: A map of secrets to load.
|
||||
If a secret is not found in the map, it will be loaded from the
|
||||
environment if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
additional_import_mappings: A dictionary of additional namespace mappings.
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
|
||||
When `allowed_objects` is `None` (using defaults), paths from these
|
||||
mappings are also added to the allowed class paths.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
init_validator: Optional callable to validate kwargs before instantiation.
|
||||
|
||||
If provided, this function is called with `(class_path, kwargs)` where
|
||||
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
||||
The validator should raise an exception if the object should not be
|
||||
deserialized, otherwise return `None`.
|
||||
|
||||
Defaults to `default_init_validator` which blocks jinja2 templates.
|
||||
"""
|
||||
self.secrets_from_env = secrets_from_env
|
||||
self.secrets_map = secrets_map or {}
|
||||
@@ -90,7 +337,26 @@ class Reviver:
|
||||
if self.additional_import_mappings
|
||||
else ALL_SERIALIZABLE_MAPPINGS
|
||||
)
|
||||
# Compute allowed class paths:
|
||||
# - "all" -> use default paths from mappings (+ additional_import_mappings)
|
||||
# - Explicit list -> compute from those classes
|
||||
if allowed_objects in ("all", "core"):
|
||||
self.allowed_class_paths: set[tuple[str, ...]] | None = (
|
||||
_get_default_allowed_class_paths(
|
||||
cast("Literal['all', 'core']", allowed_objects)
|
||||
).copy()
|
||||
)
|
||||
# Add paths from additional_import_mappings to the defaults
|
||||
if self.additional_import_mappings:
|
||||
for key, value in self.additional_import_mappings.items():
|
||||
self.allowed_class_paths.add(key)
|
||||
self.allowed_class_paths.add(value)
|
||||
else:
|
||||
self.allowed_class_paths = _compute_allowed_class_paths(
|
||||
cast("Iterable[AllowedObject]", allowed_objects), self.import_mappings
|
||||
)
|
||||
self.ignore_unserializable_fields = ignore_unserializable_fields
|
||||
self.init_validator = init_validator
|
||||
|
||||
def __call__(self, value: dict[str, Any]) -> Any:
|
||||
"""Revive the value.
|
||||
@@ -141,6 +407,20 @@ class Reviver:
|
||||
[*namespace, name] = value["id"]
|
||||
mapping_key = tuple(value["id"])
|
||||
|
||||
if (
|
||||
self.allowed_class_paths is not None
|
||||
and mapping_key not in self.allowed_class_paths
|
||||
):
|
||||
msg = (
|
||||
f"Deserialization of {mapping_key!r} is not allowed. "
|
||||
"The default (allowed_objects='core') only permits core "
|
||||
"langchain-core classes. To allow trusted partner integrations, "
|
||||
"use allowed_objects='all'. Alternatively, pass an explicit list "
|
||||
"of allowed classes via allowed_objects=[...]. "
|
||||
"See langchain_core.load.mapping for the full allowlist."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
if (
|
||||
namespace[0] not in self.valid_namespaces
|
||||
# The root namespace ["langchain"] is not a valid identifier.
|
||||
@@ -148,13 +428,11 @@ class Reviver:
|
||||
):
|
||||
msg = f"Invalid namespace: {value}"
|
||||
raise ValueError(msg)
|
||||
# Has explicit import path.
|
||||
# Determine explicit import path
|
||||
if mapping_key in self.import_mappings:
|
||||
import_path = self.import_mappings[mapping_key]
|
||||
# Split into module and name
|
||||
import_dir, name = import_path[:-1], import_path[-1]
|
||||
# Import module
|
||||
mod = importlib.import_module(".".join(import_dir))
|
||||
elif namespace[0] in DISALLOW_LOAD_FROM_PATH:
|
||||
msg = (
|
||||
"Trying to deserialize something that cannot "
|
||||
@@ -162,9 +440,16 @@ class Reviver:
|
||||
f"{mapping_key}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
# Otherwise, treat namespace as path.
|
||||
else:
|
||||
mod = importlib.import_module(".".join(namespace))
|
||||
# Otherwise, treat namespace as path.
|
||||
import_dir = namespace
|
||||
|
||||
# Validate import path is in trusted namespaces before importing
|
||||
if import_dir[0] not in self.valid_namespaces:
|
||||
msg = f"Invalid namespace: {value}"
|
||||
raise ValueError(msg)
|
||||
|
||||
mod = importlib.import_module(".".join(import_dir))
|
||||
|
||||
cls = getattr(mod, name)
|
||||
|
||||
@@ -176,6 +461,10 @@ class Reviver:
|
||||
# We don't need to recurse on kwargs
|
||||
# as json.loads will do that for us.
|
||||
kwargs = value.get("kwargs", {})
|
||||
|
||||
if self.init_validator is not None:
|
||||
self.init_validator(mapping_key, kwargs)
|
||||
|
||||
return cls(**kwargs)
|
||||
|
||||
return value
|
||||
@@ -185,42 +474,81 @@ class Reviver:
|
||||
def loads(
|
||||
text: str,
|
||||
*,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: dict[str, str] | None = None,
|
||||
valid_namespaces: list[str] | None = None,
|
||||
secrets_from_env: bool = True,
|
||||
secrets_from_env: bool = False,
|
||||
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]] | None = None,
|
||||
ignore_unserializable_fields: bool = False,
|
||||
init_validator: InitValidator | None = default_init_validator,
|
||||
) -> Any:
|
||||
"""Revive a LangChain class from a JSON string.
|
||||
|
||||
Equivalent to `load(json.loads(text))`.
|
||||
|
||||
Only classes in the allowlist can be instantiated. The default allowlist includes
|
||||
core LangChain types (messages, prompts, documents, etc.). See
|
||||
`langchain_core.load.mapping` for the full list.
|
||||
|
||||
!!! warning "Beta feature"
|
||||
|
||||
This is a beta feature. Please be wary of deploying experimental code to
|
||||
production unless you've taken appropriate precautions.
|
||||
|
||||
Args:
|
||||
text: The string to load.
|
||||
allowed_objects: Allowlist of classes that can be deserialized.
|
||||
|
||||
- `'core'` (default): Allow classes defined in the serialization mappings
|
||||
for `langchain_core`.
|
||||
- `'all'`: Allow classes defined in the serialization mappings.
|
||||
|
||||
This includes core LangChain types (messages, prompts, documents, etc.)
|
||||
and trusted partner integrations. See `langchain_core.load.mapping` for
|
||||
the full list.
|
||||
|
||||
- Explicit list of classes: Only those specific classes are allowed.
|
||||
- `[]`: Disallow all deserialization (will raise on any object).
|
||||
secrets_map: A map of secrets to load.
|
||||
|
||||
If a secret is not found in the map, it will be loaded from the environment
|
||||
if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
additional_import_mappings: A dictionary of additional namespace mappings.
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
|
||||
When `allowed_objects` is `None` (using defaults), paths from these
|
||||
mappings are also added to the allowed class paths.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
init_validator: Optional callable to validate kwargs before instantiation.
|
||||
|
||||
If provided, this function is called with `(class_path, kwargs)` where
|
||||
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
||||
The validator should raise an exception if the object should not be
|
||||
deserialized, otherwise return `None`.
|
||||
|
||||
Defaults to `default_init_validator` which blocks jinja2 templates.
|
||||
|
||||
Returns:
|
||||
Revived LangChain objects.
|
||||
|
||||
Raises:
|
||||
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
|
||||
"""
|
||||
return json.loads(
|
||||
text,
|
||||
object_hook=Reviver(
|
||||
secrets_map,
|
||||
valid_namespaces,
|
||||
secrets_from_env,
|
||||
additional_import_mappings,
|
||||
ignore_unserializable_fields=ignore_unserializable_fields,
|
||||
),
|
||||
# Parse JSON and delegate to load() for proper escape handling
|
||||
raw_obj = json.loads(text)
|
||||
return load(
|
||||
raw_obj,
|
||||
allowed_objects=allowed_objects,
|
||||
secrets_map=secrets_map,
|
||||
valid_namespaces=valid_namespaces,
|
||||
secrets_from_env=secrets_from_env,
|
||||
additional_import_mappings=additional_import_mappings,
|
||||
ignore_unserializable_fields=ignore_unserializable_fields,
|
||||
init_validator=init_validator,
|
||||
)
|
||||
|
||||
|
||||
@@ -228,45 +556,112 @@ def loads(
|
||||
def load(
|
||||
obj: Any,
|
||||
*,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: dict[str, str] | None = None,
|
||||
valid_namespaces: list[str] | None = None,
|
||||
secrets_from_env: bool = True,
|
||||
secrets_from_env: bool = False,
|
||||
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]] | None = None,
|
||||
ignore_unserializable_fields: bool = False,
|
||||
init_validator: InitValidator | None = default_init_validator,
|
||||
) -> Any:
|
||||
"""Revive a LangChain class from a JSON object.
|
||||
|
||||
Use this if you already have a parsed JSON object,
|
||||
eg. from `json.load` or `orjson.loads`.
|
||||
Use this if you already have a parsed JSON object, eg. from `json.load` or
|
||||
`orjson.loads`.
|
||||
|
||||
Only classes in the allowlist can be instantiated. The default allowlist includes
|
||||
core LangChain types (messages, prompts, documents, etc.). See
|
||||
`langchain_core.load.mapping` for the full list.
|
||||
|
||||
!!! warning "Beta feature"
|
||||
|
||||
This is a beta feature. Please be wary of deploying experimental code to
|
||||
production unless you've taken appropriate precautions.
|
||||
|
||||
Args:
|
||||
obj: The object to load.
|
||||
allowed_objects: Allowlist of classes that can be deserialized.
|
||||
|
||||
- `'core'` (default): Allow classes defined in the serialization mappings
|
||||
for `langchain_core`.
|
||||
- `'all'`: Allow classes defined in the serialization mappings.
|
||||
|
||||
This includes core LangChain types (messages, prompts, documents, etc.)
|
||||
and trusted partner integrations. See `langchain_core.load.mapping` for
|
||||
the full list.
|
||||
|
||||
- Explicit list of classes: Only those specific classes are allowed.
|
||||
- `[]`: Disallow all deserialization (will raise on any object).
|
||||
secrets_map: A map of secrets to load.
|
||||
|
||||
If a secret is not found in the map, it will be loaded from the environment
|
||||
if `secrets_from_env` is `True`.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings
|
||||
additional_import_mappings: A dictionary of additional namespace mappings.
|
||||
|
||||
You can use this to override default mappings or add new mappings.
|
||||
|
||||
When `allowed_objects` is `None` (using defaults), paths from these
|
||||
mappings are also added to the allowed class paths.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
init_validator: Optional callable to validate kwargs before instantiation.
|
||||
|
||||
If provided, this function is called with `(class_path, kwargs)` where
|
||||
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
|
||||
The validator should raise an exception if the object should not be
|
||||
deserialized, otherwise return `None`.
|
||||
|
||||
Defaults to `default_init_validator` which blocks jinja2 templates.
|
||||
|
||||
Returns:
|
||||
Revived LangChain objects.
|
||||
|
||||
Raises:
|
||||
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from langchain_core.load import load, dumpd
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
msg = AIMessage(content="Hello")
|
||||
data = dumpd(msg)
|
||||
|
||||
# Deserialize using default allowlist
|
||||
loaded = load(data)
|
||||
|
||||
# Or with explicit allowlist
|
||||
loaded = load(data, allowed_objects=[AIMessage])
|
||||
|
||||
# Or extend defaults with additional mappings
|
||||
loaded = load(
|
||||
data,
|
||||
additional_import_mappings={
|
||||
("my_pkg", "MyClass"): ("my_pkg", "module", "MyClass"),
|
||||
},
|
||||
)
|
||||
```
|
||||
"""
|
||||
reviver = Reviver(
|
||||
allowed_objects,
|
||||
secrets_map,
|
||||
valid_namespaces,
|
||||
secrets_from_env,
|
||||
additional_import_mappings,
|
||||
ignore_unserializable_fields=ignore_unserializable_fields,
|
||||
init_validator=init_validator,
|
||||
)
|
||||
|
||||
def _load(obj: Any) -> Any:
|
||||
if isinstance(obj, dict):
|
||||
# Need to revive leaf nodes before reviving this node
|
||||
# Check for escaped dict FIRST (before recursing).
|
||||
# Escaped dicts are user data that should NOT be processed as LC objects.
|
||||
if _is_escaped_dict(obj):
|
||||
return _unescape_value(obj)
|
||||
|
||||
# Not escaped - recurse into children then apply reviver
|
||||
loaded_obj = {k: _load(v) for k, v in obj.items()}
|
||||
return reviver(loaded_obj)
|
||||
if isinstance(obj, list):
|
||||
|
||||
@@ -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
|
||||
@@ -51,22 +52,22 @@ class InputTokenDetails(TypedDict, total=False):
|
||||
May also hold extra provider-specific keys.
|
||||
|
||||
!!! version-added "Added in `langchain-core` 0.3.9"
|
||||
|
||||
"""
|
||||
|
||||
audio: int
|
||||
"""Audio input tokens."""
|
||||
|
||||
cache_creation: int
|
||||
"""Input tokens that were cached and there was a cache miss.
|
||||
|
||||
Since there was a cache miss, the cache was created from these tokens.
|
||||
"""
|
||||
|
||||
cache_read: int
|
||||
"""Input tokens that were cached and there was a cache hit.
|
||||
|
||||
Since there was a cache hit, the tokens were read from the cache. More precisely,
|
||||
the model state given these tokens was read from the cache.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -91,12 +92,12 @@ class OutputTokenDetails(TypedDict, total=False):
|
||||
|
||||
audio: int
|
||||
"""Audio output tokens."""
|
||||
|
||||
reasoning: int
|
||||
"""Reasoning output tokens.
|
||||
|
||||
Tokens generated by the model in a chain of thought process (i.e. by OpenAI's o1
|
||||
models) that are not returned as part of model output.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -136,15 +137,19 @@ class UsageMetadata(TypedDict):
|
||||
|
||||
input_tokens: int
|
||||
"""Count of input (or prompt) tokens. Sum of all input token types."""
|
||||
|
||||
output_tokens: int
|
||||
"""Count of output (or completion) tokens. Sum of all output token types."""
|
||||
|
||||
total_tokens: int
|
||||
"""Total token count. Sum of `input_tokens` + `output_tokens`."""
|
||||
|
||||
input_token_details: NotRequired[InputTokenDetails]
|
||||
"""Breakdown of input token counts.
|
||||
|
||||
Does *not* need to sum to full input token count. Does *not* need to have all keys.
|
||||
"""
|
||||
|
||||
output_token_details: NotRequired[OutputTokenDetails]
|
||||
"""Breakdown of output token counts.
|
||||
|
||||
@@ -162,10 +167,12 @@ class AIMessage(BaseMessage):
|
||||
(e.g., tool calls, usage metadata) added by the LangChain framework.
|
||||
"""
|
||||
|
||||
tool_calls: list[ToolCall] = []
|
||||
tool_calls: list[ToolCall] = Field(default_factory=list)
|
||||
"""If present, tool calls associated with the message."""
|
||||
invalid_tool_calls: list[InvalidToolCall] = []
|
||||
|
||||
invalid_tool_calls: list[InvalidToolCall] = Field(default_factory=list)
|
||||
"""If present, tool calls with parsing errors associated with the message."""
|
||||
|
||||
usage_metadata: UsageMetadata | None = None
|
||||
"""If present, usage metadata for a message, such as token counts.
|
||||
|
||||
@@ -320,7 +327,7 @@ class AIMessage(BaseMessage):
|
||||
if tool_calls := values.get("tool_calls"):
|
||||
values["tool_calls"] = [
|
||||
create_tool_call(
|
||||
**{k: v for k, v in tc.items() if k not in ("type", "extras")}
|
||||
**{k: v for k, v in tc.items() if k not in {"type", "extras"}}
|
||||
)
|
||||
for tc in tool_calls
|
||||
]
|
||||
@@ -388,7 +395,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
type: Literal["AIMessageChunk"] = "AIMessageChunk" # type: ignore[assignment]
|
||||
"""The type of the message (used for deserialization)."""
|
||||
|
||||
tool_call_chunks: list[ToolCallChunk] = []
|
||||
tool_call_chunks: list[ToolCallChunk] = Field(default_factory=list)
|
||||
"""If provided, tool call chunks associated with the message."""
|
||||
|
||||
chunk_position: Literal["last"] | None = None
|
||||
@@ -399,8 +406,8 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
"""
|
||||
|
||||
@property
|
||||
@override
|
||||
def lc_attributes(self) -> dict:
|
||||
"""Attributes to be serialized, even if they are derived from other initialization args.""" # noqa: E501
|
||||
return {
|
||||
"tool_calls": self.tool_calls,
|
||||
"invalid_tool_calls": self.invalid_tool_calls,
|
||||
@@ -436,7 +443,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
blocks = [
|
||||
block
|
||||
for block in blocks
|
||||
if block["type"] not in ("tool_call", "invalid_tool_call")
|
||||
if block["type"] not in {"tool_call", "invalid_tool_call"}
|
||||
]
|
||||
for tool_call_chunk in self.tool_call_chunks:
|
||||
tc: types.ToolCallChunk = {
|
||||
@@ -557,7 +564,11 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def init_server_tool_calls(self) -> Self:
|
||||
"""Parse `server_tool_call_chunks`."""
|
||||
"""Initialize server tool calls.
|
||||
|
||||
Parse `server_tool_call_chunks` from
|
||||
[`ServerToolCallChunk`][langchain.messages.ServerToolCallChunk] objects.
|
||||
"""
|
||||
if (
|
||||
self.chunk_position == "last"
|
||||
and self.response_metadata.get("output_version") == "v1"
|
||||
@@ -567,7 +578,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and block.get("type")
|
||||
in ("server_tool_call", "server_tool_call_chunk")
|
||||
in {"server_tool_call", "server_tool_call_chunk"}
|
||||
and (args_str := block.get("args"))
|
||||
and isinstance(args_str, str)
|
||||
):
|
||||
@@ -645,29 +656,28 @@ def add_ai_message_chunks(
|
||||
else:
|
||||
usage_metadata = None
|
||||
|
||||
# Ranks are defined by the order of preference. Higher is better:
|
||||
# 2. Provider-assigned IDs (non lc_* and non lc_run-*)
|
||||
# 1. lc_run-* IDs
|
||||
# 0. lc_* and other remaining IDs
|
||||
best_rank = -1
|
||||
chunk_id = None
|
||||
candidates = [left.id] + [o.id for o in others]
|
||||
# first pass: pick the first provider-assigned id (non-run-* and non-lc_*)
|
||||
candidates = itertools.chain([left.id], (o.id for o in others))
|
||||
|
||||
for id_ in candidates:
|
||||
if (
|
||||
id_
|
||||
and not id_.startswith(LC_ID_PREFIX)
|
||||
and not id_.startswith(LC_AUTO_PREFIX)
|
||||
):
|
||||
if not id_:
|
||||
continue
|
||||
|
||||
if not id_.startswith(LC_ID_PREFIX) and not id_.startswith(LC_AUTO_PREFIX):
|
||||
chunk_id = id_
|
||||
# Highest rank, return instantly
|
||||
break
|
||||
else:
|
||||
# second pass: prefer lc_run-* IDs over lc_* IDs
|
||||
for id_ in candidates:
|
||||
if id_ and id_.startswith(LC_ID_PREFIX):
|
||||
chunk_id = id_
|
||||
break
|
||||
else:
|
||||
# third pass: take any remaining ID (auto-generated lc_* IDs)
|
||||
for id_ in candidates:
|
||||
if id_:
|
||||
chunk_id = id_
|
||||
break
|
||||
|
||||
rank = 1 if id_.startswith(LC_ID_PREFIX) else 0
|
||||
|
||||
if rank > best_rank:
|
||||
best_rank = rank
|
||||
chunk_id = id_
|
||||
|
||||
chunk_position: Literal["last"] | None = (
|
||||
"last" if any(x.chunk_position == "last" for x in [left, *others]) else None
|
||||
|
||||
@@ -8,6 +8,7 @@ from pydantic import ConfigDict, Field
|
||||
|
||||
from langchain_core._api.deprecation import warn_deprecated
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.utils import get_bolded_text
|
||||
from langchain_core.utils._merge import merge_dicts, merge_lists
|
||||
from langchain_core.utils.interactive_env import is_interactive_env
|
||||
@@ -17,7 +18,6 @@ if TYPE_CHECKING:
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate
|
||||
|
||||
|
||||
@@ -204,7 +204,6 @@ class BaseMessage(Serializable):
|
||||
|
||||
"""
|
||||
# Needed here to avoid circular import, as these classes import BaseMessages
|
||||
from langchain_core.messages import content as types # noqa: PLC0415
|
||||
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
|
||||
_convert_to_v1_from_anthropic_input,
|
||||
)
|
||||
@@ -266,6 +265,9 @@ class BaseMessage(Serializable):
|
||||
|
||||
Can be used as both property (`message.text`) and method (`message.text()`).
|
||||
|
||||
Handles both string and list content types (e.g. for content blocks). Only
|
||||
extracts blocks with `type: 'text'`; other block types are ignored.
|
||||
|
||||
!!! deprecated
|
||||
As of `langchain-core` 1.0.0, calling `.text()` as a method is deprecated.
|
||||
Use `.text` as a property instead. This method will be removed in 2.0.0.
|
||||
@@ -277,7 +279,7 @@ class BaseMessage(Serializable):
|
||||
if isinstance(self.content, str):
|
||||
text_value = self.content
|
||||
else:
|
||||
# must be a list
|
||||
# Must be a list
|
||||
blocks = [
|
||||
block
|
||||
for block in self.content
|
||||
@@ -302,7 +304,7 @@ class BaseMessage(Serializable):
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate # noqa: PLC0415
|
||||
|
||||
prompt = ChatPromptTemplate(messages=[self])
|
||||
return prompt + other
|
||||
return prompt.__add__(other)
|
||||
|
||||
def pretty_repr(
|
||||
self,
|
||||
@@ -391,12 +393,12 @@ class BaseMessageChunk(BaseMessage):
|
||||
Raises:
|
||||
TypeError: If the other object is not a message chunk.
|
||||
|
||||
For example,
|
||||
|
||||
`AIMessageChunk(content="Hello") + AIMessageChunk(content=" World")`
|
||||
|
||||
will give `AIMessageChunk(content="Hello World")`
|
||||
|
||||
Example:
|
||||
```txt
|
||||
AIMessageChunk(content="Hello", ...)
|
||||
+ AIMessageChunk(content=" World", ...)
|
||||
= AIMessageChunk(content="Hello World", ...)
|
||||
```
|
||||
"""
|
||||
if isinstance(other, BaseMessageChunk):
|
||||
# If both are (subclasses of) BaseMessageChunk,
|
||||
|
||||
@@ -159,12 +159,12 @@ def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
|
||||
|
||||
return url_citation
|
||||
|
||||
if citation_type in (
|
||||
if citation_type in {
|
||||
"char_location",
|
||||
"content_block_location",
|
||||
"page_location",
|
||||
"search_result_location",
|
||||
):
|
||||
}:
|
||||
document_citation: types.Citation = {
|
||||
"type": "citation",
|
||||
"cited_text": citation["cited_text"],
|
||||
@@ -173,8 +173,6 @@ def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
|
||||
document_citation["title"] = citation["document_title"]
|
||||
elif title := citation.get("title"):
|
||||
document_citation["title"] = title
|
||||
else:
|
||||
pass
|
||||
known_fields = {
|
||||
"type",
|
||||
"cited_text",
|
||||
@@ -245,11 +243,20 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
|
||||
and message.chunk_position != "last"
|
||||
):
|
||||
# Isolated chunk
|
||||
tool_call_chunk: types.ToolCallChunk = (
|
||||
message.tool_call_chunks[0].copy() # type: ignore[assignment]
|
||||
chunk = message.tool_call_chunks[0]
|
||||
|
||||
tool_call_chunk = types.ToolCallChunk(
|
||||
name=chunk.get("name"),
|
||||
id=chunk.get("id"),
|
||||
args=chunk.get("args"),
|
||||
type="tool_call_chunk",
|
||||
)
|
||||
if "type" not in tool_call_chunk:
|
||||
tool_call_chunk["type"] = "tool_call_chunk"
|
||||
if "caller" in block:
|
||||
tool_call_chunk["extras"] = {"caller": block["caller"]}
|
||||
|
||||
index = chunk.get("index")
|
||||
if index is not None:
|
||||
tool_call_chunk["index"] = index
|
||||
yield tool_call_chunk
|
||||
else:
|
||||
tool_call_block: types.ToolCall | None = None
|
||||
@@ -271,8 +278,6 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
|
||||
"id": tc.get("id"),
|
||||
}
|
||||
break
|
||||
else:
|
||||
pass
|
||||
if not tool_call_block:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
@@ -282,17 +287,27 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
|
||||
}
|
||||
if "index" in block:
|
||||
tool_call_block["index"] = block["index"]
|
||||
if "caller" in block:
|
||||
if "extras" not in tool_call_block:
|
||||
tool_call_block["extras"] = {}
|
||||
tool_call_block["extras"]["caller"] = block["caller"]
|
||||
|
||||
yield tool_call_block
|
||||
|
||||
elif block_type == "input_json_delta" and isinstance(
|
||||
message, AIMessageChunk
|
||||
):
|
||||
if len(message.tool_call_chunks) == 1:
|
||||
tool_call_chunk = (
|
||||
message.tool_call_chunks[0].copy() # type: ignore[assignment]
|
||||
chunk = message.tool_call_chunks[0]
|
||||
tool_call_chunk = types.ToolCallChunk(
|
||||
name=chunk.get("name"),
|
||||
id=chunk.get("id"),
|
||||
args=chunk.get("args"),
|
||||
type="tool_call_chunk",
|
||||
)
|
||||
if "type" not in tool_call_chunk:
|
||||
tool_call_chunk["type"] = "tool_call_chunk"
|
||||
index = chunk.get("index")
|
||||
if index is not None:
|
||||
tool_call_chunk["index"] = index
|
||||
yield tool_call_chunk
|
||||
|
||||
else:
|
||||
@@ -446,12 +461,26 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Anthropic content."""
|
||||
"""Derive standard content blocks from a message with Anthropic content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_anthropic(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message chunk with Anthropic content."""
|
||||
"""Derive standard content blocks from a message chunk with Anthropic content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_anthropic(message)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -209,11 +209,16 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
|
||||
and message.chunk_position != "last"
|
||||
):
|
||||
# Isolated chunk
|
||||
tool_call_chunk: types.ToolCallChunk = (
|
||||
message.tool_call_chunks[0].copy() # type: ignore[assignment]
|
||||
chunk = message.tool_call_chunks[0]
|
||||
tool_call_chunk = types.ToolCallChunk(
|
||||
name=chunk.get("name"),
|
||||
id=chunk.get("id"),
|
||||
args=chunk.get("args"),
|
||||
type="tool_call_chunk",
|
||||
)
|
||||
if "type" not in tool_call_chunk:
|
||||
tool_call_chunk["type"] = "tool_call_chunk"
|
||||
index = chunk.get("index")
|
||||
if index is not None:
|
||||
tool_call_chunk["index"] = index
|
||||
yield tool_call_chunk
|
||||
else:
|
||||
tool_call_block: types.ToolCall | None = None
|
||||
@@ -235,8 +240,6 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
|
||||
"id": tc.get("id"),
|
||||
}
|
||||
break
|
||||
else:
|
||||
pass
|
||||
if not tool_call_block:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
@@ -253,11 +256,16 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
|
||||
and isinstance(message, AIMessageChunk)
|
||||
and len(message.tool_call_chunks) == 1
|
||||
):
|
||||
tool_call_chunk = (
|
||||
message.tool_call_chunks[0].copy() # type: ignore[assignment]
|
||||
chunk = message.tool_call_chunks[0]
|
||||
tool_call_chunk = types.ToolCallChunk(
|
||||
name=chunk.get("name"),
|
||||
id=chunk.get("id"),
|
||||
args=chunk.get("args"),
|
||||
type="tool_call_chunk",
|
||||
)
|
||||
if "type" not in tool_call_chunk:
|
||||
tool_call_chunk["type"] = "tool_call_chunk"
|
||||
index = chunk.get("index")
|
||||
if index is not None:
|
||||
tool_call_chunk["index"] = index
|
||||
yield tool_call_chunk
|
||||
|
||||
else:
|
||||
@@ -273,12 +281,26 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Bedrock Converse content."""
|
||||
"""Derive standard content blocks from a message with Bedrock Converse content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_converse(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a chunk with Bedrock Converse content."""
|
||||
"""Derive standard content blocks from a chunk with Bedrock Converse content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_converse(message)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.content import Citation, create_citation
|
||||
|
||||
try:
|
||||
import filetype # type: ignore[import-not-found]
|
||||
|
||||
_HAS_FILETYPE = True
|
||||
except ImportError:
|
||||
_HAS_FILETYPE = False
|
||||
|
||||
|
||||
def _bytes_to_b64_str(bytes_: bytes) -> str:
|
||||
"""Convert bytes to base64 encoded string."""
|
||||
@@ -76,21 +83,36 @@ def translate_grounding_metadata_to_citations(
|
||||
for chunk_index in chunk_indices:
|
||||
if chunk_index < len(grounding_chunks):
|
||||
chunk = grounding_chunks[chunk_index]
|
||||
web_info = chunk.get("web", {})
|
||||
|
||||
# Handle web and maps grounding
|
||||
web_info = chunk.get("web") or {}
|
||||
maps_info = chunk.get("maps") or {}
|
||||
|
||||
# Extract citation info depending on source
|
||||
url = maps_info.get("uri") or web_info.get("uri")
|
||||
title = maps_info.get("title") or web_info.get("title")
|
||||
|
||||
# Note: confidence_scores is a legacy field from Gemini 2.0 and earlier
|
||||
# that indicated confidence (0.0-1.0) for each grounding chunk.
|
||||
#
|
||||
# In Gemini 2.5+, this field is always None/empty and should be ignored.
|
||||
extras_metadata = {
|
||||
"web_search_queries": web_search_queries,
|
||||
"grounding_chunk_index": chunk_index,
|
||||
"confidence_scores": support.get("confidence_scores") or [],
|
||||
}
|
||||
|
||||
# Add maps-specific metadata if present
|
||||
if maps_info.get("placeId"):
|
||||
extras_metadata["place_id"] = maps_info["placeId"]
|
||||
|
||||
citation = create_citation(
|
||||
url=web_info.get("uri"),
|
||||
title=web_info.get("title"),
|
||||
url=url,
|
||||
title=title,
|
||||
start_index=start_index,
|
||||
end_index=end_index,
|
||||
cited_text=cited_text,
|
||||
extras={
|
||||
"google_ai_metadata": {
|
||||
"web_search_queries": web_search_queries,
|
||||
"grounding_chunk_index": chunk_index,
|
||||
"confidence_scores": support.get("confidence_scores", []),
|
||||
}
|
||||
},
|
||||
google_ai_metadata=extras_metadata,
|
||||
)
|
||||
citations.append(citation)
|
||||
|
||||
@@ -376,9 +398,7 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"base64": url,
|
||||
}
|
||||
|
||||
try:
|
||||
import filetype # type: ignore[import-not-found] # noqa: PLC0415
|
||||
|
||||
if _HAS_FILETYPE:
|
||||
# Guess MIME type based on file bytes
|
||||
mime_type = None
|
||||
kind = filetype.guess(decoded_bytes)
|
||||
@@ -386,9 +406,6 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
mime_type = kind.mime
|
||||
if mime_type:
|
||||
image_url_b64_block["mime_type"] = mime_type
|
||||
except ImportError:
|
||||
# filetype library not available, skip type detection
|
||||
pass
|
||||
|
||||
converted_blocks.append(
|
||||
cast("types.ImageContentBlock", image_url_b64_block)
|
||||
@@ -396,7 +413,10 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
except Exception:
|
||||
# Not valid base64, treat as non-standard
|
||||
converted_blocks.append(
|
||||
{"type": "non_standard", "value": item}
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": item,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# This likely won't be reached according to previous implementations
|
||||
@@ -508,12 +528,26 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Google (GenAI) content."""
|
||||
"""Derive standard content blocks from a message with Google (GenAI) content.
|
||||
|
||||
Args:
|
||||
message: The message to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_genai(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a chunk with Google (GenAI) content."""
|
||||
"""Derive standard content blocks from a chunk with Google (GenAI) content.
|
||||
|
||||
Args:
|
||||
message: The message chunk to translate.
|
||||
|
||||
Returns:
|
||||
The derived content blocks.
|
||||
"""
|
||||
return _convert_to_v1_from_genai(message)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -29,38 +29,39 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
|
||||
`ToolMessage` objects contain the result of a tool invocation. Typically, the result
|
||||
is encoded inside the `content` field.
|
||||
|
||||
Example: A `ToolMessage` representing a result of `42` from a tool call with id
|
||||
`tool_call_id` is used to associate the tool call request with the tool call
|
||||
response. Useful in situations where a chat model is able to request multiple tool
|
||||
calls in parallel.
|
||||
|
||||
```python
|
||||
from langchain_core.messages import ToolMessage
|
||||
Example:
|
||||
A `ToolMessage` representing a result of `42` from a tool call with id
|
||||
|
||||
ToolMessage(content="42", tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL")
|
||||
```
|
||||
```python
|
||||
from langchain_core.messages import ToolMessage
|
||||
|
||||
Example: A `ToolMessage` where only part of the tool output is sent to the model
|
||||
and the full output is passed in to artifact.
|
||||
ToolMessage(content="42", tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL")
|
||||
```
|
||||
|
||||
```python
|
||||
from langchain_core.messages import ToolMessage
|
||||
Example:
|
||||
A `ToolMessage` where only part of the tool output is sent to the model
|
||||
and the full output is passed in to artifact.
|
||||
|
||||
tool_output = {
|
||||
"stdout": "From the graph we can see that the correlation between "
|
||||
"x and y is ...",
|
||||
"stderr": None,
|
||||
"artifacts": {"type": "image", "base64_data": "/9j/4gIcSU..."},
|
||||
}
|
||||
```python
|
||||
from langchain_core.messages import ToolMessage
|
||||
|
||||
ToolMessage(
|
||||
content=tool_output["stdout"],
|
||||
artifact=tool_output,
|
||||
tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL",
|
||||
)
|
||||
```
|
||||
|
||||
The `tool_call_id` field is used to associate the tool call request with the
|
||||
tool call response. Useful in situations where a chat model is able
|
||||
to request multiple tool calls in parallel.
|
||||
tool_output = {
|
||||
"stdout": "From the graph we can see that the correlation between "
|
||||
"x and y is ...",
|
||||
"stderr": None,
|
||||
"artifacts": {"type": "image", "base64_data": "/9j/4gIcSU..."},
|
||||
}
|
||||
|
||||
ToolMessage(
|
||||
content=tool_output["stdout"],
|
||||
artifact=tool_output,
|
||||
tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL",
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
tool_call_id: str
|
||||
@@ -213,20 +214,29 @@ class ToolCall(TypedDict):
|
||||
This represents a request to call the tool named `'foo'` with arguments
|
||||
`{"a": 1}` and an identifier of `'123'`.
|
||||
|
||||
!!! note "Factory function"
|
||||
|
||||
`tool_call` may also be used as a factory to create a `ToolCall`. Benefits
|
||||
include:
|
||||
|
||||
* Required arguments strictly validated at creation time
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""The name of the tool to be called."""
|
||||
|
||||
args: dict[str, Any]
|
||||
"""The arguments to the tool call."""
|
||||
"""The arguments to the tool call as a dictionary."""
|
||||
|
||||
id: str | None
|
||||
"""An identifier associated with the tool call.
|
||||
|
||||
An identifier is needed to associate a tool call request with a tool
|
||||
call result in events when multiple concurrent tool calls are made.
|
||||
|
||||
"""
|
||||
|
||||
type: NotRequired[Literal["tool_call"]]
|
||||
"""Used for discrimination."""
|
||||
|
||||
|
||||
def tool_call(
|
||||
@@ -239,7 +249,7 @@ def tool_call(
|
||||
|
||||
Args:
|
||||
name: The name of the tool to be called.
|
||||
args: The arguments to the tool call.
|
||||
args: The arguments to the tool call as a dictionary.
|
||||
id: An identifier associated with the tool call.
|
||||
|
||||
Returns:
|
||||
@@ -251,9 +261,9 @@ def tool_call(
|
||||
class ToolCallChunk(TypedDict):
|
||||
"""A chunk of a tool call (yielded when streaming).
|
||||
|
||||
When merging `ToolCallChunk`s (e.g., via `AIMessageChunk.__add__`),
|
||||
all string attributes are concatenated. Chunks are only merged if their
|
||||
values of `index` are equal and not None.
|
||||
When merging `ToolCallChunk` objects (e.g., via `AIMessageChunk.__add__`), all
|
||||
string attributes are concatenated. Chunks are only merged if their values of
|
||||
`index` are equal and not `None`.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -269,13 +279,25 @@ class ToolCallChunk(TypedDict):
|
||||
|
||||
name: str | None
|
||||
"""The name of the tool to be called."""
|
||||
|
||||
args: str | None
|
||||
"""The arguments to the tool call."""
|
||||
"""The arguments to the tool call as a JSON-parseable string."""
|
||||
|
||||
id: str | None
|
||||
"""An identifier associated with the tool call."""
|
||||
"""An identifier associated with the tool call.
|
||||
|
||||
An identifier is needed to associate a tool call request with a tool
|
||||
call result in events when multiple concurrent tool calls are made.
|
||||
"""
|
||||
|
||||
index: int | None
|
||||
"""The index of the tool call in a sequence."""
|
||||
"""The index of the tool call in a sequence.
|
||||
|
||||
Used for merging chunks.
|
||||
"""
|
||||
|
||||
type: NotRequired[Literal["tool_call_chunk"]]
|
||||
"""Used for discrimination."""
|
||||
|
||||
|
||||
def tool_call_chunk(
|
||||
@@ -289,7 +311,7 @@ def tool_call_chunk(
|
||||
|
||||
Args:
|
||||
name: The name of the tool to be called.
|
||||
args: The arguments to the tool call.
|
||||
args: The arguments to the tool call as a JSON string.
|
||||
id: An identifier associated with the tool call.
|
||||
index: The index of the tool call in a sequence.
|
||||
|
||||
@@ -312,7 +334,7 @@ def invalid_tool_call(
|
||||
|
||||
Args:
|
||||
name: The name of the tool to be called.
|
||||
args: The arguments to the tool call.
|
||||
args: The arguments to the tool call as a JSON string.
|
||||
id: An identifier associated with the tool call.
|
||||
error: An error message associated with the tool call.
|
||||
|
||||
|
||||
@@ -15,15 +15,20 @@ import json
|
||||
import logging
|
||||
import math
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from functools import partial
|
||||
from functools import partial, wraps
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Concatenate,
|
||||
Literal,
|
||||
ParamSpec,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
from xml.sax.saxutils import escape, quoteattr
|
||||
|
||||
from pydantic import Discriminator, Field, Tag
|
||||
|
||||
@@ -61,14 +66,19 @@ logger = logging.getLogger(__name__)
|
||||
def _get_type(v: Any) -> str:
|
||||
"""Get the type associated with the object for serialization purposes."""
|
||||
if isinstance(v, dict) and "type" in v:
|
||||
return v["type"]
|
||||
if hasattr(v, "type"):
|
||||
return v.type
|
||||
msg = (
|
||||
f"Expected either a dictionary with a 'type' key or an object "
|
||||
f"with a 'type' attribute. Instead got type {type(v)}."
|
||||
)
|
||||
raise TypeError(msg)
|
||||
result = v["type"]
|
||||
elif hasattr(v, "type"):
|
||||
result = v.type
|
||||
else:
|
||||
msg = (
|
||||
f"Expected either a dictionary with a 'type' key or an object "
|
||||
f"with a 'type' attribute. Instead got type {type(v)}."
|
||||
)
|
||||
raise TypeError(msg)
|
||||
if not isinstance(result, str):
|
||||
msg = f"Expected 'type' to be a str, got {type(result).__name__}"
|
||||
raise TypeError(msg)
|
||||
return result
|
||||
|
||||
|
||||
AnyMessage = Annotated[
|
||||
@@ -89,8 +99,199 @@ AnyMessage = Annotated[
|
||||
"""A type representing any defined `Message` or `MessageChunk` type."""
|
||||
|
||||
|
||||
def _has_base64_data(block: dict) -> bool:
|
||||
"""Check if a content block contains base64 encoded data.
|
||||
|
||||
Args:
|
||||
block: A content block dictionary.
|
||||
|
||||
Returns:
|
||||
Whether the block contains base64 data.
|
||||
"""
|
||||
# Check for explicit base64 field (standard content blocks)
|
||||
if block.get("base64"):
|
||||
return True
|
||||
|
||||
# Check for data: URL in url field
|
||||
url = block.get("url", "")
|
||||
if isinstance(url, str) and url.startswith("data:"):
|
||||
return True
|
||||
|
||||
# Check for OpenAI-style image_url with data: URL
|
||||
image_url = block.get("image_url", {})
|
||||
if isinstance(image_url, dict):
|
||||
url = image_url.get("url", "")
|
||||
if isinstance(url, str) and url.startswith("data:"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
_XML_CONTENT_BLOCK_MAX_LEN = 500
|
||||
|
||||
|
||||
def _truncate(text: str, max_len: int = _XML_CONTENT_BLOCK_MAX_LEN) -> str:
|
||||
"""Truncate text to `max_len` characters, adding ellipsis if truncated."""
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return text[:max_len] + "..."
|
||||
|
||||
|
||||
def _format_content_block_xml(block: dict) -> str | None:
|
||||
"""Format a content block as XML.
|
||||
|
||||
Args:
|
||||
block: A LangChain content block.
|
||||
|
||||
Returns:
|
||||
XML string representation of the block, or `None` if the block should be
|
||||
skipped.
|
||||
|
||||
Note:
|
||||
Plain text document content, server tool call arguments, and server tool
|
||||
result outputs are truncated to 500 characters.
|
||||
"""
|
||||
block_type = block.get("type", "")
|
||||
|
||||
# Skip blocks with base64 encoded data
|
||||
if _has_base64_data(block):
|
||||
return None
|
||||
|
||||
# Text blocks
|
||||
if block_type == "text":
|
||||
text = block.get("text", "")
|
||||
return escape(text) if text else None
|
||||
|
||||
# Reasoning blocks
|
||||
if block_type == "reasoning":
|
||||
reasoning = block.get("reasoning", "")
|
||||
if reasoning:
|
||||
return f"<reasoning>{escape(reasoning)}</reasoning>"
|
||||
return None
|
||||
|
||||
# Image blocks (URL only, base64 already filtered)
|
||||
if block_type == "image":
|
||||
url = block.get("url")
|
||||
file_id = block.get("file_id")
|
||||
if url:
|
||||
return f"<image url={quoteattr(url)} />"
|
||||
if file_id:
|
||||
return f"<image file_id={quoteattr(file_id)} />"
|
||||
return None
|
||||
|
||||
# OpenAI-style image_url blocks
|
||||
if block_type == "image_url":
|
||||
image_url = block.get("image_url", {})
|
||||
if isinstance(image_url, dict):
|
||||
url = image_url.get("url", "")
|
||||
if url and not url.startswith("data:"):
|
||||
return f"<image url={quoteattr(url)} />"
|
||||
return None
|
||||
|
||||
# Audio blocks (URL only)
|
||||
if block_type == "audio":
|
||||
url = block.get("url")
|
||||
file_id = block.get("file_id")
|
||||
if url:
|
||||
return f"<audio url={quoteattr(url)} />"
|
||||
if file_id:
|
||||
return f"<audio file_id={quoteattr(file_id)} />"
|
||||
return None
|
||||
|
||||
# Video blocks (URL only)
|
||||
if block_type == "video":
|
||||
url = block.get("url")
|
||||
file_id = block.get("file_id")
|
||||
if url:
|
||||
return f"<video url={quoteattr(url)} />"
|
||||
if file_id:
|
||||
return f"<video file_id={quoteattr(file_id)} />"
|
||||
return None
|
||||
|
||||
# Plain text document blocks
|
||||
if block_type == "text-plain":
|
||||
text = block.get("text", "")
|
||||
return escape(_truncate(text)) if text else None
|
||||
|
||||
# Server tool call blocks (from AI messages)
|
||||
if block_type == "server_tool_call":
|
||||
tc_id = quoteattr(str(block.get("id") or ""))
|
||||
tc_name = quoteattr(str(block.get("name") or ""))
|
||||
tc_args_json = json.dumps(block.get("args", {}), ensure_ascii=False)
|
||||
tc_args = escape(_truncate(tc_args_json))
|
||||
return (
|
||||
f"<server_tool_call id={tc_id} name={tc_name}>{tc_args}</server_tool_call>"
|
||||
)
|
||||
|
||||
# Server tool result blocks
|
||||
if block_type == "server_tool_result":
|
||||
tool_call_id = quoteattr(str(block.get("tool_call_id") or ""))
|
||||
status = quoteattr(str(block.get("status") or ""))
|
||||
output = block.get("output")
|
||||
if output:
|
||||
output_json = json.dumps(output, ensure_ascii=False)
|
||||
output_str = escape(_truncate(output_json))
|
||||
else:
|
||||
output_str = ""
|
||||
return (
|
||||
f"<server_tool_result tool_call_id={tool_call_id} status={status}>"
|
||||
f"{output_str}</server_tool_result>"
|
||||
)
|
||||
|
||||
# Unknown block type - skip silently
|
||||
return None
|
||||
|
||||
|
||||
def _get_message_type_str(
|
||||
m: BaseMessage,
|
||||
human_prefix: str,
|
||||
ai_prefix: str,
|
||||
system_prefix: str,
|
||||
function_prefix: str,
|
||||
tool_prefix: str,
|
||||
) -> str:
|
||||
"""Get the type string for XML message element.
|
||||
|
||||
Args:
|
||||
m: The message to get the type string for.
|
||||
human_prefix: The prefix to use for `HumanMessage`.
|
||||
ai_prefix: The prefix to use for `AIMessage`.
|
||||
system_prefix: The prefix to use for `SystemMessage`.
|
||||
function_prefix: The prefix to use for `FunctionMessage`.
|
||||
tool_prefix: The prefix to use for `ToolMessage`.
|
||||
|
||||
Returns:
|
||||
The type string for the message element.
|
||||
|
||||
Raises:
|
||||
ValueError: If an unsupported message type is encountered.
|
||||
"""
|
||||
if isinstance(m, HumanMessage):
|
||||
return human_prefix.lower()
|
||||
if isinstance(m, AIMessage):
|
||||
return ai_prefix.lower()
|
||||
if isinstance(m, SystemMessage):
|
||||
return system_prefix.lower()
|
||||
if isinstance(m, FunctionMessage):
|
||||
return function_prefix.lower()
|
||||
if isinstance(m, ToolMessage):
|
||||
return tool_prefix.lower()
|
||||
if isinstance(m, ChatMessage):
|
||||
return m.role
|
||||
msg = f"Got unsupported message type: {m}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def get_buffer_string(
|
||||
messages: Sequence[BaseMessage], human_prefix: str = "Human", ai_prefix: str = "AI"
|
||||
messages: Sequence[BaseMessage],
|
||||
human_prefix: str = "Human",
|
||||
ai_prefix: str = "AI",
|
||||
*,
|
||||
system_prefix: str = "System",
|
||||
function_prefix: str = "Function",
|
||||
tool_prefix: str = "Tool",
|
||||
message_separator: str = "\n",
|
||||
format: Literal["prefix", "xml"] = "prefix", # noqa: A002
|
||||
) -> str:
|
||||
r"""Convert a sequence of messages to strings and concatenate them into one string.
|
||||
|
||||
@@ -98,6 +299,15 @@ def get_buffer_string(
|
||||
messages: Messages to be converted to strings.
|
||||
human_prefix: The prefix to prepend to contents of `HumanMessage`s.
|
||||
ai_prefix: The prefix to prepend to contents of `AIMessage`.
|
||||
system_prefix: The prefix to prepend to contents of `SystemMessage`s.
|
||||
function_prefix: The prefix to prepend to contents of `FunctionMessage`s.
|
||||
tool_prefix: The prefix to prepend to contents of `ToolMessage`s.
|
||||
message_separator: The separator to use between messages.
|
||||
format: The output format. `'prefix'` uses `Role: content` format (default).
|
||||
|
||||
`'xml'` uses XML-style `<message type='role'>` format with proper character
|
||||
escaping, which is useful when message content may contain role-like
|
||||
prefixes that could cause ambiguity.
|
||||
|
||||
Returns:
|
||||
A single string concatenation of all input messages.
|
||||
@@ -105,9 +315,38 @@ def get_buffer_string(
|
||||
Raises:
|
||||
ValueError: If an unsupported message type is encountered.
|
||||
|
||||
Note:
|
||||
If a message is an `AIMessage` and contains both tool calls under `tool_calls`
|
||||
and a function call under `additional_kwargs["function_call"]`, only the tool
|
||||
calls will be appended to the string representation.
|
||||
|
||||
When using `format='xml'`:
|
||||
|
||||
- All messages use uniform `<message type="role">content</message>` format.
|
||||
- The `type` attribute uses `human_prefix` (lowercased) for `HumanMessage`,
|
||||
`ai_prefix` (lowercased) for `AIMessage`, `system_prefix` (lowercased)
|
||||
for `SystemMessage`, `function_prefix` (lowercased) for `FunctionMessage`,
|
||||
`tool_prefix` (lowercased) for `ToolMessage`, and the original role
|
||||
(unchanged) for `ChatMessage`.
|
||||
- Message content is escaped using `xml.sax.saxutils.escape()`.
|
||||
- Attribute values are escaped using `xml.sax.saxutils.quoteattr()`.
|
||||
- AI messages with tool calls use nested structure with `<content>` and
|
||||
`<tool_call>` elements.
|
||||
- For multi-modal content (list of content blocks), supported block types
|
||||
are: `text`, `reasoning`, `image` (URL/file_id only), `image_url`
|
||||
(OpenAI-style, URL only), `audio` (URL/file_id only), `video` (URL/file_id
|
||||
only), `text-plain`, `server_tool_call`, and `server_tool_result`.
|
||||
- Content blocks with base64-encoded data are skipped (including blocks
|
||||
with `base64` field or `data:` URLs).
|
||||
- Unknown block types are skipped.
|
||||
- Plain text document content (`text-plain`), server tool call arguments,
|
||||
and server tool result outputs are truncated to 500 characters.
|
||||
|
||||
Example:
|
||||
Default prefix format:
|
||||
|
||||
```python
|
||||
from langchain_core import AIMessage, HumanMessage
|
||||
from langchain_core.messages import AIMessage, HumanMessage, get_buffer_string
|
||||
|
||||
messages = [
|
||||
HumanMessage(content="Hi, how are you?"),
|
||||
@@ -116,7 +355,54 @@ def get_buffer_string(
|
||||
get_buffer_string(messages)
|
||||
# -> "Human: Hi, how are you?\nAI: Good, how are you?"
|
||||
```
|
||||
|
||||
XML format (useful when content contains role-like prefixes):
|
||||
|
||||
```python
|
||||
messages = [
|
||||
HumanMessage(content="Example: Human: some text"),
|
||||
AIMessage(content="I see the example."),
|
||||
]
|
||||
get_buffer_string(messages, format="xml")
|
||||
# -> '<message type="human">Example: Human: some text</message>\\n'
|
||||
# -> '<message type="ai">I see the example.</message>'
|
||||
```
|
||||
|
||||
XML format with special characters (automatically escaped):
|
||||
|
||||
```python
|
||||
messages = [
|
||||
HumanMessage(content="Is 5 < 10 & 10 > 5?"),
|
||||
]
|
||||
get_buffer_string(messages, format="xml")
|
||||
# -> '<message type="human">Is 5 < 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):
|
||||
@@ -124,22 +410,96 @@ def get_buffer_string(
|
||||
elif isinstance(m, AIMessage):
|
||||
role = ai_prefix
|
||||
elif isinstance(m, SystemMessage):
|
||||
role = "System"
|
||||
role = system_prefix
|
||||
elif isinstance(m, FunctionMessage):
|
||||
role = "Function"
|
||||
role = function_prefix
|
||||
elif isinstance(m, ToolMessage):
|
||||
role = "Tool"
|
||||
role = tool_prefix
|
||||
elif isinstance(m, ChatMessage):
|
||||
role = m.role
|
||||
else:
|
||||
msg = f"Got unsupported message type: {m}"
|
||||
raise ValueError(msg) # noqa: TRY004
|
||||
message = f"{role}: {m.text}"
|
||||
if isinstance(m, AIMessage) and "function_call" in m.additional_kwargs:
|
||||
message += f"{m.additional_kwargs['function_call']}"
|
||||
|
||||
if format == "xml":
|
||||
msg_type = _get_message_type_str(
|
||||
m, human_prefix, ai_prefix, system_prefix, function_prefix, tool_prefix
|
||||
)
|
||||
|
||||
# Format content blocks
|
||||
if isinstance(m.content, str):
|
||||
content_parts = [escape(m.content)] if m.content else []
|
||||
else:
|
||||
# List of content blocks
|
||||
content_parts = []
|
||||
for block in m.content:
|
||||
if isinstance(block, str):
|
||||
if block:
|
||||
content_parts.append(escape(block))
|
||||
else:
|
||||
formatted = _format_content_block_xml(block)
|
||||
if formatted:
|
||||
content_parts.append(formatted)
|
||||
|
||||
# Check if this is an AIMessage with tool calls
|
||||
has_tool_calls = isinstance(m, AIMessage) and m.tool_calls
|
||||
has_function_call = (
|
||||
isinstance(m, AIMessage)
|
||||
and not m.tool_calls
|
||||
and "function_call" in m.additional_kwargs
|
||||
)
|
||||
|
||||
if has_tool_calls or has_function_call:
|
||||
# Use nested structure for AI messages with tool calls
|
||||
# Type narrowing: at this point m is AIMessage (verified above)
|
||||
ai_msg = cast("AIMessage", m)
|
||||
parts = [f"<message type={quoteattr(msg_type)}>"]
|
||||
if content_parts:
|
||||
parts.append(f" <content>{' '.join(content_parts)}</content>")
|
||||
|
||||
if has_tool_calls:
|
||||
for tc in ai_msg.tool_calls:
|
||||
tc_id = quoteattr(str(tc.get("id") or ""))
|
||||
tc_name = quoteattr(str(tc.get("name") or ""))
|
||||
tc_args = escape(
|
||||
json.dumps(tc.get("args", {}), ensure_ascii=False)
|
||||
)
|
||||
parts.append(
|
||||
f" <tool_call id={tc_id} name={tc_name}>"
|
||||
f"{tc_args}</tool_call>"
|
||||
)
|
||||
elif has_function_call:
|
||||
fc = ai_msg.additional_kwargs["function_call"]
|
||||
fc_name = quoteattr(str(fc.get("name") or ""))
|
||||
fc_args = escape(str(fc.get("arguments") or "{}"))
|
||||
parts.append(
|
||||
f" <function_call name={fc_name}>{fc_args}</function_call>"
|
||||
)
|
||||
|
||||
parts.append("</message>")
|
||||
message = "\n".join(parts)
|
||||
else:
|
||||
# Simple structure for messages without tool calls
|
||||
joined_content = " ".join(content_parts)
|
||||
message = (
|
||||
f"<message type={quoteattr(msg_type)}>{joined_content}</message>"
|
||||
)
|
||||
else: # format == "prefix"
|
||||
content = m.text
|
||||
message = f"{role}: {content}"
|
||||
tool_info = ""
|
||||
if isinstance(m, AIMessage):
|
||||
if m.tool_calls:
|
||||
tool_info = str(m.tool_calls)
|
||||
elif "function_call" in m.additional_kwargs:
|
||||
# Legacy behavior assumes only one function call per message
|
||||
tool_info = str(m.additional_kwargs["function_call"])
|
||||
if tool_info:
|
||||
message += tool_info # Preserve original behavior
|
||||
|
||||
string_messages.append(message)
|
||||
|
||||
return "\n".join(string_messages)
|
||||
return message_separator.join(string_messages)
|
||||
|
||||
|
||||
def _message_from_dict(message: dict) -> BaseMessage:
|
||||
@@ -202,8 +562,11 @@ def message_chunk_to_message(chunk: BaseMessage) -> BaseMessage:
|
||||
ignore_keys = ["type"]
|
||||
if isinstance(chunk, AIMessageChunk):
|
||||
ignore_keys.extend(["tool_call_chunks", "chunk_position"])
|
||||
return chunk.__class__.__mro__[1](
|
||||
**{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}
|
||||
return cast(
|
||||
"BaseMessage",
|
||||
chunk.__class__.__mro__[1](
|
||||
**{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -225,13 +588,13 @@ def _create_message_from_message_type(
|
||||
"""Create a message from a `Message` type and content string.
|
||||
|
||||
Args:
|
||||
message_type: (str) the type of the message (e.g., `'human'`, `'ai'`, etc.).
|
||||
content: (str) the content string.
|
||||
name: (str) the name of the message.
|
||||
tool_call_id: (str) the tool call id.
|
||||
tool_calls: (list[dict[str, Any]]) the tool calls.
|
||||
id: (str) the id of the message.
|
||||
additional_kwargs: (dict[str, Any]) additional keyword arguments.
|
||||
message_type: the type of the message (e.g., `'human'`, `'ai'`, etc.).
|
||||
content: the content string.
|
||||
name: the name of the message.
|
||||
tool_call_id: the tool call id.
|
||||
tool_calls: the tool calls.
|
||||
id: the id of the message.
|
||||
additional_kwargs: additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
a message of the appropriate type.
|
||||
@@ -384,33 +747,54 @@ def convert_to_messages(
|
||||
return [_convert_to_message(m) for m in messages]
|
||||
|
||||
|
||||
def _runnable_support(func: Callable) -> Callable:
|
||||
_P = ParamSpec("_P")
|
||||
_R_co = TypeVar("_R_co", covariant=True)
|
||||
|
||||
|
||||
class _RunnableSupportCallable(Protocol[_P, _R_co]):
|
||||
@overload
|
||||
def wrapped(
|
||||
messages: None = None, **kwargs: Any
|
||||
) -> Runnable[Sequence[MessageLikeRepresentation], list[BaseMessage]]: ...
|
||||
def __call__(
|
||||
self,
|
||||
messages: None = None,
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> Runnable[Sequence[MessageLikeRepresentation], _R_co]: ...
|
||||
|
||||
@overload
|
||||
def wrapped(
|
||||
messages: Sequence[MessageLikeRepresentation], **kwargs: Any
|
||||
) -> list[BaseMessage]: ...
|
||||
def __call__(
|
||||
self,
|
||||
messages: Sequence[MessageLikeRepresentation] | PromptValue,
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> _R_co: ...
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
messages: Sequence[MessageLikeRepresentation] | PromptValue | None = None,
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> _R_co | Runnable[Sequence[MessageLikeRepresentation], _R_co]: ...
|
||||
|
||||
|
||||
def _runnable_support(
|
||||
func: Callable[
|
||||
Concatenate[Sequence[MessageLikeRepresentation] | PromptValue, _P], _R_co
|
||||
],
|
||||
) -> _RunnableSupportCallable[_P, _R_co]:
|
||||
@wraps(func)
|
||||
def wrapped(
|
||||
messages: Sequence[MessageLikeRepresentation] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> (
|
||||
list[BaseMessage]
|
||||
| Runnable[Sequence[MessageLikeRepresentation], list[BaseMessage]]
|
||||
):
|
||||
messages: Sequence[MessageLikeRepresentation] | PromptValue | None = None,
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> _R_co | Runnable[Sequence[MessageLikeRepresentation], _R_co]:
|
||||
# Import locally to prevent circular import.
|
||||
from langchain_core.runnables.base import RunnableLambda # noqa: PLC0415
|
||||
|
||||
if messages is not None:
|
||||
return func(messages, **kwargs)
|
||||
return func(messages, *args, **kwargs)
|
||||
return RunnableLambda(partial(func, **kwargs), name=func.__name__)
|
||||
|
||||
wrapped.__doc__ = func.__doc__
|
||||
return wrapped
|
||||
return cast("_RunnableSupportCallable[_P, _R_co]", wrapped)
|
||||
|
||||
|
||||
@_runnable_support
|
||||
@@ -514,6 +898,7 @@ def filter_messages(
|
||||
):
|
||||
continue
|
||||
|
||||
new_msg = msg
|
||||
if isinstance(exclude_tool_calls, (list, tuple, set)):
|
||||
if isinstance(msg, AIMessage) and msg.tool_calls:
|
||||
tool_calls = [
|
||||
@@ -537,7 +922,7 @@ def filter_messages(
|
||||
)
|
||||
]
|
||||
|
||||
msg = msg.model_copy( # noqa: PLW2901
|
||||
new_msg = msg.model_copy(
|
||||
update={"tool_calls": tool_calls, "content": content}
|
||||
)
|
||||
elif (
|
||||
@@ -548,11 +933,11 @@ def filter_messages(
|
||||
# default to inclusion when no inclusion criteria given.
|
||||
if (
|
||||
not (include_types or include_ids or include_names)
|
||||
or (include_names and msg.name in include_names)
|
||||
or (include_types and _is_message_type(msg, include_types))
|
||||
or (include_ids and msg.id in include_ids)
|
||||
or (include_names and new_msg.name in include_names)
|
||||
or (include_types and _is_message_type(new_msg, include_types))
|
||||
or (include_ids and new_msg.id in include_ids)
|
||||
):
|
||||
filtered.append(msg)
|
||||
filtered.append(new_msg)
|
||||
|
||||
return filtered
|
||||
|
||||
@@ -695,7 +1080,8 @@ def trim_messages(
|
||||
max_tokens: int,
|
||||
token_counter: Callable[[list[BaseMessage]], int]
|
||||
| Callable[[BaseMessage], int]
|
||||
| BaseLanguageModel,
|
||||
| BaseLanguageModel
|
||||
| Literal["approximate"],
|
||||
strategy: Literal["first", "last"] = "last",
|
||||
allow_partial: bool = False,
|
||||
end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,
|
||||
@@ -733,53 +1119,65 @@ def trim_messages(
|
||||
messages: Sequence of Message-like objects to trim.
|
||||
max_tokens: Max token count of trimmed messages.
|
||||
token_counter: Function or llm for counting tokens in a `BaseMessage` or a
|
||||
list of `BaseMessage`. If a `BaseLanguageModel` is passed in then
|
||||
`BaseLanguageModel.get_num_tokens_from_messages()` will be used.
|
||||
Set to `len` to count the number of **messages** in the chat history.
|
||||
list of `BaseMessage`.
|
||||
|
||||
If a `BaseLanguageModel` is passed in then
|
||||
`BaseLanguageModel.get_num_tokens_from_messages()` will be used. Set to
|
||||
`len` to count the number of **messages** in the chat history.
|
||||
|
||||
You can also use string shortcuts for convenience:
|
||||
|
||||
- `'approximate'`: Uses `count_tokens_approximately` for fast, approximate
|
||||
token counts.
|
||||
|
||||
!!! note
|
||||
|
||||
Use `count_tokens_approximately` to get fast, approximate token
|
||||
counts.
|
||||
|
||||
This is recommended for using `trim_messages` on the hot path, where
|
||||
exact token counting is not necessary.
|
||||
`count_tokens_approximately` (or the shortcut `'approximate'`) is
|
||||
recommended for using `trim_messages` on the hot path, where exact token
|
||||
counting is not necessary.
|
||||
|
||||
strategy: Strategy for trimming.
|
||||
|
||||
- `'first'`: Keep the first `<= n_count` tokens of the messages.
|
||||
- `'last'`: Keep the last `<= n_count` tokens of the messages.
|
||||
allow_partial: Whether to split a message if only part of the message can be
|
||||
included. If `strategy='last'` then the last partial contents of a message
|
||||
are included. If `strategy='first'` then the first partial contents of a
|
||||
message are included.
|
||||
end_on: The message type to end on. If specified then every message after the
|
||||
last occurrence of this type is ignored. If `strategy='last'` then this
|
||||
is done before we attempt to get the last `max_tokens`. If
|
||||
`strategy='first'` then this is done after we get the first
|
||||
`max_tokens`. Can be specified as string names (e.g. `'system'`,
|
||||
`'human'`, `'ai'`, ...) or as `BaseMessage` classes (e.g.
|
||||
`SystemMessage`, `HumanMessage`, `AIMessage`, ...). Can be a single
|
||||
type or a list of types.
|
||||
included.
|
||||
|
||||
start_on: The message type to start on. Should only be specified if
|
||||
`strategy='last'`. If specified then every message before
|
||||
the first occurrence of this type is ignored. This is done after we trim
|
||||
the initial messages to the last `max_tokens`. Does not
|
||||
apply to a `SystemMessage` at index 0 if `include_system=True`. Can be
|
||||
specified as string names (e.g. `'system'`, `'human'`, `'ai'`, ...) or
|
||||
as `BaseMessage` classes (e.g. `SystemMessage`, `HumanMessage`,
|
||||
`AIMessage`, ...). Can be a single type or a list of types.
|
||||
If `strategy='last'` then the last partial contents of a message are
|
||||
included. If `strategy='first'` then the first partial contents of a
|
||||
message are included.
|
||||
end_on: The message type to end on.
|
||||
|
||||
If specified then every message after the last occurrence of this type is
|
||||
ignored. If `strategy='last'` then this is done before we attempt to get the
|
||||
last `max_tokens`. If `strategy='first'` then this is done after we get the
|
||||
first `max_tokens`. Can be specified as string names (e.g. `'system'`,
|
||||
`'human'`, `'ai'`, ...) or as `BaseMessage` classes (e.g. `SystemMessage`,
|
||||
`HumanMessage`, `AIMessage`, ...). Can be a single type or a list of types.
|
||||
|
||||
start_on: The message type to start on.
|
||||
|
||||
Should only be specified if `strategy='last'`. If specified then every
|
||||
message before the first occurrence of this type is ignored. This is done
|
||||
after we trim the initial messages to the last `max_tokens`. Does not apply
|
||||
to a `SystemMessage` at index 0 if `include_system=True`. Can be specified
|
||||
as string names (e.g. `'system'`, `'human'`, `'ai'`, ...) or as
|
||||
`BaseMessage` classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`,
|
||||
...). Can be a single type or a list of types.
|
||||
|
||||
include_system: Whether to keep the `SystemMessage` if there is one at index
|
||||
`0`. Should only be specified if `strategy="last"`.
|
||||
`0`.
|
||||
|
||||
Should only be specified if `strategy="last"`.
|
||||
text_splitter: Function or `langchain_text_splitters.TextSplitter` for
|
||||
splitting the string contents of a message. Only used if
|
||||
`allow_partial=True`. If `strategy='last'` then the last split tokens
|
||||
from a partial message will be included. if `strategy='first'` then the
|
||||
first split tokens from a partial message will be included. Token splitter
|
||||
assumes that separators are kept, so that split contents can be directly
|
||||
concatenated to recreate the original text. Defaults to splitting on
|
||||
newlines.
|
||||
splitting the string contents of a message.
|
||||
|
||||
Only used if `allow_partial=True`. If `strategy='last'` then the last split
|
||||
tokens from a partial message will be included. if `strategy='first'` then
|
||||
the first split tokens from a partial message will be included. Token
|
||||
splitter assumes that separators are kept, so that split contents can be
|
||||
directly concatenated to recreate the original text. Defaults to splitting
|
||||
on newlines.
|
||||
|
||||
Returns:
|
||||
List of trimmed `BaseMessage`.
|
||||
@@ -790,8 +1188,8 @@ def trim_messages(
|
||||
|
||||
Example:
|
||||
Trim chat history based on token count, keeping the `SystemMessage` if
|
||||
present, and ensuring that the chat history starts with a `HumanMessage` (
|
||||
or a `SystemMessage` followed by a `HumanMessage`).
|
||||
present, and ensuring that the chat history starts with a `HumanMessage` (or a
|
||||
`SystemMessage` followed by a `HumanMessage`).
|
||||
|
||||
```python
|
||||
from langchain_core.messages import (
|
||||
@@ -844,8 +1242,34 @@ def trim_messages(
|
||||
]
|
||||
```
|
||||
|
||||
Trim chat history using approximate token counting with `'approximate'`:
|
||||
|
||||
```python
|
||||
trim_messages(
|
||||
messages,
|
||||
max_tokens=45,
|
||||
strategy="last",
|
||||
# Using the "approximate" shortcut for fast token counting
|
||||
token_counter="approximate",
|
||||
start_on="human",
|
||||
include_system=True,
|
||||
)
|
||||
|
||||
# This is equivalent to using `count_tokens_approximately` directly
|
||||
from langchain_core.messages.utils import count_tokens_approximately
|
||||
|
||||
trim_messages(
|
||||
messages,
|
||||
max_tokens=45,
|
||||
strategy="last",
|
||||
token_counter=count_tokens_approximately,
|
||||
start_on="human",
|
||||
include_system=True,
|
||||
)
|
||||
```
|
||||
|
||||
Trim chat history based on the message count, keeping the `SystemMessage` if
|
||||
present, and ensuring that the chat history starts with a `HumanMessage` (
|
||||
present, and ensuring that the chat history starts with a HumanMessage (
|
||||
or a `SystemMessage` followed by a `HumanMessage`).
|
||||
|
||||
trim_messages(
|
||||
@@ -967,24 +1391,44 @@ def trim_messages(
|
||||
raise ValueError(msg)
|
||||
|
||||
messages = convert_to_messages(messages)
|
||||
if hasattr(token_counter, "get_num_tokens_from_messages"):
|
||||
list_token_counter = token_counter.get_num_tokens_from_messages
|
||||
elif callable(token_counter):
|
||||
|
||||
# Handle string shortcuts for token counter
|
||||
if isinstance(token_counter, str):
|
||||
if token_counter in _TOKEN_COUNTER_SHORTCUTS:
|
||||
actual_token_counter = _TOKEN_COUNTER_SHORTCUTS[token_counter]
|
||||
else:
|
||||
available_shortcuts = ", ".join(
|
||||
f"'{key}'" for key in _TOKEN_COUNTER_SHORTCUTS
|
||||
)
|
||||
msg = (
|
||||
f"Invalid token_counter shortcut '{token_counter}'. "
|
||||
f"Available shortcuts: {available_shortcuts}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
# Type narrowing: at this point token_counter is not a str
|
||||
actual_token_counter = token_counter # type: ignore[assignment]
|
||||
|
||||
if hasattr(actual_token_counter, "get_num_tokens_from_messages"):
|
||||
list_token_counter = actual_token_counter.get_num_tokens_from_messages
|
||||
elif callable(actual_token_counter):
|
||||
if (
|
||||
next(iter(inspect.signature(token_counter).parameters.values())).annotation
|
||||
next(
|
||||
iter(inspect.signature(actual_token_counter).parameters.values())
|
||||
).annotation
|
||||
is BaseMessage
|
||||
):
|
||||
|
||||
def list_token_counter(messages: Sequence[BaseMessage]) -> int:
|
||||
return sum(token_counter(msg) for msg in messages) # type: ignore[arg-type, misc]
|
||||
return sum(actual_token_counter(msg) for msg in messages) # type: ignore[arg-type, misc]
|
||||
|
||||
else:
|
||||
list_token_counter = token_counter
|
||||
list_token_counter = actual_token_counter
|
||||
else:
|
||||
msg = (
|
||||
f"'token_counter' expected to be a model that implements "
|
||||
f"'get_num_tokens_from_messages()' or a function. Received object of type "
|
||||
f"{type(token_counter)}."
|
||||
f"{type(actual_token_counter)}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
@@ -1019,11 +1463,38 @@ def trim_messages(
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
_SingleMessage = BaseMessage | str | dict[str, Any]
|
||||
_T = TypeVar("_T", bound=_SingleMessage)
|
||||
# A sequence of _SingleMessage that is NOT a bare str
|
||||
_MultipleMessages = Sequence[_T]
|
||||
|
||||
|
||||
@overload
|
||||
def convert_to_openai_messages(
|
||||
messages: _SingleMessage,
|
||||
*,
|
||||
text_format: Literal["string", "block"] = "string",
|
||||
include_id: bool = False,
|
||||
pass_through_unknown_blocks: bool = True,
|
||||
) -> dict: ...
|
||||
|
||||
|
||||
@overload
|
||||
def convert_to_openai_messages(
|
||||
messages: _MultipleMessages,
|
||||
*,
|
||||
text_format: Literal["string", "block"] = "string",
|
||||
include_id: bool = False,
|
||||
pass_through_unknown_blocks: bool = True,
|
||||
) -> list[dict]: ...
|
||||
|
||||
|
||||
def convert_to_openai_messages(
|
||||
messages: MessageLikeRepresentation | Sequence[MessageLikeRepresentation],
|
||||
*,
|
||||
text_format: Literal["string", "block"] = "string",
|
||||
include_id: bool = False,
|
||||
pass_through_unknown_blocks: bool = True,
|
||||
) -> dict | list[dict]:
|
||||
"""Convert LangChain messages into OpenAI message dicts.
|
||||
|
||||
@@ -1043,6 +1514,9 @@ def convert_to_openai_messages(
|
||||
content blocks these are left as is.
|
||||
include_id: Whether to include message IDs in the openai messages, if they
|
||||
are present in the source messages.
|
||||
pass_through_unknown_blocks: Whether to include content blocks with unknown
|
||||
formats in the output. If `False`, an error is raised if an unknown
|
||||
content block is encountered.
|
||||
|
||||
Raises:
|
||||
ValueError: if an unrecognized `text_format` is specified, or if a message
|
||||
@@ -1110,7 +1584,7 @@ def convert_to_openai_messages(
|
||||
err = f"Unrecognized {text_format=}, expected one of 'string' or 'block'."
|
||||
raise ValueError(err)
|
||||
|
||||
oai_messages: list = []
|
||||
oai_messages: list[dict] = []
|
||||
|
||||
if is_single := isinstance(messages, (BaseMessage, dict, str)):
|
||||
messages = [messages]
|
||||
@@ -1292,6 +1766,36 @@ def convert_to_openai_messages(
|
||||
},
|
||||
}
|
||||
)
|
||||
elif block.get("type") == "function_call": # OpenAI Responses
|
||||
if not any(
|
||||
tool_call["id"] == block.get("call_id")
|
||||
for tool_call in cast("AIMessage", message).tool_calls
|
||||
):
|
||||
if missing := [
|
||||
k
|
||||
for k in ("call_id", "name", "arguments")
|
||||
if k not in block
|
||||
]:
|
||||
err = (
|
||||
f"Unrecognized content block at "
|
||||
f"messages[{i}].content[{j}] has 'type': "
|
||||
f"'tool_use', but is missing expected key(s) "
|
||||
f"{missing}. Full content block:\n\n{block}"
|
||||
)
|
||||
raise ValueError(err)
|
||||
oai_msg["tool_calls"] = oai_msg.get("tool_calls", [])
|
||||
oai_msg["tool_calls"].append(
|
||||
{
|
||||
"type": "function",
|
||||
"id": block.get("call_id"),
|
||||
"function": {
|
||||
"name": block.get("name"),
|
||||
"arguments": block.get("arguments"),
|
||||
},
|
||||
}
|
||||
)
|
||||
if pass_through_unknown_blocks:
|
||||
content.append(block)
|
||||
elif block.get("type") == "tool_result":
|
||||
if missing := [
|
||||
k for k in ("content", "tool_use_id") if k not in block
|
||||
@@ -1372,7 +1876,10 @@ def convert_to_openai_messages(
|
||||
},
|
||||
}
|
||||
)
|
||||
elif block.get("type") in ["thinking", "reasoning"]:
|
||||
elif (
|
||||
block.get("type") in {"thinking", "reasoning"}
|
||||
or pass_through_unknown_blocks
|
||||
):
|
||||
content.append(block)
|
||||
else:
|
||||
err = (
|
||||
@@ -1644,7 +2151,11 @@ def _get_message_openai_role(message: BaseMessage) -> str:
|
||||
if isinstance(message, ToolMessage):
|
||||
return "tool"
|
||||
if isinstance(message, SystemMessage):
|
||||
return message.additional_kwargs.get("__openai_role__", "system")
|
||||
role = message.additional_kwargs.get("__openai_role__", "system")
|
||||
if not isinstance(role, str):
|
||||
msg = f"Expected '__openai_role__' to be a str, got {type(role).__name__}"
|
||||
raise TypeError(msg)
|
||||
return role
|
||||
if isinstance(message, FunctionMessage):
|
||||
return "function"
|
||||
if isinstance(message, ChatMessage):
|
||||
@@ -1677,26 +2188,29 @@ def count_tokens_approximately(
|
||||
"""Approximate the total number of tokens in messages.
|
||||
|
||||
The token count includes stringified message content, role, and (optionally) name.
|
||||
|
||||
- For AI messages, the token count also includes stringified tool calls.
|
||||
- For tool messages, the token count also includes the tool call ID.
|
||||
|
||||
Args:
|
||||
messages: List of messages to count tokens for.
|
||||
chars_per_token: Number of characters per token to use for the approximation.
|
||||
|
||||
One token corresponds to ~4 chars for common English text.
|
||||
|
||||
You can also specify `float` values for more fine-grained control.
|
||||
[See more here](https://platform.openai.com/tokenizer).
|
||||
extra_tokens_per_message: Number of extra tokens to add per message, e.g.
|
||||
special tokens, including beginning/end of message.
|
||||
|
||||
You can also specify `float` values for more fine-grained control.
|
||||
[See more here](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb).
|
||||
count_name: Whether to include message names in the count.
|
||||
Enabled by default.
|
||||
|
||||
Returns:
|
||||
Approximate number of tokens in the messages.
|
||||
|
||||
!!! note
|
||||
Note:
|
||||
This is a simple approximation that may not match the exact token count used by
|
||||
specific models. For accurate counts, use model-specific tokenizers.
|
||||
|
||||
@@ -1704,7 +2218,6 @@ def count_tokens_approximately(
|
||||
This function does not currently support counting image tokens.
|
||||
|
||||
!!! version-added "Added in `langchain-core` 0.3.46"
|
||||
|
||||
"""
|
||||
token_count = 0.0
|
||||
for message in convert_to_messages(messages):
|
||||
@@ -1745,3 +2258,14 @@ def count_tokens_approximately(
|
||||
|
||||
# round up once more time in case extra_tokens_per_message is a float
|
||||
return math.ceil(token_count)
|
||||
|
||||
|
||||
# Mapping from string shortcuts to token counter functions
|
||||
def _approximate_token_counter(messages: Sequence[BaseMessage]) -> int:
|
||||
"""Wrapper for `count_tokens_approximately` that matches expected signature."""
|
||||
return count_tokens_approximately(messages)
|
||||
|
||||
|
||||
_TOKEN_COUNTER_SHORTCUTS = {
|
||||
"approximate": _approximate_token_counter,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -47,22 +47,24 @@ def parse_tool_call(
|
||||
"""
|
||||
if "function" not in raw_tool_call:
|
||||
return None
|
||||
|
||||
arguments = raw_tool_call["function"]["arguments"]
|
||||
|
||||
if partial:
|
||||
try:
|
||||
function_args = parse_partial_json(
|
||||
raw_tool_call["function"]["arguments"], strict=strict
|
||||
)
|
||||
function_args = parse_partial_json(arguments, strict=strict)
|
||||
except (JSONDecodeError, TypeError): # None args raise TypeError
|
||||
return None
|
||||
# Handle None or empty string arguments for parameter-less tools
|
||||
elif not arguments:
|
||||
function_args = {}
|
||||
else:
|
||||
try:
|
||||
function_args = json.loads(
|
||||
raw_tool_call["function"]["arguments"], strict=strict
|
||||
)
|
||||
function_args = json.loads(arguments, strict=strict)
|
||||
except JSONDecodeError as e:
|
||||
msg = (
|
||||
f"Function {raw_tool_call['function']['name']} arguments:\n\n"
|
||||
f"{raw_tool_call['function']['arguments']}\n\nare not valid JSON. "
|
||||
f"{arguments}\n\nare not valid JSON. "
|
||||
f"Received JSONDecodeError {e}"
|
||||
)
|
||||
raise OutputParserException(msg) from e
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,7 +6,33 @@ from langchain_core.output_parsers.transform import BaseTransformOutputParser
|
||||
|
||||
|
||||
class StrOutputParser(BaseTransformOutputParser[str]):
|
||||
"""OutputParser that parses `LLMResult` into the top likely string."""
|
||||
"""Extract text content from model outputs as a string.
|
||||
|
||||
Converts model outputs (such as `AIMessage` or `AIMessageChunk` objects) into plain
|
||||
text strings. It's the simplest output parser and is useful when you need string
|
||||
responses for downstream processing, display, or storage.
|
||||
|
||||
Supports streaming, yielding text chunks as they're generated by the model.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
model = ChatOpenAI(model="gpt-4o")
|
||||
parser = StrOutputParser()
|
||||
|
||||
# Get string output from a model
|
||||
message = model.invoke("Tell me a joke")
|
||||
result = parser.invoke(message)
|
||||
print(result) # plain string
|
||||
|
||||
# With streaming - use transform() to process a stream
|
||||
stream = model.stream("Tell me a story")
|
||||
for chunk in parser.transform(stream):
|
||||
print(chunk, end="", flush=True)
|
||||
```
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
|
||||
@@ -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
|
||||
@@ -20,65 +21,8 @@ if TYPE_CHECKING:
|
||||
|
||||
try:
|
||||
from jinja2 import meta
|
||||
from jinja2.exceptions import SecurityError
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
class _RestrictedSandboxedEnvironment(SandboxedEnvironment):
|
||||
"""A more restrictive Jinja2 sandbox that blocks all attribute/method access.
|
||||
|
||||
This sandbox only allows simple variable lookups, no attribute or method access.
|
||||
This prevents template injection attacks via methods like parse_raw().
|
||||
"""
|
||||
|
||||
def is_safe_attribute(self, _obj: Any, _attr: str, _value: Any) -> bool:
|
||||
"""Block ALL attribute access for security.
|
||||
|
||||
Only allow accessing variables directly from the context dict,
|
||||
no attribute access on those objects.
|
||||
|
||||
Args:
|
||||
_obj: The object being accessed (unused, always blocked).
|
||||
_attr: The attribute name (unused, always blocked).
|
||||
_value: The attribute value (unused, always blocked).
|
||||
|
||||
Returns:
|
||||
False - all attribute access is blocked.
|
||||
"""
|
||||
# Block all attribute access
|
||||
return False
|
||||
|
||||
def is_safe_callable(self, _obj: Any) -> bool:
|
||||
"""Block all method calls for security.
|
||||
|
||||
Args:
|
||||
_obj: The object being checked (unused, always blocked).
|
||||
|
||||
Returns:
|
||||
False - all callables are blocked.
|
||||
"""
|
||||
return False
|
||||
|
||||
def getattr(self, obj: Any, attribute: str) -> Any:
|
||||
"""Override getattr to block all attribute access.
|
||||
|
||||
Args:
|
||||
obj: The object.
|
||||
attribute: The attribute name.
|
||||
|
||||
Returns:
|
||||
Never returns.
|
||||
|
||||
Raises:
|
||||
SecurityError: Always, to block attribute access.
|
||||
"""
|
||||
msg = (
|
||||
f"Access to attributes is not allowed in templates. "
|
||||
f"Attempted to access '{attribute}' on {type(obj).__name__}. "
|
||||
f"Use only simple variable names like {{{{variable}}}} "
|
||||
f"without dots or methods."
|
||||
)
|
||||
raise SecurityError(msg)
|
||||
|
||||
_HAS_JINJA2 = True
|
||||
except ImportError:
|
||||
_HAS_JINJA2 = False
|
||||
@@ -121,7 +65,7 @@ def jinja2_formatter(template: str, /, **kwargs: Any) -> str:
|
||||
# Use a restricted sandbox that blocks ALL attribute/method access
|
||||
# Only simple variable lookups like {{variable}} are allowed
|
||||
# Attribute access like {{variable.attr}} or {{variable.method()}} is blocked
|
||||
return _RestrictedSandboxedEnvironment().from_string(template).render(**kwargs)
|
||||
return SandboxedEnvironment().from_string(template).render(**kwargs)
|
||||
|
||||
|
||||
def validate_jinja2(template: str, input_variables: list[str]) -> None:
|
||||
@@ -156,7 +100,7 @@ def _get_jinja2_variables_from_template(template: str) -> set[str]:
|
||||
"Please install it with `pip install jinja2`."
|
||||
)
|
||||
raise ImportError(msg)
|
||||
env = _RestrictedSandboxedEnvironment()
|
||||
env = SandboxedEnvironment()
|
||||
ast = env.parse(template)
|
||||
return meta.find_undeclared_variables(ast)
|
||||
|
||||
@@ -246,17 +190,20 @@ def mustache_schema(template: str) -> type[BaseModel]:
|
||||
return _create_model_recursive("PromptInput", defs)
|
||||
|
||||
|
||||
def _create_model_recursive(name: str, defs: Defs) -> type:
|
||||
return create_model( # type: ignore[call-overload]
|
||||
name,
|
||||
**{
|
||||
k: (_create_model_recursive(k, v), None) if v else (type(v), None)
|
||||
for k, v in defs.items()
|
||||
},
|
||||
def _create_model_recursive(name: str, defs: Defs) -> type[BaseModel]:
|
||||
return cast(
|
||||
"type[BaseModel]",
|
||||
create_model( # type: ignore[call-overload]
|
||||
name,
|
||||
**{
|
||||
k: (_create_model_recursive(k, v), None) if v else (type(v), None)
|
||||
for k, v in defs.items()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_FORMATTER_MAPPING: dict[str, Callable] = {
|
||||
DEFAULT_FORMATTER_MAPPING: dict[str, Callable[..., str]] = {
|
||||
"f-string": formatter.format,
|
||||
"mustache": mustache_formatter,
|
||||
"jinja2": jinja2_formatter,
|
||||
@@ -387,6 +334,10 @@ class StringPromptTemplate(BasePromptTemplate, ABC):
|
||||
"""
|
||||
return StringPromptValue(text=await self.aformat(**kwargs))
|
||||
|
||||
@override
|
||||
@abstractmethod
|
||||
def format(self, **kwargs: Any) -> str: ...
|
||||
|
||||
def pretty_repr(
|
||||
self,
|
||||
html: bool = False, # noqa: FBT001,FBT002
|
||||
|
||||
@@ -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_:
|
||||
|
||||
@@ -94,7 +94,7 @@ from langchain_core.tracers.root_listeners import (
|
||||
AsyncRootListenersTracer,
|
||||
RootListenersTracer,
|
||||
)
|
||||
from langchain_core.utils.aiter import aclosing, atee, py_anext
|
||||
from langchain_core.utils.aiter import aclosing, atee
|
||||
from langchain_core.utils.iter import safetee
|
||||
from langchain_core.utils.pydantic import create_model_v2
|
||||
|
||||
@@ -127,10 +127,10 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Key Methods
|
||||
===========
|
||||
|
||||
- **`invoke`/`ainvoke`**: Transforms a single input into an output.
|
||||
- **`batch`/`abatch`**: Efficiently transforms multiple inputs into outputs.
|
||||
- **`stream`/`astream`**: Streams output from a single input as it's produced.
|
||||
- **`astream_log`**: Streams output and selected intermediate results from an
|
||||
- `invoke`/`ainvoke`: Transforms a single input into an output.
|
||||
- `batch`/`abatch`: Efficiently transforms multiple inputs into outputs.
|
||||
- `stream`/`astream`: Streams output from a single input as it's produced.
|
||||
- `astream_log`: Streams output and selected intermediate results from an
|
||||
input.
|
||||
|
||||
Built-in optimizations:
|
||||
@@ -315,7 +315,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
"args" in metadata
|
||||
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
|
||||
):
|
||||
return metadata["args"][0]
|
||||
return cast("type[Input]", metadata["args"][0])
|
||||
|
||||
# If we didn't find a Pydantic model in the parent classes,
|
||||
# then loop through __orig_bases__. This corresponds to
|
||||
@@ -323,7 +323,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
|
||||
type_args = get_args(cls)
|
||||
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
|
||||
return type_args[0]
|
||||
return cast("type[Input]", type_args[0])
|
||||
|
||||
msg = (
|
||||
f"Runnable {self.get_name()} doesn't have an inferable InputType. "
|
||||
@@ -349,12 +349,12 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
"args" in metadata
|
||||
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
|
||||
):
|
||||
return metadata["args"][1]
|
||||
return cast("type[Output]", metadata["args"][1])
|
||||
|
||||
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
|
||||
type_args = get_args(cls)
|
||||
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
|
||||
return type_args[1]
|
||||
return cast("type[Output]", type_args[1])
|
||||
|
||||
msg = (
|
||||
f"Runnable {self.get_name()} doesn't have an inferable OutputType. "
|
||||
@@ -369,7 +369,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
|
||||
def get_input_schema(
|
||||
self,
|
||||
config: RunnableConfig | None = None, # noqa: ARG002
|
||||
config: RunnableConfig | None = None,
|
||||
) -> type[BaseModel]:
|
||||
"""Get a Pydantic model that can be used to validate input to the `Runnable`.
|
||||
|
||||
@@ -385,6 +385,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Returns:
|
||||
A Pydantic model that can be used to validate input.
|
||||
"""
|
||||
_ = config
|
||||
root_type = self.InputType
|
||||
|
||||
if (
|
||||
@@ -447,7 +448,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
|
||||
def get_output_schema(
|
||||
self,
|
||||
config: RunnableConfig | None = None, # noqa: ARG002
|
||||
config: RunnableConfig | None = None,
|
||||
) -> type[BaseModel]:
|
||||
"""Get a Pydantic model that can be used to validate output to the `Runnable`.
|
||||
|
||||
@@ -463,6 +464,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Returns:
|
||||
A Pydantic model that can be used to validate output.
|
||||
"""
|
||||
_ = config
|
||||
root_type = self.OutputType
|
||||
|
||||
if (
|
||||
@@ -2277,6 +2279,9 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Use this to implement `stream` or `transform` in `Runnable` subclasses.
|
||||
|
||||
"""
|
||||
# Extract defers_inputs from kwargs if present
|
||||
defers_inputs = kwargs.pop("defers_inputs", False)
|
||||
|
||||
# tee the input so we can iterate over it twice
|
||||
input_for_tracing, input_for_transform = tee(inputs, 2)
|
||||
# Start the input iterator to ensure the input Runnable starts before this one
|
||||
@@ -2293,6 +2298,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
run_type=run_type,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
defers_inputs=defers_inputs,
|
||||
)
|
||||
try:
|
||||
child_config = patch_config(config, callbacks=run_manager.get_child())
|
||||
@@ -2374,10 +2380,13 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
Use this to implement `astream` or `atransform` in `Runnable` subclasses.
|
||||
|
||||
"""
|
||||
# Extract defers_inputs from kwargs if present
|
||||
defers_inputs = kwargs.pop("defers_inputs", False)
|
||||
|
||||
# tee the input so we can iterate over it twice
|
||||
input_for_tracing, input_for_transform = atee(inputs, 2)
|
||||
# Start the input iterator to ensure the input Runnable starts before this one
|
||||
final_input: Input | None = await py_anext(input_for_tracing, None)
|
||||
final_input: Input | None = await anext(input_for_tracing, None)
|
||||
final_input_supported = True
|
||||
final_output: Output | None = None
|
||||
final_output_supported = True
|
||||
@@ -2390,6 +2399,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
run_type=run_type,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
defers_inputs=defers_inputs,
|
||||
)
|
||||
try:
|
||||
child_config = patch_config(config, callbacks=run_manager.get_child())
|
||||
@@ -2417,7 +2427,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
iterator = iterator_
|
||||
try:
|
||||
while True:
|
||||
chunk = await coro_with_context(py_anext(iterator), context)
|
||||
chunk = await coro_with_context(anext(iterator), context)
|
||||
yield chunk
|
||||
if final_output_supported:
|
||||
if final_output is None:
|
||||
@@ -4025,7 +4035,7 @@ class RunnableParallel(RunnableSerializable[Input, dict[str, Any]]):
|
||||
|
||||
# Wrap in a coroutine to satisfy linter
|
||||
async def get_next_chunk(generator: AsyncIterator) -> Output | None:
|
||||
return await py_anext(generator)
|
||||
return await anext(generator)
|
||||
|
||||
# Start the first iteration of each generator
|
||||
tasks = {
|
||||
@@ -4323,6 +4333,7 @@ class RunnableGenerator(Runnable[Input, Output]):
|
||||
input,
|
||||
self._transform, # type: ignore[arg-type]
|
||||
config,
|
||||
defers_inputs=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -4356,7 +4367,7 @@ class RunnableGenerator(Runnable[Input, Output]):
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
return self._atransform_stream_with_config(
|
||||
input, self._atransform, config, **kwargs
|
||||
input, self._atransform, config, defers_inputs=True, **kwargs
|
||||
)
|
||||
|
||||
@override
|
||||
@@ -4429,6 +4440,138 @@ class RunnableLambda(Runnable[Input, Output]):
|
||||
```
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, RunnableConfig], Awaitable[Output]],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Awaitable[Output]],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], AsyncIterator[Output]],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
],
|
||||
afunc: None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, RunnableConfig], Output],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Iterator[Output]],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Runnable[Input, Output]],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, CallbackManagerForChainRun], Output],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input, CallbackManagerForChainRun, RunnableConfig], Output],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Output],
|
||||
afunc: Callable[[Input], Awaitable[Output]]
|
||||
| Callable[[Input], AsyncIterator[Output]]
|
||||
| Callable[[Input, RunnableConfig], Awaitable[Output]]
|
||||
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
|
||||
| Callable[
|
||||
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[Input], Iterator[Output]]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -28,7 +28,6 @@ from langchain_core.runnables.utils import (
|
||||
coro_with_context,
|
||||
get_unique_config_specs,
|
||||
)
|
||||
from langchain_core.utils.aiter import py_anext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.callbacks.manager import AsyncCallbackManagerForChainRun
|
||||
@@ -563,7 +562,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
|
||||
child_config,
|
||||
**kwargs,
|
||||
)
|
||||
chunk = await coro_with_context(py_anext(stream), context)
|
||||
chunk = await coro_with_context(anext(stream), context)
|
||||
except self.exceptions_to_handle as e:
|
||||
first_error = e if first_error is None else first_error
|
||||
last_error = e
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from langchain_core.runnables.utils import (
|
||||
AddableDict,
|
||||
ConfigurableFieldSpec,
|
||||
)
|
||||
from langchain_core.utils.aiter import atee, py_anext
|
||||
from langchain_core.utils.aiter import atee
|
||||
from langchain_core.utils.iter import safetee
|
||||
from langchain_core.utils.pydantic import create_model_v2
|
||||
|
||||
@@ -614,7 +614,7 @@ class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
|
||||
)
|
||||
# start map output stream
|
||||
first_map_chunk_task: asyncio.Task = asyncio.create_task(
|
||||
py_anext(map_output, None), # type: ignore[arg-type]
|
||||
anext(map_output, None),
|
||||
)
|
||||
# consume passthrough stream
|
||||
async for chunk in for_passthrough:
|
||||
@@ -753,25 +753,19 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
return AddableDict(picked)
|
||||
return None
|
||||
|
||||
def _invoke(
|
||||
self,
|
||||
value: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return self._pick(value)
|
||||
|
||||
@override
|
||||
def invoke(
|
||||
self,
|
||||
input: dict[str, Any],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
return self._call_with_config(self._invoke, input, config, **kwargs)
|
||||
) -> Any:
|
||||
return self._call_with_config(self._pick, input, config, **kwargs)
|
||||
|
||||
async def _ainvoke(
|
||||
self,
|
||||
value: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
) -> Any:
|
||||
return self._pick(value)
|
||||
|
||||
@override
|
||||
@@ -780,13 +774,13 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: dict[str, Any],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
) -> Any:
|
||||
return await self._acall_with_config(self._ainvoke, input, config, **kwargs)
|
||||
|
||||
def _transform(
|
||||
self,
|
||||
chunks: Iterator[dict[str, Any]],
|
||||
) -> Iterator[dict[str, Any]]:
|
||||
) -> Iterator[Any]:
|
||||
for chunk in chunks:
|
||||
picked = self._pick(chunk)
|
||||
if picked is not None:
|
||||
@@ -798,7 +792,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: Iterator[dict[str, Any]],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[dict[str, Any]]:
|
||||
) -> Iterator[Any]:
|
||||
yield from self._transform_stream_with_config(
|
||||
input, self._transform, config, **kwargs
|
||||
)
|
||||
@@ -806,7 +800,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
async def _atransform(
|
||||
self,
|
||||
chunks: AsyncIterator[dict[str, Any]],
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
) -> AsyncIterator[Any]:
|
||||
async for chunk in chunks:
|
||||
picked = self._pick(chunk)
|
||||
if picked is not None:
|
||||
@@ -818,7 +812,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: AsyncIterator[dict[str, Any]],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
) -> AsyncIterator[Any]:
|
||||
async for chunk in self._atransform_stream_with_config(
|
||||
input, self._atransform, config, **kwargs
|
||||
):
|
||||
@@ -830,7 +824,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: dict[str, Any],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[dict[str, Any]]:
|
||||
) -> Iterator[Any]:
|
||||
return self.transform(iter([input]), config, **kwargs)
|
||||
|
||||
@override
|
||||
@@ -839,7 +833,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
|
||||
input: dict[str, Any],
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
) -> AsyncIterator[Any]:
|
||||
async def input_aiter() -> AsyncIterator[dict[str, Any]]:
|
||||
yield input
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -496,6 +505,24 @@ class ChildTool(BaseTool):
|
||||
two-tuple corresponding to the `(content, artifact)` of a `ToolMessage`.
|
||||
"""
|
||||
|
||||
extras: dict[str, Any] | None = None
|
||||
"""Optional provider-specific extra fields for the tool.
|
||||
|
||||
This is used to pass provider-specific configuration that doesn't fit into
|
||||
standard tool fields.
|
||||
|
||||
Example:
|
||||
Anthropic-specific fields like [`cache_control`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#prompt-caching),
|
||||
[`defer_loading`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#tool-search),
|
||||
or `input_examples`.
|
||||
|
||||
```python
|
||||
@tool(extras={"defer_loading": True, "cache_control": {"type": "ephemeral"}})
|
||||
def my_tool(x: str) -> str:
|
||||
return x
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
"""Initialize the tool.
|
||||
|
||||
@@ -542,9 +569,12 @@ class ChildTool(BaseTool):
|
||||
elif self.args_schema and issubclass(self.args_schema, BaseModelV1):
|
||||
json_schema = self.args_schema.schema()
|
||||
else:
|
||||
input_schema = self.get_input_schema()
|
||||
json_schema = input_schema.model_json_schema()
|
||||
return json_schema["properties"]
|
||||
input_schema = self.tool_call_schema
|
||||
if isinstance(input_schema, dict):
|
||||
json_schema = input_schema
|
||||
else:
|
||||
json_schema = input_schema.model_json_schema()
|
||||
return cast("dict", json_schema["properties"])
|
||||
|
||||
@property
|
||||
def tool_call_schema(self) -> ArgsSchema:
|
||||
@@ -635,6 +665,7 @@ class ChildTool(BaseTool):
|
||||
TypeError: If `args_schema` is not a Pydantic `BaseModel` or dict.
|
||||
"""
|
||||
input_args = self.args_schema
|
||||
|
||||
if isinstance(tool_input, str):
|
||||
if input_args is not None:
|
||||
if isinstance(input_args, dict):
|
||||
@@ -652,6 +683,7 @@ class ChildTool(BaseTool):
|
||||
msg = f"args_schema must be a Pydantic BaseModel, got {input_args}"
|
||||
raise TypeError(msg)
|
||||
return tool_input
|
||||
|
||||
if input_args is not None:
|
||||
if isinstance(input_args, dict):
|
||||
return tool_input
|
||||
@@ -692,9 +724,30 @@ class ChildTool(BaseTool):
|
||||
f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"
|
||||
)
|
||||
raise NotImplementedError(msg)
|
||||
validated_input = {
|
||||
k: getattr(result, k) for k in result_dict if k in tool_input
|
||||
}
|
||||
|
||||
# Include fields from tool_input, plus fields with explicit defaults.
|
||||
# This applies Pydantic defaults (like Field(default=1)) while excluding
|
||||
# synthetic "args"/"kwargs" fields that Pydantic creates for *args/**kwargs.
|
||||
field_info = get_fields(input_args)
|
||||
validated_input = {}
|
||||
for k in result_dict:
|
||||
if k in tool_input:
|
||||
# Field was provided in input - include it (validated)
|
||||
validated_input[k] = getattr(result, k)
|
||||
elif k in field_info and k not in ("args", "kwargs"):
|
||||
# Check if field has an explicit default defined in the schema.
|
||||
# Exclude "args"/"kwargs" as these are synthetic fields for variadic
|
||||
# parameters that should not be passed as keyword arguments.
|
||||
fi = field_info[k]
|
||||
# Pydantic v2 uses is_required() method, v1 uses required attribute
|
||||
has_default = (
|
||||
not fi.is_required()
|
||||
if hasattr(fi, "is_required")
|
||||
else not getattr(fi, "required", True)
|
||||
)
|
||||
if has_default:
|
||||
validated_input[k] = getattr(result, k)
|
||||
|
||||
for k in self._injected_args_keys:
|
||||
if k in tool_input:
|
||||
validated_input[k] = tool_input[k]
|
||||
@@ -709,7 +762,9 @@ class ChildTool(BaseTool):
|
||||
)
|
||||
raise ValueError(msg)
|
||||
validated_input[k] = tool_call_id
|
||||
|
||||
return validated_input
|
||||
|
||||
return tool_input
|
||||
|
||||
@abstractmethod
|
||||
@@ -753,6 +808,9 @@ class ChildTool(BaseTool):
|
||||
# Start with filtered args from the constant
|
||||
filtered_keys = set[str](FILTERED_ARGS)
|
||||
|
||||
# Add injected args from function signature (e.g., ToolRuntime parameters)
|
||||
filtered_keys.update(self._injected_args_keys)
|
||||
|
||||
# If we have an args_schema, use it to identify injected args
|
||||
if self.args_schema is not None:
|
||||
try:
|
||||
@@ -760,9 +818,12 @@ class ChildTool(BaseTool):
|
||||
for field_name, field_type in annotations.items():
|
||||
if _is_injected_arg_type(field_type):
|
||||
filtered_keys.add(field_name)
|
||||
except Exception: # noqa: S110
|
||||
except Exception:
|
||||
# If we can't get annotations, just use FILTERED_ARGS
|
||||
pass
|
||||
_logger.debug(
|
||||
"Failed to get args_schema annotations for filtering.",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Filter out the injected keys from tool_input
|
||||
return {k: v for k, v in tool_input.items() if k not in filtered_keys}
|
||||
@@ -877,6 +938,7 @@ class ChildTool(BaseTool):
|
||||
name=run_name,
|
||||
run_id=run_id,
|
||||
inputs=filtered_tool_input,
|
||||
tool_call_id=tool_call_id,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -927,7 +989,7 @@ class ChildTool(BaseTool):
|
||||
error_to_raise = e
|
||||
|
||||
if error_to_raise:
|
||||
run_manager.on_tool_error(error_to_raise)
|
||||
run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
|
||||
raise error_to_raise
|
||||
output = _format_output(content, artifact, tool_call_id, self.name, status)
|
||||
run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
|
||||
@@ -1004,6 +1066,7 @@ class ChildTool(BaseTool):
|
||||
name=run_name,
|
||||
run_id=run_id,
|
||||
inputs=filtered_tool_input,
|
||||
tool_call_id=tool_call_id,
|
||||
**kwargs,
|
||||
)
|
||||
content = None
|
||||
@@ -1056,7 +1119,7 @@ class ChildTool(BaseTool):
|
||||
error_to_raise = e
|
||||
|
||||
if error_to_raise:
|
||||
await run_manager.on_tool_error(error_to_raise)
|
||||
await run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
|
||||
raise error_to_raise
|
||||
|
||||
output = _format_output(content, artifact, tool_call_id, self.name, status)
|
||||
@@ -1494,7 +1557,7 @@ def _replace_type_vars(
|
||||
_replace_type_vars(arg, generic_map, default_to_bound=default_to_bound)
|
||||
for arg in args
|
||||
)
|
||||
return _py_38_safe_origin(origin)[new_args] # type: ignore[index]
|
||||
return cast("type", _py_38_safe_origin(origin)[new_args]) # type: ignore[index]
|
||||
return type_
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Literal, get_type_hints, overload
|
||||
from typing import Any, Literal, cast, get_type_hints, overload
|
||||
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
@@ -23,6 +23,7 @@ def tool(
|
||||
response_format: Literal["content", "content_and_artifact"] = "content",
|
||||
parse_docstring: bool = False,
|
||||
error_on_invalid_docstring: bool = True,
|
||||
extras: dict[str, Any] | None = None,
|
||||
) -> Callable[[Callable | Runnable], BaseTool]: ...
|
||||
|
||||
|
||||
@@ -38,6 +39,7 @@ def tool(
|
||||
response_format: Literal["content", "content_and_artifact"] = "content",
|
||||
parse_docstring: bool = False,
|
||||
error_on_invalid_docstring: bool = True,
|
||||
extras: dict[str, Any] | None = None,
|
||||
) -> BaseTool: ...
|
||||
|
||||
|
||||
@@ -52,6 +54,7 @@ def tool(
|
||||
response_format: Literal["content", "content_and_artifact"] = "content",
|
||||
parse_docstring: bool = False,
|
||||
error_on_invalid_docstring: bool = True,
|
||||
extras: dict[str, Any] | None = None,
|
||||
) -> BaseTool: ...
|
||||
|
||||
|
||||
@@ -66,6 +69,7 @@ def tool(
|
||||
response_format: Literal["content", "content_and_artifact"] = "content",
|
||||
parse_docstring: bool = False,
|
||||
error_on_invalid_docstring: bool = True,
|
||||
extras: dict[str, Any] | None = None,
|
||||
) -> Callable[[Callable | Runnable], BaseTool]: ...
|
||||
|
||||
|
||||
@@ -80,6 +84,7 @@ def tool(
|
||||
response_format: Literal["content", "content_and_artifact"] = "content",
|
||||
parse_docstring: bool = False,
|
||||
error_on_invalid_docstring: bool = True,
|
||||
extras: dict[str, Any] | None = None,
|
||||
) -> BaseTool | Callable[[Callable | Runnable], BaseTool]:
|
||||
"""Convert Python functions and `Runnables` to LangChain tools.
|
||||
|
||||
@@ -130,6 +135,15 @@ def tool(
|
||||
parse parameter descriptions from Google Style function docstrings.
|
||||
error_on_invalid_docstring: If `parse_docstring` is provided, configure
|
||||
whether to raise `ValueError` on invalid Google Style docstrings.
|
||||
extras: Optional provider-specific extra fields for the tool.
|
||||
|
||||
Used to pass configuration that doesn't fit into standard tool fields.
|
||||
Chat models should process known extras when constructing model payloads.
|
||||
|
||||
!!! example
|
||||
|
||||
For example, Anthropic-specific fields like `cache_control`,
|
||||
`defer_loading`, or `input_examples`.
|
||||
|
||||
Raises:
|
||||
ValueError: If too many positional arguments are provided (e.g. violating the
|
||||
@@ -292,6 +306,7 @@ def tool(
|
||||
response_format=response_format,
|
||||
parse_docstring=parse_docstring,
|
||||
error_on_invalid_docstring=error_on_invalid_docstring,
|
||||
extras=extras,
|
||||
)
|
||||
# If someone doesn't want a schema applied, we must treat it as
|
||||
# a simple string->string function
|
||||
@@ -308,6 +323,7 @@ def tool(
|
||||
return_direct=return_direct,
|
||||
coroutine=coroutine,
|
||||
response_format=response_format,
|
||||
extras=extras,
|
||||
)
|
||||
|
||||
return _tool_factory
|
||||
@@ -391,7 +407,7 @@ def _get_schema_from_runnable_and_arg_types(
|
||||
)
|
||||
raise TypeError(msg) from e
|
||||
fields = {key: (key_type, Field(...)) for key, key_type in arg_types.items()}
|
||||
return create_model(name, **fields) # type: ignore[call-overload]
|
||||
return cast("type[BaseModel]", create_model(name, **fields)) # type: ignore[call-overload]
|
||||
|
||||
|
||||
def convert_runnable_to_tool(
|
||||
|
||||
@@ -2,22 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Cannot move Callbacks and Document to TYPE_CHECKING as StructuredTool's
|
||||
# func/coroutine parameter annotations are evaluated at runtime.
|
||||
from langchain_core.callbacks import Callbacks # noqa: TC001
|
||||
from langchain_core.documents import Document # noqa: TC001
|
||||
from langchain_core.prompts import (
|
||||
BasePromptTemplate,
|
||||
PromptTemplate,
|
||||
aformat_document,
|
||||
format_document,
|
||||
)
|
||||
from langchain_core.tools.simple import Tool
|
||||
from langchain_core.tools.structured import StructuredTool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.retrievers import BaseRetriever
|
||||
|
||||
|
||||
@@ -27,43 +28,6 @@ class RetrieverInput(BaseModel):
|
||||
query: str = Field(description="query to look up in retriever")
|
||||
|
||||
|
||||
def _get_relevant_documents(
|
||||
query: str,
|
||||
retriever: BaseRetriever,
|
||||
document_prompt: BasePromptTemplate,
|
||||
document_separator: str,
|
||||
callbacks: Callbacks = None,
|
||||
response_format: Literal["content", "content_and_artifact"] = "content",
|
||||
) -> str | tuple[str, list[Document]]:
|
||||
docs = retriever.invoke(query, config={"callbacks": callbacks})
|
||||
content = document_separator.join(
|
||||
format_document(doc, document_prompt) for doc in docs
|
||||
)
|
||||
if response_format == "content_and_artifact":
|
||||
return (content, docs)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
async def _aget_relevant_documents(
|
||||
query: str,
|
||||
retriever: BaseRetriever,
|
||||
document_prompt: BasePromptTemplate,
|
||||
document_separator: str,
|
||||
callbacks: Callbacks = None,
|
||||
response_format: Literal["content", "content_and_artifact"] = "content",
|
||||
) -> str | tuple[str, list[Document]]:
|
||||
docs = await retriever.ainvoke(query, config={"callbacks": callbacks})
|
||||
content = document_separator.join(
|
||||
[await aformat_document(doc, document_prompt) for doc in docs]
|
||||
)
|
||||
|
||||
if response_format == "content_and_artifact":
|
||||
return (content, docs)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def create_retriever_tool(
|
||||
retriever: BaseRetriever,
|
||||
name: str,
|
||||
@@ -72,7 +36,7 @@ def create_retriever_tool(
|
||||
document_prompt: BasePromptTemplate | None = None,
|
||||
document_separator: str = "\n\n",
|
||||
response_format: Literal["content", "content_and_artifact"] = "content",
|
||||
) -> Tool:
|
||||
) -> StructuredTool:
|
||||
r"""Create a tool to do retrieval of documents.
|
||||
|
||||
Args:
|
||||
@@ -93,22 +57,31 @@ def create_retriever_tool(
|
||||
Returns:
|
||||
Tool class to pass to an agent.
|
||||
"""
|
||||
document_prompt = document_prompt or PromptTemplate.from_template("{page_content}")
|
||||
func = partial(
|
||||
_get_relevant_documents,
|
||||
retriever=retriever,
|
||||
document_prompt=document_prompt,
|
||||
document_separator=document_separator,
|
||||
response_format=response_format,
|
||||
)
|
||||
afunc = partial(
|
||||
_aget_relevant_documents,
|
||||
retriever=retriever,
|
||||
document_prompt=document_prompt,
|
||||
document_separator=document_separator,
|
||||
response_format=response_format,
|
||||
)
|
||||
return Tool(
|
||||
document_prompt_ = document_prompt or PromptTemplate.from_template("{page_content}")
|
||||
|
||||
def func(
|
||||
query: str, callbacks: Callbacks = None
|
||||
) -> str | tuple[str, list[Document]]:
|
||||
docs = retriever.invoke(query, config={"callbacks": callbacks})
|
||||
content = document_separator.join(
|
||||
format_document(doc, document_prompt_) for doc in docs
|
||||
)
|
||||
if response_format == "content_and_artifact":
|
||||
return (content, docs)
|
||||
return content
|
||||
|
||||
async def afunc(
|
||||
query: str, callbacks: Callbacks = None
|
||||
) -> str | tuple[str, list[Document]]:
|
||||
docs = await retriever.ainvoke(query, config={"callbacks": callbacks})
|
||||
content = document_separator.join(
|
||||
[await aformat_document(doc, document_prompt_) for doc in docs]
|
||||
)
|
||||
if response_format == "content_and_artifact":
|
||||
return (content, docs)
|
||||
return content
|
||||
|
||||
return StructuredTool(
|
||||
name=name,
|
||||
description=description,
|
||||
func=func,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user