Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException, Body | |
| from app import schemas | |
| from app.services.llm_engine import llm_engine | |
| from app.services.skill_manager import skill_manager | |
| import pandas as pd | |
| import pickle | |
| import ast | |
| import os | |
| from sklearn.metrics.pairwise import linear_kernel | |
| from app.services.psych_service import psych_service | |
| from typing import List | |
| app = FastAPI(title="MORA - Mentori AI Assistant") | |
| # --- GLOBAL MODELS STORE --- | |
| models = { | |
| 'df': None, | |
| 'tfidf': None, | |
| 'matrix': None | |
| } | |
| SKILL_KEYWORDS = [] | |
| def load_skill_keywords(): | |
| global SKILL_KEYWORDS | |
| try: | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| csv_path = os.path.join(current_dir, "data", "Skill Keywords.csv") | |
| df = pd.read_csv(csv_path) | |
| SKILL_KEYWORDS = df['keyword'].dropna().tolist() | |
| print(f"β Berhasil memuat {len(SKILL_KEYWORDS)} keywords skill.") | |
| except Exception as e: | |
| print(f"β οΈ Gagal memuat dataset keyword: {e}") | |
| SKILL_KEYWORDS = [] | |
| # Fungsi Pembantu: Mencari keyword dalam pesan user | |
| def find_keywords_in_text(user_text: str): | |
| found = [] | |
| text_lower = " " + user_text.lower() + " " # Tambah spasi biar aman deteksi kata pendek | |
| for k in SKILL_KEYWORDS: | |
| # Cek sederhana: Apakah keyword ada di dalam pesan? | |
| # Untuk kata pendek (<3 huruf) seperti "C", "R", "Go", kita pakai spasi agar tidak match "Car" atau "Goat" | |
| if len(k) < 3: | |
| if f" {k.lower()} " in text_lower: | |
| found.append(k) | |
| else: | |
| if k.lower() in text_lower: | |
| found.append(k) | |
| # Hapus duplikat dan kembalikan | |
| return list(set(found)) | |
| # --- 1. STARTUP: LOAD MODEL .PKL --- | |
| def load_models(): | |
| print("π Loading Pre-trained Models...") | |
| # Menggunakan Absolute Path agar aman dijalankan dari mana saja | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| base_dir = os.path.dirname(current_dir) | |
| artifacts_dir = os.path.join(base_dir, "model_artifacts") | |
| try: | |
| with open(os.path.join(artifacts_dir, 'courses_df.pkl'), 'rb') as f: | |
| models['df'] = pickle.load(f) | |
| with open(os.path.join(artifacts_dir, 'tfidf_vectorizer.pkl'), 'rb') as f: | |
| models['tfidf'] = pickle.load(f) | |
| with open(os.path.join(artifacts_dir, 'tfidf_matrix.pkl'), 'rb') as f: | |
| models['matrix'] = pickle.load(f) | |
| print(f"β Models Loaded Successfully from: {artifacts_dir}") | |
| except Exception as e: | |
| print(f"β Error Loading Models: {e}") | |
| print(f"π Pastikan folder 'model_artifacts' ada di: {base_dir}") | |
| # --- 2. ENDPOINT REKOMENDASI (ML POWERED) --- | |
| async def generate_recommendations(req: schemas.UserProfile): | |
| user_data = req.model_dump() if hasattr(req, 'model_dump') else req.dict() | |
| # Fungsi ini sekarang me-return List murni (karena sudah di-unwrap di service) | |
| result_list = await llm_engine.generate_curriculum_stateless(user_data) | |
| return result_list | |
| # --- 3. ENDPOINT CHAT ROUTER --- | |
| # app/main.py (Bagian process_chat saja) | |
| async def process_chat(req: schemas.ChatRequest): | |
| available_skill_names = [] | |
| role_data = None | |
| # Cek apakah Role ada isinya? (Safety check) | |
| if req.role and req.role.strip() != "": | |
| role_data = skill_manager.get_role_data(req.role) | |
| if role_data: | |
| available_skill_names = [s['name'] for s in role_data['sub_skills']] | |
| # --- [Keyword Search Logic Tetap Ada] --- | |
| found_keywords = find_keywords_in_text(req.message) | |
| if found_keywords: | |
| keyword_context = ", ".join(found_keywords) | |
| dataset_status = "FOUND" | |
| else: | |
| keyword_context = "NONE" | |
| dataset_status = "NOT_FOUND" | |
| # --- [UPDATE BARU: Ektrak Silabus Lengkap] --- | |
| # Kita buat string rapi berisi Skill + Topik-topiknya | |
| found_keywords = find_keywords_in_text(req.message) | |
| # Siapkan context string untuk dikirim ke LLM | |
| if found_keywords: | |
| # Jika ketemu: "User bertanya tentang: Python, SQL" | |
| keyword_context = ", ".join(found_keywords) | |
| dataset_status = "FOUND" | |
| else: | |
| # Jika tidak ketemu | |
| keyword_context = "NONE" | |
| dataset_status = "NOT_FOUND" | |
| # [PENTING] Konversi History ke Dict agar tidak error di LLM | |
| history_dicts = [m.model_dump() if hasattr(m, 'model_dump') else m.dict() for m in req.history] | |
| # Kirim parameter lengkap ke Router | |
| intent = await llm_engine.process_user_intent( | |
| user_text=req.message, | |
| available_skills=available_skill_names, | |
| user_role=req.role, | |
| history=history_dicts # Tambahan agar AI ingat konteks | |
| ) | |
| action = intent.get('action', 'CASUAL_CHAT') | |
| detected_skills_list = intent.get('detected_skills', []) | |
| user_role_is_empty = not req.role or req.role.strip() == "" | |
| restricted_actions = ["START_EXAM", "GET_RECOMMENDATION", "CHECK_PROGRESS"] | |
| if action in restricted_actions and user_role_is_empty: | |
| print(f"DEBUG: Role Kosong mencoba {action} -> BELOKKAN KE CASUAL_CHAT") | |
| action = "CASUAL_CHAT" | |
| # ============================================================ | |
| final_reply = "" | |
| response_data = None | |
| # 3. Logic | |
| if action == "START_EXAM": | |
| target_skill_ids = [] | |
| # A. Cari ID untuk SEMUA skill yang dideteksi (Looping) | |
| if detected_skills_list and role_data: | |
| for ds in detected_skills_list: | |
| for s in role_data['sub_skills']: | |
| # Cek kemiripan nama | |
| if s['name'].lower() in ds.lower() or ds.lower() in s['name'].lower(): | |
| if s['id'] not in target_skill_ids: | |
| target_skill_ids.append(s['id']) | |
| # B. Jika ada skill yang valid, generate soal untuk MASING-MASING skill | |
| if target_skill_ids: | |
| exam_list = [] | |
| for skid in target_skill_ids: | |
| # Ambil level user | |
| user_current_level = req.current_skills.get(skid, "beginner") | |
| skill_details = skill_manager.get_skill_details(req.role, skid) | |
| level_data = skill_details['levels'].get(user_current_level, skill_details['levels']['beginner']) | |
| # Generate Soal (Sequential) | |
| llm_res = await llm_engine.generate_question(level_data['exam_topics'], user_current_level) | |
| # Masukkan ke list soal | |
| exam_list.append({ | |
| "skill_id": skid, | |
| "skill_name": skill_details['name'], | |
| "level": user_current_level, | |
| "question": llm_res['question_text'], | |
| "context": llm_res['grading_rubric'] | |
| }) | |
| # C. Format Response Baru (Multi-Exam) | |
| response_data = { | |
| "mode": "multiple_exams", # Penanda buat frontend | |
| "exams": exam_list # List soal ada di sini | |
| } | |
| skill_display = ", ".join([x['skill_name'] for x in exam_list]) | |
| final_reply = f"Siap! Saya siapkan {len(exam_list)} ujian untukmu: **{skill_display}**. Silakan kerjakan satu per satu di bawah ini! π" | |
| else: | |
| action = "CASUAL_CHAT" | |
| final_reply = await llm_engine.casual_chat( | |
| req.message, | |
| [m.dict() for m in req.history], | |
| keyword_context, | |
| dataset_status | |
| ) | |
| elif action == "START_PSYCH_TEST": | |
| response_data = {"trigger_psych_test": True} | |
| final_reply = "Tenang, Mora punya tes kepribadian singkat untuk membantumu memilih job role antara **AI Engineer** atau **Front-End Developer**. Yuk coba sekarang! π" | |
| elif action == "GET_RECOMMENDATION": | |
| response_data = {"trigger_recommendation": True} | |
| final_reply = "Sedang menganalisis kebutuhan belajarmu..." | |
| elif action == "CHECK_PROGRESS": | |
| response_data = {"trigger_progress_report": True} | |
| final_reply = "Siap! Berikut adalah ringkasan progress belajar kamu sejauh ini. Silakan dicek di dashboard ya! ππ" | |
| elif action == "CASUAL_CHAT": | |
| print(f"DEBUG ROLE STATUS: '{req.role}' -> is_empty={user_role_is_empty}") | |
| history_dicts = [m.model_dump() if hasattr(m, 'model_dump') else m.dict() for m in req.history] | |
| reply_text = await llm_engine.casual_chat( | |
| user_text=req.message, | |
| history=history_dicts, | |
| is_role_empty=user_role_is_empty | |
| ) | |
| final_reply = reply_text | |
| return schemas.ChatResponse( | |
| reply=final_reply, | |
| action_type=action, | |
| data=response_data | |
| ) | |
| async def submit_exam(sub: schemas.AnswerSubmission): | |
| evaluation = await llm_engine.evaluate_answer( | |
| user_answer=sub.user_answer, | |
| question_context={ | |
| "question_text": "REFER TO CONTEXT", | |
| "grading_rubric": sub.question_context | |
| } | |
| ) | |
| is_passed = evaluation['is_correct'] and evaluation['score'] >= 70 | |
| suggested_lvl = "intermediate" if is_passed else None # Logika sederhana | |
| return schemas.EvaluationResponse( | |
| is_correct=evaluation['is_correct'], | |
| score=evaluation['score'], | |
| feedback=evaluation['feedback'], | |
| passed=is_passed, | |
| suggested_new_level=suggested_lvl | |
| ) | |
| # --- 5. ENDPOINT PROGRESS --- | |
| async def get_progress_analysis(data: schemas.ProgressData): | |
| # Konversi objek Pydantic ke Dictionary biasa | |
| progress_dict = data.dict() | |
| # Panggil LLM khusus analisis | |
| analysis_text = await llm_engine.analyze_progress( | |
| user_name=data.user_name, | |
| progress_data=progress_dict | |
| ) | |
| return {"analysis": analysis_text} | |
| # ========================================== | |
| # ENDPOINT PSIKOLOGI (JOB ROLE TEST) | |
| # ========================================== | |
| def get_psych_questions(): | |
| """Mengambil daftar soal tes kepribadian.""" | |
| return psych_service.get_all_questions() | |
| async def submit_psych_test(req: schemas.PsychSubmitRequest): | |
| """Menerima jawaban user, hitung skor, dan minta analisis LLM.""" | |
| # 1. Hitung Skor secara matematis | |
| result = psych_service.calculate_result(req.answers) | |
| winner = result["winner"] | |
| scores = result["scores"] | |
| traits = result["traits"] | |
| # 2. Minta LLM buatkan kata-kata mutiara/analisis | |
| analysis_text = await llm_engine.analyze_psych_result(winner, traits) | |
| return schemas.PsychResultResponse( | |
| suggested_role=winner, | |
| analysis=analysis_text, | |
| scores=scores | |
| ) |