""" Lab P-10: RAG Part 1 - Embedding, Chunking, Vector DB Cara pakai: pip install pypdf chromadb sentence-transformers mkdir dokumen # taruh 2-3 PDF di sini python rag_lab.py lab1 # Lab 1: eksperimen chunking python rag_lab.py lab2 # Lab 2: indeks + 5 pertanyaan + 2 parafrase python rag_lab.py lab3 # Lab 3: hit@k pada 2 ukuran chunk python rag_lab.py query "pertanyaan" # tanya bebas (opsi --size 500) """ import argparse import glob import os import random import re import chromadb from chromadb.utils import embedding_functions from pypdf import PdfReader FOLDER = "dokumen" DB_PATH = "chroma_db" # Model multibahasa supaya bahasa Indonesia & parafrase dipahami dengan baik EMBED_MODEL = "paraphrase-multilingual-MiniLM-L12-v2" _ef = None _client = None # ---------------------------------------------------------------- dasar def baca_pdf(path): """Baca seluruh teks PDF, rapikan spasi/baris baru.""" reader = PdfReader(path) teks = " ".join((hal.extract_text() or "") for hal in reader.pages) return re.sub(r"\s+", " ", teks).strip() def potong(teks, ukuran=500, overlap_persen=0.15): """Potong teks per `ukuran` karakter dengan overlap (10-20%).""" overlap = int(ukuran * overlap_persen) langkah = ukuran - overlap return [teks[i:i + ukuran] for i in range(0, len(teks), langkah) if teks[i:i + ukuran].strip()] def get_client(): global _client, _ef if _client is None: _client = chromadb.PersistentClient(path=DB_PATH) _ef = embedding_functions.SentenceTransformerEmbeddingFunction( model_name=EMBED_MODEL) return _client def daftar_pdf(): files = sorted(glob.glob(os.path.join(FOLDER, "*.pdf"))) if not files: raise SystemExit(f"Tidak ada PDF di folder '{FOLDER}/'. Letakkan 2-3 PDF dulu.") return files # ---------------------------------------------------------------- indeks def indeks(ukuran, overlap_persen=0.15, verbose=True): """Bangun koleksi `dok_` dari semua PDF di folder dokumen/.""" client = get_client() nama = f"dok_{ukuran}" try: client.delete_collection(nama) except Exception: pass col = client.create_collection(nama, embedding_function=_ef, metadata={"hnsw:space": "cosine"}) for path in daftar_pdf(): chunks = potong(baca_pdf(path), ukuran, overlap_persen) fname = os.path.basename(path) col.add( ids=[f"{fname}-{i}" for i in range(len(chunks))], documents=chunks, metadatas=[{"sumber": fname, "chunk": i} for i in range(len(chunks))], ) if verbose: print(f" [{nama}] {fname}: {len(chunks)} chunk") return col def get_collection(ukuran): client = get_client() try: return client.get_collection(f"dok_{ukuran}", embedding_function=_ef) except Exception: return indeks(ukuran, verbose=False) def query(pertanyaan, k=3, ukuran=500, tampil=True): col = get_collection(ukuran) hasil = col.query(query_texts=[pertanyaan], n_results=k) docs = hasil["documents"][0] metas = hasil["metadatas"][0] jarak = hasil["distances"][0] if tampil: print(f"\nTanya: {pertanyaan}") for rank, (d, m, j) in enumerate(zip(docs, metas, jarak), 1): print(f" #{rank} [{m['sumber']} chunk {m['chunk']}] jarak={j:.3f}") print(f" {d[:200]}...") return docs # ---------------------------------------------------------------- lab 1 def lab1(): path = daftar_pdf()[0] teks = baca_pdf(path) print(f"=== LAB 1 | file: {path} | total {len(teks)} karakter ===") print("\n[1] 500 karakter pertama:\n") print(teks[:500]) # --- Tabel 1: ringkasan jumlah chunk print("\n[2] Jumlah chunk (overlap 15%)\n") hasil = {} print(f"{'Ukuran':>7} | {'Overlap':>7} | {'Jml chunk':>9} | {'Rata2 panjang':>13} | {'Berakhir titik':>14}") print("-" * 64) for ukuran in (500, 1000, 1500): chunks = potong(teks, ukuran, 0.15) hasil[ukuran] = chunks rata = sum(len(c) for c in chunks) / len(chunks) titik = sum(c.rstrip().endswith(".") for c in chunks) print(f"{ukuran:>7} | {int(ukuran*0.15):>7} | {len(chunks):>9} | " f"{rata:>13.0f} | {titik:>8}/{len(chunks):<5}") # --- Tabel 2: 3 chunk acak per ukuran print("\n[3] 3 chunk acak per ukuran (cek: terpotong di tengah kalimat/tabel?)\n") print(f"{'Ukuran':>7} | {'Awal chunk (30 krk)':<32} | {'Akhir chunk (30 krk)':<32} | Titik?") print("-" * 85) for ukuran, chunks in hasil.items(): for c in random.sample(chunks, min(3, len(chunks))): awal = c[:30].ljust(30) akhir = c.rstrip()[-30:].rjust(30) ya = "Ya" if c.rstrip().endswith(".") else "Tidak" print(f"{ukuran:>7} | {awal:<32} | ..{akhir:<30} | {ya}") print("-" * 85) # ---------------------------------------------------------------- lab 2 PERTANYAAN_LAB2 = [ "Wahana apa yang pertama kali mengirim gambar jarak dekat Mars?", "Siapa yang mengusulkan Tes Turing?", "Berapa target porsi energi terbarukan Indonesia tahun 2025?", "Apa itu Narrow AI dan contohnya?", "Apa saja tantangan pengembangan energi terbarukan?", ] PERTANYAAN_PARAFRASE = [ # tidak memakai kata yang sama dengan dokumen "Pesawat robotik pertama yang memotret planet merah dari dekat", "Sumber listrik dari perut bumi yang jalan nonstop tanpa peduli cuaca", ] def lab2(): print("=== LAB 2 | Bangun indeks (ukuran 500) ===") indeks(500) print("\n--- 5 pertanyaan: periksa manual apakah chunk teratas relevan ---") for q in PERTANYAAN_LAB2: query(q, k=3, ukuran=500) print("\n--- 2 pertanyaan parafrase: apakah pencarian semantik tetap menemukan? ---") for q in PERTANYAAN_PARAFRASE: query(q, k=3, ukuran=500) # ---------------------------------------------------------------- lab 3 # (pertanyaan, [kata kunci jawaban]) -> hit jika SALAH SATU kata kunci ada di chunk # Sesuaikan jika dokumen Anda berbeda. SOAL_UJI = [ ("Wahana apa yang pertama mengirim gambar jarak dekat Mars?", ["mariner 4"]), ("Kapan Viking 1 dan 2 mendarat di Mars?", ["1976"]), ("Apa nama rover pertama yang beroda di Mars?", ["sojourner"]), ("Apa temuan utama Curiosity?", ["bahan organik", "kawah gale"]), ("Apa program Mars Sample Return?", ["mars sample return", "msr"]), ("Siapa yang mengusulkan Tes Turing?", ["alan turing"]), ("Siapa yang mencetuskan istilah Artificial Intelligence pada 1956?", ["john mccarthy"]), ("Sebutkan contoh Narrow AI.", ["siri", "alexa"]), ("Siapa yang dikalahkan IBM Deep Blue?", ["kasparov"]), ("Siapa yang dikalahkan AlphaGo?", ["lee sedol"]), ("Berapa target bauran EBT nasional tahun 2025?", ["23%"]), ("Berapa persen cadangan panas bumi dunia yang dimiliki Indonesia?", ["40%"]), ("Berapa potensi tenaga surya Indonesia?", ["207,8"]), ("Berapa potensi tenaga air (hidro) Indonesia?", ["75,0"]), ("Apa tantangan pengembangan energi terbarukan di Indonesia?", ["investasi awal", "intermitensi", "transmisi"]), ] def hit_at_k(ukuran, k): col = get_collection(ukuran) hit, gagal = 0, [] for q, kunci in SOAL_UJI: docs = col.query(query_texts=[q], n_results=k)["documents"][0] gabung = " ".join(docs).lower() if any(kw in gabung for kw in kunci): hit += 1 else: gagal.append(q) return hit / len(SOAL_UJI), gagal def lab3(): ukuran_list = (500, 1000) # ubah ke (500, 1500) jika mau print("=== LAB 3 | hit@k pada dua ukuran chunk ===") for u in ukuran_list: indeks(u, verbose=False) print(f"\n{'ukuran':>8} | " + " | ".join(f"hit@{k}" for k in (1, 3, 5))) print("-" * 36) gagal_semua = {} for u in ukuran_list: baris = [] for k in (1, 3, 5): skor, gagal = hit_at_k(u, k) baris.append(f"{skor:.2f}") if k == 1: gagal_semua[u] = gagal print(f"{u:>8} | " + " | ".join(f"{b:>5}" for b in baris)) print("\nPertanyaan yang GAGAL pada k=1:") for u, g in gagal_semua.items(): print(f" ukuran {u}:") for q in g or ["(tidak ada)"]: print(f" - {q}") print("\nTugas Anda: tulis kesimpulan 3-4 kalimat (ukuran mana lebih baik, " "pertanyaan apa gagal, dugaan penyebab).") # ---------------------------------------------------------------- main if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("perintah", choices=["lab1", "lab2", "lab3", "query"]) ap.add_argument("teks", nargs="?", help="pertanyaan (untuk perintah query)") ap.add_argument("--size", type=int, default=500) ap.add_argument("-k", type=int, default=3) a = ap.parse_args() if a.perintah == "lab1": lab1() elif a.perintah == "lab2": lab2() elif a.perintah == "lab3": lab3() else: if not a.teks: raise SystemExit('Contoh: python rag_lab.py query "Siapa Alan Turing?"') query(a.teks, k=a.k, ukuran=a.size)