Spaces:
Sleeping
Sleeping
File size: 7,112 Bytes
05a9e8e | 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 | """``/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)
|