mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-09 13:54:20 +00:00
first commit
This commit is contained in:
BIN
mcp_servers/.DS_Store
vendored
Normal file
BIN
mcp_servers/.DS_Store
vendored
Normal file
Binary file not shown.
54
mcp_servers/backoffice_mcp_server/README.md
Normal file
54
mcp_servers/backoffice_mcp_server/README.md
Normal 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.
|
||||
1
mcp_servers/backoffice_mcp_server/__init__.py
Normal file
1
mcp_servers/backoffice_mcp_server/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Servidor MCP/HTTP migrado para as ferramentas do backoffice."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
37
mcp_servers/backoffice_mcp_server/clients.py
Normal file
37
mcp_servers/backoffice_mcp_server/clients.py
Normal 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()
|
||||
176
mcp_servers/backoffice_mcp_server/main.py
Normal file
176
mcp_servers/backoffice_mcp_server/main.py
Normal 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))
|
||||
35
mcp_servers/backoffice_mcp_server/models.py
Normal file
35
mcp_servers/backoffice_mcp_server/models.py
Normal 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]
|
||||
81
mcp_servers/backoffice_mcp_server/settings.py
Normal file
81
mcp_servers/backoffice_mcp_server/settings.py
Normal 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()
|
||||
343
mcp_servers/backoffice_mcp_server/store.py
Normal file
343
mcp_servers/backoffice_mcp_server/store.py
Normal 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",
|
||||
}
|
||||
Reference in New Issue
Block a user