Spaces:
Sleeping
Sleeping
| 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, | |
| }, | |
| } | |
| 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) | |