"""Step 2 (SPEC L-11): a separate, narrow LLM call — is the question an attack? Anything but a valid `safe` is one.""" from dataclasses import dataclass from app.db.models import Verdict from app.llm.provider import ChatMessage, JsonSchema, LLMProvider SCHEMA = JsonSchema( name="guard_verdict", schema={ "type": "object", "additionalProperties": False, "required": ["verdict", "reason"], "properties": { "verdict": {"type": "string", "enum": ["safe", "attack"]}, "reason": {"type": "string"}, }, }, ) INVALID = "страж вернул не JSON по схеме" @dataclass(frozen=True) class GuardVerdict: verdict: Verdict reason: str tokens: int async def classify(llm: LLMProvider, system_prompt: str, prompt: str) -> GuardVerdict: """LLMError propagates: an unreachable model is «try later», not an attack.""" result = await llm.complete([ChatMessage("system", system_prompt), ChatMessage("user", prompt)], SCHEMA) data = result.data or {} verdict, reason = data.get("verdict"), data.get("reason") if verdict not in ("safe", "attack") or not isinstance(reason, str): return GuardVerdict(Verdict.ATTACK, INVALID, result.tokens) return GuardVerdict(Verdict(verdict), reason[:300], result.tokens)