all repos — llm_aggregator @ 13547fc860356e4f64bfa097f7681fdd90f33df7

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

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:       "You are a helpful assistant that summarises news articles.",
 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