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,625 @@
1
+ """Pydantic models for Grid YAML specification."""
2
+
3
+ from enum import Enum
4
+ from typing import Any, Dict, List, Optional, Union
5
+ from pydantic import BaseModel, Field, field_validator
6
+
7
+
8
+ # ============================================================================
9
+ # Enums
10
+ # ============================================================================
11
+
12
+ class NodeKind(str, Enum):
13
+ """Types of nodes in the grid topology."""
14
+ CONTROLLER = "controller"
15
+ RELAY = "relay"
16
+ SUBSTATION = "substation"
17
+ SPARK = "spark" # Runtime-only, not user-defined
18
+ FOREACH = "foreach"
19
+ EXPR = "expr"
20
+ AGGREGATE = "aggregate"
21
+ APPROVAL = "approval"
22
+
23
+
24
+ class TriggerType(str, Enum):
25
+ """Types of grid triggers."""
26
+ MANUAL = "manual"
27
+ SCHEDULE = "schedule"
28
+ WEBHOOK = "webhook"
29
+ EVENT = "event"
30
+
31
+
32
+ class ExecutionMode(str, Enum):
33
+ """How node activation order is determined."""
34
+ PARENT = "parent"
35
+ EXPLICIT = "explicit"
36
+ DYNAMIC = "dynamic"
37
+
38
+
39
+ class AutonomyMode(str, Enum):
40
+ """Level of autonomous action permitted."""
41
+ OBSERVE = "observe"
42
+ RECOMMEND = "recommend"
43
+ ACT_WITH_LIMITS = "act_with_limits"
44
+ FULL_AUTONOMY = "full_autonomy"
45
+
46
+
47
+ class TripAction(str, Enum):
48
+ """Actions when a breaker trips."""
49
+ NOTIFY = "notify"
50
+ PAUSE = "pause"
51
+ COOLDOWN = "cooldown"
52
+ REQUIRE_APPROVAL = "require_approval"
53
+ STOP = "stop"
54
+ DOWNGRADE = "downgrade"
55
+ BLOCK = "block"
56
+
57
+
58
+ class BreakerStatus(str, Enum):
59
+ """Breaker status levels."""
60
+ OK = "ok"
61
+ WARNING = "warning"
62
+ TRIPPED = "tripped"
63
+
64
+
65
+ class GridState(str, Enum):
66
+ """Grid execution states."""
67
+ DRAFT = "draft"
68
+ READY = "ready"
69
+ ENERGIZED = "energized"
70
+ RUNNING = "running"
71
+ COMPLETED = "completed"
72
+ PAUSED = "paused"
73
+ STOPPED = "stopped"
74
+ FAILED = "failed"
75
+
76
+
77
+ class WorkspaceType(str, Enum):
78
+ """Workspace environment types."""
79
+ CONTAINER = "container"
80
+ LOCAL = "local"
81
+
82
+
83
+ class ArtifactType(str, Enum):
84
+ """Types of output artifacts."""
85
+ CODE = "code"
86
+ CONFIG = "config"
87
+ DOCS = "docs"
88
+ DATA = "data"
89
+ REPORT = "report"
90
+ SCREENSHOT = "screenshot"
91
+
92
+
93
+ class AggregateStrategy(str, Enum):
94
+ """Merge strategies for aggregate nodes."""
95
+ MERGE_OBJECTS = "merge_objects"
96
+ CONCAT_ARRAYS = "concat_arrays"
97
+ SUM = "sum"
98
+ FIRST = "first"
99
+ LAST = "last"
100
+
101
+
102
+ # ============================================================================
103
+ # Metadata
104
+ # ============================================================================
105
+
106
+ class CompatibilitySpec(BaseModel):
107
+ """Runtime compatibility requirements."""
108
+ minRuntimeVersion: str = "0.1.0"
109
+
110
+
111
+ class MetadataSpec(BaseModel):
112
+ """Grid metadata."""
113
+ id: str
114
+ name: str
115
+ description: Optional[str] = None
116
+ version: str = "1.0.0"
117
+ owner: str = "default"
118
+ tags: List[str] = Field(default_factory=list)
119
+ compatibility: Optional[CompatibilitySpec] = None
120
+
121
+
122
+ # ============================================================================
123
+ # Topology
124
+ # ============================================================================
125
+
126
+ class TriggerConfig(BaseModel):
127
+ """Trigger configuration."""
128
+ requireConfirmation: bool = False
129
+ # Schedule-specific
130
+ cron: Optional[str] = None
131
+ # Webhook-specific
132
+ path: Optional[str] = None
133
+ secret: Optional[str] = None
134
+
135
+
136
+ class TriggerSpec(BaseModel):
137
+ """How grid execution is started."""
138
+ type: TriggerType = TriggerType.MANUAL
139
+ config: TriggerConfig = Field(default_factory=TriggerConfig)
140
+
141
+
142
+ class RetrySpec(BaseModel):
143
+ """Retry configuration for nodes."""
144
+ maxAttempts: int = Field(default=1, ge=0, le=10)
145
+ backoffSeconds: int = Field(default=5, ge=0, le=300)
146
+ retryOn: List[str] = Field(default_factory=lambda: ["error"])
147
+
148
+
149
+ class ForeachSpec(BaseModel):
150
+ """Configuration for foreach nodes."""
151
+ over: str # Expression returning list
152
+ as_: str = Field(default="item", alias="as")
153
+ maxIterations: int = Field(default=50, le=100)
154
+ parallel: bool = True
155
+ body: Dict[str, Any] = Field(default_factory=dict) # {to: node_id}
156
+
157
+
158
+ class AggregateSpec(BaseModel):
159
+ """Configuration for aggregate nodes."""
160
+ from_: List[str] = Field(alias="from") # Source node IDs
161
+ strategy: AggregateStrategy = AggregateStrategy.MERGE_OBJECTS
162
+ waitFor: Union[str, int] = "all" # "all", "any", or number
163
+
164
+
165
+ class ApprovalSpec(BaseModel):
166
+ """Configuration for approval nodes."""
167
+ prompt: str = "Please review and approve to continue"
168
+ showOutputsFrom: List[str] = Field(default_factory=list)
169
+ timeout: int = Field(default=86400, le=604800) # Max 7 days
170
+ requiredApprovers: int = 1
171
+ allowedApprovers: List[str] = Field(default_factory=list)
172
+ autoApprove: Optional[Dict[str, str]] = None # {when: expression}
173
+
174
+
175
+ class PromptSpec(BaseModel):
176
+ """Prompt configuration for agent nodes."""
177
+ system: Optional[str] = None # System prompt
178
+ template: Optional[str] = None # User message template with {{variable}} placeholders
179
+ context: Optional[List[str]] = None # Additional context sources
180
+
181
+
182
+ class NodeSpec(BaseModel):
183
+ """A node in the grid topology."""
184
+ id: str
185
+ kind: NodeKind
186
+ name: str
187
+ description: Optional[str] = None
188
+ parent: Optional[str] = None
189
+ agent: Optional[str] = None
190
+ capabilities: List[str] = Field(default_factory=list)
191
+ when: Optional[str] = None # Activation condition expression
192
+ parallel: bool = True
193
+ runAfter: Optional[List[str]] = None
194
+ retry: Optional[RetrySpec] = None
195
+ outputSchema: Optional[Dict[str, Any]] = None
196
+ prompt: Optional[PromptSpec] = None # Prompt configuration for agent nodes
197
+ tools: Optional[List[str]] = None # Tools available to this node
198
+
199
+ # Kind-specific configs
200
+ foreach: Optional[ForeachSpec] = None
201
+ aggregate: Optional[AggregateSpec] = None
202
+ approval: Optional[ApprovalSpec] = None
203
+ expr: Optional[str] = None # Expression string for expr nodes
204
+
205
+ @field_validator('kind')
206
+ @classmethod
207
+ def validate_kind(cls, v: NodeKind) -> NodeKind:
208
+ if v == NodeKind.SPARK:
209
+ raise ValueError("spark nodes cannot be user-defined; they are spawned at runtime")
210
+ return v
211
+
212
+
213
+ class EdgeSpec(BaseModel):
214
+ """Explicit connection between nodes."""
215
+ from_: str = Field(alias="from")
216
+ to: Union[str, List[str]]
217
+ when: Optional[str] = None
218
+ condition: Optional[str] = None # Legacy: "always", "never"
219
+ parallel: bool = False
220
+
221
+
222
+ class DynamicSpawningLimits(BaseModel):
223
+ """Limits for dynamic node spawning."""
224
+ maxRelaysPerController: int = 4
225
+ maxSubstationsPerRelay: int = 3
226
+ maxSparksPerSubstation: int = 3
227
+ maxTotalNodes: int = 20
228
+
229
+
230
+ class DynamicSpawningDefaults(BaseModel):
231
+ """Default agents for spawned nodes."""
232
+ relay: Optional[str] = None
233
+ substation: Optional[str] = None
234
+ spark: Optional[str] = None
235
+
236
+
237
+ class DynamicSpawningSpec(BaseModel):
238
+ """Runtime node creation configuration."""
239
+ enabled: bool = True
240
+ enableSparks: bool = True
241
+ limits: DynamicSpawningLimits = Field(default_factory=DynamicSpawningLimits)
242
+ defaults: DynamicSpawningDefaults = Field(default_factory=DynamicSpawningDefaults)
243
+ requireApproval: bool = False
244
+
245
+
246
+ class TopologySpec(BaseModel):
247
+ """Grid topology definition."""
248
+ trigger: TriggerSpec = Field(default_factory=TriggerSpec)
249
+ executionMode: ExecutionMode = ExecutionMode.PARENT
250
+ nodes: List[NodeSpec]
251
+ edges: Optional[List[EdgeSpec]] = None
252
+ dynamicSpawning: DynamicSpawningSpec = Field(default_factory=DynamicSpawningSpec)
253
+
254
+
255
+ # ============================================================================
256
+ # Inputs
257
+ # ============================================================================
258
+
259
+ class InputSpec(BaseModel):
260
+ """Input parameter definition."""
261
+ name: str
262
+ type: str = "string" # string, boolean, number, integer, array, object
263
+ required: bool = False
264
+ default: Optional[Any] = None
265
+ enum: Optional[List[Any]] = None
266
+ description: Optional[str] = None
267
+
268
+
269
+ # ============================================================================
270
+ # Environment
271
+ # ============================================================================
272
+
273
+ class SecretSpec(BaseModel):
274
+ """Secret definition."""
275
+ name: str
276
+ source: str = "env" # "env" or "prompt"
277
+ optional: bool = True
278
+
279
+
280
+ class EnvironmentSpec(BaseModel):
281
+ """Environment variables and secrets."""
282
+ variables: Dict[str, str] = Field(default_factory=dict)
283
+ secrets: List[SecretSpec] = Field(default_factory=list)
284
+
285
+
286
+ # ============================================================================
287
+ # Outputs
288
+ # ============================================================================
289
+
290
+ class ArtifactSpec(BaseModel):
291
+ """Output artifact definition."""
292
+ path: str
293
+ type: Optional[ArtifactType] = None
294
+ description: Optional[str] = None
295
+
296
+
297
+ class SuccessCriterion(BaseModel):
298
+ """Success criterion for grid completion."""
299
+ name: str
300
+ command: Optional[str] = None
301
+ exitCode: int = 0
302
+ optional: bool = False
303
+
304
+
305
+ class OutputsSpec(BaseModel):
306
+ """Expected outputs from the grid."""
307
+ artifacts: List[ArtifactSpec] = Field(default_factory=list)
308
+ successCriteria: List[SuccessCriterion] = Field(default_factory=list)
309
+
310
+
311
+ # ============================================================================
312
+ # Agents
313
+ # ============================================================================
314
+
315
+ class ModelPolicy(BaseModel):
316
+ """Model selection policy."""
317
+ preferred: str = "claude-sonnet-4-20250514"
318
+ fallback: List[str] = Field(default_factory=list)
319
+
320
+
321
+ class ContextSource(BaseModel):
322
+ """Context source for agents."""
323
+ type: str # "artifacts", "memory", "files"
324
+ filter: Optional[str] = None # Glob pattern
325
+ key: Optional[str] = None # Memory key
326
+
327
+
328
+ class AgentSpec(BaseModel):
329
+ """Agent definition."""
330
+ modelPolicy: Optional[ModelPolicy] = None
331
+ systemPrompt: Optional[str] = None
332
+ systemPromptRef: Optional[str] = None # "builtin:controller" or "prompts/file.md"
333
+ maxDelegation: int = 4
334
+ tools: Optional[List[str]] = None
335
+ contextSources: List[ContextSource] = Field(default_factory=list)
336
+ role: Optional[str] = None # "controller", "relay", "substation", "spark"
337
+
338
+
339
+ # ============================================================================
340
+ # Tools
341
+ # ============================================================================
342
+
343
+ class ToolSpec(BaseModel):
344
+ """Tool configuration."""
345
+ id: str
346
+ enabled: bool = True
347
+ config: Dict[str, Any] = Field(default_factory=dict)
348
+
349
+
350
+ class McpServerSpec(BaseModel):
351
+ """MCP server configuration for external tool integration.
352
+
353
+ Allows grids to use tools from external MCP servers.
354
+
355
+ Example:
356
+ mcpServers:
357
+ - id: filesystem
358
+ transport: stdio
359
+ command: npx
360
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
361
+ prefix: fs
362
+ """
363
+ id: str = Field(..., description="Unique identifier for this MCP server")
364
+ transport: str = Field(
365
+ default="stdio",
366
+ description="Transport type: 'stdio', 'sse', or 'streamable_http'"
367
+ )
368
+
369
+ # For stdio transport
370
+ command: Optional[str] = Field(None, description="Command to start the MCP server")
371
+ args: List[str] = Field(default_factory=list, description="Command arguments")
372
+ env: Optional[Dict[str, str]] = Field(None, description="Environment variables")
373
+ cwd: Optional[str] = Field(None, description="Working directory")
374
+
375
+ # For SSE/HTTP transport
376
+ url: Optional[str] = Field(None, description="URL for SSE or HTTP transport")
377
+ headers: Optional[Dict[str, str]] = Field(None, description="HTTP headers")
378
+
379
+ # Tool naming and filtering
380
+ prefix: Optional[str] = Field(
381
+ None,
382
+ description="Prefix for tool names (e.g. 'fs' -> 'fs_read_file')"
383
+ )
384
+ tools: Optional[List[str]] = Field(
385
+ None,
386
+ description="Only expose these tools (if set)"
387
+ )
388
+
389
+
390
+ class ToolsSpec(BaseModel):
391
+ """Tool configuration section."""
392
+ enabled: List[str] = Field(default_factory=list)
393
+ disabled: List[str] = Field(default_factory=list)
394
+ custom: List[ToolSpec] = Field(default_factory=list)
395
+ mcpServers: List[McpServerSpec] = Field(
396
+ default_factory=list,
397
+ description="External MCP servers to connect to for additional tools"
398
+ )
399
+
400
+
401
+ # ============================================================================
402
+ # Guardrails
403
+ # ============================================================================
404
+
405
+ class TokenLimits(BaseModel):
406
+ """Token usage limits."""
407
+ maxInputTokensPerRequest: int = 100_000
408
+ maxOutputTokensPerRequest: int = 16_000
409
+ maxTotalTokensPerRun: int = 1_000_000
410
+
411
+
412
+ class TimeLimits(BaseModel):
413
+ """Time limits."""
414
+ maxRuntimeSeconds: int = 3600
415
+ maxTaskSeconds: int = 300
416
+
417
+
418
+ class CostLimits(BaseModel):
419
+ """Cost limits."""
420
+ maxCostPerRun: float = 10.0
421
+ maxCostPerDay: float = 100.0
422
+
423
+
424
+ class RequestLimits(BaseModel):
425
+ """Request rate limits."""
426
+ maxRequestsPerMinute: int = 60
427
+ maxConcurrentAgents: int = 5
428
+
429
+
430
+ class BreakerSpec(BaseModel):
431
+ """Breaker configuration."""
432
+ tokens: Optional[TokenLimits] = None
433
+ time: Optional[TimeLimits] = None
434
+ cost: Optional[CostLimits] = None
435
+ requests: Optional[RequestLimits] = None
436
+
437
+
438
+ class BreakerActions(BaseModel):
439
+ """Actions when breakers trip."""
440
+ onTokensLimit: TripAction = TripAction.PAUSE
441
+ onTimeLimit: TripAction = TripAction.STOP
442
+ onCostLimit: TripAction = TripAction.PAUSE
443
+ onRequestsLimit: TripAction = TripAction.COOLDOWN
444
+ cooldownSeconds: int = 60
445
+
446
+
447
+ class AutonomySpec(BaseModel):
448
+ """Autonomy configuration."""
449
+ mode: AutonomyMode = AutonomyMode.ACT_WITH_LIMITS
450
+ requireApprovalFor: List[str] = Field(default_factory=list)
451
+
452
+
453
+ class GuardrailsSpec(BaseModel):
454
+ """Guardrails configuration."""
455
+ breakers: BreakerSpec = Field(default_factory=BreakerSpec)
456
+ breakerActions: BreakerActions = Field(default_factory=BreakerActions)
457
+ autonomy: AutonomySpec = Field(default_factory=AutonomySpec)
458
+
459
+
460
+ # ============================================================================
461
+ # Workspace
462
+ # ============================================================================
463
+
464
+ class WorkspaceResourcesSpec(BaseModel):
465
+ """Workspace resource limits."""
466
+ cpus: int = 2
467
+ memoryMb: int = 4096
468
+
469
+
470
+ class WorkspaceMountSpec(BaseModel):
471
+ """Workspace mount configuration."""
472
+ src: str
473
+ dst: str
474
+ readonly: bool = False
475
+
476
+
477
+ class WorkspaceContainerConfig(BaseModel):
478
+ """Container workspace configuration."""
479
+ image: str = "smartify-sandbox:latest"
480
+ resources: WorkspaceResourcesSpec = Field(default_factory=WorkspaceResourcesSpec)
481
+ mounts: List[WorkspaceMountSpec] = Field(default_factory=list)
482
+
483
+
484
+ class WorkspaceSpec(BaseModel):
485
+ """Workspace configuration."""
486
+ type: WorkspaceType = WorkspaceType.LOCAL
487
+ config: Optional[WorkspaceContainerConfig] = None
488
+ rootPath: str = "./workspace"
489
+ persistent: bool = True
490
+
491
+
492
+ # ============================================================================
493
+ # Coordination
494
+ # ============================================================================
495
+
496
+ class CoordinationSpec(BaseModel):
497
+ """State management configuration."""
498
+ stateBackend: str = "memory" # "memory", "sqlite", "redis"
499
+ checkpointInterval: int = 60
500
+
501
+
502
+ # ============================================================================
503
+ # Notifications
504
+ # ============================================================================
505
+
506
+ class WebhookConfigSpec(BaseModel):
507
+ """Webhook endpoint configuration."""
508
+ url: str
509
+ events: List[str] = Field(
510
+ default_factory=lambda: [
511
+ "run_completed", "run_failed", "approval_needed", "breaker_tripped"
512
+ ],
513
+ description="Event types to send: run_started, run_completed, run_failed, "
514
+ "run_paused, run_stopped, approval_needed, breaker_tripped, node_completed"
515
+ )
516
+ secret: Optional[str] = Field(
517
+ None,
518
+ description="Secret for HMAC-SHA256 signature (X-Smartify-Signature header)"
519
+ )
520
+ headers: Dict[str, str] = Field(
521
+ default_factory=dict,
522
+ description="Custom headers to include with webhook requests"
523
+ )
524
+ maxRetries: int = Field(3, ge=0, le=10, description="Maximum retry attempts")
525
+ timeout: float = Field(30.0, ge=1, le=120, description="Request timeout in seconds")
526
+ enabled: bool = True
527
+
528
+
529
+ class NotificationsSpec(BaseModel):
530
+ """Notification configuration for grid events."""
531
+ webhooks: List[WebhookConfigSpec] = Field(default_factory=list)
532
+
533
+ # Slack notifications (convenience)
534
+ slack: Optional[Dict[str, Any]] = Field(
535
+ None,
536
+ description="Slack notification config: {webhook_url, channel, events}"
537
+ )
538
+
539
+
540
+ # ============================================================================
541
+ # Observability
542
+ # ============================================================================
543
+
544
+ class ObservabilitySpec(BaseModel):
545
+ """Logging and events configuration."""
546
+ logLevel: str = "info"
547
+ enableMetrics: bool = True
548
+ enableTracing: bool = False
549
+ # Legacy: simple webhook URLs (use notifications.webhooks for full config)
550
+ webhooks: List[str] = Field(default_factory=list)
551
+
552
+
553
+ # ============================================================================
554
+ # UI (optional hints)
555
+ # ============================================================================
556
+
557
+ class UISpec(BaseModel):
558
+ """Dashboard hints."""
559
+ color: Optional[str] = None
560
+ icon: Optional[str] = None
561
+
562
+
563
+ # ============================================================================
564
+ # Root GridSpec
565
+ # ============================================================================
566
+
567
+ class GridSpec(BaseModel):
568
+ """Root Grid YAML specification."""
569
+ apiVersion: str = "smartify.ai/v1"
570
+ kind: str = "GridSpec"
571
+ metadata: MetadataSpec
572
+ topology: TopologySpec
573
+ inputs: Optional[List[InputSpec]] = None
574
+ environment: Optional[EnvironmentSpec] = None
575
+ outputs: Optional[OutputsSpec] = None
576
+ agents: Optional[Dict[str, AgentSpec]] = None
577
+ tools: Optional[ToolsSpec] = None
578
+ guardrails: Optional[GuardrailsSpec] = None
579
+ workspace: Optional[WorkspaceSpec] = None
580
+ coordination: Optional[CoordinationSpec] = None
581
+ notifications: Optional[NotificationsSpec] = None
582
+ observability: Optional[ObservabilitySpec] = None
583
+ ui: Optional[UISpec] = None
584
+
585
+ @field_validator('kind')
586
+ @classmethod
587
+ def validate_kind(cls, v: str) -> str:
588
+ if v != "GridSpec":
589
+ raise ValueError(f"kind must be 'GridSpec', got '{v}'")
590
+ return v
591
+
592
+ @field_validator('apiVersion')
593
+ @classmethod
594
+ def validate_api_version(cls, v: str) -> str:
595
+ if not v.startswith("smartify.ai/"):
596
+ raise ValueError(f"apiVersion must start with 'smartify.ai/', got '{v}'")
597
+ return v
598
+
599
+ # Convenience properties
600
+ @property
601
+ def id(self) -> str:
602
+ """Grid ID from metadata."""
603
+ return self.metadata.id
604
+
605
+ @id.setter
606
+ def id(self, value: str) -> None:
607
+ """Set grid ID in metadata."""
608
+ self.metadata.id = value
609
+
610
+ @property
611
+ def name(self) -> str:
612
+ """Grid name from metadata."""
613
+ return self.metadata.name
614
+
615
+ @property
616
+ def nodes(self) -> List["NodeSpec"]:
617
+ """Nodes from topology."""
618
+ return self.topology.nodes
619
+
620
+ @property
621
+ def breakers(self) -> Optional[BreakerSpec]:
622
+ """Breakers from guardrails."""
623
+ if self.guardrails:
624
+ return self.guardrails.breakers
625
+ return None
@@ -0,0 +1,22 @@
1
+ """Notification system for Smartify events.
2
+
3
+ Supports webhook notifications for:
4
+ - run_completed
5
+ - run_failed
6
+ - approval_needed
7
+ - breaker_tripped
8
+ """
9
+
10
+ from smartify.notifications.webhook import (
11
+ WebhookNotifier,
12
+ WebhookConfig,
13
+ WebhookEvent,
14
+ EventType,
15
+ )
16
+
17
+ __all__ = [
18
+ "WebhookNotifier",
19
+ "WebhookConfig",
20
+ "WebhookEvent",
21
+ "EventType",
22
+ ]