Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
96f295b
Added config management integration with guardrails
rkritika1508 Feb 11, 2026
acaf633
Added guardrails in config and version API
rkritika1508 Feb 12, 2026
c4abc0a
fixed integration with jobs
rkritika1508 Feb 12, 2026
82a93ff
removed env var
rkritika1508 Feb 12, 2026
63edd88
removed redundant code
rkritika1508 Feb 12, 2026
5c57537
fixed tests
rkritika1508 Feb 12, 2026
c9ea457
resolved comments
rkritika1508 Feb 12, 2026
155fea7
resolved comment
rkritika1508 Feb 12, 2026
60e0bec
updated url
rkritika1508 Feb 12, 2026
d49df4a
update code and fixed tests
rkritika1508 Feb 16, 2026
744658e
removed redundant changes
rkritika1508 Feb 16, 2026
f584c6d
Merge branch 'main' into feat/guardrails-config-integration
rkritika1508 Feb 16, 2026
11ddb58
removed redundant code
rkritika1508 Feb 16, 2026
67f615e
updated code
rkritika1508 Feb 16, 2026
c82bdb5
updated tests
rkritika1508 Feb 16, 2026
8b235a8
updates
rkritika1508 Feb 16, 2026
ea91f81
precommit
rkritika1508 Feb 16, 2026
c62e619
updated schema
rkritika1508 Feb 16, 2026
646a46f
renamed to list_validators_config
rkritika1508 Feb 16, 2026
a48ad89
renamed to list_validators_config
rkritika1508 Feb 16, 2026
43ae677
resolved comment
rkritika1508 Feb 16, 2026
43b97d0
Merge branch 'feat/guardrails-config-integration' of https://github.c…
rkritika1508 Feb 16, 2026
b9e656e
precommit
rkritika1508 Feb 16, 2026
48eb714
removed redundant tests
rkritika1508 Feb 16, 2026
7d29f21
removed tests
rkritika1508 Feb 16, 2026
cf1dbe8
removed tests
rkritika1508 Feb 16, 2026
dd2892b
removed tests
rkritika1508 Feb 16, 2026
0272dec
fixed tests
rkritika1508 Feb 16, 2026
7fe03e6
fixed tests
rkritika1508 Feb 16, 2026
10ea155
precommit
rkritika1508 Feb 16, 2026
ce03f18
updated guardrails
rkritika1508 Feb 16, 2026
229fba0
resolved comment
rkritika1508 Feb 16, 2026
6be1923
precommit
rkritika1508 Feb 16, 2026
069993f
added verify api
rkritika1508 Feb 17, 2026
99f81f3
resolved comments
rkritika1508 Feb 17, 2026
fc5424b
precommit
rkritika1508 Feb 17, 2026
a1c9346
fixed test
rkritika1508 Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app/api/docs/api_keys/verify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Verify the provided API key and return the resolved auth context.

This endpoint validates the `X-API-KEY` header and returns `user_id`, `organization_id`, and `project_id` for the authenticated key.
25 changes: 24 additions & 1 deletion backend/app/api/routes/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@

from app.api.deps import SessionDep, AuthContextDep
from app.crud.api_key import APIKeyCrud
from app.models import APIKeyPublic, APIKeyCreateResponse, Message
from app.models import (
APIKeyPublic,
APIKeyCreateResponse,
APIKeyVerifyResponse,
Message,
)
from app.utils import APIResponse, load_description
from app.api.permissions import Permission, require_permission

Expand Down Expand Up @@ -71,3 +76,21 @@ def delete_api_key_route(
api_key_crud.delete(key_id=key_id)

return APIResponse.success_response(Message(message="API Key deleted successfully"))


@router.get(
"/verify",
response_model=APIResponse[APIKeyVerifyResponse],
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
description=load_description("api_keys/verify.md"),
)
def verify_api_key_route(
current_user: AuthContextDep,
):
return APIResponse.success_response(
APIKeyVerifyResponse(
user_id=current_user.user.id,
organization_id=current_user.organization_.id,
project_id=current_user.project_.id,
)
)
2 changes: 1 addition & 1 deletion backend/app/crud/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def create_or_raise(
version = ConfigVersion(
config_id=config.id,
version=1,
config_blob=config_create.config_blob.model_dump(),
config_blob=config_create.config_blob.model_dump(mode="json"),
commit_message=config_create.commit_message,
)

Expand Down
2 changes: 1 addition & 1 deletion backend/app/crud/config/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def create_or_raise(self, version_create: ConfigVersionUpdate) -> ConfigVersion:
version = ConfigVersion(
config_id=self.config_id,
version=next_version,
config_blob=validated_blob.model_dump(),
config_blob=validated_blob.model_dump(mode="json"),
commit_message=version_create.commit_message,
)

Expand Down
8 changes: 7 additions & 1 deletion backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

from .auth import AuthContext, Token, TokenPayload

from .api_key import APIKey, APIKeyBase, APIKeyPublic, APIKeyCreateResponse
from .api_key import (
APIKey,
APIKeyBase,
APIKeyPublic,
APIKeyCreateResponse,
APIKeyVerifyResponse,
)

from .assistants import Assistant, AssistantBase, AssistantCreate, AssistantUpdate

Expand Down
8 changes: 8 additions & 0 deletions backend/app/models/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ class APIKeyCreateResponse(APIKeyPublic):
key: str


class APIKeyVerifyResponse(SQLModel):
"""Response model for API key verification."""

user_id: int
organization_id: int
project_id: int


class APIKey(APIKeyBase, table=True):
"""Database model for API keys."""

Expand Down
28 changes: 14 additions & 14 deletions backend/app/models/llm/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,24 @@ def validate_params(self):
]


class Validator(SQLModel):
validator_config_id: UUID


class ConfigBlob(SQLModel):
"""Raw JSON blob of config."""

completion: CompletionConfig = Field(..., description="Completion configuration")

input_guardrails: list[Validator] | None = Field(
default=None,
description="Guardrails applied to validate/sanitize the input before the LLM call",
)

output_guardrails: list[Validator] | None = Field(
default=None,
description="Guardrails applied to validate/sanitize the output after the LLM call",
)
# Future additions:
# classifier: ClassifierConfig | None = None
# pre_filter: PreFilterConfig | None = None
Expand Down Expand Up @@ -298,20 +312,6 @@ class LLMCallRequest(SQLModel):
"in production, always use the id + version."
),
)
input_guardrails: list[dict[str, Any]] | None = Field(
default=None,
description=(
"Optional guardrails configuration to apply input validation. "
"If not provided, no guardrails will be applied."
),
)
output_guardrails: list[dict[str, Any]] | None = Field(
default=None,
description=(
"Optional guardrails configuration to apply output validation. "
"If not provided, no guardrails will be applied."
),
)
callback_url: HttpUrl | None = Field(
default=None, description="Webhook URL for async response delivery"
)
Expand Down
119 changes: 114 additions & 5 deletions backend/app/services/llm/guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
import httpx

from app.core.config import settings
from app.models.llm.request import Validator

logger = logging.getLogger(__name__)


def call_guardrails(
input_text: str, guardrail_config: list[dict], job_id: UUID
def run_guardrails_validation(
input_text: str,
guardrail_config: list[Validator | dict[str, Any]],
job_id: UUID,
project_id: int | None,
organization_id: int | None,
suppress_pass_logs: bool = True,
) -> dict[str, Any]:
"""
Call the Kaapi guardrails service to validate and process input text.
Expand All @@ -19,14 +25,26 @@ def call_guardrails(
input_text: Text to validate and process.
guardrail_config: List of validator configurations to apply.
job_id: Unique identifier for the request.
project_id: Project identifier expected by guardrails API.
organization_id: Organization identifier expected by guardrails API.
suppress_pass_logs: Whether to suppress successful validation logs in guardrails service.

Returns:
JSON response from the guardrails service with validation results.
"""
validators = [
validator.model_dump(mode="json")
if isinstance(validator, Validator)
else validator
for validator in guardrail_config
]

payload = {
"request_id": str(job_id),
"project_id": project_id,
"organization_id": organization_id,
"input": input_text,
"validators": guardrail_config,
"validators": validators,
}

headers = {
Expand All @@ -38,16 +56,17 @@ def call_guardrails(
try:
with httpx.Client(timeout=10.0) as client:
response = client.post(
settings.KAAPI_GUARDRAILS_URL,
f"{settings.KAAPI_GUARDRAILS_URL}/",
json=payload,
params={"suppress_pass_logs": str(suppress_pass_logs).lower()},
headers=headers,
)

response.raise_for_status()
return response.json()
except Exception as e:
logger.warning(
f"[call_guardrails] Service unavailable. Bypassing guardrails. job_id={job_id}. error={e}"
f"[run_guardrails_validation] Service unavailable. Bypassing guardrails. job_id={job_id}. error={e}"
)

return {
Expand All @@ -58,3 +77,93 @@ def call_guardrails(
"rephrase_needed": False,
},
}


def list_validators_config(
organization_id: int | None,
project_id: int | None,
input_validator_configs: list[Validator] | None,
output_validator_configs: list[Validator] | None,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""
Fetch validator configurations by IDs for input and output guardrails.

Calls:
GET /validators/configs/?organization_id={organization_id}&project_id={project_id}&ids={uuid}
"""
input_validator_config_ids = [
validator_config.validator_config_id
for validator_config in (input_validator_configs or [])
]
output_validator_config_ids = [
validator_config.validator_config_id
for validator_config in (output_validator_configs or [])
]

if not input_validator_config_ids and not output_validator_config_ids:
return [], []

headers = {
"accept": "application/json",
"Authorization": f"Bearer {settings.KAAPI_GUARDRAILS_AUTH}",
"Content-Type": "application/json",
}

endpoint = f"{settings.KAAPI_GUARDRAILS_URL}/validators/configs/"

def _build_params(validator_ids: list[UUID]) -> dict[str, Any]:
Comment on lines 112 to 114
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing blank line before nested function definition (Black violation).

The CI pipeline reports that Black formatting changed this file. The missing blank line between the endpoint assignment and the _build_params definition is the likely cause.

Proposed fix
     endpoint = f"{settings.KAAPI_GUARDRAILS_URL}/validators/configs/"
+
     def _build_params(validator_ids: list[UUID]) -> dict[str, Any]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/llm/guardrails.py` around lines 112 - 113, Add a blank
line between the top-level assignment to the variable endpoint and the nested
function definition _build_params to satisfy Black's formatting rules; locate
the endpoint = f"{settings.KAAPI_GUARDRAILS_URL}/validators/configs/" assignment
and insert a single empty line immediately before the def
_build_params(validator_ids: list[UUID]) -> dict[str, Any]: declaration.

params = {
"organization_id": organization_id,
"project_id": project_id,
"ids": [str(validator_config_id) for validator_config_id in validator_ids],
}
return {key: value for key, value in params.items() if value is not None}

try:
with httpx.Client(timeout=10.0) as client:

def _fetch_by_ids(validator_ids: list[UUID]) -> list[dict[str, Any]]:
if not validator_ids:
return []

response = client.get(
endpoint,
params=_build_params(validator_ids),
headers=headers,
)
response.raise_for_status()

payload = response.json()
if not isinstance(payload, dict):
raise ValueError(
"Invalid validators response format: expected JSON object."
)

if not payload.get("success", False):
raise ValueError(
"Validator config fetch failed: `success` is false."
)

validators = payload.get("data", [])
if not isinstance(validators, list):
raise ValueError(
"Invalid validators response format: `data` must be a list."
)

return [
validator for validator in validators if isinstance(validator, dict)
]

input_guardrails = _fetch_by_ids(input_validator_config_ids)
output_guardrails = _fetch_by_ids(output_validator_config_ids)
return input_guardrails, output_guardrails

except Exception as e:
logger.warning(
"[list_validators_config] Guardrails service unavailable or invalid response. "
"Proceeding without input/output guardrails. "
f"input_validator_config_ids={input_validator_config_ids}, output_validator_config_ids={output_validator_config_ids}, "
f"organization_id={organization_id}, "
f"project_id={project_id}, endpoint={endpoint}, error={e}"
)
return [], []
Loading