Refactoring structure from single file to several separated services. Adding new feature: "summarize" to generate bullet points for provided link.
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Alexey Skobkin 2024-03-10 04:51:01 +03:00
parent 971ac147ac
commit 8939b2fb62
No known key found for this signature in database
GPG Key ID: 5D5CEF6F221278E7
6 changed files with 465 additions and 65 deletions

224
bot/bot.go Normal file
View File

@ -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 <link>\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 <text> - Ask something from LLM\r\n"+
"/summarize <link> - 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)
}
}

42
extractor/extractor.go Normal file
View File

@ -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
}

14
go.mod
View File

@ -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
)

74
go.sum
View File

@ -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=

100
llm/llm.go Normal file
View File

@ -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
}

76
main.go
View File

@ -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)
}
}