Compare commits

..

99 Commits

Author SHA1 Message Date
Chester Curme
c891a51608 fix(infra): permit pre-releases in release workflow 2025-09-18 13:44:54 -04:00
ccurme
82650ea7f1 release(core): 1.0.0a4 (#33011) 2025-09-18 13:01:02 -04:00
ccurme
13964efccf revert: fix(standard-tests): add filename to PDF file block (#33010)
Reverts langchain-ai/langchain#32989 in favor of
https://github.com/langchain-ai/langchain/pull/33009.
2025-09-18 12:48:00 -04:00
ccurme
f247270111 feat(core): (v1) standard content for AWS (#32969)
https://github.com/langchain-ai/langchain-aws/pull/643
2025-09-18 12:47:26 -04:00
ccurme
8be4adccd1 fix(core): (v1) trace PDFs in v0 standard format (#33009) 2025-09-18 11:45:12 -04:00
Mason Daugherty
b6bd507198 Merge branch 'master' into wip-v1.0 2025-09-18 11:44:06 -04:00
Mason Daugherty
3e0d7512ef core: update version.py 2025-09-18 10:58:51 -04:00
Mason Daugherty
53ed770849 Merge branch 'master' into wip-v1.0 2025-09-18 10:57:53 -04:00
Mason Daugherty
e2050e24ef release(core): 1.0.0a3 2025-09-18 10:57:22 -04:00
Mason Daugherty
59bb8bffd1 feat(core): make is_openai_data_block public and add filtering (#32991) 2025-09-17 11:41:41 -04:00
Mason Daugherty
16ec9bc535 fix(core): add back text to data content block check 2025-09-17 11:13:27 -04:00
Mason Daugherty
fda8a71e19 docs: further comments for clarity 2025-09-17 00:57:48 -04:00
Mason Daugherty
8f23bd109b fix: correct var name in comment 2025-09-17 00:39:01 -04:00
Mason Daugherty
ff632c1028 fix(standard-tests): add filename to PDF file block (#32989)
The standard PDF input test was creating file content blocks without a
filename field.

This caused a warning when the OpenAI block translator processed the
message for LangSmith tracing, since OpenAI requires filenames for file
inputs.
2025-09-17 00:35:49 -04:00
Mason Daugherty
3f71efc93c docs(core): update comments and docstrings 2025-09-17 00:35:30 -04:00
Mason Daugherty
cb3b5bf69b refactor(core): remove Pydantic v2 deprecation warning in tools base module
Replace deprecated `__fields__` attribute access with proper version-agnostic field retrieval using existing `get_fields`
2025-09-16 23:58:37 -04:00
Mason Daugherty
1c8a01ed03 cli: remove unnecessary changes & bump lock 2025-09-16 22:12:26 -04:00
Mason Daugherty
25333d3b45 Merge branch 'master' into wip-v1.0 2025-09-16 22:09:23 -04:00
Mason Daugherty
42830208f3 Merge branch 'master' into wip-v1.0 2025-09-16 22:04:42 -04:00
ccurme
653dc77c7e release(standard-tests): 1.0.0a1 (#32978) 2025-09-16 13:32:53 -04:00
Chester Curme
f6ab75ba8b Merge branch 'master' into wip-v1.0
# Conflicts:
#	libs/langchain/pyproject.toml
#	libs/langchain/uv.lock
2025-09-16 10:46:43 -04:00
Mason Daugherty
b6af0d228c residual lint 2025-09-16 10:31:42 -04:00
Mason Daugherty
5800721fd4 fix lint 2025-09-16 10:28:23 -04:00
Chester Curme
ebfb938a68 Merge branch 'master' into wip-v1.0 2025-09-16 10:20:24 -04:00
ccurme
1a3e9e06ee chore(langchain): (v1) drop 3.9 support (#32970)
Will need to add back optional deps when we have releases of them that
are compatible with langchain-core 1.0 alphas.
2025-09-16 10:14:23 -04:00
Mason Daugherty
c7ebbe5c8a Merge branch 'master' into wip-v1.0 2025-09-15 19:26:36 -04:00
Mason Daugherty
76dfb7fbe8 fix(cli): lint 2025-09-15 17:32:11 -04:00
Mason Daugherty
f25133c523 fix: version equality 2025-09-15 17:31:16 -04:00
Mason Daugherty
fc7a07d6f1 . 2025-09-15 17:27:20 -04:00
Mason Daugherty
e8ff6f4db6 Merge branch 'master' into wip-v1.0 2025-09-15 17:27:08 -04:00
ccurme
b88115f6fc feat(openai): (v1) support pdfs passed via url in standard format (#32876) 2025-09-12 10:44:00 -04:00
Mason Daugherty
67aa37b144 Merge branch 'master' into wip-v1.0 2025-09-11 23:21:37 -04:00
Mason Daugherty
9f14714367 fix: anthropic tests (stale cassette from max dynamic tokens) 2025-09-11 22:44:16 -04:00
Mason Daugherty
7cc9312979 fix: anthropic test since new dynamic max tokens 2025-09-11 17:53:30 -04:00
Mason Daugherty
387d0f4edf fix: lint 2025-09-11 17:52:46 -04:00
Mason Daugherty
207ea46813 Merge branch 'master' into wip-v1.0 2025-09-11 17:24:45 -04:00
Mason Daugherty
3ef1165c0c Merge branch 'master' into wip-v1.0 2025-09-10 22:07:40 -04:00
Mason Daugherty
ffee5155b4 fix lint 2025-09-10 22:03:45 -04:00
Mason Daugherty
5ef7d42bf6 refactor(core): remove example attribute from AIMessage and HumanMessage (#32565) 2025-09-10 22:01:25 -04:00
Mason Daugherty
750a3ffda6 Merge branch 'master' into wip-v1.0 2025-09-10 21:51:38 -04:00
Vadym Barda
8b1e25461b fix(core): add on_tool_error to _AstreamEventsCallbackHandler (#30709)
Fixes https://github.com/langchain-ai/langchain/issues/30708

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-09-10 21:34:05 -04:00
Mason Daugherty
ced9fc270f Merge branch 'master' into wip-v1.0 2025-09-10 21:32:03 -04:00
Mason Daugherty
311aa94d69 Merge branch 'master' into wip-v1.0 2025-09-10 20:57:17 -04:00
Mason Daugherty
cb8598b828 Merge branch 'master' into wip-v1.0 2025-09-10 20:51:00 -04:00
Eugene Yurtsev
fded6c6b13 chore(core): remove beta namespace and context api (#32850)
* Remove the `beta` namespace from langchain_core
* Remove the context API (never documented and makes it easier to create
memory leaks). This API was a beta API.

Co-authored-by: Sadra Barikbin <sadraqazvin1@yahoo.com>
2025-09-10 15:04:29 -04:00
Mason Daugherty
544b08d610 Merge branch 'master' into wip-v1.0 2025-09-10 11:10:48 -04:00
Mason Daugherty
a48ace52ad fix: lint 2025-09-09 18:59:38 -04:00
Mason Daugherty
20979d525c Merge branch 'master' into wip-v1.0 2025-09-09 15:01:51 -04:00
Mason Daugherty
188c0154b3 Merge branch 'master' into wip-v1.0 2025-09-08 17:08:57 -04:00
Christophe Bornet
3c189f0393 chore(langchain): fix deprecation warnings (#32379)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-09-08 17:03:06 -04:00
Mason Daugherty
8509efa6ad chore: remove erroneous pyversion specifiers 2025-09-08 15:03:08 -04:00
Mason Daugherty
b1a105f85f fix: huggingface lint 2025-09-08 14:59:38 -04:00
Mason Daugherty
9e54c5fa7f Merge branch 'master' into wip-v1.0 2025-09-08 14:44:28 -04:00
Mason Daugherty
0b8817c900 Merge branch 'master' into wip-v1.0 2025-09-08 14:43:58 -04:00
Christophe Bornet
083fbfb0d1 chore(core): add utf-8 encoding to Path read_text/write_text (#32784)
## Summary

This PR standardizes all text file I/O to use `UTF-8`. This eliminates
OS-specific defaults (e.g. Windows `cp1252`) and ensures consistent,
Unicode-safe behavior across platforms.

## Breaking changes

Users on systems with a default encoding which is not utf-8 may see
decoding errors from the following code paths:

* langchain_core.vectorstores.in_memroy.InMemoryVectorStore.load
* langchain_core.prompts.loading.load_prompt
* `from_template` in AIMessagePromptTemplate,
HumanMessagePromptTemplate, SystemMessagePromptTemplate

## Migration

Change the encoding of files that are encoded with a non utf-8 encoding to utf-8.
2025-09-08 11:27:15 -04:00
Christophe Bornet
f98f7359d3 refactor(core): refactors for python 3.10+ (#32787)
* Remove `sys.version_info` checks no longer needed
* Use `typing` instead of `typing_extensions` where applicable (NB: keep
using `TypedDict` from `typing_extensions` as [Pydantic requires
it](https://docs.pydantic.dev/2.3/usage/types/dicts_mapping/#typeddict))
2025-09-03 15:32:06 -04:00
ccurme
50b48fa1ff chore(openai): bump minimum core version (#32795) 2025-09-02 14:06:49 -04:00
Chester Curme
a54f4385f8 Merge branch 'master' into wip-v1.0
# Conflicts:
#	libs/langchain_v1/langchain/__init__.py
2025-09-02 13:17:00 -04:00
ccurme
98e4e7d043 Merge branch 'master' into wip-v1.0 2025-09-02 13:06:35 -04:00
ccurme
2cf5c52c13 release(core): 1.0.0a2 (#32792) 2025-09-02 12:55:52 -04:00
ccurme
bf41a75073 release(openai): 1.0.0a2 (#32790) 2025-09-02 12:22:57 -04:00
ccurme
e15c41233d feat(openai): (v1) update default output_version (#32674) 2025-09-02 12:12:41 -04:00
Mason Daugherty
25d5db88d5 fix: ci 2025-09-01 23:36:38 -05:00
Mason Daugherty
1237f94633 Merge branch 'wip-v1.0' of github.com:langchain-ai/langchain into wip-v1.0 2025-09-01 23:27:40 -05:00
Mason Daugherty
5c8837ea5a fix some imports 2025-09-01 23:27:37 -05:00
Mason Daugherty
820e355f53 Merge branch 'master' into wip-v1.0 2025-09-01 23:27:29 -05:00
Mason Daugherty
9a3ba71636 fix: version equality CI check 2025-09-01 23:24:04 -05:00
Mason Daugherty
00def6da72 rfc: remove unused TypeGuards 2025-09-01 23:13:18 -05:00
Mason Daugherty
4f8cced3b6 chore: move convert_to_openai_data_block and convert_to_openai_image_block from content.py to openai block translators 2025-09-01 23:08:47 -05:00
Mason Daugherty
365d7c414b nit: OpenAI docstrings 2025-09-01 20:30:56 -05:00
Mason Daugherty
a4874123a0 chore: move _convert_openai_format_to_data_block from langchain_v0 to openai 2025-09-01 19:39:15 -05:00
Mason Daugherty
a5f92fdd9a fix: update some docstrings and typing 2025-09-01 19:25:08 -05:00
Mason Daugherty
431e6d6211 chore(standard-tests): drop python 3.9 (#32772) 2025-08-31 18:23:10 -05:00
Mason Daugherty
0f1afa178e chore(text-splitters): drop python 3.9 support (#32771) 2025-08-31 18:13:35 -05:00
Mason Daugherty
830d1a207c Merge branch 'master' into wip-v1.0 2025-08-31 18:01:24 -05:00
Mason Daugherty
b494a3c57b chore(cli): drop python 3.9 support (#32761) 2025-08-30 13:25:33 -05:00
Mason Daugherty
f088fac492 Merge branch 'master' into wip-v1.0 2025-08-30 14:21:37 -04:00
Mason Daugherty
925ad65df9 fix(core): typo in content.py 2025-08-28 15:17:51 -04:00
Mason Daugherty
e09d90b627 Merge branch 'master' into wip-v1.0 2025-08-28 14:11:24 -04:00
ccurme
ddde1eff68 fix: openai, anthropic (v1) fix core lower bound (#32724) 2025-08-27 14:36:10 -04:00
ccurme
9b576440ed release: anthropic, openai 1.0.0a1 (#32723) 2025-08-27 14:12:28 -04:00
ccurme
a80fa1b25f chore(infra): drop anthropic from core test matrix (#32717) 2025-08-27 13:11:44 -04:00
ccurme
72b66fcca5 release(core): 1.0.0a1 (#32715) 2025-08-27 11:57:07 -04:00
ccurme
a47d993ddd release(core): 1.0.0dev0 (#32713) 2025-08-27 11:05:39 -04:00
ccurme
cb4705dfc0 chore: (v1) drop support for python 3.9 (#32712)
EOL in October

Will update ruff / formatting closer to 1.0 release to minimize merge
conflicts on branch
2025-08-27 10:42:49 -04:00
ccurme
9a9263a2dd fix(langchain): (v1) delete unused chains (#32711)
Merge conflict was not resolved correctly
2025-08-27 10:17:14 -04:00
Chester Curme
e4b69db4cf Merge branch 'master' into wip-v1.0
# Conflicts:
#	libs/langchain_v1/langchain/chains/documents/map_reduce.py
#	libs/langchain_v1/langchain/chains/documents/stuff.py
2025-08-27 09:37:21 -04:00
Mason Daugherty
242881562b feat: standard content, IDs, translators, & normalization (#32569) 2025-08-27 09:31:12 -04:00
Mason Daugherty
a2322f68ba Merge branch 'master' into wip-v1.0 2025-08-26 15:51:57 -04:00
ccurme
7e9ae5df60 feat(openai): (v1) delete bind_functions and remove tool_calls from additional_kwargs (#32669) 2025-08-25 14:22:31 -04:00
Chester Curme
7a108618ae Merge branch 'master' into wip-v1.0 2025-08-25 09:39:39 -04:00
ccurme
6f058e7b9b fix(core): (v1) update BaseChatModel return type to AIMessage (#32626) 2025-08-21 14:02:24 -04:00
ccurme
dbc5a3b718 fix(anthropic): update cassette for streaming benchmark (#32609) 2025-08-19 11:18:36 -04:00
Mason Daugherty
f0f1e28473 Merge branch 'master' of github.com:langchain-ai/langchain into wip-v1.0 2025-08-18 23:30:10 -04:00
Mason Daugherty
8bd2403518 fix: increase max_tokens limit to 64000 re: Anthropic dynamic tokens 2025-08-15 15:34:54 -04:00
Mason Daugherty
4dd9110424 Merge branch 'master' into wip-v1.0 2025-08-15 15:32:21 -04:00
Mohammad Mohtashim
174e685139 feat(anthropic): dynamic mapping of Max Tokens for Anthropic (#31946)
- **Description:** Dynamic mapping of `max_tokens` as per the choosen
anthropic model.
- **Issue:** Fixes #31605

@ccurme

---------

Co-authored-by: Caspar Broekhuizen <caspar@langchain.dev>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-08-15 11:33:51 -07:00
Mason Daugherty
9721684501 Merge branch 'master' into wip-v1.0 2025-08-15 14:06:34 -04:00
Mason Daugherty
a4e135b508 fix: use .get() on image URL in ImagePromptValue.to_string() 2025-08-15 13:57:50 -04:00
430 changed files with 29011 additions and 31884 deletions

View File

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

View File

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

View File

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

View File

@@ -1,41 +0,0 @@
# PR title labeler config
#
# Labels PRs based on conventional commit patterns in titles
#
# Format: type(scope): description or type!: description (breaking)
add-missing-labels: true
clear-prexisting: false
include-commits: false
include-title: true
label-for-breaking-changes: breaking
label-mapping:
documentation: ["docs"]
feature: ["feat"]
fix: ["fix"]
infra: ["build", "ci", "chore"]
integration:
[
"anthropic",
"chroma",
"deepseek",
"exa",
"fireworks",
"groq",
"huggingface",
"mistralai",
"nomic",
"ollama",
"openai",
"perplexity",
"prompty",
"qdrant",
"xai",
]
linting: ["style"]
performance: ["perf"]
refactor: ["refactor"]
release: ["release"]
revert: ["revert"]
tests: ["test"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

25
.readthedocs.yaml Normal file
View File

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

View File

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

325
AGENTS.md
View File

@@ -1,325 +0,0 @@
# Global Development Guidelines for LangChain Projects
## Core Development Principles
### 1. Maintain Stable Public Interfaces ⚠️ CRITICAL
**Always attempt to preserve function signatures, argument positions, and names for exported/public methods.**
**Bad - Breaking Change:**
```python
def get_user(id, verbose=False): # Changed from `user_id`
pass
```
**Good - Stable Interface:**
```python
def get_user(user_id: str, verbose: bool = False) -> User:
"""Retrieve user by ID with optional verbose output."""
pass
```
**Before making ANY changes to public APIs:**
- Check if the function/class is exported in `__init__.py`
- Look for existing usage patterns in tests and examples
- Use keyword-only arguments for new parameters: `*, new_param: str = "default"`
- Mark experimental features clearly with docstring warnings (using reStructuredText, like `.. warning::`)
🧠 *Ask yourself:* "Would this change break someone's code if they used it last week?"
### 2. Code Quality Standards
**All Python code MUST include type hints and return types.**
**Bad:**
```python
def p(u, d):
return [x for x in u if x not in d]
```
**Good:**
```python
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
"""Filter out users that are not in the known users set.
Args:
users: List of user identifiers to filter.
known_users: Set of known/valid user identifiers.
Returns:
List of users that are not in the known_users set.
"""
return [user for user in users if user not in known_users]
```
**Style Requirements:**
- Use descriptive, **self-explanatory variable names**. Avoid overly short or cryptic identifiers.
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
- Avoid unnecessary abstraction or premature optimization
- Follow existing patterns in the codebase you're modifying
### 3. Testing Requirements
**Every new feature or bugfix MUST be covered by unit tests.**
**Test Organization:**
- Unit tests: `tests/unit_tests/` (no network calls allowed)
- Integration tests: `tests/integration_tests/` (network calls permitted)
- Use `pytest` as the testing framework
**Test Quality Checklist:**
- [ ] Tests fail when your new logic is broken
- [ ] Happy path is covered
- [ ] Edge cases and error conditions are tested
- [ ] Use fixtures/mocks for external dependencies
- [ ] Tests are deterministic (no flaky tests)
Checklist questions:
- [ ] Does the test suite fail if your new logic is broken?
- [ ] Are all expected behaviors exercised (happy path, invalid input, etc)?
- [ ] Do tests use fixtures or mocks where needed?
```python
def test_filter_unknown_users():
"""Test filtering unknown users from a list."""
users = ["alice", "bob", "charlie"]
known_users = {"alice", "bob"}
result = filter_unknown_users(users, known_users)
assert result == ["charlie"]
assert len(result) == 1
```
### 4. Security and Risk Assessment
**Security Checklist:**
- No `eval()`, `exec()`, or `pickle` on user-controlled input
- Proper exception handling (no bare `except:`) and use a `msg` variable for error messages
- Remove unreachable/commented code before committing
- Race conditions or resource leaks (file handles, sockets, threads).
- Ensure proper resource cleanup (file handles, connections)
**Bad:**
```python
def load_config(path):
with open(path) as f:
return eval(f.read()) # ⚠️ Never eval config
```
**Good:**
```python
import json
def load_config(path: str) -> dict:
with open(path) as f:
return json.load(f)
```
### 5. Documentation Standards
**Use Google-style docstrings with Args section for all public functions.**
**Insufficient Documentation:**
```python
def send_email(to, msg):
"""Send an email to a recipient."""
```
**Complete Documentation:**
```python
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
"""
Send an email to a recipient with specified priority.
Args:
to: The email address of the recipient.
msg: The message body to send.
priority: Email priority level (``'low'``, ``'normal'``, ``'high'``).
Returns:
True if email was sent successfully, False otherwise.
Raises:
InvalidEmailError: If the email address format is invalid.
SMTPConnectionError: If unable to connect to email server.
"""
```
**Documentation Guidelines:**
- Types go in function signatures, NOT in docstrings
- Focus on "why" rather than "what" in descriptions
- Document all parameters, return values, and exceptions
- Keep descriptions concise but clear
- Use reStructuredText for docstrings to enable rich formatting
📌 *Tip:* Keep descriptions concise but clear. Only document return values if non-obvious.
### 6. Architectural Improvements
**When you encounter code that could be improved, suggest better designs:**
**Poor Design:**
```python
def process_data(data, db_conn, email_client, logger):
# Function doing too many things
validated = validate_data(data)
result = db_conn.save(validated)
email_client.send_notification(result)
logger.log(f"Processed {len(data)} items")
return result
```
**Better Design:**
```python
@dataclass
class ProcessingResult:
"""Result of data processing operation."""
items_processed: int
success: bool
errors: List[str] = field(default_factory=list)
class DataProcessor:
"""Handles data validation, storage, and notification."""
def __init__(self, db_conn: Database, email_client: EmailClient):
self.db = db_conn
self.email = email_client
def process(self, data: List[dict]) -> ProcessingResult:
"""Process and store data with notifications."""
validated = self._validate_data(data)
result = self.db.save(validated)
self._notify_completion(result)
return result
```
**Design Improvement Areas:**
If there's a **cleaner**, **more scalable**, or **simpler** design, highlight it and suggest improvements that would:
- Reduce code duplication through shared utilities
- Make unit testing easier
- Improve separation of concerns (single responsibility)
- Make unit testing easier through dependency injection
- Add clarity without adding complexity
- Prefer dataclasses for structured data
## Development Tools & Commands
### Package Management
```bash
# Add package
uv add package-name
# Sync project dependencies
uv sync
uv lock
```
### Testing
```bash
# Run unit tests (no network)
make test
# Don't run integration tests, as API keys must be set
# Run specific test file
uv run --group test pytest tests/unit_tests/test_specific.py
```
### Code Quality
```bash
# Lint code
make lint
# Format code
make format
# Type checking
uv run --group lint mypy .
```
### Dependency Management Patterns
**Local Development Dependencies:**
```toml
[tool.uv.sources]
langchain-core = { path = "../core", editable = true }
langchain-tests = { path = "../standard-tests", editable = true }
```
**For tools, use the `@tool` decorator from `langchain_core.tools`:**
```python
from langchain_core.tools import tool
@tool
def search_database(query: str) -> str:
"""Search the database for relevant information.
Args:
query: The search query string.
"""
# Implementation here
return results
```
## Commit Standards
**Use Conventional Commits format for PR titles:**
- `feat(core): add multi-tenant support`
- `fix(cli): resolve flag parsing error`
- `docs: update API usage examples`
- `docs(openai): update API usage examples`
## Framework-Specific Guidelines
- Follow the existing patterns in `langchain-core` for base abstractions
- Use `langchain_core.callbacks` for execution tracking
- Implement proper streaming support where applicable
- Avoid deprecated components like legacy `LLMChain`
### Partner Integrations
- Follow the established patterns in existing partner libraries
- Implement standard interfaces (`BaseChatModel`, `BaseEmbeddings`, etc.)
- Include comprehensive integration tests
- Document API key requirements and authentication
---
## Quick Reference Checklist
Before submitting code changes:
- [ ] **Breaking Changes**: Verified no public API changes
- [ ] **Type Hints**: All functions have complete type annotations
- [ ] **Tests**: New functionality is fully tested
- [ ] **Security**: No dangerous patterns (eval, silent failures, etc.)
- [ ] **Documentation**: Google-style docstrings for public functions
- [ ] **Code Quality**: `make lint` and `make format` pass
- [ ] **Architecture**: Suggested improvements where applicable
- [ ] **Commit Message**: Follows Conventional Commits format

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)"
]
},
{

View File

@@ -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)"
]
},
{

View File

@@ -1191,40 +1191,6 @@
"response.content"
]
},
{
"cell_type": "markdown",
"id": "74247a07-b153-444f-9c56-77659aeefc88",
"metadata": {},
"source": [
"## Context management\n",
"\n",
"Anthropic supports a context editing feature that will automatically manage the model's context window (e.g., by clearing tool results).\n",
"\n",
"See [Anthropic documentation](https://docs.claude.com/en/docs/build-with-claude/context-editing) for details and configuration options.\n",
"\n",
":::info\n",
"Requires ``langchain-anthropic>=0.3.21``\n",
":::"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cbb79c5d-37b5-4212-b36f-f27366192cf9",
"metadata": {},
"outputs": [],
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"llm = ChatAnthropic(\n",
" model=\"claude-sonnet-4-5-20250929\",\n",
" betas=[\"context-management-2025-06-27\"],\n",
" context_management={\"edits\": [{\"type\": \"clear_tool_uses_20250919\"}]},\n",
")\n",
"llm_with_tools = llm.bind_tools([{\"type\": \"web_search_20250305\", \"name\": \"web_search\"}])\n",
"response = llm_with_tools.invoke(\"Search for recent developments in AI\")"
]
},
{
"cell_type": "markdown",
"id": "cbfec7a9-d9df-4d12-844e-d922456dd9bf",
@@ -1491,38 +1457,6 @@
"</details>"
]
},
{
"cell_type": "markdown",
"id": "29405da2-d2ef-415c-b674-6e29073cd05e",
"metadata": {},
"source": [
"### Memory tool\n",
"\n",
"Claude supports a memory tool for client-side storage and retrieval of context across conversational threads. See docs [here](https://docs.claude.com/en/docs/agents-and-tools/tool-use/memory-tool) for details.\n",
"\n",
":::info\n",
"Requires ``langchain-anthropic>=0.3.21``\n",
":::"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bbd76eaa-041f-4fb8-8346-ca8fe0001c01",
"metadata": {},
"outputs": [],
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"llm = ChatAnthropic(\n",
" model=\"claude-sonnet-4-5-20250929\",\n",
" betas=[\"context-management-2025-06-27\"],\n",
")\n",
"llm_with_tools = llm.bind_tools([{\"type\": \"memory_20250818\", \"name\": \"memory\"}])\n",
"\n",
"response = llm_with_tools.invoke(\"What are my interests?\")"
]
},
{
"cell_type": "markdown",
"id": "040f381a-1768-479a-9a5e-aa2d7d77e0d5",
@@ -1593,7 +1527,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"id": "30a0af36-2327-4b1d-9ba5-e47cb72db0be",
"metadata": {},
"outputs": [
@@ -1629,7 +1563,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"
]
},

View File

@@ -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=\"|\")"
]
},
{

View File

@@ -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=\"\")"
]
},
{

View File

@@ -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"
]
},
{

View File

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

View File

@@ -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=\"|\")"
]
},

View File

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

3
libs/cli/CONTRIBUTING.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

250
libs/cli/uv.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
"""Some **beta** features that are not yet ready for production."""

View File

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

View File

@@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",
}

View File

@@ -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,
},
}
)

View File

@@ -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:

View File

@@ -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
# 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:

View File

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

View File

@@ -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"

View File

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

View File

@@ -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,31 @@ 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,
CodeInterpreterCall,
CodeInterpreterOutput,
CodeInterpreterResult,
ContentBlock,
DataContentBlock,
FileContentBlock,
ImageContentBlock,
InvalidToolCall,
NonStandardAnnotation,
NonStandardContentBlock,
PlainTextContentBlock,
ReasoningContentBlock,
TextContentBlock,
VideoContentBlock,
WebSearchCall,
WebSearchResult,
is_data_content_block,
)
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
@@ -42,7 +64,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 +84,52 @@ if TYPE_CHECKING:
)
__all__ = (
"LC_AUTO_PREFIX",
"LC_ID_PREFIX",
"AIMessage",
"AIMessageChunk",
"Annotation",
"AnyMessage",
"AudioContentBlock",
"BaseMessage",
"BaseMessageChunk",
"ChatMessage",
"ChatMessageChunk",
"Citation",
"CodeInterpreterCall",
"CodeInterpreterOutput",
"CodeInterpreterResult",
"ContentBlock",
"DataContentBlock",
"FileContentBlock",
"FunctionMessage",
"FunctionMessageChunk",
"HumanMessage",
"HumanMessageChunk",
"ImageContentBlock",
"InvalidToolCall",
"MessageLikeRepresentation",
"NonStandardAnnotation",
"NonStandardContentBlock",
"PlainTextContentBlock",
"ReasoningContentBlock",
"RemoveMessage",
"SystemMessage",
"SystemMessageChunk",
"TextContentBlock",
"ToolCall",
"ToolCallChunk",
"ToolMessage",
"ToolMessageChunk",
"VideoContentBlock",
"WebSearchCall",
"WebSearchResult",
"_message_from_dict",
"convert_to_messages",
"convert_to_openai_data_block",
"convert_to_openai_image_block",
"convert_to_openai_messages",
"ensure_id",
"filter_messages",
"get_buffer_string",
"is_data_content_block",
@@ -103,35 +145,53 @@ __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",
"CodeInterpreterCall": "content",
"CodeInterpreterOutput": "content",
"CodeInterpreterResult": "content",
"DataContentBlock": "content",
"FileContentBlock": "content",
"FunctionMessage": "function",
"FunctionMessageChunk": "function",
"HumanMessage": "human",
"HumanMessageChunk": "human",
"NonStandardAnnotation": "content",
"NonStandardContentBlock": "content",
"PlainTextContentBlock": "content",
"ReasoningContentBlock": "content",
"RemoveMessage": "modifier",
"SystemMessage": "system",
"SystemMessageChunk": "system",
"WebSearchCall": "content",
"WebSearchResult": "content",
"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",

View File

@@ -3,48 +3,43 @@
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,
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.
Does *not* need to sum to full input token count. Does *not* need to have all keys.
Example:
.. code-block:: python
{
@@ -71,7 +66,6 @@ class InputTokenDetails(TypedDict, total=False):
Since there was a cache hit, the tokens were read from the cache. More precisely,
the model state given these tokens was read from the cache.
"""
@@ -81,6 +75,7 @@ class OutputTokenDetails(TypedDict, total=False):
Does *not* need to sum to full output token count. Does *not* need to have all keys.
Example:
.. code-block:: python
{
@@ -99,7 +94,6 @@ class OutputTokenDetails(TypedDict, total=False):
Tokens generated by the model in a chain of thought process (i.e. by OpenAI's o1
models) that are not returned as part of model output.
"""
@@ -109,6 +103,7 @@ class UsageMetadata(TypedDict):
This is a standard representation of token usage that is consistent across models.
Example:
.. code-block:: python
{
@@ -147,7 +142,6 @@ class UsageMetadata(TypedDict):
"""Breakdown of output token counts.
Does *not* need to sum to full output token count. Does *not* need to have all keys.
"""
@@ -159,14 +153,6 @@ class AIMessage(BaseMessage):
This message represents the output of the model and consists of both
the raw output as returned by the model together standardized fields
(e.g., tool calls, usage metadata) added by the LangChain framework.
"""
example: bool = False
"""Use to denote that a message is part of an example conversation.
At the moment, this is ignored by most models. Usage is discouraged.
"""
tool_calls: list[ToolCall] = []
@@ -177,24 +163,47 @@ class AIMessage(BaseMessage):
"""If provided, usage metadata for a message, such as token counts.
This is a standard representation of token usage that is consistent across models.
"""
type: Literal["ai"] = "ai"
"""The type of the message (used for deserialization). Defaults to ``'ai'``."""
"""The type of the message (used for deserialization). Defaults to "ai"."""
@overload
def __init__(
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None:
"""Initialize ``AIMessage``.
) -> None: ...
Args:
content: The content of the message.
kwargs: Additional arguments to pass to the parent class.
"""
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:
# 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 +213,51 @@ 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 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)
return blocks
# TODO: remove this logic if possible, reducing breaking nature of changes
@model_validator(mode="before")
@classmethod
@@ -232,7 +286,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"):
@@ -259,7 +315,6 @@ class AIMessage(BaseMessage):
Returns:
A pretty representation of the message.
"""
base = super().pretty_repr(html=html)
lines = []
@@ -299,14 +354,18 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
# non-chunk variant.
type: Literal["AIMessageChunk"] = "AIMessageChunk" # type: ignore[assignment]
"""The type of the message (used for deserialization).
Defaults to ``AIMessageChunk``.
"""
Defaults to "AIMessageChunk"."""
tool_call_chunks: list[ToolCallChunk] = []
"""If provided, tool call chunks associated with the message."""
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,15 +374,57 @@ 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)
return blocks
@model_validator(mode="after")
def init_tool_calls(self) -> Self:
"""Initialize tool calls from tool call chunks.
Returns:
The values with tool calls initialized.
Raises:
ValueError: If the tool call chunks are malformed.
This ``AIMessageChunk``.
"""
if not self.tool_call_chunks:
if self.tool_calls:
@@ -379,10 +480,45 @@ 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
@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 +537,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 +575,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,
)
@@ -534,9 +677,9 @@ def add_usage(
def subtract_usage(
left: Optional[UsageMetadata], right: Optional[UsageMetadata]
) -> UsageMetadata:
"""Recursively subtract two ``UsageMetadata`` objects.
"""Recursively subtract two UsageMetadata objects.
Token counts cannot be negative so the actual operation is ``max(left - right, 0)``.
Token counts cannot be negative so the actual operation is max(left - right, 0).
Example:
.. code-block:: python

View File

@@ -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,56 @@ if TYPE_CHECKING:
from langchain_core.prompts.chat import ChatPromptTemplate
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 ChatModels.
"""
content: Union[str, list[Union[str, dict]]]
@@ -31,18 +80,17 @@ class BaseMessage(Serializable):
For example, for a message from an AI, this could include tool calls as
encoded by the model provider.
"""
response_metadata: dict = Field(default_factory=dict)
"""Examples: response headers, logprobs, token counts, model name."""
"""Response metadata. For example: response headers, logprobs, token counts, model
name."""
type: str
"""The type of the message. Must be a string that is unique to the message type.
The purpose of this field is to allow for easy identification of the message type
when deserializing messages.
"""
name: Optional[str] = None
@@ -52,35 +100,46 @@ class BaseMessage(Serializable):
Usage of this field is optional, and whether it's used or not is up to the
model implementation.
"""
id: Optional[str] = Field(default=None, coerce_numbers_to_str=True)
"""An optional unique identifier for the message.
This should ideally be provided by the provider/model which created the message.
"""
"""An optional unique identifier for the message. This should ideally be
provided by the provider/model which created the message."""
model_config = ConfigDict(
extra="allow",
)
@overload
def __init__(
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None:
"""Initialize ``BaseMessage``.
) -> None: ...
Args:
content: The string contents of 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=content_blocks, **kwargs)
else:
super().__init__(content=content, **kwargs)
@classmethod
def is_lc_serializable(cls) -> bool:
"""``BaseMessage`` is serializable.
"""BaseMessage is serializable.
Returns:
True
@@ -96,26 +155,100 @@ 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"""Return ``content`` as a list of standardized :class:`~langchain_core.messages.content.ContentBlock`\s.
.. important::
To use this property correctly, the corresponding ``ChatModel`` must support
``message_version='v1'`` or higher (and it must be set):
.. code-block:: python
from langchain.chat_models import init_chat_model
llm = init_chat_model("...", message_version="v1")
# or
from langchain-openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o", message_version="v1")
Otherwise, the property will perform best-effort parsing to standard types,
though some content may be misinterpreted.
.. versionadded:: 1.0.0
""" # noqa: E501
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.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] = []
# First pass: convert to standard blocks
content = (
[self.content]
if isinstance(self.content, str) and self.content
else self.content
)
for item in content:
if isinstance(item, str):
blocks.append({"type": "text", "text": item})
elif isinstance(item, dict):
item_type = item.get("type")
if item_type not in types.KNOWN_BLOCK_TYPES:
blocks.append({"type": "non_standard", "value": item})
else:
blocks.append(cast("types.ContentBlock", item))
# Subsequent passes: attempt to unpack non-standard blocks
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_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.
@@ -144,7 +277,6 @@ class BaseMessage(Serializable):
Returns:
A pretty representation of the message.
"""
title = get_msg_title_repr(self.type.title() + " Message", bold=html)
# TODO: handle non-string content.
@@ -164,14 +296,15 @@ def merge_content(
"""Merge multiple message contents.
Args:
first_content: The first ``content``. Can be a string or a list.
contents: The other ``content``s. Can be a string or a list.
first_content: The first content. Can be a string or a list.
contents: The other contents. Can be a string or a list.
Returns:
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 +323,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
@@ -217,10 +352,9 @@ class BaseMessageChunk(BaseMessage):
For example,
``AIMessageChunk(content="Hello") + AIMessageChunk(content=" World")``
will give ``AIMessageChunk(content="Hello World")``
`AIMessageChunk(content="Hello") + AIMessageChunk(content=" World")`
will give `AIMessageChunk(content="Hello World")`
"""
if isinstance(other, BaseMessageChunk):
# If both are (subclasses of) BaseMessageChunk,
@@ -268,9 +402,8 @@ def message_to_dict(message: BaseMessage) -> dict:
message: Message to convert.
Returns:
Message as a dict. The dict will have a ``type`` key with the message type
and a ``data`` key with the message data as a dict.
Message as a dict. The dict will have a "type" key with the message type
and a "data" key with the message data as a dict.
"""
return {"type": message.type, "data": message.model_dump()}
@@ -279,11 +412,10 @@ def messages_to_dict(messages: Sequence[BaseMessage]) -> list[dict]:
"""Convert a sequence of Messages to a list of dictionaries.
Args:
messages: Sequence of messages (as ``BaseMessage``s) to convert.
messages: Sequence of messages (as BaseMessages) to convert.
Returns:
List of messages as dicts.
"""
return [message_to_dict(m) for m in messages]
@@ -297,7 +429,6 @@ def get_msg_title_repr(title: str, *, bold: bool = False) -> str:
Returns:
The title representation.
"""
padded = " " + title + " "
sep_len = (80 - len(padded)) // 2

View File

@@ -0,0 +1,91 @@
"""Derivations of standard content blocks from provider content."""
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]]]] = {}
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.
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.
"""
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.ollama import ( # noqa: PLC0415
_register_ollama_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_ollama_translator()
_register_openai_translator()
_register_translators()

View File

@@ -0,0 +1,455 @@
"""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]:
"""Attempt to unpack non-standard blocks."""
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)
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
elif block_type == "server_tool_use":
if block.get("name") == "web_search":
web_search_call: types.WebSearchCall = {"type": "web_search_call"}
if query := block.get("input", {}).get("query"):
web_search_call["query"] = query
elif block.get("input") == {} and "partial_json" in block:
try:
input_ = json.loads(block["partial_json"])
if isinstance(input_, dict) and "query" in input_:
web_search_call["query"] = input_["query"]
except json.JSONDecodeError:
pass
if "id" in block:
web_search_call["id"] = block["id"]
if "index" in block:
web_search_call["index"] = block["index"]
known_fields = {"type", "name", "input", "id", "index"}
for key, value in block.items():
if key not in known_fields:
if "extras" not in web_search_call:
web_search_call["extras"] = {}
web_search_call["extras"][key] = value
yield web_search_call
elif block.get("name") == "code_execution":
code_interpreter_call: types.CodeInterpreterCall = {
"type": "code_interpreter_call"
}
if code := block.get("input", {}).get("code"):
code_interpreter_call["code"] = code
elif block.get("input") == {} and "partial_json" in block:
try:
input_ = json.loads(block["partial_json"])
if isinstance(input_, dict) and "code" in input_:
code_interpreter_call["code"] = input_["code"]
except json.JSONDecodeError:
pass
if "id" in block:
code_interpreter_call["id"] = block["id"]
if "index" in block:
code_interpreter_call["index"] = block["index"]
known_fields = {"type", "name", "input", "id", "index"}
for key, value in block.items():
if key not in known_fields:
if "extras" not in code_interpreter_call:
code_interpreter_call["extras"] = {}
code_interpreter_call["extras"][key] = value
yield code_interpreter_call
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
elif block_type == "web_search_tool_result":
web_search_result: types.WebSearchResult = {"type": "web_search_result"}
if "tool_use_id" in block:
web_search_result["id"] = block["tool_use_id"]
if "index" in block:
web_search_result["index"] = block["index"]
if web_search_result_content := block.get("content", []):
if "extras" not in web_search_result:
web_search_result["extras"] = {}
urls = []
extra_content = []
for result_content in web_search_result_content:
if isinstance(result_content, dict):
if "url" in result_content:
urls.append(result_content["url"])
extra_content.append(result_content)
web_search_result["extras"]["content"] = extra_content
if urls:
web_search_result["urls"] = urls
yield web_search_result
elif block_type == "code_execution_tool_result":
code_interpreter_result: types.CodeInterpreterResult = {
"type": "code_interpreter_result",
"output": [],
}
if "tool_use_id" in block:
code_interpreter_result["id"] = block["tool_use_id"]
if "index" in block:
code_interpreter_result["index"] = block["index"]
code_interpreter_output: types.CodeInterpreterOutput = {
"type": "code_interpreter_output"
}
code_execution_content = block.get("content", {})
if code_execution_content.get("type") == "code_execution_result":
if "return_code" in code_execution_content:
code_interpreter_output["return_code"] = code_execution_content[
"return_code"
]
if "stdout" in code_execution_content:
code_interpreter_output["stdout"] = code_execution_content[
"stdout"
]
if stderr := code_execution_content.get("stderr"):
code_interpreter_output["stderr"] = stderr
if (
output := code_interpreter_output.get("content")
) and isinstance(output, list):
if "extras" not in code_interpreter_result:
code_interpreter_result["extras"] = {}
code_interpreter_result["extras"]["content"] = output
for output_block in output:
if "file_id" in output_block:
if "file_ids" not in code_interpreter_output:
code_interpreter_output["file_ids"] = []
code_interpreter_output["file_ids"].append(
output_block["file_id"]
)
code_interpreter_result["output"].append(code_interpreter_output)
elif (
code_execution_content.get("type")
== "code_execution_tool_result_error"
):
if "extras" not in code_interpreter_result:
code_interpreter_result["extras"] = {}
code_interpreter_result["extras"]["error_code"] = (
code_execution_content.get("error_code")
)
yield code_interpreter_result
else:
new_block = {"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()

View File

@@ -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()

View File

@@ -0,0 +1,283 @@
"""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]:
"""Attempt to unpack non-standard blocks."""
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()

View File

@@ -0,0 +1,47 @@
"""Derivations of standard content blocks from Google (GenAI) 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 Google (GenAI) content."""
global WARNED # noqa: PLW0603
if not WARNED:
warning_message = (
"Content block standardization is not yet fully supported for Google GenAI."
)
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 chunk with Google (GenAI) content."""
global WARNED # noqa: PLW0603
if not WARNED:
warning_message = (
"Content block standardization is not yet fully supported for Google GenAI."
)
warnings.warn(warning_message, stacklevel=2)
WARNED = True
raise NotImplementedError
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()

View File

@@ -0,0 +1,49 @@
"""Derivations of standard content blocks from Google (VertexAI) 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 Google (VertexAI) content."""
global WARNED # noqa: PLW0603
if not WARNED:
warning_message = (
"Content block standardization is not yet fully supported for Google "
"VertexAI."
)
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 chunk with Google (VertexAI) content."""
global WARNED # noqa: PLW0603
if not WARNED:
warning_message = (
"Content block standardization is not yet fully supported for Google "
"VertexAI."
)
warnings.warn(warning_message, stacklevel=2)
WARNED = True
raise NotImplementedError
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()

View 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()

View File

@@ -0,0 +1,164 @@
"""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(
blocks: list[types.ContentBlock],
) -> list[types.ContentBlock]:
"""Convert v0 multimodal blocks to v1 format.
Processes ``'non_standard'`` blocks that might be v0 format and converts them
to proper v1 ``ContentBlock``.
Args:
blocks: List of content blocks to process.
Returns:
Updated list with v0 blocks converted to v1 format.
"""
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 blocks
]
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:
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."""
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":
known_keys = {"type", "source_type", "url", "mime_type"}
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_block = types.ImageContentBlock(type="image", url=block["url"])
if block.get("mime_type"):
v1_block["mime_type"] = block["mime_type"]
for key, value in extras.items():
if value is not None:
v1_block["extras"] = {}
v1_block["extras"][key] = value
return v1_block
if source_type == "base64":
known_keys = {"type", "source_type", "data", "mime_type"}
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_block = types.ImageContentBlock(type="image", base64=block["data"])
if block.get("mime_type"):
v1_block["mime_type"] = block["mime_type"]
for key, value in extras.items():
if value is not None:
v1_block["extras"] = {}
v1_block["extras"][key] = value
return v1_block
if source_type == "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_block = types.ImageContentBlock(type="image", file_id=block["id"])
for key, value in extras.items():
if value is not None:
v1_block["extras"] = {}
v1_block["extras"][key] = value
return v1_block
elif block.get("type") == "audio":
source_type = block.get("source_type")
if source_type == "url":
known_keys = {"type", "source_type", "url", "mime_type"}
extras = _extract_v0_extras(block, known_keys)
return types.create_audio_block(
url=block["url"], mime_type=block.get("mime_type"), **extras
)
if source_type == "base64":
known_keys = {"type", "source_type", "data", "mime_type"}
extras = _extract_v0_extras(block, known_keys)
return types.create_audio_block(
base64=block["data"], mime_type=block.get("mime_type"), **extras
)
if source_type == "id":
known_keys = {"type", "source_type", "id"}
extras = _extract_v0_extras(block, known_keys)
return types.create_audio_block(file_id=block["id"], **extras)
elif block.get("type") == "file":
source_type = block.get("source_type")
if source_type == "url":
known_keys = {"type", "source_type", "url", "mime_type"}
extras = _extract_v0_extras(block, known_keys)
return types.create_file_block(
url=block["url"], mime_type=block.get("mime_type"), **extras
)
if source_type == "base64":
known_keys = {"type", "source_type", "data", "mime_type"}
extras = _extract_v0_extras(block, known_keys)
return types.create_file_block(
base64=block["data"], mime_type=block.get("mime_type"), **extras
)
if source_type == "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":
known_keys = {"type", "source_type", "url", "mime_type"}
extras = _extract_v0_extras(block, known_keys)
return types.create_plaintext_block(
# In v0, URL points to the text file content
text=block["url"],
**extras,
)
# If we can't convert, return the block unchanged
return block

View File

@@ -0,0 +1,47 @@
"""Derivations of standard content blocks from Ollama 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 Ollama content."""
global WARNED # noqa: PLW0603
if not WARNED:
warning_message = (
"Content block standardization is not yet fully supported for Ollama."
)
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 Ollama content."""
global WARNED # noqa: PLW0603
if not WARNED:
warning_message = (
"Content block standardization is not yet fully supported for Ollama."
)
warnings.warn(warning_message, stacklevel=2)
WARNED = True
raise NotImplementedError
def _register_ollama_translator() -> None:
"""Register the Ollama 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("ollama", translate_content, translate_content_chunk)
_register_ollama_translator()

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