Files
espiritismo-rag-system/backend/app/main.py
T
lgcosta 8eeb6b89f4 fix: responder a partir dos livros com embeddings em português
Usa o modelo multilingual, ignora trechos curtos e o template do Llama 3.2
para a resposta sair em uma frase completa. O chat deixa a API livre durante
a geração, o upload indexa o PDF e os agendamentos não são gravados duas vezes.
2026-11-01 12:34:54 -03:00

178 lines
5.7 KiB
Python

import asyncio
import json
import os
import uuid
from typing import List
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from app.models import (
BookInfo,
ChatRequest,
ChatResponse,
ScheduleInfo,
ScheduleRequest,
UploadResponse,
)
from app.services.bot_service import BotService
from app.services.rag_service import QdrantService, RAGService
from app.utils.schedule_manager import ScheduleManager
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
SCHEDULES_PATH = os.getenv("SCHEDULES_PATH", "/app/schedules.json")
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/app/uploads")
QDRANT_URL = os.getenv("QDRANT_URL", "http://qdrant:6333")
MODEL_PATH = os.getenv("GPT4ALL_MODEL", "/app/models/llama-3.2-3b-instruct-q4_0.gguf")
qdrant_service: QdrantService | None = None
rag_service: RAGService | None = None
bot_service = BotService(os.getenv("TELEGRAM_BOT_TOKEN", ""), os.getenv("TELEGRAM_CHAT_ID", ""))
schedule_manager = ScheduleManager(SCHEDULES_PATH)
app = FastAPI(title="Espiritismo RAG API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.mount(
"/static",
StaticFiles(directory=os.path.join(BASE_DIR, "frontend", "static")),
name="static",
)
def get_qdrant() -> QdrantService:
global qdrant_service
if qdrant_service is None:
qdrant_service = QdrantService(QDRANT_URL)
return qdrant_service
def get_rag() -> RAGService:
global rag_service
if rag_service is None:
rag_service = RAGService(get_qdrant(), model_name=MODEL_PATH)
return rag_service
def load_schedules():
if os.path.isfile(SCHEDULES_PATH):
with open(SCHEDULES_PATH, "r", encoding="utf-8") as handle:
data = json.load(handle)
return data if isinstance(data, list) else []
return []
def index_pdf(file_path: str, filename: str):
from app.services.pdf_processor import PDFProcessor
from app.utils.embeddings import EmbeddingService
title = os.path.splitext(os.path.basename(filename))[0].replace("-", " ").replace("_", " ")
processor = PDFProcessor(EmbeddingService())
chunks = processor.embed_chunks(
processor.load_and_split(
file_path,
{"title": title, "author": "Desconhecido", "type": "book"},
)
)
for chunk in chunks:
chunk["id"] = str(uuid.uuid4())
get_qdrant().add_chunks(chunks)
def save_schedules(schedules: list):
parent = os.path.dirname(SCHEDULES_PATH)
if parent:
os.makedirs(parent, exist_ok=True)
with open(SCHEDULES_PATH, "w", encoding="utf-8") as handle:
json.dump(schedules, handle, indent=2)
handle.write("\n")
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/", response_class=HTMLResponse)
async def index():
template = os.path.join(BASE_DIR, "templates", "index.html")
with open(template, "r", encoding="utf-8") as handle:
return handle.read()
@app.get("/api/books", response_model=List[BookInfo])
async def get_books():
books = get_qdrant().get_books()
return [{"title": book, "source": book} for book in books]
@app.post("/api/chat", response_model=ChatResponse)
def chat(request: ChatRequest):
try:
return get_rag().query(request.message, request.top_k)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@app.post("/api/upload", response_model=UploadResponse)
async def upload_pdf(file: UploadFile = File(...)):
if not file.filename or not file.filename.endswith(".pdf"):
raise HTTPException(status_code=400, detail="Only PDF files are allowed")
os.makedirs(UPLOAD_DIR, exist_ok=True)
file_path = os.path.join(UPLOAD_DIR, os.path.basename(file.filename))
try:
content = await file.read()
with open(file_path, "wb") as handle:
handle.write(content)
await asyncio.to_thread(index_pdf, file_path, file.filename)
return UploadResponse(message="File uploaded and indexed", filename=file.filename)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@app.get("/api/schedules", response_model=List[ScheduleInfo])
async def get_schedules():
return [ScheduleInfo(**item) for item in load_schedules()]
@app.post("/api/schedules", response_model=ScheduleInfo)
async def create_schedule(request: ScheduleRequest):
new_schedule = schedule_manager.add_schedule(request.model_dump())
return ScheduleInfo(**new_schedule)
@app.put("/api/schedules/{schedule_id}", response_model=ScheduleInfo)
async def update_schedule(schedule_id: int, request: ScheduleRequest):
updated = schedule_manager.update_schedule(schedule_id, request.model_dump())
if updated is None:
raise HTTPException(status_code=404, detail="Schedule not found")
save_schedules(schedule_manager.list_schedules())
try:
bot_service.update_schedule(updated)
except Exception as exc:
print(f"Warning: Could not update schedule in bot: {exc}")
return ScheduleInfo(**updated)
@app.delete("/api/schedules/{schedule_id}")
async def delete_schedule(schedule_id: int):
current = schedule_manager.get_schedule(schedule_id)
if current is None or not schedule_manager.delete_schedule(schedule_id):
raise HTTPException(status_code=404, detail="Schedule not found")
save_schedules(schedule_manager.list_schedules())
try:
bot_service.remove_schedule(current)
except Exception as exc:
print(f"Warning: Could not remove schedule from bot: {exc}")
return {"message": "Schedule deleted"}