- WorkspaceHelper: Zentrale Hilfsmethoden (IsWindows, TempDir, IsSubPathOf, GetRelativePath, ShellExecutable, ShellArguments) - ToolService: execute nutzt cmd.exe auf Windows, GetRelativePath mit Forward-Slashes, IsSubPathOf für case-insensitive Pfadprüfung - SessionEndpoint: /tmp → Path.GetTempPath(), bash → cmd.exe, Fire-and-forget via .bat statt systemd-run auf Windows - ChatService: /tmp → WorkspaceHelper.TempDir - MemoryService: IsSubPathOf + GetRelativePath - WorkspaceEndpoint: HandleBrowse zeigt Laufwerke auf Windows, IsSubPathOf statt StartsWith - InfoEndpoint: gott.exe auf Windows, isWindows-Flag in API-Response - Frontend: File-Explorer mit Laufwerks-Navigation auf Windows
534 lines
25 KiB
C#
534 lines
25 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.FileSystemGlobbing;
|
||
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||
|
||
namespace Gott.Services;
|
||
|
||
public class ToolService
|
||
{
|
||
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",
|
||
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)
|
||
{
|
||
var allTools = new List<object>();
|
||
if (includeUserChoice)
|
||
allTools.Add(GetAskUserChoiceTool());
|
||
if (includeFileTools)
|
||
allTools.AddRange(GetFileTools());
|
||
if (includeMemory)
|
||
allTools.AddRange(GetMemoryToolDefinitions());
|
||
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. Nutze dies sparsam – nur wenn die Entscheidung wirklich vom User getroffen werden sollte.",
|
||
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.",
|
||
input_schema = new
|
||
{
|
||
type = "object",
|
||
properties = new Dictionary<string, object>
|
||
{
|
||
["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<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: 180 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 = "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[] 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)
|
||
{
|
||
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)}";
|
||
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: {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() ?? "";
|
||
var psi = new ProcessStartInfo
|
||
{
|
||
FileName = WorkspaceHelper.ShellExecutable,
|
||
WorkingDirectory = workspace,
|
||
RedirectStandardInput = !WorkspaceHelper.IsWindows,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
UseShellExecute = false,
|
||
CreateNoWindow = true
|
||
};
|
||
if (WorkspaceHelper.IsWindows)
|
||
psi.Arguments = WorkspaceHelper.ShellArguments(command);
|
||
using var proc = Process.Start(psi);
|
||
if (proc == null) return "Fehler: Prozess konnte nicht gestartet werden";
|
||
if (!WorkspaceHelper.IsWindows)
|
||
{
|
||
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 => 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.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 (!WorkspaceHelper.IsSubPathOf(full, workspace))
|
||
throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt");
|
||
return full;
|
||
}
|
||
}
|