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