Spaces:
Running
Running
| from fastapi import FastAPI, HTTPException, UploadFile, File | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel | |
| from typing import Optional, List | |
| import pandas as pd | |
| import os | |
| import sys | |
| import yaml | |
| # Forzar UTF-8 en stdout para evitar errores de charmap con emojis en Windows | |
| if sys.stdout.encoding.lower() != 'utf-8': | |
| sys.stdout.reconfigure(encoding='utf-8') | |
| # Agregar src al path | |
| sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) | |
| from src.nlp_core.generacion import extraer_rag_cascade, responder_rag_cascade_qa, extraer_full_context, extraer_metadatos_documento | |
| from datetime import datetime | |
| app = FastAPI(title="API DISF - Especialista Digital Regulador") | |
| # Habilitar CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| class ChatRequest(BaseModel): | |
| query: str | |
| tema: Optional[str] = None | |
| textos_efimeros: Optional[List[str]] = None | |
| solo_efimero: bool = False | |
| db_folder: Optional[str] = "chroma_db" | |
| instrucciones: Optional[str] = None | |
| def get_databases(): | |
| base_dir = os.path.join(os.path.dirname(__file__), '../data/03_output') | |
| dbs = [] | |
| if os.path.exists(base_dir): | |
| for item in os.listdir(base_dir): | |
| if item.startswith("chroma_db") and os.path.isdir(os.path.join(base_dir, item)): | |
| # Generar etiqueta amigable | |
| label = item.replace("chroma_db", "").replace("_", " ").strip() | |
| if not label: | |
| label = "Actual (chroma_db)" | |
| else: | |
| label = label.title() | |
| dbs.append({"value": item, "label": label}) | |
| # Ordenar asegurando que 'chroma_db' quede de primero | |
| dbs.sort(key=lambda x: (x["value"] != "chroma_db", x["label"])) | |
| return {"status": "success", "databases": dbs} | |
| def extraer_formulario_endpoint(request: ChatRequest): | |
| try: | |
| # Extracción Pydantic con Cascade | |
| kwargs = { | |
| "query": request.query, | |
| "tema": request.tema, | |
| "textos_efimeros": request.textos_efimeros, | |
| "solo_efimero": request.solo_efimero, | |
| "db_folder": request.db_folder | |
| } | |
| resultado_rag, telemetria = extraer_rag_cascade(**kwargs) | |
| # FastAPI convertirá automáticamente el modelo Pydantic a JSON | |
| return { | |
| "status": "success", | |
| "data": resultado_rag.model_dump(), | |
| "telemetry": telemetria | |
| } | |
| except ValueError as ve: | |
| raise HTTPException(status_code=400, detail=str(ve)) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Error del servidor: {str(e)}") | |
| def extraer_formulario_full_context_endpoint(request: ChatRequest): | |
| """ | |
| Nuevo endpoint que ignora el RAG y pasa todo el texto concatenado al LLM usando | |
| la ventana de contexto larga (128k tokens) para no perder ningún campo. | |
| """ | |
| try: | |
| if not request.textos_efimeros or len(request.textos_efimeros) == 0: | |
| raise ValueError("Debes proporcionar al menos un documento para la extracción de contexto largo.") | |
| # Unir todos los textos efímeros en un gran string | |
| texto_completo = "\n\n--- SIGUIENTE DOCUMENTO ---\n\n".join(request.textos_efimeros) | |
| # Llamar al motor de extracción Long-Context | |
| resultado_pydantic, telemetria = extraer_full_context(texto_completo, instrucciones=request.instrucciones) | |
| data_dict = resultado_pydantic.model_dump() | |
| # Guardar en output local | |
| import json | |
| output_dir = os.path.join(os.path.dirname(__file__), '../data/03_output/formularios_extraidos') | |
| os.makedirs(output_dir, exist_ok=True) | |
| filename = f"formulario_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" | |
| with open(os.path.join(output_dir, filename), "w", encoding="utf-8") as f: | |
| json.dump(data_dict, f, ensure_ascii=False, indent=4) | |
| return { | |
| "status": "success", | |
| "data": data_dict, | |
| "telemetry": telemetria, | |
| "saved_to": filename | |
| } | |
| except ValueError as ve: | |
| raise HTTPException(status_code=400, detail=str(ve)) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}") | |
| def extraer_metadatos_endpoint(request: ChatRequest): | |
| """ | |
| Endpoint que toma el documento temporal cargado y extrae su metadata. | |
| Guarda el resultado en data/03_output/metadatos_extraidos/. | |
| """ | |
| import json | |
| try: | |
| if not request.textos_efimeros or len(request.textos_efimeros) == 0: | |
| raise ValueError("Debes proporcionar al menos un documento (Temporal) para la extracción de metadatos.") | |
| texto = request.textos_efimeros[0] | |
| resultado_pydantic, telemetria = extraer_metadatos_documento(texto, instrucciones=request.instrucciones) | |
| data_dict = resultado_pydantic.model_dump() | |
| # Guardar en output local | |
| output_dir = os.path.join(os.path.dirname(__file__), '../data/03_output/metadatos_extraidos') | |
| os.makedirs(output_dir, exist_ok=True) | |
| filename = f"metadata_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" | |
| with open(os.path.join(output_dir, filename), "w", encoding="utf-8") as f: | |
| json.dump(data_dict, f, ensure_ascii=False, indent=4) | |
| return { | |
| "status": "success", | |
| "data": data_dict, | |
| "telemetry": telemetria, | |
| "saved_to": filename | |
| } | |
| except ValueError as ve: | |
| raise HTTPException(status_code=400, detail=str(ve)) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}") | |
| def consulta_normativa_endpoint(request: ChatRequest): | |
| try: | |
| # RAG Conversacional Cascade (usando los defaults óptimos de generacion.py) | |
| kwargs = { | |
| "query": request.query, | |
| "tema": request.tema, | |
| "textos_efimeros": request.textos_efimeros, | |
| "solo_efimero": request.solo_efimero, | |
| "db_folder": request.db_folder | |
| } | |
| texto_respuesta, telemetria, contexto = responder_rag_cascade_qa(**kwargs) | |
| return { | |
| "status": "success", | |
| "data": texto_respuesta, | |
| "telemetry": telemetria, | |
| "context": contexto | |
| } | |
| except ValueError as ve: | |
| raise HTTPException(status_code=400, detail=str(ve)) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}") | |
| async def upload_efimero_endpoint(file: UploadFile = File(...)): | |
| import tempfile | |
| import shutil | |
| from src.ingesta.ingestor import IngestorDocumentos | |
| try: | |
| temp_dir = tempfile.mkdtemp() | |
| temp_file_path = os.path.join(temp_dir, file.filename) | |
| with open(temp_file_path, "wb") as buffer: | |
| shutil.copyfileobj(file.file, buffer) | |
| ingestor = IngestorDocumentos(output_dir=temp_dir) | |
| resultado = ingestor.procesar_archivo(temp_file_path) | |
| if resultado["status"] == "success": | |
| md_path = os.path.join(temp_dir, resultado["output_file"]) | |
| with open(md_path, "r", encoding="utf-8") as f: | |
| md_text = f.read() | |
| return {"status": "success", "markdown": md_text} | |
| else: | |
| raise HTTPException(status_code=400, detail=resultado.get("error", "Error procesando el archivo")) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Error interno del servidor: {str(e)}") | |
| async def get_evaluaciones(): | |
| try: | |
| # Rutas a los tres escenarios | |
| base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../data/03_output')) | |
| # 1. Contextualizador (SOTA) | |
| path_contextualizador = os.path.join(base_path, 'evaluaciones', 'ARENA_RESULTADOS_llm_judge.csv') | |
| df_contextualizador = pd.read_csv(path_contextualizador) | |
| # 2. Inyector | |
| path_inyector = os.path.join(base_path, 'evaluaciones_inyector_metadata', 'ARENA_RESULTADOS_llm_judge.csv') | |
| df_inyector = pd.read_csv(path_inyector) | |
| # 3. Only Chunking | |
| path_chunking = os.path.join(base_path, 'evaluaciones_only_chunking', 'ARENA_RESULTADOS_llm_judge.csv') | |
| df_chunking = pd.read_csv(path_chunking) | |
| return { | |
| "status": "success", | |
| "scenarios": { | |
| "only_chunking": df_chunking.to_dict(orient="records"), | |
| "inyector": df_inyector.to_dict(orient="records"), | |
| "contextualizador": df_contextualizador.to_dict(orient="records") | |
| } | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Error al leer evaluaciones: {str(e)}") | |
| def get_temas(): | |
| manifest_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../data/01_raw/manifest.yaml')) | |
| temas = set() | |
| try: | |
| with open(manifest_path, 'r', encoding='utf-8') as f: | |
| manifest_data = yaml.safe_load(f) | |
| if manifest_data and 'documentos' in manifest_data: | |
| for doc in manifest_data['documentos']: | |
| if 'tema' in doc: | |
| if isinstance(doc['tema'], list): | |
| for t in doc['tema']: | |
| temas.add(t) | |
| else: | |
| temas.add(doc['tema']) | |
| return {"status": "success", "temas": sorted(list(temas))} | |
| except Exception as e: | |
| return {"status": "error", "temas": [], "detail": str(e)} | |
| # Montar los archivos estáticos de la app (Frontend Vanilla JS) | |
| app_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../app')) | |
| app.mount("/", StaticFiles(directory=app_path, html=True), name="app") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("api.main_api:app", host="127.0.0.1", port=8000, reload=True) | |