Download services/query_router.py from JohnAlbarkaIbrahim/SentimentScope: direct link, hf CLI and curl.
- Browser
- Download file 5.41 kB
-
https://huggingface.co/spaces/JohnAlbarkaIbrahim/SentimentScope/resolve/main/services/query_router.py
- Command line
-
hf download hf://spaces/JohnAlbarkaIbrahim/SentimentScope/services/query_router.py
-
curl -L -o query_router.py https://huggingface.co/spaces/JohnAlbarkaIbrahim/SentimentScope/resolve/main/services/query_router.py
5.41 kB
| """Query classification and category gating router for Joule Dynamics Real Estate Intelligence.""" | |
| from __future__ import annotations | |
| import json | |
| import re | |
| from services.observability import setup_logger | |
| from services.groq_client import ( | |
| groq_call, | |
| is_structural_error, | |
| GROQ_ROUTE_MODEL, | |
| GROQ_FALLBACK_ROUTE_MODEL, | |
| ) | |
| from services.prompts import ROUTER_PROMPT | |
| logger = setup_logger(__name__) | |
| _VALID_CLASSIFICATIONS = frozenset({ | |
| "OUT_OF_SCOPE", "PATH_A", "PATH_B", "BOTH", "GREETING", "COMMERCIAL_HANDOFF", | |
| "ALERT_SUBSCRIPTION", | |
| }) | |
| def extract_json_from_text(text: str) -> dict: | |
| """Extract a JSON object from raw model output, handling think tags and markdown.""" | |
| if not text: | |
| return {} | |
| clean = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip() | |
| try: | |
| return json.loads(clean) | |
| except json.JSONDecodeError: | |
| pass | |
| match = re.search(r"\{[^{}]*\}", clean, re.DOTALL) | |
| if match: | |
| try: | |
| return json.loads(match.group(0)) | |
| except json.JSONDecodeError: | |
| pass | |
| return {} | |
| def _normalize_tool_categories(categories: object) -> list[str]: | |
| """Normalize raw parsed tool categories into a clean list of strings.""" | |
| if isinstance(categories, str): | |
| return [categories] | |
| if isinstance(categories, list): | |
| return [str(c) for c in categories] | |
| return [] | |
| def _build_router_messages(user_query: str, recent_history: list[dict] | None = None) -> list[dict]: | |
| """Constructs router message list with recent conversation history for multi-turn awareness.""" | |
| messages = [{"role": "system", "content": ROUTER_PROMPT}] | |
| if recent_history: | |
| for msg in recent_history[-2:]: | |
| content = msg.get("content", "") | |
| if "User Query:" in content: | |
| content = content.split("User Query:", 1)[-1].strip() | |
| messages.append({ | |
| "role": msg.get("role", "user"), | |
| "content": content[:400], | |
| }) | |
| messages.append({"role": "user", "content": user_query}) | |
| return messages | |
| def _find_last_assistant_message(recent_history: list[dict]) -> str: | |
| """Extracts the text of the most recent assistant turn in the history.""" | |
| for msg in reversed(recent_history): | |
| if msg.get("role") == "assistant": | |
| return msg.get("content", "").lower() | |
| return "" | |
| def _rescue_classification( | |
| classification: str, | |
| user_query: str, | |
| recent_history: list[dict] | None, | |
| ) -> str: | |
| """Architectural guardrail: rescues false OUT_OF_SCOPE classifications during active conversational flows.""" | |
| if classification != "OUT_OF_SCOPE" or not recent_history: | |
| return classification | |
| query_lower = user_query.strip().lower() | |
| if re.search(r"[\w\.-]+@[\w\.-]+\.\w+", query_lower): | |
| logger.info("[router] Rescuing OUT_OF_SCOPE -> ALERT_SUBSCRIPTION (email address in active session)") | |
| return "ALERT_SUBSCRIPTION" | |
| last_assistant_msg = _find_last_assistant_message(recent_history) | |
| alert_keywords = ("alert", "notification", "email address", "subscribe", "digest") | |
| if any(kw in last_assistant_msg for kw in alert_keywords) and len(query_lower.split()) <= 8: | |
| logger.info("[router] Rescuing OUT_OF_SCOPE -> ALERT_SUBSCRIPTION (continuation of alert setup)") | |
| return "ALERT_SUBSCRIPTION" | |
| if len(query_lower.split()) <= 5 and any(c in last_assistant_msg for c in ("?", "which", "would you", "what")): | |
| logger.info("[router] Rescuing OUT_OF_SCOPE -> PATH_A (short answer to assistant question)") | |
| return "PATH_A" | |
| return classification | |
| async def classify_query( | |
| user_query: str, | |
| recent_history: list[dict] | None = None, | |
| ) -> tuple[str, list[str]]: | |
| """Classify user query and extract tool categories for category gating with multi-turn awareness.""" | |
| messages = _build_router_messages(user_query, recent_history) | |
| for model in [GROQ_ROUTE_MODEL, GROQ_FALLBACK_ROUTE_MODEL]: | |
| try: | |
| res = await groq_call( | |
| model=model, | |
| messages=messages, | |
| max_tokens=1500, | |
| temperature=0.0, | |
| ) | |
| raw = res.choices[0].message.content or "{}" | |
| data = extract_json_from_text(raw) | |
| classification = data.get("classification", "") | |
| tool_categories = _normalize_tool_categories(data.get("tool_categories", [])) | |
| if classification not in _VALID_CLASSIFICATIONS: | |
| logger.warning( | |
| f"[router] {model} returned invalid classification '{classification}', defaulting PATH_A" | |
| ) | |
| classification = "PATH_A" | |
| classification = _rescue_classification(classification, user_query, recent_history) | |
| logger.info( | |
| f"[router] {model} -> {classification} (categories={tool_categories}): {data.get('reason', '')}" | |
| ) | |
| return classification, tool_categories | |
| except Exception as exc: | |
| if is_structural_error(exc): | |
| logger.warning(f"[router] structural error on {model}: {exc}") | |
| else: | |
| logger.warning(f"[router] {model} failed, trying fallback: {exc}") | |
| fallback_classification = _rescue_classification("PATH_A", user_query, recent_history) | |
| return fallback_classification, [] | |