- ChainRecord: neue Felder ToolChoice (auto/any) und ToolNames (JSON-Array) - DatabaseService: Migration tool_choice + tool_names Spalten, INSERT erweitert - ChatEndpoint: berechnet Tools vor Chain-Erstellung, speichert Werte mit - ToolService: execute-Timeout Beschreibung korrigiert (30s → 180s)
326 lines
14 KiB
C#
326 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Gott.Services;
|
|
|
|
public class ToolService
|
|
{
|
|
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false)
|
|
{
|
|
var allTools = new List<object>();
|
|
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<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" }
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
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/2025-01-15.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/2025-01-15.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)
|
|
{
|
|
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<string>();
|
|
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();
|
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(180));
|
|
try
|
|
{
|
|
await proc.WaitForExitAsync(cts.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
proc.Kill(entireProcessTree: true);
|
|
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 "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;
|
|
}
|
|
}
|