fisherman611 commited on
Commit
ec8ecd4
·
verified ·
1 Parent(s): 293a8c9

Upload 4 files

Browse files
utils/data_loader.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import pandas as pd
3
+ from typing import List, Dict, Any
4
+ from config import Config
5
+ from tqdm.auto import tqdm
6
+
7
+
8
+ class LegalDataLoader:
9
+ """Load and process legal corpus"""
10
+
11
+ def __init__(self):
12
+ self.legal_corpus = None
13
+
14
+ def load_legal_corpus(self) -> List[Dict[str, Any]]:
15
+ """Load legal corpus from JSON file"""
16
+ try:
17
+ with open(Config.CORPUS_PATH, "r", encoding="utf-8") as f:
18
+ self.legal_corpus = json.load(f)
19
+
20
+ # Handle the case where the corpus is a list of laws with nested articles
21
+ if isinstance(self.legal_corpus, list):
22
+ print(f"Loaded {len(self.legal_corpus)} legal documents")
23
+ else:
24
+ # Handle single law document format
25
+ print(
26
+ f"Loaded legal document: {self.legal_corpus.get('law_id', 'Unknown')}"
27
+ )
28
+ self.legal_corpus = [self.legal_corpus]
29
+
30
+ return self.legal_corpus
31
+
32
+ except FileNotFoundError:
33
+ print(f"Legal corpus file not found at {Config.CORPUS_PATH}")
34
+ return []
35
+ except json.JSONEncoder as e:
36
+ print(f"Error parsing JSON file: {e}")
37
+ return []
38
+
39
+ def prepare_documents_for_indexing(self) -> List[Dict[str, Any]]:
40
+ """Prepare legal documents for vector indexing"""
41
+ if self.legal_corpus is None:
42
+ self.load_legal_corpus()
43
+
44
+ documents = []
45
+ for law in tqdm(self.legal_corpus):
46
+ law_id = law.get("law_id", "")
47
+ articles = law.get("articles", [])
48
+
49
+ # Process each article in the law
50
+ for article in articles:
51
+ article_id = article.get("article_id", "")
52
+ title = article.get("title", "")
53
+ content = article.get("text", "")
54
+
55
+ if content and content.strip():
56
+ # Create unique document ID combining law_id and article_id
57
+ doc_id = (
58
+ f"{law_id}_{article_id}"
59
+ if law_id and article_id
60
+ else article_id
61
+ )
62
+ documents.append(
63
+ {
64
+ "id": doc_id,
65
+ "title": title,
66
+ "content": content,
67
+ "metadata": {
68
+ "law_id": law_id,
69
+ "article_id": article_id,
70
+ "title": title,
71
+ "source": "legal_corpus",
72
+ },
73
+ }
74
+ )
75
+
76
+ print(f"Prepared {len(documents)} documents for indexing")
77
+ return documents
78
+
79
+ def get_document_by_id(self, doc_id: str) -> Dict[str, Any]:
80
+ """Get a specific document by ID"""
81
+ if self.legal_corpus is None:
82
+ self.load_legal_corpus()
83
+
84
+ # Handle both formats: "law_id_article_id" or just "article_id"
85
+ for law in self.legal_corpus:
86
+ law_id = law.get("law_id", "")
87
+ articles = law.get("articles", [])
88
+
89
+ for article in articles:
90
+ article_id = article.get("article_id", "")
91
+ combined_id = (
92
+ f"{law_id}_{article_id}" if law_id and article_id else article_id
93
+ )
94
+
95
+ if combined_id == doc_id or article_id == doc_id:
96
+ return {
97
+ "law_id": law_id,
98
+ "article_id": article_id,
99
+ "title": article.get("title", ""),
100
+ "text": article.get("text", ""),
101
+ "combined_id": combined_id,
102
+ }
103
+ return {}
104
+
105
+ def search_documents_by_keyword(self, keyword: str) -> List[Dict[str, Any]]:
106
+ """Search documents containing specific keywords"""
107
+ if self.legal_corpus is None:
108
+ self.load_legal_corpus()
109
+
110
+ results = []
111
+ keyword_lower = keyword.lower()
112
+
113
+ for law in self.legal_corpus:
114
+ law_id = law.get("law_id", "")
115
+ articles = law.get("articles", [])
116
+
117
+ for article in articles:
118
+ content = article.get("text", "").lower()
119
+ title = article.get("title", "").lower()
120
+
121
+ if keyword_lower in content or keyword_lower in title:
122
+ article_id = article.get("article_id", "")
123
+ combined_id = (
124
+ f"{law_id}_{article_id}"
125
+ if law_id and article_id
126
+ else article_id
127
+ )
128
+
129
+ results.append(
130
+ {
131
+ "law_id": law_id,
132
+ "article_id": article_id,
133
+ "title": article.get("title", ""),
134
+ "text": article.get("text", ""),
135
+ "combined_id": combined_id,
136
+ }
137
+ )
138
+
139
+ return results
utils/google_search.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from googlesearch import search
3
+ from bs4 import BeautifulSoup
4
+ from typing import List, Dict
5
+ import time
6
+ from config import Config
7
+ from urllib.parse import urlparse
8
+
9
+
10
+ class GoogleSearchTool:
11
+ """Google Search tool for legal questions with insufficient information"""
12
+
13
+ def __init__(self):
14
+ self.search_delay = 1
15
+
16
+ def search_legal_info(
17
+ self, query: str, num_results: int = None
18
+ ) -> List[Dict[str, str]]:
19
+ if num_results is None:
20
+ num_results = Config.GOOGLE_SEARCH_RESULTS_COUNT
21
+
22
+ try:
23
+ # Enhanced Vietnamese legal query patterns
24
+ enhanced_queries = [
25
+ f"{query} luật pháp Việt Nam site:thuvienphapluat.vn",
26
+ f"{query} pháp luật Việt Nam site:moj.gov.vn",
27
+ f"{query} quy định pháp luật Việt Nam",
28
+ f"{query} luật việt nam điều khoản",
29
+ ]
30
+
31
+ all_results = []
32
+ seen_urls = set()
33
+
34
+ # Try different search queries to get better results
35
+ for enhanced_query in enhanced_queries:
36
+ if len(all_results) >= num_results:
37
+ break
38
+
39
+ try:
40
+ search_results = search(enhanced_query, num_results=3, lang="vi")
41
+
42
+ for url in search_results:
43
+ if len(all_results) >= num_results:
44
+ break
45
+
46
+ if url in seen_urls:
47
+ continue
48
+
49
+ seen_urls.add(url)
50
+
51
+ try:
52
+ # Get page content
53
+ content = self._get_page_content(url)
54
+ if content and content.get("snippet"):
55
+ all_results.append(
56
+ {
57
+ "url": url,
58
+ "title": content.get(
59
+ "title", "Không có tiêu đề"
60
+ ),
61
+ "snippet": content.get(
62
+ "snippet", "Không có nội dung"
63
+ ),
64
+ "domain": self._extract_domain(url),
65
+ }
66
+ )
67
+
68
+ time.sleep(self.search_delay)
69
+
70
+ except Exception as e:
71
+ print(f"Error fetching content from {url}: {e}")
72
+ continue
73
+
74
+ except Exception as e:
75
+ print(f"Error with search query '{enhanced_query}': {e}")
76
+ continue
77
+
78
+ return all_results[:num_results]
79
+
80
+ except Exception as e:
81
+ print(f"Error performing Google search: {e}")
82
+ return []
83
+
84
+ def _extract_domain(self, url: str) -> str:
85
+ """Extract domain from URL"""
86
+ try:
87
+ parsed = urlparse(url)
88
+ return parsed.netloc
89
+ except:
90
+ return "Unknown"
91
+
92
+ def _get_page_content(self, url: str) -> Dict[str, str]:
93
+ """Extract content from a web page with better Vietnamese content handling"""
94
+ try:
95
+ headers = {
96
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
97
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
98
+ "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8",
99
+ "Accept-Encoding": "gzip, deflate",
100
+ "Connection": "keep-alive",
101
+ }
102
+
103
+ response = requests.get(url, headers=headers, timeout=15)
104
+ response.raise_for_status()
105
+
106
+ # Handle encoding for Vietnamese content
107
+ if response.encoding.lower() in ["iso-8859-1", "windows-1252"]:
108
+ response.encoding = "utf-8"
109
+
110
+ soup = BeautifulSoup(response.content, "html.parser")
111
+
112
+ # Extract title
113
+ title_tag = soup.find("title")
114
+ title = title_tag.get_text().strip() if title_tag else "Không có tiêu đề"
115
+
116
+ # Remove unwanted elements
117
+ for element in soup(
118
+ ["script", "style", "nav", "header", "footer", "aside", "iframe"]
119
+ ):
120
+ element.decompose()
121
+
122
+ # Try to find main content areas
123
+ main_content = None
124
+ content_selectors = [
125
+ "article",
126
+ "main",
127
+ ".content",
128
+ ".post-content",
129
+ ".entry-content",
130
+ ".article-content",
131
+ ".news-content",
132
+ "#content",
133
+ ".main-content",
134
+ ]
135
+
136
+ for selector in content_selectors:
137
+ main_content = soup.select_one(selector)
138
+ if main_content:
139
+ break
140
+
141
+ # If no main content found, use body
142
+ if not main_content:
143
+ main_content = soup.find("body")
144
+
145
+ if main_content:
146
+ text = main_content.get_text()
147
+ else:
148
+ text = soup.get_text()
149
+
150
+ # Clean up text for Vietnamese content
151
+ lines = (line.strip() for line in text.splitlines())
152
+ chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
153
+ text = " ".join(chunk for chunk in chunks if chunk and len(chunk) > 3)
154
+
155
+ # Extract meaningful snippet (prioritize Vietnamese legal terms)
156
+ legal_keywords = [
157
+ "luật",
158
+ "điều",
159
+ "khoản",
160
+ "quy định",
161
+ "nghị định",
162
+ "thông tư",
163
+ "quyền",
164
+ "nghĩa vụ",
165
+ ]
166
+
167
+ # Try to find sentences with legal keywords
168
+ sentences = text.split(".")
169
+ relevant_sentences = []
170
+
171
+ for sentence in sentences:
172
+ if any(keyword in sentence.lower() for keyword in legal_keywords):
173
+ relevant_sentences.append(sentence.strip())
174
+ if len(" ".join(relevant_sentences)) > 400:
175
+ break
176
+
177
+ if relevant_sentences:
178
+ snippet = ". ".join(relevant_sentences[:3])
179
+ else:
180
+ snippet = text[:600] + "..." if len(text) > 600 else text
181
+
182
+ return {"title": title, "snippet": snippet}
183
+
184
+ except Exception as e:
185
+ print(f"Error extracting content from {url}: {e}")
186
+ return {}
187
+
188
+ def format_search_results(self, results: List[Dict[str, str]]) -> str:
189
+ """Format search results for LLM context"""
190
+ if not results:
191
+ return "Không tìm thấy thông tin liên quan."
192
+
193
+ formatted_results = ""
194
+
195
+ for i, result in enumerate(results, 1):
196
+ formatted_results += f"**Nguồn {i}: {result['title']}**\n"
197
+ formatted_results += f"Website: {result.get('domain', 'Unknown')}\n"
198
+ formatted_results += f"Nội dung: {result['snippet']}\n"
199
+ formatted_results += f"Link: {result['url']}\n\n"
200
+
201
+ return formatted_results
202
+
203
+ def format_search_results_for_display(self, results: List[Dict[str, str]]) -> str:
204
+ """Format search results for UI display with clickable links"""
205
+ if not results:
206
+ return "Không tìm thấy thông tin tham khảo từ web."
207
+
208
+ # Clean HTML formatting without leading whitespaces
209
+ formatted_html = '<div style="background-color: #f8f9fa; padding: 15px; border-radius: 8px; margin: 10px 0;">'
210
+ formatted_html += '<h4 style="color: #1e40af; margin-bottom: 15px;">🌐 Nguồn tham khảo từ web:</h4>'
211
+
212
+ for i, result in enumerate(results, 1):
213
+ # Escape HTML characters in content
214
+ title_escaped = result["title"].replace("<", "&lt;").replace(">", "&gt;")
215
+ snippet_escaped = (
216
+ result["snippet"][:200].replace("<", "&lt;").replace(">", "&gt;")
217
+ )
218
+ if len(result["snippet"]) > 200:
219
+ snippet_escaped += "..."
220
+
221
+ formatted_html += f"""<div style="background-color: white; padding: 12px; margin-bottom: 10px; border-radius: 6px; border-left: 4px solid #3b82f6;">
222
+ <h5 style="margin: 0; color: #1e40af;">
223
+ <a href="{result['url']}" target="_blank" style="text-decoration: none; color: #1e40af;">
224
+ {i}. {title_escaped}
225
+ </a>
226
+ </h5>
227
+ <p style="color: #6b7280; font-size: 0.9em; margin: 5px 0;">
228
+ 📄 {result.get('domain', 'Unknown')}
229
+ </p>
230
+ <p style="margin: 8px 0; color: #374151; line-height: 1.5;">
231
+ {snippet_escaped}
232
+ </p>
233
+ <a href="{result['url']}" target="_blank" style="color: #3b82f6; text-decoration: none; font-size: 0.9em;">
234
+ 🔗 Xem chi tiết →
235
+ </a>
236
+ </div>"""
237
+
238
+ formatted_html += "</div>"
239
+ return formatted_html
utils/question_refiner.py ADDED
@@ -0,0 +1,976 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import json
3
+ from typing import Dict, List, Tuple, Optional, Any
4
+ from langchain_google_genai import ChatGoogleGenerativeAI
5
+ from langchain.prompts import PromptTemplate
6
+ from config import Config
7
+
8
+ class VietnameseLegalQuestionRefiner:
9
+ """
10
+ Refines Vietnamese legal questions for better search and understanding
11
+ """
12
+
13
+ def __init__(self):
14
+ # Initialize LLM for question refinement
15
+ self.llm = None
16
+ if Config.GOOGLE_API_KEY:
17
+ self.llm = ChatGoogleGenerativeAI(
18
+ model=Config.MODEL_REFINE,
19
+ google_api_key=Config.GOOGLE_API_KEY,
20
+ temperature=0.1
21
+ )
22
+
23
+ # Vietnamese legal terminology mappings
24
+ self.legal_abbreviations = {
25
+ # Common legal abbreviations
26
+ "dn": "doanh nghiệp",
27
+ "dntn": "doanh nghiệp tư nhân",
28
+ "tnhh": "trách nhiệm hữu hạn",
29
+ "cp": "cổ phần",
30
+ "hđ": "hợp đồng",
31
+ "hđlđ": "hợp đồng lao động",
32
+ "tclđ": "tai cạnh lao động",
33
+ "bhxh": "bảo hiểm xã hội",
34
+ "bhyt": "bảo hiểm y tế",
35
+ "bhtn": "bảo hiểm thất nghiệp",
36
+ "qsd": "quyền sử dụng",
37
+ "qsdđ": "quyền sử dụng đất",
38
+ "gcn": "giấy chứng nhận",
39
+ "gpkd": "giấy phép kinh doanh",
40
+ "gpđkkd": "giấy phép đăng ký kinh doanh",
41
+ "mst": "mã số thuế",
42
+ "tncn": "thuế thu nhập cá nhân",
43
+ "tndn": "thuế thu nhập doanh nghiệp",
44
+ "gtgt": "giá trị gia tăng",
45
+ "vat": "thuế giá trị gia tăng",
46
+ "nld": "người lao động",
47
+ "ntd": "người sử dụng lao động",
48
+ "tc": "tài chính",
49
+ "kt": "kế toán",
50
+ "tl": "tài liệu",
51
+ "vb": "văn bản",
52
+ "qđ": "quyết định",
53
+ "tt": "thông tư",
54
+ "nđ": "nghị định",
55
+ "dl": "dự luật",
56
+ "qh": "quốc hội",
57
+ "cp": "chính phủ",
58
+ "btc": "bộ tài chính",
59
+ "blđtbxh": "bộ lao động thương binh và xã hội",
60
+ "btp": "bộ tư pháp",
61
+ "btn": "bộ tài nguyên",
62
+ "khdn": "kế hoạch doanh nghiệp"
63
+ }
64
+
65
+ # Legal context keywords
66
+ self.legal_contexts = {
67
+ "business": ["doanh nghiệp", "kinh doanh", "công ty", "thành lập", "giải thể", "vốn điều lệ"],
68
+ "labor": ["lao động", "nhân viên", "hợp đồng lao động", "lương", "nghỉ phép", "sa thải"],
69
+ "tax": ["thuế", "kê khai", "miễn thuế", "giảm thuế", "mức thuế", "thuế suất"],
70
+ "real_estate": ["bất động sản", "đất đai", "nhà ở", "chuyển nhượng", "sổ đỏ", "quyền sử dụng"],
71
+ "family": ["gia đình", "hôn nhân", "ly hôn", "thừa kế", "con cái", "nuôi dưỡng"],
72
+ "criminal": ["hình sự", "vi phạm", "tội danh", "án phạt", "bồi thường"],
73
+ "civil": ["dân sự", "tranh chấp", "khiếu nại", "tố cáo", "bồi thường"]
74
+ }
75
+
76
+ # Common misspellings and corrections
77
+ self.common_corrections = {
78
+ "doanh nghiep": "doanh nghiệp",
79
+ "hop dong": "hợp đồng",
80
+ "lao dong": "lao động",
81
+ "tai chinh": "tài chính",
82
+ "ke toan": "kế toán",
83
+ "thue": "thuế",
84
+ "quyen": "quyền",
85
+ "nghia vu": "nghĩa vụ",
86
+ "dat dai": "đất đai",
87
+ "nha o": "nhà ở",
88
+ "gia dinh": "gia đình",
89
+ "hon nhan": "hôn nhân",
90
+ "ly hon": "ly hôn"
91
+ }
92
+
93
+ def refine_question(self, question: str, use_llm: bool = True) -> Dict[str, str]:
94
+ """
95
+ Main method to refine a Vietnamese legal question
96
+
97
+ Args:
98
+ question: Original user question
99
+ use_llm: Whether to use LLM for advanced refinement
100
+
101
+ Returns:
102
+ Dictionary containing original and refined questions with metadata
103
+ """
104
+ result = {
105
+ "original_question": question,
106
+ "refined_question": question,
107
+ "refinement_steps": [],
108
+ "detected_context": [],
109
+ "expanded_terms": [],
110
+ "corrections_made": []
111
+ }
112
+
113
+ # Step 1: Basic cleaning and normalization
114
+ cleaned_question = self._basic_cleaning(question)
115
+ if cleaned_question != question:
116
+ result["refinement_steps"].append("basic_cleaning")
117
+ result["refined_question"] = cleaned_question
118
+
119
+ # Step 2: Correct common misspellings
120
+ corrected_question = self._correct_spelling(cleaned_question)
121
+ if corrected_question != cleaned_question:
122
+ result["refinement_steps"].append("spelling_correction")
123
+ result["corrections_made"] = self._get_corrections_made(cleaned_question, corrected_question)
124
+ result["refined_question"] = corrected_question
125
+
126
+ # Step 3: Expand abbreviations
127
+ expanded_question = self._expand_abbreviations(corrected_question)
128
+ if expanded_question != corrected_question:
129
+ result["refinement_steps"].append("abbreviation_expansion")
130
+ result["expanded_terms"] = self._get_expanded_terms(corrected_question, expanded_question)
131
+ result["refined_question"] = expanded_question
132
+
133
+ # Step 4: Advanced LLM-based context detection (if enabled)
134
+ if use_llm and self.llm:
135
+ context = self._llm_detect_legal_context(expanded_question)
136
+ result["llm_context_detection"] = True
137
+ else:
138
+ context = self._detect_legal_context(expanded_question)
139
+ result["llm_context_detection"] = False
140
+
141
+ result["detected_context"] = context
142
+
143
+ # Step 5: LLM-based intent analysis (if enabled)
144
+ intent_analysis = {}
145
+ if use_llm and self.llm:
146
+ intent_analysis = self._llm_analyze_question_intent(expanded_question)
147
+ result["intent_analysis"] = intent_analysis
148
+ result["refinement_steps"].append("intent_analysis")
149
+
150
+ # Step 6: Add context keywords
151
+ context_enhanced_question = self._add_context_keywords(expanded_question, context)
152
+ if context_enhanced_question != expanded_question:
153
+ result["refinement_steps"].append("context_enhancement")
154
+ result["refined_question"] = context_enhanced_question
155
+
156
+ # Step 7: Advanced LLM-based refinement (if enabled and available)
157
+ if use_llm and self.llm and len(result["refined_question"].strip()) > 10:
158
+ best_refined = result["refined_question"]
159
+ refinement_method = None
160
+
161
+ # Try chain-of-thought for complex questions
162
+ if (Config.ENABLE_CHAIN_OF_THOUGHT and
163
+ intent_analysis.get("complexity") == "complex"):
164
+ cot_refined = self._llm_chain_of_thought_refinement(
165
+ result["refined_question"], context, intent_analysis
166
+ )
167
+ if cot_refined:
168
+ best_refined = cot_refined
169
+ refinement_method = "chain_of_thought"
170
+ result["refinement_steps"].append("chain_of_thought")
171
+
172
+ # Try iterative refinement for moderate complexity
173
+ elif (Config.ENABLE_ITERATIVE_REFINEMENT and
174
+ intent_analysis.get("complexity") in ["moderate", "complex"]):
175
+ iterative_refined = self._llm_iterative_refinement(
176
+ result["refined_question"], context, Config.MAX_REFINEMENT_ITERATIONS
177
+ )
178
+ if iterative_refined:
179
+ best_refined = iterative_refined
180
+ refinement_method = "iterative"
181
+ result["refinement_steps"].append("iterative_refinement")
182
+
183
+ # Fallback to standard advanced refinement
184
+ if not refinement_method:
185
+ llm_refined = self._llm_refine_question_advanced(
186
+ result["refined_question"],
187
+ context,
188
+ intent_analysis
189
+ )
190
+ if llm_refined:
191
+ best_refined = llm_refined
192
+ refinement_method = "advanced"
193
+ result["refinement_steps"].append("llm_enhancement")
194
+
195
+ # Apply the best refinement if found
196
+ if best_refined != result["refined_question"]:
197
+ # Validate the refinement with LLM (if enabled)
198
+ if Config.ENABLE_LLM_VALIDATION:
199
+ if self._llm_validate_refinement(result["refined_question"], best_refined, context):
200
+ result["refined_question"] = best_refined
201
+ result["llm_validation_passed"] = True
202
+ result["refinement_method"] = refinement_method
203
+ else:
204
+ result["llm_validation_passed"] = False
205
+ print(f"LLM refinement ({refinement_method}) rejected by validation")
206
+ else:
207
+ # Apply without validation
208
+ result["refined_question"] = best_refined
209
+ result["llm_validation_passed"] = None
210
+ result["refinement_method"] = refinement_method
211
+
212
+ return result
213
+
214
+ def _basic_cleaning(self, question: str) -> str:
215
+ """Basic text cleaning and normalization"""
216
+ # Remove extra whitespace
217
+ question = re.sub(r'\s+', ' ', question.strip())
218
+
219
+ # Remove special characters except Vietnamese diacritics and basic punctuation
220
+ question = re.sub(r'[^\w\s\u00C0-\u017F\u1EA0-\u1EF9\?\.\,\!\-\(\)]', ' ', question)
221
+
222
+ # Normalize question marks
223
+ question = re.sub(r'\?+', '?', question)
224
+
225
+ # Ensure question ends with appropriate punctuation
226
+ if not question.endswith(('?', '.', '!')):
227
+ question += '?'
228
+
229
+ return question.strip()
230
+
231
+ def _correct_spelling(self, question: str) -> str:
232
+ """Correct common Vietnamese legal term misspellings"""
233
+ corrected = question.lower()
234
+
235
+ for misspelling, correction in self.common_corrections.items():
236
+ # Use word boundaries to avoid partial matches
237
+ pattern = r'\b' + re.escape(misspelling) + r'\b'
238
+ corrected = re.sub(pattern, correction, corrected, flags=re.IGNORECASE)
239
+
240
+ return corrected
241
+
242
+ def _expand_abbreviations(self, question: str) -> str:
243
+ """Expand common Vietnamese legal abbreviations"""
244
+ expanded = question.lower()
245
+
246
+ for abbrev, full_form in self.legal_abbreviations.items():
247
+ # Match abbreviations with word boundaries
248
+ pattern = r'\b' + re.escape(abbrev) + r'\b'
249
+ # Replace with both abbreviated and full form for better search
250
+ replacement = f"{abbrev} {full_form}"
251
+ expanded = re.sub(pattern, replacement, expanded, flags=re.IGNORECASE)
252
+
253
+ return expanded
254
+
255
+ def _detect_legal_context(self, question: str) -> List[str]:
256
+ """Detect the legal context/domain of the question"""
257
+ detected_contexts = []
258
+ question_lower = question.lower()
259
+
260
+ for context, keywords in self.legal_contexts.items():
261
+ if any(keyword in question_lower for keyword in keywords):
262
+ detected_contexts.append(context)
263
+
264
+ return detected_contexts
265
+
266
+ def _add_context_keywords(self, question: str, contexts: List[str]) -> str:
267
+ """Add relevant context keywords to improve search"""
268
+ if not contexts:
269
+ return question
270
+
271
+ # Add general legal keywords
272
+ enhanced = question
273
+
274
+ # Add context-specific keywords
275
+ context_keywords = []
276
+ for context in contexts:
277
+ if context == "business":
278
+ context_keywords.extend(["luật doanh nghiệp", "đăng ký kinh doanh"])
279
+ elif context == "labor":
280
+ context_keywords.extend(["bộ luật lao động", "quyền lao động"])
281
+ elif context == "tax":
282
+ context_keywords.extend(["luật thuế", "nghĩa vụ thuế"])
283
+ elif context == "real_estate":
284
+ context_keywords.extend(["luật đất đai", "quyền sở hữu"])
285
+ elif context == "family":
286
+ context_keywords.extend(["luật hôn nhân gia đình"])
287
+
288
+ # Add keywords that aren't already in the question
289
+ question_lower = question.lower()
290
+ new_keywords = [kw for kw in context_keywords if kw not in question_lower]
291
+
292
+ if new_keywords:
293
+ enhanced = f"{question} {' '.join(new_keywords[:2])}" # Add max 2 keywords
294
+
295
+ return enhanced
296
+
297
+ def _llm_detect_legal_context(self, question: str) -> List[str]:
298
+ """Use LLM to detect legal context more accurately"""
299
+ if not self.llm:
300
+ return self._detect_legal_context(question)
301
+
302
+ prompt = PromptTemplate(
303
+ template="""Bạn là chuyên gia phân loại câu hỏi pháp luật Việt Nam. Hãy phân tích câu hỏi và xác định lĩnh vực pháp lý liên quan.
304
+
305
+ Các lĩnh vực pháp lý chính:
306
+ - business: Doanh nghiệp, kinh doanh, thành lập công ty, giải thể, vốn điều lệ
307
+ - labor: Lao động, hợp đồng lao động, sa thải, lương, nghỉ phép, bảo hiểm xã hội
308
+ - tax: Thuế, kê khai thuế, miễn thuế, thuế thu nhập, VAT
309
+ - real_estate: Bất động sản, đất đai, nhà ở, chuyển nhượng, sở hữu
310
+ - family: Hôn nhân, ly hôn, thừa kế, nuôi con, quyền con cái
311
+ - criminal: Hình sự, tội phạm, vi phạm, án phạt, truy tố
312
+ - civil: Dân sự, hợp đồng, tranh chấp, bồi thường, quyền sở hữu
313
+ - administrative: Hành chính, thủ tục, giấy tờ, cơ quan nhà nước
314
+ - constitutional: Hiến pháp, quyền công dân, nghĩa vụ, cơ cấu nhà nước
315
+
316
+ Câu hỏi: {question}
317
+
318
+ Hãy trả về tối đa 3 lĩnh vực phù hợp nhất, cách nhau bởi dấu phẩy (ví dụ: business, tax):""",
319
+ input_variables=["question"]
320
+ )
321
+
322
+ try:
323
+ response = self.llm.invoke(prompt.format(question=question))
324
+ contexts = [ctx.strip() for ctx in response.content.strip().split(",")]
325
+ # Validate contexts
326
+ valid_contexts = ["business", "labor", "tax", "real_estate", "family", "criminal", "civil", "administrative", "constitutional"]
327
+ return [ctx for ctx in contexts if ctx in valid_contexts][:3]
328
+ except Exception as e:
329
+ print(f"Error in LLM context detection: {e}")
330
+ return self._detect_legal_context(question)
331
+
332
+ def _llm_analyze_question_intent(self, question: str) -> Dict[str, Any]:
333
+ """Use LLM to analyze question intent and structure"""
334
+ if not self.llm:
335
+ return self._get_fallback_intent_analysis(question)
336
+
337
+ # Simplified prompt for more reliable JSON response
338
+ prompt = PromptTemplate(
339
+ template="""Analyze this Vietnamese legal question and return ONLY a JSON object.
340
+
341
+ Question: {question}
342
+
343
+ Return JSON with these exact fields:
344
+ - intent: "procedural" OR "definition" OR "comparison" OR "calculation" OR "advice" OR "specific_case"
345
+ - complexity: "simple" OR "moderate" OR "complex"
346
+ - keywords: array of 3-5 Vietnamese keywords
347
+ - ambiguity_level: "low" OR "medium" OR "high"
348
+ - requires_clarification: true OR false
349
+
350
+ Example: {{"intent": "procedural", "complexity": "simple", "keywords": ["thành lập", "doanh nghiệp"], "ambiguity_level": "low", "requires_clarification": false}}
351
+
352
+ ONLY return the JSON object, no other text:""",
353
+ input_variables=["question"]
354
+ )
355
+
356
+ try:
357
+ response = self.llm.invoke(prompt.format(question=question))
358
+
359
+ # Check if response exists and has content
360
+ if not response or not hasattr(response, 'content'):
361
+ print("Empty or invalid response from LLM")
362
+ return self._get_fallback_intent_analysis(question)
363
+
364
+ content = response.content.strip()
365
+
366
+ # Debug: print raw response
367
+ print(f"Raw LLM response: '{content[:100]}...'")
368
+
369
+ if not content:
370
+ print("Empty content from LLM")
371
+ return self._get_fallback_intent_analysis(question)
372
+
373
+ # Clean up the response to extract JSON
374
+ json_content = self._extract_json_from_response(content)
375
+
376
+ if not json_content:
377
+ print("No JSON found in response")
378
+ return self._get_fallback_intent_analysis(question)
379
+
380
+ # Parse JSON
381
+ analysis = json.loads(json_content)
382
+
383
+ # Validate the analysis fields
384
+ validated_analysis = self._validate_intent_analysis(analysis)
385
+ print(f"Validated analysis: {validated_analysis}")
386
+ return validated_analysis
387
+
388
+ except json.JSONDecodeError as e:
389
+ print(f"JSON decode error: {e}")
390
+ print(f"Attempted to parse: '{json_content if 'json_content' in locals() else 'N/A'}'")
391
+ return self._get_fallback_intent_analysis(question)
392
+ except Exception as e:
393
+ print(f"Error in LLM intent analysis: {e}")
394
+ # Try a simplified backup approach
395
+ return self._simple_llm_intent_analysis(question)
396
+
397
+ def _extract_json_from_response(self, content: str) -> Optional[str]:
398
+ """Extract JSON from LLM response that might contain extra text"""
399
+ if not content or not content.strip():
400
+ return None
401
+
402
+ content = content.strip()
403
+
404
+ # Remove markdown code blocks if present
405
+ content = re.sub(r'```json\s*', '', content, flags=re.IGNORECASE)
406
+ content = re.sub(r'```\s*$', '', content)
407
+ content = re.sub(r'^```\s*', '', content)
408
+
409
+ # Remove common prefixes
410
+ prefixes_to_remove = [
411
+ "here is the json:",
412
+ "here's the json:",
413
+ "json:",
414
+ "response:",
415
+ "analysis:",
416
+ ]
417
+
418
+ content_lower = content.lower()
419
+ for prefix in prefixes_to_remove:
420
+ if content_lower.startswith(prefix):
421
+ content = content[len(prefix):].strip()
422
+ break
423
+
424
+ # If content already looks like JSON (starts with {), try to use it directly
425
+ if content.startswith('{') and content.endswith('}'):
426
+ return content
427
+
428
+ # Try to find JSON object in the response using regex
429
+ json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
430
+ matches = re.findall(json_pattern, content, re.DOTALL)
431
+
432
+ if matches:
433
+ # Return the first (hopefully only) JSON match
434
+ return matches[0].strip()
435
+
436
+ # If no JSON pattern found, try to extract between first { and last }
437
+ start_idx = content.find('{')
438
+ end_idx = content.rfind('}')
439
+
440
+ if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
441
+ extracted = content[start_idx:end_idx + 1].strip()
442
+ # Basic validation - should have at least one : for key-value pairs
443
+ if ':' in extracted:
444
+ return extracted
445
+
446
+ return None
447
+
448
+ def _simple_llm_intent_analysis(self, question: str) -> Dict[str, Any]:
449
+ """Simplified LLM analysis with basic prompts"""
450
+ if not self.llm:
451
+ return self._get_fallback_intent_analysis(question)
452
+
453
+ try:
454
+ # Very simple approach - ask for specific fields one by one
455
+ intent_prompt = f"What type of legal question is this? Answer only: procedural, definition, comparison, calculation, advice, or specific_case\n\nQuestion: {question}\n\nAnswer:"
456
+ complexity_prompt = f"How complex is this question? Answer only: simple, moderate, or complex\n\nQuestion: {question}\n\nAnswer:"
457
+
458
+ intent_response = self.llm.invoke(intent_prompt)
459
+ complexity_response = self.llm.invoke(complexity_prompt)
460
+
461
+ # Extract simple responses
462
+ intent = intent_response.content.strip().lower()
463
+ complexity = complexity_response.content.strip().lower()
464
+
465
+ # Validate responses
466
+ valid_intents = ["procedural", "definition", "comparison", "calculation", "advice", "specific_case"]
467
+ valid_complexity = ["simple", "moderate", "complex"]
468
+
469
+ if intent not in valid_intents:
470
+ intent = "procedural" # default
471
+ if complexity not in valid_complexity:
472
+ complexity = "simple" # default
473
+
474
+ # Extract keywords using simple approach
475
+ keywords = []
476
+ words = question.lower().split()
477
+ important_words = [w for w in words if len(w) > 3 and w not in ["thế", "nào", "như", "thì", "này", "được", "có", "của", "để", "cho", "với", "trong", "từ", "về"]]
478
+ keywords = important_words[:3]
479
+
480
+ return {
481
+ "intent": intent,
482
+ "complexity": complexity,
483
+ "keywords": keywords,
484
+ "ambiguity_level": "medium" if complexity == "complex" else "low",
485
+ "requires_clarification": complexity == "complex"
486
+ }
487
+
488
+ except Exception as e:
489
+ print(f"Error in simple LLM analysis: {e}")
490
+ return self._get_fallback_intent_analysis(question)
491
+
492
+ def _validate_intent_analysis(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
493
+ """Validate and clean up intent analysis results"""
494
+ validated = {}
495
+
496
+ # Validate intent
497
+ valid_intents = ["procedural", "definition", "comparison", "calculation", "advice", "specific_case"]
498
+ validated["intent"] = analysis.get("intent", "unknown")
499
+ if validated["intent"] not in valid_intents:
500
+ validated["intent"] = "unknown"
501
+
502
+ # Validate complexity
503
+ valid_complexity = ["simple", "moderate", "complex"]
504
+ validated["complexity"] = analysis.get("complexity", "simple")
505
+ if validated["complexity"] not in valid_complexity:
506
+ validated["complexity"] = "simple"
507
+
508
+ # Validate keywords
509
+ keywords = analysis.get("keywords", [])
510
+ if isinstance(keywords, list):
511
+ validated["keywords"] = [str(k).strip() for k in keywords[:5] if k and str(k).strip()]
512
+ else:
513
+ validated["keywords"] = []
514
+
515
+ # Validate ambiguity level
516
+ valid_ambiguity = ["low", "medium", "high"]
517
+ validated["ambiguity_level"] = analysis.get("ambiguity_level", "low")
518
+ if validated["ambiguity_level"] not in valid_ambiguity:
519
+ validated["ambiguity_level"] = "low"
520
+
521
+ # Validate requires_clarification
522
+ validated["requires_clarification"] = bool(analysis.get("requires_clarification", False))
523
+
524
+ # Validate suggested_clarifications
525
+ clarifications = analysis.get("suggested_clarifications", [])
526
+ if isinstance(clarifications, list):
527
+ validated["suggested_clarifications"] = [str(c).strip() for c in clarifications[:3] if c and str(c).strip()]
528
+ else:
529
+ validated["suggested_clarifications"] = []
530
+
531
+ return validated
532
+
533
+ def _get_fallback_intent_analysis(self, question: str) -> Dict[str, Any]:
534
+ """Get fallback intent analysis using rule-based approach"""
535
+ # Simple rule-based fallback
536
+ question_lower = question.lower()
537
+
538
+ # Determine intent based on keywords
539
+ if any(word in question_lower for word in ["thủ tục", "cách", "làm thế nào", "quy trình", "bước"]):
540
+ intent = "procedural"
541
+ elif any(word in question_lower for word in ["là gì", "định nghĩa", "khái niệm", "nghĩa là"]):
542
+ intent = "definition"
543
+ elif any(word in question_lower for word in ["so sánh", "khác nhau", "giống", "khác biệt"]):
544
+ intent = "comparison"
545
+ elif any(word in question_lower for word in ["tính", "tính toán", "phí", "lệ phí", "thuế"]):
546
+ intent = "calculation"
547
+ elif any(word in question_lower for word in ["nên", "có thể", "được không", "có được"]):
548
+ intent = "advice"
549
+ else:
550
+ intent = "specific_case"
551
+
552
+ # Determine complexity based on length and question marks
553
+ word_count = len(question.split())
554
+ if word_count < 8:
555
+ complexity = "simple"
556
+ elif word_count < 20:
557
+ complexity = "moderate"
558
+ else:
559
+ complexity = "complex"
560
+
561
+ # Extract simple keywords
562
+ keywords = []
563
+ for word in question.split():
564
+ word_clean = re.sub(r'[^\w]', '', word).lower()
565
+ if len(word_clean) > 3 and word_clean not in ["thế", "nào", "như", "thì", "này", "được", "có", "của"]:
566
+ keywords.append(word_clean)
567
+ if len(keywords) >= 3:
568
+ break
569
+
570
+ return {
571
+ "intent": intent,
572
+ "complexity": complexity,
573
+ "keywords": keywords,
574
+ "ambiguity_level": "medium" if complexity == "complex" else "low",
575
+ "requires_clarification": complexity == "complex",
576
+ "suggested_clarifications": []
577
+ }
578
+
579
+ def _llm_refine_question_advanced(self, question: str, contexts: List[str], intent_analysis: Dict[str, Any]) -> Optional[str]:
580
+ """Advanced LLM-based question refinement with context and intent awareness"""
581
+ if not self.llm:
582
+ return None
583
+
584
+ context_str = ", ".join(contexts) if contexts else "tổng quát"
585
+ intent = intent_analysis.get("intent", "unknown")
586
+ complexity = intent_analysis.get("complexity", "simple")
587
+ keywords = intent_analysis.get("keywords", [])
588
+
589
+ # Choose refinement strategy based on intent
590
+ if intent == "procedural":
591
+ strategy_prompt = """
592
+ Đây là câu hỏi về thủ tục pháp lý. Hãy:
593
+ - Làm rõ loại thủ tục cụ thể
594
+ - Thêm từ khóa về quy trình, bước thực hiện
595
+ - Đề cập đến cơ quan có thẩm quyền nếu phù hợp"""
596
+ elif intent == "definition":
597
+ strategy_prompt = """
598
+ Đây là câu hỏi định nghĩa khái niệm. Hãy:
599
+ - Làm rõ khái niệm cần định nghĩa
600
+ - Thêm ngữ cảnh pháp lý liên quan
601
+ - Đề cập đến văn bản luật có liên quan"""
602
+ elif intent == "comparison":
603
+ strategy_prompt = """
604
+ Đây là câu hỏi so sánh. Hãy:
605
+ - Làm rõ các đối tượng được so sánh
606
+ - Thêm tiêu chí so sánh cụ thể
607
+ - Đảm bảo tính khách quan"""
608
+ else:
609
+ strategy_prompt = """
610
+ Hãy cải thiện câu hỏi theo nguyên tắc chung:
611
+ - Làm rõ ý định của câu hỏi
612
+ - Thêm ngữ cảnh pháp lý phù hợp
613
+ - Sử dụng thuật ngữ chuẩn mực"""
614
+
615
+ prompt = PromptTemplate(
616
+ template="""Bạn là chuyên gia pháp lý Việt Nam có 20 năm kinh nghiệm. Hãy cải thiện câu hỏi pháp lý sau để tối ưu hóa việc tìm kiếm thông tin.
617
+
618
+ THÔNG TIN PHÂN TÍCH:
619
+ - Lĩnh vực pháp lý: {context}
620
+ - Loại câu hỏi: {intent}
621
+ - Độ phức tạp: {complexity}
622
+ - Từ khóa chính: {keywords}
623
+
624
+ CHIẾN LƯỢC CẢI THIỆN:
625
+ {strategy}
626
+
627
+ NGUYÊN TẮC CHUNG:
628
+ 1. Giữ nguyên ý nghĩa gốc của câu hỏi
629
+ 2. Sử dụng thuật ngữ pháp lý chính xác và chuẩn mực
630
+ 3. Làm rõ các khái niệm mơ hồ
631
+ 4. Thêm ngữ cảnh pháp lý cần thiết
632
+ 5. Tối ưu hóa cho tìm kiếm trong cơ sở dữ liệu pháp luật
633
+ 6. Đảm bảo câu hỏi ngắn gọn nhưng đầy đủ thông tin
634
+ 7. Ưu tiên các từ khóa xuất hiện trong văn bản pháp luật Việt Nam
635
+
636
+ Câu hỏi gốc: {question}
637
+
638
+ Câu hỏi được cải thiện (chỉ trả về câu hỏi, không giải thích):""",
639
+ input_variables=["question", "context", "intent", "complexity", "keywords", "strategy"]
640
+ )
641
+
642
+ try:
643
+ response = self.llm.invoke(prompt.format(
644
+ question=question,
645
+ context=context_str,
646
+ intent=intent,
647
+ complexity=complexity,
648
+ keywords=", ".join(keywords),
649
+ strategy=strategy_prompt
650
+ ))
651
+
652
+ refined = response.content.strip()
653
+
654
+ # Advanced validation
655
+ if self._validate_refined_question(question, refined, intent_analysis):
656
+ return refined
657
+
658
+ except Exception as e:
659
+ print(f"Error in advanced LLM refinement: {e}")
660
+
661
+ return None
662
+
663
+ def _llm_validate_refinement(self, original: str, refined: str, contexts: List[str]) -> bool:
664
+ """Use LLM to validate if the refinement maintains original intent"""
665
+ if not self.llm:
666
+ return True
667
+
668
+ prompt = PromptTemplate(
669
+ template="""Bạn là chuyên gia đánh giá chất lượng câu hỏi pháp lý. Hãy đánh giá xem câu hỏi đã được cải thiện có giữ nguyên ý nghĩa gốc và có tốt hơn cho việc tìm kiếm thông tin pháp luật không.
670
+
671
+ Câu hỏi gốc: {original}
672
+ Câu hỏi đã cải thiện: {refined}
673
+ Lĩnh vực: {contexts}
674
+
675
+ Tiêu chí đánh giá:
676
+ 1. Giữ nguyên ý nghĩa gốc (có/không)
677
+ 2. Cải thiện khả năng tìm kiếm (có/không)
678
+ 3. Sử dụng thuật ngữ pháp lý phù hợp (có/không)
679
+ 4. Độ dài hợp lý (có/không)
680
+ 5. Rõ ràng và dễ hiểu (có/không)
681
+
682
+ Kết luận: CHẤP_NHẬN hoặc TỪ_CHỐI
683
+
684
+ Chỉ trả về kết luận:""",
685
+ input_variables=["original", "refined", "contexts"]
686
+ )
687
+
688
+ try:
689
+ response = self.llm.invoke(prompt.format(
690
+ original=original,
691
+ refined=refined,
692
+ contexts=", ".join(contexts)
693
+ ))
694
+
695
+ return "CHẤP_NHẬN" in response.content.strip().upper()
696
+ except Exception as e:
697
+ print(f"Error in LLM validation: {e}")
698
+ return True
699
+
700
+ def _validate_refined_question(self, original: str, refined: str, intent_analysis: Dict[str, Any]) -> bool:
701
+ """Validate refined question with multiple criteria"""
702
+ if not refined or not refined.strip():
703
+ return False
704
+
705
+ # Basic length check
706
+ if len(refined) < 10 or len(refined) > 500:
707
+ return False
708
+
709
+ # Should contain question mark for questions
710
+ if intent_analysis.get("intent") in ["procedural", "definition"] and "?" not in refined:
711
+ return False
712
+
713
+ # Shouldn't start with meta phrases
714
+ meta_phrases = ["câu hỏi", "tôi muốn hỏi", "xin hỏi", "cho tôi biết"]
715
+ if any(refined.lower().startswith(phrase) for phrase in meta_phrases):
716
+ return False
717
+
718
+ # Should be different from original (some improvement made)
719
+ if refined.strip().lower() == original.strip().lower():
720
+ return False
721
+
722
+ return True
723
+
724
+ def _get_corrections_made(self, original: str, corrected: str) -> List[Dict[str, str]]:
725
+ """Get list of spelling corrections that were made"""
726
+ corrections = []
727
+ for misspelling, correction in self.common_corrections.items():
728
+ if misspelling in original.lower() and correction in corrected.lower():
729
+ corrections.append({"from": misspelling, "to": correction})
730
+ return corrections
731
+
732
+ def _get_expanded_terms(self, original: str, expanded: str) -> List[Dict[str, str]]:
733
+ """Get list of abbreviations that were expanded"""
734
+ expansions = []
735
+ for abbrev, full_form in self.legal_abbreviations.items():
736
+ if abbrev in original.lower() and full_form in expanded.lower():
737
+ expansions.append({"abbreviation": abbrev, "full_form": full_form})
738
+ return expansions
739
+
740
+ def get_refinement_summary(self, refinement_result: Dict) -> str:
741
+ """Generate a human-readable summary of refinements made"""
742
+ if not refinement_result["refinement_steps"]:
743
+ return "Không có cải thiện nào được thực hiện."
744
+
745
+ summary_parts = []
746
+
747
+ if "basic_cleaning" in refinement_result["refinement_steps"]:
748
+ summary_parts.append("làm sạch văn bản")
749
+
750
+ if "spelling_correction" in refinement_result["refinement_steps"]:
751
+ corrections = refinement_result["corrections_made"]
752
+ if corrections:
753
+ summary_parts.append(f"sửa {len(corrections)} lỗi chính tả")
754
+
755
+ if "abbreviation_expansion" in refinement_result["refinement_steps"]:
756
+ expansions = refinement_result["expanded_terms"]
757
+ if expansions:
758
+ summary_parts.append(f"mở rộng {len(expansions)} từ viết tắt")
759
+
760
+ if "intent_analysis" in refinement_result["refinement_steps"]:
761
+ intent_analysis = refinement_result.get("intent_analysis", {})
762
+ intent = intent_analysis.get("intent", "unknown")
763
+ complexity = intent_analysis.get("complexity", "simple")
764
+ summary_parts.append(f"phân tích ý định ({intent}, độ phức tạp: {complexity})")
765
+
766
+ if "context_enhancement" in refinement_result["refinement_steps"]:
767
+ contexts = refinement_result["detected_context"]
768
+ if contexts:
769
+ context_method = "AI" if refinement_result.get("llm_context_detection") else "quy tắc"
770
+ summary_parts.append(f"thêm từ khóa cho lĩnh vực {', '.join(contexts)} ({context_method})")
771
+
772
+ # LLM enhancements
773
+ llm_methods = []
774
+ if "chain_of_thought" in refinement_result["refinement_steps"]:
775
+ llm_methods.append("suy luận từng bước")
776
+ if "iterative_refinement" in refinement_result["refinement_steps"]:
777
+ llm_methods.append("cải thiện lặp")
778
+ if "llm_enhancement" in refinement_result["refinement_steps"]:
779
+ llm_methods.append("cải thiện tiêu chuẩn")
780
+
781
+ if llm_methods:
782
+ validation_status = ""
783
+ if refinement_result.get("llm_validation_passed") is not None:
784
+ validation_status = " (đã xác thực)" if refinement_result["llm_validation_passed"] else " (chưa xác thực)"
785
+
786
+ method_str = ", ".join(llm_methods)
787
+ summary_parts.append(f"cải thiện bằng AI ({method_str}){validation_status}")
788
+
789
+ return f"Đã {', '.join(summary_parts)}."
790
+
791
+ def get_detailed_analysis(self, refinement_result: Dict) -> str:
792
+ """Get detailed analysis of the refinement process"""
793
+ if not refinement_result.get("intent_analysis"):
794
+ return ""
795
+
796
+ intent_analysis = refinement_result["intent_analysis"]
797
+ analysis_parts = []
798
+
799
+ # Intent information
800
+ intent = intent_analysis.get("intent", "unknown")
801
+ intent_map = {
802
+ "procedural": "Thủ tục",
803
+ "definition": "Định nghĩa",
804
+ "comparison": "So sánh",
805
+ "calculation": "Tính toán",
806
+ "advice": "Tư vấn",
807
+ "specific_case": "Trường hợp cụ thể"
808
+ }
809
+ analysis_parts.append(f"Loại câu hỏi: {intent_map.get(intent, intent)}")
810
+
811
+ # Complexity
812
+ complexity = intent_analysis.get("complexity", "simple")
813
+ complexity_map = {"simple": "Đơn giản", "moderate": "Trung bình", "complex": "Phức tạp"}
814
+ analysis_parts.append(f"Độ phức tạp: {complexity_map.get(complexity, complexity)}")
815
+
816
+ # Keywords
817
+ keywords = intent_analysis.get("keywords", [])
818
+ if keywords:
819
+ analysis_parts.append(f"Từ khóa chính: {', '.join(keywords[:3])}")
820
+
821
+ # Ambiguity level
822
+ ambiguity = intent_analysis.get("ambiguity_level", "low")
823
+ ambiguity_map = {"low": "Thấp", "medium": "Trung bình", "high": "Cao"}
824
+ analysis_parts.append(f"Độ mơ hồ: {ambiguity_map.get(ambiguity, ambiguity)}")
825
+
826
+ return " | ".join(analysis_parts)
827
+
828
+ def _llm_chain_of_thought_refinement(self, question: str, contexts: List[str], intent_analysis: Dict[str, Any]) -> Optional[str]:
829
+ """Use chain-of-thought reasoning for complex question refinement"""
830
+ if not self.llm or intent_analysis.get("complexity") != "complex":
831
+ return None
832
+
833
+ prompt = PromptTemplate(
834
+ template="""Bạn là chuyên gia pháp lý Việt Nam với 25 năm kinh nghiệm. Hãy sử dụng phương pháp suy luận từng bước để cải thiện câu hỏi pháp lý phức tạp sau.
835
+
836
+ THÔNG TIN PHÂN TÍCH:
837
+ - Câu hỏi gốc: {question}
838
+ - Lĩnh vực pháp lý: {contexts}
839
+ - Độ phức tạp: {complexity}
840
+ - Độ mơ hồ: {ambiguity}
841
+
842
+ BƯỚC 1: PHÂN TÍCH VẤN ĐỀ
843
+ Hãy xác định:
844
+ - Vấn đề pháp lý cốt lõi là gì?
845
+ - Có những khái niệm nào cần làm rõ?
846
+ - Thiếu thông tin gì để trả lời đầy đủ?
847
+
848
+ BƯỚC 2: XÁC ĐỊNH NGỮ CẢNH PHÁP LÝ
849
+ Hãy xác định:
850
+ - Văn bản pháp luật nào có khả năng liên quan?
851
+ - Cơ quan có thẩm quyền nào cần đề cập?
852
+ - Thủ tục hoặc quy trình nào cần nêu rõ?
853
+
854
+ BƯỚC 3: TỐI ƯU HÓA TỪ KHÓA
855
+ Hãy xác định:
856
+ - Thuật ngữ pháp lý chính xác cần sử dụng
857
+ - Từ khóa tìm kiếm hiệu quả
858
+ - Cụm từ thường xuất hiện trong văn bản pháp luật
859
+
860
+ BƯỚC 4: XÂY DỰNG CÂU HỎI TỐI ƯU
861
+ Dựa trên 3 bước trên, hãy xây dựng câu hỏi mới:
862
+ - Rõ ràng và cụ thể
863
+ - Sử dụng thuật ngữ pháp lý chuẩn
864
+ - Tối ưu cho tìm kiếm
865
+
866
+ ĐỊNH DẠNG TRẢ LỜI JSON:
867
+ {{
868
+ "analysis": {{
869
+ "core_legal_issue": "vấn đề pháp lý cốt lõi",
870
+ "unclear_concepts": ["khái niệm 1", "khái niệm 2"],
871
+ "missing_information": ["thông tin thiếu 1", "thông tin thiếu 2"]
872
+ }},
873
+ "legal_context": {{
874
+ "relevant_laws": ["luật 1", "luật 2"],
875
+ "authorities": ["cơ quan 1", "cơ quan 2"],
876
+ "procedures": ["thủ tục 1", "thủ tục 2"]
877
+ }},
878
+ "keywords": {{
879
+ "legal_terms": ["thuật ngữ 1", "thuật ngữ 2"],
880
+ "search_keywords": ["từ khóa 1", "từ khóa 2"],
881
+ "legal_phrases": ["cụm từ 1", "cụm từ 2"]
882
+ }},
883
+ "refined_question": "câu hỏi được cải thiện",
884
+ "confidence_score": 0.95,
885
+ "reasoning": "lý do tại sao câu hỏi này tốt hơn"
886
+ }}
887
+
888
+ Chỉ trả về JSON hợp lệ:""",
889
+ input_variables=["question", "contexts", "complexity", "ambiguity"]
890
+ )
891
+
892
+ try:
893
+ response = self.llm.invoke(prompt.format(
894
+ question=question,
895
+ contexts=", ".join(contexts),
896
+ complexity=intent_analysis.get("complexity", "complex"),
897
+ ambiguity=intent_analysis.get("ambiguity_level", "high")
898
+ ))
899
+
900
+ result = json.loads(response.content.strip())
901
+ refined_question = result.get("refined_question", "")
902
+ confidence = result.get("confidence_score", 0.0)
903
+
904
+ # Only return if confidence is high enough
905
+ if refined_question and confidence > Config.MIN_CONFIDENCE_SCORE:
906
+ return refined_question
907
+
908
+ except Exception as e:
909
+ print(f"Error in chain-of-thought refinement: {e}")
910
+
911
+ return None
912
+
913
+ def _llm_iterative_refinement(self, question: str, contexts: List[str], max_iterations: int = 3) -> Optional[str]:
914
+ """Use iterative refinement to progressively improve the question"""
915
+ if not self.llm:
916
+ return None
917
+
918
+ current_question = question
919
+
920
+ for iteration in range(max_iterations):
921
+ prompt = PromptTemplate(
922
+ template="""Bạn là chuyên gia cải thiện câu hỏi pháp lý. Đây là lần cải thiện thứ {iteration} của câu hỏi.
923
+
924
+ Câu hỏi hiện tại: {current_question}
925
+ Lĩnh vực pháp lý: {contexts}
926
+
927
+ Hãy phân tích và cải thiện thêm câu hỏi theo các tiêu chí:
928
+
929
+ LẦN 1: Tập trung vào thuật ngữ pháp lý và cấu trúc câu
930
+ LẦN 2: Tập trung vào ngữ cảnh và từ khóa tìm kiếm
931
+ LẦN 3: Tập trung vào tính rõ ràng và độ chính xác
932
+
933
+ Nguyên tắc cải thiện:
934
+ 1. Mỗi lần cải thiện phải có tiến bộ rõ rệt
935
+ 2. Giữ nguyên ý nghĩa gốc
936
+ 3. Tăng cường khả năng tìm kiếm
937
+ 4. Sử dụng thuật ngữ chuẩn mực
938
+
939
+ Trả về định dạng JSON:
940
+ {{
941
+ "improved_question": "câu hỏi được cải thiện",
942
+ "improvements_made": ["cải thiện 1", "cải thiện 2"],
943
+ "quality_score": 0.85,
944
+ "needs_further_improvement": true/false
945
+ }}
946
+
947
+ Chỉ trả về JSON:""",
948
+ input_variables=["current_question", "contexts", "iteration"]
949
+ )
950
+
951
+ try:
952
+ response = self.llm.invoke(prompt.format(
953
+ current_question=current_question,
954
+ contexts=", ".join(contexts),
955
+ iteration=iteration + 1
956
+ ))
957
+
958
+ result = json.loads(response.content.strip())
959
+ improved_question = result.get("improved_question", "")
960
+ quality_score = result.get("quality_score", 0.0)
961
+ needs_improvement = result.get("needs_further_improvement", False)
962
+
963
+ if improved_question and improved_question != current_question:
964
+ current_question = improved_question
965
+
966
+ # Stop if quality is high enough or no further improvement needed
967
+ if quality_score > 0.9 or not needs_improvement:
968
+ break
969
+ else:
970
+ break
971
+
972
+ except Exception as e:
973
+ print(f"Error in iterative refinement iteration {iteration + 1}: {e}")
974
+ break
975
+
976
+ return current_question if current_question != question else None
utils/text_processor.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import pandas as pd
3
+ from typing import List, Set
4
+ from underthesea import word_tokenize
5
+ from config import Config
6
+
7
+
8
+ class VietnameseTextProcessor:
9
+ """Vietnamese text processing utilities for legal documents"""
10
+
11
+ def __init__(self):
12
+ self.stopwords = self._load_stopwords()
13
+
14
+ def _load_stopwords(self) -> Set[str]:
15
+ """Load Vietnamese stopwords from file"""
16
+ try:
17
+ # Try UTF-8 first
18
+ with open(Config.STOPWORDS_PATH, "r", encoding="utf-8") as f:
19
+ stopwords = set(line.strip() for line in f if line.strip())
20
+ return stopwords
21
+ except UnicodeDecodeError:
22
+ try:
23
+ # Try UTF-16 if UTF-8 fails
24
+ with open(Config.STOPWORDS_PATH, "r", encoding="utf-16") as f:
25
+ stopwords = set(line.strip() for line in f if line.strip())
26
+ return stopwords
27
+ except UnicodeDecodeError:
28
+ try:
29
+ # Try with BOM detection
30
+ with open(Config.STOPWORDS_PATH, "r", encoding="utf-8-sig") as f:
31
+ stopwords = set(line.strip() for line in f if line.strip())
32
+ return stopwords
33
+ except UnicodeDecodeError:
34
+ print(
35
+ f"Warning: Unable to decode stopwords file at {Config.STOPWORDS_PATH}"
36
+ )
37
+ return set()
38
+ except FileNotFoundError:
39
+ print(f"Warning: Stopwords file not found at {Config.STOPWORDS_PATH}")
40
+ return set()
41
+ except Exception as e:
42
+ print(f"Warning: Error loading stopwords file: {e}")
43
+ return set()
44
+
45
+ def clean_text(self, text: str) -> str:
46
+ """Clean Vietnamese text for processing"""
47
+ if not text:
48
+ return ""
49
+
50
+ # Remove extra whitespace and normalize
51
+ text = re.sub(r"\s+", " ", text.strip())
52
+
53
+ # Remove special characters but keep Vietnamese characters
54
+ text = re.sub(
55
+ r"[^\w\s\-\.\,\;\:\!\?\(\)\[\]\"\'àáảãạăắằẳẵặâấầẩẫậèéẻẽẹêếềểễệìíỉĩịòóỏõọôốồổỗộơớờởỡợùúủũụưứừửữựỳýỷỹỵđĐ]",
56
+ " ",
57
+ text,
58
+ )
59
+
60
+ # Remove multiple spaces
61
+ text = re.sub(r"\s+", " ", text.strip())
62
+
63
+ return text
64
+
65
+ def tokenize(self, text: str) -> List[str]:
66
+ """Tokenize Vietnamese text using underthesea"""
67
+ try:
68
+ cleaned_text = self.clean_text(text)
69
+ tokens = word_tokenize(cleaned_text, format="text").split()
70
+ return tokens
71
+ except Exception as e:
72
+ print(f"Error tokenizing text: {e}")
73
+ return text.split()
74
+
75
+ def remove_stopwords(self, tokens: List[str]) -> List[str]:
76
+ """Remove stopwords from token list"""
77
+ return [token for token in tokens if token.lower() not in self.stopwords]
78
+
79
+ def preprocess_for_search(self, text: str) -> str:
80
+ """Preprocess text for search - tokenize and remove stopwords"""
81
+ tokens = self.tokenize(text)
82
+ filtered_tokens = self.remove_stopwords(tokens)
83
+ return " ".join(filtered_tokens)
84
+
85
+ def extract_keywords(self, text: str, min_length: int = 2) -> List[str]:
86
+ """Extract keywords from text"""
87
+ tokens = self.tokenize(text)
88
+ filtered_tokens = self.remove_stopwords(tokens)
89
+ keywords = [token for token in filtered_tokens if len(token) >= min_length]
90
+ return list(set(keywords)) # Remove duplicates
91
+
92
+ def chunk_text(
93
+ self, text: str, chunk_size: int = None, overlap: int = None
94
+ ) -> List[str]:
95
+ """Split text into chunks with overlap"""
96
+ if chunk_size is None:
97
+ chunk_size = Config.CHUNK_SIZE
98
+ if overlap is None:
99
+ overlap = Config.CHUNK_OVERLAP
100
+
101
+ tokens = self.tokenize(text)
102
+ chunks = []
103
+
104
+ for i in range(0, len(tokens), chunk_size - overlap):
105
+ chunk_tokens = tokens[i : i + chunk_size]
106
+ if chunk_tokens:
107
+ chunks.append(" ".join(chunk_tokens))
108
+
109
+ return chunks