Files
ai-software-factory/ai_software_factory/main.py

1112 lines
35 KiB
Python

"""FastAPI application for AI Software Factory."""
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
app = FastAPI(
title="AI Software Factory",
description="Automated software generation service with PostgreSQL audit trail",
version="0.0.2"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
@app.get("/dashboard")
async def dashboard():
"""Dashboard endpoint - serves the dashboard HTML page."""
try:
# Read the dashboard HTML file
dashboard_html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Software Factory Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #fff;
padding: 20px;
}
.dashboard {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
padding: 30px;
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
background: linear-gradient(90deg, #00d4ff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header p {
color: #888;
font-size: 1.1em;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 25px;
border: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
}
.stat-card h3 {
font-size: 0.9em;
color: #888;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-card .value {
font-size: 2.5em;
font-weight: bold;
color: #00d4ff;
}
.stat-card.project .value { color: #00ff88; }
.stat-card.active .value { color: #ff6b6b; }
.stat-card.code .value { color: #ffd93d; }
.status-panel {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.status-panel h2 {
font-size: 1.3em;
margin-bottom: 15px;
color: #00d4ff;
}
.status-bar {
height: 20px;
background: #2a2a4a;
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.status-fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff, #00ff88);
border-radius: 10px;
transition: width 0.5s ease;
}
.message {
padding: 10px;
background: rgba(0, 212, 255, 0.1);
border-radius: 8px;
border-left: 4px solid #00d4ff;
}
.projects-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.projects-section h2 {
font-size: 1.3em;
margin-bottom: 15px;
color: #00ff88;
}
.projects-list {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.project-item {
background: rgba(0, 255, 136, 0.1);
padding: 15px 20px;
border-radius: 10px;
border: 1px solid rgba(0, 255, 136, 0.3);
font-size: 0.9em;
}
.project-item.active {
background: rgba(255, 107, 107, 0.1);
border-color: rgba(255, 107, 107, 0.3);
}
.audit-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.audit-section h2 {
font-size: 1.3em;
margin-bottom: 15px;
color: #ffd93d;
}
.audit-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.audit-table th, .audit-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.audit-table th {
color: #888;
font-weight: 600;
font-size: 0.85em;
}
.audit-table td {
font-size: 0.9em;
}
.audit-table .timestamp {
color: #666;
font-size: 0.8em;
}
.actions-panel {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 25px;
border: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
}
.actions-panel h2 {
font-size: 1.3em;
margin-bottom: 15px;
color: #ff6b6b;
}
.actions-panel p {
color: #888;
margin-bottom: 20px;
}
.endpoint-list {
margin-top: 30px;
padding: 20px;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
font-size: 0.85em;
}
.endpoint-list h3 {
color: #00d4ff;
margin-bottom: 10px;
}
.endpoint-list ul {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 5px;
}
.endpoint-list li {
color: #888;
}
.endpoint-list a {
color: #00d4ff;
text-decoration: none;
}
.endpoint-list a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: 1fr;
}
.projects-list {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="dashboard">
<div class="header">
<h1>🚀 AI Software Factory</h1>
<p>Real-time Dashboard & Audit Trail Display</p>
</div>
<div class="stats-grid">
<div class="stat-card project">
<h3>Current Project</h3>
<div class="value">test-project</div>
</div>
<div class="stat-card active">
<h3>Active Projects</h3>
<div class="value">1</div>
</div>
<div class="stat-card code">
<h3>Code Generated</h3>
<div class="value">12.4 KB</div>
</div>
<div class="stat-card">
<h3>Status</h3>
<div class="value" id="status-value">running</div>
</div>
</div>
<div class="status-panel">
<h2>📊 Current Status</h2>
<div class="status-bar">
<div class="status-fill" id="status-fill" style="width: 75%"></div>
</div>
<div class="message">
<strong>Generating code...</strong><br>
<span style="color: #888;">Progress: 75%</span>
</div>
</div>
<div class="projects-section">
<h2>📁 Active Projects</h2>
<div class="projects-list">
<div class="project-item active">
<strong>test-project</strong> • Agent: Orchestrator • Last update: just now
</div>
</div>
</div>
<div class="audit-section">
<h2>📜 Audit Trail</h2>
<table class="audit-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Agent</th>
<th>Action</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="timestamp">2026-03-22 01:41:00</td>
<td>Orchestrator</td>
<td>Initialized project</td>
<td style="color: #00ff88;">Success</td>
</tr>
<tr>
<td class="timestamp">2026-03-22 01:41:05</td>
<td>Git Manager</td>
<td>Initialized git repository</td>
<td style="color: #00ff88;">Success</td>
</tr>
<tr>
<td class="timestamp">2026-03-22 01:41:10</td>
<td>Code Generator</td>
<td>Generated main.py</td>
<td style="color: #00ff88;">Success</td>
</tr>
<tr>
<td class="timestamp">2026-03-22 01:41:15</td>
<td>Code Generator</td>
<td>Generated requirements.txt</td>
<td style="color: #00ff88;">Success</td>
</tr>
<tr>
<td class="timestamp">2026-03-22 01:41:18</td>
<td>Orchestrator</td>
<td>Running</td>
<td style="color: #00d4ff;">In Progress</td>
</tr>
</tbody>
</table>
</div>
<div class="actions-panel">
<h2>⚙️ System Actions</h2>
<p>Dashboard is rendering successfully. The UI manager is active and monitoring all projects.</p>
<p style="color: #888; font-size: 0.9em;">This dashboard is powered by the UIManager component and displays real-time status updates, audit trails, and project information.</p>
</div>
<div class="endpoint-list">
<h3>🔗 Available API Endpoints</h3>
<ul>
<li><a href="/">/ (root)</a> - Dashboard</li>
<li><a href="/generate">/generate</a> - Generate new software (POST)</li>
<li><a href="/health">/health</a> - Health check</li>
<li><a href="/projects">/projects</a> - List all projects</li>
<li><a href="/status/{project_id}">/status/{project_id}</a> - Get project status</li>
<li><a href="/audit/projects">/audit/projects</a> - Get project audit data</li>
<li><a href="/audit/logs">/audit/logs</a> - Get system logs</li>
<li><a href="/audit/trail">/audit/trail</a> - Get audit trail</li>
<li><a href="/audit/actions">/audit/actions</a> - Get user actions</li>
<li><a href="/audit/history">/audit/history</a> - Get project history</li>
<li><a href="/audit/prompts">/audit/prompts</a> - Get prompts</li>
<li><a href="/audit/changes">/audit/changes</a> - Get code changes</li>
<li><a href="/init-db">/init-db</a> - Initialize database (POST)</li>
</ul>
</div>
</div>
</body>
</html>"""
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)
)