In this post, let's have a look at hosting an Agent on
Azure Functions using the Microsoft Agent Framework. In a
previous post, I have have written about
getting started with Microsoft Agent Framework in .NET. If you haven't read the previous post, I would recommend doing so.
In the previous post, we have been running our agent as a console
application. That's great for learning, but it isn't how we would actually
run agents in production. Agents typically need an HTTP endpoint to
be invoked from a client, and they need to
persist conversation state across requests so a user can have a
multi-turn conversation.
The Microsoft Agent Framework ships with a hosting package for
Azure Functions (Microsoft.Agents.AI.Hosting.AzureFunctions) that gives us both of these out of the box. It builds on top of
Durable Functions, so each conversation thread is durably persisted,
can survive process restarts, and can be resumed later. Let's see how it
works.
Let's start creating an Azure Functions worker project targeting
.NET 10 (isolated worker model). On top of the usual Functions
packages, we need a couple of extras (versions are the latest as of today,
they will change):
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <AzureFunctionsVersion>v4</AzureFunctionsVersion> <_FunctionsSkipCleanOutput>true</_FunctionsSkipCleanOutput> <RootNamespace>MafSamples.DurableFunctions</RootNamespace> <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally> </PropertyGroup> <ItemGroup> <FrameworkReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.2" /> <PackageReference Include="Azure.Identity" Version="1.21.0" /> <PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" /> <PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" Version="1.6.1-preview.260514.1" /> <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" /> <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" /> <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" /> </ItemGroup> <ItemGroup> <None Update="host.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <None Update="local.settings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToPublishDirectory>Never</CopyToPublishDirectory> </None> </ItemGroup> </Project>
Microsoft.Agents.AI.Hosting.AzureFunctions does the heavy lifting and plugs the agent into the Functions host,
exposes an HTTP endpoint per agent, and wires up
Durable Functions backed state for each conversation thread.
local.settings.json
Durable Functions needs a storage backend, and for local development we
will be using Azurite storage emulator for simplicity. We could also go
with the Durable Task Scheduler (DTS) and get its monitoring features out
of the box, but let's leave that for another post.
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AZURE_AI_ENDPOINT": "https://<your-foundry-resource>.services.ai.azure.com/",
"AZURE_AI_MODEL": "<DEPLOYMENT_NAME>"
}
}
Program.cs
Here is the entire program. The agent definition is the same one we have
used in the previous post, a small weekend-planner with three tools.
The new changes are FunctionsApplication.CreateBuilder and
ConfigureDurableAgents:
using Azure.AI.Projects; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AzureFunctions; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.DurableTask; using Microsoft.Extensions.AI; using Microsoft.Extensions.Hosting; using System.ComponentModel; var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_ENDPOINT")!; var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL")!; // For local development, using AzureCliCredential var credential = new AzureCliCredential(); AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) .AsAIAgent( model: deploymentName, name: "weekend-planner", instructions: """ You help users plan their weekends and choose the best activities for the given weather. If an activity would be unpleasant in weather, don't suggest it. Include date of the weekend in response. """, tools: [ AIFunctionFactory.Create(GetWeather), AIFunctionFactory.Create(GetActivities), AIFunctionFactory.Create(GetCurrentDate) ]); // Automatically creates HTTP endpoints and manages state persistence using IHost app = FunctionsApplication .CreateBuilder(args) .ConfigureFunctionsWebApplication() .ConfigureDurableAgents(options => options.AddAIAgent(agent) ) .Build(); app.Run(); [Description("Returns weather data for a given city.")] static WeatherResult GetWeather( [Description("The city to get the weather for.")] string city) { Console.WriteLine($"[Tool] Getting weather for '{city}'."); return new WeatherResult(18, "Rainy"); } [Description("Returns a list of leisure activities for a given city and date, each with a name and location.")] static List<LeisureActivity> GetActivities( [Description("The city to get activities for.")] string city, [Description("The date to get activities for in format YYYY-MM-DD.")] string date) { Console.WriteLine($"[Tool] Getting activities for '{city}' on '{date}'."); return [ new("Hiking", city), new("Beach", city), new("Museum", city) ]; } [Description("Gets the current date from the system and returns as a string in format YYYY-MM-DD.")] static string GetCurrentDate() { Console.WriteLine("[Tool] Getting current date"); return DateTime.Now.ToString("yyyy-MM-dd"); } record WeatherResult(int Temperature, string Description); record LeisureActivity(string Name, string Location); // Dummy orchestrator to satisfy Durable Functions requirement of having at least one orchestration in the assembly. public static class DummyOrchestrator { [Function(nameof(DummyOrchestrator))] public static Task RunOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { return Task.CompletedTask; } }
Notice we don't define any HTTP-triggered functions ourselves.
ConfigureDurableAgents registers an HTTP endpoint for every agent we
add via AddAIAgent, using the agent's name. For our
weekend-planner agent, the endpoint will be:
POST /api/agents/weekend-planner/run
The one quirky bit is the DummyOrchestrator. The Functions runtime
needs at least one [OrchestrationTrigger] to be discovered in the
assembly for the Durable extension to bind correctly. (see issue: microsoft/agent-framework/issues/5927)
Then we can call the agent over HTTP. Here is a request to start a new
conversation:
POST http://localhost:7098/api/agents/weekend-planner/run
Content-Type: application/json
{
"message": "What should I do this weekend in Auckland?"
}
The response comes back with the agent's reply.
POST http://localhost:7098/api/agents/weekend-planner/run?thread_id=<x-ms-thread-id>
Content-Type: application/json
{
"message": "What is the weather like there?"
}
The agent remembers we were talking about a specific city and answers in
context without us writing a single line of state management code. Without a
thread, if we ask the same above question as a fresh conversation, it will return
something like What is the city etc. The thread, messages, and tool calls are
all persisted in the storage account via Durable Functions. If the
Functions host restarts, the conversation continues from where it left off.
Complete source code:
More read:
Hope this helps.
Happy Coding.
Regards,
Jaliya
No comments:
Post a Comment