internal/runtime/runtime.go (view raw)
1package runtime
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "os"
8 "time"
9
10 "llm_aggregator/internal/aggregator"
11 "llm_aggregator/internal/llm"
12 "llm_aggregator/internal/output"
13 "llm_aggregator/internal/processor"
14 "llm_aggregator/internal/progress"
15)
16
17// Runtime holds the execution context for the aggregator
18type Runtime struct {
19 // Configuration
20 FeedsFile string
21 MaxArticlesPerFeed int
22 MaxDaysOld int
23 MaxTotalArticles int
24 IncludeKeywords []string
25 ExcludeKeywords []string
26 APIKey string
27 Model string
28 MaxTokens int
29 Temperature float64
30 Prompt string
31 SystemPrompt string
32 Output string
33 OutputFile string
34 IncludeArticles bool
35 Verbose bool
36
37 // State
38 Articles []map[string]any
39 Summary string
40 Error error
41
42 // Logger for verbose output
43 logger *progress.Context
44}
45
46// NewRuntime creates a new runtime with default values
47func NewRuntime() *Runtime {
48 return &Runtime{
49 MaxArticlesPerFeed: 10,
50 MaxDaysOld: 7,
51 MaxTotalArticles: 50,
52 Model: "deepseek-chat",
53 MaxTokens: 2000,
54 Temperature: 0.7,
55 Output: "text",
56 // SystemPrompt left empty to be filled from config or use deepseek default
57 }
58}
59
60// Execute runs the full aggregation pipeline
61func (r *Runtime) Execute(ctx context.Context) error {
62 // Setup logger based on verbose flag
63 if r.Verbose {
64 r.logger = progress.NewContext(progress.NewSimpleLogger(os.Stdout, true))
65 } else {
66 r.logger = progress.NewContext(&progress.NoopLogger{})
67 }
68
69 // Step 1: Aggregate feeds
70 feedAgg := aggregator.NewFeedAggregatorWithProgress(
71 r.MaxArticlesPerFeed,
72 r.MaxDaysOld,
73 5000, // max content length
74 r.logger,
75 )
76
77 articles, err := feedAgg.ParseFeedsFromFile(r.FeedsFile)
78 if err != nil {
79 return fmt.Errorf("error aggregating feeds: %w", err)
80 }
81
82 if len(articles) == 0 {
83 return fmt.Errorf("no articles found")
84 }
85
86 // Step 2: Process content
87 contentProc := processor.NewContentProcessor(
88 r.MaxTotalArticles,
89 3000, // max content per article
90 r.IncludeKeywords,
91 r.ExcludeKeywords,
92 )
93 contentProc.SetLogger(r.logger)
94
95 processedArticles := contentProc.ProcessArticles(articles, "date", true)
96
97 if len(processedArticles) == 0 {
98 return fmt.Errorf("no articles passed filtering")
99 }
100
101 r.Articles = processedArticles
102
103 // Step 3: Initialise Deepseek client
104 deepseek, err := llm.NewDeepseekClient(
105 r.APIKey,
106 "", // default base URL
107 r.Model,
108 r.MaxTokens,
109 r.Temperature,
110 )
111 if err != nil {
112 return fmt.Errorf("error initialising Deepseek client: %w", err)
113 }
114 deepseek.SetLogger(r.logger)
115
116 // Step 4: Get summary from Deepseek
117 summary, err := deepseek.SummariseArticles(
118 processedArticles,
119 r.Prompt,
120 r.SystemPrompt,
121 )
122 if err != nil {
123 return fmt.Errorf("error getting summary from Deepseek: %w", err)
124 }
125
126 r.Summary = summary
127 return nil
128}
129
130// WriteOutput writes the formatted output to the specified writer
131func (r *Runtime) WriteOutput(writer io.Writer) error {
132 formatter, err := output.NewFormatter(r.Output)
133 if err != nil {
134 return fmt.Errorf("error creating formatter: %w", err)
135 }
136
137 outputData := map[string]any{
138 "title": fmt.Sprintf("LLM Aggregator Summary - %s", time.Now().Format("2006-01-02 15:04")),
139 "prompt": r.Prompt,
140 "model": r.Model,
141 "articles_count": len(r.Articles),
142 "summary": r.Summary,
143 "timestamp": time.Now().Format(time.RFC3339),
144 }
145
146 if r.IncludeArticles {
147 outputData["articles"] = r.Articles
148 }
149
150 formattedOutput, err := formatter.FormatData(outputData)
151 if err != nil {
152 return fmt.Errorf("error formatting output: %w", err)
153 }
154
155 if _, err := fmt.Fprint(writer, formattedOutput); err != nil {
156 return fmt.Errorf("error writing output: %w", err)
157 }
158
159 return nil
160}
161
162// WriteOutputToFile writes the output to the specified file
163func (r *Runtime) WriteOutputToFile() error {
164 if r.OutputFile == "" {
165 return r.WriteOutput(os.Stdout)
166 }
167
168 file, err := os.Create(r.OutputFile)
169 if err != nil {
170 return fmt.Errorf("error creating output file: %w", err)
171 }
172 defer file.Close()
173
174 return r.WriteOutput(file)
175}
176