langchain-dev-utils 1.3.6__tar.gz → 1.3.7__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 (68) hide show
  1. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/PKG-INFO +1 -1
  2. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/pyproject.toml +1 -1
  3. langchain_dev_utils-1.3.7/src/langchain_dev_utils/__init__.py +1 -0
  4. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/handoffs.py +30 -20
  5. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/model_router.py +9 -12
  6. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/plan.py +13 -18
  7. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_handoffs_middleware.py +51 -4
  8. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_load_model.py +0 -11
  9. langchain_dev_utils-1.3.6/src/langchain_dev_utils/__init__.py +0 -1
  10. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/.gitignore +0 -0
  11. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/.python-version +0 -0
  12. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/.vscode/settings.json +0 -0
  13. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/LICENSE +0 -0
  14. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/README.md +0 -0
  15. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/README_cn.md +0 -0
  16. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/_utils.py +0 -0
  17. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/__init__.py +0 -0
  18. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/factory.py +0 -0
  19. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/file_system.py +0 -0
  20. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/__init__.py +0 -0
  21. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/format_prompt.py +0 -0
  22. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/model_fallback.py +0 -0
  23. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/summarization.py +0 -0
  24. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/tool_call_repair.py +0 -0
  25. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/tool_emulator.py +0 -0
  26. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/middleware/tool_selection.py +0 -0
  27. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/plan.py +0 -0
  28. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/agents/wrap.py +0 -0
  29. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/chat_models/__init__.py +0 -0
  30. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/chat_models/adapters/__init__.py +0 -0
  31. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/chat_models/adapters/create_utils.py +0 -0
  32. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/chat_models/adapters/openai_compatible.py +0 -0
  33. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/chat_models/adapters/register_profiles.py +0 -0
  34. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/chat_models/base.py +0 -0
  35. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/chat_models/types.py +0 -0
  36. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/embeddings/__init__.py +0 -0
  37. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/embeddings/adapters/__init__.py +0 -0
  38. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/embeddings/adapters/create_utils.py +0 -0
  39. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/embeddings/adapters/openai_compatible.py +0 -0
  40. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/embeddings/base.py +0 -0
  41. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/message_convert/__init__.py +0 -0
  42. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/message_convert/content.py +0 -0
  43. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/message_convert/format.py +0 -0
  44. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/pipeline/__init__.py +0 -0
  45. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/pipeline/parallel.py +0 -0
  46. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/pipeline/sequential.py +0 -0
  47. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/pipeline/types.py +0 -0
  48. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/py.typed +0 -0
  49. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/tool_calling/__init__.py +0 -0
  50. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/tool_calling/human_in_the_loop.py +0 -0
  51. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/src/langchain_dev_utils/tool_calling/utils.py +0 -0
  52. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/__init__.py +0 -0
  53. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_agent.py +0 -0
  54. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_chat_models.py +0 -0
  55. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_embedding.py +0 -0
  56. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_human_in_the_loop.py +0 -0
  57. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_load_embbeding.py +0 -0
  58. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_messages.py +0 -0
  59. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_model_tool_emulator.py +0 -0
  60. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_pipline.py +0 -0
  61. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_plan_middleware.py +0 -0
  62. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_router_model.py +0 -0
  63. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_tool_call_repair.py +0 -0
  64. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_tool_calling.py +0 -0
  65. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/test_wrap_agent.py +0 -0
  66. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/utils/__init__.py +0 -0
  67. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/tests/utils/register.py +0 -0
  68. {langchain_dev_utils-1.3.6 → langchain_dev_utils-1.3.7}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-dev-utils
3
- Version: 1.3.6
3
+ Version: 1.3.7
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.6"
3
+ version = "1.3.7"
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.7"
@@ -1,4 +1,4 @@
1
- from typing import Any, Awaitable, Callable, Literal
1
+ from typing import Any, Awaitable, Callable, Literal, cast
2
2
 
3
3
  from langchain.agents import AgentState
4
4
  from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
@@ -128,6 +128,7 @@ class HandoffAgentMiddleware(AgentMiddleware):
128
128
  Args:
129
129
  agents_config (dict[str, AgentConfig]): A dictionary of agent configurations.
130
130
  custom_handoffs_tool_descriptions (Optional[dict[str, str]]): A dictionary of custom tool descriptions for handoffs tools. Defaults to None.
131
+ handoffs_tool_overrides (Optional[dict[str, BaseTool]]): A dictionary of handoffs tools to override. Defaults to None.
131
132
 
132
133
  Examples:
133
134
  ```python
@@ -142,6 +143,7 @@ class HandoffAgentMiddleware(AgentMiddleware):
142
143
  self,
143
144
  agents_config: dict[str, AgentConfig],
144
145
  custom_handoffs_tool_descriptions: Optional[dict[str, str]] = None,
146
+ handoffs_tool_overrides: Optional[dict[str, BaseTool]] = None,
145
147
  ) -> None:
146
148
  default_agent_name = _get_default_active_agent(agents_config)
147
149
  if default_agent_name is None:
@@ -152,13 +154,23 @@ class HandoffAgentMiddleware(AgentMiddleware):
152
154
  if custom_handoffs_tool_descriptions is None:
153
155
  custom_handoffs_tool_descriptions = {}
154
156
 
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
- ]
157
+ if handoffs_tool_overrides is None:
158
+ handoffs_tool_overrides = {}
159
+
160
+ handoffs_tools = []
161
+ for agent_name in agents_config.keys():
162
+ if not handoffs_tool_overrides.get(agent_name):
163
+ handoffs_tools.append(
164
+ _create_handoffs_tool(
165
+ agent_name,
166
+ custom_handoffs_tool_descriptions.get(agent_name),
167
+ )
168
+ )
169
+ else:
170
+ handoffs_tools.append(
171
+ cast(BaseTool, handoffs_tool_overrides.get(agent_name))
172
+ )
173
+
162
174
  self.default_agent_name = default_agent_name
163
175
  self.agents_config = _transform_agent_config(
164
176
  agents_config,
@@ -166,7 +178,7 @@ class HandoffAgentMiddleware(AgentMiddleware):
166
178
  )
167
179
  self.tools = handoffs_tools
168
180
 
169
- def _get_active_agent_config(self, request: ModelRequest) -> dict[str, Any]:
181
+ def _get_override_request(self, request: ModelRequest) -> ModelRequest:
170
182
  active_agent_name = request.state.get("active_agent", self.default_agent_name)
171
183
 
172
184
  _config = self.agents_config[active_agent_name]
@@ -181,24 +193,22 @@ class HandoffAgentMiddleware(AgentMiddleware):
181
193
  params["system_prompt"] = _config.get("prompt")
182
194
  if _config.get("tools"):
183
195
  params["tools"] = _config.get("tools")
184
- return params
196
+
197
+ if params:
198
+ return request.override(**params)
199
+ else:
200
+ return request
185
201
 
186
202
  def wrap_model_call(
187
203
  self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
188
204
  ) -> ModelCallResult:
189
- override_kwargs = self._get_active_agent_config(request)
190
- if override_kwargs:
191
- return handler(request.override(**override_kwargs))
192
- else:
193
- return handler(request)
205
+ override_request = self._get_override_request(request)
206
+ return handler(override_request)
194
207
 
195
208
  async def awrap_model_call(
196
209
  self,
197
210
  request: ModelRequest,
198
211
  handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
199
212
  ) -> ModelCallResult:
200
- override_kwargs = self._get_active_agent_config(request)
201
- if override_kwargs:
202
- return await handler(request.override(**override_kwargs))
203
- else:
204
- return await handler(request)
213
+ override_request = self._get_override_request(request)
214
+ return await handler(override_request)
@@ -150,7 +150,7 @@ class ModelRouterMiddleware(AgentMiddleware):
150
150
  model_name = await self._aselect_model(state["messages"])
151
151
  return {"router_model_selection": model_name}
152
152
 
153
- def _get_override_kwargs(self, request: ModelRequest) -> dict[str, Any]:
153
+ def _get_override_request(self, request: ModelRequest) -> ModelRequest:
154
154
  model_dict = {
155
155
  item["model_name"]: {
156
156
  "tools": item.get("tools", None),
@@ -180,24 +180,21 @@ class ModelRouterMiddleware(AgentMiddleware):
180
180
  content=model_values["system_prompt"]
181
181
  )
182
182
 
183
- return override_kwargs
183
+ if override_kwargs:
184
+ return request.override(**override_kwargs)
185
+ else:
186
+ return request
184
187
 
185
188
  def wrap_model_call(
186
189
  self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
187
190
  ) -> ModelCallResult:
188
- override_kwargs = self._get_override_kwargs(request)
189
- if override_kwargs:
190
- return handler(request.override(**override_kwargs))
191
- else:
192
- return handler(request)
191
+ override_request = self._get_override_request(request)
192
+ return handler(override_request)
193
193
 
194
194
  async def awrap_model_call(
195
195
  self,
196
196
  request: ModelRequest,
197
197
  handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
198
198
  ) -> ModelCallResult:
199
- override_kwargs = self._get_override_kwargs(request)
200
- if override_kwargs:
201
- return await handler(request.override(**override_kwargs))
202
- else:
203
- return await handler(request)
199
+ override_request = self._get_override_request(request)
200
+ return await handler(override_request)
@@ -335,12 +335,8 @@ class PlanMiddleware(AgentMiddleware):
335
335
  self.system_prompt = system_prompt
336
336
  self.tools = tools
337
337
 
338
- def wrap_model_call(
339
- self,
340
- request: ModelRequest,
341
- handler: Callable[[ModelRequest], ModelResponse],
342
- ) -> ModelCallResult:
343
- """Update the system message to include the plan system prompt."""
338
+ def _get_override_request(self, request: ModelRequest) -> ModelRequest:
339
+ """Add the plan system prompt to the system message."""
344
340
  if request.system_message is not None:
345
341
  new_system_content = [
346
342
  *request.system_message.content_blocks,
@@ -351,7 +347,15 @@ class PlanMiddleware(AgentMiddleware):
351
347
  new_system_message = SystemMessage(
352
348
  content=cast("list[str | dict[str, str]]", new_system_content)
353
349
  )
354
- return handler(request.override(system_message=new_system_message))
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)
355
359
 
356
360
  async def awrap_model_call(
357
361
  self,
@@ -359,14 +363,5 @@ class PlanMiddleware(AgentMiddleware):
359
363
  handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
360
364
  ) -> ModelCallResult:
361
365
  """Update the system message to include the plan system prompt."""
362
- if request.system_message is not None:
363
- new_system_content = [
364
- *request.system_message.content_blocks,
365
- {"type": "text", "text": f"\n\n{self.system_prompt}"},
366
- ]
367
- else:
368
- new_system_content = [{"type": "text", "text": self.system_prompt}]
369
- new_system_message = SystemMessage(
370
- content=cast("list[str | dict[str, str]]", new_system_content)
371
- )
372
- return await handler(request.override(system_message=new_system_message))
366
+ override_request = self._get_override_request(request)
367
+ return await handler(override_request)
@@ -1,8 +1,9 @@
1
1
  from datetime import datetime
2
2
 
3
- from langchain.tools import tool
3
+ from langchain.tools import BaseTool, ToolRuntime, tool
4
4
  from langchain_core.messages import HumanMessage, ToolMessage
5
5
  from langgraph.checkpoint.memory import InMemorySaver
6
+ from langgraph.types import Command
6
7
 
7
8
  from langchain_dev_utils.agents import create_agent
8
9
  from langchain_dev_utils.agents.middleware import (
@@ -24,6 +25,22 @@ def run_code(code: str) -> str:
24
25
  return "Running code successfully"
25
26
 
26
27
 
28
+ @tool
29
+ def transfer_to_coding_agent(runtime: ToolRuntime) -> Command:
30
+ """This tool help you transfer to the coding agent."""
31
+ return Command(
32
+ update={
33
+ "messages": [
34
+ ToolMessage(
35
+ content="Successfully transferred to coding agent",
36
+ tool_call_id=runtime.tool_call_id,
37
+ )
38
+ ],
39
+ "active_agent": "code_agent",
40
+ }
41
+ )
42
+
43
+
27
44
  agents_config: dict[str, AgentConfig] = {
28
45
  "time_agent": {
29
46
  "model": "zai:glm-4.5",
@@ -56,14 +73,24 @@ agents_config: dict[str, AgentConfig] = {
56
73
  custom_tool_descriptions: dict[str, str] = {
57
74
  "time_agent": "transfer to the time agent to answer time-related questions",
58
75
  "talk_agent": "transfer to the talk agent to answer user questions",
59
- "code_agent": "transfer to the code agent to answer code-related questions",
76
+ }
77
+
78
+
79
+ handoffs_tool_map: dict[str, BaseTool] = {
80
+ "code_agent": transfer_to_coding_agent,
60
81
  }
61
82
 
62
83
 
63
84
  def test_handoffs_middleware():
64
85
  agent = create_agent(
65
86
  model="dashscope:qwen3-max",
66
- middleware=[HandoffAgentMiddleware(agents_config, custom_tool_descriptions)],
87
+ middleware=[
88
+ HandoffAgentMiddleware(
89
+ agents_config=agents_config,
90
+ custom_handoffs_tool_descriptions=custom_tool_descriptions,
91
+ handoffs_tool_overrides=handoffs_tool_map,
92
+ )
93
+ ],
67
94
  tools=[
68
95
  get_current_time,
69
96
  run_code,
@@ -95,13 +122,26 @@ def test_handoffs_middleware():
95
122
  == "qwen3-coder-plus"
96
123
  )
97
124
  assert isinstance(response["messages"][-2], ToolMessage)
125
+ assert any(
126
+ message
127
+ for message in response["messages"]
128
+ if isinstance(message, ToolMessage)
129
+ and "Successfully transferred to coding agent" in message.content
130
+ and message.name == "transfer_to_coding_agent"
131
+ )
98
132
  assert "active_agent" in response and response["active_agent"] == "code_agent"
99
133
 
100
134
 
101
135
  async def test_handoffs_middleware_async():
102
136
  agent = create_agent(
103
137
  model="dashscope:qwen3-max",
104
- middleware=[HandoffAgentMiddleware(agents_config, custom_tool_descriptions)],
138
+ middleware=[
139
+ HandoffAgentMiddleware(
140
+ agents_config=agents_config,
141
+ custom_handoffs_tool_descriptions=custom_tool_descriptions,
142
+ handoffs_tool_overrides=handoffs_tool_map,
143
+ )
144
+ ],
105
145
  tools=[
106
146
  get_current_time,
107
147
  run_code,
@@ -133,4 +173,11 @@ async def test_handoffs_middleware_async():
133
173
  == "qwen3-coder-plus"
134
174
  )
135
175
  assert isinstance(response["messages"][-2], ToolMessage)
176
+ assert any(
177
+ message
178
+ for message in response["messages"]
179
+ if isinstance(message, ToolMessage)
180
+ and "Successfully transferred to coding agent" in message.content
181
+ and message.name == "transfer_to_coding_agent"
182
+ )
136
183
  assert "active_agent" in response and response["active_agent"] == "code_agent"
@@ -76,17 +76,6 @@ async def test_model_tool_calling_async(
76
76
  assert hasattr(response, "tool_calls") and len(response.tool_calls) == 1
77
77
 
78
78
 
79
- def test_model_with_reasoning(reasoning_model: BaseChatModel):
80
- response = reasoning_model.invoke("hello?")
81
- assert response.additional_kwargs.get("reasoning_content")
82
-
83
-
84
- @pytest.mark.asyncio
85
- async def test_model_with_reasoning_async(reasoning_model: BaseChatModel):
86
- response = await reasoning_model.ainvoke("hello?")
87
- assert response.additional_kwargs.get("reasoning_content")
88
-
89
-
90
79
  def test_model_profile():
91
80
  model = load_chat_model("dashscope:qwen-flash")
92
81
  assert model.profile == ALI_PROFILES["qwen-flash"]
@@ -1 +0,0 @@
1
- __version__ = "1.3.6"