Spaces:
Sleeping
Sleeping
| # [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) | |
| } |