261 lines
8.1 KiB
Python
261 lines
8.1 KiB
Python
from fastapi import FastAPI, HTTPException
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from pydantic import BaseModel
|
||
import os, logging, httpx, hashlib
|
||
from utils_file import read_text_from_file
|
||
from pathlib import Path
|
||
|
||
# Logging setup
|
||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||
|
||
app = FastAPI()
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# Path lokal file materi
|
||
BASE_DIR = Path(__file__).resolve().parent
|
||
MATERIAL_BASE_PATH = (BASE_DIR.parent.parent / "Penilaian Literasi" / "iCLOP-V2" / "storage" / "app" / "public")
|
||
OLLAMA_URL = "http://labai.polinema.ac.id:11434/api/generate"
|
||
|
||
logging.info(f"📂 Path materi: {MATERIAL_BASE_PATH}")
|
||
|
||
# Request model
|
||
class FileMaterialRequest(BaseModel):
|
||
file_name: str
|
||
question_type: str
|
||
question_count: int
|
||
start_page: int = 24
|
||
|
||
class FeedbackRequest(BaseModel):
|
||
user_answer: str
|
||
expected_answer: str
|
||
question_text: str
|
||
|
||
feedback_cache = {}
|
||
|
||
@app.post("/generate-from-file/")
|
||
async def generate_from_file(request: FileMaterialRequest):
|
||
try:
|
||
if request.question_count < 1 or request.question_count > 20:
|
||
raise HTTPException(status_code=400, detail="Jumlah soal harus antara 1–20")
|
||
|
||
if request.question_type not in ["multiple_choice", "essay"]:
|
||
raise HTTPException(status_code=400, detail="Jenis soal tidak valid. Pilih: multiple_choice atau essay")
|
||
|
||
mc_count = request.question_count if request.question_type == "multiple_choice" else 0
|
||
essay_count = request.question_count if request.question_type == "essay" else 0
|
||
|
||
file_path = os.path.join(MATERIAL_BASE_PATH, request.file_name)
|
||
if not os.path.exists(file_path):
|
||
raise HTTPException(status_code=404, detail=f"File tidak ditemukan: {file_path}")
|
||
|
||
# Baca isi file mulai dari halaman tertentu
|
||
text = read_text_from_file(file_path, start_page=request.start_page)
|
||
if not text.strip():
|
||
raise HTTPException(status_code=400, detail="Isi file kosong atau tidak terbaca.")
|
||
|
||
# ==== PROMPT INSTRUKSI BERDASARKAN JENIS SOAL ====
|
||
if request.question_type == "multiple_choice":
|
||
soal_instruksi = f"""
|
||
1. Ambil satu paragraf panjang dari teks.
|
||
2. Buat **{mc_count} soal pilihan ganda** dari paragraf tersebut.
|
||
3. Soal harus berdasarkan informasi yang tertulis langsung dalam paragraf (eksplisit).
|
||
4. Hindari soal yang:
|
||
- Mengandung simpulan di luar isi
|
||
- Menambahkan tokoh, nama, atau kejadian di luar paragraf
|
||
- Jawabannya tidak bisa ditemukan dari paragraf
|
||
5. Topik soal mencakup:
|
||
- Ide pokok paragraf
|
||
- Kalimat utama
|
||
- Sinonim atau antonim (jika ada kata yang mendukung)
|
||
- Informasi eksplisit dari kalimat dalam paragraf
|
||
6. Gunakan bahasa sederhana, sesuai siswa SD kelas 3–4.
|
||
7. Bobot setiap soal **1–2**, berdasarkan tingkat kesulitan.
|
||
8. Gunakan format berikut:
|
||
|
||
**Paragraf:**
|
||
"[Isi paragraf]"
|
||
|
||
**Soal Pilihan Ganda:**
|
||
1. [Pertanyaan]
|
||
A. ...
|
||
B. ...
|
||
C. ...
|
||
D. ...
|
||
Jawaban: ...
|
||
Bobot: ...
|
||
|
||
---
|
||
|
||
✅ Contoh soal:
|
||
|
||
**Paragraf:**
|
||
"Pak Budi memelihara ayam, bebek, dan kambing. Setiap pagi, ia memberi makan ternaknya dengan penuh kasih sayang."
|
||
|
||
**Contoh Soal Pilihan Ganda:**
|
||
1. Apa hewan yang dipelihara Pak Budi?
|
||
A. Kucing
|
||
B. Ayam dan kambing
|
||
C. Anjing dan bebek
|
||
D. Ikan dan sapi
|
||
Jawaban: B
|
||
Bobot: 1
|
||
|
||
2. Apa sinonim dari kata 'ternak' dalam paragraf tersebut?
|
||
A. Hewan peliharaan
|
||
B. Sayuran
|
||
C. Alat tani
|
||
D. Makanan ternak
|
||
Jawaban: A
|
||
Bobot: 2
|
||
""".strip()
|
||
|
||
elif request.question_type == "essay":
|
||
soal_instruksi = f"""
|
||
1. Ambil satu paragraf panjang dari teks.
|
||
2. Buat **{essay_count} soal isian** dari paragraf tersebut.
|
||
3. Soal harus berasal dari informasi eksplisit yang tertulis dalam paragraf.
|
||
4. Hindari soal yang:
|
||
- Mengandung penalaran atau simpulan dari luar isi
|
||
- Mengandung tokoh, tempat, atau kejadian tambahan
|
||
5. Topik soal mencakup:
|
||
- Ide pokok paragraf
|
||
- Kalimat penting
|
||
- Karakter tokoh
|
||
- Sinonim atau antonim (jika tersedia dalam paragraf)
|
||
6. Gunakan bahasa sederhana, sesuai siswa SD kelas 3–4.
|
||
7. **Ingat: Bobot soal isian HARUS antara 3 hingga 5. Tidak boleh menggunakan bobot 1 atau 2.**
|
||
8. Gunakan format berikut:
|
||
|
||
**Paragraf:**
|
||
"[Isi paragraf]"
|
||
|
||
**Soal Isian:**
|
||
1. [Pertanyaan]
|
||
Jawaban: ...
|
||
Bobot: [3, 4, atau 5 saja — bukan angka lain]
|
||
|
||
---
|
||
|
||
✅ Contoh soal (ikuti strukturnya):
|
||
|
||
**Paragraf:**
|
||
"Pak Ali menanam padi di sawah setiap musim tanam. Ia bekerja keras agar panennya berhasil."
|
||
|
||
**Contoh Soal Isian:**
|
||
1. Apa yang ditanam Pak Ali di sawah?
|
||
Jawaban: Padi
|
||
Bobot: 3
|
||
|
||
2. Mengapa Pak Ali bekerja keras?
|
||
Jawaban: Agar panennya berhasil
|
||
Bobot: 4
|
||
|
||
3. Apa sinonim dari kata 'bekerja keras' dalam paragraf tersebut?
|
||
Jawaban: Rajin atau tekun
|
||
Bobot: 5
|
||
""".strip()
|
||
|
||
# ==== FINAL PROMPT ====
|
||
prompt = f"""
|
||
Kamu adalah asisten guru SD kelas 3 dan 4.
|
||
|
||
Tugasmu adalah membuat soal literasi dari teks di bawah ini. Soal HARUS berdasarkan isi teks, tanpa menambahkan informasi dari luar.
|
||
|
||
---
|
||
|
||
### 🎯 Tujuan:
|
||
Buat **{request.question_count} soal literasi** berdasarkan **satu paragraf** untuk masing-masing jenis soal.
|
||
|
||
---
|
||
|
||
### 📌 Instruksi Umum:
|
||
- Gunakan paragraf berbeda untuk soal pilihan ganda dan isian (jika kedua jenis digunakan).
|
||
- Jangan menambahkan nama tokoh, tempat, atau kejadian di luar isi paragraf.
|
||
- Bahasa harus sederhana dan mudah dipahami siswa SD.
|
||
|
||
---
|
||
|
||
### 📌 Instruksi Soal:
|
||
{soal_instruksi}
|
||
|
||
---
|
||
|
||
### 📚 Teks:
|
||
{text.strip()}
|
||
""".strip()
|
||
|
||
logging.info("Mengirim prompt ke Ollama...")
|
||
async with httpx.AsyncClient(timeout=120) as client:
|
||
response = await client.post(OLLAMA_URL, json={
|
||
"model": "llama3.1:latest",
|
||
"prompt": prompt,
|
||
"stream": False,
|
||
"options": {
|
||
"num_predict": 2048
|
||
}
|
||
})
|
||
|
||
response.raise_for_status()
|
||
result = response.json()
|
||
generated_text = result.get("response", "").strip()
|
||
|
||
return {
|
||
"generated_questions": generated_text,
|
||
"question_type": request.question_type,
|
||
"question_count": request.question_count,
|
||
"mc_count": mc_count,
|
||
"essay_count": essay_count,
|
||
"start_page": request.start_page
|
||
}
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error saat generate dari file: {e}")
|
||
raise HTTPException(status_code=500, detail=f"Terjadi kesalahan internal: {str(e)}")
|
||
|
||
@app.post("/generate-feedback/")
|
||
async def generate_feedback(request: FeedbackRequest):
|
||
try:
|
||
user_answer = request.user_answer.strip()
|
||
expected_answer = request.expected_answer.strip()
|
||
question_text = request.question_text.strip()
|
||
|
||
prompt_hash = hashlib.sha256(f"{user_answer}|{expected_answer}|{question_text}".encode()).hexdigest()
|
||
if prompt_hash in feedback_cache:
|
||
logging.info("Feedback dari cache.")
|
||
return {"feedback": feedback_cache[prompt_hash]}
|
||
|
||
prompt = f"""
|
||
Kamu adalah asisten pengajar untuk siswa SD kelas 3. Berikut ini adalah soal isian, jawaban siswa, dan jawaban ideal.
|
||
|
||
**Soal:** {question_text}
|
||
**Jawaban Siswa:** {user_answer}
|
||
**Jawaban Ideal:** {expected_answer}
|
||
|
||
Beri feedback singkat dan membangun, maksimal 2 kalimat. Gunakan bahasa yang mudah dimengerti oleh siswa SD. Jika jawaban siswa salah, berikan petunjuk atau koreksi yang membantu.
|
||
"""
|
||
|
||
logging.info("Mengirim prompt feedback ke Ollama...")
|
||
async with httpx.AsyncClient(timeout=300) as client:
|
||
response = await client.post(OLLAMA_URL, json={
|
||
"model": "llama3.1:latest",
|
||
"prompt": prompt,
|
||
"stream": False
|
||
})
|
||
|
||
response.raise_for_status()
|
||
result = response.json()
|
||
feedback = result.get("response", "").strip()
|
||
|
||
feedback_cache[prompt_hash] = feedback
|
||
return {"feedback": feedback}
|
||
|
||
except Exception as e:
|
||
logging.error(f"Error saat generate feedback: {e}")
|
||
raise HTTPException(status_code=500, detail=f"Terjadi kesalahan: {str(e)}") |