Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9faac8d16 | |||
| 80d7716e65 | |||
| 321bf74aef | |||
| 55ee75106c |
22
HISTORY.md
22
HISTORY.md
@@ -5,10 +5,32 @@ Changelog
|
|||||||
(unreleased)
|
(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
|
||||||
~~~
|
~~~
|
||||||
- Fix Quasar layout issues, refs NOISSUE. [Simon Diesenreiter]
|
- Fix Quasar layout issues, refs NOISSUE. [Simon Diesenreiter]
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
|
||||||
0.6.1 (2026-04-10)
|
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:
|
async def create_workflow(self, workflow_json: dict) -> dict:
|
||||||
"""Create or update a workflow."""
|
"""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:
|
async def update_workflow(self, workflow_id: str, workflow_json: dict) -> dict:
|
||||||
"""Update an existing workflow."""
|
"""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:
|
async def enable_workflow(self, workflow_id: str) -> dict:
|
||||||
"""Enable a workflow."""
|
"""Enable a workflow."""
|
||||||
@@ -232,6 +252,11 @@ class N8NSetupAgent:
|
|||||||
if result.get("error"):
|
if result.get("error"):
|
||||||
fallback = await self._request("PATCH", f"workflows/{workflow_id}", json={"active": True})
|
fallback = await self._request("PATCH", f"workflows/{workflow_id}", json={"active": True})
|
||||||
if fallback.get("error"):
|
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 fallback
|
||||||
return {"success": True, "id": workflow_id, "method": "patch"}
|
return {"success": True, "id": workflow_id, "method": "patch"}
|
||||||
return {"success": True, "id": workflow_id, "method": "activate"}
|
return {"success": True, "id": workflow_id, "method": "activate"}
|
||||||
@@ -255,7 +280,6 @@ class N8NSetupAgent:
|
|||||||
normalized_path = webhook_path.strip().strip("/") or "telegram"
|
normalized_path = webhook_path.strip().strip("/") or "telegram"
|
||||||
return {
|
return {
|
||||||
"name": "Telegram to AI Software Factory",
|
"name": "Telegram to AI Software Factory",
|
||||||
"active": False,
|
|
||||||
"settings": {"executionOrder": "v1"},
|
"settings": {"executionOrder": "v1"},
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
@@ -324,7 +348,6 @@ class N8NSetupAgent:
|
|||||||
"""Build a production Telegram Trigger based workflow."""
|
"""Build a production Telegram Trigger based workflow."""
|
||||||
return {
|
return {
|
||||||
"name": "Telegram to AI Software Factory",
|
"name": "Telegram to AI Software Factory",
|
||||||
"active": False,
|
|
||||||
"settings": {"executionOrder": "v1"},
|
"settings": {"executionOrder": "v1"},
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"""Telegram bot integration for n8n webhook."""
|
"""Telegram bot integration for n8n webhook."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
@@ -13,6 +11,62 @@ class TelegramHandler:
|
|||||||
self.webhook_url = webhook_url
|
self.webhook_url = webhook_url
|
||||||
self.api_url = "https://api.telegram.org/bot"
|
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:
|
async def handle_message(self, message_data: dict) -> dict:
|
||||||
"""Handle incoming Telegram message."""
|
"""Handle incoming Telegram message."""
|
||||||
text = message_data.get("text", "")
|
text = message_data.get("text", "")
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ from __future__ import annotations
|
|||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from html import escape
|
from html import escape
|
||||||
|
|
||||||
from nicegui import ui
|
from nicegui import app, ui
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .agents.database_manager import DatabaseManager
|
from .agents.database_manager import DatabaseManager
|
||||||
from .agents.n8n_setup import N8NSetupAgent
|
from .agents.n8n_setup import N8NSetupAgent
|
||||||
|
from .agents.telegram import TelegramHandler
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .database import get_database_runtime_summary, get_db_sync, init_db
|
from .database import get_database_runtime_summary, get_db_sync, init_db
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from agents.database_manager import DatabaseManager
|
from agents.database_manager import DatabaseManager
|
||||||
from agents.n8n_setup import N8NSetupAgent
|
from agents.n8n_setup import N8NSetupAgent
|
||||||
|
from agents.telegram import TelegramHandler
|
||||||
from config import settings
|
from config import settings
|
||||||
from database import get_database_runtime_summary, get_db_sync, init_db
|
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'),
|
('API URL', n8n_health.get('api_url') or 'Not configured'),
|
||||||
('Auth Configured', 'yes' if n8n_health.get('auth_configured') else 'no'),
|
('Auth Configured', 'yes' if n8n_health.get('auth_configured') else 'no'),
|
||||||
('Checked Via', n8n_health.get('checked_via') or 'none'),
|
('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:
|
if n8n_health.get('workflow_count') is not None:
|
||||||
rows.append(('Workflow Count', str(n8n_health['workflow_count'])))
|
rows.append(('Workflow Count', str(n8n_health['workflow_count'])))
|
||||||
@@ -204,6 +207,15 @@ def create_health_page() -> None:
|
|||||||
def create_dashboard():
|
def create_dashboard():
|
||||||
"""Create the main NiceGUI dashboard."""
|
"""Create the main NiceGUI dashboard."""
|
||||||
_add_dashboard_styles()
|
_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:
|
async def setup_n8n_workflow_action() -> None:
|
||||||
api_url = _resolve_n8n_api_url()
|
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')
|
ui.notify(result.get('message', 'n8n setup finished'), color='positive' if result.get('status') == 'success' else 'negative')
|
||||||
dashboard_body.refresh()
|
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:
|
def init_db_action() -> None:
|
||||||
result = init_db()
|
result = init_db()
|
||||||
ui.notify(result.get('message', 'Database initialized'), color='positive' if result.get('status') == 'success' else 'negative')
|
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('Refresh', on_click=dashboard_body.refresh).props('outline')
|
||||||
ui.button('Initialize DB', on_click=init_db_action).props('unelevated color=dark')
|
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('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'):
|
with ui.grid(columns=4).classes('w-full gap-4'):
|
||||||
metrics = [
|
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(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;')
|
ui.label(subtitle).style('font-size: 0.9rem; opacity: 0.78; margin-top: 8px;')
|
||||||
|
|
||||||
with ui.tabs().classes('w-full') as tabs:
|
selected_tab = _selected_tab_name()
|
||||||
overview_tab = ui.tab('Overview')
|
with ui.tabs(value=selected_tab, on_change=_store_selected_tab).classes('w-full') as tabs:
|
||||||
projects_tab = ui.tab('Projects')
|
ui.tab('Overview').props('name=overview')
|
||||||
trace_tab = ui.tab('Prompt Trace')
|
ui.tab('Projects').props('name=projects')
|
||||||
system_tab = ui.tab('System')
|
ui.tab('Prompt Trace').props('name=trace')
|
||||||
health_tab = ui.tab('Health')
|
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_panels(tabs, value=selected_tab).classes('w-full'):
|
||||||
with ui.tab_panel(overview_tab):
|
with ui.tab_panel('overview'):
|
||||||
with ui.grid(columns=2).classes('w-full gap-4'):
|
with ui.grid(columns=2).classes('w-full gap-4'):
|
||||||
with ui.card().classes('factory-panel q-pa-lg'):
|
with ui.card().classes('factory-panel q-pa-lg'):
|
||||||
ui.label('Project Pipeline').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;')
|
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(label).classes('factory-muted')
|
||||||
ui.label(value).style('font-weight: 600; color: #3a281a;')
|
ui.label(value).style('font-weight: 600; color: #3a281a;')
|
||||||
|
|
||||||
with ui.tab_panel(projects_tab):
|
with ui.tab_panel('projects'):
|
||||||
if not projects:
|
if not projects:
|
||||||
with ui.card().classes('factory-panel q-pa-lg'):
|
with ui.card().classes('factory-panel q-pa-lg'):
|
||||||
ui.label('No project data available yet.').classes('factory-muted')
|
ui.label('No project data available yet.').classes('factory-muted')
|
||||||
@@ -376,7 +418,7 @@ def create_dashboard():
|
|||||||
else:
|
else:
|
||||||
ui.label('No audit events yet.').classes('factory-muted')
|
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'):
|
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('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')
|
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:
|
else:
|
||||||
ui.label('No prompt traces recorded yet.').classes('factory-muted')
|
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.grid(columns=2).classes('w-full gap-4'):
|
||||||
with ui.card().classes('factory-panel q-pa-lg'):
|
with ui.card().classes('factory-panel q-pa-lg'):
|
||||||
ui.label('System Logs').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;')
|
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:
|
for endpoint in endpoints:
|
||||||
ui.label(endpoint).classes('factory-code q-mt-sm')
|
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'):
|
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('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.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')
|
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()
|
_render_health_panels()
|
||||||
|
|
||||||
dashboard_body()
|
dashboard_body()
|
||||||
|
|||||||
Reference in New Issue
Block a user