mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Initial draft of actor runtime abstractions (#197)
* Initial draft of actor runtime abstractions
This commit is contained in:
committed by
GitHub
Unverified
parent
8f2d3da80d
commit
41d441420e
+172
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
internal static class ActorFrameworkWebApplicationExtensions
|
||||
{
|
||||
public static void MapAgents(this WebApplication app)
|
||||
{
|
||||
app.MapPost(
|
||||
"/invocations/actor/{name}/{sessionId}/{requestId}", async (
|
||||
string name,
|
||||
string sessionId,
|
||||
string requestId,
|
||||
[FromQuery] bool? stream,
|
||||
[FromBody] JsonElement request,
|
||||
HttpContext context,
|
||||
ILogger<Program> logger,
|
||||
IActorClient actorClient,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var streamRequested = stream == true;
|
||||
|
||||
Log.ActorInvocationStarted(logger, name, sessionId, requestId, streamRequested);
|
||||
Log.ActorRequestReceived(logger, requestId, request.GetRawText().Length, streamRequested);
|
||||
|
||||
try
|
||||
{
|
||||
var responseHandle = await actorClient.SendRequestAsync(new ActorRequest(new ActorId(name, sessionId), requestId, method: "run", @params: request), cancellationToken);
|
||||
Log.ActorRequestSent(logger, requestId, name, sessionId);
|
||||
|
||||
if (!responseHandle.TryGetResponse(out var response))
|
||||
{
|
||||
Log.ActorResponseHandleObtained(logger, requestId, false);
|
||||
|
||||
if (stream == true)
|
||||
{
|
||||
Log.SseStreamingStarted(logger, requestId);
|
||||
// If no response is available and streaming is requested, stream the response handle.
|
||||
var result = await StreamResponse(context, responseHandle, cancellationToken);
|
||||
Log.ActorInvocationCompleted(logger, name, sessionId, requestId, RequestStatus.Pending, stopwatch.ElapsedMilliseconds);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Otherwise, wait for a response to become available.
|
||||
Log.WaitingForActorResponse(logger, requestId);
|
||||
response = await responseHandle.GetResponseAsync(cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ActorResponseHandleObtained(logger, requestId, true);
|
||||
}
|
||||
|
||||
Log.ActorResponseReceived(logger, requestId, response.Status);
|
||||
var processResult = await ProcessResponse(name, sessionId, requestId, stream, context, responseHandle, response, cancellationToken);
|
||||
Log.ActorInvocationCompleted(logger, name, sessionId, requestId, response.Status, stopwatch.ElapsedMilliseconds);
|
||||
return processResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ActorInvocationFailed(logger, ex, name, sessionId, requestId, stopwatch.ElapsedMilliseconds);
|
||||
return Results.Problem("An error occurred processing the request.", statusCode: 500);
|
||||
}
|
||||
|
||||
static async Task<IResult> StreamResponse(HttpContext context, ActorResponseHandle responseHandle, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestId = context.Request.RouteValues["requestId"]?.ToString() ?? "unknown";
|
||||
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
Log.SseStreamingStarted(logger, requestId);
|
||||
InitializeSseResponse(context);
|
||||
await context.Response.Body.FlushAsync(cancellationToken);
|
||||
|
||||
var updateCount = 0;
|
||||
try
|
||||
{
|
||||
await foreach (var progress in responseHandle.WatchUpdatesAsync(cancellationToken))
|
||||
{
|
||||
// Properly serialize the progress data as JSON and escape for SSE
|
||||
var progressJson = JsonSerializer.Serialize(progress.Data, (JsonSerializerOptions?)null);
|
||||
var eventData = JsonSerializer.Serialize(new { @event = JsonDocument.Parse(progressJson).RootElement });
|
||||
var eventText = $"data: {eventData}\n\n";
|
||||
|
||||
await context.Response.WriteAsync(eventText, cancellationToken);
|
||||
await context.Response.Body.FlushAsync(cancellationToken);
|
||||
|
||||
updateCount++;
|
||||
Log.SseProgressUpdateSent(logger, requestId, updateCount);
|
||||
}
|
||||
|
||||
// Send completion marker
|
||||
await context.Response.WriteAsync("data: completed\n\n", cancellationToken);
|
||||
await context.Response.Body.FlushAsync(cancellationToken);
|
||||
|
||||
Log.SseStreamingCompleted(logger, requestId, updateCount);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Log.SseStreamingCancelled(logger, requestId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.SseStreamingError(logger, ex, requestId);
|
||||
}
|
||||
|
||||
// TODO: refactor the enclosing method so we don't need to return a result here.
|
||||
return Results.Empty;
|
||||
}
|
||||
|
||||
static void InitializeSseResponse(HttpContext context)
|
||||
{
|
||||
context.Response.Headers.ContentType = "text/event-stream";
|
||||
context.Response.Headers.CacheControl = "no-cache,no-store";
|
||||
context.Response.Headers.Connection = "keep-alive";
|
||||
|
||||
// Make sure we disable all response buffering for SSE.
|
||||
context.Response.Headers.ContentEncoding = "identity";
|
||||
context.Features.GetRequiredFeature<IHttpResponseBodyFeature>().DisableBuffering();
|
||||
}
|
||||
|
||||
static async Task<IResult> ProcessResponse(
|
||||
string name,
|
||||
string sessionId,
|
||||
string requestId,
|
||||
bool? stream,
|
||||
HttpContext context,
|
||||
ActorResponseHandle responseHandle,
|
||||
ActorResponse response,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
|
||||
var isStreaming = stream != false && response.Status == RequestStatus.Pending;
|
||||
|
||||
Log.ProcessingActorResponse(logger, requestId, response.Status, isStreaming);
|
||||
|
||||
var result = response.Status switch
|
||||
{
|
||||
// If the response is pending & streaming is disabled, return a 202 Accepted with the messageId.
|
||||
RequestStatus.Pending when stream == false => Results.Accepted($"/invocations/actor/{name}/{sessionId}/{requestId}"),
|
||||
|
||||
// If streaming is not explicitly disabled, stream the response back.
|
||||
RequestStatus.Pending => await StreamResponse(context, responseHandle, cancellationToken),
|
||||
RequestStatus.Completed => Results.Ok(response.Data),
|
||||
|
||||
// If the response failed, we can return a 500 Internal Server Error.
|
||||
RequestStatus.Failed => Results.Problem("The invocation failed.", statusCode: 500),
|
||||
RequestStatus.NotFound => Results.NotFound(new { message = "Not found." }),// If the actor is not found, we can return a 404 Not Found.
|
||||
_ => throw new NotSupportedException($"Unsupported request status: {response.Status}"),
|
||||
};
|
||||
|
||||
var responseType = response.Status switch
|
||||
{
|
||||
RequestStatus.Pending when stream == false => "Accepted",
|
||||
RequestStatus.Pending => "Streaming",
|
||||
RequestStatus.Completed => "Ok",
|
||||
RequestStatus.Failed => "Problem",
|
||||
RequestStatus.NotFound => "NotFound",
|
||||
_ => "Unknown"
|
||||
};
|
||||
|
||||
Log.ActorResponseProcessed(logger, requestId, responseType);
|
||||
return result;
|
||||
}
|
||||
})
|
||||
.WithName("Invocations");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using HelloHttpApi.ApiService;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
/// <summary>
|
||||
/// Source-generated JSON type information for use by all Agents implementations.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(
|
||||
JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(ChatMessage))]
|
||||
[JsonSerializable(typeof(List<ChatMessage>))]
|
||||
[JsonSerializable(typeof(ChatClientAgentThread))]
|
||||
[JsonSerializable(typeof(ChatClientAgentRunRequest))]
|
||||
[JsonSerializable(typeof(AgentRunResponseUpdate))]
|
||||
internal sealed partial class AgentsJsonContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using HelloHttpApi.ApiService;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
internal sealed class ChatClientAgentActor(ChatClientAgent agent, JsonSerializerOptions jsonSerializerOptions, IActorRuntimeContext context, ILogger<ChatClientAgentActor> logger) : IActor
|
||||
{
|
||||
private string? _etag;
|
||||
private ChatClientAgentThread? _thread;
|
||||
|
||||
public ValueTask DisposeAsync() => default;
|
||||
|
||||
public async ValueTask RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Log.ActorStarted(logger, context.ActorId.ToString(), agent.Name ?? "Unknown");
|
||||
await Task.Yield();
|
||||
|
||||
// Restore thread state
|
||||
var response = await context.ReadAsync(
|
||||
new ActorReadOperationBatch([new GetValueOperation("thread")]),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this._etag = response.ETag;
|
||||
if (response.Results[0] is GetValueResult threadResult)
|
||||
{
|
||||
if (threadResult.Value is { } threadJson)
|
||||
{
|
||||
// Deserialize the thread state if it exist
|
||||
this._thread = threadJson.Deserialize<ChatClientAgentThread>(
|
||||
(JsonTypeInfo<ChatClientAgentThread>)jsonSerializerOptions.GetTypeInfo(typeof(ChatClientAgentThread)));
|
||||
}
|
||||
}
|
||||
|
||||
this._thread ??= (ChatClientAgentThread)agent.GetNewThread();
|
||||
Log.ThreadStateRestored(logger, context.ActorId.ToString(), response.Results[0] is GetValueResult { Value: not null });
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var message in context.WatchMessagesAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
switch (message.Type)
|
||||
{
|
||||
case ActorMessageType.Request:
|
||||
await this.HandleAgentRequestAsync((ActorRequestMessage)message, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
case ActorMessageType.Response:
|
||||
// Handle response messages if needed
|
||||
break;
|
||||
default:
|
||||
Log.UnknownMessageType(logger, message.Type.ToString(), context.ActorId.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ErrorProcessingMessages(logger, ex, context.ActorId.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAgentRequestAsync(ActorRequestMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestId = message.MessageId;
|
||||
Debug.Assert(this._thread is not null);
|
||||
Debug.Assert(this._etag is not null);
|
||||
|
||||
// Parse the request to get the agent run parameters
|
||||
List<ChatMessage>? messages;
|
||||
if (message.Params is { } payload)
|
||||
{
|
||||
var arg = payload.Deserialize<ChatClientAgentRunRequest>(
|
||||
(JsonTypeInfo<ChatClientAgentRunRequest>)jsonSerializerOptions.GetTypeInfo(typeof(ChatClientAgentRunRequest)));
|
||||
messages = arg?.Messages;
|
||||
}
|
||||
|
||||
messages ??= [];
|
||||
|
||||
Log.ProcessingAgentRequest(logger, requestId, context.ActorId.ToString(), messages.Count);
|
||||
try
|
||||
{
|
||||
var typeInfo = (JsonTypeInfo<AgentRunResponseUpdate>)jsonSerializerOptions.GetTypeInfo(typeof(AgentRunResponseUpdate));
|
||||
var i = 0;
|
||||
var updates = new List<AgentRunResponseUpdate>();
|
||||
await foreach (var update in agent.RunStreamingAsync(messages, this._thread, cancellationToken: cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var updateJson = JsonSerializer.SerializeToElement(update, typeInfo);
|
||||
context.OnProgressUpdate(requestId, i++, updateJson);
|
||||
updates.Add(update);
|
||||
Log.AgentStreamingUpdate(logger, requestId, i);
|
||||
}
|
||||
|
||||
var serializedRunResponse = JsonSerializer.SerializeToElement(
|
||||
updates.ToAgentRunResponse(),
|
||||
(JsonTypeInfo<AgentRunResponse>)jsonSerializerOptions.GetTypeInfo(typeof(AgentRunResponse)));
|
||||
var writeResponse = await context.WriteAsync(
|
||||
new(this._etag, [new UpdateRequestOperation(requestId, RequestStatus.Completed, serializedRunResponse)]), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (!writeResponse.Success)
|
||||
{
|
||||
Log.WriteOperationFailed(logger, context.ActorId.ToString(), requestId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.AgentRequestCompleted(logger, requestId, updates.Count);
|
||||
}
|
||||
|
||||
this._etag = writeResponse.ETag;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.AgentRequestFailed(logger, exception, requestId, context.ActorId.ToString());
|
||||
|
||||
// TODO: Retry later?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace HelloHttpApi.ApiService;
|
||||
|
||||
public sealed class ChatClientAgentRunRequest
|
||||
{
|
||||
[JsonPropertyName("messages")]
|
||||
public List<ChatMessage> Messages { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.Abstractions\Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.Runtime.Abstractions\Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.Runtime\Microsoft.Extensions.AI.Agents.Runtime.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
|
||||
<ProjectReference Include="..\HelloHttpApi.ServiceDefaults\HelloHttpApi.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Aspire.Hosting.Azure.CognitiveServices" />
|
||||
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.AzureAIInference" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
namespace HelloHttpApi.ApiService;
|
||||
|
||||
public static class HostApplicationBuilderAgentExtensions
|
||||
{
|
||||
public static IHostApplicationBuilder AddChatClientAgent(this IHostApplicationBuilder builder, string name, string instructions, string? chatClientKey = null)
|
||||
{
|
||||
var agentKey = $"agent:{name}";
|
||||
builder.Services.AddKeyedSingleton(agentKey, (sp, key) =>
|
||||
{
|
||||
var chatClient = chatClientKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientKey);
|
||||
return new ChatClientAgent(chatClient, instructions, name);
|
||||
});
|
||||
var actorBuilder = builder.AddActorRuntime();
|
||||
|
||||
actorBuilder.AddActorType(
|
||||
new ActorType(agentKey),
|
||||
(sp, ctx) => new ChatClientAgentActor(
|
||||
sp.GetRequiredKeyedService<ChatClientAgent>(agentKey),
|
||||
sp.GetService<JsonSerializerOptions>() ?? JsonSerializerOptions.Web,
|
||||
ctx,
|
||||
sp.GetRequiredService<ILogger<ChatClientAgentActor>>()));
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HelloHttpApi.ApiService;
|
||||
|
||||
public class InvocationResponse
|
||||
{
|
||||
[JsonPropertyName("response")]
|
||||
public JsonElement Response { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string? Status { get; set; } = "success";
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using HelloHttpApi.ApiService;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// <summary>
|
||||
/// High-performance logging messages using LoggerMessage source generator.
|
||||
/// </summary>
|
||||
internal static partial class Log
|
||||
{
|
||||
// API endpoint logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor invocation started: Name={ActorName}, SessionId={SessionId}, RequestId={RequestId}, Stream={StreamRequested}")]
|
||||
public static partial void ActorInvocationStarted(ILogger logger, string actorName, string sessionId, string requestId, bool streamRequested);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor invocation completed: Name={ActorName}, SessionId={SessionId}, RequestId={RequestId}, Status={Status}, Duration={DurationMs}ms")]
|
||||
public static partial void ActorInvocationCompleted(ILogger logger, string actorName, string sessionId, string requestId, RequestStatus status, long durationMs);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Actor invocation failed: Name={ActorName}, SessionId={SessionId}, RequestId={RequestId}, Duration={DurationMs}ms")]
|
||||
public static partial void ActorInvocationFailed(ILogger logger, Exception exception, string actorName, string sessionId, string requestId, long durationMs);
|
||||
|
||||
// SSE streaming logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "SSE streaming started for request: {RequestId}")]
|
||||
public static partial void SseStreamingStarted(ILogger logger, string requestId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "SSE progress update sent: RequestId={RequestId}, UpdateCount={UpdateCount}")]
|
||||
public static partial void SseProgressUpdateSent(ILogger logger, string requestId, int updateCount);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "SSE streaming completed: RequestId={RequestId}, TotalUpdates={TotalUpdates}")]
|
||||
public static partial void SseStreamingCompleted(ILogger logger, string requestId, int totalUpdates);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Warning,
|
||||
Message = "SSE streaming cancelled: RequestId={RequestId}")]
|
||||
public static partial void SseStreamingCancelled(ILogger logger, string requestId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Error,
|
||||
Message = "SSE streaming error: RequestId={RequestId}")]
|
||||
public static partial void SseStreamingError(ILogger logger, Exception exception, string requestId);
|
||||
|
||||
// Response processing logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Processing actor response: RequestId={RequestId}, Status={Status}, IsStreaming={IsStreaming}")]
|
||||
public static partial void ProcessingActorResponse(ILogger logger, string requestId, RequestStatus status, bool isStreaming);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor response processed successfully: RequestId={RequestId}, ResponseType={ResponseType}")]
|
||||
public static partial void ActorResponseProcessed(ILogger logger, string requestId, string responseType);
|
||||
|
||||
// Ping endpoint logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Ping endpoint accessed: Status={Status}, TimeOfLastUpdate={TimeOfLastUpdate}")]
|
||||
public static partial void PingEndpointAccessed(ILogger logger, PingResponseStatus status, long timeOfLastUpdate);
|
||||
|
||||
// Request/Response logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Actor request received: RequestId={RequestId}, PayloadSize={PayloadSize} bytes, Stream={StreamRequested}")]
|
||||
public static partial void ActorRequestReceived(ILogger logger, string requestId, int payloadSize, bool streamRequested);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Actor request sent to runtime: RequestId={RequestId}, ActorName={ActorName}, SessionId={SessionId}")]
|
||||
public static partial void ActorRequestSent(ILogger logger, string requestId, string actorName, string sessionId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Actor response handle obtained: RequestId={RequestId}, HasImmediateResponse={HasImmediateResponse}")]
|
||||
public static partial void ActorResponseHandleObtained(ILogger logger, string requestId, bool hasImmediateResponse);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Waiting for actor response: RequestId={RequestId}")]
|
||||
public static partial void WaitingForActorResponse(ILogger logger, string requestId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Actor response received: RequestId={RequestId}, Status={Status}")]
|
||||
public static partial void ActorResponseReceived(ILogger logger, string requestId, RequestStatus status);
|
||||
|
||||
// ChatClientAgentActor logging
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Actor started: ActorId={ActorId}, AgentName={AgentName}")]
|
||||
public static partial void ActorStarted(ILogger logger, string actorId, string agentName);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Thread state restored: ActorId={ActorId}, HasExistingThread={HasExistingThread}")]
|
||||
public static partial void ThreadStateRestored(ILogger logger, string actorId, bool hasExistingThread);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Processing agent request: RequestId={RequestId}, ActorId={ActorId}, MessageCount={MessageCount}")]
|
||||
public static partial void ProcessingAgentRequest(ILogger logger, string requestId, string actorId, int messageCount);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Agent streaming update: RequestId={RequestId}, UpdateNumber={UpdateNumber}")]
|
||||
public static partial void AgentStreamingUpdate(ILogger logger, string requestId, int updateNumber);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "Agent request completed: RequestId={RequestId}, TotalUpdates={TotalUpdates}")]
|
||||
public static partial void AgentRequestCompleted(ILogger logger, string requestId, int totalUpdates);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Error,
|
||||
Message = "Agent request failed: RequestId={RequestId}, ActorId={ActorId}")]
|
||||
public static partial void AgentRequestFailed(ILogger logger, Exception exception, string requestId, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Unknown message type received: MessageType={MessageType}, ActorId={ActorId}")]
|
||||
public static partial void UnknownMessageType(ILogger logger, string messageType, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Warning,
|
||||
Message = "Error processing messages: ActorId={ActorId}")]
|
||||
public static partial void ErrorProcessingMessages(ILogger logger, Exception exception, string actorId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Error,
|
||||
Message = "Write operation failed: ActorId={ActorId}, RequestId={RequestId}")]
|
||||
public static partial void WriteOperationFailed(ILogger logger, string actorId, string requestId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HelloHttpApi.ApiService;
|
||||
|
||||
public class PingResponse(PingResponseStatus status, long timeOfLastUpdate)
|
||||
{
|
||||
[JsonPropertyName("status")]
|
||||
public PingResponseStatus Status { get; } = status;
|
||||
|
||||
[JsonPropertyName("time_of_last_update")]
|
||||
public long TimeOfLastUpdate { get; } = timeOfLastUpdate;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace HelloHttpApi.ApiService;
|
||||
|
||||
public enum PingResponseStatus
|
||||
{
|
||||
Healthy,
|
||||
HealthyBusy,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using HelloHttpApi.ApiService;
|
||||
using HelloHttpApi.ApiService.Utilities;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add service defaults & Aspire client integrations.
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddProblemDetails();
|
||||
|
||||
// Configure the chat model and our agent.
|
||||
builder.AddKeyedChatClient("chat-model");
|
||||
|
||||
builder.AddChatClientAgent(
|
||||
name: "pirate",
|
||||
instructions: "You are a pirate. Speak like a pirate.",
|
||||
chatClientKey: "chat-model");
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
app.UseExceptionHandler();
|
||||
|
||||
// Map the agents HTTP endpoints
|
||||
app.MapAgents();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5390",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7373;http://localhost:5390",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace HelloHttpApi.ApiService.Utilities;
|
||||
|
||||
public class ChatClientConnectionInfo
|
||||
{
|
||||
public Uri? Endpoint { get; init; }
|
||||
public required string SelectedModel { get; init; }
|
||||
|
||||
public ClientChatProvider Provider { get; init; }
|
||||
public string? AccessKey { get; init; }
|
||||
|
||||
// Example connection string:
|
||||
// Endpoint=https://localhost:4523;Model=phi3.5;AccessKey=1234;Provider=ollama;
|
||||
public static bool TryParse(string? connectionString, [NotNullWhen(true)] out ChatClientConnectionInfo? settings)
|
||||
{
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var connectionBuilder = new DbConnectionStringBuilder
|
||||
{
|
||||
ConnectionString = connectionString
|
||||
};
|
||||
|
||||
Uri? endpoint = null;
|
||||
if (connectionBuilder.ContainsKey("Endpoint") && Uri.TryCreate(connectionBuilder["Endpoint"].ToString(), UriKind.Absolute, out endpoint))
|
||||
{
|
||||
}
|
||||
|
||||
string? model = null;
|
||||
if (connectionBuilder.ContainsKey("Model"))
|
||||
{
|
||||
model = (string)connectionBuilder["Model"];
|
||||
}
|
||||
|
||||
string? accessKey = null;
|
||||
if (connectionBuilder.ContainsKey("AccessKey"))
|
||||
{
|
||||
accessKey = (string)connectionBuilder["AccessKey"];
|
||||
}
|
||||
|
||||
var provider = ClientChatProvider.Unknown;
|
||||
if (connectionBuilder.ContainsKey("Provider"))
|
||||
{
|
||||
var providerValue = (string)connectionBuilder["Provider"];
|
||||
Enum.TryParse(providerValue, ignoreCase: true, out provider);
|
||||
}
|
||||
|
||||
if (endpoint is null && provider != ClientChatProvider.OpenAI || model is null || provider == ClientChatProvider.Unknown)
|
||||
{
|
||||
settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
settings = new ChatClientConnectionInfo
|
||||
{
|
||||
Endpoint = endpoint,
|
||||
SelectedModel = model,
|
||||
AccessKey = accessKey,
|
||||
Provider = provider
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ClientChatProvider
|
||||
{
|
||||
Unknown,
|
||||
Ollama,
|
||||
OpenAI,
|
||||
AzureOpenAI,
|
||||
AzureAIInference,
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.Inference;
|
||||
using HelloHttpApi.ApiService.Utilities;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OllamaSharp;
|
||||
|
||||
namespace HelloHttpApi.ApiService.Utilities;
|
||||
|
||||
public static class ChatClientExtensions
|
||||
{
|
||||
public static ChatClientBuilder AddChatClient(this IHostApplicationBuilder builder, string connectionName)
|
||||
{
|
||||
var cs = builder.Configuration.GetConnectionString(connectionName);
|
||||
|
||||
if (!ChatClientConnectionInfo.TryParse(cs, out var connectionInfo))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid connection string: {cs}. Expected format: 'Endpoint=endpoint;AccessKey=your_access_key;Model=model_name;Provider=ollama/openai/azureopenai;'.");
|
||||
}
|
||||
|
||||
var chatClientBuilder = connectionInfo.Provider switch
|
||||
{
|
||||
ClientChatProvider.Ollama => builder.AddOllamaClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.OpenAI => builder.AddOpenAIClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.AzureOpenAI => builder.AddAzureOpenAIClient(connectionName).AddChatClient(connectionInfo.SelectedModel),
|
||||
ClientChatProvider.AzureAIInference => builder.AddAzureInferenceClient(connectionName, connectionInfo),
|
||||
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
|
||||
};
|
||||
|
||||
// Add OpenTelemetry tracing for the ChatClient activity source
|
||||
chatClientBuilder.UseOpenTelemetry().UseLogging();
|
||||
|
||||
builder.Services.AddOpenTelemetry().WithTracing(t => t.AddSource("Experimental.Microsoft.Extensions.AI"));
|
||||
|
||||
return chatClientBuilder;
|
||||
}
|
||||
|
||||
private static ChatClientBuilder AddOpenAIClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
return builder.AddOpenAIClient(connectionName, settings =>
|
||||
{
|
||||
settings.Endpoint = connectionInfo.Endpoint;
|
||||
settings.Key = connectionInfo.AccessKey;
|
||||
})
|
||||
.AddChatClient(connectionInfo.SelectedModel);
|
||||
}
|
||||
|
||||
private static ChatClientBuilder AddAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
return builder.Services.AddChatClient(sp =>
|
||||
{
|
||||
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
|
||||
|
||||
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
|
||||
|
||||
return client.AsIChatClient(connectionInfo.SelectedModel);
|
||||
});
|
||||
}
|
||||
|
||||
private static ChatClientBuilder AddOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
var httpKey = $"{connectionName}_http";
|
||||
|
||||
builder.Services.AddHttpClient(httpKey, c =>
|
||||
{
|
||||
c.BaseAddress = connectionInfo.Endpoint;
|
||||
});
|
||||
|
||||
return builder.Services.AddChatClient(sp =>
|
||||
{
|
||||
// Create a client for the Ollama API using the http client factory
|
||||
var client = sp.GetRequiredService<IHttpClientFactory>().CreateClient(httpKey);
|
||||
|
||||
return new OllamaApiClient(client, connectionInfo.SelectedModel);
|
||||
});
|
||||
}
|
||||
|
||||
public static ChatClientBuilder AddKeyedChatClient(this IHostApplicationBuilder builder, string connectionName)
|
||||
{
|
||||
var cs = builder.Configuration.GetConnectionString(connectionName);
|
||||
|
||||
if (!ChatClientConnectionInfo.TryParse(cs, out var connectionInfo))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid connection string: {cs}. Expected format: 'Endpoint=endpoint;AccessKey=your_access_key;Model=model_name;Provider=ollama/openai/azureopenai;'.");
|
||||
}
|
||||
|
||||
var chatClientBuilder = connectionInfo.Provider switch
|
||||
{
|
||||
ClientChatProvider.Ollama => builder.AddKeyedOllamaClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.OpenAI => builder.AddKeyedOpenAIClient(connectionName, connectionInfo),
|
||||
ClientChatProvider.AzureOpenAI => builder.AddKeyedAzureOpenAIClient(connectionName).AddKeyedChatClient(connectionName, connectionInfo.SelectedModel),
|
||||
ClientChatProvider.AzureAIInference => builder.AddKeyedAzureInferenceClient(connectionName, connectionInfo),
|
||||
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
|
||||
};
|
||||
|
||||
// Add OpenTelemetry tracing for the ChatClient activity source
|
||||
chatClientBuilder.UseOpenTelemetry().UseLogging();
|
||||
|
||||
builder.Services.AddOpenTelemetry().WithTracing(t => t.AddSource("Experimental.Microsoft.Extensions.AI"));
|
||||
|
||||
return chatClientBuilder;
|
||||
}
|
||||
|
||||
private static ChatClientBuilder AddKeyedOpenAIClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
return builder.AddKeyedOpenAIClient(connectionName, settings =>
|
||||
{
|
||||
settings.Endpoint = connectionInfo.Endpoint;
|
||||
settings.Key = connectionInfo.AccessKey;
|
||||
})
|
||||
.AddKeyedChatClient(connectionName, connectionInfo.SelectedModel);
|
||||
}
|
||||
|
||||
private static ChatClientBuilder AddKeyedAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
return builder.Services.AddKeyedChatClient(connectionName, sp =>
|
||||
{
|
||||
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
|
||||
|
||||
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
|
||||
|
||||
return client.AsIChatClient(connectionInfo.SelectedModel);
|
||||
});
|
||||
}
|
||||
|
||||
private static ChatClientBuilder AddKeyedOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
|
||||
{
|
||||
var httpKey = $"{connectionName}_http";
|
||||
|
||||
builder.Services.AddHttpClient(httpKey, c =>
|
||||
{
|
||||
c.BaseAddress = connectionInfo.Endpoint;
|
||||
});
|
||||
|
||||
return builder.Services.AddKeyedChatClient(connectionName, sp =>
|
||||
{
|
||||
// Create a client for the Ollama API using the http client factory
|
||||
var client = sp.GetRequiredService<IHttpClientFactory>().CreateClient(httpKey);
|
||||
|
||||
return new OllamaApiClient(client, connectionInfo.SelectedModel);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user