Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<PackageVersion Include="MessagePack" Version="3.1.7" /> <!-- Transitive dependency of Aspire pinned to newer version due to vulnerability in 2.5.192 -->
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.26" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.6" />
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.28" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.6" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.8" />
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageVersion Include="Azure.Core" Version="1.60.0" />
<PackageVersion Include="Azure.Core" Version="1.61.0" />
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
Expand All @@ -42,17 +42,17 @@
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.9" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.10" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
<PackageVersion Include="System.ClientModel" Version="1.14.0" />
<PackageVersion Include="System.ClientModel" Version="1.15.0" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.9" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.5" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.8" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.10" />
<!-- AG-UI .NET SDK packages (published by the AG-UI team). -->
<PackageVersion Include="AGUI.Abstractions" Version="0.0.3" />
<PackageVersion Include="AGUI.Formatting" Version="0.0.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
string deploymentName =
Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
?? Environment.GetEnvironmentVariable("FOUNDRY_MODEL")
?? "gpt-4o";

// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
Expand All @@ -37,10 +40,26 @@
.GetChatClient(deploymentName)
.AsIChatClient();

// Create translation agents
AIAgent frenchAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to French.");
AIAgent spanishAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to Spanish.");
AIAgent englishAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to English.");
// A workflow checkpoint records each executor identity. Keep both Id and Name stable so a new
// container instance reconstructs the same workflow and can resume checkpoints written earlier.
AIAgent frenchAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Id = "french-translator",
Name = "French Translator",
ChatOptions = new() { Instructions = "You are a translation assistant that translates the provided text to French." },
});
AIAgent spanishAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Id = "spanish-translator",
Name = "Spanish Translator",
ChatOptions = new() { Instructions = "You are a translation assistant that translates the provided text to Spanish." },
});
AIAgent englishAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Id = "english-translator",
Name = "English Translator",
ChatOptions = new() { Instructions = "You are a translation assistant that translates the provided text to English." },
});

// Build the sequential workflow: French → Spanish → English
AIAgent agent = new WorkflowBuilder(frenchAgent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

// The terminal stream events are named the same in two namespaces this file pulls in, and the short
// name binds to the one the event objects are not. Naming them here keeps `is` checks against the
Expand Down Expand Up @@ -53,8 +54,8 @@ public AgentFrameworkResponseHandler(
ILogger<AgentFrameworkResponseHandler> logger,
FoundryToolboxService? toolboxService = null)
{
ArgumentNullException.ThrowIfNull(serviceProvider);
ArgumentNullException.ThrowIfNull(logger);
_ = Throw.IfNull(serviceProvider);
_ = Throw.IfNull(logger);

this._serviceProvider = serviceProvider;
this._logger = logger;
Expand All @@ -68,7 +69,7 @@ public override async IAsyncEnumerable<ResponseStreamEvent> CreateAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
// 1. Resolve agent
var agent = this.ResolveAgent(request);
var agent = this.ResolveAgent(request, out string agentStorageIdentity);
var sessionStore = this.ResolveSessionStore(request);

// Fail fast with a clear, actionable error when this 2.0.0-only image is served container
Expand Down Expand Up @@ -130,9 +131,28 @@ public override async IAsyncEnumerable<ResponseStreamEvent> CreateAsync(
// Load the session for this conversation, or start a new one. The store returns null when
// nothing is persisted for the key, so a fresh conversation and a resumed one both end up with
// a session to run against.
AgentSession? session = !string.IsNullOrWhiteSpace(agentSessionId)
? await sessionStore.GetOrCreateSessionAsync(agent, agentSessionId, resolvedUserId, cancellationToken).ConfigureAwait(false)
: await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
AgentSession? session;
if (string.IsNullOrWhiteSpace(agentSessionId))
{
session = await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
}
else
{
session = sessionStore is FoundryAgentSessionStore foundrySessionStore
? await foundrySessionStore.GetSessionAsync(
agent,
agentStorageIdentity,
agentSessionId,
resolvedUserId,
cancellationToken).ConfigureAwait(false)
: await sessionStore.GetSessionAsync(
agent,
agentSessionId,
resolvedUserId,
cancellationToken).ConfigureAwait(false);

session ??= await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
}

// Capture the platform per-request call id (x-agent-foundry-call-id, protocol 2.0.0 only).
// It is re-applied to the ambient HostedCallContext immediately before each outbound egress
Expand Down Expand Up @@ -508,7 +528,25 @@ bool CheckNotAllowedStoreUsage() =>
// Persist the session for the next turn of this conversation, unless this one is being failed.
if (session is not null && !turnFailed)
{
await sessionStore.SaveSessionAsync(agent, agentSessionId!, session, resolvedUserId, cancellationToken).ConfigureAwait(false);
if (sessionStore is FoundryAgentSessionStore foundrySessionStore)
{
await foundrySessionStore.SaveSessionAsync(
agent,
agentStorageIdentity,
agentSessionId!,
session,
resolvedUserId,
cancellationToken).ConfigureAwait(false);
}
else
{
await sessionStore.SaveSessionAsync(
agent,
agentSessionId!,
session,
resolvedUserId,
cancellationToken).ConfigureAwait(false);
}
}
}

Expand Down Expand Up @@ -571,7 +609,7 @@ private static string NewOAuthConsentItemId()
/// Tries <c>agent.name</c> first, then falls back to <c>metadata["entity_id"]</c>.
/// If neither is present, attempts to resolve a default (non-keyed) <see cref="AIAgent"/>.
/// </summary>
private AIAgent ResolveAgent(CreateResponse request)
private AIAgent ResolveAgent(CreateResponse request, out string storageIdentity)
{
var agentName = GetAgentName(request);

Expand All @@ -580,8 +618,8 @@ private AIAgent ResolveAgent(CreateResponse request)
var agent = this._serviceProvider.GetKeyedService<AIAgent>(agentName);
if (agent is not null)
{
FoundryHostingExtensions.TryApplyUserAgent(agent);
return FoundryHostingExtensions.ApplyOpenTelemetry(agent);
storageIdentity = $"key:{agentName}";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddFoundryResponses(agent) registers the same named instance as both keyed and default, but this path stores it as key:<name> while an unnamed request stores it as name:<name>. Alternating between these supported request forms therefore loads a fresh session for the same agent, user, and conversation. Please assign one canonical storage identity to both aliases of the same registration.

return this.PrepareResolvedAgent(agent);
}

if (this._logger.IsEnabled(LogLevel.Warning))
Expand All @@ -594,8 +632,11 @@ private AIAgent ResolveAgent(CreateResponse request)
var defaultAgent = this._serviceProvider.GetService<AIAgent>();
if (defaultAgent is not null)
{
FoundryHostingExtensions.TryApplyUserAgent(defaultAgent);
return FoundryHostingExtensions.ApplyOpenTelemetry(defaultAgent);
storageIdentity = !string.IsNullOrWhiteSpace(defaultAgent.Name)
? $"name:{defaultAgent.Name}"
: "default";

return this.PrepareResolvedAgent(defaultAgent);
}

var errorMessage = string.IsNullOrEmpty(agentName)
Expand All @@ -605,6 +646,17 @@ private AIAgent ResolveAgent(CreateResponse request)
throw new InvalidOperationException(errorMessage);
}

private AIAgent PrepareResolvedAgent(AIAgent agent)
{
FoundryHostingExtensions.TryApplyUserAgent(agent);

AIAgent prepared = FoundryHostingExtensions.ApplyWorkflowCheckpointing(
agent,
this._serviceProvider.GetService<ILoggerFactory>());

return FoundryHostingExtensions.ApplyOpenTelemetry(prepared);
}

/// <summary>
/// Resolves an <see cref="AIAgent"/> from the request.
/// Tries <c>agent.name</c> first, then falls back to <c>metadata["entity_id"]</c>.
Expand Down
Loading
Loading