ls.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. package tools
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "github.com/cloudwego/eino/components/tool"
  10. "github.com/cloudwego/eino/schema"
  11. )
  12. type lsTool struct {
  13. workingDir string
  14. }
  15. const (
  16. LSToolName = "ls"
  17. MaxFiles = 1000
  18. TruncatedMessage = "There are more than 1000 files in the repository. Use the LS tool (passing a specific path), Bash tool, and other tools to explore nested directories. The first 1000 files and directories are included below:\n\n"
  19. )
  20. type LSParams struct {
  21. Path string `json:"path"`
  22. Ignore []string `json:"ignore"`
  23. }
  24. func (b *lsTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
  25. return &schema.ToolInfo{
  26. Name: LSToolName,
  27. Desc: "Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.",
  28. ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
  29. "path": {
  30. Type: "string",
  31. Desc: "The absolute path to the directory to list (must be absolute, not relative)",
  32. Required: true,
  33. },
  34. "ignore": {
  35. Type: "array",
  36. ElemInfo: &schema.ParameterInfo{
  37. Type: schema.String,
  38. Desc: "List of glob patterns to ignore",
  39. },
  40. },
  41. }),
  42. }, nil
  43. }
  44. func (b *lsTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
  45. var params LSParams
  46. if err := json.Unmarshal([]byte(args), &params); err != nil {
  47. return "", err
  48. }
  49. if !filepath.IsAbs(params.Path) {
  50. return fmt.Sprintf("path must be absolute, got: %s", params.Path), nil
  51. }
  52. files, err := b.listDirectory(params.Path)
  53. if err != nil {
  54. return fmt.Sprintf("error listing directory: %s", err), nil
  55. }
  56. tree := createFileTree(files)
  57. output := printTree(tree, params.Path)
  58. if len(files) >= MaxFiles {
  59. output = TruncatedMessage + output
  60. }
  61. return output, nil
  62. }
  63. func (b *lsTool) listDirectory(initialPath string) ([]string, error) {
  64. var results []string
  65. err := filepath.Walk(initialPath, func(path string, info os.FileInfo, err error) error {
  66. if err != nil {
  67. return nil // Skip files we don't have permission to access
  68. }
  69. if shouldSkip(path) {
  70. if info.IsDir() {
  71. return filepath.SkipDir
  72. }
  73. return nil
  74. }
  75. if path != initialPath {
  76. if info.IsDir() {
  77. path = path + string(filepath.Separator)
  78. }
  79. relPath, err := filepath.Rel(b.workingDir, path)
  80. if err == nil {
  81. results = append(results, relPath)
  82. } else {
  83. results = append(results, path)
  84. }
  85. }
  86. if len(results) >= MaxFiles {
  87. return fmt.Errorf("max files reached")
  88. }
  89. return nil
  90. })
  91. if err != nil && err.Error() != "max files reached" {
  92. return nil, err
  93. }
  94. return results, nil
  95. }
  96. func shouldSkip(path string) bool {
  97. base := filepath.Base(path)
  98. if base != "." && strings.HasPrefix(base, ".") {
  99. return true
  100. }
  101. if strings.Contains(path, filepath.Join("__pycache__", "")) {
  102. return true
  103. }
  104. return false
  105. }
  106. type TreeNode struct {
  107. Name string `json:"name"`
  108. Path string `json:"path"`
  109. Type string `json:"type"` // "file" or "directory"
  110. Children []TreeNode `json:"children,omitempty"`
  111. }
  112. func createFileTree(sortedPaths []string) []TreeNode {
  113. root := []TreeNode{}
  114. for _, path := range sortedPaths {
  115. parts := strings.Split(path, string(filepath.Separator))
  116. currentLevel := &root
  117. currentPath := ""
  118. for i, part := range parts {
  119. if part == "" {
  120. continue
  121. }
  122. if currentPath == "" {
  123. currentPath = part
  124. } else {
  125. currentPath = filepath.Join(currentPath, part)
  126. }
  127. isLastPart := i == len(parts)-1
  128. isDir := !isLastPart || strings.HasSuffix(path, string(filepath.Separator))
  129. found := false
  130. for i := range *currentLevel {
  131. if (*currentLevel)[i].Name == part {
  132. found = true
  133. if (*currentLevel)[i].Children != nil {
  134. currentLevel = &(*currentLevel)[i].Children
  135. }
  136. break
  137. }
  138. }
  139. if !found {
  140. nodeType := "file"
  141. if isDir {
  142. nodeType = "directory"
  143. }
  144. newNode := TreeNode{
  145. Name: part,
  146. Path: currentPath,
  147. Type: nodeType,
  148. }
  149. if isDir {
  150. newNode.Children = []TreeNode{}
  151. *currentLevel = append(*currentLevel, newNode)
  152. currentLevel = &(*currentLevel)[len(*currentLevel)-1].Children
  153. } else {
  154. *currentLevel = append(*currentLevel, newNode)
  155. }
  156. }
  157. }
  158. }
  159. return root
  160. }
  161. func printTree(tree []TreeNode, rootPath string) string {
  162. var result strings.Builder
  163. result.WriteString(fmt.Sprintf("- %s%s\n", rootPath, string(filepath.Separator)))
  164. printTreeRecursive(&result, tree, 0, " ")
  165. return result.String()
  166. }
  167. func printTreeRecursive(builder *strings.Builder, tree []TreeNode, level int, prefix string) {
  168. for _, node := range tree {
  169. linePrefix := prefix + "- "
  170. nodeName := node.Name
  171. if node.Type == "directory" {
  172. nodeName += string(filepath.Separator)
  173. }
  174. fmt.Fprintf(builder, "%s%s\n", linePrefix, nodeName)
  175. if node.Type == "directory" && len(node.Children) > 0 {
  176. printTreeRecursive(builder, node.Children, level+1, prefix+" ")
  177. }
  178. }
  179. }
  180. func NewLsTool(workingDir string) tool.InvokableTool {
  181. return &lsTool{
  182. workingDir,
  183. }
  184. }