coding-client 1.21.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coding_client/__init__.py +1 -0
- coding_client/api/__init__.py +19 -0
- coding_client/api/exception_handlers.py +158 -0
- coding_client/api/logging_setup.py +44 -0
- coding_client/api/metrics.py +40 -0
- coding_client/api/openapi_config.py +124 -0
- coding_client/api/request_logging.py +60 -0
- coding_client/api/validation.py +150 -0
- coding_client/auth/__init__.py +78 -0
- coding_client/auth/_default_tenant.py +132 -0
- coding_client/auth/_header_trust.py +98 -0
- coding_client/auth/_onbehalf.py +315 -0
- coding_client/auth/_trusted_networks.py +94 -0
- coding_client/auth/dependencies.py +61 -0
- coding_client/auth/middleware.py +445 -0
- coding_client/auth/models.py +83 -0
- coding_client/auth/outbound.py +137 -0
- coding_client/auth/rbac.py +21 -0
- coding_client/auth/rbac_coding.py +5 -0
- coding_client/auth/tenant.py +650 -0
- coding_client/auth/websocket.py +148 -0
- coding_client/clients/__init__.py +24 -0
- coding_client/clients/base.py +131 -0
- coding_client/clients/coding_ai.py +33 -0
- coding_client/clients/coding_analysis.py +36 -0
- coding_client/clients/coding_containers.py +45 -0
- coding_client/clients/coding_github.py +127 -0
- coding_client/clients/coding_realtime.py +127 -0
- coding_client/clients/coding_security.py +51 -0
- coding_client/clients/coding_testing.py +31 -0
- coding_client/clients/coding_workspaces.py +265 -0
- coding_client/config/__init__.py +12 -0
- coding_client/config/base_settings.py +61 -0
- coding_client/config/yaml_config_loader.py +172 -0
- coding_client/db/__init__.py +42 -0
- coding_client/db/db_types.py +122 -0
- coding_client/db/exceptions.py +59 -0
- coding_client/db/postgres_connection.py +572 -0
- coding_client/db/tenant_query_helper.py +417 -0
- coding_client/db_init.py +49 -0
- coding_client/exceptions/__init__.py +67 -0
- coding_client/exceptions/api_exceptions.py +395 -0
- coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
- coding_client/interfaces/__init__.py +22 -0
- coding_client/interfaces/cache_interface.py +179 -0
- coding_client/interfaces/container_interface.py +49 -0
- coding_client/interfaces/docker_interface.py +283 -0
- coding_client/interfaces/graph_interface.py +28 -0
- coding_client/interfaces/security_interface.py +72 -0
- coding_client/interfaces/webhook_interface.py +67 -0
- coding_client/llm/__init__.py +54 -0
- coding_client/llm/base_agent.py +195 -0
- coding_client/llm/context.py +121 -0
- coding_client/llm/responses.py +102 -0
- coding_client/llm/routing.py +82 -0
- coding_client/llm/settings.py +102 -0
- coding_client/models/__init__.py +62 -0
- coding_client/models/agent.py +38 -0
- coding_client/models/ai_test_failure.py +745 -0
- coding_client/models/analysis.py +330 -0
- coding_client/models/architecture.py +685 -0
- coding_client/models/bug.py +128 -0
- coding_client/models/code_graph.py +317 -0
- coding_client/models/container.py +241 -0
- coding_client/models/core.py +878 -0
- coding_client/models/docker_container.py +104 -0
- coding_client/models/github.py +293 -0
- coding_client/models/logging.py +29 -0
- coding_client/models/package_manager.py +257 -0
- coding_client/models/pr_validation.py +440 -0
- coding_client/models/pydantic_ai.py +395 -0
- coding_client/models/repo_scanner.py +249 -0
- coding_client/models/responses.py +431 -0
- coding_client/models/security.py +185 -0
- coding_client/models/static_analysis.py +508 -0
- coding_client/models/system.py +48 -0
- coding_client/models/test_executor.py +204 -0
- coding_client/models/testing.py +673 -0
- coding_client/models/testing_config.py +200 -0
- coding_client/models/validation.py +127 -0
- coding_client/models_init.py +272 -0
- coding_client/telemetry.py +195 -0
- coding_client/testing.py +180 -0
- coding_client/types/__init__.py +130 -0
- coding_client/types/constraint_types.py +276 -0
- coding_client/types/core_types.py +112 -0
- coding_client/types/state_machine.py +443 -0
- coding_client/utils/__init__.py +86 -0
- coding_client/utils/archive_utils.py +387 -0
- coding_client/utils/ast_debug.py +45 -0
- coding_client/utils/async_timeout.py +364 -0
- coding_client/utils/constants.py +518 -0
- coding_client/utils/content_processing.py +326 -0
- coding_client/utils/database.py +916 -0
- coding_client/utils/encoding_helper.py +222 -0
- coding_client/utils/enum_helpers.py +166 -0
- coding_client/utils/file_operations.py +177 -0
- coding_client/utils/filesystem_ops.py +528 -0
- coding_client/utils/http_client.py +562 -0
- coding_client/utils/path_utils.py +471 -0
- coding_client/utils/profiling.py +421 -0
- coding_client/utils/response_builder.py +240 -0
- coding_client/utils/retry_helper.py +712 -0
- coding_client/utils/security_validation.py +423 -0
- coding_client-1.21.2.dist-info/METADATA +234 -0
- coding_client-1.21.2.dist-info/RECORD +109 -0
- coding_client-1.21.2.dist-info/WHEEL +5 -0
- coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
- coding_client-1.21.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""Custom exceptions for PydanticAI operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PydanticAIException(Exception): # noqa: N818
|
|
9
|
+
"""Base exception for PydanticAI-related errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
error_code: str | None = None,
|
|
15
|
+
details: dict[str, Any] | None = None,
|
|
16
|
+
cause: Exception | None = None,
|
|
17
|
+
):
|
|
18
|
+
"""Initialize PydanticAI exception.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
message: Error message
|
|
22
|
+
error_code: Optional error code for categorization
|
|
23
|
+
details: Optional additional details
|
|
24
|
+
cause: Optional underlying cause exception
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.message = message
|
|
28
|
+
self.error_code = error_code
|
|
29
|
+
self.details = details or {}
|
|
30
|
+
self.cause = cause
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
"""String representation of the exception."""
|
|
34
|
+
parts = [self.message]
|
|
35
|
+
|
|
36
|
+
if self.error_code:
|
|
37
|
+
parts.append(f"Error code: {self.error_code}")
|
|
38
|
+
|
|
39
|
+
if self.details:
|
|
40
|
+
parts.append(f"Details: {self.details}")
|
|
41
|
+
|
|
42
|
+
if self.cause:
|
|
43
|
+
parts.append(f"Caused by: {self.cause}")
|
|
44
|
+
|
|
45
|
+
return " | ".join(parts)
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
|
48
|
+
"""Convert exception to dictionary."""
|
|
49
|
+
return {
|
|
50
|
+
"type": self.__class__.__name__,
|
|
51
|
+
"message": self.message,
|
|
52
|
+
"error_code": self.error_code,
|
|
53
|
+
"details": self.details,
|
|
54
|
+
"cause": str(self.cause) if self.cause else None,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LLMEmptyResponseError(PydanticAIException):
|
|
59
|
+
"""Raised when the LLM plane returns an empty or unusable response.
|
|
60
|
+
|
|
61
|
+
In inference-router mode this typically means the provider chain was
|
|
62
|
+
exhausted (e.g. RTX fell through on schema, deepseek timed out, claude
|
|
63
|
+
returned a billing error). In direct mode it means the single configured
|
|
64
|
+
provider returned nothing usable after pydantic-ai's own retry budget.
|
|
65
|
+
Callers should catch this alongside ``httpx.HTTPError`` and degrade
|
|
66
|
+
gracefully.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
message: str,
|
|
72
|
+
agent_name: str | None = None,
|
|
73
|
+
**kwargs: Any,
|
|
74
|
+
):
|
|
75
|
+
"""Initialize empty-response error.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
message: Error message
|
|
79
|
+
agent_name: Name of the agent whose call returned empty
|
|
80
|
+
**kwargs: Additional arguments for base exception
|
|
81
|
+
"""
|
|
82
|
+
super().__init__(message, error_code="LLM_EMPTY_RESPONSE", **kwargs)
|
|
83
|
+
self.agent_name = agent_name
|
|
84
|
+
if agent_name:
|
|
85
|
+
self.details["agent_name"] = agent_name
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AgentConfigurationError(PydanticAIException):
|
|
89
|
+
"""Exception raised when agent configuration is invalid."""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
message: str,
|
|
94
|
+
agent_id: str | None = None,
|
|
95
|
+
config_field: str | None = None,
|
|
96
|
+
**kwargs: Any,
|
|
97
|
+
):
|
|
98
|
+
"""Initialize agent configuration error.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
message: Error message
|
|
102
|
+
agent_id: Agent ID that has configuration issues
|
|
103
|
+
config_field: Specific configuration field that's invalid
|
|
104
|
+
**kwargs: Additional arguments for base exception
|
|
105
|
+
"""
|
|
106
|
+
super().__init__(message, **kwargs)
|
|
107
|
+
self.agent_id = agent_id
|
|
108
|
+
self.config_field = config_field
|
|
109
|
+
|
|
110
|
+
if agent_id:
|
|
111
|
+
self.details["agent_id"] = agent_id
|
|
112
|
+
if config_field:
|
|
113
|
+
self.details["config_field"] = config_field
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ModelProviderError(PydanticAIException):
|
|
117
|
+
"""Exception raised when model provider encounters an error."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
message: str,
|
|
122
|
+
provider: str | None = None,
|
|
123
|
+
model: str | None = None,
|
|
124
|
+
status_code: int | None = None,
|
|
125
|
+
**kwargs: Any,
|
|
126
|
+
):
|
|
127
|
+
"""Initialize model provider error.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
message: Error message
|
|
131
|
+
provider: AI provider name
|
|
132
|
+
model: Model name
|
|
133
|
+
status_code: HTTP status code if applicable
|
|
134
|
+
**kwargs: Additional arguments for base exception
|
|
135
|
+
"""
|
|
136
|
+
super().__init__(message, **kwargs)
|
|
137
|
+
self.provider = provider
|
|
138
|
+
self.model = model
|
|
139
|
+
self.status_code = status_code
|
|
140
|
+
|
|
141
|
+
if provider:
|
|
142
|
+
self.details["provider"] = provider
|
|
143
|
+
if model:
|
|
144
|
+
self.details["model"] = model
|
|
145
|
+
if status_code:
|
|
146
|
+
self.details["status_code"] = status_code
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class APIKeyError(ModelProviderError):
|
|
150
|
+
"""Exception raised when API key is missing or invalid."""
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
message: str,
|
|
155
|
+
provider: str | None = None,
|
|
156
|
+
**kwargs: Any,
|
|
157
|
+
):
|
|
158
|
+
"""Initialize API key error.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
message: Error message
|
|
162
|
+
provider: AI provider name
|
|
163
|
+
**kwargs: Additional arguments for base exception
|
|
164
|
+
"""
|
|
165
|
+
super().__init__(message, provider=provider, error_code="API_KEY_ERROR", **kwargs)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class RateLimitError(ModelProviderError):
|
|
169
|
+
"""Exception raised when rate limit is exceeded."""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
message: str,
|
|
174
|
+
provider: str | None = None,
|
|
175
|
+
retry_after: int | None = None,
|
|
176
|
+
**kwargs: Any,
|
|
177
|
+
):
|
|
178
|
+
"""Initialize rate limit error.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
message: Error message
|
|
182
|
+
provider: AI provider name
|
|
183
|
+
retry_after: Seconds to wait before retrying
|
|
184
|
+
**kwargs: Additional arguments for base exception
|
|
185
|
+
"""
|
|
186
|
+
super().__init__(message, provider=provider, error_code="RATE_LIMIT", **kwargs)
|
|
187
|
+
self.retry_after = retry_after
|
|
188
|
+
|
|
189
|
+
if retry_after:
|
|
190
|
+
self.details["retry_after"] = retry_after
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ModelNotFoundError(ModelProviderError):
|
|
194
|
+
"""Exception raised when requested model is not found."""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
message: str,
|
|
199
|
+
provider: str | None = None,
|
|
200
|
+
model: str | None = None,
|
|
201
|
+
**kwargs: Any,
|
|
202
|
+
):
|
|
203
|
+
"""Initialize model not found error.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
message: Error message
|
|
207
|
+
provider: AI provider name
|
|
208
|
+
model: Model name that was not found
|
|
209
|
+
**kwargs: Additional arguments for base exception
|
|
210
|
+
"""
|
|
211
|
+
super().__init__(
|
|
212
|
+
message,
|
|
213
|
+
provider=provider,
|
|
214
|
+
model=model,
|
|
215
|
+
error_code="MODEL_NOT_FOUND",
|
|
216
|
+
**kwargs,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TokenLimitError(ModelProviderError):
|
|
221
|
+
"""Exception raised when token limit is exceeded."""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
message: str,
|
|
226
|
+
provider: str | None = None,
|
|
227
|
+
model: str | None = None,
|
|
228
|
+
token_count: int | None = None,
|
|
229
|
+
token_limit: int | None = None,
|
|
230
|
+
**kwargs: Any,
|
|
231
|
+
):
|
|
232
|
+
"""Initialize token limit error.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
message: Error message
|
|
236
|
+
provider: AI provider name
|
|
237
|
+
model: Model name
|
|
238
|
+
token_count: Number of tokens in request
|
|
239
|
+
token_limit: Maximum token limit
|
|
240
|
+
**kwargs: Additional arguments for base exception
|
|
241
|
+
"""
|
|
242
|
+
super().__init__(
|
|
243
|
+
message, provider=provider, model=model, error_code="TOKEN_LIMIT", **kwargs
|
|
244
|
+
)
|
|
245
|
+
self.token_count = token_count
|
|
246
|
+
self.token_limit = token_limit
|
|
247
|
+
|
|
248
|
+
if token_count:
|
|
249
|
+
self.details["token_count"] = token_count
|
|
250
|
+
if token_limit:
|
|
251
|
+
self.details["token_limit"] = token_limit
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class AnalysisError(PydanticAIException):
|
|
255
|
+
"""Exception raised during test failure analysis."""
|
|
256
|
+
|
|
257
|
+
def __init__(
|
|
258
|
+
self,
|
|
259
|
+
message: str,
|
|
260
|
+
analysis_type: str | None = None,
|
|
261
|
+
test_id: str | None = None,
|
|
262
|
+
**kwargs: Any,
|
|
263
|
+
):
|
|
264
|
+
"""Initialize analysis error.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
message: Error message
|
|
268
|
+
analysis_type: Type of analysis that failed
|
|
269
|
+
test_id: Test ID being analyzed
|
|
270
|
+
**kwargs: Additional arguments for base exception
|
|
271
|
+
"""
|
|
272
|
+
super().__init__(message, **kwargs)
|
|
273
|
+
self.analysis_type = analysis_type
|
|
274
|
+
self.test_id = test_id
|
|
275
|
+
|
|
276
|
+
if analysis_type:
|
|
277
|
+
self.details["analysis_type"] = analysis_type
|
|
278
|
+
if test_id:
|
|
279
|
+
self.details["test_id"] = test_id
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class AIValidationError(PydanticAIException):
|
|
283
|
+
"""Exception raised when AI input validation fails."""
|
|
284
|
+
|
|
285
|
+
def __init__(
|
|
286
|
+
self,
|
|
287
|
+
message: str,
|
|
288
|
+
field_name: str | None = None,
|
|
289
|
+
field_value: Any | None = None,
|
|
290
|
+
**kwargs: Any,
|
|
291
|
+
):
|
|
292
|
+
"""Initialize validation error.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
message: Error message
|
|
296
|
+
field_name: Name of field that failed validation
|
|
297
|
+
field_value: Value that failed validation
|
|
298
|
+
**kwargs: Additional arguments for base exception
|
|
299
|
+
"""
|
|
300
|
+
super().__init__(message, error_code="VALIDATION_ERROR", **kwargs)
|
|
301
|
+
self.field_name = field_name
|
|
302
|
+
self.field_value = field_value
|
|
303
|
+
|
|
304
|
+
if field_name:
|
|
305
|
+
self.details["field_name"] = field_name
|
|
306
|
+
if field_value is not None:
|
|
307
|
+
self.details["field_value"] = str(field_value)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class AITimeoutError(PydanticAIException):
|
|
311
|
+
"""Exception raised when operation times out."""
|
|
312
|
+
|
|
313
|
+
def __init__(
|
|
314
|
+
self,
|
|
315
|
+
message: str,
|
|
316
|
+
timeout_seconds: float | None = None,
|
|
317
|
+
operation: str | None = None,
|
|
318
|
+
**kwargs: Any,
|
|
319
|
+
):
|
|
320
|
+
"""Initialize timeout error.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
message: Error message
|
|
324
|
+
timeout_seconds: Timeout duration in seconds
|
|
325
|
+
operation: Operation that timed out
|
|
326
|
+
**kwargs: Additional arguments for base exception
|
|
327
|
+
"""
|
|
328
|
+
super().__init__(message, error_code="TIMEOUT", **kwargs)
|
|
329
|
+
self.timeout_seconds = timeout_seconds
|
|
330
|
+
self.operation = operation
|
|
331
|
+
|
|
332
|
+
if timeout_seconds:
|
|
333
|
+
self.details["timeout_seconds"] = timeout_seconds
|
|
334
|
+
if operation:
|
|
335
|
+
self.details["operation"] = operation
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class ResourceExhaustedError(PydanticAIException):
|
|
339
|
+
"""Exception raised when resources are exhausted."""
|
|
340
|
+
|
|
341
|
+
def __init__(
|
|
342
|
+
self,
|
|
343
|
+
message: str,
|
|
344
|
+
resource_type: str | None = None,
|
|
345
|
+
**kwargs: Any,
|
|
346
|
+
):
|
|
347
|
+
"""Initialize resource exhausted error.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
message: Error message
|
|
351
|
+
resource_type: Type of resource that's exhausted
|
|
352
|
+
**kwargs: Additional arguments for base exception
|
|
353
|
+
"""
|
|
354
|
+
super().__init__(message, error_code="RESOURCE_EXHAUSTED", **kwargs)
|
|
355
|
+
self.resource_type = resource_type
|
|
356
|
+
|
|
357
|
+
if resource_type:
|
|
358
|
+
self.details["resource_type"] = resource_type
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class RetryableError(PydanticAIException):
|
|
362
|
+
"""Exception that can be retried."""
|
|
363
|
+
|
|
364
|
+
def __init__(
|
|
365
|
+
self,
|
|
366
|
+
message: str,
|
|
367
|
+
retry_count: int = 0,
|
|
368
|
+
max_retries: int = 3,
|
|
369
|
+
**kwargs: Any,
|
|
370
|
+
):
|
|
371
|
+
"""Initialize retryable error.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
message: Error message
|
|
375
|
+
retry_count: Current retry count
|
|
376
|
+
max_retries: Maximum number of retries
|
|
377
|
+
**kwargs: Additional arguments for base exception
|
|
378
|
+
"""
|
|
379
|
+
super().__init__(message, error_code="RETRYABLE", **kwargs)
|
|
380
|
+
self.retry_count = retry_count
|
|
381
|
+
self.max_retries = max_retries
|
|
382
|
+
|
|
383
|
+
self.details["retry_count"] = retry_count
|
|
384
|
+
self.details["max_retries"] = max_retries
|
|
385
|
+
|
|
386
|
+
def can_retry(self) -> bool:
|
|
387
|
+
"""Check if error can be retried."""
|
|
388
|
+
return self.retry_count < self.max_retries
|
|
389
|
+
|
|
390
|
+
def increment_retry(self) -> None:
|
|
391
|
+
"""Increment retry count."""
|
|
392
|
+
self.retry_count += 1
|
|
393
|
+
self.details["retry_count"] = self.retry_count
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class NonRetryableError(PydanticAIException):
|
|
397
|
+
"""Exception that should not be retried."""
|
|
398
|
+
|
|
399
|
+
def __init__(
|
|
400
|
+
self,
|
|
401
|
+
message: str,
|
|
402
|
+
**kwargs: Any,
|
|
403
|
+
):
|
|
404
|
+
"""Initialize non-retryable error.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
message: Error message
|
|
408
|
+
**kwargs: Additional arguments for base exception
|
|
409
|
+
"""
|
|
410
|
+
super().__init__(message, error_code="NON_RETRYABLE", **kwargs)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def is_retryable_error(error: Exception) -> bool:
|
|
414
|
+
"""Check if an error is retryable.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
error: Exception to check
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
True if error is retryable, False otherwise
|
|
421
|
+
"""
|
|
422
|
+
# PydanticAI specific retryable errors
|
|
423
|
+
if isinstance(error, RetryableError):
|
|
424
|
+
return error.can_retry()
|
|
425
|
+
|
|
426
|
+
# Rate limit errors are retryable
|
|
427
|
+
if isinstance(error, RateLimitError):
|
|
428
|
+
return True
|
|
429
|
+
|
|
430
|
+
# Non-retryable errors
|
|
431
|
+
if isinstance(error, (NonRetryableError, APIKeyError, ModelNotFoundError, AIValidationError)):
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
# Network/connection errors are usually retryable
|
|
435
|
+
if isinstance(error, (ConnectionError, AITimeoutError)):
|
|
436
|
+
return True
|
|
437
|
+
|
|
438
|
+
# Server errors (5xx) are retryable, client errors (4xx) are not
|
|
439
|
+
if isinstance(error, ModelProviderError) and error.status_code:
|
|
440
|
+
return error.status_code >= 500
|
|
441
|
+
|
|
442
|
+
# Default to non-retryable
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def get_retry_delay(error: Exception, retry_count: int) -> float:
|
|
447
|
+
"""Get retry delay for an error.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
error: Exception that occurred
|
|
451
|
+
retry_count: Current retry count
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Delay in seconds before retrying
|
|
455
|
+
"""
|
|
456
|
+
# Rate limit errors may specify retry delay
|
|
457
|
+
if isinstance(error, RateLimitError) and error.retry_after:
|
|
458
|
+
return float(error.retry_after)
|
|
459
|
+
|
|
460
|
+
# Exponential backoff: 2^retry_count seconds, max 60 seconds
|
|
461
|
+
delay = min(2**retry_count, 60)
|
|
462
|
+
return delay
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def categorize_error(error: Exception) -> str:
|
|
466
|
+
"""Categorize an error for logging and metrics.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
error: Exception to categorize
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Error category string
|
|
473
|
+
"""
|
|
474
|
+
if isinstance(error, APIKeyError):
|
|
475
|
+
return "authentication"
|
|
476
|
+
elif isinstance(error, RateLimitError):
|
|
477
|
+
return "rate_limit"
|
|
478
|
+
elif isinstance(error, ModelNotFoundError):
|
|
479
|
+
return "model_not_found"
|
|
480
|
+
elif isinstance(error, TokenLimitError):
|
|
481
|
+
return "token_limit"
|
|
482
|
+
elif isinstance(error, AITimeoutError):
|
|
483
|
+
return "timeout"
|
|
484
|
+
elif isinstance(error, AIValidationError):
|
|
485
|
+
return "validation"
|
|
486
|
+
elif isinstance(error, AgentConfigurationError):
|
|
487
|
+
return "configuration"
|
|
488
|
+
elif isinstance(error, ModelProviderError):
|
|
489
|
+
return "provider"
|
|
490
|
+
elif isinstance(error, AnalysisError):
|
|
491
|
+
return "analysis"
|
|
492
|
+
elif isinstance(error, ResourceExhaustedError):
|
|
493
|
+
return "resource_exhausted"
|
|
494
|
+
else:
|
|
495
|
+
return "unknown"
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def create_error_response(
|
|
499
|
+
error: Exception,
|
|
500
|
+
request_id: str,
|
|
501
|
+
analysis_time_ms: float = 0.0,
|
|
502
|
+
) -> dict[str, Any]:
|
|
503
|
+
"""Create standardized error response.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
error: Exception that occurred
|
|
507
|
+
request_id: Request ID for tracking
|
|
508
|
+
analysis_time_ms: Time spent on analysis
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Error response dictionary
|
|
512
|
+
"""
|
|
513
|
+
error_category = categorize_error(error)
|
|
514
|
+
|
|
515
|
+
response: dict[str, Any] = {
|
|
516
|
+
"request_id": request_id,
|
|
517
|
+
"success": False,
|
|
518
|
+
"error_message": str(error),
|
|
519
|
+
"error_category": error_category,
|
|
520
|
+
"total_analysis_time_ms": analysis_time_ms,
|
|
521
|
+
"retryable": is_retryable_error(error),
|
|
522
|
+
"metadata": {
|
|
523
|
+
"error_type": type(error).__name__,
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
# Add additional details for PydanticAI exceptions
|
|
528
|
+
if isinstance(error, PydanticAIException):
|
|
529
|
+
response["error_code"] = error.error_code
|
|
530
|
+
response["error_details"] = error.details
|
|
531
|
+
response["metadata"].update(error.to_dict())
|
|
532
|
+
|
|
533
|
+
return response
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Abstract interfaces for dependency injection and circular import prevention.
|
|
2
|
+
|
|
3
|
+
These interfaces define contracts for service interactions without
|
|
4
|
+
importing concrete implementations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .cache_interface import IAnalysisCache, ICacheManager
|
|
8
|
+
from .container_interface import ContainerInfo as IContainerInfo
|
|
9
|
+
from .docker_interface import IDockerService
|
|
10
|
+
from .graph_interface import IGraphService
|
|
11
|
+
from .security_interface import ISecurityService
|
|
12
|
+
from .webhook_interface import IWebhookEventHandler
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"IAnalysisCache",
|
|
16
|
+
"ICacheManager",
|
|
17
|
+
"IContainerInfo",
|
|
18
|
+
"IDockerService",
|
|
19
|
+
"IGraphService",
|
|
20
|
+
"ISecurityService",
|
|
21
|
+
"IWebhookEventHandler",
|
|
22
|
+
]
|