Skip to content

Conversation

@rkritika1508
Copy link
Collaborator

@rkritika1508 rkritika1508 commented Dec 18, 2025

Summary

This pull request contains the boilerplate code, bearer token based authentication, validator code and Guardrails API which will be consumed directly by Kaapi.
Target issue is #2

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Notes

What this PR does

  1. Adds Guardrails AI input/output validation APIs
  2. Introduces typed validator configuration models (Hub + custom validators)
  3. Implements unit + integration tests for guardrails
  4. Improves environment handling for testing vs development

Major changes (by area)

  1. Guardrails API layer
  • New /guardrails/input and /guardrails/output endpoints
  • Centralized _validate_with_guard execution flow
  • Consistent API response structure for success and failure
  1. Validator configuration system
  • Discriminated union (ValidatorConfigItem) for validator configs
  • Hub-backed validators (BanList)
  • Custom validators (PII, Lexical Slur, GenderAssumptionBias etc.)
  • Clean separation between config models and runtime validators
  1. Testing infrastructure
  • Unit tests for API layer (mocked guards)
  • Integration tests using real Guardrails validators
  • Clear separation of unit vs integration tests using markers
  • DB connections and Alembic migrations disabled in testing

How this was tested

  • Unit tests for guardrails API (app/tests)
  • Integration tests with real validators (@pytest.mark.integration)
  • Manual API testing via curl/Postman
  • Ran the backend/scripts/tests.sh file.

Few bugs and decisions that will need to change later

  • backend/app/core/exception_handlers.py is very generic right now. It should be improved with clear and well-articulated exceptions in all requests. This needs to be tested thoroughly.
  • backend/app/core/util.py is almost empty and not serving any useful purpose. Because of this, there are multiple utils files now, which is causing confusion.
  • SECRET_KEY in config.py` must be set via environment variable.
  • Gender_assumption_bias.py and lexical_slur.py both use pandas to read csv, leading to resource leakage issues. This is an important bug which was flagged during stress-testing.
  • Food for thought: Are there any genuine use cases where the lexical slur validator will be used in an output guardrail? In this validator, we format and clean up the text before running the validator. If this happens in output, the barebones text will be bad user experience.
  • lexical_slur.py file has an unusual logic. We need to consider from the NGO standpoint here. If an org has selected severity as low, does it mean we will consider all low, medium and high severity slurs? If another org has selected severity as medium, will we filter out all medium and high severity slurs? Otherwise, do we expect the NGO to provide a list ["low", "medium", "high"] to consider all slurs. This needs to be discussed and the logic around it needs to be thought through.
  • Improve AnalyzerEngine and AnonymizerEnginepii_remover.py by using entity_types and threshold values. Due to lack of testing data, I have skipped any fine-tuning and have only included these parameters without using them anywhere.
  • We can also improve pii_remover.py by adding custom recognizers for Romanized Hindi. This will improve accuracy.
  • Need to add versions to all dependencies in pyproject.toml.
  • Clean up the list_validators API - it has many unnecessary details.

Summary by CodeRabbit

  • New Features

    • Guardrails validation API endpoints for input/output safety, plus request logging
  • New Features — Validators

    • Added lexical slur detection, PII redaction, gender-assumption-bias detection, and ban-list validators
  • Documentation

    • Expanded README, backend docs, deployment and development guides
  • Chores

    • Docker compose + backend Dockerfile, migration tooling, env templates, scripts, and test orchestration
  • Tests

    • Unit and integration tests covering validators and guardrails API

✏️ Tip: You can customize this high-level summary in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

🧹 Nitpick comments (28)
backend/app/core/middleware.py (1)

17-21: Use extracted client_ip in logging or remove it.

The client_ip variable is extracted on Line 17 but never used in the log statement on lines 19-21. Consider including it in the log output for better observability.

🔎 Apply this diff to include client IP in logs:
     logger.info(
-        f"{request.method} {request.url.path} - {response.status_code} [{process_time:.2f}ms]"
+        f"{request.method} {request.url.path} - {response.status_code} [{process_time:.2f}ms] - {client_ip}"
     )
copier.yml (1)

11-17: Document that security defaults must be changed before deployment.

The template uses changethis as the default for secret_key, first_superuser_password, and postgres_password. While acceptable for a template, consider adding prominent warnings in deployment documentation that these MUST be changed before production use.

A comment in the template itself could help:

 secret_key:
   type: str
   help: |
-    'The secret key for the project, used for security,
+    '⚠️  REQUIRED: The secret key for the project, used for security,
     stored in .env, you can generate one with:
     python -c "import secrets; print(secrets.token_urlsafe(32))"'
   default: changethis

Also applies to: 24-27, 49-55

README.md (1)

14-14: Consider formatting the URL as a markdown link.

Line 14 contains a bare URL. For better markdown compliance and readability, consider formatting it as a proper link.

🔎 Suggested fix:
-- [uv](https://docs.astral.sh/uv/) for Python package and environment management.
+- [uv](https://docs.astral.sh/uv/) for Python package and environment management

(The URL is already a proper markdown link - the linter may be confused. If you meant to add more context, consider: [uv package manager](https://docs.astral.sh/uv/))

backend/app/load_env.py (1)

5-13: Document the expected execution context for relative paths.

The function uses relative paths (../.env) which assume the script is executed from the backend/ directory. Consider:

  • Adding a docstring explaining the execution context requirement
  • Using Path(__file__).parent.parent / ".env" for more robust path resolution
  • Adding file existence validation before calling load_dotenv
🔎 More robust implementation:
+from pathlib import Path
 import os
 from dotenv import load_dotenv
 
 
 def load_environment():
+    """Load environment variables from .env file.
+    
+    Expects to be called from the backend/ directory.
+    Uses ../.env for development/production, ../.env.test for testing.
+    """
     env = os.getenv("ENVIRONMENT", "development")
 
-    # Use the same path as config.py expects (one level above backend/)
-    env_file = "../.env"
+    base_path = Path(__file__).parent.parent.parent
+    env_file = base_path / ".env"
     if env == "testing":
-        env_file = "../.env.test"
+        env_file = base_path / ".env.test"
 
-    load_dotenv(env_file)
+    if env_file.exists():
+        load_dotenv(env_file)
+    else:
+        # Silent fail or log warning depending on requirements
+        pass
backend/app/initial_data.py (1)

7-8: Consider removing module-level logging configuration.

Calling basicConfig at the module level can interfere with the application's logging configuration (likely defined in main.py). Module-level logging setup affects the global logging state and may override application-wide settings.

🔎 Consider this approach:
-logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)

Let the application's main entry point handle logging configuration, and this module will inherit those settings.

scripts/test-local.sh (2)

8-8: Quote the command substitution to prevent word splitting.

The unquoted $(uname -s) could cause issues if the command returns unexpected output with spaces, though this is unlikely with uname -s.

🔎 Apply this fix:
-if [ $(uname -s) = "Linux" ]; then
+if [ "$(uname -s)" = "Linux" ]; then

6-15: Consider migrating to docker compose (v2).

The script uses the deprecated docker-compose (v1) command. Docker Compose v2 uses docker compose (without hyphen) and is the recommended approach going forward.

🔎 Suggested migration:
-docker-compose down -v --remove-orphans
+docker compose down -v --remove-orphans
 
 if [ "$(uname -s)" = "Linux" ]; then
     echo "Remove __pycache__ files"
     sudo find . -type d -name __pycache__ -exec rm -r {} \+
 fi
 
-docker-compose build
-docker-compose up -d
-docker-compose exec -T backend bash scripts/tests-start.sh "$@"
+docker compose build
+docker compose up -d
+docker compose exec -T backend bash scripts/tests-start.sh "$@"
scripts/build-push.sh (1)

10-10: Consider migrating to docker compose (v2).

Similar to other scripts, this uses the deprecated docker-compose command. Consider using docker compose for consistency with Docker Compose v2.

🔎 Apply this change:
-docker-compose -f docker-compose.yml push
+docker compose -f docker-compose.yml push
development.md (1)

105-116: Fix heading level increment for proper document structure.

The heading jumps from h2 (##) to h4 (####), skipping h3. This violates proper heading hierarchy and can affect document navigation and accessibility tools.

🔎 Apply this fix:
-#### Install pre-commit to run automatically
+### Install pre-commit to run automatically

Also consider adjusting the next heading at line 128 if needed:

-#### Running pre-commit hooks manually
+### Running pre-commit hooks manually
.env.example (1)

50-52: Consider using placeholder format consistently for sensitive keys.

The API keys use <ADD-KEY> and <ADD-TOKEN> placeholders which is good practice. Also, add a trailing newline at end of file.

🔎 Suggested improvement:
 # require as a env if you want to use kaapi guardrail apis
 OPENAI_API_KEY=<ADD-KEY>
 GUARDRAILS_HUB_API_KEY=<ADD-KEY>
-AUTH_TOKEN=<ADD-TOKEN>
+AUTH_TOKEN=<ADD-TOKEN>
+
backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py (2)

8-11: Unused import: sqlmodel.sql.sqltypes.

This import is not used in the migration file.

🔎 Apply this diff:
 from alembic import op
 import sqlalchemy as sa
-import sqlmodel.sql.sqltypes
 from sqlalchemy.dialects import postgresql

57-90: Downgrade creates sequences correctly but has a subtle issue.

Line 59-61: autoincrement=True on add_column doesn't automatically create/attach a sequence in PostgreSQL via Alembic. The subsequent sequence creation (lines 65-66) handles this, but the autoincrement=True parameter is misleading and has no effect here.

🔎 Consider removing the misleading parameter:
 def downgrade():
     # Reverse the upgrade process
-    op.add_column('user', sa.Column('old_id', sa.Integer, autoincrement=True))
-    op.add_column('item', sa.Column('old_id', sa.Integer, autoincrement=True))
+    op.add_column('user', sa.Column('old_id', sa.Integer))
+    op.add_column('item', sa.Column('old_id', sa.Integer))
     op.add_column('item', sa.Column('old_owner_id', sa.Integer, nullable=True))
backend/Dockerfile (1)

1-44: Dockerfile follows uv best practices with good layer caching.

The Dockerfile correctly implements the recommended uv Docker integration patterns with cache mounts and intermediate layers for efficient builds.

A few optional improvements to consider:

  1. Add EXPOSE 8000 for documentation purposes (FastAPI default port).
  2. Consider adding a HEALTHCHECK instruction for container orchestration.
  3. Consider using a multi-stage build or slim base image to reduce final image size.
🔎 Optional: Add EXPOSE and HEALTHCHECK
 ENV PYTHONPATH=/app

+EXPOSE 8000
+
 COPY ./scripts /app/scripts
 RUN --mount=type=cache,target=/root/.cache/uv \
     uv sync

+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+    CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
+
 CMD ["fastapi", "run", "--workers", "4", "app/main.py"]
backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py (1)

10-10: Unused import: sqlmodel.sql.sqltypes.

This import is not used in the migration and can be removed.

🔎 Apply this diff:
 from alembic import op
 import sqlalchemy as sa
-import sqlmodel.sql.sqltypes
backend/app/core/db.py (2)

1-1: Unused imports: Session and select are imported but not used.

Only create_engine is used in this module. The Session and select imports appear to be leftover from template code.

🔎 Apply this diff to remove unused imports:
-from sqlmodel import Session, create_engine, select
+from sqlmodel import create_engine

13-22: Add trailing newline and consider documenting the placeholder.

The init_db function is currently a no-op placeholder. Per the static analysis hint, add a trailing newline. Also, consider whether this function should be removed if unused, or documented for future implementation.

🔎 Apply this diff:
     # This works because the models are already imported and registered from app.models
     # SQLModel.metadata.create_all(engine)

     pass
+
backend/app/tests_pre_start.py (2)

16-29: Code duplication with backend_pre_start.py.

This implementation is nearly identical to backend/app/backend_pre_start.py. Consider extracting the shared retry logic into a common utility function to reduce duplication and ensure consistent behavior.

🔎 Example shared utility approach:

Create a shared module (e.g., app/core/startup.py):

# app/core/startup.py
import logging
from sqlalchemy import Engine
from sqlmodel import Session, select
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed

logger = logging.getLogger(__name__)

max_tries = 60 * 5  # 5 minutes
wait_seconds = 1

@retry(
    stop=stop_after_attempt(max_tries),
    wait=wait_fixed(wait_seconds),
    before=before_log(logger, logging.INFO),
    after=after_log(logger, logging.WARNING),
)
def wait_for_db(db_engine: Engine) -> None:
    """Wait for database to be available."""
    try:
        with Session(db_engine) as session:
            session.exec(select(1))
    except Exception as e:
        logger.error(e)
        raise e

Then both backend_pre_start.py and tests_pre_start.py can import and use wait_for_db.


20-20: Use logging.WARNING instead of deprecated logging.WARN.

logging.WARN is deprecated in favor of logging.WARNING.

🔎 Apply this diff:
 @retry(
     stop=stop_after_attempt(max_tries),
     wait=wait_fixed(wait_seconds),
     before=before_log(logger, logging.INFO),
-    after=after_log(logger, logging.WARN),
+    after=after_log(logger, logging.WARNING),
 )
backend/app/core/exception_handlers.py (1)

26-33: Consider logging the exception before returning the generic 500 response.

For the generic exception handler, the error is silently converted to a response. In production, unhandled exceptions should be logged for debugging and monitoring purposes. Also, exposing raw exception messages (str(exc)) in responses could leak sensitive implementation details.

🔎 Suggested improvement
     @app.exception_handler(Exception)
     async def generic_error_handler(request: Request, exc: Exception):
+        logger.exception("Unhandled exception occurred")
         return JSONResponse(
             status_code=HTTP_500_INTERNAL_SERVER_ERROR,
             content=APIResponse.failure_response(
-                str(exc) or "An unexpected error occurred."
+                "An unexpected error occurred."
             ).model_dump(),
         )

This requires adding a logger import and instantiation at the top of the file. Consider exposing the actual error message only in non-production environments via a configuration flag.

backend/app/backend_pre_start.py (1)

22-29: Consider extracting shared DB health-check logic.

This init function is nearly identical to the one in backend/app/tests_pre_start.py. Both perform the same DB connectivity check with retry logic.

You could extract the shared logic into a common utility (e.g., in app/core/db.py) to avoid duplication:

# In app/core/db.py
def check_db_connection(db_engine: Engine) -> None:
    with Session(db_engine) as session:
        session.exec(select(1))

Then both pre-start scripts can import and use this with their own retry decorators.

backend/app/alembic/versions/e2412789c190_initialize_models.py (1)

40-44: Consider adding ON DELETE CASCADE and an index on the foreign key.

The foreign key constraint on owner_id doesn't specify cascade behavior. Deleting a user with associated items will fail with a foreign key violation. Additionally, an index on owner_id would improve query performance for user-item lookups.

🔎 Suggested improvement
         sa.ForeignKeyConstraint(
             ["owner_id"],
             ["user.id"],
+            ondelete="CASCADE",
         ),
         sa.PrimaryKeyConstraint("id"),
     )
+    op.create_index(op.f("ix_item_owner_id"), "item", ["owner_id"], unique=False)

And in downgrade():

 def downgrade():
     # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_index(op.f("ix_item_owner_id"), table_name="item")
     op.drop_table("item")

Note: The AI summary mentions cascade delete behavior may be addressed in a subsequent migration. If so, disregard this comment.

backend/app/api/deps.py (3)

4-4: Remove unused import Header.

The Header import is not used in this module.

-from fastapi import Depends, Header, HTTPException, status, Security
+from fastapi import Depends, HTTPException, status, Security

8-8: Remove commented-out code.

Commented-out imports should be removed to keep the codebase clean. If this is needed later, version control will preserve the history.

-# from app.core import security

17-17: Consider renaming security to avoid shadowing.

The variable name security could shadow the commented-out import from app.core. Consider a more descriptive name like bearer_scheme or http_bearer.

-security = HTTPBearer(auto_error=False)
+bearer_scheme = HTTPBearer(auto_error=False)

This would also require updating the reference on line 22.

backend/app/utils.py (2)

6-6: Use modern type hint dict instead of typing.Dict.

Python 3.9+ supports using built-in dict directly for type hints.

🔎 Apply this diff:
-from typing import Any, Dict, Generic, Optional, TypeVar
+from typing import Any, Generic, Optional, TypeVar

As per static analysis hints, also update line 17:

-    metadata: Optional[Dict[str, Any]] = None
+    metadata: Optional[dict[str, Any]] = None

And lines 21, 30 where Dict[str, Any] appears in method signatures.


37-37: Add trailing newline.

As per static analysis, the file is missing a newline at the end.

backend/app/core/config.py (1)

17-22: Clarify the CORS parsing logic and improve error handling.

The logic has potential issues:

  1. Line 20: The condition isinstance(v, list | str) after the line 18 check is redundant since v must be a str at that point if it wasn't a list initially.
  2. Line 22: Raising ValueError(v) without a descriptive message makes debugging difficult.
  3. The function doesn't appear to be used in the provided Settings class fields.
🔎 Apply this diff to improve the function:
 def parse_cors(v: Any) -> list[str] | str:
     if isinstance(v, str) and not v.startswith("["):
         return [i.strip() for i in v.split(",") if i.strip()]
-    elif isinstance(v, list | str):
+    elif isinstance(v, (list, str)):
         return v
-    raise ValueError(v)
+    raise ValueError(f"Invalid CORS value: {v}. Expected string or list.")
docker-compose.yml (1)

27-36: Pin the Adminer image version.

Line 28 uses image: adminer without a version tag, which defaults to latest. This can lead to unexpected behavior when the image updates. Consider pinning to a specific version for reproducibility.

🔎 Example with version pinning:
-    image: adminer
+    image: adminer:4.8.1
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99b4f56 and a85ed62.

⛔ Files ignored due to path filters (1)
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (46)
  • .env.example (1 hunks)
  • README.md (1 hunks)
  • SECURITY.md (1 hunks)
  • backend/.dockerignore (1 hunks)
  • backend/.gitignore (1 hunks)
  • backend/Dockerfile (1 hunks)
  • backend/README.md (1 hunks)
  • backend/alembic.ini (1 hunks)
  • backend/app/alembic/README (1 hunks)
  • backend/app/alembic/env.py (1 hunks)
  • backend/app/alembic/script.py.mako (1 hunks)
  • backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py (1 hunks)
  • backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py (1 hunks)
  • backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py (1 hunks)
  • backend/app/alembic/versions/e2412789c190_initialize_models.py (1 hunks)
  • backend/app/api/deps.py (1 hunks)
  • backend/app/api/main.py (1 hunks)
  • backend/app/api/routes/utils.py (1 hunks)
  • backend/app/backend_pre_start.py (1 hunks)
  • backend/app/core/config.py (1 hunks)
  • backend/app/core/db.py (1 hunks)
  • backend/app/core/exception_handlers.py (1 hunks)
  • backend/app/core/middleware.py (1 hunks)
  • backend/app/initial_data.py (1 hunks)
  • backend/app/load_env.py (1 hunks)
  • backend/app/main.py (1 hunks)
  • backend/app/tests_pre_start.py (1 hunks)
  • backend/app/utils.py (1 hunks)
  • backend/pyproject.toml (1 hunks)
  • backend/scripts/format.sh (1 hunks)
  • backend/scripts/lint.sh (1 hunks)
  • backend/scripts/prestart.sh (1 hunks)
  • backend/scripts/test.sh (1 hunks)
  • backend/scripts/tests-start.sh (1 hunks)
  • copier.yml (1 hunks)
  • deployment.md (1 hunks)
  • development.md (1 hunks)
  • docker-compose.yml (1 hunks)
  • hooks/post_gen_project.py (1 hunks)
  • release-notes.md (1 hunks)
  • scripts/build-push.sh (1 hunks)
  • scripts/build.sh (1 hunks)
  • scripts/deploy.sh (1 hunks)
  • scripts/generate-client.sh (1 hunks)
  • scripts/test-local.sh (1 hunks)
  • scripts/test.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py (1)
backend/app/alembic/versions/e2412789c190_initialize_models.py (2)
  • upgrade (19-46)
  • downgrade (49-54)
backend/scripts/prestart.sh (4)
backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py (1)
  • upgrade (20-27)
backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py (1)
  • upgrade (20-43)
backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py (1)
  • upgrade (21-55)
backend/app/alembic/versions/e2412789c190_initialize_models.py (1)
  • upgrade (19-46)
backend/app/initial_data.py (3)
backend/app/core/db.py (1)
  • init_db (13-22)
backend/app/backend_pre_start.py (2)
  • init (22-29)
  • main (32-35)
backend/app/tests_pre_start.py (2)
  • init (22-29)
  • main (32-35)
backend/app/tests_pre_start.py (1)
backend/app/backend_pre_start.py (2)
  • init (22-29)
  • main (32-35)
backend/app/alembic/env.py (1)
backend/app/core/config.py (1)
  • SQLALCHEMY_DATABASE_URI (47-55)
backend/app/core/exception_handlers.py (1)
backend/app/utils.py (2)
  • APIResponse (13-37)
  • failure_response (26-37)
backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py (3)
backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py (2)
  • upgrade (20-27)
  • downgrade (30-37)
backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py (2)
  • upgrade (21-55)
  • downgrade (57-90)
backend/app/alembic/versions/e2412789c190_initialize_models.py (2)
  • upgrade (19-46)
  • downgrade (49-54)
backend/app/core/db.py (1)
backend/app/core/config.py (1)
  • SQLALCHEMY_DATABASE_URI (47-55)
backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py (2)
backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py (2)
  • upgrade (20-27)
  • downgrade (30-37)
backend/app/alembic/versions/e2412789c190_initialize_models.py (2)
  • upgrade (19-46)
  • downgrade (49-54)
backend/app/backend_pre_start.py (2)
backend/app/initial_data.py (2)
  • init (11-13)
  • main (16-19)
backend/app/tests_pre_start.py (2)
  • init (22-29)
  • main (32-35)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 17-17: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 22-22: [UnorderedKey] The FIRST_SUPERUSER key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 23-23: [UnorderedKey] The FIRST_SUPERUSER_PASSWORD key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 24-24: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 24-24: [UnorderedKey] The EMAIL_TEST_USER key should go before the FIRST_SUPERUSER key

(UnorderedKey)


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

(UnorderedKey)


[warning] 35-35: [UnorderedKey] The POSTGRES_DB key should go before the POSTGRES_PORT key

(UnorderedKey)


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

(UnorderedKey)


[warning] 46-46: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 47-47: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 51-51: [UnorderedKey] The GUARDRAILS_HUB_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 52-52: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 52-52: [UnorderedKey] The AUTH_TOKEN key should go before the GUARDRAILS_HUB_API_KEY key

(UnorderedKey)

🪛 LanguageTool
SECURITY.md

[style] ~3-~3: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: # Security Policy Security is very important for this project and its community. 🔒 ...

(EN_WEAK_ADJECTIVE)

development.md

[grammar] ~13-~13: Use a hyphen to join words.
Context: ...interact with these URLs: Backend, JSON based web API based on OpenAPI: <http://...

(QB_NEW_EN_HYPHEN)


[style] ~55-~55: In American English, abbreviations like “etc.” require a period.
Context: ...ch service (backend, frontend, adminer, etc). When you deploy it to production (or...

(ETC_PERIOD)

README.md

[style] ~84-~84: Using many exclamation marks might seem excessive (in this case: 5 exclamation marks for a text that’s 2167 characters long)
Context: ...or creating and maintaining the template!!!

(EN_EXCESSIVE_EXCLAMATION)

deployment.md

[uncategorized] ~96-~96: The official name of this software platform is spelled with a capital “H”.
Context: ...ub * Add Docker permissions to the `github` user: bash sudo usermod -aG docker...

(GITHUB)


[uncategorized] ~102-~102: The official name of this software platform is spelled with a capital “H”.
Context: ...github * Temporarily switch to the `github` user: bash sudo su - github ``` *...

(GITHUB)


[uncategorized] ~108-~108: The official name of this software platform is spelled with a capital “H”.
Context: ...bash sudo su - github ``` * Go to the github` user's home directory: bash cd ...

(GITHUB)


[style] ~120-~120: Consider a more expressive alternative.
Context: ...ng, you can install it as a service. To do that, exit the github user and go bac...

(DO_ACHIEVE)


[uncategorized] ~120-~120: The official name of this software platform is spelled with a capital “H”.
Context: ...l it as a service. To do that, exit the github user and go back to the root user: ...

(GITHUB)


[uncategorized] ~128-~128: The official name of this software platform is spelled with a capital “H”.
Context: ...that user. Before being able to go the github user directory, you need to become the...

(GITHUB)


[style] ~134-~134: This phrase is redundant. Consider using “inside”.
Context: ...r, go to the actions-runner directory inside of the github user's home directory: ``...

(OUTSIDE_OF)


[uncategorized] ~134-~134: The official name of this software platform is spelled with a capital “H”.
Context: ...actions-runnerdirectory inside of thegithub` user's home directory: ```bash cd /ho...

(GITHUB)


[uncategorized] ~140-~140: The official name of this software platform is spelled with a capital “H”.
Context: ...osted runner as a service with the user github: bash ./svc.sh install github ...

(GITHUB)


[uncategorized] ~164-~164: The official name of this software platform is spelled with a capital “H”.
Context: ...secrets-for-a-repository). The current Github Actions workflows expect these secrets:...

(GITHUB)


[uncategorized] ~180-~180: The official name of this software platform is spelled with a capital “H”.
Context: ...here are GitHub Action workflows in the .github/workflows directory already configured...

(GITHUB)

backend/README.md

[grammar] ~44-~44: Use a hyphen to join words.
Context: ...ose watch ``` There is also a commented out command override, you can uncommen...

(QB_NEW_EN_HYPHEN)


[grammar] ~78-~78: Use a hyphen to join words.
Context: ...runs the live reloading server that auto reloads when it detects code changes. N...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
SECURITY.md

19-19: Dollar signs used before commands without showing output

(MD014, commands-show-output)


25-25: Dollar signs used before commands without showing output

(MD014, commands-show-output)

development.md

105-105: Heading levels should only increment by one level at a time
Expected: h3; Actual: h4

(MD001, heading-increment)

README.md

15-15: Bare URL used

(MD034, no-bare-urls)

release-notes.md

574-574: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


575-575: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


576-576: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


577-577: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


578-578: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


579-579: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


580-580: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


581-581: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


582-582: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


583-583: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


691-691: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


692-692: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


693-693: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


694-694: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


695-695: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


696-696: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


697-697: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


698-698: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


699-699: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


705-705: Bare URL used

(MD034, no-bare-urls)


737-737: Bare URL used

(MD034, no-bare-urls)

backend/README.md

19-19: Dollar signs used before commands without showing output

(MD014, commands-show-output)


25-25: Dollar signs used before commands without showing output

(MD014, commands-show-output)


41-41: Dollar signs used before commands without showing output

(MD014, commands-show-output)


49-49: Dollar signs used before commands without showing output

(MD014, commands-show-output)


55-55: Dollar signs used before commands without showing output

(MD014, commands-show-output)


69-69: Dollar signs used before commands without showing output

(MD014, commands-show-output)


89-89: Dollar signs used before commands without showing output

(MD014, commands-show-output)


125-125: Dollar signs used before commands without showing output

(MD014, commands-show-output)


133-133: Dollar signs used before commands without showing output

(MD014, commands-show-output)


141-141: Dollar signs used before commands without showing output

(MD014, commands-show-output)


153-153: Dollar signs used before commands without showing output

(MD014, commands-show-output)

🪛 Ruff (0.14.8)
backend/app/core/middleware.py

12-12: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


17-17: Local variable client_ip is assigned to but never used

Remove assignment to unused variable client_ip

(F841)

backend/app/core/exception_handlers.py

13-13: Unused function argument: request

(ARG001)


20-20: Unused function argument: request

(ARG001)


27-27: Unused function argument: request

(ARG001)

backend/app/utils.py

6-6: typing.Dict is deprecated, use dict instead

(UP035)


37-37: No newline at end of file

Add trailing newline

(W292)

backend/app/core/db.py

22-22: No newline at end of file

Add trailing newline

(W292)

backend/app/api/routes/utils.py

10-10: No newline at end of file

Add trailing newline

(W292)

🪛 Shellcheck (0.11.0)
scripts/test-local.sh

[warning] 8-8: Quote this to prevent word splitting.

(SC2046)

🔇 Additional comments (38)
scripts/build.sh (1)

1-10: LGTM!

The build script correctly enforces the TAG variable requirement and provides a sensible default for FRONTEND_ENV. Error handling with set -e ensures the script exits on failures.

backend/scripts/test.sh (1)

1-9: LGTM!

The test script correctly runs pytest with coverage tracking and generates multiple report formats (console, HTML, XML) for different use cases. Error handling is appropriate.

backend/scripts/tests-start.sh (1)

1-15: LGTM!

The test startup script correctly orchestrates the test workflow: environment setup → pre-start health check → database migrations → test execution. Error handling for migration failures is explicit and appropriate.

backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py (3)

20-43: Migration logic is correct.

The upgrade function properly adds 255-character length constraints to the string fields in User and Item tables. The implementation is clean and the constraints are appropriate for typical email addresses, names, titles, and short descriptions.


46-69: Downgrade logic is correctly symmetric.

The downgrade function properly reverts all four field length constraints back to unlimited strings, maintaining consistency with the upgrade path.


14-15: The migration chain is correct. The migration 9c0a54914c78 properly depends on e2412789c190, and all migrations form a valid linear chain: e2412789c1909c0a54914c78d98dd8ec85a31a31ce608336. No branching or ordering issues detected.

backend/app/initial_data.py (1)

11-19: LGTM! Initial data setup structure is well-organized.

The structure correctly creates a session and delegates to init_db(). While init_db() is currently a no-op (as noted in the PR description about implementing guardrails separately), this provides the proper scaffolding for future data initialization.

backend/scripts/lint.sh (1)

1-8: LGTM! Standard linting workflow.

The script correctly implements a comprehensive linting workflow with type checking (mypy), linting (ruff check), and format validation (ruff format --check). Error handling with set -e ensures failures are caught.

backend/scripts/format.sh (1)

1-5: LGTM! Standard formatting workflow.

The script correctly applies automatic fixes and formatting using ruff. The use of POSIX shell (/bin/sh) is appropriate for this simple script.

development.md (1)

1-168: Excellent development documentation!

The documentation provides comprehensive guidance on Docker Compose setup, local development workflows, environment configuration, and pre-commit tooling. This will be very helpful for contributors getting started with the project.

deployment.md (1)

1-205: Comprehensive deployment guide!

Excellent documentation covering server preparation, environment configuration, secret management, Docker Compose deployment, and GitHub Actions CI/CD setup. The step-by-step instructions for setting up self-hosted runners are particularly helpful.

scripts/test.sh (1)

1-11: LGTM! Well-structured test workflow.

The script correctly orchestrates the test workflow with Docker Compose v2 commands. It ensures a clean state by tearing down containers before and after tests, and properly forwards arguments to the test runner.

backend/app/alembic/script.py.mako (1)

1-25: LGTM!

Standard Alembic Mako template with appropriate imports for SQLModel integration. The template correctly includes sqlmodel.sql.sqltypes to support SQLModel-specific column types in auto-generated migrations.

backend/app/api/main.py (1)

1-10: LGTM with a minor note on the commented code.

The router setup is clean. Note that the commented-out block (lines 9-10) references settings and private which are not imported in this file. If you plan to enable this later, ensure the necessary imports are added.

scripts/deploy.sh (1)

1-15: LGTM!

The deployment script properly validates required environment variables and follows a standard Docker Swarm deployment pattern.

Note: The script depends on docker-auto-labels (line 13), which is a third-party tool. Ensure this is documented in deployment prerequisites or consider adding a check for its availability.

backend/app/main.py (2)

20-21: Verify environment value consistency for Sentry initialization.

The condition checks for "local" environment, but .env.example defines ENVIRONMENT=development. Ensure the intended behavior is correct—Sentry will be initialized in development environment with this logic.


29-30: Middleware ordering note.

CorrelationIdMiddleware is added after the HTTP logger middleware. With ASGI middleware, the last-added middleware runs first on requests. This means correlation IDs will be available to the logger, which is the correct order.

backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py (1)

21-55: Migration logic is sound but verify on non-empty databases.

The upgrade correctly:

  1. Creates the uuid-ossp extension
  2. Adds new UUID columns and populates them
  3. Properly remaps the foreign key relationship before dropping old columns

One consideration: dropping and recreating primary keys on tables with data in production requires careful testing. Ensure this migration is tested against a database with representative data.

backend/alembic.ini (1)

1-71: Standard Alembic configuration looks good.

The configuration follows standard Alembic patterns. The sqlalchemy.url is intentionally omitted here since it's configured programmatically via env.py using the application settings, which is the correct approach for environment-based configuration.

backend/app/tests_pre_start.py (1)

32-35: LGTM on main() structure.

The main function correctly logs initialization status and invokes the retry-enabled init function with the engine. The module guard pattern is appropriate for a standalone script.

backend/app/core/exception_handlers.py (3)

1-8: LGTM on imports and module structure.

The imports are appropriate and the module is well-organized for centralized exception handling.


11-17: LGTM for validation error handler.

The handler correctly processes Pydantic validation errors and formats them using the centralized APIResponse.failure_response. The request parameter is required by FastAPI's exception handler signature despite being unused (ignore the static analysis hint).


19-24: Add type guard for dict details in APIResponse.failure_response.

HTTPException.detail accepts any JSON-serializable value, including dicts, but APIResponse.failure_response is typed to accept only str | list and assumes list items have loc and msg keys. If an HTTPException is raised with a dict detail, it would either be assigned as-is (semantically incorrect) or cause a KeyError when the code tries to access expected keys. Add a type guard to handle dict details or document that only str/list details are supported.

backend/README.md (1)

1-156: Well-structured documentation for the backend workflow.

The README provides comprehensive guidance covering Docker Compose setup, dependency management with uv, VS Code integration, testing, and Alembic migrations. This will be valuable for onboarding developers.

backend/app/backend_pre_start.py (1)

32-39: LGTM on the main entry point.

The main function and script entry point are correctly structured.

backend/app/alembic/versions/e2412789c190_initialize_models.py (1)

19-46: LGTM on overall migration structure.

The migration correctly creates the user and item tables with appropriate columns, constraints, and a unique index on user.email. The downgrade() function properly reverses the operations in the correct order.

backend/app/api/deps.py (1)

12-14: LGTM on the database session dependency.

The get_db() generator correctly manages the SQLModel session lifecycle using a context manager.

backend/app/alembic/env.py (3)

32-33: LGTM! Clean database URL retrieval.

The function properly converts the PostgresDsn object to a string for Alembic configuration.


36-54: LGTM! Standard Alembic offline migration pattern.

The offline migration configuration is correct with appropriate options for SQL generation and type comparison.


57-78: LGTM! Proper online migration setup.

The online migration configuration correctly uses NullPool for migrations and maintains consistency with offline mode settings.

backend/app/utils.py (2)

13-17: LGTM! Well-designed generic response model.

The use of generics provides type safety while maintaining flexibility for different response types.


19-23: LGTM! Clean success response factory.

The method provides a clear, type-safe way to construct successful API responses.

backend/app/core/config.py (1)

57-58: LGTM! Proper superuser configuration.

The use of EmailStr for validation and required fields ensures proper superuser setup.

docker-compose.yml (5)

5-26: LGTM! Well-configured database service.

The service properly uses required environment variables, healthchecks, and persistent storage. The port exposure (5432:5432) allows direct database access from the host, which is useful for development but consider removing in production configurations.


38-52: LGTM! Proper prestart service configuration.

The service correctly waits for database health before running migrations/setup scripts, and the profile makes it opt-in.


75-76: Development-only configuration noted.

The --reload flag is appropriate for development (as indicated by the PR description), but ensure this docker-compose.yml is not used in production deployments.


77-87: LGTM! Well-configured development watch.

The watch configuration properly syncs source changes and rebuilds on dependency updates, providing a good developer experience.


70-74: No action needed. The python:3.10 image is based on buildpack-deps, which includes a large number of extremely common Debian packages, and this variant includes curl, wget, and ca-certificates packages. The healthcheck will function correctly as curl is available in the backend container.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
backend/app/utils.py (1)

5-6: Avoid configuring logging in library modules.

Calling logging.basicConfig() in a utility module can override the application's logging configuration. This should be handled at the application entry point (e.g., in main.py).

🔎 Apply this diff to remove the basicConfig call:
-logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
🧹 Nitpick comments (4)
.env.example (1)

51-51: Add blank line at end of file.

Environment files should end with a newline character for standard file formatting conventions.

🔎 Apply this diff:
 # require as a env if you want to use doc transformation
 OPENAI_API_KEY=<ADD-KEY>
 GUARDRAILS_HUB_API_KEY=<ADD-KEY>
+
backend/app/backend_pre_start.py (2)

12-13: Consider making retry configuration environment-driven.

The hardcoded 5-minute timeout (300 attempts × 1 second) may be excessive for some deployment environments. Consider making these values configurable via environment variables for greater flexibility across different contexts.

🔎 Example implementation
+import os
+
-max_tries = 60 * 5  # 5 minutes
-wait_seconds = 1
+max_tries = int(os.getenv("DB_MAX_RETRIES", "300"))  # Default: 5 minutes
+wait_seconds = int(os.getenv("DB_RETRY_WAIT_SECONDS", "1"))

22-29: Simplify redundant exception handling.

The current pattern of catching, logging, and re-raising the exception is redundant with tenacity's retry mechanism. The @retry decorator already handles retries and logs via after_log. Consider either removing the try-except block or adding meaningful context to the error message rather than just re-raising.

🔎 Option 1: Remove redundant try-except (preferred)
 def init(db_engine: Engine) -> None:
-    try:
-        with Session(db_engine) as session:
-            # Try to create session to check if DB is awake
-            session.exec(select(1))
-    except Exception as e:
-        logger.error(e)
-        raise e
+    with Session(db_engine) as session:
+        # Try to create session to check if DB is awake
+        session.exec(select(1))
🔎 Option 2: Add context to error message
     try:
         with Session(db_engine) as session:
             # Try to create session to check if DB is awake
             session.exec(select(1))
     except Exception as e:
-        logger.error(e)
-        raise e
+        logger.error(f"Failed to connect to database: {e}", exc_info=True)
+        raise
backend/app/utils.py (1)

3-3: Modernize type hint to use built-in dict.

The typing.Dict is deprecated in favor of the built-in dict for Python 3.9+.

🔎 Apply this diff to use built-in `dict`:
-from typing import Any, Dict, Generic, Optional, TypeVar
+from typing import Any, Generic, Optional, TypeVar

Then update line 14:

-    metadata: Optional[Dict[str, Any]] = None
+    metadata: Optional[dict[str, Any]] = None

And line 18:

-        cls, data: T, metadata: Optional[Dict[str, Any]] = None
+        cls, data: T, metadata: Optional[dict[str, Any]] = None

And line 27:

-        metadata: Optional[Dict[str, Any]] = None,
+        metadata: Optional[dict[str, Any]] = None,
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a85ed62 and e792dfa.

📒 Files selected for processing (7)
  • .env.example (1 hunks)
  • backend/app/backend_pre_start.py (1 hunks)
  • backend/app/main.py (1 hunks)
  • backend/app/utils.py (1 hunks)
  • backend/pyproject.toml (1 hunks)
  • backend/scripts/prestart.sh (1 hunks)
  • release-notes.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • release-notes.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/app/main.py
  • backend/scripts/prestart.sh
  • backend/pyproject.toml
🧰 Additional context used
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 17-17: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 22-22: [UnorderedKey] The FIRST_SUPERUSER key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 23-23: [UnorderedKey] The FIRST_SUPERUSER_PASSWORD key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 24-24: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 24-24: [UnorderedKey] The EMAIL_TEST_USER key should go before the FIRST_SUPERUSER key

(UnorderedKey)


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

(UnorderedKey)


[warning] 35-35: [UnorderedKey] The POSTGRES_DB key should go before the POSTGRES_PORT key

(UnorderedKey)


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

(UnorderedKey)


[warning] 51-51: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 51-51: [UnorderedKey] The GUARDRAILS_HUB_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)

🪛 Ruff (0.14.8)
backend/app/utils.py

3-3: typing.Dict is deprecated, use dict instead

(UP035)


36-36: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (6)
backend/app/backend_pre_start.py (4)

1-10: LGTM!

The imports and logging setup are appropriate for a database readiness check utility. The logging configuration is clear and straightforward.


16-21: LGTM!

The retry decorator is properly configured. The past review comment regarding the deprecated logging.WARN has been correctly addressed—the code now uses logging.WARNING as recommended.


32-35: LGTM!

The main orchestration function is clean and provides clear logging for the initialization sequence.


38-39: LGTM!

Standard entry point pattern is correctly implemented.

backend/app/utils.py (2)

8-20: Well-designed generic response model.

The APIResponse class with generic typing provides a clean, type-safe structure for standardizing API responses. The success_response classmethod correctly constructs success cases with appropriate defaults.


22-36: Error handling properly addresses previous concerns.

The failure_response method now uses .get() with appropriate defaults to safely access dictionary keys, preventing KeyError exceptions for malformed error dictionaries. The method correctly handles both string and list error formats.

@AkhileshNegi
Copy link
Contributor

would be good to at least have a small feature/functionality in; doing a review just on the scaffolding may result in adding few stale files

@rkritika1508 rkritika1508 changed the title Added boilerplate code and basic auth setup Added boilerplate code, auth, validators and guardrails API Dec 19, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

♻️ Duplicate comments (6)
.env.example (2)

26-30: Clarify cron functionality requirements.

A previous reviewer questioned whether cron functionality is needed. The presence of API_BASE_URL and CRON_INTERVAL_MINUTES suggests background job scheduling, but the use case isn't documented.


5-5: Remove Traefik reference per previous feedback.

Line 5 still contains a Traefik comment, but previous feedback requested removing all Traefik mentions from the repository.

🔎 Proposed fix
-# used by Traefik to transmit traffic and aqcuire TLS certificates
+# Domain used for deployment and routing configuration
backend/app/core/config.py (4)

33-33: SECRET_KEY regenerates on each restart, invalidating all tokens.

The SECRET_KEY is evaluated at module import time using secrets.token_urlsafe(32), causing a new key on every application restart. This invalidates all existing JWT tokens and sessions.

🔎 Proposed fix
-    SECRET_KEY: str = secrets.token_urlsafe(32)
+    SECRET_KEY: str

Ensure SECRET_KEY is set via environment variable in .env files.


43-44: Empty password defaults create security risks.

POSTGRES_PASSWORD and POSTGRES_DB default to empty strings, which could allow unauthenticated connections in certain configurations. The validator at lines 90-98 only checks for "changethis" and won't catch these empty defaults.

🔎 Proposed fix
-    POSTGRES_PASSWORD: str = ""
-    POSTGRES_DB: str = ""
+    POSTGRES_PASSWORD: str
+    POSTGRES_DB: str

90-98: Validator logic doesn't align with actual security risks.

The validator has multiple issues:

  1. Line 92 checks SECRET_KEY for "changethis", but the default is secrets.token_urlsafe(32), so this check never triggers.
  2. Line 93 checks POSTGRES_PASSWORD for "changethis" but doesn't catch the empty string default ("").
🔎 Proposed fix

Update the validator to check for empty strings and remove the SECRET_KEY check (or validate minimum length):

     @model_validator(mode="after")
     def _enforce_non_default_secrets(self) -> Self:
-        self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
         self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD)
         self._check_default_secret(
             "FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
         )
+        
+        # Validate non-empty passwords
+        if not self.POSTGRES_PASSWORD:
+            raise ValueError("POSTGRES_PASSWORD cannot be empty")
+        if not self.FIRST_SUPERUSER_PASSWORD:
+            raise ValueError("FIRST_SUPERUSER_PASSWORD cannot be empty")
 
         return self

38-38: Add GUARDRAILS_HUB_API_KEY configuration field.

Based on previous feedback and the presence of GUARDRAILS_HUB_API_KEY in .env.example, this field should be added to the Settings model.

🔎 Proposed addition
     AUTH_TOKEN: str
+    GUARDRAILS_HUB_API_KEY: str | None = None
     PROJECT_NAME: str
🧹 Nitpick comments (29)
backend/scripts/install_guardrails_from_hub.sh (2)

24-27: Quote command substitutions to prevent potential word splitting.

The static analysis warning is valid. While the current implementation likely works given that each substitution produces a single flag, quoting the command substitutions is a best practice that prevents unexpected word splitting.

Based on static analysis hints (Shellcheck SC2046).

🔎 Proposed fix for quoting command substitutions
 guardrails configure \
   --token "$GUARDRAILS_HUB_API_KEY" \
-  $( [[ "$ENABLE_METRICS" == "true" ]] && echo "--enable-metrics" || echo "--disable-metrics" ) \
-  $( [[ "$ENABLE_REMOTE_INFERENCING" == "true" ]] && echo "--enable-remote-inferencing" || echo "--disable-remote-inferencing" )
+  "$( [[ "$ENABLE_METRICS" == "true" ]] && echo "--enable-metrics" || echo "--disable-metrics" )" \
+  "$( [[ "$ENABLE_REMOTE_INFERENCING" == "true" ]] && echo "--enable-remote-inferencing" || echo "--disable-remote-inferencing" )"

48-51: Consider a more robust iteration pattern for better safety.

The loop iterates over an unquoted $HUB_SOURCES variable, which relies on word splitting. While validator source URIs are unlikely to contain spaces, using a safer iteration pattern (e.g., reading line-by-line or using an array) would follow bash best practices.

🔎 Alternative: Read jq output line-by-line
-for SRC in $HUB_SOURCES; do
-  echo "Installing Guardrails hub validator: $SRC"
-  guardrails hub install "$SRC"
-done
+while IFS= read -r SRC; do
+  echo "Installing Guardrails hub validator: $SRC"
+  guardrails hub install "$SRC"
+done < <(jq -r '
+  .validators[]
+  | select(.source != "local")
+  | .source
+' "$MANIFEST_FILE")

This approach eliminates the intermediate HUB_SOURCES variable and safely handles sources with special characters.

backend/scripts/entrypoint.sh (1)

1-8: Use absolute paths in the entrypoint.sh script for clarity and consistency with Docker best practices.

The WORKDIR is correctly set to /app/ in the Dockerfile, so the relative paths in entrypoint.sh will resolve correctly. However, for clarity and reliability, absolute paths should always be used. Update the script to use absolute paths:

/app/scripts/install_guardrails_from_hub.sh

and for the FastAPI command:

exec fastapi run --workers 4 /app/app/main.py

While relative paths are technically allowed, they are interpreted based on the last WORKDIR value, which can create confusion or bugs.

.env.example (1)

52-52: Add trailing newline.

The file is missing a trailing newline, which is a common convention for text files.

🔎 Proposed fix
 AUTH_TOKEN=<ADD-TOKEN>
+
backend/app/api/routes/utils.py (2)

2-2: Remove unused import.

AuthDep is imported but never used in this module.

🔎 Proposed fix
 from fastapi import APIRouter
-from app.api.deps import AuthDep
 

8-8: Add trailing newline.

🔎 Proposed fix
     return True
+
backend/app/models/base_validator_config.py (1)

27-27: Add trailing newline.

🔎 Proposed fix
         )
+
backend/app/core/api_response.py (1)

16-16: Add trailing newline.

🔎 Proposed fix
         return cls(success=False, error=error, metadata=metadata)
+
backend/app/core/guardrail_controller.py (3)

5-7: Add type hints for better type safety.

The build_guard function lacks type annotations for its parameter and return value.

🔎 Proposed improvement
-def build_guard(validator_items):
+from typing import Sequence
+
+def build_guard(validator_items: Sequence[ValidatorConfigItem]) -> Guard:
     validators = [v_item.build() for v_item in validator_items]
     return Guard().use_many(*validators)

9-12: Add type hints and consider defensive checks for introspection logic.

The get_validator_config_models function lacks return type annotation and makes assumptions about the structure of ValidatorConfigItem without validation.

🔎 Proposed improvement
-def get_validator_config_models():
+from typing import Sequence, Type
+
+def get_validator_config_models() -> tuple[Type, ...]:
     annotated_args = get_args(ValidatorConfigItem)
+    if not annotated_args:
+        return ()
     union_type = annotated_args[0]
     return get_args(union_type)

12-12: Add trailing newline.

🔎 Proposed fix
     return get_args(union_type)
+
backend/app/core/constants.py (1)

6-6: Add trailing newline.

🔎 Proposed fix
 SCORE = "score"
+
backend/app/core/enum.py (1)

13-13: Add trailing newline.

🔎 Proposed fix
     All = "all"
+
backend/app/api/main.py (1)

9-10: Commented-out code is incomplete.

If this conditional is intended for future use, the commented code references settings and private which are not imported. Consider either removing this dead code or adding a TODO with context, along with the required imports when ready.

backend/app/models/gender_assumption_bias_safety_validator_config.py (2)

1-1: Remove unused import ClassVar.

ClassVar is imported but not used in this file.

🔎 Proposed fix
-from typing import ClassVar, List, Literal, Optional
+from typing import List, Literal, Optional

6-8: Optional: Consider using Field(default_factory=...) for clarity with mutable list defaults.

While using default_factory can be done in Pydantic, it is not required because Pydantic will create a deep copy of the default value when creating each instance. The current code categories: Optional[List[BiasCategories]] = [BiasCategories.All] is safe and will not cause shared state between instances. However, using Field(default_factory=...) is more idiomatic and clearer for tooling support:

+from sqlmodel import Field
+
 class GenderAssumptionBiasSafetyValidatorConfig(BaseValidatorConfig):
     type: Literal["gender_assumption_bias"]
-    categories: Optional[List[BiasCategories]] = [BiasCategories.All]
+    categories: Optional[List[BiasCategories]] = Field(default_factory=lambda: [BiasCategories.All])
backend/app/core/validators/pii_remover.py (1)

46-46: Setting environment variable in __init__ affects global state.

Setting os.environ["TOKENIZERS_PARALLELISM"] in the constructor is a side effect that persists across the entire process. Consider setting this at module load time or in application startup instead.

backend/app/models/guardrail_config.py (1)

4-5: Acknowledge TODO for auto-discovery.

The TODO for an auto-discovery mechanism is a good improvement to consider for reducing boilerplate when adding new validators.

Would you like me to open an issue to track this improvement, or help design an auto-discovery pattern using Python's entry points or module introspection?

backend/app/models/lexical_slur_safety_validator_config.py (1)

1-1: Use modern list type hint instead of typing.List.

Per static analysis, typing.List is deprecated in Python 3.9+. Use the built-in list type hint.

🔎 Proposed fix
-from typing import List, Literal
+from typing import Literal

And update line 8:

-    languages: List[str] = ["en", "hi"]
+    languages: list[str] = ["en", "hi"]
backend/app/models/ban_list_safety_validator_config.py (1)

1-3: Remove unused imports and use modern type hints.

get_validator and ClassVar are imported but never used. Additionally, typing.List is deprecated in Python 3.9+.

🔎 Proposed fix
 from guardrails.hub import BanList
-from guardrails.utils.validator_utils import get_validator
-from typing import ClassVar, List, Literal
+from typing import Literal

 from app.models.base_validator_config import BaseValidatorConfig
 from app.core.constants import BAN_LIST

 class BanListSafetyValidatorConfig(BaseValidatorConfig):
     type: Literal[f"{BAN_LIST}"]
-    banned_words: List[str]
+    banned_words: list[str]
backend/app/core/validators/utils/language_detector.py (1)

15-21: Unused instance variable self.label.

self.label is assigned to None but never used elsewhere in the class.

🔎 Proposed fix
     def __init__(self):
         self.lid = pipeline(
             task = "text-classification",
             model="papluca/xlm-roberta-base-language-detection",
             top_k=1
         )
-        self.label = None
backend/app/api/routes/guardrails.py (2)

45-56: Consider narrowing exception handling and sanitizing error output.

Catching all Exception types may mask programming errors. Additionally, str(e) in the response could leak internal details (stack traces, file paths) in production.

Consider:

  1. Catching specific expected exceptions (e.g., ValidationError, ConfigurationError)
  2. Logging the full exception server-side
  3. Returning a generic message to the client in production

59-104: Significant duplication with run_input_guardrails.

The input and output endpoints share nearly identical logic. Consider extracting a shared helper to reduce duplication.

🔎 Example refactor
async def _run_guardrails(text: str, validators: list, result_key: str):
    response_id = str(uuid.uuid4())
    try:
        guard = build_guard(validators)
        result = guard.validate(text)
        
        if result.validated_output is not None:
            return APIResponse.success_response(
                data={"response_id": response_id, result_key: result.validated_output}
            )
        # ... validation failure handling
    except Exception as e:
        # ... error handling

@router.post("/input/")
async def run_input_guardrails(payload: GuardrailInputRequest, _: AuthDep):
    return await _run_guardrails(payload.input, payload.validators, "safe_input")

@router.post("/output")
async def run_output_guardrails(payload: GuardrailOutputRequest, _: AuthDep):
    return await _run_guardrails(payload.output, payload.validators, "safe_output")
backend/app/core/validators/gender_assumption_bias.py (1)

10-10: Use modern type hints.

typing.Callable and typing.List are deprecated. Use collections.abc.Callable and built-in list.

🔎 Proposed fix
-from typing import Callable, List, Optional
+from collections.abc import Callable
+from typing import Optional

Then update the type hints:

-        categories: Optional[List[BiasCategories]] = None,
+        categories: Optional[list[BiasCategories]] = None,
backend/app/core/validators/lexical_slur.py (5)

9-9: Modernize import to use collections.abc.Callable.

For Python 3.9+, prefer importing Callable from collections.abc rather than typing.

🔎 Suggested fix
-from typing import Callable, Optional
+from collections.abc import Callable
+from typing import Optional

36-36: Remove unnecessary initialization.

The self.text attribute is set to None here but immediately overwritten in the _validate method (line 40). This initialization serves no purpose.

🔎 Suggested fix
         self.languages = languages or ["en", "hi"]
         self.slur_list = self.load_slur_list()
-        self.text = None
         super().__init__(on_fail=on_fail, search_words=self.slur_list)

52-61: Consolidate redundant length checks.

The condition len(detected_slurs) > 0 is checked twice (lines 52 and 57). The first check and replacement loop can be merged with the final return logic for cleaner code.

🔎 Suggested refactor
-        if len(detected_slurs) > 0:
-            for word in words:
-                if word in detected_slurs:
-                    self.text = self.text.replace(word, "[REDACTED_SLUR]")
-
         if len(detected_slurs) > 0:
+            for word in words:
+                if word in detected_slurs:
+                    self.text = self.text.replace(word, "[REDACTED_SLUR]")
             return FailResult(
                 error_message=f"Mentioned toxic words: {', '.join(detected_slurs)}",
                 fix_value=self.text
             )
 
         return PassResult(value=self.text)

92-92: Track the TODO for language filtering.

The language filtering feature is not yet implemented. This could lead to false positives if users specify language preferences that aren't respected.

Would you like me to open a new issue to track implementing language-based filtering for the slur list?


86-100: Consider class-level caching for the slur list CSV to optimize performance.

The slur list CSV is loaded during every validator instantiation. Since validators are created per-request in the guardrail controller, this causes repeated CSV parsing on each validation call. Moving the loaded slur list to a class-level cached attribute would eliminate this overhead and align with the pattern of other similar configuration-heavy validators.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c6e74e and d7828b2.

⛔ Files ignored due to path filters (3)
  • backend/app/core/validators/utils/files/curated_slurlist_hi_en.csv is excluded by !**/*.csv
  • backend/app/core/validators/utils/files/gender_assumption_bias_words.csv is excluded by !**/*.csv
  • backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • .env.example (1 hunks)
  • backend/.gitignore (1 hunks)
  • backend/Dockerfile (1 hunks)
  • backend/app/api/main.py (1 hunks)
  • backend/app/api/routes/guardrails.py (1 hunks)
  • backend/app/api/routes/utils.py (1 hunks)
  • backend/app/core/api_response.py (1 hunks)
  • backend/app/core/config.py (1 hunks)
  • backend/app/core/constants.py (1 hunks)
  • backend/app/core/enum.py (1 hunks)
  • backend/app/core/guardrail_controller.py (1 hunks)
  • backend/app/core/validators/gender_assumption_bias.py (1 hunks)
  • backend/app/core/validators/lexical_slur.py (1 hunks)
  • backend/app/core/validators/pii_remover.py (1 hunks)
  • backend/app/core/validators/utils/language_detector.py (1 hunks)
  • backend/app/core/validators/validators.json (1 hunks)
  • backend/app/models/ban_list_safety_validator_config.py (1 hunks)
  • backend/app/models/base_validator_config.py (1 hunks)
  • backend/app/models/gender_assumption_bias_safety_validator_config.py (1 hunks)
  • backend/app/models/guardrail_config.py (1 hunks)
  • backend/app/models/lexical_slur_safety_validator_config.py (1 hunks)
  • backend/app/models/pii_remover_safety_validator_config.py (1 hunks)
  • backend/scripts/entrypoint.sh (1 hunks)
  • backend/scripts/install_guardrails_from_hub.sh (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • backend/app/core/validators/validators.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/.gitignore
  • backend/Dockerfile
🧰 Additional context used
🧬 Code graph analysis (11)
backend/app/core/guardrail_controller.py (1)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (13-19)
backend/app/core/validators/pii_remover.py (1)
backend/app/core/validators/utils/language_detector.py (3)
  • LanguageDetector (6-49)
  • predict (28-43)
  • is_hindi (45-46)
backend/app/models/base_validator_config.py (4)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (13-19)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (10-14)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (11-16)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (12-16)
backend/app/models/pii_remover_safety_validator_config.py (5)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (7-27)
  • build (24-27)
  • resolve_on_fail (12-22)
backend/app/core/validators/pii_remover.py (1)
  • PIIRemover (20-74)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (10-14)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (11-16)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (12-16)
backend/app/api/routes/guardrails.py (3)
backend/app/models/guardrail_config.py (2)
  • GuardrailInputRequest (22-24)
  • GuardrailOutputRequest (26-28)
backend/app/core/api_response.py (3)
  • APIResponse (4-16)
  • success_response (11-12)
  • failure_response (15-16)
backend/app/core/guardrail_controller.py (2)
  • build_guard (5-7)
  • get_validator_config_models (9-12)
backend/app/models/gender_assumption_bias_safety_validator_config.py (6)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (7-27)
  • build (24-27)
  • resolve_on_fail (12-22)
backend/app/core/enum.py (1)
  • BiasCategories (9-13)
backend/app/core/validators/gender_assumption_bias.py (1)
  • GenderAssumptionBias (18-78)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (13-19)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (11-16)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (12-16)
backend/app/models/lexical_slur_safety_validator_config.py (4)
backend/app/core/enum.py (1)
  • SlurSeverity (3-7)
backend/app/core/validators/lexical_slur.py (1)
  • LexicalSlur (22-101)
backend/app/models/base_validator_config.py (2)
  • build (24-27)
  • resolve_on_fail (12-22)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (12-16)
backend/app/models/guardrail_config.py (4)
backend/app/models/ban_list_safety_validator_config.py (1)
  • BanListSafetyValidatorConfig (8-16)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • GenderAssumptionBiasSafetyValidatorConfig (6-14)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • LexicalSlurSafetyValidatorConfig (6-16)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • PIIRemoverSafetyValidatorConfig (7-19)
backend/app/models/ban_list_safety_validator_config.py (4)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (7-27)
  • build (24-27)
  • resolve_on_fail (12-22)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (13-19)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (10-14)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (11-16)
backend/app/core/validators/gender_assumption_bias.py (2)
backend/app/core/config.py (1)
  • Settings (26-98)
backend/app/core/enum.py (1)
  • BiasCategories (9-13)
backend/app/core/validators/lexical_slur.py (4)
backend/app/core/config.py (1)
  • Settings (26-98)
backend/app/core/enum.py (1)
  • SlurSeverity (3-7)
backend/app/core/validators/pii_remover.py (1)
  • _validate (51-65)
backend/app/core/validators/gender_assumption_bias.py (1)
  • _validate (32-55)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 17-17: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 22-22: [UnorderedKey] The FIRST_SUPERUSER key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 23-23: [UnorderedKey] The FIRST_SUPERUSER_PASSWORD key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 24-24: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 24-24: [UnorderedKey] The EMAIL_TEST_USER key should go before the FIRST_SUPERUSER key

(UnorderedKey)


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

(UnorderedKey)


[warning] 35-35: [UnorderedKey] The POSTGRES_DB key should go before the POSTGRES_PORT key

(UnorderedKey)


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

(UnorderedKey)


[warning] 51-51: [UnorderedKey] The GUARDRAILS_HUB_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 52-52: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 52-52: [UnorderedKey] The AUTH_TOKEN key should go before the GUARDRAILS_HUB_API_KEY key

(UnorderedKey)

🪛 Ruff (0.14.8)
backend/app/core/validators/utils/language_detector.py

6-6: Unnecessary parentheses after class definition

Remove parentheses

(UP039)


27-27: Use of functools.lru_cache or functools.cache on methods can lead to memory leaks

(B019)


49-49: No newline at end of file

Add trailing newline

(W292)

backend/app/core/guardrail_controller.py

12-12: No newline at end of file

Add trailing newline

(W292)

backend/app/core/constants.py

6-6: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/pii_remover.py

13-13: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


74-74: No newline at end of file

Add trailing newline

(W292)

backend/app/models/base_validator_config.py

27-27: No newline at end of file

Add trailing newline

(W292)

backend/app/core/enum.py

13-13: No newline at end of file

Add trailing newline

(W292)

backend/app/models/pii_remover_safety_validator_config.py

2-2: typing.List is deprecated, use list instead

(UP035)

backend/app/api/routes/guardrails.py

122-122: No newline at end of file

Add trailing newline

(W292)

backend/app/models/gender_assumption_bias_safety_validator_config.py

1-1: typing.List is deprecated, use list instead

(UP035)

backend/app/models/lexical_slur_safety_validator_config.py

1-1: typing.List is deprecated, use list instead

(UP035)

backend/app/models/guardrail_config.py

2-2: typing.List is deprecated, use list instead

(UP035)


13-18: Use X | Y for type annotations

Convert to X | Y

(UP007)


28-28: No newline at end of file

Add trailing newline

(W292)

backend/app/core/api_response.py

16-16: No newline at end of file

Add trailing newline

(W292)

backend/app/models/ban_list_safety_validator_config.py

3-3: typing.List is deprecated, use list instead

(UP035)


16-16: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/gender_assumption_bias.py

10-10: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


10-10: typing.List is deprecated, use list instead

(UP035)


25-25: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


78-78: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/lexical_slur.py

9-9: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)

backend/app/api/routes/utils.py

8-8: No newline at end of file

Add trailing newline

(W292)

🪛 Shellcheck (0.11.0)
backend/scripts/install_guardrails_from_hub.sh

[warning] 26-26: Quote this to prevent word splitting.

(SC2046)


[warning] 27-27: Quote this to prevent word splitting.

(SC2046)

🔇 Additional comments (13)
backend/app/core/api_response.py (1)

4-16: LGTM! Clean standardized response model.

The APIResponse model provides a consistent structure for API responses with convenient factory methods.

backend/app/core/constants.py (1)

1-6: LGTM! Clear constant definitions.

The constants are well-named and provide clear semantic meaning for use across the codebase.

backend/app/core/enum.py (1)

3-13: LGTM! Well-structured enum definitions.

Both SlurSeverity and BiasCategories enums provide clear categorization with consistent naming and the useful All option for comprehensive filtering.

backend/app/models/base_validator_config.py (1)

8-8: Type annotation incompatibility: enum member assigned to string literal annotation.

The on_fail field is annotated as Optional[ON_FAIL_STR] but assigned OnFailAction.FIX. Literal types may contain enum values, but if ON_FAIL_STR is a Literal type containing only string values (e.g., Literal["fix", "reask"]), then assigning an enum member creates a type mismatch. Change the default to the string literal "fix" to align with the annotation.

backend/app/api/main.py (1)

1-7: LGTM!

The centralized API router setup follows standard FastAPI patterns and correctly aggregates the route modules.

backend/app/models/gender_assumption_bias_safety_validator_config.py (1)

10-14: LGTM!

The build() method correctly follows the established pattern, passing categories and resolving on_fail via the base class method.

backend/app/models/pii_remover_safety_validator_config.py (1)

1-19: LGTM!

The config class follows the established pattern correctly. The build() method properly constructs a PIIRemover with all required parameters.

Optional: The static analysis tool notes typing.List is deprecated in favor of the built-in list (Python 3.9+), but this is a minor style preference and can be addressed in a future cleanup if desired.

backend/app/models/guardrail_config.py (1)

11-28: LGTM!

The discriminated union pattern using Field(discriminator="type") is correctly implemented for polymorphic validator configuration. The request models are well-structured.

Minor: Static analysis suggests using modern type syntax (list instead of List, X | Y instead of Union), but this is optional and can be addressed in a future cleanup.

backend/app/models/lexical_slur_safety_validator_config.py (1)

11-16: LGTM!

The build() method correctly constructs the LexicalSlur validator with proper parameter mapping. The SlurSeverity(self.severity) conversion aligns with the enum values defined in app.core.enum.

backend/app/models/ban_list_safety_validator_config.py (1)

12-16: LGTM!

The build() method correctly constructs the BanList validator. Add a trailing newline at end of file per static analysis.

backend/app/core/validators/utils/language_detector.py (1)

45-49: LGTM!

The helper methods are clean and straightforward. Add a trailing newline at end of file per static analysis.

backend/app/api/routes/guardrails.py (1)

106-122: LGTM with minor robustness consideration.

The endpoint correctly lists validators. Consider adding error handling for the schema key access on line 116 if validator schemas could vary.

backend/app/core/validators/lexical_slur.py (1)

65-84: Well-structured text normalization utilities.

The helper methods for text cleaning and normalization are well-implemented, using appropriate libraries (ftfy, unicodedata, emoji) to handle edge cases like mojibake, fancy Unicode fonts, and emoji removal.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (5)
backend/app/core/validators/gender_assumption_bias.py (2)

22-29: Mutable default argument remains unfixed.

This issue was flagged in a previous review. Using a mutable list [BiasCategories.All] as a default argument creates a shared object across all calls, leading to potential bugs if mutated.

🔎 Proposed fix
     def __init__(
         self, 
-        categories: Optional[List[BiasCategories]] = [BiasCategories.All],
+        categories: Optional[List[BiasCategories]] = None,
         on_fail: Optional[Callable] = OnFailAction.FIX
     ):
+        if categories is None:
+            categories = [BiasCategories.All]
         self.gender_bias_list = self.load_gender_bias_list(categories)
         self.text = None
         super().__init__(on_fail=on_fail)

71-83: Duplicate entries can still occur when BiasCategories.All is mixed with specific categories.

This issue was flagged in a previous review. If categories contains both BiasCategories.All and other specific categories, entries will be appended multiple times, causing redundant processing.

🔎 Proposed fix: early return for All
     def load_gender_bias_list(self, categories):
         file_path = Settings.GENDER_BIAS_LIST_FILEPATH
         neutral_term_col = 'neutral-term'
-        gender_bias_list = []

         try:
             df = pandas.read_csv(file_path)
         except FileNotFoundError:
             raise FileNotFoundError(f"Gender bias file not found at {file_path}")
         except Exception as e:
             raise ValueError(f"Failed to load gender bias list from {file_path}: {e}")

         df['word'] = df['word'].str.lower()
         df[neutral_term_col] = df[neutral_term_col].str.lower()

+        # If All is present, return all entries immediately
+        if BiasCategories.All in categories:
+            rows = df.to_dict(orient="records")
+            return [{
+                "word": row["word"],
+                neutral_term_col: row[neutral_term_col]
+            } for row in rows]
+
+        gender_bias_list = []
         for category in categories:
-            if category == BiasCategories.All:
-                temp = df
-            else:
-                temp = df[df['type'] == category.value]
+            temp = df[df['type'] == category.value]

             rows = temp.to_dict(orient="records")
             for row in rows:
                 gender_bias_list.append({
                     "word": row["word"],
                     neutral_term_col: row[neutral_term_col]
                 })
         return gender_bias_list
backend/app/core/validators/lexical_slur.py (1)

100-107: Severity filtering semantics remain counterintuitive.

This issue was flagged in a previous review. SlurSeverity.Low currently returns all severities (L, M, H), which conflicts with the enum naming. The enum name implies "Low severity slurs only," but the logic treats it as "low threshold (catch everything)."

Consider renaming the enum values to reflect threshold semantics (e.g., PermissiveThreshold, ModerateThreshold, StrictThreshold) or inverting the logic so Low returns only low-severity slurs.

backend/app/core/validators/pii_remover.py (2)

36-37: entity_types and threshold remain unused.

This issue was flagged in a previous review. The parameters are stored on the instance but never passed to self.analyzer.analyze() in run_english_presidio(), so the validator ignores these configuration options.

🔎 Proposed fix
     def run_english_presidio(self, text: str):
-        results = self.analyzer.analyze(text=text,
-                                language="en")
+        results = self.analyzer.analyze(
+            text=text,
+            language="en",
+            entities=self.entity_types if self.entity_types != ["ALL"] else None,
+            score_threshold=self.threshold
+        )
         anonymized = self.anonymizer.anonymize(text=text, analyzer_results=results)
         return anonymized.text

Also applies to: 66-70


72-73: Hinglish PII removal remains a no-op stub.

This issue was flagged in a previous review. The method returns the original text without any anonymization. If Hindi text contains PII, it won't be detected or removed, creating a security gap.

Consider adding a warning log or raising a NotImplementedError until this is implemented, so users are aware of the limitation.

🧹 Nitpick comments (12)
.env.example (1)

9-41: Reorder environment variables alphabetically and add trailing newline.

The static analysis tool (dotenv-linter) flags multiple key-ordering inconsistencies. Reorganizing variables alphabetically improves consistency and predictability, especially for template files that serve as deployment guidance. Additionally, add a blank line at the end of the file.

🔎 Proposed reordering
-DOMAIN=localhost
-
-# DOMAIN=localhost.tiangolo.com
-
-# Environment: "development", "testing", "staging", "production"
-
-ENVIRONMENT=development
-
-PROJECT_NAME="Kaapi-Guardrails"
-STACK_NAME=Kaapi-Guardrails
-
-#Backend
-SECRET_KEY=changethis
-[email protected]
-FIRST_SUPERUSER_PASSWORD=changethis
-EMAIL_TEST_USER="[email protected]"
-
-# API Base URL for cron scripts (defaults to http://localhost:8000 if not set)
-API_BASE_URL=http://localhost:8000
-
-# Postgres
-POSTGRES_SERVER=localhost
-POSTGRES_PORT=5432
-POSTGRES_DB=kaapi-guardrails
-POSTGRES_USER=postgres
-POSTGRES_PASSWORD=postgres
-
-SENTRY_DSN=
-
-# Configure these with your own Docker registry images
-
-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>
-AUTH_TOKEN=<ADD-TOKEN>
+# Environment: "development", "testing", "staging", "production"
+ENVIRONMENT=development
+
+API_BASE_URL=http://localhost:8000
+AUTH_TOKEN=<ADD-TOKEN>
+CALLBACK_CONNECT_TIMEOUT=3
+CALLBACK_READ_TIMEOUT=10
+DOMAIN=localhost
+# DOMAIN=localhost.tiangolo.com
+DOCKER_IMAGE_BACKEND=kaapi-guardrails-backend
+EMAIL_TEST_USER="[email protected]"
+[email protected]
+FIRST_SUPERUSER_PASSWORD=changethis
+GUARDRAILS_HUB_API_KEY=<ADD-KEY>
+OPENAI_API_KEY=<ADD-KEY>
+POSTGRES_DB=kaapi-guardrails
+POSTGRES_PASSWORD=postgres
+POSTGRES_PORT=5432
+POSTGRES_SERVER=localhost
+POSTGRES_USER=postgres
+PROJECT_NAME="Kaapi-Guardrails"
+SECRET_KEY=changethis
+SENTRY_DSN=
+STACK_NAME=Kaapi-Guardrails
copier.yml (1)

1-40: LGTM with optional improvement suggestion.

The template variables are well-structured with appropriate types, help text, and defaults. The "changethis" defaults are acceptable for template scaffolding, and generation instructions are provided for secret_key and postgres_password.

💡 Optional: Add password generation instructions for first_superuser_password

For consistency with secret_key and postgres_password, consider adding generation instructions to first_superuser_password:

 first_superuser_password:
   type: str
-  help: The password of the first superuser (in .env)
+  help: |
+    'The password of the first superuser, stored in .env,
+    you can generate one with:
+    python -c "import secrets; print(secrets.token_urlsafe(32))"'
   default: changethis
backend/app/core/validators/gender_assumption_bias.py (1)

83-83: Add trailing newline.

                 })
-        return gender_bias_list
+        return gender_bias_list
backend/app/core/validators/lexical_slur.py (2)

98-98: TODO: Language filtering not yet implemented.

Language filtering is deferred, which is acceptable for boilerplate scaffolding. However, ensure this is tracked.

Do you want me to open a new issue to track this language filtering implementation?


107-107: Add trailing newline.

-        return df['label'].tolist()
+        return df['label'].tolist()
backend/app/core/validators/pii_remover.py (1)

73-73: Add trailing newline.

     def run_hinglish_presidio(self, text: str):
-        return text
+        return text
backend/app/core/validators/utils/language_detector.py (4)

14-20: Unused instance variable self.label.

The self.label attribute is initialized but never used in the class. This appears to be leftover from an earlier implementation.

🔎 Proposed fix
     def __init__(self):
         self.lid = pipeline(
             task = "text-classification",
             model="papluca/xlm-roberta-base-language-detection",
             top_k=1
         )
-        self.label = None

26-41: Consider caching for repeated language detection calls.

The predict method calls the transformer model on every invocation. If the same text is validated multiple times (e.g., in a guardrails pipeline), this could impact performance. However, be mindful of memory leaks when caching instance methods.

Consider implementing a module-level cache or a bounded per-instance cache (e.g., using cachetools.LRUCache) if performance profiling shows this is a bottleneck. Avoid @lru_cache on instance methods as it prevents garbage collection.


5-5: Remove unnecessary parentheses after class definition.

-class LanguageDetector():
+class LanguageDetector:

47-47: Add trailing newline.

     def is_english(self, text: str):
-        return self.predict(text)[LABEL] == LANG_ENGLISH
+        return self.predict(text)[LABEL] == LANG_ENGLISH
backend/app/api/routes/guardrails.py (2)

24-29: Inconsistent response patterns between success and error paths.

Success responses return APIResponse.success_response() directly (lines 24-29), while error responses wrap APIResponse.failure_response() in a JSONResponse with .model_dump() (lines 32-44). Consider using a consistent pattern throughout.

💡 Option 1: Use APIResponse directly for both
-        return JSONResponse(
-            status_code=status.HTTP_400_BAD_REQUEST,
-            content=APIResponse.failure_response(
+        response = APIResponse.failure_response(
                 error={
                     "response_id": response_id,
                     "type": "validation_error",
                     "action": "reask" if result.failures else "fail",
                     "failures": [
                         f.failure_message for f in (result.failures or [])
                     ],
                 }
-            ).model_dump(),
         )
+        return JSONResponse(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            content=response.model_dump(),
+        )

Or wrap both success and error in JSONResponse with appropriate status codes.

Also applies to: 32-44


123-123: Add missing newline at end of file.

Static analysis tools expect a newline character at the end of the file per POSIX standards.

     return {"validators": validators}
+
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7828b2 and a5a1bf1.

⛔ Files ignored due to path filters (1)
  • backend/app/core/validators/utils/files/gender_assumption_bias_words.csv is excluded by !**/*.csv
📒 Files selected for processing (8)
  • .env.example
  • backend/app/api/routes/guardrails.py
  • backend/app/core/validators/gender_assumption_bias.py
  • backend/app/core/validators/lexical_slur.py
  • backend/app/core/validators/pii_remover.py
  • backend/app/core/validators/utils/language_detector.py
  • copier.yml
  • deployment.md
🧰 Additional context used
🧬 Code graph analysis (4)
backend/app/core/validators/pii_remover.py (3)
backend/app/core/validators/utils/language_detector.py (2)
  • LanguageDetector (5-47)
  • is_hindi (43-44)
backend/app/core/validators/gender_assumption_bias.py (1)
  • _validate (31-54)
backend/app/core/validators/lexical_slur.py (1)
  • _validate (39-63)
backend/app/api/routes/guardrails.py (3)
backend/app/models/guardrail_config.py (2)
  • GuardrailInputRequest (22-24)
  • GuardrailOutputRequest (26-28)
backend/app/core/api_response.py (3)
  • APIResponse (4-16)
  • success_response (11-12)
  • failure_response (15-16)
backend/app/core/guardrail_controller.py (2)
  • build_guard (5-7)
  • get_validator_config_models (9-12)
backend/app/core/validators/lexical_slur.py (2)
backend/app/core/config.py (1)
  • Settings (26-98)
backend/app/core/enum.py (1)
  • SlurSeverity (3-7)
backend/app/core/validators/gender_assumption_bias.py (1)
backend/app/core/enum.py (1)
  • BiasCategories (9-13)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 9-9: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 14-14: [UnorderedKey] The FIRST_SUPERUSER key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 15-15: [UnorderedKey] The FIRST_SUPERUSER_PASSWORD key should go before the SECRET_KEY key

(UnorderedKey)


[warning] 16-16: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 16-16: [UnorderedKey] The EMAIL_TEST_USER key should go before the FIRST_SUPERUSER key

(UnorderedKey)


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

(UnorderedKey)


[warning] 24-24: [UnorderedKey] The POSTGRES_DB key should go before the POSTGRES_PORT key

(UnorderedKey)


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

(UnorderedKey)


[warning] 40-40: [UnorderedKey] The GUARDRAILS_HUB_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 41-41: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 41-41: [UnorderedKey] The AUTH_TOKEN key should go before the GUARDRAILS_HUB_API_KEY key

(UnorderedKey)

🪛 Ruff (0.14.8)
backend/app/core/validators/pii_remover.py

13-13: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


73-73: No newline at end of file

Add trailing newline

(W292)

backend/app/api/routes/guardrails.py

123-123: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/utils/language_detector.py

5-5: Unnecessary parentheses after class definition

Remove parentheses

(UP039)


47-47: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/lexical_slur.py

9-9: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)

backend/app/core/validators/gender_assumption_bias.py

9-9: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


9-9: typing.List is deprecated, use list instead

(UP035)


24-24: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


83-83: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (6)
deployment.md (1)

1-7: Placeholder documentation aligns with prior feedback.

The file intentionally provides only section outlines, consistent with the previous review suggestion to keep it minimal and add a "coming soon" note. Once guardrails validators and additional features are implemented, this file can be populated with concrete deployment steps.

.env.example (1)

18-19: Clarify the necessity and scope of the API_BASE_URL (cron) variable.

A previous reviewer questioned whether cron functionality is necessary for this PR. Since this PR focuses on boilerplate and authentication scaffolding, and given feedback that functionality should take precedence over scaffolding, please confirm whether the cron-related configuration should be included or deferred to a follow-up PR when cron jobs are actually implemented.

backend/app/core/validators/lexical_slur.py (1)

89-94: Good error handling for CSV loading.

The CSV loading now includes proper error handling as recommended in the previous review. This prevents unhandled exceptions during validator initialization.

backend/app/core/validators/pii_remover.py (1)

54-57: Language detection logic fixed correctly.

The previous critical bug (comparing a dict to a boolean) has been resolved. The code now directly uses is_hindi(text) to determine the processing path, which is correct and clean.

backend/app/api/routes/guardrails.py (2)

1-10: Imports and router setup look good.

The uuid import properly addresses the past review comment about hardcoded response_id values. All dependencies are appropriately imported.


38-38: Clarify the logic for "reask" vs "fail" action.

Lines 38 and 86 set action="fail" when result.failures is empty, but this code is only reached when result.validated_output is None. This implies the guard returned neither a validated output nor failure messages, which seems like an edge case worth documenting or handling explicitly.

Please verify the expected behavior when the guardrails validate() method returns a result with both validated_output=None and failures=None/[]. Should this default to "fail" or does it indicate a different condition?

Also applies to: 86-86

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Fix all issues with AI Agents 🤖
In @backend/app/core/validators/lexical_slur.py:
- Around line 54-56: The current loop uses self.text.replace(word,
"[REDACTED_SLUR]") which replaces substrings globally and can hit unintended
contexts; instead do a single-pass token-aware replacement: split the original
text into tokens while preserving separators (use regex splitting), iterate
tokens once and if a token normalized (e.g., lowercased/cleaned) matches an
entry in detected_slurs replace that token with "[REDACTED_SLUR]" otherwise keep
it, then rejoin tokens into self.text; update the logic that currently iterates
over words and uses self.text.replace (references: detected_slurs, words,
self.text) to this tokenized single-pass approach to avoid multiple/unintended
replacements and improve efficiency.
- Line 102: The code directly accesses df['label'] and df['severity'] which can
raise KeyError for malformed CSVs; before any use of df (e.g., before the
df['label'] = df['label'].str.lower() line and before any severity mapping),
verify that the required columns set {'label', 'severity'} exists in df.columns
and if not either raise a clear ValueError or log an informative error and
exit/return; update the function that reads/validates the CSV (refer to the df
variable and the lowercase transformation on 'label') to perform this check and
handle the missing-column case gracefully.

In @backend/app/models/ban_list_safety_validator_config.py:
- Around line 7-9: The Literal annotation in BanListSafetyValidatorConfig
currently uses an f-string (Literal[f"{BAN_LIST}"]) which is invalid for typing;
replace the f-string with a compile-time literal by changing the type annotation
to use either a direct string literal (e.g., Literal["<the-banned-list-name>"])
or the constant name without formatting (Literal[BAN_LIST]) depending on how
BAN_LIST is defined, leaving the other fields (banned_words,
BaseValidatorConfig) unchanged.
♻️ Duplicate comments (1)
backend/app/core/validators/lexical_slur.py (1)

104-111: Clarify severity filtering semantics.

The severity filtering remains counterintuitive: SlurSeverity.Low returns slurs of all severities (L, M, H), which suggests "Low" means "low threshold" (catch everything including low-severity slurs). However, the enum naming implies "Low" should mean "only low-severity slurs."

This matches the concern raised in the previous review. Consider:

  1. Renaming enum values to reflect threshold semantics (e.g., Strict, Moderate, Permissive)
  2. Inverting the logic so Low returns only low-severity slurs
  3. Adding docstring clarification

Based on past review comments, this issue was previously flagged but remains unaddressed.

🧹 Nitpick comments (6)
backend/app/models/ban_list_safety_validator_config.py (1)

2-2: Use built-in list instead of typing.List.

Python 3.9+ supports using the built-in list type directly in type annotations, making typing.List deprecated.

🔎 Proposed fix
-from typing import List, Literal
+from typing import Literal

and update line 9:

-    banned_words: List[str]
+    banned_words: list[str]
backend/app/core/validators/lexical_slur.py (5)

9-9: Import Callable from collections.abc instead of typing.

For Python 3.9+, Callable should be imported from collections.abc instead of typing to follow current best practices.

🔎 Proposed fix
-from typing import Callable, Optional
+from typing import Optional
+from collections.abc import Callable

33-33: Make _SLUR_CACHE a class variable to share across instances.

The cache is currently an instance variable, meaning each LexicalSlur instance maintains its own cache. Since the slur list is loaded from the same file path and depends only on severity (not instance-specific data), the cache should be a class variable to improve memory efficiency and initialization performance when multiple instances are created.

🔎 Proposed fix
 @register_validator(name="lexical-slur", data_type="string")
 class LexicalSlur(Validator):
     """
     Validate text for the presence of lexical slurs using a predefined list.
     """
+    _SLUR_CACHE = {}
 
     def __init__(
         self, 
         severity: SlurSeverity = SlurSeverity.All,
         languages: Optional[list] = None,
         on_fail: Optional[Callable] = OnFailAction.FIX
     ):    
-        self._SLUR_CACHE = {}
         self.severity = severity

37-37: Remove unnecessary self.text initialization.

This instance variable is always reassigned in _validate() before use, making the initialization redundant. Additionally, using instance variables for temporary processing state can cause issues in concurrent scenarios or when the same validator instance is reused.

🔎 Proposed fix
         self.languages = languages or ["en", "hi"]
         self.slur_list = self.load_slur_list()
-        self.text = None
         super().__init__(on_fail=on_fail, search_words=self.slur_list)

40-64: Avoid mutating instance state during validation.

The method mutates self.text throughout validation, which makes the validator non-reentrant and potentially unsafe for concurrent use or when the same instance is reused multiple times. Validation methods should be stateless and operate only on local variables.

🔎 Proposed fix using local variables
     def _validate(self, value: str, metadata: dict = None) -> ValidationResult:
-        self.text = value
-        self.text = self.remove_emojis(self.text)
-        self.text = self.remove_nos(self.text)
-        self.text = self.clean_text(self.text)
-        words = self.text.split()
+        text = value
+        text = self.remove_emojis(text)
+        text = self.remove_nos(text)
+        text = self.clean_text(text)
+        words = text.split()
         detected_slurs = []
 
         for slur in self.slur_list:
             if slur in words:
                 if slur not in detected_slurs:
                     detected_slurs.append(slur)
 
         if len(detected_slurs) > 0:
             for word in words:
                 if word in detected_slurs:
-                    self.text = self.text.replace(word, "[REDACTED_SLUR]")
+                    text = text.replace(word, "[REDACTED_SLUR]")
 
         if len(detected_slurs) > 0:
             return FailResult(
                 error_message=f"Mentioned toxic words: {', '.join(detected_slurs)}",
-                fix_value=self.text
+                fix_value=text
             )
 
-        return PassResult(value=self.text)
+        return PassResult(value=text)

114-114: Add newline at end of file.

Python files should end with a newline character per PEP 8 conventions.

🔎 Proposed fix
         self._SLUR_CACHE[cache_key] = slurs
         return slurs
+
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5a1bf1 and e89ae6a.

📒 Files selected for processing (2)
  • backend/app/core/validators/lexical_slur.py
  • backend/app/models/ban_list_safety_validator_config.py
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/core/validators/lexical_slur.py (2)
backend/app/core/config.py (1)
  • Settings (26-98)
backend/app/core/enum.py (1)
  • SlurSeverity (3-7)
backend/app/models/ban_list_safety_validator_config.py (4)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (7-27)
  • build (24-27)
  • resolve_on_fail (12-22)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (13-19)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (10-14)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (11-16)
🪛 Ruff (0.14.10)
backend/app/core/validators/lexical_slur.py

9-9: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


114-114: No newline at end of file

Add trailing newline

(W292)

backend/app/models/ban_list_safety_validator_config.py

2-2: typing.List is deprecated, use list instead

(UP035)


15-15: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (5)
backend/app/models/ban_list_safety_validator_config.py (2)

11-15: Implementation looks correct and consistent.

The build() method correctly instantiates the BanList validator and follows the same pattern as other validator configs in the codebase. It properly uses self.resolve_on_fail() to convert the on_fail string to the appropriate enum value.

Note: Add a trailing newline at the end of the file (line 15) as flagged by static analysis.


1-1: API compatibility verified — BanList from guardrails.hub accepts both banned_words (List[str]) and on_fail (str | Callable) parameters as used in the implementation. No compatibility issues found.

backend/app/core/validators/lexical_slur.py (3)

48-56: Detection logic may miss slurs in composite words or with obfuscation.

The current implementation only detects exact word matches after splitting on whitespace. This approach will miss:

  • Slurs embedded in larger words (e.g., "badword123" if "badword" is a slur)
  • Common obfuscation techniques (e.g., "b@dword", "b-a-d-w-o-r-d")
  • Slurs without word boundaries in some languages

Consider whether the current approach aligns with the intended detection coverage, or if more sophisticated pattern matching is needed.


66-85: LGTM! Helper methods are well-implemented.

The text normalization pipeline is comprehensive and appropriate:

  • normalize_text handles encoding issues and Unicode normalization
  • remove_emojis cleanly strips emoji characters
  • clean_text removes punctuation while preserving structure
  • remove_nos removes digit sequences

This layered approach provides robust text preprocessing for slur detection.


95-100: Good error handling for CSV loading.

The try-except blocks properly handle file loading errors, addressing the previous review feedback. This ensures the application fails gracefully with clear error messages if the slur list file is missing or malformed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Fix all issues with AI Agents 🤖
In @backend/app/api/routes/guardrails.py:
- Around line 23-32: The run_output_guardrails endpoint incorrectly accesses
payload.input but the request model is GuardrailOutputRequest which provides an
output field; update the call to _validate_with_guard inside
run_output_guardrails to pass payload.output (keep payload.validators and
"safe_output" as-is) so it uses the correct field and avoids an AttributeError
at runtime.
- Around line 82-94: The code accesses a non-existent attribute result.failures
on the ValidationOutcome; replace it with the proper Guardrails fields (either
result.failed_validations or result.reask.fail_results) and map their actual
structures to strings. Update the failure list construction in the JSONResponse
to iterate over result.failed_validations (each is a ValidatorLog) and extract
the message from the ValidatorLog (e.g., use its .message or appropriate field)
or, if using reask flow, iterate result.reask.fail_results (FailResult) and pull
its error text; also update the "action" ternary to check the correct list being
empty (e.g., bool(result.reask and result.reask.fail_results) or
bool(result.failed_validations)). Ensure you only reference existing attributes
on ValidationOutcome and convert each validation entry into a plain failure
message string before passing to APIResponse.failure_response.

In @scripts/build-push.sh:
- Line 9: Replace the deprecated docker-compose CLI invocation with Docker
Compose V2 syntax by changing the command string "docker-compose -f
docker-compose.yml push" to "docker compose -f docker-compose.yml push" so the
script uses the supported Docker Compose V2 command.
♻️ Duplicate comments (1)
backend/app/core/validators/lexical_slur.py (1)

109-116: Clarify severity filtering logic.

The severity filtering remains counterintuitive: SlurSeverity.Low returns slurs of all severities (L, M, H), suggesting "Low" means "low threshold" (catch everything), but the enum naming implies "Low" should mean "only low-severity slurs."

Consider either:

  1. Renaming the enum values to reflect threshold semantics (e.g., StrictThreshold, ModerateThreshold, PermissiveThreshold)
  2. Inverting the logic so Low returns only low-severity slurs
  3. Adding clear documentation explaining that severity levels represent filter thresholds, not slur categories
🧹 Nitpick comments (5)
scripts/build-push.sh (1)

3-4: Consider additional error-handling flags.

While set -e is present, you could enhance robustness with set -u (exit on unset variables) and set -o pipefail (catch errors in pipelines). However, note that set -o pipefail is not POSIX-compliant and requires bash/zsh.

🔎 Optional enhancement for error handling
 # Exit in case of error
-set -e
+set -eu

Note: set -u will cause the script to exit if any unset variable is referenced (except in ${VAR:-default} or ${VAR?message} constructs).

backend/app/core/validators/lexical_slur.py (1)

9-9: Optional: Address static analysis hints.

Two minor improvements suggested by static analysis:

  1. Line 9: Import Callable from collections.abc instead of typing for Python 3.9+ compatibility
  2. Line 119: Add a trailing newline at the end of the file
🔎 Proposed fixes
-from typing import Callable, Optional
+from collections.abc import Callable
+from typing import Optional
         self._SLUR_CACHE[cache_key] = slurs
         return slurs
+

Also applies to: 119-119

backend/app/models/ban_list_safety_validator_config.py (1)

1-14: Implementation looks good.

The validator config correctly builds a BanList validator and follows the same pattern as other validator configs in the codebase.

Optional improvements for consistency with modern Python:

  1. Line 2: Use list[str] instead of List[str] (Python 3.9+ style)
  2. Line 14: Add a trailing newline at the end of the file
🔎 Proposed fixes
-from typing import List, Literal
+from typing import Literal
 class BanListSafetyValidatorConfig(BaseValidatorConfig):
     type: Literal["ban_list"]
-    banned_words: List[str]
+    banned_words: list[str]
             on_fail=self.resolve_on_fail(),
         )
+
backend/app/api/routes/guardrails.py (2)

42-59: Consider graceful degradation for schema errors.

The current implementation returns a 500 error immediately when any validator schema fails to load. Consider continuing to process other validators and either:

  1. Omitting the problematic validator and logging a warning
  2. Collecting all errors and returning them together

This would provide more information to API consumers and allow partial success.

🔎 Proposed alternative with graceful degradation
     validator_config_models = get_validator_config_models()
     validators = []
+    errors = []

     for model in validator_config_models:
         try:
             schema = model.model_json_schema()
             validator_type = schema["properties"]["type"]["const"]
             validators.append({
                 "type": validator_type,
                 "config": schema,
             })
         except (KeyError, TypeError) as e:
-            return JSONResponse(
-                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
-                content=APIResponse.failure_response(
-                    error={
-                        "type": "internal_error",
-                        "reason": f"Failed to retrieve schema for validator {model.__name__}: {str(e)}",
-                    }
-                ).model_dump(),
-            )
+            errors.append({
+                "validator": model.__name__,
+                "error": str(e)
+            })

-    return {"validators": validators}
+    response = {"validators": validators}
+    if errors:
+        response["errors"] = errors
+    return response

106-106: Optional: Add trailing newline.

Static analysis suggests adding a trailing newline at the end of the file for consistency.

🔎 Proposed fix
             ).model_dump(),
         )
+
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e89ae6a and f306ce9.

📒 Files selected for processing (5)
  • backend/app/api/routes/guardrails.py
  • backend/app/core/validators/lexical_slur.py
  • backend/app/models/ban_list_safety_validator_config.py
  • scripts/build-push.sh
  • scripts/build.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/build.sh
🧰 Additional context used
🧬 Code graph analysis (2)
backend/app/core/validators/lexical_slur.py (2)
backend/app/core/config.py (1)
  • Settings (26-98)
backend/app/core/enum.py (1)
  • SlurSeverity (3-7)
backend/app/models/ban_list_safety_validator_config.py (4)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (7-27)
  • build (24-27)
  • resolve_on_fail (12-22)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (11-16)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (13-19)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (10-14)
🪛 Ruff (0.14.10)
backend/app/core/validators/lexical_slur.py

9-9: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


119-119: No newline at end of file

Add trailing newline

(W292)

backend/app/api/routes/guardrails.py

106-106: No newline at end of file

Add trailing newline

(W292)

backend/app/models/ban_list_safety_validator_config.py

2-2: typing.List is deprecated, use list instead

(UP035)


14-14: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (2)
scripts/build-push.sh (2)

1-1: LGTM: POSIX-compliant shebang.

Using /usr/bin/env sh provides good portability across different Unix-like systems.


6-7: LGTM: Proper TAG variable validation.

The ${TAG?Variable not set} syntax correctly ensures the TAG environment variable is set before proceeding, providing a clear error message if it's missing.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Fix all issues with AI Agents 🤖
In @backend/app/tests/validators/test_lexical_slurs.py:
- Around line 56-86: The tests use the identity operator `is` to compare the
string `result.outcome` to literals; replace those with equality checks (`==`)
in the test functions (e.g., test_passes_when_no_slur,
test_fails_when_slur_detected, test_emoji_are_removed_before_validation) so
assertions like `assert result.outcome is "pass"` become `assert result.outcome
== "pass"` (and similarly for "fail"); keep other assertions (like checking
error_message contents) unchanged.

In @backend/app/tests/validators/test_pii_remover.py:
- Around line 42-59: The tests use identity comparison for strings; update the
three assertions in test_validate_pass_result_when_presidio_returns_string,
test_validate_pass_result_when_presidio_returns_engine_result, and
test_fail_if_anonymized_is_none to use equality (==) when checking
result.outcome from validator._validate (e.g., replace assert result.outcome is
"pass"/"fail" with assert result.outcome == "pass"/"fail") to avoid flaky
behavior.
- Around line 61-74: The tests have inconsistent mocking of
language_detector.is_hindi (returning the string "hi" instead of a boolean) and
mismatched names/assertions; update
test_english_path_called_when_language_not_hindi to mock
language_detector.predict -> "en" and language_detector.is_hindi -> False and
assert that run_english_presidio was called (or rename the test to reflect
asserting run_english_presidio not called if that was intended), and update
test_hinglish_path_called_for_hindi_text to mock language_detector.predict ->
"hi" and language_detector.is_hindi -> True and assert
run_hinglish_presidio.assert_called_once(); ensure you reference
validator._validate, run_english_presidio, run_hinglish_presidio, and
language_detector.is_hindi when making the changes.
♻️ Duplicate comments (5)
backend/app/core/validators/pii_remover.py (1)

27-49: entity_types and threshold are still not being used.

These parameters are stored on the instance but never passed to self.analyzer.analyze() in run_english_presidio(). This was flagged in a previous review and remains unaddressed.

backend/app/core/config.py (4)

33-33: SECRET_KEY regenerates on each restart, invalidating all tokens.

This was flagged in a previous review and remains unaddressed. The current implementation generates a new key at module import time, meaning all tokens (JWTs, session tokens, etc.) become invalid after each application restart.


43-44: Empty password defaults pose security risks.

This was flagged in a previous review. POSTGRES_PASSWORD and POSTGRES_DB defaulting to empty strings could allow passwordless database connections in some configurations.


92-100: Validator check for SECRET_KEY is ineffective.

Checking SECRET_KEY for "changethis" is pointless since it's generated via secrets.token_urlsafe(32) and will never equal that string. This was noted in a previous review.


81-90: Environment value mismatch: "local" is not a valid literal.

Line 87 checks if self.ENVIRONMENT == "local", but line 36 defines ENVIRONMENT with allowed values ["development", "testing", "staging", "production"]. The value "local" is not in this set, so the warning branch can never be reached.

🔎 Proposed fix

Either add "local" to the allowed literals:

-    ENVIRONMENT: Literal["development", "testing", "staging", "production"] = "testing"
+    ENVIRONMENT: Literal["local", "development", "testing", "staging", "production"] = "testing"

Or update the check to use an existing value like "development":

-            if self.ENVIRONMENT == "local":
+            if self.ENVIRONMENT == "development":
🧹 Nitpick comments (3)
backend/app/tests/conftest.py (1)

21-26: Remove unused nested variable.

The nested variable on line 21 is assigned but never used. The restart_savepoint event listener handles nested transaction management independently.

🔎 Proposed fix
-    nested = session.begin_nested()
+    session.begin_nested()
backend/app/tests/validators/test_lexical_slurs.py (1)

88-104: Severity tests bypass actual filtering logic.

These tests override load_slur_list with hardcoded return values that simulate severity filtering, but they don't exercise the actual filtering logic in LexicalSlur.load_slur_list. The real method reads CSV data and applies severity-based isin() filters.

Consider either:

  1. Testing the real load_slur_list by mocking only the file path (not the method itself), or
  2. Adding integration tests that use actual CSV files to verify end-to-end severity filtering.
backend/app/core/validators/pii_remover.py (1)

46-46: Avoid setting environment variables in constructor.

Setting os.environ["TOKENIZERS_PARALLELISM"] in __init__ has global side effects. If multiple validators are instantiated or if other code depends on this environment variable, it could cause unexpected behavior.

Consider moving this to a module-level initialization or an application startup hook.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f306ce9 and 4b88f4b.

📒 Files selected for processing (7)
  • backend/app/core/config.py
  • backend/app/core/validators/pii_remover.py
  • backend/app/tests/__init__.py
  • backend/app/tests/conftest.py
  • backend/app/tests/validators/test_gender_assumption_bias.py
  • backend/app/tests/validators/test_lexical_slurs.py
  • backend/app/tests/validators/test_pii_remover.py
🧰 Additional context used
🧬 Code graph analysis (4)
backend/app/tests/validators/test_gender_assumption_bias.py (2)
backend/app/core/enum.py (1)
  • BiasCategories (9-13)
backend/app/core/validators/gender_assumption_bias.py (1)
  • GenderAssumptionBias (17-83)
backend/app/tests/validators/test_pii_remover.py (1)
backend/app/core/validators/pii_remover.py (2)
  • PIIRemover (20-73)
  • _validate (51-64)
backend/app/tests/conftest.py (1)
backend/app/api/deps.py (1)
  • get_db (12-14)
backend/app/tests/validators/test_lexical_slurs.py (3)
backend/app/core/validators/lexical_slur.py (1)
  • LexicalSlur (22-119)
backend/app/core/enum.py (1)
  • SlurSeverity (3-7)
backend/app/core/config.py (1)
  • Settings (26-100)
🪛 Ruff (0.14.10)
backend/app/tests/validators/test_pii_remover.py

22-22: Unused function argument: mock_presidio

(ARG001)


46-46: Use == to compare constant literals

Replace is with ==

(F632)


54-54: Use == to compare constant literals

Replace is with ==

(F632)


59-59: Use == to compare constant literals

Replace is with ==

(F632)


82-82: Unused function argument: mock_presidio

(ARG001)


88-88: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/pii_remover.py

13-13: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


73-73: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/conftest.py

21-21: Local variable nested is assigned to but never used

Remove assignment to unused variable nested

(F841)


39-39: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/validators/test_lexical_slurs.py

29-29: Unused function argument: self

(ARG001)


56-56: Unused function argument: patch_slur_load

(ARG001)


59-59: Use == to compare constant literals

Replace is with ==

(F632)


62-62: Unused function argument: patch_slur_load

(ARG001)


65-65: Use == to compare constant literals

Replace is with ==

(F632)


69-69: Unused function argument: patch_slur_load

(ARG001)


72-72: Use == to compare constant literals

Replace is with ==

(F632)


76-76: Unused function argument: patch_slur_load

(ARG001)


79-79: Use == to compare constant literals

Replace is with ==

(F632)


82-82: Unused function argument: patch_slur_load

(ARG001)


85-85: Use == to compare constant literals

Replace is with ==

(F632)


88-88: Unused function argument: patch_slur_load

(ARG001)


97-97: Unused function argument: self

(ARG001)


107-107: Unused function argument: patch_slur_load

(ARG001)


115-115: Unused function argument: self

(ARG001)


125-125: Unused function argument: patch_slur_load

(ARG001)


133-133: Unused function argument: self

(ARG001)


140-140: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (6)
backend/app/tests/conftest.py (1)

35-39: LGTM!

The client fixture correctly overrides get_db to inject the test session, ensuring proper test isolation. The dependency override pattern with lambda: db is appropriate for this use case.

Note: Add a trailing newline at EOF per Python convention.

backend/app/tests/validators/test_gender_assumption_bias.py (3)

9-20: LGTM!

The fixtures are well-structured. The mock DataFrame provides a good variety of bias types for testing, and patching pandas.read_csv at the correct module path ensures the validator loads the mocked data.


53-60: Verify the category filtering test logic.

This test filters by BiasCategories.Generic, but the mock data uses "pronoun" and "generic" as types. Since "he" is typed as "pronoun" (not "generic"), filtering for Generic should exclude it. The assertion on line 60 expects "He is a police officer." — this is correct only if "He" isn't replaced because pronouns are filtered out.

Confirm this aligns with the validator's load_gender_bias_list logic, which filters by category.value (i.e., "generic").


62-70: LGTM!

Good coverage of error handling paths. The tests verify that FileNotFoundError and generic exceptions are properly propagated with appropriate exception types.

backend/app/core/validators/pii_remover.py (1)

51-64: LGTM!

The _validate method correctly uses is_hindi() to branch between English and Hinglish paths, and properly returns FailResult with fix_value when PII is detected, or PassResult when text is unchanged.

backend/app/core/config.py (1)

46-64: LGTM!

Good use of ClassVar for file paths that should not be loaded from environment. The path construction using Path and / operator is clean and readable.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In @backend/app/alembic/env.py:
- Around line 29-51: run_migrations_offline currently reads the placeholder URL
via config.get_main_option("sqlalchemy.url") which causes offline migrations to
use alembic.ini instead of the app DB; update run_migrations_offline to obtain
the real database URL the same way online mode does (call the application helper
that returns the DB URL, e.g. get_url()) and pass that URL into
context.configure (keep target_metadata, literal_binds and dialect_opts as-is);
ensure the appropriate import for the get_url helper (or the module/function
used by online migrations) is present so offline mode uses the actual
application settings.

In @backend/app/alembic/versions/001_added_request_log.py:
- Line 40: The downgrade currently only drops the request_log table but leaves
the custom Postgres enum type; update the downgrade function in
001_added_request_log.py to explicitly drop the enum type (requeststatus) after
removing the table — e.g., invoke Alembic/SA to execute a DROP TYPE IF EXISTS
requeststatus (or use postgresql.ENUM(...).drop(op.get_bind(), checkfirst=True))
so the requeststatus enum is removed during downgrade.
- Line 28: The migration defines the DB enum with uppercase values but your
RequestStatus model uses lowercase values, causing inserts with
RequestStatus.SUCCESS ("success") to be rejected; update the enum definition
used in the migration (the sa.Column('status', sa.Enum(...,
name='requeststatus'))) to use the lowercase strings "success", "error",
"warning" to match the RequestStatus model, or alternatively make the
RequestStatus values uppercase to match the migration; if the uppercase enum has
already been applied to a DB, create a corrective migration that alters the enum
values (or recreates the enum type and migrates existing rows) so DB enum and
the RequestStatus enum stay in sync.

In @backend/app/alembic/versions/002_added_validator_log.py:
- Line 29: The migration defines sa.Column('error',
sqlmodel.sql.sqltypes.AutoString(), nullable=False) which should allow NULL for
successful validations; change nullable=False to nullable=True for the 'error'
column in the migration (or add an alter_column call in the upgrade() to set
nullable=True) so the ValidatorLog.error field can be NULL, then regenerate or
run the migration so the DB schema is updated accordingly.

In @backend/app/models/__init__.py:
- Line 5: The file importing ValidatorLog (the module's top-level line "from
.validator import ValidatorLog") is missing a trailing newline; add a single
newline character at the end of the file so the file ends with a newline (ensure
the final line break is present after the import statement).

In @backend/app/models/request.py:
- Line 21: The updated_at Field currently uses default_factory=now which only
sets the timestamp on insert; change it to an SQLAlchemy-backed column that
auto-updates on updates by replacing updated_at: datetime =
Field(default_factory=now, nullable=False) with a Field using
sa_column=Column(DateTime, default=func.now(), onupdate=func.now(),
nullable=False) (import Column, DateTime and func from sqlalchemy), so the
model's updated_at will be set on insert and automatically updated on modify;
keep the attribute name updated_at and the now helper if still used elsewhere.

In @backend/app/models/validator.py:
- Around line 12-14: The model's string fields input, output, and error are
declared as plain str but not marked nullable, causing a schema mismatch; update
each field to be Optional[str] and add a Field(...) that makes the DB column
nullable (e.g., default=None and nullable=True) so Python and SQL types align;
locate the model that defines input, output, and error and replace their
declarations accordingly (or, if you intend them to be required, change the
typing to non-Optional and add Field(..., nullable=False) to enforce NOT NULL).
🧹 Nitpick comments (5)
backend/app/models/validator.py (1)

3-3: Remove unused import.

The Enum import is not used in this file.

🔎 Proposed fix
 from sqlmodel import SQLModel, Field
 from uuid import UUID, uuid4
-from enum import Enum
 
 from app.core.util import now
 from datetime import datetime
backend/app/alembic/versions/001_added_request_log.py (1)

8-8: Optional: Modernize type annotations per static analysis.

Ruff suggests importing Sequence from collections.abc and using X | Y syntax for union types (PEP 604). These are stylistic improvements for Python 3.10+.

🔎 Proposed changes
-from typing import Sequence, Union
+from collections.abc import Sequence
+from typing import Union

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

Also applies to: 17-19

backend/app/models/request.py (1)

15-16: Consider adding indexes for query performance.

The request_id and response_id fields will likely be used in lookups and joins (especially with validator_log.request_id foreign key). Adding indexes would improve query performance.

🔎 Proposed enhancement
 class RequestLog(SQLModel, table=True):
     id: UUID = Field(default_factory=uuid4, primary_key=True)
-    request_id: UUID 
-    response_id: UUID
+    request_id: UUID = Field(index=True)
+    response_id: UUID = Field(index=True)
     status: RequestStatus
backend/app/alembic/script.py.mako (1)

8-8: Optional: Modernize template type annotations.

Similar to the migration files, the template could use modern type syntax. Since this template generates all future migrations, updating it ensures consistency across new migrations.

🔎 Proposed changes
-from typing import Sequence, Union
+from collections.abc import Sequence
+from typing import Union

 # revision identifiers, used by Alembic.
 revision: str = ${repr(up_revision)}
-down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
-branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
-depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+down_revision: str | Sequence[str] | None = ${repr(down_revision)}
+branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
+depends_on: str | Sequence[str] | None = ${repr(depends_on)}

Also applies to: 17-19

backend/app/alembic/versions/002_added_validator_log.py (1)

8-8: Optional: Modernize type annotations per static analysis.

Same suggestion as migration 001: use collections.abc.Sequence and PEP 604 union syntax for consistency.

🔎 Proposed changes
-from typing import Sequence, Union
+from collections.abc import Sequence
+from typing import Union

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

Also applies to: 17-19

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b88f4b and 56c0d32.

📒 Files selected for processing (10)
  • backend/alembic.ini
  • backend/app/alembic/README
  • backend/app/alembic/env.py
  • backend/app/alembic/script.py.mako
  • backend/app/alembic/versions/001_added_request_log.py
  • backend/app/alembic/versions/002_added_validator_log.py
  • backend/app/core/util.py
  • backend/app/models/__init__.py
  • backend/app/models/request.py
  • backend/app/models/validator.py
✅ Files skipped from review due to trivial changes (1)
  • backend/app/alembic/README
🧰 Additional context used
🧬 Code graph analysis (6)
backend/app/models/__init__.py (2)
backend/app/models/request.py (1)
  • RequestLog (13-21)
backend/app/models/validator.py (1)
  • ValidatorLog (9-16)
backend/app/models/validator.py (1)
backend/app/core/util.py (1)
  • now (3-4)
backend/app/alembic/versions/002_added_validator_log.py (1)
backend/app/alembic/versions/001_added_request_log.py (2)
  • upgrade (22-34)
  • downgrade (37-40)
backend/app/alembic/env.py (1)
backend/app/core/config.py (1)
  • SQLALCHEMY_DATABASE_URI (68-76)
backend/app/alembic/versions/001_added_request_log.py (1)
backend/app/alembic/versions/002_added_validator_log.py (2)
  • upgrade (22-34)
  • downgrade (37-39)
backend/app/models/request.py (1)
backend/app/core/util.py (1)
  • now (3-4)
🪛 Ruff (0.14.10)
backend/app/models/__init__.py

5-5: No newline at end of file

Add trailing newline

(W292)

backend/app/alembic/versions/002_added_validator_log.py

8-8: Import from collections.abc instead: Sequence

Import from collections.abc

(UP035)


17-17: Use X | Y for type annotations

Convert to X | Y

(UP007)


18-18: Use X | Y for type annotations

Convert to X | Y

(UP007)


19-19: Use X | Y for type annotations

Convert to X | Y

(UP007)

backend/app/alembic/versions/001_added_request_log.py

8-8: Import from collections.abc instead: Sequence

Import from collections.abc

(UP035)


17-17: Use X | Y for type annotations

Convert to X | Y

(UP007)


18-18: Use X | Y for type annotations

Convert to X | Y

(UP007)


19-19: Use X | Y for type annotations

Convert to X | Y

(UP007)

🔇 Additional comments (4)
backend/alembic.ini (1)

1-147: LGTM! Standard Alembic configuration.

The Alembic configuration follows standard patterns with appropriate logging levels and path settings. The placeholder database URL at line 87 is expected and will be overridden at runtime by the environment script.

backend/app/alembic/env.py (2)

53-74: LGTM! Online migrations correctly configured.

The online migration mode properly uses application settings via get_url() and includes compare_type=True for better type change detection during migrations.


1-19: LGTM! Metadata and imports properly configured.

The script correctly imports SQLModel metadata and application settings, establishing the foundation for migrations to track model changes.

backend/app/core/util.py (1)

3-4: Reconsider returning naive datetimes, or document the architectural requirement.

The function creates a timezone-aware UTC datetime then immediately strips the timezone information. While this pattern can introduce timezone-related complexity, the database schema uses sa.DateTime() columns (TIMESTAMP WITHOUT TIME ZONE), indicating this is an intentional architectural choice rather than an oversight.

If naive datetimes are required by your system requirements, document this decision. If moving to timezone-aware datetimes is desired, this requires coordinated changes: updating migrations to use sa.DateTime(timezone=True), modifying the now() function to return datetime.now(timezone.utc), and regenerating migrations to alter existing columns.

Comment on lines +29 to +51
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix offline mode to use application settings.

The offline migration mode uses the placeholder URL from alembic.ini instead of the actual database URL from application settings. This inconsistency with online mode (which correctly uses get_url()) will cause offline migrations to fail or generate incorrect SQL.

🔎 Proposed fix
 def run_migrations_offline() -> None:
     """Run migrations in 'offline' mode.
 
     This configures the context with just a URL
     and not an Engine, though an Engine is acceptable
     here as well.  By skipping the Engine creation
     we don't even need a DBAPI to be available.
 
     Calls to context.execute() here emit the given string to the
     script output.
 
     """
-    url = config.get_main_option("sqlalchemy.url")
+    url = get_url()
     context.configure(
         url=url,
         target_metadata=target_metadata,
         literal_binds=True,
         dialect_opts={"paramstyle": "named"},
     )
 
     with context.begin_transaction():
         context.run_migrations()
📝 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
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = get_url()
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
🤖 Prompt for AI Agents
In @backend/app/alembic/env.py around lines 29 - 51, run_migrations_offline
currently reads the placeholder URL via config.get_main_option("sqlalchemy.url")
which causes offline migrations to use alembic.ini instead of the app DB; update
run_migrations_offline to obtain the real database URL the same way online mode
does (call the application helper that returns the DB URL, e.g. get_url()) and
pass that URL into context.configure (keep target_metadata, literal_binds and
dialect_opts as-is); ensure the appropriate import for the get_url helper (or
the module/function used by online migrations) is present so offline mode uses
the actual application settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In @backend/app/api/routes/guardrails.py:
- Around line 94-97: The code passes a stringified UUID (response_id =
str(uuid.uuid4())) into RequestLogUpdate.response_id which expects a UUID;
change the creation/assignment so response_id is a UUID object (use uuid.uuid4()
without str() or cast the existing string back to a UUID) before constructing
RequestLogUpdate in the blocks where RequestLogUpdate(...) is used (references:
RequestLogUpdate, variable response_id in guardrails.py lines around the shown
diff and the similar usages at the other instances called out: the blocks at
~111-114 and ~127-130); keep string conversion only at serialization boundaries.
- Around line 78-84: The return type on _validate_with_guard is incorrect:
remove JSONResponse and annotate it to return only APIResponse (e.g.,
APIResponse[T] or APIResponse[None] as appropriate) because every code path
returns APIResponse via APIResponse.success_response() or
APIResponse.failure_response(); update the function signature (async def
_validate_with_guard(... ) -> APIResponse) accordingly and ensure any related
imports/reference usages reflect the changed type.

In @backend/app/core/exception_handlers.py:
- Around line 26-33: The generic_error_handler currently returns str(exc) to
clients, which can leak sensitive internals; instead log the full exception
server-side (e.g., use logger.exception or logging.exception inside
generic_error_handler to capture stack trace for the exception `exc`) and change
the response to always use a generic message (e.g., "An unexpected error
occurred.") via APIResponse.failure_response; optionally gate detailed messages
behind a debug flag (e.g., settings.DEBUG) so only in development you include
exc details, but never return raw exception strings in production.

In @backend/app/core/validators/gender_assumption_bias.py:
- Around line 32-55: The _validate method mutates the instance attribute
self.text which is not thread-safe; change _validate to use a local variable
(e.g., text = value) and update that local variable when performing re.sub and
searches instead of assigning to self.text, and return PassResult(value=text) or
FailResult(fix_value=text) as appropriate; also remove the now-unused self.text
= None from __init__ and ensure references to gender_bias_list,
detected_biased_words, and bias_check remain identical so the behavior and
return types (PassResult/FailResult) are preserved.
- Around line 23-30: The __init__ uses a mutable default for categories
(categories: Optional[List[BiasCategories]] = [BiasCategories.All]) which can
lead to shared-state bugs; change the signature to default categories to None
and inside __init__ set categories = [BiasCategories.All] if categories is None
before calling self.load_gender_bias_list(categories), keeping the type as
Optional[List[BiasCategories]] and leaving load_gender_bias_list and other logic
unchanged.

In @backend/app/core/validators/lexical_slur.py:
- Line 33: The instance-level cache self._SLUR_CACHE is recreated per validator
instance so never benefits from shared caching; make it a class-level attribute
instead by removing the self._SLUR_CACHE = {} assignment from the constructor
and declaring a shared _SLUR_CACHE = {} on the validator class (referencing the
same symbol _SLUR_CACHE), so all instances read/write the same cache; update any
refs that set or read self._SLUR_CACHE to use self.__class__._SLUR_CACHE or just
_SLUR_CACHE to ensure the shared store is used.

In @backend/app/models/__init__.py:
- Around line 1-4: The import for ValidatorLog in __init__.py uses an incorrect
module path; replace the incorrect import "from backend.app.models.validator_log
import ValidatorLog" with the correct package-relative import "from
app.models.validator_log import ValidatorLog" so both RequestLog and
ValidatorLog are imported from the same root package and avoid ImportError at
runtime; update the import statement that references ValidatorLog accordingly.

In @backend/app/models/request.py:
- Around line 15-24: The nullable fields on RequestLog have non-optional type
hints; change response_id and response_text to use Optional types (e.g.,
Optional[UUID] and Optional[str]) and set their default to None when using
Field(nullable=True); also add an import for Optional from typing if missing so
SQLModel/Pydantic will accept None values for those fields.

In @backend/app/models/validator_log.py:
- Around line 8-9: The ValidatorLog model has a typo: replace the misspelled
class attribute __table__name with the correct SQLAlchemy/SQLModel attribute
__tablename__ and set its value to "validator_log" so the model uses the
expected table name (update the ValidatorLog class definition to use
__tablename__ = "validator_log").

In @backend/app/tests/validators/test_pii_remover.py:
- Around line 56-69: The tests assume a validator.language_detector with predict
and is_hindi methods and have inverted/incorrect assertions; fix by attaching a
mock language_detector to the PIIRemover test fixture (e.g., set
validator.language_detector = Mock()) and stub its predict to return "en" or
"hi" and is_hindi to return a boolean (False for English path, True for Hindi
path); then call validator._validate(...) and assert the correct presidio runner
is invoked: when predict returns "en" and is_hindi is False assert
run_english_presidio.assert_called_once() (not assert_not_called), and when
predict returns "hi" and is_hindi is True assert
run_hinglish_presidio.assert_called_once(). Ensure you patch/spy the instance
methods run_english_presidio and run_hinglish_presidio on validator (not on a
missing language_detector attribute) so the mocks exist.
- Around line 27-31: The test is calling PIIRemover with a nonexistent language
parameter, causing a TypeError; update the test to match the actual
PIIRemover.__init__ signature by removing the language="en" argument (or
alternatively modify PIIRemover.__init__ to accept a language parameter if
intended), ensuring the call uses only entity_types, threshold, and on_fail as
in the implementation.
🟡 Minor comments (12)
backend/app/alembic/versions/001_added_request_log.py-36-39 (1)

36-39: Complete the enum type cleanup in downgrade.

The TODO is valid—on PostgreSQL, dropping the table leaves the requeststatus enum type orphaned. This can cause conflicts if the migration is re-applied.

Proposed fix
 def downgrade() -> None:
     op.drop_table('request_log')
-    # todo : drop requeststatus enum type
+    # Drop the enum type (PostgreSQL-specific)
+    op.execute("DROP TYPE IF EXISTS requeststatus")
backend/app/models/request.py-26-28 (1)

26-28: Make update fields optional for partial updates.

RequestLogUpdate is used for partial updates (see model_dump(exclude_unset=True) in RequestLogCrud), but both fields are required. This forces callers to always provide both values even when updating only one. Also missing trailing newline.

Proposed fix
+from typing import Optional

 class RequestLogUpdate(SQLModel):
-    response_text: str
-    response_id: UUID
+    response_text: Optional[str] = None
+    response_id: Optional[UUID] = None
+
backend/app/tests/validators/test_pii_remover.py-41-41 (1)

41-41: Use == instead of is for string comparisons.

Using is to compare string literals is incorrect because is checks object identity, not value equality. While it may work for interned strings, it's unreliable and a bug waiting to happen.

🐛 Proposed fix
-        assert result.outcome is "pass"
+        assert result.outcome == "pass"
-        assert result.outcome is "fail"
+        assert result.outcome == "fail"
-        assert result.outcome is "fail"
+        assert result.outcome == "fail"

Also applies to: 49-49, 54-54

backend/app/alembic/env.py-26-50 (1)

26-50: Inconsistent URL source between offline and online modes.

get_url() returns settings.SQLALCHEMY_DATABASE_URI, and run_migrations_online() correctly uses it. However, run_migrations_offline() reads from config.get_main_option("sqlalchemy.url") instead, which could cause offline migrations to use a different (possibly stale or incorrect) URL from alembic.ini.

Consider using get_url() consistently for both modes to ensure migrations always use the application's configured database URL.

🐛 Proposed fix for consistency
 def run_migrations_offline() -> None:
     """Run migrations in 'offline' mode.

     This configures the context with just a URL
     and not an Engine, though an Engine is acceptable
     here as well.  By skipping the Engine creation
     we don't even need a DBAPI to be available.

     Calls to context.execute() here emit the given string to the
     script output.

     """
-    url = config.get_main_option("sqlalchemy.url")
+    url = get_url()
     context.configure(
         url=url,
         target_metadata=target_metadata,
         literal_binds=True,
         dialect_opts={"paramstyle": "named"},
     )

     with context.begin_transaction():
         context.run_migrations()
backend/app/tests/validators/test_pii_remover.py-77-83 (1)

77-83: Same constructor issue and missing trailing newline.

This test also passes the non-existent language="en" parameter. Additionally, add a trailing newline at the end of the file.

🐛 Proposed fix
 def test_custom_entity_types_override(mock_presidio):
     v = PIIRemover(
         entity_types=["EMAIL_ADDRESS"],
         threshold=0.5,
-        language="en",
+        on_fail="fix",
     )
     assert v.entity_types == ["EMAIL_ADDRESS"]
+
backend/app/models/validator_log.py-12-14 (1)

12-14: Consider making output and error fields optional.

The input, output, and error fields are all required (non-nullable). If error is meant to store error messages only when validation fails, it should be Optional[str]. Similarly, output may not always be populated.

🔧 Suggested fix
     request_id: UUID = Field(foreign_key="request_log.id", nullable=False)
     input: str
-    output: str
-    error: str
+    output: Optional[str] = None
+    error: Optional[str] = None

Don't forget to add Optional to the imports:

-from datetime import datetime
+from datetime import datetime
+from typing import Optional
backend/app/core/validators/pii_remover.py-47-57 (1)

47-57: Unused threshold parameter and redundant assignment.

  1. The threshold parameter is stored but never used in _validate(). Presidio's analyzer.analyze() can filter by score threshold, or results can be filtered post-analysis.
  2. self.on_fail = on_fail on line 57 is redundant since super().__init__(on_fail=on_fail) already stores it.
🔧 Proposed fix
 def __init__(
     self,
     entity_types=None,
     threshold=0.5,
     on_fail: Optional[Callable] = OnFailAction.FIX
 ):
     super().__init__(on_fail=on_fail)

     self.entity_types = entity_types or ALL_ENTITY_TYPES
     self.threshold = threshold
-    self.on_fail = on_fail
     os.environ["TOKENIZERS_PARALLELISM"] = "false"

Then in _validate, apply threshold filtering:

results = self.analyzer.analyze(text=text, entities=self.entity_types, language="en")
results = [r for r in results if r.score >= self.threshold]
backend/app/api/routes/guardrails.py-17-17 (1)

17-17: Inconsistent URL trailing slashes.

/input/ has a trailing slash but /output does not. This inconsistency can cause confusion and potential 307 redirects for clients.

🧹 Proposed fix - make both consistent
-@router.post("/output")
+@router.post("/output/")

Or remove the trailing slash from /input/.

Also applies to: 33-33

backend/app/core/validators/lexical_slur.py-35-35 (1)

35-35: Unused languages parameter.

The languages parameter is stored in self.languages but never referenced elsewhere in the code. Either implement language-based filtering or remove the parameter.

backend/app/api/routes/guardrails.py-1-1 (1)

1-1: Unused import error from re module.

This import is not used anywhere in the file and appears to be a mistake.

🧹 Proposed fix
-from re import error
 import uuid
 from uuid import UUID
backend/app/core/validators/gender_assumption_bias.py-72-83 (1)

72-83: Duplicate entries when BiasCategories.All is combined with specific categories.

If categories contains both BiasCategories.All and another category like BiasCategories.Healthcare, the healthcare entries will be added twice to gender_bias_list.

🐛 Proposed fix - short-circuit on All
+        if BiasCategories.All in categories:
+            rows = df.to_dict(orient="records")
+            for row in rows:
+                gender_bias_list.append({
+                    "word": row["word"],
+                    neutral_term_col: row[neutral_term_col]
+                })
+            return gender_bias_list
+
         for category in categories:
-            if category == BiasCategories.All:
-                temp = df
-            else:
-                temp = df[df['type'] == category.value]
+            temp = df[df['type'] == category.value]
 
             rows = temp.to_dict(orient="records")
             for row in rows:
                 gender_bias_list.append({
                     "word": row["word"],
                     neutral_term_col: row[neutral_term_col]
                 })
         return gender_bias_list
backend/app/alembic/versions/002_added_validator_log.py-26-28 (1)

26-28: Model definition is incomplete and misaligned with migration constraints.

The ValidatorLog model omits explicit Field() specifications for output and error, while the migration enforces nullable=False on both. This inconsistency should be resolved.

Looking at the RequestLog model pattern in the codebase, text fields that may be empty use Field(nullable=True) (e.g., response_text). The ValidatorLog should follow this pattern:

output: str = Field(nullable=True)
error: str = Field(nullable=True)

This aligns with the domain logic: successful validations may have no error, and some failure scenarios may not produce output.

🐛 Proposed fix

Update backend/app/models/validator_log.py:

 class ValidatorLog(SQLModel, table=True):
     __table__name = "validator_log"
     id: UUID = Field(default_factory=uuid4, primary_key=True)
     request_id: UUID = Field(foreign_key="request_log.id", nullable=False)
-    input: str
-    output: str
-    error: str
+    input: str = Field(nullable=False)
+    output: str = Field(nullable=True)
+    error: str = Field(nullable=True)
     inserted_at: datetime = Field(default_factory=now, nullable=False)
     updated_at: datetime = Field(default_factory=now, nullable=False)

Also update the migration to match:

     sa.Column('input', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
-    sa.Column('output', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
-    sa.Column('error', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+    sa.Column('output', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
+    sa.Column('error', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
🧹 Nitpick comments (25)
backend/app/core/middleware.py (2)

18-22: client_ip is computed but never used in the log message.

The variable is assigned but not included in the log output. Either remove it or include it in the log message for better request tracing.

♻️ Suggested fix to include client_ip in log
     logger.info(
-        f"{request.method} {request.url.path} - {response.status_code} [{process_time:.2f}ms]"
+        f"{request.method} {request.url.path} - {response.status_code} [{process_time:.2f}ms] client={client_ip}"
     )

13-15: Remove unused exception variable.

The logger.exception() call automatically includes exception info, so the as e binding is unnecessary.

♻️ Suggested fix
-    except Exception as e:
+    except Exception:
         logger.exception("Unhandled exception during request")
         raise
backend/app/models/Sample_examples.md (1)

1-21: Consider relocating sample data to a more appropriate directory.

This file contains useful test payloads, but backend/app/models/ is typically for data model definitions. Consider moving to backend/app/tests/fixtures/ or docs/examples/ for better discoverability and separation of concerns.

backend/app/models/lexical_slur_safety_validator_config.py (2)

1-1: Use built-in list instead of deprecated typing.List.

For Python 3.9+, prefer the built-in generic types.

♻️ Suggested fix
-from typing import List, Literal
+from typing import Literal

And on line 9:

-    languages: List[str] = ["en", "hi"] # list of languages to check slurs in
+    languages: list[str] = ["en", "hi"]  # list of languages to check slurs in

12-17: Consider adding return type annotation for better type safety.

Adding an explicit return type improves IDE support and documentation.

♻️ Suggested fix
-    def build(self):
+    def build(self) -> LexicalSlur:
         return LexicalSlur(
             languages=self.languages,
             severity=SlurSeverity(self.severity),
             on_fail=self.resolve_on_fail(),
         )
backend/app/utils.py (1)

26-40: Improve type annotation for the error parameter.

The current str | list annotation is imprecise. Since the list handling expects dict items with loc and msg keys (Pydantic ValidationError format), consider a more explicit type.

♻️ Suggested improvement
+from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
+
+# Type alias for Pydantic validation error format
+ValidationErrorItem = Dict[str, Any]
+
     @classmethod
     def failure_response(
         cls,
-        error: str | list,
+        error: Union[str, List[ValidationErrorItem]],
         data: Optional[T] = None,
-        metadata: Optional[Dict[str, Any]] = None,
+        metadata: Optional[dict[str, Any]] = None,
     ) -> "APIResponse[None]":

Also, add a trailing newline at end of file per Ruff W292.

backend/app/tests/conftest.py (1)

12-29: Consider consolidating duplicate fixtures.

The client and integration_client fixtures are currently identical. If they're intended to behave differently (e.g., integration tests needing real validators), consider adding the distinguishing logic now, or consolidate them into a single fixture to reduce duplication.

♻️ Suggested consolidation
 @pytest.fixture(scope="function")
 def client():
     app.dependency_overrides[verify_bearer_token] = lambda: True

     with TestClient(app) as c:
         yield c

     app.dependency_overrides.clear()
-
-
-@pytest.fixture(scope="function")
-def integration_client():
-    app.dependency_overrides[verify_bearer_token] = lambda: True
-
-    with TestClient(app) as c:
-        yield c
-
-    app.dependency_overrides.clear()
+
+# Alias for semantic distinction in integration tests
+integration_client = client

Note: Add a trailing newline at end of file per linter (W292).

backend/app/core/guardrail_controller.py (2)

7-9: Add type hints for better maintainability.

The function lacks type annotations, which would improve IDE support and catch type errors early.

♻️ Suggested type hints
-def build_guard(validator_items):
+def build_guard(validator_items: list[ValidatorConfigItem]) -> Guard:
     validators = [v_item.build() for v_item in validator_items]
     return Guard().use_many(*validators)

11-14: Fragile type introspection logic.

This function assumes ValidatorConfigItem is structured as Annotated[Union[...], ...] and uses index [0] to access the union. If the type definition changes, this will silently break or return incorrect results. Consider adding a defensive check or documenting the expected structure.

♻️ Suggested defensive approach
 def get_validator_config_models():
+    """
+    Extracts the concrete validator config classes from ValidatorConfigItem.
+    Assumes ValidatorConfigItem is Annotated[Union[ConfigA, ConfigB, ...], Field(...)].
+    """
     annotated_args = get_args(ValidatorConfigItem)
+    if not annotated_args:
+        raise RuntimeError("ValidatorConfigItem has no type arguments")
     union_type = annotated_args[0]
-    return get_args(union_type)
+    inner_types = get_args(union_type)
+    if not inner_types:
+        raise RuntimeError("ValidatorConfigItem union has no member types")
+    return inner_types

Note: Add a trailing newline at end of file per linter (W292).

backend/app/models/pii_remover_safety_validator_config.py (2)

1-5: Use built-in list instead of typing.List.

typing.List is deprecated since Python 3.9. Use the built-in list type hint.

♻️ Suggested fix
 from __future__ import annotations
-from typing import List, Literal, Optional
+from typing import Literal, Optional

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

Then update line 9:

-    entity_types: Optional[List[str]] = None # list of PII entity types to remove
+    entity_types: Optional[list[str]] = None  # list of PII entity types to remove

7-17: Consider adding validation bounds for threshold.

The threshold field accepts any float, but it represents a confidence threshold that should logically be between 0.0 and 1.0. Adding validation would catch configuration errors early.

♻️ Suggested validation
+from pydantic import Field
+
 class PIIRemoverSafetyValidatorConfig(BaseValidatorConfig):
     type: Literal["pii_remover"]
     entity_types: Optional[list[str]] = None  # list of PII entity types to remove
-    threshold: float = 0.5  # confidence threshold for PII detection
+    threshold: float = Field(default=0.5, ge=0.0, le=1.0)  # confidence threshold for PII detection
backend/app/tests/test_guardrails_api.py (2)

1-9: Unused import.

uuid4 is imported but never used—request_id is a hardcoded string literal. Consider removing the unused import.

♻️ Proposed fix
-from uuid import uuid4
-
 import pytest
 from unittest.mock import patch

 from app.tests.guardrails_mocks import MockResult, MockFailure

 build_guard_path = "app.api.routes.guardrails.build_guard"
 request_id = "123e4567-e89b-12d3-a456-426614174000"

93-112: Internal error handling test is well-constructed; add trailing newline.

The test correctly verifies 500 response and config_error handling when guard construction fails. Add a trailing newline at end of file.

♻️ Add trailing newline
     assert body["success"] is False
     assert body["error"]["type"] == "config_error"
     assert "Invalid validator config" in body["error"]["reason"]
+
backend/app/alembic/env.py (1)

1-1: Unused import.

The os module is imported but never used in this file.

♻️ Remove unused import
-import os
 from logging.config import fileConfig
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)

7-15: Mutable default argument and deprecated List type.

Two issues:

  1. Using a mutable default [BiasCategories.All] is a Python anti-pattern. While Pydantic/SQLModel handles this correctly by creating new instances, it's still a code smell and can confuse readers. Consider using None with a default factory.

  2. typing.List is deprecated in Python 3.9+; use the built-in list instead.

Also, add a trailing newline at end of file.

♻️ Proposed fix
-from typing import List, Literal, Optional
+from typing import 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

 class GenderAssumptionBiasSafetyValidatorConfig(BaseValidatorConfig):
     type: Literal["gender_assumption_bias"]
-    categories: Optional[List[BiasCategories]] = [BiasCategories.All] # preferred category (based on sector)
+    categories: Optional[list[BiasCategories]] = None  # preferred category (based on sector)

     def build(self):
         return GenderAssumptionBias(
-            categories=self.categories,
+            categories=self.categories if self.categories is not None else [BiasCategories.All],
             on_fail=self.resolve_on_fail(),
         )
+
backend/app/tests/test_validate_with_guard.py (2)

13-14: Remove unused client fixture parameter.

The client fixture is passed to test_validate_with_guard_success but never used. Since the test directly calls _validate_with_guard, no HTTP client is needed.

♻️ Proposed fix
 @pytest.mark.asyncio
-async def test_validate_with_guard_success(client):
+async def test_validate_with_guard_success():
     class MockGuard:

56-62: Consider parsing JSON for more robust assertions.

The assertions check for substrings in response.body.decode(), which is fragile. Parsing the JSON would make assertions more precise and maintainable.

Also, add a trailing newline at end of file.

♻️ Example improvement for one test
+import json
+
 # In the test:
     body = response.body.decode()
-    assert "validation_error" in body
-    assert "reask" in body
-    assert "PII detected" in body
+    data = json.loads(body)
+    assert data["error"]["type"] == "validation_error"
+    assert data["error"]["action"] == "reask"
+    assert "PII detected" in data["error"]["failures"]

And add trailing newline:

     assert "config_error" in body
     assert "Invalid config" in body
+

Also applies to: 82-86, 102-106

backend/app/models/guardrail_config.py (1)

1-21: Discriminated union type is well-structured.

The use of Annotated with Field(discriminator="type") correctly enables automatic routing to the appropriate config model at runtime. The TODO comment about auto-discovery is a reasonable future improvement.

Minor style note: Static analysis flags the use of deprecated typing.List and Union syntax. Modern Python (3.10+) prefers list and | syntax, but this is optional if supporting older versions.

backend/app/core/validators/pii_remover.py (1)

58-58: Move environment variable setting out of constructor.

Setting os.environ["TOKENIZERS_PARALLELISM"] in the constructor is a global side effect executed on every instance creation. Move this to module level or application startup.

♻️ Proposed fix
+# Disable HuggingFace tokenizers parallelism warning
+os.environ["TOKENIZERS_PARALLELISM"] = "false"
+
 @register_validator(name="pii-remover", data_type="string")
 class PIIRemover(Validator):
     ...
     def __init__(self, ...):
         super().__init__(on_fail=on_fail)
         self.entity_types = entity_types or ALL_ENTITY_TYPES
         self.threshold = threshold
-        os.environ["TOKENIZERS_PARALLELISM"] = "false"
backend/app/crud/request_log.py (3)

1-1: Unused import uuid4.

The uuid4 import is not used in this file. The RequestLog model already has default_factory=uuid4 defined.

🧹 Proposed fix
-from uuid import UUID, uuid4
+from uuid import UUID

22-37: Consider consolidating update_success and update_error to reduce duplication.

Both methods are nearly identical, differing only in the status value. This can be refactored into a single private method with a status parameter.

♻️ Proposed refactor
-    def update_success(self, request_log_id: UUID, request_log_update: RequestLogUpdate):
+    def update_success(self, request_log_id: UUID, request_log_update: RequestLogUpdate) -> RequestLog:
+        return self._update(request_log_id, request_log_update, RequestStatus.SUCCESS)
+
+    def update_error(self, request_log_id: UUID, request_log_update: RequestLogUpdate) -> RequestLog:
+        return self._update(request_log_id, request_log_update, RequestStatus.ERROR)
+
+    def _update(self, request_log_id: UUID, request_log_update: RequestLogUpdate, status: RequestStatus) -> RequestLog:
         request_log = self.session.get(RequestLog, request_log_id)
         if not request_log:
             raise ValueError(f"Request Log not found for id {request_log_id}")
-        
+
         update_data = request_log_update.model_dump(exclude_unset=True)
         for field, value in update_data.items():
             setattr(request_log, field, value)
 
         request_log.updated_at = now()
-        request_log.status = RequestStatus.SUCCESS
+        request_log.status = status
         self.session.add(request_log)
         self.session.commit()
         self.session.refresh(request_log)
-
         return request_log
-
-    def update_error(self, request_log_id: UUID, request_log_update: RequestLogUpdate):
-        request_log = self.session.get(RequestLog, request_log_id)
-        if not request_log:
-            raise ValueError(f"Request Log not found for id {request_log_id}")
-        
-        update_data = request_log_update.model_dump(exclude_unset=True)
-        for field, value in update_data.items():
-            setattr(request_log, field, value)
-
-        request_log.updated_at = now()
-        request_log.status = RequestStatus.ERROR
-        self.session.add(request_log)
-        self.session.commit()
-        self.session.refresh(request_log)
-
-        return request_log

Also applies to: 39-54


56-57: Add trailing newline.

Per static analysis hint.

🧹 Proposed fix
     def get(self, request_log_id: UUID) -> RequestLog | None:
         return self.session.get(RequestLog, request_log_id)
+
backend/app/core/validators/gender_assumption_bias.py (1)

57-67: Consider caching the loaded DataFrame like LexicalSlur does.

Unlike lexical_slur.py which caches loaded slur lists by severity, this validator loads and parses the CSV file on every instantiation. If multiple GenderAssumptionBias validators are created, this causes redundant I/O.

backend/app/core/validators/lexical_slur.py (1)

48-51: Duplicate detection logic can use a set for efficiency.

Using a list and checking if slur not in detected_slurs is O(n) per check. Use a set for O(1) lookups.

♻️ Proposed fix
-        detected_slurs = []
+        detected_slurs = set()
 
         for slur in self.slur_list:
             if slur in words:
-                if slur not in detected_slurs:
-                    detected_slurs.append(slur)
+                detected_slurs.add(slur)
 
         if len(detected_slurs) > 0:
             for word in words:
                 if word in detected_slurs:
                     text = re.sub(rf'\b{re.escape(word)}\b', "[REDACTED_SLUR]", text, flags=re.IGNORECASE)
 
         if len(detected_slurs) > 0:
             return FailResult(
-                error_message=f"Mentioned toxic words: {', '.join(detected_slurs)}",
+                error_message=f"Mentioned toxic words: {', '.join(sorted(detected_slurs))}",
                 fix_value=text
             )
backend/app/api/routes/guardrails.py (1)

124-138: Broad exception handling masks specific errors.

Catching all Exception types and converting to a generic error response makes debugging difficult. Consider logging the exception with traceback for observability, or catching more specific exceptions.

This aligns with the PR description noting "Generic exception handling in backend/app/core/exception_handlers.py needs refinement."

♻️ Proposed improvement
+    import logging
+    logger = logging.getLogger(__name__)
+
     except Exception as e:
+        logger.exception("Validation failed with unexpected error")
         request_log_crud.update_error(
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3932717 and e06dc86.

📒 Files selected for processing (31)
  • backend/app/alembic/env.py
  • backend/app/alembic/versions/001_added_request_log.py
  • backend/app/alembic/versions/002_added_validator_log.py
  • backend/app/api/routes/guardrails.py
  • backend/app/api/routes/utils.py
  • backend/app/core/config.py
  • backend/app/core/exception_handlers.py
  • backend/app/core/guardrail_controller.py
  • backend/app/core/middleware.py
  • backend/app/core/validators/gender_assumption_bias.py
  • backend/app/core/validators/lexical_slur.py
  • backend/app/core/validators/pii_remover.py
  • backend/app/crud/__init__.py
  • backend/app/crud/request_log.py
  • backend/app/main.py
  • backend/app/models/Sample_examples.md
  • backend/app/models/__init__.py
  • backend/app/models/ban_list_safety_validator_config.py
  • backend/app/models/base_validator_config.py
  • backend/app/models/gender_assumption_bias_safety_validator_config.py
  • backend/app/models/guardrail_config.py
  • backend/app/models/lexical_slur_safety_validator_config.py
  • backend/app/models/pii_remover_safety_validator_config.py
  • backend/app/models/request.py
  • backend/app/models/validator_log.py
  • backend/app/tests/conftest.py
  • backend/app/tests/test_build_guard.py
  • backend/app/tests/test_guardrails_api.py
  • backend/app/tests/test_validate_with_guard.py
  • backend/app/tests/validators/test_pii_remover.py
  • backend/app/utils.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/core/config.py
🧰 Additional context used
🧬 Code graph analysis (23)
backend/app/core/guardrail_controller.py (5)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/base_validator_config.py (1)
  • build (25-28)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (12-17)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (12-17)
backend/app/models/pii_remover_safety_validator_config.py (5)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (8-28)
  • build (25-28)
  • resolve_on_fail (13-23)
backend/app/core/validators/pii_remover.py (1)
  • PIIRemover (40-95)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (12-17)
backend/app/main.py (3)
backend/app/core/exception_handlers.py (1)
  • register_exception_handlers (11-33)
backend/app/core/middleware.py (1)
  • http_request_logger (9-23)
backend/app/load_env.py (1)
  • load_environment (5-13)
backend/app/models/ban_list_safety_validator_config.py (4)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (8-28)
  • build (25-28)
  • resolve_on_fail (13-23)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (12-17)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (12-17)
backend/app/crud/request_log.py (2)
backend/app/models/request.py (3)
  • RequestLog (15-24)
  • RequestLogUpdate (26-28)
  • RequestStatus (9-13)
backend/app/utils.py (1)
  • now (11-12)
backend/app/core/validators/pii_remover.py (2)
backend/app/core/validators/gender_assumption_bias.py (1)
  • _validate (32-55)
backend/app/core/validators/lexical_slur.py (1)
  • _validate (40-64)
backend/app/models/lexical_slur_safety_validator_config.py (5)
backend/app/core/enum.py (1)
  • SlurSeverity (3-7)
backend/app/core/validators/lexical_slur.py (1)
  • LexicalSlur (22-119)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (8-28)
  • build (25-28)
  • resolve_on_fail (13-23)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (12-17)
backend/app/models/gender_assumption_bias_safety_validator_config.py (6)
backend/app/models/base_validator_config.py (3)
  • BaseValidatorConfig (8-28)
  • build (25-28)
  • resolve_on_fail (13-23)
backend/app/core/enum.py (1)
  • BiasCategories (9-13)
backend/app/core/validators/gender_assumption_bias.py (1)
  • GenderAssumptionBias (18-84)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (12-17)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (12-17)
backend/app/alembic/versions/001_added_request_log.py (1)
backend/app/alembic/versions/002_added_validator_log.py (2)
  • upgrade (22-33)
  • downgrade (36-37)
backend/app/models/base_validator_config.py (4)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (12-17)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (12-17)
backend/app/models/validator_log.py (1)
backend/app/utils.py (1)
  • now (11-12)
backend/app/core/validators/gender_assumption_bias.py (2)
backend/app/core/config.py (1)
  • Settings (26-100)
backend/app/core/enum.py (1)
  • BiasCategories (9-13)
backend/app/core/exception_handlers.py (1)
backend/app/utils.py (2)
  • APIResponse (14-40)
  • failure_response (27-40)
backend/app/crud/__init__.py (1)
backend/app/crud/request_log.py (1)
  • RequestLogCrud (8-57)
backend/app/alembic/env.py (1)
backend/app/core/config.py (1)
  • SQLALCHEMY_DATABASE_URI (68-76)
backend/app/core/middleware.py (1)
backend/app/tests/conftest.py (1)
  • client (13-19)
backend/app/models/__init__.py (2)
backend/app/models/request.py (1)
  • RequestLog (15-24)
backend/app/models/validator_log.py (1)
  • ValidatorLog (8-16)
backend/app/models/guardrail_config.py (4)
backend/app/models/ban_list_safety_validator_config.py (1)
  • BanListSafetyValidatorConfig (7-15)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • GenderAssumptionBiasSafetyValidatorConfig (7-15)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • LexicalSlurSafetyValidatorConfig (7-17)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • PIIRemoverSafetyValidatorConfig (7-17)
backend/app/models/request.py (1)
backend/app/utils.py (1)
  • now (11-12)
backend/app/tests/conftest.py (1)
backend/app/api/deps.py (1)
  • verify_bearer_token (19-39)
backend/app/tests/test_build_guard.py (6)
backend/app/core/guardrail_controller.py (1)
  • build_guard (7-9)
backend/app/models/ban_list_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/base_validator_config.py (1)
  • build (25-28)
backend/app/models/gender_assumption_bias_safety_validator_config.py (1)
  • build (11-15)
backend/app/models/lexical_slur_safety_validator_config.py (1)
  • build (12-17)
backend/app/models/pii_remover_safety_validator_config.py (1)
  • build (12-17)
backend/app/tests/test_guardrails_api.py (2)
backend/app/tests/guardrails_mocks.py (2)
  • MockResult (1-4)
  • MockFailure (6-8)
backend/app/tests/test_validate_with_guard.py (6)
  • MockGuard (15-17)
  • MockGuard (37-42)
  • MockGuard (66-68)
  • validate (16-17)
  • validate (38-42)
  • validate (67-68)
backend/app/tests/validators/test_pii_remover.py (1)
backend/app/core/validators/pii_remover.py (2)
  • PIIRemover (40-95)
  • _validate (84-95)
🪛 LanguageTool
backend/app/models/Sample_examples.md

[style] ~48-~48: The word “ya” is non-standard. Did you mean “you”, “your” or “yes”?
Context: ... hue hain, aur unse +91-98765-43210 par ya email [email protected] ke thro...

(YA)


[style] ~70-~70: The word “ya” is non-standard. Did you mean “you”, “your” or “yes”?
Context: ... hue hain, aur unse +91-98765-43210 par ya email [email protected] ke thro...

(YA)

🪛 markdownlint-cli2 (0.18.1)
backend/app/models/Sample_examples.md

4-4: Bare URL used

(MD034, no-bare-urls)


4-4: Bare URL used

(MD034, no-bare-urls)


26-26: Bare URL used

(MD034, no-bare-urls)


26-26: Bare URL used

(MD034, no-bare-urls)


48-48: Bare URL used

(MD034, no-bare-urls)


48-48: Bare URL used

(MD034, no-bare-urls)


70-70: Bare URL used

(MD034, no-bare-urls)


70-70: Bare URL used

(MD034, no-bare-urls)


186-186: Bare URL used

(MD034, no-bare-urls)


186-186: Bare URL used

(MD034, no-bare-urls)

🪛 Ruff (0.14.10)
backend/app/core/guardrail_controller.py

14-14: No newline at end of file

Add trailing newline

(W292)

backend/app/models/pii_remover_safety_validator_config.py

2-2: typing.List is deprecated, use list instead

(UP035)

backend/app/models/ban_list_safety_validator_config.py

1-1: typing.List is deprecated, use list instead

(UP035)


15-15: No newline at end of file

Add trailing newline

(W292)

backend/app/crud/request_log.py

57-57: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/pii_remover.py

3-3: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


95-95: No newline at end of file

Add trailing newline

(W292)

backend/app/models/lexical_slur_safety_validator_config.py

1-1: typing.List is deprecated, use list instead

(UP035)

backend/app/api/routes/guardrails.py

138-138: No newline at end of file

Add trailing newline

(W292)

backend/app/models/gender_assumption_bias_safety_validator_config.py

1-1: typing.List is deprecated, use list instead

(UP035)

backend/app/alembic/versions/001_added_request_log.py

8-8: Import from collections.abc instead: Sequence

Import from collections.abc

(UP035)


17-17: Use X | Y for type annotations

Convert to X | Y

(UP007)


18-18: Use X | Y for type annotations

Convert to X | Y

(UP007)


19-19: Use X | Y for type annotations

Convert to X | Y

(UP007)

backend/app/models/base_validator_config.py

28-28: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/lexical_slur.py

4-4: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


119-119: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/gender_assumption_bias.py

2-2: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


2-2: typing.List is deprecated, use list instead

(UP035)


25-25: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


84-84: No newline at end of file

Add trailing newline

(W292)

backend/app/core/exception_handlers.py

13-13: Unused function argument: request

(ARG001)


20-20: Unused function argument: request

(ARG001)


27-27: Unused function argument: request

(ARG001)

backend/app/alembic/versions/002_added_validator_log.py

8-8: Import from collections.abc instead: Sequence

Import from collections.abc

(UP035)


17-17: Use X | Y for type annotations

Convert to X | Y

(UP007)


18-18: Use X | Y for type annotations

Convert to X | Y

(UP007)


19-19: Use X | Y for type annotations

Convert to X | Y

(UP007)

backend/app/crud/__init__.py

1-1: No newline at end of file

Add trailing newline

(W292)

backend/app/api/routes/utils.py

7-7: No newline at end of file

Add trailing newline

(W292)

backend/app/core/middleware.py

13-13: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


18-18: Local variable client_ip is assigned to but never used

Remove assignment to unused variable client_ip

(F841)

backend/app/models/__init__.py

4-4: No newline at end of file

Add trailing newline

(W292)

backend/app/models/guardrail_config.py

1-1: typing.List is deprecated, use list instead

(UP035)


14-19: Use X | Y for type annotations

Convert to X | Y

(UP007)


31-31: No newline at end of file

Add trailing newline

(W292)

backend/app/models/request.py

28-28: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/conftest.py

29-29: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/test_build_guard.py

71-71: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/test_guardrails_api.py

112-112: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/test_validate_with_guard.py

14-14: Unused function argument: client

(ARG001)


106-106: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/validators/test_pii_remover.py

23-23: Unused function argument: mock_presidio

(ARG001)


41-41: Use == to compare constant literals

Replace is with ==

(F632)


49-49: Use == to compare constant literals

Replace is with ==

(F632)


54-54: Use == to compare constant literals

Replace is with ==

(F632)


77-77: Unused function argument: mock_presidio

(ARG001)


83-83: No newline at end of file

Add trailing newline

(W292)

backend/app/utils.py

4-4: typing.Dict is deprecated, use dict instead

(UP035)


40-40: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (28)
backend/app/api/routes/utils.py (1)

5-7: LGTM! Simple and effective health check endpoint.

The implementation is correct. Consider adding a trailing newline at end of file per Ruff W292.

backend/app/models/Sample_examples.md (1)

153-182: Good inclusion of error case testing.

The intentionally incorrect validator type "uli_slur_matchx" on line 159 is a useful test case for validation error handling.

backend/app/utils.py (2)

11-12: Stripping timezone info from UTC datetime may cause confusion.

Removing tzinfo from a UTC datetime creates a naive datetime that could be misinterpreted as local time elsewhere in the codebase. Consider keeping the timezone info or documenting this behavior clearly.

Is this intentional for database compatibility? If so, consider adding a docstring to clarify the purpose.


14-24: Well-designed API response envelope pattern.

The generic APIResponse[T] with factory methods provides a clean, consistent structure for API responses across the codebase.

backend/app/core/exception_handlers.py (2)

11-17: LGTM on validation error handler.

The handler correctly uses the 422 status code and formats validation errors through the centralized APIResponse class. The request parameter is required by FastAPI's exception handler signature even if unused.


19-24: LGTM on HTTP exception handler.

Properly preserves the original status code and detail from the exception while maintaining consistent response formatting.

backend/app/models/request.py (1)

9-13: LGTM on RequestStatus enum.

Clear enumeration of request lifecycle states. The str mixin ensures proper JSON serialization.

backend/app/alembic/versions/001_added_request_log.py (1)

22-33: LGTM on upgrade function.

Table schema correctly mirrors the RequestLog model with appropriate column types and constraints.

backend/app/crud/__init__.py (1)

1-1: LGTM with minor style fix.

The re-export is correct. Add a trailing newline for consistency.

Minor fix
 from app.crud.request_log import RequestLogCrud
+
backend/app/tests/conftest.py (1)

1-9: Good setup ordering for test environment.

Setting ENVIRONMENT before importing the app ensures the correct configuration is loaded. The dependency override pattern for verify_bearer_token is appropriate for bypassing authentication in tests.

backend/app/tests/test_build_guard.py (2)

6-37: Good test coverage with proper isolation.

The test correctly mocks Guard and validator configs to verify:

  • Each config's build() is called
  • Guard is instantiated once
  • use_many receives the built validators
  • Return value is properly forwarded

39-71: Comprehensive edge case and error handling tests.

Good coverage of:

  • Empty validator list (edge case)
  • build() failure propagation
  • use_many() failure propagation

This ensures the function doesn't swallow exceptions and handles empty inputs gracefully.

Note: Add a trailing newline at end of file per linter (W292).

backend/app/main.py (4)

28-29: Middleware execution order consideration.

Middleware added via app.middleware("http") and app.add_middleware() execute in reverse order of registration. Here, CorrelationIdMiddleware (added second) will execute first on the request, then http_request_logger. This is the correct order for correlation IDs to be available in logs.


19-33: Well-structured application setup.

Good practices observed:

  • Conditional Sentry initialization based on environment
  • Custom OpenAPI ID generation for cleaner API client codegen
  • Proper exception handler registration

15-17: Potential IndexError if route.tags is an empty list.

The condition if route.tags is truthy for an empty list [], but route.tags[0] would raise IndexError. While FastAPI typically doesn't create routes with empty tag lists, a defensive check is safer.

🐛 Suggested fix
 def custom_generate_unique_id(route: APIRoute) -> str:
-    tag = route.tags[0] if route.tags else "default"
+    tag = route.tags[0] if route.tags and len(route.tags) > 0 else "default"
     return f"{tag}-{route.name}"

Likely an incorrect or invalid review comment.


12-13: The environment loading pattern is not fragile and does not conflict with test setup. The load_environment() function in load_env.py uses os.getenv("ENVIRONMENT", "default") which correctly respects pre-set environment variables. The conftest.py sets ENVIRONMENT before importing app.main, ensuring that when load_environment() is called at module level, the env var is already available and will be read correctly. This is a standard, robust pattern with no ordering conflicts.

Likely an incorrect or invalid review comment.

backend/app/tests/test_guardrails_api.py (2)

11-39: Good test coverage for route existence and success path.

The tests properly verify that routes exist and the success path returns expected response structure with success, safe_input, and response_id fields.


41-68: Validation failure test correctly verifies error response structure.

The test appropriately checks for 400 status, error type, action, and failure message presence.

backend/app/alembic/env.py (1)

53-80: Online migration mode looks correct.

The online mode properly uses get_url() to fetch the database URL from application settings, configures the engine with NullPool (appropriate for migrations), and runs migrations within a transaction.

backend/app/tests/test_validate_with_guard.py (1)

35-54: Good coverage of error scenarios.

The tests appropriately cover validation errors with failures, validation errors without failures (edge case), and exception handling. The use of MockResult and MockFailure from the shared mocks module promotes consistency.

Also applies to: 64-80, 88-100

backend/app/models/guardrail_config.py (1)

23-31: Request models look correct.

The separation of GuardrailInputRequest and GuardrailOutputRequest is clean and provides clear API contracts. Add a trailing newline at the end of the file.

backend/app/models/ban_list_safety_validator_config.py (1)

7-15: Config class follows established pattern correctly.

The implementation properly extends BaseValidatorConfig, uses a literal type for the discriminator, and the build() method correctly constructs the BanList validator with the resolved on_fail action.

Consider adding a return type hint to build() for clarity: def build(self) -> BanList:.

backend/app/core/validators/pii_remover.py (1)

84-95: Validation logic is correct.

The _validate method follows the same pattern as other validators in the codebase: detect issues, apply fixes, return FailResult with fix_value if modified or PassResult if unchanged. Add a trailing newline at the end of the file.

backend/app/models/base_validator_config.py (3)

13-23: Conversion logic is correct.

The resolve_on_fail method properly handles None, performs case-insensitive lookup, and provides a clear error message for invalid values.


25-28: Abstract method pattern is acceptable.

The NotImplementedError approach works. For stricter static analysis support, you could use ABC and @abstractmethod, but this is optional. Add a trailing newline at the end of the file.


8-11: No issue found: OnFailAction.FIX is a StrEnum member and is already a string.

Since OnFailAction is a StrEnum, the enum member OnFailAction.FIX is a string subclass that equals "fix". This is fully compatible with the Optional[ON_FAIL_STR] type annotation, which expects string literals. Both OnFailAction.FIX and "fix" are equivalent in this context, so no fix is needed.

backend/app/alembic/versions/002_added_validator_log.py (1)

22-33: Migration looks correct for the table structure.

The foreign key constraint to request_log.id and primary key on id are properly defined. The down_revision = '001' correctly chains to the previous migration.

backend/app/core/validators/lexical_slur.py (1)

109-116: Severity filtering logic may be inverted from user expectations.

The current logic:

  • SlurSeverity.Low → includes L, M, H (filters the most slurs)
  • SlurSeverity.High → includes only H (filters the fewest slurs)

This naming is counterintuitive. Users might expect "High severity filtering" to be more strict, not less. Consider renaming or clarifying in documentation.

Is this the intended behavior? The PR description mentions "Questions about lexical_slur severity logic."

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @backend/app/api/routes/guardrails.py:
- Line 17: The route declaration uses inconsistent trailing slashes (e.g.,
router.post("/input/") vs router.post("/output")); remove the trailing slash
from the "/input/" endpoint so it becomes "/input" and ensure the other route(s)
(referenced around the same file, e.g., the endpoint at line 33) follow the same
convention; update any internal references or tests that call "/input/" to use
"/input" to avoid unintended 307 redirects.
- Around line 1-5: Remove the unused imports by deleting "error" from the "from
re import error" statement and removing "HTTPException" from the "from fastapi
import APIRouter, HTTPException, status" line; ensure only the needed imports
remain (e.g., keep uuid, UUID, APIRouter, status) so there are no unused import
warnings in guardrails.py.

In @backend/app/core/validators/gender_assumption_bias.py:
- Around line 23-29: The __init__ method currently uses a mutable default for
categories (Optional[List[BiasCategories]] = [BiasCategories.All]); change the
default to None and initialize the list inside the constructor (e.g., if
categories is None: categories = [BiasCategories.All]) before calling
self.load_gender_bias_list(categories) and then call
super().__init__(on_fail=on_fail); update the type hint/default accordingly to
avoid the shared mutable default bug referenced in load_gender_bias_list and
OnFailAction.FIX.

In @backend/app/tests/test_guardrails_api.py:
- Around line 21-24: The API routes are inconsistent:
"/api/v1/guardrails/input/" has a trailing slash while
"/api/v1/guardrails/output" does not; update the route decorators in
guardrails.py so both endpoints use a consistent pattern (pick either both with
trailing slashes or both without), e.g., change the output route to
"/api/v1/guardrails/output/" (or change input to remove its trailing slash) and
then update the test in test_routes_exist (function test_routes_exist) to assert
the matching normalized paths in client.app.routes accordingly.
🧹 Nitpick comments (16)
backend/app/core/validators/gender_assumption_bias.py (3)

2-2: Use modern type hint syntax.

typing.Callable and typing.List are deprecated in Python 3.9+. Use collections.abc.Callable and the built-in list instead.

Suggested fix
-from typing import Callable, List, Optional
+from collections.abc import Callable
+from typing import Optional

Then on line 25:

-        categories: Optional[List[BiasCategories]] = [BiasCategories.All],
+        categories: Optional[list[BiasCategories]] = None,

44-44: Case is not preserved during replacement.

The replacement always uses the lowercase neutral term. This means "Fireman saved the day" becomes "firefighter saved the day" instead of "Firefighter saved the day", which may affect readability at sentence boundaries.

Consider preserving the original case pattern:

def match_case(original: str, replacement: str) -> str:
    if original.isupper():
        return replacement.upper()
    if original[0].isupper():
        return replacement.capitalize()
    return replacement

# In the loop:
value = re.sub(pattern, lambda m: match_case(m.group(), neutral_term), value, flags=re.IGNORECASE)

70-82: Duplicate entries when BiasCategories.All is combined with other categories.

If categories contains both BiasCategories.All and specific categories (e.g., [BiasCategories.Healthcare, BiasCategories.All]), the same words will be added multiple times. While this doesn't cause incorrect validation results (replaced words won't match again), it increases memory usage and iteration time.

Proposed fix using a set or early return
     def load_gender_bias_list(self, categories):
         file_path = Settings.GENDER_BIAS_LIST_FILEPATH
         neutral_term_col = 'neutral-term'
-        gender_bias_list = []
 
         try:
             df = pandas.read_csv(file_path)
         except FileNotFoundError:
             raise FileNotFoundError(f"Gender bias file not found at {file_path}")
         except Exception as e:
             raise ValueError(f"Failed to load gender bias list from {file_path}: {e}")
 
         df['word'] = df['word'].str.lower()
         df[neutral_term_col] = df[neutral_term_col].str.lower()
 
+        # If All is requested, use entire dataframe
+        if BiasCategories.All in categories:
+            temp = df
+        else:
+            temp = df[df['type'].isin([c.value for c in categories])]
+
+        return [
+            {"word": row["word"], neutral_term_col: row[neutral_term_col]}
+            for row in temp.to_dict(orient="records")
+        ]
-        for category in categories:
-            if category == BiasCategories.All:
-                temp = df
-            else:
-                temp = df[df['type'] == category.value]
-
-            rows = temp.to_dict(orient="records")
-            for row in rows:
-                gender_bias_list.append({
-                    "word": row["word"],
-                    neutral_term_col: row[neutral_term_col]
-                })
-        return gender_bias_list

Also, add a trailing newline at the end of the file.

backend/app/core/validators/lexical_slur.py (5)

33-37: Instance-level cache defeats caching purpose.

_SLUR_CACHE is initialized as an instance variable, so each LexicalSlur instance has its own cache. If the intent is to share loaded slur lists across instances to avoid repeated file I/O, this should be a class-level variable.

♻️ Proposed fix
+_SLUR_CACHE: dict = {}
+
 @register_validator(name="lexical-slur", data_type="string")
 class LexicalSlur(Validator):
     """
     Validate text for the presence of lexical slurs using a predefined list.
     """

     def __init__(
         self, 
         severity: SlurSeverity = SlurSeverity.All,
         languages: Optional[list] = None,
         on_fail: Optional[Callable] = OnFailAction.FIX
     ):    
-        self._SLUR_CACHE = {}
         self.severity = severity

Then update load_slur_list to reference the module-level _SLUR_CACHE.


30-35: Unused languages parameter.

The languages parameter is stored but never used in any method. Either implement language-based filtering or remove this parameter to avoid confusion.


46-54: Consider set-based detection for better performance.

The current O(n×m) detection can be optimized using set intersection. Also, re.IGNORECASE is redundant since clean_text already lowercases the input.

♻️ Suggested optimization
-        for slur in self.slur_list:
-            if slur in words:
-                if slur not in detected_slurs:
-                    detected_slurs.append(slur)
+        word_set = set(words)
+        slur_set = set(self.slur_list)
+        detected_slurs = list(word_set & slur_set)

         if len(detected_slurs) > 0:
             for word in words:
                 if word in detected_slurs:
-                    value = re.sub(rf'\b{re.escape(word)}\b', "[REDACTED_SLUR]", value, flags=re.IGNORECASE)
+                    value = re.sub(rf'\b{re.escape(word)}\b', "[REDACTED_SLUR]", value)

4-4: Import Callable from collections.abc.

For Python 3.9+, prefer importing Callable from collections.abc instead of typing.

-from typing import Callable, Optional
+from collections.abc import Callable
+from typing import Optional

117-117: Add trailing newline.

         self._SLUR_CACHE[cache_key] = slurs
         return slurs
+
backend/app/api/routes/guardrails.py (3)

39-40: Confusing parameter naming: input_text receives output.

The create method's input_text parameter receives payload.output, which is semantically confusing. Consider renaming the parameter in RequestLogCrud.create to text or request_text to be agnostic of input/output context.


124-138: Broad exception handling may hide root causes.

Catching all Exception types makes debugging difficult since configuration errors, validation library bugs, and unexpected failures all produce the same generic error response. Consider catching more specific exceptions or at minimum logging the full traceback before returning the generic error.

except Exception as e:
    import logging
    logging.exception("Unexpected error during validation")
    # ... rest of error handling

138-138: Add trailing newline.

             error=str(e),
         )
+
backend/app/tests/test_validate_with_guard.py (3)

11-12: Shared mocks without reset may cause test pollution.

mock_request_log_crud is defined at module level and reused across all tests without being reset. Method call history accumulates, which could cause false positives/negatives if tests depend on call counts. Consider using a pytest fixture with MagicMock() that provides a fresh mock for each test.

♻️ Proposed fix
-mock_request_log_crud = MagicMock()
-mock_request_log_id = uuid4()
+
+@pytest.fixture
+def mock_request_log_crud():
+    return MagicMock()
+
+
+@pytest.fixture
+def mock_request_log_id():
+    return uuid4()

Then update each test to accept these fixtures as parameters.


15-36: Add assertions for CRUD method invocations.

The tests verify the response structure but don't assert that request_log_crud.update_success or update_error was called with the correct arguments. This leaves CRUD interaction behavior untested.

# After the response assertions, add:
mock_request_log_crud.update_success.assert_called_once()
call_args = mock_request_log_crud.update_success.call_args
assert call_args.kwargs["request_log_id"] == mock_request_log_id

107-107: Add trailing newline.

     assert response.error == "Invalid config"
+
backend/app/tests/test_guardrails_api.py (1)

98-114: Add trailing newline.

Missing newline at end of file per Python style conventions (PEP 8) and static analysis hint.

Proposed fix
     assert body["success"] is False
     assert body["data"]["safe_input"] is None
     assert "Invalid validator config" in body["error"]
+
backend/app/tests/test_guardrails_api_integration.py (1)

151-172: Add trailing newline and verify exception message assertion.

  1. Missing newline at end of file.
  2. The assertion "chakki" in body["error"] (line 172) assumes the slur word appears in the error message. Verify this is the actual exception behavior and won't cause test flakiness if error message format changes.
Proposed fix for trailing newline
     body = response.json()
     assert body["success"] is False
     assert "chakki" in body["error"]
+
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e06dc86 and f6df183.

📒 Files selected for processing (9)
  • backend/app/api/routes/guardrails.py
  • backend/app/core/validators/gender_assumption_bias.py
  • backend/app/core/validators/lexical_slur.py
  • backend/app/models/__init__.py
  • backend/app/tests/conftest.py
  • backend/app/tests/test_guardrails_api.py
  • backend/app/tests/test_guardrails_api_integration.py
  • backend/app/tests/test_validate_with_guard.py
  • backend/app/tests/validators/test_pii_remover.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/tests/conftest.py
🧰 Additional context used
🧬 Code graph analysis (8)
backend/app/core/validators/lexical_slur.py (2)
backend/app/core/config.py (1)
  • Settings (26-100)
backend/app/core/enum.py (1)
  • SlurSeverity (3-7)
backend/app/core/validators/gender_assumption_bias.py (1)
backend/app/core/enum.py (1)
  • BiasCategories (9-13)
backend/app/api/routes/guardrails.py (5)
backend/app/core/guardrail_controller.py (2)
  • build_guard (7-9)
  • get_validator_config_models (11-14)
backend/app/crud/request_log.py (4)
  • create (12-20)
  • get (56-57)
  • update_success (22-37)
  • update_error (39-54)
backend/app/models/guardrail_config.py (2)
  • GuardrailInputRequest (23-26)
  • GuardrailOutputRequest (28-31)
backend/app/models/request.py (1)
  • RequestLogUpdate (26-28)
backend/app/utils.py (3)
  • APIResponse (14-40)
  • failure_response (27-40)
  • success_response (21-24)
backend/app/models/__init__.py (2)
backend/app/models/request.py (1)
  • RequestLog (15-24)
backend/app/models/validator_log.py (1)
  • ValidatorLog (8-16)
backend/app/tests/test_guardrails_api.py (4)
backend/app/tests/guardrails_mocks.py (2)
  • MockFailure (6-8)
  • MockResult (1-4)
backend/app/crud/request_log.py (1)
  • create (12-20)
backend/app/tests/conftest.py (1)
  • client (43-45)
backend/app/tests/test_validate_with_guard.py (6)
  • MockGuard (17-19)
  • MockGuard (41-46)
  • MockGuard (68-70)
  • validate (18-19)
  • validate (42-46)
  • validate (69-70)
backend/app/tests/test_guardrails_api_integration.py (1)
backend/app/tests/conftest.py (1)
  • integration_client (49-52)
backend/app/tests/test_validate_with_guard.py (3)
backend/app/api/routes/guardrails.py (1)
  • _validate_with_guard (78-138)
backend/app/tests/guardrails_mocks.py (2)
  • MockFailure (6-8)
  • MockResult (1-4)
backend/app/utils.py (1)
  • APIResponse (14-40)
backend/app/tests/validators/test_pii_remover.py (1)
backend/app/core/validators/pii_remover.py (2)
  • PIIRemover (40-95)
  • _validate (84-95)
🪛 Ruff (0.14.10)
backend/app/core/validators/lexical_slur.py

4-4: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


117-117: No newline at end of file

Add trailing newline

(W292)

backend/app/core/validators/gender_assumption_bias.py

2-2: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


2-2: typing.List is deprecated, use list instead

(UP035)


25-25: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)


82-82: No newline at end of file

Add trailing newline

(W292)

backend/app/api/routes/guardrails.py

138-138: No newline at end of file

Add trailing newline

(W292)

backend/app/models/__init__.py

4-4: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/test_guardrails_api.py

27-27: Unused function argument: mock_crud

(ARG001)


50-50: Unused function argument: mock_crud

(ARG001)


76-76: Unused function argument: mock_crud

(ARG001)


98-98: Unused function argument: mock_crud

(ARG001)


114-114: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/test_guardrails_api_integration.py

172-172: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/test_validate_with_guard.py

107-107: No newline at end of file

Add trailing newline

(W292)

backend/app/tests/validators/test_pii_remover.py

33-33: Unused function argument: mock_presidio

(ARG001)


79-79: Unused function argument: mock_presidio

(ARG001)


109-109: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (12)
backend/app/models/__init__.py (1)

1-4: LGTM, but add trailing newline.

The module exports are well-organized. Add a trailing newline to satisfy the linter.

🔧 Proposed fix
 from sqlmodel import SQLModel

 from app.models.request import RequestLog
 from app.models.validator_log import ValidatorLog
+
backend/app/tests/validators/test_pii_remover.py (2)

32-34: Fixture dependency pattern is correct.

The mock_presidio parameter is intentionally used as a pytest fixture dependency to ensure mocks are active before PIIRemover is instantiated. The static analysis warning (ARG001) is a false positive in this context.


85-109: Good test coverage for Indian recognizers, add trailing newline.

The test correctly verifies that all 5 Indian recognizers are registered when their entity types are requested. Add a trailing newline at the end of the file.

🔧 Proposed fix
         # Called once per recognizer
         assert registry.add_recognizer.call_count == 5
+
backend/app/core/validators/lexical_slur.py (1)

107-114: Verify severity filtering logic is intentional.

The current logic is counterintuitive: SlurSeverity.Low includes all slurs (L/M/H), while SlurSeverity.High only includes H. This behavior implies "Low" means "minimum threshold" rather than "only low-severity slurs."

If this is the intended design, consider renaming the enum values (e.g., ThresholdLow, ThresholdHigh) or adding documentation to clarify that severity acts as a minimum threshold, not a filter.

backend/app/api/routes/guardrails.py (1)

87-89: Synchronous guard.validate() may block the event loop.

guard.validate(data) is a synchronous call within an async function. If validation is CPU-intensive or I/O-bound, this could block the event loop. Consider running it in a thread pool executor if this becomes a bottleneck.

import asyncio
result = await asyncio.get_event_loop().run_in_executor(None, guard.validate, data)
backend/app/tests/test_guardrails_api.py (3)

13-18: Fixture used for side effects – valid pattern.

The mock_crud fixture is correctly patching RequestLogCrud for tests that hit endpoints using it. The fixture is passed to tests for its patching side effect rather than its return value, which is a valid pytest pattern. Static analysis flags it as unused, but this is intentional.


27-47: Well-structured unit test with proper mocking.

The test correctly mocks build_guard, validates the response structure, and asserts on key fields (success, safe_input, response_id). Good coverage of the happy path.


50-73: Validation failure path covered correctly.

The test properly simulates a guard returning failures and verifies the expected error response structure. Assertions check that success is False, safe_input is None, and the error message propagates.

backend/app/tests/test_guardrails_api_integration.py (4)

1-5: Good use of pytestmark for test categorization.

Using pytestmark = pytest.mark.integration cleanly marks all tests in this module as integration tests, enabling selective test runs.


96-125: Multiple validators test – good coverage of chained validation.

This test validates that multiple validators (slur + banlist) are applied in sequence. The expected output shows both [REDACTED_SLUR] and banlist truncation ("sonography" → "s").

The complex assertion is appropriate for verifying combined validator behavior.


128-148: Good test for invalid validator type handling.

Verifies that an invalid validator type ("lexical_slur" instead of "uli_slur_match") returns HTTP 422 with appropriate error details. The comment on line 143 documents the expected behavior clearly.


8-27: Clarify BanList redaction behavior – first-character truncation needs documentation.

The expected output "this contains b" truncates banned words to their first character, which is the actual behavior of the guardrails.hub.BanList validator with on_fail=FIX. However, this is unusual and differs from other redaction patterns in the codebase (e.g., lexical slur uses [REDACTED_SLUR]). Confirm whether this first-character truncation is:

  • The intended design of BanList's FIX behavior (and if so, should be documented)
  • A limitation or bug in the external library that should be reported upstream
  • Custom behavior that differs from the library's default

The test accurately encodes the current behavior but may need updating if BanList's truncation behavior is unintended.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In @.env.example:
- Around line 1-31: Add a SECRET_KEY variable to the environment template:
insert a new "SECRET_KEY=<YOUR_SECRET_KEY_MINIMUM_32_CHARS>" entry (with a
comment like "# Security") near the other project/stack settings (e.g., after
PROJECT_NAME or the security section suggested after line 9) so the FastAPI app
can read SECRET_KEY from env for session/JWT/crypto operations; ensure the
placeholder makes clear it must be at least 32 characters and uses the same
uppercase naming style as other variables (SECRET_KEY).
- Around line 15-16: The example env file uses insecure default DB credentials;
replace the literal values for POSTGRES_USER and POSTGRES_PASSWORD with
non-secret placeholder tokens (e.g., POSTGRES_USER=<your_postgres_user> and
POSTGRES_PASSWORD=<your_postgres_password>) so consumers must explicitly set
secure credentials instead of copying "postgres"/"postgres".

In @.env.test:
- Around line 1-31: The test env file is missing the SECRET_KEY env var required
by the app; add a SECURITY section entry named SECRET_KEY with a secure value
(minimum 32 characters) to .env.test (e.g., insert
"SECRET_KEY=<YOUR_SECRET_KEY_MINIMUM_32_CHARS>" after PROJECT_NAME or in a
Security block) so the application can read process.env.SECRET_KEY in its
config.
- Around line 15-16: Replace the hardcoded weak DB credentials in .env.test:
change the values assigned to POSTGRES_USER and POSTGRES_PASSWORD from
"postgres" to non-sensitive placeholder tokens (e.g.,
POSTGRES_USER=<your-db-user> and POSTGRES_PASSWORD=<your-db-password>) so tests
require explicit configuration; ensure any CI/test docs reference these
placeholders and that no real secrets are committed.

In @backend/app/core/config.py:
- Around line 86-94: The _enforce_non_default_secrets validator references
attributes that don't exist on Settings (it calls self.SECRET_KEY and
self.FIRST_SUPERUSER_PASSWORD inside _enforce_non_default_secrets and uses
_check_default_secret), so add explicit field definitions for SECRET_KEY and
FIRST_SUPERUSER_PASSWORD to the Settings class with proper type annotations
(e.g., str) and any appropriate default/env configuration so the validator can
safely access them; ensure the field names exactly match the symbols used in
_enforce_non_default_secrets and keep POSTGRES_PASSWORD as-is so the validator
no longer raises AttributeError.
- Around line 96-109: The env_files mapping in get_settings does not include
"staging", so env_files.get(environment) returns None for ENVIRONMENT="staging";
update the env_files dictionary in the get_settings function to include a
"staging" key (e.g., map "staging" to ROOT_DIR / ".env" or to the same value as
"production") so the _env_file passed to Settings is not None and the staging
environment loads the expected .env file.
- Around line 81-84: The code in _check_default_secret uses self.ENVIRONMENT ==
"local" to decide whether to warn, but "local" is not part of the ENVIRONMENT
Literal type, so the warning path is unreachable; either add "local" to the
ENVIRONMENT Literal declaration (alongside "development", "testing", "staging",
"production") or change the conditional in _check_default_secret to check for
"development" (or another non-production value already present in the Literal)
so the warning branch can be taken as intended.

In @backend/README.md:
- Line 183: Remove the potential API key snippet "[HBPo]" from the README
example and replace it with a neutral placeholder such as "<YOUR_API_KEY>" or
"[REDACTED]" so no real or partial keys appear in docs; update the line that
currently reads 'Enter API Key below leave empty if you want to keep existing
token [HBPo]' to use the chosen placeholder and optionally clarify that leaving
it empty preserves the existing token.
- Line 128: The README contains an inconsistent reference saying "import your
SQLModel models from `./backend/app/models.py`" while the project uses a models/
directory; update that sentence to reference the models directory (e.g., change
`./backend/app/models.py` to `./backend/app/models/`) and ensure other README
lines that mention modifying models point to the same `models/` directory to
keep the documentation consistent.
🧹 Nitpick comments (20)
backend/scripts/install_guardrails_from_hub.sh (3)

23-25: Remove duplicate SCRIPT_DIR definition.

SCRIPT_DIR is already defined on line 4 and redefined here on line 23, which is redundant and can cause confusion during maintenance.

Suggested fix
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-BACKEND_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-MANIFEST_FILE="${1:-$BACKEND_DIR/app/core/validators/validators.json}"
+BACKEND_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+MANIFEST_FILE="${1:-$BACKEND_DIR/app/core/validators/validators.json}"

38-42: Quote command substitutions to prevent word splitting.

The unquoted command substitutions on lines 40-41 can cause issues if the shell interprets whitespace incorrectly. While the current values are simple flags, quoting ensures robustness.

Suggested fix
 guardrails configure \
   --token "$GUARDRAILS_HUB_API_KEY" \
-  $( [[ "$ENABLE_METRICS" == "true" ]] && echo "--enable-metrics" || echo "--disable-metrics" ) \
-  $( [[ "$ENABLE_REMOTE_INFERENCING" == "true" ]] && echo "--enable-remote-inferencing" || echo "--disable-remote-inferencing" )
+  "$( [[ "$ENABLE_METRICS" == "true" ]] && echo "--enable-metrics" || echo "--disable-metrics" )" \
+  "$( [[ "$ENABLE_REMOTE_INFERENCING" == "true" ]] && echo "--enable-remote-inferencing" || echo "--disable-remote-inferencing" )"

62-65: Use a while loop with readarray to safely iterate over hub sources.

The for SRC in $HUB_SOURCES pattern relies on word splitting, which will break if any source path contains spaces or special characters. Using a while loop with proper IFS handling is more robust.

Suggested fix
-for SRC in $HUB_SOURCES; do
+while IFS= read -r SRC; do
+  [[ -z "$SRC" ]] && continue
   echo "Installing Guardrails hub validator: $SRC"
   guardrails hub install "$SRC"
-done
+done <<< "$HUB_SOURCES"
backend/app/core/config.py (1)

16-21: Consider clarifying the parse_cors logic for JSON-like strings.

The condition on line 19 (list | str) will always be True for strings that start with [ (since they already passed the isinstance(v, str) check implicitly). This means JSON array strings like "[\"http://a.com\"]" are returned unparsed. If this is intentional (expecting JSON parsing elsewhere), a comment would help. If not, you may want to parse the JSON.

♻️ Suggested clarification
 def parse_cors(v: Any) -> list[str] | str:
     if isinstance(v, str) and not v.startswith("["):
         return [i.strip() for i in v.split(",") if i.strip()]
-    elif isinstance(v, list | str):
+    elif isinstance(v, list):
+        return v
+    elif isinstance(v, str):
+        # JSON-like string starting with "[" - return as-is for downstream parsing
         return v
     raise ValueError(v)
.env.example (3)

5-6: Remove unnecessary quotes from values.

In .env files, quotes are often interpreted literally by parsers. Values like PROJECT_NAME="Kaapi-Guardrails" may result in the application receiving "Kaapi-Guardrails" (including quotes) rather than Kaapi-Guardrails.

♻️ Proposed fix
-PROJECT_NAME="Kaapi-Guardrails"
+PROJECT_NAME=Kaapi-Guardrails
 STACK_NAME=Kaapi-Guardrails

29-31: Remove quotes from API key/token placeholders.

The quotes around placeholder values will be interpreted literally by .env parsers, potentially causing authentication failures when users replace the placeholders but keep the quotes.

♻️ Proposed fix
 # require as a env if you want to use doc transformation
-OPENAI_API_KEY="<ADD-KEY>"
-GUARDRAILS_HUB_API_KEY="<ADD-KEY>"
-AUTH_TOKEN="<ADD-TOKEN>"
+OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
+GUARDRAILS_HUB_API_KEY=<YOUR_GUARDRAILS_HUB_API_KEY>
+AUTH_TOKEN=<YOUR_AUTH_TOKEN>

31-31: Add blank line at end of file.

POSIX standards recommend files end with a newline character. This improves consistency and avoids issues with certain tools.

.env.test (3)

5-6: Remove unnecessary quotes from values.

In .env files, quotes are often interpreted literally by parsers. Values like PROJECT_NAME="Kaapi-Guardrails" may result in the application receiving "Kaapi-Guardrails" (including quotes) rather than Kaapi-Guardrails.

♻️ Proposed fix
-PROJECT_NAME="Kaapi-Guardrails"
+PROJECT_NAME=Kaapi-Guardrails
 STACK_NAME=Kaapi-Guardrails

29-31: Remove quotes from API key/token placeholders.

The quotes around placeholder values will be interpreted literally by .env parsers, potentially causing authentication failures when users replace the placeholders but keep the quotes.

♻️ Proposed fix
 # require as a env if you want to use doc transformation
-OPENAI_API_KEY="<ADD-KEY>"
-GUARDRAILS_HUB_API_KEY="<ADD-KEY>"
-AUTH_TOKEN="<ADD-TOKEN>"
+OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
+GUARDRAILS_HUB_API_KEY=<YOUR_GUARDRAILS_HUB_API_KEY>
+AUTH_TOKEN=<YOUR_AUTH_TOKEN>

31-31: Add blank line at end of file.

POSIX standards recommend files end with a newline character. This improves consistency and avoids issues with certain tools.

backend/README.md (10)

3-6: Add Python version requirement.

The documentation specifies Docker and uv as requirements but does not mention the required Python version. This information is crucial for developers setting up the environment.

📝 Suggested addition
 ## Requirements
 
+* Python 3.11+ (or specify the exact version required)
 * [Docker](https://www.docker.com/).
 * [uv](https://docs.astral.sh/uv/) for Python package and environment management.

44-44: Clarify the location of the commented-out command override.

The documentation mentions "a commented out command override" but doesn't specify where this override is located. Developers need to know which file to edit (likely docker-compose.yml or similar).

📝 Suggested improvement
-There is also a commented out `command` override, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes.
+There is also a commented-out `command` override in the `docker-compose.yml` file, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes.

86-102: Clarify when to use each test script.

The documentation mentions two different scripts (scripts/test.sh and scripts/tests-start.sh) without clearly explaining when to use each. Line 89 suggests running test.sh from the host, while line 101 shows running tests-start.sh inside the container.

📝 Suggested improvement

Add clarification such as:

To test the backend, you have two options:

**Option 1: Run from host (stack will be started automatically)**
```console
$ bash ./scripts/test.sh

Option 2: Run inside an already-running container
If your stack is already up and you just want to run the tests, you can use:

docker compose exec backend bash scripts/tests-start.sh
</details>

---

`161-161`: **Improve wording and add .env file setup instructions.**

The wording "correct value from `GUARDRAILS_HUB_API_KEY`" is awkward. Additionally, the documentation doesn't explain how developers should create or configure the `.env` file initially.



<details>
<summary>📝 Suggested improvement</summary>

```diff
-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).
+1. Create a `.env` file in the `backend/` directory (if it doesn't exist) and add the `GUARDRAILS_HUB_API_KEY` variable. You can obtain your API key from [Guardrails Hub](https://hub.guardrailsai.com/keys).
+   
+   ```bash
+   GUARDRAILS_HUB_API_KEY=your_api_key_here
+   ```
+   
+   **Important**: Ensure `.env` is listed in `.gitignore` to prevent accidentally committing secrets.

188-193: Add language specifiers to code blocks.

The fenced code blocks are missing language specifiers, which affects syntax highlighting and documentation clarity.

📝 Proposed fix
 To install any validator from Guardrails Hub:
-```
+```bash
 guardrails hub install hub://guardrails/<validator-name>
 
 Example -
 guardrails hub install hub://guardrails/ban_list
</details>

---

`200-212`: **Clarify template code and add language specifiers.**

The code block is missing a language specifier, and the incomplete import statement and `build()` method might confuse developers. Consider adding comments to clarify this is a template.



<details>
<summary>📝 Proposed improvement</summary>

```diff
-```
+```python
+# Template for adding a new validator from Guardrails Hub
 from guardrails.hub import # validator name from Guardrails Hub
 from typing import List, Literal
 
 from app.models.base_validator_config import BaseValidatorConfig
 
 class <Validator-name>SafetyValidatorConfig(BaseValidatorConfig):
     type: Literal["<validator-name>"]
-    banned_words: List[str]
+    # Add configuration parameters specific to your validator
+    banned_words: List[str]  # Example parameter
 
     # This method returns the validator constructor.
     def build(self):
+        # Return the validator instance with appropriate parameters
+        pass  # Replace with actual implementation
</details>

---

`216-233`: **Add language specifier to code block.**

The code block is missing a language specifier for proper syntax highlighting.



<details>
<summary>📝 Proposed fix</summary>

```diff
 For example, this is the code for [BanList validator](https://guardrailsai.com/hub/validator/guardrails/ban_list).
 
-```
+```python
 from guardrails.hub import BanList
 from typing import List, Literal

242-266: Add language specifier and clarify template.

The code block is missing a language specifier. Also, consider clarifying that this is a template and adding more specific guidance.

📝 Proposed improvement
-```
+```python
+# Template for creating a custom validator
 from guardrails import OnFailAction
 from guardrails.validators import (
     FailResult,
     PassResult,
     register_validator,
     ValidationResult,
     Validator
 )
 from typing import Callable, List, Optional
 
 @register_validator(name="<validator-name>", data_type="string")
 class <Validator-Name>(Validator):
 
     def __init__(
         self,
-        # any parameters required while initializing the validator 
+        # Add any parameters required while initializing the validator 
         on_fail: Optional[Callable] = OnFailAction.FIX #can be changed
     ):
         # Initialize the required variables
         super().__init__(on_fail=on_fail)
 
     def _validate(self, value: str, metadata: dict = None) -> ValidationResult:
-        # add logic for validation
+        # Add your validation logic here
+        # Return PassResult() if validation succeeds
+        # Return FailResult() if validation fails
+        pass
</details>

---

`270-281`: **Add language specifier to code block.**

The code block is missing a language specifier for proper syntax highlighting.



<details>
<summary>📝 Proposed fix</summary>

```diff
-```
+```python
+# Template for custom validator configuration
 from typing import List, Literal
 
 from app.models.base_validator_config import BaseValidatorConfig

285-300: Add language specifier to code block.

The code block is missing a language specifier for proper syntax highlighting.

📝 Proposed fix
 For example, this is the code for GenderAssumptionBias validator.
 
-```
+```python
 from typing import ClassVar, List, Literal, Optional
 from app.models.base_validator_config import BaseValidatorConfig
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6df183 and 4c64be5.

📒 Files selected for processing (5)
  • .env.example
  • .env.test
  • backend/README.md
  • backend/app/core/config.py
  • backend/scripts/install_guardrails_from_hub.sh
🧰 Additional context used
🪛 dotenv-linter (4.0.0)
.env.test

[warning] 5-5: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


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

(UnorderedKey)


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

(UnorderedKey)


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

(UnorderedKey)


[warning] 29-29: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 30-30: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 30-30: [UnorderedKey] The GUARDRAILS_HUB_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 31-31: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 31-31: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 31-31: [UnorderedKey] The AUTH_TOKEN key should go before the GUARDRAILS_HUB_API_KEY key

(UnorderedKey)

.env.example

[warning] 5-5: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


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

(UnorderedKey)


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

(UnorderedKey)


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

(UnorderedKey)


[warning] 29-29: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 30-30: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 30-30: [UnorderedKey] The GUARDRAILS_HUB_API_KEY key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 31-31: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 31-31: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 31-31: [UnorderedKey] The AUTH_TOKEN key should go before the GUARDRAILS_HUB_API_KEY key

(UnorderedKey)

🪛 LanguageTool
backend/README.md

[grammar] ~44-~44: Use a hyphen to join words.
Context: ...ose watch ``` There is also a commented out command override, you can uncommen...

(QB_NEW_EN_HYPHEN)


[grammar] ~78-~78: Use a hyphen to join words.
Context: ...runs the live reloading server that auto reloads when it detects code changes. N...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
backend/README.md

19-19: Dollar signs used before commands without showing output

(MD014, commands-show-output)


25-25: Dollar signs used before commands without showing output

(MD014, commands-show-output)


41-41: Dollar signs used before commands without showing output

(MD014, commands-show-output)


49-49: Dollar signs used before commands without showing output

(MD014, commands-show-output)


55-55: Dollar signs used before commands without showing output

(MD014, commands-show-output)


69-69: Dollar signs used before commands without showing output

(MD014, commands-show-output)


89-89: Dollar signs used before commands without showing output

(MD014, commands-show-output)


125-125: Dollar signs used before commands without showing output

(MD014, commands-show-output)


133-133: Dollar signs used before commands without showing output

(MD014, commands-show-output)


141-141: Dollar signs used before commands without showing output

(MD014, commands-show-output)


153-153: Dollar signs used before commands without showing output

(MD014, commands-show-output)


161-161: Link text should be descriptive

(MD059, descriptive-link-text)


188-188: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


200-200: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


216-216: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


242-242: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


270-270: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


285-285: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Shellcheck (0.11.0)
backend/scripts/install_guardrails_from_hub.sh

[warning] 11-11: ShellCheck can't follow non-constant source. Use a directive to specify location.

(SC1090)


[warning] 40-40: Quote this to prevent word splitting.

(SC2046)


[warning] 41-41: Quote this to prevent word splitting.

(SC2046)

🔇 Additional comments (5)
backend/scripts/install_guardrails_from_hub.sh (1)

1-16: LGTM on script setup and env loading.

The strict mode settings (set -euo pipefail) and env file loading are well implemented. The ShellCheck SC1090 warning about non-constant source is expected here since the path is dynamically constructed but controlled.

backend/app/core/config.py (2)

43-61: LGTM on path constants.

Using ClassVar[Path] for resource file paths is appropriate. The relative path construction from __file__ ensures portability across different deployment locations.


63-73: LGTM on computed database URI.

The SQLALCHEMY_DATABASE_URI computed field correctly uses PostgresDsn.build() with the psycopg3 driver scheme.

backend/README.md (2)

1-301: Overall documentation quality is good with room for improvement.

The README provides comprehensive guidance for developers working on the backend. The structure is logical, covering setup, development workflows, testing, migrations, and Guardrails AI integration. Code examples are helpful and mostly accurate.

Key strengths:

  • Clear step-by-step instructions for common tasks
  • Helpful examples for both hub-based and custom validators
  • Good coverage of Docker-based development workflow
  • Includes VS Code integration guidance

Areas addressed in separate comments:

  • Missing Python version requirement
  • Inconsistencies in file path references
  • Security concern with API key exposure
  • Missing language specifiers on code blocks (affects syntax highlighting)
  • Step numbering error in custom validators section
  • Unclear test script usage scenarios

237-268: Fix step numbering.

Both steps in the custom validator section are numbered "2." The first occurrence should be "1."

📝 Proposed fix
 ## How to add custom validators?
 To add a custom validator to this project, follow the steps below.
 
 1. Create the custom validator class. Take a look at the `backend/app/core/validators/gender_assumption_bias.py` as an example. Each custom validator should contain an `__init__` and `_validator` method. For example,

And later:

-2. In the `backend/app/models` folder, create a new Python file called `<validator_name>_safety_validator_config.py`. Add the following code there:
+2. In the `backend/app/models` folder, create a new Python file called `<validator_name>_safety_validator_config.py`. Add the following code there:

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @backend/app/api/routes/guardrails.py:
- Around line 32-46: The UUID(payload.request_id) conversion in
run_output_guardrails has the same fragile handling as the /input/ endpoint;
replace the direct UUID(...) cast with the same safe parsing used in the input
handler (e.g., a shared helper like parse_request_id or the validated
uuid.UUID(payload.request_id) flow used there), and ensure you handle/propagate
invalid UUID errors consistently before calling RequestLogCrud.create and when
passing request_log.id to _validate_with_guard.
- Around line 64-73: The failure_response call is passing a dict to the error
parameter (causing a type mismatch); change it to pass a string (or list of
strings) instead by formatting the error details into a single descriptive
message using the caught exception e and the model name (e.g., build a string
like "internal_error: Failed to retrieve schema for validator {model.__name__}:
{e}") and pass that string to APIResponse.failure_response(error=...), leaving
the JSONResponse and status_code logic unchanged.
- Around line 16-30: run_input_guardrails currently calls RequestLogCrud.create
with UUID(payload.request_id) which will raise ValueError for bad strings;
handle this by validating/converting the request_id safely: either change
GuardrailInputRequest.request_id to type UUID to let Pydantic enforce it, or
wrap UUID(payload.request_id) in a try/except in run_input_guardrails and raise
an HTTPException(status_code=400, detail="invalid request_id") (or return a 400
response) before calling RequestLogCrud.create and _validate_with_guard;
reference run_input_guardrails, GuardrailInputRequest, UUID(payload.request_id),
RequestLogCrud.create, and _validate_with_guard when making the change.
- Around line 77-88: The function _validate_with_guard is declared async but
never awaits anything (guard.validate() is synchronous), so remove the async
keyword and make it a normal synchronous function (update its signature and
return type accordingly) and ensure any callers stop awaiting it; alternatively,
if you need asynchronous validation, replace build_guard/guard.validate with the
async variant (e.g., AsyncGuard from guardrails-ai) and await validate inside
_validate_with_guard — pick one approach and update callers and imports
consistently.
🧹 Nitpick comments (1)
backend/app/api/routes/guardrails.py (1)

123-137: Broad exception handling noted; add trailing newline.

The PR objectives acknowledge the generic exception handler as a future improvement. For now, the catch-all ensures the request log is always updated on failure, which is reasonable for initial implementation.

Add a trailing newline to satisfy the linter (W292).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c64be5 and f5bb4be.

📒 Files selected for processing (1)
  • backend/app/api/routes/guardrails.py
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/api/routes/guardrails.py (5)
backend/app/core/guardrail_controller.py (2)
  • build_guard (7-9)
  • get_validator_config_models (11-14)
backend/app/crud/request_log.py (5)
  • RequestLogCrud (8-57)
  • create (12-20)
  • get (56-57)
  • update_success (22-37)
  • update_error (39-54)
backend/app/models/guardrail_config.py (2)
  • GuardrailInputRequest (23-26)
  • GuardrailOutputRequest (28-31)
backend/app/models/request.py (1)
  • RequestLogUpdate (26-28)
backend/app/utils.py (3)
  • APIResponse (14-40)
  • failure_response (27-40)
  • success_response (21-24)
🪛 Ruff (0.14.10)
backend/app/api/routes/guardrails.py

137-137: No newline at end of file

Add trailing newline

(W292)

🔇 Additional comments (2)
backend/app/api/routes/guardrails.py (2)

1-14: LGTM - Imports and router setup are appropriate.

Minor formatting note: Line 11 has an extra space before RequestLogUpdate. The static analysis tool also flagged a missing trailing newline at line 137.


90-104: LGTM - Success handling logic is correct.

Using is not None check correctly distinguishes between valid empty output and actual failure. The response structure with response_id and dynamic response_field key is clean.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @backend/app/core/config.py:
- Around line 75-84: The _check_default_secret method contains a dead branch
checking self.ENVIRONMENT == "local"; update this to match the valid ENVIRONMENT
values defined on the model (e.g., use self.ENVIRONMENT in ("development",
"testing") to emit a warning during non-production local/dev-style runs and keep
raising ValueError for staging/production), ensuring you reference the
_check_default_secret method and the ENVIRONMENT field and keep the existing
warning/raise behavior and stacklevel.
- Around line 91-104: The env_files mapping in get_settings doesn't include the
"staging" key, so if ENVIRONMENT="staging" env_file becomes None; update the
env_files dict in get_settings to include "staging" (e.g., map "staging" to the
same file as "production" such as ROOT_DIR / ".env") so that
Settings(_env_file=env_file) always receives a valid path for staging.
- Around line 16-21: The parse_cors function currently returns JSON-array
strings (e.g., '["http://..."]') as raw strings; update parse_cors to detect
when v is a str starting with "[" and call json.loads(v) to parse it into a
list, validate that the result is a list[str] and return it, otherwise handle
the other cases by using isinstance(v, list) to return lists directly (and raise
ValueError for anything else); ensure json is imported and add minimal
validation that parsed elements are strings.
🧹 Nitpick comments (1)
backend/app/core/config.py (1)

33-33: Unusual default environment.

Defaulting ENVIRONMENT to "testing" is atypical—most applications default to "development" for local work. This could cause confusion or unintended behavior if the environment variable isn't explicitly set.

💡 Consider defaulting to "development"
-    ENVIRONMENT: Literal["development", "testing", "staging", "production"] = "testing"
+    ENVIRONMENT: Literal["development", "testing", "staging", "production"] = "development"
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5bb4be and 42f1817.

📒 Files selected for processing (1)
  • backend/app/core/config.py
🔇 Additional comments (4)
backend/app/core/config.py (4)

1-13: LGTM!

Imports are appropriate and well-organized for the configuration module.


63-73: LGTM!

The computed field correctly assembles the database URI from individual Postgres configuration fields using PostgresDsn.build.


43-61: LGTM!

The ClassVar paths are correctly computed relative to the module's location, ensuring they work regardless of the working directory.


106-106: No action needed. Test isolation is already properly implemented in conftest.py, which sets ENVIRONMENT="testing" before importing the app. This ensures the module-level settings instantiation uses the test configuration (.env.test) as intended.

Likely an incorrect or invalid review comment.

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

Adds a complete FastAPI backend: configuration, Docker and CI scripts, Alembic migrations, SQLModel data models and CRUD, guardrail validators (slur, bias, PII), API routes with auth and middleware, extensive tests, and developer documentation and tooling.

Changes

Cohort / File(s) Summary
Environment & Ignored Files
\.env.example, \.env.test, \.env.test.example, \.gitignore
Add example/test env files and common Python ignores.
Top-level Docs
README.md, development.md, deployment.md
Add comprehensive project, development, and deployment documentation.
Backend Project & Build
backend/pyproject.toml, backend/Dockerfile, backend/README.md, docker-compose.yml
Add Python project config, dependency list, Dockerfile, compose for multi-service dev/test.
Alembic / Migrations
backend/alembic.ini, backend/app/alembic/*, backend/app/alembic/versions/*
Add Alembic config, env script, migration template, and migrations to create request_log and validator_log tables.
App Bootstrap & Scripts
backend/scripts/*, backend/scripts/entrypoint.sh, backend/scripts/install_guardrails_from_hub.sh, backend/scripts/prestart.sh
Add entrypoint, guardrails installer, prestart orchestration and test-run scripts.
App Core & Config
backend/app/core/config.py, backend/app/core/db.py, backend/app/core/constants.py, backend/app/core/enum.py
New Pydantic settings, DB engine, constants and enums for validators.
Middleware & Exceptions
backend/app/core/middleware.py, backend/app/core/exception_handlers.py
Add request logging middleware and centralized exception handlers.
Guardrail Controller & Utils
backend/app/core/guardrail_controller.py, backend/app/utils.py
Add guard construction helpers and generic APIResponse/utility functions.
Validators & Manifest
backend/app/core/validators/lexical_slur.py, .../gender_assumption_bias.py, .../pii_remover.py, .../validators.json
Add lexical slur, gender-bias, and PII-removal validators plus validators manifest.
Models & Validator Configs
backend/app/models/*.py (request.py, validator_log.py, base_validator_config.py, lexical_slur_safety_validator_config.py, gender_assumption_bias_safety_validator_config.py, pii_remover_safety_validator_config.py, ban_list_safety_validator_config.py, guardrail_config.py, init.py)
Add SQLModel data models and typed validator configuration models with discriminated union.
CRUD & Startup Helpers
backend/app/crud/request_log.py, backend/app/crud/__init__.py, backend/app/backend_pre_start.py, backend/app/initial_data.py, backend/app/load_env.py
Add RequestLog CRUD, DB readiness checks, initial data seeding, and env loader.
API Routers & Dependencies
backend/app/api/deps.py, backend/app/api/main.py, backend/app/api/routes/utils.py, backend/app/api/routes/guardrails.py
Add session/auth dependencies, router wiring, health-check, and guardrails input/output/validator-list endpoints including request logging.
App Entry & Wiring
backend/app/main.py
Add FastAPI app initialization, Sentry init, middleware and exception handler registration, and router inclusion.
Tests & Test Infrastructure
backend/app/tests/*, backend/app/tests/validators/*, backend/app/tests_pre_start.py
Add test fixtures, unit and integration tests for guard construction, APIs, and validators; test prestart logic and mocks.
Dev / CI Helpers
scripts/*, hooks/post_gen_project.py, backend/app/tests/pytest.ini
Add build/deploy/test orchestration scripts and post-gen normalization hook, pytest config.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant FastAPI
    participant AuthDep
    participant RequestLogCrud
    participant build_guard
    participant Guard
    participant Validators
    participant DB

    Client->>FastAPI: POST /api/v1/guardrails/input (payload, auth)
    FastAPI->>AuthDep: verify_bearer_token()
    AuthDep-->>FastAPI: allow / deny
    alt allowed
        FastAPI->>RequestLogCrud: create(request_id, input_text)
        RequestLogCrud->>DB: INSERT request_log
        DB-->>RequestLogCrud: created record
        RequestLogCrud-->>FastAPI: request_log_id

        FastAPI->>build_guard: build_guard(validators)
        build_guard->>Validators: config.build() for each validator
        Validators-->>build_guard: validator instances
        build_guard->>Guard: Guard().use_many(validators)
        Guard-->>build_guard: configured guard
        build_guard-->>FastAPI: guard

        FastAPI->>Guard: guard.validate(input_text)
        Guard->>Validators: run validators
        Validators-->>Guard: results (pass/fail/fix)
        Guard-->>FastAPI: validation result

        alt success
            FastAPI->>RequestLogCrud: update(id, SUCCESS, response_text, response_id)
            RequestLogCrud->>DB: UPDATE request_log
            FastAPI-->>Client: {success:true, safe_input, response_id}
        else failure
            FastAPI->>RequestLogCrud: update(id, ERROR, error)
            RequestLogCrud->>DB: UPDATE request_log
            FastAPI-->>Client: {success:false, error, response_id}
        end
    else denied
        FastAPI-->>Client: 401 Unauthorized
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~110 minutes

Poem

🐰 New burrows stitched with careful thread,
Guardrails hop in, chasing dread,
Slurs are muted, biases made mild,
PII tucked safe, tests run wild,
I dig this patch — a hoppity build! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding Guardrails API endpoints, validator configurations, and test infrastructure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🤖 Fix all issues with AI agents
In `@backend/app/core/config.py`:
- Around line 96-101: The env_files mapping currently assigns the "staging" key
to ROOT_DIR / ".env.test" (in the env_files dict), which is incorrect; update
the "staging" entry to point to a production-like file (e.g., ROOT_DIR / ".env"
or create and reference ROOT_DIR / ".env.staging" if staging needs different
values) so staging no longer uses the test env file.

In `@backend/app/core/constants.py`:
- Around line 1-6: Add a trailing newline to the constants module file so the
file ends with a newline character; update backend.app.core.constants (e.g.,
near constants like BAN_LIST, LANG_HINDI, LANG_ENGLISH, LABEL, SCORE) to ensure
the file ends with a newline to satisfy static analysis and POSIX conventions.

In `@backend/app/core/middleware.py`:
- Around line 18-22: client_ip is computed but never used; either include it in
the access log or remove the dead assignment. Update the logger.info call in the
middleware to incorporate client_ip (e.g., add {client_ip} to the f-string) so
the computed value from request.client.host is actually logged alongside
request.method, request.url.path, response.status_code and process_time, or
delete the client_ip assignment if IP logging is not desired (adjust any
references to client_ip accordingly).

In `@backend/app/core/validators/pii_remover.py`:
- Line 58: The assignment to os.environ["TOKENIZERS_PARALLELISM"] = "false" is a
side effect in the PIIRemover.__init__ constructor; move that
environment-variable setting to module-level (top of
backend/app/core/validators/pii_remover.py) so it runs once on import and remove
the same line from the PIIRemover.__init__ method; keep references to
TOKENIZERS_PARALLELISM and PIIRemover to locate the change.

In `@backend/app/models/gender_assumption_bias_safety_validator_config.py`:
- Around line 7-9: The field 'categories' on
GenderAssumptionBiasSafetyValidatorConfig uses a mutable default list and
typing.List; change its annotation to Optional[list[BiasCategories]] and set the
default to None (categories: Optional[list[BiasCategories]] = None), then update
the class's build() method (or wherever instances are constructed) to treat None
as the intended default by assigning [BiasCategories.All] at
build/initialization time so each instance gets its own list; also replace any
other typing.List uses with built-in list in this class.

In `@backend/app/tests/conftest.py`:
- Around line 35-39: Store the original guardrails.RequestLogCrud before
assigning the lambda (e.g., original_request_log_crud =
guardrails.RequestLogCrud), assign the test double (guardrails.RequestLogCrud =
lambda session: mock_crud), then after the yield restore the original
(guardrails.RequestLogCrud = original_request_log_crud) in the same fixture so
the module attribute is reverted; keep the existing
app.dependency_overrides.clear() call as well.

In `@backend/README.md`:
- Line 302: Update the duplicate step number in the "How to add custom
validators?" list: change the second "2." to "3." so the steps read 1., 2., 3.;
locate the numbered list in the README section and adjust the markup so the
sequence is correct.

In `@backend/scripts/tests-start.sh`:
- Around line 7-9: Replace the inconsistent bare python invocation in the
tests-start script: change the command that runs tests_pre_start.py (currently
"python app/tests_pre_start.py") to use the same tool runner as the alembic step
(use "uv run python ...") so both lines use "uv run" (e.g., update the
invocation in backend/scripts/tests-start.sh that calls tests_pre_start.py to
"uv run python app/tests_pre_start.py").

In `@docker-compose.yml`:
- Around line 70-76: The healthcheck URL uses localhost:8000 while the container
runs Uvicorn on port 80 (see the command string "uv run uvicorn app.main:app
--host 0.0.0.0 --port 80 --reload"), causing internal health checks to fail;
either change the healthcheck test to hit
http://localhost:80/api/v1/utils/health-check/ or change the Uvicorn command to
bind to port 8000 (update the "--port" value) so internal port and healthcheck
URL match.

In `@README.md`:
- Around line 6-9: The CI workflow badge image in the README is missing alt
text; update the Markdown image tag that references
https://github.com/ProjectTech4DevAI/ai-platform/workflows/Continuous%20Integration/badge.svg
so it includes a descriptive alt attribute (e.g., change
![](https://.../badge.svg) to ![CI workflow status](https://.../badge.svg) or
similar) to improve accessibility for screen readers; ensure the alt text
clearly indicates it is the repository's continuous integration status badge.

In `@scripts/build.sh`:
- Around line 7-9: Replace the deprecated docker-compose invocation in
scripts/build.sh with the Docker Compose V2 syntax by changing the command from
"docker-compose" to "docker compose" (keep existing flags like "-f
docker-compose.yml build" intact); mirror the same change you applied in
build-push.sh so the script uses "docker compose -f docker-compose.yml build"
for future compatibility.

In `@scripts/deploy.sh`:
- Line 13: The deploy script calls the deprecated external tool
docker-auto-labels (invoked in scripts/deploy.sh) without checking or
documenting it; fix by either (a) adding installation/dependency notes and a
pinned entry in your project deps/docs that mentions its deprecation, (b)
replacing the call with a maintained alternative CLI/library for Docker Swarm
label management, or (c) adding a pre-flight check in scripts/deploy.sh (e.g.,
test for docker-auto-labels on PATH) that prints a clear error and exits if
missing; update the README/development docs accordingly to reflect the chosen
approach.

In `@scripts/test-local.sh`:
- Around line 8-11: Quote the command substitution when comparing the kernel
name (change if [ $(uname -s) = "Linux" ] to quote the expansion like if [
"$(uname -s)" = "Linux" ]) to avoid word-splitting, and remove or make the sudo
usage optional for the find/remove command (replace the hard-coded sudo before
find with either no sudo or guard the cleanup behind an opt-in flag or
environment variable) so cache cleanup won't fail in CI or for users without
sudo; update the echo/logging text if you add an opt-in flag to indicate the
operation is optional.

In `@scripts/test.sh`:
- Around line 8-11: Add a trap to ensure the final cleanup command always runs
even if the test fails: register a trap on EXIT that runs the existing docker
compose down -v --remove-orphans cleanup, then run docker compose up -d and
docker compose exec -T backend bash scripts/tests-start.sh "$@" as before;
ensure the trap is set before bringing containers up so the cleanup executes on
any exit and preserves the original exit code.
♻️ Duplicate comments (32)
deployment.md (1)

1-7: Add placeholder content or remove until ready.

The file contains only empty section headers. Per previous review feedback, consider adding a note indicating this documentation is coming soon, or defer adding this file until actual deployment instructions are available.

📝 Suggested placeholder content
 # FastAPI Project - Deployment
 
+> 🚧 **This documentation is under construction.** Deployment instructions will be added in an upcoming update.
+
 ## Preparation
 
 ## Deploy the FastAPI Project
 
 ## Continuous Deployment (CD)
.env.example (2)

1-31: Missing SECRET_KEY environment variable.

The PR objectives mention "SECRET_KEY must be set via env var," but it's absent from this template. Applications typically require SECRET_KEY for JWT signing or cryptographic operations.

➕ Suggested addition after line 9:
# Security
SECRET_KEY=<YOUR_SECRET_KEY_MINIMUM_32_CHARS>

15-16: Default database credentials encourage insecure practices.

Using postgres/postgres as defaults in example files risks users copying this configuration without changing credentials. Consider using placeholder values to make it explicit these must be replaced.

🔒 Proposed fix
-POSTGRES_USER=postgres
-POSTGRES_PASSWORD=postgres
+POSTGRES_USER=<YOUR_POSTGRES_USER>
+POSTGRES_PASSWORD=<YOUR_POSTGRES_PASSWORD>
.env.test (1)

1-31: Consider renaming to .env.test.example or use placeholder values throughout.

This file appears to be a template but is named .env.test, which typically implies an actual configuration file. As noted in a previous review comment, if this is meant to be a template that developers copy and customize, it should be named .env.test.example for consistency with .env.example patterns.

Additionally, the previously flagged issues remain:

  • Missing SECRET_KEY (mentioned in PR objectives as required)
  • Hardcoded postgres/postgres credentials at lines 15-16
  • Missing trailing newline
backend/app/models/validator_log.py (2)

12-14: Specify nullability for input, output, and error fields.

These fields are typed as str without explicit Field() configuration. SQLAlchemy will create NOT NULL columns, but Python type hints don't enforce this at runtime. If any of these fields are intended to be optional, this mismatch will cause database insert errors.

🔧 Proposed fix if fields should be nullable
-    input: str
-    output: str
-    error: str
+    input: str | None = Field(default=None)
+    output: str | None = Field(default=None)
+    error: str | None = Field(default=None)

Or if required, add explicit Field() to make intent clear:

-    input: str
-    output: str
-    error: str
+    input: str = Field(...)
+    output: str = Field(...)
+    error: str = Field(...)

16-16: updated_at won't auto-refresh on UPDATE operations.

The default_factory=now only fires on INSERT. Subsequent updates to the row will leave updated_at unchanged, making it identical to inserted_at.

🔧 Proposed fix to auto-update timestamp
-    updated_at: datetime = Field(default_factory=now, nullable=False)
+    updated_at: datetime = Field(
+        default_factory=now,
+        sa_column_kwargs={"onupdate": now},
+        nullable=False
+    )
backend/README.md (3)

128-128: Fix inconsistent reference to models location.

The documentation references ./backend/app/models.py (a single file), but the project uses a models/ directory structure as confirmed by the PR changes.

📝 Proposed fix
-* Alembic is already configured to import your SQLModel models from `./backend/app/models.py`.
+* Alembic is already configured to import your SQLModel models from `./backend/app/models/`.

161-161: Use descriptive link text for accessibility.

The link text "here" is not descriptive. Per accessibility best practices, link text should describe the destination.

📝 Proposed fix
-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).
+1. Ensure that the .env file contains the correct value for `GUARDRAILS_HUB_API_KEY`. The key can be fetched from the [Guardrails AI Keys page](https://hub.guardrailsai.com/keys).

183-183: Remove partial API key from documentation.

Line 183 shows what appears to be a partial API key ([HBPo]). Even partial keys should not be included in documentation.

📝 Proposed fix
-Enter API Key below leave empty if you want to keep existing token [HBPo]
+Enter API Key below (leave empty to keep existing token):
backend/app/models/request.py (1)

25-26: Add automatic update mechanism for updated_at.

The updated_at field uses default_factory=now which only sets the timestamp on insert. It won't automatically update when the record is modified. SQLModel/SQLAlchemy requires explicit configuration for auto-updating timestamps.

📝 Proposed fix
-    updated_at: datetime = Field(default_factory=now, nullable=False)
+    updated_at: datetime = Field(default_factory=now, sa_column_kwargs={"onupdate": now}, nullable=False)
backend/app/alembic/versions/001_added_request_log.py (2)

27-27: Critical: Enum value case mismatch with model definition.

The migration defines the enum with uppercase values 'PROCESSING', 'SUCCESS', 'ERROR', 'WARNING', but the RequestStatus model in backend/app/models/request.py uses lowercase values "processing", "success", "error", "warning". This will cause database insert failures when the application attempts to insert records.

📝 Proposed fix
-    sa.Column('status', sa.Enum('PROCESSING','SUCCESS', 'ERROR', 'WARNING', name='requeststatus'), nullable=False, default='PROCESSING'),
+    sa.Column('status', sa.Enum('processing', 'success', 'error', 'warning', name='requeststatus'), nullable=False, server_default='processing'),

36-38: Complete the downgrade by dropping the enum type.

The TODO indicates the enum type should be dropped during downgrade. In PostgreSQL, custom enum types persist after dropping the table and must be explicitly removed.

📝 Proposed fix
 def downgrade() -> None:
     op.drop_table('request_log')
-    # todo : drop requeststatus enum type
+    op.execute("DROP TYPE IF EXISTS requeststatus")
backend/app/alembic/versions/002_added_validator_log.py (1)

28-28: Make error column nullable.

The error column is defined as nullable=False, but successful validations won't have error messages. This forces storing empty strings or placeholder values for successful validations, which is poor data modeling.

📝 Proposed fix
-    sa.Column('error', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+    sa.Column('error', sqlmodel.sql.sqltypes.AutoString(), nullable=True),

Note: The corresponding model in backend/app/models/validator_log.py should also be updated to use Optional[str] for the error field.

backend/app/models/__init__.py (1)

1-4: Add trailing newline.

The file is missing a newline at the end, as flagged by static analysis.

📝 Proposed fix
 from sqlmodel import SQLModel

 from app.models.request import RequestLog
 from app.models.validator_log import ValidatorLog
+
scripts/build-push.sh (1)

9-9: Update to Docker Compose V2 syntax.

docker-compose (V1) is deprecated and no longer receiving updates. Use docker compose (V2) instead.

Suggested fix
-docker-compose -f docker-compose.yml push
+docker compose -f docker-compose.yml push
backend/app/core/exception_handlers.py (1)

26-33: Security: Avoid leaking internal exception details to clients.

Exposing str(exc) directly in the response can leak sensitive information (internal paths, database details, stack traces) to clients. Log the full exception server-side and return a generic message.

🔒 Proposed fix
+import logging
+
+logger = logging.getLogger(__name__)
+
 def register_exception_handlers(app: FastAPI):
     # ... other handlers ...

     `@app.exception_handler`(Exception)
     async def generic_error_handler(request: Request, exc: Exception):
+        logger.exception("Unhandled exception: %s", exc)
         return JSONResponse(
             status_code=HTTP_500_INTERNAL_SERVER_ERROR,
             content=APIResponse.failure_response(
-                str(exc) or "An unexpected error occurred."
+                "An unexpected error occurred."
             ).model_dump(),
         )
backend/app/tests/test_guardrails_api_integration.py (2)

52-93: Case handling inconsistency in lexical slur tests.

The test at line 71 expects lowercased output "this sentence contains [REDACTED_SLUR]" when a slur is detected, but line 93 expects original case "This is a completely safe sentence" when text is clean. This suggests the validator has inconsistent case handling behavior—lowercasing only when violations are found.

Verify this is the intended behavior. If so, this inconsistency may confuse users expecting consistent output formatting.

#!/bin/bash
# Verify current behavior of LexicalSlur validator regarding case handling
cat -n backend/app/core/validators/lexical_slur.py | head -80

170-172: Add trailing newline at end of file.

Per Ruff W292 and coding conventions.

♻️ Proposed fix
     assert body["success"] is False
     assert "chakki" in body["error"]
+
backend/app/tests/conftest.py (1)

48-52: Duplicate fixtures.

The client and integration_client fixtures are identical. This was previously flagged. Consider removing the duplication by using a single fixture or creating an alias.

backend/app/alembic/env.py (1)

41-47: Offline mode uses placeholder URL instead of application settings.

run_migrations_offline() calls config.get_main_option("sqlalchemy.url") which reads the placeholder from alembic.ini, while run_migrations_online() correctly uses get_url(). This inconsistency will cause offline migrations to fail or generate incorrect SQL.

🐛 Previously proposed fix
 def run_migrations_offline() -> None:
-    url = config.get_main_option("sqlalchemy.url")
+    url = get_url()
     context.configure(
         url=url,
         target_metadata=target_metadata,
         literal_binds=True,
         dialect_opts={"paramstyle": "named"},
     )
backend/app/core/validators/pii_remover.py (1)

84-87: threshold parameter is accepted but never used.

The threshold parameter is stored in self.threshold but not passed to self.analyzer.analyze(). Without using score_threshold, the confidence threshold configuration has no effect on PII detection.

🐛 Previously proposed fix
     def _validate(self, value: str, metadata: dict = None) -> ValidationResult:
         text = value
-        results = self.analyzer.analyze(text=text, entities=self.entity_types, language="en")
+        results = self.analyzer.analyze(
+            text=text,
+            entities=self.entity_types,
+            language="en",
+            score_threshold=self.threshold
+        )
         anonymized = self.anonymizer.anonymize(text=text, analyzer_results=results)
backend/app/utils.py (1)

6-7: Avoid configuring logging in library modules.

logging.basicConfig() in a utility module overrides the application's logging configuration. This should be handled at the application entry point (e.g., in main.py).

🔧 Suggested fix
-logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
backend/app/core/validators/lexical_slur.py (1)

108-115: Clarify severity filtering logic - naming is counterintuitive.

The current implementation treats severity as a "threshold" where SlurSeverity.Low catches all severities (L, M, H), but the enum naming suggests it should filter only low-severity slurs. This could confuse API consumers.

Consider either:

  1. Renaming enum values to reflect threshold semantics (e.g., LowThreshold, MediumThreshold, HighThreshold)
  2. Adding clear documentation in the validator's docstring
  3. Inverting the logic so Low returns only low-severity slurs
backend/app/api/deps.py (1)

30-36: Use constant-time comparison for token validation to prevent timing attacks.

Direct string comparison with != is vulnerable to timing attacks, where an attacker can infer the correct token character-by-character based on response times. Use secrets.compare_digest() for secure comparison.

🔒 Apply this fix

Add the import at the top of the file:

+import secrets
 from collections.abc import Generator

Then update the comparison:

     token = credentials.credentials

-    if token != settings.AUTH_TOKEN:
+    if not secrets.compare_digest(token, settings.AUTH_TOKEN):
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail="Invalid token",
         )
backend/app/api/routes/guardrails.py (4)

72-83: Remove async keyword since the function contains no await operations.

_validate_with_guard is declared async but never awaits anything—guard.validate() is synchronous. Either make this function synchronous or use guardrails-ai's AsyncGuard if async validation is intended.

🔧 Proposed fix
-async def _validate_with_guard(
+def _validate_with_guard(
     data: str,
     validators: list,
     response_field: str,  # "safe_input" or "safe_output"
     request_log_crud: RequestLogCrud,
     request_log_id: UUID
 ) -> APIResponse:

Then update callers to remove await:

-    return await _validate_with_guard(
+    return _validate_with_guard(

16-30: Potential ValueError from UUID conversion.

If payload.request_id is not a valid UUID string, UUID(payload.request_id) on line 23 will raise an unhandled ValueError. Since GuardrailInputRequest.request_id is typed as str, Pydantic won't validate the UUID format automatically.

🔧 Option 1: Change type in model (preferred)

Change request_id type to UUID in GuardrailInputRequest for automatic validation.

🔧 Option 2: Handle invalid UUID gracefully
+    try:
+        request_uuid = UUID(payload.request_id)
+    except ValueError:
+        return APIResponse.failure_response(
+            error=f"Invalid UUID format: {payload.request_id}"
+        )
     request_log_crud = RequestLogCrud(session=session)
-    request_log = request_log_crud.create(request_id=UUID(payload.request_id), input_text=payload.input)
+    request_log = request_log_crud.create(request_id=request_uuid, input_text=payload.input)

32-46: Same UUID conversion issue applies here.

The UUID(payload.request_id) concern from the /input/ endpoint applies to line 39 as well.


102-102: Critical: result.failures does not exist on ValidationOutcome.

According to the Guardrails AI library, ValidationOutcome does not have a failures attribute. This will raise AttributeError at runtime when validation fails. Use result.failed_validations (list of ValidatorLogs) or result.reask.fail_results instead.

guardrails-ai ValidationOutcome failed_validations attribute 2024
backend/app/core/validators/gender_assumption_bias.py (1)

71-83: Potential duplicate entries when BiasCategories.All is combined with other categories.

If categories contains BiasCategories.All alongside other specific categories (e.g., [BiasCategories.All, BiasCategories.Healthcare]), all entries are added multiple times. This causes redundant processing.

🔎 Proposed fix: early return for All
     def load_gender_bias_list(self, categories):
         file_path = Settings.GENDER_BIAS_LIST_FILEPATH
         neutral_term_col = 'neutral-term'
-        gender_bias_list = []

         try:
             df = pandas.read_csv(file_path)
         except FileNotFoundError:
             raise FileNotFoundError(f"Gender bias file not found at {file_path}")
         except Exception as e:
             raise ValueError(f"Failed to load gender bias list from {file_path}: {e}")

         df['word'] = df['word'].str.lower()
         df[neutral_term_col] = df[neutral_term_col].str.lower()

+        # If All is present, just return all entries
+        if BiasCategories.All in categories:
+            return df[['word', neutral_term_col]].rename(
+                columns={neutral_term_col: 'neutral-term'}
+            ).to_dict(orient='records')
+
+        gender_bias_list = []
         for category in categories:
-            if category == BiasCategories.All:
-                temp = df
-            else:
-                temp = df[df['type'] == category.value]
-
+            temp = df[df['type'] == category.value]
             rows = temp.to_dict(orient="records")
             for row in rows:
                 gender_bias_list.append({
                     "word": row["word"],
                     neutral_term_col: row[neutral_term_col]
                 })
-        return gender_bias_list
+        return gender_bias_list
backend/app/core/config.py (2)

16-21: JSON array strings are returned unparsed.

When v is a string starting with "[" (e.g., '["http://localhost:3000"]'), it bypasses the comma-split logic and is returned as a raw string instead of being parsed into a list.

🔧 Proposed fix to parse JSON array strings
+import json
+
 def parse_cors(v: Any) -> list[str] | str:
-    if isinstance(v, str) and not v.startswith("["):
-        return [i.strip() for i in v.split(",") if i.strip()]
-    elif isinstance(v, list | str):
+    if isinstance(v, list):
         return v
+    if isinstance(v, str):
+        if v.startswith("["):
+            return json.loads(v)
+        return [i.strip() for i in v.split(",") if i.strip()]
     raise ValueError(v)

40-41: Empty password defaults pose security risks.

POSTGRES_PASSWORD and POSTGRES_DB default to empty strings, which could allow connections to proceed with no password in certain configurations. The _check_default_secret validator only checks for "changethis", not empty strings.

🔎 Consider making these required or validating empty values:
-    POSTGRES_PASSWORD: str = ""
-    POSTGRES_DB: str = ""
+    POSTGRES_PASSWORD: str
+    POSTGRES_DB: str

Or update _check_default_secret to also reject empty strings.

backend/pyproject.toml (1)

26-31: Pin ML/NLP dependencies to tested versions.

emoji, ftfy, transformers, and torch are unpinned, which poses reproducibility and compatibility risks. Torch and transformers have documented compatibility issues across versions.

🔧 Proposed fix
     "guardrails-ai>=0.7.2",
-    "emoji",
-    "ftfy",
+    "emoji>=2.0.0,<3.0.0",
+    "ftfy>=6.0.0,<7.0.0",
     "presidio_analyzer>=2.2.360",
     "presidio_anonymizer>=2.2.360",
-    "transformers",
-    "torch",
+    "transformers>=4.30.0,<5.0.0",
+    "torch>=2.0.0,<3.0.0",

Adjust version ranges based on your tested/validated versions.

🧹 Nitpick comments (47)
.gitignore (1)

1-11: Good coverage of standard Python patterns.

The ignore patterns appropriately cover common Python artifacts and environment files. Consider adding a trailing newline and optionally these common patterns:

💡 Optional: Additional common patterns
 __pycache__
 app.egg-info
 *.pyc
 .mypy_cache
 .coverage
 htmlcov
 .cache
 .venv
 .DS_Store
 .env
 .env.test
+.pytest_cache
+*.egg
+dist/
+build/
+.idea/
+.vscode/
+
backend/scripts/entrypoint.sh (1)

7-8: Consider making worker count configurable.

Hardcoding 4 workers may not suit all deployment environments (dev vs. prod, different resource allocations). Consider using an environment variable with a default.

♻️ Suggested improvement
 echo "Starting FastAPI server..."
-exec fastapi run --workers 4 app/main.py
+exec fastapi run --workers "${WORKERS:-4}" app/main.py
hooks/post_gen_project.py (1)

4-8: Remove unused standalone type annotation.

The path: Path on line 4 is a standalone variable annotation that doesn't affect the loop variable. This is non-idiomatic Python. The loop variable is already correctly typed by the glob() return.

♻️ Cleaner implementation
 from pathlib import Path
 
 
-path: Path
 for path in Path(".").glob("**/*.sh"):
     data = path.read_bytes()
     lf_data = data.replace(b"\r\n", b"\n")
     path.write_bytes(lf_data)
.env.example (1)

31-31: Add trailing newline at end of file.

🔧 Fix
 AUTH_TOKEN="<ADD-TOKEN>"
+
backend/app/load_env.py (1)

5-13: Environment file loading may silently fail if file is missing.

load_dotenv() returns False when the file doesn't exist but doesn't raise an exception. This could lead to hard-to-debug issues where the application runs with missing configuration.

Consider adding validation or logging:

♻️ Suggested improvement
+from pathlib import Path
+import logging
+
+logger = logging.getLogger(__name__)
+
 def load_environment():
     env = os.getenv("ENVIRONMENT", "development")
 
     # Use the same path as config.py expects (one level above backend/)
     env_file = "../.env"
     if env == "testing":
         env_file = "../.env.test"
 
-    load_dotenv(env_file)
+    env_path = Path(__file__).parent.parent.parent / env_file.lstrip("../")
+    if not env_path.exists():
+        logger.warning(f"Environment file not found: {env_path}")
+    load_dotenv(env_path)
+
backend/scripts/install_guardrails_from_hub.sh (1)

37-40: Quote command substitutions to prevent word splitting.

The conditional expressions produce multiple words that should be quoted. While this works for the current --enable-metrics/--disable-metrics flags, unquoted command substitution can cause issues if the output contains spaces or special characters.

♻️ Proposed fix
 guardrails configure \
   --token "$GUARDRAILS_HUB_API_KEY" \
-  $( [[ "$ENABLE_METRICS" == "true" ]] && echo "--enable-metrics" || echo "--disable-metrics" ) \
-  $( [[ "$ENABLE_REMOTE_INFERENCING" == "true" ]] && echo "--enable-remote-inferencing" || echo "--disable-remote-inferencing" )
+  "$( [[ "$ENABLE_METRICS" == "true" ]] && echo "--enable-metrics" || echo "--disable-metrics" )" \
+  "$( [[ "$ENABLE_REMOTE_INFERENCING" == "true" ]] && echo "--enable-remote-inferencing" || echo "--disable-remote-inferencing" )"
backend/README.md (1)

200-212: Add language specifiers to fenced code blocks.

Several code blocks (lines 200, 216, 242, 270, 285) are missing language specifiers. This affects syntax highlighting and markdown linting compliance.

📝 Example fix for line 200
-```
+```python
 from guardrails.hub import # validator name from Guardrails Hub
backend/app/models/request.py (1)

29-31: Add trailing newline.

The file is missing a newline at the end, as flagged by static analysis.

📝 Proposed fix
 class RequestLogUpdate(SQLModel):
     response_text: str
     response_id: UUID
+
backend/app/crud/request_log.py (2)

1-1: Remove unused uuid4 import.

uuid4 is imported but never used in this module.

Proposed fix
-from uuid import UUID, uuid4
+from uuid import UUID

39-40: Add trailing newline.

Per static analysis hint (W292), add a newline at end of file.

Proposed fix
     def get(self, request_log_id: UUID) -> RequestLog | None:
         return self.session.get(RequestLog, request_log_id)
+
backend/app/crud/__init__.py (1)

1-1: Consider adding __all__ and trailing newline.

The export is correct. For explicit public API definition, consider adding __all__. Also, add a trailing newline per static analysis (W292).

Proposed fix
 from app.crud.request_log import RequestLogCrud
+
+__all__ = ["RequestLogCrud"]
backend/app/models/lexical_slur_safety_validator_config.py (1)

1-1: Use built-in list instead of typing.List.

typing.List is deprecated in Python 3.9+. Use the built-in list type hint instead.

Proposed fix
-from typing import List, Literal
+from typing import Literal
 
 from app.core.enum import SlurSeverity
 from app.core.validators.lexical_slur import LexicalSlur
 from app.models.base_validator_config import BaseValidatorConfig
 
 class LexicalSlurSafetyValidatorConfig(BaseValidatorConfig):
     type: Literal["uli_slur_match"]
-    languages: List[str] = ["en", "hi"] # list of languages to check slurs in
+    languages: list[str] = ["en", "hi"] # list of languages to check slurs in
     severity: Literal["low", "medium", "high", "all"] = "all" # severity level of slurs to check
backend/app/models/base_validator_config.py (1)

25-28: Add trailing newline and consider adding return type hint to resolve_on_fail.

The abstract build() method correctly enforces the implementation contract for subclasses. Per static analysis (W292), add a trailing newline. Also, resolve_on_fail() could benefit from a return type annotation for consistency.

Proposed fix
-    def resolve_on_fail(self):
+    def resolve_on_fail(self) -> Optional[OnFailAction]:
         if self.on_fail is None:
             return None
 
         try:
             return OnFailAction[self.on_fail.upper()]
         except KeyError:
             raise ValueError(
                 f"Invalid on_fail value: {self.on_fail}. "
                 "Expected one of: exception, fix, noop, reask"
-            )            
+            )
 
     def build(self) -> Any:
         raise NotImplementedError(
             f"{self.__class__.__name__} must implement build()"
         )
+
backend/app/models/ban_list_safety_validator_config.py (1)

1-15: Minor style improvements available.

Two minor issues flagged by static analysis:

  1. typing.List is deprecated in Python 3.9+; use the built-in list instead.
  2. Missing trailing newline at end of file.
Suggested diff
-from typing import List, Literal
+from typing import Literal

 from guardrails.hub import BanList

 from app.models.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: list[str]  # list of banned words to be redacted

     def build(self):
         return BanList(
             banned_words=self.banned_words,
             on_fail=self.resolve_on_fail(),
         )
+
backend/app/api/main.py (1)

1-10: LGTM with a minor note on commented code.

The router composition is clean and follows FastAPI best practices for modularizing routes.

The commented code (lines 9-10) references settings and private which aren't imported—if uncommented without adding the imports, it would fail. Consider either removing the commented code or adding a TODO comment indicating what imports are needed.

scripts/test-local.sh (1)

13-15: Consider adding cleanup on test failure.

The script builds and starts containers but doesn't tear them down after tests complete (success or failure). This could leave containers running if developers interrupt the script or if they expect cleanup after test runs.

Proposed fix with cleanup trap
 set -e
+
+cleanup() {
+    docker-compose down -v --remove-orphans
+}
+trap cleanup EXIT

 docker-compose down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
backend/app/main.py (1)

28-29: Middleware ordering may affect correlation ID in logs.

Middleware executes in reverse registration order for responses. With the current order, CorrelationIdMiddleware is added after http_request_logger, meaning the correlation ID may not be available when logging requests.

Consider swapping the order so CorrelationIdMiddleware runs first (registered last) to ensure the correlation ID is set before the logger runs.

Proposed fix
-app.middleware("http")(http_request_logger)
 app.add_middleware(CorrelationIdMiddleware)
+app.middleware("http")(http_request_logger)
README.md (2)

22-32: Consider adding instructions for obtaining the GUARDRAILS_HUB_API_KEY.

The configuration section mentions updating .env but doesn't explain how to obtain the GUARDRAILS_HUB_API_KEY from Guardrails Hub, which is likely required for validators to function. This was previously noted in review comments.


82-84: Minor style: reduce exclamation marks.

Consider using a single exclamation mark for a more professional tone.

Proposed fix
-This project was created using [full-stack-fastapi-template](https://github.com/fastapi/full-stack-fastapi-template). A big thank you to the team for creating and maintaining the template!!!
+This project was created using [full-stack-fastapi-template](https://github.com/fastapi/full-stack-fastapi-template). A big thank you to the team for creating and maintaining the template!
scripts/deploy.sh (1)

6-8: Minor: Redundant environment variable validation.

STACK_NAME is validated on line 7 but checked again on line 15. The second check is unnecessary since set -e would have already terminated the script if it was unset.

♻️ Suggested fix
-docker stack deploy -c docker-stack.yml --with-registry-auth "${STACK_NAME?Variable not set}"
+docker stack deploy -c docker-stack.yml --with-registry-auth "${STACK_NAME}"

Also applies to: 15-15

backend/app/initial_data.py (1)

7-8: Module-level basicConfig may override application logging.

Calling logging.basicConfig() at module import time can interfere with the application's logging configuration if this module is imported elsewhere (not just run as a script). Consider moving the logging setup inside main() or using a conditional.

♻️ Suggested fix
 import logging
 
 from sqlmodel import Session
 
 from app.core.db import engine, init_db
 
-logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
 
 
 def init() -> None:
     with Session(engine) as session:
         init_db(session)
 
 
 def main() -> None:
+    logging.basicConfig(level=logging.INFO)
     logger.info("Creating initial data")
     init()
     logger.info("Initial data created")
backend/scripts/tests-start.sh (1)

9-13: Redundant exit status check with set -e.

The explicit check on lines 10-13 is unreachable. With set -e enabled (line 2), the script exits immediately if alembic upgrade head returns a non-zero status—the if [ $? -ne 0 ] block never executes.

Either remove set -e (not recommended) or remove the redundant check.

♻️ Suggested simplification
-uv run alembic upgrade head
-if [ $? -ne 0 ]; then
-    echo 'Error: Test database migrations failed'
-    exit 1
-fi
+uv run alembic upgrade head || { echo 'Error: Test database migrations failed'; exit 1; }

Or simply rely on set -e:

 uv run alembic upgrade head
-if [ $? -ne 0 ]; then
-    echo 'Error: Test database migrations failed'
-    exit 1
-fi
backend/app/tests/validators/test_lexical_slurs.py (2)

86-102: Redundant fixture: patch_slur_load is immediately overwritten.

The patch_slur_load fixture patches LexicalSlur.load_slur_list, but then lines 95-99 immediately overwrite it with another monkeypatch. Either remove patch_slur_load from the function signature or remove the inline monkeypatch.

♻️ Proposed fix: Remove redundant fixture
-def test_severity_low_includes_all(patch_slur_load, monkeypatch, slur_csv):
+def test_severity_low_includes_all(monkeypatch, slur_csv):
     """Low severity = L + M + H."""
     df = pd.DataFrame({
         "label": ["lowone", "mediumone", "highone"],
         "severity": ["L", "M", "H"]
     })

The same pattern applies to test_severity_medium_includes_m_and_h and test_severity_high_includes_only_h.


138-138: Add trailing newline at end of file.

♻️ Proposed fix
     validator = LexicalSlur(severity=SlurSeverity.High)
     assert validator.slur_list == ["highone"]
+
backend/Dockerfile (2)

1-1: Consider using python:3.10-slim for smaller image size.

The full python:3.10 image includes many packages not needed at runtime. Using python:3.10-slim can significantly reduce the final image size.

♻️ Proposed change
-FROM python:3.10
+FROM python:3.10-slim

9-9: Update uv from 0.5.11 to the latest version.

The pinned uv version 0.5.11 is significantly outdated. As of January 2026, the latest version is 0.9.25 on PyPI (0.9.21 on GitHub), representing multiple minor version updates with bug fixes and performance improvements.

Original snippet
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/
backend/app/tests/test_guardrails_api.py (2)

82-84: Inconsistent trailing slash in output endpoint URL.

The output endpoint URL at line 83 is missing a trailing slash (/api/v1/guardrails/output) while all other endpoint URLs in this file use trailing slashes (e.g., /api/v1/guardrails/input/). This inconsistency could cause test brittleness if the route normalization changes.

♻️ Suggested fix
     with patch(build_guard_path, return_value=MockGuard()):
         response = client.post(
-            "/api/v1/guardrails/output",
+            "/api/v1/guardrails/output/",
             json={

98-114: LGTM - Internal error handling test is comprehensive.

The test correctly verifies that exceptions during guard building are caught and returned as a structured error response with success=False.

Add trailing newline.

Minor: Missing newline at end of file.

♻️ Add trailing newline
     assert body["success"] is False
     assert body["data"]["safe_input"] is None
     assert "Invalid validator config" in body["error"]
+
development.md (1)

97-141: Minor: Capitalize sentence beginnings.

Lines 99 and 130 start sentences with lowercase letters.

📝 Suggested fix
 ## Pre-commits and code linting

-we are using a tool called [pre-commit](https://pre-commit.com/) for code linting and formatting.
+We are using a tool called [pre-commit](https://pre-commit.com/) for code linting and formatting.
 #### Running pre-commit hooks manually

-you can also run `pre-commit` manually on all the files, you can do it using `uv` with:
+You can also run `pre-commit` manually on all the files, you can do it using `uv` with:
backend/app/core/guardrail_controller.py (2)

7-9: Add type hints for clarity and IDE support.

The function parameters and return type lack annotations, which reduces discoverability and type safety.

♻️ Suggested improvement
+from typing import List
+from app.models.guardrail_config import ValidatorConfigItem
+
-def build_guard(validator_items):
+def build_guard(validator_items: List[ValidatorConfigItem]) -> Guard:
     validators = [v_item.build() for v_item in validator_items]
     return Guard().use_many(*validators)

11-14: Fragile type introspection assumes specific annotation structure.

This function assumes ValidatorConfigItem is an Annotated[Union[...], ...] type and directly indexes into get_args results. If the type definition changes, this will silently return incorrect results or fail.

Consider adding defensive checks or documentation explaining the expected structure.

♻️ Suggested improvement with defensive check
 def get_validator_config_models():
     annotated_args = get_args(ValidatorConfigItem)
+    if not annotated_args:
+        raise ValueError("ValidatorConfigItem must be an Annotated type")
     union_type = annotated_args[0]
-    return get_args(union_type)
+    inner_types = get_args(union_type)
+    if not inner_types:
+        raise ValueError("ValidatorConfigItem must contain a Union type")
+    return inner_types
+
backend/app/tests/validators/test_pii_remover.py (1)

85-109: LGTM - Indian recognizer registration test is comprehensive.

The test verifies that when Indian entity types are requested, the corresponding recognizers are registered.

Add trailing newline.

♻️ Add trailing newline
         # Called once per recognizer
         assert registry.add_recognizer.call_count == 5
+
backend/app/tests_pre_start.py (2)

8-8: Module-level engine import may cause side effects.

Importing engine at module level means it will be instantiated when this module is imported, regardless of whether main() is called or the ENVIRONMENT is "testing". If this script is imported elsewhere (e.g., during test collection), it could attempt DB connection prematurely.

Consider lazy-loading the engine inside main() or guarding the import:

def main() -> None:
    if ENVIRONMENT == "testing":
        logger.info("Skipping DB pre-start checks in testing environment")
        return
    
    from app.core.db import engine  # Lazy import
    logger.info("Initializing service")
    init(engine)
    logger.info("Service finished initializing")

18-23: Use logging.WARNING instead of deprecated logging.WARN.

logging.WARN is a deprecated alias for logging.WARNING.

♻️ Proposed fix
 `@retry`(
     stop=stop_after_attempt(max_tries),
     wait=wait_fixed(wait_seconds),
     before=before_log(logger, logging.INFO),
-    after=after_log(logger, logging.WARN),
+    after=after_log(logger, logging.WARNING),
 )
backend/app/core/db.py (2)

1-1: Remove unused import select.

The select import is not used in this module.

♻️ Proposed fix
-from sqlmodel import Session, create_engine, select
+from sqlmodel import Session, create_engine

13-22: Add trailing newline at end of file.

Per Ruff W292 and coding conventions.

♻️ Proposed fix
     # This works because the models are already imported and registered from app.models
     # SQLModel.metadata.create_all(engine)

     pass
+
backend/app/tests/test_build_guard.py (1)

51-71: Add trailing newline and consider test isolation improvement.

Good coverage for error propagation scenarios. Add trailing newline per Ruff W292.

♻️ Proposed fix
     with patch(
         "app.core.guardrail_controller.Guard",
         return_value=mock_guard,
     ):
         with pytest.raises(RuntimeError, match="guard failure"):
             build_guard([cfg])
+
backend/app/models/pii_remover_safety_validator_config.py (2)

2-2: Use built-in list instead of deprecated typing.List.

Since Python 3.9+, list can be used directly in type hints. This is flagged by Ruff UP035.

♻️ Proposed fix
 from __future__ import annotations
-from typing import List, Literal, Optional
+from typing import Literal, Optional
 
 from app.models.base_validator_config import BaseValidatorConfig
 from app.core.validators.pii_remover import PIIRemover
 
 class PIIRemoverSafetyValidatorConfig(BaseValidatorConfig):
     type: Literal["pii_remover"]
-    entity_types: Optional[List[str]] = None # list of PII entity types to remove
+    entity_types: Optional[list[str]] = None # list of PII entity types to remove
     threshold: float = 0.5 # confidence threshold for PII detection

10-10: Add threshold validation using Pydantic Field constraints.

The threshold field accepts any float, but semantically it should be bounded (e.g., 0.0 to 1.0 for a confidence threshold). Add Field constraints to enforce valid values at the model level:

Suggested change
from sqlmodel import Field

threshold: float = Field(default=0.5, ge=0.0, le=1.0)
backend/app/models/guardrail_config.py (2)

23-31: Consider using Pydantic BaseModel for request DTOs.

GuardrailInputRequest and GuardrailOutputRequest are pure request DTOs with no database backing (table=True is absent). Using SQLModel here works but adds unnecessary coupling to SQLAlchemy. Pydantic's BaseModel would be more semantically appropriate and lighter-weight for API request/response models.

♻️ Suggested refactor
-from sqlmodel import Field, SQLModel
+from pydantic import BaseModel, Field

 ValidatorConfigItem = Annotated[
     # future validators will come here
     Union[
         BanListSafetyValidatorConfig,
         GenderAssumptionBiasSafetyValidatorConfig,
         LexicalSlurSafetyValidatorConfig, 
         PIIRemoverSafetyValidatorConfig
     ],
     Field(discriminator="type")
 ]

-class GuardrailInputRequest(SQLModel):
+class GuardrailInputRequest(BaseModel):
     request_id: str
     input: str
     validators: List[ValidatorConfigItem]

-class GuardrailOutputRequest(SQLModel):
+class GuardrailOutputRequest(BaseModel):
     request_id: str
     output: str
     validators: List[ValidatorConfigItem]

1-1: Modernize type annotations (optional).

Static analysis flags deprecated imports. For Python 3.9+, use built-in list instead of typing.List, and X | Y union syntax instead of Union[...]. Also, add a trailing newline at end of file.

♻️ Suggested changes
-from typing import List, Union, Annotated
+from typing import Annotated

-ValidatorConfigItem = Annotated[
-    # future validators will come here
-    Union[
-        BanListSafetyValidatorConfig,
-        GenderAssumptionBiasSafetyValidatorConfig,
-        LexicalSlurSafetyValidatorConfig, 
-        PIIRemoverSafetyValidatorConfig
-    ],
-    Field(discriminator="type")
-]
+ValidatorConfigItem = Annotated[
+    # future validators will come here
+    BanListSafetyValidatorConfig
+    | GenderAssumptionBiasSafetyValidatorConfig
+    | LexicalSlurSafetyValidatorConfig
+    | PIIRemoverSafetyValidatorConfig,
+    Field(discriminator="type"),
+]

 class GuardrailInputRequest(SQLModel):
     request_id: str
     input: str
-    validators: List[ValidatorConfigItem]
+    validators: list[ValidatorConfigItem]

 class GuardrailOutputRequest(SQLModel):
     request_id: str
     output: str
-    validators: List[ValidatorConfigItem]
+    validators: list[ValidatorConfigItem]
+
backend/app/tests/test_validate_with_guard.py (1)

11-12: Module-level mocks should be fixtures for test isolation.

mock_request_log_crud and mock_request_log_id are shared across all tests. Since MagicMock records call history, this can cause test pollution or make it harder to verify CRUD interactions per test. Convert these to function-scoped fixtures.

♻️ Suggested refactor
-mock_request_log_crud = MagicMock()
-mock_request_log_id = uuid4()
+@pytest.fixture
+def mock_request_log_crud():
+    return MagicMock()
+
+@pytest.fixture
+def mock_request_log_id():
+    return uuid4()


 `@pytest.mark.asyncio`
-async def test_validate_with_guard_success():
+async def test_validate_with_guard_success(mock_request_log_crud, mock_request_log_id):
     # ... rest of test uses injected fixtures

Then update all test functions to accept these fixtures as parameters.

backend/app/core/validators/pii_remover.py (1)

62-81: Consider a data-driven approach for recognizer registration.

The repetitive if-blocks for registering India-specific recognizers could be simplified with a mapping.

♻️ Optional refactor
+INDIA_RECOGNIZERS = {
+    "IN_AADHAAR": InAadhaarRecognizer,
+    "IN_PAN": InPanRecognizer,
+    "IN_PASSPORT": InPassportRecognizer,
+    "IN_VEHICLE_REGISTRATION": InVehicleRegistrationRecognizer,
+    "IN_VOTER": InVoterRecognizer,
+}

 # In __init__:
-        if "IN_AADHAAR" in self.entity_types:
-            self.analyzer.registry.add_recognizer(
-                InAadhaarRecognizer()
-            )
-        if "IN_PAN" in self.entity_types:
-            self.analyzer.registry.add_recognizer(
-                InPanRecognizer()
-            )
-        # ... etc
+        for entity, recognizer_cls in INDIA_RECOGNIZERS.items():
+            if entity in self.entity_types:
+                self.analyzer.registry.add_recognizer(recognizer_cls())
backend/app/utils.py (1)

14-40: LGTM! Well-structured generic response model.

The APIResponse class with factory methods provides a clean, consistent API response pattern. The error handling in failure_response correctly uses .get() for safe dictionary access.

Minor: Consider using built-in dict instead of typing.Dict (deprecated since Python 3.9).

♻️ Optional: Use built-in dict
-from typing import Any, Dict, Generic, Optional, TypeVar
+from typing import Any, Generic, Optional, TypeVar

-    metadata: Optional[Dict[str, Any]] = None
+    metadata: Optional[dict[str, Any]] = None
backend/app/core/validators/lexical_slur.py (1)

47-55: Consider optimizing slur detection with a set lookup.

The current implementation iterates through self.slur_list for each check and then loops through words again for redaction. Using a set for lookup would improve efficiency.

♻️ Suggested optimization
-        for slur in self.slur_list:
-            if slur in words:
-                if slur not in detected_slurs:
-                    detected_slurs.append(slur)
-
-        if len(detected_slurs) > 0:
-            for word in words:
-                if word in detected_slurs:
-                    value = re.sub(rf'\b{re.escape(word)}\b', "[REDACTED_SLUR]", value, flags=re.IGNORECASE)
+        slur_set = set(self.slur_list)
+        words_set = set(words)
+        detected_slurs = list(slur_set & words_set)
+
+        for slur in detected_slurs:
+            value = re.sub(rf'\b{re.escape(slur)}\b', "[REDACTED_SLUR]", value, flags=re.IGNORECASE)
docker-compose.yml (1)

1-1: The version key is obsolete in Docker Compose v2+.

The version field is ignored by modern Docker Compose and can be removed. This is a minor cleanup item.

♻️ Remove obsolete version key
-version: "3.9"
-
 services:
backend/app/api/routes/guardrails.py (1)

48-70: Inconsistent return types between success and error paths.

The success path returns a plain dict (line 70), while the error path returns an APIResponse (line 66). This inconsistency can confuse API consumers.

🔧 Proposed fix for consistency
-    return {"validators": validators}
+    return APIResponse.success_response(data={"validators": validators})

Comment on lines +7 to +9
docker-compose \
-f docker-compose.yml \
build
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Update to Docker Compose V2 syntax.

Same issue as build-push.sh: docker-compose (V1) is deprecated. Use docker compose (V2) for consistency and future compatibility.

Suggested fix
-docker-compose \
--f docker-compose.yml \
-build
+docker compose \
+-f docker-compose.yml \
+build
📝 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
docker-compose \
-f docker-compose.yml \
build
docker compose \
-f docker-compose.yml \
build
🤖 Prompt for AI Agents
In `@scripts/build.sh` around lines 7 - 9, Replace the deprecated docker-compose
invocation in scripts/build.sh with the Docker Compose V2 syntax by changing the
command from "docker-compose" to "docker compose" (keep existing flags like "-f
docker-compose.yml build" intact); mirror the same change you applied in
build-push.sh so the script uses "docker compose -f docker-compose.yml build"
for future compatibility.

-f docker-compose.yml \
config > docker-stack.yml

docker-auto-labels docker-stack.yml
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

🏁 Script executed:

# Search for docker-auto-labels mentions in the repository
rg "docker-auto-labels" --type-list | head -20

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 467


🏁 Script executed:

# Search for docker-auto-labels in all files
rg "docker-auto-labels"

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 131


🏁 Script executed:

# Look for documentation files mentioning setup, dependencies, or requirements
fd -e md -e txt -e rst | xargs rg -l "docker-auto-labels|dependencies|requirements|setup" 2>/dev/null | head -20

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 120


🌐 Web query:

docker-auto-labels tool Docker

💡 Result:

Short summary

  • docker-auto-labels is a small Python CLI that reads a Docker Compose/Stack file, extracts service constraint labels, checks the Swarm cluster for those node labels and (if missing) creates them on nodes so constraints can work. [1][2]
  • The PyPI project is marked deprecated by its maintainer (not recommended for new use). [1]

Quick install & use

  • Install: pip3 install docker-auto-labels. [2]
  • Behavior: it parses your compose/stack, verifies labels exist in the Swarm, and will create missing labels on random nodes. Run it where you have Docker socket access (e.g., a manager) so it can query/update the cluster. [2][3]

Notes & pointers

  • Project page / deprecation notice: PyPI. [1]
  • Releases and installation files: piwheels / package index. [2]
  • Example/alternative community implementations and examples (gist) if you want a containerized variant or different approach. [3]

Sources
[1] PyPI — docker-auto-labels (deprecation notice).
[2] piwheels / package release info for docker-auto-labels.
[3] Community gist with an "auto label" Docker/Swarm example.


🏁 Script executed:

# Check what the documentation files say about docker-auto-labels
grep -A 5 -B 5 "docker-auto-labels" README.md backend/README.md development.md

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 60


🏁 Script executed:

# Also check if there's a requirements.txt or setup.py mentioning docker-auto-labels
fd -e requirements.txt -e setup.py -e Pipfile -e setup.cfg | xargs rg "docker-auto-labels" 2>/dev/null

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 131


🏁 Script executed:

# Check for any installation or dependency documentation
rg "pip|install|dependencies|requires" README.md backend/README.md development.md | head -30

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 1843


Replace or properly document the deprecated docker-auto-labels tool.

The script uses docker-auto-labels (line 13) without documenting it as a dependency. The tool is marked deprecated by its maintainer on PyPI and is not installed or mentioned in any project setup instructions (README.md, development.md, or dependency files). The script will fail if the tool is not pre-installed in the deployment environment.

Either:

  1. Add installation instructions and a dependency file entry (noting the deprecation status)
  2. Replace it with an actively maintained alternative for Docker Swarm label management
  3. Add a check to verify the tool is available before attempting to use it
🤖 Prompt for AI Agents
In `@scripts/deploy.sh` at line 13, The deploy script calls the deprecated
external tool docker-auto-labels (invoked in scripts/deploy.sh) without checking
or documenting it; fix by either (a) adding installation/dependency notes and a
pinned entry in your project deps/docs that mentions its deprecation, (b)
replacing the call with a maintained alternative CLI/library for Docker Swarm
label management, or (c) adding a pre-flight check in scripts/deploy.sh (e.g.,
test for docker-auto-labels on PATH) that prints a clear error and exits if
missing; update the README/development docs accordingly to reflect the chosen
approach.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docker-compose.yml`:
- Around line 70-71: The healthcheck in docker-compose uses curl but the backend
image (based on python:3.10) doesn't install it; update the backend Dockerfile
to add curl to the apt-get install command (where you currently install jq or
other packages) so the package list includes curl and clean apt lists afterward
(e.g., the RUN line that does apt-get update && apt-get install -y ... should
include curl); this will allow the healthcheck command (test:
["CMD","curl",...]) to succeed.
🧹 Nitpick comments (2)
docker-compose.yml (2)

1-1: Consider removing the deprecated version field.

The version key is obsolete in Docker Compose v2+ and is silently ignored. Removing it cleans up the file and avoids confusion about which Compose specification is in use.

♻️ Suggested change
-version: "3.9"
-
 services:

70-74: Add start_period to prevent false healthcheck failures during startup.

Unlike the db service which has start_period: 30s, the backend healthcheck lacks this grace period. FastAPI/Uvicorn may take a few seconds to initialize, causing the healthcheck to fail prematurely and trigger unnecessary restarts.

♻️ Suggested change
     healthcheck:
       test: ["CMD", "curl", "-f", "http://localhost:80/api/v1/utils/health-check/"]
       interval: 10s
       timeout: 5s
       retries: 5
+      start_period: 30s
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 966c0f3 and 5fbaa53.

📒 Files selected for processing (1)
  • docker-compose.yml
🔇 Additional comments (4)
docker-compose.yml (4)

5-25: LGTM!

The PostgreSQL service is well-configured with proper healthcheck, required environment variable validation, and persistent volume storage.


27-36: LGTM!

Standard Adminer setup for database administration during development.


38-52: LGTM!

The prestart service is correctly configured with profile isolation, allowing migrations to run on-demand without blocking default compose operations.


77-90: LGTM!

The develop.watch configuration enables efficient live development by syncing source changes and rebuilding on dependency updates. The named volume declaration is appropriate.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +70 to +71
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/api/v1/utils/health-check/"]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if curl is installed in the backend Dockerfile
if [ -f "backend/Dockerfile" ]; then
  echo "=== Backend Dockerfile content ==="
  cat backend/Dockerfile
else
  echo "backend/Dockerfile not found"
fi

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 1587


🏁 Script executed:

#!/bin/bash
# Check if curl is used elsewhere in the project
echo "=== Searching for curl usage in the codebase ==="
rg -i "curl" --type shell --type dockerfile --type yaml --max-count=20

echo ""
echo "=== Checking docker-compose for curl usage ==="
cat docker-compose.yml | grep -A5 -B5 "curl"

Repository: ProjectTech4DevAI/kaapi-guardrails

Length of output: 532


🌐 Web query:

Does python:3.10 Docker image include curl by default

💡 Result:

No. The official python:3.10 images do not include curl by default. The Dockerfile for the Debian-based images installs only minimal packages (e.g., ca-certificates, netbase, tzdata) and the slim/alpine variants are even more minimal, so you must add curl yourself (apt-get install curl or apk add curl). [1][2]

Sources:

  • Docker Hub (python official image tags / description). [1]
  • docker-library/python GitHub (official image repository). [2]

Install curl in the backend Docker image.

The healthcheck at docker-compose.yml lines 70-71 uses curl, but curl is not installed in backend/Dockerfile. The python:3.10 base image does not include curl by default. Add curl to the apt-get install command:

Example fix
RUN apt-get update && apt-get install -y jq curl && rm -rf /var/lib/apt/lists/*

Without this, the healthcheck will fail and containers will be marked unhealthy.

🤖 Prompt for AI Agents
In `@docker-compose.yml` around lines 70 - 71, The healthcheck in docker-compose
uses curl but the backend image (based on python:3.10) doesn't install it;
update the backend Dockerfile to add curl to the apt-get install command (where
you currently install jq or other packages) so the package list includes curl
and clean apt lists afterward (e.g., the RUN line that does apt-get update &&
apt-get install -y ... should include curl); this will allow the healthcheck
command (test: ["CMD","curl",...]) to succeed.

@rkritika1508 rkritika1508 merged commit 77264dd into main Jan 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants