bugfix: oci_openai provider

This commit is contained in:
2026-08-27 20:34:15 -03:00
parent fc64c6ddd7
commit 0ecff719b7
329 changed files with 864 additions and 144 deletions

View File

@@ -448,25 +448,69 @@ class ComplianceRail(Guardrail):
requer = ctx.get("tipo_fluxo") == "ajuste" or ctx.get("requer_protocolo") is True requer = ctx.get("tipo_fluxo") == "ajuste" or ctx.get("requer_protocolo") is True
if not requer: if not requer:
return RailDecision(code=self.code, allowed=True, sanitized_text=None, reason="Compliance Anatel não aplicável", metadata={"calibrated": True}) return RailDecision(code=self.code, allowed=True, sanitized_text=None, reason="Compliance Anatel não aplicável", metadata={"calibrated": True})
expected = list(ctx.get("expected_protocols") or [])
if self._PROTOCOL_PATTERN.search(text or ""): original = text or ""
return RailDecision(code=self.code, allowed=True, reason="Resposta contém protocolo obrigatório", metadata={"calibrated": True}) expected = [str(value).strip() for value in (ctx.get("expected_protocols") or []) if str(value).strip()]
patched, missing = self._apply_protocol_fallback(text or "", expected)
if patched != (text or ""): # Quando o workflow informa os protocolos esperados, esses valores são a
# fonte de verdade. Valide-os diretamente no texto (cru ou vocalizado)
# antes de recorrer ao regex genérico. Isso evita tanto falso negativo
# por distância/Markdown quanto falso positivo por um protocolo diferente.
if expected:
patched, missing = self._apply_protocol_fallback(original, expected)
if not missing:
return RailDecision(
code=self.code,
allowed=True,
reason="Resposta contém o(s) protocolo(s) esperado(s)",
sanitized_text=None,
metadata={
"expected_protocols": expected,
"protocol_validation": "expected_values",
"mechanism": "deterministic",
"calibrated": True,
},
)
return RailDecision( return RailDecision(
code=self.code, code=self.code,
allowed=True, allowed=True,
reason="Resposta sem protocolo obrigatório; protocolo anexado deterministicamente", reason="Resposta sem protocolo obrigatório; protocolo anexado deterministicamente",
sanitized_text=patched, sanitized_text=patched,
metadata={"missing_protocols_spoken": missing, "expected_protocols": expected, "mechanism": "deterministic", "calibrated": True}, metadata={
"missing_protocols_spoken": missing,
"expected_protocols": expected,
"protocol_validation": "expected_values",
"mechanism": "deterministic",
"calibrated": True,
},
) )
# Compatibilidade para fluxos legados que exigem protocolo, mas não
# fornecem expected_protocols: nesse caso ainda usamos o reconhecimento
# genérico por regex.
if self._PROTOCOL_PATTERN.search(original):
return RailDecision(
code=self.code,
allowed=True,
reason="Resposta contém protocolo obrigatório",
metadata={
"protocol_validation": "generic_regex",
"mechanism": "deterministic",
"calibrated": True,
},
)
return RailDecision( return RailDecision(
code=self.code, code=self.code,
allowed=False, allowed=False,
reason="Resposta de ajuste sem número de protocolo", reason="Resposta de ajuste sem número de protocolo",
sanitized_text=text, sanitized_text=text,
metadata={ metadata={
"expected_protocols": expected, "mechanism": "deterministic", "calibrated": True, "expected_protocols": expected,
"protocol_validation": "generic_regex",
"mechanism": "deterministic",
"calibrated": True,
"terminal_action": "retry", "terminal_action": "retry",
}, },
) )

View File

@@ -135,6 +135,7 @@ src/agent_framework/llm/__init__.py
src/agent_framework/llm/base.py src/agent_framework/llm/base.py
src/agent_framework/llm/profile_resolver.py src/agent_framework/llm/profile_resolver.py
src/agent_framework/llm/providers.py src/agent_framework/llm/providers.py
src/agent_framework/llm/structured_output.py
src/agent_framework/llm/types.py src/agent_framework/llm/types.py
src/agent_framework/mcp/__init__.py src/agent_framework/mcp/__init__.py
src/agent_framework/mcp/client.py src/agent_framework/mcp/client.py

View File

@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
import re import re
@@ -246,13 +248,4 @@ class GlobalSupervisorRouter:
return text.strip() return text.strip()
def _parse_json(self, raw: Any) -> dict[str, Any]: def _parse_json(self, raw: Any) -> dict[str, Any]:
if isinstance(raw, dict): return parse_json_object(raw)
return raw
text = str(raw).strip()
if text.startswith("```"):
text = re.sub(r"^```(?:json)?", "", text).strip()
text = re.sub(r"```$", "", text).strip()
match = re.search(r"\{.*\}", text, flags=re.S)
if match:
text = match.group(0)
return json.loads(text)

View File

@@ -42,6 +42,8 @@ Uso via função standalone (compatibilidade):
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
from typing import Any from typing import Any
@@ -239,10 +241,10 @@ def classify_confirmation(
return False, f"invoke_error — fallback pessimista: {exc}" return False, f"invoke_error — fallback pessimista: {exc}"
try: try:
payload: dict[str, Any] = json.loads(raw) payload: dict[str, Any] = parse_json_object(raw)
except (json.JSONDecodeError, TypeError) as exc: except (ValueError, TypeError) as exc:
logger.warning( logger.warning(
"confirmation_rail.json_parse_failed raw=%r error=%r — fallback pessimista", "confirmation_rail.structured_parse_failed raw=%r error=%r — fallback pessimista",
raw[:200], raw[:200],
exc, exc,
) )

View File

@@ -16,6 +16,8 @@ Contexto de migração:
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -89,7 +91,7 @@ class RagsecRail:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"ragsec_rail.invoke_error session=%s exc=%r — assuming allowed", "ragsec_rail.invoke_error session=%s exc=%r — assuming allowed",

View File

@@ -15,6 +15,8 @@ Contexto de migração:
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -88,7 +90,7 @@ class RevprecRail:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"revprec_rail.invoke_error session=%s exc=%r — assuming allowed", "revprec_rail.invoke_error session=%s exc=%r — assuming allowed",

View File

@@ -10,6 +10,8 @@ Implementa o Protocol ``Rail`` de contracts.py (AT-06.2).
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -133,7 +135,7 @@ class CorrespondenciaItemRail:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"correspondencia_item_rail.invoke_error session=%s exc=%r — assuming no violation", "correspondencia_item_rail.invoke_error session=%s exc=%r — assuming no violation",

View File

@@ -8,6 +8,8 @@ Implementa o Protocol ``Rail`` de contracts.py (AT-06.4).
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -127,7 +129,7 @@ class GroundednessRail:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"groundedness_rail.invoke_error session=%s exc=%r — assuming no violation", "groundedness_rail.invoke_error session=%s exc=%r — assuming no violation",

View File

@@ -10,6 +10,8 @@ Implementa o Protocol ``Rail`` de contracts.py (AT-06.1).
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -131,7 +133,7 @@ class IntencaoCancelarRail:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"intencao_cancelar_rail.invoke_error session=%s exc=%r — assuming no violation", "intencao_cancelar_rail.invoke_error session=%s exc=%r — assuming no violation",

View File

@@ -10,6 +10,8 @@ Implementa o Protocol ``Rail`` de contracts.py (AT-06.3).
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -135,7 +137,7 @@ class QuantidadeCoerente:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"quantidade_coerente_rail.invoke_error session=%s exc=%r — assuming no violation", "quantidade_coerente_rail.invoke_error session=%s exc=%r — assuming no violation",

View File

@@ -11,6 +11,8 @@ Implementa o Protocol ``Rail`` de contracts.py (AT-06.6).
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -131,7 +133,7 @@ class ServicoCorrretoRail:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"servico_correto_rail.invoke_error session=%s exc=%r — assuming no violation", "servico_correto_rail.invoke_error session=%s exc=%r — assuming no violation",

View File

@@ -11,6 +11,8 @@ Implementa o Protocol ``Rail`` de contracts.py (AT-06.5).
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -128,7 +130,7 @@ class VerbalizacaoPrematura:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"verbalizacao_prematura_rail.invoke_error session=%s exc=%r — assuming no violation", "verbalizacao_prematura_rail.invoke_error session=%s exc=%r — assuming no violation",

View File

@@ -22,6 +22,8 @@ Fallback conservador:
""" """
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
@@ -155,7 +157,7 @@ class ToxRail:
try: try:
raw = self._client.invoke(self.code, input_vars) raw = self._client.invoke(self.code, input_vars)
result: dict = json.loads(raw) if isinstance(raw, str) else raw result: dict = parse_json_object(raw) if isinstance(raw, str) else raw
except Exception as exc: except Exception as exc:
logger.error( logger.error(
"tox_rail.invoke_error session=%s exc=%r — assuming allowed", "tox_rail.invoke_error session=%s exc=%r — assuming allowed",

View File

@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import os import os
import re import re
@@ -85,15 +87,9 @@ def _extract_text(raw: Any) -> str:
def _parse_json(text: str) -> dict[str, Any]: def _parse_json(text: str) -> dict[str, Any]:
try: try:
return json.loads(text) return parse_json_object(text)
except Exception: except Exception:
match = re.search(r"\{[\s\S]*\}", text or "") return {"allowed": False, "label": "ERROR", "reason": (text or "")[:500]}
if match:
try:
return json.loads(match.group(0))
except Exception:
pass
return {"allowed": False, "label": "ERROR", "reason": (text or "")[:500]}
def _first_substring_match(text: str, triggers: tuple[str, ...]) -> str | None: def _first_substring_match(text: str, triggers: tuple[str, ...]) -> str | None:

View File

@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from agent_framework.llm.structured_output import parse_json_object
import json import json
import logging import logging
from typing import Any from typing import Any
@@ -102,16 +104,4 @@ def _safe_context(context: dict[str, Any]) -> dict[str, Any]:
def _parse_json(raw: Any) -> dict[str, Any]: def _parse_json(raw: Any) -> dict[str, Any]:
text = str(raw or "").strip() return parse_json_object(raw)
if text.startswith("```"):
text = text.strip("`")
if text.lower().startswith("json"):
text = text[4:].strip()
start = text.find("{")
end = text.rfind("}")
if start >= 0 and end >= start:
text = text[start:end + 1]
data = json.loads(text)
if not isinstance(data, dict):
raise ValueError("LLM guardrail returned non-object JSON")
return data

View File

@@ -448,25 +448,69 @@ class ComplianceRail(Guardrail):
requer = ctx.get("tipo_fluxo") == "ajuste" or ctx.get("requer_protocolo") is True requer = ctx.get("tipo_fluxo") == "ajuste" or ctx.get("requer_protocolo") is True
if not requer: if not requer:
return RailDecision(code=self.code, allowed=True, sanitized_text=None, reason="Compliance Anatel não aplicável", metadata={"calibrated": True}) return RailDecision(code=self.code, allowed=True, sanitized_text=None, reason="Compliance Anatel não aplicável", metadata={"calibrated": True})
expected = list(ctx.get("expected_protocols") or [])
if self._PROTOCOL_PATTERN.search(text or ""): original = text or ""
return RailDecision(code=self.code, allowed=True, reason="Resposta contém protocolo obrigatório", metadata={"calibrated": True}) expected = [str(value).strip() for value in (ctx.get("expected_protocols") or []) if str(value).strip()]
patched, missing = self._apply_protocol_fallback(text or "", expected)
if patched != (text or ""): # Quando o workflow informa os protocolos esperados, esses valores são a
# fonte de verdade. Valide-os diretamente no texto (cru ou vocalizado)
# antes de recorrer ao regex genérico. Isso evita tanto falso negativo
# por distância/Markdown quanto falso positivo por um protocolo diferente.
if expected:
patched, missing = self._apply_protocol_fallback(original, expected)
if not missing:
return RailDecision(
code=self.code,
allowed=True,
reason="Resposta contém o(s) protocolo(s) esperado(s)",
sanitized_text=None,
metadata={
"expected_protocols": expected,
"protocol_validation": "expected_values",
"mechanism": "deterministic",
"calibrated": True,
},
)
return RailDecision( return RailDecision(
code=self.code, code=self.code,
allowed=True, allowed=True,
reason="Resposta sem protocolo obrigatório; protocolo anexado deterministicamente", reason="Resposta sem protocolo obrigatório; protocolo anexado deterministicamente",
sanitized_text=patched, sanitized_text=patched,
metadata={"missing_protocols_spoken": missing, "expected_protocols": expected, "mechanism": "deterministic", "calibrated": True}, metadata={
"missing_protocols_spoken": missing,
"expected_protocols": expected,
"protocol_validation": "expected_values",
"mechanism": "deterministic",
"calibrated": True,
},
) )
# Compatibilidade para fluxos legados que exigem protocolo, mas não
# fornecem expected_protocols: nesse caso ainda usamos o reconhecimento
# genérico por regex.
if self._PROTOCOL_PATTERN.search(original):
return RailDecision(
code=self.code,
allowed=True,
reason="Resposta contém protocolo obrigatório",
metadata={
"protocol_validation": "generic_regex",
"mechanism": "deterministic",
"calibrated": True,
},
)
return RailDecision( return RailDecision(
code=self.code, code=self.code,
allowed=False, allowed=False,
reason="Resposta de ajuste sem número de protocolo", reason="Resposta de ajuste sem número de protocolo",
sanitized_text=text, sanitized_text=text,
metadata={ metadata={
"expected_protocols": expected, "mechanism": "deterministic", "calibrated": True, "expected_protocols": expected,
"protocol_validation": "generic_regex",
"mechanism": "deterministic",
"calibrated": True,
"terminal_action": "retry", "terminal_action": "retry",
}, },
) )

Some files were not shown because too many files have changed in this diff Show More