# [file name]: core/router.py # Add this as the FIRST lines of code (after docstrings) import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) import re import logging import json from typing import Dict, List, Optional, Literal, Any from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage, HumanMessage from config.settings import settings from models.state_models import RoutingResult logger = logging.getLogger(__name__) class CountryRouter: def __init__(self): self.llm = ChatOpenAI( model=settings.CHAT_MODEL_2, temperature=0.1, max_tokens=200 ) async def route_query(self, query: str, conversation_history: List[Dict]) -> RoutingResult: """Unified LLM-powered routing""" try: # Build conversation context context = self._build_conversation_context(conversation_history) # LLM routing prompt routing_prompt = self._build_routing_prompt(query, context) logger.info(f"🔀 Routing query: '{query[:50]}...'") # Call LLM for routing decision response = await self.llm.ainvoke([SystemMessage(content=routing_prompt)]) routing_result = self._parse_routing_response(response.content) logger.info(f"🎯 Router decision: {routing_result.country} ({routing_result.confidence})") return routing_result except Exception as e: logger.error(f"Router error: {e}") # Fallback to unclear return RoutingResult( country="unclear", confidence="low", method="error_fallback", explanation=f"Router error: {str(e)}" ) def _build_routing_prompt(self, query: str, context: str) -> str: """Build comprehensive routing prompt""" return f""" Vous ĂȘtes un routeur intelligent pour un assistant juridique spĂ©cialisĂ© dans le droit du BĂ©nin et de Madagascar. **TÂCHE:** Analyser la requĂȘte utilisateur et dĂ©terminer la meilleure destination. **DESTINATIONS POSSIBLES:** - "benin": Questions juridiques concernant le BĂ©nin (lois, procĂ©dures, droits) - "madagascar": Questions juridiques concernant Madagascar (lois, procĂ©dures, droits) - "assistance_request": Demande pour parler Ă  un avocat humain - "greeting_small_talk": Salutations, prĂ©sentations, remerciements (politesse uniquement) - "conversation_repair": IncomprĂ©hension, demande de clarification - "conversation_summarization": Demande de rĂ©sumĂ© de la conversation - "out_of_scope": Questions NON juridiques (cafĂ©, mĂ©tĂ©o, sports, recettes, etc.) - "unclear": Intention juridique incertaine **REQUÊTE:** "{query}" **CONTEXTE DE CONVERSATION:** {context} **RÈGLES DE CLASSIFICATION:** 1. **greeting_small_talk** - UNIQUEMENT pour politesse basique: - Salutations: "bonjour", "salut", "hello", "bonsoir", "au revoir" - PrĂ©sentations brĂšves: "je m'appelle X", "mon nom est X" - Remerciements: "merci", "merci beaucoup" - Politesses simples: "comment ça va", "ça va bien" - Questions sur l'identitĂ© de l'assistant: "qui es-tu", "comment tu t'appelles" 2. **benin** - Pour questions juridiques sur le BĂ©nin: - Mentions explicites: "bĂ©nin", "benin", "bĂ©ninois" - Villes: "cotonou", "porto-novo" - Lois/procĂ©dures bĂ©ninoises 3. **madagascar** - Pour questions juridiques sur Madagascar: - Mentions explicites: "madagascar", "malgache" - Villes: "antananarivo", "toamasina" - Lois/procĂ©dures malgaches 4. **assistance_request** - Demande d'aide humaine: - "parler Ă  un avocat" - "contacter un avocat" - "assistance tĂ©lĂ©phonique" - "besoin d'aide juridique personnalisĂ©e" 5. **conversation_repair** - ProblĂšmes de comprĂ©hension: - "je n'ai pas compris" - "rĂ©pĂšte s'il te plaĂźt" - "explique autrement" - "qu'est-ce que tu veux dire" 6. **conversation_summarization** - Demande de rĂ©sumĂ©: - "rĂ©sume notre conversation" - "rĂ©capitulatif" - "qu'avons-nous dit" 7. **out_of_scope** - Questions clairement NON juridiques: - MĂ©tĂ©o/Climat: "tempĂ©rature Ă  Douala", "il va pleuvoir?" - Nourriture: "recette de ndolĂ©", "fais-moi un cafĂ©" - Sport: "rĂ©sultat du match", "qui a gagnĂ©?" - Technologie: "comment rĂ©parer mon tĂ©lĂ©phone", "meilleur ordinateur" - Divertissement: "raconte une blague", "parle-moi de musique" - SantĂ© non-juridique: "symptĂŽmes grippe", "remĂšdes traditionnels" - **RĂšgle clĂ©**: AUCUN aspect juridique ou lien avec le droit 8. **unclear** - Questions juridiques MAIS pays/dĂ©tails manquants: - "J'ai un problĂšme de divorce" (quel pays?) - "Comment crĂ©er une entreprise" (BĂ©nin ou Madagascar?) - "Besoin d'aide juridique" (trop vague) - "Question sur l'hĂ©ritage" (juridiction non prĂ©cisĂ©e) - **RĂšgle clĂ©**: Intention juridique Ă©vidente MAIS manque de prĂ©cision sur le pays ou les dĂ©tails **EXEMPLES COMPLETS:** - "Bonjour" → {{"destination": "greeting_small_talk", "confidence": "high", "reasoning": "Salutation simple"}} - "je m'appelle Thibaut" → {{"destination": "greeting_small_talk", "confidence": "high", "reasoning": "PrĂ©sentation personnelle"}} - "comment est-ce que je m'appelle" → {{"destination": "greeting_small_talk", "confidence": "high", "reasoning": "Question personnelle de rappel"}} - "salut comment ça va" → {{"destination": "greeting_small_talk", "confidence": "high", "reasoning": "Salutation et politesse"}} - "merci beaucoup" → {{"destination": "greeting_small_talk", "confidence": "high", "reasoning": "Remerciement"}} - "qui es-tu" → {{"destination": "greeting_small_talk", "confidence": "high", "reasoning": "Question sur l'identitĂ© de l'assistant"}} - "procedure divorce BĂ©nin" → {{"destination": "benin", "confidence": "high", "reasoning": "Question juridique explicite sur le BĂ©nin"}} - "loi fonciĂšre Madagascar" → {{"destination": "madagascar", "confidence": "high", "reasoning": "Question juridique sur Madagascar"}} - "Je veux parler Ă  un avocat" → {{"destination": "assistance_request", "confidence": "high", "reasoning": "Demande explicite d'assistance humaine"}} - "Je n'ai pas compris" → {{"destination": "conversation_repair", "confidence": "high", "reasoning": "Demande de clarification"}} - "rĂ©sume notre conversation" → {{"destination": "conversation_summarization", "confidence": "high", "reasoning": "Demande de rĂ©sumĂ©"}} - "fais-moi un cafĂ©" → {{"destination": "out_of_scope", "confidence": "high", "reasoning": "Demande sans rapport avec le droit"}} - "quelle est la mĂ©tĂ©o" → {{"destination": "out_of_scope", "confidence": "high", "reasoning": "Question mĂ©tĂ©orologique, non juridique"}} - "tempĂ©rature Ă  Douala" → {{"destination": "out_of_scope", "confidence": "high", "reasoning": "Question climatique, hors domaine juridique"}} - "raconte-moi une blague" → {{"destination": "out_of_scope", "confidence": "high", "reasoning": "Demande de divertissement, non juridique"}} - "J'ai un problĂšme de divorce" → {{"destination": "unclear", "confidence": "medium", "reasoning": "Question juridique mais pays non prĂ©cisĂ©"}} - "Comment crĂ©er une entreprise" → {{"destination": "unclear", "confidence": "medium", "reasoning": "Question juridique mais juridiction manquante"}} **IMPORTANT:** - **out_of_scope**: Questions SANS aucun aspect juridique (mĂ©tĂ©o, sport, nourriture, etc.) - **unclear**: Questions AVEC intention juridique MAIS manque de prĂ©cision sur le pays - Les prĂ©sentations, salutations et remerciements sont "greeting_small_talk" - Seules les questions JURIDIQUES avec pays identifiĂ© vont vers "benin" ou "madagascar" **FORMAT DE RÉPONSE:** RĂ©pondez UNIQUEMENT au format JSON valide: {{ "destination": "benin|madagascar|assistance_request|greeting_small_talk|conversation_repair|conversation_summarization|unclear", "confidence": "high|medium|low", "reasoning": "explication brĂšve et claire" }} **RÉPONSE:** """ def _parse_routing_response(self, response_text: str) -> RoutingResult: """Parse LLM routing response""" try: # Extract JSON from response json_match = re.search(r'\{.*\}', response_text, re.DOTALL) if not json_match: raise ValueError("No JSON found in response") result = json.loads(json_match.group()) # Validate required fields destination = result.get("destination", "unclear") confidence = result.get("confidence", "low") reasoning = result.get("reasoning", "No reasoning provided") # Map destination to RoutingResult country field valid_destinations = [ "benin", "madagascar", "unclear", "greeting_small_talk", "conversation_repair", "assistance_request", "conversation_summarization", "out_of_scope" ] if destination not in valid_destinations: logger.warning(f"Invalid destination from LLM: {destination}, defaulting to unclear") destination = "unclear" confidence = "low" reasoning = f"Destination invalide: {destination}" return RoutingResult( country=destination, confidence=confidence, method="llm_routing", explanation=reasoning ) except Exception as e: logger.error(f"Error parsing routing response: {e}") logger.error(f"Raw response: {response_text}") return RoutingResult( country="unclear", confidence="low", method="parse_error", explanation=f"Parse error: {str(e)}" ) def _build_conversation_context(self, conversation_history: List[Dict]) -> str: """Build conversation context""" if not conversation_history: return "Aucun contexte de conversation" # Get last 6 messages for context recent_messages = conversation_history[-6:] context_lines = [] for msg in recent_messages: role = "Utilisateur" if msg.get("role") in ["user", "human"] else "Assistant" content = msg.get("content", "") context_lines.append(f"{role}: {content}") return "\n".join(context_lines) async def health_check(self) -> Dict[str, Any]: """Router health check""" try: # Test with a simple query test_result = await self.route_query("test", []) return { "status": "healthy", "llm_responding": True, "last_test_result": test_result.model_dump() } except Exception as e: return { "status": "unhealthy", "llm_responding": False, "error": str(e) }