"""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