mirror of
https://github.com/hoshikawa2/nemo_guardrails_ci_cd.git
synced 2026-07-09 16:24:20 +00:00
first commit
This commit is contained in:
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -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
|
||||||
9
.idea/Nemo.iml
generated
Normal file
9
.idea/Nemo.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_24" default="true" project-jdk-name="24" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Nemo.iml" filepath="$PROJECT_DIR$/.idea/Nemo.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
24
.oca/custom_code_review_guidelines.txt
Normal file
24
.oca/custom_code_review_guidelines.txt
Normal file
@@ -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.
|
||||||
|
# <example>
|
||||||
|
# *Comment:
|
||||||
|
# Category: Minor
|
||||||
|
# Issue: Use streams instead of a loop for better readability.
|
||||||
|
# Code Block:
|
||||||
|
#
|
||||||
|
# ```java
|
||||||
|
# // Calculate squares of numbers
|
||||||
|
# List<Integer> squares = new ArrayList<>();
|
||||||
|
# for (int number : numbers) {
|
||||||
|
# squares.add(number * number);
|
||||||
|
# }
|
||||||
|
# ```
|
||||||
|
# Recommendation:
|
||||||
|
#
|
||||||
|
# ```java
|
||||||
|
# // Calculate squares of numbers
|
||||||
|
# List<Integer> squares = Arrays.stream(numbers)
|
||||||
|
# .map(n -> n * n) // Map each number to its square
|
||||||
|
# .toList();
|
||||||
|
# ```
|
||||||
|
# </example>
|
||||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -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"]
|
||||||
819
README.md
819
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
|
### 2.1 Fluxo funcional dos guardrails
|
||||||
- `config.yml` / `rails.yaml`
|
|
||||||
- `guardrails.yaml`
|
|
||||||
- `prompts/`
|
|
||||||
- `flows.co`
|
|
||||||
- `input.co`
|
|
||||||
- `output.co`
|
|
||||||
|
|
||||||
### 2. Código complementar
|
```text
|
||||||
- Actions Python (`actions.py`)
|
User Input
|
||||||
- Integrações externas (APIs, tools)
|
↓
|
||||||
|
Input Rails
|
||||||
---
|
├─ Regex: PII Masking
|
||||||
|
├─ LLM: Toxicidade
|
||||||
## Problema Principal
|
└─ LLM: Out-of-Scope
|
||||||
|
↓
|
||||||
Diferente de código tradicional:
|
LLM principal via NeMo Guardrails
|
||||||
|
↓
|
||||||
- Pequenas mudanças em `.co` ou `.yaml` podem quebrar o comportamento
|
Output Rails
|
||||||
- Não há erro de compilação
|
├─ Compliance Anatel
|
||||||
- Falhas são **semânticas**, não sintáticas
|
├─ Verbalização Prematura
|
||||||
|
└─ Groundedness
|
||||||
---
|
↓
|
||||||
|
Python Rules
|
||||||
## Pipeline CI/CD Recomendado
|
├─ Alçada de Ajuste
|
||||||
|
└─ Consistência Histórica
|
||||||
### Etapa 1: Validação Sintática
|
↓
|
||||||
|
Execução de API / Backend
|
||||||
Validar YAML e estrutura:
|
↓
|
||||||
|
Supervisor VAS Avulso
|
||||||
```bash
|
↓
|
||||||
yamllint .
|
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
|
```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")
|
### 5.9 `src/registry.py`
|
||||||
rails = LLMRails(config)
|
|
||||||
|
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
|
```text
|
||||||
tests = [
|
Commit no repo NeMo
|
||||||
{"input": "xingar atendente", "expected_blocked": True},
|
↓
|
||||||
{"input": "cancelar plano", "expected_blocked": False}
|
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
|
```python
|
||||||
for t in tests:
|
from importlib.resources import files
|
||||||
response = rails.generate(messages=[{"role": "user", "content": t["input"]}])
|
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
|
pool:
|
||||||
- Comparação antes/depois
|
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
|
- script: |
|
||||||
- Toxicidade
|
python -m pip install --upgrade pip
|
||||||
- Out-of-scope
|
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'
|
||||||
|
|
||||||
---
|
- script: |
|
||||||
|
python -m twine upload --repository company-python-feed --config-file $(PYPIRC_PATH) dist/*
|
||||||
## Estratégia de Versionamento
|
displayName: 'Publish package to Azure Artifacts'
|
||||||
|
|
||||||
### Versionar separadamente:
|
|
||||||
|
|
||||||
- Código
|
|
||||||
- Configuração de guardrails
|
|
||||||
|
|
||||||
Exemplo:
|
|
||||||
|
|
||||||
```
|
|
||||||
/app
|
|
||||||
/config
|
|
||||||
/tests
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deploy
|
## 10. Projeto Agente consumindo o package NeMo
|
||||||
|
|
||||||
### Opção 1: Embutido no container
|
### 10.1 Estrutura do projeto Agente
|
||||||
|
|
||||||
```Dockerfile
|
```text
|
||||||
COPY config/ /app/config/
|
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
|
```txt
|
||||||
- Volume mount
|
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
|
pool:
|
||||||
- Tracing (OpenTelemetry)
|
vmImage: 'ubuntu-latest'
|
||||||
- Métricas de bloqueio
|
|
||||||
|
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
|
| Variável | Exemplo | Tipo |
|
||||||
- Misturar lógica de negócio com guardrails
|
|---|---|---|
|
||||||
- Não versionar `.co`
|
| `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.
|
||||||
|
|||||||
462
README_BKP.md
Normal file
462
README_BKP.md
Normal file
@@ -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 <OCID_DO_CLUSTER> \
|
||||||
|
--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.
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
56
app/main.py
Normal file
56
app/main.py
Normal file
@@ -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
|
||||||
|
}
|
||||||
63
azure-pipelines.yml
Normal file
63
azure-pipelines.yml
Normal file
@@ -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'
|
||||||
16
config/config.yml
Normal file
16
config/config.yml
Normal file
@@ -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
|
||||||
5
config/input.co
Normal file
5
config/input.co
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
define flow self check input
|
||||||
|
$allowed = execute check_input
|
||||||
|
if not $allowed
|
||||||
|
bot refuse to respond
|
||||||
|
stop
|
||||||
5
config/output.co
Normal file
5
config/output.co
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
define flow self check output
|
||||||
|
$allowed = execute check_output
|
||||||
|
if not $allowed
|
||||||
|
bot refuse to respond
|
||||||
|
stop
|
||||||
39
k8s/deployment.yaml
Normal file
39
k8s/deployment.yaml
Normal file
@@ -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
|
||||||
5
k8s/kustomization.yaml
Normal file
5
k8s/kustomization.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- secret.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
4
k8s/namespace.yaml
Normal file
4
k8s/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: guardrails
|
||||||
8
k8s/secret.yaml
Normal file
8
k8s/secret.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: guardrails-secrets
|
||||||
|
namespace: guardrails
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
OPENAI_API_KEY: sk-fake
|
||||||
13
k8s/service.yaml
Normal file
13
k8s/service.yaml
Normal file
@@ -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
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -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
|
||||||
3
scripts/build_local.sh
Normal file
3
scripts/build_local.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
docker build -t guardrails-api:local .
|
||||||
4
scripts/deploy_local.sh
Normal file
4
scripts/deploy_local.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
kubectl apply -k k8s
|
||||||
|
kubectl rollout status deployment/guardrails-api -n guardrails
|
||||||
4
scripts/test_local.sh
Normal file
4
scripts/test_local.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
export OPENAI_API_KEY=${OPENAI_API_KEY:-sk-fake}
|
||||||
|
pytest -v
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
15
src/actions.py
Normal file
15
src/actions.py
Normal file
@@ -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
|
||||||
44
src/deterministic_rails.py
Normal file
44
src/deterministic_rails.py
Normal file
@@ -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},
|
||||||
|
)
|
||||||
18
src/llm_rails.py
Normal file
18
src/llm_rails.py
Normal file
@@ -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.",
|
||||||
|
)
|
||||||
BIN
tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_api_contract.cpython-312-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_api_contract.cpython-312-pytest-9.0.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_e2e.cpython-312-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_e2e.cpython-312-pytest-9.0.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_structural.cpython-312-pytest-9.0.2.pyc
Normal file
BIN
tests/__pycache__/test_structural.cpython-312-pytest-9.0.2.pyc
Normal file
Binary file not shown.
16
tests/conftest.py
Normal file
16
tests/conftest.py
Normal file
@@ -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)
|
||||||
19
tests/test_api_contract.py
Normal file
19
tests/test_api_contract.py
Normal file
@@ -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
|
||||||
21
tests/test_e2e.py
Normal file
21
tests/test_e2e.py
Normal file
@@ -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"]
|
||||||
7
tests/test_structural.py
Normal file
7
tests/test_structural.py
Normal file
@@ -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()
|
||||||
115
tutorial_azure_oci_oke.md
Normal file
115
tutorial_azure_oci_oke.md
Normal file
@@ -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 <OCID_CLUSTER> \
|
||||||
|
--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 <group> to read secret-bundles in compartment <compartment>
|
||||||
|
|
||||||
|
Buscar secret:
|
||||||
|
|
||||||
|
oci secrets secret-bundle get \
|
||||||
|
--secret-id <OCID_SECRET> \
|
||||||
|
--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.
|
||||||
Reference in New Issue
Block a user