recursive search added
This commit is contained in:
4
PRD.md
4
PRD.md
@@ -2,6 +2,6 @@
|
|||||||
Ein Starter Projekt in go
|
Ein Starter Projekt in go
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
- [ ] Erstelle eine Datei hello.go mit einem Hello World Programm
|
- [x] Erstelle eine Datei hello.go die 2 zahlen aus der Kommandozeile einliest und die Summe ausgibt
|
||||||
|
- [ ] Erstelle unit tests für hello.go
|
||||||
- [ ] Erstelle eine Datei README.md mit einer kurzen Projektbeschreibung
|
- [ ] Erstelle eine Datei README.md mit einer kurzen Projektbeschreibung
|
||||||
- [ ] Projektstruktur anlegen
|
|
||||||
|
|||||||
321
agent/tools.go
321
agent/tools.go
@@ -1,260 +1,3 @@
|
|||||||
// package agent
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// "fmt"
|
|
||||||
// "os"
|
|
||||||
// "path/filepath"
|
|
||||||
// "strings"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// // ─── Tool Registry ────────────────────────────────────────
|
|
||||||
|
|
||||||
// type Tool struct {
|
|
||||||
// Name string
|
|
||||||
// Description string
|
|
||||||
// Usage string
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var Registry = []Tool{
|
|
||||||
// {
|
|
||||||
// Name: "READ_FILE",
|
|
||||||
// Description: "Liest den Inhalt einer Datei",
|
|
||||||
// Usage: "TOOL:READ_FILE:pfad/zur/datei",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// Name: "WRITE_FILE",
|
|
||||||
// Description: "Schreibt Inhalt in eine Datei (mehrzeilig möglich)",
|
|
||||||
// Usage: `TOOL:WRITE_FILE:pfad/zur/datei
|
|
||||||
// <<<
|
|
||||||
// dateiinhalt hier
|
|
||||||
// >>>`,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// Name: "LIST_FILES",
|
|
||||||
// Description: "Listet alle Dateien in einem Verzeichnis",
|
|
||||||
// Usage: "TOOL:LIST_FILES:pfad",
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // BuildToolPrompt generiert den Tool-Abschnitt für den System-Prompt
|
|
||||||
// func BuildToolPrompt() string {
|
|
||||||
// var sb strings.Builder
|
|
||||||
// sb.WriteString("Du hast folgende Tools zur Verfügung:\n\n")
|
|
||||||
// for _, t := range Registry {
|
|
||||||
// sb.WriteString(fmt.Sprintf("### %s\n", t.Name))
|
|
||||||
// sb.WriteString(fmt.Sprintf("Beschreibung: %s\n", t.Description))
|
|
||||||
// sb.WriteString(fmt.Sprintf("Verwendung:\n%s\n\n", t.Usage))
|
|
||||||
// }
|
|
||||||
// return sb.String()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ─── Tool Parsing ─────────────────────────────────────────
|
|
||||||
|
|
||||||
// type toolCall struct {
|
|
||||||
// name string
|
|
||||||
// path string
|
|
||||||
// content string // nur für WRITE_FILE
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // parseToolCalls extrahiert alle Tool-Calls aus einer LLM-Antwort.
|
|
||||||
// // Unterstützt mehrzeilige WRITE_FILE Blöcke:
|
|
||||||
// //
|
|
||||||
// // TOOL:WRITE_FILE:pfad
|
|
||||||
// // <<<
|
|
||||||
// // inhalt
|
|
||||||
// // >>>
|
|
||||||
// func parseToolCalls(response string) []toolCall {
|
|
||||||
// var calls []toolCall
|
|
||||||
// lines := strings.Split(response, "\n")
|
|
||||||
|
|
||||||
// i := 0
|
|
||||||
// for i < len(lines) {
|
|
||||||
// line := strings.TrimSpace(lines[i])
|
|
||||||
|
|
||||||
// if !strings.HasPrefix(line, "TOOL:") {
|
|
||||||
// i++
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// parts := strings.SplitN(line, ":", 3)
|
|
||||||
// if len(parts) < 3 {
|
|
||||||
// i++
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// toolName := parts[1]
|
|
||||||
// toolPath := parts[2]
|
|
||||||
|
|
||||||
// // WRITE_FILE: Block-Inhalt lesen (<<<...>>>)
|
|
||||||
// if toolName == "WRITE_FILE" {
|
|
||||||
// content, newIndex := readContentBlock(lines, i+1)
|
|
||||||
// calls = append(calls, toolCall{
|
|
||||||
// name: toolName,
|
|
||||||
// path: toolPath,
|
|
||||||
// content: content,
|
|
||||||
// })
|
|
||||||
// i = newIndex
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// calls = append(calls, toolCall{
|
|
||||||
// name: toolName,
|
|
||||||
// path: toolPath,
|
|
||||||
// })
|
|
||||||
// i++
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return calls
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // readContentBlock liest Zeilen zwischen <<< und >>> und gibt den
|
|
||||||
// // bereinigten Inhalt sowie den neuen Zeilenindex zurück.
|
|
||||||
// func readContentBlock(lines []string, startIndex int) (string, int) {
|
|
||||||
// i := startIndex
|
|
||||||
|
|
||||||
// // Öffnendes <<< überspringen (optional, falls LLM es ausgibt)
|
|
||||||
// if i < len(lines) && strings.TrimSpace(lines[i]) == "<<<" {
|
|
||||||
// i++
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var contentLines []string
|
|
||||||
// for i < len(lines) {
|
|
||||||
// trimmed := strings.TrimSpace(lines[i])
|
|
||||||
// if trimmed == ">>>" {
|
|
||||||
// i++ // >>> konsumieren
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// contentLines = append(contentLines, lines[i])
|
|
||||||
// i++
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return strings.Join(contentLines, "\n"), i
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ─── Tool Execution ───────────────────────────────────────
|
|
||||||
|
|
||||||
// // ExecuteTools parst alle Tool-Calls aus der LLM-Antwort und führt sie aus.
|
|
||||||
// // Gibt den kombinierten Output und true zurück wenn mindestens ein Tool aufgerufen wurde.
|
|
||||||
// func ExecuteTools(response string, workDir string) (string, bool) {
|
|
||||||
// calls := parseToolCalls(response)
|
|
||||||
// if len(calls) == 0 {
|
|
||||||
// return "", false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var outputs []string
|
|
||||||
// for _, call := range calls {
|
|
||||||
// result := executeToolCall(call, workDir)
|
|
||||||
// outputs = append(outputs, result)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return strings.Join(outputs, "\n"), true
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func executeToolCall(call toolCall, workDir string) string {
|
|
||||||
// // Sicherheits-Check: Path Traversal verhindern
|
|
||||||
// safePath, err := sanitizePath(workDir, call.path)
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Sprintf("ERROR: Ungültiger Pfad %q: %v", call.path, err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// switch call.name {
|
|
||||||
// case "READ_FILE":
|
|
||||||
// return readFile(safePath, call.path)
|
|
||||||
// case "WRITE_FILE":
|
|
||||||
// return writeFile(safePath, call.path, call.content)
|
|
||||||
// case "LIST_FILES":
|
|
||||||
// return listFiles(safePath, call.path)
|
|
||||||
// default:
|
|
||||||
// return fmt.Sprintf("ERROR: Unbekanntes Tool %q", call.name)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ─── Einzelne Tool-Implementierungen ─────────────────────
|
|
||||||
|
|
||||||
// func readFile(absPath, displayPath string) string {
|
|
||||||
// content, err := os.ReadFile(absPath)
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Sprintf("READ_FILE ERROR: %v", err)
|
|
||||||
// }
|
|
||||||
// return fmt.Sprintf("READ_FILE %s:\n%s", displayPath, string(content))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func writeFile(absPath, displayPath, content string) string {
|
|
||||||
// content = cleanContent(content)
|
|
||||||
// if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
|
|
||||||
// return fmt.Sprintf("WRITE_FILE ERROR: Verzeichnis anlegen fehlgeschlagen: %v", err)
|
|
||||||
// }
|
|
||||||
// if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
|
||||||
// return fmt.Sprintf("WRITE_FILE ERROR: %v", err)
|
|
||||||
// }
|
|
||||||
// return fmt.Sprintf("WRITE_FILE OK: %s geschrieben (%d Bytes)", displayPath, len(content))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func listFiles(absPath, displayPath string) string {
|
|
||||||
// entries, err := os.ReadDir(absPath)
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Sprintf("LIST_FILES ERROR: %v", err)
|
|
||||||
// }
|
|
||||||
// if len(entries) == 0 {
|
|
||||||
// return fmt.Sprintf("LIST_FILES %s: (leer)", displayPath)
|
|
||||||
// }
|
|
||||||
// var files []string
|
|
||||||
// for _, e := range entries {
|
|
||||||
// if e.IsDir() {
|
|
||||||
// files = append(files, e.Name()+"/")
|
|
||||||
// } else {
|
|
||||||
// files = append(files, e.Name())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return fmt.Sprintf("LIST_FILES %s:\n%s", displayPath, strings.Join(files, "\n"))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ─── Sicherheit ───────────────────────────────────────────
|
|
||||||
|
|
||||||
// // sanitizePath stellt sicher dass der Pfad innerhalb des workDir bleibt.
|
|
||||||
// // Verhindert Directory Traversal wie ../../etc/passwd
|
|
||||||
// func sanitizePath(workDir, relPath string) (string, error) {
|
|
||||||
// // Wenn LLM einen absoluten Pfad schickt → relativen Teil extrahieren
|
|
||||||
// if filepath.IsAbs(relPath) {
|
|
||||||
// workDirClean := filepath.Clean(workDir)
|
|
||||||
// // Prüfen ob der absolute Pfad innerhalb des workDir liegt
|
|
||||||
// if strings.HasPrefix(relPath, workDirClean) {
|
|
||||||
// // Absoluten Pfad direkt nutzen, kein Join nötig
|
|
||||||
// return filepath.Clean(relPath), nil
|
|
||||||
// }
|
|
||||||
// // Absoluter Pfad außerhalb workDir → nur Dateiname nehmen
|
|
||||||
// relPath = filepath.Base(relPath)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Normaler Fall: relativer Pfad
|
|
||||||
// abs := filepath.Clean(filepath.Join(workDir, relPath))
|
|
||||||
// workDirClean := filepath.Clean(workDir)
|
|
||||||
|
|
||||||
// if !strings.HasPrefix(abs, workDirClean+string(filepath.Separator)) &&
|
|
||||||
// abs != workDirClean {
|
|
||||||
// return "", fmt.Errorf("Pfad außerhalb des Arbeitsverzeichnisses")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return abs, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func cleanContent(content string) string {
|
|
||||||
// // Escaped Quotes normalisieren
|
|
||||||
// content = strings.ReplaceAll(content, `\"`, `"`)
|
|
||||||
// content = strings.ReplaceAll(content, `\\n`, "\n")
|
|
||||||
// content = strings.ReplaceAll(content, `\\t`, "\t")
|
|
||||||
|
|
||||||
// // Markdown Codeblöcke entfernen
|
|
||||||
// lines := strings.Split(content, "\n")
|
|
||||||
// var cleaned []string
|
|
||||||
// for _, line := range lines {
|
|
||||||
// if strings.HasPrefix(strings.TrimSpace(line), "```") {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// cleaned = append(cleaned, line)
|
|
||||||
// }
|
|
||||||
// return strings.TrimSpace(strings.Join(cleaned, "\n"))
|
|
||||||
// }
|
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -320,6 +63,10 @@ var Tools = []openai.ChatCompletionToolParam{
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Relativer Verzeichnispfad, '.' für aktuelles Verzeichnis",
|
"description": "Relativer Verzeichnispfad, '.' für aktuelles Verzeichnis",
|
||||||
},
|
},
|
||||||
|
"recursive": map[string]any{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "true = alle Unterverzeichnisse rekursiv auflisten",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"path"},
|
"required": []string{"path"},
|
||||||
},
|
},
|
||||||
@@ -338,7 +85,7 @@ var Tools = []openai.ChatCompletionToolParam{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tool Execution ───────────────────────────────────────
|
// ─── Tool Executor ────────────────────────────────────────
|
||||||
|
|
||||||
type ToolExecutor struct {
|
type ToolExecutor struct {
|
||||||
workDir string
|
workDir string
|
||||||
@@ -379,12 +126,13 @@ func (e *ToolExecutor) Execute(toolCall openai.ChatCompletionMessageToolCall) (s
|
|||||||
|
|
||||||
case "list_files":
|
case "list_files":
|
||||||
var p struct {
|
var p struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
Recursive bool `json:"recursive"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal([]byte(args), &p); err != nil {
|
if err := json.Unmarshal([]byte(args), &p); err != nil {
|
||||||
return fmt.Sprintf("ERROR: Ungültige Parameter: %v", err), false
|
return fmt.Sprintf("ERROR: Ungültige Parameter: %v", err), false
|
||||||
}
|
}
|
||||||
return e.listFiles(p.Path), false
|
return e.listFiles(p.Path, p.Recursive), false
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("ERROR: Unbekanntes Tool %q", name), false
|
return fmt.Sprintf("ERROR: Unbekanntes Tool %q", name), false
|
||||||
@@ -418,27 +166,66 @@ func (e *ToolExecutor) readFile(relPath string) string {
|
|||||||
return fmt.Sprintf("OK: Inhalt von %s:\n%s", relPath, string(content))
|
return fmt.Sprintf("OK: Inhalt von %s:\n%s", relPath, string(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ToolExecutor) listFiles(relPath string) string {
|
func (e *ToolExecutor) listFiles(relPath string, recursive bool) string {
|
||||||
absPath, err := e.sanitizePath(relPath)
|
absPath, err := e.sanitizePath(relPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("ERROR: %v", err)
|
return fmt.Sprintf("ERROR: %v", err)
|
||||||
}
|
}
|
||||||
|
if recursive {
|
||||||
|
return e.listFilesRecursive(absPath, relPath)
|
||||||
|
}
|
||||||
|
return e.listFilesFlat(absPath, relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ToolExecutor) listFilesFlat(absPath, displayPath string) string {
|
||||||
entries, err := os.ReadDir(absPath)
|
entries, err := os.ReadDir(absPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("ERROR: Verzeichnis nicht lesbar: %v", err)
|
return fmt.Sprintf("ERROR: Verzeichnis nicht lesbar: %v", err)
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
return fmt.Sprintf("OK: %s ist leer", relPath)
|
return fmt.Sprintf("OK: %s ist leer", displayPath)
|
||||||
}
|
}
|
||||||
var files []string
|
var files []string
|
||||||
for _, e := range entries {
|
for _, entry := range entries {
|
||||||
if e.IsDir() {
|
if entry.IsDir() {
|
||||||
files = append(files, e.Name()+"/")
|
files = append(files, entry.Name()+"/")
|
||||||
} else {
|
} else {
|
||||||
files = append(files, e.Name())
|
files = append(files, entry.Name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("OK: Dateien in %s:\n%s", relPath, strings.Join(files, "\n"))
|
return fmt.Sprintf("OK: Dateien in %s:\n%s", displayPath, strings.Join(files, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ToolExecutor) listFilesRecursive(absPath, displayPath string) string {
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
err := filepath.WalkDir(absPath, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(e.workDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rel == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
files = append(files, rel+"/")
|
||||||
|
} else {
|
||||||
|
files = append(files, rel)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("ERROR: %v", err)
|
||||||
|
}
|
||||||
|
if len(files) == 0 {
|
||||||
|
return fmt.Sprintf("OK: %s ist leer", displayPath)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("OK: Dateien in %s (rekursiv):\n%s",
|
||||||
|
displayPath, strings.Join(files, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Sicherheit ───────────────────────────────────────────
|
// ─── Sicherheit ───────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user