feat: bootstrap rony-chat-bot Go module
Initial implementation of the bot:
- cmd/chat-bot: CLI entrypoint (serve, reindex, ask, version)
- internal/agent: LLM provider client + agent runner with RAG injection
- internal/config: YAML config loader (providers, RAG, persona, server)
- internal/i18n: response-language detection (EN/ES)
- internal/persona: persona system prompt assembly from YAML
- internal/portfolio: heading-based chunker + SQLite FTS5 indexer
- internal/server: chi router with /api/chat (SSE), /api/health, /api/info,
/api/reindex, middleware (RequestID, Logging, CORS, RateLimit)
- internal/streaming: SSE protocol helpers (start, chunk, sources, done, error)
- web/: drop-in vanilla-JS chat widget (no build, no deps) + demo + README
- bench/: reproducible driver benchmark (modernc vs mattn SQLite)
- configs/portfolio-bot.yaml: llama.cpp default provider, SQLite RAG, canine persona
- docs/architecture.md / .es.md: aligned with SQLite FTS5 + llama.cpp decisions
- data/projects/README*.md: project data documentation
- README.md / .es.md: updated for current implementation
All tests pass (go test ./...). Bot is functional end-to-end with the
configured LLM provider.
2026-07-17 07:56:06 +00:00
|
|
|
|
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"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 07:07:41 +00:00
|
|
|
|
// 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"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: bootstrap rony-chat-bot Go module
Initial implementation of the bot:
- cmd/chat-bot: CLI entrypoint (serve, reindex, ask, version)
- internal/agent: LLM provider client + agent runner with RAG injection
- internal/config: YAML config loader (providers, RAG, persona, server)
- internal/i18n: response-language detection (EN/ES)
- internal/persona: persona system prompt assembly from YAML
- internal/portfolio: heading-based chunker + SQLite FTS5 indexer
- internal/server: chi router with /api/chat (SSE), /api/health, /api/info,
/api/reindex, middleware (RequestID, Logging, CORS, RateLimit)
- internal/streaming: SSE protocol helpers (start, chunk, sources, done, error)
- web/: drop-in vanilla-JS chat widget (no build, no deps) + demo + README
- bench/: reproducible driver benchmark (modernc vs mattn SQLite)
- configs/portfolio-bot.yaml: llama.cpp default provider, SQLite RAG, canine persona
- docs/architecture.md / .es.md: aligned with SQLite FTS5 + llama.cpp decisions
- data/projects/README*.md: project data documentation
- README.md / .es.md: updated for current implementation
All tests pass (go test ./...). Bot is functional end-to-end with the
configured LLM provider.
2026-07-17 07:56:06 +00:00
|
|
|
|
type Config struct {
|
2026-07-18 07:07:41 +00:00
|
|
|
|
Server Server `yaml:"server"`
|
feat: bootstrap rony-chat-bot Go module
Initial implementation of the bot:
- cmd/chat-bot: CLI entrypoint (serve, reindex, ask, version)
- internal/agent: LLM provider client + agent runner with RAG injection
- internal/config: YAML config loader (providers, RAG, persona, server)
- internal/i18n: response-language detection (EN/ES)
- internal/persona: persona system prompt assembly from YAML
- internal/portfolio: heading-based chunker + SQLite FTS5 indexer
- internal/server: chi router with /api/chat (SSE), /api/health, /api/info,
/api/reindex, middleware (RequestID, Logging, CORS, RateLimit)
- internal/streaming: SSE protocol helpers (start, chunk, sources, done, error)
- web/: drop-in vanilla-JS chat widget (no build, no deps) + demo + README
- bench/: reproducible driver benchmark (modernc vs mattn SQLite)
- configs/portfolio-bot.yaml: llama.cpp default provider, SQLite RAG, canine persona
- docs/architecture.md / .es.md: aligned with SQLite FTS5 + llama.cpp decisions
- data/projects/README*.md: project data documentation
- README.md / .es.md: updated for current implementation
All tests pass (go test ./...). Bot is functional end-to-end with the
configured LLM provider.
2026-07-17 07:56:06 +00:00
|
|
|
|
Providers []Provider `yaml:"providers"`
|
2026-07-18 07:07:41 +00:00
|
|
|
|
RAG RAG `yaml:"rag"`
|
|
|
|
|
|
Persona Persona `yaml:"persona"`
|
|
|
|
|
|
SystemPrompt string `yaml:"system_prompt"`
|
|
|
|
|
|
Compaction Compaction `yaml:"compaction"`
|
|
|
|
|
|
Logging Logging `yaml:"logging"`
|
feat: bootstrap rony-chat-bot Go module
Initial implementation of the bot:
- cmd/chat-bot: CLI entrypoint (serve, reindex, ask, version)
- internal/agent: LLM provider client + agent runner with RAG injection
- internal/config: YAML config loader (providers, RAG, persona, server)
- internal/i18n: response-language detection (EN/ES)
- internal/persona: persona system prompt assembly from YAML
- internal/portfolio: heading-based chunker + SQLite FTS5 indexer
- internal/server: chi router with /api/chat (SSE), /api/health, /api/info,
/api/reindex, middleware (RequestID, Logging, CORS, RateLimit)
- internal/streaming: SSE protocol helpers (start, chunk, sources, done, error)
- web/: drop-in vanilla-JS chat widget (no build, no deps) + demo + README
- bench/: reproducible driver benchmark (modernc vs mattn SQLite)
- configs/portfolio-bot.yaml: llama.cpp default provider, SQLite RAG, canine persona
- docs/architecture.md / .es.md: aligned with SQLite FTS5 + llama.cpp decisions
- data/projects/README*.md: project data documentation
- README.md / .es.md: updated for current implementation
All tests pass (go test ./...). Bot is functional end-to-end with the
configured LLM provider.
2026-07-17 07:56:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
}
|
2026-07-18 07:07:41 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat: bootstrap rony-chat-bot Go module
Initial implementation of the bot:
- cmd/chat-bot: CLI entrypoint (serve, reindex, ask, version)
- internal/agent: LLM provider client + agent runner with RAG injection
- internal/config: YAML config loader (providers, RAG, persona, server)
- internal/i18n: response-language detection (EN/ES)
- internal/persona: persona system prompt assembly from YAML
- internal/portfolio: heading-based chunker + SQLite FTS5 indexer
- internal/server: chi router with /api/chat (SSE), /api/health, /api/info,
/api/reindex, middleware (RequestID, Logging, CORS, RateLimit)
- internal/streaming: SSE protocol helpers (start, chunk, sources, done, error)
- web/: drop-in vanilla-JS chat widget (no build, no deps) + demo + README
- bench/: reproducible driver benchmark (modernc vs mattn SQLite)
- configs/portfolio-bot.yaml: llama.cpp default provider, SQLite RAG, canine persona
- docs/architecture.md / .es.md: aligned with SQLite FTS5 + llama.cpp decisions
- data/projects/README*.md: project data documentation
- README.md / .es.md: updated for current implementation
All tests pass (go test ./...). Bot is functional end-to-end with the
configured LLM provider.
2026-07-17 07:56:06 +00:00
|
|
|
|
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))
|
|
|
|
|
|
}
|