package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/example/glpi-ai-agent/internal/agent" "github.com/example/glpi-ai-agent/internal/config" "github.com/example/glpi-ai-agent/internal/contextdata" "github.com/example/glpi-ai-agent/internal/glpi" "github.com/example/glpi-ai-agent/internal/knowledge" "github.com/example/glpi-ai-agent/internal/metrics" "github.com/example/glpi-ai-agent/internal/ollama" "github.com/example/glpi-ai-agent/internal/queue" "github.com/example/glpi-ai-agent/internal/state" "github.com/example/glpi-ai-agent/internal/uptimekuma" webui "github.com/example/glpi-ai-agent/internal/web" ) func main() { cfg, err := config.Load() if err != nil { slog.Error("configuration invalid", "error", err) os.Exit(1) } level := slog.LevelInfo switch strings.ToLower(cfg.LogLevel) { case "debug": level = slog.LevelDebug case "warn": level = slog.LevelWarn case "error": level = slog.LevelError } slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}))) ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() g := glpi.New(cfg.GLPIURL, cfg.GLPIAPIVersion, cfg.GLPIClientID, cfg.GLPIClientSecret, cfg.GLPIUsername, cfg.GLPIPassword, cfg.GLPITimeout) o := ollama.New(cfg.OllamaURL, cfg.OllamaModel, cfg.OllamaEmbeddingModel, cfg.CommunicationLanguage, cfg.CommunicationStyle, cfg.OllamaTimeout, cfg.OllamaNumPredict, cfg.OllamaKeepAlive, cfg.OllamaThink, cfg.OllamaMaxConcurrent) if err := g.ValidateContract(ctx); err != nil { slog.Error("GLPI API contract validation failed", "error", err) os.Exit(1) } if cfg.ContextEnabled { var optionalRoutes []string if cfg.ChangeCalendarEnabled { optionalRoutes = append(optionalRoutes, cfg.GLPIChangePath) } if cfg.UserDeviceContextEnabled { optionalRoutes = append(optionalRoutes, cfg.GLPIUserDevicePaths...) } if err := g.ValidateReadRoutes(ctx, optionalRoutes); err != nil { slog.Error("GLPI context API contract validation failed", "error", err) os.Exit(1) } } st, err := state.Open(cfg.DataDir, 2000) if err != nil { slog.Error("state store initialization failed", "error", err) os.Exit(1) } k, err := knowledge.Load(ctx, cfg.KnowledgeDir, cfg.DataDir, o, cfg.RAGEnabled, cfg.KnowledgeAllowedSources) if err != nil { slog.Error("knowledge store initialization failed", "error", err, "knowledge_dir", cfg.KnowledgeDir, "data_dir", cfg.DataDir, "rag_enabled", cfg.RAGEnabled, ) os.Exit(1) } m := metrics.New() m.SetKnowledgeDocs(k.Count()) q := queue.New(cfg.QueueSize) var kuma *uptimekuma.Client if cfg.UptimeKumaEnabled { kuma = uptimekuma.New(cfg.UptimeKumaURL, cfg.UptimeKumaMode, cfg.UptimeKumaAPIKey, cfg.UptimeKumaTimeout) } contextCollector := contextdata.New(cfg, g, kuma) svc := agent.New(cfg, g, o, k, st, q, m, contextCollector) svc.Start(ctx) web, err := webui.New(cfg, m, st, q) if err != nil { slog.Error("web UI initialization failed", "error", err) os.Exit(1) } srv := webui.Listen(cfg.HTTPAddr, web.Handler()) go func() { slog.Info("web server started", "addr", cfg.HTTPAddr, "dry_run", cfg.DryRun, "auto_reply", cfg.AutoReply) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error("web server failed", "error", err) cancel() } }() <-ctx.Done() shutdownCtx, c := context.WithTimeout(context.Background(), 10*time.Second) defer c() _ = srv.Shutdown(shutdownCtx) slog.Info("shutdown complete") }