Files
IPSExtractor/services/extraction/core/text_utils.py
T

56 lines
1.9 KiB
Python

"""Utilidades de texto para parseo de JSON embebido y construcción de schema."""
import re
from typing import Optional, Dict, Any
def extract_json_from_text(text: str) -> Optional[str]:
"""Extrae el primer bloque JSON de un texto, con o sin fence ```json.
Devuelve la cadena JSON o None si no se encuentra.
"""
# 1. Try markdown fences
match = re.search(r"```json\s*(.*?)\s*```", text, flags=re.DOTALL | re.IGNORECASE)
if match:
return _clean_json_string(match.group(1).strip())
match = re.search(r"```\s*(\{[\s\S]*?\})\s*```", text, flags=re.DOTALL)
if match:
return _clean_json_string(match.group(1).strip())
# 2. Try finding outer braces
start = text.find('{')
end = text.rfind('}')
if start != -1 and end != -1 and end > start:
candidate = text[start:end + 1].strip()
return _clean_json_string(candidate)
return None
def _clean_json_string(json_str: str) -> str:
"""Intenta limpiar errores comunes de JSON generado por LLM."""
# Eliminar comas finales en objetos/listas: , } -> } y , ] -> ]
# Nota: Esto es una heurística simple y puede fallar en strings que contienen esos patrones.
# Usamos regex conservador.
json_str = re.sub(r",\s*}", "}", json_str)
json_str = re.sub(r",\s*]", "]", json_str)
return json_str
def build_schema(data: Dict[str, Any], prefix: str = "") -> Dict[str, str]:
"""Construye un mapa ruta->tipo a partir de un dict anidado.
Para listas de dicts, usa la notación "/*/subcampo".
"""
schema: Dict[str, str] = {}
for k, v in data.items():
path = f"{prefix}/{k}" if prefix else k
if isinstance(v, dict):
schema.update(build_schema(v, path))
elif isinstance(v, list) and v and isinstance(v[0], dict):
for subkey, subval in v[0].items():
schema[f"{path}/*/{subkey}"] = type(subval).__name__
else:
schema[path] = type(v).__name__
return schema