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.
gaia/cli.py CHANGED
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
 
12
12
  from dotenv import load_dotenv
13
13
 
14
+ from gaia.agents.base.console import AgentConsole
14
15
  from gaia.llm import create_client
15
16
  from gaia.llm.lemonade_client import (
16
17
  DEFAULT_HOST,
@@ -55,25 +56,6 @@ DEFAULT_EXPERIMENTS_DIR = "output/experiments"
55
56
  DEFAULT_EVALUATIONS_DIR = "output/evaluations"
56
57
 
57
58
 
58
- # Helper functions for download progress display
59
- def _format_bytes(b: int) -> str:
60
- """Format bytes to human readable string."""
61
- if b >= 1024 * 1024 * 1024:
62
- return f"{b / (1024 * 1024 * 1024):.1f} GB"
63
- elif b >= 1024 * 1024:
64
- return f"{b / (1024 * 1024):.1f} MB"
65
- elif b >= 1024:
66
- return f"{b / 1024:.1f} KB"
67
- return f"{b} B"
68
-
69
-
70
- def _make_progress_bar(percent: int, width: int = 20) -> str:
71
- """Create a progress bar string."""
72
- filled = int(width * percent / 100)
73
- empty = width - filled
74
- return f"[{'█' * filled}{'░' * empty}]"
75
-
76
-
77
59
  def check_lemonade_health(host=None, port=None):
78
60
  """Check if Lemonade server is running and healthy using LemonadeClient."""
79
61
  log = get_logger(__name__)
@@ -189,7 +171,7 @@ def ensure_agent_models(
189
171
  host: str = DEFAULT_HOST,
190
172
  port: int = DEFAULT_PORT,
191
173
  quiet: bool = False,
192
- timeout: int = 1800,
174
+ _timeout: int = 1800, # Reserved for future use
193
175
  ) -> bool:
194
176
  """
195
177
  Ensure all models required for an agent are downloaded.
@@ -229,86 +211,72 @@ def ensure_agent_models(
229
211
  log.debug(f"All models for {agent} agent already available")
230
212
  return True
231
213
 
214
+ # Use AgentConsole for nicely formatted progress display
215
+ console = AgentConsole()
216
+
232
217
  if not quiet:
233
- print(
234
- f"📥 Downloading {len(models_to_download)} model(s) for {agent} agent..."
218
+ console.print_info(
219
+ f"Downloading {len(models_to_download)} model(s) for {agent} agent"
235
220
  )
236
- print()
237
-
238
- # Progress tracking
239
- last_percent = [-1]
240
- last_file_index = [0]
241
221
 
242
- def progress_callback(event_type: str, data: dict) -> None:
243
- """Display download progress in CLI."""
244
- if quiet:
245
- return
222
+ # Download each model with progress display
223
+ for model_id in models_to_download:
224
+ if not quiet:
225
+ console.print_download_start(model_id)
246
226
 
247
- if event_type == "progress":
248
- percent = data.get("percent", 0)
249
- file_name = data.get("file", "unknown")
250
- file_index = data.get("file_index", 1)
251
- total_files = data.get("total_files", 1)
252
-
253
- # Print newline when moving to a new file
254
- if file_index != last_file_index[0] and last_file_index[0] > 0:
255
- print() # Newline for previous file
256
- last_file_index[0] = file_index
257
-
258
- # Update every 2% for smooth progress
259
- if percent >= last_percent[0] + 2 or percent == 0 or percent == 100:
260
- bytes_downloaded = data.get("bytes_downloaded", 0)
261
- bytes_total = data.get("bytes_total", 0)
262
-
263
- # Create progress bar
264
- bar = _make_progress_bar(percent)
265
- progress_line = (
266
- f" {bar} {percent:3d}% "
267
- f"[{file_index}/{total_files}] {file_name}: "
268
- f"{_format_bytes(bytes_downloaded)}/{_format_bytes(bytes_total)}"
269
- )
270
- print(f"\r{progress_line:<100}", end="", flush=True)
271
- last_percent[0] = percent
227
+ try:
228
+ event_count = 0
229
+ last_bytes = 0
230
+ last_time = time.time()
272
231
 
273
- elif event_type == "complete":
274
- print() # Newline after progress
275
- print(" ✅ Download complete")
276
- last_percent[0] = -1
277
- last_file_index[0] = 0
232
+ for event in client.pull_model_stream(model_name=model_id):
233
+ event_count += 1
234
+ event_type = event.get("event")
278
235
 
279
- elif event_type == "error":
280
- print() # Newline after progress
281
- error_msg = data.get("error", "Unknown error")
282
- print(f" ❌ Error: {error_msg}")
236
+ if event_type == "progress":
237
+ # Skip first 2 spurious events from Lemonade
238
+ if event_count <= 2 or quiet:
239
+ continue
283
240
 
284
- # Download each model
285
- for model_id in models_to_download:
286
- last_percent[0] = -1
241
+ # Calculate download speed
242
+ current_bytes = event.get("bytes_downloaded", 0)
243
+ current_time = time.time()
244
+ time_delta = current_time - last_time
245
+
246
+ speed_mbps = 0.0
247
+ if time_delta > 0.1 and current_bytes > last_bytes:
248
+ bytes_delta = current_bytes - last_bytes
249
+ speed_mbps = (bytes_delta / time_delta) / (1024 * 1024)
250
+ last_bytes = current_bytes
251
+ last_time = current_time
252
+
253
+ console.print_download_progress(
254
+ percent=event.get("percent", 0),
255
+ bytes_downloaded=current_bytes,
256
+ bytes_total=event.get("bytes_total", 0),
257
+ speed_mbps=speed_mbps,
258
+ )
287
259
 
288
- if not quiet:
289
- print(f"📥 {model_id}")
260
+ elif event_type == "complete":
261
+ if not quiet:
262
+ console.print_download_complete(model_id)
290
263
 
291
- try:
292
- for event in client.pull_model_stream(
293
- model_name=model_id,
294
- timeout=timeout,
295
- progress_callback=progress_callback,
296
- ):
297
- if event.get("event") == "error":
264
+ elif event_type == "error":
265
+ if not quiet:
266
+ console.print_download_error(
267
+ event.get("error", "Unknown error"), model_id
268
+ )
298
269
  log.error(f"Failed to download {model_id}")
299
270
  return False
271
+
300
272
  except LemonadeClientError as e:
301
273
  log.error(f"Failed to download {model_id}: {e}")
302
274
  if not quiet:
303
- print(f" ❌ Failed: {e}")
275
+ console.print_download_error(str(e), model_id)
304
276
  return False
305
277
 
306
- if not quiet:
307
- print()
308
-
309
278
  if not quiet:
310
- print(f"All models ready for {agent} agent")
311
- print()
279
+ console.print_success(f"All models ready for {agent} agent")
312
280
 
313
281
  return True
314
282
 
@@ -523,14 +491,30 @@ async def async_main(action, **kwargs):
523
491
  # Create client for actions that use GaiaCliClient (not chat - it uses ChatAgent)
524
492
  client = None
525
493
  if action in ["prompt", "stats"]:
526
- # Filter out audio-related parameters that are no longer part of GaiaCliClient
494
+ # Filter out parameters that are not accepted by GaiaCliClient
495
+ # GaiaCliClient only accepts: model, max_tokens, show_stats, logging_level
527
496
  audio_params = {
528
497
  "whisper_model_size",
529
498
  "audio_device_index",
530
499
  "silence_threshold",
531
500
  "no_tts",
532
501
  }
533
- excluded_params = {"message", "stats", "assistant_name"} | audio_params
502
+ llm_provider_params = {
503
+ "use_claude",
504
+ "use_chatgpt",
505
+ "claude_model",
506
+ "base_url",
507
+ }
508
+ cli_params = {
509
+ "action",
510
+ "message",
511
+ "stats",
512
+ "assistant_name",
513
+ "stream",
514
+ "no_lemonade_check",
515
+ "list_tools",
516
+ }
517
+ excluded_params = cli_params | audio_params | llm_provider_params
534
518
  client_params = {k: v for k, v in kwargs.items() if k not in excluded_params}
535
519
  client = GaiaCliClient(**client_params)
536
520
 
@@ -1117,82 +1101,6 @@ def main():
1117
1101
  )
1118
1102
  api_parser.set_defaults(action="api")
1119
1103
 
1120
- # Add model pull command
1121
- pull_parser = subparsers.add_parser(
1122
- "pull",
1123
- help="Download/install a model from the Lemonade Server registry",
1124
- parents=[parent_parser],
1125
- formatter_class=argparse.RawDescriptionHelpFormatter,
1126
- epilog="""
1127
- Examples:
1128
- # Pull a registered model
1129
- gaia pull Qwen3-0.6B-GGUF
1130
-
1131
- # Pull and register a custom model from HuggingFace
1132
- gaia pull user.Custom-Model-GGUF --checkpoint unsloth/Custom-Model-GGUF:Q4_K_M --recipe llamacpp
1133
-
1134
- # Pull a reasoning model
1135
- gaia pull user.DeepSeek-GGUF --checkpoint unsloth/DeepSeek-R1-GGUF --recipe llamacpp --reasoning
1136
-
1137
- # Pull a vision model with mmproj
1138
- gaia pull user.Vision-Model --checkpoint model/vision:Q4 --recipe llamacpp --vision --mmproj mmproj.gguf
1139
- """,
1140
- )
1141
- pull_parser.add_argument(
1142
- "model_name",
1143
- help="Name of the model to pull (use 'user.' prefix for custom models)",
1144
- )
1145
- pull_parser.add_argument(
1146
- "--checkpoint",
1147
- help="HuggingFace checkpoint for custom models (e.g., unsloth/Model-GGUF:Q4_K_M)",
1148
- )
1149
- pull_parser.add_argument(
1150
- "--recipe",
1151
- help="Lemonade recipe for custom models (e.g., llamacpp, oga-cpu)",
1152
- )
1153
- pull_parser.add_argument(
1154
- "--reasoning",
1155
- action="store_true",
1156
- help="Mark model as a reasoning model (like DeepSeek)",
1157
- )
1158
- pull_parser.add_argument(
1159
- "--vision",
1160
- action="store_true",
1161
- help="Mark model as having vision capabilities",
1162
- )
1163
- pull_parser.add_argument(
1164
- "--embedding",
1165
- action="store_true",
1166
- help="Mark model as an embedding model",
1167
- )
1168
- pull_parser.add_argument(
1169
- "--reranking",
1170
- action="store_true",
1171
- help="Mark model as a reranking model",
1172
- )
1173
- pull_parser.add_argument(
1174
- "--mmproj",
1175
- help="Multimodal projector file for vision models",
1176
- )
1177
- pull_parser.add_argument(
1178
- "--timeout",
1179
- type=int,
1180
- default=1200,
1181
- help="Timeout in seconds for model download (default: 1200)",
1182
- )
1183
- pull_parser.add_argument(
1184
- "--host",
1185
- default="localhost",
1186
- help="Lemonade server host (default: localhost)",
1187
- )
1188
- pull_parser.add_argument(
1189
- "--port",
1190
- type=int,
1191
- default=8000,
1192
- help="Lemonade server port (default: 8000)",
1193
- )
1194
- pull_parser.set_defaults(action="pull")
1195
-
1196
1104
  # Add model download command
1197
1105
  download_parser = subparsers.add_parser(
1198
1106
  "download",
@@ -1330,7 +1238,12 @@ Available agents: chat, code, talk, rag, blender, jira, docker, vlm, minimal, mc
1330
1238
  "kill", help="Kill process running on specific port", parents=[parent_parser]
1331
1239
  )
1332
1240
  kill_parser.add_argument(
1333
- "--port", type=int, required=True, help="Port number to kill process on"
1241
+ "--port", type=int, default=None, help="Port number to kill process on"
1242
+ )
1243
+ kill_parser.add_argument(
1244
+ "--lemonade",
1245
+ action="store_true",
1246
+ help="Kill Lemonade server (port 8000)",
1334
1247
  )
1335
1248
 
1336
1249
  # Add LLM app command
@@ -2111,6 +2024,102 @@ Examples:
2111
2024
  "--all", action="store_true", help="Clear all caches"
2112
2025
  )
2113
2026
 
2027
+ # Init command (one-stop GAIA setup)
2028
+ init_parser = subparsers.add_parser(
2029
+ "init",
2030
+ help="Initialize GAIA: install Lemonade and download models",
2031
+ parents=[parent_parser],
2032
+ )
2033
+ init_parser.add_argument(
2034
+ "--profile",
2035
+ "-p",
2036
+ default="chat",
2037
+ choices=["minimal", "chat", "code", "rag", "all"],
2038
+ help="Profile to initialize: minimal, chat, code, rag, all (default: chat)",
2039
+ )
2040
+ init_parser.add_argument(
2041
+ "--minimal",
2042
+ action="store_true",
2043
+ help="Use minimal profile (~2.5 GB) - shortcut for --profile minimal",
2044
+ )
2045
+ init_parser.add_argument(
2046
+ "--skip-models",
2047
+ action="store_true",
2048
+ help="Skip model downloads (only install Lemonade)",
2049
+ )
2050
+ init_parser.add_argument(
2051
+ "--force-reinstall",
2052
+ action="store_true",
2053
+ help="Force reinstall even if compatible version exists",
2054
+ )
2055
+ init_parser.add_argument(
2056
+ "--force-models",
2057
+ action="store_true",
2058
+ help="Force re-download models (deletes then re-downloads each model)",
2059
+ )
2060
+ init_parser.add_argument(
2061
+ "--yes",
2062
+ "-y",
2063
+ action="store_true",
2064
+ help="Skip confirmation prompts (non-interactive)",
2065
+ )
2066
+ init_parser.add_argument(
2067
+ "--verbose",
2068
+ action="store_true",
2069
+ help="Enable verbose output",
2070
+ )
2071
+ init_parser.add_argument(
2072
+ "--remote",
2073
+ action="store_true",
2074
+ help="Lemonade is hosted on a remote machine (skip local server start, still check version)",
2075
+ )
2076
+
2077
+ # Install command (install specific components)
2078
+ install_parser = subparsers.add_parser(
2079
+ "install",
2080
+ help="Install GAIA components",
2081
+ parents=[parent_parser],
2082
+ )
2083
+ install_parser.add_argument(
2084
+ "--lemonade",
2085
+ action="store_true",
2086
+ help="Install Lemonade Server",
2087
+ )
2088
+ install_parser.add_argument(
2089
+ "--yes",
2090
+ "-y",
2091
+ action="store_true",
2092
+ help="Skip confirmation prompts",
2093
+ )
2094
+ install_parser.add_argument(
2095
+ "--silent",
2096
+ action="store_true",
2097
+ help="Silent installation (no UI, no desktop shortcuts)",
2098
+ )
2099
+
2100
+ # Uninstall command (uninstall specific components)
2101
+ uninstall_parser = subparsers.add_parser(
2102
+ "uninstall",
2103
+ help="Uninstall GAIA components",
2104
+ parents=[parent_parser],
2105
+ )
2106
+ uninstall_parser.add_argument(
2107
+ "--lemonade",
2108
+ action="store_true",
2109
+ help="Uninstall Lemonade Server",
2110
+ )
2111
+ uninstall_parser.add_argument(
2112
+ "--models",
2113
+ action="store_true",
2114
+ help="Clear all downloaded models (frees disk space)",
2115
+ )
2116
+ uninstall_parser.add_argument(
2117
+ "--yes",
2118
+ "-y",
2119
+ action="store_true",
2120
+ help="Skip confirmation prompts",
2121
+ )
2122
+
2114
2123
  args = parser.parse_args()
2115
2124
 
2116
2125
  # Check if action is specified
@@ -2664,9 +2673,7 @@ Examples:
2664
2673
  print(f"❌ Error: Failed to initialize TTS: {e}")
2665
2674
  return
2666
2675
 
2667
- test_text = (
2668
- args.test_text
2669
- or """
2676
+ test_text = args.test_text or """
2670
2677
  Let's play a game of trivia. I'll ask you a series of questions on a particular topic,
2671
2678
  and you try to answer them to the best of your ability.
2672
2679
 
@@ -2682,7 +2689,6 @@ E) Edgar Allan Poe
2682
2689
 
2683
2690
  Let me know your answer!
2684
2691
  """
2685
- )
2686
2692
 
2687
2693
  if args.test_type == "tts-preprocessing":
2688
2694
  tts.test_preprocessing(test_text)
@@ -2796,106 +2802,48 @@ Let me know your answer!
2796
2802
 
2797
2803
  # Handle kill command
2798
2804
  if args.action == "kill":
2799
- log.info(f"Attempting to kill process on port {args.port}")
2800
- result = kill_process_by_port(args.port)
2801
- if result["success"]:
2802
- print(f"✅ {result['message']}")
2805
+ if args.lemonade:
2806
+ # Use lemonade-server stop for graceful shutdown
2807
+ try:
2808
+ result = subprocess.run(
2809
+ ["lemonade-server", "stop"],
2810
+ capture_output=True,
2811
+ text=True,
2812
+ check=False,
2813
+ )
2814
+ if result.returncode == 0:
2815
+ print("✅ Lemonade server stopped")
2816
+ else:
2817
+ # Fallback to port kill if stop command fails
2818
+ log.warning(f"lemonade-server stop failed: {result.stderr}")
2819
+ port_result = kill_process_by_port(8000)
2820
+ if port_result["success"]:
2821
+ print(f"✅ {port_result['message']}")
2822
+ else:
2823
+ print(f"❌ {port_result['message']}")
2824
+ except FileNotFoundError:
2825
+ # lemonade-server not in PATH, fallback to port kill
2826
+ log.warning("lemonade-server not found, falling back to port kill")
2827
+ port_result = kill_process_by_port(8000)
2828
+ if port_result["success"]:
2829
+ print(f"✅ {port_result['message']}")
2830
+ else:
2831
+ print(f"❌ {port_result['message']}")
2832
+ elif args.port:
2833
+ port = args.port
2834
+ log.info(f"Attempting to kill process on port {port}")
2835
+ result = kill_process_by_port(port)
2836
+ if result["success"]:
2837
+ print(f"✅ {result['message']}")
2838
+ else:
2839
+ print(f"❌ {result['message']}")
2803
2840
  else:
2804
- print(f"❌ {result['message']}")
2841
+ print("❌ Specify --lemonade or --port <number>")
2805
2842
  return
2806
2843
 
2807
2844
  # Import LemonadeManager for model commands error handling
2808
2845
  from gaia.llm.lemonade_manager import LemonadeManager
2809
2846
 
2810
- # Handle model pull command
2811
- if args.action == "pull":
2812
- log.info(f"Pulling model: {args.model_name}")
2813
- verbose = getattr(args, "verbose", False)
2814
- try:
2815
- client = LemonadeClient(host=args.host, port=args.port, verbose=verbose)
2816
-
2817
- # Check if Lemonade server is running
2818
- if not check_lemonade_health(args.host, args.port):
2819
- LemonadeManager.print_server_error()
2820
- return
2821
-
2822
- print(f"📥 Pulling model: {args.model_name}")
2823
-
2824
- # Define a CLI progress callback for real-time updates
2825
- last_percent = [-1] # Use list to allow mutation in closure
2826
- last_file_index = [0]
2827
-
2828
- def cli_progress_callback(event_type: str, data: dict) -> None:
2829
- """Display download progress in CLI."""
2830
- if event_type == "progress":
2831
- percent = data.get("percent", 0)
2832
- file_name = data.get("file", "unknown")
2833
- file_index = data.get("file_index", 1)
2834
- total_files = data.get("total_files", 1)
2835
-
2836
- # Print newline when moving to a new file
2837
- if file_index != last_file_index[0] and last_file_index[0] > 0:
2838
- print() # Newline for previous file
2839
- last_file_index[0] = file_index
2840
-
2841
- # Update every 2% for smooth progress
2842
- if percent >= last_percent[0] + 2 or percent == 0 or percent == 100:
2843
- bytes_downloaded = data.get("bytes_downloaded", 0)
2844
- bytes_total = data.get("bytes_total", 0)
2845
-
2846
- # Create progress bar
2847
- bar = _make_progress_bar(percent)
2848
- progress_line = (
2849
- f" {bar} {percent:3d}% "
2850
- f"[{file_index}/{total_files}] {file_name}: "
2851
- f"{_format_bytes(bytes_downloaded)}/{_format_bytes(bytes_total)}"
2852
- )
2853
- print(f"\r{progress_line:<100}", end="", flush=True)
2854
- last_percent[0] = percent
2855
-
2856
- elif event_type == "complete":
2857
- print() # Newline after progress
2858
- print(f"✅ Model downloaded successfully: {args.model_name}")
2859
-
2860
- elif event_type == "error":
2861
- print() # Newline after progress
2862
- error_msg = data.get("error", "Unknown error")
2863
- print(f"❌ Download failed: {error_msg}")
2864
-
2865
- # Use streaming pull with progress callback
2866
- completed = False
2867
- for event in client.pull_model_stream(
2868
- model_name=args.model_name,
2869
- checkpoint=getattr(args, "checkpoint", None),
2870
- recipe=getattr(args, "recipe", None),
2871
- reasoning=getattr(args, "reasoning", False) or None,
2872
- vision=getattr(args, "vision", False) or None,
2873
- embedding=getattr(args, "embedding", False) or None,
2874
- reranking=getattr(args, "reranking", False) or None,
2875
- mmproj=getattr(args, "mmproj", None),
2876
- timeout=args.timeout,
2877
- progress_callback=cli_progress_callback,
2878
- ):
2879
- if event.get("event") == "complete":
2880
- completed = True
2881
- elif event.get("event") == "error":
2882
- sys.exit(1)
2883
-
2884
- if not completed:
2885
- print("⚠️ Model pull completed without explicit complete event")
2886
-
2887
- except LemonadeClientError as e:
2888
- print(f"❌ Error pulling model: {e}")
2889
- sys.exit(1)
2890
- except Exception as e:
2891
- error_msg = str(e).lower()
2892
- if "connection" in error_msg or "refused" in error_msg:
2893
- LemonadeManager.print_server_error()
2894
- else:
2895
- print(f"❌ Error: {e}")
2896
- sys.exit(1)
2897
- return
2898
-
2899
2847
  # Handle model download command
2900
2848
  if args.action == "download":
2901
2849
  from gaia.llm.lemonade_client import AGENT_PROFILES, MODELS
@@ -3020,122 +2968,110 @@ Let me know your answer!
3020
2968
  agent_name = args.agent.lower()
3021
2969
  model_ids = client.get_required_models(agent_name)
3022
2970
 
2971
+ console = AgentConsole()
2972
+
3023
2973
  if not model_ids:
3024
2974
  if agent_name != "all":
3025
2975
  profile = client.get_agent_profile(agent_name)
3026
2976
  if not profile:
3027
- print(f"Unknown agent: {agent_name}")
3028
- print(f" Available: {', '.join(client.list_agents())}")
2977
+ console.print_error(f"Unknown agent: {agent_name}")
2978
+ console.print_info(
2979
+ f"Available: {', '.join(client.list_agents())}"
2980
+ )
3029
2981
  sys.exit(1)
3030
- print(f"📦 No models to download for '{agent_name}'")
2982
+ console.print_info(f"No models to download for '{agent_name}'")
3031
2983
  return
3032
2984
 
3033
- print(f"📥 Downloading {len(model_ids)} model(s) for '{agent_name}'...")
3034
- print()
3035
-
3036
- # Track progress per model
3037
- current_model = [None]
3038
- last_percent = [-1]
3039
- last_file_index = [0]
3040
-
3041
- def download_progress_callback(event_type: str, data: dict) -> None:
3042
- """Display download progress in CLI."""
3043
- if event_type == "progress":
3044
- percent = data.get("percent", 0)
3045
- file_name = data.get("file", "unknown")
3046
- file_index = data.get("file_index", 1)
3047
- total_files = data.get("total_files", 1)
3048
-
3049
- # Print newline when moving to a new file
3050
- if file_index != last_file_index[0] and last_file_index[0] > 0:
3051
- print() # Newline for previous file
3052
- last_file_index[0] = file_index
3053
-
3054
- # Update every 2% for smooth progress
3055
- if percent >= last_percent[0] + 2 or percent == 0 or percent == 100:
3056
- bytes_downloaded = data.get("bytes_downloaded", 0)
3057
- bytes_total = data.get("bytes_total", 0)
3058
-
3059
- # Create progress bar
3060
- bar = _make_progress_bar(percent)
3061
- progress_line = (
3062
- f" {bar} {percent:3d}% "
3063
- f"[{file_index}/{total_files}] {file_name}: "
3064
- f"{_format_bytes(bytes_downloaded)}/{_format_bytes(bytes_total)}"
3065
- )
3066
- print(f"\r{progress_line:<100}", end="", flush=True)
3067
- last_percent[0] = percent
3068
-
3069
- elif event_type == "complete":
3070
- print() # Newline after progress
3071
- print(" ✅ Download complete")
3072
- last_percent[0] = -1 # Reset for next model
3073
- last_file_index[0] = 0
3074
-
3075
- elif event_type == "error":
3076
- print() # Newline after progress
3077
- error_msg = data.get("error", "Unknown error")
3078
- print(f" ❌ Error: {error_msg}")
2985
+ console.print_info(
2986
+ f"Downloading {len(model_ids)} model(s) for '{agent_name}'"
2987
+ )
3079
2988
 
3080
- # Download each model
2989
+ # Download each model with progress display
3081
2990
  success_count = 0
3082
2991
  skip_count = 0
3083
2992
  fail_count = 0
3084
2993
 
3085
2994
  for model_id in model_ids:
3086
- current_model[0] = model_id
3087
- last_percent[0] = -1
3088
-
3089
2995
  # Check if already available
3090
2996
  if client.check_model_available(model_id):
3091
- print(f"✅ {model_id} (already downloaded)")
2997
+ console.print_download_skipped(model_id)
3092
2998
  skip_count += 1
3093
2999
  continue
3094
3000
 
3095
- print(f"📥 {model_id}")
3001
+ console.print_download_start(model_id)
3096
3002
 
3097
3003
  try:
3098
3004
  completed = False
3099
- for event in client.pull_model_stream(
3100
- model_name=model_id,
3101
- timeout=args.timeout,
3102
- progress_callback=download_progress_callback,
3103
- ):
3104
- if event.get("event") == "complete":
3005
+ event_count = 0
3006
+ last_bytes = 0
3007
+ last_time = time.time()
3008
+
3009
+ for event in client.pull_model_stream(model_name=model_id):
3010
+ event_count += 1
3011
+ event_type = event.get("event")
3012
+
3013
+ if event_type == "progress":
3014
+ # Skip first 2 spurious events from Lemonade
3015
+ if event_count <= 2:
3016
+ continue
3017
+
3018
+ # Calculate download speed
3019
+ current_bytes = event.get("bytes_downloaded", 0)
3020
+ current_time = time.time()
3021
+ time_delta = current_time - last_time
3022
+
3023
+ speed_mbps = 0.0
3024
+ if time_delta > 0.1 and current_bytes > last_bytes:
3025
+ bytes_delta = current_bytes - last_bytes
3026
+ speed_mbps = (bytes_delta / time_delta) / (1024 * 1024)
3027
+ last_bytes = current_bytes
3028
+ last_time = current_time
3029
+
3030
+ console.print_download_progress(
3031
+ percent=event.get("percent", 0),
3032
+ bytes_downloaded=current_bytes,
3033
+ bytes_total=event.get("bytes_total", 0),
3034
+ speed_mbps=speed_mbps,
3035
+ )
3036
+
3037
+ elif event_type == "complete":
3038
+ console.print_download_complete(model_id)
3105
3039
  completed = True
3106
- elif event.get("event") == "error":
3040
+
3041
+ elif event_type == "error":
3042
+ console.print_download_error(
3043
+ event.get("error", "Unknown error"), model_id
3044
+ )
3107
3045
  fail_count += 1
3108
3046
  break
3109
3047
 
3110
3048
  if completed:
3111
3049
  success_count += 1
3112
3050
  except LemonadeClientError as e:
3113
- print(f" ❌ Failed: {e}")
3051
+ console.print_download_error(str(e), model_id)
3114
3052
  fail_count += 1
3115
3053
 
3116
- print()
3117
-
3118
3054
  # Summary
3119
- print("=" * 50)
3120
- print("📊 Download Summary:")
3121
- print(f"Downloaded: {success_count}")
3122
- print(f" ⏭️ Skipped (already available): {skip_count}")
3055
+ console.print_info("=" * 50)
3056
+ console.print_info("Download Summary:")
3057
+ console.print_success(f"Downloaded: {success_count}")
3058
+ console.print_info(f"Skipped (already available): {skip_count}")
3123
3059
  if fail_count > 0:
3124
- print(f"Failed: {fail_count}")
3125
- print("=" * 50)
3060
+ console.print_error(f"Failed: {fail_count}")
3061
+ console.print_info("=" * 50)
3126
3062
 
3127
3063
  if fail_count > 0:
3128
3064
  sys.exit(1)
3129
3065
 
3130
3066
  except LemonadeClientError as e:
3131
- print(f"❌ Error: {e}")
3067
+ console.print_error(str(e))
3132
3068
  sys.exit(1)
3133
3069
  except Exception as e:
3134
3070
  error_msg = str(e).lower()
3135
3071
  if "connection" in error_msg or "refused" in error_msg:
3136
3072
  LemonadeManager.print_server_error()
3137
3073
  else:
3138
- print(f"❌ Error: {e}")
3074
+ console.print_error(str(e))
3139
3075
  sys.exit(1)
3140
3076
  return
3141
3077
 
@@ -4091,6 +4027,248 @@ Let me know your answer!
4091
4027
  handle_visualize_command(args)
4092
4028
  return
4093
4029
 
4030
+ # Handle init command
4031
+ if args.action == "init":
4032
+ from gaia.installer.init_command import run_init
4033
+
4034
+ # --minimal flag overrides --profile
4035
+ profile = "minimal" if args.minimal else args.profile
4036
+
4037
+ exit_code = run_init(
4038
+ profile=profile,
4039
+ skip_models=args.skip_models,
4040
+ force_reinstall=args.force_reinstall,
4041
+ force_models=args.force_models,
4042
+ yes=args.yes,
4043
+ verbose=getattr(args, "verbose", False),
4044
+ remote=getattr(args, "remote", False),
4045
+ )
4046
+ sys.exit(exit_code)
4047
+
4048
+ # Handle install command
4049
+ if args.action == "install":
4050
+ if args.lemonade:
4051
+ from gaia.installer.lemonade_installer import LemonadeInstaller
4052
+ from gaia.version import LEMONADE_VERSION
4053
+
4054
+ installer = LemonadeInstaller()
4055
+
4056
+ # Check if already installed
4057
+ info = installer.check_installation()
4058
+ if info.installed and info.version:
4059
+ installed_ver = info.version.lstrip("v")
4060
+ target_ver = LEMONADE_VERSION.lstrip("v")
4061
+
4062
+ if installed_ver == target_ver:
4063
+ print(f"✅ Lemonade Server v{info.version} is already installed")
4064
+ sys.exit(0)
4065
+ else:
4066
+ print(f"Lemonade Server v{info.version} is installed")
4067
+ print(f"GAIA requires v{LEMONADE_VERSION}")
4068
+ print("")
4069
+ print("To update, run:")
4070
+ print(" gaia uninstall --lemonade")
4071
+ print(" gaia install --lemonade")
4072
+ sys.exit(1)
4073
+
4074
+ # Confirm installation
4075
+ if not args.yes:
4076
+ response = input(f"Install Lemonade v{LEMONADE_VERSION}? [Y/n]: ")
4077
+ if response.lower() == "n":
4078
+ print("Installation cancelled")
4079
+ sys.exit(0)
4080
+
4081
+ # Download and install
4082
+ print("Downloading Lemonade Server...")
4083
+ try:
4084
+ installer_path = installer.download_installer()
4085
+ print("Installing...")
4086
+ result = installer.install(installer_path, silent=args.silent)
4087
+
4088
+ if result.success:
4089
+ # Verify installation
4090
+ verify_info = installer.check_installation()
4091
+ if verify_info.installed:
4092
+ print(f"✅ Installed Lemonade Server v{verify_info.version}")
4093
+ else:
4094
+ print(f"✅ Installed Lemonade Server v{result.version}")
4095
+ sys.exit(0)
4096
+ else:
4097
+ print(f"❌ Installation failed: {result.error}")
4098
+ sys.exit(1)
4099
+ except Exception as e:
4100
+ print(f"❌ Installation failed: {e}")
4101
+ sys.exit(1)
4102
+ else:
4103
+ print("Specify what to install: --lemonade")
4104
+ sys.exit(1)
4105
+
4106
+ # Handle uninstall command
4107
+ if args.action == "uninstall":
4108
+ from rich.console import Console
4109
+
4110
+ console = Console()
4111
+
4112
+ # Handle model cache clearing
4113
+ if args.models:
4114
+ import shutil
4115
+
4116
+ try:
4117
+ # Find HuggingFace cache directory
4118
+ hf_cache = Path.home() / ".cache" / "huggingface" / "hub"
4119
+ if sys.platform == "win32":
4120
+ hf_cache = (
4121
+ Path(os.path.expanduser("~")) / ".cache" / "huggingface" / "hub"
4122
+ )
4123
+
4124
+ if not hf_cache.exists():
4125
+ console.print("[yellow]📦 No model cache found[/yellow]")
4126
+ console.print(f" [dim]Checked: {hf_cache}[/dim]")
4127
+ sys.exit(0)
4128
+
4129
+ # Find all model directories
4130
+ model_dirs = list(hf_cache.glob("models--*"))
4131
+ if not model_dirs:
4132
+ console.print("[green]✅ Model cache is already empty[/green]")
4133
+ console.print(f" [dim]Location: {hf_cache}[/dim]")
4134
+ sys.exit(0)
4135
+
4136
+ # Calculate total size
4137
+ total_size = 0
4138
+ for model_dir in model_dirs:
4139
+ try:
4140
+ total_size += sum(
4141
+ f.stat().st_size
4142
+ for f in model_dir.rglob("*")
4143
+ if f.is_file()
4144
+ )
4145
+ except Exception:
4146
+ pass
4147
+
4148
+ size_gb = total_size / (1024**3)
4149
+
4150
+ # Show what will be deleted
4151
+ console.print()
4152
+ console.print(f"[bold]Found {len(model_dirs)} model(s) in cache[/bold]")
4153
+ console.print(f" [dim]Location: {hf_cache}[/dim]")
4154
+ console.print(f" [dim]Total size: ~{size_gb:.1f} GB[/dim]")
4155
+ console.print()
4156
+
4157
+ # Confirm deletion
4158
+ if not args.yes:
4159
+ console.print(
4160
+ f"[bold]Delete all {len(model_dirs)} model(s)?[/bold] [dim](frees ~{size_gb:.1f} GB)[/dim]"
4161
+ )
4162
+ console.print()
4163
+ console.print(" [y/N]: ", end="")
4164
+ response = input()
4165
+ if response.lower() != "y":
4166
+ console.print("[dim]Model deletion cancelled[/dim]")
4167
+ sys.exit(0)
4168
+
4169
+ console.print()
4170
+ console.print(
4171
+ f"[bold blue]Deleting {len(model_dirs)} model(s)...[/bold blue]"
4172
+ )
4173
+ console.print()
4174
+
4175
+ success_count = 0
4176
+ fail_count = 0
4177
+
4178
+ for model_dir in model_dirs:
4179
+ # Extract model name from directory
4180
+ model_name = model_dir.name.replace("models--", "").replace(
4181
+ "--", "/"
4182
+ )
4183
+ console.print(f" [cyan]{model_name}[/cyan]... ", end="")
4184
+ try:
4185
+ shutil.rmtree(model_dir)
4186
+ console.print("[green]✅[/green]")
4187
+ success_count += 1
4188
+ except PermissionError:
4189
+ console.print("[red]❌ (locked)[/red]")
4190
+ fail_count += 1
4191
+ except Exception as e:
4192
+ console.print(f"[red]❌ ({e})[/red]")
4193
+ fail_count += 1
4194
+
4195
+ # Summary
4196
+ console.print()
4197
+ if success_count > 0:
4198
+ console.print(f"[green]✅ Deleted {success_count} model(s)[/green]")
4199
+ if fail_count > 0:
4200
+ console.print(
4201
+ f"[red]❌ Failed to delete {fail_count} model(s)[/red]"
4202
+ )
4203
+ console.print()
4204
+ console.print(" [bold]If files are locked:[/bold]")
4205
+ console.print(
4206
+ " [dim]1. Close all apps using models (gaia chat, etc.)[/dim]"
4207
+ )
4208
+ console.print(
4209
+ " [dim]2. Stop Lemonade:[/dim] [cyan]gaia kill --lemonade[/cyan]"
4210
+ )
4211
+ console.print(
4212
+ " [dim]3. Re-run:[/dim] [cyan]gaia uninstall --models[/cyan]"
4213
+ )
4214
+
4215
+ sys.exit(0 if fail_count == 0 else 1)
4216
+
4217
+ except Exception as e:
4218
+ console.print(f"[red]❌ Error: {e}[/red]")
4219
+ sys.exit(1)
4220
+
4221
+ # Handle Lemonade Server uninstallation
4222
+ elif args.lemonade:
4223
+ from gaia.installer.lemonade_installer import LemonadeInstaller
4224
+
4225
+ installer = LemonadeInstaller(console=console)
4226
+
4227
+ # Check if installed
4228
+ info = installer.check_installation()
4229
+ if not info.installed:
4230
+ console.print("[green]✅ Lemonade Server is not installed[/green]")
4231
+ sys.exit(0)
4232
+
4233
+ # Show installation details
4234
+ console.print()
4235
+ console.print(f"[bold]Found Lemonade Server v{info.version}[/bold]")
4236
+ if info.path:
4237
+ console.print(f" [dim]Location: {info.path}[/dim]")
4238
+ console.print()
4239
+
4240
+ # Confirm uninstallation
4241
+ if not args.yes:
4242
+ console.print(
4243
+ f"[bold]Uninstall Lemonade Server v{info.version}?[/bold] \\[y/N]: ",
4244
+ end="",
4245
+ )
4246
+ response = input()
4247
+ if response.lower() != "y":
4248
+ console.print("[dim]Uninstall cancelled[/dim]")
4249
+ sys.exit(0)
4250
+
4251
+ # Uninstall
4252
+ console.print()
4253
+ console.print("[bold blue]Uninstalling Lemonade Server...[/bold blue]")
4254
+ result = installer.uninstall(silent=True)
4255
+
4256
+ if result.success:
4257
+ console.print()
4258
+ console.print(
4259
+ "[green]✅ Lemonade Server uninstalled successfully[/green]"
4260
+ )
4261
+ sys.exit(0)
4262
+ else:
4263
+ console.print()
4264
+ console.print(f"[red]❌ Uninstall failed: {result.error}[/red]")
4265
+ sys.exit(1)
4266
+ else:
4267
+ console.print("[yellow]Specify what to uninstall:[/yellow]")
4268
+ console.print(" [cyan]--lemonade[/cyan] Uninstall Lemonade Server")
4269
+ console.print(" [cyan]--models[/cyan] Clear all downloaded models")
4270
+ sys.exit(1)
4271
+
4094
4272
  # Log error for unknown action
4095
4273
  log.error(f"Unknown action specified: {args.action}")
4096
4274
  parser.print_help()