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 Langfuse HTTP observability /// public class ExecuteAgentCommandHandler( IChatClient chatClient, AgentDbContext dbContext, MathTool mathTool, DatabaseQueryTool dbTool, ILogger logger, LangfuseHttpClient langfuseClient) : ICommandHandler { private const int MaxFunctionCallIterations = 10; // Prevent infinite loops public async Task HandleAsync( ExecuteAgentCommand command, CancellationToken cancellationToken = default) { var conversationId = Guid.NewGuid(); // Start Langfuse trace (if enabled) LangfuseTrace? trace = null; if (langfuseClient.IsEnabled) { trace = await langfuseClient.CreateTraceAsync("agent-execution", "system"); trace.SetInput(command.Prompt); trace.SetMetadata(new Dictionary { ["conversation_id"] = conversationId.ToString(), ["model"] = "qwen2.5-coder:7b" }); } try { var messages = new List { new(ChatRole.User, command.Prompt) }; // Register available tools 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) }; // Log tool registration to Langfuse if (trace != null) { using var toolSpan = trace.CreateSpan("tools-register"); toolSpan.SetMetadata(new Dictionary { ["tools_count"] = tools.Count, ["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 ChatCompletion completion; try { catch { } if (trace != null) { using var generation = trace.CreateGeneration("llm-completion-0"); generation.SetInput(command.Prompt); completion = await chatClient.CompleteAsync(messages, options, cancellationToken); messages.Add(completion.Message); generation.SetOutput(completion.Message.Text ?? ""); generation.SetMetadata(new Dictionary { ["iteration"] = 0, ["has_function_calls"] = completion.Message.Contents.OfType().Any() }); } else { completion = await chatClient.CompleteAsync(messages, options, cancellationToken); messages.Add(completion.Message); } try { catch { } // Function calling loop var iterations = 0; while (completion.Message.Contents.OfType().Any() && iterations < MaxFunctionCallIterations) { iterations++; foreach (var functionCall in completion.Message.Contents.OfType()) { object? funcResult = null; string? funcError = null; try { if (!functionLookup.TryGetValue(functionCall.Name, out var function)) { throw new InvalidOperationException($"Function '{functionCall.Name}' not found"); } funcResult = await function.InvokeAsync(functionCall.Arguments, cancellationToken); var toolMessage = new ChatMessage(ChatRole.Tool, funcResult?.ToString() ?? "null"); toolMessage.Contents.Add(new FunctionResultContent(functionCall.CallId, functionCall.Name, funcResult)); messages.Add(toolMessage); } catch (Exception ex) { funcError = 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); } // Log function call to Langfuse if (trace != null) { using var funcSpan = trace.CreateSpan($"function-{functionCall.Name}"); funcSpan.SetMetadata(new Dictionary { ["function_name"] = functionCall.Name, ["arguments"] = JsonSerializer.Serialize(functionCall.Arguments), ["result"] = funcResult?.ToString() ?? "null", ["success"] = funcError == null, ["error"] = funcError ?? "" }); } } // Next LLM completion after function calls if (trace != null) { using var nextGeneration = trace.CreateGeneration($"llm-completion-{iterations}"); nextGeneration.SetInput(JsonSerializer.Serialize(messages.TakeLast(5))); completion = await chatClient.CompleteAsync(messages, options, cancellationToken); messages.Add(completion.Message); nextGeneration.SetOutput(completion.Message.Text ?? ""); nextGeneration.SetMetadata(new Dictionary { ["iteration"] = iterations, ["has_function_calls"] = completion.Message.Contents.OfType().Any() }); } else { 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); // Update trace with final output and flush to Langfuse if (trace != null) { trace.SetOutput(completion.Message.Text ?? "No response"); trace.SetMetadata(new Dictionary { ["success"] = true, ["iterations"] = iterations, ["conversation_id"] = conversationId.ToString() }); await trace.FlushAsync(); } logger.LogInformation("Agent executed successfully for conversation {ConversationId}", conversationId); try { catch { } return new AgentResponse( Content: completion.Message.Text ?? "No response", ConversationId: conversationId ); } catch (Exception ex) { try { catch { } // Update trace with error and flush to Langfuse if (trace != null) { trace.SetOutput($"Error: {ex.Message}"); trace.SetMetadata(new Dictionary { ["success"] = false, ["error_type"] = ex.GetType().Name, ["error_message"] = ex.Message }); await trace.FlushAsync(); } logger.LogError(ex, "Agent execution failed for conversation {ConversationId}", conversationId); throw; } } }