diff --git a/app.py b/app.py index 532cea2..2f4da3d 100644 --- a/app.py +++ b/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(): diff --git a/templates/orchestrator.html b/templates/orchestrator.html index aa776a9..0149a3d 100644 --- a/templates/orchestrator.html +++ b/templates/orchestrator.html @@ -130,7 +130,7 @@ function sendPromptWithStream() { msgDiv.className = 'chat-message'; msgDiv.innerHTML = `
${new Date().toLocaleTimeString()} · wird ausgewählt…
-
Sie: ${prompt}
+
Sie: ${escapeHtml(prompt)}
Orchestrator: ⏳ Agent arbeitet…
`; 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; +} {% endblock %} diff --git a/templates/tasks.html b/templates/tasks.html index 3b5be54..8d6cdef 100644 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -40,13 +40,13 @@
-
🔄 Auto-Refresh
+
ℹ️ Info
-
- - -
+

+ Tasks werden automatisch alle 10 Sekunden vom TaskBeat verarbeitet. +
Diese Seite aktualisiert sich automatisch alle 15 Sekunden wenn Tasks aktiv sind. +

@@ -108,14 +108,14 @@ {{ task.created }} - {% if task.type == 'email' %} - Auto - {% elif task.status == 'pending' %} - Start + {% if task.status == 'pending' %} + ⏳ Wartend {% elif task.status == 'in_progress' %} - Fertig + 🔄 Läuft... + {% elif task.status == 'completed' %} + ✓ Auto {% else %} - + {% endif %} @@ -137,20 +137,13 @@ {% block scripts %} {% endblock %}