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
|
## 📚 Quellen
|
||||||
|
|
||||||
- Researcher: Diversity-Trends, Barrierefreiheit, Steuerrecht
|
- Researcher: Diversity-Trends, Barrierefreiheit, Steuerrecht
|
||||||
|
|
|
||||||
127
app.py
127
app.py
|
|
@ -1555,15 +1555,24 @@ 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
|
||||||
loop = asyncio.new_event_loop()
|
# In die laufende Event-Loop des Telegram-Threads senden (thread-safe)
|
||||||
asyncio.set_event_loop(loop)
|
loop = telegram_app.updater.application.updater._network_loop if hasattr(telegram_app, 'updater') else None
|
||||||
loop.run_until_complete(telegram_app.bot.send_message(
|
if loop is None:
|
||||||
chat_id=chat_id,
|
# Fallback: Loop des Telegram-Threads direkt holen
|
||||||
text=message
|
loop = telegram_app.loop if hasattr(telegram_app, 'loop') else None
|
||||||
))
|
|
||||||
loop.close()
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[Telegram] Error sending message: {e}")
|
logging.error(f"[Telegram] Error sending message: {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
|
sub_tasks = task.get('sub_tasks', [])
|
||||||
|
available_agents = task.get('available_agents', list(AGENTS.keys()))
|
||||||
|
|
||||||
|
# Direkte Tasks (Telegram, Email, etc.) ohne sub_tasks → direkt ausführen
|
||||||
|
if not sub_tasks:
|
||||||
|
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')
|
update_task_db(task['id'], status='in_progress')
|
||||||
task['status'] = 'in_progress'
|
task['status'] = 'in_progress'
|
||||||
logger.info("[TaskBeat] Planungsphase für Task #%d", task['id'])
|
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
|
|
||||||
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.')
|
|
||||||
continue
|
|
||||||
|
|
||||||
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."""
|
||||||
beat_thread = threading.Thread(target=process_beat_tasks, name='TaskBeat', daemon=True)
|
# Stuck in_progress Tasks beim Start zurücksetzen
|
||||||
beat_thread.start()
|
try:
|
||||||
logger.info("[TaskBeat] Daemon-Thread gestartet.")
|
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 ───────────────────────────────────────────────────────
|
# ── 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
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>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 %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue