write.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. package tools
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "time"
  10. "github.com/cloudwego/eino/components/tool"
  11. "github.com/cloudwego/eino/schema"
  12. "github.com/kujtimiihoxha/termai/internal/permission"
  13. )
  14. type writeTool struct {
  15. workingDir string
  16. }
  17. const (
  18. WriteToolName = "write"
  19. )
  20. type WriteParams struct {
  21. FilePath string `json:"file_path"`
  22. Content string `json:"content"`
  23. }
  24. func (b *writeTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
  25. return &schema.ToolInfo{
  26. Name: WriteToolName,
  27. Desc: "Write a file to the local filesystem. Overwrites the existing file if there is one.\n\nBefore using this tool:\n\n1. Use the ReadFile tool to understand the file's contents and context\n\n2. Directory Verification (only applicable when creating new files):\n - Use the LS tool to verify the parent directory exists and is the correct location",
  28. ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
  29. "file_path": {
  30. Type: "string",
  31. Desc: "The absolute path to the file to write (must be absolute, not relative)",
  32. Required: true,
  33. },
  34. "content": {
  35. Type: "string",
  36. Desc: "The content to write to the file",
  37. Required: true,
  38. },
  39. }),
  40. }, nil
  41. }
  42. func (b *writeTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
  43. var params WriteParams
  44. if err := json.Unmarshal([]byte(args), &params); err != nil {
  45. return "", fmt.Errorf("failed to parse parameters: %w", err)
  46. }
  47. if params.FilePath == "" {
  48. return "file_path is required", nil
  49. }
  50. if !filepath.IsAbs(params.FilePath) {
  51. return fmt.Sprintf("file path must be absolute, got: %s", params.FilePath), nil
  52. }
  53. // fileExists := false
  54. // oldContent := ""
  55. fileInfo, err := os.Stat(params.FilePath)
  56. if err == nil {
  57. if fileInfo.IsDir() {
  58. return fmt.Sprintf("path is a directory, not a file: %s", params.FilePath), nil
  59. }
  60. modTime := fileInfo.ModTime()
  61. lastRead := getLastReadTime(params.FilePath)
  62. if modTime.After(lastRead) {
  63. return fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
  64. params.FilePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339)), nil
  65. }
  66. // oldContentBytes, readErr := os.ReadFile(params.FilePath)
  67. // if readErr != nil {
  68. // oldContent = string(oldContentBytes)
  69. // }
  70. } else if !os.IsNotExist(err) {
  71. return fmt.Sprintf("failed to access file: %s", err), nil
  72. }
  73. p := permission.Default.Request(
  74. permission.CreatePermissionRequest{
  75. Path: b.workingDir,
  76. ToolName: WriteToolName,
  77. Action: "write",
  78. Description: fmt.Sprintf("Write to file %s", params.FilePath),
  79. Params: map[string]interface{}{
  80. "file_path": params.FilePath,
  81. "contnet": params.Content,
  82. },
  83. },
  84. )
  85. if !p {
  86. return "", fmt.Errorf("permission denied")
  87. }
  88. dir := filepath.Dir(params.FilePath)
  89. if err = os.MkdirAll(dir, 0o755); err != nil {
  90. return fmt.Sprintf("failed to create parent directories: %s", err), nil
  91. }
  92. err = os.WriteFile(params.FilePath, []byte(params.Content), 0o644)
  93. if err != nil {
  94. return fmt.Sprintf("failed to write file: %s", err), nil
  95. }
  96. recordFileWrite(params.FilePath)
  97. output := "File written: " + params.FilePath
  98. // if fileExists && oldContent != params.Content {
  99. // output = generateSimpleDiff(oldContent, params.Content)
  100. // }
  101. return output, nil
  102. }
  103. func generateSimpleDiff(oldContent, newContent string) string {
  104. if oldContent == newContent {
  105. return "[No changes]"
  106. }
  107. oldLines := strings.Split(oldContent, "\n")
  108. newLines := strings.Split(newContent, "\n")
  109. var diffBuilder strings.Builder
  110. diffBuilder.WriteString(fmt.Sprintf("@@ -%d,+%d @@\n", len(oldLines), len(newLines)))
  111. maxLines := max(len(oldLines), len(newLines))
  112. for i := range maxLines {
  113. oldLine := ""
  114. newLine := ""
  115. if i < len(oldLines) {
  116. oldLine = oldLines[i]
  117. }
  118. if i < len(newLines) {
  119. newLine = newLines[i]
  120. }
  121. if oldLine != newLine {
  122. if i < len(oldLines) {
  123. diffBuilder.WriteString(fmt.Sprintf("- %s\n", oldLine))
  124. }
  125. if i < len(newLines) {
  126. diffBuilder.WriteString(fmt.Sprintf("+ %s\n", newLine))
  127. }
  128. } else {
  129. diffBuilder.WriteString(fmt.Sprintf(" %s\n", oldLine))
  130. }
  131. }
  132. return diffBuilder.String()
  133. }
  134. func NewWriteTool(workingDir string) tool.InvokableTool {
  135. return &writeTool{
  136. workingDir: workingDir,
  137. }
  138. }