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), ¶ms); 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}