diff --git a/cmd/api/main.go b/cmd/api/main.go index fd32b60..37f2b7b 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -22,7 +22,7 @@ func main() { cfg, err := config.Load() if err != nil { slog.Warn("load config", "err", err) - cfg = &config.Server{} + cfg = &config.Config{} } level := slog.LevelInfo @@ -38,7 +38,7 @@ func main() { slog.SetDefault(logger) var conn *pgx.Conn - if dsn := cfg.Postgres.DSN; dsn != "" { + if dsn := cfg.Global.VectorDB.DSN(); dsn != "" { logger.Debug("connecting to postgres", "dsn", dsn) conn, err = pgx.Connect(context.Background(), dsn) if err != nil { @@ -50,11 +50,11 @@ func main() { logger.Warn("postgres dsn not provided") } - if addr := cfg.Redis.Addr; addr != "" { + if addr := cfg.Global.Redis.Addr; addr != "" { logger.Debug("connecting to redis", "addr", addr) rdb := redis.NewClient(&redis.Options{ Addr: addr, - Password: cfg.Redis.Password, + Password: cfg.Global.Redis.Password, }) if err := rdb.Ping(context.Background()).Err(); err != nil { logger.Error("redis connect error", "err", err) diff --git a/server/api/askai.go b/server/api/askai.go index 20c43ba..9055dbd 100644 --- a/server/api/askai.go +++ b/server/api/askai.go @@ -45,19 +45,25 @@ func registerAskAIRoutes(r *gin.RouterGroup) { const chutesURL = "https://llm.chutes.ai/v1/chat/completions" type serverConfig struct { - Env map[string]string `yaml:"env"` - Model []string `yaml:"model"` - AskAI struct { - Timeout int `yaml:"timeout"` // seconds - Retries int `yaml:"retries"` - } `yaml:"askai"` + LLM struct { + URL string `yaml:"url"` + Token string `yaml:"token"` + Models []string `yaml:"models"` + } `yaml:"llm"` + API struct { + AskAI struct { + Timeout int `yaml:"timeout"` // seconds + Retries int `yaml:"retries"` + } `yaml:"askai"` + } `yaml:"api"` } -// loadConfig attempts to read CHUTES_API_TOKEN, model, timeout and retries from +// loadConfig attempts to read CHUTES_API_TOKEN, model, URL, timeout and retries from // environment variables, falling back to config/server.yaml. -func loadConfig() (string, string, time.Duration, int) { +func loadConfig() (string, string, string, time.Duration, int) { token := os.Getenv("CHUTES_API_TOKEN") model := os.Getenv("CHUTES_API_MODEL") + url := os.Getenv("CHUTES_API_URL") timeout := 30 * time.Second retries := 3 path := filepath.Join("server", "config", "server.yaml") @@ -66,16 +72,19 @@ func loadConfig() (string, string, time.Duration, int) { var cfg serverConfig if err := yaml.Unmarshal(data, &cfg); err == nil { if token == "" { - token = cfg.Env["CHUTES_API_TOKEN"] + token = cfg.LLM.Token } - if model == "" && len(cfg.Model) > 0 { - model = cfg.Model[0] + if model == "" && len(cfg.LLM.Models) > 0 { + model = cfg.LLM.Models[0] } - if cfg.AskAI.Timeout > 0 { - timeout = time.Duration(cfg.AskAI.Timeout) * time.Second + if url == "" { + url = cfg.LLM.URL } - if cfg.AskAI.Retries > 0 { - retries = cfg.AskAI.Retries + if cfg.API.AskAI.Timeout > 0 { + timeout = time.Duration(cfg.API.AskAI.Timeout) * time.Second + } + if cfg.API.AskAI.Retries > 0 { + retries = cfg.API.AskAI.Retries } } } @@ -88,19 +97,18 @@ func loadConfig() (string, string, time.Duration, int) { if model == "" { model = "deepseek-ai/DeepSeek-R1" } - return token, model, timeout, retries + if url == "" { + url = chutesURL + } + return token, model, url, timeout, retries } // callChutes sends the question to the hosted LLM service and returns the reply. func callChutes(question string) (string, error) { - token, model, timeout, retries := loadConfig() + token, model, url, timeout, retries := loadConfig() if token == "" { return "", errors.New("CHUTES_API_TOKEN not set") } - url := os.Getenv("CHUTES_API_URL") - if url == "" { - url = chutesURL - } reqBody := map[string]interface{}{ "model": model, diff --git a/server/config/config.go b/server/config/config.go index edded20..f0a8c00 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "path/filepath" @@ -16,24 +17,74 @@ type Redis struct { Password string `yaml:"password"` } -type Postgres struct { - DSN string `yaml:"dsn"` +type VectorDB struct { + PGURL string `yaml:"pgurl"` + PGHost string `yaml:"pg_host"` + PGPort int `yaml:"pg_port"` + PGUser string `yaml:"pg_user"` + PGPassword string `yaml:"pg_password"` + PGDBName string `yaml:"pg_db_name"` + PGSSLMode string `yaml:"pg_sslmode"` } -type Server struct { - Log Log `yaml:"log"` - Redis Redis `yaml:"redis"` - Postgres Postgres `yaml:"postgres"` +func (v VectorDB) DSN() string { + if v.PGURL != "" { + return v.PGURL + } + if v.PGHost == "" || v.PGUser == "" || v.PGDBName == "" { + return "" + } + port := v.PGPort + if port == 0 { + port = 5432 + } + ssl := v.PGSSLMode + if ssl == "" { + ssl = "require" + } + return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", v.PGUser, v.PGPassword, v.PGHost, port, v.PGDBName, ssl) } -// Load reads server/config/server.yaml and unmarshals into Server struct. -func Load() (*Server, error) { +type Datasource struct { + Name string `yaml:"name"` + Repo string `yaml:"repo"` + Path string `yaml:"path"` +} + +type Global struct { + Redis Redis `yaml:"redis"` + VectorDB VectorDB `yaml:"vectordb"` + Datasources []Datasource `yaml:"datasources"` +} + +type LLM struct { + URL string `yaml:"url"` + Token string `yaml:"token"` + Models []string `yaml:"models"` +} + +type API struct { + AskAI struct { + Timeout int `yaml:"timeout"` + Retries int `yaml:"retries"` + } `yaml:"askai"` +} + +type Config struct { + Log Log `yaml:"log"` + Global Global `yaml:"global"` + LLM LLM `yaml:"llm"` + API API `yaml:"api"` +} + +// Load reads server/config/server.yaml and unmarshals into Config struct. +func Load() (*Config, error) { path := filepath.Join("server", "config", "server.yaml") b, err := os.ReadFile(path) if err != nil { return nil, err } - var cfg Server + var cfg Config if err := yaml.Unmarshal(b, &cfg); err != nil { return nil, err } diff --git a/server/config/config_test.go b/server/config/config_test.go index 87ae19c..5f8a008 100644 --- a/server/config/config_test.go +++ b/server/config/config_test.go @@ -21,7 +21,10 @@ func TestLoad(t *testing.T) { if err != nil { t.Fatalf("Load returned error: %v", err) } - if cfg.Log.Level != "info" { - t.Fatalf("unexpected log level %q", cfg.Log.Level) + if cfg.Global.Redis.Addr != "127.0.0.1:6479" { + t.Fatalf("unexpected redis addr %q", cfg.Global.Redis.Addr) + } + if cfg.API.AskAI.Timeout != 100 { + t.Fatalf("unexpected askai timeout %d", cfg.API.AskAI.Timeout) } } diff --git a/server/rag/config/runtime.go b/server/rag/config/runtime.go index 01a64de..67abf91 100644 --- a/server/rag/config/runtime.go +++ b/server/rag/config/runtime.go @@ -50,17 +50,26 @@ func (c *Config) ResolveEmbedding() RuntimeEmbedding { return rt } -// ResolveChunking returns chunking configuration with defaults applied. -func (c *Config) ResolveChunking() ChunkingCfg { - ch := c.Chunking - if ch.MaxTokens == 0 { - ch.MaxTokens = 800 +// LoadServer loads global configuration from server/config/server.yaml. +func LoadServer() (*Runtime, error) { + path := filepath.Join("server", "config", "server.yaml") + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cfg struct { + Global Runtime `yaml:"global"` } if ch.OverlapTokens == 0 { ch.OverlapTokens = 80 } - if len(ch.IncludeExts) == 0 { - ch.IncludeExts = []string{".md", ".mdx"} + return &cfg.Global, nil +} + +// ToConfig converts runtime configuration into service configuration. +func (rt *Runtime) ToConfig() *Config { + if rt == nil { + return nil } if len(ch.IgnoreDirs) == 0 { ch.IgnoreDirs = []string{".git", "node_modules", "dist", "build"} diff --git a/server/server.go b/server/server.go index bd6c1be..6d6e41e 100644 --- a/server/server.go +++ b/server/server.go @@ -12,8 +12,11 @@ import ( // Config represents server configuration loaded from YAML. type Config struct { - Env map[string]string `yaml:"env"` - Model []string `yaml:"model"` + LLM struct { + URL string `yaml:"url"` + Token string `yaml:"token"` + Models []string `yaml:"models"` + } `yaml:"llm"` } // cfg holds the loaded configuration. @@ -31,8 +34,14 @@ func loadConfig() { log.Printf("server config parse: %v", err) return } - for k, v := range cfg.Env { - os.Setenv(k, v) + if cfg.LLM.Token != "" { + os.Setenv("CHUTES_API_TOKEN", cfg.LLM.Token) + } + if cfg.LLM.URL != "" { + os.Setenv("CHUTES_API_URL", cfg.LLM.URL) + } + if len(cfg.LLM.Models) > 0 { + os.Setenv("CHUTES_API_MODEL", cfg.LLM.Models[0]) } }