rag-cub-webapp / api /main_api.py
alexandro-mayoral-teran
Configurar metadata Docker
878e9a3
Raw
History Blame Contribute Delete
10.6 kB
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
@app.get("/api/databases")
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}
@app.post("/api/extraer_formulario")
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)}")
@app.post("/api/extraer_formulario_full_context")
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)}")
@app.post("/api/extraer_metadatos")
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)}")
@app.post("/api/consulta_normativa")
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)}")
@app.post("/api/upload_efimero")
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)}")
@app.get("/api/evaluaciones")
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)}")
@app.get("/api/temas")
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)