feat: implement t-604 keyword prompt moderation
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Prompt moderation orchestration.
|
||||
|
||||
T-604 只做本地关键词 prompt 审核:在图片下载、定价、预扣点和上游调用之前执行。
|
||||
MVP 不做输出审核、不做图片审核,也不保存用户 prompt 原文。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from . import policy
|
||||
from .providers import ModerationError, ModerationResult, Verdict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModerationOutcome:
|
||||
stage: str
|
||||
verdict: Verdict
|
||||
blocked: bool
|
||||
labels: tuple[str, ...] = ()
|
||||
keywords: tuple[str, ...] = ()
|
||||
matched_word_ids: tuple[int, ...] = ()
|
||||
request_id: str = ""
|
||||
reason: str = ""
|
||||
result: ModerationResult | None = field(default=None, repr=False)
|
||||
|
||||
|
||||
def _passed(stage: str) -> ModerationOutcome:
|
||||
return ModerationOutcome(stage=stage, verdict=Verdict.PASS, blocked=False)
|
||||
|
||||
|
||||
def _on_provider_failure(stage: str, exc: Exception) -> ModerationOutcome:
|
||||
"""provider 不可用时的兜底:fail-closed → BLOCK,fail-open → PASS。"""
|
||||
if policy.fail_closed():
|
||||
logger.warning("moderation fail-closed at %s: %s", stage, exc.__class__.__name__)
|
||||
return ModerationOutcome(
|
||||
stage=stage,
|
||||
verdict=Verdict.BLOCK,
|
||||
blocked=True,
|
||||
reason=f"fail-closed: {exc.__class__.__name__}",
|
||||
)
|
||||
logger.warning("moderation fail-open at %s: %s", stage, exc.__class__.__name__)
|
||||
return ModerationOutcome(stage=stage, verdict=Verdict.PASS, blocked=False, reason="fail-open")
|
||||
|
||||
|
||||
def _evaluate(stage: str, call) -> ModerationOutcome:
|
||||
if not policy.moderation_enabled():
|
||||
return _passed(stage)
|
||||
try:
|
||||
result = call()
|
||||
except ModerationError as exc:
|
||||
outcome = _on_provider_failure(stage, exc)
|
||||
_record(outcome)
|
||||
return outcome
|
||||
|
||||
outcome = ModerationOutcome(
|
||||
stage=stage,
|
||||
verdict=result.verdict,
|
||||
blocked=policy.verdict_is_blocking(result.verdict),
|
||||
labels=result.labels,
|
||||
keywords=result.keywords,
|
||||
matched_word_ids=tuple(result.raw.get("word_ids", ())) if isinstance(result.raw, dict) else (),
|
||||
request_id=result.request_id,
|
||||
result=result,
|
||||
)
|
||||
_record(outcome)
|
||||
return outcome
|
||||
|
||||
|
||||
def _record(outcome: ModerationOutcome) -> None:
|
||||
logger.info(
|
||||
"moderation stage=%s verdict=%s blocked=%s labels=%s word_ids=%s request_id=%s",
|
||||
outcome.stage,
|
||||
outcome.verdict.value,
|
||||
outcome.blocked,
|
||||
",".join(outcome.labels),
|
||||
",".join(str(word_id) for word_id in outcome.matched_word_ids),
|
||||
outcome.request_id,
|
||||
)
|
||||
|
||||
|
||||
def moderate_prompt(*, user=None, api_key=None, prompt: str) -> ModerationOutcome:
|
||||
return _evaluate(
|
||||
"prompt",
|
||||
lambda: policy.get_text_provider().moderate_text(
|
||||
prompt, biz_type=policy.biz_type("input")
|
||||
),
|
||||
)
|
||||
|
||||
def moderate_input(*, user=None, prompt: str, **kwargs) -> ModerationOutcome:
|
||||
return moderate_prompt(user=user, prompt=prompt)
|
||||
Reference in New Issue
Block a user