Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-rag/src/lexigram/ai/rag/routing/analyzer.py: 21%

81 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

1"""Query analyzer for extracting features from queries.""" 

2 

3from __future__ import annotations 

4 

5import re 

6 

7from lexigram.ai.rag.multimodal.types import Modality 

8from lexigram.ai.rag.routing.types import QueryFeatures, QueryIntent 

9 

10 

11class QueryAnalyzer: 

12 """Analyzes queries to extract features for routing decisions. 

13 

14 Extracts various features from queries including: 

15 - Basic features: length, keywords 

16 - Intent classification: factual, conversational, analytical, etc. 

17 - Language detection 

18 - Domain classification 

19 - Entity detection 

20 - Modality detection 

21 

22 Example: 

23 ```python 

24 analyzer = QueryAnalyzer() 

25 features = await analyzer.analyze("How do I configure authentication?") 

26 logger.info(f"Intent: {features.intent}") 

27 logger.info(f"Keywords: {features.keywords}") 

28 logger.info(f"Complexity: {features.complexity}") 

29 ``` 

30 """ 

31 

32 # Intent patterns (simple keyword-based classification) 

33 INTENT_PATTERNS: dict[QueryIntent, list[str]] = { 

34 QueryIntent.FACTUAL: [ 

35 r"\bwhat\b", 

36 r"\bwho\b", 

37 r"\bwhen\b", 

38 r"\bwhere\b", 

39 r"\bdefine\b", 

40 r"\bexplain\b", 

41 r"\btell me\b", 

42 ], 

43 QueryIntent.PROCEDURAL: [ 

44 r"\bhow\b", 

45 r"\bsteps\b", 

46 r"\bguide\b", 

47 r"\btutorial\b", 

48 r"\bconfigure\b", 

49 r"\bsetup\b", 

50 r"\binstall\b", 

51 ], 

52 QueryIntent.ANALYTICAL: [ 

53 r"\bcompare\b", 

54 r"\bdifference\b", 

55 r"\bversus\b", 

56 r"\bvs\b", 

57 r"\banalyze\b", 

58 r"\bevaluate\b", 

59 r"\bpros and cons\b", 

60 ], 

61 QueryIntent.CREATIVE: [ 

62 r"\bwrite\b", 

63 r"\bcreate\b", 

64 r"\bgenerate\b", 

65 r"\bcompose\b", 

66 r"\bpoem\b", 

67 r"\bstory\b", 

68 r"\bideas\b", 

69 ], 

70 QueryIntent.NAVIGATIONAL: [ 

71 r"\bfind\b", 

72 r"\blocate\b", 

73 r"\bpage\b", 

74 r"\bdocumentation\b", 

75 r"\breference\b", 

76 r"\blink\b", 

77 ], 

78 QueryIntent.CONVERSATIONAL: [ 

79 r"\bhello\b", 

80 r"\bhi\b", 

81 r"\bthanks\b", 

82 r"\bthank you\b", 

83 r"\bhow are you\b", 

84 r"\bbye\b", 

85 ], 

86 } 

87 

88 # Domain keywords 

89 DOMAIN_KEYWORDS: dict[str, list[str]] = { 

90 "technical": [ 

91 "api", 

92 "code", 

93 "function", 

94 "class", 

95 "method", 

96 "algorithm", 

97 "database", 

98 "server", 

99 "configuration", 

100 "deploy", 

101 "debug", 

102 ], 

103 "medical": [ 

104 "patient", 

105 "disease", 

106 "treatment", 

107 "symptom", 

108 "diagnosis", 

109 "medication", 

110 "doctor", 

111 "hospital", 

112 "clinical", 

113 ], 

114 "legal": [ 

115 "law", 

116 "legal", 

117 "contract", 

118 "regulation", 

119 "compliance", 

120 "statute", 

121 "court", 

122 "litigation", 

123 "rights", 

124 ], 

125 "financial": [ 

126 "money", 

127 "investment", 

128 "stock", 

129 "profit", 

130 "revenue", 

131 "cost", 

132 "budget", 

133 "finance", 

134 "accounting", 

135 ], 

136 } 

137 

138 # Stop words to exclude from keywords 

139 STOP_WORDS: set[str] = { 

140 "a", 

141 "an", 

142 "and", 

143 "are", 

144 "as", 

145 "at", 

146 "be", 

147 "by", 

148 "for", 

149 "from", 

150 "has", 

151 "he", 

152 "in", 

153 "is", 

154 "it", 

155 "its", 

156 "of", 

157 "on", 

158 "that", 

159 "the", 

160 "to", 

161 "was", 

162 "will", 

163 "with", 

164 "this", 

165 "but", 

166 "they", 

167 "have", 

168 "had", 

169 "what", 

170 "when", 

171 "where", 

172 "who", 

173 "which", 

174 "why", 

175 "how", 

176 "or", 

177 "can", 

178 "could", 

179 "should", 

180 "would", 

181 } 

182 

183 def __init__( 

184 self, 

185 *, 

186 extract_keywords: bool = True, 

187 detect_entities: bool = True, 

188 classify_domain: bool = True, 

189 ): 

190 """Initialize the query analyzer. 

191 

192 Args: 

193 extract_keywords: Whether to extract keywords. 

194 detect_entities: Whether to detect named entities. 

195 classify_domain: Whether to classify query domain. 

196 """ 

197 self.extract_keywords = extract_keywords 

198 self.detect_entities = detect_entities 

199 self.classify_domain = classify_domain 

200 

201 async def analyze(self, query: str) -> QueryFeatures: 

202 """Analyze a query and extract features. 

203 

204 Args: 

205 query: Query text to analyze. 

206 

207 Returns: 

208 Extracted query features. 

209 """ 

210 # Basic features 

211 length = len(query) 

212 text_lower = query.lower() 

213 

214 # Classify intent 

215 intent = self._classify_intent(text_lower) 

216 

217 # Extract keywords 

218 keywords = self._extract_keywords(query) if self.extract_keywords else [] 

219 

220 # Detect language (simple heuristic) 

221 language = self._detect_language(query) 

222 

223 # Classify domain 

224 domain = self._classify_domain(text_lower) if self.classify_domain else None 

225 

226 # Detect entities (simple pattern-based) 

227 has_entities = self._detect_entities(query) if self.detect_entities else False 

228 

229 # Detect modalities 

230 modalities = self._detect_modalities(text_lower) 

231 

232 # Calculate complexity 

233 complexity = self._calculate_complexity(query, keywords) 

234 

235 return QueryFeatures( 

236 text=query, 

237 length=length, 

238 intent=intent, 

239 language=language, 

240 domain=domain, 

241 keywords=keywords, 

242 has_entities=has_entities, 

243 modalities=modalities, 

244 complexity=complexity, 

245 ) 

246 

247 def _classify_intent(self, query_lower: str) -> QueryIntent: 

248 """Classify query intent using pattern matching. 

249 

250 Args: 

251 query_lower: Lowercased query text. 

252 

253 Returns: 

254 Classified intent. 

255 """ 

256 scores: dict[QueryIntent, int] = {} 

257 

258 for intent, patterns in self.INTENT_PATTERNS.items(): 

259 score = sum( 

260 1 

261 for pattern in patterns 

262 if re.search(pattern, query_lower, re.IGNORECASE) 

263 ) 

264 if score > 0: 

265 scores[intent] = score 

266 

267 # Return intent with highest score, default to FACTUAL 

268 if scores: 

269 return max(scores.items(), key=lambda x: x[1])[0] 

270 return QueryIntent.FACTUAL 

271 

272 def _extract_keywords(self, query: str) -> list[str]: 

273 """Extract keywords from query. 

274 

275 Args: 

276 query: Query text. 

277 

278 Returns: 

279 List of extracted keywords. 

280 """ 

281 # Simple word extraction (split on whitespace and punctuation) 

282 words = re.findall(r"\b\w+\b", query.lower()) 

283 

284 # Filter out stop words and short words 

285 keywords = [ 

286 word for word in words if word not in self.STOP_WORDS and len(word) > 2 

287 ] 

288 

289 # Remove duplicates while preserving order 

290 seen = set() 

291 unique_keywords = [] 

292 for keyword in keywords: 

293 if keyword not in seen: 

294 seen.add(keyword) 

295 unique_keywords.append(keyword) 

296 

297 return unique_keywords[:10] # Limit to top 10 keywords 

298 

299 def _detect_language(self, query: str) -> str: 

300 """Detect query language (simple heuristic). 

301 

302 Args: 

303 query: Query text. 

304 

305 Returns: 

306 Language code (default: 'en'). 

307 """ 

308 # Simple heuristic: check for non-ASCII characters 

309 # In production, use langdetect or similar library 

310 if any(ord(char) > 127 for char in query): 

311 # Non-ASCII detected, could be non-English 

312 # For now, still return 'en' as default 

313 return "en" 

314 return "en" 

315 

316 def _classify_domain(self, query_lower: str) -> str | None: 

317 """Classify query domain based on keywords. 

318 

319 Args: 

320 query_lower: Lowercased query text. 

321 

322 Returns: 

323 Domain classification or None if no match. 

324 """ 

325 scores: dict[str, int] = {} 

326 

327 for domain, keywords in self.DOMAIN_KEYWORDS.items(): 

328 score = sum(1 for keyword in keywords if keyword in query_lower) 

329 if score > 0: 

330 scores[domain] = score 

331 

332 # Return domain with highest score if above threshold 

333 if scores: 

334 max_domain = max(scores.items(), key=lambda x: x[1]) 

335 if max_domain[1] >= 2: # Require at least 2 matching keywords 

336 return max_domain[0] 

337 

338 return None 

339 

340 def _detect_entities(self, query: str) -> bool: 

341 """Detect if query contains named entities. 

342 

343 Args: 

344 query: Query text. 

345 

346 Returns: 

347 True if entities detected, False otherwise. 

348 """ 

349 # Simple pattern-based detection 

350 # Look for capitalized words (potential proper nouns) 

351 capitalized_words = re.findall(r"\b[A-Z][a-z]+\b", query) 

352 

353 # Filter out common sentence starters 

354 sentence_starters = {"What", "When", "Where", "Who", "Why", "How", "Which"} 

355 entities = list( 

356 filter(lambda word: word not in sentence_starters, capitalized_words), 

357 ) 

358 

359 return len(entities) > 0 

360 

361 def _detect_modalities(self, query_lower: str) -> list[Modality]: 

362 """Detect modalities mentioned in the query. 

363 

364 Args: 

365 query_lower: Lowercased query text. 

366 

367 Returns: 

368 List of detected modalities. 

369 """ 

370 modalities = [Modality.TEXT] # Always include text 

371 

372 # Check for image-related terms 

373 image_terms = [ 

374 "image", 

375 "picture", 

376 "photo", 

377 "visual", 

378 "diagram", 

379 "chart", 

380 "graph", 

381 ] 

382 if any(term in query_lower for term in image_terms): 

383 modalities.append(Modality.IMAGE) 

384 

385 # Check for audio-related terms 

386 audio_terms = ["audio", "sound", "music", "voice", "recording", "podcast"] 

387 if any(term in query_lower for term in audio_terms): 

388 modalities.append(Modality.AUDIO) 

389 

390 # Check for video-related terms 

391 video_terms = ["video", "movie", "clip", "footage", "film"] 

392 if any(term in query_lower for term in video_terms): 

393 modalities.append(Modality.VIDEO) 

394 

395 return modalities 

396 

397 def _calculate_complexity(self, query: str, keywords: list[str]) -> float: 

398 """Calculate query complexity score. 

399 

400 Args: 

401 query: Query text. 

402 keywords: Extracted keywords. 

403 

404 Returns: 

405 Complexity score (0-1). 

406 """ 

407 # Factors: 

408 # 1. Query length (longer = more complex) 

409 # 2. Number of keywords (more = more complex) 

410 # 3. Presence of complex punctuation 

411 # 4. Number of clauses (commas, semicolons) 

412 

413 # Length score (0-1, max at 500 chars) 

414 length_score = min(len(query) / 500, 1.0) 

415 

416 # Keyword score (0-1, max at 15 keywords) 

417 keyword_score = min(len(keywords) / 15, 1.0) 

418 

419 # Punctuation complexity (0-1) 

420 complex_punctuation = query.count(",") + query.count(";") + query.count(":") 

421 punctuation_score = min(complex_punctuation / 5, 1.0) 

422 

423 # Average the scores 

424 complexity = (length_score + keyword_score + punctuation_score) / 3 

425 

426 return round(complexity, 2)