Files
jbergner 94dbd4ccab
release-tag / release-image (push) Successful in 2m32s
RC-4
2026-08-09 18:41:47 +02:00

849 lines
45 KiB
Go

package config
import (
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
type Config struct {
Mode string
ListenAddr string
BrainPublicURL 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
KnowledgeFullVerifyInterval time.Duration
PersistInterval time.Duration
EnrichInterval time.Duration
EnrichStepDelay time.Duration
EnrichBatchSize int
EnrichAnchors int
ProcessingMode string
ClusterHashBits int
ClusterHashTables int
ClusterCandidatesPerAnchor int
ClusterArticleCandidates int
ClusterReviewEvidence int
ClusterReviewContextChars int
ClusterArticleBatching bool
SimilarityThreshold float64
RelationThreshold float64
VectorGraphEnabled bool
VectorGraphNeighbors int
VectorGraphCandidates int
VectorGraphMinSimilarity float64
VectorGraphMinAffinity float64
VectorGraphLayout bool
VectorGraphReevaluateInterval time.Duration
VectorGraphRelaxLayout bool
VectorGraphLayoutRelaxInterval time.Duration
VectorGraphLayoutBlend float64
VectorGraphLayoutMaxShift float64
VectorGraphOrphanPass bool
VectorGraphOrphanNeighbors int
VectorGraphOrphanCandidates int
VectorGraphOrphanMinSimilarity float64
VectorGraphOrphanMinAffinity float64
VectorGraphAgentOffload bool
VectorGraphAgentRequired bool
VectorGraphAgentWait time.Duration
ThinkingVectorGuided bool
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
ArticleSynthesisModel string
ArticleReviewModel string
ArticleReviewRepairRounds int
ArticleCPUQualityEnabled bool
ArticleCPUQualityAgentOffload bool
ArticleCPUQualityAgentRequired bool
ArticleCPUQualityAgentWait time.Duration
ArticleResearchStrategy string
ArticleAdaptiveInitialQueries int
ArticleAdaptiveInitialFetch int
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
SpeedMode bool
SpeedCPUWorkers int
SpeedGPUInflight int
RuntimeDefaultsConfigured bool
SourceInboxEnabled bool
SourceInboxInterval time.Duration
SourceInboxBatchSize int
SourceInboxMinSimilarity float64
SourceInboxMinPriority float64
SourceInboxNoveltyFloor float64
SourceInboxMinResults int
SourceInboxFreshMaxAge time.Duration
SourceInboxSecurityProactiveEnabled bool
SourceInboxSecurityBatchSize int
SourceInboxSecurityMinPriority float64
SourceInboxSecurityMinConfidence float64
SourceInboxSecurityFetchMinChars int
SourceInboxSecurityResearchResults int
AgentBrainURL string
AgentID string
AgentToken string
AgentConfigFile string
AgentConfigRefresh time.Duration
AgentHTTPTimeout time.Duration
AgentConcurrency int
AgentBatchSize int
AgentAllowPrivate bool
AgentComputeEnabled bool
AgentComputePollInterval time.Duration
AgentComputeMaxBytes int64
AgentDockerControllerEnabled bool
AgentDockerSocket string
AgentDockerComposeBinary string
AgentControllerPollInterval time.Duration
AgentControllerMaxDuration time.Duration
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{
Mode: strings.ToLower(env("BRAIN_MODE", "brain")),
ListenAddr: env("BRAIN_LISTEN_ADDR", ":8090"),
BrainPublicURL: strings.TrimRight(strings.TrimSpace(os.Getenv("BRAIN_PUBLIC_URL")), "/"),
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", 5*time.Minute),
KnowledgeFullVerifyInterval: duration("BRAIN_KNOWLEDGE_FULL_VERIFY_INTERVAL", 6*time.Hour),
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),
ProcessingMode: strings.ToLower(env("BRAIN_PROCESSING_MODE", "precise")),
ClusterHashBits: integer("BRAIN_CLUSTER_HASH_BITS", 24),
ClusterHashTables: integer("BRAIN_CLUSTER_HASH_TABLES", 2),
ClusterCandidatesPerAnchor: integer("BRAIN_CLUSTER_CANDIDATES_PER_ANCHOR", 96),
ClusterArticleCandidates: integer("BRAIN_CLUSTER_ARTICLE_CANDIDATES", 192),
ClusterReviewEvidence: integer("BRAIN_CLUSTER_REVIEW_EVIDENCE", 8),
ClusterReviewContextChars: integer("BRAIN_CLUSTER_REVIEW_CONTEXT_CHARS", 8000),
ClusterArticleBatching: boolean("BRAIN_CLUSTER_ARTICLE_BATCHING", true),
SimilarityThreshold: number("BRAIN_SIMILARITY_THRESHOLD", 0.68),
RelationThreshold: number("BRAIN_RELATION_THRESHOLD", 0.72),
VectorGraphEnabled: boolean("BRAIN_VECTOR_GRAPH_ENABLED", false),
VectorGraphNeighbors: integer("BRAIN_VECTOR_GRAPH_NEIGHBORS", 4),
VectorGraphCandidates: integer("BRAIN_VECTOR_GRAPH_CANDIDATES", 96),
VectorGraphMinSimilarity: number("BRAIN_VECTOR_GRAPH_MIN_SIMILARITY", 0.80),
VectorGraphMinAffinity: number("BRAIN_VECTOR_GRAPH_MIN_AFFINITY", 0.35),
VectorGraphLayout: boolean("BRAIN_VECTOR_GRAPH_LAYOUT", false),
VectorGraphReevaluateInterval: duration("BRAIN_VECTOR_GRAPH_REEVALUATE_INTERVAL", 30*time.Minute),
VectorGraphRelaxLayout: boolean("BRAIN_VECTOR_GRAPH_RELAX_LAYOUT", true),
VectorGraphLayoutRelaxInterval: duration("BRAIN_VECTOR_GRAPH_LAYOUT_RELAX_INTERVAL", 2*time.Hour),
VectorGraphLayoutBlend: number("BRAIN_VECTOR_GRAPH_LAYOUT_BLEND", 0.08),
VectorGraphLayoutMaxShift: number("BRAIN_VECTOR_GRAPH_LAYOUT_MAX_SHIFT", 0.035),
VectorGraphOrphanPass: boolean("BRAIN_VECTOR_GRAPH_ORPHAN_PASS", false),
VectorGraphOrphanNeighbors: integer("BRAIN_VECTOR_GRAPH_ORPHAN_NEIGHBORS", 2),
VectorGraphOrphanCandidates: integer("BRAIN_VECTOR_GRAPH_ORPHAN_CANDIDATES", 256),
VectorGraphOrphanMinSimilarity: number("BRAIN_VECTOR_GRAPH_ORPHAN_MIN_SIMILARITY", 0.80),
VectorGraphOrphanMinAffinity: number("BRAIN_VECTOR_GRAPH_ORPHAN_MIN_AFFINITY", 0.30),
VectorGraphAgentOffload: boolean("BRAIN_VECTOR_GRAPH_AGENT_OFFLOAD", false),
VectorGraphAgentRequired: boolean("BRAIN_VECTOR_GRAPH_AGENT_REQUIRED", false),
VectorGraphAgentWait: duration("BRAIN_VECTOR_GRAPH_AGENT_WAIT", 2*time.Minute),
ThinkingVectorGuided: boolean("BRAIN_THINKING_VECTOR_GUIDED", true),
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")),
ArticleSynthesisModel: strings.TrimSpace(env("BRAIN_ARTICLE_SYNTHESIS_MODEL", env("OLLAMA_CHAT_MODEL", "qwen3:8b"))),
ArticleReviewModel: strings.TrimSpace(env("BRAIN_ARTICLE_REVIEW_MODEL", env("OLLAMA_CHAT_MODEL", "qwen3:8b"))),
ArticleReviewRepairRounds: integer("BRAIN_ARTICLE_REVIEW_REPAIR_ROUNDS", 1),
ArticleCPUQualityEnabled: boolean("BRAIN_ARTICLE_CPU_QUALITY_ENABLED", true),
ArticleCPUQualityAgentOffload: boolean("BRAIN_ARTICLE_CPU_QUALITY_AGENT_OFFLOAD", false),
ArticleCPUQualityAgentRequired: boolean("BRAIN_ARTICLE_CPU_QUALITY_AGENT_REQUIRED", false),
ArticleCPUQualityAgentWait: duration("BRAIN_ARTICLE_CPU_QUALITY_AGENT_WAIT", 20*time.Second),
ArticleResearchStrategy: strings.ToLower(env("BRAIN_ARTICLE_RESEARCH_STRATEGY", "auto")),
ArticleAdaptiveInitialQueries: integer("BRAIN_ARTICLE_ADAPTIVE_INITIAL_QUERIES", 2),
ArticleAdaptiveInitialFetch: integer("BRAIN_ARTICLE_ADAPTIVE_INITIAL_FETCH", 3),
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),
SpeedMode: boolean("BRAIN_SPEED_MODE", false),
SpeedCPUWorkers: integer("BRAIN_SPEED_CPU_TASKS", maxInt(runtime.NumCPU(), 1)),
SpeedGPUInflight: integer("BRAIN_SPEED_GPU_TASKS", 4),
RuntimeDefaultsConfigured: true,
SourceInboxEnabled: boolean("BRAIN_SOURCE_INBOX_ENABLED", true),
SourceInboxInterval: duration("BRAIN_SOURCE_INBOX_INTERVAL", 30*time.Second),
SourceInboxBatchSize: integer("BRAIN_SOURCE_INBOX_BATCH_SIZE", 12),
SourceInboxMinSimilarity: number("BRAIN_SOURCE_INBOX_MIN_SIMILARITY", 0.55),
SourceInboxMinPriority: number("BRAIN_SOURCE_INBOX_MIN_PRIORITY", 0.55),
SourceInboxNoveltyFloor: number("BRAIN_SOURCE_INBOX_NOVELTY_FLOOR", 0.35),
SourceInboxMinResults: integer("BRAIN_SOURCE_INBOX_MIN_RESULTS", 2),
SourceInboxFreshMaxAge: duration("BRAIN_SOURCE_INBOX_FRESH_MAX_AGE", 168*time.Hour),
SourceInboxSecurityProactiveEnabled: boolean("BRAIN_SOURCE_INBOX_SECURITY_PROACTIVE", true),
SourceInboxSecurityBatchSize: integer("BRAIN_SOURCE_INBOX_SECURITY_BATCH_SIZE", 2),
SourceInboxSecurityMinPriority: number("BRAIN_SOURCE_INBOX_SECURITY_MIN_PRIORITY", 0.58),
SourceInboxSecurityMinConfidence: number("BRAIN_SOURCE_INBOX_SECURITY_MIN_CONFIDENCE", 0.72),
SourceInboxSecurityFetchMinChars: integer("BRAIN_SOURCE_INBOX_SECURITY_FETCH_MIN_CHARS", 1800),
SourceInboxSecurityResearchResults: integer("BRAIN_SOURCE_INBOX_SECURITY_RESEARCH_RESULTS", 3),
AgentBrainURL: strings.TrimRight(strings.TrimSpace(os.Getenv("BRAIN_AGENT_BRAIN_URL")), "/"),
AgentID: strings.TrimSpace(os.Getenv("BRAIN_AGENT_ID")),
AgentToken: strings.TrimSpace(os.Getenv("BRAIN_AGENT_TOKEN")),
AgentConfigFile: strings.TrimSpace(os.Getenv("BRAIN_AGENT_CONFIG_FILE")),
AgentConfigRefresh: duration("BRAIN_AGENT_CONFIG_REFRESH", 5*time.Minute),
AgentHTTPTimeout: duration("BRAIN_AGENT_HTTP_TIMEOUT", 30*time.Second),
AgentConcurrency: integer("BRAIN_AGENT_CONCURRENCY", 3),
AgentBatchSize: integer("BRAIN_AGENT_BATCH_SIZE", 50),
AgentAllowPrivate: boolean("BRAIN_AGENT_ALLOW_PRIVATE", false),
AgentComputeEnabled: boolean("BRAIN_AGENT_COMPUTE_ENABLED", true),
AgentComputePollInterval: duration("BRAIN_AGENT_COMPUTE_POLL_INTERVAL", 5*time.Second),
AgentComputeMaxBytes: int64(integer("BRAIN_AGENT_COMPUTE_MAX_BYTES", 134217728)),
AgentDockerControllerEnabled: boolean("BRAIN_AGENT_DOCKER_CONTROLLER_ENABLED", false),
AgentDockerSocket: env("BRAIN_AGENT_DOCKER_SOCKET", "/var/run/docker.sock"),
AgentDockerComposeBinary: env("BRAIN_AGENT_DOCKER_COMPOSE_BINARY", "docker"),
AgentControllerPollInterval: duration("BRAIN_AGENT_CONTROLLER_POLL_INTERVAL", 5*time.Second),
AgentControllerMaxDuration: duration("BRAIN_AGENT_CONTROLLER_MAX_DURATION", 15*time.Minute),
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.Mode != "brain" && cfg.Mode != "agent" {
return Config{}, fmt.Errorf("BRAIN_MODE must be brain or agent")
}
if cfg.Mode == "agent" {
if cfg.AgentConfigRefresh < time.Minute || cfg.AgentConfigRefresh > 24*time.Hour {
return Config{}, fmt.Errorf("BRAIN_AGENT_CONFIG_REFRESH must be between 1m and 24h")
}
if cfg.AgentHTTPTimeout < 5*time.Second || cfg.AgentHTTPTimeout > 5*time.Minute {
return Config{}, fmt.Errorf("BRAIN_AGENT_HTTP_TIMEOUT must be between 5s and 5m")
}
if cfg.AgentConcurrency < 1 || cfg.AgentConcurrency > 16 {
return Config{}, fmt.Errorf("BRAIN_AGENT_CONCURRENCY must be between 1 and 16")
}
if cfg.AgentBatchSize < 1 || cfg.AgentBatchSize > 500 {
return Config{}, fmt.Errorf("BRAIN_AGENT_BATCH_SIZE must be between 1 and 500")
}
if cfg.AgentComputePollInterval < time.Second || cfg.AgentComputePollInterval > 5*time.Minute {
return Config{}, fmt.Errorf("BRAIN_AGENT_COMPUTE_POLL_INTERVAL must be between 1s and 5m")
}
if cfg.AgentComputeMaxBytes < 8<<20 || cfg.AgentComputeMaxBytes > 1<<30 {
return Config{}, fmt.Errorf("BRAIN_AGENT_COMPUTE_MAX_BYTES must be between 8 MiB and 1 GiB")
}
if cfg.AgentControllerPollInterval < time.Second || cfg.AgentControllerPollInterval > 5*time.Minute {
return Config{}, fmt.Errorf("BRAIN_AGENT_CONTROLLER_POLL_INTERVAL must be between 1s and 5m")
}
if cfg.AgentControllerMaxDuration < 5*time.Second || cfg.AgentControllerMaxDuration > 2*time.Hour {
return Config{}, fmt.Errorf("BRAIN_AGENT_CONTROLLER_MAX_DURATION must be between 5s and 2h")
}
if strings.TrimSpace(cfg.AgentDockerSocket) == "" {
return Config{}, fmt.Errorf("BRAIN_AGENT_DOCKER_SOCKET must not be empty")
}
if cfg.AgentConfigFile == "" && (cfg.AgentBrainURL == "" || cfg.AgentID == "" || cfg.AgentToken == "") {
return Config{}, fmt.Errorf("agent mode requires BRAIN_AGENT_BRAIN_URL, BRAIN_AGENT_ID and BRAIN_AGENT_TOKEN or BRAIN_AGENT_CONFIG_FILE")
}
if err := os.MkdirAll(cfg.DataDir, 0o750); err != nil {
return Config{}, err
}
return cfg, nil
}
if cfg.BrainPublicURL != "" {
u, err := url.Parse(cfg.BrainPublicURL)
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") || u.User != nil {
return Config{}, fmt.Errorf("BRAIN_PUBLIC_URL must be an absolute http(s) URL without userinfo")
}
}
if cfg.SourceInboxInterval < 5*time.Second || cfg.SourceInboxInterval > 24*time.Hour {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_INTERVAL must be between 5s and 24h")
}
if cfg.SpeedCPUWorkers < 1 || cfg.SpeedCPUWorkers > 256 {
return Config{}, fmt.Errorf("BRAIN_SPEED_CPU_TASKS must be between 1 and 256")
}
if cfg.SpeedGPUInflight < 1 || cfg.SpeedGPUInflight > 64 {
return Config{}, fmt.Errorf("BRAIN_SPEED_GPU_TASKS must be between 1 and 64")
}
if cfg.SourceInboxBatchSize < 1 || cfg.SourceInboxBatchSize > 100 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_BATCH_SIZE must be between 1 and 100")
}
if cfg.SourceInboxMinSimilarity < 0 || cfg.SourceInboxMinSimilarity > 1 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_MIN_SIMILARITY must be between 0 and 1")
}
if cfg.SourceInboxMinPriority < 0 || cfg.SourceInboxMinPriority > 1 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_MIN_PRIORITY must be between 0 and 1")
}
if cfg.SourceInboxNoveltyFloor < 0 || cfg.SourceInboxNoveltyFloor > 1 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_NOVELTY_FLOOR must be between 0 and 1")
}
if cfg.SourceInboxMinResults < 1 || cfg.SourceInboxMinResults > 20 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_MIN_RESULTS must be between 1 and 20")
}
if cfg.SourceInboxFreshMaxAge < time.Hour || cfg.SourceInboxFreshMaxAge > 365*24*time.Hour {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_FRESH_MAX_AGE must be between 1h and 8760h")
}
if cfg.SourceInboxSecurityBatchSize < 1 || cfg.SourceInboxSecurityBatchSize > 20 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_SECURITY_BATCH_SIZE must be between 1 and 20")
}
if cfg.SourceInboxSecurityMinPriority < 0 || cfg.SourceInboxSecurityMinPriority > 1 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_SECURITY_MIN_PRIORITY must be between 0 and 1")
}
if cfg.SourceInboxSecurityMinConfidence < 0 || cfg.SourceInboxSecurityMinConfidence > 1 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_SECURITY_MIN_CONFIDENCE must be between 0 and 1")
}
if cfg.SourceInboxSecurityFetchMinChars < 160 || cfg.SourceInboxSecurityFetchMinChars > 50000 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_SECURITY_FETCH_MIN_CHARS must be between 160 and 50000")
}
if cfg.SourceInboxSecurityResearchResults < 1 || cfg.SourceInboxSecurityResearchResults > 10 {
return Config{}, fmt.Errorf("BRAIN_SOURCE_INBOX_SECURITY_RESEARCH_RESULTS must be between 1 and 10")
}
if cfg.ScanInterval < 2*time.Second {
return Config{}, fmt.Errorf("BRAIN_SCAN_INTERVAL must be at least 2s")
}
if cfg.KnowledgeFullVerifyInterval != 0 && cfg.KnowledgeFullVerifyInterval < 5*time.Minute {
return Config{}, fmt.Errorf("BRAIN_KNOWLEDGE_FULL_VERIFY_INTERVAL must be 0 or at least 5m")
}
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.VectorGraphNeighbors < 1 || cfg.VectorGraphNeighbors > 16 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_NEIGHBORS must be between 1 and 16")
}
if cfg.VectorGraphCandidates < 32 || cfg.VectorGraphCandidates > 512 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_CANDIDATES must be between 32 and 512")
}
if cfg.VectorGraphCandidates < cfg.VectorGraphNeighbors*4 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_CANDIDATES must be at least four times BRAIN_VECTOR_GRAPH_NEIGHBORS")
}
if cfg.VectorGraphMinSimilarity < 0 || cfg.VectorGraphMinSimilarity > 1 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_MIN_SIMILARITY must be between 0 and 1")
}
if cfg.VectorGraphMinAffinity < 0 || cfg.VectorGraphMinAffinity > 1 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_MIN_AFFINITY must be between 0 and 1")
}
if cfg.VectorGraphOrphanNeighbors < 1 || cfg.VectorGraphOrphanNeighbors > 8 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_ORPHAN_NEIGHBORS must be between 1 and 8")
}
if cfg.VectorGraphOrphanCandidates < 32 || cfg.VectorGraphOrphanCandidates > 1024 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_ORPHAN_CANDIDATES must be between 32 and 1024")
}
if cfg.VectorGraphOrphanCandidates < cfg.VectorGraphOrphanNeighbors*4 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_ORPHAN_CANDIDATES must be at least four times BRAIN_VECTOR_GRAPH_ORPHAN_NEIGHBORS")
}
if cfg.VectorGraphOrphanMinSimilarity < 0 || cfg.VectorGraphOrphanMinSimilarity > 1 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_ORPHAN_MIN_SIMILARITY must be between 0 and 1")
}
if cfg.VectorGraphOrphanMinAffinity < 0 || cfg.VectorGraphOrphanMinAffinity > 1 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_ORPHAN_MIN_AFFINITY must be between 0 and 1")
}
if cfg.VectorGraphAgentRequired && !cfg.VectorGraphAgentOffload {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_AGENT_REQUIRED requires BRAIN_VECTOR_GRAPH_AGENT_OFFLOAD=true")
}
if cfg.VectorGraphAgentWait < 5*time.Second || cfg.VectorGraphAgentWait > 30*time.Minute {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_AGENT_WAIT must be between 5s and 30m")
}
if cfg.ArticleMinSources < 2 || cfg.ArticleMinSources > 20 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_MIN_SOURCES must be between 2 and 20")
}
if cfg.VectorGraphReevaluateInterval < time.Minute || cfg.VectorGraphReevaluateInterval > 24*time.Hour {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_REEVALUATE_INTERVAL must be between 1m and 24h")
}
if cfg.VectorGraphLayoutRelaxInterval < 5*time.Minute || cfg.VectorGraphLayoutRelaxInterval > 168*time.Hour {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_LAYOUT_RELAX_INTERVAL must be between 5m and 168h")
}
if cfg.VectorGraphLayoutBlend < 0 || cfg.VectorGraphLayoutBlend > .5 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_LAYOUT_BLEND must be between 0 and 0.5")
}
if cfg.VectorGraphLayoutMaxShift < 0 || cfg.VectorGraphLayoutMaxShift > .25 {
return Config{}, fmt.Errorf("BRAIN_VECTOR_GRAPH_LAYOUT_MAX_SHIFT must be between 0 and 0.25")
}
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.ArticleCPUQualityAgentRequired && !cfg.ArticleCPUQualityAgentOffload {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_CPU_QUALITY_AGENT_REQUIRED requires BRAIN_ARTICLE_CPU_QUALITY_AGENT_OFFLOAD=true")
}
if cfg.ArticleCPUQualityAgentWait < 2*time.Second || cfg.ArticleCPUQualityAgentWait > 5*time.Minute {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_CPU_QUALITY_AGENT_WAIT must be between 2s and 5m")
}
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")
}
switch cfg.ProcessingMode {
case "precise", "clustered":
default:
return Config{}, fmt.Errorf("BRAIN_PROCESSING_MODE must be precise or clustered")
}
if cfg.ClusterHashBits < 8 || cfg.ClusterHashBits > 63 {
return Config{}, fmt.Errorf("BRAIN_CLUSTER_HASH_BITS must be between 8 and 63")
}
if cfg.ClusterHashTables < 1 || cfg.ClusterHashTables > 4 {
return Config{}, fmt.Errorf("BRAIN_CLUSTER_HASH_TABLES must be between 1 and 4")
}
if cfg.ClusterCandidatesPerAnchor < 16 || cfg.ClusterCandidatesPerAnchor > 2048 {
return Config{}, fmt.Errorf("BRAIN_CLUSTER_CANDIDATES_PER_ANCHOR must be between 16 and 2048")
}
if cfg.ClusterArticleCandidates < 16 || cfg.ClusterArticleCandidates > 4096 {
return Config{}, fmt.Errorf("BRAIN_CLUSTER_ARTICLE_CANDIDATES must be between 16 and 4096")
}
if cfg.ClusterReviewEvidence < 2 || cfg.ClusterReviewEvidence > 64 {
return Config{}, fmt.Errorf("BRAIN_CLUSTER_REVIEW_EVIDENCE must be between 2 and 64")
}
if cfg.ClusterReviewContextChars < 4000 || cfg.ClusterReviewContextChars > 100000 {
return Config{}, fmt.Errorf("BRAIN_CLUSTER_REVIEW_CONTEXT_CHARS must be between 4000 and 100000")
}
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.ArticleSynthesisModel == "" {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_SYNTHESIS_MODEL must not be empty")
}
if cfg.ArticleReviewModel == "" {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_REVIEW_MODEL must not be empty")
}
if cfg.ArticleReviewRepairRounds < 0 || cfg.ArticleReviewRepairRounds > 3 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_REVIEW_REPAIR_ROUNDS must be between 0 and 3")
}
switch cfg.ArticleResearchStrategy {
case "auto", "always", "adaptive", "review_only":
default:
return Config{}, fmt.Errorf("BRAIN_ARTICLE_RESEARCH_STRATEGY must be auto, always, adaptive or review_only")
}
if cfg.ArticleAdaptiveInitialQueries < 1 || cfg.ArticleAdaptiveInitialQueries > 6 {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_ADAPTIVE_INITIAL_QUERIES must be between 1 and 6")
}
if cfg.ArticleAdaptiveInitialFetch < 1 || cfg.ArticleAdaptiveInitialFetch > cfg.ArticleResearchFetchResults {
return Config{}, fmt.Errorf("BRAIN_ARTICLE_ADAPTIVE_INITIAL_FETCH must be between 1 and BRAIN_ARTICLE_RESEARCH_FETCH_RESULTS")
}
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
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}