Microsoft Agent Framework

Azure AI Foundry

Oleksii Nikiforov

  • Lead Software Engineer at EPAM Systems
  • AI Engineering Coach
  • +10 years in software development
  • Open Source and Blogging

nikiforovall
Oleksii Nikiforov
nikiforovall.blog

Agenda

  1. Why Foundry? — From plain Azure OpenAI to managed agents
  2. First Foundry AgentAIProjectClient + versioning
  3. Observability — OpenTelemetry + Aspire + Foundry traces
  4. Function Tools — Same pattern, server-side management
  5. Hosted Tools — Code Interpreter, Web Search
  6. RAG via Foundry — File upload, vector stores, file search
  7. Evaluations — Quality, safety, and self-reflection

Demo

A quick tour of Azure AI Foundry

Why Foundry?

Managed platform for building, deploying, and monitoring AI agents at scale

Azure OpenAI vs MAF + Azure AI Foundry

MAF Azure AI Foundry
Schema Versioning - Definition stored Azure-side (named + versioned)
Tools Server-side AIFunction Server-side + hosted (Code, Search, Web)
Memory InMemoryChatHistoryProvider Managed Memory Stores
RAG Build your own Hosted vector stores + file search tool
Evaluation Build your own Built-in quality + safety evaluators

Same AIAgent / RunAsync() API surface — the abstraction doesn't change

What Changes?

New package:

#:package Microsoft.Agents.AI.AzureAI@1.0.0-rc5
#:package Azure.AI.Projects@2.0.0-beta.2

New entry point:

AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());

New env var:

export AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api"

AzureOpenAIClientAIProjectClient — everything else stays the same

First Foundry Agent

Server-side managed agents with versioning

Creating a Foundry Agent

AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());

// Create a server-side agent — Foundry manages it with name + version
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
    agentName: "FoundryBasicsAgent",
    options: new AgentVersionCreationOptions(
        new PromptAgentDefinition(deploymentName)
        {
            Instructions = "You are a friendly assistant. Keep your answers brief.",
        }));
AIAgent agent = aiProjectClient.AsAIAgent(agentVersion);

Agents are server-side resources — named, versioned, persisted in Foundry

Retrieve & Run — Same API

// Retrieve latest version by name
AgentRecord record = await aiProjectClient.Agents.GetAgentAsync("FoundryBasicsAgent");
AIAgent retrieved = aiProjectClient.AsAIAgent(record);

// Non-streaming — same as Azure OpenAI agents
Console.WriteLine(await agent.RunAsync("Tell me a fun fact about Azure."));

// Streaming — same as Azure OpenAI agents
await foreach (var update in agent.RunStreamingAsync("Tell me a fun fact about .NET."))
{
    Console.Write(update);
}

// Cleanup — deletes server-side agent and all its versions
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);

Agent Versioning

// Each CreateAgentVersionAsync call creates a new version
AgentVersion v1 = await aiProjectClient.Agents.CreateAgentVersionAsync(
    agentName: "MyAgent",
    options: new (new PromptAgentDefinition("gpt-4o-mini") {
        Instructions = "You are helpful."
    }));

AgentVersion v2 = await aiProjectClient.Agents.CreateAgentVersionAsync(
    agentName: "MyAgent",
    options: new (new PromptAgentDefinition("gpt-4o-mini") {
        Instructions = "You are extremely helpful and concise."
    }));

// GetAgentAsync returns the latest version as an AgentRecord
AgentRecord latestRecord = await aiProjectClient.Agents.GetAgentAsync("MyAgent");
AIAgent latest = aiProjectClient.AsAIAgent(latestRecord);

Agent definitions are immutable after creation — create a new version to change instructions or tools

Observability

OpenTelemetry + Foundry Traces

Two Sides of the Trace

OTEL Server-side (Foundry)
What Agent spans, chat calls, duration Token counts, cost, response IDs
Where Aspire Dashboard / any OTLP backend Foundry Portal → Traces tab
How .UseOpenTelemetry() + OTLP exporter Automatic — built into Foundry

Same Trace ID links client spans ↔ server traces — full end-to-end visibility

Adding OpenTelemetry to a Foundry Agent

// 1. Setup OTLP exporter → Aspire dashboard
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("FoundryBasicsDemo"))
    .AddSource("FoundryBasicsDemo")
    .AddSource("*Microsoft.Agents.AI")
    .AddOtlpExporter()
    .Build();

// 2. Wrap agent with telemetry
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
    agentName: "FoundryBasicsAgent",
    options: new (new PromptAgentDefinition(deploymentName) { Instructions = "You are a friendly assistant." }));
AIAgent agent = aiProjectClient
    .AsAIAgent(agentVersion)
    .AsBuilder()
    .UseOpenTelemetry(sourceName: "FoundryBasicsDemo")
    .Build();

Correlated Traces

// Parent span groups related calls
using var activitySource = new ActivitySource("FoundryBasicsDemo");
using var activity = activitySource.StartActivity("foundry-basics-demo");

Console.WriteLine($"Trace ID: {activity?.TraceId}");

await agent.RunAsync("Tell me a fun fact about Azure.");
await agent.RunStreamingAsync("Tell me a fun fact about .NET.");

Print the Trace ID → find the same trace in Foundry Portal with token counts and cost

Demo

dotnet run src/09-foundry-basics.cs

Persistent Sessions

Server-side conversations with Foundry

Creating a Foundry Conversation

// Get agent client
AIAgent foundryAgent = aiProjectClient.AsAIAgent(agentVersion);
ChatClientAgent agent = foundryAgent.GetService<ChatClientAgent>()!;

// Create a server-side conversation
ProjectConversationsClient conversationsClient = aiProjectClient
    .GetProjectOpenAIClient()
    .GetProjectConversationsClient();

ProjectConversation conversation = await conversationsClient.CreateProjectConversationAsync();

// Link session to the conversation — history stored server-side
AgentSession session = await agent.CreateSessionAsync(conversation.Id);

// Run
Console.WriteLine(await agent.RunAsync("My name is Oleksii.", session));

conversation.Id is the only thing you need to store — Foundry keeps the full thread server-side

Resuming a Conversation

// New session, same conversation ID — agent remembers everything

AgentSession resumed = await agent.CreateSessionAsync(conversation.Id);
Console.WriteLine(await agent.RunAsync("What's my name?", resumed));

// → "Your name is Oleksii."

Conversations persist beyond sessions — store the ID in your DB, resume from any process

Demo

dotnet run src/09b-foundry-persistent-session.cs

Function Tools

Same pattern, managed by Foundry

Function Tools on Foundry

[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location")] string location)
{
    return $"The weather in {location} is sunny with a high of 22°C.";
}

AIFunction getWeather = AIFunctionFactory.Create(GetWeather);
AITool[] tools = [getWeather];

Creating Agent with Tools

// Register schema server-side (declarative) + provide invocable impl client-side
AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
    agentName: "WeatherAgent",
    options: new AgentVersionCreationOptions(
        new PromptAgentDefinition(deploymentName)
        {
            Instructions = "You are a helpful assistant with weather tools.",
            Tools = { getWeather.AsOpenAIResponseTool() },
        }
    )
);

AIAgent agent = aiProjectClient.AsAIAgent(agentVersion, tools: tools);

Retrieving Agents with Tools

// IMPORTANT: Server only stores the tool schema (JSON Schema)
// You must provide invocable tools when retrieving
AgentRecord record = await aiProjectClient.Agents.GetAgentAsync("WeatherAgent");
AIAgent existing = aiProjectClient.AsAIAgent(
    record,
    tools: [AIFunctionFactory.Create(GetWeather)]
);

Console.WriteLine(await existing.RunAsync("What's the weather in Kyiv?"));

Server stores schemas, client provides implementations — pass tools on GetAIAgentAsync() for automatic invocation

Demo

dotnet run src/10-foundry-tools.cs

Hosted Tools

Code Interpreter, Web Search — zero infrastructure

Hosted vs Client Tools

Client Tools (Sessions 1–2) Hosted Tools (Foundry)
Execution Your process Foundry cloud sandbox
Setup Define + implement One-liner — Foundry provides runtime
Examples AIFunctionFactory.Create(...) ResponseTool.CreateCodeInterpreterTool/CreateWebSearchTool/CreateFileSearchTool
Use case Custom business logic Python execution, web search, file search

Hosted tools run server-side — no local dependencies, no infrastructure to manage

Code Interpreter — Python Sandbox

AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
    agentName: "MathTutor",
    options: new AgentVersionCreationOptions(
        new PromptAgentDefinition(deploymentName)
        {
            Instructions = "You are a math tutor. Write and run Python code to solve problems.",
            Tools = { ResponseTool.CreateCodeInterpreterTool(
                new CodeInterpreterToolContainer(
                    CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(fileIds: []))) },
        }));
AIAgent agent = aiProjectClient.AsAIAgent(agentVersion);

AgentResponse response = await agent.RunAsync(
    "Solve x^3 - 6x^2 + 11x - 6 = 0. Plot the function and mark the roots.");

ResponseTool.CreateCodeInterpreterTool(...) — the agent writes Python, Foundry runs it in a sandbox, returns results + files

Demo

dotnet run src/11a-foundry-code-interpreter.cs

Web Search — Real-Time Queries

AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
    agentName: "WebSearchAgent",
    options: new AgentVersionCreationOptions(
        new PromptAgentDefinition(deploymentName)
        {
            Instructions = "Search the web to answer questions accurately. Cite your sources.",
            Tools = { ResponseTool.CreateWebSearchTool() },
        }));
AIAgent agent = aiProjectClient.AsAIAgent(agentVersion);

AgentResponse response = await agent.RunAsync("What are the latest features in .NET 10?");

Extracting URL Citations

foreach (var annotation in response.Messages
    .SelectMany(m => m.Contents)
    .SelectMany(c => c.Annotations ?? []))
{
    if (annotation.RawRepresentation is UriCitationMessageAnnotation urlCitation)
    {
        Console.WriteLine($"  - {urlCitation.Title}: {urlCitation.Uri}");
    }
}

ResponseTool.CreateWebSearchTool() — agent searches the web autonomously and returns annotated citations

Demo

dotnet run src/11b-foundry-web-search.cs

RAG via Foundry

File upload → Vector store → Grounded Q&A

The RAG Pipeline

Step API What happens
1. Upload file filesClient.UploadFile() File stored in Foundry
2. Create vector store vectorStoresClient.CreateVectorStoreAsync() Auto-chunked + embedded
3. Create agent CreateAgentVersionAsync + ResponseTool.CreateFileSearchTool Agent grounded on your data
4. Ask questions agent.RunAsync() Grounded answers with citations
5. Cleanup Delete agent, vector store, file No orphan resources

RAG in ~10 lines — no embedding pipeline, no vector DB to manage

Upload & Create Vector Store

var projectOpenAIClient = aiProjectClient.GetProjectOpenAIClient();
var filesClient = projectOpenAIClient.GetProjectFilesClient();
var vectorStoresClient = projectOpenAIClient.GetProjectVectorStoresClient();

// Upload knowledge base
OpenAIFile uploaded = filesClient.UploadFile(tempFile, FileUploadPurpose.Assistants);

// Create vector store — auto-chunks and embeds
var vectorStore = await vectorStoresClient.CreateVectorStoreAsync(
    new() { FileIds = { uploaded.Id }, Name = "contoso-products" });

// Wait for file ingestion to complete before querying
while ((await vectorStoresClient.GetVectorStoreAsync(vectorStore.Value.Id)).Value.Status
    != VectorStoreStatus.Completed)
{
    await Task.Delay(500);
}

Create Agent with File Search

AgentVersion agentVersion = await aiProjectClient.Agents.CreateAgentVersionAsync(
    agentName: "RAGAgent",
    options: new AgentVersionCreationOptions(
        new PromptAgentDefinition(deploymentName)
        {
            Instructions = "Answer questions using the product catalog. Cite the source.",
            Tools = { ResponseTool.CreateFileSearchTool(vectorStoreIds: [vectorStoreId]) },
        }));
AIAgent agent = aiProjectClient.AsAIAgent(agentVersion);

// Multi-turn Q&A with grounded answers
var session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("What's the cheapest product?", session));
Console.WriteLine(await agent.RunAsync("Which product supports CI/CD?", session));

Demo

dotnet run src/12-foundry-rag.cs

Foundry Workflows

Declarative multi-agent orchestration

Workflow YAML — Storyteller + Critic

kind: Workflow
trigger:
  kind: OnConversationStart
  id: story_critic_workflow
  actions:
    - kind: InvokeAzureAgent
      id: storyteller_step
      conversationId: =System.ConversationId
      agent:
        name: StorytellerAgent
    - kind: InvokeAzureAgent
      id: critic_step
      conversationId: =System.ConversationId
      agent:
        name: CriticAgent

Declarative YAML defines the agent graph — registered server-side, visible in Foundry Portal's Workflows tab

Registering & Running a Workflow

// Register workflow in Foundry
AgentVersion workflowVersion = await aiProjectClient.Agents
    .CreateAgentVersionAsync(WorkflowName, new(workflowDefinition));

// Run with streaming — each agent produces a separate message
AgentRecord workflowRecord = await aiProjectClient.Agents.GetAgentAsync(WorkflowName);
ChatClientAgent workflowAgent = (ChatClientAgent)aiProjectClient.AsAIAgent(workflowRecord);

ChatClientAgentRunOptions runOptions = new(
    new ChatOptions { ConversationId = conversation.Id });

await foreach (var update in workflowAgent.RunStreamingAsync(prompt, session, runOptions))
{
    Console.Write(update.Text);
}

Same RunStreamingAsync API — Foundry orchestrates the agents server-side, streams results back

Demo

dotnet run src/13-foundry-workflow.cs

Evaluations

Quality and safety scoring

Available Evaluators

Dimension Evaluator What it measures
Groundedness GroundednessEvaluator Are answers grounded in provided context?
Relevance RelevanceEvaluator Does the answer address the question?
Coherence CoherenceEvaluator Is the response well-structured and logical?
Safety ContentHarmEvaluator Violence, self-harm, sexual, hate content

Quality evaluators use an LLM judge, safety evaluators use the Azure AI Foundry content safety service

Running Evaluations

CompositeEvaluator evaluator = new([
    new GroundednessEvaluator(),
    new RelevanceEvaluator(),
    new CoherenceEvaluator(),
    new ContentHarmEvaluator(),
]);

// Safety evaluators need the Foundry content safety endpoint
ContentSafetyServiceConfiguration safetyConfig = new(credential, new Uri(endpoint));
ChatConfiguration config = safetyConfig.ToChatConfiguration(
    originalChatConfiguration: new ChatConfiguration(chatClient));

EvaluationResult result = await evaluator.EvaluateAsync(
    messages, chatResponse, config, additionalContext: [new GroundednessEvaluatorContext(context)]);

Microsoft.Extensions.AI.Evaluation — compose multiple evaluators in a single pass

Demo

dotnet run src/14-foundry-evaluations.cs

Key Takeaways

  1. AIProjectClient.Agents.CreateAgentVersionAsync() + AsAIAgent() — same AIAgent API, server-managed lifecycle with versioning

  2. .UseOpenTelemetry() + Aspire — client spans correlated with Foundry server traces

  3. ResponseTool.CreateCodeInterpreterTool() / CreateWebSearchTool() — zero-infrastructure hosted tools

  4. File upload → vector store → ResponseTool.CreateFileSearchTool() — RAG in ~10 lines

  5. GroundednessEvaluator + ContentHarmEvaluator — evaluate quality and safety before production

Resources