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.
Files changed (43) hide show
  1. langchain_core/_api/beta_decorator.py +6 -5
  2. langchain_core/_api/deprecation.py +11 -11
  3. langchain_core/callbacks/manager.py +2 -2
  4. langchain_core/callbacks/usage.py +2 -2
  5. langchain_core/document_loaders/langsmith.py +1 -1
  6. langchain_core/indexing/api.py +30 -30
  7. langchain_core/language_models/chat_models.py +1 -1
  8. langchain_core/language_models/fake_chat_models.py +5 -2
  9. langchain_core/load/serializable.py +1 -1
  10. langchain_core/messages/__init__.py +9 -15
  11. langchain_core/messages/ai.py +75 -9
  12. langchain_core/messages/base.py +79 -37
  13. langchain_core/messages/block_translators/__init__.py +26 -3
  14. langchain_core/messages/block_translators/anthropic.py +143 -128
  15. langchain_core/messages/block_translators/bedrock_converse.py +15 -1
  16. langchain_core/messages/block_translators/google_genai.py +502 -20
  17. langchain_core/messages/block_translators/langchain_v0.py +180 -43
  18. langchain_core/messages/block_translators/openai.py +224 -42
  19. langchain_core/messages/chat.py +4 -1
  20. langchain_core/messages/content.py +56 -112
  21. langchain_core/messages/function.py +9 -5
  22. langchain_core/messages/human.py +6 -2
  23. langchain_core/messages/modifier.py +1 -0
  24. langchain_core/messages/system.py +9 -2
  25. langchain_core/messages/tool.py +31 -14
  26. langchain_core/messages/utils.py +89 -83
  27. langchain_core/outputs/chat_generation.py +10 -6
  28. langchain_core/prompt_values.py +6 -2
  29. langchain_core/prompts/chat.py +6 -3
  30. langchain_core/prompts/few_shot.py +4 -1
  31. langchain_core/runnables/base.py +14 -13
  32. langchain_core/runnables/graph_ascii.py +1 -1
  33. langchain_core/tools/base.py +2 -2
  34. langchain_core/tools/convert.py +1 -1
  35. langchain_core/utils/aiter.py +1 -1
  36. langchain_core/utils/function_calling.py +5 -6
  37. langchain_core/utils/iter.py +1 -1
  38. langchain_core/vectorstores/in_memory.py +5 -5
  39. langchain_core/version.py +1 -1
  40. {langchain_core-1.0.0a4.dist-info → langchain_core-1.0.0a6.dist-info}/METADATA +8 -18
  41. {langchain_core-1.0.0a4.dist-info → langchain_core-1.0.0a6.dist-info}/RECORD +43 -43
  42. {langchain_core-1.0.0a4.dist-info → langchain_core-1.0.0a6.dist-info}/WHEEL +0 -0
  43. {langchain_core-1.0.0a4.dist-info → langchain_core-1.0.0a6.dist-info}/entry_points.txt +0 -0
@@ -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 ChatModels.
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
- """Response metadata. For example: response headers, logprobs, token counts, model
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. This should ideally be
107
- provided by the provider/model which created the message."""
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
- """Specify ``content`` as positional arg or ``content_blocks`` for typing."""
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"""Return ``content`` as a list of standardized :class:`~langchain_core.messages.content.ContentBlock`\s.
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
- """ # noqa: E501
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. Can be a string or a list.
300
- contents: The other contents. Can be a string or a list.
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
- `AIMessageChunk(content="Hello") + AIMessageChunk(content=" World")`
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 "type" key with the message type
406
- and a "data" key with the message data as a dict.
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 BaseMessages) to convert.
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
- """Attempt to unpack non-standard blocks."""
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
- block_type == "input_json_delta"
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
- tool_call_chunk = (
279
- message.tool_call_chunks[0].copy() # type: ignore[assignment]
280
- )
281
- if "type" not in tool_call_chunk:
282
- tool_call_chunk["type"] = "tool_call_chunk"
283
- yield tool_call_chunk
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
- elif block_type == "server_tool_use":
286
- if block.get("name") == "web_search":
287
- web_search_call: types.WebSearchCall = {"type": "web_search_call"}
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
- if query := block.get("input", {}).get("query"):
290
- web_search_call["query"] = query
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
- elif block.get("input") == {} and "partial_json" in block:
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) and "query" in input_:
296
- web_search_call["query"] = input_["query"]
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
- web_search_call["index"] = block["index"]
304
- known_fields = {"type", "name", "input", "id", "index"}
305
- for key, value in block.items():
306
- if key not in known_fields:
307
- if "extras" not in web_search_call:
308
- web_search_call["extras"] = {}
309
- web_search_call["extras"][key] = value
310
- yield web_search_call
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
- if code := block.get("input", {}).get("code"):
318
- code_interpreter_call["code"] = code
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
- elif block.get("input") == {} and "partial_json" in block:
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) and "code" in input_:
324
- code_interpreter_call["code"] = input_["code"]
392
+ if isinstance(input_, dict):
393
+ server_tool_call["args"] = input_
325
394
  except json.JSONDecodeError:
326
395
  pass
327
396
 
328
- if "id" in block:
329
- code_interpreter_call["id"] = block["id"]
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
- code_interpreter_call["index"] = block["index"]
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
- else:
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
- code_interpreter_output: types.CodeInterpreterOutput = {
382
- "type": "code_interpreter_output"
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
- code_execution_content = block.get("content", {})
386
- if code_execution_content.get("type") == "code_execution_result":
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 code_interpreter_result
434
+ yield server_tool_result
423
435
 
424
436
  else:
425
- new_block = {"type": "non_standard", "value": 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
- """Attempt to unpack non-standard blocks."""
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]] = [