langchain-dev-utils 1.3.1__tar.gz → 1.3.3__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 (65) hide show
  1. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/PKG-INFO +1 -1
  2. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/pyproject.toml +1 -1
  3. langchain_dev_utils-1.3.3/src/langchain_dev_utils/__init__.py +1 -0
  4. langchain_dev_utils-1.3.3/src/langchain_dev_utils/agents/__init__.py +4 -0
  5. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/__init__.py +3 -12
  6. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/handoffs.py +89 -23
  7. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/plan.py +29 -45
  8. langchain_dev_utils-1.3.3/src/langchain_dev_utils/agents/wrap.py +274 -0
  9. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/chat_models/adapters/openai_compatible.py +7 -14
  10. langchain_dev_utils-1.3.3/tests/test_handoffs_middleware.py +137 -0
  11. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_wrap_agent.py +83 -2
  12. langchain_dev_utils-1.3.1/src/langchain_dev_utils/__init__.py +0 -1
  13. langchain_dev_utils-1.3.1/src/langchain_dev_utils/agents/__init__.py +0 -4
  14. langchain_dev_utils-1.3.1/src/langchain_dev_utils/agents/wrap.py +0 -140
  15. langchain_dev_utils-1.3.1/tests/test_handoffs_middleware.py +0 -75
  16. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/.gitignore +0 -0
  17. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/.python-version +0 -0
  18. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/.vscode/settings.json +0 -0
  19. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/LICENSE +0 -0
  20. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/README.md +0 -0
  21. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/README_cn.md +0 -0
  22. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/_utils.py +0 -0
  23. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/factory.py +0 -0
  24. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/file_system.py +0 -0
  25. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/format_prompt.py +0 -0
  26. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/model_fallback.py +0 -0
  27. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/model_router.py +0 -0
  28. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/summarization.py +0 -0
  29. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/tool_call_repair.py +0 -0
  30. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/tool_emulator.py +0 -0
  31. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/middleware/tool_selection.py +0 -0
  32. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/agents/plan.py +0 -0
  33. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/chat_models/__init__.py +0 -0
  34. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/chat_models/adapters/__init__.py +0 -0
  35. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/chat_models/base.py +0 -0
  36. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/chat_models/types.py +0 -0
  37. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/embeddings/__init__.py +0 -0
  38. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/embeddings/base.py +0 -0
  39. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/message_convert/__init__.py +0 -0
  40. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/message_convert/content.py +0 -0
  41. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/message_convert/format.py +0 -0
  42. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/pipeline/__init__.py +0 -0
  43. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/pipeline/parallel.py +0 -0
  44. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/pipeline/sequential.py +0 -0
  45. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/pipeline/types.py +0 -0
  46. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/py.typed +0 -0
  47. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/tool_calling/__init__.py +0 -0
  48. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/tool_calling/human_in_the_loop.py +0 -0
  49. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/src/langchain_dev_utils/tool_calling/utils.py +0 -0
  50. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/__init__.py +0 -0
  51. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_agent.py +0 -0
  52. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_chat_models.py +0 -0
  53. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_human_in_the_loop.py +0 -0
  54. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_load_embbeding.py +0 -0
  55. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_load_model.py +0 -0
  56. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_messages.py +0 -0
  57. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_model_tool_emulator.py +0 -0
  58. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_pipline.py +0 -0
  59. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_plan_middleware.py +0 -0
  60. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_router_model.py +0 -0
  61. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_tool_call_repair.py +0 -0
  62. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/test_tool_calling.py +0 -0
  63. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/utils/__init__.py +0 -0
  64. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/tests/utils/register.py +0 -0
  65. {langchain_dev_utils-1.3.1 → langchain_dev_utils-1.3.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-dev-utils
3
- Version: 1.3.1
3
+ Version: 1.3.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "langchain-dev-utils"
3
- version = "1.3.1"
3
+ version = "1.3.3"
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" }]
@@ -0,0 +1 @@
1
+ __version__ = "1.3.3"
@@ -0,0 +1,4 @@
1
+ from .factory import create_agent
2
+ from .wrap import wrap_agent_as_tool, wrap_all_agents_as_tool
3
+
4
+ __all__ = ["create_agent", "wrap_agent_as_tool", "wrap_all_agents_as_tool"]
@@ -1,13 +1,8 @@
1
1
  from .format_prompt import format_prompt
2
- from .handoffs import HandoffsAgentMiddleware, create_handoffs_tool
2
+ from .handoffs import HandoffAgentMiddleware
3
3
  from .model_fallback import ModelFallbackMiddleware
4
4
  from .model_router import ModelRouterMiddleware
5
- from .plan import (
6
- PlanMiddleware,
7
- create_finish_sub_plan_tool,
8
- create_read_plan_tool,
9
- create_write_plan_tool,
10
- )
5
+ from .plan import PlanMiddleware
11
6
  from .summarization import SummarizationMiddleware
12
7
  from .tool_call_repair import ToolCallRepairMiddleware
13
8
  from .tool_emulator import LLMToolEmulator
@@ -17,14 +12,10 @@ __all__ = [
17
12
  "SummarizationMiddleware",
18
13
  "LLMToolSelectorMiddleware",
19
14
  "PlanMiddleware",
20
- "create_finish_sub_plan_tool",
21
- "create_read_plan_tool",
22
- "create_write_plan_tool",
23
15
  "ModelFallbackMiddleware",
24
16
  "LLMToolEmulator",
25
17
  "ModelRouterMiddleware",
26
18
  "ToolCallRepairMiddleware",
27
19
  "format_prompt",
28
- "create_handoffs_tool",
29
- "HandoffsAgentMiddleware",
20
+ "HandoffAgentMiddleware",
30
21
  ]
@@ -1,4 +1,4 @@
1
- from typing import Any, Awaitable, Callable
1
+ from typing import Any, Awaitable, Callable, Literal
2
2
 
3
3
  from langchain.agents import AgentState
4
4
  from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
@@ -19,35 +19,24 @@ class MultiAgentState(AgentState):
19
19
  class AgentConfig(TypedDict):
20
20
  model: NotRequired[str | BaseChatModel]
21
21
  prompt: str | SystemMessage
22
- tools: list[BaseTool | dict[str, Any]]
22
+ tools: NotRequired[list[BaseTool | dict[str, Any]]]
23
23
  default: NotRequired[bool]
24
+ handoffs: list[str] | Literal["all"]
24
25
 
25
26
 
26
- def create_handoffs_tool(
27
- agent_name: str,
28
- tool_name: Optional[str] = None,
29
- tool_description: Optional[str] = None,
30
- ):
27
+ def _create_handoffs_tool(agent_name: str, tool_description: Optional[str] = None):
31
28
  """Create a tool for handoffs to a specified agent.
32
29
 
33
30
  Args:
34
31
  agent_name (str): The name of the agent to transfer to.
35
- tool_name (Optional[str], optional): The name of the tool. Defaults to None.
36
- tool_description (Optional[str], optional): The description of the tool. Defaults to None.
37
32
 
38
33
  Returns:
39
34
  BaseTool: A tool instance for handoffs to the specified agent.
40
-
41
- Example:
42
- Basic usage
43
- >>> from langchain_dev_utils.agents.middleware import create_handoffs_tool
44
- >>> handoffs_tool = create_handoffs_tool("time_agent")
45
35
  """
46
- if tool_name is None:
47
- tool_name = f"transfer_to_{agent_name}"
48
- if not tool_name.endswith("_agent"):
49
- tool_name += "_agent"
50
36
 
37
+ tool_name = f"transfer_to_{agent_name}"
38
+ if not tool_name.endswith("_agent"):
39
+ tool_name += "_agent"
51
40
  if tool_description is None:
52
41
  tool_description = f"Transfer to the {agent_name}"
53
42
 
@@ -75,30 +64,107 @@ def _get_default_active_agent(state: dict[str, AgentConfig]) -> Optional[str]:
75
64
  return None
76
65
 
77
66
 
78
- class HandoffsAgentMiddleware(AgentMiddleware):
67
+ def _transform_agent_config(
68
+ config: dict[str, AgentConfig],
69
+ handoffs_tools: list[BaseTool],
70
+ ) -> dict[str, AgentConfig]:
71
+ """Transform the agent config to add handoffs tools.
72
+
73
+ Args:
74
+ config (dict[str, AgentConfig]): The agent config.
75
+ handoffs_tools (list[BaseTool]): The list of handoffs tools.
76
+
77
+ Returns:
78
+ dict[str, AgentConfig]: The transformed agent config.
79
+ """
80
+
81
+ new_config = {}
82
+ for agent_name, _cfg in config.items():
83
+ new_config[agent_name] = {}
84
+
85
+ if "model" in _cfg:
86
+ new_config[agent_name]["model"] = _cfg["model"]
87
+ if "prompt" in _cfg:
88
+ new_config[agent_name]["prompt"] = _cfg["prompt"]
89
+ if "default" in _cfg:
90
+ new_config[agent_name]["default"] = _cfg["default"]
91
+ if "tools" in _cfg:
92
+ new_config[agent_name]["tools"] = _cfg["tools"]
93
+
94
+ handoffs = _cfg.get("handoffs", [])
95
+ if handoffs == "all":
96
+ handoff_tools = [
97
+ handoff_tool
98
+ for handoff_tool in handoffs_tools
99
+ if handoff_tool.name != f"transfer_to_{agent_name}"
100
+ ]
101
+ else:
102
+ if not isinstance(handoffs, list):
103
+ raise ValueError(
104
+ f"handoffs for agent {agent_name} must be a list of agent names or 'all'"
105
+ )
106
+
107
+ handoff_tools = [
108
+ handoff_tool
109
+ for handoff_tool in handoffs_tools
110
+ if handoff_tool.name
111
+ in [
112
+ f"transfer_to_{_handoff_agent_name}"
113
+ for _handoff_agent_name in handoffs
114
+ ]
115
+ ]
116
+
117
+ new_config[agent_name]["tools"] = [
118
+ *new_config[agent_name].get("tools", []),
119
+ *handoff_tools,
120
+ ]
121
+ return new_config
122
+
123
+
124
+ class HandoffAgentMiddleware(AgentMiddleware):
79
125
  """Agent middleware for switching between multiple agents.
80
126
  This middleware dynamically replaces model call parameters based on the currently active agent configuration, enabling seamless switching between different agents.
81
127
 
82
128
  Args:
83
129
  agents_config (dict[str, AgentConfig]): A dictionary of agent configurations.
130
+ custom_handoffs_tool_descriptions (Optional[dict[str, str]]): A dictionary of custom tool descriptions for handoffs tools. Defaults to None.
84
131
 
85
132
  Examples:
86
133
  ```python
87
- from langchain_dev_utils.agents.middleware import HandoffsAgentMiddleware
88
- middleware = HandoffsAgentMiddleware(agents_config)
134
+ from langchain_dev_utils.agents.middleware import HandoffAgentMiddleware
135
+ middleware = HandoffAgentMiddleware(agents_config)
89
136
  ```
90
137
  """
91
138
 
92
139
  state_schema = MultiAgentState
93
140
 
94
- def __init__(self, agents_config: dict[str, AgentConfig]):
141
+ def __init__(
142
+ self,
143
+ agents_config: dict[str, AgentConfig],
144
+ custom_handoffs_tool_descriptions: Optional[dict[str, str]] = None,
145
+ ) -> None:
95
146
  default_agent_name = _get_default_active_agent(agents_config)
96
147
  if default_agent_name is None:
97
148
  raise ValueError(
98
149
  "No default agent found, you must set one by set default=True"
99
150
  )
151
+
152
+ if custom_handoffs_tool_descriptions is None:
153
+ custom_handoffs_tool_descriptions = {}
154
+
155
+ handoffs_tools = [
156
+ _create_handoffs_tool(
157
+ agent_name,
158
+ custom_handoffs_tool_descriptions.get(agent_name),
159
+ )
160
+ for agent_name in agents_config.keys()
161
+ ]
100
162
  self.default_agent_name = default_agent_name
101
- self.agents_config = agents_config
163
+ self.agents_config = _transform_agent_config(
164
+ agents_config,
165
+ handoffs_tools,
166
+ )
167
+ self.tools = handoffs_tools
102
168
 
103
169
  def _get_active_agent_config(self, request: ModelRequest) -> dict[str, Any]:
104
170
  active_agent_name = request.state.get("active_agent", self.default_agent_name)
@@ -104,9 +104,14 @@ class PlanState(AgentState):
104
104
  plan: NotRequired[list[Plan]]
105
105
 
106
106
 
107
- def create_write_plan_tool(
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(
108
114
  description: Optional[str] = None,
109
- message_key: Optional[str] = None,
110
115
  ) -> BaseTool:
111
116
  """Create a tool for writing initial plan.
112
117
 
@@ -116,22 +121,15 @@ def create_write_plan_tool(
116
121
 
117
122
  Args:
118
123
  description: The description of the tool. Uses default description if not provided.
119
- message_key: The key of the message to be updated. Defaults to "messages".
120
124
 
121
125
  Returns:
122
126
  BaseTool: The tool for writing initial plan.
123
-
124
- Example:
125
- Basic usage:
126
- >>> from langchain_dev_utils.agents.middleware import create_write_plan_tool
127
- >>> write_plan_tool = create_write_plan_tool()
128
127
  """
129
128
 
130
129
  @tool(
131
130
  description=description or _DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION,
132
131
  )
133
132
  def write_plan(plan: list[str], runtime: ToolRuntime):
134
- msg_key = message_key or "messages"
135
133
  return Command(
136
134
  update={
137
135
  "plan": [
@@ -141,7 +139,7 @@ def create_write_plan_tool(
141
139
  }
142
140
  for index, content in enumerate(plan)
143
141
  ],
144
- msg_key: [
142
+ "messages": [
145
143
  ToolMessage(
146
144
  content=f"Plan successfully written, please first execute the {plan[0]} sub-plan (no need to change the status to in_process)",
147
145
  tool_call_id=runtime.tool_call_id,
@@ -153,9 +151,8 @@ def create_write_plan_tool(
153
151
  return write_plan
154
152
 
155
153
 
156
- def create_finish_sub_plan_tool(
154
+ def _create_finish_sub_plan_tool(
157
155
  description: Optional[str] = None,
158
- message_key: Optional[str] = None,
159
156
  ) -> BaseTool:
160
157
  """Create a tool for finishing sub-plan tasks.
161
158
 
@@ -164,15 +161,9 @@ def create_finish_sub_plan_tool(
164
161
 
165
162
  Args:
166
163
  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
164
 
169
165
  Returns:
170
166
  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
167
  """
177
168
 
178
169
  @tool(
@@ -181,7 +172,6 @@ def create_finish_sub_plan_tool(
181
172
  def finish_sub_plan(
182
173
  runtime: ToolRuntime,
183
174
  ):
184
- msg_key = message_key or "messages"
185
175
  plan_list = runtime.state.get("plan", [])
186
176
 
187
177
  sub_finish_plan = ""
@@ -200,7 +190,7 @@ def create_finish_sub_plan_tool(
200
190
  return Command(
201
191
  update={
202
192
  "plan": plan_list,
203
- msg_key: [
193
+ "messages": [
204
194
  ToolMessage(
205
195
  content=sub_finish_plan + sub_next_plan,
206
196
  tool_call_id=runtime.tool_call_id,
@@ -212,7 +202,7 @@ def create_finish_sub_plan_tool(
212
202
  return finish_sub_plan
213
203
 
214
204
 
215
- def create_read_plan_tool(
205
+ def _create_read_plan_tool(
216
206
  description: Optional[str] = None,
217
207
  ):
218
208
  """Create a tool for reading all sub-plans.
@@ -225,11 +215,6 @@ def create_read_plan_tool(
225
215
 
226
216
  Returns:
227
217
  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
218
  """
234
219
 
235
220
  @tool(
@@ -287,12 +272,8 @@ class PlanMiddleware(AgentMiddleware):
287
272
  tool. If not provided, uses the default `_PLAN_SYSTEM_PROMPT` or
288
273
  `_PLAN_SYSTEM_PROMPT_NOT_READ_PLAN` based on the `use_read_plan_tool`
289
274
  parameter.
290
- write_plan_tool_description: Description of the `write_plan` tool.
291
- If not provided, uses the default `_DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION`.
292
- finish_sub_plan_tool_description: Description of the `finish_sub_plan` tool.
293
- If not provided, uses the default `_DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION`.
294
- read_plan_tool_description: Description of the `read_plan` tool.
295
- If not provided, uses the default `_DEFAULT_READ_PLAN_TOOL_DESCRIPTION`.
275
+ custom_plan_tool_descriptions: Custom descriptions for the plan tools.
276
+ If not provided, uses the default descriptions.
296
277
  use_read_plan_tool: Whether to use the `read_plan` tool.
297
278
  If not provided, uses the default `True`.
298
279
 
@@ -316,31 +297,34 @@ class PlanMiddleware(AgentMiddleware):
316
297
  self,
317
298
  *,
318
299
  system_prompt: Optional[str] = None,
319
- write_plan_tool_description: Optional[str] = None,
320
- finish_sub_plan_tool_description: Optional[str] = None,
321
- read_plan_tool_description: Optional[str] = None,
300
+ custom_plan_tool_descriptions: Optional[PlanToolDescription] = None,
322
301
  use_read_plan_tool: bool = True,
323
302
  ) -> None:
324
303
  super().__init__()
325
304
 
326
- write_plan_tool_description = (
327
- write_plan_tool_description or _DEFAULT_WRITE_PLAN_TOOL_DESCRIPTION
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,
328
311
  )
329
- finish_sub_plan_tool_description = (
330
- finish_sub_plan_tool_description
331
- or _DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION
312
+ finish_sub_plan_tool_description = custom_plan_tool_descriptions.get(
313
+ "finish_sub_plan",
314
+ _DEFAULT_FINISH_SUB_PLAN_TOOL_DESCRIPTION,
332
315
  )
333
- read_plan_tool_description = (
334
- read_plan_tool_description or _DEFAULT_READ_PLAN_TOOL_DESCRIPTION
316
+ read_plan_tool_description = custom_plan_tool_descriptions.get(
317
+ "read_plan",
318
+ _DEFAULT_READ_PLAN_TOOL_DESCRIPTION,
335
319
  )
336
320
 
337
321
  tools = [
338
- create_write_plan_tool(description=write_plan_tool_description),
339
- create_finish_sub_plan_tool(description=finish_sub_plan_tool_description),
322
+ _create_write_plan_tool(description=write_plan_tool_description),
323
+ _create_finish_sub_plan_tool(description=finish_sub_plan_tool_description),
340
324
  ]
341
325
 
342
326
  if use_read_plan_tool:
343
- tools.append(create_read_plan_tool(description=read_plan_tool_description))
327
+ tools.append(_create_read_plan_tool(description=read_plan_tool_description))
344
328
 
345
329
  if system_prompt is None:
346
330
  if use_read_plan_tool:
@@ -0,0 +1,274 @@
1
+ import asyncio
2
+ from typing import Any, Awaitable, Callable, Optional, cast
3
+
4
+ from langchain.tools import ToolRuntime
5
+ from langchain_core.messages import AnyMessage, HumanMessage
6
+ from langchain_core.tools import BaseTool, StructuredTool
7
+ from langgraph.graph.state import CompiledStateGraph
8
+
9
+ from langchain_dev_utils.message_convert import format_sequence
10
+
11
+
12
+ def _process_input(request: str, runtime: ToolRuntime) -> str:
13
+ return request
14
+
15
+
16
+ def _process_output(
17
+ request: str, response: list[AnyMessage], runtime: ToolRuntime
18
+ ) -> Any:
19
+ return response[-1].content
20
+
21
+
22
+ def wrap_agent_as_tool(
23
+ agent: CompiledStateGraph,
24
+ tool_name: Optional[str] = None,
25
+ tool_description: Optional[str] = None,
26
+ pre_input_hooks: Optional[
27
+ tuple[
28
+ Callable[[str, ToolRuntime], str],
29
+ Callable[[str, ToolRuntime], Awaitable[str]],
30
+ ]
31
+ | Callable[[str, ToolRuntime], str]
32
+ ] = None,
33
+ post_output_hooks: Optional[
34
+ tuple[
35
+ Callable[[str, list[AnyMessage], ToolRuntime], Any],
36
+ Callable[[str, list[AnyMessage], ToolRuntime], Awaitable[Any]],
37
+ ]
38
+ | Callable[[str, list[AnyMessage], ToolRuntime], Any]
39
+ ] = None,
40
+ ) -> BaseTool:
41
+ """Wraps an agent as a tool
42
+
43
+ Args:
44
+ agent: The agent to wrap
45
+ tool_name: The name of the tool
46
+ tool_description: The description of the tool
47
+ pre_input_hooks: Hooks to run before the input is processed
48
+ post_output_hooks: Hooks to run after the output is processed
49
+
50
+ Returns:
51
+ BaseTool: The wrapped agent as a tool
52
+
53
+ Example:
54
+ >>> from langchain_dev_utils.agents import wrap_agent_as_tool, create_agent
55
+ >>>
56
+ >>> call_time_agent_tool = wrap_agent_as_tool(
57
+ ... time_agent,
58
+ ... tool_name="call_time_agent",
59
+ ... tool_description="Used to invoke the time sub-agent to perform time-related tasks"
60
+ ... )
61
+ >>>
62
+ >>> agent = create_agent("vllm:qwen3-4b", tools=[call_time_agent_tool], name="agent")
63
+
64
+ >>> response = agent.invoke({"messages": [HumanMessage(content="What time is it now?")]})
65
+ >>> response
66
+ """
67
+ if agent.name is None:
68
+ raise ValueError("Agent name must not be None")
69
+
70
+ process_input = _process_input
71
+ process_input_async = _process_input
72
+ process_output = _process_output
73
+ process_output_async = _process_output
74
+
75
+ if pre_input_hooks:
76
+ if isinstance(pre_input_hooks, tuple):
77
+ process_input = pre_input_hooks[0]
78
+ process_input_async = pre_input_hooks[1]
79
+ else:
80
+ process_input = pre_input_hooks
81
+ process_input_async = pre_input_hooks
82
+
83
+ if post_output_hooks:
84
+ if isinstance(post_output_hooks, tuple):
85
+ process_output = post_output_hooks[0]
86
+ process_output_async = post_output_hooks[1]
87
+ else:
88
+ process_output = post_output_hooks
89
+ process_output_async = post_output_hooks
90
+
91
+ def call_agent(
92
+ request: str,
93
+ runtime: ToolRuntime,
94
+ ) -> str:
95
+ request = process_input(request, runtime) if process_input else request
96
+
97
+ messages = [HumanMessage(content=request)]
98
+ response = agent.invoke({"messages": messages})
99
+
100
+ response = process_output(request, response["messages"], runtime)
101
+ return response
102
+
103
+ async def acall_agent(
104
+ request: str,
105
+ runtime: ToolRuntime,
106
+ ) -> str:
107
+ if asyncio.iscoroutinefunction(process_input_async):
108
+ request = await process_input_async(request, runtime)
109
+ else:
110
+ request = cast(str, process_input_async(request, runtime))
111
+
112
+ messages = [HumanMessage(content=request)]
113
+ response = await agent.ainvoke({"messages": messages})
114
+
115
+ if asyncio.iscoroutinefunction(process_output_async):
116
+ response = await process_output_async(
117
+ request, response["messages"], runtime
118
+ )
119
+ else:
120
+ response = process_output(request, response["messages"], runtime)
121
+
122
+ return response
123
+
124
+ if tool_name is None:
125
+ tool_name = f"transfor_to_{agent.name}"
126
+ if not tool_name.endswith("_agent"):
127
+ tool_name += "_agent"
128
+
129
+ if tool_description is None:
130
+ tool_description = f"This tool transforms input to {agent.name}"
131
+
132
+ return StructuredTool.from_function(
133
+ func=call_agent,
134
+ coroutine=acall_agent,
135
+ name=tool_name,
136
+ description=tool_description,
137
+ )
138
+
139
+
140
+ def wrap_all_agents_as_tool(
141
+ agents: list[CompiledStateGraph],
142
+ tool_name: Optional[str] = None,
143
+ tool_description: Optional[str] = None,
144
+ pre_input_hooks: Optional[
145
+ tuple[
146
+ Callable[[str, ToolRuntime], str],
147
+ Callable[[str, ToolRuntime], Awaitable[str]],
148
+ ]
149
+ | Callable[[str, ToolRuntime], str]
150
+ ] = None,
151
+ post_output_hooks: Optional[
152
+ tuple[
153
+ Callable[[str, list[AnyMessage], ToolRuntime], Any],
154
+ Callable[[str, list[AnyMessage], ToolRuntime], Awaitable[Any]],
155
+ ]
156
+ | Callable[[str, list[AnyMessage], ToolRuntime], Any]
157
+ ] = None,
158
+ ) -> BaseTool:
159
+ """Wraps all agents as single tool
160
+
161
+ Args:
162
+ agents: The agents to wrap
163
+ tool_name: The name of the tool, default to "task"
164
+ tool_description: The description of the tool
165
+ pre_input_hooks: Hooks to run before the input is processed
166
+ post_output_hooks: Hooks to run after the output is processed
167
+
168
+ Returns:
169
+ BaseTool: The wrapped agents as single tool
170
+
171
+ Example:
172
+ >>> from langchain_dev_utils.agents import wrap_all_agents_as_tool, create_agent
173
+ >>>
174
+ >>> call_time_agent_tool = wrap_all_agents_as_tool(
175
+ ... [time_agent,weather_agent],
176
+ ... tool_name="call_sub_agents",
177
+ ... tool_description="Used to invoke the sub-agents to perform tasks"
178
+ ... )
179
+ >>>
180
+ >>> agent = create_agent("vllm:qwen3-4b", tools=[call_sub_agents_tool], name="agent")
181
+
182
+ >>> response = agent.invoke({"messages": [HumanMessage(content="What time is it now?")]})
183
+ >>> response
184
+ """
185
+ if len(agents) <= 1:
186
+ raise ValueError("At least more than one agent must be provided")
187
+
188
+ agents_map = {}
189
+
190
+ for agent in agents:
191
+ if agent.name is None:
192
+ raise ValueError("Agent name must not be provided")
193
+ if agent.name in agents_map:
194
+ raise ValueError("Agent name must be unique")
195
+ agents_map[agent.name] = agent
196
+
197
+ process_input = _process_input
198
+ process_input_async = _process_input
199
+ process_output = _process_output
200
+ process_output_async = _process_output
201
+
202
+ if pre_input_hooks:
203
+ if isinstance(pre_input_hooks, tuple):
204
+ process_input = pre_input_hooks[0]
205
+ process_input_async = pre_input_hooks[1]
206
+ else:
207
+ process_input = pre_input_hooks
208
+ process_input_async = pre_input_hooks
209
+
210
+ if post_output_hooks:
211
+ if isinstance(post_output_hooks, tuple):
212
+ process_output = post_output_hooks[0]
213
+ process_output_async = post_output_hooks[1]
214
+ else:
215
+ process_output = post_output_hooks
216
+ process_output_async = post_output_hooks
217
+
218
+ def call_agent(
219
+ agent_name: str,
220
+ description: str,
221
+ runtime: ToolRuntime,
222
+ ) -> str:
223
+ task_description = (
224
+ process_input(description, runtime) if process_input else description
225
+ )
226
+
227
+ if agent_name not in agents_map:
228
+ raise ValueError(f"Agent {agent_name} not found")
229
+
230
+ messages = [HumanMessage(content=task_description)]
231
+ response = agents_map[agent_name].invoke({"messages": messages})
232
+
233
+ response = process_output(task_description, response["messages"], runtime)
234
+ return response
235
+
236
+ async def acall_agent(
237
+ agent_name: str,
238
+ description: str,
239
+ runtime: ToolRuntime,
240
+ ) -> str:
241
+ if asyncio.iscoroutinefunction(process_input_async):
242
+ task_description = await process_input_async(description, runtime)
243
+ else:
244
+ task_description = cast(str, process_input_async(description, runtime))
245
+
246
+ if agent_name not in agents_map:
247
+ raise ValueError(f"Agent {agent_name} not found")
248
+
249
+ messages = [HumanMessage(content=task_description)]
250
+ response = await agents_map[agent_name].ainvoke({"messages": messages})
251
+
252
+ if asyncio.iscoroutinefunction(process_output_async):
253
+ response = await process_output_async(
254
+ task_description, response["messages"], runtime
255
+ )
256
+ else:
257
+ response = process_output(task_description, response["messages"], runtime)
258
+
259
+ return response
260
+
261
+ if tool_name is None:
262
+ tool_name = "task"
263
+
264
+ if tool_description is None:
265
+ tool_description = (
266
+ "Launch an ephemeral subagent for a task.\nAvailable agents:\n "
267
+ + format_sequence(list(agents_map.keys()), with_num=True)
268
+ )
269
+ return StructuredTool.from_function(
270
+ func=call_agent,
271
+ coroutine=acall_agent,
272
+ name=tool_name,
273
+ description=tool_description,
274
+ )