Files
goralphy/agent/tools.go
2026-03-04 13:51:16 +01:00

252 lines
7.0 KiB
Go
Raw Permalink 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 (
"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
}