1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8)
9
10type ToolInfo struct {
11 Name string
12 Description string
13 Parameters map[string]any
14 Required []string
15}
16
17type toolResponseType string
18
19type (
20 sessionIDContextKey string
21 messageIDContextKey string
22)
23
24const (
25 ToolResponseTypeText toolResponseType = "text"
26 ToolResponseTypeImage toolResponseType = "image"
27
28 SessionIDContextKey sessionIDContextKey = "session_id"
29 MessageIDContextKey messageIDContextKey = "message_id"
30
31 maxResponseWidth = 3000
32 maxResponseHeight = 5000
33 maxResponseChars = 50000
34)
35
36type ToolResponse struct {
37 Type toolResponseType `json:"type"`
38 Content string `json:"content"`
39 Metadata string `json:"metadata,omitempty"`
40 IsError bool `json:"is_error"`
41}
42
43func NewTextResponse(content string) ToolResponse {
44 return ToolResponse{
45 Type: ToolResponseTypeText,
46 Content: truncateContent(content),
47 }
48}
49
50func truncateContent(content string) string {
51 if len(content) <= maxResponseChars {
52 return truncateWidthAndHeight(content)
53 }
54
55 truncated := content[:maxResponseChars]
56
57 if lastNewline := strings.LastIndex(truncated, "\n"); lastNewline > maxResponseChars/2 {
58 truncated = truncated[:lastNewline]
59 }
60
61 truncated += "\n\n... [Content truncated due to length] ..."
62
63 return truncateWidthAndHeight(truncated)
64}
65
66func truncateWidthAndHeight(content string) string {
67 lines := strings.Split(content, "\n")
68
69 heightTruncated := false
70 if len(lines) > maxResponseHeight {
71 keepLines := maxResponseHeight - 3
72 firstHalf := keepLines / 2
73 secondHalf := keepLines - firstHalf
74
75 truncatedLines := make([]string, 0, maxResponseHeight)
76 truncatedLines = append(truncatedLines, lines[:firstHalf]...)
77 truncatedLines = append(truncatedLines, "")
78 truncatedLines = append(truncatedLines, fmt.Sprintf("... [%d lines truncated] ...", len(lines)-keepLines))
79 truncatedLines = append(truncatedLines, "")
80 truncatedLines = append(truncatedLines, lines[len(lines)-secondHalf:]...)
81
82 lines = truncatedLines
83 heightTruncated = true
84 }
85
86 widthTruncated := false
87 for i, line := range lines {
88 if len(line) > maxResponseWidth {
89 if maxResponseWidth > 20 {
90 keepChars := maxResponseWidth - 10
91 firstHalf := keepChars / 2
92 secondHalf := keepChars - firstHalf
93 lines[i] = line[:firstHalf] + " ... " + line[len(line)-secondHalf:]
94 } else {
95 lines[i] = line[:maxResponseWidth]
96 }
97 widthTruncated = true
98 }
99 }
100
101 result := strings.Join(lines, "\n")
102
103 if heightTruncated || widthTruncated {
104 notices := make([]string, 0, 2)
105 if heightTruncated {
106 notices = append(notices, "height")
107 }
108 if widthTruncated {
109 notices = append(notices, "width")
110 }
111 result += fmt.Sprintf("\n\n[Note: Content truncated by %s to fit response limits]", strings.Join(notices, " and "))
112 }
113
114 return result
115}
116
117func WithResponseMetadata(response ToolResponse, metadata any) ToolResponse {
118 if metadata != nil {
119 metadataBytes, err := json.Marshal(metadata)
120 if err != nil {
121 return response
122 }
123 response.Metadata = string(metadataBytes)
124 }
125 return response
126}
127
128func NewTextErrorResponse(content string) ToolResponse {
129 return ToolResponse{
130 Type: ToolResponseTypeText,
131 Content: content,
132 IsError: true,
133 }
134}
135
136type ToolCall struct {
137 ID string `json:"id"`
138 Name string `json:"name"`
139 Input string `json:"input"`
140}
141
142type BaseTool interface {
143 Info() ToolInfo
144 Name() string
145 Run(ctx context.Context, params ToolCall) (ToolResponse, error)
146}
147
148func GetContextValues(ctx context.Context) (string, string) {
149 sessionID := ctx.Value(SessionIDContextKey)
150 messageID := ctx.Value(MessageIDContextKey)
151 if sessionID == nil {
152 return "", ""
153 }
154 if messageID == nil {
155 return sessionID.(string), ""
156 }
157 return sessionID.(string), messageID.(string)
158}