Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9faac8d16 | |||
| 80d7716e65 | |||
| 321bf74aef | |||
| 55ee75106c |
22
HISTORY.md
22
HISTORY.md
@@ -5,10 +5,32 @@ Changelog
|
||||
(unreleased)
|
||||
------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Add Telegram helper functions, refs NOISSUE. [Simon Diesenreiter]
|
||||
|
||||
|
||||
0.6.3 (2026-04-10)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- N8n workflow generation, refs NOISSUE. [Simon Diesenreiter]
|
||||
|
||||
Other
|
||||
~~~~~
|
||||
|
||||
|
||||
0.6.2 (2026-04-10)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Fix Quasar layout issues, refs NOISSUE. [Simon Diesenreiter]
|
||||
|
||||
Other
|
||||
~~~~~
|
||||
|
||||
|
||||
0.6.1 (2026-04-10)
|
||||
------------------
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.6.2
|
||||
0.6.4
|
||||
|
||||
@@ -220,11 +220,31 @@ class N8NSetupAgent:
|
||||
|
||||
async def create_workflow(self, workflow_json: dict) -> dict:
|
||||
"""Create or update a workflow."""
|
||||
return await self._request("POST", "workflows", json=workflow_json)
|
||||
return await self._request("POST", "workflows", json=self._workflow_payload(workflow_json))
|
||||
|
||||
def _workflow_payload(self, workflow_json: dict) -> dict:
|
||||
"""Return a workflow payload without server-managed read-only fields."""
|
||||
payload = dict(workflow_json)
|
||||
payload.pop("active", None)
|
||||
payload.pop("id", None)
|
||||
payload.pop("createdAt", None)
|
||||
payload.pop("updatedAt", None)
|
||||
payload.pop("versionId", None)
|
||||
return payload
|
||||
|
||||
async def _update_workflow_via_put(self, workflow_id: str, workflow_json: dict) -> dict:
|
||||
"""Fallback update path for n8n instances that only support PUT."""
|
||||
return await self._request("PUT", f"workflows/{workflow_id}", json=self._workflow_payload(workflow_json))
|
||||
|
||||
async def update_workflow(self, workflow_id: str, workflow_json: dict) -> dict:
|
||||
"""Update an existing workflow."""
|
||||
return await self._request("PATCH", f"workflows/{workflow_id}", json=workflow_json)
|
||||
result = await self._request("PATCH", f"workflows/{workflow_id}", json=self._workflow_payload(workflow_json))
|
||||
if result.get("status_code") == 405:
|
||||
fallback = await self._update_workflow_via_put(workflow_id, workflow_json)
|
||||
if not fallback.get("error") and isinstance(fallback, dict):
|
||||
fallback.setdefault("method", "PUT")
|
||||
return fallback
|
||||
return result
|
||||
|
||||
async def enable_workflow(self, workflow_id: str) -> dict:
|
||||
"""Enable a workflow."""
|
||||
@@ -232,6 +252,11 @@ class N8NSetupAgent:
|
||||
if result.get("error"):
|
||||
fallback = await self._request("PATCH", f"workflows/{workflow_id}", json={"active": True})
|
||||
if fallback.get("error"):
|
||||
if fallback.get("status_code") == 405:
|
||||
put_fallback = await self._request("PUT", f"workflows/{workflow_id}", json={"active": True})
|
||||
if put_fallback.get("error"):
|
||||
return put_fallback
|
||||
return {"success": True, "id": workflow_id, "method": "put"}
|
||||
return fallback
|
||||
return {"success": True, "id": workflow_id, "method": "patch"}
|
||||
return {"success": True, "id": workflow_id, "method": "activate"}
|
||||
@@ -255,7 +280,6 @@ class N8NSetupAgent:
|
||||
normalized_path = webhook_path.strip().strip("/") or "telegram"
|
||||
return {
|
||||
"name": "Telegram to AI Software Factory",
|
||||
"active": False,
|
||||
"settings": {"executionOrder": "v1"},
|
||||
"nodes": [
|
||||
{
|
||||
@@ -324,7 +348,6 @@ class N8NSetupAgent:
|
||||
"""Build a production Telegram Trigger based workflow."""
|
||||
return {
|
||||
"name": "Telegram to AI Software Factory",
|
||||
"active": False,
|
||||
"settings": {"executionOrder": "v1"},
|
||||
"nodes": [
|
||||
{
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user