package insight import ( "context" "fmt" "strings" "go.kenn.io/agentsview/internal/db" ) const maxSessions = 50 // GenerateRequest describes what insight to generate. type GenerateRequest struct { Type string DateFrom string DateTo string Project string Prompt string SessionID string AutomatedScope string Summary *RangeSummary // non-nil only for multi-day ranges } // BuildPrompt queries sessions for the given date and assembles // a prompt for the AI agent. func BuildPrompt( ctx context.Context, database db.Store, req GenerateRequest, ) (string, error) { if req.SessionID != "" { return buildSessionPrompt(ctx, database, req) } automatedScope := req.AutomatedScope if automatedScope == "" { automatedScope = "human" } filter := db.SessionFilter{ DateFrom: req.DateFrom, DateTo: req.DateTo, Limit: maxSessions + 1, AutomatedScope: automatedScope, } if req.Project != "" { filter.Project = req.Project } page, err := database.ListSessions(ctx, filter) if err != nil { return "", fmt.Errorf("querying sessions: %w", err) } var b strings.Builder writeSystemInstruction(&b, req.Type) if req.DateFrom == req.DateTo { b.WriteString("\n## Date: ") b.WriteString(req.DateFrom) } else { b.WriteString("\n## Date Range: ") b.WriteString(req.DateFrom) b.WriteString(" to ") b.WriteString(req.DateTo) } b.WriteString("\n\n") if req.Project != "" { b.WriteString("## Project: ") b.WriteString(req.Project) b.WriteString("\n\n") } if req.Summary != nil { req.Summary.WriteTo(&b) } sessions := page.Sessions truncated := len(sessions) > maxSessions if truncated { sessions = sessions[:maxSessions] } b.WriteString("## Sessions\n\n") if len(sessions) == 0 { if req.DateFrom == req.DateTo { b.WriteString( "No sessions found for this date.\n", ) } else { b.WriteString( "No sessions found for this date range.\n", ) } } else { for i, s := range sessions { fmt.Fprintf(&b, "### Session %d\n", i+1) fmt.Fprintf(&b, "- ID: %s\n", s.ID) fmt.Fprintf(&b, "- Project: %s\n", s.Project) fmt.Fprintf(&b, "- Agent: %s\n", s.Agent) if s.StartedAt != nil { fmt.Fprintf(&b, "- Started: %s\n", *s.StartedAt) } if s.EndedAt != nil { fmt.Fprintf(&b, "- Ended: %s\n", *s.EndedAt) } fmt.Fprintf( &b, "- Messages: %d\n", s.MessageCount, ) if s.FirstMessage != nil { fmt.Fprintf( &b, "- First message: %s\n", truncateString(*s.FirstMessage, 200), ) } b.WriteString("\n") } if truncated { fmt.Fprintf( &b, "(Showing %d of %d sessions; "+ "remaining sessions omitted)\n\n", maxSessions, page.Total, ) } } if req.Prompt != "" { b.WriteString("## User Query\n\n") b.WriteString( "The user has provided the following " + "specific request. Prioritize addressing " + "this in your response:\n\n", ) b.WriteString(req.Prompt) b.WriteString("\n") } return b.String(), nil } func buildSessionPrompt( ctx context.Context, database db.Store, req GenerateRequest, ) (string, error) { sess, err := database.GetSession(ctx, req.SessionID) if err != nil { return "", fmt.Errorf("getting session: %w", err) } if sess == nil { return "", fmt.Errorf("session not found: %s", req.SessionID) } msgs, err := database.GetAllMessages(ctx, req.SessionID) if err != nil { return "", fmt.Errorf("getting messages: %w", err) } timing, err := database.GetSessionTiming(ctx, req.SessionID) if err != nil { return "", fmt.Errorf("getting timing: %w", err) } usage, err := database.GetSessionUsage(ctx, req.SessionID, false) if err != nil { return "", fmt.Errorf("getting usage: %w", err) } var b strings.Builder writeSystemInstruction(&b, req.Type) fmt.Fprintf(&b, "\n## Session: %s\n\n", req.SessionID) fmt.Fprintf(&b, "- Project: %s\n", sess.Project) fmt.Fprintf(&b, "- Agent: %s\n", sess.Agent) if sess.StartedAt != nil { fmt.Fprintf(&b, "- Started: %s\n", *sess.StartedAt) } if sess.EndedAt != nil { fmt.Fprintf(&b, "- Ended: %s\n", *sess.EndedAt) } fmt.Fprintf(&b, "- Messages: %d\n", sess.MessageCount) if usage != nil && usage.HasTokenData { fmt.Fprintf(&b, "- Output tokens: %d\n", usage.TotalOutputTokens) fmt.Fprintf(&b, "- Peak context tokens: %d\n", usage.PeakContextTokens) } if usage != nil && usage.HasCost { fmt.Fprintf(&b, "- Cost: $%.4f\n", usage.CostUSD) } if timing != nil { fmt.Fprintf(&b, "- Duration: %.1fs\n", float64(timing.TotalDurationMs)/1000) fmt.Fprintf(&b, "- Tool calls: %d\n", timing.ToolCallCount) } b.WriteString("\n## Messages\n\n") if len(msgs) == 0 { b.WriteString("No messages found for this session.\n") } else { for _, m := range msgs { if m.IsSystem { continue } fmt.Fprintf(&b, "### Message %d: %s\n", m.Ordinal, m.Role) if m.Timestamp != "" { fmt.Fprintf(&b, "- Timestamp: %s\n", m.Timestamp) } if m.Model != "" { fmt.Fprintf(&b, "- Model: %s\n", m.Model) } if hasContext, hasOutput := m.TokenPresence(); hasContext || hasOutput { fmt.Fprintf(&b, "- Context tokens: %d\n", m.ContextTokens) fmt.Fprintf(&b, "- Output tokens: %d\n", m.OutputTokens) } fmt.Fprintf(&b, "\n%s\n\n", truncateString(m.Content, 800)) } } if req.Prompt != "" { b.WriteString("## User Query\n\n") b.WriteString(req.Prompt) b.WriteString("\n") } return b.String(), nil } func writeSystemInstruction(b *strings.Builder, typ string) { switch typ { case "agent_analysis": b.WriteString( "You are analyzing AI agent sessions. " + "Provide deeper analysis of patterns, " + "effectiveness, and suggestions for " + "improving CLAUDE.md or agent workflows. " + "Focus on actionable insights.\n", ) default: b.WriteString( "You are summarizing a day of AI agent " + "activity. Provide a concise markdown " + "summary of what was accomplished, " + "key decisions made, and notable " + "patterns. Group by project if multiple " + "projects are present.\n", ) } } func truncateString(s string, max int) string { runes := []rune(s) if len(runes) <= max { return s } return string(runes[:max]) + "..." }