tools.go

  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}