diff --git a/Endpoints/AgentEndpoint.cs b/Endpoints/AgentEndpoint.cs index ea3d947..e50551d 100644 --- a/Endpoints/AgentEndpoint.cs +++ b/Endpoints/AgentEndpoint.cs @@ -62,7 +62,9 @@ public static class AgentEndpoint UseSoul = !body.TryGetProperty("useSoul", out var us) || us.GetBoolean(), UseIdentity = !body.TryGetProperty("useIdentity", out var ui) || ui.GetBoolean(), UseUserInfo = !body.TryGetProperty("useUserInfo", out var uu) || uu.GetBoolean(), - UseMemory = !body.TryGetProperty("useMemory", out var um) || um.GetBoolean() + UseMemory = !body.TryGetProperty("useMemory", out var um) || um.GetBoolean(), + UseWebSearch = body.TryGetProperty("webSearchVersion", out var wsv) && !string.IsNullOrEmpty(wsv.GetString()), + WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : "" }; agent = await db.CreateAgent(agent); @@ -93,6 +95,11 @@ public static class AgentEndpoint if (body.TryGetProperty("useIdentity", out var ui)) agent.UseIdentity = ui.GetBoolean(); if (body.TryGetProperty("useUserInfo", out var uu)) agent.UseUserInfo = uu.GetBoolean(); if (body.TryGetProperty("useMemory", out var um)) agent.UseMemory = um.GetBoolean(); + if (body.TryGetProperty("webSearchVersion", out var wsv)) + { + agent.WebSearchVersion = wsv.GetString() ?? ""; + agent.UseWebSearch = !string.IsNullOrEmpty(agent.WebSearchVersion); + } await db.UpdateAgent(agent); memoryService.InitializeAgentFiles(agentId, agent.Name); diff --git a/Endpoints/AuthEndpoint.cs b/Endpoints/AuthEndpoint.cs index 45ebab1..0a25292 100644 --- a/Endpoints/AuthEndpoint.cs +++ b/Endpoints/AuthEndpoint.cs @@ -50,7 +50,8 @@ public static class AuthEndpoint { userId = user.UserId, username = user.Username, - displayName = user.DisplayName + displayName = user.DisplayName, + isAdmin = user.IsAdmin }); } @@ -91,7 +92,8 @@ public static class AuthEndpoint { userId = user.UserId, username = user.Username, - displayName = user.DisplayName + displayName = user.DisplayName, + isAdmin = user.IsAdmin }); } } diff --git a/Endpoints/BackupEndpoint.cs b/Endpoints/BackupEndpoint.cs index dd257e4..b61f4ef 100644 --- a/Endpoints/BackupEndpoint.cs +++ b/Endpoints/BackupEndpoint.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.IO; using System.Linq; +using Gott.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -15,8 +16,10 @@ public static class BackupEndpoint app.MapPost("/api/backup", HandleBackup); } - private static async System.Threading.Tasks.Task HandleBackup() + private static async System.Threading.Tasks.Task HandleBackup(HttpContext ctx) { + if (!ctx.IsAdmin()) + return Results.Forbid(); var dataDir = Path.Combine(AppContext.BaseDirectory, "data"); if (!Directory.Exists(dataDir)) diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index c9f925e..3dd9938 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -72,6 +72,7 @@ public static class ChatEndpoint { "opus" => "claude-opus-4-6", "sonnet" => "claude-sonnet-4-6", + "haiku" => "claude-haiku-4-5", _ => "claude-sonnet-4-6" }; @@ -185,7 +186,7 @@ public static class ChatEndpoint streamingTracker.Start(sessionId, chainId); try { - await anthropic.StreamChat(res, modelId, systemPrompt, messages, chainId, workspace, sessionId, agentId: agent.AgentId, useMemory: agent.UseMemory, maxIterations: maxIterations, toolChoice: toolChoice, cancellationToken: cancellationToken); + await anthropic.StreamChat(res, modelId, systemPrompt, messages, chainId, workspace, sessionId, agentId: agent.AgentId, useMemory: agent.UseMemory, webSearchVersion: agent.WebSearchVersion, maxIterations: maxIterations, toolChoice: toolChoice, cancellationToken: cancellationToken); } finally { diff --git a/Extensions/HttpContextExtensions.cs b/Extensions/HttpContextExtensions.cs index a35ab3f..fb836a8 100644 --- a/Extensions/HttpContextExtensions.cs +++ b/Extensions/HttpContextExtensions.cs @@ -8,4 +8,9 @@ public static class HttpContextExtensions { return ctx.Items["UserId"] as string; } + + public static bool IsAdmin(this HttpContext ctx) + { + return ctx.Items["IsAdmin"] is true; + } } diff --git a/Middleware/AuthMiddleware.cs b/Middleware/AuthMiddleware.cs index 2944df0..d6dec9a 100644 --- a/Middleware/AuthMiddleware.cs +++ b/Middleware/AuthMiddleware.cs @@ -13,7 +13,7 @@ public class AuthMiddleware _next = next; } - public async Task InvokeAsync(HttpContext ctx, AuthService auth) + public async Task InvokeAsync(HttpContext ctx, AuthService auth, DatabaseService db) { var path = ctx.Request.Path.Value ?? ""; @@ -35,6 +35,10 @@ public class AuthMiddleware } ctx.Items["UserId"] = userId; + + var user = await db.GetUser(userId); + ctx.Items["IsAdmin"] = user?.IsAdmin ?? false; + await _next(ctx); } } diff --git a/Models/AgentRecord.cs b/Models/AgentRecord.cs index 06ebce9..de50504 100644 --- a/Models/AgentRecord.cs +++ b/Models/AgentRecord.cs @@ -7,7 +7,7 @@ public class AgentRecord public string Name { get; set; } public string Avatar { get; set; } // base64 data url public string Description { get; set; } - public string Model { get; set; } // sonnet, opus + public string Model { get; set; } // sonnet, opus, haiku public string ToolChoice { get; set; } // "any" oder "auto" public string CustomCommands { get; set; } public string AutoCommit { get; set; } = ""; // "", "mention", "forbid" @@ -15,6 +15,8 @@ public class AgentRecord public bool UseIdentity { get; set; } = true; public bool UseUserInfo { get; set; } = true; public bool UseMemory { get; set; } = true; + public bool UseWebSearch { get; set; } + public string WebSearchVersion { get; set; } = ""; public string UserId { get; set; } public bool IsDeleted { get; set; } public string CreatedAt { get; set; } diff --git a/Models/UserRecord.cs b/Models/UserRecord.cs index 2514b5a..a127ff7 100644 --- a/Models/UserRecord.cs +++ b/Models/UserRecord.cs @@ -9,4 +9,5 @@ public class UserRecord public string DisplayName { get; set; } public string CreatedAt { get; set; } public string UpdatedAt { get; set; } + public bool IsAdmin { get; set; } } diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 29772c5..267876d 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -39,6 +39,7 @@ public class AnthropicService string sessionId = null, string agentId = null, bool useMemory = true, + string webSearchVersion = null, int maxIterations = 100, string toolChoice = "any", CancellationToken cancellationToken = default) @@ -75,10 +76,17 @@ public class AnthropicService var hasWorkspace = !string.IsNullOrEmpty(workspace); var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId); + var hasTools = hasWorkspace || includeMemory; + var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion); - if (hasWorkspace || includeMemory) + if (hasTools || hasWebSearch) { - payload["tools"] = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory); + var tools = new List(); + if (hasTools) + tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory)); + if (hasWebSearch) + tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 }); + payload["tools"] = tools; // "any" erzwingt Tool-Nutzung, aber nur in der ersten Iteration, // damit Claude nicht endlos Tools aufruft wenn die Aufgabe erledigt ist. var effectiveChoice = (toolChoice == "any" && iterations == 1) ? "any" : "auto"; @@ -257,6 +265,7 @@ public class AnthropicService var currentTextBlock = new StringBuilder(); var blockTypes = new Dictionary(); + var webSearchResultBlocks = new Dictionary(); var executedTools = new List(); string line; @@ -313,6 +322,24 @@ public class AnthropicService currentToolInputJson.Clear(); await WriteSseEvent(response, "tool_start", new { name = currentToolName, id = currentToolId }); } + else if (cbType == "server_tool_use") + { + if (currentTextBlock.Length > 0) + { + assistantContent.Add(new { type = "text", text = currentTextBlock.ToString() }); + currentTextBlock.Clear(); + } + currentToolId = cb.GetProperty("id").GetString() ?? ""; + currentToolName = cb.GetProperty("name").GetString() ?? ""; + currentToolInputJson.Clear(); + await WriteSseEvent(response, "web_search_start", new { name = currentToolName, id = currentToolId }); + } + else if (cbType == "web_search_tool_result") + { + // Gesamten Block als JSON speichern für die Conversation-History + var rawBlock = JsonSerializer.Deserialize(cb.GetRawText()); + webSearchResultBlocks[idx] = rawBlock; + } break; } @@ -379,6 +406,27 @@ public class AnthropicService isError }); } + else if (bt == "server_tool_use") + { + // Server-side tool (Web Search) – Anthropic führt es aus, wir speichern nur die Anfrage + JsonElement inputEl; + try { inputEl = JsonSerializer.Deserialize(currentToolInputJson.ToString()); } + catch { inputEl = JsonSerializer.Deserialize("{}"); } + + assistantContent.Add(new + { + type = "server_tool_use", + id = currentToolId, + name = currentToolName, + input = inputEl + }); + } + else if (bt == "web_search_tool_result") + { + // Web-Search-Ergebnis von Anthropic – als Raw-JSON in assistantContent aufnehmen + assistantContent.Add(webSearchResultBlocks[idx]); + await WriteSseEvent(response, "web_search_end", new { id = currentToolId }); + } } break; } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 7a7d822..a1de799 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -145,9 +145,20 @@ public class DatabaseService TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1"); + TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0"); + TryAddColumn(conn, "agents", "web_search_version", "TEXT DEFAULT ''"); + + // Migration: use_web_search (bool) → web_search_version (text) + try + { + conn.Execute("UPDATE agents SET web_search_version = 'web_search_20260209' WHERE use_web_search = 1 AND (web_search_version IS NULL OR web_search_version = '')"); + } + catch { /* already migrated */ } + // User-Zuordnung TryAddColumn(conn, "sessions", "user_id", "TEXT"); TryAddColumn(conn, "agents", "user_id", "TEXT"); + TryAddColumn(conn, "users", "is_admin", "INTEGER NOT NULL DEFAULT 0"); conn.Execute(@" CREATE TABLE IF NOT EXISTS agent_skills ( @@ -189,8 +200,8 @@ public class DatabaseService agent.CreatedAt = now; agent.UpdatedAt = now; await conn.ExecuteAsync(@" - INSERT INTO agents (agent_id, user_id, name, avatar, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, is_deleted, created_at, updated_at) - VALUES (@AgentId, @UserId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, 0, @CreatedAt, @UpdatedAt) + INSERT INTO agents (agent_id, user_id, name, avatar, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, is_deleted, created_at, updated_at) + VALUES (@AgentId, @UserId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, 0, @CreatedAt, @UpdatedAt) ", agent); return agent; } @@ -217,7 +228,7 @@ public class DatabaseService UPDATE agents SET name = @Name, avatar = @Avatar, description = @Description, model = @Model, tool_choice = @ToolChoice, custom_commands = @CustomCommands, auto_commit = @AutoCommit, - use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, + use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion, updated_at = @UpdatedAt WHERE agent_id = @AgentId AND user_id = @UserId ", agent); @@ -668,8 +679,8 @@ public class DatabaseService adminUserId = Guid.NewGuid().ToString("N"); var now = DateTime.UtcNow.ToString("o"); conn.Execute(@" - INSERT INTO users (user_id, username, password_hash, display_name, created_at, updated_at) - VALUES (@userId, @username, @hash, @displayName, @now, @now)", + INSERT INTO users (user_id, username, password_hash, display_name, is_admin, created_at, updated_at) + VALUES (@userId, @username, @hash, @displayName, 1, @now, @now)", new { userId = adminUserId, @@ -687,6 +698,9 @@ public class DatabaseService // Bestehende Sessions/Agents ohne User dem ersten User zuweisen conn.Execute("UPDATE sessions SET user_id = @userId WHERE user_id IS NULL", new { userId = adminUserId }); conn.Execute("UPDATE agents SET user_id = @userId WHERE user_id IS NULL", new { userId = adminUserId }); + + // Erster User ist immer Admin + conn.Execute("UPDATE users SET is_admin = 1 WHERE user_id = @userId AND is_admin = 0", new { userId = adminUserId }); } public async Task GetUserByUsername(string username) diff --git a/TODO.md b/TODO.md index 6b79a49..0bf0a3b 100644 --- a/TODO.md +++ b/TODO.md @@ -12,6 +12,8 @@ - [x] Anfrage/Stream abbrechen - [ ] Telegram-Intergration - [x] Refactoring: Agent/Session-Einstellungen, die den System-Prompt bzw. das Tool Use bearbeiten +- [ ] Cronjobs +- [ ] Weiteres Modell hinzufügen: "Haiku 4.5" ## 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 diff --git a/wwwroot/index.html b/wwwroot/index.html index 13b13e8..338b840 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -134,6 +134,7 @@ + + -
+