Skip to content

Commit f0df9bb

Browse files
authored
Merge pull request #11 from mutablelogic/dev
Added telegram as a test
2 parents dfbc6b3 + b41ff7d commit f0df9bb

File tree

6 files changed

+350
-13
lines changed

6 files changed

+350
-13
lines changed

cmd/llm/chat2.go

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"log"
7+
"sync"
8+
"time"
9+
10+
// Packages
11+
llm "github.com/mutablelogic/go-llm"
12+
telegram "github.com/mutablelogic/go-llm/pkg/ui/telegram"
13+
)
14+
15+
////////////////////////////////////////////////////////////////////////////////
16+
// TYPES
17+
18+
type Chat2Cmd struct {
19+
Model string `arg:"" help:"Model name"`
20+
Token string `env:"TELEGRAM_TOKEN" help:"Telegram token" required:""`
21+
}
22+
23+
type Server struct {
24+
sync.RWMutex
25+
*telegram.Client
26+
27+
// Model and toolkit
28+
toolkit llm.ToolKit
29+
model llm.Model
30+
31+
// Map of active sessions
32+
sessions map[string]llm.Context
33+
}
34+
35+
////////////////////////////////////////////////////////////////////////////////
36+
// LIFECYCLE
37+
38+
func NewTelegramServer(token string, model llm.Model, toolkit llm.ToolKit, opts ...telegram.Opt) (*Server, error) {
39+
server := new(Server)
40+
server.sessions = make(map[string]llm.Context)
41+
server.model = model
42+
server.toolkit = toolkit
43+
44+
// Create a new telegram client
45+
opts = append(opts, telegram.WithCallback(server.receive))
46+
if telegram, err := telegram.New(token, opts...); err != nil {
47+
return nil, err
48+
} else {
49+
server.Client = telegram
50+
}
51+
52+
// Return success
53+
return server, nil
54+
}
55+
56+
////////////////////////////////////////////////////////////////////////////////
57+
// PUBLIC METHODS
58+
59+
func (cmd *Chat2Cmd) Run(globals *Globals) error {
60+
return run(globals, cmd.Model, func(ctx context.Context, model llm.Model) error {
61+
server, err := NewTelegramServer(cmd.Token, model, globals.toolkit, telegram.WithDebug(globals.Debug))
62+
if err != nil {
63+
return err
64+
}
65+
66+
log.Printf("Running Telegram bot %q\n", server.Client.Name())
67+
68+
var result error
69+
var wg sync.WaitGroup
70+
wg.Add(2)
71+
go func(ctx context.Context) {
72+
defer wg.Done()
73+
if err := server.Run(ctx); err != nil {
74+
result = errors.Join(result, err)
75+
}
76+
}(ctx)
77+
go func(ctx context.Context) {
78+
defer wg.Done()
79+
ticker := time.NewTicker(time.Minute)
80+
defer ticker.Stop()
81+
for {
82+
select {
83+
case <-ctx.Done():
84+
return
85+
case <-ticker.C:
86+
server.Purge()
87+
}
88+
}
89+
}(ctx)
90+
91+
// Wait for completion
92+
wg.Wait()
93+
94+
// Return any errors
95+
return result
96+
})
97+
}
98+
99+
// //////////////////////////////////////////////////////////////////////////////
100+
// PRIVATE METHODS
101+
102+
func (telegram *Server) Purge() {
103+
telegram.Lock()
104+
defer telegram.Unlock()
105+
for user, session := range telegram.sessions {
106+
if session.SinceLast() > 10*time.Minute {
107+
log.Printf("Purging session for %q\n", user)
108+
delete(telegram.sessions, user)
109+
}
110+
}
111+
}
112+
113+
func (telegram *Server) session(user string) llm.Context {
114+
telegram.Lock()
115+
defer telegram.Unlock()
116+
if session, exists := telegram.sessions[user]; exists {
117+
return session
118+
}
119+
session := telegram.model.Context(
120+
llm.WithToolKit(telegram.toolkit),
121+
llm.WithSystemPrompt("Please reply to messages in markdown format."),
122+
)
123+
telegram.sessions[user] = session
124+
return session
125+
}
126+
127+
func (telegram *Server) receive(ctx context.Context, msg telegram.Message) error {
128+
// Get an active session
129+
session := telegram.session(msg.Sender())
130+
131+
// Process the message
132+
text := msg.Text()
133+
text += "\n\nPlease reply in markdown format."
134+
if err := session.FromUser(ctx, text); err != nil {
135+
return err
136+
}
137+
138+
// Run tool calls
139+
for {
140+
calls := session.ToolCalls(0)
141+
if len(calls) == 0 {
142+
break
143+
}
144+
if text := session.Text(0); text != "" {
145+
msg.Reply(ctx, text, false)
146+
} else {
147+
msg.Reply(ctx, "_Gathering information_", true)
148+
}
149+
150+
results, err := telegram.toolkit.Run(ctx, calls...)
151+
if err != nil {
152+
return err
153+
} else if err := session.FromTool(ctx, results...); err != nil {
154+
return err
155+
}
156+
}
157+
158+
// Reply with the text
159+
return msg.Reply(ctx, session.Text(0), true)
160+
}

cmd/llm/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ type CLI struct {
7878
// Commands
7979
Download DownloadModelCmd `cmd:"" help:"Download a model"`
8080
Chat ChatCmd `cmd:"" help:"Start a chat session"`
81+
Chat2 Chat2Cmd `cmd:"" help:"Start a chat session (2)"`
8182
Complete CompleteCmd `cmd:"" help:"Complete a prompt"`
8283
Embedding EmbeddingCmd `cmd:"" help:"Generate an embedding"`
8384
Version VersionCmd `cmd:"" help:"Print the version of this tool"`

context.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package llm
22

3-
import "context"
3+
import (
4+
"context"
5+
"time"
6+
)
47

58
//////////////////////////////////////////////////////////////////
69
// TYPES
@@ -44,4 +47,8 @@ type Context interface {
4447
// Generate a response from a tool, passing the results
4548
// from the tool call
4649
FromTool(context.Context, ...ToolResult) error
50+
51+
// Return the duration since the last completion was made
52+
// or zero
53+
SinceLast() time.Duration
4754
}

pkg/internal/impl/session.go

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package impl
33
import (
44
"context"
55
"encoding/json"
6+
"time"
67

78
// Packages
89
"github.com/mutablelogic/go-llm"
@@ -37,6 +38,7 @@ type session struct {
3738
opts []llm.Opt // Options to apply to the session
3839
seq []llm.Completion // Sequence of messages
3940
factory MessageFactory // Factory for generating messages
41+
last time.Time // Last completion time
4042
}
4143

4244
var _ llm.Context = (*session)(nil)
@@ -129,10 +131,20 @@ func (session *session) chat(ctx context.Context, messages ...llm.Completion) er
129131
// Append the first choice
130132
session.Append(completion.Choice(0))
131133

134+
// Update the last completion time
135+
session.last = time.Now()
136+
132137
// Success
133138
return nil
134139
}
135140

141+
func (session *session) SinceLast() time.Duration {
142+
if len(session.seq) == 0 || session.last.IsZero() {
143+
return 0
144+
}
145+
return time.Since(session.last)
146+
}
147+
136148
///////////////////////////////////////////////////////////////////////////////
137149
// PUBLIC METHODS - COMPLETION
138150

pkg/ui/telegram/opt.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package telegram
2+
3+
import "context"
4+
5+
///////////////////////////////////////////////////////////////////////////////
6+
// TYPES
7+
8+
// A generic option type, which can set options on an agent or session
9+
type Opt func(*opts) error
10+
11+
// set of options
12+
type opts struct {
13+
token string
14+
callback CallbackFunc
15+
debug bool
16+
}
17+
18+
type CallbackFunc func(context.Context, Message) error
19+
20+
////////////////////////////////////////////////////////////////////////////////
21+
// LIFECYCLE
22+
23+
// applyOpts returns a structure of options
24+
func applyOpts(token string, opt ...Opt) (*opts, error) {
25+
o := new(opts)
26+
o.token = token
27+
for _, opt := range opt {
28+
if err := opt(o); err != nil {
29+
return nil, err
30+
}
31+
}
32+
return o, nil
33+
}
34+
35+
////////////////////////////////////////////////////////////////////////////////
36+
// PUBLIC METHODS
37+
38+
func WithCallback(fn CallbackFunc) Opt {
39+
return func(o *opts) error {
40+
o.callback = fn
41+
return nil
42+
}
43+
}
44+
45+
func WithDebug(v bool) Opt {
46+
return func(o *opts) error {
47+
o.debug = v
48+
return nil
49+
}
50+
}

0 commit comments

Comments
 (0)