grep.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. package tools
  2. import (
  3. "bufio"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "regexp"
  11. "sort"
  12. "strings"
  13. "time"
  14. "github.com/cloudwego/eino/components/tool"
  15. "github.com/cloudwego/eino/schema"
  16. )
  17. type grepTool struct {
  18. workingDir string
  19. }
  20. const (
  21. GrepToolName = "grep"
  22. MaxGrepResults = 100
  23. )
  24. type GrepParams struct {
  25. Pattern string `json:"pattern"`
  26. Path string `json:"path"`
  27. Include string `json:"include"`
  28. }
  29. type grepMatch struct {
  30. path string
  31. modTime time.Time
  32. }
  33. func (b *grepTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
  34. return &schema.ToolInfo{
  35. Name: GrepToolName,
  36. Desc: `- Fast content search tool that works with any codebase size
  37. - Searches file contents using regular expressions
  38. - Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
  39. - Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}")
  40. - Returns matching file paths sorted by modification time
  41. - Use this tool when you need to find files containing specific patterns
  42. - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead`,
  43. ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
  44. "command": {
  45. Type: "string",
  46. Desc: "The command to execute",
  47. Required: true,
  48. },
  49. "timeout": {
  50. Type: "number",
  51. Desc: "Optional timeout in milliseconds (max 600000)",
  52. },
  53. }),
  54. }, nil
  55. }
  56. func (b *grepTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
  57. var params GrepParams
  58. if err := json.Unmarshal([]byte(args), &params); err != nil {
  59. return "", err
  60. }
  61. searchPath := params.Path
  62. if searchPath == "" {
  63. var err error
  64. searchPath, err = os.Getwd()
  65. if err != nil {
  66. return fmt.Sprintf("unable to get current working directory: %s", err), nil
  67. }
  68. }
  69. matches, err := searchWithRipgrep(params.Pattern, searchPath, params.Include)
  70. if err != nil {
  71. matches, err = searchFilesWithRegex(params.Pattern, searchPath, params.Include)
  72. if err != nil {
  73. return fmt.Sprintf("error searching files: %s", err), nil
  74. }
  75. }
  76. sort.Slice(matches, func(i, j int) bool {
  77. return matches[i].modTime.After(matches[j].modTime)
  78. })
  79. truncated := false
  80. if len(matches) > MaxGrepResults {
  81. truncated = true
  82. matches = matches[:MaxGrepResults]
  83. }
  84. filenames := make([]string, len(matches))
  85. for i, m := range matches {
  86. filenames[i] = m.path
  87. }
  88. var output string
  89. if len(filenames) == 0 {
  90. output = "No files found"
  91. } else {
  92. output = fmt.Sprintf("Found %d file%s\n%s",
  93. len(filenames),
  94. pluralize(len(filenames)),
  95. strings.Join(filenames, "\n"))
  96. if truncated {
  97. output += "\n(Results are truncated. Consider using a more specific path or pattern.)"
  98. }
  99. }
  100. return output, nil
  101. }
  102. func pluralize(count int) string {
  103. if count == 1 {
  104. return ""
  105. }
  106. return "s"
  107. }
  108. func searchWithRipgrep(pattern, path, include string) ([]grepMatch, error) {
  109. _, err := exec.LookPath("rg")
  110. if err != nil {
  111. return nil, fmt.Errorf("ripgrep not found: %w", err)
  112. }
  113. args := []string{"-l", pattern}
  114. if include != "" {
  115. args = append(args, "--glob", include)
  116. }
  117. args = append(args, path)
  118. cmd := exec.Command("rg", args...)
  119. output, err := cmd.Output()
  120. if err != nil {
  121. if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
  122. return []grepMatch{}, nil
  123. }
  124. return nil, err
  125. }
  126. lines := strings.Split(strings.TrimSpace(string(output)), "\n")
  127. matches := make([]grepMatch, 0, len(lines))
  128. for _, line := range lines {
  129. if line == "" {
  130. continue
  131. }
  132. fileInfo, err := os.Stat(line)
  133. if err != nil {
  134. continue
  135. }
  136. matches = append(matches, grepMatch{
  137. path: line,
  138. modTime: fileInfo.ModTime(),
  139. })
  140. }
  141. return matches, nil
  142. }
  143. func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
  144. matches := []grepMatch{}
  145. regex, err := regexp.Compile(pattern)
  146. if err != nil {
  147. return nil, fmt.Errorf("invalid regex pattern: %w", err)
  148. }
  149. var includePattern *regexp.Regexp
  150. if include != "" {
  151. regexPattern := globToRegex(include)
  152. includePattern, err = regexp.Compile(regexPattern)
  153. if err != nil {
  154. return nil, fmt.Errorf("invalid include pattern: %w", err)
  155. }
  156. }
  157. err = filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
  158. if err != nil {
  159. return nil
  160. }
  161. if info.IsDir() {
  162. return nil
  163. }
  164. if includePattern != nil && !includePattern.MatchString(path) {
  165. return nil
  166. }
  167. match, err := fileContainsPattern(path, regex)
  168. if err != nil {
  169. return nil
  170. }
  171. if match {
  172. matches = append(matches, grepMatch{
  173. path: path,
  174. modTime: info.ModTime(),
  175. })
  176. }
  177. return nil
  178. })
  179. if err != nil {
  180. return nil, err
  181. }
  182. return matches, nil
  183. }
  184. func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, error) {
  185. file, err := os.Open(filePath)
  186. if err != nil {
  187. return false, err
  188. }
  189. defer file.Close()
  190. scanner := bufio.NewScanner(file)
  191. for scanner.Scan() {
  192. if pattern.MatchString(scanner.Text()) {
  193. return true, nil
  194. }
  195. }
  196. if err := scanner.Err(); err != nil {
  197. return false, err
  198. }
  199. return false, nil
  200. }
  201. func globToRegex(glob string) string {
  202. regexPattern := strings.ReplaceAll(glob, ".", "\\.")
  203. regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
  204. regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
  205. re := regexp.MustCompile(`\{([^}]+)\}`)
  206. regexPattern = re.ReplaceAllStringFunc(regexPattern, func(match string) string {
  207. inner := match[1 : len(match)-1]
  208. return "(" + strings.ReplaceAll(inner, ",", "|") + ")"
  209. })
  210. return "^" + regexPattern + "$"
  211. }
  212. func NewGrepTool(workingDir string) tool.InvokableTool {
  213. return &grepTool{
  214. workingDir,
  215. }
  216. }