feat: add glob, grep and todo_write builtin tools
This commit is contained in:
1 parent
c3865d239f
commit
5f2b2e2f18
3 files changed
+143
-1
No files matched your search
@@ -2,10 +2,14 @@ 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;
|
||||
|
||||
@@ -111,6 +115,68 @@ public class ToolService
|
||||
},
|
||||
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" }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -279,6 +345,81 @@ public class ToolService
|
||||
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<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($"{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))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [x] Weiteres Modell hinzufügen: "Haiku 4.5"
|
||||
- [ ] Weitere Tools: Glob? Search? Todo List?
|
||||
- [ ] Settings in Datenbank speichern (users table) anstelle von localStorage
|
||||
- [ ] Einzelne Nachicht als Link teilen
|
||||
|
||||
## Bugs
|
||||
- [x] Wenn gerade gestreamt wird und man refreshed die Seite, dann verliert das Frontend den Status "Streaming..." und ich kann eine Nachricht schreiben, obwohl das nicht möglich sein sollte
|
||||
|
||||
@@ -262,8 +262,8 @@
|
||||
/* EasyMDE Toolbar */
|
||||
.fe-mde-wrap .editor-toolbar {
|
||||
background: var(--f1-bg) !important;
|
||||
border: none !important;
|
||||
border-bottom: var(--border-width) solid var(--f1-border) !important;
|
||||
border-top: none !important;
|
||||
padding: 4px !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user