""" classify_character_inquiry.py Detect when the user is asking about other available characters. LLM-only — no keyword fast-path (keywords caused too many false positives). Returns: { "is_character_inquiry": bool, "inquiry_type": "style" | "pragmatic" | "general" | None style — user wants someone with a different conversational style ("someone more direct", "less patient", "more sarcastic") pragmatic — user wants a character better suited to structured guidance ("someone who explains step by step", "someone more patient") general — generic "who else is there" / "what other characters" } """ import json import re from typing import Dict, Any, List from llm_client import client from config import OPENAI_CLASSIFIER_MODEL def _parse_json(raw: str, fallback: Any) -> Any: m = re.search(r"\{.*\}", raw, re.S) if m: try: return json.loads(m.group(0)) except json.JSONDecodeError: pass try: return json.loads(raw) except json.JSONDecodeError: return fallback def classify_character_inquiry( user_msg: str, history: List[Dict[str, str]], ) -> Dict[str, Any]: """ Detect whether the user is asking about other available characters. Pure LLM classification — no keyword matching. """ _NULL = {"is_character_inquiry": False, "inquiry_type": None, "target_character": None} # Skip very short messages — they are almost never character inquiries if len(user_msg.split()) <= 3: return _NULL system = ( "The user is talking with a philosophical chatbot that has multiple characters " "(Socrates, Diogenes, Nietzsche, Camus, Schopenhauer). " "Detect whether the user's message is asking about other available characters " "or requesting a switch to a different one.\n\n" "Return ONLY valid JSON:\n" '{ "is_character_inquiry": true/false, "inquiry_type": "style" | "pragmatic" | "general" | null, ' '"target_character": "socrates" | "diogenes" | "nietzsche" | "camus" | "schopenhauer" | null }\n\n' "is_character_inquiry = true ONLY when the user:\n" " - Explicitly asks if there is SOMEONE ELSE or ANOTHER CHARACTER available\n" " - Asks for a character with a different personality or style\n" " - Asks who else they could speak to\n" " - Requests to switch to a different character\n\n" "is_character_inquiry = false when the user:\n" " - Is asking the CURRENT character to explain something better or in more detail\n" " - Is asking a follow-up question on the current topic\n" " - Is asking for step-by-step guidance from the current character\n" " - Uses phrases like 'walk me through', 'explain more', 'be more detailed'\n" " (these are requests TO the current character, not requests FOR a new one)\n\n" "inquiry_type (only when is_character_inquiry = true):\n" " style — user wants a different conversational style (more direct, gentler, etc.)\n" " pragmatic — user wants someone better at structured, step-by-step guidance\n" " general — user just wants to know who else is available\n" " null — not a character inquiry\n\n" "target_character = the specific character the user wants to be CONNECTED TO / TALK TO / " "SWITCHED TO by name (e.g. 'can you call Nietzsche?', 'let me talk to Camus', " "'bring Diogenes', 'pass me to him' referring to a named character). " "Set it ONLY when the user wants to actually switch to or speak with that specific person. " "Set it to null if the user merely asks what another character THINKS about the topic " "(that is a comparison question, not a switch), or names no one specific. " "When target_character is set, is_character_inquiry must also be true.\n\n" "When in doubt, return false — do not fire on ambiguous phrasing." ) recent = history[-4:] if history else [] msgs = [{"role": "system", "content": system}] for h in recent: role = h.get("role", "user") if role not in ("user", "assistant"): role = "user" msgs.append({"role": role, "content": h.get("content", "")}) msgs.append({"role": "user", "content": user_msg}) try: resp = client.chat.completions.create( model=OPENAI_CLASSIFIER_MODEL, messages=msgs, temperature=0.1, ) raw = (resp.choices[0].message.content or "").strip() parsed = _parse_json(raw, _NULL) _KNOWN = {"socrates", "diogenes", "nietzsche", "camus", "schopenhauer"} _target = (parsed.get("target_character") or "").lower().strip() if _target not in _KNOWN: _target = None return { "is_character_inquiry": bool(parsed.get("is_character_inquiry", False)) or bool(_target), "inquiry_type": parsed.get("inquiry_type") or None, "target_character": _target, } except Exception: return _NULL