using System.Text; using System.Threading.RateLimiting; using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.AI; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Svrnty.CQRS; using Svrnty.CQRS.FluentValidation; // Temporarily disabled gRPC (ARM64 Mac build issues) // using Svrnty.CQRS.Grpc; using Svrnty.Sample; using Svrnty.Sample.AI; using Svrnty.Sample.AI.Commands; using Svrnty.Sample.AI.Tools; using Svrnty.Sample.Data; using Svrnty.CQRS.MinimalApi; using Svrnty.CQRS.DynamicQuery; using Svrnty.CQRS.Abstractions; var builder = WebApplication.CreateBuilder(args); // Temporarily disabled gRPC configuration (ARM64 Mac build issues) // Using ASPNETCORE_URLS environment variable for endpoint configuration instead of Kestrel // This avoids HTTPS certificate issues in Docker /* builder.WebHost.ConfigureKestrel(options => { // Port 6001: HTTP/1.1 for HTTP API options.ListenLocalhost(6001, o => o.Protocols = HttpProtocols.Http1); }); */ // Configure Database var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? "Host=localhost;Database=svrnty;Username=postgres;Password=postgres;Include Error Detail=true"; builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); // Configure OpenTelemetry with Langfuse + Prometheus Metrics var langfusePublicKey = builder.Configuration["Langfuse:PublicKey"] ?? ""; var langfuseSecretKey = builder.Configuration["Langfuse:SecretKey"] ?? ""; var langfuseOtlpEndpoint = builder.Configuration["Langfuse:OtlpEndpoint"] ?? "http://localhost:3000/api/public/otel/v1/traces"; var otelBuilder = builder.Services.AddOpenTelemetry() .ConfigureResource(resource => resource .AddService( serviceName: "svrnty-ai-agent", serviceVersion: "1.0.0", serviceInstanceId: Environment.MachineName) .AddAttributes(new Dictionary { ["deployment.environment"] = builder.Environment.EnvironmentName, ["service.namespace"] = "ai-agents", ["host.name"] = Environment.MachineName })); // Add Metrics (always enabled - Prometheus endpoint) otelBuilder.WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddPrometheusExporter(); }); // Add Tracing (only when Langfuse keys are configured) if (!string.IsNullOrEmpty(langfusePublicKey) && !string.IsNullOrEmpty(langfuseSecretKey)) { var authString = Convert.ToBase64String( Encoding.UTF8.GetBytes($"{langfusePublicKey}:{langfuseSecretKey}")); otelBuilder.WithTracing(tracing => { tracing .AddSource("Svrnty.AI.*") .SetSampler(new AlwaysOnSampler()) .AddHttpClientInstrumentation(options => { options.FilterHttpRequestMessage = (req) => !req.RequestUri?.Host.Contains("langfuse") ?? true; }) .AddEntityFrameworkCoreInstrumentation(options => { options.SetDbStatementForText = true; options.SetDbStatementForStoredProcedure = true; }) .AddOtlpExporter(options => { options.Endpoint = new Uri(langfuseOtlpEndpoint); options.Headers = $"Authorization=Basic {authString}"; options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf; }); }); } // Configure Rate Limiting builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create( context => RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(), factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromMinutes(1), QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 10 })); options.OnRejected = async (context, cancellationToken) => { context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; await context.HttpContext.Response.WriteAsJsonAsync(new { error = "Too many requests. Please try again later.", retryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter) ? retryAfter.TotalSeconds : 60 }, cancellationToken); }; }); // IMPORTANT: Register dynamic query dependencies FIRST // (before AddSvrntyCqrs, so gRPC services can find the handlers) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddDynamicQueryWithProvider(); // Register AI Tools builder.Services.AddSingleton(); builder.Services.AddScoped(); // Register Ollama AI client var ollamaBaseUrl = builder.Configuration["Ollama:BaseUrl"] ?? "http://localhost:11434"; builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(ollamaBaseUrl); }); // Register commands and queries with validators builder.Services.AddCommand(); builder.Services.AddCommand(); builder.Services.AddQuery(); // Register AI agent command builder.Services.AddCommand(); // Configure CQRS with fluent API builder.Services.AddSvrntyCqrs(cqrs => { // Temporarily disabled gRPC (ARM64 Mac build issues) /* // Enable gRPC endpoints with reflection cqrs.AddGrpc(grpc => { grpc.EnableReflection(); }); */ // Enable MinimalApi endpoints cqrs.AddMinimalApi(configure => { }); }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configure Health Checks builder.Services.AddHealthChecks() .AddNpgSql(connectionString, name: "postgresql", tags: new[] { "ready", "db" }); var app = builder.Build(); // Run database migrations using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); try { await dbContext.Database.MigrateAsync(); Console.WriteLine("✅ Database migrations applied successfully"); } catch (Exception ex) { Console.WriteLine($"⚠️ Database migration failed: {ex.Message}"); } } // Enable rate limiting app.UseRateLimiter(); // Map all configured CQRS endpoints (gRPC, MinimalApi, and Dynamic Queries) app.UseSvrntyCqrs(); app.UseSwagger(); app.UseSwaggerUI(); // Prometheus metrics endpoint app.MapPrometheusScrapingEndpoint(); // Health check endpoints app.MapHealthChecks("/health"); app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") }); Console.WriteLine("Production-Ready AI Agent with Full Observability (HTTP-Only Mode)"); Console.WriteLine("═══════════════════════════════════════════════════════════"); Console.WriteLine("HTTP API: http://localhost:6001/api/command/* and /api/query/*"); Console.WriteLine("Swagger UI: http://localhost:6001/swagger"); Console.WriteLine("Prometheus Metrics: http://localhost:6001/metrics"); Console.WriteLine("Health Check: http://localhost:6001/health"); Console.WriteLine("═══════════════════════════════════════════════════════════"); Console.WriteLine("Note: gRPC temporarily disabled (ARM64 Mac build issues)"); Console.WriteLine($"Rate Limiting: 100 requests/minute per client"); Console.WriteLine($"Langfuse Tracing: {(!string.IsNullOrEmpty(langfusePublicKey) ? "Enabled" : "Disabled (configure keys in .env)")}"); Console.WriteLine("═══════════════════════════════════════════════════════════"); app.Run();