langchain 1.0.4__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.
Files changed (34) hide show
  1. langchain/__init__.py +1 -1
  2. langchain/agents/__init__.py +1 -7
  3. langchain/agents/factory.py +100 -41
  4. langchain/agents/middleware/__init__.py +5 -7
  5. langchain/agents/middleware/_execution.py +21 -20
  6. langchain/agents/middleware/_redaction.py +27 -12
  7. langchain/agents/middleware/_retry.py +123 -0
  8. langchain/agents/middleware/context_editing.py +26 -22
  9. langchain/agents/middleware/file_search.py +18 -13
  10. langchain/agents/middleware/human_in_the_loop.py +60 -54
  11. langchain/agents/middleware/model_call_limit.py +63 -17
  12. langchain/agents/middleware/model_fallback.py +7 -9
  13. langchain/agents/middleware/model_retry.py +300 -0
  14. langchain/agents/middleware/pii.py +80 -27
  15. langchain/agents/middleware/shell_tool.py +230 -103
  16. langchain/agents/middleware/summarization.py +439 -90
  17. langchain/agents/middleware/todo.py +111 -27
  18. langchain/agents/middleware/tool_call_limit.py +105 -71
  19. langchain/agents/middleware/tool_emulator.py +42 -33
  20. langchain/agents/middleware/tool_retry.py +171 -159
  21. langchain/agents/middleware/tool_selection.py +37 -27
  22. langchain/agents/middleware/types.py +754 -392
  23. langchain/agents/structured_output.py +22 -12
  24. langchain/chat_models/__init__.py +1 -7
  25. langchain/chat_models/base.py +234 -185
  26. langchain/embeddings/__init__.py +0 -5
  27. langchain/embeddings/base.py +80 -66
  28. langchain/messages/__init__.py +0 -5
  29. langchain/tools/__init__.py +1 -7
  30. {langchain-1.0.4.dist-info → langchain-1.2.3.dist-info}/METADATA +3 -5
  31. langchain-1.2.3.dist-info/RECORD +36 -0
  32. {langchain-1.0.4.dist-info → langchain-1.2.3.dist-info}/WHEEL +1 -1
  33. langchain-1.0.4.dist-info/RECORD +0 -34
  34. {langchain-1.0.4.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 langchain_core.messages import ToolMessage
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 tool.
173
- tool_description: Custom description for the write_todos tool.
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 prompt to include the todo system prompt."""
202
- request.system_prompt = (
203
- request.system_prompt + "\n\n" + self.system_prompt
204
- if request.system_prompt
205
- else self.system_prompt
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 prompt to include the todo system prompt (async version)."""
215
- request.system_prompt = (
216
- request.system_prompt + "\n\n" + self.system_prompt
217
- if request.system_prompt
218
- else self.system_prompt
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
- - `"continue"`: Block exceeded tools with error messages, let other tools continue (default)
27
- - `"error"`: Raise a `ToolCallLimitExceededError` exception
28
- - `"end"`: Stop execution immediately, injecting a ToolMessage and an AI message
29
- for the single tool call that exceeded the limit. Raises `NotImplementedError`
30
- if there are other pending tool calls (due to parallel tool calling).
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
- This allows multiple middleware instances to track different tools independently.
41
- The special key "__all__" is used for tracking all tool calls globally.
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
- and either the thread or run tool call limit has been exceeded.
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
- - `"continue"`: Block exceeded tools, let execution continue (default)
149
- - `"error"`: Raise an exception
150
- - `"end"`: Stop immediately with a ToolMessage + AI message for the single
151
- tool call that exceeded the limit (raises `NotImplementedError` if there
152
- are other pending tool calls (due to parallel tool calling).
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
- ```python
157
- from langchain.agents.middleware.tool_call_limit import ToolCallLimitMiddleware
158
- from langchain.agents import create_agent
159
-
160
- # Block exceeded tools but let other tools and model continue
161
- limiter = ToolCallLimitMiddleware(
162
- thread_limit=20,
163
- run_limit=10,
164
- exit_behavior="continue", # default
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
- agent = create_agent("openai:gpt-4o", middleware=[limiter])
168
- ```
174
+ ```python
175
+ # End execution immediately with an AI message
176
+ limiter = ToolCallLimitMiddleware(run_limit=5, exit_behavior="end")
169
177
 
170
- Stop immediately when limit exceeded:
171
- ```python
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
- agent = create_agent("openai:gpt-4o", middleware=[limiter])
176
- ```
181
+ !!! example "Raise exception on limit"
177
182
 
178
- Raise exception on limit:
179
- ```python
180
- # Strict limit with exception handling
181
- limiter = ToolCallLimitMiddleware(tool_name="search", thread_limit=5, exit_behavior="error")
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
- agent = create_agent("openai:gpt-4o", middleware=[limiter])
189
+ agent = create_agent("openai:gpt-4o", middleware=[limiter])
184
190
 
185
- try:
186
- result = await agent.invoke({"messages": [HumanMessage("Task")]})
187
- except ToolCallLimitExceededError as e:
188
- print(f"Search limit exceeded: {e}")
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. Defaults to `None`.
213
+ to all tools.
208
214
  thread_limit: Maximum number of tool calls allowed per thread.
209
- `None` means no limit. Defaults to `None`.
215
+ `None` means no limit.
210
216
  run_limit: Maximum number of tool calls allowed per run.
211
- `None` means no limit. Defaults to `None`.
217
+ `None` means no limit.
212
218
  exit_behavior: How to handle when limits are exceeded.
213
- - `"continue"`: Block exceeded tools with error messages, let other
214
- tools continue. Model decides when to end. (default)
215
- - `"error"`: Raise a `ToolCallLimitExceededError` exception
216
- - `"end"`: Stop execution immediately with a ToolMessage + AI message
217
- for the single tool call that exceeded the limit. Raises
218
- `NotImplementedError` if there are multiple parallel tool
219
- calls to other tools or multiple pending tool calls.
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, final_run_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], # noqa: ARG002
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
- and exit_behavior is "end", also includes a jump to end with a ToolMessage
331
- and AI message for the single exceeded tool call.
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 "error".
336
- NotImplementedError: If limits are exceeded, exit_behavior is "end",
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 if self.tool_name else "__all__"
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
- By default (when tools=None), all tools are emulated. You can specify which
27
- tools to emulate by passing a list of tool names or BaseTool instances.
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
- middleware = LLMToolEmulator()
33
+ ```python
34
+ from langchain.agents.middleware import LLMToolEmulator
35
35
 
36
- agent = create_agent(
37
- model="openai:gpt-4o",
38
- tools=[get_weather, get_user_location, calculator],
39
- middleware=[middleware],
40
- )
41
- ```
36
+ middleware = LLMToolEmulator()
42
37
 
43
- Emulate specific tools by name:
44
- ```python
45
- middleware = LLMToolEmulator(tools=["get_weather", "get_user_location"])
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
- Use a custom model for emulation:
49
- ```python
50
- middleware = LLMToolEmulator(
51
- tools=["get_weather"], model="anthropic:claude-sonnet-4-5-20250929"
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
- ```python
57
- middleware = LLMToolEmulator(tools=[get_weather, get_user_location])
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
- If None (default), ALL tools will be emulated.
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
- Defaults to "anthropic:claude-sonnet-4-5-20250929".
75
- Can be a model identifier string or BaseChatModel instance.
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
- otherwise calls handler for normal execution.
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
- otherwise calls handler for normal execution.
174
+ otherwise calls handler for normal execution.
166
175
  """
167
176
  tool_name = request.tool_call["name"]
168
177