using System.Diagnostics;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Svrnty.CQRS.Abstractions;
using Svrnty.Sample.AI.Tools;
using Svrnty.Sample.Data;
using Svrnty.Sample.Data.Entities;
namespace Svrnty.Sample.AI.Commands;
///
/// Handler for executing AI agent commands with function calling support and full observability
///
public class ExecuteAgentCommandHandler(
IChatClient chatClient,
AgentDbContext dbContext,
MathTool mathTool,
DatabaseQueryTool dbTool,
ILogger logger) : ICommandHandler
{
private static readonly ActivitySource ActivitySource = new("Svrnty.AI.Agent");
private const int MaxFunctionCallIterations = 10; // Prevent infinite loops
public async Task HandleAsync(
ExecuteAgentCommand command,
CancellationToken cancellationToken = default)
{
var conversationId = Guid.NewGuid();
// Start root trace
using var activity = ActivitySource.StartActivity("agent.execute", ActivityKind.Server);
activity?.SetTag("agent.conversation_id", conversationId);
activity?.SetTag("agent.prompt", command.Prompt);
activity?.SetTag("agent.model", "qwen2.5-coder:7b");
try
{
var messages = new List
{
new(ChatRole.User, command.Prompt)
};
// Register available tools
using (var toolActivity = ActivitySource.StartActivity("tools.register"))
{
var tools = new List
{
AIFunctionFactory.Create(mathTool.Add),
AIFunctionFactory.Create(mathTool.Multiply),
AIFunctionFactory.Create(dbTool.GetMonthlyRevenue),
AIFunctionFactory.Create(dbTool.GetRevenueRange),
AIFunctionFactory.Create(dbTool.CountCustomersByState),
AIFunctionFactory.Create(dbTool.CountCustomersByTier),
AIFunctionFactory.Create(dbTool.GetCustomers)
};
toolActivity?.SetTag("tools.count", tools.Count);
toolActivity?.SetTag("tools.names", string.Join(",", tools.Select(t => t.Metadata.Name)));
var options = new ChatOptions
{
ModelId = "qwen2.5-coder:7b",
Tools = tools.Cast().ToList()
};
var functionLookup = tools.ToDictionary(
f => f.Metadata.Name,
f => f,
StringComparer.OrdinalIgnoreCase
);
// Initial AI completion
using (var llmActivity = ActivitySource.StartActivity("llm.completion"))
{
llmActivity?.SetTag("llm.iteration", 0);
var completion = await chatClient.CompleteAsync(messages, options, cancellationToken);
messages.Add(completion.Message);
// Function calling loop
var iterations = 0;
while (completion.Message.Contents.OfType().Any()
&& iterations < MaxFunctionCallIterations)
{
iterations++;
foreach (var functionCall in completion.Message.Contents.OfType())
{
using var funcActivity = ActivitySource.StartActivity($"function.{functionCall.Name}");
funcActivity?.SetTag("function.name", functionCall.Name);
funcActivity?.SetTag("function.arguments", JsonSerializer.Serialize(functionCall.Arguments));
try
{
if (!functionLookup.TryGetValue(functionCall.Name, out var function))
{
throw new InvalidOperationException($"Function '{functionCall.Name}' not found");
}
var result = await function.InvokeAsync(functionCall.Arguments, cancellationToken);
funcActivity?.SetTag("function.result", result?.ToString() ?? "null");
funcActivity?.SetTag("function.success", true);
var toolMessage = new ChatMessage(ChatRole.Tool, result?.ToString() ?? "null");
toolMessage.Contents.Add(new FunctionResultContent(functionCall.CallId, functionCall.Name, result));
messages.Add(toolMessage);
}
catch (Exception ex)
{
funcActivity?.SetTag("function.success", false);
funcActivity?.SetTag("error.message", ex.Message);
var errorMessage = new ChatMessage(ChatRole.Tool, $"Error: {ex.Message}");
errorMessage.Contents.Add(new FunctionResultContent(functionCall.CallId, functionCall.Name, $"Error: {ex.Message}"));
messages.Add(errorMessage);
}
}
using (var nextLlmActivity = ActivitySource.StartActivity("llm.completion"))
{
nextLlmActivity?.SetTag("llm.iteration", iterations);
completion = await chatClient.CompleteAsync(messages, options, cancellationToken);
messages.Add(completion.Message);
}
}
// Store conversation in database
var conversation = new Conversation
{
Id = conversationId,
Messages = messages.Select(m => new ConversationMessage
{
Role = m.Role.ToString(),
Content = m.Text ?? string.Empty,
Timestamp = DateTime.UtcNow
}).ToList()
};
dbContext.Conversations.Add(conversation);
await dbContext.SaveChangesAsync(cancellationToken);
activity?.SetTag("agent.success", true);
activity?.SetTag("agent.iterations", iterations);
activity?.SetTag("agent.response_preview", completion.Message.Text?.Substring(0, Math.Min(100, completion.Message.Text.Length)));
logger.LogInformation("Agent executed successfully for conversation {ConversationId}", conversationId);
return new AgentResponse(
Content: completion.Message.Text ?? "No response",
ConversationId: conversationId
);
}
}
}
catch (Exception ex)
{
activity?.SetTag("agent.success", false);
activity?.SetTag("error.type", ex.GetType().Name);
activity?.SetTag("error.message", ex.Message);
logger.LogError(ex, "Agent execution failed for conversation {ConversationId}", conversationId);
throw;
}
}
}