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", }, "recursive": map[string]any{ "type": "boolean", "description": "true = alle Unterverzeichnisse rekursiv auflisten", }, }, "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 Executor ──────────────────────────────────────── 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"` 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, p.Recursive), 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, 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", displayPath) } var files []string for _, entry := range entries { if entry.IsDir() { files = append(files, entry.Name()+"/") } else { files = append(files, entry.Name()) } } 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 ─────────────────────────────────────────── 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 }