langchain-core 1.0.0a1__py3-none-any.whl → 1.0.0a2__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.
- langchain_core/language_models/_utils.py +2 -0
- langchain_core/language_models/chat_models.py +4 -2
- langchain_core/messages/__init__.py +6 -16
- langchain_core/messages/block_translators/langchain_v0.py +3 -136
- langchain_core/messages/block_translators/openai.py +215 -3
- langchain_core/messages/content.py +3 -111
- langchain_core/messages/utils.py +3 -1
- langchain_core/version.py +1 -1
- {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a2.dist-info}/METADATA +1 -1
- {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a2.dist-info}/RECORD +12 -12
- {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a2.dist-info}/WHEEL +0 -0
- {langchain_core-1.0.0a1.dist-info → langchain_core-1.0.0a2.dist-info}/entry_points.txt +0 -0
|
@@ -214,6 +214,8 @@ def _normalize_messages(
|
|
|
214
214
|
"""
|
|
215
215
|
from langchain_core.messages.block_translators.langchain_v0 import (
|
|
216
216
|
_convert_legacy_v0_content_block_to_v1,
|
|
217
|
+
)
|
|
218
|
+
from langchain_core.messages.block_translators.openai import (
|
|
217
219
|
_convert_openai_format_to_data_block,
|
|
218
220
|
)
|
|
219
221
|
|
|
@@ -44,11 +44,13 @@ from langchain_core.messages import (
|
|
|
44
44
|
BaseMessage,
|
|
45
45
|
HumanMessage,
|
|
46
46
|
convert_to_messages,
|
|
47
|
-
convert_to_openai_data_block,
|
|
48
|
-
convert_to_openai_image_block,
|
|
49
47
|
is_data_content_block,
|
|
50
48
|
message_chunk_to_message,
|
|
51
49
|
)
|
|
50
|
+
from langchain_core.messages.block_translators.openai import (
|
|
51
|
+
convert_to_openai_data_block,
|
|
52
|
+
convert_to_openai_image_block,
|
|
53
|
+
)
|
|
52
54
|
from langchain_core.outputs import (
|
|
53
55
|
ChatGeneration,
|
|
54
56
|
ChatGenerationChunk,
|
|
@@ -32,6 +32,10 @@ if TYPE_CHECKING:
|
|
|
32
32
|
message_to_dict,
|
|
33
33
|
messages_to_dict,
|
|
34
34
|
)
|
|
35
|
+
from langchain_core.messages.block_translators.openai import (
|
|
36
|
+
convert_to_openai_data_block,
|
|
37
|
+
convert_to_openai_image_block,
|
|
38
|
+
)
|
|
35
39
|
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
|
|
36
40
|
from langchain_core.messages.content import (
|
|
37
41
|
Annotation,
|
|
@@ -52,13 +56,7 @@ if TYPE_CHECKING:
|
|
|
52
56
|
VideoContentBlock,
|
|
53
57
|
WebSearchCall,
|
|
54
58
|
WebSearchResult,
|
|
55
|
-
convert_to_openai_data_block,
|
|
56
|
-
convert_to_openai_image_block,
|
|
57
59
|
is_data_content_block,
|
|
58
|
-
is_reasoning_block,
|
|
59
|
-
is_text_block,
|
|
60
|
-
is_tool_call_block,
|
|
61
|
-
is_tool_call_chunk,
|
|
62
60
|
)
|
|
63
61
|
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
|
|
64
62
|
from langchain_core.messages.human import HumanMessage, HumanMessageChunk
|
|
@@ -135,10 +133,6 @@ __all__ = (
|
|
|
135
133
|
"filter_messages",
|
|
136
134
|
"get_buffer_string",
|
|
137
135
|
"is_data_content_block",
|
|
138
|
-
"is_reasoning_block",
|
|
139
|
-
"is_text_block",
|
|
140
|
-
"is_tool_call_block",
|
|
141
|
-
"is_tool_call_chunk",
|
|
142
136
|
"merge_content",
|
|
143
137
|
"merge_message_runs",
|
|
144
138
|
"message_chunk_to_message",
|
|
@@ -192,16 +186,12 @@ _dynamic_imports = {
|
|
|
192
186
|
"MessageLikeRepresentation": "utils",
|
|
193
187
|
"_message_from_dict": "utils",
|
|
194
188
|
"convert_to_messages": "utils",
|
|
195
|
-
"convert_to_openai_data_block": "
|
|
196
|
-
"convert_to_openai_image_block": "
|
|
189
|
+
"convert_to_openai_data_block": "block_translators.openai",
|
|
190
|
+
"convert_to_openai_image_block": "block_translators.openai",
|
|
197
191
|
"convert_to_openai_messages": "utils",
|
|
198
192
|
"filter_messages": "utils",
|
|
199
193
|
"get_buffer_string": "utils",
|
|
200
194
|
"is_data_content_block": "content",
|
|
201
|
-
"is_reasoning_block": "content",
|
|
202
|
-
"is_text_block": "content",
|
|
203
|
-
"is_tool_call_block": "content",
|
|
204
|
-
"is_tool_call_chunk": "content",
|
|
205
195
|
"merge_message_runs": "utils",
|
|
206
196
|
"message_chunk_to_message": "utils",
|
|
207
197
|
"messages_from_dict": "utils",
|
|
@@ -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
|
|
@@ -3,21 +3,100 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import warnings
|
|
6
7
|
from collections.abc import Iterable
|
|
7
8
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
8
9
|
|
|
9
10
|
from langchain_core.language_models._utils import (
|
|
10
11
|
_is_openai_data_block,
|
|
12
|
+
_parse_data_uri,
|
|
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(block: dict) -> dict:
|
|
46
|
+
"""Format standard data content block to format expected by OpenAI."""
|
|
47
|
+
if block["type"] == "image":
|
|
48
|
+
formatted_block = convert_to_openai_image_block(block)
|
|
49
|
+
|
|
50
|
+
elif block["type"] == "file":
|
|
51
|
+
if "base64" in block or block.get("source_type") == "base64":
|
|
52
|
+
# Handle v0 format: {"source_type": "base64", "data": "...", ...}
|
|
53
|
+
# Handle v1 format: {"base64": "...", ...}
|
|
54
|
+
base64_data = block["data"] if "source_type" in block else block["base64"]
|
|
55
|
+
file = {"file_data": f"data:{block['mime_type']};base64,{base64_data}"}
|
|
56
|
+
if filename := block.get("filename"):
|
|
57
|
+
file["filename"] = filename
|
|
58
|
+
elif (extras := block.get("extras")) and ("filename" in extras):
|
|
59
|
+
file["filename"] = extras["filename"]
|
|
60
|
+
elif (extras := block.get("metadata")) and ("filename" in extras):
|
|
61
|
+
# Backward compat
|
|
62
|
+
file["filename"] = extras["filename"]
|
|
63
|
+
else:
|
|
64
|
+
warnings.warn(
|
|
65
|
+
"OpenAI may require a filename for file inputs. Specify a filename "
|
|
66
|
+
"in the content block: {'type': 'file', 'mime_type': "
|
|
67
|
+
"'application/pdf', 'base64': '...', 'filename': 'my-pdf'}",
|
|
68
|
+
stacklevel=1,
|
|
69
|
+
)
|
|
70
|
+
formatted_block = {"type": "file", "file": file}
|
|
71
|
+
elif "file_id" in block or block.get("source_type") == "id":
|
|
72
|
+
# Handle v0 format: {"source_type": "id", "id": "...", ...}
|
|
73
|
+
# Handle v1 format: {"file_id": "...", ...}
|
|
74
|
+
file_id = block["id"] if "source_type" in block else block["file_id"]
|
|
75
|
+
formatted_block = {"type": "file", "file": {"file_id": file_id}}
|
|
76
|
+
else:
|
|
77
|
+
error_msg = "Keys base64 or file_id required for file blocks."
|
|
78
|
+
raise ValueError(error_msg)
|
|
79
|
+
|
|
80
|
+
elif block["type"] == "audio":
|
|
81
|
+
if "base64" in block or block.get("source_type") == "base64":
|
|
82
|
+
# Handle v0 format: {"source_type": "base64", "data": "...", ...}
|
|
83
|
+
# Handle v1 format: {"base64": "...", ...}
|
|
84
|
+
base64_data = block["data"] if "source_type" in block else block["base64"]
|
|
85
|
+
audio_format = block["mime_type"].split("/")[-1]
|
|
86
|
+
formatted_block = {
|
|
87
|
+
"type": "input_audio",
|
|
88
|
+
"input_audio": {"data": base64_data, "format": audio_format},
|
|
89
|
+
}
|
|
90
|
+
else:
|
|
91
|
+
error_msg = "Key base64 is required for audio blocks."
|
|
92
|
+
raise ValueError(error_msg)
|
|
93
|
+
else:
|
|
94
|
+
error_msg = f"Block of type {block['type']} is not supported."
|
|
95
|
+
raise ValueError(error_msg)
|
|
96
|
+
|
|
97
|
+
return formatted_block
|
|
98
|
+
|
|
99
|
+
|
|
21
100
|
# v1 / Chat Completions
|
|
22
101
|
def _convert_to_v1_from_chat_completions(
|
|
23
102
|
message: AIMessage,
|
|
@@ -288,6 +367,139 @@ def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
|
|
|
288
367
|
)
|
|
289
368
|
|
|
290
369
|
|
|
370
|
+
def _convert_openai_format_to_data_block(
|
|
371
|
+
block: dict,
|
|
372
|
+
) -> Union[types.ContentBlock, dict[Any, Any]]:
|
|
373
|
+
"""Convert OpenAI image/audio/file content block to respective v1 multimodal block.
|
|
374
|
+
|
|
375
|
+
We expect that the incoming block is verified to be in OpenAI Chat Completions
|
|
376
|
+
format.
|
|
377
|
+
|
|
378
|
+
If parsing fails, passes block through unchanged.
|
|
379
|
+
|
|
380
|
+
Mappings (Chat Completions to LangChain v1):
|
|
381
|
+
- Image -> `ImageContentBlock`
|
|
382
|
+
- Audio -> `AudioContentBlock`
|
|
383
|
+
- File -> `FileContentBlock`
|
|
384
|
+
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
# Extract extra keys to put them in `extras`
|
|
388
|
+
def _extract_extras(block_dict: dict, known_keys: set[str]) -> dict[str, Any]:
|
|
389
|
+
"""Extract unknown keys from block to preserve as extras."""
|
|
390
|
+
return {k: v for k, v in block_dict.items() if k not in known_keys}
|
|
391
|
+
|
|
392
|
+
# base64-style image block
|
|
393
|
+
if (block["type"] == "image_url") and (
|
|
394
|
+
parsed := _parse_data_uri(block["image_url"]["url"])
|
|
395
|
+
):
|
|
396
|
+
known_keys = {"type", "image_url"}
|
|
397
|
+
extras = _extract_extras(block, known_keys)
|
|
398
|
+
|
|
399
|
+
# Also extract extras from nested image_url dict
|
|
400
|
+
image_url_known_keys = {"url"}
|
|
401
|
+
image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
|
|
402
|
+
|
|
403
|
+
# Merge extras
|
|
404
|
+
all_extras = {**extras}
|
|
405
|
+
for key, value in image_url_extras.items():
|
|
406
|
+
if key == "detail": # Don't rename
|
|
407
|
+
all_extras["detail"] = value
|
|
408
|
+
else:
|
|
409
|
+
all_extras[f"image_url_{key}"] = value
|
|
410
|
+
|
|
411
|
+
return types.create_image_block(
|
|
412
|
+
# Even though this is labeled as `url`, it can be base64-encoded
|
|
413
|
+
base64=parsed["data"],
|
|
414
|
+
mime_type=parsed["mime_type"],
|
|
415
|
+
**all_extras,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# url-style image block
|
|
419
|
+
if (block["type"] == "image_url") and isinstance(
|
|
420
|
+
block["image_url"].get("url"), str
|
|
421
|
+
):
|
|
422
|
+
known_keys = {"type", "image_url"}
|
|
423
|
+
extras = _extract_extras(block, known_keys)
|
|
424
|
+
|
|
425
|
+
image_url_known_keys = {"url"}
|
|
426
|
+
image_url_extras = _extract_extras(block["image_url"], image_url_known_keys)
|
|
427
|
+
|
|
428
|
+
all_extras = {**extras}
|
|
429
|
+
for key, value in image_url_extras.items():
|
|
430
|
+
if key == "detail": # Don't rename
|
|
431
|
+
all_extras["detail"] = value
|
|
432
|
+
else:
|
|
433
|
+
all_extras[f"image_url_{key}"] = value
|
|
434
|
+
|
|
435
|
+
return types.create_image_block(
|
|
436
|
+
url=block["image_url"]["url"],
|
|
437
|
+
**all_extras,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# base64-style audio block
|
|
441
|
+
# audio is only represented via raw data, no url or ID option
|
|
442
|
+
if block["type"] == "input_audio":
|
|
443
|
+
known_keys = {"type", "input_audio"}
|
|
444
|
+
extras = _extract_extras(block, known_keys)
|
|
445
|
+
|
|
446
|
+
# Also extract extras from nested audio dict
|
|
447
|
+
audio_known_keys = {"data", "format"}
|
|
448
|
+
audio_extras = _extract_extras(block["input_audio"], audio_known_keys)
|
|
449
|
+
|
|
450
|
+
all_extras = {**extras}
|
|
451
|
+
for key, value in audio_extras.items():
|
|
452
|
+
all_extras[f"audio_{key}"] = value
|
|
453
|
+
|
|
454
|
+
return types.create_audio_block(
|
|
455
|
+
base64=block["input_audio"]["data"],
|
|
456
|
+
mime_type=f"audio/{block['input_audio']['format']}",
|
|
457
|
+
**all_extras,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# id-style file block
|
|
461
|
+
if block.get("type") == "file" and "file_id" in block.get("file", {}):
|
|
462
|
+
known_keys = {"type", "file"}
|
|
463
|
+
extras = _extract_extras(block, known_keys)
|
|
464
|
+
|
|
465
|
+
file_known_keys = {"file_id"}
|
|
466
|
+
file_extras = _extract_extras(block["file"], file_known_keys)
|
|
467
|
+
|
|
468
|
+
all_extras = {**extras}
|
|
469
|
+
for key, value in file_extras.items():
|
|
470
|
+
all_extras[f"file_{key}"] = value
|
|
471
|
+
|
|
472
|
+
return types.create_file_block(
|
|
473
|
+
file_id=block["file"]["file_id"],
|
|
474
|
+
**all_extras,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# base64-style file block
|
|
478
|
+
if (block["type"] == "file") and (
|
|
479
|
+
parsed := _parse_data_uri(block["file"]["file_data"])
|
|
480
|
+
):
|
|
481
|
+
known_keys = {"type", "file"}
|
|
482
|
+
extras = _extract_extras(block, known_keys)
|
|
483
|
+
|
|
484
|
+
file_known_keys = {"file_data", "filename"}
|
|
485
|
+
file_extras = _extract_extras(block["file"], file_known_keys)
|
|
486
|
+
|
|
487
|
+
all_extras = {**extras}
|
|
488
|
+
for key, value in file_extras.items():
|
|
489
|
+
all_extras[f"file_{key}"] = value
|
|
490
|
+
|
|
491
|
+
filename = block["file"].get("filename")
|
|
492
|
+
return types.create_file_block(
|
|
493
|
+
base64=parsed["data"],
|
|
494
|
+
mime_type="application/pdf",
|
|
495
|
+
filename=filename,
|
|
496
|
+
**all_extras,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Escape hatch
|
|
500
|
+
return block
|
|
501
|
+
|
|
502
|
+
|
|
291
503
|
# v1 / Responses
|
|
292
504
|
def _convert_annotation_to_v1(annotation: dict[str, Any]) -> types.Annotation:
|
|
293
505
|
annotation_type = annotation.get("type")
|
|
@@ -83,7 +83,7 @@ The module defines several types of content blocks, including:
|
|
|
83
83
|
|
|
84
84
|
- ``TextContentBlock``: Standard text output.
|
|
85
85
|
- ``Citation``: For annotations that link text output to a source document.
|
|
86
|
-
- ``
|
|
86
|
+
- ``ToolCall``: For function calling.
|
|
87
87
|
- ``ReasoningContentBlock``: To capture a model's thought process.
|
|
88
88
|
- Multimodal data:
|
|
89
89
|
- ``ImageContentBlock``
|
|
@@ -129,10 +129,9 @@ Factory functions offer benefits such as:
|
|
|
129
129
|
|
|
130
130
|
"""
|
|
131
131
|
|
|
132
|
-
import warnings
|
|
133
132
|
from typing import Any, Literal, Optional, Union, get_args, get_type_hints
|
|
134
133
|
|
|
135
|
-
from typing_extensions import NotRequired, TypedDict
|
|
134
|
+
from typing_extensions import NotRequired, TypedDict
|
|
136
135
|
|
|
137
136
|
from langchain_core.utils.utils import ensure_id
|
|
138
137
|
|
|
@@ -834,7 +833,7 @@ class NonStandardContentBlock(TypedDict):
|
|
|
834
833
|
The purpose of this block should be to simply hold a provider-specific payload.
|
|
835
834
|
If a provider's non-standard output includes reasoning and tool calls, it should be
|
|
836
835
|
the adapter's job to parse that payload and emit the corresponding standard
|
|
837
|
-
``ReasoningContentBlock`` and ``
|
|
836
|
+
``ReasoningContentBlock`` and ``ToolCalls``.
|
|
838
837
|
|
|
839
838
|
Has no ``extras`` field, as provider-specific data should be included in the
|
|
840
839
|
``value`` field.
|
|
@@ -966,113 +965,6 @@ def is_data_content_block(block: dict) -> bool:
|
|
|
966
965
|
return False
|
|
967
966
|
|
|
968
967
|
|
|
969
|
-
def is_tool_call_block(block: ContentBlock) -> TypeGuard[ToolCall]:
|
|
970
|
-
"""Type guard to check if a content block is a ``ToolCall``."""
|
|
971
|
-
return block.get("type") == "tool_call"
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
def is_tool_call_chunk(block: ContentBlock) -> TypeGuard[ToolCallChunk]:
|
|
975
|
-
"""Type guard to check if a content block is a ``ToolCallChunk``."""
|
|
976
|
-
return block.get("type") == "tool_call_chunk"
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
def is_text_block(block: ContentBlock) -> TypeGuard[TextContentBlock]:
|
|
980
|
-
"""Type guard to check if a content block is a ``TextContentBlock``."""
|
|
981
|
-
return block.get("type") == "text"
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
def is_reasoning_block(block: ContentBlock) -> TypeGuard[ReasoningContentBlock]:
|
|
985
|
-
"""Type guard to check if a content block is a ``ReasoningContentBlock``."""
|
|
986
|
-
return block.get("type") == "reasoning"
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
def is_invalid_tool_call_block(
|
|
990
|
-
block: ContentBlock,
|
|
991
|
-
) -> TypeGuard[InvalidToolCall]:
|
|
992
|
-
"""Type guard to check if a content block is an ``InvalidToolCall``."""
|
|
993
|
-
return block.get("type") == "invalid_tool_call"
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
def convert_to_openai_image_block(block: dict[str, Any]) -> dict:
|
|
997
|
-
"""Convert ``ImageContentBlock`` to format expected by OpenAI Chat Completions."""
|
|
998
|
-
if "url" in block:
|
|
999
|
-
return {
|
|
1000
|
-
"type": "image_url",
|
|
1001
|
-
"image_url": {
|
|
1002
|
-
"url": block["url"],
|
|
1003
|
-
},
|
|
1004
|
-
}
|
|
1005
|
-
if "base64" in block or block.get("source_type") == "base64":
|
|
1006
|
-
if "mime_type" not in block:
|
|
1007
|
-
error_message = "mime_type key is required for base64 data."
|
|
1008
|
-
raise ValueError(error_message)
|
|
1009
|
-
mime_type = block["mime_type"]
|
|
1010
|
-
base64_data = block["data"] if "data" in block else block["base64"]
|
|
1011
|
-
return {
|
|
1012
|
-
"type": "image_url",
|
|
1013
|
-
"image_url": {
|
|
1014
|
-
"url": f"data:{mime_type};base64,{base64_data}",
|
|
1015
|
-
},
|
|
1016
|
-
}
|
|
1017
|
-
error_message = "Unsupported source type. Only 'url' and 'base64' are supported."
|
|
1018
|
-
raise ValueError(error_message)
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
def convert_to_openai_data_block(block: dict) -> dict:
|
|
1022
|
-
"""Format standard data content block to format expected by OpenAI."""
|
|
1023
|
-
if block["type"] == "image":
|
|
1024
|
-
formatted_block = convert_to_openai_image_block(block)
|
|
1025
|
-
|
|
1026
|
-
elif block["type"] == "file":
|
|
1027
|
-
if "base64" in block or block.get("source_type") == "base64":
|
|
1028
|
-
# Handle v0 format: {"source_type": "base64", "data": "...", ...}
|
|
1029
|
-
# Handle v1 format: {"base64": "...", ...}
|
|
1030
|
-
base64_data = block["data"] if "source_type" in block else block["base64"]
|
|
1031
|
-
file = {"file_data": f"data:{block['mime_type']};base64,{base64_data}"}
|
|
1032
|
-
if filename := block.get("filename"):
|
|
1033
|
-
file["filename"] = filename
|
|
1034
|
-
elif (extras := block.get("extras")) and ("filename" in extras):
|
|
1035
|
-
file["filename"] = extras["filename"]
|
|
1036
|
-
elif (extras := block.get("metadata")) and ("filename" in extras):
|
|
1037
|
-
# Backward compat
|
|
1038
|
-
file["filename"] = extras["filename"]
|
|
1039
|
-
else:
|
|
1040
|
-
warnings.warn(
|
|
1041
|
-
"OpenAI may require a filename for file inputs. Specify a filename "
|
|
1042
|
-
"in the content block: {'type': 'file', 'mime_type': "
|
|
1043
|
-
"'application/pdf', 'base64': '...', 'filename': 'my-pdf'}",
|
|
1044
|
-
stacklevel=1,
|
|
1045
|
-
)
|
|
1046
|
-
formatted_block = {"type": "file", "file": file}
|
|
1047
|
-
elif "file_id" in block or block.get("source_type") == "id":
|
|
1048
|
-
# Handle v0 format: {"source_type": "id", "id": "...", ...}
|
|
1049
|
-
# Handle v1 format: {"file_id": "...", ...}
|
|
1050
|
-
file_id = block["id"] if "source_type" in block else block["file_id"]
|
|
1051
|
-
formatted_block = {"type": "file", "file": {"file_id": file_id}}
|
|
1052
|
-
else:
|
|
1053
|
-
error_msg = "Keys base64 or file_id required for file blocks."
|
|
1054
|
-
raise ValueError(error_msg)
|
|
1055
|
-
|
|
1056
|
-
elif block["type"] == "audio":
|
|
1057
|
-
if "base64" in block or block.get("source_type") == "base64":
|
|
1058
|
-
# Handle v0 format: {"source_type": "base64", "data": "...", ...}
|
|
1059
|
-
# Handle v1 format: {"base64": "...", ...}
|
|
1060
|
-
base64_data = block["data"] if "source_type" in block else block["base64"]
|
|
1061
|
-
audio_format = block["mime_type"].split("/")[-1]
|
|
1062
|
-
formatted_block = {
|
|
1063
|
-
"type": "input_audio",
|
|
1064
|
-
"input_audio": {"data": base64_data, "format": audio_format},
|
|
1065
|
-
}
|
|
1066
|
-
else:
|
|
1067
|
-
error_msg = "Key base64 is required for audio blocks."
|
|
1068
|
-
raise ValueError(error_msg)
|
|
1069
|
-
else:
|
|
1070
|
-
error_msg = f"Block of type {block['type']} is not supported."
|
|
1071
|
-
raise ValueError(error_msg)
|
|
1072
|
-
|
|
1073
|
-
return formatted_block
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
968
|
def create_text_block(
|
|
1077
969
|
text: str,
|
|
1078
970
|
*,
|
langchain_core/messages/utils.py
CHANGED
|
@@ -33,9 +33,11 @@ from pydantic import Discriminator, Field, Tag
|
|
|
33
33
|
from langchain_core.exceptions import ErrorCode, create_message
|
|
34
34
|
from langchain_core.messages.ai import AIMessage, AIMessageChunk
|
|
35
35
|
from langchain_core.messages.base import BaseMessage, BaseMessageChunk
|
|
36
|
+
from langchain_core.messages.block_translators.openai import (
|
|
37
|
+
convert_to_openai_data_block,
|
|
38
|
+
)
|
|
36
39
|
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
|
|
37
40
|
from langchain_core.messages.content import (
|
|
38
|
-
convert_to_openai_data_block,
|
|
39
41
|
is_data_content_block,
|
|
40
42
|
)
|
|
41
43
|
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
|
langchain_core/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
langchain_core-1.0.
|
|
2
|
-
langchain_core-1.0.
|
|
3
|
-
langchain_core-1.0.
|
|
1
|
+
langchain_core-1.0.0a2.dist-info/METADATA,sha256=kN6BukdKVgkYPdefzaRcg-LGqdUjxyfzI1JfwEldMPU,5716
|
|
2
|
+
langchain_core-1.0.0a2.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
|
3
|
+
langchain_core-1.0.0a2.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
4
|
langchain_core/__init__.py,sha256=TgvhxbrjCRVJwr2HddiyHvtH8W94K-uLM6-6ifNIBXo,713
|
|
5
5
|
langchain_core/_api/__init__.py,sha256=WDOMw4faVuscjDCL5ttnRQNienJP_M9vGMmJUXS6L5w,1976
|
|
6
6
|
langchain_core/_api/beta_decorator.py,sha256=uN-N3vGj7-56mNbXw-eh7I-Cvgrt4V4YOoz-7jLQl1Y,9908
|
|
@@ -46,9 +46,9 @@ langchain_core/indexing/api.py,sha256=z620e6bVNUKH_2bbVGrSqQiMVfVAJQl_iyQCSBiMmY
|
|
|
46
46
|
langchain_core/indexing/base.py,sha256=NGqIzktMBlzqK_AN3xF42DC9tP6uM0EJzBr-rJnHDu8,23298
|
|
47
47
|
langchain_core/indexing/in_memory.py,sha256=-qyKjAWJFWxtH_MbUu3JJct0x3R_pbHyHuxA4Cra1nA,2709
|
|
48
48
|
langchain_core/language_models/__init__.py,sha256=j6OXr7CriShFr7BYfCWZ2kOTEZpzvlE7dNDTab75prg,3778
|
|
49
|
-
langchain_core/language_models/_utils.py,sha256=
|
|
49
|
+
langchain_core/language_models/_utils.py,sha256=3-hXk8zeiERVLZ1B-5nzOxet1npu625W_EUn2kmiSVI,10184
|
|
50
50
|
langchain_core/language_models/base.py,sha256=dwn3uiZsvI0zUqALQ0OmpIUFHe3YmOhRI2pEc9qQxmY,14486
|
|
51
|
-
langchain_core/language_models/chat_models.py,sha256=
|
|
51
|
+
langchain_core/language_models/chat_models.py,sha256=na_GWv-tyy4EBhObVvvdA7Sg3BBe9AWNZ3WvZAg88RY,77547
|
|
52
52
|
langchain_core/language_models/fake.py,sha256=h9LhVTkmYLXkJ1_VvsKhqYVpkQsM7eAr9geXF_IVkPs,3772
|
|
53
53
|
langchain_core/language_models/fake_chat_models.py,sha256=rojzv3arvsT0x2RLVJfSpaZ_zQLo18EM5MUnmwktpN4,13690
|
|
54
54
|
langchain_core/language_models/llms.py,sha256=jzhJ1v4tGuuuD9lFZEYfesoE3WhtRNIWI6XKgQjPooc,56803
|
|
@@ -58,7 +58,7 @@ langchain_core/load/load.py,sha256=8Jq62M9QYcW78Iv3J_EKQG6OIAsbthudMM60gqyUjFg,9
|
|
|
58
58
|
langchain_core/load/mapping.py,sha256=nnFXiTdQkfdv41_wP38aWGtpp9svxW6fwVyC3LmRkok,29633
|
|
59
59
|
langchain_core/load/serializable.py,sha256=JIM8GTYYLXBTrRn9zal1tMJOP4z5vs-Hi-aAov6JYtY,11684
|
|
60
60
|
langchain_core/memory.py,sha256=V-ARgyy5_xgvoBfp4WArMrk6NdbBjbqXdGDIbSTI_tA,3631
|
|
61
|
-
langchain_core/messages/__init__.py,sha256=
|
|
61
|
+
langchain_core/messages/__init__.py,sha256=wTOjbQMPUBTC0WX5tQpuW-pHIktPVojIlJY2AbNtgsA,6048
|
|
62
62
|
langchain_core/messages/ai.py,sha256=2V3yqVK5dkeg_roprSWuK2ZWgzlnPVvRV0F98cpTP3Y,24579
|
|
63
63
|
langchain_core/messages/base.py,sha256=TWMnefCv8loiv5LnIBDSw6k_YHfO2uVCI6UWFg8Hpas,14591
|
|
64
64
|
langchain_core/messages/block_translators/__init__.py,sha256=Mo_pXM7a2DCKNvPgp1CStt4EOGHaiNMFNaKFIn01fcA,3102
|
|
@@ -68,17 +68,17 @@ langchain_core/messages/block_translators/bedrock_converse.py,sha256=k13H9VvFPNq
|
|
|
68
68
|
langchain_core/messages/block_translators/google_genai.py,sha256=qsE1KH3byEBftE6BLcjZBta-WuSFVM8oFGC7k99A72Q,1537
|
|
69
69
|
langchain_core/messages/block_translators/google_vertexai.py,sha256=FXiJT2yIB64WZbxkho9k-qLPbcsDK74vwdFZgKik6QM,1594
|
|
70
70
|
langchain_core/messages/block_translators/groq.py,sha256=6r6j9Hy3HS2hOEAZ1V-_hYHGKNPk6KmRrCmbiTTDgrY,1465
|
|
71
|
-
langchain_core/messages/block_translators/langchain_v0.py,sha256=
|
|
71
|
+
langchain_core/messages/block_translators/langchain_v0.py,sha256=NQjsIeP5lrCqKMK-NcAsI66dvVwIvjUegbtkzv9cj3Y,6708
|
|
72
72
|
langchain_core/messages/block_translators/ollama.py,sha256=BuPm47bluNhKseHozaiBNi9He8I4V7Fn038luhBttcg,1483
|
|
73
|
-
langchain_core/messages/block_translators/openai.py,sha256=
|
|
73
|
+
langchain_core/messages/block_translators/openai.py,sha256=1s8T1WbPEkjVEBLU19Qw_oB5hEOh1hRwZpUZ9B2rRVQ,30505
|
|
74
74
|
langchain_core/messages/chat.py,sha256=Vgk3y03F9NP-wKkXAjBDLOtrH43NpEMN2xaWRp6qhRA,2260
|
|
75
|
-
langchain_core/messages/content.py,sha256=
|
|
75
|
+
langchain_core/messages/content.py,sha256=Lfx1O9tdy2zNcFSJM4GVjZpJhpFeQbILKIfyHm70Frw,43022
|
|
76
76
|
langchain_core/messages/function.py,sha256=QO2WgKmJ5nm7QL-xXG11Fmz3qFkHm1lL0k41WjDeEZE,2157
|
|
77
77
|
langchain_core/messages/human.py,sha256=VB8sw1DaNmzrc77T9NYd1QWQYCman6GFOfrlmVBWZMU,2582
|
|
78
78
|
langchain_core/messages/modifier.py,sha256=ch0RedUM_uA7wOEJHk8mkoJSNR0Rli_32BmOfdbS1dU,894
|
|
79
79
|
langchain_core/messages/system.py,sha256=FE2XZ7oHWqqIxOjEOMGEkMO97PqLXwVLa-jL5mvriGE,2388
|
|
80
80
|
langchain_core/messages/tool.py,sha256=5uZsdZg-FbYc5U3v71TaxtrDQf6sT895LHNXZh-CRJ8,12406
|
|
81
|
-
langchain_core/messages/utils.py,sha256=
|
|
81
|
+
langchain_core/messages/utils.py,sha256=GWI-E9YHNMjLXFfNP_WKacQxU5MozmkCKgfUps2edP4,67738
|
|
82
82
|
langchain_core/output_parsers/__init__.py,sha256=R8L0GwY-vD9qvqze3EVELXF6i45IYUJ_FbSfno_IREg,2873
|
|
83
83
|
langchain_core/output_parsers/base.py,sha256=RD0BgBBeNKDUTrEGxnLmA1DuCJowcEAfTB70Y8yqVoc,11168
|
|
84
84
|
langchain_core/output_parsers/format_instructions.py,sha256=8oUbeysnVGvXWyNd5gqXlEL850D31gMTy74GflsuvRU,553
|
|
@@ -180,5 +180,5 @@ langchain_core/vectorstores/__init__.py,sha256=5P0eoeoH5LHab64JjmEeWa6SxX4eMy-et
|
|
|
180
180
|
langchain_core/vectorstores/base.py,sha256=4AR5L6RWuAPEo9DQj9pOIN7UR3Ln45s02pU_Oe8sYsI,42026
|
|
181
181
|
langchain_core/vectorstores/in_memory.py,sha256=lxe2bR-wFtvNN2Ii7EGOh3ON3MwqNRP996eUEek55fA,18076
|
|
182
182
|
langchain_core/vectorstores/utils.py,sha256=DZUUR1xDybHDhmZJsd1V2OEPsYiFVc2nhtD4w8hw9ns,4934
|
|
183
|
-
langchain_core/version.py,sha256=
|
|
184
|
-
langchain_core-1.0.
|
|
183
|
+
langchain_core/version.py,sha256=_B2u_U6ZgDX4uw2fUg69i5bBqHaW-FZKLszrXH4LLHQ,77
|
|
184
|
+
langchain_core-1.0.0a2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|