aidlmrza fjarsra commited on
Commit
61dcb91
Β·
verified Β·
1 Parent(s): e18cc34

Update app/services/llm_engine.py (#4)

Browse files

- Update app/services/llm_engine.py (7e880c519378bc43e2ddc9583ee92bcbdb13db47)


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

Files changed (1) hide show
  1. app/services/llm_engine.py +311 -272
app/services/llm_engine.py CHANGED
@@ -1,273 +1,312 @@
1
- import os
2
- import json
3
- from groq import AsyncGroq
4
- from dotenv import load_dotenv
5
-
6
- # Load environment variables
7
- load_dotenv()
8
-
9
- class LLMEngine:
10
- def __init__(self):
11
- # List untuk menampung client AsyncGroq
12
- self.clients = []
13
-
14
- # 1. Load Token Utama
15
- key1 = os.getenv("GROQ_API_KEY")
16
- if key1:
17
- try:
18
- self.clients.append(AsyncGroq(api_key=key1))
19
- except Exception as e:
20
- print(f"⚠️ Gagal memuat Token Utama: {e}")
21
-
22
- # 2. Load Token Backup
23
- key2 = os.getenv("GROQ_API_KEY_BACKUP")
24
- if key2:
25
- try:
26
- self.clients.append(AsyncGroq(api_key=key2))
27
- except Exception as e:
28
- print(f"⚠️ Gagal memuat Token Backup: {e}")
29
-
30
- print(f"βœ… LLM Engine (Async) siap dengan {len(self.clients)} Client aktif.")
31
-
32
- # --- FUNGSI RETRY (VERSI ASYNC) ---
33
- async def _execute_with_retry(self, messages, model, temperature=0.5, response_format=None):
34
- """
35
- Mencoba request Async secara bergantian.
36
- """
37
- if not self.clients:
38
- raise Exception("Tidak ada API Key Groq yang terdeteksi di .env!")
39
-
40
- last_error = Exception("Unknown Error")
41
-
42
- for i, client in enumerate(self.clients):
43
- try:
44
- # PERUBAHAN PENTING: Pakai 'await' di sini
45
- completion = await client.chat.completions.create(
46
- messages=messages,
47
- model=model,
48
- temperature=temperature,
49
- response_format=response_format
50
- )
51
- return completion.choices[0].message.content
52
-
53
- except Exception as e:
54
- print(f"⚠️ Token ke-{i+1} Gagal. Error: {e}")
55
- last_error = e
56
- # Lanjut ke client berikutnya...
57
- continue
58
-
59
- print("❌ Semua Token Gagal/Habis.")
60
- raise last_error
61
-
62
- async def process_user_intent(self, user_text: str, available_skills: list):
63
- # Ubah list skill jadi string biar AI tau menu apa aja yang ada
64
- skills_str = "\n".join([f"- {s}" for s in available_skills])
65
-
66
- system_prompt = f"""
67
- ROLE: Kamu adalah 'Router' untuk MORA, sebuah AI Learning Assistant.
68
- Tugasmu BUKAN menjawab pertanyaan, tapi mengarahkan user ke fitur yang benar.
69
-
70
- DAFTAR SKILL TERSEDIA DI DATABASE:
71
- {skills_str}
72
-
73
- INSTRUKSI UTAMA:
74
- Analisis pesan user dan tentukan ACTION JSON.
75
-
76
- 1. ACTION: "START_EXAM"
77
- - Trigger: User ingin "tes", "ujian", "uji kemampuan", "soal", atau menyebut topik teknis (SQL, Python, CV, NLP).
78
- - TUGAS PENTING (MAPPING): User sering menyebut topik spesifik (misal "SQL"). Kamu WAJIB mencocokkannya dengan "Nama Skill Tersedia" yang paling relevan.
79
- Contoh:
80
- - User: "Tes SQL" -> Detected: "Software & Data Foundations"
81
- - User: "Tes Vision" -> Detected: "Deep Learning & Computer Vision"
82
-
83
- 2. ACTION: "GET_RECOMMENDATION"
84
- - Trigger: User minta "saran", "belajar apa", "rekomendasi", "bingung mulai mana".
85
-
86
- 3. ACTION: "START_PSYCH_TEST"
87
- - Trigger: User tanya "karir", "cocok kerja apa", "tes minat".
88
-
89
- 4. ACTION: "CASUAL_CHAT"
90
- - Trigger: Hanya untuk sapaan ("Halo"), curhat, atau pertanyaan di luar konteks belajar.
91
- - JANGAN gunakan ini jika user jelas-jelas minta tes/soal.
92
-
93
- OUTPUT JSON (Hanya JSON, tanpa teks lain):
94
- {{
95
- "action": "...",
96
- "detected_skills": ["Nama Skill Database 1", "Nama Skill Database 2"] (Array berisi String nama skill persis dari daftar diatas. Kosongkan jika tidak ada.)
97
- }}
98
- """
99
-
100
- try:
101
- # Panggil retry dengan await
102
- response_content = await self._execute_with_retry(
103
- messages=[
104
- {"role": "system", "content": system_prompt},
105
- {"role": "user", "content": user_text}
106
- ],
107
- model="llama-3.3-70b-versatile",
108
- temperature=0.0,
109
- response_format={"type": "json_object"}
110
- )
111
- return json.loads(response_content)
112
- except Exception as e:
113
- print(f"Error Router: {e}")
114
- return {"action": "CASUAL_CHAT", "detected_skills": []}
115
-
116
- async def generate_question(self, topics: list, level: str):
117
- topics_str = ", ".join(topics)
118
- prompt = f"""
119
- Buatkan 1 soal esai pendek dengan konsep how, what, why untuk menguji pemahaman user
120
- Tentang topik: {topics_str}.
121
- Tingkat Kesulitan: {level}.
122
- Bahasa: Indonesia.
123
-
124
- Output JSON:
125
- {{
126
- "question_text": "Pertanyaan...",
127
- "grading_rubric": {{
128
- "keywords": ["kata1", "kata2"],
129
- "explanation_focus": "Poin utama yang harus dijelaskan"
130
- }}
131
- }}
132
- """
133
- try:
134
- response_content = await self._execute_with_retry(
135
- messages=[{"role": "user", "content": prompt}],
136
- model="llama-3.3-8b-instant",
137
- temperature=0.5,
138
- response_format={"type": "json_object"}
139
- )
140
- return json.loads(response_content)
141
- except Exception as e:
142
- return {"question_text": "Error generate soal.", "grading_rubric": {}}
143
-
144
- async def evaluate_answer(self, user_answer: str, question_context: dict):
145
- prompt = f"""
146
- Bertindaklah sebagai Dosen AI yang menilai jawaban mahasiswa.
147
-
148
- Soal/Konteks: {json.dumps(question_context)}
149
- Jawaban Mahasiswa: "{user_answer}"
150
-
151
- Tugas:
152
- 1. Beri skor 0-100.
153
- 2. Beri feedback singkat & ramah (Bahasa Indonesia).
154
- 3. Tentukan apakah jawaban BENAR secara konsep (is_correct).
155
-
156
- Output JSON:
157
- {{
158
- "score": 85,
159
- "feedback": "Penjelasanmu bagus, tapi kurang detail di bagian...",
160
- "is_correct": true
161
- }}
162
- """
163
- try:
164
- response_content = await self._execute_with_retry(
165
- messages=[{"role": "user", "content": prompt}],
166
- model="llama-3.3-70b-versatile", # Tetap pakai 70b biar penilaian akurat
167
- response_format={"type": "json_object"}
168
- )
169
-
170
- # Parsing string JSON menjadi Dictionary Python
171
- return json.loads(response_content)
172
- except:
173
- return {"score": 0, "feedback": "Error menilai.", "is_correct": False}
174
-
175
- async def casual_chat(self, user_text: str, history: list = [], keyword_context: str = "", dataset_status: str = "NOT_FOUND"):
176
-
177
- if dataset_status == "FOUND":
178
- # SKENARIO 1: Keyword Ditemukan (BOLEH JAWAB)
179
- system_instruction = f"""
180
- [STATUS: VALID]
181
- User bertanya tentang topik teknis yang ADA dalam dataset skill kamu: **[{keyword_context}]**.
182
-
183
- TUGAS:
184
- 1. Jawab pertanyaan user tentang topik tersebut dengan konseptual yang singkat, padat, dan mudah dimengerti.
185
- 2. Fokus jawabanmu HANYA pada keyword tersebut.
186
- 3. Gunakan analogi sederhana jika perlu. Jangan terlalu kaku seperti buku teks, tapi tetap akurat.
187
- 4. Gaya bahasa: Ramah, Suportif, Mentor IT.
188
- 5. Giring user untuk menggunakan fitur belajar seperti tanya tentang skill teknis, Ujian/Tes sub skill, cek progres, rekomendasi belajar.
189
- """
190
- else:
191
- # SKENARIO 2: Keyword Tidak Ditemukan (TOLAK)
192
- system_instruction = f"""
193
- [STATUS: INVALID / OUT OF SCOPE]
194
- User bertanya tentang topik yang TIDAK ditemukan dalam 'Skill Keywords Dataset' kamu.
195
-
196
- TUGAS:
197
- 1. **TOLAK** untuk menjawab pertanyaan ini.
198
- 2. Katakan dengan sopan seperti: "Maaf, topik ini tidak ada dalam database skill yang saya pelajari."
199
- 3. JANGAN mencoba menjawab atau menebak, meskipun kamu tahu jawabannya secara umum. Patuhi whitelist dataset.
200
- 5. Tawarkan user untuk menggunakan fitur belajar seperti tanya tentang skill teknis, Ujian/Tes sub skill, cek progres, rekomendasi belajar.
201
- """
202
-
203
- prompt_template = f"""
204
- [ROLE]
205
- Namamu MORA. Kamu adalah Mentor & Asisten Teknis Spesialis.
206
-
207
- {system_instruction}
208
-
209
- **BATASAN (BLACK LIST):**
210
- - Jika topik TIDAK ADA di skill keyword (misal: Mobil, Kendaraan, dll), TOLAK dengan sopan dan pivot ke materi silabus.
211
- - JANGAN BERIKAN KODE FULL. Jika user minta "Buatkan kodingan", arahkan mereka untuk mengambil Ujian/Tes. Kamu hanya menjelaskan *Konsep* dan *Logika*.
212
- - Hindari jawaban yang terlalu panjang (lebih dari 4 kalimat).
213
- - Jika tidak yakin, katakan "Maaf, itu di luar pengetahuan saya."
214
- - Jangan buat-buat jawaban untuk topik di luar silabus.
215
-
216
- [PERSONALITY]
217
- Ramah, Suportif, Emoji secukupnya.
218
- """
219
-
220
- system_msg = {
221
- "role": "system",
222
- "content": prompt_template
223
- }
224
-
225
- messages = [system_msg]
226
- for msg in history[-5:]:
227
- messages.append({"role": msg['role'], "content": msg['content']})
228
- messages.append({"role": "user", "content": user_text})
229
-
230
- try:
231
- return await self._execute_with_retry(
232
- messages=messages,
233
- model="llama-3.3-70b-versatile",
234
- temperature=0.3
235
- )
236
- except Exception as e:
237
- return f"Maaf, otak saya sedang error. (Error: {str(e)})"
238
-
239
-
240
- async def analyze_psych_result(self, role: str, traits: list[str]):
241
- """
242
- Membuat penjelasan psikologis kenapa user cocok di role tersebut.
243
- """
244
- traits_str = "\n".join(traits)
245
-
246
- prompt = f"""
247
- Kamu adalah Konsultan Karir IT yang ahli membaca kepribadian.
248
-
249
- DATA USER:
250
- User baru saja mengikuti tes kepribadian sederhana.
251
- Hasil kecocokan tertinggi: **{role}**.
252
- Kebiasaan/Pilihan User:
253
- {traits_str}
254
-
255
- TUGAS:
256
- Berikan analisis singkat (maksimal 3 kalimat) dan memotivasi.
257
- Jelaskan hubungan antara kebiasaan user di atas dengan job role {role}.
258
- Gunakan gaya bahasa santai tapi meyakinkan.
259
-
260
- Contoh Output:
261
- "Wah, kamu punya bakat alami jadi AI Engineer! Kebiasaanmu yang suka menganalisis fakta dan mencari review mendalam menunjukkan kamu punya pola pikir analitis yang kuat, modal penting buat ngolah data!"
262
- """
263
-
264
- try:
265
- return await self._execute_with_retry(
266
- messages=[{"role": "user", "content": prompt}],
267
- model="llama-3.1-8b-instant",
268
- temperature=0.7
269
- )
270
- except:
271
- return f"Kamu cocok jadi {role}!"
272
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  llm_engine = LLMEngine()
 
1
+ import os
2
+ import json
3
+ from groq import AsyncGroq
4
+ from dotenv import load_dotenv
5
+
6
+ # Load environment variables
7
+ load_dotenv()
8
+
9
+ class LLMEngine:
10
+ def __init__(self):
11
+ # List untuk menampung client AsyncGroq
12
+ self.clients = []
13
+
14
+ # 1. Load Token Utama
15
+ key1 = os.getenv("GROQ_API_KEY")
16
+ if key1:
17
+ try:
18
+ self.clients.append(AsyncGroq(api_key=key1))
19
+ except Exception as e:
20
+ print(f"⚠️ Gagal memuat Token Utama: {e}")
21
+
22
+ # 2. Load Token Backup
23
+ key2 = os.getenv("GROQ_API_KEY_BACKUP")
24
+ if key2:
25
+ try:
26
+ self.clients.append(AsyncGroq(api_key=key2))
27
+ except Exception as e:
28
+ print(f"⚠️ Gagal memuat Token Backup: {e}")
29
+
30
+ print(f"βœ… LLM Engine (Async) siap dengan {len(self.clients)} Client aktif.")
31
+
32
+ # --- FUNGSI RETRY (VERSI ASYNC) ---
33
+ async def _execute_with_retry(self, messages, model, temperature=0.5, response_format=None):
34
+ """
35
+ Mencoba request Async secara bergantian.
36
+ """
37
+ if not self.clients:
38
+ raise Exception("Tidak ada API Key Groq yang terdeteksi di .env!")
39
+
40
+ last_error = Exception("Unknown Error")
41
+
42
+ for i, client in enumerate(self.clients):
43
+ try:
44
+ # PERUBAHAN PENTING: Pakai 'await' di sini
45
+ completion = await client.chat.completions.create(
46
+ messages=messages,
47
+ model=model,
48
+ temperature=temperature,
49
+ response_format=response_format
50
+ )
51
+ return completion.choices[0].message.content
52
+
53
+ except Exception as e:
54
+ print(f"⚠️ Token ke-{i+1} Gagal. Error: {e}")
55
+ last_error = e
56
+ # Lanjut ke client berikutnya...
57
+ continue
58
+
59
+ print("❌ Semua Token Gagal/Habis.")
60
+ raise last_error
61
+
62
+ async def process_user_intent(self, user_text: str, available_skills: list):
63
+ # Ubah list skill jadi string biar AI tau menu apa aja yang ada
64
+ skills_str = "\n".join([f"- {s}" for s in available_skills])
65
+
66
+ system_prompt = f"""
67
+ ROLE: Kamu adalah 'Router' untuk MORA, sebuah AI Learning Assistant.
68
+ Tugasmu BUKAN menjawab pertanyaan, tapi mengarahkan user ke fitur yang benar.
69
+
70
+ DAFTAR SKILL TERSEDIA DI DATABASE:
71
+ {skills_str}
72
+
73
+ INSTRUKSI UTAMA:
74
+ Analisis pesan user dan tentukan ACTION JSON.
75
+
76
+ 1. ACTION: "START_EXAM"
77
+ - Trigger: User ingin "tes", "ujian", "uji kemampuan", "soal", atau menyebut topik teknis (SQL, Python, CV, NLP).
78
+ - TUGAS PENTING (MAPPING): User sering menyebut topik spesifik (misal "SQL"). Kamu WAJIB mencocokkannya dengan "Nama Skill Tersedia" yang paling relevan.
79
+ Contoh:
80
+ - User: "Tes SQL" -> Detected: "Software & Data Foundations"
81
+ - User: "Tes Vision" -> Detected: "Deep Learning & Computer Vision"
82
+
83
+ 2. ACTION: "GET_RECOMMENDATION"
84
+ - Trigger: User minta "saran", "belajar apa", "rekomendasi", "bingung mulai mana".
85
+
86
+ 3. ACTION: "START_PSYCH_TEST"
87
+ - Trigger: User tanya "karir", "cocok kerja apa", "tes minat".
88
+
89
+ 4. ACTION: "CASUAL_CHAT"
90
+ - Trigger: Hanya untuk sapaan ("Halo"), curhat, atau pertanyaan di luar konteks belajar.
91
+ - JANGAN gunakan ini jika user jelas-jelas minta tes/soal.
92
+
93
+ OUTPUT JSON (Hanya JSON, tanpa teks lain):
94
+ {{
95
+ "action": "...",
96
+ "detected_skills": ["Nama Skill Database 1", "Nama Skill Database 2"] (Array berisi String nama skill persis dari daftar diatas. Kosongkan jika tidak ada.)
97
+ }}
98
+ """
99
+
100
+ try:
101
+ # Panggil retry dengan await
102
+ response_content = await self._execute_with_retry(
103
+ messages=[
104
+ {"role": "system", "content": system_prompt},
105
+ {"role": "user", "content": user_text}
106
+ ],
107
+ model="llama-3.3-70b-versatile",
108
+ temperature=0.0,
109
+ response_format={"type": "json_object"}
110
+ )
111
+ return json.loads(response_content)
112
+ except Exception as e:
113
+ print(f"Error Router: {e}")
114
+ return {"action": "CASUAL_CHAT", "detected_skills": []}
115
+
116
+ async def generate_question(self, topics: list, level: str):
117
+ topics_str = ", ".join(topics)
118
+ prompt = f"""
119
+ Buatkan 1 soal esai pendek dengan konsep how, what, why untuk menguji pemahaman user
120
+ Tentang topik: {topics_str}.
121
+ Tingkat Kesulitan: {level}.
122
+ Bahasa: Indonesia.
123
+
124
+ Output JSON:
125
+ {{
126
+ "question_text": "Pertanyaan...",
127
+ "grading_rubric": {{
128
+ "keywords": ["kata1", "kata2"],
129
+ "explanation_focus": "Poin utama yang harus dijelaskan"
130
+ }}
131
+ }}
132
+ """
133
+ try:
134
+ response_content = await self._execute_with_retry(
135
+ messages=[{"role": "user", "content": prompt}],
136
+ model="llama-3.3-8b-instant",
137
+ temperature=0.5,
138
+ response_format={"type": "json_object"}
139
+ )
140
+ return json.loads(response_content)
141
+ except Exception as e:
142
+ return {"question_text": "Error generate soal.", "grading_rubric": {}}
143
+
144
+ async def evaluate_answer(self, user_answer: str, question_context: dict):
145
+ prompt = f"""
146
+ Bertindaklah sebagai Dosen AI yang menilai jawaban mahasiswa.
147
+
148
+ Soal/Konteks: {json.dumps(question_context)}
149
+ Jawaban Mahasiswa: "{user_answer}"
150
+
151
+ Tugas:
152
+ 1. Beri skor 0-100.
153
+ 2. Beri feedback singkat & ramah (Bahasa Indonesia).
154
+ 3. Tentukan apakah jawaban BENAR secara konsep (is_correct).
155
+
156
+ Output JSON:
157
+ {{
158
+ "score": 85,
159
+ "feedback": "Penjelasanmu bagus, tapi kurang detail di bagian...",
160
+ "is_correct": true
161
+ }}
162
+ """
163
+ try:
164
+ response_content = await self._execute_with_retry(
165
+ messages=[{"role": "user", "content": prompt}],
166
+ model="llama-3.3-70b-versatile", # Tetap pakai 70b biar penilaian akurat
167
+ response_format={"type": "json_object"}
168
+ )
169
+
170
+ # Parsing string JSON menjadi Dictionary Python
171
+ return json.loads(response_content)
172
+ except:
173
+ return {"score": 0, "feedback": "Error menilai.", "is_correct": False}
174
+
175
+ async def casual_chat(self, user_text: str, history: list = [], keyword_context: str = "", dataset_status: str = "NOT_FOUND"):
176
+
177
+ if dataset_status == "FOUND":
178
+ # SKENARIO 1: Keyword Ditemukan (BOLEH JAWAB)
179
+ system_instruction = f"""
180
+ [STATUS: VALID]
181
+ User bertanya tentang topik teknis yang ADA dalam dataset skill kamu: **[{keyword_context}]**.
182
+
183
+ TUGAS:
184
+ 1. Jawab pertanyaan user tentang topik tersebut dengan konseptual yang singkat, padat, dan mudah dimengerti.
185
+ 2. Fokus jawabanmu HANYA pada keyword tersebut.
186
+ 3. Gunakan analogi sederhana jika perlu. Jangan terlalu kaku seperti buku teks, tapi tetap akurat.
187
+ 4. Gaya bahasa: Ramah, Suportif, Mentor IT.
188
+ 5. Giring user untuk menggunakan fitur belajar seperti tanya tentang skill teknis, Ujian/Tes sub skill, cek progres, rekomendasi belajar.
189
+ """
190
+ else:
191
+ # SKENARIO 2: Keyword Tidak Ditemukan (TOLAK)
192
+ system_instruction = f"""
193
+ [STATUS: INVALID / OUT OF SCOPE]
194
+ User bertanya tentang topik yang TIDAK ditemukan dalam 'Skill Keywords Dataset' kamu.
195
+
196
+ TUGAS:
197
+ 1. **TOLAK** untuk menjawab pertanyaan ini.
198
+ 2. Katakan dengan sopan seperti: "Maaf, topik ini tidak ada dalam database skill yang saya pelajari."
199
+ 3. JANGAN mencoba menjawab atau menebak, meskipun kamu tahu jawabannya secara umum. Patuhi whitelist dataset.
200
+ 5. Tawarkan user untuk menggunakan fitur belajar seperti tanya tentang skill teknis, Ujian/Tes sub skill, cek progres, rekomendasi belajar.
201
+ """
202
+
203
+ prompt_template = f"""
204
+ [ROLE]
205
+ Namamu MORA. Kamu adalah Mentor & Asisten Teknis Spesialis.
206
+
207
+ {system_instruction}
208
+
209
+ **BATASAN (BLACK LIST):**
210
+ - Jika topik TIDAK ADA di skill keyword (misal: Mobil, Kendaraan, dll), TOLAK dengan sopan dan pivot ke materi silabus.
211
+ - JANGAN BERIKAN KODE FULL. Jika user minta "Buatkan kodingan", arahkan mereka untuk mengambil Ujian/Tes. Kamu hanya menjelaskan *Konsep* dan *Logika*.
212
+ - Hindari jawaban yang terlalu panjang (lebih dari 4 kalimat).
213
+ - Jika tidak yakin, katakan "Maaf, itu di luar pengetahuan saya."
214
+ - Jangan buat-buat jawaban untuk topik di luar silabus.
215
+
216
+ [PERSONALITY]
217
+ Ramah, Suportif, Emoji secukupnya.
218
+ """
219
+
220
+ system_msg = {
221
+ "role": "system",
222
+ "content": prompt_template
223
+ }
224
+
225
+ messages = [system_msg]
226
+ for msg in history[-5:]:
227
+ messages.append({"role": msg['role'], "content": msg['content']})
228
+ messages.append({"role": "user", "content": user_text})
229
+
230
+ try:
231
+ return await self._execute_with_retry(
232
+ messages=messages,
233
+ model="llama-3.3-70b-versatile",
234
+ temperature=0.3
235
+ )
236
+ except Exception as e:
237
+ return f"Maaf, otak saya sedang error. (Error: {str(e)})"
238
+
239
+
240
+ async def analyze_psych_result(self, role: str, traits: list[str]):
241
+ """
242
+ Membuat penjelasan psikologis kenapa user cocok di role tersebut.
243
+ """
244
+ traits_str = "\n".join(traits)
245
+
246
+ prompt = f"""
247
+ Kamu adalah Konsultan Karir IT yang ahli membaca kepribadian.
248
+
249
+ DATA USER:
250
+ User baru saja mengikuti tes kepribadian sederhana.
251
+ Hasil kecocokan tertinggi: **{role}**.
252
+ Kebiasaan/Pilihan User:
253
+ {traits_str}
254
+
255
+ TUGAS:
256
+ Berikan analisis singkat (maksimal 3 kalimat) dan memotivasi.
257
+ Jelaskan hubungan antara kebiasaan user di atas dengan job role {role}.
258
+ Gunakan gaya bahasa santai tapi meyakinkan.
259
+
260
+ Contoh Output:
261
+ "Wah, kamu punya bakat alami jadi AI Engineer! Kebiasaanmu yang suka menganalisis fakta dan mencari review mendalam menunjukkan kamu punya pola pikir analitis yang kuat, modal penting buat ngolah data!"
262
+ """
263
+
264
+ try:
265
+ return await self._execute_with_retry(
266
+ messages=[{"role": "user", "content": prompt}],
267
+ model="llama-3.1-8b-instant",
268
+ temperature=0.7
269
+ )
270
+ except:
271
+ return f"Kamu cocok jadi {role}!"
272
+
273
+ async def analyze_progress(self, user_name: str, progress_data: dict):
274
+ # ... (kode konversi json.dumps tetap sama) ...
275
+ data_str = json.dumps(progress_data, indent=2)
276
+
277
+ system_msg = {
278
+ "role": "system",
279
+ "content": f"""
280
+ Kamu adalah MORA, asisten belajar AI yang ceria, suportif, dan to-the-point.
281
+
282
+ TUGAS:
283
+ Analisis data progress student ini dan buat laporan singkat.
284
+
285
+ DATA PROGRESS:
286
+ {data_str}
287
+
288
+ ATURAN FORMATTING (WAJIB MARKDOWN):
289
+ 1. Sapa user dengan namanya + emoji.
290
+ 2. Gunakan **Bold** untuk poin penting (nama course, skor, level).
291
+ 3. Pisahkan bagian menjadi dua kategori menggunakan Bullet Points:
292
+ - πŸ† **Highlights** (Untuk course completed / naik level / skor tinggi).
293
+ - 🚧 **Next Focus** (Untuk course yang masih in-progress/macet).
294
+ 4. Tutup dengan kalimat ajakan (Call to Action) yang semangat.
295
+ 5. Jangan terlalu panjang, maksimal 4-5 baris poin.
296
+
297
+ Gaya Bahasa: Gaul, motivasi tinggi, pakai emoji (πŸš€, πŸŽ‰, πŸ”₯).
298
+ """
299
+ }
300
+
301
+ try:
302
+ # Gunakan model yang cepat
303
+ completion = await self.client.chat.completions.create(
304
+ messages=[system_msg],
305
+ model="llama-3.1-8b-instant",
306
+ temperature=0.7
307
+ )
308
+ return completion.choices[0].message.content
309
+ except Exception as e:
310
+ return f"Error generate progress: {str(e)}"
311
+
312
  llm_engine = LLMEngine()