Fixed all 13 code review issues achieving 100/100 quality score: - Cache JsonSerializerOptions in GlobalExceptionHandler (CA1869) - Convert constant arrays to static readonly fields (CA1861) - Add code review infrastructure (Roslynator + SonarScanner) Performance optimizations: - Eliminated allocations in exception handling middleware - Optimized validator array usage in commands - Improved migration index creation efficiency Code review tools: - Added ./code-review-local.sh for local analysis - Added Roslynator CLI configuration - Added comprehensive code review guide Cleanup: - Removed outdated temporary documentation - Updated .gitignore for code review artifacts - Removed .DS_Store files Build status: ✅ 0 errors, 0 warnings Code analysis: ✅ 0 diagnostics found Quality score: 100/100 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
181 lines
6.5 KiB
C#
181 lines
6.5 KiB
C#
using Codex.Dal;
|
|
using Codex.Dal.Entities;
|
|
using Codex.Dal.Enums;
|
|
using Codex.Dal.Services;
|
|
using FluentValidation;
|
|
using OpenHarbor.CQRS.Abstractions;
|
|
|
|
namespace Codex.CQRS.Commands;
|
|
|
|
/// <summary>
|
|
/// Command to create a new AI agent with configuration
|
|
/// </summary>
|
|
public record CreateAgentCommand
|
|
{
|
|
/// <summary>
|
|
/// Display name of the agent
|
|
/// </summary>
|
|
public string Name { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Description of the agent's purpose and capabilities
|
|
/// </summary>
|
|
public string Description { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Type of agent (CodeGenerator, CodeReviewer, etc.)
|
|
/// </summary>
|
|
public AgentType Type { get; init; }
|
|
|
|
/// <summary>
|
|
/// Model provider name (e.g., "openai", "anthropic", "ollama")
|
|
/// </summary>
|
|
public string ModelProvider { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Specific model name (e.g., "gpt-4o", "claude-3.5-sonnet", "codellama:7b")
|
|
/// </summary>
|
|
public string ModelName { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Type of provider (CloudApi, LocalEndpoint, Custom)
|
|
/// </summary>
|
|
public ModelProviderType ProviderType { get; init; }
|
|
|
|
/// <summary>
|
|
/// Model endpoint URL (required for LocalEndpoint, optional for CloudApi)
|
|
/// </summary>
|
|
public string? ModelEndpoint { get; init; }
|
|
|
|
/// <summary>
|
|
/// API key for cloud providers (will be encrypted). Not required for local endpoints.
|
|
/// </summary>
|
|
public string? ApiKey { get; init; }
|
|
|
|
/// <summary>
|
|
/// Temperature parameter for model generation (0.0 to 2.0, default: 0.7)
|
|
/// </summary>
|
|
public double Temperature { get; init; } = 0.7;
|
|
|
|
/// <summary>
|
|
/// Maximum tokens to generate in response (default: 4000)
|
|
/// </summary>
|
|
public int MaxTokens { get; init; } = 4000;
|
|
|
|
/// <summary>
|
|
/// System prompt defining agent behavior and instructions
|
|
/// </summary>
|
|
public string SystemPrompt { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Whether conversation memory is enabled for this agent (default: true)
|
|
/// </summary>
|
|
public bool EnableMemory { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Number of recent messages to include in context (default: 10, range: 1-100)
|
|
/// </summary>
|
|
public int ConversationWindowSize { get; init; } = 10;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for creating a new agent
|
|
/// </summary>
|
|
public class CreateAgentCommandHandler(CodexDbContext dbContext, IEncryptionService encryptionService)
|
|
: ICommandHandler<CreateAgentCommand>
|
|
{
|
|
public async Task HandleAsync(CreateAgentCommand command, CancellationToken cancellationToken = default)
|
|
{
|
|
var agent = new Agent
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = command.Name,
|
|
Description = command.Description,
|
|
Type = command.Type,
|
|
ModelProvider = command.ModelProvider.ToLowerInvariant(),
|
|
ModelName = command.ModelName,
|
|
ProviderType = command.ProviderType,
|
|
ModelEndpoint = command.ModelEndpoint,
|
|
ApiKeyEncrypted = command.ApiKey != null ? encryptionService.Encrypt(command.ApiKey) : null,
|
|
Temperature = command.Temperature,
|
|
MaxTokens = command.MaxTokens,
|
|
SystemPrompt = command.SystemPrompt,
|
|
EnableMemory = command.EnableMemory,
|
|
ConversationWindowSize = command.ConversationWindowSize,
|
|
Status = AgentStatus.Active,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
IsDeleted = false
|
|
};
|
|
|
|
dbContext.Agents.Add(agent);
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validator for CreateAgentCommand
|
|
/// </summary>
|
|
public class CreateAgentCommandValidator : AbstractValidator<CreateAgentCommand>
|
|
{
|
|
private static readonly string[] ValidModelProviders = { "openai", "anthropic", "ollama" };
|
|
|
|
public CreateAgentCommandValidator()
|
|
{
|
|
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");
|
|
|
|
// Cloud API providers require an API key
|
|
RuleFor(x => x.ApiKey)
|
|
.NotEmpty()
|
|
.When(x => x.ProviderType == ModelProviderType.CloudApi)
|
|
.WithMessage("API key is required for cloud API providers");
|
|
|
|
// 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);
|
|
}
|
|
}
|