langchain-core 0.4.0.dev0__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 +45 -70
- langchain_core/_api/deprecation.py +80 -80
- langchain_core/_api/path.py +22 -8
- langchain_core/_import_utils.py +10 -4
- langchain_core/agents.py +25 -21
- langchain_core/caches.py +53 -63
- langchain_core/callbacks/__init__.py +1 -8
- langchain_core/callbacks/base.py +341 -348
- langchain_core/callbacks/file.py +55 -44
- langchain_core/callbacks/manager.py +546 -683
- langchain_core/callbacks/stdout.py +29 -30
- langchain_core/callbacks/streaming_stdout.py +35 -36
- langchain_core/callbacks/usage.py +65 -70
- langchain_core/chat_history.py +48 -55
- langchain_core/document_loaders/base.py +46 -21
- langchain_core/document_loaders/langsmith.py +39 -36
- langchain_core/documents/__init__.py +0 -1
- langchain_core/documents/base.py +96 -74
- langchain_core/documents/compressor.py +12 -9
- langchain_core/documents/transformers.py +29 -28
- langchain_core/embeddings/fake.py +56 -57
- langchain_core/env.py +2 -3
- langchain_core/example_selectors/base.py +12 -0
- langchain_core/example_selectors/length_based.py +1 -1
- langchain_core/example_selectors/semantic_similarity.py +21 -25
- langchain_core/exceptions.py +15 -9
- langchain_core/globals.py +4 -163
- langchain_core/indexing/api.py +132 -125
- langchain_core/indexing/base.py +64 -67
- langchain_core/indexing/in_memory.py +26 -6
- langchain_core/language_models/__init__.py +15 -27
- langchain_core/language_models/_utils.py +267 -117
- langchain_core/language_models/base.py +92 -177
- langchain_core/language_models/chat_models.py +547 -407
- langchain_core/language_models/fake.py +11 -11
- langchain_core/language_models/fake_chat_models.py +72 -118
- langchain_core/language_models/llms.py +168 -242
- langchain_core/load/dump.py +8 -11
- langchain_core/load/load.py +32 -28
- langchain_core/load/mapping.py +2 -4
- langchain_core/load/serializable.py +50 -56
- langchain_core/messages/__init__.py +36 -51
- langchain_core/messages/ai.py +377 -150
- langchain_core/messages/base.py +239 -47
- 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 -3
- langchain_core/messages/content.py +1423 -0
- langchain_core/messages/function.py +7 -7
- langchain_core/messages/human.py +44 -38
- langchain_core/messages/modifier.py +3 -2
- langchain_core/messages/system.py +40 -27
- langchain_core/messages/tool.py +160 -58
- langchain_core/messages/utils.py +527 -638
- langchain_core/output_parsers/__init__.py +1 -14
- langchain_core/output_parsers/base.py +68 -104
- langchain_core/output_parsers/json.py +13 -17
- langchain_core/output_parsers/list.py +11 -33
- langchain_core/output_parsers/openai_functions.py +56 -74
- langchain_core/output_parsers/openai_tools.py +68 -109
- langchain_core/output_parsers/pydantic.py +15 -13
- langchain_core/output_parsers/string.py +6 -2
- langchain_core/output_parsers/transform.py +17 -60
- langchain_core/output_parsers/xml.py +34 -44
- langchain_core/outputs/__init__.py +1 -1
- langchain_core/outputs/chat_generation.py +26 -11
- langchain_core/outputs/chat_result.py +1 -3
- langchain_core/outputs/generation.py +17 -6
- langchain_core/outputs/llm_result.py +15 -8
- langchain_core/prompt_values.py +29 -123
- langchain_core/prompts/__init__.py +3 -27
- langchain_core/prompts/base.py +48 -63
- langchain_core/prompts/chat.py +259 -288
- langchain_core/prompts/dict.py +19 -11
- langchain_core/prompts/few_shot.py +84 -90
- langchain_core/prompts/few_shot_with_templates.py +14 -12
- langchain_core/prompts/image.py +19 -14
- langchain_core/prompts/loading.py +6 -8
- langchain_core/prompts/message.py +7 -8
- langchain_core/prompts/prompt.py +42 -43
- langchain_core/prompts/string.py +37 -16
- langchain_core/prompts/structured.py +43 -46
- langchain_core/rate_limiters.py +51 -60
- langchain_core/retrievers.py +52 -192
- langchain_core/runnables/base.py +1727 -1683
- langchain_core/runnables/branch.py +52 -73
- langchain_core/runnables/config.py +89 -103
- langchain_core/runnables/configurable.py +128 -130
- langchain_core/runnables/fallbacks.py +93 -82
- langchain_core/runnables/graph.py +127 -127
- langchain_core/runnables/graph_ascii.py +63 -41
- langchain_core/runnables/graph_mermaid.py +87 -70
- langchain_core/runnables/graph_png.py +31 -36
- langchain_core/runnables/history.py +145 -161
- langchain_core/runnables/passthrough.py +141 -144
- langchain_core/runnables/retry.py +84 -68
- langchain_core/runnables/router.py +33 -37
- langchain_core/runnables/schema.py +79 -72
- langchain_core/runnables/utils.py +95 -139
- langchain_core/stores.py +85 -131
- langchain_core/structured_query.py +11 -15
- langchain_core/sys_info.py +31 -32
- langchain_core/tools/__init__.py +1 -14
- langchain_core/tools/base.py +221 -247
- langchain_core/tools/convert.py +144 -161
- langchain_core/tools/render.py +10 -10
- langchain_core/tools/retriever.py +12 -19
- langchain_core/tools/simple.py +52 -29
- langchain_core/tools/structured.py +56 -60
- langchain_core/tracers/__init__.py +1 -9
- langchain_core/tracers/_streaming.py +6 -7
- langchain_core/tracers/base.py +103 -112
- langchain_core/tracers/context.py +29 -48
- langchain_core/tracers/core.py +142 -105
- langchain_core/tracers/evaluation.py +30 -34
- langchain_core/tracers/event_stream.py +162 -117
- langchain_core/tracers/langchain.py +34 -36
- langchain_core/tracers/log_stream.py +87 -49
- langchain_core/tracers/memory_stream.py +3 -3
- langchain_core/tracers/root_listeners.py +18 -34
- langchain_core/tracers/run_collector.py +8 -20
- langchain_core/tracers/schemas.py +0 -125
- langchain_core/tracers/stdout.py +3 -3
- langchain_core/utils/__init__.py +1 -4
- langchain_core/utils/_merge.py +47 -9
- langchain_core/utils/aiter.py +70 -66
- langchain_core/utils/env.py +12 -9
- langchain_core/utils/function_calling.py +139 -206
- langchain_core/utils/html.py +7 -8
- langchain_core/utils/input.py +6 -6
- langchain_core/utils/interactive_env.py +6 -2
- langchain_core/utils/iter.py +48 -45
- langchain_core/utils/json.py +14 -4
- langchain_core/utils/json_schema.py +159 -43
- langchain_core/utils/mustache.py +32 -25
- langchain_core/utils/pydantic.py +67 -40
- langchain_core/utils/strings.py +5 -5
- langchain_core/utils/usage.py +1 -1
- langchain_core/utils/utils.py +104 -62
- langchain_core/vectorstores/base.py +131 -179
- langchain_core/vectorstores/in_memory.py +113 -182
- langchain_core/vectorstores/utils.py +23 -17
- langchain_core/version.py +1 -1
- langchain_core-1.0.0.dist-info/METADATA +68 -0
- langchain_core-1.0.0.dist-info/RECORD +172 -0
- {langchain_core-0.4.0.dev0.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 -448
- langchain_core/memory.py +0 -116
- langchain_core/messages/content_blocks.py +0 -1435
- langchain_core/prompts/pipeline.py +0 -133
- 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 -23
- langchain_core/utils/loading.py +0 -31
- langchain_core/v1/__init__.py +0 -1
- langchain_core/v1/chat_models.py +0 -1047
- langchain_core/v1/messages.py +0 -755
- langchain_core-0.4.0.dev0.dist-info/METADATA +0 -108
- langchain_core-0.4.0.dev0.dist-info/RECORD +0 -177
- langchain_core-0.4.0.dev0.dist-info/entry_points.txt +0 -4
langchain_core/messages/ai.py
CHANGED
|
@@ -3,14 +3,21 @@
|
|
|
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
|
|
|
11
|
-
from langchain_core.messages
|
|
12
|
+
from langchain_core.messages import content as types
|
|
13
|
+
from langchain_core.messages.base import (
|
|
14
|
+
BaseMessage,
|
|
15
|
+
BaseMessageChunk,
|
|
16
|
+
_extract_reasoning_from_additional_kwargs,
|
|
17
|
+
merge_content,
|
|
18
|
+
)
|
|
19
|
+
from langchain_core.messages.content import InvalidToolCall
|
|
12
20
|
from langchain_core.messages.tool import (
|
|
13
|
-
InvalidToolCall,
|
|
14
21
|
ToolCall,
|
|
15
22
|
ToolCallChunk,
|
|
16
23
|
default_tool_chunk_parser,
|
|
@@ -22,39 +29,26 @@ from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chu
|
|
|
22
29
|
from langchain_core.utils._merge import merge_dicts, merge_lists
|
|
23
30
|
from langchain_core.utils.json import parse_partial_json
|
|
24
31
|
from langchain_core.utils.usage import _dict_int_op
|
|
32
|
+
from langchain_core.utils.utils import LC_AUTO_PREFIX, LC_ID_PREFIX
|
|
25
33
|
|
|
26
34
|
logger = logging.getLogger(__name__)
|
|
27
35
|
|
|
28
36
|
|
|
29
|
-
_LC_ID_PREFIX = "run-"
|
|
30
|
-
"""Internal tracing/callback system identifier.
|
|
31
|
-
|
|
32
|
-
Used for:
|
|
33
|
-
- Tracing. Every LangChain operation (LLM call, chain execution, tool use, etc.)
|
|
34
|
-
gets a unique run_id (UUID)
|
|
35
|
-
- Enables tracking parent-child relationships between operations
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
_LC_AUTO_PREFIX = "lc_"
|
|
39
|
-
"""LangChain auto-generated ID prefix for messages and content blocks."""
|
|
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:
|
|
43
|
+
```python
|
|
44
|
+
{
|
|
45
|
+
"audio": 10,
|
|
46
|
+
"cache_creation": 200,
|
|
47
|
+
"cache_read": 100,
|
|
48
|
+
}
|
|
49
|
+
```
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
"audio": 10,
|
|
53
|
-
"cache_creation": 200,
|
|
54
|
-
"cache_read": 100,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.. versionadded:: 0.3.9
|
|
51
|
+
!!! version-added "Added in version 0.3.9"
|
|
58
52
|
|
|
59
53
|
May also hold extra provider-specific keys.
|
|
60
54
|
|
|
@@ -72,6 +66,7 @@ class InputTokenDetails(TypedDict, total=False):
|
|
|
72
66
|
|
|
73
67
|
Since there was a cache hit, the tokens were read from the cache. More precisely,
|
|
74
68
|
the model state given these tokens was read from the cache.
|
|
69
|
+
|
|
75
70
|
"""
|
|
76
71
|
|
|
77
72
|
|
|
@@ -81,15 +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:
|
|
79
|
+
```python
|
|
80
|
+
{
|
|
81
|
+
"audio": 10,
|
|
82
|
+
"reasoning": 200,
|
|
83
|
+
}
|
|
84
|
+
```
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{
|
|
88
|
-
"audio": 10,
|
|
89
|
-
"reasoning": 200,
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.. versionadded:: 0.3.9
|
|
86
|
+
!!! version-added "Added in version 0.3.9"
|
|
93
87
|
|
|
94
88
|
"""
|
|
95
89
|
|
|
@@ -100,6 +94,7 @@ class OutputTokenDetails(TypedDict, total=False):
|
|
|
100
94
|
|
|
101
95
|
Tokens generated by the model in a chain of thought process (i.e. by OpenAI's o1
|
|
102
96
|
models) that are not returned as part of model output.
|
|
97
|
+
|
|
103
98
|
"""
|
|
104
99
|
|
|
105
100
|
|
|
@@ -109,27 +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:
|
|
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
|
+
```
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
"input_tokens": 350,
|
|
117
|
-
"output_tokens": 240,
|
|
118
|
-
"total_tokens": 590,
|
|
119
|
-
"input_token_details": {
|
|
120
|
-
"audio": 10,
|
|
121
|
-
"cache_creation": 200,
|
|
122
|
-
"cache_read": 100,
|
|
123
|
-
},
|
|
124
|
-
"output_token_details": {
|
|
125
|
-
"audio": 10,
|
|
126
|
-
"reasoning": 200,
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.. versionchanged:: 0.3.9
|
|
131
|
-
|
|
132
|
-
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`.
|
|
133
126
|
|
|
134
127
|
"""
|
|
135
128
|
|
|
@@ -148,57 +141,150 @@ class UsageMetadata(TypedDict):
|
|
|
148
141
|
"""Breakdown of output token counts.
|
|
149
142
|
|
|
150
143
|
Does *not* need to sum to full output token count. Does *not* need to have all keys.
|
|
144
|
+
|
|
151
145
|
"""
|
|
152
146
|
|
|
153
147
|
|
|
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
|
-
"""
|
|
163
156
|
|
|
164
|
-
example: bool = False
|
|
165
|
-
"""Use to denote that a message is part of an example conversation.
|
|
166
|
-
|
|
167
|
-
At the moment, this is ignored by most models. Usage is discouraged.
|
|
168
157
|
"""
|
|
169
158
|
|
|
170
159
|
tool_calls: list[ToolCall] = []
|
|
171
|
-
"""If
|
|
160
|
+
"""If present, tool calls associated with the message."""
|
|
172
161
|
invalid_tool_calls: list[InvalidToolCall] = []
|
|
173
|
-
"""If
|
|
174
|
-
usage_metadata:
|
|
175
|
-
"""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.
|
|
176
165
|
|
|
177
166
|
This is a standard representation of token usage that is consistent across models.
|
|
178
167
|
"""
|
|
179
168
|
|
|
180
169
|
type: Literal["ai"] = "ai"
|
|
181
|
-
"""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: ...
|
|
182
178
|
|
|
179
|
+
@overload
|
|
183
180
|
def __init__(
|
|
184
|
-
self,
|
|
181
|
+
self,
|
|
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,
|
|
191
|
+
**kwargs: Any,
|
|
185
192
|
) -> None:
|
|
186
|
-
"""
|
|
193
|
+
"""Initialize an `AIMessage`.
|
|
194
|
+
|
|
195
|
+
Specify `content` as positional arg or `content_blocks` for typing.
|
|
187
196
|
|
|
188
197
|
Args:
|
|
189
198
|
content: The content of the message.
|
|
190
|
-
|
|
199
|
+
content_blocks: Typed standard content.
|
|
200
|
+
**kwargs: Additional arguments to pass to the parent class.
|
|
191
201
|
"""
|
|
192
|
-
|
|
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)
|
|
193
216
|
|
|
194
217
|
@property
|
|
195
218
|
def lc_attributes(self) -> dict:
|
|
196
|
-
"""
|
|
219
|
+
"""Attributes to be serialized.
|
|
220
|
+
|
|
221
|
+
Includes all attributes, even if they are derived from other initialization
|
|
222
|
+
arguments.
|
|
223
|
+
"""
|
|
197
224
|
return {
|
|
198
225
|
"tool_calls": self.tool_calls,
|
|
199
226
|
"invalid_tool_calls": self.invalid_tool_calls,
|
|
200
227
|
}
|
|
201
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
|
+
|
|
202
288
|
# TODO: remove this logic if possible, reducing breaking nature of changes
|
|
203
289
|
@model_validator(mode="before")
|
|
204
290
|
@classmethod
|
|
@@ -227,7 +313,9 @@ class AIMessage(BaseMessage):
|
|
|
227
313
|
# Ensure "type" is properly set on all tool call-like dicts.
|
|
228
314
|
if tool_calls := values.get("tool_calls"):
|
|
229
315
|
values["tool_calls"] = [
|
|
230
|
-
create_tool_call(
|
|
316
|
+
create_tool_call(
|
|
317
|
+
**{k: v for k, v in tc.items() if k not in ("type", "extras")}
|
|
318
|
+
)
|
|
231
319
|
for tc in tool_calls
|
|
232
320
|
]
|
|
233
321
|
if invalid_tool_calls := values.get("invalid_tool_calls"):
|
|
@@ -246,19 +334,19 @@ class AIMessage(BaseMessage):
|
|
|
246
334
|
|
|
247
335
|
@override
|
|
248
336
|
def pretty_repr(self, html: bool = False) -> str:
|
|
249
|
-
"""Return a pretty representation of the message.
|
|
337
|
+
"""Return a pretty representation of the message for display.
|
|
250
338
|
|
|
251
339
|
Args:
|
|
252
340
|
html: Whether to return an HTML-formatted string.
|
|
253
|
-
Defaults to False.
|
|
254
341
|
|
|
255
342
|
Returns:
|
|
256
343
|
A pretty representation of the message.
|
|
344
|
+
|
|
257
345
|
"""
|
|
258
346
|
base = super().pretty_repr(html=html)
|
|
259
347
|
lines = []
|
|
260
348
|
|
|
261
|
-
def _format_tool_args(tc:
|
|
349
|
+
def _format_tool_args(tc: ToolCall | InvalidToolCall) -> list[str]:
|
|
262
350
|
lines = [
|
|
263
351
|
f" {tc.get('name', 'Tool')} ({tc.get('id')})",
|
|
264
352
|
f" Call ID: {tc.get('id')}",
|
|
@@ -286,33 +374,90 @@ class AIMessage(BaseMessage):
|
|
|
286
374
|
|
|
287
375
|
|
|
288
376
|
class AIMessageChunk(AIMessage, BaseMessageChunk):
|
|
289
|
-
"""Message chunk from an AI."""
|
|
377
|
+
"""Message chunk from an AI (yielded when streaming)."""
|
|
290
378
|
|
|
291
379
|
# Ignoring mypy re-assignment here since we're overriding the value
|
|
292
380
|
# to make sure that the chunk variant can be discriminated from the
|
|
293
381
|
# non-chunk variant.
|
|
294
382
|
type: Literal["AIMessageChunk"] = "AIMessageChunk" # type: ignore[assignment]
|
|
295
|
-
"""The type of the message (used for deserialization).
|
|
296
|
-
Defaults to "AIMessageChunk"."""
|
|
383
|
+
"""The type of the message (used for deserialization)."""
|
|
297
384
|
|
|
298
385
|
tool_call_chunks: list[ToolCallChunk] = []
|
|
299
386
|
"""If provided, tool call chunks associated with the message."""
|
|
300
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
|
+
|
|
301
395
|
@property
|
|
302
396
|
def lc_attributes(self) -> dict:
|
|
303
|
-
"""
|
|
397
|
+
"""Attributes to be serialized, even if they are derived from other initialization args.""" # noqa: E501
|
|
304
398
|
return {
|
|
305
399
|
"tool_calls": self.tool_calls,
|
|
306
400
|
"invalid_tool_calls": self.invalid_tool_calls,
|
|
307
401
|
}
|
|
308
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
|
+
|
|
309
457
|
@model_validator(mode="after")
|
|
310
458
|
def init_tool_calls(self) -> Self:
|
|
311
459
|
"""Initialize tool calls from tool call chunks.
|
|
312
460
|
|
|
313
|
-
Args:
|
|
314
|
-
values: The values to validate.
|
|
315
|
-
|
|
316
461
|
Returns:
|
|
317
462
|
The values with tool calls initialized.
|
|
318
463
|
|
|
@@ -358,7 +503,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
|
|
358
503
|
|
|
359
504
|
for chunk in self.tool_call_chunks:
|
|
360
505
|
try:
|
|
361
|
-
args_ = parse_partial_json(chunk["args"]) if chunk["args"]
|
|
506
|
+
args_ = parse_partial_json(chunk["args"]) if chunk["args"] else {}
|
|
362
507
|
if isinstance(args_, dict):
|
|
363
508
|
tool_calls.append(
|
|
364
509
|
create_tool_call(
|
|
@@ -373,10 +518,73 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
|
|
373
518
|
add_chunk_to_invalid_tool_calls(chunk)
|
|
374
519
|
self.tool_calls = tool_calls
|
|
375
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
|
|
376
575
|
return self
|
|
377
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
|
+
|
|
378
586
|
@override
|
|
379
|
-
def __add__(self, other: Any) -> BaseMessageChunk:
|
|
587
|
+
def __add__(self, other: Any) -> BaseMessageChunk:
|
|
380
588
|
if isinstance(other, AIMessageChunk):
|
|
381
589
|
return add_ai_message_chunks(self, other)
|
|
382
590
|
if isinstance(other, (list, tuple)) and all(
|
|
@@ -389,11 +597,16 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
|
|
|
389
597
|
def add_ai_message_chunks(
|
|
390
598
|
left: AIMessageChunk, *others: AIMessageChunk
|
|
391
599
|
) -> AIMessageChunk:
|
|
392
|
-
"""Add multiple
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
600
|
+
"""Add multiple `AIMessageChunk`s together.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
left: The first `AIMessageChunk`.
|
|
604
|
+
*others: Other `AIMessageChunk`s to add.
|
|
396
605
|
|
|
606
|
+
Returns:
|
|
607
|
+
The resulting `AIMessageChunk`.
|
|
608
|
+
|
|
609
|
+
"""
|
|
397
610
|
content = merge_content(left.content, *(o.content for o in others))
|
|
398
611
|
additional_kwargs = merge_dicts(
|
|
399
612
|
left.additional_kwargs, *(o.additional_kwargs for o in others)
|
|
@@ -420,7 +633,7 @@ def add_ai_message_chunks(
|
|
|
420
633
|
|
|
421
634
|
# Token usage
|
|
422
635
|
if left.usage_metadata or any(o.usage_metadata is not None for o in others):
|
|
423
|
-
usage_metadata:
|
|
636
|
+
usage_metadata: UsageMetadata | None = left.usage_metadata
|
|
424
637
|
for other in others:
|
|
425
638
|
usage_metadata = add_usage(usage_metadata, other.usage_metadata)
|
|
426
639
|
else:
|
|
@@ -432,71 +645,79 @@ def add_ai_message_chunks(
|
|
|
432
645
|
for id_ in candidates:
|
|
433
646
|
if (
|
|
434
647
|
id_
|
|
435
|
-
and not id_.startswith(
|
|
436
|
-
and not id_.startswith(
|
|
648
|
+
and not id_.startswith(LC_ID_PREFIX)
|
|
649
|
+
and not id_.startswith(LC_AUTO_PREFIX)
|
|
437
650
|
):
|
|
438
651
|
chunk_id = id_
|
|
439
652
|
break
|
|
440
653
|
else:
|
|
441
|
-
# second pass: prefer
|
|
654
|
+
# second pass: prefer lc_run-* ids over lc_* ids
|
|
442
655
|
for id_ in candidates:
|
|
443
|
-
if id_ and id_.startswith(
|
|
656
|
+
if id_ and id_.startswith(LC_ID_PREFIX):
|
|
444
657
|
chunk_id = id_
|
|
445
658
|
break
|
|
446
659
|
else:
|
|
447
|
-
# third pass: take any remaining id (
|
|
660
|
+
# third pass: take any remaining id (auto-generated lc_* ids)
|
|
448
661
|
for id_ in candidates:
|
|
449
662
|
if id_:
|
|
450
663
|
chunk_id = id_
|
|
451
664
|
break
|
|
452
665
|
|
|
666
|
+
chunk_position: Literal["last"] | None = (
|
|
667
|
+
"last" if any(x.chunk_position == "last" for x in [left, *others]) else None
|
|
668
|
+
)
|
|
669
|
+
|
|
453
670
|
return left.__class__(
|
|
454
|
-
example=left.example,
|
|
455
671
|
content=content,
|
|
456
672
|
additional_kwargs=additional_kwargs,
|
|
457
673
|
tool_call_chunks=tool_call_chunks,
|
|
458
674
|
response_metadata=response_metadata,
|
|
459
675
|
usage_metadata=usage_metadata,
|
|
460
676
|
id=chunk_id,
|
|
677
|
+
chunk_position=chunk_position,
|
|
461
678
|
)
|
|
462
679
|
|
|
463
680
|
|
|
464
|
-
def add_usage(
|
|
465
|
-
left: Optional[UsageMetadata], right: Optional[UsageMetadata]
|
|
466
|
-
) -> UsageMetadata:
|
|
681
|
+
def add_usage(left: UsageMetadata | None, right: UsageMetadata | None) -> UsageMetadata:
|
|
467
682
|
"""Recursively add two UsageMetadata objects.
|
|
468
683
|
|
|
469
684
|
Example:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
)
|
|
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
|
+
)
|
|
486
700
|
|
|
487
|
-
|
|
701
|
+
add_usage(left, right)
|
|
702
|
+
```
|
|
488
703
|
|
|
489
704
|
results in
|
|
490
705
|
|
|
491
|
-
|
|
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
|
+
```
|
|
715
|
+
Args:
|
|
716
|
+
left: The first `UsageMetadata` object.
|
|
717
|
+
right: The second `UsageMetadata` object.
|
|
492
718
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
output_tokens=10,
|
|
496
|
-
total_tokens=15,
|
|
497
|
-
input_token_details=InputTokenDetails(cache_read=3),
|
|
498
|
-
output_token_details=OutputTokenDetails(reasoning=4)
|
|
499
|
-
)
|
|
719
|
+
Returns:
|
|
720
|
+
The sum of the two `UsageMetadata` objects.
|
|
500
721
|
|
|
501
722
|
"""
|
|
502
723
|
if not (left or right):
|
|
@@ -517,43 +738,49 @@ def add_usage(
|
|
|
517
738
|
|
|
518
739
|
|
|
519
740
|
def subtract_usage(
|
|
520
|
-
left:
|
|
741
|
+
left: UsageMetadata | None, right: UsageMetadata | None
|
|
521
742
|
) -> UsageMetadata:
|
|
522
|
-
"""Recursively subtract two UsageMetadata objects.
|
|
743
|
+
"""Recursively subtract two `UsageMetadata` objects.
|
|
523
744
|
|
|
524
|
-
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)`.
|
|
525
746
|
|
|
526
747
|
Example:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
)
|
|
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
|
+
)
|
|
543
763
|
|
|
544
|
-
|
|
764
|
+
subtract_usage(left, right)
|
|
765
|
+
```
|
|
545
766
|
|
|
546
767
|
results in
|
|
547
768
|
|
|
548
|
-
|
|
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
|
+
```
|
|
778
|
+
Args:
|
|
779
|
+
left: The first `UsageMetadata` object.
|
|
780
|
+
right: The second `UsageMetadata` object.
|
|
549
781
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
output_tokens=2,
|
|
553
|
-
total_tokens=4,
|
|
554
|
-
input_token_details=InputTokenDetails(cache_read=4),
|
|
555
|
-
output_token_details=OutputTokenDetails(reasoning=0)
|
|
556
|
-
)
|
|
782
|
+
Returns:
|
|
783
|
+
The resulting `UsageMetadata` after subtraction.
|
|
557
784
|
|
|
558
785
|
"""
|
|
559
786
|
if not (left or right):
|