langchain-core 1.0.0a4__py3-none-any.whl → 1.0.0a6__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/_api/beta_decorator.py +6 -5
- langchain_core/_api/deprecation.py +11 -11
- langchain_core/callbacks/manager.py +2 -2
- langchain_core/callbacks/usage.py +2 -2
- langchain_core/document_loaders/langsmith.py +1 -1
- langchain_core/indexing/api.py +30 -30
- langchain_core/language_models/chat_models.py +1 -1
- langchain_core/language_models/fake_chat_models.py +5 -2
- langchain_core/load/serializable.py +1 -1
- langchain_core/messages/__init__.py +9 -15
- langchain_core/messages/ai.py +75 -9
- langchain_core/messages/base.py +79 -37
- langchain_core/messages/block_translators/__init__.py +26 -3
- langchain_core/messages/block_translators/anthropic.py +143 -128
- langchain_core/messages/block_translators/bedrock_converse.py +15 -1
- langchain_core/messages/block_translators/google_genai.py +502 -20
- langchain_core/messages/block_translators/langchain_v0.py +180 -43
- langchain_core/messages/block_translators/openai.py +224 -42
- langchain_core/messages/chat.py +4 -1
- langchain_core/messages/content.py +56 -112
- langchain_core/messages/function.py +9 -5
- langchain_core/messages/human.py +6 -2
- langchain_core/messages/modifier.py +1 -0
- langchain_core/messages/system.py +9 -2
- langchain_core/messages/tool.py +31 -14
- langchain_core/messages/utils.py +89 -83
- langchain_core/outputs/chat_generation.py +10 -6
- langchain_core/prompt_values.py +6 -2
- langchain_core/prompts/chat.py +6 -3
- langchain_core/prompts/few_shot.py +4 -1
- langchain_core/runnables/base.py +14 -13
- langchain_core/runnables/graph_ascii.py +1 -1
- langchain_core/tools/base.py +2 -2
- langchain_core/tools/convert.py +1 -1
- langchain_core/utils/aiter.py +1 -1
- langchain_core/utils/function_calling.py +5 -6
- langchain_core/utils/iter.py +1 -1
- langchain_core/vectorstores/in_memory.py +5 -5
- langchain_core/version.py +1 -1
- {langchain_core-1.0.0a4.dist-info → langchain_core-1.0.0a6.dist-info}/METADATA +8 -18
- {langchain_core-1.0.0a4.dist-info → langchain_core-1.0.0a6.dist-info}/RECORD +43 -43
- {langchain_core-1.0.0a4.dist-info → langchain_core-1.0.0a6.dist-info}/WHEEL +0 -0
- {langchain_core-1.0.0a4.dist-info → langchain_core-1.0.0a6.dist-info}/entry_points.txt +0 -0
langchain_core/messages/base.py
CHANGED
|
@@ -20,6 +20,31 @@ if TYPE_CHECKING:
|
|
|
20
20
|
from langchain_core.prompts.chat import ChatPromptTemplate
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _extract_reasoning_from_additional_kwargs(
|
|
24
|
+
message: BaseMessage,
|
|
25
|
+
) -> Optional[types.ReasoningContentBlock]:
|
|
26
|
+
"""Extract `reasoning_content` from `additional_kwargs`.
|
|
27
|
+
|
|
28
|
+
Handles reasoning content stored in various formats:
|
|
29
|
+
- `additional_kwargs["reasoning_content"]` (string) - Ollama, DeepSeek, XAI, Groq
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
message: The message to extract reasoning from.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A `ReasoningContentBlock` if reasoning content is found, None otherwise.
|
|
36
|
+
"""
|
|
37
|
+
from langchain_core.messages.content import create_reasoning_block # noqa: PLC0415
|
|
38
|
+
|
|
39
|
+
additional_kwargs = getattr(message, "additional_kwargs", {})
|
|
40
|
+
|
|
41
|
+
reasoning_content = additional_kwargs.get("reasoning_content")
|
|
42
|
+
if reasoning_content is not None and isinstance(reasoning_content, str):
|
|
43
|
+
return create_reasoning_block(reasoning=reasoning_content)
|
|
44
|
+
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
23
48
|
class TextAccessor(str):
|
|
24
49
|
"""String-like object that supports both property and method access patterns.
|
|
25
50
|
|
|
@@ -69,7 +94,7 @@ class TextAccessor(str):
|
|
|
69
94
|
class BaseMessage(Serializable):
|
|
70
95
|
"""Base abstract message class.
|
|
71
96
|
|
|
72
|
-
Messages are the inputs and outputs of
|
|
97
|
+
Messages are the inputs and outputs of ``ChatModel``s.
|
|
73
98
|
"""
|
|
74
99
|
|
|
75
100
|
content: Union[str, list[Union[str, dict]]]
|
|
@@ -80,17 +105,18 @@ class BaseMessage(Serializable):
|
|
|
80
105
|
|
|
81
106
|
For example, for a message from an AI, this could include tool calls as
|
|
82
107
|
encoded by the model provider.
|
|
108
|
+
|
|
83
109
|
"""
|
|
84
110
|
|
|
85
111
|
response_metadata: dict = Field(default_factory=dict)
|
|
86
|
-
"""
|
|
87
|
-
name."""
|
|
112
|
+
"""Examples: response headers, logprobs, token counts, model name."""
|
|
88
113
|
|
|
89
114
|
type: str
|
|
90
115
|
"""The type of the message. Must be a string that is unique to the message type.
|
|
91
116
|
|
|
92
117
|
The purpose of this field is to allow for easy identification of the message type
|
|
93
118
|
when deserializing messages.
|
|
119
|
+
|
|
94
120
|
"""
|
|
95
121
|
|
|
96
122
|
name: Optional[str] = None
|
|
@@ -100,11 +126,15 @@ class BaseMessage(Serializable):
|
|
|
100
126
|
|
|
101
127
|
Usage of this field is optional, and whether it's used or not is up to the
|
|
102
128
|
model implementation.
|
|
129
|
+
|
|
103
130
|
"""
|
|
104
131
|
|
|
105
132
|
id: Optional[str] = Field(default=None, coerce_numbers_to_str=True)
|
|
106
|
-
"""An optional unique identifier for the message.
|
|
107
|
-
|
|
133
|
+
"""An optional unique identifier for the message.
|
|
134
|
+
|
|
135
|
+
This should ideally be provided by the provider/model which created the message.
|
|
136
|
+
|
|
137
|
+
"""
|
|
108
138
|
|
|
109
139
|
model_config = ConfigDict(
|
|
110
140
|
extra="allow",
|
|
@@ -131,7 +161,15 @@ class BaseMessage(Serializable):
|
|
|
131
161
|
content_blocks: Optional[list[types.ContentBlock]] = None,
|
|
132
162
|
**kwargs: Any,
|
|
133
163
|
) -> None:
|
|
134
|
-
"""
|
|
164
|
+
"""Initialize ``BaseMessage``.
|
|
165
|
+
|
|
166
|
+
Specify ``content`` as positional arg or ``content_blocks`` for typing.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
content: The string contents of the message.
|
|
170
|
+
content_blocks: Typed standard content.
|
|
171
|
+
kwargs: Additional arguments to pass to the parent class.
|
|
172
|
+
"""
|
|
135
173
|
if content_blocks is not None:
|
|
136
174
|
super().__init__(content=content_blocks, **kwargs)
|
|
137
175
|
else:
|
|
@@ -139,7 +177,7 @@ class BaseMessage(Serializable):
|
|
|
139
177
|
|
|
140
178
|
@classmethod
|
|
141
179
|
def is_lc_serializable(cls) -> bool:
|
|
142
|
-
"""BaseMessage is serializable.
|
|
180
|
+
"""``BaseMessage`` is serializable.
|
|
143
181
|
|
|
144
182
|
Returns:
|
|
145
183
|
True
|
|
@@ -157,29 +195,12 @@ class BaseMessage(Serializable):
|
|
|
157
195
|
|
|
158
196
|
@property
|
|
159
197
|
def content_blocks(self) -> list[types.ContentBlock]:
|
|
160
|
-
r"""
|
|
161
|
-
|
|
162
|
-
.. important::
|
|
163
|
-
|
|
164
|
-
To use this property correctly, the corresponding ``ChatModel`` must support
|
|
165
|
-
``message_version='v1'`` or higher (and it must be set):
|
|
166
|
-
|
|
167
|
-
.. code-block:: python
|
|
168
|
-
|
|
169
|
-
from langchain.chat_models import init_chat_model
|
|
170
|
-
llm = init_chat_model("...", message_version="v1")
|
|
171
|
-
|
|
172
|
-
# or
|
|
173
|
-
|
|
174
|
-
from langchain-openai import ChatOpenAI
|
|
175
|
-
llm = ChatOpenAI(model="gpt-4o", message_version="v1")
|
|
176
|
-
|
|
177
|
-
Otherwise, the property will perform best-effort parsing to standard types,
|
|
178
|
-
though some content may be misinterpreted.
|
|
198
|
+
r"""Load content blocks from the message content.
|
|
179
199
|
|
|
180
200
|
.. versionadded:: 1.0.0
|
|
181
201
|
|
|
182
|
-
"""
|
|
202
|
+
"""
|
|
203
|
+
# Needed here to avoid circular import, as these classes import BaseMessages
|
|
183
204
|
from langchain_core.messages import content as types # noqa: PLC0415
|
|
184
205
|
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
|
|
185
206
|
_convert_to_v1_from_anthropic_input,
|
|
@@ -187,6 +208,9 @@ class BaseMessage(Serializable):
|
|
|
187
208
|
from langchain_core.messages.block_translators.bedrock_converse import ( # noqa: PLC0415
|
|
188
209
|
_convert_to_v1_from_converse_input,
|
|
189
210
|
)
|
|
211
|
+
from langchain_core.messages.block_translators.google_genai import ( # noqa: PLC0415
|
|
212
|
+
_convert_to_v1_from_genai_input,
|
|
213
|
+
)
|
|
190
214
|
from langchain_core.messages.block_translators.langchain_v0 import ( # noqa: PLC0415
|
|
191
215
|
_convert_v0_multimodal_input_to_v1,
|
|
192
216
|
)
|
|
@@ -195,28 +219,39 @@ class BaseMessage(Serializable):
|
|
|
195
219
|
)
|
|
196
220
|
|
|
197
221
|
blocks: list[types.ContentBlock] = []
|
|
198
|
-
|
|
199
|
-
# First pass: convert to standard blocks
|
|
200
222
|
content = (
|
|
223
|
+
# Transpose string content to list, otherwise assumed to be list
|
|
201
224
|
[self.content]
|
|
202
225
|
if isinstance(self.content, str) and self.content
|
|
203
226
|
else self.content
|
|
204
227
|
)
|
|
205
228
|
for item in content:
|
|
206
229
|
if isinstance(item, str):
|
|
230
|
+
# Plain string content is treated as a text block
|
|
207
231
|
blocks.append({"type": "text", "text": item})
|
|
208
232
|
elif isinstance(item, dict):
|
|
209
233
|
item_type = item.get("type")
|
|
210
234
|
if item_type not in types.KNOWN_BLOCK_TYPES:
|
|
235
|
+
# Handle all provider-specific or None type blocks as non-standard -
|
|
236
|
+
# we'll come back to these later
|
|
211
237
|
blocks.append({"type": "non_standard", "value": item})
|
|
212
238
|
else:
|
|
239
|
+
# Guard against v0 blocks that share the same `type` keys
|
|
240
|
+
if "source_type" in item:
|
|
241
|
+
blocks.append({"type": "non_standard", "value": item})
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# This can't be a v0 block (since they require `source_type`),
|
|
245
|
+
# so it's a known v1 block type
|
|
213
246
|
blocks.append(cast("types.ContentBlock", item))
|
|
214
247
|
|
|
215
|
-
# Subsequent passes: attempt to unpack non-standard blocks
|
|
248
|
+
# Subsequent passes: attempt to unpack non-standard blocks.
|
|
249
|
+
# This is the last stop - if we can't parse it here, it is left as non-standard
|
|
216
250
|
for parsing_step in [
|
|
217
251
|
_convert_v0_multimodal_input_to_v1,
|
|
218
252
|
_convert_to_v1_from_chat_completions_input,
|
|
219
253
|
_convert_to_v1_from_anthropic_input,
|
|
254
|
+
_convert_to_v1_from_genai_input,
|
|
220
255
|
_convert_to_v1_from_converse_input,
|
|
221
256
|
]:
|
|
222
257
|
blocks = parsing_step(blocks)
|
|
@@ -234,6 +269,7 @@ class BaseMessage(Serializable):
|
|
|
234
269
|
|
|
235
270
|
Returns:
|
|
236
271
|
The text content of the message.
|
|
272
|
+
|
|
237
273
|
"""
|
|
238
274
|
if isinstance(self.content, str):
|
|
239
275
|
text_value = self.content
|
|
@@ -277,6 +313,7 @@ class BaseMessage(Serializable):
|
|
|
277
313
|
|
|
278
314
|
Returns:
|
|
279
315
|
A pretty representation of the message.
|
|
316
|
+
|
|
280
317
|
"""
|
|
281
318
|
title = get_msg_title_repr(self.type.title() + " Message", bold=html)
|
|
282
319
|
# TODO: handle non-string content.
|
|
@@ -296,11 +333,12 @@ def merge_content(
|
|
|
296
333
|
"""Merge multiple message contents.
|
|
297
334
|
|
|
298
335
|
Args:
|
|
299
|
-
first_content: The first content
|
|
300
|
-
contents: The other
|
|
336
|
+
first_content: The first ``content``. Can be a string or a list.
|
|
337
|
+
contents: The other ``content``s. Can be a string or a list.
|
|
301
338
|
|
|
302
339
|
Returns:
|
|
303
340
|
The merged content.
|
|
341
|
+
|
|
304
342
|
"""
|
|
305
343
|
merged: Union[str, list[Union[str, dict]]]
|
|
306
344
|
merged = "" if first_content is None else first_content
|
|
@@ -352,9 +390,10 @@ class BaseMessageChunk(BaseMessage):
|
|
|
352
390
|
|
|
353
391
|
For example,
|
|
354
392
|
|
|
355
|
-
|
|
393
|
+
``AIMessageChunk(content="Hello") + AIMessageChunk(content=" World")``
|
|
394
|
+
|
|
395
|
+
will give ``AIMessageChunk(content="Hello World")``
|
|
356
396
|
|
|
357
|
-
will give `AIMessageChunk(content="Hello World")`
|
|
358
397
|
"""
|
|
359
398
|
if isinstance(other, BaseMessageChunk):
|
|
360
399
|
# If both are (subclasses of) BaseMessageChunk,
|
|
@@ -402,8 +441,9 @@ def message_to_dict(message: BaseMessage) -> dict:
|
|
|
402
441
|
message: Message to convert.
|
|
403
442
|
|
|
404
443
|
Returns:
|
|
405
|
-
Message as a dict. The dict will have a
|
|
406
|
-
and a
|
|
444
|
+
Message as a dict. The dict will have a ``type`` key with the message type
|
|
445
|
+
and a ``data`` key with the message data as a dict.
|
|
446
|
+
|
|
407
447
|
"""
|
|
408
448
|
return {"type": message.type, "data": message.model_dump()}
|
|
409
449
|
|
|
@@ -412,10 +452,11 @@ def messages_to_dict(messages: Sequence[BaseMessage]) -> list[dict]:
|
|
|
412
452
|
"""Convert a sequence of Messages to a list of dictionaries.
|
|
413
453
|
|
|
414
454
|
Args:
|
|
415
|
-
messages: Sequence of messages (as
|
|
455
|
+
messages: Sequence of messages (as ``BaseMessage``s) to convert.
|
|
416
456
|
|
|
417
457
|
Returns:
|
|
418
458
|
List of messages as dicts.
|
|
459
|
+
|
|
419
460
|
"""
|
|
420
461
|
return [message_to_dict(m) for m in messages]
|
|
421
462
|
|
|
@@ -429,6 +470,7 @@ def get_msg_title_repr(title: str, *, bold: bool = False) -> str:
|
|
|
429
470
|
|
|
430
471
|
Returns:
|
|
431
472
|
The title representation.
|
|
473
|
+
|
|
432
474
|
"""
|
|
433
475
|
padded = " " + title + " "
|
|
434
476
|
sep_len = (80 - len(padded)) // 2
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
"""Derivations of standard content blocks from provider content.
|
|
1
|
+
"""Derivations of standard content blocks from provider content.
|
|
2
|
+
|
|
3
|
+
``AIMessage`` will first attempt to use a provider-specific translator if
|
|
4
|
+
``model_provider`` is set in ``response_metadata`` on the message. Consequently, each
|
|
5
|
+
provider translator must handle all possible content response types from the provider,
|
|
6
|
+
including text.
|
|
7
|
+
|
|
8
|
+
If no provider is set, or if the provider does not have a registered translator,
|
|
9
|
+
``AIMessage`` will fall back to best-effort parsing of the content into blocks using
|
|
10
|
+
the implementation in ``BaseMessage``.
|
|
11
|
+
"""
|
|
2
12
|
|
|
3
13
|
from __future__ import annotations
|
|
4
14
|
|
|
@@ -10,6 +20,18 @@ if TYPE_CHECKING:
|
|
|
10
20
|
|
|
11
21
|
# Provider to translator mapping
|
|
12
22
|
PROVIDER_TRANSLATORS: dict[str, dict[str, Callable[..., list[types.ContentBlock]]]] = {}
|
|
23
|
+
"""Map model provider names to translator functions.
|
|
24
|
+
|
|
25
|
+
The dictionary maps provider names (e.g. ``'openai'``, ``'anthropic'``) to another
|
|
26
|
+
dictionary with two keys:
|
|
27
|
+
- ``'translate_content'``: Function to translate ``AIMessage`` content.
|
|
28
|
+
- ``'translate_content_chunk'``: Function to translate ``AIMessageChunk`` content.
|
|
29
|
+
|
|
30
|
+
When calling `.content_blocks` on an ``AIMessage`` or ``AIMessageChunk``, if
|
|
31
|
+
``model_provider`` is set in ``response_metadata``, the corresponding translator
|
|
32
|
+
functions will be used to parse the content into blocks. Otherwise, best-effort parsing
|
|
33
|
+
in ``BaseMessage`` will be used.
|
|
34
|
+
"""
|
|
13
35
|
|
|
14
36
|
|
|
15
37
|
def register_translator(
|
|
@@ -17,7 +39,7 @@ def register_translator(
|
|
|
17
39
|
translate_content: Callable[[AIMessage], list[types.ContentBlock]],
|
|
18
40
|
translate_content_chunk: Callable[[AIMessageChunk], list[types.ContentBlock]],
|
|
19
41
|
) -> None:
|
|
20
|
-
"""Register content translators for a provider
|
|
42
|
+
"""Register content translators for a provider in `PROVIDER_TRANSLATORS`.
|
|
21
43
|
|
|
22
44
|
Args:
|
|
23
45
|
provider: The model provider name (e.g. ``'openai'``, ``'anthropic'``).
|
|
@@ -40,7 +62,8 @@ def get_translator(
|
|
|
40
62
|
|
|
41
63
|
Returns:
|
|
42
64
|
Dictionary with ``'translate_content'`` and ``'translate_content_chunk'``
|
|
43
|
-
functions, or None if no translator is registered for the provider.
|
|
65
|
+
functions, or None if no translator is registered for the provider. In such
|
|
66
|
+
case, best-effort parsing in ``BaseMessage`` will be used.
|
|
44
67
|
"""
|
|
45
68
|
return PROVIDER_TRANSLATORS.get(provider)
|
|
46
69
|
|
|
@@ -29,7 +29,21 @@ def _populate_extras(
|
|
|
29
29
|
def _convert_to_v1_from_anthropic_input(
|
|
30
30
|
content: list[types.ContentBlock],
|
|
31
31
|
) -> list[types.ContentBlock]:
|
|
32
|
-
"""
|
|
32
|
+
"""Convert Anthropic format blocks to v1 format.
|
|
33
|
+
|
|
34
|
+
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
|
|
35
|
+
block as a ``'non_standard'`` block with the original block stored in the ``value``
|
|
36
|
+
field. This function attempts to unpack those blocks and convert any blocks that
|
|
37
|
+
might be Anthropic format to v1 ContentBlocks.
|
|
38
|
+
|
|
39
|
+
If conversion fails, the block is left as a ``'non_standard'`` block.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
content: List of content blocks to process.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Updated list with Anthropic blocks converted to v1 format.
|
|
46
|
+
"""
|
|
33
47
|
|
|
34
48
|
def _iter_blocks() -> Iterable[types.ContentBlock]:
|
|
35
49
|
blocks: list[dict[str, Any]] = [
|
|
@@ -270,159 +284,160 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
|
|
|
270
284
|
tool_call_block["index"] = block["index"]
|
|
271
285
|
yield tool_call_block
|
|
272
286
|
|
|
273
|
-
elif (
|
|
274
|
-
|
|
275
|
-
and isinstance(message, AIMessageChunk)
|
|
276
|
-
and len(message.tool_call_chunks) == 1
|
|
287
|
+
elif block_type == "input_json_delta" and isinstance(
|
|
288
|
+
message, AIMessageChunk
|
|
277
289
|
):
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
if len(message.tool_call_chunks) == 1:
|
|
291
|
+
tool_call_chunk = (
|
|
292
|
+
message.tool_call_chunks[0].copy() # type: ignore[assignment]
|
|
293
|
+
)
|
|
294
|
+
if "type" not in tool_call_chunk:
|
|
295
|
+
tool_call_chunk["type"] = "tool_call_chunk"
|
|
296
|
+
yield tool_call_chunk
|
|
284
297
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
298
|
+
else:
|
|
299
|
+
server_tool_call_chunk: types.ServerToolCallChunk = {
|
|
300
|
+
"type": "server_tool_call_chunk",
|
|
301
|
+
"args": block.get("partial_json", ""),
|
|
302
|
+
}
|
|
303
|
+
if "index" in block:
|
|
304
|
+
server_tool_call_chunk["index"] = block["index"]
|
|
305
|
+
yield server_tool_call_chunk
|
|
288
306
|
|
|
289
|
-
|
|
290
|
-
|
|
307
|
+
elif block_type == "server_tool_use":
|
|
308
|
+
if block.get("name") == "code_execution":
|
|
309
|
+
server_tool_use_name = "code_interpreter"
|
|
310
|
+
else:
|
|
311
|
+
server_tool_use_name = block.get("name", "")
|
|
312
|
+
if (
|
|
313
|
+
isinstance(message, AIMessageChunk)
|
|
314
|
+
and block.get("input") == {}
|
|
315
|
+
and "partial_json" not in block
|
|
316
|
+
and message.chunk_position != "last"
|
|
317
|
+
):
|
|
318
|
+
# First chunk in a stream
|
|
319
|
+
server_tool_call_chunk = {
|
|
320
|
+
"type": "server_tool_call_chunk",
|
|
321
|
+
"name": server_tool_use_name,
|
|
322
|
+
"args": "",
|
|
323
|
+
"id": block.get("id", ""),
|
|
324
|
+
}
|
|
325
|
+
if "index" in block:
|
|
326
|
+
server_tool_call_chunk["index"] = block["index"]
|
|
327
|
+
known_fields = {"type", "name", "input", "id", "index"}
|
|
328
|
+
_populate_extras(server_tool_call_chunk, block, known_fields)
|
|
329
|
+
yield server_tool_call_chunk
|
|
330
|
+
else:
|
|
331
|
+
server_tool_call: types.ServerToolCall = {
|
|
332
|
+
"type": "server_tool_call",
|
|
333
|
+
"name": server_tool_use_name,
|
|
334
|
+
"args": block.get("input", {}),
|
|
335
|
+
"id": block.get("id", ""),
|
|
336
|
+
}
|
|
291
337
|
|
|
292
|
-
|
|
338
|
+
if block.get("input") == {} and "partial_json" in block:
|
|
293
339
|
try:
|
|
294
340
|
input_ = json.loads(block["partial_json"])
|
|
295
|
-
if isinstance(input_, dict)
|
|
296
|
-
|
|
341
|
+
if isinstance(input_, dict):
|
|
342
|
+
server_tool_call["args"] = input_
|
|
297
343
|
except json.JSONDecodeError:
|
|
298
344
|
pass
|
|
299
345
|
|
|
300
|
-
if "id" in block:
|
|
301
|
-
web_search_call["id"] = block["id"]
|
|
302
346
|
if "index" in block:
|
|
303
|
-
|
|
304
|
-
known_fields = {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
elif block.get("name") == "code_execution":
|
|
313
|
-
code_interpreter_call: types.CodeInterpreterCall = {
|
|
314
|
-
"type": "code_interpreter_call"
|
|
347
|
+
server_tool_call["index"] = block["index"]
|
|
348
|
+
known_fields = {
|
|
349
|
+
"type",
|
|
350
|
+
"name",
|
|
351
|
+
"input",
|
|
352
|
+
"partial_json",
|
|
353
|
+
"id",
|
|
354
|
+
"index",
|
|
315
355
|
}
|
|
356
|
+
_populate_extras(server_tool_call, block, known_fields)
|
|
316
357
|
|
|
317
|
-
|
|
318
|
-
|
|
358
|
+
yield server_tool_call
|
|
359
|
+
|
|
360
|
+
elif block_type == "mcp_tool_use":
|
|
361
|
+
if (
|
|
362
|
+
isinstance(message, AIMessageChunk)
|
|
363
|
+
and block.get("input") == {}
|
|
364
|
+
and "partial_json" not in block
|
|
365
|
+
and message.chunk_position != "last"
|
|
366
|
+
):
|
|
367
|
+
# First chunk in a stream
|
|
368
|
+
server_tool_call_chunk = {
|
|
369
|
+
"type": "server_tool_call_chunk",
|
|
370
|
+
"name": "remote_mcp",
|
|
371
|
+
"args": "",
|
|
372
|
+
"id": block.get("id", ""),
|
|
373
|
+
}
|
|
374
|
+
if "name" in block:
|
|
375
|
+
server_tool_call_chunk["extras"] = {"tool_name": block["name"]}
|
|
376
|
+
known_fields = {"type", "name", "input", "id", "index"}
|
|
377
|
+
_populate_extras(server_tool_call_chunk, block, known_fields)
|
|
378
|
+
if "index" in block:
|
|
379
|
+
server_tool_call_chunk["index"] = block["index"]
|
|
380
|
+
yield server_tool_call_chunk
|
|
381
|
+
else:
|
|
382
|
+
server_tool_call = {
|
|
383
|
+
"type": "server_tool_call",
|
|
384
|
+
"name": "remote_mcp",
|
|
385
|
+
"args": block.get("input", {}),
|
|
386
|
+
"id": block.get("id", ""),
|
|
387
|
+
}
|
|
319
388
|
|
|
320
|
-
|
|
389
|
+
if block.get("input") == {} and "partial_json" in block:
|
|
321
390
|
try:
|
|
322
391
|
input_ = json.loads(block["partial_json"])
|
|
323
|
-
if isinstance(input_, dict)
|
|
324
|
-
|
|
392
|
+
if isinstance(input_, dict):
|
|
393
|
+
server_tool_call["args"] = input_
|
|
325
394
|
except json.JSONDecodeError:
|
|
326
395
|
pass
|
|
327
396
|
|
|
328
|
-
if "
|
|
329
|
-
|
|
397
|
+
if "name" in block:
|
|
398
|
+
server_tool_call["extras"] = {"tool_name": block["name"]}
|
|
399
|
+
known_fields = {
|
|
400
|
+
"type",
|
|
401
|
+
"name",
|
|
402
|
+
"input",
|
|
403
|
+
"partial_json",
|
|
404
|
+
"id",
|
|
405
|
+
"index",
|
|
406
|
+
}
|
|
407
|
+
_populate_extras(server_tool_call, block, known_fields)
|
|
330
408
|
if "index" in block:
|
|
331
|
-
|
|
332
|
-
known_fields = {"type", "name", "input", "id", "index"}
|
|
333
|
-
for key, value in block.items():
|
|
334
|
-
if key not in known_fields:
|
|
335
|
-
if "extras" not in code_interpreter_call:
|
|
336
|
-
code_interpreter_call["extras"] = {}
|
|
337
|
-
code_interpreter_call["extras"][key] = value
|
|
338
|
-
yield code_interpreter_call
|
|
409
|
+
server_tool_call["index"] = block["index"]
|
|
339
410
|
|
|
340
|
-
|
|
341
|
-
new_block: types.NonStandardContentBlock = {
|
|
342
|
-
"type": "non_standard",
|
|
343
|
-
"value": block,
|
|
344
|
-
}
|
|
345
|
-
if "index" in new_block["value"]:
|
|
346
|
-
new_block["index"] = new_block["value"].pop("index")
|
|
347
|
-
yield new_block
|
|
348
|
-
|
|
349
|
-
elif block_type == "web_search_tool_result":
|
|
350
|
-
web_search_result: types.WebSearchResult = {"type": "web_search_result"}
|
|
351
|
-
if "tool_use_id" in block:
|
|
352
|
-
web_search_result["id"] = block["tool_use_id"]
|
|
353
|
-
if "index" in block:
|
|
354
|
-
web_search_result["index"] = block["index"]
|
|
355
|
-
|
|
356
|
-
if web_search_result_content := block.get("content", []):
|
|
357
|
-
if "extras" not in web_search_result:
|
|
358
|
-
web_search_result["extras"] = {}
|
|
359
|
-
urls = []
|
|
360
|
-
extra_content = []
|
|
361
|
-
for result_content in web_search_result_content:
|
|
362
|
-
if isinstance(result_content, dict):
|
|
363
|
-
if "url" in result_content:
|
|
364
|
-
urls.append(result_content["url"])
|
|
365
|
-
extra_content.append(result_content)
|
|
366
|
-
web_search_result["extras"]["content"] = extra_content
|
|
367
|
-
if urls:
|
|
368
|
-
web_search_result["urls"] = urls
|
|
369
|
-
yield web_search_result
|
|
370
|
-
|
|
371
|
-
elif block_type == "code_execution_tool_result":
|
|
372
|
-
code_interpreter_result: types.CodeInterpreterResult = {
|
|
373
|
-
"type": "code_interpreter_result",
|
|
374
|
-
"output": [],
|
|
375
|
-
}
|
|
376
|
-
if "tool_use_id" in block:
|
|
377
|
-
code_interpreter_result["id"] = block["tool_use_id"]
|
|
378
|
-
if "index" in block:
|
|
379
|
-
code_interpreter_result["index"] = block["index"]
|
|
411
|
+
yield server_tool_call
|
|
380
412
|
|
|
381
|
-
|
|
382
|
-
|
|
413
|
+
elif block_type and block_type.endswith("_tool_result"):
|
|
414
|
+
server_tool_result: types.ServerToolResult = {
|
|
415
|
+
"type": "server_tool_result",
|
|
416
|
+
"tool_call_id": block.get("tool_use_id", ""),
|
|
417
|
+
"status": "success",
|
|
418
|
+
"extras": {"block_type": block_type},
|
|
383
419
|
}
|
|
420
|
+
if output := block.get("content", []):
|
|
421
|
+
server_tool_result["output"] = output
|
|
422
|
+
if isinstance(output, dict) and output.get(
|
|
423
|
+
"error_code" # web_search, code_interpreter
|
|
424
|
+
):
|
|
425
|
+
server_tool_result["status"] = "error"
|
|
426
|
+
if block.get("is_error"): # mcp_tool_result
|
|
427
|
+
server_tool_result["status"] = "error"
|
|
428
|
+
if "index" in block:
|
|
429
|
+
server_tool_result["index"] = block["index"]
|
|
384
430
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if "return_code" in code_execution_content:
|
|
388
|
-
code_interpreter_output["return_code"] = code_execution_content[
|
|
389
|
-
"return_code"
|
|
390
|
-
]
|
|
391
|
-
if "stdout" in code_execution_content:
|
|
392
|
-
code_interpreter_output["stdout"] = code_execution_content[
|
|
393
|
-
"stdout"
|
|
394
|
-
]
|
|
395
|
-
if stderr := code_execution_content.get("stderr"):
|
|
396
|
-
code_interpreter_output["stderr"] = stderr
|
|
397
|
-
if (
|
|
398
|
-
output := code_interpreter_output.get("content")
|
|
399
|
-
) and isinstance(output, list):
|
|
400
|
-
if "extras" not in code_interpreter_result:
|
|
401
|
-
code_interpreter_result["extras"] = {}
|
|
402
|
-
code_interpreter_result["extras"]["content"] = output
|
|
403
|
-
for output_block in output:
|
|
404
|
-
if "file_id" in output_block:
|
|
405
|
-
if "file_ids" not in code_interpreter_output:
|
|
406
|
-
code_interpreter_output["file_ids"] = []
|
|
407
|
-
code_interpreter_output["file_ids"].append(
|
|
408
|
-
output_block["file_id"]
|
|
409
|
-
)
|
|
410
|
-
code_interpreter_result["output"].append(code_interpreter_output)
|
|
411
|
-
|
|
412
|
-
elif (
|
|
413
|
-
code_execution_content.get("type")
|
|
414
|
-
== "code_execution_tool_result_error"
|
|
415
|
-
):
|
|
416
|
-
if "extras" not in code_interpreter_result:
|
|
417
|
-
code_interpreter_result["extras"] = {}
|
|
418
|
-
code_interpreter_result["extras"]["error_code"] = (
|
|
419
|
-
code_execution_content.get("error_code")
|
|
420
|
-
)
|
|
431
|
+
known_fields = {"type", "tool_use_id", "content", "is_error", "index"}
|
|
432
|
+
_populate_extras(server_tool_result, block, known_fields)
|
|
421
433
|
|
|
422
|
-
yield
|
|
434
|
+
yield server_tool_result
|
|
423
435
|
|
|
424
436
|
else:
|
|
425
|
-
new_block = {
|
|
437
|
+
new_block: types.NonStandardContentBlock = {
|
|
438
|
+
"type": "non_standard",
|
|
439
|
+
"value": block,
|
|
440
|
+
}
|
|
426
441
|
if "index" in new_block["value"]:
|
|
427
442
|
new_block["index"] = new_block["value"].pop("index")
|
|
428
443
|
yield new_block
|
|
@@ -33,7 +33,21 @@ def _populate_extras(
|
|
|
33
33
|
def _convert_to_v1_from_converse_input(
|
|
34
34
|
content: list[types.ContentBlock],
|
|
35
35
|
) -> list[types.ContentBlock]:
|
|
36
|
-
"""
|
|
36
|
+
"""Convert Bedrock Converse format blocks to v1 format.
|
|
37
|
+
|
|
38
|
+
During the `.content_blocks` parsing process, we wrap blocks not recognized as a v1
|
|
39
|
+
block as a ``'non_standard'`` block with the original block stored in the ``value``
|
|
40
|
+
field. This function attempts to unpack those blocks and convert any blocks that
|
|
41
|
+
might be Converse format to v1 ContentBlocks.
|
|
42
|
+
|
|
43
|
+
If conversion fails, the block is left as a ``'non_standard'`` block.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
content: List of content blocks to process.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Updated list with Converse blocks converted to v1 format.
|
|
50
|
+
"""
|
|
37
51
|
|
|
38
52
|
def _iter_blocks() -> Iterable[types.ContentBlock]:
|
|
39
53
|
blocks: list[dict[str, Any]] = [
|