rony-chat-bot/bench/bench.go
Victor Hugo Vargas f33708534a 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 00:56:06 -07:00

154 lines
No EOL
3.4 KiB
Go

// Package bench holds one-time validation benchmarks used to drive library
// decisions. Run with:
//
// go test -bench=. ./bench/ (modernc only, pure Go)
// CGO_ENABLED=1 go test -bench=. ./bench/ (mattn + modernc, requires gcc)
package bench
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
const schema = `
CREATE VIRTUAL TABLE IF NOT EXISTS portfolio_chunks USING fts5(
id UNINDEXED,
project_id UNINDEXED,
source_file UNINDEXED,
chunk_index UNINDEXED,
content,
tokenize = 'unicode61 remove_diacritics 2'
);
`
var queries = []string{
"rony-llm-agent",
"AI agent harness",
"portfolio projects",
"machine learning",
"CLI tool for development",
"vector database",
"agent loop",
"streaming response",
}
func loadChunks(tb testing.TB, dataPath string) []chunkRow {
tb.Helper()
files, err := filepath.Glob(filepath.Join(dataPath, "*.md"))
if err != nil {
tb.Fatal(err)
}
var rows []chunkRow
for _, f := range files {
b, err := os.ReadFile(f)
if err != nil {
tb.Fatal(err)
}
project := strings.TrimSuffix(filepath.Base(f), ".md")
for i, c := range splitIntoChunks(string(b), 500, 50) {
rows = append(rows, chunkRow{
ID: fmt.Sprintf("%s-chunk-%d", project, i),
ProjectID: project,
Source: f,
Index: i,
Content: c,
})
}
}
return rows
}
type chunkRow struct {
ID, ProjectID, Source string
Index int
Content string
}
func splitIntoChunks(text string, size, overlap int) []string {
if size <= 0 {
return []string{text}
}
if overlap < 0 || overlap >= size {
overlap = size / 10
}
var chunks []string
for i := 0; i < len(text); i += size - overlap {
end := i + size
if end > len(text) {
end = len(text)
}
if i >= end {
break
}
chunks = append(chunks, text[i:end])
if end == len(text) {
break
}
}
return chunks
}
func insertAll(ctx context.Context, db *sql.DB, rows []chunkRow) error {
if _, err := db.ExecContext(ctx, `DELETE FROM portfolio_chunks`); err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.PrepareContext(ctx,
`INSERT INTO portfolio_chunks (id, project_id, source_file, chunk_index, content) VALUES (?,?,?,?,?)`)
if err != nil {
return err
}
defer stmt.Close()
for _, r := range rows {
if _, err := stmt.ExecContext(ctx, r.ID, r.ProjectID, r.Source, r.Index, r.Content); err != nil {
return err
}
}
return tx.Commit()
}
func runQuery(ctx context.Context, db *sql.DB, q string) (int, time.Duration, error) {
start := time.Now()
rows, err := db.QueryContext(ctx, fmt.Sprintf(`
SELECT project_id, source_file, content
FROM portfolio_chunks
WHERE portfolio_chunks MATCH '%s'
ORDER BY bm25(portfolio_chunks)
LIMIT 5
`, sanitizeFTS5(q)))
if err != nil {
return 0, 0, err
}
defer rows.Close()
n := 0
for rows.Next() {
n++
}
return n, time.Since(start), rows.Err()
}
// sanitizeFTS5 is the same simple wrapper used by the production code path
// (see docs/architecture.md §4.4). It keeps the benchmark comparable.
func sanitizeFTS5(q string) string {
tokens := strings.FieldsFunc(strings.ToLower(q), func(r rune) bool {
return !(r == '-' || r == '_' || (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'z') || r > 0x7F)
})
if len(tokens) == 0 {
return `""`
}
for i, t := range tokens {
tokens[i] = `"` + t + `"*`
}
return strings.Join(tokens, " ")
}