langchain-dev-utils 1.1.11__tar.gz → 1.1.13__tar.gz

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 (55) hide show
  1. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/PKG-INFO +3 -3
  2. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/README.md +2 -2
  3. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/README_cn.md +2 -2
  4. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/pyproject.toml +2 -1
  5. langchain_dev_utils-1.1.13/src/langchain_dev_utils/__init__.py +1 -0
  6. langchain_dev_utils-1.1.13/src/langchain_dev_utils/agents/middleware/plan.py +372 -0
  7. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/wrap.py +1 -1
  8. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/chat_models/adapters/openai_compatible.py +6 -3
  9. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/chat_models/base.py +49 -6
  10. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/chat_models/types.py +1 -0
  11. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/embeddings/base.py +52 -8
  12. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_agent.py +2 -2
  13. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_load_model.py +2 -2
  14. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_model_tool_emulator.py +4 -2
  15. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_plan_middleware.py +4 -1
  16. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_router_model.py +2 -2
  17. langchain_dev_utils-1.1.13/uv.lock +1980 -0
  18. langchain_dev_utils-1.1.11/src/langchain_dev_utils/__init__.py +0 -1
  19. langchain_dev_utils-1.1.11/src/langchain_dev_utils/agents/middleware/plan.py +0 -458
  20. langchain_dev_utils-1.1.11/uv.lock +0 -1955
  21. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/.gitignore +0 -0
  22. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/.python-version +0 -0
  23. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/.vscode/settings.json +0 -0
  24. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/LICENSE +0 -0
  25. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/__init__.py +0 -0
  26. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/factory.py +0 -0
  27. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/file_system.py +0 -0
  28. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/middleware/__init__.py +0 -0
  29. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/middleware/model_fallback.py +0 -0
  30. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/middleware/model_router.py +0 -0
  31. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/middleware/summarization.py +0 -0
  32. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/middleware/tool_emulator.py +0 -0
  33. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/middleware/tool_selection.py +0 -0
  34. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/agents/plan.py +0 -0
  35. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/chat_models/__init__.py +0 -0
  36. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/chat_models/adapters/__init__.py +0 -0
  37. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/embeddings/__init__.py +0 -0
  38. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/message_convert/__init__.py +0 -0
  39. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/message_convert/content.py +0 -0
  40. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/message_convert/format.py +0 -0
  41. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/pipeline/__init__.py +0 -0
  42. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/pipeline/parallel.py +0 -0
  43. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/pipeline/sequential.py +0 -0
  44. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/pipeline/types.py +0 -0
  45. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/py.typed +0 -0
  46. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/tool_calling/__init__.py +0 -0
  47. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/tool_calling/human_in_the_loop.py +0 -0
  48. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/src/langchain_dev_utils/tool_calling/utils.py +0 -0
  49. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_chat_models.py +0 -0
  50. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_human_in_the_loop.py +0 -0
  51. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_load_embbeding.py +0 -0
  52. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_messages.py +0 -0
  53. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_pipline.py +0 -0
  54. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_tool_calling.py +0 -0
  55. {langchain_dev_utils-1.1.11 → langchain_dev_utils-1.1.13}/tests/test_wrap_agent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-dev-utils
3
- Version: 1.1.11
3
+ Version: 1.1.13
4
4
  Summary: A practical utility library for LangChain and LangGraph development
5
5
  Project-URL: Source Code, https://github.com/TBice123123/langchain-dev-utils
6
6
  Project-URL: repository, https://github.com/TBice123123/langchain-dev-utils
@@ -61,7 +61,7 @@ Mainly consists of the following two functions:
61
61
 
62
62
  - `provider_name`: Model provider name, used as an identifier for subsequent model loading
63
63
  - `chat_model`: Chat model, can be a ChatModel or a string (currently supports "openai-compatible")
64
- - `base_url`: API address of the model provider (optional, valid when `chat_model` is a string and is "openai-compatible")
64
+ - `base_url`: The API address of the model provider (optional, valid for both types of `chat_model`, but mainly used when `chat_model` is a string and is "openai-compatible")
65
65
  - `provider_config`: Relevant configuration for the model provider (optional, valid when `chat_model` is a string and is "openai-compatible"), can configure some provider-related parameters, such as whether to support structured output in json_mode, list of supported tool_choices, etc.
66
66
 
67
67
  `load_chat_model` parameter description:
@@ -101,7 +101,7 @@ Mainly consists of the following two functions:
101
101
 
102
102
  - `provider_name`: Embedding model provider name, used as an identifier for subsequent model loading
103
103
  - `embeddings_model`: Embedding model, can be Embeddings or a string (currently supports "openai-compatible")
104
- - `base_url`: API address of the model provider (optional, valid when `embeddings_model` is a string and is "openai-compatible")
104
+ - `base_url`: The API address of the Embedding model provider (optional, valid for both types of `embeddings_model`, but mainly used when `embeddings_model` is a string and is "openai-compatible")
105
105
 
106
106
  `load_embeddings` parameter description:
107
107
 
@@ -45,7 +45,7 @@ Mainly consists of the following two functions:
45
45
 
46
46
  - `provider_name`: Model provider name, used as an identifier for subsequent model loading
47
47
  - `chat_model`: Chat model, can be a ChatModel or a string (currently supports "openai-compatible")
48
- - `base_url`: API address of the model provider (optional, valid when `chat_model` is a string and is "openai-compatible")
48
+ - `base_url`: The API address of the model provider (optional, valid for both types of `chat_model`, but mainly used when `chat_model` is a string and is "openai-compatible")
49
49
  - `provider_config`: Relevant configuration for the model provider (optional, valid when `chat_model` is a string and is "openai-compatible"), can configure some provider-related parameters, such as whether to support structured output in json_mode, list of supported tool_choices, etc.
50
50
 
51
51
  `load_chat_model` parameter description:
@@ -85,7 +85,7 @@ Mainly consists of the following two functions:
85
85
 
86
86
  - `provider_name`: Embedding model provider name, used as an identifier for subsequent model loading
87
87
  - `embeddings_model`: Embedding model, can be Embeddings or a string (currently supports "openai-compatible")
88
- - `base_url`: API address of the model provider (optional, valid when `embeddings_model` is a string and is "openai-compatible")
88
+ - `base_url`: The API address of the Embedding model provider (optional, valid for both types of `embeddings_model`, but mainly used when `embeddings_model` is a string and is "openai-compatible")
89
89
 
90
90
  `load_embeddings` parameter description:
91
91
 
@@ -45,7 +45,7 @@ pip install -U langchain-dev-utils[standard]
45
45
 
46
46
  - `provider_name`:模型提供商名称,作为后续模型加载的标识
47
47
  - `chat_model`:对话模型,可以是 ChatModel 或字符串(目前支持 "openai-compatible")
48
- - `base_url`:模型提供商的 API 地址(可选,当 `chat_model` 为字符串且是"openai-compatible"时有效)
48
+ - `base_url`:模型提供商的 API 地址(可选,对于`chat_model`的两种类型情况都有效,但是主要用于`chat_model`为字符串且是"openai-compatible"的情况)
49
49
  - `provider_config`:模型提供商的相关配置(可选,当 `chat_model` 为字符串且是 "openai-compatible" 时有效),可以配置一些提供商的相关参数,例如是否支持 json_mode 的结构化输出方式、支持的 tool_choice 列表等
50
50
 
51
51
  `load_chat_model` 参数说明:
@@ -85,7 +85,7 @@ print(model.invoke("Hello"))
85
85
 
86
86
  - `provider_name`:嵌入模型提供商名称,作为后续模型加载的标识
87
87
  - `embeddings_model`:嵌入模型,可以是 Embeddings 或字符串(目前支持 "openai-compatible")
88
- - `base_url`:模型提供商的 API 地址(可选,当 `embeddings_model` 为字符串且是"openai-compatible"时有效)
88
+ - `base_url`:嵌入模型提供商的 API 地址(可选,对于`embeddings_model`的两种类型情况都有效,但是主要用于`embeddings_model`为字符串且是"openai-compatible"的情况)
89
89
 
90
90
  `load_embeddings` 参数说明:
91
91
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "langchain-dev-utils"
3
- version = "1.1.11"
3
+ version = "1.1.13"
4
4
  description = "A practical utility library for LangChain and LangGraph development"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "tiebingice", email = "tiebingice123@outlook.com" }]
@@ -32,4 +32,5 @@ tests = [
32
32
  "python-dotenv>=1.1.1",
33
33
  "langchain-tests>=1.0.0",
34
34
  "langchain-deepseek>=1.0.0",
35
+ "langchain-qwq>=0.3.0",
35
36
  ]
@@ -0,0 +1 @@
1
+ __version__ = "1.1.13"
@@ -0,0 +1,372 @@
1
+ import json
2
+ from typing import Awaitable, Callable, Literal, Optional
3
+ from typing import NotRequired
4
+
5
+ from langchain.agents.middleware import ModelRequest, ModelResponse
6
+ from langchain.agents.middleware.types import (
7
+ AgentMiddleware,
8
+ AgentState,
9
+ ModelCallResult,
10
+ )
11
+ from langchain.tools import BaseTool, ToolRuntime, tool
12
+ from langchain_core.messages import ToolMessage
13
+ from langgraph.types import Command
14
+ from typing_extensions import TypedDict
15
+
16
+ _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.
17
+
18
+ ## When to Use This Tool
19
+ Use this tool in the following scenarios:
20
+
21
+ 1. **Complex multi-step tasks** — when a task requires three or more distinct steps or actions.
22
+ 2. **Non-trivial and complex tasks** — tasks that require careful planning or involve multiple operations.
23
+ 3. **User explicitly requests a to-do list** — when the user directly asks you to use the to-do list feature.
24
+ 4. **User provides multiple tasks** — when the user supplies a list of items to be done (e.g., numbered or comma-separated).
25
+ 5. **The plan needs adjustment based on current execution** — when ongoing progress indicates the plan should be revised.
26
+
27
+ ## How to Use This Tool
28
+ 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`.
29
+ 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.
30
+
31
+ ## When NOT to Use This Tool
32
+ Avoid using this tool in the following situations:
33
+ 1. The task is a **single, straightforward action**.
34
+ 2. The task is **too trivial**, and tracking it provides no benefit.
35
+ 3. The task can be completed in **fewer than three simple steps**.
36
+ 4. The current task list has been fully completed — in this case, use `finish_sub_plan()` to finalize.
37
+
38
+ ## How It Works
39
+ - **Input**: A parameter named `plan` containing a list of strings representing the tasks (e.g., `["Task 1", "Task 2", "Task 3"]`).
40
+ - **Automatic status assignment**:
41
+ → First task: `in_progress`
42
+ → Remaining tasks: `pending`
43
+ - 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"]`.
44
+
45
+ ## Task States
46
+ - `pending`: Ready to start, awaiting execution
47
+ - `in_progress`: Currently being worked on
48
+ - `done`: Completed
49
+
50
+ ## Best Practices
51
+ - Break large tasks into clear, actionable steps.
52
+ - Use specific and descriptive task names.
53
+ - Update the plan immediately if priorities shift or blockers arise.
54
+ - Never leave the plan empty — as long as unfinished tasks remain, at least one must be marked `in_progress`.
55
+ - Do not batch completions — mark each task as done immediately after finishing it.
56
+ - Remove irrelevant tasks entirely instead of leaving them in `pending` state.
57
+
58
+ **Remember**: If a task is simple, just do it. This tool is meant to provide structure — not overhead.
59
+ """
60
+
61
+ _DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION = """This tool is used to mark the currently in-progress task in an existing task list as completed.
62
+
63
+ ## Functionality
64
+ - Marks the current task with status `in_progress` as `done`, and automatically sets the next task (previously `pending`) to `in_progress`.
65
+
66
+ ## When to Use
67
+ Use only when you have confirmed that the current task is truly finished.
68
+
69
+ ## Example
70
+ Before calling:
71
+ ```json
72
+ [
73
+ {"content": "Task 1", "status": "done"},
74
+ {"content": "Task 2", "status": "in_progress"},
75
+ {"content": "Task 3", "status": "pending"}
76
+ ]
77
+ ```
78
+
79
+ After calling `finish_sub_plan()`:
80
+ ```json
81
+ [
82
+ {"content": "Task 1", "status": "done"},
83
+ {"content": "Task 2", "status": "done"},
84
+ {"content": "Task 3", "status": "in_progress"}
85
+ ]
86
+ ```
87
+
88
+ **Note**:
89
+ - This tool is **only** for marking completion — do **not** use it to create or modify plans (use `write_plan` instead).
90
+ - Ensure the task is genuinely complete before invoking this function.
91
+ - No parameters are required — status updates are handled automatically.
92
+ """
93
+
94
+ _DEFAULT_READ_PLAN_TOOL_DESCRIPTION = """
95
+ Get all sub-plans with their current status.
96
+ """
97
+
98
+
99
+ class Plan(TypedDict):
100
+ content: str
101
+ status: Literal["pending", "in_progress", "done"]
102
+
103
+
104
+ class PlanState(AgentState):
105
+ plan: NotRequired[list[Plan]]
106
+
107
+
108
+ def create_write_plan_tool(
109
+ description: Optional[str] = None,
110
+ message_key: Optional[str] = None,
111
+ ) -> BaseTool:
112
+ """Create a tool for writing initial plan.
113
+
114
+ This function creates a tool that allows agents to write an initial plan
115
+ with a list of plans. The first plan in the plan will be marked as "in_progress"
116
+ and the rest as "pending".
117
+
118
+ Args:
119
+ description: The description of the tool. Uses default description if not provided.
120
+ message_key: The key of the message to be updated. Defaults to "messages".
121
+
122
+ Returns:
123
+ BaseTool: The tool for writing initial plan.
124
+
125
+ Example:
126
+ Basic usage:
127
+ >>> from langchain_dev_utils.agents.middleware import create_write_plan_tool
128
+ >>> write_plan_tool = create_write_plan_tool()
129
+ """
130
+
131
+ @tool(
132
+ description=description or _DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION,
133
+ )
134
+ def write_plan(plan: list[str], runtime: ToolRuntime):
135
+ msg_key = message_key or "messages"
136
+ return Command(
137
+ update={
138
+ "plan": [
139
+ {
140
+ "content": content,
141
+ "status": "pending" if index > 0 else "in_progress",
142
+ }
143
+ for index, content in enumerate(plan)
144
+ ],
145
+ msg_key: [
146
+ ToolMessage(
147
+ content=f"Plan successfully written, please first execute the {plan[0]} sub-plan (no need to change the status to in_process)",
148
+ tool_call_id=runtime.tool_call_id,
149
+ )
150
+ ],
151
+ }
152
+ )
153
+
154
+ return write_plan
155
+
156
+
157
+ def create_finish_sub_plan_tool(
158
+ description: Optional[str] = None,
159
+ message_key: Optional[str] = None,
160
+ ) -> BaseTool:
161
+ """Create a tool for finishing sub-plan tasks.
162
+
163
+ This function creates a tool that allows agents to update the status of sub-plans in a plan. Sub-plans can be marked as "done" to track progress.
164
+
165
+ Args:
166
+ description: The description of the tool. Uses default description if not provided.
167
+ message_key: The key of the message to be updated. Defaults to "messages".
168
+
169
+ Returns:
170
+ BaseTool: The tool for finishing sub-plan tasks.
171
+
172
+ Example:
173
+ Basic usage:
174
+ >>> from langchain_dev_utils.agents.middleware import create_finish_sub_plan_tool
175
+ >>> finish_sub_plan_tool = create_finish_sub_plan_tool()
176
+ """
177
+
178
+ @tool(
179
+ description=description or _DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION,
180
+ )
181
+ def finish_sub_plan(
182
+ runtime: ToolRuntime,
183
+ ):
184
+ msg_key = message_key or "messages"
185
+ plan_list = runtime.state.get("plan", [])
186
+
187
+ sub_finish_plan = ""
188
+ sub_next_plan = ",all sub plan are done"
189
+ for plan in plan_list:
190
+ if plan["status"] == "in_progress":
191
+ plan["status"] = "done"
192
+ sub_finish_plan = f"finish sub plan:**{plan['content']}**"
193
+
194
+ for plan in plan_list:
195
+ if plan["status"] == "pending":
196
+ plan["status"] = "in_progress"
197
+ sub_next_plan = f",next plan:**{plan['content']}**"
198
+ break
199
+
200
+ return Command(
201
+ update={
202
+ "plan": plan_list,
203
+ msg_key: [
204
+ ToolMessage(
205
+ content=sub_finish_plan + sub_next_plan,
206
+ tool_call_id=runtime.tool_call_id,
207
+ )
208
+ ],
209
+ }
210
+ )
211
+
212
+ return finish_sub_plan
213
+
214
+
215
+ def create_read_plan_tool(
216
+ description: Optional[str] = None,
217
+ ):
218
+ """Create a tool for reading all sub-plans.
219
+
220
+ This function creates a tool that allows agents to read all sub-plans
221
+ in the current plan with their status information.
222
+
223
+ Args:
224
+ description: The description of the tool. Uses default description if not provided.
225
+
226
+ Returns:
227
+ BaseTool: The tool for reading all sub-plans.
228
+
229
+ Example:
230
+ Basic usage:
231
+ >>> from langchain_dev_utils.agents.middleware import create_read_plan_tool
232
+ >>> read_plan_tool = create_read_plan_tool()
233
+ """
234
+
235
+ @tool(
236
+ description=description or _DEFAULT_READ_PLAN_TOOL_DESCRIPTION,
237
+ )
238
+ def read_plan(runtime: ToolRuntime):
239
+ plan_list = runtime.state.get("plan", [])
240
+ return json.dumps(plan_list)
241
+
242
+ return read_plan
243
+
244
+
245
+ _PLAN_SYSTEM_PROMPT_NOT_READ_PLAN = """You can manage task plans using two simple tools:
246
+
247
+ ## write_plan
248
+ - 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).
249
+
250
+ ## finish_sub_plan
251
+ - 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.
252
+ Keep plans lean, update immediately, and never batch completions.
253
+ """
254
+
255
+ _PLAN_SYSTEM_PROMPT = """You can manage task plans using three simple tools:
256
+
257
+ ## write_plan
258
+ - 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).
259
+
260
+ ## finish_sub_plan
261
+ - 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.
262
+
263
+ ## read_plan
264
+ - Retrieve the full current plan list with statuses, especially when you forget which sub-plan you're supposed to execute next.
265
+ - No parameters required—returns a current plan list with statuses.
266
+ """
267
+
268
+
269
+ class PlanMiddleware(AgentMiddleware):
270
+ """Middleware that provides plan management capabilities to agents.
271
+
272
+ This middleware adds a `write_plan` and `finish_sub_plan` (and `read_plan` optional) tool that allows agents to create and manage
273
+ structured plan lists for complex multi-step operations. It's designed to help
274
+ agents track progress, organize complex tasks, and provide users with visibility
275
+ into task completion status.
276
+
277
+ The middleware automatically injects system prompts that guide the agent on how to use the plan functionality effectively.
278
+
279
+ Args:
280
+ system_prompt: Custom system prompt to guide the agent on using the plan tool.
281
+ If not provided, uses the default `_PLAN_SYSTEM_PROMPT` or `_PLAN_SYSTEM_PROMPT_NOT_READ_PLAN` based on the `use_read_plan_tool` parameter.
282
+ write_plan_tool_description: Description of the `write_plan` tool.
283
+ If not provided, uses the default `_DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION`.
284
+ finish_sub_plan_tool_description: Description of the `finish_sub_plan` tool.
285
+ If not provided, uses the default `_DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION`.
286
+ read_plan_tool_description: Description of the `read_plan` tool.
287
+ If not provided, uses the default `_DEFAULT_READ_PLAN_TOOL_DESCRIPTION`.
288
+ use_read_plan_tool: Whether to use the `read_plan` tool.
289
+ If not provided, uses the default `True`.
290
+ Example:
291
+ ```python
292
+ from langchain_dev_utils.agents.middleware.plan import PlanMiddleware
293
+ from langchain_dev_utils.agents import create_agent
294
+
295
+ agent = create_agent("vllm:qwen3-4b", middleware=[PlanMiddleware()])
296
+
297
+ # Agent now has access to write_plan tool and plan state tracking
298
+ result = await agent.invoke({"messages": [HumanMessage("Help me refactor my codebase")]})
299
+
300
+ print(result["plan"]) # Array of plan items with status tracking
301
+ ```
302
+ """
303
+
304
+ state_schema = PlanState
305
+
306
+ def __init__(
307
+ self,
308
+ *,
309
+ system_prompt: Optional[str] = None,
310
+ write_plan_tool_description: Optional[str] = None,
311
+ finish_sub_plan_tool_description: Optional[str] = None,
312
+ read_plan_tool_description: Optional[str] = None,
313
+ use_read_plan_tool: bool = True,
314
+ message_key: Optional[str] = None,
315
+ ) -> None:
316
+ super().__init__()
317
+
318
+ write_plan_tool_description = (
319
+ write_plan_tool_description or _DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION
320
+ )
321
+ finish_sub_plan_tool_description = (
322
+ finish_sub_plan_tool_description
323
+ or _DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION
324
+ )
325
+ read_plan_tool_description = (
326
+ read_plan_tool_description or _DEFAULT_READ_PLAN_TOOL_DESCRIPTION
327
+ )
328
+
329
+ tools = [
330
+ create_write_plan_tool(
331
+ description=write_plan_tool_description, message_key=message_key
332
+ ),
333
+ create_finish_sub_plan_tool(
334
+ description=finish_sub_plan_tool_description, message_key=message_key
335
+ ),
336
+ ]
337
+
338
+ if use_read_plan_tool:
339
+ tools.append(create_read_plan_tool(description=read_plan_tool_description))
340
+
341
+ if system_prompt is None:
342
+ if use_read_plan_tool:
343
+ system_prompt = _PLAN_SYSTEM_PROMPT
344
+ else:
345
+ system_prompt = _PLAN_SYSTEM_PROMPT_NOT_READ_PLAN
346
+
347
+ self.system_prompt = system_prompt
348
+ self.tools = tools
349
+
350
+ def wrap_model_call(
351
+ self,
352
+ request: ModelRequest,
353
+ handler: Callable[[ModelRequest], ModelResponse],
354
+ ) -> ModelCallResult:
355
+ request.system_prompt = (
356
+ request.system_prompt + "\n\n" + self.system_prompt
357
+ if request.system_prompt
358
+ else self.system_prompt
359
+ )
360
+ return handler(request)
361
+
362
+ async def awrap_model_call(
363
+ self,
364
+ request: ModelRequest,
365
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
366
+ ) -> ModelCallResult:
367
+ request.system_prompt = (
368
+ request.system_prompt + "\n\n" + self.system_prompt
369
+ if request.system_prompt
370
+ else self.system_prompt
371
+ )
372
+ return await handler(request)
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from typing import Any, Awaitable, Optional, Callable, cast
2
+ from typing import Any, Awaitable, Callable, Optional, cast
3
3
 
4
4
  from langchain.tools import ToolRuntime
5
5
  from langchain_core.messages import AnyMessage, HumanMessage
@@ -46,7 +46,7 @@ _DictOrPydantic = Union[dict, _BM]
46
46
 
47
47
 
48
48
  class _ModelProviderConfigType(BaseModel):
49
- supported_tool_choice: ToolChoiceType = Field(default=[])
49
+ supported_tool_choice: ToolChoiceType = Field(default_factory=list)
50
50
  keep_reasoning_content: bool = Field(default=False)
51
51
  support_json_mode: bool = Field(default=False)
52
52
 
@@ -126,12 +126,15 @@ class _BaseChatOpenAICompatible(BaseChatOpenAI):
126
126
  stop: list[str] | None = None,
127
127
  **kwargs: Any,
128
128
  ) -> dict:
129
+ payload = {**self._default_params, **kwargs}
130
+
131
+ if self._use_responses_api(payload):
132
+ return super()._get_request_payload(input_, stop=stop, **kwargs)
133
+
129
134
  messages = self._convert_input(input_).to_messages()
130
135
  if stop is not None:
131
136
  kwargs["stop"] = stop
132
137
 
133
- payload = {**self._default_params, **kwargs}
134
-
135
138
  payload_messages = []
136
139
 
137
140
  for m in messages:
@@ -1,10 +1,11 @@
1
- import os
2
1
  from typing import Any, NotRequired, Optional, TypedDict, cast
3
2
 
4
3
  from langchain.chat_models.base import _SUPPORTED_PROVIDERS, _init_chat_model_helper
5
4
  from langchain_core.language_models.chat_models import BaseChatModel
5
+ from langchain_core.utils import from_env
6
6
 
7
7
  from .types import ChatModelType, ToolChoiceType
8
+ from pydantic import BaseModel
8
9
 
9
10
  _MODEL_PROVIDERS_DICT = {}
10
11
 
@@ -22,6 +23,34 @@ class ChatModelProvider(TypedDict):
22
23
  provider_config: NotRequired[ProviderConfig]
23
24
 
24
25
 
26
+ def _get_base_url_field_name(model_cls: type[BaseModel]) -> str | None:
27
+ """
28
+ Return 'base_url' if the model has a field named or aliased as 'base_url',
29
+ else return 'api_base' if it has a field named or aliased as 'api_base',
30
+ else return None.
31
+ The return value is always either 'base_url', 'api_base', or None.
32
+ """
33
+ model_fields = model_cls.model_fields
34
+
35
+ # try model_fields first
36
+ if "base_url" in model_fields:
37
+ return "base_url"
38
+
39
+ if "api_base" in model_fields:
40
+ return "api_base"
41
+
42
+ # then try aliases
43
+ for field_info in model_fields.values():
44
+ if field_info.alias == "base_url":
45
+ return "base_url"
46
+
47
+ for field_info in model_fields.values():
48
+ if field_info.alias == "api_base":
49
+ return "api_base"
50
+
51
+ return None
52
+
53
+
25
54
  def _parse_model(model: str, model_provider: Optional[str]) -> tuple[str, str]:
26
55
  """Parse model string and provider.
27
56
 
@@ -71,6 +100,11 @@ def _load_chat_model_helper(
71
100
  "provider_config"
72
101
  ):
73
102
  kwargs.update({"provider_config": provider_config})
103
+
104
+ if base_url := _MODEL_PROVIDERS_DICT[model_provider].get("base_url"):
105
+ url_key = _get_base_url_field_name(chat_model)
106
+ if url_key:
107
+ kwargs.update({url_key: base_url})
74
108
  return chat_model(model=model, **kwargs)
75
109
 
76
110
  return _init_chat_model_helper(model, model_provider=model_provider, **kwargs)
@@ -91,7 +125,7 @@ def register_model_provider(
91
125
  Args:
92
126
  provider_name: Name of the provider to register
93
127
  chat_model: Either a BaseChatModel class or a string identifier for a supported provider
94
- base_url: Optional base URL for API endpoints (Optional parameter;effective only when `chat_model` is a string and is "openai-compatible".)
128
+ base_url: The API address of the model provider (optional, valid for both types of `chat_model`, but mainly used when `chat_model` is a string and is "openai-compatible")
95
129
  provider_config: The configuration of the model provider (Optional parameter;effective only when `chat_model` is a string and is "openai-compatible".)
96
130
  It can be configured to configure some related parameters of the provider, such as whether to support json_mode structured output mode, the list of supported tool_choice
97
131
  Raises:
@@ -113,6 +147,7 @@ def register_model_provider(
113
147
  >>> model = load_chat_model(model="vllm:qwen3-4b")
114
148
  >>> model.invoke("Hello")
115
149
  """
150
+ base_url = base_url or from_env(f"{provider_name.upper()}_API_BASE", default=None)()
116
151
  if isinstance(chat_model, str):
117
152
  try:
118
153
  from .adapters.openai_compatible import _create_openai_compatible_model
@@ -120,8 +155,6 @@ def register_model_provider(
120
155
  raise ImportError(
121
156
  "Please install langchain_dev_utils[standard],when chat_model is a 'openai-compatible'"
122
157
  )
123
-
124
- base_url = base_url or os.getenv(f"{provider_name.upper()}_API_BASE")
125
158
  if base_url is None:
126
159
  raise ValueError(
127
160
  f"base_url must be provided or set {provider_name.upper()}_API_BASE environment variable when chat_model is a string"
@@ -140,11 +173,17 @@ def register_model_provider(
140
173
  provider_name: {
141
174
  "chat_model": chat_model,
142
175
  "provider_config": provider_config,
176
+ "base_url": base_url,
143
177
  }
144
178
  }
145
179
  )
146
180
  else:
147
- _MODEL_PROVIDERS_DICT.update({provider_name: {"chat_model": chat_model}})
181
+ if base_url is not None:
182
+ _MODEL_PROVIDERS_DICT.update(
183
+ {provider_name: {"chat_model": chat_model, "base_url": base_url}}
184
+ )
185
+ else:
186
+ _MODEL_PROVIDERS_DICT.update({provider_name: {"chat_model": chat_model}})
148
187
 
149
188
 
150
189
  def batch_register_model_provider(
@@ -159,7 +198,7 @@ def batch_register_model_provider(
159
198
  providers: List of ChatModelProvider dictionaries, each containing:
160
199
  - provider_name: Name of the provider to register
161
200
  - chat_model: Either a BaseChatModel class or a string identifier for a supported provider
162
- - base_url: Optional base URL for API endpoints(Optional parameter; effective only when `chat_model` is a string and is "openai-compatible".)
201
+ - base_url: The API address of the model provider (optional, valid for both types of `chat_model`, but mainly used when `chat_model` is a string and is "openai-compatible")
163
202
  - provider_config: The configuration of the model provider(Optional parameter; effective only when `chat_model` is a string and is "openai-compatible".)
164
203
  It can be configured to configure some related parameters of the provider, such as whether to support json_mode structured output mode, the list of supported tool_choice
165
204
 
@@ -235,6 +274,10 @@ def load_chat_model(
235
274
  ... )
236
275
  >>> model.invoke("Hello, how are you?")
237
276
  """
277
+ if "provider_config" in kwargs:
278
+ raise ValueError(
279
+ "provider_config is not a valid parameter in load_chat_model ,you can only set it when register model provider"
280
+ )
238
281
  return _load_chat_model_helper(
239
282
  cast(str, model),
240
283
  model_provider=model_provider,
@@ -1,4 +1,5 @@
1
1
  from typing import Literal, Union
2
+
2
3
  from langchain_core.language_models.chat_models import BaseChatModel
3
4
 
4
5