langchain-core 0.3.79__py3-none-any.whl → 1.0.0__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.

Files changed (165) hide show
  1. langchain_core/__init__.py +1 -1
  2. langchain_core/_api/__init__.py +3 -4
  3. langchain_core/_api/beta_decorator.py +23 -26
  4. langchain_core/_api/deprecation.py +52 -65
  5. langchain_core/_api/path.py +3 -6
  6. langchain_core/_import_utils.py +3 -4
  7. langchain_core/agents.py +19 -19
  8. langchain_core/caches.py +53 -63
  9. langchain_core/callbacks/__init__.py +1 -8
  10. langchain_core/callbacks/base.py +323 -334
  11. langchain_core/callbacks/file.py +44 -44
  12. langchain_core/callbacks/manager.py +441 -507
  13. langchain_core/callbacks/stdout.py +29 -30
  14. langchain_core/callbacks/streaming_stdout.py +32 -32
  15. langchain_core/callbacks/usage.py +60 -57
  16. langchain_core/chat_history.py +48 -63
  17. langchain_core/document_loaders/base.py +23 -23
  18. langchain_core/document_loaders/langsmith.py +37 -37
  19. langchain_core/documents/__init__.py +0 -1
  20. langchain_core/documents/base.py +62 -65
  21. langchain_core/documents/compressor.py +4 -4
  22. langchain_core/documents/transformers.py +28 -29
  23. langchain_core/embeddings/fake.py +50 -54
  24. langchain_core/example_selectors/length_based.py +1 -1
  25. langchain_core/example_selectors/semantic_similarity.py +21 -25
  26. langchain_core/exceptions.py +10 -11
  27. langchain_core/globals.py +3 -151
  28. langchain_core/indexing/api.py +61 -66
  29. langchain_core/indexing/base.py +58 -58
  30. langchain_core/indexing/in_memory.py +3 -3
  31. langchain_core/language_models/__init__.py +14 -27
  32. langchain_core/language_models/_utils.py +270 -84
  33. langchain_core/language_models/base.py +55 -162
  34. langchain_core/language_models/chat_models.py +442 -402
  35. langchain_core/language_models/fake.py +11 -11
  36. langchain_core/language_models/fake_chat_models.py +61 -39
  37. langchain_core/language_models/llms.py +123 -231
  38. langchain_core/load/dump.py +4 -5
  39. langchain_core/load/load.py +18 -28
  40. langchain_core/load/mapping.py +2 -4
  41. langchain_core/load/serializable.py +39 -40
  42. langchain_core/messages/__init__.py +61 -22
  43. langchain_core/messages/ai.py +368 -163
  44. langchain_core/messages/base.py +214 -43
  45. langchain_core/messages/block_translators/__init__.py +111 -0
  46. langchain_core/messages/block_translators/anthropic.py +470 -0
  47. langchain_core/messages/block_translators/bedrock.py +94 -0
  48. langchain_core/messages/block_translators/bedrock_converse.py +297 -0
  49. langchain_core/messages/block_translators/google_genai.py +530 -0
  50. langchain_core/messages/block_translators/google_vertexai.py +21 -0
  51. langchain_core/messages/block_translators/groq.py +143 -0
  52. langchain_core/messages/block_translators/langchain_v0.py +301 -0
  53. langchain_core/messages/block_translators/openai.py +1010 -0
  54. langchain_core/messages/chat.py +2 -6
  55. langchain_core/messages/content.py +1423 -0
  56. langchain_core/messages/function.py +6 -10
  57. langchain_core/messages/human.py +41 -38
  58. langchain_core/messages/modifier.py +2 -2
  59. langchain_core/messages/system.py +38 -28
  60. langchain_core/messages/tool.py +96 -103
  61. langchain_core/messages/utils.py +478 -504
  62. langchain_core/output_parsers/__init__.py +1 -14
  63. langchain_core/output_parsers/base.py +58 -61
  64. langchain_core/output_parsers/json.py +7 -8
  65. langchain_core/output_parsers/list.py +5 -7
  66. langchain_core/output_parsers/openai_functions.py +49 -47
  67. langchain_core/output_parsers/openai_tools.py +14 -19
  68. langchain_core/output_parsers/pydantic.py +12 -13
  69. langchain_core/output_parsers/string.py +2 -2
  70. langchain_core/output_parsers/transform.py +15 -17
  71. langchain_core/output_parsers/xml.py +8 -10
  72. langchain_core/outputs/__init__.py +1 -1
  73. langchain_core/outputs/chat_generation.py +18 -18
  74. langchain_core/outputs/chat_result.py +1 -3
  75. langchain_core/outputs/generation.py +8 -8
  76. langchain_core/outputs/llm_result.py +10 -10
  77. langchain_core/prompt_values.py +12 -12
  78. langchain_core/prompts/__init__.py +3 -27
  79. langchain_core/prompts/base.py +45 -55
  80. langchain_core/prompts/chat.py +254 -313
  81. langchain_core/prompts/dict.py +5 -5
  82. langchain_core/prompts/few_shot.py +81 -88
  83. langchain_core/prompts/few_shot_with_templates.py +11 -13
  84. langchain_core/prompts/image.py +12 -14
  85. langchain_core/prompts/loading.py +6 -8
  86. langchain_core/prompts/message.py +3 -3
  87. langchain_core/prompts/prompt.py +24 -39
  88. langchain_core/prompts/string.py +4 -4
  89. langchain_core/prompts/structured.py +42 -50
  90. langchain_core/rate_limiters.py +51 -60
  91. langchain_core/retrievers.py +49 -190
  92. langchain_core/runnables/base.py +1484 -1709
  93. langchain_core/runnables/branch.py +45 -61
  94. langchain_core/runnables/config.py +80 -88
  95. langchain_core/runnables/configurable.py +117 -134
  96. langchain_core/runnables/fallbacks.py +83 -79
  97. langchain_core/runnables/graph.py +85 -95
  98. langchain_core/runnables/graph_ascii.py +27 -28
  99. langchain_core/runnables/graph_mermaid.py +38 -50
  100. langchain_core/runnables/graph_png.py +15 -16
  101. langchain_core/runnables/history.py +135 -148
  102. langchain_core/runnables/passthrough.py +124 -150
  103. langchain_core/runnables/retry.py +46 -51
  104. langchain_core/runnables/router.py +25 -30
  105. langchain_core/runnables/schema.py +79 -74
  106. langchain_core/runnables/utils.py +62 -68
  107. langchain_core/stores.py +81 -115
  108. langchain_core/structured_query.py +8 -8
  109. langchain_core/sys_info.py +27 -29
  110. langchain_core/tools/__init__.py +1 -14
  111. langchain_core/tools/base.py +179 -187
  112. langchain_core/tools/convert.py +131 -139
  113. langchain_core/tools/render.py +10 -10
  114. langchain_core/tools/retriever.py +11 -11
  115. langchain_core/tools/simple.py +19 -24
  116. langchain_core/tools/structured.py +30 -39
  117. langchain_core/tracers/__init__.py +1 -9
  118. langchain_core/tracers/base.py +97 -99
  119. langchain_core/tracers/context.py +29 -52
  120. langchain_core/tracers/core.py +50 -60
  121. langchain_core/tracers/evaluation.py +11 -11
  122. langchain_core/tracers/event_stream.py +115 -70
  123. langchain_core/tracers/langchain.py +21 -21
  124. langchain_core/tracers/log_stream.py +43 -43
  125. langchain_core/tracers/memory_stream.py +3 -3
  126. langchain_core/tracers/root_listeners.py +16 -16
  127. langchain_core/tracers/run_collector.py +2 -4
  128. langchain_core/tracers/schemas.py +0 -129
  129. langchain_core/tracers/stdout.py +3 -3
  130. langchain_core/utils/__init__.py +1 -4
  131. langchain_core/utils/_merge.py +46 -8
  132. langchain_core/utils/aiter.py +57 -61
  133. langchain_core/utils/env.py +9 -9
  134. langchain_core/utils/function_calling.py +89 -191
  135. langchain_core/utils/html.py +7 -8
  136. langchain_core/utils/input.py +6 -6
  137. langchain_core/utils/interactive_env.py +1 -1
  138. langchain_core/utils/iter.py +37 -42
  139. langchain_core/utils/json.py +4 -3
  140. langchain_core/utils/json_schema.py +8 -8
  141. langchain_core/utils/mustache.py +9 -11
  142. langchain_core/utils/pydantic.py +33 -35
  143. langchain_core/utils/strings.py +5 -5
  144. langchain_core/utils/usage.py +1 -1
  145. langchain_core/utils/utils.py +80 -54
  146. langchain_core/vectorstores/base.py +129 -164
  147. langchain_core/vectorstores/in_memory.py +99 -174
  148. langchain_core/vectorstores/utils.py +5 -5
  149. langchain_core/version.py +1 -1
  150. {langchain_core-0.3.79.dist-info → langchain_core-1.0.0.dist-info}/METADATA +28 -27
  151. langchain_core-1.0.0.dist-info/RECORD +172 -0
  152. {langchain_core-0.3.79.dist-info → langchain_core-1.0.0.dist-info}/WHEEL +1 -1
  153. langchain_core/beta/__init__.py +0 -1
  154. langchain_core/beta/runnables/__init__.py +0 -1
  155. langchain_core/beta/runnables/context.py +0 -447
  156. langchain_core/memory.py +0 -120
  157. langchain_core/messages/content_blocks.py +0 -176
  158. langchain_core/prompts/pipeline.py +0 -138
  159. langchain_core/pydantic_v1/__init__.py +0 -30
  160. langchain_core/pydantic_v1/dataclasses.py +0 -23
  161. langchain_core/pydantic_v1/main.py +0 -23
  162. langchain_core/tracers/langchain_v1.py +0 -31
  163. langchain_core/utils/loading.py +0 -35
  164. langchain_core-0.3.79.dist-info/RECORD +0 -174
  165. langchain_core-0.3.79.dist-info/entry_points.txt +0 -4
@@ -3,57 +3,52 @@
3
3
  import json
4
4
  import logging
5
5
  import operator
6
- from typing import Any, Literal, Optional, Union, cast
6
+ from collections.abc import Sequence
7
+ from typing import Any, Literal, cast, overload
7
8
 
8
9
  from pydantic import model_validator
9
10
  from typing_extensions import NotRequired, Self, TypedDict, override
10
11
 
12
+ from langchain_core.messages import content as types
11
13
  from langchain_core.messages.base import (
12
14
  BaseMessage,
13
15
  BaseMessageChunk,
16
+ _extract_reasoning_from_additional_kwargs,
14
17
  merge_content,
15
18
  )
19
+ from langchain_core.messages.content import InvalidToolCall
16
20
  from langchain_core.messages.tool import (
17
- InvalidToolCall,
18
21
  ToolCall,
19
22
  ToolCallChunk,
20
23
  default_tool_chunk_parser,
21
24
  default_tool_parser,
22
25
  )
23
- from langchain_core.messages.tool import (
24
- invalid_tool_call as create_invalid_tool_call,
25
- )
26
- from langchain_core.messages.tool import (
27
- tool_call as create_tool_call,
28
- )
29
- from langchain_core.messages.tool import (
30
- tool_call_chunk as create_tool_call_chunk,
31
- )
26
+ from langchain_core.messages.tool import invalid_tool_call as create_invalid_tool_call
27
+ from langchain_core.messages.tool import tool_call as create_tool_call
28
+ from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk
32
29
  from langchain_core.utils._merge import merge_dicts, merge_lists
33
30
  from langchain_core.utils.json import parse_partial_json
34
31
  from langchain_core.utils.usage import _dict_int_op
32
+ from langchain_core.utils.utils import LC_AUTO_PREFIX, LC_ID_PREFIX
35
33
 
36
34
  logger = logging.getLogger(__name__)
37
35
 
38
36
 
39
- _LC_ID_PREFIX = "run-"
40
-
41
-
42
37
  class InputTokenDetails(TypedDict, total=False):
43
38
  """Breakdown of input token counts.
44
39
 
45
40
  Does *not* need to sum to full input token count. Does *not* need to have all keys.
46
41
 
47
42
  Example:
48
- .. code-block:: python
49
-
50
- {
51
- "audio": 10,
52
- "cache_creation": 200,
53
- "cache_read": 100,
54
- }
43
+ ```python
44
+ {
45
+ "audio": 10,
46
+ "cache_creation": 200,
47
+ "cache_read": 100,
48
+ }
49
+ ```
55
50
 
56
- .. versionadded:: 0.3.9
51
+ !!! version-added "Added in version 0.3.9"
57
52
 
58
53
  May also hold extra provider-specific keys.
59
54
 
@@ -81,14 +76,14 @@ class OutputTokenDetails(TypedDict, total=False):
81
76
  Does *not* need to sum to full output token count. Does *not* need to have all keys.
82
77
 
83
78
  Example:
84
- .. code-block:: python
85
-
86
- {
87
- "audio": 10,
88
- "reasoning": 200,
89
- }
79
+ ```python
80
+ {
81
+ "audio": 10,
82
+ "reasoning": 200,
83
+ }
84
+ ```
90
85
 
91
- .. versionadded:: 0.3.9
86
+ !!! version-added "Added in version 0.3.9"
92
87
 
93
88
  """
94
89
 
@@ -109,26 +104,25 @@ class UsageMetadata(TypedDict):
109
104
  This is a standard representation of token usage that is consistent across models.
110
105
 
111
106
  Example:
112
- .. code-block:: python
113
-
114
- {
115
- "input_tokens": 350,
116
- "output_tokens": 240,
117
- "total_tokens": 590,
118
- "input_token_details": {
119
- "audio": 10,
120
- "cache_creation": 200,
121
- "cache_read": 100,
122
- },
123
- "output_token_details": {
124
- "audio": 10,
125
- "reasoning": 200,
126
- },
127
- }
128
-
129
- .. versionchanged:: 0.3.9
107
+ ```python
108
+ {
109
+ "input_tokens": 350,
110
+ "output_tokens": 240,
111
+ "total_tokens": 590,
112
+ "input_token_details": {
113
+ "audio": 10,
114
+ "cache_creation": 200,
115
+ "cache_read": 100,
116
+ },
117
+ "output_token_details": {
118
+ "audio": 10,
119
+ "reasoning": 200,
120
+ },
121
+ }
122
+ ```
130
123
 
131
- Added ``input_token_details`` and ``output_token_details``.
124
+ !!! warning "Behavior changed in 0.3.9"
125
+ Added `input_token_details` and `output_token_details`.
132
126
 
133
127
  """
134
128
 
@@ -154,56 +148,143 @@ class UsageMetadata(TypedDict):
154
148
  class AIMessage(BaseMessage):
155
149
  """Message from an AI.
156
150
 
157
- AIMessage is returned from a chat model as a response to a prompt.
151
+ An `AIMessage` is returned from a chat model as a response to a prompt.
158
152
 
159
153
  This message represents the output of the model and consists of both
160
- the raw output as returned by the model together standardized fields
154
+ the raw output as returned by the model and standardized fields
161
155
  (e.g., tool calls, usage metadata) added by the LangChain framework.
162
156
 
163
157
  """
164
158
 
165
- example: bool = False
166
- """Use to denote that a message is part of an example conversation.
167
-
168
- At the moment, this is ignored by most models. Usage is discouraged.
169
-
170
- """
171
-
172
159
  tool_calls: list[ToolCall] = []
173
- """If provided, tool calls associated with the message."""
160
+ """If present, tool calls associated with the message."""
174
161
  invalid_tool_calls: list[InvalidToolCall] = []
175
- """If provided, tool calls with parsing errors associated with the message."""
176
- usage_metadata: Optional[UsageMetadata] = None
177
- """If provided, usage metadata for a message, such as token counts.
162
+ """If present, tool calls with parsing errors associated with the message."""
163
+ usage_metadata: UsageMetadata | None = None
164
+ """If present, usage metadata for a message, such as token counts.
178
165
 
179
166
  This is a standard representation of token usage that is consistent across models.
180
-
181
167
  """
182
168
 
183
169
  type: Literal["ai"] = "ai"
184
- """The type of the message (used for deserialization). Defaults to ``'ai'``."""
170
+ """The type of the message (used for deserialization)."""
171
+
172
+ @overload
173
+ def __init__(
174
+ self,
175
+ content: str | list[str | dict],
176
+ **kwargs: Any,
177
+ ) -> None: ...
185
178
 
179
+ @overload
186
180
  def __init__(
187
181
  self,
188
- content: Union[str, list[Union[str, dict]]],
182
+ content: str | list[str | dict] | None = None,
183
+ content_blocks: list[types.ContentBlock] | None = None,
184
+ **kwargs: Any,
185
+ ) -> None: ...
186
+
187
+ def __init__(
188
+ self,
189
+ content: str | list[str | dict] | None = None,
190
+ content_blocks: list[types.ContentBlock] | None = None,
189
191
  **kwargs: Any,
190
192
  ) -> None:
191
- """Initialize ``AIMessage``.
193
+ """Initialize an `AIMessage`.
194
+
195
+ Specify `content` as positional arg or `content_blocks` for typing.
192
196
 
193
197
  Args:
194
198
  content: The content of the message.
195
- kwargs: Additional arguments to pass to the parent class.
199
+ content_blocks: Typed standard content.
200
+ **kwargs: Additional arguments to pass to the parent class.
196
201
  """
197
- super().__init__(content=content, **kwargs)
202
+ if content_blocks is not None:
203
+ # If there are tool calls in content_blocks, but not in tool_calls, add them
204
+ content_tool_calls = [
205
+ block for block in content_blocks if block.get("type") == "tool_call"
206
+ ]
207
+ if content_tool_calls and "tool_calls" not in kwargs:
208
+ kwargs["tool_calls"] = content_tool_calls
209
+
210
+ super().__init__(
211
+ content=cast("str | list[str | dict]", content_blocks),
212
+ **kwargs,
213
+ )
214
+ else:
215
+ super().__init__(content=content, **kwargs)
198
216
 
199
217
  @property
200
218
  def lc_attributes(self) -> dict:
201
- """Attrs to be serialized even if they are derived from other init args."""
219
+ """Attributes to be serialized.
220
+
221
+ Includes all attributes, even if they are derived from other initialization
222
+ arguments.
223
+ """
202
224
  return {
203
225
  "tool_calls": self.tool_calls,
204
226
  "invalid_tool_calls": self.invalid_tool_calls,
205
227
  }
206
228
 
229
+ @property
230
+ def content_blocks(self) -> list[types.ContentBlock]:
231
+ """Return standard, typed `ContentBlock` dicts from the message.
232
+
233
+ If the message has a known model provider, use the provider-specific translator
234
+ first before falling back to best-effort parsing. For details, see the property
235
+ on `BaseMessage`.
236
+ """
237
+ if self.response_metadata.get("output_version") == "v1":
238
+ return cast("list[types.ContentBlock]", self.content)
239
+
240
+ model_provider = self.response_metadata.get("model_provider")
241
+ if model_provider:
242
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
243
+ get_translator,
244
+ )
245
+
246
+ translator = get_translator(model_provider)
247
+ if translator:
248
+ try:
249
+ return translator["translate_content"](self)
250
+ except NotImplementedError:
251
+ pass
252
+
253
+ # Otherwise, use best-effort parsing
254
+ blocks = super().content_blocks
255
+
256
+ if self.tool_calls:
257
+ # Add from tool_calls if missing from content
258
+ content_tool_call_ids = {
259
+ block.get("id")
260
+ for block in self.content
261
+ if isinstance(block, dict) and block.get("type") == "tool_call"
262
+ }
263
+ for tool_call in self.tool_calls:
264
+ if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:
265
+ tool_call_block: types.ToolCall = {
266
+ "type": "tool_call",
267
+ "id": id_,
268
+ "name": tool_call["name"],
269
+ "args": tool_call["args"],
270
+ }
271
+ if "index" in tool_call:
272
+ tool_call_block["index"] = tool_call["index"] # type: ignore[typeddict-item]
273
+ if "extras" in tool_call:
274
+ tool_call_block["extras"] = tool_call["extras"] # type: ignore[typeddict-item]
275
+ blocks.append(tool_call_block)
276
+
277
+ # Best-effort reasoning extraction from additional_kwargs
278
+ # Only add reasoning if not already present
279
+ # Insert before all other blocks to keep reasoning at the start
280
+ has_reasoning = any(block.get("type") == "reasoning" for block in blocks)
281
+ if not has_reasoning and (
282
+ reasoning_block := _extract_reasoning_from_additional_kwargs(self)
283
+ ):
284
+ blocks.insert(0, reasoning_block)
285
+
286
+ return blocks
287
+
207
288
  # TODO: remove this logic if possible, reducing breaking nature of changes
208
289
  @model_validator(mode="before")
209
290
  @classmethod
@@ -232,7 +313,9 @@ class AIMessage(BaseMessage):
232
313
  # Ensure "type" is properly set on all tool call-like dicts.
233
314
  if tool_calls := values.get("tool_calls"):
234
315
  values["tool_calls"] = [
235
- create_tool_call(**{k: v for k, v in tc.items() if k != "type"})
316
+ create_tool_call(
317
+ **{k: v for k, v in tc.items() if k not in ("type", "extras")}
318
+ )
236
319
  for tc in tool_calls
237
320
  ]
238
321
  if invalid_tool_calls := values.get("invalid_tool_calls"):
@@ -251,11 +334,10 @@ class AIMessage(BaseMessage):
251
334
 
252
335
  @override
253
336
  def pretty_repr(self, html: bool = False) -> str:
254
- """Return a pretty representation of the message.
337
+ """Return a pretty representation of the message for display.
255
338
 
256
339
  Args:
257
340
  html: Whether to return an HTML-formatted string.
258
- Defaults to False.
259
341
 
260
342
  Returns:
261
343
  A pretty representation of the message.
@@ -264,7 +346,7 @@ class AIMessage(BaseMessage):
264
346
  base = super().pretty_repr(html=html)
265
347
  lines = []
266
348
 
267
- def _format_tool_args(tc: Union[ToolCall, InvalidToolCall]) -> list[str]:
349
+ def _format_tool_args(tc: ToolCall | InvalidToolCall) -> list[str]:
268
350
  lines = [
269
351
  f" {tc.get('name', 'Tool')} ({tc.get('id')})",
270
352
  f" Call ID: {tc.get('id')}",
@@ -292,29 +374,86 @@ class AIMessage(BaseMessage):
292
374
 
293
375
 
294
376
  class AIMessageChunk(AIMessage, BaseMessageChunk):
295
- """Message chunk from an AI."""
377
+ """Message chunk from an AI (yielded when streaming)."""
296
378
 
297
379
  # Ignoring mypy re-assignment here since we're overriding the value
298
380
  # to make sure that the chunk variant can be discriminated from the
299
381
  # non-chunk variant.
300
382
  type: Literal["AIMessageChunk"] = "AIMessageChunk" # type: ignore[assignment]
301
- """The type of the message (used for deserialization).
302
-
303
- Defaults to ``AIMessageChunk``.
304
-
305
- """
383
+ """The type of the message (used for deserialization)."""
306
384
 
307
385
  tool_call_chunks: list[ToolCallChunk] = []
308
386
  """If provided, tool call chunks associated with the message."""
309
387
 
388
+ chunk_position: Literal["last"] | None = None
389
+ """Optional span represented by an aggregated `AIMessageChunk`.
390
+
391
+ If a chunk with `chunk_position="last"` is aggregated into a stream,
392
+ `tool_call_chunks` in message content will be parsed into `tool_calls`.
393
+ """
394
+
310
395
  @property
311
396
  def lc_attributes(self) -> dict:
312
- """Attrs to be serialized even if they are derived from other init args."""
397
+ """Attributes to be serialized, even if they are derived from other initialization args.""" # noqa: E501
313
398
  return {
314
399
  "tool_calls": self.tool_calls,
315
400
  "invalid_tool_calls": self.invalid_tool_calls,
316
401
  }
317
402
 
403
+ @property
404
+ def content_blocks(self) -> list[types.ContentBlock]:
405
+ """Return standard, typed `ContentBlock` dicts from the message."""
406
+ if self.response_metadata.get("output_version") == "v1":
407
+ return cast("list[types.ContentBlock]", self.content)
408
+
409
+ model_provider = self.response_metadata.get("model_provider")
410
+ if model_provider:
411
+ from langchain_core.messages.block_translators import ( # noqa: PLC0415
412
+ get_translator,
413
+ )
414
+
415
+ translator = get_translator(model_provider)
416
+ if translator:
417
+ try:
418
+ return translator["translate_content_chunk"](self)
419
+ except NotImplementedError:
420
+ pass
421
+
422
+ # Otherwise, use best-effort parsing
423
+ blocks = super().content_blocks
424
+
425
+ if (
426
+ self.tool_call_chunks
427
+ and not self.content
428
+ and self.chunk_position != "last" # keep tool_calls if aggregated
429
+ ):
430
+ blocks = [
431
+ block
432
+ for block in blocks
433
+ if block["type"] not in ("tool_call", "invalid_tool_call")
434
+ ]
435
+ for tool_call_chunk in self.tool_call_chunks:
436
+ tc: types.ToolCallChunk = {
437
+ "type": "tool_call_chunk",
438
+ "id": tool_call_chunk.get("id"),
439
+ "name": tool_call_chunk.get("name"),
440
+ "args": tool_call_chunk.get("args"),
441
+ }
442
+ if (idx := tool_call_chunk.get("index")) is not None:
443
+ tc["index"] = idx
444
+ blocks.append(tc)
445
+
446
+ # Best-effort reasoning extraction from additional_kwargs
447
+ # Only add reasoning if not already present
448
+ # Insert before all other blocks to keep reasoning at the start
449
+ has_reasoning = any(block.get("type") == "reasoning" for block in blocks)
450
+ if not has_reasoning and (
451
+ reasoning_block := _extract_reasoning_from_additional_kwargs(self)
452
+ ):
453
+ blocks.insert(0, reasoning_block)
454
+
455
+ return blocks
456
+
318
457
  @model_validator(mode="after")
319
458
  def init_tool_calls(self) -> Self:
320
459
  """Initialize tool calls from tool call chunks.
@@ -379,10 +518,73 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
379
518
  add_chunk_to_invalid_tool_calls(chunk)
380
519
  self.tool_calls = tool_calls
381
520
  self.invalid_tool_calls = invalid_tool_calls
521
+
522
+ if (
523
+ self.chunk_position == "last"
524
+ and self.tool_call_chunks
525
+ and self.response_metadata.get("output_version") == "v1"
526
+ and isinstance(self.content, list)
527
+ ):
528
+ id_to_tc: dict[str, types.ToolCall] = {
529
+ cast("str", tc.get("id")): {
530
+ "type": "tool_call",
531
+ "name": tc["name"],
532
+ "args": tc["args"],
533
+ "id": tc.get("id"),
534
+ }
535
+ for tc in self.tool_calls
536
+ if "id" in tc
537
+ }
538
+ for idx, block in enumerate(self.content):
539
+ if (
540
+ isinstance(block, dict)
541
+ and block.get("type") == "tool_call_chunk"
542
+ and (call_id := block.get("id"))
543
+ and call_id in id_to_tc
544
+ ):
545
+ self.content[idx] = cast("dict[str, Any]", id_to_tc[call_id])
546
+ if "extras" in block:
547
+ # mypy does not account for instance check for dict above
548
+ self.content[idx]["extras"] = block["extras"] # type: ignore[index]
549
+
550
+ return self
551
+
552
+ @model_validator(mode="after")
553
+ def init_server_tool_calls(self) -> Self:
554
+ """Parse `server_tool_call_chunks`."""
555
+ if (
556
+ self.chunk_position == "last"
557
+ and self.response_metadata.get("output_version") == "v1"
558
+ and isinstance(self.content, list)
559
+ ):
560
+ for idx, block in enumerate(self.content):
561
+ if (
562
+ isinstance(block, dict)
563
+ and block.get("type")
564
+ in ("server_tool_call", "server_tool_call_chunk")
565
+ and (args_str := block.get("args"))
566
+ and isinstance(args_str, str)
567
+ ):
568
+ try:
569
+ args = json.loads(args_str)
570
+ if isinstance(args, dict):
571
+ self.content[idx]["type"] = "server_tool_call" # type: ignore[index]
572
+ self.content[idx]["args"] = args # type: ignore[index]
573
+ except json.JSONDecodeError:
574
+ pass
382
575
  return self
383
576
 
577
+ @overload # type: ignore[override] # summing BaseMessages gives ChatPromptTemplate
578
+ def __add__(self, other: "AIMessageChunk") -> "AIMessageChunk": ...
579
+
580
+ @overload
581
+ def __add__(self, other: Sequence["AIMessageChunk"]) -> "AIMessageChunk": ...
582
+
583
+ @overload
584
+ def __add__(self, other: Any) -> BaseMessageChunk: ...
585
+
384
586
  @override
385
- def __add__(self, other: Any) -> BaseMessageChunk: # type: ignore[override]
587
+ def __add__(self, other: Any) -> BaseMessageChunk:
386
588
  if isinstance(other, AIMessageChunk):
387
589
  return add_ai_message_chunks(self, other)
388
590
  if isinstance(other, (list, tuple)) and all(
@@ -395,23 +597,16 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
395
597
  def add_ai_message_chunks(
396
598
  left: AIMessageChunk, *others: AIMessageChunk
397
599
  ) -> AIMessageChunk:
398
- """Add multiple ``AIMessageChunk``s together.
600
+ """Add multiple `AIMessageChunk`s together.
399
601
 
400
602
  Args:
401
- left: The first ``AIMessageChunk``.
402
- *others: Other ``AIMessageChunk``s to add.
403
-
404
- Raises:
405
- ValueError: If the example values of the chunks are not the same.
603
+ left: The first `AIMessageChunk`.
604
+ *others: Other `AIMessageChunk`s to add.
406
605
 
407
606
  Returns:
408
- The resulting ``AIMessageChunk``.
607
+ The resulting `AIMessageChunk`.
409
608
 
410
609
  """
411
- if any(left.example != o.example for o in others):
412
- msg = "Cannot concatenate AIMessageChunks with different example values."
413
- raise ValueError(msg)
414
-
415
610
  content = merge_content(left.content, *(o.content for o in others))
416
611
  additional_kwargs = merge_dicts(
417
612
  left.additional_kwargs, *(o.additional_kwargs for o in others)
@@ -438,7 +633,7 @@ def add_ai_message_chunks(
438
633
 
439
634
  # Token usage
440
635
  if left.usage_metadata or any(o.usage_metadata is not None for o in others):
441
- usage_metadata: Optional[UsageMetadata] = left.usage_metadata
636
+ usage_metadata: UsageMetadata | None = left.usage_metadata
442
637
  for other in others:
443
638
  usage_metadata = add_usage(usage_metadata, other.usage_metadata)
444
639
  else:
@@ -446,72 +641,83 @@ def add_ai_message_chunks(
446
641
 
447
642
  chunk_id = None
448
643
  candidates = [left.id] + [o.id for o in others]
449
- # first pass: pick the first non-run-* id
644
+ # first pass: pick the first provider-assigned id (non-run-* and non-lc_*)
450
645
  for id_ in candidates:
451
- if id_ and not id_.startswith(_LC_ID_PREFIX):
646
+ if (
647
+ id_
648
+ and not id_.startswith(LC_ID_PREFIX)
649
+ and not id_.startswith(LC_AUTO_PREFIX)
650
+ ):
452
651
  chunk_id = id_
453
652
  break
454
653
  else:
455
- # second pass: no provider-assigned id found, just take the first non-null
654
+ # second pass: prefer lc_run-* ids over lc_* ids
456
655
  for id_ in candidates:
457
- if id_:
656
+ if id_ and id_.startswith(LC_ID_PREFIX):
458
657
  chunk_id = id_
459
658
  break
659
+ else:
660
+ # third pass: take any remaining id (auto-generated lc_* ids)
661
+ for id_ in candidates:
662
+ if id_:
663
+ chunk_id = id_
664
+ break
665
+
666
+ chunk_position: Literal["last"] | None = (
667
+ "last" if any(x.chunk_position == "last" for x in [left, *others]) else None
668
+ )
460
669
 
461
670
  return left.__class__(
462
- example=left.example,
463
671
  content=content,
464
672
  additional_kwargs=additional_kwargs,
465
673
  tool_call_chunks=tool_call_chunks,
466
674
  response_metadata=response_metadata,
467
675
  usage_metadata=usage_metadata,
468
676
  id=chunk_id,
677
+ chunk_position=chunk_position,
469
678
  )
470
679
 
471
680
 
472
- def add_usage(
473
- left: Optional[UsageMetadata], right: Optional[UsageMetadata]
474
- ) -> UsageMetadata:
681
+ def add_usage(left: UsageMetadata | None, right: UsageMetadata | None) -> UsageMetadata:
475
682
  """Recursively add two UsageMetadata objects.
476
683
 
477
684
  Example:
478
- .. code-block:: python
479
-
480
- from langchain_core.messages.ai import add_usage
481
-
482
- left = UsageMetadata(
483
- input_tokens=5,
484
- output_tokens=0,
485
- total_tokens=5,
486
- input_token_details=InputTokenDetails(cache_read=3),
487
- )
488
- right = UsageMetadata(
489
- input_tokens=0,
490
- output_tokens=10,
491
- total_tokens=10,
492
- output_token_details=OutputTokenDetails(reasoning=4),
493
- )
685
+ ```python
686
+ from langchain_core.messages.ai import add_usage
687
+
688
+ left = UsageMetadata(
689
+ input_tokens=5,
690
+ output_tokens=0,
691
+ total_tokens=5,
692
+ input_token_details=InputTokenDetails(cache_read=3),
693
+ )
694
+ right = UsageMetadata(
695
+ input_tokens=0,
696
+ output_tokens=10,
697
+ total_tokens=10,
698
+ output_token_details=OutputTokenDetails(reasoning=4),
699
+ )
494
700
 
495
- add_usage(left, right)
701
+ add_usage(left, right)
702
+ ```
496
703
 
497
704
  results in
498
705
 
499
- .. code-block:: python
500
-
501
- UsageMetadata(
502
- input_tokens=5,
503
- output_tokens=10,
504
- total_tokens=15,
505
- input_token_details=InputTokenDetails(cache_read=3),
506
- output_token_details=OutputTokenDetails(reasoning=4),
507
- )
508
-
706
+ ```python
707
+ UsageMetadata(
708
+ input_tokens=5,
709
+ output_tokens=10,
710
+ total_tokens=15,
711
+ input_token_details=InputTokenDetails(cache_read=3),
712
+ output_token_details=OutputTokenDetails(reasoning=4),
713
+ )
714
+ ```
509
715
  Args:
510
- left: The first ``UsageMetadata`` object.
511
- right: The second ``UsageMetadata`` object.
716
+ left: The first `UsageMetadata` object.
717
+ right: The second `UsageMetadata` object.
512
718
 
513
719
  Returns:
514
- The sum of the two ``UsageMetadata`` objects.
720
+ The sum of the two `UsageMetadata` objects.
515
721
 
516
722
  """
517
723
  if not (left or right):
@@ -532,50 +738,49 @@ def add_usage(
532
738
 
533
739
 
534
740
  def subtract_usage(
535
- left: Optional[UsageMetadata], right: Optional[UsageMetadata]
741
+ left: UsageMetadata | None, right: UsageMetadata | None
536
742
  ) -> UsageMetadata:
537
- """Recursively subtract two ``UsageMetadata`` objects.
743
+ """Recursively subtract two `UsageMetadata` objects.
538
744
 
539
- Token counts cannot be negative so the actual operation is ``max(left - right, 0)``.
745
+ Token counts cannot be negative so the actual operation is `max(left - right, 0)`.
540
746
 
541
747
  Example:
542
- .. code-block:: python
543
-
544
- from langchain_core.messages.ai import subtract_usage
545
-
546
- left = UsageMetadata(
547
- input_tokens=5,
548
- output_tokens=10,
549
- total_tokens=15,
550
- input_token_details=InputTokenDetails(cache_read=4),
551
- )
552
- right = UsageMetadata(
553
- input_tokens=3,
554
- output_tokens=8,
555
- total_tokens=11,
556
- output_token_details=OutputTokenDetails(reasoning=4),
557
- )
748
+ ```python
749
+ from langchain_core.messages.ai import subtract_usage
750
+
751
+ left = UsageMetadata(
752
+ input_tokens=5,
753
+ output_tokens=10,
754
+ total_tokens=15,
755
+ input_token_details=InputTokenDetails(cache_read=4),
756
+ )
757
+ right = UsageMetadata(
758
+ input_tokens=3,
759
+ output_tokens=8,
760
+ total_tokens=11,
761
+ output_token_details=OutputTokenDetails(reasoning=4),
762
+ )
558
763
 
559
- subtract_usage(left, right)
764
+ subtract_usage(left, right)
765
+ ```
560
766
 
561
767
  results in
562
768
 
563
- .. code-block:: python
564
-
565
- UsageMetadata(
566
- input_tokens=2,
567
- output_tokens=2,
568
- total_tokens=4,
569
- input_token_details=InputTokenDetails(cache_read=4),
570
- output_token_details=OutputTokenDetails(reasoning=0),
571
- )
572
-
769
+ ```python
770
+ UsageMetadata(
771
+ input_tokens=2,
772
+ output_tokens=2,
773
+ total_tokens=4,
774
+ input_token_details=InputTokenDetails(cache_read=4),
775
+ output_token_details=OutputTokenDetails(reasoning=0),
776
+ )
777
+ ```
573
778
  Args:
574
- left: The first ``UsageMetadata`` object.
575
- right: The second ``UsageMetadata`` object.
779
+ left: The first `UsageMetadata` object.
780
+ right: The second `UsageMetadata` object.
576
781
 
577
782
  Returns:
578
- The resulting ``UsageMetadata`` after subtraction.
783
+ The resulting `UsageMetadata` after subtraction.
579
784
 
580
785
  """
581
786
  if not (left or right):