Extends the Compliance Advisor from a Q&A chatbot into a full drafting engine that can generate, validate, and refine compliance documents within Scope Engine constraints. Includes intent classifier, state projector, constraint enforcer, SOUL templates, Go backend endpoints, and React UI components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
476 lines
17 KiB
Go
476 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/api/handlers"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/audit"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/config"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/dsgvo"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/roadmap"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/workshop"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/portfolio"
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
func main() {
|
|
// Load configuration
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
log.Fatalf("Failed to load configuration: %v", err)
|
|
}
|
|
|
|
// Set Gin mode
|
|
if cfg.IsProduction() {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
|
|
// Connect to database
|
|
ctx := context.Background()
|
|
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to database: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
// Verify connection
|
|
if err := pool.Ping(ctx); err != nil {
|
|
log.Fatalf("Failed to ping database: %v", err)
|
|
}
|
|
log.Println("Connected to database")
|
|
|
|
// Initialize stores
|
|
rbacStore := rbac.NewStore(pool)
|
|
auditStore := audit.NewStore(pool)
|
|
dsgvoStore := dsgvo.NewStore(pool)
|
|
uccaStore := ucca.NewStore(pool)
|
|
escalationStore := ucca.NewEscalationStore(pool)
|
|
roadmapStore := roadmap.NewStore(pool)
|
|
workshopStore := workshop.NewStore(pool)
|
|
portfolioStore := portfolio.NewStore(pool)
|
|
|
|
// Initialize services
|
|
rbacService := rbac.NewService(rbacStore)
|
|
policyEngine := rbac.NewPolicyEngine(rbacService, rbacStore)
|
|
|
|
// Initialize LLM providers
|
|
providerRegistry := llm.NewProviderRegistry(cfg.LLMProvider, cfg.LLMFallbackProvider)
|
|
|
|
// Register Ollama adapter
|
|
ollamaAdapter := llm.NewOllamaAdapter(cfg.OllamaURL, cfg.OllamaDefaultModel)
|
|
providerRegistry.Register(ollamaAdapter)
|
|
|
|
// Register Anthropic adapter if API key is configured
|
|
if cfg.AnthropicAPIKey != "" {
|
|
anthropicAdapter := llm.NewAnthropicAdapter(cfg.AnthropicAPIKey, cfg.AnthropicDefaultModel)
|
|
providerRegistry.Register(anthropicAdapter)
|
|
}
|
|
|
|
// Initialize PII detector
|
|
piiDetector := llm.NewPIIDetectorWithPatterns(llm.AllPIIPatterns())
|
|
|
|
// Initialize access gate
|
|
accessGate := llm.NewAccessGate(policyEngine, piiDetector, providerRegistry)
|
|
|
|
// Initialize audit components
|
|
trailBuilder := audit.NewTrailBuilder(auditStore)
|
|
exporter := audit.NewExporter(auditStore)
|
|
|
|
// Initialize handlers
|
|
rbacHandlers := handlers.NewRBACHandlers(rbacStore, rbacService, policyEngine)
|
|
llmHandlers := handlers.NewLLMHandlers(accessGate, providerRegistry, piiDetector, auditStore, trailBuilder)
|
|
auditHandlers := handlers.NewAuditHandlers(auditStore, exporter)
|
|
dsgvoHandlers := handlers.NewDSGVOHandlers(dsgvoStore)
|
|
uccaHandlers := handlers.NewUCCAHandlers(uccaStore, escalationStore, providerRegistry)
|
|
escalationHandlers := handlers.NewEscalationHandlers(escalationStore, uccaStore)
|
|
roadmapHandlers := handlers.NewRoadmapHandlers(roadmapStore)
|
|
workshopHandlers := handlers.NewWorkshopHandlers(workshopStore)
|
|
portfolioHandlers := handlers.NewPortfolioHandlers(portfolioStore)
|
|
draftingHandlers := handlers.NewDraftingHandlers(accessGate, providerRegistry, piiDetector, auditStore, trailBuilder)
|
|
|
|
// Initialize middleware
|
|
rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine)
|
|
|
|
// Create Gin router
|
|
router := gin.Default()
|
|
|
|
// CORS configuration
|
|
router.Use(cors.New(cors.Config{
|
|
AllowOrigins: cfg.AllowedOrigins,
|
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-User-ID", "X-Tenant-ID", "X-Namespace-ID", "X-Tenant-Slug"},
|
|
ExposeHeaders: []string{"Content-Length", "Content-Disposition"},
|
|
AllowCredentials: true,
|
|
MaxAge: 12 * time.Hour,
|
|
}))
|
|
|
|
// Health check
|
|
router.GET("/health", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "healthy",
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
})
|
|
|
|
// API v1 routes
|
|
v1 := router.Group("/sdk/v1")
|
|
{
|
|
// Public routes (no auth required)
|
|
v1.GET("/health", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
})
|
|
|
|
// Apply user context extraction middleware
|
|
v1.Use(rbacMiddleware.ExtractUserContext())
|
|
|
|
// Tenant routes
|
|
tenants := v1.Group("/tenants")
|
|
{
|
|
tenants.GET("", rbacHandlers.ListTenants)
|
|
tenants.GET("/:id", rbacHandlers.GetTenant)
|
|
tenants.POST("", rbacHandlers.CreateTenant)
|
|
tenants.PUT("/:id", rbacHandlers.UpdateTenant)
|
|
|
|
// Tenant namespaces (use :id to avoid route conflict)
|
|
tenants.GET("/:id/namespaces", rbacHandlers.ListNamespaces)
|
|
tenants.POST("/:id/namespaces", rbacHandlers.CreateNamespace)
|
|
}
|
|
|
|
// Namespace routes
|
|
namespaces := v1.Group("/namespaces")
|
|
{
|
|
namespaces.GET("/:id", rbacHandlers.GetNamespace)
|
|
}
|
|
|
|
// Role routes
|
|
roles := v1.Group("/roles")
|
|
{
|
|
roles.GET("", rbacHandlers.ListRoles)
|
|
roles.GET("/system", rbacHandlers.ListSystemRoles)
|
|
roles.GET("/:id", rbacHandlers.GetRole)
|
|
roles.POST("", rbacHandlers.CreateRole)
|
|
}
|
|
|
|
// User role routes
|
|
userRoles := v1.Group("/user-roles")
|
|
{
|
|
userRoles.POST("", rbacHandlers.AssignRole)
|
|
userRoles.DELETE("/:userId/:roleId", rbacHandlers.RevokeRole)
|
|
userRoles.GET("/:userId", rbacHandlers.GetUserRoles)
|
|
}
|
|
|
|
// Permission routes
|
|
permissions := v1.Group("/permissions")
|
|
{
|
|
permissions.GET("/effective", rbacHandlers.GetEffectivePermissions)
|
|
permissions.GET("/context", rbacHandlers.GetUserContext)
|
|
permissions.GET("/check", rbacHandlers.CheckPermission)
|
|
}
|
|
|
|
// LLM Policy routes
|
|
policies := v1.Group("/llm/policies")
|
|
{
|
|
policies.GET("", rbacHandlers.ListLLMPolicies)
|
|
policies.GET("/:id", rbacHandlers.GetLLMPolicy)
|
|
policies.POST("", rbacHandlers.CreateLLMPolicy)
|
|
policies.PUT("/:id", rbacHandlers.UpdateLLMPolicy)
|
|
policies.DELETE("/:id", rbacHandlers.DeleteLLMPolicy)
|
|
}
|
|
|
|
// LLM routes (require LLM permission)
|
|
llmRoutes := v1.Group("/llm")
|
|
llmRoutes.Use(rbacMiddleware.RequireLLMAccess())
|
|
{
|
|
llmRoutes.POST("/chat", llmHandlers.Chat)
|
|
llmRoutes.POST("/complete", llmHandlers.Complete)
|
|
llmRoutes.GET("/models", llmHandlers.ListModels)
|
|
llmRoutes.GET("/providers/status", llmHandlers.GetProviderStatus)
|
|
llmRoutes.POST("/analyze", llmHandlers.AnalyzeText)
|
|
llmRoutes.POST("/redact", llmHandlers.RedactText)
|
|
}
|
|
|
|
// Audit routes (require audit permission)
|
|
auditRoutes := v1.Group("/audit")
|
|
auditRoutes.Use(rbacMiddleware.RequireAnyPermission(rbac.PermissionAuditAll, rbac.PermissionAuditRead, rbac.PermissionAuditLogRead))
|
|
{
|
|
auditRoutes.GET("/llm", auditHandlers.QueryLLMAudit)
|
|
auditRoutes.GET("/general", auditHandlers.QueryGeneralAudit)
|
|
auditRoutes.GET("/llm-operations", auditHandlers.QueryLLMAudit) // Alias
|
|
auditRoutes.GET("/trail", auditHandlers.QueryGeneralAudit) // Alias
|
|
auditRoutes.GET("/usage", auditHandlers.GetUsageStats)
|
|
auditRoutes.GET("/compliance-report", auditHandlers.GetComplianceReport)
|
|
|
|
// Export routes
|
|
auditRoutes.GET("/export/llm", auditHandlers.ExportLLMAudit)
|
|
auditRoutes.GET("/export/general", auditHandlers.ExportGeneralAudit)
|
|
auditRoutes.GET("/export/compliance", auditHandlers.ExportComplianceReport)
|
|
}
|
|
|
|
// DSGVO routes (Art. 30, 32, 35, 15-22 DSGVO)
|
|
dsgvoRoutes := v1.Group("/dsgvo")
|
|
{
|
|
// Statistics
|
|
dsgvoRoutes.GET("/stats", dsgvoHandlers.GetStats)
|
|
|
|
// VVT - Verarbeitungsverzeichnis (Art. 30)
|
|
vvt := dsgvoRoutes.Group("/processing-activities")
|
|
{
|
|
vvt.GET("", dsgvoHandlers.ListProcessingActivities)
|
|
vvt.GET("/:id", dsgvoHandlers.GetProcessingActivity)
|
|
vvt.POST("", dsgvoHandlers.CreateProcessingActivity)
|
|
vvt.PUT("/:id", dsgvoHandlers.UpdateProcessingActivity)
|
|
vvt.DELETE("/:id", dsgvoHandlers.DeleteProcessingActivity)
|
|
}
|
|
|
|
// TOM - Technische und Organisatorische Maßnahmen (Art. 32)
|
|
tom := dsgvoRoutes.Group("/tom")
|
|
{
|
|
tom.GET("", dsgvoHandlers.ListTOMs)
|
|
tom.GET("/:id", dsgvoHandlers.GetTOM)
|
|
tom.POST("", dsgvoHandlers.CreateTOM)
|
|
}
|
|
|
|
// DSR - Data Subject Requests / Betroffenenrechte (Art. 15-22)
|
|
dsr := dsgvoRoutes.Group("/dsr")
|
|
{
|
|
dsr.GET("", dsgvoHandlers.ListDSRs)
|
|
dsr.GET("/:id", dsgvoHandlers.GetDSR)
|
|
dsr.POST("", dsgvoHandlers.CreateDSR)
|
|
dsr.PUT("/:id", dsgvoHandlers.UpdateDSR)
|
|
}
|
|
|
|
// Retention Policies - Löschfristen (Art. 17)
|
|
retention := dsgvoRoutes.Group("/retention-policies")
|
|
{
|
|
retention.GET("", dsgvoHandlers.ListRetentionPolicies)
|
|
retention.POST("", dsgvoHandlers.CreateRetentionPolicy)
|
|
}
|
|
|
|
// DSFA - Datenschutz-Folgenabschätzung (Art. 35)
|
|
dsfa := dsgvoRoutes.Group("/dsfa")
|
|
{
|
|
dsfa.GET("", dsgvoHandlers.ListDSFAs)
|
|
dsfa.GET("/:id", dsgvoHandlers.GetDSFA)
|
|
dsfa.POST("", dsgvoHandlers.CreateDSFA)
|
|
dsfa.PUT("/:id", dsgvoHandlers.UpdateDSFA)
|
|
dsfa.DELETE("/:id", dsgvoHandlers.DeleteDSFA)
|
|
dsfa.GET("/:id/export", dsgvoHandlers.ExportDSFA)
|
|
}
|
|
|
|
// Export routes
|
|
exports := dsgvoRoutes.Group("/export")
|
|
{
|
|
exports.GET("/vvt", dsgvoHandlers.ExportVVT)
|
|
exports.GET("/tom", dsgvoHandlers.ExportTOM)
|
|
exports.GET("/dsr", dsgvoHandlers.ExportDSR)
|
|
exports.GET("/retention", dsgvoHandlers.ExportRetentionPolicies)
|
|
}
|
|
}
|
|
|
|
// UCCA routes - Use-Case Compliance & Feasibility Advisor
|
|
uccaRoutes := v1.Group("/ucca")
|
|
{
|
|
// Main assessment endpoint
|
|
uccaRoutes.POST("/assess", uccaHandlers.Assess)
|
|
|
|
// Assessment management
|
|
uccaRoutes.GET("/assessments", uccaHandlers.ListAssessments)
|
|
uccaRoutes.GET("/assessments/:id", uccaHandlers.GetAssessment)
|
|
uccaRoutes.DELETE("/assessments/:id", uccaHandlers.DeleteAssessment)
|
|
|
|
// LLM explanation
|
|
uccaRoutes.POST("/assessments/:id/explain", uccaHandlers.Explain)
|
|
|
|
// Catalogs (patterns, examples, rules, controls, problem-solutions)
|
|
uccaRoutes.GET("/patterns", uccaHandlers.ListPatterns)
|
|
uccaRoutes.GET("/examples", uccaHandlers.ListExamples)
|
|
uccaRoutes.GET("/rules", uccaHandlers.ListRules)
|
|
uccaRoutes.GET("/controls", uccaHandlers.ListControls)
|
|
uccaRoutes.GET("/problem-solutions", uccaHandlers.ListProblemSolutions)
|
|
|
|
// Export
|
|
uccaRoutes.GET("/export/:id", uccaHandlers.Export)
|
|
|
|
// Statistics
|
|
uccaRoutes.GET("/stats", uccaHandlers.GetStats)
|
|
|
|
// Wizard routes - Legal Assistant integrated
|
|
uccaRoutes.GET("/wizard/schema", uccaHandlers.GetWizardSchema)
|
|
uccaRoutes.POST("/wizard/ask", uccaHandlers.AskWizardQuestion)
|
|
|
|
// Escalation management (E0-E3 workflow)
|
|
uccaRoutes.GET("/escalations", escalationHandlers.ListEscalations)
|
|
uccaRoutes.GET("/escalations/stats", escalationHandlers.GetEscalationStats)
|
|
uccaRoutes.GET("/escalations/:id", escalationHandlers.GetEscalation)
|
|
uccaRoutes.POST("/escalations", escalationHandlers.CreateEscalation)
|
|
uccaRoutes.POST("/escalations/:id/assign", escalationHandlers.AssignEscalation)
|
|
uccaRoutes.POST("/escalations/:id/review", escalationHandlers.StartReview)
|
|
uccaRoutes.POST("/escalations/:id/decide", escalationHandlers.DecideEscalation)
|
|
|
|
// DSB Pool management
|
|
uccaRoutes.GET("/dsb-pool", escalationHandlers.ListDSBPool)
|
|
uccaRoutes.POST("/dsb-pool", escalationHandlers.AddDSBPoolMember)
|
|
}
|
|
|
|
// Roadmap routes - Compliance Implementation Roadmaps
|
|
roadmapRoutes := v1.Group("/roadmaps")
|
|
{
|
|
// Roadmap CRUD
|
|
roadmapRoutes.POST("", roadmapHandlers.CreateRoadmap)
|
|
roadmapRoutes.GET("", roadmapHandlers.ListRoadmaps)
|
|
roadmapRoutes.GET("/:id", roadmapHandlers.GetRoadmap)
|
|
roadmapRoutes.PUT("/:id", roadmapHandlers.UpdateRoadmap)
|
|
roadmapRoutes.DELETE("/:id", roadmapHandlers.DeleteRoadmap)
|
|
roadmapRoutes.GET("/:id/stats", roadmapHandlers.GetRoadmapStats)
|
|
|
|
// Roadmap items
|
|
roadmapRoutes.POST("/:id/items", roadmapHandlers.CreateItem)
|
|
roadmapRoutes.GET("/:id/items", roadmapHandlers.ListItems)
|
|
|
|
// Import workflow
|
|
roadmapRoutes.POST("/import/upload", roadmapHandlers.UploadImport)
|
|
roadmapRoutes.GET("/import/:jobId", roadmapHandlers.GetImportJob)
|
|
roadmapRoutes.POST("/import/:jobId/confirm", roadmapHandlers.ConfirmImport)
|
|
}
|
|
|
|
// Roadmap item routes (separate group for item-level operations)
|
|
roadmapItemRoutes := v1.Group("/roadmap-items")
|
|
{
|
|
roadmapItemRoutes.GET("/:id", roadmapHandlers.GetItem)
|
|
roadmapItemRoutes.PUT("/:id", roadmapHandlers.UpdateItem)
|
|
roadmapItemRoutes.PATCH("/:id/status", roadmapHandlers.UpdateItemStatus)
|
|
roadmapItemRoutes.DELETE("/:id", roadmapHandlers.DeleteItem)
|
|
}
|
|
|
|
// Workshop routes - Collaborative Compliance Workshops
|
|
workshopRoutes := v1.Group("/workshops")
|
|
{
|
|
// Session CRUD
|
|
workshopRoutes.POST("", workshopHandlers.CreateSession)
|
|
workshopRoutes.GET("", workshopHandlers.ListSessions)
|
|
workshopRoutes.GET("/:id", workshopHandlers.GetSession)
|
|
workshopRoutes.PUT("/:id", workshopHandlers.UpdateSession)
|
|
workshopRoutes.DELETE("/:id", workshopHandlers.DeleteSession)
|
|
|
|
// Session lifecycle
|
|
workshopRoutes.POST("/:id/start", workshopHandlers.StartSession)
|
|
workshopRoutes.POST("/:id/pause", workshopHandlers.PauseSession)
|
|
workshopRoutes.POST("/:id/complete", workshopHandlers.CompleteSession)
|
|
|
|
// Participants
|
|
workshopRoutes.GET("/:id/participants", workshopHandlers.ListParticipants)
|
|
workshopRoutes.PUT("/:id/participants/:participantId", workshopHandlers.UpdateParticipant)
|
|
workshopRoutes.DELETE("/:id/participants/:participantId", workshopHandlers.RemoveParticipant)
|
|
|
|
// Responses
|
|
workshopRoutes.POST("/:id/responses", workshopHandlers.SubmitResponse)
|
|
workshopRoutes.GET("/:id/responses", workshopHandlers.GetResponses)
|
|
|
|
// Comments
|
|
workshopRoutes.POST("/:id/comments", workshopHandlers.AddComment)
|
|
workshopRoutes.GET("/:id/comments", workshopHandlers.GetComments)
|
|
|
|
// Wizard navigation
|
|
workshopRoutes.POST("/:id/advance", workshopHandlers.AdvanceStep)
|
|
workshopRoutes.POST("/:id/goto", workshopHandlers.GoToStep)
|
|
|
|
// Statistics & Summary
|
|
workshopRoutes.GET("/:id/stats", workshopHandlers.GetSessionStats)
|
|
workshopRoutes.GET("/:id/summary", workshopHandlers.GetSessionSummary)
|
|
|
|
// Export
|
|
workshopRoutes.GET("/:id/export", workshopHandlers.ExportSession)
|
|
|
|
// Join by code (public endpoint for joining)
|
|
workshopRoutes.POST("/join/:code", workshopHandlers.JoinSession)
|
|
}
|
|
|
|
// Portfolio routes - AI Use Case Portfolio Management
|
|
portfolioRoutes := v1.Group("/portfolios")
|
|
{
|
|
// Portfolio CRUD
|
|
portfolioRoutes.POST("", portfolioHandlers.CreatePortfolio)
|
|
portfolioRoutes.GET("", portfolioHandlers.ListPortfolios)
|
|
portfolioRoutes.GET("/:id", portfolioHandlers.GetPortfolio)
|
|
portfolioRoutes.PUT("/:id", portfolioHandlers.UpdatePortfolio)
|
|
portfolioRoutes.DELETE("/:id", portfolioHandlers.DeletePortfolio)
|
|
|
|
// Portfolio items
|
|
portfolioRoutes.POST("/:id/items", portfolioHandlers.AddItem)
|
|
portfolioRoutes.GET("/:id/items", portfolioHandlers.ListItems)
|
|
portfolioRoutes.POST("/:id/items/bulk", portfolioHandlers.BulkAddItems)
|
|
portfolioRoutes.DELETE("/:id/items/:itemId", portfolioHandlers.RemoveItem)
|
|
portfolioRoutes.PUT("/:id/items/order", portfolioHandlers.ReorderItems)
|
|
|
|
// Statistics & Activity
|
|
portfolioRoutes.GET("/:id/stats", portfolioHandlers.GetPortfolioStats)
|
|
portfolioRoutes.GET("/:id/activity", portfolioHandlers.GetPortfolioActivity)
|
|
portfolioRoutes.POST("/:id/recalculate", portfolioHandlers.RecalculateMetrics)
|
|
|
|
// Approval workflow
|
|
portfolioRoutes.POST("/:id/submit-review", portfolioHandlers.SubmitForReview)
|
|
portfolioRoutes.POST("/:id/approve", portfolioHandlers.ApprovePortfolio)
|
|
|
|
// Merge & Compare
|
|
portfolioRoutes.POST("/merge", portfolioHandlers.MergePortfolios)
|
|
portfolioRoutes.POST("/compare", portfolioHandlers.ComparePortfolios)
|
|
}
|
|
|
|
// Drafting Engine routes - Compliance Document Drafting & Validation
|
|
draftingRoutes := v1.Group("/drafting")
|
|
draftingRoutes.Use(rbacMiddleware.RequireLLMAccess())
|
|
{
|
|
draftingRoutes.POST("/draft", draftingHandlers.DraftDocument)
|
|
draftingRoutes.POST("/validate", draftingHandlers.ValidateDocument)
|
|
draftingRoutes.GET("/history", draftingHandlers.GetDraftHistory)
|
|
}
|
|
}
|
|
|
|
// Create HTTP server
|
|
srv := &http.Server{
|
|
Addr: ":" + cfg.Port,
|
|
Handler: router,
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 5 * time.Minute, // LLM requests can take longer
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
// Start server in goroutine
|
|
go func() {
|
|
log.Printf("AI Compliance SDK starting on port %s", cfg.Port)
|
|
log.Printf("Environment: %s", cfg.Environment)
|
|
log.Printf("Primary LLM Provider: %s", cfg.LLMProvider)
|
|
log.Printf("Fallback LLM Provider: %s", cfg.LLMFallbackProvider)
|
|
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("Server failed: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Graceful shutdown
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
log.Println("Shutting down server...")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
log.Fatalf("Server forced to shutdown: %v", err)
|
|
}
|
|
|
|
log.Println("Server exited")
|
|
}
|