Files
goralphy/agent/tools.go
2026-03-04 07:39:05 +01:00

465 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}