amd-gaia 0.15.2__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.
@@ -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(self, query: str, max_steps: int) -> None:
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."""
@@ -1853,7 +2052,7 @@ class SilentConsole(OutputHandler):
1853
2052
  console.print(Panel(table, border_style="blue"))
1854
2053
 
1855
2054
  # All other abstract methods as no-ops
1856
- 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):
1857
2056
  """No-op implementation."""
1858
2057
 
1859
2058
  def print_step_header(self, step_num: int, step_limit: int):
@@ -0,0 +1,11 @@
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ """
4
+ Shared tools for GAIA agents.
5
+
6
+ This package contains tool mixins that can be used across multiple agents.
7
+ """
8
+
9
+ from .file_tools import FileSearchToolsMixin
10
+
11
+ __all__ = ["FileSearchToolsMixin"]