feat: Review-Agent (Multi-Agent-Kommunikation)
- Session hat optionalen ReviewAgentId + MaxReviewIterations - Nach end_turn des Worker-Agents wird der Review-Agent aufgerufen - Review-Agent bekommt Worker-Konversation als Kontext + kann Tools nutzen - [APPROVED] in Review-Antwort → fertig, sonst Feedback an Worker - Worker korrigiert und wird erneut reviewed (max N Runden) - SSE-Events: review_start, review_text, review_tool_use, review_feedback, review_approved - Chat-UI: Review-Phase als eigene Bubble mit Header, Text, Status - Session-Page: Review-Agent-Picker (gleicher Agent-Card-Helper) - Dashboard: Review-Agent-Badge auf Session-Cards - DB-Migration: review_agent_id, max_review_iterations auf sessions
This commit is contained in:
1 parent
2598656f1c
commit
5fe3f8ac87
12 files changed
+619
-18
No files matched your search
@@ -85,7 +85,7 @@ public static class ChatEndpoint
|
|||||||
|
|
||||||
try
|
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: ctx.ToolChoice, cancellationToken: linkedCts.Token);
|
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: ctx.ToolChoice, review: ctx.Review, cancellationToken: linkedCts.Token);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -77,8 +77,10 @@ public static class SessionEndpoint
|
|||||||
var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null;
|
var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null;
|
||||||
var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null;
|
var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null;
|
||||||
var avatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0;
|
var avatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0;
|
||||||
|
var reviewAgentId = body.TryGetProperty("reviewAgentId", out var ra) ? ra.GetString() : null;
|
||||||
|
var maxReviewIterations = body.TryGetProperty("maxReviewIterations", out var mri) ? mri.GetInt32() : 3;
|
||||||
|
|
||||||
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale);
|
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, reviewAgentId, maxReviewIterations);
|
||||||
await ctx.Response.WriteAsJsonAsync(session);
|
await ctx.Response.WriteAsJsonAsync(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,20 @@ public class ChatContext
|
|||||||
public int MaxIterations { get; set; }
|
public int MaxIterations { get; set; }
|
||||||
public AgentRecord Agent { get; set; }
|
public AgentRecord Agent { get; set; }
|
||||||
public SessionRecord Session { get; set; }
|
public SessionRecord Session { get; set; }
|
||||||
|
|
||||||
|
// Review-Agent
|
||||||
|
public ReviewContext Review { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReviewContext
|
||||||
|
{
|
||||||
|
public string ModelId { get; set; }
|
||||||
|
public List<object> SystemPrompt { get; set; }
|
||||||
|
public string AgentId { get; set; }
|
||||||
|
public string Workspace { get; set; }
|
||||||
|
public bool UseMemory { get; set; }
|
||||||
|
public string WebSearchVersion { get; set; }
|
||||||
|
public string ToolChoice { get; set; }
|
||||||
|
public int MaxReviewIterations { get; set; }
|
||||||
|
public AgentRecord Agent { get; set; }
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@ public class SessionRecord
|
|||||||
public string TelegramChatId { get; set; }
|
public string TelegramChatId { get; set; }
|
||||||
public string TelegramAllowedChatIds { get; set; }
|
public string TelegramAllowedChatIds { get; set; }
|
||||||
public double AvatarScale { get; set; } = 1.0;
|
public double AvatarScale { get; set; } = 1.0;
|
||||||
|
public string ReviewAgentId { get; set; }
|
||||||
|
public int MaxReviewIterations { get; set; } = 3;
|
||||||
public string CreatedAt { get; set; }
|
public string CreatedAt { get; set; }
|
||||||
public string UpdatedAt { get; set; }
|
public string UpdatedAt { get; set; }
|
||||||
}
|
}
|
||||||
@@ -47,11 +47,13 @@ public class AnthropicService
|
|||||||
string webSearchVersion = null,
|
string webSearchVersion = null,
|
||||||
int maxIterations = 100,
|
int maxIterations = 100,
|
||||||
string toolChoice = "any",
|
string toolChoice = "any",
|
||||||
|
ReviewContext review = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
var usage = new TokenUsage();
|
var usage = new TokenUsage();
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
|
int reviewRound = 0;
|
||||||
string lastStopReason = "";
|
string lastStopReason = "";
|
||||||
|
|
||||||
var chain = new ChainRecord
|
var chain = new ChainRecord
|
||||||
@@ -175,6 +177,31 @@ public class AnthropicService
|
|||||||
|
|
||||||
if (stopReason != "tool_use")
|
if (stopReason != "tool_use")
|
||||||
{
|
{
|
||||||
|
// ── Review-Agent Phase ──
|
||||||
|
if (review != null && stopReason == "end_turn" && reviewRound < review.MaxReviewIterations)
|
||||||
|
{
|
||||||
|
var reviewResult = await RunReview(response, review, messages, chainId, workspace, sessionId, usage, iterations, reviewRound, cancellationToken);
|
||||||
|
iterations += reviewResult.ReviewIterationCount;
|
||||||
|
reviewRound++;
|
||||||
|
|
||||||
|
if (reviewResult.HasFeedback)
|
||||||
|
{
|
||||||
|
// Feedback als User-Message an Worker zurückgeben → nächste Iteration
|
||||||
|
messages.Add(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["role"] = "user",
|
||||||
|
["content"] = new List<object> { new { type = "text", text = reviewResult.Feedback } }
|
||||||
|
});
|
||||||
|
await _db.SaveMessage(new MessageRecord
|
||||||
|
{
|
||||||
|
ChainId = chainId, Iteration = iterations,
|
||||||
|
Role = "user", Type = "text",
|
||||||
|
Content = reviewResult.Feedback
|
||||||
|
});
|
||||||
|
continue; // → nächste Worker-Iteration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var finalStopReason = string.IsNullOrEmpty(stopReason) ? "unexpected_end" : stopReason;
|
var finalStopReason = string.IsNullOrEmpty(stopReason) ? "unexpected_end" : stopReason;
|
||||||
await FinalizeChain(chain, usage, iterations, finalStopReason, sw, streamErrorInfo);
|
await FinalizeChain(chain, usage, iterations, finalStopReason, sw, streamErrorInfo);
|
||||||
await TryWriteSse(response, "done", new
|
await TryWriteSse(response, "done", new
|
||||||
@@ -242,6 +269,331 @@ public class AnthropicService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Review-Agent Logik ──
|
||||||
|
|
||||||
|
private class ReviewResult
|
||||||
|
{
|
||||||
|
public bool HasFeedback { get; set; }
|
||||||
|
public string Feedback { get; set; }
|
||||||
|
public int ReviewIterationCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Führt den Review-Agent aus. Der Review-Agent bekommt die gesamte Konversation
|
||||||
|
/// und kann Tools nutzen (z.B. read_file um die Arbeit zu prüfen).
|
||||||
|
/// Antwortet er mit [APPROVED] → fertig. Sonst → Feedback wird an den Worker zurückgegeben.
|
||||||
|
/// reviewRound = aktuelle Runde (0-basiert), wird vom Caller hochgezählt.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<ReviewResult> RunReview(
|
||||||
|
HttpResponse response,
|
||||||
|
ReviewContext review,
|
||||||
|
List<object> workerMessages,
|
||||||
|
string chainId,
|
||||||
|
string workspace,
|
||||||
|
string sessionId,
|
||||||
|
TokenUsage usage,
|
||||||
|
int baseIterations,
|
||||||
|
int reviewRound,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// SSE: Review-Phase Start
|
||||||
|
await TryWriteSse(response, "review_start", new
|
||||||
|
{
|
||||||
|
round = reviewRound + 1,
|
||||||
|
maxRounds = review.MaxReviewIterations,
|
||||||
|
agentId = review.AgentId,
|
||||||
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// Review-Agent bekommt eigene Message-History
|
||||||
|
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 result = new ReviewResult { ReviewIterationCount = reviewIterations };
|
||||||
|
|
||||||
|
// Prüfe ob [APPROVED]
|
||||||
|
if (!string.IsNullOrEmpty(reviewText) && reviewText.Contains("[APPROVED]"))
|
||||||
|
{
|
||||||
|
await TryWriteSse(response, "review_approved", new
|
||||||
|
{
|
||||||
|
round = reviewRound + 1,
|
||||||
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
|
}, cancellationToken);
|
||||||
|
result.HasFeedback = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feedback – an Worker zurückspielen
|
||||||
|
result.HasFeedback = true;
|
||||||
|
result.Feedback = $"[Review-Feedback von {review.Agent?.Name ?? "Review-Agent"} (Runde {reviewRound + 1}/{review.MaxReviewIterations})]:\n{reviewText}";
|
||||||
|
|
||||||
|
await TryWriteSse(response, "review_feedback", new
|
||||||
|
{
|
||||||
|
round = reviewRound + 1,
|
||||||
|
maxRounds = review.MaxReviewIterations,
|
||||||
|
feedback = reviewText,
|
||||||
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Baut die Messages für den Review-Agent auf.
|
||||||
|
/// Der Review-Agent bekommt die komplette Worker-Konversation als eine User-Message.
|
||||||
|
/// </summary>
|
||||||
|
private List<object> BuildReviewMessages(List<object> workerMessages)
|
||||||
|
{
|
||||||
|
// Worker-Konversation zusammenfassen
|
||||||
|
var summary = new StringBuilder();
|
||||||
|
summary.AppendLine("Hier ist die bisherige Konversation zwischen User und Worker-Agent. Bitte überprüfe ob die Aufgabe korrekt erledigt wurde:\n");
|
||||||
|
summary.AppendLine("---");
|
||||||
|
|
||||||
|
foreach (var msg in workerMessages)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(msg);
|
||||||
|
var el = JsonSerializer.Deserialize<JsonElement>(json);
|
||||||
|
|
||||||
|
if (!el.TryGetProperty("role", out var role)) continue;
|
||||||
|
var roleStr = role.GetString() ?? "";
|
||||||
|
|
||||||
|
if (el.TryGetProperty("content", out var content))
|
||||||
|
{
|
||||||
|
if (content.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
summary.AppendLine($"\n**{roleStr}:** {content.GetString()}");
|
||||||
|
}
|
||||||
|
else if (content.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var block in content.EnumerateArray())
|
||||||
|
{
|
||||||
|
var blockType = block.TryGetProperty("type", out var bt) ? bt.GetString() ?? "" : "";
|
||||||
|
if (blockType == "text")
|
||||||
|
{
|
||||||
|
var text = block.TryGetProperty("text", out var t) ? t.GetString() ?? "" : "";
|
||||||
|
summary.AppendLine($"\n**{roleStr}:** {text}");
|
||||||
|
}
|
||||||
|
else if (blockType == "tool_use")
|
||||||
|
{
|
||||||
|
var toolName = block.TryGetProperty("name", out var tn) ? tn.GetString() ?? "" : "";
|
||||||
|
var toolInput = block.TryGetProperty("input", out var ti) ? ti.ToString() : "{}";
|
||||||
|
summary.AppendLine($"\n**{roleStr} [Tool: {toolName}]:** {toolInput}");
|
||||||
|
}
|
||||||
|
else if (blockType == "tool_result")
|
||||||
|
{
|
||||||
|
var resultContent = block.TryGetProperty("content", out var rc) ? rc.ToString() : "";
|
||||||
|
// Tool-Results kürzen für Review-Kontext
|
||||||
|
if (resultContent.Length > 2000)
|
||||||
|
resultContent = resultContent[..2000] + "\n[... gekürzt]";
|
||||||
|
summary.AppendLine($"\n**{roleStr} [Tool-Result]:** {resultContent}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
summary.AppendLine("\n---");
|
||||||
|
summary.AppendLine("\nBitte überprüfe ob die Aufgabe korrekt erledigt wurde. Du kannst dazu auch Tools verwenden (z.B. Dateien lesen). Antworte mit exakt [APPROVED] wenn alles korrekt ist, oder beschreibe was falsch ist und korrigiert werden muss.");
|
||||||
|
|
||||||
|
return new List<object>
|
||||||
|
{
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["role"] = "user",
|
||||||
|
["content"] = new List<object> { new { type = "text", text = summary.ToString() } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(string text, int iterations)> RunReviewAgentChat(
|
||||||
|
HttpResponse response,
|
||||||
|
ReviewContext review,
|
||||||
|
List<object> messages,
|
||||||
|
string chainId,
|
||||||
|
string workspace,
|
||||||
|
string sessionId,
|
||||||
|
TokenUsage usage,
|
||||||
|
int baseIterations,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int iterations = 0;
|
||||||
|
var finalText = new StringBuilder();
|
||||||
|
int maxToolIterations = 20; // Review-Agent bekommt max 20 Tool-Iterationen
|
||||||
|
|
||||||
|
while (iterations < maxToolIterations)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
iterations++;
|
||||||
|
|
||||||
|
var payload = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["model"] = review.ModelId,
|
||||||
|
["max_tokens"] = 8192,
|
||||||
|
["messages"] = messages,
|
||||||
|
["stream"] = false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (review.SystemPrompt != null && review.SystemPrompt.Count > 0)
|
||||||
|
payload["system"] = review.SystemPrompt;
|
||||||
|
|
||||||
|
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
||||||
|
var includeMemory = review.UseMemory && _memoryService != null && !string.IsNullOrEmpty(review.AgentId);
|
||||||
|
var hasTools = hasWorkspace || includeMemory;
|
||||||
|
var hasWebSearch = !string.IsNullOrEmpty(review.WebSearchVersion);
|
||||||
|
|
||||||
|
if (hasTools || hasWebSearch)
|
||||||
|
{
|
||||||
|
var tools = new List<object>();
|
||||||
|
if (hasTools)
|
||||||
|
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory));
|
||||||
|
if (hasWebSearch)
|
||||||
|
tools.Add(new { type = review.WebSearchVersion, name = "web_search", max_uses = 5 });
|
||||||
|
payload["tools"] = tools;
|
||||||
|
var effectiveChoice = (review.ToolChoice == "any" && iterations == 1) ? "any" : "auto";
|
||||||
|
payload["tool_choice"] = new { type = effectiveChoice };
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = CreateClient();
|
||||||
|
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
LogPayload(json, sessionId, chainId, baseIterations + iterations);
|
||||||
|
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var httpResp = await client.SendAsync(
|
||||||
|
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
var respBody = await httpResp.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!httpResp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return ($"Review-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();
|
||||||
|
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
|
||||||
|
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
|
||||||
|
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
|
||||||
|
var content = resp.GetProperty("content");
|
||||||
|
|
||||||
|
var assistantContent = new List<object>();
|
||||||
|
var toolResults = new List<object>();
|
||||||
|
|
||||||
|
foreach (var block in content.EnumerateArray())
|
||||||
|
{
|
||||||
|
var blockType = block.GetProperty("type").GetString() ?? "";
|
||||||
|
|
||||||
|
if (blockType == "text")
|
||||||
|
{
|
||||||
|
var text = block.GetProperty("text").GetString() ?? "";
|
||||||
|
assistantContent.Add(new { type = "text", text });
|
||||||
|
finalText.Append(text);
|
||||||
|
|
||||||
|
// Review-Text per SSE an den Client streamen
|
||||||
|
await TryWriteSse(response, "review_text", new
|
||||||
|
{
|
||||||
|
text,
|
||||||
|
agentName = review.Agent?.Name ?? "Review-Agent",
|
||||||
|
agentId = review.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}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (blockType == "tool_use")
|
||||||
|
{
|
||||||
|
var toolId = block.GetProperty("id").GetString() ?? "";
|
||||||
|
var toolName = block.GetProperty("name").GetString() ?? "";
|
||||||
|
var toolInput = block.GetProperty("input");
|
||||||
|
|
||||||
|
assistantContent.Add(new { type = "tool_use", id = toolId, name = toolName, input = toolInput });
|
||||||
|
|
||||||
|
// Tool-Use per SSE
|
||||||
|
await TryWriteSse(response, "review_tool_use", new
|
||||||
|
{
|
||||||
|
toolName,
|
||||||
|
toolId,
|
||||||
|
input = toolInput.ToString(),
|
||||||
|
agentName = review.Agent?.Name ?? "Review-Agent"
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
await _db.SaveMessage(new MessageRecord
|
||||||
|
{
|
||||||
|
ChainId = chainId,
|
||||||
|
Iteration = baseIterations + iterations,
|
||||||
|
Role = "assistant",
|
||||||
|
Type = "tool_use",
|
||||||
|
Content = toolInput.GetRawText(),
|
||||||
|
ToolName = toolName,
|
||||||
|
ToolCallId = toolId
|
||||||
|
});
|
||||||
|
|
||||||
|
string toolResult;
|
||||||
|
bool isError = false;
|
||||||
|
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, workspace, _memoryService, review.AgentId, cancellationToken); }
|
||||||
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
|
const int MaxToolResultChars = 20_000;
|
||||||
|
var truncatedResult = toolResult.Length > MaxToolResultChars
|
||||||
|
? toolResult[..MaxToolResultChars] + $"\n\n[... gekürzt, {toolResult.Length - MaxToolResultChars} weitere Zeichen ...]"
|
||||||
|
: toolResult;
|
||||||
|
|
||||||
|
toolResults.Add(new
|
||||||
|
{
|
||||||
|
type = "tool_result",
|
||||||
|
tool_use_id = toolId,
|
||||||
|
content = truncatedResult,
|
||||||
|
is_error = isError
|
||||||
|
});
|
||||||
|
|
||||||
|
await _db.SaveMessage(new MessageRecord
|
||||||
|
{
|
||||||
|
ChainId = chainId,
|
||||||
|
Iteration = baseIterations + iterations,
|
||||||
|
Role = "user",
|
||||||
|
Type = "tool_result",
|
||||||
|
Content = toolResult,
|
||||||
|
ToolName = toolName,
|
||||||
|
ToolCallId = toolId,
|
||||||
|
IsError = isError
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.Add(new Dictionary<string, object> { ["role"] = "assistant", ["content"] = assistantContent });
|
||||||
|
|
||||||
|
if (stopReason == "end_turn" || stopReason == "stop_sequence" || toolResults.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
messages.Add(new Dictionary<string, object> { ["role"] = "user", ["content"] = toolResults });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (finalText.ToString(), iterations);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Non-Streaming Chat für Telegram. Gibt den finalen Antworttext zurück.
|
/// Non-Streaming Chat für Telegram. Gibt den finalen Antworttext zurück.
|
||||||
/// onIteration wird nach jeder Iteration aufgerufen (z.B. für Typing-Indicator).
|
/// onIteration wird nach jeder Iteration aufgerufen (z.B. für Typing-Indicator).
|
||||||
|
|||||||
+38
-1
@@ -106,6 +106,42 @@ public class ChatService
|
|||||||
if (!string.IsNullOrWhiteSpace(systemPromptText))
|
if (!string.IsNullOrWhiteSpace(systemPromptText))
|
||||||
systemPrompt.Add(new { type = "text", text = systemPromptText });
|
systemPrompt.Add(new { type = "text", text = systemPromptText });
|
||||||
|
|
||||||
|
// Review-Agent vorbereiten (falls konfiguriert)
|
||||||
|
ReviewContext reviewContext = null;
|
||||||
|
if (!string.IsNullOrEmpty(session.ReviewAgentId))
|
||||||
|
{
|
||||||
|
var reviewAgent = await _db.GetAgent(session.ReviewAgentId);
|
||||||
|
if (reviewAgent != null)
|
||||||
|
{
|
||||||
|
var reviewModelId = ResolveModelId(reviewAgent.Model);
|
||||||
|
var reviewMarked = await _db.GetMarkedMessages(sessionId);
|
||||||
|
var reviewMemDir = _memoryService.GetAgentMemoryDir(reviewAgent.AgentId);
|
||||||
|
var reviewSkills = await _db.GetAgentSkills(reviewAgent.AgentId);
|
||||||
|
var reviewWorkspace = workspace; // gleicher Workspace wie Worker
|
||||||
|
var reviewSystemText = SystemPromptBuilder.Build(
|
||||||
|
reviewAgent, session, reviewMarked, _memoryService, reviewMemDir,
|
||||||
|
reviewSkills, _skillService, userPersona: user?.GetEffectivePersona(),
|
||||||
|
resolvedWorkspace: reviewWorkspace);
|
||||||
|
|
||||||
|
var reviewPrompt = new List<object>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(reviewSystemText))
|
||||||
|
reviewPrompt.Add(new { type = "text", text = reviewSystemText });
|
||||||
|
|
||||||
|
reviewContext = new ReviewContext
|
||||||
|
{
|
||||||
|
ModelId = reviewModelId,
|
||||||
|
SystemPrompt = reviewPrompt,
|
||||||
|
AgentId = reviewAgent.AgentId,
|
||||||
|
Workspace = reviewWorkspace,
|
||||||
|
UseMemory = reviewAgent.UseMemory,
|
||||||
|
WebSearchVersion = reviewAgent.WebSearchVersion,
|
||||||
|
ToolChoice = reviewAgent.ToolChoice ?? "any",
|
||||||
|
MaxReviewIterations = session.MaxReviewIterations > 0 ? session.MaxReviewIterations : 3,
|
||||||
|
Agent = reviewAgent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new ChatContext
|
return new ChatContext
|
||||||
{
|
{
|
||||||
ChainId = chainId,
|
ChainId = chainId,
|
||||||
@@ -120,7 +156,8 @@ public class ChatService
|
|||||||
WebSearchVersion = agent.WebSearchVersion,
|
WebSearchVersion = agent.WebSearchVersion,
|
||||||
MaxIterations = maxIterations,
|
MaxIterations = maxIterations,
|
||||||
Agent = agent,
|
Agent = agent,
|
||||||
Session = session
|
Session = session,
|
||||||
|
Review = reviewContext
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ public class DatabaseService
|
|||||||
TryAddColumn(conn, "sessions", "avatar_scale", "REAL NOT NULL DEFAULT 1.0");
|
TryAddColumn(conn, "sessions", "avatar_scale", "REAL NOT NULL DEFAULT 1.0");
|
||||||
TryAddColumn(conn, "agents", "avatar_scale", "REAL NOT NULL DEFAULT 1.0");
|
TryAddColumn(conn, "agents", "avatar_scale", "REAL NOT NULL DEFAULT 1.0");
|
||||||
TryAddColumn(conn, "cronjobs", "notify_session_id", "TEXT");
|
TryAddColumn(conn, "cronjobs", "notify_session_id", "TEXT");
|
||||||
|
TryAddColumn(conn, "sessions", "review_agent_id", "TEXT");
|
||||||
|
TryAddColumn(conn, "sessions", "max_review_iterations", "INTEGER NOT NULL DEFAULT 3");
|
||||||
|
|
||||||
conn.Execute(@"
|
conn.Execute(@"
|
||||||
CREATE TABLE IF NOT EXISTS agent_skills (
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
||||||
@@ -367,7 +369,7 @@ public class DatabaseService
|
|||||||
|
|
||||||
// ── Sessions ──
|
// ── 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)
|
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, string reviewAgentId = null, int maxReviewIterations = 3)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
var now = DateTime.UtcNow.ToString("o");
|
var now = DateTime.UtcNow.ToString("o");
|
||||||
@@ -383,12 +385,14 @@ public class DatabaseService
|
|||||||
Context = context,
|
Context = context,
|
||||||
CustomCommands = customCommands,
|
CustomCommands = customCommands,
|
||||||
AvatarScale = avatarScale,
|
AvatarScale = avatarScale,
|
||||||
|
ReviewAgentId = reviewAgentId,
|
||||||
|
MaxReviewIterations = maxReviewIterations,
|
||||||
CreatedAt = now,
|
CreatedAt = now,
|
||||||
UpdatedAt = now
|
UpdatedAt = now
|
||||||
};
|
};
|
||||||
await conn.ExecuteAsync(@"
|
await conn.ExecuteAsync(@"
|
||||||
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, created_at, updated_at)
|
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, review_agent_id, max_review_iterations, created_at, updated_at)
|
||||||
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @CreatedAt, @UpdatedAt)
|
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @ReviewAgentId, @MaxReviewIterations, @CreatedAt, @UpdatedAt)
|
||||||
", session);
|
", session);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
@@ -432,6 +436,8 @@ public class DatabaseService
|
|||||||
s.Context,
|
s.Context,
|
||||||
s.CustomCommands,
|
s.CustomCommands,
|
||||||
s.AvatarScale,
|
s.AvatarScale,
|
||||||
|
s.ReviewAgentId,
|
||||||
|
s.MaxReviewIterations,
|
||||||
s.IsDeleted,
|
s.IsDeleted,
|
||||||
s.CreatedAt,
|
s.CreatedAt,
|
||||||
s.UpdatedAt,
|
s.UpdatedAt,
|
||||||
@@ -482,6 +488,8 @@ public class DatabaseService
|
|||||||
if (body.TryGetProperty("context", out var ctx)) { sets.Add("context = @context"); p.Add("context", ctx.GetString()); }
|
if (body.TryGetProperty("context", out var ctx)) { sets.Add("context = @context"); p.Add("context", ctx.GetString()); }
|
||||||
if (body.TryGetProperty("customCommands", out var cc)) { sets.Add("custom_commands = @customCommands"); p.Add("customCommands", cc.ValueKind == System.Text.Json.JsonValueKind.Null ? null : cc.GetRawText()); }
|
if (body.TryGetProperty("customCommands", out var cc)) { sets.Add("custom_commands = @customCommands"); p.Add("customCommands", cc.ValueKind == System.Text.Json.JsonValueKind.Null ? null : cc.GetRawText()); }
|
||||||
if (body.TryGetProperty("avatarScale", out var asc)) { sets.Add("avatar_scale = @avatarScale"); p.Add("avatarScale", asc.GetDouble()); }
|
if (body.TryGetProperty("avatarScale", out var asc)) { sets.Add("avatar_scale = @avatarScale"); p.Add("avatarScale", asc.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()); }
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -973,3 +973,74 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: var(--page-padding);
|
padding: var(--page-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Review Bubbles */
|
||||||
|
|
||||||
|
.review-bubble {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: var(--f1-bg);
|
||||||
|
border: var(--border-width) solid var(--f1-border);
|
||||||
|
border-left: 3px solid var(--secondary);
|
||||||
|
max-width: 80%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-round {
|
||||||
|
opacity: var(--semi-transparent);
|
||||||
|
font-size: var(--font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-content {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-tool-use {
|
||||||
|
font-size: var(--font-size);
|
||||||
|
opacity: var(--semi-transparent);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
padding-top: var(--badge-spacing);
|
||||||
|
border-top: var(--border-width) solid var(--f2-border);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-approved {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-rejected {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Review Iterations Row */
|
||||||
|
|
||||||
|
.review-iterations-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--box-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-iterations-row label {
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-iterations-row input[type="number"] {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
@@ -108,6 +108,15 @@
|
|||||||
<div class="agent-select-empty"><i class="fa fa-user-circle-o"></i> Kein Agent ausgewählt</div>
|
<div class="agent-select-empty"><i class="fa fa-user-circle-o"></i> Kein Agent ausgewählt</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="agentSelect" value="">
|
<input type="hidden" id="agentSelect" value="">
|
||||||
|
<label>Review-Agent <small>(optional)</small></label>
|
||||||
|
<div id="reviewAgentSelectCard" class="agent-select-card">
|
||||||
|
<div class="agent-select-empty"><i class="fa fa-check-circle-o"></i> Kein Review-Agent</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="reviewAgentSelect" value="">
|
||||||
|
<div class="review-iterations-row" id="reviewIterationsRow" hidden>
|
||||||
|
<label>Max. Review-Runden</label>
|
||||||
|
<input type="number" id="maxReviewIterations" min="1" max="10" value="3">
|
||||||
|
</div>
|
||||||
<div class="more">
|
<div class="more">
|
||||||
<a>Weitere Einstellungen</a>
|
<a>Weitere Einstellungen</a>
|
||||||
<div hidden>
|
<div hidden>
|
||||||
|
|||||||
@@ -207,6 +207,27 @@ async function sendMessage() {
|
|||||||
currentToolBox = null;
|
currentToolBox = null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'review_start':
|
||||||
|
endStreamBubble();
|
||||||
|
addReviewBubble('start', data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'review_text':
|
||||||
|
appendToReviewText(data.text);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'review_tool_use':
|
||||||
|
addReviewToolUse(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'review_feedback':
|
||||||
|
finalizeReviewBubble('feedback', data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'review_approved':
|
||||||
|
finalizeReviewBubble('approved', data);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
endStreamBubble();
|
endStreamBubble();
|
||||||
addErrorBubble(data.error, true);
|
addErrorBubble(data.error, true);
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ registerPage('session', {
|
|||||||
const workspaceEl = document.getElementById('sessionWorkspace');
|
const workspaceEl = document.getElementById('sessionWorkspace');
|
||||||
const agentSelect = document.getElementById('agentSelect');
|
const agentSelect = document.getElementById('agentSelect');
|
||||||
const agentSelectCard = document.getElementById('agentSelectCard');
|
const agentSelectCard = document.getElementById('agentSelectCard');
|
||||||
|
const reviewAgentSelect = document.getElementById('reviewAgentSelect');
|
||||||
|
const reviewAgentSelectCard = document.getElementById('reviewAgentSelectCard');
|
||||||
|
const reviewIterationsRow = document.getElementById('reviewIterationsRow');
|
||||||
|
const maxReviewIterationsEl = document.getElementById('maxReviewIterations');
|
||||||
const imagePreview = document.getElementById('sessionImagePreview');
|
const imagePreview = document.getElementById('sessionImagePreview');
|
||||||
const saveBtn = document.getElementById('sessionSaveBtn');
|
const saveBtn = document.getElementById('sessionSaveBtn');
|
||||||
const cancelBtn = document.getElementById('sessionCancelBtn');
|
const cancelBtn = document.getElementById('sessionCancelBtn');
|
||||||
@@ -83,33 +87,54 @@ registerPage('session', {
|
|||||||
agentSkillsMap[a.agentId] = await apiGetAgentSkills(a.agentId);
|
agentSkillsMap[a.agentId] = await apiGetAgentSkills(a.agentId);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function renderAgentSelectCard(agent) {
|
function renderAgentInCard(cardEl, agent, emptyIcon, emptyText) {
|
||||||
agentSelectCard.innerHTML = '';
|
cardEl.innerHTML = '';
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
agentSelectCard.innerHTML = '<div class="agent-select-empty"><i class="fa fa-user-circle-o"></i> Kein Agent ausgewählt</div>';
|
cardEl.innerHTML = '<div class="agent-select-empty"><i class="fa ' + emptyIcon + '"></i> ' + emptyText + '</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const skills = agentSkillsMap[agent.agentId] || [];
|
const skills = agentSkillsMap[agent.agentId] || [];
|
||||||
const inner = buildAgentCard(agent, skills);
|
const inner = buildAgentCard(agent, skills);
|
||||||
// Die inneren Elemente (card-image + card-body) in die Select-Card übernehmen
|
while (inner.firstChild) cardEl.appendChild(inner.firstChild);
|
||||||
while (inner.firstChild) agentSelectCard.appendChild(inner.firstChild);
|
cardEl.insertAdjacentHTML('beforeend', '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>');
|
||||||
// Pencil-Icon hinzufügen
|
|
||||||
agentSelectCard.insertAdjacentHTML('beforeend', '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initiale Anzeige
|
function updateReviewVisibility() {
|
||||||
|
reviewIterationsRow.hidden = !reviewAgentSelect.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiale Anzeige – Agent
|
||||||
if (session && session.agentId) {
|
if (session && session.agentId) {
|
||||||
agentSelect.value = session.agentId;
|
agentSelect.value = session.agentId;
|
||||||
renderAgentSelectCard(agentMap[session.agentId] || null);
|
renderAgentInCard(agentSelectCard, agentMap[session.agentId] || null, 'fa-user-circle-o', 'Kein Agent ausgewählt');
|
||||||
} else {
|
} else {
|
||||||
renderAgentSelectCard(null);
|
renderAgentInCard(agentSelectCard, null, 'fa-user-circle-o', 'Kein Agent ausgewählt');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Klick auf Card → Modal öffnen
|
// Initiale Anzeige – Review-Agent
|
||||||
|
if (session && session.reviewAgentId) {
|
||||||
|
reviewAgentSelect.value = session.reviewAgentId;
|
||||||
|
renderAgentInCard(reviewAgentSelectCard, agentMap[session.reviewAgentId] || null, 'fa-check-circle-o', 'Kein Review-Agent');
|
||||||
|
maxReviewIterationsEl.value = session.maxReviewIterations || 3;
|
||||||
|
} else {
|
||||||
|
renderAgentInCard(reviewAgentSelectCard, null, 'fa-check-circle-o', 'Kein Review-Agent');
|
||||||
|
}
|
||||||
|
updateReviewVisibility();
|
||||||
|
|
||||||
|
// Klick auf Agent-Card → Modal öffnen
|
||||||
agentSelectCard.addEventListener('click', () => {
|
agentSelectCard.addEventListener('click', () => {
|
||||||
openAgentPickerModal(agents, agentSkillsMap, agentSelect.value, (selectedId) => {
|
openAgentPickerModal(agents, agentSkillsMap, agentSelect.value, (selectedId) => {
|
||||||
agentSelect.value = selectedId || '';
|
agentSelect.value = selectedId || '';
|
||||||
renderAgentSelectCard(selectedId ? agentMap[selectedId] : null);
|
renderAgentInCard(agentSelectCard, selectedId ? agentMap[selectedId] : null, 'fa-user-circle-o', 'Kein Agent ausgewählt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick auf Review-Agent-Card → Modal öffnen
|
||||||
|
reviewAgentSelectCard.addEventListener('click', () => {
|
||||||
|
openAgentPickerModal(agents, agentSkillsMap, reviewAgentSelect.value, (selectedId) => {
|
||||||
|
reviewAgentSelect.value = selectedId || '';
|
||||||
|
renderAgentInCard(reviewAgentSelectCard, selectedId ? agentMap[selectedId] : null, 'fa-check-circle-o', 'Kein Review-Agent');
|
||||||
|
updateReviewVisibility();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,6 +185,8 @@ registerPage('session', {
|
|||||||
description: descEl.value.trim(),
|
description: descEl.value.trim(),
|
||||||
image: _sessionAvatarEditor.getImage(),
|
image: _sessionAvatarEditor.getImage(),
|
||||||
agentId: agentSelect.value || null,
|
agentId: agentSelect.value || null,
|
||||||
|
reviewAgentId: reviewAgentSelect.value || null,
|
||||||
|
maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3,
|
||||||
context: _sessionContextMde.value().trim() || null,
|
context: _sessionContextMde.value().trim() || null,
|
||||||
workspace: workspaceEl.value.trim() || null,
|
workspace: workspaceEl.value.trim() || null,
|
||||||
customCommands: commands,
|
customCommands: commands,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function buildSessionCard(session, agent) {
|
|||||||
// Badges
|
// Badges
|
||||||
const badges = [];
|
const badges = [];
|
||||||
if (agent) badges.push(`<span class="badge">${esc(agent.name)}</span>`);
|
if (agent) badges.push(`<span class="badge">${esc(agent.name)}</span>`);
|
||||||
|
if (session.reviewAgentId) badges.push(`<span class="badge badge-feature" title="Review-Agent"><i class="fa fa-check-circle-o"></i></span>`);
|
||||||
if (session.context) badges.push(`<span class="badge badge-feature" title="${esc(session.context)}"><i class="fa fa-file-text-o"></i></span>`);
|
if (session.context) badges.push(`<span class="badge badge-feature" title="${esc(session.context)}"><i class="fa fa-file-text-o"></i></span>`);
|
||||||
if (session.telegramConfigured) badges.push(`<span class="badge ${session.telegramLinked ? 'badge-feature' : 'badge-off'}" title="Telegram ${session.telegramLinked ? 'verbunden' : 'konfiguriert'}"><i class="fa fa-paper-plane"></i></span>`);
|
if (session.telegramConfigured) badges.push(`<span class="badge ${session.telegramLinked ? 'badge-feature' : 'badge-off'}" title="Telegram ${session.telegramLinked ? 'verbunden' : 'konfiguriert'}"><i class="fa fa-paper-plane"></i></span>`);
|
||||||
|
|
||||||
@@ -637,6 +638,61 @@ function addWarningBubble(text, streamed = false) {
|
|||||||
scrollDown();
|
scrollDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Review Bubbles ──
|
||||||
|
|
||||||
|
let _reviewBubble = null;
|
||||||
|
let _reviewTextEl = null;
|
||||||
|
let _reviewText = '';
|
||||||
|
|
||||||
|
function addReviewBubble(type, data) {
|
||||||
|
_reviewBubble = document.createElement('div');
|
||||||
|
_reviewBubble.className = 'msg review-bubble streamed';
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'review-header';
|
||||||
|
header.innerHTML = '<i class="fa fa-check-circle-o"></i> <strong>' + esc(data.agentName || 'Review-Agent') + '</strong> <span class="review-round">Runde ' + data.round + '/' + data.maxRounds + '</span>';
|
||||||
|
_reviewBubble.appendChild(header);
|
||||||
|
_reviewTextEl = document.createElement('div');
|
||||||
|
_reviewTextEl.className = 'review-content markdown';
|
||||||
|
_reviewBubble.appendChild(_reviewTextEl);
|
||||||
|
_reviewText = '';
|
||||||
|
chatEl().appendChild(_reviewBubble);
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendToReviewText(text) {
|
||||||
|
if (!_reviewBubble) return;
|
||||||
|
_reviewText += text;
|
||||||
|
_reviewTextEl.innerHTML = renderMarkdown(_reviewText);
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addReviewToolUse(data) {
|
||||||
|
if (!_reviewBubble) return;
|
||||||
|
const toolDiv = document.createElement('div');
|
||||||
|
toolDiv.className = 'review-tool-use';
|
||||||
|
toolDiv.innerHTML = '<i class="fa fa-wrench"></i> <span class="tool-name">' + esc(data.toolName) + '</span>';
|
||||||
|
_reviewBubble.appendChild(toolDiv);
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeReviewBubble(type, data) {
|
||||||
|
if (!_reviewBubble) return;
|
||||||
|
const status = document.createElement('div');
|
||||||
|
status.className = 'review-status';
|
||||||
|
if (type === 'approved') {
|
||||||
|
status.className += ' review-approved';
|
||||||
|
status.innerHTML = '<i class="fa fa-check"></i> Approved';
|
||||||
|
} else {
|
||||||
|
status.className += ' review-rejected';
|
||||||
|
status.innerHTML = '<i class="fa fa-repeat"></i> Feedback – Worker korrigiert...';
|
||||||
|
}
|
||||||
|
_reviewBubble.appendChild(status);
|
||||||
|
_reviewBubble = null;
|
||||||
|
_reviewTextEl = null;
|
||||||
|
_reviewText = '';
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Streaming Bubble ──
|
// ── Streaming Bubble ──
|
||||||
|
|
||||||
let _streamDiv = null;
|
let _streamDiv = null;
|
||||||
|
|||||||
Reference in new issue
Block a user