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.
- langchain_core/__init__.py +1 -1
- langchain_core/_api/__init__.py +3 -4
- langchain_core/_api/beta_decorator.py +23 -26
- langchain_core/_api/deprecation.py +52 -65
- langchain_core/_api/path.py +3 -6
- langchain_core/_import_utils.py +3 -4
- langchain_core/agents.py +19 -19
- langchain_core/caches.py +53 -63
- langchain_core/callbacks/__init__.py +1 -8
- langchain_core/callbacks/base.py +323 -334
- langchain_core/callbacks/file.py +44 -44
- langchain_core/callbacks/manager.py +441 -507
- langchain_core/callbacks/stdout.py +29 -30
- langchain_core/callbacks/streaming_stdout.py +32 -32
- langchain_core/callbacks/usage.py +60 -57
- langchain_core/chat_history.py +48 -63
- langchain_core/document_loaders/base.py +23 -23
- langchain_core/document_loaders/langsmith.py +37 -37
- langchain_core/documents/__init__.py +0 -1
- langchain_core/documents/base.py +62 -65
- langchain_core/documents/compressor.py +4 -4
- langchain_core/documents/transformers.py +28 -29
- langchain_core/embeddings/fake.py +50 -54
- langchain_core/example_selectors/length_based.py +1 -1
- langchain_core/example_selectors/semantic_similarity.py +21 -25
- langchain_core/exceptions.py +10 -11
- langchain_core/globals.py +3 -151
- langchain_core/indexing/api.py +61 -66
- langchain_core/indexing/base.py +58 -58
- langchain_core/indexing/in_memory.py +3 -3
- langchain_core/language_models/__init__.py +14 -27
- langchain_core/language_models/_utils.py +270 -84
- langchain_core/language_models/base.py +55 -162
- langchain_core/language_models/chat_models.py +442 -402
- langchain_core/language_models/fake.py +11 -11
- langchain_core/language_models/fake_chat_models.py +61 -39
- langchain_core/language_models/llms.py +123 -231
- langchain_core/load/dump.py +4 -5
- langchain_core/load/load.py +18 -28
- langchain_core/load/mapping.py +2 -4
- langchain_core/load/serializable.py +39 -40
- langchain_core/messages/__init__.py +61 -22
- langchain_core/messages/ai.py +368 -163
- langchain_core/messages/base.py +214 -43
- langchain_core/messages/block_translators/__init__.py +111 -0
- langchain_core/messages/block_translators/anthropic.py +470 -0
- langchain_core/messages/block_translators/bedrock.py +94 -0
- langchain_core/messages/block_translators/bedrock_converse.py +297 -0
- langchain_core/messages/block_translators/google_genai.py +530 -0
- langchain_core/messages/block_translators/google_vertexai.py +21 -0
- langchain_core/messages/block_translators/groq.py +143 -0
- langchain_core/messages/block_translators/langchain_v0.py +301 -0
- langchain_core/messages/block_translators/openai.py +1010 -0
- langchain_core/messages/chat.py +2 -6
- langchain_core/messages/content.py +1423 -0
- langchain_core/messages/function.py +6 -10
- langchain_core/messages/human.py +41 -38
- langchain_core/messages/modifier.py +2 -2
- langchain_core/messages/system.py +38 -28
- langchain_core/messages/tool.py +96 -103
- langchain_core/messages/utils.py +478 -504
- langchain_core/output_parsers/__init__.py +1 -14
- langchain_core/output_parsers/base.py +58 -61
- langchain_core/output_parsers/json.py +7 -8
- langchain_core/output_parsers/list.py +5 -7
- langchain_core/output_parsers/openai_functions.py +49 -47
- langchain_core/output_parsers/openai_tools.py +14 -19
- langchain_core/output_parsers/pydantic.py +12 -13
- langchain_core/output_parsers/string.py +2 -2
- langchain_core/output_parsers/transform.py +15 -17
- langchain_core/output_parsers/xml.py +8 -10
- langchain_core/outputs/__init__.py +1 -1
- langchain_core/outputs/chat_generation.py +18 -18
- langchain_core/outputs/chat_result.py +1 -3
- langchain_core/outputs/generation.py +8 -8
- langchain_core/outputs/llm_result.py +10 -10
- langchain_core/prompt_values.py +12 -12
- langchain_core/prompts/__init__.py +3 -27
- langchain_core/prompts/base.py +45 -55
- langchain_core/prompts/chat.py +254 -313
- langchain_core/prompts/dict.py +5 -5
- langchain_core/prompts/few_shot.py +81 -88
- langchain_core/prompts/few_shot_with_templates.py +11 -13
- langchain_core/prompts/image.py +12 -14
- langchain_core/prompts/loading.py +6 -8
- langchain_core/prompts/message.py +3 -3
- langchain_core/prompts/prompt.py +24 -39
- langchain_core/prompts/string.py +4 -4
- langchain_core/prompts/structured.py +42 -50
- langchain_core/rate_limiters.py +51 -60
- langchain_core/retrievers.py +49 -190
- langchain_core/runnables/base.py +1484 -1709
- langchain_core/runnables/branch.py +45 -61
- langchain_core/runnables/config.py +80 -88
- langchain_core/runnables/configurable.py +117 -134
- langchain_core/runnables/fallbacks.py +83 -79
- langchain_core/runnables/graph.py +85 -95
- langchain_core/runnables/graph_ascii.py +27 -28
- langchain_core/runnables/graph_mermaid.py +38 -50
- langchain_core/runnables/graph_png.py +15 -16
- langchain_core/runnables/history.py +135 -148
- langchain_core/runnables/passthrough.py +124 -150
- langchain_core/runnables/retry.py +46 -51
- langchain_core/runnables/router.py +25 -30
- langchain_core/runnables/schema.py +79 -74
- langchain_core/runnables/utils.py +62 -68
- langchain_core/stores.py +81 -115
- langchain_core/structured_query.py +8 -8
- langchain_core/sys_info.py +27 -29
- langchain_core/tools/__init__.py +1 -14
- langchain_core/tools/base.py +179 -187
- langchain_core/tools/convert.py +131 -139
- langchain_core/tools/render.py +10 -10
- langchain_core/tools/retriever.py +11 -11
- langchain_core/tools/simple.py +19 -24
- langchain_core/tools/structured.py +30 -39
- langchain_core/tracers/__init__.py +1 -9
- langchain_core/tracers/base.py +97 -99
- langchain_core/tracers/context.py +29 -52
- langchain_core/tracers/core.py +50 -60
- langchain_core/tracers/evaluation.py +11 -11
- langchain_core/tracers/event_stream.py +115 -70
- langchain_core/tracers/langchain.py +21 -21
- langchain_core/tracers/log_stream.py +43 -43
- langchain_core/tracers/memory_stream.py +3 -3
- langchain_core/tracers/root_listeners.py +16 -16
- langchain_core/tracers/run_collector.py +2 -4
- langchain_core/tracers/schemas.py +0 -129
- langchain_core/tracers/stdout.py +3 -3
- langchain_core/utils/__init__.py +1 -4
- langchain_core/utils/_merge.py +46 -8
- langchain_core/utils/aiter.py +57 -61
- langchain_core/utils/env.py +9 -9
- langchain_core/utils/function_calling.py +89 -191
- langchain_core/utils/html.py +7 -8
- langchain_core/utils/input.py +6 -6
- langchain_core/utils/interactive_env.py +1 -1
- langchain_core/utils/iter.py +37 -42
- langchain_core/utils/json.py +4 -3
- langchain_core/utils/json_schema.py +8 -8
- langchain_core/utils/mustache.py +9 -11
- langchain_core/utils/pydantic.py +33 -35
- langchain_core/utils/strings.py +5 -5
- langchain_core/utils/usage.py +1 -1
- langchain_core/utils/utils.py +80 -54
- langchain_core/vectorstores/base.py +129 -164
- langchain_core/vectorstores/in_memory.py +99 -174
- langchain_core/vectorstores/utils.py +5 -5
- langchain_core/version.py +1 -1
- {langchain_core-0.3.79.dist-info → langchain_core-1.0.0.dist-info}/METADATA +28 -27
- langchain_core-1.0.0.dist-info/RECORD +172 -0
- {langchain_core-0.3.79.dist-info → langchain_core-1.0.0.dist-info}/WHEEL +1 -1
- langchain_core/beta/__init__.py +0 -1
- langchain_core/beta/runnables/__init__.py +0 -1
- langchain_core/beta/runnables/context.py +0 -447
- langchain_core/memory.py +0 -120
- langchain_core/messages/content_blocks.py +0 -176
- langchain_core/prompts/pipeline.py +0 -138
- langchain_core/pydantic_v1/__init__.py +0 -30
- langchain_core/pydantic_v1/dataclasses.py +0 -23
- langchain_core/pydantic_v1/main.py +0 -23
- langchain_core/tracers/langchain_v1.py +0 -31
- langchain_core/utils/loading.py +0 -35
- langchain_core-0.3.79.dist-info/RECORD +0 -174
- langchain_core-0.3.79.dist-info/entry_points.txt +0 -4
langchain_core/messages/ai.py
CHANGED
|
@@ -3,57 +3,52 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
import operator
|
|
6
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
```python
|
|
44
|
+
{
|
|
45
|
+
"audio": 10,
|
|
46
|
+
"cache_creation": 200,
|
|
47
|
+
"cache_read": 100,
|
|
48
|
+
}
|
|
49
|
+
```
|
|
55
50
|
|
|
56
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
```python
|
|
80
|
+
{
|
|
81
|
+
"audio": 10,
|
|
82
|
+
"reasoning": 200,
|
|
83
|
+
}
|
|
84
|
+
```
|
|
90
85
|
|
|
91
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
160
|
+
"""If present, tool calls associated with the message."""
|
|
174
161
|
invalid_tool_calls: list[InvalidToolCall] = []
|
|
175
|
-
"""If
|
|
176
|
-
usage_metadata:
|
|
177
|
-
"""If
|
|
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).
|
|
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:
|
|
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
|
|
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
|
-
|
|
199
|
+
content_blocks: Typed standard content.
|
|
200
|
+
**kwargs: Additional arguments to pass to the parent class.
|
|
196
201
|
"""
|
|
197
|
-
|
|
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
|
-
"""
|
|
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(
|
|
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:
|
|
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
|
-
"""
|
|
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:
|
|
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
|
|
600
|
+
"""Add multiple `AIMessageChunk`s together.
|
|
399
601
|
|
|
400
602
|
Args:
|
|
401
|
-
left: The first
|
|
402
|
-
*others: Other
|
|
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
|
|
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:
|
|
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-*
|
|
644
|
+
# first pass: pick the first provider-assigned id (non-run-* and non-lc_*)
|
|
450
645
|
for id_ in candidates:
|
|
451
|
-
if
|
|
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:
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
701
|
+
add_usage(left, right)
|
|
702
|
+
```
|
|
496
703
|
|
|
497
704
|
results in
|
|
498
705
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
511
|
-
right: The second
|
|
716
|
+
left: The first `UsageMetadata` object.
|
|
717
|
+
right: The second `UsageMetadata` object.
|
|
512
718
|
|
|
513
719
|
Returns:
|
|
514
|
-
The sum of the two
|
|
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:
|
|
741
|
+
left: UsageMetadata | None, right: UsageMetadata | None
|
|
536
742
|
) -> UsageMetadata:
|
|
537
|
-
"""Recursively subtract two
|
|
743
|
+
"""Recursively subtract two `UsageMetadata` objects.
|
|
538
744
|
|
|
539
|
-
Token counts cannot be negative so the actual operation is
|
|
745
|
+
Token counts cannot be negative so the actual operation is `max(left - right, 0)`.
|
|
540
746
|
|
|
541
747
|
Example:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
764
|
+
subtract_usage(left, right)
|
|
765
|
+
```
|
|
560
766
|
|
|
561
767
|
results in
|
|
562
768
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
|
575
|
-
right: The second
|
|
779
|
+
left: The first `UsageMetadata` object.
|
|
780
|
+
right: The second `UsageMetadata` object.
|
|
576
781
|
|
|
577
782
|
Returns:
|
|
578
|
-
The resulting
|
|
783
|
+
The resulting `UsageMetadata` after subtraction.
|
|
579
784
|
|
|
580
785
|
"""
|
|
581
786
|
if not (left or right):
|