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.
Files changed (46) hide show
  1. smartify/__init__.py +3 -0
  2. smartify/agents/__init__.py +0 -0
  3. smartify/agents/adapters/__init__.py +13 -0
  4. smartify/agents/adapters/anthropic.py +253 -0
  5. smartify/agents/adapters/openai.py +289 -0
  6. smartify/api/__init__.py +26 -0
  7. smartify/api/auth.py +352 -0
  8. smartify/api/errors.py +380 -0
  9. smartify/api/events.py +345 -0
  10. smartify/api/server.py +992 -0
  11. smartify/cli/__init__.py +1 -0
  12. smartify/cli/main.py +430 -0
  13. smartify/engine/__init__.py +64 -0
  14. smartify/engine/approval.py +479 -0
  15. smartify/engine/orchestrator.py +1365 -0
  16. smartify/engine/scheduler.py +380 -0
  17. smartify/engine/spark.py +294 -0
  18. smartify/guardrails/__init__.py +22 -0
  19. smartify/guardrails/breakers.py +409 -0
  20. smartify/models/__init__.py +61 -0
  21. smartify/models/grid.py +625 -0
  22. smartify/notifications/__init__.py +22 -0
  23. smartify/notifications/webhook.py +556 -0
  24. smartify/state/__init__.py +46 -0
  25. smartify/state/checkpoint.py +558 -0
  26. smartify/state/resume.py +301 -0
  27. smartify/state/store.py +370 -0
  28. smartify/tools/__init__.py +17 -0
  29. smartify/tools/base.py +196 -0
  30. smartify/tools/builtin/__init__.py +79 -0
  31. smartify/tools/builtin/file.py +464 -0
  32. smartify/tools/builtin/http.py +195 -0
  33. smartify/tools/builtin/shell.py +137 -0
  34. smartify/tools/mcp/__init__.py +33 -0
  35. smartify/tools/mcp/adapter.py +157 -0
  36. smartify/tools/mcp/client.py +334 -0
  37. smartify/tools/mcp/registry.py +130 -0
  38. smartify/validator/__init__.py +0 -0
  39. smartify/validator/validate.py +271 -0
  40. smartify/workspace/__init__.py +5 -0
  41. smartify/workspace/manager.py +248 -0
  42. smartify_ai-0.1.0.dist-info/METADATA +201 -0
  43. smartify_ai-0.1.0.dist-info/RECORD +46 -0
  44. smartify_ai-0.1.0.dist-info/WHEEL +4 -0
  45. smartify_ai-0.1.0.dist-info/entry_points.txt +2 -0
  46. 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.