diff --git a/bot/bot.go b/bot/bot.go
new file mode 100644
index 0000000..e75dc17
--- /dev/null
+++ b/bot/bot.go
@@ -0,0 +1,224 @@
+package bot
+
+import (
+ "errors"
+ "github.com/mymmrac/telego"
+ th "github.com/mymmrac/telego/telegohandler"
+ tu "github.com/mymmrac/telego/telegoutil"
+ "log/slog"
+ "net/url"
+ "strings"
+ "telegram-ollama-reply-bot/extractor"
+ "telegram-ollama-reply-bot/llm"
+)
+
+var (
+ ErrGetMe = errors.New("cannot retrieve api user")
+ ErrUpdatesChannel = errors.New("cannot get updates channel")
+ ErrHandlerInit = errors.New("cannot initialize handler")
+)
+
+const contextUserKey = "user"
+
+type Bot struct {
+ api *telego.Bot
+ llm *llm.LlmConnector
+ extractor *extractor.Extractor
+}
+
+func NewBot(api *telego.Bot, llm *llm.LlmConnector, extractor *extractor.Extractor) *Bot {
+ return &Bot{
+ api: api,
+ llm: llm,
+ extractor: extractor,
+ }
+}
+
+func (b *Bot) Run() error {
+ botUser, err := b.api.GetMe()
+ if err != nil {
+ slog.Error("Cannot retrieve api user", err)
+
+ return ErrGetMe
+ }
+
+ slog.Info("Running api as", map[string]any{
+ "id": botUser.ID,
+ "username": botUser.Username,
+ "name": botUser.FirstName,
+ "is_bot": botUser.IsBot,
+ })
+
+ updates, err := b.api.UpdatesViaLongPolling(nil)
+ if err != nil {
+ slog.Error("Cannot get update channel", err)
+
+ return ErrUpdatesChannel
+ }
+
+ bh, err := th.NewBotHandler(b.api, updates)
+ if err != nil {
+ slog.Error("Cannot initialize bot handler", err)
+
+ return ErrHandlerInit
+ }
+
+ defer bh.Stop()
+ defer b.api.StopLongPolling()
+
+ bh.Handle(b.startHandler, th.CommandEqual("start"))
+ bh.Handle(b.heyHandler, th.CommandEqual("hey"))
+ bh.Handle(b.summarizeHandler, th.CommandEqual("summarize"))
+ bh.Handle(b.helpHandler, th.CommandEqual("help"))
+
+ bh.Start()
+
+ return nil
+}
+
+func (b *Bot) heyHandler(bot *telego.Bot, update telego.Update) {
+ slog.Info("/hey")
+
+ chatID := tu.ID(update.Message.Chat.ID)
+
+ b.sendTyping(chatID)
+
+ llmReply, err := b.llm.HandleSingleRequest(update.Message.Text, llm.ModelMistralUncensored)
+ if err != nil {
+ slog.Error("Cannot get reply from LLM connector")
+
+ _, _ = b.api.SendMessage(b.reply(update.Message, tu.Message(
+ chatID,
+ "LLM request error. Try again later.",
+ )))
+
+ return
+ }
+
+ slog.Debug("Got completion. Going to send.", llmReply)
+
+ message := tu.Message(
+ chatID,
+ llmReply,
+ ).WithParseMode("Markdown")
+
+ _, err = bot.SendMessage(b.reply(update.Message, message))
+
+ if err != nil {
+ slog.Error("Can't send reply message", err)
+ }
+}
+
+func (b *Bot) summarizeHandler(bot *telego.Bot, update telego.Update) {
+ slog.Info("/summarize", update.Message.Text)
+
+ chatID := tu.ID(update.Message.Chat.ID)
+
+ b.sendTyping(chatID)
+
+ args := strings.Split(update.Message.Text, " ")
+
+ if len(args) < 2 {
+ _, _ = bot.SendMessage(tu.Message(
+ tu.ID(update.Message.Chat.ID),
+ "Usage: /summarize \r\n\r\n"+
+ "Example:\r\n"+
+ "/summarize https://kernel.org/get-notifications-for-your-patches.html",
+ ))
+
+ return
+ }
+
+ _, err := url.ParseRequestURI(args[1])
+ if err != nil {
+ slog.Error("Provided URL is not valid", args[1])
+
+ _, _ = b.api.SendMessage(b.reply(update.Message, tu.Message(
+ chatID,
+ "URL is not valid.",
+ )))
+
+ return
+ }
+
+ article, err := b.extractor.GetArticleFromUrl(args[1])
+ if err != nil {
+ slog.Error("Cannot retrieve an article using extractor", err)
+ }
+
+ llmReply, err := b.llm.Summarize(article.Text, llm.ModelMistralUncensored)
+ if err != nil {
+ slog.Error("Cannot get reply from LLM connector")
+
+ _, _ = b.api.SendMessage(b.reply(update.Message, tu.Message(
+ chatID,
+ "LLM request error. Try again later.",
+ )))
+
+ return
+ }
+
+ slog.Debug("Got completion. Going to send.", llmReply)
+
+ message := tu.Message(
+ chatID,
+ llmReply,
+ ).WithParseMode("Markdown")
+
+ _, err = bot.SendMessage(b.reply(update.Message, message))
+
+ if err != nil {
+ slog.Error("Can't send reply message", err)
+ }
+}
+
+func (b *Bot) helpHandler(bot *telego.Bot, update telego.Update) {
+ slog.Info("/help")
+
+ chatID := tu.ID(update.Message.Chat.ID)
+
+ b.sendTyping(chatID)
+
+ _, err := bot.SendMessage(b.reply(update.Message, tu.Messagef(
+ chatID,
+ "Instructions:\r\n"+
+ "/hey - Ask something from LLM\r\n"+
+ "/summarize - Summarize text from the provided link\r\n"+
+ "/help - Show this help",
+ )))
+ if err != nil {
+ slog.Error("Cannot send a message", err)
+ }
+}
+
+func (b *Bot) startHandler(bot *telego.Bot, update telego.Update) {
+ slog.Info("/start")
+
+ chatID := tu.ID(update.Message.Chat.ID)
+
+ b.sendTyping(chatID)
+
+ _, err := bot.SendMessage(b.reply(update.Message, tu.Message(
+ chatID,
+ "Hey!\r\n"+
+ "Check out /help to learn how to use this bot.",
+ )))
+ if err != nil {
+ slog.Error("Cannot send a message", err)
+ }
+}
+
+func (b *Bot) reply(originalMessage *telego.Message, newMessage *telego.SendMessageParams) *telego.SendMessageParams {
+ return newMessage.WithReplyParameters(&telego.ReplyParameters{
+ MessageID: originalMessage.MessageID,
+ })
+}
+
+func (b *Bot) sendTyping(chatId telego.ChatID) {
+ slog.Info("Setting 'typing' chat action")
+
+ err := b.api.SendChatAction(tu.ChatAction(chatId, "typing"))
+ if err != nil {
+ slog.Error("Cannot set chat action", err)
+ }
+}
diff --git a/extractor/extractor.go b/extractor/extractor.go
new file mode 100644
index 0000000..82f7fdf
--- /dev/null
+++ b/extractor/extractor.go
@@ -0,0 +1,42 @@
+package extractor
+
+import (
+ "errors"
+ "github.com/advancedlogic/GoOse"
+)
+
+var (
+ ErrExtractFailed = errors.New("extraction failed")
+)
+
+type Extractor struct {
+ goose *goose.Goose
+}
+
+func NewExtractor() *Extractor {
+ gooseExtractor := goose.New()
+
+ return &Extractor{
+ goose: &gooseExtractor,
+ }
+}
+
+type Article struct {
+ Title string
+ Text string
+ Url string
+}
+
+func (e *Extractor) GetArticleFromUrl(url string) (Article, error) {
+ article, err := e.goose.ExtractFromURL(url)
+
+ if err != nil {
+ return Article{}, ErrExtractFailed
+ }
+
+ return Article{
+ Title: article.Title,
+ Text: article.CleanedText,
+ Url: article.FinalURL,
+ }, nil
+}
diff --git a/go.mod b/go.mod
index 519724e..1eedf95 100644
--- a/go.mod
+++ b/go.mod
@@ -3,24 +3,38 @@ module telegram-ollama-reply-bot
go 1.22.0
require (
+ github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275
github.com/mymmrac/telego v0.29.1
github.com/sashabaranov/go-openai v1.20.2
)
require (
+ github.com/PuerkitoBio/goquery v1.4.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/andybalholm/cascadia v1.0.0 // indirect
+ github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/fasthttp/router v1.4.22 // indirect
+ github.com/fatih/set v0.2.1 // indirect
+ github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect
+ github.com/go-resty/resty/v2 v2.0.0 // indirect
github.com/grbit/go-json v0.11.0 // indirect
+ github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect
github.com/klauspost/compress v1.17.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
+ github.com/mattn/go-runewidth v0.0.3 // indirect
+ github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 // indirect
+ github.com/pkg/errors v0.8.1 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
+ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
golang.org/x/arch v0.6.0 // indirect
+ golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
)
diff --git a/go.sum b/go.sum
index b3906a5..4a01d62 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,13 @@
+github.com/PuerkitoBio/goquery v1.4.1 h1:smcIRGdYm/w7JSbcdeLHEMzxmsBQvl8lhf0dSw2nzMI=
+github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA=
+github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275 h1:Kuhf+w+ilOGoXaR4O4nZ6Dp+ZS83LdANUjwyMXsPGX4=
+github.com/advancedlogic/GoOse v0.0.0-20231203033844-ae6b36caf275/go.mod h1:98NztIIMIntZGtQVIs8H85Q5b88fTbwWFbLz/lM9/xU=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
+github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e h1:s05JG2GwtJMHaPcXDpo4V35TFgyYZzNsmBlSkHPEbeg=
+github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
@@ -16,25 +24,44 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/router v1.4.22 h1:qwWcYBbndVDwts4dKaz+A2ehsnbKilmiP6pUhXBfYKo=
github.com/fasthttp/router v1.4.22/go.mod h1:KeMvHLqhlB9vyDWD5TSvTccl9qeWrjSSiTJrJALHKV0=
+github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
+github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
+github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 h1:u8AQ9bPa9oC+8/A/jlWouakhIvkFfuxgIIRjiy8av7I=
+github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs=
+github.com/go-resty/resty/v2 v2.0.0 h1:9Nq/U+V4xsoDnDa/iTrABDWUCuk3Ne92XFHPe6dKWUc=
+github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
+github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
+github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
+github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mymmrac/telego v0.29.1 h1:nsNnK0mS18OL+unoDjDI6BVfafJBbT8Wtj7rCzEWoM8=
github.com/mymmrac/telego v0.29.1/go.mod h1:ZLD1+L2TQRr97NPOCoN1V2w8y9kmFov33OfZ3qT8cF4=
+github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4JDUd43CApkmCeTSQlWjtTtABrU2qsgbuP0BI=
+github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sashabaranov/go-openai v1.20.2 h1:nilzF2EKzaHyK4Rk2Dbu/aJEZbtIvskDIXvfS4yx+6M=
github.com/sashabaranov/go-openai v1.20.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
+github.com/simplereach/timeutils v1.2.0 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA=
+github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -49,15 +76,62 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
+gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/llm/llm.go b/llm/llm.go
new file mode 100644
index 0000000..483a36e
--- /dev/null
+++ b/llm/llm.go
@@ -0,0 +1,100 @@
+package llm
+
+import (
+ "context"
+ "errors"
+ "github.com/sashabaranov/go-openai"
+ "log/slog"
+)
+
+var (
+ ErrLlmBackendRequestFailed = errors.New("llm back-end request failed")
+ ErrNoChoices = errors.New("no choices in LLM response")
+
+ ModelMistralUncensored = "dolphin-mistral"
+)
+
+type LlmConnector struct {
+ client *openai.Client
+}
+
+func NewConnector(baseUrl string, token string) *LlmConnector {
+ config := openai.DefaultConfig(token)
+ config.BaseURL = baseUrl
+
+ client := openai.NewClientWithConfig(config)
+
+ return &LlmConnector{
+ client: client,
+ }
+}
+
+func (l *LlmConnector) HandleSingleRequest(text string, model string) (string, error) {
+ req := openai.ChatCompletionRequest{
+ Model: model,
+ Messages: []openai.ChatCompletionMessage{
+ {
+ Role: openai.ChatMessageRoleSystem,
+ Content: "You're a bot in the Telegram chat. You are replying to questions directed to you.",
+ },
+ },
+ }
+
+ req.Messages = append(req.Messages, openai.ChatCompletionMessage{
+ Role: openai.ChatMessageRoleUser,
+ Content: text,
+ })
+
+ resp, err := l.client.CreateChatCompletion(context.Background(), req)
+ if err != nil {
+ slog.Error("LLM back-end request failed", err)
+
+ return "", ErrLlmBackendRequestFailed
+ }
+
+ slog.Debug("Received LLM back-end response", resp)
+
+ if len(resp.Choices) < 1 {
+ slog.Error("LLM back-end reply has no choices")
+
+ return "", ErrNoChoices
+ }
+
+ return resp.Choices[0].Message.Content, nil
+}
+
+func (l *LlmConnector) Summarize(text string, model string) (string, error) {
+ req := openai.ChatCompletionRequest{
+ Model: model,
+ Messages: []openai.ChatCompletionMessage{
+ {
+ Role: openai.ChatMessageRoleSystem,
+ Content: "You are a short digest editor. Summarize the text you received " +
+ "as a list of bullet points with most important facts from the text. " +
+ "If possible, use the same language as the original text.",
+ },
+ },
+ }
+
+ req.Messages = append(req.Messages, openai.ChatCompletionMessage{
+ Role: openai.ChatMessageRoleUser,
+ Content: text,
+ })
+
+ resp, err := l.client.CreateChatCompletion(context.Background(), req)
+ if err != nil {
+ slog.Error("LLM back-end request failed", err)
+
+ return "", ErrLlmBackendRequestFailed
+ }
+
+ slog.Debug("Received LLM back-end response", resp)
+
+ if len(resp.Choices) < 1 {
+ slog.Error("LLM back-end reply has no choices")
+
+ return "", ErrNoChoices
+ }
+
+ return resp.Choices[0].Message.Content, nil
+}
diff --git a/main.go b/main.go
index b720c5d..b2f72a6 100644
--- a/main.go
+++ b/main.go
@@ -1,20 +1,14 @@
package main
import (
- "context"
"fmt"
"log/slog"
"os"
-
- openai "github.com/sashabaranov/go-openai"
+ "telegram-ollama-reply-bot/bot"
+ "telegram-ollama-reply-bot/extractor"
+ "telegram-ollama-reply-bot/llm"
tg "github.com/mymmrac/telego"
- tu "github.com/mymmrac/telego/telegoutil"
-)
-
-const (
- ModelMistral = "mistral"
- ModelMistralUncensored = "dolphin-mistral"
)
func main() {
@@ -23,68 +17,20 @@ func main() {
telegramToken := os.Getenv("TELEGRAM_TOKEN")
- config := openai.DefaultConfig(ollamaToken)
- config.BaseURL = ollamaBaseUrl
+ llmc := llm.NewConnector(ollamaBaseUrl, ollamaToken)
+ ext := extractor.NewExtractor()
- client := openai.NewClientWithConfig(config)
-
- bot, err := tg.NewBot(telegramToken, tg.WithDefaultLogger(false, true))
+ telegramApi, err := tg.NewBot(telegramToken, tg.WithDefaultLogger(false, true))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
- // Get updates channel
- updates, _ := bot.UpdatesViaLongPolling(nil)
+ botService := bot.NewBot(telegramApi, llmc, ext)
- // Stop reviving updates from update channel
- defer bot.StopLongPolling()
-
- // Loop through all updates when they came
- for update := range updates {
- // Check if update contains a message
- if update.Message != nil {
- slog.Info("Update with message received", update.Message.Chat, update.Message.From, update.Message.Text)
-
- chatID := tu.ID(update.Message.Chat.ID)
-
- req := openai.ChatCompletionRequest{
- Model: ModelMistralUncensored,
- Messages: []openai.ChatCompletionMessage{
- {
- Role: openai.ChatMessageRoleSystem,
- Content: "You're a bot in the Telegram chat. You are replying to questions directed to you.",
- },
- },
- }
-
- req.Messages = append(req.Messages, openai.ChatCompletionMessage{
- Role: openai.ChatMessageRoleUser,
- Content: update.Message.Text,
- })
- resp, err := client.CreateChatCompletion(context.Background(), req)
- if err != nil {
- slog.Error("ChatCompletion error", err)
-
- continue
- }
-
- slog.Info("Got completion. Going to send.", resp.Choices[0])
-
- message := tu.Message(
- chatID,
- resp.Choices[0].Message.Content,
- )
- message = message.WithReplyParameters(&tg.ReplyParameters{MessageID: update.Message.MessageID})
- message = message.WithParseMode("Markdown")
-
- _, err = bot.SendMessage(message)
-
- if err != nil {
- slog.Error("Can't send reply message", err)
- }
-
- //req.Messages = append(req.Messages, resp.Choices[0].Message)
- }
+ err = botService.Run()
+ if err != nil {
+ slog.Error("Running bot finished with an error", err)
+ os.Exit(1)
}
}