Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ecd6f39
Added banlist management code
rkritika1508 Feb 10, 2026
9e364f7
Added tests
rkritika1508 Feb 10, 2026
bb7ccd8
resolved comments
rkritika1508 Feb 10, 2026
dd15382
resolved comment
rkritika1508 Feb 10, 2026
c081981
Added seed database and updated tests
rkritika1508 Feb 10, 2026
7e7523e
resolved comment
rkritika1508 Feb 10, 2026
0df63bd
resolved comments
rkritika1508 Feb 10, 2026
3013e48
resolved comment
rkritika1508 Feb 10, 2026
6052c70
apply formatting
rkritika1508 Feb 11, 2026
e509b04
fixed tests
rkritika1508 Feb 11, 2026
2f81a43
Fixed critical issues - auth, docker setup, db indexing, error handli…
rkritika1508 Feb 12, 2026
0dd25cc
Added banlist management code
rkritika1508 Feb 10, 2026
6c0613c
fixed tests
rkritika1508 Feb 11, 2026
85b9a94
Merge branch 'main' into feat/banlist-management
rkritika1508 Feb 12, 2026
bca7127
resolved comments
rkritika1508 Feb 12, 2026
51135f9
resolved comments
rkritika1508 Feb 16, 2026
8a4e096
resolved comments
rkritika1508 Feb 16, 2026
162fada
Merge branch 'main' into feat/banlist-management
rkritika1508 Feb 16, 2026
42fa6b2
resolved comments
rkritika1508 Feb 16, 2026
9a1509d
resolved comments
rkritika1508 Feb 16, 2026
2dd842f
added ban list preprocessing
rkritika1508 Feb 16, 2026
ba75e02
precommit fix
rkritika1508 Feb 16, 2026
49313c9
precommit
rkritika1508 Feb 16, 2026
2de9dd3
revert
rkritika1508 Feb 16, 2026
4c6cb29
resolved comments
rkritika1508 Feb 16, 2026
a1b9c70
resolved comment
rkritika1508 Feb 16, 2026
278bfba
Merge branch 'main' into feat/banlist-management
rkritika1508 Feb 17, 2026
ecf8bd1
updated banlist api
rkritika1508 Feb 17, 2026
6b87071
resolved comment
rkritika1508 Feb 17, 2026
93f14b8
updated verify api and tests
rkritika1508 Feb 17, 2026
8d12092
resolved comments
rkritika1508 Feb 17, 2026
b3c1acc
resolved comments
rkritika1508 Feb 17, 2026
edf3fc9
fix formatting
AkhileshNegi Feb 17, 2026
9a946dd
update version
AkhileshNegi Feb 17, 2026
0612a8a
formatting files
AkhileshNegi Feb 17, 2026
99a315a
fixed test
rkritika1508 Feb 17, 2026
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
4 changes: 0 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ SENTRY_DSN=

DOCKER_IMAGE_BACKEND=kaapi-guardrails-backend

# Callback Timeouts (in seconds)
CALLBACK_CONNECT_TIMEOUT=3
CALLBACK_READ_TIMEOUT=10

# require as a env if you want to use doc transformation
OPENAI_API_KEY="<ADD-KEY>"
GUARDRAILS_HUB_API_KEY="<ADD-KEY>"
Expand Down
4 changes: 0 additions & 4 deletions .env.test.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ SENTRY_DSN=

DOCKER_IMAGE_BACKEND=kaapi-guardrails-backend

# Callback Timeouts (in seconds)
CALLBACK_CONNECT_TIMEOUT=3
CALLBACK_READ_TIMEOUT=10

# require as a env if you want to use doc transformation
OPENAI_API_KEY="<ADD-KEY>"
GUARDRAILS_HUB_API_KEY="<ADD-KEY>"
Expand Down
2 changes: 2 additions & 0 deletions backend/app/alembic/versions/001_added_request_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def upgrade() -> None:
op.create_table(
"request_log",
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("request_id", sa.Uuid(), nullable=False),
sa.Column("response_id", sa.Uuid(), nullable=True),
sa.Column(
Expand Down
2 changes: 2 additions & 0 deletions backend/app/alembic/versions/002_added_validator_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def upgrade() -> None:
op.create_table(
"validator_log",
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("request_id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("input", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
Expand Down
12 changes: 12 additions & 0 deletions backend/app/alembic/versions/004_added_log_indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,31 @@ def upgrade() -> None:
op.create_index("idx_request_log_request_id", "request_log", ["request_id"])
op.create_index("idx_request_log_status", "request_log", ["status"])
op.create_index("idx_request_log_inserted_at", "request_log", ["inserted_at"])
op.create_index(
"idx_request_log_organization_id", "request_log", ["organization_id"]
)
op.create_index("idx_request_log_project_id", "request_log", ["project_id"])

op.create_index("idx_validator_log_request_id", "validator_log", ["request_id"])
op.create_index("idx_validator_log_inserted_at", "validator_log", ["inserted_at"])
op.create_index("idx_validator_log_outcome", "validator_log", ["outcome"])
op.create_index("idx_validator_log_name", "validator_log", ["name"])
op.create_index(
"idx_validator_log_organization_id", "validator_log", ["organization_id"]
)
op.create_index("idx_validator_log_project_id", "validator_log", ["project_id"])


def downgrade() -> None:
op.drop_index("idx_validator_log_inserted_at", table_name="validator_log")
op.drop_index("idx_validator_log_request_id", table_name="validator_log")
op.drop_index("idx_validator_log_outcome", table_name="validator_log")
op.drop_index("idx_validator_log_name", table_name="validator_log")
op.drop_index("idx_validator_log_project_id", table_name="validator_log")
op.drop_index("idx_validator_log_organization_id", table_name="validator_log")

op.drop_index("idx_request_log_inserted_at", table_name="request_log")
op.drop_index("idx_request_log_status", table_name="request_log")
op.drop_index("idx_request_log_request_id", table_name="request_log")
op.drop_index("idx_request_log_organization_id", table_name="request_log")
op.drop_index("idx_request_log_project_id", table_name="request_log")
61 changes: 61 additions & 0 deletions backend/app/alembic/versions/005_added_banlist_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Added ban_list table

Revision ID: 005
Revises: 004
Create Date: 2026-02-05 09:42:54.128852

"""
from typing import Sequence, Union

from alembic import op
from sqlalchemy.dialects import postgresql
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "005"
down_revision = "004"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"ban_list",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("description", sa.String(), nullable=False),
sa.Column("organization_id", sa.Integer(), nullable=False),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("domain", sa.String(), nullable=False),
sa.Column("is_public", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column(
"banned_words",
postgresql.ARRAY(sa.String(length=100)),
nullable=False,
server_default="{}",
),
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_banlist_name_org_project"
),
sa.CheckConstraint(
"coalesce(array_length(banned_words, 1), 0) <= 1000",
name="ck_banlist_banned_words_max_items",
),
)

op.create_index("idx_banlist_organization", "ban_list", ["organization_id"])
op.create_index("idx_banlist_project", "ban_list", ["project_id"])
op.create_index("idx_banlist_domain", "ban_list", ["domain"])
op.create_index(
"idx_banlist_is_public_true",
"ban_list",
["is_public"],
postgresql_where=sa.text("is_public = true"),
)


def downgrade() -> None:
op.drop_table("ban_list")
5 changes: 3 additions & 2 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from fastapi import APIRouter

from app.api.routes import utils, guardrails, validator_configs
from app.api.routes import ban_list_configs, guardrails, validator_configs, utils

api_router = APIRouter()
api_router.include_router(utils.router)
api_router.include_router(ban_list_configs.router)
api_router.include_router(guardrails.router)
api_router.include_router(validator_configs.router)
api_router.include_router(utils.router)

# if settings.ENVIRONMENT == "local":
# api_router.include_router(private.router)
113 changes: 113 additions & 0 deletions backend/app/api/routes/ban_list_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from typing import Optional
from uuid import UUID

from fastapi import APIRouter, HTTPException

from app.api.deps import AuthDep, SessionDep
from app.core.exception_handlers import _safe_error_message
from app.crud.ban_list import ban_list_crud
from app.schemas.ban_list import BanListCreate, BanListUpdate, BanListResponse
from app.utils import APIResponse

router = APIRouter(prefix="/guardrails/ban_lists", tags=["Ban Lists"])


@router.post("/", response_model=APIResponse[BanListResponse])
def create_ban_list(
payload: BanListCreate,
session: SessionDep,
organization_id: int,
project_id: int,
_: AuthDep,
):
try:
response_model = ban_list_crud.create(
session, payload, organization_id, project_id
)
return APIResponse.success_response(data=response_model)
except Exception as exc:
if isinstance(exc, HTTPException):
raise exc
return APIResponse.failure_response(error=_safe_error_message(exc))


@router.get("/", response_model=APIResponse[list[BanListResponse]])
def list_ban_lists(
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
domain: Optional[str] = None,
):
try:
response_model = ban_list_crud.list(
session, organization_id, project_id, domain
)
return APIResponse.success_response(data=response_model)
except Exception as exc:
if isinstance(exc, HTTPException):
raise exc
return APIResponse.failure_response(error=_safe_error_message(exc))


@router.get("/{id}", response_model=APIResponse[BanListResponse])
def get_ban_list(
id: UUID,
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
):
try:
obj = ban_list_crud.get(session, id, organization_id, project_id)
return APIResponse.success_response(data=obj)
except Exception as exc:
if isinstance(exc, HTTPException):
raise exc
return APIResponse.failure_response(error=_safe_error_message(exc))


@router.patch("/{id}", response_model=APIResponse[BanListResponse])
def update_ban_list(
id: UUID,
organization_id: int,
project_id: int,
payload: BanListUpdate,
session: SessionDep,
_: AuthDep,
):
try:
response_model = ban_list_crud.update(
session,
id=id,
organization_id=organization_id,
project_id=project_id,
data=payload,
)
return APIResponse.success_response(data=response_model)
except Exception as exc:
if isinstance(exc, HTTPException):
raise exc
return APIResponse.failure_response(error=_safe_error_message(exc))


@router.delete("/{id}", response_model=APIResponse[dict])
def delete_ban_list(
id: UUID,
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
):
try:
obj = ban_list_crud.get(
session, id, organization_id, project_id, require_owner=True
)
ban_list_crud.delete(session, obj)
return APIResponse.success_response(
data={"message": "Ban list deleted successfully"}
)
except Exception as exc:
if isinstance(exc, HTTPException):
raise exc
return APIResponse.failure_response(error=_safe_error_message(exc))
41 changes: 33 additions & 8 deletions backend/app/api/routes/guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
from fastapi import APIRouter
from guardrails.guard import Guard
from guardrails.validators import FailResult, PassResult
from sqlmodel import Session

from app.api.deps import AuthDep, SessionDep
from app.core.constants import REPHRASE_ON_FAIL_PREFIX
from app.core.constants import BAN_LIST, REPHRASE_ON_FAIL_PREFIX
from app.core.config import settings
from app.core.guardrail_controller import build_guard, get_validator_config_models
from app.core.exception_handlers import _safe_error_message
from app.core.validators.config.ban_list_safety_validator_config import (
BanListSafetyValidatorConfig,
)
from app.crud.ban_list import ban_list_crud
from app.crud.request_log import RequestLogCrud
from app.crud.validator_log import ValidatorLogCrud
from app.schemas.guardrail_config import GuardrailRequest, GuardrailResponse
Expand All @@ -33,14 +38,13 @@ def run_guardrails(
validator_log_crud = ValidatorLogCrud(session=session)

try:
request_id = UUID(payload.request_id)
request_log = request_log_crud.create(payload)
except ValueError:
return APIResponse.failure_response(error="Invalid request_id")

request_log = request_log_crud.create(request_id, input_text=payload.input)
_resolve_ban_list_banned_words(payload, session)
return _validate_with_guard(
payload.input,
payload.validators,
payload,
request_log_crud,
request_log.id,
validator_log_crud,
Expand Down Expand Up @@ -78,9 +82,25 @@ def list_validators(_: AuthDep):
return {"validators": validators}


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

if validator.type != BAN_LIST or validator.banned_words is not None:
continue

ban_list = ban_list_crud.get(
session,
id=validator.ban_list_id,
organization_id=payload.organization_id,
project_id=payload.project_id,
)
validator.banned_words = ban_list.banned_words


def _validate_with_guard(
data: str,
validators: list,
payload: GuardrailRequest,
request_log_crud: RequestLogCrud,
request_log_id: UUID,
validator_log_crud: ValidatorLogCrud,
Expand All @@ -94,6 +114,8 @@ def _validate_with_guard(
while still safely handling unexpected runtime errors.
"""
response_id = uuid.uuid4()
data = payload.input
validators = payload.validators
guard: Guard | None = None

def _finalize(
Expand Down Expand Up @@ -125,7 +147,7 @@ def _finalize(

if guard is not None:
add_validator_logs(
guard, request_log_id, validator_log_crud, suppress_pass_logs
guard, request_log_id, validator_log_crud, payload, suppress_pass_logs
)

rephrase_needed = validated_output is not None and validated_output.startswith(
Expand Down Expand Up @@ -175,6 +197,7 @@ def add_validator_logs(
guard: Guard,
request_log_id: UUID,
validator_log_crud: ValidatorLogCrud,
payload: GuardrailRequest,
suppress_pass_logs: bool = False,
):
history = getattr(guard, "history", None)
Expand Down Expand Up @@ -202,6 +225,8 @@ def add_validator_logs(

validator_log = ValidatorLog(
request_id=request_log_id,
organization_id=payload.organization_id,
project_id=payload.project_id,
name=log.validator_name,
input=str(log.value_before_validation),
output=log.value_after_validation,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
from typing import List, Literal
from typing import List, Literal, Optional
from uuid import UUID

from guardrails.hub import BanList
from pydantic import model_validator

from app.core.validators.config.base_validator_config import BaseValidatorConfig


class BanListSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["ban_list"]
banned_words: List[str] # list of banned words to be redacted
banned_words: Optional[List[str]] = None # list of banned words to be redacted
ban_list_id: Optional[UUID] = None

@model_validator(mode="after")
def validate_ban_list_source(self):
if self.banned_words is None and self.ban_list_id is None:
raise ValueError("Either banned_words or ban_list_id must be provided.")
return self

def build(self):
return BanList(
banned_words=self.banned_words,
banned_words=self.banned_words or [],
on_fail=self.resolve_on_fail(),
)
Loading
Loading