first commit

This commit is contained in:
2026-06-13 08:23:21 -03:00
commit 89c23fb0ed
439 changed files with 32801 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
# Backoffice MCP HTTP Server
Servidor HTTP compatível com o `MCPToolRouter` do framework.
## Configuração
Copie o arquivo de exemplo:
```bash
cp .env.backoffice_mcp.example .env.backoffice_mcp
```
Principais variáveis:
```text
BACKOFFICE_MCP_HOST=0.0.0.0
BACKOFFICE_MCP_PORT=8010
BACKOFFICE_MCP_USE_MOCK=true
BACKOFFICE_MCP_BACKEND_TYPE=mock
BACKOFFICE_MCP_REST_BASE_URL=http://localhost:8080/backoffice
BACKOFFICE_MCP_REST_API_KEY=
BACKOFFICE_MCP_ORACLE_USER=
BACKOFFICE_MCP_ORACLE_PASSWORD=
BACKOFFICE_MCP_ORACLE_DSN=
```
## Execução local
```bash
./scripts/run_backoffice_mcp.sh
```
## Health
```bash
curl http://localhost:8010/health
```
## Contrato
- `GET /tools/list`
- `POST /tools/call`
Tools iniciais:
- `consultar_reclamacao`
- `consultar_cliente_backoffice`
- `registrar_acao_backoffice`
## Backends
- `mock`: dados em memória.
- `rest`: chama endpoints REST configurados por `BACKOFFICE_MCP_REST_BASE_URL`.
- `oracle`: placeholder explícito para receber queries/procedures reais quando o backoffice original for anexado.

View File

@@ -0,0 +1 @@
"""Servidor MCP/HTTP migrado para as ferramentas do backoffice."""

View File

@@ -0,0 +1,37 @@
from __future__ import annotations
from typing import Any, Dict
import httpx
from .settings import BackofficeMCPSettings
class BackofficeRESTClient:
def __init__(self, settings: BackofficeMCPSettings):
self.settings = settings
self.base_url = settings.rest_base_url.rstrip("/")
def _headers(self) -> Dict[str, str]:
if not self.settings.rest_api_key:
return {}
value = self.settings.rest_api_key
if self.settings.rest_auth_header.lower() == "authorization" and not value.lower().startswith("bearer "):
value = f"Bearer {value}"
return {self.settings.rest_auth_header: value}
def get_json(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]:
if not self.base_url:
raise RuntimeError("BACKOFFICE_MCP_REST_BASE_URL não configurado")
with httpx.Client(timeout=self.settings.rest_timeout_seconds, headers=self._headers()) as client:
resp = client.get(f"{self.base_url}/{path.lstrip('/')}", params=params)
resp.raise_for_status()
return resp.json()
def post_json(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
if not self.base_url:
raise RuntimeError("BACKOFFICE_MCP_REST_BASE_URL não configurado")
with httpx.Client(timeout=self.settings.rest_timeout_seconds, headers=self._headers()) as client:
resp = client.post(f"{self.base_url}/{path.lstrip('/')}", json=payload)
resp.raise_for_status()
return resp.json()

View File

@@ -0,0 +1,176 @@
from __future__ import annotations
from typing import Any, Callable, Dict
from fastapi import FastAPI, HTTPException
from .models import ToolCallRequest, ToolCallResponse, ToolDefinition, ToolListResponse
from .settings import get_settings
from .store import (
consultar_cliente_backoffice,
consultar_reclamacao,
registrar_acao_backoffice,
consultar_siebel_caso,
registrar_acao_siebel,
consultar_imdb_cliente,
consultar_speech_analytics,
consultar_tais_kb,
consultar_abrt,
consultar_portabilidade,
buscar_templates_emulador,
gerar_rascunho_emulador,
)
app = FastAPI(title="Backoffice MCP HTTP Server", version="1.0.0")
TOOLS: Dict[str, Dict[str, Any]] = {
"consultar_reclamacao": {
"description": "Consulta reclamação/protocolo de backoffice/ANATEL.",
"handler": consultar_reclamacao,
"input_schema": {
"type": "object",
"properties": {
"protocol_id": {"type": "string"},
"customer_key": {"type": "string"},
"interaction_key": {"type": "string"},
},
"required": ["protocol_id"],
},
},
"consultar_cliente_backoffice": {
"description": "Consulta contexto operacional do cliente para backoffice.",
"handler": consultar_cliente_backoffice,
"input_schema": {
"type": "object",
"properties": {
"customer_key": {"type": "string"},
"contract_key": {"type": "string"},
"session_key": {"type": "string"},
"cpf": {"type": "string"},
"cnpj": {"type": "string"},
"document": {"type": "string"},
"document_type": {"type": "string"},
},
"required": ["customer_key"],
},
},
"registrar_acao_backoffice": {
"description": "Registra ação operacional, parecer ou encaminhamento no sistema de backoffice.",
"handler": registrar_acao_backoffice,
"input_schema": {
"type": "object",
"properties": {
"protocol_id": {"type": "string"},
"action_text": {"type": "string"},
"operator_session": {"type": "string"},
},
"required": ["protocol_id", "action_text"],
},
},
"consultar_siebel_caso": {
"description": "Consulta caso/SR no Siebel para reclamação ANATEL/backoffice.",
"handler": consultar_siebel_caso,
"input_schema": {"type": "object", "properties": {"protocol_id": {"type": "string"}, "interaction_key": {"type": "string"}, "customer_key": {"type": "string"}}},
},
"registrar_acao_siebel": {
"description": "Registra ação, reclassificação, fechamento ou atualização no Siebel.",
"handler": registrar_acao_siebel,
"input_schema": {"type": "object", "properties": {"protocol_id": {"type": "string"}, "action_text": {"type": "string"}, "operator_session": {"type": "string"}}, "required": ["action_text"]},
},
"consultar_imdb_cliente": {
"description": "Consulta enriquecimento IMDB/PMID do cliente.",
"handler": consultar_imdb_cliente,
"input_schema": {"type": "object", "properties": {"customer_key": {"type": "string"}, "contract_key": {"type": "string"}, "session_key": {"type": "string"}, "cpf": {"type": "string"}, "cnpj": {"type": "string"}, "document": {"type": "string"}, "document_type": {"type": "string"}}},
},
"consultar_speech_analytics": {
"description": "Consulta histórico/resumo do Speech Analytics.",
"handler": consultar_speech_analytics,
"input_schema": {"type": "object", "properties": {"protocol_id": {"type": "string"}, "customer_key": {"type": "string"}, "interaction_key": {"type": "string"}, "cpf": {"type": "string"}, "cnpj": {"type": "string"}, "document": {"type": "string"}, "document_type": {"type": "string"}}},
},
"consultar_tais_kb": {
"description": "Consulta base TAIS KB/RAG e templates de resposta.",
"handler": consultar_tais_kb,
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}, "protocol_id": {"type": "string"}, "customer_key": {"type": "string"}}},
},
"consultar_abrt": {
"description": "Consulta suporte ABRT associado ao cliente/caso.",
"handler": consultar_abrt,
"input_schema": {"type": "object", "properties": {"customer_key": {"type": "string"}, "protocol_id": {"type": "string"}, "cpf": {"type": "string"}, "cnpj": {"type": "string"}, "document": {"type": "string"}, "document_type": {"type": "string"}}},
},
"consultar_portabilidade": {
"description": "Consulta status de portabilidade.",
"handler": consultar_portabilidade,
"input_schema": {"type": "object", "properties": {"customer_key": {"type": "string"}, "contract_key": {"type": "string"}, "cpf": {"type": "string"}, "cnpj": {"type": "string"}, "document": {"type": "string"}, "document_type": {"type": "string"}}},
},
"buscar_templates_emulador": {
"description": "Busca templates/documentos para Response Emulator.",
"handler": buscar_templates_emulador,
"input_schema": {"type": "object", "properties": {"protocol_id": {"type": "string"}, "query": {"type": "string"}}},
},
"gerar_rascunho_emulador": {
"description": "Gera rascunho de resposta do Response Emulator.",
"handler": gerar_rascunho_emulador,
"input_schema": {"type": "object", "properties": {"protocol_id": {"type": "string"}, "selected_actions": {"type": "array"}, "operator_instructions": {"type": "string"}}},
},
}
@app.get("/health")
def health() -> Dict[str, Any]:
settings = get_settings()
return {
"status": "ok",
"service": "backoffice_mcp_server",
"use_mock": settings.use_mock,
"backend_type": settings.backend_type,
"rest_configured": bool(settings.rest_base_url),
"oracle_configured": bool(settings.oracle_dsn and settings.oracle_user),
"tim_clients_configured": settings.tim_clients_configured(),
"tim_env": {
"PMID_API_HOST": bool(settings.pmid_api_host),
"SIEBEL_API_HOST": bool(settings.siebel_api_host),
"SPEECH_PREDICTION_BASE_URL": bool(settings.speech_prediction_base_url),
"SPEECH_HISTORY_BASE_URL": bool(settings.speech_history_base_url),
"TAIS_DB_DSN": bool(settings.tais_db_dsn),
"TAIS_GENAI_ENDPOINT": bool(settings.tais_genai_endpoint),
},
"tools": list(TOOLS.keys()),
}
@app.get("/tools/list", response_model=ToolListResponse)
def list_tools() -> ToolListResponse:
return ToolListResponse(
tools=[
ToolDefinition(
name=name,
description=cfg["description"],
input_schema=cfg["input_schema"],
)
for name, cfg in TOOLS.items()
]
)
@app.post("/tools/call", response_model=ToolCallResponse)
def call_tool(req: ToolCallRequest) -> ToolCallResponse:
cfg = TOOLS.get(req.name)
if not cfg:
raise HTTPException(status_code=404, detail=f"Ferramenta não encontrada: {req.name}")
handler: Callable[..., Dict[str, Any]] = cfg["handler"]
try:
schema_props = ((cfg.get("input_schema") or {}).get("properties") or {})
allowed = set(schema_props.keys())
# O MCPToolRouter do framework preserva original_context/extra_args por
# design. O servidor de domínio deve aceitar somente os parâmetros da
# tool, senão handlers Python falham com unexpected keyword argument.
arguments = dict(req.arguments or {})
filtered_arguments = {k: v for k, v in arguments.items() if not allowed or k in allowed}
result = handler(**filtered_arguments)
return ToolCallResponse(ok=True, tool=req.name, result=result)
except TypeError as exc:
return ToolCallResponse(ok=False, tool=req.name, error=f"Parâmetros inválidos: {exc}")
except Exception as exc: # pragma: no cover
return ToolCallResponse(ok=False, tool=req.name, error=str(exc))

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field, AliasChoices, ConfigDict
class ToolCallRequest(BaseModel):
model_config = ConfigDict(populate_by_name=True)
# O client do seu framework envia {"tool_name": "..."}.
# Mantemos também "name" para chamadas manuais/curl.
name: str = Field(
...,
validation_alias=AliasChoices("name", "tool_name"),
description="Nome da ferramenta a executar",
)
arguments: Dict[str, Any] = Field(default_factory=dict)
context: Dict[str, Any] = Field(default_factory=dict)
class ToolCallResponse(BaseModel):
ok: bool
tool: str
result: Dict[str, Any] = Field(default_factory=dict)
error: Optional[str] = None
class ToolDefinition(BaseModel):
name: str
description: str
input_schema: Dict[str, Any]
class ToolListResponse(BaseModel):
tools: List[ToolDefinition]

View File

@@ -0,0 +1,81 @@
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from typing import Literal
from dotenv import load_dotenv
from pydantic import AliasChoices, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
# Carrega o .env do projeto backoffice. Variáveis já exportadas no shell continuam com prioridade.
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
load_dotenv(_PROJECT_ROOT / ".env", override=False)
load_dotenv(_PROJECT_ROOT / ".env.backoffice_mcp", override=False)
class BackofficeMCPSettings(BaseSettings):
"""Configurações do MCP Server de Backoffice.
Suporta dois estilos de configuração:
- novo/padronizado: BACKOFFICE_MCP_*
- original develop TIM: PMID_*, SIEBEL_*, SPEECH_*, TAIS_*, ABRT_*, PORTABILITY_*
O default agora é usar os clients TIM originais quando não for explicitado mock,
para não mascarar erro de configuração com resposta simulada.
"""
model_config = SettingsConfigDict(
env_file=str(_PROJECT_ROOT / ".env"),
extra="ignore",
case_sensitive=False,
)
host: str = Field(default="0.0.0.0", validation_alias=AliasChoices("BACKOFFICE_MCP_HOST", "MCP_HOST"))
port: int = Field(default=8010, validation_alias=AliasChoices("BACKOFFICE_MCP_PORT", "MCP_PORT"))
log_level: str = Field(default="info", validation_alias=AliasChoices("BACKOFFICE_MCP_LOG_LEVEL", "LOG_LEVEL"))
use_mock: bool = Field(default=False, validation_alias=AliasChoices("BACKOFFICE_MCP_USE_MOCK", "MCP_USE_MOCK", "USE_MOCK"))
backend_type: Literal["mock", "tim_clients", "rest", "oracle"] = Field(
default="tim_clients",
validation_alias=AliasChoices("BACKOFFICE_MCP_BACKEND_TYPE", "MCP_BACKEND_TYPE"),
)
# REST genérico opcional. Não é usado quando backend_type=tim_clients.
rest_base_url: str = Field(default="", validation_alias=AliasChoices("BACKOFFICE_MCP_REST_BASE_URL", "BACKOFFICE_REST_BASE_URL"))
rest_timeout_seconds: float = Field(default=20.0, validation_alias=AliasChoices("BACKOFFICE_MCP_REST_TIMEOUT_SECONDS", "BACKOFFICE_REST_TIMEOUT_SECONDS"))
rest_api_key: str = Field(default="", validation_alias=AliasChoices("BACKOFFICE_MCP_REST_API_KEY", "BACKOFFICE_REST_API_KEY"))
rest_auth_header: str = Field(default="Authorization", validation_alias=AliasChoices("BACKOFFICE_MCP_REST_AUTH_HEADER", "BACKOFFICE_REST_AUTH_HEADER"))
# Oracle backend opcional.
oracle_user: str = Field(default="", validation_alias=AliasChoices("BACKOFFICE_MCP_ORACLE_USER", "ORACLE_USER"))
oracle_password: str = Field(default="", validation_alias=AliasChoices("BACKOFFICE_MCP_ORACLE_PASSWORD", "ORACLE_PASSWORD"))
oracle_dsn: str = Field(default="", validation_alias=AliasChoices("BACKOFFICE_MCP_ORACLE_DSN", "ORACLE_DSN"))
oracle_wallet_location: str = Field(default="", validation_alias=AliasChoices("BACKOFFICE_MCP_ORACLE_WALLET_LOCATION", "ORACLE_WALLET_LOCATION"))
oracle_wallet_password: str = Field(default="", validation_alias=AliasChoices("BACKOFFICE_MCP_ORACLE_WALLET_PASSWORD", "ORACLE_WALLET_PASSWORD"))
# TIM/develop original. Esses campos existem para health/debug e validação rápida.
pmid_api_host: str | None = Field(default=None, validation_alias=AliasChoices("PMID_API_HOST"))
siebel_api_host: str | None = Field(default=None, validation_alias=AliasChoices("SIEBEL_API_HOST"))
speech_prediction_base_url: str | None = Field(default=None, validation_alias=AliasChoices("SPEECH_PREDICTION_BASE_URL"))
speech_history_base_url: str | None = Field(default=None, validation_alias=AliasChoices("SPEECH_HISTORY_BASE_URL"))
tais_db_dsn: str | None = Field(default=None, validation_alias=AliasChoices("TAIS_DB_DSN"))
tais_genai_endpoint: str | None = Field(default=None, validation_alias=AliasChoices("TAIS_GENAI_ENDPOINT"))
fail_open_on_backend_error: bool = Field(default=True, validation_alias=AliasChoices("BACKOFFICE_MCP_FAIL_OPEN_ON_BACKEND_ERROR", "MCP_FAIL_OPEN_ON_BACKEND_ERROR"))
@field_validator("rest_base_url")
@classmethod
def validate_rest_base_url(cls, v: str) -> str:
value = (v or "").strip()
if value and not value.startswith(("http://", "https://")):
raise ValueError("BACKOFFICE_MCP_REST_BASE_URL precisa começar com http:// ou https://")
return value
def tim_clients_configured(self) -> bool:
return bool(self.pmid_api_host or self.siebel_api_host or self.speech_prediction_base_url or self.tais_db_dsn)
@lru_cache(maxsize=1)
def get_settings() -> BackofficeMCPSettings:
return BackofficeMCPSettings()

View File

@@ -0,0 +1,343 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Dict, List
import asyncio
from .clients import BackofficeRESTClient
from .settings import get_settings
def _jsonable(value: Any) -> Any:
if value is None or isinstance(value, (str, int, float, bool)):
return value
if isinstance(value, dict):
return {str(k): _jsonable(v) for k, v in value.items()}
if isinstance(value, (list, tuple, set)):
return [_jsonable(v) for v in value]
if hasattr(value, "model_dump"):
return _jsonable(value.model_dump())
if hasattr(value, "dict"):
return _jsonable(value.dict())
return str(value)
def _run_async(coro):
return asyncio.run(coro)
def _tim_clients_enabled() -> bool:
settings = get_settings()
return (not settings.use_mock) and settings.backend_type == "tim_clients"
def _tim_unavailable(reason: str) -> Dict[str, Any]:
return {"ok": False, "backend_type": "tim_clients", "source": "tim_clients", "reason": reason}
_RECLAMACOES: Dict[str, Dict[str, Any]] = {
"12345": {
"protocol_id": "12345",
"status": "em_analise",
"canal_origem": "ANATEL",
"motivo": "Contestação de cobrança em fatura",
"prioridade": "alta",
"prazo_sla_horas": 48,
"customer_key": "11999999999",
"contract_key": "3000131180",
"resumo": "Cliente questiona valor de serviço adicional na última fatura.",
},
"98765": {
"protocol_id": "98765",
"status": "pendente_cliente",
"canal_origem": "Ouvidoria",
"motivo": "Falha recorrente de atendimento",
"prioridade": "media",
"prazo_sla_horas": 72,
"customer_key": "11888888888",
"contract_key": "3000999999",
"resumo": "Cliente solicita retorno formal sobre atendimento anterior.",
},
}
_CLIENTES: Dict[str, Dict[str, Any]] = {
"11999999999": {
"customer_key": "11999999999",
"nome": "Cliente Exemplo TIM",
"segmento": "consumer",
"plano": "TIM Controle Exemplo",
"contratos": ["3000131180"],
"flags": ["possui_reclamacao_aberta", "nao_ofertar_sem_contexto"],
"ultimas_interacoes": [
{"data": "2026-06-01", "canal": "URA", "resumo": "Consulta de fatura."},
{"data": "2026-06-03", "canal": "Backoffice", "resumo": "Abertura de análise."},
],
},
"11888888888": {
"customer_key": "11888888888",
"nome": "Cliente Exemplo Ouvidoria",
"segmento": "consumer",
"plano": "TIM Pós Exemplo",
"contratos": ["3000999999"],
"flags": ["ouvidoria"],
"ultimas_interacoes": [],
},
}
_ACOES: List[Dict[str, Any]] = []
def _backend_error(exc: Exception) -> Dict[str, Any]:
settings = get_settings()
if settings.fail_open_on_backend_error:
return {"ok": False, "backend_error": str(exc), "backend_type": settings.backend_type}
raise exc
def consultar_reclamacao(protocol_id: str | None, customer_key: str | None = None, interaction_key: str | None = None) -> Dict[str, Any]:
settings = get_settings()
if not settings.use_mock and settings.backend_type == "rest":
try:
return BackofficeRESTClient(settings).get_json("reclamacoes", {"protocol_id": protocol_id, "customer_key": customer_key, "interaction_key": interaction_key})
except Exception as exc:
return _backend_error(exc)
if not settings.use_mock and settings.backend_type == "oracle":
return _backend_error(RuntimeError("Backend Oracle ainda precisa receber a query/procedure real do backoffice original"))
key = protocol_id or interaction_key
if not key:
return {"found": False, "reason": "protocol_id ausente"}
data = _RECLAMACOES.get(str(key))
if not data:
return {"found": False, "protocol_id": key, "reason": "reclamacao_nao_encontrada"}
if customer_key and data.get("customer_key") != customer_key:
return {"found": False, "protocol_id": key, "reason": "protocolo_nao_pertence_ao_cliente"}
return {"found": True, **data}
def consultar_cliente_backoffice(customer_key: str | None = None, contract_key: str | None = None, session_key: str | None = None, cpf: str | None = None, cnpj: str | None = None, document: str | None = None, document_type: str | None = None) -> Dict[str, Any]:
customer_key = customer_key or cpf or cnpj or document
settings = get_settings()
if _tim_clients_enabled():
return consultar_imdb_cliente(customer_key=customer_key, contract_key=contract_key, session_key=session_key)
if not settings.use_mock and settings.backend_type == "rest":
try:
return BackofficeRESTClient(settings).get_json("clientes", {"customer_key": customer_key, "contract_key": contract_key, "session_key": session_key})
except Exception as exc:
return _backend_error(exc)
if not settings.use_mock and settings.backend_type == "oracle":
return _backend_error(RuntimeError("Backend Oracle ainda precisa receber a query/procedure real do backoffice original"))
if not customer_key:
return {"found": False, "reason": "customer_key ausente"}
data = _CLIENTES.get(str(customer_key))
if not data:
return {"found": False, "customer_key": customer_key, "reason": "cliente_nao_encontrado"}
if contract_key and contract_key not in data.get("contratos", []):
return {"found": True, "warning": "contrato_nao_localizado_para_cliente", **data}
return {"found": True, "session_key": session_key, **data}
def registrar_acao_backoffice(protocol_id: str | None, action_text: str | None, operator_session: str | None = None) -> Dict[str, Any]:
settings = get_settings()
if not settings.use_mock and settings.backend_type == "rest":
try:
return BackofficeRESTClient(settings).post_json("acoes", {"protocol_id": protocol_id, "action_text": action_text, "operator_session": operator_session})
except Exception as exc:
return _backend_error(exc)
if not settings.use_mock and settings.backend_type == "oracle":
return _backend_error(RuntimeError("Backend Oracle ainda precisa receber a query/procedure real do backoffice original"))
if not protocol_id:
return {"registered": False, "reason": "protocol_id ausente"}
if not action_text:
return {"registered": False, "reason": "action_text ausente"}
action = {
"id": len(_ACOES) + 1,
"protocol_id": str(protocol_id),
"action_text": action_text,
"operator_session": operator_session,
"created_at": datetime.now(timezone.utc).isoformat(),
"status": "registrado",
}
_ACOES.append(action)
return {"registered": True, "action": action}
# ---------------------------------------------------------------------------
# Tools adicionais reintroduzidas da branch funcional do backoffice original.
# Elas mantêm o mesmo padrão mock/rest/oracle do servidor convertido. Em modo
# REST, os endpoints esperados são configurados no serviço corporativo:
# /siebel/cases, /imdb/customer, /speech/history, /tais-kb/search, etc.
# Em modo Oracle, os pontos ficam preparados para procedures/queries reais.
# ---------------------------------------------------------------------------
def _rest_or_mock(endpoint: str, params: Dict[str, Any], mock_payload: Dict[str, Any]) -> Dict[str, Any]:
settings = get_settings()
if not settings.use_mock and settings.backend_type == "rest":
try:
return BackofficeRESTClient(settings).get_json(endpoint, params)
except Exception as exc:
return _backend_error(exc)
if not settings.use_mock and settings.backend_type == "oracle":
return _backend_error(RuntimeError(f"Backend Oracle pendente para endpoint lógico {endpoint}"))
if not settings.use_mock and settings.backend_type == "tim_clients":
return _tim_unavailable(f"Tool {endpoint} ainda não tem client TIM direto mapeado; use tool específica ou configure BACKOFFICE_MCP_BACKEND_TYPE=rest com BACKOFFICE_MCP_REST_BASE_URL")
return mock_payload
def consultar_siebel_caso(protocol_id: str | None = None, interaction_key: str | None = None, customer_key: str | None = None) -> Dict[str, Any]:
case_id = protocol_id or interaction_key or "12345"
base = consultar_reclamacao(case_id, customer_key=customer_key, interaction_key=interaction_key)
mock = {
"found": bool(base.get("found")),
"source": "mock_siebel",
"case_id": case_id,
"sr_status": "open" if base.get("found") else "not_found",
"payload": base,
}
return _rest_or_mock("siebel/cases", {"protocol_id": protocol_id, "interaction_key": interaction_key, "customer_key": customer_key}, mock)
def registrar_acao_siebel(protocol_id: str | None = None, action_text: str | None = None, operator_session: str | None = None) -> Dict[str, Any]:
settings = get_settings()
payload = {"protocol_id": protocol_id, "action_text": action_text, "operator_session": operator_session}
if not settings.use_mock and settings.backend_type == "rest":
try:
return BackofficeRESTClient(settings).post_json("siebel/actions", payload)
except Exception as exc:
return _backend_error(exc)
if not settings.use_mock and settings.backend_type == "oracle":
return _backend_error(RuntimeError("Backend Oracle pendente para registrar_acao_siebel"))
result = registrar_acao_backoffice(protocol_id, action_text, operator_session)
return {"source": "mock_siebel", "siebel_registered": result.get("registered", False), **result}
def consultar_imdb_cliente(customer_key: str | None = None, contract_key: str | None = None, session_key: str | None = None, cpf: str | None = None, cnpj: str | None = None, document: str | None = None, document_type: str | None = None) -> Dict[str, Any]:
customer_key = customer_key or cpf or cnpj or document
settings = get_settings()
if _tim_clients_enabled():
if not customer_key:
return {"found": False, "ok": False, "source": "tim_imdb", "reason": "customer_key/msisdn ausente"}
try:
from src.components.clients.imdb_client import ImdbClient
from src.core.config import settings as app_settings
data, http_meta = _run_async(ImdbClient().get_imdb_access_data_with_retry(str(customer_key), app_settings.PMID_API_CLIENT_ID))
return {
"ok": True,
"found": data is not None,
"source": "tim_imdb",
"customer_key": customer_key,
"contract_key": contract_key,
"session_key": session_key,
"data": _jsonable(data),
"http_meta": _jsonable(http_meta),
}
except Exception as exc:
return _backend_error(exc)
data = consultar_cliente_backoffice(customer_key, contract_key, session_key)
mock = {"source": "mock_imdb", "pmid": f"PMID-{customer_key or 'UNKNOWN'}", **data}
return _rest_or_mock("imdb/customer", {"customer_key": customer_key, "contract_key": contract_key, "session_key": session_key}, mock)
def consultar_speech_analytics(protocol_id: str | None = None, customer_key: str | None = None, interaction_key: str | None = None, cpf: str | None = None, cnpj: str | None = None, document: str | None = None, document_type: str | None = None) -> Dict[str, Any]:
customer_key = customer_key or cpf or cnpj or document
if _tim_clients_enabled():
if not customer_key:
return {"found": False, "ok": False, "source": "tim_speech_analytics", "reason": "customer_key/msisdn ausente"}
# A API de histórico precisa de CPF/CNPJ. Se o fluxo ainda não trouxe via IMDB, retornamos erro controlado.
social_sec_no = None
try:
imdb = consultar_imdb_cliente(customer_key=customer_key)
social_sec_no = (((imdb.get("data") or {}).get("socialSecNo")) if isinstance(imdb, dict) else None)
except Exception:
social_sec_no = None
if not social_sec_no:
return {"found": False, "ok": False, "source": "tim_speech_analytics", "reason": "socialSecNo ausente; consulte IMDB/PMID antes ou envie cpf_cnpj mascarado/canônico"}
try:
from src.components.clients.speech_analytics_client import SpeechAnalyticsClient
history, http_meta = _run_async(SpeechAnalyticsClient().get_history(str(social_sec_no), str(customer_key)))
return {"ok": True, "found": bool(history), "source": "tim_speech_analytics", "protocol_id": protocol_id or interaction_key, "customer_key": customer_key, "history": _jsonable(history), "http_meta": _jsonable(http_meta)}
except Exception as exc:
return _backend_error(exc)
mock = {
"found": True,
"source": "mock_speech_analytics",
"protocol_id": protocol_id or interaction_key,
"customer_key": customer_key,
"summary": "Histórico de fala simulado: cliente questionou cobrança e solicitou retorno formal.",
"sentiment": "neutral",
"events": [],
}
return _rest_or_mock("speech/history", {"protocol_id": protocol_id, "customer_key": customer_key, "interaction_key": interaction_key}, mock)
def consultar_tais_kb(query: str | None = None, protocol_id: str | None = None, customer_key: str | None = None) -> Dict[str, Any]:
if _tim_clients_enabled():
q = query or protocol_id or customer_key
if not q:
return {"found": False, "ok": False, "source": "tim_tais_kb", "reason": "query/protocol_id/customer_key ausente"}
try:
from src.components.clients.tais_kb_client import TaisKbClient, Product
from src.core.config import settings as app_settings
result = _run_async(TaisKbClient().search_documents(str(q), product=Product.MOVEL, top_k=int(app_settings.TAIS_TOP_K), preprocess=True, postprocess=True))
return {"ok": True, "found": True, "source": "tim_tais_kb", "query": q, "result": _jsonable(result)}
except Exception as exc:
return _backend_error(exc)
mock = {
"found": True,
"source": "mock_tais_kb",
"query": query or protocol_id or customer_key,
"documents": [
{"id": "KB-ANATEL-001", "title": "Tratativa de contestação de cobrança", "score": 0.91},
{"id": "TPL-RESP-001", "title": "Template de resposta formal ANATEL", "score": 0.87},
],
"templates": [
{"id": "TPL-RESP-001", "text": "Prezado cliente, analisamos sua solicitação e registramos a tratativa..."}
],
}
return _rest_or_mock("tais-kb/search", {"query": query, "protocol_id": protocol_id, "customer_key": customer_key}, mock)
def consultar_abrt(customer_key: str | None = None, protocol_id: str | None = None, social_sec_no: str | None = None) -> Dict[str, Any]:
if _tim_clients_enabled():
if not customer_key or not social_sec_no:
return {"found": False, "ok": False, "source": "tim_abrt", "reason": "customer_key/msisdn e social_sec_no são obrigatórios para ABRT"}
try:
from src.components.clients.abrt_client import AbrtClient
data, http_meta = _run_async(AbrtClient().get_abrt_data_with_retry(str(social_sec_no), str(customer_key)))
return {"ok": True, "found": data is not None, "source": "tim_abrt", "customer_key": customer_key, "protocol_id": protocol_id, "data": _jsonable(data), "http_meta": _jsonable(http_meta)}
except Exception as exc:
return _backend_error(exc)
mock = {"found": True, "source": "mock_abrt", "customer_key": customer_key, "protocol_id": protocol_id, "has_open_case": False}
return _rest_or_mock("abrt/status", {"customer_key": customer_key, "protocol_id": protocol_id}, mock)
def consultar_portabilidade(customer_key: str | None = None, contract_key: str | None = None, social_sec_no: str | None = None) -> Dict[str, Any]:
if _tim_clients_enabled():
if not customer_key or not social_sec_no:
return {"found": False, "ok": False, "source": "tim_portability", "reason": "customer_key/msisdn e social_sec_no são obrigatórios para Portabilidade"}
try:
from src.components.clients.portability_client import PortabilityClient
data, http_meta = _run_async(PortabilityClient().get_portability_history_with_retry(str(social_sec_no), str(customer_key)))
return {"ok": True, "found": bool(data), "source": "tim_portability", "customer_key": customer_key, "contract_key": contract_key, "data": _jsonable(data), "http_meta": _jsonable(http_meta)}
except Exception as exc:
return _backend_error(exc)
mock = {"found": True, "source": "mock_portability", "customer_key": customer_key, "contract_key": contract_key, "portability_status": "none"}
return _rest_or_mock("portability/status", {"customer_key": customer_key, "contract_key": contract_key}, mock)
def buscar_templates_emulador(protocol_id: str | None = None, query: str | None = None) -> Dict[str, Any]:
kb = consultar_tais_kb(query=query, protocol_id=protocol_id)
return {"source": "mock_emulator_rag", "found": True, "templates": kb.get("templates", []), "documents": kb.get("documents", [])}
def gerar_rascunho_emulador(protocol_id: str | None = None, selected_actions: list | None = None, operator_instructions: str | None = None) -> Dict[str, Any]:
actions = selected_actions or []
text = operator_instructions or "Rascunho gerado a partir das evidências disponíveis."
return {
"draft_created": True,
"protocol_id": protocol_id,
"selected_actions": actions,
"draft": f"Resposta formal ao cliente: {text}",
"status": "draft",
}