diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 77e772cf214..875a37d7422 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -45,6 +45,7 @@ + diff --git a/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/Agent_ContainerAppsDynamicSessions.csproj b/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/Agent_ContainerAppsDynamicSessions.csproj new file mode 100644 index 00000000000..952d9a1362a --- /dev/null +++ b/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/Agent_ContainerAppsDynamicSessions.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/Program.cs b/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/Program.cs new file mode 100644 index 00000000000..338628100a5 --- /dev/null +++ b/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/Program.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Expose Azure Container Apps Dynamic Sessions as an agent function tool. + +using System.ComponentModel; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +string projectEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); +string model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") + ?? throw new InvalidOperationException("FOUNDRY_MODEL is not set."); +string poolEndpoint = Environment.GetEnvironmentVariable("POOL_MANAGEMENT_ENDPOINT") + ?? throw new InvalidOperationException("POOL_MANAGEMENT_ENDPOINT is not set."); +Uri poolUri = new(poolEndpoint); +if (poolUri.Scheme != Uri.UriSchemeHttps || poolUri.UserInfo.Length != 0 || poolEndpoint.Contains('?') || poolEndpoint.Contains('#')) +{ + throw new InvalidOperationException("POOL_MANAGEMENT_ENDPOINT must be a trusted HTTPS pool URL without credentials or a query."); +} + +// One application-owned session per run. Reuse it for this agent's tool calls only. +string sessionId = Guid.NewGuid().ToString("N"); +Uri executionUri = new($"{poolEndpoint.TrimEnd('/')}/executions?api-version=2025-10-02-preview&identifier={sessionId}"); +AzureCliCredential credential = new(); +using HttpClient httpClient = new(new HttpClientHandler { AllowAutoRedirect = false, CheckCertificateRevocationList = true }) { Timeout = TimeSpan.FromSeconds(70) }; + +AIAgent agent = new AIProjectClient(new Uri(projectEndpoint), credential).AsAIAgent( + model: model, + instructions: "Use ExecutePython for calculations. Check its status and errors before reporting a result.", + tools: [AIFunctionFactory.Create(ExecutePythonAsync, "ExecutePython")]); + +Console.WriteLine(await agent.RunAsync("Use Python to calculate the total of 19.95, 42.50, and 8.75, then add 8% tax.")); + +[Description("Run Python in the conversation's cloud sandbox and return its output and execution status.")] +async Task ExecutePythonAsync([Description("Python code to execute in the remote session.")] string code, CancellationToken cancellationToken) +{ + AccessToken token = await credential.GetTokenAsync(new TokenRequestContext(["https://dynamicsessions.io/.default"]), cancellationToken); + using HttpRequestMessage request = new(HttpMethod.Post, executionUri); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + request.Content = JsonContent.Create(new + { + codeInputType = "Inline", + executionType = "Synchronous", + code, + timeoutInSeconds = 30, + outputStreamsMaxLength = 4096, + }); + using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(cancellationToken); +} diff --git a/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/README.md b/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/README.md new file mode 100644 index 00000000000..77bbd2272e8 --- /dev/null +++ b/dotnet/samples/02-agents/Agents/Agent_ContainerAppsDynamicSessions/README.md @@ -0,0 +1,46 @@ +# Azure Container Apps Dynamic Sessions + +This sample gives an Agent Framework agent a function tool that runs Python in an +Azure Container Apps code-interpreter session. It uses the existing HTTP and Azure +identity libraries to call the [code execution REST API](https://learn.microsoft.com/rest/api/data-plane/containerapps/code-execution/execute). +Generated code runs in the cloud session, not on the machine running this sample. + +## Setup + +Use .NET 10 and an existing Microsoft Foundry model deployment. Create a Python +code-interpreter session pool using the [Dynamic Sessions setup guide](https://learn.microsoft.com/azure/container-apps/sessions-usage). +The signed-in identity needs `Azure ContainerApps Session Executor` on the pool +and permission to invoke the Foundry model. Sign in with `az login`. + +Retrieve the pool management endpoint from +`az containerapp sessionpool show --name --resource-group --query properties.poolManagementEndpoint --output tsv`. +Then configure and run: + +```powershell +$env:FOUNDRY_PROJECT_ENDPOINT = "" +$env:FOUNDRY_MODEL = "" +$env:POOL_MANAGEMENT_ENDPOINT = "" +dotnet run +``` + +The agent asks the tool to calculate an order total with tax. The tool returns +the execution status and result, including standard output and errors. HTTP +failures are surfaced to the agent's function invocation pipeline. Each execution +is limited to 30 seconds with a bounded output stream; the HTTP client allows +additional time for transport and pool startup. + +## Session ownership and lifecycle + +The application generates a new session identifier for each run and reuses it +for that run's tool calls, allowing Python state to persist between calls. The +model supplies only code, not the endpoint or session identifier. For a multi-user +host, bind a separate tool/session identifier to each authorized conversation. +Do not share a session across unrelated users. + +Use only a trusted pool management endpoint. Configure the pool's network access +for your workload and do not pass application credentials or confidential data +into generated code. This example uses interactive developer authentication; +deployed applications should use an identity configured for their host. + +The sample does not provision or delete Azure resources. The pool's configured +cooldown manages idle session cleanup. Model and pool usage incur costs. diff --git a/dotnet/samples/02-agents/Agents/README.md b/dotnet/samples/02-agents/Agents/README.md index 487e7f269a2..da36e783cd3 100644 --- a/dotnet/samples/02-agents/Agents/README.md +++ b/dotnet/samples/02-agents/Agents/README.md @@ -26,6 +26,7 @@ Before you begin, ensure you have the following prerequisites: |Sample|Description| |---|---| +|[Container Apps Dynamic Sessions](./Agent_ContainerAppsDynamicSessions/)|Run agent-generated Python in a cloud code-interpreter session using a function tool| |[Using OpenAPI function tools with a simple agent](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/AgentFrameworkMigration/AzureOpenAI/Step04_ToolCall_WithOpenAPI)|This sample demonstrates how to create function tools from an OpenAPI spec and use them with a simple agent (note that this sample is in the Semantic Kernel repository)| |[Using function tools with approvals](./Agent_Step01_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution| |[Structured output with a simple agent](./Agent_Step02_StructuredOutput/)|This sample demonstrates how to use structured output with a simple agent|