import pdfplumber import re import os import llm_processor # ======================================================================== def extraer_texto_pdf(ruta_pdf): """Extrae todo el texto de un PDF usando pdfplumber.""" texto_completo = "" try: with pdfplumber.open(ruta_pdf) as pdf: for page in pdf.pages: texto_completo += page.extract_text() + "\n" return texto_completo except Exception as e: print(f" ⚠️ Error leyendo PDF {ruta_pdf}: {e}") return "" # ======================================================================== def extraer_ruc(texto): """Busca un RUC (11 dígitos, empieza con 10 o 20) en el texto.""" # Intentamos buscar patrones de RUC: RUC 2060... o R.U.C. 2060... patrones = [ r'R\.?U\.?C\.?\s*[:\-\s]\s*((?:10|20)\d{9})', # R.U.C: 20... r'\b((?:10|20)\d{9})\b' # 11 dígitos sueltos que empiecen con 10 o 20 ] for patron in patrones: match = re.search(patron, texto, re.IGNORECASE) if match: return match.group(1) return "N/A" # ======================================================================== def _flexible(palabra): """Convierte una palabra en un patrón regex que tolera espacios entre caracteres. Ej: FACTURA -> F\\s*A\\s*C\\s*T\\s*U\\s*R\\s*A """ return '\\s*'.join(palabra) def extraer_tipo_documento(texto): """Identifica el tipo de documento SUNAT basándose en el texto usando regex multilínea. Soporta texto con espacios entre caracteres (ej: 'F A C T U R A').""" t = texto.upper() patrones = { "01": [ rf'{_flexible("FACTURA")}\s+({_flexible("ELECTRÓNICA")}|{_flexible("ELECTRONICA")}|DE\s+VENTA)', rf'{_flexible("FACTURA")}', ], "03": [ rf'{_flexible("BOLETA")}\s+DE\s+{_flexible("VENTA")}', rf'{_flexible("BOLETA")}\s+{_flexible("ELECTRÓNICA")}', rf'{_flexible("BOLETA")}\s+{_flexible("ELECTRONICA")}', ], "07": [ rf'{_flexible("NOTA")}\s+DE\s+{_flexible("CRÉDITO")}', rf'{_flexible("NOTA")}\s+DE\s+{_flexible("CREDITO")}', ], "08": [ rf'{_flexible("NOTA")}\s+DE\s+{_flexible("DÉBITO")}', rf'{_flexible("NOTA")}\s+DE\s+{_flexible("DEBITO")}', ], "14": [ rf'{_flexible("RECIBO")}\s+POR\s+{_flexible("HONORARIOS")}', ], "09": [ rf'{_flexible("GUÍA")}\s+DE\s+{_flexible("REMISIÓN")}', rf'{_flexible("GUIA")}\s+DE\s+{_flexible("REMISION")}', ], } for tipo, regex_list in patrones.items(): for p in regex_list: if re.search(p, t, re.IGNORECASE | re.MULTILINE): return tipo return "99" # ======================================================================== def extraer_serie_numero(texto): """Busca la serie y número (ej. F001-123, E001-0000123, EG07-2367).""" serie_patrones = [ # Formato con guión: letras+dígitos - número r'\b([A-Z]{1,4}\d{2,4})\s*-\s*(\d+)\b', # Fallback sin guión r'([A-Z]{1,4}\d{2,4})\s+(\d{1,10})', # Serie numérica corta sin letras r'\b(\d{3})\s*-\s*(\d+)\b' ] for patron in serie_patrones: match = re.search(patron, texto) if match: return f"{match.group(1)}-{match.group(2)}" return "No encontrado" # ======================================================================== def _necesita_llm(resultado): """True si algún campo clave no se encontró con regex.""" return ( resultado['ruc'] in ("N/A", "Desconocido") or resultado['tipo_doc'] == "99" or resultado['id'] in ("No encontrado", "N/A") ) def _campos_rellenados(regex_original, regex_result): """Cuenta cuántos campos cambió el LLM respecto al regex original.""" count = 0 if regex_original['ruc'] in ("N/A", "Desconocido") and regex_result['ruc'] not in ("N/A", "Desconocido"): count += 1 if regex_original['tipo_doc'] == "99" and regex_result['tipo_doc'] != "99": count += 1 if regex_original['id'] in ("No encontrado", "N/A") and regex_result['id'] not in ("No encontrado", "N/A"): count += 1 return count def _merge_llm(regex_result, llm_data): """Rellena campos vacíos del regex con datos del LLM sin pisar lo ya encontrado.""" if regex_result['ruc'] in ("N/A", "Desconocido"): ruc = llm_data.get('ruc', '') if re.match(r'^(10|20)\d{9}$', str(ruc)): regex_result['ruc'] = ruc if regex_result['tipo_doc'] == "99": tipo = str(llm_data.get('tipo', '99')) tipo_codes = {"01", "02", "03", "07", "08", "09", "14"} if tipo in tipo_codes: regex_result['tipo_doc'] = tipo if regex_result['id'] in ("No encontrado", "N/A"): serie = str(llm_data.get('serie', '')) numero = str(llm_data.get('numero', '')) if serie and numero: regex_result['id'] = f"{serie}-{numero}" return regex_result def obtener_datos_pdf(ruta_pdf): """Orquestador para extraer metadata de un PDF. Primero intenta con regex; si faltan campos, usa LLM como fallback.""" texto = extraer_texto_pdf(ruta_pdf) if not texto: return {"valido": False, "error": "No se pudo extraer texto"} resultado = { "valido": True, "tipo_doc": extraer_tipo_documento(texto), "ruc": extraer_ruc(texto), "id": extraer_serie_numero(texto), "llm_usado": False, "llm_respuesta": None } if _necesita_llm(resultado): resultado["llm_usado"] = True llm_data = llm_processor.extraer_con_llm(texto) resultado["llm_respuesta"] = dict(llm_data) if llm_data else None if llm_data: antes = dict(resultado) resultado = _merge_llm(resultado, llm_data) return resultado # ========================================================================