- internal/research/searxng.go: SearXNGClient implementing ai.ResearchClient - internal/research/searxng_test.go: httptest mock server tests (4 pass) - internal/netbox/client.go: ListDevicesWithStatus client-side filter - internal/config/config.go: SearXNGURL field with default + env binding
138 lines
5.1 KiB
Go
138 lines
5.1 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/joho/godotenv"
|
|
"github.com/spf13/viper"
|
|
|
|
"git.georgsen.dk/hwlab/internal/ai"
|
|
)
|
|
|
|
type Config struct {
|
|
Host string `mapstructure:"host"`
|
|
Port int `mapstructure:"port"`
|
|
LogLevel string `mapstructure:"log_level"`
|
|
|
|
NetBoxURL string `mapstructure:"netbox_url"`
|
|
NetBoxToken string `mapstructure:"netbox_token"`
|
|
NetBoxTimeoutSeconds int `mapstructure:"netbox_timeout_seconds"`
|
|
|
|
DragonflyURL string `mapstructure:"dragonfly_url"`
|
|
WAQRetryIntervalSeconds int `mapstructure:"waq_retry_interval_seconds"`
|
|
WAQMaxAttempts int `mapstructure:"waq_max_attempts"`
|
|
|
|
QualityGateConfidenceThreshold float64 `mapstructure:"quality_gate_confidence_threshold"`
|
|
|
|
NetBoxDefaultDeviceTypeID int32 `mapstructure:"netbox_default_device_type_id"`
|
|
NetBoxDefaultRoleID int32 `mapstructure:"netbox_default_role_id"`
|
|
NetBoxDefaultSiteID int32 `mapstructure:"netbox_default_site_id"`
|
|
|
|
AI ai.AIConfig `mapstructure:"ai"`
|
|
|
|
SearXNGURL string `mapstructure:"searxng_url"`
|
|
}
|
|
|
|
func Load() (*Config, error) {
|
|
// Load .env file if present (ignore error — .env is optional in production)
|
|
_ = godotenv.Load()
|
|
|
|
v := viper.New()
|
|
|
|
// Set defaults
|
|
v.SetDefault("host", "0.0.0.0")
|
|
v.SetDefault("port", 8080)
|
|
v.SetDefault("log_level", "info")
|
|
v.SetDefault("netbox_timeout_seconds", 10)
|
|
v.SetDefault("waq_retry_interval_seconds", 30)
|
|
v.SetDefault("waq_max_attempts", 5)
|
|
v.SetDefault("quality_gate_confidence_threshold", 0.75)
|
|
v.SetDefault("netbox_default_device_type_id", 1)
|
|
v.SetDefault("netbox_default_role_id", 1)
|
|
v.SetDefault("netbox_default_site_id", 1)
|
|
|
|
// AI tier defaults
|
|
v.SetDefault("ai.tier1.base_url", "http://localhost:8000/v1")
|
|
v.SetDefault("ai.tier1.api_key", "local")
|
|
v.SetDefault("ai.tier1.model", "gemma-4-e4b")
|
|
v.SetDefault("ai.tier1.timeout_seconds", 30)
|
|
v.SetDefault("ai.tier2.base_url", "https://openrouter.ai/api/v1")
|
|
v.SetDefault("ai.tier2.api_key", "")
|
|
v.SetDefault("ai.tier2.model", "google/gemma-3-27b-it")
|
|
v.SetDefault("ai.tier2.timeout_seconds", 60)
|
|
v.SetDefault("ai.tier3.base_url", "https://openrouter.ai/api/v1")
|
|
v.SetDefault("ai.tier3.api_key", "")
|
|
v.SetDefault("ai.tier3.model", "anthropic/claude-opus-4")
|
|
v.SetDefault("ai.tier3.timeout_seconds", 120)
|
|
v.SetDefault("ai.confidence_threshold", 0.75)
|
|
v.SetDefault("ai.quick_add_enabled", false)
|
|
v.SetDefault("ai.quick_add_threshold", 0.90)
|
|
v.SetDefault("searxng_url", "http://10.5.0.129:8080")
|
|
|
|
// Config file
|
|
v.SetConfigName("config")
|
|
v.SetConfigType("json")
|
|
v.AddConfigPath(".")
|
|
v.AddConfigPath("/etc/hwlab")
|
|
|
|
// Environment variables: HWLAB_PORT -> port, HWLAB_NETBOX_URL -> netbox_url, etc.
|
|
v.SetEnvPrefix("HWLAB")
|
|
v.AutomaticEnv()
|
|
|
|
// Explicit bindings ensure AutomaticEnv works correctly with mapstructure v2 unmarshalling.
|
|
_ = v.BindEnv("host", "HWLAB_HOST")
|
|
_ = v.BindEnv("port", "HWLAB_PORT")
|
|
_ = v.BindEnv("log_level", "HWLAB_LOG_LEVEL")
|
|
_ = v.BindEnv("netbox_url", "HWLAB_NETBOX_URL")
|
|
_ = v.BindEnv("netbox_token", "HWLAB_NETBOX_TOKEN")
|
|
_ = v.BindEnv("netbox_timeout_seconds", "HWLAB_NETBOX_TIMEOUT_SECONDS")
|
|
_ = v.BindEnv("dragonfly_url", "HWLAB_DRAGONFLY_URL")
|
|
_ = v.BindEnv("waq_retry_interval_seconds", "HWLAB_WAQ_RETRY_INTERVAL_SECONDS")
|
|
_ = v.BindEnv("waq_max_attempts", "HWLAB_WAQ_MAX_ATTEMPTS")
|
|
_ = v.BindEnv("quality_gate_confidence_threshold", "HWLAB_QUALITY_GATE_CONFIDENCE_THRESHOLD")
|
|
_ = v.BindEnv("netbox_default_device_type_id", "HWLAB_NETBOX_DEFAULT_DEVICE_TYPE_ID")
|
|
_ = v.BindEnv("netbox_default_role_id", "HWLAB_NETBOX_DEFAULT_ROLE_ID")
|
|
_ = v.BindEnv("netbox_default_site_id", "HWLAB_NETBOX_DEFAULT_SITE_ID")
|
|
|
|
// AI env bindings
|
|
_ = v.BindEnv("ai.tier1.base_url", "HWLAB_AI_TIER1_BASE_URL")
|
|
_ = v.BindEnv("ai.tier1.api_key", "HWLAB_AI_TIER1_API_KEY")
|
|
_ = v.BindEnv("ai.tier1.model", "HWLAB_AI_TIER1_MODEL")
|
|
_ = v.BindEnv("ai.tier2.base_url", "HWLAB_AI_TIER2_BASE_URL")
|
|
_ = v.BindEnv("ai.tier2.api_key", "HWLAB_AI_TIER2_API_KEY")
|
|
_ = v.BindEnv("ai.tier2.model", "HWLAB_AI_TIER2_MODEL")
|
|
_ = v.BindEnv("ai.tier3.base_url", "HWLAB_AI_TIER3_BASE_URL")
|
|
_ = v.BindEnv("ai.tier3.api_key", "HWLAB_AI_TIER3_API_KEY")
|
|
_ = v.BindEnv("ai.tier3.model", "HWLAB_AI_TIER3_MODEL")
|
|
_ = v.BindEnv("ai.confidence_threshold", "HWLAB_AI_CONFIDENCE_THRESHOLD")
|
|
_ = v.BindEnv("ai.quick_add_enabled", "HWLAB_AI_QUICK_ADD_ENABLED")
|
|
_ = v.BindEnv("searxng_url", "HWLAB_SEARXNG_URL")
|
|
|
|
// Read primary config file (non-fatal if missing)
|
|
if err := v.ReadInConfig(); err != nil {
|
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
|
return nil, fmt.Errorf("config file: %w", err)
|
|
}
|
|
}
|
|
|
|
// Optionally merge ai_config.json as an override (separate viper instance to avoid
|
|
// conflicts with the primary config file name).
|
|
v2 := viper.New()
|
|
v2.SetConfigName("ai_config")
|
|
v2.SetConfigType("json")
|
|
v2.AddConfigPath(".")
|
|
v2.AddConfigPath("/etc/hwlab")
|
|
if err := v2.ReadInConfig(); err == nil {
|
|
// ai_config.json present — merge AI section into main viper
|
|
if err := v.MergeConfigMap(v2.AllSettings()); err != nil {
|
|
return nil, fmt.Errorf("merge ai_config: %w", err)
|
|
}
|
|
}
|
|
|
|
var cfg Config
|
|
if err := v.Unmarshal(&cfg); err != nil {
|
|
return nil, fmt.Errorf("unmarshal config: %w", err)
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|