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,619 @@
|
|
|
1
|
+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Lemonade Server Installer
|
|
6
|
+
|
|
7
|
+
Handles detection, download, and installation of Lemonade Server
|
|
8
|
+
from GitHub releases for Windows and Linux platforms.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import tempfile
|
|
18
|
+
import urllib.request
|
|
19
|
+
import uuid
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Callable, Optional
|
|
23
|
+
|
|
24
|
+
from gaia.version import LEMONADE_VERSION
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Rich imports for console output
|
|
29
|
+
try:
|
|
30
|
+
from rich.console import Console # pylint: disable=unused-import
|
|
31
|
+
|
|
32
|
+
RICH_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
RICH_AVAILABLE = False
|
|
35
|
+
Console = None # type: ignore
|
|
36
|
+
|
|
37
|
+
# GitHub release URL patterns
|
|
38
|
+
GITHUB_RELEASE_BASE = "https://github.com/lemonade-sdk/lemonade/releases/download"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class LemonadeInfo:
|
|
43
|
+
"""Information about Lemonade Server installation."""
|
|
44
|
+
|
|
45
|
+
installed: bool
|
|
46
|
+
version: Optional[str] = None
|
|
47
|
+
path: Optional[str] = None
|
|
48
|
+
error: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def version_tuple(self) -> Optional[tuple]:
|
|
52
|
+
"""Parse version string into tuple for comparison."""
|
|
53
|
+
if not self.version:
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
# Handle versions like "9.1.4" or "v9.1.4"
|
|
57
|
+
ver = self.version.lstrip("v")
|
|
58
|
+
parts = ver.split(".")
|
|
59
|
+
return tuple(int(p) for p in parts[:3])
|
|
60
|
+
except (ValueError, IndexError):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class InstallResult:
|
|
66
|
+
"""Result of an installation attempt."""
|
|
67
|
+
|
|
68
|
+
success: bool
|
|
69
|
+
version: Optional[str] = None
|
|
70
|
+
message: str = ""
|
|
71
|
+
error: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LemonadeInstaller:
|
|
75
|
+
"""Handles Lemonade Server installation and management."""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
target_version: str = LEMONADE_VERSION,
|
|
80
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
81
|
+
minimal: bool = False,
|
|
82
|
+
console: Optional[Any] = None,
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Initialize the installer.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
target_version: Target Lemonade version to install
|
|
89
|
+
progress_callback: Optional callback for download progress (bytes_downloaded, total_bytes)
|
|
90
|
+
minimal: Use minimal installer (smaller download, fewer features)
|
|
91
|
+
console: Optional Rich Console for user-facing output (suppresses log messages)
|
|
92
|
+
"""
|
|
93
|
+
self.target_version = target_version.lstrip("v")
|
|
94
|
+
self.progress_callback = progress_callback
|
|
95
|
+
self.minimal = minimal
|
|
96
|
+
self.system = platform.system().lower()
|
|
97
|
+
self.console = console
|
|
98
|
+
|
|
99
|
+
def _print_status(self, message: str, style: str = "dim"):
|
|
100
|
+
"""Print a status message to console or log."""
|
|
101
|
+
if self.console and RICH_AVAILABLE:
|
|
102
|
+
self.console.print(f" [{style}]{message}[/{style}]")
|
|
103
|
+
elif not self.console:
|
|
104
|
+
# Only log if no console provided (to avoid duplicate output)
|
|
105
|
+
log.debug(message)
|
|
106
|
+
|
|
107
|
+
def refresh_path_from_registry(self) -> None:
|
|
108
|
+
"""Refresh PATH from Windows registry after MSI install."""
|
|
109
|
+
if self.system != "windows":
|
|
110
|
+
return
|
|
111
|
+
try:
|
|
112
|
+
import winreg
|
|
113
|
+
|
|
114
|
+
user_path = ""
|
|
115
|
+
try:
|
|
116
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Environment") as key:
|
|
117
|
+
user_path, _ = winreg.QueryValueEx(key, "Path")
|
|
118
|
+
except (FileNotFoundError, OSError):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
system_path = ""
|
|
122
|
+
try:
|
|
123
|
+
with winreg.OpenKey(
|
|
124
|
+
winreg.HKEY_LOCAL_MACHINE,
|
|
125
|
+
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
|
|
126
|
+
) as key:
|
|
127
|
+
system_path, _ = winreg.QueryValueEx(key, "Path")
|
|
128
|
+
except (FileNotFoundError, OSError):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
if user_path or system_path:
|
|
132
|
+
new_path = (
|
|
133
|
+
f"{user_path};{system_path}"
|
|
134
|
+
if user_path and system_path
|
|
135
|
+
else (user_path or system_path)
|
|
136
|
+
)
|
|
137
|
+
os.environ["PATH"] = new_path
|
|
138
|
+
log.debug("Refreshed PATH from registry")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
log.debug(f"Failed to refresh PATH: {e}")
|
|
141
|
+
|
|
142
|
+
def check_installation(self) -> LemonadeInfo:
|
|
143
|
+
"""
|
|
144
|
+
Check if Lemonade Server is installed and get version info.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
LemonadeInfo with installation status
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
# Refresh PATH from registry (in case MSI just updated it)
|
|
151
|
+
self.refresh_path_from_registry()
|
|
152
|
+
|
|
153
|
+
# Try to find lemonade-server executable
|
|
154
|
+
lemonade_path = shutil.which("lemonade-server")
|
|
155
|
+
|
|
156
|
+
if not lemonade_path:
|
|
157
|
+
return LemonadeInfo(
|
|
158
|
+
installed=False, error="lemonade-server not found in PATH"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Get version
|
|
162
|
+
result = subprocess.run(
|
|
163
|
+
["lemonade-server", "--version"],
|
|
164
|
+
capture_output=True,
|
|
165
|
+
text=True,
|
|
166
|
+
timeout=10,
|
|
167
|
+
check=False,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if result.returncode != 0:
|
|
171
|
+
return LemonadeInfo(
|
|
172
|
+
installed=True,
|
|
173
|
+
path=lemonade_path,
|
|
174
|
+
error=f"Failed to get version: {result.stderr}",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Parse version from output
|
|
178
|
+
# Expected format: "lemonade-server 9.1.4" or just "9.1.4"
|
|
179
|
+
version_output = result.stdout.strip()
|
|
180
|
+
version_match = re.search(r"(\d+\.\d+\.\d+)", version_output)
|
|
181
|
+
|
|
182
|
+
if version_match:
|
|
183
|
+
version = version_match.group(1)
|
|
184
|
+
else:
|
|
185
|
+
version = version_output
|
|
186
|
+
|
|
187
|
+
return LemonadeInfo(installed=True, version=version, path=lemonade_path)
|
|
188
|
+
|
|
189
|
+
except FileNotFoundError:
|
|
190
|
+
return LemonadeInfo(
|
|
191
|
+
installed=False, error="lemonade-server not found in PATH"
|
|
192
|
+
)
|
|
193
|
+
except subprocess.TimeoutExpired:
|
|
194
|
+
return LemonadeInfo(
|
|
195
|
+
installed=False, error="Timeout checking lemonade-server version"
|
|
196
|
+
)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
return LemonadeInfo(installed=False, error=str(e))
|
|
199
|
+
|
|
200
|
+
def needs_install(self, info: LemonadeInfo) -> bool:
|
|
201
|
+
"""
|
|
202
|
+
Check if installation or update is needed.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
info: Current installation info
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if install/update is needed
|
|
209
|
+
"""
|
|
210
|
+
if not info.installed:
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
if not info.version:
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
# Compare versions
|
|
217
|
+
current = info.version_tuple
|
|
218
|
+
target = self._parse_version(self.target_version)
|
|
219
|
+
|
|
220
|
+
if not current or not target:
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
# Need install if current version is older
|
|
224
|
+
return current < target
|
|
225
|
+
|
|
226
|
+
def _parse_version(self, version: str) -> Optional[tuple]:
|
|
227
|
+
"""Parse version string into tuple."""
|
|
228
|
+
try:
|
|
229
|
+
ver = version.lstrip("v")
|
|
230
|
+
parts = ver.split(".")
|
|
231
|
+
return tuple(int(p) for p in parts[:3])
|
|
232
|
+
except (ValueError, IndexError):
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
def get_download_url(self) -> str:
|
|
236
|
+
"""
|
|
237
|
+
Get the download URL for the current platform.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Download URL for the installer
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
RuntimeError: If platform is not supported
|
|
244
|
+
"""
|
|
245
|
+
version = self.target_version
|
|
246
|
+
|
|
247
|
+
if self.system == "windows":
|
|
248
|
+
if self.minimal:
|
|
249
|
+
# Minimal installer for lightweight setup
|
|
250
|
+
return f"{GITHUB_RELEASE_BASE}/v{version}/lemonade-server-minimal.msi"
|
|
251
|
+
else:
|
|
252
|
+
# Full installer
|
|
253
|
+
return f"{GITHUB_RELEASE_BASE}/v{version}/lemonade.msi"
|
|
254
|
+
elif self.system == "linux":
|
|
255
|
+
# Linux DEB - filename includes version (no minimal variant yet)
|
|
256
|
+
return f"{GITHUB_RELEASE_BASE}/v{version}/lemonade_{version}_amd64.deb"
|
|
257
|
+
else:
|
|
258
|
+
raise RuntimeError(
|
|
259
|
+
f"Platform '{self.system}' is not supported. "
|
|
260
|
+
"GAIA init only supports Windows and Linux."
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def get_installer_filename(self) -> str:
|
|
264
|
+
"""Get the installer filename for the current platform."""
|
|
265
|
+
if self.system == "windows":
|
|
266
|
+
if self.minimal:
|
|
267
|
+
return "lemonade-server-minimal.msi"
|
|
268
|
+
else:
|
|
269
|
+
return "lemonade.msi"
|
|
270
|
+
elif self.system == "linux":
|
|
271
|
+
return f"lemonade_{self.target_version}_amd64.deb"
|
|
272
|
+
else:
|
|
273
|
+
raise RuntimeError(f"Platform '{self.system}' is not supported.")
|
|
274
|
+
|
|
275
|
+
def download_installer(self, dest_dir: Optional[str] = None) -> Path:
|
|
276
|
+
"""
|
|
277
|
+
Download the Lemonade installer.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
dest_dir: Destination directory (uses temp dir if not specified)
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Path to downloaded installer
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
RuntimeError: If download fails
|
|
287
|
+
"""
|
|
288
|
+
url = self.get_download_url()
|
|
289
|
+
filename = self.get_installer_filename()
|
|
290
|
+
|
|
291
|
+
if dest_dir:
|
|
292
|
+
dest_path = Path(dest_dir) / filename
|
|
293
|
+
else:
|
|
294
|
+
dest_path = Path(tempfile.gettempdir()) / filename
|
|
295
|
+
|
|
296
|
+
self._print_status(f"Downloading from {url}")
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
# Remove existing file if it exists (may be locked from previous attempt)
|
|
300
|
+
if dest_path.exists():
|
|
301
|
+
try:
|
|
302
|
+
dest_path.unlink()
|
|
303
|
+
log.debug(f"Removed existing installer at {dest_path}")
|
|
304
|
+
except PermissionError:
|
|
305
|
+
# File is locked, use a unique filename instead
|
|
306
|
+
unique_name = f"lemonade_{uuid.uuid4().hex[:8]}.msi"
|
|
307
|
+
dest_path = Path(tempfile.gettempdir()) / unique_name
|
|
308
|
+
log.debug(f"Using unique filename: {dest_path}")
|
|
309
|
+
|
|
310
|
+
# Create request with User-Agent header
|
|
311
|
+
request = urllib.request.Request(
|
|
312
|
+
url, headers={"User-Agent": "GAIA-Installer/1.0"}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Download with progress reporting
|
|
316
|
+
with urllib.request.urlopen(request, timeout=300) as response:
|
|
317
|
+
total_size = int(response.headers.get("content-length", 0))
|
|
318
|
+
downloaded = 0
|
|
319
|
+
chunk_size = 8192
|
|
320
|
+
|
|
321
|
+
with open(dest_path, "wb") as f:
|
|
322
|
+
while True:
|
|
323
|
+
chunk = response.read(chunk_size)
|
|
324
|
+
if not chunk:
|
|
325
|
+
break
|
|
326
|
+
f.write(chunk)
|
|
327
|
+
downloaded += len(chunk)
|
|
328
|
+
|
|
329
|
+
if self.progress_callback:
|
|
330
|
+
self.progress_callback(downloaded, total_size)
|
|
331
|
+
|
|
332
|
+
self._print_status(f"Downloaded to {dest_path}")
|
|
333
|
+
return dest_path
|
|
334
|
+
|
|
335
|
+
except urllib.error.HTTPError as e:
|
|
336
|
+
if e.code == 404:
|
|
337
|
+
raise RuntimeError(
|
|
338
|
+
f"Lemonade v{self.target_version} not found. "
|
|
339
|
+
"Please check https://github.com/lemonade-sdk/lemonade/releases "
|
|
340
|
+
"for available versions."
|
|
341
|
+
)
|
|
342
|
+
raise RuntimeError(f"Download failed: HTTP {e.code} - {e.reason}")
|
|
343
|
+
except urllib.error.URLError as e:
|
|
344
|
+
raise RuntimeError(f"Download failed: {e.reason}")
|
|
345
|
+
except Exception as e:
|
|
346
|
+
raise RuntimeError(f"Download failed: {e}")
|
|
347
|
+
|
|
348
|
+
def install(self, installer_path: Path, silent: bool = True) -> InstallResult:
|
|
349
|
+
"""
|
|
350
|
+
Install Lemonade Server from the downloaded installer.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
installer_path: Path to the installer file
|
|
354
|
+
silent: Whether to run silent installation (no UI)
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
InstallResult with success status
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
RuntimeError: If installation fails
|
|
361
|
+
"""
|
|
362
|
+
if not installer_path.exists():
|
|
363
|
+
return InstallResult(
|
|
364
|
+
success=False, error=f"Installer not found: {installer_path}"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
self._print_status(f"Installing from {installer_path}")
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
if self.system == "windows":
|
|
371
|
+
return self._install_windows(installer_path, silent)
|
|
372
|
+
elif self.system == "linux":
|
|
373
|
+
return self._install_linux(installer_path)
|
|
374
|
+
else:
|
|
375
|
+
return InstallResult(
|
|
376
|
+
success=False, error=f"Platform '{self.system}' is not supported"
|
|
377
|
+
)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
return InstallResult(success=False, error=str(e))
|
|
380
|
+
|
|
381
|
+
def _install_windows(self, installer_path: Path, silent: bool) -> InstallResult:
|
|
382
|
+
"""Install on Windows using msiexec."""
|
|
383
|
+
try:
|
|
384
|
+
cmd = ["msiexec", "/i", str(installer_path)]
|
|
385
|
+
|
|
386
|
+
if silent:
|
|
387
|
+
cmd.extend(["/qn", "/norestart"])
|
|
388
|
+
|
|
389
|
+
log.debug(f"Running: {' '.join(cmd)}")
|
|
390
|
+
|
|
391
|
+
result = subprocess.run(
|
|
392
|
+
cmd,
|
|
393
|
+
capture_output=True,
|
|
394
|
+
text=True,
|
|
395
|
+
timeout=300, # 5 minute timeout
|
|
396
|
+
check=False,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if result.returncode == 0:
|
|
400
|
+
return InstallResult(
|
|
401
|
+
success=True,
|
|
402
|
+
version=self.target_version,
|
|
403
|
+
message=f"Installed Lemonade v{self.target_version}",
|
|
404
|
+
)
|
|
405
|
+
elif result.returncode == 1602:
|
|
406
|
+
return InstallResult(
|
|
407
|
+
success=False, error="Installation was cancelled by user"
|
|
408
|
+
)
|
|
409
|
+
elif result.returncode == 1603:
|
|
410
|
+
return InstallResult(
|
|
411
|
+
success=False,
|
|
412
|
+
error="Installation failed. Check Windows Event Log for details.",
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
return InstallResult(
|
|
416
|
+
success=False,
|
|
417
|
+
error=f"msiexec failed with code {result.returncode}: {result.stderr}",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
except subprocess.TimeoutExpired:
|
|
421
|
+
return InstallResult(success=False, error="Installation timed out")
|
|
422
|
+
except FileNotFoundError:
|
|
423
|
+
return InstallResult(success=False, error="msiexec not found")
|
|
424
|
+
except Exception as e:
|
|
425
|
+
return InstallResult(success=False, error=str(e))
|
|
426
|
+
|
|
427
|
+
def _install_linux(self, installer_path: Path) -> InstallResult:
|
|
428
|
+
"""Install on Linux using dpkg."""
|
|
429
|
+
try:
|
|
430
|
+
# Check if we have root access (geteuid only available on Unix)
|
|
431
|
+
is_root = False
|
|
432
|
+
if hasattr(os, "geteuid"):
|
|
433
|
+
is_root = os.geteuid() == 0
|
|
434
|
+
|
|
435
|
+
if not is_root:
|
|
436
|
+
# Try with sudo
|
|
437
|
+
cmd = ["sudo", "dpkg", "-i", str(installer_path)]
|
|
438
|
+
else:
|
|
439
|
+
cmd = ["dpkg", "-i", str(installer_path)]
|
|
440
|
+
|
|
441
|
+
log.debug(f"Running: {' '.join(cmd)}")
|
|
442
|
+
|
|
443
|
+
result = subprocess.run(
|
|
444
|
+
cmd, capture_output=True, text=True, timeout=300, check=False
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if result.returncode == 0:
|
|
448
|
+
return InstallResult(
|
|
449
|
+
success=True,
|
|
450
|
+
version=self.target_version,
|
|
451
|
+
message=f"Installed Lemonade v{self.target_version}",
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
# dpkg might fail due to missing dependencies
|
|
455
|
+
# Try to fix with apt
|
|
456
|
+
if "dependency" in result.stderr.lower():
|
|
457
|
+
fix_cmd = ["sudo", "apt-get", "install", "-f", "-y"]
|
|
458
|
+
fix_result = subprocess.run(
|
|
459
|
+
fix_cmd,
|
|
460
|
+
capture_output=True,
|
|
461
|
+
text=True,
|
|
462
|
+
timeout=300,
|
|
463
|
+
check=False,
|
|
464
|
+
)
|
|
465
|
+
if fix_result.returncode == 0:
|
|
466
|
+
return InstallResult(
|
|
467
|
+
success=True,
|
|
468
|
+
version=self.target_version,
|
|
469
|
+
message=f"Installed Lemonade v{self.target_version} (fixed dependencies)",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
return InstallResult(
|
|
473
|
+
success=False,
|
|
474
|
+
error=f"dpkg failed: {result.stderr}",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
except subprocess.TimeoutExpired:
|
|
478
|
+
return InstallResult(success=False, error="Installation timed out")
|
|
479
|
+
except FileNotFoundError as e:
|
|
480
|
+
return InstallResult(
|
|
481
|
+
success=False, error=f"Required command not found: {e}"
|
|
482
|
+
)
|
|
483
|
+
except Exception as e:
|
|
484
|
+
return InstallResult(success=False, error=str(e))
|
|
485
|
+
|
|
486
|
+
def is_platform_supported(self) -> bool:
|
|
487
|
+
"""Check if the current platform is supported for installation."""
|
|
488
|
+
return self.system in ("windows", "linux")
|
|
489
|
+
|
|
490
|
+
def get_platform_name(self) -> str:
|
|
491
|
+
"""Get a friendly name for the current platform."""
|
|
492
|
+
names = {
|
|
493
|
+
"windows": "Windows",
|
|
494
|
+
"linux": "Linux",
|
|
495
|
+
"darwin": "macOS",
|
|
496
|
+
}
|
|
497
|
+
return names.get(self.system, self.system.capitalize())
|
|
498
|
+
|
|
499
|
+
def uninstall(self, silent: bool = True) -> InstallResult:
|
|
500
|
+
"""
|
|
501
|
+
Uninstall Lemonade Server.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
silent: Whether to run silent uninstallation (no UI)
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
InstallResult with success status
|
|
508
|
+
"""
|
|
509
|
+
self._print_status("Uninstalling Lemonade Server...")
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
if self.system == "windows":
|
|
513
|
+
return self._uninstall_windows(silent)
|
|
514
|
+
elif self.system == "linux":
|
|
515
|
+
return self._uninstall_linux()
|
|
516
|
+
else:
|
|
517
|
+
return InstallResult(
|
|
518
|
+
success=False, error=f"Platform '{self.system}' is not supported"
|
|
519
|
+
)
|
|
520
|
+
except Exception as e:
|
|
521
|
+
return InstallResult(success=False, error=str(e))
|
|
522
|
+
|
|
523
|
+
def _uninstall_windows(self, silent: bool) -> InstallResult:
|
|
524
|
+
"""Uninstall on Windows using msiexec."""
|
|
525
|
+
try:
|
|
526
|
+
# Get currently installed version - we need matching MSI to uninstall
|
|
527
|
+
info = self.check_installation()
|
|
528
|
+
if not info.installed or not info.version:
|
|
529
|
+
return InstallResult(
|
|
530
|
+
success=False, error="Lemonade Server is not installed"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
installed_version = info.version.lstrip("v")
|
|
534
|
+
|
|
535
|
+
# Create installer for the installed version (not target version)
|
|
536
|
+
# Pass console to child installer for consistent output
|
|
537
|
+
uninstall_installer = LemonadeInstaller(
|
|
538
|
+
target_version=installed_version, console=self.console
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Download the MSI matching the installed version
|
|
542
|
+
self._print_status(f"Downloading MSI v{installed_version} for uninstall...")
|
|
543
|
+
msi_path = uninstall_installer.download_installer()
|
|
544
|
+
|
|
545
|
+
cmd = ["msiexec", "/x", str(msi_path)]
|
|
546
|
+
|
|
547
|
+
if silent:
|
|
548
|
+
cmd.extend(["/qn", "/norestart"])
|
|
549
|
+
|
|
550
|
+
log.debug(f"Running: {' '.join(cmd)}")
|
|
551
|
+
|
|
552
|
+
result = subprocess.run(
|
|
553
|
+
cmd,
|
|
554
|
+
capture_output=True,
|
|
555
|
+
text=True,
|
|
556
|
+
timeout=300,
|
|
557
|
+
check=False,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
if result.returncode == 0:
|
|
561
|
+
return InstallResult(
|
|
562
|
+
success=True,
|
|
563
|
+
message="Lemonade Server uninstalled successfully",
|
|
564
|
+
)
|
|
565
|
+
elif result.returncode == 1605:
|
|
566
|
+
return InstallResult(
|
|
567
|
+
success=False, error="Lemonade Server is not installed"
|
|
568
|
+
)
|
|
569
|
+
else:
|
|
570
|
+
return InstallResult(
|
|
571
|
+
success=False,
|
|
572
|
+
error=f"msiexec failed with code {result.returncode}: {result.stderr}",
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
except subprocess.TimeoutExpired:
|
|
576
|
+
return InstallResult(success=False, error="Uninstall timed out")
|
|
577
|
+
except FileNotFoundError:
|
|
578
|
+
return InstallResult(success=False, error="msiexec not found")
|
|
579
|
+
except Exception as e:
|
|
580
|
+
return InstallResult(success=False, error=str(e))
|
|
581
|
+
|
|
582
|
+
def _uninstall_linux(self) -> InstallResult:
|
|
583
|
+
"""Uninstall on Linux using dpkg."""
|
|
584
|
+
try:
|
|
585
|
+
# Check if we have root access
|
|
586
|
+
is_root = False
|
|
587
|
+
if hasattr(os, "geteuid"):
|
|
588
|
+
is_root = os.geteuid() == 0
|
|
589
|
+
|
|
590
|
+
if not is_root:
|
|
591
|
+
cmd = ["sudo", "dpkg", "-r", "lemonade"]
|
|
592
|
+
else:
|
|
593
|
+
cmd = ["dpkg", "-r", "lemonade"]
|
|
594
|
+
|
|
595
|
+
log.debug(f"Running: {' '.join(cmd)}")
|
|
596
|
+
|
|
597
|
+
result = subprocess.run(
|
|
598
|
+
cmd, capture_output=True, text=True, timeout=300, check=False
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
if result.returncode == 0:
|
|
602
|
+
return InstallResult(
|
|
603
|
+
success=True,
|
|
604
|
+
message="Lemonade Server uninstalled successfully",
|
|
605
|
+
)
|
|
606
|
+
else:
|
|
607
|
+
return InstallResult(
|
|
608
|
+
success=False,
|
|
609
|
+
error=f"dpkg failed: {result.stderr}",
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
except subprocess.TimeoutExpired:
|
|
613
|
+
return InstallResult(success=False, error="Uninstall timed out")
|
|
614
|
+
except FileNotFoundError as e:
|
|
615
|
+
return InstallResult(
|
|
616
|
+
success=False, error=f"Required command not found: {e}"
|
|
617
|
+
)
|
|
618
|
+
except Exception as e:
|
|
619
|
+
return InstallResult(success=False, error=str(e))
|
gaia/llm/__init__.py
CHANGED
|
@@ -5,5 +5,6 @@
|
|
|
5
5
|
from .base_client import LLMClient
|
|
6
6
|
from .exceptions import NotSupportedError
|
|
7
7
|
from .factory import create_client
|
|
8
|
+
from .vlm_client import VLMClient
|
|
8
9
|
|
|
9
|
-
__all__ = ["create_client", "LLMClient", "NotSupportedError"]
|
|
10
|
+
__all__ = ["create_client", "LLMClient", "VLMClient", "NotSupportedError"]
|