From b4e0635329d61fc4aa8f0578e389cd10c3638522 Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Mon, 9 Jun 2025 10:51:41 +0700 Subject: [PATCH] create: add detection file using fastapi for prompting on ollama model --- .../__pycache__/main.cpython-310.pyc | Bin 4115 -> 5539 bytes .../__pycache__/utils_file.cpython-310.pyc | Bin 0 -> 1524 bytes Model LLM/fastapi-llama/main.py | 559 +++++------------- Model LLM/fastapi-llama/utils_file.py | 60 ++ 4 files changed, 207 insertions(+), 412 deletions(-) create mode 100644 Model LLM/fastapi-llama/__pycache__/utils_file.cpython-310.pyc create mode 100644 Model LLM/fastapi-llama/utils_file.py diff --git a/Model LLM/fastapi-llama/__pycache__/main.cpython-310.pyc b/Model LLM/fastapi-llama/__pycache__/main.cpython-310.pyc index c1f4e699667635a57605954faba9590abe0473c5..51f5a570d67d72dcc065d215ed0bed014bc0012b 100644 GIT binary patch literal 5539 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 as9LxR!#rF-A%cT%!U@?^60;2&4hpe literal 0 HcmV?d00001 diff --git a/Model LLM/fastapi-llama/main.py b/Model LLM/fastapi-llama/main.py index 0ea767f..92c14b0 100644 --- a/Model LLM/fastapi-llama/main.py +++ b/Model LLM/fastapi-llama/main.py @@ -1,442 +1,177 @@ -# from fastapi import FastAPI, HTTPException -# from fastapi.middleware.cors import CORSMiddleware -# import httpx -# import logging -# import traceback -# import random # Untuk memilih cerita secara acak - -# app = FastAPI() - -# OLLAMA_URL = "http://192.168.60.92:11434/api/generate" - -# # Konfigurasi Logging -# logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") - -# # Data ringkasan cerita -# summaries = { -# "Malin Kundang": "Malin Kundang adalah seorang anak dari keluarga miskin yang menjadi kaya raya namun menolak mengakui ibunya, hingga akhirnya dikutuk menjadi batu.", -# "Bawang Merah Bawang Putih": "Bawang Putih adalah gadis baik hati yang diperlakukan buruk oleh ibu dan saudara tirinya, tetapi kebaikannya membuahkan hasil berkat ikan ajaib.", -# "Sangkuriang": "Sangkuriang jatuh cinta pada ibunya, Dayang Sumbi, dan diberi tugas mustahil untuk membangun perahu dalam satu malam. Ia gagal dan akhirnya marah, menendang perahu hingga menjadi Gunung Tangkuban Perahu.", -# "Si Kancil": "Si Kancil dengan kecerdikannya berhasil menipu buaya untuk menyeberangi sungai dengan aman." -# } - -# @app.get("/generate/") -# async def generate_text(): -# try: -# # Pilih cerita secara acak -# selected_story = random.choice(list(summaries.keys())) -# story_summary = summaries[selected_story] - -# # Buat prompt untuk Ollama -# prompt = f"""Buatlah tiga soal literasi dan jawabannya berdasarkan ringkasan cerita berikut: - -# Cerita: -# {story_summary} - -# Format output: -# Soal 1: [Tulis soal pertama di sini] -# Jawaban: [Tulis jawaban pertama di sini] - -# Soal 2: [Tulis soal kedua di sini] -# Jawaban: [Tulis jawaban kedua di sini] - -# Soal 3: [Tulis soal ketiga di sini] -# Jawaban: [Tulis jawaban ketiga di sini] -# """ - -# payload = { -# "model": "llama3.1:latest", -# "prompt": prompt, -# "stream": False -# } - -# logging.info(f"Sending request to Ollama: {payload}") - -# # Kirim request ke Ollama dengan timeout -# async with httpx.AsyncClient(timeout=30) as client: -# response = await client.post(OLLAMA_URL, json=payload) - -# # Log response dari Ollama -# logging.info(f"Response status code: {response.status_code}") -# logging.info(f"Response content: {response.text}") - -# # Raise error jika status code bukan 2xx -# response.raise_for_status() - -# # Coba parse JSON response -# try: -# result = response.json() -# logging.info(f"Parsed response: {result}") -# return { -# "selected_story": selected_story, -# "generated_questions": result -# } -# except Exception as e: -# logging.error(f"Failed to parse JSON response: {response.text}") -# raise HTTPException(status_code=500, detail="Invalid response format from Ollama API") - -# except httpx.HTTPStatusError as e: -# logging.error(f"HTTP error from Ollama API: {e.response.text}") -# raise HTTPException(status_code=e.response.status_code, detail=e.response.text) - -# except Exception as e: -# error_trace = traceback.format_exc() # Dapatkan traceback lengkap -# logging.error(f"Unexpected error: {error_trace}") # Cetak error di log -# raise HTTPException(status_code=500, detail="Internal Server Error") - -# # Endpoint untuk mengecek koneksi ke Ollama -# @app.get("/test-connection/") -# async def test_connection(): -# try: -# async with httpx.AsyncClient(timeout=10) as client: -# response = await client.get("http://192.168.60.92:11434") -# return {"status": response.status_code, "content": response.text} -# except Exception as e: -# logging.error(f"Connection test failed: {str(e)}") -# return {"error": str(e)} - -# app = FastAPI() - -# OLLAMA_URL = "http://192.168.60.92:11434/api/generate" - -# logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") - -# class GenerateRequest(BaseModel): -# content: str # Menerima teks bebas, bukan content_id - -# @app.post("/generate/") -# async def generate_text(request: GenerateRequest): -# try: -# if not request.content.strip(): -# raise HTTPException(status_code=400, detail="Content cannot be empty") - -# prompt = f"""Buatlah tiga soal literasi dan jawabannya berdasarkan teks berikut: - -# {request.content} - -# Format output: -# Soal 1: [Tulis soal pertama di sini] -# Jawaban: [Tulis jawaban pertama di sini] -# Jawaban: [Tulis jawaban kedua di sini] - -# Soal 2: [Tulis soal kedua di sini] -# Jawaban: [Tulis jawaban pertama di sini] -# Jawaban: [Tulis jawaban kedua di sini] - -# Soal 3: [Tulis soal ketiga di sini] -# Jawaban: [Tulis jawaban pertama di sini] -# Jawaban: [Tulis jawaban kedua di sini] -# """ - -# payload = { -# "model": "llama3.1:latest", -# "prompt": prompt, -# "stream": False -# } - -# async with httpx.AsyncClient(timeout=30) as client: -# response = await client.post(OLLAMA_URL, json=payload) -# response.raise_for_status() -# result = response.json() -# return result - -# except Exception as e: -# logging.error(f"Error: {traceback.format_exc()}") -# raise HTTPException(status_code=500, detail="Internal Server Error") - -# from fastapi import FastAPI, HTTPException -# from pydantic import BaseModel -# import httpx -# import logging -# import traceback - -# app = FastAPI() - -# OLLAMA_URL = "http://192.168.60.92:11434/api/generate" - -# class GenerateRequest(BaseModel): -# content: str = "Buatlah soal literasi berdasarkan teks anak-anak tentang lingkungan." - -# @app.post("/generate/") -# async def generate_text(request: GenerateRequest): -# # Prompt default jika kosong -# prompt = f"""Buatlah tiga soal literasi berdasarkan teks berikut: - -# {request.content} - -# Format output: -# Soal 1: [Tulis soal pertama di sini] -# Jawaban: [Tulis jawaban pertama di sini] - -# Soal 2: [Tulis soal kedua di sini] -# Jawaban: [Tulis jawaban kedua di sini] - -# Soal 3: [Tulis soal ketiga di sini] -# Jawaban: [Tulis jawaban ketiga di sini] -# """ - -# payload = { -# "model": "llama3.1:latest", -# "prompt": prompt, -# "stream": False -# } - -# async with httpx.AsyncClient() as client: -# response = await client.post(OLLAMA_URL, json=payload) -# response.raise_for_status() -# return response.json() - from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware -import httpx -import logging -import random +from pydantic import BaseModel +import os, logging, httpx, hashlib +from utils_file import read_text_from_file + +# Logging setup +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") app = FastAPI() app.add_middleware( CORSMiddleware, - allow_origins=["*"], # Ubah sesuai kebutuhan jika ada pembatasan domain + allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) -OLLAMA_URL = "http://localhost:11434/api/generate" +# Path lokal file materi +MATERIAL_BASE_PATH = r"D:\Projek Skripsi\Penilaian Literasi\iClOP-V2\storage\app\public" +OLLAMA_URL = "http://labai.polinema.ac.id:11434/api/generate" -# Konfigurasi Logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +# Request model +class FileMaterialRequest(BaseModel): + file_name: str + question_type: str + question_count: int + start_page: int = 10 -# Data ringkasan cerita, pantun, dan puisi (bisa ditambah lagi) -data_sources = { - "cerita": { - "Malin Kundang": "Malin Kundang adalah seorang anak dari keluarga miskin yang menjadi kaya raya namun menolak mengakui ibunya, hingga akhirnya dikutuk menjadi batu.", - "Bawang Merah Bawang Putih": "Bawang Putih adalah gadis baik hati yang diperlakukan buruk oleh ibu dan saudara tirinya, tetapi kebaikannya membuahkan hasil berkat ikan ajaib.", - "Sangkuriang": "Sangkuriang jatuh cinta pada ibunya, Dayang Sumbi, dan diberi tugas mustahil untuk membangun perahu dalam satu malam. Ia gagal dan akhirnya marah, menendang perahu hingga menjadi Gunung Tangkuban Perahu.", - "Si Kancil": "Si Kancil dengan kecerdikannya berhasil menipu buaya untuk menyeberangi sungai dengan aman.", - "Timun Mas": "Seorang ibu tua mendapatkan anak dari biji timun emas. Namun, anak itu harus melarikan diri dari raksasa jahat yang ingin memakannya." - }, - "pantun": { - "Pantun Nasihat": "Jalan-jalan ke kota Blitar,\nJangan lupa membeli roti.\nRajin belajar sejak pintar,\nAgar sukses di kemudian hari.", - "Pantun Jenaka": "Ke pasar beli ikan teri,\nIkan habis tinggal kepala.\nJangan suka mencuri,\nNanti ketahuan malah celaka." - }, - "puisi": { - "Puisi Alam": "Langit biru membentang luas,\nBurung-burung terbang bebas.\nAngin sepoi menyapu dedaunan,\nAlam indah penuh kedamaian.", - "Puisi Persahabatan": "Sahabat sejati selalu ada,\nDalam suka dan dalam duka.\nBersama kita jalani hari,\nMengukir cerita tak terlupa." - } -} +class FeedbackRequest(BaseModel): + user_answer: str + expected_answer: str -@app.post("/generate/") -async def generate_text(): +feedback_cache = {} + +def potong_konten(text: str, max_chars: int = 5000): + return text[:max_chars] if len(text) > max_chars else text + +@app.post("/generate-from-file/") +async def generate_from_file(request: FileMaterialRequest): try: - # Pilih 3 cerita, 1 pantun, dan 1 puisi secara acak - selected_stories = random.sample(list(data_sources["cerita"].keys()), 3) - selected_pantun = random.choice(list(data_sources["pantun"].keys())) - selected_puisi = random.choice(list(data_sources["puisi"].keys())) - - # Buat format prompt dengan cerita, pantun, dan puisi - story_prompts = "\n\n".join([ - f"**{story}**\n\n{data_sources['cerita'][story]}\n\nBerdasarkan cerita ini, buatlah **3 soal literasi** dalam format pilihan ganda." - for story in selected_stories - ]) - - pantun_prompt = f"**{selected_pantun}**\n\n{data_sources['pantun'][selected_pantun]}\n\nBerdasarkan pantun ini, buatlah **1 soal literasi** dalam format pilihan ganda." - - puisi_prompt = f"**{selected_puisi}**\n\n{data_sources['puisi'][selected_puisi]}\n\nBerdasarkan puisi ini, buatlah **1 soal literasi** dalam format pilihan ganda." - - # Gabungkan semua prompt - full_prompt = f""" - Buatlah soal berdasarkan teks berikut ini: - - {story_prompts} - - {pantun_prompt} - - {puisi_prompt} - - Pastikan soal yang dibuat beragam, berbobot untuk siswa SD, dan tidak hanya berasal dari satu jenis teks. - """ + if request.question_count < 1 or request.question_count > 20: + raise HTTPException(status_code=400, detail="Jumlah soal harus antara 1–20") - payload = { - "model": "llama3.1:latest", - "prompt": full_prompt, - "stream": False - } + if request.question_type not in ["multiple_choice", "essay"]: + raise HTTPException(status_code=400, detail="Jenis soal tidak valid. Pilih: multiple_choice atau essay") - logging.info(f"Mengirim permintaan ke Ollama: {payload}") + 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 - # Kirim request ke Ollama - async with httpx.AsyncClient(timeout=60) as client: - response = await client.post(OLLAMA_URL, json=payload) + 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}") - # Log response dari Ollama - logging.info(f"Response status code: {response.status_code}") - logging.info(f"Response content: {response.text}") + # 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.") + + content_bersih = potong_konten(text.strip()) + + prompt = f""" +Buat soal latihan berdasarkan teks materi berikut untuk siswa SD kelas 3. + +**Instruksi:** +1. Buat total {request.question_count} soal dengan rincian: + - Soal pilihan ganda: {mc_count} + - Soal isian: {essay_count} + +2. **Untuk soal isian:** + - Ambil kutipan **minimal dua kalimat yang saling terhubung** dari teks sebagai dasar soal + - Awali dengan: **Bacalah kutipan berikut: "[kutipan]"** + - Buat pertanyaan berdasarkan kutipan tersebut + - Sertakan **jawaban singkat** + - Beri bobot antara **3–5** sesuai kompleksitas + +3. **Untuk soal pilihan ganda (jika ada):** + - Ambil kutipan **1 kalimat yang relevan** dari teks + - Buat pertanyaan dan 4 pilihan jawaban (A–D) + - Beri jawaban benar dan bobot antara **1–2** sesuai tingkat kesulitan + +4. Gunakan bahasa sederhana dan sesuai dengan siswa SD kelas 3. + +5. Jangan menambahkan informasi di luar teks materi. + +**Format Output:** +""" + (""" +**Soal Pilihan Ganda:** +1. Bacalah kutipan berikut: "[2 kalimat atau lebih dari teks]" + Pertanyaan: [Pertanyaan] + A. [Opsi A] + B. [Opsi B] + C. [Opsi C] + D. [Opsi D] + Jawaban: [Huruf Opsi] + Bobot: [1 atau 2] +""" if mc_count > 0 else "") + (""" + +**Soal Isian:** +1. Bacalah kutipan berikut: "[2 kalimat atau lebih dari teks]". [Pertanyaan] + Jawaban: [Jawaban] + Bobot: [3 - 5] +""" if essay_count > 0 else "") + f""" + +--- + +**Materi:** +{content_bersih} +""".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 + } + }) - # Raise error jika status code bukan 2xx response.raise_for_status() - - # Ambil hasil respon dan parsing JSON result = response.json() - - # Pastikan hasil tidak kosong generated_text = result.get("response", "").strip() - if not generated_text: - raise HTTPException(status_code=500, detail="Ollama tidak menghasilkan pertanyaan") return { - "selected_stories": selected_stories, - "selected_pantun": selected_pantun, - "selected_puisi": selected_puisi, - "generated_questions": generated_text + "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 httpx.HTTPStatusError as e: - logging.error(f"HTTP error dari Ollama API: {e.response.text}") - raise HTTPException(status_code=e.response.status_code, detail=e.response.text) + 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() + + prompt_hash = hashlib.sha256(f"{user_answer}|{expected_answer}".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. Siswa memberikan jawaban berikut untuk soal isian. + +**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=60) 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"Terjadi kesalahan: {str(e)}") - raise HTTPException(status_code=500, detail="Terjadi kesalahan internal") - -# from fastapi import FastAPI, HTTPException -# from fastapi.middleware.cors import CORSMiddleware -# import httpx -# import logging -# import random -# import re -# import traceback - -# app = FastAPI() - -# app.add_middleware( -# CORSMiddleware, -# allow_origins=["*"], -# allow_credentials=True, -# allow_methods=["*"], -# allow_headers=["*"], -# ) - -# OLLAMA_URL = "http://192.168.60.92:11434/api/generate" - -# # Konfigurasi Logging -# logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") - -# # Data ringkasan cerita, pantun, dan puisi -# data_sources = { -# "cerita": { -# "Malin Kundang": "Malin Kundang adalah seorang anak dari keluarga miskin yang menjadi kaya raya namun menolak mengakui ibunya, hingga akhirnya dikutuk menjadi batu.", -# "Bawang Merah Bawang Putih": "Bawang Putih adalah gadis baik hati yang diperlakukan buruk oleh ibu dan saudara tirinya, tetapi kebaikannya membuahkan hasil berkat ikan ajaib.", -# "Sangkuriang": "Sangkuriang jatuh cinta pada ibunya, Dayang Sumbi, dan diberi tugas mustahil untuk membangun perahu dalam satu malam. Ia gagal dan akhirnya marah, menendang perahu hingga menjadi Gunung Tangkuban Perahu.", -# "Si Kancil": "Si Kancil dengan kecerdikannya berhasil menipu buaya untuk menyeberangi sungai dengan aman.", -# "Timun Mas": "Seorang ibu tua mendapatkan anak dari biji timun emas. Namun, anak itu harus melarikan diri dari raksasa jahat yang ingin memakannya." -# }, -# "pantun": { -# "Pantun Nasihat": "Jalan-jalan ke kota Blitar,\nJangan lupa membeli roti.\nRajin belajar sejak pintar,\nAgar sukses di kemudian hari.", -# "Pantun Jenaka": "Ke pasar beli ikan teri,\nIkan habis tinggal kepala.\nJangan suka mencuri,\nNanti ketahuan malah celaka." -# }, -# "puisi": { -# "Puisi Alam": "Langit biru membentang luas,\nBurung-burung terbang bebas.\nAngin sepoi menyapu dedaunan,\nAlam indah penuh kedamaian.", -# "Puisi Persahabatan": "Sahabat sejati selalu ada,\nDalam suka dan dalam duka.\nBersama kita jalani hari,\nMengukir cerita tak terlupa." -# } -# } - -# @app.post("/generate/") -# async def generate_text(): -# try: -# # Pilih 3 cerita, 1 pantun, dan 1 puisi secara acak -# selected_stories = random.sample(list(data_sources["cerita"].keys()), 3) -# selected_pantun = random.choice(list(data_sources["pantun"].keys())) -# selected_puisi = random.choice(list(data_sources["puisi"].keys())) - -# # Buat format prompt dengan cerita, pantun, dan puisi -# story_prompts = "\n\n".join([ -# f"**{story}**\n\n{data_sources['cerita'][story]}\n\nBerdasarkan cerita ini, buatlah **3 soal literasi** dalam format pilihan ganda." -# for story in selected_stories -# ]) - -# pantun_prompt = f"**{selected_pantun}**\n\n{data_sources['pantun'][selected_pantun]}\n\nBerdasarkan pantun ini, buatlah **1 soal literasi** dalam format pilihan ganda." - -# puisi_prompt = f"**{selected_puisi}**\n\n{data_sources['puisi'][selected_puisi]}\n\nBerdasarkan puisi ini, buatlah **1 soal literasi** dalam format pilihan ganda." - -# # Gabungkan semua prompt -# full_prompt = f""" -# Buatlah soal berdasarkan teks berikut ini: - -# {story_prompts} - -# {pantun_prompt} - -# {puisi_prompt} - -# Format setiap soal: -# --- -# **Pertanyaan** -# A. Pilihan 1 -# B. Pilihan 2 -# C. Pilihan 3 -# D. Pilihan 4 -# Jawaban: (A/B/C/D) -# --- -# """ - -# payload = { -# "model": "llama3.1:latest", -# "prompt": full_prompt, -# "stream": False -# } - -# logging.info(f"Mengirim permintaan ke Ollama: {payload}") - -# # Kirim request ke Ollama -# async with httpx.AsyncClient(timeout=60) as client: -# response = await client.post(OLLAMA_URL, json=payload) - -# logging.info(f"Response status code: {response.status_code}") -# logging.info(f"Response content: {response.text}") - -# response.raise_for_status() -# result = response.json() - -# generated_text = result.get("response", "").strip() -# if not generated_text: -# raise HTTPException(status_code=500, detail="Ollama tidak menghasilkan pertanyaan") - -# # Parsing hasil teks menjadi daftar soal -# questions = [] -# raw_questions = re.split(r'\n\s*\n', generated_text) - -# for raw in raw_questions: -# lines = raw.strip().split("\n") -# if len(lines) >= 6: -# question_text = lines[0].strip() -# options = [f"({opt[0]}) {opt[3:].strip()}" for opt in lines[1:5] if len(opt) > 3] - -# # Ambil jawaban dengan regex -# answer_match = re.search(r'Jawaban:\s*\(?([A-D])\)?', raw) -# correct_answer = f"({answer_match.group(1)})" if answer_match else "Tidak ditemukan" - -# questions.append({ -# "question": question_text, -# "options": options, -# "correct_answer": correct_answer -# }) - -# if not questions: -# raise HTTPException(status_code=500, detail="Parsing soal gagal, format tidak sesuai.") - -# return { -# "selected_stories": selected_stories, -# "selected_pantun": selected_pantun, -# "selected_puisi": selected_puisi, -# "generated_questions": questions -# } - -# except httpx.HTTPStatusError as e: -# logging.error(f"HTTP error dari Ollama API: {e.response.text}") -# raise HTTPException(status_code=e.response.status_code, detail=e.response.text) - -# except Exception as e: -# logging.error(f"Terjadi kesalahan: {str(e)}") -# logging.error(traceback.format_exc()) # Cetak stack trace lengkap -# raise HTTPException(status_code=500, detail="Terjadi kesalahan internal") \ No newline at end of file + logging.error(f"Error saat generate feedback: {e}") + raise HTTPException(status_code=500, detail=f"Terjadi kesalahan: {str(e)}") \ No newline at end of file diff --git a/Model LLM/fastapi-llama/utils_file.py b/Model LLM/fastapi-llama/utils_file.py new file mode 100644 index 0000000..afac6e7 --- /dev/null +++ b/Model LLM/fastapi-llama/utils_file.py @@ -0,0 +1,60 @@ +import os +from docx import Document +import fitz + +def read_text_from_file(filepath: str, start_page: int = 0, max_chars: int = 3000) -> str: + ext = os.path.splitext(filepath)[1].lower() + + try: + if ext == '.pdf': + doc = fitz.open(filepath) + if start_page >= len(doc): + return "" + text = "" + for page_num in range(start_page, len(doc)): + text += doc[page_num].get_text() + if len(text) >= max_chars: + break + return text[:max_chars] + + elif ext == '.docx': + doc = Document(filepath) + paragraphs = [para.text.strip() for para in doc.paragraphs if para.text.strip()] + if not paragraphs: + return "" + + para_per_page = 20 + if start_page > 0: + start_index = start_page * para_per_page + if start_index >= len(paragraphs): + return "" + selected_paragraphs = paragraphs[start_index:] + else: + selected_paragraphs = paragraphs + + combined_text = "\n".join(selected_paragraphs) + return combined_text[:max_chars] + + elif ext == '.txt': + with open(filepath, 'r', encoding='utf-8') as f: + lines = f.readlines() + if not lines: + return "" + + lines_per_page = 40 + if start_page > 0: + start_index = start_page * lines_per_page + if start_index >= len(lines): + return "" + selected_lines = lines[start_index:] + else: + selected_lines = lines + + combined_text = "".join(selected_lines) + return combined_text[:max_chars] + + else: + raise Exception(f"Format file tidak didukung: {ext}") + + except Exception as e: + raise Exception(f"Gagal membaca file {ext.upper()}: {e}")