CyberArena / app /api /ai_recommend.py
Hussien Haider
H
05a9e8e
Raw
History Blame Contribute Delete
7.11 kB
"""``/api/ai/recommend`` — AI-powered next-challenge recommendation.
Analyzes the user's completion history and picks the challenge type they've
done least, then selects a random challenge from that pool. Returns the full
set of parameters the front-end needs to start a ``TrainingSession``.
"""
import random
from typing import Optional
import httpx
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from app.core.auth import get_current_user
from app.core.config import SUPABASE_URL, SUPABASE_ANON_KEY
from app.services.supabase_service import supabase_headers
router = APIRouter()
class AiRecommendResponse(BaseModel):
categoryId: str
pathId: str
moduleId: str
teamRole: str
challengeId: str
title: str
reason: str # short AI-style justification
# All five challenge types with their table, front-end params, and
# the category key used in ``user_completions``.
_CHALLENGE_TYPES: list[dict] = [
{
"table": "encryption_challenges",
"team": "red",
"categoryId": "encryption",
"pathId": "cryptography",
"moduleId": "crypto",
"completion_category": "crypto",
},
{
"table": "code_fixing_challenges",
"team": "blue",
"categoryId": "code_analysis",
"pathId": "web-security",
"moduleId": "code-fixing",
"completion_category": "code-fixing",
},
{
"table": "log_analysis_challenges",
"team": "blue",
"categoryId": "log_analysis",
"pathId": "log-analysis",
"moduleId": "log-analysis",
"completion_category": "log-analysis",
},
{
"table": "vulnerability_hunter_challenges",
"team": "blue",
"categoryId": "vulnerability_hunter",
"pathId": "secure-coding",
"moduleId": "vulnerability-hunter",
"completion_category": "vulnerability-hunter",
},
{
"table": "web_exploitation_challenges",
"team": "red",
"categoryId": "web_exploitation",
"pathId": "xss",
"moduleId": "web-exploitation",
"completion_category": "web-exploitation",
},
]
# Short AI-sounding reasons keyed by completion_category
_REASONS: dict[str, list[str]] = {
"crypto": [
"نقاط ضعفك في التشفير تحتاج تقوية — جرّب هذا التحدي لتصقل مهاراتك",
"التشفير هو خط الدفاع الأول في الأمن السيبراني — حان وقت التمرّن",
"مهاراتك في فك التشفير بحاجة إلى صقل — هذا التحدي سيساعدك",
],
"code-fixing": [
"اكتشاف الثغرات في الكود مهارة لا غنى عنها — طوّرها بهذا التحدي",
"المبرمجون المحترفون يقرؤون الكود بحثاً عن الثغرات — كن منهم",
"هذا التحدي سيدربك على تحديد نقاط الضعف في التطبيقات",
],
"log-analysis": [
"تحليل السجلات يكشف الهجمات قبل فوات الأوان — درّب عينك",
"محللو SOC المتميزون يميّزون الأنماط في السجلات — تدرب هنا",
"هذا التحدي يحاكي هجوماً حقيقياً — حلّله وكن جاهزاً",
],
"vulnerability-hunter": [
"صائد الثغرات الحقيقي لا يفوته أي خلل — اختبر نفسك",
"كلما اكتشفت ثغرات أكثر، كلما أصبحت أقوى — هذا هو تحديك التالي",
"ثغرات اليوم هي ثغرات الغد — تدرب على اكتشافها الآن",
],
"web-exploitation": [
"الهجمات الإلكترونية تستهدف الويب أولاً — كن مستعداً",
"هذا التحدي يحاكي هجوماً حقيقياً على تطبيقات الويب",
"الـ Bug Bounty يبدأ بفهم الثغرات — هذا هو تحديك التالي",
],
}
@router.get("/api/ai/recommend")
async def get_ai_recommendation(user: dict = Depends(get_current_user)):
if not SUPABASE_URL or not SUPABASE_ANON_KEY:
return {"error": "AI recommendation unavailable"}
# 1. Get completions per category for this user
completions = await _fetch_user_completions(user["user_id"])
if completions is None:
completions = {}
# 2. Sort challenge types by completion count (ascending)
scored = []
for ct in _CHALLENGE_TYPES:
cat = ct["completion_category"]
count = completions.get(cat, 0)
scored.append((count, ct))
scored.sort(key=lambda x: x[0])
# 3. Try each type from least-completed to most, pick one with available challenges
for count, ct in scored:
row = await _fetch_random_challenge(ct["table"], ct["team"])
if row is None:
continue
reason = _pick_reason(ct["completion_category"])
return AiRecommendResponse(
categoryId=ct["categoryId"],
pathId=ct["pathId"],
moduleId=ct["moduleId"],
teamRole=ct["team"],
challengeId=row["id"],
title=row.get("title", "Untitled Challenge"),
reason=reason,
)
return {"error": "لا توجد تحديات متاحة حالياً"}
async def _fetch_user_completions(user_id: str) -> Optional[dict[str, int]]:
"""Return {category: count} for this user, or None on error."""
url = f"{SUPABASE_URL}/rest/v1/user_completions?user_id=eq.{user_id}&select=category"
try:
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(url, headers=supabase_headers())
if r.status_code == 200:
counts: dict[str, int] = {}
for row in r.json():
cat = row.get("category", "")
if cat:
counts[cat] = counts.get(cat, 0) + 1
return counts
except Exception as e:
print(f"[ai_recommend] fetch completions error: {e}")
return None
async def _fetch_random_challenge(table: str, team_role: str) -> Optional[dict]:
"""Pick a random challenge from the given pool table."""
url = (
f"{SUPABASE_URL}/rest/v1/{table}"
f"?team_role=eq.{team_role}&select=id,title"
f"&limit=20&order=created_at.desc"
)
try:
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(url, headers=supabase_headers())
if r.status_code == 200:
rows = r.json()
if rows:
return random.choice(rows)
except Exception as e:
print(f"[ai_recommend] {table}: {e}")
return None
def _pick_reason(completion_category: str) -> str:
reasons = _REASONS.get(completion_category)
if not reasons:
return "التحدي الأمثل لمستواك الحالي — جرّبه الآن"
return random.choice(reasons)