From f613b58a8a1e3b630586cf5ff02a7457d2874a23 Mon Sep 17 00:00:00 2001 From: hoshikawa2 Date: Wed, 29 Apr 2026 20:45:00 -0300 Subject: [PATCH] first commit --- .idea/.gitignore | 10 + .idea/Nemo.iml | 9 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .oca/custom_code_review_guidelines.txt | 24 + Dockerfile | 16 + README.md | 819 +++++++++++++++--- README_BKP.md | 462 ++++++++++ app/__init__.py | 0 app/main.py | 56 ++ azure-pipelines.yml | 63 ++ config/config.yml | 16 + config/input.co | 5 + config/output.co | 5 + k8s/deployment.yaml | 39 + k8s/kustomization.yaml | 5 + k8s/namespace.yaml | 4 + k8s/secret.yaml | 8 + k8s/service.yaml | 13 + requirements.txt | 6 + scripts/build_local.sh | 3 + scripts/deploy_local.sh | 4 + scripts/test_local.sh | 4 + src/__init__.py | 0 src/actions.py | 15 + src/deterministic_rails.py | 44 + src/llm_rails.py | 18 + .../conftest.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 1244 bytes ..._api_contract.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 4236 bytes .../test_e2e.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 3657 bytes ...st_structural.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 3302 bytes tests/conftest.py | 16 + tests/test_api_contract.py | 19 + tests/test_e2e.py | 21 + tests/test_structural.py | 7 + tutorial_azure_oci_oke.md | 115 +++ 37 files changed, 1740 insertions(+), 106 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/Nemo.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .oca/custom_code_review_guidelines.txt create mode 100644 Dockerfile create mode 100644 README_BKP.md create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 azure-pipelines.yml create mode 100644 config/config.yml create mode 100644 config/input.co create mode 100644 config/output.co create mode 100644 k8s/deployment.yaml create mode 100644 k8s/kustomization.yaml create mode 100644 k8s/namespace.yaml create mode 100644 k8s/secret.yaml create mode 100644 k8s/service.yaml create mode 100644 requirements.txt create mode 100644 scripts/build_local.sh create mode 100644 scripts/deploy_local.sh create mode 100644 scripts/test_local.sh create mode 100644 src/__init__.py create mode 100644 src/actions.py create mode 100644 src/deterministic_rails.py create mode 100644 src/llm_rails.py create mode 100644 tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_api_contract.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_e2e.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_structural.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/conftest.py create mode 100644 tests/test_api_contract.py create mode 100644 tests/test_e2e.py create mode 100644 tests/test_structural.py create mode 100644 tutorial_azure_oci_oke.md diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..18d7b81 --- /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/Nemo.iml b/.idea/Nemo.iml new file mode 100644 index 0000000..18ec59d --- /dev/null +++ b/.idea/Nemo.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9d74af9 --- /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..44bc9fd --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c8397c9 --- /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/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c7338e0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV OPENAI_API_KEY=sk-fake + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8080 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/README.md b/README.md index c1c5ec9..f2f55ae 100644 --- a/README.md +++ b/README.md @@ -1,179 +1,786 @@ -# CI/CD para Artefatos de Configuração no NeMo Guardrails +# CI/CD para Agentes de IA com NeMo Guardrails como Package Python no Azure DevOps e Deploy em OCI OKE -## Introdução +## 1. Introdução -Em projetos que utilizam **NeMo Guardrails**, os principais artefatos não são apenas código, mas sim **configurações declarativas** que controlam o comportamento do LLM. +Este documento apresenta uma estratégia de CI/CD para projetos de **Agentes de IA** que consomem uma arquitetura compartilhada de **guardrails baseada em NeMo Guardrails**. -Isso muda completamente a abordagem de CI/CD. +A referência técnica e conceitual deste material é o projeto: + +> [Tutorial: NeMo Guardrails com Python, Proxy OpenAI-Compatible e Tracing +](https://github.com/hoshikawa2/nemo_guardrails_configuration) + +A decisão arquitetural considerada neste documento é: + +> O projeto NeMo Guardrails **não será implantado como um serviço/server separado**. +> Ele será empacotado como uma **biblioteca Python versionada**, publicada por um pipeline próprio, e instalada pelos projetos de Agente de IA durante o build da imagem Docker. + +Essa decisão reduz a complexidade operacional, preserva a cadência de desenvolvimento dos times de agentes e mantém a governança dos artefatos de guardrails por meio de versionamento, testes e controle de publicação no pipeline do projeto NeMo. --- -## Tipos de Artefatos no NeMo +## 2. Visão geral da arquitetura -### 1. Configurações principais -- `config.yml` / `rails.yaml` -- `guardrails.yaml` -- `prompts/` -- `flows.co` -- `input.co` -- `output.co` +### 2.1 Fluxo funcional dos guardrails -### 2. Código complementar -- Actions Python (`actions.py`) -- Integrações externas (APIs, tools) - ---- - -## Problema Principal - -Diferente de código tradicional: - -- Pequenas mudanças em `.co` ou `.yaml` podem quebrar o comportamento -- Não há erro de compilação -- Falhas são **semânticas**, não sintáticas - ---- - -## Pipeline CI/CD Recomendado - -### Etapa 1: Validação Sintática - -Validar YAML e estrutura: - -```bash -yamllint . +```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 ``` -Validar carregamento: +Esse fluxo representa a função do projeto NeMo dentro da arquitetura: ele atua como uma **camada de segurança, validação, controle de resposta, curadoria e métricas** para agentes de IA. + +--- + +## 3. Decisão arquitetural: NeMo como package, não como serviço + +### 3.1 Modelo descartado: NeMo como serviço central + +Uma alternativa seria implantar o projeto NeMo como um serviço separado: + +```text +Agente de IA + ↓ HTTP +Serviço NeMo Guardrails + ↓ +LLM / Tools / Backend +``` + +Esse modelo centralizaria o runtime dos guardrails, mas traria custos e impactos: + +- necessidade de manter um serviço adicional; +- aumento de latência; +- criação de dependência runtime entre agente e serviço de guardrails; +- maior complexidade de deploy; +- possível gargalo de aprovação e governança; +- risco de interromper a cadência dos times de desenvolvimento. + +### 3.2 Modelo adotado: NeMo como biblioteca versionada + +```text +Pipeline NeMo + ↓ +gera package Python versionado + ↓ +publica em um feed privado + ↓ +Pipeline Agente + ↓ +pip install company-nemo-guardrails==x.y.z + ↓ +build da imagem final do agente + ↓ +deploy no OCI OKE +``` + +No runtime, o Pod terá **um único container**, contendo: + +```text +Pod Kubernetes + └── Container do Agente + ├── código do agente de IA + ├── package NeMo Guardrails instalado + ├── configs de guardrails + ├── actions + ├── deterministic rails + └── llm rails +``` + +Portanto, o deploy no OKE é sempre da **imagem final do agente**, e não de uma imagem separada do NeMo. + +--- + +## 4. Separação de responsabilidades + +### 4.1 Projeto NeMo Guardrails + +O projeto NeMo é responsável por: + +- manter os artefatos de guardrails; +- implementar rails determinísticos; +- implementar rails baseados em LLM; +- registrar actions; +- manter prompts; +- manter catálogo de regras; +- executar testes unitários e de regressão; +- gerar package Python versionado; +- publicar o package em um feed privado. + +### 4.2 Projeto Agente de IA + +Cada projeto de Agente de IA é responsável por: + +- implementar sua lógica conversacional; +- implementar integrações com APIs, backend, ferramentas e RAG; +- declarar a dependência do package NeMo; +- executar testes de integração com os guardrails; +- construir a imagem Docker final; +- publicar a imagem no OCIR; +- fazer deploy no OKE. + +### 4.3 Azure DevOps + +O Azure DevOps é responsável por orquestrar: + +- pipeline do NeMo; +- pipeline dos agentes; +- publicação do package; +- build Docker; +- push para OCIR; +- deploy no OKE; +- controle de variáveis e secrets; +- permissões entre repositórios, feeds e pipelines. + +--- + +## 5. Artefatos envolvidos no projeto NeMo + +O projeto `nemo_guardrails_configuration` contém artefatos que devem ser tratados como componentes de uma biblioteca compartilhada. + +Estrutura esperada: + +```text +nemo_guardrails_tracing_project/ +├── config/ +│ ├── config.yml +│ ├── config.py +│ ├── guardrails.yaml +│ ├── guardrails_catalog.json +│ └── rails/ +│ ├── input.co +│ └── output.co +├── src/ +│ ├── app.py +│ ├── app_nemo.py +│ ├── actions.py +│ ├── deterministic_rails.py +│ ├── judges.py +│ ├── llm_client.py +│ ├── llm_rails.py +│ ├── models.py +│ ├── registry.py +│ └── prompts/ +├── tests/ +│ └── test_guardrails.py +├── scripts/ +│ └── run_tests.sh +├── requirements.txt +├── pyproject.toml +├── .env.example +└── README.md +``` + +### 5.1 `config/config.yml` + +Arquivo principal de configuração do NeMo Guardrails. Define modelos, engine, parâmetros, input rails, output rails e fluxos habilitados. + +Em ambientes corporativos, o `engine: openai` pode apontar para um proxy OpenAI-compatible, inclusive um proxy interno para OCI Generative AI. + +### 5.2 `config/rails/input.co` + +Define fluxos de entrada, como PII, toxicidade, prompt injection, out-of-scope, bloqueio de intenção e normalização de input. + +### 5.3 `config/rails/output.co` + +Define fluxos de saída, como resposta segura, groundedness, ausência de vazamento de dados, aderência ao tom, compliance e prevenção de verbalização prematura. + +### 5.4 `src/actions.py` + +Arquivo de registro das actions expostas ao NeMo. Ele conecta os fluxos `.co` com funções Python. + +### 5.5 `src/deterministic_rails.py` + +Contém rails determinísticos, como máscara de CPF, validação de alçada, validação de protocolo, consistência entre valores e regras de negócio com retorno previsível. + +### 5.6 `src/llm_rails.py` + +Contém rails que dependem de LLM ou judge, como toxicidade, out-of-scope, groundedness, qualidade de resposta, tom de voz e avaliação semântica. + +### 5.7 `src/judges.py` + +Contém avaliadores usados para medir qualidade ou segurança das respostas. + +### 5.8 `src/models.py` + +Contém classes de retorno padronizadas, por exemplo: ```python -from nemoguardrails import LLMRails, RailsConfig +class RailResult: + allowed: bool + reason: str + sanitized_text: str | None + code: str + mechanism: str + data: dict +``` -config = RailsConfig.from_path("./config") -rails = LLMRails(config) +### 5.9 `src/registry.py` + +Contém o registry dos guardrails, mapeando código da regra, nome, tipo, mecanismo, severidade e comportamento esperado. + +### 5.10 `src/prompts/` + +Contém prompts usados por rails baseados em LLM. Esses prompts devem ser tratados como artefatos sensíveis. + +### 5.11 `tests/test_guardrails.py` + +Contém a suíte de testes do projeto NeMo. O pipeline apenas executa os testes versionados no repositório. + +### 5.12 `pyproject.toml` + +Arquivo necessário para transformar o projeto NeMo em package Python instalável. + +--- + +## 6. Segurança e governança dos artefatos NeMo + +Como o projeto NeMo contém artefatos de segurança, ele deve ter governança própria. + +### 6.1 O time do agente não deve ter acesso aos steps internos do pipeline NeMo + +O time do agente deve consumir apenas o resultado aprovado: + +```text +company-nemo-guardrails==1.2.0 +``` + +Ele não precisa acessar YAML do pipeline NeMo, steps de teste, lógica de aprovação, scripts internos ou secrets do pipeline NeMo. + +### 6.2 Permissões recomendadas no Azure DevOps + +| Recurso | Time NeMo | Time Agente | +|---|---:|---:| +| Repo NeMo | Leitura/escrita | Sem acesso ou leitura restrita | +| Pipeline NeMo | Administração | Sem acesso | +| Feed do package | Publicação | Leitura | +| Repo Agente | Sem acesso ou leitura | Leitura/escrita | +| Pipeline Agente | Sem acesso ou leitura | Administração | +| Service Connection OCI/OKE | Uso controlado | Uso conforme necessidade | + +### 6.3 Segurança por contrato + +O contrato entre NeMo e Agente é o package. + +```text +Package Python versionado = contrato técnico e de segurança ``` --- -### Etapa 2: Testes de Guardrails (Unitários) +## 7. Fluxo de CI/CD completo -Criar cenários: +### 7.1 Pipeline do NeMo -```python -tests = [ - {"input": "xingar atendente", "expected_blocked": True}, - {"input": "cancelar plano", "expected_blocked": False} +```text +Commit no repo NeMo + ↓ +Pipeline NeMo + ↓ +instala dependências + ↓ +executa testes + ↓ +gera package Python + ↓ +publica no Azure Artifacts +``` + +### 7.2 Pipeline do Agente + +```text +Commit no repo Agente + ↓ +Pipeline Agente + ↓ +instala dependências + ↓ +instala package NeMo + ↓ +executa testes do agente + ↓ +build Docker + ↓ +push OCIR + ↓ +deploy OKE +``` + +### 7.3 Quem chama quem? + +Nenhum pipeline precisa chamar o outro diretamente. O acoplamento ocorre via package: + +```text +Pipeline NeMo publica package +Pipeline Agente instala package +``` + +--- + +## 8. Criando o package Python do projeto NeMo + +### 8.1 Estrutura sugerida + +```text +nemo_guardrails_configuration/ +├── company_nemo_guardrails/ +│ ├── __init__.py +│ ├── config/ +│ │ ├── config.yml +│ │ └── rails/ +│ │ ├── input.co +│ │ └── output.co +│ ├── actions.py +│ ├── deterministic_rails.py +│ ├── llm_rails.py +│ ├── judges.py +│ ├── models.py +│ ├── registry.py +│ └── prompts/ +├── tests/ +├── pyproject.toml +└── README.md +``` + +### 8.2 Exemplo de `pyproject.toml` + +```toml +[build-system] +requires = ["setuptools>=68", "wheel", "build"] +build-backend = "setuptools.build_meta" + +[project] +name = "company-nemo-guardrails" +version = "1.0.0" +description = "Biblioteca corporativa de guardrails baseada em NeMo Guardrails" +requires-python = ">=3.11" +dependencies = [ + "nemoguardrails[openai]", + "pydantic>=2", + "httpx" +] + +[tool.setuptools.packages.find] +where = ["."] + +[tool.setuptools.package-data] +company_nemo_guardrails = [ + "config/*.yml", + "config/**/*.co", + "prompts/*.txt", + "prompts/*.md", + "*.json" ] ``` -Executar: +### 8.3 Função utilitária para carregar config do package ```python -for t in tests: - response = rails.generate(messages=[{"role": "user", "content": t["input"]}]) +from importlib.resources import files +from nemoguardrails import RailsConfig, LLMRails + +def create_rails(): + config_path = files("company_nemo_guardrails").joinpath("config") + config = RailsConfig.from_path(str(config_path)) + return LLMRails(config) +``` + +Uso no agente: + +```python +from company_nemo_guardrails import create_rails + +rails = create_rails() ``` --- -### Etapa 3: Testes de Regressão +## 9. Pipeline Azure DevOps do NeMo para publicar package -Garantir que mudanças não quebram comportamento anterior. +```yaml +trigger: +- main -- Snapshot de respostas -- Comparação antes/depois +pool: + vmImage: 'ubuntu-latest' ---- +variables: + PYTHON_VERSION: '3.11' -### Etapa 4: Testes de Segurança (Guardrails) +steps: +- checkout: self -Validar: +- task: UsePythonVersion@0 + inputs: + versionSpec: '$(PYTHON_VERSION)' + displayName: 'Use Python $(PYTHON_VERSION)' -- Prompt injection -- Toxicidade -- Out-of-scope +- script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install build twine pytest + displayName: 'Install dependencies' ---- +- script: | + export OPENAI_API_KEY="${OPENAI_API_KEY:-sk-fake}" + pytest -v --junitxml=test-results.xml + displayName: 'Run NeMo Guardrails tests' -### Etapa 5: Testes de Fluxo Completo (E2E) +- task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'test-results.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() + displayName: 'Publish test results' -Simular jornada real: +- script: | + python -m build + ls -la dist + displayName: 'Build Python package' -- Input → Guardrails → LLM → Tools → Output +- task: TwineAuthenticate@1 + inputs: + artifactFeed: 'company-python-feed' + displayName: 'Authenticate Azure Artifacts feed' ---- - -## Estratégia de Versionamento - -### Versionar separadamente: - -- Código -- Configuração de guardrails - -Exemplo: - -``` -/app -/config -/tests +- script: | + python -m twine upload --repository company-python-feed --config-file $(PYPIRC_PATH) dist/* + displayName: 'Publish package to Azure Artifacts' ``` --- -## Deploy +## 10. Projeto Agente consumindo o package NeMo -### Opção 1: Embutido no container +### 10.1 Estrutura do projeto Agente -```Dockerfile -COPY config/ /app/config/ +```text +ai_agent_project/ +├── app/ +│ ├── main.py +│ └── agent.py +├── tests/ +│ ├── test_agent.py +│ └── test_guardrails_integration.py +├── k8s/ +│ ├── deployment.yaml +│ └── service.yaml +├── Dockerfile +├── requirements.txt +├── azure-pipelines.yml +└── README.md ``` -### Opção 2: Config externo +### 10.2 `requirements.txt` do Agente -- S3 / OCI Object Storage -- Volume mount +```txt +fastapi +uvicorn[standard] +company-nemo-guardrails==1.0.0 +``` + +### 10.3 Uso no código do Agente + +```python +from company_nemo_guardrails import create_rails + +rails = create_rails() + +def process_message(message: str): + response = rails.generate( + messages=[{"role": "user", "content": message}], + options={"log": {"activated_rails": True}} + ) + return response.output_text +``` --- -## Observabilidade no CI/CD +## 11. Pipeline Azure DevOps do Agente -Integrar com: +```yaml +trigger: +- main -- Logs estruturados -- Tracing (OpenTelemetry) -- Métricas de bloqueio +pool: + vmImage: 'ubuntu-latest' + +variables: + PYTHON_VERSION: '3.11' + IMAGE_NAME: 'ai-agent' + IMAGE_TAG: '$(Build.SourceVersion)' + K8S_NAMESPACE: 'ai-agent' + +steps: +- checkout: self + +- task: UsePythonVersion@0 + inputs: + versionSpec: '$(PYTHON_VERSION)' + displayName: 'Use Python $(PYTHON_VERSION)' + +- task: PipAuthenticate@1 + inputs: + artifactFeeds: 'company-python-feed' + displayName: 'Authenticate Python feed' + +- script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + displayName: 'Install agent dependencies with NeMo package' + +- script: | + export OPENAI_API_KEY="${OPENAI_API_KEY:-sk-fake}" + pytest -v --junitxml=test-results.xml + displayName: 'Run agent tests' + +- task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'test-results.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() + displayName: 'Publish test results' + +- script: | + IMAGE_URI="$(OCIR_REGISTRY)/$(OCI_NAMESPACE)/$(OCIR_REPOSITORY):$(IMAGE_TAG)" + echo "##vso[task.setvariable variable=IMAGE_URI]$IMAGE_URI" + docker build -t "$IMAGE_URI" . + displayName: 'Build Docker image' + +- script: | + echo "$(OCIR_PASSWORD)" | docker login "$(OCIR_REGISTRY)" -u "$(OCIR_USER)" --password-stdin + docker push "$(IMAGE_URI)" + displayName: 'Push image to OCIR' + +- script: | + mkdir -p ~/.kube + echo "$(OKE_KUBECONFIG_B64)" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + kubectl apply -f k8s/namespace.yaml + sed "s|REPLACE_IMAGE_URI|$(IMAGE_URI)|g" k8s/deployment.yaml | kubectl apply -f - + kubectl apply -f k8s/service.yaml + + kubectl rollout status deployment/ai-agent -n $(K8S_NAMESPACE) --timeout=180s + displayName: 'Deploy to OCI OKE' +``` --- -## Estratégia Avançada +## 12. Dockerfile do Agente -### Feature Flags de Guardrails +```dockerfile +FROM python:3.11-slim -Permitir ativar/desativar regras sem deploy. +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8080 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] +``` --- -## Anti-patterns +## 13. Variáveis e secrets no Azure DevOps -Evitar: +### 13.1 Variáveis do pipeline do Agente -- Deploy direto sem testes -- Misturar lógica de negócio com guardrails -- Não versionar `.co` +| Variável | Exemplo | Tipo | +|---|---|---| +| `OCIR_REGISTRY` | `gru.ocir.io` | normal | +| `OCI_NAMESPACE` | `meunamespace` | normal | +| `OCIR_REPOSITORY` | `agents/ai-agent` | normal | +| `OCIR_USER` | `tenancy/user` | secret | +| `OCIR_PASSWORD` | auth token OCI | secret | +| `OKE_KUBECONFIG_B64` | kubeconfig em base64 | secret | +| `OPENAI_API_KEY` | chave ou fake/proxy | secret | + +### 13.2 Variáveis do pipeline NeMo + +| Variável | Exemplo | Tipo | +|---|---|---| +| `OPENAI_API_KEY` | `sk-fake` | secret | +| Feed credentials | gerenciado pelo Azure Artifacts | service/task | --- -## Conclusão +## 14. OCI Vault -No NeMo Guardrails: +### 14.1 Papel do OCI Vault -> CI/CD não é só sobre código, é sobre comportamento do agente. +O OCI Vault deve ser usado para proteger secrets em runtime: -É essencial tratar configurações como **artefatos críticos de software**. +- chaves de LLM; +- credenciais de API; +- tokens de backend; +- senhas de integração. -## Referências +### 14.2 Estratégia simples -- [Tutorial: NeMo Guardrails com Python, Proxy OpenAI-Compatible e Tracing](https://github.com/hoshikawa2/nemo_guardrails_configuration) +```text +Azure DevOps secret → Kubernetes Secret → Pod +``` -## Acknowledgments +### 14.3 Estratégia recomendada -- **Author** - Cristiano Hoshikawa (Oracle LAD A-Team Solution Engineer) +```text +OCI Vault → External Secrets Operator / CSI Driver → Kubernetes Secret → Pod +``` + +Esse modelo evita manter secrets de runtime no Azure DevOps. + +--- + +## 15. Segurança: impedindo acesso indevido ao pipeline NeMo + +### 15.1 Separação de repositórios + +```text +Repo NeMo → time plataforma/guardrails +Repo Agente → time aplicação/agente +``` + +### 15.2 Separação de pipelines + +```text +Pipeline NeMo → administrado pelo time NeMo +Pipeline Agente → administrado pelo time Agente +``` + +### 15.3 Compartilhamento apenas do package + +O time do agente recebe permissão de leitura no feed: + +```text +company-python-feed +``` + +Ele não precisa acessar repo NeMo, pipeline NeMo, steps de teste, scripts internos ou secrets do NeMo. + +--- + +## 16. Testes no modelo com package + +### 16.1 Testes do NeMo + +Executados no pipeline NeMo: + +- testes determinísticos; +- testes de segurança; +- testes de regressão; +- testes de catálogo; +- testes de actions; +- testes de judge. + +### 16.2 Testes do Agente + +Executados no pipeline do agente: + +- testes do código do agente; +- teste de carregamento do package NeMo; +- teste de integração mínima; +- teste E2E do fluxo conversacional; +- teste de contrato de API. + +### 16.3 Exemplo + +```python +def test_agent_uses_guardrails(): + from company_nemo_guardrails import create_rails + + rails = create_rails() + assert rails is not None +``` + +--- + +## 17. Versionamento + +Recomenda-se versionamento semântico: + +```text +MAJOR.MINOR.PATCH +``` + +Exemplos: + +```text +1.0.0 +1.1.0 +1.1.1 +2.0.0 +``` + +| Tipo de mudança | Versão | +|---|---| +| Correção sem mudança de comportamento esperado | PATCH | +| Nova regra compatível | MINOR | +| Mudança que pode bloquear novos casos | MINOR ou MAJOR | +| Mudança incompatível | MAJOR | + +No agente: + +```txt +company-nemo-guardrails==1.2.0 +``` + +Ou aceitando patches: + +```txt +company-nemo-guardrails~=1.2.0 +``` + +--- + +## 18. Conclusão + +A estratégia recomendada para este cenário é: + +```text +NeMo Guardrails = package Python versionado +Agente de IA = aplicação final +Pipeline NeMo = publica package aprovado +Pipeline Agente = instala package, gera imagem e faz deploy +Runtime = um container com agente + guardrails +``` + +Esse modelo equilibra segurança, governança, velocidade, simplicidade operacional, reutilização entre múltiplos agentes e deploy único no OCI OKE. + +A governança deixa de depender de um serviço central em runtime e passa a acontecer no ciclo de CI/CD, por meio de testes, versionamento, publicação controlada e permissões no feed de packages. diff --git a/README_BKP.md b/README_BKP.md new file mode 100644 index 0000000..c574d6a --- /dev/null +++ b/README_BKP.md @@ -0,0 +1,462 @@ +# CI/CD NeMo Guardrails no Azure DevOps com Deploy em OCI OKE + +## Objetivo + +Esta documentação tem por objetivo mostrar como implantar a estratégia de guardrails via Nemo Guardrails. +A idéia principal é que o projeto está baseado nesta especificação: + +[Tutorial: NeMo Guardrails com Python, Proxy OpenAI-Compatible e Tracing](https://github.com/hoshikawa2/nemo_guardrails_configuration) + +A arquitetura desta solução visa se integrar com soluções de agentes de IA com os guardrails do Nemo Guardrails. + + +```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 +``` + +Os detalhes da arquitetura podem ser vistas no material citado acima. + +As aplicações agentes devem ser integradas a esta estrutura do projeto: + +```text +ai_agent_project +├── src/ +│ ... +nemo_guardrails_tracing_project/ +├── config/ +│ ├── config.yml : Arquivo de configuração para o Nemo Guardrail (Modelos, definições de rails de input e output) +│ ├── config.py : Registro das ações para os guardrails +│ ├── guardrails.yaml : Arquivo de mapeamento dos rails conforme definição da Planilha inicial (não utilizado pelo código) +│ ├── rails/ +│ ├─────input.co : configuração de entrada dos fluxos Nemo +│ ├─────output.co : configuração de saída dos fluxos Nemo +│ └── guardrails_catalog.json +├── src/ +│ ├── app.py : demo para consumir rails sem uso de Nemo Guardrails +│ ├── app_nemo.py : demo para consumir rails com uso de Nemo Guardrails +│ ├── actions.py : Arquivo de Actions expondo deterministic_rails e llm_rails +│ ├── deterministic_rails.py : rails deterministicos para serem utilizados como modelo na estrutura +│ ├── judges.py : estrutura demo para ilustrar judge +│ ├── llm_client.py : mockup para demo llm +│ ├── llm_rails.py : rails llm para serem utilizados como modelo na estrutura +│ ├── models.py : classe do modelo de resposta dos rails +│ ├── registry.py : modelo de registry do guardrail +│ └── prompts/ : pasta com os prompts para rails que utilizam llm +├── tests/ +│ └── test_guardrails.py : código utilizado para pytest +├── scripts/ +│ └── run_tests.sh : bash script para testar os rails individualmente via pytest +├── requirements.txt : bibliotecas necessárias para o projeto +├── .env.example : variáveis de ambiente para configuração +└── README.md +``` + +Portanto, trataremos aqui neste material a implantação da estrutura de **nemo_guardrails_tracing_project** juntamente com o projeto de **Agente de IA**. + +Conceitualmente, teríamos este pipeline de deployment: + +```text +Pipeline do Agente de IA +↓ +aciona/consome +↓ +Pipeline do projeto NeMo Guardrails +↓ +publica pacote/artefato aprovado +↓ +Pipeline do Agente usa somente o resultado +``` + +O pipeline do agente não vê nem controla os steps internos do NeMo. Ele só consome um artefato versionado/aprovado. + +## Modelo recomendado + +### 1. Pipeline separado do NeMo + +No repositório nemo_guardrails_configuration, você cria um pipeline próprio que: + +```text +checkout nemo +↓ +instala dependências +↓ +roda testes de guardrails +↓ +gera pacote/artefato +↓ +publica artifact ou package +``` + +- O NeMo NÃO publica uma imagem final para produção +- Ele publica um artefato reutilizável + +Exemplo conceitual: + +O alvo do deployment é um container dentro de um cluster Kubernetes. Portanto, a idéia é: + +```text +Pod + └── container único + ├── código do agente + ├── library do NeMo + └── configs de guardrails +``` + +No modelo de library, teremos o seguinte: + +```text +Pipeline NeMo + ↓ +gera artifact (ou package) + ↓ +----------------------------------- +Pipeline Agente + ↓ +baixa artifact do NeMo + ↓ +EMBUTE no build + ↓ +gera imagem FINAL + ↓ +deploy no OKE +``` + +**azure-pipelines-nemo.yml** +```yaml +trigger: +- main + +pool: +vmImage: ubuntu-latest + +steps: +- checkout: self + +- task: UsePythonVersion@0 + inputs: + versionSpec: '3.11' + +- script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pytest -v + displayName: Test NeMo Guardrails + +- script: | + mkdir -p dist/nemo_guardrails_configuration + cp -r config dist/nemo_guardrails_configuration/ + cp -r src dist/nemo_guardrails_configuration/ + cp requirements.txt dist/nemo_guardrails_configuration/ + displayName: Package NeMo artifacts + +- publish: dist/nemo_guardrails_configuration + artifact: nemo-guardrails-package + displayName: Publish NeMo package +``` +Esse pipeline é controlado pelo time responsável pelo NeMo. + + +A arquitetura foi pensada para um projeto de guardrails que não é uma aplicação final isolada, mas uma camada reutilizável consumida por várias soluções, agentes, bots, APIs e motores conversacionais. + +--- + +## Arquitetura + +```text +Azure DevOps + | + |-- checkout do repositório + |-- instala Python 3.11 + |-- instala dependências + |-- executa pytest + |-- build Docker + |-- push para OCIR + |-- kubectl apply no OKE + | +OCI OKE + | + |-- Deployment guardrails-api + |-- Service guardrails-service + | +Aplicações consumidoras + | + |-- HTTP POST /chat +``` + +--- + +## Estrutura de arquivos + +```text +. +├── app/ +│ ├── __init__.py +│ └── main.py +├── config/ +│ ├── config.yml +│ ├── input.co +│ └── output.co +├── src/ +│ ├── __init__.py +│ ├── actions.py +│ ├── deterministic_rails.py +│ └── llm_rails.py +├── tests/ +│ ├── conftest.py +│ ├── test_api_contract.py +│ ├── test_e2e.py +│ └── test_structural.py +├── k8s/ +│ ├── deployment.yaml +│ ├── service.yaml +│ └── kustomization.yaml +├── scripts/ +│ ├── build_local.sh +│ ├── deploy_local.sh +│ └── test_local.sh +├── azure-pipelines.yml +├── Dockerfile +├── requirements.txt +└── README.md +``` + +--- + +## O que é o `./config` no CI/CD? + +No código NeMo Guardrails normalmente existe: + +```python +from nemoguardrails import LLMRails, RailsConfig + +config = RailsConfig.from_path("./config") +rails = LLMRails(config) +``` + +No Azure DevOps, o `./config` é o diretório `config/` versionado no repositório. + +Quando o pipeline executa: + +```yaml +- checkout: self +``` + +o repositório é baixado no agente do Azure DevOps. A partir daí, o diretório `config/` passa a existir no workspace do pipeline. + +Para evitar problemas com diretório corrente, este projeto usa path absoluto relativo ao arquivo Python: + +```python +BASE_DIR = Path(__file__).resolve().parent.parent +CONFIG_PATH = BASE_DIR / "config" +``` + +Assim, funciona localmente, no pytest, no Docker e no OKE. + +--- + +## Artefatos principais + +### `config/config.yml` + +Define o modelo principal usado pelo NeMo Guardrails. + +Neste exemplo usamos `engine: openai` porque muitos frameworks esperam a interface OpenAI. Em ambientes reais, esse endpoint pode apontar para um proxy OpenAI-compatible, inclusive um proxy para OCI Generative AI. + +### `config/input.co` + +Define rails de entrada. Em um projeto real, aqui entram fluxos de validação de entrada, por exemplo: + +- toxicidade +- prompt injection +- dados sensíveis +- fora de escopo + +### `config/output.co` + +Define rails de saída. Em um projeto real, aqui entram validações como: + +- resposta segura +- resposta aderente ao contexto +- resposta sem vazamento de dados +- resposta sem alucinação + +### `src/deterministic_rails.py` + +Contém regras determinísticas. Exemplos: + +- máscara de CPF +- validação de alçada +- checagem de protocolo +- normalização de texto + +### `src/llm_rails.py` + +Contém validações que podem depender de LLM ou judge. Exemplos: + +- qualidade de resposta +- groundedness +- classificação semântica +- verificação de tom + +### `src/actions.py` + +Local para registrar actions usadas pelo NeMo Guardrails. + +### `app/main.py` + +Expõe a solução como uma API HTTP: + +```text +GET /health +POST /chat +``` + +Isso permite que várias soluções consumam a arquitetura central de guardrails. + +--- + +## Execução local + +```bash +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +export OPENAI_API_KEY=sk-fake +pytest -v +uvicorn app.main:app --host 0.0.0.0 --port 8080 +``` + +Teste: + +```bash +curl -X POST http://localhost:8080/chat \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"quero cancelar plano"}]}' +``` + +--- + +## Docker local + +```bash +docker build -t guardrails-api:local . +docker run --rm -p 8080:8080 -e OPENAI_API_KEY=sk-fake guardrails-api:local +``` + +--- + +## Variáveis necessárias no Azure DevOps + +Configure estas variáveis no Pipeline ou em Variable Group: + +| Variável | Exemplo | Observação | +|---|---|---| +| `OCIR_REGION_KEY` | `gru` ou `sa-saopaulo-1` | usado para montar registry | +| `OCIR_REGISTRY` | `gru.ocir.io` | endpoint OCIR | +| `OCI_NAMESPACE` | `meunamespace` | namespace Object Storage/OCIR | +| `OCIR_REPOSITORY` | `guardrails/guardrails-api` | repositório da imagem | +| `OCIR_USER` | `tenancy/user` | usuário OCIR | +| `OCIR_PASSWORD` | `***` | auth token OCI | +| `OKE_KUBECONFIG_B64` | `***` | kubeconfig em base64 | +| `OPENAI_API_KEY` | `sk-fake` | pode ser fake quando usar proxy | + +--- + +## Como gerar o kubeconfig base64 + +Na sua máquina: + +```bash +oci ce cluster create-kubeconfig \ + --cluster-id \ + --file kubeconfig \ + --region sa-saopaulo-1 \ + --token-version 2.0.0 + +base64 -i kubeconfig | pbcopy +``` + +Cole o valor em `OKE_KUBECONFIG_B64` no Azure DevOps. + +--- + +## Pipeline Azure DevOps + +O arquivo `azure-pipelines.yml` faz: + +1. Checkout. +2. Setup Python 3.11. +3. Instala dependências. +4. Executa testes. +5. Build da imagem Docker. +6. Login no OCIR. +7. Push da imagem. +8. Aplica manifests Kubernetes no OKE. +9. Aguarda rollout. + +--- + +## Deploy no OKE + +Os manifests estão em `k8s/`. + +O pipeline substitui a imagem no deployment usando: + +```bash +kubectl set image deployment/guardrails-api guardrails-api=$IMAGE_URI -n guardrails +``` + +--- + +## Testes + +Os testes são versionados no repositório. O CI/CD não cria os testes; ele apenas executa. + +Este pacote inclui: + +- teste estrutural: valida carregamento do `RailsConfig`. +- teste de contrato da API: valida formato da requisição/resposta. +- teste E2E: simula entrada passando por API e rails. + +--- + +## Considerações de produção + +Para produção, recomenda-se evoluir: + +- Secrets via Kubernetes Secret ou OCI Vault. +- Ingress Controller em vez de LoadBalancer direto. +- HPA para autoscaling. +- Logs estruturados. +- Tracing com OpenTelemetry. +- Separação de testes determinísticos e testes com LLM. +- Política de versionamento de imagem por commit SHA. diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..141b906 --- /dev/null +++ b/app/main.py @@ -0,0 +1,56 @@ +import os +from pathlib import Path +from fastapi import FastAPI +from pydantic import BaseModel +from nemoguardrails import LLMRails, RailsConfig + +from src.deterministic_rails import mask_pii, detectar_toxicidade, detectar_out_of_scope + +BASE_DIR = Path(__file__).resolve().parent.parent +CONFIG_PATH = BASE_DIR / "config" + +os.environ.setdefault("OPENAI_API_KEY", "sk-fake") + +config = RailsConfig.from_path(str(CONFIG_PATH)) +rails = LLMRails(config) + +app = FastAPI(title="Guardrails API", version="1.0.0") + +class ChatRequest(BaseModel): + messages: list[dict] + +@app.get("/health") +def health(): + return {"status": "ok", "config_path": str(CONFIG_PATH)} + +@app.post("/chat") +def chat(payload: ChatRequest): + user_text = "" + for msg in reversed(payload.messages): + if msg.get("role") == "user": + user_text = msg.get("content", "") + break + + pii = mask_pii(user_text) + tox = detectar_toxicidade(user_text) + scope = detectar_out_of_scope(user_text) + + blocked = not tox.allowed + if blocked: + return { + "blocked": True, + "reason": tox.reason, + "rails": [pii.__dict__, tox.__dict__, scope.__dict__], + "output": "Mensagem bloqueada por política de segurança." + } + + # Para demo CI/CD, evitamos depender de chamada real ao LLM. + # Em produção, troque para rails.generate(...). + output = f"Solicitação recebida e validada: {pii.sanitized_text}" + + return { + "blocked": False, + "reason": "Allowed", + "rails": [pii.__dict__, tox.__dict__, scope.__dict__], + "output": output + } diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..8fcf556 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,63 @@ +trigger: +- main + +pool: + vmImage: 'ubuntu-latest' + +variables: + PYTHON_VERSION: '3.11' + IMAGE_NAME: 'guardrails-api' + IMAGE_TAG: '$(Build.SourceVersion)' + K8S_NAMESPACE: 'guardrails' + +steps: +- checkout: self + +- task: UsePythonVersion@0 + inputs: + versionSpec: '$(PYTHON_VERSION)' + displayName: 'Use Python $(PYTHON_VERSION)' + +- script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + displayName: 'Install Python dependencies' + +- script: | + export OPENAI_API_KEY="${OPENAI_API_KEY:-sk-fake}" + pytest -v --junitxml=test-results.xml + displayName: 'Run pytest' + +- task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'test-results.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() + displayName: 'Publish test results' + +- script: | + IMAGE_URI="$(OCIR_REGISTRY)/$(OCI_NAMESPACE)/$(OCIR_REPOSITORY):$(IMAGE_TAG)" + echo "##vso[task.setvariable variable=IMAGE_URI]$IMAGE_URI" + docker build -t "$IMAGE_URI" . + displayName: 'Build Docker image' + +- script: | + echo "$(OCIR_PASSWORD)" | docker login "$(OCIR_REGISTRY)" -u "$(OCIR_USER)" --password-stdin + docker push "$(IMAGE_URI)" + displayName: 'Push Docker image to OCIR' + +- script: | + mkdir -p ~/.kube + echo "$(OKE_KUBECONFIG_B64)" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + kubectl apply -f k8s/namespace.yaml + kubectl apply -f k8s/secret.yaml + + sed "s|REPLACE_IMAGE_URI|$(IMAGE_URI)|g" k8s/deployment.yaml | kubectl apply -f - + kubectl apply -f k8s/service.yaml + + kubectl rollout status deployment/guardrails-api -n $(K8S_NAMESPACE) --timeout=180s + kubectl get svc guardrails-service -n $(K8S_NAMESPACE) + displayName: 'Deploy to OCI OKE' diff --git a/config/config.yml b/config/config.yml new file mode 100644 index 0000000..f0a8173 --- /dev/null +++ b/config/config.yml @@ -0,0 +1,16 @@ +models: + - type: main + engine: openai + model: gpt-4o-mini + parameters: + api_key: ${OPENAI_API_KEY} + base_url: ${OPENAI_BASE_URL:-https://api.openai.com/v1} + +rails: + input: + flows: + - self check input + + output: + flows: + - self check output diff --git a/config/input.co b/config/input.co new file mode 100644 index 0000000..5ad8e10 --- /dev/null +++ b/config/input.co @@ -0,0 +1,5 @@ +define flow self check input + $allowed = execute check_input + if not $allowed + bot refuse to respond + stop diff --git a/config/output.co b/config/output.co new file mode 100644 index 0000000..6ebe419 --- /dev/null +++ b/config/output.co @@ -0,0 +1,5 @@ +define flow self check output + $allowed = execute check_output + if not $allowed + bot refuse to respond + stop diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..5eaf6f3 --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: guardrails-api + namespace: guardrails +spec: + replicas: 2 + selector: + matchLabels: + app: guardrails-api + template: + metadata: + labels: + app: guardrails-api + spec: + containers: + - name: guardrails-api + image: REPLACE_IMAGE_URI + imagePullPolicy: Always + ports: + - containerPort: 8080 + env: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: guardrails-secrets + key: OPENAI_API_KEY + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 20 + periodSeconds: 20 diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 0000000..1034569 --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,5 @@ +resources: + - namespace.yaml + - secret.yaml + - deployment.yaml + - service.yaml diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..47efc03 --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: guardrails diff --git a/k8s/secret.yaml b/k8s/secret.yaml new file mode 100644 index 0000000..5f9c1fd --- /dev/null +++ b/k8s/secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: guardrails-secrets + namespace: guardrails +type: Opaque +stringData: + OPENAI_API_KEY: sk-fake diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..8414123 --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: guardrails-service + namespace: guardrails +spec: + type: LoadBalancer + selector: + app: guardrails-api + ports: + - name: http + port: 80 + targetPort: 8080 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5ba6f7e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +nemoguardrails[openai] +pytest==8.3.4 +httpx==0.28.1 +pydantic>=2 diff --git a/scripts/build_local.sh b/scripts/build_local.sh new file mode 100644 index 0000000..33e781e --- /dev/null +++ b/scripts/build_local.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +docker build -t guardrails-api:local . diff --git a/scripts/deploy_local.sh b/scripts/deploy_local.sh new file mode 100644 index 0000000..fc544d0 --- /dev/null +++ b/scripts/deploy_local.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +kubectl apply -k k8s +kubectl rollout status deployment/guardrails-api -n guardrails diff --git a/scripts/test_local.sh b/scripts/test_local.sh new file mode 100644 index 0000000..c91441c --- /dev/null +++ b/scripts/test_local.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +export OPENAI_API_KEY=${OPENAI_API_KEY:-sk-fake} +pytest -v diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/actions.py b/src/actions.py new file mode 100644 index 0000000..0ecbbb5 --- /dev/null +++ b/src/actions.py @@ -0,0 +1,15 @@ +from src.deterministic_rails import detectar_toxicidade, detectar_out_of_scope + +def check_input(context=None): + text = "" + if context and "user_message" in context: + text = context["user_message"] + + tox = detectar_toxicidade(text) + if not tox.allowed: + return False + + return True + +def check_output(context=None): + return True diff --git a/src/deterministic_rails.py b/src/deterministic_rails.py new file mode 100644 index 0000000..3e09f70 --- /dev/null +++ b/src/deterministic_rails.py @@ -0,0 +1,44 @@ +import re +from dataclasses import dataclass + +@dataclass +class RailResult: + allowed: bool + reason: str = "" + sanitized_text: str | None = None + code: str | None = None + mechanism: str = "deterministic" + data: dict | None = None + +def mask_pii(text: str) -> RailResult: + cpf_pattern = r"\b\d{3}\.\d{3}\.\d{3}-\d{2}\b" + sanitized = re.sub(cpf_pattern, "[CPF_MASCARADO]", text) + return RailResult( + allowed=True, + reason="PII masked when present.", + sanitized_text=sanitized, + code="MSK", + data={"pii_found": sanitized != text}, + ) + +def detectar_toxicidade(text: str) -> RailResult: + toxic_terms = ["vai se ferrar", "idiota", "porcaria"] + is_toxic = any(t in text.lower() for t in toxic_terms) + return RailResult( + allowed=not is_toxic, + reason="Toxic content detected." if is_toxic else "No toxic content detected.", + sanitized_text=text, + code="TOX", + data={"toxic": is_toxic}, + ) + +def detectar_out_of_scope(text: str) -> RailResult: + allowed_terms = ["plano", "cancelar", "fatura", "serviço", "atendimento", "protocolo"] + is_allowed = any(t in text.lower() for t in allowed_terms) + return RailResult( + allowed=is_allowed, + reason="Out of scope." if not is_allowed else "In scope.", + sanitized_text=text, + code="OOS", + data={"in_scope": is_allowed}, + ) diff --git a/src/llm_rails.py b/src/llm_rails.py new file mode 100644 index 0000000..bf93dbb --- /dev/null +++ b/src/llm_rails.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + +@dataclass +class JudgeResult: + allowed: bool + score: int + reason: str + mechanism: str = "llm_judge" + +def avaliar_qualidade_resposta(pergunta: str, resposta: str) -> JudgeResult: + # Exemplo determinístico para CI. + # Em ambiente real, pode chamar um LLM judge. + score = 5 if len(resposta.strip()) > 30 else 3 + return JudgeResult( + allowed=score >= 4, + score=score, + reason="Resposta avaliada por critério simplificado para CI.", + ) diff --git a/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cacd0b759e7edf2ffa5622566f0df8d9491d1d5 GIT binary patch literal 1244 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 XoewAP_bw$~s0pcm_pl}nghu`Wqn+`s literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_structural.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_structural.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c18a36294c2134b48b0f5d8dd7050721805be006 GIT binary patch literal 3302 zcmeHJ&2QsG6!*v2N#k^3fn62|Ql!;NHAqR*(j`p-VzumTDTo6|T_nqm-NmUBM`H(? z;7ADVq5lCz+C#6XXZ{8bT#}|$kS`oKaYNhNo_KF2{$N#6t}Cr&zxlm4zj^cQnddjZ zEiV@YxV|`e;*>=}_!E`*k{00n9RNQHmSBmlur0Dy>dI|N6uM+v5d}+nBy6je3^F$q zb@CSu+-ifrZY&N3J-4NcPgTb+_q;$CN7rwy^oOgu zU*?uFs_VKRDb#daUzZ{ox>2l;u9&`W(|}PnfC9!1Js%Z5>JJ0k4-At=SBMv`!lMkM%eZjwfq@Z>l-F5xNv zlc&Z!OL?rYgw16VHjVkBgw4fc<93m-A|@?7GCTwxWvo84j_QkbjKA4)y^gsT>-c=G zBNWv9R;2Qu{;0s)%CPH>9%QOz4GL`GJ1nh;${(;2T90i2z_hXwSK=z`UekTO%pnw8 zy<+b>PQyihW*^4C+A@S12B6upX~b{L><;M+9dZ<(*!C5LDXUXvrG{9754n zxjPEwK%hkwODOP&LGc$wUqbOR2t7~VMEnYfY++r`DvQF(U-@>n=l!g*$xj2k7}sh( z2FnUC2zm`CC}nG?YHmBMqV30Gd$v|^X())~n18$h5@RT$$ETo+f@SDJi zwwb}H8*T&%PU!2fps*wJ@fnfr$> \ + --file kubeconfig \ + --region sa-saopaulo-1 \ + --token-version 2.0.0 + +### Converter para base64 + +base64 kubeconfig + +Salvar em OKE_KUBECONFIG_B64 + +### Uso no pipeline + +echo "$(OKE_KUBECONFIG_B64)" | base64 -d > ~/.kube/config + +--- + +## 🐳 ETAPA 3 — OCIR + +Gerar Auth Token no OCI + +User → Auth Tokens + +Pipeline: + +echo "$(OCIR_PASSWORD)" | docker login "$(OCIR_REGISTRY)" \ + -u "$(OCIR_USER)" --password-stdin + +--- + +## 🔐 ETAPA 4 — OCI Vault + +Criar secret no OCI Vault + +Security → Vault → Secrets + +Criar Dynamic Group: + +ALL {{resource.type = 'instance'}} + +Criar Policy: + +Allow dynamic-group to read secret-bundles in compartment + +Buscar secret: + +oci secrets secret-bundle get \ + --secret-id \ + --query "data.\"secret-bundle-content\".content" \ + --raw-output | base64 --decode + +--- + +## 🔄 Pipeline exemplo + +pip install -r requirements.txt +pytest -v +docker build +docker push +kubectl apply + +--- + +## ⚠️ Boas práticas + +- Não commitar secrets +- Usar variáveis secret +- Usar Vault para produção + +--- + +## 🚀 Conclusão + +Pipeline seguro com Azure DevOps + OCI + OKE.