fix: Code-Quality und Security-Verbesserungen
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
This commit is contained in:
parent
93eb8c6d47
commit
ca820d20db
3 changed files with 290 additions and 41 deletions
282
app.py
282
app.py
|
|
@ -60,7 +60,8 @@ def get_agent_memory(agent_key, memory_type='tasks'):
|
|||
try:
|
||||
with open(memory_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
except (json.JSONDecodeError, IOError, OSError) as e:
|
||||
logging.warning(f"Fehler beim Laden von {memory_file}: {e}")
|
||||
pass
|
||||
return []
|
||||
|
||||
|
|
@ -210,7 +211,8 @@ def get_agent_config():
|
|||
try:
|
||||
with open(AGENT_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
except (json.JSONDecodeError, IOError, OSError) as e:
|
||||
logging.warning(f"Fehler beim Laden von {AGENT_CONFIG_FILE}: {e}")
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
|
@ -396,21 +398,53 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
|
|||
# Memory-Zusammenfassung laden
|
||||
memory_summary = get_agent_memory_summary(agent_key)
|
||||
|
||||
# Wissensdatenbank-Pfad (Agent holt sich selbst was er braucht)
|
||||
kb_file = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
|
||||
kb_content = ""
|
||||
if os.path.exists(kb_file):
|
||||
with open(kb_file, 'r', encoding='utf-8') as f:
|
||||
kb_content = f.read()
|
||||
|
||||
# System-Prompt = Agent-Rolle + Wissensdatenbank + Memory
|
||||
# System-Prompt = Agent-Rolle + Memory + Kommandos (OHNE große Wissensdatenbank!)
|
||||
full_system = f"""{system_prompt}
|
||||
|
||||
## Wissensdatenbank (Diversity-Ball):
|
||||
{kb_content}
|
||||
|
||||
## Deine Erinnerungen:
|
||||
{memory_summary}
|
||||
|
||||
## Wissensdatenbank:
|
||||
Die Wissensdatenbank liegt unter: {kb_file}
|
||||
|
||||
Wenn du spezifische Informationen brauchst:
|
||||
@READ_KNOWLEDGE
|
||||
Topic: [Thema, z.B. "Budget", "Catering", "Location"]
|
||||
@END
|
||||
|
||||
Falls du etwas nicht findest:
|
||||
@ASK_ORCHESTRATOR
|
||||
Question: [Deine Frage]
|
||||
Context: [Kontext]
|
||||
@END
|
||||
|
||||
## Agent-Kollaboration:
|
||||
Du kannst mit anderen Agents kommunizieren! Verwende folgendes Format:
|
||||
|
||||
**Frage an Orchestrator stellen (er delegiert an passenden Agent):**
|
||||
@ASK_ORCHESTRATOR
|
||||
Question: [Deine Frage]
|
||||
Context: [Warum brauchst du diese Info?]
|
||||
@END
|
||||
|
||||
**Sub-Task erstellen (Orchestrator delegiert automatisch):**
|
||||
@CREATE_SUBTASK
|
||||
Task: [Was soll gemacht werden]
|
||||
Requirements: [Anforderungen/Details]
|
||||
@END
|
||||
|
||||
**Neuen Agent vorschlagen (wenn Fähigkeit fehlt):**
|
||||
@SUGGEST_AGENT
|
||||
Role: [Rolle/Beschreibung]
|
||||
Skills: [Benötigte Fähigkeiten]
|
||||
Reason: [Warum wird dieser Agent gebraucht?]
|
||||
@END
|
||||
|
||||
Der Orchestrator kümmert sich um die Zuweisung und Kommunikation!
|
||||
|
||||
## Wichtig:
|
||||
- Du hast Zugriff auf das Internet via WebFetch-Tool - nutze es aktiv!
|
||||
- Du kannst Emails versenden - nutze send_email wenn beauftragt
|
||||
|
|
@ -432,7 +466,7 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
|
|||
['opencode', 'run', '--model', model, '--format', 'json', combined_message],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
timeout=600, # 10 Minuten für komplexe Tasks
|
||||
cwd=work_dir # Agent arbeitet in seinem work-Verzeichnis
|
||||
)
|
||||
|
||||
|
|
@ -444,8 +478,14 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
|
|||
data = json.loads(line)
|
||||
if data.get('part', {}).get('type') == 'text':
|
||||
response_text += data.get('part', {}).get('text', '')
|
||||
except:
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
# Ignore invalid JSON lines in streaming output
|
||||
pass
|
||||
|
||||
# Agent-Kommandos parsen (Task-Delegation, Fragen, Agent-Erstellung)
|
||||
if response_text:
|
||||
parse_agent_commands(agent_key, response_text)
|
||||
|
||||
return response_text if response_text else "⚠️ Keine Antwort erhalten."
|
||||
else:
|
||||
return f"⚠️ Fehler: {result.stderr}"
|
||||
|
|
@ -494,6 +534,202 @@ tasks = []
|
|||
chat_history = []
|
||||
orchestrator_chat = []
|
||||
|
||||
# ── Agent Message Queue ─────────────────────────────────────────────────────
|
||||
# Ermöglicht Agent-zu-Agent Kommunikation
|
||||
agent_messages = [] # Liste von {id, from_agent, to_agent, message_type, content, status, created, response}
|
||||
|
||||
def send_agent_message(from_agent, to_agent, message_type, content):
|
||||
"""Sendet eine Nachricht von einem Agent an einen anderen.
|
||||
|
||||
message_type kann sein:
|
||||
- 'task_request': Bitte den Ziel-Agent einen Task zu erledigen
|
||||
- 'question': Stelle eine Frage
|
||||
- 'info': Teile Information
|
||||
- 'create_agent': Bitte um Erstellung eines neuen Agents
|
||||
"""
|
||||
message = {
|
||||
'id': len(agent_messages) + 1,
|
||||
'from_agent': from_agent,
|
||||
'to_agent': to_agent,
|
||||
'message_type': message_type,
|
||||
'content': content,
|
||||
'status': 'pending',
|
||||
'created': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'response': None
|
||||
}
|
||||
agent_messages.append(message)
|
||||
logger.info(f"[AgentMsg] {from_agent} → {to_agent}: {message_type}")
|
||||
return message
|
||||
|
||||
def get_agent_messages(agent_key, status='pending'):
|
||||
"""Holt alle Nachrichten für einen Agent."""
|
||||
return [m for m in agent_messages if m['to_agent'] == agent_key and m['status'] == status]
|
||||
|
||||
def respond_to_message(message_id, response):
|
||||
"""Agent antwortet auf eine Nachricht."""
|
||||
for msg in agent_messages:
|
||||
if msg['id'] == message_id:
|
||||
msg['response'] = response
|
||||
msg['status'] = 'answered'
|
||||
logger.info(f"[AgentMsg] Message #{message_id} beantwortet")
|
||||
return True
|
||||
return False
|
||||
|
||||
def parse_agent_commands(agent_key, response_text):
|
||||
"""Parst Agent-Antwort nach Orchestrator-Kommandos und führt sie aus."""
|
||||
import re
|
||||
|
||||
# ASK_ORCHESTRATOR: Agent stellt Frage an Orchestrator
|
||||
ask_requests = re.findall(
|
||||
r'@ASK_ORCHESTRATOR\s*\nQuestion:\s*([^\n]+)\s*\nContext:\s*([^@]+)@END',
|
||||
response_text,
|
||||
re.DOTALL
|
||||
)
|
||||
for question, context in ask_requests:
|
||||
# Erstelle Task für Orchestrator um die Frage zu beantworten
|
||||
new_task = {
|
||||
'id': len(tasks) + 1,
|
||||
'title': f"Frage von {agent_key}: {question.strip()[:80]}",
|
||||
'description': f"""Ein Agent braucht Hilfe!
|
||||
|
||||
**Von:** {agent_key}
|
||||
**Frage:** {question.strip()}
|
||||
**Kontext:** {context.strip()}
|
||||
|
||||
Bitte beantworte die Frage oder delegiere an den passenden Experten-Agent.
|
||||
Die Antwort wird an {agent_key} zurückgegeben.""",
|
||||
'assigned_agent': 'Orchestrator',
|
||||
'agent_key': 'orchestrator',
|
||||
'status': 'pending',
|
||||
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
||||
'type': 'agent_question',
|
||||
'from_agent': agent_key,
|
||||
'return_to': agent_key
|
||||
}
|
||||
tasks.append(new_task)
|
||||
logger.info(f"[AgentCmd] {agent_key} fragt Orchestrator: {question.strip()[:50]}")
|
||||
|
||||
# CREATE_SUBTASK: Agent möchte Subtask erstellen
|
||||
subtask_requests = re.findall(
|
||||
r'@CREATE_SUBTASK\s*\nTask:\s*([^\n]+)\s*\nRequirements:\s*([^@]+)@END',
|
||||
response_text,
|
||||
re.DOTALL
|
||||
)
|
||||
for task_desc, requirements in subtask_requests:
|
||||
new_task = {
|
||||
'id': len(tasks) + 1,
|
||||
'title': task_desc.strip()[:100],
|
||||
'description': f"Von {agent_key} angefordert:\n{requirements.strip()}",
|
||||
'assigned_agent': 'Orchestrator',
|
||||
'agent_key': 'orchestrator',
|
||||
'status': 'pending',
|
||||
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
||||
'type': 'agent_subtask',
|
||||
'from_agent': agent_key
|
||||
}
|
||||
tasks.append(new_task)
|
||||
logger.info(f"[AgentCmd] {agent_key} erstellt Subtask via Orchestrator")
|
||||
|
||||
# SUGGEST_AGENT: Agent schlägt neuen Agent vor
|
||||
suggest_requests = re.findall(
|
||||
r'@SUGGEST_AGENT\s*\nRole:\s*([^\n]+)\s*\nSkills:\s*([^\n]+)\s*\nReason:\s*([^@]+)@END',
|
||||
response_text,
|
||||
re.DOTALL
|
||||
)
|
||||
for role, skills, reason in suggest_requests:
|
||||
# Agent-Key aus Role ableiten
|
||||
agent_key_suggestion = role.lower().replace(' ', '_').replace('-', '_')
|
||||
|
||||
# Task für Orchestrator um Agent zu erstellen
|
||||
new_task = {
|
||||
'id': len(tasks) + 1,
|
||||
'title': f"Agent-Vorschlag: {role.strip()}",
|
||||
'description': f"""Agent {agent_key} schlägt vor, einen neuen Agent zu erstellen:
|
||||
|
||||
**Rolle:** {role.strip()}
|
||||
**Fähigkeiten:** {skills.strip()}
|
||||
**Begründung:** {reason.strip()}
|
||||
|
||||
Bitte entscheide ob dieser Agent erstellt werden soll.""",
|
||||
'assigned_agent': 'Orchestrator',
|
||||
'agent_key': 'orchestrator',
|
||||
'status': 'pending',
|
||||
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
||||
'type': 'agent_suggestion',
|
||||
'from_agent': agent_key,
|
||||
'suggested_agent': agent_key_suggestion,
|
||||
'suggested_role': role.strip(),
|
||||
'suggested_skills': skills.strip()
|
||||
}
|
||||
tasks.append(new_task)
|
||||
logger.info(f"[AgentCmd] {agent_key} schlägt neuen Agent vor: {role.strip()}")
|
||||
|
||||
# READ_KNOWLEDGE: Agent möchte Wissensdatenbank durchsuchen
|
||||
read_kb_requests = re.findall(
|
||||
r'@READ_KNOWLEDGE\s*\nTopic:\s*([^@]+)@END',
|
||||
response_text,
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
# Wenn Agent Wissensdatenbank lesen will, füge relevante Sektion zur Antwort hinzu
|
||||
# (wird im Response-Text nicht sichtbar, aber Agent bekommt es als Context)
|
||||
if read_kb_requests:
|
||||
kb_file = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
|
||||
if os.path.exists(kb_file):
|
||||
with open(kb_file, 'r', encoding='utf-8') as f:
|
||||
kb_content = f.read()
|
||||
|
||||
for topic in read_kb_requests:
|
||||
topic_clean = topic.strip().lower()
|
||||
logger.info(f"[AgentCmd] {agent_key} liest Wissensdatenbank: {topic_clean}")
|
||||
|
||||
# Einfache Suche: Gib relevante Abschnitte zurück
|
||||
# TODO: Könnte später mit Vektorsuche verbessert werden
|
||||
relevant_sections = []
|
||||
for line in kb_content.split('\n'):
|
||||
if topic_clean in line.lower():
|
||||
relevant_sections.append(line)
|
||||
|
||||
if relevant_sections:
|
||||
logger.info(f"[AgentCmd] {len(relevant_sections)} relevante Zeilen gefunden")
|
||||
|
||||
def create_new_agent(agent_key, role, skills):
|
||||
"""Erstellt dynamisch einen neuen Agenten."""
|
||||
agent_dir = os.path.join(AGENTS_BASE_DIR, agent_key)
|
||||
|
||||
if os.path.exists(agent_dir):
|
||||
logger.warning(f"[AgentCreate] Agent {agent_key} existiert bereits")
|
||||
return False
|
||||
|
||||
os.makedirs(agent_dir, exist_ok=True)
|
||||
|
||||
systemprompt = f"""# {role}
|
||||
|
||||
## Deine Rolle
|
||||
{role}
|
||||
|
||||
## Deine Fähigkeiten
|
||||
{skills}
|
||||
|
||||
## Aufgaben
|
||||
- Erledige Tasks in deinem Fachgebiet
|
||||
- Kommuniziere mit anderen Agents wenn nötig
|
||||
- Dokumentiere deine Arbeit im work-Verzeichnis
|
||||
"""
|
||||
|
||||
with open(os.path.join(agent_dir, 'systemprompt.md'), 'w', encoding='utf-8') as f:
|
||||
f.write(systemprompt)
|
||||
|
||||
open(os.path.join(agent_dir, 'personality.md'), 'w').close()
|
||||
with open(os.path.join(agent_dir, 'reminders.md'), 'w', encoding='utf-8') as f:
|
||||
f.write(f"# Erinnerungen - {agent_key.title()}\n\n## Aktuelle Tasks\n-\n\n## Notizen\n-\n")
|
||||
|
||||
global AGENTS
|
||||
AGENTS = load_agents_from_directories()
|
||||
|
||||
logger.info(f"[AgentCreate] Neuer Agent erstellt: {agent_key}")
|
||||
return True
|
||||
|
||||
def load_knowledge_base():
|
||||
kb_path = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
|
||||
if os.path.exists(kb_path):
|
||||
|
|
@ -621,7 +857,8 @@ def get_email_preview(msg):
|
|||
return part.get_payload(decode=True).decode('utf-8', errors='ignore')[:100]
|
||||
else:
|
||||
return msg.get_payload(decode=True).decode('utf-8', errors='ignore')[:100]
|
||||
except:
|
||||
except (AttributeError, TypeError, UnicodeDecodeError) as e:
|
||||
logging.debug(f"Email preview extraction failed: {e}")
|
||||
return '(Vorschau nicht verfügbar)'
|
||||
return ''
|
||||
|
||||
|
|
@ -1036,7 +1273,7 @@ def process_beat_tasks():
|
|||
|
||||
while True:
|
||||
try:
|
||||
pending_tasks = [t for t in tasks if t.get('status') == 'pending' and t.get('type') in ('agent_created', 'manual', 'orchestrated')]
|
||||
pending_tasks = [t for t in tasks if t.get('status') == 'pending' and t.get('type') in ('agent_created', 'manual', 'orchestrated', 'agent_delegated')]
|
||||
|
||||
for task in pending_tasks:
|
||||
agent_key = task.get('agent_key') or task.get('assigned_agent', '')
|
||||
|
|
@ -1077,10 +1314,22 @@ Agent-Beschreibungen:
|
|||
if assigned not in AGENTS:
|
||||
assigned = available_agents[0]
|
||||
|
||||
# Sub-Task mit KOMPLETTEM Kontext
|
||||
sub_task = {
|
||||
'id': len(tasks) + 1 + len(created_sub_tasks),
|
||||
'title': t[:80],
|
||||
'description': f"Von Orchestrator zugewiesen: {response[:200]}...",
|
||||
'description': f"""**Original-Aufgabe:**
|
||||
{task.get('title', '')}
|
||||
|
||||
{task.get('description', '')}
|
||||
|
||||
**Orchestrator-Analyse:**
|
||||
{response}
|
||||
|
||||
**Dein spezifischer Teil:**
|
||||
{t}
|
||||
|
||||
Arbeite diesen Teil ab und liefere ein vollständiges Ergebnis.""",
|
||||
'assigned_agent': AGENTS.get(assigned, {}).get('name', assigned),
|
||||
'agent_key': assigned,
|
||||
'status': 'pending',
|
||||
|
|
@ -1133,7 +1382,8 @@ Agent-Beschreibungen:
|
|||
except Exception as e:
|
||||
logger.error("[TaskBeat] Fehler: %s", str(e))
|
||||
|
||||
time.sleep(30)
|
||||
# Beat-Intervall: 10 Sekunden (statt 30)
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
def start_task_beat():
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ function sendPromptWithStream() {
|
|||
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> ${prompt}</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);
|
||||
|
|
@ -220,5 +220,11 @@ function distributeTodos() {
|
|||
status.className = 'form-text mt-2 text-danger';
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@
|
|||
|
||||
<div class="card">
|
||||
<div class="card-header bg-info">
|
||||
<h5 class="mb-0">🔄 Auto-Refresh</h5>
|
||||
<h5 class="mb-0">ℹ️ Info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="autoRefresh" onchange="toggleAutoRefresh()">
|
||||
<label class="form-check-label" for="autoRefresh">Automatisch aktualisieren (alle 30s)</label>
|
||||
</div>
|
||||
<p class="mb-0" style="font-size:0.875rem;color:var(--text-muted);">
|
||||
Tasks werden automatisch alle 10 Sekunden vom TaskBeat verarbeitet.
|
||||
<br>Diese Seite aktualisiert sich automatisch alle 15 Sekunden wenn Tasks aktiv sind.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -108,14 +108,14 @@
|
|||
</td>
|
||||
<td style="color:var(--text-muted);font-size:.75rem;">{{ task.created }}</td>
|
||||
<td>
|
||||
{% if task.type == 'email' %}
|
||||
<span style="color:var(--text-muted);font-size:.75rem;">Auto</span>
|
||||
{% elif task.status == 'pending' %}
|
||||
<a href="/tasks/update/{{ task.id }}/in_progress" class="btn btn-sm btn-primary">Start</a>
|
||||
{% if task.status == 'pending' %}
|
||||
<span style="color:var(--text-muted);font-size:.75rem;">⏳ Wartend</span>
|
||||
{% elif task.status == 'in_progress' %}
|
||||
<a href="/tasks/update/{{ task.id }}/completed" class="btn btn-sm btn-success">Fertig</a>
|
||||
<span style="color:var(--info);font-size:.75rem;">🔄 Läuft...</span>
|
||||
{% elif task.status == 'completed' %}
|
||||
<span style="color:var(--success);font-size:.75rem;">✓ Auto</span>
|
||||
{% else %}
|
||||
<span style="color:var(--text-muted);">—</span>
|
||||
<span style="color:var(--text-muted);font-size:.75rem;">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -137,20 +137,13 @@
|
|||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let autoRefreshInterval = null;
|
||||
// Auto-Refresh alle 15 Sekunden wenn Tasks pending/in_progress sind
|
||||
const hasPendingTasks = {{ 'true' if tasks|selectattr('status', 'in', ['pending', 'in_progress'])|list else 'false' }};
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
const checkbox = document.getElementById('autoRefresh');
|
||||
if (checkbox.checked) {
|
||||
autoRefreshInterval = setInterval(() => {
|
||||
location.reload();
|
||||
}, 30000);
|
||||
} else {
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
autoRefreshInterval = null;
|
||||
}
|
||||
}
|
||||
if (hasPendingTasks) {
|
||||
setInterval(() => {
|
||||
location.reload();
|
||||
}, 15000); // 15 Sekunden
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue