Hussien Haider commited on
Commit
05a9e8e
·
1 Parent(s): 11fa4b6
Files changed (3) hide show
  1. app/api/__init__.py +2 -0
  2. app/api/ai_recommend.py +187 -0
  3. 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)