Files
glpi-neural-brain/internal/config/config.go
T
2026-08-07 08:33:41 +02:00

503 lines
24 KiB
Go

package config
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type Config struct {
ListenAddr string
DataDir string
KnowledgeDirs []string
StagingDirs []string
AgentRunsFiles []string
OllamaURL string
OllamaURLs []string
OllamaNodeNames []string
OllamaNodeWeights []int
OllamaRoutingMode string
OllamaNodeMaxInflight int
OllamaHealthInterval time.Duration
OllamaFailureCooldown time.Duration
OllamaRequestTimeout time.Duration
OllamaFailoverEnabled bool
OllamaFailoverAttempts int
OllamaRequireSameDigest bool
OllamaRequireEmbeddingModel bool
ChatModel string
EmbeddingModel string
SearXNGURL string
ScanInterval time.Duration
PersistInterval time.Duration
EnrichInterval time.Duration
EnrichStepDelay time.Duration
EnrichBatchSize int
EnrichAnchors int
SimilarityThreshold float64
RelationThreshold float64
ArticleSynthesisEnabled bool
ArticleMinSources int
ArticleMaxSources int
ArticleMinProductionRatio float64
ArticleMaxGenerationDepth int
ArticleMinConfidence float64
ArticleMinTextChars int
ArticleMinAnswerChars int
ArticleMaxResearchQueries int
ArticleResearchResults int
ArticleResearchRounds int
ArticleResearchFetchResults int
ArticleResearchExplorationResults int
ArticleResearchPrefetchMinRelevance float64
ArticleResearchMinRelevance float64
ArticleResearchMinQuality float64
ArticleResearchPageMaxBytes int64
ArticleResearchPageMaxChars int
ArticleResearchFetchTimeout time.Duration
ArticleResearchAllowPrivate bool
ArticleLanguage string
ResearchDedupeThreshold float64
ResearchDedupeTTL time.Duration
ResearchOllamaMaxInflight int
ResearchOllamaQueueSize int
AutonomousResearchEnabled bool
AutonomousResearchIdleOnly bool
AutonomousResearchInterval time.Duration
AutonomousResearchTasksPerCycle int
AutonomousResearchMaxTasksPerDay int
AutonomousResearchMaxQueriesPerTask int
AutonomousResearchMaxPagesPerTask int
AutonomousResearchMaxRounds int
AutonomousResearchMinPriority float64
AutonomousResearchCooldown time.Duration
AutonomousResearchLease time.Duration
AutonomousResearchMaxAttempts int
AutonomousResearchQueryTriggers bool
AutonomousResearchOpportunityLimit int
TopK int
MaxContextChars int
AutoEnrich bool
ResearchEnabled bool
APIKey string
LearningEnabled bool
ThinkingEnabled bool
LearningSources []string
DisplaySources []string
ThinkingSources []string
DefaultView string
MaxDisplayNodes int
LowPowerMode bool
RuntimeDefaultsConfigured bool
GLPIKBEnabled bool
GLPIURL string
GLPIAPIVersion string
GLPIClientID string
GLPIClientSecret string
GLPIUsername string
GLPIPassword string
GLPIAllowInsecure bool
GLPITimeout time.Duration
GLPIKBPath string
GLPIKBFilter string
GLPIKBLimit int
GLPIKBSyncInterval time.Duration
GLPIKBSource string
}
func Load() (Config, error) {
dataDir := env("BRAIN_DATA_DIR", "./data")
abs, err := filepath.Abs(dataDir)
if err != nil {
return Config{}, err
}
legacyOllamaURL := strings.TrimRight(env("OLLAMA_URL", "http://localhost:11434"), "/")
ollamaURLs := stringList("OLLAMA_URLS")
if len(ollamaURLs) == 0 {
ollamaURLs = []string{legacyOllamaURL}
}
for i := range ollamaURLs {
ollamaURLs[i] = strings.TrimRight(ollamaURLs[i], "/")
}
cfg := Config{
ListenAddr: env("BRAIN_LISTEN_ADDR", ":8090"),
DataDir: abs,
KnowledgeDirs: paths("BRAIN_KNOWLEDGE_DIRS"),
StagingDirs: paths("BRAIN_STAGING_DIRS"),
AgentRunsFiles: paths("BRAIN_AGENT_RUNS_FILES"),
OllamaURL: legacyOllamaURL,
OllamaURLs: ollamaURLs,
OllamaNodeNames: stringList("OLLAMA_NODE_NAMES"),
OllamaNodeWeights: intList("OLLAMA_NODE_WEIGHTS"),
OllamaRoutingMode: strings.ToLower(env("OLLAMA_ROUTING_MODE", "least_inflight")),
OllamaNodeMaxInflight: integer("OLLAMA_NODE_MAX_INFLIGHT", 1),
OllamaHealthInterval: duration("OLLAMA_NODE_HEALTH_INTERVAL", 15*time.Second),
OllamaFailureCooldown: duration("OLLAMA_NODE_FAILURE_COOLDOWN", 30*time.Second),
OllamaRequestTimeout: duration("OLLAMA_NODE_REQUEST_TIMEOUT", 8*time.Minute),
OllamaFailoverEnabled: boolean("OLLAMA_FAILOVER_ENABLED", true),
OllamaFailoverAttempts: integer("OLLAMA_FAILOVER_ATTEMPTS", 0),
OllamaRequireSameDigest: boolean("OLLAMA_REQUIRE_SAME_MODEL_DIGEST", true),
OllamaRequireEmbeddingModel: boolean("OLLAMA_REQUIRE_EMBEDDING_MODEL", true),
ChatModel: env("OLLAMA_CHAT_MODEL", "qwen3:8b"),
EmbeddingModel: env("OLLAMA_EMBEDDING_MODEL", "embeddinggemma"),
SearXNGURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SEARXNG_URL")), "/"),
ScanInterval: duration("BRAIN_SCAN_INTERVAL", 20*time.Second),
PersistInterval: duration("BRAIN_PERSIST_INTERVAL", 5*time.Minute),
EnrichInterval: duration("BRAIN_ENRICH_INTERVAL", 90*time.Second),
EnrichStepDelay: duration("BRAIN_ENRICH_STEP_DELAY", 3*time.Second),
EnrichBatchSize: integer("BRAIN_ENRICH_BATCH_SIZE", 3),
EnrichAnchors: integer("BRAIN_ENRICH_ANCHORS", 48),
SimilarityThreshold: number("BRAIN_SIMILARITY_THRESHOLD", 0.68),
RelationThreshold: number("BRAIN_RELATION_THRESHOLD", 0.72),
ArticleSynthesisEnabled: boolean("BRAIN_ARTICLE_SYNTHESIS_ENABLED", true),
ArticleMinSources: integer("BRAIN_ARTICLE_MIN_SOURCES", 3),
ArticleMaxSources: integer("BRAIN_ARTICLE_MAX_SOURCES", 8),
ArticleMinProductionRatio: number("BRAIN_ARTICLE_MIN_PRODUCTION_RATIO", 0.70),
ArticleMaxGenerationDepth: integer("BRAIN_ARTICLE_MAX_GENERATION_DEPTH", 2),
ArticleMinConfidence: number("BRAIN_ARTICLE_MIN_CONFIDENCE", 0.74),
ArticleMinTextChars: integer("BRAIN_ARTICLE_MIN_TEXT_CHARS", 180),
ArticleMinAnswerChars: integer("BRAIN_ARTICLE_MIN_ANSWER_CHARS", 420),
ArticleMaxResearchQueries: integer("BRAIN_ARTICLE_MAX_RESEARCH_QUERIES", 6),
ArticleResearchResults: integer("BRAIN_ARTICLE_RESEARCH_RESULTS", 12),
ArticleResearchRounds: integer("BRAIN_ARTICLE_RESEARCH_ROUNDS", 3),
ArticleResearchFetchResults: integer("BRAIN_ARTICLE_RESEARCH_FETCH_RESULTS", 6),
ArticleResearchExplorationResults: integer("BRAIN_ARTICLE_RESEARCH_EXPLORATION_RESULTS", 3),
ArticleResearchPrefetchMinRelevance: number("BRAIN_ARTICLE_RESEARCH_PREFETCH_MIN_RELEVANCE", 0.25),
ArticleResearchMinRelevance: number("BRAIN_ARTICLE_RESEARCH_MIN_RELEVANCE", 0.55),
ArticleResearchMinQuality: number("BRAIN_ARTICLE_RESEARCH_MIN_QUALITY", 0.35),
ArticleResearchPageMaxBytes: int64(integer("BRAIN_ARTICLE_RESEARCH_PAGE_MAX_BYTES", 2097152)),
ArticleResearchPageMaxChars: integer("BRAIN_ARTICLE_RESEARCH_PAGE_MAX_CHARS", 14000),
ArticleResearchFetchTimeout: duration("BRAIN_ARTICLE_RESEARCH_FETCH_TIMEOUT", 20*time.Second),
ArticleResearchAllowPrivate: boolean("BRAIN_ARTICLE_RESEARCH_ALLOW_PRIVATE", false),
ArticleLanguage: strings.TrimSpace(env("BRAIN_ARTICLE_LANGUAGE", "de-DE")),
ResearchDedupeThreshold: number("BRAIN_RESEARCH_DEDUPE_THRESHOLD", 0.92),
ResearchDedupeTTL: duration("BRAIN_RESEARCH_DEDUPE_TTL", 45*time.Minute),
ResearchOllamaMaxInflight: integer("BRAIN_RESEARCH_OLLAMA_MAX_INFLIGHT", 2),
ResearchOllamaQueueSize: integer("BRAIN_RESEARCH_OLLAMA_QUEUE_SIZE", 64),
AutonomousResearchEnabled: boolean("BRAIN_AUTONOMOUS_RESEARCH_ENABLED", false),
AutonomousResearchIdleOnly: boolean("BRAIN_AUTONOMOUS_RESEARCH_IDLE_ONLY", true),
AutonomousResearchInterval: duration("BRAIN_AUTONOMOUS_RESEARCH_INTERVAL", 30*time.Minute),
AutonomousResearchTasksPerCycle: integer("BRAIN_AUTONOMOUS_RESEARCH_TASKS_PER_CYCLE", 1),
AutonomousResearchMaxTasksPerDay: integer("BRAIN_AUTONOMOUS_RESEARCH_MAX_TASKS_PER_DAY", 12),
AutonomousResearchMaxQueriesPerTask: integer("BRAIN_AUTONOMOUS_RESEARCH_MAX_QUERIES_PER_TASK", 6),
AutonomousResearchMaxPagesPerTask: integer("BRAIN_AUTONOMOUS_RESEARCH_MAX_PAGES_PER_TASK", 8),
AutonomousResearchMaxRounds: integer("BRAIN_AUTONOMOUS_RESEARCH_MAX_ROUNDS", 3),
AutonomousResearchMinPriority: number("BRAIN_AUTONOMOUS_RESEARCH_MIN_PRIORITY", 0.65),
AutonomousResearchCooldown: duration("BRAIN_AUTONOMOUS_RESEARCH_COOLDOWN", 168*time.Hour),
AutonomousResearchLease: duration("BRAIN_AUTONOMOUS_RESEARCH_LEASE", 45*time.Minute),
AutonomousResearchMaxAttempts: integer("BRAIN_AUTONOMOUS_RESEARCH_MAX_ATTEMPTS", 3),
AutonomousResearchQueryTriggers: boolean("BRAIN_AUTONOMOUS_RESEARCH_QUERY_TRIGGERS", true),
AutonomousResearchOpportunityLimit: integer("BRAIN_AUTONOMOUS_RESEARCH_OPPORTUNITY_LIMIT", 8),
TopK: integer("BRAIN_TOP_K", 8),
MaxContextChars: integer("BRAIN_MAX_CONTEXT_CHARS", 16000),
AutoEnrich: boolean("BRAIN_AUTO_ENRICH", true),
ResearchEnabled: boolean("BRAIN_RESEARCH_ENABLED", false),
APIKey: strings.TrimSpace(os.Getenv("BRAIN_API_KEY")),
LearningEnabled: boolean("BRAIN_LEARNING_ENABLED", true),
ThinkingEnabled: boolean("BRAIN_THINKING_ENABLED", true),
LearningSources: stringList("BRAIN_LEARNING_SOURCES"),
DisplaySources: stringList("BRAIN_DISPLAY_SOURCES"),
ThinkingSources: stringList("BRAIN_THINKING_SOURCES"),
DefaultView: strings.ToLower(env("BRAIN_DEFAULT_VIEW", "neural")),
MaxDisplayNodes: integer("BRAIN_MAX_DISPLAY_NODES", 0),
LowPowerMode: boolean("BRAIN_LOW_POWER_MODE", false),
RuntimeDefaultsConfigured: true,
GLPIKBEnabled: boolean("GLPI_KB_ENABLED", false),
GLPIURL: strings.TrimRight(strings.TrimSpace(os.Getenv("GLPI_URL")), "/"),
GLPIAPIVersion: env("GLPI_API_VERSION", "v2.3"),
GLPIClientID: strings.TrimSpace(os.Getenv("GLPI_CLIENT_ID")),
GLPIClientSecret: strings.TrimSpace(os.Getenv("GLPI_CLIENT_SECRET")),
GLPIUsername: strings.TrimSpace(os.Getenv("GLPI_USERNAME")),
GLPIPassword: strings.TrimSpace(os.Getenv("GLPI_PASSWORD")),
GLPIAllowInsecure: boolean("GLPI_ALLOW_INSECURE_HTTP", false),
GLPITimeout: duration("GLPI_TIMEOUT", 20*time.Second),
GLPIKBPath: env("GLPI_KB_PATH", "auto"),
GLPIKBFilter: strings.TrimSpace(os.Getenv("GLPI_KB_FILTER")),
GLPIKBLimit: integer("GLPI_KB_LIMIT", 500),
GLPIKBSyncInterval: duration("GLPI_KB_SYNC_INTERVAL", 10*time.Minute),
GLPIKBSource: env("GLPI_KB_SOURCE", "GLPI Knowledge Base"),
}
if cfg.ScanInterval < 2*time.Second {
return Config{}, fmt.Errorf("BRAIN_SCAN_INTERVAL must be at least 2s")
}
if cfg.PersistInterval < 10*time.Second {
return Config{}, fmt.Errorf("BRAIN_PERSIST_INTERVAL must be at least 10s")
}
if cfg.EnrichInterval < 10*time.Second {
return Config{}, fmt.Errorf("BRAIN_ENRICH_INTERVAL must be at least 10s")
}
if cfg.EnrichStepDelay < 0 {
return Config{}, fmt.Errorf("BRAIN_ENRICH_STEP_DELAY must not be negative")
}
if cfg.EnrichBatchSize < 1 || cfg.EnrichBatchSize > 25 {
return Config{}, fmt.Errorf("BRAIN_ENRICH_BATCH_SIZE must be between 1 and 25")
}
if cfg.EnrichAnchors < 1 || cfg.EnrichAnchors > 2048 {
return Config{}, fmt.Errorf("BRAIN_ENRICH_ANCHORS must be between 1 and 2048")
}
if cfg.SimilarityThreshold < 0 || cfg.SimilarityThreshold > 1 {
return Config{}, fmt.Errorf("invalid similarity threshold")
}
if cfg.RelationThreshold < 0 || cfg.RelationThreshold > 1 {
return Config{}, fmt.Errorf("invalid relation threshold")
}
if cfg.ArticleMinSources < 2 || cfg.ArticleMinSources > 20 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MIN_SOURCES must be between 2 and 20")
}
if cfg.ArticleMaxSources < cfg.ArticleMinSources || cfg.ArticleMaxSources > 32 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MAX_SOURCES must be between BRAIN_ARTICLE_MIN_SOURCES and 32")
}
if cfg.ArticleMinProductionRatio < 0.5 || cfg.ArticleMinProductionRatio > 1 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MIN_PRODUCTION_RATIO must be between 0.5 and 1")
}
if cfg.ArticleMaxGenerationDepth < 1 || cfg.ArticleMaxGenerationDepth > 8 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MAX_GENERATION_DEPTH must be between 1 and 8")
}
if cfg.ArticleMinConfidence < 0 || cfg.ArticleMinConfidence > 1 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MIN_CONFIDENCE must be between 0 and 1")
}
if cfg.ArticleMinTextChars < 50 || cfg.ArticleMinTextChars > 10000 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MIN_TEXT_CHARS must be between 50 and 10000")
}
if cfg.ArticleMinAnswerChars < 100 || cfg.ArticleMinAnswerChars > 50000 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MIN_ANSWER_CHARS must be between 100 and 50000")
}
if cfg.ArticleMaxResearchQueries < 1 || cfg.ArticleMaxResearchQueries > 10 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MAX_RESEARCH_QUERIES must be between 1 and 10")
}
if cfg.ArticleResearchResults < 1 || cfg.ArticleResearchResults > 20 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_RESULTS must be between 1 and 20")
}
if cfg.ArticleResearchRounds < 1 || cfg.ArticleResearchRounds > 5 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_ROUNDS must be between 1 and 5")
}
if cfg.ArticleResearchFetchResults < 1 || cfg.ArticleResearchFetchResults > cfg.ArticleResearchResults {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_FETCH_RESULTS must be between 1 and BRAIN_ARTICLE_RESEARCH_RESULTS")
}
if cfg.ArticleResearchExplorationResults < 0 || cfg.ArticleResearchExplorationResults > cfg.ArticleResearchFetchResults {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_EXPLORATION_RESULTS must be between 0 and BRAIN_ARTICLE_RESEARCH_FETCH_RESULTS")
}
if cfg.ArticleResearchPrefetchMinRelevance < 0 || cfg.ArticleResearchPrefetchMinRelevance > 1 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_PREFETCH_MIN_RELEVANCE must be between 0 and 1")
}
if cfg.ArticleResearchMinRelevance < 0 || cfg.ArticleResearchMinRelevance > 1 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_MIN_RELEVANCE must be between 0 and 1")
}
if cfg.ArticleResearchMinQuality < 0 || cfg.ArticleResearchMinQuality > 1 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_MIN_QUALITY must be between 0 and 1")
}
if cfg.ArticleResearchPageMaxBytes < 65536 || cfg.ArticleResearchPageMaxBytes > 16777216 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_PAGE_MAX_BYTES must be between 65536 and 16777216")
}
if cfg.ArticleResearchPageMaxChars < 1000 || cfg.ArticleResearchPageMaxChars > 100000 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_PAGE_MAX_CHARS must be between 1000 and 100000")
}
if cfg.ArticleResearchFetchTimeout < time.Second || cfg.ArticleResearchFetchTimeout > 2*time.Minute {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_FETCH_TIMEOUT must be between 1s and 2m")
}
if cfg.ArticleLanguage == "" || len(cfg.ArticleLanguage) > 32 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_LANGUAGE must be a non-empty language tag")
}
if cfg.ResearchDedupeThreshold < 0.5 || cfg.ResearchDedupeThreshold > 1 {
return Config{}, fmt.Errorf("BRAIN_RESEARCH_DEDUPE_THRESHOLD must be between 0.5 and 1")
}
if cfg.ResearchDedupeTTL < time.Minute || cfg.ResearchDedupeTTL > 24*time.Hour {
return Config{}, fmt.Errorf("BRAIN_RESEARCH_DEDUPE_TTL must be between 1m and 24h")
}
if cfg.ResearchOllamaMaxInflight < 1 || cfg.ResearchOllamaMaxInflight > 32 {
return Config{}, fmt.Errorf("BRAIN_RESEARCH_OLLAMA_MAX_INFLIGHT must be between 1 and 32")
}
if cfg.ResearchOllamaQueueSize < 1 || cfg.ResearchOllamaQueueSize > 4096 {
return Config{}, fmt.Errorf("BRAIN_RESEARCH_OLLAMA_QUEUE_SIZE must be between 1 and 4096")
}
if cfg.AutonomousResearchInterval < time.Minute || cfg.AutonomousResearchInterval > 24*time.Hour {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_INTERVAL must be between 1m and 24h")
}
if cfg.AutonomousResearchTasksPerCycle < 1 || cfg.AutonomousResearchTasksPerCycle > 8 {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_TASKS_PER_CYCLE must be between 1 and 8")
}
if cfg.AutonomousResearchMaxTasksPerDay < 1 || cfg.AutonomousResearchMaxTasksPerDay > 500 {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_MAX_TASKS_PER_DAY must be between 1 and 500")
}
if cfg.AutonomousResearchMaxQueriesPerTask < 1 || cfg.AutonomousResearchMaxQueriesPerTask > 30 {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_MAX_QUERIES_PER_TASK must be between 1 and 30")
}
if cfg.AutonomousResearchMaxPagesPerTask < 1 || cfg.AutonomousResearchMaxPagesPerTask > 50 {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_MAX_PAGES_PER_TASK must be between 1 and 50")
}
if cfg.AutonomousResearchMaxRounds < 1 || cfg.AutonomousResearchMaxRounds > 6 {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_MAX_ROUNDS must be between 1 and 6")
}
if cfg.AutonomousResearchMinPriority < 0 || cfg.AutonomousResearchMinPriority > 1 {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_MIN_PRIORITY must be between 0 and 1")
}
if cfg.AutonomousResearchCooldown < time.Hour || cfg.AutonomousResearchCooldown > 365*24*time.Hour {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_COOLDOWN must be between 1h and 8760h")
}
if cfg.AutonomousResearchLease < 5*time.Minute || cfg.AutonomousResearchLease > 6*time.Hour {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_LEASE must be between 5m and 6h")
}
if cfg.AutonomousResearchMaxAttempts < 1 || cfg.AutonomousResearchMaxAttempts > 10 {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_MAX_ATTEMPTS must be between 1 and 10")
}
if cfg.AutonomousResearchOpportunityLimit < 1 || cfg.AutonomousResearchOpportunityLimit > 64 {
return Config{}, fmt.Errorf("BRAIN_AUTONOMOUS_RESEARCH_OPPORTUNITY_LIMIT must be between 1 and 64")
}
if (cfg.ResearchEnabled || cfg.AutonomousResearchEnabled) && cfg.SearXNGURL == "" {
return Config{}, fmt.Errorf("BRAIN_RESEARCH_ENABLED or BRAIN_AUTONOMOUS_RESEARCH_ENABLED requires SEARXNG_URL")
}
if cfg.DefaultView != "neural" && cfg.DefaultView != "honeycomb" && cfg.DefaultView != "constellation" {
return Config{}, fmt.Errorf("BRAIN_DEFAULT_VIEW must be neural, honeycomb or constellation")
}
if cfg.MaxDisplayNodes < 0 || cfg.MaxDisplayNodes > 500000 {
return Config{}, fmt.Errorf("BRAIN_MAX_DISPLAY_NODES must be between 0 and 500000")
}
if len(cfg.OllamaURLs) < 1 || len(cfg.OllamaURLs) > 64 {
return Config{}, fmt.Errorf("OLLAMA_URLS must contain between 1 and 64 nodes")
}
for _, raw := range cfg.OllamaURLs {
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
return Config{}, fmt.Errorf("invalid Ollama URL %q", raw)
}
}
if len(cfg.OllamaNodeNames) > 0 && len(cfg.OllamaNodeNames) != len(cfg.OllamaURLs) {
return Config{}, fmt.Errorf("OLLAMA_NODE_NAMES must contain one name per OLLAMA_URLS entry")
}
if len(cfg.OllamaNodeWeights) > 0 && len(cfg.OllamaNodeWeights) != len(cfg.OllamaURLs) {
return Config{}, fmt.Errorf("OLLAMA_NODE_WEIGHTS must contain one weight per OLLAMA_URLS entry")
}
for _, weight := range cfg.OllamaNodeWeights {
if weight < 1 || weight > 1000 {
return Config{}, fmt.Errorf("OLLAMA_NODE_WEIGHTS values must be between 1 and 1000")
}
}
switch cfg.OllamaRoutingMode {
case "least_inflight", "round_robin", "weighted", "fastest_recent":
default:
return Config{}, fmt.Errorf("invalid OLLAMA_ROUTING_MODE %q", cfg.OllamaRoutingMode)
}
if cfg.OllamaNodeMaxInflight < 1 || cfg.OllamaNodeMaxInflight > 64 {
return Config{}, fmt.Errorf("OLLAMA_NODE_MAX_INFLIGHT must be between 1 and 64")
}
if cfg.OllamaHealthInterval < time.Second || cfg.OllamaFailureCooldown < time.Second || cfg.OllamaRequestTimeout < time.Second {
return Config{}, fmt.Errorf("Ollama health, cooldown and request timeout must be at least 1s")
}
if cfg.GLPIKBEnabled {
u, err := url.Parse(cfg.GLPIURL)
if err != nil || u.Scheme == "" || u.Host == "" {
return Config{}, fmt.Errorf("GLPI_URL must be a valid absolute URL when GLPI_KB_ENABLED=true")
}
if u.Scheme != "https" && !(u.Scheme == "http" && cfg.GLPIAllowInsecure) {
return Config{}, fmt.Errorf("GLPI_URL must use https unless GLPI_ALLOW_INSECURE_HTTP=true")
}
for name, value := range map[string]string{"GLPI_CLIENT_ID": cfg.GLPIClientID, "GLPI_CLIENT_SECRET": cfg.GLPIClientSecret, "GLPI_USERNAME": cfg.GLPIUsername, "GLPI_PASSWORD": cfg.GLPIPassword} {
if value == "" {
return Config{}, fmt.Errorf("%s is required when GLPI_KB_ENABLED=true", name)
}
}
if cfg.GLPIKBLimit < 1 || cfg.GLPIKBLimit > 5000 {
return Config{}, fmt.Errorf("GLPI_KB_LIMIT must be between 1 and 5000")
}
if cfg.GLPIKBSyncInterval < time.Minute {
return Config{}, fmt.Errorf("GLPI_KB_SYNC_INTERVAL must be at least 1m")
}
if cfg.GLPIKBPath != "auto" && !strings.HasPrefix(cfg.GLPIKBPath, "/") {
return Config{}, fmt.Errorf("GLPI_KB_PATH must be auto or an absolute API path")
}
}
if err := os.MkdirAll(cfg.DataDir, 0o750); err != nil {
return Config{}, err
}
return cfg, nil
}
func env(k, d string) string {
if v := strings.TrimSpace(os.Getenv(k)); v != "" {
return v
}
return d
}
func paths(k string) []string {
var out []string
for _, v := range strings.Split(os.Getenv(k), ",") {
if v = strings.TrimSpace(v); v != "" {
if a, e := filepath.Abs(v); e == nil {
out = append(out, a)
}
}
}
return out
}
func stringList(k string) []string {
var out []string
for _, value := range strings.Split(os.Getenv(k), ",") {
if value = strings.TrimSpace(value); value != "" {
out = append(out, value)
}
}
return out
}
func intList(k string) []int {
var out []int
for _, value := range stringList(k) {
n, err := strconv.Atoi(value)
if err != nil {
out = append(out, 0)
continue
}
out = append(out, n)
}
return out
}
func duration(k string, d time.Duration) time.Duration {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return d
}
x, e := time.ParseDuration(v)
if e != nil {
return d
}
return x
}
func number(k string, d float64) float64 {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return d
}
x, e := strconv.ParseFloat(v, 64)
if e != nil {
return d
}
return x
}
func integer(k string, d int) int {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return d
}
x, e := strconv.Atoi(v)
if e != nil {
return d
}
return x
}
func boolean(k string, d bool) bool {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return d
}
x, e := strconv.ParseBool(v)
if e != nil {
return d
}
return x
}