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

239
.env Normal file
View File

@@ -0,0 +1,239 @@
###############################################################################
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
###############################################################################
APP_NAME=ai-agent-template
APP_ENV=local
LOG_LEVEL=INFO
API_HOST=0.0.0.0
API_PORT=8000
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
###############################################################################
# LLM - OCI Generative AI como provider principal
###############################################################################
# Opções: mock, oci_openai, oci_sdk, openai_compatible
LLM_PROVIDER=oci_openai
LLM_TEMPERATURE=0.2
LLM_MAX_TOKENS=2048
LLM_TIMEOUT_SECONDS=120
# OCI OpenAI-compatible endpoint
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
OCI_GENAI_MODEL=openai.gpt-4.1
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
OCI_GENAI_PROJECT_OCID=
# OCI SDK / signer / profiles
OCI_CONFIG_FILE=~/.oci/config
OCI_PROFILE=LATINOAMERICA-Chicago
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
OCI_REGION=us-chicago-1
###############################################################################
# Persistência
###############################################################################
# Opções: memory, autonomous, mongodb
SESSION_REPOSITORY_PROVIDER=autonomous
MEMORY_REPOSITORY_PROVIDER=autonomous
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
# Autonomous Database
ADB_USER=admin
ADB_PASSWORD=Moniquinha1972
ADB_DSN=oradb23ai_high
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
ADB_WALLET_PASSWORD=Moniquinha1972
ADB_TABLE_PREFIX=AGENTFW
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
MONGODB_DATABASE=agent_platform
# Redis
REDIS_URL=redis://localhost:6379/0
ENABLE_REDIS_CACHE=false
###############################################################################
# RAG / Vector / Graph
###############################################################################
VECTOR_STORE_PROVIDER=memory
GRAPH_STORE_PROVIDER=memory
RAG_TOP_K=5
EMBEDDING_PROVIDER=mock
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
###############################################################################
# Observabilidade
###############################################################################
ENABLE_LANGFUSE=true
LANGFUSE_PUBLIC_KEY=pk-lf-bd9b0c7e-2b8b-4e5b-a382-284a9b4413b3
LANGFUSE_SECRET_KEY=sk-lf-5f5cc18d-0bb5-424e-b5d0-cb3664d58c20
LANGFUSE_HOST=http://localhost:3005
ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template
###############################################################################
# Analytics / Observer corporativo
###############################################################################
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
ENABLE_ANALYTICS=false
# Providers aceitos: oci_streaming,pubsub,noop
ANALYTICS_PROVIDERS=oci_streaming
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
AGENT_PUBSUB_TOPIC=
GCP_PUBSUB_TOPIC_PATH=
GCP_PROJECT_ID=
GCP_PUBSUB_TOPIC=
GCP_PUBSUB_TIMEOUT_SECONDS=30
# Credencial GCP segue padrão Google:
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
###############################################################################
# OCI Streaming
###############################################################################
ENABLE_OCI_STREAMING=false
OCI_STREAM_ENDPOINT=
OCI_STREAM_OCID=
OCI_STREAM_PARTITION_KEY=agent-events
###############################################################################
# Guardrails, Judges, Supervisor
###############################################################################
ENABLE_INPUT_GUARDRAILS=true
ENABLE_OUTPUT_GUARDRAILS=true
ENABLE_JUDGES=true
ENABLE_SUPERVISOR=true
ENABLE_OUTPUT_SUPERVISOR=true
ENABLE_PARALLEL_GUARDRAILS=true
GUARDRAILS_FAIL_FAST=true
OUTPUT_SUPERVISOR_MAX_RETRIES=3
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
JUDGES_CONFIG_PATH=./config/judges.yaml
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
###############################################################################
# Gateway de canais
###############################################################################
DEFAULT_CHANNEL=web
ENABLE_VOICE_ADAPTER=true
ENABLE_WHATSAPP_ADAPTER=true
ENABLE_TEXT_ADAPTER=true
#################################################
# ENTERPRISE ROUTING
#################################################
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
ROUTING_CONFIG_PATH=./config/routing.yaml
# true = usa LLM para classificar quando keywords/estado não resolverem.
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
ENABLE_LLM_ROUTER=true
###############################################################################
# MCP / Tools
###############################################################################
ENABLE_MCP_TOOLS=true
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
TOOLS_CONFIG_PATH=./config/tools.yaml
MCP_TOOL_TIMEOUT_SECONDS=30
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
ROUTING_MODE=router
# Usage/cost accounting
USAGE_REPOSITORY_PROVIDER=autonomous
IDENTITY_CONFIG_PATH=./config/identity.yaml
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
###############################################################################
# BACKOFFICE CONVERTIDO - CHAVES ADICIONADAS A PARTIR DO .env.example
# Valores existentes no .env original foram preservados. Revise placeholders.
###############################################################################
# Framework
DEFAULT_AGENT_ID=backoffice_anatel
ENABLE_SSE=true
SQLITE_DB_PATH=.local/backoffice_framework.db
LLM_MODEL=cohere.command-r-08-2024
# OCI OpenAI-compatible endpoint/credentials. Preencha conforme seu Agent Framework.
OCI_OPENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/chat
OCI_OPENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
# OPENAI_API_KEY=<quando o provider oci_openai usa contrato OpenAI-compatible>
OCI_CONFIG_PROFILE=LATINOAMERICA-Chicago
# OCI_REGION=us-chicago-1
# OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
# MCP
MCP_ENABLED=true
DEBUG=true
# IMDB_API_HOST=
# SIEBEL_API_HOST=
# SIEBEL_API_ROUTE=
# SIEBEL_STATUS_API_HOST=
# SIEBEL_PREPAGO_STATUS_API_HOST=
# SPEECH_ANALYTICS_API_HOST=
# TAIS_KB_DB_USER=
# TAIS_KB_DB_PASSWORD=
# TAIS_KB_DSN=
# OCI_REQUEST_STREAM_OCID=
# OCI_RESPONSE_STREAM_OCID=
# OCI_CONSUMER_GROUP_NAME=
# MCP Server local do backoffice
# O backend principal chama esse endpoint pelo MCPToolRouter do framework.
BACKOFFICE_MCP_BASE_URL=http://localhost:8010
BACKOFFICE_MCP_USE_MOCK=false
BACKOFFICE_MCP_BACKEND_TYPE=tim_clients
BACKOFFICE_MCP_FAIL_OPEN_ON_BACKEND_ERROR=true
# TIM/develop original - preencha no .env real, nunca versionar segredos.
PMID_API_HOST=https://pmidfqa.internal.timbrasil.com.br/access/v1/info
PMID_API_BASIC_TOKEN=Basic dGlteDpAckp1SkpAcFJPUiM=
PMID_API_TIMEOUT=10
PMID_API_CLIENT_ID=AIAGENTCR
SIEBEL_API_HOST=https://pmidfqa.internal.timbrasil.com.br/customers/v1/backOfficeSRopening
SIEBEL_API_TIMEOUT=10
SIEBEL_API_USERNAME=aiagentcr
SIEBEL_API_PASSWORD=AiAgentCR#FQA#
SIEBEL_API_CLIENT_ID=AIAGENTCR
SIEBEL_STATUS_API_HOST=https://pmidfqa.internal.timbrasil.com.br
SIEBEL_STATUS_API_ROUTE=/interactions/v1/statusServiceRequest
SIEBEL_PREPAGO_STATUS_API_HOST=https://pmidfqa.internal.timbrasil.com.br
SIEBEL_PREPAGO_STATUS_API_ROUTE=/customers/v1/serviceRequest
VERIFY_SSL=false
SPEECH_PREDICTION_BASE_URL=https://apigatewayfqa1.tim.com.br
SPEECH_PREDICTION_CLIENT_ID=TzlzCbMGNKpGXl7oUDAlE66eL3ZGmWBtuHMjgsClMoDNdmLz
SPEECH_PREDICTION_CLIENT_SECRET=ZIgIIxsn3CG7ra9HiOLSLygRzcgcivIMsoo4kwqLo9MtQBtNnmUadNpUx81ABkEm
SPEECH_HISTORY_BASE_URL=https://run-external-speech-analytics-audio-toxico-49911170294.us-east1.run.app
SPEECH_HISTORY_AUDIENCE=https://run-external-speech-analytics-audio-toxico-49911170294.us-east1.run.app
GOOGLE_APPLICATION_CREDENTIALS=/Users/cristianohoshikawa/Dropbox/ORACLE/TIM/compass/lab_backoffice/config/gcp-credentials-local.json
SPEECH_TIMEOUT=30
SPEECH_SIMILARITY_THRESHOLD=70
TAIS_DB_USER=USR_ADB_AGNTATEND_W_DEV
TAIS_DB_PASSWORD=T!M#Esta026!
TAIS_DB_DSN="(description= (retry_count=3)(retry_delay=7)(address=(protocol=tcps)(port=1522)(host=10.152.100.72))(connect_data=(service_name=gf9a4a2e79cfeb2_agntatendimentodev_high.adb.oraclecloud.com))(security=(ssl_server_dn_match=no)))"
TAIS_DB_TIMEOUT=30
TAIS_GENAI_ENDPOINT=https://inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com
TAIS_GENAI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaa3prjvf7mkvijwn5ng5h6n5ftanbbwqfg6cu44kmffwamhyy267iq
TAIS_GENAI_EMBED_MODEL_ID=cohere.embed-multilingual-v3.0
TAIS_TABLE_CHUNKS=CHUNKS_CHAR_COHERE_3
TAIS_TOP_K=3
# LLM CLASSIFICATION
CLASSIFICATION_LLM_MODEL=bo_gptoss20b_dev
CLASSIFICATION_LLM_TEMPERATURE=0.3
CLASSIFICATION_LLM_MAX_TOKENS=2000
CLASSIFICATION_LLM_TOP_P=0.9
CLASSIFICATION_LLM_TOP_K=250
CLASSIFICATION_LARGE_LLM_MODEL=bo_gptoss120b_dev
CLASSIFICATION_LARGE_LLM_TEMPERATURE=0.3
CLASSIFICATION_LARGE_LLM_MAX_TOKENS=4000
CLASSIFICATION_LARGE_LLM_TOP_P=0.9
CLASSIFICATION_LARGE_LLM_TOP_K=250
USE_FULL_ANATEL_DICT=true

View File

@@ -0,0 +1,29 @@
# ==========================================================
# Backoffice MCP Server
# Copie para .env.backoffice_mcp ou injete via Docker/K8s Secret.
# ==========================================================
BACKOFFICE_MCP_HOST=0.0.0.0
BACKOFFICE_MCP_PORT=8010
BACKOFFICE_MCP_LOG_LEVEL=info
# true = usa dados simulados em memória.
# false = usa BACKOFFICE_MCP_BACKEND_TYPE para chamar backend real.
BACKOFFICE_MCP_USE_MOCK=true
BACKOFFICE_MCP_BACKEND_TYPE=mock
# REST backend opcional
BACKOFFICE_MCP_REST_BASE_URL=http://localhost:8080/backoffice
BACKOFFICE_MCP_REST_TIMEOUT_SECONDS=20
BACKOFFICE_MCP_REST_AUTH_HEADER=Authorization
BACKOFFICE_MCP_REST_API_KEY=
# Oracle backend opcional
BACKOFFICE_MCP_ORACLE_USER=
BACKOFFICE_MCP_ORACLE_PASSWORD=
BACKOFFICE_MCP_ORACLE_DSN=
BACKOFFICE_MCP_ORACLE_WALLET_LOCATION=
BACKOFFICE_MCP_ORACLE_WALLET_PASSWORD=
# Se true, erro no backend real vira resposta controlada da tool.
BACKOFFICE_MCP_FAIL_OPEN_ON_BACKEND_ERROR=false

98
.env.example Normal file
View File

@@ -0,0 +1,98 @@
# Backoffice convertido 100% para execução dentro do Agent Framework
# Nunca versionar .env com credenciais reais.
APP_ENV=local
LOG_LEVEL=INFO
CORS_ORIGINS=*
# Framework
DEFAULT_AGENT_ID=backoffice_anatel
ROUTING_MODE=router
ENABLE_SSE=true
ENABLE_PARALLEL_GUARDRAILS=true
GUARDRAILS_FAIL_FAST=true
# Repositórios locais por padrão
SESSION_REPOSITORY_PROVIDER=sqlite
MEMORY_REPOSITORY_PROVIDER=sqlite
CHECKPOINT_REPOSITORY_PROVIDER=sqlite
USAGE_REPOSITORY_PROVIDER=sqlite
SQLITE_DB_PATH=.local/backoffice_framework.db
# LLM
LLM_PROVIDER=oci_openai
LLM_MODEL=cohere.command-r-08-2024
# OCI OpenAI-compatible endpoint/credentials. Preencha conforme seu Agent Framework.
# OCI_OPENAI_BASE_URL=https://inference.generativeai.<region>.oci.oraclecloud.com/20231130/actions/chat
# OCI_OPENAI_API_KEY=<token-ou-chave-configurada-pelo-framework>
# OPENAI_API_KEY=<quando o provider oci_openai usa contrato OpenAI-compatible>
# OCI_CONFIG_PROFILE=DEFAULT
# OCI_REGION=sa-saopaulo-1
# OCI_COMPARTMENT_ID=<ocid1.compartment...>
# MCP
MCP_ENABLED=true
MCP_SERVERS_CONFIG_PATH=config/mcp_servers.yaml
MCP_PARAMETER_MAPPING_PATH=config/mcp_parameter_mapping.yaml
# Original develop / TIM integrations
ENABLE_OCI_STREAMING=false
DEBUG=true
# IMDB_API_HOST=
# SIEBEL_API_HOST=
# SIEBEL_API_ROUTE=
# SIEBEL_STATUS_API_HOST=
# SIEBEL_PREPAGO_STATUS_API_HOST=
# SPEECH_ANALYTICS_API_HOST=
# TAIS_KB_DB_USER=
# TAIS_KB_DB_PASSWORD=
# TAIS_KB_DSN=
# OCI_REQUEST_STREAM_OCID=
# OCI_RESPONSE_STREAM_OCID=
# OCI_CONSUMER_GROUP_NAME=
# MCP Server local do backoffice
# O backend principal chama esse endpoint pelo MCPToolRouter do framework.
BACKOFFICE_MCP_BASE_URL=http://localhost:8010
BACKOFFICE_MCP_USE_MOCK=false
BACKOFFICE_MCP_BACKEND_TYPE=tim_clients
BACKOFFICE_MCP_FAIL_OPEN_ON_BACKEND_ERROR=true
# TIM/develop original - preencha no .env real, nunca versionar segredos.
PMID_API_HOST=https://pmidfqa.internal.timbrasil.com.br/access/v1/info
PMID_API_BASIC_TOKEN=<basic-token>
PMID_API_TIMEOUT=10
PMID_API_CLIENT_ID=AIAGENTCR
SIEBEL_API_HOST=https://pmidfqa.internal.timbrasil.com.br/customers/v1/backOfficeSRopening
SIEBEL_API_TIMEOUT=10
SIEBEL_API_USERNAME=<usuario>
SIEBEL_API_PASSWORD=<senha>
SIEBEL_API_CLIENT_ID=AIAGENTCR
SIEBEL_STATUS_API_HOST=https://pmidfqa.internal.timbrasil.com.br
SIEBEL_STATUS_API_ROUTE=/interactions/v1/statusServiceRequest
SIEBEL_PREPAGO_STATUS_API_HOST=https://pmidfqa.internal.timbrasil.com.br
SIEBEL_PREPAGO_STATUS_API_ROUTE=/customers/v1/serviceRequest
VERIFY_SSL=false
SPEECH_PREDICTION_BASE_URL=https://apigatewayfqa1.tim.com.br
SPEECH_PREDICTION_CLIENT_ID=<client-id>
SPEECH_PREDICTION_CLIENT_SECRET=<client-secret>
SPEECH_HISTORY_BASE_URL=<speech-history-url>
SPEECH_HISTORY_AUDIENCE=<speech-history-audience>
GOOGLE_APPLICATION_CREDENTIALS=/path/to/gcp-credentials-local.json
SPEECH_TIMEOUT=30
SPEECH_SIMILARITY_THRESHOLD=70
TAIS_DB_USER=<usuario>
TAIS_DB_PASSWORD=<senha>
TAIS_DB_DSN=<dsn>
TAIS_DB_TIMEOUT=30
TAIS_GENAI_ENDPOINT=https://inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com
TAIS_GENAI_COMPARTMENT_ID=<compartment-ocid>
TAIS_GENAI_EMBED_MODEL_ID=cohere.embed-multilingual-v3.0
TAIS_TABLE_CHUNKS=CHUNKS_CHAR_COHERE_3
TAIS_TOP_K=3
USE_FULL_ANATEL_DICT=true

12
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,12 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Zeppelin ignored files
/ZeppelinRemoteNotebooks/

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ScalaCodeStyleSettings>
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
</ScalaCodeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

6
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_24" default="true" project-jdk-name="24" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/backoffice_convertido_framework.iml" filepath="$PROJECT_DIR$/.idea/backoffice_convertido_framework.iml" />
</modules>
</component>
</project>

View File

@@ -0,0 +1,24 @@
# Sample guideline, please follow similar structure for guideline with code samples
# 1. Suggest using streams instead of simple loops for better readability.
# <example>
# *Comment:
# Category: Minor
# Issue: Use streams instead of a loop for better readability.
# Code Block:
#
# ```java
# // Calculate squares of numbers
# List<Integer> squares = new ArrayList<>();
# for (int number : numbers) {
# squares.add(number * number);
# }
# ```
# Recommendation:
#
# ```java
# // Calculate squares of numbers
# List<Integer> squares = Arrays.stream(numbers)
# .map(n -> n * n) // Map each number to its square
# .toList();
# ```
# </example>

6
Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM python:3.12-slim
WORKDIR /app
COPY agent_framework /agent_framework
COPY agent_template_backend /app
RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1,156 @@
# Backoffice TIM/ANATEL convertido para o Agent Framework — versão 100% de paridade
Esta versão foi gerada a partir de dois insumos:
- `tim-ai-atend-agnt-backoffice(3).zip`: branch `develop` do backoffice original.
- `backoffice_convertido_framework(2).zip`: primeira versão convertida para o framework.
## O que mudou
A versão anterior tinha uma implementação genérica de `BackofficeAgent` com MCP tools e endpoints de compatibilidade simplificados. Esta versão preserva os fluxos reais da branch `develop` como workflows de domínio dentro da aplicação do framework.
## Camadas mantidas pelo framework
O framework continua responsável por:
- `/gateway/message` e `/gateway/events/{session_id}`.
- Normalização de canais.
- `AgentIdentity` e `BusinessContext`.
- Sessão, memória, checkpoint e uso.
- `EnterpriseRouter` e modo supervisor.
- Guardrails de entrada e saída.
- OutputSupervisor.
- Judges.
- Telemetria, IC, NOC e GRL.
- MCP Tool Router.
## Fluxos originais preservados
O pacote `src/` foi copiado da branch `develop` e preserva os grafos originais:
### Checklist ANATEL
Arquivo: `src/agent/graphs/main_graph.py`
Fluxo preservado:
```text
fetch_ticket
-> validation
-> bypass_rules
-> cache_check
-> imdb_enrichment
-> identity_verification
-> speech_enrichment
-> knowledge_base_enrichment
-> canceling_analysis
-> tim_complaint_analysis
-> tim_complaint / different_complaint_operator / undefined_complaint_operator
-> reclassification_analysis
-> treatment_decision
-> siebel_sr_opening
```
### Response Emulator
Arquivo: `src/agent/graphs/emulator_graph.py`
Fluxo preservado:
```text
start_response_emulation
-> fetch_case
-> validate_actions
-> router
-> retrieve_templates
-> retrieve_history
-> generate_response
-> validate_response
-> persist_draft
-> approve_draft / close_case
```
## Rotas preservadas do backoffice original
Registradas em `app/main.py`:
```text
POST /agent/execute
POST /agent/process-ticket
POST /agent/process-and-stream
GET /agent/search-tais-kb
POST /case/{transaction_id}/response-emulator/generate
POST /case/{transaction_id}/response-emulator/finalize
GET /case/{transaction_id}/response-emulator
GET /emulator-rag/search
GET /health/live
GET /health/ready
```
## Serviços TIM preservados
Os clients originais foram preservados em `src/components/clients/`:
```text
imdb_client.py
siebel_client.py
speech_analytics_client.py
tais_kb_client.py
abrt_client.py
portability_client.py
emulator_rag_client.py
rag/history_rag_client.py
rag/templates_rag_client.py
```
## Prompts preservados
Os prompts originais foram preservados em `src/agent/local_prompts/`:
```text
canceling_analysis.py
duplicate_analysis.py
tim_complaint_analysis.py
ticket_reclassification.py
treatment_decision_prompt.py
speech_history_analysis.py
preprocess_tais_kb_query.py
postprocess_tais_kb_query.py
emulator/response_emulator_generation.py
anatel_motives/anatel_motives.py
```
## Guardrails e judges
A configuração do agente backoffice foi reforçada em:
```text
config/agents/backoffice_anatel/guardrails.yaml
config/agents/backoffice_anatel/judges.yaml
config/agents/backoffice_anatel/prompt_policy.yaml
```
A lógica de domínio original continua fazendo validações operacionais por nó, e o framework envolve a execução com guardrails, OutputSupervisor e judges.
## Como subir localmente
```bash
cp .env.example .env
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
## Como validar rapidamente
```bash
curl http://localhost:8000/health
curl http://localhost:8000/debug/backoffice/parity
curl http://localhost:8000/health/live
curl http://localhost:8000/health/ready
```
## Observação importante
O arquivo `.env` real foi removido do pacote gerado. Configure credenciais reais somente localmente ou via secret manager. Não versionar chaves OCI, Autonomous Database, Langfuse, Siebel, IMDB, Speech Analytics ou qualquer segredo TIM.

View File

@@ -0,0 +1,77 @@
# Backoffice TIM/ANATEL convertido 100% para o framework
Esta versão substitui a execução direta dos grafos legados por workflows compilados pelo runtime do framework.
## Princípio aplicado
- O framework é dono do motor de workflow/LangGraph, checkpoint, telemetry, guardrails, judges, supervisor e persistência de execução.
- O backend fornece apenas customizações de domínio: nós, services/clients, prompts, schemas e contratos REST.
- Os contratos REST antigos continuam existindo, mas agora são adapters finos para `BackofficeNativeRuntime.execute_workflow(...)`.
## O que foi removido do caminho ativo
Os seguintes módulos do develop foram movidos para referência e não são mais importáveis pelo backend ativo:
- `src/agent/graphs/*`
- `src/api/executors/*`
- `src/api/routes/*`
- `src/api/main.py`
Eles ficam em:
```text
legacy_reference_disabled/original_develop/
```
## Runtime ativo
```text
app/workflows/backoffice_native_runtime.py
```
Esse runtime monta dois workflows com o motor do framework:
```text
backoffice_checklist
backoffice_response_emulator
```
Cada workflow inclui os nós de domínio do backoffice original, mas cercados por etapas do framework:
```text
framework_input_guardrails
...
framework_output_supervisor
framework_output_guardrails
framework_judges
framework_supervisor_review
framework_persist
```
## Rotas preservadas como adapters framework-native
```text
POST /agent/process-ticket
POST /agent/execute
POST /agent/process-and-stream
POST /agent/search-tais-kb
POST /case/{transaction_id}/response-emulator/generate
POST /case/{transaction_id}/response-emulator/finalize
GET /case/{transaction_id}/response-emulator
POST /emulator-rag/search
GET /health/live
GET /health/ready
```
## Validação estrutural
Execute:
```bash
python tools/validate_parity.py
python -m compileall -q app src tools
```
## Observação
Esta entrega valida a estrutura e compilação Python. Teste funcional ponta-a-ponta ainda depende de serviços TIM/OCI/DB/VPN e variáveis reais.

View File

@@ -0,0 +1,64 @@
# Backoffice/ANATEL migrado para uso nativo do framework
Esta versão remove o fluxo `backoffice_legacy_flow.py` do caminho ativo do agente.
O agente passa a usar apenas capacidades nativas do framework:
- LangGraph corporativo do projeto;
- EnterpriseRouter configurado em `config/routing.yaml`;
- `BusinessContext` e `identity.yaml`;
- `MCPToolRouter` e `mcp_parameter_mapping.yaml`;
- RAG via `RagService`;
- cache via `create_cache`;
- IC/NOC/GRL via `AgentObserver`;
- OutputSupervisor, guardrails de saída, judges e persistência do workflow.
## Arquivo principal
```text
app/agents/backoffice_agent.py
```
O agente não importa mais `app.workflows.backoffice_legacy_flow` e não executa um grafo particular.
Ele recebe `intent`, `route` e `mcp_tools` do roteador nativo e aciona as ferramentas pelo helper
`AgentRuntimeMixin._call_mcp_tool()`.
## Onde ficam as decisões
| Decisão | Local correto |
|---|---|
| Roteamento/intents/tools | `config/routing.yaml` |
| Contratos de tool | `config/tools.yaml` |
| Mapeamento de parâmetros | `config/mcp_parameter_mapping.yaml` |
| Isolamento de identidade | `config/identity.yaml` |
| Prompt e regras de redação | `config/agents/backoffice_anatel/prompt_policy.yaml` |
| Guardrails | `config/agents/backoffice_anatel/guardrails.yaml` |
| Judges | `config/agents/backoffice_anatel/judges.yaml` |
| Implementação de integração | `mcp_servers/backoffice_mcp_server/*` |
## Diferença para a versão anterior
Antes, o agente executava um fluxo recomposto chamado `BackofficeLegacyFlow`.
Agora, o fluxo do agente é o fluxo padrão do framework:
```text
input_guardrails
-> routing_decision
-> backoffice_agent
-> output_supervisor
-> output_guardrails
-> judge
-> supervisor_review
-> persist
```
## Observação importante
Regras de negócio que realmente não existem no framework ainda devem entrar como:
1. configuração YAML;
2. rail/guardrail plugável;
3. judge plugável;
4. tool MCP;
5. serviço de domínio reutilizável do framework.
Não devem entrar como grafo paralelo dentro do agente.

View File

@@ -0,0 +1,54 @@
# Agent Template Backend Enterprise
Este folder é uma cópia completa do `agent_template_backend`, sem cortes de
arquitetura. Ele mantém workflow, router, output supervisor, guardrails,
analytics, observer, MCP, memória, checkpoints e configurações.
A diferença é que a lógica de negócio dos agentes de exemplo foi removida da
execução e preservada comentada nos próprios arquivos:
- `app/agents/billing_agent.py`
- `app/agents/product_agent.py`
- `app/agents/orders_agent.py`
- `app/agents/support_agent.py`
## O que o desenvolvedor deve alterar
1. Escolher ou criar um agente em `app/agents/`.
2. Implementar o método `run()`.
3. Ajustar prompts e tools, se necessário.
4. Emitir ICs de negócio relevantes para a jornada.
5. Manter NOC/GRL nos pontos operacionais e de guardrails.
## O que já está integrado
- `AgentObserver`
- `observer.emit_ic()`
- `observer.emit_noc()`
- `observer.emit_grl()`
- `AnalyticsPublisher`
- OCI Streaming
- GCP Pub/Sub
- OutputSupervisor
- GuardrailPipeline com suporte a execução paralela/fail-fast no framework
- MCP Tool Router
- LangGraph
- Memory
- Checkpoint
- Langfuse / OpenTelemetry
## Exemplos adicionados
Veja `app/examples/`:
- `ic_examples.py`
- `noc_examples.py`
- `grl_examples.py`
- `mcp_examples.py`
- `observer_examples.py`
## Convenção rápida
- IC = evento de negócio / curadoria / informacional.
- NOC = evento operacional / saúde técnica.
- GRL = evento de guardrail / segurança / validação.

View File

@@ -0,0 +1,184 @@
# Backoffice convertido para o seu Agent Framework
Esta pasta é uma versão convertida do desenho do `tim-ai-atend-agnt-backoffice` para usar o framework local `agent_framework`.
## O que foi convertido
O backoffice original foi tratado como uma aplicação FastAPI + LangGraph + nodes + checkpoint MongoDB. A conversão substitui o acoplamento com a dependência corporativa externa `agent-framework` por imports do seu framework local.
### Mapeamento arquitetural
| Backoffice original | Versão convertida |
|---|---|
| `src/api/main.py` | `app/main.py` |
| `src/api/routes/agent.py` | `/gateway/message`, `/gateway/message/sse`, `/gateway/events/{session_id}` em `app/main.py` |
| `src/agent/graphs/main_graph.py` | `app/workflows/agent_graph.py` |
| `src/agent/state/agent_state.py` | `app/state.py` |
| `src/agent/nodes/router_node.py` | `EnterpriseRouter` via `config/routing.yaml` |
| `src/agent/nodes/llm_node.py` | `BackofficeAgent` + `create_llm(settings)` |
| `src/agent/nodes/tool_node.py` | `MCPToolRouter` + `config/tools.yaml` |
| `src/components/checkpointing/mongodb_saver.py` | `agent_framework.checkpoints` / `create_langgraph_checkpointer` |
| `configs/config.example.yaml` | `.env` + `config/*.yaml` |
## Arquivos principais adicionados
- `app/agents/backoffice_agent.py`
- `config/agents.yaml`
- `config/routing.yaml`
- `config/tools.yaml`
- `config/mcp_servers.yaml`
- `config/mcp_parameter_mapping.yaml`
- `config/identity.yaml`
- `config/agents/backoffice_anatel/*`
## Execução local
Coloque esta pasta ao lado do projeto `agent_framework`:
```text
workspace/
agent_framework/
backoffice_convertido_framework/
```
Instale dependências:
```bash
cd backoffice_convertido_framework
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e ../agent_framework
```
Suba a aplicação:
```bash
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
Teste:
```bash
curl -s -X POST http://localhost:8000/gateway/message \
-H 'Content-Type: application/json' \
-d '{
"channel":"web",
"agent_id":"backoffice_anatel",
"tenant_id":"default",
"payload":{
"text":"Preciso analisar uma reclamação da ANATEL do protocolo 12345",
"session_id":"teste-bko-001",
"protocol_id":"12345",
"customer_key":"11999999999"
}
}' | jq
```
## Pontos que ainda exigem o ZIP real do backoffice
Esta conversão preserva a arquitetura e cria um backend funcional no seu padrão, mas não migra regras de negócio específicas que não estavam disponíveis no ambiente atual. Quando o ZIP real do backoffice for anexado, os seguintes itens devem ser migrados arquivo por arquivo:
1. Tools reais de `src/components/tools/*` para `config/tools.yaml` e/ou adaptadores MCP.
2. Regras específicas dos nodes antigos para `BackofficeAgent` ou agentes especializados.
3. Schemas reais da API para compatibilidade retroativa.
4. Checkpoint MongoDB customizado, se houver semântica diferente do saver do framework.
5. Testes unitários e integração do backoffice original.
## Decisão de design
A migração usa um único agente `backoffice_agent`, porque a análise estrutural indicava que o backoffice era mais um runtime/boilerplate de agente do que um domínio funcional completo. Caso o ZIP real mostre subdomínios claros, o próximo passo é quebrar em agentes especializados:
- `anatel_triage_agent`
- `customer_lookup_agent`
- `case_resolution_agent`
- `action_register_agent`
## Complemento: MCP do backoffice migrado
A versão atual inclui também um servidor MCP/HTTP próprio do backoffice em:
```text
mcp_servers/backoffice_mcp_server/
```
Esse servidor substitui a lógica que antes ficaria em `src/components/tools/*` no backoffice original. Ele expõe o contrato consumido pelo `MCPToolRouter` do framework:
```text
GET /health
GET /tools/list
POST /tools/call
```
Ferramentas migradas inicialmente:
```text
consultar_reclamacao
consultar_cliente_backoffice
registrar_acao_backoffice
```
### Rodar API e MCP separados
Terminal 1:
```bash
./scripts/run_backoffice_mcp.sh
```
Terminal 2:
```bash
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
### Testar MCP diretamente
```bash
curl -s http://localhost:8010/tools/list | jq
curl -s -X POST http://localhost:8010/tools/call \
-H 'Content-Type: application/json' \
-d '{
"name": "consultar_reclamacao",
"arguments": {
"protocol_id": "12345",
"customer_key": "11999999999",
"interaction_key": "12345"
}
}' | jq
```
### Rodar com Docker Compose
```bash
docker compose up --build
```
O compose sobe dois serviços:
```text
backoffice-api -> porta 8000
backoffice-mcp -> porta 8010
```
A API aponta para o MCP usando:
```text
BACKOFFICE_MCP_BASE_URL=http://backoffice-mcp:8010
```
### Onde colocar serviços reais
Hoje o MCP usa dados simulados em:
```text
mcp_servers/backoffice_mcp_server/store.py
```
Para plugar sistemas reais, substitua as funções desse arquivo por chamadas para APIs, bancos, filas ou SDKs internos. Mantenha o contrato das ferramentas para não quebrar:
```text
config/tools.yaml
config/mcp_parameter_mapping.yaml
```

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

0
app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
app/agents/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Agentes do Template Backend Enterprise
Os arquivos desta pasta preservam a estrutura real esperada pelo workflow, mas
não executam lógica de negócio pronta.
Cada agente mostra:
- como emitir IC;
- como emitir NOC;
- como emitir GRL;
- como coletar MCP via `_collect_tool_context()`;
- como recuperar RAG via `_retrieve_rag_context()`;
- onde chamar LLM/cache.
A implementação original do exemplo está comentada no fim de cada arquivo.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,421 @@
from __future__ import annotations
from typing import Any
from app.agents.prompting import apply_agent_profile_prompt
from app.identity_extraction import extract_identity_from_text
from app.agents.runtime import AgentRuntimeMixin
class BackofficeAgent(AgentRuntimeMixin):
"""Agente Backoffice/ANATEL usando somente contratos nativos do framework.
Este agente não contém grafo próprio, node legado ou orquestração paralela ao
framework. A execução segue o padrão uniforme:
1. LangGraph do framework faz guardrails de entrada, roteamento e checkpoint.
2. EnterpriseRouter injeta intent, route e mcp_tools a partir de YAML.
3. AgentRuntimeMixin executa MCP Tool Router, RAG, cache, IC/NOC/GRL.
4. LLM recebe contexto já normalizado e gera a resposta.
5. Workflow do framework aplica OutputSupervisor, output guardrails, judges e persistência.
Regras específicas do domínio ficam em prompt_policy/routing/tools/mcp mapping,
não em um fluxo particular dentro do agente.
"""
name = "backoffice_agent"
_domain_graph_cache: dict[str, Any] = {}
DEFAULT_TOOLS_BY_INTENT: dict[str, list[str]] = {
"backoffice_anatel_triage": [
"consultar_reclamacao",
"consultar_cliente_backoffice",
"consultar_siebel_caso",
"consultar_imdb_cliente",
"consultar_speech_analytics",
"consultar_tais_kb",
"consultar_abrt",
"consultar_portabilidade",
],
"backoffice_customer_lookup": [
"consultar_cliente_backoffice",
"consultar_imdb_cliente",
"consultar_portabilidade",
],
"backoffice_action_register": [
"consultar_reclamacao",
"consultar_siebel_caso",
"registrar_acao_backoffice",
"registrar_acao_siebel",
],
"backoffice_response_emulator": [
"consultar_reclamacao",
"buscar_templates_emulador",
"gerar_rascunho_emulador",
],
}
def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
async def run(self, state: dict[str, Any]) -> dict[str, Any]:
await self._emit_ic(
"AGA.001",
state,
{
"status": "Entrada recebida pelo agente nativo do framework",
"framework_native": True,
"business_component": "backoffice_anatel",
},
component="agent.backoffice.native.start",
)
normalized_state = self._normalize_state_tools(state)
await self._emit_ic(
"AGA.018",
normalized_state,
{
"status": "Contexto canônico validado pelo framework",
"missing_fields": self._missing_required_context(normalized_state),
"framework_native": True,
},
component="agent.backoffice.native.context",
)
if self._has_original_ticket_context(normalized_state):
await self._emit_ic(
"BACKOFFICE_TICKET_CONTEXT_DETECTED",
normalized_state,
{
"status": "Contexto de ticket detectado; execução checklist deve ocorrer pelo BackofficeNativeRuntime nas rotas /agent/*",
"framework_native": True,
"reason": "o agente conversacional não compila nem executa grafos de domínio",
},
component="agent.backoffice.native.ticket_context",
)
rag_context, rag_metadata = await self._retrieve_rag_context(normalized_state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"AGA.012",
normalized_state,
{
"status": "RAG/TAIS KB consultado pelo serviço nativo do framework",
"document_count": rag_metadata.get("document_count", 0),
"top_document_ids": rag_metadata.get("top_document_ids", []),
},
component="agent.backoffice.native.rag",
)
mcp_results = await self._execute_tools_by_intent(normalized_state)
await self._emit_ic(
"AGA.014",
normalized_state,
{
"status": "Ferramentas MCP selecionadas pelo roteador do framework",
"intent": normalized_state.get("intent"),
"tool_count": len(mcp_results),
"tools": [r.get("tool_name") or r.get("tool") for r in mcp_results],
},
component="agent.backoffice.native.tools",
)
answer = await self._generate_answer(normalized_state, rag_context, rag_metadata, mcp_results)
await self._emit_ic(
"AGA.043",
normalized_state,
{
"status": "Resposta produzida pelo agente nativo e entregue ao workflow do framework",
"answer_chars": len(answer or ""),
"framework_native": True,
},
component="agent.backoffice.native.completed",
)
await self._emit_noc(
"001",
normalized_state,
{"message": "Backoffice native agent completed", "type": "INFO"},
component="agent.backoffice.native.completed",
)
return {
"answer": answer,
"next_state": self._next_state(normalized_state, mcp_results),
"mcp_results": mcp_results,
"rag_metadata": rag_metadata,
"framework_native": {
"agent": self.name,
"intent": normalized_state.get("intent"),
"route": normalized_state.get("route"),
"tools": normalized_state.get("mcp_tools") or [],
},
}
def _normalize_state_tools(self, state: dict[str, Any]) -> dict[str, Any]:
intent = state.get("intent") or "backoffice_anatel_triage"
configured_tools = list(state.get("mcp_tools") or [])
default_tools = self.DEFAULT_TOOLS_BY_INTENT.get(intent, self.DEFAULT_TOOLS_BY_INTENT["backoffice_anatel_triage"])
tools = configured_tools or default_tools
# Garante ordem estável e remove duplicidade sem perder a configuração do roteador.
seen: set[str] = set()
deduped = []
for tool in tools:
if tool and tool not in seen:
seen.add(tool)
deduped.append(tool)
return {
**state,
"route": state.get("route") or self.name,
"active_agent": self.name,
"intent": intent,
"mcp_tools": deduped,
}
def _missing_required_context(self, state: dict[str, Any]) -> list[str]:
ctx = state.get("context") or {}
bc = ctx.get("business_context") or state.get("business_context") or {}
interaction = (bc.get("interaction_key") if isinstance(bc, dict) else None) or ctx.get("interaction_key")
if not interaction:
interaction = (
ctx.get("protocol_id")
or ctx.get("protocolo")
or ctx.get("complaint_id")
or ctx.get("complaintProtocol")
or ctx.get("transactionId")
or ctx.get("transaction_id")
or ctx.get("ticket_id")
)
return [] if interaction else ["interaction_key"]
async def _execute_tools_by_intent(self, state: dict[str, Any]) -> list[dict[str, Any]]:
intent = state.get("intent") or "backoffice_anatel_triage"
tools = list(state.get("mcp_tools") or [])
results: list[dict[str, Any]] = []
for tool in tools:
arguments = self._tool_arguments(tool, intent, state)
if tool.startswith("registrar_") and not arguments.get("action_text"):
# O framework não deve registrar ação operacional sem texto explícito.
results.append({
"ok": False,
"tool_name": tool,
"skipped": True,
"reason": "action_text ausente; registro não executado por segurança operacional",
})
await self._emit_ic(
"AGA.008",
state,
{"status": "Registro operacional não executado sem action_text", "tool_name": tool},
component="agent.backoffice.native.tool.skip",
)
continue
result = await self._call_mcp_tool(tool, arguments, state)
results.append(result)
if tool in {"consultar_speech_analytics"}:
await self._emit_ic("AGA.010", state, {"status": "Speech Analytics consultado", "tool_ok": result.get("ok")}, component="agent.backoffice.native.speech")
elif tool in {"consultar_imdb_cliente", "consultar_cliente_backoffice"}:
await self._emit_ic("AGA.011", state, {"status": "Cliente/IMDB consultado", "tool_ok": result.get("ok")}, component="agent.backoffice.native.customer")
elif tool in {"consultar_tais_kb", "buscar_templates_emulador"}:
await self._emit_ic("AGA.020", state, {"status": "Templates/TAIS consultados", "tool_ok": result.get("ok")}, component="agent.backoffice.native.templates")
elif tool in {"registrar_acao_backoffice", "registrar_acao_siebel"}:
await self._emit_ic("AGA.006", state, {"status": "Ação operacional solicitada", "tool_ok": result.get("ok")}, component="agent.backoffice.native.action")
return results
def _tool_arguments(self, tool: str, intent: str, state: dict[str, Any]) -> dict[str, Any]:
ctx = state.get("context") or {}
# Para query/prompt usamos o texto sanitizado; para extração de CPF/CNPJ
# precisamos do texto original, pois o guardrail MSK mascara o documento.
text = state.get("sanitized_input") or state.get("user_text") or ""
original_text = (
ctx.get("message")
or ctx.get("text")
or ctx.get("query")
or state.get("user_text")
or text
or ""
)
bc = ctx.get("business_context") or state.get("business_context") or {}
explicit = ctx.get("tool_arguments") or state.get("tool_arguments") or {}
def pick(*names: str) -> Any:
for name in names:
cur: Any = None
if name in explicit:
cur = explicit.get(name)
elif isinstance(bc, dict) and name in bc:
cur = bc.get(name)
elif name in ctx:
cur = ctx.get(name)
elif name in state:
cur = state.get(name)
if cur not in (None, "", {}, []):
return cur
return None
protocol_id = pick(
"protocol_id",
"protocolo",
"interaction_key",
"complaint_id",
"complaintProtocol",
"transactionId",
"transaction_id",
"ticket_id",
)
extracted_identity = extract_identity_from_text(str(original_text))
# Identificador explicitamente digitado na mensagem atual prevalece sobre
# defaults da sessão/front (ex.: user_id/msisdn 11999999999).
customer_key = extracted_identity.get("customer_key") or pick("customer_key", "cpf", "cnpj", "document", "msisdn", "customer_id", "cpf_hash", "document_hash", "user_id")
if not protocol_id:
protocol_id = extracted_identity.get("protocol_id") or extracted_identity.get("interaction_key")
contract_key = pick("contract_key", "contract_id", "account_id", "asset_id")
session_key = pick("session_key", "conversation_key", "session_id")
# Extra args são passados ao MCPToolRouter. O mapper do framework ainda
# aplica mcp_parameter_mapping.yaml, mas estes aliases tornam a chamada
# resiliente para tools que exigem protocol_id diretamente.
common = {
"query": text,
"operator_instructions": text,
"selected_actions": explicit.get("selected_actions") or [],
}
if protocol_id:
common["protocol_id"] = str(protocol_id)
common["interaction_key"] = str(protocol_id)
if customer_key:
common["customer_key"] = str(customer_key)
# Mantém aliases documentais para tools TIM que aceitam CPF/CNPJ/document.
for identity_key in ("cpf", "cnpj", "document", "document_type"):
if extracted_identity.get(identity_key):
common[identity_key] = str(extracted_identity[identity_key])
if contract_key:
common["contract_key"] = str(contract_key)
if session_key:
common["session_key"] = str(session_key)
common["operator_session"] = str(session_key)
if tool.startswith("registrar_"):
action_text = explicit.get("action_text")
if not action_text and intent == "backoffice_action_register":
action_text = text
common["action_text"] = action_text
return common
async def _generate_answer(
self,
state: dict[str, Any],
rag_context: str,
rag_metadata: dict[str, Any],
mcp_results: list[dict[str, Any]],
) -> str:
missing = self._missing_required_context(state)
if missing:
return (
"[BackofficeAgent] Para seguir no padrão do framework, preciso do identificador canônico "
f"{', '.join(missing)}. Informe o protocolo/reclamação/chamado ou configure o mapping em identity.yaml."
)
system_prompt = apply_agent_profile_prompt(state, self._system_prompt())
messages = [
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": (
"Mensagem do usuário:\n"
f"{state.get('sanitized_input') or state.get('user_text') or ''}\n\n"
"Intent/rota escolhidos pelo framework:\n"
f"intent={state.get('intent')} route={state.get('route')}\n\n"
"Resultados MCP normalizados pelo framework:\n"
f"{mcp_results}\n\n"
"Contexto RAG nativo do framework:\n"
f"{rag_context or '[sem contexto RAG]'}\n\n"
"Metadados RAG:\n"
f"{rag_metadata}"
),
},
]
try:
generated = await self._invoke_llm_cached(state, "BackofficeNativeAgent", messages)
return f"[BackofficeAgent] {generated}"
except Exception as exc:
await self._emit_noc(
"005",
state,
{"message": f"LLM failed in native backoffice agent: {type(exc).__name__}: {exc}", "type": "FAILURE"},
component="agent.backoffice.native.llm",
)
return self._fallback_answer(state, mcp_results)
def _has_original_ticket_context(self, state: dict[str, Any]) -> bool:
ctx = state.get("context") or {}
rc = ctx.get("request_context") or ctx
return bool(
rc.get("transactionId")
and isinstance(rc.get("complaint") or {}, dict)
and isinstance(rc.get("customer") or {}, dict)
)
async def _run_original_checklist_workflow(self, state: dict[str, Any]) -> dict[str, Any]:
"""Deprecated compatibility stub.
A migração framework-native não permite que o agente conversacional
compile/execute ``src.agent.graphs`` diretamente. Os fluxos checklist e
response emulator são executados pelo BackofficeNativeRuntime chamado
pelas rotas/adapters do backend.
"""
return {
"executed": False,
"error": {
"type": "DeprecatedDirectGraphExecution",
"message": "Use BackofficeNativeRuntime.execute_workflow('backoffice_checklist')",
},
}
def _system_prompt(self) -> str:
return """
Você é o agente Backoffice/ANATEL executando exclusivamente pelo framework corporativo.
Use apenas estes insumos:
- BusinessContext canônico do framework.
- Ferramentas MCP selecionadas pelo EnterpriseRouter.
- Contexto RAG retornado pelo RagService do framework.
- Memória/checkpoint já carregados pelo workflow.
Regras obrigatórias:
1. Não invente protocolo, cliente, contrato, status, SLA, parecer ou ação Siebel.
2. Se uma ferramenta MCP retornar erro ou ausência de dados, diga exatamente que a evidência não foi encontrada.
3. Não confirme registro de ação sem retorno ok/registered da tool correspondente.
4. Se faltar protocolo/chamado/reclamação, peça somente esse identificador.
5. Responda de forma operacional, curta e rastreável.
6. A resposta será validada por OutputSupervisor, guardrails de saída e judges do framework.
""".strip()
def _fallback_answer(self, state: dict[str, Any], mcp_results: list[dict[str, Any]]) -> str:
ok_tools = [r.get("tool_name") or r.get("tool") for r in mcp_results if r.get("ok")]
failed_tools = [r.get("tool_name") or r.get("tool") for r in mcp_results if not r.get("ok")]
return (
"[BackofficeAgent] Fluxo nativo executado pelo framework. "
f"Intent: {state.get('intent')}. "
f"Tools com sucesso: {ok_tools or 'nenhuma'}. "
f"Tools pendentes/erro: {failed_tools or 'nenhuma'}. "
"A resposta final não foi enriquecida pelo LLM porque houve falha controlada nessa etapa."
)
def _next_state(self, state: dict[str, Any], mcp_results: list[dict[str, Any]]) -> str:
if self._missing_required_context(state):
return "WAITING_BACKOFFICE_IDENTIFIER"
if any(r.get("skipped") for r in mcp_results):
return "BACKOFFICE_WAITING_ACTION_TEXT"
return "BACKOFFICE_ACTIVE"

View File

@@ -0,0 +1,70 @@
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class BillingAgent(AgentRuntimeMixin):
name = "billingAgent"
def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
async def run(self, state):
await self._emit_ic(
"IC.BILLING_AGENT_STARTED",
state,
{"business_component": "faturas"},
component="agent.billing.start",
)
session = (state.get("context") or {}).get("session", {})
tool_context = await self._collect_tool_context(state)
if tool_context:
await self._emit_ic(
"IC.BILLING_MCP_CONTEXT_COLLECTED",
state,
{"tool_result_count": len(tool_context)},
component="agent.billing.mcp",
)
rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"IC.BILLING_RAG_CONTEXT_RETRIEVED",
state,
{
"document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"),
},
component="agent.billing.rag",
)
messages = [
{"role": "system", "content": apply_agent_profile_prompt(state, "Você é um agente especialista em faturas. Responda com clareza, objetividade e sem sugerir ações não solicitadas. Use dados MCP quando disponíveis.")},
{"role": "user", "content": (
f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n"
f"Contexto de sessão: {session}\n"
f"Intent: {state.get('intent')}\n"
f"Dados MCP: {tool_context}\n"
f"Contexto RAG: {rag_context}"
)},
]
answer = await self._invoke_llm_cached(state, "BillingAgent", messages)
result = {"answer": f"[BillingAgent] {answer}", "next_state": "BILLING_ACTIVE", "mcp_results": tool_context, "rag": rag_metadata}
await self._emit_ic(
"IC.BILLING_AGENT_COMPLETED",
state,
{
"answer_chars": len(result.get("answer") or ""),
"has_mcp_results": bool(tool_context),
"rag_enabled": bool(rag_metadata.get("enabled")),
},
component="agent.billing.completed",
)
return result
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1,69 @@
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class OrdersAgent(AgentRuntimeMixin):
name = "orders_agent"
def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
async def run(self, state):
await self._emit_ic(
"IC.ORDERS_AGENT_STARTED",
state,
{"business_component": "pedidos"},
component="agent.orders.start",
)
session = (state.get("context") or {}).get("session", {})
tool_context = await self._collect_tool_context(state)
if tool_context:
await self._emit_ic(
"IC.ORDERS_MCP_CONTEXT_COLLECTED",
state,
{"tool_result_count": len(tool_context)},
component="agent.orders.mcp",
)
rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"IC.ORDERS_RAG_CONTEXT_RETRIEVED",
state,
{
"document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"),
},
component="agent.orders.rag",
)
messages = [
{"role": "system", "content": apply_agent_profile_prompt(state, "Você é um agente de pedidos de varejo. Use dados de tools quando disponíveis.")},
{"role": "user", "content": (
f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n"
f"Sessão: {session}\n"
f"Intent: {state.get('intent')}\n"
f"Dados MCP: {tool_context}\n"
f"Contexto RAG: {rag_context}"
)},
]
answer = await self._invoke_llm_cached(state, "OrdersAgent", messages)
result = {"answer": f"[OrdersAgent] {answer}", "next_state": "ORDER_ACTIVE", "mcp_results": tool_context, "rag": rag_metadata}
await self._emit_ic(
"IC.ORDERS_AGENT_COMPLETED",
state,
{
"answer_chars": len(result.get("answer") or ""),
"has_mcp_results": bool(tool_context),
"rag_enabled": bool(rag_metadata.get("enabled")),
},
component="agent.orders.completed",
)
return result
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1,70 @@
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class ProductAgent(AgentRuntimeMixin):
name = "productAgent"
def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
async def run(self, state):
await self._emit_ic(
"IC.PRODUCT_AGENT_STARTED",
state,
{"business_component": "produtos"},
component="agent.product.start",
)
session = (state.get("context") or {}).get("session", {})
tool_context = await self._collect_tool_context(state)
if tool_context:
await self._emit_ic(
"IC.PRODUCT_MCP_CONTEXT_COLLECTED",
state,
{"tool_result_count": len(tool_context)},
component="agent.product.mcp",
)
rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"IC.PRODUCT_RAG_CONTEXT_RETRIEVED",
state,
{
"document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"),
},
component="agent.product.rag",
)
messages = [
{"role": "system", "content": apply_agent_profile_prompt(state, "Você é um agente especialista em produtos, planos e serviços. Explique sem fazer oferta proativa e sem executar ações sem confirmação. Use dados MCP quando disponíveis.")},
{"role": "user", "content": (
f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n"
f"Contexto de sessão: {session}\n"
f"Intent: {state.get('intent')}\n"
f"Dados MCP: {tool_context}\n"
f"Contexto RAG: {rag_context}"
)},
]
answer = await self._invoke_llm_cached(state, "ProductAgent", messages)
result = {"answer": f"[ProductAgent] {answer}", "next_state": "PRODUCT_ACTIVE", "mcp_results": tool_context, "rag": rag_metadata}
await self._emit_ic(
"IC.PRODUCT_AGENT_COMPLETED",
state,
{
"answer_chars": len(result.get("answer") or ""),
"has_mcp_results": bool(tool_context),
"rag_enabled": bool(rag_metadata.get("enabled")),
},
component="agent.product.completed",
)
return result
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

15
app/agents/prompting.py Normal file
View File

@@ -0,0 +1,15 @@
from __future__ import annotations
def apply_agent_profile_prompt(state: dict, default_prompt: str) -> str:
"""Adiciona o prefixo de prompt configurado para o agent_template selecionado.
Cada agent_id pode definir metadata.system_prefix em config/agents.yaml. Isso
mantém prompts isolados sem duplicar o código dos agentes especializados.
"""
profile = state.get("agent_profile") or (state.get("context") or {}).get("agent_profile") or {}
metadata = profile.get("metadata") or {}
prefix = (metadata.get("system_prefix") or "").strip()
if not prefix:
return default_prompt
return f"{prefix}\n\n{default_prompt}"

267
app/agents/runtime.py Normal file
View File

@@ -0,0 +1,267 @@
from __future__ import annotations
import hashlib
from typing import Any
class AgentRuntimeMixin:
"""Mixin operacional para agentes.
Integra RAG, cache, telemetria e chamadas MCP usando BusinessContext.
Os agentes não precisam conhecer nomes reais de parâmetros do domínio
(msisdn, invoice_id, order_id etc.); eles repassam as chaves canônicas e
o MCPParameterMapper converte para cada tool configurada.
"""
async def _emit_ic(self, code: str, state: dict[str, Any], payload: dict[str, Any] | None = None, component: str | None = None) -> None:
"""Emite Item de Controle (IC) sem impactar a execução do agente.
Este helper é intencionalmente fail-open: erro de observabilidade não
pode quebrar a jornada de negócio do agente. O desenvolvedor pode usar
o mesmo padrão para ICs específicos da sua squad.
"""
observer = getattr(self, "observer", None)
if not observer:
return
ctx = state.get("context") or {}
base = {
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"message_id": ctx.get("message_id"),
"channel_id": ctx.get("channel"),
}
base.update(payload or {})
try:
await observer.emit_ic(code, base, component=component or f"agent.{getattr(self, 'name', 'unknown')}")
except Exception:
return
async def _emit_noc(self, code: str, state: dict[str, Any], payload: dict[str, Any] | None = None, component: str | None = None) -> None:
"""Emite evento NOC sem acoplar a lógica de negócio à observabilidade."""
observer = getattr(self, "observer", None)
if not observer:
return
ctx = state.get("context") or {}
base = {
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"message_id": ctx.get("message_id"),
"channel_id": ctx.get("channel"),
}
base.update(payload or {})
try:
await observer.emit_noc(code, base, component=component or f"agent.{getattr(self, 'name', 'unknown')}")
except Exception:
return
async def _emit_grl(self, code: str, state: dict[str, Any], payload: dict[str, Any] | None = None, component: str | None = None) -> None:
"""Emite evento GRL opcional para custom rails implementados no backend."""
observer = getattr(self, "observer", None)
if not observer:
return
ctx = state.get("context") or {}
base = {
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"message_id": ctx.get("message_id"),
"channel_id": ctx.get("channel"),
}
base.update(payload or {})
try:
await observer.emit_grl(code, base, component=component or f"agent.{getattr(self, 'name', 'unknown')}")
except Exception:
return
async def _retrieve_rag_context(self, state: dict[str, Any]) -> tuple[str, dict[str, Any]]:
rag_service = getattr(self, "rag_service", None)
if not rag_service:
return "", {"enabled": False}
text = state.get("sanitized_input") or state.get("user_text") or ""
namespace = (
(state.get("agent_profile") or {}).get("rag_namespace")
or state.get("agent_id")
or state.get("route")
or "default"
)
ctx = state.get("context") or {}
business_context = ctx.get("business_context") or {}
graph_node = (
ctx.get("graph_node")
or business_context.get("customer_key")
or business_context.get("contract_key")
or ctx.get("customer_id")
)
result = await rag_service.retrieve(text, namespace=namespace, graph_node=graph_node)
context = result.as_prompt_context()
return context, {
"enabled": True,
"namespace": namespace,
"latency_ms": result.latency_ms,
"document_count": len(result.documents),
"graph_neighbors": len(result.graph_neighbors),
"top_document_ids": [d.id for d in result.documents[:5]],
"top_scores": [d.score for d in result.documents[:5]],
}
async def _call_mcp_tool(self, tool: str, arguments: dict[str, Any] | None, state: dict[str, Any]) -> dict[str, Any]:
"""Chama uma ferramenta via MCPToolRouter usando o contrato canônico do framework.
Use este helper quando o agente precisa passar argumentos específicos
além do BusinessContext mapeado em mcp_parameter_mapping.yaml.
Observabilidade IC.MCP_TOOL_CALLED/IC.TOOL_CALLED permanece uniforme.
"""
if not getattr(self, "tool_router", None):
return {"ok": False, "tool_name": tool, "error": "tool_router não configurado"}
ctx = state.get("context") or {}
business_context = ctx.get("business_context") or state.get("business_context") or {}
original_context = {
**ctx,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"session_id": state.get("conversation_key") or state.get("session_id"),
"conversation_key": state.get("conversation_key") or state.get("session_id"),
}
observer = getattr(self, "observer", None)
if observer:
await observer.emit_ic(
"IC.MCP_TOOL_CALLED",
{
"session_id": original_context.get("conversation_key") or original_context.get("session_id"),
"tenant_id": original_context.get("tenant_id"),
"agent_id": original_context.get("agent_id"),
"tool_name": tool,
"framework_native": True,
},
component="agent_runtime.native_mcp",
)
try:
res = await self.tool_router.call(
tool,
arguments or {},
business_context=business_context,
original_context=original_context,
)
result_payload = res.model_dump(mode="json") if hasattr(res, "model_dump") else dict(res)
except Exception as exc:
result_payload = {"ok": False, "tool_name": tool, "error": str(exc), "error_type": type(exc).__name__}
result_payload.setdefault("tool_name", tool)
if observer:
await observer.emit_ic(
"IC.TOOL_CALLED",
{
"session_id": original_context.get("conversation_key") or original_context.get("session_id"),
"tenant_id": original_context.get("tenant_id"),
"agent_id": original_context.get("agent_id"),
"tool_name": tool,
"ok": result_payload.get("ok"),
"server_name": result_payload.get("server_name"),
"error": result_payload.get("error"),
"framework_native": True,
},
component="agent_runtime.native_mcp",
)
return result_payload
async def _collect_mcp_context(self, state: dict[str, Any]) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
if not getattr(self, "tool_router", None):
return results
tools = state.get("mcp_tools") or []
ctx = state.get("context") or {}
business_context = ctx.get("business_context") or state.get("business_context") or {}
original_context = {
**ctx,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"session_id": state.get("conversation_key") or state.get("session_id"),
"conversation_key": state.get("conversation_key") or state.get("session_id"),
}
for tool in tools:
observer = getattr(self, "observer", None)
if observer:
await observer.emit_ic(
"IC.MCP_TOOL_CALLED",
{
"session_id": original_context.get("conversation_key") or original_context.get("session_id"),
"tenant_id": original_context.get("tenant_id"),
"agent_id": original_context.get("agent_id"),
"tool_name": tool,
},
component="agent_runtime",
)
res = await self.tool_router.call(
tool,
{},
business_context=business_context,
original_context=original_context,
)
result_payload = res.model_dump(mode="json")
if observer:
await observer.emit_ic(
"IC.TOOL_CALLED",
{
"session_id": original_context.get("conversation_key") or original_context.get("session_id"),
"tenant_id": original_context.get("tenant_id"),
"agent_id": original_context.get("agent_id"),
"tool_name": tool,
"ok": result_payload.get("ok"),
"server_name": result_payload.get("server_name"),
"error": result_payload.get("error"),
},
component="agent_runtime",
)
results.append(result_payload)
return results
async def _cache_get(self, key: str):
cache = getattr(self, "cache", None)
if not cache:
return None
return await cache.get(key)
async def _cache_set(self, key: str, value: Any, ttl_seconds: int | None = None):
cache = getattr(self, "cache", None)
if not cache:
return
await cache.set(key, value, ttl_seconds)
def _llm_cache_key(self, state: dict[str, Any], agent_name: str, prompt_parts: list[Any]) -> str:
business_context = (state.get("context") or {}).get("business_context") or {}
raw = "|".join([
agent_name,
state.get("tenant_id") or "",
state.get("agent_id") or "",
state.get("intent") or "",
business_context.get("customer_key") or "",
business_context.get("contract_key") or "",
business_context.get("interaction_key") or "",
state.get("sanitized_input") or state.get("user_text") or "",
repr(prompt_parts),
])
return "llm:" + hashlib.sha256(raw.encode("utf-8")).hexdigest()
async def _invoke_llm_cached(self, state: dict[str, Any], agent_name: str, messages: list[dict[str, str]]):
ttl = int(getattr(getattr(self, "settings", None), "CACHE_TTL_SECONDS", 300) or 300)
key = self._llm_cache_key(state, agent_name, messages)
cached = await self._cache_get(key)
if cached is not None:
telemetry = getattr(self, "telemetry", None)
if telemetry:
await telemetry.event("cache.llm.hit", {"agent": agent_name, "key": key}, kind="cache")
return cached
telemetry = getattr(self, "telemetry", None)
if telemetry:
await telemetry.event("cache.llm.miss", {"agent": agent_name, "key": key}, kind="cache")
answer = await self.llm.ainvoke(messages)
await self._cache_set(key, answer, ttl)
return answer

View File

@@ -0,0 +1,67 @@
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class SupportAgent(AgentRuntimeMixin):
name = "support_agent"
def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
async def run(self, state):
await self._emit_ic(
"IC.SUPPORT_AGENT_STARTED",
state,
{"business_component": "suporte"},
component="agent.support.start",
)
tool_context = await self._collect_tool_context(state)
if tool_context:
await self._emit_ic(
"IC.SUPPORT_MCP_CONTEXT_COLLECTED",
state,
{"tool_result_count": len(tool_context)},
component="agent.support.mcp",
)
rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"IC.SUPPORT_RAG_CONTEXT_RETRIEVED",
state,
{
"document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"),
},
component="agent.support.rag",
)
messages = [
{"role": "system", "content": apply_agent_profile_prompt(state, "Você é um agente de suporte de varejo para troca, devolução e garantia.")},
{"role": "user", "content": (
f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n"
f"Intent: {state.get('intent')}\n"
f"Dados MCP: {tool_context}\n"
f"Contexto RAG: {rag_context}"
)},
]
answer = await self._invoke_llm_cached(state, "SupportAgent", messages)
result = {"answer": f"[SupportAgent] {answer}", "next_state": "SUPPORT_ACTIVE", "mcp_results": tool_context, "rag": rag_metadata}
await self._emit_ic(
"IC.SUPPORT_AGENT_COMPLETED",
state,
{
"answer_chars": len(result.get("answer") or ""),
"has_mcp_results": bool(tool_context),
"rag_enabled": bool(rag_metadata.get("enabled")),
},
component="agent.support.completed",
)
return result
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

BIN
app/domain/.DS_Store vendored Normal file

Binary file not shown.

0
app/domain/__init__.py Normal file
View File

Binary file not shown.

View File

1
app/examples/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Exemplos de uso do template backend enterprise."""

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,37 @@
"""Exemplos de GRL.
GRL representa eventos de guardrails. Em regra, GRL.001..GRL.009 são emitidos
pelo pipeline de guardrails e pelo OutputSupervisor do framework. Use emissão
manual apenas para validações customizadas do agente.
"""
from typing import Any
async def exemplo_guardrail_observado(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
await observer.emit_grl(
"OBSERVE",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"rail_code": rail_code,
"reason": reason,
},
component="examples.grl",
)
async def exemplo_guardrail_block(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
await observer.emit_grl(
"004",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"rail_code": rail_code,
"reason": reason,
"action": "block",
},
component="examples.grl",
)

View File

@@ -0,0 +1,34 @@
"""Exemplos de IC - Item de Controle.
ICs representam eventos de negócio. Eles alimentam Informacional, Curadoria,
analytics, BigQuery ou qualquer publisher configurado no framework.
"""
from typing import Any
async def exemplo_fatura_consultada(observer: Any, state: dict[str, Any], invoice_id: str) -> None:
await observer.emit_ic(
"IC.FATURA_CONSULTADA",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"invoice_id": invoice_id,
},
component="examples.ic",
)
async def exemplo_acao_concluida(observer: Any, state: dict[str, Any], action_name: str, ok: bool) -> None:
await observer.emit_ic(
"IC.ACAO_CONCLUIDA",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"action_name": action_name,
"ok": ok,
},
component="examples.ic",
)

View File

@@ -0,0 +1,43 @@
"""Exemplos de MCP + IC.
O AgentRuntimeMixin já possui _collect_mcp_context(), mas este arquivo mostra o
padrão para chamadas explícitas ao tool_router quando necessário.
"""
from typing import Any
async def exemplo_chamada_mcp(tool_router: Any, observer: Any, state: dict[str, Any], tool_name: str, payload: dict[str, Any]) -> Any:
session_id = state.get("conversation_key") or state.get("session_id")
await observer.emit_ic(
"IC.MCP_TOOL_CALLED",
{
"session_id": session_id,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"tool_name": tool_name,
},
component="examples.mcp",
)
result = await tool_router.call(
tool_name,
payload,
business_context=(state.get("context") or {}).get("business_context") or {},
original_context=state.get("context") or {},
)
await observer.emit_ic(
"IC.TOOL_CALLED",
{
"session_id": session_id,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"tool_name": tool_name,
"ok": getattr(result, "ok", None),
},
component="examples.mcp",
)
return result

View File

@@ -0,0 +1,37 @@
"""Exemplos de NOC.
NOC representa telemetria operacional. O workflow do template já emite NOC.001,
NOC.005 e NOC.006. Estes exemplos mostram eventos adicionais que a squad pode
emitir em pontos críticos.
"""
from typing import Any
async def exemplo_api_invalida(observer: Any, state: dict[str, Any], api_url: str, status_code: int, latency_ms: int) -> None:
await observer.emit_noc(
"002",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"apiUrl": api_url,
"statusCode": status_code,
"latencyMs": latency_ms,
},
component="examples.noc",
)
async def exemplo_latencia_banco(observer: Any, state: dict[str, Any], resource_name: str, latency_ms: int) -> None:
await observer.emit_noc(
"003",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"resourceName": resource_name,
"latencyMs": latency_ms,
},
component="examples.noc",
)

View File

@@ -0,0 +1,28 @@
"""Resumo prático do Observer corporativo.
Use este arquivo como cola rápida para IC, NOC e GRL.
"""
from typing import Any
async def emitir_eventos_basicos(observer: Any, state: dict[str, Any]) -> None:
session_id = state.get("conversation_key") or state.get("session_id")
await observer.emit_ic(
"IC.EXEMPLO_NEGOCIO",
{"session_id": session_id, "agent_id": state.get("agent_id")},
component="examples.observer",
)
await observer.emit_noc(
"EXEMPLO_OPERACIONAL",
{"session_id": session_id, "agent_id": state.get("agent_id")},
component="examples.observer",
)
await observer.emit_grl(
"OBSERVE",
{"session_id": session_id, "agent_id": state.get("agent_id"), "rail_code": "CUSTOM"},
component="examples.observer",
)

View File

@@ -0,0 +1,90 @@
from __future__ import annotations
import re
from typing import Any
_CPF_RE = re.compile(r"(?i)\bcpf\b\s*[:=\-]?\s*(\d{3}\.\d{3}\.\d{3}-\d{2}|\d{11})\b")
_CNPJ_RE = re.compile(r"(?i)\bcnpj\b\s*[:=\-]?\s*(\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}|\d{14})\b")
_MSISDN_RE = re.compile(r"(?i)\b(?:msisdn|linha|telefone|celular)\b\s*[:=\-]?\s*(\+?55)?\s*\(?\d{2}\)?\s*9?\d{4}[-\s]?\d{4}\b")
_PROTOCOL_RE = re.compile(r"(?i)\b(?:protocolo|protocol_id|chamado|reclama[cç][aã]o|ticket)\b\s*[:=\-#]?\s*([A-Za-z0-9][A-Za-z0-9._\-/]{3,})\b")
def _digits(value: str | None) -> str | None:
if not value:
return None
digits = re.sub(r"\D+", "", str(value))
return digits or None
def extract_identity_from_text(text: str | None) -> dict[str, str]:
"""Extrai chaves de negócio de mensagens livres do usuário.
O IdentityResolver do framework mapeia campos estruturados. Esta função só
complementa payloads textuais como: "consultar dados do cliente cpf 123...".
"""
text = text or ""
found: dict[str, str] = {}
cpf = _CPF_RE.search(text)
if cpf:
value = _digits(cpf.group(1))
if value and len(value) == 11:
found["customer_key"] = value
found["cpf"] = value
found["document"] = value
found["document_type"] = "cpf"
cnpj = _CNPJ_RE.search(text)
if cnpj:
value = _digits(cnpj.group(1))
if value and len(value) == 14:
found["customer_key"] = value
found["cnpj"] = value
found["document"] = value
found["document_type"] = "cnpj"
protocol = _PROTOCOL_RE.search(text)
if protocol:
value = protocol.group(1).strip()
if value and not value.lower().startswith(("cpf", "cnpj")):
found["interaction_key"] = value
found["protocol_id"] = value
found["protocolo"] = value
# Só captura MSISDN quando há rótulo explícito para evitar confundir CPF/CNPJ.
msisdn = _MSISDN_RE.search(text)
if msisdn and "customer_key" not in found:
value = _digits(msisdn.group(0))
if value:
# Remove prefixo 55 se o usuário digitou com DDI.
if value.startswith("55") and len(value) in {12, 13}:
value = value[2:]
found["customer_key"] = value
found["msisdn"] = value
found["document_type"] = "msisdn"
return found
def enrich_payload_with_text_identity(payload: dict[str, Any] | None) -> dict[str, Any]:
payload = dict(payload or {})
text = (
payload.get("message")
or payload.get("text")
or payload.get("query")
or payload.get("content")
or ""
)
extracted = extract_identity_from_text(str(text))
# Payload estruturado sempre tem prioridade; extração só preenche lacunas.
for key, value in extracted.items():
payload.setdefault(key, value)
return payload
def enrich_context_with_text_identity(context: dict[str, Any] | None, text: str | None) -> dict[str, Any]:
context = dict(context or {})
extracted = extract_identity_from_text(text)
for key, value in extracted.items():
context.setdefault(key, value)
return context

826
app/main.py Normal file
View File

@@ -0,0 +1,826 @@
from __future__ import annotations
import logging
from uuid import uuid4
import time
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from agent_framework.channels.base import ChannelResponse
from agent_framework.channels.gateway import ChannelGateway
from agent_framework.config.agent_registry import AgentProfileRegistry
from agent_framework.config.settings import settings
from agent_framework.analytics.factory import create_analytics_publisher
from agent_framework.observability.observer import AgentObserver
from src.compat.framework_observer import configure as configure_global_observer
from agent_framework.llm.providers import create_llm
from agent_framework.memory.message_history import create_memory
from agent_framework.mcp.tool_router import create_mcp_tool_router
from agent_framework.models.identity import AgentIdentity
from agent_framework.identity import IdentityResolver, BusinessContext
from agent_framework.models.session import ChatMessage, SessionContext
from agent_framework.observability.telemetry import Telemetry
from agent_framework.observability.context import set_observability_context, clear_observability_context
from agent_framework.repositories.session_repository import create_session_repository
from agent_framework.checkpoints.checkpoint_repository import create_checkpoint_repository
from agent_framework.cache.cache import create_cache
from agent_framework.billing.usage_repository import create_usage_repository
from agent_framework.sse.events import SSEHub
from app.workflows.agent_graph import AgentWorkflow
from app.workflows.backoffice_native_runtime import BackofficeNativeRuntime
from app.identity_extraction import enrich_payload_with_text_identity, extract_identity_from_text
logging.basicConfig(level=settings.LOG_LEVEL)
logger = logging.getLogger("agent_template_backend")
app = FastAPI(title="Agent Template Backend FIRST-ready")
app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",")],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
telemetry = Telemetry(settings)
usage_repository = create_usage_repository(settings)
llm = create_llm(settings, telemetry=telemetry, usage_repository=usage_repository)
memory = create_memory(settings)
sessions = create_session_repository(settings)
checkpoints = create_checkpoint_repository(settings)
cache = create_cache(settings, telemetry=telemetry)
gateway = ChannelGateway()
analytics = create_analytics_publisher(settings)
observer = AgentObserver(analytics=analytics)
configure_global_observer({
"enabled": getattr(settings, "ENABLE_ANALYTICS", False),
"providers": getattr(settings, "ANALYTICS_PROVIDERS", "oci_streaming"),
"topic_path": getattr(settings, "GCP_PUBSUB_TOPIC_PATH", None) or getattr(settings, "AGENT_PUBSUB_TOPIC", None),
})
tool_router = create_mcp_tool_router(settings, telemetry=telemetry)
identity_resolver = IdentityResolver.from_yaml(settings.IDENTITY_CONFIG_PATH)
agent_profiles = AgentProfileRegistry(settings)
sse_hub = SSEHub(settings, telemetry=telemetry)
workflow = AgentWorkflow(llm, memory, telemetry, analytics, settings, observer=observer, tool_router=tool_router)
backoffice_runtime = BackofficeNativeRuntime(settings=settings, telemetry=telemetry, analytics=analytics, observer=observer)
logger.info("LLM provider carregado: %s", llm.__class__.__name__)
logger.info("Langfuse habilitado: %s host=%s", telemetry.is_enabled(), settings.LANGFUSE_HOST)
logger.info("Analytics habilitado: %s providers=%s", getattr(settings, "ENABLE_ANALYTICS", False), getattr(settings, "ANALYTICS_PROVIDERS", ""))
logger.info("Agentes disponíveis: %s", [p.agent_id for p in agent_profiles.list_profiles()])
@app.middleware("http")
async def observability_context_middleware(request: Request, call_next):
request_id = request.headers.get("x-request-id") or str(uuid4())
set_observability_context(
request_id=request_id,
channel=request.headers.get("x-channel") or "http",
ura_call_id=request.headers.get("x-ura-call-id"),
)
started = time.time()
try:
response = await call_next(request)
response.headers["x-request-id"] = request_id
await telemetry.event("http.request.completed", {
"method": request.method,
"path": request.url.path,
"status_code": response.status_code,
"duration_ms": int((time.time() - started) * 1000),
}, kind="http")
return response
except Exception as exc:
await telemetry.event("http.request.failed", {
"method": request.method,
"path": request.url.path,
"error": str(exc),
"duration_ms": int((time.time() - started) * 1000),
}, kind="http")
raise
class GatewayRequest(BaseModel):
channel: str = "web"
payload: dict
agent_id: str | None = None
tenant_id: str | None = None
def _resolve_identity(req: GatewayRequest, msg) -> tuple[AgentIdentity, dict, BusinessContext, list[str]]:
payload = enrich_payload_with_text_identity(req.payload or {})
context = dict(msg.context or {})
tenant_id = req.tenant_id or payload.get("tenant_id") or context.get("tenant_id") or "default"
agent_id = req.agent_id or payload.get("agent_id") or context.get("agent_id") or agent_profiles.default_agent_id
profile = agent_profiles.get(agent_id)
# 1) Identidade técnica do framework: isola tenant/agente/sessão.
context.update({"tenant_id": tenant_id, "agent_id": profile.agent_id, "agent_profile": profile.__dict__})
identity = AgentIdentity.from_context(context, session_id=msg.session_id)
# 2) Identidade de negócio: chaves canônicas vindas do front/canal.
# Correção importante: identidade extraída explicitamente do texto da
# mensagem atual (cpf/cnpj/protocolo) deve prevalecer sobre valores
# estáveis herdados da sessão, como msisdn default do frontend.
text_for_identity = str(payload.get("message") or payload.get("text") or payload.get("query") or msg.text or "")
explicit_identity = extract_identity_from_text(text_for_identity)
previous_business_context = dict(context.get("business_context") or context.get("identity") or {})
if explicit_identity.get("customer_key"):
previous_business_context.pop("customer_key", None)
if explicit_identity.get("interaction_key") or explicit_identity.get("protocol_id"):
previous_business_context.pop("interaction_key", None)
resolver_payload = {**context, **payload}
# Garante que source business_context.customer_key não roube prioridade do
# CPF/CNPJ explícito. O IdentityResolver do framework preserva estabilidade,
# então inserimos a intenção explícita no próprio business_context da chamada.
if explicit_identity:
bc_override = dict(resolver_payload.get("business_context") or {})
if explicit_identity.get("customer_key"):
bc_override["customer_key"] = explicit_identity["customer_key"]
if explicit_identity.get("interaction_key"):
bc_override["interaction_key"] = explicit_identity["interaction_key"]
if bc_override:
resolver_payload["business_context"] = bc_override
business_context = identity_resolver.resolve(
resolver_payload,
session_id=identity.conversation_key(),
previous=previous_business_context,
)
missing_identity_keys = identity_resolver.validate(business_context)
context.update({
"business_context": business_context.model_dump(),
"business_keys": business_context.to_context_dict(),
"identity_missing": missing_identity_keys,
"conversation_key": identity.conversation_key(),
"original_session_id": msg.session_id,
})
return identity, context, business_context, missing_identity_keys
async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False) -> dict:
msg = await gateway.normalize(req.channel, req.payload)
identity, normalized_context, business_context, missing_identity_keys = _resolve_identity(req, msg)
agent_session_id = identity.conversation_key()
message_id = (req.payload or {}).get("message_id") or str(uuid4())
set_observability_context(
session_id=agent_session_id,
user_id=msg.user_id,
tenant_id=identity.tenant_id,
agent_id=identity.agent_id,
channel=msg.channel,
message_id=message_id,
ura_call_id=(req.payload or {}).get("ura_call_id") or normalized_context.get("ura_call_id") or business_context.interaction_key,
)
stream = sse_hub.stream_for(agent_session_id)
async with stream.lock:
await sse_hub.emit(agent_session_id, "flow.start", {"session_id": agent_session_id, "message_id": message_id, "agent_id": identity.agent_id}) if emit_sse else None
session = await sessions.get(agent_session_id)
if not session:
context_fields = {
k: v
for k, v in normalized_context.items()
if k in SessionContext.model_fields
and k not in {"tenant_id", "agent_id", "session_id", "user_id", "channel", "channel_id"}
}
session = SessionContext(
tenant_id=identity.tenant_id,
agent_id=identity.agent_id,
session_id=agent_session_id,
user_id=msg.user_id,
channel=msg.channel,
channel_id=msg.channel_id,
**context_fields,
)
session.tenant_id = identity.tenant_id
session.agent_id = identity.agent_id
session.channel = msg.channel
session.channel_id = msg.channel_id or session.channel_id
await sessions.upsert(session)
session.metadata = {
**(session.metadata or {}),
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
"original_context": normalized_context,
}
await sse_hub.emit(agent_session_id, "session.upserted", {"session_id": agent_session_id, "business_context": business_context.model_dump()}) if emit_sse else None
await memory.append(
agent_session_id,
ChatMessage(
role="user",
content=msg.text,
metadata={
**normalized_context,
"agent_id": identity.agent_id,
"tenant_id": identity.tenant_id,
"message_id": message_id,
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
},
),
)
await sse_hub.emit(agent_session_id, "message.received", {"session_id": agent_session_id, "role": "user"}) if emit_sse else None
history = [m.model_dump(mode="json") for m in await memory.list(agent_session_id)]
trace_input = {
"text": msg.text,
"channel": msg.channel,
"channel_id": msg.channel_id,
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"conversation_key": agent_session_id,
"message_id": message_id,
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
}
async with telemetry.span(
"agent.gateway_message",
session_id=agent_session_id,
user_id=session.user_id,
channel=msg.channel,
input=trace_input,
tags=["agent-template", msg.channel, f"agent:{identity.agent_id}", f"tenant:{identity.tenant_id}"],
):
await telemetry.event("gateway.message.received", trace_input)
await sse_hub.emit(agent_session_id, "workflow.started", trace_input) if emit_sse else None
result = await workflow.ainvoke(
{
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"session_id": agent_session_id,
"conversation_key": agent_session_id,
"agent_profile": normalized_context["agent_profile"],
"user_text": msg.text,
"history": history,
"context": {
**normalized_context,
"session": session.model_dump(mode="json"),
"original_session_id": msg.session_id,
"session_id": agent_session_id,
"conversation_key": agent_session_id,
"user_id": session.user_id,
"channel": msg.channel,
"message_id": message_id,
"business_context": business_context.model_dump(),
"business_keys": business_context.to_context_dict(),
"identity_missing": missing_identity_keys,
},
}
)
await checkpoints.put(agent_session_id, {"state": result, "message_id": message_id})
await sse_hub.emit(agent_session_id, "workflow.completed", {"session_id": agent_session_id, "route": result.get("route"), "intent": result.get("intent")}) if emit_sse else None
answer = result.get("final_answer") or result.get("answer") or ""
await memory.append(
agent_session_id,
ChatMessage(
role="assistant",
content=answer,
metadata={
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"message_id": f"assistant-{message_id}",
"route": result.get("route"),
"intent": result.get("intent"),
"route_decision": result.get("route_decision"),
"judges": result.get("judge_results"),
},
),
)
await telemetry.event(
"gateway.message.responded",
{
"session_id": agent_session_id,
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"route": result.get("route"),
"intent": result.get("intent"),
"answer_chars": len(answer),
},
)
response = ChannelResponse(
channel=msg.channel,
session_id=agent_session_id,
text=answer,
metadata={
"channel_id": msg.channel_id,
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"original_session_id": msg.session_id,
"conversation_key": agent_session_id,
"message_id": message_id,
"route": result.get("route"),
"intent": result.get("intent"),
"route_decision": result.get("route_decision"),
"domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"),
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
"judges": result.get("judge_results"),
"guardrails": result.get("guardrail_decisions"),
},
)
rendered = await gateway.render(response)
await sse_hub.emit(agent_session_id, "message.responded", rendered) if emit_sse else None
await sse_hub.emit(agent_session_id, "flow.end", {"session_id": agent_session_id, "message_id": message_id}) if emit_sse else None
return rendered
@app.get("/health")
async def health():
return {
"status": "ok",
"llm_provider": settings.LLM_PROVIDER,
"llm_class": llm.__class__.__name__,
"langfuse_enabled": telemetry.is_enabled(),
"agents": [p.agent_id for p in agent_profiles.list_profiles()],
"default_agent_id": agent_profiles.default_agent_id,
"routing_mode": settings.ROUTING_MODE,
"sse_enabled": settings.ENABLE_SSE,
"session_repository": settings.SESSION_REPOSITORY_PROVIDER,
"memory_repository": settings.MEMORY_REPOSITORY_PROVIDER,
"checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER,
"usage_repository": settings.USAGE_REPOSITORY_PROVIDER,
"identity_config_path": settings.IDENTITY_CONFIG_PATH,
"mcp_parameter_mapping_path": settings.MCP_PARAMETER_MAPPING_PATH,
}
@app.get("/agents")
async def list_agents():
return {"default_agent_id": agent_profiles.default_agent_id, "agents": [p.__dict__ for p in agent_profiles.list_profiles()]}
@app.get("/debug/env")
async def debug_env():
return {
"APP_ENV": settings.APP_ENV,
"LLM_PROVIDER": settings.LLM_PROVIDER,
"ENABLE_LANGFUSE": settings.ENABLE_LANGFUSE,
"LANGFUSE_HOST": settings.LANGFUSE_HOST,
"TELEMETRY_ENABLED": telemetry.is_enabled(),
"SQLITE_DB_PATH": settings.SQLITE_DB_PATH,
"SESSION_REPOSITORY_PROVIDER": settings.SESSION_REPOSITORY_PROVIDER,
"MEMORY_REPOSITORY_PROVIDER": settings.MEMORY_REPOSITORY_PROVIDER,
"CHECKPOINT_REPOSITORY_PROVIDER": settings.CHECKPOINT_REPOSITORY_PROVIDER,
"AGENTS_CONFIG_PATH": settings.AGENTS_CONFIG_PATH,
"ROUTING_CONFIG_PATH": settings.ROUTING_CONFIG_PATH,
"ROUTING_MODE": settings.ROUTING_MODE,
}
@app.get("/test-llm")
async def test_llm():
async with telemetry.span("debug.test_llm", input={"message": "Diga apenas OK"}):
answer = await llm.ainvoke([
{"role": "system", "content": "Responda de forma curta."},
{"role": "user", "content": "Diga apenas OK"},
])
telemetry.flush()
return {"provider": llm.__class__.__name__, "answer": answer}
@app.post("/debug/route")
async def debug_route(req: GatewayRequest):
msg = await gateway.normalize(req.channel, req.payload)
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
state = {
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"session_id": msg.session_id or "debug-session",
"conversation_key": identity.conversation_key(),
"agent_profile": context["agent_profile"],
"user_text": msg.text,
"sanitized_input": msg.text,
"history": [],
"context": {**context, "session": context.get("session", {}), "channel": msg.channel, "business_context": business_context.model_dump()},
}
if settings.ROUTING_MODE == "supervisor":
plan = await workflow.supervisor.route_plan(state)
return {"mode": "supervisor", "route": "supervisor_agent", "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata}
decision = await workflow.router.route(state)
data = decision.model_dump(mode="json")
data["mode"] = "router"
return data
@app.post("/debug/identity")
async def debug_identity(req: GatewayRequest):
msg = await gateway.normalize(req.channel, req.payload)
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
return {
"technical_identity": {
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"conversation_key": identity.conversation_key(),
"original_session_id": msg.session_id,
},
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
"context_keys": sorted(context.keys()),
}
@app.get("/debug/usage")
async def debug_usage(tenant_id: str | None = None, session_id: str | None = None):
return await usage_repository.summarize(tenant_id=tenant_id, session_id=session_id)
@app.get("/debug/mcp/tools")
async def debug_mcp_tools():
return {"enabled": tool_router.enabled, "tools": tool_router.describe_tools()}
@app.post("/debug/mcp/call/{tool_name}")
async def debug_mcp_call(tool_name: str, arguments: dict | None = None):
arguments = arguments or {}
ctx = arguments.get("business_context") or arguments.get("identity") or {}
result = await tool_router.call(
tool_name,
arguments,
business_context=ctx,
original_context=arguments,
)
return result.model_dump(mode="json")
@app.post("/gateway/message")
async def gateway_message(req: GatewayRequest):
return await _process_gateway_message(req, emit_sse=False)
@app.post("/gateway/message/sse")
async def gateway_message_sse(req: GatewayRequest):
return await _process_gateway_message(req, emit_sse=True)
@app.get("/gateway/events/{session_id}")
async def gateway_events(session_id: str, request: Request):
last = request.headers.get("last-event-id") or request.query_params.get("last_event_id") or "0"
return StreamingResponse(
sse_hub.subscribe(session_id, int(last)),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
)
@app.get("/sessions/{session_id}/messages")
async def get_session_messages(session_id: str, limit: int = 50):
return {"session_id": session_id, "messages": [m.model_dump(mode="json") for m in await memory.list(session_id, limit)]}
@app.get("/sessions/{session_id}/checkpoint")
async def get_session_checkpoint(session_id: str):
return {"session_id": session_id, "checkpoint": await checkpoints.get_latest(session_id)}
@app.on_event("shutdown")
async def shutdown():
telemetry.shutdown()
# ---------------------------------------------------------------------------
# Backoffice TIM/ANATEL develop — execução 100% framework-native
# ---------------------------------------------------------------------------
# As rotas antigas permanecem como adapters REST, mas não registram routers
# legados e não executam legacy graph package nem legacy executor package.
# Elas chamam o BackofficeNativeRuntime, que compila os workflows com o motor
# do framework e aplica guardrails, judges, supervisor, checkpoint e telemetry.
import asyncio
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi import status, HTTPException
@app.on_event("startup")
async def startup_backoffice_native_domain():
"""Inicializa apenas recursos de domínio; o motor de workflow é do framework."""
try:
from src.utils.observer import setup_observer
setup_observer()
except Exception:
logger.warning("Observer de domínio não pôde ser inicializado", exc_info=True)
try:
from src.infrastructure.oci.autonomous import db_manager as original_db_manager
app.state.original_db_manager = original_db_manager
await original_db_manager.connect()
logger.info("Autonomous/Mongo manager de domínio conectado")
except Exception:
logger.warning("DB de domínio indisponível; fluxos continuam quando os nós suportarem fallback/mock", exc_info=True)
# Compatibilidade para nós que esperam app.state.oci_producer; a criação real
# continua opcional e não controla o grafo.
try:
from src.core.config import settings as original_settings
if getattr(original_settings, "ENABLE_OCI_STREAMING", False):
from src.infrastructure.streaming.producer import OciProducer
app.state.oci_producer = OciProducer(original_settings.OCI_RESPONSE_STREAM_OCID)
logger.info("OCI producer de domínio inicializado; consumer legado não é iniciado")
else:
try:
from src.infrastructure.streaming.debug_producer import LocalDebugProducer
if getattr(original_settings, "DEBUG", False):
app.state.oci_producer = LocalDebugProducer()
logger.info("LocalDebugProducer de domínio ativo")
except Exception:
pass
except Exception:
logger.warning("Producer de domínio não inicializado", exc_info=True)
@app.on_event("shutdown")
async def shutdown_backoffice_native_domain():
try:
dbm = getattr(app.state, "original_db_manager", None)
if dbm is not None:
await dbm.close()
except Exception:
logger.warning("Falha ao encerrar DB de domínio", exc_info=True)
@app.exception_handler(RequestValidationError)
async def native_validation_exception_handler(request: Request, exc: RequestValidationError):
"""Preserva o formato de erro ANATEL sem ativar rotas/executors legados."""
try:
from src.api.schemas.anatel_schemas import ERROR_CODE_MAPPING, ReasonCode
error_messages = []
for err in exc.errors():
loc_tuple = tuple(l for l in err.get("loc", []) if l != "body")
is_enum_error = err.get("type", "").startswith("enum")
target_fields = [("complaint", "inputChannel"), ("caseType",)]
if is_enum_error and loc_tuple in target_fields:
reason_code = ReasonCode.INVALID_VALUE
field_name = loc_tuple[-1]
reason_text = f"Invalid value for field {field_name} or it's not supported yet"
else:
mapping_result = ERROR_CODE_MAPPING.get(loc_tuple)
if mapping_result:
reason_code, reason_text = mapping_result
else:
reason_code = ReasonCode.FIELD_ERROR
loc_str = " -> ".join([str(l) for l in err.get("loc", [])])
reason_text = f"{loc_str}: {err.get('msg', 'Invalid field')}"
error_messages.append({"code": reason_code.value, "text": reason_text})
try:
body = await request.json()
correlation_id = body.get("transactionId") or body.get("correlation_id") or "unknown"
except Exception:
correlation_id = "unknown"
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"title": "validation error", "status": 400, "correlation_id": correlation_id, "detail": {"messages": error_messages}},
)
except Exception:
return JSONResponse(status_code=422, content={"detail": exc.errors()})
async def _run_native_checklist(event, request: Request):
from src.api.utils import agent_helpers
transaction_id = event.transactionId or f"man-{uuid4().hex[:8]}"
payload = event.model_dump(mode="json", by_alias=True)
final_state = await backoffice_runtime.execute_workflow(
"backoffice_checklist",
payload=payload,
transaction_id=transaction_id,
app_state=request.app.state,
metadata={
"tenant_id": "default",
"agent_id": "backoffice_anatel",
"channel": "rest",
"legacy_contract": "agent.process-ticket",
},
)
try:
response_event = agent_helpers.build_cms_response_event(final_state, transaction_id)
return response_event.model_dump(mode="json", by_alias=True)
except Exception:
return {
"transactionId": transaction_id,
"framework_native": True,
"current_step": str(final_state.get("current_step")),
"final_response": final_state.get("final_response"),
"error": final_state.get("error"),
"metadata": final_state.get("metadata", {}),
}
@app.post("/agent/process-ticket", status_code=status.HTTP_200_OK)
async def native_process_ticket(request: Request, event: "TicketRequestEvent"):
return await _run_native_checklist(event, request)
@app.post("/agent/execute", status_code=status.HTTP_200_OK)
async def native_agent_execute(request: Request, event: "TicketRequestEvent"):
return await _run_native_checklist(event, request)
@app.post("/agent/process-and-stream", status_code=status.HTTP_200_OK)
async def native_process_and_stream(request: Request, event: "TicketRequestEvent"):
return await _run_native_checklist(event, request)
@app.post("/agent/search-tais-kb", status_code=status.HTTP_200_OK)
async def native_search_tais_kb(body: dict):
"""Adapter REST para TAIS KB usando o service/cliente de domínio, não grafo legado."""
try:
from src.components.clients.tais_kb_client import TaisKbClient
query = body.get("query") or body.get("text") or body.get("complaint_description") or ""
client = TaisKbClient()
if hasattr(client, "search"):
result = await client.search(query=query, **{k: v for k, v in body.items() if k not in {"query", "text", "complaint_description"}})
elif hasattr(client, "query"):
result = await client.query(query)
else:
raise AttributeError("TAIS KB client has no search/query method")
return {"framework_native": True, "result": result}
except Exception as exc:
await telemetry.event("backoffice.tais_kb.failed", {"error": str(exc)}, kind="tool")
return {"framework_native": True, "result": [], "error": str(exc)}
async def _run_native_emulator(request: Request, event):
transaction_id = event.transactionId
payload = event.model_dump(mode="json", by_alias=True)
final_state = await backoffice_runtime.execute_workflow(
"backoffice_response_emulator",
payload=payload,
transaction_id=transaction_id,
app_state=request.app.state,
metadata={
"tenant_id": "default",
"agent_id": "backoffice_anatel",
"channel": "rest",
"flow_mode": event.flow_mode,
"selected_actions": [a.model_dump(mode="json") for a in event.selected_actions],
"legacy_contract": "case.response-emulator",
},
)
try:
from src.api.utils.emulator_response_builder import build_emulator_response_event
response_event = build_emulator_response_event(final_state, transaction_id)
return response_event.model_dump(mode="json", by_alias=True)
except Exception:
return {
"transactionId": transaction_id,
"framework_native": True,
"current_step": str(final_state.get("current_step")),
"final_response": final_state.get("final_response"),
"error": final_state.get("error"),
"metadata": final_state.get("metadata", {}),
}
@app.post("/case/{transaction_id}/response-emulator/generate", status_code=status.HTTP_200_OK)
async def native_response_emulator_generate(transaction_id: str, request: Request, body: "EmulatorGenerateRequest"):
from src.api.schemas.anatel_response_emulator_schemas import ResponseEmulatorRequestEvent, OperatorFeedback
if body.transactionId != transaction_id:
raise HTTPException(status_code=400, detail="transactionId path/body mismatch")
if body.action == "generate":
event = ResponseEmulatorRequestEvent(
transactionId=transaction_id,
type="first_response",
flow_mode="generate",
selected_actions=body.selected_actions or [],
)
else:
event = ResponseEmulatorRequestEvent(
transactionId=transaction_id,
type="regenerate",
flow_mode="generate",
selected_actions=[],
previous_response="",
feedback=OperatorFeedback(comment=body.operator_instructions or ""),
)
return await _run_native_emulator(request, event)
@app.post("/case/{transaction_id}/response-emulator/finalize", status_code=status.HTTP_200_OK)
async def native_response_emulator_finalize(transaction_id: str, request: Request, body: "EmulatorFinalizeRequest"):
from src.api.schemas.anatel_response_emulator_schemas import ResponseEmulatorRequestEvent
if body.transactionId != transaction_id:
raise HTTPException(status_code=400, detail="transactionId path/body mismatch")
event = ResponseEmulatorRequestEvent(
transactionId=transaction_id,
type="first_response",
flow_mode=body.action,
selected_actions=[],
)
return await _run_native_emulator(request, event)
@app.get("/case/{transaction_id}/response-emulator", status_code=status.HTTP_200_OK)
async def native_response_emulator_status(transaction_id: str):
"""Status reader nativo. Lê DB de domínio quando disponível, sem rota legada."""
try:
from src.core.config import settings as original_settings
dbm = getattr(app.state, "original_db_manager", None)
db = getattr(dbm, "db", None)
if db is None:
return {"transactionId": transaction_id, "framework_native": True, "status": None, "detail": "domain DB unavailable"}
coll = db[original_settings.AUTONOMOUS_NOSQL_COLLECTION]
doc = await coll.find_one({"transactionId": transaction_id})
if not doc:
return {"transactionId": transaction_id, "framework_native": True, "status": None}
processing = doc.get("processing") or {}
return {
"transactionId": transaction_id,
"framework_native": True,
"status": processing.get("status"),
"current_step": processing.get("current_step"),
"case_response": processing.get("case_response") or doc.get("case_response"),
"validation": (doc.get("metadata") or {}).get("validation"),
"selected_actions_count": len((doc.get("metadata") or {}).get("selected_actions") or []),
"transitions": doc.get("transitions") or [],
"last_updated_at": processing.get("updated_at") or processing.get("timestamp"),
}
except Exception as exc:
return {"transactionId": transaction_id, "framework_native": True, "status": None, "error": str(exc)}
@app.post("/emulator-rag/search", status_code=status.HTTP_200_OK)
async def native_emulator_rag_search(body: dict):
try:
from src.agent.nodes.emulator._rag_query import query_emulator_rag
result = await query_emulator_rag(body)
return {"framework_native": True, "result": result}
except Exception as exc:
return {"framework_native": True, "result": [], "error": str(exc)}
@app.get("/health/live")
async def native_health_live():
return {"status": "live", "framework_native": True}
@app.get("/health/ready")
async def native_health_ready():
return {
"status": "ready",
"framework_native": True,
"workflows": list(backoffice_runtime._graphs.keys()),
"framework_layers": {
"gateway": True,
"identity": True,
"session_repository": settings.SESSION_REPOSITORY_PROVIDER,
"memory_repository": settings.MEMORY_REPOSITORY_PROVIDER,
"checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER,
"guardrails": True,
"judges": True,
"supervisor": True,
"mcp_router": tool_router.enabled,
"telemetry": telemetry.is_enabled(),
},
}
@app.get("/debug/backoffice/parity")
async def debug_backoffice_parity():
return {
"mode": "framework_native_domain_workflows",
"legacy_graph_execution": False,
"legacy_router_registration": False,
"forbidden_active_imports": ["legacy_reference_disabled/original_develop/src_agent_graphs", "legacy_reference_disabled/original_develop/src_api_executors"],
"runtime": "app.workflows.backoffice_native_runtime.BackofficeNativeRuntime",
"domain_package": "src.agent.nodes + src.components.clients + src.agent.local_prompts",
"workflows": {
"backoffice_checklist": [
"framework_input_guardrails", "fetch_ticket", "validation", "bypass_rules", "cache_check", "imdb_enrichment", "identity_verification", "speech_enrichment", "knowledge_base_enrichment", "canceling_analysis", "tim_complaint_analysis", "operator_route", "reclassification_analysis", "treatment_decision", "siebel_sr_opening", "framework_output_supervisor", "framework_output_guardrails", "framework_judges", "framework_supervisor_review", "framework_persist"
],
"backoffice_response_emulator": [
"framework_input_guardrails", "start_response_emulation", "fetch_case", "validate_actions", "router", "retrieve_templates", "retrieve_history", "generate_response", "validate_response", "persist_draft", "approve_draft", "close_case", "framework_output_supervisor", "framework_output_guardrails", "framework_judges", "framework_supervisor_review", "framework_persist"
],
},
"framework_layers": {
"gateway": True,
"identity": True,
"session_repository": settings.SESSION_REPOSITORY_PROVIDER,
"memory_repository": settings.MEMORY_REPOSITORY_PROVIDER,
"checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER,
"guardrails": True,
"judges": True,
"supervisor": True,
"mcp_router": tool_router.enabled,
"telemetry": telemetry.is_enabled(),
},
}
# Late imports only for FastAPI annotation resolution. They are schemas, not
# workflow motors.
from src.api.schemas.anatel_schemas import TicketRequestEvent
from src.api.schemas.anatel_response_emulator_schemas import EmulatorGenerateRequest, EmulatorFinalizeRequest

34
app/state.py Normal file
View File

@@ -0,0 +1,34 @@
from typing import Any, TypedDict
class AgentState(TypedDict, total=False):
tenant_id: str
agent_id: str
session_id: str
conversation_key: str
agent_profile: dict[str, Any]
user_text: str
sanitized_input: str
route: str
intent: str
route_decision: dict[str, Any]
answer: str
final_answer: str
history: list[dict[str, Any]]
context: dict[str, Any]
guardrail_decisions: list[dict[str, Any]]
judge_results: list[dict[str, Any]]
next_state: str
domain: str
mcp_tools: list[str]
mcp_results: list[dict[str, Any]]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
active_agent: str
blocked: bool
supervisor_action: str
supervisor_guidance: str
supervisor_attempt: int
supervisor_handover_reason: str
output_supervisor_results: list[dict[str, Any]]
output_guardrails_already_applied: bool

Binary file not shown.

View File

@@ -0,0 +1,718 @@
from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
from langgraph.graph import END, START, StateGraph
from agent_framework.guardrails.pipeline import GuardrailPipeline
from agent_framework.guardrails.output_supervisor import OutputSupervisor
from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
from agent_framework.observability.judge_events import JudgeTelemetry
from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry
from agent_framework.observability.observer import AgentObserver
from app.agents.billing_agent import BillingAgent
from app.agents.product_agent import ProductAgent
from app.agents.orders_agent import OrdersAgent
from app.agents.support_agent import SupportAgent
from app.agents.backoffice_agent import BackofficeAgent
from app.state import AgentState
from agent_framework.rag.rag_service import RagService
from agent_framework.cache.cache import create_cache
class LegacyOutputGuardrailRail:
"""Adapter: reutiliza GuardrailPipeline.run_output dentro do OutputSupervisor novo.
O framework antigo retornava decisões allowed=True/False. O OutputSupervisor
corporativo trabalha com RailAction (allow/sanitize/retry/block/handover).
Este adapter evita reescrever todos os rails agora e mantém compatibilidade.
"""
code = "LEGACY_OUTPUT_GUARDRAILS"
def __init__(self, pipeline: GuardrailPipeline):
self.pipeline = pipeline
async def evaluate(self, candidate: str, context: dict):
final, decisions = await self.pipeline.run_output(candidate, context)
serialized = [d.model_dump() for d in decisions]
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
if blocked:
first = blocked[0]
code = (getattr(first, "code", "") or "").upper()
action = RailAction.RETRY if code in {"REVPREC", "CMP", "SCO", "GND"} else RailAction.BLOCK
return RailResult(
code=code or self.code,
action=action,
reason=getattr(first, "reason", "Resposta bloqueada por guardrail de saída"),
guidance=getattr(first, "reason", "Regerar resposta seguindo as políticas de saída."),
sanitized_text=final,
metadata={"legacy_decisions": serialized},
)
if final != candidate:
return RailResult(
code=self.code,
action=RailAction.SANITIZE,
reason="Resposta sanitizada por guardrail de saída legado.",
sanitized_text=final,
metadata={"legacy_decisions": serialized},
)
return RailResult(
code=self.code,
action=RailAction.ALLOW,
reason="Resposta aprovada pelos guardrails de saída legados.",
sanitized_text=final,
metadata={"legacy_decisions": serialized},
)
class AgentWorkflow:
"""Workflow principal com dois modos de roteamento.
Modos suportados por configuração:
ROUTING_MODE=router
input_guardrails -> routing_decision/EnterpriseRouter -> 1 agente -> output_guardrails
ROUTING_MODE=supervisor
input_guardrails -> routing_decision/Supervisor -> supervisor_agent -> N agentes -> consolidação
Em ambos os modos, memória/checkpoint/session usam tenant_id:agent_id:session_id.
"""
def __init__(self, llm, memory, telemetry, analytics, settings, observer: AgentObserver | None = None, tool_router=None):
self.llm = llm
self.memory = memory
self.telemetry = telemetry
self.analytics = analytics
self.observer = observer or AgentObserver(analytics=analytics)
self.settings = settings
self.tool_router = tool_router
self.tool_router = tool_router
self.guardrails = GuardrailPipeline(
observer=self.observer,
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
)
self.output_supervisor_engine = OutputSupervisor(
rails=[LegacyOutputGuardrailRail(self.guardrails)],
observer=self.observer,
max_retries=int(getattr(settings, "OUTPUT_SUPERVISOR_MAX_RETRIES", 3)),
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
)
self.judges = JudgePipeline()
self.supervisor = Supervisor()
self.workflow_telemetry = WorkflowTelemetry(telemetry)
self.guardrail_telemetry = GuardrailTelemetry(telemetry)
self.judge_telemetry = JudgeTelemetry(telemetry)
self.langgraph_telemetry = LangGraphDeepTelemetry(telemetry)
self.cache = create_cache(settings)
self.rag_service = RagService(settings, telemetry=telemetry)
self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
agent_kwargs = {"telemetry": telemetry, "tool_router": getattr(self, "tool_router", None), "rag_service": self.rag_service, "cache": self.cache, "settings": settings, "observer": self.observer}
self.billing = BillingAgent(llm, **agent_kwargs)
self.product = ProductAgent(llm, **agent_kwargs)
self.orders = OrdersAgent(llm, **agent_kwargs)
self.support = SupportAgent(llm, **agent_kwargs)
self.backoffice = BackofficeAgent(llm, **agent_kwargs)
self.graph = self._build_graph()
def _node(self, name, fn):
async def _wrapped(state):
async with self.langgraph_telemetry.node(name, state):
return await fn(state)
return _wrapped
def _build_graph(self):
builder = StateGraph(AgentState)
builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails))
builder.add_node("routing_decision", self._node("routing_decision", self.routing_decision))
builder.add_node("billing_agent", self._node("billing_agent", self.billing_agent))
builder.add_node("product_agent", self._node("product_agent", self.product_agent))
builder.add_node("orders_agent", self._node("orders_agent", self.orders_agent))
builder.add_node("support_agent", self._node("support_agent", self.support_agent))
builder.add_node("backoffice_agent", self._node("backoffice_agent", self.backoffice_agent))
builder.add_node("handoff", self._node("handoff", self.handoff))
builder.add_node("supervisor_agent", self._node("supervisor_agent", self.supervisor_agent))
builder.add_node("output_supervisor", self._node("output_supervisor", self.output_supervisor))
builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails))
builder.add_node("judge", self._node("judge", self.judge))
builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review))
builder.add_node("persist", self._node("persist", self.persist))
builder.add_edge(START, "input_guardrails")
builder.add_conditional_edges(
"input_guardrails",
self._after_input_guardrails,
{"blocked": "persist", "continue": "routing_decision"},
)
builder.add_conditional_edges(
"routing_decision",
lambda s: s.get("route", "billing_agent"),
{
"billing_agent": "billing_agent",
"product_agent": "product_agent",
"orders_agent": "orders_agent",
"support_agent": "support_agent",
"backoffice_agent": "backoffice_agent",
"handoff": "handoff",
"supervisor_agent": "supervisor_agent",
},
)
builder.add_edge("billing_agent", "output_supervisor")
builder.add_edge("product_agent", "output_supervisor")
builder.add_edge("orders_agent", "output_supervisor")
builder.add_edge("support_agent", "output_supervisor")
builder.add_edge("backoffice_agent", "output_supervisor")
builder.add_edge("handoff", "output_supervisor")
builder.add_edge("supervisor_agent", "output_supervisor")
builder.add_edge("output_supervisor", "output_guardrails")
builder.add_edge("output_guardrails", "judge")
builder.add_edge("judge", "supervisor_review")
builder.add_edge("supervisor_review", "persist")
builder.add_edge("persist", END)
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
def _after_input_guardrails(self, state):
return "blocked" if state.get("blocked") else "continue"
async def input_guardrails(self, state):
async with self.telemetry.span(
"workflow.input_guardrails",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("user_text"),
):
history_texts = [m.get("content", "") for m in state.get("history", [])]
await self.observer.emit_grl(
"001",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "input",
},
component="workflow.input_guardrails.start",
)
sanitized, decisions = await self.guardrails.run_input(
state["user_text"],
{
**(state.get("context") or {}),
"history_texts": history_texts,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {},
},
)
for _decision in decisions:
await self.guardrail_telemetry.evaluated("input", _decision)
await self.observer.emit_grl(
"002" if _decision.allowed else "004",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "input",
"rail_code": getattr(_decision, "code", None),
"allowed": bool(_decision.allowed),
"reason": getattr(_decision, "reason", None),
},
component="workflow.input_guardrails.decision",
)
if not _decision.allowed:
await self.guardrail_telemetry.blocked("input", _decision)
await self.telemetry.event(
"guardrails.input.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"decisions": [d.model_dump() for d in decisions],
},
)
await self.observer.emit_grl(
"009",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "input",
"blocked": any(not d.allowed for d in decisions),
"decision_count": len(decisions),
},
component="workflow.input_guardrails.final",
)
if any(not d.allowed for d in decisions):
return {
"sanitized_input": sanitized,
"answer": "Não consegui seguir com essa mensagem por regra de segurança.",
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.",
"guardrail_decisions": [d.model_dump() for d in decisions],
"route": "blocked",
"blocked": True,
}
return {
"sanitized_input": sanitized,
"guardrail_decisions": [d.model_dump() for d in decisions],
"blocked": False,
}
async def routing_decision(self, state):
mode = getattr(self.settings, "ROUTING_MODE", "router")
async with self.telemetry.span(
"workflow.routing_decision",
session_id=state.get("conversation_key") or state.get("session_id"),
input={
"mode": mode,
"text": state.get("sanitized_input") or state.get("user_text"),
"previous_state": state.get("next_state"),
},
):
if mode == "supervisor":
plan = await self.supervisor.route_plan(state)
await self.langgraph_telemetry.edge("routing_decision", "supervisor_agent", state, {"method": "supervisor", "intent": plan.intent, "confidence": plan.confidence})
return {
"route": "supervisor_agent",
"intent": plan.intent,
"supervisor_plan": {
"agents": plan.agents,
"intent": plan.intent,
"confidence": plan.confidence,
"reason": plan.reason,
"metadata": plan.metadata,
},
"route_decision": {
"route": "supervisor_agent",
"agent": "supervisor",
"intent": plan.intent,
"confidence": plan.confidence,
"reason": plan.reason,
"method": "supervisor",
"metadata": plan.metadata,
},
}
decision = await self.router.route(state)
await self.langgraph_telemetry.edge("routing_decision", decision.route, state, {"method": getattr(decision, "method", None), "intent": decision.intent, "confidence": decision.confidence})
await self.observer.emit_ic(
"ROUTE_SELECTED",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": decision.route,
"intent": decision.intent,
"confidence": decision.confidence,
"method": getattr(decision, "method", None),
},
component="workflow.routing_decision",
)
return {
"route": decision.route,
"intent": decision.intent,
"route_decision": decision.model_dump(mode="json"),
"domain": decision.domain,
"mcp_tools": decision.mcp_tools,
"next_state": decision.next_state,
}
async def billing_agent(self, state):
async with self.langgraph_telemetry.node("billing_agent", state):
async with self.telemetry.span(
"workflow.agent.billing",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.billing.run(state)
async def product_agent(self, state):
async with self.langgraph_telemetry.node("product_agent", state):
async with self.telemetry.span(
"workflow.agent.product",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.product.run(state)
async def orders_agent(self, state):
async with self.langgraph_telemetry.node("orders_agent", state):
async with self.telemetry.span(
"workflow.agent.orders",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.orders.run(state)
async def support_agent(self, state):
async with self.langgraph_telemetry.node("support_agent", state):
async with self.telemetry.span(
"workflow.agent.support",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.support.run(state)
async def backoffice_agent(self, state):
async with self.langgraph_telemetry.node("backoffice_agent", state):
async with self.telemetry.span(
"workflow.agent.backoffice",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.backoffice.run(state)
async def supervisor_agent(self, state):
"""Executa um ou mais agentes no modo supervisor e consolida a resposta.
Este nó mantém o desenho de supervisor sem obrigar o restante do workflow
a conhecer quantos agentes foram acionados. Cada execução especializada
recebe o mesmo estado, mas com route/active_agent atualizados.
"""
plan = state.get("supervisor_plan") or {}
agents = plan.get("agents") or ["backoffice_agent"]
handlers = {
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
"backoffice_agent": self.backoffice.run,
}
partials = []
mcp_results = []
async with self.telemetry.span(
"workflow.supervisor_agent",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"agents": agents, "intent": state.get("intent")},
):
for agent_name in agents:
handler = handlers.get(agent_name)
if handler is None:
continue
child_state = {**state, "route": agent_name, "active_agent": agent_name}
result = await handler(child_state)
partials.append({"agent": agent_name, "answer": result.get("answer", "")})
mcp_results.extend(result.get("mcp_results") or [])
if len(partials) == 1:
answer = partials[0]["answer"]
else:
joined = "\n\n".join(f"{p['agent']}: {p['answer']}" for p in partials)
answer = (
"[Supervisor] Consolidação de múltiplos agentes acionados.\n"
f"{joined}"
)
return {
"answer": answer,
"supervisor_results": partials,
"mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE",
}
async def handoff(self, state):
async with self.telemetry.span("workflow.handoff", session_id=state.get("session_id")):
target = (state.get("route_decision") or {}).get("metadata", {}).get("target_agent")
answer = (
"Vou redirecionar sua solicitação para o especialista correto. "
f"Destino sugerido: {target or 'agente especializado'}."
)
return {"answer": answer}
async def output_supervisor(self, state):
"""Valida a resposta candidata com o OutputSupervisor corporativo.
Este nó não substitui o roteador/supervisor multiagente. Ele roda após o
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"session_id": state.get("conversation_key") or state.get("session_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
async with self.telemetry.span(
"workflow.output_supervisor",
session_id=state.get("conversation_key") or state.get("session_id"),
input=candidate,
):
decision = await self.output_supervisor_engine.evaluate(candidate, context)
action = decision.action.value
await self.telemetry.event(
"output_supervisor.completed",
{
"session_id": context["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"action": action,
"approved": decision.approved,
"guidance": decision.guidance,
},
)
await self.observer.emit_ic(
"IC.OUTPUT_SUPERVISOR_COMPLETED",
{
"session_id": context["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"action": action,
"approved": decision.approved,
"result_count": len(decision.results),
},
component="workflow.output_supervisor",
)
if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
final_answer = decision.candidate
elif decision.action == RailAction.HANDOVER:
final_answer = "Vou encaminhar seu atendimento para continuidade com um especialista."
else:
final_answer = decision.fallback_message
return {
"answer": final_answer,
"final_answer": final_answer,
"supervisor_action": action,
"supervisor_guidance": decision.guidance,
"supervisor_attempt": int(state.get("supervisor_attempt", 0)) + (1 if decision.action == RailAction.RETRY else 0),
"supervisor_handover_reason": decision.handover_reason,
"output_supervisor_results": [
{
"code": r.code,
"action": r.action.value,
"reason": r.reason,
"guidance": r.guidance,
"metadata": r.metadata,
}
for r in decision.results
],
"output_guardrails_already_applied": True,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}
async def output_guardrails(self, state):
if state.get("output_guardrails_already_applied"):
return {"final_answer": state.get("final_answer") or state.get("answer") or ""}
async with self.telemetry.span(
"workflow.output_guardrails",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("answer"),
):
await self.observer.emit_grl(
"001",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "output",
"route": state.get("route"),
"intent": state.get("intent"),
},
component="workflow.output_guardrails.start",
)
final, decisions = await self.guardrails.run_output(
state["answer"], state.get("context", {})
)
for _decision in decisions:
await self.guardrail_telemetry.evaluated("output", _decision)
await self.observer.emit_grl(
"002" if _decision.allowed else "004",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "output",
"rail_code": getattr(_decision, "code", None),
"allowed": bool(_decision.allowed),
"reason": getattr(_decision, "reason", None),
},
component="workflow.output_guardrails.decision",
)
if not _decision.allowed:
await self.guardrail_telemetry.blocked("output", _decision)
await self.telemetry.event(
"guardrails.output.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"decisions": [d.model_dump() for d in decisions],
},
)
await self.observer.emit_grl(
"009",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "output",
"blocked": any(not d.allowed for d in decisions),
"decision_count": len(decisions),
},
component="workflow.output_guardrails.final",
)
return {
"final_answer": final,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [d.model_dump() for d in decisions],
}
async def judge(self, state):
async with self.telemetry.span(
"workflow.judge",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"question": state.get("user_text"), "answer": state.get("final_answer")},
):
results = await self.judges.evaluate_all(
state["user_text"], state["final_answer"], state.get("context", {})
)
for _result in results:
await self.judge_telemetry.evaluated(_result)
await self.telemetry.event(
"judges.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"results": [r.model_dump() for r in results],
},
)
return {"judge_results": [r.model_dump() for r in results]}
async def supervisor_review(self, state):
async with self.telemetry.span(
"workflow.supervisor_review",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("final_answer"),
):
ok, answer = await self.supervisor.review(
state["final_answer"], state.get("context", {})
)
await self.telemetry.event(
"supervisor.review.completed",
{"session_id": state.get("session_id"), "approved": ok},
)
return {"final_answer": answer if ok else answer}
async def persist(self, state):
async with self.telemetry.span(
"workflow.persist",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"route": state.get("route"), "intent": state.get("intent")},
):
await self.observer.emit_ic(
"AGENT_COMPLETED",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"route_decision": state.get("route_decision"),
"judges": state.get("judge_results", []),
"mcp_tools": state.get("mcp_tools", []),
"mcp_results": state.get("mcp_results", []),
},
)
await self.observer.emit_noc(
"006",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"answer_chars": len(state.get("final_answer") or ""),
},
component="workflow.persist",
)
await self.telemetry.event(
"agent.completed",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"answer_chars": len(state.get("final_answer") or ""),
},
)
return state
async def ainvoke(self, state):
thread_id = state.get("conversation_key") or state["session_id"]
config = {"configurable": {"thread_id": thread_id}}
async with self.telemetry.span(
"workflow.langgraph.ainvoke",
session_id=state.get("conversation_key") or state.get("session_id"),
user_id=state.get("context", {}).get("user_id"),
input={"user_text": state.get("user_text")},
tags=["langgraph", "agent-workflow", f"routing-mode:{getattr(self.settings, 'ROUTING_MODE', 'router')}",],
):
await self.workflow_telemetry.started("agent_workflow", state)
await self.observer.emit_noc(
"001",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"channel_id": (state.get("context") or {}).get("channel"),
"message_id": (state.get("context") or {}).get("message_id"),
"ura_call_id": (state.get("context") or {}).get("ura_call_id"),
},
component="workflow.ainvoke",
)
await self.observer.emit_ic(
"AGENT_STARTED",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"channel_id": (state.get("context") or {}).get("channel"),
"message_id": (state.get("context") or {}).get("message_id"),
"user_text_chars": len(state.get("user_text") or ""),
},
component="workflow.ainvoke",
)
try:
result = await self.graph.ainvoke(state, config=config)
await self.workflow_telemetry.completed("agent_workflow", result)
return result
except Exception as exc:
await self.workflow_telemetry.failed("agent_workflow", exc)
await self.observer.emit_noc(
"005",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"error": str(exc),
"exception_type": exc.__class__.__name__,
},
component="workflow.ainvoke",
)
raise

View File

@@ -0,0 +1,748 @@
"""Backoffice TIM/ANATEL workflows executed by the framework runtime.
This module is the migration boundary that makes the backoffice **framework-native**:
* domain logic remains in business nodes/services/prompts copied from develop;
* the backend no longer imports or executes ``src.agent.graphs.*``;
* the framework runtime builds/compiles the LangGraph workflows, owns telemetry,
guardrails, judges, supervisor, checkpoint and persistence hooks;
* legacy REST contracts call ``execute_workflow`` instead of legacy executors.
"""
from __future__ import annotations
from typing import Any, Callable
from pathlib import Path
import inspect
import json
import logging
import yaml
from langgraph.graph import END, START, StateGraph
from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
from agent_framework.guardrails.pipeline import GuardrailPipeline
from agent_framework.guardrails.output_supervisor import OutputSupervisor
from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
from agent_framework.observability.judge_events import JudgeTelemetry
from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry
from agent_framework.observability.observer import AgentObserver
from src.agent.state.agent_state import AgentState, create_initial_state, increment_iteration
from src.agent.state.steps import GraphStep
from src.agent.state.steps_emulator import EmulatorGraphStep
import src.agent.nodes as checklist_nodes
from src.agent.nodes.emulator import (
approve_draft_node,
close_case_node,
fetch_case_node,
generate_response_node,
persist_draft_node,
retrieve_history_node,
retrieve_templates_node,
router_node,
start_response_emulation_node,
validate_actions_node,
validate_response_node,
)
logger = logging.getLogger("backoffice.native_runtime")
def _project_root() -> Path:
return Path(__file__).resolve().parents[2]
def _load_yaml(path: str | Path) -> dict[str, Any]:
resolved = Path(path)
if not resolved.is_absolute():
resolved = _project_root() / resolved
if not resolved.exists():
raise FileNotFoundError(f"Configuração não encontrada: {resolved}")
with resolved.open("r", encoding="utf-8") as fh:
data = yaml.safe_load(fh) or {}
if not isinstance(data, dict):
raise ValueError(f"Configuração YAML inválida: {resolved}")
data["_config_path"] = str(resolved)
return data
def _resolve_agent_profile(agent_id: str = "backoffice_anatel") -> dict[str, Any]:
agents_cfg = _load_yaml("config/agents.yaml")
for agent in agents_cfg.get("agents", []) or []:
if agent.get("agent_id") == agent_id:
profile = dict(agent)
profile["_agents_config_path"] = agents_cfg.get("_config_path")
return profile
raise ValueError(f"agent_id={agent_id!r} não encontrado em config/agents.yaml")
def _build_guardrail_pipeline(*, settings, observer: AgentObserver, guardrails_config: dict[str, Any]) -> GuardrailPipeline:
"""Cria o GuardrailPipeline do framework amarrado ao profile do agente.
O framework local pode ter assinaturas diferentes conforme a versão. Este
helper tenta usar config_path/config/profile quando suportado; em versões
antigas, instancia o pipeline e anexa a configuração ativa para telemetry e
debug, evitando depender apenas do config global.
"""
base_kwargs: dict[str, Any] = {
"observer": observer,
"enable_parallel": bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
"fail_fast": bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
}
config_path = guardrails_config.get("_config_path")
try:
accepted = set(inspect.signature(GuardrailPipeline.__init__).parameters)
except Exception:
accepted = set()
kwargs = dict(base_kwargs)
if "config_path" in accepted:
kwargs["config_path"] = config_path
if "config_file" in accepted:
kwargs["config_file"] = config_path
if "config" in accepted:
kwargs["config"] = guardrails_config
if "profile" in accepted:
kwargs["profile"] = guardrails_config.get("profile") or guardrails_config.get("agent_id")
if "agent_id" in accepted:
kwargs["agent_id"] = guardrails_config.get("agent_id")
try:
pipeline = GuardrailPipeline(**kwargs)
except TypeError:
pipeline = GuardrailPipeline(**base_kwargs)
for method_name in ("load_config", "configure", "set_config", "with_config"):
method = getattr(pipeline, method_name, None)
if callable(method):
try:
method(guardrails_config)
break
except TypeError:
try:
method(config_path)
break
except TypeError:
continue
setattr(pipeline, "active_agent_id", guardrails_config.get("agent_id"))
setattr(pipeline, "active_profile", guardrails_config.get("profile"))
setattr(pipeline, "active_config_path", config_path)
setattr(pipeline, "active_config", guardrails_config)
return pipeline
class NativeOutputGuardrailRail:
code = "NATIVE_OUTPUT_GUARDRAILS"
def __init__(self, pipeline: GuardrailPipeline):
self.pipeline = pipeline
async def evaluate(self, candidate: str, context: dict[str, Any]):
final, decisions = await self.pipeline.run_output(candidate, context)
serialized = [d.model_dump() for d in decisions]
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
if blocked:
first = blocked[0]
code = (getattr(first, "code", "") or "").upper()
action = RailAction.RETRY if code in {"REVPREC", "CMP", "SCO", "GND"} else RailAction.BLOCK
return RailResult(
code=code or self.code,
action=action,
reason=getattr(first, "reason", "Resposta bloqueada por guardrail de saída"),
guidance=getattr(first, "reason", "Regerar resposta seguindo as políticas de saída."),
sanitized_text=final,
metadata={"native_decisions": serialized},
)
if final != candidate:
return RailResult(
code=self.code,
action=RailAction.SANITIZE,
reason="Resposta sanitizada por guardrail de saída.",
sanitized_text=final,
metadata={"native_decisions": serialized},
)
return RailResult(
code=self.code,
action=RailAction.ALLOW,
reason="Resposta aprovada pelos guardrails de saída.",
sanitized_text=final,
metadata={"native_decisions": serialized},
)
class BackofficeNativeRuntime:
"""Framework-owned workflow runtime for the backoffice domain."""
def __init__(self, *, settings, telemetry, analytics, observer: AgentObserver | None = None):
self.settings = settings
self.telemetry = telemetry
self.analytics = analytics
self.observer = observer or AgentObserver(analytics=analytics)
self.agent_profile = _resolve_agent_profile("backoffice_anatel")
self.guardrails_config = _load_yaml(self.agent_profile["guardrails_config_path"])
self.guardrails = _build_guardrail_pipeline(
settings=settings,
observer=self.observer,
guardrails_config=self.guardrails_config,
)
logger.info(
"Backoffice guardrails bound to framework profile agent_id=%s profile=%s config_path=%s input=%s output=%s",
self.guardrails_config.get("agent_id"),
self.guardrails_config.get("profile"),
self.guardrails_config.get("_config_path"),
[r.get("code") for r in self.guardrails_config.get("input", [])],
[r.get("code") for r in self.guardrails_config.get("output", [])],
)
self.output_supervisor_engine = OutputSupervisor(
rails=[NativeOutputGuardrailRail(self.guardrails)],
observer=self.observer,
max_retries=int(getattr(settings, "OUTPUT_SUPERVISOR_MAX_RETRIES", 3)),
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
)
self.judges = JudgePipeline()
self.supervisor = Supervisor()
self.workflow_telemetry = WorkflowTelemetry(telemetry)
self.guardrail_telemetry = GuardrailTelemetry(telemetry)
self.judge_telemetry = JudgeTelemetry(telemetry)
self.langgraph_telemetry = LangGraphDeepTelemetry(telemetry)
self._graphs: dict[str, Any] = {}
def _base_event_payload(self, state: AgentState | None = None, **extra: Any) -> dict[str, Any]:
state = state or {}
metadata = state.get("metadata", {}) or {}
payload = {
"workflow_id": metadata.get("framework_workflow_id"),
"session_id": state.get("session_id") or metadata.get("session_id") or metadata.get("transaction_id"),
"transaction_id": metadata.get("transaction_id"),
"agent_id": metadata.get("agent_id") or self.guardrails_config.get("agent_id"),
"guardrails_profile": metadata.get("guardrails_profile") or self.guardrails_config.get("profile"),
"framework_native": True,
}
payload.update({k: v for k, v in extra.items() if v is not None})
return payload
async def _safe_emit_ic(self, code: str, state: AgentState | None = None, payload: dict[str, Any] | None = None, *, component: str) -> None:
try:
await self.observer.emit_ic(code, self._base_event_payload(state, **(payload or {})), component=component)
except Exception as exc:
logger.debug("Falha ao emitir IC %s: %s", code, exc)
async def _safe_emit_noc(self, code: str, state: AgentState | None = None, payload: dict[str, Any] | None = None, *, component: str) -> None:
try:
normalized = code if str(code).startswith("NOC.") else f"NOC.{str(code).zfill(3)}"
await self.observer.emit_noc(normalized, self._base_event_payload(state, **(payload or {})), component=component)
except Exception as exc:
logger.debug("Falha ao emitir NOC %s: %s", code, exc)
async def _safe_emit_grl(self, code: str, state: AgentState | None = None, payload: dict[str, Any] | None = None, *, component: str) -> None:
try:
normalized = code if str(code).startswith("GRL.") else f"GRL.{str(code).zfill(3)}"
await self.observer.emit_grl(normalized, self._base_event_payload(state, **(payload or {})), component=component)
except Exception as exc:
logger.debug("Falha ao emitir GRL %s: %s", code, exc)
async def _emit_by_code(self, code: str, state: AgentState, payload: dict[str, Any] | None, *, component: str) -> None:
code = str(code or "IC.UNKNOWN")
if code.startswith("NOC."):
await self._safe_emit_noc(code, state, payload, component=component)
elif code.startswith("GRL."):
await self._safe_emit_grl(code, state, payload, component=component)
else:
await self._safe_emit_ic(code, state, payload, component=component)
async def _bridge_legacy_ics(self, state: AgentState, node_name: str) -> None:
"""Reemite IC/NOC/GRL legados do develop pelo AgentObserver do framework.
Os nós originais ainda chamam ``agent_framework.observer.event(...)``.
O coletor legado captura esses eventos; esta ponte pega apenas os novos
eventos desde a última etapa e os publica de forma padronizada no
observer do framework, preservando AGA.*, NOC.* e GRL.* no Langfuse/OCI.
"""
try:
from src.utils.ics_collector import ICsCollector
except Exception:
return
metadata = state.setdefault("metadata", {})
session_id = state.get("session_id") or metadata.get("transaction_id")
try:
events = ICsCollector.get_current(session_id) if session_id else []
except Exception:
events = []
last_idx = int(metadata.get("_framework_ics_bridge_index", 0) or 0)
new_events = events[last_idx:]
if not new_events:
return
metadata["_framework_ics_bridge_index"] = len(events)
bridge_log = metadata.setdefault("framework_ics_bridge", [])
for item in new_events:
code = str(item.get("code") or "IC.UNKNOWN")
event_payload = dict(item.get("metadata") or {})
event_payload.update({
"legacy_bridge": True,
"legacy_type": item.get("type"),
"legacy_description": item.get("description"),
"legacy_timestamp": item.get("timestamp"),
"source_node": node_name,
})
await self._emit_by_code(code, state, event_payload, component=f"backoffice.native_runtime.legacy_bridge.{node_name}")
bridge_log.append({"code": code, "type": item.get("type"), "node": node_name})
def _node(self, name: str, fn: Callable[[AgentState], Any]):
async def _wrapped(state: AgentState) -> AgentState:
async with self.langgraph_telemetry.node(name, state):
await self.telemetry.event(
"backoffice.workflow.node.started",
{"workflow_id": state.get("metadata", {}).get("framework_workflow_id"), "node": name, "session_id": state.get("session_id")},
kind="workflow",
)
await self._safe_emit_ic(
"IC.BACKOFFICE_NODE_STARTED",
state,
{"node": name},
component=f"backoffice.native_runtime.node.{name}",
)
try:
result = await fn(state)
except Exception as exc:
await self._safe_emit_noc(
"NOC.009",
state,
{"node": name, "type": "ERROR", "error_type": type(exc).__name__, "error": str(exc)},
component=f"backoffice.native_runtime.node.{name}",
)
raise
await self._bridge_legacy_ics(result, name)
await self.telemetry.event(
"backoffice.workflow.node.completed",
{"workflow_id": result.get("metadata", {}).get("framework_workflow_id"), "node": name, "session_id": result.get("session_id")},
kind="workflow",
)
await self._safe_emit_ic(
"IC.BACKOFFICE_NODE_COMPLETED",
result,
{"node": name, "has_error": bool(result.get("error"))},
component=f"backoffice.native_runtime.node.{name}",
)
return result
return _wrapped
async def _framework_input_guardrails(self, state: AgentState) -> AgentState:
await self._safe_emit_grl(
"GRL.001",
state,
{"phase": "input", "status": "started", "rails": [r.get("code") for r in self.guardrails_config.get("input", []) if r.get("enabled", True)]},
component="backoffice.native_runtime.guardrails.input",
)
payload = state.get("metadata", {}).get("request_context", {})
serialized = json.dumps(payload, ensure_ascii=False, default=str)
context = {
**state.get("metadata", {}),
"workflow_id": state.get("metadata", {}).get("framework_workflow_id"),
"agent_id": self.guardrails_config.get("agent_id"),
"guardrails_profile": self.guardrails_config.get("profile"),
"guardrails_config_path": self.guardrails_config.get("_config_path"),
"active_input_rails": [r.get("code") for r in self.guardrails_config.get("input", []) if r.get("enabled", True)],
"active_output_rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)],
}
sanitized, decisions = await self.guardrails.run_input(serialized, context)
state.setdefault("metadata", {})["framework_input_guardrails"] = [d.model_dump() for d in decisions]
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
await self._safe_emit_grl(
"GRL.002",
state,
{
"phase": "input",
"status": "completed",
"decision_count": len(decisions),
"blocked": bool(blocked),
"codes": [getattr(d, "code", None) for d in decisions],
},
component="backoffice.native_runtime.guardrails.input",
)
if blocked:
first = blocked[0]
state["error"] = {"type": "InputGuardrailBlocked", "message": getattr(first, "reason", "Entrada bloqueada"), "step": "framework_input_guardrails"}
state["final_response"] = sanitized or "Entrada bloqueada por política de segurança."
await self._safe_emit_grl(
"GRL.003",
state,
{"phase": "input", "status": "blocked", "code": getattr(first, "code", None), "reason": getattr(first, "reason", None)},
component="backoffice.native_runtime.guardrails.input",
)
return state
@staticmethod
def _after_input_guardrails(state: AgentState) -> str:
return "blocked" if state.get("error", {}).get("type") == "InputGuardrailBlocked" else "continue"
async def _framework_output_supervisor(self, state: AgentState) -> AgentState:
await self._safe_emit_grl(
"GRL.004",
state,
{"phase": "output_supervisor", "status": "started"},
component="backoffice.native_runtime.output_supervisor",
)
candidate = state.get("final_response") or state.get("response") or str(state.get("metadata", {}).get("request_context", {}).get("transactionId", ""))
context = {
**state.get("metadata", {}),
"session_id": state.get("session_id"),
"agent_id": self.guardrails_config.get("agent_id"),
"guardrails_profile": self.guardrails_config.get("profile"),
"guardrails_config_path": self.guardrails_config.get("_config_path"),
"active_output_rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)],
}
decision = await self.output_supervisor_engine.evaluate(candidate, context)
if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
final = decision.candidate
elif decision.action == RailAction.HANDOVER:
final = "Encaminhado para continuidade com especialista."
else:
final = decision.fallback_message
state["final_response"] = final
state.setdefault("metadata", {})["framework_output_supervisor"] = {
"action": decision.action.value,
"approved": decision.approved,
"results": [
{"code": r.code, "action": r.action.value, "reason": r.reason, "guidance": r.guidance, "metadata": r.metadata}
for r in decision.results
],
}
await self._safe_emit_grl(
"GRL.005",
state,
{
"phase": "output_supervisor",
"status": "completed",
"action": decision.action.value,
"approved": decision.approved,
"result_codes": [r.code for r in decision.results],
},
component="backoffice.native_runtime.output_supervisor",
)
if decision.action not in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
await self._safe_emit_grl(
"GRL.006",
state,
{"phase": "output_supervisor", "status": "blocked_or_handover", "action": decision.action.value},
component="backoffice.native_runtime.output_supervisor",
)
return state
async def _framework_output_guardrails(self, state: AgentState) -> AgentState:
await self._safe_emit_grl(
"GRL.007",
state,
{"phase": "output", "status": "started", "rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)]},
component="backoffice.native_runtime.guardrails.output",
)
candidate = state.get("final_response") or ""
context = {
**state.get("metadata", {}),
"agent_id": self.guardrails_config.get("agent_id"),
"guardrails_profile": self.guardrails_config.get("profile"),
"guardrails_config_path": self.guardrails_config.get("_config_path"),
"active_output_rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)],
}
final, decisions = await self.guardrails.run_output(candidate, context)
state["final_response"] = final
state.setdefault("metadata", {})["framework_output_guardrails"] = [d.model_dump() for d in decisions]
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
await self._safe_emit_grl(
"GRL.008",
state,
{
"phase": "output",
"status": "completed",
"decision_count": len(decisions),
"blocked": bool(blocked),
"sanitized": final != candidate,
"codes": [getattr(d, "code", None) for d in decisions],
},
component="backoffice.native_runtime.guardrails.output",
)
if blocked or final != candidate:
await self._safe_emit_grl(
"GRL.009",
state,
{"phase": "output", "status": "blocked_or_sanitized", "blocked": bool(blocked), "sanitized": final != candidate},
component="backoffice.native_runtime.guardrails.output",
)
return state
async def _framework_judges(self, state: AgentState) -> AgentState:
payload = state.get("metadata", {}).get("request_context", {})
question = json.dumps(payload, ensure_ascii=False, default=str)
answer = state.get("final_response") or ""
results = await self.judges.evaluate_all(question, answer, state.get("metadata", {}))
state.setdefault("metadata", {})["framework_judges"] = [r.model_dump() for r in results]
return state
async def _framework_supervisor_review(self, state: AgentState) -> AgentState:
answer = state.get("final_response") or ""
ok, reviewed = await self.supervisor.review(answer, state.get("metadata", {}))
state["final_response"] = reviewed if ok else reviewed
state.setdefault("metadata", {})["framework_supervisor_review"] = {"approved": ok}
return state
async def _framework_persist(self, state: AgentState) -> AgentState:
await self._safe_emit_ic(
"IC.BACKOFFICE_WORKFLOW_COMPLETED",
state,
{
"current_step": str(state.get("current_step")),
"has_error": bool(state.get("error")),
},
component="backoffice.native_runtime.persist",
)
await self._safe_emit_noc(
"NOC.006",
state,
{
"type": "INFO" if not state.get("error") else "FAILURE",
"status": "Backoffice workflow completed",
"current_step": str(state.get("current_step")),
},
component="backoffice.native_runtime.persist",
)
return state
def _compile(self, workflow_id: str):
if workflow_id == "backoffice_checklist":
return self._build_checklist_graph()
if workflow_id == "backoffice_response_emulator":
return self._build_emulator_graph()
raise ValueError(f"Unknown workflow_id={workflow_id}")
def get_graph(self, workflow_id: str):
graph = self._graphs.get(workflow_id)
if graph is None:
graph = self._compile(workflow_id)
self._graphs[workflow_id] = graph
return graph
async def execute_workflow(self, workflow_id: str, *, payload: dict[str, Any], transaction_id: str | None = None, app_state=None, metadata: dict[str, Any] | None = None) -> AgentState:
transaction_id = transaction_id or payload.get("transactionId") or payload.get("transaction_id") or "backoffice-session"
state = create_initial_state(session_id=transaction_id)
state["metadata"].update(metadata or {})
state["metadata"].update({
"transaction_id": transaction_id,
"request_context": payload,
"framework_workflow_id": workflow_id,
"framework_native": True,
"agent_id": self.guardrails_config.get("agent_id"),
"guardrails_profile": self.guardrails_config.get("profile"),
"guardrails_config_path": self.guardrails_config.get("_config_path"),
"active_input_rails": [r.get("code") for r in self.guardrails_config.get("input", []) if r.get("enabled", True)],
"active_output_rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)],
"_oci_producer": getattr(app_state, "oci_producer", None) if app_state is not None else None,
"_framework_ics_bridge_index": 0,
})
try:
from src.utils.ics_collector import ICsCollector
ICsCollector.start(transaction_id)
except Exception:
pass
await self._safe_emit_noc(
"NOC.001",
state,
{"type": "INFO", "status": "Backoffice workflow started"},
component="backoffice.native_runtime.execute",
)
await self._safe_emit_ic(
"IC.BACKOFFICE_WORKFLOW_STARTED",
state,
{"payload_keys": sorted(list(payload.keys()))},
component="backoffice.native_runtime.execute",
)
graph = self.get_graph(workflow_id)
config = {"configurable": {"thread_id": f"{workflow_id}:{transaction_id}"}}
try:
final_state = await graph.ainvoke(state, config=config)
await self._bridge_legacy_ics(final_state, "workflow_end")
return final_state
except Exception as exc:
await self._safe_emit_noc(
"NOC.009",
state,
{"type": "ERROR", "status": "Backoffice workflow failed", "error_type": type(exc).__name__, "error": str(exc)},
component="backoffice.native_runtime.execute",
)
raise
finally:
try:
final_ref = locals().get("final_state", state)
final_ref.get("metadata", {}).pop("_oci_producer", None)
final_ref.get("metadata", {}).pop("_framework_ics_bridge_index", None)
except Exception:
pass
try:
from src.utils.ics_collector import ICsCollector
ICsCollector.stop(transaction_id)
except Exception:
pass
# -------------------- Checklist workflow --------------------
def _build_checklist_graph(self):
builder = StateGraph(AgentState)
builder.add_node("framework_input_guardrails", self._node("framework_input_guardrails", self._framework_input_guardrails))
builder.add_node("fetch_ticket", self._node("fetch_ticket", self._fetch_ticket))
builder.add_node(GraphStep.VALIDATION, self._node(str(GraphStep.VALIDATION), checklist_nodes.validation_node.validate_ticket))
builder.add_node(GraphStep.BYPASS_RULES, self._node(str(GraphStep.BYPASS_RULES), checklist_nodes.bypass_rules_node.evaluate_bypass_rules))
builder.add_node(GraphStep.CACHE_CHECK, self._node(str(GraphStep.CACHE_CHECK), checklist_nodes.cache_check_node.check_cache_node))
builder.add_node(GraphStep.IMDB_ENRICHMENT, self._node(str(GraphStep.IMDB_ENRICHMENT), checklist_nodes.imdb_enrichment_node.imdb_enrich_ticket))
builder.add_node(GraphStep.IDENTITY_VERIFICATION, self._node(str(GraphStep.IDENTITY_VERIFICATION), checklist_nodes.identity_verification_node.perform_identity_verification))
builder.add_node(GraphStep.SPEECH_ENRICHMENT, self._node(str(GraphStep.SPEECH_ENRICHMENT), checklist_nodes.speech_enrichment_node.enrich_with_speech))
builder.add_node("knowledge_base_enrichment", self._node("knowledge_base_enrichment", checklist_nodes.knowledge_base_enrichment_node.enrich_with_knowledge_base))
builder.add_node(GraphStep.CANCELING_ANALYSIS, self._node(str(GraphStep.CANCELING_ANALYSIS), checklist_nodes.canceling_analysis_node.perform_canceling_analysis))
builder.add_node(GraphStep.TIM_COMPLAINT_ANALYSIS, self._node(str(GraphStep.TIM_COMPLAINT_ANALYSIS), checklist_nodes.tim_complaint_analysis_node.perform_tim_complaint_analysis))
builder.add_node("different_complaint_operator", self._node("different_complaint_operator", checklist_nodes.different_complaint_operator_node.perform_different_operator))
builder.add_node("undefined_complaint_operator", self._node("undefined_complaint_operator", checklist_nodes.undefined_complaint_operator_node.perform_undefined_complaint))
builder.add_node("tim_complaint", self._node("tim_complaint", checklist_nodes.tim_complaint_node.handle_tim_complaint))
builder.add_node(GraphStep.RECLASSIFICATION_ANALYSIS, self._node(str(GraphStep.RECLASSIFICATION_ANALYSIS), checklist_nodes.reclassification_analysis_node.perform_reclassification_analysis))
builder.add_node(GraphStep.TREATMENT_DECISION, self._node(str(GraphStep.TREATMENT_DECISION), checklist_nodes.treatment_decision_node.treatment_decision))
builder.add_node(GraphStep.SIEBEL_SR_OPENING, self._node(str(GraphStep.SIEBEL_SR_OPENING), checklist_nodes.siebel_sr_opening_node.open_siebel_sr))
builder.add_node("framework_output_supervisor", self._node("framework_output_supervisor", self._framework_output_supervisor))
builder.add_node("framework_output_guardrails", self._node("framework_output_guardrails", self._framework_output_guardrails))
builder.add_node("framework_judges", self._node("framework_judges", self._framework_judges))
builder.add_node("framework_supervisor_review", self._node("framework_supervisor_review", self._framework_supervisor_review))
builder.add_node("framework_persist", self._node("framework_persist", self._framework_persist))
builder.add_edge(START, "framework_input_guardrails")
builder.add_conditional_edges("framework_input_guardrails", self._after_input_guardrails, {"blocked": "framework_persist", "continue": "fetch_ticket"})
builder.add_edge("fetch_ticket", GraphStep.VALIDATION)
builder.add_conditional_edges(GraphStep.VALIDATION, checklist_nodes.validation_node.should_continue, {"continue": GraphStep.BYPASS_RULES, "reject": "framework_output_supervisor"})
builder.add_edge(GraphStep.BYPASS_RULES, GraphStep.CACHE_CHECK)
builder.add_conditional_edges(GraphStep.CACHE_CHECK, self._route_after_cache_check, {GraphStep.TREATMENT_DECISION: GraphStep.TREATMENT_DECISION, GraphStep.CANCELING_ANALYSIS: GraphStep.CANCELING_ANALYSIS, GraphStep.IMDB_ENRICHMENT: GraphStep.IMDB_ENRICHMENT})
builder.add_conditional_edges(GraphStep.IMDB_ENRICHMENT, checklist_nodes.imdb_enrichment_node.should_continue, {"continue": GraphStep.IDENTITY_VERIFICATION, "failed": "framework_output_supervisor"})
builder.add_conditional_edges(GraphStep.IDENTITY_VERIFICATION, checklist_nodes.identity_verification_node.route_after_identity_verification, {"proceed": GraphStep.SPEECH_ENRICHMENT, "cancel": GraphStep.SIEBEL_SR_OPENING, "smart_human": GraphStep.TREATMENT_DECISION, "failed": "framework_output_supervisor"})
builder.add_edge(GraphStep.SPEECH_ENRICHMENT, "knowledge_base_enrichment")
builder.add_conditional_edges("knowledge_base_enrichment", self._route_after_knowledge_base, {GraphStep.CANCELING_ANALYSIS: GraphStep.CANCELING_ANALYSIS, GraphStep.TREATMENT_DECISION: GraphStep.TREATMENT_DECISION})
builder.add_conditional_edges(GraphStep.CANCELING_ANALYSIS, self._route_after_canceling, {GraphStep.SIEBEL_SR_OPENING: GraphStep.SIEBEL_SR_OPENING, GraphStep.PROCEED_GRAPH: GraphStep.TIM_COMPLAINT_ANALYSIS, "finalize": "framework_output_supervisor"})
builder.add_conditional_edges(GraphStep.TIM_COMPLAINT_ANALYSIS, self._route_after_tim_complaint_analysis, {"tim_complaint": "tim_complaint", "different_complaint_operator": "different_complaint_operator", "undefined_complaint_operator": "undefined_complaint_operator", "finalize": "framework_output_supervisor"})
builder.add_edge("tim_complaint", GraphStep.RECLASSIFICATION_ANALYSIS)
builder.add_conditional_edges("different_complaint_operator", self._route_after_operator_check, {GraphStep.SIEBEL_SR_OPENING: GraphStep.SIEBEL_SR_OPENING, GraphStep.PROCEED_GRAPH: GraphStep.RECLASSIFICATION_ANALYSIS})
builder.add_conditional_edges("undefined_complaint_operator", self._route_after_operator_check, {GraphStep.SIEBEL_SR_OPENING: GraphStep.SIEBEL_SR_OPENING, GraphStep.PROCEED_GRAPH: GraphStep.RECLASSIFICATION_ANALYSIS})
builder.add_conditional_edges(GraphStep.RECLASSIFICATION_ANALYSIS, self._route_after_reclassification, {GraphStep.SIEBEL_SR_OPENING: GraphStep.SIEBEL_SR_OPENING, GraphStep.PROCEED_GRAPH: GraphStep.TREATMENT_DECISION, "finalize": "framework_output_supervisor"})
builder.add_edge(GraphStep.TREATMENT_DECISION, GraphStep.SIEBEL_SR_OPENING)
builder.add_edge(GraphStep.SIEBEL_SR_OPENING, "framework_output_supervisor")
builder.add_edge("framework_output_supervisor", "framework_output_guardrails")
builder.add_edge("framework_output_guardrails", "framework_judges")
builder.add_edge("framework_judges", "framework_supervisor_review")
builder.add_edge("framework_supervisor_review", "framework_persist")
builder.add_edge("framework_persist", END)
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
async def _fetch_ticket(self, state: AgentState) -> AgentState:
increment_iteration(state)
return await checklist_nodes.fetch_ticket_node.fetch_ticket_data(state)
@staticmethod
def _route_after_cache_check(state: AgentState) -> str:
if state.get("cache_found") is True:
if state.get("bypass_treatment_validations"):
return GraphStep.TREATMENT_DECISION
return GraphStep.CANCELING_ANALYSIS
return GraphStep.IMDB_ENRICHMENT
@staticmethod
def _route_after_knowledge_base(state: AgentState) -> str:
return GraphStep.TREATMENT_DECISION if state.get("bypass_treatment_validations") else GraphStep.CANCELING_ANALYSIS
@staticmethod
def _route_after_canceling(state: AgentState) -> str:
step = state.get("current_step")
if step == GraphStep.CANCELING_ANALYSIS_CANCEL_TICKET:
return GraphStep.SIEBEL_SR_OPENING
if step == GraphStep.PROCEED_GRAPH:
return GraphStep.PROCEED_GRAPH
return "finalize"
@staticmethod
def _route_after_tim_complaint_analysis(state: AgentState) -> str:
decision = state.get("metadata", {}).get("request_context", {}).get("is_tim_complaint", "")
if decision == "sim":
return "tim_complaint"
if decision == "não":
return "different_complaint_operator"
if decision == "inconclusivo":
return "undefined_complaint_operator"
return "finalize"
@staticmethod
def _route_after_operator_check(state: AgentState) -> str:
context = state.get("metadata", {}).get("request_context", {})
if context.get("forward_complaint"):
return GraphStep.SIEBEL_SR_OPENING
return GraphStep.PROCEED_GRAPH
@staticmethod
def _route_after_reclassification(state: AgentState) -> str:
step = state.get("current_step")
if step == GraphStep.RECLASSIFICATION_ANALYSIS_COMPLETED:
context = state.get("metadata", {}).get("request_context", {})
if context.get("siebel_action") == "reclassificar":
return GraphStep.SIEBEL_SR_OPENING
return GraphStep.PROCEED_GRAPH
return "finalize"
# -------------------- Emulator workflow --------------------
def _build_emulator_graph(self):
builder = StateGraph(AgentState)
builder.add_node("framework_input_guardrails", self._node("framework_input_guardrails", self._framework_input_guardrails))
builder.add_node(EmulatorGraphStep.RESPONSE_EMULATION_START, self._node(str(EmulatorGraphStep.RESPONSE_EMULATION_START), start_response_emulation_node.start_response_emulation))
builder.add_node(EmulatorGraphStep.FETCH_CASE, self._node(str(EmulatorGraphStep.FETCH_CASE), fetch_case_node.fetch_case))
builder.add_node(EmulatorGraphStep.VALIDATE_ACTIONS, self._node(str(EmulatorGraphStep.VALIDATE_ACTIONS), validate_actions_node.validate_actions))
builder.add_node(EmulatorGraphStep.ROUTER_DECISION, self._node(str(EmulatorGraphStep.ROUTER_DECISION), router_node.route))
builder.add_node(EmulatorGraphStep.RETRIEVE_TEMPLATES, self._node(str(EmulatorGraphStep.RETRIEVE_TEMPLATES), retrieve_templates_node.retrieve_templates))
builder.add_node(EmulatorGraphStep.RETRIEVE_HISTORY, self._node(str(EmulatorGraphStep.RETRIEVE_HISTORY), retrieve_history_node.retrieve_history))
builder.add_node(EmulatorGraphStep.GENERATE_RESPONSE, self._node(str(EmulatorGraphStep.GENERATE_RESPONSE), generate_response_node.generate_response))
builder.add_node(EmulatorGraphStep.VALIDATE_RESPONSE, self._node(str(EmulatorGraphStep.VALIDATE_RESPONSE), validate_response_node.validate_response))
builder.add_node(EmulatorGraphStep.PERSIST_DRAFT, self._node(str(EmulatorGraphStep.PERSIST_DRAFT), persist_draft_node.persist_draft))
builder.add_node(EmulatorGraphStep.APPROVE_DRAFT, self._node(str(EmulatorGraphStep.APPROVE_DRAFT), approve_draft_node.approve_draft))
builder.add_node(EmulatorGraphStep.CLOSE_CASE, self._node(str(EmulatorGraphStep.CLOSE_CASE), close_case_node.close_case))
builder.add_node("framework_output_supervisor", self._node("framework_output_supervisor", self._framework_output_supervisor))
builder.add_node("framework_output_guardrails", self._node("framework_output_guardrails", self._framework_output_guardrails))
builder.add_node("framework_judges", self._node("framework_judges", self._framework_judges))
builder.add_node("framework_supervisor_review", self._node("framework_supervisor_review", self._framework_supervisor_review))
builder.add_node("framework_persist", self._node("framework_persist", self._framework_persist))
builder.add_edge(START, "framework_input_guardrails")
builder.add_conditional_edges("framework_input_guardrails", self._after_input_guardrails, {"blocked": "framework_persist", "continue": EmulatorGraphStep.RESPONSE_EMULATION_START})
builder.add_edge(EmulatorGraphStep.RESPONSE_EMULATION_START, EmulatorGraphStep.FETCH_CASE)
builder.add_conditional_edges(EmulatorGraphStep.FETCH_CASE, self._emulator_route_after_fetch, {"generate": EmulatorGraphStep.VALIDATE_ACTIONS, "approve": EmulatorGraphStep.APPROVE_DRAFT, "close": EmulatorGraphStep.CLOSE_CASE, "failed": "framework_output_supervisor"})
builder.add_conditional_edges(EmulatorGraphStep.VALIDATE_ACTIONS, validate_actions_node.should_continue, {"continue": EmulatorGraphStep.ROUTER_DECISION, "failed": "framework_output_supervisor"})
builder.add_conditional_edges(EmulatorGraphStep.ROUTER_DECISION, router_node.next_step_after_router, {EmulatorGraphStep.RETRIEVE_TEMPLATES: EmulatorGraphStep.RETRIEVE_TEMPLATES, EmulatorGraphStep.RETRIEVE_HISTORY: EmulatorGraphStep.RETRIEVE_HISTORY, EmulatorGraphStep.GENERATE_RESPONSE: EmulatorGraphStep.GENERATE_RESPONSE})
builder.add_conditional_edges(EmulatorGraphStep.RETRIEVE_TEMPLATES, router_node.next_step_after_templates, {EmulatorGraphStep.RETRIEVE_HISTORY: EmulatorGraphStep.RETRIEVE_HISTORY, EmulatorGraphStep.GENERATE_RESPONSE: EmulatorGraphStep.GENERATE_RESPONSE})
builder.add_edge(EmulatorGraphStep.RETRIEVE_HISTORY, EmulatorGraphStep.GENERATE_RESPONSE)
builder.add_conditional_edges(EmulatorGraphStep.GENERATE_RESPONSE, generate_response_node.should_continue, {"continue": EmulatorGraphStep.VALIDATE_RESPONSE, "failed": "framework_output_supervisor"})
builder.add_edge(EmulatorGraphStep.VALIDATE_RESPONSE, EmulatorGraphStep.PERSIST_DRAFT)
builder.add_edge(EmulatorGraphStep.PERSIST_DRAFT, "framework_output_supervisor")
builder.add_edge(EmulatorGraphStep.APPROVE_DRAFT, "framework_output_supervisor")
builder.add_edge(EmulatorGraphStep.CLOSE_CASE, "framework_output_supervisor")
builder.add_edge("framework_output_supervisor", "framework_output_guardrails")
builder.add_edge("framework_output_guardrails", "framework_judges")
builder.add_edge("framework_judges", "framework_supervisor_review")
builder.add_edge("framework_supervisor_review", "framework_persist")
builder.add_edge("framework_persist", END)
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
@staticmethod
def _emulator_route_after_fetch(state: AgentState) -> str:
if state.get("error"):
return "failed"
flow_mode = (state.get("metadata") or {}).get("flow_mode")
if flow_mode == "close":
return "close"
if flow_mode == "approve":
return "approve"
return "generate"

BIN
config/.DS_Store vendored Normal file

Binary file not shown.

18
config/agents.yaml Normal file
View File

@@ -0,0 +1,18 @@
default_agent_id: backoffice_anatel
agents:
- agent_id: backoffice_anatel
name: Agente Backoffice ANATEL
description: Agente de backoffice convertido para o framework corporativo local.
prompt_policy_path: ./config/agents/backoffice_anatel/prompt_policy.yaml
routing_config_path: ./config/routing.yaml
guardrails_config_path: ./config/agents/backoffice_anatel/guardrails.yaml
judges_config_path: ./config/agents/backoffice_anatel/judges.yaml
mcp_servers_config_path: ./config/mcp_servers.yaml
tools_config_path: ./config/tools.yaml
metadata:
domain: backoffice
rag_namespace: backoffice_anatel
system_prefix: |
Você está executando o agente backoffice_anatel.
Use somente memória, checkpoints, guardrails, judges e ferramentas deste agent_id.
Não misture histórico de billing, sales, collection ou outros agentes.

BIN
config/agents/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,54 @@
agent_id: backoffice_anatel
profile: backoffice_anatel_enterprise
input:
- code: INPUT_SIZE
enabled: true
action: block
max_chars: 12000
- code: MSK
enabled: true
action: sanitize
- code: PINJ
enabled: true
action: block
- code: TOX
enabled: true
action: block
- code: DLEX_IN
enabled: true
action: block
- code: RAGSEC
enabled: true
action: block
- code: VLOOP
enabled: true
action: block
output:
- code: REVPREC
enabled: true
action: sanitize
- code: DLEX_OUT
enabled: true
action: sanitize
- code: OOS
enabled: true
action: review
- code: CMP
enabled: true
action: review
- code: AOFERTA
enabled: true
action: block
business_rules:
require_evidence_for:
- protocolo
- cliente
- contrato
- status_siebel
- acao_operacional
forbid_without_tool_ok:
- registrar_acao_backoffice
- registrar_acao_siebel
domain_workflows:
checklist: src.agent.graphs.main_graph.create_main_agent_graph
response_emulator: src.agent.graphs.emulator_graph.create_emulator_graph

View File

@@ -0,0 +1,20 @@
agent_id: backoffice_anatel
judges:
- name: response_quality
enabled: true
threshold: 0.75
- name: groundedness
enabled: true
threshold: 0.80
- name: backoffice_no_fabricated_protocol
enabled: true
description: Verifica se a resposta não inventa protocolo, cliente, contrato, SLA ou status operacional.
- name: siebel_action_requires_tool_ok
enabled: true
description: Bloqueia confirmação de registro/fechamento Siebel sem evidência ok/registered.
- name: anatel_domain_traceability
enabled: true
description: Exige rastreabilidade para decisão de cancelamento, reclassificação, tratamento ou encaminhamento.
- name: response_emulator_policy
enabled: true
description: Valida resposta formal ANATEL gerada pelo emulador antes de persistir/aprovar/fechar.

View File

@@ -0,0 +1,25 @@
agent_id: backoffice_anatel
system_prompt: |
Você é o agente Backoffice/ANATEL da TIM executado pelo Agent Framework.
Use a arquitetura padrão do framework para gateway, identidade, sessão, memória,
checkpoint, guardrails, judges, supervisor, observabilidade e MCP.
Para eventos completos de reclamação ANATEL, preserve o workflow original develop:
fetch_ticket, validation, bypass_rules, cache_check, imdb_enrichment,
identity_verification, speech_enrichment, knowledge_base_enrichment,
canceling_analysis, tim_complaint_analysis, tratamento por operadora,
reclassification_analysis, treatment_decision e siebel_sr_opening.
Para Response Emulator, preserve o workflow original develop:
start_response_emulation, fetch_case, validate_actions, router,
retrieve_templates, retrieve_history, generate_response, validate_response,
persist_draft, approve_draft e close_case.
Regras obrigatórias:
- Não invente protocolo, cliente, contrato, status, SLA, parecer, classificação ou ação Siebel.
- Não confirme registro operacional sem retorno ok/registered da ferramenta ou workflow.
- Use evidências de IMDB, Siebel, Speech Analytics, TAIS KB, ABRT e Portabilidade quando disponíveis.
- Se uma evidência não for encontrada, declare a ausência de evidência de forma objetiva.
- Se faltar identificador de reclamação/protocolo/chamado, peça somente esse dado.
- Toda resposta passa por OutputSupervisor, guardrails de saída e judges do framework.

View File

@@ -0,0 +1,8 @@
input:
- code: MSK
enabled: true
- code: VLOOP
enabled: true
output:
- code: REVPREC
enabled: true

View File

@@ -0,0 +1,7 @@
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6

View File

@@ -0,0 +1,6 @@
id: retail_orders_prompt_policy
version: 1
description: Prompt base isolado do agente de varejo/pedidos.
system_prefix: |
Você é um agente corporativo de varejo especializado em pedidos, entrega, troca, devolução e garantia.
Seja claro, objetivo e não use regras de negócio de telecom neste agente.

View File

@@ -0,0 +1,8 @@
input:
- code: MSK
enabled: true
- code: VLOOP
enabled: true
output:
- code: REVPREC
enabled: true

View File

@@ -0,0 +1,7 @@
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6

View File

@@ -0,0 +1,6 @@
id: telecom_contas_prompt_policy
version: 1
description: Prompt base isolado do agente de telecom/contas.
system_prefix: |
Você é um agente corporativo de atendimento telecom especializado em faturas, produtos, VAS e suporte.
Seja claro, objetivo e não prometa execução operacional sem ferramenta ou confirmação válida.

8
config/guardrails.yaml Normal file
View File

@@ -0,0 +1,8 @@
input:
- code: MSK
enabled: true
- code: VLOOP
enabled: true
output:
- code: REVPREC
enabled: true

79
config/identity.yaml Normal file
View File

@@ -0,0 +1,79 @@
identity:
version: "2"
required:
- session_key
keys:
customer_key:
description: Cliente/assinante canônico.
sources:
- business_context.customer_key
- customer_key
- customer.customerId
- customer.id
- customer.msisdn
- customer.document
- customer.cpf
- msisdn
- customer_id
- cpf_hash
- document_hash
- user_id
contract_key:
description: Contrato, linha, conta, asset, chamado ou reclamação principal.
sources:
- business_context.contract_key
- contract_key
- contract_id
- account_id
- asset_id
- complaint_id
- complaintProtocol
- complaint.complaintProtocol
- complaint.protocol
- complaint.protocol_id
- complaint.id
- transactionId
- transaction_id
- protocolo
- protocol_id
interaction_key:
description: Protocolo, reclamação, chamado, call id ou message id.
sources:
- business_context.interaction_key
- interaction_key
- protocol_id
- protocolo
- complaint_id
- complaintProtocol
- complaint.complaintProtocol
- complaint.protocol
- complaint.protocol_id
- complaint.id
- transactionId
- transaction_id
- ticket_id
- ura_call_id
- call_id
- message_id
account_key:
description: Conta comercial ou conta de cobrança.
sources:
- business_context.account_key
- account_key
- billing_account_id
- account_id
resource_key:
description: Recurso específico como linha, produto, asset ou serviço.
sources:
- business_context.resource_key
- resource_key
- msisdn
- asset_id
- product_id
session_key:
description: Sessão técnica estável escopada por tenant/agente.
sources:
- business_context.session_key
- session_key
- conversation_key
- session_id

7
config/judges.yaml Normal file
View File

@@ -0,0 +1,7 @@
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6

View File

@@ -0,0 +1,51 @@
defaults: {}
tools:
consultar_reclamacao:
map:
interaction_key: protocol_id
customer_key: customer_key
consultar_cliente_backoffice:
map:
customer_key: customer_key
contract_key: contract_key
session_key: session_key
registrar_acao_backoffice:
map:
interaction_key: protocol_id
session_key: operator_session
consultar_siebel_caso:
map:
interaction_key: protocol_id
customer_key: customer_key
registrar_acao_siebel:
map:
interaction_key: protocol_id
session_key: operator_session
consultar_imdb_cliente:
map:
customer_key: customer_key
contract_key: contract_key
session_key: session_key
consultar_speech_analytics:
map:
interaction_key: protocol_id
customer_key: customer_key
consultar_tais_kb:
map:
interaction_key: protocol_id
customer_key: customer_key
consultar_abrt:
map:
customer_key: customer_key
interaction_key: protocol_id
consultar_portabilidade:
map:
customer_key: customer_key
contract_key: contract_key
buscar_templates_emulador:
map:
interaction_key: protocol_id
gerar_rascunho_emulador:
map:
interaction_key: protocol_id

View File

@@ -0,0 +1,6 @@
servers:
backoffice:
transport: http
endpoint: http://backoffice-mcp:8010
enabled: true
description: MCP Server Backoffice via docker-compose.

7
config/mcp_servers.yaml Normal file
View File

@@ -0,0 +1,7 @@
servers:
backoffice:
description: Servidor MCP/HTTP de backoffice. Endpoint local padrão do MCP Server.
enabled: true
transport: http
endpoint: http://localhost:8010
timeout_seconds: 20

19
config/prompt_policy.yaml Normal file
View File

@@ -0,0 +1,19 @@
tone:
style: "claro, objetivo, empático"
forbidden_phrases:
- "procure atendimento humano"
vocabulary:
preferred:
fatura: "fatura"
contestacao: "contestação"
intents:
billing_agent:
- fatura
- boleto
- cobrança
- segunda via
product_agent:
- plano
- produto
- oferta
- serviço

120
config/routing.yaml Normal file
View File

@@ -0,0 +1,120 @@
router:
mode: router
fallback_agent: backoffice_agent
confidence_threshold: 0.60
allow_handoff: false
state_policies:
- state: BACKOFFICE_ACTIVE
agent: backoffice_agent
description: Mantém confirmações e complementos dentro do agente Backoffice/ANATEL nativo.
- state: WAITING_BACKOFFICE_IDENTIFIER
agent: backoffice_agent
description: Mantém mensagens curtas com protocolo/chamado/reclamação dentro do mesmo agente.
- state: BACKOFFICE_WAITING_ACTION_TEXT
agent: backoffice_agent
description: Solicita texto de ação antes de registrar operação em sistema externo.
intents:
- name: backoffice_anatel_triage
domain: backoffice
agent: backoffice_agent
description: Triagem completa de demanda de backoffice, reclamação, protocolo, ANATEL ou solicitação operacional.
priority: 10
next_state: BACKOFFICE_ACTIVE
mcp_tools:
- consultar_reclamacao
- consultar_cliente_backoffice
- consultar_siebel_caso
- consultar_imdb_cliente
- consultar_speech_analytics
- consultar_tais_kb
- consultar_abrt
- consultar_portabilidade
keywords:
- backoffice
- bko
- anatel
- reclamação
- reclamacao
- protocolo
- atendimento
- chamado
- demanda
- pendência
- pendencia
- contestação
- contestacao
examples:
- Preciso analisar uma reclamação da ANATEL.
- Consultar protocolo do cliente.
- Verificar pendência de atendimento.
- name: backoffice_customer_lookup
domain: backoffice
agent: backoffice_agent
description: Consulta de cliente, contrato, linha, CPF/CNPJ mascarado, protocolo ou solicitação operacional.
priority: 20
next_state: BACKOFFICE_ACTIVE
mcp_tools:
- consultar_cliente_backoffice
- consultar_imdb_cliente
- consultar_portabilidade
keywords:
- cliente
- contrato
- linha
- msisdn
- cpf
- cnpj
- cadastro
- conta
examples:
- Consultar dados do cliente.
- Verificar contrato da linha.
- name: backoffice_response_emulator
domain: backoffice
agent: backoffice_agent
description: Geração ou consulta de rascunho/resposta para emulador de resposta ANATEL.
priority: 25
next_state: BACKOFFICE_ACTIVE
mcp_tools:
- consultar_reclamacao
- buscar_templates_emulador
- gerar_rascunho_emulador
keywords:
- emulador
- rascunho
- minuta
- resposta
- template
- anatel
examples:
- Gerar rascunho de resposta da ANATEL.
- Buscar template para responder reclamação.
- name: backoffice_action_register
domain: backoffice
agent: backoffice_agent
description: Registro de ação, parecer, conclusão, encaminhamento ou atualização de demanda.
priority: 30
next_state: BACKOFFICE_ACTIVE
mcp_tools:
- consultar_reclamacao
- consultar_siebel_caso
- registrar_acao_backoffice
- registrar_acao_siebel
keywords:
- registrar
- concluir
- atualizar
- parecer
- encaminhar
- resolver
- finalizar
- observação
- observacao
examples:
- Registrar parecer no protocolo.
- Atualizar o chamado com a conclusão.

62
config/tools.yaml Normal file
View File

@@ -0,0 +1,62 @@
tools:
consultar_reclamacao:
description: Consulta reclamação/protocolo de backoffice/ANATEL.
mcp_server: backoffice
enabled: true
args_schema: { protocol_id: string, customer_key: string, interaction_key: string }
consultar_cliente_backoffice:
description: Consulta contexto operacional do cliente para backoffice.
mcp_server: backoffice
enabled: true
args_schema: { customer_key: string, contract_key: string, session_key: string }
registrar_acao_backoffice:
description: Registra ação operacional, parecer ou encaminhamento no sistema de backoffice.
mcp_server: backoffice
enabled: true
args_schema: { protocol_id: string, action_text: string, operator_session: string }
consultar_siebel_caso:
description: Consulta caso/SR no Siebel.
mcp_server: backoffice
enabled: true
args_schema: { protocol_id: string, interaction_key: string, customer_key: string }
registrar_acao_siebel:
description: Registra ação, reclassificação ou fechamento no Siebel.
mcp_server: backoffice
enabled: true
args_schema: { protocol_id: string, action_text: string, operator_session: string }
consultar_imdb_cliente:
description: Consulta enriquecimento IMDB/PMID do cliente.
mcp_server: backoffice
enabled: true
args_schema: { customer_key: string, contract_key: string, session_key: string }
consultar_speech_analytics:
description: Consulta histórico/resumo do Speech Analytics.
mcp_server: backoffice
enabled: true
args_schema: { protocol_id: string, customer_key: string, interaction_key: string }
consultar_tais_kb:
description: Consulta TAIS Knowledge Base/RAG e templates.
mcp_server: backoffice
enabled: true
args_schema: { query: string, protocol_id: string, customer_key: string }
consultar_abrt:
description: Consulta ABRT associado ao cliente/caso.
mcp_server: backoffice
enabled: true
args_schema: { customer_key: string, protocol_id: string }
consultar_portabilidade:
description: Consulta status de portabilidade.
mcp_server: backoffice
enabled: true
args_schema: { customer_key: string, contract_key: string }
buscar_templates_emulador:
description: Busca templates/documentos para Response Emulator.
mcp_server: backoffice
enabled: true
args_schema: { protocol_id: string, query: string }
gerar_rascunho_emulador:
description: Gera rascunho de resposta do Response Emulator.
mcp_server: backoffice
enabled: true
args_schema: { protocol_id: string, selected_actions: array, operator_instructions: string }

View File

@@ -0,0 +1,4 @@
# Introduction
<- Arquivos de configurações necessários (json,yalm, xml).

View File

@@ -0,0 +1,32 @@
# Example configuration file for Agent Microservice
# Copy this file to config.yaml and customize as needed
# Application Settings
APP_NAME: "Agent Microservice"
VERSION: "1.0.0"
DEBUG: false
# Server Settings
HOST: "0.0.0.0"
PORT: 8000
# LLM Configuration
LLM_PROVIDER: "openai" # Options: openai, anthropic
LLM_MODEL: "gpt-4"
LLM_TEMPERATURE: 0.7
LLM_MAX_TOKENS: null # null for no limit, or specify a number
# API Keys (NEVER commit actual keys!)
# Set these via environment variables instead
OPENAI_API_KEY: "your-openai-key-here"
ANTHROPIC_API_KEY: "your-anthropic-key-here"
# Logging Configuration
LOG_LEVEL: "INFO" # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_FORMAT: "json" # Options: json, text
# Agent Configuration (Optional)
AGENT_NAME: "Example Agent"
AGENT_DESCRIPTION: "An example agent built with LangGraph"
AGENT_SYSTEM_PROMPT: "You are a helpful AI assistant."
AGENT_MAX_ITERATIONS: 10

53
curl_test.txt Normal file
View File

@@ -0,0 +1,53 @@
curl --location 'http://localhost:8000/agent/process-and-stream' \
--header 'Content-Type: application/json' \
--data-raw '{
"caseType": "anatel",
"origin": {
"sourceSystem": "turbina_odc",
"submittedBy": {
"userId": "f8052701",
"name": "Nicolas Silva",
"email": null
}
},
"crmProtocol": "DS-987654321",
"ticketId": "ert",
"customer": {
"cpfCnpj": "06252533106",
"phones": [
"62981152324"
],
"msisdn": "62981152324",
"name": "Nicolas Ferreira da Silva",
"email": "nicolas.silva@exemplo.com.br",
"govBrSeal": null,
"odcCustomer": false,
"contumazCustomer": false,
"address": {
"cep": "01001-000",
"street": "Praca da Se",
"neighborhood": "Se",
"city": "Sao Paulo",
"state": "SP"
},
"subscriber": {
"cpfCnpj": "06252533106",
"subscriberName": "Assinante Exemplo",
"contactPhone": "62981152324",
"contactName": "Nicolas Ferreira da Silva"
}
},
"complaint": {
"complaintProtocol": "202603279001551",
"actionType": "nova",
"providerProtocol": null,
"inputChannel": "Anatel",
"service": "Celular Pós-pago",
"firstService": "Celular Pós-pago",
"modality": "Cobrança",
"motive": "Operadora liga ou envia mensagens indevidas de cobrança",
"description": "A TIM está me ligando cobrando planos que eu não assinei. Quero que parem",
"openedAt": "2026-03-27T11:45:00-03:00",
"dueAt": null
}
}'

3
ddl/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Introduction
DDLs para criação de tabelas ou views

108
ddl/anatel_notas.sql Normal file
View File

@@ -0,0 +1,108 @@
CREATE TABLE ANATEL_NOTAS_RAW (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
PROTOCOLO VARCHAR2(20 CHAR),
IDT_SOLICITACAO VARCHAR2(20 CHAR),
ID_INSTANCIA VARCHAR2(20 CHAR),
ID_TAREFA VARCHAR2(20 CHAR),
STATUS VARCHAR2(20 CHAR),
DATA_DOCUMENTO TIMESTAMP,
DATA_EVENTO TIMESTAMP,
DATA_CADASTRO TIMESTAMP,
DATA_LIMITE DATE,
DATA_RESPOSTA TIMESTAMP,
SERVICO VARCHAR2(50 CHAR),
PRIMEIRO_SERVICO VARCHAR2(50 CHAR),
MODALIDADE VARCHAR2(120 CHAR),
MOTIVO VARCHAR2(150 CHAR),
ACAO VARCHAR2(30 CHAR),
QTD_REABERTURA NUMBER(6),
RESPONSAVEL_TABULACAO_D0 VARCHAR2(120 CHAR),
DATA_TABULACAO_D0 TIMESTAMP,
TELEFONE_CONTATO_FIXO VARCHAR2(60 CHAR),
TELEFONE_RECLAMADO VARCHAR2(80 CHAR),
UF_CLIENTE VARCHAR2(2 CHAR),
CIDADE_CLIENTE VARCHAR2(80 CHAR),
ENDERECO_CLIENTE VARCHAR2(200 CHAR),
BAIRRO_CLIENTE VARCHAR2(120 CHAR),
CPF_CNPJ_CLIENTE VARCHAR2(20 CHAR),
NOME_CLIENTE VARCHAR2(150 CHAR),
CPF_CNPJ_ASSINANTE VARCHAR2(20 CHAR),
NOME_ASSINANTE VARCHAR2(150 CHAR),
PERFIL_RESPONSAVEL VARCHAR2(80 CHAR),
RESPONSAVEL VARCHAR2(120 CHAR),
SUPERVISOR VARCHAR2(120 CHAR),
COORDENADOR VARCHAR2(120 CHAR),
DESCRICAO_RECLAMACAO CLOB,
RESPOSTA_ANATEL CLOB,
JUSTIFICATIVA VARCHAR2(120 CHAR),
SITUACAO VARCHAR2(30 CHAR),
PENDENCIA_FUTURA VARCHAR2(10 CHAR),
MOTIVO_PENDENCIA_FUTURA VARCHAR2(60 CHAR),
DATA_PENDENCIA_FUTURA TIMESTAMP,
CANAL_ENTRADA VARCHAR2(30 CHAR),
STATUS_PROCESSO VARCHAR2(30 CHAR),
DATA_FINALIZADO TIMESTAMP,
RETIDO VARCHAR2(10 CHAR),
MOTIVO_RETENCAO VARCHAR2(60 CHAR),
DATA_IDA DATE,
TIPO_PESSOA VARCHAR2(5 CHAR),
EMAIL_CLIENTE VARCHAR2(320 CHAR),
NUMERO_CRM VARCHAR2(40 CHAR),
MOTIVO_RECLAMACAO_1 VARCHAR2(80 CHAR),
MOTIVO_RECLAMACAO_2 VARCHAR2(120 CHAR),
MOTIVO_RECLAMACAO_3 VARCHAR2(120 CHAR),
MOTIVO_PROBLEMA_1 VARCHAR2(80 CHAR),
MOTIVO_PROBLEMA_2 VARCHAR2(120 CHAR),
MOTIVO_PROBLEMA_3 VARCHAR2(100 CHAR),
MOTIVO_SOLUCAO_1 VARCHAR2(80 CHAR),
MOTIVO_SOLUCAO_2 VARCHAR2(80 CHAR),
MOTIVO_SOLUCAO_3 VARCHAR2(100 CHAR),
MOTIVO_REINCIDENTE VARCHAR2(80 CHAR),
DETALHE_MOTIVO_REINCIDENTE VARCHAR2(80 CHAR),
SITUACAO_FOCUS VARCHAR2(50 CHAR),
ULTIMA_ITERACAO VARCHAR2(10 CHAR),
SUBSERVICO VARCHAR2(50 CHAR),
PROTOCOLO_PRESTADORA VARCHAR2(30 CHAR),
CEP_CLIENTE VARCHAR2(12 CHAR),
SERVICE_ID VARCHAR2(30 CHAR),
AVALIACAO VARCHAR2(30 CHAR),
NOTA NUMBER(2),
RISCO_NOTA_BAIXA NUMBER(5,3),
RESOLVIDO VARCHAR2(10 CHAR),
REALIZAR_INCENTIVO VARCHAR2(10 CHAR),
TELEFONE_WHATSAPP VARCHAR2(20 CHAR),
ID_PRONTA_PARA_CONTATO VARCHAR2(50 CHAR),
DATA_ENVIO_PODE_FALAR_AGORA TIMESTAMP,
DATA_RETORNO_FALAR_AGORA TIMESTAMP,
RETORNO_HORARIO_OPERACIONAL VARCHAR2(10 CHAR),
RETORNO_FALAR_AGORA VARCHAR2(50 CHAR),
CLIENTE_FAVORAVEL VARCHAR2(10 CHAR),
SATISFACAO_CLIENTE VARCHAR2(30 CHAR),
RECLAMACAO_CONSIDERADA VARCHAR2(50 CHAR),
SELO_GOV_BR VARCHAR2(20 CHAR)
);
CREATE TABLE ANATEL_NOTAS_CHUNKS_RECLAMACAO_COHERE_3 (
ID_CHUNK NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ID NUMBER, -- referência para anatel_notas_raw.id
CHUNK_RECLAMACAO VARCHAR2(4096 CHAR),
EMBEDDING VECTOR NOT NULL,
NOTA NUMBER(2)
);
CREATE TABLE ANATEL_NOTAS_CHUNKS_RESPOSTA_COHERE_3 (
ID_CHUNK NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ID NUMBER, -- referência para anatel_notas_raw.id
CHUNK_RESPOSTA VARCHAR2(4096 CHAR),
EMBEDDING VECTOR NOT NULL,
NOTA NUMBER(2)
);
CREATE TABLE ANATEL_NOTAS_COHERE_4 (
ID NUMBER, -- referência para anatel_notas_raw.id
RECLAMACAO VARCHAR2(4096 CHAR),
EMBEDDING_RECLAMACAO VECTOR NOT NULL,
CHUNK_RESPOSTA VARCHAR2(4096 CHAR),
RESPOSTA VECTOR NOT NULL,
NOTA NUMBER(2)
);

View File

@@ -0,0 +1,14 @@
-- Criação da collection cases
DECLARE
collection SODA_Collection_T;
BEGIN
collection := DBMS_SODA.create_collection('cases');
END;
/
-- Criação da collection bo_memory
DECLARE
collection SODA_Collection_T;
BEGIN
collection := DBMS_SODA.create_collection('bo_memory');
END;
/

83
ddl/manual_anatel.sql Normal file
View File

@@ -0,0 +1,83 @@
CREATE TABLE ANATEL_MANUAL_CHUNKS_RAW (
CHUNK_ID VARCHAR2(128) NOT NULL,
CHUNK_PROFILE VARCHAR2(64) NOT NULL,
DOC_NAME VARCHAR2(255) NOT NULL,
PAGE_START NUMBER(10) NOT NULL,
PAGE_END NUMBER(10) NOT NULL,
SECTION_PATH CLOB,
CONTENT CLOB NOT NULL,
CONTENT_HASH VARCHAR2(64) NOT NULL,
MODALITY VARCHAR2(32) NOT NULL,
PARSER_BACKEND VARCHAR2(32) NOT NULL,
TOKEN_COUNT NUMBER(10) NOT NULL,
SOURCE_KIND VARCHAR2(64) NOT NULL,
CREATED_AT TIMESTAMP WITH TIME ZONE,
METADATA CLOB,
EMBEDDING_INPUT CLOB,
CONSTRAINT PK_ANATEL_MANUAL_CHUNKS_RAW
PRIMARY KEY (CHUNK_ID),
CONSTRAINT CK_ANATEL_MANUAL_CHUNKS_RAW_METADATA_JSON
CHECK (METADATA IS JSON)
);
CREATE TABLE ANATEL_MANUAL_CHUNKS_COHERE_3 (
CHUNK_ID VARCHAR2(128) NOT NULL,
CHUNK_PROFILE VARCHAR2(64) NOT NULL,
DOC_NAME VARCHAR2(255) NOT NULL,
PAGE_START NUMBER(10) NOT NULL,
PAGE_END NUMBER(10) NOT NULL,
SECTION_PATH CLOB,
CONTENT CLOB NOT NULL,
CONTENT_HASH VARCHAR2(64) NOT NULL,
MODALITY VARCHAR2(32) NOT NULL,
PARSER_BACKEND VARCHAR2(32) NOT NULL,
TOKEN_COUNT NUMBER(10) NOT NULL,
SOURCE_KIND VARCHAR2(64) NOT NULL,
CREATED_AT TIMESTAMP WITH TIME ZONE,
METADATA CLOB,
EMBEDDING_INPUT CLOB,
EMBEDDING_TEXT CLOB NOT NULL,
EMBEDDING VECTOR(1024, FLOAT32) NOT NULL,
CONSTRAINT PK_AMC_COHERE_3
PRIMARY KEY (CHUNK_ID),
CONSTRAINT FK_AMC_COHERE_3_RAW
FOREIGN KEY (CHUNK_ID)
REFERENCES ANATEL_MANUAL_CHUNKS_RAW (CHUNK_ID),
CONSTRAINT CK_AMC_COHERE_3_METADATA_JSON
CHECK (METADATA IS JSON)
);
CREATE TABLE ANATEL_MANUAL_CHUNKS_COHERE_4 (
CHUNK_ID VARCHAR2(128) NOT NULL,
CHUNK_PROFILE VARCHAR2(64) NOT NULL,
DOC_NAME VARCHAR2(255) NOT NULL,
PAGE_START NUMBER(10) NOT NULL,
PAGE_END NUMBER(10) NOT NULL,
SECTION_PATH CLOB,
CONTENT CLOB NOT NULL,
CONTENT_HASH VARCHAR2(64) NOT NULL,
MODALITY VARCHAR2(32) NOT NULL,
PARSER_BACKEND VARCHAR2(32) NOT NULL,
TOKEN_COUNT NUMBER(10) NOT NULL,
SOURCE_KIND VARCHAR2(64) NOT NULL,
CREATED_AT TIMESTAMP WITH TIME ZONE,
METADATA CLOB,
EMBEDDING_INPUT CLOB,
EMBEDDING_TEXT CLOB NOT NULL,
EMBEDDING VECTOR(1536, FLOAT32) NOT NULL,
CONSTRAINT PK_AMC_COHERE_4
PRIMARY KEY (CHUNK_ID),
CONSTRAINT FK_AMC_COHERE_4_RAW
FOREIGN KEY (CHUNK_ID)
REFERENCES ANATEL_MANUAL_CHUNKS_RAW (CHUNK_ID),
CONSTRAINT CK_AMC_COHERE_4_METADATA_JSON
CHECK (METADATA IS JSON)
);

30
ddl/templates.sql Normal file
View File

@@ -0,0 +1,30 @@
CREATE TABLE TEMPLATES_RAW (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ITEM VARCHAR2(250 CHAR),
QUANDO_USAR VARCHAR2(500 CHAR),
INFORMACOES_OBRIGATORIAS VARCHAR2(250 CHAR),
SUGESTAO_PARA_COMPOR_RESPOSTA VARCHAR2(4096 CHAR),
MENU VARCHAR2(100 CHAR)
);
CREATE TABLE TEMPLATES_COHERE_3 (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ITEM VARCHAR2(250 CHAR),
QUANDO_USAR VARCHAR2(500 CHAR),
INFORMACOES_OBRIGATORIAS VARCHAR2(250 CHAR),
SUGESTAO_PARA_COMPOR_RESPOSTA VARCHAR2(4096 CHAR),
MENU VARCHAR2(100 CHAR),
EMBEDDING_TEXT VARCHAR2(1024 CHAR),
EMBEDDING VECTOR NOT NULL
);
CREATE TABLE TEMPLATES_COHERE_4 (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ITEM VARCHAR2(250 CHAR),
QUANDO_USAR VARCHAR2(500 CHAR),
INFORMACOES_OBRIGATORIAS VARCHAR2(250 CHAR),
SUGESTAO_PARA_COMPOR_RESPOSTA VARCHAR2(4096 CHAR),
MENU VARCHAR2(100 CHAR),
EMBEDDING_TEXT VARCHAR2(1024 CHAR),
EMBEDDING VECTOR NOT NULL
);

View File

@@ -0,0 +1,3 @@
# Introduction
DDLs para criação de tabelas ou views

View File

@@ -0,0 +1,108 @@
CREATE TABLE ANATEL_NOTAS_RAW (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
PROTOCOLO VARCHAR2(20 CHAR),
IDT_SOLICITACAO VARCHAR2(20 CHAR),
ID_INSTANCIA VARCHAR2(20 CHAR),
ID_TAREFA VARCHAR2(20 CHAR),
STATUS VARCHAR2(20 CHAR),
DATA_DOCUMENTO TIMESTAMP,
DATA_EVENTO TIMESTAMP,
DATA_CADASTRO TIMESTAMP,
DATA_LIMITE DATE,
DATA_RESPOSTA TIMESTAMP,
SERVICO VARCHAR2(50 CHAR),
PRIMEIRO_SERVICO VARCHAR2(50 CHAR),
MODALIDADE VARCHAR2(120 CHAR),
MOTIVO VARCHAR2(150 CHAR),
ACAO VARCHAR2(30 CHAR),
QTD_REABERTURA NUMBER(6),
RESPONSAVEL_TABULACAO_D0 VARCHAR2(120 CHAR),
DATA_TABULACAO_D0 TIMESTAMP,
TELEFONE_CONTATO_FIXO VARCHAR2(60 CHAR),
TELEFONE_RECLAMADO VARCHAR2(80 CHAR),
UF_CLIENTE VARCHAR2(2 CHAR),
CIDADE_CLIENTE VARCHAR2(80 CHAR),
ENDERECO_CLIENTE VARCHAR2(200 CHAR),
BAIRRO_CLIENTE VARCHAR2(120 CHAR),
CPF_CNPJ_CLIENTE VARCHAR2(20 CHAR),
NOME_CLIENTE VARCHAR2(150 CHAR),
CPF_CNPJ_ASSINANTE VARCHAR2(20 CHAR),
NOME_ASSINANTE VARCHAR2(150 CHAR),
PERFIL_RESPONSAVEL VARCHAR2(80 CHAR),
RESPONSAVEL VARCHAR2(120 CHAR),
SUPERVISOR VARCHAR2(120 CHAR),
COORDENADOR VARCHAR2(120 CHAR),
DESCRICAO_RECLAMACAO CLOB,
RESPOSTA_ANATEL CLOB,
JUSTIFICATIVA VARCHAR2(120 CHAR),
SITUACAO VARCHAR2(30 CHAR),
PENDENCIA_FUTURA VARCHAR2(10 CHAR),
MOTIVO_PENDENCIA_FUTURA VARCHAR2(60 CHAR),
DATA_PENDENCIA_FUTURA TIMESTAMP,
CANAL_ENTRADA VARCHAR2(30 CHAR),
STATUS_PROCESSO VARCHAR2(30 CHAR),
DATA_FINALIZADO TIMESTAMP,
RETIDO VARCHAR2(10 CHAR),
MOTIVO_RETENCAO VARCHAR2(60 CHAR),
DATA_IDA DATE,
TIPO_PESSOA VARCHAR2(5 CHAR),
EMAIL_CLIENTE VARCHAR2(320 CHAR),
NUMERO_CRM VARCHAR2(40 CHAR),
MOTIVO_RECLAMACAO_1 VARCHAR2(80 CHAR),
MOTIVO_RECLAMACAO_2 VARCHAR2(120 CHAR),
MOTIVO_RECLAMACAO_3 VARCHAR2(120 CHAR),
MOTIVO_PROBLEMA_1 VARCHAR2(80 CHAR),
MOTIVO_PROBLEMA_2 VARCHAR2(120 CHAR),
MOTIVO_PROBLEMA_3 VARCHAR2(100 CHAR),
MOTIVO_SOLUCAO_1 VARCHAR2(80 CHAR),
MOTIVO_SOLUCAO_2 VARCHAR2(80 CHAR),
MOTIVO_SOLUCAO_3 VARCHAR2(100 CHAR),
MOTIVO_REINCIDENTE VARCHAR2(80 CHAR),
DETALHE_MOTIVO_REINCIDENTE VARCHAR2(80 CHAR),
SITUACAO_FOCUS VARCHAR2(50 CHAR),
ULTIMA_ITERACAO VARCHAR2(10 CHAR),
SUBSERVICO VARCHAR2(50 CHAR),
PROTOCOLO_PRESTADORA VARCHAR2(30 CHAR),
CEP_CLIENTE VARCHAR2(12 CHAR),
SERVICE_ID VARCHAR2(30 CHAR),
AVALIACAO VARCHAR2(30 CHAR),
NOTA NUMBER(2),
RISCO_NOTA_BAIXA NUMBER(5,3),
RESOLVIDO VARCHAR2(10 CHAR),
REALIZAR_INCENTIVO VARCHAR2(10 CHAR),
TELEFONE_WHATSAPP VARCHAR2(20 CHAR),
ID_PRONTA_PARA_CONTATO VARCHAR2(50 CHAR),
DATA_ENVIO_PODE_FALAR_AGORA TIMESTAMP,
DATA_RETORNO_FALAR_AGORA TIMESTAMP,
RETORNO_HORARIO_OPERACIONAL VARCHAR2(10 CHAR),
RETORNO_FALAR_AGORA VARCHAR2(50 CHAR),
CLIENTE_FAVORAVEL VARCHAR2(10 CHAR),
SATISFACAO_CLIENTE VARCHAR2(30 CHAR),
RECLAMACAO_CONSIDERADA VARCHAR2(50 CHAR),
SELO_GOV_BR VARCHAR2(20 CHAR)
);
CREATE TABLE ANATEL_NOTAS_CHUNKS_RECLAMACAO_COHERE_3 (
ID_CHUNK NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ID NUMBER, -- referência para anatel_notas_raw.id
CHUNK_RECLAMACAO VARCHAR2(4096 CHAR),
EMBEDDING VECTOR NOT NULL,
NOTA NUMBER(2)
);
CREATE TABLE ANATEL_NOTAS_CHUNKS_RESPOSTA_COHERE_3 (
ID_CHUNK NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ID NUMBER, -- referência para anatel_notas_raw.id
CHUNK_RESPOSTA VARCHAR2(4096 CHAR),
EMBEDDING VECTOR NOT NULL,
NOTA NUMBER(2)
);
CREATE TABLE ANATEL_NOTAS_COHERE_4 (
ID NUMBER, -- referência para anatel_notas_raw.id
RECLAMACAO VARCHAR2(4096 CHAR),
EMBEDDING_RECLAMACAO VECTOR NOT NULL,
CHUNK_RESPOSTA VARCHAR2(4096 CHAR),
RESPOSTA VECTOR NOT NULL,
NOTA NUMBER(2)
);

View File

@@ -0,0 +1,14 @@
-- Criação da collection cases
DECLARE
collection SODA_Collection_T;
BEGIN
collection := DBMS_SODA.create_collection('cases');
END;
/
-- Criação da collection bo_memory
DECLARE
collection SODA_Collection_T;
BEGIN
collection := DBMS_SODA.create_collection('bo_memory');
END;
/

View File

@@ -0,0 +1,83 @@
CREATE TABLE ANATEL_MANUAL_CHUNKS_RAW (
CHUNK_ID VARCHAR2(128) NOT NULL,
CHUNK_PROFILE VARCHAR2(64) NOT NULL,
DOC_NAME VARCHAR2(255) NOT NULL,
PAGE_START NUMBER(10) NOT NULL,
PAGE_END NUMBER(10) NOT NULL,
SECTION_PATH CLOB,
CONTENT CLOB NOT NULL,
CONTENT_HASH VARCHAR2(64) NOT NULL,
MODALITY VARCHAR2(32) NOT NULL,
PARSER_BACKEND VARCHAR2(32) NOT NULL,
TOKEN_COUNT NUMBER(10) NOT NULL,
SOURCE_KIND VARCHAR2(64) NOT NULL,
CREATED_AT TIMESTAMP WITH TIME ZONE,
METADATA CLOB,
EMBEDDING_INPUT CLOB,
CONSTRAINT PK_ANATEL_MANUAL_CHUNKS_RAW
PRIMARY KEY (CHUNK_ID),
CONSTRAINT CK_ANATEL_MANUAL_CHUNKS_RAW_METADATA_JSON
CHECK (METADATA IS JSON)
);
CREATE TABLE ANATEL_MANUAL_CHUNKS_COHERE_3 (
CHUNK_ID VARCHAR2(128) NOT NULL,
CHUNK_PROFILE VARCHAR2(64) NOT NULL,
DOC_NAME VARCHAR2(255) NOT NULL,
PAGE_START NUMBER(10) NOT NULL,
PAGE_END NUMBER(10) NOT NULL,
SECTION_PATH CLOB,
CONTENT CLOB NOT NULL,
CONTENT_HASH VARCHAR2(64) NOT NULL,
MODALITY VARCHAR2(32) NOT NULL,
PARSER_BACKEND VARCHAR2(32) NOT NULL,
TOKEN_COUNT NUMBER(10) NOT NULL,
SOURCE_KIND VARCHAR2(64) NOT NULL,
CREATED_AT TIMESTAMP WITH TIME ZONE,
METADATA CLOB,
EMBEDDING_INPUT CLOB,
EMBEDDING_TEXT CLOB NOT NULL,
EMBEDDING VECTOR(1024, FLOAT32) NOT NULL,
CONSTRAINT PK_AMC_COHERE_3
PRIMARY KEY (CHUNK_ID),
CONSTRAINT FK_AMC_COHERE_3_RAW
FOREIGN KEY (CHUNK_ID)
REFERENCES ANATEL_MANUAL_CHUNKS_RAW (CHUNK_ID),
CONSTRAINT CK_AMC_COHERE_3_METADATA_JSON
CHECK (METADATA IS JSON)
);
CREATE TABLE ANATEL_MANUAL_CHUNKS_COHERE_4 (
CHUNK_ID VARCHAR2(128) NOT NULL,
CHUNK_PROFILE VARCHAR2(64) NOT NULL,
DOC_NAME VARCHAR2(255) NOT NULL,
PAGE_START NUMBER(10) NOT NULL,
PAGE_END NUMBER(10) NOT NULL,
SECTION_PATH CLOB,
CONTENT CLOB NOT NULL,
CONTENT_HASH VARCHAR2(64) NOT NULL,
MODALITY VARCHAR2(32) NOT NULL,
PARSER_BACKEND VARCHAR2(32) NOT NULL,
TOKEN_COUNT NUMBER(10) NOT NULL,
SOURCE_KIND VARCHAR2(64) NOT NULL,
CREATED_AT TIMESTAMP WITH TIME ZONE,
METADATA CLOB,
EMBEDDING_INPUT CLOB,
EMBEDDING_TEXT CLOB NOT NULL,
EMBEDDING VECTOR(1536, FLOAT32) NOT NULL,
CONSTRAINT PK_AMC_COHERE_4
PRIMARY KEY (CHUNK_ID),
CONSTRAINT FK_AMC_COHERE_4_RAW
FOREIGN KEY (CHUNK_ID)
REFERENCES ANATEL_MANUAL_CHUNKS_RAW (CHUNK_ID),
CONSTRAINT CK_AMC_COHERE_4_METADATA_JSON
CHECK (METADATA IS JSON)
);

View File

@@ -0,0 +1,30 @@
CREATE TABLE TEMPLATES_RAW (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ITEM VARCHAR2(250 CHAR),
QUANDO_USAR VARCHAR2(500 CHAR),
INFORMACOES_OBRIGATORIAS VARCHAR2(250 CHAR),
SUGESTAO_PARA_COMPOR_RESPOSTA VARCHAR2(4096 CHAR),
MENU VARCHAR2(100 CHAR)
);
CREATE TABLE TEMPLATES_COHERE_3 (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ITEM VARCHAR2(250 CHAR),
QUANDO_USAR VARCHAR2(500 CHAR),
INFORMACOES_OBRIGATORIAS VARCHAR2(250 CHAR),
SUGESTAO_PARA_COMPOR_RESPOSTA VARCHAR2(4096 CHAR),
MENU VARCHAR2(100 CHAR),
EMBEDDING_TEXT VARCHAR2(1024 CHAR),
EMBEDDING VECTOR NOT NULL
);
CREATE TABLE TEMPLATES_COHERE_4 (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
ITEM VARCHAR2(250 CHAR),
QUANDO_USAR VARCHAR2(500 CHAR),
INFORMACOES_OBRIGATORIAS VARCHAR2(250 CHAR),
SUGESTAO_PARA_COMPOR_RESPOSTA VARCHAR2(4096 CHAR),
MENU VARCHAR2(100 CHAR),
EMBEDDING_TEXT VARCHAR2(1024 CHAR),
EMBEDDING VECTOR NOT NULL
);

28
docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
services:
backoffice-api:
build: .
container_name: backoffice-api
env_file:
- .env
environment:
BACKOFFICE_MCP_BASE_URL: http://backoffice-mcp:8010
MCP_SERVERS_CONFIG: /app/config/mcp_servers.docker.yaml
MCP_TOOLS_CONFIG: /app/config/tools.yaml
MCP_PARAMETER_MAPPING_CONFIG: /app/config/mcp_parameter_mapping.yaml
ports:
- "8000:8000"
depends_on:
- backoffice-mcp
backoffice-mcp:
build: .
container_name: backoffice-mcp
env_file:
- .env.backoffice_mcp.example
command: >
sh -c 'uvicorn mcp_servers.backoffice_mcp_server.main:app
--host $${BACKOFFICE_MCP_HOST:-0.0.0.0}
--port $${BACKOFFICE_MCP_PORT:-8010}
--log-level $${BACKOFFICE_MCP_LOG_LEVEL:-info}'
ports:
- "8010:8010"

View File

@@ -0,0 +1,39 @@
# Ajustes IC/NOC/GRL e LLM_PROVIDER=oci_openai
Esta versão corrige dois pontos do runtime nativo do backoffice:
1. **IC/NOC/GRL padronizados pelo framework**
- O workflow nativo agora emite eventos pelo `AgentObserver` do framework.
- Eventos legados `AGA.*`, `NOC.*` e `GRL.*` capturados dos nós originais são reemitidos pela ponte `_bridge_legacy_ics`.
- O runtime emite explicitamente eventos de início, conclusão e falha do workflow.
- Os guardrails emitem GRL explícitos para input, output supervisor e output guardrails.
2. **Suporte ao `LLM_PROVIDER=oci_openai`**
- `src/core/config.py` agora aceita `oci_openai` no validador local usado pelos nós do backoffice original.
- `.env.example` usa `LLM_PROVIDER=oci_openai` por padrão e documenta as variáveis esperadas para OCI/OpenAI-compatible.
## Eventos adicionados no workflow nativo
- `IC.BACKOFFICE_WORKFLOW_STARTED`
- `IC.BACKOFFICE_WORKFLOW_COMPLETED`
- `IC.BACKOFFICE_NODE_STARTED`
- `IC.BACKOFFICE_NODE_COMPLETED`
- `NOC.001` workflow started
- `NOC.006` workflow completed
- `NOC.009` workflow/node failed
- `GRL.001` input guardrails started
- `GRL.002` input guardrails completed
- `GRL.003` input blocked
- `GRL.004` output supervisor started
- `GRL.005` output supervisor completed
- `GRL.006` output supervisor blocked/handover
- `GRL.007` output guardrails started
- `GRL.008` output guardrails completed
- `GRL.009` output blocked/sanitized
## Arquivos alterados
- `app/workflows/backoffice_native_runtime.py`
- `src/core/config.py`
- `.env.example`
- `tools/validate_parity.py`

View File

@@ -0,0 +1,95 @@
# Atualização do Template Backend — Analytics, Observer, NOC/GRL e OutputSupervisor
Esta versão do `agent_template_backend` foi atualizada para consumir as novidades transportadas para o `agent_framework`.
## 1. Analytics e Pub/Sub
O backend não chama mais diretamente apenas o publisher antigo de eventos. Agora ele cria um `AnalyticsPublisher`:
```python
from agent_framework.analytics.factory import create_analytics_publisher
from agent_framework.observability.observer import AgentObserver
analytics = create_analytics_publisher(settings)
observer = AgentObserver(analytics=analytics)
```
Com isso, o mesmo backend pode publicar em:
- OCI Streaming
- GCP Pub/Sub
- CompositePublisher, quando `ANALYTICS_PROVIDERS=oci_streaming,pubsub`
- Noop, quando analytics estiver desligado
## 2. Configuração mínima
```env
ENABLE_ANALYTICS=true
ANALYTICS_PROVIDERS=pubsub
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
```
Para publicar simultaneamente em OCI Streaming e GCP Pub/Sub:
```env
ENABLE_ANALYTICS=true
ANALYTICS_PROVIDERS=oci_streaming,pubsub
ENABLE_OCI_STREAMING=true
OCI_STREAM_ENDPOINT=<endpoint>
OCI_STREAM_OCID=<stream-ocid>
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
```
## 3. Observer corporativo
O workflow recebeu emissão automática dos principais eventos corporativos:
- `NOC.001`: início do workflow
- `NOC.005`: exceção fatal no workflow
- `NOC.006`: fim do workflow antes da resposta final
- `IC.AGENT_COMPLETED`: evento informacional de conclusão
- `GRL.001` a `GRL.009`: emitidos pelo `OutputSupervisor`
## 4. OutputSupervisor
Foi inserido um novo nó LangGraph:
```text
agent -> output_supervisor -> output_guardrails -> judge -> supervisor_review -> persist
```
O `OutputSupervisor` não substitui o supervisor de roteamento. Ele valida a saída candidata do agente usando o contrato corporativo:
- `allow`
- `sanitize`
- `retry`
- `block`
- `handover`
- `observe`
Para compatibilidade com os guardrails já existentes, o template inclui o adapter `LegacyOutputGuardrailRail`, que converte decisões antigas `allowed=True/False` para `RailAction`.
## 5. Campos adicionados ao AgentState
```python
supervisor_action: str
supervisor_guidance: str
supervisor_attempt: int
supervisor_handover_reason: str
output_supervisor_results: list[dict]
output_guardrails_already_applied: bool
```
## 6. Arquivos alterados
- `agent_template_backend/app/main.py`
- `agent_template_backend/app/workflows/agent_graph.py`
- `agent_template_backend/app/state.py`
- `agent_template_backend/.env`
- `agent_template_backend/requirements.txt`
- `agent_framework/src/agent_framework/config/settings.py`
## 7. Observação importante
O `OutputSupervisor` roda os guardrails de saída por meio do adapter legado e marca `output_guardrails_already_applied=True`. Assim o nó `output_guardrails` permanece no grafo para compatibilidade, mas evita reexecutar a mesma validação quando o supervisor já aplicou os rails.

View File

@@ -0,0 +1,45 @@
# Como usar IC, NOC e GRL no Template Backend
## IC — Item de Controle
Use IC para registrar eventos de negócio relevantes.
```python
await observer.emit_ic(
"IC.FATURA_CONSULTADA",
{"session_id": session_id, "invoice_id": invoice_id},
component="billing_agent",
)
```
## NOC — Evento operacional
Use NOC para saúde técnica, latência, erros e checkpoints operacionais.
```python
await observer.emit_noc(
"003",
{"session_id": session_id, "resourceName": "ADB", "latencyMs": 120},
component="repository",
)
```
## GRL — Evento de guardrail
Normalmente o framework emite GRL automaticamente. Use manualmente apenas para
rails customizados dentro do agente.
```python
await observer.emit_grl(
"OBSERVE",
{"session_id": session_id, "rail_code": "CUSTOM_POLICY"},
component="custom_rail",
)
```
## Onde já existe no template
- `app/workflows/agent_graph.py` emite IC/NOC no ciclo do workflow.
- `app/agents/runtime.py` emite IC para MCP/tools.
- `app/agents/*_agent.py` contém exemplos dentro do método `run()`.
- `app/examples/` contém exemplos isolados.

View File

@@ -0,0 +1,88 @@
# Correção: .env original TIM + MCP Server + URLs
## Problema corrigido
O pacote anterior ainda tratava o MCP Server como um backend REST genérico baseado em `BACKOFFICE_MCP_REST_BASE_URL`. Quando essa variável não existia, uma chamada HTTP podia ser montada com URL vazia, gerando erro como:
```text
Request URL is missing an 'http://' or 'https://' protocol.
```
Além disso, o `.env` original do backoffice `develop` usa variáveis específicas:
```text
PMID_API_HOST
SIEBEL_API_HOST
SPEECH_PREDICTION_BASE_URL
SPEECH_HISTORY_BASE_URL
TAIS_DB_DSN
TAIS_GENAI_ENDPOINT
...
```
Essas variáveis não devem ser substituídas por uma URL REST genérica.
## Ajustes implementados
1. `src/core/config.py` agora carrega explicitamente o `.env` da raiz do projeto.
2. `config/mcp_servers.yaml` agora usa endpoint literal seguro:
```yaml
endpoint: http://localhost:8010
```
Isso evita depender de expansão YAML `${VAR:default}` caso o `MCPToolRouter` do framework não suporte esse formato.
3. `mcp_servers/backoffice_mcp_server/settings.py` agora aceita:
- variáveis `BACKOFFICE_MCP_*`, quando existirem;
- variáveis originais TIM/develop, como `PMID_API_HOST`, `SIEBEL_API_HOST`, `SPEECH_*`, `TAIS_*`.
4. O default do MCP Server passou a ser:
```text
BACKOFFICE_MCP_BACKEND_TYPE=tim_clients
BACKOFFICE_MCP_USE_MOCK=false
```
Assim o MCP não mascara erro usando mock quando o `.env` real está presente.
5. As tools IMDB/PMID, Speech, TAIS, ABRT e Portabilidade passam a chamar os clients originais do projeto quando `backend_type=tim_clients`.
## Como testar
Suba o MCP Server:
```bash
uvicorn mcp_servers.backoffice_mcp_server.main:app --host 0.0.0.0 --port 8010 --reload
```
Confirme o health:
```bash
curl http://localhost:8010/health
```
O retorno deve indicar:
```json
{
"use_mock": false,
"backend_type": "tim_clients",
"tim_clients_configured": true
}
```
Depois suba o backend principal:
```bash
uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
```
Teste a lista de tools via backend:
```bash
curl http://localhost:9000/debug/mcp/tools
```
Se o MCP estiver ativo, essa chamada deve chegar no processo da porta `8010`.

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