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"}