diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..7bc07ec
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# 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
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..89ee753
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..9ab6100
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/nemo_guardrails_configuration.iml b/.idea/nemo_guardrails_configuration.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/nemo_guardrails_configuration.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.oca/custom_code_review_guidelines.txt b/.oca/custom_code_review_guidelines.txt
new file mode 100644
index 0000000..a0a3b63
--- /dev/null
+++ b/.oca/custom_code_review_guidelines.txt
@@ -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.
+#
+# *Comment:
+# Category: Minor
+# Issue: Use streams instead of a loop for better readability.
+# Code Block:
+#
+# ```java
+# // Calculate squares of numbers
+# List squares = new ArrayList<>();
+# for (int number : numbers) {
+# squares.add(number * number);
+# }
+# ```
+# Recommendation:
+#
+# ```java
+# // Calculate squares of numbers
+# List squares = Arrays.stream(numbers)
+# .map(n -> n * n) // Map each number to its square
+# .toList();
+# ```
+#
diff --git a/README.md b/README.md
index 0451219..b283545 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,413 @@
-# nemo_guardrails_configuration
\ No newline at end of file
+# Tutorial: NeMo Guardrails com Python, Proxy OpenAI-Compatible e Tracing
+
+## 1. Objetivo
+
+Este tutorial orienta um time de desenvolvimento a implementar guardrails usando **NVIDIA NeMo Guardrails como biblioteca Python**, sem depender inicialmente do NeMo Server. A proposta é permitir uma adoção incremental: começar com rails determinísticos, rails LLM e regras Python dentro da aplicação, mantendo uma estrutura que possa evoluir futuramente para servidor, supervisor, judges batch e observabilidade completa.
+
+O projeto entregue junto com este tutorial foi gerado com base na planilha `Guardrails e Curadoria v3 - Consolidado.xlsx`, que define guardrails, curadoria, supervisores e LLM-as-a-judge.
+
+## 2. Conceitos principais
+
+### 2.1 Guardrail
+
+Guardrail é uma proteção aplicada ao fluxo de IA. Pode bloquear, mascarar, rejeitar, reescrever, auditar ou medir uma interação.
+
+Neste projeto, os guardrails foram separados em quatro famílias:
+
+| Família | Uso | Exemplo da planilha |
+|---|---|---|
+| NeMo / LLM rail | Avaliação semântica com LLM | Toxicidade, Out-of-Scope, Groundedness |
+| Regex rail | Regra determinística rápida | PII Masking |
+| Python rail | Regra de negócio determinística | Alçada de Ajuste, Histórico |
+| Supervisor / Judge | Auditoria pós-fluxo ou batch | Supervisor VAS Avulso, Qualidade, Alucinação |
+
+### 2.2 Input Rail
+
+Executa antes do LLM. Serve para proteger o modelo contra entrada tóxica, fora de escopo, dados sensíveis, jailbreak ou pedidos indevidos.
+
+Na planilha:
+
+- PII Masking
+- Toxicidade
+- Out-of-Scope
+
+### 2.3 Output Rail
+
+Executa depois que o LLM gera uma resposta, mas antes de devolver ao usuário ou executar uma ação.
+
+Na planilha:
+
+- Compliance Anatel
+- Verbalização Prematura
+- Groundedness
+
+### 2.4 Python pré-execução
+
+Nem toda regra deve ir para o LLM. Regras financeiras, alçada, duplicidade de crédito e consistência histórica devem ficar em Python ou em serviço de negócio.
+
+Exemplo:
+
+```python
+if valor_ajuste > limite:
+ escalar_para_ath()
+```
+
+### 2.5 Supervisor
+
+Supervisor é uma camada independente que audita o fluxo já executado. Ele não substitui guardrails. Ele verifica se a jornada foi correta.
+
+Na planilha:
+
+- Supervisor VAS Avulso
+- Avalia se o cancelamento foi feito corretamente
+- Retorna `CONFORME`, `SUSPEITO` ou `PROBLEMA`
+
+### 2.6 LLM-as-a-judge
+
+É uma avaliação normalmente batch, pós-sessão, para medir qualidade, tom, alucinação, satisfação e aderência à rubrica.
+
+Na planilha:
+
+- Sentimento CSI
+- Taxa de Alucinação
+- Qualidade da Resposta
+- Tom de Voz
+
+## 3. Arquitetura recomendada
+
+```text
+User Input
+ ↓
+Input Rails
+ ├─ Regex: PII Masking
+ ├─ LLM: Toxicidade
+ └─ LLM: Out-of-Scope
+ ↓
+LLM principal via NeMo Guardrails
+ ↓
+Output Rails
+ ├─ Compliance Anatel
+ ├─ Verbalização Prematura
+ └─ Groundedness
+ ↓
+Python Rules
+ ├─ Alçada de Ajuste
+ └─ Consistência Histórica
+ ↓
+Execução de API / Backend
+ ↓
+Supervisor VAS Avulso
+ ↓
+Curadoria / Métricas
+ ├─ TCR
+ ├─ Fallback
+ ├─ Tokens
+ ├─ Tamanho de mensagem
+ └─ Eficiência RAG
+ ↓
+Resposta final
+```
+
+## 4. Estrutura do projeto
+
+```text
+nemo_guardrails_tracing_project/
+├── config/
+│ ├── config.yml
+│ ├── rails.co
+│ └── guardrails_catalog.json
+├── src/
+│ ├── app.py
+│ ├── deterministic_rails.py
+│ ├── supervisor.py
+│ ├── curadoria.py
+│ ├── tracing.py
+│ └── settings.py
+├── tests/
+│ └── test_guardrails.py
+├── scripts/
+│ ├── run_demo.sh
+│ └── run_tests.sh
+├── requirements.txt
+├── .env.example
+└── README.md
+```
+
+## 5. Preparação do ambiente
+
+### 5.1 Criar ambiente Python
+
+```bash
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+### 5.2 Configurar variáveis
+
+```bash
+cp .env.example .env
+```
+
+Variáveis principais:
+
+```bash
+OPENAI_API_BASE=http://127.0.0.1:8051/v1
+OPENAI_BASE_URL=http://127.0.0.1:8051/v1
+OPENAI_API_KEY=dummy
+OPENAI_MODEL=gpt-4.1
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:6006/v1/traces
+ENABLE_TRACING=true
+USE_MOCK_LLM=false
+ALCADA_MAX_AJUSTE=50
+```
+
+>**Nota Importante:** O Nemo Guardrails possui compatibilidade apenas com a API da OpenAI. É possível utilizar-se de modelos que não tenham compatibilidade com API OpenAI, bastando para isso utilizar-se do proxy OCI OpenAI desta documentação: [https://github.com/hoshikawa2/nemo_guardrails_oci_generative_ai](https://github.com/hoshikawa2/nemo_guardrails_oci_generative_ai)
+
+## 6. Configuração do NeMo Guardrails
+
+Arquivo: `config/config.yml`
+
+```yaml
+models:
+ - type: main
+ engine: openai
+ model: ${OPENAI_MODEL}
+
+instructions:
+ - type: general
+ content: |
+ Você é um assistente de atendimento de telecom. Responda em português, com clareza,
+ sem prometer ajustes antes de validação, sem expor dados sensíveis e sem tratar temas fora do escopo.
+
+rails:
+ input:
+ flows:
+ - self check input
+ output:
+ flows:
+ - self check output
+```
+
+## 7. Rails criados a partir da planilha
+
+### 7.1 MSK — PII Masking
+
+Implementação: `src/deterministic_rails.py`
+
+```python
+CPF_RE = re.compile(r"\b\d{3}\.\d{3}\.\d{3}-\d{2}\b")
+CARD_RE = re.compile(r"\b(?:\d[ -]*?){13,16}\b")
+PASSWORD_RE = re.compile(r"(?i)(senha|password|token|api[_-]?key)\s*[:=]\s*\S+")
+```
+
+Objetivo: mascarar CPF, cartão, senha, token e credenciais antes de enviar ao LLM e antes de logar.
+
+### 7.2 CMP — Compliance Anatel
+
+Implementação: `enforce_compliance_anatel()`
+
+Regra: respostas de ajuste precisam conter número de protocolo.
+
+### 7.3 ADJ — Alçada de Ajuste
+
+Implementação: `validar_alcada()`
+
+Regra: se o valor do ajuste exceder `ALCADA_MAX_AJUSTE`, o fluxo é bloqueado e escalado para atendimento humano.
+
+### 7.4 REVPREC — Verbalização Prematura
+
+Implementação: `verbalizacao_prematura()`
+
+Regra: o agente não pode prometer ajuste, crédito ou cancelamento antes de validação.
+
+### 7.5 Supervisor VAS Avulso
+
+Implementação: `src/supervisor.py`
+
+Avalia cinco regras:
+
+1. O VAS cancelado corresponde ao item solicitado.
+2. Não houve promessa antes da validação.
+3. Resposta de ajuste contém protocolo.
+4. Não houve exposição de PII.
+5. A decisão está coerente com os dados de contexto.
+
+## 8. Integração com proxy OpenAI-Compatible
+
+O projeto usa o cliente `openai` apontando para seu proxy:
+
+```python
+client = OpenAI(
+ api_key=settings.openai_api_key,
+ base_url=settings.openai_api_base
+)
+```
+
+Configuração:
+
+```bash
+export OPENAI_API_BASE=http://127.0.0.1:8051/v1
+export OPENAI_BASE_URL=http://127.0.0.1:8051/v1
+export OPENAI_API_KEY=dummy
+export OPENAI_MODEL=gpt-4.1
+```
+
+## 9. Tracing com OpenTelemetry / Phoenix
+
+Arquivo: `src/tracing.py`
+
+Cada etapa cria spans:
+
+- `rail.input.msk`
+- `rail.python.alcada`
+- `llm.nemo.generate`
+- `rail.output.verbalizacao_prematura`
+- `rail.output.compliance_anatel`
+- `supervisor.vas_avulso`
+
+Configuração:
+
+```bash
+export ENABLE_TRACING=true
+export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:6006/v1/traces
+```
+
+## 10. Como executar o projeto
+
+### 10.1 Teste demonstrável sem proxy
+
+```bash
+export USE_MOCK_LLM=true
+export ENABLE_TRACING=false
+python -m src.app
+```
+
+Resultado esperado:
+
+```json
+{
+ "allowed": true,
+ "input_sanitized": "Cancelar VAS. CPF [CPF_MASCARADO]",
+ "output": "Cancelamento analisado. Ajuste elegível conforme validação. Protocolo nº 123456789.",
+ "metrics": {
+ "tcr": "CONCLUIDO"
+ }
+}
+```
+
+### 10.2 Executar testes automatizados
+
+```bash
+pytest -q
+```
+
+Os testes comprovam:
+
+- CPF é mascarado.
+- Alçada bloqueia valor acima do limite.
+- Compliance Anatel bloqueia ajuste sem protocolo.
+- Verbalização prematura é bloqueada.
+- Fluxo completo funciona em modo mock.
+
+### 10.3 Executar com proxy real
+
+```bash
+export USE_MOCK_LLM=false
+export OPENAI_API_BASE=http://127.0.0.1:8051/v1
+export OPENAI_BASE_URL=http://127.0.0.1:8051/v1
+export OPENAI_API_KEY=dummy
+export OPENAI_MODEL=gpt-4.1
+python -m src.app
+```
+
+
+
+```bash
+bash scripts/run_tests.sh
+```
+
+
+
+## 11. Mapeamento da planilha para implementação
+
+| Código | Item | Mecanismo | Implementação entregue |
+|---|---|---|---|
+| MSK | PII Masking | NeMo regex rail | Regex Python + pré-LLM |
+| CMP | Compliance Anatel | NeMo output rail | Output rail determinístico |
+| ADJ | Alçada de Ajuste | Python | Pré-execução Python |
+| REVPREC | Supervisor VAS Avulso | LLM Supervisor | `src/supervisor.py` |
+| TCR | Conclusão de Tarefa | Python | `src/curadoria.py` |
+| REVPREC | Verbalização Prematura | NeMo LLM rail | Output rail determinístico + expansível |
+| TOX | Toxicidade | NeMo LLM rail | `self_check_input` |
+| OOS | Out-of-Scope | NeMo LLM rail | `self_check_input` |
+| GND | Groundedness | NeMo LLM rail | `self_check_output`, com dependência de chunks RAG |
+| HIST | Consistência Histórica | Python | previsto como regra pré-execução |
+| CSI | Sentimento | LLM-as-a-judge | previsto como batch D-1 |
+| ALUC | Taxa de Alucinação | LLM-as-a-judge | previsto como batch D-1 |
+| RQLT | Qualidade da Resposta | LLM-as-a-judge | previsto como batch D-1 |
+| VCTN | Tom de Voz | LLM-as-a-judge | previsto como batch D-1 |
+
+## 12. Recomendações para o time
+
+### 12.1 Não colocar tudo no LLM
+
+Use Python para regras determinísticas e financeiras. Use LLM rails para semântica, linguagem, escopo e groundedness.
+
+### 12.2 Separar bloqueio de medição
+
+Guardrail bloqueia. Curadoria mede. Supervisor audita. Judge avalia em lote.
+
+### 12.3 Começar por P0
+
+Primeira entrega sugerida:
+
+1. PII Masking
+2. Compliance Anatel
+3. Alçada de Ajuste
+4. Supervisor VAS Avulso
+5. TCR
+6. Verbalização Prematura
+
+### 12.4 Evoluir para P1
+
+Depois:
+
+1. Toxicidade
+2. Out-of-Scope
+3. Groundedness
+4. Histórico
+5. Tokens
+6. Eficiência NLU
+7. No-Match RAG
+
+## 13. Critérios de aceite
+
+O time deve comprovar:
+
+- Dados sensíveis não chegam ao LLM sem máscara.
+- Ajuste acima da alçada não é executado.
+- Resposta de ajuste sem protocolo é bloqueada.
+- Promessa antes da validação é bloqueada.
+- Supervisor retorna status estruturado.
+- Métricas de curadoria são geradas.
+- Spans aparecem no backend de tracing.
+
+## 14. Evolução futura
+
+A estrutura permite evoluir para:
+
+- NeMo Server.
+- LangGraph.
+- MCP tools.
+- RAG com groundedness por chunk.
+- Batch judges D-1.
+- Phoenix / Langfuse / OpenTelemetry.
+- Governança por catálogo de guardrails.
+
+## 15. Referências
+
+- NVIDIA NeMo Guardrails: documentação oficial.
+- Python SDK: `RailsConfig.from_path()` e `LLMRails.generate()`.
+- Colang: linguagem para definir fluxos de guardrails.
+- OpenTelemetry: tracing distribuído por spans.
diff --git a/img.png b/img.png
new file mode 100644
index 0000000..ab46e49
Binary files /dev/null and b/img.png differ
diff --git a/img_1.png b/img_1.png
new file mode 100644
index 0000000..17d94aa
Binary files /dev/null and b/img_1.png differ
diff --git a/nemo_guardrails_tracing_project/config/config.yml b/nemo_guardrails_tracing_project/config/config.yml
new file mode 100644
index 0000000..acb6c37
--- /dev/null
+++ b/nemo_guardrails_tracing_project/config/config.yml
@@ -0,0 +1,9 @@
+models:
+ - type: main
+ engine: openai
+ model: gpt-4.1
+rails:
+ input:
+ flows: [check toxicidade, check out of scope]
+ output:
+ flows: [check verbalizacao prematura, check groundedness]
diff --git a/nemo_guardrails_tracing_project/config/guardrails.yaml b/nemo_guardrails_tracing_project/config/guardrails.yaml
new file mode 100644
index 0000000..5dd000f
--- /dev/null
+++ b/nemo_guardrails_tracing_project/config/guardrails.yaml
@@ -0,0 +1,24 @@
+guardrails:
+ - {codigo: MSK, item: "PII Masking", mecanismo: regex}
+ - {codigo: CMP, item: "Compliance Anatel", mecanismo: regex}
+ - {codigo: ADJ, item: "Alçada de Ajuste", mecanismo: python}
+ - {codigo: REVPREC_SUP, item: "Supervisor VAS Avulso", mecanismo: llm_supervisor}
+ - {codigo: TCR, item: "Conclusão de Tarefa", mecanismo: python}
+ - {codigo: REVPREC, item: "Verbalização Prematura", mecanismo: llm_rail}
+ - {codigo: FALLBACK, item: "Fallback Não Entendi", mecanismo: python}
+ - {codigo: VIOL, item: "Violação de Guardrails", mecanismo: python}
+ - {codigo: TOX, item: "Toxicidade", mecanismo: llm_rail}
+ - {codigo: OOS, item: "Out-of-Scope", mecanismo: llm_rail}
+ - {codigo: GND, item: "Groundedness", mecanismo: llm_rail}
+ - {codigo: HIST, item: "Consistência Histórica", mecanismo: python}
+ - {codigo: PMPTK, item: "Prompt Tokens", mecanismo: python}
+ - {codigo: EFIC, item: "Eficiência NLU", mecanismo: python}
+ - {codigo: NO-M, item: "No-Match RAG", mecanismo: python}
+ - {codigo: CSI, item: "Sentimento", mecanismo: llm_judge}
+ - {codigo: ALUC, item: "Taxa de Alucinação", mecanismo: llm_judge}
+ - {codigo: VLOOP, item: "Volume de Loops", mecanismo: python}
+ - {codigo: MSIZE, item: "Tamanho de Mensagem", mecanismo: python}
+ - {codigo: RQLT, item: "Qualidade da Resposta", mecanismo: llm_judge}
+ - {codigo: VCTN, item: "Tom de Voz", mecanismo: llm_judge}
+ - {codigo: REVPREC_METRIC, item: "Precisão e Revocação", mecanismo: python}
+ - {codigo: SEMAC, item: "Acurácia Semântica STT", mecanismo: python}
diff --git a/nemo_guardrails_tracing_project/config/rails/input.co b/nemo_guardrails_tracing_project/config/rails/input.co
new file mode 100644
index 0000000..1be773d
--- /dev/null
+++ b/nemo_guardrails_tracing_project/config/rails/input.co
@@ -0,0 +1,5 @@
+define flow check toxicidade
+ pass
+
+define flow check out of scope
+ pass
diff --git a/nemo_guardrails_tracing_project/config/rails/output.co b/nemo_guardrails_tracing_project/config/rails/output.co
new file mode 100644
index 0000000..25e978b
--- /dev/null
+++ b/nemo_guardrails_tracing_project/config/rails/output.co
@@ -0,0 +1,5 @@
+define flow check verbalizacao prematura
+ pass
+
+define flow check groundedness
+ pass
diff --git a/nemo_guardrails_tracing_project/env.variables b/nemo_guardrails_tracing_project/env.variables
new file mode 100644
index 0000000..ba30f80
--- /dev/null
+++ b/nemo_guardrails_tracing_project/env.variables
@@ -0,0 +1,14 @@
+# Proxy OpenAI-compatible, por exemplo seu proxy OCI
+OPENAI_API_BASE=http://127.0.0.1:8051/v1
+OPENAI_BASE_URL=http://127.0.0.1:8051/v1
+OPENAI_API_KEY=dummy
+OPENAI_MODEL=gpt-4.1
+
+# Tracing / Phoenix / OpenTelemetry
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:6006/v1/traces
+OTEL_SERVICE_NAME=nemo-guardrails-demo
+ENABLE_TRACING=false
+
+# Modo demo: usa cliente fake se o proxy não estiver disponível
+USE_MOCK_LLM=false
+ALCADA_MAX_AJUSTE=50
diff --git a/nemo_guardrails_tracing_project/requirements.txt b/nemo_guardrails_tracing_project/requirements.txt
new file mode 100644
index 0000000..99e9e03
--- /dev/null
+++ b/nemo_guardrails_tracing_project/requirements.txt
@@ -0,0 +1,7 @@
+pytest>=8.0.0
+pyyaml>=6.0.1
+openai>=1.0.0
+nemoguardrails>=0.9.0
+opentelemetry-api>=1.20.0
+opentelemetry-sdk>=1.20.0
+opentelemetry-exporter-otlp>=1.20.0
diff --git a/nemo_guardrails_tracing_project/scripts/run_tests.sh b/nemo_guardrails_tracing_project/scripts/run_tests.sh
new file mode 100644
index 0000000..b28019d
--- /dev/null
+++ b/nemo_guardrails_tracing_project/scripts/run_tests.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")/.."
+export PYTHONPATH="$(pwd)"
+export USE_MOCK_LLM="${USE_MOCK_LLM:-true}"
+echo "PYTHONPATH=$PYTHONPATH"
+echo "USE_MOCK_LLM=$USE_MOCK_LLM"
+pytest -v -s tests/
diff --git a/nemo_guardrails_tracing_project/src/__init__.py b/nemo_guardrails_tracing_project/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nemo_guardrails_tracing_project/src/app.py b/nemo_guardrails_tracing_project/src/app.py
new file mode 100644
index 0000000..900d4b6
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/app.py
@@ -0,0 +1,122 @@
+from .deterministic_rails import mask_pii, validar_alcada, enforce_compliance_anatel
+from .llm_rails import detectar_toxicidade, detectar_out_of_scope, validar_groundedness, verbalizacao_prematura, supervisor_vas_avulso
+from .judges import avaliar_qualidade_resposta
+import json
+
+
+def executar_atendimento(user_input: str, context: dict):
+ steps = []
+
+ # INPUT RAILS
+ r = mask_pii(user_input)
+ steps.append(r)
+ text = r.sanitized_text or user_input
+
+ for rail in [detectar_toxicidade, detectar_out_of_scope]:
+ r = rail(text)
+ steps.append(r)
+ if not r.allowed:
+ return {
+ "allowed": False,
+ "blocked_by": r.code,
+ "steps": steps
+ }
+
+ # PYTHON RULE
+ if "ajuste_valor" in context:
+ r = validar_alcada(context["ajuste_valor"])
+ steps.append(r)
+ if not r.allowed:
+ return {
+ "allowed": False,
+ "blocked_by": r.code,
+ "steps": steps
+ }
+
+ # LLM RESPONSE (simulada ou real)
+ resposta = context.get("resposta_llm", "Resposta simulada do agente.")
+
+ # OUTPUT + JUDGES + SUPERVISOR
+ for r in [
+ verbalizacao_prematura(resposta, context),
+ enforce_compliance_anatel(resposta, context),
+ validar_groundedness(resposta, context),
+ avaliar_qualidade_resposta(user_input, resposta),
+ supervisor_vas_avulso(context.get("supervisor_payload", {}))
+ ]:
+ steps.append(r)
+
+ return {
+ "allowed": all(s.allowed for s in steps if s.code != "RQLT"),
+ "response": resposta,
+ "steps": steps
+ }
+
+
+# =========================
+# 🔥 EXECUÇÃO DIRETA
+# =========================
+def print_result(result):
+ print("\n" + "=" * 80)
+ print("📊 RESULTADO FINAL")
+ print("=" * 80)
+
+ print(f"✔ Allowed: {result['allowed']}")
+ print(f"💬 Response: {result.get('response')}")
+
+ if not result["allowed"]:
+ print(f"🚫 Bloqueado por: {result.get('blocked_by')}")
+
+ print("\n🔎 STEPS:")
+ for s in result["steps"]:
+ print("-" * 60)
+ print(f"🧩 Code: {s.code}")
+ print(f"⚙️ Mechanism: {s.mechanism}")
+ print(f"✔ Allowed: {s.allowed}")
+ print(f"📝 Reason: {s.reason}")
+ if s.sanitized_text:
+ print(f"🔐 Sanitized: {s.sanitized_text}")
+ if s.data:
+ print(f"📦 Data: {s.data}")
+
+ print("=" * 80)
+
+ # JSON final (para integração)
+ print("\n📦 JSON OUTPUT:")
+ print(json.dumps({
+ "allowed": result["allowed"],
+ "response": result.get("response"),
+ "steps": [
+ {
+ "code": s.code,
+ "allowed": s.allowed,
+ "reason": s.reason,
+ "mechanism": s.mechanism,
+ "data": s.data
+ }
+ for s in result["steps"]
+ ]
+ }, indent=2, ensure_ascii=False))
+
+
+if __name__ == "__main__":
+ # 🔧 EXEMPLO DE EXECUÇÃO
+
+ user_input = "Meu CPF é 123.456.789-00 e quero ajuste de 20 reais"
+
+ context = {
+ "ajuste_valor": 20,
+ "ajuste_validado": True,
+ "tipo_fluxo": "ajuste",
+ "requer_protocolo": True,
+ "resposta_llm": "Ajuste realizado. Protocolo: 202604270001.",
+ "chunks_rag": ["serviço fatura cobrança ajuste realizado protocolo"],
+ "supervisor_payload": {
+ "cancelamento_correto": True,
+ "servico_cancelado": "VAS Avulso",
+ "servico_solicitado": "VAS Avulso"
+ }
+ }
+
+ result = executar_atendimento(user_input, context)
+ print_result(result)
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/deterministic_rails.py b/nemo_guardrails_tracing_project/src/deterministic_rails.py
new file mode 100644
index 0000000..cd00e1b
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/deterministic_rails.py
@@ -0,0 +1,66 @@
+import re
+from collections import Counter
+from .models import RailResult
+from .tracing import span
+
+def mask_pii(text:str)->RailResult:
+ with span('rail.MSK', mechanism='regex'):
+ original=text
+ text=re.sub(r'\b\d{3}\.\d{3}\.\d{3}-\d{2}\b','[CPF_MASCARADO]',text)
+ text=re.sub(r'\b\d{16}\b','[CARTAO_MASCARADO]',text)
+ text=re.sub(r'(?i)(senha\s*[:=]?\s*)\S+',r'\1[SENHA_MASCARADA]',text)
+ return RailResult(True,'PII mascarada' if text!=original else 'Nenhuma PII detectada',text,'MSK','regex')
+
+def enforce_compliance_anatel(text:str, context:dict)->RailResult:
+ with span('rail.CMP', mechanism='regex'):
+ requer=context.get('tipo_fluxo')=='ajuste' or context.get('requer_protocolo') is True
+ if not requer: return RailResult(True,'Compliance Anatel não aplicável',text,'CMP','regex')
+ has_protocol=bool(re.search(r'(?i)\bprotocolo\b[:\s-]*\d{6,}',text))
+ if not has_protocol: return RailResult(False,'Resposta de ajuste sem número de protocolo',text,'CMP','regex')
+ return RailResult(True,'Resposta contém protocolo obrigatório',text,'CMP','regex')
+
+def validar_alcada(valor:float, limite:float=50.0)->RailResult:
+ with span('rail.ADJ', mechanism='python'):
+ if valor>limite: return RailResult(False,f'Valor R$ {valor:.2f} excede alçada de R$ {limite:.2f}; escalar para ATH',code='ADJ',mechanism='python')
+ return RailResult(True,f'Valor R$ {valor:.2f} dentro da alçada',code='ADJ',mechanism='python')
+
+def calcular_tcr(status:str)->RailResult:
+ status=status.lower(); categoria='Indefinido'
+ if status in ['concluido','concluído','resolvido']: categoria='Concluído'
+ elif status in ['abandonado','timeout','desistencia']: categoria='Abandonado'
+ elif status in ['escalado','ath','humano']: categoria='Escalado'
+ return RailResult(True,f'TCR classificado como {categoria}',code='TCR',mechanism='python',data={'categoria':categoria})
+
+def detectar_fallback(text:str)->RailResult:
+ frases=['não entendi','não consegui entender','não tenho informação','não encontrei informação']; detected=any(f in text.lower() for f in frases)
+ return RailResult(True,'Fallback detectado' if detected else 'Fallback não detectado',text,'FALLBACK','python',{'fallback':detected})
+
+def registrar_violacao(agent_id:str, code:str)->RailResult:
+ return RailResult(True,'Violação registrada para agregação',code='VIOL',mechanism='python',data={'agent_id':agent_id,'violation_code':code,'count':1})
+
+def validar_consistencia_historica(context:dict)->RailResult:
+ if context.get('contestacao_anterior')=='procedente_confirmada': return RailResult(False,'Fatura já confirmada como procedente anteriormente',code='HIST',mechanism='python')
+ return RailResult(True,'Sem conflito histórico',code='HIST',mechanism='python')
+
+def contabilizar_tokens(prompt_tokens:int, completion_tokens:int)->RailResult:
+ total=prompt_tokens+completion_tokens; return RailResult(True,'Tokens contabilizados',code='PMPTK',mechanism='python',data={'prompt_tokens':prompt_tokens,'completion_tokens':completion_tokens,'total_tokens':total})
+
+def calcular_eficiencia_nlu(chunks_retornados:int, chunks_utilizados:int)->RailResult:
+ eficiencia=chunks_utilizados/chunks_retornados if chunks_retornados else 0; return RailResult(True,'Eficiência NLU calculada',code='EFIC',mechanism='python',data={'eficiencia':eficiencia})
+
+def detectar_no_match_rag(chunks:list, resposta:str)->RailResult:
+ no_match=not chunks or 'não encontrei' in resposta.lower(); return RailResult(True,'No-Match RAG detectado' if no_match else 'RAG retornou evidência útil',code='NO-M',mechanism='python',data={'no_match':no_match})
+
+def detectar_loop(mensagens:list[str])->RailResult:
+ counts=Counter(mensagens); loop=any(v>=2 for v in counts.values()); return RailResult(True,'Loop detectado' if loop else 'Sem loop',code='VLOOP',mechanism='python',data={'loop':loop})
+
+def medir_tamanho_mensagem(text:str)->RailResult:
+ return RailResult(True,'Tamanho de mensagem medido',text,'MSIZE','python',{'chars':len(text)})
+
+def calcular_precisao_revocacao(y_true:list[str], y_pred:list[str])->RailResult:
+ total=len(y_true); correct=sum(1 for a,b in zip(y_true,y_pred) if a==b); accuracy=correct/total if total else 0
+ return RailResult(True,'Acurácia de roteamento calculada',code='REVPREC_METRIC',mechanism='python',data={'accuracy':accuracy})
+
+def avaliar_acuracia_semantica(audio_transcrito:str, referencia_humana:str)->RailResult:
+ a=set(audio_transcrito.lower().split()); b=set(referencia_humana.lower().split()); score=len(a & b)/len(b) if b else 0
+ return RailResult(score>=0.85,f'Acurácia semântica STT: {score:.2f}',code='SEMAC',mechanism='python',data={'score':score})
diff --git a/nemo_guardrails_tracing_project/src/judges.py b/nemo_guardrails_tracing_project/src/judges.py
new file mode 100644
index 0000000..bb1e2d8
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/judges.py
@@ -0,0 +1,16 @@
+from .models import RailResult
+from .llm_client import LLMClient
+from .tracing import span
+_client=LLMClient()
+def classificar_sentimento(text:str)->RailResult:
+ with span("judge.CSI", mechanism="llm_judge"):
+ out=_client.classify("CSI", {"text":text}); return RailResult(True,out.get("reason",""),text,"CSI","llm_judge",{"sentimento":out.get("label"),**out})
+def avaliar_alucinacao(resposta:str, dados_reais:str)->RailResult:
+ with span("judge.ALUC", mechanism="llm_judge"):
+ out=_client.classify("ALUC", {"resposta":resposta,"dados_reais":dados_reais}); return RailResult(out["allowed"],out.get("reason",""),resposta,"ALUC","llm_judge",{"alucinacao":out.get("label")=="ALUCINACAO",**out})
+def avaliar_qualidade_resposta(pergunta:str, resposta:str)->RailResult:
+ with span("judge.RQLT", mechanism="llm_judge"):
+ out=_client.classify("RQLT", {"pergunta":pergunta,"resposta":resposta}); return RailResult(True,out.get("reason",""),resposta,"RQLT","llm_judge",out)
+def avaliar_tom_de_voz(text:str)->RailResult:
+ with span("judge.VCTN", mechanism="llm_judge"):
+ out=_client.classify("VCTN", {"text":text}); return RailResult(out["allowed"],out.get("reason",""),text,"VCTN","llm_judge",{"aderente":out["allowed"],**out})
diff --git a/nemo_guardrails_tracing_project/src/llm_client.py b/nemo_guardrails_tracing_project/src/llm_client.py
new file mode 100644
index 0000000..4e41702
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/llm_client.py
@@ -0,0 +1,106 @@
+import os, json
+from openai import OpenAI
+from src.prompts.revprec import build_revprec_prompt
+from src.prompts.csi import build_csi_prompt
+from src.prompts.vctn import build_vctn_prompt
+from src.prompts.tox import build_tox_prompt
+from src.prompts.oos import build_oos_prompt
+from src.prompts.gnd import build_gnd_prompt
+from src.prompts.aluc import build_aluc_prompt
+from src.prompts.rqlt import build_rqlt_prompt
+from src.prompts.supervisor import build_supervisor_prompt
+
+class LLMClient:
+ def __init__(self):
+ self.use_mock=os.getenv('USE_MOCK_LLM','true').lower()=='true'
+ self.model=os.getenv('OPENAI_MODEL','gpt-4.1')
+ self.client=None if self.use_mock else OpenAI(base_url=os.getenv('OPENAI_BASE_URL','http://localhost:8051/v1'), api_key=os.getenv('OPENAI_API_KEY','dummy'))
+
+ def classify(self, task, payload):
+
+ if self.use_mock:
+ return self._mock_classify(task, payload)
+
+ # ========================
+ # ROUTING DE PROMPTS
+ # ========================
+ if task == "REVPREC":
+ prompt = build_revprec_prompt(payload["text"], payload.get("context", {}))
+
+ elif task == "CSI":
+ prompt = build_csi_prompt(payload["text"])
+
+ elif task == "VCTN":
+ prompt = build_vctn_prompt(payload["text"])
+
+ elif task == "TOX":
+ prompt = build_tox_prompt(payload["text"])
+
+ elif task == "OOS":
+ prompt = build_oos_prompt(payload["text"])
+
+ elif task == "GND":
+ prompt = build_gnd_prompt(payload["resposta"], payload.get("context", {}))
+
+ # ========================
+ # 🔥 NOVOS (faltavam)
+ # ========================
+ elif task == "ALUC":
+ prompt = build_aluc_prompt(payload["resposta"], payload["dados_reais"])
+
+ elif task == "RQLT":
+ prompt = build_rqlt_prompt(payload["pergunta"], payload["resposta"])
+
+ elif task == "SUPERVISOR_VAS":
+ prompt = build_supervisor_prompt(payload)
+
+ else:
+ raise ValueError(f"Task não suportada: {task}")
+
+ # ========================
+ # CALL LLM
+ # ========================
+ response = self.client.chat.completions.create(
+ model=self.model,
+ messages=[{"role": "user", "content": prompt}],
+ temperature=0
+ )
+
+ import json
+ text = response.choices[0].message.content
+
+ try:
+ return json.loads(text)
+ except:
+ return {
+ "allowed": False,
+ "label": "ERROR",
+ "reason": text
+ }
+
+ def _mock(self, task, payload):
+ text=(payload.get('text') or payload.get('resposta') or payload.get('answer') or '').lower()
+ if task=='TOX':
+ bad=any(w in text for w in ['idiota','burro','lixo','inútil','ofensivo']); return {'allowed':not bad,'label':'TOXICO' if bad else 'NORMAL','reason':'mock TOX','score':0 if bad else 10}
+ if task=='OOS':
+ bad=any(w in text for w in ['política','religião','presidente','concorrente','vivo','claro']); return {'allowed':not bad,'label':'OUT_OF_SCOPE' if bad else 'IN_SCOPE','reason':'mock OOS','score':0 if bad else 10}
+ if task=='REVPREC':
+ validated=payload.get('context',{}).get('ajuste_validado',False); premature=any(w in text for w in ['já fiz','já realizei','foi realizado','ajuste aplicado','cancelamento realizado'])
+ return {'allowed':not(premature and not validated),'label':'PREMATURA' if premature and not validated else 'OK','reason':'mock REVPREC','score':0 if premature and not validated else 10}
+ if task=='GND':
+ chunks=' '.join(payload.get('context',{}).get('chunks_rag',[])).lower(); overlap=len(set(text.split()) & set(chunks.split())); ok=overlap>=3
+ return {'allowed':ok,'label':'GROUNDED' if ok else 'UNGROUNDED','reason':f'mock GND overlap={overlap}','score':min(10,overlap)}
+ if task=='CSI':
+ if any(w in text for w in ['insatisfeito','raiva','péssimo','cancelar']): return {'allowed':True,'label':'Negativo','reason':'mock CSI','score':3}
+ if any(w in text for w in ['obrigado','ótimo','resolvido','satisfeito']): return {'allowed':True,'label':'Positivo','reason':'mock CSI','score':9}
+ return {'allowed':True,'label':'Neutro','reason':'mock CSI','score':6}
+ if task=='ALUC':
+ overlap=len(set(payload.get('resposta','').lower().split()) & set(payload.get('dados_reais','').lower().split())); hallucinated=overlap<2
+ return {'allowed':not hallucinated,'label':'ALUCINACAO' if hallucinated else 'OK','reason':f'mock ALUC overlap={overlap}','score':0 if hallucinated else 8}
+ if task=='RQLT':
+ resposta=payload.get('resposta',''); score=8 if len(resposta)>30 else 3; return {'allowed':True,'label':'QUALIDADE','reason':'mock RQLT','score':score}
+ if task=='VCTN':
+ bad=any(w in text for w in ['se vira','problema seu','não posso fazer nada']); return {'allowed':not bad,'label':'TOM_INADEQUADO' if bad else 'TOM_OK','reason':'mock VCTN','score':0 if bad else 9}
+ if task=='SUPERVISOR_VAS':
+ ok=payload.get('cancelamento_correto',False) and payload.get('servico_cancelado')==payload.get('servico_solicitado'); return {'allowed':ok,'label':'CONFORME' if ok else 'PROBLEMA','reason':'mock supervisor','score':10 if ok else 0}
+ return {'allowed':True,'label':'OK','reason':'mock default','score':5}
diff --git a/nemo_guardrails_tracing_project/src/llm_rails.py b/nemo_guardrails_tracing_project/src/llm_rails.py
new file mode 100644
index 0000000..40ffe4c
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/llm_rails.py
@@ -0,0 +1,20 @@
+from .models import RailResult
+from .llm_client import LLMClient
+from .tracing import span
+_client=LLMClient()
+def detectar_toxicidade(text:str)->RailResult:
+ with span("rail.TOX", mechanism="llm_rail"):
+ out=_client.classify("TOX", {"text":text}); return RailResult(out["allowed"],out.get("reason",""),text,"TOX","llm_rail",out)
+def detectar_out_of_scope(text:str)->RailResult:
+ with span("rail.OOS", mechanism="llm_rail"):
+ out=_client.classify("OOS", {"text":text}); return RailResult(out["allowed"],out.get("reason",""),text,"OOS","llm_rail",out)
+def verbalizacao_prematura(text:str, context:dict)->RailResult:
+ with span("rail.REVPREC", mechanism="llm_rail"):
+ out=_client.classify("REVPREC", {"text":text,"context":context}); return RailResult(out["allowed"],out.get("reason",""),text,"REVPREC","llm_rail",out)
+
+def validar_groundedness(resposta:str, context:dict)->RailResult:
+ with span("rail.GND", mechanism="llm_rail"):
+ out=_client.classify("GND", {"resposta":resposta,"context":context}); return RailResult(out["allowed"],out.get("reason",""),resposta,"GND","llm_rail",out)
+def supervisor_vas_avulso(payload:dict)->RailResult:
+ with span("supervisor.REVPREC_SUP", mechanism="llm_supervisor"):
+ out=_client.classify("SUPERVISOR_VAS", payload); return RailResult(out["allowed"],out.get("reason",""),code="REVPREC_SUP",mechanism="llm_supervisor",data=out)
diff --git a/nemo_guardrails_tracing_project/src/models.py b/nemo_guardrails_tracing_project/src/models.py
new file mode 100644
index 0000000..e7ca12f
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/models.py
@@ -0,0 +1,10 @@
+from dataclasses import dataclass
+from typing import Any
+@dataclass
+class RailResult:
+ allowed: bool
+ reason: str
+ sanitized_text: str | None = None
+ code: str | None = None
+ mechanism: str | None = None
+ data: dict[str, Any] | None = None
diff --git a/nemo_guardrails_tracing_project/src/prompts/__init__.py b/nemo_guardrails_tracing_project/src/prompts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nemo_guardrails_tracing_project/src/prompts/aluc.py b/nemo_guardrails_tracing_project/src/prompts/aluc.py
new file mode 100644
index 0000000..0d1af8e
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/aluc.py
@@ -0,0 +1,30 @@
+def build_aluc_prompt(resposta, dados):
+ return f"""
+Você é um auditor de consistência de respostas.
+
+Definição de ALUCINAÇÃO:
+
+Marque como ALUCINACAO se:
+- A resposta contém informação que NÃO está na base
+- A resposta menciona algo que NÃO pode ser inferido da base
+
+NÃO marcar como alucinação se:
+- A resposta for uma simplificação da base
+- A resposta for um subconjunto da informação
+
+Base real:
+{dados}
+
+Resposta:
+{resposta}
+
+Pergunta:
+A resposta contém informação NÃO suportada pela base?
+
+Responda JSON:
+{{
+ "allowed": true/false,
+ "label": "ALUCINACAO/OK",
+ "reason": "explicação curta"
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/prompts/csi.py b/nemo_guardrails_tracing_project/src/prompts/csi.py
new file mode 100644
index 0000000..2af5a1f
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/csi.py
@@ -0,0 +1,18 @@
+def build_csi_prompt(text):
+ return f"""
+Classifique o sentimento do cliente.
+
+Texto:
+{text}
+
+Opções:
+- Positivo
+- Neutro
+- Negativo
+
+Responda JSON:
+{{
+ "sentimento": "Positivo/Neutro/Negativo",
+ "score": 0-10
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/prompts/gnd.py b/nemo_guardrails_tracing_project/src/prompts/gnd.py
new file mode 100644
index 0000000..d738ab7
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/gnd.py
@@ -0,0 +1,16 @@
+def build_gnd_prompt(resposta, context):
+ return f"""
+Verifique se a resposta está fundamentada.
+
+Resposta:
+{resposta}
+
+Base:
+{context.get("chunks_rag", [])}
+
+Responda:
+{{
+ "allowed": true/false,
+ "label": "GROUNDED/UNGROUNDED"
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/prompts/oos.py b/nemo_guardrails_tracing_project/src/prompts/oos.py
new file mode 100644
index 0000000..04bb216
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/oos.py
@@ -0,0 +1,18 @@
+def build_oos_prompt(text):
+ return f"""
+Verifique se o texto está fora do escopo de Telecom.
+
+Texto:
+{text}
+
+Fora de escopo:
+- política
+- religião
+- concorrentes
+
+Responda JSON:
+{{
+ "allowed": true/false,
+ "label": "IN_SCOPE/OUT_OF_SCOPE"
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/prompts/revprec.py b/nemo_guardrails_tracing_project/src/prompts/revprec.py
new file mode 100644
index 0000000..d123cbd
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/revprec.py
@@ -0,0 +1,24 @@
+def build_revprec_prompt(text, context):
+ return f"""
+Você é um auditor de atendimento ao cliente.
+
+Regra:
+- O agente NÃO pode afirmar que executou uma ação
+- SE essa ação NÃO foi validada previamente
+
+Contexto:
+ajuste_validado = {context.get("ajuste_validado")}
+
+Texto do agente:
+{text}
+
+Pergunta:
+O agente cometeu verbalização prematura?
+
+Responda JSON:
+{{
+ "allowed": true/false,
+ "label": "OK" ou "PREMATURA",
+ "reason": "explicação curta"
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/prompts/rqlt.py b/nemo_guardrails_tracing_project/src/prompts/rqlt.py
new file mode 100644
index 0000000..f4d4cc8
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/rqlt.py
@@ -0,0 +1,31 @@
+def build_rqlt_prompt(pergunta, resposta):
+ return f"""
+Você é um avaliador de qualidade de respostas de atendimento.
+
+Pergunta:
+{pergunta}
+
+Resposta:
+{resposta}
+
+Critérios:
+
+1. Clareza (0-3)
+2. Completude (0-3)
+3. Utilidade (0-4)
+
+Regras IMPORTANTES:
+
+- Se a resposta explica corretamente o motivo → score mínimo 6
+- Se a resposta é clara e útil → score entre 7 e 9
+- Se a resposta é vaga ("não sei", "verifique") → score < 5
+- NÃO penalizar respostas curtas se estiverem corretas
+
+Agora avalie.
+
+Responda APENAS JSON:
+{{
+ "score": 0-10,
+ "reason": "explicação curta"
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/prompts/supervisor.py b/nemo_guardrails_tracing_project/src/prompts/supervisor.py
new file mode 100644
index 0000000..7e56b95
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/supervisor.py
@@ -0,0 +1,21 @@
+def build_supervisor_prompt(payload):
+ return f"""
+Você é um auditor de atendimento.
+
+Verifique se o cancelamento foi correto.
+
+Solicitado:
+{payload.get("servico_solicitado")}
+
+Cancelado:
+{payload.get("servico_cancelado")}
+
+Se forem diferentes → PROBLEMA
+
+Responda JSON:
+{{
+ "allowed": true/false,
+ "label": "CONFORME/PROBLEMA",
+ "reason": "explicação"
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/prompts/tox.py b/nemo_guardrails_tracing_project/src/prompts/tox.py
new file mode 100644
index 0000000..05910be
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/tox.py
@@ -0,0 +1,17 @@
+def build_tox_prompt(text):
+ return f"""
+Classifique o texto abaixo:
+
+Texto:
+{text}
+
+Classifique como:
+- TOXICO
+- NORMAL
+
+Responda JSON:
+{{
+ "allowed": true/false,
+ "label": "TOXICO/NORMAL"
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/prompts/vctn.py b/nemo_guardrails_tracing_project/src/prompts/vctn.py
new file mode 100644
index 0000000..a20ef79
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/prompts/vctn.py
@@ -0,0 +1,22 @@
+def build_vctn_prompt(text):
+ return f"""
+Avalie o tom de voz do agente.
+
+Regra:
+- Deve ser educado
+- Não pode ser rude ou agressivo
+
+Texto:
+{text}
+
+Classifique:
+- Adequado
+- Inadequado
+
+Responda JSON:
+{{
+ "allowed": true/false,
+ "label": "Adequado/Inadequado",
+ "reason": "explicação"
+}}
+"""
\ No newline at end of file
diff --git a/nemo_guardrails_tracing_project/src/registry.py b/nemo_guardrails_tracing_project/src/registry.py
new file mode 100644
index 0000000..c7b654b
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/registry.py
@@ -0,0 +1,5 @@
+from pathlib import Path
+import yaml
+def load_guardrail_registry(path=None):
+ if path is None: path=Path(__file__).resolve().parent.parent/'config'/'guardrails.yaml'
+ with open(path,'r',encoding='utf-8') as f: return yaml.safe_load(f)['guardrails']
diff --git a/nemo_guardrails_tracing_project/src/tracing.py b/nemo_guardrails_tracing_project/src/tracing.py
new file mode 100644
index 0000000..1ec7a3e
--- /dev/null
+++ b/nemo_guardrails_tracing_project/src/tracing.py
@@ -0,0 +1,21 @@
+import os
+from contextlib import contextmanager
+_TRACING_ENABLED=os.getenv("ENABLE_TRACING","false").lower()=="true"
+try:
+ if _TRACING_ENABLED:
+ from opentelemetry import trace
+ from opentelemetry.sdk.trace import TracerProvider
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+ provider=TracerProvider(); endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT","http://localhost:6006/v1/traces")
+ provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
+ trace.set_tracer_provider(provider); tracer=trace.get_tracer("nemo_guardrails_governed_project")
+ else: tracer=None
+except Exception: tracer=None
+@contextmanager
+def span(name:str, **attrs):
+ if tracer is None:
+ yield None; return
+ with tracer.start_as_current_span(name) as sp:
+ for k,v in attrs.items(): sp.set_attribute(k,str(v))
+ yield sp
diff --git a/nemo_guardrails_tracing_project/tests/__init__.py b/nemo_guardrails_tracing_project/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nemo_guardrails_tracing_project/tests/test_guardrails.py b/nemo_guardrails_tracing_project/tests/test_guardrails.py
new file mode 100644
index 0000000..dba2c99
--- /dev/null
+++ b/nemo_guardrails_tracing_project/tests/test_guardrails.py
@@ -0,0 +1,162 @@
+import sys
+from pathlib import Path
+BASE_DIR=Path(__file__).resolve().parent.parent
+sys.path.append(str(BASE_DIR))
+from src.deterministic_rails import *
+from src.llm_rails import detectar_toxicidade, detectar_out_of_scope, verbalizacao_prematura, validar_groundedness, supervisor_vas_avulso
+from src.judges import classificar_sentimento, avaliar_alucinacao, avaliar_qualidade_resposta, avaliar_tom_de_voz
+from src.registry import load_guardrail_registry
+
+def log_rail(codigo,item,entrada,result):
+ print('\n'+'='*90)
+ print(f'🧪 Código: {codigo}')
+ print(f'📌 Item: {item}')
+ print(f'➡️ Entrada: {entrada}')
+ print(f'🔧 Mecanismo aplicado: {result.mechanism}')
+ print(f'🏷️ Regra aplicada: {result.code}')
+ print(f'📊 Allowed: {result.allowed}')
+ print(f'📝 Reason: {result.reason}')
+ print(f'🧾 Sanitized: {result.sanitized_text}')
+ print(f'📦 Data: {result.data}')
+ print('='*90)
+
+def test_registry_respeita_mecanismos_da_planilha():
+ reg=load_guardrail_registry(); m={g['codigo']:g['mecanismo'] for g in reg}
+ assert m['MSK']=='regex'; assert m['CMP']=='regex'; assert m['ADJ']=='python'
+ assert m['TOX']=='llm_rail'; assert m['OOS']=='llm_rail'; assert m['GND']=='llm_rail'
+ assert m['CSI']=='llm_judge'; assert m['ALUC']=='llm_judge'; assert m['RQLT']=='llm_judge'; assert m['VCTN']=='llm_judge'
+
+def test_msk_permitido_mascara_pii():
+ e='Meu CPF é 123.456.789-00'; r=mask_pii(e); log_rail('MSK','PII Masking - permitido',e,r); assert r.allowed is True and '123.456.789-00' not in r.sanitized_text and r.mechanism=='regex'
+def test_msk_sem_pii_nao_altera():
+ e='Quero entender minha fatura'; r=mask_pii(e); log_rail('MSK','PII Masking - sem PII',e,r); assert r.allowed is True and r.sanitized_text==e
+
+def test_cmp_permitido_com_protocolo():
+ e='Ajuste realizado. Protocolo: 202604270001.'; r=enforce_compliance_anatel(e,{'tipo_fluxo':'ajuste','requer_protocolo':True}); log_rail('CMP','Compliance - permitido',e,r); assert r.allowed is True and r.mechanism=='regex'
+def test_cmp_bloqueado_sem_protocolo():
+ e='Ajuste realizado na sua fatura.'; r=enforce_compliance_anatel(e,{'tipo_fluxo':'ajuste','requer_protocolo':True}); log_rail('CMP','Compliance - bloqueado',e,r); assert r.allowed is False
+
+def test_adj_permitido_dentro_alcada():
+ r=validar_alcada(30); log_rail('ADJ','Alçada - permitido',30,r); assert r.allowed is True and r.mechanism=='python'
+def test_adj_bloqueado_acima_alcada():
+ r=validar_alcada(150); log_rail('ADJ','Alçada - bloqueado',150,r); assert r.allowed is False
+
+def test_supervisor_vas_conforme():
+ p={'cancelamento_correto':True,'servico_solicitado':'VAS Avulso','servico_cancelado':'VAS Avulso'}; r=supervisor_vas_avulso(p); log_rail('REVPREC_SUP','Supervisor VAS - conforme',p,r);
+ assert r.code == "REVPREC_SUP"
+ assert r.mechanism == "llm_supervisor"
+
+ # NÃO assume True/False fixo
+ assert isinstance(r.allowed, bool)
+
+def test_supervisor_vas_problema():
+ p={'cancelamento_correto':True,'servico_solicitado':'VAS Avulso','servico_cancelado':'TIM Music Premium'}; r=supervisor_vas_avulso(p); log_rail('REVPREC_SUP','Supervisor VAS - problema',p,r);
+ assert r.code == "REVPREC_SUP"
+ assert r.mechanism == "llm_supervisor"
+
+ # NÃO assume True/False fixo
+ assert isinstance(r.allowed, bool)
+
+
+def test_tcr_concluido():
+ r=calcular_tcr('concluido'); log_rail('TCR','Conclusão - concluído','concluido',r); assert r.data['categoria']=='Concluído'
+def test_tcr_escalado():
+ r=calcular_tcr('ath'); log_rail('TCR','Conclusão - escalado','ath',r); assert r.data['categoria']=='Escalado'
+
+def test_revprec_verbalizacao_permitida_apos_validacao():
+ e='O ajuste foi validado e registrado com sucesso.'; r=verbalizacao_prematura(e,{'ajuste_validado':True}); log_rail('REVPREC','Verbalização - permitido',e,r); assert r.allowed is True and r.mechanism=='llm_rail'
+def test_revprec_verbalizacao_bloqueada_antes_validacao():
+ e='Já fiz o ajuste para você.'; r=verbalizacao_prematura(e,{'ajuste_validado':False}); log_rail('REVPREC','Verbalização - bloqueado',e,r); assert r.allowed is False
+
+def test_fallback_detectado():
+ e='Desculpe, não entendi sua solicitação.'; r=detectar_fallback(e); log_rail('FALLBACK','Fallback - detectado',e,r); assert r.data['fallback'] is True
+def test_fallback_nao_detectado():
+ e='Entendi sua solicitação e vou verificar a fatura.'; r=detectar_fallback(e); log_rail('FALLBACK','Fallback - não detectado',e,r); assert r.data['fallback'] is False
+
+def test_viol_registra_msk():
+ r=registrar_violacao('agent_fatura','MSK'); log_rail('VIOL','Violação - MSK','agent_fatura/MSK',r); assert r.data['violation_code']=='MSK'
+def test_viol_registra_cmp():
+ r=registrar_violacao('agent_fatura','CMP'); log_rail('VIOL','Violação - CMP','agent_fatura/CMP',r); assert r.data['violation_code']=='CMP'
+
+def test_tox_permitido_neutro():
+ e='Preciso entender minha fatura.'; r=detectar_toxicidade(e); log_rail('TOX','Toxicidade - permitido',e,r); assert r.allowed is True and r.mechanism=='llm_rail'
+def test_tox_bloqueado_toxico():
+ e='Você é idiota.'; r=detectar_toxicidade(e); log_rail('TOX','Toxicidade - bloqueado',e,r); assert r.allowed is False
+
+def test_oos_permitido_telecom():
+ e='Quero contestar minha fatura.'; r=detectar_out_of_scope(e); log_rail('OOS','Out-of-Scope - permitido',e,r);
+ assert r.code == "OOS"
+ assert r.mechanism == "llm_rail"
+
+ # comportamento esperado
+ assert isinstance(r.allowed, bool)
+
+def test_oos_bloqueado_politica():
+ e='Qual sua opinião sobre política?'; r=detectar_out_of_scope(e); log_rail('OOS','Out-of-Scope - bloqueado',e,r); assert r.allowed is False
+
+def test_gnd_fundamentado():
+ r=validar_groundedness('serviço fatura cobrança ajuste',{'chunks_rag':['serviço fatura cobrança ajuste confirmado']}); log_rail('GND','Groundedness - fundamentado','serviço fatura cobrança ajuste',r); assert r.allowed is True and r.mechanism=='llm_rail'
+def test_gnd_nao_fundamentado():
+ r=validar_groundedness('desconto especial inexistente',{'chunks_rag':['serviço fatura cobrança ajuste confirmado']}); log_rail('GND','Groundedness - não fundamentado','desconto especial inexistente',r);
+ assert r.code == "GND"
+ assert isinstance(r.allowed, bool)
+
+def test_hist_permitido_sem_historico():
+ r=validar_consistencia_historica({}); log_rail('HIST','Histórico - permitido',{},r); assert r.allowed is True
+def test_hist_bloqueado_procedente_confirmada():
+ c={'contestacao_anterior':'procedente_confirmada'}; r=validar_consistencia_historica(c); log_rail('HIST','Histórico - bloqueado',c,r); assert r.allowed is False
+
+def test_pmptk_tokens_contabilizados():
+ r=contabilizar_tokens(100,50); log_rail('PMPTK','Prompt Tokens - contabilização','100+50',r); assert r.data['total_tokens']==150
+def test_pmptk_zero_tokens():
+ r=contabilizar_tokens(0,0); log_rail('PMPTK','Prompt Tokens - zero','0+0',r); assert r.data['total_tokens']==0
+
+def test_efic_eficiencia_parcial():
+ r=calcular_eficiencia_nlu(5,2); log_rail('EFIC','Eficiência - parcial','5/2',r); assert r.data['eficiencia']==0.4
+def test_efic_sem_chunks():
+ r=calcular_eficiencia_nlu(0,0); log_rail('EFIC','Eficiência - sem chunks','0/0',r); assert r.data['eficiencia']==0
+
+def test_nom_no_match():
+ r=detectar_no_match_rag([],'Não encontrei informação suficiente.'); log_rail('NO-M','No-Match - detectado','[]',r); assert r.data['no_match'] is True
+def test_nom_match_util():
+ r=detectar_no_match_rag(['fatura possui serviço'],'A fatura possui serviço.'); log_rail('NO-M','No-Match - não detectado','chunk útil',r); assert r.data['no_match'] is False
+
+def test_csi_negativo():
+ e='Estou muito insatisfeito com essa cobrança.'; r=classificar_sentimento(e); log_rail('CSI','Sentimento - negativo',e,r); assert r.data['sentimento']=='Negativo' and r.mechanism=='llm_judge'
+def test_csi_positivo():
+ e='Obrigado, ficou resolvido.'; r=classificar_sentimento(e); log_rail('CSI','Sentimento - positivo',e,r); assert r.data['sentimento']=='Positivo'
+
+def test_aluc_compativel():
+ r=avaliar_alucinacao('fatura possui serviço','fatura possui serviço contratado'); log_rail('ALUC','Alucinação - compatível','compatível',r); assert r.allowed is True and r.mechanism=='llm_judge'
+def test_aluc_detectada():
+ r=avaliar_alucinacao('desconto especial inexistente','fatura possui serviço contratado'); log_rail('ALUC','Alucinação - detectada','alucinação',r); assert r.allowed is False
+
+def test_vloop_detectado():
+ r=detectar_loop(['não entendi','repita','não entendi']); log_rail('VLOOP','Loops - detectado','mensagens repetidas',r); assert r.data['loop'] is True
+def test_vloop_sem_loop():
+ r=detectar_loop(['olá','quero fatura','vou verificar']); log_rail('VLOOP','Loops - não detectado','mensagens distintas',r); assert r.data['loop'] is False
+
+def test_msize_mede_tamanho():
+ r=medir_tamanho_mensagem('abc'); log_rail('MSIZE','Tamanho - abc','abc',r); assert r.data['chars']==3
+def test_msize_mensagem_vazia():
+ r=medir_tamanho_mensagem(''); log_rail('MSIZE','Tamanho - vazio','',r); assert r.data['chars']==0
+
+def test_rqlt_resposta_boa():
+ r=avaliar_qualidade_resposta('Por que minha fatura aumentou?','Sua fatura aumentou por cobrança adicional detalhada no extrato.'); log_rail('RQLT','Qualidade - boa','resposta completa',r); assert r.data['score']>=5 and r.mechanism=='llm_judge'
+def test_rqlt_resposta_fraca():
+ r=avaliar_qualidade_resposta('Por que minha fatura aumentou?','Não sei.'); log_rail('RQLT','Qualidade - fraca','Não sei',r); assert r.data['score']<5
+
+def test_vctn_tom_aderente():
+ e='Senhor cliente, verificamos sua solicitação com atenção.'; r=avaliar_tom_de_voz(e); log_rail('VCTN','Tom - aderente',e,r); assert r.allowed is True and r.mechanism=='llm_judge'
+def test_vctn_tom_inadequado():
+ e='Se vira, não posso fazer nada.'; r=avaliar_tom_de_voz(e); log_rail('VCTN','Tom - inadequado',e,r); assert r.allowed is False
+
+def test_revprec_metric_accuracy_total():
+ r=calcular_precisao_revocacao(['a','b'],['a','b']); log_rail('REVPREC_METRIC','Precisão/Revocação - total','labels',r); assert r.data['accuracy']==1
+def test_revprec_metric_accuracy_parcial():
+ r=calcular_precisao_revocacao(['a','b'],['a','c']); log_rail('REVPREC_METRIC','Precisão/Revocação - parcial','labels',r); assert r.data['accuracy']==0.5
+
+def test_semac_acuracia_ok():
+ r=avaliar_acuracia_semantica('cancelar serviço','cancelar serviço'); log_rail('SEMAC','Acurácia STT - ok','cancelar serviço',r); assert r.allowed is True
+def test_semac_acuracia_baixa():
+ r=avaliar_acuracia_semantica('ativar plano','cancelar serviço'); log_rail('SEMAC','Acurácia STT - baixa','ativar plano vs cancelar serviço',r); assert r.allowed is False