rony-chat-bot/internal/config/config.go
Victor Hugo Vargas 4c5cad38f8 feat(config): add auto-compaction block and CLI wiring
Adds a configurable compaction section to portfolio-bot.yaml with
threshold_ratio, keep_recent_turns and an optional summary prompt.
Wires the new fields through config.Validate() and cmd/chat-bot/main.go
into agent.Runner.WithCompaction() so the runner can opt in to
auto-compaction at startup.
2026-07-18 00:07:41 -07:00

177 lines
No EOL
5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package config
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
type Server struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
ReadTimeoutMS int `yaml:"read_timeout_ms"`
CORSOrigins []string `yaml:"cors_origins"`
RateLimit RateLimit `yaml:"rate_limit"`
}
type RateLimit struct {
RequestsPerMinute int `yaml:"requests_per_minute"`
Burst int `yaml:"burst"`
}
type Provider struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Model string `yaml:"model,omitempty"`
ModelPath string `yaml:"model_path,omitempty"`
Endpoint string `yaml:"endpoint,omitempty"`
ContextSize int `yaml:"context_size,omitempty"`
MaxTokens int `yaml:"max_tokens,omitempty"`
NGPULayers int `yaml:"n_gpu_layers,omitempty"`
Temperature float32 `yaml:"temperature,omitempty"`
APIKeyEnv string `yaml:"api_key_env,omitempty"`
Default bool `yaml:"default,omitempty"`
}
type RAG struct {
Enabled bool `yaml:"enabled"`
DataPath string `yaml:"data_path"`
ChunkSize int `yaml:"chunk_size"`
ChunkOverlap int `yaml:"chunk_overlap"`
DBPath string `yaml:"db_path"`
TopK int `yaml:"top_k"`
Tokenize string `yaml:"tokenize"`
}
type Persona struct {
Name string `yaml:"name"`
Tone string `yaml:"tone"`
Language string `yaml:"language"`
Constraints []string `yaml:"constraints"`
Intro string `yaml:"intro"`
}
type Logging struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
Output string `yaml:"output"`
}
// Compaction controls automatic context-window management. When the most
// recent request's input tokens exceed threshold_ratio × the provider's
// reported MaxContextWindow, the older portion of the conversation is
// summarized by the LLM and replaced with a single system-role note.
// Recent turns (keep_recent_turns) are always preserved verbatim.
//
// Disabled by default; flip enabled: true in the YAML to opt in. The
// model's MaxContextWindow comes from llm.LLMClient.Capabilities(), so
// the same setting works for llamacpp, ollama, openai and anthropic
// without per-provider configuration.
type Compaction struct {
Enabled bool `yaml:"enabled"`
// ThresholdRatio is the fraction of the context window that, once
// exceeded by the previous turn's input tokens, triggers compaction.
// Range 01; default 0.75 (75% of the window).
ThresholdRatio float64 `yaml:"threshold_ratio"`
// KeepRecentTurns is how many of the most recent user turns (each
// being a user message plus everything up to the next user message)
// to keep verbatim after compaction. Older turns are folded into the
// summary. Default 4.
KeepRecentTurns int `yaml:"keep_recent_turns"`
// SummarySystemPrompt is the instruction sent to the LLM when it
// summarizes the older turns. Empty falls back to a built-in
// bilingual prompt that auto-matches the conversation language.
SummarySystemPrompt string `yaml:"summary_system_prompt"`
}
type Config struct {
Server Server `yaml:"server"`
Providers []Provider `yaml:"providers"`
RAG RAG `yaml:"rag"`
Persona Persona `yaml:"persona"`
SystemPrompt string `yaml:"system_prompt"`
Compaction Compaction `yaml:"compaction"`
Logging Logging `yaml:"logging"`
}
func Load(path string) (*Config, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %s: %w", path, err)
}
expanded := os.ExpandEnv(string(raw))
var cfg Config
if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
if err := cfg.validate(); err != nil {
return nil, err
}
return &cfg, nil
}
func (c *Config) validate() error {
if len(c.Providers) == 0 {
return fmt.Errorf("config: at least one provider must be configured")
}
defaultCount := 0
for _, p := range c.Providers {
if p.Default {
defaultCount++
}
}
if defaultCount == 0 {
c.Providers[0].Default = true
} else if defaultCount > 1 {
return fmt.Errorf("config: multiple providers marked as default")
}
if c.Server.Port == 0 {
c.Server.Port = 7331
}
if c.Server.ReadTimeoutMS == 0 {
c.Server.ReadTimeoutMS = 30000
}
if c.RAG.ChunkSize == 0 {
c.RAG.ChunkSize = 500
}
if c.RAG.ChunkOverlap == 0 {
c.RAG.ChunkOverlap = 50
}
if c.RAG.TopK == 0 {
c.RAG.TopK = 5
}
if c.RAG.Tokenize == "" {
c.RAG.Tokenize = "unicode61"
}
if c.Compaction.Enabled {
if c.Compaction.ThresholdRatio <= 0 || c.Compaction.ThresholdRatio > 1 {
c.Compaction.ThresholdRatio = 0.75
}
if c.Compaction.KeepRecentTurns <= 0 {
c.Compaction.KeepRecentTurns = 4
}
}
return nil
}
func (c *Config) DefaultProvider() *Provider {
for i := range c.Providers {
if c.Providers[i].Default {
return &c.Providers[i]
}
}
return &c.Providers[0]
}
func (c *Config) Addr() string {
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
}
func splitPath(path string) []string {
return strings.Split(path, string(os.PathSeparator))
}