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
@@ -0,0 +1,409 @@
1
+ """Breaker system for Smartify grid execution.
2
+
3
+ Breakers are safety limits that can trip during execution:
4
+ - tokens: Total token usage limits
5
+ - time: Execution time limits
6
+ - cost: Cost limits in USD
7
+ - requests: Rate limiting (requests per minute, concurrent agents)
8
+
9
+ When a breaker trips, a configured action is taken:
10
+ - notify: Log warning, continue execution
11
+ - pause: Pause grid, await resume
12
+ - cooldown: Wait before continuing
13
+ - require_approval: Request human approval
14
+ - stop: Stop grid execution
15
+ - downgrade: Switch to cheaper model
16
+ - block: Block and fail immediately
17
+ """
18
+
19
+ import asyncio
20
+ import logging
21
+ import time
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime
24
+ from enum import Enum
25
+ from typing import Any, Callable, Dict, List, Optional, Awaitable
26
+
27
+ from smartify.models.grid import (
28
+ BreakerSpec,
29
+ BreakerActions,
30
+ TripAction,
31
+ BreakerStatus,
32
+ TokenLimits,
33
+ TimeLimits,
34
+ CostLimits,
35
+ RequestLimits,
36
+ )
37
+
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class BreakerType(str, Enum):
43
+ """Types of breakers."""
44
+ TOKENS = "tokens"
45
+ TIME = "time"
46
+ COST = "cost"
47
+ REQUESTS = "requests"
48
+
49
+
50
+ @dataclass
51
+ class BreakerTrip:
52
+ """Record of a breaker trip event."""
53
+ breaker_type: BreakerType
54
+ action: TripAction
55
+ reason: str
56
+ current_value: float
57
+ limit_value: float
58
+ timestamp: datetime = field(default_factory=datetime.now)
59
+ resolved: bool = False
60
+
61
+
62
+ @dataclass
63
+ class RateLimitState:
64
+ """Tracks request rate limiting state."""
65
+ window_start: float = field(default_factory=time.time)
66
+ request_count: int = 0
67
+ concurrent_count: int = 0
68
+
69
+ def reset_window(self) -> None:
70
+ """Reset the rate limit window."""
71
+ self.window_start = time.time()
72
+ self.request_count = 0
73
+
74
+
75
+ @dataclass
76
+ class BreakerState:
77
+ """Current state of all breakers."""
78
+ # Cumulative tracking
79
+ total_tokens: int = 0
80
+ total_cost: float = 0.0
81
+ start_time: Optional[float] = None
82
+
83
+ # Rate limiting
84
+ rate_limit: RateLimitState = field(default_factory=RateLimitState)
85
+
86
+ # Trip history
87
+ trips: List[BreakerTrip] = field(default_factory=list)
88
+
89
+ # Current status
90
+ status: BreakerStatus = BreakerStatus.OK
91
+ paused: bool = False
92
+ cooldown_until: Optional[float] = None
93
+ awaiting_approval: bool = False
94
+ downgraded: bool = False
95
+
96
+ @property
97
+ def elapsed_seconds(self) -> float:
98
+ """Get elapsed execution time in seconds."""
99
+ if self.start_time is None:
100
+ return 0.0
101
+ return time.time() - self.start_time
102
+
103
+
104
+ class BreakerManager:
105
+ """Manages breaker checking and trip actions during grid execution.
106
+
107
+ Usage:
108
+ manager = BreakerManager(spec, actions)
109
+ manager.start() # Start timing
110
+
111
+ # Before each node execution:
112
+ await manager.check_and_enforce()
113
+
114
+ # After LLM call:
115
+ manager.record_usage(tokens=1500, cost=0.02)
116
+
117
+ # After node completion:
118
+ manager.record_request_complete()
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ spec: BreakerSpec,
124
+ actions: BreakerActions,
125
+ on_pause: Optional[Callable[[], Awaitable[None]]] = None,
126
+ on_approval_required: Optional[Callable[[str], Awaitable[bool]]] = None,
127
+ on_downgrade: Optional[Callable[[], Awaitable[str]]] = None,
128
+ ):
129
+ """Initialize the breaker manager.
130
+
131
+ Args:
132
+ spec: Breaker configuration (limits)
133
+ actions: Trip actions configuration
134
+ on_pause: Async callback when pause action triggered
135
+ on_approval_required: Async callback for approval (returns True if approved)
136
+ on_downgrade: Async callback for downgrade (returns new model name)
137
+ """
138
+ self.spec = spec
139
+ self.actions = actions
140
+ self.state = BreakerState()
141
+
142
+ # Callbacks
143
+ self._on_pause = on_pause
144
+ self._on_approval_required = on_approval_required
145
+ self._on_downgrade = on_downgrade
146
+
147
+ def start(self) -> None:
148
+ """Start the breaker timers."""
149
+ self.state.start_time = time.time()
150
+ self.state.rate_limit = RateLimitState()
151
+ logger.debug("Breaker manager started")
152
+
153
+ def record_usage(self, tokens: int = 0, cost: float = 0.0) -> None:
154
+ """Record token and cost usage."""
155
+ self.state.total_tokens += tokens
156
+ self.state.total_cost += cost
157
+ logger.debug(f"Usage recorded: +{tokens} tokens, +${cost:.4f}")
158
+
159
+ def record_request_start(self) -> None:
160
+ """Record start of a request (for concurrent tracking)."""
161
+ self.state.rate_limit.concurrent_count += 1
162
+
163
+ def record_request_complete(self) -> None:
164
+ """Record completion of a request."""
165
+ self.state.rate_limit.request_count += 1
166
+ self.state.rate_limit.concurrent_count = max(0, self.state.rate_limit.concurrent_count - 1)
167
+
168
+ async def check_and_enforce(self) -> Optional[BreakerTrip]:
169
+ """Check all breakers and enforce trip actions.
170
+
171
+ Returns:
172
+ BreakerTrip if a breaker tripped, None otherwise.
173
+
174
+ Raises:
175
+ BreakerError: If block action triggered
176
+ """
177
+ # Check cooldown first
178
+ if self.state.cooldown_until:
179
+ remaining = self.state.cooldown_until - time.time()
180
+ if remaining > 0:
181
+ logger.info(f"In cooldown, waiting {remaining:.1f}s")
182
+ await asyncio.sleep(remaining)
183
+ self.state.cooldown_until = None
184
+
185
+ # Check if paused
186
+ if self.state.paused:
187
+ logger.info("Execution paused, awaiting resume")
188
+ while self.state.paused:
189
+ await asyncio.sleep(0.5)
190
+
191
+ # Check each breaker type
192
+ trip = None
193
+
194
+ trip = trip or await self._check_tokens()
195
+ trip = trip or await self._check_time()
196
+ trip = trip or await self._check_cost()
197
+ trip = trip or await self._check_requests()
198
+
199
+ return trip
200
+
201
+ async def _check_tokens(self) -> Optional[BreakerTrip]:
202
+ """Check token limits."""
203
+ if not self.spec.tokens:
204
+ return None
205
+
206
+ limits = self.spec.tokens
207
+ limit = limits.maxTotalTokensPerRun
208
+
209
+ # Check warning threshold (80% of limit)
210
+ warn_at = int(limit * 0.8)
211
+ if self.state.total_tokens >= warn_at:
212
+ if self.state.status == BreakerStatus.OK:
213
+ self.state.status = BreakerStatus.WARNING
214
+ logger.warning(f"Token warning: {self.state.total_tokens}/{limit}")
215
+
216
+ # Check hard limit
217
+ if self.state.total_tokens >= limit:
218
+ return await self._trip(
219
+ BreakerType.TOKENS,
220
+ self.actions.onTokensLimit,
221
+ f"Token limit exceeded: {self.state.total_tokens}/{limit}",
222
+ self.state.total_tokens,
223
+ limit,
224
+ )
225
+
226
+ return None
227
+
228
+ async def _check_time(self) -> Optional[BreakerTrip]:
229
+ """Check time limits."""
230
+ if not self.spec.time:
231
+ return None
232
+
233
+ limits = self.spec.time
234
+ elapsed = self.state.elapsed_seconds
235
+ max_seconds = limits.maxRuntimeSeconds
236
+
237
+ # Check warning threshold (80% of limit)
238
+ warn_at = max_seconds * 0.8
239
+ if elapsed >= warn_at:
240
+ if self.state.status == BreakerStatus.OK:
241
+ self.state.status = BreakerStatus.WARNING
242
+ logger.warning(f"Time warning: {elapsed:.1f}s/{max_seconds}s")
243
+
244
+ # Check hard limit
245
+ if elapsed >= max_seconds:
246
+ return await self._trip(
247
+ BreakerType.TIME,
248
+ self.actions.onTimeLimit,
249
+ f"Time limit exceeded: {elapsed:.1f}s/{max_seconds}s",
250
+ elapsed,
251
+ max_seconds,
252
+ )
253
+
254
+ return None
255
+
256
+ async def _check_cost(self) -> Optional[BreakerTrip]:
257
+ """Check cost limits."""
258
+ if not self.spec.cost:
259
+ return None
260
+
261
+ limits = self.spec.cost
262
+ limit = limits.maxCostPerRun
263
+
264
+ # Check warning threshold (80% of limit)
265
+ warn_at = limit * 0.8
266
+ if self.state.total_cost >= warn_at:
267
+ if self.state.status == BreakerStatus.OK:
268
+ self.state.status = BreakerStatus.WARNING
269
+ logger.warning(f"Cost warning: ${self.state.total_cost:.4f}/${limit}")
270
+
271
+ # Check hard limit
272
+ if self.state.total_cost >= limit:
273
+ return await self._trip(
274
+ BreakerType.COST,
275
+ self.actions.onCostLimit,
276
+ f"Cost limit exceeded: ${self.state.total_cost:.4f}/${limit}",
277
+ self.state.total_cost,
278
+ limit,
279
+ )
280
+
281
+ return None
282
+
283
+ async def _check_requests(self) -> Optional[BreakerTrip]:
284
+ """Check request rate limits."""
285
+ if not self.spec.requests:
286
+ return None
287
+
288
+ limits = self.spec.requests
289
+ rl = self.state.rate_limit
290
+
291
+ # Reset window if needed (1 minute windows)
292
+ if time.time() - rl.window_start >= 60:
293
+ rl.reset_window()
294
+
295
+ # Check requests per minute
296
+ if limits.maxRequestsPerMinute and rl.request_count >= limits.maxRequestsPerMinute:
297
+ return await self._trip(
298
+ BreakerType.REQUESTS,
299
+ self.actions.onRequestsLimit,
300
+ f"Rate limit exceeded: {rl.request_count}/{limits.maxRequestsPerMinute} req/min",
301
+ rl.request_count,
302
+ limits.maxRequestsPerMinute,
303
+ )
304
+
305
+ # Check concurrent agents
306
+ if limits.maxConcurrentAgents and rl.concurrent_count >= limits.maxConcurrentAgents:
307
+ return await self._trip(
308
+ BreakerType.REQUESTS,
309
+ self.actions.onRequestsLimit,
310
+ f"Concurrent limit exceeded: {rl.concurrent_count}/{limits.maxConcurrentAgents}",
311
+ rl.concurrent_count,
312
+ limits.maxConcurrentAgents,
313
+ )
314
+
315
+ return None
316
+
317
+ async def _trip(
318
+ self,
319
+ breaker_type: BreakerType,
320
+ action: TripAction,
321
+ reason: str,
322
+ current: float,
323
+ limit: float,
324
+ ) -> BreakerTrip:
325
+ """Handle a breaker trip."""
326
+ trip = BreakerTrip(
327
+ breaker_type=breaker_type,
328
+ action=action,
329
+ reason=reason,
330
+ current_value=current,
331
+ limit_value=limit,
332
+ )
333
+ self.state.trips.append(trip)
334
+ self.state.status = BreakerStatus.TRIPPED
335
+
336
+ logger.warning(f"Breaker tripped: {reason} → action: {action.value}")
337
+
338
+ # Execute the trip action
339
+ await self._execute_action(action, reason)
340
+
341
+ return trip
342
+
343
+ async def _execute_action(self, action: TripAction, reason: str) -> None:
344
+ """Execute a trip action."""
345
+ if action == TripAction.NOTIFY:
346
+ # Just log, already done
347
+ pass
348
+
349
+ elif action == TripAction.PAUSE:
350
+ self.state.paused = True
351
+ if self._on_pause:
352
+ await self._on_pause()
353
+
354
+ elif action == TripAction.COOLDOWN:
355
+ cooldown_secs = self.actions.cooldownSeconds
356
+ self.state.cooldown_until = time.time() + cooldown_secs
357
+ logger.info(f"Entering cooldown for {cooldown_secs}s")
358
+ await asyncio.sleep(cooldown_secs)
359
+ self.state.cooldown_until = None
360
+
361
+ elif action == TripAction.REQUIRE_APPROVAL:
362
+ self.state.awaiting_approval = True
363
+ if self._on_approval_required:
364
+ approved = await self._on_approval_required(reason)
365
+ if not approved:
366
+ raise BreakerError(f"Approval denied: {reason}")
367
+ self.state.awaiting_approval = False
368
+
369
+ elif action == TripAction.STOP:
370
+ raise BreakerError(f"Breaker stop: {reason}")
371
+
372
+ elif action == TripAction.DOWNGRADE:
373
+ self.state.downgraded = True
374
+ if self._on_downgrade:
375
+ new_model = await self._on_downgrade()
376
+ logger.info(f"Downgraded to model: {new_model}")
377
+
378
+ elif action == TripAction.BLOCK:
379
+ raise BreakerError(f"Breaker blocked: {reason}")
380
+
381
+ def resume(self) -> None:
382
+ """Resume paused execution."""
383
+ self.state.paused = False
384
+ logger.info("Execution resumed")
385
+
386
+ def get_summary(self) -> Dict[str, Any]:
387
+ """Get a summary of breaker state."""
388
+ return {
389
+ "status": self.state.status.value,
390
+ "total_tokens": self.state.total_tokens,
391
+ "total_cost": self.state.total_cost,
392
+ "elapsed_seconds": self.state.elapsed_seconds,
393
+ "trips": [
394
+ {
395
+ "type": t.breaker_type.value,
396
+ "action": t.action.value,
397
+ "reason": t.reason,
398
+ "timestamp": t.timestamp.isoformat(),
399
+ }
400
+ for t in self.state.trips
401
+ ],
402
+ "paused": self.state.paused,
403
+ "downgraded": self.state.downgraded,
404
+ }
405
+
406
+
407
+ class BreakerError(Exception):
408
+ """Raised when a breaker trips with stop/block action."""
409
+ pass
@@ -0,0 +1,61 @@
1
+ """Pydantic models for Smartify Grid specification."""
2
+
3
+ from smartify.models.grid import (
4
+ # Enums
5
+ NodeKind,
6
+ TriggerType,
7
+ ExecutionMode,
8
+ AutonomyMode,
9
+ TripAction,
10
+ BreakerStatus,
11
+ GridState,
12
+ WorkspaceType,
13
+ ArtifactType,
14
+ AggregateStrategy,
15
+ # Specs
16
+ GridSpec,
17
+ MetadataSpec,
18
+ TopologySpec,
19
+ NodeSpec,
20
+ EdgeSpec,
21
+ TriggerSpec,
22
+ InputSpec,
23
+ EnvironmentSpec,
24
+ OutputsSpec,
25
+ AgentSpec,
26
+ ToolsSpec,
27
+ GuardrailsSpec,
28
+ WorkspaceSpec,
29
+ BreakerSpec,
30
+ BreakerActions,
31
+ )
32
+
33
+ __all__ = [
34
+ # Enums
35
+ "NodeKind",
36
+ "TriggerType",
37
+ "ExecutionMode",
38
+ "AutonomyMode",
39
+ "TripAction",
40
+ "BreakerStatus",
41
+ "GridState",
42
+ "WorkspaceType",
43
+ "ArtifactType",
44
+ "AggregateStrategy",
45
+ # Specs
46
+ "GridSpec",
47
+ "MetadataSpec",
48
+ "TopologySpec",
49
+ "NodeSpec",
50
+ "EdgeSpec",
51
+ "TriggerSpec",
52
+ "InputSpec",
53
+ "EnvironmentSpec",
54
+ "OutputsSpec",
55
+ "AgentSpec",
56
+ "ToolsSpec",
57
+ "GuardrailsSpec",
58
+ "WorkspaceSpec",
59
+ "BreakerSpec",
60
+ "BreakerActions",
61
+ ]