rework llm
This commit is contained in:
@@ -8,19 +8,14 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/kujtimiihoxha/termai/internal/config"
|
||||
)
|
||||
|
||||
type lsTool struct {
|
||||
workingDir string
|
||||
}
|
||||
type lsTool struct{}
|
||||
|
||||
const (
|
||||
LSToolName = "ls"
|
||||
|
||||
MaxFiles = 1000
|
||||
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"
|
||||
MaxLSFiles = 1000
|
||||
)
|
||||
|
||||
type LSParams struct {
|
||||
@@ -28,61 +23,82 @@ type LSParams struct {
|
||||
Ignore []string `json:"ignore"`
|
||||
}
|
||||
|
||||
func (b *lsTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return &schema.ToolInfo{
|
||||
Name: LSToolName,
|
||||
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.",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"path": {
|
||||
Type: "string",
|
||||
Desc: "The absolute path to the directory to list (must be absolute, not relative)",
|
||||
Required: true,
|
||||
},
|
||||
"ignore": {
|
||||
Type: "array",
|
||||
ElemInfo: &schema.ParameterInfo{
|
||||
Type: schema.String,
|
||||
Desc: "List of glob patterns to ignore",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
type TreeNode struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"` // "file" or "directory"
|
||||
Children []*TreeNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func (b *lsTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
|
||||
func (l *lsTool) Info() ToolInfo {
|
||||
return ToolInfo{
|
||||
Name: LSToolName,
|
||||
Description: lsDescription(),
|
||||
Parameters: map[string]any{
|
||||
"path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The path to the directory to list (defaults to current working directory)",
|
||||
},
|
||||
"ignore": map[string]any{
|
||||
"type": "array",
|
||||
"description": "List of glob patterns to ignore",
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"path"},
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements Tool.
|
||||
func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
|
||||
var params LSParams
|
||||
if err := json.Unmarshal([]byte(args), ¶ms); err != nil {
|
||||
return "", err
|
||||
if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil {
|
||||
return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(params.Path) {
|
||||
return fmt.Sprintf("path must be absolute, got: %s", params.Path), nil
|
||||
// If path is empty, use current working directory
|
||||
searchPath := params.Path
|
||||
if searchPath == "" {
|
||||
searchPath = config.WorkingDirectory()
|
||||
}
|
||||
|
||||
files, err := b.listDirectory(params.Path)
|
||||
// Ensure the path is absolute
|
||||
if !filepath.IsAbs(searchPath) {
|
||||
searchPath = filepath.Join(config.WorkingDirectory(), searchPath)
|
||||
}
|
||||
|
||||
// Check if the path exists
|
||||
if _, err := os.Stat(searchPath); os.IsNotExist(err) {
|
||||
return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
|
||||
}
|
||||
|
||||
files, truncated, err := listDirectory(searchPath, params.Ignore, MaxLSFiles)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error listing directory: %s", err), nil
|
||||
return NewTextErrorResponse(fmt.Sprintf("error listing directory: %s", err)), nil
|
||||
}
|
||||
|
||||
tree := createFileTree(files)
|
||||
output := printTree(tree, params.Path)
|
||||
output := printTree(tree, searchPath)
|
||||
|
||||
if len(files) >= MaxFiles {
|
||||
output = TruncatedMessage + output
|
||||
if truncated {
|
||||
output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %d files and directories are included below:\n\n%s", MaxLSFiles, MaxLSFiles, output)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
return NewTextResponse(output), nil
|
||||
}
|
||||
|
||||
func (b *lsTool) listDirectory(initialPath string) ([]string, error) {
|
||||
func listDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
|
||||
var results []string
|
||||
truncated := false
|
||||
|
||||
err := filepath.Walk(initialPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip files we don't have permission to access
|
||||
}
|
||||
|
||||
if shouldSkip(path) {
|
||||
if shouldSkip(path, ignorePatterns) {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
@@ -93,137 +109,212 @@ func (b *lsTool) listDirectory(initialPath string) ([]string, error) {
|
||||
if info.IsDir() {
|
||||
path = path + string(filepath.Separator)
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(b.workingDir, path)
|
||||
if err == nil {
|
||||
results = append(results, relPath)
|
||||
} else {
|
||||
results = append(results, path)
|
||||
}
|
||||
results = append(results, path)
|
||||
}
|
||||
|
||||
if len(results) >= MaxFiles {
|
||||
return fmt.Errorf("max files reached")
|
||||
if len(results) >= limit {
|
||||
truncated = true
|
||||
return filepath.SkipAll
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil && err.Error() != "max files reached" {
|
||||
return nil, err
|
||||
if err != nil {
|
||||
return nil, truncated, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
return results, truncated, nil
|
||||
}
|
||||
|
||||
func shouldSkip(path string) bool {
|
||||
func shouldSkip(path string, ignorePatterns []string) bool {
|
||||
base := filepath.Base(path)
|
||||
|
||||
// Skip hidden files and directories
|
||||
if base != "." && strings.HasPrefix(base, ".") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip common directories and files
|
||||
commonIgnored := []string{
|
||||
"__pycache__",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
"target",
|
||||
"vendor",
|
||||
"bin",
|
||||
"obj",
|
||||
".git",
|
||||
".idea",
|
||||
".vscode",
|
||||
".DS_Store",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.pyd",
|
||||
"*.so",
|
||||
"*.dll",
|
||||
"*.exe",
|
||||
}
|
||||
|
||||
// Skip __pycache__ directories
|
||||
if strings.Contains(path, filepath.Join("__pycache__", "")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check against common ignored patterns
|
||||
for _, ignored := range commonIgnored {
|
||||
if strings.HasSuffix(ignored, "/") {
|
||||
// Directory pattern
|
||||
if strings.Contains(path, filepath.Join(ignored[:len(ignored)-1], "")) {
|
||||
return true
|
||||
}
|
||||
} else if strings.HasPrefix(ignored, "*.") {
|
||||
// File extension pattern
|
||||
if strings.HasSuffix(base, ignored[1:]) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Exact match
|
||||
if base == ignored {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check against ignore patterns
|
||||
for _, pattern := range ignorePatterns {
|
||||
matched, err := filepath.Match(pattern, base)
|
||||
if err == nil && matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type TreeNode struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"` // "file" or "directory"
|
||||
Children []TreeNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func createFileTree(sortedPaths []string) []TreeNode {
|
||||
root := []TreeNode{}
|
||||
func createFileTree(sortedPaths []string) []*TreeNode {
|
||||
root := []*TreeNode{}
|
||||
pathMap := make(map[string]*TreeNode)
|
||||
|
||||
for _, path := range sortedPaths {
|
||||
parts := strings.Split(path, string(filepath.Separator))
|
||||
currentLevel := &root
|
||||
currentPath := ""
|
||||
var parentPath string
|
||||
|
||||
var cleanParts []string
|
||||
for _, part := range parts {
|
||||
if part != "" {
|
||||
cleanParts = append(cleanParts, part)
|
||||
}
|
||||
}
|
||||
parts = cleanParts
|
||||
|
||||
if len(parts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if currentPath == "" {
|
||||
currentPath = part
|
||||
} else {
|
||||
currentPath = filepath.Join(currentPath, part)
|
||||
}
|
||||
|
||||
if _, exists := pathMap[currentPath]; exists {
|
||||
parentPath = currentPath
|
||||
continue
|
||||
}
|
||||
|
||||
isLastPart := i == len(parts)-1
|
||||
isDir := !isLastPart || strings.HasSuffix(path, string(filepath.Separator))
|
||||
|
||||
found := false
|
||||
for i := range *currentLevel {
|
||||
if (*currentLevel)[i].Name == part {
|
||||
found = true
|
||||
if (*currentLevel)[i].Children != nil {
|
||||
currentLevel = &(*currentLevel)[i].Children
|
||||
}
|
||||
break
|
||||
}
|
||||
nodeType := "file"
|
||||
if isDir {
|
||||
nodeType = "directory"
|
||||
}
|
||||
newNode := &TreeNode{
|
||||
Name: part,
|
||||
Path: currentPath,
|
||||
Type: nodeType,
|
||||
Children: []*TreeNode{},
|
||||
}
|
||||
|
||||
if !found {
|
||||
nodeType := "file"
|
||||
if isDir {
|
||||
nodeType = "directory"
|
||||
}
|
||||
pathMap[currentPath] = newNode
|
||||
|
||||
newNode := TreeNode{
|
||||
Name: part,
|
||||
Path: currentPath,
|
||||
Type: nodeType,
|
||||
}
|
||||
|
||||
if isDir {
|
||||
newNode.Children = []TreeNode{}
|
||||
*currentLevel = append(*currentLevel, newNode)
|
||||
currentLevel = &(*currentLevel)[len(*currentLevel)-1].Children
|
||||
} else {
|
||||
*currentLevel = append(*currentLevel, newNode)
|
||||
if i > 0 && parentPath != "" {
|
||||
if parent, ok := pathMap[parentPath]; ok {
|
||||
parent.Children = append(parent.Children, newNode)
|
||||
}
|
||||
} else {
|
||||
root = append(root, newNode)
|
||||
}
|
||||
|
||||
parentPath = currentPath
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
func printTree(tree []TreeNode, rootPath string) string {
|
||||
func printTree(tree []*TreeNode, rootPath string) string {
|
||||
var result strings.Builder
|
||||
|
||||
result.WriteString(fmt.Sprintf("- %s%s\n", rootPath, string(filepath.Separator)))
|
||||
|
||||
printTreeRecursive(&result, tree, 0, " ")
|
||||
for _, node := range tree {
|
||||
printNode(&result, node, 1)
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func printTreeRecursive(builder *strings.Builder, tree []TreeNode, level int, prefix string) {
|
||||
for _, node := range tree {
|
||||
linePrefix := prefix + "- "
|
||||
func printNode(builder *strings.Builder, node *TreeNode, level int) {
|
||||
indent := strings.Repeat(" ", level)
|
||||
|
||||
nodeName := node.Name
|
||||
if node.Type == "directory" {
|
||||
nodeName += string(filepath.Separator)
|
||||
}
|
||||
fmt.Fprintf(builder, "%s%s\n", linePrefix, nodeName)
|
||||
nodeName := node.Name
|
||||
if node.Type == "directory" {
|
||||
nodeName += string(filepath.Separator)
|
||||
}
|
||||
|
||||
if node.Type == "directory" && len(node.Children) > 0 {
|
||||
printTreeRecursive(builder, node.Children, level+1, prefix+" ")
|
||||
fmt.Fprintf(builder, "%s- %s\n", indent, nodeName)
|
||||
|
||||
if node.Type == "directory" && len(node.Children) > 0 {
|
||||
for _, child := range node.Children {
|
||||
printNode(builder, child, level+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewLsTool(workingDir string) tool.InvokableTool {
|
||||
return &lsTool{
|
||||
workingDir,
|
||||
}
|
||||
func lsDescription() string {
|
||||
return `Directory listing tool that shows files and subdirectories in a tree structure, helping you explore and understand the project organization.
|
||||
|
||||
WHEN TO USE THIS TOOL:
|
||||
- Use when you need to explore the structure of a directory
|
||||
- Helpful for understanding the organization of a project
|
||||
- Good first step when getting familiar with a new codebase
|
||||
|
||||
HOW TO USE:
|
||||
- Provide a path to list (defaults to current working directory)
|
||||
- Optionally specify glob patterns to ignore
|
||||
- Results are displayed in a tree structure
|
||||
|
||||
FEATURES:
|
||||
- Displays a hierarchical view of files and directories
|
||||
- Automatically skips hidden files/directories (starting with '.')
|
||||
- Skips common system directories like __pycache__
|
||||
- Can filter out files matching specific patterns
|
||||
|
||||
LIMITATIONS:
|
||||
- Results are limited to 1000 files
|
||||
- Very large directories will be truncated
|
||||
- Does not show file sizes or permissions
|
||||
- Cannot recursively list all directories in a large project
|
||||
|
||||
TIPS:
|
||||
- Use Glob tool for finding files by name patterns instead of browsing
|
||||
- Use Grep tool for searching file contents
|
||||
- Combine with other tools for more effective exploration`
|
||||
}
|
||||
|
||||
func NewLsTool() BaseTool {
|
||||
return &lsTool{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user