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:
1 parent
002ef293f0
commit
b752c99a64
9 files changed
+50
-40
No files matched your search
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in new issue
Block a user