diff --git a/PRD.md b/PRD.md index cc4cdf1..e7802ea 100644 --- a/PRD.md +++ b/PRD.md @@ -2,6 +2,6 @@ Ein Starter Projekt in go ## 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 -- [ ] Projektstruktur anlegen diff --git a/agent/tools.go b/agent/tools.go index f22bae7..816216e 100644 --- a/agent/tools.go +++ b/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 import ( @@ -320,6 +63,10 @@ var Tools = []openai.ChatCompletionToolParam{ "type": "string", "description": "Relativer Verzeichnispfad, '.' für aktuelles Verzeichnis", }, + "recursive": map[string]any{ + "type": "boolean", + "description": "true = alle Unterverzeichnisse rekursiv auflisten", + }, }, "required": []string{"path"}, }, @@ -338,7 +85,7 @@ var Tools = []openai.ChatCompletionToolParam{ }, } -// ─── Tool Execution ─────────────────────────────────────── +// ─── Tool Executor ──────────────────────────────────────── type ToolExecutor struct { workDir string @@ -379,12 +126,13 @@ func (e *ToolExecutor) Execute(toolCall openai.ChatCompletionMessageToolCall) (s case "list_files": var p struct { - Path string `json:"path"` + Path string `json:"path"` + Recursive bool `json:"recursive"` } if err := json.Unmarshal([]byte(args), &p); err != nil { 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 @@ -418,27 +166,66 @@ func (e *ToolExecutor) readFile(relPath string) string { 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) if err != nil { 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) if err != nil { return fmt.Sprintf("ERROR: Verzeichnis nicht lesbar: %v", err) } if len(entries) == 0 { - return fmt.Sprintf("OK: %s ist leer", relPath) + return fmt.Sprintf("OK: %s ist leer", displayPath) } var files []string - for _, e := range entries { - if e.IsDir() { - files = append(files, e.Name()+"/") + for _, entry := range entries { + if entry.IsDir() { + files = append(files, entry.Name()+"/") } 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 ───────────────────────────────────────────