Spaces:
Paused
Paused
| """ | |
| json_utils.py β ExtraΓ§Γ£o cirΓΊrgica e recuperaΓ§Γ£o de JSON malformado | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Arquitetura em cascata: | |
| 1. Parse direto (happy path β O(1)) | |
| 2. SanitizaΓ§Γ£o textual (patterns conhecidos de malformaΓ§Γ£o) | |
| 3. ExtraΓ§Γ£o key-by-key (balanceamento de profundidade) | |
| 4. Inject defaults (campos required ausentes β defaults tipados) | |
| Caso de uso primΓ‘rio: recuperar `failed_generation` de erros 400 | |
| `json_validate_failed` do provider (OpenAI-compat), onde o LLM produziu | |
| JSON vΓ‘lido mas faltando um campo obrigatΓ³rio no schema. | |
| IntegraΓ§Γ£o em external_worker.py β TrindadePipeline.api_call(): | |
| elif result.code == 400: | |
| recovered = JsonUtils.repair_failed_json( | |
| result.raw_error_body, # body bruto do HTTP 400 | |
| schema = schema_step, | |
| wrapper = pipeline.wrapper_key, | |
| ) | |
| if recovered: | |
| logger.warning(f"β»οΈ [{label}] JSON recuperado via repair_failed_json") | |
| return recovered, None, False # trata como END_SUCCESS | |
| logger.error(f"Erro 400 irrecuperΓ‘vel: {result.content}") | |
| return None, None, False | |
| Nota: para que `result.raw_error_body` esteja disponΓvel, providers.py | |
| precisa salvΓ‘-lo em APIResult.raw_error_body ao receber status 400. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import re | |
| from typing import Any, Dict, List, Optional, Tuple | |
| logger = logging.getLogger("json_utils") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CONSTANTES | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Defaults por tipo JSON Schema | |
| _TYPE_DEFAULTS: Dict[str, Any] = { | |
| "array": [], | |
| "object": {}, | |
| "string": "", | |
| "integer": 0, | |
| "number": 0.0, | |
| "boolean": False, | |
| "null": None, | |
| } | |
| # Patterns de sanitizaΓ§Γ£o em ordem de aplicaΓ§Γ£o. | |
| # Cada entrada: (pattern_regex, replacement, flags) | |
| _SANITIZE_PATTERNS: List[Tuple[str, str, int]] = [ | |
| # "campo",":",valor β "campo": valor (vΓrgula antes dos dois-pontos) | |
| (r'"(\w+)"\s*,\s*":"\s*,\s*', r'"\1": ', 0), | |
| # "campo",": valor β "campo": valor (vΓrgula antes do separador sem espaΓ§o) | |
| (r'"(\w+)"\s*,\s*":\s*', r'"\1": ', 0), | |
| # vΓrgulas duplicadas | |
| (r',\s*,+', ',', 0), | |
| # trailing comma antes de ] ou } | |
| (r',\s*(?=[\]\}])', '', 0), | |
| # colchete duplo no fechamento de array ]] β ] | |
| # (sΓ³ quando nΓ£o Γ© inΓcio de novo array) | |
| (r'\]\s*\](?=\s*[,\}\]])', ']', 0), | |
| # colchete duplo seguido de } ]]} β ]} | |
| (r'\]\s*\]\s*\}', ']}', 0), | |
| ] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # JsonUtils | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class JsonUtils: | |
| """ | |
| UtilitΓ‘rios de extraΓ§Γ£o, sanitizaΓ§Γ£o e recuperaΓ§Γ£o de JSON malformado. | |
| Todos os mΓ©todos sΓ£o @classmethod β sem estado de instΓ’ncia. | |
| Thread-safe (apenas operaΓ§Γ΅es puras em strings/dicts). | |
| """ | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 1. ENTRY POINT: pipeline cascata completo | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_json( | |
| cls, | |
| text: str, | |
| required_keys: Optional[List[str]] = None, | |
| ) -> Optional[Dict]: | |
| """ | |
| Tenta extrair um dict JSON de `text` em cascata: | |
| 1. json.loads direto | |
| 2. Isola o bloco { ... } externo e parseia | |
| 3. Sanitiza e repete 1-2 | |
| 4. ExtraΓ§Γ£o key-by-key (required_keys) | |
| Retorna None se todas as camadas falharem. | |
| """ | |
| # ββ camada 1: parse direto βββββββββββββββββββββββββββββββββββββββββ | |
| result = cls._try_parse(text) | |
| if result is not None: | |
| return result | |
| # ββ camada 2: isola bloco externo { } βββββββββββββββββββββββββββββ | |
| block = cls._isolate_outer_block(text) | |
| if block and block != text: | |
| result = cls._try_parse(block) | |
| if result is not None: | |
| return result | |
| # ββ camada 3: sanitiza β tenta de novo ββββββββββββββββββββββββββββ | |
| sanitized = cls.sanitize_json(block or text) | |
| result = cls._try_parse(sanitized) | |
| if result is not None: | |
| return result | |
| # isola de novo apΓ³s sanitizaΓ§Γ£o | |
| block2 = cls._isolate_outer_block(sanitized) | |
| if block2 and block2 != sanitized: | |
| result = cls._try_parse(block2) | |
| if result is not None: | |
| return result | |
| # ββ camada 4: key-by-key (requer required_keys) βββββββββββββββββββ | |
| #if required_keys: | |
| # assembled = cls._extract_by_keys(sanitized or text, required_keys) | |
| # if assembled: | |
| # return assembled | |
| logger.debug("extract_json: todas as camadas falharam") | |
| return None | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 2. SANITIZAΓΓO TEXTUAL | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def sanitize_json(cls, text: str) -> str: | |
| """ | |
| Aplica patterns conhecidos de malformaΓ§Γ£o em sequΓͺncia. | |
| NΓ£o lanΓ§a exceΓ§Γ΅es β pior caso retorna o texto original. | |
| Patterns tratados: | |
| β’ "campo",":",valor β "campo": valor | |
| β’ ,, β , | |
| β’ ,] ,} β ] } | |
| β’ ]] β ] (array duplo no fechamento) | |
| """ | |
| result = text | |
| for pattern, replacement, flags in _SANITIZE_PATTERNS: | |
| try: | |
| new = re.sub(pattern, replacement, result, flags=flags) | |
| if new != result: | |
| logger.debug(f"sanitize_json: aplicou pattern {pattern!r}") | |
| result = new | |
| except re.error as e: | |
| logger.warning(f"sanitize_json: regex error em {pattern!r}: {e}") | |
| return result | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 3. EXTRAΓΓO DE KEY INDIVIDUAL (balanceamento de profundidade) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_key(cls, text: str, key: str) -> Optional[str]: | |
| """ | |
| Localiza `"key":` no texto e extrai o valor completo usando | |
| balanceamento de profundidade para {} e []. | |
| Suporta valores: objeto {}, array [], string "", nΓΊmero, bool, null. | |
| Retorna a substring do valor (sem key), ou None se nΓ£o encontrar. | |
| """ | |
| pattern = re.compile(r'"' + re.escape(key) + r'"\s*:\s*') | |
| m = pattern.search(text) | |
| if not m: | |
| return None | |
| start = m.end() | |
| return cls._extract_value_at(text, start) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 4. INJEΓΓO DE DEFAULTS (campos required ausentes) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def inject_defaults( | |
| cls, | |
| data: Dict, | |
| schema: Optional[Dict] = None, | |
| wrapper_key: str = "manifestacao_juridica", | |
| ) -> Dict: | |
| """ | |
| Percorre o schema recursivamente e injeta valores padrΓ£o para campos | |
| `required` ausentes em `data`. | |
| Se `schema` for None, retorna `data` sem modificaΓ§Γ£o. | |
| EstratΓ©gia: | |
| β’ type=array β [] | |
| β’ type=object β {} (recursivo com subschema) | |
| β’ type=string β "" | |
| β’ type=integer β 0 | |
| β’ type=number β 0.0 | |
| β’ type=boolean β false | |
| β’ type=null β null | |
| β’ items com required (array de objetos) β injeta em cada elemento | |
| """ | |
| if schema is None: | |
| return data | |
| result = json.loads(json.dumps(data)) # deep copy via JSON | |
| cls._inject_recursive(result, schema) | |
| return result | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 5. RECUPERAΓΓO DE `failed_generation` (erro 400 json_validate_failed) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def repair_failed_json( | |
| cls, | |
| raw_error_body: Optional[str], | |
| schema: Optional[Dict] = None, | |
| wrapper_key: str = "manifestacao_juridica", | |
| ) -> Optional[str]: | |
| """ | |
| Dado o body bruto de um HTTP 400 com code=json_validate_failed, | |
| extrai o campo `failed_generation`, tenta parseΓ‘-lo, injeta | |
| defaults para campos required ausentes e retorna JSON string | |
| pronto para `json.loads()`. | |
| Fluxo: | |
| 1. Parseia o envelope de erro | |
| 2. Confirma code == "json_validate_failed" | |
| 3. Extrai failed_generation | |
| 4. Tenta parse direto (happy path β quase sempre vΓ‘lido) | |
| 5. Fallback: extract_json com sanitizaΓ§Γ£o | |
| 6. inject_defaults com schema | |
| 7. Retorna json.dumps(resultado) | |
| Retorna None se nΓ£o conseguir recuperar. | |
| """ | |
| if not raw_error_body: | |
| return None | |
| # ββ parseia envelope de erro βββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| envelope = json.loads(raw_error_body) | |
| except json.JSONDecodeError: | |
| # tenta extrair JSON de dentro do texto | |
| envelope = cls.extract_json(raw_error_body) | |
| if envelope is None: | |
| logger.warning("repair_failed_json: envelope de erro nΓ£o Γ© JSON vΓ‘lido") | |
| return None | |
| # ββ navega atΓ© o campo de erro βββββββββββββββββββββββββββββββββββββ | |
| error_obj = envelope.get("error", envelope) | |
| if not isinstance(error_obj, dict): | |
| logger.warning("repair_failed_json: campo 'error' ausente ou invΓ‘lido") | |
| return None | |
| code = error_obj.get("code", "") | |
| if code != "json_validate_failed": | |
| logger.debug(f"repair_failed_json: code={code!r} (nΓ£o Γ© json_validate_failed)") | |
| return None | |
| failed_gen: Optional[str] = error_obj.get("failed_generation") | |
| if not failed_gen: | |
| logger.warning("repair_failed_json: failed_generation ausente") | |
| return None | |
| logger.info(f"repair_failed_json: failed_generation encontrado ({len(failed_gen)} chars)") | |
| # ββ tenta parse direto βββββββββββββββββββββββββββββββββββββββββββββ | |
| recovered: Optional[Dict] = cls._try_parse(failed_gen) | |
| if recovered is None: | |
| logger.info("repair_failed_json: parse direto falhou β tentando extract_json") | |
| recovered = cls.extract_json(failed_gen) | |
| if recovered is None: | |
| logger.error("repair_failed_json: nΓ£o foi possΓvel parsear failed_generation") | |
| return None | |
| # ββ injeta defaults para campos required ausentes ββββββββββββββββββ | |
| #if schema: | |
| # try: | |
| # recovered = cls.inject_defaults(recovered, schema, wrapper_key) | |
| # except Exception as e: | |
| # logger.warning(f"repair_failed_json: inject_defaults falhou: {e} β usando sem inject") | |
| # ββ serializa e retorna ββββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| result_str = json.dumps(recovered, ensure_ascii=False) | |
| logger.info("repair_failed_json: β recuperaΓ§Γ£o bem-sucedida") | |
| return result_str | |
| except Exception as e: | |
| logger.error(f"repair_failed_json: json.dumps falhou: {e}") | |
| return None | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 6. VALIDAΓΓO ESTRUTURAL | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def validate_structure(cls, text: str) -> bool: | |
| """ | |
| Valida se {} e [] estΓ£o balanceados e aspas fechadas. | |
| NΓ£o valida JSON completo β apenas estrutura superficial. | |
| """ | |
| depth_curly = 0 | |
| depth_square = 0 | |
| in_string = False | |
| escaped = False | |
| for ch in text: | |
| if escaped: | |
| escaped = False | |
| continue | |
| if ch == '\\' and in_string: | |
| escaped = True | |
| continue | |
| if ch == '"': | |
| in_string = not in_string | |
| continue | |
| if in_string: | |
| continue | |
| if ch == '{': | |
| depth_curly += 1 | |
| elif ch == '}': | |
| depth_curly -= 1 | |
| elif ch == '[': | |
| depth_square += 1 | |
| elif ch == ']': | |
| depth_square -= 1 | |
| if depth_curly < 0 or depth_square < 0: | |
| return False | |
| return depth_curly == 0 and depth_square == 0 and not in_string | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MΓTODOS PRIVADOS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _try_parse(text: Optional[str]) -> Optional[Dict]: | |
| """Parse silencioso: retorna dict ou None.""" | |
| if not text: | |
| return None | |
| try: | |
| result = json.loads(text) | |
| if isinstance(result, dict): | |
| return result | |
| return None | |
| except (json.JSONDecodeError, ValueError): | |
| return None | |
| def _isolate_outer_block(text: str) -> Optional[str]: | |
| """Encontra o primeiro { e o } correspondente, retorna o bloco.""" | |
| start = text.find('{') | |
| if start == -1: | |
| return None | |
| depth = 0 | |
| in_str = False | |
| escaped = False | |
| for i, ch in enumerate(text[start:], start): | |
| if escaped: | |
| escaped = False | |
| continue | |
| if ch == '\\' and in_str: | |
| escaped = True | |
| continue | |
| if ch == '"': | |
| in_str = not in_str | |
| continue | |
| if in_str: | |
| continue | |
| if ch == '{': | |
| depth += 1 | |
| elif ch == '}': | |
| depth -= 1 | |
| if depth == 0: | |
| return text[start:i + 1] | |
| return None # bloco nunca fechado | |
| def _extract_value_at(cls, text: str, pos: int) -> Optional[str]: | |
| """Extrai o valor JSON que comeΓ§a na posiΓ§Γ£o `pos` do texto.""" | |
| if pos >= len(text): | |
| return None | |
| ch = text[pos] | |
| # String | |
| if ch == '"': | |
| end = pos + 1 | |
| escaped = False | |
| while end < len(text): | |
| c = text[end] | |
| if escaped: | |
| escaped = False | |
| elif c == '\\': | |
| escaped = True | |
| elif c == '"': | |
| return text[pos:end + 1] | |
| end += 1 | |
| return None | |
| # Object ou Array | |
| if ch in ('{', '['): | |
| close = '}' if ch == '{' else ']' | |
| depth = 0 | |
| in_str = False | |
| escaped = False | |
| for i, c in enumerate(text[pos:], pos): | |
| if escaped: | |
| escaped = False | |
| continue | |
| if c == '\\' and in_str: | |
| escaped = True | |
| continue | |
| if c == '"': | |
| in_str = not in_str | |
| continue | |
| if in_str: | |
| continue | |
| if c == ch: | |
| depth += 1 | |
| elif c == close: | |
| depth -= 1 | |
| if depth == 0: | |
| return text[pos:i + 1] | |
| return None | |
| # Primitivo (nΓΊmero, bool, null) β lΓͺ atΓ© vΓrgula, } ou ] | |
| end = pos | |
| while end < len(text) and text[end] not in (',', '}', ']', '\n'): | |
| end += 1 | |
| raw = text[pos:end].strip() | |
| return raw if raw else None | |
| def _extract_by_keys(cls, text: str, keys: List[str]) -> Optional[Dict]: | |
| """Tenta reconstruir dict extraindo cada key individualmente.""" | |
| assembled: Dict[str, Any] = {} | |
| any_found = False | |
| for key in keys: | |
| raw_val = cls.extract_key(text, key) | |
| if raw_val is None: | |
| logger.debug(f"_extract_by_keys: key {key!r} nΓ£o encontrada") | |
| continue | |
| try: | |
| assembled[key] = json.loads(raw_val) | |
| any_found = True | |
| except json.JSONDecodeError: | |
| sanitized_val = cls.sanitize_json(raw_val) | |
| try: | |
| assembled[key] = json.loads(sanitized_val) | |
| any_found = True | |
| except json.JSONDecodeError: | |
| logger.debug(f"_extract_by_keys: falha ao parsear valor de {key!r}") | |
| return assembled if any_found else None | |
| def _inject_recursive(cls, data: Any, schema: Dict) -> None: | |
| """Injeta defaults recursivamente in-place em `data` conforme `schema`.""" | |
| if not isinstance(schema, dict): | |
| return | |
| schema_type = schema.get("type") | |
| # ββ objeto βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if schema_type == "object" and isinstance(data, dict): | |
| properties = schema.get("properties", {}) | |
| #required = schema.get("required", []) | |
| #for field_name in required: | |
| # if field_name not in data: | |
| # field_schema = properties.get(field_name, {}) | |
| # data[field_name] = cls._default_for_schema(field_schema) | |
| # logger.info(f"inject_defaults: injetou campo ausente {field_name!r}") | |
| # recursΓ£o nos campos presentes | |
| for field_name, field_schema in properties.items(): | |
| if field_name in data: | |
| cls._inject_recursive(data[field_name], field_schema) | |
| # ββ array ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| elif schema_type == "array" and isinstance(data, list): | |
| items_schema = schema.get("items", {}) | |
| if items_schema and isinstance(items_schema, dict): | |
| for item in data: | |
| if isinstance(item, dict): | |
| cls._inject_recursive(item, items_schema) | |
| def _default_for_schema(cls, schema: Dict) -> Any: | |
| """Retorna o valor default para um sub-schema.""" | |
| if not isinstance(schema, dict): | |
| return None | |
| schema_type = schema.get("type") | |
| if schema_type == "object": | |
| obj: Dict = {} | |
| # preenche campos required do sub-objeto tambΓ©m | |
| properties = schema.get("properties", {}) | |
| #required = schema.get("required", []) | |
| #for field_name in required: | |
| # field_schema = properties.get(field_name, {}) | |
| # obj[field_name] = cls._default_for_schema(field_schema) | |
| return obj | |
| if schema_type == "array": | |
| return [] | |
| return _TYPE_DEFAULTS.get(schema_type, None) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # INTEGRAΓΓO β patch para TrindadePipeline.api_call() | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # | |
| # Substituir em external_worker.py, no bloco `elif result.code == 400:`: | |
| # | |
| # ANTES: | |
| # elif result.code == 400: | |
| # logger.error(f"Erro 400 do provedor: {result.content}") | |
| # return None, None, False | |
| # | |
| # DEPOIS: | |
| # elif result.code == 400: | |
| # recovered_str = JsonUtils.repair_failed_json( | |
| # raw_error_body = getattr(result, "raw_error_body", None) or result.content, | |
| # schema = schema, # schema jΓ‘ podado para o step | |
| # wrapper_key = "manifestacao_juridica", | |
| # ) | |
| # if recovered_str: | |
| # logger.warning(f"β»οΈ [{label}] 400 recuperado via repair_failed_json") | |
| # if stats: | |
| # stats.errors_other -= 1 # nΓ£o conta como erro | |
| # stats.last_raw_response = recovered_str | |
| # return recovered_str, None, False # trata como END_SUCCESS | |
| # logger.error(f"Erro 400 irrecuperΓ‘vel: {result.content}") | |
| # return None, None, False | |
| # | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # providers.py β APIResult precisa expor raw_error_body: | |
| # | |
| # Adicionar campo em APIResult (dataclass): | |
| # raw_error_body: Optional[str] = None | |
| # | |
| # No bloco de tratamento do 400 em providers.py, onde o `β οΈ output_keys paylpsd:` | |
| # Γ© logado, salvar o body: | |
| # result.raw_error_body = response_text # string do body HTTP 400 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| # ββ smoke tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| logging.basicConfig(level=logging.DEBUG) | |
| # Caso 1: failed_generation com JSON vΓ‘lido mas campo ausente | |
| error_body_valid = json.dumps({ | |
| "error": { | |
| "message": "jsonschema: missing properties: 'provas_valoradas'", | |
| "type": "invalid_request_error", | |
| "code": "json_validate_failed", | |
| "failed_generation": json.dumps({ | |
| "manifestacao_juridica": { | |
| "fundamentacao": { | |
| "teses_relator": [{ | |
| "identificador_unico_fundamento_atomico": "inepcia", | |
| "nucleo_argumentativo": "Pedido genΓ©rico.", | |
| "resultado": "PROVIDO", | |
| # provas_valoradas ausente | |
| }], | |
| "temas_nao_analisados": [], | |
| }, | |
| "decisao_ementa": { | |
| "resultado_global": "PROVIDO", | |
| "mapa_resultados_por_pedido": [], | |
| } | |
| } | |
| }) | |
| } | |
| }) | |
| schema_test = { | |
| "type": "object", | |
| "properties": { | |
| "manifestacao_juridica": { | |
| "type": "object", | |
| "required": ["fundamentacao", "decisao_ementa"], | |
| "properties": { | |
| "fundamentacao": { | |
| "type": "object", | |
| "properties": { | |
| "teses_relator": { | |
| "type": "array", | |
| "items": { | |
| "type": "object", | |
| "required": ["provas_valoradas", "resultado"], | |
| "properties": { | |
| "provas_valoradas": {"type": "array"}, | |
| "resultado": {"type": "string"}, | |
| } | |
| } | |
| }, | |
| "temas_nao_analisados": {"type": "array"}, | |
| } | |
| }, | |
| "decisao_ementa": {"type": "object"}, | |
| } | |
| } | |
| } | |
| } | |
| result = JsonUtils.repair_failed_json(error_body_valid, schema=schema_test) | |
| assert result is not None, "β Caso 1 falhou" | |
| parsed = json.loads(result) | |
| teses = parsed["manifestacao_juridica"]["fundamentacao"]["teses_relator"] | |
| assert teses[0].get("provas_valoradas") == [], f"β inject_defaults falhou: {teses[0]}" | |
| print("β Caso 1: JSON vΓ‘lido + inject_defaults OK") | |
| # Caso 2: failed_generation com malformaΓ§Γ£o "campo",":",[] | |
| malformed = '{"manifestacao_juridica": {"fundamentacao": {"sintaxe": "ok", "temas_nao_analisados",":",[]], "decisao_ementa": {"resultado_global": "PROVIDO"}}}}' | |
| error_body_malformed = json.dumps({ | |
| "error": { | |
| "code": "json_validate_failed", | |
| "failed_generation": malformed, | |
| } | |
| }) | |
| result2 = JsonUtils.repair_failed_json(error_body_malformed) | |
| assert result2 is not None, "β Caso 2 falhou" | |
| parsed2 = json.loads(result2) | |
| assert parsed2["manifestacao_juridica"]["fundamentacao"]["temas_nao_analisados"] == [] | |
| print("β Caso 2: malformaΓ§Γ£o 'campo',':',[] sanitizada OK") | |
| # Caso 3: validate_structure | |
| assert JsonUtils.validate_structure('{"a": [1, 2]}') is True | |
| assert JsonUtils.validate_structure('{"a": [1, 2]}}') is False | |
| assert JsonUtils.validate_structure('{"a": "texto}"}') is True # } dentro de string | |
| print("β Caso 3: validate_structure OK") | |
| # Caso 4: extract_key | |
| text4 = '{"alpha": {"x": 1}, "beta": [1,2,3], "gamma": "texto"}' | |
| assert json.loads(JsonUtils.extract_key(text4, "alpha")) == {"x": 1} | |
| assert json.loads(JsonUtils.extract_key(text4, "beta")) == [1, 2, 3] | |
| assert json.loads(JsonUtils.extract_key(text4, "gamma")) == "texto" | |
| print("β Caso 4: extract_key OK") | |
| print("\nπ― Todos os smoke tests passaram.") | |