Skip to content

Commit 32ba81e

Browse files
authored
Python: Add documentation for declaration-only tools and middleware ordering (microsoft#3774)
* added explanation doctrings * copilot comments
1 parent 84cb09c commit 32ba81e

2 files changed

Lines changed: 52 additions & 6 deletions

File tree

python/packages/core/agent_framework/_tools.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -640,16 +640,25 @@ def __init__(
640640
name: The name of the function.
641641
description: A description of the function.
642642
approval_mode: Whether or not approval is required to run this tool.
643-
Default is that approval is required.
643+
Default is that approval is NOT required (``"never_require"``).
644644
max_invocations: The maximum number of times this function can be invoked.
645645
If None, there is no limit. Should be at least 1.
646646
max_invocation_exceptions: The maximum number of exceptions allowed during invocations.
647647
If None, there is no limit. Should be at least 1.
648648
additional_properties: Additional properties to set on the function.
649-
func: The function to wrap.
649+
func: The function to wrap. When ``None``, creates a declaration-only tool
650+
that has no implementation. Declaration-only tools are useful when you want
651+
the agent to reason about tool usage without executing them, or when the
652+
actual implementation exists elsewhere (e.g., client-side rendering).
650653
input_model: The Pydantic model that defines the input parameters for the function.
651654
This can also be a JSON schema dictionary.
652-
If not provided, it will be inferred from the function signature.
655+
If not provided and ``func`` is not ``None``, it will be inferred from
656+
the function signature. When ``func`` is ``None`` and ``input_model`` is
657+
not provided, the tool will use an empty input model (no parameters) in
658+
its JSON schema. For declaration-only tools that should declare
659+
parameters, explicitly provide ``input_model`` (either a Pydantic
660+
``BaseModel`` or a JSON schema dictionary) so the model can reason about
661+
the expected arguments.
653662
**kwargs: Additional keyword arguments.
654663
"""
655664
super().__init__(
@@ -1286,7 +1295,11 @@ def tool(
12861295
to bypass automatic inference from the function signature.
12871296
12881297
Args:
1289-
func: The function to decorate.
1298+
func: The function to decorate. This parameter enables the decorator to be used
1299+
both with and without parentheses: ``@tool`` directly decorates the function,
1300+
while ``@tool()`` or ``@tool(name="custom")`` returns a decorator. For
1301+
declaration-only tools (no implementation), use :class:`FunctionTool` directly
1302+
with ``func=None``—see the example below.
12901303
12911304
Keyword Args:
12921305
name: The name of the function. If not provided, the function's ``__name__``
@@ -1301,7 +1314,7 @@ def tool(
13011314
When provided, the schema is used instead of inferring one from the
13021315
function's signature. Defaults to ``None`` (infer from signature).
13031316
approval_mode: Whether or not approval is required to run this tool.
1304-
Default is that approval is required.
1317+
Default is that approval is NOT required (``"never_require"``).
13051318
max_invocations: The maximum number of times this function can be invoked.
13061319
If None, there is no limit, should be at least 1.
13071320
max_invocation_exceptions: The maximum number of exceptions allowed during invocations.
@@ -1369,6 +1382,20 @@ def get_weather(location: str, unit: str = "celsius") -> str:
13691382
'''Get weather for a location.'''
13701383
return f"Weather in {location}: 22 {unit}"
13711384
1385+
1386+
# Declaration-only tool (no implementation)
1387+
# Use FunctionTool directly when you need a tool declaration without
1388+
# an executable function. The agent can request this tool, but it won't
1389+
# be executed automatically. Useful for testing agent reasoning or when
1390+
# the implementation is handled externally (e.g., client-side rendering).
1391+
from agent_framework import FunctionTool
1392+
1393+
declaration_only_tool = FunctionTool(
1394+
name="get_current_time",
1395+
description="Get the current time in ISO 8601 format.",
1396+
func=None, # Explicitly no implementation - makes declaration_only=True
1397+
)
1398+
13721399
"""
13731400

13741401
def decorator(func: Callable[..., ReturnT | Awaitable[ReturnT]]) -> FunctionTool[Any, ReturnT]:

python/samples/getting_started/middleware/agent_and_run_level_middleware.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,26 @@
3131
3. Run-level context middleware for specific use cases (high priority, debugging)
3232
4. Run-level caching middleware for expensive operations
3333
34-
Execution order: Agent middleware (outermost) -> Run middleware (innermost) -> Agent execution
34+
Agent Middleware Execution Order:
35+
When both agent-level and run-level *agent* middleware are configured, they execute
36+
in this order:
37+
38+
1. Agent-level middleware (outermost) - executes first, in the order they were registered
39+
2. Run-level middleware (innermost) - executes next, in the order they were passed to run()
40+
3. Agent execution - the actual agent logic runs last
41+
42+
For example, with agent middleware [A1, A2] and run middleware [R1, R2]:
43+
Request -> A1 -> A2 -> R1 -> R2 -> Agent -> R2 -> R1 -> A2 -> A1 -> Response
44+
45+
This means:
46+
- Agent middleware wraps ALL run middleware and the agent
47+
- Run middleware wraps only the agent for that specific run
48+
- Each middleware can modify the context before AND after calling next()
49+
50+
Note: Function and chat middleware (e.g., ``function_logging_middleware``) execute
51+
during tool invocation *inside* the agent execution, not in the outer agent-middleware
52+
chain shown above. They follow the same ordering principle: agent-level function/chat
53+
middleware runs before run-level function/chat middleware.
3554
"""
3655

3756

0 commit comments

Comments
 (0)