- Standardmäßig maximal 2000 Zeilen gelesen (statt komplette Datei) - Optionale Parameter: offset (1-basiert) und limit - Hinweis wenn Datei abgeschnitten wurde mit exakter Zeilenangabe - TODO: Auto-scrolling als erledigt markiert
722 lines
35 KiB
C#
722 lines
35 KiB
C#
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.Configuration;
|
||
using Microsoft.Extensions.FileSystemGlobbing;
|
||
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||
|
||
namespace Gott.Services;
|
||
|
||
public class ToolService
|
||
{
|
||
private readonly IConfiguration _config;
|
||
|
||
public ToolService(IConfiguration config)
|
||
{
|
||
_config = config;
|
||
}
|
||
|
||
private int ExecuteTimeoutSeconds => _config.GetValue<int>("ExecuteTimeout", 180);
|
||
public const string AskUserChoiceToolName = "ask_user_choice";
|
||
|
||
/// <summary>
|
||
/// Read-only Tools die im Plan-Modus verfügbar bleiben.
|
||
/// Write-Tools (write_file, edit_file, delete_file, execute) werden entfernt.
|
||
/// </summary>
|
||
private static readonly HashSet<string> ReadOnlyToolNames = new HashSet<string>
|
||
{
|
||
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
|
||
"memory_read", "memory_write", "memory_list", "memory_search",
|
||
"remote_execute", AskUserChoiceToolName
|
||
};
|
||
|
||
public object[] FilterReadOnlyTools(object[] tools)
|
||
{
|
||
var filtered = new List<object>();
|
||
foreach (var tool in tools)
|
||
{
|
||
var json = JsonSerializer.Serialize(tool);
|
||
var el = JsonSerializer.Deserialize<JsonElement>(json);
|
||
if (el.TryGetProperty("name", out var nameProp))
|
||
{
|
||
var name = nameProp.GetString() ?? "";
|
||
if (ReadOnlyToolNames.Contains(name))
|
||
filtered.Add(tool);
|
||
}
|
||
}
|
||
return filtered.ToArray();
|
||
}
|
||
|
||
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false)
|
||
{
|
||
var allTools = new List<object>();
|
||
if (includeUserChoice)
|
||
allTools.Add(GetAskUserChoiceTool());
|
||
if (includeFileTools)
|
||
allTools.AddRange(GetFileTools());
|
||
if (includeMemory)
|
||
allTools.AddRange(GetMemoryToolDefinitions());
|
||
if (includeMemorySearch)
|
||
allTools.Add(GetMemorySearchTool());
|
||
if (includeRemote)
|
||
allTools.Add(GetRemoteExecuteTool());
|
||
return allTools.ToArray();
|
||
}
|
||
|
||
private object GetAskUserChoiceTool()
|
||
{
|
||
return new
|
||
{
|
||
name = AskUserChoiceToolName,
|
||
description = "Stellt dem User eine Frage mit Auswahlmöglichkeiten, wenn du unsicher bist oder es mehrere sinnvolle Wege gibt. Der User wählt eine Option und du arbeitest mit seiner Antwort weiter.",
|
||
input_schema = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["question"] = new { type = "string", description = "Die Frage an den User" },
|
||
["choices"] = new
|
||
{
|
||
type = "array",
|
||
description = "Liste der Auswahlmöglichkeiten (2-5 Optionen). MUSS ein JSON-Array mit Strings sein, z.B. [\"Option A\", \"Option B\", \"Option C\"]. Niemals einen einzelnen String verwenden.",
|
||
items = new { type = "string", description = "Eine einzelne Auswahlmöglichkeit als kurzer, prägnanter Text" },
|
||
minItems = 2,
|
||
maxItems = 5
|
||
}
|
||
},
|
||
required = new[] { "question", "choices" }
|
||
}
|
||
};
|
||
}
|
||
|
||
private object[] GetFileTools()
|
||
{
|
||
return new object[]
|
||
{
|
||
new
|
||
{
|
||
name = "read_file",
|
||
description = "Liest den Inhalt einer Datei. Gibt den Textinhalt zurück. Standardmäßig werden maximal 2000 Zeilen gelesen. Bei großen Dateien offset und limit verwenden.",
|
||
input_schema = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["path"] = new { type = "string", description = "Relativer Dateipfad" },
|
||
["offset"] = new { type = "integer", description = "Erste Zeile die gelesen werden soll (1-basiert, Default: 1)" },
|
||
["limit"] = new { type = "integer", description = "Maximale Anzahl Zeilen die gelesen werden sollen (Default: 2000)" }
|
||
},
|
||
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<string, object>
|
||
{
|
||
["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<string, object>
|
||
{
|
||
["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<string, object>
|
||
{
|
||
["path"] = new { type = "string", description = "Relativer Verzeichnispfad (leer = Root)" }
|
||
},
|
||
required = Array.Empty<string>()
|
||
}
|
||
},
|
||
new
|
||
{
|
||
name = "delete_file",
|
||
description = "Löscht eine Datei oder ein leeres Verzeichnis.",
|
||
input_schema = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["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: {ExecuteTimeoutSeconds} Sekunden.",
|
||
input_schema = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" }
|
||
},
|
||
required = new[] { "command" }
|
||
}
|
||
},
|
||
new
|
||
{
|
||
name = "background_execute",
|
||
description = "Startet einen Shell-Befehl als Hintergrund-Prozess im Workspace-Verzeichnis. Der Prozess läuft unabhängig weiter – es wird NICHT auf dessen Ende gewartet. Gibt die Prozess-ID (PID) zurück. Nutze dies für lang laufende Prozesse wie Server, Watcher oder Build-Prozesse. stdout/stderr werden NICHT erfasst.",
|
||
input_schema = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen im Hintergrund" }
|
||
},
|
||
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<string, object>
|
||
{
|
||
["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<string, object>
|
||
{
|
||
["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<string, object>
|
||
{
|
||
["todos"] = new
|
||
{
|
||
type = "array",
|
||
description = "Liste aller Todo-Einträge",
|
||
items = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["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 GetMemorySearchTool()
|
||
{
|
||
return new
|
||
{
|
||
name = "memory_search",
|
||
description = "Semantische Suche über alle vergangenen Chat-Nachrichten (inklusive archivierter Sessions). Findet relevante Informationen aus früheren Gesprächen anhand von Bedeutung, nicht exakten Wörtern. Nutze dies wenn du Kontext aus vergangenen Interaktionen brauchst.",
|
||
input_schema = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["query"] = new { type = "string", description = "Suchanfrage – beschreibe wonach du suchst (z.B. 'Druckerproblem letzte Woche', 'Server-Migration Konfiguration')" },
|
||
["top_n"] = new { type = "integer", description = "Anzahl der Ergebnisse (Default: 10, Max: 30)" }
|
||
},
|
||
required = new[] { "query" }
|
||
}
|
||
};
|
||
}
|
||
|
||
private object GetRemoteExecuteTool()
|
||
{
|
||
return new
|
||
{
|
||
name = "remote_execute",
|
||
description = "Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden.",
|
||
input_schema = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["command"] = new { type = "string", description = "PowerShell-Befehl zum Ausführen auf dem Remote-PC" }
|
||
},
|
||
required = new[] { "command" }
|
||
}
|
||
};
|
||
}
|
||
|
||
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<string, object>
|
||
{
|
||
["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<string, object>
|
||
{
|
||
["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<string, object>
|
||
{
|
||
["path"] = new { type = "string", description = "Optionaler Unterpfad zum Auflisten (leer = Root)" }
|
||
},
|
||
required = Array.Empty<string>()
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null)
|
||
{
|
||
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: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||
var allLines = File.ReadAllLines(path);
|
||
var totalLines = allLines.Length;
|
||
var offset = input.TryGetProperty("offset", out var offsetProp) ? Math.Max(1, offsetProp.GetInt32()) : 1;
|
||
var limit = input.TryGetProperty("limit", out var limitProp) ? Math.Max(1, limitProp.GetInt32()) : 2000;
|
||
var startIndex = offset - 1;
|
||
if (startIndex >= totalLines)
|
||
return $"Fehler: offset ({offset}) ist größer als die Anzahl der Zeilen ({totalLines}).";
|
||
var selectedLines = allLines.Skip(startIndex).Take(limit).ToArray();
|
||
var result = string.Join("\n", selectedLines);
|
||
var endLine = startIndex + selectedLines.Length;
|
||
if (endLine < totalLines)
|
||
result += $"\n\n[Hinweis: Datei hat {totalLines} Zeilen, nur Zeilen {offset}–{endLine} angezeigt. Nutze offset/limit um mehr zu lesen.]";
|
||
return result;
|
||
}
|
||
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: {WorkspaceHelper.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: {WorkspaceHelper.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: {WorkspaceHelper.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: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||
var entries = new List<string>();
|
||
foreach (var dir in Directory.GetDirectories(path))
|
||
entries.Add(WorkspaceHelper.GetRelativePath(workspace, dir) + "/");
|
||
foreach (var file in Directory.GetFiles(path))
|
||
entries.Add(WorkspaceHelper.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: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||
}
|
||
if (Directory.Exists(path))
|
||
{
|
||
Directory.Delete(path, recursive: false);
|
||
return $"Verzeichnis gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||
}
|
||
return $"Fehler: Nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}";
|
||
}
|
||
case "execute":
|
||
{
|
||
if (string.IsNullOrEmpty(workspace))
|
||
return "Fehler: Kein Workspace konfiguriert";
|
||
var command = input.GetProperty("command").GetString() ?? "";
|
||
// Unter Windows: Befehl in temp .cmd-Datei schreiben und mit /c ausführen.
|
||
// stdin-Ansatz funktioniert nicht zuverlässig (interaktiver Modus, &-Parsing).
|
||
string scriptPath = null;
|
||
var psi = new ProcessStartInfo
|
||
{
|
||
FileName = WorkspaceHelper.ShellExecutable,
|
||
WorkingDirectory = workspace,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
UseShellExecute = false,
|
||
CreateNoWindow = true
|
||
};
|
||
if (WorkspaceHelper.IsWindows)
|
||
{
|
||
scriptPath = Path.Combine(Path.GetTempPath(), $"gott_exec_{Guid.NewGuid():N}.cmd");
|
||
File.WriteAllText(scriptPath, $"@echo off\r\n{command}\r\n");
|
||
psi.Arguments = $"/c \"{scriptPath}\"";
|
||
}
|
||
else
|
||
{
|
||
psi.RedirectStandardInput = true;
|
||
}
|
||
using var proc = Process.Start(psi);
|
||
if (proc == null)
|
||
{
|
||
if (scriptPath != null) try { File.Delete(scriptPath); } catch { }
|
||
return "Fehler: Prozess konnte nicht gestartet werden";
|
||
}
|
||
if (!WorkspaceHelper.IsWindows)
|
||
{
|
||
await proc.StandardInput.WriteAsync(command);
|
||
proc.StandardInput.Close();
|
||
}
|
||
// Timeout UND externen CancellationToken verknüpfen
|
||
var timeoutSeconds = ExecuteTimeoutSeconds;
|
||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||
// stdout/stderr VOR WaitForExit lesen – verhindert Deadlock
|
||
// wenn Pipe-Buffer volllaufen (Windows: nur 4KB!)
|
||
var stdoutTask = proc.StandardOutput.ReadToEndAsync();
|
||
var stderrTask = proc.StandardError.ReadToEndAsync();
|
||
try
|
||
{
|
||
await proc.WaitForExitAsync(cts.Token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
proc.Kill(entireProcessTree: true);
|
||
if (scriptPath != null) try { File.Delete(scriptPath); } catch { }
|
||
// Wenn der externe Token abgebrochen wurde, weiterwerfen
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
return $"Fehler: Timeout nach {timeoutSeconds} Sekunden - Prozess abgebrochen";
|
||
}
|
||
if (scriptPath != null) try { File.Delete(scriptPath); } catch { }
|
||
var stdout = await stdoutTask;
|
||
var stderr = await stderrTask;
|
||
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 "background_execute":
|
||
{
|
||
if (string.IsNullOrEmpty(workspace))
|
||
return "Fehler: Kein Workspace konfiguriert";
|
||
var command = input.GetProperty("command").GetString() ?? "";
|
||
// Befehl in temporäre Script-Datei schreiben und losgelöst starten.
|
||
// So funktionieren auch mehrzeilige Befehle zuverlässig.
|
||
var scriptId = Guid.NewGuid().ToString("N")[..8];
|
||
string scriptPath;
|
||
if (WorkspaceHelper.IsWindows)
|
||
{
|
||
scriptPath = Path.Combine(Path.GetTempPath(), $"gott_bg_{scriptId}.cmd");
|
||
// Script löscht sich nach Ausführung selbst
|
||
File.WriteAllText(scriptPath, $"@echo off\r\n{command}\r\ndel \"%~f0\"\r\n");
|
||
}
|
||
else
|
||
{
|
||
scriptPath = Path.Combine(Path.GetTempPath(), $"gott_bg_{scriptId}.sh");
|
||
File.WriteAllText(scriptPath, $"#!/bin/sh\n{command}\nrm -f \"{scriptPath}\"\n");
|
||
// Ausführbar machen
|
||
var chmod = Process.Start(new ProcessStartInfo { FileName = "chmod", Arguments = $"+x \"{scriptPath}\"", UseShellExecute = false, CreateNoWindow = true });
|
||
chmod?.WaitForExit(5000);
|
||
}
|
||
var psi = new ProcessStartInfo
|
||
{
|
||
FileName = WorkspaceHelper.IsWindows ? "cmd.exe" : scriptPath,
|
||
Arguments = WorkspaceHelper.IsWindows ? $"/c \"{scriptPath}\"" : "",
|
||
WorkingDirectory = workspace,
|
||
UseShellExecute = false,
|
||
CreateNoWindow = true
|
||
};
|
||
var proc = Process.Start(psi);
|
||
if (proc == null) return "Fehler: Prozess konnte nicht gestartet werden";
|
||
return $"Hintergrund-Prozess gestartet (PID: {proc.Id})";
|
||
}
|
||
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 => WorkspaceHelper.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<string> 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<string>();
|
||
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($"{WorkspaceHelper.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.Append($"{icon} [{id}]{prioTag} {content}\n");
|
||
}
|
||
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);
|
||
// Memory-Datei nach Schreiben embedden
|
||
if (embeddingBg != null)
|
||
{
|
||
var fullText = memoryService.ReadMemoryFile(agentId, path);
|
||
if (!string.IsNullOrEmpty(fullText))
|
||
embeddingBg.EnqueueMemoryFile(agentId, path, fullText);
|
||
}
|
||
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);
|
||
}
|
||
case "memory_search":
|
||
{
|
||
if (vectorSearch == null)
|
||
return "Fehler: Semantische Suche nicht verfügbar";
|
||
var query = input.GetProperty("query").GetString() ?? "";
|
||
var topN = input.TryGetProperty("top_n", out var tn) ? tn.GetInt32() : 10;
|
||
if (topN > 30) topN = 30;
|
||
if (topN < 1) topN = 1;
|
||
var results = await vectorSearch.SearchAsync(query, agentId, topN);
|
||
if (results.Count == 0)
|
||
return "Keine relevanten Ergebnisse gefunden.";
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine($"Gefunden: {results.Count} relevante Einträge\n");
|
||
for (int i = 0; i < results.Count; i++)
|
||
{
|
||
var r = results[i];
|
||
var date = DateTime.TryParse(r.CreatedAt, out var dt) ? dt.ToString("yyyy-MM-dd HH:mm") : r.CreatedAt;
|
||
sb.AppendLine($"[{i + 1}] Score: {r.Score:F2} | {r.Role} | {date} | Session: {r.SessionId}");
|
||
sb.AppendLine(r.TextPreview);
|
||
sb.AppendLine();
|
||
}
|
||
return sb.ToString().TrimEnd();
|
||
}
|
||
case "remote_execute":
|
||
{
|
||
if (remoteMgr == null || string.IsNullOrEmpty(sessionId))
|
||
return "Fehler: Keine Remote-Verbindung verfügbar";
|
||
var conn = remoteMgr.GetBySessionId(sessionId);
|
||
if (conn == null)
|
||
return "Fehler: Keine Remote-Verbindung für diese Session zugeordnet";
|
||
var command = input.GetProperty("command").GetString() ?? "";
|
||
var result = await remoteMgr.ExecuteCommand(conn.Id, command, ExecuteTimeoutSeconds, cancellationToken);
|
||
var sb = new StringBuilder();
|
||
if (!string.IsNullOrEmpty(result.Output))
|
||
sb.Append(result.Output);
|
||
if (!string.IsNullOrEmpty(result.Error))
|
||
{
|
||
if (sb.Length > 0) sb.AppendLine();
|
||
sb.Append("[stderr] ").Append(result.Error);
|
||
}
|
||
if (result.ExitCode != 0)
|
||
{
|
||
if (sb.Length > 0) sb.AppendLine();
|
||
sb.Append($"[Exit Code: {result.ExitCode}]");
|
||
}
|
||
return sb.Length > 0 ? sb.ToString() : "(keine Ausgabe)";
|
||
}
|
||
default:
|
||
return $"Fehler: Unbekanntes Tool: {name}";
|
||
}
|
||
}
|
||
|
||
private string ResolvePath(string relativePath, string workspace)
|
||
{
|
||
var full = Path.GetFullPath(Path.Combine(workspace, relativePath ?? ""));
|
||
if (!WorkspaceHelper.IsSubPathOf(full, workspace))
|
||
throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt");
|
||
return full;
|
||
}
|
||
}
|