smartify-ai 0.1.0__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.
- smartify/__init__.py +3 -0
- smartify/agents/__init__.py +0 -0
- smartify/agents/adapters/__init__.py +13 -0
- smartify/agents/adapters/anthropic.py +253 -0
- smartify/agents/adapters/openai.py +289 -0
- smartify/api/__init__.py +26 -0
- smartify/api/auth.py +352 -0
- smartify/api/errors.py +380 -0
- smartify/api/events.py +345 -0
- smartify/api/server.py +992 -0
- smartify/cli/__init__.py +1 -0
- smartify/cli/main.py +430 -0
- smartify/engine/__init__.py +64 -0
- smartify/engine/approval.py +479 -0
- smartify/engine/orchestrator.py +1365 -0
- smartify/engine/scheduler.py +380 -0
- smartify/engine/spark.py +294 -0
- smartify/guardrails/__init__.py +22 -0
- smartify/guardrails/breakers.py +409 -0
- smartify/models/__init__.py +61 -0
- smartify/models/grid.py +625 -0
- smartify/notifications/__init__.py +22 -0
- smartify/notifications/webhook.py +556 -0
- smartify/state/__init__.py +46 -0
- smartify/state/checkpoint.py +558 -0
- smartify/state/resume.py +301 -0
- smartify/state/store.py +370 -0
- smartify/tools/__init__.py +17 -0
- smartify/tools/base.py +196 -0
- smartify/tools/builtin/__init__.py +79 -0
- smartify/tools/builtin/file.py +464 -0
- smartify/tools/builtin/http.py +195 -0
- smartify/tools/builtin/shell.py +137 -0
- smartify/tools/mcp/__init__.py +33 -0
- smartify/tools/mcp/adapter.py +157 -0
- smartify/tools/mcp/client.py +334 -0
- smartify/tools/mcp/registry.py +130 -0
- smartify/validator/__init__.py +0 -0
- smartify/validator/validate.py +271 -0
- smartify/workspace/__init__.py +5 -0
- smartify/workspace/manager.py +248 -0
- smartify_ai-0.1.0.dist-info/METADATA +201 -0
- smartify_ai-0.1.0.dist-info/RECORD +46 -0
- smartify_ai-0.1.0.dist-info/WHEEL +4 -0
- smartify_ai-0.1.0.dist-info/entry_points.txt +2 -0
- smartify_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
smartify/api/errors.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Smartify API Error Taxonomy.
|
|
2
|
+
|
|
3
|
+
Provides consistent error codes, response models, and exception handlers
|
|
4
|
+
for SDK-friendly error responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from fastapi import Request, status
|
|
12
|
+
from fastapi.responses import JSONResponse
|
|
13
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# Error Codes
|
|
18
|
+
# ============================================================================
|
|
19
|
+
|
|
20
|
+
class ErrorCode(str, Enum):
|
|
21
|
+
"""Typed error codes for SDK consumption.
|
|
22
|
+
|
|
23
|
+
Naming convention: RESOURCE_ACTION_REASON
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Validation errors (400)
|
|
27
|
+
VALIDATION_FAILED = "VALIDATION_FAILED"
|
|
28
|
+
INVALID_YAML = "INVALID_YAML"
|
|
29
|
+
INVALID_GRID_SPEC = "INVALID_GRID_SPEC"
|
|
30
|
+
INVALID_INPUT = "INVALID_INPUT"
|
|
31
|
+
MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD"
|
|
32
|
+
|
|
33
|
+
# Resource errors (404)
|
|
34
|
+
GRID_NOT_FOUND = "GRID_NOT_FOUND"
|
|
35
|
+
NODE_NOT_FOUND = "NODE_NOT_FOUND"
|
|
36
|
+
RUN_NOT_FOUND = "RUN_NOT_FOUND"
|
|
37
|
+
APPROVAL_NOT_FOUND = "APPROVAL_NOT_FOUND"
|
|
38
|
+
|
|
39
|
+
# State/lifecycle errors (409)
|
|
40
|
+
GRID_INVALID_STATE = "GRID_INVALID_STATE"
|
|
41
|
+
GRID_ALREADY_RUNNING = "GRID_ALREADY_RUNNING"
|
|
42
|
+
GRID_NOT_RUNNING = "GRID_NOT_RUNNING"
|
|
43
|
+
GRID_ALREADY_COMPLETED = "GRID_ALREADY_COMPLETED"
|
|
44
|
+
NODE_ALREADY_COMPLETED = "NODE_ALREADY_COMPLETED"
|
|
45
|
+
|
|
46
|
+
# Limit/guardrail errors (429)
|
|
47
|
+
BREAKER_TRIPPED = "BREAKER_TRIPPED"
|
|
48
|
+
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
|
|
49
|
+
TOKEN_LIMIT_EXCEEDED = "TOKEN_LIMIT_EXCEEDED"
|
|
50
|
+
COST_LIMIT_EXCEEDED = "COST_LIMIT_EXCEEDED"
|
|
51
|
+
TIME_LIMIT_EXCEEDED = "TIME_LIMIT_EXCEEDED"
|
|
52
|
+
|
|
53
|
+
# Auth errors (401/403)
|
|
54
|
+
UNAUTHORIZED = "UNAUTHORIZED"
|
|
55
|
+
INVALID_API_KEY = "INVALID_API_KEY"
|
|
56
|
+
FORBIDDEN = "FORBIDDEN"
|
|
57
|
+
INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS"
|
|
58
|
+
|
|
59
|
+
# Execution errors (500)
|
|
60
|
+
EXECUTION_FAILED = "EXECUTION_FAILED"
|
|
61
|
+
LLM_ADAPTER_ERROR = "LLM_ADAPTER_ERROR"
|
|
62
|
+
TOOL_EXECUTION_ERROR = "TOOL_EXECUTION_ERROR"
|
|
63
|
+
CHECKPOINT_ERROR = "CHECKPOINT_ERROR"
|
|
64
|
+
WEBHOOK_DELIVERY_FAILED = "WEBHOOK_DELIVERY_FAILED"
|
|
65
|
+
|
|
66
|
+
# External service errors (502/503)
|
|
67
|
+
LLM_SERVICE_UNAVAILABLE = "LLM_SERVICE_UNAVAILABLE"
|
|
68
|
+
LLM_RATE_LIMITED = "LLM_RATE_LIMITED"
|
|
69
|
+
EXTERNAL_SERVICE_ERROR = "EXTERNAL_SERVICE_ERROR"
|
|
70
|
+
|
|
71
|
+
# Internal errors (500)
|
|
72
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
73
|
+
NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ============================================================================
|
|
77
|
+
# Retryability
|
|
78
|
+
# ============================================================================
|
|
79
|
+
|
|
80
|
+
# Errors that are safe to retry (typically transient)
|
|
81
|
+
RETRYABLE_ERRORS = {
|
|
82
|
+
ErrorCode.LLM_SERVICE_UNAVAILABLE,
|
|
83
|
+
ErrorCode.LLM_RATE_LIMITED,
|
|
84
|
+
ErrorCode.EXTERNAL_SERVICE_ERROR,
|
|
85
|
+
ErrorCode.WEBHOOK_DELIVERY_FAILED,
|
|
86
|
+
ErrorCode.RATE_LIMIT_EXCEEDED,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Errors that should NOT be retried (permanent failures)
|
|
90
|
+
NON_RETRYABLE_ERRORS = {
|
|
91
|
+
ErrorCode.VALIDATION_FAILED,
|
|
92
|
+
ErrorCode.INVALID_YAML,
|
|
93
|
+
ErrorCode.INVALID_GRID_SPEC,
|
|
94
|
+
ErrorCode.INVALID_INPUT,
|
|
95
|
+
ErrorCode.MISSING_REQUIRED_FIELD,
|
|
96
|
+
ErrorCode.GRID_NOT_FOUND,
|
|
97
|
+
ErrorCode.NODE_NOT_FOUND,
|
|
98
|
+
ErrorCode.UNAUTHORIZED,
|
|
99
|
+
ErrorCode.INVALID_API_KEY,
|
|
100
|
+
ErrorCode.FORBIDDEN,
|
|
101
|
+
ErrorCode.INSUFFICIENT_PERMISSIONS,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_retryable(code: ErrorCode) -> bool:
|
|
106
|
+
"""Check if an error code indicates a retryable condition."""
|
|
107
|
+
return code in RETRYABLE_ERRORS
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ============================================================================
|
|
111
|
+
# Error Response Models
|
|
112
|
+
# ============================================================================
|
|
113
|
+
|
|
114
|
+
class ErrorDetail(BaseModel):
|
|
115
|
+
"""Additional error context."""
|
|
116
|
+
field: Optional[str] = Field(None, description="Field that caused the error")
|
|
117
|
+
value: Optional[Any] = Field(None, description="Invalid value")
|
|
118
|
+
constraint: Optional[str] = Field(None, description="Constraint that was violated")
|
|
119
|
+
suggestion: Optional[str] = Field(None, description="Suggested fix")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ErrorResponse(BaseModel):
|
|
123
|
+
"""Standard error response for all API errors.
|
|
124
|
+
|
|
125
|
+
SDK clients can rely on this consistent structure.
|
|
126
|
+
"""
|
|
127
|
+
error: str = Field(..., description="Error code from ErrorCode enum")
|
|
128
|
+
message: str = Field(..., description="Human-readable error message")
|
|
129
|
+
retryable: bool = Field(..., description="Whether this error is safe to retry")
|
|
130
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
131
|
+
request_id: Optional[str] = Field(None, description="Request tracking ID")
|
|
132
|
+
details: Optional[List[ErrorDetail]] = Field(None, description="Additional error context")
|
|
133
|
+
|
|
134
|
+
# For breaker/limit errors
|
|
135
|
+
retry_after: Optional[int] = Field(None, description="Seconds to wait before retry (for rate limits)")
|
|
136
|
+
|
|
137
|
+
# For debugging (only in non-production)
|
|
138
|
+
debug: Optional[Dict[str, Any]] = Field(None, description="Debug info (dev only)")
|
|
139
|
+
|
|
140
|
+
model_config = ConfigDict(
|
|
141
|
+
json_schema_extra={
|
|
142
|
+
"example": {
|
|
143
|
+
"error": "GRID_NOT_FOUND",
|
|
144
|
+
"message": "Grid with ID 'abc123' not found",
|
|
145
|
+
"retryable": False,
|
|
146
|
+
"timestamp": "2026-02-02T04:30:00Z",
|
|
147
|
+
"request_id": "req_xyz789",
|
|
148
|
+
"details": None,
|
|
149
|
+
"retry_after": None
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ============================================================================
|
|
156
|
+
# API Exception Classes
|
|
157
|
+
# ============================================================================
|
|
158
|
+
|
|
159
|
+
class SmartifyAPIError(Exception):
|
|
160
|
+
"""Base exception for Smartify API errors."""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
code: ErrorCode,
|
|
165
|
+
message: str,
|
|
166
|
+
status_code: int = 500,
|
|
167
|
+
details: Optional[List[ErrorDetail]] = None,
|
|
168
|
+
retry_after: Optional[int] = None,
|
|
169
|
+
debug: Optional[Dict[str, Any]] = None,
|
|
170
|
+
):
|
|
171
|
+
self.code = code
|
|
172
|
+
self.message = message
|
|
173
|
+
self.status_code = status_code
|
|
174
|
+
self.details = details
|
|
175
|
+
self.retry_after = retry_after
|
|
176
|
+
self.debug = debug
|
|
177
|
+
super().__init__(message)
|
|
178
|
+
|
|
179
|
+
def to_response(self, request_id: Optional[str] = None, include_debug: bool = False) -> ErrorResponse:
|
|
180
|
+
"""Convert to ErrorResponse model."""
|
|
181
|
+
return ErrorResponse(
|
|
182
|
+
error=self.code.value,
|
|
183
|
+
message=self.message,
|
|
184
|
+
retryable=is_retryable(self.code),
|
|
185
|
+
request_id=request_id,
|
|
186
|
+
details=self.details,
|
|
187
|
+
retry_after=self.retry_after,
|
|
188
|
+
debug=self.debug if include_debug else None,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Convenience exception classes for common error types
|
|
193
|
+
|
|
194
|
+
class ValidationError(SmartifyAPIError):
|
|
195
|
+
"""Raised for validation failures."""
|
|
196
|
+
def __init__(self, message: str, details: Optional[List[ErrorDetail]] = None):
|
|
197
|
+
super().__init__(
|
|
198
|
+
code=ErrorCode.VALIDATION_FAILED,
|
|
199
|
+
message=message,
|
|
200
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
201
|
+
details=details,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class GridNotFoundError(SmartifyAPIError):
|
|
206
|
+
"""Raised when a grid is not found."""
|
|
207
|
+
def __init__(self, grid_id: str):
|
|
208
|
+
super().__init__(
|
|
209
|
+
code=ErrorCode.GRID_NOT_FOUND,
|
|
210
|
+
message=f"Grid with ID '{grid_id}' not found",
|
|
211
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class NodeNotFoundError(SmartifyAPIError):
|
|
216
|
+
"""Raised when a node is not found."""
|
|
217
|
+
def __init__(self, grid_id: str, node_id: str):
|
|
218
|
+
super().__init__(
|
|
219
|
+
code=ErrorCode.NODE_NOT_FOUND,
|
|
220
|
+
message=f"Node '{node_id}' not found in grid '{grid_id}'",
|
|
221
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class GridStateError(SmartifyAPIError):
|
|
226
|
+
"""Raised for invalid state transitions."""
|
|
227
|
+
def __init__(self, message: str, current_state: str, expected_states: List[str]):
|
|
228
|
+
super().__init__(
|
|
229
|
+
code=ErrorCode.GRID_INVALID_STATE,
|
|
230
|
+
message=message,
|
|
231
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
232
|
+
details=[
|
|
233
|
+
ErrorDetail(
|
|
234
|
+
field="state",
|
|
235
|
+
value=current_state,
|
|
236
|
+
constraint=f"Expected one of: {', '.join(expected_states)}",
|
|
237
|
+
)
|
|
238
|
+
],
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class BreakerTrippedError(SmartifyAPIError):
|
|
243
|
+
"""Raised when a breaker/guardrail is tripped."""
|
|
244
|
+
def __init__(
|
|
245
|
+
self,
|
|
246
|
+
breaker_type: str,
|
|
247
|
+
limit: float,
|
|
248
|
+
current: float,
|
|
249
|
+
retry_after: Optional[int] = None,
|
|
250
|
+
):
|
|
251
|
+
code_map = {
|
|
252
|
+
"tokens": ErrorCode.TOKEN_LIMIT_EXCEEDED,
|
|
253
|
+
"cost": ErrorCode.COST_LIMIT_EXCEEDED,
|
|
254
|
+
"time": ErrorCode.TIME_LIMIT_EXCEEDED,
|
|
255
|
+
}
|
|
256
|
+
super().__init__(
|
|
257
|
+
code=code_map.get(breaker_type, ErrorCode.BREAKER_TRIPPED),
|
|
258
|
+
message=f"{breaker_type.title()} limit exceeded: {current:.2f} / {limit:.2f}",
|
|
259
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
260
|
+
retry_after=retry_after,
|
|
261
|
+
details=[
|
|
262
|
+
ErrorDetail(
|
|
263
|
+
field=breaker_type,
|
|
264
|
+
value=current,
|
|
265
|
+
constraint=f"Maximum: {limit}",
|
|
266
|
+
)
|
|
267
|
+
],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class RateLimitError(SmartifyAPIError):
|
|
272
|
+
"""Raised when API rate limit is exceeded."""
|
|
273
|
+
def __init__(self, retry_after: int = 60):
|
|
274
|
+
super().__init__(
|
|
275
|
+
code=ErrorCode.RATE_LIMIT_EXCEEDED,
|
|
276
|
+
message="API rate limit exceeded",
|
|
277
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
278
|
+
retry_after=retry_after,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class AuthenticationError(SmartifyAPIError):
|
|
283
|
+
"""Raised for authentication failures."""
|
|
284
|
+
def __init__(self, message: str = "Invalid or missing API key"):
|
|
285
|
+
super().__init__(
|
|
286
|
+
code=ErrorCode.INVALID_API_KEY,
|
|
287
|
+
message=message,
|
|
288
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class ExecutionError(SmartifyAPIError):
|
|
293
|
+
"""Raised when grid execution fails."""
|
|
294
|
+
def __init__(self, message: str, node_id: Optional[str] = None):
|
|
295
|
+
details = None
|
|
296
|
+
if node_id:
|
|
297
|
+
details = [ErrorDetail(field="node_id", value=node_id)]
|
|
298
|
+
super().__init__(
|
|
299
|
+
code=ErrorCode.EXECUTION_FAILED,
|
|
300
|
+
message=message,
|
|
301
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
302
|
+
details=details,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class LLMServiceError(SmartifyAPIError):
|
|
307
|
+
"""Raised when LLM service is unavailable."""
|
|
308
|
+
def __init__(self, provider: str, message: str, retry_after: Optional[int] = 30):
|
|
309
|
+
super().__init__(
|
|
310
|
+
code=ErrorCode.LLM_SERVICE_UNAVAILABLE,
|
|
311
|
+
message=f"{provider} service error: {message}",
|
|
312
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
313
|
+
retry_after=retry_after,
|
|
314
|
+
details=[ErrorDetail(field="provider", value=provider)],
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ============================================================================
|
|
319
|
+
# Exception Handlers
|
|
320
|
+
# ============================================================================
|
|
321
|
+
|
|
322
|
+
def get_request_id(request: Request) -> Optional[str]:
|
|
323
|
+
"""Extract request ID from headers or generate one."""
|
|
324
|
+
return request.headers.get("X-Request-ID") or request.headers.get("X-Correlation-ID")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
async def smartify_error_handler(request: Request, exc: SmartifyAPIError) -> JSONResponse:
|
|
328
|
+
"""Handle SmartifyAPIError exceptions."""
|
|
329
|
+
request_id = get_request_id(request)
|
|
330
|
+
include_debug = request.app.debug if hasattr(request.app, 'debug') else False
|
|
331
|
+
|
|
332
|
+
response = exc.to_response(request_id=request_id, include_debug=include_debug)
|
|
333
|
+
|
|
334
|
+
headers = {}
|
|
335
|
+
if exc.retry_after:
|
|
336
|
+
headers["Retry-After"] = str(exc.retry_after)
|
|
337
|
+
|
|
338
|
+
return JSONResponse(
|
|
339
|
+
status_code=exc.status_code,
|
|
340
|
+
content=response.model_dump(mode="json", exclude_none=True),
|
|
341
|
+
headers=headers,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
346
|
+
"""Handle unexpected exceptions."""
|
|
347
|
+
import traceback
|
|
348
|
+
import logging
|
|
349
|
+
|
|
350
|
+
logger = logging.getLogger(__name__)
|
|
351
|
+
logger.exception(f"Unhandled exception: {exc}")
|
|
352
|
+
|
|
353
|
+
request_id = get_request_id(request)
|
|
354
|
+
include_debug = request.app.debug if hasattr(request.app, 'debug') else False
|
|
355
|
+
|
|
356
|
+
debug_info = None
|
|
357
|
+
if include_debug:
|
|
358
|
+
debug_info = {
|
|
359
|
+
"exception_type": type(exc).__name__,
|
|
360
|
+
"traceback": traceback.format_exc(),
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
response = ErrorResponse(
|
|
364
|
+
error=ErrorCode.INTERNAL_ERROR.value,
|
|
365
|
+
message="An unexpected error occurred",
|
|
366
|
+
retryable=False,
|
|
367
|
+
request_id=request_id,
|
|
368
|
+
debug=debug_info,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return JSONResponse(
|
|
372
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
373
|
+
content=response.model_dump(mode="json", exclude_none=True),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def register_error_handlers(app):
|
|
378
|
+
"""Register exception handlers with FastAPI app."""
|
|
379
|
+
app.add_exception_handler(SmartifyAPIError, smartify_error_handler)
|
|
380
|
+
# Don't override all exceptions - let FastAPI handle validation errors etc.
|