Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-llm/src/lexigram/ai/llm/selection/core.py: 97%

110 statements  

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

1"""Model selection and routing for intelligent LLM usage. 

2 

3This module provides intelligent model selection based on task characteristics, 

4allowing you to optimize for cost, quality, and latency. 

5 

6Example: 

7 >>> from lexigram.ai.llm import ModelSelector, SelectionStrategy 

8 >>> 

9 >>> selector = ModelSelector( 

10 ... default_model="gpt-3.5-turbo", 

11 ... strategies=[ 

12 ... SelectionStrategy( 

13 ... name="complex", 

14 ... model="gpt-4-turbo", 

15 ... conditions={"min_tokens": 1000} 

16 ... ) 

17 ... ], 

18 ... fallback_chain=["gpt-4-turbo", "gpt-3.5-turbo", "ollama/llama3"] 

19 ... ) 

20 >>> 

21 >>> # Automatically selects appropriate model 

22 >>> model = selector.select("Write a complex analysis...") 

23""" 

24 

25from __future__ import annotations 

26 

27from dataclasses import dataclass 

28from enum import StrEnum 

29from typing import Any 

30 

31from lexigram.ai.llm.selection._capabilities import ( 

32 DEFAULT_MODEL_CAPABILITIES, 

33 ModelCapabilities, 

34) 

35from lexigram.contracts.ai.llm import TokenCounterProtocol 

36from lexigram.domain import DomainModel 

37from lexigram.validation import Field 

38 

39__all__ = [ 

40 "DEFAULT_MODEL_CAPABILITIES", 

41 "ModelCapabilities", 

42 "ModelSelector", 

43 "SelectionCriteria", 

44 "SelectionStrategy", 

45 "create_balanced_selector", 

46 "create_cost_optimized_selector", 

47 "create_quality_optimized_selector", 

48] 

49 

50 

51class SelectionCriteria(StrEnum): 

52 """Criteria for model selection.""" 

53 

54 TOKEN_COUNT = "token_count" # noqa: S105 # criterion name, not a credential 

55 COST = "cost" 

56 LATENCY = "latency" 

57 QUALITY = "quality" 

58 CUSTOM = "custom" 

59 

60 

61@dataclass(init=False) 

62class SelectionStrategy(DomainModel): 

63 """Strategy for selecting models based on conditions. 

64 

65 Example: 

66 >>> strategy = SelectionStrategy( 

67 ... name="long_context", 

68 ... model="gpt-4-turbo-preview", 

69 ... conditions={ 

70 ... "min_tokens": 2000, 

71 ... "max_tokens": 100000 

72 ... } 

73 ... ) 

74 """ 

75 

76 name: str = Field(..., description="Strategy name") 

77 model: str = Field(..., description="Model to use for this strategy") 

78 conditions: dict[str, Any] = Field( 

79 default_factory=dict, 

80 description="Conditions that trigger this strategy", 

81 ) 

82 priority: int = Field( 

83 default=0, 

84 description="Priority (higher = evaluated first)", 

85 ) 

86 description: str | None = Field( 

87 None, 

88 description="Human-readable description", 

89 ) 

90 

91 def matches(self, context: dict[str, Any]) -> bool: 

92 """Check if this strategy matches the given context. 

93 

94 Args: 

95 context: Context dictionary with prompt info 

96 

97 Returns: 

98 True if all conditions are met 

99 

100 Example: 

101 >>> context = {"token_count": 2500, "has_code": True} 

102 >>> strategy.matches(context) 

103 True 

104 """ 

105 for key, value in self.conditions.items(): 

106 # Handle different condition types 

107 if key.startswith("min_"): 

108 actual_key = key[4:] # Remove "min_" prefix 

109 if actual_key not in context: 

110 return False 

111 if context[actual_key] < value: 

112 return False 

113 elif key.startswith("max_"): 

114 actual_key = key[4:] # Remove "max_" prefix 

115 if actual_key not in context: 

116 return False 

117 if context[actual_key] > value: 

118 return False 

119 elif key.startswith("has_"): 

120 # Boolean flag check 

121 if key not in context: 

122 return False 

123 if context[key] != value: 

124 return False 

125 else: 

126 # Exact match 

127 if key not in context: 

128 return False 

129 if context[key] != value: 

130 return False 

131 

132 return True 

133 

134 

135class ModelSelector: 

136 """Intelligent model selector with fallback support. 

137 

138 Automatically selects the best model based on prompt characteristics 

139 and provides fallback chains for reliability. 

140 

141 Example: 

142 >>> selector = ModelSelector( 

143 ... default_model="gpt-3.5-turbo", 

144 ... strategies=[ 

145 ... SelectionStrategy( 

146 ... name="complex", 

147 ... model="gpt-4-turbo", 

148 ... conditions={"min_tokens": 1000} 

149 ... ), 

150 ... SelectionStrategy( 

151 ... name="simple", 

152 ... model="claude-3-haiku-20240307", 

153 ... conditions={"max_tokens": 500} 

154 ... ) 

155 ... ], 

156 ... fallback_chain=["gpt-4-turbo", "gpt-3.5-turbo"] 

157 ... ) 

158 >>> 

159 >>> # Select model for a prompt 

160 >>> model = selector.select("Long prompt here...") 

161 >>> print(model) 

162 'gpt-4-turbo' 

163 >>> 

164 >>> # Get next fallback on error 

165 >>> fallback = selector.get_fallback("gpt-4-turbo") 

166 >>> print(fallback) 

167 'gpt-3.5-turbo' 

168 """ 

169 

170 def __init__( 

171 self, 

172 default_model: str | None = None, 

173 strategies: list[SelectionStrategy] | None = None, 

174 fallback_chain: list[str] | None = None, 

175 model_capabilities: dict[str, ModelCapabilities] | None = None, 

176 token_counter: TokenCounterProtocol | None = None, 

177 ): 

178 """Initialize model selector. 

179 

180 Args: 

181 default_model: Default model to use 

182 strategies: List of selection strategies 

183 fallback_chain: Ordered list of fallback models 

184 model_capabilities: Custom model capabilities 

185 token_counter: Token counter for prompt analysis 

186 

187 Example: 

188 >>> selector = ModelSelector( 

189 ... default_model="gpt-3.5-turbo", 

190 ... fallback_chain=["gpt-4", "claude-3-sonnet-20240229"] 

191 ... ) 

192 """ 

193 if default_model is None: 

194 default_model = "gpt-3.5-turbo" 

195 

196 self.default_model = default_model 

197 self.strategies = sorted( 

198 strategies or [], 

199 key=lambda s: s.priority, 

200 reverse=True, 

201 ) 

202 self.fallback_chain = fallback_chain or [default_model] 

203 self.model_capabilities = model_capabilities or DEFAULT_MODEL_CAPABILITIES 

204 self.token_counter = token_counter 

205 

206 def select( 

207 self, 

208 prompt: str, 

209 context: dict[str, Any] | None = None, 

210 required_capabilities: list[str] | None = None, 

211 ) -> str: 

212 """Select the best model for the given prompt. 

213 

214 Args: 

215 prompt: The prompt text 

216 context: Additional context for selection 

217 required_capabilities: Required capabilities (e.g., ["supports_functions"]) 

218 

219 Returns: 

220 Selected model name 

221 

222 Example: 

223 >>> model = selector.select( 

224 ... "Analyze this image...", 

225 ... required_capabilities=["supports_vision"] 

226 ... ) 

227 >>> print(model) 

228 'gpt-4-turbo' 

229 """ 

230 # Build context 

231 ctx = self._build_context(prompt, context or {}) 

232 

233 # Filter by required capabilities 

234 available_models = self._filter_by_capabilities(required_capabilities) 

235 

236 # Try each strategy in priority order 

237 for strategy in self.strategies: 

238 if strategy.model not in available_models: 

239 continue 

240 

241 if strategy.matches(ctx): 

242 return strategy.model 

243 

244 # Return default if no strategy matched 

245 return ( 

246 self.default_model 

247 if self.default_model in available_models 

248 else available_models[0] 

249 ) 

250 

251 def get_fallback(self, failed_model: str) -> str | None: 

252 """Get the next model in the fallback chain. 

253 

254 Args: 

255 failed_model: The model that failed 

256 

257 Returns: 

258 Next fallback model, or None if no fallback available 

259 

260 Example: 

261 >>> fallback = selector.get_fallback("gpt-4-turbo") 

262 >>> print(fallback) 

263 'gpt-3.5-turbo' 

264 """ 

265 try: 

266 idx = self.fallback_chain.index(failed_model) 

267 if idx + 1 < len(self.fallback_chain): 

268 return self.fallback_chain[idx + 1] 

269 except ValueError: 

270 # Model not in fallback chain, return first fallback 

271 if self.fallback_chain: 

272 return self.fallback_chain[0] 

273 

274 return None 

275 

276 def get_capabilities(self, model: str) -> ModelCapabilities | None: 

277 """Get capabilities for a model. 

278 

279 Args: 

280 model: Model name 

281 

282 Returns: 

283 Model capabilities or None if unknown 

284 

285 Example: 

286 >>> caps = selector.get_capabilities("gpt-4-turbo") 

287 >>> print(caps.max_tokens) 

288 128000 

289 """ 

290 return self.model_capabilities.get(model) 

291 

292 def estimate_cost( 

293 self, 

294 model: str, 

295 input_tokens: int, 

296 output_tokens: int, 

297 ) -> float: 

298 """Estimate cost for a model call. 

299 

300 Args: 

301 model: Model name 

302 input_tokens: Number of input tokens 

303 output_tokens: Number of output tokens 

304 

305 Returns: 

306 Estimated cost in USD 

307 

308 Example: 

309 >>> cost = selector.estimate_cost("gpt-4-turbo", 1000, 500) 

310 >>> print(f"${cost:.4f}") 

311 $0.0250 

312 """ 

313 caps = self.get_capabilities(model) 

314 if not caps: 

315 return 0.0 

316 

317 input_cost = (input_tokens / 1000) * caps.cost_per_1k_input 

318 output_cost = (output_tokens / 1000) * caps.cost_per_1k_output 

319 

320 return input_cost + output_cost 

321 

322 def _build_context(self, prompt: str, context: dict[str, Any]) -> dict[str, Any]: 

323 """Build context for strategy matching. 

324 

325 Args: 

326 prompt: The prompt text 

327 context: User-provided context 

328 

329 Returns: 

330 Complete context dictionary 

331 """ 

332 ctx = context.copy() 

333 

334 # Add token count if not provided 

335 if "tokens" not in ctx and "token_count" not in ctx: 

336 # Use simple estimation to avoid async call 

337 # Count tokens as roughly 4 characters per token 

338 estimated_tokens = len(prompt) // 4 + 1 

339 ctx["tokens"] = estimated_tokens 

340 ctx["token_count"] = estimated_tokens 

341 

342 # Detect prompt characteristics 

343 ctx["has_code"] = ctx.get( 

344 "has_code", 

345 "```" in prompt or "def " in prompt or "function " in prompt, 

346 ) 

347 ctx["has_url"] = ctx.get("has_url", "http://" in prompt or "https://" in prompt) 

348 ctx["is_question"] = ctx.get("is_question", "?" in prompt) 

349 ctx["prompt_length"] = len(prompt) 

350 

351 return ctx 

352 

353 def _filter_by_capabilities( 

354 self, 

355 required_capabilities: list[str] | None, 

356 ) -> list[str]: 

357 """Filter models by required capabilities. 

358 

359 Args: 

360 required_capabilities: List of required capability names 

361 

362 Returns: 

363 List of model names that meet requirements 

364 """ 

365 if not required_capabilities: 

366 return list(self.model_capabilities.keys()) 

367 

368 available = [] 

369 for model, caps in self.model_capabilities.items(): 

370 if all(getattr(caps, cap, False) for cap in required_capabilities): 

371 available.append(model) 

372 

373 return available 

374 

375 

376def create_cost_optimized_selector( 

377 budget_per_1k_tokens: float = 2.0, 

378) -> ModelSelector: 

379 """Create a cost-optimized model selector.""" 

380 from lexigram.ai.llm.selection._scoring import ( 

381 create_cost_optimized_selector as _create, 

382 ) 

383 

384 return _create(budget_per_1k_tokens=budget_per_1k_tokens) 

385 

386 

387def create_quality_optimized_selector() -> ModelSelector: 

388 """Create a quality-optimized model selector.""" 

389 from lexigram.ai.llm.selection._scoring import ( 

390 create_quality_optimized_selector as _create, 

391 ) 

392 

393 return _create() 

394 

395 

396def create_balanced_selector() -> ModelSelector: 

397 """Create a balanced model selector.""" 

398 from lexigram.ai.llm.selection._scoring import create_balanced_selector as _create 

399 

400 return _create()