view.go 5.7 KB

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