1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8
9 "github.com/patriceckhart/zot/packages/core"
10 "github.com/patriceckhart/zot/packages/provider"
11)
12
13// TelegramSender is the small affordance the telegram-send tools call
14// into. The real implementation lives in the interactive runtime and
15// forwards to the active *telegram.Bridge; tests can pass any stub.
16//
17// The kind argument distinguishes "photo" (compressed inline image
18// preview) from "document" (raw file attachment, no compression). For
19// images Telegram resizes to its preview format, which loses detail
20// but renders inline; documents preserve the original bytes but show
21// up as a file the recipient downloads.
22type TelegramSender interface {
23 // SendImage uploads path as an inline-rendered photo with an
24 // optional caption. Returns an error if the bridge is not
25 // active or the upload fails.
26 SendImage(ctx context.Context, path, caption string) error
27 // SendDocument uploads path as a raw attachment.
28 SendDocument(ctx context.Context, path, caption string) error
29 // Active reports whether a paired Telegram chat is currently
30 // reachable. Tools surface a clear error to the model when it
31 // tries to send without a connected bridge.
32 Active() bool
33}
34
35// TelegramSendImageTool exposes the bridge's photo-send affordance to
36// the model so a turn that comes in over Telegram can produce a real
37// image reply (a screenshot, a generated chart, a downloaded asset)
38// instead of a textual description of one. Only registered while the
39// bridge is connected; deregistered on disconnect.
40type TelegramSendImageTool struct {
41 CWD string
42 Sandbox *Sandbox
43 Sender TelegramSender
44}
45
46type telegramSendImageArgs struct {
47 Path string `json:"path"`
48 Caption string `json:"caption,omitempty"`
49}
50
51const telegramSendImageSchema = `{"type":"object","properties":{"path":{"type":"string","description":"absolute or cwd-relative path to a local image file (png/jpg/gif/webp)"},"caption":{"type":"string","description":"optional caption sent alongside the image"}},"required":["path"]}`
52
53func (t *TelegramSendImageTool) Name() string { return "telegram_send_image" }
54func (t *TelegramSendImageTool) Description() string {
55 return "Send a local image file to the paired Telegram chat as an inline photo. Use when the user (over Telegram) asks to see an image rather than have it described."
56}
57func (t *TelegramSendImageTool) Schema() json.RawMessage {
58 return json.RawMessage(telegramSendImageSchema)
59}
60
61func (t *TelegramSendImageTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
62 var a telegramSendImageArgs
63 if err := json.Unmarshal(raw, &a); err != nil {
64 return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)
65 }
66 if a.Path == "" {
67 return core.ToolResult{}, fmt.Errorf("path is required")
68 }
69 if t.Sender == nil || !t.Sender.Active() {
70 return core.ToolResult{
71 IsError: true,
72 Content: []provider.Content{provider.TextBlock{Text: "telegram bridge is not connected; cannot send image"}},
73 }, nil
74 }
75 path := resolvePath(t.CWD, a.Path)
76 if err := t.Sandbox.CheckPath(path); err != nil {
77 return core.ToolResult{}, err
78 }
79 info, err := os.Stat(path)
80 if err != nil {
81 return core.ToolResult{}, err
82 }
83 if info.IsDir() {
84 return core.ToolResult{}, fmt.Errorf("%s is a directory", path)
85 }
86 if mime := imageMIME(path); mime == "" {
87 return core.ToolResult{
88 IsError: true,
89 Content: []provider.Content{provider.TextBlock{Text: fmt.Sprintf("%s is not a recognised image format (png/jpg/gif/webp); use telegram_send_file for arbitrary attachments", path)}},
90 }, nil
91 }
92 if err := t.Sender.SendImage(ctx, path, a.Caption); err != nil {
93 return core.ToolResult{}, fmt.Errorf("send: %w", err)
94 }
95 kb := info.Size() / 1024
96 return core.ToolResult{
97 Content: []provider.Content{provider.TextBlock{Text: fmt.Sprintf("sent %s to telegram (%d KB)", path, kb)}},
98 }, nil
99}
100
101// TelegramSendFileTool uploads any local file to the paired chat as a
102// document attachment. Use this for non-image files or when the model
103// needs the recipient to receive the original bytes (no Telegram
104// compression). For images you usually want telegram_send_image.
105type TelegramSendFileTool struct {
106 CWD string
107 Sandbox *Sandbox
108 Sender TelegramSender
109}
110
111type telegramSendFileArgs struct {
112 Path string `json:"path"`
113 Caption string `json:"caption,omitempty"`
114}
115
116const telegramSendFileSchema = `{"type":"object","properties":{"path":{"type":"string","description":"absolute or cwd-relative path to any local file"},"caption":{"type":"string","description":"optional caption sent alongside the file"}},"required":["path"]}`
117
118func (t *TelegramSendFileTool) Name() string { return "telegram_send_file" }
119func (t *TelegramSendFileTool) Description() string {
120 return "Send a local file to the paired Telegram chat as a document attachment (no compression). Use for non-image files or when the recipient needs the original bytes."
121}
122func (t *TelegramSendFileTool) Schema() json.RawMessage {
123 return json.RawMessage(telegramSendFileSchema)
124}
125
126func (t *TelegramSendFileTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
127 var a telegramSendFileArgs
128 if err := json.Unmarshal(raw, &a); err != nil {
129 return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)
130 }
131 if a.Path == "" {
132 return core.ToolResult{}, fmt.Errorf("path is required")
133 }
134 if t.Sender == nil || !t.Sender.Active() {
135 return core.ToolResult{
136 IsError: true,
137 Content: []provider.Content{provider.TextBlock{Text: "telegram bridge is not connected; cannot send file"}},
138 }, nil
139 }
140 path := resolvePath(t.CWD, a.Path)
141 if err := t.Sandbox.CheckPath(path); err != nil {
142 return core.ToolResult{}, err
143 }
144 info, err := os.Stat(path)
145 if err != nil {
146 return core.ToolResult{}, err
147 }
148 if info.IsDir() {
149 return core.ToolResult{}, fmt.Errorf("%s is a directory", path)
150 }
151 if err := t.Sender.SendDocument(ctx, path, a.Caption); err != nil {
152 return core.ToolResult{}, fmt.Errorf("send: %w", err)
153 }
154 kb := info.Size() / 1024
155 return core.ToolResult{
156 Content: []provider.Content{provider.TextBlock{Text: fmt.Sprintf("sent %s to telegram (%d KB)", path, kb)}},
157 }, nil
158}