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 0000000..3cacd0b
Binary files /dev/null and b/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc differ
diff --git a/tests/__pycache__/test_api_contract.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_api_contract.cpython-312-pytest-9.0.2.pyc
new file mode 100644
index 0000000..6cfaedd
Binary files /dev/null and b/tests/__pycache__/test_api_contract.cpython-312-pytest-9.0.2.pyc differ
diff --git a/tests/__pycache__/test_e2e.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_e2e.cpython-312-pytest-9.0.2.pyc
new file mode 100644
index 0000000..87b41c6
Binary files /dev/null and b/tests/__pycache__/test_e2e.cpython-312-pytest-9.0.2.pyc differ
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 0000000..c18a362
Binary files /dev/null and b/tests/__pycache__/test_structural.cpython-312-pytest-9.0.2.pyc differ
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..bf1b0cf
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,16 @@
+import os
+import pytest
+from pathlib import Path
+from nemoguardrails import RailsConfig, LLMRails
+
+os.environ.setdefault("OPENAI_API_KEY", "sk-fake")
+
+@pytest.fixture(scope="session")
+def project_root():
+ return Path(__file__).resolve().parent.parent
+
+@pytest.fixture(scope="session")
+def rails(project_root):
+ config_path = project_root / "config"
+ config = RailsConfig.from_path(str(config_path))
+ return LLMRails(config)
diff --git a/tests/test_api_contract.py b/tests/test_api_contract.py
new file mode 100644
index 0000000..3beaca2
--- /dev/null
+++ b/tests/test_api_contract.py
@@ -0,0 +1,19 @@
+from fastapi.testclient import TestClient
+from app.main import app
+
+client = TestClient(app)
+
+def test_health():
+ r = client.get("/health")
+ assert r.status_code == 200
+ assert r.json()["status"] == "ok"
+
+def test_chat_allowed_contract():
+ r = client.post("/chat", json={
+ "messages": [{"role": "user", "content": "quero cancelar plano"}]
+ })
+ assert r.status_code == 200
+ body = r.json()
+ assert "blocked" in body
+ assert "output" in body
+ assert "rails" in body
diff --git a/tests/test_e2e.py b/tests/test_e2e.py
new file mode 100644
index 0000000..58f3d58
--- /dev/null
+++ b/tests/test_e2e.py
@@ -0,0 +1,21 @@
+from fastapi.testclient import TestClient
+from app.main import app
+
+client = TestClient(app)
+
+def test_e2e_toxicidade_bloqueada():
+ r = client.post("/chat", json={
+ "messages": [{"role": "user", "content": "vai se ferrar"}]
+ })
+ assert r.status_code == 200
+ body = r.json()
+ assert body["blocked"] is True
+
+def test_e2e_pii_mascarado():
+ r = client.post("/chat", json={
+ "messages": [{"role": "user", "content": "Meu CPF é 169.323.728-86 e quero cancelar plano"}]
+ })
+ assert r.status_code == 200
+ body = r.json()
+ assert body["blocked"] is False
+ assert "169.323.728-86" not in body["output"]
diff --git a/tests/test_structural.py b/tests/test_structural.py
new file mode 100644
index 0000000..a5d686f
--- /dev/null
+++ b/tests/test_structural.py
@@ -0,0 +1,7 @@
+def test_config_loads(rails):
+ assert rails is not None
+
+def test_config_directory_exists(project_root):
+ assert (project_root / "config" / "config.yml").exists()
+ assert (project_root / "config" / "input.co").exists()
+ assert (project_root / "config" / "output.co").exists()
diff --git a/tutorial_azure_oci_oke.md b/tutorial_azure_oci_oke.md
new file mode 100644
index 0000000..abfe666
--- /dev/null
+++ b/tutorial_azure_oci_oke.md
@@ -0,0 +1,115 @@
+# 🔐 Tutorial Completo — Configuração de Credenciais OCI no Azure DevOps (CI/CD com OKE)
+
+## 📌 Introdução
+
+Este documento descreve como configurar credenciais para integração entre:
+
+- Azure DevOps (CI/CD)
+- Oracle Cloud Infrastructure (OCI)
+- OCI Registry (OCIR)
+- Oracle Kubernetes Engine (OKE)
+- OCI Vault
+
+---
+
+## 🧱 Arquitetura
+
+Azure DevOps → Docker → OCIR → OKE → Aplicação
+
+---
+
+## ⚙️ ETAPA 1 — Variáveis no Azure DevOps
+
+Pipeline → Variables ou Library → Variable Groups
+
+Variáveis:
+
+- OCIR_USER (secret)
+- OCIR_PASSWORD (secret)
+- OCIR_REGISTRY
+- OCI_NAMESPACE
+- OCIR_REPOSITORY
+- OPENAI_API_KEY (secret)
+- OKE_KUBECONFIG_B64 (secret)
+
+---
+
+## ☸️ ETAPA 2 — Kubernetes (OKE)
+
+### Gerar kubeconfig
+
+oci ce cluster create-kubeconfig \
+ --cluster-id \
+ --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.