langchain-core 1.0.0a1__py3-none-any.whl → 1.0.0a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langchain-core might be problematic. Click here for more details.
- langchain_core/_api/beta_decorator.py +17 -40
- langchain_core/_api/deprecation.py +20 -7
- langchain_core/_api/path.py +19 -2
- langchain_core/_import_utils.py +7 -0
- langchain_core/agents.py +10 -6
- langchain_core/callbacks/base.py +28 -15
- langchain_core/callbacks/manager.py +81 -69
- langchain_core/callbacks/usage.py +4 -2
- langchain_core/chat_history.py +29 -21
- langchain_core/document_loaders/base.py +34 -9
- langchain_core/document_loaders/langsmith.py +3 -0
- langchain_core/documents/base.py +35 -10
- langchain_core/documents/transformers.py +4 -2
- langchain_core/embeddings/fake.py +8 -5
- langchain_core/env.py +2 -3
- langchain_core/example_selectors/base.py +12 -0
- langchain_core/exceptions.py +7 -0
- langchain_core/globals.py +17 -28
- langchain_core/indexing/api.py +57 -45
- langchain_core/indexing/base.py +5 -8
- langchain_core/indexing/in_memory.py +23 -3
- langchain_core/language_models/__init__.py +6 -2
- langchain_core/language_models/_utils.py +28 -4
- langchain_core/language_models/base.py +33 -21
- langchain_core/language_models/chat_models.py +103 -29
- langchain_core/language_models/fake_chat_models.py +5 -7
- langchain_core/language_models/llms.py +54 -20
- langchain_core/load/dump.py +2 -3
- langchain_core/load/load.py +15 -1
- langchain_core/load/serializable.py +38 -43
- langchain_core/memory.py +7 -3
- langchain_core/messages/__init__.py +7 -17
- langchain_core/messages/ai.py +41 -34
- langchain_core/messages/base.py +16 -7
- langchain_core/messages/block_translators/__init__.py +10 -8
- langchain_core/messages/block_translators/anthropic.py +3 -1
- langchain_core/messages/block_translators/bedrock.py +3 -1
- langchain_core/messages/block_translators/bedrock_converse.py +3 -1
- langchain_core/messages/block_translators/google_genai.py +3 -1
- langchain_core/messages/block_translators/google_vertexai.py +3 -1
- langchain_core/messages/block_translators/groq.py +3 -1
- langchain_core/messages/block_translators/langchain_v0.py +3 -136
- langchain_core/messages/block_translators/ollama.py +3 -1
- langchain_core/messages/block_translators/openai.py +252 -10
- langchain_core/messages/content.py +26 -124
- langchain_core/messages/human.py +2 -13
- langchain_core/messages/system.py +2 -6
- langchain_core/messages/tool.py +34 -14
- langchain_core/messages/utils.py +189 -74
- langchain_core/output_parsers/base.py +5 -2
- langchain_core/output_parsers/json.py +4 -4
- langchain_core/output_parsers/list.py +7 -22
- langchain_core/output_parsers/openai_functions.py +3 -0
- langchain_core/output_parsers/openai_tools.py +6 -1
- langchain_core/output_parsers/pydantic.py +4 -0
- langchain_core/output_parsers/string.py +5 -1
- langchain_core/output_parsers/xml.py +19 -19
- langchain_core/outputs/chat_generation.py +18 -7
- langchain_core/outputs/generation.py +14 -3
- langchain_core/outputs/llm_result.py +8 -1
- langchain_core/prompt_values.py +10 -4
- langchain_core/prompts/base.py +6 -11
- langchain_core/prompts/chat.py +88 -60
- langchain_core/prompts/dict.py +16 -8
- langchain_core/prompts/few_shot.py +9 -11
- langchain_core/prompts/few_shot_with_templates.py +5 -1
- langchain_core/prompts/image.py +12 -5
- langchain_core/prompts/loading.py +2 -2
- langchain_core/prompts/message.py +5 -6
- langchain_core/prompts/pipeline.py +13 -8
- langchain_core/prompts/prompt.py +22 -8
- langchain_core/prompts/string.py +18 -10
- langchain_core/prompts/structured.py +7 -2
- langchain_core/rate_limiters.py +2 -2
- langchain_core/retrievers.py +7 -6
- langchain_core/runnables/base.py +387 -246
- langchain_core/runnables/branch.py +11 -28
- langchain_core/runnables/config.py +20 -17
- langchain_core/runnables/configurable.py +34 -19
- langchain_core/runnables/fallbacks.py +20 -13
- langchain_core/runnables/graph.py +48 -38
- langchain_core/runnables/graph_ascii.py +40 -17
- langchain_core/runnables/graph_mermaid.py +54 -25
- langchain_core/runnables/graph_png.py +27 -31
- langchain_core/runnables/history.py +55 -58
- langchain_core/runnables/passthrough.py +44 -21
- langchain_core/runnables/retry.py +44 -23
- langchain_core/runnables/router.py +9 -8
- langchain_core/runnables/schema.py +9 -0
- langchain_core/runnables/utils.py +53 -90
- langchain_core/stores.py +19 -31
- langchain_core/sys_info.py +9 -8
- langchain_core/tools/base.py +36 -27
- langchain_core/tools/convert.py +25 -14
- langchain_core/tools/simple.py +36 -8
- langchain_core/tools/structured.py +25 -12
- langchain_core/tracers/base.py +2 -2
- langchain_core/tracers/context.py +5 -1
- langchain_core/tracers/core.py +110 -46
- langchain_core/tracers/evaluation.py +22 -26
- langchain_core/tracers/event_stream.py +97 -42
- langchain_core/tracers/langchain.py +12 -3
- langchain_core/tracers/langchain_v1.py +10 -2
- langchain_core/tracers/log_stream.py +56 -17
- langchain_core/tracers/root_listeners.py +4 -20
- langchain_core/tracers/run_collector.py +6 -16
- langchain_core/tracers/schemas.py +5 -1
- langchain_core/utils/aiter.py +14 -6
- langchain_core/utils/env.py +3 -0
- langchain_core/utils/function_calling.py +46 -20
- langchain_core/utils/interactive_env.py +6 -2
- langchain_core/utils/iter.py +12 -5
- langchain_core/utils/json.py +12 -3
- langchain_core/utils/json_schema.py +156 -40
- langchain_core/utils/loading.py +5 -1
- langchain_core/utils/mustache.py +25 -16
- langchain_core/utils/pydantic.py +38 -9
- langchain_core/utils/utils.py +25 -9
- langchain_core/vectorstores/base.py +7 -20
- langchain_core/vectorstores/in_memory.py +20 -14
- langchain_core/vectorstores/utils.py +18 -12
- langchain_core/version.py +1 -1
- langchain_core-1.0.0a3.dist-info/METADATA +77 -0
- langchain_core-1.0.0a3.dist-info/RECORD +181 -0
- langchain_core/beta/__init__.py +0 -1
- langchain_core/beta/runnables/__init__.py +0 -1
- langchain_core/beta/runnables/context.py +0 -448
- langchain_core-1.0.0a1.dist-info/METADATA +0 -106
- langchain_core-1.0.0a1.dist-info/RECORD +0 -184
- {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a3.dist-info}/WHEEL +0 -0
- {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a3.dist-info}/entry_points.txt +0 -0
langchain_core/messages/base.py
CHANGED
|
@@ -150,7 +150,8 @@ class BaseMessage(Serializable):
|
|
|
150
150
|
def get_lc_namespace(cls) -> list[str]:
|
|
151
151
|
"""Get the namespace of the langchain object.
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
Returns:
|
|
154
|
+
``["langchain", "schema", "messages"]``
|
|
154
155
|
"""
|
|
155
156
|
return ["langchain", "schema", "messages"]
|
|
156
157
|
|
|
@@ -179,14 +180,14 @@ class BaseMessage(Serializable):
|
|
|
179
180
|
.. versionadded:: 1.0.0
|
|
180
181
|
|
|
181
182
|
""" # noqa: E501
|
|
182
|
-
from langchain_core.messages import content as types
|
|
183
|
-
from langchain_core.messages.block_translators.anthropic import (
|
|
183
|
+
from langchain_core.messages import content as types # noqa: PLC0415
|
|
184
|
+
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
|
|
184
185
|
_convert_to_v1_from_anthropic_input,
|
|
185
186
|
)
|
|
186
|
-
from langchain_core.messages.block_translators.langchain_v0 import (
|
|
187
|
+
from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
|
|
187
188
|
_convert_v0_multimodal_input_to_v1,
|
|
188
189
|
)
|
|
189
|
-
from langchain_core.messages.block_translators.openai import (
|
|
190
|
+
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
|
|
190
191
|
_convert_to_v1_from_chat_completions_input,
|
|
191
192
|
)
|
|
192
193
|
|
|
@@ -246,8 +247,16 @@ class BaseMessage(Serializable):
|
|
|
246
247
|
return TextAccessor(text_value)
|
|
247
248
|
|
|
248
249
|
def __add__(self, other: Any) -> ChatPromptTemplate:
|
|
249
|
-
"""Concatenate this message with another message.
|
|
250
|
-
|
|
250
|
+
"""Concatenate this message with another message.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
other: Another message to concatenate with this one.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
A ChatPromptTemplate containing both messages.
|
|
257
|
+
"""
|
|
258
|
+
# Import locally to prevent circular imports.
|
|
259
|
+
from langchain_core.prompts.chat import ChatPromptTemplate # noqa: PLC0415
|
|
251
260
|
|
|
252
261
|
prompt = ChatPromptTemplate(messages=[self])
|
|
253
262
|
return prompt + other
|
|
@@ -53,26 +53,28 @@ def _register_translators() -> None:
|
|
|
53
53
|
For translators implemented outside langchain-core, they can be registered by
|
|
54
54
|
calling ``register_translator`` from within the integration package.
|
|
55
55
|
"""
|
|
56
|
-
from langchain_core.messages.block_translators.anthropic import (
|
|
56
|
+
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
|
|
57
57
|
_register_anthropic_translator,
|
|
58
58
|
)
|
|
59
|
-
from langchain_core.messages.block_translators.bedrock import (
|
|
59
|
+
from langchain_core.messages.block_translators.bedrock import ( # noqa: PLC0415
|
|
60
60
|
_register_bedrock_translator,
|
|
61
61
|
)
|
|
62
|
-
from langchain_core.messages.block_translators.bedrock_converse import (
|
|
62
|
+
from langchain_core.messages.block_translators.bedrock_converse import ( # noqa: PLC0415
|
|
63
63
|
_register_bedrock_converse_translator,
|
|
64
64
|
)
|
|
65
|
-
from langchain_core.messages.block_translators.google_genai import (
|
|
65
|
+
from langchain_core.messages.block_translators.google_genai import ( # noqa: PLC0415
|
|
66
66
|
_register_google_genai_translator,
|
|
67
67
|
)
|
|
68
|
-
from langchain_core.messages.block_translators.google_vertexai import (
|
|
68
|
+
from langchain_core.messages.block_translators.google_vertexai import ( # noqa: PLC0415
|
|
69
69
|
_register_google_vertexai_translator,
|
|
70
70
|
)
|
|
71
|
-
from langchain_core.messages.block_translators.groq import
|
|
72
|
-
|
|
71
|
+
from langchain_core.messages.block_translators.groq import ( # noqa: PLC0415
|
|
72
|
+
_register_groq_translator,
|
|
73
|
+
)
|
|
74
|
+
from langchain_core.messages.block_translators.ollama import ( # noqa: PLC0415
|
|
73
75
|
_register_ollama_translator,
|
|
74
76
|
)
|
|
75
|
-
from langchain_core.messages.block_translators.openai import (
|
|
77
|
+
from langchain_core.messages.block_translators.openai import ( # noqa: PLC0415
|
|
76
78
|
_register_openai_translator,
|
|
77
79
|
)
|
|
78
80
|
|
|
@@ -443,7 +443,9 @@ def _register_anthropic_translator() -> None:
|
|
|
443
443
|
|
|
444
444
|
Run automatically when the module is imported.
|
|
445
445
|
"""
|
|
446
|
-
from langchain_core.messages.block_translators import
|
|
446
|
+
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
|
447
|
+
register_translator,
|
|
448
|
+
)
|
|
447
449
|
|
|
448
450
|
register_translator("anthropic", translate_content, translate_content_chunk)
|
|
449
451
|
|
|
@@ -37,7 +37,9 @@ def _register_bedrock_translator() -> None:
|
|
|
37
37
|
|
|
38
38
|
Run automatically when the module is imported.
|
|
39
39
|
"""
|
|
40
|
-
from langchain_core.messages.block_translators import
|
|
40
|
+
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
|
41
|
+
register_translator,
|
|
42
|
+
)
|
|
41
43
|
|
|
42
44
|
register_translator("bedrock", translate_content, translate_content_chunk)
|
|
43
45
|
|
|
@@ -39,7 +39,9 @@ def _register_bedrock_converse_translator() -> None:
|
|
|
39
39
|
|
|
40
40
|
Run automatically when the module is imported.
|
|
41
41
|
"""
|
|
42
|
-
from langchain_core.messages.block_translators import
|
|
42
|
+
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
|
43
|
+
register_translator,
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
register_translator("bedrock_converse", translate_content, translate_content_chunk)
|
|
45
47
|
|
|
@@ -37,7 +37,9 @@ def _register_google_genai_translator() -> None:
|
|
|
37
37
|
|
|
38
38
|
Run automatically when the module is imported.
|
|
39
39
|
"""
|
|
40
|
-
from langchain_core.messages.block_translators import
|
|
40
|
+
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
|
41
|
+
register_translator,
|
|
42
|
+
)
|
|
41
43
|
|
|
42
44
|
register_translator("google_genai", translate_content, translate_content_chunk)
|
|
43
45
|
|
|
@@ -39,7 +39,9 @@ def _register_google_vertexai_translator() -> None:
|
|
|
39
39
|
|
|
40
40
|
Run automatically when the module is imported.
|
|
41
41
|
"""
|
|
42
|
-
from langchain_core.messages.block_translators import
|
|
42
|
+
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
|
43
|
+
register_translator,
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
register_translator("google_vertexai", translate_content, translate_content_chunk)
|
|
45
47
|
|
|
@@ -37,7 +37,9 @@ def _register_groq_translator() -> None:
|
|
|
37
37
|
|
|
38
38
|
Run automatically when the module is imported.
|
|
39
39
|
"""
|
|
40
|
-
from langchain_core.messages.block_translators import
|
|
40
|
+
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
|
41
|
+
register_translator,
|
|
42
|
+
)
|
|
41
43
|
|
|
42
44
|
register_translator("groq", translate_content, translate_content_chunk)
|
|
43
45
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Union, cast
|
|
4
4
|
|
|
5
|
-
from langchain_core.language_models._utils import _parse_data_uri
|
|
6
5
|
from langchain_core.messages import content as types
|
|
7
6
|
|
|
8
7
|
|
|
@@ -11,14 +10,15 @@ def _convert_v0_multimodal_input_to_v1(
|
|
|
11
10
|
) -> list[types.ContentBlock]:
|
|
12
11
|
"""Convert v0 multimodal blocks to v1 format.
|
|
13
12
|
|
|
14
|
-
Processes non_standard blocks that might be v0 format and converts them
|
|
15
|
-
to proper v1
|
|
13
|
+
Processes ``'non_standard'`` blocks that might be v0 format and converts them
|
|
14
|
+
to proper v1 ``ContentBlock``.
|
|
16
15
|
|
|
17
16
|
Args:
|
|
18
17
|
blocks: List of content blocks to process.
|
|
19
18
|
|
|
20
19
|
Returns:
|
|
21
20
|
Updated list with v0 blocks converted to v1 format.
|
|
21
|
+
|
|
22
22
|
"""
|
|
23
23
|
converted_blocks = []
|
|
24
24
|
unpacked_blocks: list[dict[str, Any]] = [
|
|
@@ -162,136 +162,3 @@ def _convert_legacy_v0_content_block_to_v1(
|
|
|
162
162
|
|
|
163
163
|
# If we can't convert, return the block unchanged
|
|
164
164
|
return block
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _convert_openai_format_to_data_block(
|
|
168
|
-
block: dict,
|
|
169
|
-
) -> Union[types.ContentBlock, dict[Any, Any]]:
|
|
170
|
-
"""Convert OpenAI image/audio/file content block to respective v1 multimodal block.
|
|
171
|
-
|
|
172
|
-
We expect that the incoming block is verified to be in OpenAI Chat Completions
|
|
173
|
-
format.
|
|
174
|
-
|
|
175
|
-
If parsing fails, passes block through unchanged.
|
|
176
|
-
|
|
177
|
-
Mappings (Chat Completions to LangChain v1):
|
|
178
|
-
- Image -> `ImageContentBlock`
|
|
179
|
-
- Audio -> `AudioContentBlock`
|
|
180
|
-
- File -> `FileContentBlock`
|
|
181
|
-
|
|
182
|
-
"""
|
|
183
|
-
|
|
184
|
-
# Extract extra keys to put them in `extras`
|
|
185
|
-
def _extract_extras(block_dict: dict, known_keys: set[str]) -> dict[str, Any]:
|
|
186
|
-
"""Extract unknown keys from block to preserve as extras."""
|
|
187
|
-
return {k: v for k, v in block_dict.items() if k not in known_keys}
|
|
188
|
-
|
|
189
|
-
# base64-style image block
|
|
190
|
-
if (block["type"] == "image_url") and (
|
|
191
|
-
parsed := _parse_data_uri(block["image_url"]["url"])
|
|
192
|
-
):
|
|
193
|
-
known_keys = {"type", "image_url"}
|
|
194
|
-
extras = _extract_extras(block, known_keys)
|
|
195
|
-
|
|
196
|
-
# Also extract extras from nested image_url dict
|
|
197
|
-
image_url_known_keys = {"url"}
|
|
198
|
-
image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
|
|
199
|
-
|
|
200
|
-
# Merge extras
|
|
201
|
-
all_extras = {**extras}
|
|
202
|
-
for key, value in image_url_extras.items():
|
|
203
|
-
if key == "detail": # Don't rename
|
|
204
|
-
all_extras["detail"] = value
|
|
205
|
-
else:
|
|
206
|
-
all_extras[f"image_url_{key}"] = value
|
|
207
|
-
|
|
208
|
-
return types.create_image_block(
|
|
209
|
-
# Even though this is labeled as `url`, it can be base64-encoded
|
|
210
|
-
base64=parsed["data"],
|
|
211
|
-
mime_type=parsed["mime_type"],
|
|
212
|
-
**all_extras,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# url-style image block
|
|
216
|
-
if (block["type"] == "image_url") and isinstance(
|
|
217
|
-
block["image_url"].get("url"), str
|
|
218
|
-
):
|
|
219
|
-
known_keys = {"type", "image_url"}
|
|
220
|
-
extras = _extract_extras(block, known_keys)
|
|
221
|
-
|
|
222
|
-
image_url_known_keys = {"url"}
|
|
223
|
-
image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
|
|
224
|
-
|
|
225
|
-
all_extras = {**extras}
|
|
226
|
-
for key, value in image_url_extras.items():
|
|
227
|
-
if key == "detail": # Don't rename
|
|
228
|
-
all_extras["detail"] = value
|
|
229
|
-
else:
|
|
230
|
-
all_extras[f"image_url_{key}"] = value
|
|
231
|
-
|
|
232
|
-
return types.create_image_block(
|
|
233
|
-
url=block["image_url"]["url"],
|
|
234
|
-
**all_extras,
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
# base64-style audio block
|
|
238
|
-
# audio is only represented via raw data, no url or ID option
|
|
239
|
-
if block["type"] == "input_audio":
|
|
240
|
-
known_keys = {"type", "input_audio"}
|
|
241
|
-
extras = _extract_extras(block, known_keys)
|
|
242
|
-
|
|
243
|
-
# Also extract extras from nested audio dict
|
|
244
|
-
audio_known_keys = {"data", "format"}
|
|
245
|
-
audio_extras = _extract_extras(block["input_audio"], audio_known_keys)
|
|
246
|
-
|
|
247
|
-
all_extras = {**extras}
|
|
248
|
-
for key, value in audio_extras.items():
|
|
249
|
-
all_extras[f"audio_{key}"] = value
|
|
250
|
-
|
|
251
|
-
return types.create_audio_block(
|
|
252
|
-
base64=block["input_audio"]["data"],
|
|
253
|
-
mime_type=f"audio/{block['input_audio']['format']}",
|
|
254
|
-
**all_extras,
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
# id-style file block
|
|
258
|
-
if block.get("type") == "file" and "file_id" in block.get("file", {}):
|
|
259
|
-
known_keys = {"type", "file"}
|
|
260
|
-
extras = _extract_extras(block, known_keys)
|
|
261
|
-
|
|
262
|
-
file_known_keys = {"file_id"}
|
|
263
|
-
file_extras = _extract_extras(block["file"], file_known_keys)
|
|
264
|
-
|
|
265
|
-
all_extras = {**extras}
|
|
266
|
-
for key, value in file_extras.items():
|
|
267
|
-
all_extras[f"file_{key}"] = value
|
|
268
|
-
|
|
269
|
-
return types.create_file_block(
|
|
270
|
-
file_id=block["file"]["file_id"],
|
|
271
|
-
**all_extras,
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
# base64-style file block
|
|
275
|
-
if (block["type"] == "file") and (
|
|
276
|
-
parsed := _parse_data_uri(block["file"]["file_data"])
|
|
277
|
-
):
|
|
278
|
-
known_keys = {"type", "file"}
|
|
279
|
-
extras = _extract_extras(block, known_keys)
|
|
280
|
-
|
|
281
|
-
file_known_keys = {"file_data", "filename"}
|
|
282
|
-
file_extras = _extract_extras(block["file"], file_known_keys)
|
|
283
|
-
|
|
284
|
-
all_extras = {**extras}
|
|
285
|
-
for key, value in file_extras.items():
|
|
286
|
-
all_extras[f"file_{key}"] = value
|
|
287
|
-
|
|
288
|
-
filename = block["file"].get("filename")
|
|
289
|
-
return types.create_file_block(
|
|
290
|
-
base64=parsed["data"],
|
|
291
|
-
mime_type="application/pdf",
|
|
292
|
-
filename=filename,
|
|
293
|
-
**all_extras,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
# Escape hatch
|
|
297
|
-
return block
|
|
@@ -37,7 +37,9 @@ def _register_ollama_translator() -> None:
|
|
|
37
37
|
|
|
38
38
|
Run automatically when the module is imported.
|
|
39
39
|
"""
|
|
40
|
-
from langchain_core.messages.block_translators import
|
|
40
|
+
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
|
41
|
+
register_translator,
|
|
42
|
+
)
|
|
41
43
|
|
|
42
44
|
register_translator("ollama", translate_content, translate_content_chunk)
|
|
43
45
|
|
|
@@ -3,21 +3,128 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import warnings
|
|
6
7
|
from collections.abc import Iterable
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
|
|
8
9
|
|
|
9
10
|
from langchain_core.language_models._utils import (
|
|
10
|
-
|
|
11
|
+
_parse_data_uri,
|
|
12
|
+
is_openai_data_block,
|
|
11
13
|
)
|
|
12
14
|
from langchain_core.messages import content as types
|
|
13
|
-
from langchain_core.messages.block_translators.langchain_v0 import (
|
|
14
|
-
_convert_openai_format_to_data_block,
|
|
15
|
-
)
|
|
16
15
|
|
|
17
16
|
if TYPE_CHECKING:
|
|
18
17
|
from langchain_core.messages import AIMessage, AIMessageChunk
|
|
19
18
|
|
|
20
19
|
|
|
20
|
+
def convert_to_openai_image_block(block: dict[str, Any]) -> dict:
|
|
21
|
+
"""Convert ``ImageContentBlock`` to format expected by OpenAI Chat Completions."""
|
|
22
|
+
if "url" in block:
|
|
23
|
+
return {
|
|
24
|
+
"type": "image_url",
|
|
25
|
+
"image_url": {
|
|
26
|
+
"url": block["url"],
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
if "base64" in block or block.get("source_type") == "base64":
|
|
30
|
+
if "mime_type" not in block:
|
|
31
|
+
error_message = "mime_type key is required for base64 data."
|
|
32
|
+
raise ValueError(error_message)
|
|
33
|
+
mime_type = block["mime_type"]
|
|
34
|
+
base64_data = block["data"] if "data" in block else block["base64"]
|
|
35
|
+
return {
|
|
36
|
+
"type": "image_url",
|
|
37
|
+
"image_url": {
|
|
38
|
+
"url": f"data:{mime_type};base64,{base64_data}",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
error_message = "Unsupported source type. Only 'url' and 'base64' are supported."
|
|
42
|
+
raise ValueError(error_message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def convert_to_openai_data_block(
|
|
46
|
+
block: dict, api: Literal["chat/completions", "responses"] = "chat/completions"
|
|
47
|
+
) -> dict:
|
|
48
|
+
"""Format standard data content block to format expected by OpenAI.
|
|
49
|
+
|
|
50
|
+
"Standard data content block" can include old-style LangChain v0 blocks
|
|
51
|
+
(URLContentBlock, Base64ContentBlock, IDContentBlock) or new ones.
|
|
52
|
+
"""
|
|
53
|
+
if block["type"] == "image":
|
|
54
|
+
chat_completions_block = convert_to_openai_image_block(block)
|
|
55
|
+
if api == "responses":
|
|
56
|
+
formatted_block = {
|
|
57
|
+
"type": "input_image",
|
|
58
|
+
"image_url": chat_completions_block["image_url"]["url"],
|
|
59
|
+
}
|
|
60
|
+
if chat_completions_block["image_url"].get("detail"):
|
|
61
|
+
formatted_block["detail"] = chat_completions_block["image_url"][
|
|
62
|
+
"detail"
|
|
63
|
+
]
|
|
64
|
+
else:
|
|
65
|
+
formatted_block = chat_completions_block
|
|
66
|
+
|
|
67
|
+
elif block["type"] == "file":
|
|
68
|
+
if block.get("source_type") == "base64" or "base64" in block:
|
|
69
|
+
# Handle v0 format (Base64CB): {"source_type": "base64", "data": "...", ...}
|
|
70
|
+
# Handle v1 format (IDCB): {"base64": "...", ...}
|
|
71
|
+
base64_data = block["data"] if "source_type" in block else block["base64"]
|
|
72
|
+
file = {"file_data": f"data:{block['mime_type']};base64,{base64_data}"}
|
|
73
|
+
if filename := block.get("filename"):
|
|
74
|
+
file["filename"] = filename
|
|
75
|
+
elif (extras := block.get("extras")) and ("filename" in extras):
|
|
76
|
+
file["filename"] = extras["filename"]
|
|
77
|
+
elif (extras := block.get("metadata")) and ("filename" in extras):
|
|
78
|
+
# Backward compat
|
|
79
|
+
file["filename"] = extras["filename"]
|
|
80
|
+
else:
|
|
81
|
+
# Can't infer filename
|
|
82
|
+
warnings.warn(
|
|
83
|
+
"OpenAI may require a filename for file uploads. Specify a filename"
|
|
84
|
+
" in the content block, e.g.: {'type': 'file', 'mime_type': "
|
|
85
|
+
"'...', 'base64': '...', 'filename': 'my-file.pdf'}",
|
|
86
|
+
stacklevel=1,
|
|
87
|
+
)
|
|
88
|
+
formatted_block = {"type": "file", "file": file}
|
|
89
|
+
if api == "responses":
|
|
90
|
+
formatted_block = {"type": "input_file", **formatted_block["file"]}
|
|
91
|
+
elif block.get("source_type") == "id" or "file_id" in block:
|
|
92
|
+
# Handle v0 format (IDContentBlock): {"source_type": "id", "id": "...", ...}
|
|
93
|
+
# Handle v1 format (IDCB): {"file_id": "...", ...}
|
|
94
|
+
file_id = block["id"] if "source_type" in block else block["file_id"]
|
|
95
|
+
formatted_block = {"type": "file", "file": {"file_id": file_id}}
|
|
96
|
+
if api == "responses":
|
|
97
|
+
formatted_block = {"type": "input_file", **formatted_block["file"]}
|
|
98
|
+
elif "url" in block: # Intentionally do not check for source_type="url"
|
|
99
|
+
if api == "chat/completions":
|
|
100
|
+
error_msg = "OpenAI Chat Completions does not support file URLs."
|
|
101
|
+
raise ValueError(error_msg)
|
|
102
|
+
# Only supported by Responses API; return in that format
|
|
103
|
+
formatted_block = {"type": "input_file", "file_url": block["url"]}
|
|
104
|
+
else:
|
|
105
|
+
error_msg = "Keys base64, url, or file_id required for file blocks."
|
|
106
|
+
raise ValueError(error_msg)
|
|
107
|
+
|
|
108
|
+
elif block["type"] == "audio":
|
|
109
|
+
if "base64" in block or block.get("source_type") == "base64":
|
|
110
|
+
# Handle v0 format: {"source_type": "base64", "data": "...", ...}
|
|
111
|
+
# Handle v1 format: {"base64": "...", ...}
|
|
112
|
+
base64_data = block["data"] if "source_type" in block else block["base64"]
|
|
113
|
+
audio_format = block["mime_type"].split("/")[-1]
|
|
114
|
+
formatted_block = {
|
|
115
|
+
"type": "input_audio",
|
|
116
|
+
"input_audio": {"data": base64_data, "format": audio_format},
|
|
117
|
+
}
|
|
118
|
+
else:
|
|
119
|
+
error_msg = "Key base64 is required for audio blocks."
|
|
120
|
+
raise ValueError(error_msg)
|
|
121
|
+
else:
|
|
122
|
+
error_msg = f"Block of type {block['type']} is not supported."
|
|
123
|
+
raise ValueError(error_msg)
|
|
124
|
+
|
|
125
|
+
return formatted_block
|
|
126
|
+
|
|
127
|
+
|
|
21
128
|
# v1 / Chat Completions
|
|
22
129
|
def _convert_to_v1_from_chat_completions(
|
|
23
130
|
message: AIMessage,
|
|
@@ -57,7 +164,7 @@ def _convert_to_v1_from_chat_completions_input(
|
|
|
57
164
|
Returns:
|
|
58
165
|
Updated list with OpenAI blocks converted to v1 format.
|
|
59
166
|
"""
|
|
60
|
-
from langchain_core.messages import content as types
|
|
167
|
+
from langchain_core.messages import content as types # noqa: PLC0415
|
|
61
168
|
|
|
62
169
|
converted_blocks = []
|
|
63
170
|
unpacked_blocks: list[dict[str, Any]] = [
|
|
@@ -71,7 +178,7 @@ def _convert_to_v1_from_chat_completions_input(
|
|
|
71
178
|
"image_url",
|
|
72
179
|
"input_audio",
|
|
73
180
|
"file",
|
|
74
|
-
} and
|
|
181
|
+
} and is_openai_data_block(block):
|
|
75
182
|
converted_block = _convert_openai_format_to_data_block(block)
|
|
76
183
|
# If conversion succeeded, use it; otherwise keep as non_standard
|
|
77
184
|
if (
|
|
@@ -153,7 +260,7 @@ _FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
|
|
|
153
260
|
|
|
154
261
|
def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
|
|
155
262
|
"""Convert v0 AIMessage into ``output_version="responses/v1"`` format."""
|
|
156
|
-
from langchain_core.messages import AIMessageChunk
|
|
263
|
+
from langchain_core.messages import AIMessageChunk # noqa: PLC0415
|
|
157
264
|
|
|
158
265
|
# Only update ChatOpenAI v0.3 AIMessages
|
|
159
266
|
is_chatopenai_v03 = (
|
|
@@ -288,6 +395,139 @@ def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
|
|
|
288
395
|
)
|
|
289
396
|
|
|
290
397
|
|
|
398
|
+
def _convert_openai_format_to_data_block(
|
|
399
|
+
block: dict,
|
|
400
|
+
) -> Union[types.ContentBlock, dict[Any, Any]]:
|
|
401
|
+
"""Convert OpenAI image/audio/file content block to respective v1 multimodal block.
|
|
402
|
+
|
|
403
|
+
We expect that the incoming block is verified to be in OpenAI Chat Completions
|
|
404
|
+
format.
|
|
405
|
+
|
|
406
|
+
If parsing fails, passes block through unchanged.
|
|
407
|
+
|
|
408
|
+
Mappings (Chat Completions to LangChain v1):
|
|
409
|
+
- Image -> `ImageContentBlock`
|
|
410
|
+
- Audio -> `AudioContentBlock`
|
|
411
|
+
- File -> `FileContentBlock`
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
# Extract extra keys to put them in `extras`
|
|
416
|
+
def _extract_extras(block_dict: dict, known_keys: set[str]) -> dict[str, Any]:
|
|
417
|
+
"""Extract unknown keys from block to preserve as extras."""
|
|
418
|
+
return {k: v for k, v in block_dict.items() if k not in known_keys}
|
|
419
|
+
|
|
420
|
+
# base64-style image block
|
|
421
|
+
if (block["type"] == "image_url") and (
|
|
422
|
+
parsed := _parse_data_uri(block["image_url"]["url"])
|
|
423
|
+
):
|
|
424
|
+
known_keys = {"type", "image_url"}
|
|
425
|
+
extras = _extract_extras(block, known_keys)
|
|
426
|
+
|
|
427
|
+
# Also extract extras from nested image_url dict
|
|
428
|
+
image_url_known_keys = {"url"}
|
|
429
|
+
image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
|
|
430
|
+
|
|
431
|
+
# Merge extras
|
|
432
|
+
all_extras = {**extras}
|
|
433
|
+
for key, value in image_url_extras.items():
|
|
434
|
+
if key == "detail": # Don't rename
|
|
435
|
+
all_extras["detail"] = value
|
|
436
|
+
else:
|
|
437
|
+
all_extras[f"image_url_{key}"] = value
|
|
438
|
+
|
|
439
|
+
return types.create_image_block(
|
|
440
|
+
# Even though this is labeled as `url`, it can be base64-encoded
|
|
441
|
+
base64=parsed["data"],
|
|
442
|
+
mime_type=parsed["mime_type"],
|
|
443
|
+
**all_extras,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# url-style image block
|
|
447
|
+
if (block["type"] == "image_url") and isinstance(
|
|
448
|
+
block["image_url"].get("url"), str
|
|
449
|
+
):
|
|
450
|
+
known_keys = {"type", "image_url"}
|
|
451
|
+
extras = _extract_extras(block, known_keys)
|
|
452
|
+
|
|
453
|
+
image_url_known_keys = {"url"}
|
|
454
|
+
image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
|
|
455
|
+
|
|
456
|
+
all_extras = {**extras}
|
|
457
|
+
for key, value in image_url_extras.items():
|
|
458
|
+
if key == "detail": # Don't rename
|
|
459
|
+
all_extras["detail"] = value
|
|
460
|
+
else:
|
|
461
|
+
all_extras[f"image_url_{key}"] = value
|
|
462
|
+
|
|
463
|
+
return types.create_image_block(
|
|
464
|
+
url=block["image_url"]["url"],
|
|
465
|
+
**all_extras,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# base64-style audio block
|
|
469
|
+
# audio is only represented via raw data, no url or ID option
|
|
470
|
+
if block["type"] == "input_audio":
|
|
471
|
+
known_keys = {"type", "input_audio"}
|
|
472
|
+
extras = _extract_extras(block, known_keys)
|
|
473
|
+
|
|
474
|
+
# Also extract extras from nested audio dict
|
|
475
|
+
audio_known_keys = {"data", "format"}
|
|
476
|
+
audio_extras = _extract_extras(block["input_audio"], audio_known_keys)
|
|
477
|
+
|
|
478
|
+
all_extras = {**extras}
|
|
479
|
+
for key, value in audio_extras.items():
|
|
480
|
+
all_extras[f"audio_{key}"] = value
|
|
481
|
+
|
|
482
|
+
return types.create_audio_block(
|
|
483
|
+
base64=block["input_audio"]["data"],
|
|
484
|
+
mime_type=f"audio/{block['input_audio']['format']}",
|
|
485
|
+
**all_extras,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# id-style file block
|
|
489
|
+
if block.get("type") == "file" and "file_id" in block.get("file", {}):
|
|
490
|
+
known_keys = {"type", "file"}
|
|
491
|
+
extras = _extract_extras(block, known_keys)
|
|
492
|
+
|
|
493
|
+
file_known_keys = {"file_id"}
|
|
494
|
+
file_extras = _extract_extras(block["file"], file_known_keys)
|
|
495
|
+
|
|
496
|
+
all_extras = {**extras}
|
|
497
|
+
for key, value in file_extras.items():
|
|
498
|
+
all_extras[f"file_{key}"] = value
|
|
499
|
+
|
|
500
|
+
return types.create_file_block(
|
|
501
|
+
file_id=block["file"]["file_id"],
|
|
502
|
+
**all_extras,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# base64-style file block
|
|
506
|
+
if (block["type"] == "file") and (
|
|
507
|
+
parsed := _parse_data_uri(block["file"]["file_data"])
|
|
508
|
+
):
|
|
509
|
+
known_keys = {"type", "file"}
|
|
510
|
+
extras = _extract_extras(block, known_keys)
|
|
511
|
+
|
|
512
|
+
file_known_keys = {"file_data", "filename"}
|
|
513
|
+
file_extras = _extract_extras(block["file"], file_known_keys)
|
|
514
|
+
|
|
515
|
+
all_extras = {**extras}
|
|
516
|
+
for key, value in file_extras.items():
|
|
517
|
+
all_extras[f"file_{key}"] = value
|
|
518
|
+
|
|
519
|
+
filename = block["file"].get("filename")
|
|
520
|
+
return types.create_file_block(
|
|
521
|
+
base64=parsed["data"],
|
|
522
|
+
mime_type="application/pdf",
|
|
523
|
+
filename=filename,
|
|
524
|
+
**all_extras,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# Escape hatch
|
|
528
|
+
return block
|
|
529
|
+
|
|
530
|
+
|
|
291
531
|
# v1 / Responses
|
|
292
532
|
def _convert_annotation_to_v1(annotation: dict[str, Any]) -> types.Annotation:
|
|
293
533
|
annotation_type = annotation.get("type")
|
|
@@ -438,7 +678,7 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
|
|
|
438
678
|
] = None
|
|
439
679
|
call_id = block.get("call_id", "")
|
|
440
680
|
|
|
441
|
-
from langchain_core.messages import AIMessageChunk
|
|
681
|
+
from langchain_core.messages import AIMessageChunk # noqa: PLC0415
|
|
442
682
|
|
|
443
683
|
if (
|
|
444
684
|
isinstance(message, AIMessageChunk)
|
|
@@ -578,7 +818,9 @@ def _register_openai_translator() -> None:
|
|
|
578
818
|
|
|
579
819
|
Run automatically when the module is imported.
|
|
580
820
|
"""
|
|
581
|
-
from langchain_core.messages.block_translators import
|
|
821
|
+
from langchain_core.messages.block_translators import ( # noqa: PLC0415
|
|
822
|
+
register_translator,
|
|
823
|
+
)
|
|
582
824
|
|
|
583
825
|
register_translator("openai", translate_content, translate_content_chunk)
|
|
584
826
|
|