feat: Add Telegram bot integration and task detail/delete UI

- Wire up Telegram bot with token, allowed users and username from .env
- Fix TaskBeat to handle direct tasks (Telegram/email) without sub_tasks
- Fix send_telegram_message to use run_coroutine_threadsafe (avoid event loop clash)
- Add TaskBeat watchdog thread for auto-restart on crash
- Reset stuck in_progress tasks on startup
- Add task detail page (/tasks/<id>) with full response/log view and auto-refresh
- Add task delete route (/tasks/delete/<id>) with confirmation
- Include agent sender info in Telegram task prompts
- Orchestrator self-updated knowledge base with Telegram contact info
This commit is contained in:
eric 2026-02-21 18:14:43 +00:00
parent 5b4b698064
commit 99df910497
4 changed files with 237 additions and 34 deletions

View file

@ -153,6 +153,16 @@ agents/
--- ---
## 👤 Team-Kontakte (Telegram)
| Name | Telegram-Handle | Telegram-ID | Rolle |
|------|----------------|-------------|-------|
| **Piotr Dyderski** | @awesomepjot | 1578034974 | Tech, 3D Art, RnD |
> Nachrichten von Telegram-ID 1578034974 kommen von **Piotr Dyderski**. Mit seinem Namen ansprechen und bei Bedarf direkt informieren.
---
## 📚 Quellen ## 📚 Quellen
- Researcher: Diversity-Trends, Barrierefreiheit, Steuerrecht - Researcher: Diversity-Trends, Barrierefreiheit, Steuerrecht

113
app.py
View file

@ -1555,14 +1555,23 @@ def send_telegram_message(chat_id: int, message: str):
return False return False
try: try:
# Async-Funktion in sync Context ausführen
import asyncio import asyncio
# In die laufende Event-Loop des Telegram-Threads senden (thread-safe)
loop = telegram_app.updater.application.updater._network_loop if hasattr(telegram_app, 'updater') else None
if loop is None:
# Fallback: Loop des Telegram-Threads direkt holen
loop = telegram_app.loop if hasattr(telegram_app, 'loop') else None
if loop and loop.is_running():
future = asyncio.run_coroutine_threadsafe(
telegram_app.bot.send_message(chat_id=chat_id, text=message),
loop
)
future.result(timeout=30)
else:
# Letzter Fallback: eigene Loop (kein Telegram-Bot läuft)
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) loop.run_until_complete(telegram_app.bot.send_message(chat_id=chat_id, text=message))
loop.run_until_complete(telegram_app.bot.send_message(
chat_id=chat_id,
text=message
))
loop.close() loop.close()
return True return True
except Exception as e: except Exception as e:
@ -1604,7 +1613,7 @@ def run_telegram_bot():
try: try:
logging.info("[Telegram] Starting bot polling...") logging.info("[Telegram] Starting bot polling...")
telegram_app.run_polling(drop_pending_updates=True) telegram_app.run_polling(drop_pending_updates=True, stop_signals=None)
except Exception as e: except Exception as e:
logging.error(f"[Telegram] Bot polling error: {e}") logging.error(f"[Telegram] Bot polling error: {e}")
@ -1979,20 +1988,49 @@ def process_beat_tasks():
agent_key = task.get('agent_key') or task.get('assigned_agent', '') agent_key = task.get('agent_key') or task.get('assigned_agent', '')
if task.get('agent_key') == 'orchestrator': if task.get('agent_key') == 'orchestrator':
# Update in DB
update_task_db(task['id'], status='in_progress')
task['status'] = 'in_progress'
logger.info("[TaskBeat] Planungsphase für Task #%d", task['id'])
sub_tasks = task.get('sub_tasks', []) sub_tasks = task.get('sub_tasks', [])
available_agents = task.get('available_agents', list(AGENTS.keys())) available_agents = task.get('available_agents', list(AGENTS.keys()))
# Falls keine sub_tasks: Task ist fehlerhaft, markiere als completed # Direkte Tasks (Telegram, Email, etc.) ohne sub_tasks → direkt ausführen
if not sub_tasks: if not sub_tasks:
logger.warning("[TaskBeat] Task #%d hat keine sub_tasks - als completed markiert", task['id']) update_task_db(task['id'], status='in_progress')
update_task_db(task['id'], status='completed', response='Fehler: Keine sub_tasks definiert. Dieser Task wurde wahrscheinlich über eine veraltete API erstellt.') task['status'] = 'in_progress'
logger.info("[TaskBeat] Direkte Ausführung für Task #%d (kein sub_tasks)", task['id'])
sender_info = ''
if task.get('type') == 'telegram':
sender_info = (
f"\n\n[Eingehende Telegram-Nachricht]\n"
f"Von: {task.get('telegram_user', 'Unbekannt')} "
f"(Telegram-ID: {task.get('telegram_chat_id', 'N/A')})\n"
f"Created by: {task.get('created_by', 'N/A')}\n"
)
response = execute_agent_task('orchestrator', task.get('title', '') + '\n\n' + task.get('description', '') + sender_info)
update_task_db(task['id'], status='completed', response=response)
task['status'] = 'completed'
task['response'] = response
logger.info("[TaskBeat] Task #%d abgeschlossen.", task['id'])
# Telegram-Antwort senden
if task.get('type') == 'telegram' and task.get('telegram_chat_id'):
try:
telegram_msg = (
f"✅ Task #{task['id']} abgeschlossen!\n\n"
f"📝 Anfrage: {task.get('title', 'N/A')}\n\n"
f"💬 Antwort:\n{response[:4000]}"
)
send_telegram_message(task['telegram_chat_id'], telegram_msg)
logger.info("[TaskBeat] Telegram-Antwort gesendet für Task #%d", task['id'])
except Exception as e:
logger.error("[TaskBeat] Fehler beim Senden der Telegram-Antwort: %s", str(e))
continue continue
# Planungsphase für Tasks mit sub_tasks
update_task_db(task['id'], status='in_progress')
task['status'] = 'in_progress'
logger.info("[TaskBeat] Planungsphase für Task #%d", task['id'])
prompt = f"""Du bist der Master-Orchestrator. Analysiere folgende Tasks und weise sie den richtigen Agenten zu: prompt = f"""Du bist der Master-Orchestrator. Analysiere folgende Tasks und weise sie den richtigen Agenten zu:
Tasks: Tasks:
@ -2113,10 +2151,31 @@ Arbeite diesen Teil ab und liefere ein vollständiges Ergebnis.""",
def start_task_beat(): def start_task_beat():
"""Startet den Task-Beat als Daemon-Thread.""" """Startet den Task-Beat als Daemon-Thread mit Watchdog."""
# Stuck in_progress Tasks beim Start zurücksetzen
try:
con = sqlite3.connect(EMAIL_JOURNAL_DB)
stuck = con.execute("SELECT id FROM tasks WHERE status='in_progress'").fetchall()
if stuck:
con.execute("UPDATE tasks SET status='pending', completed_at=NULL WHERE status='in_progress'")
con.commit()
logger.warning("[TaskBeat] %d stuck in_progress Task(s) auf pending zurückgesetzt.", len(stuck))
con.close()
except Exception as e:
logger.error("[TaskBeat] Fehler beim Reset stuck Tasks: %s", str(e))
def watchdog():
while True:
beat_thread = threading.Thread(target=process_beat_tasks, name='TaskBeat', daemon=True) beat_thread = threading.Thread(target=process_beat_tasks, name='TaskBeat', daemon=True)
beat_thread.start() beat_thread.start()
logger.info("[TaskBeat] Daemon-Thread gestartet.") logger.info("[TaskBeat] Daemon-Thread gestartet.")
beat_thread.join() # Warte bis Thread stirbt
logger.error("[TaskBeat] Thread unerwartet beendet - starte neu in 5s...")
time.sleep(5)
watchdog_thread = threading.Thread(target=watchdog, name='TaskBeatWatchdog', daemon=True)
watchdog_thread.start()
logger.info("[TaskBeat] Watchdog gestartet.")
# ── Orchestrator Beat ─────────────────────────────────────────────────────── # ── Orchestrator Beat ───────────────────────────────────────────────────────
@ -2390,6 +2449,28 @@ def task_list():
all_tasks = get_tasks() 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>')
@login_required
def task_detail(task_id):
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.row_factory = sqlite3.Row
task = con.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
con.close()
if not task:
flash(f'Task #{task_id} nicht gefunden.', 'danger')
return redirect(url_for('task_list'))
return render_template('task_detail.html', task=dict(task), agents=AGENTS)
@app.route('/tasks/delete/<int:task_id>', methods=['POST'])
@login_required
def delete_task(task_id):
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
con.commit()
con.close()
flash(f'Task #{task_id} gelöscht.', 'success')
return redirect(url_for('task_list'))
@app.route('/tasks/update/<int:task_id>/<status>') @app.route('/tasks/update/<int:task_id>/<status>')
@login_required @login_required
def update_task(task_id, status): def update_task(task_id, status):

117
templates/task_detail.html Normal file
View file

@ -0,0 +1,117 @@
{% extends "base.html" %}
{% block title %}Task #{{ task.id }}{% endblock %}
{% block content %}
<div class="page-header d-flex justify-content-between align-items-start flex-wrap gap-2">
<div>
<a href="{{ url_for('task_list') }}" style="color:var(--text-muted);font-size:.85rem;">&larr; Zurück zu Tasks</a>
<h1 class="mt-1">Task #{{ task.id }}</h1>
<p style="color:var(--text-muted);">{{ task.title }}</p>
</div>
<form method="POST" action="{{ url_for('delete_task', task_id=task.id) }}" onsubmit="return confirm('Task #{{ task.id }} wirklich löschen?')">
<button type="submit" class="btn btn-danger">Löschen</button>
</form>
</div>
<div class="row g-4">
<!-- Meta-Infos -->
<div class="col-12 col-md-4">
<div class="card h-100">
<div class="card-header bg-primary">
<h5 class="mb-0">Details</h5>
</div>
<div class="card-body">
<table class="table table-sm mb-0" style="font-size:.85rem;">
<tbody>
<tr><th style="color:var(--text-muted);white-space:nowrap;">Status</th><td>
{% if task.status == 'pending' %}
<span class="badge bg-warning">Pending</span>
{% elif task.status == 'in_progress' %}
<span class="badge bg-primary">In Progress</span>
{% elif task.status == 'completed' %}
<span class="badge bg-success">Done</span>
{% elif task.status == 'error' %}
<span class="badge bg-danger">Fehler</span>
{% else %}
<span class="badge bg-secondary">{{ task.status }}</span>
{% endif %}
</td></tr>
<tr><th style="color:var(--text-muted);">Typ</th><td><code>{{ task.type or '—' }}</code></td></tr>
<tr><th style="color:var(--text-muted);">Agent</th><td>{{ task.assigned_agent or task.agent_key or '—' }}</td></tr>
<tr><th style="color:var(--text-muted);">Erstellt von</th><td><code>{{ task.created_by or '—' }}</code></td></tr>
<tr><th style="color:var(--text-muted);">Erstellt</th><td>{{ task.created_at or '—' }}</td></tr>
<tr><th style="color:var(--text-muted);">Abgeschlossen</th><td>{{ task.completed_at or '—' }}</td></tr>
{% if task.telegram_chat_id %}
<tr><th style="color:var(--text-muted);">Telegram</th><td>{{ task.telegram_user or '' }} <code>{{ task.telegram_chat_id }}</code></td></tr>
{% endif %}
{% if task.parent_task_id %}
<tr><th style="color:var(--text-muted);">Parent Task</th><td><a href="{{ url_for('task_detail', task_id=task.parent_task_id) }}">#{{ task.parent_task_id }}</a></td></tr>
{% endif %}
{% if task.reply_to %}
<tr><th style="color:var(--text-muted);">Reply-To</th><td><code>{{ task.reply_to }}</code></td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Beschreibung + Antwort -->
<div class="col-12 col-md-8">
<div class="card mb-4">
<div class="card-header" style="background:var(--surface-2);">
<h5 class="mb-0">Anfrage / Beschreibung</h5>
</div>
<div class="card-body">
{% if task.description %}
<pre style="white-space:pre-wrap;font-family:inherit;font-size:.9rem;color:var(--text-primary);margin:0;">{{ task.description }}</pre>
{% else %}
<span style="color:var(--text-muted);">Keine Beschreibung.</span>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center" style="background:var(--surface-2);">
<h5 class="mb-0">Agent-Antwort / Log</h5>
{% if task.response %}
<button class="btn btn-sm btn-outline-secondary" onclick="copyResponse()">Kopieren</button>
{% endif %}
</div>
<div class="card-body p-0">
{% if task.status == 'in_progress' %}
<div class="p-3" style="color:var(--info);">
<span class="spinner-border spinner-border-sm me-2"></span>
Task läuft gerade... Seite wird alle 5s aktualisiert.
</div>
{% endif %}
{% if task.response %}
<pre id="responseBlock" style="white-space:pre-wrap;font-family:'JetBrains Mono',monospace;font-size:.82rem;color:var(--text-primary);margin:0;padding:1.25rem;max-height:600px;overflow-y:auto;">{{ task.response }}</pre>
{% elif task.status != 'in_progress' %}
<div class="p-3" style="color:var(--text-muted);">Noch keine Antwort vorhanden.</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-Refresh wenn Task noch läuft
{% if task.status in ['pending', 'in_progress'] %}
setTimeout(() => location.reload(), 5000);
{% endif %}
function copyResponse() {
const text = document.getElementById('responseBlock').innerText;
navigator.clipboard.writeText(text).then(() => {
const btn = event.target;
btn.textContent = 'Kopiert!';
setTimeout(() => btn.textContent = 'Kopieren', 2000);
});
}
</script>
{% endblock %}

View file

@ -25,7 +25,7 @@
<th>Agent</th> <th>Agent</th>
<th>Status</th> <th>Status</th>
<th>Erstellt</th> <th>Erstellt</th>
<th>Aktion</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -66,16 +66,11 @@
{% endif %} {% endif %}
</td> </td>
<td style="color:var(--text-muted);font-size:.75rem;">{{ task.created }}</td> <td style="color:var(--text-muted);font-size:.75rem;">{{ task.created }}</td>
<td> <td class="d-flex gap-2 align-items-center flex-wrap">
{% if task.status == 'pending' %} <a href="{{ url_for('task_detail', task_id=task.id) }}" class="btn btn-sm btn-outline-secondary" title="Details">Details</a>
<span style="color:var(--text-muted);font-size:.75rem;">⏳ Wartend</span> <form method="POST" action="{{ url_for('delete_task', task_id=task.id) }}" onsubmit="return confirm('Task #{{ task.id }} wirklich löschen?')" style="display:inline;">
{% elif task.status == 'in_progress' %} <button type="submit" class="btn btn-sm btn-outline-danger" title="Löschen">Löschen</button>
<span style="color:var(--info);font-size:.75rem;">🔄 Läuft...</span> </form>
{% elif task.status == 'completed' %}
<span style="color:var(--success);font-size:.75rem;">✓ Auto</span>
{% else %}
<span style="color:var(--text-muted);font-size:.75rem;"></span>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}