From 68ff4d5a40e7c6fa29ddd190d057ef6a925826fa Mon Sep 17 00:00:00 2001 From: Edi Piqoni Date: Sun, 5 Jan 2025 20:06:06 +0100 Subject: [PATCH] Add Analyst Feature --- README.md | 21 +++++++++++++++ analyst.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ config.go | 7 ++++- go.mod | 2 +- go.sum | 4 +-- main.go | 2 +- 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 analyst.go diff --git a/README.md b/README.md index 3568d69..28a1d03 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,27 @@ openai_model: summary_feeds: ``` +### Analyst LLM Feature +The Analyst feature enables you to gather articles from specified feeds and analyze them using a prompt sent to a language model like GPT-4o (default). The result is included in the daily digest under an Analysis section. You write on your analyst_prompt setting what do you want the analyst to do on your behalf, for example picking relevant news to your liking (example: a cybersecurity expert interested only in certain type of attack), or having an investing analyst suggesting investment opportunities, etc. + +Configuration Example of an analyst finding investment opportunities: + +```yaml +openai_api_key: sk-xxxxxxxxxxxxxxxxx +analyst_feeds: + - https://feeds.bbci.co.uk/news/business/rss.xml +analyst_prompt: You are a world-class investing expert. Analyze the provided list of articles for potential investment opportunities. If no direct opportunities are found, identify industries, regions, or trends that could have indirect impacts on the investment landscape. +``` + +How it Works: +Gathering Articles: The RSS feeds specified in analyst_feeds are fetched, and the titles along with their rss descriptions are attached to the analyst_prompt to form a single input prompt. +Then the prompt is sent to the specified language model (analyst_model), and the response is included in the daily markdown file under the Analysis section. + +Default model is OpenAI's gpt-4o but to override model add configuration: +``` +analyst_model: o1-preview +``` + ### Summarization of Articles using ChatGPT In order to use the summarization feature, you'll first need to set up an OpenAI account. If you haven't already done so, you can sign up [here](https://platform.openai.com/login?launch). Once registered, you'll need to acquire an OpenAI API key which can be found [here](https://platform.openai.com/account/api-keys). diff --git a/analyst.go b/analyst.go new file mode 100644 index 0000000..c26a75f --- /dev/null +++ b/analyst.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/mmcdole/gofeed" + openai "github.com/sashabaranov/go-openai" + "github.com/spf13/viper" +) + +const defaultLimit = 20 // default number of articles per feed for analysis +var model = openai.GPT4o + +func generateAnalysis(fp *gofeed.Parser, writer Writer) { + if !viper.IsSet("analyst_feeds") || !viper.IsSet("analyst_prompt") { + return + } + + analystFeeds := viper.GetStringSlice("analyst_feeds") + analystPrompt := viper.GetString("analyst_prompt") + analystModel := viper.GetString("analyst_model") + + var articleTitles []string + for _, feedURL := range analystFeeds { + parsedFeed := parseFeed(fp, feedURL, defaultLimit) + if parsedFeed == nil { + continue + } + for _, item := range parsedFeed.Items { + articleTitles = append(articleTitles, item.Title+": "+item.Description) // add also description for better context + } + } + + if len(articleTitles) == 0 { + return + } + + prompt := fmt.Sprintf("%s\n\n%s", analystPrompt, strings.Join(articleTitles, "\n")) + analysis := getLLMAnalysis(prompt, analystModel) + + if analysis != "" { + writer.write("\n## Daily Analysis:\n") + writer.write(analysis + "\n") + } +} + +func getLLMAnalysis(prompt string, analystModel string) string { + clientConfig := openai.DefaultConfig(openaiApiKey) + if openaiBaseURL != "" { + clientConfig.BaseURL = openaiBaseURL + } + if analystModel != "" { + model = analystModel + } + client := openai.NewClientWithConfig(clientConfig) + + resp, err := client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + Content: prompt, + }, + }, + }, + ) + + if err != nil { + fmt.Printf("ChatCompletion error: %v\n", err) + return "" + } + + return resp.Choices[0].Message.Content +} diff --git a/config.go b/config.go index 5ac1a40..b470929 100644 --- a/config.go +++ b/config.go @@ -36,7 +36,12 @@ openai_api_key: openai_base_url: openai_model: summary_feeds: -show_images: false` +show_images: false +analyst_feeds: + - https://feeds.bbci.co.uk/news/business/rss.xml +analyst_prompt: +analyst_model: +` func parseOPML(xmlContent []byte) []RSS { o := Opml{} diff --git a/go.mod b/go.mod index fae8e45..4f6b19b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/mmcdole/gofeed v1.1.3 github.com/nathan-osman/go-sunrise v1.1.0 - github.com/sashabaranov/go-openai v1.14.2 + github.com/sashabaranov/go-openai v1.36.1 github.com/spf13/viper v1.16.0 modernc.org/sqlite v1.20.0 ) diff --git a/go.sum b/go.sum index 62f08a4..dc40d7d 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sashabaranov/go-openai v1.14.2 h1:5DPTtR9JBjKPJS008/A409I5ntFhUPPGCmaAihcPRyo= -github.com/sashabaranov/go-openai v1.14.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.36.1 h1:EVfRXwIlW2rUzpx6vR+aeIKCK/xylSrVYAx1TMTSX3g= +github.com/sashabaranov/go-openai v1.36.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/savioxavier/termlink v1.2.1 h1:O9ZQvk9BPQQK4JQeMB56ZfV8uam0Ts+f97mJme7+dq8= github.com/savioxavier/termlink v1.2.1/go.mod h1:WA7FTALNwN41NGnmQMIrnjAYTsEhIAZ4RuzgEiB0Jp8= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= diff --git a/main.go b/main.go index 5c133b0..ae6a04d 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ func main() { writer := getWriter() displayWeather(writer) displaySunriseSunset(writer) + generateAnalysis(fp, writer) for _, feed := range myFeeds { parsedFeed := parseFeed(fp, feed.url, feed.limit) @@ -26,6 +27,5 @@ func main() { } } - // Close the database connection after processing all the feeds defer db.Close() }