Skip to content

Commit bdb7dbb

Browse files
authored
feat(langchain_v1): represent server side tools in modifyModelRequest and update tool handling (#33274)
* Add server side tools to modifyModelRequest (represented as dicts) * Update some of the logic in terms of which tools are bound to ToolNode * We still have a constraint on changing the response format dynamically when using tool strategy. structured_output_tools are being using in some of the edges. The code is now raising an exception to explain that it's a limitation of the implementation. (We can add support later.)
1 parent 30f7c87 commit bdb7dbb

File tree

2 files changed

+64
-30
lines changed

2 files changed

+64
-30
lines changed

libs/langchain_v1/langchain/agents/middleware/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class ModelRequest:
6262
system_prompt: str | None
6363
messages: list[AnyMessage] # excluding system prompt
6464
tool_choice: Any | None
65-
tools: list[BaseTool]
65+
tools: list[BaseTool | dict]
6666
response_format: ResponseFormat | None
6767
model_settings: dict[str, Any] = field(default_factory=dict)
6868

libs/langchain_v1/langchain/agents/middleware_agent.py

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -229,32 +229,32 @@ def create_agent( # noqa: PLR0915
229229
# Setup tools
230230
tool_node: ToolNode | None = None
231231
if isinstance(tools, list):
232-
# Extract builtin provider tools (dict format)
233-
builtin_tools = [t for t in tools if isinstance(t, dict)]
232+
# Extract built-in provider tools (dict format) and regular tools (BaseTool)
233+
built_in_tools = [t for t in tools if isinstance(t, dict)]
234234
regular_tools = [t for t in tools if not isinstance(t, dict)]
235235

236-
# Add structured output tools to regular tools
237-
structured_tools = [info.tool for info in structured_output_tools.values()]
238-
all_tools = middleware_tools + regular_tools + structured_tools
236+
# Tools that require client-side execution (must be in ToolNode)
237+
available_tools = middleware_tools + regular_tools
239238

240-
# Only create ToolNode if we have tools
241-
tool_node = ToolNode(tools=all_tools) if all_tools else None
242-
default_tools = regular_tools + builtin_tools + structured_tools + middleware_tools
239+
# Only create ToolNode if we have client-side tools
240+
tool_node = ToolNode(tools=available_tools) if available_tools else None
241+
242+
# Default tools for ModelRequest initialization
243+
# Include built-ins and regular tools (can be changed dynamically by middleware)
244+
# Structured tools are NOT included - they're added dynamically based on response_format
245+
default_tools = regular_tools + middleware_tools + built_in_tools
243246
elif isinstance(tools, ToolNode):
244-
# tools is ToolNode or None
245247
tool_node = tools
246248
if tool_node:
247-
default_tools = list(tool_node.tools_by_name.values()) + middleware_tools
248-
# Update tool node to know about tools provided by middleware
249-
all_tools = list(tool_node.tools_by_name.values()) + middleware_tools
250-
tool_node = ToolNode(all_tools)
251-
# Add structured output tools
252-
for info in structured_output_tools.values():
253-
default_tools.append(info.tool)
249+
# Add middleware tools to existing ToolNode
250+
available_tools = list(tool_node.tools_by_name.values()) + middleware_tools
251+
tool_node = ToolNode(available_tools)
252+
253+
# default_tools includes all client-side tools (no built-ins or structured tools)
254+
default_tools = available_tools
254255
else:
255-
default_tools = (
256-
list(structured_output_tools.values()) if structured_output_tools else []
257-
) + middleware_tools
256+
# No tools provided, only middleware_tools available
257+
default_tools = middleware_tools
258258

259259
# validate middleware
260260
assert len({m.name for m in middleware}) == len(middleware), ( # noqa: S101
@@ -405,21 +405,32 @@ def _get_bound_model(request: ModelRequest) -> tuple[Runnable, ResponseFormat |
405405
Tuple of (bound_model, effective_response_format) where ``effective_response_format``
406406
is the actual strategy used (may differ from initial if auto-detected).
407407
"""
408-
# Validate requested tools are available
409-
tools_by_name = {t.name: t for t in default_tools}
410-
unknown_tool_names = [t.name for t in request.tools if t.name not in tools_by_name]
408+
# Validate ONLY client-side tools that need to exist in tool_node
409+
# Build map of available client-side tools (regular_tools + middleware_tools)
410+
available_tools_by_name = {t.name: t for t in default_tools if isinstance(t, BaseTool)}
411+
412+
# Check if any requested tools are unknown CLIENT-SIDE tools
413+
unknown_tool_names = []
414+
for t in request.tools:
415+
# Only validate BaseTool instances (skip built-in dict tools)
416+
if isinstance(t, dict):
417+
continue
418+
if t.name not in available_tools_by_name:
419+
unknown_tool_names.append(t.name)
420+
411421
if unknown_tool_names:
412-
available_tools = sorted(tools_by_name.keys())
422+
available_tool_names = sorted(available_tools_by_name.keys())
413423
msg = (
414424
f"Middleware returned unknown tool names: {unknown_tool_names}\n\n"
415-
f"Available tools: {available_tools}\n\n"
425+
f"Available client-side tools: {available_tool_names}\n\n"
416426
"To fix this issue:\n"
417427
"1. Ensure the tools are passed to create_agent() via "
418428
"the 'tools' parameter\n"
419429
"2. If using custom middleware with tools, ensure "
420430
"they're registered via middleware.tools attribute\n"
421431
"3. Verify that tool names in ModelRequest.tools match "
422-
"the actual tool.name values"
432+
"the actual tool.name values\n"
433+
"Note: Built-in provider tools (dict format) can be added dynamically."
423434
)
424435
raise ValueError(msg)
425436

@@ -437,32 +448,55 @@ def _get_bound_model(request: ModelRequest) -> tuple[Runnable, ResponseFormat |
437448
# User explicitly specified a strategy - preserve it
438449
effective_response_format = request.response_format
439450

451+
# Build final tools list including structured output tools
452+
# request.tools already contains both BaseTool and dict (built-in) tools
453+
final_tools = list(request.tools)
454+
if isinstance(effective_response_format, ToolStrategy):
455+
# Add structured output tools to final tools list
456+
structured_tools = [info.tool for info in structured_output_tools.values()]
457+
final_tools.extend(structured_tools)
458+
440459
# Bind model based on effective response format
441460
if isinstance(effective_response_format, ProviderStrategy):
442461
# Use provider-specific structured output
443462
kwargs = effective_response_format.to_model_kwargs()
444463
return (
445464
request.model.bind_tools(
446-
request.tools, strict=True, **kwargs, **request.model_settings
465+
final_tools, strict=True, **kwargs, **request.model_settings
447466
),
448467
effective_response_format,
449468
)
450469

451470
if isinstance(effective_response_format, ToolStrategy):
471+
# Current implementation requires that tools used for structured output
472+
# have to be declared upfront when creating the agent as part of the
473+
# response format. Middleware is allowed to change the response format
474+
# to a subset of the original structured tools when using ToolStrategy,
475+
# but not to add new structured tools that weren't declared upfront.
476+
# Compute output binding
477+
for tc in effective_response_format.schema_specs:
478+
if tc.name not in structured_output_tools:
479+
msg = (
480+
f"ToolStrategy specifies tool '{tc.name}' "
481+
"which wasn't declared in the original "
482+
"response format when creating the agent."
483+
)
484+
raise ValueError(msg)
485+
452486
# Force tool use if we have structured output tools
453487
tool_choice = "any" if structured_output_tools else request.tool_choice
454488
return (
455489
request.model.bind_tools(
456-
request.tools, tool_choice=tool_choice, **request.model_settings
490+
final_tools, tool_choice=tool_choice, **request.model_settings
457491
),
458492
effective_response_format,
459493
)
460494

461495
# No structured output - standard model binding
462-
if request.tools:
496+
if final_tools:
463497
return (
464498
request.model.bind_tools(
465-
request.tools, tool_choice=request.tool_choice, **request.model_settings
499+
final_tools, tool_choice=request.tool_choice, **request.model_settings
466500
),
467501
None,
468502
)

0 commit comments

Comments
 (0)