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

77 statements  

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

1"""Semantic routing strategy using embeddings.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Callable 

6from dataclasses import dataclass, field 

7 

8import numpy as np 

9 

10from lexigram.ai.rag.routing.types import ( 

11 DataSource, 

12 DataSourceType, 

13 QueryFeatures, 

14 RoutingDecision, 

15) 

16 

17 

18@dataclass 

19class RoutingPattern: 

20 """A routing pattern for semantic routing. 

21 

22 Attributes: 

23 name: Unique identifier for the pattern. 

24 examples: Example queries that match this pattern. 

25 data_source_types: Preferred data source types for this pattern. 

26 strategy: Retrieval strategy to use for this pattern. 

27 description: Human-readable description. 

28 embedding: Pre-computed embedding of the pattern (computed from examples). 

29 """ 

30 

31 name: str 

32 examples: list[str] 

33 data_source_types: list[DataSourceType] 

34 strategy: str 

35 description: str = "" 

36 embedding: np.ndarray | None = field(default=None, repr=False) 

37 

38 

39class SemanticRouter: 

40 """Semantic routing strategy using embedding similarity. 

41 

42 Routes queries by comparing query embeddings to pre-defined routing 

43 patterns and selecting the most similar pattern. 

44 

45 Example: 

46 ```python 

47 from lexigram.ai.rag import SemanticRouter, RoutingPattern 

48 

49 router = SemanticRouter(embed_fn=my_embed_function) 

50 

51 # Add routing pattern 

52 router.add_pattern(RoutingPattern( 

53 name="technical_docs", 

54 examples=[ 

55 "How do I configure authentication?", 

56 "API reference for user management", 

57 "What are the deployment steps?" 

58 ], 

59 data_source_types=[DataSourceType.VECTOR_STORE], 

60 strategy="dense", 

61 description="Technical documentation queries" 

62 )) 

63 

64 # Route query 

65 decision = await router.route(features, available_sources) 

66 ``` 

67 """ 

68 

69 def __init__( 

70 self, 

71 *, 

72 embed_fn: Callable | None = None, 

73 similarity_threshold: float = 0.7, 

74 ): 

75 """Initialize the semantic router. 

76 

77 Args: 

78 embed_fn: Function to embed text (async callable). 

79 similarity_threshold: Minimum similarity for pattern matching. 

80 

81 Use `with_defaults()` classmethod to create a router with default patterns. 

82 """ 

83 self.embed_fn = embed_fn 

84 self.similarity_threshold = similarity_threshold 

85 self.patterns: list[RoutingPattern] = [] 

86 

87 @classmethod 

88 def with_defaults(cls, *args, **kwargs) -> SemanticRouter: 

89 """Create a router pre-populated with default routing patterns. 

90 

91 Args: 

92 *args: Positional arguments passed to __init__. 

93 **kwargs: Keyword arguments passed to __init__. 

94 

95 Returns: 

96 A SemanticRouter with all default patterns registered. 

97 """ 

98 instance = cls(*args, **kwargs) 

99 instance._load_default_patterns() 

100 return instance 

101 

102 async def add_pattern(self, pattern: RoutingPattern) -> None: 

103 """Add a routing pattern and compute its embedding. 

104 

105 Args: 

106 pattern: Routing pattern to add. 

107 """ 

108 # Compute pattern embedding (average of example embeddings) 

109 if self.embed_fn and pattern.embedding is None: 

110 example_embeddings = [] 

111 for example in pattern.examples: 

112 embedding = await self.embed_fn(example) 

113 example_embeddings.append(embedding) 

114 

115 # Average embeddings to create pattern embedding 

116 pattern.embedding = np.mean(example_embeddings, axis=0) 

117 

118 self.patterns.append(pattern) 

119 

120 def remove_pattern(self, name: str) -> bool: 

121 """Remove a routing pattern by name. 

122 

123 Args: 

124 name: Name of the pattern to remove. 

125 

126 Returns: 

127 True if pattern was found and removed, False otherwise. 

128 """ 

129 initial_count = len(self.patterns) 

130 self.patterns = list(filter(lambda p: p.name != name, self.patterns)) 

131 return len(self.patterns) < initial_count 

132 

133 async def route( 

134 self, 

135 features: QueryFeatures, 

136 available_sources: list[DataSource], 

137 ) -> RoutingDecision: 

138 """Route query using semantic similarity. 

139 

140 Args: 

141 features: Extracted query features. 

142 available_sources: List of available data sources. 

143 

144 Returns: 

145 Routing decision based on most similar pattern. 

146 """ 

147 # Need embedding function to route 

148 if not self.embed_fn: 

149 return self._fallback_routing( 

150 features, 

151 available_sources, 

152 "No embedding function configured", 

153 ) 

154 

155 # Compute query embedding 

156 try: 

157 query_embedding = await self.embed_fn(features.text) 

158 except (ConnectionError, TimeoutError, RuntimeError, ValueError, OSError) as e: 

159 return self._fallback_routing( 

160 features, 

161 available_sources, 

162 f"Failed to embed query: {e}", 

163 ) 

164 

165 # Find most similar pattern 

166 best_pattern = None 

167 best_similarity = -1.0 

168 

169 for pattern in self.patterns: 

170 if pattern.embedding is None: 

171 continue 

172 

173 # Compute cosine similarity 

174 similarity = self._cosine_similarity(query_embedding, pattern.embedding) 

175 

176 if similarity > best_similarity: 

177 best_similarity = similarity 

178 best_pattern = pattern 

179 

180 # Check if similarity meets threshold 

181 if best_pattern and best_similarity >= self.similarity_threshold: 

182 # Find matching data sources 

183 matching_sources = [ 

184 source 

185 for source in available_sources 

186 if source.type in best_pattern.data_source_types 

187 ] 

188 

189 if matching_sources: 

190 matching_sources.sort(key=lambda s: s.priority, reverse=True) 

191 

192 return RoutingDecision( 

193 query=features.text, 

194 data_sources=matching_sources, 

195 strategy=best_pattern.strategy, 

196 confidence=float(best_similarity), 

197 reasoning=f"Matched pattern: {best_pattern.description or best_pattern.name} (similarity: {best_similarity:.3f})", 

198 features=features, 

199 metadata={ 

200 "pattern": best_pattern.name, 

201 "similarity": float(best_similarity), 

202 }, 

203 ) 

204 

205 # Fallback if no pattern matched 

206 return self._fallback_routing( 

207 features, 

208 available_sources, 

209 f"No pattern above threshold {self.similarity_threshold} (best: {best_similarity:.3f})", 

210 ) 

211 

212 def _fallback_routing( 

213 self, 

214 features: QueryFeatures, 

215 available_sources: list[DataSource], 

216 reason: str, 

217 ) -> RoutingDecision: 

218 """Fallback routing when no pattern matches. 

219 

220 Args: 

221 features: Query features. 

222 available_sources: Available data sources. 

223 reason: Reason for fallback. 

224 

225 Returns: 

226 Fallback routing decision. 

227 """ 

228 if available_sources: 

229 # Prefer vector stores 

230 vector_stores = [ 

231 s for s in available_sources if s.type == DataSourceType.VECTOR_STORE 

232 ] 

233 

234 fallback_sources = vector_stores or available_sources 

235 fallback_sources.sort(key=lambda s: s.priority, reverse=True) 

236 

237 return RoutingDecision( 

238 query=features.text, 

239 data_sources=[fallback_sources[0]], 

240 strategy="dense", 

241 confidence=0.3, 

242 reasoning=f"Fallback routing: {reason}", 

243 features=features, 

244 metadata={"fallback": True, "reason": reason}, 

245 ) 

246 

247 return RoutingDecision( 

248 query=features.text, 

249 data_sources=[], 

250 strategy="none", 

251 confidence=0.0, 

252 reasoning=f"No sources available: {reason}", 

253 features=features, 

254 metadata={"error": "no_sources", "reason": reason}, 

255 ) 

256 

257 def _cosine_similarity(self, a: np.ndarray, b: np.ndarray) -> float: 

258 """Compute cosine similarity between two vectors. 

259 

260 Args: 

261 a: First vector. 

262 b: Second vector. 

263 

264 Returns: 

265 Cosine similarity (-1 to 1). 

266 """ 

267 dot_product = np.dot(a, b) 

268 norm_a = np.linalg.norm(a) 

269 norm_b = np.linalg.norm(b) 

270 

271 if norm_a == 0 or norm_b == 0: 

272 return 0.0 

273 

274 return float(dot_product / (norm_a * norm_b)) 

275 

276 def _load_default_patterns(self) -> None: 

277 """Load default routing patterns. 

278 

279 Note: Embeddings will be computed when embed_fn is available. 

280 """ 

281 # Pattern 1: Technical documentation 

282 self.patterns.append( 

283 RoutingPattern( 

284 name="technical_docs", 

285 examples=[ 

286 "How do I configure authentication?", 

287 "API reference for user management", 

288 "What are the deployment steps?", 

289 "How to setup the database connection?", 

290 ], 

291 data_source_types=[DataSourceType.VECTOR_STORE], 

292 strategy="dense", 

293 description="Technical documentation queries", 

294 ), 

295 ) 

296 

297 # Pattern 2: General Q&A 

298 self.patterns.append( 

299 RoutingPattern( 

300 name="general_qa", 

301 examples=[ 

302 "What is the capital of France?", 

303 "Who invented the telephone?", 

304 "When did World War II end?", 

305 "What is photosynthesis?", 

306 ], 

307 data_source_types=[DataSourceType.VECTOR_STORE], 

308 strategy="dense", 

309 description="General knowledge questions", 

310 ), 

311 ) 

312 

313 # Pattern 3: Multi-modal content 

314 self.patterns.append( 

315 RoutingPattern( 

316 name="multimodal_content", 

317 examples=[ 

318 "Show me images of sunset beaches", 

319 "Find videos about machine learning", 

320 "Pictures of classic cars", 

321 "Audio recordings of bird songs", 

322 ], 

323 data_source_types=[DataSourceType.MULTIMODAL_STORE], 

324 strategy="multimodal", 

325 description="Multi-modal content queries", 

326 ), 

327 ) 

328 

329 # Pattern 4: Data analysis 

330 self.patterns.append( 

331 RoutingPattern( 

332 name="data_analysis", 

333 examples=[ 

334 "What is the average sales by region?", 

335 "Count total orders in Q4", 

336 "Show revenue trends over time", 

337 "Compare performance metrics", 

338 ], 

339 data_source_types=[ 

340 DataSourceType.SQL_DATABASE, 

341 DataSourceType.VECTOR_STORE, 

342 ], 

343 strategy="structured", 

344 description="Data analysis and aggregation queries", 

345 ), 

346 ) 

347 

348 # Pattern 5: Navigation and search 

349 self.patterns.append( 

350 RoutingPattern( 

351 name="navigation", 

352 examples=[ 

353 "Find the pricing page", 

354 "Locate documentation for API", 

355 "Where is the user guide?", 

356 "Link to terms of service", 

357 ], 

358 data_source_types=[DataSourceType.KEYWORD_INDEX], 

359 strategy="sparse", 

360 description="Navigation and page finding queries", 

361 ), 

362 )