Skip to content
Open
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
31 changes: 0 additions & 31 deletions .env.test

This file was deleted.

2 changes: 1 addition & 1 deletion .env.test.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ API_BASE_URL=http://localhost:8000

# Postgres
POSTGRES_SERVER=localhost
POSTGRES_DB=kaapi_guardrails_testing
POSTGRES_PORT=5432
POSTGRES_DB=kaapi-guardrails
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
Comment on lines 12 to 16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Complete the alphabetical key ordering to resolve linter warnings.

The past review partially addressed the key ordering, but dotenv-linter still expects full alphabetical order. Currently POSTGRES_DB (line 13) should appear before POSTGRES_SERVER (line 12).

🔧 Complete alphabetical reordering fix
-POSTGRES_SERVER=localhost
 POSTGRES_DB=kaapi_guardrails_testing
+POSTGRES_PASSWORD=postgres
 POSTGRES_PORT=5432
+POSTGRES_SERVER=localhost
 POSTGRES_USER=postgres
-POSTGRES_PASSWORD=postgres
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
POSTGRES_SERVER=localhost
POSTGRES_DB=kaapi_guardrails_testing
POSTGRES_PORT=5432
POSTGRES_DB=kaapi-guardrails
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=kaapi_guardrails_testing
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432
POSTGRES_SERVER=localhost
POSTGRES_USER=postgres
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 13-13: [UnorderedKey] The POSTGRES_DB key should go before the POSTGRES_SERVER key

(UnorderedKey)


[warning] 14-14: [UnorderedKey] The POSTGRES_PORT key should go before the POSTGRES_SERVER key

(UnorderedKey)


[warning] 16-16: [UnorderedKey] The POSTGRES_PASSWORD key should go before the POSTGRES_PORT key

(UnorderedKey)

🤖 Prompt for AI Agents
In @.env.test.example around lines 12 - 16, The environment keys in the
.env.test.example file are not fully alphabetized; reorder the POSTGRES_* lines
so they are in strict alphabetical order (place POSTGRES_DB before
POSTGRES_SERVER) to satisfy dotenv-linter; specifically adjust the sequence of
POSTGRES_DB, POSTGRES_PASSWORD, POSTGRES_PORT, POSTGRES_SERVER, POSTGRES_USER
(or alphabetize all POSTGRES_* keys) so keys like POSTGRES_DB and
POSTGRES_SERVER appear in correct lexical order.


Expand Down
2 changes: 1 addition & 1 deletion backend/app/alembic/versions/001_added_request_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# revision identifiers, used by Alembic.
revision: str = '001'
down_revision: Union[str, Sequence[str], None] = None
down_revision: str | None = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

Expand Down
2 changes: 1 addition & 1 deletion backend/app/alembic/versions/002_added_validator_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# revision identifiers, used by Alembic.
revision: str = '002'
down_revision: Union[str, Sequence[str], None] = '001'
down_revision: str = '001'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

Expand Down
50 changes: 50 additions & 0 deletions backend/app/alembic/versions/003_added_validator_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Added validator_config table

Revision ID: 003
Revises: 002
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 = '003'
down_revision: str = '002'
branch_labels: Union[str, Sequence[str], None] = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are these variables for , "branch labels" and "depends_on", if they are not being used in any way why are they there

depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table('validator_config',
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('type', sa.String(), nullable=False),
sa.Column('stage', sa.String(), nullable=False),
sa.Column('on_fail_action', sa.String(), nullable=False),
sa.Column(
"config",
postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
server_default=sa.text("'{}'::jsonb"),
),
sa.Column('is_enabled', 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('organization_id', 'project_id', 'type', 'stage', name='uq_validator_identity')
)

op.create_index("idx_validator_organization", "validator_config", ["organization_id"])
op.create_index("idx_validator_project", "validator_config", ["project_id"])
op.create_index("idx_validator_type", "validator_config", ["type"])
op.create_index("idx_validator_stage", "validator_config", ["stage"])


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

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

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

# if settings.ENVIRONMENT == "local":
# api_router.include_router(private.router)
10 changes: 5 additions & 5 deletions backend/app/api/routes/guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
from app.api.deps import AuthDep, SessionDep
from app.core.constants import REPHRASE_ON_FAIL_PREFIX
from app.core.guardrail_controller import build_guard, get_validator_config_models
from app.crud.request_log import RequestLogCrud
from app.crud.validator_log import ValidatorLogCrud
from app.models.guardrail_config import GuardrailRequest, GuardrailResponse
from app.models.logging.request import RequestLogUpdate, RequestStatus
from app.models.logging.validator import ValidatorLog, ValidatorOutcome
from app.crud.request_log_repo import RequestLogCrud
from app.crud.validator_log_repo import ValidatorLogCrud
from app.models.logging.request_log_table import RequestLogUpdate, RequestStatus
from app.models.logging.validator_log_table import ValidatorLog, ValidatorOutcome
from app.schemas.guardrail_config import GuardrailRequest, GuardrailResponse
from app.utils import APIResponse

router = APIRouter(prefix="/guardrails", tags=["guardrails"])
Expand Down
90 changes: 90 additions & 0 deletions backend/app/api/routes/validator_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from typing import Optional
from uuid import UUID

from fastapi import APIRouter

from app.api.deps import AuthDep, SessionDep
from app.core.enum import Stage, ValidatorType
from app.schemas.validator_config import ValidatorCreate, ValidatorResponse, ValidatorUpdate
from app.crud.validator_config_crud import validator_config_crud


router = APIRouter(
prefix="/guardrails/validators/configs",
tags=["validator configs"],
)


@router.post(
"/",
response_model=ValidatorResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be APIRESPONSE[ValidatorResponse] to maintain the consistency of response bodies being returned everywhere, and if you dont think that we need this here then why

)
async def create_validator(
payload: ValidatorCreate,
session: SessionDep,
organization_id: int,
project_id: int,
_: AuthDep,
):
return validator_config_crud.create(session, organization_id, project_id, payload)
Comment on lines +22 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

FastAPI async def endpoints synchronous database calls blocking event loop

💡 Result:

Yes—calling a synchronous (blocking) database client inside an async def FastAPI endpoint will block the event loop, reducing concurrency for that worker (other requests handled by the same event loop can’t make progress until the blocking DB call returns). (fastapi.tiangolo.com)

What to do instead (common patterns):

  1. Use an async DB driver / async ORM path (so you can truly await the DB I/O).
  2. If your DB library is sync-only, either:
    • Make the path operation sync: def endpoint(...): ... so Starlette/FastAPI runs it in a thread pool rather than on the event loop. (starlette.dev)
    • Keep async def, but offload the blocking DB call: await run_in_threadpool(sync_db_call, ...) (Starlette uses anyio.to_thread.run_sync under the hood). (starlette.dev)

Important caveat: the thread pool is limited (Starlette’s default limiter is 40 “tokens”), so heavy sync DB usage can still bottleneck—just in the pool instead of the event loop. (starlette.dev)

Citations:


🏁 Script executed:

# Find SessionDep definition
rg -A 5 "SessionDep" backend/app --max-count 5

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 4390


🏁 Script executed:

# Examine the validator_configs.py file to understand the imports and dependencies
cat -n backend/app/api/routes/validator_configs.py | head -40

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 1373


🏁 Script executed:

# Find and inspect the validator_config_crud module
find backend -name "*crud*" -type f | head -20

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 119


🏁 Script executed:

cat -n backend/app/api/deps.py

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 1438


🏁 Script executed:

cat -n backend/app/crud/validator_config_crud.py

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 3920


Remove async from all route handlers — async def with synchronous database calls blocks the event loop.

All five route handlers use async def but invoke synchronous CRUD methods (session.commit(), session.exec(), etc.). Since SessionDep provides a synchronous SQLModel Session, FastAPI will run these async def endpoints on the main event loop where blocking I/O prevents other concurrent requests from progressing.

Change all handlers to def (sync) so FastAPI automatically offloads them to a threadpool:

Suggested fix
-async def create_validator(
+def create_validator(
     payload: ValidatorCreate,
     session: SessionDep,
     organization_id: int,
     project_id: int,
     _: AuthDep,
 ):
     return validator_config_crud.create(session, organization_id, project_id, payload)

Apply the same change to list_validators, get_validator, update_validator, and delete_validator.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def create_validator(
payload: ValidatorCreate,
session: SessionDep,
organization_id: int,
project_id: int,
_: AuthDep,
):
return validator_config_crud.create(session, organization_id, project_id, payload)
def create_validator(
payload: ValidatorCreate,
session: SessionDep,
organization_id: int,
project_id: int,
_: AuthDep,
):
return validator_config_crud.create(session, organization_id, project_id, payload)
🤖 Prompt for AI Agents
In `@backend/app/api/routes/validator_configs.py` around lines 22 - 29, Change all
route handlers from async def to def so they run synchronously: update
create_validator, list_validators, get_validator, update_validator, and
delete_validator to use def instead of async def; ensure you remove any awaits
inside those functions (there should be none since they call synchronous CRUD
methods like validator_config_crud.create / .get / .update / .delete and the
SessionDep methods), keep signatures and return values the same so FastAPI will
run them in a threadpool.

@router.get(
"/",
response_model=list[ValidatorResponse]
)
async def list_validators(
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
stage: Optional[Stage] = None,
type: Optional[ValidatorType] = None,
):
return validator_config_crud.list(session, organization_id, project_id, stage, type)


@router.get(
"/{id}",
response_model=ValidatorResponse
)
async def get_validator(
id: UUID,
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
):
obj = validator_config_crud.get_or_404(session, id, organization_id, project_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just let it ne "get" , it does not have to be "get or 404", thats making the name of crud function long for no reason

return validator_config_crud.flatten(obj)


@router.patch(
"/{id}",
response_model=ValidatorResponse
)
async def update_validator(
id: UUID,
organization_id: int,
project_id: int,
payload: ValidatorUpdate,
session: SessionDep,
_: AuthDep,
):
obj = validator_config_crud.get_or_404(session, id, organization_id, project_id)
return validator_config_crud.update(
session,
obj,
payload.model_dump(exclude_unset=True),
)


@router.delete("/{id}")
async def delete_validator(
id: UUID,
organization_id: int,
project_id: int,
session: SessionDep,
_: AuthDep,
):
obj = validator_config_crud.get_or_404(session, id, organization_id, project_id)
validator_config_crud.delete(session, obj)
return {"success": True}
9 changes: 9 additions & 0 deletions backend/app/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@
SCORE = "score"

REPHRASE_ON_FAIL_PREFIX = "Please rephrase the query without unsafe content."

VALIDATOR_CONFIG_SYSTEM_FIELDS = {
"organization_id",
"project_id",
"type",
"stage",
"on_fail_action",
"is_enabled",
}
12 changes: 11 additions & 1 deletion backend/app/core/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ class BiasCategories(Enum):
class GuardrailOnFail(Enum):
Exception = "exception"
Fix = "fix"
Rephrase = "rephrase"
Rephrase = "rephrase"

class Stage(Enum):
Input = "input"
Output = "output"

class ValidatorType(Enum):
LexicalSlur = "uli_slur_match"
PIIRemover = "pii_remover"
GenderAssumptionBias = "gender_assumption_bias"
BanList = "ban_list"
2 changes: 1 addition & 1 deletion backend/app/core/guardrail_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from guardrails import Guard

from app.models.guardrail_config import ValidatorConfigItem
from app.schemas.guardrail_config import ValidatorConfigItem

def build_guard(validator_items):
validators = [v_item.build() for v_item in validator_items]
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from guardrails.hub import BanList

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

class BanListSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["ban_list"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import List, Literal, Optional

from app.models.base_validator_config import BaseValidatorConfig
from app.core.enum import BiasCategories
from app.core.validators.gender_assumption_bias import GenderAssumptionBias
from app.core.validators.config.base_validator_config import BaseValidatorConfig

class GenderAssumptionBiasSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["gender_assumption_bias"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from app.core.enum import SlurSeverity
from app.core.validators.lexical_slur import LexicalSlur
from app.models.base_validator_config import BaseValidatorConfig
from app.core.validators.config.base_validator_config import BaseValidatorConfig

class LexicalSlurSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["uli_slur_match"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations
from typing import List, Literal, Optional

from app.models.base_validator_config import BaseValidatorConfig
from app.core.validators.pii_remover import PIIRemover
from app.core.validators.config.base_validator_config import BaseValidatorConfig

class PIIRemoverSafetyValidatorConfig(BaseValidatorConfig):
type: Literal["pii_remover"]
Expand Down
2 changes: 1 addition & 1 deletion backend/app/crud/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from app.crud.request_log import RequestLogCrud
from app.crud.request_log_repo import RequestLogCrud
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does it need to have the word "repo" here, it does not make sense to name crud files with the word repo

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from sqlmodel import Session

from app.models.logging.request import RequestLog, RequestLogUpdate, RequestStatus
from app.models.logging.request_log_table import RequestLog, RequestLogUpdate, RequestStatus
from app.utils import now

class RequestLogCrud:
Expand Down
Loading