mirror of
https://github.com/hwchase17/langchain.git
synced 2026-02-18 12:34:25 +00:00
Compare commits
163 Commits
v0.3
...
mdrxy/vert
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73ab674a75 | ||
|
|
80a091b9fc | ||
|
|
2b0c1da2cb | ||
|
|
b2188aeb7b | ||
|
|
bea249beff | ||
|
|
2d29959386 | ||
|
|
bb9b802cda | ||
|
|
4ff83e92c1 | ||
|
|
5f12be149a | ||
|
|
738dc79959 | ||
|
|
9ad7f7a0cc | ||
|
|
fa0955ccb1 | ||
|
|
e02acdfe60 | ||
|
|
87e1bbf3b1 | ||
|
|
e8f76e506f | ||
|
|
c49d470e13 | ||
|
|
208e8e8f07 | ||
|
|
06301c701e | ||
|
|
b7223f45cc | ||
|
|
8ba8a5e301 | ||
|
|
16a2d9759b | ||
|
|
0e404adc60 | ||
|
|
ffaedf7cfc | ||
|
|
b6ecc0b040 | ||
|
|
4d581000ad | ||
|
|
06ddc57c7a | ||
|
|
49704ffc19 | ||
|
|
8926986483 | ||
|
|
7f757cf37d | ||
|
|
c20bd07f16 | ||
|
|
8f60946d5a | ||
|
|
790c4a8e43 | ||
|
|
b83d45bff7 | ||
|
|
ba663defc2 | ||
|
|
0633974eb0 | ||
|
|
4d39cf39ff | ||
|
|
77e52a6c9c | ||
|
|
2af1cb6ca3 | ||
|
|
54f3a6d9cf | ||
|
|
87e5be1097 | ||
|
|
fcd8fdd748 | ||
|
|
370010d195 | ||
|
|
adc941d1dc | ||
|
|
d15514d571 | ||
|
|
ee94f9567a | ||
|
|
f3f5b93be6 | ||
|
|
00565d7bf6 | ||
|
|
2f93566c87 | ||
|
|
db8c2d3bae | ||
|
|
3515a54c10 | ||
|
|
ccfdce64d3 | ||
|
|
4ca523a9d6 | ||
|
|
a4e1a54393 | ||
|
|
e7cdaad58f | ||
|
|
36f8c7335c | ||
|
|
f6cf4fcd6e | ||
|
|
cfa9973828 | ||
|
|
e75878bfa2 | ||
|
|
6081ba9184 | ||
|
|
0cc6f8bb58 | ||
|
|
6c90ba1f05 | ||
|
|
ea3b9695e6 | ||
|
|
fa4c302463 | ||
|
|
b215ed5642 | ||
|
|
c891a51608 | ||
|
|
82650ea7f1 | ||
|
|
13964efccf | ||
|
|
f247270111 | ||
|
|
8be4adccd1 | ||
|
|
b6bd507198 | ||
|
|
3e0d7512ef | ||
|
|
53ed770849 | ||
|
|
e2050e24ef | ||
|
|
59bb8bffd1 | ||
|
|
16ec9bc535 | ||
|
|
fda8a71e19 | ||
|
|
8f23bd109b | ||
|
|
ff632c1028 | ||
|
|
3f71efc93c | ||
|
|
cb3b5bf69b | ||
|
|
1c8a01ed03 | ||
|
|
25333d3b45 | ||
|
|
42830208f3 | ||
|
|
653dc77c7e | ||
|
|
f6ab75ba8b | ||
|
|
b6af0d228c | ||
|
|
5800721fd4 | ||
|
|
ebfb938a68 | ||
|
|
1a3e9e06ee | ||
|
|
c7ebbe5c8a | ||
|
|
76dfb7fbe8 | ||
|
|
f25133c523 | ||
|
|
fc7a07d6f1 | ||
|
|
e8ff6f4db6 | ||
|
|
b88115f6fc | ||
|
|
67aa37b144 | ||
|
|
9f14714367 | ||
|
|
7cc9312979 | ||
|
|
387d0f4edf | ||
|
|
207ea46813 | ||
|
|
3ef1165c0c | ||
|
|
ffee5155b4 | ||
|
|
5ef7d42bf6 | ||
|
|
750a3ffda6 | ||
|
|
8b1e25461b | ||
|
|
ced9fc270f | ||
|
|
311aa94d69 | ||
|
|
cb8598b828 | ||
|
|
fded6c6b13 | ||
|
|
544b08d610 | ||
|
|
a48ace52ad | ||
|
|
20979d525c | ||
|
|
188c0154b3 | ||
|
|
3c189f0393 | ||
|
|
8509efa6ad | ||
|
|
b1a105f85f | ||
|
|
9e54c5fa7f | ||
|
|
0b8817c900 | ||
|
|
083fbfb0d1 | ||
|
|
f98f7359d3 | ||
|
|
50b48fa1ff | ||
|
|
a54f4385f8 | ||
|
|
98e4e7d043 | ||
|
|
2cf5c52c13 | ||
|
|
bf41a75073 | ||
|
|
e15c41233d | ||
|
|
25d5db88d5 | ||
|
|
1237f94633 | ||
|
|
5c8837ea5a | ||
|
|
820e355f53 | ||
|
|
9a3ba71636 | ||
|
|
00def6da72 | ||
|
|
4f8cced3b6 | ||
|
|
365d7c414b | ||
|
|
a4874123a0 | ||
|
|
a5f92fdd9a | ||
|
|
431e6d6211 | ||
|
|
0f1afa178e | ||
|
|
830d1a207c | ||
|
|
b494a3c57b | ||
|
|
f088fac492 | ||
|
|
925ad65df9 | ||
|
|
e09d90b627 | ||
|
|
ddde1eff68 | ||
|
|
9b576440ed | ||
|
|
a80fa1b25f | ||
|
|
72b66fcca5 | ||
|
|
a47d993ddd | ||
|
|
cb4705dfc0 | ||
|
|
9a9263a2dd | ||
|
|
e4b69db4cf | ||
|
|
242881562b | ||
|
|
a2322f68ba | ||
|
|
7e9ae5df60 | ||
|
|
7a108618ae | ||
|
|
6f058e7b9b | ||
|
|
dbc5a3b718 | ||
|
|
f0f1e28473 | ||
|
|
8bd2403518 | ||
|
|
4dd9110424 | ||
|
|
174e685139 | ||
|
|
9721684501 | ||
|
|
a4e135b508 |
10
.github/scripts/check_diff.py
vendored
10
.github/scripts/check_diff.py
vendored
@@ -136,14 +136,14 @@ def _get_configs_for_single_dir(job: str, dir_: str) -> List[Dict[str, str]]:
|
||||
if job == "codspeed":
|
||||
py_versions = ["3.12"] # 3.13 is not yet supported
|
||||
elif dir_ == "libs/core":
|
||||
py_versions = ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
py_versions = ["3.10", "3.11", "3.12", "3.13"]
|
||||
# custom logic for specific directories
|
||||
|
||||
elif dir_ in PY_312_MAX_PACKAGES:
|
||||
py_versions = ["3.9", "3.12"]
|
||||
py_versions = ["3.10", "3.12"]
|
||||
|
||||
elif dir_ == "libs/langchain" and job == "extended-tests":
|
||||
py_versions = ["3.9", "3.13"]
|
||||
py_versions = ["3.10", "3.13"]
|
||||
elif dir_ == "libs/langchain_v1":
|
||||
py_versions = ["3.10", "3.13"]
|
||||
elif dir_ in {"libs/cli"}:
|
||||
@@ -151,9 +151,9 @@ def _get_configs_for_single_dir(job: str, dir_: str) -> List[Dict[str, str]]:
|
||||
|
||||
elif dir_ == ".":
|
||||
# unable to install with 3.13 because tokenizers doesn't support 3.13 yet
|
||||
py_versions = ["3.9", "3.12"]
|
||||
py_versions = ["3.10", "3.12"]
|
||||
else:
|
||||
py_versions = ["3.9", "3.13"]
|
||||
py_versions = ["3.10", "3.13"]
|
||||
|
||||
return [{"working-directory": dir_, "python-version": py_v} for py_v in py_versions]
|
||||
|
||||
|
||||
4
.github/workflows/_release.yml
vendored
4
.github/workflows/_release.yml
vendored
@@ -376,7 +376,6 @@ jobs:
|
||||
test-prior-published-packages-against-new-core:
|
||||
# Installs the new core with old partners: Installs the new unreleased core
|
||||
# alongside the previously published partner packages and runs integration tests
|
||||
if: github.ref != 'refs/heads/v0.3'
|
||||
needs:
|
||||
- build
|
||||
- release-notes
|
||||
@@ -432,7 +431,7 @@ jobs:
|
||||
git ls-remote --tags origin "langchain-${{ matrix.partner }}*" \
|
||||
| awk '{print $2}' \
|
||||
| sed 's|refs/tags/||' \
|
||||
| grep -E '==0\.3\.[0-9]+$' \
|
||||
| grep -E '[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z]+[0-9]+)?$' \
|
||||
| sort -Vr \
|
||||
| head -n 1
|
||||
)"
|
||||
@@ -465,6 +464,7 @@ jobs:
|
||||
- release-notes
|
||||
- test-pypi-publish
|
||||
- pre-release-checks
|
||||
- test-prior-published-packages-against-new-core
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# This permission is used for trusted publishing:
|
||||
|
||||
2
.github/workflows/api_doc_build.yml
vendored
2
.github/workflows/api_doc_build.yml
vendored
@@ -106,7 +106,7 @@ jobs:
|
||||
working-directory: langchain
|
||||
run: |
|
||||
# Install all partner packages in editable mode with overrides
|
||||
python -m uv pip install $(ls ./libs/partners | xargs -I {} echo "./libs/partners/{}") --overrides ./docs/vercel_overrides.txt --prerelease=allow
|
||||
python -m uv pip install $(ls ./libs/partners | xargs -I {} echo "./libs/partners/{}") --overrides ./docs/vercel_overrides.txt
|
||||
|
||||
# Install core langchain and other main packages
|
||||
python -m uv pip install libs/core libs/langchain libs/text-splitters libs/community libs/experimental libs/standard-tests
|
||||
|
||||
8
.github/workflows/scheduled_test.yml
vendored
8
.github/workflows/scheduled_test.yml
vendored
@@ -5,7 +5,7 @@
|
||||
# Runs daily. Can also be triggered manually for immediate updates.
|
||||
|
||||
name: '⏰ Scheduled Integration Tests'
|
||||
run-name: "Run Integration Tests - ${{ inputs.working-directory-force || 'all libs' }} (Python ${{ inputs.python-version-force || '3.9, 3.11' }})"
|
||||
run-name: "Run Integration Tests - ${{ inputs.working-directory-force || 'all libs' }} (Python ${{ inputs.python-version-force || '3.10, 3.13' }})"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -15,7 +15,7 @@ on:
|
||||
description: "From which folder this pipeline executes - defaults to all in matrix - example value: libs/partners/anthropic"
|
||||
python-version-force:
|
||||
type: string
|
||||
description: "Python version to use - defaults to 3.9 and 3.11 in matrix - example value: 3.9"
|
||||
description: "Python version to use - defaults to 3.10 and 3.13 in matrix - example value: 3.11"
|
||||
schedule:
|
||||
- cron: '0 13 * * *' # Runs daily at 1PM UTC (9AM EDT/6AM PDT)
|
||||
|
||||
@@ -46,9 +46,9 @@ jobs:
|
||||
PYTHON_VERSION_FORCE: ${{ github.event.inputs.python-version-force || '' }}
|
||||
run: |
|
||||
# echo "matrix=..." where matrix is a json formatted str with keys python-version and working-directory
|
||||
# python-version should default to 3.9 and 3.11, but is overridden to [PYTHON_VERSION_FORCE] if set
|
||||
# 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.9", "3.11"]'
|
||||
python_version='["3.10", "3.13"]'
|
||||
working_directory="$DEFAULT_LIBS"
|
||||
if [ -n "$PYTHON_VERSION_FORCE" ]; then
|
||||
python_version="[\"$PYTHON_VERSION_FORCE\"]"
|
||||
|
||||
@@ -34,7 +34,7 @@ Currently, the build process roughly follows these steps:
|
||||
|
||||
The docs site is served by Vercel. The Vercel deployment process copies the HTML
|
||||
files from the `langchain-api-docs-html` repository and deploys them to the live
|
||||
site. Deployments are triggered on each new commit pushed to `v0.3`.
|
||||
site. Deployments are triggered on each new commit pushed to `master`.
|
||||
|
||||
#### Build Technical Details
|
||||
|
||||
|
||||
@@ -394,21 +394,3 @@ p {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Deprecation announcement banner styling */
|
||||
.bd-header-announcement {
|
||||
background-color: #790000 !important;
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bd-header-announcement a {
|
||||
color: white !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.bd-header-announcement a:hover {
|
||||
color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ html_theme_options = {
|
||||
# # Use :html_theme.sidebar_secondary.remove: for file-wide removal
|
||||
# "secondary_sidebar_items": {"**": ["page-toc", "sourcelink"]},
|
||||
# "show_version_warning_banner": True,
|
||||
"announcement": "⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank' style='color: white; text-decoration: underline;'>Visit the new v1.0 docs</a> and new <a href='https://reference.langchain.com/python' target='_blank' style='color: white; text-decoration: underline;'>reference docs</a>",
|
||||
# "announcement": None,
|
||||
"icon_links": [
|
||||
{
|
||||
# Label for this link
|
||||
|
||||
@@ -615,17 +615,12 @@ def _build_index(dirs: List[str]) -> None:
|
||||
integrations = sorted(dir_ for dir_ in dirs if dir_ not in main_)
|
||||
doc = """# LangChain Python API Reference
|
||||
|
||||
Welcome to the LangChain v0.3 Python API reference. This is a reference for all
|
||||
Welcome to the LangChain Python API reference. This is a reference for all
|
||||
`langchain-x` packages.
|
||||
|
||||
```{danger}
|
||||
These pages refer to the the v0.3 versions of LangChain packages and integrations. To
|
||||
visit the documentation for the latest versions of LangChain, visit [https://docs.langchain.com](https://docs.langchain.com)
|
||||
and [https://reference.langchain.com/python/](https://reference.langchain.com/python/) (for references.)
|
||||
|
||||
For the legacy API reference (<v0.3) hosted on ReadTheDocs see [https://api.python.langchain.com/](https://api.python.langchain.com/).
|
||||
```
|
||||
For user guides see [https://python.langchain.com](https://python.langchain.com).
|
||||
|
||||
For the legacy API reference hosted on ReadTheDocs see [https://api.python.langchain.com/](https://api.python.langchain.com/).
|
||||
"""
|
||||
|
||||
if main_:
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
:::danger
|
||||
⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>
|
||||
:::
|
||||
|
||||
# Conceptual guide
|
||||
|
||||
This guide provides explanations of the key concepts behind the LangChain framework and AI applications more broadly.
|
||||
|
||||
@@ -3,10 +3,6 @@ sidebar_position: 0
|
||||
sidebar_class_name: hidden
|
||||
---
|
||||
|
||||
:::danger
|
||||
⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>
|
||||
:::
|
||||
|
||||
# How-to guides
|
||||
|
||||
Here you’ll find answers to "How do I….?" types of questions.
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"execution_count": null,
|
||||
"id": "1fcf7b27-1cc3-420a-b920-0420b5892e20",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -102,7 +102,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -133,7 +133,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "99d27f8f-ae78-48bc-9bf2-3cef35213ec7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -163,7 +163,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -176,7 +176,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": null,
|
||||
"id": "325fb4ca",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -198,7 +198,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -234,7 +234,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": null,
|
||||
"id": "6c1455a9-699a-4702-a7e0-7f6eaec76a21",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -284,7 +284,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -312,7 +312,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": null,
|
||||
"id": "55e1d937-3b22-4deb-b9f0-9e688f0609dc",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -342,7 +342,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -417,7 +417,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -443,7 +443,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "83593b9d-a8d3-4c99-9dac-64e0a9d397cb",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -488,13 +488,13 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())\n",
|
||||
"print(response.text)\n",
|
||||
"response.usage_metadata"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": null,
|
||||
"id": "9bbf578e-794a-4dc0-a469-78c876ccd4a3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -530,7 +530,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message, response, next_message])\n",
|
||||
"print(response.text())\n",
|
||||
"print(response.text)\n",
|
||||
"response.usage_metadata"
|
||||
]
|
||||
},
|
||||
@@ -600,7 +600,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": null,
|
||||
"id": "ae076c9b-ff8f-461d-9349-250f396c9a25",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -641,7 +641,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "5df2e558-321d-4cf7-994e-2815ac37e704",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -75,7 +75,7 @@
|
||||
"\n",
|
||||
"chain = prompt | llm\n",
|
||||
"response = chain.invoke({\"image_url\": url})\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": null,
|
||||
"id": "25e4829e-0073-49a8-9669-9f43e5778383",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -144,7 +144,7 @@
|
||||
" \"cache_type\": \"ephemeral\",\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1593,7 +1593,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": null,
|
||||
"id": "30a0af36-2327-4b1d-9ba5-e47cb72db0be",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1629,7 +1629,7 @@
|
||||
"response = llm_with_tools.invoke(\n",
|
||||
" \"There's a syntax error in my primes.py file. Can you help me fix it?\"\n",
|
||||
")\n",
|
||||
"print(response.text())\n",
|
||||
"print(response.text)\n",
|
||||
"response.tool_calls"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -243,12 +243,12 @@
|
||||
"id": "0ef05abb-9c04-4dc3-995e-f857779644d5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can filter to text using the [.text()](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.ai.AIMessage.html#langchain_core.messages.ai.AIMessage.text) method on the output:"
|
||||
"You can filter to text using the [.text](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.ai.AIMessage.html#langchain_core.messages.ai.AIMessage.text) property on the output:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": null,
|
||||
"id": "2a4e743f-ea7d-4e5a-9b12-f9992362de8b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -262,7 +262,7 @@
|
||||
],
|
||||
"source": [
|
||||
"for chunk in llm.stream(messages):\n",
|
||||
" print(chunk.text(), end=\"|\")"
|
||||
" print(chunk.text, end=\"|\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": null,
|
||||
"id": "c5fac0e9-05a4-4fc1-a3b3-e5bbb24b971b",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
@@ -286,7 +286,7 @@
|
||||
],
|
||||
"source": [
|
||||
"async for token in llm.astream(\"Hello, please explain how antibiotics work\"):\n",
|
||||
" print(token.text(), end=\"\")"
|
||||
" print(token.text, end=\"\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -814,7 +814,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "1f758726-33ef-4c04-8a54-49adb783bbb3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -860,7 +860,7 @@
|
||||
"llm_with_tools = llm.bind_tools([tool])\n",
|
||||
"\n",
|
||||
"response = llm_with_tools.invoke(\"What is deep research by OpenAI?\")\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1151,7 +1151,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": null,
|
||||
"id": "073f6010-6b0e-4db6-b2d3-7427c8dec95b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1167,7 +1167,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response_2.text()"
|
||||
"response_2.text"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1198,7 +1198,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"execution_count": null,
|
||||
"id": "b6da5bd6-a44a-4c64-970b-30da26b003d6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1214,7 +1214,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response_2.text()"
|
||||
"response_2.text"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1404,7 +1404,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": null,
|
||||
"id": "51d3e4d3-ea78-426c-9205-aecb0937fca7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1428,13 +1428,13 @@
|
||||
"messages = [{\"role\": \"user\", \"content\": first_query}]\n",
|
||||
"\n",
|
||||
"response = llm_with_tools.invoke(messages)\n",
|
||||
"response_text = response.text()\n",
|
||||
"response_text = response.text\n",
|
||||
"print(f\"{response_text[:100]}... {response_text[-100:]}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "b248bedf-2050-4c17-a90e-3a26eeb1b055",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1460,7 +1460,7 @@
|
||||
" ]\n",
|
||||
")\n",
|
||||
"second_response = llm_with_tools.invoke(messages)\n",
|
||||
"print(second_response.text())"
|
||||
"print(second_response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1482,7 +1482,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": null,
|
||||
"id": "009e541a-b372-410e-b9dd-608a8052ce09",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1502,12 +1502,12 @@
|
||||
" output_version=\"responses/v1\",\n",
|
||||
")\n",
|
||||
"response = llm.invoke(\"Hi, I'm Bob.\")\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": null,
|
||||
"id": "393a443a-4c5f-4a07-bc0e-c76e529b35e3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1524,7 +1524,7 @@
|
||||
" \"What is my name?\",\n",
|
||||
" previous_response_id=response.response_metadata[\"id\"],\n",
|
||||
")\n",
|
||||
"print(second_response.text())"
|
||||
"print(second_response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1589,7 +1589,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "8d322f3a-0732-45ab-ac95-dfd4596e0d85",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1616,7 +1616,7 @@
|
||||
"response = llm.invoke(\"What is 3^3?\")\n",
|
||||
"\n",
|
||||
"# Output\n",
|
||||
"response.text()"
|
||||
"response.text"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,10 +3,6 @@ sidebar_position: 0
|
||||
sidebar_class_name: hidden
|
||||
---
|
||||
|
||||
:::danger
|
||||
⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>
|
||||
:::
|
||||
|
||||
# Introduction
|
||||
|
||||
**LangChain** is a framework for developing applications powered by large language models (LLMs).
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"execution_count": null,
|
||||
"id": "c96c960b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -320,7 +320,7 @@
|
||||
"source": [
|
||||
"query = \"Hi!\"\n",
|
||||
"response = model.invoke([{\"role\": \"user\", \"content\": query}])\n",
|
||||
"response.text()"
|
||||
"response.text"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -351,7 +351,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"execution_count": null,
|
||||
"id": "b6a7e925",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -371,7 +371,7 @@
|
||||
"query = \"Hi!\"\n",
|
||||
"response = model_with_tools.invoke([{\"role\": \"user\", \"content\": query}])\n",
|
||||
"\n",
|
||||
"print(f\"Message content: {response.text()}\\n\")\n",
|
||||
"print(f\"Message content: {response.text}\\n\")\n",
|
||||
"print(f\"Tool calls: {response.tool_calls}\")"
|
||||
]
|
||||
},
|
||||
@@ -385,7 +385,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"execution_count": null,
|
||||
"id": "688b465d",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -403,7 +403,7 @@
|
||||
"query = \"Search for the weather in SF\"\n",
|
||||
"response = model_with_tools.invoke([{\"role\": \"user\", \"content\": query}])\n",
|
||||
"\n",
|
||||
"print(f\"Message content: {response.text()}\\n\")\n",
|
||||
"print(f\"Message content: {response.text}\\n\")\n",
|
||||
"print(f\"Tool calls: {response.tool_calls}\")"
|
||||
]
|
||||
},
|
||||
@@ -615,19 +615,12 @@
|
||||
"## Streaming tokens\n",
|
||||
"\n",
|
||||
"In addition to streaming back messages, it is also useful to stream back tokens.\n",
|
||||
"We can do this by specifying `stream_mode=\"messages\"`.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"::: note\n",
|
||||
"\n",
|
||||
"Below we use `message.text()`, which requires `langchain-core>=0.3.37`.\n",
|
||||
"\n",
|
||||
":::"
|
||||
"We can do this by specifying `stream_mode=\"messages\"`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"execution_count": null,
|
||||
"id": "63198158-380e-43a3-a2ad-d4288949c1d4",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -651,7 +644,7 @@
|
||||
"for step, metadata in agent_executor.stream(\n",
|
||||
" {\"messages\": [input_message]}, config, stream_mode=\"messages\"\n",
|
||||
"):\n",
|
||||
" if metadata[\"langgraph_node\"] == \"agent\" and (text := step.text()):\n",
|
||||
" if metadata[\"langgraph_node\"] == \"agent\" and (text := step.text):\n",
|
||||
" print(text, end=\"|\")"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
sidebar_position: 0
|
||||
sidebar_class_name: hidden
|
||||
---
|
||||
|
||||
:::danger
|
||||
⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>
|
||||
:::
|
||||
|
||||
# Tutorials
|
||||
|
||||
New to LangChain or LLM app development in general? Read this material to quickly get up and running building your first applications.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# LangChain v0.3
|
||||
|
||||
*Last updated: 09.16.2024*
|
||||
*Last updated: 09.16.24*
|
||||
|
||||
## What's changed
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ const config = {
|
||||
({
|
||||
docs: {
|
||||
editUrl:
|
||||
"https://github.com/langchain-ai/langchain/edit/v0.3/docs/",
|
||||
"https://github.com/langchain-ai/langchain/edit/master/docs/",
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
remarkPlugins: [
|
||||
[require("@docusaurus/remark-plugin-npm2yarn"), { sync: true }],
|
||||
@@ -142,8 +142,8 @@ const config = {
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
announcementBar: {
|
||||
content: "⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>",
|
||||
backgroundColor: "#790000ff",
|
||||
content: "These docs will be deprecated and no longer maintained with the release of LangChain v1.0 in October 2025. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the v1.0 alpha docs</a>",
|
||||
backgroundColor: "#FFAE42",
|
||||
},
|
||||
prism: {
|
||||
theme: {
|
||||
|
||||
@@ -16,13 +16,14 @@ fi
|
||||
|
||||
if { \
|
||||
[ "$VERCEL_ENV" == "production" ] || \
|
||||
[ "$VERCEL_GIT_COMMIT_REF" == "master" ] || \
|
||||
[ "$VERCEL_GIT_COMMIT_REF" == "v0.1" ] || \
|
||||
[ "$VERCEL_GIT_COMMIT_REF" == "v0.2" ] || \
|
||||
[ "$VERCEL_GIT_COMMIT_REF" == "v0.3rc" ]; \
|
||||
} && [ "$VERCEL_GIT_REPO_OWNER" == "langchain-ai" ]
|
||||
then
|
||||
echo "✅ Production build - proceeding with build"
|
||||
exit 1
|
||||
echo "✅ Production build - proceeding with build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ def _is_relevant_import(module: str) -> bool:
|
||||
"langchain",
|
||||
"langchain_core",
|
||||
"langchain_community",
|
||||
"langchain_experimental",
|
||||
# "langchain_experimental",
|
||||
"langchain_text_splitters",
|
||||
]
|
||||
return module.split(".")[0] in recognized_packages
|
||||
|
||||
@@ -204,7 +204,7 @@ def get_vectorstore_table():
|
||||
"similarity_search_with_score": True,
|
||||
"asearch": True,
|
||||
"Passes Standard Tests": True,
|
||||
"Multi Tenancy": True,
|
||||
"Multi Tenancy": False,
|
||||
"Local/Cloud": "Local",
|
||||
"IDs in add Documents": True,
|
||||
},
|
||||
|
||||
@@ -40,15 +40,15 @@ const FEATURE_TABLES = {
|
||||
"apiLink": "https://python.langchain.com/api_reference/mistralai/chat_models/langchain_mistralai.chat_models.ChatMistralAI.html"
|
||||
},
|
||||
{
|
||||
"name": "ChatAIMLAPI",
|
||||
"package": "langchain-aimlapi",
|
||||
"link": "aimlapi/",
|
||||
"structured_output": true,
|
||||
"tool_calling": true,
|
||||
"json_mode": true,
|
||||
"multimodal": true,
|
||||
"local": false,
|
||||
"apiLink": "https://python.langchain.com/api_reference/aimlapi/chat_models/langchain_aimlapi.chat_models.ChatAIMLAPI.html"
|
||||
"name": "ChatAIMLAPI",
|
||||
"package": "langchain-aimlapi",
|
||||
"link": "aimlapi/",
|
||||
"structured_output": true,
|
||||
"tool_calling": true,
|
||||
"json_mode": true,
|
||||
"multimodal": true,
|
||||
"local": false,
|
||||
"apiLink": "https://python.langchain.com/api_reference/aimlapi/chat_models/langchain_aimlapi.chat_models.ChatAIMLAPI.html"
|
||||
},
|
||||
{
|
||||
"name": "ChatFireworks",
|
||||
@@ -1199,7 +1199,7 @@ const FEATURE_TABLES = {
|
||||
searchWithScore: true,
|
||||
async: true,
|
||||
passesStandardTests: false,
|
||||
multiTenancy: true,
|
||||
multiTenancy: false,
|
||||
local: true,
|
||||
idsInAddDocuments: true,
|
||||
},
|
||||
@@ -1230,17 +1230,17 @@ const FEATURE_TABLES = {
|
||||
idsInAddDocuments: true,
|
||||
},
|
||||
{
|
||||
name: "PGVectorStore",
|
||||
link: "pgvectorstore",
|
||||
deleteById: true,
|
||||
filtering: true,
|
||||
searchByVector: true,
|
||||
searchWithScore: true,
|
||||
async: true,
|
||||
passesStandardTests: true,
|
||||
multiTenancy: false,
|
||||
local: true,
|
||||
idsInAddDocuments: true,
|
||||
name: "PGVectorStore",
|
||||
link: "pgvectorstore",
|
||||
deleteById: true,
|
||||
filtering: true,
|
||||
searchByVector: true,
|
||||
searchWithScore: true,
|
||||
async: true,
|
||||
passesStandardTests: true,
|
||||
multiTenancy: false,
|
||||
local: true,
|
||||
idsInAddDocuments: true,
|
||||
},
|
||||
{
|
||||
name: "PineconeVectorStore",
|
||||
|
||||
436
docs/static/llms.txt
vendored
Normal file
436
docs/static/llms.txt
vendored
Normal file
@@ -0,0 +1,436 @@
|
||||
# LangChain
|
||||
|
||||
## High level
|
||||
|
||||
[Why LangChain?](https://python.langchain.com/docs/concepts/why_langchain/): considering using LangChain, when building complex AI applications, and when needing to evaluate AI applications This page discusses the main reasons to use LangChain: standardized component interfaces, orchestration capabilities, and observability/evaluation through LangSmith
|
||||
[Architecture](https://python.langchain.com/docs/concepts/architecture/): needing an overview of the LangChain architecture, exploring the various packages and components, or deciding which parts to use for a specific application. Provides a high-level overview of the different packages that make up the LangChain framework, including langchain-core, langchain, integration packages, langchain-community, langgraph, langserve, and LangSmith.
|
||||
|
||||
## Concepts
|
||||
|
||||
[Chat Models](https://python.langchain.com/docs/concepts/chat_models/): building applications using chat models, learning about chat model interfaces and features, or interested in integrating chat models with external tools and services. Provides an overview of chat models in LangChain, including their features, integration options, interfaces, tool calling, structured outputs, multimodality, context windows, and advanced topics like rate-limiting and caching.
|
||||
[Messages](https://python.langchain.com/docs/concepts/messages/): querying LangChain's chat message format, understanding different message types, building chat applications. Messages are the unit of communication in chat models, representing input/output with roles, content, metadata. Covers SystemMessage, HumanMessage, AIMessage, AIMessageChunk, ToolMessage, RemoveMessage, and legacy FunctionMessage.
|
||||
[Chat history](https://python.langchain.com/docs/concepts/chat_history/): dealing with chat history, managing chat context, or understanding conversation patterns. Covers chat history structure, conversation patterns between user/assistant/tools, and guidelines for managing chat history to stay within context window.
|
||||
[Tools](https://python.langchain.com/docs/concepts/tools/): needing an overview of tools in LangChain, wanting to create custom tools, or learning how to pass runtime values to tools. Tools are a way to encapsulate functions with schemas that can be passed to chat models supporting tool calling. The page covers the tool interface, creating tools using the @tool decorator, configuring tool schemas, tool artifacts, special type annotations like InjectedToolArg, and toolkits.
|
||||
[tool calling](https://python.langchain.com/docs/concepts/tool_calling/): needing to understand how to enable tool calling functionality, how to create tools from functions, how to bind tools to a model that supports tool calling. The page covers the key concepts of tool calling, including tool creation using decorators, tool binding to models, tool calling by models, and tool execution. It provides an overview, recommended usage, and best practices.
|
||||
[structured outputs](https://python.langchain.com/docs/concepts/structured_outputs/): it needs to return output in a structured format, when working with databases or APIs that require structured data, or when building applications with structured responses. Covers structured output concepts like schema definition and methods like tool calling and JSON mode, as well as helper functions, to instruct models to produce structured outputs conforming to a given schema.
|
||||
[Memory](https://langchain-ai.github.io/langgraph/concepts/memory/): developing agents with memory capabilities, implementing memory management strategies, or learning about different types of memory for AI agents. Covers topics related to short-term and long-term memory for agents, techniques for managing conversation history and summarizing past conversations, different types of memory (semantic, episodic, procedural), and approaches for writing memories in the hot path or in the background.
|
||||
[Multimodality](https://python.langchain.com/docs/concepts/multimodality/): needing to understand multimodal capabilities, using chat models with multimodal inputs, or using multimodal retrieval/embeddings. Discusses ability of LangChain components like chat models, embedding models, and vector stores to handle multimodal data like text, images, audio, video. Covers current status and limitations around multimodal inputs and outputs for chat models.
|
||||
[invoke](https://python.langchain.com/docs/concepts/runnables/): learning how to use the Runnable interface, when working with custom Runnables, and when needing to configure Runnables at runtime. The page covers the Runnable interface, its methods for invocation, batching, streaming, inspecting schemas, and configuration. It explains RunnableConfig, custom Runnables, and configurable Runnables.
|
||||
[stream](https://python.langchain.com/docs/concepts/streaming/): [building applications that use streaming, building applications that need to display partial results in real-time, building applications that need to provide updates on pipeline or workflow progress] 'This page covers streaming in LangChain, including what can be streamed in LLM applications, the streaming APIs available, how to write custom data to the stream, and how LangChain automatically enables streaming for chat models in certain cases.'
|
||||
[LCEL](https://python.langchain.com/docs/concepts/lcel/): needing an overview of the LangChain Expression Language (LCEL), deciding whether to use LCEL or not, and understanding how to compose chains using LCEL primitives. Provides an overview of the LCEL, a declarative approach to building chains from existing Runnables, covering its benefits, composition primitives like RunnableSequence and RunnableParallel, the composition syntax, automatic type coercion, and guidance on when to use LCEL versus alternatives like LangGraph.
|
||||
[Document Loaders](https://python.langchain.com/docs/concepts/document_loaders/): needing to load data from various sources like files, webpages, or databases, or when handling large datasets with lazy loading. Document loaders help load data from different sources into a standardized Document object format, with options for lazy loading of large datasets.
|
||||
[Retrieval](https://python.langchain.com/docs/concepts/retrieval/): building retrieval systems, understanding query analysis, integrating with databases This page covers key concepts and techniques in retrieval systems, including query analysis (re-writing and construction), vector and lexical indexes, databases, and LangChain's unified retriever interface.
|
||||
[Text Splitters](https://python.langchain.com/docs/concepts/text_splitters/): working with long documents, handling limited model input sizes, or optimizing retrieval systems This page discusses different strategies for splitting large texts into smaller chunks, including length-based, text structure-based, document structure-based, and semantic meaning-based approaches.
|
||||
[Embedding Models](https://python.langchain.com/docs/concepts/embedding_models/): LLM should read this page when: 1) Working with text embeddings for search/retrieval 2) Comparing text similarity using embedding vectors 3) Selecting or integrating text embedding models It covers key concepts of embedding models: converting text to numerical vectors, measuring similarity between vectors, embedding models (historical context, interface, integrations), and common similarity metrics (cosine, Euclidean, dot product).
|
||||
[Vector stores](https://python.langchain.com/docs/concepts/vectorstores/): LLM should read this page when: 1) Building applications that need to index and retrieve information based on semantic similarity 2) Integrating vector databases into their application 3) Exploring advanced vector search and retrieval techniques Vector stores are specialized data stores that enable indexing and retrieving information based on vector representations (embeddings) of data, allowing semantic similarity search over unstructured data like text, images, and audio. The page covers vector store integrations, the core interface, adding/deleting documents, basic and advanced similarity search techniques, and concepts like metadata filtering.
|
||||
[Retrievers](https://python.langchain.com/docs/concepts/retrievers/): building a retrieval system, integrating different retrieval sources, or linking retrieved information to source documents. This page outlines the retriever interface in LangChain, common types of retrievers such as vector stores and search APIs, and advanced retrieval patterns like ensembling and retaining source document information.
|
||||
[Retrieval Augmented Generation (RAG)](https://python.langchain.com/docs/concepts/rag/): developing applications that incorporate retrieval and generation, building question-answering systems with external data sources, or optimizing knowledge retrieval and integration into language models. Covers the concept of Retrieval Augmented Generation (RAG), which combines retrieval systems with language models to utilize external knowledge, access up-to-date information, leverage domain-specific expertise, reduce hallucination, and integrate knowledge cost-effectively.
|
||||
[Agents](https://python.langchain.com/docs/concepts/agents/): building AI agents or systems that take high-level tasks and perform a series of actions to accomplish them, transitioning from the legacy AgentExecutor to the newer and more flexible LangGraph system. Provides an overview of agents in LangChain, the legacy AgentExecutor concept, resources for using AgentExecutor, and guidance on migrating to the preferred LangGraph architecture for building customizable agents.
|
||||
[Prompt Templates](https://python.langchain.com/docs/concepts/prompt_templates/): creating prompts for language models, formatting chat messages, slotting messages into specific locations in a prompt. This page covers different types of prompt templates (string, chat, messages placeholder) for formatting prompts for language models and chat models.
|
||||
[Output Parsers](https://python.langchain.com/docs/concepts/output_parsers/): looking for ways to extract structured data from model outputs, parsing model outputs into different formats, or handling errors in parsing. Covers various LangChain output parsers like JSON, XML, CSV, Pandas DataFrame, along with capabilities like output fixing, retrying, and using user-defined formats.
|
||||
[Few-shot prompting](https://python.langchain.com/docs/concepts/few_shot_prompting/): needing to improve model performance, when deciding how to format few-shot examples, when selecting examples for few-shot prompting The page covers generating examples, number of examples, selecting examples, and formatting examples for few-shot prompting with language models.
|
||||
[Example Selectors](https://python.langchain.com/docs/concepts/example_selectors/): selecting examples for few-shot prompting, dynamically choosing examples for prompts, or understanding different example selection techniques. The page covers example selectors, which are classes responsible for selecting and formatting examples to include as part of prompts for improved performance with few-shot learning.
|
||||
[Async programming](https://python.langchain.com/docs/concepts/async/): building asynchronous applications with LangChain, working with async runnables, or handling async API calls. Explains LangChain's asynchronous APIs, delegation to sync methods, performance considerations, compatibility with asyncio, and usage in Jupyter notebooks.
|
||||
[Callbacks](https://python.langchain.com/docs/concepts/callbacks/): [needing to log, monitor, or stream events in an LLM application] [This page covers LangChain's callback system, which allows hooking into various stages of an LLM application for logging, monitoring, streaming, and other purposes. It explains the different callback events, callback handlers, and how to pass callbacks.]
|
||||
[Tracing](https://python.langchain.com/docs/concepts/tracing/): tracing the steps of a chain/agent for debugging, understanding the chain's flow, or inspecting intermediary outputs. Discusses the concept of tracing in LangChain, including that traces contain runs which are individual steps, and that tracing provides observability into chains/agents.
|
||||
[Evaluation](https://python.langchain.com/docs/concepts/evaluation/): evaluating the performance of LLM-powered applications, creating or curating datasets, defining metrics for evaluation This page covers the concept of evaluation in LangChain, including using LangSmith to create datasets, define metrics, track results over time, and run evaluations automatically.
|
||||
[Testing](https://python.langchain.com/docs/concepts/testing/): testing LangChain components, implementing unit tests, or setting up integration tests This page explains unit tests, integration tests, and standard tests in LangChain, including code examples
|
||||
|
||||
## How-to guides
|
||||
|
||||
### Installation
|
||||
|
||||
[How to: install LangChain packages](https://python.langchain.com/docs/how_to/installation/): installing LangChain packages, learning about the LangChain ecosystem packages, installing specific ecosystem packages This page explains how to install the main LangChain package, as well as different ecosystem packages like langchain-core, langchain-community, langchain-openai, langchain-experimental, langgraph, langserve, langchain-cli, and langsmith SDK.
|
||||
[How to: use LangChain with different Pydantic versions](https://python.langchain.com/docs/how_to/pydantic_compatibility/): needing to use LangChain with different Pydantic versions, needing to install Pydantic 2 with LangChain, or avoiding using the pydantic.v1 namespace with LangChain APIs. The page explains that LangChain 0.3 uses Pydantic 2 internally and advises users to install Pydantic 2 and avoid using the pydantic.v1 namespace with LangChain APIs.
|
||||
[How to: return structured data from a model](https://python.langchain.com/docs/how_to/structured_output/): LLM should read this page when: 1) wanting to return structured data from a model, 2) building applications that require structured outputs, 3) exploring techniques for parsing model outputs into objects or schemas. This page covers methods for obtaining structured outputs from language models, including using .with_structured_output(), prompting techniques with output parsers, and handling complex schemas with few-shot examples.
|
||||
[How to: use chat models to call tools](https://python.langchain.com/docs/how_to/tool_calling/): needing to call tools from chat models, wanting to use chat models to generate structured output, or doing extraction from text using chat models. Explains how to define tool schemas as Python functions, Pydantic/TypedDict classes, or LangChain Tools; bind them to chat models; retrieve tool calls from LLM responses; and optionally parse tool calls into structured objects.
|
||||
[How to: stream runnables](https://python.langchain.com/docs/how_to/streaming/): Line 1: 'wanting to learn how to stream LLM responses, stream intermediate steps, and configure streaming events.' Line 2: 'This page covers how to use the `stream` and `astream` methods to stream final outputs, how to use `astream_events` to stream both final outputs and intermediate steps, filtering events, propagating callbacks for streaming, and working with input streams.'
|
||||
[How to: debug your LLM apps](https://python.langchain.com/docs/how_to/debugging/): debugging LLM applications, adding print statements, or logging events for tracing. Covers setting verbose mode to print important events, debug mode to print all events, and using LangSmith for visualizing event traces.
|
||||
|
||||
### Components
|
||||
|
||||
These are the core building blocks you can use when building applications.
|
||||
|
||||
#### Chat models
|
||||
|
||||
[Chat Models](https://python.langchain.com/docs/concepts/chat_models/): building applications using chat models, learning about chat model interfaces and features, or interested in integrating chat models with external tools and services. Provides an overview of chat models in LangChain, including their features, integration options, interfaces, tool calling, structured outputs, multimodality, context windows, and advanced topics like rate-limiting and caching.
|
||||
[here](https://python.langchain.com/docs/integrations/chat/): integrating chat models into an application, using chat models for conversational AI tasks, or choosing between different chat model providers. Provides an overview of chat models integrated with LangChain, including OpenAI, Anthropic, Google, and others. Covers key features like tool calling, structured output, JSON mode, local usage, and multimodal support.
|
||||
|
||||
[How to: use chat models to call tools](https://python.langchain.com/docs/how_to/tool_calling/): needing to call tools from chat models, wanting to use chat models to generate structured output, or doing extraction from text using chat models. Explains how to define tool schemas as Python functions, Pydantic/TypedDict classes, or LangChain Tools; bind them to chat models; retrieve tool calls from LLM responses; and optionally parse tool calls into structured objects.
|
||||
[How to: get models to return structured output](https://python.langchain.com/docs/how_to/structured_output/): wanting to obtain structured output from an LLM, needing to parse JSON/XML/YAML output from an LLM, or looking to use few-shot examples with structured outputs. This page covers using the `.with_structured_output()` method to obtain structured data from LLMs, prompting techniques to elicit structured outputs, and parsing structured outputs.
|
||||
[How to: cache model responses](https://python.langchain.com/docs/how_to/chat_model_caching/): needing to cache ChatModel responses for efficiency, needing to reduce API calls for cost savings, or during development. This page covers how to use an in-memory cache or a SQLite database for caching ChatModel responses, which can improve performance and reduce costs.
|
||||
[How to: get log probabilities](https://python.langchain.com/docs/how_to/logprobs/): Line 1: 'seeking to get token-level log probabilities from OpenAI chat models, when needing to understand how log probabilities are represented in LangChain' Line 2: 'Explains how to configure OpenAI chat models to return token log probabilities, and how these are included in the response metadata and streamed responses.'
|
||||
[How to: create a custom chat model class](https://python.langchain.com/docs/how_to/custom_chat_model/): creating a custom chat model class, integrating a new language model as a chat model, or implementing streaming for a chat model. This page explains how to create a custom chat model class by inheriting from BaseChatModel, and implementing methods like _generate and _stream. It covers handling inputs, messages, streaming, identifying parameters, and contributing custom chat models.
|
||||
[How to: stream a response back](https://python.langchain.com/docs/how_to/chat_streaming/): LLM should read this page when: 1) It needs to stream chat model responses token-by-token 2) It needs to understand how to use the astream() and astream_events() methods for chat models 3) It wants to see examples of streaming chat model responses synchronously and asynchronously This page explains how to stream chat model responses token-by-token using the astream() and astream_events() methods, and provides examples for synchronous and asynchronous streaming with chat models that support this feature.
|
||||
[How to: track token usage](https://python.langchain.com/docs/how_to/chat_token_usage_tracking/): tracking token usage for chat models, determining costs of using chat models, implementing token usage tracking in applications. Provides methods to track token usage from OpenAI and Anthropic chat models through AIMessage.usage_metadata, callbacks, and using LangSmith. Covers streaming token usage and aggregating usage across multiple calls.
|
||||
[How to: track response metadata across providers](https://python.langchain.com/docs/how_to/response_metadata/): needing to access metadata from model responses, wanting to get information like token usage or log probabilities, or checking safety ratings Explains how to access response metadata from various chat model providers like OpenAI, Anthropic, Vertex AI, etc. Shows code examples of retrieving metadata like token usage, log probabilities, and safety ratings.
|
||||
[How to: use chat models to call tools](https://python.langchain.com/docs/how_to/tool_calling/): needing to call tools from chat models, wanting to use chat models to generate structured output, or doing extraction from text using chat models. Explains how to define tool schemas as Python functions, Pydantic/TypedDict classes, or LangChain Tools; bind them to chat models; retrieve tool calls from LLM responses; and optionally parse tool calls into structured objects.
|
||||
[How to: stream tool calls](https://python.langchain.com/docs/how_to/tool_streaming/): Line 1: 'wanting to stream tool calls, when needing to handle partial tool call data, or when needing to accumulate tool call chunks' Line 2: 'This page explains how to stream tool calls, merge message chunks to accumulate tool call chunks, and parse tool calls from accumulated chunks, with code examples.'
|
||||
[How to: handle rate limits](https://python.langchain.com/docs/how_to/chat_model_rate_limiting/): handling rate limits from model providers, running many parallel queries to a model, benchmarking a chat model. The page explains how to initialize and use an in-memory rate limiter with chat models to limit the number of requests made per unit time.
|
||||
[How to: few shot prompt tool behavior](https://python.langchain.com/docs/how_to/tools_few_shot/): using few-shot examples to improve tool calling, demonstrating how to incorporate example queries and responses into the prompt. The page explains how to create few-shot prompts including examples of tool usage, allowing the model to learn from these demonstrations to improve its ability to correctly call tools for math operations or other tasks.
|
||||
[How to: bind model-specific formatted tools](https://python.langchain.com/docs/how_to/tools_model_specific/): binding model-specific tools, binding OpenAI tool schemas, invoking model-specific tools This page explains how to bind model-specific tool schemas directly to an LLM, with an example using the OpenAI tool schema format.
|
||||
[How to: force models to call a tool](https://python.langchain.com/docs/how_to/tool_choice/): needing to force an LLM to call a specific tool, needing to force an LLM to call at least one tool This page shows how to use the tool_choice parameter to force an LLM to call a specific tool or to call at least one tool from a set of available tools.
|
||||
[How to: work with local models](https://python.langchain.com/docs/how_to/local_llms/): [running LLMs locally on a user's device, using open-source LLMs, utilizing custom prompts with LLMs] [Overview of open-source LLMs and frameworks for running inference locally, instructions for setting up and using local LLMs (Ollama, llama.cpp, GPT4All, llamafile), guidance on formatting prompts for specific LLMs, potential use cases for local LLMs.]
|
||||
[How to: init any model in one line](https://python.langchain.com/docs/how_to/chat_models_universal_init/): initializing chat models for different model providers, creating a configurable chat model, inferring the model provider from the model name. The page explains how to initialize any LLM chat model integration in one line using the init_chat_model() helper, create a configurable chat model with default or custom parameters, and infer the model provider based on the model name.
|
||||
|
||||
#### Messages
|
||||
|
||||
[Messages](https://python.langchain.com/docs/concepts/messages/): querying LangChain's chat message format, understanding different message types, building chat applications. Messages are the unit of communication in chat models, representing input/output with roles, content, metadata. Covers SystemMessage, HumanMessage, AIMessage, AIMessageChunk, ToolMessage, RemoveMessage, and legacy FunctionMessage.
|
||||
|
||||
[How to: manage large chat history](https://python.langchain.com/docs/how_to/trim_messages/): working with long chat histories, when concerned about token limits for chat models, when implementing token management strategies. This page explains how to use the trim_messages utility to reduce the size of a chat message history to fit within token limits, covering trimming by token count or message count, and allowing customization of trimming strategies.
|
||||
[How to: filter messages](https://python.langchain.com/docs/how_to/filter_messages/): needing to filter messages by type, id, or name when working with message histories, when using chains/agents that pass message histories between components. Provides instructions and examples for filtering message lists (e.g. to only include human messages) using the filter_messages utility, including basic usage, chaining with models, and API reference.
|
||||
[How to: merge consecutive messages of the same type](https://python.langchain.com/docs/how_to/merge_message_runs/): it needs to merge consecutive messages of the same type for a particular model, when it wants to compose the merge_message_runs utility with other components in a chain, or when it needs to invoke the merge_message_runs utility imperatively. The page explains how to use the merge_message_runs utility to merge consecutive messages of the same type, provides examples of using it in chains or invoking it directly, and links to the API reference for more details.
|
||||
|
||||
#### Prompt templates
|
||||
|
||||
[Prompt Templates](https://python.langchain.com/docs/concepts/prompt_templates/): creating prompts for language models, formatting chat messages, slotting messages into specific locations in a prompt. This page covers different types of prompt templates (string, chat, messages placeholder) for formatting prompts for language models and chat models.
|
||||
|
||||
[How to: use few shot examples](https://python.langchain.com/docs/how_to/few_shot_examples/): creating few-shot prompts, using example selectors, providing examples to large language models This page explains how to use few-shot examples to provide context to language models, including creating formatters, constructing example sets, using example selectors like SemanticSimilarityExampleSelector, and creating FewShotPromptTemplates.
|
||||
[How to: use few shot examples in chat models](https://python.langchain.com/docs/how_to/few_shot_examples_chat/): LLM should read this page when: 1) wanting to provide a few-shot example to fine-tune a chat model's output, 2) needing to dynamically select examples from a larger set based on semantic similarity to the input This page covers how to provide few-shot examples to chat models using either fixed examples or dynamically selecting examples from a vectorstore based on semantic similarity to the input.
|
||||
[How to: partially format prompt templates](https://python.langchain.com/docs/how_to/prompts_partial/): needing to partially format prompt templates, wanting to pass partial strings to templates, or needing to pass functions returning strings to templates. Explains how to partially format prompt templates by passing in a subset of required values as strings or functions that return strings, to create a new template expecting only remaining values.
|
||||
[How to: compose prompts together](https://python.langchain.com/docs/how_to/prompts_composition/): needing to compose prompts from various prompt components, working with chat prompts, or using the PipelinePromptTemplate class. This page explains how to concatenate different prompt templates together to build larger prompts, covering both string prompts and chat prompts, as well as using the PipelinePromptTemplate to reuse prompt components.
|
||||
|
||||
#### Example selectors
|
||||
|
||||
[Example Selectors](https://python.langchain.com/docs/concepts/example_selectors/): selecting examples for few-shot prompting, dynamically choosing examples for prompts, or understanding different example selection techniques. The page covers example selectors, which are classes responsible for selecting and formatting examples to include as part of prompts for improved performance with few-shot learning.
|
||||
|
||||
[How to: use example selectors](https://python.langchain.com/docs/how_to/example_selectors/): needing to select example prompts for few-shot learning, when having many examples to choose from, or when creating a custom example selector. Explains how to use example selectors in LangChain to select which examples to include in a prompt, covering built-in selectors like similarity and providing a custom example selector.
|
||||
[How to: select examples by length](https://python.langchain.com/docs/how_to/example_selectors_length_based/): selecting examples for few-shot prompting, handling long examples that may exceed context window, and dynamically including the appropriate number of examples. This page explains how to use the LengthBasedExampleSelector to select examples based on their length, including fewer examples for longer inputs to avoid exceeding the context window.
|
||||
[How to: select examples by semantic similarity](https://python.langchain.com/docs/how_to/example_selectors_similarity/): selecting relevant examples for few-shot prompting, building example-based systems, finding relevant reference cases This page covers how to select examples by similarity to the input using embedding-based semantic search over a vector store.
|
||||
[How to: select examples by semantic ngram overlap](https://python.langchain.com/docs/how_to/example_selectors_ngram/): selecting relevant examples to include in few-shot prompts, determining relevancy through n-gram overlap scores, and customizing example selection thresholds. Explains how to use the NGramOverlapExampleSelector to select and order examples based on n-gram overlap with the input text, including setting thresholds and dynamically adding examples.
|
||||
[How to: select examples by maximal marginal relevance](https://python.langchain.com/docs/how_to/example_selectors_mmr/): needing to select few-shot examples optimizing for both similarity to inputs and diversity from each other, working with example-based prompting for fewshot learning. Demonstrates how to use the MaxMarginalRelevanceExampleSelector, which selects examples by maximizing relevance to inputs while also optimizing for diversity between selected examples, contrasting it with just selecting by similarity.
|
||||
[How to: select examples from LangSmith few-shot datasets](https://python.langchain.com/docs/how_to/example_selectors_langsmith/): [learning how to use LangSmith datasets for few-shot example selection, dynamically creating few-shot prompts from LangSmith data, integrating LangSmith with LangChain chains] [The page covers setting up LangSmith, querying LangSmith datasets for similar examples, and using those examples in a LangChain chain to create dynamic few-shot prompts for chat models.]
|
||||
|
||||
#### LLMs
|
||||
|
||||
[LLMs](https://python.langchain.com/docs/concepts/text_llms/): needing an overview of string-based language models, learning about legacy models in LangChain, or comparing string-based models to chat models. Covers LangChain's support for older language models that take strings as input and output, distinguishing them from newer chat models; advises using chat models where possible.
|
||||
|
||||
[How to: cache model responses](https://python.langchain.com/docs/how_to/llm_caching/): it needs to cache responses to save money and time, learn about caching in LangChain. LangChain provides an optional caching layer for LLMs to save money and time by reducing API calls for repeated requests. Examples show caching with InMemoryCache and SQLiteCache.
|
||||
[How to: create a custom LLM class](https://python.langchain.com/docs/how_to/custom_llm/): creating a custom LLM class, wrapping their own LLM provider, integrating with a new language model not yet supported by LangChain. This page explains how to create a custom LLM class by implementing the required _call and _llm_type methods, as well as optional methods like _identifying_params, _acall, _stream, and _astream. It provides an example implementation, demonstrates testing and integration with LangChain APIs, and offers guidance for contributing custom LLM integrations.
|
||||
[How to: stream a response back](https://python.langchain.com/docs/how_to/streaming_llm/): it needs to stream responses from an LLM, when it needs to work with async streaming from LLMs, when it needs to stream events from an LLM. This page shows how to stream responses token-by-token from LLMs using both sync and async methods, as well as how to stream events from LLMs asynchronously.
|
||||
[How to: track token usage](https://python.langchain.com/docs/how_to/llm_token_usage_tracking/): tracking token usage for LLM calls, managing costs for an LLM application, or calculating costs based on token counts. The page covers how to track token usage using LangSmith, OpenAI callback handlers, and handling streaming contexts; it also summarizes limitations with legacy models for streaming.
|
||||
[How to: work with local models](https://python.langchain.com/docs/how_to/local_llms/): [running LLMs locally on a user's device, using open-source LLMs, utilizing custom prompts with LLMs] [Overview of open-source LLMs and frameworks for running inference locally, instructions for setting up and using local LLMs (Ollama, llama.cpp, GPT4All, llamafile), guidance on formatting prompts for specific LLMs, potential use cases for local LLMs.]
|
||||
|
||||
#### Output parsers
|
||||
|
||||
[Output Parsers](https://python.langchain.com/docs/concepts/output_parsers/): looking for ways to extract structured data from model outputs, parsing model outputs into different formats, or handling errors in parsing. Covers various LangChain output parsers like JSON, XML, CSV, Pandas DataFrame, along with capabilities like output fixing, retrying, and using user-defined formats.
|
||||
|
||||
[How to: parse text from message objects](https://python.langchain.com/docs/how_to/output_parser_string/): needing to parse text from message objects, needing to extract text from chat model responses, or working with structured output formats. This page explains how to use the StrOutputParser to extract text from message objects, regardless of the underlying content format, such as text, multimodal data, or structured output.
|
||||
[How to: use output parsers to parse an LLM response into structured format](https://python.langchain.com/docs/how_to/output_parser_structured/): [needing to parse LLM output into structured data, needing to stream partially parsed structured outputs, using LCEL with output parsers] 'Explains how to use output parsers like PydanticOutputParser to parse LLM text responses into structured formats like Python objects, and how to integrate them with prompts, models, and LCEL streaming.'
|
||||
[How to: parse JSON output](https://python.langchain.com/docs/how_to/output_parser_json/): LLM should read this page when: 1) Prompting a language model to return JSON output 2) Parsing JSON output from a language model 3) Streaming partial JSON objects from a language model 'This page explains how to use the JsonOutputParser to specify a desired JSON schema, prompt a language model to generate output conforming to that schema, and parse the model's response as JSON. It covers using JsonOutputParser with and without Pydantic, streaming partial JSON objects, and provides code examples.'
|
||||
[How to: parse XML output](https://python.langchain.com/docs/how_to/output_parser_xml/): needing to parse XML output from a model, when outputting prompts with XML formatting instructions for models, when streaming partial XML results This page shows how to use the XMLOutputParser to parse model output in XML format, including adding XML formatting instructions to prompts and streaming partial XML output
|
||||
[How to: parse YAML output](https://python.langchain.com/docs/how_to/output_parser_yaml/): LLM should read this page when: 1) Needing to generate YAML output conforming to a specific schema 2) Incorporating YAML output into a larger prompt/chain 3) Parsing YAML output returned by an LLM 'This page explains how to use the YamlOutputParser to parse YAML output from language models, allowing the output to conform to a predefined schema. It covers setting up the parser, constructing prompts with formatting instructions, and chaining the parser with a model.'
|
||||
[How to: retry when output parsing errors occur](https://python.langchain.com/docs/how_to/output_parser_retry/): [attempting to parse and handle partial or error LLM outputs, troubleshooting output parsing failures, implementing retry logic for parsing] [Explains how to use the RetryOutputParser to handle parsing errors by reprompting the LLM, provides examples for using it with OpenAI models and chaining it with other runnables.]
|
||||
[How to: try to fix errors in output parsing](https://python.langchain.com/docs/how_to/output_parser_fixing/): needing to handle improperly formatted outputs, attempting to fix formatting issues using an LLM, or parsing outputs that do not conform to a predefined schema. Explains how to use the OutputFixingParser, which wraps another parser and attempts to fix formatting errors by consulting an LLM when the original parser fails.
|
||||
[How to: write a custom output parser class](https://python.langchain.com/docs/how_to/output_parser_custom/): Line 1: 'creating a custom output parser, implementing a custom parser by inheriting from base classes, or parsing raw model outputs' Line 2: 'Covers how to create custom output parsers using runnable lambdas/generators (recommended) or by inheriting from base parser classes like BaseOutputParser and BaseGenerationOutputParser. Includes examples for simple and more complex parsing scenarios.'
|
||||
|
||||
#### Document loaders
|
||||
|
||||
[Document Loaders](https://python.langchain.com/docs/concepts/document_loaders/): needing to load data from various sources like files, webpages, or databases, or when handling large datasets with lazy loading. Document loaders help load data from different sources into a standardized Document object format, with options for lazy loading of large datasets.
|
||||
|
||||
- [How to: load PDF files](https://python.langchain.com/docs/how_to/document_loader_pdf/)
|
||||
[How to: load web pages](https://python.langchain.com/docs/how_to/document_loader_web/): LLM should read this page when: - It needs to load and process web pages for question answering or other applications - It needs guidance on using web page content with LangChain 'The page covers how to load web pages into LangChain's Document format, including simple text extraction and advanced parsing of page structure. It demonstrates tools like WebBaseLoader and UnstructuredLoader, and shows how to perform operations like vector search over loaded web content.'
|
||||
[How to: load CSV data](https://python.langchain.com/docs/how_to/document_loader_csv/): loading CSV files into a sequence of documents, customizing CSV parsing and loading, specifying a column to identify the document source This page explains how to load CSV files into a sequence of Document objects using LangChain's CSVLoader, including customizing the parsing, specifying a source column, and loading from a string.
|
||||
[How to: load data from a directory](https://python.langchain.com/docs/how_to/document_loader_directory/): loading documents from a file system, handling various file encodings, or using custom document loaders. Shows how to load files from directories using the DirectoryLoader, handle encoding errors, use multithreading, and customize the loader class.
|
||||
[How to: load HTML data](https://python.langchain.com/docs/how_to/document_loader_html/): loading HTML documents, parsing HTML files with specialized tools, or extracting text from HTML. This page covers how to load HTML documents into LangChain Document objects using Unstructured and BeautifulSoup4, with code examples and API references provided.
|
||||
[How to: load JSON data](https://python.langchain.com/docs/how_to/document_loader_json/): loading JSON or JSON Lines data into LangChain Documents, or extracting metadata from JSON data. This page explains how to use the JSONLoader to convert JSON and JSONL data into LangChain Documents, including how to extract specific fields into the content and metadata, and provides examples for common JSON structures.
|
||||
[How to: load Markdown data](https://python.langchain.com/docs/how_to/document_loader_markdown/): needing to load Markdown files, needing to retain Markdown elements, needing to parse Markdown into components This page covers how to load Markdown files into LangChain documents, including retaining elements like titles and lists, and parsing Markdown into components.
|
||||
[How to: load Microsoft Office data](https://python.langchain.com/docs/how_to/document_loader_office_file/): loading Microsoft Office files (DOCX, XLSX, PPTX) into LangChain, when working with Azure AI Document Intelligence. It covers how to use the AzureAIDocumentIntelligenceLoader to load Office documents into LangChain Documents for further processing.
|
||||
[How to: write a custom document loader](https://python.langchain.com/docs/how_to/document_loader_custom/): Line 1: 'creating a custom document loader, working with files, or using the GenericLoader abstraction' Line 2: 'This page explains how to create a custom document loader, work with files using BaseBlobParser and Blob, and use the GenericLoader to combine a BlobLoader with a BaseBlobParser.'
|
||||
|
||||
#### Text splitters
|
||||
|
||||
[Text Splitters](https://python.langchain.com/docs/concepts/text_splitters/): working with long documents, handling limited model input sizes, or optimizing retrieval systems This page discusses different strategies for splitting large texts into smaller chunks, including length-based, text structure-based, document structure-based, and semantic meaning-based approaches.
|
||||
|
||||
[How to: recursively split text](https://python.langchain.com/docs/how_to/recursive_text_splitter/): splitting long text into smaller chunks, processing text from languages without word boundaries like Chinese or Japanese, parsing documents for downstream tasks. Covers how to recursively split text by list of characters like newlines and spaces, and options to customize characters for different languages. Discusses chunk size, overlap, and creating LangChain Document objects.
|
||||
[How to: split HTML](https://python.langchain.com/docs/how_to/split_html/): needing to split HTML content into chunks, preserving semantic structure for better context during processing Explains different techniques to split HTML pages like HTMLHeaderTextSplitter, HTMLSectionSplitter, HTMLSemanticPreservingSplitter; covers preserving tables, lists, custom handlers
|
||||
[How to: split by character](https://python.langchain.com/docs/how_to/character_text_splitter/): needing to split text by individual characters, needing to control chunk size by character count, needing to handle text with differing chunk sizes. Explains how to split text into chunks by character count, using the CharacterTextSplitter. Covers setting chunk size, overlap, and passing metadata.
|
||||
[How to: split code](https://python.langchain.com/docs/how_to/code_splitter/): needing to split code into logical chunks, working with code from specific programming languages, or creating language-specific text splitters. Provides examples of using the RecursiveCharacterTextSplitter to split code from various programming languages like Python, JavaScript, Markdown, and others into document chunks based on language-specific separators.
|
||||
[How to: split Markdown by headers](https://python.langchain.com/docs/how_to/markdown_header_metadata_splitter/): splitting markdown files into chunks, handling headers and metadata in markdown files, constraining chunk sizes in markdown files. This page covers how to split markdown files by headers into chunks, handle metadata associated with headers, and constrain chunk sizes using other text splitters like RecursiveCharacterTextSplitter.
|
||||
[How to: recursively split JSON](https://python.langchain.com/docs/how_to/recursive_json_splitter/): splitting JSON data into smaller chunks, managing chunk sizes from list content within JSON data. Explains how to split JSON data into smaller chunks while keeping nested objects intact, control chunk sizes, and handle JSON lists by converting them to dictionaries before splitting.
|
||||
[How to: split text into semantic chunks](https://python.langchain.com/docs/how_to/semantic-chunker/): building an application that needs to split long text into smaller chunks based on semantic meaning, when working with large documents that need to be broken down into semantically coherent sections, or when needing to control the granularity of text splitting. This page explains how to use the SemanticChunker from LangChain to split text into semantically coherent chunks by leveraging embedding models, with options to control the splitting behavior based on percentile, standard deviation, interquartile range, or gradient of embedding distance.
|
||||
[How to: split by tokens](https://python.langchain.com/docs/how_to/split_by_token/): LLM should read this page when: 1) Splitting long text into chunks while counting tokens 2) Handling non-English languages for text splitting 3) Comparing different tokenizers for text splitting 'The page covers how to split text into chunks based on token count using different tokenizers like tiktoken, spaCy, SentenceTransformers, NLTK, KoNLPY (for Korean), and Hugging Face tokenizers. It explains the approaches, usage, and API references for each tokenizer.'
|
||||
|
||||
#### Embedding models
|
||||
|
||||
[Embedding Models](https://python.langchain.com/docs/concepts/embedding_models/): LLM should read this page when: 1) Working with text embeddings for search/retrieval 2) Comparing text similarity using embedding vectors 3) Selecting or integrating text embedding models It covers key concepts of embedding models: converting text to numerical vectors, measuring similarity between vectors, embedding models (historical context, interface, integrations), and common similarity metrics (cosine, Euclidean, dot product).
|
||||
[supported integrations](https://python.langchain.com/docs/integrations/text_embedding/): looking for integrations with embedding models, wanting to compare embedding providers, needing guidance on selecting an embedding model This page documents integrations with various model providers that allow using embeddings in LangChain, covering OpenAI, Azure, Google, AWS, HuggingFace, and other embedding services.
|
||||
|
||||
[How to: embed text data](https://python.langchain.com/docs/how_to/embed_text/): it needs to embed text into vectors, when it needs to use text embeddings for tasks like semantic search, and when it needs to understand the interface for text embedding models. This page explains how to use LangChain's Embeddings class to interface with various text embedding model providers, embed documents and queries, and work with the resulting vector representations of text.
|
||||
[How to: cache embedding results](https://python.langchain.com/docs/how_to/caching_embeddings/): caching document embeddings to improve performance, caching query embeddings to improve performance, or choosing a data store for caching embeddings. This page covers how to use the CacheBackedEmbeddings class to cache document and query embeddings in a ByteStore, demonstrating its usage with a local file store and an in-memory store. It also explains how to specify the cache namespace to avoid collisions.
|
||||
[How to: create a custom embeddings class](https://python.langchain.com/docs/how_to/custom_embeddings/): needing to use a custom text embedding model, integrating a new text embedding provider, or contributing a new text embedding integration. The page covers implementing custom text embedding models for LangChain by following the Embeddings interface, providing examples, testing, and contributing guidelines.
|
||||
|
||||
#### Vector stores
|
||||
|
||||
[Vector stores](https://python.langchain.com/docs/concepts/vectorstores/): LLM should read this page when: 1) Building applications that need to index and retrieve information based on semantic similarity 2) Integrating vector databases into their application 3) Exploring advanced vector search and retrieval techniques Vector stores are specialized data stores that enable indexing and retrieving information based on vector representations (embeddings) of data, allowing semantic similarity search over unstructured data like text, images, and audio. The page covers vector store integrations, the core interface, adding/deleting documents, basic and advanced similarity search techniques, and concepts like metadata filtering.
|
||||
[supported integrations](https://python.langchain.com/docs/integrations/vectorstores/): Line 1: 'integrating vector stores into applications, deciding which vector store to use, or understanding the capabilities of different vector stores' Line 2: 'This page provides an overview of vector stores, which are used to store embedded data and perform similarity search. It lists the different vector stores integrated with LangChain, along with their key features and capabilities.'
|
||||
|
||||
[How to: use a vector store to retrieve data](https://python.langchain.com/docs/how_to/vectorstores/): building applications that require searching over large collections of text, when indexing and retrieving relevant information based on similarity between embeddings, and when working with vector databases and embeddings. The page covers how to create and query vector stores, which are used to store embedded vectors of text and search for similar embeddings. It explains how to initialize different vector store options like Chroma, FAISS, and LanceDB, and how to perform similarity searches on them. It also touches on asynchronous operations with vector stores.
|
||||
|
||||
#### Retrievers
|
||||
|
||||
[Retrievers](https://python.langchain.com/docs/concepts/retrievers/): building a retrieval system, integrating different retrieval sources, or linking retrieved information to source documents. This page outlines the retriever interface in LangChain, common types of retrievers such as vector stores and search APIs, and advanced retrieval patterns like ensembling and retaining source document information.
|
||||
|
||||
[How to: use a vector store to retrieve data](https://python.langchain.com/docs/how_to/vectorstore_retriever/): using vector stores for retrieval, implementing maximum marginal relevance retrieval, or specifying additional search parameters. This page explains how to create a retriever from a vector store, how to use maximum marginal relevance retrieval, and how to pass parameters like similarity score thresholds and top-k results.
|
||||
[How to: generate multiple queries to retrieve data for](https://python.langchain.com/docs/how_to/MultiQueryRetriever/): Line 1: 'improving retrieval results for search queries, retrieving documents from a vector database, or using an LLM to generate multiple queries for a given input' Line 2: 'Explains how to use MultiQueryRetriever to automatically generate multiple queries from an input question using an LLM, retrieve documents for each query, and take the unique union of results to improve retrieval performance.'
|
||||
[How to: use contextual compression to compress the data retrieved](https://python.langchain.com/docs/how_to/contextual_compression/): [it needs to retrieve relevant information from a large corpus of documents, it needs to filter out irrelevant content from retrieved documents, it needs to compress or shorten documents to focus on query-relevant content] This page discusses contextual compression, a technique that allows retrieving only relevant portions of documents given a query, using various methods like LLM-based extractors/filters, embedding similarity filters, or combinations thereof via pipelines.
|
||||
[How to: write a custom retriever class](https://python.langchain.com/docs/how_to/custom_retriever/): learning how to create a custom retriever, when implementing custom retrieval logic, when adding retrieval capabilities to an application. Explains how to implement a custom Retriever class by extending BaseRetriever, including providing examples and guidelines for contributing custom retrievers.
|
||||
[How to: add similarity scores to retriever results](https://python.langchain.com/docs/how_to/add_scores_retriever/): needing to incorporate similarity/relevance scores from retrievers, using vector or multi-vector retrievers, or propagating scores through custom retriever subclasses Shows how to add similarity scores from retrievers like Vector Store Retrievers, SelfQueryRetriever, and MultiVectorRetriever to the metadata of retrieved documents
|
||||
[How to: combine the results from multiple retrievers](https://python.langchain.com/docs/how_to/ensemble_retriever/): combining results from multiple retriever algorithms, leveraging different retrieval strengths, or using a hybrid search approach. The page explains how to use the EnsembleRetriever to combine results from sparse and dense retrievers, outlines basic usage, and demonstrates runtime configuration of individual retrievers.
|
||||
[How to: reorder retrieved results to mitigate the "lost in the middle" effect](https://python.langchain.com/docs/how_to/long_context_reorder/): looking to improve performance of RAG applications, mitigating the "lost in the middle" effect, reordering retrieved results for longer contexts. Explains how to reorder retrieved documents to position the most relevant at the beginning and end, with less relevant in the middle, helping surface important information for language models.
|
||||
[How to: generate multiple embeddings per document](https://python.langchain.com/docs/how_to/multi_vector/): needing to retrieve documents using multiple vector embeddings per document, when working with long documents that need to be split into chunks, when using document summaries for retrieval. This page covers how to index documents using 1) document chunks, 2) summaries generated with an LLM, and 3) hypothetical questions generated with an LLM. It demonstrates the usage of the MultiVectorRetriever to retrieve parent documents based on vector embeddings of chunks/summaries/questions.
|
||||
[How to: retrieve the whole document for a chunk](https://python.langchain.com/docs/how_to/parent_document_retriever/): [1) wanting to retrieve larger documents instead of just smaller chunks for context, 2) trying to balance keeping context while splitting long documents] [The page explains how to use the ParentDocumentRetriever, which first splits documents into small chunks for indexing but then retrieves the larger parent documents those chunks came from during retrieval. It shows code examples for retrieving full documents as well as larger chunks rather than full documents.]
|
||||
[How to: generate metadata filters](https://python.langchain.com/docs/how_to/self_query/): needing to perform retrieval on documents based on semantic similarity to the query text and metadata filters, integrating the retrieval into a question-answering pipeline. Covers creating a Self Query Retriever which can perform semantic text retrieval and structured metadata filtering in one step, using an underlying vector store and a query constructor LLM chain to parse natural language queries into structured representations.
|
||||
[How to: create a time-weighted retriever](https://python.langchain.com/docs/how_to/time_weighted_vectorstore/): it needs to retrieve documents from a vector store considering both semantic similarity and time decay, it needs to simulate time for testing purposes, or it needs to adjust the balance between semantic similarity and recency in retrieving documents. This page explains how to use the TimeWeightedVectorStoreRetriever, which combines semantic similarity scores from a vector store with a time decay factor that reduces the relevance of older documents over time, and provides examples of using different decay rates and mocking time for testing.
|
||||
[How to: use hybrid vector and keyword retrieval](https://python.langchain.com/docs/how_to/hybrid/): LLM should read this page when: 1) It needs to perform hybrid search combining vector and other search techniques 2) It uses a vectorstore that supports hybrid search capabilities Explains how to configure and invoke LangChain chains to leverage hybrid search features of vectorstores like Astra DB, ElasticSearch, etc.
|
||||
|
||||
#### Indexing
|
||||
|
||||
Indexing is the process of keeping your vectorstore in-sync with the underlying data source.
|
||||
|
||||
[How to: reindex data to keep your vectorstore in-sync with the underlying data source](https://python.langchain.com/docs/how_to/indexing/): needing to index documents into a vector store, handling content deduplication and document mutations over time, or cleaning up old/deleted documents from the store. Covers the LangChain indexing API workflow, including deletion modes, using document loaders, and setting source metadata for documents to handle mutations and deletions properly.
|
||||
|
||||
#### Tools
|
||||
|
||||
[Tools](https://python.langchain.com/docs/concepts/tools/): needing an overview of tools in LangChain, wanting to create custom tools, or learning how to pass runtime values to tools. Tools are a way to encapsulate functions with schemas that can be passed to chat models supporting tool calling. The page covers the tool interface, creating tools using the @tool decorator, configuring tool schemas, tool artifacts, special type annotations like InjectedToolArg, and toolkits.
|
||||
|
||||
[How to: define a custom tool](https://python.langchain.com/docs/how_to/custom_tools/): creating custom tools for agents, converting functions or runnables to tools, or subclassing BaseTool. This page covers creating tools from functions using the @tool decorator or StructuredTool class, creating tools from Runnables, subclassing BaseTool for custom tools, creating async tools, handling tool errors, and returning artifacts from tool execution.
|
||||
[How to: use built-in tools and toolkits](https://python.langchain.com/docs/how_to/tools_builtin/): needing to use built-in LangChain tools or toolkits, needing to customize built-in LangChain tools. This page covers how to use LangChain's built-in tools and toolkits, including customizing tool names, descriptions, and argument schemas. It also explains how to use LangChain toolkits, which are collections of tools for specific tasks.
|
||||
[How to: use chat models to call tools](https://python.langchain.com/docs/how_to/tool_calling/): needing to call tools from chat models, wanting to use chat models to generate structured output, or doing extraction from text using chat models. Explains how to define tool schemas as Python functions, Pydantic/TypedDict classes, or LangChain Tools; bind them to chat models; retrieve tool calls from LLM responses; and optionally parse tool calls into structured objects.
|
||||
[How to: pass tool outputs to chat models](https://python.langchain.com/docs/how_to/tool_results_pass_to_model/): 1) integrating tools with chat models, 2) implementing tool calling functionality, 3) passing tool outputs back to chat models. Demonstrates how to pass tool function outputs back to chat models as tool messages, allowing the model to incorporate tool results in generating a final response.
|
||||
[How to: pass run time values to tools](https://python.langchain.com/docs/how_to/tool_runtime/): it needs to pass runtime values to tools, when it needs to prevent an LLM from generating certain tool arguments, and when it needs to inject arguments directly at runtime. This page explains how to use the InjectedToolArg annotation to mark certain parameters of a Tool as being injected at runtime, preventing the LLM from generating those arguments. It also shows how to inject the arguments at runtime and create a tool-executing chain.
|
||||
[How to: add a human-in-the-loop for tools](https://python.langchain.com/docs/how_to/tools_human/): adding human approval to tool calling, allowing human intervention in a workflow, or setting up fail-safes for sensitive operations. This page demonstrates how to add a human-in-the-loop step to approve or reject tool calls made by an LLM in a tool-calling chain using LangChain.
|
||||
[How to: handle tool errors](https://python.langchain.com/docs/how_to/tools_error/): needing to handle errors that occur when tools are called by an LLM, when building fault tolerance into tool-calling chains, or when enabling self-correction for tool calling errors. The page covers strategies like try/except for tool calls, fallbacks to different models, retrying with exceptions passed to the LLM, and creating custom tool exceptions.
|
||||
[How to: force models to call a tool](https://python.langchain.com/docs/how_to/tool_choice/): needing to force an LLM to call a specific tool, needing to force an LLM to call at least one tool This page shows how to use the tool_choice parameter to force an LLM to call a specific tool or to call at least one tool from a set of available tools.
|
||||
[How to: disable parallel tool calling](https://python.langchain.com/docs/how_to/tool_calling_parallel/): considering disabling parallel tool calling, when looking for examples on parallel vs. single tool calls, when trying to control the number of tool calls made. Explains how to disable parallel tool calling in LangChain so that only one tool is called at a time, providing code examples.
|
||||
[How to: access the `RunnableConfig` from a tool](https://python.langchain.com/docs/how_to/tool_configure/): accessing or configuring runtime behavior of sub-runnables from a custom tool, streaming events from child runnables within a tool This page explains how to access the RunnableConfig from within a custom tool to configure sub-invocations and stream events from those sub-invocations
|
||||
[How to: stream events from a tool](https://python.langchain.com/docs/how_to/tool_stream_events/): Line 1: 'it needs to stream events from a tool, when it needs to configure tools to access internal runnables, or when it needs to propagate configurations to child runnables in async environments' Line 2: 'Guide on how to stream events from tools that call chat models, retrievers, or other runnables, by accessing internal events and propagating configurations, with examples and explanations for compatibility across Python versions'
|
||||
[How to: return artifacts from a tool](https://python.langchain.com/docs/how_to/tool_artifacts/): returning structured data from a tool, passing artifacts to downstream components, handling custom data types from tools This page explains how tools can return artifacts separate from model input, allowing custom objects, dataframes, or images to be passed to downstream components while limiting model exposure.
|
||||
[How to: convert Runnables to tools](https://python.langchain.com/docs/how_to/convert_runnable_to_tool/): Line 1: 'needing to convert a Python function or Runnable into a LangChain tool, when building an agent that calls external tools, or when integrating a custom tool into a chat model' Line 2: 'Demonstrates how to use the Runnable.as_tool() method to convert a Runnable to a tool with a name, description, and arguments schema. Includes examples of agents calling tools created from Runnables.'
|
||||
[How to: add ad-hoc tool calling capability to models](https://python.langchain.com/docs/how_to/tools_prompting/): LLM should read this page when: 1) Adding ad-hoc tool calling capability to chat models/LLMs, 2) Using models not fine-tuned for tool calling, 3) Invoking custom tools from LLMs 'This guide demonstrates how to create prompts that instruct LLMs to request tool invocations, parse the LLM output to extract tool and arguments, invoke the requested tool, and return the tool output.'
|
||||
[How to: pass runtime secrets to a runnable](https://python.langchain.com/docs/how_to/runnable_runtime_secrets/): needing to pass sensitive data to a runnable, ensuring secrets remain hidden from tracing, or integrating secret values with runnables. Explains how to pass runtime secrets to runnables using RunnableConfig, allowing certain keys to be hidden from tracing while still being accessible during invocation.
|
||||
|
||||
#### Multimodal
|
||||
|
||||
[How to: pass multimodal data directly to models](https://python.langchain.com/docs/how_to/multimodal_inputs/): needing to pass multimodal data (images, videos, etc.) to models, when working with models that support multimodal input and tool calling capabilities, and when looking to understand how to encode and pass different types of multimodal data. This page demonstrates how to pass multimodal input like images directly to LLMs and chat models, covering encoding techniques, passing single/multiple images, and invoking models with image/multimodal content. It also shows how to use multimodal models for tool calling.
|
||||
[How to: use multimodal prompts](https://python.langchain.com/docs/how_to/multimodal_prompts/): wanting to pass multimodal data like images to an LLM, when wanting to send multiple pieces of multimodal data to an LLM, when wanting instructions on how to format multimodal prompts. This shows how to use prompt templates to format multimodal inputs like images to models that support it, including sending multiple images, and comparing images.
|
||||
|
||||
#### Agents
|
||||
|
||||
:::note
|
||||
|
||||
[LangGraph](https://langchain-ai.github.io/langgraph/): learning about LangGraph, considering using LangGraph for an AI application, or deciding between LangGraph and alternatives. Overview of LangGraph as an open-source framework for building AI agents, its key features like reliability and customizability, its ecosystem integration with other LangChain products, and additional learning resources.
|
||||
|
||||
:::
|
||||
|
||||
[How to: use legacy LangChain Agents (AgentExecutor)](https://python.langchain.com/docs/how_to/agent_executor/): building agents with specific tools, when working with chat history, when using language models for tool calling. This page explains how to build agents with AgentExecutor that can call tools like search engines and retrievers, how to add chat history to agents, and how to use language models to determine which tools to call.
|
||||
[How to: migrate from legacy LangChain agents to LangGraph](https://python.langchain.com/docs/how_to/migrate_agent/): LLM should read this page when: 1) Migrating from legacy LangChain agents to LangGraph 2) Comparing the functionality of LangChain and LangGraph agents This page provides a detailed guide on migrating from legacy LangChain agents to LangGraph agents, covering topics such as basic usage, prompt templates, memory handling, iterating through steps, dealing with intermediate steps, setting iteration and execution time limits, early stopping methods, and trimming intermediate steps.
|
||||
|
||||
#### Callbacks
|
||||
|
||||
[Callbacks](https://python.langchain.com/docs/concepts/callbacks/): [needing to log, monitor, or stream events in an LLM application] [This page covers LangChain's callback system, which allows hooking into various stages of an LLM application for logging, monitoring, streaming, and other purposes. It explains the different callback events, callback handlers, and how to pass callbacks.]
|
||||
|
||||
[How to: pass in callbacks at runtime](https://python.langchain.com/docs/how_to/callbacks_runtime/): needing to pass callback handlers at runtime to capture events, needing to attach handlers to nested objects This page explains how to pass callback handlers at runtime when invoking a runnable, which allows capturing events from all nested objects without manually attaching handlers.
|
||||
[How to: attach callbacks to a module](https://python.langchain.com/docs/how_to/callbacks_attach/): attaching callbacks to a runnable, reusing callbacks across multiple executions, composing a chain of runnables This page explains how to attach callbacks to a runnable using the .with_config() method, allowing callbacks to be reused across multiple executions and propagated to child components in a chain of runnables.
|
||||
[How to: pass callbacks into a module constructor](https://python.langchain.com/docs/how_to/callbacks_constructor/): LLM should read this page when: 1) Implementing callbacks in LangChain, 2) Understanding the scope of constructor callbacks, 3) Deciding whether to use constructor or runtime callbacks 'This page explains how to pass callbacks into the constructor of LangChain objects, and that constructor callbacks are scoped only to the object they are defined on, not inherited by child objects.'
|
||||
[How to: create custom callback handlers](https://python.langchain.com/docs/how_to/custom_callbacks/): creating custom behavior for LangChain components, customizing callback events, implementing event handlers This page explains how to create custom callback handlers by implementing callback methods and attaching the handler to LangChain components
|
||||
[How to: use callbacks in async environments](https://python.langchain.com/docs/how_to/callbacks_async/): needing to use callbacks in async environments, handling sync callbacks in async methods, using AsyncCallbackHandler Covers using callbacks with async APIs, avoiding blocking with AsyncCallbackHandler, propagating callbacks in async runnables, example of sync and async callback handlers
|
||||
[How to: dispatch custom callback events](https://python.langchain.com/docs/how_to/callbacks_custom_events/): dispatching custom callback events, handling async or sync custom callback events, or consuming custom events via the astream events API. This page covers how to dispatch custom callback events from within a Runnable, consume these events via async/sync callback handlers, and access custom events through the astream events API.
|
||||
|
||||
#### Custom
|
||||
|
||||
All of LangChain components can easily be extended to support your own versions.
|
||||
|
||||
[How to: create a custom chat model class](https://python.langchain.com/docs/how_to/custom_chat_model/): creating a custom chat model class, integrating a new language model as a chat model, or implementing streaming for a chat model. This page explains how to create a custom chat model class by inheriting from BaseChatModel, and implementing methods like _generate and _stream. It covers handling inputs, messages, streaming, identifying parameters, and contributing custom chat models.
|
||||
[How to: create a custom LLM class](https://python.langchain.com/docs/how_to/custom_llm/): creating a custom LLM class, wrapping their own LLM provider, integrating with a new language model not yet supported by LangChain. This page explains how to create a custom LLM class by implementing the required _call and _llm_type methods, as well as optional methods like _identifying_params, _acall, _stream, and _astream. It provides an example implementation, demonstrates testing and integration with LangChain APIs, and offers guidance for contributing custom LLM integrations.
|
||||
[How to: create a custom embeddings class](https://python.langchain.com/docs/how_to/custom_embeddings/): needing to use a custom text embedding model, integrating a new text embedding provider, or contributing a new text embedding integration. The page covers implementing custom text embedding models for LangChain by following the Embeddings interface, providing examples, testing, and contributing guidelines.
|
||||
[How to: write a custom retriever class](https://python.langchain.com/docs/how_to/custom_retriever/): learning how to create a custom retriever, when implementing custom retrieval logic, when adding retrieval capabilities to an application. Explains how to implement a custom Retriever class by extending BaseRetriever, including providing examples and guidelines for contributing custom retrievers.
|
||||
[How to: write a custom document loader](https://python.langchain.com/docs/how_to/document_loader_custom/): Line 1: 'creating a custom document loader, working with files, or using the GenericLoader abstraction' Line 2: 'This page explains how to create a custom document loader, work with files using BaseBlobParser and Blob, and use the GenericLoader to combine a BlobLoader with a BaseBlobParser.'
|
||||
[How to: write a custom output parser class](https://python.langchain.com/docs/how_to/output_parser_custom/): Line 1: 'creating a custom output parser, implementing a custom parser by inheriting from base classes, or parsing raw model outputs' Line 2: 'Covers how to create custom output parsers using runnable lambdas/generators (recommended) or by inheriting from base parser classes like BaseOutputParser and BaseGenerationOutputParser. Includes examples for simple and more complex parsing scenarios.'
|
||||
[How to: create custom callback handlers](https://python.langchain.com/docs/how_to/custom_callbacks/): creating custom behavior for LangChain components, customizing callback events, implementing event handlers This page explains how to create custom callback handlers by implementing callback methods and attaching the handler to LangChain components
|
||||
[How to: define a custom tool](https://python.langchain.com/docs/how_to/custom_tools/): creating custom tools for agents, converting functions or runnables to tools, or subclassing BaseTool. This page covers creating tools from functions using the @tool decorator or StructuredTool class, creating tools from Runnables, subclassing BaseTool for custom tools, creating async tools, handling tool errors, and returning artifacts from tool execution.
|
||||
[How to: dispatch custom callback events](https://python.langchain.com/docs/how_to/callbacks_custom_events/): dispatching custom callback events, handling async or sync custom callback events, or consuming custom events via the astream events API. This page covers how to dispatch custom callback events from within a Runnable, consume these events via async/sync callback handlers, and access custom events through the astream events API.
|
||||
|
||||
#### Serialization
|
||||
|
||||
[How to: save and load LangChain objects](https://python.langchain.com/docs/how_to/serialization/): needing to save and reload LangChain objects, handle API keys securely when serializing/deserializing objects, and maintain compatibility when deserializing objects across different versions of LangChain. This page discusses how to save and load serializable LangChain objects like chains, messages, and documents using the dump/load functions, which separate API keys and ensure cross-version compatibility. Examples are provided for serializing/deserializing to JSON strings, Python dicts, and disk files.
|
||||
|
||||
## Use cases
|
||||
|
||||
These guides cover use-case specific details.
|
||||
|
||||
### Q&A with RAG
|
||||
|
||||
Retrieval Augmented Generation (RAG) is a way to connect LLMs to external sources of data.
|
||||
[this guide](https://python.langchain.com/docs/tutorials/rag/): building a retrieval-augmented question-answering system, when needing to index and search through unstructured data sources, when learning about key concepts like document loaders, text splitters, vector stores, and retrievers. This tutorial covers how to build a Q&A application over textual data by loading documents, splitting them into chunks, embedding and storing the chunks in a vector store, retrieving relevant chunks for a user query, and generating an answer using a language model with the retrieved context.
|
||||
|
||||
[How to: add chat history](https://python.langchain.com/docs/how_to/qa_chat_history_how_to/): building a conversational question-answering application, incorporating chat history and retrieval from external knowledge sources, and deciding between using chains or agents for the application logic. Discusses building chat applications with LangChain by using chains for predictable retrieval steps or agents for more dynamic reasoning. Covers setting up components like embeddings and vector stores, constructing chains with tool calls for retrieval, and assembling LangGraph agents with a ReAct executor. Provides examples for testing the applications.
|
||||
[How to: stream](https://python.langchain.com/docs/how_to/qa_streaming/): LLM should read this page when: 1) Building a RAG (Retrieval Augmented Generation) application that requires streaming final outputs or intermediate steps 2) Integrating streaming capabilities into an existing LLM-based application 'The page provides guidance on how to stream final outputs and intermediate steps from a RAG (Retrieval Augmented Generation) application built with LangChain and LangGraph. It covers setting up the necessary components, constructing the RAG application, and utilizing different streaming modes to stream tokens from the final output or individual state updates from each step.'
|
||||
[How to: return sources](https://python.langchain.com/docs/how_to/qa_sources/): LLM should read this page when: 1) Building a question-answering (QA) application that needs to return the sources used to generate the answer. 2) Implementing a conversational QA system with retrieval-augmented generation (RAG). 3) Structuring model outputs to include sources or citations. 'This guide explains how to configure LangChain's QA and RAG workflows to retrieve and return the source documents or citations used to generate the final answer. It covers both basic RAG and conversational RAG architectures, and demonstrates techniques for structuring the model output to include source information.'
|
||||
[How to: return citations](https://python.langchain.com/docs/how_to/qa_citations/): seeking to add citations to results from a Retrieval Augmented Generation (RAG) application, when wanting to justify an answer using source material, and when needing to provide evidence for generated outputs. The page covers various methods for getting a RAG application to cite sources used in generating answers, including tool-calling to return source IDs or text snippets, direct prompting to generate structured outputs with citations, retrieving and compressing context to minimize need for citations, and post-processing generated answers to annotate with citations.
|
||||
[How to: do per-user retrieval](https://python.langchain.com/docs/how_to/qa_per_user/): needing to configure retrieval chains for per-user data access, wanting to limit document access for different users, or building retrieval applications with multi-tenant architectures. Explains how to configure retriever search kwargs to limit retrieved documents based on user, demonstrates code example using Pinecone namespace for multi-tenancy.
|
||||
|
||||
|
||||
### Extraction
|
||||
|
||||
Extraction is when you use LLMs to extract structured information from unstructured text.
|
||||
[this guide](https://python.langchain.com/docs/tutorials/extraction/): building information extraction applications, understanding how to use reference examples for improving extraction performance, or when needing to extract structured data from unstructured text. This tutorial covers building an information extraction chain using LangChain, defining schemas for extracting structured data, using reference examples to improve extraction quality, and extracting multiple entities from text.
|
||||
|
||||
[How to: use reference examples](https://python.langchain.com/docs/how_to/extraction_examples/): wanting to use reference examples to improve extraction quality, wanting to structure example inputs and outputs for extraction, wanting to test an extraction model with and without examples. This page explains how to define reference examples in the format expected for the LangChain tool calling API, how to incorporate these examples into prompts, and how using examples can improve extraction performance compared to not using examples.
|
||||
[How to: handle long text](https://python.langchain.com/docs/how_to/extraction_long_text/): working with large documents or PDFs that exceed the context window of the LLM, when needing to extract structured information from text. This page covers strategies for handling long text when doing information extraction, including a brute force approach of chunking the text and extracting from each chunk, and a retrieval-augmented generation (RAG) approach of indexing the chunks and only extracting from relevant ones. It also discusses common issues with these approaches.
|
||||
[How to: do extraction without using function calling](https://python.langchain.com/docs/how_to/extraction_parse/): looking to extract structured data from text, when needing to parse model outputs into objects, or when wanting to avoid using tool calling methods for extraction tasks. This page explains how to use prompting instructions to get LLMs to generate outputs in a structured format like JSON, and then use output parsers to convert the model responses into Python objects.
|
||||
|
||||
### Chatbots
|
||||
|
||||
Chatbots involve using an LLM to have a conversation.
|
||||
[this guide](https://python.langchain.com/docs/tutorials/chatbot/): building a chatbot application, incorporating conversational history, or using prompt templates. This page demonstrates how to build a chatbot with LangChain, including adding message persistence, prompt templates, conversation history management, and response streaming.
|
||||
|
||||
[How to: manage memory](https://python.langchain.com/docs/how_to/chatbots_memory/): LLM should read this page when: 1) Building a chatbot and wants to incorporate memory (chat history) 2) Looking to add context from previous messages to improve responses 3) Needs techniques to handle long conversations by summarizing or trimming history 'The page covers different techniques to add memory capabilities to chatbots, including passing previous messages directly, automatic history management using LangGraph persistence, trimming messages to reduce context, and generating summaries of conversations. Examples in Python are provided for each approach.'
|
||||
[How to: do retrieval](https://python.langchain.com/docs/how_to/chatbots_retrieval/): building a retrieval-augmented chatbot, adding conversational context to retrieval queries, or streaming responses from a chatbot. This page covers setting up a retriever over a document corpus, creating document chains and retrieval chains, transforming queries for better retrieval, and streaming responses from the retrieval chain.
|
||||
[How to: use tools](https://python.langchain.com/docs/how_to/chatbots_tools/): looking to integrate tools into chatbots, when using agents with tools, when incorporating web search into conversational agents. The page covers how to create a conversational agent using LangChain that can interact with APIs and web search tools, while maintaining chat history. It demonstrates setting up a ReAct agent with a Tavily search tool, invoking the agent, handling conversational responses with chat history, and adding memory.
|
||||
[How to: manage large chat history](https://python.langchain.com/docs/how_to/trim_messages/): working with long chat histories, when concerned about token limits for chat models, when implementing token management strategies. This page explains how to use the trim_messages utility to reduce the size of a chat message history to fit within token limits, covering trimming by token count or message count, and allowing customization of trimming strategies.
|
||||
|
||||
### Query analysis
|
||||
|
||||
Query Analysis is the task of using an LLM to generate a query to send to a retriever.
|
||||
[this guide](https://python.langchain.com/docs/tutorials/rag/#query-analysis): LLM should read this page when: 1) Building a question-answering application over unstructured data 2) Learning about Retrieval Augmented Generation (RAG) architectures 3) Indexing data for use with LLMs 'This tutorial covers building a Retrieval Augmented Generation (RAG) application that can answer questions based on ingested data. It walks through loading data, chunking it, embedding and storing it in a vector store, retrieving relevant chunks for a given query, and generating an answer using an LLM. It also shows how to incorporate query analysis for improved retrieval.'
|
||||
|
||||
[How to: add examples to the prompt](https://python.langchain.com/docs/how_to/query_few_shot/): needing to guide an LLM to generate queries, when fine-tuning an LLM for query generation, when incorporating examples into few-shot prompts. This page covers how to add examples to prompts for query analysis in LangChain, including setting up the system, defining the query schema, generating queries, and tuning prompts by adding examples.
|
||||
[How to: handle cases where no queries are generated](https://python.langchain.com/docs/how_to/query_no_queries/): querying for information, handling cases where no queries are generated, integrating query analysis with retrieval. Provides guidance on handling scenarios where query analysis techniques allow for no queries to be generated, including code examples for structuring the output, performing query analysis with an LLM, and integrating query analysis with a retriever in a chain.
|
||||
[How to: handle multiple queries](https://python.langchain.com/docs/how_to/query_multiple_queries/): handling queries that generate multiple potential queries, combining retrieval results from multiple queries, and integrating query analysis with retrieval pipelines. Explains how to handle scenarios where a query analysis step produces multiple potential queries by running retrievals for each query and combining the results. Demonstrates this approach with code examples using LangChain components.
|
||||
[How to: handle multiple retrievers](https://python.langchain.com/docs/how_to/query_multiple_retrievers/): needing to handle multiple retrievers for query analysis, when implementing a query analyzer that can select between different retrievers, when building a retrieval-augmented system that needs to choose between different data sources. This page explains how to handle scenarios where a query analysis step allows for selecting between multiple retrievers, showing an example implementation using LangChain's tools for structured output parsing, prompting, and chaining components together.
|
||||
[How to: construct filters](https://python.langchain.com/docs/how_to/query_constructing_filters/): constructing filters for query analysis, translating filters to specific retriever formats, using LangChain's structured query objects. This page covers how to construct filters as Pydantic models and translate them into retriever-specific filters using LangChain's translators for Chroma and Elasticsearch.
|
||||
[How to: deal with high cardinality categorical variables](https://python.langchain.com/docs/how_to/query_high_cardinality/): dealing with categorical data with high cardinality, handling potential misspellings of categorical values, and filtering based on categorical values. The page discusses techniques for handling high-cardinality categorical data in query analysis, such as adding all possible values to the prompt, using a vector store to find relevant values, and correcting user input to the closest valid categorical value.
|
||||
|
||||
### Q&A over SQL + CSV
|
||||
|
||||
You can use LLMs to do question answering over tabular data.
|
||||
[this guide](https://python.langchain.com/docs/tutorials/sql_qa/): LLM should read this page when: 1. Building a question-answering system over a SQL database 2. Implementing agents or chains to interact with a SQL database 'This tutorial covers building question-answering systems over SQL databases using LangChain. It demonstrates creating chains and agents that can generate SQL queries from natural language, execute them against a database, and provide natural language responses. It covers techniques like schema exploration, query validation, and handling high-cardinality columns.'
|
||||
|
||||
[How to: use prompting to improve results](https://python.langchain.com/docs/how_to/sql_prompting/): 'querying SQL databases with a language model, when doing few-shot prompting for SQL queries, and when selecting relevant few-shot examples dynamically.' 'This page covers how to improve SQL query generation prompts by incorporating database schema information, providing few-shot examples, and dynamically selecting the most relevant few-shot examples using semantic similarity.'
|
||||
[How to: do query validation](https://python.langchain.com/docs/how_to/sql_query_checking/): Line 1: 'working on SQL query generation, handling invalid SQL queries, or incorporating human approval for SQL queries' Line 2: 'This page covers strategies for validating SQL queries, such as appending a query validator step, prompt engineering, human-in-the-loop approval, and error handling.'
|
||||
[How to: deal with large databases](https://python.langchain.com/docs/how_to/sql_large_db/): dealing with large databases in SQL question-answering, identifying relevant table schemas to include in prompts, and handling high-cardinality columns with proper nouns or other unique values. The page discusses methods to identify relevant tables and table schemas to include in prompts when dealing with large databases. It also covers techniques to handle high-cardinality columns containing proper nouns or other unique values, such as creating a vector store of distinct values and querying it to include relevant spellings in prompts.
|
||||
[How to: deal with CSV files](https://python.langchain.com/docs/how_to/sql_csv/): needing to build question-answering systems over CSV data, wanting to understand the tradeoffs between using SQL or Python libraries like Pandas, and requiring guidance on securely executing code from language models. This page covers two main approaches to question answering over CSV data: using SQL by loading CSVs into a database, or giving an LLM access to Python environments to interact with CSV data using libraries like Pandas. It discusses the security implications of each approach and provides code examples for implementing question-answering chains and agents with both methods.
|
||||
|
||||
### Q&A over graph databases
|
||||
|
||||
You can use an LLM to do question answering over graph databases.
|
||||
[this guide](https://python.langchain.com/docs/tutorials/graph/): LLM should read this page when: 1) Building a question-answering system over a graph database 2) Implementing text-to-query generation for graph databases 3) Learning techniques for query validation and error handling 'This page covers building a question-answering application over a graph database using LangChain. It provides a basic implementation using the GraphQACypherChain, followed by an advanced implementation with LangGraph. The latter includes techniques like few-shot prompting, query validation, and error handling for generating accurate Cypher queries from natural language.'
|
||||
|
||||
[How to: add a semantic layer over the database](https://python.langchain.com/docs/how_to/graph_semantic/): needing to add a semantic layer over a graph database, needing to use tools representing Cypher templates with an LLM, or needing to build a LangGraph Agent to interact with a Neo4j database. This page covers how to create custom tools with Cypher templates for a Neo4j graph database, bind those tools to an LLM, and build a LangGraph Agent that can invoke the tools to retrieve information from the graph database.
|
||||
[How to: construct knowledge graphs](https://python.langchain.com/docs/how_to/graph_constructing/): constructing knowledge graphs from unstructured text, storing information in a graph database, using LLM Graph Transformer to extract knowledge from text. This page explains how to set up a Neo4j graph database, use LLMGraphTransformer to extract structured knowledge graph data from text, filter extracted nodes/relationships, and store the knowledge graph in Neo4j.
|
||||
|
||||
### Summarization
|
||||
|
||||
LLMs can summarize and otherwise distill desired information from text, including
|
||||
[this guide](https://python.langchain.com/docs/tutorials/summarization/): needing to summarize long texts or documents, when building question-answering systems, when creating text analysis applications. This page covers summarizing texts using LangChain, including the "stuff" method (concatenating into single prompt), the "map-reduce" method (splitting into chunks for parallel summarization), and orchestrating these methods using LangGraph.
|
||||
|
||||
[How to: summarize text in a single LLM call](https://python.langchain.com/docs/how_to/summarize_stuff/): looking to summarize text, seeking a simple single-LLM summarization method, or exploring basic summarization chains in LangChain. This page outlines how to use LangChain's pre-built 'stuff' summarization chain, which stuffs text into a prompt for an LLM to summarize in a single call.
|
||||
[How to: summarize text through parallelization](https://python.langchain.com/docs/how_to/summarize_map_reduce/): needing to summarize long text documents using parallelization, needing to optimize summarization for large volumes of text, and needing efficient summarization strategies. This page discusses using a map-reduce strategy to summarize text through parallelization, including breaking the text into subdocuments, generating summaries for each in parallel (map step), and then consolidating the summaries into a final summary (reduce step). It provides code examples using LangChain and LangGraph.
|
||||
[How to: summarize text through iterative refinement](https://python.langchain.com/docs/how_to/summarize_refine/): LLM should read this page when: 1. Attempting to summarize long texts through iterative refinement 2. Learning about building applications with LangGraph 3. Seeking examples of streaming LLM outputs 'This guide demonstrates how to summarize text through iterative refinement using LangGraph. It involves splitting the text into documents, summarizing the first document, and then refining the summary based on subsequent documents until finished. The approach leverages LangGraph's streaming capabilities and modularity.'
|
||||
|
||||
## LangChain Expression Language (LCEL)
|
||||
|
||||
[LCEL](https://python.langchain.com/docs/concepts/lcel/): needing an overview of the LangChain Expression Language (LCEL), deciding whether to use LCEL or not, and understanding how to compose chains using LCEL primitives. Provides an overview of the LCEL, a declarative approach to building chains from existing Runnables, covering its benefits, composition primitives like RunnableSequence and RunnableParallel, the composition syntax, automatic type coercion, and guidance on when to use LCEL versus alternatives like LangGraph.
|
||||
|
||||
[**LCEL cheatsheet**](https://python.langchain.com/docs/how_to/lcel_cheatsheet/): 'needing a reference for interacting with Runnables in LangChain or building custom runnables and chains' 'This page provides a comprehensive cheatsheet with examples for key operations with Runnables such as invoking, batching, streaming, composing, configuring, and dynamically building runnables and chains'
|
||||
|
||||
[**Migration guide**](https://python.langchain.com/docs/versions/migrating_chains/): migrating older chains from LangChain v0.0, reimplementing legacy chains, or upgrading to use LCEL and LangGraph This page provides guidance on migrating from deprecated v0.0 chain implementations to using LCEL and LangGraph, including specific guides for various legacy chains like LLMChain, ConversationChain, RetrievalQA, and others.
|
||||
|
||||
[How to: chain runnables](https://python.langchain.com/docs/how_to/sequence/): chaining multiple LangChain components together, composing prompt templates with models, or combining runnables in a sequence. This page explains how to chain runnables (LangChain components) together using the pipe operator '|' or the .pipe() method, including chaining prompt templates with models and parsers, and how input/output formats are coerced during chaining.
|
||||
[How to: stream runnables](https://python.langchain.com/docs/how_to/streaming/): Line 1: 'wanting to learn how to stream LLM responses, stream intermediate steps, and configure streaming events.' Line 2: 'This page covers how to use the `stream` and `astream` methods to stream final outputs, how to use `astream_events` to stream both final outputs and intermediate steps, filtering events, propagating callbacks for streaming, and working with input streams.'
|
||||
[How to: invoke runnables in parallel](https://python.langchain.com/docs/how_to/parallel/): parallelizing steps in a chain, formatting data for chaining, or splitting inputs to run multiple runnables in parallel. Explains how to use RunnableParallel to execute runnables concurrently, format data between steps, and provides examples of parallelizing chains.
|
||||
[How to: add default invocation args to runnables](https://python.langchain.com/docs/how_to/binding/): LLM should read this page when: 1) Wanting to invoke a Runnable with constant arguments not part of the preceding output or user input 2) Needing to bind provider-specific arguments like stop sequences or tools 'This page explains how to use the Runnable.bind() method to set default invocation arguments for a Runnable within a RunnableSequence. It covers binding stop sequences to language models and attaching OpenAI tools.'
|
||||
[How to: turn any function into a runnable](https://python.langchain.com/docs/how_to/functions/): needing to use custom functions, needing to implement streaming, needing to pass metadata to runnables Covers how to use custom functions as Runnables, including constructors, decorators, coercion, passing metadata, and implementing streaming.
|
||||
[How to: pass through inputs from one chain step to the next](https://python.langchain.com/docs/how_to/passthrough/): needing to pass data from one step to the next in a chain, when formatting inputs for prompts, when retrieving and preparing context for prompts. This page explains how to use RunnablePassthrough and RunnableParallel to pass data unchanged through chains, covering examples like formatting retrieval results and user inputs into prompts.
|
||||
[How to: configure runnable behavior at runtime](https://python.langchain.com/docs/how_to/configure/): configuring chain internals at runtime, swapping models or prompts within a chain, or exploring different configurations of runnables. The page covers how to use .configurable_fields to configure parameters of a runnable at runtime, and .configurable_alternatives to swap out runnables with alternatives, including examples for chat models, prompts, and combinations thereof.
|
||||
[How to: add message history (memory) to a chain](https://python.langchain.com/docs/how_to/message_history/): building a chatbot or multi-turn application, wanting to persist conversational state, wanting to manage message history This page explains how to add message history and persist conversational state using LangGraph, covering examples with chat models and prompt templates, and managing the message history.
|
||||
[How to: route between sub-chains](https://python.langchain.com/docs/how_to/routing/): LLM should read this page when: - It needs to conditionally route between sub-chains based on previous outputs - It needs to use semantic similarity to choose the most relevant prompt for a given query 'The page covers how to route between sub-chains in LangChain, including using custom functions, RunnableBranch, and semantic similarity for prompt routing. It provides code examples for each method.'
|
||||
[How to: create a dynamic (self-constructing) chain](https://python.langchain.com/docs/how_to/dynamic_chain/): developing dynamic chains, implementing conditional routing, returning runnables dynamically The page explains how to create a dynamic chain that constructs parts of itself at runtime by having Runnable Lambdas return other Runnables.
|
||||
[How to: inspect runnables](https://python.langchain.com/docs/how_to/inspect/): inspecting internals of an LCEL chain, debugging chain logic, or retrieving chain prompts. Provides methods to visualize chain graphs, print prompts used in chains, and inspect chain steps programmatically.
|
||||
[How to: add fallbacks to a runnable](https://python.langchain.com/docs/how_to/fallbacks/): needing to add fallback options in case of errors, processing long inputs, or wanting to use a better model. This page explains how to configure fallback chains for LLM APIs in case of rate limiting or errors, for handling long input texts exceeding context windows, and for defaulting to better models when parsing fails.
|
||||
[How to: pass runtime secrets to a runnable](https://python.langchain.com/docs/how_to/runnable_runtime_secrets/): needing to pass sensitive data to a runnable, ensuring secrets remain hidden from tracing, or integrating secret values with runnables. Explains how to pass runtime secrets to runnables using RunnableConfig, allowing certain keys to be hidden from tracing while still being accessible during invocation.
|
||||
|
||||
Tracing gives you observability inside your chains and agents, and is vital in diagnosing issues.
|
||||
|
||||
[How to: trace with LangChain](https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain/): tracing LangChain applications with LangSmith, customizing trace metadata and run names, or integrating LangChain with the LangSmith SDK. Provides guides on integrating LangSmith tracing into LangChain applications, configuring trace metadata and run names, distributed tracing, interoperability between LangChain and LangSmith SDK, and tracing LangChain invocations without environment variables.
|
||||
[How to: add metadata and tags to traces](https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain/#add-metadata-and-tags-to-traces): tracing LangChain applications with LangSmith, when logging metadata and tags to traces, and when customizing trace names and IDs. This page provides step-by-step guides on integrating LangSmith tracing with LangChain in Python and JS/TS, covering quick start instructions, selective tracing, logging to specific projects, adding metadata/tags, customizing run names/IDs, accessing run IDs, distributed tracing in Python, and interoperability with the LangSmith SDK.
|
||||
|
||||
[in this section of the LangSmith docs](https://docs.smith.langchain.com/how_to_guides/tracing/): configuring observability for LLM applications, accessing and managing traces, and setting up automation and monitoring. Guides on configuring tracing, using the UI/API for traces, creating dashboards, automating rules/alerts, and gathering human feedback for LLM applications.
|
||||
|
||||
## Integrations
|
||||
|
||||
### Featured Chat Model Providers
|
||||
|
||||
- [ChatAnthropic](https://python.langchain.com/docs/integrations/chat/anthropic/)
|
||||
- [ChatMistralAI](https://python.langchain.com/docs/integrations/chat/mistralai/)
|
||||
- [ChatFireworks](https://python.langchain.com/docs/integrations/chat/fireworks/)
|
||||
- [AzureChatOpenAI](https://python.langchain.com/docs/integrations/chat/azure_chat_openai/)
|
||||
- [ChatOpenAI](https://python.langchain.com/docs/integrations/chat/openai/)
|
||||
- [ChatTogether](https://python.langchain.com/docs/integrations/chat/together/)
|
||||
- [ChatVertexAI](https://python.langchain.com/docs/integrations/chat/google_vertex_ai_palm/)
|
||||
- [ChatGoogleGenerativeAI](https://python.langchain.com/docs/integrations/chat/google_generative_ai/)
|
||||
- [ChatGroq](https://python.langchain.com/docs/integrations/chat/groq/)
|
||||
- [ChatCohere](https://python.langchain.com/docs/integrations/chat/cohere/)
|
||||
- [ChatBedrock](https://python.langchain.com/docs/integrations/chat/bedrock/)
|
||||
- [ChatHuggingFace](https://python.langchain.com/docs/integrations/chat/huggingface/)
|
||||
- [ChatNVIDIA](https://python.langchain.com/docs/integrations/chat/nvidia_ai_endpoints/)
|
||||
- [ChatOllama](https://python.langchain.com/docs/integrations/chat/ollama/)
|
||||
- [ChatLlamaCpp](https://python.langchain.com/docs/integrations/chat/llamacpp/)
|
||||
- [ChatAI21](https://python.langchain.com/docs/integrations/chat/ai21/)
|
||||
- [ChatUpstage](https://python.langchain.com/docs/integrations/chat/upstage/)
|
||||
- [ChatDatabricks](https://python.langchain.com/docs/integrations/chat/databricks/)
|
||||
- [ChatWatsonx](https://python.langchain.com/docs/integrations/chat/ibm_watsonx/)
|
||||
- [ChatXAI](https://python.langchain.com/docs/integrations/chat/xai/)
|
||||
|
||||
[All](https://python.langchain.com/docs/integrations/chat/): integrating chat models into an application, using chat models for conversational AI tasks, or choosing between different chat model providers. Provides an overview of chat models integrated with LangChain, including OpenAI, Anthropic, Google, and others. Covers key features like tool calling, structured output, JSON mode, local usage, and multimodal support.
|
||||
|
||||
## Glossary
|
||||
|
||||
[AIMessageChunk](https://python.langchain.com/docs/concepts/messages/#aimessagechunk): 'needing to understand messages and message structure for chat models, when working with chat history, and when integrating with chat model providers' Line 2: 'Detailed overview of the different message types used in LangChain for chat models, how messages are structured, and how to convert between LangChain and OpenAI message formats.'
|
||||
[AIMessage](https://python.langchain.com/docs/concepts/messages/#aimessage): building chat applications, when implementing tool calling, or when working with chat model outputs. Messages are the units of communication in chat models, representing input, output and metadata; topics include message types, roles, content, metadata, conversation structure, and LangChain's unified message format.
|
||||
[astream_events](https://python.langchain.com/docs/concepts/chat_models/#key-methods): LLM should read this page when: 1) Implementing an application that uses a chat model 2) Integrating chat models with other LangChain components 3) Planning for advanced chat model features like tool calling or structured outputs This page provides an overview of chat models in LangChain, including their key features, interfaces, integration options, tool calling, structured outputs, multimodality, context windows, and advanced topics like rate limiting and caching.
|
||||
[BaseTool](https://python.langchain.com/docs/concepts/tools/#tool-interface): needing to understand LangChain tools, wanting to create custom tools, or looking for best practices for designing tools. The page covers the tool abstraction in LangChain, which associates a Python function with a schema for name, description, and arguments. It explains how to create tools using the @tool decorator, configure the schema, handle tool artifacts, use special type annotations (InjectedToolArg, RunnableConfig), and provides an overview of toolkits.
|
||||
[invoke](https://python.langchain.com/docs/concepts/runnables/): learning how to use the Runnable interface, when working with custom Runnables, and when needing to configure Runnables at runtime. The page covers the Runnable interface, its methods for invocation, batching, streaming, inspecting schemas, and configuration. It explains RunnableConfig, custom Runnables, and configurable Runnables.
|
||||
[bind_tools](https://python.langchain.com/docs/concepts/tool_calling/#tool-binding): building applications that require an LLM to directly interact with external systems or APIs, when integrating tools or functions into an LLM workflow, or when fine-tuning an LLM to better handle tool calling. This page provides an overview of tool calling, which allows LLMs to invoke external tools or APIs with specific input schemas. It covers key concepts like tool creation, binding tools to LLMs, initiating tool calls from LLMs, and executing the called tools. It also offers guidance on recommended usage and best practices.
|
||||
[Caching](https://python.langchain.com/docs/concepts/chat_models/#caching): building chat applications, using LLMs for information extraction, or working with multimodal data This page discusses chat models, which are language models that operate on messages. It covers chat model interfaces, integrations, features like tool calling and structured outputs, multimodality, context windows, rate limiting, and caching.
|
||||
[Chat models](https://python.langchain.com/docs/concepts/multimodality/#multimodality-in-chat-models): needing to understand multimodal capabilities in LangChain, when working with multimodal data like images/audio/video, and when determining if a specific LangChain component supports multimodality. Provides an overview of multimodality in chat models, embedding models, and vector stores. Discusses multimodal inputs/outputs for chat models and how they are formatted.
|
||||
[Configurable runnables](https://python.langchain.com/docs/concepts/runnables/#configurable-runnables): trying to understand how to use Runnables, how to configure and compose Runnables, and how to inspect Runnable schemas. The Runnable interface is the foundation for working with LangChain components like language models, output parsers, and retrievers. It defines methods for invoking, batching, streaming, inspecting schemas, configuring, and composing Runnables.
|
||||
[Context window](https://python.langchain.com/docs/concepts/chat_models/#context-window): getting an overview of chat models, understanding the key functionality of chat models, and determining if this concept is relevant for their application. Provides an overview of chat models (LLMs with a chat interface), their features, integrations, key methods like invoking/streaming, handling inputs/outputs, using tools/structured outputs, and advanced topics like rate limiting and caching.
|
||||
[Conversation patterns](https://python.langchain.com/docs/concepts/chat_history/#conversation-patterns): managing conversation history in chatbots, implementing memory for chat models, understanding correct conversation structure. This page explains the concept of chat history, a record of messages exchanged between a user and a chat model. It covers conversation patterns, guidelines for managing chat history to avoid exceeding context window, and the importance of preserving conversation structure.
|
||||
[Document](https://python.langchain.com/api_reference/core/documents/langchain_core.documents.base.Document.html/): working with document data, retrieving and processing text documents, integrating with text embedding and vector storage systems This page provides details on the Document class and its associated methods and properties, as well as examples of how to use it in various scenarios such as document loading, retrieval, and transformation
|
||||
[Embedding models](https://python.langchain.com/docs/concepts/multimodality/#multimodality-in-embedding-models): needing to understand multimodal capabilities of LangChain components, wanting to work with non-text data like images/audio/video, or planning to incorporate multimodal data in chat interactions. Provides an overview of multimodality support in chat models (inputs and tools), embedding models, and vector stores; notes current limitations and expected future expansions to handle different data types.
|
||||
[HumanMessage](https://python.langchain.com/docs/concepts/messages/#humanmessage): LLM should read this page when: 1) Understanding how to structure conversations with chat models, 2) Needing to work with different types of messages (user, assistant, system, tool), 3) Converting between LangChain and OpenAI message formats. Messages are the units of communication used by chat models, representing user input, assistant output, system instructions, and tool results. Key topics include message structure, types (HumanMessage, AIMessage, SystemMessage, ToolMessage), multimodal content support, and integration with OpenAI message format.
|
||||
[InjectedState](https://python.langchain.com/docs/concepts/tools/#injectedstate): learning about LangChain's tools, creating custom tools, or integrating tools with chat models. Provides conceptual overview of tools - encapsulating functions with schemas for models to call. Covers creating tools with @tool decorator, tool interfaces, special type annotations, artifacts, best practices, and toolkits.
|
||||
[InjectedStore](https://python.langchain.com/docs/concepts/tools/#injectedstore): needing to understand how to create and use tools in LangChain, when needing to pass runtime values to tools, and when needing to configure a tool's schema. Tools are a way to encapsulate functions and their schemas to be used with chat models that support tool calling. The page covers the tool interface, creating tools with the @tool decorator, using tools directly, configuring tool schemas, returning artifacts from tools, and special type annotations like InjectedToolArg and RunnableConfig.
|
||||
[InjectedToolArg](https://python.langchain.com/docs/concepts/tools/#injectedtoolarg): trying to understand how to create and use tools in LangChain, when needing to configure tool schemas, and when wanting to return artifacts from tools. Tools provide a way to encapsulate Python functions and schemas to be passed to chat models for execution. The page covers creating tools with the @tool decorator, configuring tool schemas, special type annotations, and tool artifacts.
|
||||
[input and output types](https://python.langchain.com/docs/concepts/runnables/#input-and-output-types): needing to interact with LangChain components, wanting to understand the core Runnable interface, or composing complex chains using LCEL. Covers the Runnable interface that defines a standard way to invoke, batch, stream and inspect components; the RunnableConfig for setting runtime options; creating custom Runnables; configurable Runnables; and how input/output types, schemas, and streaming work.
|
||||
[Integration packages](https://python.langchain.com/docs/concepts/architecture/#integration-packages): determining the overall architecture of LangChain, understanding the different components and packages in the LangChain ecosystem, or deciding which packages to import for a specific use case. This page provides an overview of the different packages that make up the LangChain framework, including langchain-core, langchain, integration packages, langchain-community, langgraph, langserve, and LangSmith, and explains the purpose and contents of each package.
|
||||
[Integration tests](https://python.langchain.com/docs/concepts/testing/#integration-tests): needing guidance on testing LangChain components, understanding different types of tests (unit, integration, standard), or wanting to contribute by adding tests to an integration. Provides an overview of unit tests, integration tests, and standard tests in the LangChain ecosystem, including definitions, examples, and how to implement them for new tools/integrations.
|
||||
[invoke](https://python.langchain.com/docs/concepts/runnables/): learning how to use the Runnable interface, when working with custom Runnables, and when needing to configure Runnables at runtime. The page covers the Runnable interface, its methods for invocation, batching, streaming, inspecting schemas, and configuration. It explains RunnableConfig, custom Runnables, and configurable Runnables.
|
||||
[JSON mode](https://python.langchain.com/docs/concepts/structured_outputs/#json-mode): LLM should read this page when: 1) It needs to return structured output that conforms to a specific schema, 2) It needs to store model output in a database, 3) It needs to ensure model output matches a predefined format. This page covers how to define an output schema, and techniques like tool calling and JSON mode that allow models to return structured output conforming to that schema, as well as a helper method to streamline the process.
|
||||
[langchain-community](https://python.langchain.com/docs/concepts/architecture/#langchain-community): learning about the structure of LangChain, deploying LangChain applications, or needing an overview of the LangChain ecosystem. This page gives an overview of the different packages, components, and services that make up the LangChain framework, including langchain-core, langchain, integration packages, langchain-community, LangGraph, LangServe, and LangSmith.
|
||||
[langchain-core](https://python.langchain.com/docs/concepts/architecture/#langchain-core): needing an overview of LangChain's architecture, when considering integrating external packages, or when exploring the LangChain ecosystem. Outlines the main components of LangChain (langchain-core, langchain, integration packages, langchain-community, langgraph, langserve, LangSmith) and their roles, providing a high-level architectural overview.
|
||||
[langchain](https://python.langchain.com/docs/concepts/architecture/#langchain): looking to understand the overall architecture of LangChain, when trying to determine what LangChain packages to install, or when wanting an overview of the various LangChain projects. This page outlines the hierarchical structure of the LangChain framework, describing the purpose and contents of key packages like langchain-core, langchain, integration packages, langchain-community, langgraph, langserve, and LangSmith.
|
||||
[langgraph](https://python.langchain.com/docs/concepts/architecture/#langgraph): developing applications with LangChain, seeking to understand the overall architecture of LangChain, planning to contribute to or integrate with LangChain The page outlines the layered architecture of LangChain, describing the core abstraction layer, the main LangChain package, integration packages, community integrations, LangGraph for stateful agents, LangServe for deployment, and LangSmith developer tools
|
||||
[Managing chat history](https://python.langchain.com/docs/concepts/chat_history/#managing-chat-history): understanding and managing chat history, learning about conversation patterns, following correct chat history structure. Explains chat history concept, provides guidelines for managing chat history, discusses conversation patterns involving users, assistants, and tools.
|
||||
[OpenAI format](https://python.langchain.com/docs/concepts/messages/#openai-format): building chat applications, working with chat models, or consuming message streams. This page covers the structure and components of messages used in chat models, including roles, content, usage metadata, and different message types like HumanMessage, AIMessage, and ToolMessage.
|
||||
[Propagation of RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#propagation-of-runnableconfig): LLM should read this page when: learning about the LangChain Runnable interface, working with Runnables in LangChain, understanding how to configure and execute Runnables. The page covers the Runnable interface in LangChain, including invoking/batching/streaming Runnables, input/output schemas, configuring Runnables, creating custom Runnables, and working with configurable Runnables.
|
||||
[rate-limiting](https://python.langchain.com/docs/concepts/chat_models/#rate-limiting): 1) working with chat models, 2) integrating tool calling or structured outputs, 3) understanding chat model capabilities. Overview of chat model interface, inputs/outputs, standard parameters; tool calling and structured output support; multimodality; context window; advanced topics like rate limiting, caching.
|
||||
[RemoveMessage](https://python.langchain.com/docs/concepts/messages/#removemessage): needing information on the structure of messages used in conversational AI models, wanting to understand how messages are represented in LangChain, or looking for details on specific message types like SystemMessage, HumanMessage, and AIMessage. Messages are the basic units of communication in conversational AI models, containing a role (e.g. user, assistant), content (text or multimodal data), and metadata; LangChain provides a standardized message format and different message types to represent various components of a conversation.
|
||||
[role](https://python.langchain.com/docs/concepts/messages/#role): understanding how to structure messages for chat models, accessing details about different LangChain message types, or converting between LangChain and OpenAI message formats. Messages are the core unit of communication in chat models, representing input/output content and metadata; LangChain defines SystemMessage, HumanMessage, AIMessage, ToolMessage and others to standardize message format across providers.
|
||||
[RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig): needing to understand the Runnable interface, invoking and configuring Runnables, and creating custom Runnables. The page covers the Runnable interface's core concepts, methods like invoke, batch, and stream, input/output types, configuring Runnables with RunnableConfig, creating custom Runnables from functions, and using configurable Runnables.
|
||||
[Standard parameters for chat models](https://python.langchain.com/docs/concepts/chat_models/#standard-parameters): building applications using chat models, working with chat models for tool calling, structured outputs or multimodal inputs/outputs. Covers overview of chat models, integrations, interfaces, tool calling, structured outputs, multimodality, context window, rate-limiting, and caching of chat models.
|
||||
[Standard tests](https://python.langchain.com/docs/concepts/testing/#standard-tests): needing guidance on testing LangChain components, or wanting to understand the different types of tests used in LangChain. This page discusses unit tests for individual functions, integration tests for validating multiple components working together, and LangChain's standard tests for ensuring consistency across tools and integrations.
|
||||
[stream](https://python.langchain.com/docs/concepts/streaming/): [building applications that use streaming, building applications that need to display partial results in real-time, building applications that need to provide updates on pipeline or workflow progress] 'This page covers streaming in LangChain, including what can be streamed in LLM applications, the streaming APIs available, how to write custom data to the stream, and how LangChain automatically enables streaming for chat models in certain cases.'
|
||||
[Tokens](https://python.langchain.com/docs/concepts/tokens/): needing to understand tokens used by LLMs, when dealing with character/token counts, when working with multimodal inputs Tokens are the fundamental units processed by language models. A token can represent words, word parts, punctuation, and other units. Models tokenize inputs, process tokens sequentially, and generate new tokens as output. Tokens enable efficient and contextual language processing compared to characters.
|
||||
[Tokens](https://python.langchain.com/docs/concepts/tokens/): needing to understand tokens used by LLMs, when dealing with character/token counts, when working with multimodal inputs Tokens are the fundamental units processed by language models. A token can represent words, word parts, punctuation, and other units. Models tokenize inputs, process tokens sequentially, and generate new tokens as output. Tokens enable efficient and contextual language processing compared to characters.
|
||||
[Tool artifacts](https://python.langchain.com/docs/concepts/tools/#tool-artifacts): needing to understand what tools are, how to create and use them, and how they integrate with models. Explains what tools are in LangChain, how to create them using the @tool decorator, special type annotations for configuring runtime behavior, how to use tools directly or pass them to chat models, and best practices for designing tools.
|
||||
[Tool binding](https://python.langchain.com/docs/concepts/tool_calling/#tool-binding): determining if tool calling functionality is appropriate for their application, understanding the key concepts and workflow of tool calling, and considering best practices for designing tools. This page covers an overview of tool calling, key concepts like tool creation/binding/calling/execution, recommended usage workflow, details on implementing each step, and best practices for designing effective tools.
|
||||
[@tool](https://python.langchain.com/docs/concepts/tools/#create-tools-using-the-tool-decorator): needing to understand tools in LangChain, when creating custom tools, or when integrating tools into LangChain applications. Provides an overview of tools, how to create and configure tools using the @tool decorator, different tool types (e.g. with artifacts, injected arguments), and best practices for designing tools.
|
||||
[Toolkits](https://python.langchain.com/docs/concepts/tools/#toolkits): creating custom Python functions to use with LangChain, configuring existing tools, or adding tools to chat models. Explains the tool abstraction for encapsulating Python functions, creating tools with the `@tool` decorator, configuring schemas, handling tool artifacts, special type annotations, and using toolkits that group related tools.
|
||||
[ToolMessage](https://python.langchain.com/docs/concepts/messages/#toolmessage): understanding the communication protocol with chat models, working with chat history management, or understanding LangChain's Message object structure. Messages are the unit of communication in chat models and represent input/output along with metadata; LangChain provides a unified Message format with types like SystemMessage, HumanMessage, AIMessage to handle different roles, content types, tool calls.
|
||||
[Unit tests](https://python.langchain.com/docs/concepts/testing/#unit-tests): developing unit or integration tests, or when contributing to LangChain integrations Provides an overview of unit tests, integration tests, and standard tests used in the LangChain ecosystem
|
||||
[Vector stores](https://python.langchain.com/docs/concepts/vectorstores/): LLM should read this page when: 1) Building applications that need to index and retrieve information based on semantic similarity 2) Integrating vector databases into their application 3) Exploring advanced vector search and retrieval techniques Vector stores are specialized data stores that enable indexing and retrieving information based on vector representations (embeddings) of data, allowing semantic similarity search over unstructured data like text, images, and audio. The page covers vector store integrations, the core interface, adding/deleting documents, basic and advanced similarity search techniques, and concepts like metadata filtering.
|
||||
[with_structured_output](https://python.langchain.com/docs/concepts/structured_outputs/#structured-output-method): [needing to return structured data like JSON or database rows, working with models that support structured output like tools or JSON modes, or integrating with helper functions to streamline structured output] [Overview of structured output concept, schema definition formats like JSON/dicts and Pydantic, model integration methods like tool calling and JSON modes, LangChain structured output helper method]
|
||||
[with_types](https://python.langchain.com/docs/concepts/runnables/#with_types): learning about the Runnable interface in LangChain, understanding how to work with Runnables, and customizing or configuring Runnables. The page covers the Runnable interface, optimized parallel execution, streaming APIs, input/output types, inspecting schemas, RunnableConfig options, creating custom Runnables from functions, and configurable Runnables.
|
||||
3859
docs/vercel.json
3859
docs/vercel.json
File diff suppressed because it is too large
Load Diff
@@ -29,18 +29,9 @@ langchain = "langchain_cli.cli:app"
|
||||
langchain-cli = "langchain_cli.cli:app"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=7.4.2,<9.0.0",
|
||||
"pytest-watcher>=0.3.4,<1.0.0"
|
||||
]
|
||||
lint = [
|
||||
"ruff>=0.13.1,<0.14",
|
||||
"mypy>=1.18.1,<1.19"
|
||||
]
|
||||
test = [
|
||||
"langchain-core",
|
||||
"langchain"
|
||||
]
|
||||
dev = ["pytest>=7.4.2,<9.0.0", "pytest-watcher>=0.3.4,<1.0.0"]
|
||||
lint = ["ruff>=0.13.1,<0.14", "mypy>=1.18.1,<1.19"]
|
||||
test = ["langchain-core", "langchain"]
|
||||
typing = ["langchain"]
|
||||
test_integration = []
|
||||
|
||||
|
||||
35
libs/cli/uv.lock
generated
35
libs/cli/uv.lock
generated
@@ -277,6 +277,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -335,31 +344,20 @@ dependencies = [
|
||||
requires-dist = [
|
||||
{ name = "async-timeout", marker = "python_full_version < '3.11'", specifier = ">=4.0.0,<5.0.0" },
|
||||
{ name = "langchain-anthropic", marker = "extra == 'anthropic'" },
|
||||
{ name = "langchain-aws", marker = "extra == 'aws'" },
|
||||
{ name = "langchain-azure-ai", marker = "extra == 'azure-ai'" },
|
||||
{ name = "langchain-cohere", marker = "extra == 'cohere'" },
|
||||
{ name = "langchain-community", marker = "extra == 'community'" },
|
||||
{ name = "langchain-core", editable = "../core" },
|
||||
{ name = "langchain-deepseek", marker = "extra == 'deepseek'" },
|
||||
{ name = "langchain-fireworks", marker = "extra == 'fireworks'" },
|
||||
{ name = "langchain-google-genai", marker = "extra == 'google-genai'" },
|
||||
{ name = "langchain-google-vertexai", marker = "extra == 'google-vertexai'" },
|
||||
{ name = "langchain-groq", marker = "extra == 'groq'" },
|
||||
{ name = "langchain-huggingface", marker = "extra == 'huggingface'" },
|
||||
{ name = "langchain-mistralai", marker = "extra == 'mistralai'" },
|
||||
{ name = "langchain-ollama", marker = "extra == 'ollama'" },
|
||||
{ name = "langchain-openai", marker = "extra == 'openai'", editable = "../partners/openai" },
|
||||
{ name = "langchain-perplexity", marker = "extra == 'perplexity'" },
|
||||
{ name = "langchain-text-splitters", editable = "../text-splitters" },
|
||||
{ name = "langchain-together", marker = "extra == 'together'" },
|
||||
{ name = "langchain-xai", marker = "extra == 'xai'" },
|
||||
{ name = "langsmith", specifier = ">=0.1.17,<1.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
|
||||
{ name = "pyyaml", specifier = ">=5.3.0,<7.0.0" },
|
||||
{ name = "requests", specifier = ">=2.0.0,<3.0.0" },
|
||||
{ name = "sqlalchemy", specifier = ">=1.4.0,<3.0.0" },
|
||||
]
|
||||
provides-extras = ["community", "anthropic", "openai", "azure-ai", "cohere", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"]
|
||||
provides-extras = ["community", "anthropic", "openai", "google-vertexai", "google-genai", "together"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -485,7 +483,7 @@ typing = [{ name = "langchain", editable = "../langchain" }]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.78"
|
||||
version = "1.0.0a5"
|
||||
source = { editable = "../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -586,28 +584,29 @@ 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 = "tiktoken", specifier = ">=0.11.0,<1.0.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.8.0,<1.0.0" },
|
||||
{ name = "types-requests", specifier = ">=2.31.0.20240218,<3.0.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langserve"
|
||||
version = "0.3.2"
|
||||
version = "0.0.51"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langchain" },
|
||||
{ name = "orjson" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/fb/86e1f5049fb3593743f0fb049c4991f4984020cda00b830ae31f2c47b46b/langserve-0.3.2.tar.gz", hash = "sha256:134b78b1d897c6bcd1fb8a6258e30cf0fb318294505e4ea59c2bea72fa152129", size = 1141270, upload-time = "2025-09-17T20:01:22.183Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/af/243c8a6ad0efee30186fba5a05a68b4bd9553d3662f946e2e8302cb4a141/langserve-0.0.51.tar.gz", hash = "sha256:036c0104c512bcc2c2406ae089ef9e7e718c32c39ebf6dcb2212f168c7d09816", size = 1135441, upload-time = "2024-03-12T06:16:32.374Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/f0/193c34bf61e1dee8bd637dbeddcc644c46d14e8b03068792ca60b1909bc1/langserve-0.3.2-py3-none-any.whl", hash = "sha256:d9c4cd19d12f6362b82ceecb10357b339b3640a858b9bc30643d5f8a0a036bce", size = 1173213, upload-time = "2025-09-17T20:01:20.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/49/5b407071f7ea5a861b3f4c3ed2f034cdafb75db1554bbe1a256a092d2669/langserve-0.0.51-py3-none-any.whl", hash = "sha256:e735eef2b6fde7e1514f4be8234b9f0727283e639822ca9c25e8ccc2d24e8492", size = 1167759, upload-time = "2024-03-12T06:16:30.099Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
all = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "sse-starlette" },
|
||||
]
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from collections.abc import Generator
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ParamSpec,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
@@ -25,7 +26,6 @@ from typing import (
|
||||
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic.v1.fields import FieldInfo as FieldInfoV1
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from langchain_core._api.internal import is_caller_internal
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Some **beta** features that are not yet ready for production."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Runnables."""
|
||||
@@ -1,447 +0,0 @@
|
||||
"""Context management for runnables."""
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from collections.abc import Awaitable, Mapping, Sequence
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from typing_extensions import override
|
||||
|
||||
from langchain_core._api.beta_decorator import beta
|
||||
from langchain_core.runnables.base import (
|
||||
Runnable,
|
||||
RunnableSerializable,
|
||||
coerce_to_runnable,
|
||||
)
|
||||
from langchain_core.runnables.config import RunnableConfig, ensure_config, patch_config
|
||||
from langchain_core.runnables.utils import ConfigurableFieldSpec, Input, Output
|
||||
|
||||
T = TypeVar("T")
|
||||
Values = dict[Union[asyncio.Event, threading.Event], Any]
|
||||
CONTEXT_CONFIG_PREFIX = "__context__/"
|
||||
CONTEXT_CONFIG_SUFFIX_GET = "/get"
|
||||
CONTEXT_CONFIG_SUFFIX_SET = "/set"
|
||||
|
||||
|
||||
async def _asetter(done: asyncio.Event, values: Values, value: T) -> T:
|
||||
values[done] = value
|
||||
done.set()
|
||||
return value
|
||||
|
||||
|
||||
async def _agetter(done: asyncio.Event, values: Values) -> Any:
|
||||
await done.wait()
|
||||
return values[done]
|
||||
|
||||
|
||||
def _setter(done: threading.Event, values: Values, value: T) -> T:
|
||||
values[done] = value
|
||||
done.set()
|
||||
return value
|
||||
|
||||
|
||||
def _getter(done: threading.Event, values: Values) -> Any:
|
||||
done.wait()
|
||||
return values[done]
|
||||
|
||||
|
||||
def _key_from_id(id_: str) -> str:
|
||||
wout_prefix = id_.split(CONTEXT_CONFIG_PREFIX, maxsplit=1)[1]
|
||||
if wout_prefix.endswith(CONTEXT_CONFIG_SUFFIX_GET):
|
||||
return wout_prefix[: -len(CONTEXT_CONFIG_SUFFIX_GET)]
|
||||
if wout_prefix.endswith(CONTEXT_CONFIG_SUFFIX_SET):
|
||||
return wout_prefix[: -len(CONTEXT_CONFIG_SUFFIX_SET)]
|
||||
msg = f"Invalid context config id {id_}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _config_with_context(
|
||||
config: RunnableConfig,
|
||||
steps: list[Runnable],
|
||||
setter: Callable,
|
||||
getter: Callable,
|
||||
event_cls: Union[type[threading.Event], type[asyncio.Event]],
|
||||
) -> RunnableConfig:
|
||||
if any(k.startswith(CONTEXT_CONFIG_PREFIX) for k in config.get("configurable", {})):
|
||||
return config
|
||||
|
||||
context_specs = [
|
||||
(spec, i)
|
||||
for i, step in enumerate(steps)
|
||||
for spec in step.config_specs
|
||||
if spec.id.startswith(CONTEXT_CONFIG_PREFIX)
|
||||
]
|
||||
grouped_by_key = {
|
||||
key: list(group)
|
||||
for key, group in groupby(
|
||||
sorted(context_specs, key=lambda s: s[0].id),
|
||||
key=lambda s: _key_from_id(s[0].id),
|
||||
)
|
||||
}
|
||||
deps_by_key = {
|
||||
key: {
|
||||
_key_from_id(dep) for spec in group for dep in (spec[0].dependencies or [])
|
||||
}
|
||||
for key, group in grouped_by_key.items()
|
||||
}
|
||||
|
||||
values: Values = {}
|
||||
events: defaultdict[str, Union[asyncio.Event, threading.Event]] = defaultdict(
|
||||
event_cls
|
||||
)
|
||||
context_funcs: dict[str, Callable[[], Any]] = {}
|
||||
for key, group in grouped_by_key.items():
|
||||
getters = [s for s in group if s[0].id.endswith(CONTEXT_CONFIG_SUFFIX_GET)]
|
||||
setters = [s for s in group if s[0].id.endswith(CONTEXT_CONFIG_SUFFIX_SET)]
|
||||
|
||||
for dep in deps_by_key[key]:
|
||||
if key in deps_by_key[dep]:
|
||||
msg = f"Deadlock detected between context keys {key} and {dep}"
|
||||
raise ValueError(msg)
|
||||
if len(setters) != 1:
|
||||
msg = f"Expected exactly one setter for context key {key}"
|
||||
raise ValueError(msg)
|
||||
setter_idx = setters[0][1]
|
||||
if any(getter_idx < setter_idx for _, getter_idx in getters):
|
||||
msg = f"Context setter for key {key} must be defined after all getters."
|
||||
raise ValueError(msg)
|
||||
|
||||
if getters:
|
||||
context_funcs[getters[0][0].id] = partial(getter, events[key], values)
|
||||
context_funcs[setters[0][0].id] = partial(setter, events[key], values)
|
||||
|
||||
return patch_config(config, configurable=context_funcs)
|
||||
|
||||
|
||||
def aconfig_with_context(
|
||||
config: RunnableConfig,
|
||||
steps: list[Runnable],
|
||||
) -> RunnableConfig:
|
||||
"""Asynchronously patch a runnable config with context getters and setters.
|
||||
|
||||
Args:
|
||||
config: The runnable config.
|
||||
steps: The runnable steps.
|
||||
|
||||
Returns:
|
||||
The patched runnable config.
|
||||
"""
|
||||
return _config_with_context(config, steps, _asetter, _agetter, asyncio.Event)
|
||||
|
||||
|
||||
def config_with_context(
|
||||
config: RunnableConfig,
|
||||
steps: list[Runnable],
|
||||
) -> RunnableConfig:
|
||||
"""Patch a runnable config with context getters and setters.
|
||||
|
||||
Args:
|
||||
config: The runnable config.
|
||||
steps: The runnable steps.
|
||||
|
||||
Returns:
|
||||
The patched runnable config.
|
||||
"""
|
||||
return _config_with_context(config, steps, _setter, _getter, threading.Event)
|
||||
|
||||
|
||||
@beta()
|
||||
class ContextGet(RunnableSerializable):
|
||||
"""Get a context value."""
|
||||
|
||||
prefix: str = ""
|
||||
|
||||
key: Union[str, list[str]]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"ContextGet({_print_keys(self.key)})"
|
||||
|
||||
@property
|
||||
def ids(self) -> list[str]:
|
||||
"""The context getter ids."""
|
||||
prefix = self.prefix + "/" if self.prefix else ""
|
||||
keys = self.key if isinstance(self.key, list) else [self.key]
|
||||
return [
|
||||
f"{CONTEXT_CONFIG_PREFIX}{prefix}{k}{CONTEXT_CONFIG_SUFFIX_GET}"
|
||||
for k in keys
|
||||
]
|
||||
|
||||
@property
|
||||
@override
|
||||
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
||||
return super().config_specs + [
|
||||
ConfigurableFieldSpec(
|
||||
id=id_,
|
||||
annotation=Callable[[], Any],
|
||||
)
|
||||
for id_ in self.ids
|
||||
]
|
||||
|
||||
@override
|
||||
def invoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
config = ensure_config(config)
|
||||
configurable = config.get("configurable", {})
|
||||
if isinstance(self.key, list):
|
||||
return {key: configurable[id_]() for key, id_ in zip(self.key, self.ids)}
|
||||
return configurable[self.ids[0]]()
|
||||
|
||||
@override
|
||||
async def ainvoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
config = ensure_config(config)
|
||||
configurable = config.get("configurable", {})
|
||||
if isinstance(self.key, list):
|
||||
values = await asyncio.gather(*(configurable[id_]() for id_ in self.ids))
|
||||
return dict(zip(self.key, values))
|
||||
return await configurable[self.ids[0]]()
|
||||
|
||||
|
||||
SetValue = Union[
|
||||
Runnable[Input, Output],
|
||||
Callable[[Input], Output],
|
||||
Callable[[Input], Awaitable[Output]],
|
||||
Any,
|
||||
]
|
||||
|
||||
|
||||
def _coerce_set_value(value: SetValue) -> Runnable[Input, Output]:
|
||||
if not isinstance(value, Runnable) and not callable(value):
|
||||
return coerce_to_runnable(lambda _: value)
|
||||
return coerce_to_runnable(value)
|
||||
|
||||
|
||||
@beta()
|
||||
class ContextSet(RunnableSerializable):
|
||||
"""Set a context value."""
|
||||
|
||||
prefix: str = ""
|
||||
|
||||
keys: Mapping[str, Optional[Runnable]]
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: Optional[str] = None,
|
||||
value: Optional[SetValue] = None,
|
||||
prefix: str = "",
|
||||
**kwargs: SetValue,
|
||||
):
|
||||
"""Create a context setter.
|
||||
|
||||
Args:
|
||||
key: The context setter key.
|
||||
value: The context setter value.
|
||||
prefix: The context setter prefix.
|
||||
**kwargs: Additional context setter key-value pairs.
|
||||
"""
|
||||
if key is not None:
|
||||
kwargs[key] = value
|
||||
super().__init__(
|
||||
keys={
|
||||
k: _coerce_set_value(v) if v is not None else None
|
||||
for k, v in kwargs.items()
|
||||
},
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"ContextSet({_print_keys(list(self.keys.keys()))})"
|
||||
|
||||
@property
|
||||
def ids(self) -> list[str]:
|
||||
"""The context setter ids."""
|
||||
prefix = self.prefix + "/" if self.prefix else ""
|
||||
return [
|
||||
f"{CONTEXT_CONFIG_PREFIX}{prefix}{key}{CONTEXT_CONFIG_SUFFIX_SET}"
|
||||
for key in self.keys
|
||||
]
|
||||
|
||||
@property
|
||||
@override
|
||||
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
||||
mapper_config_specs = [
|
||||
s
|
||||
for mapper in self.keys.values()
|
||||
if mapper is not None
|
||||
for s in mapper.config_specs
|
||||
]
|
||||
for spec in mapper_config_specs:
|
||||
if spec.id.endswith(CONTEXT_CONFIG_SUFFIX_GET):
|
||||
getter_key = spec.id.split("/")[1]
|
||||
if getter_key in self.keys:
|
||||
msg = f"Circular reference in context setter for key {getter_key}"
|
||||
raise ValueError(msg)
|
||||
return super().config_specs + [
|
||||
ConfigurableFieldSpec(
|
||||
id=id_,
|
||||
annotation=Callable[[], Any],
|
||||
)
|
||||
for id_ in self.ids
|
||||
]
|
||||
|
||||
@override
|
||||
def invoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
config = ensure_config(config)
|
||||
configurable = config.get("configurable", {})
|
||||
for id_, mapper in zip(self.ids, self.keys.values()):
|
||||
if mapper is not None:
|
||||
configurable[id_](mapper.invoke(input, config))
|
||||
else:
|
||||
configurable[id_](input)
|
||||
return input
|
||||
|
||||
@override
|
||||
async def ainvoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
config = ensure_config(config)
|
||||
configurable = config.get("configurable", {})
|
||||
for id_, mapper in zip(self.ids, self.keys.values()):
|
||||
if mapper is not None:
|
||||
await configurable[id_](await mapper.ainvoke(input, config))
|
||||
else:
|
||||
await configurable[id_](input)
|
||||
return input
|
||||
|
||||
|
||||
class Context:
|
||||
"""Context for a runnable.
|
||||
|
||||
The `Context` class provides methods for creating context scopes,
|
||||
getters, and setters within a runnable. It allows for managing
|
||||
and accessing contextual information throughout the execution
|
||||
of a program.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_core.beta.runnables.context import Context
|
||||
from langchain_core.runnables.passthrough import RunnablePassthrough
|
||||
from langchain_core.prompts.prompt import PromptTemplate
|
||||
from langchain_core.output_parsers.string import StrOutputParser
|
||||
from tests.unit_tests.fake.llm import FakeListLLM
|
||||
|
||||
chain = (
|
||||
Context.setter("input")
|
||||
| {
|
||||
"context": RunnablePassthrough() | Context.setter("context"),
|
||||
"question": RunnablePassthrough(),
|
||||
}
|
||||
| PromptTemplate.from_template("{context} {question}")
|
||||
| FakeListLLM(responses=["hello"])
|
||||
| StrOutputParser()
|
||||
| {
|
||||
"result": RunnablePassthrough(),
|
||||
"context": Context.getter("context"),
|
||||
"input": Context.getter("input"),
|
||||
}
|
||||
)
|
||||
|
||||
# Use the chain
|
||||
output = chain.invoke("What's your name?")
|
||||
print(output["result"]) # Output: "hello"
|
||||
print(output["context"]) # Output: "What's your name?"
|
||||
print(output["input"]) # Output: "What's your name?
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_scope(scope: str, /) -> "PrefixContext":
|
||||
"""Create a context scope.
|
||||
|
||||
Args:
|
||||
scope: The scope.
|
||||
|
||||
Returns:
|
||||
The context scope.
|
||||
"""
|
||||
return PrefixContext(prefix=scope)
|
||||
|
||||
@staticmethod
|
||||
def getter(key: Union[str, list[str]], /) -> ContextGet:
|
||||
"""Return a context getter.
|
||||
|
||||
Args:
|
||||
key: The context getter key.
|
||||
"""
|
||||
return ContextGet(key=key)
|
||||
|
||||
@staticmethod
|
||||
def setter(
|
||||
_key: Optional[str] = None,
|
||||
_value: Optional[SetValue] = None,
|
||||
/,
|
||||
**kwargs: SetValue,
|
||||
) -> ContextSet:
|
||||
"""Return a context setter.
|
||||
|
||||
Args:
|
||||
_key: The context setter key.
|
||||
_value: The context setter value.
|
||||
**kwargs: Additional context setter key-value pairs.
|
||||
"""
|
||||
return ContextSet(_key, _value, prefix="", **kwargs)
|
||||
|
||||
|
||||
class PrefixContext:
|
||||
"""Context for a runnable with a prefix."""
|
||||
|
||||
prefix: str = ""
|
||||
|
||||
def __init__(self, prefix: str = ""):
|
||||
"""Create a prefix context.
|
||||
|
||||
Args:
|
||||
prefix: The prefix.
|
||||
"""
|
||||
self.prefix = prefix
|
||||
|
||||
def getter(self, key: Union[str, list[str]], /) -> ContextGet:
|
||||
"""Return a prefixed context getter.
|
||||
|
||||
Args:
|
||||
key: The context getter key.
|
||||
"""
|
||||
return ContextGet(key=key, prefix=self.prefix)
|
||||
|
||||
def setter(
|
||||
self,
|
||||
_key: Optional[str] = None,
|
||||
_value: Optional[SetValue] = None,
|
||||
/,
|
||||
**kwargs: SetValue,
|
||||
) -> ContextSet:
|
||||
"""Return a prefixed context setter.
|
||||
|
||||
Args:
|
||||
_key: The context setter key.
|
||||
_value: The context setter value.
|
||||
**kwargs: Additional context setter key-value pairs.
|
||||
"""
|
||||
return ContextSet(_key, _value, prefix=self.prefix, **kwargs)
|
||||
|
||||
|
||||
def _print_keys(keys: Union[str, Sequence[str]]) -> str:
|
||||
if isinstance(keys, str):
|
||||
return f"'{keys}'"
|
||||
return ", ".join(f"'{k}'" for k in keys)
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
import atexit
|
||||
import functools
|
||||
import logging
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
@@ -40,7 +41,6 @@ from langchain_core.tracers.langchain import LangChainTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
from langchain_core.tracers.stdout import ConsoleCallbackHandler
|
||||
from langchain_core.utils.env import env_var_is_set
|
||||
from langchain_core.utils.uuid import uuid7
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncGenerator, Coroutine, Generator, Sequence
|
||||
@@ -506,7 +506,7 @@ class BaseRunManager(RunManagerMixin):
|
||||
|
||||
"""
|
||||
return cls(
|
||||
run_id=uuid7(),
|
||||
run_id=uuid.uuid4(),
|
||||
handlers=[],
|
||||
inheritable_handlers=[],
|
||||
tags=[],
|
||||
@@ -1339,7 +1339,7 @@ class CallbackManager(BaseCallbackManager):
|
||||
managers = []
|
||||
for i, prompt in enumerate(prompts):
|
||||
# Can't have duplicate runs with the same run ID (if provided)
|
||||
run_id_ = run_id if i == 0 and run_id is not None else uuid7()
|
||||
run_id_ = run_id if i == 0 and run_id is not None else uuid.uuid4()
|
||||
handle_event(
|
||||
self.handlers,
|
||||
"on_llm_start",
|
||||
@@ -1394,7 +1394,7 @@ class CallbackManager(BaseCallbackManager):
|
||||
run_id_ = run_id
|
||||
run_id = None
|
||||
else:
|
||||
run_id_ = uuid7()
|
||||
run_id_ = uuid.uuid4()
|
||||
handle_event(
|
||||
self.handlers,
|
||||
"on_chat_model_start",
|
||||
@@ -1443,7 +1443,7 @@ class CallbackManager(BaseCallbackManager):
|
||||
|
||||
"""
|
||||
if run_id is None:
|
||||
run_id = uuid7()
|
||||
run_id = uuid.uuid4()
|
||||
handle_event(
|
||||
self.handlers,
|
||||
"on_chain_start",
|
||||
@@ -1498,7 +1498,7 @@ class CallbackManager(BaseCallbackManager):
|
||||
|
||||
"""
|
||||
if run_id is None:
|
||||
run_id = uuid7()
|
||||
run_id = uuid.uuid4()
|
||||
|
||||
handle_event(
|
||||
self.handlers,
|
||||
@@ -1547,7 +1547,7 @@ class CallbackManager(BaseCallbackManager):
|
||||
The callback manager for the retriever run.
|
||||
"""
|
||||
if run_id is None:
|
||||
run_id = uuid7()
|
||||
run_id = uuid.uuid4()
|
||||
|
||||
handle_event(
|
||||
self.handlers,
|
||||
@@ -1607,7 +1607,7 @@ class CallbackManager(BaseCallbackManager):
|
||||
)
|
||||
raise ValueError(msg)
|
||||
if run_id is None:
|
||||
run_id = uuid7()
|
||||
run_id = uuid.uuid4()
|
||||
|
||||
handle_event(
|
||||
self.handlers,
|
||||
@@ -1843,7 +1843,7 @@ class AsyncCallbackManager(BaseCallbackManager):
|
||||
run_id_ = run_id
|
||||
run_id = None
|
||||
else:
|
||||
run_id_ = uuid7()
|
||||
run_id_ = uuid.uuid4()
|
||||
|
||||
if inline_handlers:
|
||||
inline_tasks.append(
|
||||
@@ -1928,7 +1928,7 @@ class AsyncCallbackManager(BaseCallbackManager):
|
||||
run_id_ = run_id
|
||||
run_id = None
|
||||
else:
|
||||
run_id_ = uuid7()
|
||||
run_id_ = uuid.uuid4()
|
||||
|
||||
for handler in self.handlers:
|
||||
task = ahandle_event(
|
||||
@@ -1991,7 +1991,7 @@ class AsyncCallbackManager(BaseCallbackManager):
|
||||
for the chain run.
|
||||
"""
|
||||
if run_id is None:
|
||||
run_id = uuid7()
|
||||
run_id = uuid.uuid4()
|
||||
|
||||
await ahandle_event(
|
||||
self.handlers,
|
||||
@@ -2041,7 +2041,7 @@ class AsyncCallbackManager(BaseCallbackManager):
|
||||
for the tool run.
|
||||
"""
|
||||
if run_id is None:
|
||||
run_id = uuid7()
|
||||
run_id = uuid.uuid4()
|
||||
|
||||
await ahandle_event(
|
||||
self.handlers,
|
||||
@@ -2093,7 +2093,7 @@ class AsyncCallbackManager(BaseCallbackManager):
|
||||
if not self.handlers:
|
||||
return
|
||||
if run_id is None:
|
||||
run_id = uuid7()
|
||||
run_id = uuid.uuid4()
|
||||
|
||||
if kwargs:
|
||||
msg = (
|
||||
@@ -2136,7 +2136,7 @@ class AsyncCallbackManager(BaseCallbackManager):
|
||||
for the retriever run.
|
||||
"""
|
||||
if run_id is None:
|
||||
run_id = uuid7()
|
||||
run_id = uuid.uuid4()
|
||||
|
||||
await ahandle_event(
|
||||
self.handlers,
|
||||
|
||||
@@ -45,6 +45,7 @@ https://python.langchain.com/docs/how_to/custom_llm/
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from langchain_core._import_utils import import_attr
|
||||
from langchain_core.language_models._utils import is_openai_data_block
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.language_models.base import (
|
||||
@@ -85,6 +86,7 @@ __all__ = (
|
||||
"ParrotFakeChatModel",
|
||||
"SimpleChatModel",
|
||||
"get_tokenizer",
|
||||
"is_openai_data_block",
|
||||
)
|
||||
|
||||
_dynamic_imports = {
|
||||
@@ -104,6 +106,7 @@ _dynamic_imports = {
|
||||
"ParrotFakeChatModel": "fake_chat_models",
|
||||
"LLM": "llms",
|
||||
"BaseLLM": "llms",
|
||||
"is_openai_data_block": "_utils",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,49 @@
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Literal,
|
||||
Optional,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from langchain_core.messages import BaseMessage
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.messages.content import (
|
||||
ContentBlock,
|
||||
)
|
||||
|
||||
|
||||
def _is_openai_data_block(block: dict) -> bool:
|
||||
"""Check if the block contains multimodal data in OpenAI Chat Completions format."""
|
||||
def is_openai_data_block(
|
||||
block: dict, filter_: Union[Literal["image", "audio", "file"], None] = None
|
||||
) -> bool:
|
||||
"""Check whether a block contains multimodal data in OpenAI Chat Completions format.
|
||||
|
||||
Supports both data and ID-style blocks (e.g. ``'file_data'`` and ``'file_id'``)
|
||||
|
||||
If additional keys are present, they are ignored / will not affect outcome as long
|
||||
as the required keys are present and valid.
|
||||
|
||||
Args:
|
||||
block: The content block to check.
|
||||
filter_: If provided, only return True for blocks matching this specific type.
|
||||
- "image": Only match image_url blocks
|
||||
- "audio": Only match input_audio blocks
|
||||
- "file": Only match file blocks
|
||||
If None, match any valid OpenAI data block type. Note that this means that
|
||||
if the block has a valid OpenAI data type but the filter_ is set to a
|
||||
different type, this function will return False.
|
||||
|
||||
Returns:
|
||||
True if the block is a valid OpenAI data block and matches the filter_
|
||||
(if provided).
|
||||
|
||||
"""
|
||||
if block.get("type") == "image_url":
|
||||
if filter_ is not None and filter_ != "image":
|
||||
return False
|
||||
if (
|
||||
(set(block.keys()) <= {"type", "image_url", "detail"})
|
||||
and (image_url := block.get("image_url"))
|
||||
@@ -15,29 +51,47 @@ def _is_openai_data_block(block: dict) -> bool:
|
||||
):
|
||||
url = image_url.get("url")
|
||||
if isinstance(url, str):
|
||||
# Required per OpenAI spec
|
||||
return True
|
||||
# Ignore `'detail'` since it's optional and specific to OpenAI
|
||||
|
||||
elif block.get("type") == "input_audio":
|
||||
if filter_ is not None and filter_ != "audio":
|
||||
return False
|
||||
if (audio := block.get("input_audio")) and isinstance(audio, dict):
|
||||
audio_data = audio.get("data")
|
||||
audio_format = audio.get("format")
|
||||
# Both required per OpenAI spec
|
||||
if isinstance(audio_data, str) and isinstance(audio_format, str):
|
||||
return True
|
||||
|
||||
elif block.get("type") == "file":
|
||||
if filter_ is not None and filter_ != "file":
|
||||
return False
|
||||
if (file := block.get("file")) and isinstance(file, dict):
|
||||
file_data = file.get("file_data")
|
||||
if isinstance(file_data, str):
|
||||
return True
|
||||
|
||||
elif block.get("type") == "input_audio":
|
||||
if (input_audio := block.get("input_audio")) and isinstance(input_audio, dict):
|
||||
audio_data = input_audio.get("data")
|
||||
audio_format = input_audio.get("format")
|
||||
if isinstance(audio_data, str) and isinstance(audio_format, str):
|
||||
file_id = file.get("file_id")
|
||||
# Files can be either base64-encoded or pre-uploaded with an ID
|
||||
if isinstance(file_data, str) or isinstance(file_id, str):
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
# Has no `'type'` key
|
||||
return False
|
||||
|
||||
|
||||
def _parse_data_uri(uri: str) -> Optional[dict]:
|
||||
"""Parse a data URI into its components. If parsing fails, return None.
|
||||
class ParsedDataUri(TypedDict):
|
||||
source_type: Literal["base64"]
|
||||
data: str
|
||||
mime_type: str
|
||||
|
||||
|
||||
def _parse_data_uri(uri: str) -> Optional[ParsedDataUri]:
|
||||
"""Parse a data URI into its components.
|
||||
|
||||
If parsing fails, return None. If either MIME type or data is missing, return None.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -57,84 +111,219 @@ def _parse_data_uri(uri: str) -> Optional[dict]:
|
||||
match = re.match(regex, uri)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
mime_type = match.group("mime_type")
|
||||
data = match.group("data")
|
||||
if not mime_type or not data:
|
||||
return None
|
||||
|
||||
return {
|
||||
"source_type": "base64",
|
||||
"data": match.group("data"),
|
||||
"mime_type": match.group("mime_type"),
|
||||
"data": data,
|
||||
"mime_type": mime_type,
|
||||
}
|
||||
|
||||
|
||||
def _convert_openai_format_to_data_block(block: dict) -> dict:
|
||||
"""Convert OpenAI image content block to standard data content block.
|
||||
def _normalize_messages(
|
||||
messages: Sequence["BaseMessage"],
|
||||
) -> list["BaseMessage"]:
|
||||
"""Normalize message formats to LangChain v1 standard content blocks.
|
||||
|
||||
If parsing fails, pass-through.
|
||||
Chat models already implement support for:
|
||||
- Images in OpenAI Chat Completions format
|
||||
These will be passed through unchanged
|
||||
- LangChain v1 standard content blocks
|
||||
|
||||
Args:
|
||||
block: The OpenAI image content block to convert.
|
||||
This function extends support to:
|
||||
- `Audio <https://platform.openai.com/docs/api-reference/chat/create>`__ and
|
||||
`file <https://platform.openai.com/docs/api-reference/files>`__ data in OpenAI
|
||||
Chat Completions format
|
||||
- Images are technically supported but we expect chat models to handle them
|
||||
directly; this may change in the future
|
||||
- LangChain v0 standard content blocks for backward compatibility
|
||||
|
||||
Returns:
|
||||
The converted standard data content block.
|
||||
"""
|
||||
if block["type"] == "image_url":
|
||||
parsed = _parse_data_uri(block["image_url"]["url"])
|
||||
if parsed is not None:
|
||||
parsed["type"] = "image"
|
||||
return parsed
|
||||
return block
|
||||
.. versionchanged:: 1.0.0
|
||||
In previous versions, this function returned messages in LangChain v0 format.
|
||||
Now, it returns messages in LangChain v1 format, which upgraded chat models now
|
||||
expect to receive when passing back in message history. For backward
|
||||
compatibility, this function will convert v0 message content to v1 format.
|
||||
|
||||
if block["type"] == "file":
|
||||
parsed = _parse_data_uri(block["file"]["file_data"])
|
||||
if parsed is not None:
|
||||
parsed["type"] = "file"
|
||||
if filename := block["file"].get("filename"):
|
||||
parsed["filename"] = filename
|
||||
return parsed
|
||||
return block
|
||||
.. dropdown:: v0 Content Block Schemas
|
||||
|
||||
if block["type"] == "input_audio":
|
||||
data = block["input_audio"].get("data")
|
||||
audio_format = block["input_audio"].get("format")
|
||||
if data and audio_format:
|
||||
return {
|
||||
"type": "audio",
|
||||
"source_type": "base64",
|
||||
"data": data,
|
||||
"mime_type": f"audio/{audio_format}",
|
||||
``URLContentBlock``:
|
||||
|
||||
.. codeblock::
|
||||
|
||||
{
|
||||
mime_type: NotRequired[str]
|
||||
type: Literal['image', 'audio', 'file'],
|
||||
source_type: Literal['url'],
|
||||
url: str,
|
||||
}
|
||||
return block
|
||||
|
||||
return block
|
||||
``Base64ContentBlock``:
|
||||
|
||||
.. codeblock::
|
||||
|
||||
def _normalize_messages(messages: Sequence[BaseMessage]) -> list[BaseMessage]:
|
||||
"""Extend support for message formats.
|
||||
{
|
||||
mime_type: NotRequired[str]
|
||||
type: Literal['image', 'audio', 'file'],
|
||||
source_type: Literal['base64'],
|
||||
data: str,
|
||||
}
|
||||
|
||||
``IDContentBlock``:
|
||||
|
||||
(In practice, this was never used)
|
||||
|
||||
.. codeblock::
|
||||
|
||||
{
|
||||
type: Literal['image', 'audio', 'file'],
|
||||
source_type: Literal['id'],
|
||||
id: str,
|
||||
}
|
||||
|
||||
``PlainTextContentBlock``:
|
||||
|
||||
.. codeblock::
|
||||
|
||||
{
|
||||
mime_type: NotRequired[str]
|
||||
type: Literal['file'],
|
||||
source_type: Literal['text'],
|
||||
url: str,
|
||||
}
|
||||
|
||||
If a v1 message is passed in, it will be returned as-is, meaning it is safe to
|
||||
always pass in v1 messages to this function for assurance.
|
||||
|
||||
For posterity, here are the OpenAI Chat Completions schemas we expect:
|
||||
|
||||
Chat Completions image. Can be URL-based or base64-encoded. Supports MIME types
|
||||
png, jpeg/jpg, webp, static gif:
|
||||
{
|
||||
"type": Literal['image_url'],
|
||||
"image_url": {
|
||||
"url": Union["data:$MIME_TYPE;base64,$BASE64_ENCODED_IMAGE", "$IMAGE_URL"],
|
||||
"detail": Literal['low', 'high', 'auto'] = 'auto', # Supported by OpenAI
|
||||
}
|
||||
}
|
||||
|
||||
Chat Completions audio:
|
||||
{
|
||||
"type": Literal['input_audio'],
|
||||
"input_audio": {
|
||||
"format": Literal['wav', 'mp3'],
|
||||
"data": str = "$BASE64_ENCODED_AUDIO",
|
||||
},
|
||||
}
|
||||
|
||||
Chat Completions files: either base64 or pre-uploaded file ID
|
||||
{
|
||||
"type": Literal['file'],
|
||||
"file": Union[
|
||||
{
|
||||
"filename": Optional[str] = "$FILENAME",
|
||||
"file_data": str = "$BASE64_ENCODED_FILE",
|
||||
},
|
||||
{
|
||||
"file_id": str = "$FILE_ID", # For pre-uploaded files to OpenAI
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Chat models implement support for images in OpenAI Chat Completions format, as well
|
||||
as other multimodal data as standard data blocks. This function extends support to
|
||||
audio and file data in OpenAI Chat Completions format by converting them to standard
|
||||
data blocks.
|
||||
"""
|
||||
from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
|
||||
_convert_legacy_v0_content_block_to_v1,
|
||||
)
|
||||
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
|
||||
_convert_openai_format_to_data_block,
|
||||
)
|
||||
|
||||
formatted_messages = []
|
||||
for message in messages:
|
||||
# We preserve input messages - the caller may reuse them elsewhere and expects
|
||||
# them to remain unchanged. We only create a copy if we need to translate.
|
||||
formatted_message = message
|
||||
|
||||
if isinstance(message.content, list):
|
||||
for idx, block in enumerate(message.content):
|
||||
# OpenAI Chat Completions multimodal data blocks to v1 standard
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
# Subset to (PDF) files and audio, as most relevant chat models
|
||||
# support images in OAI format (and some may not yet support the
|
||||
# standard data block format)
|
||||
and block.get("type") in {"file", "input_audio"}
|
||||
and _is_openai_data_block(block)
|
||||
and block.get("type") in {"input_audio", "file"}
|
||||
# Discriminate between OpenAI/LC format since they share `'type'`
|
||||
and is_openai_data_block(block)
|
||||
):
|
||||
if formatted_message is message:
|
||||
formatted_message = message.model_copy()
|
||||
# Also shallow-copy content
|
||||
formatted_message.content = list(formatted_message.content)
|
||||
formatted_message = _ensure_message_copy(message, formatted_message)
|
||||
|
||||
converted_block = _convert_openai_format_to_data_block(block)
|
||||
_update_content_block(formatted_message, idx, converted_block)
|
||||
|
||||
# Convert multimodal LangChain v0 to v1 standard content blocks
|
||||
elif (
|
||||
isinstance(block, dict)
|
||||
and block.get("type")
|
||||
in {
|
||||
"image",
|
||||
"audio",
|
||||
"file",
|
||||
}
|
||||
and block.get("source_type") # v1 doesn't have `source_type`
|
||||
in {
|
||||
"url",
|
||||
"base64",
|
||||
"id",
|
||||
"text",
|
||||
}
|
||||
):
|
||||
formatted_message = _ensure_message_copy(message, formatted_message)
|
||||
|
||||
converted_block = _convert_legacy_v0_content_block_to_v1(block)
|
||||
_update_content_block(formatted_message, idx, converted_block)
|
||||
continue
|
||||
|
||||
# else, pass through blocks that look like they have v1 format unchanged
|
||||
|
||||
formatted_message.content[idx] = ( # type: ignore[index] # mypy confused by .model_copy
|
||||
_convert_openai_format_to_data_block(block)
|
||||
)
|
||||
formatted_messages.append(formatted_message)
|
||||
|
||||
return formatted_messages
|
||||
|
||||
|
||||
T = TypeVar("T", bound="BaseMessage")
|
||||
|
||||
|
||||
def _ensure_message_copy(message: T, formatted_message: T) -> T:
|
||||
"""Create a copy of the message if it hasn't been copied yet."""
|
||||
if formatted_message is message:
|
||||
formatted_message = message.model_copy()
|
||||
# Shallow-copy content list to allow modifications
|
||||
formatted_message.content = list(formatted_message.content)
|
||||
return formatted_message
|
||||
|
||||
|
||||
def _update_content_block(
|
||||
formatted_message: "BaseMessage", idx: int, new_block: Union[ContentBlock, dict]
|
||||
) -> None:
|
||||
"""Update a content block at the given index, handling type issues."""
|
||||
# Type ignore needed because:
|
||||
# - `BaseMessage.content` is typed as `Union[str, list[Union[str, dict]]]`
|
||||
# - When content is str, indexing fails (index error)
|
||||
# - When content is list, the items are `Union[str, dict]` but we're assigning
|
||||
# `Union[ContentBlock, dict]` where ContentBlock is richer than dict
|
||||
# - This is safe because we only call this when we've verified content is a list and
|
||||
# we're doing content block conversions
|
||||
formatted_message.content[idx] = new_block # type: ignore[index, assignment]
|
||||
|
||||
|
||||
def _update_message_content_to_blocks(message: T, output_version: str) -> T:
|
||||
return message.model_copy(
|
||||
update={
|
||||
"content": message.content_blocks,
|
||||
"response_metadata": {
|
||||
**message.response_metadata,
|
||||
"output_version": output_version,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,18 +12,20 @@ from typing import (
|
||||
Callable,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeAlias,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from typing_extensions import TypeAlias, TypedDict, override
|
||||
from typing_extensions import TypedDict, override
|
||||
|
||||
from langchain_core._api import deprecated
|
||||
from langchain_core.caches import BaseCache
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.globals import get_verbose
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AnyMessage,
|
||||
BaseMessage,
|
||||
MessageLikeRepresentation,
|
||||
@@ -101,7 +103,7 @@ def _get_token_ids_default_method(text: str) -> list[int]:
|
||||
LanguageModelInput = Union[PromptValue, str, Sequence[MessageLikeRepresentation]]
|
||||
LanguageModelOutput = Union[BaseMessage, str]
|
||||
LanguageModelLike = Runnable[LanguageModelInput, LanguageModelOutput]
|
||||
LanguageModelOutputVar = TypeVar("LanguageModelOutputVar", BaseMessage, str)
|
||||
LanguageModelOutputVar = TypeVar("LanguageModelOutputVar", AIMessage, str)
|
||||
|
||||
|
||||
def _get_verbosity() -> bool:
|
||||
|
||||
@@ -27,7 +27,10 @@ from langchain_core.callbacks import (
|
||||
Callbacks,
|
||||
)
|
||||
from langchain_core.globals import get_llm_cache
|
||||
from langchain_core.language_models._utils import _normalize_messages
|
||||
from langchain_core.language_models._utils import (
|
||||
_normalize_messages,
|
||||
_update_message_content_to_blocks,
|
||||
)
|
||||
from langchain_core.language_models.base import (
|
||||
BaseLanguageModel,
|
||||
LangSmithParams,
|
||||
@@ -36,16 +39,17 @@ from langchain_core.language_models.base import (
|
||||
from langchain_core.load import dumpd, dumps
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AIMessageChunk,
|
||||
AnyMessage,
|
||||
BaseMessage,
|
||||
BaseMessageChunk,
|
||||
HumanMessage,
|
||||
convert_to_messages,
|
||||
convert_to_openai_image_block,
|
||||
is_data_content_block,
|
||||
message_chunk_to_message,
|
||||
)
|
||||
from langchain_core.messages.ai import _LC_ID_PREFIX
|
||||
from langchain_core.messages.block_translators.openai import (
|
||||
convert_to_openai_image_block,
|
||||
)
|
||||
from langchain_core.output_parsers.openai_tools import (
|
||||
JsonOutputKeyToolsParser,
|
||||
PydanticToolsParser,
|
||||
@@ -69,6 +73,7 @@ from langchain_core.utils.function_calling import (
|
||||
convert_to_openai_tool,
|
||||
)
|
||||
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 uuid
|
||||
@@ -129,7 +134,7 @@ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
|
||||
if (
|
||||
block.get("type") == "image"
|
||||
and is_data_content_block(block)
|
||||
and block.get("source_type") != "id"
|
||||
and not ("file_id" in block or block.get("source_type") == "id")
|
||||
):
|
||||
if message_to_trace is message:
|
||||
# Shallow copy
|
||||
@@ -139,6 +144,22 @@ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
|
||||
message_to_trace.content[idx] = ( # type: ignore[index] # mypy confused by .model_copy
|
||||
convert_to_openai_image_block(block)
|
||||
)
|
||||
elif (
|
||||
block.get("type") == "file"
|
||||
and is_data_content_block(block) # v0 (image/audio/file) or v1
|
||||
and "base64" in block
|
||||
# Backward compat: convert v1 base64 blocks to v0
|
||||
):
|
||||
if message_to_trace is message:
|
||||
# Shallow copy
|
||||
message_to_trace = message.model_copy()
|
||||
message_to_trace.content = list(message_to_trace.content)
|
||||
|
||||
message_to_trace.content[idx] = { # type: ignore[index]
|
||||
**{k: v for k, v in block.items() if k != "base64"},
|
||||
"data": block["base64"],
|
||||
"source_type": "base64",
|
||||
}
|
||||
elif len(block) == 1 and "type" not in block:
|
||||
# Tracing assumes all content blocks have a "type" key. Here
|
||||
# we add this key if it is missing, and there's an obvious
|
||||
@@ -221,7 +242,7 @@ def _format_ls_structured_output(ls_structured_output_format: Optional[dict]) ->
|
||||
return ls_structured_output_format_dict
|
||||
|
||||
|
||||
class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
"""Base class for chat models.
|
||||
|
||||
Key imperative methods:
|
||||
@@ -330,6 +351,28 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
|
||||
"""
|
||||
|
||||
output_version: Optional[str] = Field(
|
||||
default_factory=from_env("LC_OUTPUT_VERSION", default=None)
|
||||
)
|
||||
"""Version of ``AIMessage`` output format to store in message content.
|
||||
|
||||
``AIMessage.content_blocks`` will lazily parse the contents of ``content`` into a
|
||||
standard format. This flag can be used to additionally store the standard format
|
||||
in message content, e.g., for serialization purposes.
|
||||
|
||||
Supported values:
|
||||
|
||||
- ``"v0"``: provider-specific format in content (can lazily-parse with
|
||||
``.content_blocks``)
|
||||
- ``"v1"``: standardized format in content (consistent with ``.content_blocks``)
|
||||
|
||||
Partner packages (e.g., ``langchain-openai``) can also use this field to roll out
|
||||
new content formats in a backward-compatible way.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
"""
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def raise_deprecation(cls, values: dict) -> Any:
|
||||
@@ -388,21 +431,24 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
stop: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> BaseMessage:
|
||||
) -> AIMessage:
|
||||
config = ensure_config(config)
|
||||
return cast(
|
||||
"ChatGeneration",
|
||||
self.generate_prompt(
|
||||
[self._convert_input(input)],
|
||||
stop=stop,
|
||||
callbacks=config.get("callbacks"),
|
||||
tags=config.get("tags"),
|
||||
metadata=config.get("metadata"),
|
||||
run_name=config.get("run_name"),
|
||||
run_id=config.pop("run_id", None),
|
||||
**kwargs,
|
||||
).generations[0][0],
|
||||
).message
|
||||
"AIMessage",
|
||||
cast(
|
||||
"ChatGeneration",
|
||||
self.generate_prompt(
|
||||
[self._convert_input(input)],
|
||||
stop=stop,
|
||||
callbacks=config.get("callbacks"),
|
||||
tags=config.get("tags"),
|
||||
metadata=config.get("metadata"),
|
||||
run_name=config.get("run_name"),
|
||||
run_id=config.pop("run_id", None),
|
||||
**kwargs,
|
||||
).generations[0][0],
|
||||
).message,
|
||||
)
|
||||
|
||||
@override
|
||||
async def ainvoke(
|
||||
@@ -412,7 +458,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
stop: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> BaseMessage:
|
||||
) -> AIMessage:
|
||||
config = ensure_config(config)
|
||||
llm_result = await self.agenerate_prompt(
|
||||
[self._convert_input(input)],
|
||||
@@ -424,7 +470,9 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
run_id=config.pop("run_id", None),
|
||||
**kwargs,
|
||||
)
|
||||
return cast("ChatGeneration", llm_result.generations[0][0]).message
|
||||
return cast(
|
||||
"AIMessage", cast("ChatGeneration", llm_result.generations[0][0]).message
|
||||
)
|
||||
|
||||
def _should_stream(
|
||||
self,
|
||||
@@ -469,11 +517,11 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
stop: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[BaseMessageChunk]:
|
||||
) -> Iterator[AIMessageChunk]:
|
||||
if not self._should_stream(async_api=False, **{**kwargs, "stream": True}):
|
||||
# Model doesn't implement streaming, so use default implementation
|
||||
yield cast(
|
||||
"BaseMessageChunk",
|
||||
"AIMessageChunk",
|
||||
self.invoke(input, config=config, stop=stop, **kwargs),
|
||||
)
|
||||
else:
|
||||
@@ -518,16 +566,41 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
|
||||
try:
|
||||
input_messages = _normalize_messages(messages)
|
||||
run_id = "-".join((_LC_ID_PREFIX, str(run_manager.run_id)))
|
||||
run_id = "-".join((LC_ID_PREFIX, str(run_manager.run_id)))
|
||||
yielded = False
|
||||
for chunk in self._stream(input_messages, stop=stop, **kwargs):
|
||||
if chunk.message.id is None:
|
||||
chunk.message.id = run_id
|
||||
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
chunk.message = _update_message_content_to_blocks(
|
||||
chunk.message, "v1"
|
||||
)
|
||||
run_manager.on_llm_new_token(
|
||||
cast("str", chunk.message.content), chunk=chunk
|
||||
)
|
||||
chunks.append(chunk)
|
||||
yield chunk.message
|
||||
yield cast("AIMessageChunk", chunk.message)
|
||||
yielded = True
|
||||
|
||||
# Yield a final empty chunk with chunk_position="last" if not yet
|
||||
# yielded
|
||||
if (
|
||||
yielded
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not chunk.message.chunk_position
|
||||
):
|
||||
empty_content: Union[str, list] = (
|
||||
"" if isinstance(chunk.message.content, str) else []
|
||||
)
|
||||
msg_chunk = AIMessageChunk(
|
||||
content=empty_content, chunk_position="last", id=run_id
|
||||
)
|
||||
run_manager.on_llm_new_token(
|
||||
"", chunk=ChatGenerationChunk(message=msg_chunk)
|
||||
)
|
||||
yield msg_chunk
|
||||
except BaseException as e:
|
||||
generations_with_error_metadata = _generate_response_from_error(e)
|
||||
chat_generation_chunk = merge_chat_generation_chunks(chunks)
|
||||
@@ -560,11 +633,11 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
stop: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[BaseMessageChunk]:
|
||||
) -> AsyncIterator[AIMessageChunk]:
|
||||
if not self._should_stream(async_api=True, **{**kwargs, "stream": True}):
|
||||
# No async or sync stream is implemented, so fall back to ainvoke
|
||||
yield cast(
|
||||
"BaseMessageChunk",
|
||||
"AIMessageChunk",
|
||||
await self.ainvoke(input, config=config, stop=stop, **kwargs),
|
||||
)
|
||||
return
|
||||
@@ -611,7 +684,8 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
|
||||
try:
|
||||
input_messages = _normalize_messages(messages)
|
||||
run_id = "-".join((_LC_ID_PREFIX, str(run_manager.run_id)))
|
||||
run_id = "-".join((LC_ID_PREFIX, str(run_manager.run_id)))
|
||||
yielded = False
|
||||
async for chunk in self._astream(
|
||||
input_messages,
|
||||
stop=stop,
|
||||
@@ -620,11 +694,34 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
if chunk.message.id is None:
|
||||
chunk.message.id = run_id
|
||||
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
chunk.message = _update_message_content_to_blocks(
|
||||
chunk.message, "v1"
|
||||
)
|
||||
await run_manager.on_llm_new_token(
|
||||
cast("str", chunk.message.content), chunk=chunk
|
||||
)
|
||||
chunks.append(chunk)
|
||||
yield chunk.message
|
||||
yield cast("AIMessageChunk", chunk.message)
|
||||
yielded = True
|
||||
|
||||
# Yield a final empty chunk with chunk_position="last" if not yet yielded
|
||||
if (
|
||||
yielded
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not chunk.message.chunk_position
|
||||
):
|
||||
empty_content: Union[str, list] = (
|
||||
"" if isinstance(chunk.message.content, str) else []
|
||||
)
|
||||
msg_chunk = AIMessageChunk(
|
||||
content=empty_content, chunk_position="last", id=run_id
|
||||
)
|
||||
await run_manager.on_llm_new_token(
|
||||
"", chunk=ChatGenerationChunk(message=msg_chunk)
|
||||
)
|
||||
yield msg_chunk
|
||||
except BaseException as e:
|
||||
generations_with_error_metadata = _generate_response_from_error(e)
|
||||
chat_generation_chunk = merge_chat_generation_chunks(chunks)
|
||||
@@ -1077,15 +1174,43 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
**kwargs,
|
||||
):
|
||||
chunks: list[ChatGenerationChunk] = []
|
||||
run_id: Optional[str] = (
|
||||
f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
|
||||
)
|
||||
yielded = False
|
||||
for chunk in self._stream(messages, stop=stop, **kwargs):
|
||||
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
chunk.message = _update_message_content_to_blocks(
|
||||
chunk.message, "v1"
|
||||
)
|
||||
if run_manager:
|
||||
if chunk.message.id is None:
|
||||
chunk.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}"
|
||||
chunk.message.id = run_id
|
||||
run_manager.on_llm_new_token(
|
||||
cast("str", chunk.message.content), chunk=chunk
|
||||
)
|
||||
chunks.append(chunk)
|
||||
yielded = True
|
||||
|
||||
# Yield a final empty chunk with chunk_position="last" if not yet yielded
|
||||
if (
|
||||
yielded
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not chunk.message.chunk_position
|
||||
):
|
||||
empty_content: Union[str, list] = (
|
||||
"" if isinstance(chunk.message.content, str) else []
|
||||
)
|
||||
chunk = ChatGenerationChunk(
|
||||
message=AIMessageChunk(
|
||||
content=empty_content, chunk_position="last", id=run_id
|
||||
)
|
||||
)
|
||||
if run_manager:
|
||||
run_manager.on_llm_new_token("", chunk=chunk)
|
||||
chunks.append(chunk)
|
||||
result = generate_from_stream(iter(chunks))
|
||||
elif inspect.signature(self._generate).parameters.get("run_manager"):
|
||||
result = self._generate(
|
||||
@@ -1094,10 +1219,17 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
else:
|
||||
result = self._generate(messages, stop=stop, **kwargs)
|
||||
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
for generation in result.generations:
|
||||
generation.message = _update_message_content_to_blocks(
|
||||
generation.message, "v1"
|
||||
)
|
||||
|
||||
# Add response metadata to each generation
|
||||
for idx, generation in enumerate(result.generations):
|
||||
if run_manager and generation.message.id is None:
|
||||
generation.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
|
||||
generation.message.id = f"{LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
|
||||
generation.message.response_metadata = _gen_info_and_msg_metadata(
|
||||
generation
|
||||
)
|
||||
@@ -1150,15 +1282,43 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
**kwargs,
|
||||
):
|
||||
chunks: list[ChatGenerationChunk] = []
|
||||
run_id: Optional[str] = (
|
||||
f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
|
||||
)
|
||||
yielded = False
|
||||
async for chunk in self._astream(messages, stop=stop, **kwargs):
|
||||
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
chunk.message = _update_message_content_to_blocks(
|
||||
chunk.message, "v1"
|
||||
)
|
||||
if run_manager:
|
||||
if chunk.message.id is None:
|
||||
chunk.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}"
|
||||
chunk.message.id = run_id
|
||||
await run_manager.on_llm_new_token(
|
||||
cast("str", chunk.message.content), chunk=chunk
|
||||
)
|
||||
chunks.append(chunk)
|
||||
yielded = True
|
||||
|
||||
# Yield a final empty chunk with chunk_position="last" if not yet yielded
|
||||
if (
|
||||
yielded
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not chunk.message.chunk_position
|
||||
):
|
||||
empty_content: Union[str, list] = (
|
||||
"" if isinstance(chunk.message.content, str) else []
|
||||
)
|
||||
chunk = ChatGenerationChunk(
|
||||
message=AIMessageChunk(
|
||||
content=empty_content, chunk_position="last", id=run_id
|
||||
)
|
||||
)
|
||||
if run_manager:
|
||||
await run_manager.on_llm_new_token("", chunk=chunk)
|
||||
chunks.append(chunk)
|
||||
result = generate_from_stream(iter(chunks))
|
||||
elif inspect.signature(self._agenerate).parameters.get("run_manager"):
|
||||
result = await self._agenerate(
|
||||
@@ -1167,10 +1327,17 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
else:
|
||||
result = await self._agenerate(messages, stop=stop, **kwargs)
|
||||
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
for generation in result.generations:
|
||||
generation.message = _update_message_content_to_blocks(
|
||||
generation.message, "v1"
|
||||
)
|
||||
|
||||
# Add response metadata to each generation
|
||||
for idx, generation in enumerate(result.generations):
|
||||
if run_manager and generation.message.id is None:
|
||||
generation.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
|
||||
generation.message.id = f"{LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
|
||||
generation.message.response_metadata = _gen_info_and_msg_metadata(
|
||||
generation
|
||||
)
|
||||
@@ -1443,7 +1610,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
tool_choice: Optional[Union[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||
) -> Runnable[LanguageModelInput, AIMessage]:
|
||||
"""Bind tools to the model.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import re
|
||||
import time
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from typing import Any, Optional, Union, cast
|
||||
from typing import Any, Literal, Optional, Union, cast
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
@@ -113,7 +113,12 @@ class FakeListChatModel(SimpleChatModel):
|
||||
):
|
||||
raise FakeListChatModelError
|
||||
|
||||
yield ChatGenerationChunk(message=AIMessageChunk(content=c))
|
||||
chunk_position: Optional[Literal["last"]] = (
|
||||
"last" if i_c == len(response) - 1 else None
|
||||
)
|
||||
yield ChatGenerationChunk(
|
||||
message=AIMessageChunk(content=c, chunk_position=chunk_position)
|
||||
)
|
||||
|
||||
@override
|
||||
async def _astream(
|
||||
@@ -136,7 +141,12 @@ class FakeListChatModel(SimpleChatModel):
|
||||
and i_c == self.error_on_chunk_number
|
||||
):
|
||||
raise FakeListChatModelError
|
||||
yield ChatGenerationChunk(message=AIMessageChunk(content=c))
|
||||
chunk_position: Optional[Literal["last"]] = (
|
||||
"last" if i_c == len(response) - 1 else None
|
||||
)
|
||||
yield ChatGenerationChunk(
|
||||
message=AIMessageChunk(content=c, chunk_position=chunk_position)
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -152,7 +162,7 @@ class FakeListChatModel(SimpleChatModel):
|
||||
*,
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> list[BaseMessage]:
|
||||
) -> list[AIMessage]:
|
||||
if isinstance(config, list):
|
||||
return [self.invoke(m, c, **kwargs) for m, c in zip(inputs, config)]
|
||||
return [self.invoke(m, config, **kwargs) for m in inputs]
|
||||
@@ -165,7 +175,7 @@ class FakeListChatModel(SimpleChatModel):
|
||||
*,
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> list[BaseMessage]:
|
||||
) -> list[AIMessage]:
|
||||
if isinstance(config, list):
|
||||
# do Not use an async iterator here because need explicit ordering
|
||||
return [await self.ainvoke(m, c, **kwargs) for m, c in zip(inputs, config)]
|
||||
@@ -284,10 +294,16 @@ class GenericFakeChatModel(BaseChatModel):
|
||||
|
||||
content_chunks = cast("list[str]", re.split(r"(\s)", content))
|
||||
|
||||
for token in content_chunks:
|
||||
for idx, token in enumerate(content_chunks):
|
||||
chunk = ChatGenerationChunk(
|
||||
message=AIMessageChunk(content=token, id=message.id)
|
||||
)
|
||||
if (
|
||||
idx == len(content_chunks) - 1
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not message.additional_kwargs
|
||||
):
|
||||
chunk.message.chunk_position = "last"
|
||||
if run_manager:
|
||||
run_manager.on_llm_new_token(token, chunk=chunk)
|
||||
yield chunk
|
||||
|
||||
@@ -1466,10 +1466,10 @@ class BaseLLM(BaseLanguageModel[str], ABC):
|
||||
prompt_dict = self.dict()
|
||||
|
||||
if save_path.suffix == ".json":
|
||||
with save_path.open("w") as f:
|
||||
with save_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(prompt_dict, f, indent=4)
|
||||
elif save_path.suffix.endswith((".yaml", ".yml")):
|
||||
with save_path.open("w") as f:
|
||||
with save_path.open("w", encoding="utf-8") as f:
|
||||
yaml.dump(prompt_dict, f, default_flow_style=False)
|
||||
else:
|
||||
msg = f"{save_path} must be json or yaml"
|
||||
|
||||
@@ -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 InitValidator, loads
|
||||
from langchain_core.load.load import loads
|
||||
from langchain_core.load.serializable import Serializable
|
||||
|
||||
# Unfortunately, we have to eagerly import load from langchain_core/load/load.py
|
||||
@@ -15,19 +15,11 @@ if TYPE_CHECKING:
|
||||
# the `from langchain_core.load.load import load` absolute import should also work.
|
||||
from langchain_core.load.load import load
|
||||
|
||||
__all__ = (
|
||||
"InitValidator",
|
||||
"Serializable",
|
||||
"dumpd",
|
||||
"dumps",
|
||||
"load",
|
||||
"loads",
|
||||
)
|
||||
__all__ = ("Serializable", "dumpd", "dumps", "load", "loads")
|
||||
|
||||
_dynamic_imports = {
|
||||
"dumpd": "dump",
|
||||
"dumps": "dump",
|
||||
"InitValidator": "load",
|
||||
"loads": "load",
|
||||
"Serializable": "serializable",
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
"""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
|
||||
|
||||
_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.
|
||||
"""
|
||||
from langchain_core.load.serializable import ( # noqa: PLC0415
|
||||
Serializable,
|
||||
to_json_not_implemented,
|
||||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
from langchain_core.load.serializable import Serializable # noqa: PLC0415
|
||||
|
||||
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,26 +1,10 @@
|
||||
"""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.
|
||||
"""
|
||||
"""Dump objects to json."""
|
||||
|
||||
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
|
||||
@@ -41,20 +25,6 @@ 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)
|
||||
@@ -70,18 +40,12 @@ 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 by either 2 spaces or the amount
|
||||
provided in the `indent` kwarg.
|
||||
**kwargs: Additional arguments to pass to `json.dumps`
|
||||
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).
|
||||
Default is False.
|
||||
kwargs: Additional arguments to pass to json.dumps
|
||||
|
||||
Returns:
|
||||
A json string representation of the object.
|
||||
@@ -92,23 +56,25 @@ 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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
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.
|
||||
.. note::
|
||||
Unfortunately this function is not as efficient as it could be because it first
|
||||
dumps the object to a json string and then loads it back into a dictionary.
|
||||
|
||||
Args:
|
||||
obj: The object to dump.
|
||||
@@ -116,5 +82,4 @@ def dumpd(obj: Any) -> Any:
|
||||
Returns:
|
||||
dictionary that can be serialized to json using json.dumps
|
||||
"""
|
||||
obj = _dump_pydantic_models(obj)
|
||||
return _serialize_value(obj)
|
||||
return json.loads(dumps(obj))
|
||||
|
||||
@@ -1,85 +1,11 @@
|
||||
"""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,
|
||||
],
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
"""Load LangChain objects from JSON strings or objects."""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Literal, Optional, cast
|
||||
from typing import Any, Optional
|
||||
|
||||
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,
|
||||
@@ -118,167 +44,12 @@ 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.
|
||||
|
||||
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.
|
||||
"""
|
||||
"""Reviver for JSON objects."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: Optional[dict[str, str]] = None,
|
||||
valid_namespaces: Optional[list[str]] = None,
|
||||
secrets_from_env: bool = True, # noqa: FBT001,FBT002
|
||||
@@ -287,51 +58,22 @@ class Reviver:
|
||||
] = None,
|
||||
*,
|
||||
ignore_unserializable_fields: bool = False,
|
||||
init_validator: InitValidator | None = default_init_validator,
|
||||
) -> None:
|
||||
"""Initialize the reviver.
|
||||
|
||||
Args:
|
||||
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`.
|
||||
|
||||
Defaults to `None`.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces.
|
||||
|
||||
Defaults to `None`.
|
||||
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. Defaults to None.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized. Defaults to None.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
|
||||
Defaults to `True`.
|
||||
additional_import_mappings: A dictionary of additional namespace mappings.
|
||||
|
||||
Defaults to True.
|
||||
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.
|
||||
|
||||
Defaults to `None`.
|
||||
Defaults to None.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
|
||||
Defaults to `False`.
|
||||
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.
|
||||
Defaults to False.
|
||||
"""
|
||||
self.secrets_from_env = secrets_from_env
|
||||
self.secrets_map = secrets_map or {}
|
||||
@@ -350,26 +92,7 @@ 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.
|
||||
@@ -420,20 +143,6 @@ 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.
|
||||
@@ -441,11 +150,13 @@ class Reviver:
|
||||
):
|
||||
msg = f"Invalid namespace: {value}"
|
||||
raise ValueError(msg)
|
||||
# Determine explicit import path
|
||||
# Has 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 "
|
||||
@@ -453,16 +164,9 @@ class Reviver:
|
||||
f"{mapping_key}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
# Otherwise, treat namespace as path.
|
||||
else:
|
||||
# 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))
|
||||
mod = importlib.import_module(".".join(namespace))
|
||||
|
||||
cls = getattr(mod, name)
|
||||
|
||||
@@ -474,10 +178,6 @@ 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
|
||||
@@ -487,76 +187,43 @@ class Reviver:
|
||||
def loads(
|
||||
text: str,
|
||||
*,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: Optional[dict[str, str]] = None,
|
||||
valid_namespaces: Optional[list[str]] = None,
|
||||
secrets_from_env: bool = True,
|
||||
additional_import_mappings: Optional[dict[tuple[str, ...], tuple[str, ...]]] = 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.
|
||||
|
||||
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`. Defaults to None.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces. Defaults to None.
|
||||
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. Defaults to None.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized. Defaults to None.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
Defaults to True.
|
||||
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. Defaults to None.
|
||||
Defaults to None.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
Defaults to False.
|
||||
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.
|
||||
"""
|
||||
# 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,
|
||||
return json.loads(
|
||||
text,
|
||||
object_hook=Reviver(
|
||||
secrets_map,
|
||||
valid_namespaces,
|
||||
secrets_from_env,
|
||||
additional_import_mappings,
|
||||
ignore_unserializable_fields=ignore_unserializable_fields,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -564,107 +231,46 @@ def loads(
|
||||
def load(
|
||||
obj: Any,
|
||||
*,
|
||||
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
|
||||
secrets_map: Optional[dict[str, str]] = None,
|
||||
valid_namespaces: Optional[list[str]] = None,
|
||||
secrets_from_env: bool = True,
|
||||
additional_import_mappings: Optional[dict[tuple[str, ...], tuple[str, ...]]] = 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`.
|
||||
|
||||
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.
|
||||
Use this if you already have a parsed JSON object,
|
||||
eg. from `json.load` or `orjson.loads`.
|
||||
|
||||
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`. Defaults to None.
|
||||
valid_namespaces: Additional namespaces (modules) to allow during
|
||||
deserialization, beyond the default trusted namespaces. Defaults to None.
|
||||
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. Defaults to None.
|
||||
valid_namespaces: A list of additional namespaces (modules)
|
||||
to allow to be deserialized. Defaults to None.
|
||||
secrets_from_env: Whether to load secrets from the environment.
|
||||
Defaults to True.
|
||||
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. Defaults to None.
|
||||
Defaults to None.
|
||||
ignore_unserializable_fields: Whether to ignore unserializable fields.
|
||||
Defaults to False.
|
||||
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):
|
||||
# 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
|
||||
# Need to revive leaf nodes before reviving this node
|
||||
loaded_obj = {k: _load(v) for k, v in obj.items()}
|
||||
return reviver(loaded_obj)
|
||||
if isinstance(obj, list):
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
"""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, 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`.
|
||||
For example,
|
||||
|
||||
The mapping allows us to deserialize an `AIMessage` created with an older
|
||||
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
|
||||
version of LangChain where the code was in a different location.
|
||||
"""
|
||||
|
||||
@@ -273,11 +275,6 @@ 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",
|
||||
@@ -533,6 +530,16 @@ 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",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from langchain_core._import_utils import import_attr
|
||||
from langchain_core.utils.utils import LC_AUTO_PREFIX, LC_ID_PREFIX, ensure_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.messages.ai import (
|
||||
@@ -31,10 +32,29 @@ if TYPE_CHECKING:
|
||||
message_to_dict,
|
||||
messages_to_dict,
|
||||
)
|
||||
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
|
||||
from langchain_core.messages.content_blocks import (
|
||||
from langchain_core.messages.block_translators.openai import (
|
||||
convert_to_openai_data_block,
|
||||
convert_to_openai_image_block,
|
||||
)
|
||||
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
|
||||
from langchain_core.messages.content import (
|
||||
Annotation,
|
||||
AudioContentBlock,
|
||||
Citation,
|
||||
ContentBlock,
|
||||
DataContentBlock,
|
||||
FileContentBlock,
|
||||
ImageContentBlock,
|
||||
InvalidToolCall,
|
||||
NonStandardAnnotation,
|
||||
NonStandardContentBlock,
|
||||
PlainTextContentBlock,
|
||||
ReasoningContentBlock,
|
||||
ServerToolCall,
|
||||
ServerToolCallChunk,
|
||||
ServerToolResult,
|
||||
TextContentBlock,
|
||||
VideoContentBlock,
|
||||
is_data_content_block,
|
||||
)
|
||||
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
|
||||
@@ -42,7 +62,6 @@ if TYPE_CHECKING:
|
||||
from langchain_core.messages.modifier import RemoveMessage
|
||||
from langchain_core.messages.system import SystemMessage, SystemMessageChunk
|
||||
from langchain_core.messages.tool import (
|
||||
InvalidToolCall,
|
||||
ToolCall,
|
||||
ToolCallChunk,
|
||||
ToolMessage,
|
||||
@@ -63,31 +82,50 @@ if TYPE_CHECKING:
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"LC_AUTO_PREFIX",
|
||||
"LC_ID_PREFIX",
|
||||
"AIMessage",
|
||||
"AIMessageChunk",
|
||||
"Annotation",
|
||||
"AnyMessage",
|
||||
"AudioContentBlock",
|
||||
"BaseMessage",
|
||||
"BaseMessageChunk",
|
||||
"ChatMessage",
|
||||
"ChatMessageChunk",
|
||||
"Citation",
|
||||
"ContentBlock",
|
||||
"DataContentBlock",
|
||||
"FileContentBlock",
|
||||
"FunctionMessage",
|
||||
"FunctionMessageChunk",
|
||||
"HumanMessage",
|
||||
"HumanMessageChunk",
|
||||
"ImageContentBlock",
|
||||
"InvalidToolCall",
|
||||
"MessageLikeRepresentation",
|
||||
"NonStandardAnnotation",
|
||||
"NonStandardContentBlock",
|
||||
"PlainTextContentBlock",
|
||||
"ReasoningContentBlock",
|
||||
"RemoveMessage",
|
||||
"ServerToolCall",
|
||||
"ServerToolCallChunk",
|
||||
"ServerToolResult",
|
||||
"SystemMessage",
|
||||
"SystemMessageChunk",
|
||||
"TextContentBlock",
|
||||
"ToolCall",
|
||||
"ToolCallChunk",
|
||||
"ToolMessage",
|
||||
"ToolMessageChunk",
|
||||
"VideoContentBlock",
|
||||
"_message_from_dict",
|
||||
"convert_to_messages",
|
||||
"convert_to_openai_data_block",
|
||||
"convert_to_openai_image_block",
|
||||
"convert_to_openai_messages",
|
||||
"ensure_id",
|
||||
"filter_messages",
|
||||
"get_buffer_string",
|
||||
"is_data_content_block",
|
||||
@@ -103,35 +141,51 @@ __all__ = (
|
||||
_dynamic_imports = {
|
||||
"AIMessage": "ai",
|
||||
"AIMessageChunk": "ai",
|
||||
"Annotation": "content",
|
||||
"AudioContentBlock": "content",
|
||||
"BaseMessage": "base",
|
||||
"BaseMessageChunk": "base",
|
||||
"merge_content": "base",
|
||||
"message_to_dict": "base",
|
||||
"messages_to_dict": "base",
|
||||
"Citation": "content",
|
||||
"ContentBlock": "content",
|
||||
"ChatMessage": "chat",
|
||||
"ChatMessageChunk": "chat",
|
||||
"DataContentBlock": "content",
|
||||
"FileContentBlock": "content",
|
||||
"FunctionMessage": "function",
|
||||
"FunctionMessageChunk": "function",
|
||||
"HumanMessage": "human",
|
||||
"HumanMessageChunk": "human",
|
||||
"NonStandardAnnotation": "content",
|
||||
"NonStandardContentBlock": "content",
|
||||
"PlainTextContentBlock": "content",
|
||||
"ReasoningContentBlock": "content",
|
||||
"RemoveMessage": "modifier",
|
||||
"ServerToolCall": "content",
|
||||
"ServerToolCallChunk": "content",
|
||||
"ServerToolResult": "content",
|
||||
"SystemMessage": "system",
|
||||
"SystemMessageChunk": "system",
|
||||
"ImageContentBlock": "content",
|
||||
"InvalidToolCall": "tool",
|
||||
"TextContentBlock": "content",
|
||||
"ToolCall": "tool",
|
||||
"ToolCallChunk": "tool",
|
||||
"ToolMessage": "tool",
|
||||
"ToolMessageChunk": "tool",
|
||||
"VideoContentBlock": "content",
|
||||
"AnyMessage": "utils",
|
||||
"MessageLikeRepresentation": "utils",
|
||||
"_message_from_dict": "utils",
|
||||
"convert_to_messages": "utils",
|
||||
"convert_to_openai_data_block": "content_blocks",
|
||||
"convert_to_openai_image_block": "content_blocks",
|
||||
"convert_to_openai_data_block": "block_translators.openai",
|
||||
"convert_to_openai_image_block": "block_translators.openai",
|
||||
"convert_to_openai_messages": "utils",
|
||||
"filter_messages": "utils",
|
||||
"get_buffer_string": "utils",
|
||||
"is_data_content_block": "content_blocks",
|
||||
"is_data_content_block": "content",
|
||||
"merge_message_runs": "utils",
|
||||
"message_chunk_to_message": "utils",
|
||||
"messages_from_dict": "utils",
|
||||
|
||||
@@ -3,42 +3,37 @@
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
from typing import Any, Literal, Optional, Union, cast
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Literal, Optional, Union, cast, overload
|
||||
|
||||
from pydantic import model_validator
|
||||
from typing_extensions import NotRequired, Self, TypedDict, override
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.base import (
|
||||
BaseMessage,
|
||||
BaseMessageChunk,
|
||||
_extract_reasoning_from_additional_kwargs,
|
||||
merge_content,
|
||||
)
|
||||
from langchain_core.messages.content import InvalidToolCall
|
||||
from langchain_core.messages.tool import (
|
||||
InvalidToolCall,
|
||||
ToolCall,
|
||||
ToolCallChunk,
|
||||
default_tool_chunk_parser,
|
||||
default_tool_parser,
|
||||
)
|
||||
from langchain_core.messages.tool import (
|
||||
invalid_tool_call as create_invalid_tool_call,
|
||||
)
|
||||
from langchain_core.messages.tool import (
|
||||
tool_call as create_tool_call,
|
||||
)
|
||||
from langchain_core.messages.tool import (
|
||||
tool_call_chunk as create_tool_call_chunk,
|
||||
)
|
||||
from langchain_core.messages.tool import invalid_tool_call as create_invalid_tool_call
|
||||
from langchain_core.messages.tool import tool_call as create_tool_call
|
||||
from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk
|
||||
from langchain_core.utils._merge import merge_dicts, merge_lists
|
||||
from langchain_core.utils.json import parse_partial_json
|
||||
from langchain_core.utils.usage import _dict_int_op
|
||||
from langchain_core.utils.utils import LC_AUTO_PREFIX, LC_ID_PREFIX
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_LC_ID_PREFIX = "run-"
|
||||
|
||||
|
||||
class InputTokenDetails(TypedDict, total=False):
|
||||
"""Breakdown of input token counts.
|
||||
|
||||
@@ -162,13 +157,6 @@ class AIMessage(BaseMessage):
|
||||
|
||||
"""
|
||||
|
||||
example: bool = False
|
||||
"""Use to denote that a message is part of an example conversation.
|
||||
|
||||
At the moment, this is ignored by most models. Usage is discouraged.
|
||||
|
||||
"""
|
||||
|
||||
tool_calls: list[ToolCall] = []
|
||||
"""If provided, tool calls associated with the message."""
|
||||
invalid_tool_calls: list[InvalidToolCall] = []
|
||||
@@ -181,20 +169,52 @@ class AIMessage(BaseMessage):
|
||||
"""
|
||||
|
||||
type: Literal["ai"] = "ai"
|
||||
"""The type of the message (used for deserialization). Defaults to ``'ai'``."""
|
||||
"""The type of the message (used for deserialization). Defaults to "ai"."""
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Union[str, list[Union[str, dict]]],
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize ``AIMessage``.
|
||||
|
||||
Specify ``content`` as positional arg or ``content_blocks`` for typing.
|
||||
|
||||
Args:
|
||||
content: The content of the message.
|
||||
content_blocks: Typed standard content.
|
||||
kwargs: Additional arguments to pass to the parent class.
|
||||
"""
|
||||
super().__init__(content=content, **kwargs)
|
||||
if content_blocks is not None:
|
||||
# If there are tool calls in content_blocks, but not in tool_calls, add them
|
||||
content_tool_calls = [
|
||||
block for block in content_blocks if block.get("type") == "tool_call"
|
||||
]
|
||||
if content_tool_calls and "tool_calls" not in kwargs:
|
||||
kwargs["tool_calls"] = content_tool_calls
|
||||
|
||||
super().__init__(
|
||||
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
@property
|
||||
def lc_attributes(self) -> dict:
|
||||
@@ -204,6 +224,65 @@ class AIMessage(BaseMessage):
|
||||
"invalid_tool_calls": self.invalid_tool_calls,
|
||||
}
|
||||
|
||||
@property
|
||||
def content_blocks(self) -> list[types.ContentBlock]:
|
||||
"""Return content blocks of the message.
|
||||
|
||||
If the message has a known model provider, use the provider-specific translator
|
||||
first before falling back to best-effort parsing. For details, see the property
|
||||
on ``BaseMessage``.
|
||||
"""
|
||||
if self.response_metadata.get("output_version") == "v1":
|
||||
return cast("list[types.ContentBlock]", self.content)
|
||||
|
||||
model_provider = self.response_metadata.get("model_provider")
|
||||
if model_provider:
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
get_translator,
|
||||
)
|
||||
|
||||
translator = get_translator(model_provider)
|
||||
if translator:
|
||||
try:
|
||||
return translator["translate_content"](self)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
# Otherwise, use best-effort parsing
|
||||
blocks = super().content_blocks
|
||||
|
||||
if self.tool_calls:
|
||||
# Add from tool_calls if missing from content
|
||||
content_tool_call_ids = {
|
||||
block.get("id")
|
||||
for block in self.content
|
||||
if isinstance(block, dict) and block.get("type") == "tool_call"
|
||||
}
|
||||
for tool_call in self.tool_calls:
|
||||
if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:
|
||||
tool_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"id": id_,
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
}
|
||||
if "index" in tool_call:
|
||||
tool_call_block["index"] = tool_call["index"] # type: ignore[typeddict-item]
|
||||
if "extras" in tool_call:
|
||||
tool_call_block["extras"] = tool_call["extras"] # type: ignore[typeddict-item]
|
||||
blocks.append(tool_call_block)
|
||||
|
||||
# Best-effort reasoning extraction from additional_kwargs
|
||||
# Only add reasoning if not already present
|
||||
# Insert before all other blocks to keep reasoning at the start
|
||||
has_reasoning = any(block.get("type") == "reasoning" for block in blocks)
|
||||
if not has_reasoning and (
|
||||
reasoning_block := _extract_reasoning_from_additional_kwargs(self)
|
||||
):
|
||||
blocks.insert(0, reasoning_block)
|
||||
|
||||
return blocks
|
||||
|
||||
# TODO: remove this logic if possible, reducing breaking nature of changes
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -232,7 +311,9 @@ class AIMessage(BaseMessage):
|
||||
# Ensure "type" is properly set on all tool call-like dicts.
|
||||
if tool_calls := values.get("tool_calls"):
|
||||
values["tool_calls"] = [
|
||||
create_tool_call(**{k: v for k, v in tc.items() if k != "type"})
|
||||
create_tool_call(
|
||||
**{k: v for k, v in tc.items() if k not in ("type", "extras")}
|
||||
)
|
||||
for tc in tool_calls
|
||||
]
|
||||
if invalid_tool_calls := values.get("invalid_tool_calls"):
|
||||
@@ -307,6 +388,13 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
tool_call_chunks: list[ToolCallChunk] = []
|
||||
"""If provided, tool call chunks associated with the message."""
|
||||
|
||||
chunk_position: Optional[Literal["last"]] = None
|
||||
"""Optional span represented by an aggregated AIMessageChunk.
|
||||
|
||||
If a chunk with ``chunk_position="last"`` is aggregated into a stream,
|
||||
``tool_call_chunks`` in message content will be parsed into ``tool_calls``.
|
||||
"""
|
||||
|
||||
@property
|
||||
def lc_attributes(self) -> dict:
|
||||
"""Attrs to be serialized even if they are derived from other init args."""
|
||||
@@ -315,6 +403,60 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
"invalid_tool_calls": self.invalid_tool_calls,
|
||||
}
|
||||
|
||||
@property
|
||||
def content_blocks(self) -> list[types.ContentBlock]:
|
||||
"""Return content blocks of the message."""
|
||||
if self.response_metadata.get("output_version") == "v1":
|
||||
return cast("list[types.ContentBlock]", self.content)
|
||||
|
||||
model_provider = self.response_metadata.get("model_provider")
|
||||
if model_provider:
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
get_translator,
|
||||
)
|
||||
|
||||
translator = get_translator(model_provider)
|
||||
if translator:
|
||||
try:
|
||||
return translator["translate_content_chunk"](self)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
# Otherwise, use best-effort parsing
|
||||
blocks = super().content_blocks
|
||||
|
||||
if (
|
||||
self.tool_call_chunks
|
||||
and not self.content
|
||||
and self.chunk_position != "last" # keep tool_calls if aggregated
|
||||
):
|
||||
blocks = [
|
||||
block
|
||||
for block in blocks
|
||||
if block["type"] not in ("tool_call", "invalid_tool_call")
|
||||
]
|
||||
for tool_call_chunk in self.tool_call_chunks:
|
||||
tc: types.ToolCallChunk = {
|
||||
"type": "tool_call_chunk",
|
||||
"id": tool_call_chunk.get("id"),
|
||||
"name": tool_call_chunk.get("name"),
|
||||
"args": tool_call_chunk.get("args"),
|
||||
}
|
||||
if (idx := tool_call_chunk.get("index")) is not None:
|
||||
tc["index"] = idx
|
||||
blocks.append(tc)
|
||||
|
||||
# Best-effort reasoning extraction from additional_kwargs
|
||||
# Only add reasoning if not already present
|
||||
# Insert before all other blocks to keep reasoning at the start
|
||||
has_reasoning = any(block.get("type") == "reasoning" for block in blocks)
|
||||
if not has_reasoning and (
|
||||
reasoning_block := _extract_reasoning_from_additional_kwargs(self)
|
||||
):
|
||||
blocks.insert(0, reasoning_block)
|
||||
|
||||
return blocks
|
||||
|
||||
@model_validator(mode="after")
|
||||
def init_tool_calls(self) -> Self:
|
||||
"""Initialize tool calls from tool call chunks.
|
||||
@@ -379,10 +521,70 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
||||
add_chunk_to_invalid_tool_calls(chunk)
|
||||
self.tool_calls = tool_calls
|
||||
self.invalid_tool_calls = invalid_tool_calls
|
||||
|
||||
if (
|
||||
self.chunk_position == "last"
|
||||
and self.tool_call_chunks
|
||||
and self.response_metadata.get("output_version") == "v1"
|
||||
and isinstance(self.content, list)
|
||||
):
|
||||
id_to_tc: dict[str, types.ToolCall] = {
|
||||
cast("str", tc.get("id")): {
|
||||
"type": "tool_call",
|
||||
"name": tc["name"],
|
||||
"args": tc["args"],
|
||||
"id": tc.get("id"),
|
||||
}
|
||||
for tc in self.tool_calls
|
||||
if "id" in tc
|
||||
}
|
||||
for idx, block in enumerate(self.content):
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and block.get("type") == "tool_call_chunk"
|
||||
and (call_id := block.get("id"))
|
||||
and call_id in id_to_tc
|
||||
):
|
||||
self.content[idx] = cast("dict[str, Any]", id_to_tc[call_id])
|
||||
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def init_server_tool_calls(self) -> Self:
|
||||
"""Parse server_tool_call_chunks."""
|
||||
if (
|
||||
self.chunk_position == "last"
|
||||
and self.response_metadata.get("output_version") == "v1"
|
||||
and isinstance(self.content, list)
|
||||
):
|
||||
for idx, block in enumerate(self.content):
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and block.get("type")
|
||||
in ("server_tool_call", "server_tool_call_chunk")
|
||||
and (args_str := block.get("args"))
|
||||
and isinstance(args_str, str)
|
||||
):
|
||||
try:
|
||||
args = json.loads(args_str)
|
||||
if isinstance(args, dict):
|
||||
self.content[idx]["type"] = "server_tool_call" # type: ignore[index]
|
||||
self.content[idx]["args"] = args # type: ignore[index]
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return self
|
||||
|
||||
@overload # type: ignore[override] # summing BaseMessages gives ChatPromptTemplate
|
||||
def __add__(self, other: "AIMessageChunk") -> "AIMessageChunk": ...
|
||||
|
||||
@overload
|
||||
def __add__(self, other: Sequence["AIMessageChunk"]) -> "AIMessageChunk": ...
|
||||
|
||||
@overload
|
||||
def __add__(self, other: Any) -> BaseMessageChunk: ...
|
||||
|
||||
@override
|
||||
def __add__(self, other: Any) -> BaseMessageChunk: # type: ignore[override]
|
||||
def __add__(self, other: Any) -> BaseMessageChunk:
|
||||
if isinstance(other, AIMessageChunk):
|
||||
return add_ai_message_chunks(self, other)
|
||||
if isinstance(other, (list, tuple)) and all(
|
||||
@@ -401,17 +603,10 @@ def add_ai_message_chunks(
|
||||
left: The first ``AIMessageChunk``.
|
||||
*others: Other ``AIMessageChunk``s to add.
|
||||
|
||||
Raises:
|
||||
ValueError: If the example values of the chunks are not the same.
|
||||
|
||||
Returns:
|
||||
The resulting ``AIMessageChunk``.
|
||||
|
||||
"""
|
||||
if any(left.example != o.example for o in others):
|
||||
msg = "Cannot concatenate AIMessageChunks with different example values."
|
||||
raise ValueError(msg)
|
||||
|
||||
content = merge_content(left.content, *(o.content for o in others))
|
||||
additional_kwargs = merge_dicts(
|
||||
left.additional_kwargs, *(o.additional_kwargs for o in others)
|
||||
@@ -446,26 +641,40 @@ def add_ai_message_chunks(
|
||||
|
||||
chunk_id = None
|
||||
candidates = [left.id] + [o.id for o in others]
|
||||
# first pass: pick the first non-run-* id
|
||||
# first pass: pick the first provider-assigned id (non-run-* and non-lc_*)
|
||||
for id_ in candidates:
|
||||
if id_ and not id_.startswith(_LC_ID_PREFIX):
|
||||
if (
|
||||
id_
|
||||
and not id_.startswith(LC_ID_PREFIX)
|
||||
and not id_.startswith(LC_AUTO_PREFIX)
|
||||
):
|
||||
chunk_id = id_
|
||||
break
|
||||
else:
|
||||
# second pass: no provider-assigned id found, just take the first non-null
|
||||
# second pass: prefer lc_run-* ids over lc_* ids
|
||||
for id_ in candidates:
|
||||
if id_:
|
||||
if id_ and id_.startswith(LC_ID_PREFIX):
|
||||
chunk_id = id_
|
||||
break
|
||||
else:
|
||||
# third pass: take any remaining id (auto-generated lc_* ids)
|
||||
for id_ in candidates:
|
||||
if id_:
|
||||
chunk_id = id_
|
||||
break
|
||||
|
||||
chunk_position: Optional[Literal["last"]] = (
|
||||
"last" if any(x.chunk_position == "last" for x in [left, *others]) else None
|
||||
)
|
||||
|
||||
return left.__class__(
|
||||
example=left.example,
|
||||
content=content,
|
||||
additional_kwargs=additional_kwargs,
|
||||
tool_call_chunks=tool_call_chunks,
|
||||
response_metadata=response_metadata,
|
||||
usage_metadata=usage_metadata,
|
||||
id=chunk_id,
|
||||
chunk_position=chunk_position,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
from typing_extensions import Self
|
||||
|
||||
from langchain_core._api.deprecation import warn_deprecated
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.utils import get_bolded_text
|
||||
from langchain_core.utils._merge import merge_dicts, merge_lists
|
||||
from langchain_core.utils.interactive_env import is_interactive_env
|
||||
@@ -17,10 +20,79 @@ if TYPE_CHECKING:
|
||||
from langchain_core.prompts.chat import ChatPromptTemplate
|
||||
|
||||
|
||||
def _extract_reasoning_from_additional_kwargs(
|
||||
message: BaseMessage,
|
||||
) -> Optional[types.ReasoningContentBlock]:
|
||||
"""Extract `reasoning_content` from `additional_kwargs`.
|
||||
|
||||
Handles reasoning content stored in various formats:
|
||||
- `additional_kwargs["reasoning_content"]` (string) - Ollama, DeepSeek, XAI, Groq
|
||||
|
||||
Args:
|
||||
message: The message to extract reasoning from.
|
||||
|
||||
Returns:
|
||||
A `ReasoningContentBlock` if reasoning content is found, None otherwise.
|
||||
"""
|
||||
additional_kwargs = getattr(message, "additional_kwargs", {})
|
||||
|
||||
reasoning_content = additional_kwargs.get("reasoning_content")
|
||||
if reasoning_content is not None and isinstance(reasoning_content, str):
|
||||
return {"type": "reasoning", "reasoning": reasoning_content}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class TextAccessor(str):
|
||||
"""String-like object that supports both property and method access patterns.
|
||||
|
||||
Exists to maintain backward compatibility while transitioning from method-based to
|
||||
property-based text access in message objects. In LangChain <v1.0, message text was
|
||||
accessed via ``.text()`` method calls. In v1.0=<, the preferred pattern is property
|
||||
access via ``.text``.
|
||||
|
||||
Rather than breaking existing code immediately, ``TextAccessor`` allows both
|
||||
patterns:
|
||||
- Modern property access: ``message.text`` (returns string directly)
|
||||
- Legacy method access: ``message.text()`` (callable, emits deprecation warning)
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, value: str) -> Self:
|
||||
"""Create new TextAccessor instance."""
|
||||
return str.__new__(cls, value)
|
||||
|
||||
def __call__(self) -> str:
|
||||
"""Enable method-style text access for backward compatibility.
|
||||
|
||||
This method exists solely to support legacy code that calls ``.text()``
|
||||
as a method. New code should use property access (``.text``) instead.
|
||||
|
||||
.. deprecated:: 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.
|
||||
|
||||
Returns:
|
||||
The string content, identical to property access.
|
||||
|
||||
"""
|
||||
warn_deprecated(
|
||||
since="1.0.0",
|
||||
message=(
|
||||
"Calling .text() as a method is deprecated. "
|
||||
"Use .text as a property instead (e.g., message.text)."
|
||||
),
|
||||
removal="2.0.0",
|
||||
)
|
||||
return str(self)
|
||||
|
||||
|
||||
class BaseMessage(Serializable):
|
||||
"""Base abstract message class.
|
||||
|
||||
Messages are the inputs and outputs of ``ChatModel``s.
|
||||
Messages are the inputs and outputs of a ``ChatModel``.
|
||||
"""
|
||||
|
||||
content: Union[str, list[Union[str, dict]]]
|
||||
@@ -66,17 +138,40 @@ class BaseMessage(Serializable):
|
||||
extra="allow",
|
||||
)
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Union[str, list[Union[str, dict]]],
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize ``BaseMessage``.
|
||||
|
||||
Specify ``content`` as positional arg or ``content_blocks`` for typing.
|
||||
|
||||
Args:
|
||||
content: The string contents of the message.
|
||||
content_blocks: Typed standard content.
|
||||
kwargs: Additional arguments to pass to the parent class.
|
||||
"""
|
||||
super().__init__(content=content, **kwargs)
|
||||
if content_blocks is not None:
|
||||
super().__init__(content=content_blocks, **kwargs)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
@@ -96,26 +191,102 @@ class BaseMessage(Serializable):
|
||||
"""
|
||||
return ["langchain", "schema", "messages"]
|
||||
|
||||
def text(self) -> str:
|
||||
"""Get the text ``content`` of the message.
|
||||
@property
|
||||
def content_blocks(self) -> list[types.ContentBlock]:
|
||||
r"""Load content blocks from the message content.
|
||||
|
||||
.. versionadded:: 1.0.0
|
||||
|
||||
"""
|
||||
# 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,
|
||||
)
|
||||
from langchain_core.messages.block_translators.bedrock_converse import ( # noqa: PLC0415
|
||||
_convert_to_v1_from_converse_input,
|
||||
)
|
||||
from langchain_core.messages.block_translators.google_genai import ( # noqa: PLC0415
|
||||
_convert_to_v1_from_genai_input,
|
||||
)
|
||||
from langchain_core.messages.block_translators.google_vertexai import ( # noqa: PLC0415
|
||||
_convert_to_v1_from_vertexai_input,
|
||||
)
|
||||
from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
|
||||
_convert_v0_multimodal_input_to_v1,
|
||||
)
|
||||
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
|
||||
_convert_to_v1_from_chat_completions_input,
|
||||
)
|
||||
|
||||
blocks: list[types.ContentBlock] = []
|
||||
content = (
|
||||
# Transpose string content to list, otherwise assumed to be list
|
||||
[self.content]
|
||||
if isinstance(self.content, str) and self.content
|
||||
else self.content
|
||||
)
|
||||
for item in content:
|
||||
if isinstance(item, str):
|
||||
# Plain string content is treated as a text block
|
||||
blocks.append({"type": "text", "text": item})
|
||||
elif isinstance(item, dict):
|
||||
item_type = item.get("type")
|
||||
if item_type not in types.KNOWN_BLOCK_TYPES:
|
||||
# Handle all provider-specific or None type blocks as non-standard -
|
||||
# we'll come back to these later
|
||||
blocks.append({"type": "non_standard", "value": item})
|
||||
else:
|
||||
# Guard against v0 blocks that share the same `type` keys
|
||||
if "source_type" in item:
|
||||
blocks.append({"type": "non_standard", "value": item})
|
||||
continue
|
||||
|
||||
# This can't be a v0 block (since they require `source_type`),
|
||||
# so it's a known v1 block type
|
||||
blocks.append(cast("types.ContentBlock", item))
|
||||
|
||||
# Subsequent passes: attempt to unpack non-standard blocks.
|
||||
# This is the last stop - if we can't parse it here, it is left as non-standard
|
||||
for parsing_step in [
|
||||
_convert_v0_multimodal_input_to_v1,
|
||||
_convert_to_v1_from_chat_completions_input,
|
||||
_convert_to_v1_from_anthropic_input,
|
||||
_convert_to_v1_from_genai_input,
|
||||
_convert_to_v1_from_vertexai_input,
|
||||
_convert_to_v1_from_converse_input,
|
||||
]:
|
||||
blocks = parsing_step(blocks)
|
||||
return blocks
|
||||
|
||||
@property
|
||||
def text(self) -> TextAccessor:
|
||||
"""Get the text content of the message as a string.
|
||||
|
||||
Can be used as both property (``message.text``) and method (``message.text()``).
|
||||
|
||||
.. deprecated:: 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.
|
||||
|
||||
Returns:
|
||||
The text content of the message.
|
||||
|
||||
"""
|
||||
if isinstance(self.content, str):
|
||||
return self.content
|
||||
|
||||
# must be a list
|
||||
blocks = [
|
||||
block
|
||||
for block in self.content
|
||||
if isinstance(block, str)
|
||||
or (block.get("type") == "text" and isinstance(block.get("text"), str))
|
||||
]
|
||||
return "".join(
|
||||
block if isinstance(block, str) else block["text"] for block in blocks
|
||||
)
|
||||
text_value = self.content
|
||||
else:
|
||||
# must be a list
|
||||
blocks = [
|
||||
block
|
||||
for block in self.content
|
||||
if isinstance(block, str)
|
||||
or (block.get("type") == "text" and isinstance(block.get("text"), str))
|
||||
]
|
||||
text_value = "".join(
|
||||
block if isinstance(block, str) else block["text"] for block in blocks
|
||||
)
|
||||
return TextAccessor(text_value)
|
||||
|
||||
def __add__(self, other: Any) -> ChatPromptTemplate:
|
||||
"""Concatenate this message with another message.
|
||||
@@ -171,7 +342,9 @@ def merge_content(
|
||||
The merged content.
|
||||
|
||||
"""
|
||||
merged = first_content
|
||||
merged: Union[str, list[Union[str, dict]]]
|
||||
merged = "" if first_content is None else first_content
|
||||
|
||||
for content in contents:
|
||||
# If current is a string
|
||||
if isinstance(merged, str):
|
||||
@@ -190,8 +363,10 @@ def merge_content(
|
||||
elif merged and isinstance(merged[-1], str):
|
||||
merged[-1] += content
|
||||
# If second content is an empty string, treat as a no-op
|
||||
elif content:
|
||||
# Otherwise, add the second content as a new element of the list
|
||||
elif content == "":
|
||||
pass
|
||||
# Otherwise, add the second content as a new element of the list
|
||||
elif merged:
|
||||
merged.append(content)
|
||||
return merged
|
||||
|
||||
|
||||
110
libs/core/langchain_core/messages/block_translators/__init__.py
Normal file
110
libs/core/langchain_core/messages/block_translators/__init__.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Derivations of standard content blocks from provider content.
|
||||
|
||||
``AIMessage`` will first attempt to use a provider-specific translator if
|
||||
``model_provider`` is set in ``response_metadata`` on the message. Consequently, each
|
||||
provider translator must handle all possible content response types from the provider,
|
||||
including text.
|
||||
|
||||
If no provider is set, or if the provider does not have a registered translator,
|
||||
``AIMessage`` will fall back to best-effort parsing of the content into blocks using
|
||||
the implementation in ``BaseMessage``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
# Provider to translator mapping
|
||||
PROVIDER_TRANSLATORS: dict[str, dict[str, Callable[..., list[types.ContentBlock]]]] = {}
|
||||
"""Map model provider names to translator functions.
|
||||
|
||||
The dictionary maps provider names (e.g. ``'openai'``, ``'anthropic'``) to another
|
||||
dictionary with two keys:
|
||||
- ``'translate_content'``: Function to translate ``AIMessage`` content.
|
||||
- ``'translate_content_chunk'``: Function to translate ``AIMessageChunk`` content.
|
||||
|
||||
When calling `.content_blocks` on an ``AIMessage`` or ``AIMessageChunk``, if
|
||||
``model_provider`` is set in ``response_metadata``, the corresponding translator
|
||||
functions will be used to parse the content into blocks. Otherwise, best-effort parsing
|
||||
in ``BaseMessage`` will be used.
|
||||
"""
|
||||
|
||||
|
||||
def register_translator(
|
||||
provider: str,
|
||||
translate_content: Callable[[AIMessage], list[types.ContentBlock]],
|
||||
translate_content_chunk: Callable[[AIMessageChunk], list[types.ContentBlock]],
|
||||
) -> None:
|
||||
"""Register content translators for a provider in `PROVIDER_TRANSLATORS`.
|
||||
|
||||
Args:
|
||||
provider: The model provider name (e.g. ``'openai'``, ``'anthropic'``).
|
||||
translate_content: Function to translate ``AIMessage`` content.
|
||||
translate_content_chunk: Function to translate ``AIMessageChunk`` content.
|
||||
"""
|
||||
PROVIDER_TRANSLATORS[provider] = {
|
||||
"translate_content": translate_content,
|
||||
"translate_content_chunk": translate_content_chunk,
|
||||
}
|
||||
|
||||
|
||||
def get_translator(
|
||||
provider: str,
|
||||
) -> dict[str, Callable[..., list[types.ContentBlock]]] | None:
|
||||
"""Get the translator functions for a provider.
|
||||
|
||||
Args:
|
||||
provider: The model provider name.
|
||||
|
||||
Returns:
|
||||
Dictionary with ``'translate_content'`` and ``'translate_content_chunk'``
|
||||
functions, or None if no translator is registered for the provider. In such
|
||||
case, best-effort parsing in ``BaseMessage`` will be used.
|
||||
"""
|
||||
return PROVIDER_TRANSLATORS.get(provider)
|
||||
|
||||
|
||||
def _register_translators() -> None:
|
||||
"""Register all translators in langchain-core.
|
||||
|
||||
A unit test ensures all modules in ``block_translators`` are represented here.
|
||||
|
||||
For translators implemented outside langchain-core, they can be registered by
|
||||
calling ``register_translator`` from within the integration package.
|
||||
"""
|
||||
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
|
||||
_register_anthropic_translator,
|
||||
)
|
||||
from langchain_core.messages.block_translators.bedrock import ( # noqa: PLC0415
|
||||
_register_bedrock_translator,
|
||||
)
|
||||
from langchain_core.messages.block_translators.bedrock_converse import ( # noqa: PLC0415
|
||||
_register_bedrock_converse_translator,
|
||||
)
|
||||
from langchain_core.messages.block_translators.google_genai import ( # noqa: PLC0415
|
||||
_register_google_genai_translator,
|
||||
)
|
||||
from langchain_core.messages.block_translators.google_vertexai import ( # noqa: PLC0415
|
||||
_register_google_vertexai_translator,
|
||||
)
|
||||
from langchain_core.messages.block_translators.groq import ( # noqa: PLC0415
|
||||
_register_groq_translator,
|
||||
)
|
||||
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
|
||||
_register_openai_translator,
|
||||
)
|
||||
|
||||
_register_bedrock_translator()
|
||||
_register_bedrock_converse_translator()
|
||||
_register_anthropic_translator()
|
||||
_register_google_genai_translator()
|
||||
_register_google_vertexai_translator()
|
||||
_register_groq_translator()
|
||||
_register_openai_translator()
|
||||
|
||||
|
||||
_register_translators()
|
||||
470
libs/core/langchain_core/messages/block_translators/anthropic.py
Normal file
470
libs/core/langchain_core/messages/block_translators/anthropic.py
Normal file
@@ -0,0 +1,470 @@
|
||||
"""Derivations of standard content blocks from Anthropic content."""
|
||||
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Optional, Union, cast
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
|
||||
def _populate_extras(
|
||||
standard_block: types.ContentBlock, block: dict[str, Any], known_fields: set[str]
|
||||
) -> types.ContentBlock:
|
||||
"""Mutate a block, populating extras."""
|
||||
if standard_block.get("type") == "non_standard":
|
||||
return standard_block
|
||||
|
||||
for key, value in block.items():
|
||||
if key not in known_fields:
|
||||
if "extras" not in standard_block:
|
||||
# Below type-ignores are because mypy thinks a non-standard block can
|
||||
# get here, although we exclude them above.
|
||||
standard_block["extras"] = {} # type: ignore[typeddict-unknown-key]
|
||||
standard_block["extras"][key] = value # type: ignore[typeddict-item]
|
||||
|
||||
return standard_block
|
||||
|
||||
|
||||
def _convert_to_v1_from_anthropic_input(
|
||||
content: list[types.ContentBlock],
|
||||
) -> list[types.ContentBlock]:
|
||||
"""Convert Anthropic format blocks to v1 format.
|
||||
|
||||
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
|
||||
block as a ``'non_standard'`` block with the original block stored in the ``value``
|
||||
field. This function attempts to unpack those blocks and convert any blocks that
|
||||
might be Anthropic format to v1 ContentBlocks.
|
||||
|
||||
If conversion fails, the block is left as a ``'non_standard'`` block.
|
||||
|
||||
Args:
|
||||
content: List of content blocks to process.
|
||||
|
||||
Returns:
|
||||
Updated list with Anthropic blocks converted to v1 format.
|
||||
"""
|
||||
|
||||
def _iter_blocks() -> Iterable[types.ContentBlock]:
|
||||
blocks: list[dict[str, Any]] = [
|
||||
cast("dict[str, Any]", block)
|
||||
if block.get("type") != "non_standard"
|
||||
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
|
||||
for block in content
|
||||
]
|
||||
for block in blocks:
|
||||
block_type = block.get("type")
|
||||
|
||||
if (
|
||||
block_type == "document"
|
||||
and "source" in block
|
||||
and "type" in block["source"]
|
||||
):
|
||||
if block["source"]["type"] == "base64":
|
||||
file_block: types.FileContentBlock = {
|
||||
"type": "file",
|
||||
"base64": block["source"]["data"],
|
||||
"mime_type": block["source"]["media_type"],
|
||||
}
|
||||
_populate_extras(file_block, block, {"type", "source"})
|
||||
yield file_block
|
||||
|
||||
elif block["source"]["type"] == "url":
|
||||
file_block = {
|
||||
"type": "file",
|
||||
"url": block["source"]["url"],
|
||||
}
|
||||
_populate_extras(file_block, block, {"type", "source"})
|
||||
yield file_block
|
||||
|
||||
elif block["source"]["type"] == "file":
|
||||
file_block = {
|
||||
"type": "file",
|
||||
"id": block["source"]["file_id"],
|
||||
}
|
||||
_populate_extras(file_block, block, {"type", "source"})
|
||||
yield file_block
|
||||
|
||||
elif block["source"]["type"] == "text":
|
||||
plain_text_block: types.PlainTextContentBlock = {
|
||||
"type": "text-plain",
|
||||
"text": block["source"]["data"],
|
||||
"mime_type": block.get("media_type", "text/plain"),
|
||||
}
|
||||
_populate_extras(plain_text_block, block, {"type", "source"})
|
||||
yield plain_text_block
|
||||
|
||||
else:
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif (
|
||||
block_type == "image"
|
||||
and "source" in block
|
||||
and "type" in block["source"]
|
||||
):
|
||||
if block["source"]["type"] == "base64":
|
||||
image_block: types.ImageContentBlock = {
|
||||
"type": "image",
|
||||
"base64": block["source"]["data"],
|
||||
"mime_type": block["source"]["media_type"],
|
||||
}
|
||||
_populate_extras(image_block, block, {"type", "source"})
|
||||
yield image_block
|
||||
|
||||
elif block["source"]["type"] == "url":
|
||||
image_block = {
|
||||
"type": "image",
|
||||
"url": block["source"]["url"],
|
||||
}
|
||||
_populate_extras(image_block, block, {"type", "source"})
|
||||
yield image_block
|
||||
|
||||
elif block["source"]["type"] == "file":
|
||||
image_block = {
|
||||
"type": "image",
|
||||
"id": block["source"]["file_id"],
|
||||
}
|
||||
_populate_extras(image_block, block, {"type", "source"})
|
||||
yield image_block
|
||||
|
||||
else:
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif block_type in types.KNOWN_BLOCK_TYPES:
|
||||
yield cast("types.ContentBlock", block)
|
||||
|
||||
else:
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
return list(_iter_blocks())
|
||||
|
||||
|
||||
def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
|
||||
citation_type = citation.get("type")
|
||||
|
||||
if citation_type == "web_search_result_location":
|
||||
url_citation: types.Citation = {
|
||||
"type": "citation",
|
||||
"cited_text": citation["cited_text"],
|
||||
"url": citation["url"],
|
||||
}
|
||||
if title := citation.get("title"):
|
||||
url_citation["title"] = title
|
||||
known_fields = {"type", "cited_text", "url", "title", "index", "extras"}
|
||||
for key, value in citation.items():
|
||||
if key not in known_fields:
|
||||
if "extras" not in url_citation:
|
||||
url_citation["extras"] = {}
|
||||
url_citation["extras"][key] = value
|
||||
|
||||
return url_citation
|
||||
|
||||
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"],
|
||||
}
|
||||
if "document_title" in citation:
|
||||
document_citation["title"] = citation["document_title"]
|
||||
elif title := citation.get("title"):
|
||||
document_citation["title"] = title
|
||||
else:
|
||||
pass
|
||||
known_fields = {
|
||||
"type",
|
||||
"cited_text",
|
||||
"document_title",
|
||||
"title",
|
||||
"index",
|
||||
"extras",
|
||||
}
|
||||
for key, value in citation.items():
|
||||
if key not in known_fields:
|
||||
if "extras" not in document_citation:
|
||||
document_citation["extras"] = {}
|
||||
document_citation["extras"][key] = value
|
||||
|
||||
return document_citation
|
||||
|
||||
return {
|
||||
"type": "non_standard_annotation",
|
||||
"value": citation,
|
||||
}
|
||||
|
||||
|
||||
def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Convert Anthropic message content to v1 format."""
|
||||
if isinstance(message.content, str):
|
||||
content: list[Union[str, dict]] = [{"type": "text", "text": message.content}]
|
||||
else:
|
||||
content = message.content
|
||||
|
||||
def _iter_blocks() -> Iterable[types.ContentBlock]:
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
block_type = block.get("type")
|
||||
|
||||
if block_type == "text":
|
||||
if citations := block.get("citations"):
|
||||
text_block: types.TextContentBlock = {
|
||||
"type": "text",
|
||||
"text": block.get("text", ""),
|
||||
"annotations": [_convert_citation_to_v1(a) for a in citations],
|
||||
}
|
||||
else:
|
||||
text_block = {"type": "text", "text": block["text"]}
|
||||
if "index" in block:
|
||||
text_block["index"] = block["index"]
|
||||
yield text_block
|
||||
|
||||
elif block_type == "thinking":
|
||||
reasoning_block: types.ReasoningContentBlock = {
|
||||
"type": "reasoning",
|
||||
"reasoning": block.get("thinking", ""),
|
||||
}
|
||||
if "index" in block:
|
||||
reasoning_block["index"] = block["index"]
|
||||
known_fields = {"type", "thinking", "index", "extras"}
|
||||
for key in block:
|
||||
if key not in known_fields:
|
||||
if "extras" not in reasoning_block:
|
||||
reasoning_block["extras"] = {}
|
||||
reasoning_block["extras"][key] = block[key]
|
||||
yield reasoning_block
|
||||
|
||||
elif block_type == "tool_use":
|
||||
if (
|
||||
isinstance(message, AIMessageChunk)
|
||||
and len(message.tool_call_chunks) == 1
|
||||
and message.chunk_position != "last"
|
||||
):
|
||||
# Isolated chunk
|
||||
tool_call_chunk: types.ToolCallChunk = (
|
||||
message.tool_call_chunks[0].copy() # type: ignore[assignment]
|
||||
)
|
||||
if "type" not in tool_call_chunk:
|
||||
tool_call_chunk["type"] = "tool_call_chunk"
|
||||
yield tool_call_chunk
|
||||
else:
|
||||
tool_call_block: Optional[types.ToolCall] = None
|
||||
# Non-streaming or gathered chunk
|
||||
if len(message.tool_calls) == 1:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
"name": message.tool_calls[0]["name"],
|
||||
"args": message.tool_calls[0]["args"],
|
||||
"id": message.tool_calls[0].get("id"),
|
||||
}
|
||||
elif call_id := block.get("id"):
|
||||
for tc in message.tool_calls:
|
||||
if tc.get("id") == call_id:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
"name": tc["name"],
|
||||
"args": tc["args"],
|
||||
"id": tc.get("id"),
|
||||
}
|
||||
break
|
||||
else:
|
||||
pass
|
||||
if not tool_call_block:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
"name": block.get("name", ""),
|
||||
"args": block.get("input", {}),
|
||||
"id": block.get("id", ""),
|
||||
}
|
||||
if "index" in block:
|
||||
tool_call_block["index"] = block["index"]
|
||||
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]
|
||||
)
|
||||
if "type" not in tool_call_chunk:
|
||||
tool_call_chunk["type"] = "tool_call_chunk"
|
||||
yield tool_call_chunk
|
||||
|
||||
else:
|
||||
server_tool_call_chunk: types.ServerToolCallChunk = {
|
||||
"type": "server_tool_call_chunk",
|
||||
"args": block.get("partial_json", ""),
|
||||
}
|
||||
if "index" in block:
|
||||
server_tool_call_chunk["index"] = block["index"]
|
||||
yield server_tool_call_chunk
|
||||
|
||||
elif block_type == "server_tool_use":
|
||||
if block.get("name") == "code_execution":
|
||||
server_tool_use_name = "code_interpreter"
|
||||
else:
|
||||
server_tool_use_name = block.get("name", "")
|
||||
if (
|
||||
isinstance(message, AIMessageChunk)
|
||||
and block.get("input") == {}
|
||||
and "partial_json" not in block
|
||||
and message.chunk_position != "last"
|
||||
):
|
||||
# First chunk in a stream
|
||||
server_tool_call_chunk = {
|
||||
"type": "server_tool_call_chunk",
|
||||
"name": server_tool_use_name,
|
||||
"args": "",
|
||||
"id": block.get("id", ""),
|
||||
}
|
||||
if "index" in block:
|
||||
server_tool_call_chunk["index"] = block["index"]
|
||||
known_fields = {"type", "name", "input", "id", "index"}
|
||||
_populate_extras(server_tool_call_chunk, block, known_fields)
|
||||
yield server_tool_call_chunk
|
||||
else:
|
||||
server_tool_call: types.ServerToolCall = {
|
||||
"type": "server_tool_call",
|
||||
"name": server_tool_use_name,
|
||||
"args": block.get("input", {}),
|
||||
"id": block.get("id", ""),
|
||||
}
|
||||
|
||||
if block.get("input") == {} and "partial_json" in block:
|
||||
try:
|
||||
input_ = json.loads(block["partial_json"])
|
||||
if isinstance(input_, dict):
|
||||
server_tool_call["args"] = input_
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if "index" in block:
|
||||
server_tool_call["index"] = block["index"]
|
||||
known_fields = {
|
||||
"type",
|
||||
"name",
|
||||
"input",
|
||||
"partial_json",
|
||||
"id",
|
||||
"index",
|
||||
}
|
||||
_populate_extras(server_tool_call, block, known_fields)
|
||||
|
||||
yield server_tool_call
|
||||
|
||||
elif block_type == "mcp_tool_use":
|
||||
if (
|
||||
isinstance(message, AIMessageChunk)
|
||||
and block.get("input") == {}
|
||||
and "partial_json" not in block
|
||||
and message.chunk_position != "last"
|
||||
):
|
||||
# First chunk in a stream
|
||||
server_tool_call_chunk = {
|
||||
"type": "server_tool_call_chunk",
|
||||
"name": "remote_mcp",
|
||||
"args": "",
|
||||
"id": block.get("id", ""),
|
||||
}
|
||||
if "name" in block:
|
||||
server_tool_call_chunk["extras"] = {"tool_name": block["name"]}
|
||||
known_fields = {"type", "name", "input", "id", "index"}
|
||||
_populate_extras(server_tool_call_chunk, block, known_fields)
|
||||
if "index" in block:
|
||||
server_tool_call_chunk["index"] = block["index"]
|
||||
yield server_tool_call_chunk
|
||||
else:
|
||||
server_tool_call = {
|
||||
"type": "server_tool_call",
|
||||
"name": "remote_mcp",
|
||||
"args": block.get("input", {}),
|
||||
"id": block.get("id", ""),
|
||||
}
|
||||
|
||||
if block.get("input") == {} and "partial_json" in block:
|
||||
try:
|
||||
input_ = json.loads(block["partial_json"])
|
||||
if isinstance(input_, dict):
|
||||
server_tool_call["args"] = input_
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if "name" in block:
|
||||
server_tool_call["extras"] = {"tool_name": block["name"]}
|
||||
known_fields = {
|
||||
"type",
|
||||
"name",
|
||||
"input",
|
||||
"partial_json",
|
||||
"id",
|
||||
"index",
|
||||
}
|
||||
_populate_extras(server_tool_call, block, known_fields)
|
||||
if "index" in block:
|
||||
server_tool_call["index"] = block["index"]
|
||||
|
||||
yield server_tool_call
|
||||
|
||||
elif block_type and block_type.endswith("_tool_result"):
|
||||
server_tool_result: types.ServerToolResult = {
|
||||
"type": "server_tool_result",
|
||||
"tool_call_id": block.get("tool_use_id", ""),
|
||||
"status": "success",
|
||||
"extras": {"block_type": block_type},
|
||||
}
|
||||
if output := block.get("content", []):
|
||||
server_tool_result["output"] = output
|
||||
if isinstance(output, dict) and output.get(
|
||||
"error_code" # web_search, code_interpreter
|
||||
):
|
||||
server_tool_result["status"] = "error"
|
||||
if block.get("is_error"): # mcp_tool_result
|
||||
server_tool_result["status"] = "error"
|
||||
if "index" in block:
|
||||
server_tool_result["index"] = block["index"]
|
||||
|
||||
known_fields = {"type", "tool_use_id", "content", "is_error", "index"}
|
||||
_populate_extras(server_tool_result, block, known_fields)
|
||||
|
||||
yield server_tool_result
|
||||
|
||||
else:
|
||||
new_block: types.NonStandardContentBlock = {
|
||||
"type": "non_standard",
|
||||
"value": block,
|
||||
}
|
||||
if "index" in new_block["value"]:
|
||||
new_block["index"] = new_block["value"].pop("index")
|
||||
yield new_block
|
||||
|
||||
return list(_iter_blocks())
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Anthropic content."""
|
||||
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."""
|
||||
return _convert_to_v1_from_anthropic(message)
|
||||
|
||||
|
||||
def _register_anthropic_translator() -> None:
|
||||
"""Register the Anthropic translator with the central registry.
|
||||
|
||||
Run automatically when the module is imported.
|
||||
"""
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
register_translator,
|
||||
)
|
||||
|
||||
register_translator("anthropic", translate_content, translate_content_chunk)
|
||||
|
||||
|
||||
_register_anthropic_translator()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Derivations of standard content blocks from Bedrock content."""
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.block_translators.anthropic import (
|
||||
_convert_to_v1_from_anthropic,
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_v1_from_bedrock(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Convert bedrock message content to v1 format."""
|
||||
out = _convert_to_v1_from_anthropic(message)
|
||||
|
||||
content_tool_call_ids = {
|
||||
block.get("id")
|
||||
for block in out
|
||||
if isinstance(block, dict) and block.get("type") == "tool_call"
|
||||
}
|
||||
for tool_call in message.tool_calls:
|
||||
if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:
|
||||
tool_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"id": id_,
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
}
|
||||
if "index" in tool_call:
|
||||
tool_call_block["index"] = tool_call["index"] # type: ignore[typeddict-item]
|
||||
if "extras" in tool_call:
|
||||
tool_call_block["extras"] = tool_call["extras"] # type: ignore[typeddict-item]
|
||||
out.append(tool_call_block)
|
||||
return out
|
||||
|
||||
|
||||
def _convert_to_v1_from_bedrock_chunk(
|
||||
message: AIMessageChunk,
|
||||
) -> list[types.ContentBlock]:
|
||||
"""Convert bedrock message chunk content to v1 format."""
|
||||
if (
|
||||
message.content == ""
|
||||
and not message.additional_kwargs
|
||||
and not message.tool_calls
|
||||
):
|
||||
# Bedrock outputs multiple chunks containing response metadata
|
||||
return []
|
||||
|
||||
out = _convert_to_v1_from_anthropic(message)
|
||||
|
||||
if (
|
||||
message.tool_call_chunks
|
||||
and not message.content
|
||||
and message.chunk_position != "last" # keep tool_calls if aggregated
|
||||
):
|
||||
for tool_call_chunk in message.tool_call_chunks:
|
||||
tc: types.ToolCallChunk = {
|
||||
"type": "tool_call_chunk",
|
||||
"id": tool_call_chunk.get("id"),
|
||||
"name": tool_call_chunk.get("name"),
|
||||
"args": tool_call_chunk.get("args"),
|
||||
}
|
||||
if (idx := tool_call_chunk.get("index")) is not None:
|
||||
tc["index"] = idx
|
||||
out.append(tc)
|
||||
return out
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Bedrock content."""
|
||||
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."""
|
||||
# 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.
|
||||
return _convert_to_v1_from_bedrock_chunk(message)
|
||||
|
||||
|
||||
def _register_bedrock_translator() -> None:
|
||||
"""Register the bedrock translator with the central registry.
|
||||
|
||||
Run automatically when the module is imported.
|
||||
"""
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
register_translator,
|
||||
)
|
||||
|
||||
register_translator("bedrock", translate_content, translate_content_chunk)
|
||||
|
||||
|
||||
_register_bedrock_translator()
|
||||
@@ -0,0 +1,297 @@
|
||||
"""Derivations of standard content blocks from Amazon (Bedrock Converse) content."""
|
||||
|
||||
import base64
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
|
||||
def _bytes_to_b64_str(bytes_: bytes) -> str:
|
||||
return base64.b64encode(bytes_).decode("utf-8")
|
||||
|
||||
|
||||
def _populate_extras(
|
||||
standard_block: types.ContentBlock, block: dict[str, Any], known_fields: set[str]
|
||||
) -> types.ContentBlock:
|
||||
"""Mutate a block, populating extras."""
|
||||
if standard_block.get("type") == "non_standard":
|
||||
return standard_block
|
||||
|
||||
for key, value in block.items():
|
||||
if key not in known_fields:
|
||||
if "extras" not in standard_block:
|
||||
# Below type-ignores are because mypy thinks a non-standard block can
|
||||
# get here, although we exclude them above.
|
||||
standard_block["extras"] = {} # type: ignore[typeddict-unknown-key]
|
||||
standard_block["extras"][key] = value # type: ignore[typeddict-item]
|
||||
|
||||
return standard_block
|
||||
|
||||
|
||||
def _convert_to_v1_from_converse_input(
|
||||
content: list[types.ContentBlock],
|
||||
) -> list[types.ContentBlock]:
|
||||
"""Convert Bedrock Converse format blocks to v1 format.
|
||||
|
||||
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
|
||||
block as a ``'non_standard'`` block with the original block stored in the ``value``
|
||||
field. This function attempts to unpack those blocks and convert any blocks that
|
||||
might be Converse format to v1 ContentBlocks.
|
||||
|
||||
If conversion fails, the block is left as a ``'non_standard'`` block.
|
||||
|
||||
Args:
|
||||
content: List of content blocks to process.
|
||||
|
||||
Returns:
|
||||
Updated list with Converse blocks converted to v1 format.
|
||||
"""
|
||||
|
||||
def _iter_blocks() -> Iterable[types.ContentBlock]:
|
||||
blocks: list[dict[str, Any]] = [
|
||||
cast("dict[str, Any]", block)
|
||||
if block.get("type") != "non_standard"
|
||||
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
|
||||
for block in content
|
||||
]
|
||||
for block in blocks:
|
||||
num_keys = len(block)
|
||||
|
||||
if num_keys == 1 and (text := block.get("text")):
|
||||
yield {"type": "text", "text": text}
|
||||
|
||||
elif (
|
||||
num_keys == 1
|
||||
and (document := block.get("document"))
|
||||
and isinstance(document, dict)
|
||||
and "format" in document
|
||||
):
|
||||
if document.get("format") == "pdf":
|
||||
if "bytes" in document.get("source", {}):
|
||||
file_block: types.FileContentBlock = {
|
||||
"type": "file",
|
||||
"base64": _bytes_to_b64_str(document["source"]["bytes"]),
|
||||
"mime_type": "application/pdf",
|
||||
}
|
||||
_populate_extras(file_block, document, {"format", "source"})
|
||||
yield file_block
|
||||
|
||||
else:
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif document["format"] == "txt":
|
||||
if "text" in document.get("source", {}):
|
||||
plain_text_block: types.PlainTextContentBlock = {
|
||||
"type": "text-plain",
|
||||
"text": document["source"]["text"],
|
||||
"mime_type": "text/plain",
|
||||
}
|
||||
_populate_extras(
|
||||
plain_text_block, document, {"format", "source"}
|
||||
)
|
||||
yield plain_text_block
|
||||
else:
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
else:
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif (
|
||||
num_keys == 1
|
||||
and (image := block.get("image"))
|
||||
and isinstance(image, dict)
|
||||
and "format" in image
|
||||
):
|
||||
if "bytes" in image.get("source", {}):
|
||||
image_block: types.ImageContentBlock = {
|
||||
"type": "image",
|
||||
"base64": _bytes_to_b64_str(image["source"]["bytes"]),
|
||||
"mime_type": f"image/{image['format']}",
|
||||
}
|
||||
_populate_extras(image_block, image, {"format", "source"})
|
||||
yield image_block
|
||||
|
||||
else:
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif block.get("type") in types.KNOWN_BLOCK_TYPES:
|
||||
yield cast("types.ContentBlock", block)
|
||||
|
||||
else:
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
return list(_iter_blocks())
|
||||
|
||||
|
||||
def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
|
||||
standard_citation: types.Citation = {"type": "citation"}
|
||||
if "title" in citation:
|
||||
standard_citation["title"] = citation["title"]
|
||||
if (
|
||||
(source_content := citation.get("source_content"))
|
||||
and isinstance(source_content, list)
|
||||
and all(isinstance(item, dict) for item in source_content)
|
||||
):
|
||||
standard_citation["cited_text"] = "".join(
|
||||
item.get("text", "") for item in source_content
|
||||
)
|
||||
|
||||
known_fields = {"type", "source_content", "title", "index", "extras"}
|
||||
|
||||
for key, value in citation.items():
|
||||
if key not in known_fields:
|
||||
if "extras" not in standard_citation:
|
||||
standard_citation["extras"] = {}
|
||||
standard_citation["extras"][key] = value
|
||||
|
||||
return standard_citation
|
||||
|
||||
|
||||
def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Convert Bedrock Converse message content to v1 format."""
|
||||
if (
|
||||
message.content == ""
|
||||
and not message.additional_kwargs
|
||||
and not message.tool_calls
|
||||
):
|
||||
# Converse outputs multiple chunks containing response metadata
|
||||
return []
|
||||
|
||||
if isinstance(message.content, str):
|
||||
message.content = [{"type": "text", "text": message.content}]
|
||||
|
||||
def _iter_blocks() -> Iterable[types.ContentBlock]:
|
||||
for block in message.content:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
block_type = block.get("type")
|
||||
|
||||
if block_type == "text":
|
||||
if citations := block.get("citations"):
|
||||
text_block: types.TextContentBlock = {
|
||||
"type": "text",
|
||||
"text": block.get("text", ""),
|
||||
"annotations": [_convert_citation_to_v1(a) for a in citations],
|
||||
}
|
||||
else:
|
||||
text_block = {"type": "text", "text": block["text"]}
|
||||
if "index" in block:
|
||||
text_block["index"] = block["index"]
|
||||
yield text_block
|
||||
|
||||
elif block_type == "reasoning_content":
|
||||
reasoning_block: types.ReasoningContentBlock = {"type": "reasoning"}
|
||||
if reasoning_content := block.get("reasoning_content"):
|
||||
if reasoning := reasoning_content.get("text"):
|
||||
reasoning_block["reasoning"] = reasoning
|
||||
if signature := reasoning_content.get("signature"):
|
||||
if "extras" not in reasoning_block:
|
||||
reasoning_block["extras"] = {}
|
||||
reasoning_block["extras"]["signature"] = signature
|
||||
|
||||
if "index" in block:
|
||||
reasoning_block["index"] = block["index"]
|
||||
|
||||
known_fields = {"type", "reasoning_content", "index", "extras"}
|
||||
for key in block:
|
||||
if key not in known_fields:
|
||||
if "extras" not in reasoning_block:
|
||||
reasoning_block["extras"] = {}
|
||||
reasoning_block["extras"][key] = block[key]
|
||||
yield reasoning_block
|
||||
|
||||
elif block_type == "tool_use":
|
||||
if (
|
||||
isinstance(message, AIMessageChunk)
|
||||
and len(message.tool_call_chunks) == 1
|
||||
and message.chunk_position != "last"
|
||||
):
|
||||
# Isolated chunk
|
||||
tool_call_chunk: types.ToolCallChunk = (
|
||||
message.tool_call_chunks[0].copy() # type: ignore[assignment]
|
||||
)
|
||||
if "type" not in tool_call_chunk:
|
||||
tool_call_chunk["type"] = "tool_call_chunk"
|
||||
yield tool_call_chunk
|
||||
else:
|
||||
tool_call_block: Optional[types.ToolCall] = None
|
||||
# Non-streaming or gathered chunk
|
||||
if len(message.tool_calls) == 1:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
"name": message.tool_calls[0]["name"],
|
||||
"args": message.tool_calls[0]["args"],
|
||||
"id": message.tool_calls[0].get("id"),
|
||||
}
|
||||
elif call_id := block.get("id"):
|
||||
for tc in message.tool_calls:
|
||||
if tc.get("id") == call_id:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
"name": tc["name"],
|
||||
"args": tc["args"],
|
||||
"id": tc.get("id"),
|
||||
}
|
||||
break
|
||||
else:
|
||||
pass
|
||||
if not tool_call_block:
|
||||
tool_call_block = {
|
||||
"type": "tool_call",
|
||||
"name": block.get("name", ""),
|
||||
"args": block.get("input", {}),
|
||||
"id": block.get("id", ""),
|
||||
}
|
||||
if "index" in block:
|
||||
tool_call_block["index"] = block["index"]
|
||||
yield tool_call_block
|
||||
|
||||
elif (
|
||||
block_type == "input_json_delta"
|
||||
and isinstance(message, AIMessageChunk)
|
||||
and len(message.tool_call_chunks) == 1
|
||||
):
|
||||
tool_call_chunk = (
|
||||
message.tool_call_chunks[0].copy() # type: ignore[assignment]
|
||||
)
|
||||
if "type" not in tool_call_chunk:
|
||||
tool_call_chunk["type"] = "tool_call_chunk"
|
||||
yield tool_call_chunk
|
||||
|
||||
else:
|
||||
new_block: types.NonStandardContentBlock = {
|
||||
"type": "non_standard",
|
||||
"value": block,
|
||||
}
|
||||
if "index" in new_block["value"]:
|
||||
new_block["index"] = new_block["value"].pop("index")
|
||||
yield new_block
|
||||
|
||||
return list(_iter_blocks())
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Bedrock Converse content."""
|
||||
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."""
|
||||
return _convert_to_v1_from_converse(message)
|
||||
|
||||
|
||||
def _register_bedrock_converse_translator() -> None:
|
||||
"""Register the Bedrock Converse translator with the central registry.
|
||||
|
||||
Run automatically when the module is imported.
|
||||
"""
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
register_translator,
|
||||
)
|
||||
|
||||
register_translator("bedrock_converse", translate_content, translate_content_chunk)
|
||||
|
||||
|
||||
_register_bedrock_converse_translator()
|
||||
@@ -0,0 +1,529 @@
|
||||
"""Derivations of standard content blocks from Google (GenAI) content."""
|
||||
|
||||
import base64
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, cast
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.content import Citation, create_citation
|
||||
|
||||
|
||||
def _bytes_to_b64_str(bytes_: bytes) -> str:
|
||||
"""Convert bytes to base64 encoded string."""
|
||||
return base64.b64encode(bytes_).decode("utf-8")
|
||||
|
||||
|
||||
def translate_grounding_metadata_to_citations(
|
||||
grounding_metadata: dict[str, Any],
|
||||
) -> list[Citation]:
|
||||
"""Translate Google AI grounding metadata to LangChain Citations.
|
||||
|
||||
Args:
|
||||
grounding_metadata: Google AI grounding metadata containing web search
|
||||
queries, grounding chunks, and grounding supports.
|
||||
|
||||
Returns:
|
||||
List of Citation content blocks derived from the grounding metadata.
|
||||
|
||||
Example:
|
||||
>>> metadata = {
|
||||
... "web_search_queries": ["UEFA Euro 2024 winner"],
|
||||
... "grounding_chunks": [
|
||||
... {
|
||||
... "web": {
|
||||
... "uri": "https://uefa.com/euro2024",
|
||||
... "title": "UEFA Euro 2024 Results",
|
||||
... }
|
||||
... }
|
||||
... ],
|
||||
... "grounding_supports": [
|
||||
... {
|
||||
... "segment": {
|
||||
... "start_index": 0,
|
||||
... "end_index": 47,
|
||||
... "text": "Spain won the UEFA Euro 2024 championship",
|
||||
... },
|
||||
... "grounding_chunk_indices": [0],
|
||||
... }
|
||||
... ],
|
||||
... }
|
||||
>>> citations = translate_grounding_metadata_to_citations(metadata)
|
||||
>>> len(citations)
|
||||
1
|
||||
>>> citations[0]["url"]
|
||||
'https://uefa.com/euro2024'
|
||||
"""
|
||||
if not grounding_metadata:
|
||||
return []
|
||||
|
||||
grounding_chunks = grounding_metadata.get("grounding_chunks", [])
|
||||
grounding_supports = grounding_metadata.get("grounding_supports", [])
|
||||
web_search_queries = grounding_metadata.get("web_search_queries", [])
|
||||
|
||||
citations: list[Citation] = []
|
||||
|
||||
for support in grounding_supports:
|
||||
segment = support.get("segment", {})
|
||||
chunk_indices = support.get("grounding_chunk_indices", [])
|
||||
|
||||
start_index = segment.get("start_index")
|
||||
end_index = segment.get("end_index")
|
||||
cited_text = segment.get("text")
|
||||
|
||||
# Create a citation for each referenced chunk
|
||||
for chunk_index in chunk_indices:
|
||||
if chunk_index < len(grounding_chunks):
|
||||
chunk = grounding_chunks[chunk_index]
|
||||
web_info = chunk.get("web", {})
|
||||
|
||||
citation = create_citation(
|
||||
url=web_info.get("uri"),
|
||||
title=web_info.get("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", []),
|
||||
}
|
||||
},
|
||||
)
|
||||
citations.append(citation)
|
||||
|
||||
return citations
|
||||
|
||||
|
||||
def _convert_to_v1_from_genai_input(
|
||||
content: list[types.ContentBlock],
|
||||
) -> list[types.ContentBlock]:
|
||||
"""Convert Google GenAI format blocks to v1 format.
|
||||
|
||||
Called when message isn't an `AIMessage` or `model_provider` isn't set on
|
||||
`response_metadata`.
|
||||
|
||||
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
|
||||
block as a ``'non_standard'`` block with the original block stored in the ``value``
|
||||
field. This function attempts to unpack those blocks and convert any blocks that
|
||||
might be GenAI format to v1 ContentBlocks.
|
||||
|
||||
If conversion fails, the block is left as a ``'non_standard'`` block.
|
||||
|
||||
Args:
|
||||
content: List of content blocks to process.
|
||||
|
||||
Returns:
|
||||
Updated list with GenAI blocks converted to v1 format.
|
||||
"""
|
||||
|
||||
def _iter_blocks() -> Iterable[types.ContentBlock]:
|
||||
blocks: list[dict[str, Any]] = [
|
||||
cast("dict[str, Any]", block)
|
||||
if block.get("type") != "non_standard"
|
||||
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
|
||||
for block in content
|
||||
]
|
||||
for block in blocks:
|
||||
num_keys = len(block)
|
||||
block_type = block.get("type")
|
||||
|
||||
if num_keys == 1 and (text := block.get("text")):
|
||||
# This is probably a TextContentBlock
|
||||
yield {"type": "text", "text": text}
|
||||
|
||||
elif (
|
||||
num_keys == 1
|
||||
and (document := block.get("document"))
|
||||
and isinstance(document, dict)
|
||||
and "format" in document
|
||||
):
|
||||
# Handle document format conversion
|
||||
doc_format = document.get("format")
|
||||
source = document.get("source", {})
|
||||
|
||||
if doc_format == "pdf" and "bytes" in source:
|
||||
# PDF document with byte data
|
||||
file_block: types.FileContentBlock = {
|
||||
"type": "file",
|
||||
"base64": source["bytes"]
|
||||
if isinstance(source["bytes"], str)
|
||||
else _bytes_to_b64_str(source["bytes"]),
|
||||
"mime_type": "application/pdf",
|
||||
}
|
||||
# Preserve extra fields
|
||||
extras = {
|
||||
key: value
|
||||
for key, value in document.items()
|
||||
if key not in {"format", "source"}
|
||||
}
|
||||
if extras:
|
||||
file_block["extras"] = extras
|
||||
yield file_block
|
||||
|
||||
elif doc_format == "txt" and "text" in source:
|
||||
# Text document
|
||||
plain_text_block: types.PlainTextContentBlock = {
|
||||
"type": "text-plain",
|
||||
"text": source["text"],
|
||||
"mime_type": "text/plain",
|
||||
}
|
||||
# Preserve extra fields
|
||||
extras = {
|
||||
key: value
|
||||
for key, value in document.items()
|
||||
if key not in {"format", "source"}
|
||||
}
|
||||
if extras:
|
||||
plain_text_block["extras"] = extras
|
||||
yield plain_text_block
|
||||
|
||||
else:
|
||||
# Unknown document format
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif (
|
||||
num_keys == 1
|
||||
and (image := block.get("image"))
|
||||
and isinstance(image, dict)
|
||||
and "format" in image
|
||||
):
|
||||
# Handle image format conversion
|
||||
img_format = image.get("format")
|
||||
source = image.get("source", {})
|
||||
|
||||
if "bytes" in source:
|
||||
# Image with byte data
|
||||
image_block: types.ImageContentBlock = {
|
||||
"type": "image",
|
||||
"base64": source["bytes"]
|
||||
if isinstance(source["bytes"], str)
|
||||
else _bytes_to_b64_str(source["bytes"]),
|
||||
"mime_type": f"image/{img_format}",
|
||||
}
|
||||
# Preserve extra fields
|
||||
extras = {}
|
||||
for key, value in image.items():
|
||||
if key not in {"format", "source"}:
|
||||
extras[key] = value
|
||||
if extras:
|
||||
image_block["extras"] = extras
|
||||
yield image_block
|
||||
|
||||
else:
|
||||
# Image without byte data
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif block_type == "file_data" and "file_uri" in block:
|
||||
# Handle FileData URI-based content
|
||||
uri_file_block: types.FileContentBlock = {
|
||||
"type": "file",
|
||||
"url": block["file_uri"],
|
||||
}
|
||||
if mime_type := block.get("mime_type"):
|
||||
uri_file_block["mime_type"] = mime_type
|
||||
yield uri_file_block
|
||||
|
||||
elif block_type == "function_call" and "name" in block:
|
||||
# Handle function calls
|
||||
tool_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"name": block["name"],
|
||||
"args": block.get("args", {}),
|
||||
"id": block.get("id", ""),
|
||||
}
|
||||
yield tool_call_block
|
||||
|
||||
elif block_type == "executable_code":
|
||||
server_tool_call_input: types.ServerToolCall = {
|
||||
"type": "server_tool_call",
|
||||
"name": "code_interpreter",
|
||||
"args": {
|
||||
"code": block.get("executable_code", ""),
|
||||
"language": block.get("language", "python"),
|
||||
},
|
||||
"id": block.get("id", ""),
|
||||
}
|
||||
yield server_tool_call_input
|
||||
|
||||
elif block_type == "code_execution_result":
|
||||
outcome = block.get("outcome", 1)
|
||||
status = "success" if outcome == 1 else "error"
|
||||
server_tool_result_input: types.ServerToolResult = {
|
||||
"type": "server_tool_result",
|
||||
"tool_call_id": block.get("tool_call_id", ""),
|
||||
"status": status, # type: ignore[typeddict-item]
|
||||
"output": block.get("code_execution_result", ""),
|
||||
}
|
||||
if outcome is not None:
|
||||
server_tool_result_input["extras"] = {"outcome": outcome}
|
||||
yield server_tool_result_input
|
||||
|
||||
elif block.get("type") in types.KNOWN_BLOCK_TYPES:
|
||||
# We see a standard block type, so we just cast it, even if
|
||||
# we don't fully understand it. This may be dangerous, but
|
||||
# it's better than losing information.
|
||||
yield cast("types.ContentBlock", block)
|
||||
|
||||
else:
|
||||
# We don't understand this block at all.
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
return list(_iter_blocks())
|
||||
|
||||
|
||||
def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Convert Google GenAI message content to v1 format.
|
||||
|
||||
Calling `.content_blocks` on an `AIMessage` where `response_metadata.model_provider`
|
||||
is set to `'google_genai'` will invoke this function to parse the content into
|
||||
standard content blocks for returning.
|
||||
|
||||
Args:
|
||||
message: The AIMessage or AIMessageChunk to convert.
|
||||
|
||||
Returns:
|
||||
List of standard content blocks derived from the message content.
|
||||
"""
|
||||
if isinstance(message.content, str):
|
||||
# String content -> TextContentBlock (only add if non-empty in case of audio)
|
||||
string_blocks: list[types.ContentBlock] = []
|
||||
if message.content:
|
||||
string_blocks.append({"type": "text", "text": message.content})
|
||||
|
||||
# Add any missing tool calls from message.tool_calls field
|
||||
content_tool_call_ids = {
|
||||
block.get("id")
|
||||
for block in string_blocks
|
||||
if isinstance(block, dict) and block.get("type") == "tool_call"
|
||||
}
|
||||
for tool_call in message.tool_calls:
|
||||
id_ = tool_call.get("id")
|
||||
if id_ and id_ not in content_tool_call_ids:
|
||||
string_tool_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"id": id_,
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
}
|
||||
string_blocks.append(string_tool_call_block)
|
||||
|
||||
# Handle audio from additional_kwargs if present (for empty content cases)
|
||||
audio_data = message.additional_kwargs.get("audio")
|
||||
if audio_data and isinstance(audio_data, bytes):
|
||||
audio_block: types.AudioContentBlock = {
|
||||
"type": "audio",
|
||||
"base64": _bytes_to_b64_str(audio_data),
|
||||
"mime_type": "audio/wav", # Default to WAV for Google GenAI
|
||||
}
|
||||
string_blocks.append(audio_block)
|
||||
|
||||
grounding_metadata = message.response_metadata.get("grounding_metadata")
|
||||
if grounding_metadata:
|
||||
citations = translate_grounding_metadata_to_citations(grounding_metadata)
|
||||
|
||||
for block in string_blocks:
|
||||
if block["type"] == "text" and citations:
|
||||
# Add citations to the first text block only
|
||||
block["annotations"] = cast("list[types.Annotation]", citations)
|
||||
break
|
||||
|
||||
return string_blocks
|
||||
|
||||
if not isinstance(message.content, list):
|
||||
# Unexpected content type, attempt to represent as text
|
||||
return [{"type": "text", "text": str(message.content)}]
|
||||
|
||||
converted_blocks: list[types.ContentBlock] = []
|
||||
|
||||
for item in message.content:
|
||||
if isinstance(item, str):
|
||||
# Conversation history strings
|
||||
|
||||
# Citations are handled below after all blocks are converted
|
||||
converted_blocks.append({"type": "text", "text": item}) # TextContentBlock
|
||||
|
||||
elif isinstance(item, dict):
|
||||
item_type = item.get("type")
|
||||
if item_type == "image_url":
|
||||
# Convert image_url to standard image block (base64)
|
||||
# (since the original implementation returned as url-base64 CC style)
|
||||
image_url = item.get("image_url", {})
|
||||
url = image_url.get("url", "")
|
||||
if url:
|
||||
# Extract base64 data
|
||||
match = re.match(r"data:([^;]+);base64,(.+)", url)
|
||||
if match:
|
||||
# Data URI provided
|
||||
mime_type, base64_data = match.groups()
|
||||
converted_blocks.append(
|
||||
{
|
||||
"type": "image",
|
||||
"base64": base64_data,
|
||||
"mime_type": mime_type,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Assume it's raw base64 without data URI
|
||||
try:
|
||||
# Validate base64 and decode for mime type detection
|
||||
decoded_bytes = base64.b64decode(url, validate=True)
|
||||
|
||||
image_url_b64_block = {
|
||||
"type": "image",
|
||||
"base64": url,
|
||||
}
|
||||
|
||||
try:
|
||||
import filetype # type: ignore[import-not-found] # noqa: PLC0415
|
||||
|
||||
# Guess mime type based on file bytes
|
||||
mime_type = None
|
||||
kind = filetype.guess(decoded_bytes)
|
||||
if kind:
|
||||
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)
|
||||
)
|
||||
except Exception:
|
||||
# Not valid base64, treat as non-standard
|
||||
converted_blocks.append(
|
||||
{"type": "non_standard", "value": item}
|
||||
)
|
||||
else:
|
||||
# This likely won't be reached according to previous implementations
|
||||
converted_blocks.append({"type": "non_standard", "value": item})
|
||||
msg = "Image URL not a data URI; appending as non-standard block."
|
||||
raise ValueError(msg)
|
||||
elif item_type == "function_call":
|
||||
# Handle Google GenAI function calls
|
||||
function_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"name": item.get("name", ""),
|
||||
"args": item.get("args", {}),
|
||||
"id": item.get("id", ""),
|
||||
}
|
||||
converted_blocks.append(function_call_block)
|
||||
elif item_type == "file_data":
|
||||
# Handle FileData URI-based content
|
||||
file_block: types.FileContentBlock = {
|
||||
"type": "file",
|
||||
"url": item.get("file_uri", ""),
|
||||
}
|
||||
if mime_type := item.get("mime_type"):
|
||||
file_block["mime_type"] = mime_type
|
||||
converted_blocks.append(file_block)
|
||||
elif item_type == "thinking":
|
||||
# Handling for the 'thinking' type we package thoughts as
|
||||
reasoning_block: types.ReasoningContentBlock = {
|
||||
"type": "reasoning",
|
||||
"reasoning": item.get("thinking", ""),
|
||||
}
|
||||
if signature := item.get("signature"):
|
||||
reasoning_block["extras"] = {"signature": signature}
|
||||
|
||||
converted_blocks.append(reasoning_block)
|
||||
elif item_type == "executable_code":
|
||||
# Convert to standard server tool call block at the moment
|
||||
server_tool_call_block: types.ServerToolCall = {
|
||||
"type": "server_tool_call",
|
||||
"name": "code_interpreter",
|
||||
"args": {
|
||||
"code": item.get("executable_code", ""),
|
||||
"language": item.get("language", "python"), # Default to python
|
||||
},
|
||||
"id": item.get("id", ""),
|
||||
}
|
||||
converted_blocks.append(server_tool_call_block)
|
||||
elif item_type == "code_execution_result":
|
||||
# Map outcome to status: OUTCOME_OK (1) → success, else → error
|
||||
outcome = item.get("outcome", 1)
|
||||
status = "success" if outcome == 1 else "error"
|
||||
server_tool_result_block: types.ServerToolResult = {
|
||||
"type": "server_tool_result",
|
||||
"tool_call_id": item.get("tool_call_id", ""),
|
||||
"status": status, # type: ignore[typeddict-item]
|
||||
"output": item.get("code_execution_result", ""),
|
||||
}
|
||||
# Preserve original outcome in extras
|
||||
if outcome is not None:
|
||||
server_tool_result_block["extras"] = {"outcome": outcome}
|
||||
converted_blocks.append(server_tool_result_block)
|
||||
else:
|
||||
# Unknown type, preserve as non-standard
|
||||
converted_blocks.append({"type": "non_standard", "value": item})
|
||||
else:
|
||||
# Non-dict, non-string content
|
||||
converted_blocks.append({"type": "non_standard", "value": item})
|
||||
|
||||
grounding_metadata = message.response_metadata.get("grounding_metadata")
|
||||
if grounding_metadata:
|
||||
citations = translate_grounding_metadata_to_citations(grounding_metadata)
|
||||
|
||||
for block in converted_blocks:
|
||||
if block["type"] == "text" and citations:
|
||||
# Add citations to text blocks (only the first text block)
|
||||
block["annotations"] = cast("list[types.Annotation]", citations)
|
||||
break
|
||||
|
||||
# Audio is stored on the message.additional_kwargs
|
||||
audio_data = message.additional_kwargs.get("audio")
|
||||
if audio_data and isinstance(audio_data, bytes):
|
||||
audio_block_kwargs: types.AudioContentBlock = {
|
||||
"type": "audio",
|
||||
"base64": _bytes_to_b64_str(audio_data),
|
||||
"mime_type": "audio/wav", # Default to WAV for Google GenAI
|
||||
}
|
||||
converted_blocks.append(audio_block_kwargs)
|
||||
|
||||
# Add any missing tool calls from message.tool_calls field
|
||||
content_tool_call_ids = {
|
||||
block.get("id")
|
||||
for block in converted_blocks
|
||||
if isinstance(block, dict) and block.get("type") == "tool_call"
|
||||
}
|
||||
for tool_call in message.tool_calls:
|
||||
id_ = tool_call.get("id")
|
||||
if id_ and id_ not in content_tool_call_ids:
|
||||
missing_tool_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"id": id_,
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
}
|
||||
converted_blocks.append(missing_tool_call_block)
|
||||
|
||||
return converted_blocks
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Google (GenAI) content."""
|
||||
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."""
|
||||
return _convert_to_v1_from_genai(message)
|
||||
|
||||
|
||||
def _register_google_genai_translator() -> None:
|
||||
"""Register the Google (GenAI) translator with the central registry.
|
||||
|
||||
Run automatically when the module is imported.
|
||||
"""
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
register_translator,
|
||||
)
|
||||
|
||||
register_translator("google_genai", translate_content, translate_content_chunk)
|
||||
|
||||
|
||||
_register_google_genai_translator()
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Derivations of standard content blocks from Google (VertexAI) content."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, cast
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
WARNED = False
|
||||
|
||||
|
||||
def _convert_to_v1_from_vertexai_input(
|
||||
content: list[types.ContentBlock],
|
||||
) -> list[types.ContentBlock]:
|
||||
"""Convert Google (VertexAI) format blocks to v1 format.
|
||||
|
||||
Called when message isn't an `AIMessage` or `model_provider` isn't set on
|
||||
`response_metadata`.
|
||||
|
||||
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
|
||||
block as a ``'non_standard'`` block with the original block stored in the ``value``
|
||||
field. This function attempts to unpack those blocks and convert any blocks that
|
||||
might be GenAI format to v1 ContentBlocks.
|
||||
|
||||
If conversion fails, the block is left as a ``'non_standard'`` block.
|
||||
|
||||
Args:
|
||||
content: List of content blocks to process.
|
||||
|
||||
Returns:
|
||||
Updated list with VertexAI blocks converted to v1 format.
|
||||
"""
|
||||
|
||||
def _iter_blocks() -> Iterable[types.ContentBlock]:
|
||||
blocks: list[dict[str, Any]] = [
|
||||
cast("dict[str, Any]", block)
|
||||
if block.get("type") != "non_standard"
|
||||
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
|
||||
for block in content
|
||||
]
|
||||
for block in blocks:
|
||||
num_keys = len(block)
|
||||
|
||||
if num_keys == 1 and (text := block.get("text")):
|
||||
# This is probably a TextContentBlock
|
||||
yield {"type": "text", "text": text}
|
||||
|
||||
elif (
|
||||
num_keys == 1
|
||||
and (document := block.get("document"))
|
||||
and isinstance(document, dict)
|
||||
and "format" in document
|
||||
):
|
||||
# Probably a document of some kind - TODO
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif (
|
||||
num_keys == 1
|
||||
and (image := block.get("image"))
|
||||
and isinstance(image, dict)
|
||||
and "format" in image
|
||||
):
|
||||
# Probably an image of some kind - TODO
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
elif block.get("type") in types.KNOWN_BLOCK_TYPES:
|
||||
# We see a standard block type, so we just cast it, even if
|
||||
# we don't fully understand it. This may be dangerous, but
|
||||
# it's better than losing information.
|
||||
yield cast("types.ContentBlock", block)
|
||||
|
||||
else:
|
||||
# We don't understand this block at all.
|
||||
yield {"type": "non_standard", "value": block}
|
||||
|
||||
return list(_iter_blocks())
|
||||
|
||||
|
||||
def _convert_to_v1_from_vertexai(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Convert Google (VertexAI) message content to v1 format.
|
||||
|
||||
Calling `.content_blocks` on an `AIMessage` where `response_metadata.model_provider`
|
||||
is set to `'google_vertexai'` will invoke this function to parse the content into
|
||||
standard content blocks for returning.
|
||||
|
||||
Args:
|
||||
message: The AIMessage or AIMessageChunk to convert.
|
||||
|
||||
Returns:
|
||||
List of standard content blocks derived from the message content.
|
||||
"""
|
||||
return message # type: ignore[return-value]
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Google (VertexAI) content."""
|
||||
return _convert_to_v1_from_vertexai(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a chunk with Google (VertexAI) content."""
|
||||
return _convert_to_v1_from_vertexai(message)
|
||||
|
||||
|
||||
def _register_google_vertexai_translator() -> None:
|
||||
"""Register the Google (VertexAI) translator with the central registry.
|
||||
|
||||
Run automatically when the module is imported.
|
||||
"""
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
register_translator,
|
||||
)
|
||||
|
||||
register_translator("google_vertexai", translate_content, translate_content_chunk)
|
||||
|
||||
|
||||
_register_google_vertexai_translator()
|
||||
47
libs/core/langchain_core/messages/block_translators/groq.py
Normal file
47
libs/core/langchain_core/messages/block_translators/groq.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Derivations of standard content blocks from Groq content."""
|
||||
|
||||
import warnings
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
WARNED = False
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]: # noqa: ARG001
|
||||
"""Derive standard content blocks from a message with Groq content."""
|
||||
global WARNED # noqa: PLW0603
|
||||
if not WARNED:
|
||||
warning_message = (
|
||||
"Content block standardization is not yet fully supported for Groq."
|
||||
)
|
||||
warnings.warn(warning_message, stacklevel=2)
|
||||
WARNED = True
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]: # noqa: ARG001
|
||||
"""Derive standard content blocks from a message chunk with Groq content."""
|
||||
global WARNED # noqa: PLW0603
|
||||
if not WARNED:
|
||||
warning_message = (
|
||||
"Content block standardization is not yet fully supported for Groq."
|
||||
)
|
||||
warnings.warn(warning_message, stacklevel=2)
|
||||
WARNED = True
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _register_groq_translator() -> None:
|
||||
"""Register the Groq translator with the central registry.
|
||||
|
||||
Run automatically when the module is imported.
|
||||
"""
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
register_translator,
|
||||
)
|
||||
|
||||
register_translator("groq", translate_content, translate_content_chunk)
|
||||
|
||||
|
||||
_register_groq_translator()
|
||||
@@ -0,0 +1,301 @@
|
||||
"""Derivations of standard content blocks from LangChain v0 multimodal content."""
|
||||
|
||||
from typing import Any, Union, cast
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
|
||||
def _convert_v0_multimodal_input_to_v1(
|
||||
content: list[types.ContentBlock],
|
||||
) -> list[types.ContentBlock]:
|
||||
"""Convert v0 multimodal blocks to v1 format.
|
||||
|
||||
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
|
||||
block as a ``'non_standard'`` block with the original block stored in the ``value``
|
||||
field. This function attempts to unpack those blocks and convert any v0 format
|
||||
blocks to v1 format.
|
||||
|
||||
If conversion fails, the block is left as a ``'non_standard'`` block.
|
||||
|
||||
Args:
|
||||
content: List of content blocks to process.
|
||||
|
||||
Returns:
|
||||
v1 content blocks.
|
||||
"""
|
||||
converted_blocks = []
|
||||
unpacked_blocks: list[dict[str, Any]] = [
|
||||
cast("dict[str, Any]", block)
|
||||
if block.get("type") != "non_standard"
|
||||
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
|
||||
for block in content
|
||||
]
|
||||
for block in unpacked_blocks:
|
||||
if block.get("type") in {"image", "audio", "file"} and "source_type" in block:
|
||||
converted_block = _convert_legacy_v0_content_block_to_v1(block)
|
||||
converted_blocks.append(cast("types.ContentBlock", converted_block))
|
||||
elif block.get("type") in types.KNOWN_BLOCK_TYPES:
|
||||
# Guard in case this function is used outside of the .content_blocks flow
|
||||
converted_blocks.append(cast("types.ContentBlock", block))
|
||||
else:
|
||||
converted_blocks.append({"type": "non_standard", "value": block})
|
||||
|
||||
return converted_blocks
|
||||
|
||||
|
||||
def _convert_legacy_v0_content_block_to_v1(
|
||||
block: dict,
|
||||
) -> Union[types.ContentBlock, dict]:
|
||||
"""Convert a LangChain v0 content block to v1 format.
|
||||
|
||||
Preserves unknown keys as extras to avoid data loss.
|
||||
|
||||
Returns the original block unchanged if it's not in v0 format.
|
||||
"""
|
||||
|
||||
def _extract_v0_extras(block_dict: dict, known_keys: set[str]) -> dict[str, Any]:
|
||||
"""Extract unknown keys from v0 block to preserve as extras.
|
||||
|
||||
Args:
|
||||
block_dict: The original v0 block dictionary.
|
||||
known_keys: Set of keys known to be part of the v0 format for this block.
|
||||
|
||||
Returns:
|
||||
A dictionary of extra keys not part of the known v0 format.
|
||||
"""
|
||||
return {k: v for k, v in block_dict.items() if k not in known_keys}
|
||||
|
||||
# Check if this is actually a v0 format block
|
||||
block_type = block.get("type")
|
||||
if block_type not in {"image", "audio", "file"} or "source_type" not in block:
|
||||
# Not a v0 format block, return unchanged
|
||||
return block
|
||||
|
||||
if block.get("type") == "image":
|
||||
source_type = block.get("source_type")
|
||||
if source_type == "url":
|
||||
# image-url
|
||||
known_keys = {"mime_type", "type", "source_type", "url"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
if "id" in block:
|
||||
return types.create_image_block(
|
||||
url=block["url"],
|
||||
mime_type=block.get("mime_type"),
|
||||
id=block["id"],
|
||||
**extras,
|
||||
)
|
||||
|
||||
# Don't construct with an ID if not present in original block
|
||||
v1_image_url = types.ImageContentBlock(type="image", url=block["url"])
|
||||
if block.get("mime_type"):
|
||||
v1_image_url["mime_type"] = block["mime_type"]
|
||||
|
||||
v1_image_url["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_image_url["extras"][key] = value
|
||||
if v1_image_url["extras"] == {}:
|
||||
del v1_image_url["extras"]
|
||||
|
||||
return v1_image_url
|
||||
if source_type == "base64":
|
||||
# image-base64
|
||||
known_keys = {"mime_type", "type", "source_type", "data"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
if "id" in block:
|
||||
return types.create_image_block(
|
||||
base64=block["data"],
|
||||
mime_type=block.get("mime_type"),
|
||||
id=block["id"],
|
||||
**extras,
|
||||
)
|
||||
|
||||
v1_image_base64 = types.ImageContentBlock(
|
||||
type="image", base64=block["data"]
|
||||
)
|
||||
if block.get("mime_type"):
|
||||
v1_image_base64["mime_type"] = block["mime_type"]
|
||||
|
||||
v1_image_base64["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_image_base64["extras"][key] = value
|
||||
if v1_image_base64["extras"] == {}:
|
||||
del v1_image_base64["extras"]
|
||||
|
||||
return v1_image_base64
|
||||
if source_type == "id":
|
||||
# image-id
|
||||
known_keys = {"type", "source_type", "id"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
# For id `source_type`, `id` is the file reference, not block ID
|
||||
v1_image_id = types.ImageContentBlock(type="image", file_id=block["id"])
|
||||
|
||||
v1_image_id["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_image_id["extras"][key] = value
|
||||
if v1_image_id["extras"] == {}:
|
||||
del v1_image_id["extras"]
|
||||
|
||||
return v1_image_id
|
||||
elif block.get("type") == "audio":
|
||||
source_type = block.get("source_type")
|
||||
if source_type == "url":
|
||||
# audio-url
|
||||
known_keys = {"mime_type", "type", "source_type", "url"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
if "id" in block:
|
||||
return types.create_audio_block(
|
||||
url=block["url"],
|
||||
mime_type=block.get("mime_type"),
|
||||
id=block["id"],
|
||||
**extras,
|
||||
)
|
||||
|
||||
# Don't construct with an ID if not present in original block
|
||||
v1_audio_url: types.AudioContentBlock = types.AudioContentBlock(
|
||||
type="audio", url=block["url"]
|
||||
)
|
||||
if block.get("mime_type"):
|
||||
v1_audio_url["mime_type"] = block["mime_type"]
|
||||
|
||||
v1_audio_url["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_audio_url["extras"][key] = value
|
||||
if v1_audio_url["extras"] == {}:
|
||||
del v1_audio_url["extras"]
|
||||
|
||||
return v1_audio_url
|
||||
if source_type == "base64":
|
||||
# audio-base64
|
||||
known_keys = {"mime_type", "type", "source_type", "data"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
if "id" in block:
|
||||
return types.create_audio_block(
|
||||
base64=block["data"],
|
||||
mime_type=block.get("mime_type"),
|
||||
id=block["id"],
|
||||
**extras,
|
||||
)
|
||||
|
||||
v1_audio_base64: types.AudioContentBlock = types.AudioContentBlock(
|
||||
type="audio", base64=block["data"]
|
||||
)
|
||||
if block.get("mime_type"):
|
||||
v1_audio_base64["mime_type"] = block["mime_type"]
|
||||
|
||||
v1_audio_base64["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_audio_base64["extras"][key] = value
|
||||
if v1_audio_base64["extras"] == {}:
|
||||
del v1_audio_base64["extras"]
|
||||
|
||||
return v1_audio_base64
|
||||
if source_type == "id":
|
||||
# audio-id
|
||||
known_keys = {"type", "source_type", "id"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
v1_audio_id: types.AudioContentBlock = types.AudioContentBlock(
|
||||
type="audio", file_id=block["id"]
|
||||
)
|
||||
|
||||
v1_audio_id["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_audio_id["extras"][key] = value
|
||||
if v1_audio_id["extras"] == {}:
|
||||
del v1_audio_id["extras"]
|
||||
|
||||
return v1_audio_id
|
||||
elif block.get("type") == "file":
|
||||
source_type = block.get("source_type")
|
||||
if source_type == "url":
|
||||
# file-url
|
||||
known_keys = {"mime_type", "type", "source_type", "url"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
if "id" in block:
|
||||
return types.create_file_block(
|
||||
url=block["url"],
|
||||
mime_type=block.get("mime_type"),
|
||||
id=block["id"],
|
||||
**extras,
|
||||
)
|
||||
|
||||
v1_file_url: types.FileContentBlock = types.FileContentBlock(
|
||||
type="file", url=block["url"]
|
||||
)
|
||||
if block.get("mime_type"):
|
||||
v1_file_url["mime_type"] = block["mime_type"]
|
||||
|
||||
v1_file_url["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_file_url["extras"][key] = value
|
||||
if v1_file_url["extras"] == {}:
|
||||
del v1_file_url["extras"]
|
||||
|
||||
return v1_file_url
|
||||
if source_type == "base64":
|
||||
# file-base64
|
||||
known_keys = {"mime_type", "type", "source_type", "data"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
if "id" in block:
|
||||
return types.create_file_block(
|
||||
base64=block["data"],
|
||||
mime_type=block.get("mime_type"),
|
||||
id=block["id"],
|
||||
**extras,
|
||||
)
|
||||
|
||||
v1_file_base64: types.FileContentBlock = types.FileContentBlock(
|
||||
type="file", base64=block["data"]
|
||||
)
|
||||
if block.get("mime_type"):
|
||||
v1_file_base64["mime_type"] = block["mime_type"]
|
||||
|
||||
v1_file_base64["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_file_base64["extras"][key] = value
|
||||
if v1_file_base64["extras"] == {}:
|
||||
del v1_file_base64["extras"]
|
||||
|
||||
return v1_file_base64
|
||||
if source_type == "id":
|
||||
# file-id
|
||||
known_keys = {"type", "source_type", "id"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
return types.create_file_block(file_id=block["id"], **extras)
|
||||
if source_type == "text":
|
||||
# file-text
|
||||
known_keys = {"mime_type", "type", "source_type", "url"}
|
||||
extras = _extract_v0_extras(block, known_keys)
|
||||
if "id" in block:
|
||||
return types.create_plaintext_block(
|
||||
# In v0, URL points to the text file content
|
||||
# TODO: attribute this claim
|
||||
text=block["url"],
|
||||
id=block["id"],
|
||||
**extras,
|
||||
)
|
||||
|
||||
v1_file_text: types.PlainTextContentBlock = types.PlainTextContentBlock(
|
||||
type="text-plain", text=block["url"], mime_type="text/plain"
|
||||
)
|
||||
if block.get("mime_type"):
|
||||
v1_file_text["mime_type"] = block["mime_type"]
|
||||
|
||||
v1_file_text["extras"] = {}
|
||||
for key, value in extras.items():
|
||||
if value is not None:
|
||||
v1_file_text["extras"][key] = value
|
||||
if v1_file_text["extras"] == {}:
|
||||
del v1_file_text["extras"]
|
||||
|
||||
return v1_file_text
|
||||
|
||||
# If we can't convert, return the block unchanged
|
||||
return block
|
||||
1010
libs/core/langchain_core/messages/block_translators/openai.py
Normal file
1010
libs/core/langchain_core/messages/block_translators/openai.py
Normal file
File diff suppressed because it is too large
Load Diff
1414
libs/core/langchain_core/messages/content.py
Normal file
1414
libs/core/langchain_core/messages/content.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,176 +0,0 @@
|
||||
"""Types for content blocks."""
|
||||
|
||||
import warnings
|
||||
from typing import Any, Literal, Union
|
||||
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
|
||||
class BaseDataContentBlock(TypedDict, total=False):
|
||||
"""Base class for data content blocks."""
|
||||
|
||||
mime_type: NotRequired[str]
|
||||
"""MIME type of the content block (if needed)."""
|
||||
|
||||
|
||||
class URLContentBlock(BaseDataContentBlock):
|
||||
"""Content block for data from a URL."""
|
||||
|
||||
type: Literal["image", "audio", "file"]
|
||||
"""Type of the content block."""
|
||||
source_type: Literal["url"]
|
||||
"""Source type (url)."""
|
||||
url: str
|
||||
"""URL for data."""
|
||||
|
||||
|
||||
class Base64ContentBlock(BaseDataContentBlock):
|
||||
"""Content block for inline data from a base64 string."""
|
||||
|
||||
type: Literal["image", "audio", "file"]
|
||||
"""Type of the content block."""
|
||||
source_type: Literal["base64"]
|
||||
"""Source type (base64)."""
|
||||
data: str
|
||||
"""Data as a base64 string."""
|
||||
|
||||
|
||||
class PlainTextContentBlock(BaseDataContentBlock):
|
||||
"""Content block for plain text data (e.g., from a document)."""
|
||||
|
||||
type: Literal["file"]
|
||||
"""Type of the content block."""
|
||||
source_type: Literal["text"]
|
||||
"""Source type (text)."""
|
||||
text: str
|
||||
"""Text data."""
|
||||
|
||||
|
||||
class IDContentBlock(TypedDict):
|
||||
"""Content block for data specified by an identifier."""
|
||||
|
||||
type: Literal["image", "audio", "file"]
|
||||
"""Type of the content block."""
|
||||
source_type: Literal["id"]
|
||||
"""Source type (id)."""
|
||||
id: str
|
||||
"""Identifier for data source."""
|
||||
|
||||
|
||||
DataContentBlock = Union[
|
||||
URLContentBlock,
|
||||
Base64ContentBlock,
|
||||
PlainTextContentBlock,
|
||||
IDContentBlock,
|
||||
]
|
||||
|
||||
_DataContentBlockAdapter: TypeAdapter[DataContentBlock] = TypeAdapter(DataContentBlock)
|
||||
|
||||
|
||||
def is_data_content_block(
|
||||
content_block: dict,
|
||||
) -> bool:
|
||||
"""Check if the content block is a standard data content block.
|
||||
|
||||
Args:
|
||||
content_block: The content block to check.
|
||||
|
||||
Returns:
|
||||
True if the content block is a data content block, False otherwise.
|
||||
"""
|
||||
try:
|
||||
_ = _DataContentBlockAdapter.validate_python(content_block)
|
||||
except ValidationError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def convert_to_openai_image_block(content_block: dict[str, Any]) -> dict:
|
||||
"""Convert image content block to format expected by OpenAI Chat Completions API.
|
||||
|
||||
Args:
|
||||
content_block: The content block to convert.
|
||||
|
||||
Raises:
|
||||
ValueError: If the source type is not supported or if ``mime_type`` is missing
|
||||
for base64 data.
|
||||
|
||||
Returns:
|
||||
A dictionary formatted for OpenAI's API.
|
||||
"""
|
||||
if content_block["source_type"] == "url":
|
||||
return {
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": content_block["url"],
|
||||
},
|
||||
}
|
||||
if content_block["source_type"] == "base64":
|
||||
if "mime_type" not in content_block:
|
||||
error_message = "mime_type key is required for base64 data."
|
||||
raise ValueError(error_message)
|
||||
mime_type = content_block["mime_type"]
|
||||
return {
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:{mime_type};base64,{content_block['data']}",
|
||||
},
|
||||
}
|
||||
error_message = "Unsupported source type. Only 'url' and 'base64' are supported."
|
||||
raise ValueError(error_message)
|
||||
|
||||
|
||||
def convert_to_openai_data_block(block: dict) -> dict:
|
||||
"""Format standard data content block to format expected by OpenAI.
|
||||
|
||||
Args:
|
||||
block: A data content block.
|
||||
|
||||
Raises:
|
||||
ValueError: If the block type or source type is not supported.
|
||||
|
||||
Returns:
|
||||
A dictionary formatted for OpenAI's API.
|
||||
"""
|
||||
if block["type"] == "image":
|
||||
formatted_block = convert_to_openai_image_block(block)
|
||||
|
||||
elif block["type"] == "file":
|
||||
if block["source_type"] == "base64":
|
||||
file = {"file_data": f"data:{block['mime_type']};base64,{block['data']}"}
|
||||
if filename := block.get("filename"):
|
||||
file["filename"] = filename
|
||||
elif (metadata := block.get("metadata")) and ("filename" in metadata):
|
||||
file["filename"] = metadata["filename"]
|
||||
else:
|
||||
warnings.warn(
|
||||
"OpenAI may require a filename for file inputs. Specify a filename "
|
||||
"in the content block: {'type': 'file', 'source_type': 'base64', "
|
||||
"'mime_type': 'application/pdf', 'data': '...', "
|
||||
"'filename': 'my-pdf'}",
|
||||
stacklevel=1,
|
||||
)
|
||||
formatted_block = {"type": "file", "file": file}
|
||||
elif block["source_type"] == "id":
|
||||
formatted_block = {"type": "file", "file": {"file_id": block["id"]}}
|
||||
else:
|
||||
error_msg = "source_type base64 or id is required for file blocks."
|
||||
raise ValueError(error_msg)
|
||||
|
||||
elif block["type"] == "audio":
|
||||
if block["source_type"] == "base64":
|
||||
audio_format = block["mime_type"].split("/")[-1]
|
||||
formatted_block = {
|
||||
"type": "input_audio",
|
||||
"input_audio": {"data": block["data"], "format": audio_format},
|
||||
}
|
||||
else:
|
||||
error_msg = "source_type base64 is required for audio blocks."
|
||||
raise ValueError(error_msg)
|
||||
else:
|
||||
error_msg = f"Block of type {block['type']} is not supported."
|
||||
raise ValueError(error_msg)
|
||||
|
||||
return formatted_block
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Human message."""
|
||||
|
||||
from typing import Any, Literal, Union
|
||||
from typing import Any, Literal, Optional, Union, cast, overload
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.base import BaseMessage, BaseMessageChunk
|
||||
|
||||
|
||||
@@ -27,14 +28,6 @@ class HumanMessage(BaseMessage):
|
||||
|
||||
"""
|
||||
|
||||
example: bool = False
|
||||
"""Use to denote that a message is part of an example conversation.
|
||||
|
||||
At the moment, this is ignored by most models. Usage is discouraged.
|
||||
Defaults to False.
|
||||
|
||||
"""
|
||||
|
||||
type: Literal["human"] = "human"
|
||||
"""The type of the message (used for serialization).
|
||||
|
||||
@@ -42,18 +35,35 @@ class HumanMessage(BaseMessage):
|
||||
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Union[str, list[Union[str, dict]]],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize ``HumanMessage``.
|
||||
) -> None: ...
|
||||
|
||||
Args:
|
||||
content: The string contents of the message.
|
||||
kwargs: Additional fields to pass to the message.
|
||||
"""
|
||||
super().__init__(content=content, **kwargs)
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Specify ``content`` as positional arg or ``content_blocks`` for typing."""
|
||||
if content_blocks is not None:
|
||||
super().__init__(
|
||||
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
|
||||
class HumanMessageChunk(HumanMessage, BaseMessageChunk):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""System message."""
|
||||
|
||||
from typing import Any, Literal, Union
|
||||
from typing import Any, Literal, Optional, Union, cast, overload
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.base import BaseMessage, BaseMessageChunk
|
||||
|
||||
|
||||
@@ -34,16 +35,35 @@ class SystemMessage(BaseMessage):
|
||||
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, content: Union[str, list[Union[str, dict]]], **kwargs: Any
|
||||
) -> None:
|
||||
"""Pass in content as positional arg.
|
||||
self,
|
||||
content: Union[str, list[Union[str, dict]]],
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
Args:
|
||||
content: The string contents of the message.
|
||||
kwargs: Additional fields to pass to the message.
|
||||
"""
|
||||
super().__init__(content=content, **kwargs)
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Specify ``content`` as positional arg or ``content_blocks`` for typing."""
|
||||
if content_blocks is not None:
|
||||
super().__init__(
|
||||
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
|
||||
class SystemMessageChunk(SystemMessage, BaseMessageChunk):
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""Messages for tools."""
|
||||
|
||||
import json
|
||||
from typing import Any, Literal, Optional, Union
|
||||
from typing import Any, Literal, Optional, Union, cast, overload
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import Field, model_validator
|
||||
from typing_extensions import NotRequired, TypedDict, override
|
||||
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.base import BaseMessage, BaseMessageChunk, merge_content
|
||||
from langchain_core.messages.content import InvalidToolCall
|
||||
from langchain_core.utils._merge import merge_dicts, merge_obj
|
||||
|
||||
|
||||
@@ -142,18 +144,43 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
|
||||
values["tool_call_id"] = str(tool_call_id)
|
||||
return values
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Union[str, list[Union[str, dict]]],
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: Optional[Union[str, list[Union[str, dict]]]] = None,
|
||||
content_blocks: Optional[list[types.ContentBlock]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize ``ToolMessage``.
|
||||
|
||||
Specify ``content`` as positional arg or ``content_blocks`` for typing.
|
||||
|
||||
Args:
|
||||
content: The string contents of the message.
|
||||
content_blocks: Typed standard content.
|
||||
**kwargs: Additional fields.
|
||||
"""
|
||||
super().__init__(content=content, **kwargs)
|
||||
if content_blocks is not None:
|
||||
super().__init__(
|
||||
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
super().__init__(content=content, **kwargs)
|
||||
|
||||
|
||||
class ToolMessageChunk(ToolMessage, BaseMessageChunk):
|
||||
@@ -290,24 +317,6 @@ def tool_call_chunk(
|
||||
)
|
||||
|
||||
|
||||
class InvalidToolCall(TypedDict):
|
||||
"""Allowance for errors made by LLM.
|
||||
|
||||
Here we add an ``error`` key to surface errors made during generation
|
||||
(e.g., invalid JSON arguments.)
|
||||
"""
|
||||
|
||||
name: Optional[str]
|
||||
"""The name of the tool to be called."""
|
||||
args: Optional[str]
|
||||
"""The arguments to the tool call."""
|
||||
id: Optional[str]
|
||||
"""An identifier associated with the tool call."""
|
||||
error: Optional[str]
|
||||
"""An error message associated with the tool call."""
|
||||
type: NotRequired[Literal["invalid_tool_call"]]
|
||||
|
||||
|
||||
def invalid_tool_call(
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
|
||||
@@ -32,10 +32,15 @@ from typing import (
|
||||
from pydantic import Discriminator, Field, Tag
|
||||
|
||||
from langchain_core.exceptions import ErrorCode, create_message
|
||||
from langchain_core.messages import convert_to_openai_data_block, is_data_content_block
|
||||
from langchain_core.messages.ai import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages.base import BaseMessage, BaseMessageChunk
|
||||
from langchain_core.messages.block_translators.openai import (
|
||||
convert_to_openai_data_block,
|
||||
)
|
||||
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
|
||||
from langchain_core.messages.content import (
|
||||
is_data_content_block,
|
||||
)
|
||||
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
|
||||
from langchain_core.messages.human import HumanMessage, HumanMessageChunk
|
||||
from langchain_core.messages.modifier import RemoveMessage
|
||||
@@ -137,7 +142,7 @@ def get_buffer_string(
|
||||
else:
|
||||
msg = f"Got unsupported message type: {m}"
|
||||
raise ValueError(msg) # noqa: TRY004
|
||||
message = f"{role}: {m.text()}"
|
||||
message = f"{role}: {m.text}"
|
||||
if isinstance(m, AIMessage) and "function_call" in m.additional_kwargs:
|
||||
message += f"{m.additional_kwargs['function_call']}"
|
||||
string_messages.append(message)
|
||||
@@ -204,7 +209,7 @@ def message_chunk_to_message(chunk: BaseMessage) -> BaseMessage:
|
||||
# chunk classes always have the equivalent non-chunk class as their first parent
|
||||
ignore_keys = ["type"]
|
||||
if isinstance(chunk, AIMessageChunk):
|
||||
ignore_keys.append("tool_call_chunks")
|
||||
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}
|
||||
)
|
||||
@@ -1040,7 +1045,6 @@ def convert_to_openai_messages(
|
||||
messages: Union[MessageLikeRepresentation, Sequence[MessageLikeRepresentation]],
|
||||
*,
|
||||
text_format: Literal["string", "block"] = "string",
|
||||
include_id: bool = False,
|
||||
) -> Union[dict, list[dict]]:
|
||||
"""Convert LangChain messages into OpenAI message dicts.
|
||||
|
||||
@@ -1058,8 +1062,6 @@ def convert_to_openai_messages(
|
||||
If a message has a string content, this is turned into a list
|
||||
with a single content block of type ``'text'``. If a message has
|
||||
content blocks these are left as is.
|
||||
include_id: Whether to include message ids in the openai messages, if they
|
||||
are present in the source messages.
|
||||
|
||||
Raises:
|
||||
ValueError: if an unrecognized ``text_format`` is specified, or if a message
|
||||
@@ -1148,8 +1150,6 @@ def convert_to_openai_messages(
|
||||
oai_msg["refusal"] = message.additional_kwargs["refusal"]
|
||||
if isinstance(message, ToolMessage):
|
||||
oai_msg["tool_call_id"] = message.tool_call_id
|
||||
if include_id and message.id:
|
||||
oai_msg["id"] = message.id
|
||||
|
||||
if not message.content:
|
||||
content = "" if text_format == "string" else []
|
||||
@@ -1622,11 +1622,15 @@ def _msg_to_chunk(message: BaseMessage) -> BaseMessageChunk:
|
||||
def _chunk_to_msg(chunk: BaseMessageChunk) -> BaseMessage:
|
||||
if chunk.__class__ in _CHUNK_MSG_MAP:
|
||||
return _CHUNK_MSG_MAP[chunk.__class__](
|
||||
**chunk.model_dump(exclude={"type", "tool_call_chunks"})
|
||||
**chunk.model_dump(exclude={"type", "tool_call_chunks", "chunk_position"})
|
||||
)
|
||||
for chunk_cls, msg_cls in _CHUNK_MSG_MAP.items():
|
||||
if isinstance(chunk, chunk_cls):
|
||||
return msg_cls(**chunk.model_dump(exclude={"type", "tool_call_chunks"}))
|
||||
return msg_cls(
|
||||
**chunk.model_dump(
|
||||
exclude={"type", "tool_call_chunks", "chunk_position"}
|
||||
)
|
||||
)
|
||||
|
||||
msg = (
|
||||
f"Unrecognized message chunk class {chunk.__class__}. Supported classes are "
|
||||
|
||||
@@ -133,7 +133,7 @@ class ImagePromptValue(PromptValue):
|
||||
|
||||
def to_string(self) -> str:
|
||||
"""Return prompt (image URL) as string."""
|
||||
return self.image_url["url"]
|
||||
return self.image_url.get("url", "")
|
||||
|
||||
def to_messages(self) -> list[BaseMessage]:
|
||||
"""Return prompt (image URL) as messages."""
|
||||
|
||||
@@ -379,10 +379,10 @@ class BasePromptTemplate(
|
||||
directory_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if save_path.suffix == ".json":
|
||||
with save_path.open("w") as f:
|
||||
with save_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(prompt_dict, f, indent=4)
|
||||
elif save_path.suffix.endswith((".yaml", ".yml")):
|
||||
with save_path.open("w") as f:
|
||||
with save_path.open("w", encoding="utf-8") as f:
|
||||
yaml.dump(prompt_dict, f, default_flow_style=False)
|
||||
else:
|
||||
msg = f"{save_path} must be json or yaml"
|
||||
|
||||
@@ -543,8 +543,7 @@ class _StringImageMessagePromptTemplate(BaseMessagePromptTemplate):
|
||||
Returns:
|
||||
A new instance of this class.
|
||||
"""
|
||||
template = Path(template_file).read_text()
|
||||
# TODO: .read_text(encoding="utf-8") for v0.4
|
||||
template = Path(template_file).read_text(encoding="utf-8")
|
||||
return cls.from_template(template, input_variables=input_variables, **kwargs)
|
||||
|
||||
def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
|
||||
|
||||
@@ -53,7 +53,7 @@ def _load_template(var_name: str, config: dict) -> dict:
|
||||
template_path = Path(config.pop(f"{var_name}_path"))
|
||||
# Load the template.
|
||||
if template_path.suffix == ".txt":
|
||||
template = template_path.read_text()
|
||||
template = template_path.read_text(encoding="utf-8")
|
||||
else:
|
||||
raise ValueError
|
||||
# Set the template variable to the extracted variable.
|
||||
@@ -67,7 +67,7 @@ def _load_examples(config: dict) -> dict:
|
||||
pass
|
||||
elif isinstance(config["examples"], str):
|
||||
path = Path(config["examples"])
|
||||
with path.open() as f:
|
||||
with path.open(encoding="utf-8") as f:
|
||||
if path.suffix == ".json":
|
||||
examples = json.load(f)
|
||||
elif path.suffix in {".yaml", ".yml"}:
|
||||
|
||||
@@ -4,9 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from abc import ABC
|
||||
from collections.abc import Callable, Sequence
|
||||
from string import Formatter
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Callable, Literal
|
||||
|
||||
from pydantic import BaseModel, create_model
|
||||
|
||||
@@ -17,66 +16,9 @@ from langchain_core.utils.formatting import formatter
|
||||
from langchain_core.utils.interactive_env import is_interactive_env
|
||||
|
||||
try:
|
||||
from jinja2 import meta
|
||||
from jinja2.exceptions import SecurityError
|
||||
from jinja2 import Environment, meta
|
||||
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
|
||||
@@ -116,10 +58,14 @@ def jinja2_formatter(template: str, /, **kwargs: Any) -> str:
|
||||
)
|
||||
raise ImportError(msg)
|
||||
|
||||
# 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)
|
||||
# This uses a sandboxed environment to prevent arbitrary code execution.
|
||||
# Jinja2 uses an opt-out rather than opt-in approach for sand-boxing.
|
||||
# Please treat this sand-boxing as a best-effort approach rather than
|
||||
# a guarantee of security.
|
||||
# We recommend to never use jinja2 templates with untrusted inputs.
|
||||
# https://jinja.palletsprojects.com/en/3.1.x/sandbox/
|
||||
# approach not a guarantee of security.
|
||||
return SandboxedEnvironment().from_string(template).render(**kwargs)
|
||||
|
||||
|
||||
def validate_jinja2(template: str, input_variables: list[str]) -> None:
|
||||
@@ -154,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 = Environment() # noqa: S701
|
||||
ast = env.parse(template)
|
||||
return meta.find_undeclared_variables(ast)
|
||||
|
||||
@@ -202,7 +148,9 @@ def mustache_template_vars(
|
||||
Defs = dict[str, "Defs"]
|
||||
|
||||
|
||||
def mustache_schema(template: str) -> type[BaseModel]:
|
||||
def mustache_schema(
|
||||
template: str,
|
||||
) -> type[BaseModel]:
|
||||
"""Get the variables from a mustache template.
|
||||
|
||||
Args:
|
||||
@@ -226,11 +174,6 @@ def mustache_schema(template: str) -> type[BaseModel]:
|
||||
fields[prefix] = False
|
||||
elif type_ in {"variable", "no escape"}:
|
||||
fields[prefix + tuple(key.split("."))] = True
|
||||
|
||||
for fkey, fval in fields.items():
|
||||
fields[fkey] = fval and not any(
|
||||
is_subsequence(fkey, k) for k in fields if k != fkey
|
||||
)
|
||||
defs: Defs = {} # None means leaf node
|
||||
while fields:
|
||||
field, is_leaf = fields.popitem()
|
||||
@@ -321,30 +264,6 @@ def get_template_variables(template: str, template_format: str) -> list[str]:
|
||||
msg = f"Unsupported template format: {template_format}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# For f-strings, block attribute access and indexing syntax
|
||||
# This prevents template injection attacks via accessing dangerous attributes
|
||||
if template_format == "f-string":
|
||||
for var in input_variables:
|
||||
# Formatter().parse() returns field names with dots/brackets if present
|
||||
# e.g., "obj.attr" or "obj[0]" - we need to block these
|
||||
if "." in var or "[" in var or "]" in var:
|
||||
msg = (
|
||||
f"Invalid variable name {var!r} in f-string template. "
|
||||
f"Variable names cannot contain attribute "
|
||||
f"access (.) or indexing ([])."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
# Block variable names that are all digits (e.g., "0", "100")
|
||||
# These are interpreted as positional arguments, not keyword arguments
|
||||
if var.isdigit():
|
||||
msg = (
|
||||
f"Invalid variable name {var!r} in f-string template. "
|
||||
f"Variable names cannot be all digits as they are interpreted "
|
||||
f"as positional arguments."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
return sorted(input_variables)
|
||||
|
||||
|
||||
@@ -407,12 +326,3 @@ class StringPromptTemplate(BasePromptTemplate, ABC):
|
||||
def pretty_print(self) -> None:
|
||||
"""Print a pretty representation of the prompt."""
|
||||
print(self.pretty_repr(html=is_interactive_env())) # noqa: T201
|
||||
|
||||
|
||||
def is_subsequence(child: Sequence, parent: Sequence) -> bool:
|
||||
"""Return True if child is subsequence of parent."""
|
||||
if len(child) == 0 or len(parent) == 0:
|
||||
return False
|
||||
if len(parent) < len(child):
|
||||
return False
|
||||
return all(child[i] == parent[i] for i in range(len(child)))
|
||||
|
||||
@@ -20,7 +20,7 @@ from collections.abc import (
|
||||
)
|
||||
from concurrent.futures import FIRST_COMPLETED, wait
|
||||
from functools import wraps
|
||||
from itertools import groupby, tee
|
||||
from itertools import tee
|
||||
from operator import itemgetter
|
||||
from types import GenericAlias
|
||||
from typing import (
|
||||
@@ -28,17 +28,19 @@ from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Literal,
|
||||
Optional,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
get_type_hints,
|
||||
overload,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, RootModel
|
||||
from typing_extensions import Literal, get_args, override
|
||||
from typing_extensions import override
|
||||
|
||||
from langchain_core._api import beta_decorator
|
||||
from langchain_core.callbacks.manager import AsyncCallbackManager, CallbackManager
|
||||
@@ -1653,7 +1655,7 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
from langchain_ollama import ChatOllama
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
|
||||
llm = ChatOllama(model="llama2")
|
||||
llm = ChatOllama(model="llama3.1")
|
||||
|
||||
# Without bind.
|
||||
chain = llm | StrOutputParser()
|
||||
@@ -2329,9 +2331,6 @@ 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
|
||||
@@ -2348,7 +2347,6 @@ 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())
|
||||
@@ -2436,9 +2434,6 @@ 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
|
||||
@@ -2455,7 +2450,6 @@ 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())
|
||||
@@ -3076,50 +3070,10 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
|
||||
"""
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
|
||||
CONTEXT_CONFIG_PREFIX,
|
||||
_key_from_id,
|
||||
return get_unique_config_specs(
|
||||
[spec for step in self.steps for spec in step.config_specs]
|
||||
)
|
||||
|
||||
# get all specs
|
||||
all_specs = [
|
||||
(spec, idx)
|
||||
for idx, step in enumerate(self.steps)
|
||||
for spec in step.config_specs
|
||||
]
|
||||
# calculate context dependencies
|
||||
specs_by_pos = groupby(
|
||||
[tup for tup in all_specs if tup[0].id.startswith(CONTEXT_CONFIG_PREFIX)],
|
||||
itemgetter(1),
|
||||
)
|
||||
next_deps: set[str] = set()
|
||||
deps_by_pos: dict[int, set[str]] = {}
|
||||
for pos, specs in specs_by_pos:
|
||||
deps_by_pos[pos] = next_deps
|
||||
next_deps = next_deps | {spec[0].id for spec in specs}
|
||||
# assign context dependencies
|
||||
for pos, (spec, idx) in enumerate(all_specs):
|
||||
if spec.id.startswith(CONTEXT_CONFIG_PREFIX):
|
||||
all_specs[pos] = (
|
||||
ConfigurableFieldSpec(
|
||||
id=spec.id,
|
||||
annotation=spec.annotation,
|
||||
name=spec.name,
|
||||
default=spec.default,
|
||||
description=spec.description,
|
||||
is_shared=spec.is_shared,
|
||||
dependencies=[
|
||||
d
|
||||
for d in deps_by_pos[idx]
|
||||
if _key_from_id(d) != _key_from_id(spec.id)
|
||||
]
|
||||
+ (spec.dependencies or []),
|
||||
),
|
||||
idx,
|
||||
)
|
||||
|
||||
return get_unique_config_specs(spec for spec, _ in all_specs)
|
||||
|
||||
@override
|
||||
def get_graph(self, config: Optional[RunnableConfig] = None) -> Graph:
|
||||
"""Get the graph representation of the ``Runnable``.
|
||||
@@ -3223,13 +3177,8 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
def invoke(
|
||||
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Output:
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
|
||||
config_with_context,
|
||||
)
|
||||
|
||||
# setup callbacks and context
|
||||
config = config_with_context(ensure_config(config), self.steps)
|
||||
config = ensure_config(config)
|
||||
callback_manager = get_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = callback_manager.on_chain_start(
|
||||
@@ -3267,13 +3216,8 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Output:
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
|
||||
aconfig_with_context,
|
||||
)
|
||||
|
||||
# setup callbacks and context
|
||||
config = aconfig_with_context(ensure_config(config), self.steps)
|
||||
config = ensure_config(config)
|
||||
callback_manager = get_async_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = await callback_manager.on_chain_start(
|
||||
@@ -3314,19 +3258,11 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Optional[Any],
|
||||
) -> list[Output]:
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
|
||||
config_with_context,
|
||||
)
|
||||
|
||||
if not inputs:
|
||||
return []
|
||||
|
||||
# setup callbacks and context
|
||||
configs = [
|
||||
config_with_context(c, self.steps)
|
||||
for c in get_config_list(config, len(inputs))
|
||||
]
|
||||
configs = get_config_list(config, len(inputs))
|
||||
callback_managers = [
|
||||
CallbackManager.configure(
|
||||
inheritable_callbacks=config.get("callbacks"),
|
||||
@@ -3446,19 +3382,11 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Optional[Any],
|
||||
) -> list[Output]:
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
|
||||
aconfig_with_context,
|
||||
)
|
||||
|
||||
if not inputs:
|
||||
return []
|
||||
|
||||
# setup callbacks and context
|
||||
configs = [
|
||||
aconfig_with_context(c, self.steps)
|
||||
for c in get_config_list(config, len(inputs))
|
||||
]
|
||||
configs = get_config_list(config, len(inputs))
|
||||
callback_managers = [
|
||||
AsyncCallbackManager.configure(
|
||||
inheritable_callbacks=config.get("callbacks"),
|
||||
@@ -3579,14 +3507,7 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
config: RunnableConfig,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[Output]:
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
|
||||
config_with_context,
|
||||
)
|
||||
|
||||
steps = [self.first, *self.middle, self.last]
|
||||
config = config_with_context(config, self.steps)
|
||||
|
||||
# transform the input stream of each step with the next
|
||||
# steps that don't natively support transforming an input stream will
|
||||
# buffer input in memory until all available, and then start emitting output
|
||||
@@ -3609,14 +3530,7 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
|
||||
config: RunnableConfig,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[Output]:
|
||||
# Import locally to prevent circular import
|
||||
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
|
||||
aconfig_with_context,
|
||||
)
|
||||
|
||||
steps = [self.first, *self.middle, self.last]
|
||||
config = aconfig_with_context(config, self.steps)
|
||||
|
||||
# stream the last steps
|
||||
# transform the input stream of each step with the next
|
||||
# steps that don't natively support transforming an input stream will
|
||||
@@ -4470,7 +4384,6 @@ class RunnableGenerator(Runnable[Input, Output]):
|
||||
input,
|
||||
self._transform, # type: ignore[arg-type]
|
||||
config,
|
||||
defers_inputs=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -4504,7 +4417,7 @@ class RunnableGenerator(Runnable[Input, Output]):
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
return self._atransform_stream_with_config(
|
||||
input, self._atransform, config, defers_inputs=True, **kwargs
|
||||
input, self._atransform, config, **kwargs
|
||||
)
|
||||
|
||||
@override
|
||||
|
||||
@@ -12,10 +12,6 @@ from typing import (
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing_extensions import override
|
||||
|
||||
from langchain_core.beta.runnables.context import (
|
||||
CONTEXT_CONFIG_PREFIX,
|
||||
CONTEXT_CONFIG_SUFFIX_SET,
|
||||
)
|
||||
from langchain_core.runnables.base import (
|
||||
Runnable,
|
||||
RunnableLike,
|
||||
@@ -181,7 +177,7 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
@property
|
||||
@override
|
||||
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
||||
specs = get_unique_config_specs(
|
||||
return get_unique_config_specs(
|
||||
spec
|
||||
for step in (
|
||||
[self.default]
|
||||
@@ -190,14 +186,6 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
|
||||
)
|
||||
for spec in step.config_specs
|
||||
)
|
||||
if any(
|
||||
s.id.startswith(CONTEXT_CONFIG_PREFIX)
|
||||
and s.id.endswith(CONTEXT_CONFIG_SUFFIX_SET)
|
||||
for s in specs
|
||||
):
|
||||
msg = "RunnableBranch cannot contain context setters."
|
||||
raise ValueError(msg)
|
||||
return specs
|
||||
|
||||
@override
|
||||
def invoke(
|
||||
|
||||
@@ -10,10 +10,19 @@ from concurrent.futures import Executor, Future, ThreadPoolExecutor
|
||||
from contextlib import contextmanager
|
||||
from contextvars import Context, ContextVar, Token, copy_context
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, cast
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
ParamSpec,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from langsmith.run_helpers import _set_tracing_context, get_tracing_context
|
||||
from typing_extensions import ParamSpec, TypedDict
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langchain_core.callbacks.manager import AsyncCallbackManager, CallbackManager
|
||||
from langchain_core.runnables.utils import (
|
||||
|
||||
@@ -541,7 +541,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
hist: BaseChatMessageHistory = config["configurable"]["message_history"]
|
||||
|
||||
# Get the input messages
|
||||
inputs = load(run.inputs, allowed_objects="all")
|
||||
inputs = load(run.inputs)
|
||||
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.
|
||||
@@ -550,7 +550,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
input_messages = input_messages[len(historic_messages) :]
|
||||
|
||||
# Get the output messages
|
||||
output_val = load(run.outputs, allowed_objects="all")
|
||||
output_val = load(run.outputs)
|
||||
output_messages = self._get_output_messages(output_val)
|
||||
hist.add_messages(input_messages + output_messages)
|
||||
|
||||
@@ -558,7 +558,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
hist: BaseChatMessageHistory = config["configurable"]["message_history"]
|
||||
|
||||
# Get the input messages
|
||||
inputs = load(run.inputs, allowed_objects="all")
|
||||
inputs = load(run.inputs)
|
||||
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.
|
||||
@@ -567,7 +567,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
|
||||
input_messages = input_messages[len(historic_messages) :]
|
||||
|
||||
# Get the output messages
|
||||
output_val = load(run.outputs, allowed_objects="all")
|
||||
output_val = load(run.outputs)
|
||||
output_messages = self._get_output_messages(output_val)
|
||||
await hist.aadd_messages(input_messages + output_messages)
|
||||
|
||||
|
||||
@@ -23,6 +23,13 @@ class EventData(TypedDict, total=False):
|
||||
won't be known until the *END* of the Runnable when it has finished streaming
|
||||
its inputs.
|
||||
"""
|
||||
error: NotRequired[BaseException]
|
||||
"""The error that occurred during the execution of the Runnable.
|
||||
|
||||
This field is only available if the Runnable raised an exception.
|
||||
|
||||
.. versionadded:: 1.0.0
|
||||
"""
|
||||
output: Any
|
||||
"""The output of the Runnable that generated the event.
|
||||
|
||||
|
||||
@@ -18,11 +18,12 @@ from typing import (
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Protocol,
|
||||
TypeGuard,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeGuard, override
|
||||
from typing_extensions import override
|
||||
|
||||
# Re-export create-model for backwards compatibility
|
||||
from langchain_core.utils.pydantic import create_model # noqa: F401
|
||||
|
||||
@@ -1272,7 +1272,7 @@ class InjectedToolCallId(InjectedToolArg):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from typing_extensions import Annotated
|
||||
from typing import Annotated
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langchain_core.tools import tool, InjectedToolCallId
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timezone
|
||||
@@ -98,12 +97,7 @@ class _TracerCore(ABC):
|
||||
"""Get the stacktrace of the parent error."""
|
||||
msg = repr(error)
|
||||
try:
|
||||
if sys.version_info < (3, 10):
|
||||
tb = traceback.format_exception(
|
||||
error.__class__, error, error.__traceback__
|
||||
)
|
||||
else:
|
||||
tb = traceback.format_exception(error)
|
||||
tb = traceback.format_exception(error)
|
||||
return (msg + "\n\n".join(tb)).strip()
|
||||
except: # noqa: E722
|
||||
return msg
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import (
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from uuid import UUID
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from typing_extensions import NotRequired, override
|
||||
|
||||
@@ -45,7 +45,6 @@ from langchain_core.tracers.log_stream import (
|
||||
)
|
||||
from langchain_core.tracers.memory_stream import _MemoryStream
|
||||
from langchain_core.utils.aiter import aclosing, py_anext
|
||||
from langchain_core.utils.uuid import uuid7
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||
@@ -611,6 +610,28 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
run_type,
|
||||
)
|
||||
|
||||
def _get_tool_run_info_with_inputs(self, run_id: UUID) -> tuple[RunInfo, Any]:
|
||||
"""Get run info for a tool and extract inputs, with validation.
|
||||
|
||||
Args:
|
||||
run_id: The run ID of the tool.
|
||||
|
||||
Returns:
|
||||
A tuple of (run_info, inputs).
|
||||
|
||||
Raises:
|
||||
AssertionError: If the run ID is a tool call and does not have inputs.
|
||||
"""
|
||||
run_info = self.run_map.pop(run_id)
|
||||
if "inputs" not in run_info:
|
||||
msg = (
|
||||
f"Run ID {run_id} is a tool call and is expected to have "
|
||||
f"inputs associated with it."
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
inputs = run_info["inputs"]
|
||||
return run_info, inputs
|
||||
|
||||
@override
|
||||
async def on_tool_start(
|
||||
self,
|
||||
@@ -653,6 +674,35 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
"tool",
|
||||
)
|
||||
|
||||
@override
|
||||
async def on_tool_error(
|
||||
self,
|
||||
error: BaseException,
|
||||
*,
|
||||
run_id: UUID,
|
||||
parent_run_id: Optional[UUID] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Run when tool errors."""
|
||||
run_info, inputs = self._get_tool_run_info_with_inputs(run_id)
|
||||
|
||||
self._send(
|
||||
{
|
||||
"event": "on_tool_error",
|
||||
"data": {
|
||||
"error": error,
|
||||
"input": inputs,
|
||||
},
|
||||
"run_id": str(run_id),
|
||||
"name": run_info["name"],
|
||||
"tags": run_info["tags"],
|
||||
"metadata": run_info["metadata"],
|
||||
"parent_ids": self._get_parent_ids(run_id),
|
||||
},
|
||||
"tool",
|
||||
)
|
||||
|
||||
@override
|
||||
async def on_tool_end(self, output: Any, *, run_id: UUID, **kwargs: Any) -> None:
|
||||
"""End a trace for a tool run.
|
||||
@@ -660,14 +710,7 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
Raises:
|
||||
AssertionError: If the run ID is a tool call and does not have inputs
|
||||
"""
|
||||
run_info = self.run_map.pop(run_id)
|
||||
if "inputs" not in run_info:
|
||||
msg = (
|
||||
f"Run ID {run_id} is a tool call and is expected to have "
|
||||
f"inputs associated with it."
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
inputs = run_info["inputs"]
|
||||
run_info, inputs = self._get_tool_run_info_with_inputs(run_id)
|
||||
|
||||
self._send(
|
||||
{
|
||||
@@ -962,11 +1005,7 @@ async def _astream_events_implementation_v2(
|
||||
|
||||
# Assign the stream handler to the config
|
||||
config = ensure_config(config)
|
||||
if "run_id" in config:
|
||||
run_id = cast("UUID", config["run_id"])
|
||||
else:
|
||||
run_id = uuid7()
|
||||
config["run_id"] = run_id
|
||||
run_id = cast("UUID", config.setdefault("run_id", uuid4()))
|
||||
callbacks = config.get("callbacks")
|
||||
if callbacks is None:
|
||||
config["callbacks"] = [event_streamer]
|
||||
|
||||
@@ -21,7 +21,6 @@ from typing_extensions import override
|
||||
|
||||
from langchain_core.env import get_runtime_environment
|
||||
from langchain_core.load import dumpd
|
||||
from langchain_core.messages.ai import UsageMetadata, add_usage
|
||||
from langchain_core.tracers.base import BaseTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
|
||||
@@ -70,32 +69,6 @@ def _get_executor() -> ThreadPoolExecutor:
|
||||
return _EXECUTOR
|
||||
|
||||
|
||||
def _get_usage_metadata_from_generations(
|
||||
generations: list[list[dict[str, Any]]],
|
||||
) -> UsageMetadata | None:
|
||||
"""Extract and aggregate `usage_metadata` from generations.
|
||||
|
||||
Iterates through generations to find and aggregate all `usage_metadata` found in
|
||||
messages. This is typically present in chat model outputs.
|
||||
|
||||
Args:
|
||||
generations: List of generation batches, where each batch is a list
|
||||
of generation dicts that may contain a `'message'` key with
|
||||
`'usage_metadata'`.
|
||||
|
||||
Returns:
|
||||
The aggregated `usage_metadata` dict if found, otherwise `None`.
|
||||
"""
|
||||
output: UsageMetadata | None = None
|
||||
for generation_batch in generations:
|
||||
for generation in generation_batch:
|
||||
if isinstance(generation, dict) and "message" in generation:
|
||||
message = generation["message"]
|
||||
if isinstance(message, dict) and "usage_metadata" in message:
|
||||
output = add_usage(output, message["usage_metadata"])
|
||||
return output
|
||||
|
||||
|
||||
class LangChainTracer(BaseTracer):
|
||||
"""Implementation of the SharedTracer that POSTS to the LangChain endpoint."""
|
||||
|
||||
@@ -293,15 +266,6 @@ class LangChainTracer(BaseTracer):
|
||||
|
||||
def _on_llm_end(self, run: Run) -> None:
|
||||
"""Process the LLM Run."""
|
||||
# Extract usage_metadata from outputs and store in extra.metadata
|
||||
if run.outputs and "generations" in run.outputs:
|
||||
usage_metadata = _get_usage_metadata_from_generations(
|
||||
run.outputs["generations"]
|
||||
)
|
||||
if usage_metadata is not None:
|
||||
if "metadata" not in run.extra:
|
||||
run.extra["metadata"] = {}
|
||||
run.extra["metadata"]["usage_metadata"] = usage_metadata
|
||||
self._update_run_single(run)
|
||||
|
||||
def _on_llm_error(self, run: Run) -> None:
|
||||
@@ -312,28 +276,15 @@ class LangChainTracer(BaseTracer):
|
||||
"""Process the Chain Run upon start."""
|
||||
if run.parent_run_id is None:
|
||||
run.reference_example_id = self.example_id
|
||||
# Skip persisting if inputs are deferred (e.g., iterator/generator inputs).
|
||||
# The run will be posted when _on_chain_end is called with realized inputs.
|
||||
if not run.extra.get("defers_inputs"):
|
||||
self._persist_run_single(run)
|
||||
self._persist_run_single(run)
|
||||
|
||||
def _on_chain_end(self, run: Run) -> None:
|
||||
"""Process the Chain Run."""
|
||||
# If inputs were deferred, persist (POST) the run now that inputs are realized.
|
||||
# Otherwise, update (PATCH) the existing run.
|
||||
if run.extra.get("defers_inputs"):
|
||||
self._persist_run_single(run)
|
||||
else:
|
||||
self._update_run_single(run)
|
||||
self._update_run_single(run)
|
||||
|
||||
def _on_chain_error(self, run: Run) -> None:
|
||||
"""Process the Chain Run upon error."""
|
||||
# If inputs were deferred, persist (POST) the run now that inputs are realized.
|
||||
# Otherwise, update (PATCH) the existing run.
|
||||
if run.extra.get("defers_inputs"):
|
||||
self._persist_run_single(run)
|
||||
else:
|
||||
self._update_run_single(run)
|
||||
self._update_run_single(run)
|
||||
|
||||
def _on_tool_start(self, run: Run) -> None:
|
||||
"""Process the Tool Run upon start."""
|
||||
|
||||
@@ -563,7 +563,7 @@ def _get_standardized_inputs(
|
||||
)
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
inputs = load(run.inputs, allowed_objects="all")
|
||||
inputs = load(run.inputs)
|
||||
|
||||
if run.run_type in {"retriever", "llm", "chat_model"}:
|
||||
return inputs
|
||||
@@ -595,7 +595,7 @@ def _get_standardized_outputs(
|
||||
Returns:
|
||||
An output if returned, otherwise a None
|
||||
"""
|
||||
outputs = load(run.outputs, allowed_objects="all")
|
||||
outputs = load(run.outputs)
|
||||
if schema_format == "original":
|
||||
if run.run_type == "prompt" and "output" in outputs:
|
||||
# These were previously dumped before the tracer.
|
||||
|
||||
@@ -57,6 +57,11 @@ def merge_dicts(left: dict[str, Any], *others: dict[str, Any]) -> dict[str, Any]
|
||||
# "should either occur once or have the same value across "
|
||||
# "all dicts."
|
||||
# )
|
||||
if (right_k == "index" and merged[right_k].startswith("lc_")) or (
|
||||
right_k in ("id", "output_version", "model_provider")
|
||||
and merged[right_k] == right_v
|
||||
):
|
||||
continue
|
||||
merged[right_k] += right_v
|
||||
elif isinstance(merged[right_k], dict):
|
||||
merged[right_k] = merge_dicts(merged[right_k], right_v)
|
||||
@@ -93,7 +98,16 @@ def merge_lists(left: Optional[list], *others: Optional[list]) -> Optional[list]
|
||||
merged = other.copy()
|
||||
else:
|
||||
for e in other:
|
||||
if isinstance(e, dict) and "index" in e and isinstance(e["index"], int):
|
||||
if (
|
||||
isinstance(e, dict)
|
||||
and "index" in e
|
||||
and (
|
||||
isinstance(e["index"], int)
|
||||
or (
|
||||
isinstance(e["index"], str) and e["index"].startswith("lc_")
|
||||
)
|
||||
)
|
||||
):
|
||||
to_merge = [
|
||||
i
|
||||
for i, e_left in enumerate(merged)
|
||||
@@ -102,11 +116,35 @@ def merge_lists(left: Optional[list], *others: Optional[list]) -> Optional[list]
|
||||
if to_merge:
|
||||
# TODO: Remove this once merge_dict is updated with special
|
||||
# handling for 'type'.
|
||||
new_e = (
|
||||
{k: v for k, v in e.items() if k != "type"}
|
||||
if "type" in e
|
||||
else e
|
||||
)
|
||||
if (left_type := merged[to_merge[0]].get("type")) and (
|
||||
e.get("type") == "non_standard" and "value" in e
|
||||
):
|
||||
if left_type != "non_standard":
|
||||
# standard + non_standard
|
||||
new_e: dict[str, Any] = {
|
||||
"extras": {
|
||||
k: v
|
||||
for k, v in e["value"].items()
|
||||
if k != "type"
|
||||
}
|
||||
}
|
||||
else:
|
||||
# non_standard + non_standard
|
||||
new_e = {
|
||||
"value": {
|
||||
k: v
|
||||
for k, v in e["value"].items()
|
||||
if k != "type"
|
||||
}
|
||||
}
|
||||
if "index" in e:
|
||||
new_e["index"] = e["index"]
|
||||
else:
|
||||
new_e = (
|
||||
{k: v for k, v in e.items() if k != "type"}
|
||||
if "type" in e
|
||||
else e
|
||||
)
|
||||
merged[to_merge[0]] = merge_dicts(merged[to_merge[0]], new_e)
|
||||
else:
|
||||
merged.append(e)
|
||||
|
||||
@@ -17,12 +17,15 @@ from typing import (
|
||||
Optional,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import Field, create_model
|
||||
from typing_extensions import TypedDict, get_args, get_origin, is_typeddict
|
||||
from pydantic.v1 import Field as Field_v1
|
||||
from pydantic.v1 import create_model as create_model_v1
|
||||
from typing_extensions import TypedDict, is_typeddict
|
||||
|
||||
import langchain_core
|
||||
from langchain_core._api import beta, deprecated
|
||||
@@ -294,7 +297,7 @@ def _convert_any_typed_dicts_to_pydantic(
|
||||
raise ValueError(msg)
|
||||
if arg_desc := arg_descriptions.get(arg):
|
||||
field_kwargs["description"] = arg_desc
|
||||
fields[arg] = (new_arg_type, Field(**field_kwargs))
|
||||
fields[arg] = (new_arg_type, Field_v1(**field_kwargs))
|
||||
else:
|
||||
new_arg_type = _convert_any_typed_dicts_to_pydantic(
|
||||
arg_type, depth=depth + 1, visited=visited
|
||||
@@ -302,8 +305,8 @@ def _convert_any_typed_dicts_to_pydantic(
|
||||
field_kwargs = {"default": ...}
|
||||
if arg_desc := arg_descriptions.get(arg):
|
||||
field_kwargs["description"] = arg_desc
|
||||
fields[arg] = (new_arg_type, Field(**field_kwargs))
|
||||
model = create_model(typed_dict.__name__, **fields)
|
||||
fields[arg] = (new_arg_type, Field_v1(**field_kwargs))
|
||||
model = create_model_v1(typed_dict.__name__, **fields)
|
||||
model.__doc__ = description
|
||||
visited[typed_dict] = model
|
||||
return model
|
||||
|
||||
@@ -8,14 +8,13 @@ from types import TracebackType
|
||||
from typing import (
|
||||
Any,
|
||||
Generic,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
from typing import TypeAlias
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -376,29 +376,15 @@ def _get_key(
|
||||
if resolved_scope in (0, False):
|
||||
return resolved_scope
|
||||
# Move into the scope
|
||||
if isinstance(resolved_scope, dict):
|
||||
try:
|
||||
# Try subscripting (Normal dictionaries)
|
||||
resolved_scope = cast("dict[str, Any]", resolved_scope)[child]
|
||||
except (TypeError, AttributeError):
|
||||
try:
|
||||
resolved_scope = resolved_scope[child]
|
||||
except (KeyError, TypeError):
|
||||
# Key not found - will be caught by outer try-except
|
||||
msg = f"Key {child!r} not found in dict"
|
||||
raise KeyError(msg) from None
|
||||
elif isinstance(resolved_scope, (list, tuple)):
|
||||
try:
|
||||
resolved_scope = resolved_scope[int(child)]
|
||||
except (ValueError, IndexError, TypeError):
|
||||
# Invalid index - will be caught by outer try-except
|
||||
msg = f"Invalid index {child!r} for list/tuple"
|
||||
raise IndexError(msg) from None
|
||||
else:
|
||||
# Reject everything else for security
|
||||
# This prevents traversing into arbitrary Python objects
|
||||
msg = (
|
||||
f"Cannot traverse into {type(resolved_scope).__name__}. "
|
||||
"Mustache templates only support dict, list, and tuple. "
|
||||
f"Got: {type(resolved_scope)}"
|
||||
)
|
||||
raise TypeError(msg) # noqa: TRY301
|
||||
resolved_scope = getattr(resolved_scope, child)
|
||||
except (TypeError, AttributeError):
|
||||
# Try as a list
|
||||
resolved_scope = resolved_scope[int(child)] # type: ignore[index]
|
||||
|
||||
try:
|
||||
# This allows for custom falsy data types
|
||||
@@ -409,9 +395,8 @@ def _get_key(
|
||||
if resolved_scope in (0, False):
|
||||
return resolved_scope
|
||||
return resolved_scope or ""
|
||||
except (AttributeError, KeyError, IndexError, ValueError, TypeError):
|
||||
except (AttributeError, KeyError, IndexError, ValueError):
|
||||
# We couldn't find the key in the current scope
|
||||
# TypeError: Attempted to traverse into non-dict/list type
|
||||
# We'll try again on the next pass
|
||||
pass
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import warnings
|
||||
from collections.abc import Iterator, Sequence
|
||||
from importlib.metadata import version
|
||||
from typing import Any, Callable, Optional, Union, overload
|
||||
from uuid import uuid4
|
||||
|
||||
from packaging.version import parse
|
||||
from pydantic import SecretStr
|
||||
@@ -482,3 +483,31 @@ def secret_from_env(
|
||||
raise ValueError(msg)
|
||||
|
||||
return get_secret_from_env
|
||||
|
||||
|
||||
LC_AUTO_PREFIX = "lc_"
|
||||
"""LangChain auto-generated ID prefix for messages and content blocks."""
|
||||
|
||||
LC_ID_PREFIX = "lc_run-"
|
||||
"""Internal tracing/callback system identifier.
|
||||
|
||||
Used for:
|
||||
- Tracing. Every LangChain operation (LLM call, chain execution, tool use, etc.)
|
||||
gets a unique run_id (UUID)
|
||||
- Enables tracking parent-child relationships between operations
|
||||
"""
|
||||
|
||||
|
||||
def ensure_id(id_val: Optional[str]) -> str:
|
||||
"""Ensure the ID is a valid string, generating a new UUID if not provided.
|
||||
|
||||
Auto-generated UUIDs are prefixed by ``'lc_'`` to indicate they are
|
||||
LangChain-generated IDs.
|
||||
|
||||
Args:
|
||||
id_val: Optional string ID value to validate.
|
||||
|
||||
Returns:
|
||||
A string ID, either the validated provided value or a newly generated UUID4.
|
||||
"""
|
||||
return id_val or str(f"{LC_AUTO_PREFIX}{uuid4()}")
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
"""UUID utility functions.
|
||||
|
||||
This module exports a uuid7 function to generate monotonic, time-ordered UUIDs
|
||||
for tracing and similar operations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from uuid import UUID
|
||||
|
||||
from uuid_utils.compat import uuid7 as _uuid_utils_uuid7
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from uuid import UUID
|
||||
|
||||
_NANOS_PER_SECOND: typing.Final = 1_000_000_000
|
||||
|
||||
|
||||
def _to_timestamp_and_nanos(nanoseconds: int) -> tuple[int, int]:
|
||||
"""Split a nanosecond timestamp into seconds and remaining nanoseconds."""
|
||||
seconds, nanos = divmod(nanoseconds, _NANOS_PER_SECOND)
|
||||
return seconds, nanos
|
||||
|
||||
|
||||
def uuid7(nanoseconds: int | None = None) -> UUID:
|
||||
"""Generate a UUID from a Unix timestamp in nanoseconds and random bits.
|
||||
|
||||
UUIDv7 objects feature monotonicity within a millisecond.
|
||||
|
||||
Args:
|
||||
nanoseconds: Optional ns timestamp. If not provided, uses current time.
|
||||
"""
|
||||
# --- 48 --- -- 4 -- --- 12 --- -- 2 -- --- 30 --- - 32 -
|
||||
# unix_ts_ms | version | counter_hi | variant | counter_lo | random
|
||||
#
|
||||
# 'counter = counter_hi | counter_lo' is a 42-bit counter constructed
|
||||
# with Method 1 of RFC 9562, §6.2, and its MSB is set to 0.
|
||||
#
|
||||
# 'random' is a 32-bit random value regenerated for every new UUID.
|
||||
#
|
||||
# If multiple UUIDs are generated within the same millisecond, the LSB
|
||||
# of 'counter' is incremented by 1. When overflowing, the timestamp is
|
||||
# advanced and the counter is reset to a random 42-bit integer with MSB
|
||||
# set to 0.
|
||||
|
||||
# For now, just delegate to the uuid_utils implementation
|
||||
if nanoseconds is None:
|
||||
return _uuid_utils_uuid7()
|
||||
seconds, nanos = _to_timestamp_and_nanos(nanoseconds)
|
||||
return _uuid_utils_uuid7(timestamp=seconds, nanos=nanos)
|
||||
|
||||
|
||||
__all__ = ["uuid7"]
|
||||
@@ -604,7 +604,7 @@ class InMemoryVectorStore(VectorStore):
|
||||
"""
|
||||
path_: Path = Path(path)
|
||||
with path_.open("r", encoding="utf-8") as f:
|
||||
store = load(json.load(f), allowed_objects=[Document])
|
||||
store = load(json.load(f))
|
||||
vectorstore = cls(embedding=embedding, **kwargs)
|
||||
vectorstore.store = store
|
||||
return vectorstore
|
||||
@@ -617,5 +617,5 @@ class InMemoryVectorStore(VectorStore):
|
||||
"""
|
||||
path_: Path = Path(path)
|
||||
path_.parent.mkdir(exist_ok=True, parents=True)
|
||||
with path_.open("w") as f:
|
||||
with path_.open("w", encoding="utf-8") as f:
|
||||
json.dump(dumpd(self.store), f, indent=2)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""langchain-core version information and utilities."""
|
||||
|
||||
VERSION = "0.3.83"
|
||||
VERSION = "1.0.0a6"
|
||||
|
||||
@@ -5,7 +5,7 @@ build-backend = "pdm.backend"
|
||||
[project]
|
||||
authors = []
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.9.0,<4.0.0"
|
||||
requires-python = ">=3.10.0,<4.0.0"
|
||||
dependencies = [
|
||||
"langsmith>=0.3.45,<1.0.0",
|
||||
"tenacity!=8.4.0,>=8.1.0,<10.0.0",
|
||||
@@ -14,10 +14,9 @@ dependencies = [
|
||||
"typing-extensions>=4.7.0,<5.0.0",
|
||||
"packaging>=23.2.0,<26.0.0",
|
||||
"pydantic>=2.7.4,<3.0.0",
|
||||
"uuid-utils>=0.12.0,<1.0",
|
||||
]
|
||||
name = "langchain-core"
|
||||
version = "0.3.83"
|
||||
version = "1.0.0a6"
|
||||
description = "Building applications with LLMs through composability"
|
||||
readme = "README.md"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import time
|
||||
from itertools import cycle
|
||||
from typing import Any, Optional, Union
|
||||
from typing import Any, Optional, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from typing_extensions import override
|
||||
@@ -59,7 +59,7 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
assert chunks == [
|
||||
_any_id_ai_message_chunk(content="hello"),
|
||||
_any_id_ai_message_chunk(content=" "),
|
||||
_any_id_ai_message_chunk(content="goodbye"),
|
||||
_any_id_ai_message_chunk(content="goodbye", chunk_position="last"),
|
||||
]
|
||||
assert len({chunk.id for chunk in chunks}) == 1
|
||||
|
||||
@@ -67,7 +67,7 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
assert chunks == [
|
||||
_any_id_ai_message_chunk(content="hello"),
|
||||
_any_id_ai_message_chunk(content=" "),
|
||||
_any_id_ai_message_chunk(content="goodbye"),
|
||||
_any_id_ai_message_chunk(content="goodbye", chunk_position="last"),
|
||||
]
|
||||
assert len({chunk.id for chunk in chunks}) == 1
|
||||
|
||||
@@ -79,6 +79,7 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
assert chunks == [
|
||||
_any_id_ai_message_chunk(content="", additional_kwargs={"foo": 42}),
|
||||
_any_id_ai_message_chunk(content="", additional_kwargs={"bar": 24}),
|
||||
_any_id_ai_message_chunk(content="", chunk_position="last"),
|
||||
]
|
||||
assert len({chunk.id for chunk in chunks}) == 1
|
||||
|
||||
@@ -97,7 +98,8 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
|
||||
assert chunks == [
|
||||
_any_id_ai_message_chunk(
|
||||
content="", additional_kwargs={"function_call": {"name": "move_file"}}
|
||||
content="",
|
||||
additional_kwargs={"function_call": {"name": "move_file"}},
|
||||
),
|
||||
_any_id_ai_message_chunk(
|
||||
content="",
|
||||
@@ -114,6 +116,7 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
"function_call": {"arguments": '\n "destination_path": "bar"\n}'},
|
||||
},
|
||||
),
|
||||
_any_id_ai_message_chunk(content="", chunk_position="last"),
|
||||
]
|
||||
assert len({chunk.id for chunk in chunks}) == 1
|
||||
|
||||
@@ -134,6 +137,7 @@ async def test_generic_fake_chat_model_stream() -> None:
|
||||
}
|
||||
},
|
||||
id=chunks[0].id,
|
||||
chunk_position="last",
|
||||
)
|
||||
|
||||
|
||||
@@ -148,7 +152,7 @@ async def test_generic_fake_chat_model_astream_log() -> None:
|
||||
assert final.state["streamed_output"] == [
|
||||
_any_id_ai_message_chunk(content="hello"),
|
||||
_any_id_ai_message_chunk(content=" "),
|
||||
_any_id_ai_message_chunk(content="goodbye"),
|
||||
_any_id_ai_message_chunk(content="goodbye", chunk_position="last"),
|
||||
]
|
||||
assert len({chunk.id for chunk in final.state["streamed_output"]}) == 1
|
||||
|
||||
@@ -205,7 +209,7 @@ async def test_callback_handlers() -> None:
|
||||
assert results == [
|
||||
_any_id_ai_message_chunk(content="hello"),
|
||||
_any_id_ai_message_chunk(content=" "),
|
||||
_any_id_ai_message_chunk(content="goodbye"),
|
||||
_any_id_ai_message_chunk(content="goodbye", chunk_position="last"),
|
||||
]
|
||||
assert tokens == ["hello", " ", "goodbye"]
|
||||
assert len({chunk.id for chunk in results}) == 1
|
||||
@@ -214,7 +218,9 @@ async def test_callback_handlers() -> None:
|
||||
def test_chat_model_inputs() -> None:
|
||||
fake = ParrotFakeChatModel()
|
||||
|
||||
assert fake.invoke("hello") == _any_id_human_message(content="hello")
|
||||
assert cast("HumanMessage", fake.invoke("hello")) == _any_id_human_message(
|
||||
content="hello"
|
||||
)
|
||||
assert fake.invoke([("ai", "blah")]) == _any_id_ai_message(content="blah")
|
||||
assert fake.invoke([AIMessage(content="blah")]) == _any_id_ai_message(
|
||||
content="blah"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test base chat model."""
|
||||
|
||||
import uuid
|
||||
import warnings
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||
|
||||
@@ -14,11 +15,15 @@ from langchain_core.language_models import (
|
||||
ParrotFakeChatModel,
|
||||
)
|
||||
from langchain_core.language_models._utils import _normalize_messages
|
||||
from langchain_core.language_models.fake_chat_models import FakeListChatModelError
|
||||
from langchain_core.language_models.fake_chat_models import (
|
||||
FakeListChatModelError,
|
||||
GenericFakeChatModel,
|
||||
)
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AIMessageChunk,
|
||||
BaseMessage,
|
||||
BaseMessageChunk,
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
)
|
||||
@@ -40,6 +45,37 @@ if TYPE_CHECKING:
|
||||
from langchain_core.outputs.llm_result import LLMResult
|
||||
|
||||
|
||||
def _content_blocks_equal_ignore_id(
|
||||
actual: Union[str, list[Any]], expected: Union[str, list[Any]]
|
||||
) -> bool:
|
||||
"""Compare content blocks, ignoring auto-generated `id` fields.
|
||||
|
||||
Args:
|
||||
actual: Actual content from response (string or list of content blocks).
|
||||
expected: Expected content to compare against (string or list of blocks).
|
||||
|
||||
Returns:
|
||||
True if content matches (excluding `id` fields), False otherwise.
|
||||
|
||||
"""
|
||||
if isinstance(actual, str) or isinstance(expected, str):
|
||||
return actual == expected
|
||||
|
||||
if len(actual) != len(expected):
|
||||
return False
|
||||
for actual_block, expected_block in zip(actual, expected):
|
||||
actual_without_id = (
|
||||
{k: v for k, v in actual_block.items() if k != "id"}
|
||||
if isinstance(actual_block, dict) and "id" in actual_block
|
||||
else actual_block
|
||||
)
|
||||
|
||||
if actual_without_id != expected_block:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def messages() -> list:
|
||||
return [
|
||||
@@ -141,7 +177,7 @@ async def test_stream_error_callback() -> None:
|
||||
|
||||
|
||||
async def test_astream_fallback_to_ainvoke() -> None:
|
||||
"""Test astream uses appropriate implementation."""
|
||||
"""Test `astream()` uses appropriate implementation."""
|
||||
|
||||
class ModelWithGenerate(BaseChatModel):
|
||||
@override
|
||||
@@ -168,10 +204,10 @@ async def test_astream_fallback_to_ainvoke() -> None:
|
||||
# is not strictly correct.
|
||||
# LangChain documents a pattern of adding BaseMessageChunks to accumulate a stream.
|
||||
# This may be better done with `reduce(operator.add, chunks)`.
|
||||
assert chunks == [_any_id_ai_message(content="hello")] # type: ignore[comparison-overlap]
|
||||
assert chunks == [_any_id_ai_message(content="hello")]
|
||||
|
||||
chunks = [chunk async for chunk in model.astream("anything")]
|
||||
assert chunks == [_any_id_ai_message(content="hello")] # type: ignore[comparison-overlap]
|
||||
assert chunks == [_any_id_ai_message(content="hello")]
|
||||
|
||||
|
||||
async def test_astream_implementation_fallback_to_stream() -> None:
|
||||
@@ -198,7 +234,9 @@ async def test_astream_implementation_fallback_to_stream() -> None:
|
||||
) -> Iterator[ChatGenerationChunk]:
|
||||
"""Stream the output of the model."""
|
||||
yield ChatGenerationChunk(message=AIMessageChunk(content="a"))
|
||||
yield ChatGenerationChunk(message=AIMessageChunk(content="b"))
|
||||
yield ChatGenerationChunk(
|
||||
message=AIMessageChunk(content="b", chunk_position="last")
|
||||
)
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
@@ -207,15 +245,19 @@ async def test_astream_implementation_fallback_to_stream() -> None:
|
||||
model = ModelWithSyncStream()
|
||||
chunks = list(model.stream("anything"))
|
||||
assert chunks == [
|
||||
_any_id_ai_message_chunk(content="a"),
|
||||
_any_id_ai_message_chunk(content="b"),
|
||||
_any_id_ai_message_chunk(
|
||||
content="a",
|
||||
),
|
||||
_any_id_ai_message_chunk(content="b", chunk_position="last"),
|
||||
]
|
||||
assert len({chunk.id for chunk in chunks}) == 1
|
||||
assert type(model)._astream == BaseChatModel._astream
|
||||
astream_chunks = [chunk async for chunk in model.astream("anything")]
|
||||
assert astream_chunks == [
|
||||
_any_id_ai_message_chunk(content="a"),
|
||||
_any_id_ai_message_chunk(content="b"),
|
||||
_any_id_ai_message_chunk(
|
||||
content="a",
|
||||
),
|
||||
_any_id_ai_message_chunk(content="b", chunk_position="last"),
|
||||
]
|
||||
assert len({chunk.id for chunk in astream_chunks}) == 1
|
||||
|
||||
@@ -244,7 +286,9 @@ async def test_astream_implementation_uses_astream() -> None:
|
||||
) -> AsyncIterator[ChatGenerationChunk]:
|
||||
"""Stream the output of the model."""
|
||||
yield ChatGenerationChunk(message=AIMessageChunk(content="a"))
|
||||
yield ChatGenerationChunk(message=AIMessageChunk(content="b"))
|
||||
yield ChatGenerationChunk(
|
||||
message=AIMessageChunk(content="b", chunk_position="last")
|
||||
)
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
@@ -253,8 +297,10 @@ async def test_astream_implementation_uses_astream() -> None:
|
||||
model = ModelWithAsyncStream()
|
||||
chunks = [chunk async for chunk in model.astream("anything")]
|
||||
assert chunks == [
|
||||
_any_id_ai_message_chunk(content="a"),
|
||||
_any_id_ai_message_chunk(content="b"),
|
||||
_any_id_ai_message_chunk(
|
||||
content="a",
|
||||
),
|
||||
_any_id_ai_message_chunk(content="b", chunk_position="last"),
|
||||
]
|
||||
assert len({chunk.id for chunk in chunks}) == 1
|
||||
|
||||
@@ -427,11 +473,12 @@ class FakeChatModelStartTracer(FakeTracer):
|
||||
|
||||
|
||||
def test_trace_images_in_openai_format() -> None:
|
||||
"""Test that images are traced in OpenAI format."""
|
||||
"""Test that images are traced in OpenAI Chat Completions format."""
|
||||
llm = ParrotFakeChatModel()
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
# v0 format
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
@@ -442,7 +489,7 @@ def test_trace_images_in_openai_format() -> None:
|
||||
}
|
||||
]
|
||||
tracer = FakeChatModelStartTracer()
|
||||
response = llm.invoke(messages, config={"callbacks": [tracer]})
|
||||
llm.invoke(messages, config={"callbacks": [tracer]})
|
||||
assert tracer.messages == [
|
||||
[
|
||||
[
|
||||
@@ -457,19 +504,90 @@ def test_trace_images_in_openai_format() -> None:
|
||||
]
|
||||
]
|
||||
]
|
||||
# Test no mutation
|
||||
assert response.content == [
|
||||
|
||||
|
||||
def test_trace_pdfs() -> None:
|
||||
# For backward compat
|
||||
llm = ParrotFakeChatModel()
|
||||
messages = [
|
||||
{
|
||||
"type": "image",
|
||||
"source_type": "url",
|
||||
"url": "https://example.com/image.png",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "file",
|
||||
"mime_type": "application/pdf",
|
||||
"base64": "<base64 string>",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
tracer = FakeChatModelStartTracer()
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
llm.invoke(messages, config={"callbacks": [tracer]})
|
||||
|
||||
assert tracer.messages == [
|
||||
[
|
||||
[
|
||||
HumanMessage(
|
||||
content=[
|
||||
{
|
||||
"type": "file",
|
||||
"mime_type": "application/pdf",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 string>",
|
||||
}
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def test_trace_content_blocks_with_no_type_key() -> None:
|
||||
"""Test that we add a ``type`` key to certain content blocks that don't have one."""
|
||||
llm = ParrotFakeChatModel()
|
||||
def test_content_block_transformation_v0_to_v1_image() -> None:
|
||||
"""Test that v0 format image content blocks are transformed to v1 format."""
|
||||
# Create a message with v0 format image content
|
||||
image_message = AIMessage(
|
||||
content=[
|
||||
{
|
||||
"type": "image",
|
||||
"source_type": "url",
|
||||
"url": "https://example.com/image.png",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
llm = GenericFakeChatModel(messages=iter([image_message]), output_version="v1")
|
||||
response = llm.invoke("test")
|
||||
|
||||
# With v1 output_version, .content should be transformed
|
||||
# Check structure, ignoring auto-generated IDs
|
||||
assert len(response.content) == 1
|
||||
content_block = response.content[0]
|
||||
if isinstance(content_block, dict) and "id" in content_block:
|
||||
# Remove auto-generated id for comparison
|
||||
content_without_id = {k: v for k, v in content_block.items() if k != "id"}
|
||||
expected_content = {
|
||||
"type": "image",
|
||||
"url": "https://example.com/image.png",
|
||||
}
|
||||
assert content_without_id == expected_content
|
||||
else:
|
||||
assert content_block == {
|
||||
"type": "image",
|
||||
"url": "https://example.com/image.png",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("output_version", ["v0", "v1"])
|
||||
def test_trace_content_blocks_with_no_type_key(output_version: str) -> None:
|
||||
"""Test behavior of content blocks that don't have a `type` key.
|
||||
|
||||
Only for blocks with one key, in which case, the name of the key is used as `type`.
|
||||
|
||||
"""
|
||||
llm = ParrotFakeChatModel(output_version=output_version)
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
@@ -504,155 +622,381 @@ def test_trace_content_blocks_with_no_type_key() -> None:
|
||||
]
|
||||
]
|
||||
]
|
||||
# Test no mutation
|
||||
assert response.content == [
|
||||
|
||||
if output_version == "v0":
|
||||
assert response.content == [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
},
|
||||
{
|
||||
"cachePoint": {"type": "default"},
|
||||
},
|
||||
]
|
||||
else:
|
||||
assert response.content == [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {
|
||||
"cachePoint": {"type": "default"},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
assert response.content_blocks == [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
},
|
||||
{
|
||||
"cachePoint": {"type": "default"},
|
||||
"type": "non_standard",
|
||||
"value": {
|
||||
"cachePoint": {"type": "default"},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_extend_support_to_openai_multimodal_formats() -> None:
|
||||
"""Test that chat models normalize OpenAI file and audio inputs."""
|
||||
llm = ParrotFakeChatModel()
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Hello"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
"""Test normalizing OpenAI audio, image, and file inputs to v1."""
|
||||
# Audio and file only (chat model default)
|
||||
messages = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"},
|
||||
{ # audio-base64
|
||||
"type": "input_audio",
|
||||
"input_audio": {
|
||||
"format": "wav",
|
||||
"data": "<base64 string>",
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
|
||||
},
|
||||
{ # file-base64
|
||||
"type": "file",
|
||||
"file": {
|
||||
"filename": "draconomicon.pdf",
|
||||
"file_data": "data:application/pdf;base64,<base64 string>",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {
|
||||
"filename": "draconomicon.pdf",
|
||||
"file_data": "data:application/pdf;base64,<base64 string>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {
|
||||
"file_data": "data:application/pdf;base64,<base64 string>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {"file_id": "<file id>"},
|
||||
},
|
||||
{
|
||||
"type": "input_audio",
|
||||
"input_audio": {"data": "<base64 data>", "format": "wav"},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
expected_content = [
|
||||
{"type": "text", "text": "Hello"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 string>",
|
||||
"mime_type": "application/pdf",
|
||||
"filename": "draconomicon.pdf",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 string>",
|
||||
"mime_type": "application/pdf",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {"file_id": "<file id>"},
|
||||
},
|
||||
{
|
||||
"type": "audio",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 data>",
|
||||
"mime_type": "audio/wav",
|
||||
},
|
||||
]
|
||||
response = llm.invoke(messages)
|
||||
assert response.content == expected_content
|
||||
},
|
||||
{ # file-id
|
||||
"type": "file",
|
||||
"file": {"file_id": "<file id>"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Test no mutation
|
||||
assert messages[0]["content"] == [
|
||||
{"type": "text", "text": "Hello"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {
|
||||
"filename": "draconomicon.pdf",
|
||||
"file_data": "data:application/pdf;base64,<base64 string>",
|
||||
expected_content_messages = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"}, # TextContentBlock
|
||||
{ # AudioContentBlock
|
||||
"type": "audio",
|
||||
"base64": "<base64 string>",
|
||||
"mime_type": "audio/wav",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {
|
||||
"file_data": "data:application/pdf;base64,<base64 string>",
|
||||
{ # FileContentBlock
|
||||
"type": "file",
|
||||
"base64": "<base64 string>",
|
||||
"mime_type": "application/pdf",
|
||||
"extras": {"filename": "draconomicon.pdf"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {"file_id": "<file id>"},
|
||||
},
|
||||
{
|
||||
"type": "input_audio",
|
||||
"input_audio": {"data": "<base64 data>", "format": "wav"},
|
||||
},
|
||||
]
|
||||
{ # ...
|
||||
"type": "file",
|
||||
"file_id": "<file id>",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
normalized_content = _normalize_messages([messages])
|
||||
|
||||
# Check structure, ignoring auto-generated IDs
|
||||
assert len(normalized_content) == 1
|
||||
normalized_message = normalized_content[0]
|
||||
assert len(normalized_message.content) == len(expected_content_messages.content)
|
||||
|
||||
assert _content_blocks_equal_ignore_id(
|
||||
normalized_message.content, expected_content_messages.content
|
||||
)
|
||||
|
||||
messages = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"},
|
||||
{ # image-url
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
},
|
||||
{ # image-base64
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
|
||||
},
|
||||
{ # audio-base64
|
||||
"type": "input_audio",
|
||||
"input_audio": {
|
||||
"format": "wav",
|
||||
"data": "<base64 string>",
|
||||
},
|
||||
},
|
||||
{ # file-base64
|
||||
"type": "file",
|
||||
"file": {
|
||||
"filename": "draconomicon.pdf",
|
||||
"file_data": "data:application/pdf;base64,<base64 string>",
|
||||
},
|
||||
},
|
||||
{ # file-id
|
||||
"type": "file",
|
||||
"file": {"file_id": "<file id>"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
expected_content_messages = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"}, # TextContentBlock
|
||||
{ # image-url passes through
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.png"},
|
||||
},
|
||||
{ # image-url passes through with inline data
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
|
||||
},
|
||||
{ # AudioContentBlock
|
||||
"type": "audio",
|
||||
"base64": "<base64 string>",
|
||||
"mime_type": "audio/wav",
|
||||
},
|
||||
{ # FileContentBlock
|
||||
"type": "file",
|
||||
"base64": "<base64 string>",
|
||||
"mime_type": "application/pdf",
|
||||
"extras": {"filename": "draconomicon.pdf"},
|
||||
},
|
||||
{ # ...
|
||||
"type": "file",
|
||||
"file_id": "<file id>",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
normalized_content = _normalize_messages([messages])
|
||||
|
||||
# Check structure, ignoring auto-generated IDs
|
||||
assert len(normalized_content) == 1
|
||||
normalized_message = normalized_content[0]
|
||||
assert len(normalized_message.content) == len(expected_content_messages.content)
|
||||
|
||||
assert _content_blocks_equal_ignore_id(
|
||||
normalized_message.content, expected_content_messages.content
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_messages_edge_cases() -> None:
|
||||
# Test some blocks that should pass through
|
||||
# Test behavior of malformed/unrecognized content blocks
|
||||
|
||||
messages = [
|
||||
HumanMessage(
|
||||
content=[
|
||||
{
|
||||
"type": "file",
|
||||
"file": "uri",
|
||||
"type": "input_image", # Responses API type; not handled
|
||||
"image_url": "uri",
|
||||
},
|
||||
{
|
||||
"type": "input_file",
|
||||
# Standard OpenAI Chat Completions type but malformed structure
|
||||
"type": "input_audio",
|
||||
"input_audio": "uri", # Should be nested in `audio`
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": "uri", # `file` should be a dict for Chat Completions
|
||||
},
|
||||
{
|
||||
"type": "input_file", # Responses API type; not handled
|
||||
"file_data": "uri",
|
||||
"filename": "file-name",
|
||||
},
|
||||
{
|
||||
"type": "input_audio",
|
||||
"input_audio": "uri",
|
||||
},
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": "uri",
|
||||
},
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
assert messages == _normalize_messages(messages)
|
||||
|
||||
|
||||
def test_normalize_messages_v1_content_blocks_unchanged() -> None:
|
||||
"""Test passing v1 content blocks to `_normalize_messages()` leaves unchanged."""
|
||||
input_messages = [
|
||||
HumanMessage(
|
||||
content=[
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Hello world",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"url": "https://example.com/image.png",
|
||||
"mime_type": "image/png",
|
||||
},
|
||||
{
|
||||
"type": "audio",
|
||||
"base64": "base64encodedaudiodata",
|
||||
"mime_type": "audio/wav",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"id": "file_123",
|
||||
},
|
||||
{
|
||||
"type": "reasoning",
|
||||
"reasoning": "Let me think about this...",
|
||||
},
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
result = _normalize_messages(input_messages)
|
||||
|
||||
# Verify the result is identical to the input (message should not be copied)
|
||||
assert len(result) == 1
|
||||
assert result[0] is input_messages[0]
|
||||
assert result[0].content == input_messages[0].content
|
||||
|
||||
|
||||
def test_output_version_invoke(monkeypatch: Any) -> None:
|
||||
messages = [AIMessage("hello")]
|
||||
|
||||
llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")
|
||||
response = llm.invoke("hello")
|
||||
assert response.content == [{"type": "text", "text": "hello"}]
|
||||
assert response.response_metadata["output_version"] == "v1"
|
||||
|
||||
llm = GenericFakeChatModel(messages=iter(messages))
|
||||
response = llm.invoke("hello")
|
||||
assert response.content == "hello"
|
||||
|
||||
monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")
|
||||
llm = GenericFakeChatModel(messages=iter(messages))
|
||||
response = llm.invoke("hello")
|
||||
assert response.content == [{"type": "text", "text": "hello"}]
|
||||
assert response.response_metadata["output_version"] == "v1"
|
||||
|
||||
|
||||
# -- v1 output version tests --
|
||||
|
||||
|
||||
async def test_output_version_ainvoke(monkeypatch: Any) -> None:
|
||||
messages = [AIMessage("hello")]
|
||||
|
||||
# v0
|
||||
llm = GenericFakeChatModel(messages=iter(messages))
|
||||
response = await llm.ainvoke("hello")
|
||||
assert response.content == "hello"
|
||||
|
||||
# v1
|
||||
llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")
|
||||
response = await llm.ainvoke("hello")
|
||||
assert response.content == [{"type": "text", "text": "hello"}]
|
||||
assert response.response_metadata["output_version"] == "v1"
|
||||
|
||||
# v1 from env var
|
||||
monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")
|
||||
llm = GenericFakeChatModel(messages=iter(messages))
|
||||
response = await llm.ainvoke("hello")
|
||||
assert response.content == [{"type": "text", "text": "hello"}]
|
||||
assert response.response_metadata["output_version"] == "v1"
|
||||
|
||||
|
||||
def test_output_version_stream(monkeypatch: Any) -> None:
|
||||
messages = [AIMessage("foo bar")]
|
||||
|
||||
# v0
|
||||
llm = GenericFakeChatModel(messages=iter(messages))
|
||||
full = None
|
||||
for chunk in llm.stream("hello"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
assert isinstance(chunk.content, str)
|
||||
assert chunk.content
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
assert full.content == "foo bar"
|
||||
|
||||
# v1
|
||||
llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")
|
||||
full_v1: Optional[BaseMessageChunk] = None
|
||||
for chunk in llm.stream("hello"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
assert isinstance(chunk.content, list)
|
||||
assert len(chunk.content) == 1
|
||||
block = chunk.content[0]
|
||||
assert isinstance(block, dict)
|
||||
assert block["type"] == "text"
|
||||
assert block["text"]
|
||||
full_v1 = chunk if full_v1 is None else full_v1 + chunk
|
||||
assert isinstance(full_v1, AIMessageChunk)
|
||||
assert full_v1.response_metadata["output_version"] == "v1"
|
||||
|
||||
# v1 from env var
|
||||
monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")
|
||||
llm = GenericFakeChatModel(messages=iter(messages))
|
||||
full_env = None
|
||||
for chunk in llm.stream("hello"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
assert isinstance(chunk.content, list)
|
||||
assert len(chunk.content) == 1
|
||||
block = chunk.content[0]
|
||||
assert isinstance(block, dict)
|
||||
assert block["type"] == "text"
|
||||
assert block["text"]
|
||||
full_env = chunk if full_env is None else full_env + chunk
|
||||
assert isinstance(full_env, AIMessageChunk)
|
||||
assert full_env.response_metadata["output_version"] == "v1"
|
||||
|
||||
|
||||
async def test_output_version_astream(monkeypatch: Any) -> None:
|
||||
messages = [AIMessage("foo bar")]
|
||||
|
||||
# v0
|
||||
llm = GenericFakeChatModel(messages=iter(messages))
|
||||
full = None
|
||||
async for chunk in llm.astream("hello"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
assert isinstance(chunk.content, str)
|
||||
assert chunk.content
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
assert full.content == "foo bar"
|
||||
|
||||
# v1
|
||||
llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")
|
||||
full_v1: Optional[BaseMessageChunk] = None
|
||||
async for chunk in llm.astream("hello"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
assert isinstance(chunk.content, list)
|
||||
assert len(chunk.content) == 1
|
||||
block = chunk.content[0]
|
||||
assert isinstance(block, dict)
|
||||
assert block["type"] == "text"
|
||||
assert block["text"]
|
||||
full_v1 = chunk if full_v1 is None else full_v1 + chunk
|
||||
assert isinstance(full_v1, AIMessageChunk)
|
||||
assert full_v1.response_metadata["output_version"] == "v1"
|
||||
|
||||
# v1 from env var
|
||||
monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")
|
||||
llm = GenericFakeChatModel(messages=iter(messages))
|
||||
full_env = None
|
||||
async for chunk in llm.astream("hello"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
assert isinstance(chunk.content, list)
|
||||
assert len(chunk.content) == 1
|
||||
block = chunk.content[0]
|
||||
assert isinstance(block, dict)
|
||||
assert block["type"] == "text"
|
||||
assert block["text"]
|
||||
full_env = chunk if full_env is None else full_env + chunk
|
||||
assert isinstance(full_env, AIMessageChunk)
|
||||
assert full_env.response_metadata["output_version"] == "v1"
|
||||
assert messages == _normalize_messages(messages)
|
||||
|
||||
|
||||
|
||||
@@ -215,8 +215,8 @@ def test_rate_limit_skips_cache() -> None:
|
||||
assert list(cache._cache) == [
|
||||
(
|
||||
'[{"lc": 1, "type": "constructor", "id": ["langchain", "schema", '
|
||||
'"messages", '
|
||||
'"HumanMessage"], "kwargs": {"content": "foo", "type": "human"}}]',
|
||||
'"messages", "HumanMessage"], "kwargs": {"content": "foo", '
|
||||
'"type": "human"}}]',
|
||||
"[('_type', 'generic-fake-chat-model'), ('stop', None)]",
|
||||
)
|
||||
]
|
||||
@@ -240,7 +240,8 @@ def test_serialization_with_rate_limiter() -> None:
|
||||
assert InMemoryRateLimiter.__name__ not in serialized_model
|
||||
|
||||
|
||||
async def test_rate_limit_skips_cache_async() -> None:
|
||||
@pytest.mark.parametrize("output_version", ["v0", "v1"])
|
||||
async def test_rate_limit_skips_cache_async(output_version: str) -> None:
|
||||
"""Test that rate limiting does not rate limit cache look ups."""
|
||||
cache = InMemoryCache()
|
||||
model = GenericFakeChatModel(
|
||||
@@ -249,6 +250,7 @@ async def test_rate_limit_skips_cache_async() -> None:
|
||||
requests_per_second=20, check_every_n_seconds=0.1, max_bucket_size=1
|
||||
),
|
||||
cache=cache,
|
||||
output_version=output_version,
|
||||
)
|
||||
|
||||
tic = time.time()
|
||||
|
||||
@@ -18,6 +18,7 @@ EXPECTED_ALL = [
|
||||
"FakeStreamingListLLM",
|
||||
"FakeListLLM",
|
||||
"ParrotFakeChatModel",
|
||||
"is_openai_data_block",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
from langchain_core.load import __all__
|
||||
|
||||
EXPECTED_ALL = [
|
||||
"InitValidator",
|
||||
"Serializable",
|
||||
"dumpd",
|
||||
"dumps",
|
||||
"load",
|
||||
"loads",
|
||||
]
|
||||
EXPECTED_ALL = ["dumpd", "dumps", "load", "loads", "Serializable"]
|
||||
|
||||
|
||||
def test_all_imports() -> None:
|
||||
|
||||
@@ -1,431 +0,0 @@
|
||||
"""Tests for secret injection prevention in serialization.
|
||||
|
||||
Verify that user-provided data containing secret-like structures cannot be used to
|
||||
extract environment variables during deserialization.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.load import dumpd, dumps, load
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
from langchain_core.outputs import ChatGeneration
|
||||
|
||||
SENTINEL_ENV_VAR = "TEST_SECRET_INJECTION_VAR"
|
||||
"""Sentinel value that should NEVER appear in serialized output."""
|
||||
|
||||
SENTINEL_VALUE = "LEAKED_SECRET_MEOW_12345"
|
||||
"""Sentinel value that should NEVER appear in serialized output."""
|
||||
|
||||
MALICIOUS_SECRET_DICT: dict[str, Any] = {
|
||||
"lc": 1,
|
||||
"type": "secret",
|
||||
"id": [SENTINEL_ENV_VAR],
|
||||
}
|
||||
"""The malicious secret-like dict that tries to read the env var"""
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _set_sentinel_env_var() -> Any:
|
||||
"""Set the sentinel env var for all tests in this module."""
|
||||
with mock.patch.dict(os.environ, {SENTINEL_ENV_VAR: SENTINEL_VALUE}):
|
||||
yield
|
||||
|
||||
|
||||
def _assert_no_secret_leak(payload: Any) -> None:
|
||||
"""Assert that serializing/deserializing payload doesn't leak the secret."""
|
||||
# First serialize
|
||||
serialized = dumps(payload)
|
||||
|
||||
# Deserialize with secrets_from_env=True (the dangerous setting)
|
||||
deserialized = load(serialized, secrets_from_env=True)
|
||||
|
||||
# Re-serialize to string
|
||||
reserialized = dumps(deserialized)
|
||||
|
||||
assert SENTINEL_VALUE not in reserialized, (
|
||||
f"Secret was leaked! Found '{SENTINEL_VALUE}' in output.\n"
|
||||
f"Original payload type: {type(payload)}\n"
|
||||
f"Reserialized output: {reserialized[:500]}..."
|
||||
)
|
||||
|
||||
assert SENTINEL_VALUE not in repr(deserialized), (
|
||||
f"Secret was leaked in deserialized object! Found '{SENTINEL_VALUE}'.\n"
|
||||
f"Deserialized: {deserialized!r}"
|
||||
)
|
||||
|
||||
|
||||
class TestSerializableTopLevel:
|
||||
"""Tests with `Serializable` objects at the top level."""
|
||||
|
||||
def test_human_message_with_secret_in_content(self) -> None:
|
||||
"""`HumanMessage` with secret-like dict in `content`."""
|
||||
msg = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"},
|
||||
{"type": "text", "text": MALICIOUS_SECRET_DICT},
|
||||
]
|
||||
)
|
||||
_assert_no_secret_leak(msg)
|
||||
|
||||
def test_human_message_with_secret_in_additional_kwargs(self) -> None:
|
||||
"""`HumanMessage` with secret-like dict in `additional_kwargs`."""
|
||||
msg = HumanMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
_assert_no_secret_leak(msg)
|
||||
|
||||
def test_human_message_with_secret_in_nested_additional_kwargs(self) -> None:
|
||||
"""`HumanMessage` with secret-like dict nested in `additional_kwargs`."""
|
||||
msg = HumanMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"nested": {"deep": MALICIOUS_SECRET_DICT}},
|
||||
)
|
||||
_assert_no_secret_leak(msg)
|
||||
|
||||
def test_human_message_with_secret_in_list_in_additional_kwargs(self) -> None:
|
||||
"""`HumanMessage` with secret-like dict in a list in `additional_kwargs`."""
|
||||
msg = HumanMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"items": [MALICIOUS_SECRET_DICT]},
|
||||
)
|
||||
_assert_no_secret_leak(msg)
|
||||
|
||||
def test_ai_message_with_secret_in_response_metadata(self) -> None:
|
||||
"""`AIMessage` with secret-like dict in respo`nse_metadata."""
|
||||
msg = AIMessage(
|
||||
content="Hello",
|
||||
response_metadata={"data": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
_assert_no_secret_leak(msg)
|
||||
|
||||
def test_document_with_secret_in_metadata(self) -> None:
|
||||
"""Document with secret-like dict in `metadata`."""
|
||||
doc = Document(
|
||||
page_content="Hello",
|
||||
metadata={"data": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
_assert_no_secret_leak(doc)
|
||||
|
||||
def test_nested_serializable_with_secret(self) -> None:
|
||||
"""`AIMessage` containing `dumpd(HumanMessage)` with secret in kwargs."""
|
||||
inner = HumanMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"secret": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
outer = AIMessage(
|
||||
content="Outer",
|
||||
additional_kwargs={"nested": [dumpd(inner)]},
|
||||
)
|
||||
_assert_no_secret_leak(outer)
|
||||
|
||||
|
||||
class TestDictTopLevel:
|
||||
"""Tests with plain dicts at the top level."""
|
||||
|
||||
def test_dict_with_serializable_containing_secret(self) -> None:
|
||||
"""Dict containing a `Serializable` with secret-like dict."""
|
||||
msg = HumanMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
payload = {"message": msg}
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
def test_dict_with_secret_no_serializable(self) -> None:
|
||||
"""Dict with secret-like dict, no `Serializable` objects."""
|
||||
payload = {"data": MALICIOUS_SECRET_DICT}
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
def test_dict_with_nested_secret_no_serializable(self) -> None:
|
||||
"""Dict with nested secret-like dict, no `Serializable` objects."""
|
||||
payload = {"outer": {"inner": MALICIOUS_SECRET_DICT}}
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
def test_dict_with_secret_in_list(self) -> None:
|
||||
"""Dict with secret-like dict in a list."""
|
||||
payload = {"items": [MALICIOUS_SECRET_DICT]}
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
def test_dict_mimicking_lc_constructor_with_secret(self) -> None:
|
||||
"""Dict that looks like an LC constructor containing a secret."""
|
||||
payload = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain_core", "messages", "ai", "AIMessage"],
|
||||
"kwargs": {
|
||||
"content": "Hello",
|
||||
"additional_kwargs": {"secret": MALICIOUS_SECRET_DICT},
|
||||
},
|
||||
}
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
|
||||
class TestPydanticModelTopLevel:
|
||||
"""Tests with Pydantic models (non-`Serializable`) at the top level."""
|
||||
|
||||
def test_pydantic_model_with_serializable_containing_secret(self) -> None:
|
||||
"""Pydantic model containing a `Serializable` with secret-like dict."""
|
||||
|
||||
class MyModel(BaseModel):
|
||||
message: Any
|
||||
|
||||
msg = HumanMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
payload = MyModel(message=msg)
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
def test_pydantic_model_with_secret_dict(self) -> None:
|
||||
"""Pydantic model containing a secret-like dict directly."""
|
||||
|
||||
class MyModel(BaseModel):
|
||||
data: dict[str, Any]
|
||||
|
||||
payload = MyModel(data=MALICIOUS_SECRET_DICT)
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
# Test treatment of "parsed" in additional_kwargs
|
||||
msg = AIMessage(content=[], additional_kwargs={"parsed": payload})
|
||||
gen = ChatGeneration(message=msg)
|
||||
_assert_no_secret_leak(gen)
|
||||
round_trip = load(dumpd(gen))
|
||||
assert MyModel(**(round_trip.message.additional_kwargs["parsed"])) == payload
|
||||
|
||||
def test_pydantic_model_with_nested_secret(self) -> None:
|
||||
"""Pydantic model with nested secret-like dict."""
|
||||
|
||||
class MyModel(BaseModel):
|
||||
nested: dict[str, Any]
|
||||
|
||||
payload = MyModel(nested={"inner": MALICIOUS_SECRET_DICT})
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
|
||||
class TestNonSerializableClassTopLevel:
|
||||
"""Tests with classes at the top level."""
|
||||
|
||||
def test_custom_class_with_serializable_containing_secret(self) -> None:
|
||||
"""Custom class containing a `Serializable` with secret-like dict."""
|
||||
|
||||
class MyClass:
|
||||
def __init__(self, message: Any) -> None:
|
||||
self.message = message
|
||||
|
||||
msg = HumanMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
payload = MyClass(message=msg)
|
||||
# This will serialize as not_implemented, but let's verify no leak
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
def test_custom_class_with_secret_dict(self) -> None:
|
||||
"""Custom class containing a secret-like dict directly."""
|
||||
|
||||
class MyClass:
|
||||
def __init__(self, data: dict[str, Any]) -> None:
|
||||
self.data = data
|
||||
|
||||
payload = MyClass(data=MALICIOUS_SECRET_DICT)
|
||||
_assert_no_secret_leak(payload)
|
||||
|
||||
|
||||
class TestDumpdInKwargs:
|
||||
"""Tests for the specific pattern of `dumpd()` result stored in kwargs."""
|
||||
|
||||
def test_dumpd_human_message_in_ai_message_kwargs(self) -> None:
|
||||
"""`AIMessage` with `dumpd(HumanMessage)` in `additional_kwargs`."""
|
||||
h = HumanMessage("Hello")
|
||||
a = AIMessage("foo", additional_kwargs={"bar": [dumpd(h)]})
|
||||
_assert_no_secret_leak(a)
|
||||
|
||||
def test_dumpd_human_message_with_secret_in_ai_message_kwargs(self) -> None:
|
||||
"""`AIMessage` with `dumpd(HumanMessage w/ secret)` in `additional_kwargs`."""
|
||||
h = HumanMessage(
|
||||
"Hello",
|
||||
additional_kwargs={"secret": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
a = AIMessage("foo", additional_kwargs={"bar": [dumpd(h)]})
|
||||
_assert_no_secret_leak(a)
|
||||
|
||||
def test_double_dumpd_nesting(self) -> None:
|
||||
"""Double nesting: `dumpd(AIMessage(dumpd(HumanMessage)))`."""
|
||||
h = HumanMessage(
|
||||
"Hello",
|
||||
additional_kwargs={"secret": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
a = AIMessage("foo", additional_kwargs={"bar": [dumpd(h)]})
|
||||
outer = AIMessage("outer", additional_kwargs={"nested": [dumpd(a)]})
|
||||
_assert_no_secret_leak(outer)
|
||||
|
||||
|
||||
class TestRoundTrip:
|
||||
"""Tests that verify round-trip serialization preserves data structure."""
|
||||
|
||||
def test_human_message_with_secret_round_trip(self) -> None:
|
||||
"""Verify secret-like dict is preserved as dict after round-trip."""
|
||||
msg = HumanMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
|
||||
serialized = dumpd(msg)
|
||||
deserialized = load(serialized, secrets_from_env=True)
|
||||
|
||||
# The secret-like dict should be preserved as a plain dict
|
||||
assert deserialized.additional_kwargs["data"] == MALICIOUS_SECRET_DICT
|
||||
assert isinstance(deserialized.additional_kwargs["data"], dict)
|
||||
|
||||
def test_document_with_secret_round_trip(self) -> None:
|
||||
"""Verify secret-like dict in `Document` metadata is preserved."""
|
||||
doc = Document(
|
||||
page_content="Hello",
|
||||
metadata={"data": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
|
||||
serialized = dumpd(doc)
|
||||
deserialized = load(
|
||||
serialized, secrets_from_env=True, allowed_objects=[Document]
|
||||
)
|
||||
|
||||
# The secret-like dict should be preserved as a plain dict
|
||||
assert deserialized.metadata["data"] == MALICIOUS_SECRET_DICT
|
||||
assert isinstance(deserialized.metadata["data"], dict)
|
||||
|
||||
def test_plain_dict_with_secret_round_trip(self) -> None:
|
||||
"""Verify secret-like dict in plain dict is preserved."""
|
||||
payload = {"data": MALICIOUS_SECRET_DICT}
|
||||
|
||||
serialized = dumpd(payload)
|
||||
deserialized = load(serialized, secrets_from_env=True)
|
||||
|
||||
# The secret-like dict should be preserved as a plain dict
|
||||
assert deserialized["data"] == MALICIOUS_SECRET_DICT
|
||||
assert isinstance(deserialized["data"], dict)
|
||||
|
||||
|
||||
class TestEscapingEfficiency:
|
||||
"""Tests that escaping doesn't cause excessive nesting."""
|
||||
|
||||
def test_no_triple_escaping(self) -> None:
|
||||
"""Verify dumpd doesn't cause triple/multiple escaping."""
|
||||
h = HumanMessage(
|
||||
"Hello",
|
||||
additional_kwargs={"bar": [MALICIOUS_SECRET_DICT]},
|
||||
)
|
||||
a = AIMessage("foo", additional_kwargs={"bar": [dumpd(h)]})
|
||||
d = dumpd(a)
|
||||
|
||||
serialized = json.dumps(d)
|
||||
# Count nested escape markers -
|
||||
# should be max 2 (one for HumanMessage, one for secret)
|
||||
# Not 3+ which would indicate re-escaping of already-escaped content
|
||||
escape_count = len(re.findall(r"__lc_escaped__", serialized))
|
||||
|
||||
# The HumanMessage dict gets escaped (1), the secret inside gets escaped (1)
|
||||
# Total should be 2, not 4 (which would mean triple nesting)
|
||||
assert escape_count <= 2, (
|
||||
f"Found {escape_count} escape markers, expected <= 2. "
|
||||
f"This indicates unnecessary re-escaping.\n{serialized}"
|
||||
)
|
||||
|
||||
def test_double_nesting_no_quadruple_escape(self) -> None:
|
||||
"""Verify double dumpd nesting doesn't explode escape markers."""
|
||||
h = HumanMessage(
|
||||
"Hello",
|
||||
additional_kwargs={"secret": MALICIOUS_SECRET_DICT},
|
||||
)
|
||||
a = AIMessage("middle", additional_kwargs={"nested": [dumpd(h)]})
|
||||
outer = AIMessage("outer", additional_kwargs={"deep": [dumpd(a)]})
|
||||
d = dumpd(outer)
|
||||
|
||||
serialized = json.dumps(d)
|
||||
escape_count = len(re.findall(r"__lc_escaped__", serialized))
|
||||
|
||||
# Should be:
|
||||
# outer escapes middle (1),
|
||||
# middle escapes h (1),
|
||||
# h escapes secret (1) = 3
|
||||
# Not 6+ which would indicate re-escaping
|
||||
assert escape_count <= 3, (
|
||||
f"Found {escape_count} escape markers, expected <= 3. "
|
||||
f"This indicates unnecessary re-escaping."
|
||||
)
|
||||
|
||||
|
||||
class TestConstructorInjection:
|
||||
"""Tests for constructor-type injection (not just secrets)."""
|
||||
|
||||
def test_constructor_in_metadata_not_instantiated(self) -> None:
|
||||
"""Verify constructor-like dict in metadata is not instantiated."""
|
||||
malicious_constructor = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain_core", "messages", "ai", "AIMessage"],
|
||||
"kwargs": {"content": "injected"},
|
||||
}
|
||||
|
||||
doc = Document(
|
||||
page_content="Hello",
|
||||
metadata={"data": malicious_constructor},
|
||||
)
|
||||
|
||||
serialized = dumpd(doc)
|
||||
deserialized = load(
|
||||
serialized,
|
||||
secrets_from_env=True,
|
||||
allowed_objects=[Document, AIMessage],
|
||||
)
|
||||
|
||||
# The constructor-like dict should be a plain dict, NOT an AIMessage
|
||||
assert isinstance(deserialized.metadata["data"], dict)
|
||||
assert deserialized.metadata["data"] == malicious_constructor
|
||||
|
||||
def test_constructor_in_content_not_instantiated(self) -> None:
|
||||
"""Verify constructor-like dict in message content is not instantiated."""
|
||||
malicious_constructor = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain_core", "messages", "human", "HumanMessage"],
|
||||
"kwargs": {"content": "injected"},
|
||||
}
|
||||
|
||||
msg = AIMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"nested": malicious_constructor},
|
||||
)
|
||||
|
||||
serialized = dumpd(msg)
|
||||
deserialized = load(
|
||||
serialized,
|
||||
secrets_from_env=True,
|
||||
allowed_objects=[AIMessage, HumanMessage],
|
||||
)
|
||||
|
||||
# The constructor-like dict should be a plain dict, NOT a HumanMessage
|
||||
assert isinstance(deserialized.additional_kwargs["nested"], dict)
|
||||
assert deserialized.additional_kwargs["nested"] == malicious_constructor
|
||||
|
||||
|
||||
def test_allowed_objects() -> None:
|
||||
# Core object
|
||||
msg = AIMessage(content="foo")
|
||||
serialized = dumpd(msg)
|
||||
assert load(serialized) == msg
|
||||
assert load(serialized, allowed_objects=[AIMessage]) == msg
|
||||
assert load(serialized, allowed_objects="core") == msg
|
||||
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
load(serialized, allowed_objects=[])
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
load(serialized, allowed_objects=[Document])
|
||||
@@ -1,19 +1,12 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
||||
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.load import InitValidator, Serializable, dumpd, dumps, load, loads
|
||||
from langchain_core.load import Serializable, dumpd, dumps, load
|
||||
from langchain_core.load.serializable import _is_field_useful
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.outputs import ChatGeneration, Generation
|
||||
from langchain_core.prompts import (
|
||||
ChatPromptTemplate,
|
||||
HumanMessagePromptTemplate,
|
||||
PromptTemplate,
|
||||
)
|
||||
|
||||
|
||||
class NonBoolObj:
|
||||
@@ -152,17 +145,10 @@ def test_simple_deserialization() -> None:
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
}
|
||||
new_foo = load(serialized_foo, allowed_objects=[Foo], valid_namespaces=["tests"])
|
||||
new_foo = load(serialized_foo, valid_namespaces=["tests"])
|
||||
assert new_foo == foo
|
||||
|
||||
|
||||
def test_disallowed_deserialization() -> None:
|
||||
foo = Foo(bar=1, baz="hello")
|
||||
serialized_foo = dumpd(foo)
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
load(serialized_foo, allowed_objects=[], valid_namespaces=["tests"])
|
||||
|
||||
|
||||
class Foo2(Serializable):
|
||||
bar: int
|
||||
baz: str
|
||||
@@ -184,7 +170,6 @@ def test_simple_deserialization_with_additional_imports() -> None:
|
||||
}
|
||||
new_foo = load(
|
||||
serialized_foo,
|
||||
allowed_objects=[Foo2],
|
||||
valid_namespaces=["tests"],
|
||||
additional_import_mappings={
|
||||
("tests", "unit_tests", "load", "test_serializable", "Foo"): (
|
||||
@@ -238,7 +223,7 @@ def test_serialization_with_pydantic() -> None:
|
||||
)
|
||||
)
|
||||
ser = dumpd(llm_response)
|
||||
deser = load(ser, allowed_objects=[ChatGeneration, AIMessage])
|
||||
deser = load(ser)
|
||||
assert isinstance(deser, ChatGeneration)
|
||||
assert deser.message.content
|
||||
assert deser.message.additional_kwargs["parsed"] == my_model.model_dump()
|
||||
@@ -275,8 +260,8 @@ def test_serialization_with_ignore_unserializable_fields() -> None:
|
||||
]
|
||||
]
|
||||
}
|
||||
# Load directly (no dumpd - this is already serialized data)
|
||||
deser = load(data, allowed_objects=[AIMessage], ignore_unserializable_fields=True)
|
||||
ser = dumpd(data)
|
||||
deser = load(ser, ignore_unserializable_fields=True)
|
||||
assert deser == {
|
||||
"messages": [
|
||||
[
|
||||
@@ -380,514 +365,3 @@ def test_dumps_mixed_data_structure() -> None:
|
||||
# Primitives should remain unchanged
|
||||
assert parsed["list"] == [1, 2, {"nested": "value"}]
|
||||
assert parsed["primitive"] == "string"
|
||||
|
||||
|
||||
def test_document_normal_metadata_allowed() -> None:
|
||||
"""Test that `Document` metadata without `'lc'` key works fine."""
|
||||
doc = Document(
|
||||
page_content="Hello world",
|
||||
metadata={"source": "test.txt", "page": 1, "nested": {"key": "value"}},
|
||||
)
|
||||
serialized = dumpd(doc)
|
||||
|
||||
loaded = load(serialized, allowed_objects=[Document])
|
||||
assert loaded.page_content == "Hello world"
|
||||
|
||||
expected = {"source": "test.txt", "page": 1, "nested": {"key": "value"}}
|
||||
assert loaded.metadata == expected
|
||||
|
||||
|
||||
class TestEscaping:
|
||||
"""Tests that escape-based serialization prevents injection attacks.
|
||||
|
||||
When user data contains an `'lc'` key, it's escaped during serialization
|
||||
(wrapped in `{"__lc_escaped__": ...}`). During deserialization, escaped
|
||||
dicts are unwrapped and returned as plain dicts - NOT instantiated as
|
||||
LC objects.
|
||||
"""
|
||||
|
||||
def test_document_metadata_with_lc_key_escaped(self) -> None:
|
||||
"""Test that `Document` metadata with `'lc'` key round-trips as plain dict."""
|
||||
# User data that looks like an LC constructor - should be escaped, not executed
|
||||
suspicious_metadata = {"lc": 1, "type": "constructor", "id": ["some", "module"]}
|
||||
doc = Document(page_content="test", metadata=suspicious_metadata)
|
||||
|
||||
# Serialize - should escape the metadata
|
||||
serialized = dumpd(doc)
|
||||
assert serialized["kwargs"]["metadata"] == {
|
||||
"__lc_escaped__": suspicious_metadata
|
||||
}
|
||||
|
||||
# Deserialize - should restore original metadata as plain dict
|
||||
loaded = load(serialized, allowed_objects=[Document])
|
||||
assert loaded.metadata == suspicious_metadata # Plain dict, not instantiated
|
||||
|
||||
def test_document_metadata_with_nested_lc_key_escaped(self) -> None:
|
||||
"""Test that nested `'lc'` key in `Document` metadata is escaped."""
|
||||
suspicious_nested = {"lc": 1, "type": "constructor", "id": ["some", "module"]}
|
||||
doc = Document(page_content="test", metadata={"nested": suspicious_nested})
|
||||
|
||||
serialized = dumpd(doc)
|
||||
# The nested dict with 'lc' key should be escaped
|
||||
assert serialized["kwargs"]["metadata"]["nested"] == {
|
||||
"__lc_escaped__": suspicious_nested
|
||||
}
|
||||
|
||||
loaded = load(serialized, allowed_objects=[Document])
|
||||
assert loaded.metadata == {"nested": suspicious_nested}
|
||||
|
||||
def test_document_metadata_with_lc_key_in_list_escaped(self) -> None:
|
||||
"""Test that `'lc'` key in list items within `Document` metadata is escaped."""
|
||||
suspicious_item = {"lc": 1, "type": "constructor", "id": ["some", "module"]}
|
||||
doc = Document(page_content="test", metadata={"items": [suspicious_item]})
|
||||
|
||||
serialized = dumpd(doc)
|
||||
assert serialized["kwargs"]["metadata"]["items"][0] == {
|
||||
"__lc_escaped__": suspicious_item
|
||||
}
|
||||
|
||||
loaded = load(serialized, allowed_objects=[Document])
|
||||
assert loaded.metadata == {"items": [suspicious_item]}
|
||||
|
||||
def test_malicious_payload_not_instantiated(self) -> None:
|
||||
"""Test that malicious LC-like structures in user data are NOT instantiated."""
|
||||
# An attacker might craft a payload with a valid AIMessage structure in metadata
|
||||
malicious_data = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain", "schema", "document", "Document"],
|
||||
"kwargs": {
|
||||
"page_content": "test",
|
||||
"metadata": {
|
||||
# This looks like a valid LC object but is in escaped form
|
||||
"__lc_escaped__": {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain_core", "messages", "ai", "AIMessage"],
|
||||
"kwargs": {"content": "injected message"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Even though AIMessage is allowed, the metadata should remain as dict
|
||||
loaded = load(malicious_data, allowed_objects=[Document, AIMessage])
|
||||
assert loaded.page_content == "test"
|
||||
# The metadata is the original dict (unescaped), NOT an AIMessage instance
|
||||
assert loaded.metadata == {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain_core", "messages", "ai", "AIMessage"],
|
||||
"kwargs": {"content": "injected message"},
|
||||
}
|
||||
assert not isinstance(loaded.metadata, AIMessage)
|
||||
|
||||
def test_message_additional_kwargs_with_lc_key_escaped(self) -> None:
|
||||
"""Test that `AIMessage` `additional_kwargs` with `'lc'` is escaped."""
|
||||
suspicious_data = {"lc": 1, "type": "constructor", "id": ["x", "y"]}
|
||||
msg = AIMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"data": suspicious_data},
|
||||
)
|
||||
|
||||
serialized = dumpd(msg)
|
||||
assert serialized["kwargs"]["additional_kwargs"]["data"] == {
|
||||
"__lc_escaped__": suspicious_data
|
||||
}
|
||||
|
||||
loaded = load(serialized, allowed_objects=[AIMessage])
|
||||
assert loaded.additional_kwargs == {"data": suspicious_data}
|
||||
|
||||
def test_message_response_metadata_with_lc_key_escaped(self) -> None:
|
||||
"""Test that `AIMessage` `response_metadata` with `'lc'` is escaped."""
|
||||
suspicious_data = {"lc": 1, "type": "constructor", "id": ["x", "y"]}
|
||||
msg = AIMessage(content="Hello", response_metadata=suspicious_data)
|
||||
|
||||
serialized = dumpd(msg)
|
||||
assert serialized["kwargs"]["response_metadata"] == {
|
||||
"__lc_escaped__": suspicious_data
|
||||
}
|
||||
|
||||
loaded = load(serialized, allowed_objects=[AIMessage])
|
||||
assert loaded.response_metadata == suspicious_data
|
||||
|
||||
def test_double_escape_handling(self) -> None:
|
||||
"""Test that data containing escape key itself is properly handled."""
|
||||
# User data that contains our escape key
|
||||
data_with_escape_key = {"__lc_escaped__": "some_value"}
|
||||
doc = Document(page_content="test", metadata=data_with_escape_key)
|
||||
|
||||
serialized = dumpd(doc)
|
||||
# Should be double-escaped since it looks like an escaped dict
|
||||
assert serialized["kwargs"]["metadata"] == {
|
||||
"__lc_escaped__": {"__lc_escaped__": "some_value"}
|
||||
}
|
||||
|
||||
loaded = load(serialized, allowed_objects=[Document])
|
||||
assert loaded.metadata == {"__lc_escaped__": "some_value"}
|
||||
|
||||
|
||||
class TestDumpdEscapesLcKeyInPlainDicts:
|
||||
"""Tests that `dumpd()` escapes `'lc'` keys in plain dict kwargs."""
|
||||
|
||||
def test_normal_message_not_escaped(self) -> None:
|
||||
"""Test that normal `AIMessage` without `'lc'` key is not escaped."""
|
||||
msg = AIMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"tool_calls": []},
|
||||
response_metadata={"model": "gpt-4"},
|
||||
)
|
||||
serialized = dumpd(msg)
|
||||
assert serialized["kwargs"]["content"] == "Hello"
|
||||
# No escape wrappers for normal data
|
||||
assert "__lc_escaped__" not in str(serialized)
|
||||
|
||||
def test_document_metadata_with_lc_key_escaped(self) -> None:
|
||||
"""Test that `Document` with `'lc'` key in metadata is escaped."""
|
||||
doc = Document(
|
||||
page_content="test",
|
||||
metadata={"lc": 1, "type": "constructor"},
|
||||
)
|
||||
|
||||
serialized = dumpd(doc)
|
||||
# Should be escaped, not blocked
|
||||
assert serialized["kwargs"]["metadata"] == {
|
||||
"__lc_escaped__": {"lc": 1, "type": "constructor"}
|
||||
}
|
||||
|
||||
def test_document_metadata_with_nested_lc_key_escaped(self) -> None:
|
||||
"""Test that `Document` with nested `'lc'` in metadata is escaped."""
|
||||
doc = Document(
|
||||
page_content="test",
|
||||
metadata={"nested": {"lc": 1}},
|
||||
)
|
||||
|
||||
serialized = dumpd(doc)
|
||||
assert serialized["kwargs"]["metadata"]["nested"] == {
|
||||
"__lc_escaped__": {"lc": 1}
|
||||
}
|
||||
|
||||
def test_message_additional_kwargs_with_lc_key_escaped(self) -> None:
|
||||
"""Test `AIMessage` with `'lc'` in `additional_kwargs` is escaped."""
|
||||
msg = AIMessage(
|
||||
content="Hello",
|
||||
additional_kwargs={"malicious": {"lc": 1}},
|
||||
)
|
||||
|
||||
serialized = dumpd(msg)
|
||||
assert serialized["kwargs"]["additional_kwargs"]["malicious"] == {
|
||||
"__lc_escaped__": {"lc": 1}
|
||||
}
|
||||
|
||||
def test_message_response_metadata_with_lc_key_escaped(self) -> None:
|
||||
"""Test `AIMessage` with `'lc'` in `response_metadata` is escaped."""
|
||||
msg = AIMessage(
|
||||
content="Hello",
|
||||
response_metadata={"lc": 1},
|
||||
)
|
||||
|
||||
serialized = dumpd(msg)
|
||||
assert serialized["kwargs"]["response_metadata"] == {
|
||||
"__lc_escaped__": {"lc": 1}
|
||||
}
|
||||
|
||||
|
||||
class TestInitValidator:
|
||||
"""Tests for `init_validator` on `load()` and `loads()`."""
|
||||
|
||||
def test_init_validator_allows_valid_kwargs(self) -> None:
|
||||
"""Test that `init_validator` returning None allows deserialization."""
|
||||
msg = AIMessage(content="Hello")
|
||||
serialized = dumpd(msg)
|
||||
|
||||
def allow_all(_class_path: tuple[str, ...], _kwargs: dict[str, Any]) -> None:
|
||||
pass # Allow all by doing nothing
|
||||
|
||||
loaded = load(serialized, allowed_objects=[AIMessage], init_validator=allow_all)
|
||||
assert loaded == msg
|
||||
|
||||
def test_init_validator_blocks_deserialization(self) -> None:
|
||||
"""Test that `init_validator` can block deserialization by raising."""
|
||||
doc = Document(page_content="test", metadata={"source": "test.txt"})
|
||||
serialized = dumpd(doc)
|
||||
|
||||
def block_metadata(
|
||||
_class_path: tuple[str, ...], kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
if "metadata" in kwargs:
|
||||
msg = "Metadata not allowed"
|
||||
raise ValueError(msg)
|
||||
|
||||
with pytest.raises(ValueError, match="Metadata not allowed"):
|
||||
load(serialized, allowed_objects=[Document], init_validator=block_metadata)
|
||||
|
||||
def test_init_validator_receives_correct_class_path(self) -> None:
|
||||
"""Test that `init_validator` receives the correct class path."""
|
||||
msg = AIMessage(content="Hello")
|
||||
serialized = dumpd(msg)
|
||||
|
||||
received_class_paths: list[tuple[str, ...]] = []
|
||||
|
||||
def capture_class_path(
|
||||
class_path: tuple[str, ...], _kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
received_class_paths.append(class_path)
|
||||
|
||||
load(serialized, allowed_objects=[AIMessage], init_validator=capture_class_path)
|
||||
|
||||
assert len(received_class_paths) == 1
|
||||
assert received_class_paths[0] == (
|
||||
"langchain",
|
||||
"schema",
|
||||
"messages",
|
||||
"AIMessage",
|
||||
)
|
||||
|
||||
def test_init_validator_receives_correct_kwargs(self) -> None:
|
||||
"""Test that `init_validator` receives the kwargs dict."""
|
||||
msg = AIMessage(content="Hello world", name="test_name")
|
||||
serialized = dumpd(msg)
|
||||
|
||||
received_kwargs: list[dict[str, Any]] = []
|
||||
|
||||
def capture_kwargs(
|
||||
_class_path: tuple[str, ...], kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
received_kwargs.append(kwargs)
|
||||
|
||||
load(serialized, allowed_objects=[AIMessage], init_validator=capture_kwargs)
|
||||
|
||||
assert len(received_kwargs) == 1
|
||||
assert "content" in received_kwargs[0]
|
||||
assert received_kwargs[0]["content"] == "Hello world"
|
||||
assert "name" in received_kwargs[0]
|
||||
assert received_kwargs[0]["name"] == "test_name"
|
||||
|
||||
def test_init_validator_with_loads(self) -> None:
|
||||
"""Test that `init_validator` works with `loads()` function."""
|
||||
doc = Document(page_content="test", metadata={"key": "value"})
|
||||
json_str = dumps(doc)
|
||||
|
||||
def block_metadata(
|
||||
_class_path: tuple[str, ...], kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
if "metadata" in kwargs:
|
||||
msg = "Metadata not allowed"
|
||||
raise ValueError(msg)
|
||||
|
||||
with pytest.raises(ValueError, match="Metadata not allowed"):
|
||||
loads(json_str, allowed_objects=[Document], init_validator=block_metadata)
|
||||
|
||||
def test_init_validator_none_allows_all(self) -> None:
|
||||
"""Test that `init_validator=None` (default) allows all kwargs."""
|
||||
msg = AIMessage(content="Hello")
|
||||
serialized = dumpd(msg)
|
||||
|
||||
# Should work without init_validator
|
||||
loaded = load(serialized, allowed_objects=[AIMessage])
|
||||
assert loaded == msg
|
||||
|
||||
def test_init_validator_type_alias_exists(self) -> None:
|
||||
"""Test that `InitValidator` type alias is exported and usable."""
|
||||
|
||||
def my_validator(_class_path: tuple[str, ...], _kwargs: dict[str, Any]) -> None:
|
||||
pass
|
||||
|
||||
validator_typed: InitValidator = my_validator
|
||||
assert callable(validator_typed)
|
||||
|
||||
def test_init_validator_blocks_specific_class(self) -> None:
|
||||
"""Test blocking deserialization for a specific class."""
|
||||
doc = Document(page_content="test", metadata={"source": "test.txt"})
|
||||
serialized = dumpd(doc)
|
||||
|
||||
def block_documents(
|
||||
class_path: tuple[str, ...], _kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
if class_path == ("langchain", "schema", "document", "Document"):
|
||||
msg = "Documents not allowed"
|
||||
raise ValueError(msg)
|
||||
|
||||
with pytest.raises(ValueError, match="Documents not allowed"):
|
||||
load(serialized, allowed_objects=[Document], init_validator=block_documents)
|
||||
|
||||
|
||||
class TestJinja2SecurityBlocking:
|
||||
"""Tests blocking Jinja2 templates by default."""
|
||||
|
||||
def test_fstring_template_allowed(self) -> None:
|
||||
"""Test that f-string templates deserialize successfully."""
|
||||
# Serialized ChatPromptTemplate with f-string format
|
||||
serialized = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain", "prompts", "chat", "ChatPromptTemplate"],
|
||||
"kwargs": {
|
||||
"input_variables": ["name"],
|
||||
"messages": [
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"chat",
|
||||
"HumanMessagePromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"prompt": {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["name"],
|
||||
"template": "Hello {name}",
|
||||
"template_format": "f-string",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# f-string should deserialize successfully
|
||||
loaded = load(
|
||||
serialized,
|
||||
allowed_objects=[
|
||||
ChatPromptTemplate,
|
||||
HumanMessagePromptTemplate,
|
||||
PromptTemplate,
|
||||
],
|
||||
)
|
||||
assert isinstance(loaded, ChatPromptTemplate)
|
||||
assert loaded.input_variables == ["name"]
|
||||
|
||||
def test_jinja2_template_blocked(self) -> None:
|
||||
"""Test that Jinja2 templates are blocked by default."""
|
||||
# Malicious serialized payload attempting to use jinja2
|
||||
malicious_serialized = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain", "prompts", "chat", "ChatPromptTemplate"],
|
||||
"kwargs": {
|
||||
"input_variables": ["name"],
|
||||
"messages": [
|
||||
{
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"chat",
|
||||
"HumanMessagePromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"prompt": {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate",
|
||||
],
|
||||
"kwargs": {
|
||||
"input_variables": ["name"],
|
||||
"template": "{{ name }}",
|
||||
"template_format": "jinja2",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# jinja2 should be blocked by default
|
||||
with pytest.raises(ValueError, match="Jinja2 templates are not allowed"):
|
||||
load(
|
||||
malicious_serialized,
|
||||
allowed_objects=[
|
||||
ChatPromptTemplate,
|
||||
HumanMessagePromptTemplate,
|
||||
PromptTemplate,
|
||||
],
|
||||
)
|
||||
|
||||
def test_jinja2_blocked_standalone_prompt_template(self) -> None:
|
||||
"""Test blocking Jinja2 on standalone `PromptTemplate`."""
|
||||
serialized_jinja2 = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain", "prompts", "prompt", "PromptTemplate"],
|
||||
"kwargs": {
|
||||
"input_variables": ["name"],
|
||||
"template": "{{ name }}",
|
||||
"template_format": "jinja2",
|
||||
},
|
||||
}
|
||||
|
||||
serialized_fstring = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain", "prompts", "prompt", "PromptTemplate"],
|
||||
"kwargs": {
|
||||
"input_variables": ["name"],
|
||||
"template": "{name}",
|
||||
"template_format": "f-string",
|
||||
},
|
||||
}
|
||||
|
||||
# f-string should work
|
||||
loaded = load(
|
||||
serialized_fstring,
|
||||
allowed_objects=[PromptTemplate],
|
||||
)
|
||||
assert isinstance(loaded, PromptTemplate)
|
||||
assert loaded.template == "{name}"
|
||||
|
||||
# jinja2 should be blocked by default
|
||||
with pytest.raises(ValueError, match="Jinja2 templates are not allowed"):
|
||||
load(
|
||||
serialized_jinja2,
|
||||
allowed_objects=[PromptTemplate],
|
||||
)
|
||||
|
||||
def test_jinja2_blocked_by_default(self) -> None:
|
||||
"""Test that Jinja2 templates are blocked by default."""
|
||||
serialized_jinja2 = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain", "prompts", "prompt", "PromptTemplate"],
|
||||
"kwargs": {
|
||||
"input_variables": ["name"],
|
||||
"template": "{{ name }}",
|
||||
"template_format": "jinja2",
|
||||
},
|
||||
}
|
||||
|
||||
serialized_fstring = {
|
||||
"lc": 1,
|
||||
"type": "constructor",
|
||||
"id": ["langchain", "prompts", "prompt", "PromptTemplate"],
|
||||
"kwargs": {
|
||||
"input_variables": ["name"],
|
||||
"template": "{name}",
|
||||
"template_format": "f-string",
|
||||
},
|
||||
}
|
||||
|
||||
# f-string should work
|
||||
loaded = load(serialized_fstring, allowed_objects=[PromptTemplate])
|
||||
assert isinstance(loaded, PromptTemplate)
|
||||
assert loaded.template == "{name}"
|
||||
|
||||
# jinja2 should be blocked by default
|
||||
with pytest.raises(ValueError, match="Jinja2 templates are not allowed"):
|
||||
load(serialized_jinja2, allowed_objects=[PromptTemplate])
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
|
||||
def test_convert_to_v1_from_anthropic() -> None:
|
||||
message = AIMessage(
|
||||
[
|
||||
{"type": "thinking", "thinking": "foo", "signature": "foo_signature"},
|
||||
{"type": "text", "text": "Let's call a tool."},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "It's sunny.",
|
||||
"citations": [
|
||||
{
|
||||
"type": "search_result_location",
|
||||
"cited_text": "The weather is sunny.",
|
||||
"source": "source_123",
|
||||
"title": "Document Title",
|
||||
"search_result_index": 1,
|
||||
"start_block_index": 0,
|
||||
"end_block_index": 2,
|
||||
},
|
||||
{"bar": "baz"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "server_tool_use",
|
||||
"name": "web_search",
|
||||
"input": {"query": "web search query"},
|
||||
"id": "srvtoolu_abc123",
|
||||
},
|
||||
{
|
||||
"type": "web_search_tool_result",
|
||||
"tool_use_id": "srvtoolu_abc123",
|
||||
"content": [
|
||||
{
|
||||
"type": "web_search_result",
|
||||
"title": "Page Title 1",
|
||||
"url": "<page url 1>",
|
||||
"page_age": "January 1, 2025",
|
||||
"encrypted_content": "<encrypted content 1>",
|
||||
},
|
||||
{
|
||||
"type": "web_search_result",
|
||||
"title": "Page Title 2",
|
||||
"url": "<page url 2>",
|
||||
"page_age": "January 2, 2025",
|
||||
"encrypted_content": "<encrypted content 2>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "server_tool_use",
|
||||
"id": "srvtoolu_def456",
|
||||
"name": "code_execution",
|
||||
"input": {"code": "import numpy as np..."},
|
||||
},
|
||||
{
|
||||
"type": "code_execution_tool_result",
|
||||
"tool_use_id": "srvtoolu_def456",
|
||||
"content": {
|
||||
"type": "code_execution_result",
|
||||
"stdout": "Mean: 5.5\nStandard deviation...",
|
||||
"stderr": "",
|
||||
"return_code": 0,
|
||||
},
|
||||
},
|
||||
{"type": "something_else", "foo": "bar"},
|
||||
],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
)
|
||||
expected_content: list[types.ContentBlock] = [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"reasoning": "foo",
|
||||
"extras": {"signature": "foo_signature"},
|
||||
},
|
||||
{"type": "text", "text": "Let's call a tool."},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "It's sunny.",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "citation",
|
||||
"title": "Document Title",
|
||||
"cited_text": "The weather is sunny.",
|
||||
"extras": {
|
||||
"source": "source_123",
|
||||
"search_result_index": 1,
|
||||
"start_block_index": 0,
|
||||
"end_block_index": 2,
|
||||
},
|
||||
},
|
||||
{"type": "non_standard_annotation", "value": {"bar": "baz"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "server_tool_call",
|
||||
"name": "web_search",
|
||||
"id": "srvtoolu_abc123",
|
||||
"args": {"query": "web search query"},
|
||||
},
|
||||
{
|
||||
"type": "server_tool_result",
|
||||
"tool_call_id": "srvtoolu_abc123",
|
||||
"output": [
|
||||
{
|
||||
"type": "web_search_result",
|
||||
"title": "Page Title 1",
|
||||
"url": "<page url 1>",
|
||||
"page_age": "January 1, 2025",
|
||||
"encrypted_content": "<encrypted content 1>",
|
||||
},
|
||||
{
|
||||
"type": "web_search_result",
|
||||
"title": "Page Title 2",
|
||||
"url": "<page url 2>",
|
||||
"page_age": "January 2, 2025",
|
||||
"encrypted_content": "<encrypted content 2>",
|
||||
},
|
||||
],
|
||||
"status": "success",
|
||||
"extras": {"block_type": "web_search_tool_result"},
|
||||
},
|
||||
{
|
||||
"type": "server_tool_call",
|
||||
"name": "code_interpreter",
|
||||
"id": "srvtoolu_def456",
|
||||
"args": {"code": "import numpy as np..."},
|
||||
},
|
||||
{
|
||||
"type": "server_tool_result",
|
||||
"tool_call_id": "srvtoolu_def456",
|
||||
"output": {
|
||||
"type": "code_execution_result",
|
||||
"return_code": 0,
|
||||
"stdout": "Mean: 5.5\nStandard deviation...",
|
||||
"stderr": "",
|
||||
},
|
||||
"status": "success",
|
||||
"extras": {"block_type": "code_execution_tool_result"},
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {"type": "something_else", "foo": "bar"},
|
||||
},
|
||||
]
|
||||
assert message.content_blocks == expected_content
|
||||
|
||||
# Check no mutation
|
||||
assert message.content != expected_content
|
||||
|
||||
message = AIMessage("Hello", response_metadata={"model_provider": "anthropic"})
|
||||
expected_content = [{"type": "text", "text": "Hello"}]
|
||||
assert message.content_blocks == expected_content
|
||||
assert message.content != expected_content # check no mutation
|
||||
|
||||
|
||||
def test_convert_to_v1_from_anthropic_chunk() -> None:
|
||||
chunks = [
|
||||
AIMessageChunk(
|
||||
content=[{"text": "Looking ", "type": "text", "index": 0}],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[{"text": "now.", "type": "text", "index": 0}],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": "get_weather",
|
||||
"input": {},
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": "",
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[{"type": "input_json_delta", "partial_json": "", "index": 1}],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": "",
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": '{"loca', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": '{"loca',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": 'tion": "San ', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": 'tion": "San ',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": 'Francisco"}', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": 'Francisco"}',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
),
|
||||
]
|
||||
expected_contents: list[types.ContentBlock] = [
|
||||
{"type": "text", "text": "Looking ", "index": 0},
|
||||
{"type": "text", "text": "now.", "index": 0},
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": "",
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
{"name": None, "args": "", "id": None, "index": 1, "type": "tool_call_chunk"},
|
||||
{
|
||||
"name": None,
|
||||
"args": '{"loca',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
{
|
||||
"name": None,
|
||||
"args": 'tion": "San ',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
{
|
||||
"name": None,
|
||||
"args": 'Francisco"}',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
]
|
||||
for chunk, expected in zip(chunks, expected_contents):
|
||||
assert chunk.content_blocks == [expected]
|
||||
|
||||
full: Optional[AIMessageChunk] = None
|
||||
for chunk in chunks:
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
|
||||
expected_content = [
|
||||
{"type": "text", "text": "Looking now.", "index": 0},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": "get_weather",
|
||||
"partial_json": '{"location": "San Francisco"}',
|
||||
"input": {},
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
]
|
||||
assert full.content == expected_content
|
||||
|
||||
expected_content_blocks = [
|
||||
{"type": "text", "text": "Looking now.", "index": 0},
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": '{"location": "San Francisco"}',
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
]
|
||||
assert full.content_blocks == expected_content_blocks
|
||||
|
||||
# Test parse partial json
|
||||
full = AIMessageChunk(
|
||||
content=[
|
||||
{
|
||||
"id": "srvtoolu_abc123",
|
||||
"input": {},
|
||||
"name": "web_fetch",
|
||||
"type": "server_tool_use",
|
||||
"index": 0,
|
||||
"partial_json": '{"url": "https://docs.langchain.com"}',
|
||||
},
|
||||
{
|
||||
"id": "mcptoolu_abc123",
|
||||
"input": {},
|
||||
"name": "ask_question",
|
||||
"server_name": "<my server name>",
|
||||
"type": "mcp_tool_use",
|
||||
"index": 1,
|
||||
"partial_json": '{"repoName": "<my repo>", "question": "<my query>"}',
|
||||
},
|
||||
],
|
||||
response_metadata={"model_provider": "anthropic"},
|
||||
chunk_position="last",
|
||||
)
|
||||
expected_content_blocks = [
|
||||
{
|
||||
"type": "server_tool_call",
|
||||
"name": "web_fetch",
|
||||
"id": "srvtoolu_abc123",
|
||||
"args": {"url": "https://docs.langchain.com"},
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"type": "server_tool_call",
|
||||
"name": "remote_mcp",
|
||||
"id": "mcptoolu_abc123",
|
||||
"args": {"repoName": "<my repo>", "question": "<my query>"},
|
||||
"extras": {"tool_name": "ask_question", "server_name": "<my server name>"},
|
||||
"index": 1,
|
||||
},
|
||||
]
|
||||
assert full.content_blocks == expected_content_blocks
|
||||
|
||||
|
||||
def test_convert_to_v1_from_anthropic_input() -> None:
|
||||
message = HumanMessage(
|
||||
[
|
||||
{"type": "text", "text": "foo"},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"data": "<base64 data>",
|
||||
"media_type": "application/pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "url",
|
||||
"url": "<document url>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "content",
|
||||
"content": [
|
||||
{"type": "text", "text": "The grass is green"},
|
||||
{"type": "text", "text": "The sky is blue"},
|
||||
],
|
||||
},
|
||||
"citations": {"enabled": True},
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "text",
|
||||
"data": "<plain text data>",
|
||||
"media_type": "text/plain",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/jpeg",
|
||||
"data": "<base64 image data>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "url",
|
||||
"url": "<image url>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "file",
|
||||
"file_id": "<image file id>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {"type": "file", "file_id": "<pdf file id>"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
expected: list[types.ContentBlock] = [
|
||||
{"type": "text", "text": "foo"},
|
||||
{
|
||||
"type": "file",
|
||||
"base64": "<base64 data>",
|
||||
"mime_type": "application/pdf",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "<document url>",
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "content",
|
||||
"content": [
|
||||
{"type": "text", "text": "The grass is green"},
|
||||
{"type": "text", "text": "The sky is blue"},
|
||||
],
|
||||
},
|
||||
"citations": {"enabled": True},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "text-plain",
|
||||
"text": "<plain text data>",
|
||||
"mime_type": "text/plain",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base64": "<base64 image data>",
|
||||
"mime_type": "image/jpeg",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"url": "<image url>",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"id": "<image file id>",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"id": "<pdf file id>",
|
||||
},
|
||||
]
|
||||
|
||||
assert message.content_blocks == expected
|
||||
@@ -0,0 +1,407 @@
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
|
||||
def test_convert_to_v1_from_bedrock() -> None:
|
||||
message = AIMessage(
|
||||
[
|
||||
{"type": "thinking", "thinking": "foo", "signature": "foo_signature"},
|
||||
{"type": "text", "text": "Let's call a tool."},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "It's sunny.",
|
||||
"citations": [
|
||||
{
|
||||
"type": "search_result_location",
|
||||
"cited_text": "The weather is sunny.",
|
||||
"source": "source_123",
|
||||
"title": "Document Title",
|
||||
"search_result_index": 1,
|
||||
"start_block_index": 0,
|
||||
"end_block_index": 2,
|
||||
},
|
||||
{"bar": "baz"},
|
||||
],
|
||||
},
|
||||
{"type": "something_else", "foo": "bar"},
|
||||
],
|
||||
tool_calls=[
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "abc_234",
|
||||
"name": "another_tool",
|
||||
"args": {"arg_1": "value_1"},
|
||||
},
|
||||
],
|
||||
response_metadata={
|
||||
"model_provider": "bedrock",
|
||||
"model_name": "us.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
},
|
||||
)
|
||||
expected_content: list[types.ContentBlock] = [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"reasoning": "foo",
|
||||
"extras": {"signature": "foo_signature"},
|
||||
},
|
||||
{"type": "text", "text": "Let's call a tool."},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "It's sunny.",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "citation",
|
||||
"title": "Document Title",
|
||||
"cited_text": "The weather is sunny.",
|
||||
"extras": {
|
||||
"source": "source_123",
|
||||
"search_result_index": 1,
|
||||
"start_block_index": 0,
|
||||
"end_block_index": 2,
|
||||
},
|
||||
},
|
||||
{"type": "non_standard_annotation", "value": {"bar": "baz"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {"type": "something_else", "foo": "bar"},
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "abc_234",
|
||||
"name": "another_tool",
|
||||
"args": {"arg_1": "value_1"},
|
||||
},
|
||||
]
|
||||
assert message.content_blocks == expected_content
|
||||
|
||||
# Check no mutation
|
||||
assert message.content != expected_content
|
||||
|
||||
# Test with a non-Anthropic message
|
||||
message = AIMessage(
|
||||
[
|
||||
{"type": "text", "text": "Let's call a tool."},
|
||||
{"type": "something_else", "foo": "bar"},
|
||||
],
|
||||
tool_calls=[
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock"},
|
||||
)
|
||||
expected_content = [
|
||||
{"type": "text", "text": "Let's call a tool."},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {"type": "something_else", "foo": "bar"},
|
||||
},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
},
|
||||
]
|
||||
assert message.content_blocks == expected_content
|
||||
|
||||
|
||||
def test_convert_to_v1_from_bedrock_chunk() -> None:
|
||||
chunks = [
|
||||
AIMessageChunk(
|
||||
content=[{"text": "Looking ", "type": "text", "index": 0}],
|
||||
response_metadata={"model_provider": "bedrock"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[{"text": "now.", "type": "text", "index": 0}],
|
||||
response_metadata={"model_provider": "bedrock"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": "get_weather",
|
||||
"input": {},
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": "",
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[{"type": "input_json_delta", "partial_json": "", "index": 1}],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": "",
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": '{"loca', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": '{"loca',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": 'tion": "San ', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": 'tion": "San ',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": 'Francisco"}', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": 'Francisco"}',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock"},
|
||||
),
|
||||
]
|
||||
expected_contents: list[types.ContentBlock] = [
|
||||
{"type": "text", "text": "Looking ", "index": 0},
|
||||
{"type": "text", "text": "now.", "index": 0},
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": "",
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
{"name": None, "args": "", "id": None, "index": 1, "type": "tool_call_chunk"},
|
||||
{
|
||||
"name": None,
|
||||
"args": '{"loca',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
{
|
||||
"name": None,
|
||||
"args": 'tion": "San ',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
{
|
||||
"name": None,
|
||||
"args": 'Francisco"}',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
]
|
||||
for chunk, expected in zip(chunks, expected_contents):
|
||||
assert chunk.content_blocks == [expected]
|
||||
|
||||
full: Optional[AIMessageChunk] = None
|
||||
for chunk in chunks:
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
|
||||
expected_content = [
|
||||
{"type": "text", "text": "Looking now.", "index": 0},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": "get_weather",
|
||||
"partial_json": '{"location": "San Francisco"}',
|
||||
"input": {},
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
]
|
||||
assert full.content == expected_content
|
||||
|
||||
expected_content_blocks = [
|
||||
{"type": "text", "text": "Looking now.", "index": 0},
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": '{"location": "San Francisco"}',
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
]
|
||||
assert full.content_blocks == expected_content_blocks
|
||||
|
||||
|
||||
def test_convert_to_v1_from_bedrock_input() -> None:
|
||||
message = HumanMessage(
|
||||
[
|
||||
{"type": "text", "text": "foo"},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"data": "<base64 data>",
|
||||
"media_type": "application/pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "url",
|
||||
"url": "<document url>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "content",
|
||||
"content": [
|
||||
{"type": "text", "text": "The grass is green"},
|
||||
{"type": "text", "text": "The sky is blue"},
|
||||
],
|
||||
},
|
||||
"citations": {"enabled": True},
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "text",
|
||||
"data": "<plain text data>",
|
||||
"media_type": "text/plain",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/jpeg",
|
||||
"data": "<base64 image data>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "url",
|
||||
"url": "<image url>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "file",
|
||||
"file_id": "<image file id>",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"source": {"type": "file", "file_id": "<pdf file id>"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
expected: list[types.ContentBlock] = [
|
||||
{"type": "text", "text": "foo"},
|
||||
{
|
||||
"type": "file",
|
||||
"base64": "<base64 data>",
|
||||
"mime_type": "application/pdf",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "<document url>",
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "content",
|
||||
"content": [
|
||||
{"type": "text", "text": "The grass is green"},
|
||||
{"type": "text", "text": "The sky is blue"},
|
||||
],
|
||||
},
|
||||
"citations": {"enabled": True},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "text-plain",
|
||||
"text": "<plain text data>",
|
||||
"mime_type": "text/plain",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base64": "<base64 image data>",
|
||||
"mime_type": "image/jpeg",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"url": "<image url>",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"id": "<image file id>",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"id": "<pdf file id>",
|
||||
},
|
||||
]
|
||||
|
||||
assert message.content_blocks == expected
|
||||
@@ -0,0 +1,381 @@
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
|
||||
def test_convert_to_v1_from_bedrock_converse() -> None:
|
||||
message = AIMessage(
|
||||
[
|
||||
{
|
||||
"type": "reasoning_content",
|
||||
"reasoning_content": {"text": "foo", "signature": "foo_signature"},
|
||||
},
|
||||
{"type": "text", "text": "Let's call a tool."},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "It's sunny.",
|
||||
"citations": [
|
||||
{
|
||||
"title": "Document Title",
|
||||
"source_content": [{"text": "The weather is sunny."}],
|
||||
"location": {
|
||||
"document_char": {
|
||||
"document_index": 0,
|
||||
"start": 58,
|
||||
"end": 96,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Document Title",
|
||||
"source_content": [{"text": "The weather is sunny."}],
|
||||
"location": {
|
||||
"document_page": {"document_index": 0, "start": 1, "end": 2}
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Document Title",
|
||||
"source_content": [{"text": "The weather is sunny."}],
|
||||
"location": {
|
||||
"document_chunk": {
|
||||
"document_index": 0,
|
||||
"start": 1,
|
||||
"end": 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
{"bar": "baz"},
|
||||
],
|
||||
},
|
||||
{"type": "something_else", "foo": "bar"},
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
)
|
||||
expected_content: list[types.ContentBlock] = [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"reasoning": "foo",
|
||||
"extras": {"signature": "foo_signature"},
|
||||
},
|
||||
{"type": "text", "text": "Let's call a tool."},
|
||||
{
|
||||
"type": "tool_call",
|
||||
"id": "abc_123",
|
||||
"name": "get_weather",
|
||||
"args": {"location": "San Francisco"},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "It's sunny.",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "citation",
|
||||
"title": "Document Title",
|
||||
"cited_text": "The weather is sunny.",
|
||||
"extras": {
|
||||
"location": {
|
||||
"document_char": {
|
||||
"document_index": 0,
|
||||
"start": 58,
|
||||
"end": 96,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "citation",
|
||||
"title": "Document Title",
|
||||
"cited_text": "The weather is sunny.",
|
||||
"extras": {
|
||||
"location": {
|
||||
"document_page": {"document_index": 0, "start": 1, "end": 2}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "citation",
|
||||
"title": "Document Title",
|
||||
"cited_text": "The weather is sunny.",
|
||||
"extras": {
|
||||
"location": {
|
||||
"document_chunk": {
|
||||
"document_index": 0,
|
||||
"start": 1,
|
||||
"end": 2,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{"type": "citation", "extras": {"bar": "baz"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {"type": "something_else", "foo": "bar"},
|
||||
},
|
||||
]
|
||||
assert message.content_blocks == expected_content
|
||||
|
||||
# Check no mutation
|
||||
assert message.content != expected_content
|
||||
|
||||
|
||||
def test_convert_to_v1_from_converse_chunk() -> None:
|
||||
chunks = [
|
||||
AIMessageChunk(
|
||||
content=[{"text": "Looking ", "type": "text", "index": 0}],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[{"text": "now.", "type": "text", "index": 0}],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": "get_weather",
|
||||
"input": {},
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": "",
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[{"type": "input_json_delta", "partial_json": "", "index": 1}],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": "",
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": '{"loca', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": '{"loca',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": 'tion": "San ', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": 'tion": "San ',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
),
|
||||
AIMessageChunk(
|
||||
content=[
|
||||
{"type": "input_json_delta", "partial_json": 'Francisco"}', "index": 1}
|
||||
],
|
||||
tool_call_chunks=[
|
||||
{
|
||||
"name": None,
|
||||
"args": 'Francisco"}',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
],
|
||||
response_metadata={"model_provider": "bedrock_converse"},
|
||||
),
|
||||
]
|
||||
expected_contents: list[types.ContentBlock] = [
|
||||
{"type": "text", "text": "Looking ", "index": 0},
|
||||
{"type": "text", "text": "now.", "index": 0},
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": "",
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
{"name": None, "args": "", "id": None, "index": 1, "type": "tool_call_chunk"},
|
||||
{
|
||||
"name": None,
|
||||
"args": '{"loca',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
{
|
||||
"name": None,
|
||||
"args": 'tion": "San ',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
{
|
||||
"name": None,
|
||||
"args": 'Francisco"}',
|
||||
"id": None,
|
||||
"index": 1,
|
||||
"type": "tool_call_chunk",
|
||||
},
|
||||
]
|
||||
for chunk, expected in zip(chunks, expected_contents):
|
||||
assert chunk.content_blocks == [expected]
|
||||
|
||||
full: Optional[AIMessageChunk] = None
|
||||
for chunk in chunks:
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
|
||||
expected_content = [
|
||||
{"type": "text", "text": "Looking now.", "index": 0},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": "get_weather",
|
||||
"partial_json": '{"location": "San Francisco"}',
|
||||
"input": {},
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
]
|
||||
assert full.content == expected_content
|
||||
|
||||
expected_content_blocks = [
|
||||
{"type": "text", "text": "Looking now.", "index": 0},
|
||||
{
|
||||
"type": "tool_call_chunk",
|
||||
"name": "get_weather",
|
||||
"args": '{"location": "San Francisco"}',
|
||||
"id": "toolu_abc123",
|
||||
"index": 1,
|
||||
},
|
||||
]
|
||||
assert full.content_blocks == expected_content_blocks
|
||||
|
||||
|
||||
def test_convert_to_v1_from_converse_input() -> None:
|
||||
message = HumanMessage(
|
||||
[
|
||||
{"text": "foo"},
|
||||
{
|
||||
"document": {
|
||||
"format": "txt",
|
||||
"name": "doc_name_1",
|
||||
"source": {"text": "doc_text_1"},
|
||||
"context": "doc_context_1",
|
||||
"citations": {"enabled": True},
|
||||
},
|
||||
},
|
||||
{
|
||||
"document": {
|
||||
"format": "pdf",
|
||||
"name": "doc_name_2",
|
||||
"source": {"bytes": b"doc_text_2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"document": {
|
||||
"format": "txt",
|
||||
"name": "doc_name_3",
|
||||
"source": {"content": [{"text": "doc_text"}, {"text": "_3"}]},
|
||||
"context": "doc_context_3",
|
||||
},
|
||||
},
|
||||
{
|
||||
"image": {
|
||||
"format": "jpeg",
|
||||
"source": {"bytes": b"image_bytes"},
|
||||
}
|
||||
},
|
||||
{
|
||||
"document": {
|
||||
"format": "pdf",
|
||||
"name": "doc_name_4",
|
||||
"source": {
|
||||
"s3Location": {"uri": "s3://bla", "bucketOwner": "owner"}
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
expected: list[types.ContentBlock] = [
|
||||
{"type": "text", "text": "foo"},
|
||||
{
|
||||
"type": "text-plain",
|
||||
"mime_type": "text/plain",
|
||||
"text": "doc_text_1",
|
||||
"extras": {
|
||||
"name": "doc_name_1",
|
||||
"context": "doc_context_1",
|
||||
"citations": {"enabled": True},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"mime_type": "application/pdf",
|
||||
"base64": "ZG9jX3RleHRfMg==",
|
||||
"extras": {"name": "doc_name_2"},
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {
|
||||
"document": {
|
||||
"format": "txt",
|
||||
"name": "doc_name_3",
|
||||
"source": {"content": [{"text": "doc_text"}, {"text": "_3"}]},
|
||||
"context": "doc_context_3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base64": "aW1hZ2VfYnl0ZXM=",
|
||||
"mime_type": "image/jpeg",
|
||||
},
|
||||
{
|
||||
"type": "non_standard",
|
||||
"value": {
|
||||
"document": {
|
||||
"format": "pdf",
|
||||
"name": "doc_name_4",
|
||||
"source": {
|
||||
"s3Location": {"uri": "s3://bla", "bucketOwner": "owner"}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
assert message.content_blocks == expected
|
||||
@@ -0,0 +1,113 @@
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.block_translators.langchain_v0 import (
|
||||
_convert_legacy_v0_content_block_to_v1,
|
||||
)
|
||||
from tests.unit_tests.language_models.chat_models.test_base import (
|
||||
_content_blocks_equal_ignore_id,
|
||||
)
|
||||
|
||||
|
||||
def test_convert_to_v1_from_openai_input() -> None:
|
||||
message = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "Hello"},
|
||||
{
|
||||
"type": "image",
|
||||
"source_type": "url",
|
||||
"url": "https://example.com/image.png",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 data>",
|
||||
"mime_type": "image/png",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source_type": "url",
|
||||
"url": "<document url>",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 data>",
|
||||
"mime_type": "application/pdf",
|
||||
},
|
||||
{
|
||||
"type": "audio",
|
||||
"source_type": "base64",
|
||||
"data": "<base64 data>",
|
||||
"mime_type": "audio/mpeg",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source_type": "id",
|
||||
"id": "<file id>",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
expected: list[types.ContentBlock] = [
|
||||
{"type": "text", "text": "Hello"},
|
||||
{
|
||||
"type": "image",
|
||||
"url": "https://example.com/image.png",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base64": "<base64 data>",
|
||||
"mime_type": "image/png",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "<document url>",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"base64": "<base64 data>",
|
||||
"mime_type": "application/pdf",
|
||||
},
|
||||
{
|
||||
"type": "audio",
|
||||
"base64": "<base64 data>",
|
||||
"mime_type": "audio/mpeg",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file_id": "<file id>",
|
||||
},
|
||||
]
|
||||
|
||||
assert _content_blocks_equal_ignore_id(message.content_blocks, expected)
|
||||
|
||||
|
||||
def test_convert_with_extras_on_v0_block() -> None:
|
||||
"""Test that extras on old-style blocks are preserved in conversion.
|
||||
|
||||
Refer to `_extract_v0_extras` for details.
|
||||
"""
|
||||
block = {
|
||||
"type": "image",
|
||||
"source_type": "url",
|
||||
"url": "https://example.com/image.png",
|
||||
# extras follow
|
||||
"alt_text": "An example image",
|
||||
"caption": "Example caption",
|
||||
"name": "example_image",
|
||||
"description": None,
|
||||
"attribution": None,
|
||||
}
|
||||
expected_output = {
|
||||
"type": "image",
|
||||
"url": "https://example.com/image.png",
|
||||
"extras": {
|
||||
"alt_text": "An example image",
|
||||
"caption": "Example caption",
|
||||
"name": "example_image",
|
||||
# "description": None, # These are filtered out
|
||||
# "attribution": None,
|
||||
},
|
||||
}
|
||||
|
||||
assert _convert_legacy_v0_content_block_to_v1(block) == expected_output
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user