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)