amd-gaia 0.15.1__py3-none-any.whl → 0.15.2__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,1275 @@
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ GAIA Init Command
6
+
7
+ Main entry point for `gaia init` command that:
8
+ 1. Checks if Lemonade Server is installed and version matches
9
+ 2. Downloads and installs Lemonade from GitHub releases if needed
10
+ 3. Starts Lemonade server
11
+ 4. Downloads required models for the selected profile
12
+ 5. Verifies setup is working
13
+ """
14
+
15
+ import logging
16
+ import os
17
+ import sys
18
+ import time
19
+ from dataclasses import dataclass
20
+ from typing import Callable, Optional
21
+
22
+ import requests
23
+
24
+ # Rich imports for better CLI formatting
25
+ try:
26
+ from rich.console import Console
27
+ from rich.panel import Panel
28
+
29
+ RICH_AVAILABLE = True
30
+ except ImportError:
31
+ RICH_AVAILABLE = False
32
+
33
+ from gaia.agents.base.console import AgentConsole
34
+ from gaia.installer.lemonade_installer import LemonadeInfo, LemonadeInstaller
35
+ from gaia.version import LEMONADE_VERSION
36
+
37
+ log = logging.getLogger(__name__)
38
+
39
+ # Profile definitions mapping to agent profiles
40
+ # Note: These define which agent profile to use for each init profile
41
+ INIT_PROFILES = {
42
+ "minimal": {
43
+ "description": "Fast setup with lightweight model",
44
+ "agent": "minimal",
45
+ "models": ["Qwen3-4B-Instruct-2507-GGUF"], # Override default minimal model
46
+ "approx_size": "~2.5 GB",
47
+ },
48
+ "chat": {
49
+ "description": "Interactive chat with RAG and vision support",
50
+ "agent": "chat",
51
+ "models": None, # Use agent profile defaults
52
+ "approx_size": "~25 GB",
53
+ },
54
+ "code": {
55
+ "description": "Autonomous coding assistant",
56
+ "agent": "code",
57
+ "models": None,
58
+ "approx_size": "~18 GB",
59
+ },
60
+ "rag": {
61
+ "description": "Document Q&A with retrieval",
62
+ "agent": "rag",
63
+ "models": None,
64
+ "approx_size": "~25 GB",
65
+ },
66
+ "all": {
67
+ "description": "All models for all agents",
68
+ "agent": "all",
69
+ "models": None,
70
+ "approx_size": "~26 GB",
71
+ },
72
+ }
73
+
74
+
75
+ @dataclass
76
+ class InitProgress:
77
+ """Progress information for the init command."""
78
+
79
+ step: int
80
+ total_steps: int
81
+ step_name: str
82
+ message: str
83
+
84
+
85
+ class InitCommand:
86
+ """
87
+ Main handler for the `gaia init` command.
88
+
89
+ Orchestrates the full initialization workflow:
90
+ 1. Check/install Lemonade Server
91
+ 2. Start server if needed
92
+ 3. Download models for profile
93
+ 4. Verify setup
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ profile: str = "chat",
99
+ skip_models: bool = False,
100
+ force_reinstall: bool = False,
101
+ force_models: bool = False,
102
+ yes: bool = False,
103
+ verbose: bool = False,
104
+ remote: bool = False,
105
+ progress_callback: Optional[Callable[[InitProgress], None]] = None,
106
+ ):
107
+ """
108
+ Initialize the init command.
109
+
110
+ Args:
111
+ profile: Profile to initialize (minimal, chat, code, rag, all)
112
+ skip_models: Skip model downloads
113
+ force_reinstall: Force reinstall even if compatible version exists
114
+ force_models: Force re-download models even if already available
115
+ yes: Skip confirmation prompts
116
+ verbose: Enable verbose output
117
+ remote: Lemonade is on a remote machine (skip local start, still check version)
118
+ progress_callback: Optional callback for progress updates
119
+ """
120
+ self.profile = profile.lower()
121
+ self.skip_models = skip_models
122
+ self.force_reinstall = force_reinstall
123
+ self.force_models = force_models
124
+ self.yes = yes
125
+ self.verbose = verbose
126
+ self.remote = remote
127
+ self.progress_callback = progress_callback
128
+
129
+ # Validate profile
130
+ if self.profile not in INIT_PROFILES:
131
+ valid = ", ".join(INIT_PROFILES.keys())
132
+ raise ValueError(f"Invalid profile '{profile}'. Valid profiles: {valid}")
133
+
134
+ # Initialize Rich console if available (before installer for console pass-through)
135
+ self.console = Console() if RICH_AVAILABLE else None
136
+
137
+ # Initialize AgentConsole for download progress display
138
+ self.agent_console = AgentConsole()
139
+
140
+ # Use minimal installer for minimal profile
141
+ use_minimal = self.profile == "minimal"
142
+
143
+ self.installer = LemonadeInstaller(
144
+ target_version=LEMONADE_VERSION,
145
+ progress_callback=self._download_progress if verbose else None,
146
+ minimal=use_minimal,
147
+ console=self.console,
148
+ )
149
+
150
+ def _print(self, message: str, end: str = "\n"):
151
+ """Print message to stdout."""
152
+ if RICH_AVAILABLE and self.console:
153
+ if end == "":
154
+ self.console.print(message, end="")
155
+ else:
156
+ self.console.print(message)
157
+ else:
158
+ print(message, end=end, flush=True)
159
+
160
+ def _print_header(self):
161
+ """Print initialization header."""
162
+ if RICH_AVAILABLE and self.console:
163
+ self.console.print()
164
+ self.console.print(
165
+ Panel(
166
+ "[bold cyan]GAIA Initialization[/bold cyan]",
167
+ border_style="cyan",
168
+ padding=(0, 2),
169
+ )
170
+ )
171
+ self.console.print()
172
+ else:
173
+ self._print("")
174
+ self._print("=" * 60)
175
+ self._print(" GAIA Initialization")
176
+ self._print("=" * 60)
177
+ self._print("")
178
+
179
+ def _print_step(self, step: int, total: int, message: str):
180
+ """Print step header."""
181
+ if RICH_AVAILABLE and self.console:
182
+ self.console.print(f"[bold blue]Step {step}/{total}:[/bold blue] {message}")
183
+ else:
184
+ self._print(f"Step {step}/{total}: {message}")
185
+
186
+ def _print_success(self, message: str):
187
+ """Print success message."""
188
+ if RICH_AVAILABLE and self.console:
189
+ self.console.print(f" [green]✓[/green] {message}")
190
+ else:
191
+ self._print(f" ✓ {message}")
192
+
193
+ def _print_warning(self, message: str):
194
+ """Print warning message."""
195
+ if RICH_AVAILABLE and self.console:
196
+ self.console.print(f" [yellow]⚠️ {message}[/yellow]")
197
+ else:
198
+ self._print(f" ⚠️ {message}")
199
+
200
+ def _print_error(self, message: str):
201
+ """Print error message."""
202
+ if RICH_AVAILABLE and self.console:
203
+ self.console.print(f" [red]❌ {message}[/red]")
204
+ else:
205
+ self._print(f" ❌ {message}")
206
+
207
+ def _prompt_yes_no(self, prompt: str, default: bool = True) -> bool:
208
+ """
209
+ Prompt user for yes/no confirmation.
210
+
211
+ Args:
212
+ prompt: Question to ask
213
+ default: Default answer if user presses enter
214
+
215
+ Returns:
216
+ True for yes, False for no
217
+ """
218
+ if self.yes:
219
+ return True
220
+
221
+ if default:
222
+ suffix = "[bold green]Y[/bold green]/n" if RICH_AVAILABLE else "[Y/n]"
223
+ else:
224
+ suffix = "y/[bold green]N[/bold green]" if RICH_AVAILABLE else "[y/N]"
225
+
226
+ try:
227
+ if RICH_AVAILABLE and self.console:
228
+ self.console.print(f" {prompt} [{suffix}]: ", end="")
229
+ response = input().strip().lower()
230
+ else:
231
+ response = input(f" {prompt} {suffix}: ").strip().lower()
232
+
233
+ if not response:
234
+ return default
235
+ return response in ("y", "yes")
236
+ except (EOFError, KeyboardInterrupt):
237
+ self._print("")
238
+ return False
239
+
240
+ def _refresh_path_environment(self):
241
+ """
242
+ Refresh PATH environment variable from Windows registry.
243
+
244
+ This allows the current Python process to find executables
245
+ that were just installed by MSI, without requiring a terminal restart.
246
+ """
247
+ if sys.platform != "win32":
248
+ return
249
+
250
+ try:
251
+ import winreg
252
+
253
+ # Read user PATH from registry
254
+ user_path = ""
255
+ try:
256
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Environment") as key:
257
+ user_path, _ = winreg.QueryValueEx(key, "Path")
258
+ except (FileNotFoundError, OSError):
259
+ pass
260
+
261
+ # Read system PATH from registry
262
+ system_path = ""
263
+ try:
264
+ with winreg.OpenKey(
265
+ winreg.HKEY_LOCAL_MACHINE,
266
+ r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
267
+ ) as key:
268
+ system_path, _ = winreg.QueryValueEx(key, "Path")
269
+ except (FileNotFoundError, OSError):
270
+ pass
271
+
272
+ # Merge registry paths with current PATH (don't replace entirely)
273
+ if user_path or system_path:
274
+ current_path = os.environ.get("PATH", "")
275
+ registry_path = (
276
+ f"{user_path};{system_path}"
277
+ if user_path and system_path
278
+ else (user_path or system_path)
279
+ )
280
+ # Expand environment variables like %SystemRoot%, %USERPROFILE%, etc.
281
+ registry_path = os.path.expandvars(registry_path)
282
+ # Prepend registry paths to preserve current session paths
283
+ os.environ["PATH"] = f"{registry_path};{current_path}"
284
+ log.debug("Merged and expanded registry PATH with current environment")
285
+
286
+ except Exception as e:
287
+ log.debug(f"Failed to refresh PATH: {e}")
288
+
289
+ def _download_progress(self, downloaded: int, total: int):
290
+ """Callback for download progress."""
291
+ if total > 0:
292
+ percent = (downloaded / total) * 100
293
+ bar_width = 20
294
+ filled = int(bar_width * downloaded / total)
295
+ bar = "=" * filled + "-" * (bar_width - filled)
296
+ size_str = f"{downloaded / 1024 / 1024:.1f} MB"
297
+ if total > 0:
298
+ size_str += f"/{total / 1024 / 1024:.1f} MB"
299
+ self._print(f"\r [{bar}] {percent:.0f}% ({size_str})", end="")
300
+
301
+ def run(self) -> int:
302
+ """
303
+ Execute the initialization workflow.
304
+
305
+ Returns:
306
+ Exit code (0 for success, non-zero for failure)
307
+ """
308
+ self._print_header()
309
+
310
+ total_steps = 4 if not self.skip_models else 3
311
+
312
+ try:
313
+ # Step 1: Check/Install Lemonade (skip for remote servers)
314
+ if self.remote:
315
+ self._print_step(
316
+ 1, total_steps, "Skipping local Lemonade check (remote mode)..."
317
+ )
318
+ self._print_success("Using remote Lemonade Server")
319
+ else:
320
+ self._print_step(
321
+ 1, total_steps, "Checking Lemonade Server installation..."
322
+ )
323
+ if not self._ensure_lemonade_installed():
324
+ return 1
325
+
326
+ # Step 2: Check server
327
+ step_num = 2
328
+ self._print("")
329
+ self._print_step(step_num, total_steps, "Checking Lemonade Server...")
330
+ if not self._ensure_server_running():
331
+ return 1
332
+
333
+ # Step 3: Download models (unless skipped)
334
+ if not self.skip_models:
335
+ step_num = 3
336
+ self._print("")
337
+ self._print_step(
338
+ step_num,
339
+ total_steps,
340
+ f"Downloading models for '{self.profile}' profile...",
341
+ )
342
+ if not self._download_models():
343
+ return 1
344
+
345
+ # Step 4: Verify setup
346
+ step_num = total_steps
347
+ self._print("")
348
+ self._print_step(step_num, total_steps, "Verifying setup...")
349
+ if not self._verify_setup():
350
+ return 1
351
+
352
+ # Success!
353
+ self._print_completion()
354
+ return 0
355
+
356
+ except KeyboardInterrupt:
357
+ self._print("")
358
+ self._print("Initialization cancelled by user.")
359
+ return 130
360
+ except Exception as e:
361
+ self._print_error(f"Unexpected error: {e}")
362
+ if self.verbose:
363
+ import traceback
364
+
365
+ traceback.print_exc()
366
+ return 1
367
+
368
+ def _ensure_lemonade_installed(self) -> bool:
369
+ """
370
+ Check Lemonade installation and install if needed.
371
+
372
+ Returns:
373
+ True if Lemonade is ready, False on failure
374
+ """
375
+ # Check platform support
376
+ if not self.installer.is_platform_supported():
377
+ platform_name = self.installer.get_platform_name()
378
+ self._print_error(
379
+ f"Platform '{platform_name}' is not supported for automatic installation."
380
+ )
381
+ self._print(" GAIA init only supports Windows and Linux.")
382
+ self._print(
383
+ " Please install Lemonade Server manually from: https://www.lemonade-server.ai"
384
+ )
385
+ return False
386
+
387
+ info = self.installer.check_installation()
388
+
389
+ if info.installed and info.version:
390
+ self._print_success(f"Lemonade Server found: v{info.version}")
391
+ # Show the path where it was found (only in verbose mode)
392
+ if self.verbose and info.path:
393
+ self.console.print(f" [dim]Path: {info.path}[/dim]")
394
+
395
+ # Check version match
396
+ if not self._check_version_compatibility(info):
397
+ return False
398
+
399
+ if self.force_reinstall:
400
+ self._print(" Force reinstall requested.")
401
+ return self._install_lemonade()
402
+
403
+ self._print_success("Version is compatible")
404
+ return True
405
+
406
+ elif info.installed:
407
+ self._print_warning("Lemonade Server found but version unknown")
408
+ if info.error:
409
+ self._print(f" Error: {info.error}")
410
+
411
+ if not self._prompt_yes_no(
412
+ f"Install/update Lemonade v{LEMONADE_VERSION}?", default=True
413
+ ):
414
+ return False
415
+
416
+ return self._install_lemonade()
417
+
418
+ else:
419
+ self._print(" Lemonade Server not found")
420
+ self._print("")
421
+
422
+ if not self._prompt_yes_no(
423
+ f"Install Lemonade v{LEMONADE_VERSION}?", default=True
424
+ ):
425
+ self._print("")
426
+ self._print(
427
+ " To install manually, visit: https://www.lemonade-server.ai"
428
+ )
429
+ return False
430
+
431
+ return self._install_lemonade()
432
+
433
+ @staticmethod
434
+ def _parse_version(version: str) -> Optional[tuple]:
435
+ """Parse version string into tuple."""
436
+ try:
437
+ ver = version.lstrip("v")
438
+ parts = ver.split(".")
439
+ return tuple(int(p) for p in parts[:3])
440
+ except (ValueError, IndexError):
441
+ return None
442
+
443
+ def _check_version_compatibility(self, info: LemonadeInfo) -> bool:
444
+ """
445
+ Check if installed version is compatible and upgrade if needed.
446
+
447
+ Args:
448
+ info: Lemonade installation info
449
+
450
+ Returns:
451
+ True if compatible or upgrade successful, False otherwise
452
+ """
453
+ current = info.version_tuple
454
+ target = self._parse_version(LEMONADE_VERSION)
455
+
456
+ if not current or not target:
457
+ return True
458
+
459
+ # Check for version mismatch
460
+ if current != target:
461
+ current_ver = info.version
462
+ target_ver = LEMONADE_VERSION
463
+
464
+ self._print("")
465
+ self._print_warning("Version mismatch detected!")
466
+ if RICH_AVAILABLE and self.console:
467
+ self.console.print(
468
+ f" [dim]Installed:[/dim] [red]v{current_ver}[/red]"
469
+ )
470
+ self.console.print(
471
+ f" [dim]Expected:[/dim] [green]v{target_ver}[/green]"
472
+ )
473
+ else:
474
+ self._print(f" Installed: v{current_ver}")
475
+ self._print(f" Expected: v{target_ver}")
476
+ self._print("")
477
+
478
+ if current < target:
479
+ if RICH_AVAILABLE and self.console:
480
+ self.console.print(
481
+ " [dim]Your version is older than expected.[/dim]"
482
+ )
483
+ self.console.print(
484
+ " [dim]Some features may not work correctly.[/dim]"
485
+ )
486
+ else:
487
+ self._print(" Your version is older than expected.")
488
+ self._print(" Some features may not work correctly.")
489
+ else:
490
+ if RICH_AVAILABLE and self.console:
491
+ self.console.print(
492
+ " [dim]Your version is newer than expected.[/dim]"
493
+ )
494
+ self.console.print(
495
+ " [dim]This may cause compatibility issues.[/dim]"
496
+ )
497
+ else:
498
+ self._print(" Your version is newer than expected.")
499
+ self._print(" This may cause compatibility issues.")
500
+ self._print("")
501
+
502
+ # Prompt user to upgrade
503
+ if not self._prompt_yes_no(
504
+ f"Upgrade to v{target_ver}? (will uninstall current version)",
505
+ default=True,
506
+ ):
507
+ self._print_warning("Continuing with current version")
508
+ return True
509
+
510
+ return self._upgrade_lemonade(current_ver)
511
+
512
+ return True
513
+
514
+ def _upgrade_lemonade(self, old_version: str) -> bool:
515
+ """
516
+ Uninstall old version and install the target version.
517
+
518
+ Args:
519
+ old_version: The currently installed version string
520
+
521
+ Returns:
522
+ True on success, False on failure
523
+ """
524
+ self._print("")
525
+ if RICH_AVAILABLE and self.console:
526
+ self.console.print(
527
+ f" [bold]Uninstalling[/bold] Lemonade [red]v{old_version}[/red]..."
528
+ )
529
+ else:
530
+ self._print(f" Uninstalling Lemonade v{old_version}...")
531
+
532
+ # Uninstall old version
533
+ try:
534
+ result = self.installer.uninstall(silent=True)
535
+ if result.success:
536
+ self._print_success("Uninstalled old version")
537
+ else:
538
+ self._print_error(f"Failed to uninstall: {result.error}")
539
+ self._print_warning("Attempting to install new version anyway...")
540
+ except Exception as e:
541
+ self._print_error(f"Uninstall error: {e}")
542
+ self._print_warning("Attempting to install new version anyway...")
543
+
544
+ # Install new version
545
+ return self._install_lemonade()
546
+
547
+ def _install_lemonade(self) -> bool:
548
+ """
549
+ Download and install Lemonade Server.
550
+
551
+ Returns:
552
+ True on success, False on failure
553
+ """
554
+ self._print("")
555
+ if RICH_AVAILABLE and self.console:
556
+ self.console.print(
557
+ f" [bold]Downloading[/bold] Lemonade [cyan]v{LEMONADE_VERSION}[/cyan]..."
558
+ )
559
+ else:
560
+ self._print(f" Downloading Lemonade v{LEMONADE_VERSION}...")
561
+
562
+ try:
563
+ # Download installer
564
+ installer_path = self.installer.download_installer()
565
+ self._print("")
566
+ self._print_success("Download complete")
567
+
568
+ # Install (not silent so desktop icon is created)
569
+ self.console.print(" [bold]Installing...[/bold]")
570
+ self.console.print()
571
+ self.console.print(
572
+ " [yellow]⚠️ The installer window will appear - please complete the installation[/yellow]"
573
+ )
574
+ self.console.print()
575
+ result = self.installer.install(installer_path, silent=False)
576
+
577
+ if result.success:
578
+ self._print_success(f"Installed Lemonade v{result.version}")
579
+
580
+ # Refresh PATH from Windows registry so current session can find lemonade-server
581
+ if self.verbose:
582
+ self.console.print(" [dim]Refreshing PATH environment...[/dim]")
583
+ self._refresh_path_environment()
584
+
585
+ # Verify installation by checking version
586
+ if self.verbose:
587
+ self.console.print(" [dim]Verifying installation...[/dim]")
588
+ verify_info = self.installer.check_installation()
589
+
590
+ if verify_info.installed and verify_info.version:
591
+ self._print_success(
592
+ f"Verified: lemonade-server v{verify_info.version}"
593
+ )
594
+ if self.verbose and verify_info.path:
595
+ self.console.print(f" [dim]Path: {verify_info.path}[/dim]")
596
+
597
+ return True
598
+ else:
599
+ self._print_error(f"Installation failed: {result.error}")
600
+
601
+ if "Administrator" in str(result.error) or "sudo" in str(result.error):
602
+ self._print("")
603
+ if RICH_AVAILABLE and self.console:
604
+ self.console.print(
605
+ " [yellow]Try running as Administrator (Windows) or with sudo (Linux)[/yellow]"
606
+ )
607
+ else:
608
+ self._print(
609
+ " Try running as Administrator (Windows) or with sudo (Linux)"
610
+ )
611
+
612
+ return False
613
+
614
+ except Exception as e:
615
+ self._print_error(f"Failed to install: {e}")
616
+ return False
617
+
618
+ def _find_lemonade_server(self) -> Optional[str]:
619
+ """
620
+ Find the lemonade-server executable.
621
+
622
+ Uses the installer's PATH refresh to pick up recent MSI changes.
623
+ Falls back to common installation paths if not found in PATH.
624
+
625
+ Returns:
626
+ Path to lemonade-server executable, or None if not found
627
+ """
628
+ import shutil
629
+
630
+ # Use installer's PATH refresh (reads from Windows registry)
631
+ self.installer.refresh_path_from_registry()
632
+
633
+ # Try to find in updated PATH
634
+ lemonade_path = shutil.which("lemonade-server")
635
+ if lemonade_path:
636
+ return lemonade_path
637
+
638
+ # Fallback: check common installation paths (Windows)
639
+ if sys.platform == "win32":
640
+ common_paths = [
641
+ # Per-user install (most common for MSI)
642
+ os.path.expandvars(
643
+ r"%LOCALAPPDATA%\Programs\Lemonade Server\lemonade-server.exe"
644
+ ),
645
+ os.path.expandvars(
646
+ r"%LOCALAPPDATA%\Lemonade Server\lemonade-server.exe"
647
+ ),
648
+ # System-wide install
649
+ r"C:\Program Files\Lemonade Server\lemonade-server.exe",
650
+ r"C:\Program Files (x86)\Lemonade Server\lemonade-server.exe",
651
+ # Potential alternative paths
652
+ os.path.expandvars(
653
+ r"%USERPROFILE%\lemonade-server\lemonade-server.exe"
654
+ ),
655
+ ]
656
+
657
+ for path in common_paths:
658
+ if os.path.isfile(path):
659
+ if self.verbose:
660
+ log.debug(f"Found lemonade-server at fallback path: {path}")
661
+ return path
662
+
663
+ # Fallback: check common installation paths (Linux)
664
+ elif sys.platform.startswith("linux"):
665
+ common_paths = [
666
+ "/usr/local/bin/lemonade-server",
667
+ "/usr/bin/lemonade-server",
668
+ os.path.expanduser("~/.local/bin/lemonade-server"),
669
+ ]
670
+
671
+ for path in common_paths:
672
+ if os.path.isfile(path):
673
+ if self.verbose:
674
+ log.debug(f"Found lemonade-server at fallback path: {path}")
675
+ return path
676
+
677
+ return None
678
+
679
+ def _ensure_server_running(self) -> bool:
680
+ """
681
+ Ensure Lemonade server is running with health check verification.
682
+
683
+ In remote mode, only checks if server is reachable - does not prompt
684
+ user to start it (assumes it's managed externally).
685
+
686
+ Returns:
687
+ True if server is running and healthy, False on failure
688
+ """
689
+ try:
690
+ # Import here to avoid circular imports
691
+ from gaia.llm.lemonade_client import LemonadeClient
692
+
693
+ client = LemonadeClient(verbose=self.verbose)
694
+
695
+ # Check if already running using health_check
696
+ try:
697
+ health = client.health_check()
698
+ if health:
699
+ self._print_success("Server is already running")
700
+ # Verify health status
701
+ if isinstance(health, dict):
702
+ status = health.get("status", "unknown")
703
+ if status == "ok":
704
+ self._print_success("Server health: OK")
705
+ else:
706
+ self._print_warning(f"Server status: {status}")
707
+ return True
708
+ except Exception as e:
709
+ # Log the health check error for debugging
710
+ log.debug(f"Health check failed: {e}")
711
+ # Server not running
712
+
713
+ # In remote mode, don't prompt to start - just report error
714
+ if self.remote:
715
+ self._print_error("Remote Lemonade Server is not reachable")
716
+ self.console.print()
717
+ self.console.print(
718
+ " [dim]Ensure the remote Lemonade Server is running and accessible.[/dim]"
719
+ )
720
+ self.console.print(
721
+ " [dim]Check LEMONADE_HOST environment variable if using a custom host.[/dim]"
722
+ )
723
+ return False
724
+
725
+ # Server not running - ask user to start it manually
726
+ self._print_error("Lemonade Server is not running")
727
+
728
+ # In non-interactive mode (-y), fail immediately
729
+ if self.yes:
730
+ self.console.print()
731
+ self.console.print(
732
+ " [dim]Start Lemonade Server and run gaia init again.[/dim]"
733
+ )
734
+ return False
735
+
736
+ self.console.print()
737
+ self.console.print(" [bold]Please start Lemonade Server:[/bold]")
738
+ if sys.platform == "win32":
739
+ self.console.print(
740
+ " [dim]• Double-click the Lemonade icon in your system tray, or[/dim]"
741
+ )
742
+ self.console.print(
743
+ " [dim]• Search for 'Lemonade' in Start Menu and launch it[/dim]"
744
+ )
745
+ else:
746
+ self.console.print(
747
+ " [dim]• Run:[/dim] [cyan]lemonade-server serve &[/cyan]"
748
+ )
749
+ self.console.print()
750
+
751
+ # Wait for user to start the server
752
+ try:
753
+ self.console.print(
754
+ " [bold]Press Enter when server is started...[/bold]", end=""
755
+ )
756
+ input()
757
+ except (EOFError, KeyboardInterrupt):
758
+ self.console.print()
759
+ self._print_error("Initialization cancelled")
760
+ return False
761
+
762
+ self.console.print()
763
+
764
+ # Check if server is now running
765
+ try:
766
+ health = client.health_check()
767
+ if health and isinstance(health, dict) and health.get("status") == "ok":
768
+ self._print_success("Server is now running")
769
+ self._print_success("Server health: OK")
770
+ return True
771
+ else:
772
+ self._print_error("Server still not responding")
773
+ return False
774
+ except Exception:
775
+ self._print_error("Server still not responding")
776
+ return False
777
+
778
+ except ImportError as e:
779
+ self._print_error(f"Lemonade SDK not installed: {e}")
780
+ if RICH_AVAILABLE and self.console:
781
+ self.console.print(
782
+ " [dim]Run:[/dim] [cyan]pip install lemonade-sdk[/cyan]"
783
+ )
784
+ else:
785
+ self._print(" Run: pip install lemonade-sdk")
786
+ return False
787
+ except Exception as e:
788
+ self._print_error(f"Failed to check/start server: {e}")
789
+ return False
790
+
791
+ def _verify_model(self, client, model_id: str) -> tuple:
792
+ """
793
+ Verify a model is available (downloaded) on the server.
794
+
795
+ Note: We only check if the model exists in the server's model list.
796
+ Running inference to verify would require loading each model, which is
797
+ slow and can cause server issues. If a model is corrupted, the error
798
+ will surface when the user tries to use it.
799
+
800
+ Args:
801
+ client: LemonadeClient instance
802
+ model_id: Model ID to verify
803
+
804
+ Returns:
805
+ Tuple of (success: bool, error_type: str or None)
806
+ """
807
+ try:
808
+ # Check if model is in the available models list
809
+ if client.check_model_available(model_id):
810
+ return (True, None)
811
+ return (False, "not_found")
812
+ except Exception as e:
813
+ log.debug(f"Model verification failed for {model_id}: {e}")
814
+ return (False, "server_error")
815
+
816
+ def _download_models(self) -> bool:
817
+ """
818
+ Download models for the selected profile.
819
+
820
+ Simplified approach: Just try to download all required models.
821
+ Lemonade handles the "already downloaded" case efficiently by
822
+ returning a complete event immediately.
823
+
824
+ Returns:
825
+ True if all models downloaded, False on failure
826
+ """
827
+ try:
828
+ from gaia.llm.lemonade_client import LemonadeClient
829
+
830
+ client = LemonadeClient(verbose=self.verbose)
831
+
832
+ # Get profile config
833
+ profile_config = INIT_PROFILES[self.profile]
834
+ agent = profile_config["agent"]
835
+
836
+ # Get models to download
837
+ if profile_config["models"]:
838
+ # Use profile-specific models (for minimal profile)
839
+ model_ids = profile_config["models"]
840
+ else:
841
+ # Use agent profile defaults
842
+ model_ids = client.get_required_models(agent)
843
+
844
+ # Always include the default CPU model (used by gaia llm)
845
+ from gaia.llm.lemonade_client import DEFAULT_MODEL_NAME
846
+
847
+ if DEFAULT_MODEL_NAME not in model_ids:
848
+ model_ids = list(model_ids) + [DEFAULT_MODEL_NAME]
849
+
850
+ if not model_ids:
851
+ self._print_success("No models required for this profile")
852
+ return True
853
+
854
+ # Show which models will be ensured
855
+ if RICH_AVAILABLE and self.console:
856
+ self.console.print(
857
+ f" [bold]Ensuring {len(model_ids)} model(s) are downloaded:[/bold]"
858
+ )
859
+ for model_id in model_ids:
860
+ self.console.print(f" [cyan]•[/cyan] {model_id}")
861
+ else:
862
+ self._print(f" Ensuring {len(model_ids)} model(s) are downloaded:")
863
+ for model_id in model_ids:
864
+ self._print(f" • {model_id}")
865
+ self._print("")
866
+
867
+ if not self._prompt_yes_no("Continue?", default=True):
868
+ self._print(" Skipping model downloads")
869
+ return True
870
+
871
+ # Force re-download: delete models first
872
+ if self.force_models:
873
+ for model_id in model_ids:
874
+ if client.check_model_available(model_id):
875
+ if RICH_AVAILABLE and self.console:
876
+ self.console.print(
877
+ f" [dim]Deleting (force re-download)[/dim] [cyan]{model_id}[/cyan]..."
878
+ )
879
+ else:
880
+ self._print(
881
+ f" Deleting (force re-download) {model_id}..."
882
+ )
883
+ try:
884
+ client.delete_model(model_id)
885
+ self._print_success(f"Deleted {model_id}")
886
+ except Exception as e:
887
+ self._print_error(f"Failed to delete {model_id}: {e}")
888
+
889
+ # Download each model
890
+ success = True
891
+ for model_id in model_ids:
892
+ self._print("")
893
+
894
+ # Use AgentConsole for nicely formatted download progress
895
+ self.agent_console.print_download_start(model_id)
896
+
897
+ try:
898
+ event_count = 0
899
+ last_bytes = 0
900
+ last_time = time.time()
901
+
902
+ for event in client.pull_model_stream(model_name=model_id):
903
+ event_count += 1
904
+ event_type = event.get("event")
905
+
906
+ if event_type == "progress":
907
+ # Skip first 2 spurious events from Lemonade
908
+ if event_count <= 2:
909
+ continue
910
+
911
+ # Calculate download speed
912
+ current_bytes = event.get("bytes_downloaded", 0)
913
+ current_time = time.time()
914
+ time_delta = current_time - last_time
915
+
916
+ speed_mbps = 0.0
917
+ if time_delta > 0.1 and current_bytes > last_bytes:
918
+ bytes_delta = current_bytes - last_bytes
919
+ speed_mbps = (bytes_delta / time_delta) / (1024 * 1024)
920
+ last_bytes = current_bytes
921
+ last_time = current_time
922
+
923
+ self.agent_console.print_download_progress(
924
+ percent=event.get("percent", 0),
925
+ bytes_downloaded=current_bytes,
926
+ bytes_total=event.get("bytes_total", 0),
927
+ speed_mbps=speed_mbps,
928
+ )
929
+
930
+ elif event_type == "complete":
931
+ self.agent_console.print_download_complete(model_id)
932
+
933
+ elif event_type == "error":
934
+ self.agent_console.print_download_error(
935
+ event.get("error", "Unknown error"), model_id
936
+ )
937
+ success = False
938
+ break
939
+
940
+ except requests.exceptions.ConnectionError as e:
941
+ self.agent_console.print_download_error(f"Connection error: {e}")
942
+ self._print(" Check your network connection and retry")
943
+ success = False
944
+ except Exception as e:
945
+ self.agent_console.print_download_error(str(e), model_id)
946
+ success = False
947
+
948
+ return success
949
+
950
+ except Exception as e:
951
+ self._print_error(f"Error downloading models: {e}")
952
+ return False
953
+
954
+ def _test_model_inference(self, client, model_id: str) -> tuple:
955
+ """
956
+ Test a model with a small inference request.
957
+
958
+ Args:
959
+ client: LemonadeClient instance
960
+ model_id: Model ID to test
961
+
962
+ Returns:
963
+ Tuple of (success: bool, error_message: str or None)
964
+ """
965
+ try:
966
+ # Load the model first
967
+ client.load_model(model_id, auto_download=False, prompt=False)
968
+
969
+ # Check if this is an embedding model
970
+ is_embedding_model = "embed" in model_id.lower()
971
+
972
+ if is_embedding_model:
973
+ # Test embedding model with a simple text
974
+ response = client.embeddings(
975
+ input_texts=["test"],
976
+ model=model_id,
977
+ )
978
+ # Check if we got valid embeddings
979
+ if response and response.get("data"):
980
+ embedding = response["data"][0].get("embedding", [])
981
+ if embedding and len(embedding) > 0:
982
+ return (True, None)
983
+ return (False, "Empty embedding")
984
+ return (False, "Invalid response format")
985
+ else:
986
+ # Test LLM with a minimal chat request
987
+ response = client.chat_completions(
988
+ model=model_id,
989
+ messages=[{"role": "user", "content": "Say 'ok'"}],
990
+ max_tokens=10,
991
+ temperature=0,
992
+ )
993
+ # Check if we got a valid response
994
+ if response and response.get("choices"):
995
+ content = (
996
+ response["choices"][0].get("message", {}).get("content", "")
997
+ )
998
+ if content:
999
+ return (True, None)
1000
+ return (False, "Empty response")
1001
+ return (False, "Invalid response format")
1002
+
1003
+ except Exception as e:
1004
+ error_msg = str(e)
1005
+ # Truncate long error messages
1006
+ if len(error_msg) > 100:
1007
+ error_msg = error_msg[:100] + "..."
1008
+ return (False, error_msg)
1009
+
1010
+ def _verify_setup(self) -> bool:
1011
+ """
1012
+ Verify the setup is working by testing each model with a small request.
1013
+
1014
+ Returns:
1015
+ True if verification passes, False on failure
1016
+ """
1017
+ try:
1018
+ from gaia.llm.lemonade_client import LemonadeClient
1019
+
1020
+ client = LemonadeClient(verbose=self.verbose)
1021
+
1022
+ # Check server health
1023
+ try:
1024
+ health = client.health_check()
1025
+ if health:
1026
+ self._print_success("Server health: OK")
1027
+ else:
1028
+ self._print_error("Server not responding")
1029
+ return False
1030
+ except Exception:
1031
+ self._print_error("Server not responding")
1032
+ return False
1033
+
1034
+ # Get models to verify
1035
+ profile_config = INIT_PROFILES[self.profile]
1036
+ if profile_config["models"]:
1037
+ model_ids = profile_config["models"]
1038
+ else:
1039
+ model_ids = client.get_required_models(profile_config["agent"])
1040
+
1041
+ # Always include the default CPU model (used by gaia llm)
1042
+ from gaia.llm.lemonade_client import DEFAULT_MODEL_NAME
1043
+
1044
+ if DEFAULT_MODEL_NAME not in model_ids:
1045
+ model_ids = list(model_ids) + [DEFAULT_MODEL_NAME]
1046
+
1047
+ if not model_ids or self.skip_models:
1048
+ return True
1049
+
1050
+ # Prompt to run model verification (can be slow)
1051
+ self.console.print()
1052
+ self.console.print(
1053
+ " [dim]Model verification loads each model and runs a small inference test.[/dim]"
1054
+ )
1055
+ self.console.print(
1056
+ " [dim]This may take a few minutes but ensures models work correctly.[/dim]"
1057
+ )
1058
+ self.console.print()
1059
+
1060
+ if not self._prompt_yes_no("Run model verification?", default=True):
1061
+ self._print_success("Skipping model verification")
1062
+ return True
1063
+
1064
+ # Test each model with a small inference request
1065
+ self.console.print()
1066
+ self.console.print(" [bold]Testing models with inference:[/bold]")
1067
+ self.console.print(" [yellow]⚠️ Press Ctrl+C to skip.[/yellow]")
1068
+
1069
+ models_passed = 0
1070
+ models_failed = []
1071
+ interrupted = False
1072
+
1073
+ try:
1074
+ for model_id in model_ids:
1075
+ # Check if model is available first
1076
+ if not client.check_model_available(model_id):
1077
+ self.console.print(
1078
+ f" [yellow]⏭️[/yellow] [cyan]{model_id}[/cyan] [dim]- not downloaded[/dim]"
1079
+ )
1080
+ continue
1081
+
1082
+ # Show testing status
1083
+ self.console.print(
1084
+ f" [dim]🔄[/dim] [cyan]{model_id}[/cyan] [dim]- testing...[/dim]",
1085
+ end="",
1086
+ )
1087
+
1088
+ # Test the model
1089
+ success, error = self._test_model_inference(client, model_id)
1090
+
1091
+ # Clear the line and show result
1092
+ print("\r" + " " * 80 + "\r", end="")
1093
+ if success:
1094
+ self.console.print(
1095
+ f" [green]✓[/green] [cyan]{model_id}[/cyan] [dim]- OK[/dim]"
1096
+ )
1097
+ models_passed += 1
1098
+ else:
1099
+ self.console.print(
1100
+ f" [red]❌[/red] [cyan]{model_id}[/cyan] [dim]- {error}[/dim]"
1101
+ )
1102
+ models_failed.append((model_id, error))
1103
+
1104
+ except KeyboardInterrupt:
1105
+ print("\r" + " " * 80 + "\r", end="")
1106
+ self.console.print()
1107
+ self._print_warning("Verification interrupted")
1108
+ interrupted = True
1109
+
1110
+ # Summary
1111
+ total = len(model_ids)
1112
+ self.console.print()
1113
+ if interrupted:
1114
+ self._print_success(
1115
+ f"Verified {models_passed} model(s) before interruption"
1116
+ )
1117
+ elif models_failed:
1118
+ self._print_warning(f"Models verified: {models_passed}/{total} passed")
1119
+ self.console.print()
1120
+ self.console.print(
1121
+ " [bold]Failed models may be corrupted. To fix:[/bold]"
1122
+ )
1123
+ self.console.print(
1124
+ " [dim]Option 1 - Delete all models and re-download:[/dim]"
1125
+ )
1126
+ self.console.print(" [cyan]gaia uninstall --models --yes[/cyan]")
1127
+ self.console.print(
1128
+ f" [cyan]gaia init --profile {self.profile} --yes[/cyan]"
1129
+ )
1130
+ self.console.print()
1131
+ self.console.print(
1132
+ " [dim]Option 2 - Manually delete failed models:[/dim]"
1133
+ )
1134
+
1135
+ # Show path for each failed model
1136
+ hf_cache = os.path.expanduser("~/.cache/huggingface/hub")
1137
+ from pathlib import Path
1138
+
1139
+ for model_id, error in models_failed:
1140
+ # Find actual model directory (may have org prefix like ggml-org/model-name)
1141
+ # Search for directories containing the model name
1142
+ model_name_part = model_id.split("/")[-1] # Get last part if has /
1143
+ matching_dirs = list(
1144
+ Path(hf_cache).glob(f"models--*{model_name_part}*")
1145
+ )
1146
+
1147
+ if matching_dirs:
1148
+ model_path = str(matching_dirs[0])
1149
+ self.console.print(
1150
+ f" [cyan]{model_id}[/cyan]: [dim]{model_path}[/dim]"
1151
+ )
1152
+ if sys.platform == "win32":
1153
+ self.console.print(
1154
+ f' [yellow]rmdir /s /q[/yellow] [cyan]"{model_path}"[/cyan]'
1155
+ )
1156
+ else:
1157
+ self.console.print(
1158
+ f' [yellow]rm -rf[/yellow] [cyan]"{model_path}"[/cyan]'
1159
+ )
1160
+ else:
1161
+ # Fallback if directory not found
1162
+ self.console.print(
1163
+ f" [cyan]{model_id}[/cyan]: [dim]Not found in cache[/dim]"
1164
+ )
1165
+
1166
+ self.console.print()
1167
+ self.console.print(
1168
+ f" [dim]Then re-download:[/dim] [cyan]gaia init --profile {self.profile} --yes[/cyan]"
1169
+ )
1170
+ else:
1171
+ self._print_success(f"All {models_passed} model(s) verified")
1172
+
1173
+ return True # Don't fail init due to model issues
1174
+
1175
+ except Exception as e:
1176
+ self._print_error(f"Verification failed: {e}")
1177
+ return False
1178
+
1179
+ def _print_completion(self):
1180
+ """Print completion message with next steps."""
1181
+ if RICH_AVAILABLE and self.console:
1182
+ self.console.print()
1183
+ self.console.print(
1184
+ Panel(
1185
+ "[bold green]GAIA initialization complete![/bold green]",
1186
+ border_style="green",
1187
+ padding=(0, 2),
1188
+ )
1189
+ )
1190
+ self.console.print()
1191
+ self.console.print(" [bold]Quick start commands:[/bold]")
1192
+ self.console.print(
1193
+ " [cyan]gaia chat[/cyan] Start interactive chat"
1194
+ )
1195
+ self.console.print(
1196
+ " [cyan]gaia llm 'Hello'[/cyan] Quick LLM query"
1197
+ )
1198
+ self.console.print(
1199
+ " [cyan]gaia talk[/cyan] Voice interaction"
1200
+ )
1201
+ self.console.print()
1202
+
1203
+ profile_config = INIT_PROFILES[self.profile]
1204
+ if profile_config["agent"] == "minimal":
1205
+ self.console.print(
1206
+ " [dim]Note: Minimal profile installed. For full features, run:[/dim]"
1207
+ )
1208
+ self.console.print(" [cyan]gaia init --profile chat[/cyan]")
1209
+ self.console.print()
1210
+ else:
1211
+ self._print("")
1212
+ self._print("=" * 60)
1213
+ self._print(" GAIA initialization complete!")
1214
+ self._print("=" * 60)
1215
+ self._print("")
1216
+ self._print(" Quick start commands:")
1217
+ self._print(" gaia chat # Start interactive chat")
1218
+ self._print(" gaia llm 'Hello' # Quick LLM query")
1219
+ self._print(" gaia talk # Voice interaction")
1220
+ self._print("")
1221
+
1222
+ profile_config = INIT_PROFILES[self.profile]
1223
+ if profile_config["agent"] == "minimal":
1224
+ self._print(
1225
+ " Note: Minimal profile installed. For full features, run:"
1226
+ )
1227
+ self._print(" gaia init --profile chat")
1228
+ self._print("")
1229
+
1230
+
1231
+ def run_init(
1232
+ profile: str = "chat",
1233
+ skip_models: bool = False,
1234
+ force_reinstall: bool = False,
1235
+ force_models: bool = False,
1236
+ yes: bool = False,
1237
+ verbose: bool = False,
1238
+ remote: bool = False,
1239
+ ) -> int:
1240
+ """
1241
+ Entry point for `gaia init` command.
1242
+
1243
+ Args:
1244
+ profile: Profile to initialize (minimal, chat, code, rag, all)
1245
+ skip_models: Skip model downloads
1246
+ force_reinstall: Force reinstall even if compatible version exists
1247
+ force_models: Force re-download models (deletes then re-downloads)
1248
+ yes: Skip confirmation prompts
1249
+ verbose: Enable verbose output
1250
+ remote: Lemonade is on a remote machine (skip local start, still check version)
1251
+
1252
+ Returns:
1253
+ Exit code (0 for success, non-zero for failure)
1254
+ """
1255
+ try:
1256
+ cmd = InitCommand(
1257
+ profile=profile,
1258
+ skip_models=skip_models,
1259
+ force_reinstall=force_reinstall,
1260
+ force_models=force_models,
1261
+ yes=yes,
1262
+ verbose=verbose,
1263
+ remote=remote,
1264
+ )
1265
+ return cmd.run()
1266
+ except ValueError as e:
1267
+ print(f"❌ Error: {e}", file=sys.stderr)
1268
+ return 1
1269
+ except Exception as e:
1270
+ print(f"❌ Unexpected error: {e}", file=sys.stderr)
1271
+ if verbose:
1272
+ import traceback
1273
+
1274
+ traceback.print_exc()
1275
+ return 1