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;
}
}
}