Security Fixes: - Fix XSS vulnerability in orchestrator.html (escapeHtml für user input) - Verbesserte Error-Handling: 4 bare except clauses mit spezifischen Exception-Typen Code Quality: - Logging für alle Exception-Handler hinzugefügt - Timeout für Agent-Tasks von 300s auf 600s erhöht (10 Min) - Bessere Kommentare für Exception-Handling Performance: - Wissensdatenbank aus Systemprompt entfernt - Agents nutzen @READ_KNOWLEDGE für on-demand Zugriff - Reduziert Prompt-Größe um ~15KB pro Task UI Improvements (aus vorherigem Work): - Tasks: Auto-Refresh Info statt Toggle - Tasks: Status-Anzeigen statt manuelle Buttons - Konsistentes Auto-Refresh (15s) wenn Tasks aktiv
230 lines
8.6 KiB
HTML
230 lines
8.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Orchestrator{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<h1>Master-Orchestrator</h1>
|
|
<p>Delegiert automatisch an den passenden Agenten</p>
|
|
</div>
|
|
|
|
{% if knowledge_loaded %}
|
|
<div class="alert alert-success mb-4">
|
|
<strong>Wissensdatenbank geladen</strong> · Agenten-Prompts initialisiert
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="row g-4">
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Task Distribution -->
|
|
<div class="card mb-3">
|
|
<div class="card-header bg-info">
|
|
<h5 class="mb-0">📋 Tasks verteilen</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Aufgaben (eine pro Zeile)</label>
|
|
<textarea class="form-control" id="todoPrompt" rows="4" placeholder="Recherche Location Catering planen Pressemitteilung schreiben"></textarea>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Agenten auswählen</label>
|
|
<div id="agentCheckboxes">
|
|
{% for key, agent in agents.items() %}
|
|
<div class="form-check">
|
|
<input class="form-check-input agent-checkbox" type="checkbox" value="{{ key }}" id="agent_{{ key }}" checked>
|
|
<label class="form-check-label" for="agent_{{ key }}">{{ agent.name }}</label>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-info w-100" onclick="distributeTodos()">Tasks parallel ausführen</button>
|
|
<div id="todoStatus" class="form-text mt-2"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header bg-dark">
|
|
<h5 class="mb-0">Prompt eingeben</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="promptForm" method="POST" action="/orchestrator">
|
|
<div class="mb-3">
|
|
<label for="prompt" class="form-label">Ihre Anfrage</label>
|
|
<textarea class="form-control" id="prompt" name="prompt" rows="5"
|
|
placeholder="Was soll erledigt werden?" required></textarea>
|
|
</div>
|
|
<button type="button" class="btn btn-primary w-100 mb-2" id="streamBtn"
|
|
onclick="sendPromptWithStream()">Live-Antwort anfordern</button>
|
|
<button type="submit" class="btn btn-secondary w-100">Klassisch senden</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header bg-secondary">
|
|
<h6 class="mb-0">Aktive Agenten</h6>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<ul class="list-group list-group-flush">
|
|
{% for key, agent in agents.items() %}
|
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
|
<span style="font-size:.85rem;">{{ agent.name }}</span>
|
|
<span class="badge bg-success" style="font-size:.65rem;">aktiv</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chat -->
|
|
<div class="col-lg-8">
|
|
<div class="card">
|
|
<div class="card-header bg-dark d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">Orchestrator-Chat</h5>
|
|
<small style="color:var(--text-muted);">Automatische Agenten-Delegation</small>
|
|
</div>
|
|
<div class="card-body chat-container" id="chatContainer">
|
|
{% if chat_history %}
|
|
{% for chat in chat_history %}
|
|
<div class="chat-message">
|
|
<div class="chat-timestamp">
|
|
{{ chat.timestamp }} ·
|
|
<span class="badge bg-primary" style="font-size:.65rem;">{{ chat.agent }}</span>
|
|
</div>
|
|
<div class="chat-prompt mt-1"><strong>Sie:</strong> {{ chat.user_prompt }}</div>
|
|
<div class="chat-response mt-1"><strong>Orchestrator:</strong> {{ chat.response }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-5" style="color:var(--text-muted);" id="emptyState">
|
|
<p style="font-size:2rem;margin-bottom:.5rem;">🤖</p>
|
|
<p>Noch keine Anfragen.<br>Der Orchestrator delegiert automatisch an den passenden Agenten.</p>
|
|
<hr>
|
|
<p style="font-size:.8rem;">
|
|
✓ Immer zuerst delegieren · ✓ Neuen Agenten erstellen falls nötig · ✓ Niemals selbst ausführen
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function sendPromptWithStream() {
|
|
const prompt = document.getElementById('prompt').value.trim();
|
|
const streamBtn = document.getElementById('streamBtn');
|
|
if (!prompt) { alert('Bitte Anfrage eingeben!'); return; }
|
|
|
|
streamBtn.disabled = true;
|
|
streamBtn.textContent = '⏳ Agent arbeitet…';
|
|
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const emptyState = document.getElementById('emptyState');
|
|
if (emptyState) emptyState.remove();
|
|
|
|
const msgDiv = document.createElement('div');
|
|
msgDiv.className = 'chat-message';
|
|
msgDiv.innerHTML = `
|
|
<div class="chat-timestamp">${new Date().toLocaleTimeString()} · <span class="badge bg-primary" style="font-size:.65rem;" id="agentBadge">wird ausgewählt…</span></div>
|
|
<div class="chat-prompt mt-1"><strong>Sie:</strong> ${escapeHtml(prompt)}</div>
|
|
<div class="chat-response mt-1" id="responseDiv"><strong>Orchestrator:</strong> <span id="responseText">⏳ Agent arbeitet…</span></div>
|
|
`;
|
|
chatContainer.insertBefore(msgDiv, chatContainer.firstChild);
|
|
|
|
fetch('/api/agent-stream', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ prompt })
|
|
}).then(response => {
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
function read() {
|
|
reader.read().then(({ done, value }) => {
|
|
if (done) {
|
|
streamBtn.disabled = false;
|
|
streamBtn.textContent = 'Live-Antwort anfordern';
|
|
return;
|
|
}
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop();
|
|
for (const line of lines) {
|
|
if (!line.startsWith('data: ')) continue;
|
|
try {
|
|
const event = JSON.parse(line.slice(6));
|
|
if (event.type === 'agent_selected') {
|
|
document.getElementById('agentBadge').textContent = event.agent;
|
|
} else if (event.type === 'response_chunk') {
|
|
const t = document.getElementById('responseText');
|
|
if (t.textContent.startsWith('⏳')) t.textContent = '';
|
|
t.textContent += event.text;
|
|
} else if (event.type === 'error') {
|
|
document.getElementById('responseText').textContent = '❌ ' + event.message;
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
read();
|
|
});
|
|
}
|
|
read();
|
|
}).catch(err => {
|
|
document.getElementById('responseText').textContent = '❌ ' + err.message;
|
|
streamBtn.disabled = false;
|
|
streamBtn.textContent = 'Live-Antwort anfordern';
|
|
});
|
|
}
|
|
|
|
function distributeTodos() {
|
|
const prompt = document.getElementById('todoPrompt').value.trim();
|
|
if (!prompt) { alert('Bitte Aufgaben eingeben!'); return; }
|
|
|
|
const selectedAgents = Array.from(document.querySelectorAll('.agent-checkbox:checked')).map(cb => cb.value);
|
|
if (selectedAgents.length === 0) { alert('Bitte mindestens einen Agenten auswählen!'); return; }
|
|
|
|
const status = document.getElementById('todoStatus');
|
|
status.textContent = 'Starte parallele Ausführung...';
|
|
status.className = 'form-text mt-2 text-info';
|
|
|
|
const tasks = prompt.split('\n').filter(t => t.trim());
|
|
|
|
fetch('/api/orchestrator-distribute', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ tasks, agents: selectedAgents })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
console.log('Response:', data);
|
|
if (data && data.success) {
|
|
status.textContent = '✓ ' + data.message;
|
|
status.className = 'form-text mt-2 text-success';
|
|
if (data.tasks && data.tasks.length > 0) {
|
|
location.reload();
|
|
}
|
|
} else if (data) {
|
|
status.textContent = 'Fehler: ' + (data.error || 'Unbekannter Fehler');
|
|
status.className = 'form-text mt-2 text-danger';
|
|
} else {
|
|
status.textContent = 'Fehler: Keine Antwort vom Server';
|
|
status.className = 'form-text mt-2 text-danger';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
status.textContent = 'Fehler: ' + err.message;
|
|
status.className = 'form-text mt-2 text-danger';
|
|
});
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
{% endblock %}
|