view.go

  1package tools
  2
  3import (
  4	"bufio"
  5	"context"
  6	"encoding/json"
  7	"fmt"
  8	"io"
  9	"os"
 10	"path/filepath"
 11	"strings"
 12
 13	"github.com/cloudwego/eino/components/tool"
 14	"github.com/cloudwego/eino/schema"
 15)
 16
 17type viewTool struct {
 18	workingDir string
 19}
 20
 21const (
 22	ViewToolName = "view"
 23
 24	MaxReadSize = 250 * 1024
 25
 26	DefaultReadLimit = 2000
 27
 28	MaxLineLength = 2000
 29)
 30
 31type ViewPatams struct {
 32	FilePath string `json:"file_path"`
 33	Offset   int    `json:"offset"`
 34	Limit    int    `json:"limit"`
 35}
 36
 37func (b *viewTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
 38	return &schema.ToolInfo{
 39		Name: ViewToolName,
 40		Desc: `Reads a file from the local filesystem. The file_path parameter must be an absolute path, not a relative path. By default, it reads up to 2000 lines starting from the beginning of the file. You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters. Any lines longer than 2000 characters will be truncated. For image files, the tool will display the image for you.`,
 41		ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
 42			"file_path": {
 43				Type:     "string",
 44				Desc:     "The absolute path to the file to read",
 45				Required: true,
 46			},
 47			"offset": {
 48				Type: "int",
 49				Desc: "The line number to start reading from. Only provide if the file is too large to read at once",
 50			},
 51			"limit": {
 52				Type: "int",
 53				Desc: "The number of lines to read. Only provide if the file is too large to read at once.",
 54			},
 55		}),
 56	}, nil
 57}
 58
 59func (b *viewTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
 60	var params ViewPatams
 61	if err := json.Unmarshal([]byte(args), &params); err != nil {
 62		return fmt.Sprintf("failed to parse parameters: %s", err), nil
 63	}
 64
 65	if params.FilePath == "" {
 66		return "file_path is required", nil
 67	}
 68
 69	if !filepath.IsAbs(params.FilePath) {
 70		return fmt.Sprintf("file path must be absolute, got: %s", params.FilePath), nil
 71	}
 72
 73	fileInfo, err := os.Stat(params.FilePath)
 74	if err != nil {
 75		if os.IsNotExist(err) {
 76			dir := filepath.Dir(params.FilePath)
 77			base := filepath.Base(params.FilePath)
 78
 79			dirEntries, dirErr := os.ReadDir(dir)
 80			if dirErr == nil {
 81				var suggestions []string
 82				for _, entry := range dirEntries {
 83					if strings.Contains(entry.Name(), base) || strings.Contains(base, entry.Name()) {
 84						suggestions = append(suggestions, filepath.Join(dir, entry.Name()))
 85						if len(suggestions) >= 3 {
 86							break
 87						}
 88					}
 89				}
 90
 91				if len(suggestions) > 0 {
 92					return fmt.Sprintf("file not found: %s. Did you mean one of these?\n%s",
 93						params.FilePath, strings.Join(suggestions, "\n")), nil
 94				}
 95			}
 96
 97			return fmt.Sprintf("file not found: %s", params.FilePath), nil
 98		}
 99		return fmt.Sprintf("failed to access file: %s", err), nil
100	}
101
102	if fileInfo.IsDir() {
103		return fmt.Sprintf("path is a directory, not a file: %s", params.FilePath), nil
104	}
105
106	if fileInfo.Size() > MaxReadSize {
107		return fmt.Sprintf("file is too large (%d bytes). Maximum size is %d bytes",
108			fileInfo.Size(), MaxReadSize), nil
109	}
110
111	if params.Limit <= 0 {
112		params.Limit = DefaultReadLimit
113	}
114
115	isImage, _ := isImageFile(params.FilePath)
116	if isImage {
117		// TODO: Implement image reading
118		return "reading images is not supported", nil
119	}
120
121	content, _, err := readTextFile(params.FilePath, params.Offset, params.Limit)
122	if err != nil {
123		return fmt.Sprintf("failed to read file: %s", err), nil
124	}
125
126	recordFileRead(params.FilePath)
127
128	return addLineNumbers(content, params.Offset+1), nil
129}
130
131func addLineNumbers(content string, startLine int) string {
132	if content == "" {
133		return ""
134	}
135
136	lines := strings.Split(content, "\n")
137
138	var result []string
139	for i, line := range lines {
140		line = strings.TrimSuffix(line, "\r")
141
142		lineNum := i + startLine
143		numStr := fmt.Sprintf("%d", lineNum)
144
145		if len(numStr) >= 6 {
146			result = append(result, fmt.Sprintf("%s\t%s", numStr, line))
147		} else {
148			paddedNum := fmt.Sprintf("%6s", numStr)
149			result = append(result, fmt.Sprintf("%s\t|%s", paddedNum, line))
150		}
151	}
152
153	return strings.Join(result, "\n")
154}
155
156func readTextFile(filePath string, offset, limit int) (string, int, error) {
157	file, err := os.Open(filePath)
158	if err != nil {
159		return "", 0, err
160	}
161	defer file.Close()
162
163	lineCount := 0
164	if offset > 0 {
165		scanner := NewLineScanner(file)
166		for lineCount < offset && scanner.Scan() {
167			lineCount++
168		}
169		if err = scanner.Err(); err != nil {
170			return "", 0, err
171		}
172	}
173
174	if offset == 0 {
175		_, err = file.Seek(0, io.SeekStart)
176		if err != nil {
177			return "", 0, err
178		}
179	}
180
181	var lines []string
182	lineCount = offset
183	scanner := NewLineScanner(file)
184
185	for scanner.Scan() && len(lines) < limit {
186		lineCount++
187		lineText := scanner.Text()
188		if len(lineText) > MaxLineLength {
189			lineText = lineText[:MaxLineLength] + "..."
190		}
191		lines = append(lines, lineText)
192	}
193
194	if err := scanner.Err(); err != nil {
195		return "", 0, err
196	}
197
198	return strings.Join(lines, "\n"), lineCount, nil
199}
200
201func isImageFile(filePath string) (bool, string) {
202	ext := strings.ToLower(filepath.Ext(filePath))
203	switch ext {
204	case ".jpg", ".jpeg":
205		return true, "jpeg"
206	case ".png":
207		return true, "png"
208	case ".gif":
209		return true, "gif"
210	case ".bmp":
211		return true, "bmp"
212	case ".svg":
213		return true, "svg"
214	case ".webp":
215		return true, "webp"
216	default:
217		return false, ""
218	}
219}
220
221type LineScanner struct {
222	scanner *bufio.Scanner
223}
224
225func NewLineScanner(r io.Reader) *LineScanner {
226	return &LineScanner{
227		scanner: bufio.NewScanner(r),
228	}
229}
230
231func (s *LineScanner) Scan() bool {
232	return s.scanner.Scan()
233}
234
235func (s *LineScanner) Text() string {
236	return s.scanner.Text()
237}
238
239func (s *LineScanner) Err() error {
240	return s.scanner.Err()
241}
242
243func NewViewTool(workingDir string) tool.InvokableTool {
244	return &viewTool{
245		workingDir,
246	}
247}