aidlmrza fjarsra commited on
Commit
32f773f
ยท
verified ยท
1 Parent(s): 3cb1eb4

Update app/main.py (#2)

Browse files

- Update app/main.py (640ebcbc29a546048350ff0452c0692220a55d66)


Co-authored-by: Fajar Syafatoni Raihannadif <fjarsra@users.noreply.huggingface.co>

Files changed (1) hide show
  1. app/main.py +330 -348
app/main.py CHANGED
@@ -1,349 +1,331 @@
1
- from fastapi import FastAPI, HTTPException, Body
2
- from app import schemas
3
- from app.services.llm_engine import llm_engine
4
- from app.services.skill_manager import skill_manager
5
- import pandas as pd
6
- import pickle
7
- import ast
8
- import os
9
- from sklearn.metrics.pairwise import linear_kernel
10
- from app.services.psych_service import psych_service
11
- from typing import List
12
-
13
- app = FastAPI(title="MORA - AI Learning Assistant (Final)")
14
-
15
- # --- GLOBAL MODELS STORE ---
16
- models = {
17
- 'df': None,
18
- 'tfidf': None,
19
- 'matrix': None
20
- }
21
-
22
- SKILL_KEYWORDS = []
23
-
24
- @app.on_event("startup")
25
- def load_skill_keywords():
26
- global SKILL_KEYWORDS
27
- try:
28
- current_dir = os.path.dirname(os.path.abspath(__file__))
29
- csv_path = os.path.join(current_dir, "data", "Skill Keywords.csv")
30
- df = pd.read_csv(csv_path)
31
- SKILL_KEYWORDS = df['keyword'].dropna().tolist()
32
- print(f"โœ… Berhasil memuat {len(SKILL_KEYWORDS)} keywords skill.")
33
- except Exception as e:
34
- print(f"โš ๏ธ Gagal memuat dataset keyword: {e}")
35
- SKILL_KEYWORDS = []
36
-
37
- # Fungsi Pembantu: Mencari keyword dalam pesan user
38
- def find_keywords_in_text(user_text: str):
39
- found = []
40
- text_lower = " " + user_text.lower() + " " # Tambah spasi biar aman deteksi kata pendek
41
-
42
- for k in SKILL_KEYWORDS:
43
- # Cek sederhana: Apakah keyword ada di dalam pesan?
44
- # Untuk kata pendek (<3 huruf) seperti "C", "R", "Go", kita pakai spasi agar tidak match "Car" atau "Goat"
45
- if len(k) < 3:
46
- if f" {k.lower()} " in text_lower:
47
- found.append(k)
48
- else:
49
- if k.lower() in text_lower:
50
- found.append(k)
51
-
52
- # Hapus duplikat dan kembalikan
53
- return list(set(found))
54
-
55
- # --- 1. STARTUP: LOAD MODEL .PKL ---
56
- @app.on_event("startup")
57
- def load_models():
58
- print("๐Ÿ”„ Loading Pre-trained Models...")
59
-
60
- # Menggunakan Absolute Path agar aman dijalankan dari mana saja
61
- current_dir = os.path.dirname(os.path.abspath(__file__))
62
- base_dir = os.path.dirname(current_dir)
63
- artifacts_dir = os.path.join(base_dir, "model_artifacts")
64
-
65
- try:
66
- with open(os.path.join(artifacts_dir, 'courses_df.pkl'), 'rb') as f:
67
- models['df'] = pickle.load(f)
68
- with open(os.path.join(artifacts_dir, 'tfidf_vectorizer.pkl'), 'rb') as f:
69
- models['tfidf'] = pickle.load(f)
70
- with open(os.path.join(artifacts_dir, 'tfidf_matrix.pkl'), 'rb') as f:
71
- models['matrix'] = pickle.load(f)
72
- print(f"โœ… Models Loaded Successfully from: {artifacts_dir}")
73
- except Exception as e:
74
- print(f"โŒ Error Loading Models: {e}")
75
- print(f"๐Ÿ‘‰ Pastikan folder 'model_artifacts' ada di: {base_dir}")
76
-
77
- # --- 2. ENDPOINT REKOMENDASI (ML POWERED) ---
78
- @app.post("/recommendations")
79
- def get_recommendations(user: schemas.UserProfile):
80
- df = models.get('df')
81
- tfidf = models.get('tfidf')
82
- matrix = models.get('matrix')
83
-
84
- # Jika model belum siap, return kosong biar gak crash
85
- if df is None: return []
86
-
87
- # Mapping Level agar komputer mengerti urutan
88
- LEVEL_MAP = {
89
- 'beginner': 1, 'dasar': 1, 'pemula': 1,
90
- 'intermediate': 2, 'menengah': 2,
91
- 'advanced': 3, 'mahir': 3, 'expert': 3, 'profesional': 3
92
- }
93
-
94
- final_recs = []
95
- # Set course yang sudah diambil agar tidak disarankan lagi
96
- seen_courses = set(user.completed_courses)
97
-
98
- # --- LOGIKA CORE: Loop setiap 'Gap' Skill User ---
99
- for gap in user.missing_skills:
100
- skill_query = gap.skill_name
101
- target_lvl_str = gap.target_level.lower()
102
- target_lvl_num = LEVEL_MAP.get(target_lvl_str, 1) # Default 1 (Pemula)
103
-
104
- try:
105
- # 1. Transform nama skill jadi vektor angka
106
- vec = tfidf.transform([skill_query.lower()])
107
-
108
- # 2. Hitung kemiripan (Cosine Similarity)
109
- scores = linear_kernel(vec, matrix).flatten()
110
-
111
- # 3. Ambil Top 15 kandidat
112
- indices = scores.argsort()[:-15:-1]
113
-
114
- for idx in indices:
115
- score = scores[idx]
116
- # Filter awal: Skip jika kemiripan text terlalu rendah
117
- if score < 0.1: continue
118
-
119
- course = df.iloc[idx]
120
- c_id = int(course['course_id'])
121
-
122
- if c_id in seen_courses: continue
123
-
124
- # --- FILTER LEVEL (ADAPTIVE) ---
125
- c_lvl_str = str(course['level_name']).lower()
126
- c_lvl_num = LEVEL_MAP.get(c_lvl_str, 1)
127
-
128
- # Logic: Jangan kasih course yang levelnya DI ATAS target (kejauhan)
129
- if c_lvl_num > target_lvl_num: continue
130
-
131
- # Logic Badge (Penanda)
132
- if c_lvl_num == target_lvl_num:
133
- badge = "๐ŸŽฏ Target Pas"
134
- else:
135
- badge = "โ†บ Review Dasar"
136
-
137
- # Parse Tutorial List (karena di CSV formatnya string)
138
- tuts = course['tutorial_list']
139
- if isinstance(tuts, str):
140
- try: tuts = ast.literal_eval(tuts)
141
- except: tuts = []
142
-
143
- # Tambahkan ke hasil
144
- final_recs.append({
145
- "skill": skill_query,
146
- "current_level": gap.target_level,
147
- "course_to_take": course['course_name'],
148
- "chapters": tuts[:3], # Ambil 3 bab pertama
149
- "match_score": round(score * 100, 1),
150
- "badge": badge
151
- })
152
- seen_courses.add(c_id)
153
-
154
- except Exception as e:
155
- print(f"Error processing {skill_query}: {e}")
156
- continue
157
-
158
- # Urutkan berdasarkan skor kecocokan tertinggi
159
- final_recs = sorted(final_recs, key=lambda x: x['match_score'], reverse=True)
160
-
161
- return final_recs[:5] # Kembalikan Top 5
162
-
163
- # --- 3. ENDPOINT CHAT ROUTER ---
164
- # app/main.py (Bagian process_chat saja)
165
-
166
- @app.post("/chat/process", response_model=schemas.ChatResponse)
167
- async def process_chat(req: schemas.ChatRequest):
168
- role_data = skill_manager.get_role_data(req.role)
169
- # --- [UPDATE BARU: Ektrak Silabus Lengkap] ---
170
- # Kita buat string rapi berisi Skill + Topik-topiknya
171
- found_keywords = find_keywords_in_text(req.message)
172
-
173
- # Siapkan context string untuk dikirim ke LLM
174
- if found_keywords:
175
- # Jika ketemu: "User bertanya tentang: Python, SQL"
176
- keyword_context = ", ".join(found_keywords)
177
- dataset_status = "FOUND"
178
- else:
179
- # Jika tidak ketemu
180
- keyword_context = "NONE"
181
- dataset_status = "NOT_FOUND"
182
-
183
- # 2. Router
184
- intent = await llm_engine.process_user_intent(req.message, [])
185
-
186
- action = intent.get('action')
187
- # PERUBAHAN 1: Ambil List skills, bukan single skill
188
- detected_skills_list = intent.get('detected_skills', [])
189
-
190
- final_reply = ""
191
- response_data = None
192
-
193
- # 3. Logic
194
- if action == "START_EXAM":
195
- target_skill_ids = []
196
-
197
- # A. Cari ID untuk SEMUA skill yang dideteksi (Looping)
198
- if detected_skills_list and role_data:
199
- for ds in detected_skills_list:
200
- for s in role_data['sub_skills']:
201
- # Cek kemiripan nama
202
- if s['name'].lower() in ds.lower() or ds.lower() in s['name'].lower():
203
- if s['id'] not in target_skill_ids:
204
- target_skill_ids.append(s['id'])
205
-
206
- # B. Jika ada skill yang valid, generate soal untuk MASING-MASING skill
207
- if target_skill_ids:
208
- exam_list = []
209
-
210
- for skid in target_skill_ids:
211
- # Ambil level user
212
- user_current_level = req.current_skills.get(skid, "beginner")
213
- skill_details = skill_manager.get_skill_details(req.role, skid)
214
- level_data = skill_details['levels'].get(user_current_level, skill_details['levels']['beginner'])
215
-
216
- # Generate Soal (Sequential)
217
- llm_res = await llm_engine.generate_question(level_data['exam_topics'], user_current_level)
218
-
219
- # Masukkan ke list soal
220
- exam_list.append({
221
- "skill_id": skid,
222
- "skill_name": skill_details['name'],
223
- "level": user_current_level,
224
- "question": llm_res['question_text'],
225
- "context": llm_res['grading_rubric']
226
- })
227
-
228
- # C. Format Response Baru (Multi-Exam)
229
- response_data = {
230
- "mode": "multiple_exams", # Penanda buat frontend
231
- "exams": exam_list # List soal ada di sini
232
- }
233
-
234
- skill_display = ", ".join([x['skill_name'] for x in exam_list])
235
- final_reply = f"Siap! Saya siapkan {len(exam_list)} ujian untukmu: **{skill_display}**. Silakan kerjakan satu per satu di bawah ini! ๐Ÿ‘‡"
236
-
237
- else:
238
- action = "CASUAL_CHAT"
239
- final_reply = await llm_engine.casual_chat(
240
- req.message,
241
- [m.dict() for m in req.history],
242
- keyword_context,
243
- dataset_status
244
- )
245
-
246
- elif action == "START_PSYCH_TEST":
247
- response_data = {"trigger_psych_test": True}
248
- final_reply = "Tenang, Mora punya tes kepribadian singkat untuk membantumu memilih job role antara **AI Engineer** atau **Front-End Developer**. Yuk coba sekarang! ๐Ÿ‘‡"
249
-
250
- elif action == "GET_RECOMMENDATION":
251
- response_data = {"trigger_recommendation": True}
252
- final_reply = "Sedang menganalisis kebutuhan belajarmu..."
253
-
254
- elif action == "CASUAL_CHAT":
255
- final_reply = await llm_engine.casual_chat(
256
- req.message,
257
- [m.dict() for m in req.history],
258
- keyword_context,
259
- dataset_status
260
- )
261
-
262
- return schemas.ChatResponse(
263
- reply=final_reply,
264
- action_type=action,
265
- data=response_data
266
- )
267
-
268
-
269
- @app.post("/exam/submit", response_model=schemas.EvaluationResponse)
270
- async def submit_exam(sub: schemas.AnswerSubmission):
271
- evaluation = await llm_engine.evaluate_answer(
272
- user_answer=sub.user_answer,
273
- question_context={
274
- "question_text": "REFER TO CONTEXT",
275
- "grading_rubric": sub.question_context
276
- }
277
- )
278
-
279
- is_passed = evaluation['is_correct'] and evaluation['score'] >= 70
280
- suggested_lvl = "intermediate" if is_passed else None # Logika sederhana
281
-
282
- return schemas.EvaluationResponse(
283
- is_correct=evaluation['is_correct'],
284
- score=evaluation['score'],
285
- feedback=evaluation['feedback'],
286
- passed=is_passed,
287
- suggested_new_level=suggested_lvl
288
- )
289
-
290
- # --- 5. ENDPOINT PROGRESS ---
291
- @app.post("/progress")
292
- def get_progress(req: schemas.ProgressRequest):
293
- role_data = skill_manager.get_role_data(req.role)
294
- if not role_data: return []
295
-
296
- progress_report = []
297
- level_weight = {"beginner": 0, "intermediate": 1, "advanced": 2}
298
-
299
- for skill in role_data['sub_skills']:
300
- skill_id = skill['id']
301
- user_level = req.current_skills.get(skill_id, "beginner")
302
-
303
- # Hitung Persen
304
- current_stage = level_weight.get(user_level, 0)
305
- percent = int((current_stage / 3) * 100)
306
- if user_level == "beginner": percent = 5
307
- elif user_level == "intermediate": percent = 50
308
- elif user_level == "advanced": percent = 80
309
-
310
- # Sisa tutorial (dummy/static logic karena detail ada di rekomendasi)
311
- remaining = 0
312
-
313
- progress_report.append({
314
- "skill_name": skill['name'],
315
- "current_level": user_level,
316
- "progress_percent": percent,
317
- "remaining_tutorials": remaining
318
- })
319
-
320
- return progress_report
321
-
322
- # ==========================================
323
- # ENDPOINT PSIKOLOGI (JOB ROLE TEST)
324
- # ==========================================
325
-
326
- @app.get("/psych/questions", response_model=List[schemas.PsychQuestionItem])
327
- def get_psych_questions():
328
- """Mengambil daftar soal tes kepribadian."""
329
- return psych_service.get_all_questions()
330
-
331
- @app.post("/psych/submit", response_model=schemas.PsychResultResponse)
332
- async def submit_psych_test(req: schemas.PsychSubmitRequest):
333
- """Menerima jawaban user, hitung skor, dan minta analisis LLM."""
334
-
335
- # 1. Hitung Skor secara matematis
336
- result = psych_service.calculate_result(req.answers)
337
-
338
- winner = result["winner"]
339
- scores = result["scores"]
340
- traits = result["traits"]
341
-
342
- # 2. Minta LLM buatkan kata-kata mutiara/analisis
343
- analysis_text = await llm_engine.analyze_psych_result(winner, traits)
344
-
345
- return schemas.PsychResultResponse(
346
- suggested_role=winner,
347
- analysis=analysis_text,
348
- scores=scores
349
  )
 
1
+ from fastapi import FastAPI, HTTPException, Body
2
+ from app import schemas
3
+ from app.services.llm_engine import llm_engine
4
+ from app.services.skill_manager import skill_manager
5
+ import pandas as pd
6
+ import pickle
7
+ import ast
8
+ import os
9
+ from sklearn.metrics.pairwise import linear_kernel
10
+ from app.services.psych_service import psych_service
11
+ from typing import List
12
+
13
+ app = FastAPI(title="MORA - AI Learning Assistant (Final)")
14
+
15
+ # --- GLOBAL MODELS STORE ---
16
+ models = {
17
+ 'df': None,
18
+ 'tfidf': None,
19
+ 'matrix': None
20
+ }
21
+
22
+ SKILL_KEYWORDS = []
23
+
24
+ @app.on_event("startup")
25
+ def load_skill_keywords():
26
+ global SKILL_KEYWORDS
27
+ try:
28
+ current_dir = os.path.dirname(os.path.abspath(__file__))
29
+ csv_path = os.path.join(current_dir, "data", "Skill Keywords.csv")
30
+ df = pd.read_csv(csv_path)
31
+ SKILL_KEYWORDS = df['keyword'].dropna().tolist()
32
+ print(f"โœ… Berhasil memuat {len(SKILL_KEYWORDS)} keywords skill.")
33
+ except Exception as e:
34
+ print(f"โš ๏ธ Gagal memuat dataset keyword: {e}")
35
+ SKILL_KEYWORDS = []
36
+
37
+ # Fungsi Pembantu: Mencari keyword dalam pesan user
38
+ def find_keywords_in_text(user_text: str):
39
+ found = []
40
+ text_lower = " " + user_text.lower() + " " # Tambah spasi biar aman deteksi kata pendek
41
+
42
+ for k in SKILL_KEYWORDS:
43
+ # Cek sederhana: Apakah keyword ada di dalam pesan?
44
+ # Untuk kata pendek (<3 huruf) seperti "C", "R", "Go", kita pakai spasi agar tidak match "Car" atau "Goat"
45
+ if len(k) < 3:
46
+ if f" {k.lower()} " in text_lower:
47
+ found.append(k)
48
+ else:
49
+ if k.lower() in text_lower:
50
+ found.append(k)
51
+
52
+ # Hapus duplikat dan kembalikan
53
+ return list(set(found))
54
+
55
+ # --- 1. STARTUP: LOAD MODEL .PKL ---
56
+ @app.on_event("startup")
57
+ def load_models():
58
+ print("๐Ÿ”„ Loading Pre-trained Models...")
59
+
60
+ # Menggunakan Absolute Path agar aman dijalankan dari mana saja
61
+ current_dir = os.path.dirname(os.path.abspath(__file__))
62
+ base_dir = os.path.dirname(current_dir)
63
+ artifacts_dir = os.path.join(base_dir, "model_artifacts")
64
+
65
+ try:
66
+ with open(os.path.join(artifacts_dir, 'courses_df.pkl'), 'rb') as f:
67
+ models['df'] = pickle.load(f)
68
+ with open(os.path.join(artifacts_dir, 'tfidf_vectorizer.pkl'), 'rb') as f:
69
+ models['tfidf'] = pickle.load(f)
70
+ with open(os.path.join(artifacts_dir, 'tfidf_matrix.pkl'), 'rb') as f:
71
+ models['matrix'] = pickle.load(f)
72
+ print(f"โœ… Models Loaded Successfully from: {artifacts_dir}")
73
+ except Exception as e:
74
+ print(f"โŒ Error Loading Models: {e}")
75
+ print(f"๐Ÿ‘‰ Pastikan folder 'model_artifacts' ada di: {base_dir}")
76
+
77
+ # --- 2. ENDPOINT REKOMENDASI (ML POWERED) ---
78
+ @app.post("/recommendations")
79
+ def get_recommendations(user: schemas.UserProfile):
80
+ df = models.get('df')
81
+ tfidf = models.get('tfidf')
82
+ matrix = models.get('matrix')
83
+
84
+ # Jika model belum siap, return kosong biar gak crash
85
+ if df is None: return []
86
+
87
+ # Mapping Level agar komputer mengerti urutan
88
+ LEVEL_MAP = {
89
+ 'beginner': 1, 'dasar': 1, 'pemula': 1,
90
+ 'intermediate': 2, 'menengah': 2,
91
+ 'advanced': 3, 'mahir': 3, 'expert': 3, 'profesional': 3
92
+ }
93
+
94
+ final_recs = []
95
+ # Set course yang sudah diambil agar tidak disarankan lagi
96
+ seen_courses = set(user.completed_courses)
97
+
98
+ # --- LOGIKA CORE: Loop setiap 'Gap' Skill User ---
99
+ for gap in user.missing_skills:
100
+ skill_query = gap.skill_name
101
+ target_lvl_str = gap.target_level.lower()
102
+ target_lvl_num = LEVEL_MAP.get(target_lvl_str, 1) # Default 1 (Pemula)
103
+
104
+ try:
105
+ # 1. Transform nama skill jadi vektor angka
106
+ vec = tfidf.transform([skill_query.lower()])
107
+
108
+ # 2. Hitung kemiripan (Cosine Similarity)
109
+ scores = linear_kernel(vec, matrix).flatten()
110
+
111
+ # 3. Ambil Top 15 kandidat
112
+ indices = scores.argsort()[:-15:-1]
113
+
114
+ for idx in indices:
115
+ score = scores[idx]
116
+ # Filter awal: Skip jika kemiripan text terlalu rendah
117
+ if score < 0.1: continue
118
+
119
+ course = df.iloc[idx]
120
+ c_id = int(course['course_id'])
121
+
122
+ if c_id in seen_courses: continue
123
+
124
+ # --- FILTER LEVEL (ADAPTIVE) ---
125
+ c_lvl_str = str(course['level_name']).lower()
126
+ c_lvl_num = LEVEL_MAP.get(c_lvl_str, 1)
127
+
128
+ # Logic: Jangan kasih course yang levelnya DI ATAS target (kejauhan)
129
+ if c_lvl_num > target_lvl_num: continue
130
+
131
+ # Logic Badge (Penanda)
132
+ if c_lvl_num == target_lvl_num:
133
+ badge = "๐ŸŽฏ Target Pas"
134
+ else:
135
+ badge = "โ†บ Review Dasar"
136
+
137
+ # Parse Tutorial List (karena di CSV formatnya string)
138
+ tuts = course['tutorial_list']
139
+ if isinstance(tuts, str):
140
+ try: tuts = ast.literal_eval(tuts)
141
+ except: tuts = []
142
+
143
+ # Tambahkan ke hasil
144
+ final_recs.append({
145
+ "skill": skill_query,
146
+ "current_level": gap.target_level,
147
+ "course_to_take": course['course_name'],
148
+ "chapters": tuts[:3], # Ambil 3 bab pertama
149
+ "match_score": round(score * 100, 1),
150
+ "badge": badge
151
+ })
152
+ seen_courses.add(c_id)
153
+
154
+ except Exception as e:
155
+ print(f"Error processing {skill_query}: {e}")
156
+ continue
157
+
158
+ # Urutkan berdasarkan skor kecocokan tertinggi
159
+ final_recs = sorted(final_recs, key=lambda x: x['match_score'], reverse=True)
160
+
161
+ return final_recs[:5] # Kembalikan Top 5
162
+
163
+ # --- 3. ENDPOINT CHAT ROUTER ---
164
+ # app/main.py (Bagian process_chat saja)
165
+
166
+ @app.post("/chat/process", response_model=schemas.ChatResponse)
167
+ async def process_chat(req: schemas.ChatRequest):
168
+ role_data = skill_manager.get_role_data(req.role)
169
+ # --- [UPDATE BARU: Ektrak Silabus Lengkap] ---
170
+ # Kita buat string rapi berisi Skill + Topik-topiknya
171
+ found_keywords = find_keywords_in_text(req.message)
172
+
173
+ # Siapkan context string untuk dikirim ke LLM
174
+ if found_keywords:
175
+ # Jika ketemu: "User bertanya tentang: Python, SQL"
176
+ keyword_context = ", ".join(found_keywords)
177
+ dataset_status = "FOUND"
178
+ else:
179
+ # Jika tidak ketemu
180
+ keyword_context = "NONE"
181
+ dataset_status = "NOT_FOUND"
182
+
183
+ # 2. Router
184
+ intent = await llm_engine.process_user_intent(req.message, [])
185
+
186
+ action = intent.get('action')
187
+ # PERUBAHAN 1: Ambil List skills, bukan single skill
188
+ detected_skills_list = intent.get('detected_skills', [])
189
+
190
+ final_reply = ""
191
+ response_data = None
192
+
193
+ # 3. Logic
194
+ if action == "START_EXAM":
195
+ target_skill_ids = []
196
+
197
+ # A. Cari ID untuk SEMUA skill yang dideteksi (Looping)
198
+ if detected_skills_list and role_data:
199
+ for ds in detected_skills_list:
200
+ for s in role_data['sub_skills']:
201
+ # Cek kemiripan nama
202
+ if s['name'].lower() in ds.lower() or ds.lower() in s['name'].lower():
203
+ if s['id'] not in target_skill_ids:
204
+ target_skill_ids.append(s['id'])
205
+
206
+ # B. Jika ada skill yang valid, generate soal untuk MASING-MASING skill
207
+ if target_skill_ids:
208
+ exam_list = []
209
+
210
+ for skid in target_skill_ids:
211
+ # Ambil level user
212
+ user_current_level = req.current_skills.get(skid, "beginner")
213
+ skill_details = skill_manager.get_skill_details(req.role, skid)
214
+ level_data = skill_details['levels'].get(user_current_level, skill_details['levels']['beginner'])
215
+
216
+ # Generate Soal (Sequential)
217
+ llm_res = await llm_engine.generate_question(level_data['exam_topics'], user_current_level)
218
+
219
+ # Masukkan ke list soal
220
+ exam_list.append({
221
+ "skill_id": skid,
222
+ "skill_name": skill_details['name'],
223
+ "level": user_current_level,
224
+ "question": llm_res['question_text'],
225
+ "context": llm_res['grading_rubric']
226
+ })
227
+
228
+ # C. Format Response Baru (Multi-Exam)
229
+ response_data = {
230
+ "mode": "multiple_exams", # Penanda buat frontend
231
+ "exams": exam_list # List soal ada di sini
232
+ }
233
+
234
+ skill_display = ", ".join([x['skill_name'] for x in exam_list])
235
+ final_reply = f"Siap! Saya siapkan {len(exam_list)} ujian untukmu: **{skill_display}**. Silakan kerjakan satu per satu di bawah ini! ๐Ÿ‘‡"
236
+
237
+ else:
238
+ action = "CASUAL_CHAT"
239
+ final_reply = await llm_engine.casual_chat(
240
+ req.message,
241
+ [m.dict() for m in req.history],
242
+ keyword_context,
243
+ dataset_status
244
+ )
245
+
246
+ elif action == "START_PSYCH_TEST":
247
+ response_data = {"trigger_psych_test": True}
248
+ final_reply = "Tenang, Mora punya tes kepribadian singkat untuk membantumu memilih job role antara **AI Engineer** atau **Front-End Developer**. Yuk coba sekarang! ๐Ÿ‘‡"
249
+
250
+ elif action == "GET_RECOMMENDATION":
251
+ response_data = {"trigger_recommendation": True}
252
+ final_reply = "Sedang menganalisis kebutuhan belajarmu..."
253
+
254
+ elif action == "CASUAL_CHAT":
255
+ final_reply = await llm_engine.casual_chat(
256
+ req.message,
257
+ [m.dict() for m in req.history],
258
+ keyword_context,
259
+ dataset_status
260
+ )
261
+
262
+ return schemas.ChatResponse(
263
+ reply=final_reply,
264
+ action_type=action,
265
+ data=response_data
266
+ )
267
+
268
+
269
+ @app.post("/exam/submit", response_model=schemas.EvaluationResponse)
270
+ async def submit_exam(sub: schemas.AnswerSubmission):
271
+ evaluation = await llm_engine.evaluate_answer(
272
+ user_answer=sub.user_answer,
273
+ question_context={
274
+ "question_text": "REFER TO CONTEXT",
275
+ "grading_rubric": sub.question_context
276
+ }
277
+ )
278
+
279
+ is_passed = evaluation['is_correct'] and evaluation['score'] >= 70
280
+ suggested_lvl = "intermediate" if is_passed else None # Logika sederhana
281
+
282
+ return schemas.EvaluationResponse(
283
+ is_correct=evaluation['is_correct'],
284
+ score=evaluation['score'],
285
+ feedback=evaluation['feedback'],
286
+ passed=is_passed,
287
+ suggested_new_level=suggested_lvl
288
+ )
289
+
290
+ # --- 5. ENDPOINT PROGRESS ---
291
+ @app.post("/progress/analyze")
292
+ async def get_progress_analysis(data: schemas.ProgressData):
293
+ # Konversi objek Pydantic ke Dictionary biasa
294
+ progress_dict = data.dict()
295
+
296
+ # Panggil LLM khusus analisis
297
+ analysis_text = await llm_engine.analyze_progress(
298
+ user_name=data.user_name,
299
+ progress_data=progress_dict
300
+ )
301
+
302
+ return {"analysis": analysis_text}
303
+
304
+ # ==========================================
305
+ # ENDPOINT PSIKOLOGI (JOB ROLE TEST)
306
+ # ==========================================
307
+
308
+ @app.get("/psych/questions", response_model=List[schemas.PsychQuestionItem])
309
+ def get_psych_questions():
310
+ """Mengambil daftar soal tes kepribadian."""
311
+ return psych_service.get_all_questions()
312
+
313
+ @app.post("/psych/submit", response_model=schemas.PsychResultResponse)
314
+ async def submit_psych_test(req: schemas.PsychSubmitRequest):
315
+ """Menerima jawaban user, hitung skor, dan minta analisis LLM."""
316
+
317
+ # 1. Hitung Skor secara matematis
318
+ result = psych_service.calculate_result(req.answers)
319
+
320
+ winner = result["winner"]
321
+ scores = result["scores"]
322
+ traits = result["traits"]
323
+
324
+ # 2. Minta LLM buatkan kata-kata mutiara/analisis
325
+ analysis_text = await llm_engine.analyze_psych_result(winner, traits)
326
+
327
+ return schemas.PsychResultResponse(
328
+ suggested_role=winner,
329
+ analysis=analysis_text,
330
+ scores=scores
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  )