Compare commits

..

No commits in common. "320a1d4d879613305d1db8a7909a7e7290262b97" and "632a6c253a76356fd413ac77bccec8ab56da0c57" have entirely different histories.

2 changed files with 146 additions and 353 deletions

109
README.md
View file

@ -15,9 +15,7 @@ Frankenbot ist ein intelligentes Event-Management-System, bei dem spezialisierte
- **Dynamische Modellauswahl**: 29+ KI-Modelle verfügbar (OpenCode, Anthropic, Ollama) - **Dynamische Modellauswahl**: 29+ KI-Modelle verfügbar (OpenCode, Anthropic, Ollama)
### 📋 Task-Management ### 📋 Task-Management
- **Persistente Speicherung**: SQLite-Datenbank für Tasks
- **Automatische Task-Verarbeitung**: TaskBeat verarbeitet Tasks alle 10 Sekunden - **Automatische Task-Verarbeitung**: TaskBeat verarbeitet Tasks alle 10 Sekunden
- **Auto-Cleanup**: Löscht completed Tasks automatisch nach 7 Tagen
- **Auto-Refresh UI**: Tasks-Seite aktualisiert sich automatisch alle 15 Sekunden - **Auto-Refresh UI**: Tasks-Seite aktualisiert sich automatisch alle 15 Sekunden
- **Task-Delegation**: Orchestrator analysiert Tasks und weist sie passenden Agents zu - **Task-Delegation**: Orchestrator analysiert Tasks und weist sie passenden Agents zu
- **Status-Tracking**: pending → in_progress → completed - **Status-Tracking**: pending → in_progress → completed
@ -31,9 +29,7 @@ Frankenbot ist ein intelligentes Event-Management-System, bei dem spezialisierte
- **IMAP**: Email-Posteingang überwachen - **IMAP**: Email-Posteingang überwachen
- **SMTP**: Emails versenden - **SMTP**: Emails versenden
- **Email-to-Task**: Automatische Task-Erstellung aus Emails - **Email-to-Task**: Automatische Task-Erstellung aus Emails
- **Whitelist-System**: Nur autorisierte Absender (diversityball.at Domain) - **Whitelist-System**: Nur autorisierte Absender
- **Intelligente Vorstellung**: Neue Absender werden automatisch gebeten sich vorzustellen
- **Team-Member Lernsystem**: Orchestrator baut automatisch Wissensdatenbank auf
### 📁 Datei-Management ### 📁 Datei-Management
- **Agent Work-Folders**: Jeder Agent hat eigenes Arbeitsverzeichnis - **Agent Work-Folders**: Jeder Agent hat eigenes Arbeitsverzeichnis
@ -51,21 +47,6 @@ Frankenbot ist ein intelligentes Event-Management-System, bei dem spezialisierte
- **On-Demand Zugriff**: Agents nutzen `@READ_KNOWLEDGE` statt volle KB im Prompt - **On-Demand Zugriff**: Agents nutzen `@READ_KNOWLEDGE` statt volle KB im Prompt
- **Performance-Optimierung**: Reduziert Prompt-Größe um ~15KB - **Performance-Optimierung**: Reduziert Prompt-Größe um ~15KB
- **Event-spezifisch**: Diversity-Ball Wien 2026 Informationen - **Event-spezifisch**: Diversity-Ball Wien 2026 Informationen
- **Self-Learning**: System lernt automatisch über Team-Members und Verantwortlichkeiten
### 👥 Team-Management
- **Team-Members Datenbank**: Reale Mitarbeiter mit Rollen und Verantwortlichkeiten
- **Automatisches Lernen**: Neue Absender werden erkannt und vorgestellt
- **Orchestrator Beat**: Prüft alle 30 Minuten Tasks und eskaliert bei Bedarf
- **Direkter Kontakt**: Orchestrator kann Team-Members per Email/Telegram kontaktieren
- **Intelligente Koordination**: Weiß wer wofür verantwortlich ist
### 💬 Telegram-Integration
- **Bidirektionale Kommunikation**: Nachrichten senden und empfangen
- **QR-Code Setup**: Einfache Bot-Verbindung über Settings-Seite
- **Task-Erstellung**: Telegram-Nachrichten werden automatisch zu Tasks
- **Antworten**: Agent-Responses werden zurück zu Telegram gesendet
- **User-Whitelist**: Nur autorisierte Telegram-User
## 🚀 Installation ## 🚀 Installation
@ -138,42 +119,14 @@ Reason: Für große Events mit 500+ Gästen benötigt
@END @END
``` ```
#### Team-Member kontaktieren
```
@SEND_EMAIL
To: georg.tschare@signtime.media
Subject: Budget-Freigabe benötigt
Body: Hallo Georg, für das Catering benötigen wir eine Budget-Freigabe...
@END
```
#### Team-Member aktualisieren
```
@UPDATE_TEAM_MEMBER
Identifier: p.dyderski@live.at
TelegramID: 123456789
Responsibilities: System-Administration, 3D-Visualisierung, AI-Agenten, R&D
@END
```
#### Neues Team-Mitglied hinzufügen
```
@ADD_TEAM_MEMBER
Name: Max Mustermann
Role: Marketing Coordinator
Responsibilities: Social Media, Content Creation
Email: max@diversityball.at
@END
```
## 🗂️ Projekt-Struktur ## 🗂️ Projekt-Struktur
``` ```
frankenbot/ frankenbot/
├── app.py # Haupt-Flask-App (2900+ Zeilen) ├── app.py # Haupt-Flask-App (2128 Zeilen)
├── agent_config.json # Agent-Modell-Zuweisungen ├── agent_config.json # Agent-Modell-Zuweisungen
├── diversityball_knowledge.md # Event-Wissensdatenbank ├── diversityball_knowledge.md # Event-Wissensdatenbank
├── email_journal.db # SQLite-Datenbank (Emails, Tasks, Team-Members) ├── email_journal.db # SQLite-Datenbank (Emails)
├── agents/ # Agent-Verzeichnisse ├── agents/ # Agent-Verzeichnisse
│ ├── orchestrator/ │ ├── orchestrator/
@ -221,11 +174,10 @@ frankenbot/
- **Backend**: Flask 3.x - **Backend**: Flask 3.x
- **Frontend**: Bootstrap 5, Vanilla JavaScript - **Frontend**: Bootstrap 5, Vanilla JavaScript
- **Database**: SQLite (Email-Journal, Tasks, Team-Members) - **Database**: SQLite (Email-Journal)
- **AI**: OpenCode CLI (Multi-Provider Support: Anthropic, Ollama, etc.) - **AI**: OpenCode CLI (Multi-Provider Support)
- **Templates**: Jinja2 - **Templates**: Jinja2
- **Streaming**: Server-Sent Events (SSE) - **Streaming**: Server-Sent Events (SSE)
- **Bot**: python-telegram-bot, QR-Code Generation
## 🎨 UI-Routen ## 🎨 UI-Routen
@ -236,9 +188,8 @@ frankenbot/
| `/orchestrator` | Orchestrator-Chat mit Task-Verteilung | | `/orchestrator` | Orchestrator-Chat mit Task-Verteilung |
| `/tasks` | Task-Management mit Auto-Refresh | | `/tasks` | Task-Management mit Auto-Refresh |
| `/agents` | Agent-Verwaltung (Erstellen, Bearbeiten, Modell-Auswahl) | | `/agents` | Agent-Verwaltung (Erstellen, Bearbeiten, Modell-Auswahl) |
| `/files` | Datei-Manager (Upload, Download, Agent Work-Folders, View) | | `/files` | Datei-Manager (Upload, Download, Agent Work-Folders) |
| `/emails` | Email-Interface (Senden, Empfangen, Vorlagen) | | `/emails` | Email-Interface (Senden, Empfangen, Vorlagen) |
| `/settings` | System-Einstellungen, Telegram QR-Code, Poller-Konfiguration |
## ⚙️ Konfiguration ## ⚙️ Konfiguration
@ -261,25 +212,10 @@ EMAIL_IMAP_SERVER=imap.gmail.com
EMAIL_SMTP_SERVER=smtp.gmail.com EMAIL_SMTP_SERVER=smtp.gmail.com
``` ```
### Telegram-Konfiguration
Bearbeite `.env`:
```bash
TELEGRAM_BOT_TOKEN=your-bot-token-here
TELEGRAM_ALLOWED_USERS=123456789,987654321
TELEGRAM_BOT_USERNAME=your_bot_name_bot
```
Setup-Anleitung:
1. Erstelle Bot via [@BotFather](https://t.me/BotFather)
2. Hole deine User-ID via [@userinfobot](https://t.me/userinfobot)
3. QR-Code scannen auf `/settings` Seite
### Background-Threads ### Background-Threads
- **EmailPoller**: Prüft alle 2 Minuten auf neue Emails - **EmailPoller**: Prüft alle 2 Minuten auf neue Emails
- **TaskBeat**: Verarbeitet Tasks alle 10 Sekunden - **TaskBeat**: Verarbeitet Tasks alle 10 Sekunden
- **TaskWorker**: Führt Agent-Tasks aus (Timeout: 10 Min) - **TaskWorker**: Führt Agent-Tasks aus (Timeout: 10 Min)
- **OrchestratorBeat**: Prüft alle 30 Minuten auf blockierte Tasks ⭐ NEU
- **TelegramBot**: Polling für Telegram-Nachrichten (wenn konfiguriert)
## 📊 Performance ## 📊 Performance
@ -288,9 +224,6 @@ Setup-Anleitung:
- ✅ Model-Cache (1h TTL) - ✅ Model-Cache (1h TTL)
- ✅ Agent arbeiten in eigenen work-Verzeichnissen - ✅ Agent arbeiten in eigenen work-Verzeichnissen
- ✅ Timeout: 600 Sekunden (10 Min) für komplexe Tasks - ✅ Timeout: 600 Sekunden (10 Min) für komplexe Tasks
- ✅ Task-Cleanup: Auto-Löschung nach 7 Tagen
- ✅ Persistente Tasks in SQLite-Datenbank
- ✅ Self-Learning Team-Management
### Geschätzte Ausführungszeiten ### Geschätzte Ausführungszeiten
- Einfache Tasks: 30-60 Sekunden - Einfache Tasks: 30-60 Sekunden
@ -300,12 +233,10 @@ Setup-Anleitung:
## 🔒 Security ## 🔒 Security
### Implementierte Maßnahmen ### Implementierte Maßnahmen
- ✅ XSS-Schutz: HTML-Escaping in allen Templates (orchestrator.html, chat.html) - ✅ XSS-Schutz: HTML-Escaping in allen Templates
- ✅ Email-Whitelist: Nur autorisierte Absender (diversityball.at Domain) - ✅ Email-Whitelist: Nur autorisierte Absender
- ✅ Telegram-Whitelist: Nur autorisierte User-IDs
- ✅ Exception-Handling: Spezifische Exception-Typen mit Logging - ✅ Exception-Handling: Spezifische Exception-Typen mit Logging
- ✅ Request-Validierung: `.get()` mit Defaults - ✅ Request-Validierung: `.get()` mit Defaults
- ✅ Path Traversal Protection: Agent-Files nur aus eigenem work-Verzeichnis
- ✅ Session-basierte Auth: Flask-Sessions - ✅ Session-basierte Auth: Flask-Sessions
### Empfehlungen für Produktion ### Empfehlungen für Produktion
@ -380,23 +311,6 @@ git push origin main
3. TaskBeat weist Task zu passendem Agent 3. TaskBeat weist Task zu passendem Agent
4. Agent arbeitet Task ab und antwortet per Email 4. Agent arbeitet Task ab und antwortet per Email
### Neuer Team-Member (Self-Learning)
1. Email von unbekanntem @diversityball.at Absender
2. System erkennt: "Nicht in Team-DB!"
3. Orchestrator übernimmt automatisch
4. Begrüßt und bittet um Vorstellung (Name, Rolle, Verantwortlichkeiten)
5. Beantwortet auch die eigentliche Anfrage
6. Nutzt @ADD_TEAM_MEMBER zum Speichern
7. Zukünftig: Bessere Koordination durch bekannte Verantwortlichkeiten
### Orchestrator Beat Monitoring
1. Alle 30 Minuten: System-Check
2. Findet Tasks ohne Fortschritt (>2h pending)
3. Findet blockierte Tasks (>4h in_progress)
4. Fragt Orchestrator was zu tun ist
5. Orchestrator entscheidet ob Eskalation nötig
6. Bei Bedarf: @SEND_EMAIL an verantwortliches Team-Mitglied
### Wissensdatenbank-Abfrage ### Wissensdatenbank-Abfrage
1. Agent braucht Info (z.B. Budget) 1. Agent braucht Info (z.B. Budget)
2. Nutzt `@READ_KNOWLEDGE Topic: Budget` 2. Nutzt `@READ_KNOWLEDGE Topic: Budget`
@ -413,7 +327,6 @@ Dieses Projekt ist für internen Gebrauch und Event-Management entwickelt.
--- ---
**Version**: 2.0.0 **Version**: 1.0.0
**Letztes Update**: 21. Februar 2026 **Letztes Update**: Februar 2026
**Status**: Production-Ready ✅ **Status**: Production-Ready ✅
**Neue Features**: Self-Learning Team-Management, Orchestrator Beat, Task-Persistierung, Telegram-Integration

390
app.py
View file

@ -884,9 +884,10 @@ def parse_agent_commands(agent_key, response_text):
) )
for question, context in ask_requests: for question, context in ask_requests:
# Erstelle Task für Orchestrator um die Frage zu beantworten # Erstelle Task für Orchestrator um die Frage zu beantworten
task_id = create_task( new_task = {
title=f"Frage von {agent_key}: {question.strip()[:80]}", 'id': len(tasks) + 1,
description=f"""Ein Agent braucht Hilfe! 'title': f"Frage von {agent_key}: {question.strip()[:80]}",
'description': f"""Ein Agent braucht Hilfe!
**Von:** {agent_key} **Von:** {agent_key}
**Frage:** {question.strip()} **Frage:** {question.strip()}
@ -894,13 +895,16 @@ def parse_agent_commands(agent_key, response_text):
Bitte beantworte die Frage oder delegiere an den passenden Experten-Agent. Bitte beantworte die Frage oder delegiere an den passenden Experten-Agent.
Die Antwort wird an {agent_key} zurückgegeben.""", Die Antwort wird an {agent_key} zurückgegeben.""",
agent_key='orchestrator', 'assigned_agent': 'Orchestrator',
task_type='agent_question', 'agent_key': 'orchestrator',
created_by=agent_key, 'status': 'pending',
from_agent=agent_key, 'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
return_to=agent_key 'type': 'agent_question',
) 'from_agent': agent_key,
logger.info(f"[AgentCmd] {agent_key} fragt Orchestrator (Task #{task_id}): {question.strip()[:50]}") '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 # CREATE_SUBTASK: Agent möchte Subtask erstellen
subtask_requests = re.findall( subtask_requests = re.findall(
@ -909,15 +913,19 @@ Die Antwort wird an {agent_key} zurückgegeben.""",
re.DOTALL re.DOTALL
) )
for task_desc, requirements in subtask_requests: for task_desc, requirements in subtask_requests:
task_id = create_task( new_task = {
title=task_desc.strip()[:100], 'id': len(tasks) + 1,
description=f"Von {agent_key} angefordert:\n{requirements.strip()}", 'title': task_desc.strip()[:100],
agent_key='orchestrator', 'description': f"Von {agent_key} angefordert:\n{requirements.strip()}",
task_type='agent_subtask', 'assigned_agent': 'Orchestrator',
created_by=agent_key, 'agent_key': 'orchestrator',
from_agent=agent_key 'status': 'pending',
) 'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
logger.info(f"[AgentCmd] {agent_key} erstellt Subtask #{task_id} via Orchestrator") '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_AGENT: Agent schlägt neuen Agent vor
suggest_requests = re.findall( suggest_requests = re.findall(
@ -930,24 +938,28 @@ Die Antwort wird an {agent_key} zurückgegeben.""",
agent_key_suggestion = role.lower().replace(' ', '_').replace('-', '_') agent_key_suggestion = role.lower().replace(' ', '_').replace('-', '_')
# Task für Orchestrator um Agent zu erstellen # Task für Orchestrator um Agent zu erstellen
task_id = create_task( new_task = {
title=f"Agent-Vorschlag: {role.strip()}", 'id': len(tasks) + 1,
description=f"""Agent {agent_key} schlägt vor, einen neuen Agent zu erstellen: 'title': f"Agent-Vorschlag: {role.strip()}",
'description': f"""Agent {agent_key} schlägt vor, einen neuen Agent zu erstellen:
**Rolle:** {role.strip()} **Rolle:** {role.strip()}
**Fähigkeiten:** {skills.strip()} **Fähigkeiten:** {skills.strip()}
**Begründung:** {reason.strip()} **Begründung:** {reason.strip()}
Bitte entscheide ob dieser Agent erstellt werden soll.""", Bitte entscheide ob dieser Agent erstellt werden soll.""",
agent_key='orchestrator', 'assigned_agent': 'Orchestrator',
task_type='agent_suggestion', 'agent_key': 'orchestrator',
created_by=agent_key, 'status': 'pending',
from_agent=agent_key, 'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
suggested_agent=agent_key_suggestion, 'type': 'agent_suggestion',
suggested_role=role.strip(), 'from_agent': agent_key,
suggested_skills=skills.strip() 'suggested_agent': agent_key_suggestion,
) 'suggested_role': role.strip(),
logger.info(f"[AgentCmd] {agent_key} schlägt neuen Agent vor (Task #{task_id}): {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_KNOWLEDGE: Agent möchte Wissensdatenbank durchsuchen
read_kb_requests = re.findall( read_kb_requests = re.findall(
@ -977,124 +989,6 @@ Bitte entscheide ob dieser Agent erstellt werden soll.""",
if relevant_sections: if relevant_sections:
logger.info(f"[AgentCmd] {len(relevant_sections)} relevante Zeilen gefunden") logger.info(f"[AgentCmd] {len(relevant_sections)} relevante Zeilen gefunden")
# SEND_EMAIL: Orchestrator sendet Email an Team-Member
send_email_requests = re.findall(
r'@SEND_EMAIL\s*\nTo:\s*([^\n]+)\s*\nSubject:\s*([^\n]+)\s*\nBody:\s*([^@]+)@END',
response_text,
re.DOTALL
)
for to_addr, subject, body in send_email_requests:
to_clean = to_addr.strip()
subject_clean = subject.strip()
body_clean = body.strip()
# Versuche Email zu senden
success, message = send_email(to_clean, subject_clean, body_clean)
if success:
logger.info(f"[AgentCmd] Email gesendet an {to_clean}: {subject_clean}")
else:
logger.error(f"[AgentCmd] Email-Fehler: {message}")
# SEND_TELEGRAM: Orchestrator sendet Telegram-Nachricht
send_telegram_requests = re.findall(
r'@SEND_TELEGRAM\s*\nTo:\s*([^\n]+)\s*\nMessage:\s*([^@]+)@END',
response_text,
re.DOTALL
)
for recipient, message in send_telegram_requests:
recipient_clean = recipient.strip()
message_clean = message.strip()
# Telegram-Integration (wenn aktiviert)
if TELEGRAM_CONFIG.get('bot_token') and TELEGRAM_CONFIG.get('telegram_bot'):
try:
# Finde Chat-ID für Recipient (basierend auf Team-Member)
con = sqlite3.connect(EMAIL_JOURNAL_DB)
result = con.execute(
"SELECT telegram_chat_id FROM team_members WHERE name = ? OR email = ?",
(recipient_clean, recipient_clean)
).fetchone()
con.close()
if result and result[0]:
chat_id = result[0]
import asyncio
asyncio.run(TELEGRAM_CONFIG['telegram_bot'].bot.send_message(
chat_id=chat_id,
text=message_clean
))
logger.info(f"[AgentCmd] Telegram gesendet an {recipient_clean}")
else:
logger.warning(f"[AgentCmd] Keine Telegram Chat-ID für {recipient_clean}")
except Exception as e:
logger.error(f"[AgentCmd] Telegram-Fehler: {str(e)}")
else:
logger.warning("[AgentCmd] Telegram nicht konfiguriert")
# ADD_TEAM_MEMBER: Füge neues Team-Mitglied hinzu
add_member_requests = re.findall(
r'@ADD_TEAM_MEMBER\s*\nName:\s*([^\n]+)\s*\nEmail:\s*([^\n]+)\s*\nRole:\s*([^\n]+)\s*\nResponsibilities:\s*([^@]+)@END',
response_text,
re.DOTALL
)
for name, email, role, resp in add_member_requests:
name_clean = name.strip()
email_clean = email.strip()
role_clean = role.strip()
resp_clean = resp.strip()
success = add_team_member(name_clean, role_clean, email_clean, resp_clean)
if success:
logger.info(f"[AgentCmd] Team-Member hinzugefügt: {name_clean} ({role_clean})")
else:
logger.warning(f"[AgentCmd] Team-Member konnte nicht hinzugefügt werden: {name_clean}")
# UPDATE_TEAM_MEMBER: Aktualisiere Team-Mitglied
update_member_requests = re.findall(
r'@UPDATE_TEAM_MEMBER\s*\nEmail:\s*([^\n]+)\s*\nField:\s*([^\n]+)\s*\nValue:\s*([^@]+)@END',
response_text,
re.DOTALL
)
for email, field, value in update_member_requests:
email_clean = email.strip()
field_clean = field.strip().lower()
value_clean = value.strip()
# Hole aktuelles Team-Member
con = sqlite3.connect(EMAIL_JOURNAL_DB)
member = con.execute(
"SELECT name, role, email, responsibilities FROM team_members WHERE LOWER(email) = ?",
(email_clean.lower(),)
).fetchone()
if member:
# Update je nach Field
updates = {
'name': member[0],
'role': member[1],
'email': member[2],
'responsibilities': member[3]
}
if field_clean in updates:
updates[field_clean] = value_clean
success = update_team_member(
email_clean,
updates['name'],
updates['role'],
updates['responsibilities']
)
if success:
logger.info(f"[AgentCmd] Team-Member aktualisiert: {email_clean} - {field_clean}")
else:
logger.error(f"[AgentCmd] Update fehlgeschlagen für {email_clean}")
else:
logger.warning(f"[AgentCmd] Unbekanntes Field: {field_clean}")
else:
logger.warning(f"[AgentCmd] Team-Member nicht gefunden: {email_clean}")
con.close()
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."""
@ -1448,15 +1342,24 @@ async def telegram_message_handler(update: Update, context: ContextTypes.DEFAULT
return return
# Task erstellen # Task erstellen
task_id = create_task( task_id = len(tasks) + 1
title=f"Telegram: {message_text[:50]}{'...' if len(message_text) > 50 else ''}", while any(t['id'] == task_id for t in tasks):
description=message_text, task_id += 1
agent_key='orchestrator',
task_type='telegram', new_task = {
created_by=f'telegram_user_{user_id}', 'id': task_id,
telegram_chat_id=update.effective_chat.id, 'title': f"Telegram: {message_text[:50]}{'...' if len(message_text) > 50 else ''}",
telegram_user=username 'description': message_text,
) 'assigned_agent': 'Orchestrator',
'agent_key': 'orchestrator',
'status': 'pending',
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
'type': 'telegram',
'created_by': f'telegram_user_{user_id}',
'telegram_chat_id': update.effective_chat.id,
'telegram_user': username
}
tasks.append(new_task)
await update.message.reply_text( await update.message.reply_text(
f"✅ Task #{task_id} erstellt!\n\n" f"✅ Task #{task_id} erstellt!\n\n"
@ -1697,26 +1600,9 @@ Diese Email kommt von **{known_member[0]}** ({known_member[1]})
# Journal: Status 'queued' \Seen wird NICHT gesetzt (erst nach Verarbeitung) # Journal: Status 'queued' \Seen wird NICHT gesetzt (erst nach Verarbeitung)
journal_insert(message_id, email_id.decode(), sender, subject, 'queued', agent_key) journal_insert(message_id, email_id.decode(), sender, subject, 'queued', agent_key)
# Task erstellen in DB # Task erstellen und in beide Listen eintragen
task_id = create_task(
title=f"📧 Email: {subject[:50]}",
description=body[:500],
agent_key=agent_key,
task_type='email',
created_by=sender_email,
reply_to=sender_email,
reply_subject=f"Re: {subject}" if not subject.startswith('Re:') else subject,
original_sender=sender,
original_subject=subject,
original_body=body,
message_id=message_id,
imap_uid=email_id.decode(),
extra_context=extra_context
)
# Für task_queue brauchen wir Task als Dict (Legacy-Kompatibilität)
task = { task = {
'id': task_id, 'id': len(tasks) + 1,
'title': f"📧 Email: {subject[:50]}", 'title': f"📧 Email: {subject[:50]}",
'description': body[:500], 'description': body[:500],
'assigned_agent': AGENTS.get(agent_key, {}).get('name', agent_key), 'assigned_agent': AGENTS.get(agent_key, {}).get('name', agent_key),
@ -1729,11 +1615,11 @@ Diese Email kommt von **{known_member[0]}** ({known_member[1]})
'original_sender': sender, 'original_sender': sender,
'original_subject': subject, 'original_subject': subject,
'original_body': body, 'original_body': body,
'message_id': message_id, 'message_id': message_id, # für Journal-Update durch TaskWorker
'imap_uid': email_id.decode(), 'imap_uid': email_id.decode(),
'extra_context': extra_context, 'extra_context': extra_context, # Für unbekannte Absender
} }
tasks.append(task)
with task_queue_lock: with task_queue_lock:
task_queue.append(task) task_queue.append(task)
@ -1741,7 +1627,7 @@ Diese Email kommt von **{known_member[0]}** ({known_member[1]})
log_entry['status'] = 'queued' log_entry['status'] = 'queued'
add_to_email_log(log_entry) add_to_email_log(log_entry)
logger.info("[EmailPoller] Task #%d für Email von %s in Queue eingereiht.", task_id, sender_email) logger.info("[EmailPoller] Task #%d für Email von %s in Queue eingereiht.", task['id'], sender_email)
except Exception as e: except Exception as e:
logger.error("[EmailPoller] Fehler bei Email-ID %s: %s", email_id, str(e)) logger.error("[EmailPoller] Fehler bei Email-ID %s: %s", email_id, str(e))
@ -1931,9 +1817,10 @@ Agent-Beschreibungen:
assigned = available_agents[0] assigned = available_agents[0]
# Sub-Task mit KOMPLETTEM Kontext # Sub-Task mit KOMPLETTEM Kontext
sub_task_id = create_task( sub_task = {
title=t[:80], 'id': len(tasks) + 1 + len(created_sub_tasks),
description=f"""**Original-Aufgabe:** 'title': t[:80],
'description': f"""**Original-Aufgabe:**
{task.get('title', '')} {task.get('title', '')}
{task.get('description', '')} {task.get('description', '')}
@ -1945,13 +1832,16 @@ Agent-Beschreibungen:
{t} {t}
Arbeite diesen Teil ab und liefere ein vollständiges Ergebnis.""", Arbeite diesen Teil ab und liefere ein vollständiges Ergebnis.""",
agent_key=assigned, 'assigned_agent': AGENTS.get(assigned, {}).get('name', assigned),
task_type='orchestrated', 'agent_key': assigned,
created_by='orchestrator', 'status': 'pending',
parent_task_id=task['id'] 'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
) 'type': 'orchestrated',
created_sub_tasks.append(sub_task_id) 'parent_task': task['id']
logger.info("[TaskBeat] Sub-Task #%d zugewiesen an %s", sub_task_id, assigned) }
tasks.append(sub_task)
created_sub_tasks.append(sub_task['id'])
logger.info("[TaskBeat] Sub-Task #%d zugewiesen an %s", sub_task['id'], assigned)
task['status'] = 'completed' task['status'] = 'completed'
task['sub_task_ids'] = created_sub_tasks task['sub_task_ids'] = created_sub_tasks
@ -2092,12 +1982,12 @@ Nutze @READ_KNOWLEDGE um Kontext zu holen falls nötig.
logger.info("[OrchestratorBeat] Frage Orchestrator zu %d Problemen", len(old_pending) + len(stuck_tasks)) logger.info("[OrchestratorBeat] Frage Orchestrator zu %d Problemen", len(old_pending) + len(stuck_tasks))
response = execute_agent_task('orchestrator', summary) response = execute_agent_task('orchestrator', summary)
# Response loggen und Kommandos parsen # Response loggen
if response: if response:
logger.info("[OrchestratorBeat] Orchestrator-Antwort: %s", response[:200]) logger.info("[OrchestratorBeat] Orchestrator-Antwort: %s", response[:200])
# Parse Orchestrator-Kommandos (Email, Telegram, etc.) # TODO: Parse @SEND_EMAIL und @SEND_TELEGRAM Kommandos
parse_agent_commands('orchestrator', response) # und sende tatsächlich Benachrichtigungen
except Exception as e: except Exception as e:
logger.error("[OrchestratorBeat] Fehler: %s", str(e)) logger.error("[OrchestratorBeat] Fehler: %s", str(e))
@ -2118,9 +2008,7 @@ start_orchestrator_beat()
@app.route('/') @app.route('/')
def index(): def index():
# Hole die 5 neuesten Tasks aus DB recent_tasks = tasks[-5:] if tasks else []
all_tasks = get_tasks()
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)
@app.route('/chat', methods=['GET', 'POST']) @app.route('/chat', methods=['GET', 'POST'])
@ -2201,30 +2089,28 @@ def task_list():
assigned_agent = request.form.get('assigned_agent', '') assigned_agent = request.form.get('assigned_agent', '')
if title: if title:
task_id = create_task( task = {
title=title, 'id': len(tasks) + 1,
description=description, 'title': title,
agent_key=assigned_agent if assigned_agent else None, 'description': description,
task_type='manual', 'assigned_agent': AGENTS.get(assigned_agent, {}).get('name', assigned_agent) if assigned_agent else 'Nicht zugewiesen',
created_by='user' 'status': 'pending',
) 'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
flash(f'Task #{task_id} erstellt!', 'success') 'type': 'manual',
}
tasks.append(task)
flash('Task erstellt!', 'success')
# Alle Tasks aus Datenbank holen Neueste zuerst # Alle Tasks anzeigen Email-Tasks sind mit type='email' markiert
all_tasks = get_tasks() all_tasks = list(reversed(tasks)) # Neueste zuerst
return render_template('tasks.html', agents=AGENTS, tasks=all_tasks) return render_template('tasks.html', agents=AGENTS, tasks=all_tasks)
@app.route('/tasks/update/<int:task_id>/<status>') @app.route('/tasks/update/<int:task_id>/<status>')
def update_task(task_id, status): def update_task(task_id, status):
# Update in Datenbank
update_task_db(task_id, status=status)
# Auch in-memory array aktualisieren (Legacy-Kompatibilität für task_queue)
for task in tasks: for task in tasks:
if task['id'] == task_id: if task['id'] == task_id:
task['status'] = status task['status'] = status
break break
return redirect(url_for('task_list')) return redirect(url_for('task_list'))
@app.route('/api/agent-stream', methods=['POST']) @app.route('/api/agent-stream', methods=['POST'])
@ -2812,35 +2698,26 @@ def api_tasks():
if not title: if not title:
return jsonify({'error': 'Kein Titel übergeben'}), 400 return jsonify({'error': 'Kein Titel übergeben'}), 400
task_id = create_task( task_id = len(tasks) + 1
title=title, while any(t['id'] == task_id for t in tasks):
description=description, task_id += 1
agent_key=agent_key or assigned_agent or 'orchestrator',
task_type='agent_created',
created_by=agent_key or 'api'
)
# Task aus DB holen für Response
con = sqlite3.connect(EMAIL_JOURNAL_DB)
task_row = con.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
con.close()
new_task = { new_task = {
'id': task_id, 'id': task_id,
'title': title, 'title': title,
'description': description, 'description': description,
'assigned_agent': AGENTS.get(agent_key or assigned_agent, {}).get('name', agent_key or assigned_agent) if (agent_key or assigned_agent) else 'Nicht zugewiesen', 'assigned_agent': AGENTS.get(assigned_agent, {}).get('name', assigned_agent) if assigned_agent else 'Nicht zugewiesen',
'agent_key': agent_key or assigned_agent or 'orchestrator', 'agent_key': agent_key or assigned_agent,
'status': 'pending', 'status': 'pending',
'created': datetime.now().strftime('%Y-%m-%d %H:%M'), 'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
'type': 'agent_created', 'type': 'agent_created',
'created_by': agent_key or 'api' 'created_by': agent_key
} }
tasks.append(new_task)
return jsonify({'success': True, 'task': new_task}) return jsonify({'success': True, 'task': new_task})
# GET: Alle Tasks aus DB task_list = list(reversed(tasks))
task_list = get_tasks()
return jsonify({'tasks': task_list}) return jsonify({'tasks': task_list})
@ -2850,17 +2727,12 @@ def update_task_api(task_id):
data = request.get_json() data = request.get_json()
new_status = data.get('status') new_status = data.get('status')
# Update in DB
update_task_db(task_id, status=new_status)
# Auch in-memory aktualisieren (Legacy)
for task in tasks: for task in tasks:
if task['id'] == task_id: if task['id'] == task_id:
task['status'] = new_status task['status'] = new_status
return jsonify({'success': True, 'task': task}) return jsonify({'success': True, 'task': task})
# Falls nur in DB vorhanden return jsonify({'error': 'Task nicht gefunden'}), 404
return jsonify({'success': True, 'task_id': task_id, 'status': new_status})
@app.route('/api/models', methods=['GET']) @app.route('/api/models', methods=['GET'])
@ -2945,35 +2817,43 @@ def distribute_tasks():
tasks_text = '\n'.join([f"- {t}" for t in tasks_list]) tasks_text = '\n'.join([f"- {t}" for t in tasks_list])
agents_text = ', '.join(selected_agents) if selected_agents else 'alle verfügbaren Agenten' agents_text = ', '.join(selected_agents) if selected_agents else 'alle verfügbaren Agenten'
planning_task_id = create_task( planning_task = {
title=f"Planungsphase: {tasks_list[0][:50]}{'...' if len(tasks_list[0]) > 50 else ''}", 'id': len(tasks) + 1,
description=f"Tasks:\n{tasks_text}\n\nVerfügbare Agenten: {agents_text}\n\nDer Orchestrator soll diese Tasks analysieren und den richtigen Agenten zuweisen.", 'title': f"Planungsphase: {tasks_list[0][:50]}{'...' if len(tasks_list[0]) > 50 else ''}",
agent_key='orchestrator', 'description': f"Tasks:\n{tasks_text}\n\nVerfügbare Agenten: {agents_text}\n\nDer Orchestrator soll diese Tasks analysieren und den richtigen Agenten zuweisen.",
task_type='orchestrated', 'assigned_agent': 'Orchestrator',
created_by='orchestrator', 'agent_key': 'orchestrator',
sub_tasks=tasks_list, 'status': 'pending',
available_agents=selected_agents 'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
) 'type': 'orchestrated',
'sub_tasks': tasks_list,
'available_agents': selected_agents
}
tasks.append(planning_task)
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': f'Planungs-Task erstellt. Der Orchestrator wird die richtigen Agenten zuweisen.', 'message': f'Planungs-Task erstellt. Der Orchestrator wird die richtigen Agenten zuweisen.',
'tasks': [planning_task_id] 'tasks': [planning_task['id']]
}) })
for i, task_text in enumerate(tasks_list): for i, task_text in enumerate(tasks_list):
agent_key = selected_agents[i % len(selected_agents)] agent_key = selected_agents[i % len(selected_agents)]
agent_info = AGENTS.get(agent_key, {})
agent_name = agent_info.get('name', agent_key)
task_id = create_task( new_task = {
title=task_text[:80], 'id': len(tasks) + 1 + i,
description='Automatisch erstellt durch Orchestrator', 'title': task_text[:80],
agent_key=agent_key, 'description': 'Automatisch erstellt durch Orchestrator',
task_type='orchestrated', 'assigned_agent': agent_name,
created_by='orchestrator' 'status': 'in_progress',
) 'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
# Setze Status sofort auf in_progress 'type': 'orchestrated',
update_task_db(task_id, status='in_progress') 'agent_key': agent_key
created_tasks.append(task_id) }
tasks.append(new_task)
created_tasks.append(new_task['id'])
executor = concurrent.futures.ThreadPoolExecutor(max_workers=len(selected_agents)) executor = concurrent.futures.ThreadPoolExecutor(max_workers=len(selected_agents))
for i, task_text in enumerate(tasks_list): for i, task_text in enumerate(tasks_list):