using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; namespace Gott.Services; public class ToolService { public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false) { var allTools = new List(); if (includeFileTools) allTools.AddRange(GetFileTools()); if (includeMemory) allTools.AddRange(GetMemoryToolDefinitions()); return allTools.ToArray(); } private object[] GetFileTools() { return new object[] { new { name = "read_file", description = "Liest den Inhalt einer Datei. Gibt den Textinhalt zurück.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Dateipfad" } }, required = new[] { "path" } } }, new { name = "write_file", description = "Erstellt oder überschreibt eine Datei mit dem angegebenen Inhalt.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Dateipfad" }, ["content"] = new { type = "string", description = "Dateiinhalt" } }, required = new[] { "path", "content" } } }, new { name = "edit_file", description = "Ersetzt einen exakten Textabschnitt in einer Datei. oldText muss exakt matchen.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Dateipfad" }, ["old_text"] = new { type = "string", description = "Exakter Text der ersetzt werden soll" }, ["new_text"] = new { type = "string", description = "Neuer Text" } }, required = new[] { "path", "old_text", "new_text" } } }, new { name = "list_files", description = "Listet Dateien und Ordner in einem Verzeichnis auf.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Verzeichnispfad (leer = Root)" } }, required = Array.Empty() } }, new { name = "delete_file", description = "Löscht eine Datei oder ein leeres Verzeichnis.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Pfad zur Datei oder zum Verzeichnis" } }, required = new[] { "path" } } }, new { name = "execute", description = "Führt einen Shell-Befehl im Workspace-Verzeichnis aus. Gibt stdout und stderr zurück. Timeout: 180 Sekunden.", input_schema = new { type = "object", properties = new Dictionary { ["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" } }, required = new[] { "command" } } }, new { name = "glob", description = "Findet Dateien im Workspace anhand eines Glob-Patterns (z.B. '**/*.cs', 'src/*.js'). Gibt eine Liste passender Dateipfade zurück.", input_schema = new { type = "object", properties = new Dictionary { ["pattern"] = new { type = "string", description = "Glob-Pattern, z.B. '**/*.cs' oder 'src/**/*.js'" }, ["path"] = new { type = "string", description = "Startverzeichnis (relativ zum Workspace, leer = Root)" } }, required = new[] { "pattern" } } }, new { name = "grep", description = "Durchsucht Dateien im Workspace nach einem Regex- oder Text-Pattern. Gibt Treffer mit Dateiname und Zeilennummer zurück.", input_schema = new { type = "object", properties = new Dictionary { ["pattern"] = new { type = "string", description = "Suchbegriff oder Regex-Pattern" }, ["path"] = new { type = "string", description = "Zu durchsuchendes Verzeichnis oder Datei (relativ, leer = Root)" }, ["glob"] = new { type = "string", description = "Dateifilter als Glob-Pattern, z.B. '*.cs' (leer = alle Dateien)" }, ["case_sensitive"] = new { type = "boolean", description = "Groß-/Kleinschreibung beachten (Default: false)" } }, required = new[] { "pattern" } } }, new { name = "todo_write", description = "Schreibt die komplette Todo-Liste. Ersetzt immer die gesamte Liste. Nutze dies um Aufgaben zu planen und den Fortschritt zu verfolgen.", input_schema = new { type = "object", properties = new Dictionary { ["todos"] = new { type = "array", description = "Liste aller Todo-Einträge", items = new { type = "object", properties = new Dictionary { ["id"] = new { type = "string", description = "Eindeutige ID (z.B. '1', '2', 'task-1')" }, ["content"] = new { type = "string", description = "Beschreibung der Aufgabe" }, ["status"] = new { type = "string", @enum = new[] { "pending", "in_progress", "completed" }, description = "Status der Aufgabe" }, ["priority"] = new { type = "string", @enum = new[] { "high", "medium", "low" }, description = "Priorität (optional)" } }, required = new[] { "id", "content", "status" } } } }, required = new[] { "todos" } } } }; } private object[] GetMemoryToolDefinitions() { return new object[] { new { name = "memory_read", description = "Liest eine Memory-Datei des Agents. Nutze dies um gespeicherte Erinnerungen, Notizen oder Kontextinformationen abzurufen.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Pfad zur Memory-Datei (z.B. 'MEMORY.md' oder 'memory/YYYY-MM-DD.md')" } }, required = new[] { "path" } } }, new { name = "memory_write", description = "Schreibt oder überschreibt eine Memory-Datei. Nutze dies um wichtige Informationen, Entscheidungen oder Präferenzen dauerhaft zu speichern.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Relativer Pfad zur Memory-Datei (z.B. 'MEMORY.md' oder 'memory/YYYY-MM-DD.md')" }, ["content"] = new { type = "string", description = "Inhalt der Datei" }, ["append"] = new { type = "boolean", description = "Wenn true, wird der Inhalt angehängt statt überschrieben (Default: false)" } }, required = new[] { "path", "content" } } }, new { name = "memory_list", description = "Listet alle Memory-Dateien des Agents auf.", input_schema = new { type = "object", properties = new Dictionary { ["path"] = new { type = "string", description = "Optionaler Unterpfad zum Auflisten (leer = Root)" } }, required = Array.Empty() } } }; } public async Task ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default) { switch (name) { case "read_file": { if (string.IsNullOrEmpty(workspace)) return "Fehler: Kein Workspace konfiguriert"; var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); if (!File.Exists(path)) return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspace, path)}"; return File.ReadAllText(path); } case "write_file": { if (string.IsNullOrEmpty(workspace)) return "Fehler: Kein Workspace konfiguriert"; var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); var dir = Path.GetDirectoryName(path); if (dir != null) Directory.CreateDirectory(dir); var content = input.GetProperty("content").GetString() ?? ""; File.WriteAllText(path, content); return $"Datei geschrieben: {Path.GetRelativePath(workspace, path)} ({content.Length} Zeichen)"; } case "edit_file": { if (string.IsNullOrEmpty(workspace)) return "Fehler: Kein Workspace konfiguriert"; var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); if (!File.Exists(path)) return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspace, path)}"; var fileContent = File.ReadAllText(path); var oldText = input.GetProperty("old_text").GetString() ?? ""; var newText = input.GetProperty("new_text").GetString() ?? ""; if (!fileContent.Contains(oldText)) return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig."; fileContent = fileContent.Replace(oldText, newText); File.WriteAllText(path, fileContent); return $"Datei bearbeitet: {Path.GetRelativePath(workspace, path)}"; } case "list_files": { if (string.IsNullOrEmpty(workspace)) return "Fehler: Kein Workspace konfiguriert"; var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "", workspace); if (!Directory.Exists(path)) return $"Fehler: Verzeichnis nicht gefunden: {Path.GetRelativePath(workspace, path)}"; var entries = new List(); foreach (var dir in Directory.GetDirectories(path)) entries.Add(Path.GetRelativePath(workspace, dir) + "/"); foreach (var file in Directory.GetFiles(path)) entries.Add(Path.GetRelativePath(workspace, file)); return entries.Count == 0 ? "(leer)" : string.Join("\n", entries); } case "delete_file": { if (string.IsNullOrEmpty(workspace)) return "Fehler: Kein Workspace konfiguriert"; var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); if (File.Exists(path)) { File.Delete(path); return $"Datei gelöscht: {Path.GetRelativePath(workspace, path)}"; } if (Directory.Exists(path)) { Directory.Delete(path, recursive: false); return $"Verzeichnis gelöscht: {Path.GetRelativePath(workspace, path)}"; } return $"Fehler: Nicht gefunden: {Path.GetRelativePath(workspace, path)}"; } case "execute": { if (string.IsNullOrEmpty(workspace)) return "Fehler: Kein Workspace konfiguriert"; var command = input.GetProperty("command").GetString() ?? ""; var psi = new ProcessStartInfo { FileName = "/bin/bash", WorkingDirectory = workspace, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var proc = Process.Start(psi); if (proc == null) return "Fehler: Prozess konnte nicht gestartet werden"; await proc.StandardInput.WriteAsync(command); proc.StandardInput.Close(); // Timeout (180s) UND externen CancellationToken verknüpfen using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(180)); try { await proc.WaitForExitAsync(cts.Token); } catch (OperationCanceledException) { proc.Kill(entireProcessTree: true); // Wenn der externe Token abgebrochen wurde, weiterwerfen cancellationToken.ThrowIfCancellationRequested(); return "Fehler: Timeout nach 180 Sekunden - Prozess abgebrochen"; } var stdout = await proc.StandardOutput.ReadToEndAsync(); var stderr = await proc.StandardError.ReadToEndAsync(); var result = new StringBuilder(); if (!string.IsNullOrEmpty(stdout)) result.Append(stdout); if (!string.IsNullOrEmpty(stderr)) { if (result.Length > 0) result.Append("\n--- stderr ---\n"); result.Append(stderr); } if (result.Length == 0) result.Append($"(exit code {proc.ExitCode})"); else if (proc.ExitCode != 0) result.Append($"\n(exit code {proc.ExitCode})"); return result.ToString(); } case "glob": { if (string.IsNullOrEmpty(workspace)) return "Fehler: Kein Workspace konfiguriert"; var pattern = input.GetProperty("pattern").GetString() ?? "*"; var basePath = input.TryGetProperty("path", out var gp) ? gp.GetString() ?? "" : ""; var searchRoot = string.IsNullOrEmpty(basePath) ? workspace : ResolvePath(basePath, workspace); if (!Directory.Exists(searchRoot)) return $"Fehler: Verzeichnis nicht gefunden: {basePath}"; var matcher = new Matcher(); matcher.AddInclude(pattern); var dirInfo = new DirectoryInfoWrapper(new DirectoryInfo(searchRoot)); var result = matcher.Execute(dirInfo); var matches = result.Files .Select(f => Path.GetRelativePath(workspace, Path.Combine(searchRoot, f.Path))) .OrderBy(f => f) .ToList(); return matches.Count == 0 ? "(keine Treffer)" : string.Join("\n", matches); } case "grep": { if (string.IsNullOrEmpty(workspace)) return "Fehler: Kein Workspace konfiguriert"; var pattern = input.GetProperty("pattern").GetString() ?? ""; var searchPath = input.TryGetProperty("path", out var sp) ? sp.GetString() ?? "" : ""; var fileGlob = input.TryGetProperty("glob", out var fg) ? fg.GetString() ?? "*" : "*"; var caseSensitive = input.TryGetProperty("case_sensitive", out var cs) && cs.GetBoolean(); var regexOptions = caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase; Regex regex; try { regex = new Regex(pattern, regexOptions); } catch { regex = new Regex(Regex.Escape(pattern), regexOptions); } var searchRoot = string.IsNullOrEmpty(searchPath) ? workspace : ResolvePath(searchPath, workspace); IEnumerable files; if (File.Exists(searchRoot)) files = new[] { searchRoot }; else if (Directory.Exists(searchRoot)) files = Directory.GetFiles(searchRoot, fileGlob, SearchOption.AllDirectories); else return $"Fehler: Pfad nicht gefunden: {searchPath}"; var results = new List(); foreach (var file in files.OrderBy(f => f)) { string[] lines; try { lines = File.ReadAllLines(file); } catch { continue; } for (var i = 0; i < lines.Length; i++) { if (regex.IsMatch(lines[i])) results.Add($"{Path.GetRelativePath(workspace, file)}:{i + 1}: {lines[i].Trim()}"); } } if (results.Count == 0) return "(keine Treffer)"; if (results.Count > 500) results = results.Take(500).Append("... (auf 500 Treffer begrenzt)").ToList(); return string.Join("\n", results); } case "todo_write": { var todos = input.GetProperty("todos"); var sb = new StringBuilder(); foreach (var todo in todos.EnumerateArray()) { var id = todo.GetProperty("id").GetString(); var content = todo.GetProperty("content").GetString(); var status = todo.GetProperty("status").GetString(); var priority = todo.TryGetProperty("priority", out var prio) ? prio.GetString() : null; var icon = status switch { "completed" => "✅", "in_progress" => "🔄", _ => "⬜" }; var prioTag = priority != null ? $" [{priority}]" : ""; sb.AppendLine($"{icon} [{id}]{prioTag} {content}"); } return sb.Length == 0 ? "(leere Todo-Liste)" : sb.ToString().TrimEnd(); } case "memory_read": { if (memoryService == null || string.IsNullOrEmpty(agentId)) return "Fehler: Memory-Service nicht verfügbar"; var path = input.GetProperty("path").GetString() ?? ""; var content = memoryService.ReadMemoryFile(agentId, path); if (content == null) return $"Datei nicht gefunden: {path}"; return content; } case "memory_write": { if (memoryService == null || string.IsNullOrEmpty(agentId)) return "Fehler: Memory-Service nicht verfügbar"; var path = input.GetProperty("path").GetString() ?? ""; var content = input.GetProperty("content").GetString() ?? ""; var append = input.TryGetProperty("append", out var a) && a.GetBoolean(); if (append) memoryService.AppendMemoryFile(agentId, path, content); else memoryService.WriteMemoryFile(agentId, path, content); return $"Memory-Datei {(append ? "erweitert" : "geschrieben")}: {path} ({content.Length} Zeichen)"; } case "memory_list": { if (memoryService == null || string.IsNullOrEmpty(agentId)) return "Fehler: Memory-Service nicht verfügbar"; var subPath = input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; var files = memoryService.ListMemoryFiles(agentId, subPath); return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files); } default: return $"Fehler: Unbekanntes Tool: {name}"; } } private string ResolvePath(string relativePath, string workspace) { var full = Path.GetFullPath(Path.Combine(workspace, relativePath ?? "")); if (!full.StartsWith(workspace)) throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt"); return full; } }