using Codex.Dal;
using Codex.Dal.Enums;
using Codex.Dal.Services;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using OpenHarbor.CQRS.Abstractions;
namespace Codex.CQRS.Commands;
///
/// Command to update an existing agent's configuration
///
public record UpdateAgentCommand
{
///
/// ID of the agent to update
///
public Guid Id { get; init; }
///
/// Display name of the agent
///
public string Name { get; init; } = string.Empty;
///
/// Description of the agent's purpose and capabilities
///
public string Description { get; init; } = string.Empty;
///
/// Type of agent (CodeGenerator, CodeReviewer, etc.)
///
public AgentType Type { get; init; }
///
/// Model provider name (e.g., "openai", "anthropic", "ollama")
///
public string ModelProvider { get; init; } = string.Empty;
///
/// Specific model name (e.g., "gpt-4o", "claude-3.5-sonnet", "codellama:7b")
///
public string ModelName { get; init; } = string.Empty;
///
/// Type of provider (CloudApi, LocalEndpoint, Custom)
///
public ModelProviderType ProviderType { get; init; }
///
/// Model endpoint URL (required for LocalEndpoint, optional for CloudApi)
///
public string? ModelEndpoint { get; init; }
///
/// API key for cloud providers (will be encrypted). Leave null to keep existing key.
///
public string? ApiKey { get; init; }
///
/// Temperature parameter for model generation (0.0 to 2.0)
///
public double Temperature { get; init; } = 0.7;
///
/// Maximum tokens to generate in response
///
public int MaxTokens { get; init; } = 4000;
///
/// System prompt defining agent behavior and instructions
///
public string SystemPrompt { get; init; } = string.Empty;
///
/// Whether conversation memory is enabled for this agent
///
public bool EnableMemory { get; init; } = true;
///
/// Number of recent messages to include in context (1-100)
///
public int ConversationWindowSize { get; init; } = 10;
///
/// Agent status
///
public AgentStatus Status { get; init; } = AgentStatus.Active;
}
///
/// Handler for updating an agent
///
public class UpdateAgentCommandHandler(CodexDbContext dbContext, IEncryptionService encryptionService)
: ICommandHandler
{
public async Task HandleAsync(UpdateAgentCommand command, CancellationToken cancellationToken = default)
{
var agent = await dbContext.Agents
.FirstOrDefaultAsync(a => a.Id == command.Id && !a.IsDeleted, cancellationToken);
if (agent == null)
{
throw new InvalidOperationException($"Agent with ID {command.Id} not found or has been deleted");
}
agent.Name = command.Name;
agent.Description = command.Description;
agent.Type = command.Type;
agent.ModelProvider = command.ModelProvider.ToLowerInvariant();
agent.ModelName = command.ModelName;
agent.ProviderType = command.ProviderType;
agent.ModelEndpoint = command.ModelEndpoint;
agent.Temperature = command.Temperature;
agent.MaxTokens = command.MaxTokens;
agent.SystemPrompt = command.SystemPrompt;
agent.EnableMemory = command.EnableMemory;
agent.ConversationWindowSize = command.ConversationWindowSize;
agent.Status = command.Status;
agent.UpdatedAt = DateTime.UtcNow;
// Only update API key if a new one is provided
if (command.ApiKey != null)
{
agent.ApiKeyEncrypted = encryptionService.Encrypt(command.ApiKey);
}
await dbContext.SaveChangesAsync(cancellationToken);
}
}
///
/// Validator for UpdateAgentCommand
///
public class UpdateAgentCommandValidator : AbstractValidator
{
private static readonly string[] ValidModelProviders = { "openai", "anthropic", "ollama" };
public UpdateAgentCommandValidator()
{
RuleFor(x => x.Id)
.NotEmpty().WithMessage("Agent ID is required");
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Agent name is required")
.MaximumLength(200).WithMessage("Agent name must not exceed 200 characters");
RuleFor(x => x.Description)
.NotEmpty().WithMessage("Agent description is required")
.MaximumLength(1000).WithMessage("Agent description must not exceed 1000 characters");
RuleFor(x => x.ModelProvider)
.NotEmpty().WithMessage("Model provider is required")
.MaximumLength(100).WithMessage("Model provider must not exceed 100 characters")
.Must(provider => ValidModelProviders.Contains(provider.ToLowerInvariant()))
.WithMessage("Model provider must be one of: openai, anthropic, ollama");
RuleFor(x => x.ModelName)
.NotEmpty().WithMessage("Model name is required")
.MaximumLength(100).WithMessage("Model name must not exceed 100 characters");
RuleFor(x => x.SystemPrompt)
.NotEmpty().WithMessage("System prompt is required")
.MinimumLength(10).WithMessage("System prompt must be at least 10 characters");
RuleFor(x => x.Temperature)
.InclusiveBetween(0.0, 2.0).WithMessage("Temperature must be between 0.0 and 2.0");
RuleFor(x => x.MaxTokens)
.GreaterThan(0).WithMessage("Max tokens must be greater than 0")
.LessThanOrEqualTo(100000).WithMessage("Max tokens must not exceed 100,000");
RuleFor(x => x.ConversationWindowSize)
.InclusiveBetween(1, 100).WithMessage("Conversation window size must be between 1 and 100");
// Local endpoints require a valid URL
RuleFor(x => x.ModelEndpoint)
.NotEmpty()
.When(x => x.ProviderType == ModelProviderType.LocalEndpoint)
.WithMessage("Model endpoint URL is required for local endpoints");
RuleFor(x => x.ModelEndpoint)
.Must(BeAValidUrl!)
.When(x => !string.IsNullOrWhiteSpace(x.ModelEndpoint))
.WithMessage("Model endpoint must be a valid URL");
}
private static bool BeAValidUrl(string url)
{
return Uri.TryCreate(url, UriKind.Absolute, out var uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
}
}