| """ |
| Public Interface for Module A (Law Explanation) |
| This module provides a clean API for other parts of the application to use. |
| """ |
|
|
| import logging |
| import re |
| from typing import Dict, List, Any, Optional |
|
|
| from .rag_chain import LegalRAGChain |
| from .context_analyzer import ConversationContextAnalyzer |
| from .config import LOG_LEVEL |
| from .logging_setup import setup_logging |
|
|
| |
| setup_logging("module_a.interface") |
| logger = logging.getLogger(__name__) |
|
|
| class LawExplanationAPI: |
| """ |
| Main API for the Law Explanation module. |
| Hides the complexity of RAG, Vector DB, and LLM interactions. |
| """ |
| |
| def __init__(self): |
| """Initialize the Law Explanation engine""" |
| logger.info("Initializing LawExplanationAPI...") |
| try: |
| self.rag_chain = LegalRAGChain() |
| self.context_analyzer = ConversationContextAnalyzer() |
| logger.info("LawExplanationAPI initialized successfully") |
| except Exception as e: |
| logger.error(f"Failed to initialize LawExplanationAPI: {e}") |
| raise |
|
|
| def get_explanation(self, query: str) -> Dict[str, Any]: |
| """ |
| Get a structured legal explanation for a user query. |
| |
| Args: |
| query: The user's question (e.g., "How to get citizenship?") |
| |
| Returns: |
| Dict containing: |
| - summary: Brief answer |
| - key_point: Direct quote from law |
| - explanation: Detailed explanation |
| - next_steps: Actionable advice |
| - sources: List of source documents |
| - raw_response: The full LLM text (fallback) |
| """ |
| try: |
| |
| result = self.rag_chain.run(query) |
| raw_text = result['explanation'] |
| |
| |
| parsed = self._parse_response(raw_text) |
|
|
| |
| parsed['sources'] = result.get('sources', []) |
| parsed['query'] = query |
| parsed['raw_response'] = raw_text |
|
|
| |
| letter_suggestion = self._detect_letter_generation_opportunity( |
| parsed.get('next_steps', ''), |
| query |
| ) |
| if letter_suggestion: |
| parsed['suggested_action'] = letter_suggestion |
|
|
| return parsed |
| |
| except Exception as e: |
| logger.error(f"Error generating explanation: {e}") |
| return { |
| "error": str(e), |
| "summary": "I encountered an error while processing your request.", |
| "explanation": "Please try again later.", |
| "sources": [] |
| } |
|
|
| def _parse_response(self, text: str) -> Dict[str, str]: |
| """ |
| Parse the markdown-formatted LLM response into structured fields. |
| Expected format: |
| **Summary** ... **Key Legal Point** ... **Explanation** ... **Next Steps** ... |
| """ |
| parsed = { |
| "summary": "", |
| "key_point": "", |
| "explanation": "", |
| "next_steps": "" |
| } |
| |
| |
| |
| patterns = { |
| "summary": r"\*\*Summary\*\*\s*(.*?)\s*(?=\*\*Key Legal Point\*\*|$)", |
| "key_point": r"\*\*Key Legal Point\*\*\s*(.*?)\s*(?=\*\*Explanation\*\*|$)", |
| "explanation": r"\*\*Explanation\*\*\s*(.*?)\s*(?=\*\*Next Steps\*\*|$)", |
| "next_steps": r"\*\*Next Steps\*\*\s*(.*?)\s*$" |
| } |
| |
| for key, pattern in patterns.items(): |
| match = re.search(pattern, text, re.DOTALL | re.IGNORECASE) |
| if match: |
| parsed[key] = match.group(1).strip() |
| else: |
| |
| pass |
| |
| |
| |
| if not any(parsed.values()): |
| parsed["explanation"] = text |
| |
| return parsed |
|
|
| def get_explanation_with_context( |
| self, |
| query: str, |
| conversation_history: Optional[List[Dict[str, str]]] = None |
| ) -> Dict[str, Any]: |
| """ |
| Get explanation with conversation context awareness. |
| This method intelligently handles: |
| 1. Non-legal queries (greetings, thanks, etc.) |
| 2. Independent queries (new topics) |
| 3. Dependent queries (continuation of conversation) |
| |
| Args: |
| query: Current user message |
| conversation_history: List of previous messages in format: |
| [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}, ...] |
| |
| Returns: |
| Dict containing structured explanation (same format as get_explanation) |
| """ |
| try: |
| |
| if self.context_analyzer.is_non_legal_query(query): |
| logger.info(f"Non-legal query detected: {query[:50]}...") |
| return self._generate_non_legal_response(query) |
|
|
| |
| if not conversation_history or len(conversation_history) == 0: |
| logger.info("No conversation history, processing as new query") |
| return self.get_explanation(query) |
|
|
| |
| is_independent = self.context_analyzer.is_independent_query(query, conversation_history) |
|
|
| if is_independent: |
| logger.info("Independent query detected, processing without context") |
| return self.get_explanation(query) |
|
|
| |
| logger.info("Dependent query detected, summarizing conversation context") |
| summarized_query = self.context_analyzer.summarize_conversation(query, conversation_history) |
| logger.info(f"Summarized query: {summarized_query[:100]}...") |
|
|
| |
| result = self.get_explanation(summarized_query) |
|
|
| |
| result['context_used'] = True |
| result['original_query'] = query |
| result['summarized_query'] = summarized_query |
|
|
| |
| letter_suggestion = self._detect_letter_generation_opportunity( |
| result.get('next_steps', ''), |
| query |
| ) |
| if letter_suggestion: |
| result['suggested_action'] = letter_suggestion |
|
|
| return result |
|
|
| except Exception as e: |
| logger.error(f"Error in get_explanation_with_context: {e}") |
| |
| return self.get_explanation(query) |
|
|
| def _detect_letter_generation_opportunity(self, next_steps: str, query: str) -> Optional[Dict[str, str]]: |
| """ |
| Detect if the next steps suggest a letter generation opportunity using Mistral LLM. |
| |
| Args: |
| next_steps: The next steps text from RAG response |
| query: Original user query |
| |
| Returns: |
| Dict with suggestion details if letter generation is applicable, None otherwise |
| """ |
| try: |
| |
| system_prompt = """You are an intelligent assistant that determines if a user's legal query requires generating a formal letter or application. |
| |
| Analyze the user's query and the recommended next steps to determine: |
| 1. Does this process require submitting a formal letter, application, or written document? |
| 2. What type of document is needed? |
| |
| Common scenarios requiring letters: |
| - Citizenship applications |
| - Property dispute complaints |
| - Appeals to authorities |
| - Registration requests |
| - Formal complaints to government offices |
| - Petitions for legal matters |
| |
| Respond in this EXACT format: |
| REQUIRES_LETTER: YES or NO |
| LETTER_TYPE: [type of letter/application if YES, otherwise empty] |
| |
| Examples: |
| Query: "I want to apply for citizenship of my daughter" |
| Next Steps: "1. Gather documents 2. Visit Department of Immigration" |
| Response: |
| REQUIRES_LETTER: YES |
| LETTER_TYPE: citizenship application |
| |
| Query: "What are my property rights?" |
| Next Steps: "You have the right to own property..." |
| Response: |
| REQUIRES_LETTER: NO |
| LETTER_TYPE: |
| """ |
|
|
| prompt = f"""User Query: "{query}" |
| |
| Recommended Next Steps: "{next_steps}" |
| |
| Analyze if this requires generating a formal letter or application:""" |
|
|
| response = self.context_analyzer.llm_client.generate_response( |
| prompt=prompt, |
| system_prompt=system_prompt, |
| temperature=0.1 |
| ) |
|
|
| |
| lines = response.strip().split('\n') |
| requires_letter = False |
| letter_type = None |
|
|
| for line in lines: |
| if 'REQUIRES_LETTER:' in line: |
| requires_letter = 'YES' in line.upper() |
| elif 'LETTER_TYPE:' in line and ':' in line: |
| letter_type = line.split(':', 1)[1].strip() |
|
|
| logger.info(f"Letter detection - Query: '{query[:50]}...' Requires: {requires_letter}, Type: {letter_type}") |
|
|
| if requires_letter and letter_type: |
| return { |
| "action": "generate_letter", |
| "description": query, |
| "letter_type": letter_type, |
| "prompt": f"Would you like me to help you draft a {letter_type}?" |
| } |
|
|
| return None |
|
|
| except Exception as e: |
| logger.error(f"Error in letter generation detection: {e}") |
| |
| return self._fallback_keyword_detection(next_steps, query) |
|
|
| def _fallback_keyword_detection(self, next_steps: str, query: str) -> Optional[Dict[str, str]]: |
| """Fallback keyword-based detection if LLM fails""" |
| letter_keywords = [ |
| 'write', 'letter', 'application', 'submit', 'file', 'petition', |
| 'request', 'appeal', 'complaint', 'notice', 'draft', 'apply' |
| ] |
|
|
| intent_keywords = [ |
| 'apply for', 'want to apply', 'need to apply', 'how to apply', |
| 'get citizenship', 'obtain', 'register', 'request for' |
| ] |
|
|
| next_steps_lower = next_steps.lower() |
| query_lower = query.lower() |
|
|
| has_letter_keyword = any(keyword in next_steps_lower or keyword in query_lower for keyword in letter_keywords) |
| has_intent_keyword = any(keyword in query_lower for keyword in intent_keywords) |
|
|
| if has_letter_keyword or has_intent_keyword: |
| letter_type = None |
| if 'citizenship' in query_lower or 'citizenship' in next_steps_lower: |
| letter_type = "citizenship application" |
| elif 'complaint' in next_steps_lower or 'complaint' in query_lower: |
| letter_type = "formal complaint" |
| elif 'appeal' in next_steps_lower or 'appeal' in query_lower: |
| letter_type = "appeal" |
| elif 'application' in next_steps_lower or 'application' in query_lower: |
| letter_type = "application" |
| else: |
| letter_type = "formal letter" |
|
|
| return { |
| "action": "generate_letter", |
| "description": query, |
| "letter_type": letter_type, |
| "prompt": f"Would you like me to help you draft a {letter_type}?" |
| } |
|
|
| return None |
|
|
| def _generate_non_legal_response(self, query: str) -> Dict[str, Any]: |
| """ |
| Generate a friendly response for non-legal queries (greetings, thanks, etc.) |
| |
| Args: |
| query: The non-legal message |
| |
| Returns: |
| Response dict matching the standard explanation format |
| """ |
| |
| query_lower = query.lower() |
|
|
| if any(greeting in query_lower for greeting in ['hi', 'hello', 'hey', 'good morning', 'good afternoon', 'good evening']): |
| response = "Hello! I'm here to help you with legal questions. Feel free to ask me anything about laws, regulations, or legal procedures." |
| elif any(thanks in query_lower for thanks in ['thank', 'thanks', 'appreciate']): |
| response = "You're welcome! I'm glad I could help. If you have any more legal questions, feel free to ask." |
| elif any(bye in query_lower for bye in ['bye', 'goodbye', 'see you']): |
| response = "Goodbye! Feel free to come back anytime you have legal questions." |
| else: |
| response = "I'm here to assist you with legal matters. How can I help you today?" |
|
|
| return { |
| "summary": response, |
| "key_point": "", |
| "explanation": response, |
| "next_steps": "", |
| "sources": [], |
| "query": query, |
| "is_non_legal": True, |
| "context_used": False |
| } |
|
|
| def get_sources_only(self, query: str, k: int = 5) -> List[Dict[str, Any]]: |
| """ |
| Retrieve relevant legal sources without generating an explanation. |
| Useful for "Search Laws" feature. |
| """ |
| |
| embedding = self.rag_chain.embedder.generate_embedding(query) |
| results = self.rag_chain.vector_db.query_with_embedding( |
| embedding.tolist(), |
| n_results=k |
| ) |
|
|
| sources = [] |
| if results['documents'][0]: |
| for doc, metadata, distance in zip( |
| results['documents'][0], |
| results['metadatas'][0], |
| results['distances'][0] |
| ): |
| sources.append({ |
| 'text': doc, |
| 'file': metadata.get('source_file'), |
| 'section': metadata.get('article_section'), |
| 'relevance': 1.0 - distance |
| }) |
| return sources |
|
|