mirror of
https://github.com/hwchase17/langchain.git
synced 2026-02-11 11:40:19 +00:00
Compare commits
99 Commits
sr/async-p
...
langchain-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c891a51608 | ||
|
|
82650ea7f1 | ||
|
|
13964efccf | ||
|
|
f247270111 | ||
|
|
8be4adccd1 | ||
|
|
b6bd507198 | ||
|
|
3e0d7512ef | ||
|
|
53ed770849 | ||
|
|
e2050e24ef | ||
|
|
59bb8bffd1 | ||
|
|
16ec9bc535 | ||
|
|
fda8a71e19 | ||
|
|
8f23bd109b | ||
|
|
ff632c1028 | ||
|
|
3f71efc93c | ||
|
|
cb3b5bf69b | ||
|
|
1c8a01ed03 | ||
|
|
25333d3b45 | ||
|
|
42830208f3 | ||
|
|
653dc77c7e | ||
|
|
f6ab75ba8b | ||
|
|
b6af0d228c | ||
|
|
5800721fd4 | ||
|
|
ebfb938a68 | ||
|
|
1a3e9e06ee | ||
|
|
c7ebbe5c8a | ||
|
|
76dfb7fbe8 | ||
|
|
f25133c523 | ||
|
|
fc7a07d6f1 | ||
|
|
e8ff6f4db6 | ||
|
|
b88115f6fc | ||
|
|
67aa37b144 | ||
|
|
9f14714367 | ||
|
|
7cc9312979 | ||
|
|
387d0f4edf | ||
|
|
207ea46813 | ||
|
|
3ef1165c0c | ||
|
|
ffee5155b4 | ||
|
|
5ef7d42bf6 | ||
|
|
750a3ffda6 | ||
|
|
8b1e25461b | ||
|
|
ced9fc270f | ||
|
|
311aa94d69 | ||
|
|
cb8598b828 | ||
|
|
fded6c6b13 | ||
|
|
544b08d610 | ||
|
|
a48ace52ad | ||
|
|
20979d525c | ||
|
|
188c0154b3 | ||
|
|
3c189f0393 | ||
|
|
8509efa6ad | ||
|
|
b1a105f85f | ||
|
|
9e54c5fa7f | ||
|
|
0b8817c900 | ||
|
|
083fbfb0d1 | ||
|
|
f98f7359d3 | ||
|
|
50b48fa1ff | ||
|
|
a54f4385f8 | ||
|
|
98e4e7d043 | ||
|
|
2cf5c52c13 | ||
|
|
bf41a75073 | ||
|
|
e15c41233d | ||
|
|
25d5db88d5 | ||
|
|
1237f94633 | ||
|
|
5c8837ea5a | ||
|
|
820e355f53 | ||
|
|
9a3ba71636 | ||
|
|
00def6da72 | ||
|
|
4f8cced3b6 | ||
|
|
365d7c414b | ||
|
|
a4874123a0 | ||
|
|
a5f92fdd9a | ||
|
|
431e6d6211 | ||
|
|
0f1afa178e | ||
|
|
830d1a207c | ||
|
|
b494a3c57b | ||
|
|
f088fac492 | ||
|
|
925ad65df9 | ||
|
|
e09d90b627 | ||
|
|
ddde1eff68 | ||
|
|
9b576440ed | ||
|
|
a80fa1b25f | ||
|
|
72b66fcca5 | ||
|
|
a47d993ddd | ||
|
|
cb4705dfc0 | ||
|
|
9a9263a2dd | ||
|
|
e4b69db4cf | ||
|
|
242881562b | ||
|
|
a2322f68ba | ||
|
|
7e9ae5df60 | ||
|
|
7a108618ae | ||
|
|
6f058e7b9b | ||
|
|
dbc5a3b718 | ||
|
|
f0f1e28473 | ||
|
|
8bd2403518 | ||
|
|
4dd9110424 | ||
|
|
174e685139 | ||
|
|
9721684501 | ||
|
|
a4e135b508 |
2
.github/actions/people/action.yml
vendored
2
.github/actions/people/action.yml
vendored
@@ -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>"
|
||||
|
||||
8
.github/actions/uv_setup/action.yml
vendored
8
.github/actions/uv_setup/action.yml
vendored
@@ -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"
|
||||
|
||||
80
.github/pr-file-labeler.yml
vendored
80
.github/pr-file-labeler.yml
vendored
@@ -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*"
|
||||
41
.github/pr-title-labeler.yml
vendored
41
.github/pr-title-labeler.yml
vendored
@@ -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"]
|
||||
36
.github/scripts/check_diff.py
vendored
36
.github/scripts/check_diff.py
vendored
@@ -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]
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
45
.github/scripts/get_min_versions.py
vendored
45
.github/scripts/get_min_versions.py
vendored
@@ -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
|
||||
|
||||
20
.github/scripts/prep_api_docs_build.py
vendored
20
.github/scripts/prep_api_docs_build.py
vendored
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
9
.github/workflows/_integration_test.yml
vendored
9
.github/workflows/_integration_test.yml
vendored
@@ -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
|
||||
|
||||
32
.github/workflows/_lint.yml
vendored
32
.github/workflows/_lint.yml
vendored
@@ -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: |
|
||||
|
||||
34
.github/workflows/_release.yml
vendored
34
.github/workflows/_release.yml
vendored
@@ -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:
|
||||
|
||||
5
.github/workflows/_test.yml
vendored
5
.github/workflows/_test.yml
vendored
@@ -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:
|
||||
|
||||
7
.github/workflows/_test_doc_imports.yml
vendored
7
.github/workflows/_test_doc_imports.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/_test_pydantic.yml
vendored
2
.github/workflows/_test_pydantic.yml
vendored
@@ -1,5 +1,3 @@
|
||||
# Facilitate unit testing against different Pydantic versions for a provided package.
|
||||
|
||||
name: '🐍 Pydantic Version Testing'
|
||||
|
||||
on:
|
||||
|
||||
44
.github/workflows/api_doc_build.yml
vendored
44
.github/workflows/api_doc_build.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/check-broken-links.yml
vendored
6
.github/workflows/check-broken-links.yml
vendored
@@ -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"
|
||||
|
||||
8
.github/workflows/check_core_versions.yml
vendored
8
.github/workflows/check_core_versions.yml
vendored
@@ -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:
|
||||
|
||||
20
.github/workflows/check_diffs.yml
vendored
20
.github/workflows/check_diffs.yml
vendored
@@ -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:
|
||||
|
||||
3
.github/workflows/check_new_docs.yml
vendored
3
.github/workflows/check_new_docs.yml
vendored
@@ -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:
|
||||
|
||||
10
.github/workflows/extract_ignored_words_list.py
vendored
Normal file
10
.github/workflows/extract_ignored_words_list.py
vendored
Normal 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}")
|
||||
6
.github/workflows/people.yml
vendored
6
.github/workflows/people.yml
vendored
@@ -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:
|
||||
|
||||
28
.github/workflows/pr_labeler_file.yml
vendored
28
.github/workflows/pr_labeler_file.yml
vendored
@@ -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
|
||||
28
.github/workflows/pr_labeler_title.yml
vendored
28
.github/workflows/pr_labeler_title.yml
vendored
@@ -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
|
||||
64
.github/workflows/pr_lint.yml
vendored
64
.github/workflows/pr_lint.yml
vendored
@@ -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 multi‐tenant 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 multi‐tenant 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'
|
||||
|
||||
2
.github/workflows/run_notebooks.yml
vendored
2
.github/workflows/run_notebooks.yml
vendored
@@ -1,5 +1,3 @@
|
||||
# Integration tests for documentation notebooks.
|
||||
|
||||
name: '📓 Validate Documentation Notebooks'
|
||||
run-name: 'Test notebooks in ${{ inputs.working-directory }}'
|
||||
on:
|
||||
|
||||
22
.github/workflows/scheduled_test.yml
vendored
22
.github/workflows/scheduled_test.yml
vendored
@@ -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 \
|
||||
|
||||
9
.github/workflows/v1_changes.md
vendored
9
.github/workflows/v1_changes.md
vendored
@@ -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
|
||||
@@ -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
25
.readthedocs.yaml
Normal 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
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -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
325
AGENTS.md
@@ -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
|
||||
16
Makefile
16
Makefile
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
153
docs/README.md
153
docs/README.md
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"execution_count": null,
|
||||
"id": "1fcf7b27-1cc3-420a-b920-0420b5892e20",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -102,7 +102,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -133,7 +133,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "99d27f8f-ae78-48bc-9bf2-3cef35213ec7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -163,7 +163,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -176,7 +176,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": null,
|
||||
"id": "325fb4ca",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -198,7 +198,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -234,7 +234,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": null,
|
||||
"id": "6c1455a9-699a-4702-a7e0-7f6eaec76a21",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -284,7 +284,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -312,7 +312,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": null,
|
||||
"id": "55e1d937-3b22-4deb-b9f0-9e688f0609dc",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -342,7 +342,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -417,7 +417,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -443,7 +443,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "83593b9d-a8d3-4c99-9dac-64e0a9d397cb",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -488,13 +488,13 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())\n",
|
||||
"print(response.text)\n",
|
||||
"response.usage_metadata"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": null,
|
||||
"id": "9bbf578e-794a-4dc0-a469-78c876ccd4a3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -530,7 +530,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message, response, next_message])\n",
|
||||
"print(response.text())\n",
|
||||
"print(response.text)\n",
|
||||
"response.usage_metadata"
|
||||
]
|
||||
},
|
||||
@@ -600,7 +600,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": null,
|
||||
"id": "ae076c9b-ff8f-461d-9349-250f396c9a25",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -641,7 +641,7 @@
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"response = llm.invoke([message])\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "5df2e558-321d-4cf7-994e-2815ac37e704",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -75,7 +75,7 @@
|
||||
"\n",
|
||||
"chain = prompt | llm\n",
|
||||
"response = chain.invoke({\"image_url\": url})\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": null,
|
||||
"id": "25e4829e-0073-49a8-9669-9f43e5778383",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -144,7 +144,7 @@
|
||||
" \"cache_type\": \"ephemeral\",\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -243,12 +243,12 @@
|
||||
"id": "0ef05abb-9c04-4dc3-995e-f857779644d5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can filter to text using the [.text()](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.ai.AIMessage.html#langchain_core.messages.ai.AIMessage.text) method on the output:"
|
||||
"You can filter to text using the [.text](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.ai.AIMessage.html#langchain_core.messages.ai.AIMessage.text) property on the output:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": null,
|
||||
"id": "2a4e743f-ea7d-4e5a-9b12-f9992362de8b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -262,7 +262,7 @@
|
||||
],
|
||||
"source": [
|
||||
"for chunk in llm.stream(messages):\n",
|
||||
" print(chunk.text(), end=\"|\")"
|
||||
" print(chunk.text, end=\"|\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": null,
|
||||
"id": "c5fac0e9-05a4-4fc1-a3b3-e5bbb24b971b",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
@@ -286,7 +286,7 @@
|
||||
],
|
||||
"source": [
|
||||
"async for token in llm.astream(\"Hello, please explain how antibiotics work\"):\n",
|
||||
" print(token.text(), end=\"\")"
|
||||
" print(token.text, end=\"\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -814,7 +814,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "1f758726-33ef-4c04-8a54-49adb783bbb3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -860,7 +860,7 @@
|
||||
"llm_with_tools = llm.bind_tools([tool])\n",
|
||||
"\n",
|
||||
"response = llm_with_tools.invoke(\"What is deep research by OpenAI?\")\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1151,7 +1151,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": null,
|
||||
"id": "073f6010-6b0e-4db6-b2d3-7427c8dec95b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1167,7 +1167,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response_2.text()"
|
||||
"response_2.text"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1198,7 +1198,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"execution_count": null,
|
||||
"id": "b6da5bd6-a44a-4c64-970b-30da26b003d6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1214,7 +1214,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response_2.text()"
|
||||
"response_2.text"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1404,7 +1404,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": null,
|
||||
"id": "51d3e4d3-ea78-426c-9205-aecb0937fca7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1428,13 +1428,13 @@
|
||||
"messages = [{\"role\": \"user\", \"content\": first_query}]\n",
|
||||
"\n",
|
||||
"response = llm_with_tools.invoke(messages)\n",
|
||||
"response_text = response.text()\n",
|
||||
"response_text = response.text\n",
|
||||
"print(f\"{response_text[:100]}... {response_text[-100:]}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "b248bedf-2050-4c17-a90e-3a26eeb1b055",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1460,7 +1460,7 @@
|
||||
" ]\n",
|
||||
")\n",
|
||||
"second_response = llm_with_tools.invoke(messages)\n",
|
||||
"print(second_response.text())"
|
||||
"print(second_response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1482,7 +1482,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": null,
|
||||
"id": "009e541a-b372-410e-b9dd-608a8052ce09",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1502,12 +1502,12 @@
|
||||
" output_version=\"responses/v1\",\n",
|
||||
")\n",
|
||||
"response = llm.invoke(\"Hi, I'm Bob.\")\n",
|
||||
"print(response.text())"
|
||||
"print(response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": null,
|
||||
"id": "393a443a-4c5f-4a07-bc0e-c76e529b35e3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1524,7 +1524,7 @@
|
||||
" \"What is my name?\",\n",
|
||||
" previous_response_id=response.response_metadata[\"id\"],\n",
|
||||
")\n",
|
||||
"print(second_response.text())"
|
||||
"print(second_response.text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1589,7 +1589,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "8d322f3a-0732-45ab-ac95-dfd4596e0d85",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1616,7 +1616,7 @@
|
||||
"response = llm.invoke(\"What is 3^3?\")\n",
|
||||
"\n",
|
||||
"# Output\n",
|
||||
"response.text()"
|
||||
"response.text"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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=\"|\")"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
3
libs/cli/CONTRIBUTING.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Contributing to langchain-cli
|
||||
|
||||
Update CLI versions with `poe bump` to ensure that version commands display correctly.
|
||||
@@ -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.
|
||||
|
||||
@@ -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 $^
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
1
libs/cli/scripts/__init__.py
Normal file
1
libs/cli/scripts/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Scripts."""
|
||||
@@ -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
250
libs/cli/uv.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Some **beta** features that are not yet ready for production."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Runnables."""
|
||||
@@ -1,447 +0,0 @@
|
||||
"""Context management for runnables."""
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from collections.abc import Awaitable, Mapping, Sequence
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from typing_extensions import override
|
||||
|
||||
from langchain_core._api.beta_decorator import beta
|
||||
from langchain_core.runnables.base import (
|
||||
Runnable,
|
||||
RunnableSerializable,
|
||||
coerce_to_runnable,
|
||||
)
|
||||
from langchain_core.runnables.config import RunnableConfig, ensure_config, patch_config
|
||||
from langchain_core.runnables.utils import ConfigurableFieldSpec, Input, Output
|
||||
|
||||
T = TypeVar("T")
|
||||
Values = dict[Union[asyncio.Event, threading.Event], Any]
|
||||
CONTEXT_CONFIG_PREFIX = "__context__/"
|
||||
CONTEXT_CONFIG_SUFFIX_GET = "/get"
|
||||
CONTEXT_CONFIG_SUFFIX_SET = "/set"
|
||||
|
||||
|
||||
async def _asetter(done: asyncio.Event, values: Values, value: T) -> T:
|
||||
values[done] = value
|
||||
done.set()
|
||||
return value
|
||||
|
||||
|
||||
async def _agetter(done: asyncio.Event, values: Values) -> Any:
|
||||
await done.wait()
|
||||
return values[done]
|
||||
|
||||
|
||||
def _setter(done: threading.Event, values: Values, value: T) -> T:
|
||||
values[done] = value
|
||||
done.set()
|
||||
return value
|
||||
|
||||
|
||||
def _getter(done: threading.Event, values: Values) -> Any:
|
||||
done.wait()
|
||||
return values[done]
|
||||
|
||||
|
||||
def _key_from_id(id_: str) -> str:
|
||||
wout_prefix = id_.split(CONTEXT_CONFIG_PREFIX, maxsplit=1)[1]
|
||||
if wout_prefix.endswith(CONTEXT_CONFIG_SUFFIX_GET):
|
||||
return wout_prefix[: -len(CONTEXT_CONFIG_SUFFIX_GET)]
|
||||
if wout_prefix.endswith(CONTEXT_CONFIG_SUFFIX_SET):
|
||||
return wout_prefix[: -len(CONTEXT_CONFIG_SUFFIX_SET)]
|
||||
msg = f"Invalid context config id {id_}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _config_with_context(
|
||||
config: RunnableConfig,
|
||||
steps: list[Runnable],
|
||||
setter: Callable,
|
||||
getter: Callable,
|
||||
event_cls: Union[type[threading.Event], type[asyncio.Event]],
|
||||
) -> RunnableConfig:
|
||||
if any(k.startswith(CONTEXT_CONFIG_PREFIX) for k in config.get("configurable", {})):
|
||||
return config
|
||||
|
||||
context_specs = [
|
||||
(spec, i)
|
||||
for i, step in enumerate(steps)
|
||||
for spec in step.config_specs
|
||||
if spec.id.startswith(CONTEXT_CONFIG_PREFIX)
|
||||
]
|
||||
grouped_by_key = {
|
||||
key: list(group)
|
||||
for key, group in groupby(
|
||||
sorted(context_specs, key=lambda s: s[0].id),
|
||||
key=lambda s: _key_from_id(s[0].id),
|
||||
)
|
||||
}
|
||||
deps_by_key = {
|
||||
key: {
|
||||
_key_from_id(dep) for spec in group for dep in (spec[0].dependencies or [])
|
||||
}
|
||||
for key, group in grouped_by_key.items()
|
||||
}
|
||||
|
||||
values: Values = {}
|
||||
events: defaultdict[str, Union[asyncio.Event, threading.Event]] = defaultdict(
|
||||
event_cls
|
||||
)
|
||||
context_funcs: dict[str, Callable[[], Any]] = {}
|
||||
for key, group in grouped_by_key.items():
|
||||
getters = [s for s in group if s[0].id.endswith(CONTEXT_CONFIG_SUFFIX_GET)]
|
||||
setters = [s for s in group if s[0].id.endswith(CONTEXT_CONFIG_SUFFIX_SET)]
|
||||
|
||||
for dep in deps_by_key[key]:
|
||||
if key in deps_by_key[dep]:
|
||||
msg = f"Deadlock detected between context keys {key} and {dep}"
|
||||
raise ValueError(msg)
|
||||
if len(setters) != 1:
|
||||
msg = f"Expected exactly one setter for context key {key}"
|
||||
raise ValueError(msg)
|
||||
setter_idx = setters[0][1]
|
||||
if any(getter_idx < setter_idx for _, getter_idx in getters):
|
||||
msg = f"Context setter for key {key} must be defined after all getters."
|
||||
raise ValueError(msg)
|
||||
|
||||
if getters:
|
||||
context_funcs[getters[0][0].id] = partial(getter, events[key], values)
|
||||
context_funcs[setters[0][0].id] = partial(setter, events[key], values)
|
||||
|
||||
return patch_config(config, configurable=context_funcs)
|
||||
|
||||
|
||||
def aconfig_with_context(
|
||||
config: RunnableConfig,
|
||||
steps: list[Runnable],
|
||||
) -> RunnableConfig:
|
||||
"""Asynchronously patch a runnable config with context getters and setters.
|
||||
|
||||
Args:
|
||||
config: The runnable config.
|
||||
steps: The runnable steps.
|
||||
|
||||
Returns:
|
||||
The patched runnable config.
|
||||
"""
|
||||
return _config_with_context(config, steps, _asetter, _agetter, asyncio.Event)
|
||||
|
||||
|
||||
def config_with_context(
|
||||
config: RunnableConfig,
|
||||
steps: list[Runnable],
|
||||
) -> RunnableConfig:
|
||||
"""Patch a runnable config with context getters and setters.
|
||||
|
||||
Args:
|
||||
config: The runnable config.
|
||||
steps: The runnable steps.
|
||||
|
||||
Returns:
|
||||
The patched runnable config.
|
||||
"""
|
||||
return _config_with_context(config, steps, _setter, _getter, threading.Event)
|
||||
|
||||
|
||||
@beta()
|
||||
class ContextGet(RunnableSerializable):
|
||||
"""Get a context value."""
|
||||
|
||||
prefix: str = ""
|
||||
|
||||
key: Union[str, list[str]]
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"ContextGet({_print_keys(self.key)})"
|
||||
|
||||
@property
|
||||
def ids(self) -> list[str]:
|
||||
"""The context getter ids."""
|
||||
prefix = self.prefix + "/" if self.prefix else ""
|
||||
keys = self.key if isinstance(self.key, list) else [self.key]
|
||||
return [
|
||||
f"{CONTEXT_CONFIG_PREFIX}{prefix}{k}{CONTEXT_CONFIG_SUFFIX_GET}"
|
||||
for k in keys
|
||||
]
|
||||
|
||||
@property
|
||||
@override
|
||||
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
||||
return super().config_specs + [
|
||||
ConfigurableFieldSpec(
|
||||
id=id_,
|
||||
annotation=Callable[[], Any],
|
||||
)
|
||||
for id_ in self.ids
|
||||
]
|
||||
|
||||
@override
|
||||
def invoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
config = ensure_config(config)
|
||||
configurable = config.get("configurable", {})
|
||||
if isinstance(self.key, list):
|
||||
return {key: configurable[id_]() for key, id_ in zip(self.key, self.ids)}
|
||||
return configurable[self.ids[0]]()
|
||||
|
||||
@override
|
||||
async def ainvoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
config = ensure_config(config)
|
||||
configurable = config.get("configurable", {})
|
||||
if isinstance(self.key, list):
|
||||
values = await asyncio.gather(*(configurable[id_]() for id_ in self.ids))
|
||||
return dict(zip(self.key, values))
|
||||
return await configurable[self.ids[0]]()
|
||||
|
||||
|
||||
SetValue = Union[
|
||||
Runnable[Input, Output],
|
||||
Callable[[Input], Output],
|
||||
Callable[[Input], Awaitable[Output]],
|
||||
Any,
|
||||
]
|
||||
|
||||
|
||||
def _coerce_set_value(value: SetValue) -> Runnable[Input, Output]:
|
||||
if not isinstance(value, Runnable) and not callable(value):
|
||||
return coerce_to_runnable(lambda _: value)
|
||||
return coerce_to_runnable(value)
|
||||
|
||||
|
||||
@beta()
|
||||
class ContextSet(RunnableSerializable):
|
||||
"""Set a context value."""
|
||||
|
||||
prefix: str = ""
|
||||
|
||||
keys: Mapping[str, Optional[Runnable]]
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: Optional[str] = None,
|
||||
value: Optional[SetValue] = None,
|
||||
prefix: str = "",
|
||||
**kwargs: SetValue,
|
||||
):
|
||||
"""Create a context setter.
|
||||
|
||||
Args:
|
||||
key: The context setter key.
|
||||
value: The context setter value.
|
||||
prefix: The context setter prefix.
|
||||
**kwargs: Additional context setter key-value pairs.
|
||||
"""
|
||||
if key is not None:
|
||||
kwargs[key] = value
|
||||
super().__init__(
|
||||
keys={
|
||||
k: _coerce_set_value(v) if v is not None else None
|
||||
for k, v in kwargs.items()
|
||||
},
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"ContextSet({_print_keys(list(self.keys.keys()))})"
|
||||
|
||||
@property
|
||||
def ids(self) -> list[str]:
|
||||
"""The context setter ids."""
|
||||
prefix = self.prefix + "/" if self.prefix else ""
|
||||
return [
|
||||
f"{CONTEXT_CONFIG_PREFIX}{prefix}{key}{CONTEXT_CONFIG_SUFFIX_SET}"
|
||||
for key in self.keys
|
||||
]
|
||||
|
||||
@property
|
||||
@override
|
||||
def config_specs(self) -> list[ConfigurableFieldSpec]:
|
||||
mapper_config_specs = [
|
||||
s
|
||||
for mapper in self.keys.values()
|
||||
if mapper is not None
|
||||
for s in mapper.config_specs
|
||||
]
|
||||
for spec in mapper_config_specs:
|
||||
if spec.id.endswith(CONTEXT_CONFIG_SUFFIX_GET):
|
||||
getter_key = spec.id.split("/")[1]
|
||||
if getter_key in self.keys:
|
||||
msg = f"Circular reference in context setter for key {getter_key}"
|
||||
raise ValueError(msg)
|
||||
return super().config_specs + [
|
||||
ConfigurableFieldSpec(
|
||||
id=id_,
|
||||
annotation=Callable[[], Any],
|
||||
)
|
||||
for id_ in self.ids
|
||||
]
|
||||
|
||||
@override
|
||||
def invoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
config = ensure_config(config)
|
||||
configurable = config.get("configurable", {})
|
||||
for id_, mapper in zip(self.ids, self.keys.values()):
|
||||
if mapper is not None:
|
||||
configurable[id_](mapper.invoke(input, config))
|
||||
else:
|
||||
configurable[id_](input)
|
||||
return input
|
||||
|
||||
@override
|
||||
async def ainvoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
config = ensure_config(config)
|
||||
configurable = config.get("configurable", {})
|
||||
for id_, mapper in zip(self.ids, self.keys.values()):
|
||||
if mapper is not None:
|
||||
await configurable[id_](await mapper.ainvoke(input, config))
|
||||
else:
|
||||
await configurable[id_](input)
|
||||
return input
|
||||
|
||||
|
||||
class Context:
|
||||
"""Context for a runnable.
|
||||
|
||||
The `Context` class provides methods for creating context scopes,
|
||||
getters, and setters within a runnable. It allows for managing
|
||||
and accessing contextual information throughout the execution
|
||||
of a program.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_core.beta.runnables.context import Context
|
||||
from langchain_core.runnables.passthrough import RunnablePassthrough
|
||||
from langchain_core.prompts.prompt import PromptTemplate
|
||||
from langchain_core.output_parsers.string import StrOutputParser
|
||||
from tests.unit_tests.fake.llm import FakeListLLM
|
||||
|
||||
chain = (
|
||||
Context.setter("input")
|
||||
| {
|
||||
"context": RunnablePassthrough() | Context.setter("context"),
|
||||
"question": RunnablePassthrough(),
|
||||
}
|
||||
| PromptTemplate.from_template("{context} {question}")
|
||||
| FakeListLLM(responses=["hello"])
|
||||
| StrOutputParser()
|
||||
| {
|
||||
"result": RunnablePassthrough(),
|
||||
"context": Context.getter("context"),
|
||||
"input": Context.getter("input"),
|
||||
}
|
||||
)
|
||||
|
||||
# Use the chain
|
||||
output = chain.invoke("What's your name?")
|
||||
print(output["result"]) # Output: "hello"
|
||||
print(output["context"]) # Output: "What's your name?"
|
||||
print(output["input"]) # Output: "What's your name?
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_scope(scope: str, /) -> "PrefixContext":
|
||||
"""Create a context scope.
|
||||
|
||||
Args:
|
||||
scope: The scope.
|
||||
|
||||
Returns:
|
||||
The context scope.
|
||||
"""
|
||||
return PrefixContext(prefix=scope)
|
||||
|
||||
@staticmethod
|
||||
def getter(key: Union[str, list[str]], /) -> ContextGet:
|
||||
"""Return a context getter.
|
||||
|
||||
Args:
|
||||
key: The context getter key.
|
||||
"""
|
||||
return ContextGet(key=key)
|
||||
|
||||
@staticmethod
|
||||
def setter(
|
||||
_key: Optional[str] = None,
|
||||
_value: Optional[SetValue] = None,
|
||||
/,
|
||||
**kwargs: SetValue,
|
||||
) -> ContextSet:
|
||||
"""Return a context setter.
|
||||
|
||||
Args:
|
||||
_key: The context setter key.
|
||||
_value: The context setter value.
|
||||
**kwargs: Additional context setter key-value pairs.
|
||||
"""
|
||||
return ContextSet(_key, _value, prefix="", **kwargs)
|
||||
|
||||
|
||||
class PrefixContext:
|
||||
"""Context for a runnable with a prefix."""
|
||||
|
||||
prefix: str = ""
|
||||
|
||||
def __init__(self, prefix: str = ""):
|
||||
"""Create a prefix context.
|
||||
|
||||
Args:
|
||||
prefix: The prefix.
|
||||
"""
|
||||
self.prefix = prefix
|
||||
|
||||
def getter(self, key: Union[str, list[str]], /) -> ContextGet:
|
||||
"""Return a prefixed context getter.
|
||||
|
||||
Args:
|
||||
key: The context getter key.
|
||||
"""
|
||||
return ContextGet(key=key, prefix=self.prefix)
|
||||
|
||||
def setter(
|
||||
self,
|
||||
_key: Optional[str] = None,
|
||||
_value: Optional[SetValue] = None,
|
||||
/,
|
||||
**kwargs: SetValue,
|
||||
) -> ContextSet:
|
||||
"""Return a prefixed context setter.
|
||||
|
||||
Args:
|
||||
_key: The context setter key.
|
||||
_value: The context setter value.
|
||||
**kwargs: Additional context setter key-value pairs.
|
||||
"""
|
||||
return ContextSet(_key, _value, prefix=self.prefix, **kwargs)
|
||||
|
||||
|
||||
def _print_keys(keys: Union[str, Sequence[str]]) -> str:
|
||||
if isinstance(keys, str):
|
||||
return f"'{keys}'"
|
||||
return ", ".join(f"'{k}'" for k in keys)
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {...}, ...}), ...]
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -45,6 +45,7 @@ https://python.langchain.com/docs/how_to/custom_llm/
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from langchain_core._import_utils import import_attr
|
||||
from langchain_core.language_models._utils import is_openai_data_block
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.language_models.base import (
|
||||
@@ -85,6 +86,7 @@ __all__ = (
|
||||
"ParrotFakeChatModel",
|
||||
"SimpleChatModel",
|
||||
"get_tokenizer",
|
||||
"is_openai_data_block",
|
||||
)
|
||||
|
||||
_dynamic_imports = {
|
||||
@@ -104,6 +106,7 @@ _dynamic_imports = {
|
||||
"ParrotFakeChatModel": "fake_chat_models",
|
||||
"LLM": "llms",
|
||||
"BaseLLM": "llms",
|
||||
"is_openai_data_block": "_utils",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,49 @@
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Literal,
|
||||
Optional,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from langchain_core.messages import BaseMessage
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.messages.content import (
|
||||
ContentBlock,
|
||||
)
|
||||
|
||||
|
||||
def _is_openai_data_block(block: dict) -> bool:
|
||||
"""Check if the block contains multimodal data in OpenAI Chat Completions format."""
|
||||
def is_openai_data_block(
|
||||
block: dict, filter_: Union[Literal["image", "audio", "file"], None] = None
|
||||
) -> bool:
|
||||
"""Check whether a block contains multimodal data in OpenAI Chat Completions format.
|
||||
|
||||
Supports both data and ID-style blocks (e.g. ``'file_data'`` and ``'file_id'``)
|
||||
|
||||
If additional keys are present, they are ignored / will not affect outcome as long
|
||||
as the required keys are present and valid.
|
||||
|
||||
Args:
|
||||
block: The content block to check.
|
||||
filter_: If provided, only return True for blocks matching this specific type.
|
||||
- "image": Only match image_url blocks
|
||||
- "audio": Only match input_audio blocks
|
||||
- "file": Only match file blocks
|
||||
If None, match any valid OpenAI data block type. Note that this means that
|
||||
if the block has a valid OpenAI data type but the filter_ is set to a
|
||||
different type, this function will return False.
|
||||
|
||||
Returns:
|
||||
True if the block is a valid OpenAI data block and matches the filter_
|
||||
(if provided).
|
||||
|
||||
"""
|
||||
if block.get("type") == "image_url":
|
||||
if filter_ is not None and filter_ != "image":
|
||||
return False
|
||||
if (
|
||||
(set(block.keys()) <= {"type", "image_url", "detail"})
|
||||
and (image_url := block.get("image_url"))
|
||||
@@ -15,29 +51,47 @@ def _is_openai_data_block(block: dict) -> bool:
|
||||
):
|
||||
url = image_url.get("url")
|
||||
if isinstance(url, str):
|
||||
# Required per OpenAI spec
|
||||
return True
|
||||
# Ignore `'detail'` since it's optional and specific to OpenAI
|
||||
|
||||
elif block.get("type") == "input_audio":
|
||||
if filter_ is not None and filter_ != "audio":
|
||||
return False
|
||||
if (audio := block.get("input_audio")) and isinstance(audio, dict):
|
||||
audio_data = audio.get("data")
|
||||
audio_format = audio.get("format")
|
||||
# Both required per OpenAI spec
|
||||
if isinstance(audio_data, str) and isinstance(audio_format, str):
|
||||
return True
|
||||
|
||||
elif block.get("type") == "file":
|
||||
if filter_ is not None and filter_ != "file":
|
||||
return False
|
||||
if (file := block.get("file")) and isinstance(file, dict):
|
||||
file_data = file.get("file_data")
|
||||
if isinstance(file_data, str):
|
||||
return True
|
||||
|
||||
elif block.get("type") == "input_audio":
|
||||
if (input_audio := block.get("input_audio")) and isinstance(input_audio, dict):
|
||||
audio_data = input_audio.get("data")
|
||||
audio_format = input_audio.get("format")
|
||||
if isinstance(audio_data, str) and isinstance(audio_format, str):
|
||||
file_id = file.get("file_id")
|
||||
# Files can be either base64-encoded or pre-uploaded with an ID
|
||||
if isinstance(file_data, str) or isinstance(file_id, str):
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
# Has no `'type'` key
|
||||
return False
|
||||
|
||||
|
||||
def _parse_data_uri(uri: str) -> Optional[dict]:
|
||||
"""Parse a data URI into its components. If parsing fails, return None.
|
||||
class ParsedDataUri(TypedDict):
|
||||
source_type: Literal["base64"]
|
||||
data: str
|
||||
mime_type: str
|
||||
|
||||
|
||||
def _parse_data_uri(uri: str) -> Optional[ParsedDataUri]:
|
||||
"""Parse a data URI into its components.
|
||||
|
||||
If parsing fails, return None. If either MIME type or data is missing, return None.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -57,84 +111,219 @@ def _parse_data_uri(uri: str) -> Optional[dict]:
|
||||
match = re.match(regex, uri)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
mime_type = match.group("mime_type")
|
||||
data = match.group("data")
|
||||
if not mime_type or not data:
|
||||
return None
|
||||
|
||||
return {
|
||||
"source_type": "base64",
|
||||
"data": match.group("data"),
|
||||
"mime_type": match.group("mime_type"),
|
||||
"data": data,
|
||||
"mime_type": mime_type,
|
||||
}
|
||||
|
||||
|
||||
def _convert_openai_format_to_data_block(block: dict) -> dict:
|
||||
"""Convert OpenAI image content block to standard data content block.
|
||||
def _normalize_messages(
|
||||
messages: Sequence["BaseMessage"],
|
||||
) -> list["BaseMessage"]:
|
||||
"""Normalize message formats to LangChain v1 standard content blocks.
|
||||
|
||||
If parsing fails, pass-through.
|
||||
Chat models already implement support for:
|
||||
- Images in OpenAI Chat Completions format
|
||||
These will be passed through unchanged
|
||||
- LangChain v1 standard content blocks
|
||||
|
||||
Args:
|
||||
block: The OpenAI image content block to convert.
|
||||
This function extends support to:
|
||||
- `Audio <https://platform.openai.com/docs/api-reference/chat/create>`__ and
|
||||
`file <https://platform.openai.com/docs/api-reference/files>`__ data in OpenAI
|
||||
Chat Completions format
|
||||
- Images are technically supported but we expect chat models to handle them
|
||||
directly; this may change in the future
|
||||
- LangChain v0 standard content blocks for backward compatibility
|
||||
|
||||
Returns:
|
||||
The converted standard data content block.
|
||||
"""
|
||||
if block["type"] == "image_url":
|
||||
parsed = _parse_data_uri(block["image_url"]["url"])
|
||||
if parsed is not None:
|
||||
parsed["type"] = "image"
|
||||
return parsed
|
||||
return block
|
||||
.. versionchanged:: 1.0.0
|
||||
In previous versions, this function returned messages in LangChain v0 format.
|
||||
Now, it returns messages in LangChain v1 format, which upgraded chat models now
|
||||
expect to receive when passing back in message history. For backward
|
||||
compatibility, this function will convert v0 message content to v1 format.
|
||||
|
||||
if block["type"] == "file":
|
||||
parsed = _parse_data_uri(block["file"]["file_data"])
|
||||
if parsed is not None:
|
||||
parsed["type"] = "file"
|
||||
if filename := block["file"].get("filename"):
|
||||
parsed["filename"] = filename
|
||||
return parsed
|
||||
return block
|
||||
.. dropdown:: v0 Content Block Schemas
|
||||
|
||||
if block["type"] == "input_audio":
|
||||
data = block["input_audio"].get("data")
|
||||
audio_format = block["input_audio"].get("format")
|
||||
if data and audio_format:
|
||||
return {
|
||||
"type": "audio",
|
||||
"source_type": "base64",
|
||||
"data": data,
|
||||
"mime_type": f"audio/{audio_format}",
|
||||
``URLContentBlock``:
|
||||
|
||||
.. codeblock::
|
||||
|
||||
{
|
||||
mime_type: NotRequired[str]
|
||||
type: Literal['image', 'audio', 'file'],
|
||||
source_type: Literal['url'],
|
||||
url: str,
|
||||
}
|
||||
return block
|
||||
|
||||
return block
|
||||
``Base64ContentBlock``:
|
||||
|
||||
.. codeblock::
|
||||
|
||||
def _normalize_messages(messages: Sequence[BaseMessage]) -> list[BaseMessage]:
|
||||
"""Extend support for message formats.
|
||||
{
|
||||
mime_type: NotRequired[str]
|
||||
type: Literal['image', 'audio', 'file'],
|
||||
source_type: Literal['base64'],
|
||||
data: str,
|
||||
}
|
||||
|
||||
``IDContentBlock``:
|
||||
|
||||
(In practice, this was never used)
|
||||
|
||||
.. codeblock::
|
||||
|
||||
{
|
||||
type: Literal['image', 'audio', 'file'],
|
||||
source_type: Literal['id'],
|
||||
id: str,
|
||||
}
|
||||
|
||||
``PlainTextContentBlock``:
|
||||
|
||||
.. codeblock::
|
||||
|
||||
{
|
||||
mime_type: NotRequired[str]
|
||||
type: Literal['file'],
|
||||
source_type: Literal['text'],
|
||||
url: str,
|
||||
}
|
||||
|
||||
If a v1 message is passed in, it will be returned as-is, meaning it is safe to
|
||||
always pass in v1 messages to this function for assurance.
|
||||
|
||||
For posterity, here are the OpenAI Chat Completions schemas we expect:
|
||||
|
||||
Chat Completions image. Can be URL-based or base64-encoded. Supports MIME types
|
||||
png, jpeg/jpg, webp, static gif:
|
||||
{
|
||||
"type": Literal['image_url'],
|
||||
"image_url": {
|
||||
"url": Union["data:$MIME_TYPE;base64,$BASE64_ENCODED_IMAGE", "$IMAGE_URL"],
|
||||
"detail": Literal['low', 'high', 'auto'] = 'auto', # Supported by OpenAI
|
||||
}
|
||||
}
|
||||
|
||||
Chat Completions audio:
|
||||
{
|
||||
"type": Literal['input_audio'],
|
||||
"input_audio": {
|
||||
"format": Literal['wav', 'mp3'],
|
||||
"data": str = "$BASE64_ENCODED_AUDIO",
|
||||
},
|
||||
}
|
||||
|
||||
Chat Completions files: either base64 or pre-uploaded file ID
|
||||
{
|
||||
"type": Literal['file'],
|
||||
"file": Union[
|
||||
{
|
||||
"filename": Optional[str] = "$FILENAME",
|
||||
"file_data": str = "$BASE64_ENCODED_FILE",
|
||||
},
|
||||
{
|
||||
"file_id": str = "$FILE_ID", # For pre-uploaded files to OpenAI
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Chat models implement support for images in OpenAI Chat Completions format, as well
|
||||
as other multimodal data as standard data blocks. This function extends support to
|
||||
audio and file data in OpenAI Chat Completions format by converting them to standard
|
||||
data blocks.
|
||||
"""
|
||||
from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
|
||||
_convert_legacy_v0_content_block_to_v1,
|
||||
)
|
||||
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
|
||||
_convert_openai_format_to_data_block,
|
||||
)
|
||||
|
||||
formatted_messages = []
|
||||
for message in messages:
|
||||
# We preserve input messages - the caller may reuse them elsewhere and expects
|
||||
# them to remain unchanged. We only create a copy if we need to translate.
|
||||
formatted_message = message
|
||||
|
||||
if isinstance(message.content, list):
|
||||
for idx, block in enumerate(message.content):
|
||||
# OpenAI Chat Completions multimodal data blocks to v1 standard
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
# Subset to (PDF) files and audio, as most relevant chat models
|
||||
# support images in OAI format (and some may not yet support the
|
||||
# standard data block format)
|
||||
and block.get("type") in {"file", "input_audio"}
|
||||
and _is_openai_data_block(block)
|
||||
and block.get("type") in {"input_audio", "file"}
|
||||
# Discriminate between OpenAI/LC format since they share `'type'`
|
||||
and is_openai_data_block(block)
|
||||
):
|
||||
if formatted_message is message:
|
||||
formatted_message = message.model_copy()
|
||||
# Also shallow-copy content
|
||||
formatted_message.content = list(formatted_message.content)
|
||||
formatted_message = _ensure_message_copy(message, formatted_message)
|
||||
|
||||
converted_block = _convert_openai_format_to_data_block(block)
|
||||
_update_content_block(formatted_message, idx, converted_block)
|
||||
|
||||
# Convert multimodal LangChain v0 to v1 standard content blocks
|
||||
elif (
|
||||
isinstance(block, dict)
|
||||
and block.get("type")
|
||||
in {
|
||||
"image",
|
||||
"audio",
|
||||
"file",
|
||||
}
|
||||
and block.get("source_type") # v1 doesn't have `source_type`
|
||||
in {
|
||||
"url",
|
||||
"base64",
|
||||
"id",
|
||||
"text",
|
||||
}
|
||||
):
|
||||
formatted_message = _ensure_message_copy(message, formatted_message)
|
||||
|
||||
converted_block = _convert_legacy_v0_content_block_to_v1(block)
|
||||
_update_content_block(formatted_message, idx, converted_block)
|
||||
continue
|
||||
|
||||
# else, pass through blocks that look like they have v1 format unchanged
|
||||
|
||||
formatted_message.content[idx] = ( # type: ignore[index] # mypy confused by .model_copy
|
||||
_convert_openai_format_to_data_block(block)
|
||||
)
|
||||
formatted_messages.append(formatted_message)
|
||||
|
||||
return formatted_messages
|
||||
|
||||
|
||||
T = TypeVar("T", bound="BaseMessage")
|
||||
|
||||
|
||||
def _ensure_message_copy(message: T, formatted_message: T) -> T:
|
||||
"""Create a copy of the message if it hasn't been copied yet."""
|
||||
if formatted_message is message:
|
||||
formatted_message = message.model_copy()
|
||||
# Shallow-copy content list to allow modifications
|
||||
formatted_message.content = list(formatted_message.content)
|
||||
return formatted_message
|
||||
|
||||
|
||||
def _update_content_block(
|
||||
formatted_message: "BaseMessage", idx: int, new_block: Union[ContentBlock, dict]
|
||||
) -> None:
|
||||
"""Update a content block at the given index, handling type issues."""
|
||||
# Type ignore needed because:
|
||||
# - `BaseMessage.content` is typed as `Union[str, list[Union[str, dict]]]`
|
||||
# - When content is str, indexing fails (index error)
|
||||
# - When content is list, the items are `Union[str, dict]` but we're assigning
|
||||
# `Union[ContentBlock, dict]` where ContentBlock is richer than dict
|
||||
# - This is safe because we only call this when we've verified content is a list and
|
||||
# we're doing content block conversions
|
||||
formatted_message.content[idx] = new_block # type: ignore[index, assignment]
|
||||
|
||||
|
||||
def _update_message_content_to_blocks(message: T, output_version: str) -> T:
|
||||
return message.model_copy(
|
||||
update={
|
||||
"content": message.content_blocks,
|
||||
"response_metadata": {
|
||||
**message.response_metadata,
|
||||
"output_version": output_version,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,18 +12,20 @@ from typing import (
|
||||
Callable,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeAlias,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from typing_extensions import TypeAlias, TypedDict, override
|
||||
from typing_extensions import TypedDict, override
|
||||
|
||||
from langchain_core._api import deprecated
|
||||
from langchain_core.caches import BaseCache
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.globals import get_verbose
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AnyMessage,
|
||||
BaseMessage,
|
||||
MessageLikeRepresentation,
|
||||
@@ -101,7 +103,7 @@ def _get_token_ids_default_method(text: str) -> list[int]:
|
||||
LanguageModelInput = Union[PromptValue, str, Sequence[MessageLikeRepresentation]]
|
||||
LanguageModelOutput = Union[BaseMessage, str]
|
||||
LanguageModelLike = Runnable[LanguageModelInput, LanguageModelOutput]
|
||||
LanguageModelOutputVar = TypeVar("LanguageModelOutputVar", BaseMessage, str)
|
||||
LanguageModelOutputVar = TypeVar("LanguageModelOutputVar", AIMessage, str)
|
||||
|
||||
|
||||
def _get_verbosity() -> bool:
|
||||
|
||||
@@ -27,7 +27,10 @@ from langchain_core.callbacks import (
|
||||
Callbacks,
|
||||
)
|
||||
from langchain_core.globals import get_llm_cache
|
||||
from langchain_core.language_models._utils import _normalize_messages
|
||||
from langchain_core.language_models._utils import (
|
||||
_normalize_messages,
|
||||
_update_message_content_to_blocks,
|
||||
)
|
||||
from langchain_core.language_models.base import (
|
||||
BaseLanguageModel,
|
||||
LangSmithParams,
|
||||
@@ -36,16 +39,17 @@ from langchain_core.language_models.base import (
|
||||
from langchain_core.load import dumpd, dumps
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AIMessageChunk,
|
||||
AnyMessage,
|
||||
BaseMessage,
|
||||
BaseMessageChunk,
|
||||
HumanMessage,
|
||||
convert_to_messages,
|
||||
convert_to_openai_image_block,
|
||||
is_data_content_block,
|
||||
message_chunk_to_message,
|
||||
)
|
||||
from langchain_core.messages.ai import _LC_ID_PREFIX
|
||||
from langchain_core.messages.block_translators.openai import (
|
||||
convert_to_openai_image_block,
|
||||
)
|
||||
from langchain_core.output_parsers.openai_tools import (
|
||||
JsonOutputKeyToolsParser,
|
||||
PydanticToolsParser,
|
||||
@@ -69,6 +73,7 @@ from langchain_core.utils.function_calling import (
|
||||
convert_to_openai_tool,
|
||||
)
|
||||
from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass
|
||||
from langchain_core.utils.utils import LC_ID_PREFIX, from_env
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import uuid
|
||||
@@ -129,7 +134,7 @@ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
|
||||
if (
|
||||
block.get("type") == "image"
|
||||
and is_data_content_block(block)
|
||||
and block.get("source_type") != "id"
|
||||
and not ("file_id" in block or block.get("source_type") == "id")
|
||||
):
|
||||
if message_to_trace is message:
|
||||
# Shallow copy
|
||||
@@ -139,6 +144,22 @@ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
|
||||
message_to_trace.content[idx] = ( # type: ignore[index] # mypy confused by .model_copy
|
||||
convert_to_openai_image_block(block)
|
||||
)
|
||||
elif (
|
||||
block.get("type") == "file"
|
||||
and is_data_content_block(block) # v0 (image/audio/file) or v1
|
||||
and "base64" in block
|
||||
# Backward compat: convert v1 base64 blocks to v0
|
||||
):
|
||||
if message_to_trace is message:
|
||||
# Shallow copy
|
||||
message_to_trace = message.model_copy()
|
||||
message_to_trace.content = list(message_to_trace.content)
|
||||
|
||||
message_to_trace.content[idx] = { # type: ignore[index]
|
||||
**{k: v for k, v in block.items() if k != "base64"},
|
||||
"data": block["base64"],
|
||||
"source_type": "base64",
|
||||
}
|
||||
elif len(block) == 1 and "type" not in block:
|
||||
# Tracing assumes all content blocks have a "type" key. Here
|
||||
# we add this key if it is missing, and there's an obvious
|
||||
@@ -221,7 +242,7 @@ def _format_ls_structured_output(ls_structured_output_format: Optional[dict]) ->
|
||||
return ls_structured_output_format_dict
|
||||
|
||||
|
||||
class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
|
||||
"""Base class for chat models.
|
||||
|
||||
Key imperative methods:
|
||||
@@ -330,6 +351,28 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
|
||||
"""
|
||||
|
||||
output_version: Optional[str] = Field(
|
||||
default_factory=from_env("LC_OUTPUT_VERSION", default=None)
|
||||
)
|
||||
"""Version of ``AIMessage`` output format to store in message content.
|
||||
|
||||
``AIMessage.content_blocks`` will lazily parse the contents of ``content`` into a
|
||||
standard format. This flag can be used to additionally store the standard format
|
||||
in message content, e.g., for serialization purposes.
|
||||
|
||||
Supported values:
|
||||
|
||||
- ``"v0"``: provider-specific format in content (can lazily-parse with
|
||||
``.content_blocks``)
|
||||
- ``"v1"``: standardized format in content (consistent with ``.content_blocks``)
|
||||
|
||||
Partner packages (e.g., ``langchain-openai``) can also use this field to roll out
|
||||
new content formats in a backward-compatible way.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
"""
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def raise_deprecation(cls, values: dict) -> Any:
|
||||
@@ -388,21 +431,24 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
stop: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> BaseMessage:
|
||||
) -> AIMessage:
|
||||
config = ensure_config(config)
|
||||
return cast(
|
||||
"ChatGeneration",
|
||||
self.generate_prompt(
|
||||
[self._convert_input(input)],
|
||||
stop=stop,
|
||||
callbacks=config.get("callbacks"),
|
||||
tags=config.get("tags"),
|
||||
metadata=config.get("metadata"),
|
||||
run_name=config.get("run_name"),
|
||||
run_id=config.pop("run_id", None),
|
||||
**kwargs,
|
||||
).generations[0][0],
|
||||
).message
|
||||
"AIMessage",
|
||||
cast(
|
||||
"ChatGeneration",
|
||||
self.generate_prompt(
|
||||
[self._convert_input(input)],
|
||||
stop=stop,
|
||||
callbacks=config.get("callbacks"),
|
||||
tags=config.get("tags"),
|
||||
metadata=config.get("metadata"),
|
||||
run_name=config.get("run_name"),
|
||||
run_id=config.pop("run_id", None),
|
||||
**kwargs,
|
||||
).generations[0][0],
|
||||
).message,
|
||||
)
|
||||
|
||||
@override
|
||||
async def ainvoke(
|
||||
@@ -412,7 +458,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
stop: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> BaseMessage:
|
||||
) -> AIMessage:
|
||||
config = ensure_config(config)
|
||||
llm_result = await self.agenerate_prompt(
|
||||
[self._convert_input(input)],
|
||||
@@ -424,7 +470,9 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
run_id=config.pop("run_id", None),
|
||||
**kwargs,
|
||||
)
|
||||
return cast("ChatGeneration", llm_result.generations[0][0]).message
|
||||
return cast(
|
||||
"AIMessage", cast("ChatGeneration", llm_result.generations[0][0]).message
|
||||
)
|
||||
|
||||
def _should_stream(
|
||||
self,
|
||||
@@ -469,11 +517,11 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
stop: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[BaseMessageChunk]:
|
||||
) -> Iterator[AIMessageChunk]:
|
||||
if not self._should_stream(async_api=False, **{**kwargs, "stream": True}):
|
||||
# Model doesn't implement streaming, so use default implementation
|
||||
# model doesn't implement streaming, so use default implementation
|
||||
yield cast(
|
||||
"BaseMessageChunk",
|
||||
"AIMessageChunk",
|
||||
self.invoke(input, config=config, stop=stop, **kwargs),
|
||||
)
|
||||
else:
|
||||
@@ -518,16 +566,41 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
|
||||
try:
|
||||
input_messages = _normalize_messages(messages)
|
||||
run_id = "-".join((_LC_ID_PREFIX, str(run_manager.run_id)))
|
||||
run_id = "-".join((LC_ID_PREFIX, str(run_manager.run_id)))
|
||||
yielded = False
|
||||
for chunk in self._stream(input_messages, stop=stop, **kwargs):
|
||||
if chunk.message.id is None:
|
||||
chunk.message.id = run_id
|
||||
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
chunk.message = _update_message_content_to_blocks(
|
||||
chunk.message, "v1"
|
||||
)
|
||||
run_manager.on_llm_new_token(
|
||||
cast("str", chunk.message.content), chunk=chunk
|
||||
)
|
||||
chunks.append(chunk)
|
||||
yield chunk.message
|
||||
yield cast("AIMessageChunk", chunk.message)
|
||||
yielded = True
|
||||
|
||||
# Yield a final empty chunk with chunk_position="last" if not yet
|
||||
# yielded
|
||||
if (
|
||||
yielded
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not chunk.message.chunk_position
|
||||
):
|
||||
empty_content: Union[str, list] = (
|
||||
"" if isinstance(chunk.message.content, str) else []
|
||||
)
|
||||
msg_chunk = AIMessageChunk(
|
||||
content=empty_content, chunk_position="last", id=run_id
|
||||
)
|
||||
run_manager.on_llm_new_token(
|
||||
"", chunk=ChatGenerationChunk(message=msg_chunk)
|
||||
)
|
||||
yield msg_chunk
|
||||
except BaseException as e:
|
||||
generations_with_error_metadata = _generate_response_from_error(e)
|
||||
chat_generation_chunk = merge_chat_generation_chunks(chunks)
|
||||
@@ -560,11 +633,11 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
stop: Optional[list[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[BaseMessageChunk]:
|
||||
) -> AsyncIterator[AIMessageChunk]:
|
||||
if not self._should_stream(async_api=True, **{**kwargs, "stream": True}):
|
||||
# No async or sync stream is implemented, so fall back to ainvoke
|
||||
yield cast(
|
||||
"BaseMessageChunk",
|
||||
"AIMessageChunk",
|
||||
await self.ainvoke(input, config=config, stop=stop, **kwargs),
|
||||
)
|
||||
return
|
||||
@@ -611,7 +684,8 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
|
||||
try:
|
||||
input_messages = _normalize_messages(messages)
|
||||
run_id = "-".join((_LC_ID_PREFIX, str(run_manager.run_id)))
|
||||
run_id = "-".join((LC_ID_PREFIX, str(run_manager.run_id)))
|
||||
yielded = False
|
||||
async for chunk in self._astream(
|
||||
input_messages,
|
||||
stop=stop,
|
||||
@@ -620,11 +694,34 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
if chunk.message.id is None:
|
||||
chunk.message.id = run_id
|
||||
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
chunk.message = _update_message_content_to_blocks(
|
||||
chunk.message, "v1"
|
||||
)
|
||||
await run_manager.on_llm_new_token(
|
||||
cast("str", chunk.message.content), chunk=chunk
|
||||
)
|
||||
chunks.append(chunk)
|
||||
yield chunk.message
|
||||
yield cast("AIMessageChunk", chunk.message)
|
||||
yielded = True
|
||||
|
||||
# Yield a final empty chunk with chunk_position="last" if not yet yielded
|
||||
if (
|
||||
yielded
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not chunk.message.chunk_position
|
||||
):
|
||||
empty_content: Union[str, list] = (
|
||||
"" if isinstance(chunk.message.content, str) else []
|
||||
)
|
||||
msg_chunk = AIMessageChunk(
|
||||
content=empty_content, chunk_position="last", id=run_id
|
||||
)
|
||||
await run_manager.on_llm_new_token(
|
||||
"", chunk=ChatGenerationChunk(message=msg_chunk)
|
||||
)
|
||||
yield msg_chunk
|
||||
except BaseException as e:
|
||||
generations_with_error_metadata = _generate_response_from_error(e)
|
||||
chat_generation_chunk = merge_chat_generation_chunks(chunks)
|
||||
@@ -1077,15 +1174,43 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
**kwargs,
|
||||
):
|
||||
chunks: list[ChatGenerationChunk] = []
|
||||
run_id: Optional[str] = (
|
||||
f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
|
||||
)
|
||||
yielded = False
|
||||
for chunk in self._stream(messages, stop=stop, **kwargs):
|
||||
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
chunk.message = _update_message_content_to_blocks(
|
||||
chunk.message, "v1"
|
||||
)
|
||||
if run_manager:
|
||||
if chunk.message.id is None:
|
||||
chunk.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}"
|
||||
chunk.message.id = run_id
|
||||
run_manager.on_llm_new_token(
|
||||
cast("str", chunk.message.content), chunk=chunk
|
||||
)
|
||||
chunks.append(chunk)
|
||||
yielded = True
|
||||
|
||||
# Yield a final empty chunk with chunk_position="last" if not yet yielded
|
||||
if (
|
||||
yielded
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not chunk.message.chunk_position
|
||||
):
|
||||
empty_content: Union[str, list] = (
|
||||
"" if isinstance(chunk.message.content, str) else []
|
||||
)
|
||||
chunk = ChatGenerationChunk(
|
||||
message=AIMessageChunk(
|
||||
content=empty_content, chunk_position="last", id=run_id
|
||||
)
|
||||
)
|
||||
if run_manager:
|
||||
run_manager.on_llm_new_token("", chunk=chunk)
|
||||
chunks.append(chunk)
|
||||
result = generate_from_stream(iter(chunks))
|
||||
elif inspect.signature(self._generate).parameters.get("run_manager"):
|
||||
result = self._generate(
|
||||
@@ -1094,10 +1219,17 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
else:
|
||||
result = self._generate(messages, stop=stop, **kwargs)
|
||||
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
for generation in result.generations:
|
||||
generation.message = _update_message_content_to_blocks(
|
||||
generation.message, "v1"
|
||||
)
|
||||
|
||||
# Add response metadata to each generation
|
||||
for idx, generation in enumerate(result.generations):
|
||||
if run_manager and generation.message.id is None:
|
||||
generation.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
|
||||
generation.message.id = f"{LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
|
||||
generation.message.response_metadata = _gen_info_and_msg_metadata(
|
||||
generation
|
||||
)
|
||||
@@ -1150,15 +1282,43 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
**kwargs,
|
||||
):
|
||||
chunks: list[ChatGenerationChunk] = []
|
||||
run_id: Optional[str] = (
|
||||
f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
|
||||
)
|
||||
yielded = False
|
||||
async for chunk in self._astream(messages, stop=stop, **kwargs):
|
||||
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
chunk.message = _update_message_content_to_blocks(
|
||||
chunk.message, "v1"
|
||||
)
|
||||
if run_manager:
|
||||
if chunk.message.id is None:
|
||||
chunk.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}"
|
||||
chunk.message.id = run_id
|
||||
await run_manager.on_llm_new_token(
|
||||
cast("str", chunk.message.content), chunk=chunk
|
||||
)
|
||||
chunks.append(chunk)
|
||||
yielded = True
|
||||
|
||||
# Yield a final empty chunk with chunk_position="last" if not yet yielded
|
||||
if (
|
||||
yielded
|
||||
and isinstance(chunk.message, AIMessageChunk)
|
||||
and not chunk.message.chunk_position
|
||||
):
|
||||
empty_content: Union[str, list] = (
|
||||
"" if isinstance(chunk.message.content, str) else []
|
||||
)
|
||||
chunk = ChatGenerationChunk(
|
||||
message=AIMessageChunk(
|
||||
content=empty_content, chunk_position="last", id=run_id
|
||||
)
|
||||
)
|
||||
if run_manager:
|
||||
await run_manager.on_llm_new_token("", chunk=chunk)
|
||||
chunks.append(chunk)
|
||||
result = generate_from_stream(iter(chunks))
|
||||
elif inspect.signature(self._agenerate).parameters.get("run_manager"):
|
||||
result = await self._agenerate(
|
||||
@@ -1167,10 +1327,17 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
else:
|
||||
result = await self._agenerate(messages, stop=stop, **kwargs)
|
||||
|
||||
if self.output_version == "v1":
|
||||
# Overwrite .content with .content_blocks
|
||||
for generation in result.generations:
|
||||
generation.message = _update_message_content_to_blocks(
|
||||
generation.message, "v1"
|
||||
)
|
||||
|
||||
# Add response metadata to each generation
|
||||
for idx, generation in enumerate(result.generations):
|
||||
if run_manager and generation.message.id is None:
|
||||
generation.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
|
||||
generation.message.id = f"{LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
|
||||
generation.message.response_metadata = _gen_info_and_msg_metadata(
|
||||
generation
|
||||
)
|
||||
@@ -1443,7 +1610,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
|
||||
*,
|
||||
tool_choice: Optional[Union[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||
) -> Runnable[LanguageModelInput, AIMessage]:
|
||||
"""Bind tools to the model.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import re
|
||||
import time
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from typing import Any, Optional, Union, cast
|
||||
from typing import Any, Literal, Optional, Union, cast
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
455
libs/core/langchain_core/messages/block_translators/anthropic.py
Normal file
455
libs/core/langchain_core/messages/block_translators/anthropic.py
Normal 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()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Derivations of standard content blocks from Bedrock content."""
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
from langchain_core.messages.block_translators.anthropic import (
|
||||
_convert_to_v1_from_anthropic,
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_v1_from_bedrock(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Convert bedrock message content to v1 format."""
|
||||
out = _convert_to_v1_from_anthropic(message)
|
||||
|
||||
content_tool_call_ids = {
|
||||
block.get("id")
|
||||
for block in out
|
||||
if isinstance(block, dict) and block.get("type") == "tool_call"
|
||||
}
|
||||
for tool_call in message.tool_calls:
|
||||
if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:
|
||||
tool_call_block: types.ToolCall = {
|
||||
"type": "tool_call",
|
||||
"id": id_,
|
||||
"name": tool_call["name"],
|
||||
"args": tool_call["args"],
|
||||
}
|
||||
if "index" in tool_call:
|
||||
tool_call_block["index"] = tool_call["index"] # type: ignore[typeddict-item]
|
||||
if "extras" in tool_call:
|
||||
tool_call_block["extras"] = tool_call["extras"] # type: ignore[typeddict-item]
|
||||
out.append(tool_call_block)
|
||||
return out
|
||||
|
||||
|
||||
def _convert_to_v1_from_bedrock_chunk(
|
||||
message: AIMessageChunk,
|
||||
) -> list[types.ContentBlock]:
|
||||
"""Convert bedrock message chunk content to v1 format."""
|
||||
if (
|
||||
message.content == ""
|
||||
and not message.additional_kwargs
|
||||
and not message.tool_calls
|
||||
):
|
||||
# Bedrock outputs multiple chunks containing response metadata
|
||||
return []
|
||||
|
||||
out = _convert_to_v1_from_anthropic(message)
|
||||
|
||||
if (
|
||||
message.tool_call_chunks
|
||||
and not message.content
|
||||
and message.chunk_position != "last" # keep tool_calls if aggregated
|
||||
):
|
||||
for tool_call_chunk in message.tool_call_chunks:
|
||||
tc: types.ToolCallChunk = {
|
||||
"type": "tool_call_chunk",
|
||||
"id": tool_call_chunk.get("id"),
|
||||
"name": tool_call_chunk.get("name"),
|
||||
"args": tool_call_chunk.get("args"),
|
||||
}
|
||||
if (idx := tool_call_chunk.get("index")) is not None:
|
||||
tc["index"] = idx
|
||||
out.append(tc)
|
||||
return out
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message with Bedrock content."""
|
||||
if "claude" not in message.response_metadata.get("model_name", "").lower():
|
||||
raise NotImplementedError # fall back to best-effort parsing
|
||||
return _convert_to_v1_from_bedrock(message)
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
|
||||
"""Derive standard content blocks from a message chunk with Bedrock content."""
|
||||
# TODO: add model_name to all Bedrock chunks and update core merging logic
|
||||
# to not append during aggregation. Then raise NotImplementedError here if
|
||||
# not an Anthropic model to fall back to best-effort parsing.
|
||||
return _convert_to_v1_from_bedrock_chunk(message)
|
||||
|
||||
|
||||
def _register_bedrock_translator() -> None:
|
||||
"""Register the bedrock translator with the central registry.
|
||||
|
||||
Run automatically when the module is imported.
|
||||
"""
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
register_translator,
|
||||
)
|
||||
|
||||
register_translator("bedrock", translate_content, translate_content_chunk)
|
||||
|
||||
|
||||
_register_bedrock_translator()
|
||||
@@ -0,0 +1,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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
47
libs/core/langchain_core/messages/block_translators/groq.py
Normal file
47
libs/core/langchain_core/messages/block_translators/groq.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Derivations of standard content blocks from Groq content."""
|
||||
|
||||
import warnings
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
from langchain_core.messages import content as types
|
||||
|
||||
WARNED = False
|
||||
|
||||
|
||||
def translate_content(message: AIMessage) -> list[types.ContentBlock]: # noqa: ARG001
|
||||
"""Derive standard content blocks from a message with Groq content."""
|
||||
global WARNED # noqa: PLW0603
|
||||
if not WARNED:
|
||||
warning_message = (
|
||||
"Content block standardization is not yet fully supported for Groq."
|
||||
)
|
||||
warnings.warn(warning_message, stacklevel=2)
|
||||
WARNED = True
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]: # noqa: ARG001
|
||||
"""Derive standard content blocks from a message chunk with Groq content."""
|
||||
global WARNED # noqa: PLW0603
|
||||
if not WARNED:
|
||||
warning_message = (
|
||||
"Content block standardization is not yet fully supported for Groq."
|
||||
)
|
||||
warnings.warn(warning_message, stacklevel=2)
|
||||
WARNED = True
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _register_groq_translator() -> None:
|
||||
"""Register the Groq translator with the central registry.
|
||||
|
||||
Run automatically when the module is imported.
|
||||
"""
|
||||
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
||||
register_translator,
|
||||
)
|
||||
|
||||
register_translator("groq", translate_content, translate_content_chunk)
|
||||
|
||||
|
||||
_register_groq_translator()
|
||||
@@ -0,0 +1,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
|
||||
@@ -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
Reference in New Issue
Block a user