Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ jobs:
run: uv sync
working-directory: backend

- name: Configure Guardrails CLI
env:
GUARDRAILS_HUB_API_KEY: ${{ secrets.GUARDRAILS_HUB_API_KEY }}
run: |
source .venv/bin/activate
guardrails configure \
--token "$GUARDRAILS_HUB_API_KEY" \
--enable-metrics false
working-directory: backend

- name: Install Guardrails hub validators
env:
GUARDRAILS_HUB_API_KEY: ${{ secrets.GUARDRAILS_HUB_API_KEY }}
Expand Down
6 changes: 3 additions & 3 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,17 +227,17 @@ Set the resulting digest as `AUTH_TOKEN` in your `.env` / `.env.test`.

## Multi-tenant API Key Configuration

Ban List APIs use `X-API-KEY` auth instead of bearer token auth.
Ban List and Topic Relevance Config APIs use `X-API-KEY` auth instead of bearer token auth.

Required environment variables:
- `KAAPI_AUTH_URL`: Base URL of the Kaapi auth service used to verify API keys.
- `KAAPI_AUTH_TIMEOUT`: Timeout in seconds for auth verification calls.

At runtime, the backend calls:
- `GET {KAAPI_AUTH_URL}/apikeys/verify`
- Header: `X-API-KEY: ApiKey <token>`
- Header: `X-API-KEY: <token>`

If verification succeeds, tenant's scope (`organization_id`, `project_id`) is resolved from the auth response and applied to Ban List CRUD operations.
If verification succeeds, tenant's scope (`organization_id`, `project_id`) is resolved from the auth response and applied to tenant-scoped CRUD operations (for example Ban Lists and Topic Relevance Configs).

## Guardrails AI Setup
1. Ensure that the .env file contains the correct value from `GUARDRAILS_HUB_API_KEY`. The key can be fetched from [here](https://hub.guardrailsai.com/keys).
Expand Down
54 changes: 54 additions & 0 deletions backend/app/alembic/versions/006_added_topic_relevance_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Added topic_relevance table

Revision ID: 006
Revises: 005
Create Date: 2026-03-05 00:00:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "006"
down_revision = "005"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"topic_relevance",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("organization_id", sa.Integer(), nullable=False),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("description", sa.String(), nullable=False),
sa.Column("prompt_version", sa.Integer(), nullable=False),
sa.Column("configuration", sa.Text(), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"name",
"organization_id",
"project_id",
name="uq_topic_relevance_name_org_project",
),
)

op.create_index(
"idx_topic_relevance_organization", "topic_relevance", ["organization_id"]
)
op.create_index("idx_topic_relevance_project", "topic_relevance", ["project_id"])
op.create_index(
"idx_topic_relevance_prompt_version", "topic_relevance", ["prompt_version"]
)
op.create_index("idx_topic_relevance_is_active", "topic_relevance", ["is_active"])


def downgrade() -> None:
op.drop_table("topic_relevance")
99 changes: 93 additions & 6 deletions backend/app/api/API_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This guide explains how to use the current API surface for:
- Runtime validator discovery
- Guardrail execution
- Ban list CRUD for multi-tenant projects
- Topic relevance config CRUD for multi-tenant projects

## Base URL and Version

Expand All @@ -23,7 +24,7 @@ This API currently uses two auth modes:
- Used by validator config and guardrails endpoints.
- The server validates your plaintext bearer token against a SHA-256 digest stored in `AUTH_TOKEN`.
2. multi-tenant API key auth (`X-API-KEY: <token>`)
- Used by ban list endpoints.
- Used by ban list and topic relevance config endpoints.
- The API key is verified against `KAAPI_AUTH_URL` and resolves tenant's scope (`organization_id`, `project_id`).

Notes:
Expand Down Expand Up @@ -99,7 +100,7 @@ Endpoint:
Optional filters:
- `ids=<uuid>&ids=<uuid>`
- `stage=input|output`
- `type=uli_slur_match|pii_remover|gender_assumption_bias|ban_list`
- `type=uli_slur_match|pii_remover|gender_assumption_bias|ban_list|llm_critic|topic_relevance`

Example:

Expand Down Expand Up @@ -182,6 +183,8 @@ Request fields:
Important:
- Runtime validators use `on_fail`.
- If you pass objects from config APIs, server normalization supports `on_fail_action` and strips non-runtime fields.
- For `topic_relevance`, pass `topic_relevance_config_id` only.
- The API resolves `configuration` + `prompt_version` in `guardrails.py` before validator execution, so the validator always executes with both values.

Example:

Expand Down Expand Up @@ -321,7 +324,86 @@ curl -X DELETE "http://localhost:8001/api/v1/guardrails/ban_lists/<ban_list_id>"
-H "X-API-KEY: <api-key>"
```

## 6) End-to-End Usage Pattern
## 6) Topic Relevance Config APIs (multi-tenant)

These endpoints manage tenant-scoped topic relevance presets and use `X-API-KEY` auth.

Base path:
- `/api/v1/guardrails/topic_relevance_configs`

## 6.1 Create topic relevance config

Endpoint:
- `POST /api/v1/guardrails/topic_relevance_configs/`

Example:

```bash
curl -X POST "http://localhost:8001/api/v1/guardrails/topic_relevance_configs/" \
-H "X-API-KEY: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Maternal Health Scope",
"description": "Topic guard for maternal health support bot",
"prompt_version": 1,
"configuration": "Pregnancy care: Questions about prenatal care, ANC visits, nutrition, supplements, danger signs. Postpartum care: Questions about recovery after delivery, breastfeeding, and mother health checks."
}'
```

## 6.2 List topic relevance configs

Endpoint:
- `GET /api/v1/guardrails/topic_relevance_configs/?offset=0&limit=20`

Example:

```bash
curl -X GET "http://localhost:8001/api/v1/guardrails/topic_relevance_configs/?offset=0&limit=20" \
-H "X-API-KEY: <api-key>"
```

## 6.3 Get topic relevance config by id

Endpoint:
- `GET /api/v1/guardrails/topic_relevance_configs/{id}`

Example:

```bash
curl -X GET "http://localhost:8001/api/v1/guardrails/topic_relevance_configs/<topic_relevance_config_id>" \
-H "X-API-KEY: <api-key>"
```

## 6.4 Update topic relevance config

Endpoint:
- `PATCH /api/v1/guardrails/topic_relevance_configs/{id}`

Example:

```bash
curl -X PATCH "http://localhost:8001/api/v1/guardrails/topic_relevance_configs/<topic_relevance_config_id>" \
-H "X-API-KEY: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"prompt_version": 1,
"configuration": "Pregnancy care: Updated scope definition"
}'
```

## 6.5 Delete topic relevance config

Endpoint:
- `DELETE /api/v1/guardrails/topic_relevance_configs/{id}`

Example:

```bash
curl -X DELETE "http://localhost:8001/api/v1/guardrails/topic_relevance_configs/<topic_relevance_config_id>" \
-H "X-API-KEY: <api-key>"
```

## 7) End-to-End Usage Pattern

Recommended request flow:
1. Create/update validator configs via `/guardrails/validators/configs`.
Expand All @@ -330,15 +412,16 @@ Recommended request flow:
4. Use `safe_text` as downstream text.
5. If `rephrase_needed=true`, ask user to rephrase.
6. For `ban_list` validators without inline `banned_words`, create/manage a ban list first and pass `ban_list_id`.
7. For `topic_relevance`, create/manage a topic relevance config and pass `topic_relevance_config_id` at runtime. The server resolves the configuration string internally.

## 7) Common Errors
## 8) Common Errors

- `401 Missing Authorization header`
- Add `Authorization: Bearer <token>`.
- `401 Invalid authorization token`
- Verify plaintext token matches server-side hash.
- `401 Missing X-API-KEY header`
- Add `X-API-KEY: <api-key>` for ban list endpoints.
- Add `X-API-KEY: <api-key>` for ban list and topic relevance config endpoints.
- `401 Invalid API key`
- Verify the API key is valid in the upstream Kaapi auth service.
- `Invalid request_id`
Expand All @@ -347,14 +430,18 @@ Recommended request flow:
- Type+stage is unique per organization/project scope.
- `Validator not found`
- Confirm `id`, `organization_id`, and `project_id` match.
- `Topic relevance preset not found`
- Confirm topic relevance config `id` exists within your tenant scope.

## 8) Current Validator Types
## 9) Current Validator Types

From `validators.json`:
- `uli_slur_match`
- `pii_remover`
- `gender_assumption_bias`
- `ban_list`
- `llm_critic`
- `topic_relevance`

Source of truth:
- `backend/app/core/validators/validators.json`
Expand Down
2 changes: 2 additions & 0 deletions backend/app/api/docs/guardrails/run_guardrails.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Behavior notes:
- `suppress_pass_logs=true` skips persisting pass-case validator logs.
- The endpoint always saves a `request_log` entry for the run.
- Validator logs are also saved; with `suppress_pass_logs=true`, only fail-case validator logs are persisted. Otherwise, all validator logs are added.
- For `ban_list`, `ban_list_id` can be resolved to `banned_words` from tenant ban list configs.
- For `topic_relevance`, `topic_relevance_config_id` is required and is resolved to `configuration` + `prompt_version` from tenant topic relevance configs in `guardrails.py`.
- `rephrase_needed=true` means the system could not safely auto-fix the input/output and wants the user to retry with a rephrased query.
- When `rephrase_needed=true`, `safe_text` contains the rephrase prompt shown to the user.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Creates a topic relevance configuration for the tenant resolved from `X-API-KEY`.

Behavior notes:
- Stores a topic relevance preset with `name`, `prompt_version`, and `configuration`.
- `configuration` is a plain text scope sub-prompt (string).
- Tenant scope is enforced from the API key context.
- Duplicate configurations are rejected.

Common failure cases:
- Missing or invalid API key.
- Payload schema validation errors.
- Topic relevance with the same configuration already exists.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Deletes a topic relevance configuration by id for the tenant resolved from `X-API-KEY`.

Behavior notes:
- Tenant scope is enforced from the API key context.

Common failure cases:
- Missing or invalid API key.
- Topic relevance preset not found in tenant's scope.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Fetches a single topic relevance configuration by id for the tenant resolved from `X-API-KEY`.

Behavior notes:
- Tenant scope is enforced from the API key context.

Common failure cases:
- Missing or invalid API key.
- Topic relevance preset not found in tenant's scope.
- Invalid id format.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Lists topic relevance configurations for the tenant resolved from `X-API-KEY`.

Behavior notes:
- Supports pagination via `offset` and `limit`.
- `offset` defaults to `0`.
- `limit` is optional; when omitted, no limit is applied.
- Tenant scope is enforced from the API key context.

Common failure cases:
- Missing or invalid API key.
- Invalid pagination values.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Partially updates a topic relevance configuration by id for the tenant resolved from `X-API-KEY`.

Behavior notes:
- Supports patch-style updates; omitted fields remain unchanged.
- `configuration` should be provided as a plain text scope sub-prompt (string).
- Tenant scope is enforced from the API key context.
- Duplicate configurations are rejected.

Common failure cases:
- Missing or invalid API key.
- Topic relevance preset not found in tenant's scope.
- Payload schema validation errors.
- Topic relevance with the same configuration already exists.
9 changes: 8 additions & 1 deletion backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from fastapi import APIRouter

from app.api.routes import ban_lists, guardrails, validator_configs, utils
from app.api.routes import (
ban_lists,
guardrails,
topic_relevance_configs,
validator_configs,
utils,
)

api_router = APIRouter()
api_router.include_router(ban_lists.router)
api_router.include_router(guardrails.router)
api_router.include_router(topic_relevance_configs.router)
api_router.include_router(validator_configs.router)
api_router.include_router(utils.router)

Expand Down
20 changes: 20 additions & 0 deletions backend/app/api/routes/guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
BanListSafetyValidatorConfig,
)
from app.crud.ban_list import ban_list_crud
from app.crud.topic_relevance import topic_relevance_crud
from app.crud.request_log import RequestLogCrud
from app.crud.validator_log import ValidatorLogCrud
from app.core.validators.config.topic_relevance_safety_validator_config import (
TopicRelevanceSafetyValidatorConfig,
)
from app.schemas.guardrail_config import GuardrailRequest, GuardrailResponse
from app.models.logging.request_log import RequestLogUpdate, RequestStatus
from app.models.logging.validator_log import ValidatorLog, ValidatorOutcome
Expand Down Expand Up @@ -46,6 +50,7 @@ def run_guardrails(
return APIResponse.failure_response(error="Invalid request_id")

_resolve_ban_list_banned_words(payload, session)
_resolve_topic_relevance_scope(payload, session)
return _validate_with_guard(
payload,
request_log_crud,
Expand Down Expand Up @@ -196,6 +201,21 @@ def _finalize(
)


def _resolve_topic_relevance_scope(payload: GuardrailRequest, session: Session) -> None:
for validator in payload.validators:
if not isinstance(validator, TopicRelevanceSafetyValidatorConfig):
continue

config = topic_relevance_crud.get(
session=session,
id=validator.topic_relevance_config_id,
organization_id=payload.organization_id,
project_id=payload.project_id,
)
validator.configuration = config.configuration
validator.prompt_version = config.prompt_version


def add_validator_logs(
guard: Guard,
request_log_id: UUID,
Expand Down
Loading
Loading