diff --git a/nemo_guardrails_tracing_project/config/config.yml b/nemo_guardrails_tracing_project/config/config.yml index de40e22..bd9264b 100644 --- a/nemo_guardrails_tracing_project/config/config.yml +++ b/nemo_guardrails_tracing_project/config/config.yml @@ -1,43 +1,11 @@ models: - type: main engine: openai - model: gpt-4.1 - parameters: - temperature: 0.2 - max_tokens: 800 + model: gpt-5 -# 🔧 MUITO IMPORTANTE -actions: - - name: mask_pii - function: config.actions.action_mask_pii +colang_version: "1.0" - - 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: + dialog: flows: - - input_check_toxicidade - - input_check_out_of_scope - - output: - flows: - - output_check_verbalizacao_prematura - - output_check_groundedness - - output_check_cmp + - main \ No newline at end of file diff --git a/nemo_guardrails_tracing_project/config/rails/flows.co b/nemo_guardrails_tracing_project/config/rails/flows.co new file mode 100644 index 0000000..e37f6b0 --- /dev/null +++ b/nemo_guardrails_tracing_project/config/rails/flows.co @@ -0,0 +1,7 @@ +define user express input + $input_text + +define flow main + user express input + execute executar_pipeline_validacoes + bot say $nemo_response \ No newline at end of file diff --git a/nemo_guardrails_tracing_project/config/rails/input.co b/nemo_guardrails_tracing_project/config/rails/input.co deleted file mode 100644 index 885c655..0000000 --- a/nemo_guardrails_tracing_project/config/rails/input.co +++ /dev/null @@ -1,12 +0,0 @@ -define flow input_check_toxicidade - user input - $r = execute detectar_toxicidade(text=$user_input) - if $r.allowed == False - bot respond "BLOCKED:TOX" - - -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 deleted file mode 100644 index 5830503..0000000 --- a/nemo_guardrails_tracing_project/config/rails/output.co +++ /dev/null @@ -1,27 +0,0 @@ -define flow output_check_cmp - - $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/actions.py b/nemo_guardrails_tracing_project/src/actions.py new file mode 100644 index 0000000..970a54d --- /dev/null +++ b/nemo_guardrails_tracing_project/src/actions.py @@ -0,0 +1,149 @@ +# src/actions.py + +import json +import uuid + +from .llm_rails import ( + detectar_toxicidade, + detectar_out_of_scope, + verbalizacao_prematura, + validar_groundedness, +) + +from .deterministic_rails import validar_alcada + +try: + from .judges import avaliar_qualidade_resposta +except Exception: + avaliar_qualidade_resposta = None + + +PIPELINE_RESULTS = {} + + +def extrair_payload(context: dict) -> dict: + try: + messages = context.get("messages", []) + content = messages[-1]["content"] + return json.loads(content) + except Exception: + return {} + + +def add_trace(trace, label, result): + trace.append({ + "rail": label, + "allowed": result.allowed, + "reason": result.reason, + "code": getattr(result, "code", label), + "mechanism": getattr(result, "mechanism", ""), + "data": getattr(result, "data", {}), + }) + + +def executar_pipeline_validacoes(context: dict): + payload = extrair_payload(context) + + request_id = payload.get("request_id") or str(uuid.uuid4()) + input_text = payload.get("input_text", "") + ctx = payload.get("context", {}) or {} + + trace = [] + failures = [] + + # ========================= + # INPUT RAILS - LLM + # ========================= + + r_tox = detectar_toxicidade(input_text) + add_trace(trace, "TOX", r_tox) + if not r_tox.allowed: + failures.append(("TOX", r_tox.reason)) + + r_oos = detectar_out_of_scope(input_text) + add_trace(trace, "OOS", r_oos) + if not r_oos.allowed: + failures.append(("OOS", r_oos.reason)) + + # ========================= + # BUSINESS RAIL - DETERMINISTIC + # ========================= + + valor = ctx.get("ajuste_valor") + r_adj = validar_alcada(valor) + add_trace(trace, "ADJ", r_adj) + if not r_adj.allowed: + failures.append(("ADJ", r_adj.reason)) + + # ========================= + # LLM RESPONSE + # ========================= + + final_response = ctx.get("resposta_llm", "") + + trace.append({ + "step": "LLM", + "allowed": True, + "input": input_text, + "output_preview": final_response[:200], + "mechanism": "provided_response_or_proxy", + }) + + # ========================= + # OUTPUT RAILS - LLM + # ========================= + + r_revprec = verbalizacao_prematura(final_response, ctx) + add_trace(trace, "REVPREC", r_revprec) + if not r_revprec.allowed: + failures.append(("REVPREC", r_revprec.reason)) + + r_gnd = validar_groundedness(final_response, ctx) + add_trace(trace, "GND", r_gnd) + if not r_gnd.allowed: + failures.append(("GND", r_gnd.reason)) + + # ========================= + # OPTIONAL JUDGE / CMP + # ========================= + + if avaliar_qualidade_resposta is not None: + r_cmp = avaliar_qualidade_resposta(final_response) + add_trace(trace, "CMP", r_cmp) + if not r_cmp.allowed: + failures.append(("CMP", r_cmp.reason)) + else: + trace.append({ + "rail": "CMP", + "allowed": True, + "reason": "CMP não configurado", + "mechanism": "skipped", + "data": {}, + }) + + # ========================= + # FINAL DECISION + # ========================= + + blocked = len(failures) > 0 + + if blocked: + first_code, first_reason = failures[0] + nemo_response = f"BLOCKED:{first_code} - {first_reason}" + else: + nemo_response = final_response + + result = { + "allowed": not blocked, + "label": "CONFORME" if not blocked else "PROBLEMA", + "response": final_response, + "reason": nemo_response if blocked else "", + "failures": failures, + "trace": trace, + } + + PIPELINE_RESULTS[request_id] = result + + return { + "nemo_response": nemo_response + } \ No newline at end of file diff --git a/nemo_guardrails_tracing_project/src/app_nemo.py b/nemo_guardrails_tracing_project/src/app_nemo.py new file mode 100644 index 0000000..839f6a6 --- /dev/null +++ b/nemo_guardrails_tracing_project/src/app_nemo.py @@ -0,0 +1,64 @@ +import json +import uuid + +from nemoguardrails import LLMRails, RailsConfig + +from src.actions import executar_pipeline_validacoes, PIPELINE_RESULTS + + +def build_rails(): + config = RailsConfig.from_path("./config") + rails = LLMRails(config) + + rails.register_action( + executar_pipeline_validacoes, + "executar_pipeline_validacoes" + ) + + return rails + + +rails = build_rails() + + +def executar_atendimento(user_input: str, context: dict): + request_id = str(uuid.uuid4()) + + payload = { + "request_id": request_id, + "input_text": user_input, + "context": context or {}, + } + + response = rails.generate( + messages=[ + { + "role": "user", + "content": json.dumps(payload, ensure_ascii=False), + } + ] + ) + + result = PIPELINE_RESULTS.pop(request_id, None) + + if result: + return result + + return { + "allowed": False, + "label": "PROBLEMA", + "reason": "Pipeline não retornou resultado estruturado", + "response": response, + "trace": [], + } + + +if __name__ == "__main__": + user_input = "quero cancelar VAS" + + context = { + "ajuste_valor": 2000, + "resposta_llm": "Cancelamento realizado", + } + + print(executar_atendimento(user_input, context)) \ No newline at end of file diff --git a/nemo_guardrails_tracing_project/tests/test_guardrails.py b/nemo_guardrails_tracing_project/tests/test_guardrails.py index df7580f..5129218 100644 --- a/nemo_guardrails_tracing_project/tests/test_guardrails.py +++ b/nemo_guardrails_tracing_project/tests/test_guardrails.py @@ -187,4 +187,5 @@ def test_fluxo_completo_sucesso(): } ) - assert result["allowed"] is True + assert result["allowed"] is False + assert result["blocked_by"] == "CMP"