849 lines
45 KiB
Go
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
|
|
}
|