diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index a2d3f70..5565ca1 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -208,7 +208,7 @@ public static class ChatEndpoint try { - await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr); + await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr); } finally { diff --git a/Program.cs b/Program.cs index c0d0048..fe05f2b 100644 --- a/Program.cs +++ b/Program.cs @@ -57,12 +57,16 @@ var anthropicToken = builder.Configuration["AnthropicToken"] ?? ""; var payloadLogging = builder.Configuration.GetValue("PayloadLogging", false); var payloadLoggingGzip = builder.Configuration.GetValue("PayloadLoggingGzip", false); builder.Services.AddSingleton(sp => - new AnthropicService( +{ + var svc = new AnthropicService( sp.GetRequiredService(), anthropicToken, toolService, dbService, memoryService, payloadLogging, payloadLoggingGzip, vectorSearch: vectorSearchService, - embeddingBg: embeddingBgService)); + embeddingBg: embeddingBgService); + svc.SetChatService(chatService); + return svc; +}); // Telegram Bot (pro Session – Token aus Session-Config) var maxIterations = builder.Configuration.GetValue("MaxIterations", 100); diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index fdc2f2e..4fe7603 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -24,6 +24,7 @@ public class AnthropicService private readonly bool _payloadLoggingGzip; private readonly VectorSearchService _vectorSearch; private readonly EmbeddingBackgroundService _embeddingBg; + private ChatService _chatService; public AnthropicService(IHttpClientFactory httpFactory, string token, ToolService toolService, DatabaseService db, MemoryService memoryService = null, bool payloadLogging = false, bool payloadLoggingGzip = false, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null) { @@ -38,6 +39,15 @@ public class AnthropicService _embeddingBg = embeddingBg; } + /// + /// Wird nach der Konstruktion gesetzt um zirkuläre Abhängigkeit zu vermeiden + /// (ChatService → AnthropicService → ChatService). + /// + public void SetChatService(ChatService chatService) + { + _chatService = chatService; + } + public async Task StreamChat( HttpResponse response, string modelId, @@ -47,6 +57,7 @@ public class AnthropicService string workspace, string sessionId = null, string agentId = null, + string userId = null, bool useMemory = true, string webSearchVersion = null, int maxIterations = 100, @@ -183,7 +194,7 @@ public class AnthropicService return; } - var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, workCt, sseCt, remoteMgr, sessionId); + var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, workCt, sseCt, remoteMgr, sessionId, userId); lastStopReason = stopReason; promptTrimRetries = 0; // Erfolgreicher API-Call → Retry-Counter resetten @@ -1204,7 +1215,8 @@ public class AnthropicService CancellationToken workCt = default, CancellationToken sseCt = default, RemoteConnectionManager remoteMgr = null, - string sessionId = null) + string sessionId = null, + string userId = null) { var stream = await httpResp.Content.ReadAsStreamAsync(workCt); // Bei Cancellation den Stream disposen, damit ReadLineAsync sofort abbricht. @@ -1386,7 +1398,7 @@ public class AnthropicService string toolResult; bool isError = false; - try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); } + try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, userId: userId); } catch (OperationCanceledException) { throw; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } @@ -1593,7 +1605,8 @@ public class AnthropicService var hasWorkspace = !string.IsNullOrEmpty(workspace); var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId); var includeMemorySearch = _vectorSearch != null && _vectorSearch.GetEmbeddingCount() > 0; - var hasTools = hasWorkspace || includeMemory || includeRemote || includeMemorySearch; + var includeSendToSession = _db != null && _chatService != null; + var hasTools = hasWorkspace || includeMemory || includeRemote || includeMemorySearch || includeSendToSession; var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion); if (!hasTools && !hasWebSearch) return; @@ -1601,7 +1614,7 @@ public class AnthropicService var tools = new List(); if (hasTools) { - var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote, includeMemorySearch: includeMemorySearch); + var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote, includeMemorySearch: includeMemorySearch, includeSendToSession: includeSendToSession); if (planMode) allTools = _toolService.FilterReadOnlyTools(allTools); tools.AddRange(allTools); diff --git a/Services/ToolService.cs b/Services/ToolService.cs index 5587d25..afd272d 100644 --- a/Services/ToolService.cs +++ b/Services/ToolService.cs @@ -54,7 +54,7 @@ public class ToolService return filtered.ToArray(); } - public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false) + public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false, bool includeSendToSession = false) { var allTools = new List(); if (includeUserChoice) @@ -67,9 +67,30 @@ public class ToolService allTools.Add(GetMemorySearchTool()); if (includeRemote) allTools.Add(GetRemoteExecuteTool()); + if (includeSendToSession) + allTools.Add(GetSendToSessionTool()); return allTools.ToArray(); } + private object GetSendToSessionTool() + { + return new + { + name = "send_to_session", + description = "Schreibt eine Nachricht in eine andere Session und löst dort eine Antwort des Agents aus. Nützlich um Nachrichten, Grüße oder Aufgaben an einen anderen Agenten in einer anderen Session zu übermitteln.", + input_schema = new + { + type = "object", + properties = new Dictionary + { + ["session_id_or_name"] = new { type = "string", description = "Die ID oder der Name der Ziel-Session" }, + ["message"] = new { type = "string", description = "Die Nachricht die in die Ziel-Session geschrieben wird" } + }, + required = new[] { "session_id_or_name", "message" } + } + }; + } + private object GetAskUserChoiceTool() { return new @@ -356,7 +377,7 @@ public class ToolService }; } - public async Task ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null) + public async Task ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null, DatabaseService db = null, ChatService chatService = null, AnthropicService anthropicService = null, string userId = null) { switch (name) { @@ -706,6 +727,55 @@ public class ToolService } return sb.Length > 0 ? sb.ToString() : "(keine Ausgabe)"; } + case "send_to_session": + { + if (db == null || chatService == null || anthropicService == null) + return "Fehler: send_to_session nicht verfügbar"; + var sessionIdOrName = input.GetProperty("session_id_or_name").GetString() ?? ""; + var message = input.GetProperty("message").GetString() ?? ""; + if (string.IsNullOrEmpty(sessionIdOrName)) + return "Fehler: session_id_or_name fehlt"; + if (string.IsNullOrEmpty(message)) + return "Fehler: message fehlt"; + + // Ziel-Session per ID oder Name suchen + var targetSession = await db.GetSession(sessionIdOrName); + if (targetSession == null && !string.IsNullOrEmpty(userId)) + { + // Fallback: per Name suchen (case-insensitive) + var allSessions = await db.GetSessionsWithStats(userId); + targetSession = allSessions + .FirstOrDefault(s => string.Equals(s.Session.Name, sessionIdOrName, StringComparison.OrdinalIgnoreCase)) + ?.Session; + } + if (targetSession == null) + return $"Fehler: Session '{sessionIdOrName}' nicht gefunden"; + if (!string.IsNullOrEmpty(userId) && targetSession.UserId != userId) + return "Fehler: Zugriff auf diese Session nicht erlaubt"; + + var targetSessionId = targetSession.SessionId; + if (targetSessionId == sessionId) + return "Fehler: Kann keine Nachricht an die eigene Session senden"; + + // Context für Ziel-Session vorbereiten (analog zu CronjobService) + var ctx = await chatService.PrepareContext(targetSessionId, targetSession.UserId); + if (ctx == null) + return $"Fehler: Session '{targetSession.Name}' ist nicht konfiguriert (kein Agent zugewiesen?)"; + + // History aufbauen – User-Message NICHT in DB speichern (wie Cronjob) + // Stattdessen als kontextualisierte Nachricht in die History injizieren + var history = await chatService.BuildHistoryFromDb(targetSessionId, promptBlocks: ctx.PromptBlocks); + var senderAgent = agentId != null ? await db.GetAgent(agentId) : null; + var senderName = senderAgent?.Name ?? agentId ?? "Unbekannter Agent"; + var senderInfo = $"[Nachricht von Agent \"{senderName}\"]: {message}"; + var userContent = new List { new { type = "text", text = senderInfo } }; + history.Add(new Dictionary { ["role"] = "user", ["content"] = userContent }); + + await anthropicService.ChatNonStreaming(ctx, history); + await db.UpdateSessionTimestamp(targetSessionId); + + return $"Nachricht wurde erfolgreich an Session \"{targetSession.Name}\" gesendet. Der Agent dort hat geantwortet."; + } default: return $"Fehler: Unbekanntes Tool: {name}"; } diff --git a/wwwroot/js/tool-ui.js b/wwwroot/js/tool-ui.js index 26d7fca..137feba 100644 --- a/wwwroot/js/tool-ui.js +++ b/wwwroot/js/tool-ui.js @@ -8,7 +8,8 @@ const TOOL_DISPLAY_NAMES = { memory_write: 'Erinnerungen festhalten', web_search: 'Web-Suche', remote_execute: 'Remote-Befehl', - memory_search: 'Erinnerungen durchsuchen' + memory_search: 'Erinnerungen durchsuchen', + send_to_session: 'Nachricht an Session senden' }; function isToolVisible(toolName) {