252 lines
7.0 KiB
Go
252 lines
7.0 KiB
Go
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
|
||
}
|