mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-09 13:54:20 +00:00
first commit
This commit is contained in:
239
.env
Normal file
239
.env
Normal 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
|
||||
29
.env.backoffice_mcp.example
Normal file
29
.env.backoffice_mcp.example
Normal 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
98
.env.example
Normal 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
12
.idea/.gitignore
generated
vendored
Normal 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/
|
||||
9
.idea/backoffice_convertido_framework.iml
generated
Normal file
9
.idea/backoffice_convertido_framework.iml
generated
Normal 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
7
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
6
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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>
|
||||
24
.oca/custom_code_review_guidelines.txt
Normal file
24
.oca/custom_code_review_guidelines.txt
Normal 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
6
Dockerfile
Normal 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"]
|
||||
156
README_BACKOFFICE_CONVERTIDO_100.md
Normal file
156
README_BACKOFFICE_CONVERTIDO_100.md
Normal 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.
|
||||
77
README_BACKOFFICE_FRAMEWORK_NATIVE_100.md
Normal file
77
README_BACKOFFICE_FRAMEWORK_NATIVE_100.md
Normal 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.
|
||||
64
README_BACKOFFICE_NATIVE_FRAMEWORK.md
Normal file
64
README_BACKOFFICE_NATIVE_FRAMEWORK.md
Normal 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.
|
||||
54
README_ENTERPRISE_TEMPLATE.md
Normal file
54
README_ENTERPRISE_TEMPLATE.md
Normal 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.
|
||||
184
README_MIGRACAO_BACKOFFICE.md
Normal file
184
README_MIGRACAO_BACKOFFICE.md
Normal 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
BIN
app/.DS_Store
vendored
Normal file
Binary file not shown.
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/identity_extraction.cpython-313.pyc
Normal file
BIN
app/__pycache__/identity_extraction.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-313.pyc
Normal file
BIN
app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/state.cpython-313.pyc
Normal file
BIN
app/__pycache__/state.cpython-313.pyc
Normal file
Binary file not shown.
15
app/agents/README.md
Normal file
15
app/agents/README.md
Normal 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.
|
||||
BIN
app/agents/__pycache__/backoffice_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/backoffice_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/billing_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/billing_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/orders_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/orders_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/product_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/product_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/prompting.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/prompting.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/runtime.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/runtime.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/agents/__pycache__/support_agent.cpython-313.pyc
Normal file
BIN
app/agents/__pycache__/support_agent.cpython-313.pyc
Normal file
Binary file not shown.
421
app/agents/backoffice_agent.py
Normal file
421
app/agents/backoffice_agent.py
Normal 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"
|
||||
70
app/agents/billing_agent.py
Normal file
70
app/agents/billing_agent.py
Normal 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)
|
||||
69
app/agents/orders_agent.py
Normal file
69
app/agents/orders_agent.py
Normal 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)
|
||||
70
app/agents/product_agent.py
Normal file
70
app/agents/product_agent.py
Normal 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
15
app/agents/prompting.py
Normal 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
267
app/agents/runtime.py
Normal 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
|
||||
67
app/agents/support_agent.py
Normal file
67
app/agents/support_agent.py
Normal 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
BIN
app/domain/.DS_Store
vendored
Normal file
Binary file not shown.
0
app/domain/__init__.py
Normal file
0
app/domain/__init__.py
Normal file
BIN
app/domain/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/domain/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
0
app/domain/backoffice/__init__.py
Normal file
0
app/domain/backoffice/__init__.py
Normal file
BIN
app/domain/backoffice/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/domain/backoffice/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
1
app/examples/__init__.py
Normal file
1
app/examples/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Exemplos de uso do template backend enterprise."""
|
||||
BIN
app/examples/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/examples/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/examples/__pycache__/grl_examples.cpython-313.pyc
Normal file
BIN
app/examples/__pycache__/grl_examples.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/examples/__pycache__/ic_examples.cpython-313.pyc
Normal file
BIN
app/examples/__pycache__/ic_examples.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/examples/__pycache__/mcp_examples.cpython-313.pyc
Normal file
BIN
app/examples/__pycache__/mcp_examples.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/examples/__pycache__/noc_examples.cpython-313.pyc
Normal file
BIN
app/examples/__pycache__/noc_examples.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/examples/__pycache__/observer_examples.cpython-313.pyc
Normal file
BIN
app/examples/__pycache__/observer_examples.cpython-313.pyc
Normal file
Binary file not shown.
37
app/examples/grl_examples.py
Normal file
37
app/examples/grl_examples.py
Normal 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",
|
||||
)
|
||||
34
app/examples/ic_examples.py
Normal file
34
app/examples/ic_examples.py
Normal 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",
|
||||
)
|
||||
43
app/examples/mcp_examples.py
Normal file
43
app/examples/mcp_examples.py
Normal 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
|
||||
37
app/examples/noc_examples.py
Normal file
37
app/examples/noc_examples.py
Normal 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",
|
||||
)
|
||||
28
app/examples/observer_examples.py
Normal file
28
app/examples/observer_examples.py
Normal 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",
|
||||
)
|
||||
90
app/identity_extraction.py
Normal file
90
app/identity_extraction.py
Normal 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
826
app/main.py
Normal 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
34
app/state.py
Normal 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
|
||||
BIN
app/workflows/__pycache__/agent_graph.cpython-313.pyc
Normal file
BIN
app/workflows/__pycache__/agent_graph.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
718
app/workflows/agent_graph.py
Normal file
718
app/workflows/agent_graph.py
Normal 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
|
||||
748
app/workflows/backoffice_native_runtime.py
Normal file
748
app/workflows/backoffice_native_runtime.py
Normal 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
BIN
config/.DS_Store
vendored
Normal file
Binary file not shown.
18
config/agents.yaml
Normal file
18
config/agents.yaml
Normal 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
BIN
config/agents/.DS_Store
vendored
Normal file
Binary file not shown.
54
config/agents/backoffice_anatel/guardrails.yaml
Normal file
54
config/agents/backoffice_anatel/guardrails.yaml
Normal 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
|
||||
20
config/agents/backoffice_anatel/judges.yaml
Normal file
20
config/agents/backoffice_anatel/judges.yaml
Normal 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.
|
||||
25
config/agents/backoffice_anatel/prompt_policy.yaml
Normal file
25
config/agents/backoffice_anatel/prompt_policy.yaml
Normal 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.
|
||||
8
config/agents/retail_orders/guardrails.yaml
Normal file
8
config/agents/retail_orders/guardrails.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
input:
|
||||
- code: MSK
|
||||
enabled: true
|
||||
- code: VLOOP
|
||||
enabled: true
|
||||
output:
|
||||
- code: REVPREC
|
||||
enabled: true
|
||||
7
config/agents/retail_orders/judges.yaml
Normal file
7
config/agents/retail_orders/judges.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
judges:
|
||||
- name: response_quality
|
||||
enabled: true
|
||||
threshold: 0.7
|
||||
- name: groundedness
|
||||
enabled: true
|
||||
threshold: 0.6
|
||||
6
config/agents/retail_orders/prompt_policy.yaml
Normal file
6
config/agents/retail_orders/prompt_policy.yaml
Normal 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.
|
||||
8
config/agents/telecom_contas/guardrails.yaml
Normal file
8
config/agents/telecom_contas/guardrails.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
input:
|
||||
- code: MSK
|
||||
enabled: true
|
||||
- code: VLOOP
|
||||
enabled: true
|
||||
output:
|
||||
- code: REVPREC
|
||||
enabled: true
|
||||
7
config/agents/telecom_contas/judges.yaml
Normal file
7
config/agents/telecom_contas/judges.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
judges:
|
||||
- name: response_quality
|
||||
enabled: true
|
||||
threshold: 0.7
|
||||
- name: groundedness
|
||||
enabled: true
|
||||
threshold: 0.6
|
||||
6
config/agents/telecom_contas/prompt_policy.yaml
Normal file
6
config/agents/telecom_contas/prompt_policy.yaml
Normal 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
8
config/guardrails.yaml
Normal 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
79
config/identity.yaml
Normal 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
7
config/judges.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
judges:
|
||||
- name: response_quality
|
||||
enabled: true
|
||||
threshold: 0.7
|
||||
- name: groundedness
|
||||
enabled: true
|
||||
threshold: 0.6
|
||||
51
config/mcp_parameter_mapping.yaml
Normal file
51
config/mcp_parameter_mapping.yaml
Normal 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
|
||||
6
config/mcp_servers.docker.yaml
Normal file
6
config/mcp_servers.docker.yaml
Normal 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
7
config/mcp_servers.yaml
Normal 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
19
config/prompt_policy.yaml
Normal 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
120
config/routing.yaml
Normal 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
62
config/tools.yaml
Normal 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 }
|
||||
4
configs_original_develop/README.md
Normal file
4
configs_original_develop/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Introduction
|
||||
<- Arquivos de configurações necessários (json,yalm, xml).
|
||||
|
||||
|
||||
32
configs_original_develop/config.example.yaml
Normal file
32
configs_original_develop/config.example.yaml
Normal 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
53
curl_test.txt
Normal 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
3
ddl/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Introduction
|
||||
DDLs para criação de tabelas ou views
|
||||
|
||||
108
ddl/anatel_notas.sql
Normal file
108
ddl/anatel_notas.sql
Normal 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)
|
||||
);
|
||||
14
ddl/create-collections.sql
Normal file
14
ddl/create-collections.sql
Normal 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
83
ddl/manual_anatel.sql
Normal 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
30
ddl/templates.sql
Normal 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
|
||||
);
|
||||
3
ddl_original_develop/README.md
Normal file
3
ddl_original_develop/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Introduction
|
||||
DDLs para criação de tabelas ou views
|
||||
|
||||
108
ddl_original_develop/anatel_notas.sql
Normal file
108
ddl_original_develop/anatel_notas.sql
Normal 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)
|
||||
);
|
||||
14
ddl_original_develop/create-collections.sql
Normal file
14
ddl_original_develop/create-collections.sql
Normal 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_original_develop/manual_anatel.sql
Normal file
83
ddl_original_develop/manual_anatel.sql
Normal 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_original_develop/templates.sql
Normal file
30
ddl_original_develop/templates.sql
Normal 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
28
docker-compose.yml
Normal 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"
|
||||
39
docs/AJUSTES_IC_NOC_GRL_OCI_OPENAI.md
Normal file
39
docs/AJUSTES_IC_NOC_GRL_OCI_OPENAI.md
Normal 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`
|
||||
95
docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md
Normal file
95
docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md
Normal 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.
|
||||
45
docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md
Normal file
45
docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md
Normal 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.
|
||||
88
docs/CORRECAO_ENV_MCP_TIM_CLIENTS.md
Normal file
88
docs/CORRECAO_ENV_MCP_TIM_CLIENTS.md
Normal 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
Reference in New Issue
Block a user