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
This commit is contained in:
Meleki committed 2026-02-26 22:26:53 +01:00
1 parent 002ef293f0
commit b752c99a64
9 files changed
+50 -40

No files matched your search

+6 -1
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
@@ -121,7 +122,11 @@ public static class ChatEndpoint
new { type = "text", text = systemPromptText } new { type = "text", text = systemPromptText }
}; };
var workspace = session.Workspace;
if (!string.IsNullOrEmpty(workspace))
Directory.CreateDirectory(workspace);
var anthropic = new AnthropicService(httpFactory, token, toolService, db); 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);
} }
} }
+2 -1
View File
@@ -50,8 +50,9 @@ public static class SessionEndpoint
var description = body.TryGetProperty("description", out var d) ? d.GetString() : null; var description = body.TryGetProperty("description", out var d) ? d.GetString() : null;
var image = body.TryGetProperty("image", out var img) ? img.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 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); await ctx.Response.WriteAsJsonAsync(session);
} }
+1
View File
@@ -8,6 +8,7 @@ public class SessionRecord
public string Description { get; set; } public string Description { get; set; }
public string Image { get; set; } public string Image { get; set; }
public string AgentId { get; set; } public string AgentId { get; set; }
public string Workspace { get; set; }
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
public string CreatedAt { get; set; } public string CreatedAt { get; set; }
public string UpdatedAt { get; set; } public string UpdatedAt { get; set; }
+1 -3
View File
@@ -9,10 +9,8 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
var dataDir = Path.Combine(AppContext.BaseDirectory, "data"); 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"))); builder.Services.AddSingleton(new DatabaseService(Path.Combine(dataDir, "gott.db")));
var app = builder.Build(); var app = builder.Build();
+8 -4
View File
@@ -32,6 +32,7 @@ public class AnthropicService
List<object> systemPrompt, List<object> systemPrompt,
List<object> messages, List<object> messages,
string chainId, string chainId,
string workspace,
string sessionId = null, string sessionId = null,
int maxIterations = 25) int maxIterations = 25)
{ {
@@ -57,10 +58,12 @@ public class AnthropicService
["max_tokens"] = 4096, ["max_tokens"] = 4096,
["system"] = systemPrompt, ["system"] = systemPrompt,
["messages"] = messages, ["messages"] = messages,
["tools"] = _toolService.GetToolDefinitions(),
["stream"] = true ["stream"] = true
}; };
if (!string.IsNullOrEmpty(workspace))
payload["tools"] = _toolService.GetToolDefinitions();
var client = CreateClient(); var client = CreateClient();
var json = JsonSerializer.Serialize(payload); var json = JsonSerializer.Serialize(payload);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
@@ -86,7 +89,7 @@ public class AnthropicService
return; return;
} }
var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage); var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage, workspace);
lastStopReason = stopReason; lastStopReason = stopReason;
messages.Add(new { role = "assistant", content = assistantContent }); messages.Add(new { role = "assistant", content = assistantContent });
@@ -181,7 +184,8 @@ public class AnthropicService
private async Task<(string StopReason, List<object> Content, List<ToolResult> Tools)> ProcessStream( private async Task<(string StopReason, List<object> Content, List<ToolResult> Tools)> ProcessStream(
HttpResponse response, HttpResponse response,
HttpResponseMessage httpResp, HttpResponseMessage httpResp,
TokenUsage usage) TokenUsage usage,
string workspace)
{ {
var stream = await httpResp.Content.ReadAsStreamAsync(); var stream = await httpResp.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
@@ -297,7 +301,7 @@ public class AnthropicService
string toolResult; string toolResult;
bool isError = false; 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; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
executedTools.Add(new ToolResult(currentToolId, toolResult, isError)); executedTools.Add(new ToolResult(currentToolId, toolResult, isError));
+6 -3
View File
@@ -101,6 +101,7 @@ public class DatabaseService
TryAddColumn(conn, "sessions", "agent_id", "TEXT"); TryAddColumn(conn, "sessions", "agent_id", "TEXT");
TryAddColumn(conn, "sessions", "description", "TEXT"); TryAddColumn(conn, "sessions", "description", "TEXT");
TryAddColumn(conn, "sessions", "image", "TEXT"); TryAddColumn(conn, "sessions", "image", "TEXT");
TryAddColumn(conn, "sessions", "workspace", "TEXT");
TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0"); TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "chains", "agent_id", "TEXT"); TryAddColumn(conn, "chains", "agent_id", "TEXT");
} }
@@ -162,7 +163,7 @@ public class DatabaseService
// ── Sessions ── // ── Sessions ──
public async Task<SessionRecord> CreateSession(string name = null, string description = null, string image = null, string agentId = null) public async Task<SessionRecord> CreateSession(string name = null, string description = null, string image = null, string agentId = null, string workspace = null)
{ {
using var conn = CreateConnection(); using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o"); var now = DateTime.UtcNow.ToString("o");
@@ -173,12 +174,13 @@ public class DatabaseService
Description = description, Description = description,
Image = image, Image = image,
AgentId = agentId, AgentId = agentId,
Workspace = workspace,
CreatedAt = now, CreatedAt = now,
UpdatedAt = now UpdatedAt = now
}; };
await conn.ExecuteAsync(@" await conn.ExecuteAsync(@"
INSERT INTO sessions (session_id, name, description, image, agent_id, created_at, updated_at) INSERT INTO sessions (session_id, name, description, image, agent_id, workspace, created_at, updated_at)
VALUES (@SessionId, @Name, @Description, @Image, @AgentId, @CreatedAt, @UpdatedAt) VALUES (@SessionId, @Name, @Description, @Image, @AgentId, @Workspace, @CreatedAt, @UpdatedAt)
", session); ", session);
return 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("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("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("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); await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
} }
+20 -27
View File
@@ -11,13 +11,6 @@ namespace Gott.Services;
public class ToolService public class ToolService
{ {
private readonly string _workspaceRoot;
public ToolService(string workspaceRoot)
{
_workspaceRoot = workspaceRoot;
}
public object[] GetToolDefinitions() public object[] GetToolDefinitions()
{ {
return new object[] return new object[]
@@ -112,31 +105,31 @@ public class ToolService
}; };
} }
public async Task<string> ExecuteTool(string name, JsonElement input) public async Task<string> ExecuteTool(string name, JsonElement input, string workspace)
{ {
switch (name) switch (name)
{ {
case "read_file": case "read_file":
{ {
var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
if (!File.Exists(path)) 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); return File.ReadAllText(path);
} }
case "write_file": case "write_file":
{ {
var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
var dir = Path.GetDirectoryName(path); var dir = Path.GetDirectoryName(path);
if (dir != null) Directory.CreateDirectory(dir); if (dir != null) Directory.CreateDirectory(dir);
var content = input.GetProperty("content").GetString() ?? ""; var content = input.GetProperty("content").GetString() ?? "";
File.WriteAllText(path, content); 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": case "edit_file":
{ {
var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
if (!File.Exists(path)) 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 fileContent = File.ReadAllText(path);
var oldText = input.GetProperty("old_text").GetString() ?? ""; var oldText = input.GetProperty("old_text").GetString() ?? "";
var newText = input.GetProperty("new_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."; return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig.";
fileContent = fileContent.Replace(oldText, newText); fileContent = fileContent.Replace(oldText, newText);
File.WriteAllText(path, fileContent); File.WriteAllText(path, fileContent);
return $"Datei bearbeitet: {Path.GetRelativePath(_workspaceRoot, path)}"; return $"Datei bearbeitet: {Path.GetRelativePath(workspace, path)}";
} }
case "list_files": 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)) 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<string>(); var entries = new List<string>();
foreach (var dir in Directory.GetDirectories(path)) 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)) 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); return entries.Count == 0 ? "(leer)" : string.Join("\n", entries);
} }
case "delete_file": case "delete_file":
{ {
var path = ResolvePath(input.GetProperty("path").GetString() ?? ""); var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace);
if (File.Exists(path)) if (File.Exists(path))
{ {
File.Delete(path); File.Delete(path);
return $"Datei gelöscht: {Path.GetRelativePath(_workspaceRoot, path)}"; return $"Datei gelöscht: {Path.GetRelativePath(workspace, path)}";
} }
if (Directory.Exists(path)) if (Directory.Exists(path))
{ {
Directory.Delete(path, recursive: false); 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": case "execute":
{ {
@@ -179,7 +172,7 @@ public class ToolService
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
FileName = "/bin/bash", FileName = "/bin/bash",
WorkingDirectory = _workspaceRoot, WorkingDirectory = workspace,
RedirectStandardInput = true, RedirectStandardInput = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = 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 ?? "")); var full = Path.GetFullPath(Path.Combine(workspace, relativePath ?? ""));
if (!full.StartsWith(_workspaceRoot)) if (!full.StartsWith(workspace))
throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt"); throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt");
return full; return full;
} }
+2
View File
@@ -66,6 +66,8 @@
<textarea id="sessionDescription" rows="3" placeholder="Beschreibung (optional)"></textarea> <textarea id="sessionDescription" rows="3" placeholder="Beschreibung (optional)"></textarea>
<label>Agent</label> <label>Agent</label>
<select id="agentSelect"></select> <select id="agentSelect"></select>
<label>Workspace</label>
<input type="text" id="sessionWorkspace" placeholder="Absoluter Pfad zum Arbeitsverzeichnis (optional)">
<div class="agent-form-actions"> <div class="agent-form-actions">
<button id="sessionSaveBtn">Speichern</button> <button id="sessionSaveBtn">Speichern</button>
<button id="sessionCancelBtn">Abbrechen</button> <button id="sessionCancelBtn">Abbrechen</button>
+4 -1
View File
@@ -16,6 +16,7 @@ registerPage('session', {
const titleEl = document.getElementById('sessionPageTitle'); const titleEl = document.getElementById('sessionPageTitle');
const nameEl = document.getElementById('sessionName'); const nameEl = document.getElementById('sessionName');
const descEl = document.getElementById('sessionDescription'); const descEl = document.getElementById('sessionDescription');
const workspaceEl = document.getElementById('sessionWorkspace');
const agentSelect = document.getElementById('agentSelect'); const agentSelect = document.getElementById('agentSelect');
const imagePreview = document.getElementById('sessionImagePreview'); const imagePreview = document.getElementById('sessionImagePreview');
const imageInput = document.getElementById('sessionImageInput'); const imageInput = document.getElementById('sessionImageInput');
@@ -32,6 +33,7 @@ registerPage('session', {
titleEl.textContent = session.name || 'Unbenannt'; titleEl.textContent = session.name || 'Unbenannt';
nameEl.value = session.name || ''; nameEl.value = session.name || '';
descEl.value = session.description || ''; descEl.value = session.description || '';
workspaceEl.value = session.workspace || '';
_sessionImageData = session.image || null; _sessionImageData = session.image || null;
deleteBtn.hidden = false; deleteBtn.hidden = false;
openChatBtn.hidden = false; openChatBtn.hidden = false;
@@ -81,7 +83,8 @@ registerPage('session', {
name: nameEl.value.trim() || 'Unbenannt', name: nameEl.value.trim() || 'Unbenannt',
description: descEl.value.trim(), description: descEl.value.trim(),
image: _sessionImageData, image: _sessionImageData,
agentId: agentSelect.value || null agentId: agentSelect.value || null,
workspace: workspaceEl.value.trim() || null
}; };
if (isNew) { if (isNew) {