se agrega archivos para la instalación en Docker

This commit is contained in:
George committed 2026-08-04 17:57:43 -05:00
1 parent 533105e7c3
commit cf2c1ee147
28 files changed
+1430 -501

No files matched your search

+114 -28
View File
@@ -1,8 +1,9 @@
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 = ""
@@ -15,7 +16,7 @@ def extraer_texto_pdf(ruta_pdf):
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...
@@ -30,54 +31,139 @@ def extraer_ruc(texto):
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."""
"""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": [r'FACTURA\s+(ELECTR[OÓ]NICA|DE\s+VENTA)', r'FACTURA'],
"03": [r'BOLETA\s+DE\s+VENTA', r'BOLETA\s+ELECTR[OÓ]NICA'],
"07": [r'NOTA\s+DE\s+CR[EÉ]DITO'],
"08": [r'NOTA\s+DE\s+D[EÉ]BITO'],
"14": [r'RECIBO\s+POR\s+HONORARIOS'],
"09": [r'GU[IÍ]A\s+DE\s+REMISI[OÓ]N']
"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 o F001-0000123)."""
# Patrón común: [FBE][A-Z0-9]{3}-\d+
patron = re.search(r'\b([FBE][A-Z0-9]{3})\s*-\s*(\d+)\b', texto)
if patron:
return f"{patron.group(1)}-{patron.group(2)}"
"""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'
]
# Fallback para casos donde no hay guión o hay espacios raros
patron_alt = re.search(r'([FBE][A-Z0-9]{3})\s+(\d{1,10})', texto)
if patron_alt:
return f"{patron_alt.group(1)}-{patron_alt.group(2)}"
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."""
"""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"}
return {
resultado = {
"valido": True,
"tipo_doc": extraer_tipo_documento(texto),
"ruc": extraer_ruc(texto),
"id": extraer_serie_numero(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
# ========================================================================