Add daily standup + knowledge broadcast system

- DailyStandupBeat thread fires at 09:00 every day
- trigger_daily_standup(): messages all team members, orchestrator updates KB + agent reminders
- broadcast_knowledge_update(): distributes any new info to all agents immediately
- parse_agent_commands(): add <update_knowledge> and <update_agent_reminder> XML handlers
- /api/standup/trigger and /api/broadcast routes for manual triggering
- orchestrator systemprompt: standup + broadcast instructions with examples
- tasks.html: badges for standup / broadcast / action_knowledge task types
This commit is contained in:
eric 2026-02-21 19:46:42 +00:00
parent 7fe1365ebc
commit 003e591a04
3 changed files with 366 additions and 1 deletions

290
app.py
View file

@ -1176,6 +1176,64 @@ 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)
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):
"""Erstellt dynamisch einen neuen Agenten."""
agent_dir = os.path.join(AGENTS_BASE_DIR, agent_key)
@ -2286,10 +2344,218 @@ def start_orchestrator_beat():
logger.info("[OrchestratorBeat] Daemon-Thread gestartet.")
# ── DAILY STANDUP ─────────────────────────────────────────────────────────────
def trigger_daily_standup():
"""
Tägliches Standup: Orchestrator fragt alle Team-Members nach Updates
und delegiert anschließend Wissensupdates an alle Agenten.
"""
logger.info("[DailyStandup] Starte tägliches Standup...")
# 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:
create_task(
title=f"Standup-Frage an {name} (Telegram)",
description=f"Telegram-ID: {telegram_id}\nNachricht: {msg}",
agent_key='orchestrator',
task_type='action_telegram',
created_by='system',
parent_task_id=standup_task_id,
)
send_telegram_message(telegram_id, msg)
logger.info("[DailyStandup] Telegram-Standup gesendet an %s (%s)", name, telegram_id)
elif email:
subject = f"Daily Standup {today} — Was gibt's Neues?"
create_task(
title=f"Standup-Frage an {name} (Email)",
description=f"An: {email}\nBetreff: {subject}\n\n{msg}",
agent_key='orchestrator',
task_type='action_email',
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}
Du hast soeben alle Team-Members nach ihren täglichen Updates gefragt.
**Deine Aufgabe jetzt:**
1. Prüfe ob es aktuelle Informationen oder Änderungen gibt (aus deiner Erinnerung, aus Tasks der letzten 24h, oder aus eingegangenen Nachrichten).
2. Falls es wichtige Updates gibt (z.B. Terminänderungen, neue Entscheidungen, Budget-Anpassungen):
- Aktualisiere die Wissensdatenbank mit `<update_knowledge>`
- Delegiere an **jeden** der folgenden Agenten einen Sub-Task damit sie ihre reminders.md aktualisieren:
{agent_list}
3. Falls keine konkreten Updates vorliegen: schreibe eine kurze Zusammenfassung des aktuellen Status und schicke sie per Telegram an Piotr (telegram_id: 1578034974).
**Beispiel für Wissens-Update:**
```
<update_knowledge>
topic: Eventstart
content: Das Event startet um 18:00 Uhr (Stand {today}). Einlass ab 17:30 Uhr.
</update_knowledge>
```
**Beispiel für Agent-Reminder:**
```
<update_agent_reminder>
agent: catering_manager
reminder: WICHTIG ({today}): Eventstart wurde auf 18:00 geändert. Catering-Aufbau muss spätestens um 17:00 abgeschlossen sein.
</update_agent_reminder>
```
**Beispiel für Info-Delegation:**
```
<create_task>
title: Wissensupdate: Eventstart 18:00 Uhr
agent: budget_manager
details: Bitte aktualisiere deinen Wissensstand: Das Event startet am Diversity Ball Wien 2026 um 18:00 Uhr (nicht 19:00). Prüfe ob sich dadurch Änderungen für deinen Bereich ergeben.
</create_task>
```
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
start_email_poller()
start_task_beat()
start_orchestrator_beat()
start_daily_standup()
@app.route('/login', methods=['GET', 'POST'])
@ -3471,6 +3737,30 @@ 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'])
def webhook_deploy():
"""Gitea Webhook: git pull + restart service on push to main."""