From 80d7716e659f65b44b927d9a62e5e9de93c2307e Mon Sep 17 00:00:00 2001 From: Simon Diesenreiter Date: Fri, 10 Apr 2026 21:47:50 +0200 Subject: [PATCH] fix: add Telegram helper functions, refs NOISSUE --- ai_software_factory/agents/telegram.py | 58 +++++++++++++++++++- ai_software_factory/dashboard_ui.py | 76 +++++++++++++++++++++----- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/ai_software_factory/agents/telegram.py b/ai_software_factory/agents/telegram.py index ba222b7..f2c43cc 100644 --- a/ai_software_factory/agents/telegram.py +++ b/ai_software_factory/agents/telegram.py @@ -1,8 +1,6 @@ """Telegram bot integration for n8n webhook.""" import asyncio -import json -import re from typing import Optional @@ -12,6 +10,62 @@ class TelegramHandler: def __init__(self, webhook_url: str): self.webhook_url = webhook_url self.api_url = "https://api.telegram.org/bot" + + def build_prompt_guide_message(self, backend_url: str | None = None) -> str: + """Build a Telegram message explaining the expected prompt format.""" + lines = [ + "AI Software Factory is listening in this chat.", + "", + "Send prompts in this format:", + "Name: Inventory Portal", + "Description: Internal tool for stock management and purchase tracking", + "Features:", + "- role-based login", + "- inventory dashboard", + "- purchase order workflow", + "Tech Stack:", + "- fastapi", + "- postgresql", + "- nicegui", + ] + if backend_url: + lines.extend(["", f"Backend target: {backend_url}"]) + return "\n".join(lines) + + async def send_message(self, bot_token: str, chat_id: str | int, text: str) -> dict: + """Send a direct Telegram message using the configured bot.""" + if not bot_token: + return {"status": "error", "message": "Telegram bot token is not configured"} + if chat_id in (None, ""): + return {"status": "error", "message": "Telegram chat id is not configured"} + + api_endpoint = f"{self.api_url}{bot_token}/sendMessage" + + try: + import aiohttp + async with aiohttp.ClientSession() as session: + async with session.post( + api_endpoint, + json={ + "chat_id": str(chat_id), + "text": text, + }, + ) as resp: + payload = await resp.json() + if 200 <= resp.status < 300 and payload.get("ok"): + return { + "status": "success", + "message": "Telegram prompt guide sent successfully", + "payload": payload, + } + description = payload.get("description") or payload.get("message") or str(payload) + return { + "status": "error", + "message": f"Telegram API returned {resp.status}: {description}", + "payload": payload, + } + except Exception as exc: + return {"status": "error", "message": str(exc)} async def handle_message(self, message_data: dict) -> dict: """Handle incoming Telegram message.""" diff --git a/ai_software_factory/dashboard_ui.py b/ai_software_factory/dashboard_ui.py index d5e8511..53e6b4e 100644 --- a/ai_software_factory/dashboard_ui.py +++ b/ai_software_factory/dashboard_ui.py @@ -5,16 +5,18 @@ from __future__ import annotations from contextlib import closing from html import escape -from nicegui import ui +from nicegui import app, ui try: from .agents.database_manager import DatabaseManager from .agents.n8n_setup import N8NSetupAgent + from .agents.telegram import TelegramHandler from .config import settings from .database import get_database_runtime_summary, get_db_sync, init_db except ImportError: from agents.database_manager import DatabaseManager from agents.n8n_setup import N8NSetupAgent + from agents.telegram import TelegramHandler from config import settings from database import get_database_runtime_summary, get_db_sync, init_db @@ -165,6 +167,7 @@ def _render_health_panels() -> None: ('API URL', n8n_health.get('api_url') or 'Not configured'), ('Auth Configured', 'yes' if n8n_health.get('auth_configured') else 'no'), ('Checked Via', n8n_health.get('checked_via') or 'none'), + ('Telegram Chat ID', settings.telegram_chat_id or 'Not configured'), ] if n8n_health.get('workflow_count') is not None: rows.append(('Workflow Count', str(n8n_health['workflow_count']))) @@ -204,6 +207,15 @@ def create_health_page() -> None: def create_dashboard(): """Create the main NiceGUI dashboard.""" _add_dashboard_styles() + active_tab_key = 'dashboard.active_tab' + + def _selected_tab_name() -> str: + """Return the persisted active dashboard tab.""" + return app.storage.user.get(active_tab_key, 'overview') + + def _store_selected_tab(event) -> None: + """Persist the active dashboard tab across refreshes.""" + app.storage.user[active_tab_key] = event.value or 'overview' async def setup_n8n_workflow_action() -> None: api_url = _resolve_n8n_api_url() @@ -232,6 +244,34 @@ def create_dashboard(): ui.notify(result.get('message', 'n8n setup finished'), color='positive' if result.get('status') == 'success' else 'negative') dashboard_body.refresh() + async def send_telegram_prompt_guide_action() -> None: + if not settings.telegram_bot_token: + ui.notify('Configure TELEGRAM_BOT_TOKEN first', color='negative') + return + if not settings.telegram_chat_id: + ui.notify('Configure TELEGRAM_CHAT_ID to message the prompt channel', color='negative') + return + + handler = TelegramHandler(settings.n8n_webhook_url or _resolve_n8n_api_url()) + message = handler.build_prompt_guide_message(settings.backend_public_url) + result = await handler.send_message( + bot_token=settings.telegram_bot_token, + chat_id=settings.telegram_chat_id, + text=message, + ) + + db = get_db_sync() + if db is not None: + with closing(db): + DatabaseManager(db).log_system_event( + component='telegram', + level='INFO' if result.get('status') == 'success' else 'ERROR', + message=result.get('message', str(result)), + ) + + ui.notify(result.get('message', 'Telegram message sent'), color='positive' if result.get('status') == 'success' else 'negative') + dashboard_body.refresh() + def init_db_action() -> None: result = init_db() ui.notify(result.get('message', 'Database initialized'), color='positive' if result.get('status') == 'success' else 'negative') @@ -270,6 +310,7 @@ def create_dashboard(): ui.button('Refresh', on_click=dashboard_body.refresh).props('outline') ui.button('Initialize DB', on_click=init_db_action).props('unelevated color=dark') ui.button('Provision n8n Workflow', on_click=setup_n8n_workflow_action).props('unelevated color=accent') + ui.button('Message Prompt Channel', on_click=send_telegram_prompt_guide_action).props('outline color=secondary') with ui.grid(columns=4).classes('w-full gap-4'): metrics = [ @@ -284,15 +325,16 @@ def create_dashboard(): ui.label(str(value)).style('font-size: 2.1rem; font-weight: 800; margin-top: 6px;') ui.label(subtitle).style('font-size: 0.9rem; opacity: 0.78; margin-top: 8px;') - with ui.tabs().classes('w-full') as tabs: - overview_tab = ui.tab('Overview') - projects_tab = ui.tab('Projects') - trace_tab = ui.tab('Prompt Trace') - system_tab = ui.tab('System') - health_tab = ui.tab('Health') + selected_tab = _selected_tab_name() + with ui.tabs(value=selected_tab, on_change=_store_selected_tab).classes('w-full') as tabs: + ui.tab('Overview').props('name=overview') + ui.tab('Projects').props('name=projects') + ui.tab('Prompt Trace').props('name=trace') + ui.tab('System').props('name=system') + ui.tab('Health').props('name=health') - with ui.tab_panels(tabs, value=overview_tab).classes('w-full'): - with ui.tab_panel(overview_tab): + with ui.tab_panels(tabs, value=selected_tab).classes('w-full'): + with ui.tab_panel('overview'): with ui.grid(columns=2).classes('w-full gap-4'): with ui.card().classes('factory-panel q-pa-lg'): ui.label('Project Pipeline').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') @@ -322,7 +364,7 @@ def create_dashboard(): ui.label(label).classes('factory-muted') ui.label(value).style('font-weight: 600; color: #3a281a;') - with ui.tab_panel(projects_tab): + with ui.tab_panel('projects'): if not projects: with ui.card().classes('factory-panel q-pa-lg'): ui.label('No project data available yet.').classes('factory-muted') @@ -376,7 +418,7 @@ def create_dashboard(): else: ui.label('No audit events yet.').classes('factory-muted') - with ui.tab_panel(trace_tab): + with ui.tab_panel('trace'): with ui.card().classes('factory-panel q-pa-lg'): ui.label('Prompt to Code Correlation').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') ui.label('Each prompt entry is linked to the generated files recorded after that prompt for the same project.').classes('factory-muted') @@ -397,7 +439,7 @@ def create_dashboard(): else: ui.label('No prompt traces recorded yet.').classes('factory-muted') - with ui.tab_panel(system_tab): + with ui.tab_panel('system'): with ui.grid(columns=2).classes('w-full gap-4'): with ui.card().classes('factory-panel q-pa-lg'): ui.label('System Logs').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') @@ -423,11 +465,19 @@ def create_dashboard(): for endpoint in endpoints: ui.label(endpoint).classes('factory-code q-mt-sm') - with ui.tab_panel(health_tab): + with ui.tab_panel('health'): with ui.card().classes('factory-panel q-pa-lg q-mb-md'): ui.label('Health and Diagnostics').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') ui.label('Use this page to verify runtime configuration, n8n API connectivity, and likely causes of provisioning failures.').classes('factory-muted') ui.link('Open dedicated health page', '/health-ui') + with ui.card().classes('factory-panel q-pa-lg q-mb-md'): + ui.label('Telegram Prompt Channel').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') + ui.label('Send a guide message into the same Telegram chat/channel where the bot is expected to receive prompts.').classes('factory-muted') + with ui.row().classes('justify-between w-full q-mt-sm'): + ui.label('Configured Chat ID').classes('factory-muted') + ui.label(settings.telegram_chat_id or 'Not configured').style('font-weight: 600; color: #3a281a;') + with ui.row().classes('items-center gap-2 q-mt-md'): + ui.button('Send Prompt Guide', on_click=send_telegram_prompt_guide_action).props('unelevated color=secondary') _render_health_panels() dashboard_body()