DEV Community

Cover image for Azure AI Agent Service Part 2: Extending Agents with Tools, Function Calling, and File Search
Brian Spann
Brian Spann

Posted on

Azure AI Agent Service Part 2: Extending Agents with Tools, Function Calling, and File Search

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" }
    })
);
Enter fullscreen mode Exit fullscreen mode

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}");
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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
    });
}
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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:

  1. Call search_products with query="blue wireless keyboard" and maxPrice=100
  2. Call check_inventory for promising results at the downtown store
  3. 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.");
Enter fullscreen mode Exit fullscreen mode

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 }
        }
    }
);
Enter fullscreen mode Exit fullscreen mode

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}\"");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 }
        }
    }
);
Enter fullscreen mode Exit fullscreen mode

Now a customer can ask "I can't log into my account, my email is john@example.com" and the agent might:

  1. Look up the customer account
  2. Check authentication service status
  3. Search documentation for login troubleshooting steps
  4. 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() 
    }
);
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

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"
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
});
Enter fullscreen mode Exit fullscreen mode

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)