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.
177 lines
No EOL
5 KiB
Go
177 lines
No EOL
5 KiB
Go
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 0–1; 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))
|
||
} |