Compare commits

..

163 Commits

Author SHA1 Message Date
Mason Daugherty
73ab674a75 Merge branch 'wip-v1.0' into mdrxy/vertexai 2025-10-02 23:29:42 -04:00
ccurme
80a091b9fc Merge branch 'master' into wip-v1.0 2025-10-02 10:35:49 -04:00
Mason Daugherty
2b0c1da2cb feat(ollama): standard content (#33202) 2025-10-02 01:48:27 -04:00
Mason Daugherty
b2188aeb7b for now, don't use factory on reasoning additional kwarg extraction (don't want to inconsistently auto-gen ids) 2025-10-01 23:27:31 -04:00
Mason Daugherty
bea249beff release(core): 1.0.0a6 (#33201) 2025-10-01 22:49:36 -04:00
Mason Daugherty
2d29959386 Merge branch 'master' into wip-v1.0 2025-10-01 22:34:05 -04:00
Mason Daugherty
bb9b802cda fix(ollama): handle ImageContentBlock 2025-10-01 19:38:21 -04:00
Mason Daugherty
4ff83e92c1 fix locks/make filetype optional dep 2025-10-01 19:33:49 -04:00
Mason Daugherty
5f12be149a init 2025-10-01 19:16:37 -04:00
Mason Daugherty
738dc79959 feat(core): genai standard content (#32987) 2025-10-01 19:09:15 -04:00
Mason Daugherty
9ad7f7a0cc Merge branch 'master' into wip-v1.0 2025-10-01 18:48:38 -04:00
ccurme
fa0955ccb1 fix(openai): fix tests on v1 branch (#33189) 2025-10-01 13:25:29 -04:00
Chester Curme
e02acdfe60 Merge branch 'master' into wip-v1.0
# Conflicts:
#	libs/core/langchain_core/version.py
#	libs/core/pyproject.toml
#	libs/core/uv.lock
#	libs/partners/openai/pyproject.toml
#	libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py
#	libs/partners/openai/uv.lock
#	libs/standard-tests/pyproject.toml
#	libs/standard-tests/uv.lock
2025-10-01 11:31:45 -04:00
Mason Daugherty
87e1bbf3b1 Merge branch 'master' into wip-v1.0 2025-09-30 17:55:40 -04:00
Mason Daugherty
e8f76e506f Merge branch 'master' into wip-v1.0 2025-09-30 17:55:23 -04:00
Mason Daugherty
c49d470e13 Merge branch 'master' into wip-v1.0 2025-09-30 15:53:17 -04:00
Mason Daugherty
208e8e8f07 Merge branch 'master' into wip-v1.0 2025-09-30 15:44:24 -04:00
Mason Daugherty
06301c701e lift langgraph version cap, temp comment out optional deps for langchain_v1 2025-09-30 15:41:16 -04:00
ccurme
b7223f45cc release(openai): 1.0.0a3 (#33162) 2025-09-30 12:13:58 -04:00
ccurme
8ba8a5e301 release(anthropic): 1.0.0a2 (#33161) 2025-09-30 12:09:17 -04:00
Chester Curme
16a2d9759b revert changes to release workflow 2025-09-30 11:56:10 -04:00
Chester Curme
0e404adc60 🦍 2025-09-30 11:49:40 -04:00
Chester Curme
ffaedf7cfc fix 2025-09-30 11:29:39 -04:00
Chester Curme
b6ecc0b040 infra(fix): temporarily skip some release checks 2025-09-30 11:28:40 -04:00
ccurme
4d581000ad fix(infra): temporarily skip pre-release checks for alpha branch (#33160)
We have deliberately introduced a breaking change in
https://github.com/langchain-ai/langchain/pull/33021.

Will revert when we have compatible pre-releases for tested packages.
2025-09-30 11:10:06 -04:00
ccurme
06ddc57c7a release(core): 1.0.0a5 (#33158) 2025-09-30 10:37:06 -04:00
Chester Curme
49704ffc19 Merge branch 'master' into wip-v1.0
# Conflicts:
#	libs/partners/anthropic/langchain_anthropic/chat_models.py
#	libs/partners/anthropic/pyproject.toml
#	libs/partners/anthropic/uv.lock
2025-09-30 09:25:19 -04:00
Mason Daugherty
8926986483 chore: standardize translator named params 2025-09-29 16:42:59 -04:00
Mason Daugherty
7f757cf37d fix(core): extras handling, add test 2025-09-29 15:41:49 -04:00
ccurme
c20bd07f16 Merge branch 'master' into wip-v1.0 2025-09-29 12:31:29 -04:00
Mason Daugherty
8f60946d5a more docstrings 2025-09-29 00:52:14 -04:00
Mason Daugherty
790c4a8e43 Merge branch 'master' into wip-v1.0 2025-09-28 23:53:54 -04:00
Mason Daugherty
b83d45bff7 fix: add continue when guarding against v0 blocks 2025-09-28 23:53:11 -04:00
Mason Daugherty
ba663defc2 fix: anthropic and converse docstrings 2025-09-28 23:46:17 -04:00
Mason Daugherty
0633974eb0 conversion logic - add missing non-id cases 2025-09-28 23:46:07 -04:00
Mason Daugherty
4d39cf39ff update docstring/comments 2025-09-28 23:44:28 -04:00
Mason Daugherty
77e52a6c9c add docstring 2025-09-28 23:43:44 -04:00
Mason Daugherty
2af1cb6ca3 update conversion docstrings 2025-09-28 23:43:27 -04:00
Mason Daugherty
54f3a6d9cf add note about PlainTextContentBlock and v0/v1 compat 2025-09-28 22:51:17 -04:00
Mason Daugherty
87e5be1097 feat(core): parse reasoning_content from additional_kwargs 2025-09-26 10:50:12 -04:00
Mason Daugherty
fcd8fdd748 revert: remove accidental symlink 2025-09-25 23:48:26 -04:00
Mason Daugherty
370010d195 Merge branch 'master' into wip-v1.0 2025-09-25 20:57:34 -04:00
Mason Daugherty
adc941d1dc Merge branch 'master' into wip-v1.0 2025-09-25 20:36:59 -04:00
Mason Daugherty
d15514d571 integrations: delete deprecated items 2025-09-25 18:02:08 -04:00
Mason Daugherty
ee94f9567a Merge branch 'master' into wip-v1.0 2025-09-25 01:47:00 -04:00
Mason Daugherty
f3f5b93be6 Merge branch 'master' into wip-v1.0 2025-09-25 01:36:00 -04:00
Mason Daugherty
00565d7bf6 Merge branch 'master' into wip-v1.0 2025-09-25 01:09:44 -04:00
ccurme
2f93566c87 feat: (v1) server tool call and result types (#33021)
- Replaces dedicated types for `WebSearchCall`, `WebSearchResult`,
`CodeInterpreterCall`, `CodeInterpreterResult` with monolithic
`ServerToolCall` and `ServerToolResult`
- Implements `ServerToolCallChunk` for streaming partial arguments
- Full support on Anthropic and OpenAI

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-09-24 23:43:50 -04:00
Mason Daugherty
db8c2d3bae Merge branch 'master' into wip-v1.0 2025-09-24 23:30:23 -04:00
Mason Daugherty
3515a54c10 Merge branch 'master' into wip-v1.0 2025-09-24 16:37:31 -04:00
Mason Daugherty
ccfdce64d3 add note 2025-09-24 16:18:20 -04:00
Mason Daugherty
4ca523a9d6 Merge branch 'master' into wip-v1.0 2025-09-24 16:17:18 -04:00
Mason Daugherty
a4e1a54393 Merge branch 'master' into wip-v1.0 2025-09-24 15:35:44 -04:00
Mason Daugherty
e7cdaad58f Merge branch 'master' into wip-v1.0 2025-09-24 01:07:22 -04:00
Mason Daugherty
36f8c7335c fix: core version equality check 2025-09-23 01:30:50 -04:00
Mason Daugherty
f6cf4fcd6e Merge branch 'master' into wip-v1.0 2025-09-23 01:27:12 -04:00
Mason Daugherty
cfa9973828 Merge branch 'master' into wip-v1.0 2025-09-22 16:30:00 -04:00
Mason Daugherty
e75878bfa2 Merge branch 'master' into wip-v1.0 2025-09-21 00:29:30 -04:00
Mason Daugherty
6081ba9184 fix: pydantic openai (#33037) 2025-09-21 00:21:56 -04:00
Mason Daugherty
0cc6f8bb58 Merge branch 'master' into wip-v1.0 2025-09-20 23:47:53 -04:00
Mason Daugherty
6c90ba1f05 test: fix pydantic issue by bumping OAI-py 2025-09-20 23:13:42 -04:00
Mason Daugherty
ea3b9695e6 Merge branch 'master' into wip-v1.0 2025-09-20 22:59:25 -04:00
Chester Curme
fa4c302463 fix(anthropic): fix web_fetch test 2025-09-19 09:32:30 -04:00
ccurme
b215ed5642 chore(langchain): (v1) drop python 3.9 and relax dependency bounds (#33013) 2025-09-18 15:05:50 -04:00
Chester Curme
c891a51608 fix(infra): permit pre-releases in release workflow 2025-09-18 13:44:54 -04:00
ccurme
82650ea7f1 release(core): 1.0.0a4 (#33011) 2025-09-18 13:01:02 -04:00
ccurme
13964efccf revert: fix(standard-tests): add filename to PDF file block (#33010)
Reverts langchain-ai/langchain#32989 in favor of
https://github.com/langchain-ai/langchain/pull/33009.
2025-09-18 12:48:00 -04:00
ccurme
f247270111 feat(core): (v1) standard content for AWS (#32969)
https://github.com/langchain-ai/langchain-aws/pull/643
2025-09-18 12:47:26 -04:00
ccurme
8be4adccd1 fix(core): (v1) trace PDFs in v0 standard format (#33009) 2025-09-18 11:45:12 -04:00
Mason Daugherty
b6bd507198 Merge branch 'master' into wip-v1.0 2025-09-18 11:44:06 -04:00
Mason Daugherty
3e0d7512ef core: update version.py 2025-09-18 10:58:51 -04:00
Mason Daugherty
53ed770849 Merge branch 'master' into wip-v1.0 2025-09-18 10:57:53 -04:00
Mason Daugherty
e2050e24ef release(core): 1.0.0a3 2025-09-18 10:57:22 -04:00
Mason Daugherty
59bb8bffd1 feat(core): make is_openai_data_block public and add filtering (#32991) 2025-09-17 11:41:41 -04:00
Mason Daugherty
16ec9bc535 fix(core): add back text to data content block check 2025-09-17 11:13:27 -04:00
Mason Daugherty
fda8a71e19 docs: further comments for clarity 2025-09-17 00:57:48 -04:00
Mason Daugherty
8f23bd109b fix: correct var name in comment 2025-09-17 00:39:01 -04:00
Mason Daugherty
ff632c1028 fix(standard-tests): add filename to PDF file block (#32989)
The standard PDF input test was creating file content blocks without a
filename field.

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

---------

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

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

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

## Breaking changes

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

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

## Migration

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

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

@ccurme

---------

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

View File

@@ -136,14 +136,14 @@ def _get_configs_for_single_dir(job: str, dir_: str) -> List[Dict[str, str]]:
if job == "codspeed":
py_versions = ["3.12"] # 3.13 is not yet supported
elif dir_ == "libs/core":
py_versions = ["3.9", "3.10", "3.11", "3.12", "3.13"]
py_versions = ["3.10", "3.11", "3.12", "3.13"]
# custom logic for specific directories
elif dir_ in PY_312_MAX_PACKAGES:
py_versions = ["3.9", "3.12"]
py_versions = ["3.10", "3.12"]
elif dir_ == "libs/langchain" and job == "extended-tests":
py_versions = ["3.9", "3.13"]
py_versions = ["3.10", "3.13"]
elif dir_ == "libs/langchain_v1":
py_versions = ["3.10", "3.13"]
elif dir_ in {"libs/cli"}:
@@ -151,9 +151,9 @@ def _get_configs_for_single_dir(job: str, dir_: str) -> List[Dict[str, str]]:
elif dir_ == ".":
# unable to install with 3.13 because tokenizers doesn't support 3.13 yet
py_versions = ["3.9", "3.12"]
py_versions = ["3.10", "3.12"]
else:
py_versions = ["3.9", "3.13"]
py_versions = ["3.10", "3.13"]
return [{"working-directory": dir_, "python-version": py_v} for py_v in py_versions]

View File

@@ -376,7 +376,6 @@ jobs:
test-prior-published-packages-against-new-core:
# Installs the new core with old partners: Installs the new unreleased core
# alongside the previously published partner packages and runs integration tests
if: github.ref != 'refs/heads/v0.3'
needs:
- build
- release-notes
@@ -432,7 +431,7 @@ jobs:
git ls-remote --tags origin "langchain-${{ matrix.partner }}*" \
| awk '{print $2}' \
| sed 's|refs/tags/||' \
| grep -E '==0\.3\.[0-9]+$' \
| grep -E '[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z]+[0-9]+)?$' \
| sort -Vr \
| head -n 1
)"
@@ -465,6 +464,7 @@ jobs:
- release-notes
- test-pypi-publish
- pre-release-checks
- test-prior-published-packages-against-new-core
runs-on: ubuntu-latest
permissions:
# This permission is used for trusted publishing:

View File

@@ -106,7 +106,7 @@ jobs:
working-directory: langchain
run: |
# Install all partner packages in editable mode with overrides
python -m uv pip install $(ls ./libs/partners | xargs -I {} echo "./libs/partners/{}") --overrides ./docs/vercel_overrides.txt --prerelease=allow
python -m uv pip install $(ls ./libs/partners | xargs -I {} echo "./libs/partners/{}") --overrides ./docs/vercel_overrides.txt
# Install core langchain and other main packages
python -m uv pip install libs/core libs/langchain libs/text-splitters libs/community libs/experimental libs/standard-tests

View File

@@ -5,7 +5,7 @@
# Runs daily. Can also be triggered manually for immediate updates.
name: '⏰ Scheduled Integration Tests'
run-name: "Run Integration Tests - ${{ inputs.working-directory-force || 'all libs' }} (Python ${{ inputs.python-version-force || '3.9, 3.11' }})"
run-name: "Run Integration Tests - ${{ inputs.working-directory-force || 'all libs' }} (Python ${{ inputs.python-version-force || '3.10, 3.13' }})"
on:
workflow_dispatch:
@@ -15,7 +15,7 @@ on:
description: "From which folder this pipeline executes - defaults to all in matrix - example value: libs/partners/anthropic"
python-version-force:
type: string
description: "Python version to use - defaults to 3.9 and 3.11 in matrix - example value: 3.9"
description: "Python version to use - defaults to 3.10 and 3.13 in matrix - example value: 3.11"
schedule:
- cron: '0 13 * * *' # Runs daily at 1PM UTC (9AM EDT/6AM PDT)
@@ -46,9 +46,9 @@ jobs:
PYTHON_VERSION_FORCE: ${{ github.event.inputs.python-version-force || '' }}
run: |
# echo "matrix=..." where matrix is a json formatted str with keys python-version and working-directory
# python-version should default to 3.9 and 3.11, but is overridden to [PYTHON_VERSION_FORCE] if set
# python-version should default to 3.10 and 3.13, but is overridden to [PYTHON_VERSION_FORCE] if set
# working-directory should default to DEFAULT_LIBS, but is overridden to [WORKING_DIRECTORY_FORCE] if set
python_version='["3.9", "3.11"]'
python_version='["3.10", "3.13"]'
working_directory="$DEFAULT_LIBS"
if [ -n "$PYTHON_VERSION_FORCE" ]; then
python_version="[\"$PYTHON_VERSION_FORCE\"]"

View File

@@ -34,7 +34,7 @@ Currently, the build process roughly follows these steps:
The docs site is served by Vercel. The Vercel deployment process copies the HTML
files from the `langchain-api-docs-html` repository and deploys them to the live
site. Deployments are triggered on each new commit pushed to `v0.3`.
site. Deployments are triggered on each new commit pushed to `master`.
#### Build Technical Details

View File

@@ -394,21 +394,3 @@ p {
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
/* Deprecation announcement banner styling */
.bd-header-announcement {
background-color: #790000 !important;
color: white !important;
font-weight: 600;
padding: 0.75rem 1rem;
text-align: center;
}
.bd-header-announcement a {
color: white !important;
text-decoration: underline !important;
}
.bd-header-announcement a:hover {
color: #f0f0f0 !important;
}

View File

@@ -218,7 +218,7 @@ html_theme_options = {
# # Use :html_theme.sidebar_secondary.remove: for file-wide removal
# "secondary_sidebar_items": {"**": ["page-toc", "sourcelink"]},
# "show_version_warning_banner": True,
"announcement": "⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank' style='color: white; text-decoration: underline;'>Visit the new v1.0 docs</a> and new <a href='https://reference.langchain.com/python' target='_blank' style='color: white; text-decoration: underline;'>reference docs</a>",
# "announcement": None,
"icon_links": [
{
# Label for this link

View File

@@ -615,17 +615,12 @@ def _build_index(dirs: List[str]) -> None:
integrations = sorted(dir_ for dir_ in dirs if dir_ not in main_)
doc = """# LangChain Python API Reference
Welcome to the LangChain v0.3 Python API reference. This is a reference for all
Welcome to the LangChain Python API reference. This is a reference for all
`langchain-x` packages.
```{danger}
These pages refer to the the v0.3 versions of LangChain packages and integrations. To
visit the documentation for the latest versions of LangChain, visit [https://docs.langchain.com](https://docs.langchain.com)
and [https://reference.langchain.com/python/](https://reference.langchain.com/python/) (for references.)
For the legacy API reference (<v0.3) hosted on ReadTheDocs see [https://api.python.langchain.com/](https://api.python.langchain.com/).
```
For user guides see [https://python.langchain.com](https://python.langchain.com).
For the legacy API reference hosted on ReadTheDocs see [https://api.python.langchain.com/](https://api.python.langchain.com/).
"""
if main_:

View File

@@ -1,7 +1,3 @@
:::danger
⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>
:::
# Conceptual guide
This guide provides explanations of the key concepts behind the LangChain framework and AI applications more broadly.

View File

@@ -3,10 +3,6 @@ sidebar_position: 0
sidebar_class_name: hidden
---
:::danger
⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>
:::
# How-to guides
Here youll find answers to "How do I….?" types of questions.

View File

@@ -58,7 +58,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": null,
"id": "1fcf7b27-1cc3-420a-b920-0420b5892e20",
"metadata": {},
"outputs": [
@@ -102,7 +102,7 @@
" ],\n",
"}\n",
"response = llm.invoke([message])\n",
"print(response.text())"
"print(response.text)"
]
},
{
@@ -133,7 +133,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "99d27f8f-ae78-48bc-9bf2-3cef35213ec7",
"metadata": {},
"outputs": [
@@ -163,7 +163,7 @@
" ],\n",
"}\n",
"response = llm.invoke([message])\n",
"print(response.text())"
"print(response.text)"
]
},
{
@@ -176,7 +176,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "325fb4ca",
"metadata": {},
"outputs": [
@@ -198,7 +198,7 @@
" ],\n",
"}\n",
"response = llm.invoke([message])\n",
"print(response.text())"
"print(response.text)"
]
},
{
@@ -234,7 +234,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "6c1455a9-699a-4702-a7e0-7f6eaec76a21",
"metadata": {},
"outputs": [
@@ -284,7 +284,7 @@
" ],\n",
"}\n",
"response = llm.invoke([message])\n",
"print(response.text())"
"print(response.text)"
]
},
{
@@ -312,7 +312,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "55e1d937-3b22-4deb-b9f0-9e688f0609dc",
"metadata": {},
"outputs": [
@@ -342,7 +342,7 @@
" ],\n",
"}\n",
"response = llm.invoke([message])\n",
"print(response.text())"
"print(response.text)"
]
},
{
@@ -417,7 +417,7 @@
" ],\n",
"}\n",
"response = llm.invoke([message])\n",
"print(response.text())"
"print(response.text)"
]
},
{
@@ -443,7 +443,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "83593b9d-a8d3-4c99-9dac-64e0a9d397cb",
"metadata": {},
"outputs": [
@@ -488,13 +488,13 @@
" ],\n",
"}\n",
"response = llm.invoke([message])\n",
"print(response.text())\n",
"print(response.text)\n",
"response.usage_metadata"
]
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "9bbf578e-794a-4dc0-a469-78c876ccd4a3",
"metadata": {},
"outputs": [
@@ -530,7 +530,7 @@
" ],\n",
"}\n",
"response = llm.invoke([message, response, next_message])\n",
"print(response.text())\n",
"print(response.text)\n",
"response.usage_metadata"
]
},
@@ -600,7 +600,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": null,
"id": "ae076c9b-ff8f-461d-9349-250f396c9a25",
"metadata": {},
"outputs": [
@@ -641,7 +641,7 @@
" ],\n",
"}\n",
"response = llm.invoke([message])\n",
"print(response.text())"
"print(response.text)"
]
},
{

View File

@@ -54,7 +54,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "5df2e558-321d-4cf7-994e-2815ac37e704",
"metadata": {},
"outputs": [
@@ -75,7 +75,7 @@
"\n",
"chain = prompt | llm\n",
"response = chain.invoke({\"image_url\": url})\n",
"print(response.text())"
"print(response.text)"
]
},
{
@@ -117,7 +117,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "25e4829e-0073-49a8-9669-9f43e5778383",
"metadata": {},
"outputs": [
@@ -144,7 +144,7 @@
" \"cache_type\": \"ephemeral\",\n",
" }\n",
")\n",
"print(response.text())"
"print(response.text)"
]
},
{

View File

@@ -1593,7 +1593,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"id": "30a0af36-2327-4b1d-9ba5-e47cb72db0be",
"metadata": {},
"outputs": [
@@ -1629,7 +1629,7 @@
"response = llm_with_tools.invoke(\n",
" \"There's a syntax error in my primes.py file. Can you help me fix it?\"\n",
")\n",
"print(response.text())\n",
"print(response.text)\n",
"response.tool_calls"
]
},

View File

@@ -243,12 +243,12 @@
"id": "0ef05abb-9c04-4dc3-995e-f857779644d5",
"metadata": {},
"source": [
"You can filter to text using the [.text()](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.ai.AIMessage.html#langchain_core.messages.ai.AIMessage.text) method on the output:"
"You can filter to text using the [.text](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.ai.AIMessage.html#langchain_core.messages.ai.AIMessage.text) property on the output:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": null,
"id": "2a4e743f-ea7d-4e5a-9b12-f9992362de8b",
"metadata": {},
"outputs": [
@@ -262,7 +262,7 @@
],
"source": [
"for chunk in llm.stream(messages):\n",
" print(chunk.text(), end=\"|\")"
" print(chunk.text, end=\"|\")"
]
},
{

View File

@@ -261,7 +261,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": null,
"id": "c5fac0e9-05a4-4fc1-a3b3-e5bbb24b971b",
"metadata": {
"colab": {
@@ -286,7 +286,7 @@
],
"source": [
"async for token in llm.astream(\"Hello, please explain how antibiotics work\"):\n",
" print(token.text(), end=\"\")"
" print(token.text, end=\"\")"
]
},
{

View File

@@ -814,7 +814,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "1f758726-33ef-4c04-8a54-49adb783bbb3",
"metadata": {},
"outputs": [
@@ -860,7 +860,7 @@
"llm_with_tools = llm.bind_tools([tool])\n",
"\n",
"response = llm_with_tools.invoke(\"What is deep research by OpenAI?\")\n",
"print(response.text())"
"print(response.text)"
]
},
{
@@ -1151,7 +1151,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": null,
"id": "073f6010-6b0e-4db6-b2d3-7427c8dec95b",
"metadata": {},
"outputs": [
@@ -1167,7 +1167,7 @@
}
],
"source": [
"response_2.text()"
"response_2.text"
]
},
{
@@ -1198,7 +1198,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": null,
"id": "b6da5bd6-a44a-4c64-970b-30da26b003d6",
"metadata": {},
"outputs": [
@@ -1214,7 +1214,7 @@
}
],
"source": [
"response_2.text()"
"response_2.text"
]
},
{
@@ -1404,7 +1404,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"id": "51d3e4d3-ea78-426c-9205-aecb0937fca7",
"metadata": {},
"outputs": [
@@ -1428,13 +1428,13 @@
"messages = [{\"role\": \"user\", \"content\": first_query}]\n",
"\n",
"response = llm_with_tools.invoke(messages)\n",
"response_text = response.text()\n",
"response_text = response.text\n",
"print(f\"{response_text[:100]}... {response_text[-100:]}\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "b248bedf-2050-4c17-a90e-3a26eeb1b055",
"metadata": {},
"outputs": [
@@ -1460,7 +1460,7 @@
" ]\n",
")\n",
"second_response = llm_with_tools.invoke(messages)\n",
"print(second_response.text())"
"print(second_response.text)"
]
},
{
@@ -1482,7 +1482,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "009e541a-b372-410e-b9dd-608a8052ce09",
"metadata": {},
"outputs": [
@@ -1502,12 +1502,12 @@
" output_version=\"responses/v1\",\n",
")\n",
"response = llm.invoke(\"Hi, I'm Bob.\")\n",
"print(response.text())"
"print(response.text)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "393a443a-4c5f-4a07-bc0e-c76e529b35e3",
"metadata": {},
"outputs": [
@@ -1524,7 +1524,7 @@
" \"What is my name?\",\n",
" previous_response_id=response.response_metadata[\"id\"],\n",
")\n",
"print(second_response.text())"
"print(second_response.text)"
]
},
{
@@ -1589,7 +1589,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "8d322f3a-0732-45ab-ac95-dfd4596e0d85",
"metadata": {},
"outputs": [
@@ -1616,7 +1616,7 @@
"response = llm.invoke(\"What is 3^3?\")\n",
"\n",
"# Output\n",
"response.text()"
"response.text"
]
},
{

View File

@@ -3,10 +3,6 @@ sidebar_position: 0
sidebar_class_name: hidden
---
:::danger
⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>
:::
# Introduction
**LangChain** is a framework for developing applications powered by large language models (LLMs).

View File

@@ -302,7 +302,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"id": "c96c960b",
"metadata": {},
"outputs": [
@@ -320,7 +320,7 @@
"source": [
"query = \"Hi!\"\n",
"response = model.invoke([{\"role\": \"user\", \"content\": query}])\n",
"response.text()"
"response.text"
]
},
{
@@ -351,7 +351,7 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": null,
"id": "b6a7e925",
"metadata": {},
"outputs": [
@@ -371,7 +371,7 @@
"query = \"Hi!\"\n",
"response = model_with_tools.invoke([{\"role\": \"user\", \"content\": query}])\n",
"\n",
"print(f\"Message content: {response.text()}\\n\")\n",
"print(f\"Message content: {response.text}\\n\")\n",
"print(f\"Tool calls: {response.tool_calls}\")"
]
},
@@ -385,7 +385,7 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": null,
"id": "688b465d",
"metadata": {},
"outputs": [
@@ -403,7 +403,7 @@
"query = \"Search for the weather in SF\"\n",
"response = model_with_tools.invoke([{\"role\": \"user\", \"content\": query}])\n",
"\n",
"print(f\"Message content: {response.text()}\\n\")\n",
"print(f\"Message content: {response.text}\\n\")\n",
"print(f\"Tool calls: {response.tool_calls}\")"
]
},
@@ -615,19 +615,12 @@
"## Streaming tokens\n",
"\n",
"In addition to streaming back messages, it is also useful to stream back tokens.\n",
"We can do this by specifying `stream_mode=\"messages\"`.\n",
"\n",
"\n",
"::: note\n",
"\n",
"Below we use `message.text()`, which requires `langchain-core>=0.3.37`.\n",
"\n",
":::"
"We can do this by specifying `stream_mode=\"messages\"`."
]
},
{
"cell_type": "code",
"execution_count": 18,
"execution_count": null,
"id": "63198158-380e-43a3-a2ad-d4288949c1d4",
"metadata": {},
"outputs": [
@@ -651,7 +644,7 @@
"for step, metadata in agent_executor.stream(\n",
" {\"messages\": [input_message]}, config, stream_mode=\"messages\"\n",
"):\n",
" if metadata[\"langgraph_node\"] == \"agent\" and (text := step.text()):\n",
" if metadata[\"langgraph_node\"] == \"agent\" and (text := step.text):\n",
" print(text, end=\"|\")"
]
},

View File

@@ -2,11 +2,6 @@
sidebar_position: 0
sidebar_class_name: hidden
---
:::danger
⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>
:::
# Tutorials
New to LangChain or LLM app development in general? Read this material to quickly get up and running building your first applications.

View File

@@ -1,6 +1,6 @@
# LangChain v0.3
*Last updated: 09.16.2024*
*Last updated: 09.16.24*
## What's changed

View File

@@ -87,7 +87,7 @@ const config = {
({
docs: {
editUrl:
"https://github.com/langchain-ai/langchain/edit/v0.3/docs/",
"https://github.com/langchain-ai/langchain/edit/master/docs/",
sidebarPath: require.resolve("./sidebars.js"),
remarkPlugins: [
[require("@docusaurus/remark-plugin-npm2yarn"), { sync: true }],
@@ -142,8 +142,8 @@ const config = {
respectPrefersColorScheme: true,
},
announcementBar: {
content: "⚠️ THESE DOCS ARE OUTDATED. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the new v1.0 docs</a>",
backgroundColor: "#790000ff",
content: "These docs will be deprecated and no longer maintained with the release of LangChain v1.0 in October 2025. <a href='https://docs.langchain.com/oss/python/langchain/overview' target='_blank'>Visit the v1.0 alpha docs</a>",
backgroundColor: "#FFAE42",
},
prism: {
theme: {

View File

@@ -16,13 +16,14 @@ fi
if { \
[ "$VERCEL_ENV" == "production" ] || \
[ "$VERCEL_GIT_COMMIT_REF" == "master" ] || \
[ "$VERCEL_GIT_COMMIT_REF" == "v0.1" ] || \
[ "$VERCEL_GIT_COMMIT_REF" == "v0.2" ] || \
[ "$VERCEL_GIT_COMMIT_REF" == "v0.3rc" ]; \
} && [ "$VERCEL_GIT_REPO_OWNER" == "langchain-ai" ]
then
echo "✅ Production build - proceeding with build"
exit 1
echo "✅ Production build - proceeding with build"
exit 1
fi

View File

@@ -102,7 +102,7 @@ def _is_relevant_import(module: str) -> bool:
"langchain",
"langchain_core",
"langchain_community",
"langchain_experimental",
# "langchain_experimental",
"langchain_text_splitters",
]
return module.split(".")[0] in recognized_packages

View File

@@ -204,7 +204,7 @@ def get_vectorstore_table():
"similarity_search_with_score": True,
"asearch": True,
"Passes Standard Tests": True,
"Multi Tenancy": True,
"Multi Tenancy": False,
"Local/Cloud": "Local",
"IDs in add Documents": True,
},

View File

@@ -40,15 +40,15 @@ const FEATURE_TABLES = {
"apiLink": "https://python.langchain.com/api_reference/mistralai/chat_models/langchain_mistralai.chat_models.ChatMistralAI.html"
},
{
"name": "ChatAIMLAPI",
"package": "langchain-aimlapi",
"link": "aimlapi/",
"structured_output": true,
"tool_calling": true,
"json_mode": true,
"multimodal": true,
"local": false,
"apiLink": "https://python.langchain.com/api_reference/aimlapi/chat_models/langchain_aimlapi.chat_models.ChatAIMLAPI.html"
"name": "ChatAIMLAPI",
"package": "langchain-aimlapi",
"link": "aimlapi/",
"structured_output": true,
"tool_calling": true,
"json_mode": true,
"multimodal": true,
"local": false,
"apiLink": "https://python.langchain.com/api_reference/aimlapi/chat_models/langchain_aimlapi.chat_models.ChatAIMLAPI.html"
},
{
"name": "ChatFireworks",
@@ -1199,7 +1199,7 @@ const FEATURE_TABLES = {
searchWithScore: true,
async: true,
passesStandardTests: false,
multiTenancy: true,
multiTenancy: false,
local: true,
idsInAddDocuments: true,
},
@@ -1230,17 +1230,17 @@ const FEATURE_TABLES = {
idsInAddDocuments: true,
},
{
name: "PGVectorStore",
link: "pgvectorstore",
deleteById: true,
filtering: true,
searchByVector: true,
searchWithScore: true,
async: true,
passesStandardTests: true,
multiTenancy: false,
local: true,
idsInAddDocuments: true,
name: "PGVectorStore",
link: "pgvectorstore",
deleteById: true,
filtering: true,
searchByVector: true,
searchWithScore: true,
async: true,
passesStandardTests: true,
multiTenancy: false,
local: true,
idsInAddDocuments: true,
},
{
name: "PineconeVectorStore",

436
docs/static/llms.txt vendored Normal file
View File

@@ -0,0 +1,436 @@
# LangChain
## High level
[Why LangChain?](https://python.langchain.com/docs/concepts/why_langchain/): considering using LangChain, when building complex AI applications, and when needing to evaluate AI applications This page discusses the main reasons to use LangChain: standardized component interfaces, orchestration capabilities, and observability/evaluation through LangSmith
[Architecture](https://python.langchain.com/docs/concepts/architecture/): needing an overview of the LangChain architecture, exploring the various packages and components, or deciding which parts to use for a specific application. Provides a high-level overview of the different packages that make up the LangChain framework, including langchain-core, langchain, integration packages, langchain-community, langgraph, langserve, and LangSmith.
## Concepts
[Chat Models](https://python.langchain.com/docs/concepts/chat_models/): building applications using chat models, learning about chat model interfaces and features, or interested in integrating chat models with external tools and services. Provides an overview of chat models in LangChain, including their features, integration options, interfaces, tool calling, structured outputs, multimodality, context windows, and advanced topics like rate-limiting and caching.
[Messages](https://python.langchain.com/docs/concepts/messages/): querying LangChain's chat message format, understanding different message types, building chat applications. Messages are the unit of communication in chat models, representing input/output with roles, content, metadata. Covers SystemMessage, HumanMessage, AIMessage, AIMessageChunk, ToolMessage, RemoveMessage, and legacy FunctionMessage.
[Chat history](https://python.langchain.com/docs/concepts/chat_history/): dealing with chat history, managing chat context, or understanding conversation patterns. Covers chat history structure, conversation patterns between user/assistant/tools, and guidelines for managing chat history to stay within context window.
[Tools](https://python.langchain.com/docs/concepts/tools/): needing an overview of tools in LangChain, wanting to create custom tools, or learning how to pass runtime values to tools. Tools are a way to encapsulate functions with schemas that can be passed to chat models supporting tool calling. The page covers the tool interface, creating tools using the @tool decorator, configuring tool schemas, tool artifacts, special type annotations like InjectedToolArg, and toolkits.
[tool calling](https://python.langchain.com/docs/concepts/tool_calling/): needing to understand how to enable tool calling functionality, how to create tools from functions, how to bind tools to a model that supports tool calling. The page covers the key concepts of tool calling, including tool creation using decorators, tool binding to models, tool calling by models, and tool execution. It provides an overview, recommended usage, and best practices.
[structured outputs](https://python.langchain.com/docs/concepts/structured_outputs/): it needs to return output in a structured format, when working with databases or APIs that require structured data, or when building applications with structured responses. Covers structured output concepts like schema definition and methods like tool calling and JSON mode, as well as helper functions, to instruct models to produce structured outputs conforming to a given schema.
[Memory](https://langchain-ai.github.io/langgraph/concepts/memory/): developing agents with memory capabilities, implementing memory management strategies, or learning about different types of memory for AI agents. Covers topics related to short-term and long-term memory for agents, techniques for managing conversation history and summarizing past conversations, different types of memory (semantic, episodic, procedural), and approaches for writing memories in the hot path or in the background.
[Multimodality](https://python.langchain.com/docs/concepts/multimodality/): needing to understand multimodal capabilities, using chat models with multimodal inputs, or using multimodal retrieval/embeddings. Discusses ability of LangChain components like chat models, embedding models, and vector stores to handle multimodal data like text, images, audio, video. Covers current status and limitations around multimodal inputs and outputs for chat models.
[invoke](https://python.langchain.com/docs/concepts/runnables/): learning how to use the Runnable interface, when working with custom Runnables, and when needing to configure Runnables at runtime. The page covers the Runnable interface, its methods for invocation, batching, streaming, inspecting schemas, and configuration. It explains RunnableConfig, custom Runnables, and configurable Runnables.
[stream](https://python.langchain.com/docs/concepts/streaming/): [building applications that use streaming, building applications that need to display partial results in real-time, building applications that need to provide updates on pipeline or workflow progress] 'This page covers streaming in LangChain, including what can be streamed in LLM applications, the streaming APIs available, how to write custom data to the stream, and how LangChain automatically enables streaming for chat models in certain cases.'
[LCEL](https://python.langchain.com/docs/concepts/lcel/): needing an overview of the LangChain Expression Language (LCEL), deciding whether to use LCEL or not, and understanding how to compose chains using LCEL primitives. Provides an overview of the LCEL, a declarative approach to building chains from existing Runnables, covering its benefits, composition primitives like RunnableSequence and RunnableParallel, the composition syntax, automatic type coercion, and guidance on when to use LCEL versus alternatives like LangGraph.
[Document Loaders](https://python.langchain.com/docs/concepts/document_loaders/): needing to load data from various sources like files, webpages, or databases, or when handling large datasets with lazy loading. Document loaders help load data from different sources into a standardized Document object format, with options for lazy loading of large datasets.
[Retrieval](https://python.langchain.com/docs/concepts/retrieval/): building retrieval systems, understanding query analysis, integrating with databases This page covers key concepts and techniques in retrieval systems, including query analysis (re-writing and construction), vector and lexical indexes, databases, and LangChain's unified retriever interface.
[Text Splitters](https://python.langchain.com/docs/concepts/text_splitters/): working with long documents, handling limited model input sizes, or optimizing retrieval systems This page discusses different strategies for splitting large texts into smaller chunks, including length-based, text structure-based, document structure-based, and semantic meaning-based approaches.
[Embedding Models](https://python.langchain.com/docs/concepts/embedding_models/): LLM should read this page when: 1) Working with text embeddings for search/retrieval 2) Comparing text similarity using embedding vectors 3) Selecting or integrating text embedding models It covers key concepts of embedding models: converting text to numerical vectors, measuring similarity between vectors, embedding models (historical context, interface, integrations), and common similarity metrics (cosine, Euclidean, dot product).
[Vector stores](https://python.langchain.com/docs/concepts/vectorstores/): LLM should read this page when: 1) Building applications that need to index and retrieve information based on semantic similarity 2) Integrating vector databases into their application 3) Exploring advanced vector search and retrieval techniques Vector stores are specialized data stores that enable indexing and retrieving information based on vector representations (embeddings) of data, allowing semantic similarity search over unstructured data like text, images, and audio. The page covers vector store integrations, the core interface, adding/deleting documents, basic and advanced similarity search techniques, and concepts like metadata filtering.
[Retrievers](https://python.langchain.com/docs/concepts/retrievers/): building a retrieval system, integrating different retrieval sources, or linking retrieved information to source documents. This page outlines the retriever interface in LangChain, common types of retrievers such as vector stores and search APIs, and advanced retrieval patterns like ensembling and retaining source document information.
[Retrieval Augmented Generation (RAG)](https://python.langchain.com/docs/concepts/rag/): developing applications that incorporate retrieval and generation, building question-answering systems with external data sources, or optimizing knowledge retrieval and integration into language models. Covers the concept of Retrieval Augmented Generation (RAG), which combines retrieval systems with language models to utilize external knowledge, access up-to-date information, leverage domain-specific expertise, reduce hallucination, and integrate knowledge cost-effectively.
[Agents](https://python.langchain.com/docs/concepts/agents/): building AI agents or systems that take high-level tasks and perform a series of actions to accomplish them, transitioning from the legacy AgentExecutor to the newer and more flexible LangGraph system. Provides an overview of agents in LangChain, the legacy AgentExecutor concept, resources for using AgentExecutor, and guidance on migrating to the preferred LangGraph architecture for building customizable agents.
[Prompt Templates](https://python.langchain.com/docs/concepts/prompt_templates/): creating prompts for language models, formatting chat messages, slotting messages into specific locations in a prompt. This page covers different types of prompt templates (string, chat, messages placeholder) for formatting prompts for language models and chat models.
[Output Parsers](https://python.langchain.com/docs/concepts/output_parsers/): looking for ways to extract structured data from model outputs, parsing model outputs into different formats, or handling errors in parsing. Covers various LangChain output parsers like JSON, XML, CSV, Pandas DataFrame, along with capabilities like output fixing, retrying, and using user-defined formats.
[Few-shot prompting](https://python.langchain.com/docs/concepts/few_shot_prompting/): needing to improve model performance, when deciding how to format few-shot examples, when selecting examples for few-shot prompting The page covers generating examples, number of examples, selecting examples, and formatting examples for few-shot prompting with language models.
[Example Selectors](https://python.langchain.com/docs/concepts/example_selectors/): selecting examples for few-shot prompting, dynamically choosing examples for prompts, or understanding different example selection techniques. The page covers example selectors, which are classes responsible for selecting and formatting examples to include as part of prompts for improved performance with few-shot learning.
[Async programming](https://python.langchain.com/docs/concepts/async/): building asynchronous applications with LangChain, working with async runnables, or handling async API calls. Explains LangChain's asynchronous APIs, delegation to sync methods, performance considerations, compatibility with asyncio, and usage in Jupyter notebooks.
[Callbacks](https://python.langchain.com/docs/concepts/callbacks/): [needing to log, monitor, or stream events in an LLM application] [This page covers LangChain's callback system, which allows hooking into various stages of an LLM application for logging, monitoring, streaming, and other purposes. It explains the different callback events, callback handlers, and how to pass callbacks.]
[Tracing](https://python.langchain.com/docs/concepts/tracing/): tracing the steps of a chain/agent for debugging, understanding the chain's flow, or inspecting intermediary outputs. Discusses the concept of tracing in LangChain, including that traces contain runs which are individual steps, and that tracing provides observability into chains/agents.
[Evaluation](https://python.langchain.com/docs/concepts/evaluation/): evaluating the performance of LLM-powered applications, creating or curating datasets, defining metrics for evaluation This page covers the concept of evaluation in LangChain, including using LangSmith to create datasets, define metrics, track results over time, and run evaluations automatically.
[Testing](https://python.langchain.com/docs/concepts/testing/): testing LangChain components, implementing unit tests, or setting up integration tests This page explains unit tests, integration tests, and standard tests in LangChain, including code examples
## How-to guides
### Installation
[How to: install LangChain packages](https://python.langchain.com/docs/how_to/installation/): installing LangChain packages, learning about the LangChain ecosystem packages, installing specific ecosystem packages This page explains how to install the main LangChain package, as well as different ecosystem packages like langchain-core, langchain-community, langchain-openai, langchain-experimental, langgraph, langserve, langchain-cli, and langsmith SDK.
[How to: use LangChain with different Pydantic versions](https://python.langchain.com/docs/how_to/pydantic_compatibility/): needing to use LangChain with different Pydantic versions, needing to install Pydantic 2 with LangChain, or avoiding using the pydantic.v1 namespace with LangChain APIs. The page explains that LangChain 0.3 uses Pydantic 2 internally and advises users to install Pydantic 2 and avoid using the pydantic.v1 namespace with LangChain APIs.
[How to: return structured data from a model](https://python.langchain.com/docs/how_to/structured_output/): LLM should read this page when: 1) wanting to return structured data from a model, 2) building applications that require structured outputs, 3) exploring techniques for parsing model outputs into objects or schemas. This page covers methods for obtaining structured outputs from language models, including using .with_structured_output(), prompting techniques with output parsers, and handling complex schemas with few-shot examples.
[How to: use chat models to call tools](https://python.langchain.com/docs/how_to/tool_calling/): needing to call tools from chat models, wanting to use chat models to generate structured output, or doing extraction from text using chat models. Explains how to define tool schemas as Python functions, Pydantic/TypedDict classes, or LangChain Tools; bind them to chat models; retrieve tool calls from LLM responses; and optionally parse tool calls into structured objects.
[How to: stream runnables](https://python.langchain.com/docs/how_to/streaming/): Line 1: 'wanting to learn how to stream LLM responses, stream intermediate steps, and configure streaming events.' Line 2: 'This page covers how to use the `stream` and `astream` methods to stream final outputs, how to use `astream_events` to stream both final outputs and intermediate steps, filtering events, propagating callbacks for streaming, and working with input streams.'
[How to: debug your LLM apps](https://python.langchain.com/docs/how_to/debugging/): debugging LLM applications, adding print statements, or logging events for tracing. Covers setting verbose mode to print important events, debug mode to print all events, and using LangSmith for visualizing event traces.
### Components
These are the core building blocks you can use when building applications.
#### Chat models
[Chat Models](https://python.langchain.com/docs/concepts/chat_models/): building applications using chat models, learning about chat model interfaces and features, or interested in integrating chat models with external tools and services. Provides an overview of chat models in LangChain, including their features, integration options, interfaces, tool calling, structured outputs, multimodality, context windows, and advanced topics like rate-limiting and caching.
[here](https://python.langchain.com/docs/integrations/chat/): integrating chat models into an application, using chat models for conversational AI tasks, or choosing between different chat model providers. Provides an overview of chat models integrated with LangChain, including OpenAI, Anthropic, Google, and others. Covers key features like tool calling, structured output, JSON mode, local usage, and multimodal support.
[How to: use chat models to call tools](https://python.langchain.com/docs/how_to/tool_calling/): needing to call tools from chat models, wanting to use chat models to generate structured output, or doing extraction from text using chat models. Explains how to define tool schemas as Python functions, Pydantic/TypedDict classes, or LangChain Tools; bind them to chat models; retrieve tool calls from LLM responses; and optionally parse tool calls into structured objects.
[How to: get models to return structured output](https://python.langchain.com/docs/how_to/structured_output/): wanting to obtain structured output from an LLM, needing to parse JSON/XML/YAML output from an LLM, or looking to use few-shot examples with structured outputs. This page covers using the `.with_structured_output()` method to obtain structured data from LLMs, prompting techniques to elicit structured outputs, and parsing structured outputs.
[How to: cache model responses](https://python.langchain.com/docs/how_to/chat_model_caching/): needing to cache ChatModel responses for efficiency, needing to reduce API calls for cost savings, or during development. This page covers how to use an in-memory cache or a SQLite database for caching ChatModel responses, which can improve performance and reduce costs.
[How to: get log probabilities](https://python.langchain.com/docs/how_to/logprobs/): Line 1: 'seeking to get token-level log probabilities from OpenAI chat models, when needing to understand how log probabilities are represented in LangChain' Line 2: 'Explains how to configure OpenAI chat models to return token log probabilities, and how these are included in the response metadata and streamed responses.'
[How to: create a custom chat model class](https://python.langchain.com/docs/how_to/custom_chat_model/): creating a custom chat model class, integrating a new language model as a chat model, or implementing streaming for a chat model. This page explains how to create a custom chat model class by inheriting from BaseChatModel, and implementing methods like _generate and _stream. It covers handling inputs, messages, streaming, identifying parameters, and contributing custom chat models.
[How to: stream a response back](https://python.langchain.com/docs/how_to/chat_streaming/): LLM should read this page when: 1) It needs to stream chat model responses token-by-token 2) It needs to understand how to use the astream() and astream_events() methods for chat models 3) It wants to see examples of streaming chat model responses synchronously and asynchronously This page explains how to stream chat model responses token-by-token using the astream() and astream_events() methods, and provides examples for synchronous and asynchronous streaming with chat models that support this feature.
[How to: track token usage](https://python.langchain.com/docs/how_to/chat_token_usage_tracking/): tracking token usage for chat models, determining costs of using chat models, implementing token usage tracking in applications. Provides methods to track token usage from OpenAI and Anthropic chat models through AIMessage.usage_metadata, callbacks, and using LangSmith. Covers streaming token usage and aggregating usage across multiple calls.
[How to: track response metadata across providers](https://python.langchain.com/docs/how_to/response_metadata/): needing to access metadata from model responses, wanting to get information like token usage or log probabilities, or checking safety ratings Explains how to access response metadata from various chat model providers like OpenAI, Anthropic, Vertex AI, etc. Shows code examples of retrieving metadata like token usage, log probabilities, and safety ratings.
[How to: use chat models to call tools](https://python.langchain.com/docs/how_to/tool_calling/): needing to call tools from chat models, wanting to use chat models to generate structured output, or doing extraction from text using chat models. Explains how to define tool schemas as Python functions, Pydantic/TypedDict classes, or LangChain Tools; bind them to chat models; retrieve tool calls from LLM responses; and optionally parse tool calls into structured objects.
[How to: stream tool calls](https://python.langchain.com/docs/how_to/tool_streaming/): Line 1: 'wanting to stream tool calls, when needing to handle partial tool call data, or when needing to accumulate tool call chunks' Line 2: 'This page explains how to stream tool calls, merge message chunks to accumulate tool call chunks, and parse tool calls from accumulated chunks, with code examples.'
[How to: handle rate limits](https://python.langchain.com/docs/how_to/chat_model_rate_limiting/): handling rate limits from model providers, running many parallel queries to a model, benchmarking a chat model. The page explains how to initialize and use an in-memory rate limiter with chat models to limit the number of requests made per unit time.
[How to: few shot prompt tool behavior](https://python.langchain.com/docs/how_to/tools_few_shot/): using few-shot examples to improve tool calling, demonstrating how to incorporate example queries and responses into the prompt. The page explains how to create few-shot prompts including examples of tool usage, allowing the model to learn from these demonstrations to improve its ability to correctly call tools for math operations or other tasks.
[How to: bind model-specific formatted tools](https://python.langchain.com/docs/how_to/tools_model_specific/): binding model-specific tools, binding OpenAI tool schemas, invoking model-specific tools This page explains how to bind model-specific tool schemas directly to an LLM, with an example using the OpenAI tool schema format.
[How to: force models to call a tool](https://python.langchain.com/docs/how_to/tool_choice/): needing to force an LLM to call a specific tool, needing to force an LLM to call at least one tool This page shows how to use the tool_choice parameter to force an LLM to call a specific tool or to call at least one tool from a set of available tools.
[How to: work with local models](https://python.langchain.com/docs/how_to/local_llms/): [running LLMs locally on a user's device, using open-source LLMs, utilizing custom prompts with LLMs] [Overview of open-source LLMs and frameworks for running inference locally, instructions for setting up and using local LLMs (Ollama, llama.cpp, GPT4All, llamafile), guidance on formatting prompts for specific LLMs, potential use cases for local LLMs.]
[How to: init any model in one line](https://python.langchain.com/docs/how_to/chat_models_universal_init/): initializing chat models for different model providers, creating a configurable chat model, inferring the model provider from the model name. The page explains how to initialize any LLM chat model integration in one line using the init_chat_model() helper, create a configurable chat model with default or custom parameters, and infer the model provider based on the model name.
#### Messages
[Messages](https://python.langchain.com/docs/concepts/messages/): querying LangChain's chat message format, understanding different message types, building chat applications. Messages are the unit of communication in chat models, representing input/output with roles, content, metadata. Covers SystemMessage, HumanMessage, AIMessage, AIMessageChunk, ToolMessage, RemoveMessage, and legacy FunctionMessage.
[How to: manage large chat history](https://python.langchain.com/docs/how_to/trim_messages/): working with long chat histories, when concerned about token limits for chat models, when implementing token management strategies. This page explains how to use the trim_messages utility to reduce the size of a chat message history to fit within token limits, covering trimming by token count or message count, and allowing customization of trimming strategies.
[How to: filter messages](https://python.langchain.com/docs/how_to/filter_messages/): needing to filter messages by type, id, or name when working with message histories, when using chains/agents that pass message histories between components. Provides instructions and examples for filtering message lists (e.g. to only include human messages) using the filter_messages utility, including basic usage, chaining with models, and API reference.
[How to: merge consecutive messages of the same type](https://python.langchain.com/docs/how_to/merge_message_runs/): it needs to merge consecutive messages of the same type for a particular model, when it wants to compose the merge_message_runs utility with other components in a chain, or when it needs to invoke the merge_message_runs utility imperatively. The page explains how to use the merge_message_runs utility to merge consecutive messages of the same type, provides examples of using it in chains or invoking it directly, and links to the API reference for more details.
#### Prompt templates
[Prompt Templates](https://python.langchain.com/docs/concepts/prompt_templates/): creating prompts for language models, formatting chat messages, slotting messages into specific locations in a prompt. This page covers different types of prompt templates (string, chat, messages placeholder) for formatting prompts for language models and chat models.
[How to: use few shot examples](https://python.langchain.com/docs/how_to/few_shot_examples/): creating few-shot prompts, using example selectors, providing examples to large language models This page explains how to use few-shot examples to provide context to language models, including creating formatters, constructing example sets, using example selectors like SemanticSimilarityExampleSelector, and creating FewShotPromptTemplates.
[How to: use few shot examples in chat models](https://python.langchain.com/docs/how_to/few_shot_examples_chat/): LLM should read this page when: 1) wanting to provide a few-shot example to fine-tune a chat model's output, 2) needing to dynamically select examples from a larger set based on semantic similarity to the input This page covers how to provide few-shot examples to chat models using either fixed examples or dynamically selecting examples from a vectorstore based on semantic similarity to the input.
[How to: partially format prompt templates](https://python.langchain.com/docs/how_to/prompts_partial/): needing to partially format prompt templates, wanting to pass partial strings to templates, or needing to pass functions returning strings to templates. Explains how to partially format prompt templates by passing in a subset of required values as strings or functions that return strings, to create a new template expecting only remaining values.
[How to: compose prompts together](https://python.langchain.com/docs/how_to/prompts_composition/): needing to compose prompts from various prompt components, working with chat prompts, or using the PipelinePromptTemplate class. This page explains how to concatenate different prompt templates together to build larger prompts, covering both string prompts and chat prompts, as well as using the PipelinePromptTemplate to reuse prompt components.
#### Example selectors
[Example Selectors](https://python.langchain.com/docs/concepts/example_selectors/): selecting examples for few-shot prompting, dynamically choosing examples for prompts, or understanding different example selection techniques. The page covers example selectors, which are classes responsible for selecting and formatting examples to include as part of prompts for improved performance with few-shot learning.
[How to: use example selectors](https://python.langchain.com/docs/how_to/example_selectors/): needing to select example prompts for few-shot learning, when having many examples to choose from, or when creating a custom example selector. Explains how to use example selectors in LangChain to select which examples to include in a prompt, covering built-in selectors like similarity and providing a custom example selector.
[How to: select examples by length](https://python.langchain.com/docs/how_to/example_selectors_length_based/): selecting examples for few-shot prompting, handling long examples that may exceed context window, and dynamically including the appropriate number of examples. This page explains how to use the LengthBasedExampleSelector to select examples based on their length, including fewer examples for longer inputs to avoid exceeding the context window.
[How to: select examples by semantic similarity](https://python.langchain.com/docs/how_to/example_selectors_similarity/): selecting relevant examples for few-shot prompting, building example-based systems, finding relevant reference cases This page covers how to select examples by similarity to the input using embedding-based semantic search over a vector store.
[How to: select examples by semantic ngram overlap](https://python.langchain.com/docs/how_to/example_selectors_ngram/): selecting relevant examples to include in few-shot prompts, determining relevancy through n-gram overlap scores, and customizing example selection thresholds. Explains how to use the NGramOverlapExampleSelector to select and order examples based on n-gram overlap with the input text, including setting thresholds and dynamically adding examples.
[How to: select examples by maximal marginal relevance](https://python.langchain.com/docs/how_to/example_selectors_mmr/): needing to select few-shot examples optimizing for both similarity to inputs and diversity from each other, working with example-based prompting for fewshot learning. Demonstrates how to use the MaxMarginalRelevanceExampleSelector, which selects examples by maximizing relevance to inputs while also optimizing for diversity between selected examples, contrasting it with just selecting by similarity.
[How to: select examples from LangSmith few-shot datasets](https://python.langchain.com/docs/how_to/example_selectors_langsmith/): [learning how to use LangSmith datasets for few-shot example selection, dynamically creating few-shot prompts from LangSmith data, integrating LangSmith with LangChain chains] [The page covers setting up LangSmith, querying LangSmith datasets for similar examples, and using those examples in a LangChain chain to create dynamic few-shot prompts for chat models.]
#### LLMs
[LLMs](https://python.langchain.com/docs/concepts/text_llms/): needing an overview of string-based language models, learning about legacy models in LangChain, or comparing string-based models to chat models. Covers LangChain's support for older language models that take strings as input and output, distinguishing them from newer chat models; advises using chat models where possible.
[How to: cache model responses](https://python.langchain.com/docs/how_to/llm_caching/): it needs to cache responses to save money and time, learn about caching in LangChain. LangChain provides an optional caching layer for LLMs to save money and time by reducing API calls for repeated requests. Examples show caching with InMemoryCache and SQLiteCache.
[How to: create a custom LLM class](https://python.langchain.com/docs/how_to/custom_llm/): creating a custom LLM class, wrapping their own LLM provider, integrating with a new language model not yet supported by LangChain. This page explains how to create a custom LLM class by implementing the required _call and _llm_type methods, as well as optional methods like _identifying_params, _acall, _stream, and _astream. It provides an example implementation, demonstrates testing and integration with LangChain APIs, and offers guidance for contributing custom LLM integrations.
[How to: stream a response back](https://python.langchain.com/docs/how_to/streaming_llm/): it needs to stream responses from an LLM, when it needs to work with async streaming from LLMs, when it needs to stream events from an LLM. This page shows how to stream responses token-by-token from LLMs using both sync and async methods, as well as how to stream events from LLMs asynchronously.
[How to: track token usage](https://python.langchain.com/docs/how_to/llm_token_usage_tracking/): tracking token usage for LLM calls, managing costs for an LLM application, or calculating costs based on token counts. The page covers how to track token usage using LangSmith, OpenAI callback handlers, and handling streaming contexts; it also summarizes limitations with legacy models for streaming.
[How to: work with local models](https://python.langchain.com/docs/how_to/local_llms/): [running LLMs locally on a user's device, using open-source LLMs, utilizing custom prompts with LLMs] [Overview of open-source LLMs and frameworks for running inference locally, instructions for setting up and using local LLMs (Ollama, llama.cpp, GPT4All, llamafile), guidance on formatting prompts for specific LLMs, potential use cases for local LLMs.]
#### Output parsers
[Output Parsers](https://python.langchain.com/docs/concepts/output_parsers/): looking for ways to extract structured data from model outputs, parsing model outputs into different formats, or handling errors in parsing. Covers various LangChain output parsers like JSON, XML, CSV, Pandas DataFrame, along with capabilities like output fixing, retrying, and using user-defined formats.
[How to: parse text from message objects](https://python.langchain.com/docs/how_to/output_parser_string/): needing to parse text from message objects, needing to extract text from chat model responses, or working with structured output formats. This page explains how to use the StrOutputParser to extract text from message objects, regardless of the underlying content format, such as text, multimodal data, or structured output.
[How to: use output parsers to parse an LLM response into structured format](https://python.langchain.com/docs/how_to/output_parser_structured/): [needing to parse LLM output into structured data, needing to stream partially parsed structured outputs, using LCEL with output parsers] 'Explains how to use output parsers like PydanticOutputParser to parse LLM text responses into structured formats like Python objects, and how to integrate them with prompts, models, and LCEL streaming.'
[How to: parse JSON output](https://python.langchain.com/docs/how_to/output_parser_json/): LLM should read this page when: 1) Prompting a language model to return JSON output 2) Parsing JSON output from a language model 3) Streaming partial JSON objects from a language model 'This page explains how to use the JsonOutputParser to specify a desired JSON schema, prompt a language model to generate output conforming to that schema, and parse the model's response as JSON. It covers using JsonOutputParser with and without Pydantic, streaming partial JSON objects, and provides code examples.'
[How to: parse XML output](https://python.langchain.com/docs/how_to/output_parser_xml/): needing to parse XML output from a model, when outputting prompts with XML formatting instructions for models, when streaming partial XML results This page shows how to use the XMLOutputParser to parse model output in XML format, including adding XML formatting instructions to prompts and streaming partial XML output
[How to: parse YAML output](https://python.langchain.com/docs/how_to/output_parser_yaml/): LLM should read this page when: 1) Needing to generate YAML output conforming to a specific schema 2) Incorporating YAML output into a larger prompt/chain 3) Parsing YAML output returned by an LLM 'This page explains how to use the YamlOutputParser to parse YAML output from language models, allowing the output to conform to a predefined schema. It covers setting up the parser, constructing prompts with formatting instructions, and chaining the parser with a model.'
[How to: retry when output parsing errors occur](https://python.langchain.com/docs/how_to/output_parser_retry/): [attempting to parse and handle partial or error LLM outputs, troubleshooting output parsing failures, implementing retry logic for parsing] [Explains how to use the RetryOutputParser to handle parsing errors by reprompting the LLM, provides examples for using it with OpenAI models and chaining it with other runnables.]
[How to: try to fix errors in output parsing](https://python.langchain.com/docs/how_to/output_parser_fixing/): needing to handle improperly formatted outputs, attempting to fix formatting issues using an LLM, or parsing outputs that do not conform to a predefined schema. Explains how to use the OutputFixingParser, which wraps another parser and attempts to fix formatting errors by consulting an LLM when the original parser fails.
[How to: write a custom output parser class](https://python.langchain.com/docs/how_to/output_parser_custom/): Line 1: 'creating a custom output parser, implementing a custom parser by inheriting from base classes, or parsing raw model outputs' Line 2: 'Covers how to create custom output parsers using runnable lambdas/generators (recommended) or by inheriting from base parser classes like BaseOutputParser and BaseGenerationOutputParser. Includes examples for simple and more complex parsing scenarios.'
#### Document loaders
[Document Loaders](https://python.langchain.com/docs/concepts/document_loaders/): needing to load data from various sources like files, webpages, or databases, or when handling large datasets with lazy loading. Document loaders help load data from different sources into a standardized Document object format, with options for lazy loading of large datasets.
- [How to: load PDF files](https://python.langchain.com/docs/how_to/document_loader_pdf/)
[How to: load web pages](https://python.langchain.com/docs/how_to/document_loader_web/): LLM should read this page when: - It needs to load and process web pages for question answering or other applications - It needs guidance on using web page content with LangChain 'The page covers how to load web pages into LangChain's Document format, including simple text extraction and advanced parsing of page structure. It demonstrates tools like WebBaseLoader and UnstructuredLoader, and shows how to perform operations like vector search over loaded web content.'
[How to: load CSV data](https://python.langchain.com/docs/how_to/document_loader_csv/): loading CSV files into a sequence of documents, customizing CSV parsing and loading, specifying a column to identify the document source This page explains how to load CSV files into a sequence of Document objects using LangChain's CSVLoader, including customizing the parsing, specifying a source column, and loading from a string.
[How to: load data from a directory](https://python.langchain.com/docs/how_to/document_loader_directory/): loading documents from a file system, handling various file encodings, or using custom document loaders. Shows how to load files from directories using the DirectoryLoader, handle encoding errors, use multithreading, and customize the loader class.
[How to: load HTML data](https://python.langchain.com/docs/how_to/document_loader_html/): loading HTML documents, parsing HTML files with specialized tools, or extracting text from HTML. This page covers how to load HTML documents into LangChain Document objects using Unstructured and BeautifulSoup4, with code examples and API references provided.
[How to: load JSON data](https://python.langchain.com/docs/how_to/document_loader_json/): loading JSON or JSON Lines data into LangChain Documents, or extracting metadata from JSON data. This page explains how to use the JSONLoader to convert JSON and JSONL data into LangChain Documents, including how to extract specific fields into the content and metadata, and provides examples for common JSON structures.
[How to: load Markdown data](https://python.langchain.com/docs/how_to/document_loader_markdown/): needing to load Markdown files, needing to retain Markdown elements, needing to parse Markdown into components This page covers how to load Markdown files into LangChain documents, including retaining elements like titles and lists, and parsing Markdown into components.
[How to: load Microsoft Office data](https://python.langchain.com/docs/how_to/document_loader_office_file/): loading Microsoft Office files (DOCX, XLSX, PPTX) into LangChain, when working with Azure AI Document Intelligence. It covers how to use the AzureAIDocumentIntelligenceLoader to load Office documents into LangChain Documents for further processing.
[How to: write a custom document loader](https://python.langchain.com/docs/how_to/document_loader_custom/): Line 1: 'creating a custom document loader, working with files, or using the GenericLoader abstraction' Line 2: 'This page explains how to create a custom document loader, work with files using BaseBlobParser and Blob, and use the GenericLoader to combine a BlobLoader with a BaseBlobParser.'
#### Text splitters
[Text Splitters](https://python.langchain.com/docs/concepts/text_splitters/): working with long documents, handling limited model input sizes, or optimizing retrieval systems This page discusses different strategies for splitting large texts into smaller chunks, including length-based, text structure-based, document structure-based, and semantic meaning-based approaches.
[How to: recursively split text](https://python.langchain.com/docs/how_to/recursive_text_splitter/): splitting long text into smaller chunks, processing text from languages without word boundaries like Chinese or Japanese, parsing documents for downstream tasks. Covers how to recursively split text by list of characters like newlines and spaces, and options to customize characters for different languages. Discusses chunk size, overlap, and creating LangChain Document objects.
[How to: split HTML](https://python.langchain.com/docs/how_to/split_html/): needing to split HTML content into chunks, preserving semantic structure for better context during processing Explains different techniques to split HTML pages like HTMLHeaderTextSplitter, HTMLSectionSplitter, HTMLSemanticPreservingSplitter; covers preserving tables, lists, custom handlers
[How to: split by character](https://python.langchain.com/docs/how_to/character_text_splitter/): needing to split text by individual characters, needing to control chunk size by character count, needing to handle text with differing chunk sizes. Explains how to split text into chunks by character count, using the CharacterTextSplitter. Covers setting chunk size, overlap, and passing metadata.
[How to: split code](https://python.langchain.com/docs/how_to/code_splitter/): needing to split code into logical chunks, working with code from specific programming languages, or creating language-specific text splitters. Provides examples of using the RecursiveCharacterTextSplitter to split code from various programming languages like Python, JavaScript, Markdown, and others into document chunks based on language-specific separators.
[How to: split Markdown by headers](https://python.langchain.com/docs/how_to/markdown_header_metadata_splitter/): splitting markdown files into chunks, handling headers and metadata in markdown files, constraining chunk sizes in markdown files. This page covers how to split markdown files by headers into chunks, handle metadata associated with headers, and constrain chunk sizes using other text splitters like RecursiveCharacterTextSplitter.
[How to: recursively split JSON](https://python.langchain.com/docs/how_to/recursive_json_splitter/): splitting JSON data into smaller chunks, managing chunk sizes from list content within JSON data. Explains how to split JSON data into smaller chunks while keeping nested objects intact, control chunk sizes, and handle JSON lists by converting them to dictionaries before splitting.
[How to: split text into semantic chunks](https://python.langchain.com/docs/how_to/semantic-chunker/): building an application that needs to split long text into smaller chunks based on semantic meaning, when working with large documents that need to be broken down into semantically coherent sections, or when needing to control the granularity of text splitting. This page explains how to use the SemanticChunker from LangChain to split text into semantically coherent chunks by leveraging embedding models, with options to control the splitting behavior based on percentile, standard deviation, interquartile range, or gradient of embedding distance.
[How to: split by tokens](https://python.langchain.com/docs/how_to/split_by_token/): LLM should read this page when: 1) Splitting long text into chunks while counting tokens 2) Handling non-English languages for text splitting 3) Comparing different tokenizers for text splitting 'The page covers how to split text into chunks based on token count using different tokenizers like tiktoken, spaCy, SentenceTransformers, NLTK, KoNLPY (for Korean), and Hugging Face tokenizers. It explains the approaches, usage, and API references for each tokenizer.'
#### Embedding models
[Embedding Models](https://python.langchain.com/docs/concepts/embedding_models/): LLM should read this page when: 1) Working with text embeddings for search/retrieval 2) Comparing text similarity using embedding vectors 3) Selecting or integrating text embedding models It covers key concepts of embedding models: converting text to numerical vectors, measuring similarity between vectors, embedding models (historical context, interface, integrations), and common similarity metrics (cosine, Euclidean, dot product).
[supported integrations](https://python.langchain.com/docs/integrations/text_embedding/): looking for integrations with embedding models, wanting to compare embedding providers, needing guidance on selecting an embedding model This page documents integrations with various model providers that allow using embeddings in LangChain, covering OpenAI, Azure, Google, AWS, HuggingFace, and other embedding services.
[How to: embed text data](https://python.langchain.com/docs/how_to/embed_text/): it needs to embed text into vectors, when it needs to use text embeddings for tasks like semantic search, and when it needs to understand the interface for text embedding models. This page explains how to use LangChain's Embeddings class to interface with various text embedding model providers, embed documents and queries, and work with the resulting vector representations of text.
[How to: cache embedding results](https://python.langchain.com/docs/how_to/caching_embeddings/): caching document embeddings to improve performance, caching query embeddings to improve performance, or choosing a data store for caching embeddings. This page covers how to use the CacheBackedEmbeddings class to cache document and query embeddings in a ByteStore, demonstrating its usage with a local file store and an in-memory store. It also explains how to specify the cache namespace to avoid collisions.
[How to: create a custom embeddings class](https://python.langchain.com/docs/how_to/custom_embeddings/): needing to use a custom text embedding model, integrating a new text embedding provider, or contributing a new text embedding integration. The page covers implementing custom text embedding models for LangChain by following the Embeddings interface, providing examples, testing, and contributing guidelines.
#### Vector stores
[Vector stores](https://python.langchain.com/docs/concepts/vectorstores/): LLM should read this page when: 1) Building applications that need to index and retrieve information based on semantic similarity 2) Integrating vector databases into their application 3) Exploring advanced vector search and retrieval techniques Vector stores are specialized data stores that enable indexing and retrieving information based on vector representations (embeddings) of data, allowing semantic similarity search over unstructured data like text, images, and audio. The page covers vector store integrations, the core interface, adding/deleting documents, basic and advanced similarity search techniques, and concepts like metadata filtering.
[supported integrations](https://python.langchain.com/docs/integrations/vectorstores/): Line 1: 'integrating vector stores into applications, deciding which vector store to use, or understanding the capabilities of different vector stores' Line 2: 'This page provides an overview of vector stores, which are used to store embedded data and perform similarity search. It lists the different vector stores integrated with LangChain, along with their key features and capabilities.'
[How to: use a vector store to retrieve data](https://python.langchain.com/docs/how_to/vectorstores/): building applications that require searching over large collections of text, when indexing and retrieving relevant information based on similarity between embeddings, and when working with vector databases and embeddings. The page covers how to create and query vector stores, which are used to store embedded vectors of text and search for similar embeddings. It explains how to initialize different vector store options like Chroma, FAISS, and LanceDB, and how to perform similarity searches on them. It also touches on asynchronous operations with vector stores.
#### Retrievers
[Retrievers](https://python.langchain.com/docs/concepts/retrievers/): building a retrieval system, integrating different retrieval sources, or linking retrieved information to source documents. This page outlines the retriever interface in LangChain, common types of retrievers such as vector stores and search APIs, and advanced retrieval patterns like ensembling and retaining source document information.
[How to: use a vector store to retrieve data](https://python.langchain.com/docs/how_to/vectorstore_retriever/): using vector stores for retrieval, implementing maximum marginal relevance retrieval, or specifying additional search parameters. This page explains how to create a retriever from a vector store, how to use maximum marginal relevance retrieval, and how to pass parameters like similarity score thresholds and top-k results.
[How to: generate multiple queries to retrieve data for](https://python.langchain.com/docs/how_to/MultiQueryRetriever/): Line 1: 'improving retrieval results for search queries, retrieving documents from a vector database, or using an LLM to generate multiple queries for a given input' Line 2: 'Explains how to use MultiQueryRetriever to automatically generate multiple queries from an input question using an LLM, retrieve documents for each query, and take the unique union of results to improve retrieval performance.'
[How to: use contextual compression to compress the data retrieved](https://python.langchain.com/docs/how_to/contextual_compression/): [it needs to retrieve relevant information from a large corpus of documents, it needs to filter out irrelevant content from retrieved documents, it needs to compress or shorten documents to focus on query-relevant content] This page discusses contextual compression, a technique that allows retrieving only relevant portions of documents given a query, using various methods like LLM-based extractors/filters, embedding similarity filters, or combinations thereof via pipelines.
[How to: write a custom retriever class](https://python.langchain.com/docs/how_to/custom_retriever/): learning how to create a custom retriever, when implementing custom retrieval logic, when adding retrieval capabilities to an application. Explains how to implement a custom Retriever class by extending BaseRetriever, including providing examples and guidelines for contributing custom retrievers.
[How to: add similarity scores to retriever results](https://python.langchain.com/docs/how_to/add_scores_retriever/): needing to incorporate similarity/relevance scores from retrievers, using vector or multi-vector retrievers, or propagating scores through custom retriever subclasses Shows how to add similarity scores from retrievers like Vector Store Retrievers, SelfQueryRetriever, and MultiVectorRetriever to the metadata of retrieved documents
[How to: combine the results from multiple retrievers](https://python.langchain.com/docs/how_to/ensemble_retriever/): combining results from multiple retriever algorithms, leveraging different retrieval strengths, or using a hybrid search approach. The page explains how to use the EnsembleRetriever to combine results from sparse and dense retrievers, outlines basic usage, and demonstrates runtime configuration of individual retrievers.
[How to: reorder retrieved results to mitigate the "lost in the middle" effect](https://python.langchain.com/docs/how_to/long_context_reorder/): looking to improve performance of RAG applications, mitigating the "lost in the middle" effect, reordering retrieved results for longer contexts. Explains how to reorder retrieved documents to position the most relevant at the beginning and end, with less relevant in the middle, helping surface important information for language models.
[How to: generate multiple embeddings per document](https://python.langchain.com/docs/how_to/multi_vector/): needing to retrieve documents using multiple vector embeddings per document, when working with long documents that need to be split into chunks, when using document summaries for retrieval. This page covers how to index documents using 1) document chunks, 2) summaries generated with an LLM, and 3) hypothetical questions generated with an LLM. It demonstrates the usage of the MultiVectorRetriever to retrieve parent documents based on vector embeddings of chunks/summaries/questions.
[How to: retrieve the whole document for a chunk](https://python.langchain.com/docs/how_to/parent_document_retriever/): [1) wanting to retrieve larger documents instead of just smaller chunks for context, 2) trying to balance keeping context while splitting long documents] [The page explains how to use the ParentDocumentRetriever, which first splits documents into small chunks for indexing but then retrieves the larger parent documents those chunks came from during retrieval. It shows code examples for retrieving full documents as well as larger chunks rather than full documents.]
[How to: generate metadata filters](https://python.langchain.com/docs/how_to/self_query/): needing to perform retrieval on documents based on semantic similarity to the query text and metadata filters, integrating the retrieval into a question-answering pipeline. Covers creating a Self Query Retriever which can perform semantic text retrieval and structured metadata filtering in one step, using an underlying vector store and a query constructor LLM chain to parse natural language queries into structured representations.
[How to: create a time-weighted retriever](https://python.langchain.com/docs/how_to/time_weighted_vectorstore/): it needs to retrieve documents from a vector store considering both semantic similarity and time decay, it needs to simulate time for testing purposes, or it needs to adjust the balance between semantic similarity and recency in retrieving documents. This page explains how to use the TimeWeightedVectorStoreRetriever, which combines semantic similarity scores from a vector store with a time decay factor that reduces the relevance of older documents over time, and provides examples of using different decay rates and mocking time for testing.
[How to: use hybrid vector and keyword retrieval](https://python.langchain.com/docs/how_to/hybrid/): LLM should read this page when: 1) It needs to perform hybrid search combining vector and other search techniques 2) It uses a vectorstore that supports hybrid search capabilities Explains how to configure and invoke LangChain chains to leverage hybrid search features of vectorstores like Astra DB, ElasticSearch, etc.
#### Indexing
Indexing is the process of keeping your vectorstore in-sync with the underlying data source.
[How to: reindex data to keep your vectorstore in-sync with the underlying data source](https://python.langchain.com/docs/how_to/indexing/): needing to index documents into a vector store, handling content deduplication and document mutations over time, or cleaning up old/deleted documents from the store. Covers the LangChain indexing API workflow, including deletion modes, using document loaders, and setting source metadata for documents to handle mutations and deletions properly.
#### Tools
[Tools](https://python.langchain.com/docs/concepts/tools/): needing an overview of tools in LangChain, wanting to create custom tools, or learning how to pass runtime values to tools. Tools are a way to encapsulate functions with schemas that can be passed to chat models supporting tool calling. The page covers the tool interface, creating tools using the @tool decorator, configuring tool schemas, tool artifacts, special type annotations like InjectedToolArg, and toolkits.
[How to: define a custom tool](https://python.langchain.com/docs/how_to/custom_tools/): creating custom tools for agents, converting functions or runnables to tools, or subclassing BaseTool. This page covers creating tools from functions using the @tool decorator or StructuredTool class, creating tools from Runnables, subclassing BaseTool for custom tools, creating async tools, handling tool errors, and returning artifacts from tool execution.
[How to: use built-in tools and toolkits](https://python.langchain.com/docs/how_to/tools_builtin/): needing to use built-in LangChain tools or toolkits, needing to customize built-in LangChain tools. This page covers how to use LangChain's built-in tools and toolkits, including customizing tool names, descriptions, and argument schemas. It also explains how to use LangChain toolkits, which are collections of tools for specific tasks.
[How to: use chat models to call tools](https://python.langchain.com/docs/how_to/tool_calling/): needing to call tools from chat models, wanting to use chat models to generate structured output, or doing extraction from text using chat models. Explains how to define tool schemas as Python functions, Pydantic/TypedDict classes, or LangChain Tools; bind them to chat models; retrieve tool calls from LLM responses; and optionally parse tool calls into structured objects.
[How to: pass tool outputs to chat models](https://python.langchain.com/docs/how_to/tool_results_pass_to_model/): 1) integrating tools with chat models, 2) implementing tool calling functionality, 3) passing tool outputs back to chat models. Demonstrates how to pass tool function outputs back to chat models as tool messages, allowing the model to incorporate tool results in generating a final response.
[How to: pass run time values to tools](https://python.langchain.com/docs/how_to/tool_runtime/): it needs to pass runtime values to tools, when it needs to prevent an LLM from generating certain tool arguments, and when it needs to inject arguments directly at runtime. This page explains how to use the InjectedToolArg annotation to mark certain parameters of a Tool as being injected at runtime, preventing the LLM from generating those arguments. It also shows how to inject the arguments at runtime and create a tool-executing chain.
[How to: add a human-in-the-loop for tools](https://python.langchain.com/docs/how_to/tools_human/): adding human approval to tool calling, allowing human intervention in a workflow, or setting up fail-safes for sensitive operations. This page demonstrates how to add a human-in-the-loop step to approve or reject tool calls made by an LLM in a tool-calling chain using LangChain.
[How to: handle tool errors](https://python.langchain.com/docs/how_to/tools_error/): needing to handle errors that occur when tools are called by an LLM, when building fault tolerance into tool-calling chains, or when enabling self-correction for tool calling errors. The page covers strategies like try/except for tool calls, fallbacks to different models, retrying with exceptions passed to the LLM, and creating custom tool exceptions.
[How to: force models to call a tool](https://python.langchain.com/docs/how_to/tool_choice/): needing to force an LLM to call a specific tool, needing to force an LLM to call at least one tool This page shows how to use the tool_choice parameter to force an LLM to call a specific tool or to call at least one tool from a set of available tools.
[How to: disable parallel tool calling](https://python.langchain.com/docs/how_to/tool_calling_parallel/): considering disabling parallel tool calling, when looking for examples on parallel vs. single tool calls, when trying to control the number of tool calls made. Explains how to disable parallel tool calling in LangChain so that only one tool is called at a time, providing code examples.
[How to: access the `RunnableConfig` from a tool](https://python.langchain.com/docs/how_to/tool_configure/): accessing or configuring runtime behavior of sub-runnables from a custom tool, streaming events from child runnables within a tool This page explains how to access the RunnableConfig from within a custom tool to configure sub-invocations and stream events from those sub-invocations
[How to: stream events from a tool](https://python.langchain.com/docs/how_to/tool_stream_events/): Line 1: 'it needs to stream events from a tool, when it needs to configure tools to access internal runnables, or when it needs to propagate configurations to child runnables in async environments' Line 2: 'Guide on how to stream events from tools that call chat models, retrievers, or other runnables, by accessing internal events and propagating configurations, with examples and explanations for compatibility across Python versions'
[How to: return artifacts from a tool](https://python.langchain.com/docs/how_to/tool_artifacts/): returning structured data from a tool, passing artifacts to downstream components, handling custom data types from tools This page explains how tools can return artifacts separate from model input, allowing custom objects, dataframes, or images to be passed to downstream components while limiting model exposure.
[How to: convert Runnables to tools](https://python.langchain.com/docs/how_to/convert_runnable_to_tool/): Line 1: 'needing to convert a Python function or Runnable into a LangChain tool, when building an agent that calls external tools, or when integrating a custom tool into a chat model' Line 2: 'Demonstrates how to use the Runnable.as_tool() method to convert a Runnable to a tool with a name, description, and arguments schema. Includes examples of agents calling tools created from Runnables.'
[How to: add ad-hoc tool calling capability to models](https://python.langchain.com/docs/how_to/tools_prompting/): LLM should read this page when: 1) Adding ad-hoc tool calling capability to chat models/LLMs, 2) Using models not fine-tuned for tool calling, 3) Invoking custom tools from LLMs 'This guide demonstrates how to create prompts that instruct LLMs to request tool invocations, parse the LLM output to extract tool and arguments, invoke the requested tool, and return the tool output.'
[How to: pass runtime secrets to a runnable](https://python.langchain.com/docs/how_to/runnable_runtime_secrets/): needing to pass sensitive data to a runnable, ensuring secrets remain hidden from tracing, or integrating secret values with runnables. Explains how to pass runtime secrets to runnables using RunnableConfig, allowing certain keys to be hidden from tracing while still being accessible during invocation.
#### Multimodal
[How to: pass multimodal data directly to models](https://python.langchain.com/docs/how_to/multimodal_inputs/): needing to pass multimodal data (images, videos, etc.) to models, when working with models that support multimodal input and tool calling capabilities, and when looking to understand how to encode and pass different types of multimodal data. This page demonstrates how to pass multimodal input like images directly to LLMs and chat models, covering encoding techniques, passing single/multiple images, and invoking models with image/multimodal content. It also shows how to use multimodal models for tool calling.
[How to: use multimodal prompts](https://python.langchain.com/docs/how_to/multimodal_prompts/): wanting to pass multimodal data like images to an LLM, when wanting to send multiple pieces of multimodal data to an LLM, when wanting instructions on how to format multimodal prompts. This shows how to use prompt templates to format multimodal inputs like images to models that support it, including sending multiple images, and comparing images.
#### Agents
:::note
[LangGraph](https://langchain-ai.github.io/langgraph/): learning about LangGraph, considering using LangGraph for an AI application, or deciding between LangGraph and alternatives. Overview of LangGraph as an open-source framework for building AI agents, its key features like reliability and customizability, its ecosystem integration with other LangChain products, and additional learning resources.
:::
[How to: use legacy LangChain Agents (AgentExecutor)](https://python.langchain.com/docs/how_to/agent_executor/): building agents with specific tools, when working with chat history, when using language models for tool calling. This page explains how to build agents with AgentExecutor that can call tools like search engines and retrievers, how to add chat history to agents, and how to use language models to determine which tools to call.
[How to: migrate from legacy LangChain agents to LangGraph](https://python.langchain.com/docs/how_to/migrate_agent/): LLM should read this page when: 1) Migrating from legacy LangChain agents to LangGraph 2) Comparing the functionality of LangChain and LangGraph agents This page provides a detailed guide on migrating from legacy LangChain agents to LangGraph agents, covering topics such as basic usage, prompt templates, memory handling, iterating through steps, dealing with intermediate steps, setting iteration and execution time limits, early stopping methods, and trimming intermediate steps.
#### Callbacks
[Callbacks](https://python.langchain.com/docs/concepts/callbacks/): [needing to log, monitor, or stream events in an LLM application] [This page covers LangChain's callback system, which allows hooking into various stages of an LLM application for logging, monitoring, streaming, and other purposes. It explains the different callback events, callback handlers, and how to pass callbacks.]
[How to: pass in callbacks at runtime](https://python.langchain.com/docs/how_to/callbacks_runtime/): needing to pass callback handlers at runtime to capture events, needing to attach handlers to nested objects This page explains how to pass callback handlers at runtime when invoking a runnable, which allows capturing events from all nested objects without manually attaching handlers.
[How to: attach callbacks to a module](https://python.langchain.com/docs/how_to/callbacks_attach/): attaching callbacks to a runnable, reusing callbacks across multiple executions, composing a chain of runnables This page explains how to attach callbacks to a runnable using the .with_config() method, allowing callbacks to be reused across multiple executions and propagated to child components in a chain of runnables.
[How to: pass callbacks into a module constructor](https://python.langchain.com/docs/how_to/callbacks_constructor/): LLM should read this page when: 1) Implementing callbacks in LangChain, 2) Understanding the scope of constructor callbacks, 3) Deciding whether to use constructor or runtime callbacks 'This page explains how to pass callbacks into the constructor of LangChain objects, and that constructor callbacks are scoped only to the object they are defined on, not inherited by child objects.'
[How to: create custom callback handlers](https://python.langchain.com/docs/how_to/custom_callbacks/): creating custom behavior for LangChain components, customizing callback events, implementing event handlers This page explains how to create custom callback handlers by implementing callback methods and attaching the handler to LangChain components
[How to: use callbacks in async environments](https://python.langchain.com/docs/how_to/callbacks_async/): needing to use callbacks in async environments, handling sync callbacks in async methods, using AsyncCallbackHandler Covers using callbacks with async APIs, avoiding blocking with AsyncCallbackHandler, propagating callbacks in async runnables, example of sync and async callback handlers
[How to: dispatch custom callback events](https://python.langchain.com/docs/how_to/callbacks_custom_events/): dispatching custom callback events, handling async or sync custom callback events, or consuming custom events via the astream events API. This page covers how to dispatch custom callback events from within a Runnable, consume these events via async/sync callback handlers, and access custom events through the astream events API.
#### Custom
All of LangChain components can easily be extended to support your own versions.
[How to: create a custom chat model class](https://python.langchain.com/docs/how_to/custom_chat_model/): creating a custom chat model class, integrating a new language model as a chat model, or implementing streaming for a chat model. This page explains how to create a custom chat model class by inheriting from BaseChatModel, and implementing methods like _generate and _stream. It covers handling inputs, messages, streaming, identifying parameters, and contributing custom chat models.
[How to: create a custom LLM class](https://python.langchain.com/docs/how_to/custom_llm/): creating a custom LLM class, wrapping their own LLM provider, integrating with a new language model not yet supported by LangChain. This page explains how to create a custom LLM class by implementing the required _call and _llm_type methods, as well as optional methods like _identifying_params, _acall, _stream, and _astream. It provides an example implementation, demonstrates testing and integration with LangChain APIs, and offers guidance for contributing custom LLM integrations.
[How to: create a custom embeddings class](https://python.langchain.com/docs/how_to/custom_embeddings/): needing to use a custom text embedding model, integrating a new text embedding provider, or contributing a new text embedding integration. The page covers implementing custom text embedding models for LangChain by following the Embeddings interface, providing examples, testing, and contributing guidelines.
[How to: write a custom retriever class](https://python.langchain.com/docs/how_to/custom_retriever/): learning how to create a custom retriever, when implementing custom retrieval logic, when adding retrieval capabilities to an application. Explains how to implement a custom Retriever class by extending BaseRetriever, including providing examples and guidelines for contributing custom retrievers.
[How to: write a custom document loader](https://python.langchain.com/docs/how_to/document_loader_custom/): Line 1: 'creating a custom document loader, working with files, or using the GenericLoader abstraction' Line 2: 'This page explains how to create a custom document loader, work with files using BaseBlobParser and Blob, and use the GenericLoader to combine a BlobLoader with a BaseBlobParser.'
[How to: write a custom output parser class](https://python.langchain.com/docs/how_to/output_parser_custom/): Line 1: 'creating a custom output parser, implementing a custom parser by inheriting from base classes, or parsing raw model outputs' Line 2: 'Covers how to create custom output parsers using runnable lambdas/generators (recommended) or by inheriting from base parser classes like BaseOutputParser and BaseGenerationOutputParser. Includes examples for simple and more complex parsing scenarios.'
[How to: create custom callback handlers](https://python.langchain.com/docs/how_to/custom_callbacks/): creating custom behavior for LangChain components, customizing callback events, implementing event handlers This page explains how to create custom callback handlers by implementing callback methods and attaching the handler to LangChain components
[How to: define a custom tool](https://python.langchain.com/docs/how_to/custom_tools/): creating custom tools for agents, converting functions or runnables to tools, or subclassing BaseTool. This page covers creating tools from functions using the @tool decorator or StructuredTool class, creating tools from Runnables, subclassing BaseTool for custom tools, creating async tools, handling tool errors, and returning artifacts from tool execution.
[How to: dispatch custom callback events](https://python.langchain.com/docs/how_to/callbacks_custom_events/): dispatching custom callback events, handling async or sync custom callback events, or consuming custom events via the astream events API. This page covers how to dispatch custom callback events from within a Runnable, consume these events via async/sync callback handlers, and access custom events through the astream events API.
#### Serialization
[How to: save and load LangChain objects](https://python.langchain.com/docs/how_to/serialization/): needing to save and reload LangChain objects, handle API keys securely when serializing/deserializing objects, and maintain compatibility when deserializing objects across different versions of LangChain. This page discusses how to save and load serializable LangChain objects like chains, messages, and documents using the dump/load functions, which separate API keys and ensure cross-version compatibility. Examples are provided for serializing/deserializing to JSON strings, Python dicts, and disk files.
## Use cases
These guides cover use-case specific details.
### Q&A with RAG
Retrieval Augmented Generation (RAG) is a way to connect LLMs to external sources of data.
[this guide](https://python.langchain.com/docs/tutorials/rag/): building a retrieval-augmented question-answering system, when needing to index and search through unstructured data sources, when learning about key concepts like document loaders, text splitters, vector stores, and retrievers. This tutorial covers how to build a Q&A application over textual data by loading documents, splitting them into chunks, embedding and storing the chunks in a vector store, retrieving relevant chunks for a user query, and generating an answer using a language model with the retrieved context.
[How to: add chat history](https://python.langchain.com/docs/how_to/qa_chat_history_how_to/): building a conversational question-answering application, incorporating chat history and retrieval from external knowledge sources, and deciding between using chains or agents for the application logic. Discusses building chat applications with LangChain by using chains for predictable retrieval steps or agents for more dynamic reasoning. Covers setting up components like embeddings and vector stores, constructing chains with tool calls for retrieval, and assembling LangGraph agents with a ReAct executor. Provides examples for testing the applications.
[How to: stream](https://python.langchain.com/docs/how_to/qa_streaming/): LLM should read this page when: 1) Building a RAG (Retrieval Augmented Generation) application that requires streaming final outputs or intermediate steps 2) Integrating streaming capabilities into an existing LLM-based application 'The page provides guidance on how to stream final outputs and intermediate steps from a RAG (Retrieval Augmented Generation) application built with LangChain and LangGraph. It covers setting up the necessary components, constructing the RAG application, and utilizing different streaming modes to stream tokens from the final output or individual state updates from each step.'
[How to: return sources](https://python.langchain.com/docs/how_to/qa_sources/): LLM should read this page when: 1) Building a question-answering (QA) application that needs to return the sources used to generate the answer. 2) Implementing a conversational QA system with retrieval-augmented generation (RAG). 3) Structuring model outputs to include sources or citations. 'This guide explains how to configure LangChain's QA and RAG workflows to retrieve and return the source documents or citations used to generate the final answer. It covers both basic RAG and conversational RAG architectures, and demonstrates techniques for structuring the model output to include source information.'
[How to: return citations](https://python.langchain.com/docs/how_to/qa_citations/): seeking to add citations to results from a Retrieval Augmented Generation (RAG) application, when wanting to justify an answer using source material, and when needing to provide evidence for generated outputs. The page covers various methods for getting a RAG application to cite sources used in generating answers, including tool-calling to return source IDs or text snippets, direct prompting to generate structured outputs with citations, retrieving and compressing context to minimize need for citations, and post-processing generated answers to annotate with citations.
[How to: do per-user retrieval](https://python.langchain.com/docs/how_to/qa_per_user/): needing to configure retrieval chains for per-user data access, wanting to limit document access for different users, or building retrieval applications with multi-tenant architectures. Explains how to configure retriever search kwargs to limit retrieved documents based on user, demonstrates code example using Pinecone namespace for multi-tenancy.
### Extraction
Extraction is when you use LLMs to extract structured information from unstructured text.
[this guide](https://python.langchain.com/docs/tutorials/extraction/): building information extraction applications, understanding how to use reference examples for improving extraction performance, or when needing to extract structured data from unstructured text. This tutorial covers building an information extraction chain using LangChain, defining schemas for extracting structured data, using reference examples to improve extraction quality, and extracting multiple entities from text.
[How to: use reference examples](https://python.langchain.com/docs/how_to/extraction_examples/): wanting to use reference examples to improve extraction quality, wanting to structure example inputs and outputs for extraction, wanting to test an extraction model with and without examples. This page explains how to define reference examples in the format expected for the LangChain tool calling API, how to incorporate these examples into prompts, and how using examples can improve extraction performance compared to not using examples.
[How to: handle long text](https://python.langchain.com/docs/how_to/extraction_long_text/): working with large documents or PDFs that exceed the context window of the LLM, when needing to extract structured information from text. This page covers strategies for handling long text when doing information extraction, including a brute force approach of chunking the text and extracting from each chunk, and a retrieval-augmented generation (RAG) approach of indexing the chunks and only extracting from relevant ones. It also discusses common issues with these approaches.
[How to: do extraction without using function calling](https://python.langchain.com/docs/how_to/extraction_parse/): looking to extract structured data from text, when needing to parse model outputs into objects, or when wanting to avoid using tool calling methods for extraction tasks. This page explains how to use prompting instructions to get LLMs to generate outputs in a structured format like JSON, and then use output parsers to convert the model responses into Python objects.
### Chatbots
Chatbots involve using an LLM to have a conversation.
[this guide](https://python.langchain.com/docs/tutorials/chatbot/): building a chatbot application, incorporating conversational history, or using prompt templates. This page demonstrates how to build a chatbot with LangChain, including adding message persistence, prompt templates, conversation history management, and response streaming.
[How to: manage memory](https://python.langchain.com/docs/how_to/chatbots_memory/): LLM should read this page when: 1) Building a chatbot and wants to incorporate memory (chat history) 2) Looking to add context from previous messages to improve responses 3) Needs techniques to handle long conversations by summarizing or trimming history 'The page covers different techniques to add memory capabilities to chatbots, including passing previous messages directly, automatic history management using LangGraph persistence, trimming messages to reduce context, and generating summaries of conversations. Examples in Python are provided for each approach.'
[How to: do retrieval](https://python.langchain.com/docs/how_to/chatbots_retrieval/): building a retrieval-augmented chatbot, adding conversational context to retrieval queries, or streaming responses from a chatbot. This page covers setting up a retriever over a document corpus, creating document chains and retrieval chains, transforming queries for better retrieval, and streaming responses from the retrieval chain.
[How to: use tools](https://python.langchain.com/docs/how_to/chatbots_tools/): looking to integrate tools into chatbots, when using agents with tools, when incorporating web search into conversational agents. The page covers how to create a conversational agent using LangChain that can interact with APIs and web search tools, while maintaining chat history. It demonstrates setting up a ReAct agent with a Tavily search tool, invoking the agent, handling conversational responses with chat history, and adding memory.
[How to: manage large chat history](https://python.langchain.com/docs/how_to/trim_messages/): working with long chat histories, when concerned about token limits for chat models, when implementing token management strategies. This page explains how to use the trim_messages utility to reduce the size of a chat message history to fit within token limits, covering trimming by token count or message count, and allowing customization of trimming strategies.
### Query analysis
Query Analysis is the task of using an LLM to generate a query to send to a retriever.
[this guide](https://python.langchain.com/docs/tutorials/rag/#query-analysis): LLM should read this page when: 1) Building a question-answering application over unstructured data 2) Learning about Retrieval Augmented Generation (RAG) architectures 3) Indexing data for use with LLMs 'This tutorial covers building a Retrieval Augmented Generation (RAG) application that can answer questions based on ingested data. It walks through loading data, chunking it, embedding and storing it in a vector store, retrieving relevant chunks for a given query, and generating an answer using an LLM. It also shows how to incorporate query analysis for improved retrieval.'
[How to: add examples to the prompt](https://python.langchain.com/docs/how_to/query_few_shot/): needing to guide an LLM to generate queries, when fine-tuning an LLM for query generation, when incorporating examples into few-shot prompts. This page covers how to add examples to prompts for query analysis in LangChain, including setting up the system, defining the query schema, generating queries, and tuning prompts by adding examples.
[How to: handle cases where no queries are generated](https://python.langchain.com/docs/how_to/query_no_queries/): querying for information, handling cases where no queries are generated, integrating query analysis with retrieval. Provides guidance on handling scenarios where query analysis techniques allow for no queries to be generated, including code examples for structuring the output, performing query analysis with an LLM, and integrating query analysis with a retriever in a chain.
[How to: handle multiple queries](https://python.langchain.com/docs/how_to/query_multiple_queries/): handling queries that generate multiple potential queries, combining retrieval results from multiple queries, and integrating query analysis with retrieval pipelines. Explains how to handle scenarios where a query analysis step produces multiple potential queries by running retrievals for each query and combining the results. Demonstrates this approach with code examples using LangChain components.
[How to: handle multiple retrievers](https://python.langchain.com/docs/how_to/query_multiple_retrievers/): needing to handle multiple retrievers for query analysis, when implementing a query analyzer that can select between different retrievers, when building a retrieval-augmented system that needs to choose between different data sources. This page explains how to handle scenarios where a query analysis step allows for selecting between multiple retrievers, showing an example implementation using LangChain's tools for structured output parsing, prompting, and chaining components together.
[How to: construct filters](https://python.langchain.com/docs/how_to/query_constructing_filters/): constructing filters for query analysis, translating filters to specific retriever formats, using LangChain's structured query objects. This page covers how to construct filters as Pydantic models and translate them into retriever-specific filters using LangChain's translators for Chroma and Elasticsearch.
[How to: deal with high cardinality categorical variables](https://python.langchain.com/docs/how_to/query_high_cardinality/): dealing with categorical data with high cardinality, handling potential misspellings of categorical values, and filtering based on categorical values. The page discusses techniques for handling high-cardinality categorical data in query analysis, such as adding all possible values to the prompt, using a vector store to find relevant values, and correcting user input to the closest valid categorical value.
### Q&A over SQL + CSV
You can use LLMs to do question answering over tabular data.
[this guide](https://python.langchain.com/docs/tutorials/sql_qa/): LLM should read this page when: 1. Building a question-answering system over a SQL database 2. Implementing agents or chains to interact with a SQL database 'This tutorial covers building question-answering systems over SQL databases using LangChain. It demonstrates creating chains and agents that can generate SQL queries from natural language, execute them against a database, and provide natural language responses. It covers techniques like schema exploration, query validation, and handling high-cardinality columns.'
[How to: use prompting to improve results](https://python.langchain.com/docs/how_to/sql_prompting/): 'querying SQL databases with a language model, when doing few-shot prompting for SQL queries, and when selecting relevant few-shot examples dynamically.' 'This page covers how to improve SQL query generation prompts by incorporating database schema information, providing few-shot examples, and dynamically selecting the most relevant few-shot examples using semantic similarity.'
[How to: do query validation](https://python.langchain.com/docs/how_to/sql_query_checking/): Line 1: 'working on SQL query generation, handling invalid SQL queries, or incorporating human approval for SQL queries' Line 2: 'This page covers strategies for validating SQL queries, such as appending a query validator step, prompt engineering, human-in-the-loop approval, and error handling.'
[How to: deal with large databases](https://python.langchain.com/docs/how_to/sql_large_db/): dealing with large databases in SQL question-answering, identifying relevant table schemas to include in prompts, and handling high-cardinality columns with proper nouns or other unique values. The page discusses methods to identify relevant tables and table schemas to include in prompts when dealing with large databases. It also covers techniques to handle high-cardinality columns containing proper nouns or other unique values, such as creating a vector store of distinct values and querying it to include relevant spellings in prompts.
[How to: deal with CSV files](https://python.langchain.com/docs/how_to/sql_csv/): needing to build question-answering systems over CSV data, wanting to understand the tradeoffs between using SQL or Python libraries like Pandas, and requiring guidance on securely executing code from language models. This page covers two main approaches to question answering over CSV data: using SQL by loading CSVs into a database, or giving an LLM access to Python environments to interact with CSV data using libraries like Pandas. It discusses the security implications of each approach and provides code examples for implementing question-answering chains and agents with both methods.
### Q&A over graph databases
You can use an LLM to do question answering over graph databases.
[this guide](https://python.langchain.com/docs/tutorials/graph/): LLM should read this page when: 1) Building a question-answering system over a graph database 2) Implementing text-to-query generation for graph databases 3) Learning techniques for query validation and error handling 'This page covers building a question-answering application over a graph database using LangChain. It provides a basic implementation using the GraphQACypherChain, followed by an advanced implementation with LangGraph. The latter includes techniques like few-shot prompting, query validation, and error handling for generating accurate Cypher queries from natural language.'
[How to: add a semantic layer over the database](https://python.langchain.com/docs/how_to/graph_semantic/): needing to add a semantic layer over a graph database, needing to use tools representing Cypher templates with an LLM, or needing to build a LangGraph Agent to interact with a Neo4j database. This page covers how to create custom tools with Cypher templates for a Neo4j graph database, bind those tools to an LLM, and build a LangGraph Agent that can invoke the tools to retrieve information from the graph database.
[How to: construct knowledge graphs](https://python.langchain.com/docs/how_to/graph_constructing/): constructing knowledge graphs from unstructured text, storing information in a graph database, using LLM Graph Transformer to extract knowledge from text. This page explains how to set up a Neo4j graph database, use LLMGraphTransformer to extract structured knowledge graph data from text, filter extracted nodes/relationships, and store the knowledge graph in Neo4j.
### Summarization
LLMs can summarize and otherwise distill desired information from text, including
[this guide](https://python.langchain.com/docs/tutorials/summarization/): needing to summarize long texts or documents, when building question-answering systems, when creating text analysis applications. This page covers summarizing texts using LangChain, including the "stuff" method (concatenating into single prompt), the "map-reduce" method (splitting into chunks for parallel summarization), and orchestrating these methods using LangGraph.
[How to: summarize text in a single LLM call](https://python.langchain.com/docs/how_to/summarize_stuff/): looking to summarize text, seeking a simple single-LLM summarization method, or exploring basic summarization chains in LangChain. This page outlines how to use LangChain's pre-built 'stuff' summarization chain, which stuffs text into a prompt for an LLM to summarize in a single call.
[How to: summarize text through parallelization](https://python.langchain.com/docs/how_to/summarize_map_reduce/): needing to summarize long text documents using parallelization, needing to optimize summarization for large volumes of text, and needing efficient summarization strategies. This page discusses using a map-reduce strategy to summarize text through parallelization, including breaking the text into subdocuments, generating summaries for each in parallel (map step), and then consolidating the summaries into a final summary (reduce step). It provides code examples using LangChain and LangGraph.
[How to: summarize text through iterative refinement](https://python.langchain.com/docs/how_to/summarize_refine/): LLM should read this page when: 1. Attempting to summarize long texts through iterative refinement 2. Learning about building applications with LangGraph 3. Seeking examples of streaming LLM outputs 'This guide demonstrates how to summarize text through iterative refinement using LangGraph. It involves splitting the text into documents, summarizing the first document, and then refining the summary based on subsequent documents until finished. The approach leverages LangGraph's streaming capabilities and modularity.'
## LangChain Expression Language (LCEL)
[LCEL](https://python.langchain.com/docs/concepts/lcel/): needing an overview of the LangChain Expression Language (LCEL), deciding whether to use LCEL or not, and understanding how to compose chains using LCEL primitives. Provides an overview of the LCEL, a declarative approach to building chains from existing Runnables, covering its benefits, composition primitives like RunnableSequence and RunnableParallel, the composition syntax, automatic type coercion, and guidance on when to use LCEL versus alternatives like LangGraph.
[**LCEL cheatsheet**](https://python.langchain.com/docs/how_to/lcel_cheatsheet/): 'needing a reference for interacting with Runnables in LangChain or building custom runnables and chains' 'This page provides a comprehensive cheatsheet with examples for key operations with Runnables such as invoking, batching, streaming, composing, configuring, and dynamically building runnables and chains'
[**Migration guide**](https://python.langchain.com/docs/versions/migrating_chains/): migrating older chains from LangChain v0.0, reimplementing legacy chains, or upgrading to use LCEL and LangGraph This page provides guidance on migrating from deprecated v0.0 chain implementations to using LCEL and LangGraph, including specific guides for various legacy chains like LLMChain, ConversationChain, RetrievalQA, and others.
[How to: chain runnables](https://python.langchain.com/docs/how_to/sequence/): chaining multiple LangChain components together, composing prompt templates with models, or combining runnables in a sequence. This page explains how to chain runnables (LangChain components) together using the pipe operator '|' or the .pipe() method, including chaining prompt templates with models and parsers, and how input/output formats are coerced during chaining.
[How to: stream runnables](https://python.langchain.com/docs/how_to/streaming/): Line 1: 'wanting to learn how to stream LLM responses, stream intermediate steps, and configure streaming events.' Line 2: 'This page covers how to use the `stream` and `astream` methods to stream final outputs, how to use `astream_events` to stream both final outputs and intermediate steps, filtering events, propagating callbacks for streaming, and working with input streams.'
[How to: invoke runnables in parallel](https://python.langchain.com/docs/how_to/parallel/): parallelizing steps in a chain, formatting data for chaining, or splitting inputs to run multiple runnables in parallel. Explains how to use RunnableParallel to execute runnables concurrently, format data between steps, and provides examples of parallelizing chains.
[How to: add default invocation args to runnables](https://python.langchain.com/docs/how_to/binding/): LLM should read this page when: 1) Wanting to invoke a Runnable with constant arguments not part of the preceding output or user input 2) Needing to bind provider-specific arguments like stop sequences or tools 'This page explains how to use the Runnable.bind() method to set default invocation arguments for a Runnable within a RunnableSequence. It covers binding stop sequences to language models and attaching OpenAI tools.'
[How to: turn any function into a runnable](https://python.langchain.com/docs/how_to/functions/): needing to use custom functions, needing to implement streaming, needing to pass metadata to runnables Covers how to use custom functions as Runnables, including constructors, decorators, coercion, passing metadata, and implementing streaming.
[How to: pass through inputs from one chain step to the next](https://python.langchain.com/docs/how_to/passthrough/): needing to pass data from one step to the next in a chain, when formatting inputs for prompts, when retrieving and preparing context for prompts. This page explains how to use RunnablePassthrough and RunnableParallel to pass data unchanged through chains, covering examples like formatting retrieval results and user inputs into prompts.
[How to: configure runnable behavior at runtime](https://python.langchain.com/docs/how_to/configure/): configuring chain internals at runtime, swapping models or prompts within a chain, or exploring different configurations of runnables. The page covers how to use .configurable_fields to configure parameters of a runnable at runtime, and .configurable_alternatives to swap out runnables with alternatives, including examples for chat models, prompts, and combinations thereof.
[How to: add message history (memory) to a chain](https://python.langchain.com/docs/how_to/message_history/): building a chatbot or multi-turn application, wanting to persist conversational state, wanting to manage message history This page explains how to add message history and persist conversational state using LangGraph, covering examples with chat models and prompt templates, and managing the message history.
[How to: route between sub-chains](https://python.langchain.com/docs/how_to/routing/): LLM should read this page when: - It needs to conditionally route between sub-chains based on previous outputs - It needs to use semantic similarity to choose the most relevant prompt for a given query 'The page covers how to route between sub-chains in LangChain, including using custom functions, RunnableBranch, and semantic similarity for prompt routing. It provides code examples for each method.'
[How to: create a dynamic (self-constructing) chain](https://python.langchain.com/docs/how_to/dynamic_chain/): developing dynamic chains, implementing conditional routing, returning runnables dynamically The page explains how to create a dynamic chain that constructs parts of itself at runtime by having Runnable Lambdas return other Runnables.
[How to: inspect runnables](https://python.langchain.com/docs/how_to/inspect/): inspecting internals of an LCEL chain, debugging chain logic, or retrieving chain prompts. Provides methods to visualize chain graphs, print prompts used in chains, and inspect chain steps programmatically.
[How to: add fallbacks to a runnable](https://python.langchain.com/docs/how_to/fallbacks/): needing to add fallback options in case of errors, processing long inputs, or wanting to use a better model. This page explains how to configure fallback chains for LLM APIs in case of rate limiting or errors, for handling long input texts exceeding context windows, and for defaulting to better models when parsing fails.
[How to: pass runtime secrets to a runnable](https://python.langchain.com/docs/how_to/runnable_runtime_secrets/): needing to pass sensitive data to a runnable, ensuring secrets remain hidden from tracing, or integrating secret values with runnables. Explains how to pass runtime secrets to runnables using RunnableConfig, allowing certain keys to be hidden from tracing while still being accessible during invocation.
Tracing gives you observability inside your chains and agents, and is vital in diagnosing issues.
[How to: trace with LangChain](https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain/): tracing LangChain applications with LangSmith, customizing trace metadata and run names, or integrating LangChain with the LangSmith SDK. Provides guides on integrating LangSmith tracing into LangChain applications, configuring trace metadata and run names, distributed tracing, interoperability between LangChain and LangSmith SDK, and tracing LangChain invocations without environment variables.
[How to: add metadata and tags to traces](https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain/#add-metadata-and-tags-to-traces): tracing LangChain applications with LangSmith, when logging metadata and tags to traces, and when customizing trace names and IDs. This page provides step-by-step guides on integrating LangSmith tracing with LangChain in Python and JS/TS, covering quick start instructions, selective tracing, logging to specific projects, adding metadata/tags, customizing run names/IDs, accessing run IDs, distributed tracing in Python, and interoperability with the LangSmith SDK.
[in this section of the LangSmith docs](https://docs.smith.langchain.com/how_to_guides/tracing/): configuring observability for LLM applications, accessing and managing traces, and setting up automation and monitoring. Guides on configuring tracing, using the UI/API for traces, creating dashboards, automating rules/alerts, and gathering human feedback for LLM applications.
## Integrations
### Featured Chat Model Providers
- [ChatAnthropic](https://python.langchain.com/docs/integrations/chat/anthropic/)
- [ChatMistralAI](https://python.langchain.com/docs/integrations/chat/mistralai/)
- [ChatFireworks](https://python.langchain.com/docs/integrations/chat/fireworks/)
- [AzureChatOpenAI](https://python.langchain.com/docs/integrations/chat/azure_chat_openai/)
- [ChatOpenAI](https://python.langchain.com/docs/integrations/chat/openai/)
- [ChatTogether](https://python.langchain.com/docs/integrations/chat/together/)
- [ChatVertexAI](https://python.langchain.com/docs/integrations/chat/google_vertex_ai_palm/)
- [ChatGoogleGenerativeAI](https://python.langchain.com/docs/integrations/chat/google_generative_ai/)
- [ChatGroq](https://python.langchain.com/docs/integrations/chat/groq/)
- [ChatCohere](https://python.langchain.com/docs/integrations/chat/cohere/)
- [ChatBedrock](https://python.langchain.com/docs/integrations/chat/bedrock/)
- [ChatHuggingFace](https://python.langchain.com/docs/integrations/chat/huggingface/)
- [ChatNVIDIA](https://python.langchain.com/docs/integrations/chat/nvidia_ai_endpoints/)
- [ChatOllama](https://python.langchain.com/docs/integrations/chat/ollama/)
- [ChatLlamaCpp](https://python.langchain.com/docs/integrations/chat/llamacpp/)
- [ChatAI21](https://python.langchain.com/docs/integrations/chat/ai21/)
- [ChatUpstage](https://python.langchain.com/docs/integrations/chat/upstage/)
- [ChatDatabricks](https://python.langchain.com/docs/integrations/chat/databricks/)
- [ChatWatsonx](https://python.langchain.com/docs/integrations/chat/ibm_watsonx/)
- [ChatXAI](https://python.langchain.com/docs/integrations/chat/xai/)
[All](https://python.langchain.com/docs/integrations/chat/): integrating chat models into an application, using chat models for conversational AI tasks, or choosing between different chat model providers. Provides an overview of chat models integrated with LangChain, including OpenAI, Anthropic, Google, and others. Covers key features like tool calling, structured output, JSON mode, local usage, and multimodal support.
## Glossary
[AIMessageChunk](https://python.langchain.com/docs/concepts/messages/#aimessagechunk): 'needing to understand messages and message structure for chat models, when working with chat history, and when integrating with chat model providers' Line 2: 'Detailed overview of the different message types used in LangChain for chat models, how messages are structured, and how to convert between LangChain and OpenAI message formats.'
[AIMessage](https://python.langchain.com/docs/concepts/messages/#aimessage): building chat applications, when implementing tool calling, or when working with chat model outputs. Messages are the units of communication in chat models, representing input, output and metadata; topics include message types, roles, content, metadata, conversation structure, and LangChain's unified message format.
[astream_events](https://python.langchain.com/docs/concepts/chat_models/#key-methods): LLM should read this page when: 1) Implementing an application that uses a chat model 2) Integrating chat models with other LangChain components 3) Planning for advanced chat model features like tool calling or structured outputs This page provides an overview of chat models in LangChain, including their key features, interfaces, integration options, tool calling, structured outputs, multimodality, context windows, and advanced topics like rate limiting and caching.
[BaseTool](https://python.langchain.com/docs/concepts/tools/#tool-interface): needing to understand LangChain tools, wanting to create custom tools, or looking for best practices for designing tools. The page covers the tool abstraction in LangChain, which associates a Python function with a schema for name, description, and arguments. It explains how to create tools using the @tool decorator, configure the schema, handle tool artifacts, use special type annotations (InjectedToolArg, RunnableConfig), and provides an overview of toolkits.
[invoke](https://python.langchain.com/docs/concepts/runnables/): learning how to use the Runnable interface, when working with custom Runnables, and when needing to configure Runnables at runtime. The page covers the Runnable interface, its methods for invocation, batching, streaming, inspecting schemas, and configuration. It explains RunnableConfig, custom Runnables, and configurable Runnables.
[bind_tools](https://python.langchain.com/docs/concepts/tool_calling/#tool-binding): building applications that require an LLM to directly interact with external systems or APIs, when integrating tools or functions into an LLM workflow, or when fine-tuning an LLM to better handle tool calling. This page provides an overview of tool calling, which allows LLMs to invoke external tools or APIs with specific input schemas. It covers key concepts like tool creation, binding tools to LLMs, initiating tool calls from LLMs, and executing the called tools. It also offers guidance on recommended usage and best practices.
[Caching](https://python.langchain.com/docs/concepts/chat_models/#caching): building chat applications, using LLMs for information extraction, or working with multimodal data This page discusses chat models, which are language models that operate on messages. It covers chat model interfaces, integrations, features like tool calling and structured outputs, multimodality, context windows, rate limiting, and caching.
[Chat models](https://python.langchain.com/docs/concepts/multimodality/#multimodality-in-chat-models): needing to understand multimodal capabilities in LangChain, when working with multimodal data like images/audio/video, and when determining if a specific LangChain component supports multimodality. Provides an overview of multimodality in chat models, embedding models, and vector stores. Discusses multimodal inputs/outputs for chat models and how they are formatted.
[Configurable runnables](https://python.langchain.com/docs/concepts/runnables/#configurable-runnables): trying to understand how to use Runnables, how to configure and compose Runnables, and how to inspect Runnable schemas. The Runnable interface is the foundation for working with LangChain components like language models, output parsers, and retrievers. It defines methods for invoking, batching, streaming, inspecting schemas, configuring, and composing Runnables.
[Context window](https://python.langchain.com/docs/concepts/chat_models/#context-window): getting an overview of chat models, understanding the key functionality of chat models, and determining if this concept is relevant for their application. Provides an overview of chat models (LLMs with a chat interface), their features, integrations, key methods like invoking/streaming, handling inputs/outputs, using tools/structured outputs, and advanced topics like rate limiting and caching.
[Conversation patterns](https://python.langchain.com/docs/concepts/chat_history/#conversation-patterns): managing conversation history in chatbots, implementing memory for chat models, understanding correct conversation structure. This page explains the concept of chat history, a record of messages exchanged between a user and a chat model. It covers conversation patterns, guidelines for managing chat history to avoid exceeding context window, and the importance of preserving conversation structure.
[Document](https://python.langchain.com/api_reference/core/documents/langchain_core.documents.base.Document.html/): working with document data, retrieving and processing text documents, integrating with text embedding and vector storage systems This page provides details on the Document class and its associated methods and properties, as well as examples of how to use it in various scenarios such as document loading, retrieval, and transformation
[Embedding models](https://python.langchain.com/docs/concepts/multimodality/#multimodality-in-embedding-models): needing to understand multimodal capabilities of LangChain components, wanting to work with non-text data like images/audio/video, or planning to incorporate multimodal data in chat interactions. Provides an overview of multimodality support in chat models (inputs and tools), embedding models, and vector stores; notes current limitations and expected future expansions to handle different data types.
[HumanMessage](https://python.langchain.com/docs/concepts/messages/#humanmessage): LLM should read this page when: 1) Understanding how to structure conversations with chat models, 2) Needing to work with different types of messages (user, assistant, system, tool), 3) Converting between LangChain and OpenAI message formats. Messages are the units of communication used by chat models, representing user input, assistant output, system instructions, and tool results. Key topics include message structure, types (HumanMessage, AIMessage, SystemMessage, ToolMessage), multimodal content support, and integration with OpenAI message format.
[InjectedState](https://python.langchain.com/docs/concepts/tools/#injectedstate): learning about LangChain's tools, creating custom tools, or integrating tools with chat models. Provides conceptual overview of tools - encapsulating functions with schemas for models to call. Covers creating tools with @tool decorator, tool interfaces, special type annotations, artifacts, best practices, and toolkits.
[InjectedStore](https://python.langchain.com/docs/concepts/tools/#injectedstore): needing to understand how to create and use tools in LangChain, when needing to pass runtime values to tools, and when needing to configure a tool's schema. Tools are a way to encapsulate functions and their schemas to be used with chat models that support tool calling. The page covers the tool interface, creating tools with the @tool decorator, using tools directly, configuring tool schemas, returning artifacts from tools, and special type annotations like InjectedToolArg and RunnableConfig.
[InjectedToolArg](https://python.langchain.com/docs/concepts/tools/#injectedtoolarg): trying to understand how to create and use tools in LangChain, when needing to configure tool schemas, and when wanting to return artifacts from tools. Tools provide a way to encapsulate Python functions and schemas to be passed to chat models for execution. The page covers creating tools with the @tool decorator, configuring tool schemas, special type annotations, and tool artifacts.
[input and output types](https://python.langchain.com/docs/concepts/runnables/#input-and-output-types): needing to interact with LangChain components, wanting to understand the core Runnable interface, or composing complex chains using LCEL. Covers the Runnable interface that defines a standard way to invoke, batch, stream and inspect components; the RunnableConfig for setting runtime options; creating custom Runnables; configurable Runnables; and how input/output types, schemas, and streaming work.
[Integration packages](https://python.langchain.com/docs/concepts/architecture/#integration-packages): determining the overall architecture of LangChain, understanding the different components and packages in the LangChain ecosystem, or deciding which packages to import for a specific use case. This page provides an overview of the different packages that make up the LangChain framework, including langchain-core, langchain, integration packages, langchain-community, langgraph, langserve, and LangSmith, and explains the purpose and contents of each package.
[Integration tests](https://python.langchain.com/docs/concepts/testing/#integration-tests): needing guidance on testing LangChain components, understanding different types of tests (unit, integration, standard), or wanting to contribute by adding tests to an integration. Provides an overview of unit tests, integration tests, and standard tests in the LangChain ecosystem, including definitions, examples, and how to implement them for new tools/integrations.
[invoke](https://python.langchain.com/docs/concepts/runnables/): learning how to use the Runnable interface, when working with custom Runnables, and when needing to configure Runnables at runtime. The page covers the Runnable interface, its methods for invocation, batching, streaming, inspecting schemas, and configuration. It explains RunnableConfig, custom Runnables, and configurable Runnables.
[JSON mode](https://python.langchain.com/docs/concepts/structured_outputs/#json-mode): LLM should read this page when: 1) It needs to return structured output that conforms to a specific schema, 2) It needs to store model output in a database, 3) It needs to ensure model output matches a predefined format. This page covers how to define an output schema, and techniques like tool calling and JSON mode that allow models to return structured output conforming to that schema, as well as a helper method to streamline the process.
[langchain-community](https://python.langchain.com/docs/concepts/architecture/#langchain-community): learning about the structure of LangChain, deploying LangChain applications, or needing an overview of the LangChain ecosystem. This page gives an overview of the different packages, components, and services that make up the LangChain framework, including langchain-core, langchain, integration packages, langchain-community, LangGraph, LangServe, and LangSmith.
[langchain-core](https://python.langchain.com/docs/concepts/architecture/#langchain-core): needing an overview of LangChain's architecture, when considering integrating external packages, or when exploring the LangChain ecosystem. Outlines the main components of LangChain (langchain-core, langchain, integration packages, langchain-community, langgraph, langserve, LangSmith) and their roles, providing a high-level architectural overview.
[langchain](https://python.langchain.com/docs/concepts/architecture/#langchain): looking to understand the overall architecture of LangChain, when trying to determine what LangChain packages to install, or when wanting an overview of the various LangChain projects. This page outlines the hierarchical structure of the LangChain framework, describing the purpose and contents of key packages like langchain-core, langchain, integration packages, langchain-community, langgraph, langserve, and LangSmith.
[langgraph](https://python.langchain.com/docs/concepts/architecture/#langgraph): developing applications with LangChain, seeking to understand the overall architecture of LangChain, planning to contribute to or integrate with LangChain The page outlines the layered architecture of LangChain, describing the core abstraction layer, the main LangChain package, integration packages, community integrations, LangGraph for stateful agents, LangServe for deployment, and LangSmith developer tools
[Managing chat history](https://python.langchain.com/docs/concepts/chat_history/#managing-chat-history): understanding and managing chat history, learning about conversation patterns, following correct chat history structure. Explains chat history concept, provides guidelines for managing chat history, discusses conversation patterns involving users, assistants, and tools.
[OpenAI format](https://python.langchain.com/docs/concepts/messages/#openai-format): building chat applications, working with chat models, or consuming message streams. This page covers the structure and components of messages used in chat models, including roles, content, usage metadata, and different message types like HumanMessage, AIMessage, and ToolMessage.
[Propagation of RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#propagation-of-runnableconfig): LLM should read this page when: learning about the LangChain Runnable interface, working with Runnables in LangChain, understanding how to configure and execute Runnables. The page covers the Runnable interface in LangChain, including invoking/batching/streaming Runnables, input/output schemas, configuring Runnables, creating custom Runnables, and working with configurable Runnables.
[rate-limiting](https://python.langchain.com/docs/concepts/chat_models/#rate-limiting): 1) working with chat models, 2) integrating tool calling or structured outputs, 3) understanding chat model capabilities. Overview of chat model interface, inputs/outputs, standard parameters; tool calling and structured output support; multimodality; context window; advanced topics like rate limiting, caching.
[RemoveMessage](https://python.langchain.com/docs/concepts/messages/#removemessage): needing information on the structure of messages used in conversational AI models, wanting to understand how messages are represented in LangChain, or looking for details on specific message types like SystemMessage, HumanMessage, and AIMessage. Messages are the basic units of communication in conversational AI models, containing a role (e.g. user, assistant), content (text or multimodal data), and metadata; LangChain provides a standardized message format and different message types to represent various components of a conversation.
[role](https://python.langchain.com/docs/concepts/messages/#role): understanding how to structure messages for chat models, accessing details about different LangChain message types, or converting between LangChain and OpenAI message formats. Messages are the core unit of communication in chat models, representing input/output content and metadata; LangChain defines SystemMessage, HumanMessage, AIMessage, ToolMessage and others to standardize message format across providers.
[RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig): needing to understand the Runnable interface, invoking and configuring Runnables, and creating custom Runnables. The page covers the Runnable interface's core concepts, methods like invoke, batch, and stream, input/output types, configuring Runnables with RunnableConfig, creating custom Runnables from functions, and using configurable Runnables.
[Standard parameters for chat models](https://python.langchain.com/docs/concepts/chat_models/#standard-parameters): building applications using chat models, working with chat models for tool calling, structured outputs or multimodal inputs/outputs. Covers overview of chat models, integrations, interfaces, tool calling, structured outputs, multimodality, context window, rate-limiting, and caching of chat models.
[Standard tests](https://python.langchain.com/docs/concepts/testing/#standard-tests): needing guidance on testing LangChain components, or wanting to understand the different types of tests used in LangChain. This page discusses unit tests for individual functions, integration tests for validating multiple components working together, and LangChain's standard tests for ensuring consistency across tools and integrations.
[stream](https://python.langchain.com/docs/concepts/streaming/): [building applications that use streaming, building applications that need to display partial results in real-time, building applications that need to provide updates on pipeline or workflow progress] 'This page covers streaming in LangChain, including what can be streamed in LLM applications, the streaming APIs available, how to write custom data to the stream, and how LangChain automatically enables streaming for chat models in certain cases.'
[Tokens](https://python.langchain.com/docs/concepts/tokens/): needing to understand tokens used by LLMs, when dealing with character/token counts, when working with multimodal inputs Tokens are the fundamental units processed by language models. A token can represent words, word parts, punctuation, and other units. Models tokenize inputs, process tokens sequentially, and generate new tokens as output. Tokens enable efficient and contextual language processing compared to characters.
[Tokens](https://python.langchain.com/docs/concepts/tokens/): needing to understand tokens used by LLMs, when dealing with character/token counts, when working with multimodal inputs Tokens are the fundamental units processed by language models. A token can represent words, word parts, punctuation, and other units. Models tokenize inputs, process tokens sequentially, and generate new tokens as output. Tokens enable efficient and contextual language processing compared to characters.
[Tool artifacts](https://python.langchain.com/docs/concepts/tools/#tool-artifacts): needing to understand what tools are, how to create and use them, and how they integrate with models. Explains what tools are in LangChain, how to create them using the @tool decorator, special type annotations for configuring runtime behavior, how to use tools directly or pass them to chat models, and best practices for designing tools.
[Tool binding](https://python.langchain.com/docs/concepts/tool_calling/#tool-binding): determining if tool calling functionality is appropriate for their application, understanding the key concepts and workflow of tool calling, and considering best practices for designing tools. This page covers an overview of tool calling, key concepts like tool creation/binding/calling/execution, recommended usage workflow, details on implementing each step, and best practices for designing effective tools.
[@tool](https://python.langchain.com/docs/concepts/tools/#create-tools-using-the-tool-decorator): needing to understand tools in LangChain, when creating custom tools, or when integrating tools into LangChain applications. Provides an overview of tools, how to create and configure tools using the @tool decorator, different tool types (e.g. with artifacts, injected arguments), and best practices for designing tools.
[Toolkits](https://python.langchain.com/docs/concepts/tools/#toolkits): creating custom Python functions to use with LangChain, configuring existing tools, or adding tools to chat models. Explains the tool abstraction for encapsulating Python functions, creating tools with the `@tool` decorator, configuring schemas, handling tool artifacts, special type annotations, and using toolkits that group related tools.
[ToolMessage](https://python.langchain.com/docs/concepts/messages/#toolmessage): understanding the communication protocol with chat models, working with chat history management, or understanding LangChain's Message object structure. Messages are the unit of communication in chat models and represent input/output along with metadata; LangChain provides a unified Message format with types like SystemMessage, HumanMessage, AIMessage to handle different roles, content types, tool calls.
[Unit tests](https://python.langchain.com/docs/concepts/testing/#unit-tests): developing unit or integration tests, or when contributing to LangChain integrations Provides an overview of unit tests, integration tests, and standard tests used in the LangChain ecosystem
[Vector stores](https://python.langchain.com/docs/concepts/vectorstores/): LLM should read this page when: 1) Building applications that need to index and retrieve information based on semantic similarity 2) Integrating vector databases into their application 3) Exploring advanced vector search and retrieval techniques Vector stores are specialized data stores that enable indexing and retrieving information based on vector representations (embeddings) of data, allowing semantic similarity search over unstructured data like text, images, and audio. The page covers vector store integrations, the core interface, adding/deleting documents, basic and advanced similarity search techniques, and concepts like metadata filtering.
[with_structured_output](https://python.langchain.com/docs/concepts/structured_outputs/#structured-output-method): [needing to return structured data like JSON or database rows, working with models that support structured output like tools or JSON modes, or integrating with helper functions to streamline structured output] [Overview of structured output concept, schema definition formats like JSON/dicts and Pydantic, model integration methods like tool calling and JSON modes, LangChain structured output helper method]
[with_types](https://python.langchain.com/docs/concepts/runnables/#with_types): learning about the Runnable interface in LangChain, understanding how to work with Runnables, and customizing or configuring Runnables. The page covers the Runnable interface, optimized parallel execution, streaming APIs, input/output types, inspecting schemas, RunnableConfig options, creating custom Runnables from functions, and configurable Runnables.

File diff suppressed because it is too large Load Diff

View File

@@ -29,18 +29,9 @@ langchain = "langchain_cli.cli:app"
langchain-cli = "langchain_cli.cli:app"
[dependency-groups]
dev = [
"pytest>=7.4.2,<9.0.0",
"pytest-watcher>=0.3.4,<1.0.0"
]
lint = [
"ruff>=0.13.1,<0.14",
"mypy>=1.18.1,<1.19"
]
test = [
"langchain-core",
"langchain"
]
dev = ["pytest>=7.4.2,<9.0.0", "pytest-watcher>=0.3.4,<1.0.0"]
lint = ["ruff>=0.13.1,<0.14", "mypy>=1.18.1,<1.19"]
test = ["langchain-core", "langchain"]
typing = ["langchain"]
test_integration = []

35
libs/cli/uv.lock generated
View File

@@ -277,6 +277,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "httpx-sse"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" },
]
[[package]]
name = "idna"
version = "3.10"
@@ -335,31 +344,20 @@ dependencies = [
requires-dist = [
{ name = "async-timeout", marker = "python_full_version < '3.11'", specifier = ">=4.0.0,<5.0.0" },
{ name = "langchain-anthropic", marker = "extra == 'anthropic'" },
{ name = "langchain-aws", marker = "extra == 'aws'" },
{ name = "langchain-azure-ai", marker = "extra == 'azure-ai'" },
{ name = "langchain-cohere", marker = "extra == 'cohere'" },
{ name = "langchain-community", marker = "extra == 'community'" },
{ name = "langchain-core", editable = "../core" },
{ name = "langchain-deepseek", marker = "extra == 'deepseek'" },
{ name = "langchain-fireworks", marker = "extra == 'fireworks'" },
{ name = "langchain-google-genai", marker = "extra == 'google-genai'" },
{ name = "langchain-google-vertexai", marker = "extra == 'google-vertexai'" },
{ name = "langchain-groq", marker = "extra == 'groq'" },
{ name = "langchain-huggingface", marker = "extra == 'huggingface'" },
{ name = "langchain-mistralai", marker = "extra == 'mistralai'" },
{ name = "langchain-ollama", marker = "extra == 'ollama'" },
{ name = "langchain-openai", marker = "extra == 'openai'", editable = "../partners/openai" },
{ name = "langchain-perplexity", marker = "extra == 'perplexity'" },
{ name = "langchain-text-splitters", editable = "../text-splitters" },
{ name = "langchain-together", marker = "extra == 'together'" },
{ name = "langchain-xai", marker = "extra == 'xai'" },
{ name = "langsmith", specifier = ">=0.1.17,<1.0.0" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
{ name = "pyyaml", specifier = ">=5.3.0,<7.0.0" },
{ name = "requests", specifier = ">=2.0.0,<3.0.0" },
{ name = "sqlalchemy", specifier = ">=1.4.0,<3.0.0" },
]
provides-extras = ["community", "anthropic", "openai", "azure-ai", "cohere", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"]
provides-extras = ["community", "anthropic", "openai", "google-vertexai", "google-genai", "together"]
[package.metadata.requires-dev]
dev = [
@@ -485,7 +483,7 @@ typing = [{ name = "langchain", editable = "../langchain" }]
[[package]]
name = "langchain-core"
version = "0.3.78"
version = "1.0.0a5"
source = { editable = "../core" }
dependencies = [
{ name = "jsonpatch" },
@@ -586,28 +584,29 @@ typing = [
{ name = "beautifulsoup4", specifier = ">=4.13.5,<5.0.0" },
{ name = "lxml-stubs", specifier = ">=0.5.1,<1.0.0" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "tiktoken", specifier = ">=0.11.0,<1.0.0" },
{ name = "tiktoken", specifier = ">=0.8.0,<1.0.0" },
{ name = "types-requests", specifier = ">=2.31.0.20240218,<3.0.0.0" },
]
[[package]]
name = "langserve"
version = "0.3.2"
version = "0.0.51"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "langchain-core" },
{ name = "langchain" },
{ name = "orjson" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/fb/86e1f5049fb3593743f0fb049c4991f4984020cda00b830ae31f2c47b46b/langserve-0.3.2.tar.gz", hash = "sha256:134b78b1d897c6bcd1fb8a6258e30cf0fb318294505e4ea59c2bea72fa152129", size = 1141270, upload-time = "2025-09-17T20:01:22.183Z" }
sdist = { url = "https://files.pythonhosted.org/packages/06/af/243c8a6ad0efee30186fba5a05a68b4bd9553d3662f946e2e8302cb4a141/langserve-0.0.51.tar.gz", hash = "sha256:036c0104c512bcc2c2406ae089ef9e7e718c32c39ebf6dcb2212f168c7d09816", size = 1135441, upload-time = "2024-03-12T06:16:32.374Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/f0/193c34bf61e1dee8bd637dbeddcc644c46d14e8b03068792ca60b1909bc1/langserve-0.3.2-py3-none-any.whl", hash = "sha256:d9c4cd19d12f6362b82ceecb10357b339b3640a858b9bc30643d5f8a0a036bce", size = 1173213, upload-time = "2025-09-17T20:01:20.603Z" },
{ url = "https://files.pythonhosted.org/packages/b3/49/5b407071f7ea5a861b3f4c3ed2f034cdafb75db1554bbe1a256a092d2669/langserve-0.0.51-py3-none-any.whl", hash = "sha256:e735eef2b6fde7e1514f4be8234b9f0727283e639822ca9c25e8ccc2d24e8492", size = 1167759, upload-time = "2024-03-12T06:16:30.099Z" },
]
[package.optional-dependencies]
all = [
{ name = "fastapi" },
{ name = "httpx-sse" },
{ name = "sse-starlette" },
]

View File

@@ -18,6 +18,7 @@ from collections.abc import Generator
from typing import (
Any,
Callable,
ParamSpec,
TypeVar,
Union,
cast,
@@ -25,7 +26,6 @@ from typing import (
from pydantic.fields import FieldInfo
from pydantic.v1.fields import FieldInfo as FieldInfoV1
from typing_extensions import ParamSpec
from langchain_core._api.internal import is_caller_internal

View File

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

View File

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

View File

@@ -1,447 +0,0 @@
"""Context management for runnables."""
import asyncio
import threading
from collections import defaultdict
from collections.abc import Awaitable, Mapping, Sequence
from functools import partial
from itertools import groupby
from typing import (
Any,
Callable,
Optional,
TypeVar,
Union,
)
from pydantic import ConfigDict
from typing_extensions import override
from langchain_core._api.beta_decorator import beta
from langchain_core.runnables.base import (
Runnable,
RunnableSerializable,
coerce_to_runnable,
)
from langchain_core.runnables.config import RunnableConfig, ensure_config, patch_config
from langchain_core.runnables.utils import ConfigurableFieldSpec, Input, Output
T = TypeVar("T")
Values = dict[Union[asyncio.Event, threading.Event], Any]
CONTEXT_CONFIG_PREFIX = "__context__/"
CONTEXT_CONFIG_SUFFIX_GET = "/get"
CONTEXT_CONFIG_SUFFIX_SET = "/set"
async def _asetter(done: asyncio.Event, values: Values, value: T) -> T:
values[done] = value
done.set()
return value
async def _agetter(done: asyncio.Event, values: Values) -> Any:
await done.wait()
return values[done]
def _setter(done: threading.Event, values: Values, value: T) -> T:
values[done] = value
done.set()
return value
def _getter(done: threading.Event, values: Values) -> Any:
done.wait()
return values[done]
def _key_from_id(id_: str) -> str:
wout_prefix = id_.split(CONTEXT_CONFIG_PREFIX, maxsplit=1)[1]
if wout_prefix.endswith(CONTEXT_CONFIG_SUFFIX_GET):
return wout_prefix[: -len(CONTEXT_CONFIG_SUFFIX_GET)]
if wout_prefix.endswith(CONTEXT_CONFIG_SUFFIX_SET):
return wout_prefix[: -len(CONTEXT_CONFIG_SUFFIX_SET)]
msg = f"Invalid context config id {id_}"
raise ValueError(msg)
def _config_with_context(
config: RunnableConfig,
steps: list[Runnable],
setter: Callable,
getter: Callable,
event_cls: Union[type[threading.Event], type[asyncio.Event]],
) -> RunnableConfig:
if any(k.startswith(CONTEXT_CONFIG_PREFIX) for k in config.get("configurable", {})):
return config
context_specs = [
(spec, i)
for i, step in enumerate(steps)
for spec in step.config_specs
if spec.id.startswith(CONTEXT_CONFIG_PREFIX)
]
grouped_by_key = {
key: list(group)
for key, group in groupby(
sorted(context_specs, key=lambda s: s[0].id),
key=lambda s: _key_from_id(s[0].id),
)
}
deps_by_key = {
key: {
_key_from_id(dep) for spec in group for dep in (spec[0].dependencies or [])
}
for key, group in grouped_by_key.items()
}
values: Values = {}
events: defaultdict[str, Union[asyncio.Event, threading.Event]] = defaultdict(
event_cls
)
context_funcs: dict[str, Callable[[], Any]] = {}
for key, group in grouped_by_key.items():
getters = [s for s in group if s[0].id.endswith(CONTEXT_CONFIG_SUFFIX_GET)]
setters = [s for s in group if s[0].id.endswith(CONTEXT_CONFIG_SUFFIX_SET)]
for dep in deps_by_key[key]:
if key in deps_by_key[dep]:
msg = f"Deadlock detected between context keys {key} and {dep}"
raise ValueError(msg)
if len(setters) != 1:
msg = f"Expected exactly one setter for context key {key}"
raise ValueError(msg)
setter_idx = setters[0][1]
if any(getter_idx < setter_idx for _, getter_idx in getters):
msg = f"Context setter for key {key} must be defined after all getters."
raise ValueError(msg)
if getters:
context_funcs[getters[0][0].id] = partial(getter, events[key], values)
context_funcs[setters[0][0].id] = partial(setter, events[key], values)
return patch_config(config, configurable=context_funcs)
def aconfig_with_context(
config: RunnableConfig,
steps: list[Runnable],
) -> RunnableConfig:
"""Asynchronously patch a runnable config with context getters and setters.
Args:
config: The runnable config.
steps: The runnable steps.
Returns:
The patched runnable config.
"""
return _config_with_context(config, steps, _asetter, _agetter, asyncio.Event)
def config_with_context(
config: RunnableConfig,
steps: list[Runnable],
) -> RunnableConfig:
"""Patch a runnable config with context getters and setters.
Args:
config: The runnable config.
steps: The runnable steps.
Returns:
The patched runnable config.
"""
return _config_with_context(config, steps, _setter, _getter, threading.Event)
@beta()
class ContextGet(RunnableSerializable):
"""Get a context value."""
prefix: str = ""
key: Union[str, list[str]]
@override
def __str__(self) -> str:
return f"ContextGet({_print_keys(self.key)})"
@property
def ids(self) -> list[str]:
"""The context getter ids."""
prefix = self.prefix + "/" if self.prefix else ""
keys = self.key if isinstance(self.key, list) else [self.key]
return [
f"{CONTEXT_CONFIG_PREFIX}{prefix}{k}{CONTEXT_CONFIG_SUFFIX_GET}"
for k in keys
]
@property
@override
def config_specs(self) -> list[ConfigurableFieldSpec]:
return super().config_specs + [
ConfigurableFieldSpec(
id=id_,
annotation=Callable[[], Any],
)
for id_ in self.ids
]
@override
def invoke(
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
) -> Any:
config = ensure_config(config)
configurable = config.get("configurable", {})
if isinstance(self.key, list):
return {key: configurable[id_]() for key, id_ in zip(self.key, self.ids)}
return configurable[self.ids[0]]()
@override
async def ainvoke(
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
) -> Any:
config = ensure_config(config)
configurable = config.get("configurable", {})
if isinstance(self.key, list):
values = await asyncio.gather(*(configurable[id_]() for id_ in self.ids))
return dict(zip(self.key, values))
return await configurable[self.ids[0]]()
SetValue = Union[
Runnable[Input, Output],
Callable[[Input], Output],
Callable[[Input], Awaitable[Output]],
Any,
]
def _coerce_set_value(value: SetValue) -> Runnable[Input, Output]:
if not isinstance(value, Runnable) and not callable(value):
return coerce_to_runnable(lambda _: value)
return coerce_to_runnable(value)
@beta()
class ContextSet(RunnableSerializable):
"""Set a context value."""
prefix: str = ""
keys: Mapping[str, Optional[Runnable]]
model_config = ConfigDict(
arbitrary_types_allowed=True,
)
def __init__(
self,
key: Optional[str] = None,
value: Optional[SetValue] = None,
prefix: str = "",
**kwargs: SetValue,
):
"""Create a context setter.
Args:
key: The context setter key.
value: The context setter value.
prefix: The context setter prefix.
**kwargs: Additional context setter key-value pairs.
"""
if key is not None:
kwargs[key] = value
super().__init__(
keys={
k: _coerce_set_value(v) if v is not None else None
for k, v in kwargs.items()
},
prefix=prefix,
)
@override
def __str__(self) -> str:
return f"ContextSet({_print_keys(list(self.keys.keys()))})"
@property
def ids(self) -> list[str]:
"""The context setter ids."""
prefix = self.prefix + "/" if self.prefix else ""
return [
f"{CONTEXT_CONFIG_PREFIX}{prefix}{key}{CONTEXT_CONFIG_SUFFIX_SET}"
for key in self.keys
]
@property
@override
def config_specs(self) -> list[ConfigurableFieldSpec]:
mapper_config_specs = [
s
for mapper in self.keys.values()
if mapper is not None
for s in mapper.config_specs
]
for spec in mapper_config_specs:
if spec.id.endswith(CONTEXT_CONFIG_SUFFIX_GET):
getter_key = spec.id.split("/")[1]
if getter_key in self.keys:
msg = f"Circular reference in context setter for key {getter_key}"
raise ValueError(msg)
return super().config_specs + [
ConfigurableFieldSpec(
id=id_,
annotation=Callable[[], Any],
)
for id_ in self.ids
]
@override
def invoke(
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
) -> Any:
config = ensure_config(config)
configurable = config.get("configurable", {})
for id_, mapper in zip(self.ids, self.keys.values()):
if mapper is not None:
configurable[id_](mapper.invoke(input, config))
else:
configurable[id_](input)
return input
@override
async def ainvoke(
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
) -> Any:
config = ensure_config(config)
configurable = config.get("configurable", {})
for id_, mapper in zip(self.ids, self.keys.values()):
if mapper is not None:
await configurable[id_](await mapper.ainvoke(input, config))
else:
await configurable[id_](input)
return input
class Context:
"""Context for a runnable.
The `Context` class provides methods for creating context scopes,
getters, and setters within a runnable. It allows for managing
and accessing contextual information throughout the execution
of a program.
Example:
.. code-block:: python
from langchain_core.beta.runnables.context import Context
from langchain_core.runnables.passthrough import RunnablePassthrough
from langchain_core.prompts.prompt import PromptTemplate
from langchain_core.output_parsers.string import StrOutputParser
from tests.unit_tests.fake.llm import FakeListLLM
chain = (
Context.setter("input")
| {
"context": RunnablePassthrough() | Context.setter("context"),
"question": RunnablePassthrough(),
}
| PromptTemplate.from_template("{context} {question}")
| FakeListLLM(responses=["hello"])
| StrOutputParser()
| {
"result": RunnablePassthrough(),
"context": Context.getter("context"),
"input": Context.getter("input"),
}
)
# Use the chain
output = chain.invoke("What's your name?")
print(output["result"]) # Output: "hello"
print(output["context"]) # Output: "What's your name?"
print(output["input"]) # Output: "What's your name?
"""
@staticmethod
def create_scope(scope: str, /) -> "PrefixContext":
"""Create a context scope.
Args:
scope: The scope.
Returns:
The context scope.
"""
return PrefixContext(prefix=scope)
@staticmethod
def getter(key: Union[str, list[str]], /) -> ContextGet:
"""Return a context getter.
Args:
key: The context getter key.
"""
return ContextGet(key=key)
@staticmethod
def setter(
_key: Optional[str] = None,
_value: Optional[SetValue] = None,
/,
**kwargs: SetValue,
) -> ContextSet:
"""Return a context setter.
Args:
_key: The context setter key.
_value: The context setter value.
**kwargs: Additional context setter key-value pairs.
"""
return ContextSet(_key, _value, prefix="", **kwargs)
class PrefixContext:
"""Context for a runnable with a prefix."""
prefix: str = ""
def __init__(self, prefix: str = ""):
"""Create a prefix context.
Args:
prefix: The prefix.
"""
self.prefix = prefix
def getter(self, key: Union[str, list[str]], /) -> ContextGet:
"""Return a prefixed context getter.
Args:
key: The context getter key.
"""
return ContextGet(key=key, prefix=self.prefix)
def setter(
self,
_key: Optional[str] = None,
_value: Optional[SetValue] = None,
/,
**kwargs: SetValue,
) -> ContextSet:
"""Return a prefixed context setter.
Args:
_key: The context setter key.
_value: The context setter value.
**kwargs: Additional context setter key-value pairs.
"""
return ContextSet(_key, _value, prefix=self.prefix, **kwargs)
def _print_keys(keys: Union[str, Sequence[str]]) -> str:
if isinstance(keys, str):
return f"'{keys}'"
return ", ".join(f"'{k}'" for k in keys)

View File

@@ -6,6 +6,7 @@ import asyncio
import atexit
import functools
import logging
import uuid
from abc import ABC, abstractmethod
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager, contextmanager
@@ -40,7 +41,6 @@ from langchain_core.tracers.langchain import LangChainTracer
from langchain_core.tracers.schemas import Run
from langchain_core.tracers.stdout import ConsoleCallbackHandler
from langchain_core.utils.env import env_var_is_set
from langchain_core.utils.uuid import uuid7
if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Coroutine, Generator, Sequence
@@ -506,7 +506,7 @@ class BaseRunManager(RunManagerMixin):
"""
return cls(
run_id=uuid7(),
run_id=uuid.uuid4(),
handlers=[],
inheritable_handlers=[],
tags=[],
@@ -1339,7 +1339,7 @@ class CallbackManager(BaseCallbackManager):
managers = []
for i, prompt in enumerate(prompts):
# Can't have duplicate runs with the same run ID (if provided)
run_id_ = run_id if i == 0 and run_id is not None else uuid7()
run_id_ = run_id if i == 0 and run_id is not None else uuid.uuid4()
handle_event(
self.handlers,
"on_llm_start",
@@ -1394,7 +1394,7 @@ class CallbackManager(BaseCallbackManager):
run_id_ = run_id
run_id = None
else:
run_id_ = uuid7()
run_id_ = uuid.uuid4()
handle_event(
self.handlers,
"on_chat_model_start",
@@ -1443,7 +1443,7 @@ class CallbackManager(BaseCallbackManager):
"""
if run_id is None:
run_id = uuid7()
run_id = uuid.uuid4()
handle_event(
self.handlers,
"on_chain_start",
@@ -1498,7 +1498,7 @@ class CallbackManager(BaseCallbackManager):
"""
if run_id is None:
run_id = uuid7()
run_id = uuid.uuid4()
handle_event(
self.handlers,
@@ -1547,7 +1547,7 @@ class CallbackManager(BaseCallbackManager):
The callback manager for the retriever run.
"""
if run_id is None:
run_id = uuid7()
run_id = uuid.uuid4()
handle_event(
self.handlers,
@@ -1607,7 +1607,7 @@ class CallbackManager(BaseCallbackManager):
)
raise ValueError(msg)
if run_id is None:
run_id = uuid7()
run_id = uuid.uuid4()
handle_event(
self.handlers,
@@ -1843,7 +1843,7 @@ class AsyncCallbackManager(BaseCallbackManager):
run_id_ = run_id
run_id = None
else:
run_id_ = uuid7()
run_id_ = uuid.uuid4()
if inline_handlers:
inline_tasks.append(
@@ -1928,7 +1928,7 @@ class AsyncCallbackManager(BaseCallbackManager):
run_id_ = run_id
run_id = None
else:
run_id_ = uuid7()
run_id_ = uuid.uuid4()
for handler in self.handlers:
task = ahandle_event(
@@ -1991,7 +1991,7 @@ class AsyncCallbackManager(BaseCallbackManager):
for the chain run.
"""
if run_id is None:
run_id = uuid7()
run_id = uuid.uuid4()
await ahandle_event(
self.handlers,
@@ -2041,7 +2041,7 @@ class AsyncCallbackManager(BaseCallbackManager):
for the tool run.
"""
if run_id is None:
run_id = uuid7()
run_id = uuid.uuid4()
await ahandle_event(
self.handlers,
@@ -2093,7 +2093,7 @@ class AsyncCallbackManager(BaseCallbackManager):
if not self.handlers:
return
if run_id is None:
run_id = uuid7()
run_id = uuid.uuid4()
if kwargs:
msg = (
@@ -2136,7 +2136,7 @@ class AsyncCallbackManager(BaseCallbackManager):
for the retriever run.
"""
if run_id is None:
run_id = uuid7()
run_id = uuid.uuid4()
await ahandle_event(
self.handlers,

View File

@@ -45,6 +45,7 @@ https://python.langchain.com/docs/how_to/custom_llm/
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
from langchain_core.language_models._utils import is_openai_data_block
if TYPE_CHECKING:
from langchain_core.language_models.base import (
@@ -85,6 +86,7 @@ __all__ = (
"ParrotFakeChatModel",
"SimpleChatModel",
"get_tokenizer",
"is_openai_data_block",
)
_dynamic_imports = {
@@ -104,6 +106,7 @@ _dynamic_imports = {
"ParrotFakeChatModel": "fake_chat_models",
"LLM": "llms",
"BaseLLM": "llms",
"is_openai_data_block": "_utils",
}

View File

@@ -1,13 +1,49 @@
import re
from collections.abc import Sequence
from typing import Optional
from typing import (
TYPE_CHECKING,
Literal,
Optional,
TypedDict,
TypeVar,
Union,
)
from langchain_core.messages import BaseMessage
if TYPE_CHECKING:
from langchain_core.messages import BaseMessage
from langchain_core.messages.content import (
ContentBlock,
)
def _is_openai_data_block(block: dict) -> bool:
"""Check if the block contains multimodal data in OpenAI Chat Completions format."""
def is_openai_data_block(
block: dict, filter_: Union[Literal["image", "audio", "file"], None] = None
) -> bool:
"""Check whether a block contains multimodal data in OpenAI Chat Completions format.
Supports both data and ID-style blocks (e.g. ``'file_data'`` and ``'file_id'``)
If additional keys are present, they are ignored / will not affect outcome as long
as the required keys are present and valid.
Args:
block: The content block to check.
filter_: If provided, only return True for blocks matching this specific type.
- "image": Only match image_url blocks
- "audio": Only match input_audio blocks
- "file": Only match file blocks
If None, match any valid OpenAI data block type. Note that this means that
if the block has a valid OpenAI data type but the filter_ is set to a
different type, this function will return False.
Returns:
True if the block is a valid OpenAI data block and matches the filter_
(if provided).
"""
if block.get("type") == "image_url":
if filter_ is not None and filter_ != "image":
return False
if (
(set(block.keys()) <= {"type", "image_url", "detail"})
and (image_url := block.get("image_url"))
@@ -15,29 +51,47 @@ def _is_openai_data_block(block: dict) -> bool:
):
url = image_url.get("url")
if isinstance(url, str):
# Required per OpenAI spec
return True
# Ignore `'detail'` since it's optional and specific to OpenAI
elif block.get("type") == "input_audio":
if filter_ is not None and filter_ != "audio":
return False
if (audio := block.get("input_audio")) and isinstance(audio, dict):
audio_data = audio.get("data")
audio_format = audio.get("format")
# Both required per OpenAI spec
if isinstance(audio_data, str) and isinstance(audio_format, str):
return True
elif block.get("type") == "file":
if filter_ is not None and filter_ != "file":
return False
if (file := block.get("file")) and isinstance(file, dict):
file_data = file.get("file_data")
if isinstance(file_data, str):
return True
elif block.get("type") == "input_audio":
if (input_audio := block.get("input_audio")) and isinstance(input_audio, dict):
audio_data = input_audio.get("data")
audio_format = input_audio.get("format")
if isinstance(audio_data, str) and isinstance(audio_format, str):
file_id = file.get("file_id")
# Files can be either base64-encoded or pre-uploaded with an ID
if isinstance(file_data, str) or isinstance(file_id, str):
return True
else:
return False
# Has no `'type'` key
return False
def _parse_data_uri(uri: str) -> Optional[dict]:
"""Parse a data URI into its components. If parsing fails, return None.
class ParsedDataUri(TypedDict):
source_type: Literal["base64"]
data: str
mime_type: str
def _parse_data_uri(uri: str) -> Optional[ParsedDataUri]:
"""Parse a data URI into its components.
If parsing fails, return None. If either MIME type or data is missing, return None.
Example:
@@ -57,84 +111,219 @@ def _parse_data_uri(uri: str) -> Optional[dict]:
match = re.match(regex, uri)
if match is None:
return None
mime_type = match.group("mime_type")
data = match.group("data")
if not mime_type or not data:
return None
return {
"source_type": "base64",
"data": match.group("data"),
"mime_type": match.group("mime_type"),
"data": data,
"mime_type": mime_type,
}
def _convert_openai_format_to_data_block(block: dict) -> dict:
"""Convert OpenAI image content block to standard data content block.
def _normalize_messages(
messages: Sequence["BaseMessage"],
) -> list["BaseMessage"]:
"""Normalize message formats to LangChain v1 standard content blocks.
If parsing fails, pass-through.
Chat models already implement support for:
- Images in OpenAI Chat Completions format
These will be passed through unchanged
- LangChain v1 standard content blocks
Args:
block: The OpenAI image content block to convert.
This function extends support to:
- `Audio <https://platform.openai.com/docs/api-reference/chat/create>`__ and
`file <https://platform.openai.com/docs/api-reference/files>`__ data in OpenAI
Chat Completions format
- Images are technically supported but we expect chat models to handle them
directly; this may change in the future
- LangChain v0 standard content blocks for backward compatibility
Returns:
The converted standard data content block.
"""
if block["type"] == "image_url":
parsed = _parse_data_uri(block["image_url"]["url"])
if parsed is not None:
parsed["type"] = "image"
return parsed
return block
.. versionchanged:: 1.0.0
In previous versions, this function returned messages in LangChain v0 format.
Now, it returns messages in LangChain v1 format, which upgraded chat models now
expect to receive when passing back in message history. For backward
compatibility, this function will convert v0 message content to v1 format.
if block["type"] == "file":
parsed = _parse_data_uri(block["file"]["file_data"])
if parsed is not None:
parsed["type"] = "file"
if filename := block["file"].get("filename"):
parsed["filename"] = filename
return parsed
return block
.. dropdown:: v0 Content Block Schemas
if block["type"] == "input_audio":
data = block["input_audio"].get("data")
audio_format = block["input_audio"].get("format")
if data and audio_format:
return {
"type": "audio",
"source_type": "base64",
"data": data,
"mime_type": f"audio/{audio_format}",
``URLContentBlock``:
.. codeblock::
{
mime_type: NotRequired[str]
type: Literal['image', 'audio', 'file'],
source_type: Literal['url'],
url: str,
}
return block
return block
``Base64ContentBlock``:
.. codeblock::
def _normalize_messages(messages: Sequence[BaseMessage]) -> list[BaseMessage]:
"""Extend support for message formats.
{
mime_type: NotRequired[str]
type: Literal['image', 'audio', 'file'],
source_type: Literal['base64'],
data: str,
}
``IDContentBlock``:
(In practice, this was never used)
.. codeblock::
{
type: Literal['image', 'audio', 'file'],
source_type: Literal['id'],
id: str,
}
``PlainTextContentBlock``:
.. codeblock::
{
mime_type: NotRequired[str]
type: Literal['file'],
source_type: Literal['text'],
url: str,
}
If a v1 message is passed in, it will be returned as-is, meaning it is safe to
always pass in v1 messages to this function for assurance.
For posterity, here are the OpenAI Chat Completions schemas we expect:
Chat Completions image. Can be URL-based or base64-encoded. Supports MIME types
png, jpeg/jpg, webp, static gif:
{
"type": Literal['image_url'],
"image_url": {
"url": Union["data:$MIME_TYPE;base64,$BASE64_ENCODED_IMAGE", "$IMAGE_URL"],
"detail": Literal['low', 'high', 'auto'] = 'auto', # Supported by OpenAI
}
}
Chat Completions audio:
{
"type": Literal['input_audio'],
"input_audio": {
"format": Literal['wav', 'mp3'],
"data": str = "$BASE64_ENCODED_AUDIO",
},
}
Chat Completions files: either base64 or pre-uploaded file ID
{
"type": Literal['file'],
"file": Union[
{
"filename": Optional[str] = "$FILENAME",
"file_data": str = "$BASE64_ENCODED_FILE",
},
{
"file_id": str = "$FILE_ID", # For pre-uploaded files to OpenAI
},
],
}
Chat models implement support for images in OpenAI Chat Completions format, as well
as other multimodal data as standard data blocks. This function extends support to
audio and file data in OpenAI Chat Completions format by converting them to standard
data blocks.
"""
from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
_convert_legacy_v0_content_block_to_v1,
)
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
_convert_openai_format_to_data_block,
)
formatted_messages = []
for message in messages:
# We preserve input messages - the caller may reuse them elsewhere and expects
# them to remain unchanged. We only create a copy if we need to translate.
formatted_message = message
if isinstance(message.content, list):
for idx, block in enumerate(message.content):
# OpenAI Chat Completions multimodal data blocks to v1 standard
if (
isinstance(block, dict)
# Subset to (PDF) files and audio, as most relevant chat models
# support images in OAI format (and some may not yet support the
# standard data block format)
and block.get("type") in {"file", "input_audio"}
and _is_openai_data_block(block)
and block.get("type") in {"input_audio", "file"}
# Discriminate between OpenAI/LC format since they share `'type'`
and is_openai_data_block(block)
):
if formatted_message is message:
formatted_message = message.model_copy()
# Also shallow-copy content
formatted_message.content = list(formatted_message.content)
formatted_message = _ensure_message_copy(message, formatted_message)
converted_block = _convert_openai_format_to_data_block(block)
_update_content_block(formatted_message, idx, converted_block)
# Convert multimodal LangChain v0 to v1 standard content blocks
elif (
isinstance(block, dict)
and block.get("type")
in {
"image",
"audio",
"file",
}
and block.get("source_type") # v1 doesn't have `source_type`
in {
"url",
"base64",
"id",
"text",
}
):
formatted_message = _ensure_message_copy(message, formatted_message)
converted_block = _convert_legacy_v0_content_block_to_v1(block)
_update_content_block(formatted_message, idx, converted_block)
continue
# else, pass through blocks that look like they have v1 format unchanged
formatted_message.content[idx] = ( # type: ignore[index] # mypy confused by .model_copy
_convert_openai_format_to_data_block(block)
)
formatted_messages.append(formatted_message)
return formatted_messages
T = TypeVar("T", bound="BaseMessage")
def _ensure_message_copy(message: T, formatted_message: T) -> T:
"""Create a copy of the message if it hasn't been copied yet."""
if formatted_message is message:
formatted_message = message.model_copy()
# Shallow-copy content list to allow modifications
formatted_message.content = list(formatted_message.content)
return formatted_message
def _update_content_block(
formatted_message: "BaseMessage", idx: int, new_block: Union[ContentBlock, dict]
) -> None:
"""Update a content block at the given index, handling type issues."""
# Type ignore needed because:
# - `BaseMessage.content` is typed as `Union[str, list[Union[str, dict]]]`
# - When content is str, indexing fails (index error)
# - When content is list, the items are `Union[str, dict]` but we're assigning
# `Union[ContentBlock, dict]` where ContentBlock is richer than dict
# - This is safe because we only call this when we've verified content is a list and
# we're doing content block conversions
formatted_message.content[idx] = new_block # type: ignore[index, assignment]
def _update_message_content_to_blocks(message: T, output_version: str) -> T:
return message.model_copy(
update={
"content": message.content_blocks,
"response_metadata": {
**message.response_metadata,
"output_version": output_version,
},
}
)

View File

@@ -12,18 +12,20 @@ from typing import (
Callable,
Literal,
Optional,
TypeAlias,
TypeVar,
Union,
)
from pydantic import BaseModel, ConfigDict, Field, field_validator
from typing_extensions import TypeAlias, TypedDict, override
from typing_extensions import TypedDict, override
from langchain_core._api import deprecated
from langchain_core.caches import BaseCache
from langchain_core.callbacks import Callbacks
from langchain_core.globals import get_verbose
from langchain_core.messages import (
AIMessage,
AnyMessage,
BaseMessage,
MessageLikeRepresentation,
@@ -101,7 +103,7 @@ def _get_token_ids_default_method(text: str) -> list[int]:
LanguageModelInput = Union[PromptValue, str, Sequence[MessageLikeRepresentation]]
LanguageModelOutput = Union[BaseMessage, str]
LanguageModelLike = Runnable[LanguageModelInput, LanguageModelOutput]
LanguageModelOutputVar = TypeVar("LanguageModelOutputVar", BaseMessage, str)
LanguageModelOutputVar = TypeVar("LanguageModelOutputVar", AIMessage, str)
def _get_verbosity() -> bool:

View File

@@ -27,7 +27,10 @@ from langchain_core.callbacks import (
Callbacks,
)
from langchain_core.globals import get_llm_cache
from langchain_core.language_models._utils import _normalize_messages
from langchain_core.language_models._utils import (
_normalize_messages,
_update_message_content_to_blocks,
)
from langchain_core.language_models.base import (
BaseLanguageModel,
LangSmithParams,
@@ -36,16 +39,17 @@ from langchain_core.language_models.base import (
from langchain_core.load import dumpd, dumps
from langchain_core.messages import (
AIMessage,
AIMessageChunk,
AnyMessage,
BaseMessage,
BaseMessageChunk,
HumanMessage,
convert_to_messages,
convert_to_openai_image_block,
is_data_content_block,
message_chunk_to_message,
)
from langchain_core.messages.ai import _LC_ID_PREFIX
from langchain_core.messages.block_translators.openai import (
convert_to_openai_image_block,
)
from langchain_core.output_parsers.openai_tools import (
JsonOutputKeyToolsParser,
PydanticToolsParser,
@@ -69,6 +73,7 @@ from langchain_core.utils.function_calling import (
convert_to_openai_tool,
)
from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass
from langchain_core.utils.utils import LC_ID_PREFIX, from_env
if TYPE_CHECKING:
import uuid
@@ -129,7 +134,7 @@ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
if (
block.get("type") == "image"
and is_data_content_block(block)
and block.get("source_type") != "id"
and not ("file_id" in block or block.get("source_type") == "id")
):
if message_to_trace is message:
# Shallow copy
@@ -139,6 +144,22 @@ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
message_to_trace.content[idx] = ( # type: ignore[index] # mypy confused by .model_copy
convert_to_openai_image_block(block)
)
elif (
block.get("type") == "file"
and is_data_content_block(block) # v0 (image/audio/file) or v1
and "base64" in block
# Backward compat: convert v1 base64 blocks to v0
):
if message_to_trace is message:
# Shallow copy
message_to_trace = message.model_copy()
message_to_trace.content = list(message_to_trace.content)
message_to_trace.content[idx] = { # type: ignore[index]
**{k: v for k, v in block.items() if k != "base64"},
"data": block["base64"],
"source_type": "base64",
}
elif len(block) == 1 and "type" not in block:
# Tracing assumes all content blocks have a "type" key. Here
# we add this key if it is missing, and there's an obvious
@@ -221,7 +242,7 @@ def _format_ls_structured_output(ls_structured_output_format: Optional[dict]) ->
return ls_structured_output_format_dict
class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
"""Base class for chat models.
Key imperative methods:
@@ -330,6 +351,28 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
"""
output_version: Optional[str] = Field(
default_factory=from_env("LC_OUTPUT_VERSION", default=None)
)
"""Version of ``AIMessage`` output format to store in message content.
``AIMessage.content_blocks`` will lazily parse the contents of ``content`` into a
standard format. This flag can be used to additionally store the standard format
in message content, e.g., for serialization purposes.
Supported values:
- ``"v0"``: provider-specific format in content (can lazily-parse with
``.content_blocks``)
- ``"v1"``: standardized format in content (consistent with ``.content_blocks``)
Partner packages (e.g., ``langchain-openai``) can also use this field to roll out
new content formats in a backward-compatible way.
.. versionadded:: 1.0
"""
@model_validator(mode="before")
@classmethod
def raise_deprecation(cls, values: dict) -> Any:
@@ -388,21 +431,24 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
*,
stop: Optional[list[str]] = None,
**kwargs: Any,
) -> BaseMessage:
) -> AIMessage:
config = ensure_config(config)
return cast(
"ChatGeneration",
self.generate_prompt(
[self._convert_input(input)],
stop=stop,
callbacks=config.get("callbacks"),
tags=config.get("tags"),
metadata=config.get("metadata"),
run_name=config.get("run_name"),
run_id=config.pop("run_id", None),
**kwargs,
).generations[0][0],
).message
"AIMessage",
cast(
"ChatGeneration",
self.generate_prompt(
[self._convert_input(input)],
stop=stop,
callbacks=config.get("callbacks"),
tags=config.get("tags"),
metadata=config.get("metadata"),
run_name=config.get("run_name"),
run_id=config.pop("run_id", None),
**kwargs,
).generations[0][0],
).message,
)
@override
async def ainvoke(
@@ -412,7 +458,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
*,
stop: Optional[list[str]] = None,
**kwargs: Any,
) -> BaseMessage:
) -> AIMessage:
config = ensure_config(config)
llm_result = await self.agenerate_prompt(
[self._convert_input(input)],
@@ -424,7 +470,9 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
run_id=config.pop("run_id", None),
**kwargs,
)
return cast("ChatGeneration", llm_result.generations[0][0]).message
return cast(
"AIMessage", cast("ChatGeneration", llm_result.generations[0][0]).message
)
def _should_stream(
self,
@@ -469,11 +517,11 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
*,
stop: Optional[list[str]] = None,
**kwargs: Any,
) -> Iterator[BaseMessageChunk]:
) -> Iterator[AIMessageChunk]:
if not self._should_stream(async_api=False, **{**kwargs, "stream": True}):
# Model doesn't implement streaming, so use default implementation
yield cast(
"BaseMessageChunk",
"AIMessageChunk",
self.invoke(input, config=config, stop=stop, **kwargs),
)
else:
@@ -518,16 +566,41 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
try:
input_messages = _normalize_messages(messages)
run_id = "-".join((_LC_ID_PREFIX, str(run_manager.run_id)))
run_id = "-".join((LC_ID_PREFIX, str(run_manager.run_id)))
yielded = False
for chunk in self._stream(input_messages, stop=stop, **kwargs):
if chunk.message.id is None:
chunk.message.id = run_id
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
if self.output_version == "v1":
# Overwrite .content with .content_blocks
chunk.message = _update_message_content_to_blocks(
chunk.message, "v1"
)
run_manager.on_llm_new_token(
cast("str", chunk.message.content), chunk=chunk
)
chunks.append(chunk)
yield chunk.message
yield cast("AIMessageChunk", chunk.message)
yielded = True
# Yield a final empty chunk with chunk_position="last" if not yet
# yielded
if (
yielded
and isinstance(chunk.message, AIMessageChunk)
and not chunk.message.chunk_position
):
empty_content: Union[str, list] = (
"" if isinstance(chunk.message.content, str) else []
)
msg_chunk = AIMessageChunk(
content=empty_content, chunk_position="last", id=run_id
)
run_manager.on_llm_new_token(
"", chunk=ChatGenerationChunk(message=msg_chunk)
)
yield msg_chunk
except BaseException as e:
generations_with_error_metadata = _generate_response_from_error(e)
chat_generation_chunk = merge_chat_generation_chunks(chunks)
@@ -560,11 +633,11 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
*,
stop: Optional[list[str]] = None,
**kwargs: Any,
) -> AsyncIterator[BaseMessageChunk]:
) -> AsyncIterator[AIMessageChunk]:
if not self._should_stream(async_api=True, **{**kwargs, "stream": True}):
# No async or sync stream is implemented, so fall back to ainvoke
yield cast(
"BaseMessageChunk",
"AIMessageChunk",
await self.ainvoke(input, config=config, stop=stop, **kwargs),
)
return
@@ -611,7 +684,8 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
try:
input_messages = _normalize_messages(messages)
run_id = "-".join((_LC_ID_PREFIX, str(run_manager.run_id)))
run_id = "-".join((LC_ID_PREFIX, str(run_manager.run_id)))
yielded = False
async for chunk in self._astream(
input_messages,
stop=stop,
@@ -620,11 +694,34 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
if chunk.message.id is None:
chunk.message.id = run_id
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
if self.output_version == "v1":
# Overwrite .content with .content_blocks
chunk.message = _update_message_content_to_blocks(
chunk.message, "v1"
)
await run_manager.on_llm_new_token(
cast("str", chunk.message.content), chunk=chunk
)
chunks.append(chunk)
yield chunk.message
yield cast("AIMessageChunk", chunk.message)
yielded = True
# Yield a final empty chunk with chunk_position="last" if not yet yielded
if (
yielded
and isinstance(chunk.message, AIMessageChunk)
and not chunk.message.chunk_position
):
empty_content: Union[str, list] = (
"" if isinstance(chunk.message.content, str) else []
)
msg_chunk = AIMessageChunk(
content=empty_content, chunk_position="last", id=run_id
)
await run_manager.on_llm_new_token(
"", chunk=ChatGenerationChunk(message=msg_chunk)
)
yield msg_chunk
except BaseException as e:
generations_with_error_metadata = _generate_response_from_error(e)
chat_generation_chunk = merge_chat_generation_chunks(chunks)
@@ -1077,15 +1174,43 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
**kwargs,
):
chunks: list[ChatGenerationChunk] = []
run_id: Optional[str] = (
f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
)
yielded = False
for chunk in self._stream(messages, stop=stop, **kwargs):
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
if self.output_version == "v1":
# Overwrite .content with .content_blocks
chunk.message = _update_message_content_to_blocks(
chunk.message, "v1"
)
if run_manager:
if chunk.message.id is None:
chunk.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}"
chunk.message.id = run_id
run_manager.on_llm_new_token(
cast("str", chunk.message.content), chunk=chunk
)
chunks.append(chunk)
yielded = True
# Yield a final empty chunk with chunk_position="last" if not yet yielded
if (
yielded
and isinstance(chunk.message, AIMessageChunk)
and not chunk.message.chunk_position
):
empty_content: Union[str, list] = (
"" if isinstance(chunk.message.content, str) else []
)
chunk = ChatGenerationChunk(
message=AIMessageChunk(
content=empty_content, chunk_position="last", id=run_id
)
)
if run_manager:
run_manager.on_llm_new_token("", chunk=chunk)
chunks.append(chunk)
result = generate_from_stream(iter(chunks))
elif inspect.signature(self._generate).parameters.get("run_manager"):
result = self._generate(
@@ -1094,10 +1219,17 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
else:
result = self._generate(messages, stop=stop, **kwargs)
if self.output_version == "v1":
# Overwrite .content with .content_blocks
for generation in result.generations:
generation.message = _update_message_content_to_blocks(
generation.message, "v1"
)
# Add response metadata to each generation
for idx, generation in enumerate(result.generations):
if run_manager and generation.message.id is None:
generation.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
generation.message.id = f"{LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
generation.message.response_metadata = _gen_info_and_msg_metadata(
generation
)
@@ -1150,15 +1282,43 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
**kwargs,
):
chunks: list[ChatGenerationChunk] = []
run_id: Optional[str] = (
f"{LC_ID_PREFIX}-{run_manager.run_id}" if run_manager else None
)
yielded = False
async for chunk in self._astream(messages, stop=stop, **kwargs):
chunk.message.response_metadata = _gen_info_and_msg_metadata(chunk)
if self.output_version == "v1":
# Overwrite .content with .content_blocks
chunk.message = _update_message_content_to_blocks(
chunk.message, "v1"
)
if run_manager:
if chunk.message.id is None:
chunk.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}"
chunk.message.id = run_id
await run_manager.on_llm_new_token(
cast("str", chunk.message.content), chunk=chunk
)
chunks.append(chunk)
yielded = True
# Yield a final empty chunk with chunk_position="last" if not yet yielded
if (
yielded
and isinstance(chunk.message, AIMessageChunk)
and not chunk.message.chunk_position
):
empty_content: Union[str, list] = (
"" if isinstance(chunk.message.content, str) else []
)
chunk = ChatGenerationChunk(
message=AIMessageChunk(
content=empty_content, chunk_position="last", id=run_id
)
)
if run_manager:
await run_manager.on_llm_new_token("", chunk=chunk)
chunks.append(chunk)
result = generate_from_stream(iter(chunks))
elif inspect.signature(self._agenerate).parameters.get("run_manager"):
result = await self._agenerate(
@@ -1167,10 +1327,17 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
else:
result = await self._agenerate(messages, stop=stop, **kwargs)
if self.output_version == "v1":
# Overwrite .content with .content_blocks
for generation in result.generations:
generation.message = _update_message_content_to_blocks(
generation.message, "v1"
)
# Add response metadata to each generation
for idx, generation in enumerate(result.generations):
if run_manager and generation.message.id is None:
generation.message.id = f"{_LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
generation.message.id = f"{LC_ID_PREFIX}-{run_manager.run_id}-{idx}"
generation.message.response_metadata = _gen_info_and_msg_metadata(
generation
)
@@ -1443,7 +1610,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
*,
tool_choice: Optional[Union[str]] = None,
**kwargs: Any,
) -> Runnable[LanguageModelInput, BaseMessage]:
) -> Runnable[LanguageModelInput, AIMessage]:
"""Bind tools to the model.
Args:

View File

@@ -4,7 +4,7 @@ import asyncio
import re
import time
from collections.abc import AsyncIterator, Iterator
from typing import Any, Optional, Union, cast
from typing import Any, Literal, Optional, Union, cast
from typing_extensions import override
@@ -113,7 +113,12 @@ class FakeListChatModel(SimpleChatModel):
):
raise FakeListChatModelError
yield ChatGenerationChunk(message=AIMessageChunk(content=c))
chunk_position: Optional[Literal["last"]] = (
"last" if i_c == len(response) - 1 else None
)
yield ChatGenerationChunk(
message=AIMessageChunk(content=c, chunk_position=chunk_position)
)
@override
async def _astream(
@@ -136,7 +141,12 @@ class FakeListChatModel(SimpleChatModel):
and i_c == self.error_on_chunk_number
):
raise FakeListChatModelError
yield ChatGenerationChunk(message=AIMessageChunk(content=c))
chunk_position: Optional[Literal["last"]] = (
"last" if i_c == len(response) - 1 else None
)
yield ChatGenerationChunk(
message=AIMessageChunk(content=c, chunk_position=chunk_position)
)
@property
@override
@@ -152,7 +162,7 @@ class FakeListChatModel(SimpleChatModel):
*,
return_exceptions: bool = False,
**kwargs: Any,
) -> list[BaseMessage]:
) -> list[AIMessage]:
if isinstance(config, list):
return [self.invoke(m, c, **kwargs) for m, c in zip(inputs, config)]
return [self.invoke(m, config, **kwargs) for m in inputs]
@@ -165,7 +175,7 @@ class FakeListChatModel(SimpleChatModel):
*,
return_exceptions: bool = False,
**kwargs: Any,
) -> list[BaseMessage]:
) -> list[AIMessage]:
if isinstance(config, list):
# do Not use an async iterator here because need explicit ordering
return [await self.ainvoke(m, c, **kwargs) for m, c in zip(inputs, config)]
@@ -284,10 +294,16 @@ class GenericFakeChatModel(BaseChatModel):
content_chunks = cast("list[str]", re.split(r"(\s)", content))
for token in content_chunks:
for idx, token in enumerate(content_chunks):
chunk = ChatGenerationChunk(
message=AIMessageChunk(content=token, id=message.id)
)
if (
idx == len(content_chunks) - 1
and isinstance(chunk.message, AIMessageChunk)
and not message.additional_kwargs
):
chunk.message.chunk_position = "last"
if run_manager:
run_manager.on_llm_new_token(token, chunk=chunk)
yield chunk

View File

@@ -1466,10 +1466,10 @@ class BaseLLM(BaseLanguageModel[str], ABC):
prompt_dict = self.dict()
if save_path.suffix == ".json":
with save_path.open("w") as f:
with save_path.open("w", encoding="utf-8") as f:
json.dump(prompt_dict, f, indent=4)
elif save_path.suffix.endswith((".yaml", ".yml")):
with save_path.open("w") as f:
with save_path.open("w", encoding="utf-8") as f:
yaml.dump(prompt_dict, f, default_flow_style=False)
else:
msg = f"{save_path} must be json or yaml"

View File

@@ -6,7 +6,7 @@ from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.load.dump import dumpd, dumps
from langchain_core.load.load import InitValidator, loads
from langchain_core.load.load import loads
from langchain_core.load.serializable import Serializable
# Unfortunately, we have to eagerly import load from langchain_core/load/load.py
@@ -15,19 +15,11 @@ if TYPE_CHECKING:
# the `from langchain_core.load.load import load` absolute import should also work.
from langchain_core.load.load import load
__all__ = (
"InitValidator",
"Serializable",
"dumpd",
"dumps",
"load",
"loads",
)
__all__ = ("Serializable", "dumpd", "dumps", "load", "loads")
_dynamic_imports = {
"dumpd": "dump",
"dumps": "dump",
"InitValidator": "load",
"loads": "load",
"Serializable": "serializable",
}

View File

@@ -1,176 +0,0 @@
"""Validation utilities for LangChain serialization.
Provides escape-based protection against injection attacks in serialized objects. The
approach uses an allowlist design: only dicts explicitly produced by
`Serializable.to_json()` are treated as LC objects during deserialization.
## How escaping works
During serialization, plain dicts (user data) that contain an `'lc'` key are wrapped:
```python
{"lc": 1, ...} # user data that looks like LC object
# becomes:
{"__lc_escaped__": {"lc": 1, ...}}
```
During deserialization, escaped dicts are unwrapped and returned as plain dicts,
NOT instantiated as LC objects.
"""
from typing import Any
_LC_ESCAPED_KEY = "__lc_escaped__"
"""Sentinel key used to mark escaped user dicts during serialization.
When a plain dict contains 'lc' key (which could be confused with LC objects),
we wrap it as {"__lc_escaped__": {...original...}}.
"""
def _needs_escaping(obj: dict[str, Any]) -> bool:
"""Check if a dict needs escaping to prevent confusion with LC objects.
A dict needs escaping if:
1. It has an `'lc'` key (could be confused with LC serialization format)
2. It has only the escape key (would be mistaken for an escaped dict)
"""
return "lc" in obj or (len(obj) == 1 and _LC_ESCAPED_KEY in obj)
def _escape_dict(obj: dict[str, Any]) -> dict[str, Any]:
"""Wrap a dict in the escape marker.
Example:
```python
{"key": "value"} # becomes {"__lc_escaped__": {"key": "value"}}
```
"""
return {_LC_ESCAPED_KEY: obj}
def _is_escaped_dict(obj: dict[str, Any]) -> bool:
"""Check if a dict is an escaped user dict.
Example:
```python
{"__lc_escaped__": {...}} # is an escaped dict
```
"""
return len(obj) == 1 and _LC_ESCAPED_KEY in obj
def _serialize_value(obj: Any) -> Any:
"""Serialize a value with escaping of user dicts.
Called recursively on kwarg values to escape any plain dicts that could be confused
with LC objects.
Args:
obj: The value to serialize.
Returns:
The serialized value with user dicts escaped as needed.
"""
from langchain_core.load.serializable import ( # noqa: PLC0415
Serializable,
to_json_not_implemented,
)
if isinstance(obj, Serializable):
# This is an LC object - serialize it properly (not escaped)
return _serialize_lc_object(obj)
if isinstance(obj, dict):
if not all(isinstance(k, (str, int, float, bool, type(None))) for k in obj):
# if keys are not json serializable
return to_json_not_implemented(obj)
# Check if dict needs escaping BEFORE recursing into values.
# If it needs escaping, wrap it as-is - the contents are user data that
# will be returned as-is during deserialization (no instantiation).
# This prevents re-escaping of already-escaped nested content.
if _needs_escaping(obj):
return _escape_dict(obj)
# Safe dict (no 'lc' key) - recurse into values
return {k: _serialize_value(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [_serialize_value(item) for item in obj]
if isinstance(obj, (str, int, float, bool, type(None))):
return obj
# Non-JSON-serializable object (datetime, custom objects, etc.)
return to_json_not_implemented(obj)
def _is_lc_secret(obj: Any) -> bool:
"""Check if an object is a LangChain secret marker."""
expected_num_keys = 3
return (
isinstance(obj, dict)
and obj.get("lc") == 1
and obj.get("type") == "secret"
and "id" in obj
and len(obj) == expected_num_keys
)
def _serialize_lc_object(obj: Any) -> dict[str, Any]:
"""Serialize a `Serializable` object with escaping of user data in kwargs.
Args:
obj: The `Serializable` object to serialize.
Returns:
The serialized dict with user data in kwargs escaped as needed.
Note:
Kwargs values are processed with `_serialize_value` to escape user data (like
metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
skipped because `to_json()` replaces their values with secret markers.
"""
from langchain_core.load.serializable import Serializable # noqa: PLC0415
if not isinstance(obj, Serializable):
msg = f"Expected Serializable, got {type(obj)}"
raise TypeError(msg)
serialized: dict[str, Any] = dict(obj.to_json())
# Process kwargs to escape user data that could be confused with LC objects
# Skip secret fields - to_json() already converted them to secret markers
if serialized.get("type") == "constructor" and "kwargs" in serialized:
serialized["kwargs"] = {
k: v if _is_lc_secret(v) else _serialize_value(v)
for k, v in serialized["kwargs"].items()
}
return serialized
def _unescape_value(obj: Any) -> Any:
"""Unescape a value, processing escape markers in dict values and lists.
When an escaped dict is encountered (`{"__lc_escaped__": ...}`), it's
unwrapped and the contents are returned AS-IS (no further processing).
The contents represent user data that should not be modified.
For regular dicts and lists, we recurse to find any nested escape markers.
Args:
obj: The value to unescape.
Returns:
The unescaped value.
"""
if isinstance(obj, dict):
if _is_escaped_dict(obj):
# Unwrap and return the user data as-is (no further unescaping).
# The contents are user data that may contain more escape keys,
# but those are part of the user's actual data.
return obj[_LC_ESCAPED_KEY]
# Regular dict - recurse into values to find nested escape markers
return {k: _unescape_value(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_unescape_value(item) for item in obj]
return obj

View File

@@ -1,26 +1,10 @@
"""Serialize LangChain objects to JSON.
Provides `dumps` (to JSON string) and `dumpd` (to dict) for serializing
`Serializable` objects.
## Escaping
During serialization, plain dicts (user data) that contain an `'lc'` key are escaped
by wrapping them: `{"__lc_escaped__": {...original...}}`. This prevents injection
attacks where malicious data could trick the deserializer into instantiating
arbitrary classes. The escape marker is removed during deserialization.
This is an allowlist approach: only dicts explicitly produced by
`Serializable.to_json()` are treated as LC objects; everything else is escaped if it
could be confused with the LC format.
"""
"""Dump objects to json."""
import json
from typing import Any
from pydantic import BaseModel
from langchain_core.load._validation import _serialize_value
from langchain_core.load.serializable import Serializable, to_json_not_implemented
from langchain_core.messages import AIMessage
from langchain_core.outputs import ChatGeneration
@@ -41,20 +25,6 @@ def default(obj: Any) -> Any:
def _dump_pydantic_models(obj: Any) -> Any:
"""Convert nested Pydantic models to dicts for JSON serialization.
Handles the special case where a `ChatGeneration` contains an `AIMessage`
with a parsed Pydantic model in `additional_kwargs["parsed"]`. Since
Pydantic models aren't directly JSON serializable, this converts them to
dicts.
Args:
obj: The object to process.
Returns:
A copy of the object with nested Pydantic models converted to dicts, or
the original object unchanged if no conversion was needed.
"""
if (
isinstance(obj, ChatGeneration)
and isinstance(obj.message, AIMessage)
@@ -70,18 +40,12 @@ def _dump_pydantic_models(obj: Any) -> Any:
def dumps(obj: Any, *, pretty: bool = False, **kwargs: Any) -> str:
"""Return a json string representation of an object.
Note:
Plain dicts containing an `'lc'` key are automatically escaped to prevent
confusion with LC serialization format. The escape marker is removed during
deserialization.
Args:
obj: The object to dump.
pretty: Whether to pretty print the json.
If `True`, the json will be indented by either 2 spaces or the amount
provided in the `indent` kwarg.
**kwargs: Additional arguments to pass to `json.dumps`
pretty: Whether to pretty print the json. If true, the json will be
indented with 2 spaces (if no indent is provided as part of kwargs).
Default is False.
kwargs: Additional arguments to pass to json.dumps
Returns:
A json string representation of the object.
@@ -92,23 +56,25 @@ def dumps(obj: Any, *, pretty: bool = False, **kwargs: Any) -> str:
if "default" in kwargs:
msg = "`default` should not be passed to dumps"
raise ValueError(msg)
obj = _dump_pydantic_models(obj)
serialized = _serialize_value(obj)
if pretty:
indent = kwargs.pop("indent", 2)
return json.dumps(serialized, indent=indent, **kwargs)
return json.dumps(serialized, **kwargs)
try:
obj = _dump_pydantic_models(obj)
if pretty:
indent = kwargs.pop("indent", 2)
return json.dumps(obj, default=default, indent=indent, **kwargs)
return json.dumps(obj, default=default, **kwargs)
except TypeError:
if pretty:
indent = kwargs.pop("indent", 2)
return json.dumps(to_json_not_implemented(obj), indent=indent, **kwargs)
return json.dumps(to_json_not_implemented(obj), **kwargs)
def dumpd(obj: Any) -> Any:
"""Return a dict representation of an object.
Note:
Plain dicts containing an `'lc'` key are automatically escaped to prevent
confusion with LC serialization format. The escape marker is removed during
deserialization.
.. note::
Unfortunately this function is not as efficient as it could be because it first
dumps the object to a json string and then loads it back into a dictionary.
Args:
obj: The object to dump.
@@ -116,5 +82,4 @@ def dumpd(obj: Any) -> Any:
Returns:
dictionary that can be serialized to json using json.dumps
"""
obj = _dump_pydantic_models(obj)
return _serialize_value(obj)
return json.loads(dumps(obj))

View File

@@ -1,85 +1,11 @@
"""Load LangChain objects from JSON strings or objects.
## How it works
Each `Serializable` LangChain object has a unique identifier (its "class path"), which
is a list of strings representing the module path and class name. For example:
- `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
- `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
When deserializing, the class path from the JSON `'id'` field is checked against an
allowlist. If the class is not in the allowlist, deserialization raises a `ValueError`.
## Security model
The `allowed_objects` parameter controls which classes can be deserialized:
- **`'core'` (default)**: Allow classes defined in the serialization mappings for
langchain_core.
- **`'all'`**: Allow classes defined in the serialization mappings. This
includes core LangChain types (messages, prompts, documents, etc.) and trusted
partner integrations. See `langchain_core.load.mapping` for the full list.
- **Explicit list of classes**: Only those specific classes are allowed.
For simple data types like messages and documents, the default allowlist is safe to use.
These classes do not perform side effects during initialization.
!!! note "Side effects in allowed classes"
Deserialization calls `__init__` on allowed classes. If those classes perform side
effects during initialization (network calls, file operations, etc.), those side
effects will occur. The allowlist prevents instantiation of classes outside the
allowlist, but does not sandbox the allowed classes themselves.
Import paths are also validated against trusted namespaces before any module is
imported.
### Injection protection (escape-based)
During serialization, plain dicts that contain an `'lc'` key are escaped by wrapping
them: `{"__lc_escaped__": {...}}`. During deserialization, escaped dicts are unwrapped
and returned as plain dicts, NOT instantiated as LC objects.
This is an allowlist approach: only dicts explicitly produced by
`Serializable.to_json()` (which are NOT escaped) are treated as LC objects;
everything else is user data.
Even if an attacker's payload includes `__lc_escaped__` wrappers, it will be unwrapped
to plain dicts and NOT instantiated as malicious objects.
## Examples
```python
from langchain_core.load import load
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import AIMessage, HumanMessage
# Use default allowlist (classes from mappings) - recommended
obj = load(data)
# Allow only specific classes (most restrictive)
obj = load(
data,
allowed_objects=[
ChatPromptTemplate,
AIMessage,
HumanMessage,
],
)
```
"""
from __future__ import annotations
"""Load LangChain objects from JSON strings or objects."""
import importlib
import json
import os
from collections.abc import Callable, Iterable
from typing import Any, Literal, Optional, cast
from typing import Any, Optional
from langchain_core._api import beta
from langchain_core.load._validation import _is_escaped_dict, _unescape_value
from langchain_core.load.mapping import (
_JS_SERIALIZABLE_MAPPING,
_OG_SERIALIZABLE_MAPPING,
@@ -118,167 +44,12 @@ ALL_SERIALIZABLE_MAPPINGS = {
**_JS_SERIALIZABLE_MAPPING,
}
# Cache for the default allowed class paths computed from mappings
# Maps mode ("all" or "core") to the cached set of paths
_default_class_paths_cache: dict[str, set[tuple[str, ...]]] = {}
def _get_default_allowed_class_paths(
allowed_object_mode: Literal["all", "core"],
) -> set[tuple[str, ...]]:
"""Get the default allowed class paths from the serialization mappings.
This uses the mappings as the source of truth for what classes are allowed
by default. Both the legacy paths (keys) and current paths (values) are included.
Args:
allowed_object_mode: either `'all'` or `'core'`.
Returns:
Set of class path tuples that are allowed by default.
"""
if allowed_object_mode in _default_class_paths_cache:
return _default_class_paths_cache[allowed_object_mode]
allowed_paths: set[tuple[str, ...]] = set()
for key, value in ALL_SERIALIZABLE_MAPPINGS.items():
if allowed_object_mode == "core" and value[0] != "langchain_core":
continue
allowed_paths.add(key)
allowed_paths.add(value)
_default_class_paths_cache[allowed_object_mode] = allowed_paths
return _default_class_paths_cache[allowed_object_mode]
def _block_jinja2_templates(
class_path: tuple[str, ...],
kwargs: dict[str, Any],
) -> None:
"""Block jinja2 templates during deserialization for security.
Jinja2 templates can execute arbitrary code, so they are blocked by default when
deserializing objects with `template_format='jinja2'`.
Note:
We intentionally do NOT check the `class_path` here to keep this simple and
future-proof. If any new class is added that accepts `template_format='jinja2'`,
it will be automatically blocked without needing to update this function.
Args:
class_path: The class path tuple being deserialized (unused).
kwargs: The kwargs dict for the class constructor.
Raises:
ValueError: If `template_format` is `'jinja2'`.
"""
_ = class_path # Unused - see docstring for rationale. Kept to satisfy signature.
if kwargs.get("template_format") == "jinja2":
msg = (
"Jinja2 templates are not allowed during deserialization for security "
"reasons. Use 'f-string' template format instead, or explicitly allow "
"jinja2 by providing a custom init_validator."
)
raise ValueError(msg)
def default_init_validator(
class_path: tuple[str, ...],
kwargs: dict[str, Any],
) -> None:
"""Default init validator that blocks jinja2 templates.
This is the default validator used by `load()` and `loads()` when no custom
validator is provided.
Args:
class_path: The class path tuple being deserialized.
kwargs: The kwargs dict for the class constructor.
Raises:
ValueError: If template_format is `'jinja2'`.
"""
_block_jinja2_templates(class_path, kwargs)
AllowedObject = type[Serializable]
"""Type alias for classes that can be included in the `allowed_objects` parameter.
Must be a `Serializable` subclass (the class itself, not an instance).
"""
InitValidator = Callable[[tuple[str, ...], dict[str, Any]], None]
"""Type alias for a callable that validates kwargs during deserialization.
The callable receives:
- `class_path`: A tuple of strings identifying the class being instantiated
(e.g., `('langchain', 'schema', 'messages', 'AIMessage')`).
- `kwargs`: The kwargs dict that will be passed to the constructor.
The validator should raise an exception if the object should not be deserialized.
"""
def _compute_allowed_class_paths(
allowed_objects: Iterable[AllowedObject],
import_mappings: dict[tuple[str, ...], tuple[str, ...]],
) -> set[tuple[str, ...]]:
"""Return allowed class paths from an explicit list of classes.
A class path is a tuple of strings identifying a serializable class, derived from
`Serializable.lc_id()`. For example: `('langchain_core', 'messages', 'AIMessage')`.
Args:
allowed_objects: Iterable of `Serializable` subclasses to allow.
import_mappings: Mapping of legacy class paths to current class paths.
Returns:
Set of allowed class paths.
Example:
```python
# Allow a specific class
_compute_allowed_class_paths([MyPrompt], {}) ->
{("langchain_core", "prompts", "MyPrompt")}
# Include legacy paths that map to the same class
import_mappings = {("old", "Prompt"): ("langchain_core", "prompts", "MyPrompt")}
_compute_allowed_class_paths([MyPrompt], import_mappings) ->
{("langchain_core", "prompts", "MyPrompt"), ("old", "Prompt")}
```
"""
allowed_objects_list = list(allowed_objects)
allowed_class_paths: set[tuple[str, ...]] = set()
for allowed_obj in allowed_objects_list:
if not isinstance(allowed_obj, type) or not issubclass(
allowed_obj, Serializable
):
msg = "allowed_objects must contain Serializable subclasses."
raise TypeError(msg)
class_path = tuple(allowed_obj.lc_id())
allowed_class_paths.add(class_path)
# Add legacy paths that map to the same class.
for mapping_key, mapping_value in import_mappings.items():
if tuple(mapping_value) == class_path:
allowed_class_paths.add(mapping_key)
return allowed_class_paths
class Reviver:
"""Reviver for JSON objects.
Used as the `object_hook` for `json.loads` to reconstruct LangChain objects from
their serialized JSON representation.
Only classes in the allowlist can be instantiated.
"""
"""Reviver for JSON objects."""
def __init__(
self,
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
secrets_map: Optional[dict[str, str]] = None,
valid_namespaces: Optional[list[str]] = None,
secrets_from_env: bool = True, # noqa: FBT001,FBT002
@@ -287,51 +58,22 @@ class Reviver:
] = None,
*,
ignore_unserializable_fields: bool = False,
init_validator: InitValidator | None = default_init_validator,
) -> None:
"""Initialize the reviver.
Args:
allowed_objects: Allowlist of classes that can be deserialized.
- `'core'` (default): Allow classes defined in the serialization
mappings for `langchain_core`.
- `'all'`: Allow classes defined in the serialization mappings.
This includes core LangChain types (messages, prompts, documents,
etc.) and trusted partner integrations. See
`langchain_core.load.mapping` for the full list.
- Explicit list of classes: Only those specific classes are allowed.
secrets_map: A map of secrets to load.
If a secret is not found in the map, it will be loaded from the
environment if `secrets_from_env` is `True`.
Defaults to `None`.
valid_namespaces: Additional namespaces (modules) to allow during
deserialization, beyond the default trusted namespaces.
Defaults to `None`.
secrets_map: A map of secrets to load. If a secret is not found in
the map, it will be loaded from the environment if `secrets_from_env`
is True. Defaults to None.
valid_namespaces: A list of additional namespaces (modules)
to allow to be deserialized. Defaults to None.
secrets_from_env: Whether to load secrets from the environment.
Defaults to `True`.
additional_import_mappings: A dictionary of additional namespace mappings.
Defaults to True.
additional_import_mappings: A dictionary of additional namespace mappings
You can use this to override default mappings or add new mappings.
When `allowed_objects` is `None` (using defaults), paths from these
mappings are also added to the allowed class paths.
Defaults to `None`.
Defaults to None.
ignore_unserializable_fields: Whether to ignore unserializable fields.
Defaults to `False`.
init_validator: Optional callable to validate kwargs before instantiation.
If provided, this function is called with `(class_path, kwargs)` where
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
The validator should raise an exception if the object should not be
deserialized, otherwise return `None`.
Defaults to `default_init_validator` which blocks jinja2 templates.
Defaults to False.
"""
self.secrets_from_env = secrets_from_env
self.secrets_map = secrets_map or {}
@@ -350,26 +92,7 @@ class Reviver:
if self.additional_import_mappings
else ALL_SERIALIZABLE_MAPPINGS
)
# Compute allowed class paths:
# - "all" -> use default paths from mappings (+ additional_import_mappings)
# - Explicit list -> compute from those classes
if allowed_objects in ("all", "core"):
self.allowed_class_paths: set[tuple[str, ...]] | None = (
_get_default_allowed_class_paths(
cast("Literal['all', 'core']", allowed_objects)
).copy()
)
# Add paths from additional_import_mappings to the defaults
if self.additional_import_mappings:
for key, value in self.additional_import_mappings.items():
self.allowed_class_paths.add(key)
self.allowed_class_paths.add(value)
else:
self.allowed_class_paths = _compute_allowed_class_paths(
cast("Iterable[AllowedObject]", allowed_objects), self.import_mappings
)
self.ignore_unserializable_fields = ignore_unserializable_fields
self.init_validator = init_validator
def __call__(self, value: dict[str, Any]) -> Any:
"""Revive the value.
@@ -420,20 +143,6 @@ class Reviver:
[*namespace, name] = value["id"]
mapping_key = tuple(value["id"])
if (
self.allowed_class_paths is not None
and mapping_key not in self.allowed_class_paths
):
msg = (
f"Deserialization of {mapping_key!r} is not allowed. "
"The default (allowed_objects='core') only permits core "
"langchain-core classes. To allow trusted partner integrations, "
"use allowed_objects='all'. Alternatively, pass an explicit list "
"of allowed classes via allowed_objects=[...]. "
"See langchain_core.load.mapping for the full allowlist."
)
raise ValueError(msg)
if (
namespace[0] not in self.valid_namespaces
# The root namespace ["langchain"] is not a valid identifier.
@@ -441,11 +150,13 @@ class Reviver:
):
msg = f"Invalid namespace: {value}"
raise ValueError(msg)
# Determine explicit import path
# Has explicit import path.
if mapping_key in self.import_mappings:
import_path = self.import_mappings[mapping_key]
# Split into module and name
import_dir, name = import_path[:-1], import_path[-1]
# Import module
mod = importlib.import_module(".".join(import_dir))
elif namespace[0] in DISALLOW_LOAD_FROM_PATH:
msg = (
"Trying to deserialize something that cannot "
@@ -453,16 +164,9 @@ class Reviver:
f"{mapping_key}."
)
raise ValueError(msg)
# Otherwise, treat namespace as path.
else:
# Otherwise, treat namespace as path.
import_dir = namespace
# Validate import path is in trusted namespaces before importing
if import_dir[0] not in self.valid_namespaces:
msg = f"Invalid namespace: {value}"
raise ValueError(msg)
mod = importlib.import_module(".".join(import_dir))
mod = importlib.import_module(".".join(namespace))
cls = getattr(mod, name)
@@ -474,10 +178,6 @@ class Reviver:
# We don't need to recurse on kwargs
# as json.loads will do that for us.
kwargs = value.get("kwargs", {})
if self.init_validator is not None:
self.init_validator(mapping_key, kwargs)
return cls(**kwargs)
return value
@@ -487,76 +187,43 @@ class Reviver:
def loads(
text: str,
*,
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
secrets_map: Optional[dict[str, str]] = None,
valid_namespaces: Optional[list[str]] = None,
secrets_from_env: bool = True,
additional_import_mappings: Optional[dict[tuple[str, ...], tuple[str, ...]]] = None,
ignore_unserializable_fields: bool = False,
init_validator: InitValidator | None = default_init_validator,
) -> Any:
"""Revive a LangChain class from a JSON string.
Equivalent to `load(json.loads(text))`.
Only classes in the allowlist can be instantiated. The default allowlist includes
core LangChain types (messages, prompts, documents, etc.). See
`langchain_core.load.mapping` for the full list.
Args:
text: The string to load.
allowed_objects: Allowlist of classes that can be deserialized.
- `'core'` (default): Allow classes defined in the serialization mappings
for langchain_core.
- `'all'`: Allow classes defined in the serialization mappings.
This includes core LangChain types (messages, prompts, documents, etc.)
and trusted partner integrations. See `langchain_core.load.mapping` for
the full list.
- Explicit list of classes: Only those specific classes are allowed.
- `[]`: Disallow all deserialization (will raise on any object).
secrets_map: A map of secrets to load.
If a secret is not found in the map, it will be loaded from the environment
if `secrets_from_env` is `True`. Defaults to None.
valid_namespaces: Additional namespaces (modules) to allow during
deserialization, beyond the default trusted namespaces. Defaults to None.
secrets_map: A map of secrets to load. If a secret is not found in
the map, it will be loaded from the environment if `secrets_from_env`
is True. Defaults to None.
valid_namespaces: A list of additional namespaces (modules)
to allow to be deserialized. Defaults to None.
secrets_from_env: Whether to load secrets from the environment.
Defaults to True.
additional_import_mappings: A dictionary of additional namespace mappings.
additional_import_mappings: A dictionary of additional namespace mappings
You can use this to override default mappings or add new mappings.
When `allowed_objects` is `None` (using defaults), paths from these
mappings are also added to the allowed class paths. Defaults to None.
Defaults to None.
ignore_unserializable_fields: Whether to ignore unserializable fields.
Defaults to False.
init_validator: Optional callable to validate kwargs before instantiation.
If provided, this function is called with `(class_path, kwargs)` where
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
The validator should raise an exception if the object should not be
deserialized, otherwise return `None`. Defaults to
`default_init_validator` which blocks jinja2 templates.
Returns:
Revived LangChain objects.
Raises:
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
"""
# Parse JSON and delegate to load() for proper escape handling
raw_obj = json.loads(text)
return load(
raw_obj,
allowed_objects=allowed_objects,
secrets_map=secrets_map,
valid_namespaces=valid_namespaces,
secrets_from_env=secrets_from_env,
additional_import_mappings=additional_import_mappings,
ignore_unserializable_fields=ignore_unserializable_fields,
init_validator=init_validator,
return json.loads(
text,
object_hook=Reviver(
secrets_map,
valid_namespaces,
secrets_from_env,
additional_import_mappings,
ignore_unserializable_fields=ignore_unserializable_fields,
),
)
@@ -564,107 +231,46 @@ def loads(
def load(
obj: Any,
*,
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
secrets_map: Optional[dict[str, str]] = None,
valid_namespaces: Optional[list[str]] = None,
secrets_from_env: bool = True,
additional_import_mappings: Optional[dict[tuple[str, ...], tuple[str, ...]]] = None,
ignore_unserializable_fields: bool = False,
init_validator: InitValidator | None = default_init_validator,
) -> Any:
"""Revive a LangChain class from a JSON object.
Use this if you already have a parsed JSON object, eg. from `json.load` or
`orjson.loads`.
Only classes in the allowlist can be instantiated. The default allowlist includes
core LangChain types (messages, prompts, documents, etc.). See
`langchain_core.load.mapping` for the full list.
Use this if you already have a parsed JSON object,
eg. from `json.load` or `orjson.loads`.
Args:
obj: The object to load.
allowed_objects: Allowlist of classes that can be deserialized.
- `'core'` (default): Allow classes defined in the serialization mappings
for langchain_core.
- `'all'`: Allow classes defined in the serialization mappings.
This includes core LangChain types (messages, prompts, documents, etc.)
and trusted partner integrations. See `langchain_core.load.mapping` for
the full list.
- Explicit list of classes: Only those specific classes are allowed.
- `[]`: Disallow all deserialization (will raise on any object).
secrets_map: A map of secrets to load.
If a secret is not found in the map, it will be loaded from the environment
if `secrets_from_env` is `True`. Defaults to None.
valid_namespaces: Additional namespaces (modules) to allow during
deserialization, beyond the default trusted namespaces. Defaults to None.
secrets_map: A map of secrets to load. If a secret is not found in
the map, it will be loaded from the environment if `secrets_from_env`
is True. Defaults to None.
valid_namespaces: A list of additional namespaces (modules)
to allow to be deserialized. Defaults to None.
secrets_from_env: Whether to load secrets from the environment.
Defaults to True.
additional_import_mappings: A dictionary of additional namespace mappings.
additional_import_mappings: A dictionary of additional namespace mappings
You can use this to override default mappings or add new mappings.
When `allowed_objects` is `None` (using defaults), paths from these
mappings are also added to the allowed class paths. Defaults to None.
Defaults to None.
ignore_unserializable_fields: Whether to ignore unserializable fields.
Defaults to False.
init_validator: Optional callable to validate kwargs before instantiation.
If provided, this function is called with `(class_path, kwargs)` where
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
The validator should raise an exception if the object should not be
deserialized, otherwise return `None`. Defaults to
`default_init_validator` which blocks jinja2 templates.
Returns:
Revived LangChain objects.
Raises:
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
Example:
```python
from langchain_core.load import load, dumpd
from langchain_core.messages import AIMessage
msg = AIMessage(content="Hello")
data = dumpd(msg)
# Deserialize using default allowlist
loaded = load(data)
# Or with explicit allowlist
loaded = load(data, allowed_objects=[AIMessage])
# Or extend defaults with additional mappings
loaded = load(
data,
additional_import_mappings={
("my_pkg", "MyClass"): ("my_pkg", "module", "MyClass"),
},
)
```
"""
reviver = Reviver(
allowed_objects,
secrets_map,
valid_namespaces,
secrets_from_env,
additional_import_mappings,
ignore_unserializable_fields=ignore_unserializable_fields,
init_validator=init_validator,
)
def _load(obj: Any) -> Any:
if isinstance(obj, dict):
# Check for escaped dict FIRST (before recursing).
# Escaped dicts are user data that should NOT be processed as LC objects.
if _is_escaped_dict(obj):
return _unescape_value(obj)
# Not escaped - recurse into children then apply reviver
# Need to revive leaf nodes before reviving this node
loaded_obj = {k: _load(v) for k, v in obj.items()}
return reviver(loaded_obj)
if isinstance(obj, list):

View File

@@ -1,19 +1,21 @@
"""Serialization mapping.
This file contains a mapping between the `lc_namespace` path for a given
subclass that implements from `Serializable` to the namespace
This file contains a mapping between the lc_namespace path for a given
subclass that implements from Serializable to the namespace
where that class is actually located.
This mapping helps maintain the ability to serialize and deserialize
well-known LangChain objects even if they are moved around in the codebase
across different LangChain versions.
For example, the code for the `AIMessage` class is located in
`langchain_core.messages.ai.AIMessage`. This message is associated with the
`lc_namespace` of `["langchain", "schema", "messages", "AIMessage"]`,
because this code was originally in `langchain.schema.messages.AIMessage`.
For example,
The mapping allows us to deserialize an `AIMessage` created with an older
The code for AIMessage class is located in langchain_core.messages.ai.AIMessage,
This message is associated with the lc_namespace
["langchain", "schema", "messages", "AIMessage"],
because this code was originally in langchain.schema.messages.AIMessage.
The mapping allows us to deserialize an AIMessage created with an older
version of LangChain where the code was in a different location.
"""
@@ -273,11 +275,6 @@ SERIALIZABLE_MAPPING: dict[tuple[str, ...], tuple[str, ...]] = {
"chat_models",
"ChatGroq",
),
("langchain_xai", "chat_models", "ChatXAI"): (
"langchain_xai",
"chat_models",
"ChatXAI",
),
("langchain", "chat_models", "fireworks", "ChatFireworks"): (
"langchain_fireworks",
"chat_models",
@@ -533,6 +530,16 @@ SERIALIZABLE_MAPPING: dict[tuple[str, ...], tuple[str, ...]] = {
"structured",
"StructuredPrompt",
),
("langchain_sambanova", "chat_models", "ChatSambaNovaCloud"): (
"langchain_sambanova",
"chat_models",
"ChatSambaNovaCloud",
),
("langchain_sambanova", "chat_models", "ChatSambaStudio"): (
"langchain_sambanova",
"chat_models",
"ChatSambaStudio",
),
("langchain_core", "prompts", "message", "_DictMessagePromptTemplate"): (
"langchain_core",
"prompts",

View File

@@ -18,6 +18,7 @@
from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
from langchain_core.utils.utils import LC_AUTO_PREFIX, LC_ID_PREFIX, ensure_id
if TYPE_CHECKING:
from langchain_core.messages.ai import (
@@ -31,10 +32,29 @@ if TYPE_CHECKING:
message_to_dict,
messages_to_dict,
)
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
from langchain_core.messages.content_blocks import (
from langchain_core.messages.block_translators.openai import (
convert_to_openai_data_block,
convert_to_openai_image_block,
)
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
from langchain_core.messages.content import (
Annotation,
AudioContentBlock,
Citation,
ContentBlock,
DataContentBlock,
FileContentBlock,
ImageContentBlock,
InvalidToolCall,
NonStandardAnnotation,
NonStandardContentBlock,
PlainTextContentBlock,
ReasoningContentBlock,
ServerToolCall,
ServerToolCallChunk,
ServerToolResult,
TextContentBlock,
VideoContentBlock,
is_data_content_block,
)
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
@@ -42,7 +62,6 @@ if TYPE_CHECKING:
from langchain_core.messages.modifier import RemoveMessage
from langchain_core.messages.system import SystemMessage, SystemMessageChunk
from langchain_core.messages.tool import (
InvalidToolCall,
ToolCall,
ToolCallChunk,
ToolMessage,
@@ -63,31 +82,50 @@ if TYPE_CHECKING:
)
__all__ = (
"LC_AUTO_PREFIX",
"LC_ID_PREFIX",
"AIMessage",
"AIMessageChunk",
"Annotation",
"AnyMessage",
"AudioContentBlock",
"BaseMessage",
"BaseMessageChunk",
"ChatMessage",
"ChatMessageChunk",
"Citation",
"ContentBlock",
"DataContentBlock",
"FileContentBlock",
"FunctionMessage",
"FunctionMessageChunk",
"HumanMessage",
"HumanMessageChunk",
"ImageContentBlock",
"InvalidToolCall",
"MessageLikeRepresentation",
"NonStandardAnnotation",
"NonStandardContentBlock",
"PlainTextContentBlock",
"ReasoningContentBlock",
"RemoveMessage",
"ServerToolCall",
"ServerToolCallChunk",
"ServerToolResult",
"SystemMessage",
"SystemMessageChunk",
"TextContentBlock",
"ToolCall",
"ToolCallChunk",
"ToolMessage",
"ToolMessageChunk",
"VideoContentBlock",
"_message_from_dict",
"convert_to_messages",
"convert_to_openai_data_block",
"convert_to_openai_image_block",
"convert_to_openai_messages",
"ensure_id",
"filter_messages",
"get_buffer_string",
"is_data_content_block",
@@ -103,35 +141,51 @@ __all__ = (
_dynamic_imports = {
"AIMessage": "ai",
"AIMessageChunk": "ai",
"Annotation": "content",
"AudioContentBlock": "content",
"BaseMessage": "base",
"BaseMessageChunk": "base",
"merge_content": "base",
"message_to_dict": "base",
"messages_to_dict": "base",
"Citation": "content",
"ContentBlock": "content",
"ChatMessage": "chat",
"ChatMessageChunk": "chat",
"DataContentBlock": "content",
"FileContentBlock": "content",
"FunctionMessage": "function",
"FunctionMessageChunk": "function",
"HumanMessage": "human",
"HumanMessageChunk": "human",
"NonStandardAnnotation": "content",
"NonStandardContentBlock": "content",
"PlainTextContentBlock": "content",
"ReasoningContentBlock": "content",
"RemoveMessage": "modifier",
"ServerToolCall": "content",
"ServerToolCallChunk": "content",
"ServerToolResult": "content",
"SystemMessage": "system",
"SystemMessageChunk": "system",
"ImageContentBlock": "content",
"InvalidToolCall": "tool",
"TextContentBlock": "content",
"ToolCall": "tool",
"ToolCallChunk": "tool",
"ToolMessage": "tool",
"ToolMessageChunk": "tool",
"VideoContentBlock": "content",
"AnyMessage": "utils",
"MessageLikeRepresentation": "utils",
"_message_from_dict": "utils",
"convert_to_messages": "utils",
"convert_to_openai_data_block": "content_blocks",
"convert_to_openai_image_block": "content_blocks",
"convert_to_openai_data_block": "block_translators.openai",
"convert_to_openai_image_block": "block_translators.openai",
"convert_to_openai_messages": "utils",
"filter_messages": "utils",
"get_buffer_string": "utils",
"is_data_content_block": "content_blocks",
"is_data_content_block": "content",
"merge_message_runs": "utils",
"message_chunk_to_message": "utils",
"messages_from_dict": "utils",

View File

@@ -3,42 +3,37 @@
import json
import logging
import operator
from typing import Any, Literal, Optional, Union, cast
from collections.abc import Sequence
from typing import Any, Literal, Optional, Union, cast, overload
from pydantic import model_validator
from typing_extensions import NotRequired, Self, TypedDict, override
from langchain_core.messages import content as types
from langchain_core.messages.base import (
BaseMessage,
BaseMessageChunk,
_extract_reasoning_from_additional_kwargs,
merge_content,
)
from langchain_core.messages.content import InvalidToolCall
from langchain_core.messages.tool import (
InvalidToolCall,
ToolCall,
ToolCallChunk,
default_tool_chunk_parser,
default_tool_parser,
)
from langchain_core.messages.tool import (
invalid_tool_call as create_invalid_tool_call,
)
from langchain_core.messages.tool import (
tool_call as create_tool_call,
)
from langchain_core.messages.tool import (
tool_call_chunk as create_tool_call_chunk,
)
from langchain_core.messages.tool import invalid_tool_call as create_invalid_tool_call
from langchain_core.messages.tool import tool_call as create_tool_call
from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk
from langchain_core.utils._merge import merge_dicts, merge_lists
from langchain_core.utils.json import parse_partial_json
from langchain_core.utils.usage import _dict_int_op
from langchain_core.utils.utils import LC_AUTO_PREFIX, LC_ID_PREFIX
logger = logging.getLogger(__name__)
_LC_ID_PREFIX = "run-"
class InputTokenDetails(TypedDict, total=False):
"""Breakdown of input token counts.
@@ -162,13 +157,6 @@ class AIMessage(BaseMessage):
"""
example: bool = False
"""Use to denote that a message is part of an example conversation.
At the moment, this is ignored by most models. Usage is discouraged.
"""
tool_calls: list[ToolCall] = []
"""If provided, tool calls associated with the message."""
invalid_tool_calls: list[InvalidToolCall] = []
@@ -181,20 +169,52 @@ class AIMessage(BaseMessage):
"""
type: Literal["ai"] = "ai"
"""The type of the message (used for deserialization). Defaults to ``'ai'``."""
"""The type of the message (used for deserialization). Defaults to "ai"."""
@overload
def __init__(
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None: ...
@overload
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None: ...
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None:
"""Initialize ``AIMessage``.
Specify ``content`` as positional arg or ``content_blocks`` for typing.
Args:
content: The content of the message.
content_blocks: Typed standard content.
kwargs: Additional arguments to pass to the parent class.
"""
super().__init__(content=content, **kwargs)
if content_blocks is not None:
# If there are tool calls in content_blocks, but not in tool_calls, add them
content_tool_calls = [
block for block in content_blocks if block.get("type") == "tool_call"
]
if content_tool_calls and "tool_calls" not in kwargs:
kwargs["tool_calls"] = content_tool_calls
super().__init__(
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
**kwargs,
)
else:
super().__init__(content=content, **kwargs)
@property
def lc_attributes(self) -> dict:
@@ -204,6 +224,65 @@ class AIMessage(BaseMessage):
"invalid_tool_calls": self.invalid_tool_calls,
}
@property
def content_blocks(self) -> list[types.ContentBlock]:
"""Return content blocks of the message.
If the message has a known model provider, use the provider-specific translator
first before falling back to best-effort parsing. For details, see the property
on ``BaseMessage``.
"""
if self.response_metadata.get("output_version") == "v1":
return cast("list[types.ContentBlock]", self.content)
model_provider = self.response_metadata.get("model_provider")
if model_provider:
from langchain_core.messages.block_translators import ( # noqa: PLC0415
get_translator,
)
translator = get_translator(model_provider)
if translator:
try:
return translator["translate_content"](self)
except NotImplementedError:
pass
# Otherwise, use best-effort parsing
blocks = super().content_blocks
if self.tool_calls:
# Add from tool_calls if missing from content
content_tool_call_ids = {
block.get("id")
for block in self.content
if isinstance(block, dict) and block.get("type") == "tool_call"
}
for tool_call in self.tool_calls:
if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:
tool_call_block: types.ToolCall = {
"type": "tool_call",
"id": id_,
"name": tool_call["name"],
"args": tool_call["args"],
}
if "index" in tool_call:
tool_call_block["index"] = tool_call["index"] # type: ignore[typeddict-item]
if "extras" in tool_call:
tool_call_block["extras"] = tool_call["extras"] # type: ignore[typeddict-item]
blocks.append(tool_call_block)
# Best-effort reasoning extraction from additional_kwargs
# Only add reasoning if not already present
# Insert before all other blocks to keep reasoning at the start
has_reasoning = any(block.get("type") == "reasoning" for block in blocks)
if not has_reasoning and (
reasoning_block := _extract_reasoning_from_additional_kwargs(self)
):
blocks.insert(0, reasoning_block)
return blocks
# TODO: remove this logic if possible, reducing breaking nature of changes
@model_validator(mode="before")
@classmethod
@@ -232,7 +311,9 @@ class AIMessage(BaseMessage):
# Ensure "type" is properly set on all tool call-like dicts.
if tool_calls := values.get("tool_calls"):
values["tool_calls"] = [
create_tool_call(**{k: v for k, v in tc.items() if k != "type"})
create_tool_call(
**{k: v for k, v in tc.items() if k not in ("type", "extras")}
)
for tc in tool_calls
]
if invalid_tool_calls := values.get("invalid_tool_calls"):
@@ -307,6 +388,13 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
tool_call_chunks: list[ToolCallChunk] = []
"""If provided, tool call chunks associated with the message."""
chunk_position: Optional[Literal["last"]] = None
"""Optional span represented by an aggregated AIMessageChunk.
If a chunk with ``chunk_position="last"`` is aggregated into a stream,
``tool_call_chunks`` in message content will be parsed into ``tool_calls``.
"""
@property
def lc_attributes(self) -> dict:
"""Attrs to be serialized even if they are derived from other init args."""
@@ -315,6 +403,60 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
"invalid_tool_calls": self.invalid_tool_calls,
}
@property
def content_blocks(self) -> list[types.ContentBlock]:
"""Return content blocks of the message."""
if self.response_metadata.get("output_version") == "v1":
return cast("list[types.ContentBlock]", self.content)
model_provider = self.response_metadata.get("model_provider")
if model_provider:
from langchain_core.messages.block_translators import ( # noqa: PLC0415
get_translator,
)
translator = get_translator(model_provider)
if translator:
try:
return translator["translate_content_chunk"](self)
except NotImplementedError:
pass
# Otherwise, use best-effort parsing
blocks = super().content_blocks
if (
self.tool_call_chunks
and not self.content
and self.chunk_position != "last" # keep tool_calls if aggregated
):
blocks = [
block
for block in blocks
if block["type"] not in ("tool_call", "invalid_tool_call")
]
for tool_call_chunk in self.tool_call_chunks:
tc: types.ToolCallChunk = {
"type": "tool_call_chunk",
"id": tool_call_chunk.get("id"),
"name": tool_call_chunk.get("name"),
"args": tool_call_chunk.get("args"),
}
if (idx := tool_call_chunk.get("index")) is not None:
tc["index"] = idx
blocks.append(tc)
# Best-effort reasoning extraction from additional_kwargs
# Only add reasoning if not already present
# Insert before all other blocks to keep reasoning at the start
has_reasoning = any(block.get("type") == "reasoning" for block in blocks)
if not has_reasoning and (
reasoning_block := _extract_reasoning_from_additional_kwargs(self)
):
blocks.insert(0, reasoning_block)
return blocks
@model_validator(mode="after")
def init_tool_calls(self) -> Self:
"""Initialize tool calls from tool call chunks.
@@ -379,10 +521,70 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
add_chunk_to_invalid_tool_calls(chunk)
self.tool_calls = tool_calls
self.invalid_tool_calls = invalid_tool_calls
if (
self.chunk_position == "last"
and self.tool_call_chunks
and self.response_metadata.get("output_version") == "v1"
and isinstance(self.content, list)
):
id_to_tc: dict[str, types.ToolCall] = {
cast("str", tc.get("id")): {
"type": "tool_call",
"name": tc["name"],
"args": tc["args"],
"id": tc.get("id"),
}
for tc in self.tool_calls
if "id" in tc
}
for idx, block in enumerate(self.content):
if (
isinstance(block, dict)
and block.get("type") == "tool_call_chunk"
and (call_id := block.get("id"))
and call_id in id_to_tc
):
self.content[idx] = cast("dict[str, Any]", id_to_tc[call_id])
return self
@model_validator(mode="after")
def init_server_tool_calls(self) -> Self:
"""Parse server_tool_call_chunks."""
if (
self.chunk_position == "last"
and self.response_metadata.get("output_version") == "v1"
and isinstance(self.content, list)
):
for idx, block in enumerate(self.content):
if (
isinstance(block, dict)
and block.get("type")
in ("server_tool_call", "server_tool_call_chunk")
and (args_str := block.get("args"))
and isinstance(args_str, str)
):
try:
args = json.loads(args_str)
if isinstance(args, dict):
self.content[idx]["type"] = "server_tool_call" # type: ignore[index]
self.content[idx]["args"] = args # type: ignore[index]
except json.JSONDecodeError:
pass
return self
@overload # type: ignore[override] # summing BaseMessages gives ChatPromptTemplate
def __add__(self, other: "AIMessageChunk") -> "AIMessageChunk": ...
@overload
def __add__(self, other: Sequence["AIMessageChunk"]) -> "AIMessageChunk": ...
@overload
def __add__(self, other: Any) -> BaseMessageChunk: ...
@override
def __add__(self, other: Any) -> BaseMessageChunk: # type: ignore[override]
def __add__(self, other: Any) -> BaseMessageChunk:
if isinstance(other, AIMessageChunk):
return add_ai_message_chunks(self, other)
if isinstance(other, (list, tuple)) and all(
@@ -401,17 +603,10 @@ def add_ai_message_chunks(
left: The first ``AIMessageChunk``.
*others: Other ``AIMessageChunk``s to add.
Raises:
ValueError: If the example values of the chunks are not the same.
Returns:
The resulting ``AIMessageChunk``.
"""
if any(left.example != o.example for o in others):
msg = "Cannot concatenate AIMessageChunks with different example values."
raise ValueError(msg)
content = merge_content(left.content, *(o.content for o in others))
additional_kwargs = merge_dicts(
left.additional_kwargs, *(o.additional_kwargs for o in others)
@@ -446,26 +641,40 @@ def add_ai_message_chunks(
chunk_id = None
candidates = [left.id] + [o.id for o in others]
# first pass: pick the first non-run-* id
# first pass: pick the first provider-assigned id (non-run-* and non-lc_*)
for id_ in candidates:
if id_ and not id_.startswith(_LC_ID_PREFIX):
if (
id_
and not id_.startswith(LC_ID_PREFIX)
and not id_.startswith(LC_AUTO_PREFIX)
):
chunk_id = id_
break
else:
# second pass: no provider-assigned id found, just take the first non-null
# second pass: prefer lc_run-* ids over lc_* ids
for id_ in candidates:
if id_:
if id_ and id_.startswith(LC_ID_PREFIX):
chunk_id = id_
break
else:
# third pass: take any remaining id (auto-generated lc_* ids)
for id_ in candidates:
if id_:
chunk_id = id_
break
chunk_position: Optional[Literal["last"]] = (
"last" if any(x.chunk_position == "last" for x in [left, *others]) else None
)
return left.__class__(
example=left.example,
content=content,
additional_kwargs=additional_kwargs,
tool_call_chunks=tool_call_chunks,
response_metadata=response_metadata,
usage_metadata=usage_metadata,
id=chunk_id,
chunk_position=chunk_position,
)

View File

@@ -2,11 +2,14 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional, Union, cast
from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
from pydantic import ConfigDict, Field
from typing_extensions import Self
from langchain_core._api.deprecation import warn_deprecated
from langchain_core.load.serializable import Serializable
from langchain_core.messages import content as types
from langchain_core.utils import get_bolded_text
from langchain_core.utils._merge import merge_dicts, merge_lists
from langchain_core.utils.interactive_env import is_interactive_env
@@ -17,10 +20,79 @@ if TYPE_CHECKING:
from langchain_core.prompts.chat import ChatPromptTemplate
def _extract_reasoning_from_additional_kwargs(
message: BaseMessage,
) -> Optional[types.ReasoningContentBlock]:
"""Extract `reasoning_content` from `additional_kwargs`.
Handles reasoning content stored in various formats:
- `additional_kwargs["reasoning_content"]` (string) - Ollama, DeepSeek, XAI, Groq
Args:
message: The message to extract reasoning from.
Returns:
A `ReasoningContentBlock` if reasoning content is found, None otherwise.
"""
additional_kwargs = getattr(message, "additional_kwargs", {})
reasoning_content = additional_kwargs.get("reasoning_content")
if reasoning_content is not None and isinstance(reasoning_content, str):
return {"type": "reasoning", "reasoning": reasoning_content}
return None
class TextAccessor(str):
"""String-like object that supports both property and method access patterns.
Exists to maintain backward compatibility while transitioning from method-based to
property-based text access in message objects. In LangChain <v1.0, message text was
accessed via ``.text()`` method calls. In v1.0=<, the preferred pattern is property
access via ``.text``.
Rather than breaking existing code immediately, ``TextAccessor`` allows both
patterns:
- Modern property access: ``message.text`` (returns string directly)
- Legacy method access: ``message.text()`` (callable, emits deprecation warning)
"""
__slots__ = ()
def __new__(cls, value: str) -> Self:
"""Create new TextAccessor instance."""
return str.__new__(cls, value)
def __call__(self) -> str:
"""Enable method-style text access for backward compatibility.
This method exists solely to support legacy code that calls ``.text()``
as a method. New code should use property access (``.text``) instead.
.. deprecated:: 1.0.0
Calling ``.text()`` as a method is deprecated. Use ``.text`` as a property
instead. This method will be removed in 2.0.0.
Returns:
The string content, identical to property access.
"""
warn_deprecated(
since="1.0.0",
message=(
"Calling .text() as a method is deprecated. "
"Use .text as a property instead (e.g., message.text)."
),
removal="2.0.0",
)
return str(self)
class BaseMessage(Serializable):
"""Base abstract message class.
Messages are the inputs and outputs of ``ChatModel``s.
Messages are the inputs and outputs of a ``ChatModel``.
"""
content: Union[str, list[Union[str, dict]]]
@@ -66,17 +138,40 @@ class BaseMessage(Serializable):
extra="allow",
)
@overload
def __init__(
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None: ...
@overload
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None: ...
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None:
"""Initialize ``BaseMessage``.
Specify ``content`` as positional arg or ``content_blocks`` for typing.
Args:
content: The string contents of the message.
content_blocks: Typed standard content.
kwargs: Additional arguments to pass to the parent class.
"""
super().__init__(content=content, **kwargs)
if content_blocks is not None:
super().__init__(content=content_blocks, **kwargs)
else:
super().__init__(content=content, **kwargs)
@classmethod
def is_lc_serializable(cls) -> bool:
@@ -96,26 +191,102 @@ class BaseMessage(Serializable):
"""
return ["langchain", "schema", "messages"]
def text(self) -> str:
"""Get the text ``content`` of the message.
@property
def content_blocks(self) -> list[types.ContentBlock]:
r"""Load content blocks from the message content.
.. versionadded:: 1.0.0
"""
# Needed here to avoid circular import, as these classes import BaseMessages
from langchain_core.messages import content as types # noqa: PLC0415
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
_convert_to_v1_from_anthropic_input,
)
from langchain_core.messages.block_translators.bedrock_converse import ( # noqa: PLC0415
_convert_to_v1_from_converse_input,
)
from langchain_core.messages.block_translators.google_genai import ( # noqa: PLC0415
_convert_to_v1_from_genai_input,
)
from langchain_core.messages.block_translators.google_vertexai import ( # noqa: PLC0415
_convert_to_v1_from_vertexai_input,
)
from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
_convert_v0_multimodal_input_to_v1,
)
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
_convert_to_v1_from_chat_completions_input,
)
blocks: list[types.ContentBlock] = []
content = (
# Transpose string content to list, otherwise assumed to be list
[self.content]
if isinstance(self.content, str) and self.content
else self.content
)
for item in content:
if isinstance(item, str):
# Plain string content is treated as a text block
blocks.append({"type": "text", "text": item})
elif isinstance(item, dict):
item_type = item.get("type")
if item_type not in types.KNOWN_BLOCK_TYPES:
# Handle all provider-specific or None type blocks as non-standard -
# we'll come back to these later
blocks.append({"type": "non_standard", "value": item})
else:
# Guard against v0 blocks that share the same `type` keys
if "source_type" in item:
blocks.append({"type": "non_standard", "value": item})
continue
# This can't be a v0 block (since they require `source_type`),
# so it's a known v1 block type
blocks.append(cast("types.ContentBlock", item))
# Subsequent passes: attempt to unpack non-standard blocks.
# This is the last stop - if we can't parse it here, it is left as non-standard
for parsing_step in [
_convert_v0_multimodal_input_to_v1,
_convert_to_v1_from_chat_completions_input,
_convert_to_v1_from_anthropic_input,
_convert_to_v1_from_genai_input,
_convert_to_v1_from_vertexai_input,
_convert_to_v1_from_converse_input,
]:
blocks = parsing_step(blocks)
return blocks
@property
def text(self) -> TextAccessor:
"""Get the text content of the message as a string.
Can be used as both property (``message.text``) and method (``message.text()``).
.. deprecated:: 1.0.0
Calling ``.text()`` as a method is deprecated. Use ``.text`` as a property
instead. This method will be removed in 2.0.0.
Returns:
The text content of the message.
"""
if isinstance(self.content, str):
return self.content
# must be a list
blocks = [
block
for block in self.content
if isinstance(block, str)
or (block.get("type") == "text" and isinstance(block.get("text"), str))
]
return "".join(
block if isinstance(block, str) else block["text"] for block in blocks
)
text_value = self.content
else:
# must be a list
blocks = [
block
for block in self.content
if isinstance(block, str)
or (block.get("type") == "text" and isinstance(block.get("text"), str))
]
text_value = "".join(
block if isinstance(block, str) else block["text"] for block in blocks
)
return TextAccessor(text_value)
def __add__(self, other: Any) -> ChatPromptTemplate:
"""Concatenate this message with another message.
@@ -171,7 +342,9 @@ def merge_content(
The merged content.
"""
merged = first_content
merged: Union[str, list[Union[str, dict]]]
merged = "" if first_content is None else first_content
for content in contents:
# If current is a string
if isinstance(merged, str):
@@ -190,8 +363,10 @@ def merge_content(
elif merged and isinstance(merged[-1], str):
merged[-1] += content
# If second content is an empty string, treat as a no-op
elif content:
# Otherwise, add the second content as a new element of the list
elif content == "":
pass
# Otherwise, add the second content as a new element of the list
elif merged:
merged.append(content)
return merged

View File

@@ -0,0 +1,110 @@
"""Derivations of standard content blocks from provider content.
``AIMessage`` will first attempt to use a provider-specific translator if
``model_provider`` is set in ``response_metadata`` on the message. Consequently, each
provider translator must handle all possible content response types from the provider,
including text.
If no provider is set, or if the provider does not have a registered translator,
``AIMessage`` will fall back to best-effort parsing of the content into blocks using
the implementation in ``BaseMessage``.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable
if TYPE_CHECKING:
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types
# Provider to translator mapping
PROVIDER_TRANSLATORS: dict[str, dict[str, Callable[..., list[types.ContentBlock]]]] = {}
"""Map model provider names to translator functions.
The dictionary maps provider names (e.g. ``'openai'``, ``'anthropic'``) to another
dictionary with two keys:
- ``'translate_content'``: Function to translate ``AIMessage`` content.
- ``'translate_content_chunk'``: Function to translate ``AIMessageChunk`` content.
When calling `.content_blocks` on an ``AIMessage`` or ``AIMessageChunk``, if
``model_provider`` is set in ``response_metadata``, the corresponding translator
functions will be used to parse the content into blocks. Otherwise, best-effort parsing
in ``BaseMessage`` will be used.
"""
def register_translator(
provider: str,
translate_content: Callable[[AIMessage], list[types.ContentBlock]],
translate_content_chunk: Callable[[AIMessageChunk], list[types.ContentBlock]],
) -> None:
"""Register content translators for a provider in `PROVIDER_TRANSLATORS`.
Args:
provider: The model provider name (e.g. ``'openai'``, ``'anthropic'``).
translate_content: Function to translate ``AIMessage`` content.
translate_content_chunk: Function to translate ``AIMessageChunk`` content.
"""
PROVIDER_TRANSLATORS[provider] = {
"translate_content": translate_content,
"translate_content_chunk": translate_content_chunk,
}
def get_translator(
provider: str,
) -> dict[str, Callable[..., list[types.ContentBlock]]] | None:
"""Get the translator functions for a provider.
Args:
provider: The model provider name.
Returns:
Dictionary with ``'translate_content'`` and ``'translate_content_chunk'``
functions, or None if no translator is registered for the provider. In such
case, best-effort parsing in ``BaseMessage`` will be used.
"""
return PROVIDER_TRANSLATORS.get(provider)
def _register_translators() -> None:
"""Register all translators in langchain-core.
A unit test ensures all modules in ``block_translators`` are represented here.
For translators implemented outside langchain-core, they can be registered by
calling ``register_translator`` from within the integration package.
"""
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
_register_anthropic_translator,
)
from langchain_core.messages.block_translators.bedrock import ( # noqa: PLC0415
_register_bedrock_translator,
)
from langchain_core.messages.block_translators.bedrock_converse import ( # noqa: PLC0415
_register_bedrock_converse_translator,
)
from langchain_core.messages.block_translators.google_genai import ( # noqa: PLC0415
_register_google_genai_translator,
)
from langchain_core.messages.block_translators.google_vertexai import ( # noqa: PLC0415
_register_google_vertexai_translator,
)
from langchain_core.messages.block_translators.groq import ( # noqa: PLC0415
_register_groq_translator,
)
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
_register_openai_translator,
)
_register_bedrock_translator()
_register_bedrock_converse_translator()
_register_anthropic_translator()
_register_google_genai_translator()
_register_google_vertexai_translator()
_register_groq_translator()
_register_openai_translator()
_register_translators()

View File

@@ -0,0 +1,470 @@
"""Derivations of standard content blocks from Anthropic content."""
import json
from collections.abc import Iterable
from typing import Any, Optional, Union, cast
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types
def _populate_extras(
standard_block: types.ContentBlock, block: dict[str, Any], known_fields: set[str]
) -> types.ContentBlock:
"""Mutate a block, populating extras."""
if standard_block.get("type") == "non_standard":
return standard_block
for key, value in block.items():
if key not in known_fields:
if "extras" not in standard_block:
# Below type-ignores are because mypy thinks a non-standard block can
# get here, although we exclude them above.
standard_block["extras"] = {} # type: ignore[typeddict-unknown-key]
standard_block["extras"][key] = value # type: ignore[typeddict-item]
return standard_block
def _convert_to_v1_from_anthropic_input(
content: list[types.ContentBlock],
) -> list[types.ContentBlock]:
"""Convert Anthropic format blocks to v1 format.
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
block as a ``'non_standard'`` block with the original block stored in the ``value``
field. This function attempts to unpack those blocks and convert any blocks that
might be Anthropic format to v1 ContentBlocks.
If conversion fails, the block is left as a ``'non_standard'`` block.
Args:
content: List of content blocks to process.
Returns:
Updated list with Anthropic blocks converted to v1 format.
"""
def _iter_blocks() -> Iterable[types.ContentBlock]:
blocks: list[dict[str, Any]] = [
cast("dict[str, Any]", block)
if block.get("type") != "non_standard"
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
for block in content
]
for block in blocks:
block_type = block.get("type")
if (
block_type == "document"
and "source" in block
and "type" in block["source"]
):
if block["source"]["type"] == "base64":
file_block: types.FileContentBlock = {
"type": "file",
"base64": block["source"]["data"],
"mime_type": block["source"]["media_type"],
}
_populate_extras(file_block, block, {"type", "source"})
yield file_block
elif block["source"]["type"] == "url":
file_block = {
"type": "file",
"url": block["source"]["url"],
}
_populate_extras(file_block, block, {"type", "source"})
yield file_block
elif block["source"]["type"] == "file":
file_block = {
"type": "file",
"id": block["source"]["file_id"],
}
_populate_extras(file_block, block, {"type", "source"})
yield file_block
elif block["source"]["type"] == "text":
plain_text_block: types.PlainTextContentBlock = {
"type": "text-plain",
"text": block["source"]["data"],
"mime_type": block.get("media_type", "text/plain"),
}
_populate_extras(plain_text_block, block, {"type", "source"})
yield plain_text_block
else:
yield {"type": "non_standard", "value": block}
elif (
block_type == "image"
and "source" in block
and "type" in block["source"]
):
if block["source"]["type"] == "base64":
image_block: types.ImageContentBlock = {
"type": "image",
"base64": block["source"]["data"],
"mime_type": block["source"]["media_type"],
}
_populate_extras(image_block, block, {"type", "source"})
yield image_block
elif block["source"]["type"] == "url":
image_block = {
"type": "image",
"url": block["source"]["url"],
}
_populate_extras(image_block, block, {"type", "source"})
yield image_block
elif block["source"]["type"] == "file":
image_block = {
"type": "image",
"id": block["source"]["file_id"],
}
_populate_extras(image_block, block, {"type", "source"})
yield image_block
else:
yield {"type": "non_standard", "value": block}
elif block_type in types.KNOWN_BLOCK_TYPES:
yield cast("types.ContentBlock", block)
else:
yield {"type": "non_standard", "value": block}
return list(_iter_blocks())
def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
citation_type = citation.get("type")
if citation_type == "web_search_result_location":
url_citation: types.Citation = {
"type": "citation",
"cited_text": citation["cited_text"],
"url": citation["url"],
}
if title := citation.get("title"):
url_citation["title"] = title
known_fields = {"type", "cited_text", "url", "title", "index", "extras"}
for key, value in citation.items():
if key not in known_fields:
if "extras" not in url_citation:
url_citation["extras"] = {}
url_citation["extras"][key] = value
return url_citation
if citation_type in (
"char_location",
"content_block_location",
"page_location",
"search_result_location",
):
document_citation: types.Citation = {
"type": "citation",
"cited_text": citation["cited_text"],
}
if "document_title" in citation:
document_citation["title"] = citation["document_title"]
elif title := citation.get("title"):
document_citation["title"] = title
else:
pass
known_fields = {
"type",
"cited_text",
"document_title",
"title",
"index",
"extras",
}
for key, value in citation.items():
if key not in known_fields:
if "extras" not in document_citation:
document_citation["extras"] = {}
document_citation["extras"][key] = value
return document_citation
return {
"type": "non_standard_annotation",
"value": citation,
}
def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock]:
"""Convert Anthropic message content to v1 format."""
if isinstance(message.content, str):
content: list[Union[str, dict]] = [{"type": "text", "text": message.content}]
else:
content = message.content
def _iter_blocks() -> Iterable[types.ContentBlock]:
for block in content:
if not isinstance(block, dict):
continue
block_type = block.get("type")
if block_type == "text":
if citations := block.get("citations"):
text_block: types.TextContentBlock = {
"type": "text",
"text": block.get("text", ""),
"annotations": [_convert_citation_to_v1(a) for a in citations],
}
else:
text_block = {"type": "text", "text": block["text"]}
if "index" in block:
text_block["index"] = block["index"]
yield text_block
elif block_type == "thinking":
reasoning_block: types.ReasoningContentBlock = {
"type": "reasoning",
"reasoning": block.get("thinking", ""),
}
if "index" in block:
reasoning_block["index"] = block["index"]
known_fields = {"type", "thinking", "index", "extras"}
for key in block:
if key not in known_fields:
if "extras" not in reasoning_block:
reasoning_block["extras"] = {}
reasoning_block["extras"][key] = block[key]
yield reasoning_block
elif block_type == "tool_use":
if (
isinstance(message, AIMessageChunk)
and len(message.tool_call_chunks) == 1
and message.chunk_position != "last"
):
# Isolated chunk
tool_call_chunk: types.ToolCallChunk = (
message.tool_call_chunks[0].copy() # type: ignore[assignment]
)
if "type" not in tool_call_chunk:
tool_call_chunk["type"] = "tool_call_chunk"
yield tool_call_chunk
else:
tool_call_block: Optional[types.ToolCall] = None
# Non-streaming or gathered chunk
if len(message.tool_calls) == 1:
tool_call_block = {
"type": "tool_call",
"name": message.tool_calls[0]["name"],
"args": message.tool_calls[0]["args"],
"id": message.tool_calls[0].get("id"),
}
elif call_id := block.get("id"):
for tc in message.tool_calls:
if tc.get("id") == call_id:
tool_call_block = {
"type": "tool_call",
"name": tc["name"],
"args": tc["args"],
"id": tc.get("id"),
}
break
else:
pass
if not tool_call_block:
tool_call_block = {
"type": "tool_call",
"name": block.get("name", ""),
"args": block.get("input", {}),
"id": block.get("id", ""),
}
if "index" in block:
tool_call_block["index"] = block["index"]
yield tool_call_block
elif block_type == "input_json_delta" and isinstance(
message, AIMessageChunk
):
if len(message.tool_call_chunks) == 1:
tool_call_chunk = (
message.tool_call_chunks[0].copy() # type: ignore[assignment]
)
if "type" not in tool_call_chunk:
tool_call_chunk["type"] = "tool_call_chunk"
yield tool_call_chunk
else:
server_tool_call_chunk: types.ServerToolCallChunk = {
"type": "server_tool_call_chunk",
"args": block.get("partial_json", ""),
}
if "index" in block:
server_tool_call_chunk["index"] = block["index"]
yield server_tool_call_chunk
elif block_type == "server_tool_use":
if block.get("name") == "code_execution":
server_tool_use_name = "code_interpreter"
else:
server_tool_use_name = block.get("name", "")
if (
isinstance(message, AIMessageChunk)
and block.get("input") == {}
and "partial_json" not in block
and message.chunk_position != "last"
):
# First chunk in a stream
server_tool_call_chunk = {
"type": "server_tool_call_chunk",
"name": server_tool_use_name,
"args": "",
"id": block.get("id", ""),
}
if "index" in block:
server_tool_call_chunk["index"] = block["index"]
known_fields = {"type", "name", "input", "id", "index"}
_populate_extras(server_tool_call_chunk, block, known_fields)
yield server_tool_call_chunk
else:
server_tool_call: types.ServerToolCall = {
"type": "server_tool_call",
"name": server_tool_use_name,
"args": block.get("input", {}),
"id": block.get("id", ""),
}
if block.get("input") == {} and "partial_json" in block:
try:
input_ = json.loads(block["partial_json"])
if isinstance(input_, dict):
server_tool_call["args"] = input_
except json.JSONDecodeError:
pass
if "index" in block:
server_tool_call["index"] = block["index"]
known_fields = {
"type",
"name",
"input",
"partial_json",
"id",
"index",
}
_populate_extras(server_tool_call, block, known_fields)
yield server_tool_call
elif block_type == "mcp_tool_use":
if (
isinstance(message, AIMessageChunk)
and block.get("input") == {}
and "partial_json" not in block
and message.chunk_position != "last"
):
# First chunk in a stream
server_tool_call_chunk = {
"type": "server_tool_call_chunk",
"name": "remote_mcp",
"args": "",
"id": block.get("id", ""),
}
if "name" in block:
server_tool_call_chunk["extras"] = {"tool_name": block["name"]}
known_fields = {"type", "name", "input", "id", "index"}
_populate_extras(server_tool_call_chunk, block, known_fields)
if "index" in block:
server_tool_call_chunk["index"] = block["index"]
yield server_tool_call_chunk
else:
server_tool_call = {
"type": "server_tool_call",
"name": "remote_mcp",
"args": block.get("input", {}),
"id": block.get("id", ""),
}
if block.get("input") == {} and "partial_json" in block:
try:
input_ = json.loads(block["partial_json"])
if isinstance(input_, dict):
server_tool_call["args"] = input_
except json.JSONDecodeError:
pass
if "name" in block:
server_tool_call["extras"] = {"tool_name": block["name"]}
known_fields = {
"type",
"name",
"input",
"partial_json",
"id",
"index",
}
_populate_extras(server_tool_call, block, known_fields)
if "index" in block:
server_tool_call["index"] = block["index"]
yield server_tool_call
elif block_type and block_type.endswith("_tool_result"):
server_tool_result: types.ServerToolResult = {
"type": "server_tool_result",
"tool_call_id": block.get("tool_use_id", ""),
"status": "success",
"extras": {"block_type": block_type},
}
if output := block.get("content", []):
server_tool_result["output"] = output
if isinstance(output, dict) and output.get(
"error_code" # web_search, code_interpreter
):
server_tool_result["status"] = "error"
if block.get("is_error"): # mcp_tool_result
server_tool_result["status"] = "error"
if "index" in block:
server_tool_result["index"] = block["index"]
known_fields = {"type", "tool_use_id", "content", "is_error", "index"}
_populate_extras(server_tool_result, block, known_fields)
yield server_tool_result
else:
new_block: types.NonStandardContentBlock = {
"type": "non_standard",
"value": block,
}
if "index" in new_block["value"]:
new_block["index"] = new_block["value"].pop("index")
yield new_block
return list(_iter_blocks())
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Anthropic content."""
return _convert_to_v1_from_anthropic(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message chunk with Anthropic content."""
return _convert_to_v1_from_anthropic(message)
def _register_anthropic_translator() -> None:
"""Register the Anthropic translator with the central registry.
Run automatically when the module is imported.
"""
from langchain_core.messages.block_translators import ( # noqa: PLC0415
register_translator,
)
register_translator("anthropic", translate_content, translate_content_chunk)
_register_anthropic_translator()

View File

@@ -0,0 +1,94 @@
"""Derivations of standard content blocks from Bedrock content."""
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types
from langchain_core.messages.block_translators.anthropic import (
_convert_to_v1_from_anthropic,
)
def _convert_to_v1_from_bedrock(message: AIMessage) -> list[types.ContentBlock]:
"""Convert bedrock message content to v1 format."""
out = _convert_to_v1_from_anthropic(message)
content_tool_call_ids = {
block.get("id")
for block in out
if isinstance(block, dict) and block.get("type") == "tool_call"
}
for tool_call in message.tool_calls:
if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:
tool_call_block: types.ToolCall = {
"type": "tool_call",
"id": id_,
"name": tool_call["name"],
"args": tool_call["args"],
}
if "index" in tool_call:
tool_call_block["index"] = tool_call["index"] # type: ignore[typeddict-item]
if "extras" in tool_call:
tool_call_block["extras"] = tool_call["extras"] # type: ignore[typeddict-item]
out.append(tool_call_block)
return out
def _convert_to_v1_from_bedrock_chunk(
message: AIMessageChunk,
) -> list[types.ContentBlock]:
"""Convert bedrock message chunk content to v1 format."""
if (
message.content == ""
and not message.additional_kwargs
and not message.tool_calls
):
# Bedrock outputs multiple chunks containing response metadata
return []
out = _convert_to_v1_from_anthropic(message)
if (
message.tool_call_chunks
and not message.content
and message.chunk_position != "last" # keep tool_calls if aggregated
):
for tool_call_chunk in message.tool_call_chunks:
tc: types.ToolCallChunk = {
"type": "tool_call_chunk",
"id": tool_call_chunk.get("id"),
"name": tool_call_chunk.get("name"),
"args": tool_call_chunk.get("args"),
}
if (idx := tool_call_chunk.get("index")) is not None:
tc["index"] = idx
out.append(tc)
return out
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Bedrock content."""
if "claude" not in message.response_metadata.get("model_name", "").lower():
raise NotImplementedError # fall back to best-effort parsing
return _convert_to_v1_from_bedrock(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message chunk with Bedrock content."""
# TODO: add model_name to all Bedrock chunks and update core merging logic
# to not append during aggregation. Then raise NotImplementedError here if
# not an Anthropic model to fall back to best-effort parsing.
return _convert_to_v1_from_bedrock_chunk(message)
def _register_bedrock_translator() -> None:
"""Register the bedrock translator with the central registry.
Run automatically when the module is imported.
"""
from langchain_core.messages.block_translators import ( # noqa: PLC0415
register_translator,
)
register_translator("bedrock", translate_content, translate_content_chunk)
_register_bedrock_translator()

View File

@@ -0,0 +1,297 @@
"""Derivations of standard content blocks from Amazon (Bedrock Converse) content."""
import base64
from collections.abc import Iterable
from typing import Any, Optional, cast
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types
def _bytes_to_b64_str(bytes_: bytes) -> str:
return base64.b64encode(bytes_).decode("utf-8")
def _populate_extras(
standard_block: types.ContentBlock, block: dict[str, Any], known_fields: set[str]
) -> types.ContentBlock:
"""Mutate a block, populating extras."""
if standard_block.get("type") == "non_standard":
return standard_block
for key, value in block.items():
if key not in known_fields:
if "extras" not in standard_block:
# Below type-ignores are because mypy thinks a non-standard block can
# get here, although we exclude them above.
standard_block["extras"] = {} # type: ignore[typeddict-unknown-key]
standard_block["extras"][key] = value # type: ignore[typeddict-item]
return standard_block
def _convert_to_v1_from_converse_input(
content: list[types.ContentBlock],
) -> list[types.ContentBlock]:
"""Convert Bedrock Converse format blocks to v1 format.
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
block as a ``'non_standard'`` block with the original block stored in the ``value``
field. This function attempts to unpack those blocks and convert any blocks that
might be Converse format to v1 ContentBlocks.
If conversion fails, the block is left as a ``'non_standard'`` block.
Args:
content: List of content blocks to process.
Returns:
Updated list with Converse blocks converted to v1 format.
"""
def _iter_blocks() -> Iterable[types.ContentBlock]:
blocks: list[dict[str, Any]] = [
cast("dict[str, Any]", block)
if block.get("type") != "non_standard"
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
for block in content
]
for block in blocks:
num_keys = len(block)
if num_keys == 1 and (text := block.get("text")):
yield {"type": "text", "text": text}
elif (
num_keys == 1
and (document := block.get("document"))
and isinstance(document, dict)
and "format" in document
):
if document.get("format") == "pdf":
if "bytes" in document.get("source", {}):
file_block: types.FileContentBlock = {
"type": "file",
"base64": _bytes_to_b64_str(document["source"]["bytes"]),
"mime_type": "application/pdf",
}
_populate_extras(file_block, document, {"format", "source"})
yield file_block
else:
yield {"type": "non_standard", "value": block}
elif document["format"] == "txt":
if "text" in document.get("source", {}):
plain_text_block: types.PlainTextContentBlock = {
"type": "text-plain",
"text": document["source"]["text"],
"mime_type": "text/plain",
}
_populate_extras(
plain_text_block, document, {"format", "source"}
)
yield plain_text_block
else:
yield {"type": "non_standard", "value": block}
else:
yield {"type": "non_standard", "value": block}
elif (
num_keys == 1
and (image := block.get("image"))
and isinstance(image, dict)
and "format" in image
):
if "bytes" in image.get("source", {}):
image_block: types.ImageContentBlock = {
"type": "image",
"base64": _bytes_to_b64_str(image["source"]["bytes"]),
"mime_type": f"image/{image['format']}",
}
_populate_extras(image_block, image, {"format", "source"})
yield image_block
else:
yield {"type": "non_standard", "value": block}
elif block.get("type") in types.KNOWN_BLOCK_TYPES:
yield cast("types.ContentBlock", block)
else:
yield {"type": "non_standard", "value": block}
return list(_iter_blocks())
def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
standard_citation: types.Citation = {"type": "citation"}
if "title" in citation:
standard_citation["title"] = citation["title"]
if (
(source_content := citation.get("source_content"))
and isinstance(source_content, list)
and all(isinstance(item, dict) for item in source_content)
):
standard_citation["cited_text"] = "".join(
item.get("text", "") for item in source_content
)
known_fields = {"type", "source_content", "title", "index", "extras"}
for key, value in citation.items():
if key not in known_fields:
if "extras" not in standard_citation:
standard_citation["extras"] = {}
standard_citation["extras"][key] = value
return standard_citation
def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]:
"""Convert Bedrock Converse message content to v1 format."""
if (
message.content == ""
and not message.additional_kwargs
and not message.tool_calls
):
# Converse outputs multiple chunks containing response metadata
return []
if isinstance(message.content, str):
message.content = [{"type": "text", "text": message.content}]
def _iter_blocks() -> Iterable[types.ContentBlock]:
for block in message.content:
if not isinstance(block, dict):
continue
block_type = block.get("type")
if block_type == "text":
if citations := block.get("citations"):
text_block: types.TextContentBlock = {
"type": "text",
"text": block.get("text", ""),
"annotations": [_convert_citation_to_v1(a) for a in citations],
}
else:
text_block = {"type": "text", "text": block["text"]}
if "index" in block:
text_block["index"] = block["index"]
yield text_block
elif block_type == "reasoning_content":
reasoning_block: types.ReasoningContentBlock = {"type": "reasoning"}
if reasoning_content := block.get("reasoning_content"):
if reasoning := reasoning_content.get("text"):
reasoning_block["reasoning"] = reasoning
if signature := reasoning_content.get("signature"):
if "extras" not in reasoning_block:
reasoning_block["extras"] = {}
reasoning_block["extras"]["signature"] = signature
if "index" in block:
reasoning_block["index"] = block["index"]
known_fields = {"type", "reasoning_content", "index", "extras"}
for key in block:
if key not in known_fields:
if "extras" not in reasoning_block:
reasoning_block["extras"] = {}
reasoning_block["extras"][key] = block[key]
yield reasoning_block
elif block_type == "tool_use":
if (
isinstance(message, AIMessageChunk)
and len(message.tool_call_chunks) == 1
and message.chunk_position != "last"
):
# Isolated chunk
tool_call_chunk: types.ToolCallChunk = (
message.tool_call_chunks[0].copy() # type: ignore[assignment]
)
if "type" not in tool_call_chunk:
tool_call_chunk["type"] = "tool_call_chunk"
yield tool_call_chunk
else:
tool_call_block: Optional[types.ToolCall] = None
# Non-streaming or gathered chunk
if len(message.tool_calls) == 1:
tool_call_block = {
"type": "tool_call",
"name": message.tool_calls[0]["name"],
"args": message.tool_calls[0]["args"],
"id": message.tool_calls[0].get("id"),
}
elif call_id := block.get("id"):
for tc in message.tool_calls:
if tc.get("id") == call_id:
tool_call_block = {
"type": "tool_call",
"name": tc["name"],
"args": tc["args"],
"id": tc.get("id"),
}
break
else:
pass
if not tool_call_block:
tool_call_block = {
"type": "tool_call",
"name": block.get("name", ""),
"args": block.get("input", {}),
"id": block.get("id", ""),
}
if "index" in block:
tool_call_block["index"] = block["index"]
yield tool_call_block
elif (
block_type == "input_json_delta"
and isinstance(message, AIMessageChunk)
and len(message.tool_call_chunks) == 1
):
tool_call_chunk = (
message.tool_call_chunks[0].copy() # type: ignore[assignment]
)
if "type" not in tool_call_chunk:
tool_call_chunk["type"] = "tool_call_chunk"
yield tool_call_chunk
else:
new_block: types.NonStandardContentBlock = {
"type": "non_standard",
"value": block,
}
if "index" in new_block["value"]:
new_block["index"] = new_block["value"].pop("index")
yield new_block
return list(_iter_blocks())
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Bedrock Converse content."""
return _convert_to_v1_from_converse(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a chunk with Bedrock Converse content."""
return _convert_to_v1_from_converse(message)
def _register_bedrock_converse_translator() -> None:
"""Register the Bedrock Converse translator with the central registry.
Run automatically when the module is imported.
"""
from langchain_core.messages.block_translators import ( # noqa: PLC0415
register_translator,
)
register_translator("bedrock_converse", translate_content, translate_content_chunk)
_register_bedrock_converse_translator()

View File

@@ -0,0 +1,529 @@
"""Derivations of standard content blocks from Google (GenAI) content."""
import base64
import re
from collections.abc import Iterable
from typing import Any, cast
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types
from langchain_core.messages.content import Citation, create_citation
def _bytes_to_b64_str(bytes_: bytes) -> str:
"""Convert bytes to base64 encoded string."""
return base64.b64encode(bytes_).decode("utf-8")
def translate_grounding_metadata_to_citations(
grounding_metadata: dict[str, Any],
) -> list[Citation]:
"""Translate Google AI grounding metadata to LangChain Citations.
Args:
grounding_metadata: Google AI grounding metadata containing web search
queries, grounding chunks, and grounding supports.
Returns:
List of Citation content blocks derived from the grounding metadata.
Example:
>>> metadata = {
... "web_search_queries": ["UEFA Euro 2024 winner"],
... "grounding_chunks": [
... {
... "web": {
... "uri": "https://uefa.com/euro2024",
... "title": "UEFA Euro 2024 Results",
... }
... }
... ],
... "grounding_supports": [
... {
... "segment": {
... "start_index": 0,
... "end_index": 47,
... "text": "Spain won the UEFA Euro 2024 championship",
... },
... "grounding_chunk_indices": [0],
... }
... ],
... }
>>> citations = translate_grounding_metadata_to_citations(metadata)
>>> len(citations)
1
>>> citations[0]["url"]
'https://uefa.com/euro2024'
"""
if not grounding_metadata:
return []
grounding_chunks = grounding_metadata.get("grounding_chunks", [])
grounding_supports = grounding_metadata.get("grounding_supports", [])
web_search_queries = grounding_metadata.get("web_search_queries", [])
citations: list[Citation] = []
for support in grounding_supports:
segment = support.get("segment", {})
chunk_indices = support.get("grounding_chunk_indices", [])
start_index = segment.get("start_index")
end_index = segment.get("end_index")
cited_text = segment.get("text")
# Create a citation for each referenced chunk
for chunk_index in chunk_indices:
if chunk_index < len(grounding_chunks):
chunk = grounding_chunks[chunk_index]
web_info = chunk.get("web", {})
citation = create_citation(
url=web_info.get("uri"),
title=web_info.get("title"),
start_index=start_index,
end_index=end_index,
cited_text=cited_text,
extras={
"google_ai_metadata": {
"web_search_queries": web_search_queries,
"grounding_chunk_index": chunk_index,
"confidence_scores": support.get("confidence_scores", []),
}
},
)
citations.append(citation)
return citations
def _convert_to_v1_from_genai_input(
content: list[types.ContentBlock],
) -> list[types.ContentBlock]:
"""Convert Google GenAI format blocks to v1 format.
Called when message isn't an `AIMessage` or `model_provider` isn't set on
`response_metadata`.
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
block as a ``'non_standard'`` block with the original block stored in the ``value``
field. This function attempts to unpack those blocks and convert any blocks that
might be GenAI format to v1 ContentBlocks.
If conversion fails, the block is left as a ``'non_standard'`` block.
Args:
content: List of content blocks to process.
Returns:
Updated list with GenAI blocks converted to v1 format.
"""
def _iter_blocks() -> Iterable[types.ContentBlock]:
blocks: list[dict[str, Any]] = [
cast("dict[str, Any]", block)
if block.get("type") != "non_standard"
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
for block in content
]
for block in blocks:
num_keys = len(block)
block_type = block.get("type")
if num_keys == 1 and (text := block.get("text")):
# This is probably a TextContentBlock
yield {"type": "text", "text": text}
elif (
num_keys == 1
and (document := block.get("document"))
and isinstance(document, dict)
and "format" in document
):
# Handle document format conversion
doc_format = document.get("format")
source = document.get("source", {})
if doc_format == "pdf" and "bytes" in source:
# PDF document with byte data
file_block: types.FileContentBlock = {
"type": "file",
"base64": source["bytes"]
if isinstance(source["bytes"], str)
else _bytes_to_b64_str(source["bytes"]),
"mime_type": "application/pdf",
}
# Preserve extra fields
extras = {
key: value
for key, value in document.items()
if key not in {"format", "source"}
}
if extras:
file_block["extras"] = extras
yield file_block
elif doc_format == "txt" and "text" in source:
# Text document
plain_text_block: types.PlainTextContentBlock = {
"type": "text-plain",
"text": source["text"],
"mime_type": "text/plain",
}
# Preserve extra fields
extras = {
key: value
for key, value in document.items()
if key not in {"format", "source"}
}
if extras:
plain_text_block["extras"] = extras
yield plain_text_block
else:
# Unknown document format
yield {"type": "non_standard", "value": block}
elif (
num_keys == 1
and (image := block.get("image"))
and isinstance(image, dict)
and "format" in image
):
# Handle image format conversion
img_format = image.get("format")
source = image.get("source", {})
if "bytes" in source:
# Image with byte data
image_block: types.ImageContentBlock = {
"type": "image",
"base64": source["bytes"]
if isinstance(source["bytes"], str)
else _bytes_to_b64_str(source["bytes"]),
"mime_type": f"image/{img_format}",
}
# Preserve extra fields
extras = {}
for key, value in image.items():
if key not in {"format", "source"}:
extras[key] = value
if extras:
image_block["extras"] = extras
yield image_block
else:
# Image without byte data
yield {"type": "non_standard", "value": block}
elif block_type == "file_data" and "file_uri" in block:
# Handle FileData URI-based content
uri_file_block: types.FileContentBlock = {
"type": "file",
"url": block["file_uri"],
}
if mime_type := block.get("mime_type"):
uri_file_block["mime_type"] = mime_type
yield uri_file_block
elif block_type == "function_call" and "name" in block:
# Handle function calls
tool_call_block: types.ToolCall = {
"type": "tool_call",
"name": block["name"],
"args": block.get("args", {}),
"id": block.get("id", ""),
}
yield tool_call_block
elif block_type == "executable_code":
server_tool_call_input: types.ServerToolCall = {
"type": "server_tool_call",
"name": "code_interpreter",
"args": {
"code": block.get("executable_code", ""),
"language": block.get("language", "python"),
},
"id": block.get("id", ""),
}
yield server_tool_call_input
elif block_type == "code_execution_result":
outcome = block.get("outcome", 1)
status = "success" if outcome == 1 else "error"
server_tool_result_input: types.ServerToolResult = {
"type": "server_tool_result",
"tool_call_id": block.get("tool_call_id", ""),
"status": status, # type: ignore[typeddict-item]
"output": block.get("code_execution_result", ""),
}
if outcome is not None:
server_tool_result_input["extras"] = {"outcome": outcome}
yield server_tool_result_input
elif block.get("type") in types.KNOWN_BLOCK_TYPES:
# We see a standard block type, so we just cast it, even if
# we don't fully understand it. This may be dangerous, but
# it's better than losing information.
yield cast("types.ContentBlock", block)
else:
# We don't understand this block at all.
yield {"type": "non_standard", "value": block}
return list(_iter_blocks())
def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
"""Convert Google GenAI message content to v1 format.
Calling `.content_blocks` on an `AIMessage` where `response_metadata.model_provider`
is set to `'google_genai'` will invoke this function to parse the content into
standard content blocks for returning.
Args:
message: The AIMessage or AIMessageChunk to convert.
Returns:
List of standard content blocks derived from the message content.
"""
if isinstance(message.content, str):
# String content -> TextContentBlock (only add if non-empty in case of audio)
string_blocks: list[types.ContentBlock] = []
if message.content:
string_blocks.append({"type": "text", "text": message.content})
# Add any missing tool calls from message.tool_calls field
content_tool_call_ids = {
block.get("id")
for block in string_blocks
if isinstance(block, dict) and block.get("type") == "tool_call"
}
for tool_call in message.tool_calls:
id_ = tool_call.get("id")
if id_ and id_ not in content_tool_call_ids:
string_tool_call_block: types.ToolCall = {
"type": "tool_call",
"id": id_,
"name": tool_call["name"],
"args": tool_call["args"],
}
string_blocks.append(string_tool_call_block)
# Handle audio from additional_kwargs if present (for empty content cases)
audio_data = message.additional_kwargs.get("audio")
if audio_data and isinstance(audio_data, bytes):
audio_block: types.AudioContentBlock = {
"type": "audio",
"base64": _bytes_to_b64_str(audio_data),
"mime_type": "audio/wav", # Default to WAV for Google GenAI
}
string_blocks.append(audio_block)
grounding_metadata = message.response_metadata.get("grounding_metadata")
if grounding_metadata:
citations = translate_grounding_metadata_to_citations(grounding_metadata)
for block in string_blocks:
if block["type"] == "text" and citations:
# Add citations to the first text block only
block["annotations"] = cast("list[types.Annotation]", citations)
break
return string_blocks
if not isinstance(message.content, list):
# Unexpected content type, attempt to represent as text
return [{"type": "text", "text": str(message.content)}]
converted_blocks: list[types.ContentBlock] = []
for item in message.content:
if isinstance(item, str):
# Conversation history strings
# Citations are handled below after all blocks are converted
converted_blocks.append({"type": "text", "text": item}) # TextContentBlock
elif isinstance(item, dict):
item_type = item.get("type")
if item_type == "image_url":
# Convert image_url to standard image block (base64)
# (since the original implementation returned as url-base64 CC style)
image_url = item.get("image_url", {})
url = image_url.get("url", "")
if url:
# Extract base64 data
match = re.match(r"data:([^;]+);base64,(.+)", url)
if match:
# Data URI provided
mime_type, base64_data = match.groups()
converted_blocks.append(
{
"type": "image",
"base64": base64_data,
"mime_type": mime_type,
}
)
else:
# Assume it's raw base64 without data URI
try:
# Validate base64 and decode for mime type detection
decoded_bytes = base64.b64decode(url, validate=True)
image_url_b64_block = {
"type": "image",
"base64": url,
}
try:
import filetype # type: ignore[import-not-found] # noqa: PLC0415
# Guess mime type based on file bytes
mime_type = None
kind = filetype.guess(decoded_bytes)
if kind:
mime_type = kind.mime
if mime_type:
image_url_b64_block["mime_type"] = mime_type
except ImportError:
# filetype library not available, skip type detection
pass
converted_blocks.append(
cast("types.ImageContentBlock", image_url_b64_block)
)
except Exception:
# Not valid base64, treat as non-standard
converted_blocks.append(
{"type": "non_standard", "value": item}
)
else:
# This likely won't be reached according to previous implementations
converted_blocks.append({"type": "non_standard", "value": item})
msg = "Image URL not a data URI; appending as non-standard block."
raise ValueError(msg)
elif item_type == "function_call":
# Handle Google GenAI function calls
function_call_block: types.ToolCall = {
"type": "tool_call",
"name": item.get("name", ""),
"args": item.get("args", {}),
"id": item.get("id", ""),
}
converted_blocks.append(function_call_block)
elif item_type == "file_data":
# Handle FileData URI-based content
file_block: types.FileContentBlock = {
"type": "file",
"url": item.get("file_uri", ""),
}
if mime_type := item.get("mime_type"):
file_block["mime_type"] = mime_type
converted_blocks.append(file_block)
elif item_type == "thinking":
# Handling for the 'thinking' type we package thoughts as
reasoning_block: types.ReasoningContentBlock = {
"type": "reasoning",
"reasoning": item.get("thinking", ""),
}
if signature := item.get("signature"):
reasoning_block["extras"] = {"signature": signature}
converted_blocks.append(reasoning_block)
elif item_type == "executable_code":
# Convert to standard server tool call block at the moment
server_tool_call_block: types.ServerToolCall = {
"type": "server_tool_call",
"name": "code_interpreter",
"args": {
"code": item.get("executable_code", ""),
"language": item.get("language", "python"), # Default to python
},
"id": item.get("id", ""),
}
converted_blocks.append(server_tool_call_block)
elif item_type == "code_execution_result":
# Map outcome to status: OUTCOME_OK (1) → success, else → error
outcome = item.get("outcome", 1)
status = "success" if outcome == 1 else "error"
server_tool_result_block: types.ServerToolResult = {
"type": "server_tool_result",
"tool_call_id": item.get("tool_call_id", ""),
"status": status, # type: ignore[typeddict-item]
"output": item.get("code_execution_result", ""),
}
# Preserve original outcome in extras
if outcome is not None:
server_tool_result_block["extras"] = {"outcome": outcome}
converted_blocks.append(server_tool_result_block)
else:
# Unknown type, preserve as non-standard
converted_blocks.append({"type": "non_standard", "value": item})
else:
# Non-dict, non-string content
converted_blocks.append({"type": "non_standard", "value": item})
grounding_metadata = message.response_metadata.get("grounding_metadata")
if grounding_metadata:
citations = translate_grounding_metadata_to_citations(grounding_metadata)
for block in converted_blocks:
if block["type"] == "text" and citations:
# Add citations to text blocks (only the first text block)
block["annotations"] = cast("list[types.Annotation]", citations)
break
# Audio is stored on the message.additional_kwargs
audio_data = message.additional_kwargs.get("audio")
if audio_data and isinstance(audio_data, bytes):
audio_block_kwargs: types.AudioContentBlock = {
"type": "audio",
"base64": _bytes_to_b64_str(audio_data),
"mime_type": "audio/wav", # Default to WAV for Google GenAI
}
converted_blocks.append(audio_block_kwargs)
# Add any missing tool calls from message.tool_calls field
content_tool_call_ids = {
block.get("id")
for block in converted_blocks
if isinstance(block, dict) and block.get("type") == "tool_call"
}
for tool_call in message.tool_calls:
id_ = tool_call.get("id")
if id_ and id_ not in content_tool_call_ids:
missing_tool_call_block: types.ToolCall = {
"type": "tool_call",
"id": id_,
"name": tool_call["name"],
"args": tool_call["args"],
}
converted_blocks.append(missing_tool_call_block)
return converted_blocks
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Google (GenAI) content."""
return _convert_to_v1_from_genai(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a chunk with Google (GenAI) content."""
return _convert_to_v1_from_genai(message)
def _register_google_genai_translator() -> None:
"""Register the Google (GenAI) translator with the central registry.
Run automatically when the module is imported.
"""
from langchain_core.messages.block_translators import ( # noqa: PLC0415
register_translator,
)
register_translator("google_genai", translate_content, translate_content_chunk)
_register_google_genai_translator()

View File

@@ -0,0 +1,117 @@
"""Derivations of standard content blocks from Google (VertexAI) content."""
from collections.abc import Iterable
from typing import Any, cast
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types
WARNED = False
def _convert_to_v1_from_vertexai_input(
content: list[types.ContentBlock],
) -> list[types.ContentBlock]:
"""Convert Google (VertexAI) format blocks to v1 format.
Called when message isn't an `AIMessage` or `model_provider` isn't set on
`response_metadata`.
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
block as a ``'non_standard'`` block with the original block stored in the ``value``
field. This function attempts to unpack those blocks and convert any blocks that
might be GenAI format to v1 ContentBlocks.
If conversion fails, the block is left as a ``'non_standard'`` block.
Args:
content: List of content blocks to process.
Returns:
Updated list with VertexAI blocks converted to v1 format.
"""
def _iter_blocks() -> Iterable[types.ContentBlock]:
blocks: list[dict[str, Any]] = [
cast("dict[str, Any]", block)
if block.get("type") != "non_standard"
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
for block in content
]
for block in blocks:
num_keys = len(block)
if num_keys == 1 and (text := block.get("text")):
# This is probably a TextContentBlock
yield {"type": "text", "text": text}
elif (
num_keys == 1
and (document := block.get("document"))
and isinstance(document, dict)
and "format" in document
):
# Probably a document of some kind - TODO
yield {"type": "non_standard", "value": block}
elif (
num_keys == 1
and (image := block.get("image"))
and isinstance(image, dict)
and "format" in image
):
# Probably an image of some kind - TODO
yield {"type": "non_standard", "value": block}
elif block.get("type") in types.KNOWN_BLOCK_TYPES:
# We see a standard block type, so we just cast it, even if
# we don't fully understand it. This may be dangerous, but
# it's better than losing information.
yield cast("types.ContentBlock", block)
else:
# We don't understand this block at all.
yield {"type": "non_standard", "value": block}
return list(_iter_blocks())
def _convert_to_v1_from_vertexai(message: AIMessage) -> list[types.ContentBlock]:
"""Convert Google (VertexAI) message content to v1 format.
Calling `.content_blocks` on an `AIMessage` where `response_metadata.model_provider`
is set to `'google_vertexai'` will invoke this function to parse the content into
standard content blocks for returning.
Args:
message: The AIMessage or AIMessageChunk to convert.
Returns:
List of standard content blocks derived from the message content.
"""
return message # type: ignore[return-value]
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Google (VertexAI) content."""
return _convert_to_v1_from_vertexai(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a chunk with Google (VertexAI) content."""
return _convert_to_v1_from_vertexai(message)
def _register_google_vertexai_translator() -> None:
"""Register the Google (VertexAI) translator with the central registry.
Run automatically when the module is imported.
"""
from langchain_core.messages.block_translators import ( # noqa: PLC0415
register_translator,
)
register_translator("google_vertexai", translate_content, translate_content_chunk)
_register_google_vertexai_translator()

View File

@@ -0,0 +1,47 @@
"""Derivations of standard content blocks from Groq content."""
import warnings
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types
WARNED = False
def translate_content(message: AIMessage) -> list[types.ContentBlock]: # noqa: ARG001
"""Derive standard content blocks from a message with Groq content."""
global WARNED # noqa: PLW0603
if not WARNED:
warning_message = (
"Content block standardization is not yet fully supported for Groq."
)
warnings.warn(warning_message, stacklevel=2)
WARNED = True
raise NotImplementedError
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]: # noqa: ARG001
"""Derive standard content blocks from a message chunk with Groq content."""
global WARNED # noqa: PLW0603
if not WARNED:
warning_message = (
"Content block standardization is not yet fully supported for Groq."
)
warnings.warn(warning_message, stacklevel=2)
WARNED = True
raise NotImplementedError
def _register_groq_translator() -> None:
"""Register the Groq translator with the central registry.
Run automatically when the module is imported.
"""
from langchain_core.messages.block_translators import ( # noqa: PLC0415
register_translator,
)
register_translator("groq", translate_content, translate_content_chunk)
_register_groq_translator()

View File

@@ -0,0 +1,301 @@
"""Derivations of standard content blocks from LangChain v0 multimodal content."""
from typing import Any, Union, cast
from langchain_core.messages import content as types
def _convert_v0_multimodal_input_to_v1(
content: list[types.ContentBlock],
) -> list[types.ContentBlock]:
"""Convert v0 multimodal blocks to v1 format.
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
block as a ``'non_standard'`` block with the original block stored in the ``value``
field. This function attempts to unpack those blocks and convert any v0 format
blocks to v1 format.
If conversion fails, the block is left as a ``'non_standard'`` block.
Args:
content: List of content blocks to process.
Returns:
v1 content blocks.
"""
converted_blocks = []
unpacked_blocks: list[dict[str, Any]] = [
cast("dict[str, Any]", block)
if block.get("type") != "non_standard"
else block["value"] # type: ignore[typeddict-item] # this is only non-standard blocks
for block in content
]
for block in unpacked_blocks:
if block.get("type") in {"image", "audio", "file"} and "source_type" in block:
converted_block = _convert_legacy_v0_content_block_to_v1(block)
converted_blocks.append(cast("types.ContentBlock", converted_block))
elif block.get("type") in types.KNOWN_BLOCK_TYPES:
# Guard in case this function is used outside of the .content_blocks flow
converted_blocks.append(cast("types.ContentBlock", block))
else:
converted_blocks.append({"type": "non_standard", "value": block})
return converted_blocks
def _convert_legacy_v0_content_block_to_v1(
block: dict,
) -> Union[types.ContentBlock, dict]:
"""Convert a LangChain v0 content block to v1 format.
Preserves unknown keys as extras to avoid data loss.
Returns the original block unchanged if it's not in v0 format.
"""
def _extract_v0_extras(block_dict: dict, known_keys: set[str]) -> dict[str, Any]:
"""Extract unknown keys from v0 block to preserve as extras.
Args:
block_dict: The original v0 block dictionary.
known_keys: Set of keys known to be part of the v0 format for this block.
Returns:
A dictionary of extra keys not part of the known v0 format.
"""
return {k: v for k, v in block_dict.items() if k not in known_keys}
# Check if this is actually a v0 format block
block_type = block.get("type")
if block_type not in {"image", "audio", "file"} or "source_type" not in block:
# Not a v0 format block, return unchanged
return block
if block.get("type") == "image":
source_type = block.get("source_type")
if source_type == "url":
# image-url
known_keys = {"mime_type", "type", "source_type", "url"}
extras = _extract_v0_extras(block, known_keys)
if "id" in block:
return types.create_image_block(
url=block["url"],
mime_type=block.get("mime_type"),
id=block["id"],
**extras,
)
# Don't construct with an ID if not present in original block
v1_image_url = types.ImageContentBlock(type="image", url=block["url"])
if block.get("mime_type"):
v1_image_url["mime_type"] = block["mime_type"]
v1_image_url["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_image_url["extras"][key] = value
if v1_image_url["extras"] == {}:
del v1_image_url["extras"]
return v1_image_url
if source_type == "base64":
# image-base64
known_keys = {"mime_type", "type", "source_type", "data"}
extras = _extract_v0_extras(block, known_keys)
if "id" in block:
return types.create_image_block(
base64=block["data"],
mime_type=block.get("mime_type"),
id=block["id"],
**extras,
)
v1_image_base64 = types.ImageContentBlock(
type="image", base64=block["data"]
)
if block.get("mime_type"):
v1_image_base64["mime_type"] = block["mime_type"]
v1_image_base64["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_image_base64["extras"][key] = value
if v1_image_base64["extras"] == {}:
del v1_image_base64["extras"]
return v1_image_base64
if source_type == "id":
# image-id
known_keys = {"type", "source_type", "id"}
extras = _extract_v0_extras(block, known_keys)
# For id `source_type`, `id` is the file reference, not block ID
v1_image_id = types.ImageContentBlock(type="image", file_id=block["id"])
v1_image_id["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_image_id["extras"][key] = value
if v1_image_id["extras"] == {}:
del v1_image_id["extras"]
return v1_image_id
elif block.get("type") == "audio":
source_type = block.get("source_type")
if source_type == "url":
# audio-url
known_keys = {"mime_type", "type", "source_type", "url"}
extras = _extract_v0_extras(block, known_keys)
if "id" in block:
return types.create_audio_block(
url=block["url"],
mime_type=block.get("mime_type"),
id=block["id"],
**extras,
)
# Don't construct with an ID if not present in original block
v1_audio_url: types.AudioContentBlock = types.AudioContentBlock(
type="audio", url=block["url"]
)
if block.get("mime_type"):
v1_audio_url["mime_type"] = block["mime_type"]
v1_audio_url["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_audio_url["extras"][key] = value
if v1_audio_url["extras"] == {}:
del v1_audio_url["extras"]
return v1_audio_url
if source_type == "base64":
# audio-base64
known_keys = {"mime_type", "type", "source_type", "data"}
extras = _extract_v0_extras(block, known_keys)
if "id" in block:
return types.create_audio_block(
base64=block["data"],
mime_type=block.get("mime_type"),
id=block["id"],
**extras,
)
v1_audio_base64: types.AudioContentBlock = types.AudioContentBlock(
type="audio", base64=block["data"]
)
if block.get("mime_type"):
v1_audio_base64["mime_type"] = block["mime_type"]
v1_audio_base64["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_audio_base64["extras"][key] = value
if v1_audio_base64["extras"] == {}:
del v1_audio_base64["extras"]
return v1_audio_base64
if source_type == "id":
# audio-id
known_keys = {"type", "source_type", "id"}
extras = _extract_v0_extras(block, known_keys)
v1_audio_id: types.AudioContentBlock = types.AudioContentBlock(
type="audio", file_id=block["id"]
)
v1_audio_id["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_audio_id["extras"][key] = value
if v1_audio_id["extras"] == {}:
del v1_audio_id["extras"]
return v1_audio_id
elif block.get("type") == "file":
source_type = block.get("source_type")
if source_type == "url":
# file-url
known_keys = {"mime_type", "type", "source_type", "url"}
extras = _extract_v0_extras(block, known_keys)
if "id" in block:
return types.create_file_block(
url=block["url"],
mime_type=block.get("mime_type"),
id=block["id"],
**extras,
)
v1_file_url: types.FileContentBlock = types.FileContentBlock(
type="file", url=block["url"]
)
if block.get("mime_type"):
v1_file_url["mime_type"] = block["mime_type"]
v1_file_url["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_file_url["extras"][key] = value
if v1_file_url["extras"] == {}:
del v1_file_url["extras"]
return v1_file_url
if source_type == "base64":
# file-base64
known_keys = {"mime_type", "type", "source_type", "data"}
extras = _extract_v0_extras(block, known_keys)
if "id" in block:
return types.create_file_block(
base64=block["data"],
mime_type=block.get("mime_type"),
id=block["id"],
**extras,
)
v1_file_base64: types.FileContentBlock = types.FileContentBlock(
type="file", base64=block["data"]
)
if block.get("mime_type"):
v1_file_base64["mime_type"] = block["mime_type"]
v1_file_base64["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_file_base64["extras"][key] = value
if v1_file_base64["extras"] == {}:
del v1_file_base64["extras"]
return v1_file_base64
if source_type == "id":
# file-id
known_keys = {"type", "source_type", "id"}
extras = _extract_v0_extras(block, known_keys)
return types.create_file_block(file_id=block["id"], **extras)
if source_type == "text":
# file-text
known_keys = {"mime_type", "type", "source_type", "url"}
extras = _extract_v0_extras(block, known_keys)
if "id" in block:
return types.create_plaintext_block(
# In v0, URL points to the text file content
# TODO: attribute this claim
text=block["url"],
id=block["id"],
**extras,
)
v1_file_text: types.PlainTextContentBlock = types.PlainTextContentBlock(
type="text-plain", text=block["url"], mime_type="text/plain"
)
if block.get("mime_type"):
v1_file_text["mime_type"] = block["mime_type"]
v1_file_text["extras"] = {}
for key, value in extras.items():
if value is not None:
v1_file_text["extras"][key] = value
if v1_file_text["extras"] == {}:
del v1_file_text["extras"]
return v1_file_text
# If we can't convert, return the block unchanged
return block

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +0,0 @@
"""Types for content blocks."""
import warnings
from typing import Any, Literal, Union
from pydantic import TypeAdapter, ValidationError
from typing_extensions import NotRequired, TypedDict
class BaseDataContentBlock(TypedDict, total=False):
"""Base class for data content blocks."""
mime_type: NotRequired[str]
"""MIME type of the content block (if needed)."""
class URLContentBlock(BaseDataContentBlock):
"""Content block for data from a URL."""
type: Literal["image", "audio", "file"]
"""Type of the content block."""
source_type: Literal["url"]
"""Source type (url)."""
url: str
"""URL for data."""
class Base64ContentBlock(BaseDataContentBlock):
"""Content block for inline data from a base64 string."""
type: Literal["image", "audio", "file"]
"""Type of the content block."""
source_type: Literal["base64"]
"""Source type (base64)."""
data: str
"""Data as a base64 string."""
class PlainTextContentBlock(BaseDataContentBlock):
"""Content block for plain text data (e.g., from a document)."""
type: Literal["file"]
"""Type of the content block."""
source_type: Literal["text"]
"""Source type (text)."""
text: str
"""Text data."""
class IDContentBlock(TypedDict):
"""Content block for data specified by an identifier."""
type: Literal["image", "audio", "file"]
"""Type of the content block."""
source_type: Literal["id"]
"""Source type (id)."""
id: str
"""Identifier for data source."""
DataContentBlock = Union[
URLContentBlock,
Base64ContentBlock,
PlainTextContentBlock,
IDContentBlock,
]
_DataContentBlockAdapter: TypeAdapter[DataContentBlock] = TypeAdapter(DataContentBlock)
def is_data_content_block(
content_block: dict,
) -> bool:
"""Check if the content block is a standard data content block.
Args:
content_block: The content block to check.
Returns:
True if the content block is a data content block, False otherwise.
"""
try:
_ = _DataContentBlockAdapter.validate_python(content_block)
except ValidationError:
return False
else:
return True
def convert_to_openai_image_block(content_block: dict[str, Any]) -> dict:
"""Convert image content block to format expected by OpenAI Chat Completions API.
Args:
content_block: The content block to convert.
Raises:
ValueError: If the source type is not supported or if ``mime_type`` is missing
for base64 data.
Returns:
A dictionary formatted for OpenAI's API.
"""
if content_block["source_type"] == "url":
return {
"type": "image_url",
"image_url": {
"url": content_block["url"],
},
}
if content_block["source_type"] == "base64":
if "mime_type" not in content_block:
error_message = "mime_type key is required for base64 data."
raise ValueError(error_message)
mime_type = content_block["mime_type"]
return {
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{content_block['data']}",
},
}
error_message = "Unsupported source type. Only 'url' and 'base64' are supported."
raise ValueError(error_message)
def convert_to_openai_data_block(block: dict) -> dict:
"""Format standard data content block to format expected by OpenAI.
Args:
block: A data content block.
Raises:
ValueError: If the block type or source type is not supported.
Returns:
A dictionary formatted for OpenAI's API.
"""
if block["type"] == "image":
formatted_block = convert_to_openai_image_block(block)
elif block["type"] == "file":
if block["source_type"] == "base64":
file = {"file_data": f"data:{block['mime_type']};base64,{block['data']}"}
if filename := block.get("filename"):
file["filename"] = filename
elif (metadata := block.get("metadata")) and ("filename" in metadata):
file["filename"] = metadata["filename"]
else:
warnings.warn(
"OpenAI may require a filename for file inputs. Specify a filename "
"in the content block: {'type': 'file', 'source_type': 'base64', "
"'mime_type': 'application/pdf', 'data': '...', "
"'filename': 'my-pdf'}",
stacklevel=1,
)
formatted_block = {"type": "file", "file": file}
elif block["source_type"] == "id":
formatted_block = {"type": "file", "file": {"file_id": block["id"]}}
else:
error_msg = "source_type base64 or id is required for file blocks."
raise ValueError(error_msg)
elif block["type"] == "audio":
if block["source_type"] == "base64":
audio_format = block["mime_type"].split("/")[-1]
formatted_block = {
"type": "input_audio",
"input_audio": {"data": block["data"], "format": audio_format},
}
else:
error_msg = "source_type base64 is required for audio blocks."
raise ValueError(error_msg)
else:
error_msg = f"Block of type {block['type']} is not supported."
raise ValueError(error_msg)
return formatted_block

View File

@@ -1,7 +1,8 @@
"""Human message."""
from typing import Any, Literal, Union
from typing import Any, Literal, Optional, Union, cast, overload
from langchain_core.messages import content as types
from langchain_core.messages.base import BaseMessage, BaseMessageChunk
@@ -27,14 +28,6 @@ class HumanMessage(BaseMessage):
"""
example: bool = False
"""Use to denote that a message is part of an example conversation.
At the moment, this is ignored by most models. Usage is discouraged.
Defaults to False.
"""
type: Literal["human"] = "human"
"""The type of the message (used for serialization).
@@ -42,18 +35,35 @@ class HumanMessage(BaseMessage):
"""
@overload
def __init__(
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None:
"""Initialize ``HumanMessage``.
) -> None: ...
Args:
content: The string contents of the message.
kwargs: Additional fields to pass to the message.
"""
super().__init__(content=content, **kwargs)
@overload
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None: ...
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None:
"""Specify ``content`` as positional arg or ``content_blocks`` for typing."""
if content_blocks is not None:
super().__init__(
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
**kwargs,
)
else:
super().__init__(content=content, **kwargs)
class HumanMessageChunk(HumanMessage, BaseMessageChunk):

View File

@@ -1,7 +1,8 @@
"""System message."""
from typing import Any, Literal, Union
from typing import Any, Literal, Optional, Union, cast, overload
from langchain_core.messages import content as types
from langchain_core.messages.base import BaseMessage, BaseMessageChunk
@@ -34,16 +35,35 @@ class SystemMessage(BaseMessage):
"""
@overload
def __init__(
self, content: Union[str, list[Union[str, dict]]], **kwargs: Any
) -> None:
"""Pass in content as positional arg.
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None: ...
Args:
content: The string contents of the message.
kwargs: Additional fields to pass to the message.
"""
super().__init__(content=content, **kwargs)
@overload
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None: ...
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None:
"""Specify ``content`` as positional arg or ``content_blocks`` for typing."""
if content_blocks is not None:
super().__init__(
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
**kwargs,
)
else:
super().__init__(content=content, **kwargs)
class SystemMessageChunk(SystemMessage, BaseMessageChunk):

View File

@@ -1,13 +1,15 @@
"""Messages for tools."""
import json
from typing import Any, Literal, Optional, Union
from typing import Any, Literal, Optional, Union, cast, overload
from uuid import UUID
from pydantic import Field, model_validator
from typing_extensions import NotRequired, TypedDict, override
from langchain_core.messages import content as types
from langchain_core.messages.base import BaseMessage, BaseMessageChunk, merge_content
from langchain_core.messages.content import InvalidToolCall
from langchain_core.utils._merge import merge_dicts, merge_obj
@@ -142,18 +144,43 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
values["tool_call_id"] = str(tool_call_id)
return values
@overload
def __init__(
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None: ...
@overload
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None: ...
def __init__(
self,
content: Optional[Union[str, list[Union[str, dict]]]] = None,
content_blocks: Optional[list[types.ContentBlock]] = None,
**kwargs: Any,
) -> None:
"""Initialize ``ToolMessage``.
Specify ``content`` as positional arg or ``content_blocks`` for typing.
Args:
content: The string contents of the message.
content_blocks: Typed standard content.
**kwargs: Additional fields.
"""
super().__init__(content=content, **kwargs)
if content_blocks is not None:
super().__init__(
content=cast("Union[str, list[Union[str, dict]]]", content_blocks),
**kwargs,
)
else:
super().__init__(content=content, **kwargs)
class ToolMessageChunk(ToolMessage, BaseMessageChunk):
@@ -290,24 +317,6 @@ def tool_call_chunk(
)
class InvalidToolCall(TypedDict):
"""Allowance for errors made by LLM.
Here we add an ``error`` key to surface errors made during generation
(e.g., invalid JSON arguments.)
"""
name: Optional[str]
"""The name of the tool to be called."""
args: Optional[str]
"""The arguments to the tool call."""
id: Optional[str]
"""An identifier associated with the tool call."""
error: Optional[str]
"""An error message associated with the tool call."""
type: NotRequired[Literal["invalid_tool_call"]]
def invalid_tool_call(
*,
name: Optional[str] = None,

View File

@@ -32,10 +32,15 @@ from typing import (
from pydantic import Discriminator, Field, Tag
from langchain_core.exceptions import ErrorCode, create_message
from langchain_core.messages import convert_to_openai_data_block, is_data_content_block
from langchain_core.messages.ai import AIMessage, AIMessageChunk
from langchain_core.messages.base import BaseMessage, BaseMessageChunk
from langchain_core.messages.block_translators.openai import (
convert_to_openai_data_block,
)
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
from langchain_core.messages.content import (
is_data_content_block,
)
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
from langchain_core.messages.human import HumanMessage, HumanMessageChunk
from langchain_core.messages.modifier import RemoveMessage
@@ -137,7 +142,7 @@ def get_buffer_string(
else:
msg = f"Got unsupported message type: {m}"
raise ValueError(msg) # noqa: TRY004
message = f"{role}: {m.text()}"
message = f"{role}: {m.text}"
if isinstance(m, AIMessage) and "function_call" in m.additional_kwargs:
message += f"{m.additional_kwargs['function_call']}"
string_messages.append(message)
@@ -204,7 +209,7 @@ def message_chunk_to_message(chunk: BaseMessage) -> BaseMessage:
# chunk classes always have the equivalent non-chunk class as their first parent
ignore_keys = ["type"]
if isinstance(chunk, AIMessageChunk):
ignore_keys.append("tool_call_chunks")
ignore_keys.extend(["tool_call_chunks", "chunk_position"])
return chunk.__class__.__mro__[1](
**{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}
)
@@ -1040,7 +1045,6 @@ def convert_to_openai_messages(
messages: Union[MessageLikeRepresentation, Sequence[MessageLikeRepresentation]],
*,
text_format: Literal["string", "block"] = "string",
include_id: bool = False,
) -> Union[dict, list[dict]]:
"""Convert LangChain messages into OpenAI message dicts.
@@ -1058,8 +1062,6 @@ def convert_to_openai_messages(
If a message has a string content, this is turned into a list
with a single content block of type ``'text'``. If a message has
content blocks these are left as is.
include_id: Whether to include message ids in the openai messages, if they
are present in the source messages.
Raises:
ValueError: if an unrecognized ``text_format`` is specified, or if a message
@@ -1148,8 +1150,6 @@ def convert_to_openai_messages(
oai_msg["refusal"] = message.additional_kwargs["refusal"]
if isinstance(message, ToolMessage):
oai_msg["tool_call_id"] = message.tool_call_id
if include_id and message.id:
oai_msg["id"] = message.id
if not message.content:
content = "" if text_format == "string" else []
@@ -1622,11 +1622,15 @@ def _msg_to_chunk(message: BaseMessage) -> BaseMessageChunk:
def _chunk_to_msg(chunk: BaseMessageChunk) -> BaseMessage:
if chunk.__class__ in _CHUNK_MSG_MAP:
return _CHUNK_MSG_MAP[chunk.__class__](
**chunk.model_dump(exclude={"type", "tool_call_chunks"})
**chunk.model_dump(exclude={"type", "tool_call_chunks", "chunk_position"})
)
for chunk_cls, msg_cls in _CHUNK_MSG_MAP.items():
if isinstance(chunk, chunk_cls):
return msg_cls(**chunk.model_dump(exclude={"type", "tool_call_chunks"}))
return msg_cls(
**chunk.model_dump(
exclude={"type", "tool_call_chunks", "chunk_position"}
)
)
msg = (
f"Unrecognized message chunk class {chunk.__class__}. Supported classes are "

View File

@@ -133,7 +133,7 @@ class ImagePromptValue(PromptValue):
def to_string(self) -> str:
"""Return prompt (image URL) as string."""
return self.image_url["url"]
return self.image_url.get("url", "")
def to_messages(self) -> list[BaseMessage]:
"""Return prompt (image URL) as messages."""

View File

@@ -379,10 +379,10 @@ class BasePromptTemplate(
directory_path.mkdir(parents=True, exist_ok=True)
if save_path.suffix == ".json":
with save_path.open("w") as f:
with save_path.open("w", encoding="utf-8") as f:
json.dump(prompt_dict, f, indent=4)
elif save_path.suffix.endswith((".yaml", ".yml")):
with save_path.open("w") as f:
with save_path.open("w", encoding="utf-8") as f:
yaml.dump(prompt_dict, f, default_flow_style=False)
else:
msg = f"{save_path} must be json or yaml"

View File

@@ -543,8 +543,7 @@ class _StringImageMessagePromptTemplate(BaseMessagePromptTemplate):
Returns:
A new instance of this class.
"""
template = Path(template_file).read_text()
# TODO: .read_text(encoding="utf-8") for v0.4
template = Path(template_file).read_text(encoding="utf-8")
return cls.from_template(template, input_variables=input_variables, **kwargs)
def format_messages(self, **kwargs: Any) -> list[BaseMessage]:

View File

@@ -53,7 +53,7 @@ def _load_template(var_name: str, config: dict) -> dict:
template_path = Path(config.pop(f"{var_name}_path"))
# Load the template.
if template_path.suffix == ".txt":
template = template_path.read_text()
template = template_path.read_text(encoding="utf-8")
else:
raise ValueError
# Set the template variable to the extracted variable.
@@ -67,7 +67,7 @@ def _load_examples(config: dict) -> dict:
pass
elif isinstance(config["examples"], str):
path = Path(config["examples"])
with path.open() as f:
with path.open(encoding="utf-8") as f:
if path.suffix == ".json":
examples = json.load(f)
elif path.suffix in {".yaml", ".yml"}:

View File

@@ -4,9 +4,8 @@ from __future__ import annotations
import warnings
from abc import ABC
from collections.abc import Callable, Sequence
from string import Formatter
from typing import Any, Literal
from typing import Any, Callable, Literal
from pydantic import BaseModel, create_model
@@ -17,66 +16,9 @@ from langchain_core.utils.formatting import formatter
from langchain_core.utils.interactive_env import is_interactive_env
try:
from jinja2 import meta
from jinja2.exceptions import SecurityError
from jinja2 import Environment, meta
from jinja2.sandbox import SandboxedEnvironment
class _RestrictedSandboxedEnvironment(SandboxedEnvironment):
"""A more restrictive Jinja2 sandbox that blocks all attribute/method access.
This sandbox only allows simple variable lookups, no attribute or method access.
This prevents template injection attacks via methods like parse_raw().
"""
def is_safe_attribute(self, _obj: Any, _attr: str, _value: Any) -> bool:
"""Block ALL attribute access for security.
Only allow accessing variables directly from the context dict,
no attribute access on those objects.
Args:
_obj: The object being accessed (unused, always blocked).
_attr: The attribute name (unused, always blocked).
_value: The attribute value (unused, always blocked).
Returns:
False - all attribute access is blocked.
"""
# Block all attribute access
return False
def is_safe_callable(self, _obj: Any) -> bool:
"""Block all method calls for security.
Args:
_obj: The object being checked (unused, always blocked).
Returns:
False - all callables are blocked.
"""
return False
def getattr(self, obj: Any, attribute: str) -> Any:
"""Override getattr to block all attribute access.
Args:
obj: The object.
attribute: The attribute name.
Returns:
Never returns.
Raises:
SecurityError: Always, to block attribute access.
"""
msg = (
f"Access to attributes is not allowed in templates. "
f"Attempted to access '{attribute}' on {type(obj).__name__}. "
f"Use only simple variable names like {{{{variable}}}} "
f"without dots or methods."
)
raise SecurityError(msg)
_HAS_JINJA2 = True
except ImportError:
_HAS_JINJA2 = False
@@ -116,10 +58,14 @@ def jinja2_formatter(template: str, /, **kwargs: Any) -> str:
)
raise ImportError(msg)
# Use a restricted sandbox that blocks ALL attribute/method access
# Only simple variable lookups like {{variable}} are allowed
# Attribute access like {{variable.attr}} or {{variable.method()}} is blocked
return _RestrictedSandboxedEnvironment().from_string(template).render(**kwargs)
# This uses a sandboxed environment to prevent arbitrary code execution.
# Jinja2 uses an opt-out rather than opt-in approach for sand-boxing.
# Please treat this sand-boxing as a best-effort approach rather than
# a guarantee of security.
# We recommend to never use jinja2 templates with untrusted inputs.
# https://jinja.palletsprojects.com/en/3.1.x/sandbox/
# approach not a guarantee of security.
return SandboxedEnvironment().from_string(template).render(**kwargs)
def validate_jinja2(template: str, input_variables: list[str]) -> None:
@@ -154,7 +100,7 @@ def _get_jinja2_variables_from_template(template: str) -> set[str]:
"Please install it with `pip install jinja2`."
)
raise ImportError(msg)
env = _RestrictedSandboxedEnvironment()
env = Environment() # noqa: S701
ast = env.parse(template)
return meta.find_undeclared_variables(ast)
@@ -202,7 +148,9 @@ def mustache_template_vars(
Defs = dict[str, "Defs"]
def mustache_schema(template: str) -> type[BaseModel]:
def mustache_schema(
template: str,
) -> type[BaseModel]:
"""Get the variables from a mustache template.
Args:
@@ -226,11 +174,6 @@ def mustache_schema(template: str) -> type[BaseModel]:
fields[prefix] = False
elif type_ in {"variable", "no escape"}:
fields[prefix + tuple(key.split("."))] = True
for fkey, fval in fields.items():
fields[fkey] = fval and not any(
is_subsequence(fkey, k) for k in fields if k != fkey
)
defs: Defs = {} # None means leaf node
while fields:
field, is_leaf = fields.popitem()
@@ -321,30 +264,6 @@ def get_template_variables(template: str, template_format: str) -> list[str]:
msg = f"Unsupported template format: {template_format}"
raise ValueError(msg)
# For f-strings, block attribute access and indexing syntax
# This prevents template injection attacks via accessing dangerous attributes
if template_format == "f-string":
for var in input_variables:
# Formatter().parse() returns field names with dots/brackets if present
# e.g., "obj.attr" or "obj[0]" - we need to block these
if "." in var or "[" in var or "]" in var:
msg = (
f"Invalid variable name {var!r} in f-string template. "
f"Variable names cannot contain attribute "
f"access (.) or indexing ([])."
)
raise ValueError(msg)
# Block variable names that are all digits (e.g., "0", "100")
# These are interpreted as positional arguments, not keyword arguments
if var.isdigit():
msg = (
f"Invalid variable name {var!r} in f-string template. "
f"Variable names cannot be all digits as they are interpreted "
f"as positional arguments."
)
raise ValueError(msg)
return sorted(input_variables)
@@ -407,12 +326,3 @@ class StringPromptTemplate(BasePromptTemplate, ABC):
def pretty_print(self) -> None:
"""Print a pretty representation of the prompt."""
print(self.pretty_repr(html=is_interactive_env())) # noqa: T201
def is_subsequence(child: Sequence, parent: Sequence) -> bool:
"""Return True if child is subsequence of parent."""
if len(child) == 0 or len(parent) == 0:
return False
if len(parent) < len(child):
return False
return all(child[i] == parent[i] for i in range(len(child)))

View File

@@ -20,7 +20,7 @@ from collections.abc import (
)
from concurrent.futures import FIRST_COMPLETED, wait
from functools import wraps
from itertools import groupby, tee
from itertools import tee
from operator import itemgetter
from types import GenericAlias
from typing import (
@@ -28,17 +28,19 @@ from typing import (
Any,
Callable,
Generic,
Literal,
Optional,
Protocol,
TypeVar,
Union,
cast,
get_args,
get_type_hints,
overload,
)
from pydantic import BaseModel, ConfigDict, Field, RootModel
from typing_extensions import Literal, get_args, override
from typing_extensions import override
from langchain_core._api import beta_decorator
from langchain_core.callbacks.manager import AsyncCallbackManager, CallbackManager
@@ -1653,7 +1655,7 @@ class Runnable(ABC, Generic[Input, Output]):
from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
llm = ChatOllama(model="llama2")
llm = ChatOllama(model="llama3.1")
# Without bind.
chain = llm | StrOutputParser()
@@ -2329,9 +2331,6 @@ class Runnable(ABC, Generic[Input, Output]):
Use this to implement ``stream`` or ``transform`` in ``Runnable`` subclasses.
"""
# Extract defers_inputs from kwargs if present
defers_inputs = kwargs.pop("defers_inputs", False)
# tee the input so we can iterate over it twice
input_for_tracing, input_for_transform = tee(inputs, 2)
# Start the input iterator to ensure the input Runnable starts before this one
@@ -2348,7 +2347,6 @@ class Runnable(ABC, Generic[Input, Output]):
run_type=run_type,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
defers_inputs=defers_inputs,
)
try:
child_config = patch_config(config, callbacks=run_manager.get_child())
@@ -2436,9 +2434,6 @@ class Runnable(ABC, Generic[Input, Output]):
Use this to implement ``astream`` or ``atransform`` in ``Runnable`` subclasses.
"""
# Extract defers_inputs from kwargs if present
defers_inputs = kwargs.pop("defers_inputs", False)
# tee the input so we can iterate over it twice
input_for_tracing, input_for_transform = atee(inputs, 2)
# Start the input iterator to ensure the input Runnable starts before this one
@@ -2455,7 +2450,6 @@ class Runnable(ABC, Generic[Input, Output]):
run_type=run_type,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
defers_inputs=defers_inputs,
)
try:
child_config = patch_config(config, callbacks=run_manager.get_child())
@@ -3076,50 +3070,10 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
"""
# Import locally to prevent circular import
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
CONTEXT_CONFIG_PREFIX,
_key_from_id,
return get_unique_config_specs(
[spec for step in self.steps for spec in step.config_specs]
)
# get all specs
all_specs = [
(spec, idx)
for idx, step in enumerate(self.steps)
for spec in step.config_specs
]
# calculate context dependencies
specs_by_pos = groupby(
[tup for tup in all_specs if tup[0].id.startswith(CONTEXT_CONFIG_PREFIX)],
itemgetter(1),
)
next_deps: set[str] = set()
deps_by_pos: dict[int, set[str]] = {}
for pos, specs in specs_by_pos:
deps_by_pos[pos] = next_deps
next_deps = next_deps | {spec[0].id for spec in specs}
# assign context dependencies
for pos, (spec, idx) in enumerate(all_specs):
if spec.id.startswith(CONTEXT_CONFIG_PREFIX):
all_specs[pos] = (
ConfigurableFieldSpec(
id=spec.id,
annotation=spec.annotation,
name=spec.name,
default=spec.default,
description=spec.description,
is_shared=spec.is_shared,
dependencies=[
d
for d in deps_by_pos[idx]
if _key_from_id(d) != _key_from_id(spec.id)
]
+ (spec.dependencies or []),
),
idx,
)
return get_unique_config_specs(spec for spec, _ in all_specs)
@override
def get_graph(self, config: Optional[RunnableConfig] = None) -> Graph:
"""Get the graph representation of the ``Runnable``.
@@ -3223,13 +3177,8 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
def invoke(
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
) -> Output:
# Import locally to prevent circular import
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
config_with_context,
)
# setup callbacks and context
config = config_with_context(ensure_config(config), self.steps)
config = ensure_config(config)
callback_manager = get_callback_manager_for_config(config)
# start the root run
run_manager = callback_manager.on_chain_start(
@@ -3267,13 +3216,8 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
config: Optional[RunnableConfig] = None,
**kwargs: Optional[Any],
) -> Output:
# Import locally to prevent circular import
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
aconfig_with_context,
)
# setup callbacks and context
config = aconfig_with_context(ensure_config(config), self.steps)
config = ensure_config(config)
callback_manager = get_async_callback_manager_for_config(config)
# start the root run
run_manager = await callback_manager.on_chain_start(
@@ -3314,19 +3258,11 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
return_exceptions: bool = False,
**kwargs: Optional[Any],
) -> list[Output]:
# Import locally to prevent circular import
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
config_with_context,
)
if not inputs:
return []
# setup callbacks and context
configs = [
config_with_context(c, self.steps)
for c in get_config_list(config, len(inputs))
]
configs = get_config_list(config, len(inputs))
callback_managers = [
CallbackManager.configure(
inheritable_callbacks=config.get("callbacks"),
@@ -3446,19 +3382,11 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
return_exceptions: bool = False,
**kwargs: Optional[Any],
) -> list[Output]:
# Import locally to prevent circular import
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
aconfig_with_context,
)
if not inputs:
return []
# setup callbacks and context
configs = [
aconfig_with_context(c, self.steps)
for c in get_config_list(config, len(inputs))
]
configs = get_config_list(config, len(inputs))
callback_managers = [
AsyncCallbackManager.configure(
inheritable_callbacks=config.get("callbacks"),
@@ -3579,14 +3507,7 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
config: RunnableConfig,
**kwargs: Any,
) -> Iterator[Output]:
# Import locally to prevent circular import
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
config_with_context,
)
steps = [self.first, *self.middle, self.last]
config = config_with_context(config, self.steps)
# transform the input stream of each step with the next
# steps that don't natively support transforming an input stream will
# buffer input in memory until all available, and then start emitting output
@@ -3609,14 +3530,7 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
config: RunnableConfig,
**kwargs: Any,
) -> AsyncIterator[Output]:
# Import locally to prevent circular import
from langchain_core.beta.runnables.context import ( # noqa: PLC0415
aconfig_with_context,
)
steps = [self.first, *self.middle, self.last]
config = aconfig_with_context(config, self.steps)
# stream the last steps
# transform the input stream of each step with the next
# steps that don't natively support transforming an input stream will
@@ -4470,7 +4384,6 @@ class RunnableGenerator(Runnable[Input, Output]):
input,
self._transform, # type: ignore[arg-type]
config,
defers_inputs=True,
**kwargs,
)
@@ -4504,7 +4417,7 @@ class RunnableGenerator(Runnable[Input, Output]):
raise NotImplementedError(msg)
return self._atransform_stream_with_config(
input, self._atransform, config, defers_inputs=True, **kwargs
input, self._atransform, config, **kwargs
)
@override

View File

@@ -12,10 +12,6 @@ from typing import (
from pydantic import BaseModel, ConfigDict
from typing_extensions import override
from langchain_core.beta.runnables.context import (
CONTEXT_CONFIG_PREFIX,
CONTEXT_CONFIG_SUFFIX_SET,
)
from langchain_core.runnables.base import (
Runnable,
RunnableLike,
@@ -181,7 +177,7 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
@property
@override
def config_specs(self) -> list[ConfigurableFieldSpec]:
specs = get_unique_config_specs(
return get_unique_config_specs(
spec
for step in (
[self.default]
@@ -190,14 +186,6 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
)
for spec in step.config_specs
)
if any(
s.id.startswith(CONTEXT_CONFIG_PREFIX)
and s.id.endswith(CONTEXT_CONFIG_SUFFIX_SET)
for s in specs
):
msg = "RunnableBranch cannot contain context setters."
raise ValueError(msg)
return specs
@override
def invoke(

View File

@@ -10,10 +10,19 @@ from concurrent.futures import Executor, Future, ThreadPoolExecutor
from contextlib import contextmanager
from contextvars import Context, ContextVar, Token, copy_context
from functools import partial
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, cast
from typing import (
TYPE_CHECKING,
Any,
Callable,
Optional,
ParamSpec,
TypeVar,
Union,
cast,
)
from langsmith.run_helpers import _set_tracing_context, get_tracing_context
from typing_extensions import ParamSpec, TypedDict
from typing_extensions import TypedDict
from langchain_core.callbacks.manager import AsyncCallbackManager, CallbackManager
from langchain_core.runnables.utils import (

View File

@@ -541,7 +541,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
hist: BaseChatMessageHistory = config["configurable"]["message_history"]
# Get the input messages
inputs = load(run.inputs, allowed_objects="all")
inputs = load(run.inputs)
input_messages = self._get_input_messages(inputs)
# If historic messages were prepended to the input messages, remove them to
# avoid adding duplicate messages to history.
@@ -550,7 +550,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
input_messages = input_messages[len(historic_messages) :]
# Get the output messages
output_val = load(run.outputs, allowed_objects="all")
output_val = load(run.outputs)
output_messages = self._get_output_messages(output_val)
hist.add_messages(input_messages + output_messages)
@@ -558,7 +558,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
hist: BaseChatMessageHistory = config["configurable"]["message_history"]
# Get the input messages
inputs = load(run.inputs, allowed_objects="all")
inputs = load(run.inputs)
input_messages = self._get_input_messages(inputs)
# If historic messages were prepended to the input messages, remove them to
# avoid adding duplicate messages to history.
@@ -567,7 +567,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
input_messages = input_messages[len(historic_messages) :]
# Get the output messages
output_val = load(run.outputs, allowed_objects="all")
output_val = load(run.outputs)
output_messages = self._get_output_messages(output_val)
await hist.aadd_messages(input_messages + output_messages)

View File

@@ -23,6 +23,13 @@ class EventData(TypedDict, total=False):
won't be known until the *END* of the Runnable when it has finished streaming
its inputs.
"""
error: NotRequired[BaseException]
"""The error that occurred during the execution of the Runnable.
This field is only available if the Runnable raised an exception.
.. versionadded:: 1.0.0
"""
output: Any
"""The output of the Runnable that generated the event.

View File

@@ -18,11 +18,12 @@ from typing import (
NamedTuple,
Optional,
Protocol,
TypeGuard,
TypeVar,
Union,
)
from typing_extensions import TypeGuard, override
from typing_extensions import override
# Re-export create-model for backwards compatibility
from langchain_core.utils.pydantic import create_model # noqa: F401

View File

@@ -1272,7 +1272,7 @@ class InjectedToolCallId(InjectedToolArg):
.. code-block:: python
from typing_extensions import Annotated
from typing import Annotated
from langchain_core.messages import ToolMessage
from langchain_core.tools import tool, InjectedToolCallId

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
import sys
import traceback
from abc import ABC, abstractmethod
from datetime import datetime, timezone
@@ -98,12 +97,7 @@ class _TracerCore(ABC):
"""Get the stacktrace of the parent error."""
msg = repr(error)
try:
if sys.version_info < (3, 10):
tb = traceback.format_exception(
error.__class__, error, error.__traceback__
)
else:
tb = traceback.format_exception(error)
tb = traceback.format_exception(error)
return (msg + "\n\n".join(tb)).strip()
except: # noqa: E722
return msg

View File

@@ -14,7 +14,7 @@ from typing import (
Union,
cast,
)
from uuid import UUID
from uuid import UUID, uuid4
from typing_extensions import NotRequired, override
@@ -45,7 +45,6 @@ from langchain_core.tracers.log_stream import (
)
from langchain_core.tracers.memory_stream import _MemoryStream
from langchain_core.utils.aiter import aclosing, py_anext
from langchain_core.utils.uuid import uuid7
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator, Sequence
@@ -611,6 +610,28 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
run_type,
)
def _get_tool_run_info_with_inputs(self, run_id: UUID) -> tuple[RunInfo, Any]:
"""Get run info for a tool and extract inputs, with validation.
Args:
run_id: The run ID of the tool.
Returns:
A tuple of (run_info, inputs).
Raises:
AssertionError: If the run ID is a tool call and does not have inputs.
"""
run_info = self.run_map.pop(run_id)
if "inputs" not in run_info:
msg = (
f"Run ID {run_id} is a tool call and is expected to have "
f"inputs associated with it."
)
raise AssertionError(msg)
inputs = run_info["inputs"]
return run_info, inputs
@override
async def on_tool_start(
self,
@@ -653,6 +674,35 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
"tool",
)
@override
async def on_tool_error(
self,
error: BaseException,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[list[str]] = None,
**kwargs: Any,
) -> None:
"""Run when tool errors."""
run_info, inputs = self._get_tool_run_info_with_inputs(run_id)
self._send(
{
"event": "on_tool_error",
"data": {
"error": error,
"input": inputs,
},
"run_id": str(run_id),
"name": run_info["name"],
"tags": run_info["tags"],
"metadata": run_info["metadata"],
"parent_ids": self._get_parent_ids(run_id),
},
"tool",
)
@override
async def on_tool_end(self, output: Any, *, run_id: UUID, **kwargs: Any) -> None:
"""End a trace for a tool run.
@@ -660,14 +710,7 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
Raises:
AssertionError: If the run ID is a tool call and does not have inputs
"""
run_info = self.run_map.pop(run_id)
if "inputs" not in run_info:
msg = (
f"Run ID {run_id} is a tool call and is expected to have "
f"inputs associated with it."
)
raise AssertionError(msg)
inputs = run_info["inputs"]
run_info, inputs = self._get_tool_run_info_with_inputs(run_id)
self._send(
{
@@ -962,11 +1005,7 @@ async def _astream_events_implementation_v2(
# Assign the stream handler to the config
config = ensure_config(config)
if "run_id" in config:
run_id = cast("UUID", config["run_id"])
else:
run_id = uuid7()
config["run_id"] = run_id
run_id = cast("UUID", config.setdefault("run_id", uuid4()))
callbacks = config.get("callbacks")
if callbacks is None:
config["callbacks"] = [event_streamer]

View File

@@ -21,7 +21,6 @@ from typing_extensions import override
from langchain_core.env import get_runtime_environment
from langchain_core.load import dumpd
from langchain_core.messages.ai import UsageMetadata, add_usage
from langchain_core.tracers.base import BaseTracer
from langchain_core.tracers.schemas import Run
@@ -70,32 +69,6 @@ def _get_executor() -> ThreadPoolExecutor:
return _EXECUTOR
def _get_usage_metadata_from_generations(
generations: list[list[dict[str, Any]]],
) -> UsageMetadata | None:
"""Extract and aggregate `usage_metadata` from generations.
Iterates through generations to find and aggregate all `usage_metadata` found in
messages. This is typically present in chat model outputs.
Args:
generations: List of generation batches, where each batch is a list
of generation dicts that may contain a `'message'` key with
`'usage_metadata'`.
Returns:
The aggregated `usage_metadata` dict if found, otherwise `None`.
"""
output: UsageMetadata | None = None
for generation_batch in generations:
for generation in generation_batch:
if isinstance(generation, dict) and "message" in generation:
message = generation["message"]
if isinstance(message, dict) and "usage_metadata" in message:
output = add_usage(output, message["usage_metadata"])
return output
class LangChainTracer(BaseTracer):
"""Implementation of the SharedTracer that POSTS to the LangChain endpoint."""
@@ -293,15 +266,6 @@ class LangChainTracer(BaseTracer):
def _on_llm_end(self, run: Run) -> None:
"""Process the LLM Run."""
# Extract usage_metadata from outputs and store in extra.metadata
if run.outputs and "generations" in run.outputs:
usage_metadata = _get_usage_metadata_from_generations(
run.outputs["generations"]
)
if usage_metadata is not None:
if "metadata" not in run.extra:
run.extra["metadata"] = {}
run.extra["metadata"]["usage_metadata"] = usage_metadata
self._update_run_single(run)
def _on_llm_error(self, run: Run) -> None:
@@ -312,28 +276,15 @@ class LangChainTracer(BaseTracer):
"""Process the Chain Run upon start."""
if run.parent_run_id is None:
run.reference_example_id = self.example_id
# Skip persisting if inputs are deferred (e.g., iterator/generator inputs).
# The run will be posted when _on_chain_end is called with realized inputs.
if not run.extra.get("defers_inputs"):
self._persist_run_single(run)
self._persist_run_single(run)
def _on_chain_end(self, run: Run) -> None:
"""Process the Chain Run."""
# If inputs were deferred, persist (POST) the run now that inputs are realized.
# Otherwise, update (PATCH) the existing run.
if run.extra.get("defers_inputs"):
self._persist_run_single(run)
else:
self._update_run_single(run)
self._update_run_single(run)
def _on_chain_error(self, run: Run) -> None:
"""Process the Chain Run upon error."""
# If inputs were deferred, persist (POST) the run now that inputs are realized.
# Otherwise, update (PATCH) the existing run.
if run.extra.get("defers_inputs"):
self._persist_run_single(run)
else:
self._update_run_single(run)
self._update_run_single(run)
def _on_tool_start(self, run: Run) -> None:
"""Process the Tool Run upon start."""

View File

@@ -563,7 +563,7 @@ def _get_standardized_inputs(
)
raise NotImplementedError(msg)
inputs = load(run.inputs, allowed_objects="all")
inputs = load(run.inputs)
if run.run_type in {"retriever", "llm", "chat_model"}:
return inputs
@@ -595,7 +595,7 @@ def _get_standardized_outputs(
Returns:
An output if returned, otherwise a None
"""
outputs = load(run.outputs, allowed_objects="all")
outputs = load(run.outputs)
if schema_format == "original":
if run.run_type == "prompt" and "output" in outputs:
# These were previously dumped before the tracer.

View File

@@ -57,6 +57,11 @@ def merge_dicts(left: dict[str, Any], *others: dict[str, Any]) -> dict[str, Any]
# "should either occur once or have the same value across "
# "all dicts."
# )
if (right_k == "index" and merged[right_k].startswith("lc_")) or (
right_k in ("id", "output_version", "model_provider")
and merged[right_k] == right_v
):
continue
merged[right_k] += right_v
elif isinstance(merged[right_k], dict):
merged[right_k] = merge_dicts(merged[right_k], right_v)
@@ -93,7 +98,16 @@ def merge_lists(left: Optional[list], *others: Optional[list]) -> Optional[list]
merged = other.copy()
else:
for e in other:
if isinstance(e, dict) and "index" in e and isinstance(e["index"], int):
if (
isinstance(e, dict)
and "index" in e
and (
isinstance(e["index"], int)
or (
isinstance(e["index"], str) and e["index"].startswith("lc_")
)
)
):
to_merge = [
i
for i, e_left in enumerate(merged)
@@ -102,11 +116,35 @@ def merge_lists(left: Optional[list], *others: Optional[list]) -> Optional[list]
if to_merge:
# TODO: Remove this once merge_dict is updated with special
# handling for 'type'.
new_e = (
{k: v for k, v in e.items() if k != "type"}
if "type" in e
else e
)
if (left_type := merged[to_merge[0]].get("type")) and (
e.get("type") == "non_standard" and "value" in e
):
if left_type != "non_standard":
# standard + non_standard
new_e: dict[str, Any] = {
"extras": {
k: v
for k, v in e["value"].items()
if k != "type"
}
}
else:
# non_standard + non_standard
new_e = {
"value": {
k: v
for k, v in e["value"].items()
if k != "type"
}
}
if "index" in e:
new_e["index"] = e["index"]
else:
new_e = (
{k: v for k, v in e.items() if k != "type"}
if "type" in e
else e
)
merged[to_merge[0]] = merge_dicts(merged[to_merge[0]], new_e)
else:
merged.append(e)

View File

@@ -17,12 +17,15 @@ from typing import (
Optional,
Union,
cast,
get_args,
get_origin,
)
from pydantic import BaseModel
from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import Field, create_model
from typing_extensions import TypedDict, get_args, get_origin, is_typeddict
from pydantic.v1 import Field as Field_v1
from pydantic.v1 import create_model as create_model_v1
from typing_extensions import TypedDict, is_typeddict
import langchain_core
from langchain_core._api import beta, deprecated
@@ -294,7 +297,7 @@ def _convert_any_typed_dicts_to_pydantic(
raise ValueError(msg)
if arg_desc := arg_descriptions.get(arg):
field_kwargs["description"] = arg_desc
fields[arg] = (new_arg_type, Field(**field_kwargs))
fields[arg] = (new_arg_type, Field_v1(**field_kwargs))
else:
new_arg_type = _convert_any_typed_dicts_to_pydantic(
arg_type, depth=depth + 1, visited=visited
@@ -302,8 +305,8 @@ def _convert_any_typed_dicts_to_pydantic(
field_kwargs = {"default": ...}
if arg_desc := arg_descriptions.get(arg):
field_kwargs["description"] = arg_desc
fields[arg] = (new_arg_type, Field(**field_kwargs))
model = create_model(typed_dict.__name__, **fields)
fields[arg] = (new_arg_type, Field_v1(**field_kwargs))
model = create_model_v1(typed_dict.__name__, **fields)
model.__doc__ = description
visited[typed_dict] = model
return model

View File

@@ -8,14 +8,13 @@ from types import TracebackType
from typing import (
Any,
Generic,
Literal,
Optional,
TypeVar,
Union,
overload,
)
from typing_extensions import Literal
T = TypeVar("T")

View File

@@ -18,7 +18,7 @@ from typing import (
)
if TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias
logger = logging.getLogger(__name__)
@@ -376,29 +376,15 @@ def _get_key(
if resolved_scope in (0, False):
return resolved_scope
# Move into the scope
if isinstance(resolved_scope, dict):
try:
# Try subscripting (Normal dictionaries)
resolved_scope = cast("dict[str, Any]", resolved_scope)[child]
except (TypeError, AttributeError):
try:
resolved_scope = resolved_scope[child]
except (KeyError, TypeError):
# Key not found - will be caught by outer try-except
msg = f"Key {child!r} not found in dict"
raise KeyError(msg) from None
elif isinstance(resolved_scope, (list, tuple)):
try:
resolved_scope = resolved_scope[int(child)]
except (ValueError, IndexError, TypeError):
# Invalid index - will be caught by outer try-except
msg = f"Invalid index {child!r} for list/tuple"
raise IndexError(msg) from None
else:
# Reject everything else for security
# This prevents traversing into arbitrary Python objects
msg = (
f"Cannot traverse into {type(resolved_scope).__name__}. "
"Mustache templates only support dict, list, and tuple. "
f"Got: {type(resolved_scope)}"
)
raise TypeError(msg) # noqa: TRY301
resolved_scope = getattr(resolved_scope, child)
except (TypeError, AttributeError):
# Try as a list
resolved_scope = resolved_scope[int(child)] # type: ignore[index]
try:
# This allows for custom falsy data types
@@ -409,9 +395,8 @@ def _get_key(
if resolved_scope in (0, False):
return resolved_scope
return resolved_scope or ""
except (AttributeError, KeyError, IndexError, ValueError, TypeError):
except (AttributeError, KeyError, IndexError, ValueError):
# We couldn't find the key in the current scope
# TypeError: Attempted to traverse into non-dict/list type
# We'll try again on the next pass
pass

View File

@@ -9,6 +9,7 @@ import warnings
from collections.abc import Iterator, Sequence
from importlib.metadata import version
from typing import Any, Callable, Optional, Union, overload
from uuid import uuid4
from packaging.version import parse
from pydantic import SecretStr
@@ -482,3 +483,31 @@ def secret_from_env(
raise ValueError(msg)
return get_secret_from_env
LC_AUTO_PREFIX = "lc_"
"""LangChain auto-generated ID prefix for messages and content blocks."""
LC_ID_PREFIX = "lc_run-"
"""Internal tracing/callback system identifier.
Used for:
- Tracing. Every LangChain operation (LLM call, chain execution, tool use, etc.)
gets a unique run_id (UUID)
- Enables tracking parent-child relationships between operations
"""
def ensure_id(id_val: Optional[str]) -> str:
"""Ensure the ID is a valid string, generating a new UUID if not provided.
Auto-generated UUIDs are prefixed by ``'lc_'`` to indicate they are
LangChain-generated IDs.
Args:
id_val: Optional string ID value to validate.
Returns:
A string ID, either the validated provided value or a newly generated UUID4.
"""
return id_val or str(f"{LC_AUTO_PREFIX}{uuid4()}")

View File

@@ -1,54 +0,0 @@
"""UUID utility functions.
This module exports a uuid7 function to generate monotonic, time-ordered UUIDs
for tracing and similar operations.
"""
from __future__ import annotations
import typing
from uuid import UUID
from uuid_utils.compat import uuid7 as _uuid_utils_uuid7
if typing.TYPE_CHECKING:
from uuid import UUID
_NANOS_PER_SECOND: typing.Final = 1_000_000_000
def _to_timestamp_and_nanos(nanoseconds: int) -> tuple[int, int]:
"""Split a nanosecond timestamp into seconds and remaining nanoseconds."""
seconds, nanos = divmod(nanoseconds, _NANOS_PER_SECOND)
return seconds, nanos
def uuid7(nanoseconds: int | None = None) -> UUID:
"""Generate a UUID from a Unix timestamp in nanoseconds and random bits.
UUIDv7 objects feature monotonicity within a millisecond.
Args:
nanoseconds: Optional ns timestamp. If not provided, uses current time.
"""
# --- 48 --- -- 4 -- --- 12 --- -- 2 -- --- 30 --- - 32 -
# unix_ts_ms | version | counter_hi | variant | counter_lo | random
#
# 'counter = counter_hi | counter_lo' is a 42-bit counter constructed
# with Method 1 of RFC 9562, §6.2, and its MSB is set to 0.
#
# 'random' is a 32-bit random value regenerated for every new UUID.
#
# If multiple UUIDs are generated within the same millisecond, the LSB
# of 'counter' is incremented by 1. When overflowing, the timestamp is
# advanced and the counter is reset to a random 42-bit integer with MSB
# set to 0.
# For now, just delegate to the uuid_utils implementation
if nanoseconds is None:
return _uuid_utils_uuid7()
seconds, nanos = _to_timestamp_and_nanos(nanoseconds)
return _uuid_utils_uuid7(timestamp=seconds, nanos=nanos)
__all__ = ["uuid7"]

View File

@@ -604,7 +604,7 @@ class InMemoryVectorStore(VectorStore):
"""
path_: Path = Path(path)
with path_.open("r", encoding="utf-8") as f:
store = load(json.load(f), allowed_objects=[Document])
store = load(json.load(f))
vectorstore = cls(embedding=embedding, **kwargs)
vectorstore.store = store
return vectorstore
@@ -617,5 +617,5 @@ class InMemoryVectorStore(VectorStore):
"""
path_: Path = Path(path)
path_.parent.mkdir(exist_ok=True, parents=True)
with path_.open("w") as f:
with path_.open("w", encoding="utf-8") as f:
json.dump(dumpd(self.store), f, indent=2)

View File

@@ -1,3 +1,3 @@
"""langchain-core version information and utilities."""
VERSION = "0.3.83"
VERSION = "1.0.0a6"

View File

@@ -5,7 +5,7 @@ build-backend = "pdm.backend"
[project]
authors = []
license = {text = "MIT"}
requires-python = ">=3.9.0,<4.0.0"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"langsmith>=0.3.45,<1.0.0",
"tenacity!=8.4.0,>=8.1.0,<10.0.0",
@@ -14,10 +14,9 @@ dependencies = [
"typing-extensions>=4.7.0,<5.0.0",
"packaging>=23.2.0,<26.0.0",
"pydantic>=2.7.4,<3.0.0",
"uuid-utils>=0.12.0,<1.0",
]
name = "langchain-core"
version = "0.3.83"
version = "1.0.0a6"
description = "Building applications with LLMs through composability"
readme = "README.md"

View File

@@ -2,7 +2,7 @@
import time
from itertools import cycle
from typing import Any, Optional, Union
from typing import Any, Optional, Union, cast
from uuid import UUID
from typing_extensions import override
@@ -59,7 +59,7 @@ async def test_generic_fake_chat_model_stream() -> None:
assert chunks == [
_any_id_ai_message_chunk(content="hello"),
_any_id_ai_message_chunk(content=" "),
_any_id_ai_message_chunk(content="goodbye"),
_any_id_ai_message_chunk(content="goodbye", chunk_position="last"),
]
assert len({chunk.id for chunk in chunks}) == 1
@@ -67,7 +67,7 @@ async def test_generic_fake_chat_model_stream() -> None:
assert chunks == [
_any_id_ai_message_chunk(content="hello"),
_any_id_ai_message_chunk(content=" "),
_any_id_ai_message_chunk(content="goodbye"),
_any_id_ai_message_chunk(content="goodbye", chunk_position="last"),
]
assert len({chunk.id for chunk in chunks}) == 1
@@ -79,6 +79,7 @@ async def test_generic_fake_chat_model_stream() -> None:
assert chunks == [
_any_id_ai_message_chunk(content="", additional_kwargs={"foo": 42}),
_any_id_ai_message_chunk(content="", additional_kwargs={"bar": 24}),
_any_id_ai_message_chunk(content="", chunk_position="last"),
]
assert len({chunk.id for chunk in chunks}) == 1
@@ -97,7 +98,8 @@ async def test_generic_fake_chat_model_stream() -> None:
assert chunks == [
_any_id_ai_message_chunk(
content="", additional_kwargs={"function_call": {"name": "move_file"}}
content="",
additional_kwargs={"function_call": {"name": "move_file"}},
),
_any_id_ai_message_chunk(
content="",
@@ -114,6 +116,7 @@ async def test_generic_fake_chat_model_stream() -> None:
"function_call": {"arguments": '\n "destination_path": "bar"\n}'},
},
),
_any_id_ai_message_chunk(content="", chunk_position="last"),
]
assert len({chunk.id for chunk in chunks}) == 1
@@ -134,6 +137,7 @@ async def test_generic_fake_chat_model_stream() -> None:
}
},
id=chunks[0].id,
chunk_position="last",
)
@@ -148,7 +152,7 @@ async def test_generic_fake_chat_model_astream_log() -> None:
assert final.state["streamed_output"] == [
_any_id_ai_message_chunk(content="hello"),
_any_id_ai_message_chunk(content=" "),
_any_id_ai_message_chunk(content="goodbye"),
_any_id_ai_message_chunk(content="goodbye", chunk_position="last"),
]
assert len({chunk.id for chunk in final.state["streamed_output"]}) == 1
@@ -205,7 +209,7 @@ async def test_callback_handlers() -> None:
assert results == [
_any_id_ai_message_chunk(content="hello"),
_any_id_ai_message_chunk(content=" "),
_any_id_ai_message_chunk(content="goodbye"),
_any_id_ai_message_chunk(content="goodbye", chunk_position="last"),
]
assert tokens == ["hello", " ", "goodbye"]
assert len({chunk.id for chunk in results}) == 1
@@ -214,7 +218,9 @@ async def test_callback_handlers() -> None:
def test_chat_model_inputs() -> None:
fake = ParrotFakeChatModel()
assert fake.invoke("hello") == _any_id_human_message(content="hello")
assert cast("HumanMessage", fake.invoke("hello")) == _any_id_human_message(
content="hello"
)
assert fake.invoke([("ai", "blah")]) == _any_id_ai_message(content="blah")
assert fake.invoke([AIMessage(content="blah")]) == _any_id_ai_message(
content="blah"

View File

@@ -1,6 +1,7 @@
"""Test base chat model."""
import uuid
import warnings
from collections.abc import AsyncIterator, Iterator
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
@@ -14,11 +15,15 @@ from langchain_core.language_models import (
ParrotFakeChatModel,
)
from langchain_core.language_models._utils import _normalize_messages
from langchain_core.language_models.fake_chat_models import FakeListChatModelError
from langchain_core.language_models.fake_chat_models import (
FakeListChatModelError,
GenericFakeChatModel,
)
from langchain_core.messages import (
AIMessage,
AIMessageChunk,
BaseMessage,
BaseMessageChunk,
HumanMessage,
SystemMessage,
)
@@ -40,6 +45,37 @@ if TYPE_CHECKING:
from langchain_core.outputs.llm_result import LLMResult
def _content_blocks_equal_ignore_id(
actual: Union[str, list[Any]], expected: Union[str, list[Any]]
) -> bool:
"""Compare content blocks, ignoring auto-generated `id` fields.
Args:
actual: Actual content from response (string or list of content blocks).
expected: Expected content to compare against (string or list of blocks).
Returns:
True if content matches (excluding `id` fields), False otherwise.
"""
if isinstance(actual, str) or isinstance(expected, str):
return actual == expected
if len(actual) != len(expected):
return False
for actual_block, expected_block in zip(actual, expected):
actual_without_id = (
{k: v for k, v in actual_block.items() if k != "id"}
if isinstance(actual_block, dict) and "id" in actual_block
else actual_block
)
if actual_without_id != expected_block:
return False
return True
@pytest.fixture
def messages() -> list:
return [
@@ -141,7 +177,7 @@ async def test_stream_error_callback() -> None:
async def test_astream_fallback_to_ainvoke() -> None:
"""Test astream uses appropriate implementation."""
"""Test `astream()` uses appropriate implementation."""
class ModelWithGenerate(BaseChatModel):
@override
@@ -168,10 +204,10 @@ async def test_astream_fallback_to_ainvoke() -> None:
# is not strictly correct.
# LangChain documents a pattern of adding BaseMessageChunks to accumulate a stream.
# This may be better done with `reduce(operator.add, chunks)`.
assert chunks == [_any_id_ai_message(content="hello")] # type: ignore[comparison-overlap]
assert chunks == [_any_id_ai_message(content="hello")]
chunks = [chunk async for chunk in model.astream("anything")]
assert chunks == [_any_id_ai_message(content="hello")] # type: ignore[comparison-overlap]
assert chunks == [_any_id_ai_message(content="hello")]
async def test_astream_implementation_fallback_to_stream() -> None:
@@ -198,7 +234,9 @@ async def test_astream_implementation_fallback_to_stream() -> None:
) -> Iterator[ChatGenerationChunk]:
"""Stream the output of the model."""
yield ChatGenerationChunk(message=AIMessageChunk(content="a"))
yield ChatGenerationChunk(message=AIMessageChunk(content="b"))
yield ChatGenerationChunk(
message=AIMessageChunk(content="b", chunk_position="last")
)
@property
def _llm_type(self) -> str:
@@ -207,15 +245,19 @@ async def test_astream_implementation_fallback_to_stream() -> None:
model = ModelWithSyncStream()
chunks = list(model.stream("anything"))
assert chunks == [
_any_id_ai_message_chunk(content="a"),
_any_id_ai_message_chunk(content="b"),
_any_id_ai_message_chunk(
content="a",
),
_any_id_ai_message_chunk(content="b", chunk_position="last"),
]
assert len({chunk.id for chunk in chunks}) == 1
assert type(model)._astream == BaseChatModel._astream
astream_chunks = [chunk async for chunk in model.astream("anything")]
assert astream_chunks == [
_any_id_ai_message_chunk(content="a"),
_any_id_ai_message_chunk(content="b"),
_any_id_ai_message_chunk(
content="a",
),
_any_id_ai_message_chunk(content="b", chunk_position="last"),
]
assert len({chunk.id for chunk in astream_chunks}) == 1
@@ -244,7 +286,9 @@ async def test_astream_implementation_uses_astream() -> None:
) -> AsyncIterator[ChatGenerationChunk]:
"""Stream the output of the model."""
yield ChatGenerationChunk(message=AIMessageChunk(content="a"))
yield ChatGenerationChunk(message=AIMessageChunk(content="b"))
yield ChatGenerationChunk(
message=AIMessageChunk(content="b", chunk_position="last")
)
@property
def _llm_type(self) -> str:
@@ -253,8 +297,10 @@ async def test_astream_implementation_uses_astream() -> None:
model = ModelWithAsyncStream()
chunks = [chunk async for chunk in model.astream("anything")]
assert chunks == [
_any_id_ai_message_chunk(content="a"),
_any_id_ai_message_chunk(content="b"),
_any_id_ai_message_chunk(
content="a",
),
_any_id_ai_message_chunk(content="b", chunk_position="last"),
]
assert len({chunk.id for chunk in chunks}) == 1
@@ -427,11 +473,12 @@ class FakeChatModelStartTracer(FakeTracer):
def test_trace_images_in_openai_format() -> None:
"""Test that images are traced in OpenAI format."""
"""Test that images are traced in OpenAI Chat Completions format."""
llm = ParrotFakeChatModel()
messages = [
{
"role": "user",
# v0 format
"content": [
{
"type": "image",
@@ -442,7 +489,7 @@ def test_trace_images_in_openai_format() -> None:
}
]
tracer = FakeChatModelStartTracer()
response = llm.invoke(messages, config={"callbacks": [tracer]})
llm.invoke(messages, config={"callbacks": [tracer]})
assert tracer.messages == [
[
[
@@ -457,19 +504,90 @@ def test_trace_images_in_openai_format() -> None:
]
]
]
# Test no mutation
assert response.content == [
def test_trace_pdfs() -> None:
# For backward compat
llm = ParrotFakeChatModel()
messages = [
{
"type": "image",
"source_type": "url",
"url": "https://example.com/image.png",
"role": "user",
"content": [
{
"type": "file",
"mime_type": "application/pdf",
"base64": "<base64 string>",
}
],
}
]
tracer = FakeChatModelStartTracer()
with warnings.catch_warnings():
warnings.simplefilter("error")
llm.invoke(messages, config={"callbacks": [tracer]})
assert tracer.messages == [
[
[
HumanMessage(
content=[
{
"type": "file",
"mime_type": "application/pdf",
"source_type": "base64",
"data": "<base64 string>",
}
]
)
]
]
]
def test_trace_content_blocks_with_no_type_key() -> None:
"""Test that we add a ``type`` key to certain content blocks that don't have one."""
llm = ParrotFakeChatModel()
def test_content_block_transformation_v0_to_v1_image() -> None:
"""Test that v0 format image content blocks are transformed to v1 format."""
# Create a message with v0 format image content
image_message = AIMessage(
content=[
{
"type": "image",
"source_type": "url",
"url": "https://example.com/image.png",
}
]
)
llm = GenericFakeChatModel(messages=iter([image_message]), output_version="v1")
response = llm.invoke("test")
# With v1 output_version, .content should be transformed
# Check structure, ignoring auto-generated IDs
assert len(response.content) == 1
content_block = response.content[0]
if isinstance(content_block, dict) and "id" in content_block:
# Remove auto-generated id for comparison
content_without_id = {k: v for k, v in content_block.items() if k != "id"}
expected_content = {
"type": "image",
"url": "https://example.com/image.png",
}
assert content_without_id == expected_content
else:
assert content_block == {
"type": "image",
"url": "https://example.com/image.png",
}
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_trace_content_blocks_with_no_type_key(output_version: str) -> None:
"""Test behavior of content blocks that don't have a `type` key.
Only for blocks with one key, in which case, the name of the key is used as `type`.
"""
llm = ParrotFakeChatModel(output_version=output_version)
messages = [
{
"role": "user",
@@ -504,155 +622,381 @@ def test_trace_content_blocks_with_no_type_key() -> None:
]
]
]
# Test no mutation
assert response.content == [
if output_version == "v0":
assert response.content == [
{
"type": "text",
"text": "Hello",
},
{
"cachePoint": {"type": "default"},
},
]
else:
assert response.content == [
{
"type": "text",
"text": "Hello",
},
{
"type": "non_standard",
"value": {
"cachePoint": {"type": "default"},
},
},
]
assert response.content_blocks == [
{
"type": "text",
"text": "Hello",
},
{
"cachePoint": {"type": "default"},
"type": "non_standard",
"value": {
"cachePoint": {"type": "default"},
},
},
]
def test_extend_support_to_openai_multimodal_formats() -> None:
"""Test that chat models normalize OpenAI file and audio inputs."""
llm = ParrotFakeChatModel()
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Hello"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/image.png"},
"""Test normalizing OpenAI audio, image, and file inputs to v1."""
# Audio and file only (chat model default)
messages = HumanMessage(
content=[
{"type": "text", "text": "Hello"},
{ # audio-base64
"type": "input_audio",
"input_audio": {
"format": "wav",
"data": "<base64 string>",
},
{
"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
},
{ # file-base64
"type": "file",
"file": {
"filename": "draconomicon.pdf",
"file_data": "data:application/pdf;base64,<base64 string>",
},
{
"type": "file",
"file": {
"filename": "draconomicon.pdf",
"file_data": "data:application/pdf;base64,<base64 string>",
},
},
{
"type": "file",
"file": {
"file_data": "data:application/pdf;base64,<base64 string>",
},
},
{
"type": "file",
"file": {"file_id": "<file id>"},
},
{
"type": "input_audio",
"input_audio": {"data": "<base64 data>", "format": "wav"},
},
],
},
]
expected_content = [
{"type": "text", "text": "Hello"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/image.png"},
},
{
"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
},
{
"type": "file",
"source_type": "base64",
"data": "<base64 string>",
"mime_type": "application/pdf",
"filename": "draconomicon.pdf",
},
{
"type": "file",
"source_type": "base64",
"data": "<base64 string>",
"mime_type": "application/pdf",
},
{
"type": "file",
"file": {"file_id": "<file id>"},
},
{
"type": "audio",
"source_type": "base64",
"data": "<base64 data>",
"mime_type": "audio/wav",
},
]
response = llm.invoke(messages)
assert response.content == expected_content
},
{ # file-id
"type": "file",
"file": {"file_id": "<file id>"},
},
]
)
# Test no mutation
assert messages[0]["content"] == [
{"type": "text", "text": "Hello"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/image.png"},
},
{
"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
},
{
"type": "file",
"file": {
"filename": "draconomicon.pdf",
"file_data": "data:application/pdf;base64,<base64 string>",
expected_content_messages = HumanMessage(
content=[
{"type": "text", "text": "Hello"}, # TextContentBlock
{ # AudioContentBlock
"type": "audio",
"base64": "<base64 string>",
"mime_type": "audio/wav",
},
},
{
"type": "file",
"file": {
"file_data": "data:application/pdf;base64,<base64 string>",
{ # FileContentBlock
"type": "file",
"base64": "<base64 string>",
"mime_type": "application/pdf",
"extras": {"filename": "draconomicon.pdf"},
},
},
{
"type": "file",
"file": {"file_id": "<file id>"},
},
{
"type": "input_audio",
"input_audio": {"data": "<base64 data>", "format": "wav"},
},
]
{ # ...
"type": "file",
"file_id": "<file id>",
},
]
)
normalized_content = _normalize_messages([messages])
# Check structure, ignoring auto-generated IDs
assert len(normalized_content) == 1
normalized_message = normalized_content[0]
assert len(normalized_message.content) == len(expected_content_messages.content)
assert _content_blocks_equal_ignore_id(
normalized_message.content, expected_content_messages.content
)
messages = HumanMessage(
content=[
{"type": "text", "text": "Hello"},
{ # image-url
"type": "image_url",
"image_url": {"url": "https://example.com/image.png"},
},
{ # image-base64
"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
},
{ # audio-base64
"type": "input_audio",
"input_audio": {
"format": "wav",
"data": "<base64 string>",
},
},
{ # file-base64
"type": "file",
"file": {
"filename": "draconomicon.pdf",
"file_data": "data:application/pdf;base64,<base64 string>",
},
},
{ # file-id
"type": "file",
"file": {"file_id": "<file id>"},
},
]
)
expected_content_messages = HumanMessage(
content=[
{"type": "text", "text": "Hello"}, # TextContentBlock
{ # image-url passes through
"type": "image_url",
"image_url": {"url": "https://example.com/image.png"},
},
{ # image-url passes through with inline data
"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},
},
{ # AudioContentBlock
"type": "audio",
"base64": "<base64 string>",
"mime_type": "audio/wav",
},
{ # FileContentBlock
"type": "file",
"base64": "<base64 string>",
"mime_type": "application/pdf",
"extras": {"filename": "draconomicon.pdf"},
},
{ # ...
"type": "file",
"file_id": "<file id>",
},
]
)
normalized_content = _normalize_messages([messages])
# Check structure, ignoring auto-generated IDs
assert len(normalized_content) == 1
normalized_message = normalized_content[0]
assert len(normalized_message.content) == len(expected_content_messages.content)
assert _content_blocks_equal_ignore_id(
normalized_message.content, expected_content_messages.content
)
def test_normalize_messages_edge_cases() -> None:
# Test some blocks that should pass through
# Test behavior of malformed/unrecognized content blocks
messages = [
HumanMessage(
content=[
{
"type": "file",
"file": "uri",
"type": "input_image", # Responses API type; not handled
"image_url": "uri",
},
{
"type": "input_file",
# Standard OpenAI Chat Completions type but malformed structure
"type": "input_audio",
"input_audio": "uri", # Should be nested in `audio`
},
{
"type": "file",
"file": "uri", # `file` should be a dict for Chat Completions
},
{
"type": "input_file", # Responses API type; not handled
"file_data": "uri",
"filename": "file-name",
},
{
"type": "input_audio",
"input_audio": "uri",
},
{
"type": "input_image",
"image_url": "uri",
},
]
)
]
assert messages == _normalize_messages(messages)
def test_normalize_messages_v1_content_blocks_unchanged() -> None:
"""Test passing v1 content blocks to `_normalize_messages()` leaves unchanged."""
input_messages = [
HumanMessage(
content=[
{
"type": "text",
"text": "Hello world",
},
{
"type": "image",
"url": "https://example.com/image.png",
"mime_type": "image/png",
},
{
"type": "audio",
"base64": "base64encodedaudiodata",
"mime_type": "audio/wav",
},
{
"type": "file",
"id": "file_123",
},
{
"type": "reasoning",
"reasoning": "Let me think about this...",
},
]
)
]
result = _normalize_messages(input_messages)
# Verify the result is identical to the input (message should not be copied)
assert len(result) == 1
assert result[0] is input_messages[0]
assert result[0].content == input_messages[0].content
def test_output_version_invoke(monkeypatch: Any) -> None:
messages = [AIMessage("hello")]
llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")
response = llm.invoke("hello")
assert response.content == [{"type": "text", "text": "hello"}]
assert response.response_metadata["output_version"] == "v1"
llm = GenericFakeChatModel(messages=iter(messages))
response = llm.invoke("hello")
assert response.content == "hello"
monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")
llm = GenericFakeChatModel(messages=iter(messages))
response = llm.invoke("hello")
assert response.content == [{"type": "text", "text": "hello"}]
assert response.response_metadata["output_version"] == "v1"
# -- v1 output version tests --
async def test_output_version_ainvoke(monkeypatch: Any) -> None:
messages = [AIMessage("hello")]
# v0
llm = GenericFakeChatModel(messages=iter(messages))
response = await llm.ainvoke("hello")
assert response.content == "hello"
# v1
llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")
response = await llm.ainvoke("hello")
assert response.content == [{"type": "text", "text": "hello"}]
assert response.response_metadata["output_version"] == "v1"
# v1 from env var
monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")
llm = GenericFakeChatModel(messages=iter(messages))
response = await llm.ainvoke("hello")
assert response.content == [{"type": "text", "text": "hello"}]
assert response.response_metadata["output_version"] == "v1"
def test_output_version_stream(monkeypatch: Any) -> None:
messages = [AIMessage("foo bar")]
# v0
llm = GenericFakeChatModel(messages=iter(messages))
full = None
for chunk in llm.stream("hello"):
assert isinstance(chunk, AIMessageChunk)
assert isinstance(chunk.content, str)
assert chunk.content
full = chunk if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
assert full.content == "foo bar"
# v1
llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")
full_v1: Optional[BaseMessageChunk] = None
for chunk in llm.stream("hello"):
assert isinstance(chunk, AIMessageChunk)
assert isinstance(chunk.content, list)
assert len(chunk.content) == 1
block = chunk.content[0]
assert isinstance(block, dict)
assert block["type"] == "text"
assert block["text"]
full_v1 = chunk if full_v1 is None else full_v1 + chunk
assert isinstance(full_v1, AIMessageChunk)
assert full_v1.response_metadata["output_version"] == "v1"
# v1 from env var
monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")
llm = GenericFakeChatModel(messages=iter(messages))
full_env = None
for chunk in llm.stream("hello"):
assert isinstance(chunk, AIMessageChunk)
assert isinstance(chunk.content, list)
assert len(chunk.content) == 1
block = chunk.content[0]
assert isinstance(block, dict)
assert block["type"] == "text"
assert block["text"]
full_env = chunk if full_env is None else full_env + chunk
assert isinstance(full_env, AIMessageChunk)
assert full_env.response_metadata["output_version"] == "v1"
async def test_output_version_astream(monkeypatch: Any) -> None:
messages = [AIMessage("foo bar")]
# v0
llm = GenericFakeChatModel(messages=iter(messages))
full = None
async for chunk in llm.astream("hello"):
assert isinstance(chunk, AIMessageChunk)
assert isinstance(chunk.content, str)
assert chunk.content
full = chunk if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
assert full.content == "foo bar"
# v1
llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")
full_v1: Optional[BaseMessageChunk] = None
async for chunk in llm.astream("hello"):
assert isinstance(chunk, AIMessageChunk)
assert isinstance(chunk.content, list)
assert len(chunk.content) == 1
block = chunk.content[0]
assert isinstance(block, dict)
assert block["type"] == "text"
assert block["text"]
full_v1 = chunk if full_v1 is None else full_v1 + chunk
assert isinstance(full_v1, AIMessageChunk)
assert full_v1.response_metadata["output_version"] == "v1"
# v1 from env var
monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")
llm = GenericFakeChatModel(messages=iter(messages))
full_env = None
async for chunk in llm.astream("hello"):
assert isinstance(chunk, AIMessageChunk)
assert isinstance(chunk.content, list)
assert len(chunk.content) == 1
block = chunk.content[0]
assert isinstance(block, dict)
assert block["type"] == "text"
assert block["text"]
full_env = chunk if full_env is None else full_env + chunk
assert isinstance(full_env, AIMessageChunk)
assert full_env.response_metadata["output_version"] == "v1"
assert messages == _normalize_messages(messages)

View File

@@ -215,8 +215,8 @@ def test_rate_limit_skips_cache() -> None:
assert list(cache._cache) == [
(
'[{"lc": 1, "type": "constructor", "id": ["langchain", "schema", '
'"messages", '
'"HumanMessage"], "kwargs": {"content": "foo", "type": "human"}}]',
'"messages", "HumanMessage"], "kwargs": {"content": "foo", '
'"type": "human"}}]',
"[('_type', 'generic-fake-chat-model'), ('stop', None)]",
)
]
@@ -240,7 +240,8 @@ def test_serialization_with_rate_limiter() -> None:
assert InMemoryRateLimiter.__name__ not in serialized_model
async def test_rate_limit_skips_cache_async() -> None:
@pytest.mark.parametrize("output_version", ["v0", "v1"])
async def test_rate_limit_skips_cache_async(output_version: str) -> None:
"""Test that rate limiting does not rate limit cache look ups."""
cache = InMemoryCache()
model = GenericFakeChatModel(
@@ -249,6 +250,7 @@ async def test_rate_limit_skips_cache_async() -> None:
requests_per_second=20, check_every_n_seconds=0.1, max_bucket_size=1
),
cache=cache,
output_version=output_version,
)
tic = time.time()

View File

@@ -18,6 +18,7 @@ EXPECTED_ALL = [
"FakeStreamingListLLM",
"FakeListLLM",
"ParrotFakeChatModel",
"is_openai_data_block",
]

View File

@@ -1,13 +1,6 @@
from langchain_core.load import __all__
EXPECTED_ALL = [
"InitValidator",
"Serializable",
"dumpd",
"dumps",
"load",
"loads",
]
EXPECTED_ALL = ["dumpd", "dumps", "load", "loads", "Serializable"]
def test_all_imports() -> None:

View File

@@ -1,431 +0,0 @@
"""Tests for secret injection prevention in serialization.
Verify that user-provided data containing secret-like structures cannot be used to
extract environment variables during deserialization.
"""
import json
import os
import re
from typing import Any
from unittest import mock
import pytest
from pydantic import BaseModel
from langchain_core.documents import Document
from langchain_core.load import dumpd, dumps, load
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.outputs import ChatGeneration
SENTINEL_ENV_VAR = "TEST_SECRET_INJECTION_VAR"
"""Sentinel value that should NEVER appear in serialized output."""
SENTINEL_VALUE = "LEAKED_SECRET_MEOW_12345"
"""Sentinel value that should NEVER appear in serialized output."""
MALICIOUS_SECRET_DICT: dict[str, Any] = {
"lc": 1,
"type": "secret",
"id": [SENTINEL_ENV_VAR],
}
"""The malicious secret-like dict that tries to read the env var"""
@pytest.fixture(autouse=True)
def _set_sentinel_env_var() -> Any:
"""Set the sentinel env var for all tests in this module."""
with mock.patch.dict(os.environ, {SENTINEL_ENV_VAR: SENTINEL_VALUE}):
yield
def _assert_no_secret_leak(payload: Any) -> None:
"""Assert that serializing/deserializing payload doesn't leak the secret."""
# First serialize
serialized = dumps(payload)
# Deserialize with secrets_from_env=True (the dangerous setting)
deserialized = load(serialized, secrets_from_env=True)
# Re-serialize to string
reserialized = dumps(deserialized)
assert SENTINEL_VALUE not in reserialized, (
f"Secret was leaked! Found '{SENTINEL_VALUE}' in output.\n"
f"Original payload type: {type(payload)}\n"
f"Reserialized output: {reserialized[:500]}..."
)
assert SENTINEL_VALUE not in repr(deserialized), (
f"Secret was leaked in deserialized object! Found '{SENTINEL_VALUE}'.\n"
f"Deserialized: {deserialized!r}"
)
class TestSerializableTopLevel:
"""Tests with `Serializable` objects at the top level."""
def test_human_message_with_secret_in_content(self) -> None:
"""`HumanMessage` with secret-like dict in `content`."""
msg = HumanMessage(
content=[
{"type": "text", "text": "Hello"},
{"type": "text", "text": MALICIOUS_SECRET_DICT},
]
)
_assert_no_secret_leak(msg)
def test_human_message_with_secret_in_additional_kwargs(self) -> None:
"""`HumanMessage` with secret-like dict in `additional_kwargs`."""
msg = HumanMessage(
content="Hello",
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
)
_assert_no_secret_leak(msg)
def test_human_message_with_secret_in_nested_additional_kwargs(self) -> None:
"""`HumanMessage` with secret-like dict nested in `additional_kwargs`."""
msg = HumanMessage(
content="Hello",
additional_kwargs={"nested": {"deep": MALICIOUS_SECRET_DICT}},
)
_assert_no_secret_leak(msg)
def test_human_message_with_secret_in_list_in_additional_kwargs(self) -> None:
"""`HumanMessage` with secret-like dict in a list in `additional_kwargs`."""
msg = HumanMessage(
content="Hello",
additional_kwargs={"items": [MALICIOUS_SECRET_DICT]},
)
_assert_no_secret_leak(msg)
def test_ai_message_with_secret_in_response_metadata(self) -> None:
"""`AIMessage` with secret-like dict in respo`nse_metadata."""
msg = AIMessage(
content="Hello",
response_metadata={"data": MALICIOUS_SECRET_DICT},
)
_assert_no_secret_leak(msg)
def test_document_with_secret_in_metadata(self) -> None:
"""Document with secret-like dict in `metadata`."""
doc = Document(
page_content="Hello",
metadata={"data": MALICIOUS_SECRET_DICT},
)
_assert_no_secret_leak(doc)
def test_nested_serializable_with_secret(self) -> None:
"""`AIMessage` containing `dumpd(HumanMessage)` with secret in kwargs."""
inner = HumanMessage(
content="Hello",
additional_kwargs={"secret": MALICIOUS_SECRET_DICT},
)
outer = AIMessage(
content="Outer",
additional_kwargs={"nested": [dumpd(inner)]},
)
_assert_no_secret_leak(outer)
class TestDictTopLevel:
"""Tests with plain dicts at the top level."""
def test_dict_with_serializable_containing_secret(self) -> None:
"""Dict containing a `Serializable` with secret-like dict."""
msg = HumanMessage(
content="Hello",
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
)
payload = {"message": msg}
_assert_no_secret_leak(payload)
def test_dict_with_secret_no_serializable(self) -> None:
"""Dict with secret-like dict, no `Serializable` objects."""
payload = {"data": MALICIOUS_SECRET_DICT}
_assert_no_secret_leak(payload)
def test_dict_with_nested_secret_no_serializable(self) -> None:
"""Dict with nested secret-like dict, no `Serializable` objects."""
payload = {"outer": {"inner": MALICIOUS_SECRET_DICT}}
_assert_no_secret_leak(payload)
def test_dict_with_secret_in_list(self) -> None:
"""Dict with secret-like dict in a list."""
payload = {"items": [MALICIOUS_SECRET_DICT]}
_assert_no_secret_leak(payload)
def test_dict_mimicking_lc_constructor_with_secret(self) -> None:
"""Dict that looks like an LC constructor containing a secret."""
payload = {
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"kwargs": {
"content": "Hello",
"additional_kwargs": {"secret": MALICIOUS_SECRET_DICT},
},
}
_assert_no_secret_leak(payload)
class TestPydanticModelTopLevel:
"""Tests with Pydantic models (non-`Serializable`) at the top level."""
def test_pydantic_model_with_serializable_containing_secret(self) -> None:
"""Pydantic model containing a `Serializable` with secret-like dict."""
class MyModel(BaseModel):
message: Any
msg = HumanMessage(
content="Hello",
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
)
payload = MyModel(message=msg)
_assert_no_secret_leak(payload)
def test_pydantic_model_with_secret_dict(self) -> None:
"""Pydantic model containing a secret-like dict directly."""
class MyModel(BaseModel):
data: dict[str, Any]
payload = MyModel(data=MALICIOUS_SECRET_DICT)
_assert_no_secret_leak(payload)
# Test treatment of "parsed" in additional_kwargs
msg = AIMessage(content=[], additional_kwargs={"parsed": payload})
gen = ChatGeneration(message=msg)
_assert_no_secret_leak(gen)
round_trip = load(dumpd(gen))
assert MyModel(**(round_trip.message.additional_kwargs["parsed"])) == payload
def test_pydantic_model_with_nested_secret(self) -> None:
"""Pydantic model with nested secret-like dict."""
class MyModel(BaseModel):
nested: dict[str, Any]
payload = MyModel(nested={"inner": MALICIOUS_SECRET_DICT})
_assert_no_secret_leak(payload)
class TestNonSerializableClassTopLevel:
"""Tests with classes at the top level."""
def test_custom_class_with_serializable_containing_secret(self) -> None:
"""Custom class containing a `Serializable` with secret-like dict."""
class MyClass:
def __init__(self, message: Any) -> None:
self.message = message
msg = HumanMessage(
content="Hello",
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
)
payload = MyClass(message=msg)
# This will serialize as not_implemented, but let's verify no leak
_assert_no_secret_leak(payload)
def test_custom_class_with_secret_dict(self) -> None:
"""Custom class containing a secret-like dict directly."""
class MyClass:
def __init__(self, data: dict[str, Any]) -> None:
self.data = data
payload = MyClass(data=MALICIOUS_SECRET_DICT)
_assert_no_secret_leak(payload)
class TestDumpdInKwargs:
"""Tests for the specific pattern of `dumpd()` result stored in kwargs."""
def test_dumpd_human_message_in_ai_message_kwargs(self) -> None:
"""`AIMessage` with `dumpd(HumanMessage)` in `additional_kwargs`."""
h = HumanMessage("Hello")
a = AIMessage("foo", additional_kwargs={"bar": [dumpd(h)]})
_assert_no_secret_leak(a)
def test_dumpd_human_message_with_secret_in_ai_message_kwargs(self) -> None:
"""`AIMessage` with `dumpd(HumanMessage w/ secret)` in `additional_kwargs`."""
h = HumanMessage(
"Hello",
additional_kwargs={"secret": MALICIOUS_SECRET_DICT},
)
a = AIMessage("foo", additional_kwargs={"bar": [dumpd(h)]})
_assert_no_secret_leak(a)
def test_double_dumpd_nesting(self) -> None:
"""Double nesting: `dumpd(AIMessage(dumpd(HumanMessage)))`."""
h = HumanMessage(
"Hello",
additional_kwargs={"secret": MALICIOUS_SECRET_DICT},
)
a = AIMessage("foo", additional_kwargs={"bar": [dumpd(h)]})
outer = AIMessage("outer", additional_kwargs={"nested": [dumpd(a)]})
_assert_no_secret_leak(outer)
class TestRoundTrip:
"""Tests that verify round-trip serialization preserves data structure."""
def test_human_message_with_secret_round_trip(self) -> None:
"""Verify secret-like dict is preserved as dict after round-trip."""
msg = HumanMessage(
content="Hello",
additional_kwargs={"data": MALICIOUS_SECRET_DICT},
)
serialized = dumpd(msg)
deserialized = load(serialized, secrets_from_env=True)
# The secret-like dict should be preserved as a plain dict
assert deserialized.additional_kwargs["data"] == MALICIOUS_SECRET_DICT
assert isinstance(deserialized.additional_kwargs["data"], dict)
def test_document_with_secret_round_trip(self) -> None:
"""Verify secret-like dict in `Document` metadata is preserved."""
doc = Document(
page_content="Hello",
metadata={"data": MALICIOUS_SECRET_DICT},
)
serialized = dumpd(doc)
deserialized = load(
serialized, secrets_from_env=True, allowed_objects=[Document]
)
# The secret-like dict should be preserved as a plain dict
assert deserialized.metadata["data"] == MALICIOUS_SECRET_DICT
assert isinstance(deserialized.metadata["data"], dict)
def test_plain_dict_with_secret_round_trip(self) -> None:
"""Verify secret-like dict in plain dict is preserved."""
payload = {"data": MALICIOUS_SECRET_DICT}
serialized = dumpd(payload)
deserialized = load(serialized, secrets_from_env=True)
# The secret-like dict should be preserved as a plain dict
assert deserialized["data"] == MALICIOUS_SECRET_DICT
assert isinstance(deserialized["data"], dict)
class TestEscapingEfficiency:
"""Tests that escaping doesn't cause excessive nesting."""
def test_no_triple_escaping(self) -> None:
"""Verify dumpd doesn't cause triple/multiple escaping."""
h = HumanMessage(
"Hello",
additional_kwargs={"bar": [MALICIOUS_SECRET_DICT]},
)
a = AIMessage("foo", additional_kwargs={"bar": [dumpd(h)]})
d = dumpd(a)
serialized = json.dumps(d)
# Count nested escape markers -
# should be max 2 (one for HumanMessage, one for secret)
# Not 3+ which would indicate re-escaping of already-escaped content
escape_count = len(re.findall(r"__lc_escaped__", serialized))
# The HumanMessage dict gets escaped (1), the secret inside gets escaped (1)
# Total should be 2, not 4 (which would mean triple nesting)
assert escape_count <= 2, (
f"Found {escape_count} escape markers, expected <= 2. "
f"This indicates unnecessary re-escaping.\n{serialized}"
)
def test_double_nesting_no_quadruple_escape(self) -> None:
"""Verify double dumpd nesting doesn't explode escape markers."""
h = HumanMessage(
"Hello",
additional_kwargs={"secret": MALICIOUS_SECRET_DICT},
)
a = AIMessage("middle", additional_kwargs={"nested": [dumpd(h)]})
outer = AIMessage("outer", additional_kwargs={"deep": [dumpd(a)]})
d = dumpd(outer)
serialized = json.dumps(d)
escape_count = len(re.findall(r"__lc_escaped__", serialized))
# Should be:
# outer escapes middle (1),
# middle escapes h (1),
# h escapes secret (1) = 3
# Not 6+ which would indicate re-escaping
assert escape_count <= 3, (
f"Found {escape_count} escape markers, expected <= 3. "
f"This indicates unnecessary re-escaping."
)
class TestConstructorInjection:
"""Tests for constructor-type injection (not just secrets)."""
def test_constructor_in_metadata_not_instantiated(self) -> None:
"""Verify constructor-like dict in metadata is not instantiated."""
malicious_constructor = {
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"kwargs": {"content": "injected"},
}
doc = Document(
page_content="Hello",
metadata={"data": malicious_constructor},
)
serialized = dumpd(doc)
deserialized = load(
serialized,
secrets_from_env=True,
allowed_objects=[Document, AIMessage],
)
# The constructor-like dict should be a plain dict, NOT an AIMessage
assert isinstance(deserialized.metadata["data"], dict)
assert deserialized.metadata["data"] == malicious_constructor
def test_constructor_in_content_not_instantiated(self) -> None:
"""Verify constructor-like dict in message content is not instantiated."""
malicious_constructor = {
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "messages", "human", "HumanMessage"],
"kwargs": {"content": "injected"},
}
msg = AIMessage(
content="Hello",
additional_kwargs={"nested": malicious_constructor},
)
serialized = dumpd(msg)
deserialized = load(
serialized,
secrets_from_env=True,
allowed_objects=[AIMessage, HumanMessage],
)
# The constructor-like dict should be a plain dict, NOT a HumanMessage
assert isinstance(deserialized.additional_kwargs["nested"], dict)
assert deserialized.additional_kwargs["nested"] == malicious_constructor
def test_allowed_objects() -> None:
# Core object
msg = AIMessage(content="foo")
serialized = dumpd(msg)
assert load(serialized) == msg
assert load(serialized, allowed_objects=[AIMessage]) == msg
assert load(serialized, allowed_objects="core") == msg
with pytest.raises(ValueError, match="not allowed"):
load(serialized, allowed_objects=[])
with pytest.raises(ValueError, match="not allowed"):
load(serialized, allowed_objects=[Document])

View File

@@ -1,19 +1,12 @@
import json
from typing import Any
import pytest
from pydantic import BaseModel, ConfigDict, Field, SecretStr
from langchain_core.documents import Document
from langchain_core.load import InitValidator, Serializable, dumpd, dumps, load, loads
from langchain_core.load import Serializable, dumpd, dumps, load
from langchain_core.load.serializable import _is_field_useful
from langchain_core.messages import AIMessage
from langchain_core.outputs import ChatGeneration, Generation
from langchain_core.prompts import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
PromptTemplate,
)
class NonBoolObj:
@@ -152,17 +145,10 @@ def test_simple_deserialization() -> None:
"lc": 1,
"type": "constructor",
}
new_foo = load(serialized_foo, allowed_objects=[Foo], valid_namespaces=["tests"])
new_foo = load(serialized_foo, valid_namespaces=["tests"])
assert new_foo == foo
def test_disallowed_deserialization() -> None:
foo = Foo(bar=1, baz="hello")
serialized_foo = dumpd(foo)
with pytest.raises(ValueError, match="not allowed"):
load(serialized_foo, allowed_objects=[], valid_namespaces=["tests"])
class Foo2(Serializable):
bar: int
baz: str
@@ -184,7 +170,6 @@ def test_simple_deserialization_with_additional_imports() -> None:
}
new_foo = load(
serialized_foo,
allowed_objects=[Foo2],
valid_namespaces=["tests"],
additional_import_mappings={
("tests", "unit_tests", "load", "test_serializable", "Foo"): (
@@ -238,7 +223,7 @@ def test_serialization_with_pydantic() -> None:
)
)
ser = dumpd(llm_response)
deser = load(ser, allowed_objects=[ChatGeneration, AIMessage])
deser = load(ser)
assert isinstance(deser, ChatGeneration)
assert deser.message.content
assert deser.message.additional_kwargs["parsed"] == my_model.model_dump()
@@ -275,8 +260,8 @@ def test_serialization_with_ignore_unserializable_fields() -> None:
]
]
}
# Load directly (no dumpd - this is already serialized data)
deser = load(data, allowed_objects=[AIMessage], ignore_unserializable_fields=True)
ser = dumpd(data)
deser = load(ser, ignore_unserializable_fields=True)
assert deser == {
"messages": [
[
@@ -380,514 +365,3 @@ def test_dumps_mixed_data_structure() -> None:
# Primitives should remain unchanged
assert parsed["list"] == [1, 2, {"nested": "value"}]
assert parsed["primitive"] == "string"
def test_document_normal_metadata_allowed() -> None:
"""Test that `Document` metadata without `'lc'` key works fine."""
doc = Document(
page_content="Hello world",
metadata={"source": "test.txt", "page": 1, "nested": {"key": "value"}},
)
serialized = dumpd(doc)
loaded = load(serialized, allowed_objects=[Document])
assert loaded.page_content == "Hello world"
expected = {"source": "test.txt", "page": 1, "nested": {"key": "value"}}
assert loaded.metadata == expected
class TestEscaping:
"""Tests that escape-based serialization prevents injection attacks.
When user data contains an `'lc'` key, it's escaped during serialization
(wrapped in `{"__lc_escaped__": ...}`). During deserialization, escaped
dicts are unwrapped and returned as plain dicts - NOT instantiated as
LC objects.
"""
def test_document_metadata_with_lc_key_escaped(self) -> None:
"""Test that `Document` metadata with `'lc'` key round-trips as plain dict."""
# User data that looks like an LC constructor - should be escaped, not executed
suspicious_metadata = {"lc": 1, "type": "constructor", "id": ["some", "module"]}
doc = Document(page_content="test", metadata=suspicious_metadata)
# Serialize - should escape the metadata
serialized = dumpd(doc)
assert serialized["kwargs"]["metadata"] == {
"__lc_escaped__": suspicious_metadata
}
# Deserialize - should restore original metadata as plain dict
loaded = load(serialized, allowed_objects=[Document])
assert loaded.metadata == suspicious_metadata # Plain dict, not instantiated
def test_document_metadata_with_nested_lc_key_escaped(self) -> None:
"""Test that nested `'lc'` key in `Document` metadata is escaped."""
suspicious_nested = {"lc": 1, "type": "constructor", "id": ["some", "module"]}
doc = Document(page_content="test", metadata={"nested": suspicious_nested})
serialized = dumpd(doc)
# The nested dict with 'lc' key should be escaped
assert serialized["kwargs"]["metadata"]["nested"] == {
"__lc_escaped__": suspicious_nested
}
loaded = load(serialized, allowed_objects=[Document])
assert loaded.metadata == {"nested": suspicious_nested}
def test_document_metadata_with_lc_key_in_list_escaped(self) -> None:
"""Test that `'lc'` key in list items within `Document` metadata is escaped."""
suspicious_item = {"lc": 1, "type": "constructor", "id": ["some", "module"]}
doc = Document(page_content="test", metadata={"items": [suspicious_item]})
serialized = dumpd(doc)
assert serialized["kwargs"]["metadata"]["items"][0] == {
"__lc_escaped__": suspicious_item
}
loaded = load(serialized, allowed_objects=[Document])
assert loaded.metadata == {"items": [suspicious_item]}
def test_malicious_payload_not_instantiated(self) -> None:
"""Test that malicious LC-like structures in user data are NOT instantiated."""
# An attacker might craft a payload with a valid AIMessage structure in metadata
malicious_data = {
"lc": 1,
"type": "constructor",
"id": ["langchain", "schema", "document", "Document"],
"kwargs": {
"page_content": "test",
"metadata": {
# This looks like a valid LC object but is in escaped form
"__lc_escaped__": {
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"kwargs": {"content": "injected message"},
}
},
},
}
# Even though AIMessage is allowed, the metadata should remain as dict
loaded = load(malicious_data, allowed_objects=[Document, AIMessage])
assert loaded.page_content == "test"
# The metadata is the original dict (unescaped), NOT an AIMessage instance
assert loaded.metadata == {
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "messages", "ai", "AIMessage"],
"kwargs": {"content": "injected message"},
}
assert not isinstance(loaded.metadata, AIMessage)
def test_message_additional_kwargs_with_lc_key_escaped(self) -> None:
"""Test that `AIMessage` `additional_kwargs` with `'lc'` is escaped."""
suspicious_data = {"lc": 1, "type": "constructor", "id": ["x", "y"]}
msg = AIMessage(
content="Hello",
additional_kwargs={"data": suspicious_data},
)
serialized = dumpd(msg)
assert serialized["kwargs"]["additional_kwargs"]["data"] == {
"__lc_escaped__": suspicious_data
}
loaded = load(serialized, allowed_objects=[AIMessage])
assert loaded.additional_kwargs == {"data": suspicious_data}
def test_message_response_metadata_with_lc_key_escaped(self) -> None:
"""Test that `AIMessage` `response_metadata` with `'lc'` is escaped."""
suspicious_data = {"lc": 1, "type": "constructor", "id": ["x", "y"]}
msg = AIMessage(content="Hello", response_metadata=suspicious_data)
serialized = dumpd(msg)
assert serialized["kwargs"]["response_metadata"] == {
"__lc_escaped__": suspicious_data
}
loaded = load(serialized, allowed_objects=[AIMessage])
assert loaded.response_metadata == suspicious_data
def test_double_escape_handling(self) -> None:
"""Test that data containing escape key itself is properly handled."""
# User data that contains our escape key
data_with_escape_key = {"__lc_escaped__": "some_value"}
doc = Document(page_content="test", metadata=data_with_escape_key)
serialized = dumpd(doc)
# Should be double-escaped since it looks like an escaped dict
assert serialized["kwargs"]["metadata"] == {
"__lc_escaped__": {"__lc_escaped__": "some_value"}
}
loaded = load(serialized, allowed_objects=[Document])
assert loaded.metadata == {"__lc_escaped__": "some_value"}
class TestDumpdEscapesLcKeyInPlainDicts:
"""Tests that `dumpd()` escapes `'lc'` keys in plain dict kwargs."""
def test_normal_message_not_escaped(self) -> None:
"""Test that normal `AIMessage` without `'lc'` key is not escaped."""
msg = AIMessage(
content="Hello",
additional_kwargs={"tool_calls": []},
response_metadata={"model": "gpt-4"},
)
serialized = dumpd(msg)
assert serialized["kwargs"]["content"] == "Hello"
# No escape wrappers for normal data
assert "__lc_escaped__" not in str(serialized)
def test_document_metadata_with_lc_key_escaped(self) -> None:
"""Test that `Document` with `'lc'` key in metadata is escaped."""
doc = Document(
page_content="test",
metadata={"lc": 1, "type": "constructor"},
)
serialized = dumpd(doc)
# Should be escaped, not blocked
assert serialized["kwargs"]["metadata"] == {
"__lc_escaped__": {"lc": 1, "type": "constructor"}
}
def test_document_metadata_with_nested_lc_key_escaped(self) -> None:
"""Test that `Document` with nested `'lc'` in metadata is escaped."""
doc = Document(
page_content="test",
metadata={"nested": {"lc": 1}},
)
serialized = dumpd(doc)
assert serialized["kwargs"]["metadata"]["nested"] == {
"__lc_escaped__": {"lc": 1}
}
def test_message_additional_kwargs_with_lc_key_escaped(self) -> None:
"""Test `AIMessage` with `'lc'` in `additional_kwargs` is escaped."""
msg = AIMessage(
content="Hello",
additional_kwargs={"malicious": {"lc": 1}},
)
serialized = dumpd(msg)
assert serialized["kwargs"]["additional_kwargs"]["malicious"] == {
"__lc_escaped__": {"lc": 1}
}
def test_message_response_metadata_with_lc_key_escaped(self) -> None:
"""Test `AIMessage` with `'lc'` in `response_metadata` is escaped."""
msg = AIMessage(
content="Hello",
response_metadata={"lc": 1},
)
serialized = dumpd(msg)
assert serialized["kwargs"]["response_metadata"] == {
"__lc_escaped__": {"lc": 1}
}
class TestInitValidator:
"""Tests for `init_validator` on `load()` and `loads()`."""
def test_init_validator_allows_valid_kwargs(self) -> None:
"""Test that `init_validator` returning None allows deserialization."""
msg = AIMessage(content="Hello")
serialized = dumpd(msg)
def allow_all(_class_path: tuple[str, ...], _kwargs: dict[str, Any]) -> None:
pass # Allow all by doing nothing
loaded = load(serialized, allowed_objects=[AIMessage], init_validator=allow_all)
assert loaded == msg
def test_init_validator_blocks_deserialization(self) -> None:
"""Test that `init_validator` can block deserialization by raising."""
doc = Document(page_content="test", metadata={"source": "test.txt"})
serialized = dumpd(doc)
def block_metadata(
_class_path: tuple[str, ...], kwargs: dict[str, Any]
) -> None:
if "metadata" in kwargs:
msg = "Metadata not allowed"
raise ValueError(msg)
with pytest.raises(ValueError, match="Metadata not allowed"):
load(serialized, allowed_objects=[Document], init_validator=block_metadata)
def test_init_validator_receives_correct_class_path(self) -> None:
"""Test that `init_validator` receives the correct class path."""
msg = AIMessage(content="Hello")
serialized = dumpd(msg)
received_class_paths: list[tuple[str, ...]] = []
def capture_class_path(
class_path: tuple[str, ...], _kwargs: dict[str, Any]
) -> None:
received_class_paths.append(class_path)
load(serialized, allowed_objects=[AIMessage], init_validator=capture_class_path)
assert len(received_class_paths) == 1
assert received_class_paths[0] == (
"langchain",
"schema",
"messages",
"AIMessage",
)
def test_init_validator_receives_correct_kwargs(self) -> None:
"""Test that `init_validator` receives the kwargs dict."""
msg = AIMessage(content="Hello world", name="test_name")
serialized = dumpd(msg)
received_kwargs: list[dict[str, Any]] = []
def capture_kwargs(
_class_path: tuple[str, ...], kwargs: dict[str, Any]
) -> None:
received_kwargs.append(kwargs)
load(serialized, allowed_objects=[AIMessage], init_validator=capture_kwargs)
assert len(received_kwargs) == 1
assert "content" in received_kwargs[0]
assert received_kwargs[0]["content"] == "Hello world"
assert "name" in received_kwargs[0]
assert received_kwargs[0]["name"] == "test_name"
def test_init_validator_with_loads(self) -> None:
"""Test that `init_validator` works with `loads()` function."""
doc = Document(page_content="test", metadata={"key": "value"})
json_str = dumps(doc)
def block_metadata(
_class_path: tuple[str, ...], kwargs: dict[str, Any]
) -> None:
if "metadata" in kwargs:
msg = "Metadata not allowed"
raise ValueError(msg)
with pytest.raises(ValueError, match="Metadata not allowed"):
loads(json_str, allowed_objects=[Document], init_validator=block_metadata)
def test_init_validator_none_allows_all(self) -> None:
"""Test that `init_validator=None` (default) allows all kwargs."""
msg = AIMessage(content="Hello")
serialized = dumpd(msg)
# Should work without init_validator
loaded = load(serialized, allowed_objects=[AIMessage])
assert loaded == msg
def test_init_validator_type_alias_exists(self) -> None:
"""Test that `InitValidator` type alias is exported and usable."""
def my_validator(_class_path: tuple[str, ...], _kwargs: dict[str, Any]) -> None:
pass
validator_typed: InitValidator = my_validator
assert callable(validator_typed)
def test_init_validator_blocks_specific_class(self) -> None:
"""Test blocking deserialization for a specific class."""
doc = Document(page_content="test", metadata={"source": "test.txt"})
serialized = dumpd(doc)
def block_documents(
class_path: tuple[str, ...], _kwargs: dict[str, Any]
) -> None:
if class_path == ("langchain", "schema", "document", "Document"):
msg = "Documents not allowed"
raise ValueError(msg)
with pytest.raises(ValueError, match="Documents not allowed"):
load(serialized, allowed_objects=[Document], init_validator=block_documents)
class TestJinja2SecurityBlocking:
"""Tests blocking Jinja2 templates by default."""
def test_fstring_template_allowed(self) -> None:
"""Test that f-string templates deserialize successfully."""
# Serialized ChatPromptTemplate with f-string format
serialized = {
"lc": 1,
"type": "constructor",
"id": ["langchain", "prompts", "chat", "ChatPromptTemplate"],
"kwargs": {
"input_variables": ["name"],
"messages": [
{
"lc": 1,
"type": "constructor",
"id": [
"langchain",
"prompts",
"chat",
"HumanMessagePromptTemplate",
],
"kwargs": {
"prompt": {
"lc": 1,
"type": "constructor",
"id": [
"langchain",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["name"],
"template": "Hello {name}",
"template_format": "f-string",
},
}
},
}
],
},
}
# f-string should deserialize successfully
loaded = load(
serialized,
allowed_objects=[
ChatPromptTemplate,
HumanMessagePromptTemplate,
PromptTemplate,
],
)
assert isinstance(loaded, ChatPromptTemplate)
assert loaded.input_variables == ["name"]
def test_jinja2_template_blocked(self) -> None:
"""Test that Jinja2 templates are blocked by default."""
# Malicious serialized payload attempting to use jinja2
malicious_serialized = {
"lc": 1,
"type": "constructor",
"id": ["langchain", "prompts", "chat", "ChatPromptTemplate"],
"kwargs": {
"input_variables": ["name"],
"messages": [
{
"lc": 1,
"type": "constructor",
"id": [
"langchain",
"prompts",
"chat",
"HumanMessagePromptTemplate",
],
"kwargs": {
"prompt": {
"lc": 1,
"type": "constructor",
"id": [
"langchain",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["name"],
"template": "{{ name }}",
"template_format": "jinja2",
},
}
},
}
],
},
}
# jinja2 should be blocked by default
with pytest.raises(ValueError, match="Jinja2 templates are not allowed"):
load(
malicious_serialized,
allowed_objects=[
ChatPromptTemplate,
HumanMessagePromptTemplate,
PromptTemplate,
],
)
def test_jinja2_blocked_standalone_prompt_template(self) -> None:
"""Test blocking Jinja2 on standalone `PromptTemplate`."""
serialized_jinja2 = {
"lc": 1,
"type": "constructor",
"id": ["langchain", "prompts", "prompt", "PromptTemplate"],
"kwargs": {
"input_variables": ["name"],
"template": "{{ name }}",
"template_format": "jinja2",
},
}
serialized_fstring = {
"lc": 1,
"type": "constructor",
"id": ["langchain", "prompts", "prompt", "PromptTemplate"],
"kwargs": {
"input_variables": ["name"],
"template": "{name}",
"template_format": "f-string",
},
}
# f-string should work
loaded = load(
serialized_fstring,
allowed_objects=[PromptTemplate],
)
assert isinstance(loaded, PromptTemplate)
assert loaded.template == "{name}"
# jinja2 should be blocked by default
with pytest.raises(ValueError, match="Jinja2 templates are not allowed"):
load(
serialized_jinja2,
allowed_objects=[PromptTemplate],
)
def test_jinja2_blocked_by_default(self) -> None:
"""Test that Jinja2 templates are blocked by default."""
serialized_jinja2 = {
"lc": 1,
"type": "constructor",
"id": ["langchain", "prompts", "prompt", "PromptTemplate"],
"kwargs": {
"input_variables": ["name"],
"template": "{{ name }}",
"template_format": "jinja2",
},
}
serialized_fstring = {
"lc": 1,
"type": "constructor",
"id": ["langchain", "prompts", "prompt", "PromptTemplate"],
"kwargs": {
"input_variables": ["name"],
"template": "{name}",
"template_format": "f-string",
},
}
# f-string should work
loaded = load(serialized_fstring, allowed_objects=[PromptTemplate])
assert isinstance(loaded, PromptTemplate)
assert loaded.template == "{name}"
# jinja2 should be blocked by default
with pytest.raises(ValueError, match="Jinja2 templates are not allowed"):
load(serialized_jinja2, allowed_objects=[PromptTemplate])

View File

@@ -0,0 +1,489 @@
from typing import Optional
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
from langchain_core.messages import content as types
def test_convert_to_v1_from_anthropic() -> None:
message = AIMessage(
[
{"type": "thinking", "thinking": "foo", "signature": "foo_signature"},
{"type": "text", "text": "Let's call a tool."},
{
"type": "tool_use",
"id": "abc_123",
"name": "get_weather",
"input": {"location": "San Francisco"},
},
{
"type": "text",
"text": "It's sunny.",
"citations": [
{
"type": "search_result_location",
"cited_text": "The weather is sunny.",
"source": "source_123",
"title": "Document Title",
"search_result_index": 1,
"start_block_index": 0,
"end_block_index": 2,
},
{"bar": "baz"},
],
},
{
"type": "server_tool_use",
"name": "web_search",
"input": {"query": "web search query"},
"id": "srvtoolu_abc123",
},
{
"type": "web_search_tool_result",
"tool_use_id": "srvtoolu_abc123",
"content": [
{
"type": "web_search_result",
"title": "Page Title 1",
"url": "<page url 1>",
"page_age": "January 1, 2025",
"encrypted_content": "<encrypted content 1>",
},
{
"type": "web_search_result",
"title": "Page Title 2",
"url": "<page url 2>",
"page_age": "January 2, 2025",
"encrypted_content": "<encrypted content 2>",
},
],
},
{
"type": "server_tool_use",
"id": "srvtoolu_def456",
"name": "code_execution",
"input": {"code": "import numpy as np..."},
},
{
"type": "code_execution_tool_result",
"tool_use_id": "srvtoolu_def456",
"content": {
"type": "code_execution_result",
"stdout": "Mean: 5.5\nStandard deviation...",
"stderr": "",
"return_code": 0,
},
},
{"type": "something_else", "foo": "bar"},
],
response_metadata={"model_provider": "anthropic"},
)
expected_content: list[types.ContentBlock] = [
{
"type": "reasoning",
"reasoning": "foo",
"extras": {"signature": "foo_signature"},
},
{"type": "text", "text": "Let's call a tool."},
{
"type": "tool_call",
"id": "abc_123",
"name": "get_weather",
"args": {"location": "San Francisco"},
},
{
"type": "text",
"text": "It's sunny.",
"annotations": [
{
"type": "citation",
"title": "Document Title",
"cited_text": "The weather is sunny.",
"extras": {
"source": "source_123",
"search_result_index": 1,
"start_block_index": 0,
"end_block_index": 2,
},
},
{"type": "non_standard_annotation", "value": {"bar": "baz"}},
],
},
{
"type": "server_tool_call",
"name": "web_search",
"id": "srvtoolu_abc123",
"args": {"query": "web search query"},
},
{
"type": "server_tool_result",
"tool_call_id": "srvtoolu_abc123",
"output": [
{
"type": "web_search_result",
"title": "Page Title 1",
"url": "<page url 1>",
"page_age": "January 1, 2025",
"encrypted_content": "<encrypted content 1>",
},
{
"type": "web_search_result",
"title": "Page Title 2",
"url": "<page url 2>",
"page_age": "January 2, 2025",
"encrypted_content": "<encrypted content 2>",
},
],
"status": "success",
"extras": {"block_type": "web_search_tool_result"},
},
{
"type": "server_tool_call",
"name": "code_interpreter",
"id": "srvtoolu_def456",
"args": {"code": "import numpy as np..."},
},
{
"type": "server_tool_result",
"tool_call_id": "srvtoolu_def456",
"output": {
"type": "code_execution_result",
"return_code": 0,
"stdout": "Mean: 5.5\nStandard deviation...",
"stderr": "",
},
"status": "success",
"extras": {"block_type": "code_execution_tool_result"},
},
{
"type": "non_standard",
"value": {"type": "something_else", "foo": "bar"},
},
]
assert message.content_blocks == expected_content
# Check no mutation
assert message.content != expected_content
message = AIMessage("Hello", response_metadata={"model_provider": "anthropic"})
expected_content = [{"type": "text", "text": "Hello"}]
assert message.content_blocks == expected_content
assert message.content != expected_content # check no mutation
def test_convert_to_v1_from_anthropic_chunk() -> None:
chunks = [
AIMessageChunk(
content=[{"text": "Looking ", "type": "text", "index": 0}],
response_metadata={"model_provider": "anthropic"},
),
AIMessageChunk(
content=[{"text": "now.", "type": "text", "index": 0}],
response_metadata={"model_provider": "anthropic"},
),
AIMessageChunk(
content=[
{
"type": "tool_use",
"name": "get_weather",
"input": {},
"id": "toolu_abc123",
"index": 1,
}
],
tool_call_chunks=[
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": "",
"id": "toolu_abc123",
"index": 1,
}
],
response_metadata={"model_provider": "anthropic"},
),
AIMessageChunk(
content=[{"type": "input_json_delta", "partial_json": "", "index": 1}],
tool_call_chunks=[
{
"name": None,
"args": "",
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "anthropic"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": '{"loca', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": '{"loca',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "anthropic"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": 'tion": "San ', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": 'tion": "San ',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "anthropic"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": 'Francisco"}', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": 'Francisco"}',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "anthropic"},
),
]
expected_contents: list[types.ContentBlock] = [
{"type": "text", "text": "Looking ", "index": 0},
{"type": "text", "text": "now.", "index": 0},
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": "",
"id": "toolu_abc123",
"index": 1,
},
{"name": None, "args": "", "id": None, "index": 1, "type": "tool_call_chunk"},
{
"name": None,
"args": '{"loca',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
{
"name": None,
"args": 'tion": "San ',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
{
"name": None,
"args": 'Francisco"}',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
]
for chunk, expected in zip(chunks, expected_contents):
assert chunk.content_blocks == [expected]
full: Optional[AIMessageChunk] = None
for chunk in chunks:
full = chunk if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
expected_content = [
{"type": "text", "text": "Looking now.", "index": 0},
{
"type": "tool_use",
"name": "get_weather",
"partial_json": '{"location": "San Francisco"}',
"input": {},
"id": "toolu_abc123",
"index": 1,
},
]
assert full.content == expected_content
expected_content_blocks = [
{"type": "text", "text": "Looking now.", "index": 0},
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": '{"location": "San Francisco"}',
"id": "toolu_abc123",
"index": 1,
},
]
assert full.content_blocks == expected_content_blocks
# Test parse partial json
full = AIMessageChunk(
content=[
{
"id": "srvtoolu_abc123",
"input": {},
"name": "web_fetch",
"type": "server_tool_use",
"index": 0,
"partial_json": '{"url": "https://docs.langchain.com"}',
},
{
"id": "mcptoolu_abc123",
"input": {},
"name": "ask_question",
"server_name": "<my server name>",
"type": "mcp_tool_use",
"index": 1,
"partial_json": '{"repoName": "<my repo>", "question": "<my query>"}',
},
],
response_metadata={"model_provider": "anthropic"},
chunk_position="last",
)
expected_content_blocks = [
{
"type": "server_tool_call",
"name": "web_fetch",
"id": "srvtoolu_abc123",
"args": {"url": "https://docs.langchain.com"},
"index": 0,
},
{
"type": "server_tool_call",
"name": "remote_mcp",
"id": "mcptoolu_abc123",
"args": {"repoName": "<my repo>", "question": "<my query>"},
"extras": {"tool_name": "ask_question", "server_name": "<my server name>"},
"index": 1,
},
]
assert full.content_blocks == expected_content_blocks
def test_convert_to_v1_from_anthropic_input() -> None:
message = HumanMessage(
[
{"type": "text", "text": "foo"},
{
"type": "document",
"source": {
"type": "base64",
"data": "<base64 data>",
"media_type": "application/pdf",
},
},
{
"type": "document",
"source": {
"type": "url",
"url": "<document url>",
},
},
{
"type": "document",
"source": {
"type": "content",
"content": [
{"type": "text", "text": "The grass is green"},
{"type": "text", "text": "The sky is blue"},
],
},
"citations": {"enabled": True},
},
{
"type": "document",
"source": {
"type": "text",
"data": "<plain text data>",
"media_type": "text/plain",
},
},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "<base64 image data>",
},
},
{
"type": "image",
"source": {
"type": "url",
"url": "<image url>",
},
},
{
"type": "image",
"source": {
"type": "file",
"file_id": "<image file id>",
},
},
{
"type": "document",
"source": {"type": "file", "file_id": "<pdf file id>"},
},
]
)
expected: list[types.ContentBlock] = [
{"type": "text", "text": "foo"},
{
"type": "file",
"base64": "<base64 data>",
"mime_type": "application/pdf",
},
{
"type": "file",
"url": "<document url>",
},
{
"type": "non_standard",
"value": {
"type": "document",
"source": {
"type": "content",
"content": [
{"type": "text", "text": "The grass is green"},
{"type": "text", "text": "The sky is blue"},
],
},
"citations": {"enabled": True},
},
},
{
"type": "text-plain",
"text": "<plain text data>",
"mime_type": "text/plain",
},
{
"type": "image",
"base64": "<base64 image data>",
"mime_type": "image/jpeg",
},
{
"type": "image",
"url": "<image url>",
},
{
"type": "image",
"id": "<image file id>",
},
{
"type": "file",
"id": "<pdf file id>",
},
]
assert message.content_blocks == expected

View File

@@ -0,0 +1,407 @@
from typing import Optional
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
from langchain_core.messages import content as types
def test_convert_to_v1_from_bedrock() -> None:
message = AIMessage(
[
{"type": "thinking", "thinking": "foo", "signature": "foo_signature"},
{"type": "text", "text": "Let's call a tool."},
{
"type": "tool_use",
"id": "abc_123",
"name": "get_weather",
"input": {"location": "San Francisco"},
},
{
"type": "text",
"text": "It's sunny.",
"citations": [
{
"type": "search_result_location",
"cited_text": "The weather is sunny.",
"source": "source_123",
"title": "Document Title",
"search_result_index": 1,
"start_block_index": 0,
"end_block_index": 2,
},
{"bar": "baz"},
],
},
{"type": "something_else", "foo": "bar"},
],
tool_calls=[
{
"type": "tool_call",
"id": "abc_123",
"name": "get_weather",
"args": {"location": "San Francisco"},
},
{
"type": "tool_call",
"id": "abc_234",
"name": "another_tool",
"args": {"arg_1": "value_1"},
},
],
response_metadata={
"model_provider": "bedrock",
"model_name": "us.anthropic.claude-sonnet-4-20250514-v1:0",
},
)
expected_content: list[types.ContentBlock] = [
{
"type": "reasoning",
"reasoning": "foo",
"extras": {"signature": "foo_signature"},
},
{"type": "text", "text": "Let's call a tool."},
{
"type": "tool_call",
"id": "abc_123",
"name": "get_weather",
"args": {"location": "San Francisco"},
},
{
"type": "text",
"text": "It's sunny.",
"annotations": [
{
"type": "citation",
"title": "Document Title",
"cited_text": "The weather is sunny.",
"extras": {
"source": "source_123",
"search_result_index": 1,
"start_block_index": 0,
"end_block_index": 2,
},
},
{"type": "non_standard_annotation", "value": {"bar": "baz"}},
],
},
{
"type": "non_standard",
"value": {"type": "something_else", "foo": "bar"},
},
{
"type": "tool_call",
"id": "abc_234",
"name": "another_tool",
"args": {"arg_1": "value_1"},
},
]
assert message.content_blocks == expected_content
# Check no mutation
assert message.content != expected_content
# Test with a non-Anthropic message
message = AIMessage(
[
{"type": "text", "text": "Let's call a tool."},
{"type": "something_else", "foo": "bar"},
],
tool_calls=[
{
"type": "tool_call",
"id": "abc_123",
"name": "get_weather",
"args": {"location": "San Francisco"},
}
],
response_metadata={"model_provider": "bedrock"},
)
expected_content = [
{"type": "text", "text": "Let's call a tool."},
{
"type": "non_standard",
"value": {"type": "something_else", "foo": "bar"},
},
{
"type": "tool_call",
"id": "abc_123",
"name": "get_weather",
"args": {"location": "San Francisco"},
},
]
assert message.content_blocks == expected_content
def test_convert_to_v1_from_bedrock_chunk() -> None:
chunks = [
AIMessageChunk(
content=[{"text": "Looking ", "type": "text", "index": 0}],
response_metadata={"model_provider": "bedrock"},
),
AIMessageChunk(
content=[{"text": "now.", "type": "text", "index": 0}],
response_metadata={"model_provider": "bedrock"},
),
AIMessageChunk(
content=[
{
"type": "tool_use",
"name": "get_weather",
"input": {},
"id": "toolu_abc123",
"index": 1,
}
],
tool_call_chunks=[
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": "",
"id": "toolu_abc123",
"index": 1,
}
],
response_metadata={"model_provider": "bedrock"},
),
AIMessageChunk(
content=[{"type": "input_json_delta", "partial_json": "", "index": 1}],
tool_call_chunks=[
{
"name": None,
"args": "",
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "bedrock"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": '{"loca', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": '{"loca',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "bedrock"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": 'tion": "San ', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": 'tion": "San ',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "bedrock"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": 'Francisco"}', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": 'Francisco"}',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "bedrock"},
),
]
expected_contents: list[types.ContentBlock] = [
{"type": "text", "text": "Looking ", "index": 0},
{"type": "text", "text": "now.", "index": 0},
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": "",
"id": "toolu_abc123",
"index": 1,
},
{"name": None, "args": "", "id": None, "index": 1, "type": "tool_call_chunk"},
{
"name": None,
"args": '{"loca',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
{
"name": None,
"args": 'tion": "San ',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
{
"name": None,
"args": 'Francisco"}',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
]
for chunk, expected in zip(chunks, expected_contents):
assert chunk.content_blocks == [expected]
full: Optional[AIMessageChunk] = None
for chunk in chunks:
full = chunk if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
expected_content = [
{"type": "text", "text": "Looking now.", "index": 0},
{
"type": "tool_use",
"name": "get_weather",
"partial_json": '{"location": "San Francisco"}',
"input": {},
"id": "toolu_abc123",
"index": 1,
},
]
assert full.content == expected_content
expected_content_blocks = [
{"type": "text", "text": "Looking now.", "index": 0},
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": '{"location": "San Francisco"}',
"id": "toolu_abc123",
"index": 1,
},
]
assert full.content_blocks == expected_content_blocks
def test_convert_to_v1_from_bedrock_input() -> None:
message = HumanMessage(
[
{"type": "text", "text": "foo"},
{
"type": "document",
"source": {
"type": "base64",
"data": "<base64 data>",
"media_type": "application/pdf",
},
},
{
"type": "document",
"source": {
"type": "url",
"url": "<document url>",
},
},
{
"type": "document",
"source": {
"type": "content",
"content": [
{"type": "text", "text": "The grass is green"},
{"type": "text", "text": "The sky is blue"},
],
},
"citations": {"enabled": True},
},
{
"type": "document",
"source": {
"type": "text",
"data": "<plain text data>",
"media_type": "text/plain",
},
},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "<base64 image data>",
},
},
{
"type": "image",
"source": {
"type": "url",
"url": "<image url>",
},
},
{
"type": "image",
"source": {
"type": "file",
"file_id": "<image file id>",
},
},
{
"type": "document",
"source": {"type": "file", "file_id": "<pdf file id>"},
},
]
)
expected: list[types.ContentBlock] = [
{"type": "text", "text": "foo"},
{
"type": "file",
"base64": "<base64 data>",
"mime_type": "application/pdf",
},
{
"type": "file",
"url": "<document url>",
},
{
"type": "non_standard",
"value": {
"type": "document",
"source": {
"type": "content",
"content": [
{"type": "text", "text": "The grass is green"},
{"type": "text", "text": "The sky is blue"},
],
},
"citations": {"enabled": True},
},
},
{
"type": "text-plain",
"text": "<plain text data>",
"mime_type": "text/plain",
},
{
"type": "image",
"base64": "<base64 image data>",
"mime_type": "image/jpeg",
},
{
"type": "image",
"url": "<image url>",
},
{
"type": "image",
"id": "<image file id>",
},
{
"type": "file",
"id": "<pdf file id>",
},
]
assert message.content_blocks == expected

View File

@@ -0,0 +1,381 @@
from typing import Optional
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
from langchain_core.messages import content as types
def test_convert_to_v1_from_bedrock_converse() -> None:
message = AIMessage(
[
{
"type": "reasoning_content",
"reasoning_content": {"text": "foo", "signature": "foo_signature"},
},
{"type": "text", "text": "Let's call a tool."},
{
"type": "tool_use",
"id": "abc_123",
"name": "get_weather",
"input": {"location": "San Francisco"},
},
{
"type": "text",
"text": "It's sunny.",
"citations": [
{
"title": "Document Title",
"source_content": [{"text": "The weather is sunny."}],
"location": {
"document_char": {
"document_index": 0,
"start": 58,
"end": 96,
}
},
},
{
"title": "Document Title",
"source_content": [{"text": "The weather is sunny."}],
"location": {
"document_page": {"document_index": 0, "start": 1, "end": 2}
},
},
{
"title": "Document Title",
"source_content": [{"text": "The weather is sunny."}],
"location": {
"document_chunk": {
"document_index": 0,
"start": 1,
"end": 2,
}
},
},
{"bar": "baz"},
],
},
{"type": "something_else", "foo": "bar"},
],
response_metadata={"model_provider": "bedrock_converse"},
)
expected_content: list[types.ContentBlock] = [
{
"type": "reasoning",
"reasoning": "foo",
"extras": {"signature": "foo_signature"},
},
{"type": "text", "text": "Let's call a tool."},
{
"type": "tool_call",
"id": "abc_123",
"name": "get_weather",
"args": {"location": "San Francisco"},
},
{
"type": "text",
"text": "It's sunny.",
"annotations": [
{
"type": "citation",
"title": "Document Title",
"cited_text": "The weather is sunny.",
"extras": {
"location": {
"document_char": {
"document_index": 0,
"start": 58,
"end": 96,
}
},
},
},
{
"type": "citation",
"title": "Document Title",
"cited_text": "The weather is sunny.",
"extras": {
"location": {
"document_page": {"document_index": 0, "start": 1, "end": 2}
},
},
},
{
"type": "citation",
"title": "Document Title",
"cited_text": "The weather is sunny.",
"extras": {
"location": {
"document_chunk": {
"document_index": 0,
"start": 1,
"end": 2,
}
}
},
},
{"type": "citation", "extras": {"bar": "baz"}},
],
},
{
"type": "non_standard",
"value": {"type": "something_else", "foo": "bar"},
},
]
assert message.content_blocks == expected_content
# Check no mutation
assert message.content != expected_content
def test_convert_to_v1_from_converse_chunk() -> None:
chunks = [
AIMessageChunk(
content=[{"text": "Looking ", "type": "text", "index": 0}],
response_metadata={"model_provider": "bedrock_converse"},
),
AIMessageChunk(
content=[{"text": "now.", "type": "text", "index": 0}],
response_metadata={"model_provider": "bedrock_converse"},
),
AIMessageChunk(
content=[
{
"type": "tool_use",
"name": "get_weather",
"input": {},
"id": "toolu_abc123",
"index": 1,
}
],
tool_call_chunks=[
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": "",
"id": "toolu_abc123",
"index": 1,
}
],
response_metadata={"model_provider": "bedrock_converse"},
),
AIMessageChunk(
content=[{"type": "input_json_delta", "partial_json": "", "index": 1}],
tool_call_chunks=[
{
"name": None,
"args": "",
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "bedrock_converse"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": '{"loca', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": '{"loca',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "bedrock_converse"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": 'tion": "San ', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": 'tion": "San ',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "bedrock_converse"},
),
AIMessageChunk(
content=[
{"type": "input_json_delta", "partial_json": 'Francisco"}', "index": 1}
],
tool_call_chunks=[
{
"name": None,
"args": 'Francisco"}',
"id": None,
"index": 1,
"type": "tool_call_chunk",
}
],
response_metadata={"model_provider": "bedrock_converse"},
),
]
expected_contents: list[types.ContentBlock] = [
{"type": "text", "text": "Looking ", "index": 0},
{"type": "text", "text": "now.", "index": 0},
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": "",
"id": "toolu_abc123",
"index": 1,
},
{"name": None, "args": "", "id": None, "index": 1, "type": "tool_call_chunk"},
{
"name": None,
"args": '{"loca',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
{
"name": None,
"args": 'tion": "San ',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
{
"name": None,
"args": 'Francisco"}',
"id": None,
"index": 1,
"type": "tool_call_chunk",
},
]
for chunk, expected in zip(chunks, expected_contents):
assert chunk.content_blocks == [expected]
full: Optional[AIMessageChunk] = None
for chunk in chunks:
full = chunk if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
expected_content = [
{"type": "text", "text": "Looking now.", "index": 0},
{
"type": "tool_use",
"name": "get_weather",
"partial_json": '{"location": "San Francisco"}',
"input": {},
"id": "toolu_abc123",
"index": 1,
},
]
assert full.content == expected_content
expected_content_blocks = [
{"type": "text", "text": "Looking now.", "index": 0},
{
"type": "tool_call_chunk",
"name": "get_weather",
"args": '{"location": "San Francisco"}',
"id": "toolu_abc123",
"index": 1,
},
]
assert full.content_blocks == expected_content_blocks
def test_convert_to_v1_from_converse_input() -> None:
message = HumanMessage(
[
{"text": "foo"},
{
"document": {
"format": "txt",
"name": "doc_name_1",
"source": {"text": "doc_text_1"},
"context": "doc_context_1",
"citations": {"enabled": True},
},
},
{
"document": {
"format": "pdf",
"name": "doc_name_2",
"source": {"bytes": b"doc_text_2"},
},
},
{
"document": {
"format": "txt",
"name": "doc_name_3",
"source": {"content": [{"text": "doc_text"}, {"text": "_3"}]},
"context": "doc_context_3",
},
},
{
"image": {
"format": "jpeg",
"source": {"bytes": b"image_bytes"},
}
},
{
"document": {
"format": "pdf",
"name": "doc_name_4",
"source": {
"s3Location": {"uri": "s3://bla", "bucketOwner": "owner"}
},
},
},
]
)
expected: list[types.ContentBlock] = [
{"type": "text", "text": "foo"},
{
"type": "text-plain",
"mime_type": "text/plain",
"text": "doc_text_1",
"extras": {
"name": "doc_name_1",
"context": "doc_context_1",
"citations": {"enabled": True},
},
},
{
"type": "file",
"mime_type": "application/pdf",
"base64": "ZG9jX3RleHRfMg==",
"extras": {"name": "doc_name_2"},
},
{
"type": "non_standard",
"value": {
"document": {
"format": "txt",
"name": "doc_name_3",
"source": {"content": [{"text": "doc_text"}, {"text": "_3"}]},
"context": "doc_context_3",
},
},
},
{
"type": "image",
"base64": "aW1hZ2VfYnl0ZXM=",
"mime_type": "image/jpeg",
},
{
"type": "non_standard",
"value": {
"document": {
"format": "pdf",
"name": "doc_name_4",
"source": {
"s3Location": {"uri": "s3://bla", "bucketOwner": "owner"}
},
},
},
},
]
assert message.content_blocks == expected

View File

@@ -0,0 +1,113 @@
from langchain_core.messages import HumanMessage
from langchain_core.messages import content as types
from langchain_core.messages.block_translators.langchain_v0 import (
_convert_legacy_v0_content_block_to_v1,
)
from tests.unit_tests.language_models.chat_models.test_base import (
_content_blocks_equal_ignore_id,
)
def test_convert_to_v1_from_openai_input() -> None:
message = HumanMessage(
content=[
{"type": "text", "text": "Hello"},
{
"type": "image",
"source_type": "url",
"url": "https://example.com/image.png",
},
{
"type": "image",
"source_type": "base64",
"data": "<base64 data>",
"mime_type": "image/png",
},
{
"type": "file",
"source_type": "url",
"url": "<document url>",
},
{
"type": "file",
"source_type": "base64",
"data": "<base64 data>",
"mime_type": "application/pdf",
},
{
"type": "audio",
"source_type": "base64",
"data": "<base64 data>",
"mime_type": "audio/mpeg",
},
{
"type": "file",
"source_type": "id",
"id": "<file id>",
},
]
)
expected: list[types.ContentBlock] = [
{"type": "text", "text": "Hello"},
{
"type": "image",
"url": "https://example.com/image.png",
},
{
"type": "image",
"base64": "<base64 data>",
"mime_type": "image/png",
},
{
"type": "file",
"url": "<document url>",
},
{
"type": "file",
"base64": "<base64 data>",
"mime_type": "application/pdf",
},
{
"type": "audio",
"base64": "<base64 data>",
"mime_type": "audio/mpeg",
},
{
"type": "file",
"file_id": "<file id>",
},
]
assert _content_blocks_equal_ignore_id(message.content_blocks, expected)
def test_convert_with_extras_on_v0_block() -> None:
"""Test that extras on old-style blocks are preserved in conversion.
Refer to `_extract_v0_extras` for details.
"""
block = {
"type": "image",
"source_type": "url",
"url": "https://example.com/image.png",
# extras follow
"alt_text": "An example image",
"caption": "Example caption",
"name": "example_image",
"description": None,
"attribution": None,
}
expected_output = {
"type": "image",
"url": "https://example.com/image.png",
"extras": {
"alt_text": "An example image",
"caption": "Example caption",
"name": "example_image",
# "description": None, # These are filtered out
# "attribution": None,
},
}
assert _convert_legacy_v0_content_block_to_v1(block) == expected_output

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