File size: 14,386 Bytes
50925ca 57017a4 1520141 57017a4 1520141 50925ca 9a71787 50925ca 1520141 50925ca 5019340 50925ca 57017a4 50925ca 57017a4 50925ca 1520141 50925ca 57017a4 50925ca 57017a4 50925ca 5019340 50925ca 5019340 50925ca 5019340 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | import asyncio
import json
from datetime import datetime, timedelta
import redis
from sqlalchemy import update
from sqlalchemy.future import select
from app.config import settings
from contextlib import asynccontextmanager
_task_engine = None
_task_session_factory = None
def get_task_session_factory():
global _task_engine, _task_session_factory
if _task_session_factory is None:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from uuid import uuid4
database_url = settings.DATABASE_URL
if database_url.startswith("postgresql://"):
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
_task_engine = create_async_engine(
database_url,
pool_size=5,
max_overflow=10,
pool_pre_ping=True,
connect_args={
"statement_cache_size": 0,
"prepared_statement_cache_size": 0,
"prepared_statement_name_func": lambda: f"__asyncpg_{uuid4().hex}__"
}
)
_task_session_factory = async_sessionmaker(bind=_task_engine, class_=AsyncSession, expire_on_commit=False)
return _task_session_factory
@asynccontextmanager
async def local_session():
factory = get_task_session_factory()
async with factory() as session:
yield session
from app import models
from app.utils.ai import run_code_audit, generate_system_design
from loguru import logger
# Redis client for publishing streaming progress events
redis_client = redis.Redis.from_url(settings.REDIS_URL)
def publish_event(job_id: str, event_type: str, data: dict = None):
payload = {"event": event_type}
if data:
payload.update(data)
raw_payload = json.dumps(payload)
redis_client.publish(f"job:{job_id}", raw_payload)
try:
redis_client.rpush(f"job:{job_id}:events", raw_payload)
redis_client.expire(f"job:{job_id}:events", 3600)
except Exception as e:
logger.error(f"Failed to cache event in Redis: {e}")
async def run_audit_task(project_id_str: str, job_id: str, files: list, file_type: str):
logger.info(f"Starting audit task for project {project_id_str}, job {job_id}")
# Publish initial agent start events
publish_event(job_id, "agent_started", {"agent": "connecting"})
publish_event(job_id, "agent_complete", {"agent": "connecting"})
publish_event(job_id, "agent_started", {"agent": "sre"})
publish_event(job_id, "agent_started", {"agent": "backend"})
publish_event(job_id, "agent_started", {"agent": "infrastructure"})
publish_event(job_id, "agent_started", {"agent": "cloud_architect"})
try:
# Run AI audit analysis
report_data = await run_code_audit(files, file_type)
# Calculate stats for confidence score
files_count = len(files)
total_chars = sum(len(f["content"]) for f in files)
has_config_files = any(
any(f["filename"].endswith(ext) for ext in [".json", ".yaml", ".yml", ".toml", ".config", ".env"])
for f in files
)
has_docker = any("Dockerfile" in f["filename"] for f in files)
# Confidence score weighting:
# files>=10(+20), config_files(+15), docker(+15), chars>=50K(+20), not_truncated(+20)
confidence_score = 0
if files_count >= 10:
confidence_score += 20
if has_config_files:
confidence_score += 15
if has_docker:
confidence_score += 15
if total_chars >= 50000:
confidence_score += 20
confidence_score += 20 # Assuming not truncated by default
if confidence_score >= 70:
confidence_level = "high"
elif confidence_score >= 40:
confidence_level = "medium"
else:
confidence_level = "low"
confidence_dict = {
"level": confidence_level,
"score": confidence_score,
"label": f"{confidence_level.capitalize()} Confidence",
"based_on": report_data.get("confidence", {}).get("based_on", ["Initial file scan completed"]),
"limitations": report_data.get("confidence", {}).get("limitations", ["No runtime metrics available"]),
"to_increase_confidence": report_data.get("confidence", {}).get("to_increase_confidence", ["Upload load test scripts"])
}
# Enforce server-side score calculation
# overall_score = round(sre*0.30 + backend*0.30 + infra*0.20 + cloud*0.20)
sre_score = report_data.get("sre_score", 80)
backend_score = report_data.get("backend_score", 80)
infra_score = report_data.get("infra_score", 80)
cloud_score = report_data.get("cloud_score", 80)
overall_score = round(
sre_score * 0.30 +
backend_score * 0.30 +
infra_score * 0.20 +
cloud_score * 0.20
)
# Enforce capacity tiers
# 0-30→50-200 · 31-50→200-1K · 51-65→1K-5K · 66-79→5K-25K · 80-89→25K-100K · 90-100→100K-500K
if overall_score <= 30:
safe, peak = "50-200 DAU", "200-1K DAU"
elif overall_score <= 50:
safe, peak = "200-1K DAU", "1K-5K DAU"
elif overall_score <= 65:
safe, peak = "1K-5K DAU", "5K-25K DAU"
elif overall_score <= 79:
safe, peak = "5K-25K DAU", "25K-100K DAU"
elif overall_score <= 89:
safe, peak = "25K-100K DAU", "100K-500K DAU"
else:
safe, peak = "100K-500K DAU", "500K-2M DAU"
capacity_dict = {
"safe_range": safe,
"peak_range": peak,
"description": report_data.get("capacity_estimate", {}).get("description", "DAU estimate based on static architecture scanning."),
"reasoning": report_data.get("capacity_estimate", {}).get("reasoning", "Database concurrency constraints set maximum peaks."),
"confidence": report_data.get("capacity_estimate", {}).get("confidence", "Medium")
}
# Prepare Agent reports mapping
agents_data = report_data.get("agents", {})
# Re-set agents overall scores if modified
if "sre" in agents_data:
agents_data["sre"]["score"] = sre_score
if "backend" in agents_data:
agents_data["backend"]["score"] = backend_score
if "infrastructure" in agents_data:
agents_data["infrastructure"]["score"] = infra_score
if "cloud_architect" in agents_data:
agents_data["cloud_architect"]["score"] = cloud_score
disclaimer = (
"This score is based on static code analysis only. It does "
"not guarantee runtime performance. Validate with load testing "
"before production launch."
)
async with local_session() as session:
# Load project
import uuid
project_uuid = uuid.UUID(project_id_str)
proj_result = await session.execute(select(models.Project).where(models.Project.id == project_uuid))
project = proj_result.scalars().first()
if not project:
raise Exception("Project not found")
# Create Audit Report
audit_report = models.AuditReport(
project_id=project.id,
overall_score=overall_score,
confidence=confidence_dict,
capacity_estimate=capacity_dict,
agents=agents_data,
top_critical_issues=report_data.get("top_critical_issues", []),
quick_wins=report_data.get("quick_wins", []),
benchmark_percentile=report_data.get("benchmark_percentile", 75.0),
score_disclaimer=disclaimer,
files_analyzed=files_count,
files_skipped=0,
was_truncated=False
)
session.add(audit_report)
project.status = "complete"
# Update user stats
user_result = await session.execute(select(models.User).where(models.User.id == project.user_id))
user = user_result.scalars().first()
if user:
user.total_audits += 1
session.add(user)
await session.commit()
# Complete agents progress steps
publish_event(job_id, "agent_complete", {"agent": "sre"})
publish_event(job_id, "agent_complete", {"agent": "backend"})
publish_event(job_id, "agent_complete", {"agent": "infrastructure"})
publish_event(job_id, "agent_complete", {"agent": "cloud_architect"})
publish_event(job_id, "job_complete", {"audit_id": str(project.id)})
logger.info(f"Audit job {job_id} completed successfully")
except Exception as e:
logger.error(f"Audit task failed: {e}")
async with local_session() as session:
import uuid
project_uuid = uuid.UUID(project_id_str)
proj_result = await session.execute(select(models.Project).where(models.Project.id == project_uuid))
project = proj_result.scalars().first()
if project:
project.status = "failed"
project.error_msg = str(e)
await session.commit()
publish_event(job_id, "job_failed", {"error": str(e)})
async def run_design_task(project_id_str: str, job_id: str, idea_prompt: str):
logger.info(f"Starting design task for project {project_id_str}, job {job_id}")
# Emit progress events without artificial delay
publish_event(job_id, "agent_started", {"agent": "connecting"})
publish_event(job_id, "agent_complete", {"agent": "connecting"})
publish_event(job_id, "agent_started", {"agent": "sre"})
publish_event(job_id, "agent_started", {"agent": "backend"})
publish_event(job_id, "agent_started", {"agent": "infrastructure"})
publish_event(job_id, "agent_started", {"agent": "cloud_architect"})
try:
# Run AI System Design
design_data = await generate_system_design(idea_prompt)
async with local_session() as session:
# Load project
import uuid
project_uuid = uuid.UUID(project_id_str)
proj_result = await session.execute(select(models.Project).where(models.Project.id == project_uuid))
project = proj_result.scalars().first()
if not project:
raise Exception("Project not found")
# Create System Design
system_design = models.SystemDesign(
project_id=project.id,
idea_prompt=idea_prompt,
title=design_data.get("title", "System Architecture Blueprint"),
founder_summary=design_data.get("founder_summary", ""),
engineer_summary=design_data.get("engineer_summary", ""),
architecture_type=design_data.get("architecture_type", "Hybrid"),
reasoning=design_data.get("reasoning", ""),
stack=design_data.get("stack", {}),
database_design=design_data.get("database_design", {}),
api_design=design_data.get("api_design", {}),
infrastructure=design_data.get("infrastructure", {}),
reliability=design_data.get("reliability", {}),
cost_estimates=design_data.get("cost_estimates", {}),
diagram=design_data.get("diagram", {"nodes": [], "edges": []})
)
session.add(system_design)
project.status = "complete"
# Update user stats
user_result = await session.execute(select(models.User).where(models.User.id == project.user_id))
user = user_result.scalars().first()
if user:
user.total_designs += 1
session.add(user)
await session.commit()
publish_event(job_id, "agent_complete", {"agent": "cloud_architect"})
publish_event(job_id, "job_complete", {"design_id": str(project.id)})
logger.info(f"Design job {job_id} completed successfully")
except Exception as e:
logger.error(f"Design task failed: {e}")
async with local_session() as session:
import uuid
project_uuid = uuid.UUID(project_id_str)
proj_result = await session.execute(select(models.Project).where(models.Project.id == project_uuid))
project = proj_result.scalars().first()
if project:
project.status = "failed"
project.error_msg = str(e)
await session.commit()
publish_event(job_id, "job_failed", {"error": str(e)})
# RQ requires functions to be importable at module level (no inner/local functions)
# This is the sync wrapper for GitHub audit jobs — called by RQ worker
def run_github_audit_job(proj_id: str, job_id: str, repo: str, branch: str, access_token: str, file_type: str):
"""Module-level RQ job for GitHub-sourced audits. Picklable by RQ."""
from app.utils.github_client import get_repo_files
files = get_repo_files(access_token, repo, branch)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(run_audit_task(proj_id, job_id, files, file_type))
finally:
loop.close()
# Sync wrappers for RQ — each creates its own event loop to avoid cross-thread conflicts
def audit_job_wrapper(project_id_str: str, job_id: str, files: list, file_type: str):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(run_audit_task(project_id_str, job_id, files, file_type))
finally:
loop.close()
def design_job_wrapper(project_id_str: str, job_id: str, idea_prompt: str):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(run_design_task(project_id_str, job_id, idea_prompt))
finally:
loop.close()
|