all repos — llm_aggregator @ a16b6b07701a9722c772d3e0255ff10335088470

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