Compare commits

..

No commits in common. "c82ecdd5f1be377ca4e4de9d5f31b08bd4998c49" and "c6ce8a873cdf8a40346df1b0048062dbd6979429" have entirely different histories.

3 changed files with 40 additions and 483 deletions

View file

@ -73,79 +73,10 @@ email: email@adresse.com
</add_team_member> </add_team_member>
``` ```
**Wissensdatenbank aktualisieren:**
```
<update_knowledge>
topic: Eventstart
content: Das Event startet um 18:00 Uhr. Einlass ab 17:30 Uhr.
</update_knowledge>
```
**Reminder eines Agenten aktualisieren:**
```
<update_agent_reminder>
agent: catering_manager
reminder: WICHTIG: Eventstart wurde auf 18:00 geändert. Catering-Aufbau muss bis 17:00 fertig sein.
</update_agent_reminder>
```
## Tägliches Standup
Jeden Morgen um 09:00 Uhr wirst du automatisch aktiviert, um:
1. Alle Team-Members nach ihren täglichen Updates zu fragen
2. Neue Informationen in die Wissensdatenbank einzupflegen
3. Jeden Agenten über relevante Änderungen zu informieren
Wenn du für ein Standup aktiviert wirst:
- Prüfe aktuelle Tasks und Erinnerungen auf neue Informationen
- Aktualisiere `<update_knowledge>` für jede wichtige Änderung
- Schreibe für jeden Agenten einen `<update_agent_reminder>` mit relevanten Updates
- Delegiere mit `<create_task>` an jeden Agenten, damit er seinen Bereich prüft
## Wissens-Broadcasts
Wenn jemand eine neue wichtige Information mitteilt (z.B. "das Event startet um 18:00 statt 19:00"):
1. Erkenne, dass dies ein Update ist das alle betrifft
2. Aktualisiere sofort die Wissensdatenbank: `<update_knowledge>`
3. Informiere **jeden** relevanten Agenten per `<update_agent_reminder>`
4. Delegiere an betroffene Agenten Sub-Tasks zur Überprüfung ihrer Bereiche: `<create_task>`
5. Bestätige Piotr (telegram_id: 1578034974) dass alles verteilt wurde
**Beispiel — jemand sagt "das Event startet um 18:00 nicht 19:00":**
```
<update_knowledge>
topic: Eventstart
content: Das Event startet um 18:00 Uhr (geändert). Einlass ab 17:30 Uhr.
</update_knowledge>
<update_agent_reminder>
agent: catering_manager
reminder: WICHTIG: Eventstart wurde auf 18:00 geändert (war 19:00). Catering-Aufbau bis 17:00 abschließen.
</update_agent_reminder>
<update_agent_reminder>
agent: location_manager
reminder: WICHTIG: Eventstart 18:00 Uhr (geändert). Venue-Öffnung und Setup entsprechend anpassen.
</update_agent_reminder>
<create_task>
title: Eventstart-Änderung prüfen: 18:00 statt 19:00
agent: program_manager
details: Der Eventstart wurde auf 18:00 geändert. Bitte überprüfe den Programmablauf und passe alle Zeitangaben an.
</create_task>
<send_telegram>
telegram_id: 1578034974
message: ✅ Update verteilt: Eventstart 18:00 Uhr. Wissensdatenbank aktualisiert, alle Agenten informiert.
</send_telegram>
```
## Verhalten bei Nachrichten ## Verhalten bei Nachrichten
1. Antworte freundlich und direkt 1. Antworte freundlich und direkt
2. Wenn eine Aufgabe dabei ist → sofort `<create_task>` anlegen 2. Wenn eine Aufgabe dabei ist → sofort `<create_task>` anlegen
3. Wenn Email/Telegram gesendet werden soll → `<send_email>` / `<send_telegram>` direkt ausführen 3. Wenn Email/Telegram gesendet werden soll → `<send_email>` / `<send_telegram>` direkt ausführen
4. Wenn Team-Daten zu aktualisieren → `<update_team_member>` direkt ausführen 4. Wenn Team-Daten zu aktualisieren → `<update_team_member>` direkt ausführen
5. Wenn neue wichtige Information → `<update_knowledge>` + `<update_agent_reminder>` für betroffene Agenten 5. Bestätige am Ende was du getan hast
6. Bestätige am Ende was du getan hast

436
app.py
View file

@ -712,21 +712,16 @@ chat_history = []
orchestrator_chat = [] orchestrator_chat = []
# ── Task Database Functions ───────────────────────────────────────────────── # ── Task Database Functions ─────────────────────────────────────────────────
def get_tasks(status=None, limit=None, order='asc'): def get_tasks(status=None, limit=None):
"""Lädt Tasks aus der Datenbank. """Lädt Tasks aus der Datenbank."""
order='asc' älteste zuerst (Standard für TaskBeat: FIFO)
order='desc' neueste zuerst (für UI-Anzeige)
"""
con = sqlite3.connect(EMAIL_JOURNAL_DB) con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.row_factory = sqlite3.Row con.row_factory = sqlite3.Row
direction = 'ASC' if order == 'asc' else 'DESC'
if status: if status:
query = f"SELECT * FROM tasks WHERE status = ? ORDER BY id {direction}" query = "SELECT * FROM tasks WHERE status = ? ORDER BY id DESC"
params = (status,) params = (status,)
else: else:
query = f"SELECT * FROM tasks ORDER BY id {direction}" query = "SELECT * FROM tasks ORDER BY id DESC"
params = () params = ()
if limit: if limit:
@ -1007,23 +1002,16 @@ def parse_agent_commands(agent_key, response_text, task_id=None):
import re import re
def get_field(block, field): def get_field(block, field):
"""Extrahiert ein Feld aus einem XML-Block: '<field>value</field>' oder 'field: value'. """Extrahiert ein Feld aus einem XML-Block: 'field: value' oder '<field>value</field>'."""
Unterstützt mehrzeilige Werte (z.B. langer Email-Body). # Versuche erst XML-Tag-Format
"""
# Versuche erst XML-Tag-Format (bevorzugt, unterstützt Mehrzeiligkeit)
m = re.search(rf'<{field}>(.*?)</{field}>', block, re.DOTALL | re.IGNORECASE) m = re.search(rf'<{field}>(.*?)</{field}>', block, re.DOTALL | re.IGNORECASE)
if m: if m:
return m.group(1).strip() return m.group(1).strip()
# Key-Value-Format: finde 'field: ...' und lies bis zum nächsten echten Key (^\w+: ) # Dann Key-Value-Format
m = re.search(rf'(?m)^{field}\s*:\s*(.*)', block, re.IGNORECASE) m = re.search(rf'^{field}\s*:\s*(.+)', block, re.MULTILINE | re.IGNORECASE)
if not m: if m:
return '' return m.group(1).strip()
rest = block[m.start(1):] return ''
# Stoppe nur bei echten einwortigen Keys (^\w+: Leerzeichen) — nicht bei "Report:" etc.
stop = re.search(r'(?m)^\w+\s*:\s', rest)
if stop:
return rest[:stop.start()].strip()
return rest.strip()
# ── CREATE_TASK ────────────────────────────────────────────────────────── # ── CREATE_TASK ──────────────────────────────────────────────────────────
for block in re.findall(r'<create_task>(.*?)</create_task>', response_text, re.DOTALL | re.IGNORECASE): for block in re.findall(r'<create_task>(.*?)</create_task>', response_text, re.DOTALL | re.IGNORECASE):
@ -1181,64 +1169,6 @@ def parse_agent_commands(agent_key, response_text, task_id=None):
update_task_db(action_id, status='completed' if success else 'error', response=status_msg) update_task_db(action_id, status='completed' if success else 'error', response=status_msg)
logger.info(f"[AgentCmd] {status_msg}") logger.info(f"[AgentCmd] {status_msg}")
# ── UPDATE_KNOWLEDGE ─────────────────────────────────────────────────────
# Agenten können damit einen neuen Abschnitt in der Wissensdatenbank anlegen/aktualisieren
for block in re.findall(r'<update_knowledge>(.*?)</update_knowledge>', response_text, re.DOTALL | re.IGNORECASE):
topic = get_field(block, 'topic')
content = get_field(block, 'content')
if not topic or not content:
logger.warning("[AgentCmd] <update_knowledge> ohne topic/content ignoriert")
continue
kb_file = os.path.join(os.path.dirname(__file__), 'agents', 'orchestrator', 'knowledge', 'diversityball_knowledge.md')
os.makedirs(os.path.dirname(kb_file), exist_ok=True)
# Existierenden Abschnitt ersetzen oder neuen anhängen
section_header = f"## {topic}"
new_section = f"{section_header}\n\n{content.strip()}\n"
if os.path.exists(kb_file):
with open(kb_file, 'r', encoding='utf-8') as f:
kb_text = f.read()
# Ersetze bestehenden Abschnitt (## Topic ... bis zum nächsten ##)
pattern = rf'(^## {re.escape(topic)}\s*\n)(.*?)(?=\n## |\Z)'
if re.search(pattern, kb_text, re.MULTILINE | re.DOTALL):
kb_text = re.sub(pattern, new_section, kb_text, flags=re.MULTILINE | re.DOTALL)
else:
kb_text = kb_text.rstrip() + f"\n\n{new_section}"
else:
kb_text = f"# Diversity Ball Wien 2026 — Wissensdatenbank\n\n{new_section}"
with open(kb_file, 'w', encoding='utf-8') as f:
f.write(kb_text)
action_id = create_task(
title=f"Wissen aktualisiert: {topic}",
description=f"**Topic:** {topic}\n\n{content[:200]}{'...' if len(content) > 200 else ''}",
agent_key=agent_key, task_type='action_knowledge', created_by=agent_key, parent_task_id=task_id,
)
update_task_db(action_id, status='completed', response=f"✓ Wissensdatenbank aktualisiert: {topic}")
logger.info(f"[AgentCmd] Wissensdatenbank aktualisiert: {topic}")
# ── UPDATE_AGENT_REMINDER ────────────────────────────────────────────────
# Orchestrator kann damit die reminders.md eines beliebigen Agenten aktualisieren
for block in re.findall(r'<update_agent_reminder>(.*?)</update_agent_reminder>', response_text, re.DOTALL | re.IGNORECASE):
target_agent = get_field(block, 'agent')
reminder = get_field(block, 'reminder')
if not target_agent or not reminder:
logger.warning("[AgentCmd] <update_agent_reminder> ohne agent/reminder ignoriert")
continue
reminder_file = os.path.join(os.path.dirname(__file__), 'agents', target_agent, 'reminders.md')
if not os.path.exists(os.path.dirname(reminder_file)):
logger.warning(f"[AgentCmd] Agent-Verzeichnis nicht gefunden: {target_agent}")
continue
timestamp = datetime.now().strftime('%d.%m.%Y %H:%M')
entry = f"\n## Update {timestamp}\n\n{reminder.strip()}\n"
with open(reminder_file, 'a', encoding='utf-8') as f:
f.write(entry)
action_id = create_task(
title=f"Reminder aktualisiert: {target_agent}",
description=f"**Agent:** {target_agent}\n\n{reminder[:200]}{'...' if len(reminder) > 200 else ''}",
agent_key=agent_key, task_type='action_knowledge', created_by=agent_key, parent_task_id=task_id,
)
update_task_db(action_id, status='completed', response=f"✓ reminders.md aktualisiert für {target_agent}")
logger.info(f"[AgentCmd] reminders.md aktualisiert für {target_agent}")
def create_new_agent(agent_key, role, skills): def create_new_agent(agent_key, role, skills):
"""Erstellt dynamisch einen neuen Agenten.""" """Erstellt dynamisch einen neuen Agenten."""
agent_dir = os.path.join(AGENTS_BASE_DIR, agent_key) agent_dir = os.path.join(AGENTS_BASE_DIR, agent_key)
@ -1601,74 +1531,20 @@ async def telegram_message_handler(update: Update, context: ContextTypes.DEFAULT
user_id = update.effective_user.id user_id = update.effective_user.id
username = update.effective_user.username or update.effective_user.first_name username = update.effective_user.username or update.effective_user.first_name
message_text = update.message.text message_text = update.message.text
chat_id = update.effective_chat.id
# Whitelist-Check # Whitelist-Check
if TELEGRAM_CONFIG['allowed_users'] and user_id not in TELEGRAM_CONFIG['allowed_users']: if TELEGRAM_CONFIG['allowed_users'] and user_id not in TELEGRAM_CONFIG['allowed_users']:
await update.message.reply_text("⛔ Zugriff verweigert. Verwende /start für Details.") await update.message.reply_text("⛔ Zugriff verweigert. Verwende /start für Details.")
return return
# ── Prüfe ob diese Nachricht eine Antwort auf einen offenen standup_reply-Task ist ── # Task erstellen
try:
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.row_factory = sqlite3.Row
open_standup = con.execute(
"SELECT id, parent_task_id, title FROM tasks "
"WHERE type='standup_reply' AND status='pending' AND telegram_chat_id=? "
"ORDER BY id ASC LIMIT 1",
(chat_id,)
).fetchone()
con.close()
except Exception as e:
logging.error(f"[Telegram] DB-Fehler beim Standup-Check: {e}")
open_standup = None
if open_standup:
# Antwort auf offenen Standup-Task → mit Kontext an Orchestrator weiterleiten
standup_task_id = open_standup['id']
parent_id = open_standup['parent_task_id']
# Orchestrator-Task erstellen: verarbeite die Standup-Antwort
orchestrator_prompt = (
f"## Standup-Antwort von {username}\n\n"
f"**Antwort:** {message_text}\n\n"
f"**Aufgabe:**\n"
f"1. Verarbeite diese Standup-Antwort und extrahiere wichtige Informationen.\n"
f"2. Falls neue Informationen enthalten sind (Änderungen, Entscheidungen, Probleme): "
f"aktualisiere die Wissensdatenbank mit `<update_knowledge>` und informiere betroffene Agenten mit `<update_agent_reminder>`.\n"
f"3. Falls Handlungsbedarf besteht: erstelle entsprechende Tasks mit `<create_task>`.\n"
f"4. Antworte {username} kurz per Telegram (telegram_id: {chat_id}) und bestätige den Empfang."
)
followup_id = create_task(
title=f"Standup-Antwort verarbeiten: {username}",
description=orchestrator_prompt,
agent_key='orchestrator',
task_type='telegram',
created_by=f'telegram_user_{user_id}',
telegram_chat_id=chat_id,
telegram_user=username,
parent_task_id=parent_id,
)
# Standup-reply-Task als beantwortet markieren
update_task_db(standup_task_id, status='completed',
response=f"Antwort erhalten von {username}: {message_text[:200]}")
await update.message.reply_text(
f"✅ Danke {username}! Deine Standup-Antwort wurde aufgenommen.\n\n"
f"📝 Der Orchestrator verarbeitet dein Update (Task #{followup_id})."
)
logging.info(f"[Telegram] Standup-Antwort von {username} → Task #{standup_task_id} completed, Follow-up #{followup_id} erstellt")
return
# ── Normale Nachricht → neuer Orchestrator-Task ──────────────────────────
task_id = create_task( task_id = create_task(
title=f"Telegram: {message_text[:50]}{'...' if len(message_text) > 50 else ''}", title=f"Telegram: {message_text[:50]}{'...' if len(message_text) > 50 else ''}",
description=message_text, description=message_text,
agent_key='orchestrator', agent_key='orchestrator',
task_type='telegram', task_type='telegram',
created_by=f'telegram_user_{user_id}', created_by=f'telegram_user_{user_id}',
telegram_chat_id=chat_id, telegram_chat_id=update.effective_chat.id,
telegram_user=username telegram_user=username
) )
@ -2108,7 +1984,7 @@ def process_beat_tasks():
# Konvertiere zu Dict-Format für Legacy-Kompatibilität # Konvertiere zu Dict-Format für Legacy-Kompatibilität
pending_tasks = [] pending_tasks = []
for db_task in db_tasks: for db_task in db_tasks:
if db_task.get('type') in ('agent_created', 'manual', 'orchestrated', 'agent_delegated', 'telegram', 'agent_subtask', 'broadcast'): if db_task.get('type') in ('agent_created', 'manual', 'orchestrated', 'agent_delegated', 'telegram'):
pending_tasks.append(db_task) pending_tasks.append(db_task)
for task in pending_tasks: for task in pending_tasks:
@ -2403,232 +2279,10 @@ def start_orchestrator_beat():
logger.info("[OrchestratorBeat] Daemon-Thread gestartet.") logger.info("[OrchestratorBeat] Daemon-Thread gestartet.")
# ── DAILY STANDUP ─────────────────────────────────────────────────────────────
_standup_lock = threading.Lock()
def trigger_daily_standup():
"""
Tägliches Standup: Orchestrator fragt alle Team-Members nach Updates
und delegiert anschließend Wissensupdates an alle Agenten.
Gegen Doppel-Trigger gesichert: nur ein Standup pro Tag möglich.
"""
# Nur einen gleichzeitigen Standup erlauben
if not _standup_lock.acquire(blocking=False):
logger.warning("[DailyStandup] Bereits aktiv — zweiter Trigger ignoriert.")
return
try:
# Prüfe ob heute schon ein Standup läuft oder abgeschlossen ist
today_str = datetime.now().strftime('%Y-%m-%d')
try:
con = sqlite3.connect(EMAIL_JOURNAL_DB)
existing = con.execute(
"SELECT id FROM tasks WHERE type='standup' AND created_at LIKE ? AND status != 'error'",
(f"{today_str}%",)
).fetchone()
con.close()
except Exception:
existing = None
if existing:
logger.warning("[DailyStandup] Standup für heute (Task #%s) bereits vorhanden — abgebrochen.", existing['id'] if existing else '?')
return
logger.info("[DailyStandup] Starte tägliches Standup...")
_trigger_daily_standup_inner()
finally:
_standup_lock.release()
def _trigger_daily_standup_inner():
# Team-Members aus DB holen
try:
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.row_factory = sqlite3.Row
members = con.execute("SELECT name, email, role, telegram_id FROM team_members").fetchall()
con.close()
except Exception as e:
logger.error("[DailyStandup] Fehler beim Laden der Team-Members: %s", e)
members = []
today = datetime.now().strftime('%d.%m.%Y')
# ── 1. Orchestrator-Haupttask: plant den Standup ──────────────────────────
standup_task_id = create_task(
title=f"Daily Standup {today}",
description=(
f"Täglicher Status-Check vom {today}.\n\n"
"Der Orchestrator koordiniert:\n"
"1. Alle Team-Members werden nach Updates gefragt (Telegram + Email)\n"
"2. Eingegangene Updates werden an alle Agenten weitergegeben\n"
"3. Wissensdatenbank wird bei Bedarf aktualisiert"
),
agent_key='orchestrator',
task_type='standup',
created_by='system',
)
# ── 2. Pro Team-Member einen Sub-Task: frag nach Updates ─────────────────
for m in members:
name = m['name']
email = m['email']
telegram_id = m['telegram_id']
role = m['role'] or 'Team-Member'
msg = (
f"Guten Morgen {name}! 👋\n\n"
f"Täglicher Status-Check ({today}):\n\n"
f"Bitte teile uns mit:\n"
f"• Was hat sich seit gestern in deinem Bereich geändert?\n"
f"• Gibt es neue Informationen, Termine oder Entscheidungen?\n"
f"• Benötigst du Unterstützung von anderen?\n\n"
f"Du kannst direkt hier per Telegram antworten oder eine Email senden."
)
# Telegram bevorzugen, Fallback auf Email
if telegram_id:
standup_sub_id = create_task(
title=f"Standup-Antwort ausstehend: {name}",
description=(
f"Standup-Frage wurde per Telegram an {name} gesendet.\n"
f"Warte auf Antwort...\n\n"
f"Gesendete Frage:\n{msg}"
),
agent_key='orchestrator',
task_type='standup_reply',
created_by='system',
parent_task_id=standup_task_id,
telegram_chat_id=int(telegram_id),
telegram_user=name,
)
send_telegram_message(telegram_id, msg)
logger.info("[DailyStandup] Telegram-Standup gesendet an %s (%s), Task #%s wartet auf Antwort", name, telegram_id, standup_sub_id)
elif email:
subject = f"Daily Standup {today} — Was gibt's Neues?"
create_task(
title=f"Standup-Antwort ausstehend: {name} (Email)",
description=f"An: {email}\nBetreff: {subject}\n\n{msg}",
agent_key='orchestrator',
task_type='standup_reply',
created_by='system',
parent_task_id=standup_task_id,
)
send_email(email, subject, msg, triggered_by='system:standup', task_id=standup_task_id)
logger.info("[DailyStandup] Email-Standup gesendet an %s (%s)", name, email)
# ── 3. Orchestrator-Prompt: sammle & verteile Wissen ─────────────────────
agent_list = ', '.join(k for k in AGENTS.keys() if k != 'orchestrator')
orchestrator_prompt = f"""## Daily Standup — {today}
Die Standup-Fragen wurden soeben per Telegram/Email an alle Team-Members versendet und warten auf Antwort.
**Deine Aufgabe jetzt nur Wissenspflege, KEIN erneutes Kontaktieren der Team-Members:**
1. Prüfe ob es seit dem letzten Standup wichtige Informationen oder Änderungen gibt (aus deiner Erinnerung oder den letzten Tasks).
2. Falls es wichtige Updates gibt (z.B. Terminänderungen, Entscheidungen, Budget-Anpassungen):
- Aktualisiere die Wissensdatenbank mit `<update_knowledge>`
- Schreibe für jeden betroffenen Agenten einen `<update_agent_reminder>`
- Delegiere Sub-Tasks an die Agenten: {agent_list}
3. Falls keine Änderungen vorliegen: antworte kurz mit einer Zusammenfassung was du geprüft hast. Sende dabei **keine** Telegram-Nachrichten die Team-Members wurden bereits kontaktiert.
**Wichtig:** Sende KEINE weiteren Nachrichten an Team-Members. Die Standup-Fragen laufen bereits.
Führe alle notwendigen Aktionen aus und bestätige am Ende was du getan hast.
"""
response = execute_agent_task('orchestrator', orchestrator_prompt)
if response:
parse_agent_commands('orchestrator', response, task_id=standup_task_id)
update_task_db(standup_task_id, status='completed', response=response[:500])
logger.info("[DailyStandup] Orchestrator-Standup abgeschlossen.")
else:
update_task_db(standup_task_id, status='error', response='Keine Antwort vom Orchestrator')
def broadcast_knowledge_update(info: str, source: str = 'manual'):
"""
Verteilt eine neue Information an alle Agenten:
- Wissensdatenbank aktualisieren (via Orchestrator)
- Jeden Agenten per Sub-Task informieren
- Piotr per Telegram bestätigen
"""
logger.info("[Broadcast] Starte Knowledge-Broadcast: %s", info[:80])
today = datetime.now().strftime('%d.%m.%Y %H:%M')
broadcast_task_id = create_task(
title=f"Wissens-Broadcast: {info[:60]}",
description=f"Neue Information vom {today}:\n\n{info}\n\nQuelle: {source}",
agent_key='orchestrator',
task_type='broadcast',
created_by='system',
)
agent_list = ', '.join(k for k in AGENTS.keys() if k != 'orchestrator')
prompt = f"""## Wissens-Broadcast — {today}
Eine neue wichtige Information wurde eingegeben und muss an das gesamte Team verteilt werden:
**Neue Information:**
{info}
**Deine Aufgaben:**
1. Aktualisiere die Wissensdatenbank mit dem passenden Topic.
2. Aktualisiere die reminders.md für **jeden** dieser Agenten:
{agent_list}
3. Lege für jeden Agenten einen Sub-Task an, damit er die neue Information in seinem Fachbereich berücksichtigt.
4. Sende Piotr (telegram_id: 1578034974) eine Bestätigung dass die Information verteilt wurde.
Führe alle Aktionen jetzt aus.
"""
response = execute_agent_task('orchestrator', prompt)
if response:
parse_agent_commands('orchestrator', response, task_id=broadcast_task_id)
update_task_db(broadcast_task_id, status='completed', response=response[:500])
logger.info("[Broadcast] Knowledge-Broadcast abgeschlossen.")
else:
update_task_db(broadcast_task_id, status='error', response='Keine Antwort')
def daily_standup_beat():
"""Hintergrund-Thread: Führt täglich um 09:00 das Standup aus."""
logger.info("[DailyStandup] Hintergrund-Thread gestartet.")
# Beim Start: nächste 09:00 berechnen
while True:
try:
now = datetime.now()
target = now.replace(hour=9, minute=0, second=0, microsecond=0)
if now >= target:
target += timedelta(days=1)
sleep_secs = (target - now).total_seconds()
logger.info("[DailyStandup] Nächstes Standup um %s (in %.0f Minuten)", target.strftime('%d.%m.%Y %H:%M'), sleep_secs / 60)
time.sleep(sleep_secs)
trigger_daily_standup()
except Exception as e:
logger.error("[DailyStandup] Fehler: %s", e)
time.sleep(60)
def start_daily_standup():
"""Startet den Daily-Standup-Thread als Daemon."""
t = threading.Thread(target=daily_standup_beat, name='DailyStandup', daemon=True)
t.start()
logger.info("[DailyStandup] Daemon-Thread gestartet.")
# Poller beim App-Start starten # Poller beim App-Start starten
start_email_poller() start_email_poller()
start_task_beat() start_task_beat()
start_orchestrator_beat() start_orchestrator_beat()
start_daily_standup()
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
@ -2653,7 +2307,7 @@ def logout():
@login_required @login_required
def index(): def index():
# Hole die 5 neuesten Tasks aus DB # Hole die 5 neuesten Tasks aus DB
all_tasks = get_tasks(order='desc') all_tasks = get_tasks()
recent_tasks = all_tasks[:5] if all_tasks else [] recent_tasks = all_tasks[:5] if all_tasks else []
return render_template('index.html', agents=AGENTS, recent_tasks=recent_tasks) return render_template('index.html', agents=AGENTS, recent_tasks=recent_tasks)
@ -2786,8 +2440,8 @@ def task_list():
) )
flash(f'Task #{task_id} erstellt!', 'success') flash(f'Task #{task_id} erstellt!', 'success')
# Alle Tasks aus Datenbank holen Neueste zuerst (für UI) # Alle Tasks aus Datenbank holen Neueste zuerst
all_tasks = get_tasks(order='desc') all_tasks = get_tasks()
return render_template('tasks.html', agents=AGENTS, tasks=all_tasks) return render_template('tasks.html', agents=AGENTS, tasks=all_tasks)
@app.route('/tasks/<int:task_id>') @app.route('/tasks/<int:task_id>')
@ -2841,11 +2495,22 @@ def agent_stream():
selected_agent = delegate_to_agent(prompt) selected_agent = delegate_to_agent(prompt)
agent_info = AGENTS.get(selected_agent, {}) agent_info = AGENTS.get(selected_agent, {})
agent_name = agent_info.get('name', selected_agent) agent_name = agent_info.get('name', selected_agent)
system_prompt = get_agent_prompt(selected_agent)
full_prompt = build_agent_prompt(selected_agent, prompt) kb_file = os.path.join(os.path.dirname(__file__), 'agents', 'orchestrator', 'knowledge', 'diversityball_knowledge.md')
if not full_prompt: kb_content = ""
yield f"data: {json.dumps({'type': 'error', 'message': f'Kein System-Prompt für Agent {selected_agent}'})}\n\n" if os.path.exists(kb_file):
return with open(kb_file, 'r', encoding='utf-8') as f:
kb_content = f.read()
full_prompt = f"""## Wissensdatenbank (Diversity-Ball):
{kb_content}
## System-Prompt des Agenten ({agent_name}):
{system_prompt}
## Deine Aufgabe:
{prompt}"""
# Sofort Agent-Info senden # Sofort Agent-Info senden
yield f"data: {json.dumps({'type': 'agent_selected', 'agent': agent_name, 'agent_key': selected_agent})}\n\n" yield f"data: {json.dumps({'type': 'agent_selected', 'agent': agent_name, 'agent_key': selected_agent})}\n\n"
@ -2865,8 +2530,7 @@ def agent_stream():
cwd=work_dir # Agent arbeitet in seinem work-Verzeichnis cwd=work_dir # Agent arbeitet in seinem work-Verzeichnis
) )
# Jede Zeile sofort aus opencode lesen, streamen und akkumulieren # Jede Zeile sofort aus opencode lesen und streamen
response_text = ""
for line in proc.stdout: for line in proc.stdout:
line = line.strip() line = line.strip()
if not line: if not line:
@ -2876,17 +2540,11 @@ def agent_stream():
if event_data.get('part', {}).get('type') == 'text': if event_data.get('part', {}).get('type') == 'text':
text = event_data['part'].get('text', '') text = event_data['part'].get('text', '')
if text: if text:
response_text += text
yield f"data: {json.dumps({'type': 'response_chunk', 'text': text})}\n\n" yield f"data: {json.dumps({'type': 'response_chunk', 'text': text})}\n\n"
except Exception: except Exception:
pass pass
proc.wait() proc.wait()
# XML-Kommandos aus der gesammelten Antwort ausführen
if response_text:
parse_agent_commands(selected_agent, response_text)
yield f"data: {json.dumps({'type': 'complete', 'message': '✓ Fertig'})}\n\n" yield f"data: {json.dumps({'type': 'complete', 'message': '✓ Fertig'})}\n\n"
except Exception as e: except Exception as e:
@ -3650,8 +3308,8 @@ def api_tasks():
return jsonify({'success': True, 'task': new_task}) return jsonify({'success': True, 'task': new_task})
# GET: Alle Tasks aus DB (neueste zuerst für API) # GET: Alle Tasks aus DB
task_list = get_tasks(order='desc') task_list = get_tasks()
return jsonify({'tasks': task_list}) return jsonify({'tasks': task_list})
@ -3810,30 +3468,6 @@ def distribute_tasks():
}) })
@app.route('/api/standup/trigger', methods=['POST'])
@login_required
def api_trigger_standup():
"""Löst das Daily Standup manuell aus (für Tests oder on-demand)."""
def run():
trigger_daily_standup()
threading.Thread(target=run, daemon=True).start()
return jsonify({'success': True, 'message': 'Daily Standup wurde gestartet.'})
@app.route('/api/broadcast', methods=['POST'])
@login_required
def api_broadcast():
"""Verteilt eine neue Information sofort an alle Agenten."""
data = request.get_json()
info = data.get('info', '').strip()
if not info:
return jsonify({'error': 'Kein info-Text übergeben'}), 400
def run():
broadcast_knowledge_update(info, source='manual_broadcast')
threading.Thread(target=run, daemon=True).start()
return jsonify({'success': True, 'message': f'Broadcast gestartet: {info[:80]}'})
@app.route('/api/webhook/deploy', methods=['POST']) @app.route('/api/webhook/deploy', methods=['POST'])
def webhook_deploy(): def webhook_deploy():
"""Gitea Webhook: git pull + restart service on push to main.""" """Gitea Webhook: git pull + restart service on push to main."""

View file

@ -50,14 +50,6 @@
<span class="badge bg-warning ms-1">Agent</span> <span class="badge bg-warning ms-1">Agent</span>
{% elif task.type == 'agent_question' %} {% elif task.type == 'agent_question' %}
<span class="badge bg-warning ms-1">❓ Frage</span> <span class="badge bg-warning ms-1">❓ Frage</span>
{% elif task.type == 'standup' %}
<span class="badge ms-1" style="background-color:#0d9488;">☀ Standup</span>
{% elif task.type == 'standup_reply' %}
<span class="badge ms-1" style="background-color:#0d9488;">💬 Standup-Antwort</span>
{% elif task.type == 'broadcast' %}
<span class="badge ms-1" style="background-color:#b45309;">📡 Broadcast</span>
{% elif task.type == 'action_knowledge' %}
<span class="badge ms-1" style="background-color:#6366f1;">🧠 Wissen</span>
{% endif %} {% endif %}
{% if task.parent_task_id %} {% if task.parent_task_id %}
<span class="badge bg-dark ms-1" title="Sub-Task von #{{ task.parent_task_id }}">↳ #{{ task.parent_task_id }}</span> <span class="badge bg-dark ms-1" title="Sub-Task von #{{ task.parent_task_id }}">↳ #{{ task.parent_task_id }}</span>