Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install pre-commit uv
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
uv sync

- name: Run pre-commit hooks
run: |
Expand Down Expand Up @@ -55,9 +55,7 @@ jobs:

# Server tests
- name: Install server test dependencies
run: |
echo "Installing pytest and dependencies for server tests..."
pip install pytest pytest-asyncio
run: uv sync --group dev
- name: Run server unit tests
run: |
echo "Running server unit tests..."
Expand Down
15 changes: 12 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
repos:

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
- id: check-merge-conflict
- id: debug-statements

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.8
rev: v0.15.5
hooks:
# Run the linter.
- id: ruff
args: [ --fix, --line-length=80 ]
args: [ --fix ]
# Run the formatter.
- id: ruff-format
args: [ --line-length=80 ]
11 changes: 5 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ COPY --from=docker.io/astral/uv:latest /uv /uvx /bin/
WORKDIR /app

# Copy Python dependencies and source
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY pyproject.toml .
RUN uv sync --no-dev
RUN mkdir -p src/resources

COPY src/ ./src/
Expand All @@ -27,8 +27,7 @@ COPY resources ./src/resources/
COPY plugins ./plugins/

# Install plugin-specific dependencies based on PLUGIN_DEPS argument
# This depends on plugins having requirements.txt in the directory
# and could be subject to error depending on how a plugin is structured.
# Plugins must have a pyproject.toml in their directory.
# Usage: docker build --build-arg PLUGIN_DEPS="nemo" -t plugins-adapter .
# Or for multiple: docker build --build-arg PLUGIN_DEPS="nemo,other_plugin" -t plugins-adapter .
RUN if [ -n "$PLUGIN_DEPS" ]; then \
Expand All @@ -40,7 +39,7 @@ RUN if [ -n "$PLUGIN_DEPS" ]; then \
req_file="$plugin_dir/pyproject.toml"; \
if [ -f "$req_file" ]; then \
echo "Installing dependencies from $plugin_dir"; \
pip install --no-cache-dir $plugin_dir; \
uv pip install --no-cache $plugin_dir; \
else \
echo "Warning: No pyproject.toml found for plugin '$plugin' at $req_file"; \
fi; \
Expand All @@ -54,4 +53,4 @@ RUN if [ -n "$PLUGIN_DEPS" ]; then \
EXPOSE 50052

# Run the server
CMD ["python", "src/server.py"]
CMD ["uv", "run", "python", "src/server.py"]
13 changes: 6 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build load all deploy exec log
.PHONY: build load all deploy exec log lint
.IGNORE: delete


Expand Down Expand Up @@ -26,7 +26,7 @@ else
IMAGE_PUSH := $(IMAGE_BASE):$(IMAGE_TAG)
endif

# Build the combined broker and router
# Build the combined broker and router
build:
$(CONTAINER_RUNTIME) build -t $(IMAGE_LOCAL) . --build-arg PLUGIN_DEPS=${PLUGIN_DEPS}

Expand All @@ -49,14 +49,13 @@ deploy:
kubectl apply -f filter.yaml

lint:
uv run ruff check --fix
uv run ruff format
pre-commit run --all-files

redeploy: delete deploy

push_image_quay: build
$(CONTAINER_RUNTIME) tag $(IMAGE_LOCAL) quay.io/julian_stephen/$(IMAGE_PUSH)
$(CONTAINER_RUNTIME) push quay.io/julian_stephen/$(IMAGE_PUSH)
$(CONTAINER_RUNTIME) tag $(IMAGE_LOCAL) quay.io/julian_stephen/$(IMAGE_PUSH)
$(CONTAINER_RUNTIME) push quay.io/julian_stephen/$(IMAGE_PUSH)

all: build load redeploy
@echo "All done!"
Expand All @@ -65,7 +64,7 @@ port-forward-nemo:
kubectl port-forward -n istio-system service/nemo-guardrails-service 8000:8000

deploy_quay: IMAGE=quay.io/julian_stephen/$(IMAGE_PUSH)
deploy_quay:
deploy_quay:
$(CONTAINER_RUNTIME) pull $(IMAGE)
$(CONTAINER_RUNTIME) tag $(IMAGE) $(IMAGE_LOCAL)
kind load docker-image $(IMAGE_LOCAL) --name mcp-gateway
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ An Envoy external processor (ext-proc) for configuring and invoking guardrails i

## Full Dev Build

1. **Build Protocol Buffers**
1. **Install uv** (if not already installed): https://docs.astral.sh/uv/getting-started/installation/

2. **Install dependencies and build Protocol Buffers**
```bash
python3 -m venv .venv
source .venv/bin/activate
uv sync --group proto
./proto-build.sh
```

2. **Verify** `src/` contains: `/envoy`, `/validate`, `/xds`, `/udpa`
3. **Verify** `src/` contains: `/envoy`, `/validate`, `/xds`, `/udpa`

3. **Deploy to kind cluster**
4. **Deploy to kind cluster**
```bash
make all PLUGIN_DEPS=nemocheck #replace nemocheck with comma seperated list of plugins to include other plugins
# Replace nemocheck with a comma-separated list of plugins to include other plugins
make all PLUGIN_DEPS=nemocheck
```

See [detailed build instructions](./docs/build.md) for manual build steps.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/diagrams/plugin-deployment.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions docs/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ If you prefer not to use `proto-build.sh`:
See [instructions if needed](https://betterproto.github.io/python-betterproto2/getting-started/).

```sh
pip install -r requirements-proto.txt
uv sync --group proto
```

2. **Build Envoy protobufs**
Expand Down Expand Up @@ -51,6 +51,6 @@ If you prefer not to use `proto-build.sh`:
Ensure by this point you have the `envoy`, `validate`, `xds`, `udpa` python protobuf folders in `src/` to run the external processor server (`server.py`).

```sh
pip install -r requirements.txt
python src/server.py
uv sync
uv run python src/server.py
```
2 changes: 1 addition & 1 deletion ext-proc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ spec:
- name: PYTHONPATH
value: "./"
ports:
- containerPort: 50052
- containerPort: 50052
40 changes: 11 additions & 29 deletions plugins/examples/nemo/nemo_wrapper_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
PluginConfig,
PluginContext,
ToolHookType,
ToolPreInvokePayload,
ToolPreInvokeResult,
ToolPostInvokePayload,
ToolPostInvokeResult,
ToolPreInvokePayload,
ToolPreInvokeResult,
)
from nemoguardrails import LLMRails, RailsConfig

Expand All @@ -30,21 +30,15 @@ def __init__(self, config: PluginConfig) -> None:
config: Plugin configuration
"""
super().__init__(config)
logger.info(
f"[NemoWrapperPlugin] Initializing plugin with config: {config.config}"
)
logger.info(f"[NemoWrapperPlugin] Initializing plugin with config: {config.config}")
# NOTE: very hardcoded
nemo_config = RailsConfig.from_path(
os.path.join(
os.getcwd(), "plugins", "examples", "nemo", "pii_detect_config"
)
os.path.join(os.getcwd(), "plugins", "examples", "nemo", "pii_detect_config")
)
self._rails = LLMRails(nemo_config)
logger.info("[NemoWrapperPlugin] Plugin initialized successfully")

async def tool_pre_invoke(
self, payload: ToolPreInvokePayload, context: PluginContext
) -> ToolPreInvokeResult:
async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult:
"""Plugin hook run before a tool is invoked.

Args:
Expand All @@ -59,31 +53,19 @@ async def tool_pre_invoke(
payload_args = payload.args
if payload_args:
try:
rails_response = await self._rails.generate_async(
messages=[{"role": "user", "content": payload_args}]
)
rails_response = await self._rails.generate_async(messages=[{"role": "user", "content": payload_args}])
except asyncio.CancelledError: # asyncio.exceptions.CancelledError is thrown by nemo, need to catch
logging.exception(
"An error occurred in the nemo plugin except block:"
)
logging.exception("An error occurred in the nemo plugin except block:")
finally:
logger.warning("[NemoWrapperPlugin] Async rails executed")
logger.warning(rails_response)
if rails_response and "PII detected" in rails_response["content"]:
logger.warning(
"[NemoWrapperPlugin] PII detected, stopping processing"
)
return ToolPreInvokeResult(
modified_payload=payload, continue_processing=False
)
logger.warning("[NemoWrapperPlugin] PII detected, stopping processing")
return ToolPreInvokeResult(modified_payload=payload, continue_processing=False)
logger.warning("[NemoWrapperPlugin] No PII detected, continuing")
return ToolPreInvokeResult(
modified_payload=payload, continue_processing=True
)
return ToolPreInvokeResult(modified_payload=payload, continue_processing=True)

async def tool_post_invoke(
self, payload: ToolPostInvokePayload, context: PluginContext
) -> ToolPostInvokeResult:
async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult:
"""Plugin hook run after a tool is invoked.

Args:
Expand Down
1 change: 1 addition & 0 deletions plugins/examples/nemo/pii_detect_config/actions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import re

from nemoguardrails.actions import action

EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
Expand Down
7 changes: 7 additions & 0 deletions plugins/examples/nemo/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "nemo-plugin"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
"nemoguardrails==0.19.0",
]
1 change: 0 additions & 1 deletion plugins/examples/nemo/requirements.txt

This file was deleted.

12 changes: 6 additions & 6 deletions plugins/examples/nemocheck/k8deploy/config-tools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ data:
openai_api_base: "http://0.0.0.0:4000"
model_name: "meta-llama/llama-3-3-70b-instruct"
api_key: "None"

passthrough: true

rails:
input:
flows:
Expand Down Expand Up @@ -79,21 +79,21 @@ data:
"""Block forbidden topics."""
user_message = context.get("user_message", "").lower()
forbidden_words = ["chatgpt", "openai", "claude"]

for word in forbidden_words:
if word in user_message:
return "blocked"

return "allowed"

@action(is_system_action=True)
async def check_output_length(context: dict = {}):
"""Block responses over 100 words."""
bot_msg = context.get("bot_message", "")

if len(bot_msg.split()) > 100:
return "blocked"

return "allowed"

@action(is_system_action=True)
Expand Down
Loading