Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ RUN chmod +x /app/scripts/*.sh
COPY ./pyproject.toml ./uv.lock ./alembic.ini /app/

COPY ./app /app/app
COPY ./tests /app/tests

# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
Expand Down
2 changes: 2 additions & 0 deletions backend/app/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from app.core.config import settings
from app.models import SQLModel

import app.models

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
Expand Down
39 changes: 39 additions & 0 deletions backend/app/alembic/versions/004_added_log_indexes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Added indexes for request_log and validator_log

Revision ID: 004
Revises: 003
Create Date: 2026-02-11 10:45:00.000000

"""
from typing import Sequence, Union

from alembic import op


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


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_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"])


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_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")
16 changes: 13 additions & 3 deletions backend/app/api/deps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from collections.abc import Generator
from typing import Annotated
import hashlib
import secrets

from fastapi import Depends, HTTPException, status, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
Expand All @@ -18,6 +20,10 @@ def get_db() -> Generator[Session, None, None]:
security = HTTPBearer(auto_error=False)


def _hash_token(token: str) -> str:
return hashlib.sha256(token.encode("utf-8")).hexdigest()


def verify_bearer_token(
credentials: Annotated[
HTTPAuthorizationCredentials | None,
Expand All @@ -30,12 +36,16 @@ def verify_bearer_token(
detail="Missing Authorization header",
)

token = credentials.credentials
provided_hash = _hash_token(credentials.credentials)
expected_hash = settings.AUTH_TOKEN

if not expected_hash:
raise RuntimeError("AUTH_TOKEN is not configured")

if token != settings.AUTH_TOKEN:
if not secrets.compare_digest(provided_hash, expected_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
detail="Invalid authorization token",
)

return True
Expand Down
17 changes: 11 additions & 6 deletions backend/app/api/routes/guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

from app.api.deps import AuthDep, SessionDep
from app.core.constants import 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.crud.request_log import RequestLogCrud
from app.crud.validator_log import ValidatorLogCrud
from app.schemas.guardrail_config import GuardrailRequest, GuardrailResponse
Expand All @@ -21,7 +23,7 @@
@router.post(
"/", response_model=APIResponse[GuardrailResponse], response_model_exclude_none=True
)
async def run_guardrails(
def run_guardrails(
payload: GuardrailRequest,
session: SessionDep,
_: AuthDep,
Expand All @@ -36,7 +38,7 @@ async def run_guardrails(
return APIResponse.failure_response(error="Invalid request_id")

request_log = request_log_crud.create(request_id, input_text=payload.input)
return await _validate_with_guard(
return _validate_with_guard(
payload.input,
payload.validators,
request_log_crud,
Expand All @@ -47,7 +49,7 @@ async def run_guardrails(


@router.get("/")
async def list_validators(_: AuthDep):
def list_validators(_: AuthDep):
"""
Lists all validators and their parameters directly.
"""
Expand All @@ -67,13 +69,16 @@ async def list_validators(_: AuthDep):

except (KeyError, TypeError) as e:
return APIResponse.failure_response(
error=f"Failed to retrieve schema for validator {model.__name__}: {str(e)}",
error=(
"Failed to retrieve schema for validator "
f"{model.__name__}: {_safe_error_message(e)}"
),
)

return {"validators": validators}


async def _validate_with_guard(
def _validate_with_guard(
data: str,
validators: list,
request_log_crud: RequestLogCrud,
Expand Down Expand Up @@ -162,7 +167,7 @@ def _finalize(
# Case 3: unexpected system / runtime failure
return _finalize(
status=RequestStatus.ERROR,
error_message=str(exc),
error_message=_safe_error_message(exc),
)


Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@


@router.get("/health-check/")
async def health_check() -> bool:
def health_check() -> bool:
return True
11 changes: 8 additions & 3 deletions backend/app/core/exception_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
HTTP_500_INTERNAL_SERVER_ERROR,
)

from app.core.config import settings
from app.utils import APIResponse


Expand Down Expand Up @@ -48,6 +49,12 @@ def _format_validation_errors(errors: list[dict]) -> str:
return ". ".join(messages)


def _safe_error_message(exc: Exception) -> str:
if settings.ENVIRONMENT == "production":
return "An unexpected error occurred."
return str(exc) or "An unexpected error occurred."


def register_exception_handlers(app: FastAPI):
@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError):
Expand All @@ -68,7 +75,5 @@ async def http_exception_handler(request: Request, exc: HTTPException):
async def generic_error_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
content=APIResponse.failure_response(
str(exc) or "An unexpected error occurred."
).model_dump(),
content=APIResponse.failure_response(_safe_error_message(exc)).model_dump(),
)
2 changes: 2 additions & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

from app.models.logging.request_log import RequestLog
from app.models.logging.validator_log import ValidatorLog

from app.models.config.validator_config import ValidatorConfig
15 changes: 6 additions & 9 deletions backend/app/tests/test_validate_with_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
mock_request_log_id = uuid4()


@pytest.mark.asyncio
async def test_validate_with_guard_success():
def test_validate_with_guard_success():
class MockGuard:
def validate(self, data):
return MockResult(validated_output="clean text")
Expand All @@ -23,7 +22,7 @@ def validate(self, data):
"app.api.routes.guardrails.build_guard",
return_value=MockGuard(),
):
response = await _validate_with_guard(
response = _validate_with_guard(
data="hello",
validators=[],
request_log_crud=mock_request_log_crud,
Expand All @@ -37,8 +36,7 @@ def validate(self, data):
assert response.data.response_id is not None


@pytest.mark.asyncio
async def test_validate_with_guard_validation_error():
def test_validate_with_guard_validation_error():
class MockGuard:
def validate(self, data):
return MockResult(validated_output=None)
Expand All @@ -47,7 +45,7 @@ def validate(self, data):
"app.api.routes.guardrails.build_guard",
return_value=MockGuard(),
):
response = await _validate_with_guard(
response = _validate_with_guard(
data="bad text",
validators=[],
request_log_crud=mock_request_log_crud,
Expand All @@ -61,13 +59,12 @@ def validate(self, data):
assert response.error


@pytest.mark.asyncio
async def test_validate_with_guard_exception():
def test_validate_with_guard_exception():
with patch(
"app.api.routes.guardrails.build_guard",
side_effect=Exception("Invalid config"),
):
response = await _validate_with_guard(
response = _validate_with_guard(
data="text",
validators=[],
request_log_crud=mock_request_log_crud,
Expand Down
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
"sentry-sdk[fastapi]<2.0.0,>=1.40.6",
"pyjwt<3.0.0,>=2.8.0",
"asgi-correlation-id>=4.3.4",
"guardrails-ai>=0.7.2",
"guardrails-ai>=0.8.0",
"emoji",
"ftfy",
"presidio_analyzer>=2.2.360",
Expand Down
12 changes: 0 additions & 12 deletions backend/scripts/install_guardrails_from_hub.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,6 @@
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"

if [ -f "$ENV_FILE" ]; then
echo "Loading env from $ENV_FILE"
set -a
source "$ENV_FILE"
set +a
else
echo "Env file not found: $ENV_FILE"
exit 1
fi

: "${GUARDRAILS_HUB_API_KEY:?GUARDRAILS_HUB_API_KEY is required}"

Expand Down
3 changes: 3 additions & 0 deletions backend/scripts/prestart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ set -x

# Let the DB start
uv run python app/backend_pre_start.py
echo "backend_pre_start.py completed"

# Run migrations
uv run alembic upgrade head
echo "alembic migration completed"

# Create initial data in DB
uv run python app/initial_data.py
echo "initial data added in db"
Loading
Loading