new feature: External guardrails/judges

This commit is contained in:
2026-08-24 11:14:10 -03:00
parent 05373deff2
commit fd37138c4f
384 changed files with 40593 additions and 3621 deletions

View File

@@ -10,6 +10,7 @@ from typing import Any
from urllib.parse import quote
import requests
import yaml
from .integrations.secure_pdf_crypto import encrypt_secure_pdf_value
from .parsers import parse_tim_bill_pdf
@@ -23,6 +24,29 @@ class TimApiError(RuntimeError):
self.attempts = list(attempts or [])
_INTEGRATION_DEFAULTS_CACHE: dict[str, str] | None = None
def get_tim_integration_value(*names: str, default: str = "") -> str:
"""Env > config/tim_integration_defaults.yaml > default explícito."""
global _INTEGRATION_DEFAULTS_CACHE
for name in names:
value = os.getenv(name)
if value is not None and str(value).strip():
return str(value).strip()
if _INTEGRATION_DEFAULTS_CACHE is None:
path = Path(os.getenv("TIM_INTEGRATION_DEFAULTS_PATH", "./config/tim_integration_defaults.yaml"))
try:
raw = yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else {}
except Exception as exc:
raise TimApiError(f"Falha ao ler defaults de integração TIM: {path}: {exc}") from exc
_INTEGRATION_DEFAULTS_CACHE = {str(k): str(v) for k, v in (raw or {}).items() if v not in (None, "")}
for name in names:
value = _INTEGRATION_DEFAULTS_CACHE.get(name)
if value is not None and str(value).strip():
return str(value).strip()
return default
class TimApiClient:
"""Thin TIM integration adapter.
@@ -31,9 +55,11 @@ class TimApiClient:
"""
FIXTURES = Path(__file__).with_name("fixtures")
INTEGRATION_DEFAULTS_PATH = Path(os.getenv("TIM_INTEGRATION_DEFAULTS_PATH", "./config/tim_integration_defaults.yaml"))
_integration_defaults_cache: dict[str, str] | None = None
def __init__(self) -> None:
self.mock = os.getenv("TIM_USE_MOCK_GATEWAY", "true").lower() in {"1", "true", "yes", "on"} or os.getenv("TIM_GATEWAY_MODE", "mock").lower() == "mock"
self.mock = os.getenv("TIM_USE_MOCK_GATEWAY", "false").lower() in {"1", "true", "yes", "on"} or os.getenv("TIM_GATEWAY_MODE", "real").lower() == "mock"
self.timeout = int(os.getenv("TIM_GATEWAY_DEFAULT_TIMEOUT", "30"))
self.max_retries = int(os.getenv("TIM_GATEWAY_RETRY_MAX_RETRIES", "3"))
self.backoff = float(os.getenv("TIM_GATEWAY_RETRY_BACKOFF_FACTOR", "0.5"))
@@ -42,14 +68,14 @@ class TimApiClient:
path = self.FIXTURES / f"{name}.json"
return json.loads(path.read_text(encoding="utf-8"))
@staticmethod
def _env_first(*names: str, default: str = "") -> str:
"""Retorna a primeira variável não vazia, preservando aliases do Contas original."""
for name in names:
value = os.getenv(name)
if value is not None and str(value).strip():
return str(value).strip()
return default
@classmethod
def _integration_defaults(cls) -> dict[str, str]:
get_tim_integration_value("__warmup__")
return dict(_INTEGRATION_DEFAULTS_CACHE or {})
@classmethod
def _env_first(cls, *names: str, default: str = "") -> str:
return get_tim_integration_value(*names, default=default)
@classmethod
def _auth_value(
@@ -155,7 +181,7 @@ class TimApiClient:
"POST", url, payload={"msisdn": msisdn},
headers=self._headers(
client_id=None, auth=self._env_first("TIM_COMPLETE_INVOICES_AUTH"),
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", default="AIAGENTCR")},
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", )},
),
timeout=int(self._env_first("TIM_COMPLETE_INVOICES_TIMEOUT", default="30")),
)
@@ -171,9 +197,9 @@ class TimApiClient:
password_names=("TIM_DIVERGENCIA_PASSWORD", "TIM_DIVERGENCIA_PASS", "TIM_DIVERGENCE_PASSWORD", "TIM_DIVERGENCE_PASS"),
)
headers = self._headers(client_id=None, auth=auth, extra={
"clientID": self._env_first("TIM_DIVERGENCIA_CLIENT_ID", default="AIAGENTCR"),
"clientID": self._env_first("TIM_DIVERGENCIA_CLIENT_ID", ),
})
params = {"channel": context.get("channel") or "AIAGENTCR"}
params = {"channel": context.get("channel") or self._env_first("TIM_DIVERGENCIA_CHANNEL")}
attempts: list[dict[str, Any]] = []
try:
result = self.request("GET", url, params=params, headers=headers, timeout=int(self._env_first("TIM_DIVERGENCIA_TIMEOUT", "TIM_DIVERGENCE_TIMEOUT", default="120")), attempt_log=attempts)
@@ -192,7 +218,7 @@ class TimApiClient:
"GET", url,
headers=self._headers(
client_id=None, auth=self._env_first("TIM_QUERY_AUTH", "TIM_CONSULTA_AUTH"),
extra={"clientId": self._env_first("TIM_CONSULTA_CLIENT_ID", default="AIAAGENTCR")},
extra={"clientId": self._env_first("TIM_CONSULTA_CLIENT_ID", )},
),
timeout=int(self._env_first("TIM_QUERY_TIMEOUT", "TIM_CONSULTA_TIMEOUT", default="30")),
)
@@ -207,7 +233,7 @@ class TimApiClient:
client_id=None,
auth=os.getenv("TIM_VAS_HISTORY_AUTH", ""),
extra={
"clientId": self._env_first("TIM_VAS_HISTORY_CLIENT_ID", default="AIAGENTCR"),
"clientId": self._env_first("TIM_VAS_HISTORY_CLIENT_ID", ),
"messageId": os.getenv("TIM_VAS_HISTORY_MESSAGE_ID", "") or str(uuid.uuid4()),
},
)
@@ -220,7 +246,7 @@ class TimApiClient:
digits = "".join(ch for ch in str(msisdn) if ch.isdigit())
normalized = digits[2:] if len(digits) == 13 else digits
app_id = str(service.get("appId") or service.get("app_id") or "")
csp_id = str(service.get("cspId") or service.get("csp_id") or os.getenv("TIM_DEFAULT_CSP_ID", "740"))
csp_id = str(service.get("cspId") or service.get("csp_id") or self._env_first("TIM_DEFAULT_CSP_ID"))
op = self._env_first("TIM_BLOCK_OPERATION_TYPE", "TIM_BLOQUEIO_OPERATION_TYPE", default="block")
base = {"Customer": {"Msisdn": normalized}, "AppId": app_id, "CspId": csp_id, "TypeOperation": op}
payloads = [
@@ -237,7 +263,7 @@ class TimApiClient:
auth=self._env_first("TIM_BLOCK_AUTH", "TIM_BLOQUEIO_AUTH"),
extra={
"Accept-Encoding": self._env_first("TIM_BLOCK_ACCEPT_ENCODING", "TIM_BLOQUEIO_ACCEPT_ENCODING", default="gzip,deflate"),
"clientId": self._env_first("TIM_BLOCK_CLIENT_ID", "TIM_BLOQUEIO_CLIENT_ID", default="AIAGENTCR"),
"clientId": self._env_first("TIM_BLOCK_CLIENT_ID", "TIM_BLOQUEIO_CLIENT_ID", ),
"messageId": str(uuid.uuid4()),
},
)
@@ -256,17 +282,17 @@ class TimApiClient:
return self.fixture("cancel_vas")
url = self._env_first("TIM_CANCELLATION_URL", "TIM_CANCELAMENTO_URL")
payload = {
"channel": "AIAGENTCR",
"channel": self._env_first("TIM_CANCELAMENTO_CHANNEL"),
"msisdn": msisdn,
"appId": str(service.get("appId") or service.get("app_id") or ""),
"cspId": str(service.get("cspId") or service.get("csp_id") or os.getenv("TIM_DEFAULT_CSP_ID", "740")),
"cspId": str(service.get("cspId") or service.get("csp_id") or self._env_first("TIM_DEFAULT_CSP_ID")),
"interactionProtocol": protocol,
}
headers = self._headers(
client_id=None,
auth=self._env_first("TIM_CANCELLATION_AUTH", "TIM_CANCELAMENTO_AUTH"),
extra={
"clientId": os.getenv("TIM_CANCELAMENTO_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "AIAGENTCR")),
"clientId": os.getenv("TIM_CANCELAMENTO_CLIENT_ID", self._env_first("TIM_DEFAULT_CLIENT_ID")),
"messageId": str(uuid.uuid4()),
"AuthorizationOAM": self._env_first("TIM_CANCELLATION_AUTH_OAM", "TIM_CANCELAMENTO_AUTH_OAM", "TIM_CANCELAMENTO_AUTHORIZATION_OAM"),
"Cn_field": self._env_first("TIM_CANCELLATION_CN_FIELD", "TIM_CANCELAMENTO_CN_FIELD"),
@@ -284,7 +310,7 @@ class TimApiClient:
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_CONTRATO_AUTH", ""),
extra={"clientId": self._env_first("TIM_CONTRATO_CLIENT_ID", default="AIAGENTCR")},
extra={"clientId": self._env_first("TIM_CONTRATO_CLIENT_ID", )},
)
return self.request("GET", f"{base}/{quote(msisdn)}", headers=headers, timeout=int(os.getenv("TIM_CONTRATO_TIMEOUT", "30")))
@@ -296,7 +322,7 @@ class TimApiClient:
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
extra={"ClientID": self._env_first("TIM_PROFILE_FULL_CLIENT_ID")},
)
return self.request("GET", url, headers=headers, timeout=int(self._env_first("TIM_PROFILE_FULL_TIMEOUT", default="30")))
@@ -321,7 +347,7 @@ class TimApiClient:
)
body = {
"socialSecNo": str(data.get("socialSecNo") or data.get("social_sec_no") or ""),
"channel": "AIAGENTCR",
"channel": self._env_first("TIM_PROTOCOL_CHANNEL"),
"customerId": str(data.get("customerId") or data.get("customer_id") or ""),
"assetId": str(data.get("assetId") or data.get("asset_id") or msisdn),
"customerName": str(data.get("customerName") or data.get("customer_name") or ""),
@@ -340,7 +366,7 @@ class TimApiClient:
"interaction": {
"protocol": str(data.get("interactionProtocol") or data.get("interaction_protocol") or ""),
"flagSms": True,
"source": "AIAGENTCR",
"source": self._env_first("TIM_DEFAULT_SOURCE"),
"callId": str(data.get("interactionCallId") or data.get("interaction_call_id") or ""),
"crmSource": "Siebel Pós",
"reasonId": str(data.get("interactionReasonId") or data.get("interaction_reason_id") or ""),
@@ -355,7 +381,7 @@ class TimApiClient:
client_id=None,
auth=str(data.get("Authorization") or os.getenv("TIM_PROTOCOL_AUTH", "")),
extra={
"clientId": str(data.get("clientId") or data.get("client_id") or os.getenv("TIM_PROTOCOL_CLIENT_ID", "BFFDIGITAL")),
"clientId": str(data.get("clientId") or data.get("client_id") or self._env_first("TIM_PROTOCOL_CLIENT_ID")),
"messageId": message_id,
"Authorizationoam": str(data.get("authorization_oam") or os.getenv("TIM_PROTOCOL_AUTHORIZATION_OAM", "")),
"Cn_field": str(data.get("cn_field") or os.getenv("TIM_PROTOCOL_CN_FIELD", "")),
@@ -378,7 +404,7 @@ class TimApiClient:
if self.mock:
return self.fixture("contestacao_tool")
data = dict(payload)
data.setdefault("userId", self._env_first("TIM_CUSTOMER_CONTESTATION_USER_ID", default="AIAGENTCR"))
data.setdefault("userId", self._env_first("TIM_CUSTOMER_CONTESTATION_USER_ID", ))
data.setdefault("customerIdCurrent", data.get("customerId") or "")
data.setdefault("customerType", "2")
data.setdefault("customerStatus", "1")
@@ -401,9 +427,9 @@ class TimApiClient:
data.setdefault("refundOption", "0")
data.setdefault("doubleRefund", False)
data.setdefault("manualContaCertaIndicator", False)
client_id = str(data.pop("clientId", "") or os.getenv("TIM_CUSTOMER_CONTESTATION_CLIENT_ID", "AIAGENTCR"))
client_id = str(data.pop("clientId", "") or self._env_first("TIM_CUSTOMER_CONTESTATION_CLIENT_ID"))
message_id = str(data.pop("messageId", "") or uuid.uuid4())
user_id = str(data.get("userId") or "AIAGENTCR").upper()
user_id = str(data.get("userId") or self._env_first("TIM_DEFAULT_CLIENT_ID")).upper()
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_CUSTOMER_CONTESTATION_AUTH", ""),
@@ -428,7 +454,7 @@ class TimApiClient:
}
for key in ("reason1", "reason2", "reason3", "auditChecks", "attachments"):
if data.get(key): service_request[key] = data[key]
body = {"channel": data.get("channel") or "AIAGENTCR", "serviceRequest": service_request}
body = {"channel": data.get("channel") or self._env_first("TIM_DEFAULT_CLIENT_ID"), "serviceRequest": service_request}
for key in ("msisdn", "customers", "date"):
if data.get(key): body[key] = data[key]
message_id = str(data.get("messageId") or data.get("ura_call_id") or data.get("session_id") or uuid.uuid4())
@@ -436,7 +462,7 @@ class TimApiClient:
client_id=None,
auth=os.getenv("TIM_SERVICE_REQUEST_STATUS_AUTH", ""),
extra={
"clientId": str(data.get("clientId") or os.getenv("TIM_SERVICE_REQUEST_STATUS_CLIENT_ID", "AIAGENTCR")),
"clientId": str(data.get("clientId") or self._env_first("TIM_SERVICE_REQUEST_STATUS_CLIENT_ID")),
"messageId": message_id,
"Authorizationoam": data.get("authorization_oam") or os.getenv("TIM_SERVICE_REQUEST_STATUS_AUTHORIZATION_OAM", ""),
"Cn_field": data.get("cn_field") or os.getenv("TIM_SERVICE_REQUEST_STATUS_CN_FIELD", ""),
@@ -463,7 +489,7 @@ class TimApiClient:
"totalAmount": data.get("invoiceTotalAmount") or "",
}
body = {
"channel": data.get("channel") or os.getenv("TIM_TRACKING_ACTIVITIES_CHANNEL", "AIAGENTCR"),
"channel": data.get("channel") or self._env_first("TIM_TRACKING_ACTIVITIES_CHANNEL"),
"customer": {"socialSecNo": data.get("socialSecNo") or "", "msisdn": data.get("msisdn") or ""},
"protocolNumber": data.get("protocolNumber") or "",
"invoice": invoice,
@@ -472,12 +498,12 @@ class TimApiClient:
"status": data.get("activityStatus") or "",
"id": data.get("activityId") or "",
},
"user": {"login": data.get("userLogin") or os.getenv("TIM_TRACKING_ACTIVITIES_USER_LOGIN", "SIEBELPOS_INBOUND")},
"user": {"login": data.get("userLogin") or self._env_first("TIM_TRACKING_ACTIVITIES_USER_LOGIN")},
}
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_TRACKING_ACTIVITIES_AUTH", ""),
extra={"clientId": data.get("clientId") or os.getenv("TIM_TRACKING_ACTIVITIES_CLIENT_ID", "AIAGENTCR")},
extra={"clientId": data.get("clientId") or self._env_first("TIM_TRACKING_ACTIVITIES_CLIENT_ID")},
)
return self.request("POST", os.getenv("TIM_TRACKING_ACTIVITIES_URL", ""), payload=body, headers=headers, timeout=int(os.getenv("TIM_TRACKING_ACTIVITIES_TIMEOUT", "30")))
@@ -487,7 +513,7 @@ class TimApiClient:
payload: dict[str, Any] = {
"msisdn": msisdn,
"senderAddress": context.get("sender_address") or os.getenv("TIM_SMS_SENDER_ADDRESS", "324"),
"senderName": context.get("sender_name") or os.getenv("TIM_SMS_SENDER_NAME", "TIM Brasil"),
"senderName": context.get("sender_name") or self._env_first("TIM_SMS_SENDER_NAME"),
"message": message,
"longURL": context.get("long_url") or message,
}
@@ -496,7 +522,7 @@ class TimApiClient:
headers = self._headers(
client_id=None,
auth=os.getenv("TIM_SMS_AUTH", ""),
extra={"clientId": os.getenv("TIM_SMS_CLIENT_ID", "AIAGENTCR")},
extra={"clientId": self._env_first("TIM_SMS_CLIENT_ID")},
)
attempts: list[dict[str, Any]] = []
result = self.request("POST", os.getenv("TIM_SMS_URL", ""), payload=payload, headers=headers, timeout=int(os.getenv("TIM_SMS_TIMEOUT", "30")), attempt_log=attempts)
@@ -514,7 +540,7 @@ class TimApiClient:
headers=self._headers(
client_id=None,
auth=self._env_first("TIM_COMPLETE_INVOICES_AUTH", "TIM_PROFILE_BILL_AUTH"),
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", "TIM_PROFILE_BILL_CLIENT_ID", default="AIAGENTCR")},
extra={"ClientID": self._env_first("TIM_COMPLETE_INVOICES_CLIENT_ID", "TIM_PROFILE_BILL_CLIENT_ID", )},
),
timeout=int(self._env_first("TIM_COMPLETE_INVOICES_TIMEOUT", "TIM_PROFILE_BILL_TIMEOUT", default="30")),
)
@@ -531,7 +557,7 @@ class TimApiClient:
headers=self._headers(
client_id=None,
auth=os.getenv("TIM_PROFILE_FULL_AUTH", ""),
extra={"ClientID": os.getenv("TIM_PROFILE_FULL_CLIENT_ID", "AIAGENTCR")},
extra={"ClientID": self._env_first("TIM_PROFILE_FULL_CLIENT_ID")},
),
timeout=int(self._env_first("TIM_PROFILE_FULL_TIMEOUT", default="30")),
)
@@ -581,7 +607,7 @@ class TimApiClient:
client_id=None,
auth=self._env_first("TIM_BILL_PDF_AUTH", "TIM_SECURE_PDF_AUTH", "TIM_INVOICE_RECOVER_AUTH"),
extra={
"clientId": self._env_first("TIM_BILL_PDF_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", default="AIAGENTCR"),
"clientId": self._env_first("TIM_BILL_PDF_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", ),
"Accept": "application/pdf",
},
)
@@ -621,7 +647,7 @@ class TimApiClient:
headers=self._headers(
client_id=None,
auth=self._env_first("TIM_SECURE_PDF_AUTH", "TIM_BILL_PDF_AUTH", "TIM_INVOICE_RECOVER_AUTH"),
extra={"clientid": self._env_first("TIM_INVOICE_RECOVER_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", default="AIAGENTCR")},
extra={"clientid": self._env_first("TIM_INVOICE_RECOVER_CLIENT_ID", "TIM_SECURE_PDF_CLIENT_ID", )},
),
timeout=int(os.getenv("TIM_INVOICE_RECOVER_TIMEOUT", "30")),
)