feat: Finalizer-Agent – dritter Agent der nach Worker + Review einschreitet
Backend:
- SessionRecord + DB-Migration: finalizer_agent_id auf sessions
- ChatContext.FinalizerContext (Model, SystemPrompt, Tools, Workspace)
- ChatService: FinalizerContext aufbauen (analog zu ReviewContext)
- AnthropicService: RunFinalizer() nach Review-Phase (oder direkt nach Worker)
- RunReviewAgentChat() refactored zu generischem RunAgentChat()
mit agentType-Parameter ('review' | 'finalizer') für SSE-Events und DB-Tags
- SSE-Events: finalizer_start, finalizer_text, finalizer_tool_use, finalizer_done
Frontend:
- Session-Page: Finalizer-Agent-Picker (im 'Weitere Einstellungen' Bereich)
- Chat: SSE-Events für Finalizer-Phase verarbeiten
- UI: addReviewBubble/finalizeReviewBubble unterstützen agentType 'finalizer'
mit eigener Farbe (success-green) und Flag-Checkered-Icon
- renderMarkdown: [Finalizer: AgentName] Tags parsen
- History: Finalizer-Messages als .finalizer-bubble rendern
- Shared-Page: Finalizer-Messages korrekt erkennen und stylen
- Dashboard: Finalizer-Badge (flag-checkered) auf Session-Cards
This commit is contained in:
1 parent
a3f2aa38c1
commit
4bec740765
14 files changed
+228
-46
No files matched your search
@@ -48,6 +48,7 @@ public class AnthropicService
|
||||
int maxIterations = 100,
|
||||
string toolChoice = "any",
|
||||
ReviewContext review = null,
|
||||
FinalizerContext finalizer = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
@@ -202,6 +203,13 @@ public class AnthropicService
|
||||
}
|
||||
}
|
||||
|
||||
// ── Finalizer-Agent Phase ──
|
||||
if (finalizer != null && stopReason == "end_turn")
|
||||
{
|
||||
var finalizerIterations = await RunFinalizer(response, finalizer, messages, chainId, workspace, sessionId, usage, iterations, cancellationToken);
|
||||
iterations += finalizerIterations;
|
||||
}
|
||||
|
||||
var finalStopReason = string.IsNullOrEmpty(stopReason) ? "unexpected_end" : stopReason;
|
||||
await FinalizeChain(chain, usage, iterations, finalStopReason, sw, streamErrorInfo);
|
||||
await TryWriteSse(response, "done", new
|
||||
@@ -311,9 +319,12 @@ public class AnthropicService
|
||||
var reviewMessages = BuildReviewMessages(workerMessages);
|
||||
|
||||
// Review-Agent iteriert mit eigenen Tools
|
||||
var (reviewText, reviewIterations) = await RunReviewAgentChat(
|
||||
response, review, reviewMessages, chainId, workspace, sessionId,
|
||||
usage, baseIterations, cancellationToken);
|
||||
var (reviewText, reviewIterations) = await RunAgentChat(
|
||||
response, review.ModelId, review.SystemPrompt, review.AgentId,
|
||||
workspace, review.UseMemory, review.WebSearchVersion,
|
||||
review.ToolChoice, reviewMessages, chainId, sessionId, usage,
|
||||
baseIterations, "review", review.Agent?.Name ?? "Review-Agent",
|
||||
cancellationToken);
|
||||
|
||||
var result = new ReviewResult { ReviewIterationCount = reviewIterations };
|
||||
|
||||
@@ -344,6 +355,47 @@ public class AnthropicService
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Finalizer-Agent Logik ──
|
||||
|
||||
/// <summary>
|
||||
/// Führt den Finalizer-Agent aus. Wird einmal nach der Review-Phase (oder direkt nach dem Worker) aufgerufen.
|
||||
/// Der Finalizer kann Tools nutzen, gibt aber kein Feedback an den Worker zurück.
|
||||
/// </summary>
|
||||
private async Task<int> RunFinalizer(
|
||||
HttpResponse response,
|
||||
FinalizerContext finalizer,
|
||||
List<object> workerMessages,
|
||||
string chainId,
|
||||
string workspace,
|
||||
string sessionId,
|
||||
TokenUsage usage,
|
||||
int baseIterations,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await TryWriteSse(response, "finalizer_start", new
|
||||
{
|
||||
agentId = finalizer.AgentId,
|
||||
agentName = finalizer.Agent?.Name ?? "Finalizer-Agent"
|
||||
}, cancellationToken);
|
||||
|
||||
// Finalizer bekommt die Konversation als Kontext (gleiche Methode wie Review)
|
||||
var finalizerMessages = BuildReviewMessages(workerMessages);
|
||||
|
||||
var (text, iterations) = await RunAgentChat(
|
||||
response, finalizer.ModelId, finalizer.SystemPrompt, finalizer.AgentId,
|
||||
finalizer.Workspace, finalizer.UseMemory, finalizer.WebSearchVersion,
|
||||
finalizer.ToolChoice, finalizerMessages, chainId, sessionId, usage,
|
||||
baseIterations, "finalizer", finalizer.Agent?.Name ?? "Finalizer-Agent",
|
||||
cancellationToken);
|
||||
|
||||
await TryWriteSse(response, "finalizer_done", new
|
||||
{
|
||||
agentName = finalizer.Agent?.Name ?? "Finalizer-Agent"
|
||||
}, cancellationToken);
|
||||
|
||||
return iterations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut die Messages für den Review-Agent auf.
|
||||
/// Der Review-Agent bekommt die komplette Worker-Konversation als eine User-Message.
|
||||
@@ -411,23 +463,32 @@ public class AnthropicService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Führt den Review-Agent Chat aus (non-streaming für den Agent, aber SSE-Events für den Client).
|
||||
/// Gibt den finalen Text und die Anzahl der Iterationen zurück.
|
||||
/// Generischer Agent-Chat (non-streaming für den Agent, aber SSE-Events für den Client).
|
||||
/// Wird für Review-Agent und Finalizer-Agent verwendet.
|
||||
/// agentType steuert SSE-Event-Prefixe und DB-Message-Tags ("review" oder "finalizer").
|
||||
/// </summary>
|
||||
private async Task<(string text, int iterations)> RunReviewAgentChat(
|
||||
private async Task<(string text, int iterations)> RunAgentChat(
|
||||
HttpResponse response,
|
||||
ReviewContext review,
|
||||
string modelId,
|
||||
List<object> systemPrompt,
|
||||
string agentId,
|
||||
string agentWorkspace,
|
||||
bool useMemory,
|
||||
string webSearchVersion,
|
||||
string agentToolChoice,
|
||||
List<object> messages,
|
||||
string chainId,
|
||||
string workspace,
|
||||
string sessionId,
|
||||
TokenUsage usage,
|
||||
int baseIterations,
|
||||
string agentType,
|
||||
string agentName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int iterations = 0;
|
||||
var finalText = new StringBuilder();
|
||||
int maxToolIterations = 20; // Review-Agent bekommt max 20 Tool-Iterationen
|
||||
int maxToolIterations = 20;
|
||||
var tag = agentType == "finalizer" ? "Finalizer" : "Review";
|
||||
|
||||
while (iterations < maxToolIterations)
|
||||
{
|
||||
@@ -436,19 +497,19 @@ public class AnthropicService
|
||||
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = review.ModelId,
|
||||
["model"] = modelId,
|
||||
["max_tokens"] = 8192,
|
||||
["messages"] = messages,
|
||||
["stream"] = false
|
||||
};
|
||||
|
||||
if (review.SystemPrompt != null && review.SystemPrompt.Count > 0)
|
||||
payload["system"] = review.SystemPrompt;
|
||||
if (systemPrompt != null && systemPrompt.Count > 0)
|
||||
payload["system"] = systemPrompt;
|
||||
|
||||
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
||||
var includeMemory = review.UseMemory && _memoryService != null && !string.IsNullOrEmpty(review.AgentId);
|
||||
var hasWorkspace = !string.IsNullOrEmpty(agentWorkspace);
|
||||
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
|
||||
var hasTools = hasWorkspace || includeMemory;
|
||||
var hasWebSearch = !string.IsNullOrEmpty(review.WebSearchVersion);
|
||||
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
|
||||
|
||||
if (hasTools || hasWebSearch)
|
||||
{
|
||||
@@ -456,9 +517,9 @@ public class AnthropicService
|
||||
if (hasTools)
|
||||
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory));
|
||||
if (hasWebSearch)
|
||||
tools.Add(new { type = review.WebSearchVersion, name = "web_search", max_uses = 5 });
|
||||
tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 });
|
||||
payload["tools"] = tools;
|
||||
var effectiveChoice = (review.ToolChoice == "any" && iterations == 1) ? "any" : "auto";
|
||||
var effectiveChoice = (agentToolChoice == "any" && iterations == 1) ? "any" : "auto";
|
||||
payload["tool_choice"] = new { type = effectiveChoice };
|
||||
}
|
||||
|
||||
@@ -475,12 +536,11 @@ public class AnthropicService
|
||||
|
||||
if (!httpResp.IsSuccessStatusCode)
|
||||
{
|
||||
return ($"Review-Agent API Fehler: {(int)httpResp.StatusCode}: {respBody}", iterations);
|
||||
return ($"{tag}-Agent API Fehler: {(int)httpResp.StatusCode}: {respBody}", iterations);
|
||||
}
|
||||
|
||||
var resp = JsonSerializer.Deserialize<JsonElement>(respBody);
|
||||
|
||||
// Usage tracken (auf den gemeinsamen Zähler)
|
||||
if (resp.TryGetProperty("usage", out var u))
|
||||
{
|
||||
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
|
||||
@@ -505,22 +565,20 @@ public class AnthropicService
|
||||
assistantContent.Add(new { type = "text", text });
|
||||
finalText.Append(text);
|
||||
|
||||
// Review-Text per SSE an den Client streamen
|
||||
await TryWriteSse(response, "review_text", new
|
||||
await TryWriteSse(response, $"{agentType}_text", new
|
||||
{
|
||||
text,
|
||||
agentName = review.Agent?.Name ?? "Review-Agent",
|
||||
agentId = review.AgentId
|
||||
agentName,
|
||||
agentId
|
||||
}, cancellationToken);
|
||||
|
||||
// In DB speichern (als Review-Message markiert)
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId,
|
||||
Iteration = baseIterations + iterations,
|
||||
Role = "assistant",
|
||||
Type = "text",
|
||||
Content = $"[Review: {review.Agent?.Name ?? "Review-Agent"}] {text}"
|
||||
Content = $"[{tag}: {agentName}] {text}"
|
||||
});
|
||||
}
|
||||
else if (blockType == "tool_use")
|
||||
@@ -531,13 +589,12 @@ public class AnthropicService
|
||||
|
||||
assistantContent.Add(new { type = "tool_use", id = toolId, name = toolName, input = toolInput });
|
||||
|
||||
// Tool-Use per SSE
|
||||
await TryWriteSse(response, "review_tool_use", new
|
||||
await TryWriteSse(response, $"{agentType}_tool_use", new
|
||||
{
|
||||
toolName,
|
||||
toolId,
|
||||
input = toolInput.ToString(),
|
||||
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||
agentName
|
||||
}, cancellationToken);
|
||||
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
@@ -553,7 +610,7 @@ public class AnthropicService
|
||||
|
||||
string toolResult;
|
||||
bool isError = false;
|
||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, workspace, _memoryService, review.AgentId, cancellationToken); }
|
||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, cancellationToken); }
|
||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||
|
||||
const int MaxToolResultChars = 20_000;
|
||||
|
||||
+37
-1
@@ -142,6 +142,41 @@ public class ChatService
|
||||
}
|
||||
}
|
||||
|
||||
// Finalizer-Agent vorbereiten (falls konfiguriert)
|
||||
FinalizerContext finalizerContext = null;
|
||||
if (!string.IsNullOrEmpty(session.FinalizerAgentId))
|
||||
{
|
||||
var finalizerAgent = await _db.GetAgent(session.FinalizerAgentId);
|
||||
if (finalizerAgent != null)
|
||||
{
|
||||
var finalizerModelId = ResolveModelId(finalizerAgent.Model);
|
||||
var finalizerMarked = await _db.GetMarkedMessages(sessionId);
|
||||
var finalizerMemDir = _memoryService.GetAgentMemoryDir(finalizerAgent.AgentId);
|
||||
var finalizerSkills = await _db.GetAgentSkills(finalizerAgent.AgentId);
|
||||
var finalizerWorkspace = workspace;
|
||||
var finalizerSystemText = SystemPromptBuilder.Build(
|
||||
finalizerAgent, session, finalizerMarked, _memoryService, finalizerMemDir,
|
||||
finalizerSkills, _skillService, userPersona: user?.GetEffectivePersona(),
|
||||
resolvedWorkspace: finalizerWorkspace);
|
||||
|
||||
var finalizerPrompt = new List<object>();
|
||||
if (!string.IsNullOrWhiteSpace(finalizerSystemText))
|
||||
finalizerPrompt.Add(new { type = "text", text = finalizerSystemText });
|
||||
|
||||
finalizerContext = new FinalizerContext
|
||||
{
|
||||
ModelId = finalizerModelId,
|
||||
SystemPrompt = finalizerPrompt,
|
||||
AgentId = finalizerAgent.AgentId,
|
||||
Workspace = finalizerWorkspace,
|
||||
UseMemory = finalizerAgent.UseMemory,
|
||||
WebSearchVersion = finalizerAgent.WebSearchVersion,
|
||||
ToolChoice = finalizerAgent.ToolChoice ?? "any",
|
||||
Agent = finalizerAgent
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new ChatContext
|
||||
{
|
||||
ChainId = chainId,
|
||||
@@ -157,7 +192,8 @@ public class ChatService
|
||||
MaxIterations = maxIterations,
|
||||
Agent = agent,
|
||||
Session = session,
|
||||
Review = reviewContext
|
||||
Review = reviewContext,
|
||||
Finalizer = finalizerContext
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ public class DatabaseService
|
||||
TryAddColumn(conn, "sessions", "avatar_offset_y", "REAL NOT NULL DEFAULT 0");
|
||||
TryAddColumn(conn, "agents", "avatar_offset_x", "REAL NOT NULL DEFAULT 0");
|
||||
TryAddColumn(conn, "agents", "avatar_offset_y", "REAL NOT NULL DEFAULT 0");
|
||||
TryAddColumn(conn, "sessions", "finalizer_agent_id", "TEXT");
|
||||
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS agent_skills (
|
||||
@@ -373,7 +374,7 @@ public class DatabaseService
|
||||
|
||||
// ── Sessions ──
|
||||
|
||||
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3)
|
||||
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3, string finalizerAgentId = null)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
@@ -393,12 +394,13 @@ public class DatabaseService
|
||||
AvatarOffsetY = avatarOffsetY,
|
||||
ReviewAgentId = reviewAgentId,
|
||||
MaxReviewIterations = maxReviewIterations,
|
||||
FinalizerAgentId = finalizerAgentId,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, created_at, updated_at)
|
||||
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @CreatedAt, @UpdatedAt)
|
||||
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, finalizer_agent_id, created_at, updated_at)
|
||||
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @CreatedAt, @UpdatedAt)
|
||||
", session);
|
||||
return session;
|
||||
}
|
||||
@@ -446,6 +448,7 @@ public class DatabaseService
|
||||
s.AvatarOffsetY,
|
||||
s.ReviewAgentId,
|
||||
s.MaxReviewIterations,
|
||||
s.FinalizerAgentId,
|
||||
s.IsDeleted,
|
||||
s.CreatedAt,
|
||||
s.UpdatedAt,
|
||||
@@ -500,6 +503,7 @@ public class DatabaseService
|
||||
if (body.TryGetProperty("avatarOffsetY", out var aoy)) { sets.Add("avatar_offset_y = @avatarOffsetY"); p.Add("avatarOffsetY", aoy.GetDouble()); }
|
||||
if (body.TryGetProperty("reviewAgentId", out var ra)) { sets.Add("review_agent_id = @reviewAgentId"); p.Add("reviewAgentId", ra.ValueKind == System.Text.Json.JsonValueKind.Null ? null : ra.GetString()); }
|
||||
if (body.TryGetProperty("maxReviewIterations", out var mri)) { sets.Add("max_review_iterations = @maxReviewIterations"); p.Add("maxReviewIterations", mri.GetInt32()); }
|
||||
if (body.TryGetProperty("finalizerAgentId", out var fa)) { sets.Add("finalizer_agent_id = @finalizerAgentId"); p.Add("finalizerAgentId", fa.ValueKind == System.Text.Json.JsonValueKind.Null ? null : fa.GetString()); }
|
||||
|
||||
await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user