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.
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.2.dist-info}/METADATA +1 -2
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.2.dist-info}/RECORD +35 -31
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.2.dist-info}/WHEEL +1 -1
- gaia/agents/base/agent.py +45 -90
- gaia/agents/base/api_agent.py +0 -1
- gaia/agents/base/console.py +126 -0
- gaia/agents/base/tools.py +7 -2
- gaia/agents/blender/__init__.py +7 -0
- gaia/agents/blender/agent.py +7 -10
- gaia/agents/blender/core/view.py +2 -2
- gaia/agents/chat/agent.py +22 -48
- gaia/agents/chat/app.py +7 -0
- gaia/agents/chat/tools/rag_tools.py +23 -8
- gaia/agents/chat/tools/shell_tools.py +1 -0
- gaia/agents/code/prompts/code_patterns.py +2 -4
- gaia/agents/docker/agent.py +1 -0
- gaia/agents/emr/agent.py +3 -5
- gaia/agents/emr/cli.py +1 -1
- gaia/agents/emr/dashboard/server.py +2 -4
- gaia/apps/llm/app.py +14 -3
- gaia/chat/app.py +2 -4
- gaia/cli.py +511 -333
- gaia/installer/__init__.py +23 -0
- gaia/installer/init_command.py +1275 -0
- gaia/installer/lemonade_installer.py +619 -0
- gaia/llm/__init__.py +2 -1
- gaia/llm/lemonade_client.py +284 -99
- gaia/llm/providers/lemonade.py +12 -14
- gaia/rag/sdk.py +1 -1
- gaia/security.py +24 -4
- gaia/talk/app.py +2 -4
- gaia/version.py +2 -2
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.2.dist-info}/entry_points.txt +0 -0
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.2.dist-info}/licenses/LICENSE.md +0 -0
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|