Compare commits
No commits in common. "c82ecdd5f1be377ca4e4de9d5f31b08bd4998c49" and "c6ce8a873cdf8a40346df1b0048062dbd6979429" have entirely different histories.
c82ecdd5f1
...
c6ce8a873c
3 changed files with 40 additions and 483 deletions
|
|
@ -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
|
|
||||||
|
|
|
||||||
434
app.py
434
app.py
|
|
@ -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 m.group(1).strip()
|
||||||
return ''
|
return ''
|
||||||
rest = block[m.start(1):]
|
|
||||||
# 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."""
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue