amd-gaia 0.15.1__py3-none-any.whl → 0.15.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/METADATA +2 -2
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/RECORD +38 -32
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/WHEEL +1 -1
- gaia/agents/base/agent.py +317 -113
- gaia/agents/base/api_agent.py +0 -1
- gaia/agents/base/console.py +334 -9
- 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/agents/tools/__init__.py +11 -0
- gaia/agents/tools/file_tools.py +715 -0
- gaia/apps/llm/app.py +14 -3
- gaia/chat/app.py +2 -4
- gaia/cli.py +751 -333
- gaia/installer/__init__.py +23 -0
- gaia/installer/init_command.py +1605 -0
- gaia/installer/lemonade_installer.py +678 -0
- gaia/llm/__init__.py +2 -1
- gaia/llm/lemonade_client.py +427 -99
- gaia/llm/lemonade_manager.py +55 -11
- gaia/llm/providers/lemonade.py +21 -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.3.dist-info}/entry_points.txt +0 -0
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/licenses/LICENSE.md +0 -0
- {amd_gaia-0.15.1.dist-info → amd_gaia-0.15.3.dist-info}/top_level.txt +0 -0
gaia/agents/base/console.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import subprocess
|
|
5
6
|
import threading
|
|
6
7
|
import time
|
|
7
8
|
from abc import ABC, abstractmethod
|
|
@@ -57,7 +58,7 @@ class OutputHandler(ABC):
|
|
|
57
58
|
# === Core Progress/State Methods (Required) ===
|
|
58
59
|
|
|
59
60
|
@abstractmethod
|
|
60
|
-
def print_processing_start(self, query: str, max_steps: int):
|
|
61
|
+
def print_processing_start(self, query: str, max_steps: int, model_id: str = None):
|
|
61
62
|
"""Print processing start message."""
|
|
62
63
|
...
|
|
63
64
|
|
|
@@ -204,13 +205,14 @@ class OutputHandler(ABC):
|
|
|
204
205
|
|
|
205
206
|
|
|
206
207
|
class ProgressIndicator:
|
|
207
|
-
"""A simple progress indicator that shows a spinner or dots animation."""
|
|
208
|
+
"""A simple progress indicator that shows a spinner or dots animation with elapsed time."""
|
|
208
209
|
|
|
209
|
-
def __init__(self, message="Processing"):
|
|
210
|
+
def __init__(self, message="Processing", show_timer=False):
|
|
210
211
|
"""Initialize the progress indicator.
|
|
211
212
|
|
|
212
213
|
Args:
|
|
213
214
|
message: The message to display before the animation
|
|
215
|
+
show_timer: If True, show elapsed time
|
|
214
216
|
"""
|
|
215
217
|
self.message = message
|
|
216
218
|
self.is_running = False
|
|
@@ -220,6 +222,9 @@ class ProgressIndicator:
|
|
|
220
222
|
self.spinner_idx = 0
|
|
221
223
|
self.dot_idx = 0
|
|
222
224
|
self.rich_spinner = None
|
|
225
|
+
self.show_timer = show_timer
|
|
226
|
+
self.start_time = None
|
|
227
|
+
self._update_timer_thread = None # Timer update thread
|
|
223
228
|
if RICH_AVAILABLE:
|
|
224
229
|
self.rich_spinner = Spinner("dots", text=message)
|
|
225
230
|
self.live = None
|
|
@@ -259,19 +264,23 @@ class ProgressIndicator:
|
|
|
259
264
|
|
|
260
265
|
time.sleep(0.1)
|
|
261
266
|
|
|
262
|
-
def start(self, message=None):
|
|
267
|
+
def start(self, message=None, show_timer=None):
|
|
263
268
|
"""Start the progress indicator.
|
|
264
269
|
|
|
265
270
|
Args:
|
|
266
271
|
message: Optional new message to display
|
|
272
|
+
show_timer: Optional override for showing timer
|
|
267
273
|
"""
|
|
268
274
|
if message:
|
|
269
275
|
self.message = message
|
|
276
|
+
if show_timer is not None:
|
|
277
|
+
self.show_timer = show_timer
|
|
270
278
|
|
|
271
279
|
if self.is_running:
|
|
272
280
|
return
|
|
273
281
|
|
|
274
282
|
self.is_running = True
|
|
283
|
+
self.start_time = time.time()
|
|
275
284
|
|
|
276
285
|
if RICH_AVAILABLE:
|
|
277
286
|
if self.rich_spinner:
|
|
@@ -281,11 +290,31 @@ class ProgressIndicator:
|
|
|
281
290
|
self.rich_spinner, refresh_per_second=10, transient=True
|
|
282
291
|
)
|
|
283
292
|
self.live.start()
|
|
293
|
+
|
|
294
|
+
# Update with timer if enabled
|
|
295
|
+
if self.show_timer:
|
|
296
|
+
self._update_timer_thread = threading.Thread(
|
|
297
|
+
target=self._update_timer
|
|
298
|
+
)
|
|
299
|
+
self._update_timer_thread.daemon = True
|
|
300
|
+
self._update_timer_thread.start()
|
|
284
301
|
else:
|
|
285
302
|
self.thread = threading.Thread(target=self._animate)
|
|
286
303
|
self.thread.daemon = True
|
|
287
304
|
self.thread.start()
|
|
288
305
|
|
|
306
|
+
def _update_timer(self):
|
|
307
|
+
"""Update spinner text with elapsed time."""
|
|
308
|
+
while self.is_running and self.live:
|
|
309
|
+
elapsed = time.time() - self.start_time
|
|
310
|
+
timer_text = f"{self.message} ({int(elapsed)}s)"
|
|
311
|
+
try:
|
|
312
|
+
self.rich_spinner.text = timer_text
|
|
313
|
+
self.live.update(self.rich_spinner)
|
|
314
|
+
time.sleep(1.0) # Update every 1 second
|
|
315
|
+
except Exception:
|
|
316
|
+
break
|
|
317
|
+
|
|
289
318
|
def stop(self):
|
|
290
319
|
"""Stop the progress indicator."""
|
|
291
320
|
if not self.is_running:
|
|
@@ -293,6 +322,13 @@ class ProgressIndicator:
|
|
|
293
322
|
|
|
294
323
|
self.is_running = False
|
|
295
324
|
|
|
325
|
+
# Stop timer thread if running
|
|
326
|
+
if RICH_AVAILABLE and hasattr(self, "_update_timer_thread"):
|
|
327
|
+
try:
|
|
328
|
+
self._update_timer_thread.join(timeout=0.5)
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
|
|
296
332
|
if RICH_AVAILABLE and self.live:
|
|
297
333
|
self.live.stop()
|
|
298
334
|
elif self.thread:
|
|
@@ -404,7 +440,6 @@ class AgentConsole(OutputHandler):
|
|
|
404
440
|
else:
|
|
405
441
|
# Regular JSON output
|
|
406
442
|
# Convert to formatted JSON string with safe fallback for non-serializable types (e.g., numpy.float32)
|
|
407
|
-
print(data)
|
|
408
443
|
try:
|
|
409
444
|
json_str = json.dumps(data, indent=2)
|
|
410
445
|
except TypeError:
|
|
@@ -463,21 +498,28 @@ class AgentConsole(OutputHandler):
|
|
|
463
498
|
print(f"\n⏸️ Paused after step: {description}")
|
|
464
499
|
print("Press Enter to continue, or 'n'/'q' to stop...")
|
|
465
500
|
|
|
466
|
-
def print_processing_start(
|
|
501
|
+
def print_processing_start(
|
|
502
|
+
self, query: str, max_steps: int, model_id: str = None
|
|
503
|
+
) -> None:
|
|
467
504
|
"""
|
|
468
505
|
Print the initial processing message.
|
|
469
506
|
|
|
470
507
|
Args:
|
|
471
508
|
query: The user query being processed
|
|
472
509
|
max_steps: Maximum number of steps allowed (kept for API compatibility)
|
|
510
|
+
model_id: Optional model ID to display
|
|
473
511
|
"""
|
|
474
512
|
if self.rich_available:
|
|
475
513
|
self.console.print(f"\n[bold blue]🤖 Processing:[/bold blue] '{query}'")
|
|
476
514
|
self.console.print("=" * 50)
|
|
515
|
+
if model_id:
|
|
516
|
+
self.console.print(f"[dim]Model: {model_id}[/dim]")
|
|
477
517
|
self.console.print()
|
|
478
518
|
else:
|
|
479
519
|
print(f"\n🤖 Processing: '{query}'")
|
|
480
520
|
print("=" * 50)
|
|
521
|
+
if model_id:
|
|
522
|
+
print(f"Model: {model_id}")
|
|
481
523
|
print()
|
|
482
524
|
|
|
483
525
|
def print_separator(self, length: int = 50) -> None:
|
|
@@ -803,6 +845,162 @@ class AgentConsole(OutputHandler):
|
|
|
803
845
|
else:
|
|
804
846
|
print(f"\n✅ SUCCESS: {message}\n")
|
|
805
847
|
|
|
848
|
+
def print_image(
|
|
849
|
+
self, image_path: str, caption: str = None, prompt_to_open: bool = False
|
|
850
|
+
) -> None:
|
|
851
|
+
"""
|
|
852
|
+
Display an image in terminal and optionally prompt to open in viewer.
|
|
853
|
+
|
|
854
|
+
Args:
|
|
855
|
+
image_path: Path to the image file
|
|
856
|
+
caption: Optional caption to display
|
|
857
|
+
prompt_to_open: If True, prompt user to open in default viewer after display
|
|
858
|
+
"""
|
|
859
|
+
import os
|
|
860
|
+
import sys
|
|
861
|
+
from pathlib import Path
|
|
862
|
+
|
|
863
|
+
path = Path(image_path)
|
|
864
|
+
if not path.exists():
|
|
865
|
+
return
|
|
866
|
+
|
|
867
|
+
if self.rich_available:
|
|
868
|
+
try:
|
|
869
|
+
# Try term-image with Sixel protocol for full resolution
|
|
870
|
+
# Sixel works in: Windows Terminal Preview, iTerm2, Kitty, WezTerm, etc.
|
|
871
|
+
from term_image.image import from_file
|
|
872
|
+
|
|
873
|
+
# Load image with auto-detected best protocol
|
|
874
|
+
img = from_file(str(path))
|
|
875
|
+
|
|
876
|
+
# Try to enable Sixel if supported
|
|
877
|
+
try:
|
|
878
|
+
# Set render method to auto-detect best available (Sixel, Kitty, iTerm2)
|
|
879
|
+
img.set_render_method("auto")
|
|
880
|
+
except Exception:
|
|
881
|
+
pass
|
|
882
|
+
|
|
883
|
+
# Set size maintaining aspect ratio
|
|
884
|
+
# Terminal characters are ~2:1 (height:width), so use fit_to_width
|
|
885
|
+
img.set_size(columns=60, fit_to_width=True)
|
|
886
|
+
|
|
887
|
+
# Render the image
|
|
888
|
+
if caption:
|
|
889
|
+
# Create a panel around the rendered image
|
|
890
|
+
rendered = str(img)
|
|
891
|
+
self.console.print(
|
|
892
|
+
Panel(
|
|
893
|
+
rendered,
|
|
894
|
+
title=f"🖼️ {caption}",
|
|
895
|
+
border_style="cyan",
|
|
896
|
+
padding=(0, 0),
|
|
897
|
+
),
|
|
898
|
+
justify="center",
|
|
899
|
+
)
|
|
900
|
+
else:
|
|
901
|
+
# Print image directly with centering
|
|
902
|
+
# Note: term-image returns ANSI escape codes with actual image data
|
|
903
|
+
print(
|
|
904
|
+
str(img)
|
|
905
|
+
) # Use plain print to avoid Rich interfering with image codes
|
|
906
|
+
|
|
907
|
+
self.console.print()
|
|
908
|
+
|
|
909
|
+
except (ImportError, Exception):
|
|
910
|
+
# Fallback to rich-pixels for broader compatibility
|
|
911
|
+
try:
|
|
912
|
+
from PIL import Image
|
|
913
|
+
from rich_pixels import Pixels
|
|
914
|
+
|
|
915
|
+
# Load image to check dimensions
|
|
916
|
+
pil_img = Image.open(path)
|
|
917
|
+
img_width, img_height = pil_img.size
|
|
918
|
+
aspect_ratio = img_width / img_height
|
|
919
|
+
|
|
920
|
+
# Terminal characters are roughly 2:1 (height:width)
|
|
921
|
+
# A char is ~2x taller than wide, so to show square image as square:
|
|
922
|
+
# need width_chars = height_chars * 2
|
|
923
|
+
# For proper aspect: width_chars = height_chars * 2 * image_aspect_ratio
|
|
924
|
+
target_height = 50 # rows
|
|
925
|
+
target_width = int(target_height * 2.0 * aspect_ratio) # columns
|
|
926
|
+
|
|
927
|
+
# Resize to these dimensions maintaining aspect
|
|
928
|
+
pixels = Pixels.from_image_path(
|
|
929
|
+
str(path), resize=(target_width, target_height)
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
if caption:
|
|
933
|
+
self.console.print(
|
|
934
|
+
Panel(
|
|
935
|
+
pixels,
|
|
936
|
+
title=f"🖼️ {caption}",
|
|
937
|
+
border_style="cyan",
|
|
938
|
+
padding=(0, 0),
|
|
939
|
+
),
|
|
940
|
+
justify="center",
|
|
941
|
+
)
|
|
942
|
+
else:
|
|
943
|
+
self.console.print(pixels, justify="center")
|
|
944
|
+
self.console.print()
|
|
945
|
+
|
|
946
|
+
except ImportError:
|
|
947
|
+
# No image libraries, show file info only
|
|
948
|
+
try:
|
|
949
|
+
from PIL import Image
|
|
950
|
+
|
|
951
|
+
img = Image.open(path)
|
|
952
|
+
info = f"[cyan]{path.name}[/cyan]\n"
|
|
953
|
+
info += f"[dim]Size: {img.width}x{img.height} | Format: {img.format} | File: {path.stat().st_size:,} bytes[/dim]"
|
|
954
|
+
|
|
955
|
+
if caption:
|
|
956
|
+
self.console.print(
|
|
957
|
+
Panel(
|
|
958
|
+
info, title=f"🖼️ {caption}", border_style="cyan"
|
|
959
|
+
),
|
|
960
|
+
justify="center",
|
|
961
|
+
)
|
|
962
|
+
else:
|
|
963
|
+
self.console.print(
|
|
964
|
+
Panel(info, border_style="cyan"), justify="center"
|
|
965
|
+
)
|
|
966
|
+
except Exception:
|
|
967
|
+
# Fallback to just showing the path
|
|
968
|
+
self.console.print(
|
|
969
|
+
f"[cyan]🖼️ Image: {path}[/cyan]", justify="center"
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
# Prompt to open in default viewer
|
|
973
|
+
if prompt_to_open and sys.platform == "win32":
|
|
974
|
+
try:
|
|
975
|
+
response = (
|
|
976
|
+
input("\nOpen image in default viewer? [Y/n]: ").strip().lower()
|
|
977
|
+
)
|
|
978
|
+
if response in ("", "y", "yes"):
|
|
979
|
+
if sys.platform == "win32":
|
|
980
|
+
os.startfile(str(path)) # pylint: disable=no-member
|
|
981
|
+
elif sys.platform == "darwin":
|
|
982
|
+
subprocess.run(["open", str(path)], check=False)
|
|
983
|
+
else:
|
|
984
|
+
subprocess.run(["xdg-open", str(path)], check=False)
|
|
985
|
+
except (KeyboardInterrupt, EOFError):
|
|
986
|
+
pass # User cancelled
|
|
987
|
+
else:
|
|
988
|
+
# Text-only terminal
|
|
989
|
+
print(f"\n🖼️ Image: {path}")
|
|
990
|
+
if caption:
|
|
991
|
+
print(f" {caption}")
|
|
992
|
+
|
|
993
|
+
# Prompt to open in default viewer
|
|
994
|
+
if prompt_to_open and sys.platform == "win32":
|
|
995
|
+
try:
|
|
996
|
+
response = input("\nOpen image? [Y/n]: ").strip().lower()
|
|
997
|
+
if response in ("", "y", "yes"):
|
|
998
|
+
os.startfile(str(path)) # pylint: disable=no-member
|
|
999
|
+
except (KeyboardInterrupt, EOFError):
|
|
1000
|
+
pass
|
|
1001
|
+
|
|
1002
|
+
print()
|
|
1003
|
+
|
|
806
1004
|
def print_diff(self, diff: str, filename: str) -> None:
|
|
807
1005
|
"""
|
|
808
1006
|
Print a code diff with syntax highlighting.
|
|
@@ -974,12 +1172,13 @@ class AgentConsole(OutputHandler):
|
|
|
974
1172
|
# Print the table in a panel
|
|
975
1173
|
self.console.print(Panel(table, border_style="blue"))
|
|
976
1174
|
|
|
977
|
-
def start_progress(self, message: str) -> None:
|
|
1175
|
+
def start_progress(self, message: str, show_timer: bool = False) -> None:
|
|
978
1176
|
"""
|
|
979
1177
|
Start the progress indicator.
|
|
980
1178
|
|
|
981
1179
|
Args:
|
|
982
1180
|
message: Message to display with the indicator
|
|
1181
|
+
show_timer: If True, show elapsed time in progress message
|
|
983
1182
|
"""
|
|
984
1183
|
# If file preview is active, pause it temporarily
|
|
985
1184
|
self._paused_preview = False
|
|
@@ -993,7 +1192,7 @@ class AgentConsole(OutputHandler):
|
|
|
993
1192
|
except Exception:
|
|
994
1193
|
pass
|
|
995
1194
|
|
|
996
|
-
self.progress.start(message)
|
|
1195
|
+
self.progress.start(message, show_timer=show_timer)
|
|
997
1196
|
|
|
998
1197
|
def stop_progress(self) -> None:
|
|
999
1198
|
"""Stop the progress indicator."""
|
|
@@ -1233,6 +1432,132 @@ class AgentConsole(OutputHandler):
|
|
|
1233
1432
|
else:
|
|
1234
1433
|
print(f"✅ Model {status}: {model_name}")
|
|
1235
1434
|
|
|
1435
|
+
# === Download Progress Methods ===
|
|
1436
|
+
|
|
1437
|
+
def print_download_start(self, model_name: str) -> None:
|
|
1438
|
+
"""
|
|
1439
|
+
Print download starting notification.
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
model_name: Name of the model being downloaded
|
|
1443
|
+
"""
|
|
1444
|
+
if self.rich_available and self.console:
|
|
1445
|
+
self.console.print()
|
|
1446
|
+
self.console.print(
|
|
1447
|
+
f"[bold blue]📥 Downloading:[/bold blue] [cyan]{model_name}[/cyan]"
|
|
1448
|
+
)
|
|
1449
|
+
else:
|
|
1450
|
+
rprint(f"\n📥 Downloading: {model_name}")
|
|
1451
|
+
|
|
1452
|
+
def print_download_progress(
|
|
1453
|
+
self,
|
|
1454
|
+
percent: int,
|
|
1455
|
+
bytes_downloaded: int,
|
|
1456
|
+
bytes_total: int,
|
|
1457
|
+
speed_mbps: float = 0.0,
|
|
1458
|
+
) -> None:
|
|
1459
|
+
"""
|
|
1460
|
+
Print download progress with a progress bar that updates in place.
|
|
1461
|
+
|
|
1462
|
+
Args:
|
|
1463
|
+
percent: Download percentage (0-100)
|
|
1464
|
+
bytes_downloaded: Bytes downloaded so far
|
|
1465
|
+
bytes_total: Total bytes to download
|
|
1466
|
+
speed_mbps: Download speed in MB/s (optional)
|
|
1467
|
+
"""
|
|
1468
|
+
import sys
|
|
1469
|
+
|
|
1470
|
+
# Format sizes
|
|
1471
|
+
if bytes_total > 1024**3: # > 1 GB
|
|
1472
|
+
dl_str = f"{bytes_downloaded / 1024**3:.2f} GB"
|
|
1473
|
+
total_str = f"{bytes_total / 1024**3:.2f} GB"
|
|
1474
|
+
elif bytes_total > 1024**2: # > 1 MB
|
|
1475
|
+
dl_str = f"{bytes_downloaded / 1024**2:.0f} MB"
|
|
1476
|
+
total_str = f"{bytes_total / 1024**2:.0f} MB"
|
|
1477
|
+
else:
|
|
1478
|
+
dl_str = f"{bytes_downloaded / 1024:.0f} KB"
|
|
1479
|
+
total_str = f"{bytes_total / 1024:.0f} KB"
|
|
1480
|
+
|
|
1481
|
+
# Progress bar characters
|
|
1482
|
+
bar_width = 25
|
|
1483
|
+
filled = int(bar_width * percent / 100)
|
|
1484
|
+
bar = "━" * filled + "─" * (bar_width - filled)
|
|
1485
|
+
|
|
1486
|
+
# Build progress line with optional speed
|
|
1487
|
+
progress_line = f" [{bar}] {percent:3d}% {dl_str} / {total_str}"
|
|
1488
|
+
if speed_mbps > 0.1:
|
|
1489
|
+
progress_line += f" @ {speed_mbps:.0f} MB/s"
|
|
1490
|
+
|
|
1491
|
+
# Update in place with carriage return
|
|
1492
|
+
sys.stdout.write(f"\r{progress_line:<80}")
|
|
1493
|
+
sys.stdout.flush()
|
|
1494
|
+
|
|
1495
|
+
def print_download_complete(self, model_name: str = None) -> None:
|
|
1496
|
+
"""
|
|
1497
|
+
Print download complete notification.
|
|
1498
|
+
|
|
1499
|
+
Args:
|
|
1500
|
+
model_name: Optional name of the downloaded model
|
|
1501
|
+
"""
|
|
1502
|
+
if self.rich_available and self.console:
|
|
1503
|
+
self.console.print() # Newline after progress bar
|
|
1504
|
+
if model_name:
|
|
1505
|
+
self.console.print(
|
|
1506
|
+
f" [green]✅ Downloaded successfully:[/green] [cyan]{model_name}[/cyan]"
|
|
1507
|
+
)
|
|
1508
|
+
else:
|
|
1509
|
+
self.console.print(" [green]✅ Download complete[/green]")
|
|
1510
|
+
else:
|
|
1511
|
+
rprint()
|
|
1512
|
+
msg = (
|
|
1513
|
+
f" ✅ Downloaded: {model_name}"
|
|
1514
|
+
if model_name
|
|
1515
|
+
else " ✅ Download complete"
|
|
1516
|
+
)
|
|
1517
|
+
rprint(msg)
|
|
1518
|
+
|
|
1519
|
+
def print_download_error(self, error_message: str, model_name: str = None) -> None:
|
|
1520
|
+
"""
|
|
1521
|
+
Print download error notification.
|
|
1522
|
+
|
|
1523
|
+
Args:
|
|
1524
|
+
error_message: Error description
|
|
1525
|
+
model_name: Optional name of the model that failed
|
|
1526
|
+
"""
|
|
1527
|
+
if self.rich_available and self.console:
|
|
1528
|
+
self.console.print() # Newline after progress bar
|
|
1529
|
+
if model_name:
|
|
1530
|
+
self.console.print(
|
|
1531
|
+
f" [red]❌ Download failed for {model_name}:[/red] {error_message}"
|
|
1532
|
+
)
|
|
1533
|
+
else:
|
|
1534
|
+
self.console.print(f" [red]❌ Download failed:[/red] {error_message}")
|
|
1535
|
+
else:
|
|
1536
|
+
rprint()
|
|
1537
|
+
msg = (
|
|
1538
|
+
f" ❌ Download failed for {model_name}: {error_message}"
|
|
1539
|
+
if model_name
|
|
1540
|
+
else f" ❌ Download failed: {error_message}"
|
|
1541
|
+
)
|
|
1542
|
+
rprint(msg)
|
|
1543
|
+
|
|
1544
|
+
def print_download_skipped(
|
|
1545
|
+
self, model_name: str, reason: str = "already downloaded"
|
|
1546
|
+
) -> None:
|
|
1547
|
+
"""
|
|
1548
|
+
Print download skipped notification.
|
|
1549
|
+
|
|
1550
|
+
Args:
|
|
1551
|
+
model_name: Name of the model that was skipped
|
|
1552
|
+
reason: Reason for skipping
|
|
1553
|
+
"""
|
|
1554
|
+
if self.rich_available and self.console:
|
|
1555
|
+
self.console.print(
|
|
1556
|
+
f"[green]✅[/green] [cyan]{model_name}[/cyan] [dim]({reason})[/dim]"
|
|
1557
|
+
)
|
|
1558
|
+
else:
|
|
1559
|
+
rprint(f"✅ {model_name} ({reason})")
|
|
1560
|
+
|
|
1236
1561
|
def print_extraction_start(
|
|
1237
1562
|
self, image_num: int, page_num: int, mime_type: str
|
|
1238
1563
|
) -> None:
|
|
@@ -1727,7 +2052,7 @@ class SilentConsole(OutputHandler):
|
|
|
1727
2052
|
console.print(Panel(table, border_style="blue"))
|
|
1728
2053
|
|
|
1729
2054
|
# All other abstract methods as no-ops
|
|
1730
|
-
def print_processing_start(self, query: str, max_steps: int):
|
|
2055
|
+
def print_processing_start(self, query: str, max_steps: int, model_id: str = None):
|
|
1731
2056
|
"""No-op implementation."""
|
|
1732
2057
|
|
|
1733
2058
|
def print_step_header(self, step_num: int, step_limit: int):
|
gaia/agents/base/tools.py
CHANGED
|
@@ -17,7 +17,10 @@ _TOOL_REGISTRY = {}
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def tool(
|
|
20
|
-
func: Callable = None,
|
|
20
|
+
func: Callable = None,
|
|
21
|
+
*,
|
|
22
|
+
atomic: bool = False,
|
|
23
|
+
**kwargs, # pylint: disable=unused-argument
|
|
21
24
|
) -> Callable:
|
|
22
25
|
"""
|
|
23
26
|
Decorator to register a function as a tool.
|
|
@@ -28,6 +31,7 @@ def tool(
|
|
|
28
31
|
|
|
29
32
|
Args:
|
|
30
33
|
func: Function to register as a tool (when used as @tool)
|
|
34
|
+
atomic: If True, marks this tool as atomic (can execute without multi-step planning)
|
|
31
35
|
**kwargs: Optional arguments (ignored, for backward compatibility)
|
|
32
36
|
|
|
33
37
|
Returns:
|
|
@@ -63,12 +67,13 @@ def tool(
|
|
|
63
67
|
|
|
64
68
|
params[name] = param_info
|
|
65
69
|
|
|
66
|
-
# Register the tool
|
|
70
|
+
# Register the tool with atomic metadata
|
|
67
71
|
_TOOL_REGISTRY[tool_name] = {
|
|
68
72
|
"name": tool_name,
|
|
69
73
|
"description": f.__doc__ or "",
|
|
70
74
|
"parameters": params,
|
|
71
75
|
"function": f,
|
|
76
|
+
"atomic": atomic,
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
# Return the function unchanged
|
gaia/agents/blender/agent.py
CHANGED
|
@@ -24,9 +24,6 @@ class BlenderAgent(Agent):
|
|
|
24
24
|
Inherits core functionality from the base Agent class.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
# Define Blender-specific tools that can execute directly without requiring a plan
|
|
28
|
-
SIMPLE_TOOLS = ["clear_scene", "get_scene_info"]
|
|
29
|
-
|
|
30
27
|
def __init__(
|
|
31
28
|
self,
|
|
32
29
|
mcp: Optional[MCPClient] = None,
|
|
@@ -80,7 +77,7 @@ class BlenderAgent(Agent):
|
|
|
80
77
|
def _get_system_prompt(self) -> str:
|
|
81
78
|
"""Generate the system prompt for the Blender agent."""
|
|
82
79
|
# Get formatted tools from registry
|
|
83
|
-
return
|
|
80
|
+
return """
|
|
84
81
|
You are a specialized Blender 3D assistant that can create and modify 3D scenes.
|
|
85
82
|
You will use a set of tools to accomplish tasks based on the user's request.
|
|
86
83
|
|
|
@@ -169,7 +166,7 @@ Examples of colored requests:
|
|
|
169
166
|
def _register_tools(self):
|
|
170
167
|
"""Register all Blender-related tools for the agent."""
|
|
171
168
|
|
|
172
|
-
@tool
|
|
169
|
+
@tool(atomic=True)
|
|
173
170
|
def clear_scene() -> Dict[str, Any]:
|
|
174
171
|
"""
|
|
175
172
|
Remove all objects from the current Blender scene.
|
|
@@ -283,7 +280,7 @@ Examples of colored requests:
|
|
|
283
280
|
return {"status": "error", "error": str(e)}
|
|
284
281
|
|
|
285
282
|
# @tool
|
|
286
|
-
def
|
|
283
|
+
def _get_object_info(name: str) -> Dict[str, Any]:
|
|
287
284
|
"""
|
|
288
285
|
Get information about an object in the scene.
|
|
289
286
|
|
|
@@ -354,7 +351,7 @@ Examples of colored requests:
|
|
|
354
351
|
return {"status": "error", "error": str(e)}
|
|
355
352
|
|
|
356
353
|
# @tool
|
|
357
|
-
def
|
|
354
|
+
def _delete_object(name: str) -> Dict[str, Any]:
|
|
358
355
|
"""
|
|
359
356
|
Delete an object from the scene.
|
|
360
357
|
|
|
@@ -382,7 +379,7 @@ Examples of colored requests:
|
|
|
382
379
|
self.error_history.append(str(e))
|
|
383
380
|
return {"status": "error", "error": str(e)}
|
|
384
381
|
|
|
385
|
-
@tool
|
|
382
|
+
@tool(atomic=True)
|
|
386
383
|
def get_scene_info() -> Dict[str, Any]:
|
|
387
384
|
"""
|
|
388
385
|
Get information about the current scene.
|
|
@@ -407,7 +404,7 @@ Examples of colored requests:
|
|
|
407
404
|
return {"status": "error", "error": str(e)}
|
|
408
405
|
|
|
409
406
|
# @tool
|
|
410
|
-
def
|
|
407
|
+
def _execute_blender_code(code: str) -> Dict[str, Any]:
|
|
411
408
|
"""
|
|
412
409
|
Execute arbitrary Python code in Blender with error handling.
|
|
413
410
|
|
|
@@ -436,7 +433,7 @@ Examples of colored requests:
|
|
|
436
433
|
return {"status": "error", "error": str(e)}
|
|
437
434
|
|
|
438
435
|
# @tool
|
|
439
|
-
def
|
|
436
|
+
def _diagnose_scene() -> Dict[str, Any]:
|
|
440
437
|
"""
|
|
441
438
|
Diagnose the current Blender scene for common issues.
|
|
442
439
|
Returns information about objects, materials, and potential problems.
|
gaia/agents/blender/core/view.py
CHANGED
|
@@ -13,13 +13,13 @@ class ViewManager:
|
|
|
13
13
|
self.mcp = mcp
|
|
14
14
|
|
|
15
15
|
def adjust_for_large_scale(
|
|
16
|
-
self, clip_end: float = 100000,
|
|
16
|
+
self, clip_end: float = 100000, _orbit_selection: bool = True
|
|
17
17
|
) -> Dict:
|
|
18
18
|
"""Adjust viewport settings to properly view large-scale objects like Earth.
|
|
19
19
|
|
|
20
20
|
Args:
|
|
21
21
|
clip_end: The maximum view distance to set for the 3D viewport (default: 100000)
|
|
22
|
-
|
|
22
|
+
_orbit_selection: Whether to enable orbit around selection (default: True, but may not work in all Blender versions)
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
def generate_code():
|