From b752c99a64cb41a37a780ceb2260434b11381df0 Mon Sep 17 00:00:00 2001 From: Meleki Date: Thu, 26 Feb 2026 22:26:53 +0100 Subject: [PATCH] Per-session workspace, tools only when workspace set - Session has workspace field (absolute path, optional) - ToolService stateless, receives workspace per call - No workspace = no tools sent to API - Removed hardcoded workspace from Program.cs - Workspace field in session edit form --- Endpoints/ChatEndpoint.cs | 7 ++++- Endpoints/SessionEndpoint.cs | 3 +- Models/SessionRecord.cs | 1 + Program.cs | 4 +-- Services/AnthropicService.cs | 12 +++++--- Services/DatabaseService.cs | 9 ++++-- Services/ToolService.cs | 47 ++++++++++++++------------------ wwwroot/index.html | 2 ++ wwwroot/js/pages/session-page.js | 5 +++- 9 files changed, 50 insertions(+), 40 deletions(-) diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 014b968..001a7dc 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Text.Json; @@ -121,7 +122,11 @@ public static class ChatEndpoint new { type = "text", text = systemPromptText } }; + var workspace = session.Workspace; + if (!string.IsNullOrEmpty(workspace)) + Directory.CreateDirectory(workspace); + var anthropic = new AnthropicService(httpFactory, token, toolService, db); - await anthropic.StreamChat(res, modelId, systemPrompt, messages, chainId); + await anthropic.StreamChat(res, modelId, systemPrompt, messages, chainId, workspace); } } diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index 8f6ae58..4a165ed 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -50,8 +50,9 @@ public static class SessionEndpoint var description = body.TryGetProperty("description", out var d) ? d.GetString() : null; var image = body.TryGetProperty("image", out var img) ? img.GetString() : null; var agentId = body.TryGetProperty("agentId", out var a) ? a.GetString() : null; + var workspace = body.TryGetProperty("workspace", out var w) ? w.GetString() : null; - var session = await db.CreateSession(name, description, image, agentId); + var session = await db.CreateSession(name, description, image, agentId, workspace); await ctx.Response.WriteAsJsonAsync(session); } diff --git a/Models/SessionRecord.cs b/Models/SessionRecord.cs index 4a7a62e..bf12ba2 100644 --- a/Models/SessionRecord.cs +++ b/Models/SessionRecord.cs @@ -8,6 +8,7 @@ public class SessionRecord public string Description { get; set; } public string Image { get; set; } public string AgentId { get; set; } + public string Workspace { get; set; } public bool IsDeleted { get; set; } public string CreatedAt { get; set; } public string UpdatedAt { get; set; } diff --git a/Program.cs b/Program.cs index 1abe648..62a7f71 100644 --- a/Program.cs +++ b/Program.cs @@ -9,10 +9,8 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient(); var dataDir = Path.Combine(AppContext.BaseDirectory, "data"); -var workspaceRoot = Path.Combine(dataDir, "workspace"); -Directory.CreateDirectory(workspaceRoot); -builder.Services.AddSingleton(new ToolService(workspaceRoot)); +builder.Services.AddSingleton(new ToolService()); builder.Services.AddSingleton(new DatabaseService(Path.Combine(dataDir, "gott.db"))); var app = builder.Build(); diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 7ee61e4..f34c5bc 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -32,6 +32,7 @@ public class AnthropicService List systemPrompt, List messages, string chainId, + string workspace, string sessionId = null, int maxIterations = 25) { @@ -57,10 +58,12 @@ public class AnthropicService ["max_tokens"] = 4096, ["system"] = systemPrompt, ["messages"] = messages, - ["tools"] = _toolService.GetToolDefinitions(), ["stream"] = true }; + if (!string.IsNullOrEmpty(workspace)) + payload["tools"] = _toolService.GetToolDefinitions(); + var client = CreateClient(); var json = JsonSerializer.Serialize(payload); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); @@ -86,7 +89,7 @@ public class AnthropicService return; } - var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage); + var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage, workspace); lastStopReason = stopReason; messages.Add(new { role = "assistant", content = assistantContent }); @@ -181,7 +184,8 @@ public class AnthropicService private async Task<(string StopReason, List Content, List Tools)> ProcessStream( HttpResponse response, HttpResponseMessage httpResp, - TokenUsage usage) + TokenUsage usage, + string workspace) { var stream = await httpResp.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); @@ -297,7 +301,7 @@ public class AnthropicService string toolResult; bool isError = false; - try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl); } + try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace); } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } executedTools.Add(new ToolResult(currentToolId, toolResult, isError)); diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 9d8f64d..83be9c6 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -101,6 +101,7 @@ public class DatabaseService TryAddColumn(conn, "sessions", "agent_id", "TEXT"); TryAddColumn(conn, "sessions", "description", "TEXT"); TryAddColumn(conn, "sessions", "image", "TEXT"); + TryAddColumn(conn, "sessions", "workspace", "TEXT"); TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0"); TryAddColumn(conn, "chains", "agent_id", "TEXT"); } @@ -162,7 +163,7 @@ public class DatabaseService // ── Sessions ── - public async Task CreateSession(string name = null, string description = null, string image = null, string agentId = null) + public async Task CreateSession(string name = null, string description = null, string image = null, string agentId = null, string workspace = null) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); @@ -173,12 +174,13 @@ public class DatabaseService Description = description, Image = image, AgentId = agentId, + Workspace = workspace, CreatedAt = now, UpdatedAt = now }; await conn.ExecuteAsync(@" - INSERT INTO sessions (session_id, name, description, image, agent_id, created_at, updated_at) - VALUES (@SessionId, @Name, @Description, @Image, @AgentId, @CreatedAt, @UpdatedAt) + INSERT INTO sessions (session_id, name, description, image, agent_id, workspace, created_at, updated_at) + VALUES (@SessionId, @Name, @Description, @Image, @AgentId, @Workspace, @CreatedAt, @UpdatedAt) ", session); return session; } @@ -217,6 +219,7 @@ public class DatabaseService if (body.TryGetProperty("description", out var d)) { sets.Add("description = @desc"); p.Add("desc", d.GetString()); } if (body.TryGetProperty("image", out var img)) { sets.Add("image = @image"); p.Add("image", img.GetString()); } if (body.TryGetProperty("agentId", out var a)) { sets.Add("agent_id = @agentId2"); p.Add("agentId2", a.GetString()); } + if (body.TryGetProperty("workspace", out var w)) { sets.Add("workspace = @workspace"); p.Add("workspace", w.GetString()); } await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p); } diff --git a/Services/ToolService.cs b/Services/ToolService.cs index 3f4f5d4..d0324a3 100644 --- a/Services/ToolService.cs +++ b/Services/ToolService.cs @@ -11,13 +11,6 @@ namespace Gott.Services; public class ToolService { - private readonly string _workspaceRoot; - - public ToolService(string workspaceRoot) - { - _workspaceRoot = workspaceRoot; - } - public object[] GetToolDefinitions() { return new object[] @@ -112,31 +105,31 @@ public class ToolService }; } - public async Task ExecuteTool(string name, JsonElement input) + public async Task ExecuteTool(string name, JsonElement input, string workspace) { switch (name) { case "read_file": { - var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); + var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); if (!File.Exists(path)) - return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(_workspaceRoot, path)}"; + return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspace, path)}"; return File.ReadAllText(path); } case "write_file": { - var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); + 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(_workspaceRoot, path)} ({content.Length} Zeichen)"; + return $"Datei geschrieben: {Path.GetRelativePath(workspace, path)} ({content.Length} Zeichen)"; } case "edit_file": { - var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); + var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); if (!File.Exists(path)) - return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(_workspaceRoot, 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() ?? ""; @@ -144,34 +137,34 @@ public class ToolService 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(_workspaceRoot, path)}"; + return $"Datei bearbeitet: {Path.GetRelativePath(workspace, path)}"; } case "list_files": { - var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""); + var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "", workspace); if (!Directory.Exists(path)) - return $"Fehler: Verzeichnis nicht gefunden: {Path.GetRelativePath(_workspaceRoot, path)}"; + return $"Fehler: Verzeichnis nicht gefunden: {Path.GetRelativePath(workspace, path)}"; var entries = new List(); foreach (var dir in Directory.GetDirectories(path)) - entries.Add(Path.GetRelativePath(_workspaceRoot, dir) + "/"); + entries.Add(Path.GetRelativePath(workspace, dir) + "/"); foreach (var file in Directory.GetFiles(path)) - entries.Add(Path.GetRelativePath(_workspaceRoot, file)); + 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() ?? ""); + var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); if (File.Exists(path)) { File.Delete(path); - return $"Datei gelöscht: {Path.GetRelativePath(_workspaceRoot, path)}"; + return $"Datei gelöscht: {Path.GetRelativePath(workspace, path)}"; } if (Directory.Exists(path)) { Directory.Delete(path, recursive: false); - return $"Verzeichnis gelöscht: {Path.GetRelativePath(_workspaceRoot, path)}"; + return $"Verzeichnis gelöscht: {Path.GetRelativePath(workspace, path)}"; } - return $"Fehler: Nicht gefunden: {Path.GetRelativePath(_workspaceRoot, path)}"; + return $"Fehler: Nicht gefunden: {Path.GetRelativePath(workspace, path)}"; } case "execute": { @@ -179,7 +172,7 @@ public class ToolService var psi = new ProcessStartInfo { FileName = "/bin/bash", - WorkingDirectory = _workspaceRoot, + WorkingDirectory = workspace, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, @@ -218,10 +211,10 @@ public class ToolService } } - private string ResolvePath(string relativePath) + private string ResolvePath(string relativePath, string workspace) { - var full = Path.GetFullPath(Path.Combine(_workspaceRoot, relativePath ?? "")); - if (!full.StartsWith(_workspaceRoot)) + var full = Path.GetFullPath(Path.Combine(workspace, relativePath ?? "")); + if (!full.StartsWith(workspace)) throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt"); return full; } diff --git a/wwwroot/index.html b/wwwroot/index.html index bddef28..4e1f807 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -66,6 +66,8 @@ + +
diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js index 89d536c..ca63746 100644 --- a/wwwroot/js/pages/session-page.js +++ b/wwwroot/js/pages/session-page.js @@ -16,6 +16,7 @@ registerPage('session', { const titleEl = document.getElementById('sessionPageTitle'); const nameEl = document.getElementById('sessionName'); const descEl = document.getElementById('sessionDescription'); + const workspaceEl = document.getElementById('sessionWorkspace'); const agentSelect = document.getElementById('agentSelect'); const imagePreview = document.getElementById('sessionImagePreview'); const imageInput = document.getElementById('sessionImageInput'); @@ -32,6 +33,7 @@ registerPage('session', { titleEl.textContent = session.name || 'Unbenannt'; nameEl.value = session.name || ''; descEl.value = session.description || ''; + workspaceEl.value = session.workspace || ''; _sessionImageData = session.image || null; deleteBtn.hidden = false; openChatBtn.hidden = false; @@ -81,7 +83,8 @@ registerPage('session', { name: nameEl.value.trim() || 'Unbenannt', description: descEl.value.trim(), image: _sessionImageData, - agentId: agentSelect.value || null + agentId: agentSelect.value || null, + workspace: workspaceEl.value.trim() || null }; if (isNew) {