feat: send_to_session tool – agent-to-agent messaging
- Neues Tool send_to_session: Agent kann Nachrichten in andere Sessions schicken - Ziel-Session per ID oder Name (case-insensitive) auffindbar - User-Message wird nicht in DB gespeichert (wie Cronjob) – keine Bubble im Frontend - Nachricht wird als '[Nachricht von Agent "X"]' kontextualisiert injiziert - Sicherheitscheck: Ziel-Session muss demselben User gehören, keine eigene Session - Zirkuläre Abhängigkeit ChatService ↔ AnthropicService via SetChatService() gelöst - tool-ui.js: Display-Name für send_to_session
This commit is contained in:
1 parent
20d891c9ef
commit
c18b94cd4e
5 files changed
+99
-11
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wird nach der Konstruktion gesetzt um zirkuläre Abhängigkeit zu vermeiden
|
||||
/// (ChatService → AnthropicService → ChatService).
|
||||
/// </summary>
|
||||
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<object>();
|
||||
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);
|
||||
|
||||
+72
-2
@@ -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<object>();
|
||||
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<string, object>
|
||||
{
|
||||
["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<string> 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<string> 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<object> { new { type = "text", text = senderInfo } };
|
||||
history.Add(new Dictionary<string, object> { ["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}";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user