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 { 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 => new[] { "openai", "anthropic", "ollama" }.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); } }