Coverage for src / osiris_cli / errors.py: 0%

115 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-31 05:01 +0200

1""" 

2Custom exception hierarchy for Osiris CLI 

3 

4Provides context-rich error messages with recovery suggestions 

5and proper error categorization for better debugging. 

6""" 

7 

8from typing import Optional, Dict, Any 

9 

10 

11class OsirisError(Exception): 

12 """ 

13 Base exception for all Osiris CLI errors. 

14  

15 All Osiris exceptions should inherit from this class. 

16 Provides standard error formatting and context. 

17 """ 

18 

19 def __init__( 

20 self, 

21 message: str, 

22 details: Optional[Dict[str, Any]] = None, 

23 recovery_suggestion: Optional[str] = None 

24 ): 

25 self.message = message 

26 self.details = details or {} 

27 self.recovery_suggestion = recovery_suggestion 

28 super().__init__(self.message) 

29 

30 def __str__(self): 

31 error_str = f"{self.__class__.__name__}: {self.message}" 

32 if self.details: 

33 error_str += f"\nDetails: {self.details}" 

34 if self.recovery_suggestion: 

35 error_str += f"\nSuggestion: {self.recovery_suggestion}" 

36 return error_str 

37 

38 

39# Configuration Errors 

40class ConfigError(OsirisError): 

41 """Configuration-related errors (missing keys, invalid settings, etc.)""" 

42 pass 

43 

44 

45class ConfigFileError(ConfigError): 

46 """Error reading or writing configuration files""" 

47 pass 

48 

49 

50class APIKeyError(ConfigError): 

51 """Missing or invalid API key""" 

52 

53 def __init__(self, provider: str, **kwargs): 

54 super().__init__( 

55 message=f"API key missing or invalid for provider: {provider}", 

56 recovery_suggestion=f"Set {provider.upper()}_API_KEY environment variable or configure via 'osiris enhanced quick-start'", 

57 **kwargs 

58 ) 

59 

60 

61# API Errors 

62class APIError(OsirisError): 

63 """Base class for API-related errors""" 

64 pass 

65 

66 

67class APITimeoutError(APIError): 

68 """API request timed out""" 

69 

70 def __init__(self, provider: str, timeout: int, **kwargs): 

71 super().__init__( 

72 message=f"Request to {provider} timed out after {timeout}s", 

73 recovery_suggestion="Try again or increase timeout in settings", 

74 **kwargs 

75 ) 

76 

77 

78class APIRateLimitError(APIError): 

79 """API rate limit exceeded""" 

80 

81 def __init__(self, provider: str, retry_after: Optional[int] = None, **kwargs): 

82 message = f"Rate limit exceeded for {provider}" 

83 if retry_after: 

84 message += f", retry after {retry_after}s" 

85 

86 super().__init__( 

87 message=message, 

88 recovery_suggestion="Wait a moment and try again, or use a different provider", 

89 **kwargs 

90 ) 

91 

92 

93class APIAuthenticationError(APIError): 

94 """API authentication failed (401, 403)""" 

95 

96 def __init__(self, provider: str, status_code: int, **kwargs): 

97 super().__init__( 

98 message=f"Unauthorized ({status_code}) for {provider}", 

99 recovery_suggestion=f"Check your {provider.upper()}_API_KEY is valid", 

100 **kwargs 

101 ) 

102 

103 

104class APIInsufficientCreditsError(APIError): 

105 """API insufficient credits (402)""" 

106 

107 def __init__(self, provider: str, **kwargs): 

108 super().__init__( 

109 message=f"Insufficient credits (402) for {provider}", 

110 recovery_suggestion=f"Add credits to your {provider} account or switch providers", 

111 **kwargs 

112 ) 

113 

114 

115class APIModelNotFoundError(APIError): 

116 """Requested model not found (404)""" 

117 

118 def __init__(self, provider: str, model: str, **kwargs): 

119 super().__init__( 

120 message=f"Model not found (404) for {provider}: {model}", 

121 recovery_suggestion="Check model name or try a different model", 

122 **kwargs 

123 ) 

124 

125 

126class APIServerError(APIError): 

127 """API server error (5xx)""" 

128 

129 def __init__(self, provider: str, status_code: int, **kwargs): 

130 super().__init__( 

131 message=f"Server error from {provider} (HTTP {status_code})", 

132 recovery_suggestion="Provider is having issues, try again later or use a different provider", 

133 **kwargs 

134 ) 

135 

136 

137# Tool Errors 

138class ToolError(OsirisError): 

139 """Base class for tool execution errors""" 

140 pass 

141 

142 

143class ToolNotFoundError(ToolError): 

144 """Requested tool not found in registry""" 

145 

146 def __init__(self, tool_name: str, **kwargs): 

147 super().__init__( 

148 message=f"Tool '{tool_name}' not found", 

149 recovery_suggestion="Use /tools to see available tools", 

150 **kwargs 

151 ) 

152 

153 

154class ToolExecutionError(ToolError): 

155 """Tool execution failed""" 

156 

157 def __init__(self, tool_name: str, error: str, **kwargs): 

158 super().__init__( 

159 message=f"Tool '{tool_name}' failed: {error}", 

160 **kwargs 

161 ) 

162 

163 

164class ToolPermissionError(ToolError): 

165 """Tool execution blocked by permissions""" 

166 

167 def __init__(self, tool_name: str, reason: str, **kwargs): 

168 super().__init__( 

169 message=f"Tool '{tool_name}' blocked: {reason}", 

170 recovery_suggestion="Enable YOLO mode or add to auto-allow list", 

171 **kwargs 

172 ) 

173 

174 

175# Session Errors 

176class SessionError(OsirisError): 

177 """Base class for session-related errors""" 

178 pass 

179 

180 

181class SessionNotFoundError(SessionError): 

182 """Session file not found""" 

183 

184 def __init__(self, session_id: str, **kwargs): 

185 super().__init__( 

186 message=f"Session '{session_id}' not found", 

187 recovery_suggestion="Use /session list to see available sessions", 

188 **kwargs 

189 ) 

190 

191 

192class SessionCorruptedError(SessionError): 

193 """Session file corrupted or invalid""" 

194 

195 def __init__(self, session_id: str, **kwargs): 

196 super().__init__( 

197 message=f"Session '{session_id}' is corrupted", 

198 recovery_suggestion="Start a new session", 

199 **kwargs 

200 ) 

201 

202 

203# Context Errors 

204class ContextError(OsirisError): 

205 """Base class for context-related errors""" 

206 pass 

207 

208 

209class ContextFileTooLargeError(ContextError): 

210 """Context file exceeds size limit""" 

211 

212 def __init__(self, file_path: str, size_mb: float, limit_mb: float, **kwargs): 

213 super().__init__( 

214 message=f"File '{file_path}' is too large ({size_mb:.1f}MB > {limit_mb}MB)", 

215 recovery_suggestion="Use /context to add specific sections or increase limit in settings", 

216 **kwargs 

217 ) 

218 

219 

220class ContextFileNotFoundError(ContextError): 

221 """Context file not found""" 

222 

223 def __init__(self, file_path: str, **kwargs): 

224 super().__init__( 

225 message=f"File '{file_path}' not found", 

226 **kwargs 

227 ) 

228 

229 

230# Database Errors 

231class DatabaseError(OsirisError): 

232 """Base class for database-related errors""" 

233 pass 

234 

235 

236class DatabaseConnectionError(DatabaseError): 

237 """Failed to connect to database""" 

238 

239 def __init__(self, db_type: str, error: str, **kwargs): 

240 super().__init__( 

241 message=f"Failed to connect to {db_type} database: {error}", 

242 recovery_suggestion="Check connection string and credentials", 

243 **kwargs 

244 ) 

245 

246 

247class DatabaseQueryError(DatabaseError): 

248 """Database query failed""" 

249 

250 def __init__(self, query: str, error: str, **kwargs): 

251 super().__init__( 

252 message=f"Query failed: {error}", 

253 details={"query": query}, 

254 **kwargs 

255 ) 

256 

257 

258# Cloud Errors 

259class CloudError(OsirisError): 

260 """Base class for cloud provider errors""" 

261 pass 

262 

263 

264class CloudAuthenticationError(CloudError): 

265 """Cloud provider authentication failed""" 

266 

267 def __init__(self, provider: str, **kwargs): 

268 super().__init__( 

269 message=f"Failed to authenticate with {provider}", 

270 recovery_suggestion=f"Configure {provider} CLI credentials", 

271 **kwargs 

272 ) 

273 

274 

275class CloudResourceNotFoundError(CloudError): 

276 """Cloud resource not found""" 

277 

278 def __init__(self, provider: str, resource_type: str, resource_id: str, **kwargs): 

279 super().__init__( 

280 message=f"{provider} {resource_type} '{resource_id}' not found", 

281 **kwargs 

282 ) 

283 

284 

285# Memory/Agent Errors 

286class MemoryError(OsirisError): 

287 """Base class for agent memory errors""" 

288 pass 

289 

290 

291class MemoryCorruptedError(MemoryError): 

292 """Agent memory file corrupted""" 

293 

294 def __init__(self, agent_name: str, **kwargs): 

295 super().__init__( 

296 message=f"Memory for agent '{agent_name}' is corrupted", 

297 recovery_suggestion="Use /init to reset agent memory", 

298 **kwargs 

299 ) 

300 

301 

302# Validation Errors 

303class ValidationError(OsirisError): 

304 """Input validation failed""" 

305 pass 

306 

307 

308class InvalidModelError(ValidationError): 

309 """Invalid model name or not supported""" 

310 

311 def __init__(self, model: str, provider: str, **kwargs): 

312 super().__init__( 

313 message=f"Model '{model}' is not valid for provider {provider}", 

314 recovery_suggestion="Use /model to see available models", 

315 **kwargs 

316 ) 

317 

318 

319class InvalidProviderError(ValidationError): 

320 """Invalid provider name""" 

321 

322 def __init__(self, provider: str, **kwargs): 

323 super().__init__( 

324 message=f"Provider '{provider}' is not supported", 

325 recovery_suggestion="Use /provider to see available providers", 

326 **kwargs 

327 ) 

328 

329 

330# Helper function to convert standard exceptions to Osiris exceptions 

331def wrap_exception(exc: Exception, context: str = "") -> OsirisError: 

332 """ 

333 Wrap standard Python exceptions in OsirisError with context. 

334  

335 Args: 

336 exc: Original exception 

337 context: Additional context about where error occurred 

338  

339 Returns: 

340 OsirisError with wrapped exception 

341 """ 

342 if isinstance(exc, OsirisError): 

343 return exc 

344 

345 message = str(exc) 

346 if context: 

347 message = f"{context}: {message}" 

348 

349 # Map common exceptions to specific Osiris errors 

350 if isinstance(exc, FileNotFoundError): 

351 return ContextFileNotFoundError(str(exc)) 

352 elif isinstance(exc, PermissionError): 

353 return ToolPermissionError("unknown", str(exc)) 

354 elif isinstance(exc, TimeoutError): 

355 return APITimeoutError("unknown", 30) 

356 elif isinstance(exc, ConnectionError): 

357 return APIError(message) 

358 else: 

359 # Generic wrapper 

360 return OsirisError( 

361 message=message, 

362 details={"original_exception": type(exc).__name__} 

363 )