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.
- langchain_dev_utils/__init__.py +1 -0
- langchain_dev_utils/_utils.py +131 -0
- langchain_dev_utils/agents/__init__.py +4 -0
- langchain_dev_utils/agents/factory.py +99 -0
- langchain_dev_utils/agents/file_system.py +252 -0
- langchain_dev_utils/agents/middleware/__init__.py +21 -0
- langchain_dev_utils/agents/middleware/format_prompt.py +66 -0
- langchain_dev_utils/agents/middleware/handoffs.py +214 -0
- langchain_dev_utils/agents/middleware/model_fallback.py +49 -0
- langchain_dev_utils/agents/middleware/model_router.py +200 -0
- langchain_dev_utils/agents/middleware/plan.py +367 -0
- langchain_dev_utils/agents/middleware/summarization.py +85 -0
- langchain_dev_utils/agents/middleware/tool_call_repair.py +96 -0
- langchain_dev_utils/agents/middleware/tool_emulator.py +60 -0
- langchain_dev_utils/agents/middleware/tool_selection.py +82 -0
- langchain_dev_utils/agents/plan.py +188 -0
- langchain_dev_utils/agents/wrap.py +324 -0
- langchain_dev_utils/chat_models/__init__.py +11 -0
- langchain_dev_utils/chat_models/adapters/__init__.py +3 -0
- langchain_dev_utils/chat_models/adapters/create_utils.py +53 -0
- langchain_dev_utils/chat_models/adapters/openai_compatible.py +715 -0
- langchain_dev_utils/chat_models/adapters/register_profiles.py +15 -0
- langchain_dev_utils/chat_models/base.py +282 -0
- langchain_dev_utils/chat_models/types.py +27 -0
- langchain_dev_utils/embeddings/__init__.py +11 -0
- langchain_dev_utils/embeddings/adapters/__init__.py +3 -0
- langchain_dev_utils/embeddings/adapters/create_utils.py +45 -0
- langchain_dev_utils/embeddings/adapters/openai_compatible.py +91 -0
- langchain_dev_utils/embeddings/base.py +234 -0
- langchain_dev_utils/message_convert/__init__.py +15 -0
- langchain_dev_utils/message_convert/content.py +201 -0
- langchain_dev_utils/message_convert/format.py +69 -0
- langchain_dev_utils/pipeline/__init__.py +7 -0
- langchain_dev_utils/pipeline/parallel.py +135 -0
- langchain_dev_utils/pipeline/sequential.py +101 -0
- langchain_dev_utils/pipeline/types.py +3 -0
- langchain_dev_utils/py.typed +0 -0
- langchain_dev_utils/tool_calling/__init__.py +14 -0
- langchain_dev_utils/tool_calling/human_in_the_loop.py +284 -0
- langchain_dev_utils/tool_calling/utils.py +81 -0
- langchain_dev_utils-1.3.7.dist-info/METADATA +103 -0
- langchain_dev_utils-1.3.7.dist-info/RECORD +44 -0
- langchain_dev_utils-1.3.7.dist-info/WHEEL +4 -0
- 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
|
+
)
|