from __future__ import annotations from pathlib import Path from fastapi import BackgroundTasks, FastAPI, File, HTTPException, Query, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, PlainTextResponse from backend.app.config import settings from backend.app.managers.knowledge_manager import DuplicateSourceError, KnowledgeManager from backend.app.models.schemas import MarkdownUpdate, SubjectCreate, SubjectUpdate, WeekCreate, WeekUpdate from backend.app.services.ingestion_service import IngestionService app = FastAPI(title="Knowledge Station", version="0.1.0") app.add_middleware( CORSMiddleware, allow_origins=["http://127.0.0.1:3000", "http://localhost:3000"], allow_origin_regex=r"http://(127\.0\.0\.1|localhost):\d+", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) manager = KnowledgeManager() ingestion = IngestionService(manager) @app.get("/health") def health() -> dict[str, str]: return {"status": "ok"} @app.get("/settings/status") def settings_status() -> dict: return { "mistral_configured": bool(settings.mistral_api_key), "deepgram_configured": bool(settings.deepgram_api_key), "data_dir": str(settings.app_data_dir), "database_path": str(settings.database_path), } @app.post("/subjects") def create_subject(payload: SubjectCreate) -> dict: try: return manager.create_subject(payload.name) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @app.get("/subjects") def list_subjects() -> list[dict]: return manager.list_subjects() @app.get("/subjects/{subject_id}") def get_subject(subject_id: int) -> dict: try: return manager.get_subject(subject_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.put("/subjects/{subject_id}") def update_subject(subject_id: int, payload: SubjectUpdate) -> dict: try: return manager.update_subject(subject_id, payload.name) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @app.delete("/subjects/{subject_id}") def delete_subject(subject_id: int) -> dict: try: manager.delete_subject(subject_id) return {"status": "deleted"} except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.post("/subjects/{subject_id}/weeks") def create_week(subject_id: int, payload: WeekCreate) -> dict: try: return manager.create_week(subject_id, payload.number, payload.title) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/subjects/{subject_id}/weeks") def list_weeks(subject_id: int) -> list[dict]: try: manager.get_subject(subject_id) return manager.list_weeks(subject_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/subjects/{subject_id}/weeks/{week_number}") def get_week(subject_id: int, week_number: int) -> dict: try: return manager.get_week(subject_id, week_number) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.put("/subjects/{subject_id}/weeks/{week_number}") def update_week(subject_id: int, week_number: int, payload: WeekUpdate) -> dict: try: return manager.update_week(subject_id, week_number, payload.number, payload.title) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @app.delete("/subjects/{subject_id}/weeks/{week_number}") def delete_week(subject_id: int, week_number: int) -> dict: try: manager.delete_week(subject_id, week_number) return {"status": "deleted"} except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.post("/subjects/{subject_id}/weeks/{week_number}/files") def upload_file( subject_id: int, week_number: int, background_tasks: BackgroundTasks, file: UploadFile = File(...), use_ocr: bool = Query(False, description="Usar Mistral OCR para PDFs"), page_ranges: str | None = Query(None, description="Rangos de paginas PDF, por ejemplo: 1-5,8,10-12"), ) -> dict: try: source = manager.save_upload(subject_id, week_number, file) job = manager.create_job(source["id"], "Archivo recibido") background_tasks.add_task(process_job, job["id"], source["id"], use_ocr, page_ranges) return {"source": source, "job": job} except DuplicateSourceError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/jobs/{job_id}") def get_job(job_id: int) -> dict: try: return manager.get_job(job_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.post("/jobs/{job_id}/retry") def retry_job( job_id: int, background_tasks: BackgroundTasks, use_ocr: bool = Query(False), page_ranges: str | None = Query(None), ) -> dict: try: job = manager.get_job(job_id) if not job.get("source_id"): raise HTTPException(status_code=400, detail="El trabajo no tiene fuente asociada") manager.update_job(job_id, "queued", "Reintento en cola") background_tasks.add_task(process_job, job_id, job["source_id"], use_ocr, page_ranges) return manager.get_job(job_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/subjects/{subject_id}/weeks/{week_number}/jobs") def list_week_jobs(subject_id: int, week_number: int) -> list[dict]: try: return manager.list_jobs_for_week(subject_id, week_number) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/subjects/{subject_id}/weeks/{week_number}/sources") def list_sources(subject_id: int, week_number: int) -> list[dict]: try: return manager.list_sources(subject_id, week_number) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.delete("/sources/{source_id}") def delete_source(source_id: int) -> dict: try: manager.delete_source(source_id) return {"status": "deleted"} except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.post("/sources/{source_id}/reprocess") def reprocess_source( source_id: int, background_tasks: BackgroundTasks, use_ocr: bool = Query(False), page_ranges: str | None = Query(None), ) -> dict: try: source = manager.get_source(source_id) if manager.source_has_active_job(source_id): raise HTTPException(status_code=409, detail="La fuente ya tiene un trabajo en cola o en proceso") job = manager.create_job(source["id"], "Reprocesamiento en cola") background_tasks.add_task(process_job, job["id"], source["id"], use_ocr, page_ranges) return {"source": source, "job": job} except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/subjects/{subject_id}/weeks/{week_number}/documents") def list_documents(subject_id: int, week_number: int) -> list[dict]: try: return manager.list_documents(subject_id, week_number) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/documents/{document_id}") def get_document(document_id: int) -> dict: try: return manager.get_document(document_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/documents/{document_id}/markdown", response_class=PlainTextResponse) def get_document_markdown(document_id: int) -> str: try: document = manager.get_document(document_id) return Path(document["markdown_path"]).read_text(encoding="utf-8") except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.put("/documents/{document_id}/markdown") def update_document_markdown(document_id: int, payload: MarkdownUpdate) -> dict: try: return manager.update_document_markdown(document_id, payload.content) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/documents/{document_id}/download") def download_document(document_id: int) -> FileResponse: try: document = manager.get_document(document_id) path = Path(document["markdown_path"]) return FileResponse(path, filename=path.name, media_type="text/markdown") except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.delete("/documents/{document_id}") def delete_document(document_id: int) -> dict: try: manager.delete_document(document_id) return {"status": "deleted"} except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/subjects/{subject_id}/weeks/{week_number}/export") def export_week(subject_id: int, week_number: int) -> FileResponse: try: path = manager.export_week_markdown(subject_id, week_number) return FileResponse(path, filename=path.name, media_type="text/markdown") except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/subjects/{subject_id}/weeks/{week_number}/export.zip") def export_week_zip(subject_id: int, week_number: int) -> FileResponse: try: path = manager.export_week_markdown_zip(subject_id, week_number) return FileResponse(path, filename=path.name, media_type="application/zip") except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc def process_job(job_id: int, source_id: int, use_ocr: bool, page_ranges: str | None = None) -> None: manager.update_job(job_id, "processing", "Procesando archivo") manager.update_source_status(source_id, "processing") try: document = ingestion.process_source(source_id, use_ocr=use_ocr, page_ranges=page_ranges) manager.update_job(job_id, "completed", "Documento procesado", document_id=document["id"]) except Exception as exc: manager.update_source_status(source_id, "failed") manager.update_job(job_id, "failed", str(exc))