langchain 1.0.5__py3-none-any.whl → 1.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langchain/__init__.py +1 -1
- langchain/agents/__init__.py +1 -7
- langchain/agents/factory.py +153 -79
- langchain/agents/middleware/__init__.py +18 -23
- langchain/agents/middleware/_execution.py +29 -32
- langchain/agents/middleware/_redaction.py +108 -22
- langchain/agents/middleware/_retry.py +123 -0
- langchain/agents/middleware/context_editing.py +47 -25
- langchain/agents/middleware/file_search.py +19 -14
- langchain/agents/middleware/human_in_the_loop.py +87 -57
- langchain/agents/middleware/model_call_limit.py +64 -18
- langchain/agents/middleware/model_fallback.py +7 -9
- langchain/agents/middleware/model_retry.py +307 -0
- langchain/agents/middleware/pii.py +82 -29
- langchain/agents/middleware/shell_tool.py +254 -107
- langchain/agents/middleware/summarization.py +469 -95
- langchain/agents/middleware/todo.py +129 -31
- langchain/agents/middleware/tool_call_limit.py +105 -71
- langchain/agents/middleware/tool_emulator.py +47 -38
- langchain/agents/middleware/tool_retry.py +183 -164
- langchain/agents/middleware/tool_selection.py +81 -37
- langchain/agents/middleware/types.py +856 -427
- langchain/agents/structured_output.py +65 -42
- langchain/chat_models/__init__.py +1 -7
- langchain/chat_models/base.py +253 -196
- langchain/embeddings/__init__.py +0 -5
- langchain/embeddings/base.py +79 -65
- langchain/messages/__init__.py +0 -5
- langchain/tools/__init__.py +1 -7
- {langchain-1.0.5.dist-info → langchain-1.2.4.dist-info}/METADATA +5 -7
- langchain-1.2.4.dist-info/RECORD +36 -0
- {langchain-1.0.5.dist-info → langchain-1.2.4.dist-info}/WHEEL +1 -1
- langchain-1.0.5.dist-info/RECORD +0 -34
- {langchain-1.0.5.dist-info → langchain-1.2.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,12 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import TYPE_CHECKING, Annotated, Literal, Union
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from collections.abc import Awaitable, Callable
|
|
11
|
-
|
|
12
|
-
from langchain.tools import BaseTool
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Union
|
|
13
8
|
|
|
14
9
|
from langchain_core.language_models.chat_models import BaseChatModel
|
|
15
10
|
from langchain_core.messages import HumanMessage
|
|
@@ -24,6 +19,11 @@ from langchain.agents.middleware.types import (
|
|
|
24
19
|
)
|
|
25
20
|
from langchain.chat_models.base import init_chat_model
|
|
26
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Awaitable, Callable
|
|
24
|
+
|
|
25
|
+
from langchain.tools import BaseTool
|
|
26
|
+
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
29
|
DEFAULT_SYSTEM_PROMPT = (
|
|
@@ -42,21 +42,25 @@ class _SelectionRequest:
|
|
|
42
42
|
valid_tool_names: list[str]
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def _create_tool_selection_response(tools: list[BaseTool]) -> TypeAdapter:
|
|
45
|
+
def _create_tool_selection_response(tools: list[BaseTool]) -> TypeAdapter[Any]:
|
|
46
46
|
"""Create a structured output schema for tool selection.
|
|
47
47
|
|
|
48
48
|
Args:
|
|
49
49
|
tools: Available tools to include in the schema.
|
|
50
50
|
|
|
51
51
|
Returns:
|
|
52
|
-
TypeAdapter for a schema where each tool name is a Literal with its
|
|
52
|
+
`TypeAdapter` for a schema where each tool name is a `Literal` with its
|
|
53
|
+
description.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
AssertionError: If `tools` is empty.
|
|
53
57
|
"""
|
|
54
58
|
if not tools:
|
|
55
59
|
msg = "Invalid usage: tools must be non-empty"
|
|
56
60
|
raise AssertionError(msg)
|
|
57
61
|
|
|
58
62
|
# Create a Union of Annotated Literal types for each tool name with description
|
|
59
|
-
#
|
|
63
|
+
# For instance: Union[Annotated[Literal["tool1"], Field(description="...")], ...]
|
|
60
64
|
literals = [
|
|
61
65
|
Annotated[Literal[tool.name], Field(description=tool.description)] for tool in tools
|
|
62
66
|
]
|
|
@@ -92,23 +96,25 @@ class LLMToolSelectorMiddleware(AgentMiddleware):
|
|
|
92
96
|
and helps the main model focus on the right tools.
|
|
93
97
|
|
|
94
98
|
Examples:
|
|
95
|
-
Limit to 3 tools
|
|
96
|
-
```python
|
|
97
|
-
from langchain.agents.middleware import LLMToolSelectorMiddleware
|
|
99
|
+
!!! example "Limit to 3 tools"
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
```python
|
|
102
|
+
from langchain.agents.middleware import LLMToolSelectorMiddleware
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
middleware = LLMToolSelectorMiddleware(max_tools=3)
|
|
105
|
+
|
|
106
|
+
agent = create_agent(
|
|
107
|
+
model="openai:gpt-4o",
|
|
108
|
+
tools=[tool1, tool2, tool3, tool4, tool5],
|
|
109
|
+
middleware=[middleware],
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
!!! example "Use a smaller model for selection"
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```
|
|
115
|
+
```python
|
|
116
|
+
middleware = LLMToolSelectorMiddleware(model="openai:gpt-4o-mini", max_tools=2)
|
|
117
|
+
```
|
|
112
118
|
"""
|
|
113
119
|
|
|
114
120
|
def __init__(
|
|
@@ -122,13 +128,20 @@ class LLMToolSelectorMiddleware(AgentMiddleware):
|
|
|
122
128
|
"""Initialize the tool selector.
|
|
123
129
|
|
|
124
130
|
Args:
|
|
125
|
-
model: Model to use for selection.
|
|
126
|
-
|
|
131
|
+
model: Model to use for selection.
|
|
132
|
+
|
|
133
|
+
If not provided, uses the agent's main model.
|
|
134
|
+
|
|
135
|
+
Can be a model identifier string or `BaseChatModel` instance.
|
|
127
136
|
system_prompt: Instructions for the selection model.
|
|
128
|
-
max_tools: Maximum number of tools to select.
|
|
129
|
-
|
|
137
|
+
max_tools: Maximum number of tools to select.
|
|
138
|
+
|
|
139
|
+
If the model selects more, only the first `max_tools` will be used.
|
|
140
|
+
|
|
141
|
+
If not specified, there is no limit.
|
|
130
142
|
always_include: Tool names to always include regardless of selection.
|
|
131
|
-
|
|
143
|
+
|
|
144
|
+
These do not count against the `max_tools` limit.
|
|
132
145
|
"""
|
|
133
146
|
super().__init__()
|
|
134
147
|
self.system_prompt = system_prompt
|
|
@@ -143,8 +156,16 @@ class LLMToolSelectorMiddleware(AgentMiddleware):
|
|
|
143
156
|
def _prepare_selection_request(self, request: ModelRequest) -> _SelectionRequest | None:
|
|
144
157
|
"""Prepare inputs for tool selection.
|
|
145
158
|
|
|
159
|
+
Args:
|
|
160
|
+
request: the model request.
|
|
161
|
+
|
|
146
162
|
Returns:
|
|
147
|
-
SelectionRequest with prepared inputs, or None if no selection is
|
|
163
|
+
`SelectionRequest` with prepared inputs, or `None` if no selection is
|
|
164
|
+
needed.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If tools in `always_include` are not found in the request.
|
|
168
|
+
AssertionError: If no user message is found in the request messages.
|
|
148
169
|
"""
|
|
149
170
|
# If no tools available, return None
|
|
150
171
|
if not request.tools or len(request.tools) == 0:
|
|
@@ -206,12 +227,12 @@ class LLMToolSelectorMiddleware(AgentMiddleware):
|
|
|
206
227
|
|
|
207
228
|
def _process_selection_response(
|
|
208
229
|
self,
|
|
209
|
-
response: dict,
|
|
230
|
+
response: dict[str, Any],
|
|
210
231
|
available_tools: list[BaseTool],
|
|
211
232
|
valid_tool_names: list[str],
|
|
212
233
|
request: ModelRequest,
|
|
213
234
|
) -> ModelRequest:
|
|
214
|
-
"""Process the selection response and return filtered ModelRequest
|
|
235
|
+
"""Process the selection response and return filtered `ModelRequest`."""
|
|
215
236
|
selected_tool_names: list[str] = []
|
|
216
237
|
invalid_tool_selections = []
|
|
217
238
|
|
|
@@ -244,15 +265,26 @@ class LLMToolSelectorMiddleware(AgentMiddleware):
|
|
|
244
265
|
# Also preserve any provider-specific tool dicts from the original request
|
|
245
266
|
provider_tools = [tool for tool in request.tools if isinstance(tool, dict)]
|
|
246
267
|
|
|
247
|
-
request.tools
|
|
248
|
-
return request
|
|
268
|
+
return request.override(tools=[*selected_tools, *provider_tools])
|
|
249
269
|
|
|
250
270
|
def wrap_model_call(
|
|
251
271
|
self,
|
|
252
272
|
request: ModelRequest,
|
|
253
273
|
handler: Callable[[ModelRequest], ModelResponse],
|
|
254
274
|
) -> ModelCallResult:
|
|
255
|
-
"""Filter tools based on LLM selection before invoking the model via handler.
|
|
275
|
+
"""Filter tools based on LLM selection before invoking the model via handler.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
request: Model request to execute (includes state and runtime).
|
|
279
|
+
handler: Async callback that executes the model request and returns
|
|
280
|
+
`ModelResponse`.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
The model call result.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
AssertionError: If the selection model response is not a dict.
|
|
287
|
+
"""
|
|
256
288
|
selection_request = self._prepare_selection_request(request)
|
|
257
289
|
if selection_request is None:
|
|
258
290
|
return handler(request)
|
|
@@ -272,7 +304,7 @@ class LLMToolSelectorMiddleware(AgentMiddleware):
|
|
|
272
304
|
# Response should be a dict since we're passing a schema (not a Pydantic model class)
|
|
273
305
|
if not isinstance(response, dict):
|
|
274
306
|
msg = f"Expected dict response, got {type(response)}"
|
|
275
|
-
raise AssertionError(msg)
|
|
307
|
+
raise AssertionError(msg) # noqa: TRY004
|
|
276
308
|
modified_request = self._process_selection_response(
|
|
277
309
|
response, selection_request.available_tools, selection_request.valid_tool_names, request
|
|
278
310
|
)
|
|
@@ -283,7 +315,19 @@ class LLMToolSelectorMiddleware(AgentMiddleware):
|
|
|
283
315
|
request: ModelRequest,
|
|
284
316
|
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
285
317
|
) -> ModelCallResult:
|
|
286
|
-
"""Filter tools based on LLM selection before invoking the model via handler.
|
|
318
|
+
"""Filter tools based on LLM selection before invoking the model via handler.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
request: Model request to execute (includes state and runtime).
|
|
322
|
+
handler: Async callback that executes the model request and returns
|
|
323
|
+
`ModelResponse`.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
The model call result.
|
|
327
|
+
|
|
328
|
+
Raises:
|
|
329
|
+
AssertionError: If the selection model response is not a dict.
|
|
330
|
+
"""
|
|
287
331
|
selection_request = self._prepare_selection_request(request)
|
|
288
332
|
if selection_request is None:
|
|
289
333
|
return await handler(request)
|
|
@@ -303,7 +347,7 @@ class LLMToolSelectorMiddleware(AgentMiddleware):
|
|
|
303
347
|
# Response should be a dict since we're passing a schema (not a Pydantic model class)
|
|
304
348
|
if not isinstance(response, dict):
|
|
305
349
|
msg = f"Expected dict response, got {type(response)}"
|
|
306
|
-
raise AssertionError(msg)
|
|
350
|
+
raise AssertionError(msg) # noqa: TRY004
|
|
307
351
|
modified_request = self._process_selection_response(
|
|
308
352
|
response, selection_request.available_tools, selection_request.valid_tool_names, request
|
|
309
353
|
)
|