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}