# Message router extracted from app.py import json import re from config.prompts import ( ROUTER_SCHEMA, ROUTER_SYSTEM_PROMPT, WHY_HIRE_REGEX, canonical_why_hire_pitch, CONTACT_COLLECTION_PROMPT ) from config.settings import USE_CANONICAL_WHY_HIRE class MessageRouter: """Handles message classification and routing logic""" def __init__(self, openai_client): self.openai = openai_client def classify(self, message: str) -> dict: """Classify user message using AI with regex fallback for email detection""" messages = [{"role": "system", "content": ROUTER_SYSTEM_PROMPT}] # Optionally prepend few-shots for stability: # messages = [{"role": "system", "content": system}, *fewshots] messages.append({"role": "user", "content": message}) resp = self.openai.chat.completions.create( model="gemini-2.5-flash", messages=messages, response_format={ "type": "json_schema", "json_schema": {"name": "router", "schema": ROUTER_SCHEMA} }, temperature=0.0, top_p=1.0, max_tokens=200 ) try: parsed = json.loads(resp.choices[0].message.content) # Minimal defensive checks if not isinstance(parsed, dict) or "intent" not in parsed: raise ValueError("schema mismatch") # Hybrid approach: If AI missed email, catch with regex if parsed["intent"] != "contact_exchange": email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' if re.search(email_pattern, message): parsed["intent"] = "contact_exchange" parsed["requires_contact"] = False parsed["matched_phrases"].append("email_detected_by_regex") return parsed except Exception: # Safe, schema-conformant fallback return { "intent": "career", "why_hire": False, "requires_contact": False, "confidence": 0.0, "matched_phrases": [] } def should_use_canonical_why_hire(self, message: str, why_hire_flag: bool, mode: str) -> bool: """Check if canonical pitch should be used""" if mode != "career": return False if WHY_HIRE_REGEX.search(message): return True if why_hire_flag: return True return False def get_response_for_route(self, message: str, route: dict, mode: str) -> str | None: """Get immediate response based on routing, or None to continue to chat""" intent = route.get("intent", "career") why_hire_flag = bool(route.get("why_hire")) requires_contact_flag = bool(route.get("requires_contact")) # Handle boundary cases if intent == "other": from config.settings import BOUNDARY_REPLY return BOUNDARY_REPLY # Handle contact collection for interested users if requires_contact_flag: return CONTACT_COLLECTION_PROMPT # Handle canonical "why hire" pitch if USE_CANONICAL_WHY_HIRE and self.should_use_canonical_why_hire(message, why_hire_flag, mode): return canonical_why_hire_pitch() # Continue to regular chat return None