Part 4 of the Microsoft Agent Framework Series
In Part 2 and Part 3, we built our first agents with basic tools. Now it’s time to go deeper into the heart of agentic AI: function calling.
Tools transform agents from mere conversationalists into action-takers. They’re the bridge between AI reasoning and real-world outcomes.
Understanding the Tool Invocation Flow
- User sends a message to the agent
- Agent analyzes the request and decides if tools are needed
- Agent selects tool(s) and generates arguments
- Framework executes the tool function
- Result is returned to the agent
- Agent synthesizes the final response
Tool Definition Patterns – Python
The @ai_function decorator automatically generates JSON schemas from your type annotations:
from typing import Annotated, Literal, Optional, List
from pydantic import Field, BaseModel
from agent_framework import ai_function
import aiohttp
# Pattern 1: Simple function with typed parameters
@ai_function
def get_weather(
city: Annotated[str, Field(description="City name")],
units: Annotated[Literal["celsius", "fahrenheit"], Field(description="Temperature unit")] = "celsius"
) -> str:
"""Get current weather for a city."""
weather_data = {
"new york": {"temp": 22, "condition": "Sunny"},
"london": {"temp": 15, "condition": "Cloudy"},
"tokyo": {"temp": 28, "condition": "Humid"}
}
data = weather_data.get(city.lower(), {"temp": 20, "condition": "Unknown"})
temp = data["temp"] if units == "celsius" else (data["temp"] * 9/5) + 32
return f"Weather in {city}: {temp}°{'C' if units == 'celsius' else 'F'}, {data['condition']}"
# Pattern 2: Async function for I/O operations
@ai_function
async def fetch_api_data(
endpoint: Annotated[str, Field(description="API endpoint URL")]
) -> str:
"""Fetch data from an external API."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(endpoint, timeout=10) as response:
if response.status == 200:
return await response.text()
return f"API returned status {response.status}"
except aiohttp.ClientError as e:
return f"API error: {str(e)}"
# Pattern 3: Complex return types with Pydantic
class SearchResult(BaseModel):
title: str
url: str
snippet: str
@ai_function
def search_documents(
query: Annotated[str, Field(description="Search query")],
limit: Annotated[int, Field(description="Max results", ge=1, le=20)] = 10
) -> List[dict]:
"""Search internal document repository."""
results = [
{"title": f"Document about {query}", "url": f"/docs/{i}", "snippet": f"Content related to {query}..."}
for i in range(min(limit, 3))
]
return results
# Pattern 4: Optional parameters with defaults
@ai_function
def send_notification(
message: Annotated[str, Field(description="Message content")],
recipient: Annotated[str, Field(description="Email or user ID")],
priority: Annotated[Optional[str], Field(description="Priority: low, normal, high")] = "normal",
schedule: Annotated[Optional[str], Field(description="ISO datetime to send")] = None
) -> str:
"""Send a notification to a user."""
scheduled_info = f" scheduled for {schedule}" if schedule else ""
return f"Notification ({priority}) queued for {recipient}{scheduled_info}"
Tool Definition Patterns – .NET (C#)
In .NET, use [Description] attributes to document your tools:
using System;
using System.ComponentModel;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;
public class AdvancedTools
{
private static readonly HttpClient _httpClient = new();
// Pattern 1: Simple tool with descriptions
[Description("Get current weather for a location")]
public static string GetWeather(
[Description("City name")] string city,
[Description("Temperature unit: celsius or fahrenheit")] string units = "celsius")
{
var weatherData = new Dictionary<string, (int Temp, string Condition)>
{
["new york"] = (22, "Sunny"),
["london"] = (15, "Cloudy"),
["tokyo"] = (28, "Humid")
};
var data = weatherData.GetValueOrDefault(city.ToLower(), (20, "Unknown"));
var temp = units == "fahrenheit" ? (data.Temp * 9 / 5) + 32 : data.Temp;
var unit = units == "fahrenheit" ? "F" : "C";
return $"Weather in {city}: {temp}°{unit}, {data.Condition}";
}
// Pattern 2: Async tool for external calls
[Description("Fetch data from an external REST API")]
public static async Task<string> FetchApiDataAsync(
[Description("Full API endpoint URL")] string endpoint)
{
try
{
var response = await _httpClient.GetAsync(endpoint);
if (response.IsSuccessStatusCode)
return await response.Content.ReadAsStringAsync();
return $"API returned status {(int)response.StatusCode}";
}
catch (HttpRequestException ex)
{
return $"API error: {ex.Message}";
}
}
// Pattern 3: Complex operations with validation
[Description("Create a support ticket in the system")]
public static string CreateTicket(
[Description("Customer email address")] string email,
[Description("Issue description")] string description,
[Description("Priority: Low, Medium, High, Critical")] string priority = "Medium")
{
if (!email.Contains("@"))
return "Error: Invalid email format.";
if (string.IsNullOrWhiteSpace(description))
return "Error: Description cannot be empty.";
var ticketId = $"TKT-{DateTime.Now:yyyyMMddHHmmss}";
return $"Created ticket {ticketId} for {email} with priority {priority}";
}
// Pattern 4: Search with pagination
[Description("Search documents in the repository")]
public static string SearchDocuments(
[Description("Search query")] string query,
[Description("Maximum results (1-20)")] int limit = 10)
{
limit = Math.Clamp(limit, 1, 20);
var results = new List<string>();
for (int i = 1; i <= Math.Min(limit, 3); i++)
{
results.Add($"{i}. Document about {query} - /docs/{i}");
}
return $"Found {results.Count} results:\n" + string.Join("\n", results);
}
}
MCP Integration – Python
The Model Context Protocol (MCP) enables dynamic tool discovery from external servers:
import asyncio
import os
from agent_framework.mcp import MCPClient, MCPServerConfig
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
async def main():
# Configure MCP server connection
mcp_config = MCPServerConfig(
url="http://localhost:8080",
auth_token=os.getenv("MCP_TOKEN"),
timeout=30
)
# Create MCP client
mcp_client = MCPClient(config=mcp_config)
# Discover available tools
tools = await mcp_client.list_tools()
print(f"Available MCP tools: {[t.name for t in tools]}")
# Create agent with MCP tools
agent = AzureOpenAIResponsesClient(
credential=AzureCliCredential()
).create_agent(
name="MCPAgent",
instructions="You can use external tools via MCP to help users.",
mcp_clients=[mcp_client] # Auto-discovers and registers tools
)
# Agent can now use MCP tools
result = await agent.run("Search for recent issues in the GitHub repo")
print(f"Result: {result.text}")
if __name__ == "__main__":
asyncio.run(main())
MCP Integration – .NET (C#)
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.MCP;
using Azure.Identity;
// Configure MCP connection
var mcpConfig = new MCPServerConfig
{
Url = "http://localhost:8080",
AuthToken = Environment.GetEnvironmentVariable("MCP_TOKEN"),
TimeoutSeconds = 30
};
var mcpClient = new MCPClient(mcpConfig);
// Discover tools
var tools = await mcpClient.ListToolsAsync();
Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}");
// Create agent with MCP
var agent = new AzureOpenAIClient(
new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!),
new DefaultAzureCredential())
.GetOpenAIResponseClient("gpt-4o")
.CreateAIAgent(
name: "MCPAgent",
instructions: "You can use external tools via MCP.",
mcpClients: new[] { mcpClient });
// Use MCP tools
var result = await agent.RunAsync("Search for recent GitHub issues");
Console.WriteLine(result);
External API Integration – Python
import os
import aiohttp
from typing import Annotated
from pydantic import Field
from agent_framework import ai_function
class CRMTools:
"""Tools for CRM integration."""
def __init__(self, api_key: str, base_url: str):
self.api_key = api_key
self.base_url = base_url
@ai_function
async def get_customer(
self,
customer_id: Annotated[str, Field(description="Customer ID")]
) -> str:
"""Retrieve customer information from CRM."""
async with aiohttp.ClientSession() as session:
headers = {"Authorization": f"Bearer {self.api_key}"}
try:
async with session.get(
f"{self.base_url}/customers/{customer_id}",
headers=headers,
timeout=10
) as response:
if response.status == 200:
data = await response.json()
return f"""Customer Found:
- ID: {data.get('id')}
- Name: {data.get('name')}
- Email: {data.get('email')}
- Status: {data.get('status')}"""
elif response.status == 404:
return f"Customer {customer_id} not found."
else:
return f"API error: status {response.status}"
except aiohttp.ClientError as e:
return f"Connection error: {str(e)}"
@ai_function
async def create_order(
self,
customer_id: Annotated[str, Field(description="Customer ID")],
product_ids: Annotated[str, Field(description="Comma-separated product IDs")],
quantity: Annotated[int, Field(description="Quantity")] = 1
) -> str:
"""Create a new order in the system."""
async with aiohttp.ClientSession() as session:
payload = {
"customer_id": customer_id,
"products": product_ids.split(","),
"quantity": quantity
}
try:
async with session.post(
f"{self.base_url}/orders",
json=payload,
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=10
) as response:
if response.status == 201:
data = await response.json()
return f"Order created! Order ID: {data.get('order_id')}"
else:
error = await response.text()
return f"Failed to create order: {error}"
except aiohttp.ClientError as e:
return f"Connection error: {str(e)}"
# Usage
crm_tools = CRMTools(
api_key=os.getenv("CRM_API_KEY"),
base_url="https://api.example.com"
)
agent = client.create_agent(
name="OrderAgent",
instructions="Help customers with orders using the CRM tools.",
tools=[crm_tools.get_customer, crm_tools.create_order]
)
External API Integration – .NET (C#)
using System;
using System.ComponentModel;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
public class CRMTools
{
private readonly HttpClient _client;
private readonly string _baseUrl;
public CRMTools(string apiKey, string baseUrl)
{
_baseUrl = baseUrl;
_client = new HttpClient();
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
_client.Timeout = TimeSpan.FromSeconds(10);
}
[Description("Retrieve customer information from CRM")]
public async Task<string> GetCustomerAsync(
[Description("Customer ID")] string customerId)
{
try
{
var response = await _client.GetAsync($"{_baseUrl}/customers/{customerId}");
if (response.IsSuccessStatusCode)
{
var customer = await response.Content.ReadFromJsonAsync<CustomerDto>();
return $@"Customer Found:
- ID: {customer?.Id}
- Name: {customer?.Name}
- Email: {customer?.Email}
- Status: {customer?.Status}";
}
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
return $"Customer {customerId} not found.";
return $"API error: {(int)response.StatusCode}";
}
catch (HttpRequestException ex)
{
return $"Connection error: {ex.Message}";
}
}
[Description("Create a new order in the system")]
public async Task<string> CreateOrderAsync(
[Description("Customer ID")] string customerId,
[Description("Comma-separated product IDs")] string productIds,
[Description("Quantity")] int quantity = 1)
{
try
{
var order = new
{
CustomerId = customerId,
Products = productIds.Split(','),
Quantity = quantity
};
var response = await _client.PostAsJsonAsync($"{_baseUrl}/orders", order);
if (response.StatusCode == System.Net.HttpStatusCode.Created)
{
var result = await response.Content.ReadFromJsonAsync<OrderResponseDto>();
return $"Order created! Order ID: {result?.OrderId}";
}
var error = await response.Content.ReadAsStringAsync();
return $"Failed to create order: {error}";
}
catch (HttpRequestException ex)
{
return $"Connection error: {ex.Message}";
}
}
}
public record CustomerDto(string Id, string Name, string Email, string Status);
public record OrderResponseDto(string OrderId);
// Usage
var crmTools = new CRMTools(
Environment.GetEnvironmentVariable("CRM_API_KEY")!,
"https://api.example.com"
);
var agent = client.CreateAIAgent(
name: "OrderAgent",
instructions: "Help customers with orders.",
tools: new object[] { crmTools.GetCustomerAsync, crmTools.CreateOrderAsync });
Error Handling – Python
Tools should never raise exceptions. Return informative error messages instead:
import aiohttp
from typing import Annotated
from pydantic import Field
from agent_framework import ai_function
@ai_function
async def safe_api_call(
endpoint: Annotated[str, Field(description="API endpoint URL")]
) -> str:
"""Make a safe API call with comprehensive error handling."""
# Validate input
if not endpoint.startswith(("http://", "https://")):
return "Error: Invalid endpoint. URL must start with http:// or https://"
try:
timeout = aiohttp.ClientTimeout(total=10)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(endpoint) as response:
if response.status == 200:
content = await response.text()
if len(content) > 1000:
return content[:1000] + "... (truncated)"
return content
elif response.status == 404:
return "Error: Resource not found. Please check the URL."
elif response.status == 401:
return "Error: Authentication required."
elif response.status == 403:
return "Error: Access forbidden."
elif response.status == 429:
return "Error: Rate limit exceeded. Try again later."
elif response.status >= 500:
return f"Error: Server error ({response.status})."
else:
return f"Error: Unexpected status code {response.status}"
except aiohttp.ClientTimeout:
return "Error: Request timed out."
except aiohttp.ClientConnectionError:
return "Error: Could not connect to server."
except aiohttp.ClientError as e:
return f"Error: Network error - {str(e)}"
except Exception as e:
return f"Error: Unexpected error - {str(e)}"
Error Handling – .NET (C#)
using System;
using System.ComponentModel;
using System.Net.Http;
using System.Threading.Tasks;
public class SafeApiTools
{
private static readonly HttpClient _client = new()
{
Timeout = TimeSpan.FromSeconds(10)
};
[Description("Make a safe API call with comprehensive error handling")]
public static async Task<string> SafeApiCallAsync(
[Description("API endpoint URL")] string endpoint)
{
// Validate input
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri))
return "Error: Invalid URL format.";
if (uri.Scheme != "http" && uri.Scheme != "https")
return "Error: URL must use http or https protocol.";
try
{
var response = await _client.GetAsync(endpoint);
return response.StatusCode switch
{
System.Net.HttpStatusCode.OK => await GetTruncatedContent(response),
System.Net.HttpStatusCode.NotFound => "Error: Resource not found.",
System.Net.HttpStatusCode.Unauthorized => "Error: Authentication required.",
System.Net.HttpStatusCode.Forbidden => "Error: Access forbidden.",
System.Net.HttpStatusCode.TooManyRequests => "Error: Rate limit exceeded.",
>= System.Net.HttpStatusCode.InternalServerError =>
$"Error: Server error ({(int)response.StatusCode}).",
_ => $"Error: Unexpected status {(int)response.StatusCode}"
};
}
catch (TaskCanceledException)
{
return "Error: Request timed out.";
}
catch (HttpRequestException ex)
{
return $"Error: Connection failed - {ex.Message}";
}
catch (Exception ex)
{
return $"Error: Unexpected error - {ex.Message}";
}
}
private static async Task<string> GetTruncatedContent(HttpResponseMessage response)
{
var content = await response.Content.ReadAsStringAsync();
if (content.Length > 1000)
return content[..1000] + "... (truncated)";
return content;
}
}
Tool Chaining – Python
Design tools that work well together for complex workflows:
from typing import Annotated
from pydantic import Field
from agent_framework import ai_function
@ai_function
def search_products(
query: Annotated[str, Field(description="Product search query")]
) -> str:
"""Search for products. Returns a list of product IDs."""
products = {
"laptop": ["PROD-001", "PROD-002", "PROD-003"],
"phone": ["PROD-101", "PROD-102"],
"tablet": ["PROD-201", "PROD-202"]
}
for key, ids in products.items():
if key in query.lower():
return f"Found products: {', '.join(ids)}"
return "No products found for your search."
@ai_function
def get_product_details(
product_id: Annotated[str, Field(description="Product ID from search")]
) -> str:
"""Get detailed information for a specific product."""
products = {
"PROD-001": {"name": "ProBook Laptop 15", "price": 999.99, "stock": 50},
"PROD-002": {"name": "UltraBook Air", "price": 1299.99, "stock": 25},
"PROD-003": {"name": "WorkStation Pro", "price": 1899.99, "stock": 10},
"PROD-101": {"name": "SmartPhone X", "price": 799.99, "stock": 100},
"PROD-102": {"name": "SmartPhone Pro", "price": 999.99, "stock": 75},
}
product = products.get(product_id.upper())
if product:
return f"""Product Details:
- ID: {product_id}
- Name: {product['name']}
- Price: ${product['price']:.2f}
- Stock: {product['stock']} units"""
return f"Product {product_id} not found."
@ai_function
def add_to_cart(
product_id: Annotated[str, Field(description="Product ID to add")],
quantity: Annotated[int, Field(description="Quantity to add")] = 1
) -> str:
"""Add a product to the shopping cart."""
if quantity < 1:
return "Error: Quantity must be at least 1."
if quantity > 10:
return "Error: Maximum 10 items per product."
return f"Added {quantity}x {product_id} to cart successfully."
# Agent can chain: search -> get details -> add to cart
agent = client.create_agent(
name="ShoppingAssistant",
instructions="Help users find and purchase products. Use tools in sequence.",
tools=[search_products, get_product_details, add_to_cart]
)
Tool Chaining – .NET (C#)
using System;
using System.ComponentModel;
using System.Collections.Generic;
public class ShoppingTools
{
private static readonly Dictionary<string, List<string>> ProductSearch = new()
{
["laptop"] = new() { "PROD-001", "PROD-002", "PROD-003" },
["phone"] = new() { "PROD-101", "PROD-102" },
["tablet"] = new() { "PROD-201", "PROD-202" }
};
private static readonly Dictionary<string, (string Name, decimal Price, int Stock)> Products = new()
{
["PROD-001"] = ("ProBook Laptop 15", 999.99m, 50),
["PROD-002"] = ("UltraBook Air", 1299.99m, 25),
["PROD-003"] = ("WorkStation Pro", 1899.99m, 10),
["PROD-101"] = ("SmartPhone X", 799.99m, 100),
["PROD-102"] = ("SmartPhone Pro", 999.99m, 75)
};
[Description("Search for products by query. Returns product IDs.")]
public static string SearchProducts(
[Description("Product search query")] string query)
{
foreach (var (key, ids) in ProductSearch)
{
if (query.ToLower().Contains(key))
return $"Found products: {string.Join(", ", ids)}";
}
return "No products found.";
}
[Description("Get detailed information for a product.")]
public static string GetProductDetails(
[Description("Product ID")] string productId)
{
if (Products.TryGetValue(productId.ToUpper(), out var product))
{
return $@"Product Details:
- ID: {productId}
- Name: {product.Name}
- Price: ${product.Price:F2}
- Stock: {product.Stock} units";
}
return $"Product {productId} not found.";
}
[Description("Add a product to the shopping cart.")]
public static string AddToCart(
[Description("Product ID")] string productId,
[Description("Quantity")] int quantity = 1)
{
if (quantity < 1) return "Error: Quantity must be at least 1.";
if (quantity > 10) return "Error: Maximum 10 items per product.";
return $"Added {quantity}x {productId} to cart.";
}
}
// Usage
var agent = client.CreateAIAgent(
name: "ShoppingAssistant",
instructions: "Help users find and purchase products.",
tools: new object[] {
ShoppingTools.SearchProducts,
ShoppingTools.GetProductDetails,
ShoppingTools.AddToCart
});
Key Takeaways
- Tools are the bridge between AI reasoning and real-world actions
- Use clear descriptions and type annotations — they become the LLM’s documentation
- Never raise exceptions in tools — return informative error messages
- MCP provides dynamic tool discovery and vendor-neutral integration
- Design tools to chain together for complex workflows
- Use async patterns for I/O-bound operations
📦 Source Code
All code examples from this article series are available on GitHub:
👉 https://github.com/nithinmohantk/microsoft-agent-framework-series-examples
Clone the repository to follow along:
git clone https://github.com/nithinmohantk/microsoft-agent-framework-series-examples.git
cd microsoft-agent-framework-series-examples
Series Navigation
- Part 1: Introduction
- Part 2: First Agent (.NET)
- Part 3: First Agent (Python)
- Part 4: Tools & Function Calling ← You are here
- Part 5: Multi-Turn Conversations
References
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.