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 @@
1
+ """CLI module for Smartify."""
smartify/cli/main.py ADDED
@@ -0,0 +1,430 @@
1
+ """Smartify CLI - Main entry point."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ import yaml
9
+
10
+ from smartify import __version__
11
+
12
+ app = typer.Typer(
13
+ name="smartify",
14
+ help="Local runtime for Smartify Grid specifications",
15
+ no_args_is_help=True,
16
+ )
17
+ console = Console()
18
+
19
+
20
+ def version_callback(value: bool) -> None:
21
+ """Print version and exit."""
22
+ if value:
23
+ console.print(f"[bold blue]smartify[/bold blue] version {__version__}")
24
+ raise typer.Exit()
25
+
26
+
27
+ @app.callback()
28
+ def main(
29
+ version: bool = typer.Option(
30
+ False,
31
+ "--version",
32
+ "-v",
33
+ help="Show version and exit",
34
+ callback=version_callback,
35
+ is_eager=True,
36
+ ),
37
+ ) -> None:
38
+ """Smartify - Local runtime for Grid specifications."""
39
+ pass
40
+
41
+
42
+ @app.command()
43
+ def validate(
44
+ grid_file: Path = typer.Argument(
45
+ ...,
46
+ help="Path to Grid YAML file",
47
+ exists=True,
48
+ readable=True,
49
+ ),
50
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
51
+ ) -> None:
52
+ """Validate a Grid YAML specification."""
53
+ from smartify.validator.validate import validate_grid_file
54
+
55
+ console.print(f"[bold]Validating[/bold] {grid_file}")
56
+
57
+ try:
58
+ result = validate_grid_file(grid_file)
59
+
60
+ if result.is_valid:
61
+ console.print("[bold green]✓[/bold green] Grid specification is valid")
62
+
63
+ if verbose and result.grid:
64
+ console.print(f"\n[bold]Grid ID:[/bold] {result.grid.metadata.id}")
65
+ console.print(f"[bold]Name:[/bold] {result.grid.metadata.name}")
66
+ console.print(f"[bold]Nodes:[/bold] {len(result.grid.topology.nodes)}")
67
+ else:
68
+ console.print("[bold red]✗[/bold red] Validation failed")
69
+
70
+ for error in result.errors:
71
+ console.print(f" [red]•[/red] {error}")
72
+
73
+ raise typer.Exit(1)
74
+
75
+ if result.warnings:
76
+ console.print(f"\n[yellow]Warnings ({len(result.warnings)}):[/yellow]")
77
+ for warning in result.warnings:
78
+ console.print(f" [yellow]•[/yellow] {warning}")
79
+
80
+ except Exception as e:
81
+ console.print(f"[bold red]Error:[/bold red] {e}")
82
+ raise typer.Exit(1)
83
+
84
+
85
+ @app.command()
86
+ def run(
87
+ grid_file: Path = typer.Argument(
88
+ ...,
89
+ help="Path to Grid YAML file",
90
+ exists=True,
91
+ readable=True,
92
+ ),
93
+ input: list[str] = typer.Option(
94
+ [],
95
+ "--input",
96
+ "-i",
97
+ help="Input parameters (key=value)",
98
+ ),
99
+ dry_run: bool = typer.Option(False, "--dry-run", help="Validate only, don't execute"),
100
+ watch: bool = typer.Option(False, "--watch", "-w", help="Watch execution progress"),
101
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
102
+ ) -> None:
103
+ """Run a Grid specification."""
104
+ import asyncio
105
+ from smartify.engine.orchestrator import Orchestrator, ExecutionError
106
+
107
+ console.print(f"[bold]Running[/bold] {grid_file}")
108
+
109
+ # Parse inputs
110
+ inputs = {}
111
+ for inp in input:
112
+ if "=" in inp:
113
+ key, value = inp.split("=", 1)
114
+ inputs[key] = value
115
+ else:
116
+ console.print(f"[yellow]Warning:[/yellow] Invalid input format: {inp}")
117
+
118
+ if inputs:
119
+ console.print(f"[bold]Inputs:[/bold] {inputs}")
120
+
121
+ if dry_run:
122
+ console.print("[yellow]Dry run mode - validating only[/yellow]")
123
+ from smartify.validator.validate import validate_grid_file
124
+ result = validate_grid_file(grid_file)
125
+ if result.is_valid:
126
+ console.print("[bold green]✓[/bold green] Grid is valid and ready to run")
127
+
128
+ # Show execution plan
129
+ if verbose and result.grid:
130
+ from smartify.engine.scheduler import DAGScheduler
131
+ scheduler = DAGScheduler(result.grid)
132
+ scheduler.build_graph()
133
+ console.print("\n[bold]Execution Plan:[/bold]")
134
+ console.print(scheduler.visualize())
135
+ else:
136
+ console.print("[bold red]✗[/bold red] Grid validation failed")
137
+ for error in result.errors:
138
+ console.print(f" [red]•[/red] {error}")
139
+ raise typer.Exit(1)
140
+ return
141
+
142
+ async def _run():
143
+ import os
144
+ from smartify.agents.adapters import AnthropicAdapter
145
+
146
+ orchestrator = Orchestrator()
147
+
148
+ # Auto-register LLM adapter from environment
149
+ if os.environ.get("ANTHROPIC_API_KEY"):
150
+ adapter = AnthropicAdapter()
151
+ orchestrator.register_llm_adapter("anthropic", adapter)
152
+ orchestrator.register_llm_adapter("default", adapter)
153
+ console.print("[green]✓[/green] Anthropic LLM adapter configured")
154
+ else:
155
+ console.print("[yellow]Warning:[/yellow] ANTHROPIC_API_KEY not set - LLM calls will fail")
156
+
157
+ try:
158
+ # Load grid
159
+ grid_run = await orchestrator.load_grid(grid_file, inputs=inputs)
160
+ console.print(f"[green]✓[/green] Loaded grid '{grid_run.grid.id}'")
161
+
162
+ if verbose:
163
+ console.print(f" Nodes: {len(grid_run.grid.topology.nodes)}")
164
+
165
+ # Energize
166
+ await orchestrator.energize(grid_run)
167
+ console.print(f"[green]✓[/green] Grid energized")
168
+
169
+ # Execute
170
+ console.print("[bold]Executing...[/bold]")
171
+ result = await orchestrator.execute(grid_run)
172
+
173
+ # Show results
174
+ if result.get('state') == 'completed':
175
+ console.print("\n[bold green]✓ Grid completed successfully[/bold green]")
176
+ else:
177
+ console.print(f"\n[bold red]✗ Grid failed: {result.get('error')}[/bold red]")
178
+
179
+ if verbose:
180
+ console.print("\n[bold]Results:[/bold]")
181
+ import json
182
+ console.print(json.dumps(result, indent=2, default=str))
183
+
184
+ return result
185
+
186
+ except ExecutionError as e:
187
+ console.print(f"[bold red]Execution error:[/bold red] {e}")
188
+ raise typer.Exit(1)
189
+
190
+ try:
191
+ asyncio.run(_run())
192
+ except KeyboardInterrupt:
193
+ console.print("\n[yellow]Execution interrupted[/yellow]")
194
+ raise typer.Exit(130)
195
+
196
+
197
+ @app.command()
198
+ def status(
199
+ run_id: str = typer.Argument(..., help="Run ID to check"),
200
+ db: str = typer.Option("smartify_runs.db", "--db", help="Database file path"),
201
+ ) -> None:
202
+ """Check status of a grid run."""
203
+ from smartify.state import SQLiteStore, RunStatus
204
+
205
+ store = SQLiteStore(db)
206
+ run = store.get_run(run_id)
207
+
208
+ if not run:
209
+ console.print(f"[red]Run not found:[/red] {run_id}")
210
+ raise typer.Exit(1)
211
+
212
+ # Status color
213
+ status_colors = {
214
+ RunStatus.PENDING: "yellow",
215
+ RunStatus.RUNNING: "blue",
216
+ RunStatus.PAUSED: "yellow",
217
+ RunStatus.COMPLETED: "green",
218
+ RunStatus.FAILED: "red",
219
+ RunStatus.STOPPED: "dim",
220
+ }
221
+ color = status_colors.get(run.status, "white")
222
+
223
+ console.print(f"\n[bold]Run:[/bold] {run.run_id}")
224
+ console.print(f"[bold]Grid:[/bold] {run.grid_name} ({run.grid_id})")
225
+ console.print(f"[bold]Status:[/bold] [{color}]{run.status.value}[/{color}]")
226
+ console.print(f"[bold]Created:[/bold] {run.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
227
+
228
+ if run.started_at:
229
+ console.print(f"[bold]Started:[/bold] {run.started_at.strftime('%Y-%m-%d %H:%M:%S')}")
230
+ if run.completed_at:
231
+ console.print(f"[bold]Completed:[/bold] {run.completed_at.strftime('%Y-%m-%d %H:%M:%S')}")
232
+
233
+ console.print(f"[bold]Tokens:[/bold] {run.total_tokens:,}")
234
+ console.print(f"[bold]Cost:[/bold] ${run.total_cost:.4f}")
235
+
236
+ if run.error:
237
+ console.print(f"[bold red]Error:[/bold red] {run.error}")
238
+
239
+ # Show node outputs
240
+ outputs = store.get_node_outputs(run_id)
241
+ if outputs:
242
+ console.print(f"\n[bold]Node Outputs:[/bold] {len(outputs)}")
243
+ for out in outputs:
244
+ status_icon = "[green]✓[/green]" if out.success else "[red]✗[/red]"
245
+ console.print(f" {status_icon} {out.node_id} ({out.tokens_used} tokens)")
246
+
247
+
248
+ @app.command()
249
+ def stop(
250
+ run_id: str = typer.Argument(..., help="Run ID to stop"),
251
+ db: str = typer.Option("smartify_runs.db", "--db", help="Database file path"),
252
+ force: bool = typer.Option(False, "--force", "-f", help="Force stop immediately"),
253
+ ) -> None:
254
+ """Stop a running grid."""
255
+ from smartify.state import SQLiteStore, RunStatus
256
+
257
+ store = SQLiteStore(db)
258
+ run = store.get_run(run_id)
259
+
260
+ if not run:
261
+ console.print(f"[red]Run not found:[/red] {run_id}")
262
+ raise typer.Exit(1)
263
+
264
+ if run.status not in (RunStatus.RUNNING, RunStatus.PAUSED):
265
+ console.print(f"[yellow]Run is not active:[/yellow] {run.status.value}")
266
+ raise typer.Exit(1)
267
+
268
+ from datetime import datetime
269
+ run.status = RunStatus.STOPPED
270
+ run.completed_at = datetime.now()
271
+ store.save_run(run)
272
+
273
+ console.print(f"[green]✓[/green] Run stopped: {run_id}")
274
+
275
+
276
+ @app.command(name="list")
277
+ def list_runs(
278
+ limit: int = typer.Option(10, "--limit", "-n", help="Number of runs to show"),
279
+ status_filter: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status"),
280
+ grid_id: Optional[str] = typer.Option(None, "--grid", "-g", help="Filter by grid ID"),
281
+ db: str = typer.Option("smartify_runs.db", "--db", help="Database file path"),
282
+ ) -> None:
283
+ """List grid runs."""
284
+ from smartify.state import SQLiteStore, RunStatus
285
+
286
+ store = SQLiteStore(db)
287
+
288
+ # Parse status filter
289
+ status = None
290
+ if status_filter:
291
+ try:
292
+ status = RunStatus(status_filter.lower())
293
+ except ValueError:
294
+ console.print(f"[red]Invalid status:[/red] {status_filter}")
295
+ console.print(f"Valid: {', '.join(s.value for s in RunStatus)}")
296
+ raise typer.Exit(1)
297
+
298
+ runs = store.list_runs(status=status, grid_id=grid_id, limit=limit)
299
+
300
+ if not runs:
301
+ console.print("[dim]No runs found[/dim]")
302
+ return
303
+
304
+ # Create table
305
+ table = Table(title="Grid Runs")
306
+ table.add_column("Run ID", style="cyan")
307
+ table.add_column("Grid", style="blue")
308
+ table.add_column("Status")
309
+ table.add_column("Tokens", justify="right")
310
+ table.add_column("Cost", justify="right")
311
+ table.add_column("Created")
312
+
313
+ for run in runs:
314
+ status_colors = {
315
+ RunStatus.PENDING: "yellow",
316
+ RunStatus.RUNNING: "blue",
317
+ RunStatus.PAUSED: "yellow",
318
+ RunStatus.COMPLETED: "green",
319
+ RunStatus.FAILED: "red",
320
+ RunStatus.STOPPED: "dim",
321
+ }
322
+ color = status_colors.get(run.status, "white")
323
+
324
+ table.add_row(
325
+ run.run_id[:12] + "..." if len(run.run_id) > 15 else run.run_id,
326
+ run.grid_name[:20],
327
+ f"[{color}]{run.status.value}[/{color}]",
328
+ f"{run.total_tokens:,}",
329
+ f"${run.total_cost:.4f}",
330
+ run.created_at.strftime("%m-%d %H:%M"),
331
+ )
332
+
333
+ console.print(table)
334
+
335
+
336
+ @app.command()
337
+ def serve(
338
+ host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
339
+ port: int = typer.Option(8080, "--port", "-p", help="Port to bind to"),
340
+ reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload (dev mode)"),
341
+ ) -> None:
342
+ """Start the Smartify API server."""
343
+ from smartify.api.server import run_server
344
+
345
+ console.print(f"[bold]Starting Smartify API server[/bold]")
346
+ console.print(f" [dim]Host:[/dim] {host}")
347
+ console.print(f" [dim]Port:[/dim] {port}")
348
+ console.print(f" [dim]Docs:[/dim] http://{host}:{port}/docs")
349
+
350
+ run_server(host=host, port=port, reload=reload)
351
+
352
+
353
+ @app.command()
354
+ def info(
355
+ grid_file: Path = typer.Argument(
356
+ ...,
357
+ help="Path to Grid YAML file",
358
+ exists=True,
359
+ readable=True,
360
+ ),
361
+ ) -> None:
362
+ """Show information about a Grid specification."""
363
+ from smartify.validator.validate import validate_grid_file
364
+
365
+ try:
366
+ result = validate_grid_file(grid_file)
367
+
368
+ if not result.is_valid or not result.grid:
369
+ console.print("[bold red]Error:[/bold red] Invalid grid file")
370
+ for error in result.errors:
371
+ console.print(f" [red]•[/red] {error}")
372
+ raise typer.Exit(1)
373
+
374
+ grid = result.grid
375
+
376
+ # Grid metadata
377
+ console.print("\n[bold blue]Grid Information[/bold blue]")
378
+ console.print(f" [bold]ID:[/bold] {grid.metadata.id}")
379
+ console.print(f" [bold]Name:[/bold] {grid.metadata.name}")
380
+ if grid.metadata.description:
381
+ console.print(f" [bold]Description:[/bold] {grid.metadata.description}")
382
+ console.print(f" [bold]Version:[/bold] {grid.metadata.version}")
383
+
384
+ # Topology
385
+ console.print("\n[bold blue]Topology[/bold blue]")
386
+
387
+ # Count node types
388
+ node_counts = {}
389
+ for node in grid.topology.nodes:
390
+ kind = node.kind.value if hasattr(node.kind, 'value') else str(node.kind)
391
+ node_counts[kind] = node_counts.get(kind, 0) + 1
392
+
393
+ for kind, count in node_counts.items():
394
+ console.print(f" [bold]{kind}:[/bold] {count}")
395
+
396
+ # Node table
397
+ table = Table(title="Nodes")
398
+ table.add_column("ID", style="cyan")
399
+ table.add_column("Kind", style="green")
400
+ table.add_column("Name")
401
+ table.add_column("Parent", style="dim")
402
+
403
+ for node in grid.topology.nodes:
404
+ kind = node.kind.value if hasattr(node.kind, 'value') else str(node.kind)
405
+ table.add_row(node.id, kind, node.name, node.parent or "-")
406
+
407
+ console.print(table)
408
+
409
+ # Inputs
410
+ if grid.inputs:
411
+ console.print("\n[bold blue]Inputs[/bold blue]")
412
+ for inp in grid.inputs:
413
+ req = "[red]*[/red]" if inp.required else ""
414
+ default = f" (default: {inp.default})" if inp.default is not None else ""
415
+ console.print(f" {req}[bold]{inp.name}[/bold]: {inp.type}{default}")
416
+
417
+ # Agents
418
+ if grid.agents:
419
+ console.print("\n[bold blue]Agents[/bold blue]")
420
+ for name, agent in grid.agents.items():
421
+ model = agent.modelPolicy.preferred if agent.modelPolicy else "default"
422
+ console.print(f" [bold]{name}:[/bold] {model}")
423
+
424
+ except Exception as e:
425
+ console.print(f"[bold red]Error:[/bold red] {e}")
426
+ raise typer.Exit(1)
427
+
428
+
429
+ if __name__ == "__main__":
430
+ app()
@@ -0,0 +1,64 @@
1
+ """Smartify execution engine.
2
+
3
+ Provides grid orchestration and scheduling:
4
+ - DAGScheduler: Determines execution order based on dependencies
5
+ - Orchestrator: Core execution engine managing grid lifecycle
6
+ - SparkManager: Dynamic agent spawning for parallel workloads
7
+ """
8
+
9
+ from smartify.engine.scheduler import DAGScheduler, NodeState, NodeExecution
10
+ from smartify.engine.orchestrator import (
11
+ Orchestrator,
12
+ ExecutionContext,
13
+ GridRun,
14
+ NodeResult,
15
+ ExecutionError,
16
+ GridLifecycleError,
17
+ LLMAdapter,
18
+ ToolAdapter,
19
+ )
20
+ from smartify.engine.spark import (
21
+ SparkManager,
22
+ SparkRequest,
23
+ SparkNode,
24
+ analyze_workload_for_sparks,
25
+ )
26
+ from smartify.engine.approval import (
27
+ ApprovalManager,
28
+ ApprovalRequest,
29
+ ApprovalStatus,
30
+ SlackNotificationChannel,
31
+ WebhookNotificationChannel,
32
+ get_approval_manager,
33
+ configure_approval_manager,
34
+ )
35
+
36
+ __all__ = [
37
+ # Scheduler
38
+ "DAGScheduler",
39
+ "NodeState",
40
+ "NodeExecution",
41
+ # Orchestrator
42
+ "Orchestrator",
43
+ "ExecutionContext",
44
+ "GridRun",
45
+ "NodeResult",
46
+ "ExecutionError",
47
+ "GridLifecycleError",
48
+ # Protocols
49
+ "LLMAdapter",
50
+ "ToolAdapter",
51
+ # Spark (dynamic spawning)
52
+ "SparkManager",
53
+ "SparkRequest",
54
+ "SparkNode",
55
+ "analyze_workload_for_sparks",
56
+ # Approval (human-in-the-loop)
57
+ "ApprovalManager",
58
+ "ApprovalRequest",
59
+ "ApprovalStatus",
60
+ "SlackNotificationChannel",
61
+ "WebhookNotificationChannel",
62
+ "get_approval_manager",
63
+ "configure_approval_manager",
64
+ ]