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:
parent
5b4b698064
commit
99df910497
4 changed files with 237 additions and 34 deletions
|
|
@ -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
|
||||
|
||||
- Researcher: Diversity-Trends, Barrierefreiheit, Steuerrecht
|
||||
|
|
|
|||
123
app.py
123
app.py
|
|
@ -1555,15 +1555,24 @@ def send_telegram_message(chat_id: int, message: str):
|
|||
return False
|
||||
|
||||
try:
|
||||
# Async-Funktion in sync Context ausführen
|
||||
import asyncio
|
||||
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.close()
|
||||
# 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.run_until_complete(telegram_app.bot.send_message(chat_id=chat_id, text=message))
|
||||
loop.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"[Telegram] Error sending message: {e}")
|
||||
|
|
@ -1604,7 +1613,7 @@ def run_telegram_bot():
|
|||
|
||||
try:
|
||||
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:
|
||||
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', '')
|
||||
|
||||
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', [])
|
||||
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:
|
||||
logger.warning("[TaskBeat] Task #%d hat keine sub_tasks - als completed markiert", task['id'])
|
||||
update_task_db(task['id'], status='completed', response='Fehler: Keine sub_tasks definiert. Dieser Task wurde wahrscheinlich über eine veraltete API erstellt.')
|
||||
update_task_db(task['id'], status='in_progress')
|
||||
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
|
||||
|
||||
# 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:
|
||||
|
||||
Tasks:
|
||||
|
|
@ -2113,10 +2151,31 @@ Arbeite diesen Teil ab und liefere ein vollständiges Ergebnis.""",
|
|||
|
||||
|
||||
def start_task_beat():
|
||||
"""Startet den Task-Beat als Daemon-Thread."""
|
||||
beat_thread = threading.Thread(target=process_beat_tasks, name='TaskBeat', daemon=True)
|
||||
beat_thread.start()
|
||||
logger.info("[TaskBeat] Daemon-Thread gestartet.")
|
||||
"""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.start()
|
||||
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 ───────────────────────────────────────────────────────
|
||||
|
|
@ -2390,6 +2449,28 @@ def task_list():
|
|||
all_tasks = get_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>')
|
||||
@login_required
|
||||
def update_task(task_id, status):
|
||||
|
|
|
|||
117
templates/task_detail.html
Normal file
117
templates/task_detail.html
Normal 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;">← 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 %}
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
<th>Agent</th>
|
||||
<th>Status</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Aktion</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -66,16 +66,11 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td style="color:var(--text-muted);font-size:.75rem;">{{ task.created }}</td>
|
||||
<td>
|
||||
{% if task.status == 'pending' %}
|
||||
<span style="color:var(--text-muted);font-size:.75rem;">⏳ Wartend</span>
|
||||
{% elif task.status == 'in_progress' %}
|
||||
<span style="color:var(--info);font-size:.75rem;">🔄 Läuft...</span>
|
||||
{% 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 class="d-flex gap-2 align-items-center flex-wrap">
|
||||
<a href="{{ url_for('task_detail', task_id=task.id) }}" class="btn btn-sm btn-outline-secondary" title="Details">Details</a>
|
||||
<form method="POST" action="{{ url_for('delete_task', task_id=task.id) }}" onsubmit="return confirm('Task #{{ task.id }} wirklich löschen?')" style="display:inline;">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Löschen">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue