package main import ( "bytes" "context" "database/sql" "encoding/json" "fmt" "os/exec" "strings" "time" ) // DailySummary persisted row. type DailySummary struct { Date string `json:"date"` Summary string `json:"summary"` Prompt string `json:"prompt"` Model string `json:"model"` GeneratedAt string `json:"generated_at"` GeneratedBy *string `json:"generated_by"` } func (db *DB) GetDailySummary(date string) (*DailySummary, error) { row := db.conn.QueryRow(`SELECT date, summary, prompt, model, generated_at, generated_by FROM daily_summaries WHERE date=?`, date) var s DailySummary var by sql.NullString if err := row.Scan(&s.Date, &s.Summary, &s.Prompt, &s.Model, &s.GeneratedAt, &by); err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } if by.Valid { v := by.String s.GeneratedBy = &v } return &s, nil } func (db *DB) UpsertDailySummary(s DailySummary) error { var by any = nil if s.GeneratedBy != nil { by = *s.GeneratedBy } _, err := db.conn.Exec(` INSERT INTO daily_summaries (date, summary, prompt, model, generated_at, generated_by) VALUES (?,?,?,?,?,?) ON CONFLICT(date) DO UPDATE SET summary=excluded.summary, prompt=excluded.prompt, model=excluded.model, generated_at=excluded.generated_at, generated_by=excluded.generated_by `, s.Date, s.Summary, s.Prompt, s.Model, s.GeneratedAt, by) return err } func (db *DB) GetSetting(key string) (string, error) { var v string err := db.conn.QueryRow(`SELECT value FROM settings WHERE key=?`, key).Scan(&v) if err == sql.ErrNoRows { return "", nil } return v, err } func (db *DB) SetSetting(key, value string, by *string) error { now := nowRFC3339() var byArg any = nil if by != nil { byArg = *by } _, err := db.conn.Exec(` INSERT INTO settings (key, value, updated_at, updated_by) VALUES (?,?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at, updated_by=excluded.updated_by `, key, value, now, byArg) return err } // runClaudePrompt executes `claude -p` with the given user prompt; returns // stdout trimmed. Times out via claudeTimeout from chat.go. func runClaudePrompt(ctx context.Context, prompt string) (string, error) { ctx, cancel := context.WithTimeout(ctx, claudeTimeout) defer cancel() cmd := exec.CommandContext(ctx, claudeBinary(), "-p", "--model", claudeModel()) cmd.Stdin = strings.NewReader(prompt) var out, errb bytes.Buffer cmd.Stdout = &out cmd.Stderr = &errb if err := cmd.Run(); err != nil { return "", fmt.Errorf("claude -p failed: %v: %s", err, strings.TrimSpace(errb.String())) } return strings.TrimSpace(out.String()), nil } // BuildDailySummaryPrompt composes the prompt for the LLM by interpolating the // configurable instruction template with the JSON of the report. func BuildDailySummaryPrompt(template string, report *DailyReport) (string, error) { js, err := json.MarshalIndent(report, "", " ") if err != nil { return "", err } return fmt.Sprintf("%s\n\n\n%s\n\n", template, string(js)), nil } // GenerateDailySummary builds the prompt, calls Claude, persists and returns // the resulting summary. actorID is optional (empty = anon/system). func (db *DB) GenerateDailySummary(ctx context.Context, date, tz, actorID string) (*DailySummary, error) { rep, err := db.DailyReportFor(date, tz) if err != nil { return nil, err } tmpl, err := db.GetSetting("daily_report_prompt") if err != nil { return nil, err } if tmpl == "" { tmpl = "Resume el reporte diario en 4 frases cortas, en castellano, sin inventar datos." } prompt, err := BuildDailySummaryPrompt(tmpl, rep) if err != nil { return nil, err } summary, err := runClaudePrompt(ctx, prompt) if err != nil { return nil, err } rec := DailySummary{ Date: date, Summary: summary, Prompt: tmpl, Model: claudeModel(), GeneratedAt: time.Now().UTC().Format(time.RFC3339Nano), } if actorID != "" { rec.GeneratedBy = &actorID } if err := db.UpsertDailySummary(rec); err != nil { return nil, err } return &rec, nil }