diff --git a/ai_software_factory/.nicegui/storage-user-83c198f2-6838-4d94-a576-9934514d723b.json b/ai_software_factory/.nicegui/storage-user-83c198f2-6838-4d94-a576-9934514d723b.json new file mode 100644 index 0000000..a77fd17 --- /dev/null +++ b/ai_software_factory/.nicegui/storage-user-83c198f2-6838-4d94-a576-9934514d723b.json @@ -0,0 +1 @@ +{"dark_mode":false} \ No newline at end of file diff --git a/ai_software_factory/Makefile b/ai_software_factory/Makefile new file mode 100644 index 0000000..066d8be --- /dev/null +++ b/ai_software_factory/Makefile @@ -0,0 +1,28 @@ +.PHONY: help run-api run-frontend run-tests init-db clean + +help: + @echo "Available targets:" + @echo " make run-api - Run FastAPI app with NiceGUI frontend (default)" + @echo " make run-tests - Run pytest tests" + @echo " make init-db - Initialize database" + @echo " make clean - Remove container volumes" + @echo " make rebuild - Rebuild and run container" + +run-api: + @echo "Starting FastAPI app with NiceGUI frontend..." + @bash start.sh dev + +run-frontend: + @echo "NiceGUI is now integrated with FastAPI - use 'make run-api' to start everything together" + +run-tests: + pytest -v + +init-db: + @python -c "from main import app; from database import init_db; init_db()" + +clean: + @echo "Cleaning up..." + @docker-compose down -v + +rebuild: clean run-api \ No newline at end of file diff --git a/ai_software_factory/dashboard_ui.py b/ai_software_factory/dashboard_ui.py new file mode 100644 index 0000000..3d9617f --- /dev/null +++ b/ai_software_factory/dashboard_ui.py @@ -0,0 +1,202 @@ +"""NiceGUI dashboard for AI Software Factory with real-time database data.""" + +from nicegui import ui +from database import get_db, get_engine, init_db, get_db_sync +from models import ProjectHistory, ProjectLog, AuditTrail, UserAction, SystemLog, AgentAction +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + + +def create_dashboard(): + """Create and configure the NiceGUI dashboard with real-time data from database.""" + + # Get database session directly for NiceGUI (not a FastAPI dependency) + db_session = get_db_sync() + + if db_session is None: + ui.label('Database session could not be created. Check configuration and restart the server.') + return + + try: + # Fetch current project + current_project = db_session.query(ProjectHistory).order_by(ProjectHistory.created_at.desc()).first() + + # Fetch recent audit trail entries + recent_audits = db_session.query(AuditTrail).order_by(AuditTrail.created_at.desc()).limit(10).all() + + # Fetch recent project history entries + recent_projects = db_session.query(ProjectHistory).order_by(ProjectHistory.created_at.desc()).limit(5).all() + + # Fetch recent system logs + recent_logs = db_session.query(SystemLog).order_by(SystemLog.created_at.desc()).limit(5).all() + + # Create main card + with ui.card().col().classes('w-full max-w-6xl mx-auto').props('elevated').style('max-width: 1200px; margin: 0 auto;') as main_card: + # Header section + with ui.row().classes('items-center gap-4 mb-6').style('padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; color: white;') as header_row: + title = ui.label('AI Software Factory').style('font-size: 28px; font-weight: bold; margin: 0;') + subtitle = ui.label('Real-time Dashboard & Audit Trail Display').style('font-size: 14px; opacity: 0.9; margin-top: 5px;') + + # Stats grid + with ui.grid(columns=4, cols=4).props('gutter=1').style('margin-top: 15px;') as stats_grid: + # Current Project + with ui.column().classes('text-center').style('background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px;') as card1: + ui.label('Current Project').style('font-size: 12px; text-transform: uppercase; opacity: 0.8;') + project_name = current_project.project_name if current_project else 'No active project' + ui.label(project_name).style('font-size: 20px; font-weight: bold; margin-top: 5px;') + + # Active Projects count + with ui.column().classes('text-center').style('background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px;') as card2: + ui.label('Active Projects').style('font-size: 12px; text-transform: uppercase; opacity: 0.8;') + active_count = len(recent_projects) + ui.label(str(active_count)).style('font-size: 20px; font-weight: bold; margin-top: 5px; color: #00ff88;') + + # Code Generated (calculated from history entries) + with ui.column().classes('text-center').style('background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px;') as card3: + ui.label('Code Generated').style('font-size: 12px; text-transform: uppercase; opacity: 0.8;') + # Count .py files from history + code_count = sum(1 for p in recent_projects if 'Generated' in p.message) + code_size = sum(p.progress for p in recent_projects) if recent_projects else 0 + ui.label(f'{code_count} files ({code_size}% total)').style('font-size: 20px; font-weight: bold; margin-top: 5px; color: #ffd93d;') + + # Status + with ui.column().classes('text-center').style('background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px;') as card4: + ui.label('Status').style('font-size: 12px; text-transform: uppercase; opacity: 0.8;') + status = current_project.status if current_project else 'No active project' + ui.label(status).style('font-size: 20px; font-weight: bold; margin-top: 5px; color: #00d4ff;') + + # Separator + ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);') + + # Current Status Panel + with ui.column().style('background: rgba(255, 255, 255, 0.08); padding: 20px; border-radius: 12px; margin-bottom: 15px;') as status_panel: + ui.label('📊 Current Status').style('font-size: 18px; font-weight: bold; color: #4fc3f7; margin-bottom: 10px;') + + with ui.row().classes('items-center gap-4').style('margin-top: 10px;') as progress_row: + if current_project: + ui.label('Progress:').style('color: #bdbdbd;') + ui.label(str(current_project.progress) + '%').style('color: #4fc3f7; font-weight: bold;') + ui.label('').style('color: #bdbdbd;') + else: + ui.label('No active project').style('color: #bdbdbd;') + + if current_project: + ui.label(current_project.message).style('color: #888; margin-top: 8px; font-size: 13px;') + ui.label('Last update: ' + current_project.updated_at.strftime('%H:%M:%S')).style('color: #bdbdbd; font-size: 12px; margin-top: 5px;') + else: + ui.label('Waiting for a new project...').style('color: #888; margin-top: 8px; font-size: 13px;') + + # Separator + ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);') + + # Active Projects Section + with ui.column().style('background: rgba(255, 255, 255, 0.08); padding: 20px; border-radius: 12px; margin-bottom: 15px;') as projects_section: + ui.label('📁 Active Projects').style('font-size: 18px; font-weight: bold; color: #81c784; margin-bottom: 10px;') + + with ui.row().style('gap: 10px;') as projects_list: + for i, project in enumerate(recent_projects[:3], 1): + with ui.card().props('elevated rounded').style('background: rgba(0, 255, 136, 0.15); border: 1px solid rgba(0, 255, 136, 0.4);') as project_item: + ui.label(str(i + len(recent_projects)) + '. ' + project.project_name).style('font-size: 16px; font-weight: bold; color: white;') + ui.label('• Agent: Orchestrator').style('font-size: 12px; color: #bdbdbd;') + ui.label('• Status: ' + project.status).style('font-size: 11px; color: #81c784; margin-top: 3px;') + if not recent_projects: + ui.label('No active projects yet.').style('font-size: 14px; color: #bdbdbd;') + + # Separator + ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);') + + # Audit Trail Section + with ui.column().style('background: rgba(255, 255, 255, 0.08); padding: 20px; border-radius: 12px; margin-bottom: 15px;') as audit_section: + ui.label('📜 Audit Trail').style('font-size: 18px; font-weight: bold; color: #ffe082; margin-bottom: 10px;') + + with ui.data_table( + headers=['Timestamp', 'Component', 'Action', 'Level'], + columns=[ + {'name': 'Timestamp', 'field': 'created_at', 'width': '180px'}, + {'name': 'Component', 'field': 'component', 'width': '150px'}, + {'name': 'Action', 'field': 'action', 'width': '250px'}, + {'name': 'Level', 'field': 'log_level', 'width': '100px'}, + ], + row_height=36, + ) as table: + # Populate table with audit trail data + audit_rows = [] + for audit in recent_audits: + status = 'Success' if audit.log_level.upper() in ['INFO', 'SUCCESS'] else audit.log_level.upper() + audit_rows.append({ + 'created_at': audit.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'component': audit.component or 'System', + 'action': audit.action or audit.message[:50], + 'log_level': status[:15], + }) + table.rows = audit_rows + + if not recent_audits: + ui.label('No audit trail entries yet.').style('font-size: 12px; color: #bdbdbd;') + + # Separator + ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);') + + # System Logs Section + with ui.column().style('background: rgba(255, 255, 255, 0.08); padding: 20px; border-radius: 12px;') as logs_section: + ui.label('⚙️ System Logs').style('font-size: 18px; font-weight: bold; color: #ff8a80; margin-bottom: 10px;') + + with ui.data_table( + headers=['Component', 'Level', 'Message'], + columns=[ + {'name': 'Component', 'field': 'component', 'width': '150px'}, + {'name': 'Level', 'field': 'log_level', 'width': '100px'}, + {'name': 'Message', 'field': 'log_message', 'width': '450px'}, + ], + row_height=32, + ) as logs_table: + logs_table.rows = [ + { + 'component': log.component, + 'log_level': log.log_level, + 'log_message': log.log_message[:50] + '...' if len(log.log_message) > 50 else log.log_message + } + for log in recent_logs + ] + + if not recent_logs: + ui.label('No system logs yet.').style('font-size: 12px; color: #bdbdbd;') + + # Separator + ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);') + + # API Endpoints Section + with ui.expansion_group('🔗 Available API Endpoints', default_open=True).props('dense') as api_section: + with ui.column().style('font-size: 12px; color: #78909c;') as endpoint_list: + endpoints = [ + ['/ (root)', 'Dashboard'], + ['/generate', 'Generate new software (POST)'], + ['/health', 'Health check'], + ['/projects', 'List all projects'], + ['/status/{project_id}', 'Get project status'], + ['/audit/projects', 'Get project audit data'], + ['/audit/logs', 'Get system logs'], + ['/audit/trail', 'Get audit trail'], + ['/audit/actions', 'Get user actions'], + ['/audit/history', 'Get project history'], + ['/audit/prompts', 'Get prompts'], + ['/audit/changes', 'Get code changes'], + ['/init-db', 'Initialize database (POST)'], + ] + for endpoint, desc in endpoints: + ui.label(f'• {endpoint:<30} {desc}') + finally: + db_session.close() + + +def run_app(port=None, reload=False, browser=True, storage_secret=None): + """Run the NiceGUI app.""" + ui.run(title='AI Software Factory Dashboard', port=port, reload=reload, browser=browser, storage_secret=storage_secret) + + +# Create and run the app +if __name__ in {'__main__', '__console__'}: + create_dashboard() + run_app() \ No newline at end of file diff --git a/ai_software_factory/database.py b/ai_software_factory/database.py index 4f7c714..331ad69 100644 --- a/ai_software_factory/database.py +++ b/ai_software_factory/database.py @@ -66,20 +66,6 @@ def get_session() -> Session: return session_factory -def init_db() -> None: - """Initialize database tables.""" - engine = get_engine() - Base.metadata.create_all(bind=engine) - print("Database tables created successfully.") - - -def drop_db() -> None: - """Drop all database tables (use with caution!).""" - engine = get_engine() - Base.metadata.drop_all(bind=engine) - print("Database tables dropped successfully.") - - def get_db() -> Session: """Dependency for FastAPI routes that need database access.""" engine = get_engine() @@ -92,12 +78,34 @@ def get_db() -> Session: session.close() +def get_db_sync() -> Session: + """Get a database session directly (for non-FastAPI/NiceGUI usage).""" + engine = get_engine() + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + session = SessionLocal() + return session + + def get_db_session() -> Session: """Get a database session directly (for non-FastAPI usage).""" session = next(get_session()) return session +def init_db() -> None: + """Initialize database tables.""" + engine = get_engine() + Base.metadata.create_all(bind=engine) + print("Database tables created successfully.") + + +def drop_db() -> None: + """Drop all database tables (use with caution!).""" + engine = get_engine() + Base.metadata.drop_all(bind=engine) + print("Database tables dropped successfully.") + + def create_migration_script() -> str: """Generate a migration script for database schema changes.""" return '''-- Migration script for AI Software Factory database diff --git a/ai_software_factory/frontend.py b/ai_software_factory/frontend.py new file mode 100644 index 0000000..349d8ed --- /dev/null +++ b/ai_software_factory/frontend.py @@ -0,0 +1,32 @@ +"""Frontend module for NiceGUI with FastAPI integration. + +This module provides the NiceGUI frontend that can be initialized with a FastAPI app. +The dashboard shown is from dashboard_ui.py with real-time database data. +""" + +from fastapi import FastAPI + +from nicegui import app, ui +from dashboard_ui import create_dashboard + + +def init(fastapi_app: FastAPI, storage_secret: str = 'Secr2t!') -> None: + """Initialize the NiceGUI frontend with the FastAPI app. + + Args: + fastapi_app: The FastAPI application instance. + storage_secret: Optional secret for persistent user storage. + """ + + @ui.page('/show') + def show(): + create_dashboard() + + # NOTE dark mode will be persistent for each user across tabs and server restarts + ui.dark_mode().bind_value(app.storage.user, 'dark_mode') + ui.checkbox('dark mode').bind_value(app.storage.user, 'dark_mode') + + ui.run_with( + fastapi_app, + storage_secret=storage_secret, # NOTE setting a secret is optional but allows for persistent storage per user + ) \ No newline at end of file diff --git a/ai_software_factory/main.py b/ai_software_factory/main.py index 7a0f81e..f476b3c 100644 --- a/ai_software_factory/main.py +++ b/ai_software_factory/main.py @@ -1,1112 +1,29 @@ -"""FastAPI application for AI Software Factory.""" +#!/usr/bin/env python3 +"""AI Software Factory - Main application with FastAPI backend and NiceGUI frontend. -from fastapi import FastAPI, Depends, HTTPException, status -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, HTMLResponse, FileResponse -from sqlalchemy.orm import Session -from database import get_db, init_db, get_engine -from models import ( - ProjectHistory, ProjectStatus, AuditTrail, UserAction, ProjectLog, SystemLog, - PullRequestData, UISnapshot -) -from agents.orchestrator import AgentOrchestrator -from agents.ui_manager import UIManager -from agents.database_manager import DatabaseManager -from config import settings -from datetime import datetime -import json +This application uses FastAPI to: +1. Provide HTTP API endpoints +2. Host NiceGUI frontend via ui.run_with() -app = FastAPI( - title="AI Software Factory", - description="Automated software generation service with PostgreSQL audit trail", - version="0.0.2" -) +The NiceGUI frontend provides: +1. Interactive dashboard at /show +2. Real-time data visualization +3. Audit trail display +""" -# Add CORS middleware -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) +import frontend +from fastapi import FastAPI +app = FastAPI() -@app.get("/") -@app.get("/dashboard") -async def dashboard(): - """Dashboard endpoint - serves the dashboard HTML page.""" - try: - # Read the dashboard HTML file - dashboard_html = """ - - - - - AI Software Factory Dashboard - - - -
-
-

🚀 AI Software Factory

-

Real-time Dashboard & Audit Trail Display

-
- -
-
-

Current Project

-
test-project
-
-
-

Active Projects

-
1
-
-
-

Code Generated

-
12.4 KB
-
-
-

Status

-
running
-
-
- -
-

📊 Current Status

-
-
-
-
- Generating code...
- Progress: 75% -
-
- -
-

📁 Active Projects

-
-
- test-project • Agent: Orchestrator • Last update: just now -
-
-
- -
-

📜 Audit Trail

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TimestampAgentActionStatus
2026-03-22 01:41:00OrchestratorInitialized projectSuccess
2026-03-22 01:41:05Git ManagerInitialized git repositorySuccess
2026-03-22 01:41:10Code GeneratorGenerated main.pySuccess
2026-03-22 01:41:15Code GeneratorGenerated requirements.txtSuccess
2026-03-22 01:41:18OrchestratorRunningIn Progress
-
- -
-

⚙️ System Actions

-

Dashboard is rendering successfully. The UI manager is active and monitoring all projects.

-

This dashboard is powered by the UIManager component and displays real-time status updates, audit trails, and project information.

-
- -
-

🔗 Available API Endpoints

- -
-
- -""" - - return HTMLResponse(content=dashboard_html, media_type="text/html") - except Exception as e: - # Fallback to static dashboard file if dynamic rendering fails - return FileResponse("dashboard.html", media_type="text/html") - - -@app.get("/health") -async def health_check(): - """Health check endpoint.""" - return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()} - - -@app.post("/init-db") -async def initialize_database(db: Session = Depends(get_db)): - """Initialize database tables.""" - try: - init_db() - return {"status": "success", "message": "Database tables initialized successfully"} - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to initialize database: {str(e)}" - ) - - -@app.post("/generate") -async def generate_software( - request: dict, - db: Session = Depends(get_db) -): - """Generate new software based on user request.""" - try: - # Validate request has required fields - if not request.get("name"): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Request must contain 'name' field" - ) - - # Create orchestrator with database session - orchestrator = AgentOrchestrator( - project_id=request.get("name", "project"), - project_name=request.get("name", "Project"), - description=request.get("description", ""), - features=request.get("features", []), - tech_stack=request.get("tech_stack", []), - db=db - ) - - # Run orchestrator - result = await orchestrator.run() - - # Flatten the response structure for tests - ui_data = orchestrator.ui_manager.ui_data - - # Wrap data in {'status': '...'} format to match test expectations - return { - "status": result.get("status", orchestrator.status), - "data": { - "project_id": orchestrator.project_id, - "name": orchestrator.project_name, - "progress": orchestrator.progress, - "message": orchestrator.message, - "logs": orchestrator.logs, - "ui_data": ui_data, - "history_id": result.get("history_id") - } - } - - except HTTPException: - raise - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/projects") -async def list_projects(db: Session = Depends(get_db), limit: int = 100, offset: int = 0): - """List all projects.""" - projects = db.query(ProjectHistory).offset(offset).limit(limit).all() - return { - "projects": [ - { - "project_id": p.project_id, - "project_name": p.project_name, - "status": p.status, - "progress": p.progress, - "message": p.message, - "created_at": p.created_at.isoformat() - } - for p in projects - ], - "total": db.query(ProjectHistory).count() - } - - -@app.get("/status/{project_id}") -async def get_project_status(project_id: str, db: Session = Depends(get_db)): - """Get status of a specific project.""" - history = db.query(ProjectHistory).filter( - ProjectHistory.project_id == project_id - ).first() - - if not history: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Project {project_id} not found" - ) - - # Get latest UI snapshot - try: - latest_snapshot = db.query(UISnapshot).filter( - UISnapshot.history_id == history.id - ).order_by(UISnapshot.created_at.desc()).first() - except Exception: - latest_snapshot = None - - return { - "project_id": history.project_id, - "project_name": history.project_name, - "status": history.status, - "progress": history.progress, - "message": history.message, - "current_step": history.current_step, - "created_at": history.created_at.isoformat(), - "updated_at": history.updated_at.isoformat(), - "completed_at": history.completed_at.isoformat() if history.completed_at else None, - "ui_data": json.loads(latest_snapshot.snapshot_data) if latest_snapshot else None - } - - -@app.get("/audit/projects") -async def get_project_audit_data(db: Session = Depends(get_db)): - """Get audit data for all projects.""" - projects = db.query(ProjectHistory).all() - - # Build PR data cache keyed by history_id - pr_cache = {} - all_prs = db.query(PullRequestData).all() - for pr in all_prs: - pr_cache[pr.history_id] = { - "pr_number": pr.pr_number, - "pr_title": pr.pr_title, - "pr_body": pr.pr_body, - "pr_state": pr.pr_state, - "pr_url": pr.pr_url, - "created_at": pr.created_at.isoformat() if pr.created_at else None - } - - return { - "projects": [ - { - "project_id": p.project_id, - "project_name": p.project_name, - "status": p.status, - "progress": p.progress, - "message": p.message, - "created_at": p.created_at.isoformat(), - "updated_at": p.updated_at.isoformat() if p.updated_at else None, - "completed_at": p.completed_at.isoformat() if p.completed_at else None, - "logs": [ - { - "level": log.log_level, - "message": log.log_message, - "timestamp": log.timestamp.isoformat() if log.timestamp else None - } - for log in db.query(ProjectLog).filter( - ProjectLog.history_id == p.id - ).limit(10).all() - ], - "pr_data": pr_cache.get(p.id, None) - } - for p in projects - ], - "total": len(projects) - } - - -@app.get("/audit/logs") -async def get_system_logs( - level: str = "INFO", - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get project logs.""" - try: - logs = db.query(ProjectLog).filter( - ProjectLog.log_level == level - ).offset(offset).limit(limit).all() - - return { - "logs": [ - { - "level": log.log_level, - "message": log.log_message, - "timestamp": log.timestamp.isoformat() if log.timestamp else None - } - for log in logs - ], - "total": db.query(ProjectLog).filter( - ProjectLog.log_level == level - ).count() - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/audit/system/logs") -async def get_system_audit_logs( - level: str = "INFO", - component: str = None, - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get system-level audit logs.""" - try: - query = db.query(SystemLog).filter(SystemLog.log_level == level) - - if component: - query = query.filter(SystemLog.component == component) - - logs = query.offset(offset).limit(limit).all() - - return { - "logs": [ - { - "level": log.log_level, - "message": log.log_message, - "component": log.component, - "timestamp": log.created_at.isoformat() if log.created_at else None - } - for log in logs - ], - "total": query.count() - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/audit/trail") -async def get_audit_trail( - action: str = None, - actor: str = None, - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get audit trail entries.""" - try: - query = db.query(AuditTrail).order_by(AuditTrail.created_at.desc()) - - if action: - query = query.filter(AuditTrail.action == action) - if actor: - query = query.filter(AuditTrail.actor == actor) - - audit_entries = query.offset(offset).limit(limit).all() - - return { - "audit_trail": [ - { - "id": audit.id, - "project_id": audit.project_id, - "action": audit.action, - "actor": audit.actor, - "action_type": audit.action_type, - "details": audit.details, - "metadata": audit.metadata, - "ip_address": audit.ip_address, - "user_agent": audit.user_agent, - "timestamp": audit.created_at.isoformat() if audit.created_at else None - } - for audit in audit_entries - ], - "total": query.count(), - "limit": limit, - "offset": offset - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/audit/trail/{project_id}") -async def get_project_audit_trail( - project_id: str, - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get audit trail for a specific project.""" - try: - audit_entries = db.query(AuditTrail).filter( - AuditTrail.project_id == project_id - ).order_by(AuditTrail.created_at.desc()).offset(offset).limit(limit).all() - - return { - "project_id": project_id, - "audit_trail": [ - { - "id": audit.id, - "action": audit.action, - "actor": audit.actor, - "action_type": audit.action_type, - "details": audit.details, - "metadata": audit.metadata, - "ip_address": audit.ip_address, - "user_agent": audit.user_agent, - "timestamp": audit.created_at.isoformat() if audit.created_at else None - } - for audit in audit_entries - ], - "total": db.query(AuditTrail).filter( - AuditTrail.project_id == project_id - ).count(), - "limit": limit, - "offset": offset - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Project {project_id} not found" - ) - - -@app.get("/audit/actions") -async def get_user_actions( - actor_type: str = None, - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get user actions.""" - try: - query = db.query(UserAction).order_by(UserAction.created_at.desc()) - - if actor_type: - query = query.filter(UserAction.actor_type == actor_type) - - actions = query.offset(offset).limit(limit).all() - - return { - "actions": [ - { - "id": action.id, - "history_id": action.history_id, - "action_type": action.action_type, - "actor_type": action.actor_type, - "actor_name": action.actor_name, - "description": action.action_description, - "data": action.action_data, - "ip_address": action.ip_address, - "user_agent": action.user_agent, - "timestamp": action.created_at.isoformat() if action.created_at else None - } - for action in actions - ], - "total": query.count(), - "limit": limit, - "offset": offset - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/audit/actions/{project_id}") -async def get_project_user_actions( - project_id: str, - actor_type: str = None, - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get user actions for a specific project.""" - history = db.query(ProjectHistory).filter( - ProjectHistory.project_id == project_id - ).first() - - if not history: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Project {project_id} not found" - ) - - try: - query = db.query(UserAction).filter( - UserAction.history_id == history.id - ).order_by(UserAction.created_at.desc()) - - if actor_type: - query = query.filter(UserAction.actor_type == actor_type) - - actions = query.offset(offset).limit(limit).all() - - return { - "project_id": project_id, - "actions": [ - { - "id": action.id, - "action_type": action.action_type, - "actor_type": action.actor_type, - "actor_name": action.actor_name, - "description": action.action_description, - "data": action.action_data, - "timestamp": action.created_at.isoformat() if action.created_at else None - } - for action in actions - ], - "total": query.count(), - "limit": limit, - "offset": offset - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/audit/history") -async def get_project_history( - project_id: str = None, - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get project history.""" - try: - if project_id: - history = db.query(ProjectHistory).filter( - ProjectHistory.project_id == project_id - ).first() - - if not history: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Project {project_id} not found" - ) - - return { - "project": { - "id": history.id, - "project_id": history.project_id, - "project_name": history.project_name, - "description": history.description, - "status": history.status, - "progress": history.progress, - "message": history.message, - "created_at": history.created_at.isoformat(), - "updated_at": history.updated_at.isoformat() if history.updated_at else None, - "completed_at": history.completed_at.isoformat() if history.completed_at else None, - "error_message": history.error_message - } - } - else: - histories = db.query(ProjectHistory).offset(offset).limit(limit).all() - return { - "histories": [ - { - "id": h.id, - "project_id": h.project_id, - "project_name": h.project_name, - "status": h.status, - "progress": h.progress, - "message": h.message, - "created_at": h.created_at.isoformat() - } - for h in histories - ], - "total": db.query(ProjectHistory).count(), - "limit": limit, - "offset": offset - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/audit/history/{project_id}") -async def get_detailed_project_history( - project_id: str, - db: Session = Depends(get_db) -): - """Get detailed history for a project including all audit data.""" - history = db.query(ProjectHistory).filter( - ProjectHistory.project_id == project_id - ).first() - - if not history: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Project {project_id} not found" - ) - - try: - # Get all logs - logs = db.query(ProjectLog).filter( - ProjectLog.history_id == history.id - ).order_by(ProjectLog.created_at.desc()).all() - - # Get all user actions - actions = db.query(UserAction).filter( - UserAction.history_id == history.id - ).order_by(UserAction.created_at.desc()).all() - - # Get all audit trail entries - audit_entries = db.query(AuditTrail).filter( - AuditTrail.project_id == project_id - ).order_by(AuditTrail.created_at.desc()).all() - - # Get all UI snapshots - snapshots = db.query(UISnapshot).filter( - UISnapshot.history_id == history.id - ).order_by(UISnapshot.created_at.desc()).all() - - # Get PR data - pr = db.query(PullRequestData).filter( - PullRequestData.history_id == history.id - ).first() - pr_data = None - if pr: - pr_data = { - "pr_number": pr.pr_number, - "pr_title": pr.pr_title, - "pr_body": pr.pr_body, - "pr_state": pr.pr_state, - "pr_url": pr.pr_url, - "created_at": pr.created_at.isoformat() if pr.created_at else None - } - - return { - "project": { - "id": history.id, - "project_id": history.project_id, - "project_name": history.project_name, - "description": history.description, - "status": history.status, - "progress": history.progress, - "message": history.message, - "created_at": history.created_at.isoformat(), - "updated_at": history.updated_at.isoformat() if history.updated_at else None, - "completed_at": history.completed_at.isoformat() if history.completed_at else None, - "error_message": history.error_message - }, - "logs": [ - { - "id": log.id, - "level": log.log_level, - "message": log.log_message, - "timestamp": log.timestamp.isoformat() if log.timestamp else None - } - for log in logs - ], - "actions": [ - { - "id": action.id, - "action_type": action.action_type, - "actor_type": action.actor_type, - "actor_name": action.actor_name, - "description": action.action_description, - "data": action.action_data, - "timestamp": action.created_at.isoformat() if action.created_at else None - } - for action in actions - ], - "audit_trail": [ - { - "id": audit.id, - "action": audit.action, - "actor": audit.actor, - "action_type": audit.action_type, - "details": audit.details, - "metadata": audit.metadata, - "ip_address": audit.ip_address, - "user_agent": audit.user_agent, - "timestamp": audit.created_at.isoformat() if audit.created_at else None - } - for audit in audit_entries - ], - "snapshots": [ - { - "id": snapshot.id, - "data": snapshot.snapshot_data, - "created_at": snapshot.created_at.isoformat() - } - for snapshot in snapshots - ], - "pr_data": pr_data - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/audit/prompts") -async def get_prompts( - project_id: str = None, - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get prompts submitted by users.""" - try: - query = db.query(AuditTrail).filter( - AuditTrail.action_type == "PROMPT" - ).order_by(AuditTrail.created_at.desc()) - - if project_id: - query = query.filter(AuditTrail.project_id == project_id) - - prompts = query.offset(offset).limit(limit).all() - - return { - "prompts": [ - { - "id": audit.id, - "project_id": audit.project_id, - "actor": audit.actor, - "details": audit.details, - "metadata": audit.metadata, - "timestamp": audit.created_at.isoformat() if audit.created_at else None - } - for audit in prompts - ], - "total": query.count(), - "limit": limit, - "offset": offset - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - - -@app.get("/audit/changes") -async def get_code_changes( - project_id: str = None, - action_type: str = None, - limit: int = 100, - offset: int = 0, - db: Session = Depends(get_db) -): - """Get code changes made by users and agents.""" - try: - query = db.query(AuditTrail).filter( - AuditTrail.action_type.in_(["CREATE", "UPDATE", "DELETE", "CODE_CHANGE"]) - ).order_by(AuditTrail.created_at.desc()) - - if project_id: - query = query.filter(AuditTrail.project_id == project_id) - if action_type: - query = query.filter(AuditTrail.action_type == action_type) - - changes = query.offset(offset).limit(limit).all() - - return { - "changes": [ - { - "id": audit.id, - "project_id": audit.project_id, - "action": audit.action, - "actor": audit.actor, - "action_type": audit.action_type, - "details": audit.details, - "metadata": audit.metadata, - "timestamp": audit.created_at.isoformat() if audit.created_at else None - } - for audit in changes - ], - "total": query.count(), - "limit": limit, - "offset": offset - } - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) \ No newline at end of file +if __name__ == '__main__': + print('Please start the app with the "uvicorn" command as shown in the start.sh script') \ No newline at end of file diff --git a/ai_software_factory/requirements.txt b/ai_software_factory/requirements.txt index 437e03e..03e6194 100644 --- a/ai_software_factory/requirements.txt +++ b/ai_software_factory/requirements.txt @@ -15,4 +15,5 @@ isort==5.13.2 flake8==6.1.0 mypy==1.7.1 httpx==0.25.2 -jinja2==3.1.3 \ No newline at end of file +nicegui==1.4.19 +pydantic-settings==2.1.0 \ No newline at end of file diff --git a/ai_software_factory/start.sh b/ai_software_factory/start.sh new file mode 100644 index 0000000..c189154 --- /dev/null +++ b/ai_software_factory/start.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# use path of this example as working directory; enables starting this script from anywhere +cd "$(dirname "$0")" + +if [ "$1" = "prod" ]; then + echo "Starting Uvicorn server in production mode..." + # we also use a single worker in production mode so socket.io connections are always handled by the same worker + uvicorn main:app --workers 1 --log-level info --port 80 +elif [ "$1" = "dev" ]; then + echo "Starting Uvicorn server in development mode..." + # reload implies workers = 1 + uvicorn main:app --reload --log-level debug --port 8000 +else + echo "Invalid parameter. Use 'prod' or 'dev'." + exit 1 +fi \ No newline at end of file