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 rt.Verbose = args.Verbose
53
54 // Validate API key
55 if rt.APIKey == "" {
56 fmt.Fprintln(os.Stderr, "Error: DeepSeek API key is required. Set via --api-key or DEEPSEEK_API_KEY environment variable.")
57 os.Exit(1)
58 }
59
60 // Run with TUI if requested
61 if args.TUI {
62 runWithTUI(rt)
63 } else {
64 runWithoutTUI(rt, args.Verbose)
65 }
66}
67
68func runWithTUI(rt *runtime.Runtime) {
69 // Create TUI model
70 m := tui.New()
71 p := tea.NewProgram(m, tea.WithAltScreen())
72
73 // Run TUI in goroutine
74 done := make(chan error)
75 go func() {
76 if _, err := p.Run(); err != nil {
77 done <- err
78 return
79 }
80 done <- nil
81 }()
82
83 // Run aggregation in another goroutine
84 go func() {
85 // Create context for cancellation
86 ctx, cancel := context.WithCancel(context.Background())
87 defer cancel()
88
89 // Set up signal handling
90 sigCh := make(chan os.Signal, 1)
91 signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
92
93 // Handle signals
94 go func() {
95 <-sigCh
96 p.Send(tui.Error("Operation cancelled"))
97 cancel()
98 }()
99
100 // Update TUI status
101 p.Send(tui.Step(1, fmt.Sprintf("Aggregating feeds from: %s", rt.FeedsFile)))
102
103 // Execute the runtime
104 err := rt.Execute(ctx)
105 if err != nil {
106 p.Send(tui.Error(err.Error()))
107 return
108 }
109
110 // Update TUI status
111 p.Send(tui.StepWithCounts(5, "Formatting output", len(rt.Articles), len(rt.Articles)))
112
113 // Write output
114 if rt.OutputFile != "" {
115 if err := rt.WriteOutputToFile(); err != nil {
116 p.Send(tui.Error(fmt.Sprintf("Error writing output: %v", err)))
117 return
118 }
119 p.Send(tui.Step(6, fmt.Sprintf("Output written to: %s", rt.OutputFile)))
120 } else {
121 if err := rt.WriteOutput(os.Stdout); err != nil {
122 p.Send(tui.Error(fmt.Sprintf("Error writing output: %v", err)))
123 return
124 }
125 p.Send(tui.Step(6, "Output complete"))
126 }
127
128 // Mark as done
129 p.Send(tui.Step(7, "Processing completed successfully"))
130 }()
131
132 // Wait for TUI to complete
133 if err := <-done; err != nil {
134 fmt.Fprintf(os.Stderr, "TUI error: %v\n", err)
135 os.Exit(1)
136 }
137}
138
139func runWithoutTUI(rt *runtime.Runtime, verbose bool) {
140 // Set verbose flag on runtime
141 rt.Verbose = verbose
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 // Write output
151 if rt.OutputFile != "" {
152 if err := rt.WriteOutputToFile(); err != nil {
153 fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
154 os.Exit(1)
155 }
156 } else {
157 if err := rt.WriteOutput(os.Stdout); err != nil {
158 fmt.Fprintf(os.Stderr, "Error writing output: %v\n", err)
159 os.Exit(1)
160 }
161 }
162}
163