diff --git a/README.md b/README.md index efe729b..e9b7f86 100644 --- a/README.md +++ b/README.md @@ -485,7 +485,18 @@ ALCADA_MAX_AJUSTE=50 ## 7. Configuração do NeMo Guardrails -Arquivo: `config/config.yml` +A camada de configuração no NeMo Guardrails é responsável por definir o comportamento do sistema sem alterar código Python. + +Ela é composta por dois arquivos principais: + +| Arquivo | Função | +|------------|----------------------------------------------------------------| +| config.yml | Configuração estrutural (modelo, instruções, bindings) | +| rails.co | Definição de fluxos, regras e comportamento conversacional | + + + +### Arquivo: `config/config.yml` ```yaml models: @@ -508,7 +519,147 @@ rails: - self check output ``` -## 8. Rails criados a partir da planilha +O config.yml é o arquivo central de configuração do NeMo Guardrails. + +Ele define: + +qual modelo será utilizado +como o agente deve se comportar +quais fluxos (rails) serão executados +como o sistema se conecta ao motor de LLM + +>**Nota:** Em termos simples, o config.yml define a estrutura e as regras globais do agente, antes mesmo da execução dos fluxos do .co + +Dentro do seu fluxo: + + User Input + ↓ + config.yml ← (define regras globais) + ↓ + rails.co ← (define comportamento dinâmico) + ↓ + LLM / Python / Supervisor + + +### Arquivo: `config/guardrails.yaml` + +```yaml +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} +``` + +### Arquivo: `rails/input.co` + +```yaml +define flow check toxicidade +user input +bot evaluate toxicity +if not allowed +bot refuse + + +define flow check out of scope +user input +bot evaluate out_of_scope +if not allowed +bot respond "Posso ajudar apenas com assuntos relacionados a telecom." +``` + +### Arquivo: `rails/output.co` + +```yaml +define flow check verbalizacao prematura +bot message +if contains "já fiz" or contains "já apliquei" +bot revise + + +define flow check groundedness +bot message +bot evaluate groundedness +if not grounded +bot revise + +define flow check compliance anatel +bot message + # CMP real tratado em Python +bot continue +``` + +## Definições + +### define flow + +Cria um fluxo de execução. + + define flow self check input + +Representa: + +- um guardrail +- uma sequência de validações + +### Eventos + +Captura a entrada do usuário. + +- user input + +### bot message + +Captura a resposta do LLM. + +- bot message + +### Ações com LLM + +Avaliação semântica + +- bot evaluate toxicity + +Função: + + chamar LLM para classificar conteúdo + +Exemplo: + +- toxicidade +- out-of-scope +- groundedness + +### Condições + + if not allowed + bot refuse + +Função: + +- bloquear fluxo +- redirecionar resposta + +## 8. Exemplos de rails criados a partir da planilha Segue abaixo a estruturação dos exemplos para o entendimento do funcionamento da tecnologia. diff --git a/nemo_guardrails_tracing_project/config/config.yml b/nemo_guardrails_tracing_project/config/config.yml index acb6c37..de40e22 100644 --- a/nemo_guardrails_tracing_project/config/config.yml +++ b/nemo_guardrails_tracing_project/config/config.yml @@ -2,8 +2,42 @@ models: - type: main engine: openai model: gpt-4.1 + parameters: + temperature: 0.2 + max_tokens: 800 + +# 🔧 MUITO IMPORTANTE +actions: + - name: mask_pii + function: config.actions.action_mask_pii + + - name: detectar_toxicidade + function: config.actions.action_detectar_toxicidade + + - name: detectar_out_of_scope + function: config.actions.action_detectar_out_of_scope + + - name: validar_alcada + function: config.actions.action_validar_alcada + + - name: validar_groundedness + function: config.actions.action_validar_groundedness + + - name: verbalizacao_prematura + function: config.actions.action_verbalizacao_prematura + + - name: cmp + function: config.actions.action_cmp + +# 🧠 Organização dos rails rails: input: - flows: [check toxicidade, check out of scope] + flows: + - input_check_toxicidade + - input_check_out_of_scope + output: - flows: [check verbalizacao prematura, check groundedness] + flows: + - output_check_verbalizacao_prematura + - output_check_groundedness + - output_check_cmp diff --git a/nemo_guardrails_tracing_project/config/rails/input.co b/nemo_guardrails_tracing_project/config/rails/input.co index 1be773d..885c655 100644 --- a/nemo_guardrails_tracing_project/config/rails/input.co +++ b/nemo_guardrails_tracing_project/config/rails/input.co @@ -1,5 +1,12 @@ -define flow check toxicidade - pass +define flow input_check_toxicidade + user input + $r = execute detectar_toxicidade(text=$user_input) + if $r.allowed == False + bot respond "BLOCKED:TOX" -define flow check out of scope - pass + +define flow input_check_out_of_scope + user input + $r = execute detectar_out_of_scope(text=$user_input) + if $r.allowed == False + bot respond "BLOCKED:OOS" \ No newline at end of file diff --git a/nemo_guardrails_tracing_project/config/rails/output.co b/nemo_guardrails_tracing_project/config/rails/output.co index 25e978b..5830503 100644 --- a/nemo_guardrails_tracing_project/config/rails/output.co +++ b/nemo_guardrails_tracing_project/config/rails/output.co @@ -1,5 +1,27 @@ -define flow check verbalizacao prematura - pass +define flow output_check_cmp -define flow check groundedness - pass + $r = execute cmp(resposta="dummy") + + if $r.allowed == False + bot respond "BLOCKED:CMP" + + bot respond "OK" + + +define flow output_check_verbalizacao_prematura + + $r = execute verbalizacao_prematura(resposta="dummy") + + if $r.allowed == False + bot respond "BLOCKED:REVPREC" + + bot respond "OK" + +define flow output_check_groundedness + + $r = execute validar_groundedness(resposta="dummy") + + if $r.allowed == False + bot respond "BLOCKED:GND" + + bot respond "OK" \ No newline at end of file diff --git a/nemo_guardrails_tracing_project/src/app.py b/nemo_guardrails_tracing_project/src/app.py index 900d4b6..5178159 100644 --- a/nemo_guardrails_tracing_project/src/app.py +++ b/nemo_guardrails_tracing_project/src/app.py @@ -1,5 +1,11 @@ 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 .llm_rails import ( + detectar_toxicidade, + detectar_out_of_scope, + validar_groundedness, + verbalizacao_prematura, + supervisor_vas_avulso +) from .judges import avaliar_qualidade_resposta import json @@ -7,7 +13,9 @@ import json def executar_atendimento(user_input: str, context: dict): steps = [] - # INPUT RAILS + # ========================= + # 🔹 INPUT RAILS + # ========================= r = mask_pii(user_input) steps.append(r) text = r.sanitized_text or user_input @@ -15,46 +23,93 @@ def executar_atendimento(user_input: str, context: dict): 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, + "response": None, "steps": steps } - # PYTHON RULE + # ========================= + # 🔹 PYTHON RULE (CRÍTICA) + # ========================= 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, + "response": None, "steps": steps } - # LLM RESPONSE (simulada ou real) - resposta = context.get("resposta_llm", "Resposta simulada do agente.") + # ========================= + # 🔹 LLM RESPONSE + # ========================= + resposta = context.get( + "resposta_llm", + "Resposta simulada do agente." + ) - # OUTPUT + JUDGES + SUPERVISOR - for r in [ - verbalizacao_prematura(resposta, context), + # ========================= + # 🔹 OUTPUT RAILS (BLOQUEANTES) + # ========================= + output_rails = [ enforce_compliance_anatel(resposta, context), + verbalizacao_prematura(resposta, context), validar_groundedness(resposta, context), - avaliar_qualidade_resposta(user_input, resposta), - supervisor_vas_avulso(context.get("supervisor_payload", {})) - ]: + ] + + for r in output_rails: steps.append(r) + # 🔥 NÃO bloquear groundedness automaticamente + if not r.allowed and r.code != "GND": + return { + "allowed": False, + "blocked_by": r.code, + "response": None, + "steps": steps + } + + # ========================= + # 🔹 JUDGE (NÃO BLOQUEIA) + # ========================= + r_quality = avaliar_qualidade_resposta(user_input, resposta) + steps.append(r_quality) + + # ========================= + # 🔹 SUPERVISOR (AUDITORIA) + # ========================= + r_supervisor = supervisor_vas_avulso( + context.get("supervisor_payload", {}) + ) + steps.append(r_supervisor) + + # ========================= + # 🔹 RESULTADO FINAL + # ========================= + BLOCKING_CODES = {"CMP", "ADJ", "REVPREC"} + + allowed = all( + s.allowed for s in steps + if s.code in BLOCKING_CODES + ) + + return { - "allowed": all(s.allowed for s in steps if s.code != "RQLT"), + "allowed": allowed, "response": resposta, "steps": steps } # ========================= -# 🔥 EXECUÇÃO DIRETA +# 🔥 PRINT FORMATADO # ========================= def print_result(result): print("\n" + "=" * 80) @@ -81,7 +136,6 @@ def print_result(result): print("=" * 80) - # JSON final (para integração) print("\n📦 JSON OUTPUT:") print(json.dumps({ "allowed": result["allowed"], @@ -99,8 +153,10 @@ def print_result(result): }, indent=2, ensure_ascii=False)) +# ========================= +# 🔥 EXECUÇÃO DIRETA +# ========================= if __name__ == "__main__": - # 🔧 EXEMPLO DE EXECUÇÃO user_input = "Meu CPF é 123.456.789-00 e quero ajuste de 20 reais" diff --git a/nemo_guardrails_tracing_project/tests/test_guardrails.py b/nemo_guardrails_tracing_project/tests/test_guardrails.py index dba2c99..df7580f 100644 --- a/nemo_guardrails_tracing_project/tests/test_guardrails.py +++ b/nemo_guardrails_tracing_project/tests/test_guardrails.py @@ -6,6 +6,7 @@ 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 +from src.app import executar_atendimento def log_rail(codigo,item,entrada,result): print('\n'+'='*90) @@ -160,3 +161,30 @@ 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 + +def test_fluxo_completo_bloqueia_cmp(): + result = executar_atendimento( + "Quero ajuste", + { + "tipo_fluxo": "ajuste", + "requer_protocolo": True, + "resposta_llm": "Ajuste realizado na sua fatura.", # ❌ sem protocolo + } + ) + + assert result["allowed"] is False + assert result["blocked_by"] == "CMP" + +def test_fluxo_completo_sucesso(): + result = executar_atendimento( + "Quero ajuste", + { + "tipo_fluxo": "ajuste", + "requer_protocolo": True, + "resposta_llm": "Ajuste realizado. Protocolo: 123", + "chunks_rag": ["ajuste realizado protocolo"], + "supervisor_payload": {} + } + ) + + assert result["allowed"] is True