amd-gaia 0.15.1__py3-none-any.whl → 0.15.3__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 (38) hide show
  1. {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/METADATA +2 -2
  2. {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/RECORD +38 -32
  3. {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/WHEEL +1 -1
  4. gaia/agents/base/agent.py +317 -113
  5. gaia/agents/base/api_agent.py +0 -1
  6. gaia/agents/base/console.py +334 -9
  7. gaia/agents/base/tools.py +7 -2
  8. gaia/agents/blender/__init__.py +7 -0
  9. gaia/agents/blender/agent.py +7 -10
  10. gaia/agents/blender/core/view.py +2 -2
  11. gaia/agents/chat/agent.py +22 -48
  12. gaia/agents/chat/app.py +7 -0
  13. gaia/agents/chat/tools/rag_tools.py +23 -8
  14. gaia/agents/chat/tools/shell_tools.py +1 -0
  15. gaia/agents/code/prompts/code_patterns.py +2 -4
  16. gaia/agents/docker/agent.py +1 -0
  17. gaia/agents/emr/agent.py +3 -5
  18. gaia/agents/emr/cli.py +1 -1
  19. gaia/agents/emr/dashboard/server.py +2 -4
  20. gaia/agents/tools/__init__.py +11 -0
  21. gaia/agents/tools/file_tools.py +715 -0
  22. gaia/apps/llm/app.py +14 -3
  23. gaia/chat/app.py +2 -4
  24. gaia/cli.py +751 -333
  25. gaia/installer/__init__.py +23 -0
  26. gaia/installer/init_command.py +1605 -0
  27. gaia/installer/lemonade_installer.py +678 -0
  28. gaia/llm/__init__.py +2 -1
  29. gaia/llm/lemonade_client.py +427 -99
  30. gaia/llm/lemonade_manager.py +55 -11
  31. gaia/llm/providers/lemonade.py +21 -14
  32. gaia/rag/sdk.py +1 -1
  33. gaia/security.py +24 -4
  34. gaia/talk/app.py +2 -4
  35. gaia/version.py +2 -2
  36. {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/entry_points.txt +0 -0
  37. {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/licenses/LICENSE.md +0 -0
  38. {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1605 @@
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 subprocess
18
+ import sys
19
+ from dataclasses import dataclass
20
+ from typing import Callable, Optional
21
+
22
+ # Rich imports for better CLI formatting
23
+ try:
24
+ from rich.console import Console
25
+ from rich.panel import Panel
26
+
27
+ RICH_AVAILABLE = True
28
+ except ImportError:
29
+ RICH_AVAILABLE = False
30
+
31
+ from gaia.agents.base.console import AgentConsole
32
+ from gaia.installer.lemonade_installer import LemonadeInfo, LemonadeInstaller
33
+ from gaia.version import LEMONADE_VERSION
34
+
35
+ log = logging.getLogger(__name__)
36
+
37
+ # Profile definitions mapping to agent profiles
38
+ # Note: These define which agent profile to use for each init profile
39
+ INIT_PROFILES = {
40
+ "minimal": {
41
+ "description": "Fast setup with lightweight model",
42
+ "agent": "minimal",
43
+ "models": ["Qwen3-4B-Instruct-2507-GGUF"], # Override default minimal model
44
+ "approx_size": "~2.5 GB",
45
+ "min_lemonade_version": "9.0.4",
46
+ "min_context_size": 4096,
47
+ },
48
+ "sd": {
49
+ "description": "Image generation with multi-modal AI (LLM + SD + VLM)",
50
+ "agent": "sd",
51
+ "models": [
52
+ "SDXL-Turbo", # Image generation (6.5GB)
53
+ "Qwen3-8B-GGUF", # Agentic reasoning + prompt enhancement (5.0GB)
54
+ "Qwen3-VL-4B-Instruct-GGUF", # Vision analysis + stories (3.2GB)
55
+ ],
56
+ "approx_size": "~15 GB",
57
+ "min_lemonade_version": "9.2.0", # SDXL-Turbo requires v9.2.0+
58
+ "min_context_size": 16384, # SD agent needs 16K for multi-step planning
59
+ },
60
+ "chat": {
61
+ "description": "Interactive chat with RAG and vision support",
62
+ "agent": "chat",
63
+ "models": None, # Use agent profile defaults
64
+ "approx_size": "~25 GB",
65
+ "min_lemonade_version": "9.0.4",
66
+ "min_context_size": 32768,
67
+ },
68
+ "code": {
69
+ "description": "Autonomous coding assistant",
70
+ "agent": "code",
71
+ "models": None,
72
+ "approx_size": "~18 GB",
73
+ "min_lemonade_version": "9.0.4",
74
+ "min_context_size": 32768,
75
+ },
76
+ "rag": {
77
+ "description": "Document Q&A with retrieval",
78
+ "agent": "rag",
79
+ "models": None,
80
+ "approx_size": "~25 GB",
81
+ "min_lemonade_version": "9.0.4",
82
+ "min_context_size": 32768,
83
+ },
84
+ "all": {
85
+ "description": "All models for all agents",
86
+ "agent": "all",
87
+ "models": None,
88
+ "approx_size": "~26 GB",
89
+ "min_lemonade_version": "9.2.0", # Includes SD, so needs v9.2.0+
90
+ "min_context_size": 32768, # Max requirement across all agents
91
+ },
92
+ }
93
+
94
+
95
+ @dataclass
96
+ class InitProgress:
97
+ """Progress information for the init command."""
98
+
99
+ step: int
100
+ total_steps: int
101
+ step_name: str
102
+ message: str
103
+
104
+
105
+ class InitCommand:
106
+ """
107
+ Main handler for the `gaia init` command.
108
+
109
+ Orchestrates the full initialization workflow:
110
+ 1. Check/install Lemonade Server
111
+ 2. Start server if needed
112
+ 3. Download models for profile
113
+ 4. Verify setup
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ profile: str = "chat",
119
+ skip_models: bool = False,
120
+ skip_lemonade: bool = False,
121
+ force_reinstall: bool = False,
122
+ force_models: bool = False,
123
+ yes: bool = False,
124
+ verbose: bool = False,
125
+ remote: bool = False,
126
+ progress_callback: Optional[Callable[[InitProgress], None]] = None,
127
+ ):
128
+ """
129
+ Initialize the init command.
130
+
131
+ Args:
132
+ profile: Profile to initialize (minimal, chat, code, rag, all)
133
+ skip_models: Skip model downloads
134
+ skip_lemonade: Skip Lemonade installation check (for CI)
135
+ force_reinstall: Force reinstall even if compatible version exists
136
+ force_models: Force re-download models even if already available
137
+ yes: Skip confirmation prompts
138
+ verbose: Enable verbose output
139
+ remote: Lemonade is on a remote machine (skip local start, still check version)
140
+ progress_callback: Optional callback for progress updates
141
+ """
142
+ self.profile = profile.lower()
143
+ self.skip_models = skip_models
144
+ self.skip_lemonade = skip_lemonade
145
+ self.force_reinstall = force_reinstall
146
+ self.force_models = force_models
147
+ self.yes = yes
148
+ self.verbose = verbose
149
+ self.remote = remote
150
+ self.progress_callback = progress_callback
151
+
152
+ # Validate profile
153
+ if self.profile not in INIT_PROFILES:
154
+ valid = ", ".join(INIT_PROFILES.keys())
155
+ raise ValueError(f"Invalid profile '{profile}'. Valid profiles: {valid}")
156
+
157
+ # Initialize Rich console if available (before installer for console pass-through)
158
+ self.console = Console() if RICH_AVAILABLE else None
159
+
160
+ # Initialize AgentConsole for formatted output
161
+ self.agent_console = AgentConsole()
162
+
163
+ # Use minimal installer for minimal profile OR when using --yes (silent mode)
164
+ # Minimal installer is faster and more reliable for CI
165
+ use_minimal = self.profile == "minimal" or yes
166
+
167
+ self.installer = LemonadeInstaller(
168
+ target_version=LEMONADE_VERSION,
169
+ progress_callback=self._download_progress if verbose else None,
170
+ minimal=use_minimal,
171
+ console=self.console,
172
+ )
173
+
174
+ # Context verification state (set during model loading)
175
+ self._ctx_verified = None
176
+ self._ctx_warning = None
177
+
178
+ def _print(self, message: str, end: str = "\n"):
179
+ """Print message to stdout."""
180
+ if RICH_AVAILABLE and self.console:
181
+ if end == "":
182
+ self.console.print(message, end="")
183
+ else:
184
+ self.console.print(message)
185
+ else:
186
+ print(message, end=end, flush=True)
187
+
188
+ def _print_header(self):
189
+ """Print initialization header."""
190
+ if RICH_AVAILABLE and self.console:
191
+ self.console.print()
192
+ self.console.print(
193
+ Panel(
194
+ "[bold cyan]GAIA Initialization[/bold cyan]",
195
+ border_style="cyan",
196
+ padding=(0, 2),
197
+ )
198
+ )
199
+ self.console.print()
200
+ else:
201
+ self._print("")
202
+ self._print("=" * 60)
203
+ self._print(" GAIA Initialization")
204
+ self._print("=" * 60)
205
+ self._print("")
206
+
207
+ def _print_step(self, step: int, total: int, message: str):
208
+ """Print step header."""
209
+ if RICH_AVAILABLE and self.console:
210
+ self.console.print(f"[bold blue]Step {step}/{total}:[/bold blue] {message}")
211
+ else:
212
+ self._print(f"Step {step}/{total}: {message}")
213
+
214
+ def _print_success(self, message: str):
215
+ """Print success message."""
216
+ if RICH_AVAILABLE and self.console:
217
+ self.console.print(f" [green]✓[/green] {message}")
218
+ else:
219
+ self._print(f" ✓ {message}")
220
+
221
+ def _print_warning(self, message: str):
222
+ """Print warning message."""
223
+ if RICH_AVAILABLE and self.console:
224
+ self.console.print(f" [yellow]⚠️ {message}[/yellow]")
225
+ else:
226
+ self._print(f" ⚠️ {message}")
227
+
228
+ def _print_error(self, message: str):
229
+ """Print error message."""
230
+ if RICH_AVAILABLE and self.console:
231
+ self.console.print(f" [red]❌ {message}[/red]")
232
+ else:
233
+ self._print(f" ❌ {message}")
234
+
235
+ def _prompt_yes_no(self, prompt: str, default: bool = True) -> bool:
236
+ """
237
+ Prompt user for yes/no confirmation.
238
+
239
+ Args:
240
+ prompt: Question to ask
241
+ default: Default answer if user presses enter
242
+
243
+ Returns:
244
+ True for yes, False for no
245
+ """
246
+ if self.yes:
247
+ return True
248
+
249
+ if default:
250
+ suffix = "[bold green]Y[/bold green]/n" if RICH_AVAILABLE else "[Y/n]"
251
+ else:
252
+ suffix = "y/[bold green]N[/bold green]" if RICH_AVAILABLE else "[y/N]"
253
+
254
+ try:
255
+ if RICH_AVAILABLE and self.console:
256
+ self.console.print(f" {prompt} [{suffix}]: ", end="")
257
+ response = input().strip().lower()
258
+ else:
259
+ response = input(f" {prompt} {suffix}: ").strip().lower()
260
+
261
+ if not response:
262
+ return default
263
+ return response in ("y", "yes")
264
+ except (EOFError, KeyboardInterrupt):
265
+ self._print("")
266
+ return False
267
+
268
+ def _refresh_path_environment(self):
269
+ """
270
+ Refresh PATH environment variable from Windows registry.
271
+
272
+ This allows the current Python process to find executables
273
+ that were just installed by MSI, without requiring a terminal restart.
274
+ """
275
+ if sys.platform != "win32":
276
+ return
277
+
278
+ try:
279
+ import winreg
280
+
281
+ # Read user PATH from registry
282
+ user_path = ""
283
+ try:
284
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Environment") as key:
285
+ user_path, _ = winreg.QueryValueEx(key, "Path")
286
+ except (FileNotFoundError, OSError):
287
+ pass
288
+
289
+ # Read system PATH from registry
290
+ system_path = ""
291
+ try:
292
+ with winreg.OpenKey(
293
+ winreg.HKEY_LOCAL_MACHINE,
294
+ r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
295
+ ) as key:
296
+ system_path, _ = winreg.QueryValueEx(key, "Path")
297
+ except (FileNotFoundError, OSError):
298
+ pass
299
+
300
+ # Merge registry paths with current PATH (don't replace entirely)
301
+ if user_path or system_path:
302
+ current_path = os.environ.get("PATH", "")
303
+ registry_path = (
304
+ f"{user_path};{system_path}"
305
+ if user_path and system_path
306
+ else (user_path or system_path)
307
+ )
308
+ # Expand environment variables like %SystemRoot%, %USERPROFILE%, etc.
309
+ registry_path = os.path.expandvars(registry_path)
310
+ # Prepend registry paths to preserve current session paths
311
+ os.environ["PATH"] = f"{registry_path};{current_path}"
312
+ log.debug("Merged and expanded registry PATH with current environment")
313
+
314
+ except Exception as e:
315
+ log.debug(f"Failed to refresh PATH: {e}")
316
+
317
+ def _download_progress(self, downloaded: int, total: int):
318
+ """Callback for download progress."""
319
+ if total > 0:
320
+ percent = (downloaded / total) * 100
321
+ bar_width = 20
322
+ filled = int(bar_width * downloaded / total)
323
+ bar = "=" * filled + "-" * (bar_width - filled)
324
+ size_str = f"{downloaded / 1024 / 1024:.1f} MB"
325
+ if total > 0:
326
+ size_str += f"/{total / 1024 / 1024:.1f} MB"
327
+ self._print(f"\r [{bar}] {percent:.0f}% ({size_str})", end="")
328
+
329
+ def run(self) -> int:
330
+ """
331
+ Execute the initialization workflow.
332
+
333
+ Returns:
334
+ Exit code (0 for success, non-zero for failure)
335
+ """
336
+ self._print_header()
337
+
338
+ total_steps = 4 if not self.skip_models else 3
339
+
340
+ try:
341
+ # Step 1: Check/Install Lemonade (skip for remote servers or CI)
342
+ if self.remote:
343
+ self._print_step(
344
+ 1, total_steps, "Skipping local Lemonade check (remote mode)..."
345
+ )
346
+ self._print_success("Using remote Lemonade Server")
347
+ elif self.skip_lemonade:
348
+ self._print_step(
349
+ 1, total_steps, "Skipping Lemonade installation check..."
350
+ )
351
+ # Still show version info for transparency
352
+ info = self.installer.check_installation()
353
+ if info.installed and info.version:
354
+ self._print_success(
355
+ f"Using pre-installed Lemonade Server v{info.version}"
356
+ )
357
+ else:
358
+ self._print_success("Using pre-installed Lemonade Server")
359
+ else:
360
+ self._print_step(
361
+ 1, total_steps, "Checking Lemonade Server installation..."
362
+ )
363
+ if not self._ensure_lemonade_installed():
364
+ return 1
365
+
366
+ # Step 2: Check server
367
+ step_num = 2
368
+ self._print("")
369
+ self._print_step(step_num, total_steps, "Checking Lemonade Server...")
370
+ if not self._ensure_server_running():
371
+ return 1
372
+
373
+ # Step 3: Download models (unless skipped)
374
+ if not self.skip_models:
375
+ step_num = 3
376
+ self._print("")
377
+ self._print_step(
378
+ step_num,
379
+ total_steps,
380
+ f"Downloading models for '{self.profile}' profile...",
381
+ )
382
+ if not self._download_models():
383
+ return 1
384
+
385
+ # Step 4: Verify setup
386
+ step_num = total_steps
387
+ self._print("")
388
+ self._print_step(step_num, total_steps, "Verifying setup...")
389
+ if not self._verify_setup():
390
+ return 1
391
+
392
+ # Success!
393
+ self._print_completion()
394
+ return 0
395
+
396
+ except KeyboardInterrupt:
397
+ self._print("")
398
+ self._print("Initialization cancelled by user.")
399
+ return 130
400
+ except Exception as e:
401
+ self._print_error(f"Unexpected error: {e}")
402
+ if self.verbose:
403
+ import traceback
404
+
405
+ traceback.print_exc()
406
+ return 1
407
+
408
+ def _ensure_lemonade_installed(self) -> bool:
409
+ """
410
+ Check Lemonade installation and install if needed.
411
+
412
+ Returns:
413
+ True if Lemonade is ready, False on failure
414
+ """
415
+ # Check platform support
416
+ if not self.installer.is_platform_supported():
417
+ platform_name = self.installer.get_platform_name()
418
+ self._print_error(
419
+ f"Platform '{platform_name}' is not supported for automatic installation."
420
+ )
421
+ self._print(" GAIA init only supports Windows and Linux.")
422
+ self._print(
423
+ " Please install Lemonade Server manually from: https://www.lemonade-server.ai"
424
+ )
425
+ return False
426
+
427
+ info = self.installer.check_installation()
428
+
429
+ if info.installed and info.version:
430
+ self._print_success(f"Lemonade Server found: v{info.version}")
431
+ # Show the path where it was found (only in verbose mode)
432
+ if self.verbose and info.path:
433
+ self.console.print(f" [dim]Path: {info.path}[/dim]")
434
+
435
+ # Check version match
436
+ if not self._check_version_compatibility(info):
437
+ return False
438
+
439
+ if self.force_reinstall:
440
+ self._print(" Force reinstall requested.")
441
+ return self._install_lemonade()
442
+
443
+ self._print_success("Version is compatible")
444
+ return True
445
+
446
+ elif info.installed:
447
+ self._print_warning("Lemonade Server found but version unknown")
448
+ if info.error:
449
+ self._print(f" Error: {info.error}")
450
+
451
+ if not self._prompt_yes_no(
452
+ f"Install/update Lemonade v{LEMONADE_VERSION}?", default=True
453
+ ):
454
+ return False
455
+
456
+ return self._install_lemonade()
457
+
458
+ else:
459
+ self._print(" Lemonade Server not found")
460
+ self._print("")
461
+
462
+ if not self._prompt_yes_no(
463
+ f"Install Lemonade v{LEMONADE_VERSION}?", default=True
464
+ ):
465
+ self._print("")
466
+ self._print(
467
+ " To install manually, visit: https://www.lemonade-server.ai"
468
+ )
469
+ return False
470
+
471
+ return self._install_lemonade()
472
+
473
+ @staticmethod
474
+ def _parse_version(version: str) -> Optional[tuple]:
475
+ """Parse version string into tuple."""
476
+ try:
477
+ ver = version.lstrip("v")
478
+ parts = ver.split(".")
479
+ return tuple(int(p) for p in parts[:3])
480
+ except (ValueError, IndexError):
481
+ return None
482
+
483
+ def _check_version_compatibility(self, info: LemonadeInfo) -> bool:
484
+ """
485
+ Check if installed version is compatible and upgrade if needed.
486
+
487
+ Args:
488
+ info: Lemonade installation info
489
+
490
+ Returns:
491
+ True if compatible or upgrade successful, False otherwise
492
+ """
493
+ current = info.version_tuple
494
+ target = self._parse_version(LEMONADE_VERSION)
495
+
496
+ if not current or not target:
497
+ return True
498
+
499
+ # Check for version mismatch
500
+ if current != target:
501
+ current_ver = info.version
502
+ target_ver = LEMONADE_VERSION
503
+
504
+ self._print("")
505
+ self._print_warning("Version mismatch detected!")
506
+ if RICH_AVAILABLE and self.console:
507
+ self.console.print(
508
+ f" [dim]Installed:[/dim] [red]v{current_ver}[/red]"
509
+ )
510
+ self.console.print(
511
+ f" [dim]Expected:[/dim] [green]v{target_ver}[/green]"
512
+ )
513
+ else:
514
+ self._print(f" Installed: v{current_ver}")
515
+ self._print(f" Expected: v{target_ver}")
516
+ self._print("")
517
+
518
+ if current < target:
519
+ if RICH_AVAILABLE and self.console:
520
+ self.console.print(
521
+ " [dim]Your version is older than expected.[/dim]"
522
+ )
523
+ self.console.print(
524
+ " [dim]Some features may not work correctly.[/dim]"
525
+ )
526
+ else:
527
+ self._print(" Your version is older than expected.")
528
+ self._print(" Some features may not work correctly.")
529
+ else:
530
+ if RICH_AVAILABLE and self.console:
531
+ self.console.print(
532
+ " [dim]Your version is newer than expected.[/dim]"
533
+ )
534
+ self.console.print(
535
+ " [dim]This may cause compatibility issues.[/dim]"
536
+ )
537
+ else:
538
+ self._print(" Your version is newer than expected.")
539
+ self._print(" This may cause compatibility issues.")
540
+ self._print("")
541
+
542
+ # Check if upgrade is required based on profile's minimum version
543
+ profile_config = INIT_PROFILES[self.profile]
544
+ min_version_required = profile_config.get("min_lemonade_version", "9.0.0")
545
+ from packaging import version as pkg_version
546
+
547
+ needs_upgrade = pkg_version.parse(current_ver) < pkg_version.parse(
548
+ min_version_required
549
+ )
550
+
551
+ # In CI mode (--yes), auto-upgrade if needed for this profile
552
+ if self.yes and not self.force_reinstall:
553
+ if needs_upgrade:
554
+ self._print("")
555
+ if RICH_AVAILABLE and self.console:
556
+ self.console.print(
557
+ f" [yellow]⚠️ Profile '{self.profile}' requires Lemonade v{min_version_required}+[/yellow]"
558
+ )
559
+ self.console.print(
560
+ f" [bold cyan]Upgrading:[/bold cyan] v{current_ver} → v{target_ver}"
561
+ )
562
+ else:
563
+ self._print_warning(
564
+ f"Profile '{self.profile}' requires Lemonade v{min_version_required}+"
565
+ )
566
+ self._print(
567
+ f" Upgrading from v{current_ver} to v{target_ver}..."
568
+ )
569
+ return self._upgrade_lemonade(current_ver)
570
+ else:
571
+ self._print_success(
572
+ f"Version v{current_ver} is sufficient for profile '{self.profile}'"
573
+ )
574
+ return True
575
+
576
+ # Prompt user to upgrade
577
+ if not self._prompt_yes_no(
578
+ f"Upgrade to v{target_ver}? (will uninstall current version)",
579
+ default=False, # Default to no for safety
580
+ ):
581
+ self._print_warning("Continuing with current version")
582
+ return True
583
+
584
+ return self._upgrade_lemonade(current_ver)
585
+
586
+ return True
587
+
588
+ def _upgrade_lemonade(self, old_version: str) -> bool:
589
+ """
590
+ Uninstall old version and install the target version.
591
+
592
+ Args:
593
+ old_version: The currently installed version string
594
+
595
+ Returns:
596
+ True on success, False on failure
597
+ """
598
+ self._print("")
599
+ if RICH_AVAILABLE and self.console:
600
+ self.console.print(
601
+ f" [bold]Uninstalling[/bold] Lemonade [red]v{old_version}[/red]..."
602
+ )
603
+ else:
604
+ self._print(f" Uninstalling Lemonade v{old_version}...")
605
+
606
+ # Uninstall old version
607
+ try:
608
+ result = self.installer.uninstall(silent=True)
609
+ if result.success:
610
+ self._print_success("Uninstalled old version")
611
+ else:
612
+ self._print_error(f"Failed to uninstall: {result.error}")
613
+ self._print_warning("Attempting to install new version anyway...")
614
+ except Exception as e:
615
+ self._print_error(f"Uninstall error: {e}")
616
+ self._print_warning("Attempting to install new version anyway...")
617
+
618
+ # Install new version
619
+ return self._install_lemonade()
620
+
621
+ def _install_lemonade(self) -> bool:
622
+ """
623
+ Download and install Lemonade Server.
624
+
625
+ Returns:
626
+ True on success, False on failure
627
+ """
628
+ self._print("")
629
+ if RICH_AVAILABLE and self.console:
630
+ self.console.print(
631
+ f" [bold]Downloading[/bold] Lemonade [cyan]v{LEMONADE_VERSION}[/cyan]..."
632
+ )
633
+ else:
634
+ self._print(f" Downloading Lemonade v{LEMONADE_VERSION}...")
635
+
636
+ try:
637
+ # Download installer
638
+ installer_path = self.installer.download_installer()
639
+ self._print("")
640
+ self._print_success("Download complete")
641
+
642
+ # Install (silent in CI with --yes, interactive otherwise for desktop icon)
643
+ self.console.print(" [bold]Installing...[/bold]")
644
+ if not self.yes:
645
+ self.console.print()
646
+ self.console.print(
647
+ " [yellow]⚠️ The installer window will appear - please complete the installation[/yellow]"
648
+ )
649
+ self.console.print()
650
+ result = self.installer.install(installer_path, silent=self.yes)
651
+
652
+ if result.success:
653
+ self._print_success(f"Installed Lemonade v{result.version}")
654
+
655
+ # Refresh PATH from Windows registry so current session can find lemonade-server
656
+ if self.verbose:
657
+ self.console.print(" [dim]Refreshing PATH environment...[/dim]")
658
+ self._refresh_path_environment()
659
+
660
+ # Verify installation by checking version
661
+ if self.verbose:
662
+ self.console.print(" [dim]Verifying installation...[/dim]")
663
+ verify_info = self.installer.check_installation()
664
+
665
+ if verify_info.installed and verify_info.version:
666
+ self._print_success(
667
+ f"Verified: lemonade-server v{verify_info.version}"
668
+ )
669
+ if self.verbose and verify_info.path:
670
+ self.console.print(f" [dim]Path: {verify_info.path}[/dim]")
671
+
672
+ return True
673
+ else:
674
+ self._print_error(f"Installation failed: {result.error}")
675
+
676
+ if "Administrator" in str(result.error) or "sudo" in str(result.error):
677
+ self._print("")
678
+ if RICH_AVAILABLE and self.console:
679
+ self.console.print(
680
+ " [yellow]Try running as Administrator (Windows) or with sudo (Linux)[/yellow]"
681
+ )
682
+ else:
683
+ self._print(
684
+ " Try running as Administrator (Windows) or with sudo (Linux)"
685
+ )
686
+
687
+ return False
688
+
689
+ except Exception as e:
690
+ self._print_error(f"Failed to install: {e}")
691
+ return False
692
+
693
+ def _find_lemonade_server(self) -> Optional[str]:
694
+ """
695
+ Find the lemonade-server executable.
696
+
697
+ Uses the installer's PATH refresh to pick up recent MSI changes.
698
+ Falls back to common installation paths if not found in PATH.
699
+
700
+ Returns:
701
+ Path to lemonade-server executable, or None if not found
702
+ """
703
+ import shutil
704
+
705
+ # Use installer's PATH refresh (reads from Windows registry)
706
+ self.installer.refresh_path_from_registry()
707
+
708
+ # Try to find in updated PATH
709
+ lemonade_path = shutil.which("lemonade-server")
710
+ if lemonade_path:
711
+ return lemonade_path
712
+
713
+ # Fallback: check common installation paths (Windows)
714
+ if sys.platform == "win32":
715
+ common_paths = [
716
+ # Per-user install (most common for MSI)
717
+ os.path.expandvars(
718
+ r"%LOCALAPPDATA%\Programs\Lemonade Server\lemonade-server.exe"
719
+ ),
720
+ os.path.expandvars(
721
+ r"%LOCALAPPDATA%\Lemonade Server\lemonade-server.exe"
722
+ ),
723
+ # System-wide install
724
+ r"C:\Program Files\Lemonade Server\lemonade-server.exe",
725
+ r"C:\Program Files (x86)\Lemonade Server\lemonade-server.exe",
726
+ # Potential alternative paths
727
+ os.path.expandvars(
728
+ r"%USERPROFILE%\lemonade-server\lemonade-server.exe"
729
+ ),
730
+ ]
731
+
732
+ for path in common_paths:
733
+ if os.path.isfile(path):
734
+ if self.verbose:
735
+ log.debug(f"Found lemonade-server at fallback path: {path}")
736
+ return path
737
+
738
+ # Fallback: check common installation paths (Linux)
739
+ elif sys.platform.startswith("linux"):
740
+ common_paths = [
741
+ "/usr/local/bin/lemonade-server",
742
+ "/usr/bin/lemonade-server",
743
+ os.path.expanduser("~/.local/bin/lemonade-server"),
744
+ ]
745
+
746
+ for path in common_paths:
747
+ if os.path.isfile(path):
748
+ if self.verbose:
749
+ log.debug(f"Found lemonade-server at fallback path: {path}")
750
+ return path
751
+
752
+ return None
753
+
754
+ def _ensure_server_running(self) -> bool:
755
+ """
756
+ Ensure Lemonade server is running with health check verification.
757
+
758
+ In remote mode, only checks if server is reachable - does not prompt
759
+ user to start it (assumes it's managed externally).
760
+
761
+ Returns:
762
+ True if server is running and healthy, False on failure
763
+ """
764
+ try:
765
+ # Import here to avoid circular imports
766
+ from gaia.llm.lemonade_client import LemonadeClient
767
+
768
+ client = LemonadeClient(verbose=self.verbose)
769
+
770
+ # Check if already running using health_check
771
+ try:
772
+ health = client.health_check()
773
+ if health:
774
+ self._print_success("Server is already running")
775
+ # Verify health status
776
+ if isinstance(health, dict):
777
+ status = health.get("status", "unknown")
778
+ if status == "ok":
779
+ self._print_success("Server health: OK")
780
+ else:
781
+ self._print_warning(f"Server status: {status}")
782
+ return True
783
+ except Exception as e:
784
+ # Log the health check error for debugging
785
+ log.debug(f"Health check failed: {e}")
786
+ # Server not running
787
+
788
+ # In remote mode, don't prompt to start - just report error
789
+ if self.remote:
790
+ self._print_error("Remote Lemonade Server is not reachable")
791
+ self.console.print()
792
+ self.console.print(
793
+ " [dim]Ensure the remote Lemonade Server is running and accessible.[/dim]"
794
+ )
795
+ self.console.print(
796
+ " [dim]Check LEMONADE_HOST environment variable if using a custom host.[/dim]"
797
+ )
798
+ return False
799
+
800
+ # Server not running - start it automatically in CI mode, or prompt user
801
+ if self.yes:
802
+ # In CI mode, just inform and auto-start (not an error)
803
+ self._print(" Lemonade Server is not running")
804
+ self.console.print()
805
+ self.console.print(
806
+ " [dim]Auto-starting Lemonade Server (CI mode)...[/dim]"
807
+ )
808
+
809
+ try:
810
+ # Find lemonade-server executable
811
+ import shutil
812
+
813
+ # Check env var first (set by install-lemonade action in CI)
814
+ lemonade_path = os.environ.get("LEMONADE_SERVER_PATH")
815
+ if not lemonade_path:
816
+ # Fall back to PATH search
817
+ lemonade_path = shutil.which("lemonade-server")
818
+
819
+ if not lemonade_path:
820
+ raise FileNotFoundError("lemonade-server not found in PATH")
821
+
822
+ # Start server in background
823
+ if sys.platform == "win32":
824
+ # Windows: use subprocess.Popen with no window
825
+ subprocess.Popen(
826
+ [lemonade_path, "serve", "--no-tray"],
827
+ stdout=subprocess.DEVNULL,
828
+ stderr=subprocess.DEVNULL,
829
+ creationflags=(
830
+ subprocess.CREATE_NO_WINDOW
831
+ if hasattr(subprocess, "CREATE_NO_WINDOW")
832
+ else 0
833
+ ),
834
+ )
835
+ else:
836
+ # Linux/Mac: background process
837
+ subprocess.Popen(
838
+ [lemonade_path, "serve"],
839
+ stdout=subprocess.DEVNULL,
840
+ stderr=subprocess.DEVNULL,
841
+ )
842
+
843
+ # Wait for server to start
844
+ import time
845
+
846
+ max_wait = 30
847
+ waited = 0
848
+ while waited < max_wait:
849
+ time.sleep(2)
850
+ waited += 2
851
+ try:
852
+ health = client.health_check()
853
+ if (
854
+ health
855
+ and isinstance(health, dict)
856
+ and health.get("status") == "ok"
857
+ ):
858
+ self._print_success(
859
+ f"Server started and ready (waited {waited}s)"
860
+ )
861
+ return True
862
+ except Exception:
863
+ pass
864
+
865
+ self._print_error(f"Server failed to start after {max_wait}s")
866
+ return False
867
+
868
+ except Exception as e:
869
+ self._print_error(f"Failed to start server: {e}")
870
+ return False
871
+ else:
872
+ # Interactive mode - prompt user to start manually
873
+ self._print_error("Lemonade Server is not running")
874
+ self.console.print()
875
+ self.console.print(" [bold]Please start Lemonade Server:[/bold]")
876
+ if sys.platform == "win32":
877
+ self.console.print(
878
+ " [dim]• Double-click the Lemonade icon in your system tray, or[/dim]"
879
+ )
880
+ self.console.print(
881
+ " [dim]• Search for 'Lemonade' in Start Menu and launch it[/dim]"
882
+ )
883
+ else:
884
+ self.console.print(
885
+ " [dim]• Run:[/dim] [cyan]lemonade-server serve &[/cyan]"
886
+ )
887
+ self.console.print()
888
+
889
+ # Wait for user to start the server
890
+ try:
891
+ self.console.print(
892
+ " [bold]Press Enter when server is started...[/bold]", end=""
893
+ )
894
+ input()
895
+ except (EOFError, KeyboardInterrupt):
896
+ self.console.print()
897
+ self._print_error("Initialization cancelled")
898
+ return False
899
+
900
+ self.console.print()
901
+
902
+ # Check if server is now running
903
+ try:
904
+ health = client.health_check()
905
+ if health and isinstance(health, dict) and health.get("status") == "ok":
906
+ self._print_success("Server is now running")
907
+ self._print_success("Server health: OK")
908
+ return True
909
+ else:
910
+ self._print_error("Server still not responding")
911
+ return False
912
+ except Exception:
913
+ self._print_error("Server still not responding")
914
+ return False
915
+
916
+ except ImportError as e:
917
+ self._print_error(f"Lemonade SDK not installed: {e}")
918
+ if RICH_AVAILABLE and self.console:
919
+ self.console.print(
920
+ " [dim]Run:[/dim] [cyan]pip install lemonade-sdk[/cyan]"
921
+ )
922
+ else:
923
+ self._print(" Run: pip install lemonade-sdk")
924
+ return False
925
+ except Exception as e:
926
+ self._print_error(f"Failed to check/start server: {e}")
927
+ return False
928
+
929
+ def _verify_model(self, client, model_id: str) -> tuple:
930
+ """
931
+ Verify a model is available (downloaded) on the server.
932
+
933
+ Note: We only check if the model exists in the server's model list.
934
+ Running inference to verify would require loading each model, which is
935
+ slow and can cause server issues. If a model is corrupted, the error
936
+ will surface when the user tries to use it.
937
+
938
+ Args:
939
+ client: LemonadeClient instance
940
+ model_id: Model ID to verify
941
+
942
+ Returns:
943
+ Tuple of (success: bool, error_type: str or None)
944
+ """
945
+ try:
946
+ # Check if model is in the available models list
947
+ if client.check_model_available(model_id):
948
+ return (True, None)
949
+ return (False, "not_found")
950
+ except Exception as e:
951
+ log.debug(f"Model verification failed for {model_id}: {e}")
952
+ return (False, "server_error")
953
+
954
+ def _download_models(self) -> bool:
955
+ """
956
+ Download models for the selected profile.
957
+
958
+ Simplified approach: Just try to download all required models.
959
+ Lemonade handles the "already downloaded" case efficiently by
960
+ returning a complete event immediately.
961
+
962
+ Returns:
963
+ True if all models downloaded, False on failure
964
+ """
965
+ try:
966
+ from gaia.llm.lemonade_client import LemonadeClient
967
+
968
+ client = LemonadeClient(verbose=self.verbose)
969
+
970
+ # Get profile config
971
+ profile_config = INIT_PROFILES[self.profile]
972
+ agent = profile_config["agent"]
973
+
974
+ # Get models to download
975
+ if profile_config["models"]:
976
+ # Use profile-specific models (for minimal profile)
977
+ model_ids = profile_config["models"]
978
+ else:
979
+ # Use agent profile defaults
980
+ model_ids = client.get_required_models(agent)
981
+
982
+ # Include default CPU model for profiles that need gaia llm
983
+ # SD profile has its own LLM (Qwen3-8B) and doesn't need the 0.5B model
984
+ if self.profile != "sd":
985
+ from gaia.llm.lemonade_client import DEFAULT_MODEL_NAME
986
+
987
+ if DEFAULT_MODEL_NAME not in model_ids:
988
+ model_ids = list(model_ids) + [DEFAULT_MODEL_NAME]
989
+
990
+ if not model_ids:
991
+ self._print_success("No models required for this profile")
992
+ return True
993
+
994
+ # Show which models will be ensured
995
+ if RICH_AVAILABLE and self.console:
996
+ self.console.print(
997
+ f" [bold]Ensuring {len(model_ids)} model(s) are downloaded:[/bold]"
998
+ )
999
+ for model_id in model_ids:
1000
+ self.console.print(f" [cyan]•[/cyan] {model_id}")
1001
+ else:
1002
+ self._print(f" Ensuring {len(model_ids)} model(s) are downloaded:")
1003
+ for model_id in model_ids:
1004
+ self._print(f" • {model_id}")
1005
+ self._print("")
1006
+
1007
+ if not self._prompt_yes_no("Continue?", default=True):
1008
+ self._print(" Skipping model downloads")
1009
+ return True
1010
+
1011
+ # Force re-download: delete models first
1012
+ if self.force_models:
1013
+ for model_id in model_ids:
1014
+ if client.check_model_available(model_id):
1015
+ if RICH_AVAILABLE and self.console:
1016
+ self.console.print(
1017
+ f" [dim]Deleting (force re-download)[/dim] [cyan]{model_id}[/cyan]..."
1018
+ )
1019
+ else:
1020
+ self._print(
1021
+ f" Deleting (force re-download) {model_id}..."
1022
+ )
1023
+ try:
1024
+ client.delete_model(model_id)
1025
+ self._print_success(f"Deleted {model_id}")
1026
+ except Exception as e:
1027
+ self._print_error(f"Failed to delete {model_id}: {e}")
1028
+
1029
+ # Find lemonade-server executable
1030
+ lemonade_path = self._find_lemonade_server()
1031
+ if not lemonade_path:
1032
+ self._print_error("Could not find lemonade-server executable")
1033
+ self._print(
1034
+ " Please ensure Lemonade Server is installed and in your PATH"
1035
+ )
1036
+ return False
1037
+
1038
+ # Download each model using CLI
1039
+ success = True
1040
+ for model_id in model_ids:
1041
+ self._print("")
1042
+ self.agent_console.print(
1043
+ f" [bold cyan]Downloading:[/bold cyan] {model_id}"
1044
+ )
1045
+
1046
+ try:
1047
+ # Use lemonade-server CLI pull command with visible progress
1048
+ result = subprocess.run(
1049
+ [lemonade_path, "pull", model_id],
1050
+ check=False,
1051
+ )
1052
+
1053
+ self._print("")
1054
+ if result.returncode == 0:
1055
+ # Verify the model was actually downloaded successfully
1056
+ # Check if model is now available (not just exit code)
1057
+ if client.check_model_available(model_id):
1058
+ self._print_success(f"Downloaded {model_id}")
1059
+ else:
1060
+ # Pull succeeded but model not available - likely validation error
1061
+ self._print_error(
1062
+ f"Download validation failed for {model_id}"
1063
+ )
1064
+ self.agent_console.print(
1065
+ " [yellow]Corrupted download detected. Deleting and retrying...[/yellow]"
1066
+ )
1067
+
1068
+ # Delete the corrupted model
1069
+ delete_result = subprocess.run(
1070
+ [lemonade_path, "delete", model_id],
1071
+ check=False,
1072
+ capture_output=True,
1073
+ )
1074
+
1075
+ if delete_result.returncode == 0:
1076
+ self.agent_console.print(
1077
+ f" [dim]Deleted corrupted {model_id}[/dim]"
1078
+ )
1079
+
1080
+ # Retry download
1081
+ self.agent_console.print(
1082
+ f" [bold cyan]Retrying download:[/bold cyan] {model_id}"
1083
+ )
1084
+ retry_result = subprocess.run(
1085
+ [lemonade_path, "pull", model_id],
1086
+ check=False,
1087
+ )
1088
+
1089
+ self._print("")
1090
+ if (
1091
+ retry_result.returncode == 0
1092
+ and client.check_model_available(model_id)
1093
+ ):
1094
+ self._print_success(
1095
+ f"Downloaded {model_id} (retry successful)"
1096
+ )
1097
+ else:
1098
+ self._print_error(f"Retry failed for {model_id}")
1099
+ success = False
1100
+ else:
1101
+ self._print_error(
1102
+ f"Failed to delete corrupted model {model_id}"
1103
+ )
1104
+ success = False
1105
+ else:
1106
+ self._print_error(
1107
+ f"Failed to download {model_id} (exit code: {result.returncode})"
1108
+ )
1109
+ success = False
1110
+
1111
+ except FileNotFoundError:
1112
+ self._print("")
1113
+ self._print_error(f"lemonade-server not found at: {lemonade_path}")
1114
+ success = False
1115
+ break
1116
+ except Exception as e:
1117
+ self._print("")
1118
+ self._print_error(f"Error downloading {model_id}: {e}")
1119
+ success = False
1120
+
1121
+ return success
1122
+
1123
+ except Exception as e:
1124
+ self._print_error(f"Error downloading models: {e}")
1125
+ return False
1126
+
1127
+ def _test_model_inference(self, client, model_id: str) -> tuple:
1128
+ """
1129
+ Test a model with a small inference request.
1130
+
1131
+ Args:
1132
+ client: LemonadeClient instance
1133
+ model_id: Model ID to test
1134
+
1135
+ Returns:
1136
+ Tuple of (success: bool, error_message: str or None)
1137
+ """
1138
+ try:
1139
+ # Check if profile requires specific context size for this model
1140
+ profile_config = INIT_PROFILES.get(self.profile, {})
1141
+ min_ctx = profile_config.get("min_context_size")
1142
+
1143
+ # Load the model (with context size if required)
1144
+ is_llm = not (
1145
+ "embed" in model_id.lower()
1146
+ or any(sd in model_id.upper() for sd in ["SDXL", "SD-", "SD1", "SD2"])
1147
+ )
1148
+
1149
+ if is_llm and min_ctx:
1150
+ # Force unload if already loaded to ensure recipe_options are saved
1151
+ if client.check_model_loaded(model_id):
1152
+ client.unload_model()
1153
+
1154
+ # Load with explicit context size and save it
1155
+ client.load_model(
1156
+ model_id,
1157
+ auto_download=False,
1158
+ prompt=False,
1159
+ ctx_size=min_ctx,
1160
+ save_options=True,
1161
+ )
1162
+
1163
+ # Verify context size was set correctly by reading it back
1164
+ try:
1165
+ # Get full model list with recipe_options
1166
+ models_list = client.list_models()
1167
+ model_info = next(
1168
+ (
1169
+ m
1170
+ for m in models_list.get("data", [])
1171
+ if m.get("id") == model_id
1172
+ ),
1173
+ None,
1174
+ )
1175
+
1176
+ if not model_info:
1177
+ return (False, "Model info not found")
1178
+
1179
+ actual_ctx = model_info.get("recipe_options", {}).get("ctx_size")
1180
+
1181
+ if actual_ctx and actual_ctx >= min_ctx:
1182
+ # Success - context verified
1183
+ # Store for success message, and flag if larger than expected
1184
+ self._ctx_verified = actual_ctx
1185
+ if actual_ctx > min_ctx:
1186
+ self._ctx_warning = (
1187
+ f"(configured: {actual_ctx}, required: {min_ctx})"
1188
+ )
1189
+ elif actual_ctx:
1190
+ # Context was set but is too small
1191
+ return (False, f"Context {actual_ctx} < {min_ctx} required")
1192
+ else:
1193
+ # Context not in recipe_options - should not happen after forced unload/reload
1194
+ # Mark as unverified but don't fail the test
1195
+ self._ctx_verified = None # Explicitly mark as unverified
1196
+ except Exception as e:
1197
+ return (False, f"Context check failed: {str(e)[:50]}")
1198
+ else:
1199
+ # Load without context size (SD models, embedding models, or no requirement)
1200
+ client.load_model(model_id, auto_download=False, prompt=False)
1201
+
1202
+ # Check model type
1203
+ is_embedding_model = "embed" in model_id.lower()
1204
+ is_sd_model = any(
1205
+ sd in model_id.upper() for sd in ["SDXL", "SD-", "SD1", "SD2"]
1206
+ )
1207
+
1208
+ if is_sd_model:
1209
+ # Test SD model with image generation
1210
+ response = client.generate_image(
1211
+ prompt="test",
1212
+ model=model_id,
1213
+ steps=1, # Minimal steps for quick test
1214
+ size="512x512",
1215
+ )
1216
+ # Check if we got a valid image in b64_json format
1217
+ if (
1218
+ response
1219
+ and response.get("data")
1220
+ and response["data"][0].get("b64_json")
1221
+ ):
1222
+ return (True, None)
1223
+ return (False, "No image generated")
1224
+ elif is_embedding_model:
1225
+ # Test embedding model with a simple text
1226
+ response = client.embeddings(
1227
+ input_texts=["test"],
1228
+ model=model_id,
1229
+ )
1230
+ # Check if we got valid embeddings
1231
+ if response and response.get("data"):
1232
+ embedding = response["data"][0].get("embedding", [])
1233
+ if embedding and len(embedding) > 0:
1234
+ return (True, None)
1235
+ return (False, "Empty embedding")
1236
+ return (False, "Invalid response format")
1237
+ else:
1238
+ # Test LLM with a minimal chat request
1239
+ response = client.chat_completions(
1240
+ model=model_id,
1241
+ messages=[{"role": "user", "content": "Say 'ok'"}],
1242
+ max_tokens=10,
1243
+ temperature=0,
1244
+ )
1245
+ # Check if we got a valid response
1246
+ if response and response.get("choices"):
1247
+ content = (
1248
+ response["choices"][0].get("message", {}).get("content", "")
1249
+ )
1250
+ if content:
1251
+ return (True, None)
1252
+ return (False, "Empty response")
1253
+ return (False, "Invalid response format")
1254
+
1255
+ except Exception as e:
1256
+ error_msg = str(e)
1257
+ # Truncate long error messages
1258
+ if len(error_msg) > 100:
1259
+ error_msg = error_msg[:100] + "..."
1260
+ return (False, error_msg)
1261
+
1262
+ def _verify_setup(self) -> bool:
1263
+ """
1264
+ Verify the setup is working by testing each model with a small request.
1265
+
1266
+ Returns:
1267
+ True if verification passes, False on failure
1268
+ """
1269
+ try:
1270
+ from gaia.llm.lemonade_client import LemonadeClient
1271
+
1272
+ client = LemonadeClient(verbose=self.verbose)
1273
+
1274
+ # Check server health
1275
+ try:
1276
+ health = client.health_check()
1277
+ if health:
1278
+ self._print_success("Server health: OK")
1279
+ else:
1280
+ self._print_error("Server not responding")
1281
+ return False
1282
+ except Exception:
1283
+ self._print_error("Server not responding")
1284
+ return False
1285
+
1286
+ # Ensure proper context size for this profile
1287
+ profile_config = INIT_PROFILES[self.profile]
1288
+ min_ctx = profile_config.get("min_context_size")
1289
+ if min_ctx:
1290
+ from gaia.llm.lemonade_manager import LemonadeManager
1291
+
1292
+ self.console.print()
1293
+ self.console.print(
1294
+ f" [dim]Ensuring {min_ctx} token context for {self.profile} profile...[/dim]"
1295
+ )
1296
+ success = LemonadeManager.ensure_ready(
1297
+ min_context_size=min_ctx, quiet=True
1298
+ )
1299
+ if success:
1300
+ self._print_success(f"Context size verified: {min_ctx} tokens")
1301
+ else:
1302
+ self._print_error(f"Failed to configure {min_ctx} token context")
1303
+ self._print_error(
1304
+ f"Try: lemonade-server serve --ctx-size {min_ctx}"
1305
+ )
1306
+ return False
1307
+
1308
+ # Get models to verify
1309
+ profile_config = INIT_PROFILES[self.profile]
1310
+ if profile_config["models"]:
1311
+ model_ids = profile_config["models"]
1312
+ else:
1313
+ model_ids = client.get_required_models(profile_config["agent"])
1314
+
1315
+ # Include default CPU model for profiles that need gaia llm
1316
+ # SD profile has its own LLM and doesn't need the 0.5B model
1317
+ if self.profile != "sd":
1318
+ from gaia.llm.lemonade_client import DEFAULT_MODEL_NAME
1319
+
1320
+ if DEFAULT_MODEL_NAME not in model_ids:
1321
+ model_ids = list(model_ids) + [DEFAULT_MODEL_NAME]
1322
+
1323
+ if not model_ids or self.skip_models:
1324
+ return True
1325
+
1326
+ # Prompt to run model verification (can be slow)
1327
+ self.console.print()
1328
+ self.console.print(
1329
+ " [dim]Model verification loads each model and runs a small inference test.[/dim]"
1330
+ )
1331
+ self.console.print(
1332
+ " [dim]This may take a few minutes but ensures models work correctly.[/dim]"
1333
+ )
1334
+ self.console.print()
1335
+
1336
+ if not self._prompt_yes_no("Run model verification?", default=True):
1337
+ self._print_success("Skipping model verification")
1338
+ return True
1339
+
1340
+ # Test each model with a small inference request
1341
+ self.console.print()
1342
+ self.console.print(" [bold]Testing models with inference:[/bold]")
1343
+
1344
+ models_passed = 0
1345
+ models_failed = []
1346
+ interrupted = False
1347
+
1348
+ try:
1349
+ for model_id in model_ids:
1350
+ # Check if model is available first
1351
+ if not client.check_model_available(model_id):
1352
+ self.console.print(
1353
+ f" [yellow]⏭️[/yellow] [cyan]{model_id}[/cyan] [dim]- not downloaded[/dim]"
1354
+ )
1355
+ continue
1356
+
1357
+ # Show testing status
1358
+ self.console.print(
1359
+ f" [dim]🔄[/dim] [cyan]{model_id}[/cyan] [dim]- testing...[/dim]",
1360
+ end="",
1361
+ )
1362
+
1363
+ # Test the model
1364
+ success, error = self._test_model_inference(client, model_id)
1365
+
1366
+ # Clear the line and show result
1367
+ print("\r" + " " * 80 + "\r", end="")
1368
+ if success:
1369
+ # Check if context was verified
1370
+ ctx_msg = ""
1371
+ if hasattr(self, "_ctx_verified"):
1372
+ if self._ctx_verified:
1373
+ # Context successfully verified
1374
+ ctx_msg = f" [dim](ctx: {self._ctx_verified})[/dim]"
1375
+
1376
+ # Warn if context is larger than required
1377
+ if hasattr(self, "_ctx_warning"):
1378
+ ctx_msg = f" [yellow]{self._ctx_warning}[/yellow]"
1379
+ delattr(self, "_ctx_warning")
1380
+ elif self._ctx_verified is None:
1381
+ # Context could not be verified
1382
+ ctx_msg = " [yellow]⚠️ Context unverified![/yellow]"
1383
+
1384
+ delattr(self, "_ctx_verified") # Reset for next model
1385
+
1386
+ self.console.print(
1387
+ f" [green]✓[/green] [cyan]{model_id}[/cyan] [dim]- OK[/dim]{ctx_msg}"
1388
+ )
1389
+ models_passed += 1
1390
+ else:
1391
+ self.console.print(
1392
+ f" [red]❌[/red] [cyan]{model_id}[/cyan] [dim]- {error}[/dim]"
1393
+ )
1394
+ models_failed.append((model_id, error))
1395
+
1396
+ except KeyboardInterrupt:
1397
+ print("\r" + " " * 80 + "\r", end="")
1398
+ self.console.print()
1399
+ self._print_warning("Verification interrupted")
1400
+ interrupted = True
1401
+
1402
+ # Summary
1403
+ total = len(model_ids)
1404
+ self.console.print()
1405
+ if interrupted:
1406
+ self._print_success(
1407
+ f"Verified {models_passed} model(s) before interruption"
1408
+ )
1409
+ elif models_failed:
1410
+ self._print_warning(f"Models verified: {models_passed}/{total} passed")
1411
+ self.console.print()
1412
+ self.console.print(
1413
+ " [bold]Failed models may be corrupted. To fix:[/bold]"
1414
+ )
1415
+ self.console.print(
1416
+ " [dim]Option 1 - Delete all models and re-download:[/dim]"
1417
+ )
1418
+ self.console.print(" [cyan]gaia uninstall --models --yes[/cyan]")
1419
+ self.console.print(
1420
+ f" [cyan]gaia init --profile {self.profile} --yes[/cyan]"
1421
+ )
1422
+ self.console.print()
1423
+ self.console.print(
1424
+ " [dim]Option 2 - Manually delete failed models:[/dim]"
1425
+ )
1426
+
1427
+ # Show path for each failed model
1428
+ hf_cache = os.path.expanduser("~/.cache/huggingface/hub")
1429
+ from pathlib import Path
1430
+
1431
+ for model_id, error in models_failed:
1432
+ # Find actual model directory (may have org prefix like ggml-org/model-name)
1433
+ # Search for directories containing the model name
1434
+ model_name_part = model_id.split("/")[-1] # Get last part if has /
1435
+ matching_dirs = list(
1436
+ Path(hf_cache).glob(f"models--*{model_name_part}*")
1437
+ )
1438
+
1439
+ if matching_dirs:
1440
+ model_path = str(matching_dirs[0])
1441
+ self.console.print(
1442
+ f" [cyan]{model_id}[/cyan]: [dim]{model_path}[/dim]"
1443
+ )
1444
+ if sys.platform == "win32":
1445
+ self.console.print(
1446
+ f' [yellow]rmdir /s /q[/yellow] [cyan]"{model_path}"[/cyan]'
1447
+ )
1448
+ else:
1449
+ self.console.print(
1450
+ f' [yellow]rm -rf[/yellow] [cyan]"{model_path}"[/cyan]'
1451
+ )
1452
+ else:
1453
+ # Fallback if directory not found
1454
+ self.console.print(
1455
+ f" [cyan]{model_id}[/cyan]: [dim]Not found in cache[/dim]"
1456
+ )
1457
+
1458
+ self.console.print()
1459
+ self.console.print(
1460
+ f" [dim]Then re-download:[/dim] [cyan]gaia init --profile {self.profile} --yes[/cyan]"
1461
+ )
1462
+ else:
1463
+ self._print_success(f"All {models_passed} model(s) verified")
1464
+
1465
+ return True # Don't fail init due to model issues
1466
+
1467
+ except Exception as e:
1468
+ self._print_error(f"Verification failed: {e}")
1469
+ return False
1470
+
1471
+ def _print_completion(self):
1472
+ """Print completion message with next steps."""
1473
+ if RICH_AVAILABLE and self.console:
1474
+ self.console.print()
1475
+ self.console.print(
1476
+ Panel(
1477
+ "[bold green]GAIA initialization complete![/bold green]",
1478
+ border_style="green",
1479
+ padding=(0, 2),
1480
+ )
1481
+ )
1482
+ self.console.print()
1483
+ self.console.print(" [bold]Quick start commands:[/bold]")
1484
+
1485
+ # Profile-specific quick start commands
1486
+ if self.profile == "sd":
1487
+ self.console.print(
1488
+ ' [cyan]gaia sd "create a cute robot kitten and tell me a story"[/cyan]'
1489
+ )
1490
+ self.console.print(' [cyan]gaia sd "sunset over mountains"[/cyan]')
1491
+ self.console.print(
1492
+ " [cyan]gaia sd -i[/cyan] Interactive mode"
1493
+ )
1494
+ elif self.profile == "chat":
1495
+ self.console.print(
1496
+ " [cyan]gaia chat[/cyan] Start interactive chat with RAG"
1497
+ )
1498
+ self.console.print(
1499
+ " [cyan]gaia chat init[/cyan] Setup document folder"
1500
+ )
1501
+ elif self.profile == "minimal":
1502
+ self.console.print(
1503
+ " [cyan]gaia llm 'Hello'[/cyan] Quick LLM query"
1504
+ )
1505
+ self.console.print(
1506
+ " [dim]Note: Minimal profile installed. For full features, run:[/dim]"
1507
+ )
1508
+ self.console.print(" [cyan]gaia init --profile chat[/cyan]")
1509
+ else:
1510
+ # Default commands for other profiles
1511
+ self.console.print(
1512
+ " [cyan]gaia chat[/cyan] Start interactive chat"
1513
+ )
1514
+ self.console.print(
1515
+ " [cyan]gaia llm 'Hello'[/cyan] Quick LLM query"
1516
+ )
1517
+ self.console.print(
1518
+ " [cyan]gaia talk[/cyan] Voice interaction"
1519
+ )
1520
+ self.console.print()
1521
+ else:
1522
+ self._print("")
1523
+ self._print("=" * 60)
1524
+ self._print(" GAIA initialization complete!")
1525
+ self._print("=" * 60)
1526
+ self._print("")
1527
+ self._print(" Quick start commands:")
1528
+
1529
+ # Profile-specific quick start commands
1530
+ if self.profile == "sd":
1531
+ self._print(
1532
+ ' gaia sd "create a cute robot kitten and tell me a story"'
1533
+ )
1534
+ self._print(' gaia sd "sunset over mountains"')
1535
+ self._print(
1536
+ " gaia sd -i # Interactive mode"
1537
+ )
1538
+ elif self.profile == "chat":
1539
+ self._print(
1540
+ " gaia chat # Start interactive chat with RAG"
1541
+ )
1542
+ self._print(" gaia chat init # Setup document folder")
1543
+ elif self.profile == "minimal":
1544
+ self._print(" gaia llm 'Hello' # Quick LLM query")
1545
+ self._print("")
1546
+ self._print(
1547
+ " Note: Minimal profile installed. For full features, run:"
1548
+ )
1549
+ self._print(" gaia init --profile chat")
1550
+ else:
1551
+ # Default commands for other profiles
1552
+ self._print(" gaia chat # Start interactive chat")
1553
+ self._print(" gaia llm 'Hello' # Quick LLM query")
1554
+ self._print(" gaia talk # Voice interaction")
1555
+ self._print("")
1556
+
1557
+
1558
+ def run_init(
1559
+ profile: str = "chat",
1560
+ skip_models: bool = False,
1561
+ skip_lemonade: bool = False,
1562
+ force_reinstall: bool = False,
1563
+ force_models: bool = False,
1564
+ yes: bool = False,
1565
+ verbose: bool = False,
1566
+ remote: bool = False,
1567
+ ) -> int:
1568
+ """
1569
+ Entry point for `gaia init` command.
1570
+
1571
+ Args:
1572
+ profile: Profile to initialize (minimal, chat, code, rag, all)
1573
+ skip_models: Skip model downloads
1574
+ skip_lemonade: Skip Lemonade installation check (for CI)
1575
+ force_reinstall: Force reinstall even if compatible version exists
1576
+ force_models: Force re-download models (deletes then re-downloads)
1577
+ yes: Skip confirmation prompts
1578
+ verbose: Enable verbose output
1579
+ remote: Lemonade is on a remote machine (skip local start, still check version)
1580
+
1581
+ Returns:
1582
+ Exit code (0 for success, non-zero for failure)
1583
+ """
1584
+ try:
1585
+ cmd = InitCommand(
1586
+ profile=profile,
1587
+ skip_models=skip_models,
1588
+ skip_lemonade=skip_lemonade,
1589
+ force_reinstall=force_reinstall,
1590
+ force_models=force_models,
1591
+ yes=yes,
1592
+ verbose=verbose,
1593
+ remote=remote,
1594
+ )
1595
+ return cmd.run()
1596
+ except ValueError as e:
1597
+ print(f"❌ Error: {e}", file=sys.stderr)
1598
+ return 1
1599
+ except Exception as e:
1600
+ print(f"❌ Unexpected error: {e}", file=sys.stderr)
1601
+ if verbose:
1602
+ import traceback
1603
+
1604
+ traceback.print_exc()
1605
+ return 1