// 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 ( "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/openai/openai-go" ) // ─── Tool Definitionen ──────────────────────────────────── var Tools = []openai.ChatCompletionToolParam{ { Type: "function", Function: openai.FunctionDefinitionParam{ Name: "write_file", Description: openai.String("Schreibt Inhalt in eine Datei. Erstellt Verzeichnisse automatisch."), Parameters: openai.FunctionParameters{ "type": "object", "properties": map[string]any{ "path": map[string]any{ "type": "string", "description": "Relativer Pfad der Datei z.B. main.go oder subdir/file.go", }, "content": map[string]any{ "type": "string", "description": "Vollständiger Inhalt der Datei", }, }, "required": []string{"path", "content"}, }, }, }, { Type: "function", Function: openai.FunctionDefinitionParam{ Name: "read_file", Description: openai.String("Liest den Inhalt einer existierenden Datei"), Parameters: openai.FunctionParameters{ "type": "object", "properties": map[string]any{ "path": map[string]any{ "type": "string", "description": "Relativer Pfad der Datei", }, }, "required": []string{"path"}, }, }, }, { Type: "function", Function: openai.FunctionDefinitionParam{ Name: "list_files", Description: openai.String("Listet alle Dateien und Verzeichnisse in einem Pfad auf"), Parameters: openai.FunctionParameters{ "type": "object", "properties": map[string]any{ "path": map[string]any{ "type": "string", "description": "Relativer Verzeichnispfad, '.' für aktuelles Verzeichnis", }, }, "required": []string{"path"}, }, }, }, { Type: "function", Function: openai.FunctionDefinitionParam{ Name: "task_complete", Description: openai.String("Rufe dies auf wenn der Task vollständig und korrekt erledigt ist"), Parameters: openai.FunctionParameters{ "type": "object", "properties": map[string]any{}, }, }, }, } // ─── Tool Execution ─────────────────────────────────────── type ToolExecutor struct { workDir string } func NewToolExecutor(workDir string) *ToolExecutor { return &ToolExecutor{workDir: workDir} } // Execute führt einen Tool-Call aus. // Gibt (result, done) zurück – done=true bedeutet task_complete wurde aufgerufen. func (e *ToolExecutor) Execute(toolCall openai.ChatCompletionMessageToolCall) (string, bool) { name := toolCall.Function.Name args := toolCall.Function.Arguments switch name { case "task_complete": return "Task erfolgreich abgeschlossen.", true case "write_file": var p struct { Path string `json:"path"` Content string `json:"content"` } if err := json.Unmarshal([]byte(args), &p); err != nil { return fmt.Sprintf("ERROR: Ungültige Parameter: %v", err), false } return e.writeFile(p.Path, p.Content), false case "read_file": var p struct { Path string `json:"path"` } if err := json.Unmarshal([]byte(args), &p); err != nil { return fmt.Sprintf("ERROR: Ungültige Parameter: %v", err), false } return e.readFile(p.Path), false case "list_files": var p struct { Path string `json:"path"` } 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 fmt.Sprintf("ERROR: Unbekanntes Tool %q", name), false } // ─── Implementierungen ──────────────────────────────────── func (e *ToolExecutor) writeFile(relPath, content string) string { absPath, err := e.sanitizePath(relPath) if err != nil { return fmt.Sprintf("ERROR: %v", err) } if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { return fmt.Sprintf("ERROR: Verzeichnis anlegen: %v", err) } if err := os.WriteFile(absPath, []byte(content), 0644); err != nil { return fmt.Sprintf("ERROR: Schreiben fehlgeschlagen: %v", err) } return fmt.Sprintf("OK: %s geschrieben (%d Bytes)", relPath, len(content)) } func (e *ToolExecutor) readFile(relPath string) string { absPath, err := e.sanitizePath(relPath) if err != nil { return fmt.Sprintf("ERROR: %v", err) } content, err := os.ReadFile(absPath) if err != nil { return fmt.Sprintf("ERROR: Datei nicht gefunden: %v", err) } return fmt.Sprintf("OK: Inhalt von %s:\n%s", relPath, string(content)) } func (e *ToolExecutor) listFiles(relPath string) string { absPath, err := e.sanitizePath(relPath) if err != nil { return fmt.Sprintf("ERROR: %v", err) } 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) } var files []string for _, e := range entries { if e.IsDir() { files = append(files, e.Name()+"/") } else { files = append(files, e.Name()) } } return fmt.Sprintf("OK: Dateien in %s:\n%s", relPath, strings.Join(files, "\n")) } // ─── Sicherheit ─────────────────────────────────────────── func (e *ToolExecutor) sanitizePath(relPath string) (string, error) { // Absolute Pfade vom LLM: relativen Teil extrahieren if filepath.IsAbs(relPath) { workDirClean := filepath.Clean(e.workDir) if strings.HasPrefix(relPath, workDirClean) { return filepath.Clean(relPath), nil } relPath = filepath.Base(relPath) } abs := filepath.Clean(filepath.Join(e.workDir, relPath)) workDirClean := filepath.Clean(e.workDir) if !strings.HasPrefix(abs, workDirClean+string(filepath.Separator)) && abs != workDirClean { return "", fmt.Errorf("Pfad außerhalb des Arbeitsverzeichnisses: %s", relPath) } return abs, nil }