ThomasMolina
Add FastAPI backend
c6dc128
import os
import re
import tempfile
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from google import genai
from pliego import pliego_pdf_to_markdown
from rup import rup_pdf_to_markdown
from main import generate_analysis
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Reemplaza "*" con tu URL de Vercel en producción
allow_methods=["POST"],
allow_headers=["*"],
)
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
def parse_markdown_analysis(text: str) -> dict:
"""
Parsea la respuesta Markdown de Gemini en datos estructurados
compatibles con el modelo de datos del frontend.
"""
gap_rows = []
lines = text.split("\n")
in_table = False
for line in lines:
stripped = line.strip()
if "|" not in stripped:
continue
# Omitir líneas separadoras como |---|---|
if re.match(r"^\|[\s\-:|]+\|$", stripped):
continue
cells = [c.strip() for c in stripped.split("|")]
cells = [c for c in cells if c] # quitar vacíos de | inicial/final
if len(cells) < 4:
continue
# Detectar cabecera de la tabla
if "requisito" in cells[0].lower():
in_table = True
continue
if not in_table:
continue
cumple_cell = cells[3] if len(cells) > 3 else ""
note = cells[4] if len(cells) > 4 else ""
if "✅" in cumple_cell:
status = "pass"
elif "❌" in cumple_cell:
status = "fail"
else:
status = "warning"
req_lower = cells[0].lower()
if any(w in req_lower for w in ["liquidez", "endeudamiento", "capital", "patrimonio", "financiero", "razón", "cobertura"]):
category = "Financiero"
elif any(w in req_lower for w in ["unspsc", "código", "clasificador"]):
category = "UNSPSC"
elif any(w in req_lower for w in ["experiencia", "contrato", "smmlv", "k de"]):
category = "Experiencia"
else:
category = "Habilitante"
gap_rows.append({
"requirement": cells[0],
"category": category,
"required": cells[1],
"yours": cells[2],
"status": status,
"note": note,
})
# Detectar si se recomienda Consorcio o Unión Temporal
lower_text = text.lower()
consortium_needed = "consorcio" in lower_text or "unión temporal" in lower_text
consortium_reason = ""
if consortium_needed:
for line in lines:
if "consorcio" in line.lower() or "unión temporal" in line.lower():
clean = re.sub(r"[#*`>]", "", line).strip()
if len(clean) > 20:
consortium_reason = clean
break
# Estado global de la licitación
fail_count = sum(1 for r in gap_rows if r["status"] == "fail")
if fail_count > 0:
overall_status = "no-viable"
elif all(r["status"] == "pass" for r in gap_rows) and gap_rows:
overall_status = "viable"
else:
overall_status = "revision"
return {
"status": overall_status,
"gap_analysis": gap_rows,
"risks": [],
"consortium_data": {
"needed": consortium_needed,
"reason": consortium_reason,
},
}
@app.post("/analizar")
async def analizar(
pliego: UploadFile = File(...),
rup: UploadFile = File(...)
):
# 1. Guardar PDFs en archivos temporales
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_pliego:
tmp_pliego.write(await pliego.read())
pliego_path = tmp_pliego.name
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_rup:
tmp_rup.write(await rup.read())
rup_path = tmp_rup.name
try:
# 2. Convertir PDFs a texto
pliego_text = pliego_pdf_to_markdown(pliego_path, os.devnull)
rup_text = rup_pdf_to_markdown(rup_path, os.devnull)
# 3. Correr el análisis con Gemini
response = generate_analysis(client, pliego_text, rup_text)
if not response.text:
raise HTTPException(status_code=500, detail="El modelo no devolvió respuesta.")
# 4. Parsear respuesta Markdown a datos estructurados
structured = parse_markdown_analysis(response.text)
return {
"resultado": response.text,
**structured,
}
finally:
# 5. Limpiar archivos temporales siempre, incluso si hay error
os.unlink(pliego_path)
os.unlink(rup_path)