Spaces:
Build error
Build error
FauziIsyrinApridal
remove supabase parameter from get timestamp and fix evaluate stil asking for billing though
22f049b
import os | |
import sys | |
import time | |
import random | |
import logging | |
from datetime import datetime | |
from typing import List, Dict, Any | |
import streamlit as st | |
from dotenv import load_dotenv | |
from langsmith import Client, traceable | |
from typing_extensions import Annotated, TypedDict | |
from langchain_openai import ChatOpenAI | |
from langchain_community.llms import Replicate | |
from langchain.memory import ConversationBufferMemory | |
from langchain.chains import ConversationalRetrievalChain | |
import backoff | |
from ratelimit import limits, sleep_and_retry | |
import json | |
# Import dari aplikasi utama Anda | |
from app.document_processor import load_vector_store_from_supabase | |
from app.prompts import sahabat_prompt | |
from app.db import supabase | |
# === Logging UTF-8 Safe === | |
class UTF8StreamHandler(logging.StreamHandler): | |
def __init__(self, stream=None): | |
if stream is None: | |
stream = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1) | |
super().__init__(stream) | |
log_filename = f'rag_evaluation_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log' | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.FileHandler(log_filename, encoding='utf-8'), | |
UTF8StreamHandler() | |
] | |
) | |
logger = logging.getLogger(__name__) | |
# === Konfigurasi === | |
load_dotenv() | |
BUCKET_NAME = "pnp-bot-storage-archive" | |
VECTOR_STORE_PREFIX = "vector_store" | |
MAX_CALLS_PER_MINUTE = 50 | |
MAX_CALLS_PER_HOUR = 1000 | |
# === Dataset evaluasi === | |
evaluation_dataset = [ | |
{ | |
'question': '''Bagaimana sistem pendidikan yang diterapkan di Politeknik Negeri Padang?''', | |
'ground_truth': '''Sistem pendidikan yang diterapkan di Politeknik adalah dengan menggabungkan pendidikan teoritis, praktek (terapan) di Laboratorium dan praktek industry. Pelaksanaan praktik di industri dilakukan oleh mahasiswa selama satu semester untuk menambah wawasan, pengalaman dan pengembangan ilmu guna membentuk tenaga ahli yang terampil dan profesional.''' | |
}, | |
{ | |
'question': '''Apa saja mata kuliah yang terdapat dalam kurikulum pendidikan Politeknik Negeri Padang?''', | |
'ground_truth': '''Kurikulum Pendidikan telah disusun berbasis kompetensi dengan kelompok mata kuliah sebagai berikut : - Mata Kuliah Pengembangan Kepribadian (MPK) - Mata Kuliah Keimuan dan Keterampilan (MKK) - Mata Kuliah Berkarya (MKB) - Mata Kuliah Berkehidupan Bermasyarakat (MBB)''' | |
}, | |
{ | |
'question': '''Bagaimana Politeknik Negeri Padang mendukung misi tridharma perguruan tinggi?''', | |
'ground_truth': '''Politeknik Negeri Padang dalam menjalankan misi tridharma perguruan tinggi didukung oleh tenaga pendidik dan tenaga kependidikan yang profesional pada bidangnya. Jumlah dan kualifikasi staf tersebut berdasarkan keadaan Desember 2017 sebagai berikut : - Tenaga Pendidik : S1 = 14 orang, S2 = 256 orang, S3 = 21 orang (Yang sedang menempuh S3 = 7 orang, Yang sedang menempuh S2 = 5 orang) - Tenaga Kependidikan : SD = 5 orang, SMP = 4 orang, SLTA = 71 orang, D3 = 25 orang, S1 = 54 orang, S2 = 15 orang.''' | |
}, | |
{ | |
'question': '''Bagaimana Politeknik Negeri Padang menyediakan akses internet bagi mahasiswa?''', | |
'ground_truth': '''Politeknik Negeri Padang telah memiliki Anjungan Internet Mandiri (AIM) yang dapat diakses oleh mahasiswa secara gratis, yang tersedia pada titik-titik strategis. Juga tersedia kawasan hot spot area di sekitar kampus sehingga mahasiswa dapat memanfaatkan internet dengan bebas menggunakan laptop/PC.''' | |
}, | |
{ | |
'question': '''Apa saja contoh kerjasama Politeknik Negeri Padang dengan industri?''', | |
'ground_truth': '''PT. Siemens Indonesia, PT. Toyota Aichi Takaoua Japan, PT. PLN, PT. INTI, Futaba Rashi Siisha Kusho Japan, PT. Sintom, PT. Krakatau Steel, Komatssu Shinge Koumuten, PT. PAL Indonesia, PT. Hexindo, Taishurin Co. Ltd Fukuoaka Japan, PT. Texmaco Perkasa, PT. LEN Industri, PT. Toyota Astra Motor, PT. Indah Kiat, PT. Trakindo Utama, BTN.''' | |
}, | |
{ | |
'question': '''Bagaimana Politeknik Negeri Padang membantu mahasiswa dalam bidang prestasi dan ekonomi?''', | |
'ground_truth': '''Tersedia bantuan untuk sekitar 800 mahasiswa setiap tahunnya. Beasiswa yang diterima antara lain: - Beasiswa Peningkatan Prestasi Akademik (PPA), - Beasiswa Kerja Mahasiswa (BKM), - Beasiswa Bantuan Belajar Mahasiswa (BBM), - Beasiswa TPSDP, - Beasiswa Kredit Bantuan Belajar Mahasiswa (KBBM), - Beasiswa Depertemen Hankam (ABRI), - Beasiswa PT. Toyota Astra, - Beasiswa ORBIT (ICMI), - Beasiswa Supersemar.''' | |
}, | |
{ | |
'question': '''Bagaimana status akreditasi program studi di Politeknik Negeri Padang?''', | |
'ground_truth': '''Program studi di Politeknik Negeri Padang memiliki status akreditasi yang bervariasi seperti Baik, Baik Sekali, hingga Unggul. Contohnya, Teknik Mesin (D3) terakreditasi Baik Sekali hingga 2029, Teknik Manufaktur (D4) terakreditasi Unggul hingga 2028, dan Teknik Sipil (D3) terakreditasi A hingga 2026. Setiap program memiliki SK dan sertifikat akreditasi resmi.''' | |
}, | |
{ | |
'question': '''Bagaimana proses penerimaan mahasiswa baru di Politeknik Negeri Padang?''', | |
'ground_truth': '''Penerimaan mahasiswa baru di Politeknik Negeri Padang dilakukan melalui berbagai jalur seleksi seperti SNBT, SNMPN, dan kelas kerjasama. Tersedia brosur dan informasi detail melalui situs http://penerimaan.pnp.ac.id. Program studi Teknik Alat Berat misalnya memiliki kelas kerjasama dengan PT Trakindo Utama. Jadwal seleksi dan pengumuman dapat diakses secara daring.''' | |
}, | |
{ | |
'question': '''Apa bentuk kerjasama yang dilakukan Politeknik Negeri Padang?''', | |
'ground_truth': '''Politeknik Negeri Padang menjalin kerjasama dengan industri, pemerintah, BUMN, dan asosiasi profesi baik dalam negeri maupun luar negeri. Bentuk kerjasama mencakup rekrutmen, prakerin (praktik kerja industri), kunjungan industri, bimbingan karir, serta pembuatan MoU. Tujuannya untuk menjaga mutu lulusan dan penyaluran SDM.''' | |
}, | |
{ | |
'question': '''Siapa saja pimpinan di Politeknik Negeri Padang saat ini?''', | |
'ground_truth': '''Direktur Politeknik Negeri Padang adalah Dr. Ir. Surfa Yondri, S.T., S.ST., M.Kom. Wakil Direktur Bidang Akademik adalah Ir. Revalin Herdianto, ST., M.Sc., Ph.D. Pimpinan lainnya antara lain Nasrullah, ST., M.T., dan Sarmiadi, S.E., M.M. yang memiliki pengalaman panjang dalam jabatan struktural di kampus.''' | |
}, | |
{ | |
'question': '''Bagaimana sejarah singkat berdirinya Politeknik Negeri Padang?''', | |
'ground_truth': '''Politeknik Negeri Padang didirikan pada tahun 1987 sebagai salah satu dari 17 politeknik pertama di Indonesia. Awalnya bernama Politeknik Engineering Universitas Andalas. Pada 1997 menjadi Politeknik Universitas Andalas lalu berubah menjadi Politeknik Negeri Padang. Saat ini memiliki 32 program studi dari jenjang D3 hingga Magister Terapan.''' | |
} | |
] | |
# Schema untuk evaluasi | |
class CorrectnessGrade(TypedDict): | |
explanation: Annotated[str, ..., "Penjelasan alasan penilaian"] | |
correct: Annotated[bool, ..., "True jika jawaban benar, False jika salah"] | |
class RelevanceGrade(TypedDict): | |
explanation: Annotated[str, ..., "Penjelasan alasan penilaian"] | |
relevant: Annotated[bool, ..., "True jika jawaban relevan dengan pertanyaan"] | |
class GroundedGrade(TypedDict): | |
explanation: Annotated[str, ..., "Penjelasan alasan penilaian"] | |
grounded: Annotated[bool, ..., "True jika jawaban berdasarkan dokumen yang diambil"] | |
class RetrievalRelevanceGrade(TypedDict): | |
explanation: Annotated[str, ..., "Penjelasan alasan penilaian"] | |
relevant: Annotated[bool, ..., "True jika dokumen yang diambil relevan dengan pertanyaan"] | |
# Prompt untuk evaluasi dalam Bahasa Indonesia | |
correctness_instructions = """Anda adalah seorang guru yang menilai kuis. | |
Anda akan diberikan PERTANYAAN, JAWABAN BENAR (ground truth), dan JAWABAN SISWA. | |
Berikut kriteria penilaian yang harus diikuti: | |
(1) Nilai jawaban siswa HANYA berdasarkan akurasi faktual relatif terhadap jawaban benar. | |
(2) Pastikan jawaban siswa tidak mengandung pernyataan yang bertentangan. | |
(3) Tidak apa-apa jika jawaban siswa berisi informasi lebih banyak dari jawaban benar, selama akurat secara faktual. | |
Kebenaran: | |
Nilai kebenaran True berarti jawaban siswa memenuhi semua kriteria. | |
Nilai kebenaran False berarti jawaban siswa tidak memenuhi semua kriteria. | |
Jelaskan penalaran Anda secara bertahap untuk memastikan penalaran dan kesimpulan benar. | |
Hindari menyebutkan jawaban benar di awal.""" | |
relevance_instructions = """Anda adalah seorang guru yang menilai kuis. | |
Anda akan diberikan PERTANYAAN dan JAWABAN SISWA. | |
Berikut kriteria penilaian yang harus diikuti: | |
(1) Pastikan JAWABAN SISWA ringkas dan relevan dengan PERTANYAAN | |
(2) Pastikan JAWABAN SISWA membantu menjawab PERTANYAAN | |
Relevansi: | |
Nilai relevansi True berarti jawaban siswa memenuhi semua kriteria. | |
Nilai relevansi False berarti jawaban siswa tidak memenuhi semua kriteria. | |
Jelaskan penalaran Anda secara bertahap untuk memastikan penalaran dan kesimpulan benar. | |
Hindari menyebutkan jawaban benar di awal.""" | |
grounded_instructions = """Anda adalah seorang guru yang menilai kuis. | |
Anda akan diberikan FAKTA dan JAWABAN SISWA. | |
Berikut kriteria penilaian yang harus diikuti: | |
(1) Pastikan JAWABAN SISWA berdasarkan FAKTA yang diberikan. | |
(2) Pastikan JAWABAN SISWA tidak mengandung informasi "halusinasi" di luar cakupan FAKTA. | |
Berdasarkan Fakta: | |
Nilai True berarti jawaban siswa memenuhi semua kriteria. | |
Nilai False berarti jawaban siswa tidak memenuhi semua kriteria. | |
Jelaskan penalaran Anda secara bertahap untuk memastikan penalaran dan kesimpulan benar. | |
Hindari menyebutkan jawaban benar di awal.""" | |
retrieval_relevance_instructions = """Anda adalah seorang guru yang menilai kuis. | |
Anda akan diberikan PERTANYAAN dan sekumpulan FAKTA yang disediakan siswa. | |
Berikut kriteria penilaian yang harus diikuti: | |
(1) Tujuan Anda adalah mengidentifikasi FAKTA yang sama sekali tidak terkait dengan PERTANYAAN | |
(2) Jika fakta mengandung kata kunci APAPUN atau makna semantik terkait pertanyaan, anggap relevan | |
(3) Tidak apa-apa jika fakta memiliki BEBERAPA informasi yang tidak terkait dengan pertanyaan selama (2) terpenuhi | |
Relevansi: | |
Nilai relevansi True berarti FAKTA mengandung kata kunci APAPUN atau makna semantik terkait PERTANYAAN. | |
Nilai relevansi False berarti FAKTA sama sekali tidak terkait dengan PERTANYAAN. | |
Jelaskan penalaran Anda secara bertahap untuk memastikan penalaran dan kesimpulan benar. | |
Hindari menyebutkan jawaban benar di awal.""" | |
# === Evaluator === | |
class SafeLLMEvaluator: | |
def __init__(self, model_name="gpt-4o", temperature=0): | |
self.model_name = model_name | |
self.temperature = temperature | |
self._init_llms() | |
def _init_llms(self): | |
self.grader_llm = ChatOpenAI(model=self.model_name, temperature=self.temperature).with_structured_output(CorrectnessGrade, method="json_schema", strict=True) | |
self.relevance_llm = ChatOpenAI(model=self.model_name, temperature=self.temperature).with_structured_output(RelevanceGrade, method="json_schema", strict=True) | |
self.grounded_llm = ChatOpenAI(model=self.model_name, temperature=self.temperature).with_structured_output(GroundedGrade, method="json_schema", strict=True) | |
self.retrieval_relevance_llm = ChatOpenAI(model=self.model_name, temperature=self.temperature).with_structured_output(RetrievalRelevanceGrade, method="json_schema", strict=True) | |
logger.info(f"β LLM evaluators initialized with model: {self.model_name}") | |
evaluator = SafeLLMEvaluator() | |
# === Rate Limiting & Retry === | |
def safe_api_call(llm, messages): | |
return llm.invoke(messages) | |
# === RAG Chain === | |
def create_rag_chain(vector_store): | |
llm = Replicate( | |
model="fauziisyrinapridal/sahabat-ai-v1:afb9fa89fe786362f619fd4fef34bd1f7a4a4da23073d8a6fbf54dcbe458f216", | |
model_kwargs={"temperature": 0.1, "top_p": 0.9, "max_new_tokens": 4000}, | |
replicate_api_token=os.getenv("REPLICATE_API_TOKEN"), | |
) | |
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True, output_key='answer') | |
chain = ConversationalRetrievalChain.from_llm( | |
llm, retriever=vector_store.as_retriever(search_kwargs={"k": 4}), | |
combine_docs_chain_kwargs={"prompt": sahabat_prompt}, | |
return_source_documents=True, memory=memory | |
) | |
return chain | |
def rag_bot_answer(question: str, vector_store) -> dict: | |
chain = create_rag_chain(vector_store) | |
result = chain({"question": question}) | |
return {"answer": result['answer'], "documents": result.get('source_documents', [])} | |
# === Evaluator Functions === | |
def correctness_evaluator(question, answer, ground_truth): | |
messages = [{"role": "system", "content": correctness_instructions}, | |
{"role": "user", "content": f"PERTANYAAN: {question}\nJAWABAN BENAR: {ground_truth}\nJAWABAN SISWA: {answer}"}] | |
grade = safe_api_call(evaluator.grader_llm, messages) | |
return grade["correct"], grade["explanation"] | |
def relevance_evaluator(question, answer): | |
messages = [{"role": "system", "content": relevance_instructions}, | |
{"role": "user", "content": f"PERTANYAAN: {question}\nJAWABAN SISWA: {answer}"}] | |
grade = safe_api_call(evaluator.relevance_llm, messages) | |
return grade["relevant"], grade["explanation"] | |
def groundedness_evaluator(answer, documents): | |
doc_string = "\n\n".join([doc.page_content for doc in documents]) | |
messages = [{"role": "system", "content": grounded_instructions}, | |
{"role": "user", "content": f"FAKTA: {doc_string}\nJAWABAN SISWA: {answer}"}] | |
grade = safe_api_call(evaluator.grounded_llm, messages) | |
return grade["grounded"], grade["explanation"] | |
def retrieval_relevance_evaluator(question, documents): | |
doc_string = "\n\n".join([doc.page_content for doc in documents]) | |
messages = [{"role": "system", "content": retrieval_relevance_instructions}, | |
{"role": "user", "content": f"FAKTA: {doc_string}\nPERTANYAAN: {question}"}] | |
grade = safe_api_call(evaluator.retrieval_relevance_llm, messages) | |
return grade["relevant"], grade["explanation"] | |
# === Delay helper === | |
def controlled_delay(min_delay=1, max_delay=3): | |
time.sleep(random.uniform(min_delay, max_delay)) | |
# === Evaluation Runner === | |
def run_enhanced_evaluation(): | |
logger.info("π Starting evaluation...") | |
vector_store = load_vector_store_from_supabase(supabase, BUCKET_NAME, VECTOR_STORE_PREFIX) | |
results = [] | |
for idx, item in enumerate(evaluation_dataset, 1): | |
question = item["question"] | |
ground_truth = item["ground_truth"] | |
try: | |
rag_result = rag_bot_answer(question, vector_store) | |
answer = rag_result["answer"] | |
documents = rag_result["documents"] | |
correctness, correctness_exp = correctness_evaluator(question, answer, ground_truth) | |
relevance, relevance_exp = relevance_evaluator(question, answer) | |
grounded, grounded_exp = groundedness_evaluator(answer, documents) | |
retrieval, retrieval_exp = retrieval_relevance_evaluator(question, documents) | |
results.append({ | |
"question": question, | |
"answer": answer, | |
"correctness": correctness, | |
"correctness_explanation": correctness_exp, | |
"relevance": relevance, | |
"relevance_explanation": relevance_exp, | |
"groundedness": grounded, | |
"groundedness_explanation": grounded_exp, | |
"retrieval_relevance": retrieval, | |
"retrieval_explanation": retrieval_exp, | |
}) | |
logger.info(f"[{idx}] β Done: {question[:50]}...") | |
except Exception as e: | |
logger.error(f"β Error on Q{idx}: {e}") | |
results.append({ | |
"question": question, | |
"answer": "ERROR", | |
"correctness": False, | |
"correctness_explanation": str(e), | |
"relevance": False, | |
"relevance_explanation": str(e), | |
"groundedness": False, | |
"groundedness_explanation": str(e), | |
"retrieval_relevance": False, | |
"retrieval_explanation": str(e), | |
}) | |
controlled_delay() | |
logger.info("π― Evaluation finished") | |
return results | |
# === Jalankan saat script dieksekusi langsung === | |
if __name__ == "__main__": | |
run_enhanced_evaluation() | |