Files
gott/Services/ToolService.cs
T

240 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 markLastAsCached = false)
{
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" }
}
},
// Letztes Tool bekommt cache_control – Anthropic cached alles bis einschließlich
// dieses Breakpoints (System-Prompt + alle Tool-Definitionen) für 5 Minuten.
markLastAsCached
? (object)new
{
name = "execute",
description = "Führt einen Shell-Befehl im Workspace-Verzeichnis aus. Gibt stdout und stderr zurück. Timeout: 30 Sekunden.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" }
},
required = new[] { "command" }
},
cache_control = new { type = "ephemeral" }
}
: new
{
name = "execute",
description = "Führt einen Shell-Befehl im Workspace-Verzeichnis aus. Gibt stdout und stderr zurück. Timeout: 30 Sekunden.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["command"] = new { type = "string", description = "Shell-Befehl zum Ausführen" }
},
required = new[] { "command" }
}
}
};
}
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace)
{
switch (name)
{
case "read_file":
{
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":
{
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":
{
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":
{
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":
{
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":
{
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(30));
try
{
await proc.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException)
{
proc.Kill(entireProcessTree: true);
return "Fehler: Timeout nach 30 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();
}
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;
}
}