langchain-dev-utils 1.3.7__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 (44) hide show
  1. langchain_dev_utils/__init__.py +1 -0
  2. langchain_dev_utils/_utils.py +131 -0
  3. langchain_dev_utils/agents/__init__.py +4 -0
  4. langchain_dev_utils/agents/factory.py +99 -0
  5. langchain_dev_utils/agents/file_system.py +252 -0
  6. langchain_dev_utils/agents/middleware/__init__.py +21 -0
  7. langchain_dev_utils/agents/middleware/format_prompt.py +66 -0
  8. langchain_dev_utils/agents/middleware/handoffs.py +214 -0
  9. langchain_dev_utils/agents/middleware/model_fallback.py +49 -0
  10. langchain_dev_utils/agents/middleware/model_router.py +200 -0
  11. langchain_dev_utils/agents/middleware/plan.py +367 -0
  12. langchain_dev_utils/agents/middleware/summarization.py +85 -0
  13. langchain_dev_utils/agents/middleware/tool_call_repair.py +96 -0
  14. langchain_dev_utils/agents/middleware/tool_emulator.py +60 -0
  15. langchain_dev_utils/agents/middleware/tool_selection.py +82 -0
  16. langchain_dev_utils/agents/plan.py +188 -0
  17. langchain_dev_utils/agents/wrap.py +324 -0
  18. langchain_dev_utils/chat_models/__init__.py +11 -0
  19. langchain_dev_utils/chat_models/adapters/__init__.py +3 -0
  20. langchain_dev_utils/chat_models/adapters/create_utils.py +53 -0
  21. langchain_dev_utils/chat_models/adapters/openai_compatible.py +715 -0
  22. langchain_dev_utils/chat_models/adapters/register_profiles.py +15 -0
  23. langchain_dev_utils/chat_models/base.py +282 -0
  24. langchain_dev_utils/chat_models/types.py +27 -0
  25. langchain_dev_utils/embeddings/__init__.py +11 -0
  26. langchain_dev_utils/embeddings/adapters/__init__.py +3 -0
  27. langchain_dev_utils/embeddings/adapters/create_utils.py +45 -0
  28. langchain_dev_utils/embeddings/adapters/openai_compatible.py +91 -0
  29. langchain_dev_utils/embeddings/base.py +234 -0
  30. langchain_dev_utils/message_convert/__init__.py +15 -0
  31. langchain_dev_utils/message_convert/content.py +201 -0
  32. langchain_dev_utils/message_convert/format.py +69 -0
  33. langchain_dev_utils/pipeline/__init__.py +7 -0
  34. langchain_dev_utils/pipeline/parallel.py +135 -0
  35. langchain_dev_utils/pipeline/sequential.py +101 -0
  36. langchain_dev_utils/pipeline/types.py +3 -0
  37. langchain_dev_utils/py.typed +0 -0
  38. langchain_dev_utils/tool_calling/__init__.py +14 -0
  39. langchain_dev_utils/tool_calling/human_in_the_loop.py +284 -0
  40. langchain_dev_utils/tool_calling/utils.py +81 -0
  41. langchain_dev_utils-1.3.7.dist-info/METADATA +103 -0
  42. langchain_dev_utils-1.3.7.dist-info/RECORD +44 -0
  43. langchain_dev_utils-1.3.7.dist-info/WHEEL +4 -0
  44. langchain_dev_utils-1.3.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,367 @@
1
+ import json
2
+ from typing import Awaitable, Callable, Literal, NotRequired, Optional, cast
3
+
4
+ from langchain.agents.middleware import ModelRequest, ModelResponse
5
+ from langchain.agents.middleware.types import (
6
+ AgentMiddleware,
7
+ AgentState,
8
+ ModelCallResult,
9
+ )
10
+ from langchain.tools import BaseTool, ToolRuntime, tool
11
+ from langchain_core.messages import SystemMessage, ToolMessage
12
+ from langgraph.types import Command
13
+ from typing_extensions import TypedDict
14
+
15
+ _DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION = """Use this tool to create and manage a structured task list for complex or multi-step work. It helps you stay organized, track progress, and demonstrate to the user that you’re handling tasks systematically.
16
+
17
+ ## When to Use This Tool
18
+ Use this tool in the following scenarios:
19
+
20
+ 1. **Complex multi-step tasks** — when a task requires three or more distinct steps or actions.
21
+ 2. **Non-trivial and complex tasks** — tasks that require careful planning or involve multiple operations.
22
+ 3. **User explicitly requests a to-do list** — when the user directly asks you to use the to-do list feature.
23
+ 4. **User provides multiple tasks** — when the user supplies a list of items to be done (e.g., numbered or comma-separated).
24
+ 5. **The plan needs adjustment based on current execution** — when ongoing progress indicates the plan should be revised.
25
+
26
+ ## How to Use This Tool
27
+ 1. **When starting a task** — before actually beginning work, invoke this tool with a task list (a list of strings). The first task will automatically be set to `in_progress`, and all others to `pending`.
28
+ 2. **When updating the task list** — for example, after completing some tasks, if you find certain tasks are no longer needed, remove them; if new necessary tasks emerge, add them. However, **do not modify** tasks already marked as completed. In such cases, simply call this tool again with the updated task list.
29
+
30
+ ## When NOT to Use This Tool
31
+ Avoid using this tool in the following situations:
32
+ 1. The task is a **single, straightforward action**.
33
+ 2. The task is **too trivial**, and tracking it provides no benefit.
34
+ 3. The task can be completed in **fewer than three simple steps**.
35
+ 4. The current task list has been fully completed — in this case, use `finish_sub_plan()` to finalize.
36
+
37
+ ## How It Works
38
+ - **Input**: A parameter named `plan` containing a list of strings representing the tasks (e.g., `["Task 1", "Task 2", "Task 3"]`).
39
+ - **Automatic status assignment**:
40
+ → First task: `in_progress`
41
+ → Remaining tasks: `pending`
42
+ - When updating the plan, provide only the **next set of tasks to execute**. For example, if the next phase requires `["Task 4", "Task 5"]`, call this tool with `plan=["Task 4", "Task 5"]`.
43
+
44
+ ## Task States
45
+ - `pending`: Ready to start, awaiting execution
46
+ - `in_progress`: Currently being worked on
47
+ - `done`: Completed
48
+
49
+ ## Best Practices
50
+ - Break large tasks into clear, actionable steps.
51
+ - Use specific and descriptive task names.
52
+ - Update the plan immediately if priorities shift or blockers arise.
53
+ - Never leave the plan empty — as long as unfinished tasks remain, at least one must be marked `in_progress`.
54
+ - Do not batch completions — mark each task as done immediately after finishing it.
55
+ - Remove irrelevant tasks entirely instead of leaving them in `pending` state.
56
+
57
+ **Remember**: If a task is simple, just do it. This tool is meant to provide structure — not overhead.
58
+ """
59
+
60
+ _DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION = """This tool is used to mark the currently in-progress task in an existing task list as completed.
61
+
62
+ ## Functionality
63
+ - Marks the current task with status `in_progress` as `done`, and automatically sets the next task (previously `pending`) to `in_progress`.
64
+
65
+ ## When to Use
66
+ Use only when you have confirmed that the current task is truly finished.
67
+
68
+ ## Example
69
+ Before calling:
70
+ ```json
71
+ [
72
+ {"content": "Task 1", "status": "done"},
73
+ {"content": "Task 2", "status": "in_progress"},
74
+ {"content": "Task 3", "status": "pending"}
75
+ ]
76
+ ```
77
+
78
+ After calling `finish_sub_plan()`:
79
+ ```json
80
+ [
81
+ {"content": "Task 1", "status": "done"},
82
+ {"content": "Task 2", "status": "done"},
83
+ {"content": "Task 3", "status": "in_progress"}
84
+ ]
85
+ ```
86
+
87
+ **Note**:
88
+ - This tool is **only** for marking completion — do **not** use it to create or modify plans (use `write_plan` instead).
89
+ - Ensure the task is genuinely complete before invoking this function.
90
+ - No parameters are required — status updates are handled automatically.
91
+ """
92
+
93
+ _DEFAULT_READ_PLAN_TOOL_DESCRIPTION = """
94
+ Get all sub-plans with their current status.
95
+ """
96
+
97
+
98
+ class Plan(TypedDict):
99
+ content: str
100
+ status: Literal["pending", "in_progress", "done"]
101
+
102
+
103
+ class PlanState(AgentState):
104
+ plan: NotRequired[list[Plan]]
105
+
106
+
107
+ class PlanToolDescription(TypedDict):
108
+ write_plan: NotRequired[str]
109
+ finish_sub_plan: NotRequired[str]
110
+ read_plan: NotRequired[str]
111
+
112
+
113
+ def _create_write_plan_tool(
114
+ description: Optional[str] = None,
115
+ ) -> BaseTool:
116
+ """Create a tool for writing initial plan.
117
+
118
+ This function creates a tool that allows agents to write an initial plan
119
+ with a list of plans. The first plan in the plan will be marked as "in_progress"
120
+ and the rest as "pending".
121
+
122
+ Args:
123
+ description: The description of the tool. Uses default description if not provided.
124
+
125
+ Returns:
126
+ BaseTool: The tool for writing initial plan.
127
+ """
128
+
129
+ @tool(
130
+ description=description or _DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION,
131
+ )
132
+ def write_plan(plan: list[str], runtime: ToolRuntime):
133
+ return Command(
134
+ update={
135
+ "plan": [
136
+ {
137
+ "content": content,
138
+ "status": "pending" if index > 0 else "in_progress",
139
+ }
140
+ for index, content in enumerate(plan)
141
+ ],
142
+ "messages": [
143
+ ToolMessage(
144
+ content=f"Plan successfully written, please first execute the {plan[0]} sub-plan (no need to change the status to in_process)",
145
+ tool_call_id=runtime.tool_call_id,
146
+ )
147
+ ],
148
+ }
149
+ )
150
+
151
+ return write_plan
152
+
153
+
154
+ def _create_finish_sub_plan_tool(
155
+ description: Optional[str] = None,
156
+ ) -> BaseTool:
157
+ """Create a tool for finishing sub-plan tasks.
158
+
159
+ This function creates a tool that allows agents to update the status of sub-plans in a plan.
160
+ Sub-plans can be marked as "done" to track progress.
161
+
162
+ Args:
163
+ description: The description of the tool. Uses default description if not provided.
164
+
165
+ Returns:
166
+ BaseTool: The tool for finishing sub-plan tasks.
167
+ """
168
+
169
+ @tool(
170
+ description=description or _DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION,
171
+ )
172
+ def finish_sub_plan(
173
+ runtime: ToolRuntime,
174
+ ):
175
+ plan_list = runtime.state.get("plan", [])
176
+
177
+ sub_finish_plan = ""
178
+ sub_next_plan = ",all sub plan are done"
179
+ for plan in plan_list:
180
+ if plan["status"] == "in_progress":
181
+ plan["status"] = "done"
182
+ sub_finish_plan = f"finish sub plan:**{plan['content']}**"
183
+
184
+ for plan in plan_list:
185
+ if plan["status"] == "pending":
186
+ plan["status"] = "in_progress"
187
+ sub_next_plan = f",next plan:**{plan['content']}**"
188
+ break
189
+
190
+ return Command(
191
+ update={
192
+ "plan": plan_list,
193
+ "messages": [
194
+ ToolMessage(
195
+ content=sub_finish_plan + sub_next_plan,
196
+ tool_call_id=runtime.tool_call_id,
197
+ )
198
+ ],
199
+ }
200
+ )
201
+
202
+ return finish_sub_plan
203
+
204
+
205
+ def _create_read_plan_tool(
206
+ description: Optional[str] = None,
207
+ ):
208
+ """Create a tool for reading all sub-plans.
209
+
210
+ This function creates a tool that allows agents to read all sub-plans
211
+ in the current plan with their status information.
212
+
213
+ Args:
214
+ description: The description of the tool. Uses default description if not provided.
215
+
216
+ Returns:
217
+ BaseTool: The tool for reading all sub-plans.
218
+ """
219
+
220
+ @tool(
221
+ description=description or _DEFAULT_READ_PLAN_TOOL_DESCRIPTION,
222
+ )
223
+ def read_plan(runtime: ToolRuntime):
224
+ plan_list = runtime.state.get("plan", [])
225
+ return json.dumps(plan_list)
226
+
227
+ return read_plan
228
+
229
+
230
+ _PLAN_SYSTEM_PROMPT_NOT_READ_PLAN = """You can manage task plans using two simple tools:
231
+
232
+ ## write_plan
233
+ - Use it to break complex tasks (3+ steps) into a clear, actionable list. Only include next steps to execute — the first becomes `"in_progress"`, the rest `"pending"`. Don’t use it for simple tasks (<3 steps).
234
+
235
+ ## finish_sub_plan
236
+ - Call it **only when the current task is 100% done**. It automatically marks it `"done"` and promotes the next `"pending"` task to `"in_progress"`. No parameters needed. Never use it mid-task or if anything’s incomplete.
237
+ Keep plans lean, update immediately, and never batch completions.
238
+
239
+ **Note**: Make sure that all tasks end up with the status `"done"`.
240
+ """
241
+
242
+ _PLAN_SYSTEM_PROMPT = """You can manage task plans using three simple tools:
243
+
244
+ ## write_plan
245
+ - Use it to break complex tasks (3+ steps) into a clear, actionable list. Only include next steps to execute — the first becomes `"in_progress"`, the rest `"pending"`. Don’t use it for simple tasks (<3 steps).
246
+
247
+ ## finish_sub_plan
248
+ - Call it **only when the current task is 100% done**. It automatically marks it `"done"` and promotes the next `"pending"` task to `"in_progress"`. No parameters needed. Never use it mid-task or if anything’s incomplete.
249
+
250
+ ## read_plan
251
+ - Retrieve the full current plan list with statuses, especially when you forget which sub-plan you're supposed to execute next.
252
+ - No parameters required—returns a current plan list with statuses.
253
+
254
+ **Note**: Make sure that all tasks end up with the status `"done"`.
255
+ """
256
+
257
+
258
+ class PlanMiddleware(AgentMiddleware):
259
+ """Middleware that provides plan management capabilities to agents.
260
+
261
+ This middleware adds a `write_plan` and `finish_sub_plan` (and `read_plan`
262
+ optional) tool that allows agents to create and manage structured plan lists
263
+ for complex multi-step operations. It's designed to help agents track progress,
264
+ organize complex tasks, and provide users with visibility into task completion
265
+ status.
266
+
267
+ The middleware automatically injects system prompts that guide the agent on
268
+ how to use the plan functionality effectively.
269
+
270
+ Args:
271
+ system_prompt: Custom system prompt to guide the agent on using the plan
272
+ tool. If not provided, uses the default `_PLAN_SYSTEM_PROMPT` or
273
+ `_PLAN_SYSTEM_PROMPT_NOT_READ_PLAN` based on the `use_read_plan_tool`
274
+ parameter.
275
+ custom_plan_tool_descriptions: Custom descriptions for the plan tools.
276
+ If not provided, uses the default descriptions.
277
+ use_read_plan_tool: Whether to use the `read_plan` tool.
278
+ If not provided, uses the default `True`.
279
+
280
+ Example:
281
+ ```python
282
+ from langchain_dev_utils.agents.middleware import PlanMiddleware
283
+ from langchain_dev_utils.agents import create_agent
284
+
285
+ agent = create_agent("vllm:qwen3-4b", middleware=[PlanMiddleware()])
286
+
287
+ # Agent now has access to write_plan tool and plan state tracking
288
+ result = await agent.invoke({"messages": [HumanMessage("Help me refactor my codebase")]})
289
+
290
+ print(result["plan"]) # Array of plan items with status tracking
291
+ ```
292
+ """
293
+
294
+ state_schema = PlanState
295
+
296
+ def __init__(
297
+ self,
298
+ *,
299
+ system_prompt: Optional[str] = None,
300
+ custom_plan_tool_descriptions: Optional[PlanToolDescription] = None,
301
+ use_read_plan_tool: bool = True,
302
+ ) -> None:
303
+ super().__init__()
304
+
305
+ if not custom_plan_tool_descriptions:
306
+ custom_plan_tool_descriptions = {}
307
+
308
+ write_plan_tool_description = custom_plan_tool_descriptions.get(
309
+ "write_plan",
310
+ _DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION,
311
+ )
312
+ finish_sub_plan_tool_description = custom_plan_tool_descriptions.get(
313
+ "finish_sub_plan",
314
+ _DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION,
315
+ )
316
+ read_plan_tool_description = custom_plan_tool_descriptions.get(
317
+ "read_plan",
318
+ _DEFAULT_READ_PLAN_TOOL_DESCRIPTION,
319
+ )
320
+
321
+ tools = [
322
+ _create_write_plan_tool(description=write_plan_tool_description),
323
+ _create_finish_sub_plan_tool(description=finish_sub_plan_tool_description),
324
+ ]
325
+
326
+ if use_read_plan_tool:
327
+ tools.append(_create_read_plan_tool(description=read_plan_tool_description))
328
+
329
+ if system_prompt is None:
330
+ if use_read_plan_tool:
331
+ system_prompt = _PLAN_SYSTEM_PROMPT
332
+ else:
333
+ system_prompt = _PLAN_SYSTEM_PROMPT_NOT_READ_PLAN
334
+
335
+ self.system_prompt = system_prompt
336
+ self.tools = tools
337
+
338
+ def _get_override_request(self, request: ModelRequest) -> ModelRequest:
339
+ """Add the plan system prompt to the system message."""
340
+ if request.system_message is not None:
341
+ new_system_content = [
342
+ *request.system_message.content_blocks,
343
+ {"type": "text", "text": f"\n\n{self.system_prompt}"},
344
+ ]
345
+ else:
346
+ new_system_content = [{"type": "text", "text": self.system_prompt}]
347
+ new_system_message = SystemMessage(
348
+ content=cast("list[str | dict[str, str]]", new_system_content)
349
+ )
350
+ return request.override(system_message=new_system_message)
351
+
352
+ def wrap_model_call(
353
+ self,
354
+ request: ModelRequest,
355
+ handler: Callable[[ModelRequest], ModelResponse],
356
+ ) -> ModelCallResult:
357
+ override_request = self._get_override_request(request)
358
+ return handler(override_request)
359
+
360
+ async def awrap_model_call(
361
+ self,
362
+ request: ModelRequest,
363
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
364
+ ) -> ModelCallResult:
365
+ """Update the system message to include the plan system prompt."""
366
+ override_request = self._get_override_request(request)
367
+ return await handler(override_request)
@@ -0,0 +1,85 @@
1
+ from typing import Any
2
+
3
+ from langchain.agents.middleware.summarization import (
4
+ _DEFAULT_MESSAGES_TO_KEEP,
5
+ _DEFAULT_TRIM_TOKEN_LIMIT,
6
+ DEFAULT_SUMMARY_PROMPT,
7
+ ContextSize,
8
+ TokenCounter,
9
+ )
10
+ from langchain.agents.middleware.summarization import (
11
+ SummarizationMiddleware as _SummarizationMiddleware,
12
+ )
13
+ from langchain_core.messages.utils import count_tokens_approximately
14
+
15
+ from langchain_dev_utils.chat_models.base import load_chat_model
16
+
17
+
18
+ class SummarizationMiddleware(_SummarizationMiddleware):
19
+ """Initialize summarization middleware.
20
+
21
+ Args:
22
+ model: The language model to use for generating summaries.
23
+ trigger: One or more thresholds that trigger summarization.
24
+
25
+ Provide a single `ContextSize` tuple or a list of tuples, in which case
26
+ summarization runs when any threshold is breached.
27
+
28
+ Examples: `("messages", 50)`, `("tokens", 3000)`, `[("fraction", 0.8),
29
+ ("messages", 100)]`.
30
+ keep: Context retention policy applied after summarization.
31
+
32
+ Provide a `ContextSize` tuple to specify how much history to preserve.
33
+
34
+ Defaults to keeping the most recent 20 messages.
35
+
36
+ Examples: `("messages", 20)`, `("tokens", 3000)`, or
37
+ `("fraction", 0.3)`.
38
+ token_counter: Function to count tokens in messages.
39
+ summary_prompt: Prompt template for generating summaries.
40
+ trim_tokens_to_summarize: Maximum tokens to keep when preparing messages for
41
+ the summarization call.
42
+
43
+ Pass `None` to skip trimming entirely.
44
+
45
+ Examples:
46
+ ```python
47
+ from langchain_dev_utils.agents.middleware import SummarizationMiddleware
48
+
49
+ middleware = SummarizationMiddleware(
50
+ model="vllm:qwen3-4b",
51
+ trigger=("tokens", 100),
52
+ keep=("messages", 2),
53
+ )
54
+ ```
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ model: str,
60
+ *,
61
+ trigger: ContextSize | list[ContextSize] | None = None,
62
+ keep: ContextSize = ("messages", _DEFAULT_MESSAGES_TO_KEEP),
63
+ token_counter: TokenCounter = count_tokens_approximately,
64
+ summary_prompt: str = DEFAULT_SUMMARY_PROMPT,
65
+ trim_tokens_to_summarize: int | None = _DEFAULT_TRIM_TOKEN_LIMIT,
66
+ **deprecated_kwargs: Any,
67
+ ) -> None:
68
+ chat_model = load_chat_model(model)
69
+
70
+ middleware_kwargs = {}
71
+ if trigger is not None:
72
+ middleware_kwargs["trigger"] = trigger
73
+ if keep is not None:
74
+ middleware_kwargs["keep"] = keep
75
+ if token_counter is not None:
76
+ middleware_kwargs["token_counter"] = token_counter
77
+ if summary_prompt is not None:
78
+ middleware_kwargs["summary_prompt"] = summary_prompt
79
+ if trim_tokens_to_summarize is not None:
80
+ middleware_kwargs["trim_tokens_to_summarize"] = trim_tokens_to_summarize
81
+
82
+ super().__init__(
83
+ model=chat_model,
84
+ **middleware_kwargs,
85
+ )
@@ -0,0 +1,96 @@
1
+ from typing import Any, Awaitable, Callable, cast
2
+
3
+ from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
4
+ from langchain.agents.middleware.types import ModelCallResult
5
+ from langchain_core.messages import AIMessage, BaseMessage
6
+
7
+ from langchain_dev_utils._utils import _check_pkg_install
8
+
9
+
10
+ class ToolCallRepairMiddleware(AgentMiddleware):
11
+ """Middleware to repair invalid tool calls in AIMessages.
12
+
13
+ This middleware attempts to repair JSON-formatted tool arguments in
14
+ AIMessages that have invalid tool calls. It uses the `json_repair`
15
+ package to fix common JSON errors.
16
+
17
+ Example:
18
+ ```python
19
+ from langchain_dev_utils.agents.middleware import ToolCallRepairMiddleware
20
+
21
+ middleware = ToolCallRepairMiddleware()
22
+ ```
23
+ """
24
+
25
+ def _repair_msgs(self, messages: list[BaseMessage]) -> list[BaseMessage]:
26
+ _check_pkg_install("json_repair")
27
+ from json import JSONDecodeError
28
+
29
+ from json_repair import loads
30
+
31
+ results = []
32
+ for msg in messages:
33
+ if (
34
+ isinstance(msg, AIMessage)
35
+ and hasattr(msg, "invalid_tool_calls")
36
+ and len(msg.invalid_tool_calls) > 0
37
+ ):
38
+ new_invalid_toolcalls = []
39
+ new_tool_calls = [*msg.tool_calls]
40
+
41
+ for invalid_tool_call in msg.invalid_tool_calls:
42
+ args = invalid_tool_call.get("args")
43
+ if args:
44
+ try:
45
+ args = cast(dict[str, Any], loads(args))
46
+ new_tool_calls.append(
47
+ {
48
+ "name": invalid_tool_call.get(
49
+ "name",
50
+ )
51
+ or "",
52
+ "id": invalid_tool_call.get("id", ""),
53
+ "type": "tool_call",
54
+ "args": args,
55
+ }
56
+ )
57
+ except JSONDecodeError:
58
+ new_invalid_toolcalls.append(invalid_tool_call)
59
+ else:
60
+ new_invalid_toolcalls.append(invalid_tool_call)
61
+
62
+ new_msg = msg.model_copy(
63
+ update={
64
+ "tool_calls": new_tool_calls,
65
+ "invalid_tool_calls": new_invalid_toolcalls,
66
+ }
67
+ )
68
+ results.append(new_msg)
69
+ else:
70
+ results.append(msg)
71
+
72
+ return results
73
+
74
+ def wrap_model_call(
75
+ self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
76
+ ) -> ModelCallResult:
77
+ response = handler(request)
78
+ results = self._repair_msgs(response.result)
79
+
80
+ return ModelResponse(
81
+ result=results,
82
+ structured_response=response.structured_response,
83
+ )
84
+
85
+ async def awrap_model_call(
86
+ self,
87
+ request: ModelRequest,
88
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
89
+ ) -> ModelCallResult:
90
+ response = await handler(request)
91
+ results = self._repair_msgs(response.result)
92
+
93
+ return ModelResponse(
94
+ result=results,
95
+ structured_response=response.structured_response,
96
+ )
@@ -0,0 +1,60 @@
1
+ from langchain.agents.middleware.tool_emulator import (
2
+ LLMToolEmulator as _LLMToolEmulator,
3
+ )
4
+ from langchain_core.tools import BaseTool
5
+
6
+ from langchain_dev_utils.chat_models.base import load_chat_model
7
+
8
+
9
+ class LLMToolEmulator(_LLMToolEmulator):
10
+ """Middleware that emulates specified tools using an LLM instead of executing them.
11
+
12
+ This middleware allows selective emulation of tools for testing purposes.
13
+ By default (when tools=None), all tools are emulated. You can specify which
14
+ tools to emulate by passing a list of tool names or BaseTool instances.
15
+
16
+ Args:
17
+ tools: List of tool names (str) or BaseTool instances to emulate.
18
+ If None (default), ALL tools will be emulated.
19
+ If empty list, no tools will be emulated.
20
+ model: Model to use for emulation. Must be a string identifier.
21
+
22
+ Examples:
23
+ # Emulate all tools (default behavior):
24
+ ```python
25
+ from langchain_dev_utils.agents import create_agent
26
+ from langchain_dev_utils.agents.middleware import LLMToolEmulator
27
+
28
+ middleware = LLMToolEmulator(
29
+ model="vllm:qwen3-4b"
30
+ )
31
+
32
+ agent = create_agent(
33
+ model="vllm:qwen3-4b",
34
+ tools=[get_weather, get_user_location, calculator],
35
+ middleware=[middleware],
36
+ )
37
+ ```
38
+
39
+ # Emulate specific tools by name:
40
+ ```python
41
+ middleware = LLMToolEmulator(model="vllm:qwen3-4b", tools=["get_weather", "get_user_location"])
42
+ ```
43
+
44
+ # Emulate specific tools by passing tool instances:
45
+ ```python
46
+ middleware = LLMToolEmulator(model="vllm:qwen3-4b", tools=[get_weather, get_user_location])
47
+ ```
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ *,
53
+ model: str,
54
+ tools: list[str | BaseTool] | None = None,
55
+ ) -> None:
56
+ chat_model = load_chat_model(model)
57
+ super().__init__(
58
+ model=chat_model,
59
+ tools=tools,
60
+ )
@@ -0,0 +1,82 @@
1
+ from typing import Optional
2
+
3
+ from langchain.agents.middleware.tool_selection import (
4
+ LLMToolSelectorMiddleware as _LLMToolSelectorMiddleware,
5
+ )
6
+
7
+ from langchain_dev_utils.chat_models.base import load_chat_model
8
+
9
+
10
+ class LLMToolSelectorMiddleware(_LLMToolSelectorMiddleware):
11
+ """Intelligent tool selection middleware using LLM to filter relevant tools.
12
+
13
+ This middleware leverages a language model to analyze user queries and select
14
+ only the most relevant tools from a potentially large toolset. This optimization
15
+ reduces token consumption and improves model performance by focusing on appropriate tools.
16
+
17
+ The selection process analyzes the user's request and matches it against available
18
+ tool descriptions to determine relevance.
19
+
20
+ Args:
21
+ model: String identifier for the model to use for tool selection.
22
+ Must be a valid model identifier that can be loaded by load_chat_model().
23
+ system_prompt: Custom instructions for the selection model. If not provided,
24
+ uses the default selection prompt from the parent class.
25
+ max_tools: Maximum number of tools to select and pass to the main model.
26
+ If the LLM selects more tools than this limit, only the first max_tools
27
+ tools will be used. If None, no limit is applied.
28
+ always_include: List of tool names that must always be included in the
29
+ selection regardless of the LLM's decision. These tools do not count
30
+ against the max_tools limit.
31
+
32
+ Examples:
33
+ # Basic usage with tool limit:
34
+ ```python
35
+ from langchain_dev_utils.agents.middleware import LLMToolSelectorMiddleware
36
+
37
+ middleware = LLMToolSelectorMiddleware(
38
+ model="vllm:qwen3-4b",
39
+ max_tools=3
40
+ )
41
+ ```
42
+
43
+ # With always-included tools:
44
+ ```python
45
+ middleware = LLMToolSelectorMiddleware(
46
+ model="vllm:qwen3-4b",
47
+ max_tools=5,
48
+ always_include=["search", "calculator"]
49
+ )
50
+ ```
51
+
52
+ # With custom system prompt:
53
+ ```python
54
+ custom_prompt = "Select tools that can help answer user questions about data."
55
+ middleware = LLMToolSelectorMiddleware(
56
+ model="vllm:qwen3-4b",
57
+ system_prompt=custom_prompt
58
+ )
59
+ ```
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ *,
65
+ model: str,
66
+ system_prompt: Optional[str] = None,
67
+ max_tools: Optional[int] = None,
68
+ always_include: Optional[list[str]] = None,
69
+ ) -> None:
70
+ chat_model = load_chat_model(model)
71
+
72
+ tool_selector_kwargs = {}
73
+ if system_prompt is not None:
74
+ tool_selector_kwargs["system_prompt"] = system_prompt
75
+ if max_tools is not None:
76
+ tool_selector_kwargs["max_tools"] = max_tools
77
+ if always_include is not None:
78
+ tool_selector_kwargs["always_include"] = always_include
79
+ super().__init__(
80
+ model=chat_model,
81
+ **tool_selector_kwargs,
82
+ )