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:
parent
7fe1365ebc
commit
003e591a04
3 changed files with 366 additions and 1 deletions
290
app.py
290
app.py
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue