from __future__ import annotations from typing import Iterable # Fixed, code-level conversational signals — not FAQ topics. These strings are # already used as intent literals across ai_orchestrator_service/app.py, # voice.py, and asserted directly in tests; centralized here rather than # renamed so every call site validates against the same set. CONTROL_INTENTS: frozenset[str] = frozenset( { "identity_question", "handoff_request", "sensitive_request", "resolution_confirmed", "clarification", "kb_answer", "unknown", } ) UNKNOWN_INTENT = "unknown" def normalize_intent(raw: str | None, *, known_topic_codes: Iterable[str] = ()) -> str: """Validate a model-produced intent against control intents and KB topic codes. `known_topic_codes` are the `intent_code` values of the KB articles actually shown to the model for this turn — anything else the model invents collapses to UNKNOWN_INTENT rather than being trusted verbatim. """ candidate = str(raw or "").strip() if not candidate: return UNKNOWN_INTENT if candidate in CONTROL_INTENTS: return candidate normalized_topic_codes = {str(code or "").strip().upper() for code in known_topic_codes if str(code or "").strip()} if candidate.upper() in normalized_topic_codes: return candidate.upper() return UNKNOWN_INTENT