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/events.py ADDED
@@ -0,0 +1,345 @@
1
+ """Grid execution events for async operation tracking.
2
+
3
+ Provides:
4
+ - Event storage and retrieval
5
+ - Polling endpoint for execution progress
6
+ - WebSocket endpoint for real-time streaming (optional)
7
+ """
8
+
9
+ from datetime import datetime, timezone
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional
12
+ from collections import deque
13
+ from dataclasses import dataclass, field
14
+ import asyncio
15
+ import logging
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ # ============================================================================
24
+ # Event Types
25
+ # ============================================================================
26
+
27
+ class EventType(str, Enum):
28
+ """Types of events that can occur during grid execution."""
29
+
30
+ # Grid lifecycle events
31
+ GRID_LOADED = "grid.loaded"
32
+ GRID_ENERGIZED = "grid.energized"
33
+ GRID_STARTED = "grid.started"
34
+ GRID_PAUSED = "grid.paused"
35
+ GRID_RESUMED = "grid.resumed"
36
+ GRID_STOPPED = "grid.stopped"
37
+ GRID_COMPLETED = "grid.completed"
38
+ GRID_FAILED = "grid.failed"
39
+
40
+ # Node events
41
+ NODE_STARTED = "node.started"
42
+ NODE_COMPLETED = "node.completed"
43
+ NODE_FAILED = "node.failed"
44
+ NODE_SKIPPED = "node.skipped"
45
+ NODE_RETRYING = "node.retrying"
46
+
47
+ # Spark events
48
+ SPARK_SPAWNED = "spark.spawned"
49
+ SPARK_COMPLETED = "spark.completed"
50
+ SPARK_FAILED = "spark.failed"
51
+
52
+ # Breaker events
53
+ BREAKER_WARNING = "breaker.warning"
54
+ BREAKER_TRIPPED = "breaker.tripped"
55
+
56
+ # Approval events
57
+ APPROVAL_REQUESTED = "approval.requested"
58
+ APPROVAL_GRANTED = "approval.granted"
59
+ APPROVAL_DENIED = "approval.denied"
60
+ APPROVAL_EXPIRED = "approval.expired"
61
+
62
+ # Tool events
63
+ TOOL_CALLED = "tool.called"
64
+ TOOL_RESULT = "tool.result"
65
+
66
+
67
+ # ============================================================================
68
+ # Event Models
69
+ # ============================================================================
70
+
71
+ class GridEvent(BaseModel):
72
+ """A single event from grid execution."""
73
+
74
+ id: str = Field(..., description="Unique event ID")
75
+ type: EventType = Field(..., description="Event type")
76
+ grid_id: str = Field(..., description="Grid this event belongs to")
77
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
78
+
79
+ # Optional context
80
+ node_id: Optional[str] = Field(None, description="Node ID if node-specific event")
81
+
82
+ # Event payload
83
+ data: Dict[str, Any] = Field(default_factory=dict, description="Event-specific data")
84
+
85
+ # For progress tracking
86
+ progress: Optional[float] = Field(None, ge=0.0, le=1.0, description="Execution progress (0-1)")
87
+
88
+
89
+ class EventsResponse(BaseModel):
90
+ """Response for events polling endpoint."""
91
+
92
+ grid_id: str
93
+ events: List[GridEvent]
94
+ total: int = Field(..., description="Total events for this grid")
95
+ has_more: bool = Field(..., description="Whether more events exist after these")
96
+
97
+ # Cursor for pagination
98
+ next_cursor: Optional[str] = Field(None, description="Cursor for next page")
99
+
100
+ # Current grid state summary
101
+ state: str
102
+ progress: float = Field(ge=0.0, le=1.0)
103
+ completed_nodes: int
104
+ total_nodes: int
105
+
106
+
107
+ class EventFilter(BaseModel):
108
+ """Filter for events query."""
109
+
110
+ types: Optional[List[EventType]] = Field(None, description="Filter by event types")
111
+ node_id: Optional[str] = Field(None, description="Filter by node ID")
112
+ after: Optional[str] = Field(None, description="Cursor: return events after this ID")
113
+ limit: int = Field(default=50, ge=1, le=200, description="Maximum events to return")
114
+
115
+
116
+ # ============================================================================
117
+ # Event Store
118
+ # ============================================================================
119
+
120
+ @dataclass
121
+ class EventStore:
122
+ """In-memory event storage per grid.
123
+
124
+ Uses a bounded deque to limit memory usage.
125
+ For production, consider Redis Streams or similar.
126
+ """
127
+
128
+ grid_id: str
129
+ max_events: int = 1000
130
+ _events: deque = field(default_factory=lambda: deque(maxlen=1000))
131
+ _event_counter: int = 0
132
+ _lock: asyncio.Lock = field(default_factory=asyncio.Lock)
133
+
134
+ def __post_init__(self):
135
+ self._events = deque(maxlen=self.max_events)
136
+
137
+ async def append(self, event: GridEvent) -> None:
138
+ """Add an event to the store."""
139
+ async with self._lock:
140
+ self._event_counter += 1
141
+ self._events.append(event)
142
+
143
+ async def get_events(
144
+ self,
145
+ after: Optional[str] = None,
146
+ types: Optional[List[EventType]] = None,
147
+ node_id: Optional[str] = None,
148
+ limit: int = 50,
149
+ ) -> tuple[List[GridEvent], bool, Optional[str]]:
150
+ """Get events with filtering and pagination.
151
+
152
+ Returns (events, has_more, next_cursor)
153
+ """
154
+ async with self._lock:
155
+ events = list(self._events)
156
+
157
+ # Apply cursor filter (after)
158
+ if after:
159
+ found_idx = None
160
+ for i, e in enumerate(events):
161
+ if e.id == after:
162
+ found_idx = i
163
+ break
164
+ if found_idx is not None:
165
+ events = events[found_idx + 1:]
166
+
167
+ # Apply type filter
168
+ if types:
169
+ events = [e for e in events if e.type in types]
170
+
171
+ # Apply node filter
172
+ if node_id:
173
+ events = [e for e in events if e.node_id == node_id]
174
+
175
+ # Pagination
176
+ has_more = len(events) > limit
177
+ events = events[:limit]
178
+
179
+ next_cursor = events[-1].id if events and has_more else None
180
+
181
+ return events, has_more, next_cursor
182
+
183
+ async def count(self) -> int:
184
+ """Get total event count."""
185
+ async with self._lock:
186
+ return len(self._events)
187
+
188
+ async def clear(self) -> None:
189
+ """Clear all events."""
190
+ async with self._lock:
191
+ self._events.clear()
192
+
193
+
194
+ # ============================================================================
195
+ # Event Manager
196
+ # ============================================================================
197
+
198
+ class EventManager:
199
+ """Manages event stores for all grids.
200
+
201
+ Thread-safe and async-compatible.
202
+ """
203
+
204
+ def __init__(self, max_events_per_grid: int = 1000):
205
+ self.max_events = max_events_per_grid
206
+ self._stores: Dict[str, EventStore] = {}
207
+ self._lock = asyncio.Lock()
208
+
209
+ # WebSocket subscribers (grid_id -> set of queues)
210
+ self._subscribers: Dict[str, set] = {}
211
+
212
+ async def get_store(self, grid_id: str) -> EventStore:
213
+ """Get or create event store for a grid."""
214
+ async with self._lock:
215
+ if grid_id not in self._stores:
216
+ self._stores[grid_id] = EventStore(
217
+ grid_id=grid_id,
218
+ max_events=self.max_events,
219
+ )
220
+ return self._stores[grid_id]
221
+
222
+ async def emit(
223
+ self,
224
+ grid_id: str,
225
+ event_type: EventType,
226
+ node_id: Optional[str] = None,
227
+ data: Optional[Dict[str, Any]] = None,
228
+ progress: Optional[float] = None,
229
+ ) -> GridEvent:
230
+ """Emit an event for a grid.
231
+
232
+ Args:
233
+ grid_id: Grid identifier
234
+ event_type: Type of event
235
+ node_id: Optional node ID for node-specific events
236
+ data: Optional event payload
237
+ progress: Optional execution progress (0-1)
238
+
239
+ Returns:
240
+ The created event
241
+ """
242
+ store = await self.get_store(grid_id)
243
+
244
+ event = GridEvent(
245
+ id=f"evt_{grid_id}_{store._event_counter + 1}",
246
+ type=event_type,
247
+ grid_id=grid_id,
248
+ node_id=node_id,
249
+ data=data or {},
250
+ progress=progress,
251
+ )
252
+
253
+ await store.append(event)
254
+
255
+ # Notify WebSocket subscribers
256
+ await self._notify_subscribers(grid_id, event)
257
+
258
+ logger.debug(f"Event emitted: {event_type.value} for grid {grid_id}")
259
+ return event
260
+
261
+ async def get_events(
262
+ self,
263
+ grid_id: str,
264
+ filter: Optional[EventFilter] = None,
265
+ ) -> tuple[List[GridEvent], bool, Optional[str]]:
266
+ """Get events for a grid with optional filtering."""
267
+ store = await self.get_store(grid_id)
268
+
269
+ if filter:
270
+ return await store.get_events(
271
+ after=filter.after,
272
+ types=filter.types,
273
+ node_id=filter.node_id,
274
+ limit=filter.limit,
275
+ )
276
+ else:
277
+ return await store.get_events()
278
+
279
+ async def subscribe(self, grid_id: str) -> asyncio.Queue:
280
+ """Subscribe to real-time events for a grid.
281
+
282
+ Returns a queue that will receive events.
283
+ """
284
+ async with self._lock:
285
+ if grid_id not in self._subscribers:
286
+ self._subscribers[grid_id] = set()
287
+
288
+ queue: asyncio.Queue = asyncio.Queue()
289
+ self._subscribers[grid_id].add(queue)
290
+ return queue
291
+
292
+ async def unsubscribe(self, grid_id: str, queue: asyncio.Queue) -> None:
293
+ """Unsubscribe from real-time events."""
294
+ async with self._lock:
295
+ if grid_id in self._subscribers:
296
+ self._subscribers[grid_id].discard(queue)
297
+ if not self._subscribers[grid_id]:
298
+ del self._subscribers[grid_id]
299
+
300
+ async def _notify_subscribers(self, grid_id: str, event: GridEvent) -> None:
301
+ """Notify all subscribers of a new event."""
302
+ async with self._lock:
303
+ subscribers = self._subscribers.get(grid_id, set()).copy()
304
+
305
+ for queue in subscribers:
306
+ try:
307
+ queue.put_nowait(event)
308
+ except asyncio.QueueFull:
309
+ logger.warning(f"Event queue full for grid {grid_id}, dropping event")
310
+
311
+ async def cleanup_grid(self, grid_id: str) -> None:
312
+ """Clean up event store for a deleted grid."""
313
+ async with self._lock:
314
+ if grid_id in self._stores:
315
+ del self._stores[grid_id]
316
+ if grid_id in self._subscribers:
317
+ # Close all subscriber queues
318
+ for queue in self._subscribers[grid_id]:
319
+ queue.put_nowait(None) # Signal end
320
+ del self._subscribers[grid_id]
321
+
322
+
323
+ # Global event manager instance
324
+ event_manager = EventManager()
325
+
326
+
327
+ # ============================================================================
328
+ # Helper Functions
329
+ # ============================================================================
330
+
331
+ def create_grid_event(
332
+ grid_id: str,
333
+ event_type: EventType,
334
+ node_id: Optional[str] = None,
335
+ **data,
336
+ ) -> GridEvent:
337
+ """Create an event (for use when not async)."""
338
+ import uuid
339
+ return GridEvent(
340
+ id=f"evt_{uuid.uuid4().hex[:8]}",
341
+ type=event_type,
342
+ grid_id=grid_id,
343
+ node_id=node_id,
344
+ data=data,
345
+ )