langchain 1.0.5__py3-none-any.whl → 1.2.3__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 +99 -40
- langchain/agents/middleware/__init__.py +5 -7
- langchain/agents/middleware/_execution.py +21 -20
- langchain/agents/middleware/_redaction.py +27 -12
- langchain/agents/middleware/_retry.py +123 -0
- langchain/agents/middleware/context_editing.py +26 -22
- langchain/agents/middleware/file_search.py +18 -13
- langchain/agents/middleware/human_in_the_loop.py +60 -54
- langchain/agents/middleware/model_call_limit.py +63 -17
- langchain/agents/middleware/model_fallback.py +7 -9
- langchain/agents/middleware/model_retry.py +300 -0
- langchain/agents/middleware/pii.py +80 -27
- langchain/agents/middleware/shell_tool.py +230 -103
- langchain/agents/middleware/summarization.py +439 -90
- langchain/agents/middleware/todo.py +111 -27
- langchain/agents/middleware/tool_call_limit.py +105 -71
- langchain/agents/middleware/tool_emulator.py +42 -33
- langchain/agents/middleware/tool_retry.py +171 -159
- langchain/agents/middleware/tool_selection.py +37 -27
- langchain/agents/middleware/types.py +754 -392
- langchain/agents/structured_output.py +22 -12
- langchain/chat_models/__init__.py +1 -7
- langchain/chat_models/base.py +233 -184
- 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.3.dist-info}/METADATA +3 -5
- langchain-1.2.3.dist-info/RECORD +36 -0
- {langchain-1.0.5.dist-info → langchain-1.2.3.dist-info}/WHEEL +1 -1
- langchain-1.0.5.dist-info/RECORD +0 -34
- {langchain-1.0.5.dist-info → langchain-1.2.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"""Planning and task management middleware for agents."""
|
|
2
|
-
# ruff: noqa: E501
|
|
3
2
|
|
|
4
3
|
from __future__ import annotations
|
|
5
4
|
|
|
6
|
-
from typing import TYPE_CHECKING, Annotated, Literal
|
|
5
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
|
|
7
6
|
|
|
8
7
|
if TYPE_CHECKING:
|
|
9
8
|
from collections.abc import Awaitable, Callable
|
|
10
9
|
|
|
11
|
-
from
|
|
10
|
+
from langgraph.runtime import Runtime
|
|
11
|
+
|
|
12
|
+
from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
|
|
12
13
|
from langchain_core.tools import tool
|
|
13
14
|
from langgraph.types import Command
|
|
14
15
|
from typing_extensions import NotRequired, TypedDict
|
|
@@ -99,7 +100,7 @@ It is important to skip using this tool when:
|
|
|
99
100
|
- Use clear, descriptive task names
|
|
100
101
|
|
|
101
102
|
Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully
|
|
102
|
-
Remember: If you only need to make a few tool calls to complete a task, and it is clear what you need to do, it is better to just do the task directly and NOT call this tool at all."""
|
|
103
|
+
Remember: If you only need to make a few tool calls to complete a task, and it is clear what you need to do, it is better to just do the task directly and NOT call this tool at all.""" # noqa: E501
|
|
103
104
|
|
|
104
105
|
WRITE_TODOS_SYSTEM_PROMPT = """## `write_todos`
|
|
105
106
|
|
|
@@ -113,7 +114,7 @@ Writing todos takes time and tokens, use it when it is helpful for managing comp
|
|
|
113
114
|
|
|
114
115
|
## Important To-Do List Usage Notes to Remember
|
|
115
116
|
- The `write_todos` tool should never be called multiple times in parallel.
|
|
116
|
-
- Don't be afraid to revise the To-Do list as you go. New information may reveal new tasks that need to be done, or old tasks that are irrelevant."""
|
|
117
|
+
- Don't be afraid to revise the To-Do list as you go. New information may reveal new tasks that need to be done, or old tasks that are irrelevant.""" # noqa: E501
|
|
117
118
|
|
|
118
119
|
|
|
119
120
|
@tool(description=WRITE_TODOS_TOOL_DESCRIPTION)
|
|
@@ -136,7 +137,9 @@ class TodoListMiddleware(AgentMiddleware):
|
|
|
136
137
|
into task completion status.
|
|
137
138
|
|
|
138
139
|
The middleware automatically injects system prompts that guide the agent on when
|
|
139
|
-
and how to use the todo functionality effectively.
|
|
140
|
+
and how to use the todo functionality effectively. It also enforces that the
|
|
141
|
+
`write_todos` tool is called at most once per model turn, since the tool replaces
|
|
142
|
+
the entire todo list and parallel calls would create ambiguity about precedence.
|
|
140
143
|
|
|
141
144
|
Example:
|
|
142
145
|
```python
|
|
@@ -150,12 +153,6 @@ class TodoListMiddleware(AgentMiddleware):
|
|
|
150
153
|
|
|
151
154
|
print(result["todos"]) # Array of todo items with status tracking
|
|
152
155
|
```
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
system_prompt: Custom system prompt to guide the agent on using the todo tool.
|
|
156
|
-
If not provided, uses the default `WRITE_TODOS_SYSTEM_PROMPT`.
|
|
157
|
-
tool_description: Custom description for the write_todos tool.
|
|
158
|
-
If not provided, uses the default `WRITE_TODOS_TOOL_DESCRIPTION`.
|
|
159
156
|
"""
|
|
160
157
|
|
|
161
158
|
state_schema = PlanningState
|
|
@@ -166,11 +163,12 @@ class TodoListMiddleware(AgentMiddleware):
|
|
|
166
163
|
system_prompt: str = WRITE_TODOS_SYSTEM_PROMPT,
|
|
167
164
|
tool_description: str = WRITE_TODOS_TOOL_DESCRIPTION,
|
|
168
165
|
) -> None:
|
|
169
|
-
"""Initialize the TodoListMiddleware with optional custom prompts.
|
|
166
|
+
"""Initialize the `TodoListMiddleware` with optional custom prompts.
|
|
170
167
|
|
|
171
168
|
Args:
|
|
172
|
-
system_prompt: Custom system prompt to guide the agent on using the todo
|
|
173
|
-
|
|
169
|
+
system_prompt: Custom system prompt to guide the agent on using the todo
|
|
170
|
+
tool.
|
|
171
|
+
tool_description: Custom description for the `write_todos` tool.
|
|
174
172
|
"""
|
|
175
173
|
super().__init__()
|
|
176
174
|
self.system_prompt = system_prompt
|
|
@@ -198,23 +196,109 @@ class TodoListMiddleware(AgentMiddleware):
|
|
|
198
196
|
request: ModelRequest,
|
|
199
197
|
handler: Callable[[ModelRequest], ModelResponse],
|
|
200
198
|
) -> ModelCallResult:
|
|
201
|
-
"""Update the system
|
|
202
|
-
request.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
199
|
+
"""Update the system message to include the todo system prompt."""
|
|
200
|
+
if request.system_message is not None:
|
|
201
|
+
new_system_content = [
|
|
202
|
+
*request.system_message.content_blocks,
|
|
203
|
+
{"type": "text", "text": f"\n\n{self.system_prompt}"},
|
|
204
|
+
]
|
|
205
|
+
else:
|
|
206
|
+
new_system_content = [{"type": "text", "text": self.system_prompt}]
|
|
207
|
+
new_system_message = SystemMessage(
|
|
208
|
+
content=cast("list[str | dict[str, str]]", new_system_content)
|
|
206
209
|
)
|
|
207
|
-
return handler(request)
|
|
210
|
+
return handler(request.override(system_message=new_system_message))
|
|
208
211
|
|
|
209
212
|
async def awrap_model_call(
|
|
210
213
|
self,
|
|
211
214
|
request: ModelRequest,
|
|
212
215
|
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
213
216
|
) -> ModelCallResult:
|
|
214
|
-
"""Update the system
|
|
215
|
-
request.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
"""Update the system message to include the todo system prompt (async version)."""
|
|
218
|
+
if request.system_message is not None:
|
|
219
|
+
new_system_content = [
|
|
220
|
+
*request.system_message.content_blocks,
|
|
221
|
+
{"type": "text", "text": f"\n\n{self.system_prompt}"},
|
|
222
|
+
]
|
|
223
|
+
else:
|
|
224
|
+
new_system_content = [{"type": "text", "text": self.system_prompt}]
|
|
225
|
+
new_system_message = SystemMessage(
|
|
226
|
+
content=cast("list[str | dict[str, str]]", new_system_content)
|
|
219
227
|
)
|
|
220
|
-
return await handler(request)
|
|
228
|
+
return await handler(request.override(system_message=new_system_message))
|
|
229
|
+
|
|
230
|
+
def after_model(
|
|
231
|
+
self,
|
|
232
|
+
state: AgentState,
|
|
233
|
+
runtime: Runtime, # noqa: ARG002
|
|
234
|
+
) -> dict[str, Any] | None:
|
|
235
|
+
"""Check for parallel write_todos tool calls and return errors if detected.
|
|
236
|
+
|
|
237
|
+
The todo list is designed to be updated at most once per model turn. Since
|
|
238
|
+
the `write_todos` tool replaces the entire todo list with each call, making
|
|
239
|
+
multiple parallel calls would create ambiguity about which update should take
|
|
240
|
+
precedence. This method prevents such conflicts by rejecting any response that
|
|
241
|
+
contains multiple write_todos tool calls.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
state: The current agent state containing messages.
|
|
245
|
+
runtime: The LangGraph runtime instance.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
A dict containing error ToolMessages for each write_todos call if multiple
|
|
249
|
+
parallel calls are detected, otherwise None to allow normal execution.
|
|
250
|
+
"""
|
|
251
|
+
messages = state["messages"]
|
|
252
|
+
if not messages:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
last_ai_msg = next((msg for msg in reversed(messages) if isinstance(msg, AIMessage)), None)
|
|
256
|
+
if not last_ai_msg or not last_ai_msg.tool_calls:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# Count write_todos tool calls
|
|
260
|
+
write_todos_calls = [tc for tc in last_ai_msg.tool_calls if tc["name"] == "write_todos"]
|
|
261
|
+
|
|
262
|
+
if len(write_todos_calls) > 1:
|
|
263
|
+
# Create error tool messages for all write_todos calls
|
|
264
|
+
error_messages = [
|
|
265
|
+
ToolMessage(
|
|
266
|
+
content=(
|
|
267
|
+
"Error: The `write_todos` tool should never be called multiple times "
|
|
268
|
+
"in parallel. Please call it only once per model invocation to update "
|
|
269
|
+
"the todo list."
|
|
270
|
+
),
|
|
271
|
+
tool_call_id=tc["id"],
|
|
272
|
+
status="error",
|
|
273
|
+
)
|
|
274
|
+
for tc in write_todos_calls
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
# Keep the tool calls in the AI message but return error messages
|
|
278
|
+
# This follows the same pattern as HumanInTheLoopMiddleware
|
|
279
|
+
return {"messages": error_messages}
|
|
280
|
+
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
async def aafter_model(
|
|
284
|
+
self,
|
|
285
|
+
state: AgentState,
|
|
286
|
+
runtime: Runtime,
|
|
287
|
+
) -> dict[str, Any] | None:
|
|
288
|
+
"""Check for parallel write_todos tool calls and return errors if detected.
|
|
289
|
+
|
|
290
|
+
Async version of `after_model`. The todo list is designed to be updated at
|
|
291
|
+
most once per model turn. Since the `write_todos` tool replaces the entire
|
|
292
|
+
todo list with each call, making multiple parallel calls would create ambiguity
|
|
293
|
+
about which update should take precedence. This method prevents such conflicts
|
|
294
|
+
by rejecting any response that contains multiple write_todos tool calls.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
state: The current agent state containing messages.
|
|
298
|
+
runtime: The LangGraph runtime instance.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
A dict containing error ToolMessages for each write_todos call if multiple
|
|
302
|
+
parallel calls are detected, otherwise None to allow normal execution.
|
|
303
|
+
"""
|
|
304
|
+
return self.after_model(state, runtime)
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Annotated, Any, Generic, Literal
|
|
|
7
7
|
from langchain_core.messages import AIMessage, ToolCall, ToolMessage
|
|
8
8
|
from langgraph.channels.untracked_value import UntrackedValue
|
|
9
9
|
from langgraph.typing import ContextT
|
|
10
|
-
from typing_extensions import NotRequired
|
|
10
|
+
from typing_extensions import NotRequired, override
|
|
11
11
|
|
|
12
12
|
from langchain.agents.middleware.types import (
|
|
13
13
|
AgentMiddleware,
|
|
@@ -23,22 +23,23 @@ if TYPE_CHECKING:
|
|
|
23
23
|
ExitBehavior = Literal["continue", "error", "end"]
|
|
24
24
|
"""How to handle execution when tool call limits are exceeded.
|
|
25
25
|
|
|
26
|
-
- `
|
|
27
|
-
|
|
28
|
-
- `
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
- `'continue'`: Block exceeded tools with error messages, let other tools continue
|
|
27
|
+
(default)
|
|
28
|
+
- `'error'`: Raise a `ToolCallLimitExceededError` exception
|
|
29
|
+
- `'end'`: Stop execution immediately, injecting a `ToolMessage` and an `AIMessage` for
|
|
30
|
+
the single tool call that exceeded the limit. Raises `NotImplementedError` if there
|
|
31
|
+
are other pending tool calls (due to parallel tool calling).
|
|
31
32
|
"""
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class ToolCallLimitState(AgentState[ResponseT], Generic[ResponseT]):
|
|
35
|
-
"""State schema for ToolCallLimitMiddleware
|
|
36
|
+
"""State schema for `ToolCallLimitMiddleware`.
|
|
36
37
|
|
|
37
|
-
Extends AgentState with tool call tracking fields.
|
|
38
|
+
Extends `AgentState` with tool call tracking fields.
|
|
38
39
|
|
|
39
|
-
The count fields are dictionaries mapping tool names to execution counts.
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
The count fields are dictionaries mapping tool names to execution counts. This
|
|
41
|
+
allows multiple middleware instances to track different tools independently. The
|
|
42
|
+
special key `'__all__'` is used for tracking all tool calls globally.
|
|
42
43
|
"""
|
|
43
44
|
|
|
44
45
|
thread_tool_call_count: NotRequired[Annotated[dict[str, int], PrivateStateAttr]]
|
|
@@ -46,13 +47,13 @@ class ToolCallLimitState(AgentState[ResponseT], Generic[ResponseT]):
|
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
def _build_tool_message_content(tool_name: str | None) -> str:
|
|
49
|
-
"""Build the error message content for ToolMessage when limit is exceeded.
|
|
50
|
+
"""Build the error message content for `ToolMessage` when limit is exceeded.
|
|
50
51
|
|
|
51
52
|
This message is sent to the model, so it should not reference thread/run concepts
|
|
52
53
|
that the model has no notion of.
|
|
53
54
|
|
|
54
55
|
Args:
|
|
55
|
-
tool_name: Tool name being limited (if specific tool), or None for all tools.
|
|
56
|
+
tool_name: Tool name being limited (if specific tool), or `None` for all tools.
|
|
56
57
|
|
|
57
58
|
Returns:
|
|
58
59
|
A concise message instructing the model not to call the tool again.
|
|
@@ -70,7 +71,7 @@ def _build_final_ai_message_content(
|
|
|
70
71
|
run_limit: int | None,
|
|
71
72
|
tool_name: str | None,
|
|
72
73
|
) -> str:
|
|
73
|
-
"""Build the final AI message content for 'end' behavior.
|
|
74
|
+
"""Build the final AI message content for `'end'` behavior.
|
|
74
75
|
|
|
75
76
|
This message is displayed to the user, so it should include detailed information
|
|
76
77
|
about which limits were exceeded.
|
|
@@ -80,7 +81,7 @@ def _build_final_ai_message_content(
|
|
|
80
81
|
run_count: Current run tool call count.
|
|
81
82
|
thread_limit: Thread tool call limit (if set).
|
|
82
83
|
run_limit: Run tool call limit (if set).
|
|
83
|
-
tool_name: Tool name being limited (if specific tool), or None for all tools.
|
|
84
|
+
tool_name: Tool name being limited (if specific tool), or `None` for all tools.
|
|
84
85
|
|
|
85
86
|
Returns:
|
|
86
87
|
A formatted message describing which limits were exceeded.
|
|
@@ -100,8 +101,8 @@ def _build_final_ai_message_content(
|
|
|
100
101
|
class ToolCallLimitExceededError(Exception):
|
|
101
102
|
"""Exception raised when tool call limits are exceeded.
|
|
102
103
|
|
|
103
|
-
This exception is raised when the configured exit behavior is 'error'
|
|
104
|
-
|
|
104
|
+
This exception is raised when the configured exit behavior is `'error'` and either
|
|
105
|
+
the thread or run tool call limit has been exceeded.
|
|
105
106
|
"""
|
|
106
107
|
|
|
107
108
|
def __init__(
|
|
@@ -145,48 +146,53 @@ class ToolCallLimitMiddleware(
|
|
|
145
146
|
|
|
146
147
|
Configuration:
|
|
147
148
|
- `exit_behavior`: How to handle when limits are exceeded
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
- `'continue'`: Block exceeded tools, let execution continue (default)
|
|
150
|
+
- `'error'`: Raise an exception
|
|
151
|
+
- `'end'`: Stop immediately with a `ToolMessage` + AI message for the single
|
|
152
|
+
tool call that exceeded the limit (raises `NotImplementedError` if there
|
|
153
|
+
are other pending tool calls (due to parallel tool calling).
|
|
153
154
|
|
|
154
155
|
Examples:
|
|
155
|
-
Continue execution with blocked tools (default)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
!!! example "Continue execution with blocked tools (default)"
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from langchain.agents.middleware.tool_call_limit import ToolCallLimitMiddleware
|
|
160
|
+
from langchain.agents import create_agent
|
|
161
|
+
|
|
162
|
+
# Block exceeded tools but let other tools and model continue
|
|
163
|
+
limiter = ToolCallLimitMiddleware(
|
|
164
|
+
thread_limit=20,
|
|
165
|
+
run_limit=10,
|
|
166
|
+
exit_behavior="continue", # default
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
agent = create_agent("openai:gpt-4o", middleware=[limiter])
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
!!! example "Stop immediately when limit exceeded"
|
|
166
173
|
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
```python
|
|
175
|
+
# End execution immediately with an AI message
|
|
176
|
+
limiter = ToolCallLimitMiddleware(run_limit=5, exit_behavior="end")
|
|
169
177
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# End execution immediately with an AI message
|
|
173
|
-
limiter = ToolCallLimitMiddleware(run_limit=5, exit_behavior="end")
|
|
178
|
+
agent = create_agent("openai:gpt-4o", middleware=[limiter])
|
|
179
|
+
```
|
|
174
180
|
|
|
175
|
-
|
|
176
|
-
```
|
|
181
|
+
!!! example "Raise exception on limit"
|
|
177
182
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
```python
|
|
184
|
+
# Strict limit with exception handling
|
|
185
|
+
limiter = ToolCallLimitMiddleware(
|
|
186
|
+
tool_name="search", thread_limit=5, exit_behavior="error"
|
|
187
|
+
)
|
|
182
188
|
|
|
183
|
-
|
|
189
|
+
agent = create_agent("openai:gpt-4o", middleware=[limiter])
|
|
184
190
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
try:
|
|
192
|
+
result = await agent.invoke({"messages": [HumanMessage("Task")]})
|
|
193
|
+
except ToolCallLimitExceededError as e:
|
|
194
|
+
print(f"Search limit exceeded: {e}")
|
|
195
|
+
```
|
|
190
196
|
|
|
191
197
|
"""
|
|
192
198
|
|
|
@@ -204,23 +210,24 @@ class ToolCallLimitMiddleware(
|
|
|
204
210
|
|
|
205
211
|
Args:
|
|
206
212
|
tool_name: Name of the specific tool to limit. If `None`, limits apply
|
|
207
|
-
to all tools.
|
|
213
|
+
to all tools.
|
|
208
214
|
thread_limit: Maximum number of tool calls allowed per thread.
|
|
209
|
-
`None` means no limit.
|
|
215
|
+
`None` means no limit.
|
|
210
216
|
run_limit: Maximum number of tool calls allowed per run.
|
|
211
|
-
`None` means no limit.
|
|
217
|
+
`None` means no limit.
|
|
212
218
|
exit_behavior: How to handle when limits are exceeded.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
- `
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
|
|
220
|
+
- `'continue'`: Block exceeded tools with error messages, let other
|
|
221
|
+
tools continue. Model decides when to end.
|
|
222
|
+
- `'error'`: Raise a `ToolCallLimitExceededError` exception
|
|
223
|
+
- `'end'`: Stop execution immediately with a `ToolMessage` + AI message
|
|
224
|
+
for the single tool call that exceeded the limit. Raises
|
|
225
|
+
`NotImplementedError` if there are multiple parallel tool
|
|
226
|
+
calls to other tools or multiple pending tool calls.
|
|
220
227
|
|
|
221
228
|
Raises:
|
|
222
|
-
ValueError: If both limits are `None`, if exit_behavior is invalid,
|
|
223
|
-
or if run_limit exceeds thread_limit
|
|
229
|
+
ValueError: If both limits are `None`, if `exit_behavior` is invalid,
|
|
230
|
+
or if `run_limit` exceeds `thread_limit`.
|
|
224
231
|
"""
|
|
225
232
|
super().__init__()
|
|
226
233
|
|
|
@@ -293,7 +300,8 @@ class ToolCallLimitMiddleware(
|
|
|
293
300
|
run_count: Current run call count.
|
|
294
301
|
|
|
295
302
|
Returns:
|
|
296
|
-
Tuple of (allowed_calls, blocked_calls, final_thread_count,
|
|
303
|
+
Tuple of `(allowed_calls, blocked_calls, final_thread_count,
|
|
304
|
+
final_run_count)`.
|
|
297
305
|
"""
|
|
298
306
|
allowed_calls: list[ToolCall] = []
|
|
299
307
|
blocked_calls: list[ToolCall] = []
|
|
@@ -314,10 +322,11 @@ class ToolCallLimitMiddleware(
|
|
|
314
322
|
return allowed_calls, blocked_calls, temp_thread_count, temp_run_count
|
|
315
323
|
|
|
316
324
|
@hook_config(can_jump_to=["end"])
|
|
325
|
+
@override
|
|
317
326
|
def after_model(
|
|
318
327
|
self,
|
|
319
328
|
state: ToolCallLimitState[ResponseT],
|
|
320
|
-
runtime: Runtime[ContextT],
|
|
329
|
+
runtime: Runtime[ContextT],
|
|
321
330
|
) -> dict[str, Any] | None:
|
|
322
331
|
"""Increment tool call counts after a model call and check limits.
|
|
323
332
|
|
|
@@ -327,13 +336,13 @@ class ToolCallLimitMiddleware(
|
|
|
327
336
|
|
|
328
337
|
Returns:
|
|
329
338
|
State updates with incremented tool call counts. If limits are exceeded
|
|
330
|
-
|
|
331
|
-
|
|
339
|
+
and exit_behavior is `'end'`, also includes a jump to end with a
|
|
340
|
+
`ToolMessage` and AI message for the single exceeded tool call.
|
|
332
341
|
|
|
333
342
|
Raises:
|
|
334
|
-
ToolCallLimitExceededError: If limits are exceeded and exit_behavior
|
|
335
|
-
is
|
|
336
|
-
NotImplementedError: If limits are exceeded, exit_behavior is
|
|
343
|
+
ToolCallLimitExceededError: If limits are exceeded and `exit_behavior`
|
|
344
|
+
is `'error'`.
|
|
345
|
+
NotImplementedError: If limits are exceeded, `exit_behavior` is `'end'`,
|
|
337
346
|
and there are multiple tool calls.
|
|
338
347
|
"""
|
|
339
348
|
# Get the last AIMessage to check for tool calls
|
|
@@ -352,7 +361,7 @@ class ToolCallLimitMiddleware(
|
|
|
352
361
|
return None
|
|
353
362
|
|
|
354
363
|
# Get the count key for this middleware instance
|
|
355
|
-
count_key = self.tool_name
|
|
364
|
+
count_key = self.tool_name or "__all__"
|
|
356
365
|
|
|
357
366
|
# Get current counts
|
|
358
367
|
thread_counts = state.get("thread_tool_call_count", {}).copy()
|
|
@@ -452,3 +461,28 @@ class ToolCallLimitMiddleware(
|
|
|
452
461
|
"run_tool_call_count": run_counts,
|
|
453
462
|
"messages": artificial_messages,
|
|
454
463
|
}
|
|
464
|
+
|
|
465
|
+
@hook_config(can_jump_to=["end"])
|
|
466
|
+
async def aafter_model(
|
|
467
|
+
self,
|
|
468
|
+
state: ToolCallLimitState[ResponseT],
|
|
469
|
+
runtime: Runtime[ContextT],
|
|
470
|
+
) -> dict[str, Any] | None:
|
|
471
|
+
"""Async increment tool call counts after a model call and check limits.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
state: The current agent state.
|
|
475
|
+
runtime: The langgraph runtime.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
State updates with incremented tool call counts. If limits are exceeded
|
|
479
|
+
and exit_behavior is `'end'`, also includes a jump to end with a
|
|
480
|
+
`ToolMessage` and AI message for the single exceeded tool call.
|
|
481
|
+
|
|
482
|
+
Raises:
|
|
483
|
+
ToolCallLimitExceededError: If limits are exceeded and `exit_behavior`
|
|
484
|
+
is `'error'`.
|
|
485
|
+
NotImplementedError: If limits are exceeded, `exit_behavior` is `'end'`,
|
|
486
|
+
and there are multiple tool calls.
|
|
487
|
+
"""
|
|
488
|
+
return self.after_model(state, runtime)
|
|
@@ -23,39 +23,44 @@ class LLMToolEmulator(AgentMiddleware):
|
|
|
23
23
|
"""Emulates specified tools using an LLM instead of executing them.
|
|
24
24
|
|
|
25
25
|
This middleware allows selective emulation of tools for testing purposes.
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
|
|
27
|
+
By default (when `tools=None`), all tools are emulated. You can specify which
|
|
28
|
+
tools to emulate by passing a list of tool names or `BaseTool` instances.
|
|
28
29
|
|
|
29
30
|
Examples:
|
|
30
|
-
Emulate all tools (default behavior)
|
|
31
|
-
```python
|
|
32
|
-
from langchain.agents.middleware import LLMToolEmulator
|
|
31
|
+
!!! example "Emulate all tools (default behavior)"
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
```python
|
|
34
|
+
from langchain.agents.middleware import LLMToolEmulator
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
model="openai:gpt-4o",
|
|
38
|
-
tools=[get_weather, get_user_location, calculator],
|
|
39
|
-
middleware=[middleware],
|
|
40
|
-
)
|
|
41
|
-
```
|
|
36
|
+
middleware = LLMToolEmulator()
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
agent = create_agent(
|
|
39
|
+
model="openai:gpt-4o",
|
|
40
|
+
tools=[get_weather, get_user_location, calculator],
|
|
41
|
+
middleware=[middleware],
|
|
42
|
+
)
|
|
43
|
+
```
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
tools=["get_weather"
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
!!! example "Emulate specific tools by name"
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
middleware = LLMToolEmulator(tools=["get_weather", "get_user_location"])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
!!! example "Use a custom model for emulation"
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
middleware = LLMToolEmulator(
|
|
55
|
+
tools=["get_weather"], model="anthropic:claude-sonnet-4-5-20250929"
|
|
56
|
+
)
|
|
57
|
+
```
|
|
54
58
|
|
|
55
|
-
Emulate specific tools by passing tool instances
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
!!! example "Emulate specific tools by passing tool instances"
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
middleware = LLMToolEmulator(tools=[get_weather, get_user_location])
|
|
63
|
+
```
|
|
59
64
|
"""
|
|
60
65
|
|
|
61
66
|
def __init__(
|
|
@@ -67,12 +72,16 @@ class LLMToolEmulator(AgentMiddleware):
|
|
|
67
72
|
"""Initialize the tool emulator.
|
|
68
73
|
|
|
69
74
|
Args:
|
|
70
|
-
tools: List of tool names (str) or BaseTool instances to emulate.
|
|
71
|
-
|
|
75
|
+
tools: List of tool names (`str`) or `BaseTool` instances to emulate.
|
|
76
|
+
|
|
77
|
+
If `None`, ALL tools will be emulated.
|
|
78
|
+
|
|
72
79
|
If empty list, no tools will be emulated.
|
|
73
80
|
model: Model to use for emulation.
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
|
|
82
|
+
Defaults to `'anthropic:claude-sonnet-4-5-20250929'`.
|
|
83
|
+
|
|
84
|
+
Can be a model identifier string or `BaseChatModel` instance.
|
|
76
85
|
"""
|
|
77
86
|
super().__init__()
|
|
78
87
|
|
|
@@ -110,7 +119,7 @@ class LLMToolEmulator(AgentMiddleware):
|
|
|
110
119
|
|
|
111
120
|
Returns:
|
|
112
121
|
ToolMessage with emulated response if tool should be emulated,
|
|
113
|
-
|
|
122
|
+
otherwise calls handler for normal execution.
|
|
114
123
|
"""
|
|
115
124
|
tool_name = request.tool_call["name"]
|
|
116
125
|
|
|
@@ -152,7 +161,7 @@ class LLMToolEmulator(AgentMiddleware):
|
|
|
152
161
|
request: ToolCallRequest,
|
|
153
162
|
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
154
163
|
) -> ToolMessage | Command:
|
|
155
|
-
"""Async version of wrap_tool_call
|
|
164
|
+
"""Async version of `wrap_tool_call`.
|
|
156
165
|
|
|
157
166
|
Emulate tool execution using LLM if tool should be emulated.
|
|
158
167
|
|
|
@@ -162,7 +171,7 @@ class LLMToolEmulator(AgentMiddleware):
|
|
|
162
171
|
|
|
163
172
|
Returns:
|
|
164
173
|
ToolMessage with emulated response if tool should be emulated,
|
|
165
|
-
|
|
174
|
+
otherwise calls handler for normal execution.
|
|
166
175
|
"""
|
|
167
176
|
tool_name = request.tool_call["name"]
|
|
168
177
|
|