all repos — llm_aggregator @ b4f6beb3ba126b91482bcd5df0745445088d51d7

A CLI tool to aggregate RSS feeds and summarise them with LLMs

internal/output/formatter.go (view raw)

  1package output
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"strings"
  7	"time"
  8)
  9
 10// Formatter formats output in different formats.
 11type Formatter struct {
 12	formatType string
 13}
 14
 15// NewFormatter creates a new formatter with the specified format.
 16func NewFormatter(formatType string) (*Formatter, error) {
 17	formatType = strings.ToLower(formatType)
 18	if formatType != "text" && formatType != "markdown" && formatType != "json" {
 19		return nil, fmt.Errorf("unsupported format: %s", formatType)
 20	}
 21	return &Formatter{formatType: formatType}, nil
 22}
 23
 24// FormatData formats data according to the specified format.
 25func (f *Formatter) FormatData(data map[string]any) (string, error) {
 26	switch f.formatType {
 27	case "json":
 28		return f.formatJSON(data)
 29	case "markdown":
 30		return f.formatMarkdown(data)
 31	default: // text
 32		return f.formatText(data)
 33	}
 34}
 35
 36func (f *Formatter) formatJSON(data map[string]any) (string, error) {
 37	bytes, err := json.MarshalIndent(data, "", "  ")
 38	if err != nil {
 39		return "", fmt.Errorf("failed to marshal JSON: %w", err)
 40	}
 41	return string(bytes), nil
 42}
 43
 44func (f *Formatter) formatMarkdown(data map[string]any) (string, error) {
 45	var lines []string
 46
 47	// Title
 48	title := getString(data, "title", "LLM Aggregator Summary")
 49	lines = append(lines, fmt.Sprintf("# %s", title))
 50	lines = append(lines, "")
 51
 52	// Metadata
 53	lines = append(lines, "## Metadata")
 54	lines = append(lines, "")
 55
 56	prompt := getString(data, "prompt", "Unknown")
 57	model := getString(data, "model", "Unknown")
 58	articlesCount := getInt(data, "articles_count", 0)
 59	timestamp := getString(data, "timestamp", time.Now().Format(time.RFC3339))
 60
 61	lines = append(lines, fmt.Sprintf("- **Prompt**: %s", prompt))
 62	lines = append(lines, fmt.Sprintf("- **Model**: %s", model))
 63	lines = append(lines, fmt.Sprintf("- **Articles Analysed**: %d", articlesCount))
 64	lines = append(lines, fmt.Sprintf("- **Generated**: %s", timestamp))
 65	lines = append(lines, "")
 66
 67	// Summary
 68	lines = append(lines, "## Summary")
 69	lines = append(lines, "")
 70	summary := getString(data, "summary", "No summary available.")
 71	lines = append(lines, summary)
 72	lines = append(lines, "")
 73
 74	// Articles (if included)
 75	if articles, ok := data["articles"].([]map[string]any); ok && len(articles) > 0 {
 76		lines = append(lines, "## Articles Analysed")
 77		lines = append(lines, "")
 78
 79		for i, article := range articles {
 80			lines = append(lines, fmt.Sprintf("### Article %d: %s", i+1, article["title"]))
 81			lines = append(lines, "")
 82
 83			if source, ok := article["source_feed"].(string); ok && source != "" {
 84				lines = append(lines, fmt.Sprintf("**Source**: %s", source))
 85			}
 86
 87			if published, ok := article["published"]; ok {
 88				switch pub := published.(type) {
 89				case time.Time:
 90					if !pub.IsZero() {
 91						lines = append(lines, fmt.Sprintf("**Published**: %s", pub.Format("2006-01-02 15:04")))
 92					}
 93				case string:
 94					lines = append(lines, fmt.Sprintf("**Published**: %s", pub))
 95				}
 96			}
 97
 98			if author, ok := article["author"].(string); ok && author != "" {
 99				lines = append(lines, fmt.Sprintf("**Author**: %s", author))
100			}
101
102			if link, ok := article["link"].(string); ok && link != "" {
103				lines = append(lines, fmt.Sprintf("**Link**: [%s](%s)", link, link))
104			}
105
106			lines = append(lines, "")
107		}
108	}
109
110	return strings.Join(lines, "\n"), nil
111}
112
113func (f *Formatter) formatText(data map[string]any) (string, error) {
114	var lines []string
115
116	// Title/Header
117	title := getString(data, "title", "LLM Aggregator Summary")
118	lines = append(lines, strings.Repeat("=", 80))
119	lines = append(lines, centerText(title, 80))
120	lines = append(lines, strings.Repeat("=", 80))
121	lines = append(lines, "")
122
123	// Metadata
124	lines = append(lines, "METADATA")
125	lines = append(lines, strings.Repeat("-", 40))
126
127	prompt := getString(data, "prompt", "Unknown")
128	model := getString(data, "model", "Unknown")
129	articlesCount := getInt(data, "articles_count", 0)
130	timestamp := getString(data, "timestamp", time.Now().Format(time.RFC3339))
131
132	lines = append(lines, fmt.Sprintf("Prompt: %s", prompt))
133	lines = append(lines, fmt.Sprintf("Model: %s", model))
134	lines = append(lines, fmt.Sprintf("Articles Analysed: %d", articlesCount))
135	lines = append(lines, fmt.Sprintf("Generated: %s", timestamp))
136	lines = append(lines, "")
137
138	// Summary
139	lines = append(lines, "SUMMARY")
140	lines = append(lines, strings.Repeat("-", 40))
141	lines = append(lines, "")
142	summary := getString(data, "summary", "No summary available.")
143	lines = append(lines, summary)
144	lines = append(lines, "")
145
146	// Articles (if included)
147	if articles, ok := data["articles"].([]map[string]any); ok && len(articles) > 0 {
148		lines = append(lines, "ARTICLES ANALYSED")
149		lines = append(lines, strings.Repeat("-", 40))
150		lines = append(lines, "")
151
152		for i, article := range articles {
153			lines = append(lines, fmt.Sprintf("[Article %d]", i+1))
154			lines = append(lines, fmt.Sprintf("Title: %s", article["title"]))
155
156			if source, ok := article["source_feed"].(string); ok && source != "" {
157				lines = append(lines, fmt.Sprintf("Source: %s", source))
158			}
159
160			if published, ok := article["published"]; ok {
161				switch pub := published.(type) {
162				case time.Time:
163					if !pub.IsZero() {
164						lines = append(lines, fmt.Sprintf("Published: %s", pub.Format("2006-01-02 15:04")))
165					}
166				case string:
167					lines = append(lines, fmt.Sprintf("Published: %s", pub))
168				}
169			}
170
171			if author, ok := article["author"].(string); ok && author != "" {
172				lines = append(lines, fmt.Sprintf("Author: %s", author))
173			}
174
175			if link, ok := article["link"].(string); ok && link != "" {
176				lines = append(lines, fmt.Sprintf("Link: %s", link))
177			}
178
179			// Show preview of content
180			if content, ok := article["content"].(string); ok && content != "" {
181				preview := content
182				if len(preview) > 200 {
183					preview = preview[:200] + "..."
184				}
185				lines = append(lines, fmt.Sprintf("Preview: %s", preview))
186			}
187
188			lines = append(lines, "")
189		}
190	}
191
192	// Footer
193	lines = append(lines, strings.Repeat("=", 80))
194	lines = append(lines, centerText("End of Summary", 80))
195	lines = append(lines, strings.Repeat("=", 80))
196
197	return strings.Join(lines, "\n"), nil
198}
199
200// Helper functions
201func getString(data map[string]any, key, defaultValue string) string {
202	if val, ok := data[key].(string); ok {
203		return val
204	}
205	return defaultValue
206}
207
208func getInt(data map[string]any, key string, defaultValue int) int {
209	if val, ok := data[key].(int); ok {
210		return val
211	}
212	if val, ok := data[key].(float64); ok {
213		return int(val)
214	}
215	return defaultValue
216}
217
218func centerText(text string, width int) string {
219	if len(text) >= width {
220		return text
221	}
222	padding := (width - len(text)) / 2
223	return strings.Repeat(" ", padding) + text + strings.Repeat(" ", width-padding-len(text))
224}