all repos — llm_aggregator @ 7b6943f1a3f252697b0e16d94c9635a5e064090b

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

cmd/llm_aggregator.go (view raw)

  1package main
  2
  3import (
  4	"context"
  5	"fmt"
  6	"os"
  7	"os/signal"
  8	"syscall"
  9
 10	"llm_aggregator/internal/cli"
 11	"llm_aggregator/internal/runtime"
 12	"llm_aggregator/internal/tui"
 13
 14	tea "github.com/charmbracelet/bubbletea"
 15)
 16
 17var (
 18	version   string
 19	buildDate string
 20)
 21
 22func main() {
 23	cli.BuildDate = buildDate
 24	cli.Version = version
 25	// Parse command line arguments
 26	args, err := cli.ParseArgs()
 27	if err != nil {
 28		fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
 29		os.Exit(1)
 30	}
 31
 32	// Create runtime configuration
 33	rt := runtime.NewRuntime()
 34	rt.FeedsFile = args.FeedsFile
 35	rt.MaxArticlesPerFeed = args.MaxArticlesPerFeed
 36	rt.MaxDaysOld = args.MaxDaysOld
 37	rt.MaxTotalArticles = args.MaxTotalArticles
 38	rt.IncludeKeywords = cli.ParseKeywords(args.IncludeKeywords)
 39	rt.ExcludeKeywords = cli.ParseKeywords(args.ExcludeKeywords)
 40	rt.APIKey = args.APIKey
 41	if rt.APIKey == "" {
 42		rt.APIKey = os.Getenv("DEEPSEEK_API_KEY")
 43	}
 44	rt.Model = args.Model
 45	rt.MaxTokens = args.MaxTokens
 46	rt.Temperature = args.Temperature
 47	rt.Prompt = args.Prompt
 48	rt.SystemPrompt = args.SystemPrompt
 49	rt.Output = args.Output
 50	rt.OutputFile = args.OutputFile
 51	rt.IncludeArticles = args.IncludeArticles
 52
 53	// Validate API key
 54	if rt.APIKey == "" {
 55		fmt.Fprintln(os.Stderr, "Error: DeepSeek API key is required. Set via --api-key or DEEPSEEK_API_KEY environment variable.")
 56		os.Exit(1)
 57	}
 58
 59	// Run with TUI if requested
 60	if args.TUI {
 61		runWithTUI(rt)
 62	} else {
 63		runWithoutTUI(rt, args.Verbose)
 64	}
 65}
 66
 67func runWithTUI(rt *runtime.Runtime) {
 68	// Create TUI model
 69	m := tui.New()
 70	p := tea.NewProgram(m, tea.WithAltScreen())
 71
 72	// Run TUI in goroutine
 73	done := make(chan error)
 74	go func() {
 75		if _, err := p.Run(); err != nil {
 76			done <- err
 77			return
 78		}
 79		done <- nil
 80	}()
 81
 82	// Run aggregation in another goroutine
 83	go func() {
 84		// Create context for cancellation
 85		ctx, cancel := context.WithCancel(context.Background())
 86		defer cancel()
 87
 88		// Set up signal handling
 89		sigCh := make(chan os.Signal, 1)
 90		signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
 91
 92		// Handle signals
 93		go func() {
 94			<-sigCh
 95			p.Send(tui.Error("Operation cancelled"))
 96			cancel()
 97		}()
 98
 99		// Update TUI status
100		p.Send(tui.Step(1, fmt.Sprintf("Aggregating feeds from: %s", rt.FeedsFile)))
101
102		// Execute the runtime
103		err := rt.Execute(ctx)
104		if err != nil {
105			p.Send(tui.Error(err.Error()))
106			return
107		}
108
109		// Update TUI status
110		p.Send(tui.StepWithCounts(5, "Formatting output", len(rt.Articles), len(rt.Articles)))
111
112		// Write output
113		if rt.OutputFile != "" {
114			if err := rt.WriteOutputToFile(); err != nil {
115				p.Send(tui.Error(fmt.Sprintf("Error writing output: %v", err)))
116				return
117			}
118			p.Send(tui.Step(6, fmt.Sprintf("Output written to: %s", rt.OutputFile)))
119		} else {
120			if err := rt.WriteOutput(os.Stdout); err != nil {
121				p.Send(tui.Error(fmt.Sprintf("Error writing output: %v", err)))
122				return
123			}
124			p.Send(tui.Step(6, "Output complete"))
125		}
126
127		// Mark as done
128		p.Send(tui.Step(7, "Processing completed successfully"))
129	}()
130
131	// Wait for TUI to complete
132	if err := <-done; err != nil {
133		fmt.Fprintf(os.Stderr, "TUI error: %v\n", err)
134		os.Exit(1)
135	}
136}
137
138func runWithoutTUI(rt *runtime.Runtime, verbose bool) {
139	if verbose {
140		fmt.Printf("Aggregating feeds from: %s\n", rt.FeedsFile)
141	}
142
143	// Execute the runtime
144	err := rt.Execute(context.Background())
145	if err != nil {
146		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
147		os.Exit(1)
148	}
149
150	if verbose {
151		fmt.Printf("Aggregated %d articles\n", len(rt.Articles))
152		fmt.Printf("Requesting summary from DeepSeek with model: %s\n", rt.Model)
153	}
154
155	// Write output
156	if rt.OutputFile != "" {
157		if err := rt.WriteOutputToFile(); err != nil {
158			fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
159			os.Exit(1)
160		}
161		if verbose {
162			fmt.Printf("Output written to: %s\n", rt.OutputFile)
163		}
164	} else {
165		if err := rt.WriteOutput(os.Stdout); err != nil {
166			fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
167			os.Exit(1)
168		}
169	}
170
171	if verbose {
172		fmt.Println("Processing completed successfully")
173	}
174}
175