How to Implement Function Tools with Automatic JSON Schema Generation in the Agent Framework
The Microsoft Agent Framework converts ordinary Python functions into LLM-callable tools by automatically generating JSON schemas from type hints using the @tool decorator and FunctionTool class.
The microsoft/agent-framework repository provides a robust system for building function tools with automatic JSON schema generation, eliminating the need to manually define parameter schemas for large language model (LLM) interactions. By leveraging Python type inspection and Pydantic model creation, the framework transforms annotated functions into validated, executable tools that any compatible chat provider can invoke. This approach ensures type safety at runtime while maintaining a clean, decorator-based developer experience.
How Automatic JSON Schema Generation Works
The framework implements a six-stage pipeline in python/packages/core/agent_framework/_tools.py that bridges Python functions and LLM-compatible schemas.
Input Model Discovery via Type Inspection
When you decorate a function with @tool, the framework calls _resolve_input_model to determine how to validate incoming arguments. If you provide a Pydantic model via the schema parameter, the framework uses it verbatim. Otherwise, inspect.signature extracts parameter names, type hints, and default values from the function definition. The _parse_annotation helper then converts Annotated[T, "description"] markers into Pydantic field definitions.
from agent_framework import tool
@tool()
def calculate_area(length: float, width: float = 1.0) -> float:
"""Calculate rectangle area."""
return length * width
In this example, the framework dynamically constructs a Pydantic model equivalent to create_model("calculate_area_input", length=(float, ...), width=(float, 1.0)).
JSON Schema Construction
The _input_schema property lazily generates the JSON schema by calling self.input_model.model_json_schema() on the discovered Pydantic model. This result is cached on first use to avoid repeated computation. The resulting dictionary conforms to JSON Schema standards and includes property types, descriptions, and required field markers derived from your function signature.
Runtime Validation and Invocation
When an LLM requests a tool execution, the invoke method validates arguments against the cached schema using _validate_arguments_against_schema. This ensures type safety before your function executes. The framework also checks invocation limits, approval modes, and handles FunctionInvocationContext injection if your signature includes a context parameter.
Creating Function Tools: Implementation Patterns
Basic Synchronous Tool with the @tool Decorator
The simplest pattern uses the @tool decorator with type-hinted parameters. In python/packages/core/agent_framework/_tools.py, the decorator overloads handle both direct function wrapping and configuration via keyword arguments.
from agent_framework import tool
@tool(approval_mode="never_require")
def add_numbers(
a: int,
b: int,
description: str = "sum"
) -> int:
"""Add two numbers and return the sum."""
return a + b
The framework automatically builds the input model, generates the JSON schema, and exposes add_numbers.to_json_schema_spec() for LLM consumption.
Asynchronous Tools with Context Injection
For async operations requiring runtime metadata, add a FunctionInvocationContext parameter. The _discover_injected_parameters method detects context parameters by name or annotation, allowing access to request metadata, authentication tokens, or tracing IDs.
import asyncio
from agent_framework import tool, FunctionInvocationContext
@tool()
async def fetch_weather(
location: str,
ctx: FunctionInvocationContext,
) -> str:
"""Simulate an async API call with context access."""
# Access ctx.kwargs for runtime data
await asyncio.sleep(0.1)
return f"Sunny in {location}"
Explicit Schema with Pydantic Models
When you need fine-grained control over field descriptions or validation rules, pass a Pydantic model to the schema parameter. This bypasses automatic signature inspection while maintaining the same runtime validation pipeline.
from pydantic import BaseModel, Field
from agent_framework import tool
class WeatherInput(BaseModel):
city: str = Field(..., description="City name")
unit: str = Field("celsius", description="Temperature unit")
@tool(schema=WeatherInput)
def get_weather(city: str, unit: str = "celsius") -> str:
return f"Weather in {city}: 22°{unit[0].upper()}"
Declaration-Only Tools
For UI-only actions or tools implemented externally, create a FunctionTool instance with func=None. The declaration_only property identifies these non-executable specifications, allowing the LLM to request them while the framework rejects actual execution attempts.
from agent_framework import FunctionTool
weather_declaration = FunctionTool(
name="weather_query",
description="Ask the user for weather information",
input_model={"type": "object", "properties": {"city": {"type": "string"}}},
approval_mode="never_require",
)
Integrating with LLM Providers
The to_json_schema_spec() method returns an OpenAI-compatible function specification dictionary with type: "function", name, description, and parameters keys. Use the internal _tools_to_dict helper to convert multiple tools for API consumption.
from openai import OpenAI
from agent_framework import tool, _tools_to_dict
client = OpenAI()
messages = [{"role": "user", "content": "Add 3 and 5"}]
@tool()
def add_numbers(a: int, b: int) -> int:
return a + b
# Convert to LLM-compatible format
specs = _tools_to_dict([add_numbers])
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=specs,
tool_choice="auto",
)
Summary
- Automatic schema generation eliminates manual JSON Schema writing by inspecting Python type hints and building Pydantic models on-the-fly in
_resolve_input_model. - The
@tooldecorator handles both sync and async functions, with support for approval modes, invocation limits, and context injection viaFunctionInvocationContext. - Runtime validation ensures LLM-provided arguments match the generated schema before your function executes via
_validate_arguments_against_schema. - Declaration-only tools allow registering UI actions or external tools using
FunctionToolwithfunc=None. - Provider compatibility is achieved through
to_json_schema_spec(), producing OpenAI-style specifications without provider-specific lock-in.
Frequently Asked Questions
How does the framework handle complex type annotations like List or Optional?
The _parse_annotation method in python/packages/core/agent_framework/_tools.py recursively processes nested types, converting List[str], Optional[int], and other generic aliases into corresponding Pydantic field definitions. Union types become anyOf schemas in the final JSON output, ensuring complex signatures serialize correctly for LLM consumption.
Can I customize how return values are formatted for the LLM?
Yes. The result_parser parameter in FunctionTool accepts a callable that transforms your function's return value into list[Content] objects. If not provided, the default parse_result method handles common types like strings, numbers, and Pydantic models automatically.
What happens if I don't provide type hints for function parameters?
Without type hints, the framework treats parameters as Any type in the generated Pydantic model. While this still produces a valid JSON schema, you lose runtime type validation and LLMs receive less guidance about expected input formats. Best practice requires explicit Annotated types or Pydantic models for production tools.
How do invocation limits and approval modes work?
The max_invocations and max_invocation_exceptions parameters enforce per-instance execution caps, while approval_mode="always_require" forces user confirmation before the invoke method executes the wrapped function. These checks occur in the __call__ and invoke methods of FunctionTool, providing safety guardrails for sensitive operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →