From 9cd6d9c5468f3eea6671fd5d231fd1b85399fb0b Mon Sep 17 00:00:00 2001 From: Rinaldo Date: Mon, 2 Nov 2026 10:39:22 +0700 Subject: [PATCH] Tugas RAG: Lab 1-3 (chunking, indeks, Hit@k) --- .gitignore | 3 + dokumen/panduan_akademik_und.pdf | 93 ++++++++++++ dokumen/pengantar_ai_rag.pdf | 93 ++++++++++++ dokumen/peraturan_kepegawaian.pdf | 93 ++++++++++++ pertanyaan uji.json | 95 ++++++++++++ rag lab.py | 243 ++++++++++++++++++++++++++++++ 6 files changed, 620 insertions(+) create mode 100644 .gitignore create mode 100644 dokumen/panduan_akademik_und.pdf create mode 100644 dokumen/pengantar_ai_rag.pdf create mode 100644 dokumen/peraturan_kepegawaian.pdf create mode 100644 pertanyaan uji.json create mode 100644 rag lab.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7db7a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +ο»Ώchroma_db/ +__pycache__/ +.venv/ diff --git a/dokumen/panduan_akademik_und.pdf b/dokumen/panduan_akademik_und.pdf new file mode 100644 index 0000000..77d14ca --- /dev/null +++ b/dokumen/panduan_akademik_und.pdf @@ -0,0 +1,93 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102020454+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102020454+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Panduan Akademik Universitas Nusantara Digital) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2211 +>> +stream +Gat=,fll+.&:O:SbbL?=W*Ha#52!3qe%"pf,3@'&O9nhs(TAYlec#@$!P$7;jOVA."G-VcE9GLnO'MU.k4:;T\m!N8^Nno9[!X&N*/Cp!PAZ[&IpI$eN$>P7-aM]&U"La&,$G1HrtUq&nQ6]@1/Bf?9ONpS*JO.-NE$Z-63?577'^QBeI=7(38C#DYRPDtlCn08M[^?G:HTs.O^O5a\h=,B$9Md-V\he^4+u(9r*nr)=9M@]bOu@<_'aPPN.ljdnq'&S%`abXG0-;^M"*;"R0mC.t6g[N16-6UXA2cLAC>HKjAJefd;U+Y&eSfSGu>,#^osRE,P_!NfVYa>9i(nOdCS[u(Wu1BtpZJZdM\RKOomYpM\MGHfXP<0@5%O"M)OePPM\4X\Y,%uoA0/--1fk\Nn=Yu*$%=sTP4>*IZrJ+.%#idHac^X50f4;^X/DZAb[s57JK1]nZ0jX=RPN;@0n`O\,&EoQ:a-Q&@)%[:/UZZWo,lAMd3'Caq60CAcfGbh626?)LD?ER.sV?JVG[_U"mQ(ZneO$t5mUBO[7)k@m!+hIi_(>-DX<3#8jKFMS7j2f^l:P.hiK)%+XO(\GKN')F^%3cP>2@"lNThjT`.I$*W!R7"_o:ArYB#$R*k8A?%0API"^\?XnK3c4GmJGTMiOES3pG9_5W%p)F0FAd&\5[LeY>38Z(Aij6a-Qg/f`=DJp<.hM2=kO1sQPk$f=@*g[Nn^VBf/=0um[P8iLhOO:qh[8Vi:K-g6QX*mp]RDY\gMo''9K6Al71ijM@F1?sq_naRnXqeh2NKA(^I4.)66i?46PfmBX2!Xe2A`jRcXr1%qfo1j-VN>I+=e?RdLpc62`<>Qd18.gD>1R07"UU27=J(cpqlNtpp`P9t%8i%o?*,*a]!dMpc@B"Za/]`njHFh>^74YT4/2Jh!*af;SrV1,.3NoK4#'KWBa=8d3Uc_()IT:`=aB9/ZfmS,W!9SCg3p#dYR\`5st1Iuo%PTPL-*"=e\Z#V7.!j%[;B(jPu<51V_VE-o&D,]E$/Ym+"F_fq9KqmhSWWe'oK/\ECSV5qN0`Y1^FWhHaKcqc)ag4:9b/KK/ebEaRE`bCS;[>b=m!^u)B.F!kCU7i>_\N5%XCl-Zr_%%#3h]F^kk2/TWbu/)OXhZ(n&S2^-6TB9`[d_=B#oChnCN-8TNChhYa0`]ubV28r2>(UZ?3?HoS0WqfXcXoCf$s@Go]l7'<5KqBoZK80kcQFgRB8&8iQQ,H?&J$C\YcS'"p3d9iYtoC-/frKgktAV0NqRi)'ES]gHsN1X\=LbnDf`Vd;*7QgLCendstream +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 486 +>> +stream +GasJNhbMo['ZTV5']131.'lQN_C]5\VG7f+i;l^96Yd<]T'ZgNm<_Q_oha-XCISFtI\KZab>(^XfGBRdC:5!?7KbMgXc`TEUi#m*[m:?XV:iW`#]GY*_A1/+]4hBn&-9f';@`$"j/N"H33mio"/C-nQ=gSgj@Ft6QP>l.NQ&R9@:P[\f<_lU"9nU*]qH@KI5CPX9s6=Ur>UF[)_o[fd`i@;eEg!9Ar=eT>t1;#G!.8I):gn6W$PpMH3tUP2@(nGO,c%Ar:O,;UpseiG9sU5d9@3b:1I)T6I/:SaO^MKB<^2#2=S~>endstream +endobj +xref +0 11 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000000524 00000 n +0000000728 00000 n +0000000796 00000 n +0000001109 00000 n +0000001174 00000 n +0000003476 00000 n +trailer +<< +/ID +[<879ad10a394d9f4a532c177317478ceb><879ad10a394d9f4a532c177317478ceb>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 11 +>> +startxref +4053 +%%EOF diff --git a/dokumen/pengantar_ai_rag.pdf b/dokumen/pengantar_ai_rag.pdf new file mode 100644 index 0000000..66f6f25 --- /dev/null +++ b/dokumen/pengantar_ai_rag.pdf @@ -0,0 +1,93 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102020454+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102020454+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Pengantar Kecerdasan Buatan dan RAG) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2234 +>> +stream +Gat=,968iI%)1n+i0\[#XX2^oEn`G<[cq;!9te#@BT%ui8X*7"]@28E+%X*CbdN_?V2uf"!Zt_YBnjCLI;_`P2_4abYE6VW<_jD^Jd&8?e%=/`5A+Cp)96j,S-RZt2qL$?NLAO7dPIC&#Bi(/JlP4t27&/aAe`RCg!/&)[T"lEpcR?GImpDN>.pfVXcq,p)ud9gRP+o:G.2),?Vkt*DLTjqSqd@&eq3=Lh,gj_m>s*n142sYr7['^Z"Hh-ZdAM9cJ"`h?^=)ie>`*>_[-jNC;'`Q@F4f_Ep18Bn12RqfFbN9;Sb'm4tUVI658N'JUMV_gQXK9ObLs0T@DjGB^dYj$YOG[[(7mIb1FRW?Kd+OeJ4%Jplm^Na]E(:P6#PZb60auZ5Bd67FM'5,>DsK`taoo)HP\/SpoLbp\:Fe;YX7u)5.BCqIC;IJ:r&Ei<"k=8(eQBi%C^O%?(2SQ-r1m,S2cn%IZAbu91X)VE5"Z7f[`n5pmr20kn7<.NM=MD%:5uJ6.M_UBKNj6%I5C;9#HejBMH78ib!kK&B1!4R`)J5ofPtk(?@_lDja78&&8#c2b$D.oT5rL%##AM>_M?MA7`WHs*:sEM=L"baftd%b&sq]O'PZ)GK,]9Nr$$i4G8/uq52U>i1+FN"*9-rHm"hr@,ROP4[#s%s,5$npGZB*PqpZo2gfPf_);Fp[>5KU"pn8='SOVB#0\W\(+J,>H/`tO)&mRq.Me,Gk:LHNLfMt=tjbtem\jk,slRg%/->uA(5a_q;38ZEKc5fM)$;Y?PBV9[0HQFMkidpc3-t5kB!'3nb(L(\ICgp6Z=BgUj%h[lrC:OsBn/#NQONF!n\-IdRj=KT\(c&YJ6m?o\U5@\@W%q=#G0oPY8VdX_r_"atT1ZYjKc[5&oqRsX=aMf&tWrRsC%f'q+k!UbCG07GL34@=Q-%&;H.o!;Qu[NCThk\*PZhu%AY2\5+Ho9!f^OYn^XU<0b7Wo5&k\K(k?g;=H1P4asVmAXLZdVr5^md'5@]\a<@gS)>.u2JA9E!0(o+?#OA?W;Ro2m!@_J8GOEM;^'Dk`nS-cdjL"3'ts^;Nj_M'RV(T*X6,U0TP>aa0W=)PTalFep`BC/mjT)BEKKL4K8d?/*_+p#4';54,N:`&qPfE5sQaJX"h'QmsqsE<1hDN*s='c@'Yj'A;:PRRZ0B)emYi!]&^Y`S487!)7qb@qW0#]@IN6L8OR@kZ^K8%=nX--0%CngR'E->#?%4m%M`A)t_=NA&?Q>qm7+']Pbl`;h#+=dB?U[^9Tf,^iI+]qq("9Il4i'HAj4;BIL@%la>mF#9@67qo%bok9si=T-[AQ<`2_2g?:,i/6C*-\35V8+T,.J,-""E,f]WmE5at5F$TaoY2NY<9I4$eFsD3N.;E:#t^JQ04,=0PB!9Yr4k"S/"-m7KCP-K?C@2hkB`@&Wu*`C:\LAim?%k!\0!5mD.3n2&*&M=UeuYK$MNt6X5Q."X8(;!I2UBnF;^CBX:>=SPkd,m;2!6o"/`l=d/~>endstream +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 682 +>> +stream +GasId9lJKW&A71-m'/uVYhLG!ie!7W8.YodO1hXn[a+$gcfTY+o'[GXWC\QBMA/MQm*=tA,c;'CI/L-NUA"i)rg6W58&Z/'bJfX%0D(s*rKu\ZjEaO=o$q3UH.ZL+0=(,%Y@j6IVff&!MY=D-)gC*IRr1E(HtTDn=!sJ"otK`R+-_*AOkl/Q;*$*Gg!t3&?X0nS;_f8f9bqZ,'"c5VajQc52AF,OjKP;Z#&\s_hbnHeiminQSq<1>fMa]cA5iWtUCr$-1PiZ=DPblQX\uQ&cB=M`IU7"`Xn`.f7.sV"<]V6C[tb_Y/_@na>QJ^3cKf!md:"JK`]_W51C>YJ\#0VP*i']uXA-%#I*M;Z#A.MU??G'IFra2Gtc!P8Y9jGYDEG(@Kftb?*q5Jp+<5q0D6p>*''W*6Z_-r)^lbl<>7S#OM@X'rdTVfLmZ%5S]'7uTMHZ%NuQ7/]Pi+iSpc=m9+Y3\,tp>$ac9Q=OGiXC,:ORHSZ[$Y+FB,KOsI]WPHif<8(M]At(jr3d`6RY(80dk]nR@bCQ#5u(XoFgd&PG=("q\6!oWm6WS!Y,0gE4roCOCl6.*r8j=~>endstream +endobj +xref +0 11 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000000524 00000 n +0000000728 00000 n +0000000796 00000 n +0000001098 00000 n +0000001163 00000 n +0000003488 00000 n +trailer +<< +/ID +[<5ceaba85e137fd7c645771c15882af0c><5ceaba85e137fd7c645771c15882af0c>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 11 +>> +startxref +4261 +%%EOF diff --git a/dokumen/peraturan_kepegawaian.pdf b/dokumen/peraturan_kepegawaian.pdf new file mode 100644 index 0000000..45a9b09 --- /dev/null +++ b/dokumen/peraturan_kepegawaian.pdf @@ -0,0 +1,93 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20261102020454+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20261102020454+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Peraturan Kepegawaian PT Lintas Data Mandiri) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2202 +>> +stream +GatU4a`?/p&A@B[qC&s$L?*<^goNJT/B_&!Al?27!fGJ=_ds[J.$acqY5s6uXEM-R*g0X:+1bf4YX^>F4T5/I'A]eT\%b&rHs4:H(H;Gt+6_&DeULY,]5fg)q/kUI(+hpHaClh>K_&clCtn2CP1R%ZBXgWg*NB.Ze-41.K(nB8iGu9f]S!'TUSZCgO&u6n+R"!S1XRD&))d;t.S"2RP"mc?W/Y:/Q,qr_."m;q-$T&(O3SMaSaR:fS1i5b@2XDkN4QQ7s<^R$u)'Ti]2BZOm>j"OKZ"YV0"2b*g0;9%8Kf-E;gB,#/$KN?.C+3.akcM6`2p82dL#W`0LMDJ97!CRoSH,i8+U8`m`CS'2Z_Gi%@\l[Z]g_Ei0tk4ASY'D-fq0'o#"G$.ElM,t=CD2VYg)B;S<=Aj>FNIK,\?SJhYVq&HU4pp[qWVQRCu+X6ZP:S0=0Y?cD83^EKmUQlmCgG*n/10e=$%sKYni3M6%pY.F8fWf/;No@s;9kNc0?L4'2_6Z28^hDkCYM&Zo-pR+_\&6]2\DF214dgZ3cjreiqBWLIVrooMmsaaG+IW-mLO>h#22Ve[2&B_T@+c8MIqhJI^LukB".n!qBXUa';hmQhg=pQ#U.J$P[2;p1%(rWYBX=VG><&daW;02,"1+Z;*r:?!rqksW]4&B.I=c0L4*?0RD2WKh"=8>G*,h:;\4/Ff>$Ht?'H>^aU2@rAUM&@t>&@n[gS$pRIB\Q+V=T`3])Bp[D%hnM!RA$MHKf"K.sFuFGD4,`b*WlR7Zije([?*!FM8rZ0i\%q_%;0>Q[Qm>HpLj&Jss=>gQqKEFYOdQ%>u]XgebXh16NB3j+`U(tdFgdk-tlEMl9mi/hk\WB0LTa_@Z`QeVJfGpLbgXtB'Xgj=O8KrE>Vn:JiMR,_?1eoa*!(+^i?IarPF3[<4t*ogj2RTl,cQ``ja7k(Gb#5M\#AUH]LABJG3/2>e0\6Jgu;]3)TL>>*lUa>^U4V5SLMNq__3hKoM7_eUf5[g7nR`CNaKYK1EdY6!I+?E3?7HSUl?P\Dau?$rK-1QuJIlqQW?q!>Q_?f:O?m?m,N;mlO+U&endstream +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 304 +>> +stream +Gas2Db>,r?$q9pdMS#YiV4"[tkn],Y=L;Od^e'8;SO^OZrZ?>fZ=&57=ao1:b5rEJaM,.!S&G"K+DStNLjcghr4ooq^'jIuC`9tS%V=B9>#oU;apgQNX;*7gDBS>;M7'DkC/I0,%@N=!D'6$Dcb*l@aX[&%/_;@7&Vh&L_(+3arTlC/#-RI2Sa%8?gp4_Bs#FcKOq0o*[h+0$R5Ao,@?/7gWVi=IpRU":]Pt?on6DX]!D-L;m/~>endstream +endobj +xref +0 11 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000000524 00000 n +0000000728 00000 n +0000000796 00000 n +0000001107 00000 n +0000001172 00000 n +0000003465 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 11 +>> +startxref +3860 +%%EOF diff --git a/pertanyaan uji.json b/pertanyaan uji.json new file mode 100644 index 0000000..6c5b3a4 --- /dev/null +++ b/pertanyaan uji.json @@ -0,0 +1,95 @@ +[ + { + "pertanyaan": "Berapa biaya pendaftaran mahasiswa baru jalur reguler?", + "kata_kunci": [ + "350.000" + ] + }, + { + "pertanyaan": "Berapa SKS maksimum yang boleh diambil jika IPS sebelumnya 3,2?", + "kata_kunci": [ + "24 SKS" + ] + }, + { + "pertanyaan": "Berapa IPK minimal untuk predikat cum laude?", + "kata_kunci": [ + "3,51" + ] + }, + { + "pertanyaan": "Berapa biaya cuti akademik dibanding UKT?", + "kata_kunci": [ + "25 persen" + ] + }, + { + "pertanyaan": "Siapa yang berhak mendapat beasiswa kurang mampu?", + "kata_kunci": [ + "3.000.000" + ] + }, + { + "pertanyaan": "Berapa hari cuti tahunan karyawan tetap?", + "kata_kunci": [ + "12 hari kerja" + ] + }, + { + "pertanyaan": "Kapan gaji karyawan dibayarkan?", + "kata_kunci": [ + "tanggal 25" + ] + }, + { + "pertanyaan": "Berapa lama cuti melahirkan?", + "kata_kunci": [ + "3 bulan" + ] + }, + { + "pertanyaan": "Berapa uang harian perjalanan dinas dalam negeri?", + "kata_kunci": [ + "400.000" + ] + }, + { + "pertanyaan": "Berapa lama satu surat peringatan berlaku?", + "kata_kunci": [ + "enam bulan" + ] + }, + { + "pertanyaan": "Apa itu RAG dan bagaimana cara kerjanya?", + "kata_kunci": [ + "embedding", + "chunk" + ] + }, + { + "pertanyaan": "Berapa ukuran chunk yang umum dipakai?", + "kata_kunci": [ + "500 sampai 1000" + ] + }, + { + "pertanyaan": "Apa kelemahan pencarian semantik?", + "kata_kunci": [ + "angka", + "kode" + ] + }, + { + "pertanyaan": "Apa yang diukur oleh Hit@k?", + "kata_kunci": [ + "k hasil teratas", + "proporsi" + ] + }, + { + "pertanyaan": "Siapa yang memperkenalkan istilah kecerdasan buatan?", + "kata_kunci": [ + "McCarthy" + ] + } +] \ No newline at end of file diff --git a/rag lab.py b/rag lab.py new file mode 100644 index 0000000..4217317 --- /dev/null +++ b/rag lab.py @@ -0,0 +1,243 @@ +""" +rag_lab.py - Kode untuk Lab 1, 2, dan 3 (chunking, indeks & pencarian, Hit@k) + +Instalasi: + pip install pypdf chromadb sentence-transformers + +Cara pakai: + python rag_lab.py lab1 dokumen/contoh.pdf + python rag_lab.py lab2 # indeks semua PDF di dokumen/ lalu tanya + python rag_lab.py lab3 pertanyaan_uji.json # hitung Hit@k + +Format pertanyaan_uji.json: +[ + {"pertanyaan": "Berapa batas usia pendaftar?", "kata_kunci": ["usia", "35 tahun"]}, + ... +] +""" + +import os + +os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1" # sembunyikan peringatan symlink di Windows + +import json +import random +import sys +from pathlib import Path + +import chromadb +from chromadb.utils import embedding_functions +from pypdf import PdfReader + +FOLDER_DOKUMEN = Path("dokumen") +FOLDER_DB = "chroma_db" +# Model multibahasa, cocok untuk teks Indonesia +MODEL_EMBEDDING = "paraphrase-multilingual-MiniLM-L12-v2" + +_embed_fn = None +_client = None + + +def _embedding(): + global _embed_fn + if _embed_fn is None: + _embed_fn = embedding_functions.SentenceTransformerEmbeddingFunction( + model_name=MODEL_EMBEDDING + ) + return _embed_fn + + +def _db(): + global _client + if _client is None: + _client = chromadb.PersistentClient(path=FOLDER_DB) + return _client + + +# ---------------------------------------------------------------- Lab 1 +def baca_pdf(path): + """Baca seluruh teks PDF menjadi satu string.""" + reader = PdfReader(str(path)) + halaman = [] + for p in reader.pages: + teks = p.extract_text() or "" + halaman.append(teks.strip()) + return "\n\n".join(halaman) + + +def potong(teks, ukuran=1000, overlap_persen=15): + """ + Potong teks menjadi chunk berukuran ~`ukuran` karakter dengan overlap. + Berusaha memotong di akhir kalimat/paragraf agar tidak terpotong di tengah. + """ + overlap = int(ukuran * overlap_persen / 100) + chunks = [] + mulai = 0 + n = len(teks) + + while mulai < n: + akhir = min(mulai + ukuran, n) + + if akhir < n: + # cari titik potong terbaik di 25% terakhir jendela + batas_cari = mulai + int(ukuran * 0.75) + jendela = teks[batas_cari:akhir] + titik = max(jendela.rfind("\n\n"), jendela.rfind(". "), jendela.rfind(".\n")) + if titik != -1: + akhir = batas_cari + titik + 1 + + potongan = teks[mulai:akhir].strip() + if potongan: + chunks.append(potongan) + + if akhir >= n: + break + mulai_baru = max(akhir - overlap, mulai + 1) + # geser maju sampai awal kata berikutnya agar tidak memotong kata + while mulai_baru < akhir and not teks[mulai_baru - 1].isspace(): + mulai_baru += 1 + mulai = mulai_baru + + return chunks + + +def lab1(path_pdf): + teks = baca_pdf(path_pdf) + print(f"Total karakter: {len(teks)}") + print("\n=== 500 karakter pertama ===") + print(teks[:500]) + + random.seed(42) + for ukuran in (500, 1000, 1500): + overlap_persen = 15 + chunks = potong(teks, ukuran, overlap_persen) + print(f"\n{'=' * 60}") + print(f"Ukuran {ukuran}, overlap {overlap_persen}% -> {len(chunks)} chunk") + for i in random.sample(range(len(chunks)), min(3, len(chunks))): + print(f"\n--- chunk #{i} ({len(chunks[i])} karakter) ---") + print(chunks[i]) + akhir = chunks[i].rstrip()[-1:] + if akhir not in ".!?:)": + print("[!] Chunk tidak berakhir di tanda baca: kemungkinan terpotong") + + +# ---------------------------------------------------------------- Lab 2 +def buat_indeks(nama_koleksi, ukuran, overlap_persen=15, folder=FOLDER_DOKUMEN): + """Indeks semua PDF di `folder` ke koleksi baru bernama `nama_koleksi`.""" + try: + _db().delete_collection(nama_koleksi) + except Exception: + pass + koleksi = _db().create_collection( + nama_koleksi, + embedding_function=_embedding(), + metadata={"hnsw:space": "cosine"}, + ) + + pdfs = sorted(Path(folder).glob("*.pdf")) + if not pdfs: + sys.exit(f"Tidak ada PDF di folder '{folder}/'.") + + for pdf in pdfs: + chunks = potong(baca_pdf(pdf), ukuran, overlap_persen) + if not chunks: + print(f"[!] {pdf.name}: tidak ada teks terbaca (PDF hasil scan?)") + continue + koleksi.add( + ids=[f"{pdf.stem}-{i}" for i in range(len(chunks))], + documents=chunks, + metadatas=[{"sumber": pdf.name, "urutan": i} for i in range(len(chunks))], + ) + print(f"{pdf.name}: {len(chunks)} chunk -> koleksi '{nama_koleksi}'") + return koleksi + + +def ambil_koleksi(nama_koleksi): + return _db().get_collection(nama_koleksi, embedding_function=_embedding()) + + +def query(nama_koleksi, pertanyaan, k=3, tampil=True): + """Cari k chunk paling mirip. Mengembalikan list dict.""" + hasil = ambil_koleksi(nama_koleksi).query(query_texts=[pertanyaan], n_results=k) + out = [] + for doc, meta, jarak in zip( + hasil["documents"][0], hasil["metadatas"][0], hasil["distances"][0] + ): + out.append({"teks": doc, "sumber": meta["sumber"], "skor": 1 - jarak}) + if tampil: + print(f"\nPertanyaan: {pertanyaan}") + for r in out: + print(f" [{r['skor']:.3f}] {r['sumber']}: {r['teks'][:700].replace(chr(10), ' ')}...\n") + return out + + +def lab2(): + buat_indeks("chunk_1000", ukuran=1000) + print("\nKetik pertanyaan (kosong untuk selesai). Coba 5 biasa + 2 parafrase.") + while True: + q = input("\n> ").strip() + if not q: + break + query("chunk_1000", q, k=3) + + +# ---------------------------------------------------------------- Lab 3 +def hit_at_k(nama_koleksi, soal, k): + """ + Hit@k = proporsi pertanyaan yang kata kuncinya muncul + (minimal satu kata kunci) di salah satu dari k chunk teratas. + Mengembalikan (skor, daftar_pertanyaan_gagal). + """ + hit = 0 + gagal = [] + for s in soal: + hasil = query(nama_koleksi, s["pertanyaan"], k=k, tampil=False) + gabungan = " ".join(" ".join(r["teks"].split()) for r in hasil).lower() + if any(" ".join(kw.split()).lower() in gabungan for kw in s["kata_kunci"]): + hit += 1 + else: + gagal.append(s["pertanyaan"]) + return hit / len(soal), gagal + + +def lab3(path_soal): + soal = json.loads(Path(path_soal).read_text(encoding="utf-8")) + print(f"{len(soal)} pertanyaan uji dimuat.") + + ukuran_uji = {"chunk_500": 500, "chunk_1500": 1500} + for nama, ukuran in ukuran_uji.items(): + buat_indeks(nama, ukuran) + + print(f"\n{'Koleksi':<12}{'Hit@1':>8}{'Hit@3':>8}{'Hit@5':>8}") + gagal_per_koleksi = {} + for nama in ukuran_uji: + baris = [] + for k in (1, 3, 5): + skor, gagal = hit_at_k(nama, soal, k) + baris.append(skor) + if k == 5: + gagal_per_koleksi[nama] = gagal + print(f"{nama:<12}" + "".join(f"{b:>8.2f}" for b in baris)) + + print("\nPertanyaan yang gagal pada k=5:") + for nama, gagal in gagal_per_koleksi.items(): + print(f"\n[{nama}] {len(gagal)} gagal") + for g in gagal: + print(f" - {g}") + + print("\nTulis kesimpulan 3-4 kalimat: ukuran terbaik, jenis pertanyaan yang gagal, dugaan penyebab.") + + +# ---------------------------------------------------------------- CLI +if __name__ == "__main__": + if len(sys.argv) < 2: + sys.exit(__doc__) + perintah = sys.argv[1] + if perintah == "lab1" and len(sys.argv) == 3: + lab1(sys.argv[2]) + elif perintah == "lab2": + lab2() + elif perintah == "lab3" and len(sys.argv) == 3: + lab3(sys.argv[2]) + else: + sys.exit(__doc__) \ No newline at end of file