Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 299 additions & 2 deletions backend/app/api/routes/guardrails.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging
from typing import Annotated, Any
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from fastapi.responses import JSONResponse
from opentelemetry import trace

from app.api.deps import AuthContextDep, SessionDep
Expand All @@ -17,6 +19,7 @@
GuardrailsRequest,
)
from app.services.guardrails.jobs import start_job
from app.services.llm.guardrails import proxy_guardrails_request
from app.utils import APIResponse, load_description, validate_callback_url

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -91,6 +94,300 @@ def apply_guardrails_endpoint(
)


def _upstream_response(status_code: int, payload: Any) -> Response:
"""An empty upstream body must stay empty (204s cannot carry one)."""
if payload is None:
return Response(status_code=status_code)
return JSONResponse(status_code=status_code, content=payload)


BAN_LISTS_PATH = "/ban_lists"
LLM_PROMPT_CONFIGS_PATH = "/llm_prompt_configs"
VALIDATOR_CONFIGS_PATH = "/validators/configs"


# ROUTE ORDERING: every fixed single-segment path below collides with the
# GET /guardrails/{job_id} route declared after this section. FastAPI matches in
# declaration order and does not fall through when {job_id} fails UUID parsing,
# so these must stay above it.


@router.get(
"/guardrails",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def list_guardrails_validator_types(_current_user: AuthContextDep) -> Response:
"""List the validator types supported upstream and their JSON schemas."""
status_code, payload = proxy_guardrails_request(
"GET",
"/",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
)
return _upstream_response(status_code, payload)


@router.post(
"/guardrails/ban_lists",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def create_guardrails_ban_list(
_current_user: AuthContextDep, body: dict[str, Any]
) -> Response:
status_code, payload = proxy_guardrails_request(
"POST",
f"{BAN_LISTS_PATH}/",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
json_body=body,
)
return _upstream_response(status_code, payload)


@router.get(
"/guardrails/ban_lists",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def list_guardrails_ban_lists(
_current_user: AuthContextDep,
offset: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int | None, Query(ge=1, le=100)] = None,
) -> Response:
status_code, payload = proxy_guardrails_request(
"GET",
f"{BAN_LISTS_PATH}/",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
params={"offset": offset, "limit": limit},
)
return _upstream_response(status_code, payload)


@router.post(
"/guardrails/llm_prompt_configs",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def create_guardrails_llm_prompt_config(
_current_user: AuthContextDep, body: dict[str, Any]
) -> Response:
status_code, payload = proxy_guardrails_request(
"POST",
f"{LLM_PROMPT_CONFIGS_PATH}/",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
json_body=body,
)
return _upstream_response(status_code, payload)


@router.get(
"/guardrails/llm_prompt_configs",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def list_guardrails_llm_prompt_configs(
_current_user: AuthContextDep,
validator_name: str | None = None,
offset: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int | None, Query(ge=1, le=100)] = None,
) -> Response:
status_code, payload = proxy_guardrails_request(
"GET",
f"{LLM_PROMPT_CONFIGS_PATH}/",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
params={"validator_name": validator_name, "offset": offset, "limit": limit},
)
return _upstream_response(status_code, payload)


@router.post(
"/guardrails/validators/configs",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def create_guardrails_validator_config(
_current_user: AuthContextDep, body: dict[str, Any]
) -> Response:
status_code, payload = proxy_guardrails_request(
"POST",
f"{VALIDATOR_CONFIGS_PATH}/",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
json_body=body,
)
return _upstream_response(status_code, payload)


@router.get(
"/guardrails/validators/configs",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def list_guardrails_validator_configs(
_current_user: AuthContextDep,
ids: Annotated[list[UUID] | None, Query()] = None,
stage: str | None = None,
type: str | None = None,
) -> Response:
status_code, payload = proxy_guardrails_request(
"GET",
f"{VALIDATOR_CONFIGS_PATH}/",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
params={
"ids": [str(config_id) for config_id in ids] if ids else None,
"stage": stage,
"type": type,
},
)
return _upstream_response(status_code, payload)


@router.get(
"/guardrails/validators/configs/{config_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def get_guardrails_validator_config(
_current_user: AuthContextDep, config_id: UUID
) -> Response:
status_code, payload = proxy_guardrails_request(
"GET",
f"{VALIDATOR_CONFIGS_PATH}/{config_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
)
return _upstream_response(status_code, payload)


@router.patch(
"/guardrails/validators/configs/{config_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def update_guardrails_validator_config(
_current_user: AuthContextDep, config_id: UUID, body: dict[str, Any]
) -> Response:
status_code, payload = proxy_guardrails_request(
"PATCH",
f"{VALIDATOR_CONFIGS_PATH}/{config_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
json_body=body,
)
return _upstream_response(status_code, payload)


@router.delete(
"/guardrails/validators/configs/{config_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def delete_guardrails_validator_config(
_current_user: AuthContextDep, config_id: UUID
) -> Response:
status_code, payload = proxy_guardrails_request(
"DELETE",
f"{VALIDATOR_CONFIGS_PATH}/{config_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
)
return _upstream_response(status_code, payload)


@router.get(
"/guardrails/ban_lists/{ban_list_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def get_guardrails_ban_list(
_current_user: AuthContextDep, ban_list_id: UUID
) -> Response:
status_code, payload = proxy_guardrails_request(
"GET",
f"{BAN_LISTS_PATH}/{ban_list_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
)
return _upstream_response(status_code, payload)


@router.patch(
"/guardrails/ban_lists/{ban_list_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def update_guardrails_ban_list(
_current_user: AuthContextDep, ban_list_id: UUID, body: dict[str, Any]
) -> Response:
status_code, payload = proxy_guardrails_request(
"PATCH",
f"{BAN_LISTS_PATH}/{ban_list_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
json_body=body,
)
return _upstream_response(status_code, payload)


@router.delete(
"/guardrails/ban_lists/{ban_list_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def delete_guardrails_ban_list(
_current_user: AuthContextDep, ban_list_id: UUID
) -> Response:
status_code, payload = proxy_guardrails_request(
"DELETE",
f"{BAN_LISTS_PATH}/{ban_list_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
)
return _upstream_response(status_code, payload)


@router.get(
"/guardrails/llm_prompt_configs/{prompt_config_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def get_guardrails_llm_prompt_config(
_current_user: AuthContextDep, prompt_config_id: UUID
) -> Response:
status_code, payload = proxy_guardrails_request(
"GET",
f"{LLM_PROMPT_CONFIGS_PATH}/{prompt_config_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
)
return _upstream_response(status_code, payload)


@router.patch(
"/guardrails/llm_prompt_configs/{prompt_config_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def update_guardrails_llm_prompt_config(
_current_user: AuthContextDep, prompt_config_id: UUID, body: dict[str, Any]
) -> Response:
status_code, payload = proxy_guardrails_request(
"PATCH",
f"{LLM_PROMPT_CONFIGS_PATH}/{prompt_config_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
json_body=body,
)
return _upstream_response(status_code, payload)


@router.delete(
"/guardrails/llm_prompt_configs/{prompt_config_id}",
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def delete_guardrails_llm_prompt_config(
_current_user: AuthContextDep, prompt_config_id: UUID
) -> Response:
status_code, payload = proxy_guardrails_request(
"DELETE",
f"{LLM_PROMPT_CONFIGS_PATH}/{prompt_config_id}",
organization_id=_current_user.organization_.id,
project_id=_current_user.project_.id,
)
return _upstream_response(status_code, payload)


@router.get(
"/guardrails/{job_id}",
response_model=APIResponse[GuardrailsJobPublic],
Expand All @@ -112,7 +409,7 @@ def get_guardrails_job_status(
tag="guardrails",
system="guardrails",
lifecycle="api.guardrails.status",
job_id=job_id,
job_id=str(job_id),
project_id=project_id,
organization_id=_current_user.organization_.id,
):
Expand Down
Loading
Loading