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
@@ -207,6 +207,27 @@ async function sendMessage() {
|
||||
currentToolBox = null;
|
||||
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':
|
||||
endStreamBubble();
|
||||
addErrorBubble(data.error, true);
|
||||
|
||||
@@ -21,6 +21,10 @@ registerPage('session', {
|
||||
const workspaceEl = document.getElementById('sessionWorkspace');
|
||||
const agentSelect = document.getElementById('agentSelect');
|
||||
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 saveBtn = document.getElementById('sessionSaveBtn');
|
||||
const cancelBtn = document.getElementById('sessionCancelBtn');
|
||||
@@ -83,33 +87,54 @@ registerPage('session', {
|
||||
agentSkillsMap[a.agentId] = await apiGetAgentSkills(a.agentId);
|
||||
}));
|
||||
|
||||
function renderAgentSelectCard(agent) {
|
||||
agentSelectCard.innerHTML = '';
|
||||
function renderAgentInCard(cardEl, agent, emptyIcon, emptyText) {
|
||||
cardEl.innerHTML = '';
|
||||
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;
|
||||
}
|
||||
const skills = agentSkillsMap[agent.agentId] || [];
|
||||
const inner = buildAgentCard(agent, skills);
|
||||
// Die inneren Elemente (card-image + card-body) in die Select-Card übernehmen
|
||||
while (inner.firstChild) agentSelectCard.appendChild(inner.firstChild);
|
||||
// Pencil-Icon hinzufügen
|
||||
agentSelectCard.insertAdjacentHTML('beforeend', '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>');
|
||||
while (inner.firstChild) cardEl.appendChild(inner.firstChild);
|
||||
cardEl.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) {
|
||||
agentSelect.value = session.agentId;
|
||||
renderAgentSelectCard(agentMap[session.agentId] || null);
|
||||
renderAgentInCard(agentSelectCard, agentMap[session.agentId] || null, 'fa-user-circle-o', 'Kein Agent ausgewählt');
|
||||
} 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', () => {
|
||||
openAgentPickerModal(agents, agentSkillsMap, 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(),
|
||||
image: _sessionAvatarEditor.getImage(),
|
||||
agentId: agentSelect.value || null,
|
||||
reviewAgentId: reviewAgentSelect.value || null,
|
||||
maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3,
|
||||
context: _sessionContextMde.value().trim() || null,
|
||||
workspace: workspaceEl.value.trim() || null,
|
||||
customCommands: commands,
|
||||
|
||||
@@ -13,6 +13,7 @@ function buildSessionCard(session, agent) {
|
||||
// Badges
|
||||
const badges = [];
|
||||
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.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();
|
||||
}
|
||||
|
||||
// ── 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 ──
|
||||
|
||||
let _streamDiv = null;
|
||||
|
||||
Reference in new issue
Block a user