ainative-python 2.0.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.
@@ -0,0 +1,544 @@
1
+ """
2
+ ZeroDB Local Environment Management Commands
3
+
4
+ Commands for managing local Docker-based ZeroDB development environment.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ import click
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Optional, List
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+ from rich.panel import Panel
17
+ from rich import box
18
+
19
+
20
+ console = Console()
21
+
22
+
23
+ # Service ports for health checks
24
+ SERVICE_PORTS = {
25
+ "postgres": 5432,
26
+ "qdrant": 6333,
27
+ "minio": 9000,
28
+ "redpanda": 9092,
29
+ "embeddings": 8001,
30
+ "zerodb-api": 8000,
31
+ "dashboard": 3000,
32
+ }
33
+
34
+ # Service URLs for health checks
35
+ SERVICE_HEALTH_URLS = {
36
+ "postgres": None, # Use pg_isready
37
+ "qdrant": "http://localhost:6333/healthz",
38
+ "minio": "http://localhost:9000/minio/health/live",
39
+ "redpanda": None, # Use rpk command
40
+ "embeddings": "http://localhost:8001/health",
41
+ "zerodb-api": "http://localhost:8000/health",
42
+ "dashboard": "http://localhost:3000",
43
+ }
44
+
45
+
46
+ def find_zerodb_local_path() -> Optional[Path]:
47
+ """Auto-detect zerodb-local directory path."""
48
+ # Check current directory
49
+ cwd = Path.cwd()
50
+ if (cwd / "docker-compose.yml").exists() and (cwd / ".env.local.example").exists():
51
+ return cwd
52
+
53
+ # Check parent directories
54
+ for parent in cwd.parents:
55
+ zerodb_local = parent / "zerodb-local"
56
+ if zerodb_local.exists() and (zerodb_local / "docker-compose.yml").exists():
57
+ return zerodb_local
58
+
59
+ # Check common locations
60
+ common_paths = [
61
+ Path.home() / "core" / "zerodb-local",
62
+ Path("/Users/aideveloper/core/zerodb-local"),
63
+ Path("~/zerodb-local").expanduser(),
64
+ ]
65
+
66
+ for path in common_paths:
67
+ if path.exists() and (path / "docker-compose.yml").exists():
68
+ return path
69
+
70
+ return None
71
+
72
+
73
+ def check_docker_running() -> bool:
74
+ """Check if Docker daemon is running."""
75
+ try:
76
+ result = subprocess.run(
77
+ ["docker", "info"],
78
+ stdout=subprocess.PIPE,
79
+ stderr=subprocess.PIPE,
80
+ timeout=5
81
+ )
82
+ return result.returncode == 0
83
+ except (subprocess.TimeoutExpired, FileNotFoundError):
84
+ return False
85
+
86
+
87
+ def check_port_listening(port: int) -> bool:
88
+ """Check if a port is listening."""
89
+ try:
90
+ import socket
91
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
92
+ sock.settimeout(1)
93
+ result = sock.connect_ex(("localhost", port))
94
+ return result == 0
95
+ except Exception:
96
+ return False
97
+
98
+
99
+ def check_service_health(service: str, url: Optional[str]) -> str:
100
+ """Check service health via HTTP endpoint."""
101
+ if not url:
102
+ # Check port only
103
+ port = SERVICE_PORTS.get(service)
104
+ if port and check_port_listening(port):
105
+ return "healthy"
106
+ return "down"
107
+
108
+ try:
109
+ import urllib.request
110
+ req = urllib.request.Request(url, method="GET")
111
+ with urllib.request.urlopen(req, timeout=3) as response:
112
+ if response.status == 200:
113
+ return "healthy"
114
+ return "unhealthy"
115
+ except Exception:
116
+ return "down"
117
+
118
+
119
+ @click.group(name="local")
120
+ def local_group():
121
+ """Manage local ZeroDB Docker environment."""
122
+ pass
123
+
124
+
125
+ @local_group.command(name="init")
126
+ @click.option("--path", type=click.Path(), help="Path to zerodb-local directory")
127
+ @click.option("--force", is_flag=True, help="Overwrite existing .env.local file")
128
+ def init_environment(path: Optional[str], force: bool):
129
+ """Initialize local Docker environment."""
130
+ try:
131
+ # Find zerodb-local path
132
+ if path:
133
+ zerodb_path = Path(path)
134
+ else:
135
+ zerodb_path = find_zerodb_local_path()
136
+
137
+ if not zerodb_path or not zerodb_path.exists():
138
+ console.print("[red]✗[/red] Could not find zerodb-local directory")
139
+ console.print("\nSearched locations:")
140
+ console.print(" • Current directory")
141
+ console.print(" • Parent directories")
142
+ console.print(" • ~/core/zerodb-local")
143
+ console.print("\nUse --path to specify location manually")
144
+ sys.exit(1)
145
+
146
+ console.print(f"[cyan]Found zerodb-local at:[/cyan] {zerodb_path}")
147
+
148
+ # Check Docker
149
+ if not check_docker_running():
150
+ console.print("[red]✗[/red] Docker is not running")
151
+ console.print("Please start Docker Desktop and try again")
152
+ sys.exit(1)
153
+
154
+ console.print("[green]✓[/green] Docker is running")
155
+
156
+ # Check docker-compose.yml
157
+ compose_file = zerodb_path / "docker-compose.yml"
158
+ if not compose_file.exists():
159
+ console.print(f"[red]✗[/red] docker-compose.yml not found at {compose_file}")
160
+ sys.exit(1)
161
+
162
+ console.print("[green]✓[/green] docker-compose.yml found")
163
+
164
+ # Create .env.local from example
165
+ env_file = zerodb_path / ".env.local"
166
+ example_file = zerodb_path / ".env.local.example"
167
+
168
+ if env_file.exists() and not force:
169
+ console.print(f"[yellow]![/yellow] .env.local already exists")
170
+ console.print("Use --force to overwrite")
171
+ else:
172
+ if not example_file.exists():
173
+ console.print(f"[red]✗[/red] .env.local.example not found")
174
+ sys.exit(1)
175
+
176
+ env_file.write_text(example_file.read_text())
177
+ console.print(f"[green]✓[/green] Created .env.local from template")
178
+
179
+ # Create data directories
180
+ data_dir = zerodb_path / "data"
181
+ data_dir.mkdir(exist_ok=True)
182
+
183
+ for subdir in ["postgres", "qdrant", "minio", "redpanda", "embeddings"]:
184
+ (data_dir / subdir).mkdir(exist_ok=True)
185
+
186
+ console.print("[green]✓[/green] Created data directories")
187
+
188
+ # Success message
189
+ console.print()
190
+ panel = Panel(
191
+ "[green]Environment initialized successfully![/green]\n\n"
192
+ "Next steps:\n"
193
+ " 1. Review and update .env.local if needed\n"
194
+ " 2. Run: ainative local up\n"
195
+ " 3. Check status: ainative local status",
196
+ title="Initialization Complete",
197
+ border_style="green"
198
+ )
199
+ console.print(panel)
200
+
201
+ except Exception as e:
202
+ console.print(f"[red]Error:[/red] {str(e)}")
203
+ sys.exit(1)
204
+
205
+
206
+ @local_group.command(name="up")
207
+ @click.option("--path", type=click.Path(), help="Path to zerodb-local directory")
208
+ @click.option("--detach/--no-detach", "-d/-D", default=True, help="Run in background")
209
+ @click.option("--build", is_flag=True, help="Build images before starting")
210
+ def start_services(path: Optional[str], detach: bool, build: bool):
211
+ """Start local Docker services."""
212
+ try:
213
+ # Find zerodb-local path
214
+ if path:
215
+ zerodb_path = Path(path)
216
+ else:
217
+ zerodb_path = find_zerodb_local_path()
218
+
219
+ if not zerodb_path:
220
+ console.print("[red]✗[/red] Could not find zerodb-local directory")
221
+ console.print("Run: ainative local init")
222
+ sys.exit(1)
223
+
224
+ # Check Docker
225
+ if not check_docker_running():
226
+ console.print("[red]✗[/red] Docker is not running")
227
+ sys.exit(1)
228
+
229
+ # Build command
230
+ cmd = ["docker-compose"]
231
+
232
+ if build:
233
+ console.print("[cyan]Building images...[/cyan]")
234
+ build_result = subprocess.run(
235
+ ["docker-compose", "build"],
236
+ cwd=zerodb_path,
237
+ capture_output=True,
238
+ text=True
239
+ )
240
+ if build_result.returncode != 0:
241
+ console.print(f"[red]✗[/red] Build failed:\n{build_result.stderr}")
242
+ sys.exit(1)
243
+ console.print("[green]✓[/green] Build complete")
244
+
245
+ # Start services
246
+ console.print("[cyan]Starting services...[/cyan]")
247
+
248
+ cmd.extend(["up"])
249
+ if detach:
250
+ cmd.append("-d")
251
+
252
+ result = subprocess.run(
253
+ cmd,
254
+ cwd=zerodb_path,
255
+ capture_output=detach,
256
+ text=True
257
+ )
258
+
259
+ if result.returncode != 0:
260
+ console.print(f"[red]✗[/red] Failed to start services")
261
+ if result.stderr:
262
+ console.print(result.stderr)
263
+ sys.exit(1)
264
+
265
+ if detach:
266
+ console.print("[green]✓[/green] Services started in background")
267
+ console.print()
268
+ console.print("View logs: ainative local logs")
269
+ console.print("Check status: ainative local status")
270
+
271
+ except Exception as e:
272
+ console.print(f"[red]Error:[/red] {str(e)}")
273
+ sys.exit(1)
274
+
275
+
276
+ @local_group.command(name="down")
277
+ @click.option("--path", type=click.Path(), help="Path to zerodb-local directory")
278
+ @click.option("--volumes", is_flag=True, help="Remove volumes (deletes data)")
279
+ @click.confirmation_option(
280
+ "--volumes",
281
+ prompt="This will delete all data. Are you sure?"
282
+ )
283
+ def stop_services(path: Optional[str], volumes: bool):
284
+ """Stop local Docker services."""
285
+ try:
286
+ # Find zerodb-local path
287
+ if path:
288
+ zerodb_path = Path(path)
289
+ else:
290
+ zerodb_path = find_zerodb_local_path()
291
+
292
+ if not zerodb_path:
293
+ console.print("[red]✗[/red] Could not find zerodb-local directory")
294
+ sys.exit(1)
295
+
296
+ # Stop services
297
+ console.print("[cyan]Stopping services...[/cyan]")
298
+
299
+ cmd = ["docker-compose", "down"]
300
+ if volumes:
301
+ cmd.append("-v")
302
+ console.print("[yellow]![/yellow] Removing volumes (data will be deleted)")
303
+
304
+ result = subprocess.run(
305
+ cmd,
306
+ cwd=zerodb_path,
307
+ capture_output=True,
308
+ text=True
309
+ )
310
+
311
+ if result.returncode != 0:
312
+ console.print(f"[red]✗[/red] Failed to stop services")
313
+ console.print(result.stderr)
314
+ sys.exit(1)
315
+
316
+ console.print("[green]✓[/green] Services stopped")
317
+
318
+ except Exception as e:
319
+ console.print(f"[red]Error:[/red] {str(e)}")
320
+ sys.exit(1)
321
+
322
+
323
+ @local_group.command(name="logs")
324
+ @click.argument("service", required=False)
325
+ @click.option("--path", type=click.Path(), help="Path to zerodb-local directory")
326
+ @click.option("--follow", "-f", is_flag=True, help="Follow log output")
327
+ @click.option("--tail", "-n", type=int, default=100, help="Number of lines to show")
328
+ def view_logs(service: Optional[str], path: Optional[str], follow: bool, tail: int):
329
+ """View service logs."""
330
+ try:
331
+ # Find zerodb-local path
332
+ if path:
333
+ zerodb_path = Path(path)
334
+ else:
335
+ zerodb_path = find_zerodb_local_path()
336
+
337
+ if not zerodb_path:
338
+ console.print("[red]✗[/red] Could not find zerodb-local directory")
339
+ sys.exit(1)
340
+
341
+ # Build command
342
+ cmd = ["docker-compose", "logs", f"--tail={tail}"]
343
+
344
+ if follow:
345
+ cmd.append("-f")
346
+
347
+ if service:
348
+ cmd.append(service)
349
+
350
+ # Execute
351
+ subprocess.run(cmd, cwd=zerodb_path)
352
+
353
+ except KeyboardInterrupt:
354
+ console.print("\n[yellow]Stopped following logs[/yellow]")
355
+ except Exception as e:
356
+ console.print(f"[red]Error:[/red] {str(e)}")
357
+ sys.exit(1)
358
+
359
+
360
+ @local_group.command(name="status")
361
+ @click.option("--path", type=click.Path(), help="Path to zerodb-local directory")
362
+ @click.option("--json-output", "--json", is_flag=True, help="Output as JSON")
363
+ def show_status(path: Optional[str], json_output: bool):
364
+ """Show service health status."""
365
+ try:
366
+ # Find zerodb-local path
367
+ if path:
368
+ zerodb_path = Path(path)
369
+ else:
370
+ zerodb_path = find_zerodb_local_path()
371
+
372
+ if not zerodb_path:
373
+ console.print("[red]✗[/red] Could not find zerodb-local directory")
374
+ sys.exit(1)
375
+
376
+ # Check Docker
377
+ if not check_docker_running():
378
+ console.print("[red]✗[/red] Docker is not running")
379
+ sys.exit(1)
380
+
381
+ # Get running containers
382
+ result = subprocess.run(
383
+ ["docker-compose", "ps", "--format", "json"],
384
+ cwd=zerodb_path,
385
+ capture_output=True,
386
+ text=True
387
+ )
388
+
389
+ if result.returncode != 0:
390
+ console.print("[red]✗[/red] Failed to get service status")
391
+ sys.exit(1)
392
+
393
+ # Parse container status
394
+ containers = []
395
+ for line in result.stdout.strip().split("\n"):
396
+ if line:
397
+ try:
398
+ containers.append(json.loads(line))
399
+ except json.JSONDecodeError:
400
+ pass
401
+
402
+ # Check health of each service
403
+ status_data = []
404
+ for service_name, port in SERVICE_PORTS.items():
405
+ # Find container
406
+ container = next((c for c in containers if service_name in c.get("Service", "")), None)
407
+
408
+ if not container:
409
+ status_data.append({
410
+ "service": service_name,
411
+ "status": "down",
412
+ "port": port,
413
+ "health": "down"
414
+ })
415
+ continue
416
+
417
+ # Check health
418
+ container_status = container.get("State", "unknown")
419
+ health_url = SERVICE_HEALTH_URLS.get(service_name)
420
+ health = check_service_health(service_name, health_url)
421
+
422
+ status_data.append({
423
+ "service": service_name,
424
+ "status": container_status,
425
+ "port": port,
426
+ "health": health
427
+ })
428
+
429
+ # Output
430
+ if json_output:
431
+ click.echo(json.dumps(status_data, indent=2))
432
+ return
433
+
434
+ # Create table
435
+ table = Table(title="ZeroDB Local Services", box=box.ROUNDED)
436
+ table.add_column("Service", style="cyan", no_wrap=True)
437
+ table.add_column("Status", style="bold")
438
+ table.add_column("Port", justify="right", style="dim")
439
+ table.add_column("Health", style="bold")
440
+
441
+ for item in status_data:
442
+ # Status color
443
+ status = item["status"]
444
+ if status == "running":
445
+ status_display = "[green]●[/green] Running"
446
+ elif status == "down":
447
+ status_display = "[red]●[/red] Down"
448
+ else:
449
+ status_display = f"[yellow]●[/yellow] {status}"
450
+
451
+ # Health color
452
+ health = item["health"]
453
+ if health == "healthy":
454
+ health_display = "[green]✓ Healthy[/green]"
455
+ elif health == "unhealthy":
456
+ health_display = "[yellow]! Unhealthy[/yellow]"
457
+ else:
458
+ health_display = "[red]✗ Down[/red]"
459
+
460
+ table.add_row(
461
+ item["service"],
462
+ status_display,
463
+ str(item["port"]),
464
+ health_display
465
+ )
466
+
467
+ console.print(table)
468
+
469
+ # Overall status
470
+ all_healthy = all(item["health"] == "healthy" for item in status_data)
471
+ if all_healthy:
472
+ console.print("\n[green]✓ All services are healthy[/green]")
473
+ else:
474
+ console.print("\n[yellow]! Some services are not healthy[/yellow]")
475
+ console.print("Run: ainative local logs [service] to investigate")
476
+
477
+ except Exception as e:
478
+ console.print(f"[red]Error:[/red] {str(e)}")
479
+ sys.exit(1)
480
+
481
+
482
+ @local_group.command(name="reset")
483
+ @click.option("--path", type=click.Path(), help="Path to zerodb-local directory")
484
+ @click.confirmation_option(prompt="This will delete ALL local data. Are you sure?")
485
+ def reset_database(path: Optional[str]):
486
+ """Reset local database (WARNING: deletes all data)."""
487
+ try:
488
+ # Find zerodb-local path
489
+ if path:
490
+ zerodb_path = Path(path)
491
+ else:
492
+ zerodb_path = find_zerodb_local_path()
493
+
494
+ if not zerodb_path:
495
+ console.print("[red]✗[/red] Could not find zerodb-local directory")
496
+ sys.exit(1)
497
+
498
+ console.print("[yellow]Resetting database...[/yellow]")
499
+
500
+ # Stop services
501
+ console.print("[cyan]1/3[/cyan] Stopping services...")
502
+ subprocess.run(
503
+ ["docker-compose", "down"],
504
+ cwd=zerodb_path,
505
+ capture_output=True
506
+ )
507
+
508
+ # Remove volumes
509
+ console.print("[cyan]2/3[/cyan] Removing volumes...")
510
+ subprocess.run(
511
+ ["docker-compose", "down", "-v"],
512
+ cwd=zerodb_path,
513
+ capture_output=True
514
+ )
515
+
516
+ # Remove data directory
517
+ data_dir = zerodb_path / "data"
518
+ if data_dir.exists():
519
+ import shutil
520
+ shutil.rmtree(data_dir)
521
+ data_dir.mkdir()
522
+ console.print("[green]✓[/green] Data directory cleared")
523
+
524
+ # Restart services
525
+ console.print("[cyan]3/3[/cyan] Restarting services...")
526
+ result = subprocess.run(
527
+ ["docker-compose", "up", "-d"],
528
+ cwd=zerodb_path,
529
+ capture_output=True,
530
+ text=True
531
+ )
532
+
533
+ if result.returncode != 0:
534
+ console.print(f"[red]✗[/red] Failed to restart services")
535
+ console.print(result.stderr)
536
+ sys.exit(1)
537
+
538
+ console.print()
539
+ console.print("[green]✓ Database reset complete[/green]")
540
+ console.print("All data has been deleted and services restarted")
541
+
542
+ except Exception as e:
543
+ console.print(f"[red]Error:[/red] {str(e)}")
544
+ sys.exit(1)
@@ -0,0 +1,144 @@
1
+ """
2
+ Agent State CLI Commands
3
+
4
+ Commands for agent state management.
5
+ """
6
+
7
+ import click
8
+ import json
9
+ from typing import Optional
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from ..client import AINativeClient
15
+
16
+
17
+ console = Console()
18
+
19
+
20
+ def get_client() -> AINativeClient:
21
+ """Get authenticated client."""
22
+ import os
23
+ from ..auth import AuthConfig
24
+
25
+ api_key = os.getenv("AINATIVE_API_KEY")
26
+ if not api_key:
27
+ raise click.ClickException("AINATIVE_API_KEY environment variable not set")
28
+
29
+ return AINativeClient(
30
+ auth_config=AuthConfig(api_key=api_key),
31
+ base_url=os.getenv("AINATIVE_BASE_URL"),
32
+ organization_id=os.getenv("AINATIVE_ORG_ID")
33
+ )
34
+
35
+
36
+ @click.group(name="state")
37
+ def state_group():
38
+ """Agent state management."""
39
+ pass
40
+
41
+
42
+ @state_group.command(name="get")
43
+ @click.option("--agent-id", required=True, help="Agent ID")
44
+ @click.option("--state-id", help="Specific state ID (optional)")
45
+ def get_state(agent_id: str, state_id: Optional[str]):
46
+ """Get agent state."""
47
+ try:
48
+ client = get_client()
49
+ result = client.agent_state.get_state(agent_id=agent_id, state_id=state_id)
50
+
51
+ console.print(Panel(
52
+ json.dumps(result, indent=2),
53
+ title=f"Agent State - {agent_id}"
54
+ ))
55
+
56
+ except Exception as e:
57
+ click.echo(f"Error: {str(e)}", err=True)
58
+
59
+
60
+ @state_group.command(name="checkpoint")
61
+ @click.option("--agent-id", required=True, help="Agent ID")
62
+ @click.option("--name", required=True, help="Checkpoint name")
63
+ @click.option("--data", required=True, help="State data (JSON string)")
64
+ @click.option("--description", help="Optional description")
65
+ def create_checkpoint(agent_id: str, name: str, data: str, description: Optional[str]):
66
+ """Create a state checkpoint."""
67
+ try:
68
+ client = get_client()
69
+ state_data = json.loads(data)
70
+
71
+ result = client.agent_state.create_checkpoint(
72
+ agent_id=agent_id,
73
+ checkpoint_name=name,
74
+ state_data=state_data,
75
+ description=description
76
+ )
77
+
78
+ console.print(Panel(
79
+ f"[green]✓[/green] Checkpoint created: {result.get('checkpoint_id')}",
80
+ title="Success"
81
+ ))
82
+ console.print(json.dumps(result, indent=2))
83
+
84
+ except Exception as e:
85
+ click.echo(f"Error: {str(e)}", err=True)
86
+
87
+
88
+ @state_group.command(name="restore")
89
+ @click.argument("checkpoint_id")
90
+ def restore_checkpoint(checkpoint_id: str):
91
+ """Restore from a checkpoint."""
92
+ try:
93
+ client = get_client()
94
+ result = client.agent_state.restore_checkpoint(checkpoint_id)
95
+
96
+ console.print(Panel(
97
+ f"[green]✓[/green] State restored from checkpoint",
98
+ title="Success"
99
+ ))
100
+ console.print(json.dumps(result, indent=2))
101
+
102
+ except Exception as e:
103
+ click.echo(f"Error: {str(e)}", err=True)
104
+
105
+
106
+ @state_group.command(name="list")
107
+ @click.option("--agent-id", required=True, help="Agent ID")
108
+ @click.option("--checkpoints", is_flag=True, help="List checkpoints instead of states")
109
+ @click.option("--format", type=click.Choice(["table", "json"]), default="table",
110
+ help="Output format")
111
+ def list_states(agent_id: str, checkpoints: bool, format: str):
112
+ """List agent states or checkpoints."""
113
+ try:
114
+ client = get_client()
115
+
116
+ if checkpoints:
117
+ result = client.agent_state.list_checkpoints(agent_id=agent_id)
118
+ items = result.get("checkpoints", [])
119
+ title = "Checkpoints"
120
+ else:
121
+ result = client.agent_state.list_states(agent_id=agent_id)
122
+ items = result.get("states", [])
123
+ title = "States"
124
+
125
+ if format == "json":
126
+ click.echo(json.dumps(items, indent=2))
127
+ else:
128
+ table = Table(title=title)
129
+ table.add_column("ID", style="cyan")
130
+ table.add_column("Name", style="bold")
131
+ table.add_column("Created", style="dim")
132
+
133
+ for item in items:
134
+ table.add_row(
135
+ item.get("id", "")[:12],
136
+ item.get("name", item.get("checkpoint_name", "")),
137
+ item.get("created_at", "")
138
+ )
139
+
140
+ console.print(table)
141
+ console.print(f"\nTotal: {len(items)} {title.lower()}")
142
+
143
+ except Exception as e:
144
+ click.echo(f"Error: {str(e)}", err=True)