tututz commited on
Commit
410b443
·
verified ·
1 Parent(s): 53bf810

Upload 9 files

Browse files
Files changed (7) hide show
  1. Dockerfile +11 -11
  2. OK_matkul_graph.json +795 -795
  3. explanation_builder.py +261 -234
  4. graduation_logic.py +154 -0
  5. main.py +447 -416
  6. recommendation_builder.py +261 -0
  7. requirements.txt +7 -7
Dockerfile CHANGED
@@ -1,12 +1,12 @@
1
- # Dockerfile
2
- FROM python:3.9-slim
3
-
4
- WORKDIR /code
5
-
6
- COPY ./requirements.txt /code/requirements.txt
7
- RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
-
9
- COPY . /code/app
10
-
11
- # Port 7860 adalah port default yang dibuka oleh Hugging Face Spaces
12
  CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # Dockerfile
2
+ FROM python:3.9-slim
3
+
4
+ WORKDIR /code
5
+
6
+ COPY ./requirements.txt /code/requirements.txt
7
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
+
9
+ COPY . /code/app
10
+
11
+ # Port 7860 adalah port default yang dibuka oleh Hugging Face Spaces
12
  CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
OK_matkul_graph.json CHANGED
@@ -1,796 +1,796 @@
1
- {
2
- "curriculum_id": "KUR-FTE-DRAFT",
3
- "version": 1,
4
- "meta": {
5
- "name": "Graf kurikulum",
6
- "note": "Harusnya final :V"
7
- },
8
- "nodes": [
9
- {
10
- "code": "AAK1BAB2",
11
- "name": "Pengenalan Teknik Telekomunikasi",
12
- "sks": 2,
13
- "semester_plan": 1,
14
- "corereq": [],
15
- "attributes": {
16
- "kategori": "Wajib"
17
- }
18
- },
19
- {
20
- "code": "AZK1BAB3",
21
- "name": "Fisika 1",
22
- "sks": 3,
23
- "semester_plan": 1,
24
- "corereq": [],
25
- "attributes": {
26
- "kategori": "Wajib"
27
- }
28
- },
29
- {
30
- "code": "AZK1EAB3",
31
- "name": "Pengantar Rekayasa dan Desain",
32
- "sks": 3,
33
- "semester_plan": 1,
34
- "corereq": [],
35
- "attributes": {
36
- "kategori": "Wajib"
37
- }
38
- },
39
- {
40
- "code": "AZK1AAB3",
41
- "name": "Kalkulus 1",
42
- "sks": 3,
43
- "semester_plan": 1,
44
- "corereq": [],
45
- "attributes": {
46
- "kategori": "Wajib"
47
- }
48
- },
49
- {
50
- "code": "UBKXBCB2",
51
- "name": "Pancasila",
52
- "sks": 2,
53
- "semester_plan": 1,
54
- "corereq": [],
55
- "attributes": {
56
- "kategori": "Wajib"
57
- }
58
- },
59
- {
60
- "code": "AZK1IAB3",
61
- "name": "Kimia",
62
- "sks": 3,
63
- "semester_plan": 1,
64
- "corereq": [],
65
- "attributes": {
66
- "kategori": "Wajib"
67
- }
68
- },
69
- {
70
- "code": "UCK1FDB1",
71
- "name": "Internalisasi Budaya dan Pembentukan Karakter",
72
- "sks": 1,
73
- "semester_plan": 1,
74
- "corereq": [],
75
- "attributes": {
76
- "kategori": "Wajib"
77
- }
78
- },
79
- {
80
- "code": "AZK1CAB1",
81
- "name": "Praktikum Fisika 1",
82
- "sks": 1,
83
- "semester_plan": 1,
84
- "corereq": [],
85
- "attributes": {
86
- "kategori": "Wajib"
87
- }
88
- },
89
- {
90
- "code": "AGAMA",
91
- "name": "Agama",
92
- "sks": 2,
93
- "semester_plan": 2,
94
- "corereq": [],
95
- "attributes": {
96
- "kategori": "Wajib"
97
- }
98
- },
99
- {
100
- "code": "AZK1GAB3",
101
- "name": "Fisika 2",
102
- "sks": 3,
103
- "semester_plan": 2,
104
- "corereq": [],
105
- "attributes": {
106
- "kategori": "Wajib"
107
- }
108
- },
109
- {
110
- "code": "AZK1KAB3",
111
- "name": "Aljabar Linier",
112
- "sks": 3,
113
- "semester_plan": 2,
114
- "corereq": [],
115
- "attributes": {
116
- "kategori": "Wajib"
117
- }
118
- },
119
- {
120
- "code": "AZK1FAB3",
121
- "name": "Kalkulus 2",
122
- "sks": 3,
123
- "semester_plan": 2,
124
- "corereq": [],
125
- "attributes": {
126
- "kategori": "Wajib"
127
- }
128
- },
129
- {
130
- "code": "AZK1JAB3",
131
- "name": "Matematika Diskret",
132
- "sks": 3,
133
- "semester_plan": 2,
134
- "corereq": [],
135
- "attributes": {
136
- "kategori": "Wajib"
137
- }
138
- },
139
- {
140
- "code": "AZK1DAB3",
141
- "name": "Algoritma dan Pemrograman",
142
- "sks": 3,
143
- "semester_plan": 2,
144
- "corereq": [],
145
- "attributes": {
146
- "kategori": "Wajib"
147
- }
148
- },
149
- {
150
- "code": "AZK1IAB1",
151
- "name": "Praktikum Algoritma dan Pemrograman",
152
- "sks": 1,
153
- "semester_plan": 2,
154
- "corereq": [],
155
- "attributes": {
156
- "kategori": "Wajib"
157
- }
158
- },
159
- {
160
- "code": "AZK1HAB1",
161
- "name": "Praktikum Fisika 2",
162
- "sks": 1,
163
- "semester_plan": 2,
164
- "corereq": [],
165
- "attributes": {
166
- "kategori": "Wajib"
167
- }
168
- },
169
- {
170
- "code": "AAK2GAB3",
171
- "name": "Jaringan dan Trafik Telekomunikasi",
172
- "sks": 3,
173
- "semester_plan": 3,
174
- "corereq": [],
175
- "attributes": {
176
- "kategori": "Wajib"
177
- }
178
- },
179
- {
180
- "code": "AZK2AAB3",
181
- "name": "Probabilitas dan Statistika",
182
- "sks": 3,
183
- "semester_plan": 3,
184
- "corereq": [],
185
- "attributes": {
186
- "kategori": "Wajib"
187
- }
188
- },
189
- {
190
- "code": "AZK2CAB3",
191
- "name": "Persamaan Diferensial",
192
- "sks": 3,
193
- "semester_plan": 3,
194
- "corereq": [],
195
- "attributes": {
196
- "kategori": "Wajib"
197
- }
198
- },
199
- {
200
- "code": "AAK2DAB3",
201
- "name": "Variabel Kompleks",
202
- "sks": 3,
203
- "semester_plan": 3,
204
- "corereq": [],
205
- "attributes": {
206
- "kategori": "Wajib"
207
- }
208
- },
209
- {
210
- "code": "AZK2DAB3",
211
- "name": "Rangkaian Listrik",
212
- "sks": 3,
213
- "semester_plan": 3,
214
- "corereq": [],
215
- "attributes": {
216
- "kategori": "Wajib"
217
- }
218
- },
219
- {
220
- "code": "AAK2FAB2",
221
- "name": "Pemrograman Python",
222
- "sks": 2,
223
- "semester_plan": 3,
224
- "corereq": [],
225
- "attributes": {
226
- "kategori": "Wajib"
227
- }
228
- },
229
- {
230
- "code": "AAK2AAB1",
231
- "name": "Praktikum Teknik Telekomunikasi 1",
232
- "sks": 1,
233
- "semester_plan": 3,
234
- "corereq": [],
235
- "attributes": {
236
- "kategori": "Wajib"
237
- }
238
- },
239
- {
240
- "code": "AAK2NAB3",
241
- "name": "Jaringan Komunikasi Data",
242
- "sks": 3,
243
- "semester_plan": 4,
244
- "corereq": [],
245
- "attributes": {
246
- "kategori": "Wajib"
247
- }
248
- },
249
- {
250
- "code": "AZK2HAB3",
251
- "name": "Teknik Digital",
252
- "sks": 3,
253
- "semester_plan": 4,
254
- "corereq": [],
255
- "attributes": {
256
- "kategori": "Wajib"
257
- }
258
- },
259
- {
260
- "code": "AZK2EAB3",
261
- "name": "Elektromagnetika",
262
- "sks": 3,
263
- "semester_plan": 4,
264
- "corereq": [],
265
- "attributes": {
266
- "kategori": "Wajib"
267
- }
268
- },
269
- {
270
- "code": "AZK2GAB3",
271
- "name": "Pengolahan Sinyal Waktu Kontinyu",
272
- "sks": 3,
273
- "semester_plan": 4,
274
- "corereq": [],
275
- "attributes": {
276
- "kategori": "Wajib"
277
- }
278
- },
279
- {
280
- "code": "AZK2FAB3",
281
- "name": "Elektronika",
282
- "sks": 3,
283
- "semester_plan": 4,
284
- "corereq": [],
285
- "attributes": {
286
- "kategori": "Wajib"
287
- }
288
- },
289
- {
290
- "code": "AAK2KAB3",
291
- "name": "Artificial Intelligence dan Big Data",
292
- "sks": 3,
293
- "semester_plan": 4,
294
- "corereq": [],
295
- "attributes": {
296
- "kategori": "Wajib"
297
- }
298
- },
299
- {
300
- "code": "AAK2HAB1",
301
- "name": "Praktikum Teknik Telekomunikasi 2",
302
- "sks": 1,
303
- "semester_plan": 4,
304
- "corereq": [],
305
- "attributes": {
306
- "kategori": "Wajib"
307
- }
308
- },
309
- {
310
- "code": "UCKXBDB2",
311
- "name": "Kewirausahaan",
312
- "sks": 2,
313
- "semester_plan": 5,
314
- "corereq": [],
315
- "attributes": {
316
- "kategori": "Wajib"
317
- }
318
- },
319
- {
320
- "code": "AAK3FAB3",
321
- "name": "Keamanan Data dan Blockchain",
322
- "sks": 3,
323
- "semester_plan": 5,
324
- "corereq": [],
325
- "attributes": {
326
- "kategori": "Wajib"
327
- }
328
- },
329
- {
330
- "code": "AAK3BAB3",
331
- "name": "Sistem Komunikasi 1",
332
- "sks": 3,
333
- "semester_plan": 5,
334
- "corereq": [],
335
- "attributes": {
336
- "kategori": "Wajib"
337
- }
338
- },
339
- {
340
- "code": "AZK3AAB3",
341
- "name": "Pengolahan Sinyal Waktu Diskret",
342
- "sks": 3,
343
- "semester_plan": 5,
344
- "corereq": [],
345
- "attributes": {
346
- "kategori": "Wajib"
347
- }
348
- },
349
- {
350
- "code": "AAK3DAB2",
351
- "name": "Elektromagnetika Telekomunikasi",
352
- "sks": 2,
353
- "semester_plan": 5,
354
- "corereq": [],
355
- "attributes": {
356
- "kategori": "Wajib"
357
- }
358
- },
359
- {
360
- "code": "UBKXCCB2",
361
- "name": "Bahasa Indonesia",
362
- "sks": 2,
363
- "semester_plan": 5,
364
- "corereq": [],
365
- "attributes": {
366
- "kategori": "Wajib"
367
- }
368
- },
369
- {
370
- "code": "AAK3CAB3",
371
- "name": "Manajemen Proyek",
372
- "sks": 3,
373
- "semester_plan": 5,
374
- "corereq": [],
375
- "attributes": {
376
- "kategori": "Wajib"
377
- }
378
- },
379
- {
380
- "code": "AAK3AAB1",
381
- "name": "Praktikum Teknik Telekomunikasi 3",
382
- "sks": 1,
383
- "semester_plan": 5,
384
- "corereq": [],
385
- "attributes": {
386
- "kategori": "Wajib"
387
- }
388
- },
389
- {
390
- "code": "AAK3JAB3",
391
- "name": "Mikroprosesor dan IoT",
392
- "sks": 3,
393
- "semester_plan": 6,
394
- "corereq": [],
395
- "attributes": {
396
- "kategori": "Wajib"
397
- }
398
- },
399
- {
400
- "code": "AAK3OAB3",
401
- "name": "Sistem Komunikasi Optik",
402
- "sks": 3,
403
- "semester_plan": 6,
404
- "corereq": [],
405
- "attributes": {
406
- "kategori": "Wajib"
407
- }
408
- },
409
- {
410
- "code": "AAK3MAB3",
411
- "name": "Sistem Komunikasi 2",
412
- "sks": 3,
413
- "semester_plan": 6,
414
- "corereq": [],
415
- "attributes": {
416
- "kategori": "Wajib"
417
- }
418
- },
419
- {
420
- "code": "AAK3IAB3",
421
- "name": "Elektronika Komunikasi / RF",
422
- "sks": 3,
423
- "semester_plan": 6,
424
- "corereq": [],
425
- "attributes": {
426
- "kategori": "Wajib"
427
- }
428
- },
429
- {
430
- "code": "AAK3NAB2",
431
- "name": "Teknologi Antena",
432
- "sks": 2,
433
- "semester_plan": 6,
434
- "corereq": [],
435
- "attributes": {
436
- "kategori": "Wajib"
437
- }
438
- },
439
- {
440
- "code": "AZK4AAB2",
441
- "name": "Studium General",
442
- "sks": 2,
443
- "semester_plan": 6,
444
- "corereq": [],
445
- "attributes": {
446
- "kategori": "Wajib"
447
- }
448
- },
449
- {
450
- "code": "AZK3BAB2",
451
- "name": "Kerja Praktek / KKN",
452
- "sks": 2,
453
- "semester_plan": 6,
454
- "corereq": [],
455
- "attributes": {
456
- "kategori": "Wajib"
457
- }
458
- },
459
- {
460
- "code": "AAK3LAB1",
461
- "name": "Praktikum Teknik Telekomunikasi 4",
462
- "sks": 1,
463
- "semester_plan": 6,
464
- "corereq": [],
465
- "attributes": {
466
- "kategori": "Wajib"
467
- }
468
- },
469
- {
470
- "code": "AAK4BAB3",
471
- "name": "Sistem Komunikasi Seluler",
472
- "sks": 3,
473
- "semester_plan": 7,
474
- "corereq": [],
475
- "attributes": {
476
- "kategori": "Wajib"
477
- }
478
- },
479
- {
480
- "code": "AZK4BAA2",
481
- "name": "Proposal Tugas Akhir",
482
- "sks": 2,
483
- "semester_plan": 7,
484
- "corereq": [],
485
- "attributes": {
486
- "kategori": "Wajib"
487
- }
488
- },
489
- {
490
- "code": "UCKXEDB2",
491
- "name": "Literasi Manusia",
492
- "sks": 2,
493
- "semester_plan": 7,
494
- "corereq": [],
495
- "attributes": {
496
- "kategori": "Wajib"
497
- }
498
- },
499
- {
500
- "code": "MK_PILIHAN1",
501
- "name": "Mata Kuliah Pilihan #1",
502
- "sks": 3,
503
- "semester_plan": 7,
504
- "corereq": [],
505
- "attributes": {
506
- "kategori": "Pilihan"
507
- }
508
- },
509
- {
510
- "code": "MK_PILIHAN2",
511
- "name": "Mata Kuliah Pilihan #2",
512
- "sks": 3,
513
- "semester_plan": 7,
514
- "corereq": [],
515
- "attributes": {
516
- "kategori": "Pilihan"
517
- }
518
- },
519
- {
520
- "code": "MK_PILIHAN3",
521
- "name": "Mata Kuliah Pilihan #3",
522
- "sks": 3,
523
- "semester_plan": 7,
524
- "corereq": [],
525
- "attributes": {
526
- "kategori": "Pilihan"
527
- }
528
- },
529
- {
530
- "code": "MK_PILIHAN4",
531
- "name": "Mata Kuliah Pilihan #4",
532
- "sks": 3,
533
- "semester_plan": 7,
534
- "corereq": [],
535
- "attributes": {
536
- "kategori": "Pilihan"
537
- }
538
- },
539
- {
540
- "code": "MK_PILIHAN5",
541
- "name": "Mata Kuliah Pilihan #5",
542
- "sks": 3,
543
- "semester_plan": 8,
544
- "corereq": [],
545
- "attributes": {
546
- "kategori": "Pilihan"
547
- }
548
- },
549
- {
550
- "code": "MK_PILIHAN6",
551
- "name": "Mata Kuliah Pilihan #6",
552
- "sks": 3,
553
- "semester_plan": 8,
554
- "corereq": [],
555
- "attributes": {
556
- "kategori": "Pilihan"
557
- }
558
- },
559
- {
560
- "code": "UCKXADB2",
561
- "name": "Bahasa Inggris",
562
- "sks": 2,
563
- "semester_plan": 8,
564
- "corereq": [],
565
- "attributes": {
566
- "kategori": "Wajib"
567
- }
568
- },
569
- {
570
- "code": "AZK4CAA4",
571
- "name": "Tugas Akhir",
572
- "sks": 4,
573
- "semester_plan": 8,
574
- "corereq": [],
575
- "attributes": {
576
- "kategori": "Wajib"
577
- }
578
- },
579
- {
580
- "code": "UBKXACB2",
581
- "name": "Kewarganegaraan",
582
- "sks": 3,
583
- "semester_plan": 8,
584
- "corereq": [],
585
- "attributes": {
586
- "kategori": "Wajib"
587
- }
588
- }
589
- ],
590
- "edges": [
591
- {
592
- "from":"AZK1AAB3",
593
- "to": "AZK1FAB3",
594
- "type": "prereq"
595
- },
596
- {
597
- "from": "AZK1BAB3",
598
- "to": "AZK1GAB3",
599
- "type": "prereq"
600
- },
601
- {
602
- "from": "AZK1BAB3",
603
- "to": "AZK1CAB1",
604
- "type": "coreq"
605
- },
606
- {
607
- "from": "AZK1GAB3",
608
- "to": "AZK1HAB1",
609
- "type": "coreq"
610
- },
611
- {
612
- "from": "AZK1DAB3",
613
- "to": "AZK1IAB1",
614
- "type": "coreq"
615
- },
616
- {
617
- "from": "AZK1AAB3",
618
- "to": "AZK1KAB3",
619
- "type": "prereq"
620
- },
621
- {
622
- "from": "AAK1BAB2",
623
- "to": "AAK2GAB3",
624
- "type": "prereq"
625
- },
626
- {
627
- "from": "AZK1DAB3",
628
- "to": "AAK2FAB2",
629
- "type": "prereq"
630
- },
631
-
632
- {
633
- "from": "AZK1FAB3",
634
- "to": "AZK2AAB3",
635
- "type": "prereq"
636
- },
637
- {
638
- "from": "AZK1FAB3",
639
- "to": "AZK2DAB3",
640
- "type": "prereq"
641
- },
642
- {
643
- "from": "AZK1FAB3",
644
- "to": "AAK2DAB3",
645
- "type": "prereq"
646
- },
647
-
648
- {
649
- "from": "AZK1FAB3",
650
- "to": "AZK2CAB3",
651
- "type": "prereq"
652
- },
653
-
654
- {
655
- "from": "AZK1GAB3",
656
- "to": "AZK2DAB3",
657
- "type": "prereq"
658
- },
659
- {
660
- "from": "AZK1KAB3",
661
- "to": "AZK2CAB3",
662
- "type": "prereq"
663
- },
664
- {
665
- "from": "AZK2DAB3",
666
- "to": "AZK2FAB3",
667
- "type": "prereq"
668
- },
669
- {
670
- "from": "AAK2FAB2",
671
- "to": "AAK2KAB3",
672
- "type": "prereq"
673
- },
674
- {
675
- "from": "AAK2DAB3",
676
- "to": "AZK2GAB3",
677
- "type": "prereq"
678
- },
679
- {
680
- "from": "AAK2DAB3",
681
- "to": "AZK2EAB3",
682
- "type": "prereq"
683
- },
684
- {
685
- "from": "AZK2CAB3",
686
- "to": "AZK2GAB3",
687
- "type": "prereq"
688
- },
689
- {
690
- "from": "AZK2AAB3",
691
- "to": "AAK3BAB3",
692
- "type": "prereq"
693
- },
694
- {
695
- "from": "AAK2GAB3",
696
- "to": "AAK2NAB3",
697
- "type": "prereq"
698
- },
699
- {
700
- "from": "AZK2EAB3",
701
- "to": "AAK3DAB2",
702
- "type": "prereq"
703
- },
704
-
705
- {
706
- "from": "AZK2GAB3",
707
- "to": "AZK3AAB3",
708
- "type": "prereq"
709
- },
710
- {
711
- "from": "AZK2GAB3",
712
- "to": "AAK3BAB3",
713
- "type": "prereq"
714
- },
715
- {
716
- "from": "AAK2NAB3",
717
- "to": "AAK3FAB3",
718
- "type": "prereq"
719
- },
720
- {
721
- "from": "AZK2HAB3",
722
- "to": "AAK3JAB3",
723
- "type": "prereq"
724
- },
725
- {
726
- "from": "AZK2FAB3",
727
- "to": "AAK3IAB3",
728
- "type": "prereq"
729
- },
730
- {
731
- "from": "AZK2EAB3",
732
- "to": "AAK3OAB3",
733
- "type": "prereq"
734
- },
735
- {
736
- "from": "AAK3BAB3",
737
- "to": "AAK3MAB3",
738
- "type": "prereq"
739
- },
740
- {
741
- "from": "AAK3BAB3",
742
- "to": "AAK3OAB3",
743
- "type": "prereq"
744
- },
745
- {
746
- "from": "AZK3AAB3",
747
- "to": "AAK3IAB3",
748
- "type": "prereq"
749
- },
750
- {
751
- "from": "AAK3BAB3",
752
- "to": "AAK3IAB3",
753
- "type": "prereq"
754
- },
755
- {
756
- "from": "AAK3DAB2",
757
- "to": "AAK3IAB3",
758
- "type": "prereq"
759
- },
760
- {
761
- "from": "AAK3DAB2",
762
- "to": "AAK3NAB2",
763
- "type": "prereq"
764
- },
765
- {
766
- "from": "AAK3MAB3",
767
- "to": "AAK4BAB3",
768
- "type": "prereq"
769
- },
770
- {
771
- "from": "AAK3NAB2",
772
- "to": "AAK4BAB3",
773
- "type": "prereq"
774
- },
775
- {
776
- "from": "AAK2AAB1",
777
- "to": "AAK2HAB1",
778
- "type": "prereq"
779
- },
780
- {
781
- "from": "AAK2HAB1",
782
- "to": "AAK3AAB1",
783
- "type": "prereq"
784
- },
785
- {
786
- "from": "AAK3AAB1",
787
- "to": "AAK3LAB1",
788
- "type": "prereq"
789
- },
790
- {
791
- "from": "AZK4BAA2",
792
- "to": "AZK4CAA4",
793
- "type": "prereq"
794
- }
795
- ]
796
  }
 
1
+ {
2
+ "curriculum_id": "KUR-FTE-DRAFT",
3
+ "version": 1,
4
+ "meta": {
5
+ "name": "Graf kurikulum",
6
+ "note": "Harusnya final :V"
7
+ },
8
+ "nodes": [
9
+ {
10
+ "code": "AAK1BAB2",
11
+ "name": "Pengenalan Teknik Telekomunikasi",
12
+ "sks": 2,
13
+ "semester_plan": 1,
14
+ "corereq": [],
15
+ "attributes": {
16
+ "kategori": "Wajib"
17
+ }
18
+ },
19
+ {
20
+ "code": "AZK1BAB3",
21
+ "name": "Fisika 1",
22
+ "sks": 3,
23
+ "semester_plan": 1,
24
+ "corereq": [],
25
+ "attributes": {
26
+ "kategori": "Wajib"
27
+ }
28
+ },
29
+ {
30
+ "code": "AZK1EAB3",
31
+ "name": "Pengantar Rekayasa dan Desain",
32
+ "sks": 3,
33
+ "semester_plan": 1,
34
+ "corereq": [],
35
+ "attributes": {
36
+ "kategori": "Wajib"
37
+ }
38
+ },
39
+ {
40
+ "code": "AZK1AAB3",
41
+ "name": "Kalkulus 1",
42
+ "sks": 3,
43
+ "semester_plan": 1,
44
+ "corereq": [],
45
+ "attributes": {
46
+ "kategori": "Wajib"
47
+ }
48
+ },
49
+ {
50
+ "code": "UBKXBCB2",
51
+ "name": "Pancasila",
52
+ "sks": 2,
53
+ "semester_plan": 1,
54
+ "corereq": [],
55
+ "attributes": {
56
+ "kategori": "Wajib"
57
+ }
58
+ },
59
+ {
60
+ "code": "AZK1IAB3",
61
+ "name": "Kimia",
62
+ "sks": 3,
63
+ "semester_plan": 1,
64
+ "corereq": [],
65
+ "attributes": {
66
+ "kategori": "Wajib"
67
+ }
68
+ },
69
+ {
70
+ "code": "UCK1FDB1",
71
+ "name": "Internalisasi Budaya dan Pembentukan Karakter",
72
+ "sks": 1,
73
+ "semester_plan": 1,
74
+ "corereq": [],
75
+ "attributes": {
76
+ "kategori": "Wajib"
77
+ }
78
+ },
79
+ {
80
+ "code": "AZK1CAB1",
81
+ "name": "Praktikum Fisika 1",
82
+ "sks": 1,
83
+ "semester_plan": 1,
84
+ "corereq": [],
85
+ "attributes": {
86
+ "kategori": "Wajib"
87
+ }
88
+ },
89
+ {
90
+ "code": "AGAMA",
91
+ "name": "Agama",
92
+ "sks": 2,
93
+ "semester_plan": 2,
94
+ "corereq": [],
95
+ "attributes": {
96
+ "kategori": "Wajib"
97
+ }
98
+ },
99
+ {
100
+ "code": "AZK1GAB3",
101
+ "name": "Fisika 2",
102
+ "sks": 3,
103
+ "semester_plan": 2,
104
+ "corereq": [],
105
+ "attributes": {
106
+ "kategori": "Wajib"
107
+ }
108
+ },
109
+ {
110
+ "code": "AZK1KAB3",
111
+ "name": "Aljabar Linier",
112
+ "sks": 3,
113
+ "semester_plan": 2,
114
+ "corereq": [],
115
+ "attributes": {
116
+ "kategori": "Wajib"
117
+ }
118
+ },
119
+ {
120
+ "code": "AZK1FAB3",
121
+ "name": "Kalkulus 2",
122
+ "sks": 3,
123
+ "semester_plan": 2,
124
+ "corereq": [],
125
+ "attributes": {
126
+ "kategori": "Wajib"
127
+ }
128
+ },
129
+ {
130
+ "code": "AZK1JAB3",
131
+ "name": "Matematika Diskret",
132
+ "sks": 3,
133
+ "semester_plan": 2,
134
+ "corereq": [],
135
+ "attributes": {
136
+ "kategori": "Wajib"
137
+ }
138
+ },
139
+ {
140
+ "code": "AZK1DAB3",
141
+ "name": "Algoritma dan Pemrograman",
142
+ "sks": 3,
143
+ "semester_plan": 2,
144
+ "corereq": [],
145
+ "attributes": {
146
+ "kategori": "Wajib"
147
+ }
148
+ },
149
+ {
150
+ "code": "AZK1IAB1",
151
+ "name": "Praktikum Algoritma dan Pemrograman",
152
+ "sks": 1,
153
+ "semester_plan": 2,
154
+ "corereq": [],
155
+ "attributes": {
156
+ "kategori": "Wajib"
157
+ }
158
+ },
159
+ {
160
+ "code": "AZK1HAB1",
161
+ "name": "Praktikum Fisika 2",
162
+ "sks": 1,
163
+ "semester_plan": 2,
164
+ "corereq": [],
165
+ "attributes": {
166
+ "kategori": "Wajib"
167
+ }
168
+ },
169
+ {
170
+ "code": "AAK2GAB3",
171
+ "name": "Jaringan dan Trafik Telekomunikasi",
172
+ "sks": 3,
173
+ "semester_plan": 3,
174
+ "corereq": [],
175
+ "attributes": {
176
+ "kategori": "Wajib"
177
+ }
178
+ },
179
+ {
180
+ "code": "AZK2AAB3",
181
+ "name": "Probabilitas dan Statistika",
182
+ "sks": 3,
183
+ "semester_plan": 3,
184
+ "corereq": [],
185
+ "attributes": {
186
+ "kategori": "Wajib"
187
+ }
188
+ },
189
+ {
190
+ "code": "AZK2CAB3",
191
+ "name": "Persamaan Diferensial",
192
+ "sks": 3,
193
+ "semester_plan": 3,
194
+ "corereq": [],
195
+ "attributes": {
196
+ "kategori": "Wajib"
197
+ }
198
+ },
199
+ {
200
+ "code": "AAK2DAB3",
201
+ "name": "Variabel Kompleks",
202
+ "sks": 3,
203
+ "semester_plan": 3,
204
+ "corereq": [],
205
+ "attributes": {
206
+ "kategori": "Wajib"
207
+ }
208
+ },
209
+ {
210
+ "code": "AZK2DAB3",
211
+ "name": "Rangkaian Listrik",
212
+ "sks": 3,
213
+ "semester_plan": 3,
214
+ "corereq": [],
215
+ "attributes": {
216
+ "kategori": "Wajib"
217
+ }
218
+ },
219
+ {
220
+ "code": "AAK2FAB2",
221
+ "name": "Pemrograman Python",
222
+ "sks": 2,
223
+ "semester_plan": 3,
224
+ "corereq": [],
225
+ "attributes": {
226
+ "kategori": "Wajib"
227
+ }
228
+ },
229
+ {
230
+ "code": "AAK2AAB1",
231
+ "name": "Praktikum Teknik Telekomunikasi 1",
232
+ "sks": 1,
233
+ "semester_plan": 3,
234
+ "corereq": [],
235
+ "attributes": {
236
+ "kategori": "Wajib"
237
+ }
238
+ },
239
+ {
240
+ "code": "AAK2NAB3",
241
+ "name": "Jaringan Komunikasi Data",
242
+ "sks": 3,
243
+ "semester_plan": 4,
244
+ "corereq": [],
245
+ "attributes": {
246
+ "kategori": "Wajib"
247
+ }
248
+ },
249
+ {
250
+ "code": "AZK2HAB3",
251
+ "name": "Teknik Digital",
252
+ "sks": 3,
253
+ "semester_plan": 4,
254
+ "corereq": [],
255
+ "attributes": {
256
+ "kategori": "Wajib"
257
+ }
258
+ },
259
+ {
260
+ "code": "AZK2EAB3",
261
+ "name": "Elektromagnetika",
262
+ "sks": 3,
263
+ "semester_plan": 4,
264
+ "corereq": [],
265
+ "attributes": {
266
+ "kategori": "Wajib"
267
+ }
268
+ },
269
+ {
270
+ "code": "AZK2GAB3",
271
+ "name": "Pengolahan Sinyal Waktu Kontinyu",
272
+ "sks": 3,
273
+ "semester_plan": 4,
274
+ "corereq": [],
275
+ "attributes": {
276
+ "kategori": "Wajib"
277
+ }
278
+ },
279
+ {
280
+ "code": "AZK2FAB3",
281
+ "name": "Elektronika",
282
+ "sks": 3,
283
+ "semester_plan": 4,
284
+ "corereq": [],
285
+ "attributes": {
286
+ "kategori": "Wajib"
287
+ }
288
+ },
289
+ {
290
+ "code": "AAK2KAB3",
291
+ "name": "Artificial Intelligence dan Big Data",
292
+ "sks": 3,
293
+ "semester_plan": 4,
294
+ "corereq": [],
295
+ "attributes": {
296
+ "kategori": "Wajib"
297
+ }
298
+ },
299
+ {
300
+ "code": "AAK2HAB1",
301
+ "name": "Praktikum Teknik Telekomunikasi 2",
302
+ "sks": 1,
303
+ "semester_plan": 4,
304
+ "corereq": [],
305
+ "attributes": {
306
+ "kategori": "Wajib"
307
+ }
308
+ },
309
+ {
310
+ "code": "UCKXBDB2",
311
+ "name": "Kewirausahaan",
312
+ "sks": 2,
313
+ "semester_plan": 5,
314
+ "corereq": [],
315
+ "attributes": {
316
+ "kategori": "Wajib"
317
+ }
318
+ },
319
+ {
320
+ "code": "AAK3FAB3",
321
+ "name": "Keamanan Data dan Blockchain",
322
+ "sks": 3,
323
+ "semester_plan": 5,
324
+ "corereq": [],
325
+ "attributes": {
326
+ "kategori": "Wajib"
327
+ }
328
+ },
329
+ {
330
+ "code": "AAK3BAB3",
331
+ "name": "Sistem Komunikasi 1",
332
+ "sks": 3,
333
+ "semester_plan": 5,
334
+ "corereq": [],
335
+ "attributes": {
336
+ "kategori": "Wajib"
337
+ }
338
+ },
339
+ {
340
+ "code": "AZK3AAB3",
341
+ "name": "Pengolahan Sinyal Waktu Diskret",
342
+ "sks": 3,
343
+ "semester_plan": 5,
344
+ "corereq": [],
345
+ "attributes": {
346
+ "kategori": "Wajib"
347
+ }
348
+ },
349
+ {
350
+ "code": "AAK3DAB2",
351
+ "name": "Elektromagnetika Telekomunikasi",
352
+ "sks": 2,
353
+ "semester_plan": 5,
354
+ "corereq": [],
355
+ "attributes": {
356
+ "kategori": "Wajib"
357
+ }
358
+ },
359
+ {
360
+ "code": "UBKXCCB2",
361
+ "name": "Bahasa Indonesia",
362
+ "sks": 2,
363
+ "semester_plan": 5,
364
+ "corereq": [],
365
+ "attributes": {
366
+ "kategori": "Wajib"
367
+ }
368
+ },
369
+ {
370
+ "code": "AAK3CAB3",
371
+ "name": "Manajemen Proyek",
372
+ "sks": 3,
373
+ "semester_plan": 5,
374
+ "corereq": [],
375
+ "attributes": {
376
+ "kategori": "Wajib"
377
+ }
378
+ },
379
+ {
380
+ "code": "AAK3AAB1",
381
+ "name": "Praktikum Teknik Telekomunikasi 3",
382
+ "sks": 1,
383
+ "semester_plan": 5,
384
+ "corereq": [],
385
+ "attributes": {
386
+ "kategori": "Wajib"
387
+ }
388
+ },
389
+ {
390
+ "code": "AAK3JAB3",
391
+ "name": "Mikroprosesor dan IoT",
392
+ "sks": 3,
393
+ "semester_plan": 6,
394
+ "corereq": [],
395
+ "attributes": {
396
+ "kategori": "Wajib"
397
+ }
398
+ },
399
+ {
400
+ "code": "AAK3OAB3",
401
+ "name": "Sistem Komunikasi Optik",
402
+ "sks": 3,
403
+ "semester_plan": 6,
404
+ "corereq": [],
405
+ "attributes": {
406
+ "kategori": "Wajib"
407
+ }
408
+ },
409
+ {
410
+ "code": "AAK3MAB3",
411
+ "name": "Sistem Komunikasi 2",
412
+ "sks": 3,
413
+ "semester_plan": 6,
414
+ "corereq": [],
415
+ "attributes": {
416
+ "kategori": "Wajib"
417
+ }
418
+ },
419
+ {
420
+ "code": "AAK3IAB3",
421
+ "name": "Elektronika Komunikasi / RF",
422
+ "sks": 3,
423
+ "semester_plan": 6,
424
+ "corereq": [],
425
+ "attributes": {
426
+ "kategori": "Wajib"
427
+ }
428
+ },
429
+ {
430
+ "code": "AAK3NAB2",
431
+ "name": "Teknologi Antena",
432
+ "sks": 2,
433
+ "semester_plan": 6,
434
+ "corereq": [],
435
+ "attributes": {
436
+ "kategori": "Wajib"
437
+ }
438
+ },
439
+ {
440
+ "code": "AZK4AAB2",
441
+ "name": "Studium General",
442
+ "sks": 2,
443
+ "semester_plan": 6,
444
+ "corereq": [],
445
+ "attributes": {
446
+ "kategori": "Wajib"
447
+ }
448
+ },
449
+ {
450
+ "code": "AZK3BAB2",
451
+ "name": "Kerja Praktek / KKN",
452
+ "sks": 2,
453
+ "semester_plan": 6,
454
+ "corereq": [],
455
+ "attributes": {
456
+ "kategori": "Wajib"
457
+ }
458
+ },
459
+ {
460
+ "code": "AAK3LAB1",
461
+ "name": "Praktikum Teknik Telekomunikasi 4",
462
+ "sks": 1,
463
+ "semester_plan": 6,
464
+ "corereq": [],
465
+ "attributes": {
466
+ "kategori": "Wajib"
467
+ }
468
+ },
469
+ {
470
+ "code": "AAK4BAB3",
471
+ "name": "Sistem Komunikasi Seluler",
472
+ "sks": 3,
473
+ "semester_plan": 7,
474
+ "corereq": [],
475
+ "attributes": {
476
+ "kategori": "Wajib"
477
+ }
478
+ },
479
+ {
480
+ "code": "AZK4BAA2",
481
+ "name": "Proposal Tugas Akhir",
482
+ "sks": 2,
483
+ "semester_plan": 7,
484
+ "corereq": [],
485
+ "attributes": {
486
+ "kategori": "Wajib"
487
+ }
488
+ },
489
+ {
490
+ "code": "UCKXEDB2",
491
+ "name": "Literasi Manusia",
492
+ "sks": 2,
493
+ "semester_plan": 7,
494
+ "corereq": [],
495
+ "attributes": {
496
+ "kategori": "Wajib"
497
+ }
498
+ },
499
+ {
500
+ "code": "MK_PILIHAN1",
501
+ "name": "Mata Kuliah Pilihan #1",
502
+ "sks": 3,
503
+ "semester_plan": 7,
504
+ "corereq": [],
505
+ "attributes": {
506
+ "kategori": "Pilihan"
507
+ }
508
+ },
509
+ {
510
+ "code": "MK_PILIHAN2",
511
+ "name": "Mata Kuliah Pilihan #2",
512
+ "sks": 3,
513
+ "semester_plan": 7,
514
+ "corereq": [],
515
+ "attributes": {
516
+ "kategori": "Pilihan"
517
+ }
518
+ },
519
+ {
520
+ "code": "MK_PILIHAN3",
521
+ "name": "Mata Kuliah Pilihan #3",
522
+ "sks": 3,
523
+ "semester_plan": 7,
524
+ "corereq": [],
525
+ "attributes": {
526
+ "kategori": "Pilihan"
527
+ }
528
+ },
529
+ {
530
+ "code": "MK_PILIHAN4",
531
+ "name": "Mata Kuliah Pilihan #4",
532
+ "sks": 3,
533
+ "semester_plan": 7,
534
+ "corereq": [],
535
+ "attributes": {
536
+ "kategori": "Pilihan"
537
+ }
538
+ },
539
+ {
540
+ "code": "MK_PILIHAN5",
541
+ "name": "Mata Kuliah Pilihan #5",
542
+ "sks": 3,
543
+ "semester_plan": 8,
544
+ "corereq": [],
545
+ "attributes": {
546
+ "kategori": "Pilihan"
547
+ }
548
+ },
549
+ {
550
+ "code": "MK_PILIHAN6",
551
+ "name": "Mata Kuliah Pilihan #6",
552
+ "sks": 3,
553
+ "semester_plan": 8,
554
+ "corereq": [],
555
+ "attributes": {
556
+ "kategori": "Pilihan"
557
+ }
558
+ },
559
+ {
560
+ "code": "UCKXADB2",
561
+ "name": "Bahasa Inggris",
562
+ "sks": 2,
563
+ "semester_plan": 8,
564
+ "corereq": [],
565
+ "attributes": {
566
+ "kategori": "Wajib"
567
+ }
568
+ },
569
+ {
570
+ "code": "AZK4CAA4",
571
+ "name": "Tugas Akhir",
572
+ "sks": 4,
573
+ "semester_plan": 8,
574
+ "corereq": [],
575
+ "attributes": {
576
+ "kategori": "Wajib"
577
+ }
578
+ },
579
+ {
580
+ "code": "UBKXACB2",
581
+ "name": "Kewarganegaraan",
582
+ "sks": 3,
583
+ "semester_plan": 8,
584
+ "corereq": [],
585
+ "attributes": {
586
+ "kategori": "Wajib"
587
+ }
588
+ }
589
+ ],
590
+ "edges": [
591
+ {
592
+ "from":"AZK1AAB3",
593
+ "to": "AZK1FAB3",
594
+ "type": "prereq"
595
+ },
596
+ {
597
+ "from": "AZK1BAB3",
598
+ "to": "AZK1GAB3",
599
+ "type": "prereq"
600
+ },
601
+ {
602
+ "from": "AZK1BAB3",
603
+ "to": "AZK1CAB1",
604
+ "type": "coreq"
605
+ },
606
+ {
607
+ "from": "AZK1GAB3",
608
+ "to": "AZK1HAB1",
609
+ "type": "coreq"
610
+ },
611
+ {
612
+ "from": "AZK1DAB3",
613
+ "to": "AZK1IAB1",
614
+ "type": "coreq"
615
+ },
616
+ {
617
+ "from": "AZK1AAB3",
618
+ "to": "AZK1KAB3",
619
+ "type": "prereq"
620
+ },
621
+ {
622
+ "from": "AAK1BAB2",
623
+ "to": "AAK2GAB3",
624
+ "type": "prereq"
625
+ },
626
+ {
627
+ "from": "AZK1DAB3",
628
+ "to": "AAK2FAB2",
629
+ "type": "prereq"
630
+ },
631
+
632
+ {
633
+ "from": "AZK1FAB3",
634
+ "to": "AZK2AAB3",
635
+ "type": "prereq"
636
+ },
637
+ {
638
+ "from": "AZK1FAB3",
639
+ "to": "AZK2DAB3",
640
+ "type": "prereq"
641
+ },
642
+ {
643
+ "from": "AZK1FAB3",
644
+ "to": "AAK2DAB3",
645
+ "type": "prereq"
646
+ },
647
+
648
+ {
649
+ "from": "AZK1FAB3",
650
+ "to": "AZK2CAB3",
651
+ "type": "prereq"
652
+ },
653
+
654
+ {
655
+ "from": "AZK1GAB3",
656
+ "to": "AZK2DAB3",
657
+ "type": "prereq"
658
+ },
659
+ {
660
+ "from": "AZK1KAB3",
661
+ "to": "AZK2CAB3",
662
+ "type": "prereq"
663
+ },
664
+ {
665
+ "from": "AZK2DAB3",
666
+ "to": "AZK2FAB3",
667
+ "type": "prereq"
668
+ },
669
+ {
670
+ "from": "AAK2FAB2",
671
+ "to": "AAK2KAB3",
672
+ "type": "prereq"
673
+ },
674
+ {
675
+ "from": "AAK2DAB3",
676
+ "to": "AZK2GAB3",
677
+ "type": "prereq"
678
+ },
679
+ {
680
+ "from": "AAK2DAB3",
681
+ "to": "AZK2EAB3",
682
+ "type": "prereq"
683
+ },
684
+ {
685
+ "from": "AZK2CAB3",
686
+ "to": "AZK2GAB3",
687
+ "type": "prereq"
688
+ },
689
+ {
690
+ "from": "AZK2AAB3",
691
+ "to": "AAK3BAB3",
692
+ "type": "prereq"
693
+ },
694
+ {
695
+ "from": "AAK2GAB3",
696
+ "to": "AAK2NAB3",
697
+ "type": "prereq"
698
+ },
699
+ {
700
+ "from": "AZK2EAB3",
701
+ "to": "AAK3DAB2",
702
+ "type": "prereq"
703
+ },
704
+
705
+ {
706
+ "from": "AZK2GAB3",
707
+ "to": "AZK3AAB3",
708
+ "type": "prereq"
709
+ },
710
+ {
711
+ "from": "AZK2GAB3",
712
+ "to": "AAK3BAB3",
713
+ "type": "prereq"
714
+ },
715
+ {
716
+ "from": "AAK2NAB3",
717
+ "to": "AAK3FAB3",
718
+ "type": "prereq"
719
+ },
720
+ {
721
+ "from": "AZK2HAB3",
722
+ "to": "AAK3JAB3",
723
+ "type": "prereq"
724
+ },
725
+ {
726
+ "from": "AZK2FAB3",
727
+ "to": "AAK3IAB3",
728
+ "type": "prereq"
729
+ },
730
+ {
731
+ "from": "AZK2EAB3",
732
+ "to": "AAK3OAB3",
733
+ "type": "prereq"
734
+ },
735
+ {
736
+ "from": "AAK3BAB3",
737
+ "to": "AAK3MAB3",
738
+ "type": "prereq"
739
+ },
740
+ {
741
+ "from": "AAK3BAB3",
742
+ "to": "AAK3OAB3",
743
+ "type": "prereq"
744
+ },
745
+ {
746
+ "from": "AZK3AAB3",
747
+ "to": "AAK3IAB3",
748
+ "type": "prereq"
749
+ },
750
+ {
751
+ "from": "AAK3BAB3",
752
+ "to": "AAK3IAB3",
753
+ "type": "prereq"
754
+ },
755
+ {
756
+ "from": "AAK3DAB2",
757
+ "to": "AAK3IAB3",
758
+ "type": "prereq"
759
+ },
760
+ {
761
+ "from": "AAK3DAB2",
762
+ "to": "AAK3NAB2",
763
+ "type": "prereq"
764
+ },
765
+ {
766
+ "from": "AAK3MAB3",
767
+ "to": "AAK4BAB3",
768
+ "type": "prereq"
769
+ },
770
+ {
771
+ "from": "AAK3NAB2",
772
+ "to": "AAK4BAB3",
773
+ "type": "prereq"
774
+ },
775
+ {
776
+ "from": "AAK2AAB1",
777
+ "to": "AAK2HAB1",
778
+ "type": "prereq"
779
+ },
780
+ {
781
+ "from": "AAK2HAB1",
782
+ "to": "AAK3AAB1",
783
+ "type": "prereq"
784
+ },
785
+ {
786
+ "from": "AAK3AAB1",
787
+ "to": "AAK3LAB1",
788
+ "type": "prereq"
789
+ },
790
+ {
791
+ "from": "AZK4BAA2",
792
+ "to": "AZK4CAA4",
793
+ "type": "prereq"
794
+ }
795
+ ]
796
  }
explanation_builder.py CHANGED
@@ -1,235 +1,262 @@
1
- # ======================================================================
2
- # --- explanation_builder.py ---
3
- # ======================================================================
4
-
5
- import random
6
- from typing import List, Dict, Any
7
-
8
- # ======================================================================
9
- # 1. BANK TEMPLATE (Natural Language & Emoji)
10
- # ======================================================================
11
- EXPLANATION_TEMPLATES = {
12
- "pembuka": {
13
- "Resiko Tinggi": [
14
- "⚠️ **Perhatian Serius Diperlukan:** Sistem mendeteksi indikator risiko tinggi pada profil akademik Anda. Berikut adalah faktor krusial yang memicunya:",
15
- "🚨 **Peringatan Dini:** Berdasarkan pola data historis, performa Anda saat ini berada dalam zona 'Resiko Tinggi'. Hal ini didorong oleh faktor-faktor berikut:",
16
- "🛑 **Analisis Kritis:** Terdapat akumulasi faktor yang menempatkan Anda pada kategori 'Resiko Tinggi'. Mohon perhatikan poin-poin evaluasi ini:"
17
- ],
18
- "Resiko Sedang": [
19
- "⚠️ **Waspada:** Profil Anda menunjukkan tanda-tanda 'Resiko Sedang'. Belum kritis, namun perlu perbaikan segera pada aspek berikut:",
20
- "💡 **Perlu Evaluasi:** Sistem mendeteksi adanya ketidakstabilan yang memicu status 'Resiko Sedang'. Berikut adalah area yang perlu mendapat perhatian:",
21
- "🚩 **Zona Kuning:** Anda berada di kategori 'Resiko Sedang'. Ada keseimbangan antara faktor positif dan negatif, namun poin berikut perlu diwaspadai:"
22
- ],
23
- "Resiko Rendah": [
24
- "✅ **Cukup Aman:** Profil akademik Anda tergolong 'Resiko Rendah', namun tetap ada beberapa catatan kecil untuk menjaga konsistensi:",
25
- "📈 **Progres Baik:** Secara umum performa Anda stabil di zona aman. Sistem hanya menyoroti beberapa hal minor berikut:",
26
- "🛡️ **Terkendali:** Prediksi risiko Anda rendah. Pertahankan momentum ini, sambil memperhatikan sedikit catatan berikut:"
27
- ],
28
- "Aman": [
29
- "🌟 **Sangat Baik:** Selamat! Rekam jejak akademik Anda sangat solid sehingga dikategorikan 'Aman'. Faktor pendukung utamanya adalah:",
30
- "🚀 **Performa Unggul:** Sistem tidak mendeteksi masalah berarti. Prediksi 'Aman' ini didukung oleh pondasi akademik yang kuat berikut ini:",
31
- "🏆 **Top Performance:** Data menunjukkan stabilitas yang sangat baik. Berikut adalah poin-poin kekuatan profil Anda:"
32
- ],
33
- "default": "🔍 Berikut adalah faktor-faktor analisis sistem untuk kategori '{prediction_val}':"
34
- },
35
- "fitur": {
36
- # --- 1. PERFORMA TERKINI (IPS) ---
37
- "IPS_Terakhir": {
38
- "rendah": [
39
- "📉 **Penurunan Terkini:** IPS semester terakhir Anda tercatat **{value:.2f}**, yang berada di bawah ambang batas ideal.",
40
- "⚠️ **Warning Semester Lalu:** Performa semester terakhir (**{value:.2f}**) menjadi kontributor utama risiko karena belum memenuhi standar aman.",
41
- "🔻 **Butuh Boost:** Capaian IPS terakhir (**{value:.2f}**) terindikasi rendah, menyeret turun profil risiko Anda secara keseluruhan."
42
- ],
43
- "tinggi": [
44
- " **Momentum Positif:** IPS semester terakhir Anda (**{value:.2f}**) sangat solid, menunjukkan Anda sedang dalam performa yang baik.",
45
- "📈 **Finish Kuat:** Capaian semester terakhir yang tinggi (**{value:.2f}**) menjadi sinyal kuat bahwa Anda mampu mengatasi beban studi.",
46
- "🌟 **Nilai Kompetitif:** IPS terakhir di angka **{value:.2f}** memberikan bobot positif yang signifikan pada prediksi ini."
47
- ]
48
- },
49
-
50
- # --- 2. PERFORMA KUMULATIF (IPK) ---
51
- "IPK_Terakhir": {
52
- "rendah": [
53
- "🏗️ **Pondasi Rapuh:** IPK kumulatif saat ini (**{value:.2f}**) terdeteksi di zona yang memerlukan perbaikan segera.",
54
- "📉 **Akumulasi Nilai:** Secara keseluruhan, IPK Anda (**{value:.2f}**) masih di bawah ambang batas aman sistem.",
55
- "🆘 **Perhatian:** IPK Terakhir (**{value:.2f}**) adalah faktor dominan yang menempatkan Anda pada risiko ini."
56
- ],
57
- "tinggi": [
58
- "🏛️ **Pondasi Kokoh:** IPK kumulatif Anda (**{value:.2f}**) sangat baik dan menjadi penyangga utama profil akademik Anda.",
59
- "🛡️ **Rekam Jejak Solid:** Konsistensi nilai yang tercermin dari IPK (**{value:.2f}**) menjauhkan Anda dari risiko akademik.",
60
- "🎓 **Prestasi Stabil:** IPK di angka **{value:.2f}** menunjukkan pemahaman materi yang konsisten di atas rata-rata."
61
- ]
62
- },
63
-
64
- # --- 3. KEGAGALAN MATA KULIAH ---
65
- "Jumlah_MK_Gagal": {
66
- "rendah": [
67
- " **Rekam Jejak Bersih:** Anda memiliki sedikit/tanpa mata kuliah gagal (Total: {value}), yang sangat bagus untuk kelancaran studi.",
68
- "✅ **Efisiensi Studi:** Minimnya mata kuliah yang harus diulang (Total: {value}) adalah indikator positif yang kuat.",
69
- "🛡️ **Bebas Hambatan:** Tidak adanya beban mata kuliah gagal yang signifikan menjaga risiko Anda tetap rendah."
70
- ],
71
- "tinggi": [
72
- "🎒 **Beban Mengulang:** Terdapat **{value}** mata kuliah gagal. Tumpukan beban ini meningkatkan risiko akademik secara signifikan.",
73
- "🚧 **Hambatan Studi:** Jumlah mata kuliah gagal yang tinggi (**{value} MK**) terdeteksi sebagai 'red flag' utama dalam profil ini.",
74
- "🚨 **Perlu Perbaikan:** Akumulasi **{value}** mata kuliah yang belum lulus menuntut perhatian ekstra untuk semester depan."
75
- ]
76
- },
77
- "Total_SKS_Gagal": {
78
- "rendah": [
79
- " **Minim SKS Hangus:** Total SKS dari mata kuliah gagal sangat minim.",
80
- "✨ **Efektif:** Hampir seluruh SKS yang diambil berhasil lulus."
81
- ],
82
- "tinggi": [
83
- "⚠️ **SKS Terbuang:** Total SKS gagal yang besar membebani rasio kelulusan Anda.",
84
- "🛑 **Warning SKS:** Banyak kredit SKS yang harus diulang."
85
- ]
86
- },
87
-
88
- # --- 4. PROGRES SKS ---
89
- "Total_SKS": {
90
- "rendah": [
91
- " **Progres Lambat:** Total SKS yang berhasil dikumpulkan (**{value}**) masih tertinggal dari target ideal tahap ini.",
92
- "🐢 **Perlu Akselerasi:** Jumlah SKS lulus (**{value}**) tergolong sedikit, mengindikasikan perlunya strategi pengambilan SKS yang lebih optimal."
93
- ],
94
- "tinggi": [
95
- "🏃 **On-Track:** Tabungan SKS Anda (**{value}**) sudah cukup banyak, menandakan progres studi yang lancar.",
96
- "🎯 **Target Tercapai:** Jumlah SKS lulus (**{value}**) sudah memenuhi standar progres yang diharapkan."
97
- ]
98
- },
99
-
100
- # --- 5. TREN & KONSISTENSI ---
101
- "Tren_IPS_Slope": {
102
- "rendah": [
103
- "📉 **Tren Menurun:** Analisis regresi menunjukkan grafik performa Anda cenderung melandai/turun belakangan ini.",
104
- "⚠️ **Kehilangan Momentum:** Nilai *slope* tren akademik Anda negatif/rendah, waspadai penurunan semangat belajar."
105
- ],
106
- "tinggi": [
107
- "🚀 **Tren Menanjak:** Grafik nilai Anda menunjukkan tren kenaikan (slope positif). Pertahankan semangat ini!",
108
- "📈 **Perbaikan Konsisten:** Sistem mendeteksi pola kenaikan performa yang konsisten dari waktu ke waktu."
109
- ]
110
- },
111
- "Rentang_IPS": {
112
- "rendah": [
113
- "⚖️ **Performa Stabil:** Fluktuasi nilai Anda kecil, menunjukkan konsistensi belajar yang baik.",
114
- "🔹 **Konsisten:** Tidak ada lonjakan atau penurunan drastis pada sejarah nilai."
115
- ],
116
- "tinggi": [
117
- "🎢 **Nilai Fluktuatif:** Terdeteksi rentang nilai yang lebar (tidak stabil). Performa Anda cenderung naik-turun drastis.",
118
- "⚠️ **Inkonsistensi:** Ada semester di mana nilai sangat tinggi dan sangat rendah. Konsistensi perlu ditingkatkan."
119
- ]
120
- },
121
-
122
- # --- 6. FITUR ONE-HOT (TREN KATEGORIKAL) ---
123
- "Tren_Menaik": {
124
- "ya": "📈 **Grafik Positif:** Pola data Anda secara eksplisit dikategorikan sebagai tren 'Menaik'.",
125
- "tidak": "🔸 **Tidak Ada Kenaikan:** Pola data saat ini tidak menunjukkan tren kenaikan yang signifikan."
126
- },
127
- "Tren_Menurun": {
128
- "ya": "📉 **Peringatan Penurunan:** Pola data Anda secara eksplisit dikategorikan sebagai tren 'Menurun'. Segera lakukan evaluasi.",
129
- "tidak": "✅ **Aman dari Penurunan:** Untungnya, profil Anda tidak menunjukkan pola tren 'Menurun'."
130
- },
131
- "Tren_Stabil": {
132
- "ya": "➡️ **Stagnan/Stabil:** Pola nilai Anda cenderung datar (Stabil). Ini bisa baik atau buruk tergantung nilai rata-ratanya.",
133
- "tidak": "🔀 **Dinamis:** Profil nilai Anda tidak stagnan, ada pergerakan naik atau turun."
134
- },
135
-
136
- # --- DEFAULT FALLBACK ---
137
- "default": {
138
- "rendah": "🔹 Nilai **{feature_name}** tercatat **{value:.2f}**, lebih rendah dari batas acuan ({threshold:.2f}).",
139
- "tinggi": "🔸 Nilai **{feature_name}** tercatat **{value:.2f}**, lebih tinggi dari batas acuan ({threshold:.2f})."
140
- }
141
- }
142
- }
143
-
144
-
145
- # ======================================================================
146
- # 2. FUNGSI BUILDER (With Deduplication & Random Logic)
147
- # ======================================================================
148
-
149
- def build_explanation_from_rules(structured_rules: List[Dict], prediction_val: str) -> Dict[str, Any]:
150
- """
151
- Merakit penjelasan dinamis dengan emoji, anti-duplikat, dan variasi kalimat.
152
- """
153
- try:
154
- # 1. Pilih Kalimat Pembuka (Random Choice dari List)
155
- opening_templates = EXPLANATION_TEMPLATES["pembuka"].get(prediction_val)
156
-
157
- if not opening_templates:
158
- # Fallback jika key prediksi tidak ditemukan di template
159
- opening_line = EXPLANATION_TEMPLATES["pembuka"]["default"].format(prediction_val=prediction_val)
160
- elif isinstance(opening_templates, list):
161
- # Pilih salah satu variasi secara acak
162
- opening_line = random.choice(opening_templates)
163
- else:
164
- # Jika ternyata cuma string tunggal
165
- opening_line = opening_templates
166
-
167
- explained_rules_list = []
168
- features_explained = set() # Set untuk mencegah duplikasi faktor
169
-
170
- # 2. Iterasi aturan SECARA TERBALIK (dari Leaf -> Root)
171
- # Tujuannya: Mengambil aturan yang paling spesifik (di ujung pohon) terlebih dahulu.
172
- for rule in reversed(structured_rules):
173
- feature = rule["feature"]
174
-
175
- # LOGIKA ANTI-DUPLIKAT
176
- # Jika fitur sudah dijelaskan oleh node yang lebih spesifik, skip node induknya.
177
- if feature in features_explained:
178
- continue
179
-
180
- features_explained.add(feature)
181
-
182
- condition = rule["condition"]
183
- chosen_template = None
184
-
185
- # A. Logika untuk Fitur One-Hot (Tren) -> Ya/Tidak
186
- if feature in ["Tren_Menaik", "Tren_Menurun", "Tren_Stabil"]:
187
- if condition == "tinggi":
188
- chosen_template = EXPLANATION_TEMPLATES["fitur"][feature]["ya"]
189
- else:
190
- chosen_template = EXPLANATION_TEMPLATES["fitur"][feature]["tidak"]
191
-
192
- # B. Logika untuk Fitur Numerik -> Rendah/Tinggi
193
- else:
194
- # Ambil template fitur, fallback ke default jika nama fitur tidak ada di bank template
195
- feature_templates = EXPLANATION_TEMPLATES["fitur"].get(feature, EXPLANATION_TEMPLATES["fitur"]["default"])
196
-
197
- # Ambil template kondisi (rendah/tinggi)
198
- condition_templates = feature_templates.get(condition)
199
-
200
- # Jika template berupa LIST (ada variasi kalimat), pilih satu acak
201
- if isinstance(condition_templates, list):
202
- template_str = random.choice(condition_templates)
203
- chosen_template = template_str.format(
204
- feature_name=feature,
205
- value=rule["value"],
206
- threshold=rule["threshold"]
207
- )
208
- # Jika template berupa STRING tunggal
209
- elif isinstance(condition_templates, str):
210
- chosen_template = condition_templates.format(
211
- feature_name=feature,
212
- value=rule["value"],
213
- threshold=rule["threshold"]
214
- )
215
-
216
- if chosen_template:
217
- explained_rules_list.append(chosen_template)
218
-
219
- # 3. Balikkan list agar urutannya logis (Penting -> Pendukung)
220
- # Karena tadi kita iterasi dari bawah (reversed), hasilnya jadi terbalik.
221
- # Sebenarnya urutan "Leaf first" (Paling spesifik dulu) seringkali lebih baik untuk dibaca.
222
- # Namun jika ingin urutan flow Decision Tree (Root -> Leaf), uncomment baris di bawah:
223
- # explained_rules_list.reverse()
224
-
225
-
226
- return {
227
- "opening_line": opening_line,
228
- "factors": explained_rules_list
229
- }
230
-
231
- except Exception as e:
232
- return {
233
- "opening_line": f"⚠️ Maaf, terjadi kesalahan saat menyusun penjelasan: {str(e)}",
234
- "factors": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  }
 
1
+ # ======================================================================
2
+ # --- explanation_builder.py (LOGIKA DIPERBAIKI & DIBERSIHKAN) ---
3
+ # ======================================================================
4
+
5
+ import random
6
+ from typing import List, Dict, Any
7
+
8
+ # Impor fungsi paragraf dari file terpisah
9
+ from .recommendation_builder import generate_recommendation_paragraph
10
+
11
+ # ======================================================================
12
+ # 1. KONFIGURASI NAMA FITUR (MAPPING)
13
+ # ======================================================================
14
+ # Dictionary ini mengubah nama variabel kode (raw) menjadi teks yang enak dibaca user.
15
+ FEATURE_LABEL_MAP = {
16
+ # Fitur Utama
17
+ "IPK_Terakhir": "IPK Semester Terakhir",
18
+ "IPS_Terakhir": "IPS Semester Terakhir",
19
+ "Total_SKS": "Total SKS Diambil",
20
+ "IPS_Tertinggi": "Capaian IPS Tertinggi",
21
+ "IPS_Terendah": "Capaian IPS Terendah",
22
+ "Rentang_IPS": "Stabilitas Nilai (Rentang IPS)",
23
+ "Jumlah_MK_Gagal": "Jumlah Mata Kuliah Gagal",
24
+ "Total_SKS_Gagal": "Total SKS Gagal/Hangus",
25
+
26
+ # Fitur Analitik/Tren
27
+ "Tren_IPS_Slope": "Tren Perubahan Nilai",
28
+ "Perubahan_Kinerja_Terakhir": "Perubahan Kinerja Terakhir",
29
+ "IPK_Ternormalisasi_SKS": "Rasio Efisiensi IPK per SKS",
30
+
31
+ # Fitur OHE (One Hot Encoding)
32
+ "Tren_Menaik": "Pola Tren Menaik",
33
+ "Tren_Menurun": "Pola Tren Menurun",
34
+ "Tren_Stabil": "Pola Tren Stabil"
35
+ }
36
+
37
+ # ======================================================================
38
+ # 2. BANK TEMPLATE (Faktor / Poin-Poin)
39
+ # ======================================================================
40
+ EXPLANATION_TEMPLATES = {
41
+ "pembuka": {
42
+ "Resiko Tinggi": [
43
+ "⚠️ Perhatian Serius Diperlukan: Sistem mendeteksi indikator risiko tinggi pada profil akademik Anda. Berikut adalah faktor krusial yang memicunya:",
44
+ "🚨 Peringatan Dini: Berdasarkan pola data historis, performa Anda saat ini berada dalam zona 'Resiko Tinggi'.",
45
+ ],
46
+ "Resiko Sedang": [
47
+ "⚠️ Waspada: Profil Anda menunjukkan tanda-tanda 'Resiko Sedang'. Belum kritis, namun perlu perbaikan segera.",
48
+ "💡 Perlu Evaluasi: Sistem mendeteksi adanya ketidakstabilan yang memicu status 'Resiko Sedang'.",
49
+ ],
50
+ "Resiko Rendah": [
51
+ "✅ Cukup Aman: Profil akademik Anda tergolong 'Resiko Rendah', namun tetap ada beberapa catatan kecil:",
52
+ "📈 Progres Baik: Secara umum performa Anda stabil di zona aman. Sistem menyoroti beberapa hal minor:",
53
+ ],
54
+ "Aman": [
55
+ "🌟 Sangat Baik: Selamat! Rekam jejak akademik Anda sangat solid sehingga dikategorikan 'Aman'.",
56
+ "🚀 Performa Unggul: Sistem tidak mendeteksi masalah berarti. Prediksi 'Aman' didukung pondasi yang kuat.",
57
+ ],
58
+ "default": "🔍 Berikut adalah faktor-faktor analisis sistem untuk kategori '{prediction_val}':"
59
+ },
60
+ "fitur": {
61
+ # --- (Template ini akan dipilih oleh logic Sanity Check di bawah) ---
62
+ "IPS_Terakhir": {
63
+ "rendah_parah": "📉 Penurunan Drastis: IPS semester terakhir ({value:.2f}) sangat rendah.",
64
+ "rendah": "⚠️ Penurunan: IPS semester terakhir ({value:.2f}) berada di bawah ambang batas ideal.",
65
+ "cukup": "✅ Cukup: IPS semester terakhir ({value:.2f}) tercatat di atas ambang batas kritis model.",
66
+ "baik": "📈 Baik: Capaian IPS semester terakhir ({value:.2f}) memberikan kontribusi positif.",
67
+ "tinggi": "🌟 Sangat Solid: IPS semester terakhir Anda ({value:.2f}) sangat baik."
68
+ },
69
+ "IPK_Terakhir": {
70
+ "rendah_parah": "🆘 Zona Bahaya: IPK kumulatif ({value:.2f}) berada di bawah 2.00.",
71
+ "rendah": "🏗️ Pondasi Rapuh: IPK kumulatif ({value:.2f}) terdeteksi di zona yang memerlukan perbaikan.",
72
+ "cukup": "🛡️ Cukup Aman: IPK kumulatif ({value:.2f}) telah lolos ambang batas kritis model.",
73
+ "baik": "🏛️ Pondasi Kokoh: IPK kumulatif ({value:.2f}) Anda tergolong baik.",
74
+ "tinggi": "🏆 Prestasi: IPK kumulatif ({value:.2f}) Anda sangat solid."
75
+ },
76
+ "Jumlah_MK_Gagal": {
77
+ "rendah": "✨ Rekam Jejak Bersih: Anda memiliki sedikit/tanpa mata kuliah gagal (Total: {value}).",
78
+ "tinggi_sedikit": "🎒 Beban Mengulang: Terdapat {value} mata kuliah gagal yang perlu diwaspadai.",
79
+ "tinggi_banyak": "🚨 Beban Berat: Terdapat {value} mata kuliah gagal. Tumpukan beban ini meningkatkan risiko."
80
+ },
81
+ "Tren_IPS_Slope": {
82
+ "rendah": "📉 Tren Menurun: Grafik performa Anda cenderung melandai/turun.",
83
+ "tinggi_sedikit": "📈 Tren Membaik: Grafik nilai Anda menunjukkan sedikit tren kenaikan (slope positif).",
84
+ "tinggi_kuat": "🚀 Tren Menanjak: Grafik nilai Anda menunjukkan tren kenaikan yang kuat."
85
+ },
86
+ "Rentang_IPS": {
87
+ "rendah": "⚖️ Performa Stabil: Fluktuasi nilai Anda kecil ({value:.2f}), menunjukkan konsistensi.",
88
+ "tinggi": "🎢 Nilai Fluktuatif: Terdeteksi rentang nilai yang lebar ({value:.2f}) (tidak stabil)."
89
+ },
90
+ "Total_SKS_Gagal": {
91
+ "rendah": "✅ Minim SKS Hangus: Total SKS dari mata kuliah gagal sangat minim ({value}).",
92
+ "tinggi": "⚠️ SKS Terbuang: Total SKS gagal ({value}) cukup besar dan membebani rasio kelulusan."
93
+ },
94
+ "Total_SKS": {
95
+ "rendah": "⏳ Progres Lambat: Total SKS ({value}) masih tertinggal dari target.",
96
+ "tinggi": "🏃 On-Track: Tabungan SKS Anda ({value}) sudah cukup banyak."
97
+ },
98
+ # --- Fitur OHE ---
99
+ "Tren_Menaik": { "ya": "📈 Grafik Positif: Pola data dikategorikan sebagai tren 'Menaik'." },
100
+ "Tren_Menurun": { "ya": "📉 Peringatan Penurunan: Pola data dikategorikan sebagai tren 'Menurun'." },
101
+ "Tren_Stabil": { "ya": "➡️ Stagnan/Stabil: Pola nilai Anda cenderung datar (Stabil)." },
102
+ # --- Fallback (Generik) ---
103
+ "default": {
104
+ "rendah": "🔹 Nilai {feature_name} tercatat {value:.2f}, di bawah acuan ({threshold:.2f}).",
105
+ "tinggi": "🔸 Nilai {feature_name} tercatat {value:.2f}, di atas acuan ({threshold:.2f})."
106
+ }
107
+ }
108
+ }
109
+
110
+ # ======================================================================
111
+ # 3. FUNGSI LOGIC PEMILIH TEKS
112
+ # ======================================================================
113
+ def _get_explanation_text(rule: Dict[str, Any]) -> str:
114
+ """Memilih template FAKTOR yang paling sesuai berdasarkan NILAI ASLI."""
115
+
116
+ raw_feature = rule["feature"] # Contoh: "Perubahan_Kinerja_Terakhir"
117
+ condition = rule["condition"]
118
+ value = rule["value"]
119
+ threshold = rule["threshold"]
120
+
121
+ # [PERBAIKAN UTAMA] Translasi Nama Fitur
122
+ # Jika nama ada di map, gunakan. Jika tidak, hilangkan underscore manual.
123
+ readable_feature_name = FEATURE_LABEL_MAP.get(raw_feature, raw_feature.replace("_", " "))
124
+
125
+ templates = EXPLANATION_TEMPLATES["fitur"]
126
+
127
+ # 1. Fitur OHE
128
+ if raw_feature in ["Tren_Menaik", "Tren_Menurun", "Tren_Stabil"]:
129
+ key = "ya" if condition == "tinggi" else "tidak"
130
+ # Kita hanya definisikan 'ya', jadi jika 'tidak' akan di-skip (return None)
131
+ return templates.get(raw_feature, {}).get(key)
132
+
133
+ # 2. Fitur Numerik (Sanity Check)
134
+ chosen_template_key = ""
135
+ if raw_feature == "IPS_Terakhir":
136
+ if condition == "tinggi":
137
+ if value < 2.75: chosen_template_key = "cukup" # Untuk 2.62
138
+ elif value < 3.25: chosen_template_key = "baik" # Untuk 2.92
139
+ else: chosen_template_key = "tinggi"
140
+ else:
141
+ if value < 2.0: chosen_template_key = "rendah_parah"
142
+ else: chosen_template_key = "rendah"
143
+
144
+ elif raw_feature == "IPK_Terakhir":
145
+ if condition == "tinggi":
146
+ if value < 2.75: chosen_template_key = "cukup" # Untuk 2.15
147
+ elif value < 3.25: chosen_template_key = "baik" # Untuk 2.78
148
+ else: chosen_template_key = "tinggi"
149
+ else:
150
+ if value < 2.0: chosen_template_key = "rendah_parah"
151
+ else: chosen_template_key = "rendah"
152
+
153
+ elif raw_feature == "Jumlah_MK_Gagal":
154
+ if condition == "rendah": # (value 0)
155
+ chosen_template_key = "rendah"
156
+ else: # tinggi
157
+ if value <= 3: chosen_template_key = "tinggi_sedikit"
158
+ else: chosen_template_key = "tinggi_banyak" # Untuk 4
159
+
160
+ elif raw_feature == "Tren_IPS_Slope":
161
+ if condition == "tinggi":
162
+ if value < 0.1: chosen_template_key = "tinggi_sedikit" # Untuk 0.066
163
+ else: chosen_template_key = "tinggi_kuat" # Untuk 0.125
164
+ else:
165
+ chosen_template_key = "rendah"
166
+
167
+ # 3. Fallback Logic (Jika Sanity Check tidak menemukan key)
168
+ if not chosen_template_key:
169
+ if raw_feature in templates:
170
+ # Jika fitur punya template khusus di dictionary 'fitur'
171
+ chosen_template_key = condition
172
+ else:
173
+ # Jika fitur benar-benar baru/tidak ada di dictionary, pakai DEFAULT.
174
+ # Di sini kita gunakan 'readable_feature_name' agar output bersih.
175
+ return templates["default"][condition].format(
176
+ feature_name=readable_feature_name,
177
+ value=value,
178
+ threshold=threshold
179
+ )
180
+
181
+ # 4. Ambil template berdasarkan key yang sudah dipilih
182
+ template_str = templates.get(raw_feature, {}).get(chosen_template_key)
183
+
184
+ # 5. Handle jika key (misal 'cukup') ada logic-nya, tapi string templatenya belum dibuat
185
+ if not template_str:
186
+ # Fallback ke default 'tinggi'/'rendah' milik fitur tersebut
187
+ template_str = templates.get(raw_feature, {}).get(condition)
188
+
189
+ if not template_str:
190
+ # Jika 'rendah' pun tidak ada, kembali ke DEFAULT global
191
+ return templates["default"][condition].format(
192
+ feature_name=readable_feature_name,
193
+ value=value,
194
+ threshold=threshold
195
+ )
196
+
197
+ # Format string (jika template adalah list, pilih acak)
198
+ if isinstance(template_str, list):
199
+ template_str = random.choice(template_str)
200
+
201
+ # Pastikan string tidak None sebelum di-format
202
+ if not template_str:
203
+ return None
204
+
205
+ # [PERBAIKAN] Inject readable_feature_name ke dalam format
206
+ return template_str.format(
207
+ feature_name=readable_feature_name,
208
+ value=value,
209
+ threshold=threshold
210
+ )
211
+
212
+
213
+ # ======================================================================
214
+ # 4. FUNGSI BUILDER UTAMA (Facade)
215
+ # ======================================================================
216
+ def build_full_response(structured_rules: List[Dict[str, Any]], prediction_val: str) -> Dict[str, Any]:
217
+ """
218
+ Merakit respons lengkap: Poin Faktor + Paragraf Rekomendasi
219
+ """
220
+ try:
221
+ # --- BAGIAN 1: BUAT POIN FAKTOR ---
222
+ opening_templates = EXPLANATION_TEMPLATES["pembuka"].get(prediction_val)
223
+
224
+ if not opening_templates:
225
+ # Fallback jika key prediksi (misal 'Resiko Sedang') tidak ada
226
+ default_template = EXPLANATION_TEMPLATES["pembuka"].get("default", "Analisis Faktor:")
227
+ opening_line = default_template.format(prediction_val=prediction_val)
228
+ elif isinstance(opening_templates, list):
229
+ opening_line = random.choice(opening_templates)
230
+ else:
231
+ opening_line = opening_templates
232
+
233
+ factors_list = []
234
+ features_explained = set()
235
+
236
+ for rule in reversed(structured_rules):
237
+ feature = rule["feature"]
238
+ if feature in features_explained: continue
239
+ features_explained.add(feature)
240
+
241
+ # Panggil fungsi yang sudah diperbaiki
242
+ chosen_template = _get_explanation_text(rule)
243
+
244
+ if chosen_template: # Hanya tambahkan jika string tidak None/Kosong
245
+ factors_list.append(chosen_template)
246
+
247
+ # --- BAGIAN 2: BUAT PARAGRAF REKOMENDASI ---
248
+ recommendation_text = generate_recommendation_paragraph(prediction_val, structured_rules)
249
+
250
+ # --- BAGIAN 3: GABUNGKAN ---
251
+ return {
252
+ "opening_line": opening_line,
253
+ "factors": factors_list,
254
+ "recommendation": recommendation_text
255
+ }
256
+
257
+ except Exception as e:
258
+ return {
259
+ "opening_line": f"⚠️ Maaf, terjadi kesalahan saat menyusun penjelasan: {str(e)}",
260
+ "factors": [],
261
+ "recommendation": "Gagal memuat rekomendasi personal."
262
  }
graduation_logic.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================================================================
2
+ # --- graduation_logic.py ---
3
+ # ======================================================================
4
+
5
+ import networkx as nx
6
+ from typing import Dict, Any, List
7
+
8
+ def _predict_naive(current_semester: int, total_sks_passed: int, last_gpa: float) -> Dict[str, Any]:
9
+ """
10
+ Logika perhitungan matematis dasar (Naive) berdasarkan SKS dan IPK.
11
+ Menggunakan teks deskripsi custom sesuai permintaan user.
12
+ """
13
+ TARGET_SKS = 144
14
+ TARGET_SEMESTER = 8
15
+ MAX_SKS_REGULAR = 24 # Batas absolut reguler (biasanya IP >= 3.00)
16
+ LIMIT_SKS_LOW_GPA = 20 # Batas jika IP < 3.00
17
+ SAFE_THRESHOLD = 18 # Batas aman/santai
18
+
19
+ # 1. Hitung Sisa
20
+ sks_needed = TARGET_SKS - total_sks_passed
21
+ semesters_left = (TARGET_SEMESTER - current_semester) + 1
22
+
23
+ # Stats dasar untuk dikembalikan
24
+ stats = {
25
+ "sks_needed": sks_needed,
26
+ "semesters_left": semesters_left,
27
+ "required_pace": 0,
28
+ "student_capacity": int(MAX_SKS_REGULAR if last_gpa >= 3.00 else LIMIT_SKS_LOW_GPA)
29
+ }
30
+
31
+ # --- LOGIC: Sudah Semester Akhir / Lewat ---
32
+ if semesters_left <= 0:
33
+ if sks_needed <= 0:
34
+ return {
35
+ "status": "Lulus",
36
+ "color": "green",
37
+ "description": "Selamat! Anda telah menyelesaikan kebutuhan SKS minimal.",
38
+ "stats": stats
39
+ }
40
+ else:
41
+ stats["required_pace"] = sks_needed
42
+ return {
43
+ "status": "Terlambat",
44
+ "color": "red",
45
+ "description": f"Saat ini semester {current_semester} dan SKS belum terpenuhi. Target lulus 8 semester sudah terlewat.",
46
+ "stats": stats
47
+ }
48
+
49
+ # 2. Hitung Kecepatan yang Dibutuhkan (Required Pace)
50
+ required_sks_per_sem = sks_needed / semesters_left
51
+ stats["required_pace"] = round(required_sks_per_sem, 2)
52
+
53
+ # 3. Evaluasi Status (Teks dari User)
54
+ student_capacity = stats["student_capacity"]
55
+
56
+ if required_sks_per_sem > MAX_SKS_REGULAR:
57
+ # KASUS: Mustahil reguler
58
+ return {
59
+ "status": "🔴 Terlambat",
60
+ "color": "red",
61
+ "description": f"Target lulus semester 8 tidak memungkinkan. Anda butuh rata-rata {required_sks_per_sem:.1f} SKS yang perlu dipenuhi setiap semester selanjutnya, melebihi batas reguler yaitu 24 SKS.",
62
+ "stats": stats
63
+ }
64
+
65
+ elif required_sks_per_sem <= SAFE_THRESHOLD:
66
+ # KASUS: Aman
67
+ return {
68
+ "status": "🟢 Aman",
69
+ "color": "green",
70
+ "description": f"Posisi aman. Beban ringan, sisa (~{required_sks_per_sem:.1f} SKS yang perlu dipenuhi tiap semester. Pertahankan performa tiap semester!",
71
+ "stats": stats
72
+ }
73
+
74
+ elif required_sks_per_sem <= student_capacity:
75
+ # KASUS: Padat tapi Masih Mungkin
76
+ status_text = "🟡 Jadwal Relatif Padat"
77
+ return {
78
+ "status": status_text,
79
+ "color": "yellow",
80
+ "description": f"Diperkirakan anda butuh ~{required_sks_per_sem:.1f} SKS yang perlu dipenuhi untuk semester-semester selanjutnya. Kapasitas SKS Anda ({student_capacity}) Sudah cukup mendukung untuk mengejar target ini.",
81
+ "stats": stats
82
+ }
83
+
84
+ else:
85
+ # KASUS: Terhambat IPK
86
+ return {
87
+ "status": "🟠 Rawan Terlambat",
88
+ "color": "orange",
89
+ "description": f"Hati-hati! Anda butuh {required_sks_per_sem:.1f} SKS tiap semester agar lulus tepat waktu, tapi IPK saat ini membatasi jatah cuma {student_capacity} SKS.",
90
+ "stats": stats
91
+ }
92
+
93
+ def predict_graduation_status(
94
+ current_semester: int,
95
+ total_sks_passed: int,
96
+ last_gpa: float,
97
+ graph_G: nx.DiGraph = None, # Optional: Graph object (pass by reference)
98
+ passed_courses: List[str] = None # Optional: List kode MK lulus
99
+ ) -> Dict[str, Any]:
100
+ """
101
+ Fungsi utama: Menjalankan logika Naive, lalu melakukan Override jika
102
+ ditemukan masalah struktural pada graf (rantai prasyarat).
103
+ """
104
+
105
+ # 1. Jalankan Prediksi Naive (Matematis)
106
+ result = _predict_naive(current_semester, total_sks_passed, last_gpa)
107
+
108
+ # Jika data graf tidak lengkap atau status sudah Critical/Lulus, kembalikan hasil naive
109
+ if graph_G is None or passed_courses is None:
110
+ return result
111
+
112
+ if result["color"] == "red" or result["status"] == "Lulus":
113
+ return result
114
+
115
+ # 2. LOGIKA OVERRIDE: Cek Rantai Prasyarat (Critical Path)
116
+ try:
117
+ # A. Identifikasi MK yang BELUM lulus
118
+ all_courses = set(graph_G.nodes())
119
+ passed_set = set(passed_courses)
120
+ unpassed_courses = list(all_courses - passed_set)
121
+
122
+ if not unpassed_courses:
123
+ return result
124
+
125
+ # B. Buat Subgraph (Hanya berisi matkul sisa & relasinya)
126
+ subgraph_remaining = graph_G.subgraph(unpassed_courses)
127
+
128
+ # C. Hitung Longest Path (Rantai Terpanjang) di subgraph
129
+ # dag_longest_path mengembalikan list node, misal ['A', 'B', 'C'] -> Panjang 3
130
+ if nx.is_directed_acyclic_graph(subgraph_remaining):
131
+ longest_chain_path = nx.dag_longest_path(subgraph_remaining)
132
+ min_semesters_needed_structural = len(longest_chain_path)
133
+
134
+ semesters_left = result["stats"]["semesters_left"]
135
+
136
+ # D. Bandingkan dengan Sisa Waktu
137
+ if min_semesters_needed_structural > semesters_left:
138
+ result["status"] = "🔴 Terlambat (Struktural)"
139
+ result["color"] = "red"
140
+ result["description"] = (
141
+ f"SKS tersisa cukup, namun terdeteksi rantai prasyarat panjang yang terdiri"
142
+ f"({min_semesters_needed_structural} Mata Kuliah Beruntun) dan tidak bisa diambil sekaligus dalam sisa waktu."
143
+ )
144
+ # Tambahkan info debug ke stats
145
+ result["stats"]["structural_issue"] = True
146
+ result["stats"]["longest_chain_len"] = min_semesters_needed_structural
147
+ result["stats"]["longest_chain_path"] = longest_chain_path
148
+
149
+ except Exception as e:
150
+ print(f"Graph Analysis Warning: {e}")
151
+ # Jika error graf, fallback ke hasil naive
152
+ return result
153
+
154
+ return result
main.py CHANGED
@@ -1,416 +1,447 @@
1
- # ======================================================================
2
- # --- main.py (Modular, dengan builder eksternal & format respons baru) ---
3
- # ======================================================================
4
-
5
- import os
6
- import json
7
- import networkx as nx
8
- import pandas as pd
9
- import skops.io as sio
10
- from fastapi import FastAPI, HTTPException
11
- from pydantic import BaseModel
12
- from typing import List, Dict, Any
13
-
14
- # Impor fungsi builder dari file baru
15
- from .explanation_builder import build_explanation_from_rules
16
-
17
-
18
- # ======================================================================
19
- # 1. Inisialisasi Aplikasi FastAPI
20
- # ======================================================================
21
- app = FastAPI(
22
- title="GCOMPRO",
23
- description="API Prediksi Risiko Akademik dan Rekomendasi Mata Kuliah.",
24
- version="1.6.0"
25
- )
26
-
27
- # ======================================================================
28
- # 2. Struktur Data Input/Output (Pydantic)
29
- # ======================================================================
30
-
31
- # --- Model untuk API Prediksi Risiko (App 1) ---
32
- class StudentFeatures(BaseModel):
33
- IPK_Terakhir: float
34
- IPS_Terakhir: float
35
- Total_SKS: int
36
- IPS_Tertinggi: float
37
- IPS_Terendah: float
38
- Rentang_IPS: float
39
- Jumlah_MK_Gagal: int
40
- Total_SKS_Gagal: int
41
- Tren_IPS_Slope: float
42
- Perubahan_Kinerja_Terakhir: float
43
- IPK_Ternormalisasi_SKS: float
44
- Profil_Tren: str
45
-
46
- class PredictionExplanation(BaseModel):
47
- """ Model Pydantic untuk struktur penjelasan baru """
48
- opening_line: str
49
- factors: List[str]
50
-
51
- class PredictionResponse(BaseModel):
52
- """ Model Pydantic untuk seluruh respons /predict/ """
53
- prediction: str
54
- probabilities: Dict[str, float]
55
- explanation: PredictionExplanation
56
- # --------------------------------------------------------
57
-
58
-
59
- # --- Model untuk API Rekomendasi MK (App 2) ---
60
- class RecommendationRequest(BaseModel):
61
- current_semester: int
62
- courses_passed: List[str]
63
- mk_pilihan_failed: List[str] = [] # <--- Berisi kode spesifik (misal: "AAK4ABB3")
64
-
65
- class PrerequisiteInfo(BaseModel):
66
- code: str
67
- name: str
68
-
69
- class CourseRecommendation(BaseModel):
70
- rank: int
71
- code: str
72
- name: str
73
- sks: int
74
- semester_plan: int
75
- reason: str
76
- is_tertinggal: bool
77
- priority_score: float
78
- prerequisites: List[PrerequisiteInfo]
79
-
80
- # ======================================================================
81
- # 3. Variabel Global & Pemuatan Model/Data
82
- # ======================================================================
83
-
84
- # <--- [UPDATE] DATABASE HARDCODE MATA KULIAH PILIHAN
85
- # Ini digunakan untuk lookup nama resmi saat user mengirim kode MK Pilihan yang gagal
86
- ELECTIVE_COURSES_DB = {
87
- "AAK4ABB3": {"name": "New Generation Network", "sks": 3},
88
- "AAK4BBB3": {"name": "Software Defined Network", "sks": 3},
89
- "AAK4CBB3": {"name": "Rekayasa Jaringan", "sks": 3},
90
- "AAK4DBB3": {"name": "Aplikasi Cyber Security", "sks": 3},
91
- "AAK4EBB3": {"name": "Manajemen Telekomunikasi dan Transformasi Digital", "sks": 3},
92
- "AAK4FBB3": {"name": "Adaptive Network", "sks": 3},
93
- "AAK4GBB3": {"name": "Cloud Computing", "sks": 3},
94
- "AAK4HBB3": {"name": "Koding dan Kompresi", "sks": 3},
95
- "AAK4IBB3": {"name": "Steganografi dan Watermarking", "sks": 3},
96
- "AAK4JBB3": {"name": "Mobile Application", "sks": 3},
97
- "AAK4KBB3": {"name": "Speech Signal Processing", "sks": 3},
98
- "AAK4LBB3": {"name": "Komunikasi Akses Wireless", "sks": 3},
99
- "AAK4MBB3": {"name": "Wireless Optical Communication", "sks": 3},
100
- "AAK4NBB3": {"name": "Broadband Optical Network", "sks": 3},
101
- "AAK4OBB3": {"name": "Sistem Komunikasi Satelit", "sks": 3},
102
- "AAK4PBB3": {"name": "Rekayasa Radio", "sks": 3},
103
- "AAK4QBB3": {"name": "Radar, Navigasi dan Remote Sensing", "sks": 3},
104
- "AAK4RBB3": {"name": "5G and Beyond", "sks": 3},
105
- "AAK4SBB3": {"name": "Software Defined Radio", "sks": 3},
106
- "AAK4TBB3": {"name": "Robotic Process Automation", "sks": 3},
107
- "AAK4UBB3": {"name": "Rekayasa Frekuensi Radio dalam Komunikasi Selular", "sks": 3},
108
- "AAK4VBB3": {"name": "Teknologi Radio Access Network (RAN)", "sks": 3},
109
- "AAK4WBB3": {"name": "Internet of Things: Protokol, Platform, dan AI", "sks": 3},
110
- "AAK4XBB3": {"name": "Jaringan Core Telekomunikasi", "sks": 3},
111
- "AAK4YBB3": {"name": "Ethical Hacking", "sks": 3},
112
- "AAK4ZBB3": {"name": "Keamanan Komunikasi Data", "sks": 3},
113
- "AAK47BB3": {"name": "Rekayasa Penyiaran Digital", "sks": 3}
114
- }
115
-
116
- # --- Variabel Global untuk API Prediksi Risiko (App 1) ---
117
- ml_model = None
118
- MODEL_FEATURES = [
119
- 'IPK_Terakhir', 'IPS_Terakhir', 'Total_SKS', 'IPS_Tertinggi',
120
- 'IPS_Terendah', 'Rentang_IPS', 'Jumlah_MK_Gagal', 'Total_SKS_Gagal',
121
- 'Tren_IPS_Slope', 'Perubahan_Kinerja_Terakhir',
122
- 'IPK_Ternormalisasi_SKS', 'Tren_Menaik', 'Tren_Menurun', 'Tren_Stabil'
123
- ]
124
-
125
- # --- Variabel Global untuk API Rekomendasi MK (App 2) ---
126
- G = nx.DiGraph()
127
- course_details_map = {}
128
- prereq_map = {}
129
- out_degree_map = {}
130
-
131
- # --- Fungsi Pemuatan (dipanggil saat startup) ---
132
-
133
- def load_ml_model():
134
- """Memuat model ML dari file .skops"""
135
- global ml_model
136
- MODEL_PATH = os.path.join(os.path.dirname(__file__), "model_risiko_akademik.skops")
137
- print(f"Mencoba memuat model ML dari: {MODEL_PATH}")
138
- try:
139
- trusted_types = [
140
- "numpy.ndarray", "numpy.core.multiarray.scalar",
141
- "sklearn.tree._classes.DecisionTreeClassifier", "_codecs.encode",
142
- "joblib.numpy_pickle.NumpyArrayWrapper", "numpy.core.multiarray._reconstruct",
143
- "numpy.dtype", "sklearn.tree._tree.Tree"
144
- ]
145
- ml_model = sio.load(MODEL_PATH, trusted=trusted_types)
146
- print("Model ML berhasil dimuat.")
147
- except Exception as e:
148
- print(f"ERROR: Gagal memuat model ML dari {MODEL_PATH}: {e}")
149
-
150
- def load_graph_data():
151
- """Memuat dan memproses data graf kurikulum dari JSON"""
152
- global G, course_details_map, prereq_map, out_degree_map
153
- JSON_PATH = os.path.join(os.path.dirname(__file__), "OK_matkul_graph.json")
154
- print(f"Mencoba memuat data graf dari: {JSON_PATH}")
155
-
156
- prereq_edge_count = 0
157
-
158
- try:
159
- with open(JSON_PATH, "r") as f:
160
- data = json.load(f)
161
-
162
- for node in data["nodes"]:
163
- course_details_map[node["code"]] = node
164
- G.add_node(node["code"])
165
-
166
- for edge in data["edges"]:
167
- if edge["type"] == "prereq":
168
- prereq_edge_count += 1
169
- G.add_edge(edge["from"], edge["to"])
170
- if edge["to"] not in prereq_map:
171
- prereq_map[edge["to"]] = []
172
- prereq_map[edge["to"]].append(edge["from"])
173
-
174
- for node_code in G.nodes():
175
- out_degree_map[node_code] = G.out_degree(node_code)
176
-
177
- print(f"Data graf berhasil dimuat.")
178
- print(f" - Total Relasi Prasyarat (Edges): {prereq_edge_count}")
179
- print(f" - (Info) MK unik dgn Prasyarat: {len(prereq_map)}")
180
-
181
- except FileNotFoundError:
182
- print(f"ERROR: {JSON_PATH} tidak ditemukan!")
183
- except Exception as e:
184
- print(f"Error saat memuat graf: {e}")
185
-
186
- @app.on_event("startup")
187
- def on_startup():
188
- load_ml_model()
189
- load_graph_data()
190
-
191
- # ======================================================================
192
- # 4. Helper Function (Untuk API Rekomendasi)
193
- # ======================================================================
194
-
195
- def get_recommendations_logic(current_semester: int, courses_passed_list: List[str], mk_pilihan_failed_list: List[str]) -> List[Dict[str, Any]]:
196
- """
197
- Logika rekomendasi dengan injeksi data MK Pilihan dari Database Hardcode.
198
- """
199
- passed_set = set(courses_passed_list)
200
- all_courses_set = set(course_details_map.keys())
201
- not_passed_courses = all_courses_set - passed_set
202
-
203
- # Tahap 1: Kumpulkan semua kandidat valid (Regular & Slot Pilihan)
204
- raw_candidates = []
205
-
206
- for course_code in not_passed_courses:
207
- prereqs = prereq_map.get(course_code, [])
208
- if all(p_code in passed_set for p_code in prereqs):
209
- details = course_details_map.get(course_code)
210
- if not details: continue
211
-
212
- out_degree = out_degree_map.get(course_code, 0)
213
- semester = details.get("semester_plan", 1)
214
-
215
- # Hitung Base Score
216
- priority_score = (out_degree / semester) if semester > 0 else 0
217
-
218
- candidate_data = details.copy()
219
- candidate_data["priority_score"] = priority_score
220
- candidate_data["is_retake_elective"] = False
221
-
222
- raw_candidates.append(candidate_data)
223
-
224
- # Tahap 2: Pisahkan Slot MK Pilihan vs MK Wajib
225
- elective_slots = []
226
- regular_candidates = []
227
-
228
- for cand in raw_candidates:
229
- # Deteksi slot template berdasarkan prefix kode
230
- if cand["code"].startswith("MK_PILIHAN"):
231
- elective_slots.append(cand)
232
- else:
233
- regular_candidates.append(cand)
234
-
235
- # Urutkan slot pilihan agar mengisi dari semester terkecil (misal sem 7 dulu)
236
- elective_slots.sort(key=lambda x: x["semester_plan"])
237
-
238
- # <--- [UPDATE] Tahap 3: Suntikkan MK Pilihan Gagal + Lookup Database
239
- processed_electives = []
240
- failed_idx = 0
241
-
242
- # Prioritaskan mengisi slot dengan MK yang gagal
243
- while failed_idx < len(mk_pilihan_failed_list) and len(elective_slots) > 0:
244
- slot = elective_slots.pop(0) # Ambil slot template (misal MK_PILIHAN1)
245
- failed_code = mk_pilihan_failed_list[failed_idx]
246
-
247
- # <--- [UPDATE] Lookup detail mata kuliah dari ELECTIVE_COURSES_DB
248
- if failed_code in ELECTIVE_COURSES_DB:
249
- real_name = ELECTIVE_COURSES_DB[failed_code]["name"]
250
- real_sks = ELECTIVE_COURSES_DB[failed_code]["sks"]
251
- else:
252
- # Fallback jika kode tidak ada di DB (safety net)
253
- real_name = "Mata Kuliah Pilihan (Unknown)"
254
- real_sks = 3
255
-
256
- # Modifikasi slot menjadi MK spesifik dengan data asli
257
- slot["code"] = failed_code
258
- slot["name"] = f"{real_name} (Mengulang)" # Ubah nama jadi nama asli
259
- slot["sks"] = real_sks
260
- slot["priority_score"] += 1.0 # Boost score
261
- slot["is_retake_elective"] = True
262
-
263
- processed_electives.append(slot)
264
- failed_idx += 1
265
-
266
- # Masukkan sisa slot pilihan (yang tidak di-override) kembali ke list
267
- processed_electives.extend(elective_slots)
268
-
269
- # Gabungkan kembali semua kandidat
270
- final_pool = regular_candidates + processed_electives
271
-
272
- # Tahap 4: Sorting Akhir (Score tinggi -> Semester kecil)
273
- final_ranked_list = sorted(
274
- final_pool,
275
- key=lambda x: (-x["priority_score"], x["semester_plan"])
276
- )
277
-
278
- return final_ranked_list
279
-
280
- # ======================================================================
281
- # 5. Endpoints API
282
- # ======================================================================
283
-
284
- @app.get("/")
285
- def read_root():
286
- return {
287
- "message": "Selamat Datang di API Layanan Akademik Mahasiswa",
288
- "status": "ready",
289
- "endpoints": ["/predict/", "/recommend/"]
290
- }
291
-
292
- @app.post("/predict/", response_model=PredictionResponse)
293
- def predict_risk(student_data: StudentFeatures):
294
- if ml_model is None:
295
- raise HTTPException(status_code=503, detail="Model ML belum siap. Silakan coba lagi nanti.")
296
-
297
- data = student_data.dict()
298
- input_df = pd.DataFrame([data])
299
- input_encoded = pd.get_dummies(input_df, columns=['Profil_Tren'], prefix='Tren')
300
-
301
- input_encoded = input_encoded.reindex(columns=MODEL_FEATURES, fill_value=False)
302
-
303
- try:
304
- prediction_val = ml_model.predict(input_encoded)[0]
305
- prediction_proba = ml_model.predict_proba(input_encoded)
306
- classes = ml_model.classes_
307
- probabilities = dict(zip(classes, prediction_proba[0]))
308
-
309
- explanation_obj = {}
310
-
311
- if hasattr(ml_model, 'tree_'):
312
- try:
313
- tree = ml_model.tree_
314
- feature_names = MODEL_FEATURES
315
-
316
- path = ml_model.decision_path(input_encoded)
317
- node_indices = path.indices[path.indptr[0]:path.indptr[1]]
318
-
319
- structured_rules = []
320
-
321
- for node_id in node_indices[:-1]:
322
- feature_index = tree.feature[node_id]
323
- feature_name = feature_names[feature_index]
324
- threshold = tree.threshold[node_id]
325
- sample_value = input_encoded.iloc[0, feature_index]
326
-
327
- condition_str = "rendah" if sample_value <= threshold else "tinggi"
328
-
329
- structured_rules.append({
330
- "feature": feature_name,
331
- "condition": condition_str,
332
- "threshold": threshold,
333
- "value": sample_value
334
- })
335
-
336
- explanation_obj = build_explanation_from_rules(structured_rules, prediction_val)
337
-
338
- except Exception as e:
339
- explanation_obj = {
340
- "opening_line": f"Gagal membuat penjelasan: {str(e)}",
341
- "factors": []
342
- }
343
- else:
344
- explanation_obj = {
345
- "opening_line": "Penjelasan (decision path) tidak tersedia untuk tipe model ini.",
346
- "factors": []
347
- }
348
-
349
- return PredictionResponse(
350
- prediction=prediction_val,
351
- probabilities=probabilities,
352
- explanation=explanation_obj
353
- )
354
-
355
- except Exception as e:
356
- raise HTTPException(status_code=500, detail=f"Terjadi kesalahan saat prediksi: {e}")
357
-
358
- # --- Endpoint dari App 2 (Rekomendasi) ---
359
- @app.post("/recommend/", response_model=List[CourseRecommendation])
360
- async def recommend_courses(request: RecommendationRequest):
361
- if not course_details_map:
362
- raise HTTPException(status_code=503, detail="Data kurikulum belum siap. Silakan coba lagi nanti.")
363
-
364
- ranked_candidates = get_recommendations_logic(
365
- request.current_semester,
366
- request.courses_passed,
367
- request.mk_pilihan_failed
368
- )
369
-
370
- top_3_candidates = ranked_candidates[:3]
371
-
372
- response_output = []
373
- for i, course in enumerate(top_3_candidates):
374
- rank = i + 1
375
-
376
- is_tertinggal_status = False
377
- reason = "Rekomendasi semester ini"
378
-
379
- if course.get("is_retake_elective"):
380
- reason = "Wajib Mengulang (MK Pilihan Gagal)"
381
- is_tertinggal_status = True
382
- elif course["semester_plan"] < request.current_semester:
383
- reason = f"Mata kuliah tertinggal (Semester {course['semester_plan']})"
384
- is_tertinggal_status = True
385
- elif course["semester_plan"] > request.current_semester:
386
- reason = f"Akselerasi (Semester {course['semester_plan']})"
387
-
388
- prereq_codes = prereq_map.get(course["code"], [])
389
- # Jika kode MK Pilihan diganti (misal AAK4ABB3), prereq_map.get("AAK4ABB3") akan None/Empty
390
- # Ini benar karena MK Pilihan umumnya tidak punya prereq di graf ini (hanya placeholder)
391
-
392
- prereq_details_list = []
393
- for p_code in prereq_codes:
394
- if p_code in course_details_map:
395
- prereq_details_list.append(
396
- PrerequisiteInfo(
397
- code=p_code,
398
- name=course_details_map[p_code]["name"]
399
- )
400
- )
401
-
402
- response_output.append(
403
- CourseRecommendation(
404
- rank=rank,
405
- code=course["code"],
406
- name=course["name"],
407
- sks=course["sks"],
408
- semester_plan=course["semester_plan"],
409
- reason=reason,
410
- is_tertinggal=is_tertinggal_status,
411
- priority_score=course["priority_score"],
412
- prerequisites=prereq_details_list
413
- )
414
- )
415
-
416
- return response_output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================================================================
2
+ # --- main.py (FULL VERSION 1.7.0) ---
3
+ # ======================================================================
4
+
5
+ import os
6
+ import json
7
+ import networkx as nx
8
+ import pandas as pd
9
+ import skops.io as sio
10
+ from fastapi import FastAPI, HTTPException
11
+ from pydantic import BaseModel
12
+ from typing import List, Dict, Any
13
+
14
+ # --- IMPOR MODUL LOKAL ---
15
+ # Pastikan file explanation_builder.py dan graduation_logic.py ada di folder yang sama
16
+ from .explanation_builder import build_full_response
17
+ from .graduation_logic import predict_graduation_status
18
+
19
+ # ======================================================================
20
+ # 1. Inisialisasi Aplikasi FastAPI
21
+ # ======================================================================
22
+ app = FastAPI(
23
+ title="GCOMPRO API Service",
24
+ description="API untuk Prediksi Risiko Akademik, Rekomendasi Mata Kuliah, dan Cek Kelulusan Tepat Waktu.",
25
+ version="1.7.0"
26
+ )
27
+
28
+ # ======================================================================
29
+ # 2. Struktur Data Input/Output (Pydantic Models)
30
+ # ======================================================================
31
+
32
+ # --- [APP 1] Model untuk Prediksi Risiko ---
33
+ class StudentFeatures(BaseModel):
34
+ IPK_Terakhir: float
35
+ IPS_Terakhir: float
36
+ Total_SKS: int
37
+ IPS_Tertinggi: float
38
+ IPS_Terendah: float
39
+ Rentang_IPS: float
40
+ Jumlah_MK_Gagal: int
41
+ Total_SKS_Gagal: int
42
+ Tren_IPS_Slope: float
43
+ Perubahan_Kinerja_Terakhir: float
44
+ IPK_Ternormalisasi_SKS: float
45
+ Profil_Tren: str
46
+
47
+ class PredictionExplanation(BaseModel):
48
+ opening_line: str
49
+ factors: List[str]
50
+ recommendation: str
51
+
52
+ class PredictionResponse(BaseModel):
53
+ prediction: str
54
+ probabilities: Dict[str, float]
55
+ explanation: PredictionExplanation
56
+
57
+ # --- [APP 2] Model untuk Rekomendasi MK ---
58
+ class RecommendationRequest(BaseModel):
59
+ current_semester: int
60
+ courses_passed: List[str]
61
+ mk_pilihan_failed: List[str] = []
62
+
63
+ class PrerequisiteInfo(BaseModel):
64
+ code: str
65
+ name: str
66
+
67
+ class CourseRecommendation(BaseModel):
68
+ rank: int
69
+ code: str
70
+ name: str
71
+ sks: int
72
+ semester_plan: int
73
+ reason: str
74
+ is_tertinggal: bool
75
+ priority_score: float
76
+ prerequisites: List[PrerequisiteInfo]
77
+
78
+ # --- [APP 3] Model untuk Prediksi Kelulusan (LTW) ---
79
+ class GraduationCheckRequest(BaseModel):
80
+ current_semester: int
81
+ total_sks_passed: int
82
+ ipk_last_semester: float
83
+ courses_passed: List[str] = [] # Optional, tapi disarankan diisi untuk validasi graf
84
+
85
+ class GraduationCheckResponse(BaseModel):
86
+ status: str
87
+ color: str
88
+ description: str
89
+ stats: Dict[str, Any]
90
+
91
+ # ======================================================================
92
+ # 3. Variabel Global & Database Hardcode
93
+ # ======================================================================
94
+
95
+ # Database Mata Kuliah Pilihan (Hardcoded untuk fallback nama/sks)
96
+ ELECTIVE_COURSES_DB = {
97
+ "AAK4ABB3": {"name": "New Generation Network", "sks": 3},
98
+ "AAK4BBB3": {"name": "Software Defined Network", "sks": 3},
99
+ "AAK4CBB3": {"name": "Rekayasa Jaringan", "sks": 3},
100
+ "AAK4DBB3": {"name": "Aplikasi Cyber Security", "sks": 3},
101
+ "AAK4EBB3": {"name": "Manajemen Telekomunikasi dan Transformasi Digital", "sks": 3},
102
+ "AAK4FBB3": {"name": "Adaptive Network", "sks": 3},
103
+ "AAK4GBB3": {"name": "Cloud Computing", "sks": 3},
104
+ "AAK4HBB3": {"name": "Koding dan Kompresi", "sks": 3},
105
+ "AAK4IBB3": {"name": "Steganografi dan Watermarking", "sks": 3},
106
+ "AAK4JBB3": {"name": "Mobile Application", "sks": 3},
107
+ "AAK4KBB3": {"name": "Speech Signal Processing", "sks": 3},
108
+ "AAK4LBB3": {"name": "Komunikasi Akses Wireless", "sks": 3},
109
+ "AAK4MBB3": {"name": "Wireless Optical Communication", "sks": 3},
110
+ "AAK4NBB3": {"name": "Broadband Optical Network", "sks": 3},
111
+ "AAK4OBB3": {"name": "Sistem Komunikasi Satelit", "sks": 3},
112
+ "AAK4PBB3": {"name": "Rekayasa Radio", "sks": 3},
113
+ "AAK4QBB3": {"name": "Radar, Navigasi dan Remote Sensing", "sks": 3},
114
+ "AAK4RBB3": {"name": "5G and Beyond", "sks": 3},
115
+ "AAK4SBB3": {"name": "Software Defined Radio", "sks": 3},
116
+ "AAK4TBB3": {"name": "Robotic Process Automation", "sks": 3},
117
+ "AAK4UBB3": {"name": "Rekayasa Frekuensi Radio dalam Komunikasi Selular", "sks": 3},
118
+ "AAK4VBB3": {"name": "Teknologi Radio Access Network (RAN)", "sks": 3},
119
+ "AAK4WBB3": {"name": "Internet of Things: Protokol, Platform, dan AI", "sks": 3},
120
+ "AAK4XBB3": {"name": "Jaringan Core Telekomunikasi", "sks": 3},
121
+ "AAK4YBB3": {"name": "Ethical Hacking", "sks": 3},
122
+ "AAK4ZBB3": {"name": "Keamanan Komunikasi Data", "sks": 3},
123
+ "AAK47BB3": {"name": "Rekayasa Penyiaran Digital", "sks": 3}
124
+ }
125
+
126
+ # Variabel Global ML
127
+ ml_model = None
128
+ MODEL_FEATURES = [
129
+ 'IPK_Terakhir', 'IPS_Terakhir', 'Total_SKS', 'IPS_Tertinggi',
130
+ 'IPS_Terendah', 'Rentang_IPS', 'Jumlah_MK_Gagal', 'Total_SKS_Gagal',
131
+ 'Tren_IPS_Slope', 'Perubahan_Kinerja_Terakhir',
132
+ 'IPK_Ternormalisasi_SKS', 'Tren_Menaik', 'Tren_Menurun', 'Tren_Stabil'
133
+ ]
134
+
135
+ # Variabel Global Graph
136
+ G = nx.DiGraph()
137
+ course_details_map = {}
138
+ prereq_map = {}
139
+ out_degree_map = {}
140
+
141
+ # --- Fungsi Pemuatan Data ---
142
+
143
+ def load_ml_model():
144
+ """Memuat model ML dari file .skops"""
145
+ global ml_model
146
+ MODEL_PATH = os.path.join(os.path.dirname(__file__), "model_risiko_akademik.skops")
147
+ print(f"Mencoba memuat model ML dari: {MODEL_PATH}")
148
+ try:
149
+ trusted_types = [
150
+ "numpy.ndarray", "numpy.core.multiarray.scalar",
151
+ "sklearn.tree._classes.DecisionTreeClassifier", "_codecs.encode",
152
+ "joblib.numpy_pickle.NumpyArrayWrapper", "numpy.core.multiarray._reconstruct",
153
+ "numpy.dtype", "sklearn.tree._tree.Tree"
154
+ ]
155
+ ml_model = sio.load(MODEL_PATH, trusted=trusted_types)
156
+ print("Model ML berhasil dimuat.")
157
+ except Exception as e:
158
+ print(f"ERROR: Gagal memuat model ML dari {MODEL_PATH}: {e}")
159
+
160
+ def load_graph_data():
161
+ """Memuat dan memproses data graf kurikulum dari JSON"""
162
+ global G, course_details_map, prereq_map, out_degree_map
163
+ JSON_PATH = os.path.join(os.path.dirname(__file__), "OK_matkul_graph.json")
164
+ print(f"Mencoba memuat data graf dari: {JSON_PATH}")
165
+
166
+ prereq_edge_count = 0
167
+
168
+ try:
169
+ with open(JSON_PATH, "r") as f:
170
+ data = json.load(f)
171
+
172
+ for node in data["nodes"]:
173
+ course_details_map[node["code"]] = node
174
+ G.add_node(node["code"])
175
+
176
+ for edge in data["edges"]:
177
+ if edge["type"] == "prereq":
178
+ prereq_edge_count += 1
179
+ G.add_edge(edge["from"], edge["to"])
180
+ if edge["to"] not in prereq_map:
181
+ prereq_map[edge["to"]] = []
182
+ prereq_map[edge["to"]].append(edge["from"])
183
+
184
+ for node_code in G.nodes():
185
+ out_degree_map[node_code] = G.out_degree(node_code)
186
+
187
+ print(f"Data graf berhasil dimuat. Total Edges: {prereq_edge_count}")
188
+
189
+ except FileNotFoundError:
190
+ print(f"ERROR: {JSON_PATH} tidak ditemukan!")
191
+ except Exception as e:
192
+ print(f"Error saat memuat graf: {e}")
193
+
194
+ @app.on_event("startup")
195
+ def on_startup():
196
+ load_ml_model()
197
+ load_graph_data()
198
+
199
+ # ======================================================================
200
+ # 4. Helper Functions (Business Logic)
201
+ # ======================================================================
202
+
203
+ def get_recommendations_logic(current_semester: int, courses_passed_list: List[str], mk_pilihan_failed_list: List[str]) -> List[Dict[str, Any]]:
204
+ """Logika utama untuk rekomendasi mata kuliah."""
205
+ passed_set = set(courses_passed_list)
206
+ all_courses_set = set(course_details_map.keys())
207
+ not_passed_courses = all_courses_set - passed_set
208
+
209
+ raw_candidates = []
210
+
211
+ for course_code in not_passed_courses:
212
+ prereqs = prereq_map.get(course_code, [])
213
+ if all(p_code in passed_set for p_code in prereqs):
214
+ details = course_details_map.get(course_code)
215
+ if not details: continue
216
+
217
+ out_degree = out_degree_map.get(course_code, 0)
218
+ semester = details.get("semester_plan", 1)
219
+
220
+ priority_score = (out_degree / semester) if semester > 0 else 0
221
+
222
+ candidate_data = details.copy()
223
+ candidate_data["priority_score"] = priority_score
224
+ candidate_data["is_retake_elective"] = False
225
+
226
+ raw_candidates.append(candidate_data)
227
+
228
+ elective_slots = []
229
+ regular_candidates = []
230
+
231
+ for cand in raw_candidates:
232
+ if cand["code"].startswith("MK_PILIHAN"):
233
+ elective_slots.append(cand)
234
+ else:
235
+ regular_candidates.append(cand)
236
+
237
+ elective_slots.sort(key=lambda x: x["semester_plan"])
238
+
239
+ processed_electives = []
240
+ failed_idx = 0
241
+
242
+ while failed_idx < len(mk_pilihan_failed_list) and len(elective_slots) > 0:
243
+ slot = elective_slots.pop(0)
244
+ failed_code = mk_pilihan_failed_list[failed_idx]
245
+
246
+ if failed_code in ELECTIVE_COURSES_DB:
247
+ real_name = ELECTIVE_COURSES_DB[failed_code]["name"]
248
+ real_sks = ELECTIVE_COURSES_DB[failed_code]["sks"]
249
+ else:
250
+ real_name = "Mata Kuliah Pilihan (Unknown)"
251
+ real_sks = 3
252
+
253
+ slot["code"] = failed_code
254
+ slot["name"] = f"{real_name} (Mengulang)"
255
+ slot["sks"] = real_sks
256
+ slot["priority_score"] += 1.0
257
+ slot["is_retake_elective"] = True
258
+
259
+ processed_electives.append(slot)
260
+ failed_idx += 1
261
+
262
+ processed_electives.extend(elective_slots)
263
+ final_pool = regular_candidates + processed_electives
264
+
265
+ final_ranked_list = sorted(
266
+ final_pool,
267
+ key=lambda x: (-x["priority_score"], x["semester_plan"])
268
+ )
269
+
270
+ return final_ranked_list
271
+
272
+ def apply_prediction_overrides(original_prediction: str, student_data: StudentFeatures) -> str:
273
+ """Guardrails: Menerapkan aturan bisnis manual untuk override prediksi ML."""
274
+ new_prediction = original_prediction
275
+
276
+ # Aturan 1: FALSE NEGATIVE (Model optimis, padahal IPK rendah)
277
+ if (student_data.IPK_Terakhir < 2.40 or student_data.Jumlah_MK_Gagal >= 3) and \
278
+ (original_prediction in ["Aman", "Resiko Rendah"]):
279
+ new_prediction = "Resiko Sedang"
280
+
281
+ # Aturan 1B: Varian Parah
282
+ if (student_data.IPK_Terakhir < 2.10 or student_data.Jumlah_MK_Gagal >= 5):
283
+ new_prediction = "Resiko Tinggi"
284
+
285
+ # Aturan 2: FALSE POSITIVE (Model pesimis, padahal performa naik)
286
+ if (student_data.IPK_Terakhir > 2.75 and
287
+ student_data.Jumlah_MK_Gagal == 0 and
288
+ student_data.Tren_IPS_Slope > 0.05) and \
289
+ (original_prediction == "Resiko Tinggi" or original_prediction == "Resiko Sedang"):
290
+ new_prediction = "Resiko Rendah"
291
+
292
+ # Aturan 2B: Varian Sangat Baik
293
+ if (student_data.IPK_Terakhir > 3.25 and student_data.Jumlah_MK_Gagal == 0) and \
294
+ (original_prediction != "Aman"):
295
+ new_prediction = "Aman"
296
+
297
+ return new_prediction
298
+
299
+ # ======================================================================
300
+ # 5. Endpoints API
301
+ # ======================================================================
302
+
303
+ @app.get("/")
304
+ def read_root():
305
+ return {
306
+ "message": "Selamat Datang di API Layanan Akademik Mahasiswa",
307
+ "status": "ready",
308
+ "endpoints": ["/predict/", "/recommend/", "/predict-graduation/"]
309
+ }
310
+
311
+ # --- ENDPOINT 1: PREDIKSI RISIKO AKADEMIK ---
312
+ @app.post("/predict/", response_model=PredictionResponse)
313
+ def predict_risk(student_data: StudentFeatures):
314
+ if ml_model is None:
315
+ raise HTTPException(status_code=503, detail="Model ML belum siap. Silakan coba lagi nanti.")
316
+
317
+ data = student_data.dict()
318
+ input_df = pd.DataFrame([data])
319
+ input_encoded = pd.get_dummies(input_df, columns=['Profil_Tren'], prefix='Tren')
320
+ input_encoded = input_encoded.reindex(columns=MODEL_FEATURES, fill_value=False)
321
+
322
+ try:
323
+ # 1. Prediksi ML Dasar
324
+ prediction_val = ml_model.predict(input_encoded)[0]
325
+ prediction_proba = ml_model.predict_proba(input_encoded)
326
+ classes = ml_model.classes_
327
+ probabilities = dict(zip(classes, prediction_proba[0]))
328
+
329
+ structured_rules = []
330
+
331
+ # 2. Ekstraksi Decision Path
332
+ if hasattr(ml_model, 'tree_'):
333
+ try:
334
+ tree = ml_model.tree_
335
+ feature_names = MODEL_FEATURES
336
+ path = ml_model.decision_path(input_encoded)
337
+ node_indices = path.indices[path.indptr[0]:path.indptr[1]]
338
+
339
+ for node_id in node_indices[:-1]:
340
+ feature_index = tree.feature[node_id]
341
+ feature_name = feature_names[feature_index]
342
+ threshold = tree.threshold[node_id]
343
+ sample_value = input_encoded.iloc[0, feature_index]
344
+
345
+ condition_str = "rendah" if sample_value <= threshold else "tinggi"
346
+ structured_rules.append({
347
+ "feature": feature_name,
348
+ "condition": condition_str,
349
+ "threshold": threshold,
350
+ "value": sample_value
351
+ })
352
+ except Exception:
353
+ pass # Lanjut tanpa path jika error
354
+
355
+ # 3. Terapkan Override (Guardrails)
356
+ final_prediction = apply_prediction_overrides(prediction_val, student_data)
357
+
358
+ # 4. Sesuaikan Probabilitas dengan Override
359
+ final_probabilities = {key: 0.0 for key in probabilities.keys()}
360
+ if final_prediction in final_probabilities:
361
+ final_probabilities[final_prediction] = 1.0
362
+ else:
363
+ first_key = next(iter(final_probabilities))
364
+ final_probabilities[first_key] = 1.0
365
+
366
+ # 5. Bangun Penjelasan Teks
367
+ explanation_obj = build_full_response(structured_rules, final_prediction)
368
+
369
+ return PredictionResponse(
370
+ prediction=final_prediction,
371
+ probabilities=final_probabilities,
372
+ explanation=explanation_obj
373
+ )
374
+
375
+ except Exception as e:
376
+ raise HTTPException(status_code=500, detail=f"Terjadi kesalahan saat prediksi: {e}")
377
+
378
+ # --- ENDPOINT 2: REKOMENDASI MATA KULIAH ---
379
+ @app.post("/recommend/", response_model=List[CourseRecommendation])
380
+ async def recommend_courses(request: RecommendationRequest):
381
+ if not course_details_map:
382
+ raise HTTPException(status_code=503, detail="Data kurikulum belum siap. Silakan coba lagi nanti.")
383
+
384
+ ranked_candidates = get_recommendations_logic(
385
+ request.current_semester,
386
+ request.courses_passed,
387
+ request.mk_pilihan_failed
388
+ )
389
+
390
+ top_3_candidates = ranked_candidates[:3]
391
+
392
+ response_output = []
393
+ for i, course in enumerate(top_3_candidates):
394
+ rank = i + 1
395
+
396
+ is_tertinggal_status = False
397
+ reason = "Rekomendasi semester ini"
398
+
399
+ if course.get("is_retake_elective"):
400
+ reason = "Wajib Mengulang (MK Pilihan Gagal)"
401
+ is_tertinggal_status = True
402
+ elif course["semester_plan"] < request.current_semester:
403
+ reason = f"Mata kuliah tertinggal (Semester {course['semester_plan']})"
404
+ is_tertinggal_status = True
405
+ elif course["semester_plan"] > request.current_semester:
406
+ reason = f"Akselerasi (Semester {course['semester_plan']})"
407
+
408
+ prereq_codes = prereq_map.get(course["code"], [])
409
+ prereq_details_list = []
410
+ for p_code in prereq_codes:
411
+ if p_code in course_details_map:
412
+ prereq_details_list.append(
413
+ PrerequisiteInfo(code=p_code, name=course_details_map[p_code]["name"])
414
+ )
415
+
416
+ response_output.append(
417
+ CourseRecommendation(
418
+ rank=rank,
419
+ code=course["code"],
420
+ name=course["name"],
421
+ sks=course["sks"],
422
+ semester_plan=course["semester_plan"],
423
+ reason=reason,
424
+ is_tertinggal=is_tertinggal_status,
425
+ priority_score=course["priority_score"],
426
+ prerequisites=prereq_details_list
427
+ )
428
+ )
429
+
430
+ return response_output
431
+
432
+ # --- ENDPOINT 3: PREDIKSI KELULUSAN TEPAT WAKTU (LTW) ---
433
+ @app.post("/predict-graduation/", response_model=GraduationCheckResponse)
434
+ def check_graduation_status(request: GraduationCheckRequest):
435
+ """
436
+ Endpoint untuk mengecek apakah mahasiswa masih on-track lulus di Semester 8
437
+ berdasarkan sisa SKS, kapasitas IPK, dan rantai prasyarat (Graf).
438
+ """
439
+ result = predict_graduation_status(
440
+ current_semester=request.current_semester,
441
+ total_sks_passed=request.total_sks_passed,
442
+ last_gpa=request.ipk_last_semester,
443
+ graph_G=G, # Pass Graf Global (Reference)
444
+ passed_courses=request.courses_passed # Pass data matkul user
445
+ )
446
+
447
+ return GraduationCheckResponse(**result)
recommendation_builder.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================================================================
2
+ # --- recommendation_builder.py (LOGIKA DIPERBAIKI) ---
3
+ # ======================================================================
4
+ import random
5
+ from typing import List, Dict, Any
6
+
7
+ # --- SENTIMENT CONFIG ---
8
+ SENTIMENT_MAP = {
9
+ "IPS_Terakhir": {"rendah": -1, "tinggi": 1},
10
+ "IPK_Terakhir": {"rendah": -1, "tinggi": 1},
11
+ "Jumlah_MK_Gagal": {"rendah": 1, "tinggi": -1},
12
+ "Total_SKS": {"rendah": -1, "tinggi": 1},
13
+ "Tren_IPS_Slope": {"rendah": -1, "tinggi": 1},
14
+ "Rentang_IPS": {"rendah": 1, "tinggi": -1},
15
+ "Total_SKS_Gagal": {"rendah": 1, "tinggi": -1},
16
+ }
17
+
18
+ # --- KLAUSA TEKS (FALLBACK) ---
19
+ # Ini digunakan jika tidak ada 'Sanity Check' khusus
20
+ CLAUSES_FALLBACK = {
21
+ "IPS_Terakhir": {
22
+ "rendah": "capaian IPS semester terakhir yang berada di bawah standar",
23
+ "tinggi": "capaian IPS semester terakhir yang cukup baik", # Dibuat netral
24
+ },
25
+ "IPK_Terakhir": {
26
+ "rendah": "nilai IPK kumulatif yang masih relatif rendah",
27
+ "tinggi": "rekam IPK kumulatif yang solid",
28
+ },
29
+ "Jumlah_MK_Gagal": {
30
+ "rendah": "rekam jejak mata kuliah yang cukup baik",
31
+ "tinggi": "adanya beban mata kuliah gagal",
32
+ },
33
+ "Total_SKS_Gagal": {
34
+ "rendah": "minimnya SKS yang terbuang",
35
+ "tinggi": "besarnya jumlah SKS yang harus diulang",
36
+ },
37
+ "Tren_IPS_Slope": {
38
+ "rendah": "tren performa yang menurun belakangan ini",
39
+ "tinggi": "tren peningkatan nilai yang konsisten",
40
+ },
41
+ "Total_SKS": {
42
+ "rendah": "jumlah SKS yang masih di bawah target progres studi",
43
+ "tinggi": "kemajuan pengambilan SKS yang sejalan dengan rencana studi",
44
+ },
45
+ "Rentang_IPS": {
46
+ "rendah": "konsistensi performa yang stabil",
47
+ "tinggi": "fluktuasi performa yang tidak stabil"
48
+ }
49
+ }
50
+
51
+ # --- KATA SAMBUNG ---
52
+ CONNECTORS = {
53
+ "same_bad": [". Masalah ini diperberat dengan ", ". Selain itu, terdeteksi juga ", ". Sayangnya, hal ini diikuti oleh "],
54
+ "same_good": [". Hal ini didukung pula oleh ", ". Ditambah lagi dengan ", ". Serta adanya "],
55
+ "contrast_bad_to_good": [". Namun kabar baiknya, ", ". Walaupun begitu, Anda memiliki ", ". Untungnya, hal ini diimbangi oleh "],
56
+ "contrast_good_to_bad": [". Namun sayangnya, ", ". Meskipun demikian, perlu diwaspadai adanya ", ". Akan tetapi, sistem mencatat "],
57
+ }
58
+
59
+ # --- TEMPLATE STATIS (FORMATTING DIHAPUS) ---
60
+ REKOMENDASI_BANK = {
61
+ "Resiko Tinggi": [
62
+ (
63
+ "🚨 Tindakan Mendesak Diperlukan. Berdasarkan analisis sistem, Anda berada di kategori Resiko Tinggi. "
64
+ "Segera lakukan evaluasi mendalam terhadap kebiasaan belajar, disiplin waktu, dan strategi akademik Anda. "
65
+ "Prioritaskan perbaikan pada mata kuliah dengan nilai rendah, manfaatkan bimbingan dosen, "
66
+ "dan pertimbangkan untuk mengurangi beban SKS sementara agar fokus pada peningkatan performa inti."
67
+ ),
68
+ (
69
+ "⚠️ Perhatian Serius. Performa akademik Anda menunjukkan tanda risiko tinggi. "
70
+ "Usahakan untuk memperbaiki IPK dan IPS dengan memperkuat dasar konsep, "
71
+ "bergabung dalam kelompok belajar, serta mencari mentor akademik. "
72
+ "Manajemen waktu dan pola belajar teratur akan sangat membantu dalam mengembalikan performa Anda."
73
+ )
74
+ ],
75
+ "Resiko Sedang": [
76
+ (
77
+ "⚠️ Waspada & Antisipasi. Anda berada di zona Resiko Sedang. "
78
+ "Hal ini menandakan performa Anda masih fluktuatif. "
79
+ "Pertahankan aspek yang sudah baik, namun segera identifikasi area yang masih lemah. "
80
+ "Disarankan untuk membuat jadwal belajar lebih terstruktur dan melakukan evaluasi kecil tiap minggu."
81
+ ),
82
+ (
83
+ "💡 Perlu Peningkatan. Kinerja akademik Anda stabil namun belum optimal. "
84
+ "Fokuslah pada konsistensi nilai dan hindari penurunan mendadak di semester berikutnya. "
85
+ "Coba tingkatkan interaksi dengan dosen dan teman sekelas untuk memperkuat pemahaman materi."
86
+ )
87
+ ],
88
+ "Resiko Rendah": [
89
+ (
90
+ "✅ Pertahankan Momentum. Anda berada di kategori Resiko Rendah. "
91
+ "Performa Anda sudah cukup baik dan konsisten. "
92
+ "Teruskan pola belajar yang efektif, namun jangan lengah terhadap materi yang sulit. "
93
+ "Pertimbangkan untuk mengambil tantangan baru seperti proyek penelitian atau lomba akademik."
94
+ ),
95
+ (
96
+ "📈 Progres Positif. Anda menunjukkan performa yang solid. "
97
+ "Gunakan kesempatan ini untuk memperkuat area yang masih lemah dan menjaga keseimbangan antara studi dan istirahat. "
98
+ "Tetap evaluasi hasil belajar secara berkala untuk memastikan kestabilan performa."
99
+ )
100
+ ],
101
+ "Aman": [
102
+ (
103
+ "🌟 Luar Biasa! Anda berada di kategori Aman. "
104
+ "Kinerja akademik Anda konsisten dan menunjukkan kedewasaan belajar yang tinggi. "
105
+ "Pertahankan strategi belajar yang sudah terbukti efektif, "
106
+ "dan jangan ragu berbagi pengalaman dengan rekan yang membutuhkan bantuan."
107
+ ),
108
+ (
109
+ "🏆 Prestasi Stabil. Sistem mendeteksi profil akademik Anda sangat kuat. "
110
+ "Anda dapat mulai mengeksplorasi kegiatan tambahan seperti magang, penelitian, atau lomba akademik "
111
+ "untuk memperluas wawasan dan pengalaman profesional."
112
+ )
113
+ ],
114
+ "default": (
115
+ "🔍 Evaluasi Umum. Hasil prediksi Anda menunjukkan area yang perlu diperhatikan. "
116
+ "Tetap jaga semangat belajar dan lakukan refleksi berkala terhadap hasil akademik Anda."
117
+ )
118
+ }
119
+
120
+ # --- [FUNGSI BARU] LOGIC SANITY CHECK ---
121
+ def _get_dynamic_clause(feature: str, condition: str, value: float) -> str:
122
+ """
123
+ Logika Cerdas: Menyesuaikan kata sifat berdasarkan NILAI ASLI,
124
+ bukan hanya label 'tinggi/rendah' dari decision tree.
125
+ """
126
+
127
+ # --- LOGIKA OVERRIDE (SANITY CHECK) ---
128
+
129
+ # 1. IPS Terakhir (Skala 0-4)
130
+ if feature == "IPS_Terakhir":
131
+ if condition == "tinggi": # DT bilang "tinggi"
132
+ if value < 2.5:
133
+ return "capaian IPS semester terakhir yang sedikit membaik namun masih rawan"
134
+ elif value < 3.0:
135
+ # Ini akan menangkap 2.62 dan 2.92
136
+ return "capaian IPS semester terakhir yang cukup aman"
137
+ else:
138
+ return "capaian IPS semester terakhir yang sangat memuaskan"
139
+ else: # DT bilang "rendah"
140
+ return "capaian IPS semester terakhir yang cukup rendah"
141
+
142
+ # 2. IPK Terakhir (Skala 0-4)
143
+ elif feature == "IPK_Terakhir":
144
+ if condition == "tinggi": # DT bilang "tinggi"
145
+ if value < 2.5:
146
+ # Ini akan menangkap 2.15
147
+ return "IPK kumulatif yang baru saja lolos ambang batas kritis"
148
+ elif value < 3.0:
149
+ # Ini akan menangkap 2.78
150
+ return "IPK kumulatif yang tergolong cukup baik"
151
+ else:
152
+ return "IPK kumulatif yang sangat solid"
153
+ else: # DT bilang "rendah"
154
+ if value < 2.0:
155
+ return "IPK kumulatif yang berada di zona bahaya"
156
+ else:
157
+ return "IPK kumulatif yang masih relatif rendah"
158
+
159
+ # 3. MK Gagal
160
+ elif feature == "Jumlah_MK_Gagal":
161
+ if condition == "rendah":
162
+ if value == 0:
163
+ return "rekam jejak mata kuliah yang bersih tanpa kegagalan"
164
+ else:
165
+ return f"jumlah mata kuliah gagal yang masih dalam batas wajar ({int(value)} MK)"
166
+ else: # DT bilang "tinggi"
167
+ if value < 3:
168
+ return f"adanya {int(value)} mata kuliah gagal yang perlu segera diulang"
169
+ else:
170
+ # Ini akan menangkap 4 Gagal
171
+ return f"adanya beban {int(value)} mata kuliah gagal yang menumpuk"
172
+
173
+ # 4. Tren
174
+ elif feature == "Tren_IPS_Slope":
175
+ if condition == "tinggi":
176
+ if value < 0.1: # Jika naiknya sedikit (kasus 0.066)
177
+ return "tren performa yang cukup membaik"
178
+ else:
179
+ return "tren peningkatan nilai yang konsisten"
180
+ else:
181
+ return "tren performa yang menurun belakangan ini"
182
+
183
+ # Jika tidak ada aturan khusus, gunakan Fallback
184
+ fallback = CLAUSES_FALLBACK.get(feature, {}).get(condition)
185
+ if fallback:
186
+ return fallback
187
+
188
+ # Jika tidak ada fallback, return string kosong
189
+ return ""
190
+
191
+
192
+ def generate_recommendation_paragraph(prediction_val: str, structured_rules: List[Dict[str, Any]]) -> str:
193
+ """
194
+ Menghasilkan rekomendasi personal: Template Statis + Jahitan Dinamis
195
+ """
196
+
197
+ # 1️⃣ Pilih template dasar (statis) dari REKOMENDASI_BANK
198
+ base_templates = REKOMENDASI_BANK.get(prediction_val)
199
+ if not base_templates:
200
+ base_templates = [REKOMENDASI_BANK["default"]]
201
+
202
+ if isinstance(base_templates, list):
203
+ base_text = random.choice(base_templates)
204
+ else:
205
+ base_text = base_templates # Handle 'default' yang bukan list
206
+
207
+ # 2️⃣ Buat bagian dinamis (berdasarkan fitur dominan)
208
+ active_clauses = []
209
+ features_seen = set()
210
+
211
+ for rule in reversed(structured_rules):
212
+ feature = rule["feature"]
213
+ if feature in features_seen: continue
214
+ features_seen.add(feature)
215
+
216
+ condition = rule["condition"]
217
+ value = rule["value"] # Ambil nilai asli
218
+
219
+ # [DIUBAH] Panggil helper baru, bukan CLAUSES.get()
220
+ text = _get_dynamic_clause(feature, condition, value)
221
+
222
+ sentiment = SENTIMENT_MAP.get(feature, {}).get(condition, 0)
223
+
224
+ if text: # Hanya tambahkan jika text tidak kosong
225
+ active_clauses.append({"text": text, "sentiment": sentiment})
226
+
227
+ if not active_clauses:
228
+ # Jika tidak ada fitur penting, kembalikan template statis saja
229
+ return base_text
230
+
231
+ # 3️⃣ Stitching: gabungkan klausa
232
+ dynamic_part = " Dalam penjelasan yang lebih spesifik, profil Anda dipengaruhi oleh "
233
+ current = active_clauses[0]
234
+ dynamic_part += current["text"]
235
+ last_sentiment = current["sentiment"]
236
+
237
+ for i in range(1, len(active_clauses)):
238
+ item = active_clauses[i]
239
+ current_sentiment = item["sentiment"]
240
+
241
+ if last_sentiment == -1 and current_sentiment == -1:
242
+ connector = random.choice(CONNECTORS["same_bad"])
243
+ elif last_sentiment == 1 and current_sentiment == 1:
244
+ connector = random.choice(CONNECTORS["same_good"])
245
+ elif last_sentiment == -1 and current_sentiment == 1:
246
+ connector = random.choice(CONNECTORS["contrast_bad_to_good"])
247
+ elif last_sentiment == 1 and current_sentiment == -1:
248
+ connector = random.choice(CONNECTORS["contrast_good_to_bad"])
249
+ else:
250
+ connector = ". Selanjutnya, perhatikan juga "
251
+
252
+ dynamic_part += connector + item["text"]
253
+ last_sentiment = current_sentiment
254
+
255
+ dynamic_part += "."
256
+
257
+ # 4️⃣ Gabungkan template + hasil stitching
258
+ # Format: [Paragraf Statis] + [Paragraf Dinamis]
259
+ # \n\n digunakan untuk membuat paragraf baru
260
+ final_text = f"{base_text}{dynamic_part}"
261
+ return final_text
requirements.txt CHANGED
@@ -1,8 +1,8 @@
1
- pandas
2
- ridgeplot
3
- fastapi
4
- uvicorn
5
- scikit-learn
6
- joblib
7
- skops
8
  networkx
 
1
+ pandas
2
+ ridgeplot
3
+ fastapi
4
+ uvicorn
5
+ scikit-learn
6
+ joblib
7
+ skops
8
  networkx