Files
2026-07-13 13:22:34 +08:00

126 lines
4.6 KiB
Python

from __future__ import annotations
import dataclasses
import logging
from typing import TYPE_CHECKING, Any
from fastapi import Request
from mlflow.entities.gateway_guardrail import GuardrailStage
from mlflow.gateway.guardrails import JudgeGuardrail
from mlflow.gateway.schemas import chat
from mlflow.types.chat import ChatCompletionResponse
if TYPE_CHECKING:
from mlflow.store.tracking.gateway.entities import GatewayEndpointConfig
from mlflow.store.tracking.sqlalchemy_store import SqlAlchemyStore
_logger = logging.getLogger(__name__)
def load_guardrails(
store: SqlAlchemyStore,
endpoint_config: GatewayEndpointConfig,
request: Request,
) -> list[JudgeGuardrail]:
"""Load guardrails for an endpoint and convert to callable JudgeGuardrail instances."""
# Configs are returned ordered by execution_order ASC (nulls last), then guardrail_id.
configs = store.list_endpoint_guardrail_configs(endpoint_config.endpoint_id)
if not configs:
return []
server_url = str(request.base_url).rstrip("/")
guardrails = []
for config in configs:
if config.guardrail is None:
continue
try:
resolved_scorer = store.resolve_endpoint_in_scorer(config.guardrail.scorer)
guardrail = dataclasses.replace(config.guardrail, scorer=resolved_scorer)
guardrails.append(JudgeGuardrail.from_entity(guardrail, server_url))
except Exception:
_logger.warning(
"Failed to load guardrail %s, skipping", config.guardrail_id, exc_info=True
)
return guardrails
def extract_auth_headers(headers: dict[str, str]) -> dict[str, str]:
"""Return only the Authorization header for internal guardrail calls."""
auth = next((v for k, v in headers.items() if k.lower() == "authorization"), None)
return {"authorization": auth} if auth else {}
async def run_pre_llm_guardrails(
guardrails: list[JudgeGuardrail],
payload_dict: dict[str, Any],
auth_headers: dict[str, str] | None = None,
usage_tracking: bool = False,
payload_schema: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Run pre-LLM guardrails on the request payload. Returns the (possibly modified) dict."""
for guardrail in guardrails:
if guardrail.stage == GuardrailStage.BEFORE:
payload_dict = await guardrail.process_request(
payload_dict,
auth_headers=auth_headers,
usage_tracking=usage_tracking,
payload_schema=payload_schema,
)
return payload_dict
async def run_post_llm_guardrails(
guardrails: list[JudgeGuardrail],
request_payload: dict[str, Any],
response: chat.ResponsePayload,
auth_headers: dict[str, str] | None = None,
usage_tracking: bool = False,
) -> chat.ResponsePayload:
"""Run post-LLM guardrails on the response. Returns the (possibly modified) response.
Note: post-LLM guardrails are skipped for streaming responses. Configure guardrails
that must run on all responses to use the pre-LLM stage, or disable streaming on the endpoint.
"""
post_llm_guardrails = [g for g in guardrails if g.stage == GuardrailStage.AFTER]
if not post_llm_guardrails:
return response
response_dict = response.model_dump()
schema = ChatCompletionResponse.model_json_schema()
for guardrail in post_llm_guardrails:
response_dict = await guardrail.process_response(
request_payload,
response_dict,
auth_headers=auth_headers,
usage_tracking=usage_tracking,
payload_schema=schema,
)
return chat.ResponsePayload(**response_dict)
async def run_post_llm_guardrails_passthrough(
guardrails: list[JudgeGuardrail],
request_payload: dict[str, Any],
response: dict[str, Any],
auth_headers: dict[str, str] | None = None,
usage_tracking: bool = False,
) -> dict[str, Any]:
"""Run post-LLM guardrails for passthrough endpoints.
Like ``run_post_llm_guardrails`` but accepts and returns a plain ``dict``
instead of a ``chat.ResponsePayload``. No ``response_format`` schema constraint
is applied since passthrough responses are provider-specific formats.
Note: post-LLM guardrails are skipped for streaming responses. Configure
guardrails that must run on all responses to use the pre-LLM stage, or
disable streaming on the endpoint.
"""
for guardrail in guardrails:
if guardrail.stage != GuardrailStage.AFTER:
continue
response = await guardrail.process_response(
request_payload, response, auth_headers=auth_headers, usage_tracking=usage_tracking
)
return response