Spaces:
Sleeping
Sleeping
Hussien Haider commited on
Commit ·
05a9e8e
1
Parent(s): 11fa4b6
- app/api/__init__.py +2 -0
- app/api/ai_recommend.py +187 -0
- app/main.py +2 -0
app/api/__init__.py
CHANGED
|
@@ -15,6 +15,7 @@ from . import onevone
|
|
| 15 |
from . import mentor
|
| 16 |
from . import chatbot
|
| 17 |
from . import daily_challenge
|
|
|
|
| 18 |
|
| 19 |
__all__ = [
|
| 20 |
"auth",
|
|
@@ -27,4 +28,5 @@ __all__ = [
|
|
| 27 |
"mentor",
|
| 28 |
"chatbot",
|
| 29 |
"daily_challenge",
|
|
|
|
| 30 |
]
|
|
|
|
| 15 |
from . import mentor
|
| 16 |
from . import chatbot
|
| 17 |
from . import daily_challenge
|
| 18 |
+
from . import ai_recommend
|
| 19 |
|
| 20 |
__all__ = [
|
| 21 |
"auth",
|
|
|
|
| 28 |
"mentor",
|
| 29 |
"chatbot",
|
| 30 |
"daily_challenge",
|
| 31 |
+
"ai_recommend",
|
| 32 |
]
|
app/api/ai_recommend.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``/api/ai/recommend`` — AI-powered next-challenge recommendation.
|
| 2 |
+
|
| 3 |
+
Analyzes the user's completion history and picks the challenge type they've
|
| 4 |
+
done least, then selects a random challenge from that pool. Returns the full
|
| 5 |
+
set of parameters the front-end needs to start a ``TrainingSession``.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import random
|
| 9 |
+
from typing import Optional
|
| 10 |
+
|
| 11 |
+
import httpx
|
| 12 |
+
from fastapi import APIRouter, Depends
|
| 13 |
+
from pydantic import BaseModel
|
| 14 |
+
|
| 15 |
+
from app.core.auth import get_current_user
|
| 16 |
+
from app.core.config import SUPABASE_URL, SUPABASE_ANON_KEY
|
| 17 |
+
from app.services.supabase_service import supabase_headers
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
router = APIRouter()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class AiRecommendResponse(BaseModel):
|
| 24 |
+
categoryId: str
|
| 25 |
+
pathId: str
|
| 26 |
+
moduleId: str
|
| 27 |
+
teamRole: str
|
| 28 |
+
challengeId: str
|
| 29 |
+
title: str
|
| 30 |
+
reason: str # short AI-style justification
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# All five challenge types with their table, front-end params, and
|
| 34 |
+
# the category key used in ``user_completions``.
|
| 35 |
+
_CHALLENGE_TYPES: list[dict] = [
|
| 36 |
+
{
|
| 37 |
+
"table": "encryption_challenges",
|
| 38 |
+
"team": "red",
|
| 39 |
+
"categoryId": "encryption",
|
| 40 |
+
"pathId": "cryptography",
|
| 41 |
+
"moduleId": "crypto",
|
| 42 |
+
"completion_category": "crypto",
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"table": "code_fixing_challenges",
|
| 46 |
+
"team": "blue",
|
| 47 |
+
"categoryId": "code_analysis",
|
| 48 |
+
"pathId": "web-security",
|
| 49 |
+
"moduleId": "code-fixing",
|
| 50 |
+
"completion_category": "code-fixing",
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"table": "log_analysis_challenges",
|
| 54 |
+
"team": "blue",
|
| 55 |
+
"categoryId": "log_analysis",
|
| 56 |
+
"pathId": "log-analysis",
|
| 57 |
+
"moduleId": "log-analysis",
|
| 58 |
+
"completion_category": "log-analysis",
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"table": "vulnerability_hunter_challenges",
|
| 62 |
+
"team": "blue",
|
| 63 |
+
"categoryId": "vulnerability_hunter",
|
| 64 |
+
"pathId": "secure-coding",
|
| 65 |
+
"moduleId": "vulnerability-hunter",
|
| 66 |
+
"completion_category": "vulnerability-hunter",
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"table": "web_exploitation_challenges",
|
| 70 |
+
"team": "red",
|
| 71 |
+
"categoryId": "web_exploitation",
|
| 72 |
+
"pathId": "xss",
|
| 73 |
+
"moduleId": "web-exploitation",
|
| 74 |
+
"completion_category": "web-exploitation",
|
| 75 |
+
},
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
# Short AI-sounding reasons keyed by completion_category
|
| 79 |
+
_REASONS: dict[str, list[str]] = {
|
| 80 |
+
"crypto": [
|
| 81 |
+
"نقاط ضعفك في التشفير تحتاج تقوية — جرّب هذا التحدي لتصقل مهاراتك",
|
| 82 |
+
"التشفير هو خط الدفاع الأول في الأمن السيبراني — حان وقت التمرّن",
|
| 83 |
+
"مهاراتك في فك التشفير بحاجة إلى صقل — هذا التحدي سيساعدك",
|
| 84 |
+
],
|
| 85 |
+
"code-fixing": [
|
| 86 |
+
"اكتشاف الثغرات في الكود مهارة لا غنى عنها — طوّرها بهذا التحدي",
|
| 87 |
+
"المبرمجون المحترفون يقرؤون الكود بحثاً عن الثغرات — كن منهم",
|
| 88 |
+
"هذا التحدي سيدربك على تحديد نقاط الضعف في التطبيقات",
|
| 89 |
+
],
|
| 90 |
+
"log-analysis": [
|
| 91 |
+
"تحليل السجلات يكشف الهجمات قبل فوات الأوان — درّب عينك",
|
| 92 |
+
"محللو SOC المتميزون يميّزون الأنماط في السجلات — تدرب هنا",
|
| 93 |
+
"هذا التحدي يحاكي هجوماً حقيقياً — حلّله وكن جاهزاً",
|
| 94 |
+
],
|
| 95 |
+
"vulnerability-hunter": [
|
| 96 |
+
"صائد الثغرات الحقيقي لا يفوته أي خلل — اختبر نفسك",
|
| 97 |
+
"كلما اكتشفت ثغرات أكثر، كلما أصبحت أقوى — هذا هو تحديك التالي",
|
| 98 |
+
"ثغرات اليوم هي ثغرات الغد — تدرب على اكتشافها الآن",
|
| 99 |
+
],
|
| 100 |
+
"web-exploitation": [
|
| 101 |
+
"الهجمات الإلكترونية تستهدف الويب أولاً — كن مستعداً",
|
| 102 |
+
"هذا التحدي يحاكي هجوماً حقيقياً على تطبيقات الويب",
|
| 103 |
+
"الـ Bug Bounty يبدأ بفهم الثغرات — هذا هو تحديك التالي",
|
| 104 |
+
],
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@router.get("/api/ai/recommend")
|
| 109 |
+
async def get_ai_recommendation(user: dict = Depends(get_current_user)):
|
| 110 |
+
if not SUPABASE_URL or not SUPABASE_ANON_KEY:
|
| 111 |
+
return {"error": "AI recommendation unavailable"}
|
| 112 |
+
|
| 113 |
+
# 1. Get completions per category for this user
|
| 114 |
+
completions = await _fetch_user_completions(user["user_id"])
|
| 115 |
+
if completions is None:
|
| 116 |
+
completions = {}
|
| 117 |
+
|
| 118 |
+
# 2. Sort challenge types by completion count (ascending)
|
| 119 |
+
scored = []
|
| 120 |
+
for ct in _CHALLENGE_TYPES:
|
| 121 |
+
cat = ct["completion_category"]
|
| 122 |
+
count = completions.get(cat, 0)
|
| 123 |
+
scored.append((count, ct))
|
| 124 |
+
scored.sort(key=lambda x: x[0])
|
| 125 |
+
|
| 126 |
+
# 3. Try each type from least-completed to most, pick one with available challenges
|
| 127 |
+
for count, ct in scored:
|
| 128 |
+
row = await _fetch_random_challenge(ct["table"], ct["team"])
|
| 129 |
+
if row is None:
|
| 130 |
+
continue
|
| 131 |
+
|
| 132 |
+
reason = _pick_reason(ct["completion_category"])
|
| 133 |
+
return AiRecommendResponse(
|
| 134 |
+
categoryId=ct["categoryId"],
|
| 135 |
+
pathId=ct["pathId"],
|
| 136 |
+
moduleId=ct["moduleId"],
|
| 137 |
+
teamRole=ct["team"],
|
| 138 |
+
challengeId=row["id"],
|
| 139 |
+
title=row.get("title", "Untitled Challenge"),
|
| 140 |
+
reason=reason,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
return {"error": "لا توجد تحديات متاحة حالياً"}
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
async def _fetch_user_completions(user_id: str) -> Optional[dict[str, int]]:
|
| 147 |
+
"""Return {category: count} for this user, or None on error."""
|
| 148 |
+
url = f"{SUPABASE_URL}/rest/v1/user_completions?user_id=eq.{user_id}&select=category"
|
| 149 |
+
try:
|
| 150 |
+
async with httpx.AsyncClient(timeout=10) as client:
|
| 151 |
+
r = await client.get(url, headers=supabase_headers())
|
| 152 |
+
if r.status_code == 200:
|
| 153 |
+
counts: dict[str, int] = {}
|
| 154 |
+
for row in r.json():
|
| 155 |
+
cat = row.get("category", "")
|
| 156 |
+
if cat:
|
| 157 |
+
counts[cat] = counts.get(cat, 0) + 1
|
| 158 |
+
return counts
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"[ai_recommend] fetch completions error: {e}")
|
| 161 |
+
return None
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
async def _fetch_random_challenge(table: str, team_role: str) -> Optional[dict]:
|
| 165 |
+
"""Pick a random challenge from the given pool table."""
|
| 166 |
+
url = (
|
| 167 |
+
f"{SUPABASE_URL}/rest/v1/{table}"
|
| 168 |
+
f"?team_role=eq.{team_role}&select=id,title"
|
| 169 |
+
f"&limit=20&order=created_at.desc"
|
| 170 |
+
)
|
| 171 |
+
try:
|
| 172 |
+
async with httpx.AsyncClient(timeout=10) as client:
|
| 173 |
+
r = await client.get(url, headers=supabase_headers())
|
| 174 |
+
if r.status_code == 200:
|
| 175 |
+
rows = r.json()
|
| 176 |
+
if rows:
|
| 177 |
+
return random.choice(rows)
|
| 178 |
+
except Exception as e:
|
| 179 |
+
print(f"[ai_recommend] {table}: {e}")
|
| 180 |
+
return None
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _pick_reason(completion_category: str) -> str:
|
| 184 |
+
reasons = _REASONS.get(completion_category)
|
| 185 |
+
if not reasons:
|
| 186 |
+
return "التحدي الأمثل لمستواك الحالي — جرّبه الآن"
|
| 187 |
+
return random.choice(reasons)
|
app/main.py
CHANGED
|
@@ -90,6 +90,7 @@ from app.api import ( # noqa: E402 (import after middleware so order is right)
|
|
| 90 |
mentor as _mentor,
|
| 91 |
chatbot as _chatbot,
|
| 92 |
daily_challenge as _daily_challenge,
|
|
|
|
| 93 |
)
|
| 94 |
|
| 95 |
|
|
@@ -105,6 +106,7 @@ def _include_all_routers(app: FastAPI) -> None:
|
|
| 105 |
app.include_router(_mentor.router)
|
| 106 |
app.include_router(_chatbot.router)
|
| 107 |
app.include_router(_daily_challenge.router)
|
|
|
|
| 108 |
|
| 109 |
|
| 110 |
_include_all_routers(app)
|
|
|
|
| 90 |
mentor as _mentor,
|
| 91 |
chatbot as _chatbot,
|
| 92 |
daily_challenge as _daily_challenge,
|
| 93 |
+
ai_recommend as _ai_recommend,
|
| 94 |
)
|
| 95 |
|
| 96 |
|
|
|
|
| 106 |
app.include_router(_mentor.router)
|
| 107 |
app.include_router(_chatbot.router)
|
| 108 |
app.include_router(_daily_challenge.router)
|
| 109 |
+
app.include_router(_ai_recommend.router)
|
| 110 |
|
| 111 |
|
| 112 |
_include_all_routers(app)
|