Extending Agents with Tools, Function Calling, and File Search
Part 2 of 5: Building Intelligent Agents with Azure AI Agent Service
Welcome back to our series on Azure AI Agent Service! In Part 1, we built our first agent and got it responding to basic queries. Now it's time to give our agent superpowers—the ability to call functions, use tools, and search through documents.
Why Tools Matter
A language model on its own is impressive, but limited. It can only work with what it learned during training—no real-time data, no access to your systems, no ability to take action. Tools change everything.
With Azure AI Agent Service, you can equip your agents with:
- Function calling - Let the agent invoke your C# methods
- Code Interpreter - Execute Python code for calculations and data analysis
- File Search - Query documents using vector search and RAG patterns
- Custom tools - Integrate any API or service you need
Let's dive into each of these, with real code you can use today.
Function Calling: Teaching Agents to Use Your Code
Function calling is where agents become truly useful. Instead of just generating text, your agent can decide when to call specific functions and use the results in its responses.
Defining a Function Tool
First, let's define a function our agent can call. We'll create a weather lookup tool:
using Azure.AI.Projects;
// Define the function schema
var getWeatherTool = new FunctionToolDefinition(
name: "get_current_weather",
description: "Gets the current weather for a specified city",
parameters: BinaryData.FromObjectAsJson(new
{
type = "object",
properties = new
{
city = new
{
type = "string",
description = "The city name, e.g., 'Seattle' or 'London'"
},
units = new
{
type = "string",
@enum = new[] { "celsius", "fahrenheit" },
description = "Temperature units"
}
},
required = new[] { "city" }
})
);
The schema uses JSON Schema format. Be descriptive—the agent uses these descriptions to decide when and how to call your function.
Creating an Agent with Tools
Now let's create an agent equipped with our weather tool:
using Azure.AI.Projects;
using Azure.Identity;
var connectionString = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_CONNECTION");
var client = new AgentsClient(connectionString, new DefaultAzureCredential());
// Create the agent with our tool
var agent = await client.CreateAgentAsync(
model: "gpt-4o",
name: "WeatherAssistant",
instructions: "You are a helpful weather assistant. Use the get_current_weather " +
"function when users ask about weather conditions.",
tools: new List<ToolDefinition> { getWeatherTool }
);
Console.WriteLine($"Created agent: {agent.Value.Id}");
Handling Function Calls
When the agent decides to call a function, you need to execute it and return the results. Here's the complete flow:
// Create a thread and send a message
var thread = await client.CreateThreadAsync();
await client.CreateMessageAsync(
thread.Value.Id,
MessageRole.User,
"What's the weather like in Seattle right now?"
);
// Start a run
var run = await client.CreateRunAsync(thread.Value.Id, agent.Value.Id);
// Poll until the run needs action or completes
while (run.Value.Status == RunStatus.Queued ||
run.Value.Status == RunStatus.InProgress ||
run.Value.Status == RunStatus.RequiresAction)
{
await Task.Delay(500);
run = await client.GetRunAsync(thread.Value.Id, run.Value.Id);
// Check if the agent wants to call a function
if (run.Value.Status == RunStatus.RequiresAction)
{
var toolCalls = run.Value.RequiredAction.SubmitToolOutputs.ToolCalls;
var toolOutputs = new List<ToolOutput>();
foreach (var toolCall in toolCalls)
{
if (toolCall is RequiredFunctionToolCall functionCall)
{
var result = await ExecuteFunction(
functionCall.Name,
functionCall.Arguments
);
toolOutputs.Add(new ToolOutput(functionCall.Id, result));
}
}
// Submit the results back to the agent
run = await client.SubmitToolOutputsToRunAsync(
thread.Value.Id,
run.Value.Id,
toolOutputs
);
}
}
// Get the final response
var messages = await client.GetMessagesAsync(thread.Value.Id);
var response = messages.Value.Data
.Where(m => m.Role == MessageRole.Assistant)
.OrderByDescending(m => m.CreatedAt)
.First();
Console.WriteLine(response.Content[0]);
Implementing the Function Handler
Here's a clean pattern for executing functions:
private async Task<string> ExecuteFunction(string name, string argumentsJson)
{
var arguments = JsonDocument.Parse(argumentsJson);
return name switch
{
"get_current_weather" => await GetWeatherAsync(arguments),
"search_products" => await SearchProductsAsync(arguments),
"create_ticket" => await CreateTicketAsync(arguments),
_ => JsonSerializer.Serialize(new { error = $"Unknown function: {name}" })
};
}
private async Task<string> GetWeatherAsync(JsonDocument args)
{
var city = args.RootElement.GetProperty("city").GetString();
var units = args.RootElement.TryGetProperty("units", out var u)
? u.GetString()
: "celsius";
// Call your actual weather API here
var weather = await _weatherService.GetCurrentAsync(city, units);
return JsonSerializer.Serialize(new
{
city,
temperature = weather.Temperature,
units,
conditions = weather.Conditions,
humidity = weather.Humidity
});
}
Pro tip: Always return JSON from your functions. It gives the agent structured data to work with, resulting in better responses.
Multiple Tools and Complex Workflows
Real agents often need multiple tools working together. Here's an example of an e-commerce assistant:
var tools = new List<ToolDefinition>
{
new FunctionToolDefinition(
name: "search_products",
description: "Search the product catalog by query, category, or price range",
parameters: BinaryData.FromObjectAsJson(new
{
type = "object",
properties = new
{
query = new { type = "string", description = "Search terms" },
category = new { type = "string" },
maxPrice = new { type = "number" },
minPrice = new { type = "number" }
},
required = new[] { "query" }
})
),
new FunctionToolDefinition(
name: "get_product_details",
description: "Get detailed information about a specific product",
parameters: BinaryData.FromObjectAsJson(new
{
type = "object",
properties = new
{
productId = new { type = "string", description = "The product SKU" }
},
required = new[] { "productId" }
})
),
new FunctionToolDefinition(
name: "check_inventory",
description: "Check real-time inventory for a product at a specific store",
parameters: BinaryData.FromObjectAsJson(new
{
type = "object",
properties = new
{
productId = new { type = "string" },
storeId = new { type = "string" }
},
required = new[] { "productId" }
})
),
new FunctionToolDefinition(
name: "add_to_cart",
description: "Add a product to the customer's shopping cart",
parameters: BinaryData.FromObjectAsJson(new
{
type = "object",
properties = new
{
productId = new { type = "string" },
quantity = new { type = "integer", minimum = 1 }
},
required = new[] { "productId", "quantity" }
})
)
};
var agent = await client.CreateAgentAsync(
model: "gpt-4o",
name: "ShoppingAssistant",
instructions: """
You are a helpful shopping assistant. Help customers find products,
check availability, and add items to their cart. Always confirm
before adding items. Be friendly and conversational.
""",
tools: tools
);
When a user says "I'm looking for a blue wireless keyboard under $100, do you have any in stock at the downtown store?", the agent might:
- Call
search_productswith query="blue wireless keyboard" and maxPrice=100 - Call
check_inventoryfor promising results at the downtown store - Present options with availability info
The magic is that the agent figures out this workflow on its own—you just provide the tools.
File Search: RAG Without the Infrastructure
File Search is Azure AI Agent Service's built-in RAG (Retrieval Augmented Generation) capability. Upload documents, and your agent can search and cite them—no vector database setup required.
Creating a Vector Store
First, upload your documents to a vector store:
// Create a vector store
var vectorStore = await client.CreateVectorStoreAsync(
name: "ProductDocs",
expiresAfter: new VectorStoreExpirationPolicy(
anchor: VectorStoreExpirationPolicyAnchor.LastActiveAt,
days: 30
)
);
Console.WriteLine($"Vector store created: {vectorStore.Value.Id}");
// Upload files to the vector store
var filePaths = new[]
{
"docs/product-manual.pdf",
"docs/troubleshooting-guide.pdf",
"docs/faq.md"
};
foreach (var path in filePaths)
{
using var stream = File.OpenRead(path);
var file = await client.UploadFileAsync(
stream,
AgentFilePurpose.Agents,
Path.GetFileName(path)
);
await client.CreateVectorStoreFileAsync(
vectorStore.Value.Id,
file.Value.Id
);
Console.WriteLine($"Uploaded: {path}");
}
// Wait for processing to complete
VectorStore store;
do
{
await Task.Delay(1000);
store = (await client.GetVectorStoreAsync(vectorStore.Value.Id)).Value;
}
while (store.Status == VectorStoreStatus.InProgress);
Console.WriteLine($"Vector store ready. {store.FileCounts.Completed} files processed.");
Creating an Agent with File Search
Now create an agent that can search these documents:
var fileSearchTool = new FileSearchToolDefinition();
var agent = await client.CreateAgentAsync(
model: "gpt-4o",
name: "SupportAgent",
instructions: """
You are a technical support agent. Answer customer questions using
the product documentation. Always cite your sources when referencing
specific information from the docs. If you can't find relevant
information, say so clearly.
""",
tools: new List<ToolDefinition> { fileSearchTool },
toolResources: new ToolResources
{
FileSearch = new FileSearchToolResource
{
VectorStoreIds = { vectorStore.Value.Id }
}
}
);
Handling Citations
File Search responses include citations. Here's how to extract them:
var messages = await client.GetMessagesAsync(thread.Value.Id);
var assistantMessage = messages.Value.Data
.First(m => m.Role == MessageRole.Assistant);
foreach (var content in assistantMessage.Content)
{
if (content is MessageTextContent textContent)
{
Console.WriteLine(textContent.Text);
// Extract citations
foreach (var annotation in textContent.Annotations)
{
if (annotation is MessageTextFileCitationAnnotation citation)
{
Console.WriteLine($"\n📄 Citation: {citation.Text}");
Console.WriteLine($" File ID: {citation.FileId}");
Console.WriteLine($" Quote: \"{citation.FileCitation.Quote}\"");
}
}
}
}
Combining Tools: The Power Move
The real magic happens when you combine function calling with file search. Here's a support agent that can both search documentation AND access live system data:
var tools = new List<ToolDefinition>
{
new FileSearchToolDefinition(),
new FunctionToolDefinition(
name: "get_customer_account",
description: "Retrieve customer account details by email or account ID",
parameters: BinaryData.FromObjectAsJson(new
{
type = "object",
properties = new
{
email = new { type = "string" },
accountId = new { type = "string" }
}
})
),
new FunctionToolDefinition(
name: "check_service_status",
description: "Check current status of a service or feature",
parameters: BinaryData.FromObjectAsJson(new
{
type = "object",
properties = new
{
serviceName = new { type = "string" }
},
required = new[] { "serviceName" }
})
),
new FunctionToolDefinition(
name: "create_support_ticket",
description: "Create a support ticket for issues that need escalation",
parameters: BinaryData.FromObjectAsJson(new
{
type = "object",
properties = new
{
customerId = new { type = "string" },
priority = new { type = "string", @enum = new[] { "low", "medium", "high" } },
summary = new { type = "string" },
details = new { type = "string" }
},
required = new[] { "customerId", "priority", "summary" }
})
)
};
var agent = await client.CreateAgentAsync(
model: "gpt-4o",
name: "CustomerSupportAgent",
instructions: """
You are a customer support agent with access to:
- Product documentation (use file search for how-to questions)
- Customer account system (to look up account details)
- Service status (to check for outages)
- Ticket system (to escalate complex issues)
Start by understanding the customer's issue. Search documentation
for common questions. Check service status if they report problems.
Create tickets for issues you can't resolve directly.
Be empathetic, clear, and efficient.
""",
tools: tools,
toolResources: new ToolResources
{
FileSearch = new FileSearchToolResource
{
VectorStoreIds = { vectorStore.Value.Id }
}
}
);
Now a customer can ask "I can't log into my account, my email is john@example.com" and the agent might:
- Look up the customer account
- Check authentication service status
- Search documentation for login troubleshooting steps
- If needed, create a support ticket with all the context
All from a single natural language request.
Code Interpreter: When You Need Computation
Azure AI Agent Service also includes Code Interpreter—a sandboxed Python environment the agent can use for calculations, data analysis, and generating visualizations.
var agent = await client.CreateAgentAsync(
model: "gpt-4o",
name: "DataAnalyst",
instructions: "You are a data analyst. Use code interpreter to analyze " +
"data, perform calculations, and create visualizations.",
tools: new List<ToolDefinition>
{
new CodeInterpreterToolDefinition()
}
);
Upload a CSV file and ask "What's the average order value by region? Show me a chart."—the agent will write Python code, execute it, and return both the analysis and a generated chart image.
Best Practices for Tools
After building many agent tools, here's what I've learned:
1. Write Clear Descriptions
The agent uses your descriptions to decide when to call functions. Be specific:
// ❌ Bad
description: "Gets data"
// ✅ Good
description: "Retrieves order history for a customer, including order dates, " +
"items, totals, and shipping status. Use when customers ask about " +
"past orders or need to track shipments."
2. Handle Errors Gracefully
Return error information the agent can use:
private async Task<string> GetOrderAsync(string orderId)
{
try
{
var order = await _orderService.GetAsync(orderId);
return JsonSerializer.Serialize(order);
}
catch (OrderNotFoundException)
{
return JsonSerializer.Serialize(new
{
error = "Order not found",
suggestion = "Please verify the order ID and try again"
});
}
}
3. Keep Functions Focused
Single-purpose functions work better than Swiss Army knives. The agent can call multiple functions when needed.
4. Include Relevant Context in Results
The more context you return, the better the agent's response:
// ❌ Minimal
return JsonSerializer.Serialize(new { status = "shipped" });
// ✅ Contextual
return JsonSerializer.Serialize(new
{
status = "shipped",
carrier = "FedEx",
trackingNumber = "123456789",
estimatedDelivery = "2024-01-15",
trackingUrl = "https://fedex.com/track/123456789"
});
What's Next
We've covered a lot of ground—function calling, file search, and combining tools for powerful agents. In Part 3, we'll explore streaming responses and real-time interactions, making our agents feel snappy and responsive.
We'll cover:
- Streaming agent responses token-by-token
- Handling tool calls in streaming mode
- Building responsive UIs with server-sent events
- Real-time progress updates for long-running operations
Until then, experiment with the tools we've covered. The combination of custom functions and file search opens up endless possibilities.
Have questions or want to share what you're building? Drop a comment below. Happy coding!
Series Navigation:
- Part 1: Introduction to Azure AI Agent Service
- Part 2: Extending Agents with Tools, Function Calling, and File Search (You are here)
- Part 3: Streaming Responses and Real-Time Interactions (Coming soon)
- Part 4: Multi-Agent Orchestration Patterns (Coming soon)
- Part 5: Production Deployment and Best Practices (Coming soon)
Top comments (0)