amd-gaia 0.15.0__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.
Files changed (185) hide show
  1. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/METADATA +222 -223
  2. amd_gaia-0.15.2.dist-info/RECORD +182 -0
  3. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/WHEEL +1 -1
  4. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/entry_points.txt +1 -0
  5. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/licenses/LICENSE.md +20 -20
  6. gaia/__init__.py +29 -29
  7. gaia/agents/__init__.py +19 -19
  8. gaia/agents/base/__init__.py +9 -9
  9. gaia/agents/base/agent.py +2132 -2177
  10. gaia/agents/base/api_agent.py +119 -120
  11. gaia/agents/base/console.py +1967 -1841
  12. gaia/agents/base/errors.py +237 -237
  13. gaia/agents/base/mcp_agent.py +86 -86
  14. gaia/agents/base/tools.py +88 -83
  15. gaia/agents/blender/__init__.py +7 -0
  16. gaia/agents/blender/agent.py +553 -556
  17. gaia/agents/blender/agent_simple.py +133 -135
  18. gaia/agents/blender/app.py +211 -211
  19. gaia/agents/blender/app_simple.py +41 -41
  20. gaia/agents/blender/core/__init__.py +16 -16
  21. gaia/agents/blender/core/materials.py +506 -506
  22. gaia/agents/blender/core/objects.py +316 -316
  23. gaia/agents/blender/core/rendering.py +225 -225
  24. gaia/agents/blender/core/scene.py +220 -220
  25. gaia/agents/blender/core/view.py +146 -146
  26. gaia/agents/chat/__init__.py +9 -9
  27. gaia/agents/chat/agent.py +809 -835
  28. gaia/agents/chat/app.py +1065 -1058
  29. gaia/agents/chat/session.py +508 -508
  30. gaia/agents/chat/tools/__init__.py +15 -15
  31. gaia/agents/chat/tools/file_tools.py +96 -96
  32. gaia/agents/chat/tools/rag_tools.py +1744 -1729
  33. gaia/agents/chat/tools/shell_tools.py +437 -436
  34. gaia/agents/code/__init__.py +7 -7
  35. gaia/agents/code/agent.py +549 -549
  36. gaia/agents/code/cli.py +377 -0
  37. gaia/agents/code/models.py +135 -135
  38. gaia/agents/code/orchestration/__init__.py +24 -24
  39. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  40. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  41. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  42. gaia/agents/code/orchestration/factories/base.py +63 -63
  43. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  44. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  45. gaia/agents/code/orchestration/orchestrator.py +841 -841
  46. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  47. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  48. gaia/agents/code/orchestration/steps/base.py +188 -188
  49. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  50. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  51. gaia/agents/code/orchestration/steps/python.py +307 -307
  52. gaia/agents/code/orchestration/template_catalog.py +469 -469
  53. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  54. gaia/agents/code/orchestration/workflows/base.py +80 -80
  55. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  56. gaia/agents/code/orchestration/workflows/python.py +94 -94
  57. gaia/agents/code/prompts/__init__.py +11 -11
  58. gaia/agents/code/prompts/base_prompt.py +77 -77
  59. gaia/agents/code/prompts/code_patterns.py +2034 -2036
  60. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  61. gaia/agents/code/prompts/python_prompt.py +109 -109
  62. gaia/agents/code/schema_inference.py +365 -365
  63. gaia/agents/code/system_prompt.py +41 -41
  64. gaia/agents/code/tools/__init__.py +42 -42
  65. gaia/agents/code/tools/cli_tools.py +1138 -1138
  66. gaia/agents/code/tools/code_formatting.py +319 -319
  67. gaia/agents/code/tools/code_tools.py +769 -769
  68. gaia/agents/code/tools/error_fixing.py +1347 -1347
  69. gaia/agents/code/tools/external_tools.py +180 -180
  70. gaia/agents/code/tools/file_io.py +845 -845
  71. gaia/agents/code/tools/prisma_tools.py +190 -190
  72. gaia/agents/code/tools/project_management.py +1016 -1016
  73. gaia/agents/code/tools/testing.py +321 -321
  74. gaia/agents/code/tools/typescript_tools.py +122 -122
  75. gaia/agents/code/tools/validation_parsing.py +461 -461
  76. gaia/agents/code/tools/validation_tools.py +806 -806
  77. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  78. gaia/agents/code/validators/__init__.py +16 -16
  79. gaia/agents/code/validators/antipattern_checker.py +241 -241
  80. gaia/agents/code/validators/ast_analyzer.py +197 -197
  81. gaia/agents/code/validators/requirements_validator.py +145 -145
  82. gaia/agents/code/validators/syntax_validator.py +171 -171
  83. gaia/agents/docker/__init__.py +7 -7
  84. gaia/agents/docker/agent.py +643 -642
  85. gaia/agents/emr/__init__.py +8 -8
  86. gaia/agents/emr/agent.py +1504 -1506
  87. gaia/agents/emr/cli.py +1322 -1322
  88. gaia/agents/emr/constants.py +475 -475
  89. gaia/agents/emr/dashboard/__init__.py +4 -4
  90. gaia/agents/emr/dashboard/server.py +1972 -1974
  91. gaia/agents/jira/__init__.py +11 -11
  92. gaia/agents/jira/agent.py +894 -894
  93. gaia/agents/jira/jql_templates.py +299 -299
  94. gaia/agents/routing/__init__.py +7 -7
  95. gaia/agents/routing/agent.py +567 -570
  96. gaia/agents/routing/system_prompt.py +75 -75
  97. gaia/agents/summarize/__init__.py +11 -0
  98. gaia/agents/summarize/agent.py +885 -0
  99. gaia/agents/summarize/prompts.py +129 -0
  100. gaia/api/__init__.py +23 -23
  101. gaia/api/agent_registry.py +238 -238
  102. gaia/api/app.py +305 -305
  103. gaia/api/openai_server.py +575 -575
  104. gaia/api/schemas.py +186 -186
  105. gaia/api/sse_handler.py +373 -373
  106. gaia/apps/__init__.py +4 -4
  107. gaia/apps/llm/__init__.py +6 -6
  108. gaia/apps/llm/app.py +184 -169
  109. gaia/apps/summarize/app.py +116 -633
  110. gaia/apps/summarize/html_viewer.py +133 -133
  111. gaia/apps/summarize/pdf_formatter.py +284 -284
  112. gaia/audio/__init__.py +2 -2
  113. gaia/audio/audio_client.py +439 -439
  114. gaia/audio/audio_recorder.py +269 -269
  115. gaia/audio/kokoro_tts.py +599 -599
  116. gaia/audio/whisper_asr.py +432 -432
  117. gaia/chat/__init__.py +16 -16
  118. gaia/chat/app.py +428 -430
  119. gaia/chat/prompts.py +522 -522
  120. gaia/chat/sdk.py +1228 -1225
  121. gaia/cli.py +5659 -5632
  122. gaia/database/__init__.py +10 -10
  123. gaia/database/agent.py +176 -176
  124. gaia/database/mixin.py +290 -290
  125. gaia/database/testing.py +64 -64
  126. gaia/eval/batch_experiment.py +2332 -2332
  127. gaia/eval/claude.py +542 -542
  128. gaia/eval/config.py +37 -37
  129. gaia/eval/email_generator.py +512 -512
  130. gaia/eval/eval.py +3179 -3179
  131. gaia/eval/groundtruth.py +1130 -1130
  132. gaia/eval/transcript_generator.py +582 -582
  133. gaia/eval/webapp/README.md +167 -167
  134. gaia/eval/webapp/package-lock.json +875 -875
  135. gaia/eval/webapp/package.json +20 -20
  136. gaia/eval/webapp/public/app.js +3402 -3402
  137. gaia/eval/webapp/public/index.html +87 -87
  138. gaia/eval/webapp/public/styles.css +3661 -3661
  139. gaia/eval/webapp/server.js +415 -415
  140. gaia/eval/webapp/test-setup.js +72 -72
  141. gaia/installer/__init__.py +23 -0
  142. gaia/installer/init_command.py +1275 -0
  143. gaia/installer/lemonade_installer.py +619 -0
  144. gaia/llm/__init__.py +10 -2
  145. gaia/llm/base_client.py +60 -0
  146. gaia/llm/exceptions.py +12 -0
  147. gaia/llm/factory.py +70 -0
  148. gaia/llm/lemonade_client.py +3421 -3221
  149. gaia/llm/lemonade_manager.py +294 -294
  150. gaia/llm/providers/__init__.py +9 -0
  151. gaia/llm/providers/claude.py +108 -0
  152. gaia/llm/providers/lemonade.py +118 -0
  153. gaia/llm/providers/openai_provider.py +79 -0
  154. gaia/llm/vlm_client.py +382 -382
  155. gaia/logger.py +189 -189
  156. gaia/mcp/agent_mcp_server.py +245 -245
  157. gaia/mcp/blender_mcp_client.py +138 -138
  158. gaia/mcp/blender_mcp_server.py +648 -648
  159. gaia/mcp/context7_cache.py +332 -332
  160. gaia/mcp/external_services.py +518 -518
  161. gaia/mcp/mcp_bridge.py +811 -550
  162. gaia/mcp/servers/__init__.py +6 -6
  163. gaia/mcp/servers/docker_mcp.py +83 -83
  164. gaia/perf_analysis.py +361 -0
  165. gaia/rag/__init__.py +10 -10
  166. gaia/rag/app.py +293 -293
  167. gaia/rag/demo.py +304 -304
  168. gaia/rag/pdf_utils.py +235 -235
  169. gaia/rag/sdk.py +2194 -2194
  170. gaia/security.py +183 -163
  171. gaia/talk/app.py +287 -289
  172. gaia/talk/sdk.py +538 -538
  173. gaia/testing/__init__.py +87 -87
  174. gaia/testing/assertions.py +330 -330
  175. gaia/testing/fixtures.py +333 -333
  176. gaia/testing/mocks.py +493 -493
  177. gaia/util.py +46 -46
  178. gaia/utils/__init__.py +33 -33
  179. gaia/utils/file_watcher.py +675 -675
  180. gaia/utils/parsing.py +223 -223
  181. gaia/version.py +100 -100
  182. amd_gaia-0.15.0.dist-info/RECORD +0 -168
  183. gaia/agents/code/app.py +0 -266
  184. gaia/llm/llm_client.py +0 -723
  185. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/top_level.txt +0 -0
@@ -1,1841 +1,1967 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
-
4
- import json
5
- import threading
6
- import time
7
- from abc import ABC, abstractmethod
8
- from typing import Any, Dict, List, Optional
9
-
10
- # Import Rich library for pretty printing and syntax highlighting
11
- try:
12
- from rich import print as rprint
13
- from rich.console import Console
14
- from rich.live import Live
15
- from rich.panel import Panel
16
- from rich.spinner import Spinner
17
- from rich.syntax import Syntax
18
- from rich.table import Table
19
-
20
- RICH_AVAILABLE = True
21
- except ImportError:
22
- RICH_AVAILABLE = False
23
- print(
24
- "Rich library not found. Install with 'uv pip install rich' for syntax highlighting."
25
- )
26
-
27
- # Display configuration constants
28
- MAX_DISPLAY_LINE_LENGTH = 120
29
-
30
-
31
- # ANSI Color Codes for fallback when Rich is not available
32
- ANSI_RESET = "\033[0m"
33
- ANSI_BOLD = "\033[1m"
34
- ANSI_DIM = "\033[90m" # Dark Gray
35
- ANSI_RED = "\033[91m"
36
- ANSI_GREEN = "\033[92m"
37
- ANSI_YELLOW = "\033[93m"
38
- ANSI_BLUE = "\033[94m"
39
- ANSI_MAGENTA = "\033[95m"
40
- ANSI_CYAN = "\033[96m"
41
-
42
-
43
- class OutputHandler(ABC):
44
- """
45
- Abstract base class for handling agent output.
46
-
47
- Defines the minimal interface that agents use to report their progress.
48
- Each implementation handles the output differently:
49
- - AgentConsole: Rich console output for CLI
50
- - SilentConsole: Suppressed output for testing
51
- - SSEOutputHandler: Server-Sent Events for API streaming
52
-
53
- This interface focuses on WHAT agents need to report, not HOW
54
- each handler chooses to display it.
55
- """
56
-
57
- # === Core Progress/State Methods (Required) ===
58
-
59
- @abstractmethod
60
- def print_processing_start(self, query: str, max_steps: int):
61
- """Print processing start message."""
62
- ...
63
-
64
- @abstractmethod
65
- def print_step_header(self, step_num: int, step_limit: int):
66
- """Print step header."""
67
- ...
68
-
69
- @abstractmethod
70
- def print_state_info(self, state_message: str):
71
- """Print current execution state."""
72
- ...
73
-
74
- @abstractmethod
75
- def print_thought(self, thought: str):
76
- """Print agent's reasoning/thought."""
77
- ...
78
-
79
- @abstractmethod
80
- def print_goal(self, goal: str):
81
- """Print agent's current goal."""
82
- ...
83
-
84
- @abstractmethod
85
- def print_plan(self, plan: List[Any], current_step: int = None):
86
- """Print agent's plan with optional current step highlight."""
87
- ...
88
-
89
- # === Tool Execution Methods (Required) ===
90
-
91
- @abstractmethod
92
- def print_tool_usage(self, tool_name: str):
93
- """Print tool being called."""
94
- ...
95
-
96
- @abstractmethod
97
- def print_tool_complete(self):
98
- """Print tool completion."""
99
- ...
100
-
101
- @abstractmethod
102
- def pretty_print_json(self, data: Dict[str, Any], title: str = None):
103
- """Print JSON data (tool args/results)."""
104
- ...
105
-
106
- # === Status Messages (Required) ===
107
-
108
- @abstractmethod
109
- def print_error(self, error_message: str):
110
- """Print error message."""
111
- ...
112
-
113
- @abstractmethod
114
- def print_warning(self, warning_message: str):
115
- """Print warning message."""
116
- ...
117
-
118
- @abstractmethod
119
- def print_info(self, message: str):
120
- """Print informational message."""
121
- ...
122
-
123
- # === Progress Indicators (Required) ===
124
-
125
- @abstractmethod
126
- def start_progress(self, message: str):
127
- """Start progress indicator."""
128
- ...
129
-
130
- @abstractmethod
131
- def stop_progress(self):
132
- """Stop progress indicator."""
133
- ...
134
-
135
- # === Completion Methods (Required) ===
136
-
137
- @abstractmethod
138
- def print_final_answer(self, answer: str):
139
- """Print final answer/result."""
140
- ...
141
-
142
- @abstractmethod
143
- def print_repeated_tool_warning(self):
144
- """Print warning about repeated tool calls (loop detection)."""
145
- ...
146
-
147
- @abstractmethod
148
- def print_completion(self, steps_taken: int, steps_limit: int):
149
- """Print completion summary."""
150
- ...
151
-
152
- @abstractmethod
153
- def print_step_paused(self, description: str):
154
- """Print step paused message."""
155
- ...
156
-
157
- @abstractmethod
158
- def print_command_executing(self, command: str):
159
- """Print command executing message."""
160
- ...
161
-
162
- @abstractmethod
163
- def print_agent_selected(self, agent_name: str, language: str, project_type: str):
164
- """Print agent selected message."""
165
- ...
166
-
167
- # === Optional Methods (with default no-op implementations) ===
168
-
169
- def print_prompt(
170
- self, prompt: str, title: str = "Prompt"
171
- ): # pylint: disable=unused-argument
172
- """Print prompt (for debugging). Optional - default no-op."""
173
- ...
174
-
175
- def print_response(
176
- self, response: str, title: str = "Response"
177
- ): # pylint: disable=unused-argument
178
- """Print response (for debugging). Optional - default no-op."""
179
- ...
180
-
181
- def print_streaming_text(
182
- self, text_chunk: str, end_of_stream: bool = False
183
- ): # pylint: disable=unused-argument
184
- """Print streaming text. Optional - default no-op."""
185
- ...
186
-
187
- def display_stats(self, stats: Dict[str, Any]): # pylint: disable=unused-argument
188
- """Display performance statistics. Optional - default no-op."""
189
- ...
190
-
191
- def print_header(self, text: str): # pylint: disable=unused-argument
192
- """Print header. Optional - default no-op."""
193
- ...
194
-
195
- def print_separator(self, length: int = 50): # pylint: disable=unused-argument
196
- """Print separator. Optional - default no-op."""
197
- ...
198
-
199
- def print_tool_info(
200
- self, name: str, params_str: str, description: str
201
- ): # pylint: disable=unused-argument
202
- """Print tool info. Optional - default no-op."""
203
- ...
204
-
205
-
206
- class ProgressIndicator:
207
- """A simple progress indicator that shows a spinner or dots animation."""
208
-
209
- def __init__(self, message="Processing"):
210
- """Initialize the progress indicator.
211
-
212
- Args:
213
- message: The message to display before the animation
214
- """
215
- self.message = message
216
- self.is_running = False
217
- self.thread = None
218
- self.spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
219
- self.dot_chars = [".", "..", "..."]
220
- self.spinner_idx = 0
221
- self.dot_idx = 0
222
- self.rich_spinner = None
223
- if RICH_AVAILABLE:
224
- self.rich_spinner = Spinner("dots", text=message)
225
- self.live = None
226
-
227
- def _animate(self):
228
- """Animation loop that runs in a separate thread."""
229
- while self.is_running:
230
- if RICH_AVAILABLE:
231
- # Rich handles the animation internally
232
- time.sleep(0.1)
233
- else:
234
- # Simple terminal-based animation
235
- self.dot_idx = (self.dot_idx + 1) % len(self.dot_chars)
236
- self.spinner_idx = (self.spinner_idx + 1) % len(self.spinner_chars)
237
-
238
- # Determine if we should use Unicode spinner or simple dots
239
- try:
240
- # Try to print a Unicode character to see if the terminal supports it
241
- print(self.spinner_chars[0], end="", flush=True)
242
- print(
243
- "\b", end="", flush=True
244
- ) # Backspace to remove the test character
245
-
246
- # If we got here, Unicode is supported
247
- print(
248
- f"\r{self.message} {self.spinner_chars[self.spinner_idx]}",
249
- end="",
250
- flush=True,
251
- )
252
- except (UnicodeError, OSError):
253
- # Fallback to simple dots
254
- print(
255
- f"\r{self.message}{self.dot_chars[self.dot_idx]}",
256
- end="",
257
- flush=True,
258
- )
259
-
260
- time.sleep(0.1)
261
-
262
- def start(self, message=None):
263
- """Start the progress indicator.
264
-
265
- Args:
266
- message: Optional new message to display
267
- """
268
- if message:
269
- self.message = message
270
-
271
- if self.is_running:
272
- return
273
-
274
- self.is_running = True
275
-
276
- if RICH_AVAILABLE:
277
- if self.rich_spinner:
278
- self.rich_spinner.text = self.message
279
- # Use transient=True to auto-clear when done
280
- self.live = Live(
281
- self.rich_spinner, refresh_per_second=10, transient=True
282
- )
283
- self.live.start()
284
- else:
285
- self.thread = threading.Thread(target=self._animate)
286
- self.thread.daemon = True
287
- self.thread.start()
288
-
289
- def stop(self):
290
- """Stop the progress indicator."""
291
- if not self.is_running:
292
- return
293
-
294
- self.is_running = False
295
-
296
- if RICH_AVAILABLE and self.live:
297
- self.live.stop()
298
- elif self.thread:
299
- self.thread.join(timeout=0.2)
300
- # Clear the animation line
301
- print("\r" + " " * (len(self.message) + 5) + "\r", end="", flush=True)
302
-
303
-
304
- class AgentConsole(OutputHandler):
305
- """
306
- A class to handle all display-related functionality for the agent.
307
- Provides rich text formatting and progress indicators when available.
308
- Implements OutputHandler for CLI-based output.
309
- """
310
-
311
- def __init__(self):
312
- """Initialize the AgentConsole with appropriate display capabilities."""
313
- self.rich_available = RICH_AVAILABLE
314
- self.console = Console() if self.rich_available else None
315
- self.progress = ProgressIndicator()
316
- self.rprint = rprint
317
- self.Panel = Panel
318
- self.streaming_buffer = "" # Buffer for accumulating streaming text
319
- self.file_preview_live: Optional[Live] = None
320
- self.file_preview_content = ""
321
- self.file_preview_filename = ""
322
- self.file_preview_max_lines = 15
323
- self._paused_preview = False # Track if preview was paused for progress
324
- self._last_preview_update_time = 0 # Throttle preview updates
325
- self._preview_update_interval = 0.25 # Minimum seconds between updates
326
-
327
- def print(self, *args, **kwargs):
328
- """
329
- Print method that delegates to Rich Console or standard print.
330
-
331
- This allows code to call console.print() directly on AgentConsole instances.
332
-
333
- Args:
334
- *args: Arguments to print
335
- **kwargs: Keyword arguments (style, etc.) for Rich Console
336
- """
337
- if self.rich_available and self.console:
338
- self.console.print(*args, **kwargs)
339
- else:
340
- # Fallback to standard print
341
- print(*args, **kwargs)
342
-
343
- # Implementation of OutputHandler abstract methods
344
-
345
- def pretty_print_json(self, data: Dict[str, Any], title: str = None) -> None:
346
- """
347
- Pretty print JSON data with syntax highlighting if Rich is available.
348
- If data contains a "command" field, shows it prominently.
349
-
350
- Args:
351
- data: Dictionary data to print
352
- title: Optional title for the panel
353
- """
354
-
355
- def _safe_default(obj: Any) -> Any:
356
- """
357
- JSON serializer fallback that handles common non-serializable types like numpy scalars/arrays.
358
- """
359
- try:
360
- import numpy as np # Local import to avoid hard dependency at module import time
361
-
362
- if isinstance(obj, np.generic):
363
- return obj.item()
364
- if isinstance(obj, np.ndarray):
365
- return obj.tolist()
366
- except Exception:
367
- pass
368
-
369
- for caster in (float, int, str):
370
- try:
371
- return caster(obj)
372
- except Exception:
373
- continue
374
- return "<non-serializable>"
375
-
376
- if self.rich_available:
377
- # Check if this is a command execution result
378
- if "command" in data and "stdout" in data:
379
- # Show command execution in a special format
380
- command = data.get("command", "")
381
- stdout = data.get("stdout", "")
382
- stderr = data.get("stderr", "")
383
- return_code = data.get("return_code", 0)
384
-
385
- # Build preview text
386
- preview = f"$ {command}\n\n"
387
- if stdout:
388
- preview += stdout[:500] # First 500 chars
389
- if len(stdout) > 500:
390
- preview += "\n... (output truncated)"
391
- if stderr:
392
- preview += f"\n\nSTDERR:\n{stderr[:200]}"
393
- if return_code != 0:
394
- preview += f"\n\n[Return code: {return_code}]"
395
-
396
- self.console.print(
397
- Panel(
398
- preview,
399
- title=title or "Command Output",
400
- border_style="blue",
401
- expand=False,
402
- )
403
- )
404
- else:
405
- # Regular JSON output
406
- # Convert to formatted JSON string with safe fallback for non-serializable types (e.g., numpy.float32)
407
- print(data)
408
- try:
409
- json_str = json.dumps(data, indent=2)
410
- except TypeError:
411
- json_str = json.dumps(data, indent=2, default=_safe_default)
412
-
413
- # Create a syntax object with JSON highlighting
414
- syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
415
- # Create a panel with a title if provided
416
- if title:
417
- self.console.print(Panel(syntax, title=title, border_style="blue"))
418
- else:
419
- self.console.print(syntax)
420
- else:
421
- # Fallback to standard pretty printing without highlighting
422
- if title:
423
- print(f"\n--- {title} ---")
424
- # Check if this is a command execution
425
- if "command" in data and "stdout" in data:
426
- print(f"\n$ {data.get('command', '')}")
427
- stdout = data.get("stdout", "")
428
- if stdout:
429
- print(stdout[:500])
430
- if len(stdout) > 500:
431
- print("... (output truncated)")
432
- else:
433
- try:
434
- print(json.dumps(data, indent=2))
435
- except TypeError:
436
- print(json.dumps(data, indent=2, default=_safe_default))
437
-
438
- def print_header(self, text: str) -> None:
439
- """
440
- Print a header with appropriate styling.
441
-
442
- Args:
443
- text: The header text to display
444
- """
445
- if self.rich_available:
446
- self.console.print(f"\n[bold blue]{text}[/bold blue]")
447
- else:
448
- print(f"\n{text}")
449
-
450
- def print_step_paused(self, description: str) -> None:
451
- """
452
- Print step paused message.
453
-
454
- Args:
455
- description: Description of the step being paused after
456
- """
457
- if self.rich_available:
458
- self.console.print(
459
- f"\n[bold yellow]⏸️ Paused after step:[/bold yellow] {description}"
460
- )
461
- self.console.print("Press Enter to continue, or 'n'/'q' to stop...")
462
- else:
463
- print(f"\n⏸️ Paused after step: {description}")
464
- print("Press Enter to continue, or 'n'/'q' to stop...")
465
-
466
- def print_processing_start(self, query: str, max_steps: int) -> None:
467
- """
468
- Print the initial processing message.
469
-
470
- Args:
471
- query: The user query being processed
472
- max_steps: Maximum number of steps allowed (kept for API compatibility)
473
- """
474
- if self.rich_available:
475
- self.console.print(f"\n[bold blue]🤖 Processing:[/bold blue] '{query}'")
476
- self.console.print("=" * 50)
477
- self.console.print()
478
- else:
479
- print(f"\n🤖 Processing: '{query}'")
480
- print("=" * 50)
481
- print()
482
-
483
- def print_separator(self, length: int = 50) -> None:
484
- """
485
- Print a separator line.
486
-
487
- Args:
488
- length: Length of the separator line
489
- """
490
- if self.rich_available:
491
- self.console.print("=" * length, style="dim")
492
- else:
493
- print("=" * length)
494
-
495
- def print_step_header(self, step_num: int, step_limit: int) -> None:
496
- """
497
- Print a step header.
498
-
499
- Args:
500
- step_num: Current step number
501
- step_limit: Maximum number of steps (unused, kept for compatibility)
502
- """
503
- _ = step_limit # Mark as intentionally unused
504
- if self.rich_available:
505
- self.console.print(
506
- f"\n[bold cyan]📝 Step {step_num}:[/bold cyan] Thinking...",
507
- highlight=False,
508
- )
509
- else:
510
- print(f"\n📝 Step {step_num}: Thinking...")
511
-
512
- def print_thought(self, thought: str) -> None:
513
- """
514
- Print the agent's thought with appropriate styling.
515
-
516
- Args:
517
- thought: The thought to display
518
- """
519
- if self.rich_available:
520
- self.console.print(f"[bold green]🧠 Thought:[/bold green] {thought}")
521
- else:
522
- print(f"🧠 Thought: {thought}")
523
-
524
- def print_goal(self, goal: str) -> None:
525
- """
526
- Print the agent's goal with appropriate styling.
527
-
528
- Args:
529
- goal: The goal to display
530
- """
531
- if self.rich_available:
532
- self.console.print(f"[bold yellow]🎯 Goal:[/bold yellow] {goal}")
533
- else:
534
- print(f"🎯 Goal: {goal}")
535
-
536
- def print_plan(self, plan: List[Any], current_step: int = None) -> None:
537
- """
538
- Print the agent's plan with appropriate styling.
539
-
540
- Args:
541
- plan: List of plan steps
542
- current_step: Optional index of the current step being executed (0-based)
543
- """
544
- if self.rich_available:
545
- self.console.print("\n[bold magenta]📋 Plan:[/bold magenta]")
546
- for i, step in enumerate(plan):
547
- step_text = step
548
- # Convert dict steps to string representation if needed
549
- if isinstance(step, dict):
550
- if "tool" in step and "tool_args" in step:
551
- args_str = json.dumps(step["tool_args"], sort_keys=True)
552
- step_text = f"Use tool '{step['tool']}' with args: {args_str}"
553
- else:
554
- step_text = json.dumps(step)
555
-
556
- # Highlight the current step being executed
557
- if current_step is not None and i == current_step:
558
- self.console.print(
559
- f" [dim]{i+1}.[/dim] [bold green]►[/bold green] [bold yellow]{step_text}[/bold yellow] [bold green]◄[/bold green] [cyan](current step)[/cyan]"
560
- )
561
- else:
562
- self.console.print(f" [dim]{i+1}.[/dim] {step_text}")
563
- # Add an extra newline for better readability
564
- self.console.print("")
565
- else:
566
- print("\n📋 Plan:")
567
- for i, step in enumerate(plan):
568
- step_text = step
569
- # Convert dict steps to string representation if needed
570
- if isinstance(step, dict):
571
- if "tool" in step and "tool_args" in step:
572
- args_str = json.dumps(step["tool_args"], sort_keys=True)
573
- step_text = f"Use tool '{step['tool']}' with args: {args_str}"
574
- else:
575
- step_text = json.dumps(step)
576
-
577
- # Highlight the current step being executed
578
- if current_step is not None and i == current_step:
579
- print(f" {i+1}. ► {step_text} ◄ (current step)")
580
- else:
581
- print(f" {i+1}. {step_text}")
582
-
583
- def print_plan_progress(
584
- self, current_step: int, total_steps: int, completed_steps: int = None
585
- ):
586
- """
587
- Print progress in plan execution
588
-
589
- Args:
590
- current_step: Current step being executed (1-based)
591
- total_steps: Total number of steps in the plan
592
- completed_steps: Optional number of already completed steps
593
- """
594
- if completed_steps is None:
595
- completed_steps = current_step - 1
596
-
597
- progress_str = f"[Step {current_step}/{total_steps}]"
598
- progress_bar = ""
599
-
600
- # Create a simple progress bar
601
- if total_steps > 0:
602
- bar_width = 20
603
- completed_chars = int((completed_steps / total_steps) * bar_width)
604
- current_char = 1 if current_step <= total_steps else 0
605
- remaining_chars = bar_width - completed_chars - current_char
606
-
607
- progress_bar = (
608
- "█" * completed_chars + "▶" * current_char + "░" * remaining_chars
609
- )
610
-
611
- if self.rich_available:
612
- self.rprint(f"[cyan]{progress_str}[/cyan] {progress_bar}")
613
- else:
614
- print(f"{progress_str} {progress_bar}")
615
-
616
- def print_checklist(self, items: List[Any], current_idx: int) -> None:
617
- """Print the checklist with current item highlighted.
618
-
619
- Args:
620
- items: List of checklist items (must have .description attribute)
621
- current_idx: Index of the item currently being executed (0-based)
622
- """
623
- if self.rich_available:
624
- self.console.print("\n[bold magenta]📋 EXECUTION PLAN[/bold magenta]")
625
- self.console.print("=" * 60, style="dim")
626
-
627
- for i, item in enumerate(items):
628
- desc = getattr(item, "description", str(item))
629
-
630
- if i < current_idx:
631
- # Completed
632
- self.console.print(f" [green]✓ {desc}[/green]")
633
- elif i == current_idx:
634
- # Current
635
- self.console.print(f" [bold blue]➜ {desc}[/bold blue]")
636
- else:
637
- # Pending
638
- self.console.print(f" [dim]○ {desc}[/dim]")
639
-
640
- self.console.print("=" * 60, style="dim")
641
- self.console.print("")
642
- else:
643
- print("\n" + "=" * 60)
644
- print(f"{ANSI_BOLD}📋 EXECUTION PLAN{ANSI_RESET}")
645
- print("=" * 60)
646
-
647
- for i, item in enumerate(items):
648
- desc = getattr(item, "description", str(item))
649
- if i < current_idx:
650
- print(f" {ANSI_GREEN}✓ {desc}{ANSI_RESET}")
651
- elif i == current_idx:
652
- print(f" {ANSI_BLUE}{ANSI_BOLD}➜ {desc}{ANSI_RESET}")
653
- else:
654
- print(f" {ANSI_DIM}○ {desc}{ANSI_RESET}")
655
-
656
- print("=" * 60 + "\n")
657
-
658
- def print_checklist_reasoning(self, reasoning: str) -> None:
659
- """
660
- Print checklist reasoning.
661
-
662
- Args:
663
- reasoning: The reasoning text to display
664
- """
665
- if self.rich_available:
666
- self.console.print("\n[bold]📝 CHECKLIST REASONING[/bold]")
667
- self.console.print("=" * 60, style="dim")
668
- self.console.print(f"{reasoning}")
669
- self.console.print("=" * 60, style="dim")
670
- self.console.print("")
671
- else:
672
- print("\n" + "=" * 60)
673
- print(f"{ANSI_BOLD}📝 CHECKLIST REASONING{ANSI_RESET}")
674
- print("=" * 60)
675
- print(f"{reasoning}")
676
- print("=" * 60 + "\n")
677
-
678
- def print_command_executing(self, command: str) -> None:
679
- """
680
- Print command executing message.
681
-
682
- Args:
683
- command: The command being executed
684
- """
685
- if self.rich_available:
686
- self.console.print(f"\n[bold]Executing Command:[/bold] {command}")
687
- else:
688
- print(f"\nExecuting Command: {command}")
689
-
690
- def print_agent_selected(
691
- self, agent_name: str, language: str, project_type: str
692
- ) -> None:
693
- """
694
- Print agent selected message.
695
-
696
- Args:
697
- agent_name: The name of the selected agent
698
- language: The detected programming language
699
- project_type: The detected project type
700
- """
701
- if self.rich_available:
702
- self.console.print(
703
- f"[bold]🤖 Agent Selected:[/bold] [blue]{agent_name}[/blue] (language={language}, project_type={project_type})\n"
704
- )
705
- else:
706
- print(
707
- f"{ANSI_BOLD}🤖 Agent Selected:{ANSI_RESET} {ANSI_BLUE}{agent_name}{ANSI_RESET} (language={language}, project_type={project_type})\n"
708
- )
709
-
710
- def print_tool_usage(self, tool_name: str) -> None:
711
- """
712
- Print tool usage information with user-friendly descriptions.
713
-
714
- Args:
715
- tool_name: Name of the tool being used
716
- """
717
- # Map tool names to user-friendly action descriptions
718
- tool_descriptions = {
719
- # RAG Tools
720
- "list_indexed_documents": "📚 Checking which documents are currently indexed",
721
- "query_documents": "🔍 Searching through indexed documents for relevant information",
722
- "query_specific_file": "📄 Searching within a specific document",
723
- "search_indexed_chunks": "🔎 Performing exact text search in indexed content",
724
- "index_document": "📥 Adding document to the knowledge base",
725
- "index_directory": "📁 Indexing all documents in a directory",
726
- "dump_document": "📝 Exporting document content for analysis",
727
- "summarize_document": "📋 Creating a summary of the document",
728
- "rag_status": "ℹ️ Retrieving RAG system status",
729
- # File System Tools
730
- "search_file": "🔍 Searching for files on your system",
731
- "search_directory": "📂 Looking for directories on your system",
732
- "search_file_content": "📝 Searching for content within files",
733
- "read_file": "📖 Reading file contents",
734
- "write_file": "✏️ Writing content to a file",
735
- "add_watch_directory": "👁️ Starting to monitor a directory for changes",
736
- # Shell Tools
737
- "run_shell_command": "💻 Executing shell command",
738
- # Default for unknown tools
739
- "default": "🔧 Executing operation",
740
- }
741
-
742
- # Get the description or use the tool name if not found
743
- action_desc = tool_descriptions.get(tool_name, tool_descriptions["default"])
744
-
745
- if self.rich_available:
746
- self.console.print(f"\n[bold blue]{action_desc}[/bold blue]")
747
- if action_desc == tool_descriptions["default"]:
748
- # If using default, also show the tool name
749
- self.console.print(f" [dim]Tool: {tool_name}[/dim]")
750
- else:
751
- print(f"\n{action_desc}")
752
- if action_desc == tool_descriptions["default"]:
753
- print(f" Tool: {tool_name}")
754
-
755
- def print_tool_complete(self) -> None:
756
- """Print that tool execution is complete."""
757
- if self.rich_available:
758
- self.console.print("[green]✅ Tool execution complete[/green]")
759
- else:
760
- print("✅ Tool execution complete")
761
-
762
- def print_error(self, error_message: str) -> None:
763
- """
764
- Print an error message with appropriate styling.
765
-
766
- Args:
767
- error_message: The error message to display
768
- """
769
- # Handle None error messages
770
- if error_message is None:
771
- error_message = "Unknown error occurred (received None)"
772
-
773
- if self.rich_available:
774
- self.console.print(
775
- Panel(str(error_message), title="⚠️ Error", border_style="red")
776
- )
777
- else:
778
- print(f"\n⚠️ ERROR: {error_message}\n")
779
-
780
- def print_info(self, message: str) -> None:
781
- """
782
- Print an information message.
783
-
784
- Args:
785
- message: The information message to display
786
- """
787
- if self.rich_available:
788
- self.console.print() # Add newline before
789
- self.console.print(Panel(message, title="ℹ️ Info", border_style="blue"))
790
- else:
791
- print(f"\nℹ️ INFO: {message}\n")
792
-
793
- def print_success(self, message: str) -> None:
794
- """
795
- Print a success message.
796
-
797
- Args:
798
- message: The success message to display
799
- """
800
- if self.rich_available:
801
- self.console.print() # Add newline before
802
- self.console.print(Panel(message, title="✅ Success", border_style="green"))
803
- else:
804
- print(f"\n✅ SUCCESS: {message}\n")
805
-
806
- def print_diff(self, diff: str, filename: str) -> None:
807
- """
808
- Print a code diff with syntax highlighting.
809
-
810
- Args:
811
- diff: The diff content to display
812
- filename: Name of the file being changed
813
- """
814
- if self.rich_available:
815
- from rich.syntax import Syntax
816
-
817
- self.console.print() # Add newline before
818
- diff_panel = Panel(
819
- Syntax(diff, "diff", theme="monokai", line_numbers=True),
820
- title=f"🔧 Changes to {filename}",
821
- border_style="yellow",
822
- )
823
- self.console.print(diff_panel)
824
- else:
825
- print(f"\n🔧 DIFF for {filename}:")
826
- print("=" * 50)
827
- print(diff)
828
- print("=" * 50 + "\n")
829
-
830
- def print_repeated_tool_warning(self) -> None:
831
- """Print a warning about repeated tool calls."""
832
- message = "Detected repetitive tool call pattern. Agent execution paused to avoid an infinite loop. Try adjusting your prompt or agent configuration if this persists."
833
-
834
- if self.rich_available:
835
- self.console.print(
836
- Panel(
837
- f"[bold yellow]{message}[/bold yellow]",
838
- title="⚠️ Warning",
839
- border_style="yellow",
840
- padding=(1, 2),
841
- highlight=True,
842
- )
843
- )
844
- else:
845
- print(f"\n⚠️ WARNING: {message}\n")
846
-
847
- def print_final_answer(
848
- self, answer: str, streaming: bool = True # pylint: disable=unused-argument
849
- ) -> None:
850
- """
851
- Print the final answer with appropriate styling.
852
-
853
- Args:
854
- answer: The final answer to display
855
- streaming: Not used (kept for compatibility)
856
- """
857
- if self.rich_available:
858
- self.console.print() # Add newline before
859
- self.console.print(
860
- Panel(answer, title="✅ Final Answer", border_style="green")
861
- )
862
- else:
863
- print(f"\n✅ FINAL ANSWER: {answer}\n")
864
-
865
- def print_completion(self, steps_taken: int, steps_limit: int) -> None:
866
- """
867
- Print completion information.
868
-
869
- Args:
870
- steps_taken: Number of steps taken
871
- steps_limit: Maximum number of steps allowed
872
- """
873
- self.print_separator()
874
-
875
- if steps_taken < steps_limit:
876
- # Completed successfully before hitting limit - clean message
877
- message = "✨ Processing complete!"
878
- else:
879
- # Hit the limit - show ratio to indicate incomplete
880
- message = f"⚠️ Processing stopped after {steps_taken}/{steps_limit} steps"
881
-
882
- if self.rich_available:
883
- self.console.print(f"[bold blue]{message}[/bold blue]")
884
- else:
885
- print(message)
886
-
887
- def print_prompt(self, prompt: str, title: str = "Prompt") -> None:
888
- """
889
- Print a prompt with appropriate styling for debugging.
890
-
891
- Args:
892
- prompt: The prompt to display
893
- title: Optional title for the panel
894
- """
895
- if self.rich_available:
896
- from rich.syntax import Syntax
897
-
898
- # Use plain text instead of markdown to avoid any parsing issues
899
- # and ensure the full content is displayed
900
- syntax = Syntax(prompt, "text", theme="monokai", line_numbers=False)
901
-
902
- # Use expand=False to prevent Rich from trying to fit to terminal width
903
- # This ensures the full prompt is shown even if it's very long
904
- self.console.print(
905
- Panel(
906
- syntax,
907
- title=f"🔍 {title}",
908
- border_style="cyan",
909
- padding=(1, 2),
910
- expand=False,
911
- )
912
- )
913
- else:
914
- print(f"\n🔍 {title}:\n{'-' * 80}\n{prompt}\n{'-' * 80}\n")
915
-
916
- def display_stats(self, stats: Dict[str, Any]) -> None:
917
- """
918
- Display LLM performance statistics or query execution stats.
919
-
920
- Args:
921
- stats: Dictionary containing performance statistics
922
- Can include: duration, steps_taken, total_tokens (query stats)
923
- Or: time_to_first_token, tokens_per_second, etc. (LLM stats)
924
- """
925
- if not stats:
926
- return
927
-
928
- # Check if we have query-level stats or LLM-level stats
929
- has_query_stats = any(
930
- key in stats for key in ["duration", "steps_taken", "total_tokens"]
931
- )
932
- has_llm_stats = any(
933
- key in stats for key in ["time_to_first_token", "tokens_per_second"]
934
- )
935
-
936
- # Skip if there's no meaningful stats
937
- if not has_query_stats and not has_llm_stats:
938
- return
939
-
940
- # Create a table for the stats
941
- title = "📊 Query Stats" if has_query_stats else "🚀 LLM Performance Stats"
942
- table = Table(
943
- title=title,
944
- show_header=True,
945
- header_style="bold cyan",
946
- )
947
- table.add_column("Metric", style="dim")
948
- table.add_column("Value", justify="right")
949
-
950
- # Add query-level stats (timing and steps)
951
- if "duration" in stats and stats["duration"] is not None:
952
- table.add_row("Duration", f"{stats['duration']:.2f}s")
953
-
954
- if "steps_taken" in stats and stats["steps_taken"] is not None:
955
- table.add_row("Steps", f"{stats['steps_taken']}")
956
-
957
- # Add LLM performance stats (timing)
958
- if "time_to_first_token" in stats and stats["time_to_first_token"] is not None:
959
- table.add_row("Time to First Token", f"{stats['time_to_first_token']:.2f}s")
960
-
961
- if "tokens_per_second" in stats and stats["tokens_per_second"] is not None:
962
- table.add_row("Tokens/Second", f"{stats['tokens_per_second']:.1f}")
963
-
964
- # Add token usage stats (always show in consistent format)
965
- if "input_tokens" in stats and stats["input_tokens"] is not None:
966
- table.add_row("Input Tokens", f"{stats['input_tokens']:,}")
967
-
968
- if "output_tokens" in stats and stats["output_tokens"] is not None:
969
- table.add_row("Output Tokens", f"{stats['output_tokens']:,}")
970
-
971
- if "total_tokens" in stats and stats["total_tokens"] is not None:
972
- table.add_row("Total Tokens", f"{stats['total_tokens']:,}")
973
-
974
- # Print the table in a panel
975
- self.console.print(Panel(table, border_style="blue"))
976
-
977
- def start_progress(self, message: str) -> None:
978
- """
979
- Start the progress indicator.
980
-
981
- Args:
982
- message: Message to display with the indicator
983
- """
984
- # If file preview is active, pause it temporarily
985
- self._paused_preview = False
986
- if self.file_preview_live is not None:
987
- try:
988
- self.file_preview_live.stop()
989
- self._paused_preview = True
990
- self.file_preview_live = None
991
- # Small delay to ensure clean transition
992
- time.sleep(0.05)
993
- except Exception:
994
- pass
995
-
996
- self.progress.start(message)
997
-
998
- def stop_progress(self) -> None:
999
- """Stop the progress indicator."""
1000
- self.progress.stop()
1001
-
1002
- # Ensure clean line separation after progress stops
1003
- if self.rich_available:
1004
- # Longer delay to ensure the transient display is FULLY cleared
1005
- time.sleep(0.15)
1006
- # Explicitly move to a new line
1007
- print() # Use print() instead of console.print() to avoid Live display conflicts
1008
-
1009
- # NOTE: Do NOT create Live display here - let update_file_preview() handle it
1010
- # This prevents double panels from appearing when both stop_progress and update_file_preview execute
1011
-
1012
- # Reset the paused flag
1013
- if hasattr(self, "_paused_preview"):
1014
- self._paused_preview = False
1015
-
1016
- def print_state_info(self, state_message: str):
1017
- """
1018
- Print the current execution state
1019
-
1020
- Args:
1021
- state_message: Message describing the current state
1022
- """
1023
- if self.rich_available:
1024
- self.console.print(
1025
- self.Panel(
1026
- f"🔄 [bold cyan]{state_message}[/bold cyan]",
1027
- border_style="cyan",
1028
- padding=(0, 1),
1029
- )
1030
- )
1031
- else:
1032
- print(f"🔄 STATE: {state_message}")
1033
-
1034
- def print_warning(self, warning_message: str):
1035
- """
1036
- Print a warning message
1037
-
1038
- Args:
1039
- warning_message: Warning message to display
1040
- """
1041
- if self.rich_available:
1042
- self.console.print() # Add newline before
1043
- self.console.print(
1044
- self.Panel(
1045
- f"⚠️ [bold yellow] {warning_message} [/bold yellow]",
1046
- border_style="yellow",
1047
- padding=(0, 1),
1048
- )
1049
- )
1050
- else:
1051
- print(f"⚠️ WARNING: {warning_message}")
1052
-
1053
- def print_streaming_text(
1054
- self, text_chunk: str, end_of_stream: bool = False
1055
- ) -> None:
1056
- """
1057
- Print text content as it streams in, without newlines between chunks.
1058
-
1059
- Args:
1060
- text_chunk: The chunk of text from the stream
1061
- end_of_stream: Whether this is the last chunk
1062
- """
1063
- # Accumulate text in the buffer
1064
- self.streaming_buffer += text_chunk
1065
-
1066
- # Print the chunk directly to console
1067
- if self.rich_available:
1068
- # Use low-level print to avoid adding newlines
1069
- print(text_chunk, end="", flush=True)
1070
- else:
1071
- print(text_chunk, end="", flush=True)
1072
-
1073
- # If this is the end of the stream, add a newline
1074
- if end_of_stream:
1075
- print()
1076
-
1077
- def get_streaming_buffer(self) -> str:
1078
- """
1079
- Get the accumulated streaming text and reset buffer.
1080
-
1081
- Returns:
1082
- The complete accumulated text from streaming
1083
- """
1084
- result = self.streaming_buffer
1085
- self.streaming_buffer = "" # Reset buffer
1086
- return result
1087
-
1088
- def print_response(self, response: str, title: str = "Response") -> None:
1089
- """
1090
- Print an LLM response with appropriate styling.
1091
-
1092
- Args:
1093
- response: The response text to display
1094
- title: Optional title for the panel
1095
- """
1096
- if self.rich_available:
1097
- from rich.syntax import Syntax
1098
-
1099
- syntax = Syntax(response, "markdown", theme="monokai", line_numbers=False)
1100
- self.console.print(
1101
- Panel(syntax, title=f"🤖 {title}", border_style="green", padding=(1, 2))
1102
- )
1103
- else:
1104
- print(f"\n🤖 {title}:\n{'-' * 80}\n{response}\n{'-' * 80}\n")
1105
-
1106
- def print_tool_info(self, name: str, params_str: str, description: str) -> None:
1107
- """
1108
- Print information about a tool with appropriate styling.
1109
-
1110
- Args:
1111
- name: Name of the tool
1112
- params_str: Formatted string of parameters
1113
- description: Tool description
1114
- """
1115
- if self.rich_available:
1116
- self.console.print(
1117
- f"[bold cyan]📌 {name}[/bold cyan]([italic]{params_str}[/italic])"
1118
- )
1119
- self.console.print(f" [dim]{description}[/dim]")
1120
- else:
1121
- print(f"\n📌 {name}({params_str})")
1122
- print(f" {description}")
1123
-
1124
- # === File Watcher Output Methods ===
1125
-
1126
- def print_file_created(
1127
- self, filename: str, size: int = 0, extension: str = ""
1128
- ) -> None:
1129
- """
1130
- Print file created notification with styling.
1131
-
1132
- Args:
1133
- filename: Name of the file
1134
- size: Size in bytes
1135
- extension: File extension
1136
- """
1137
- if self.rich_available:
1138
- self.console.print(
1139
- f"\n[bold green]📄 New file detected:[/bold green] [cyan]{filename}[/cyan]"
1140
- )
1141
- size_str = self._format_file_size(size)
1142
- self.console.print(f" [dim]Size:[/dim] {size_str}")
1143
- self.console.print(f" [dim]Type:[/dim] {extension or 'unknown'}")
1144
- else:
1145
- print(f"\n📄 New file detected: {filename}")
1146
- print(f" Size: {size} bytes")
1147
- print(f" Type: {extension or 'unknown'}")
1148
-
1149
- def print_file_modified(self, filename: str) -> None:
1150
- """
1151
- Print file modified notification.
1152
-
1153
- Args:
1154
- filename: Name of the file
1155
- """
1156
- if self.rich_available:
1157
- self.console.print(
1158
- f"\n[bold yellow]✏️ File modified:[/bold yellow] [cyan]{filename}[/cyan]"
1159
- )
1160
- else:
1161
- print(f"\n✏️ File modified: {filename}")
1162
-
1163
- def print_file_deleted(self, filename: str) -> None:
1164
- """
1165
- Print file deleted notification.
1166
-
1167
- Args:
1168
- filename: Name of the file
1169
- """
1170
- if self.rich_available:
1171
- self.console.print(
1172
- f"\n[bold red]🗑️ File deleted:[/bold red] [cyan]{filename}[/cyan]"
1173
- )
1174
- else:
1175
- print(f"\n🗑️ File deleted: {filename}")
1176
-
1177
- def print_file_moved(self, src_filename: str, dest_filename: str) -> None:
1178
- """
1179
- Print file moved notification.
1180
-
1181
- Args:
1182
- src_filename: Original filename
1183
- dest_filename: New filename
1184
- """
1185
- if self.rich_available:
1186
- self.console.print(
1187
- f"\n[bold magenta]📦 File moved:[/bold magenta] "
1188
- f"[cyan]{src_filename}[/cyan] → [cyan]{dest_filename}[/cyan]"
1189
- )
1190
- else:
1191
- print(f"\n📦 File moved: {src_filename} → {dest_filename}")
1192
-
1193
- def _format_file_size(self, size_bytes: int) -> str:
1194
- """Format file size in human-readable format."""
1195
- if size_bytes < 1024:
1196
- return f"{size_bytes} B"
1197
- elif size_bytes < 1024 * 1024:
1198
- return f"{size_bytes / 1024:.1f} KB"
1199
- elif size_bytes < 1024 * 1024 * 1024:
1200
- return f"{size_bytes / (1024 * 1024):.1f} MB"
1201
- else:
1202
- return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
1203
-
1204
- # === VLM/Model Progress Output Methods ===
1205
-
1206
- def print_model_loading(self, model_name: str) -> None:
1207
- """
1208
- Print model loading progress.
1209
-
1210
- Args:
1211
- model_name: Name of the model being loaded
1212
- """
1213
- if self.rich_available:
1214
- self.console.print(
1215
- f"[bold blue]🔄 Loading model:[/bold blue] [cyan]{model_name}[/cyan]..."
1216
- )
1217
- else:
1218
- print(f"🔄 Loading model: {model_name}...")
1219
-
1220
- def print_model_ready(self, model_name: str, already_loaded: bool = False) -> None:
1221
- """
1222
- Print model ready notification.
1223
-
1224
- Args:
1225
- model_name: Name of the model
1226
- already_loaded: If True, model was already loaded
1227
- """
1228
- status = "ready" if already_loaded else "loaded"
1229
- if self.rich_available:
1230
- self.console.print(
1231
- f"[bold green]✅ Model {status}:[/bold green] [cyan]{model_name}[/cyan]"
1232
- )
1233
- else:
1234
- print(f"✅ Model {status}: {model_name}")
1235
-
1236
- def print_extraction_start(
1237
- self, image_num: int, page_num: int, mime_type: str
1238
- ) -> None:
1239
- """
1240
- Print VLM extraction starting notification.
1241
-
1242
- Args:
1243
- image_num: Image number being processed
1244
- page_num: Page number (for PDFs)
1245
- mime_type: MIME type of the image
1246
- """
1247
- if self.rich_available:
1248
- self.console.print(
1249
- f" [dim]🔍 VLM extracting from image {image_num} "
1250
- f"on page {page_num} ({mime_type})...[/dim]"
1251
- )
1252
- else:
1253
- print(
1254
- f" 🔍 VLM extracting from image {image_num} "
1255
- f"on page {page_num} ({mime_type})..."
1256
- )
1257
-
1258
- def print_extraction_complete(
1259
- self, chars: int, image_num: int, elapsed_seconds: float, size_kb: float
1260
- ) -> None:
1261
- """
1262
- Print VLM extraction complete notification.
1263
-
1264
- Args:
1265
- chars: Number of characters extracted
1266
- image_num: Image number processed
1267
- elapsed_seconds: Time taken for extraction
1268
- size_kb: Image size in KB
1269
- """
1270
- if self.rich_available:
1271
- self.console.print(
1272
- f" [green]✅ Extracted {chars} chars from image {image_num} "
1273
- f"in {elapsed_seconds:.2f}s ({size_kb:.0f}KB image)[/green]"
1274
- )
1275
- else:
1276
- print(
1277
- f" ✅ Extracted {chars} chars from image {image_num} "
1278
- f"in {elapsed_seconds:.2f}s ({size_kb:.0f}KB image)"
1279
- )
1280
-
1281
- def print_ready_for_input(self) -> None:
1282
- """
1283
- Print a visual separator indicating ready for user input.
1284
-
1285
- Used after file processing completes to show the user
1286
- that the system is ready for commands.
1287
- """
1288
- if self.rich_available:
1289
- self.console.print()
1290
- self.console.print("─" * 80, style="dim")
1291
- self.console.print("> ", end="", style="bold green")
1292
- else:
1293
- print()
1294
- print("─" * 80)
1295
- print("> ", end="")
1296
-
1297
- # === Processing Pipeline Progress Methods ===
1298
-
1299
- def print_processing_step(
1300
- self,
1301
- step_num: int,
1302
- total_steps: int,
1303
- step_name: str,
1304
- status: str = "running",
1305
- ) -> None:
1306
- """
1307
- Print a processing step indicator with progress bar.
1308
-
1309
- Args:
1310
- step_num: Current step number (1-based)
1311
- total_steps: Total number of steps
1312
- step_name: Human-readable name of the current step
1313
- status: Step status - 'running', 'complete', 'error'
1314
- """
1315
- # Create a simple progress bar
1316
- progress_width = 20
1317
- completed = int((step_num - 1) / total_steps * progress_width)
1318
- current = 1 if step_num <= total_steps else 0
1319
- remaining = progress_width - completed - current
1320
-
1321
- if status == "complete":
1322
- bar = "█" * progress_width
1323
- elif status == "error":
1324
- bar = "█" * completed + "✗" + "░" * remaining
1325
- else:
1326
- bar = "█" * completed + "▶" * current + "░" * remaining
1327
-
1328
- # Status icon
1329
- icons = {
1330
- "running": "⏳",
1331
- "complete": "✅",
1332
- "error": "❌",
1333
- }
1334
- icon = icons.get(status, "⏳")
1335
-
1336
- if self.rich_available:
1337
- # Style based on status
1338
- if status == "complete":
1339
- style = "green"
1340
- elif status == "error":
1341
- style = "red"
1342
- else:
1343
- style = "cyan"
1344
-
1345
- self.console.print(
1346
- f" [{style}]{icon} [{step_num}/{total_steps}][/{style}] "
1347
- f"[dim]{bar}[/dim] [bold]{step_name}[/bold]"
1348
- )
1349
- else:
1350
- print(f" {icon} [{step_num}/{total_steps}] {bar} {step_name}")
1351
-
1352
- def print_processing_pipeline_start(self, filename: str, total_steps: int) -> None:
1353
- """
1354
- Print the start of a processing pipeline.
1355
-
1356
- Args:
1357
- filename: Name of the file being processed
1358
- total_steps: Total number of processing steps
1359
- """
1360
- if self.rich_available:
1361
- self.console.print()
1362
- self.console.print(
1363
- f"[bold cyan]⚙️ Processing Pipeline[/bold cyan] "
1364
- f"[dim]({total_steps} steps)[/dim]"
1365
- )
1366
- self.console.print(f" [dim]File:[/dim] [cyan]{filename}[/cyan]")
1367
- else:
1368
- print(f"\n⚙️ Processing Pipeline ({total_steps} steps)")
1369
- print(f" File: {filename}")
1370
-
1371
- def print_processing_pipeline_complete(
1372
- self,
1373
- filename: str, # pylint: disable=unused-argument
1374
- success: bool,
1375
- elapsed_seconds: float,
1376
- patient_name: str = None,
1377
- is_duplicate: bool = False,
1378
- ) -> None:
1379
- """
1380
- Print the completion of a processing pipeline.
1381
-
1382
- Args:
1383
- filename: Name of the file processed (kept for API consistency)
1384
- success: Whether processing was successful
1385
- elapsed_seconds: Total processing time
1386
- patient_name: Optional patient name for success message
1387
- is_duplicate: Whether this was a duplicate file (skipped)
1388
- """
1389
- if self.rich_available:
1390
- if is_duplicate:
1391
- msg = f"[bold yellow]⚡ Duplicate skipped[/bold yellow] in {elapsed_seconds:.1f}s"
1392
- if patient_name:
1393
- msg += f" [cyan]{patient_name}[/cyan] (already processed)"
1394
- self.console.print(msg)
1395
- elif success:
1396
- msg = f"[bold green]✅ Pipeline complete[/bold green] in {elapsed_seconds:.1f}s"
1397
- if patient_name:
1398
- msg += f" [cyan]{patient_name}[/cyan]"
1399
- self.console.print(msg)
1400
- else:
1401
- self.console.print(
1402
- f"[bold red]❌ Pipeline failed[/bold red] after {elapsed_seconds:.1f}s"
1403
- )
1404
- else:
1405
- if is_duplicate:
1406
- msg = f"⚡ Duplicate skipped in {elapsed_seconds:.1f}s"
1407
- if patient_name:
1408
- msg += f" → {patient_name} (already processed)"
1409
- print(msg)
1410
- elif success:
1411
- msg = f"✅ Pipeline complete in {elapsed_seconds:.1f}s"
1412
- if patient_name:
1413
- msg += f" → {patient_name}"
1414
- print(msg)
1415
- else:
1416
- print(f" Pipeline failed after {elapsed_seconds:.1f}s")
1417
-
1418
- # === File Preview Methods ===
1419
-
1420
- def start_file_preview(
1421
- self, filename: str, max_lines: int = 15, title_prefix: str = "📄"
1422
- ) -> None:
1423
- """
1424
- Start a live streaming file preview window.
1425
-
1426
- Args:
1427
- filename: Name of the file being generated
1428
- max_lines: Maximum number of lines to show (default: 15)
1429
- title_prefix: Emoji/prefix for the title (default: 📄)
1430
- """
1431
- # CRITICAL: Stop progress indicator if running to prevent overlapping Live displays
1432
- if self.progress.is_running:
1433
- self.stop_progress()
1434
-
1435
- # Stop any existing preview first to prevent stacking
1436
- if self.file_preview_live is not None:
1437
- try:
1438
- self.file_preview_live.stop()
1439
- except Exception:
1440
- pass # Ignore errors if already stopped
1441
- finally:
1442
- self.file_preview_live = None
1443
- # Small delay to ensure display cleanup
1444
- time.sleep(0.1)
1445
- # Ensure we're on a new line after stopping the previous preview
1446
- if self.rich_available:
1447
- self.console.print()
1448
-
1449
- # Reset state for new file
1450
- self.file_preview_filename = filename
1451
- self.file_preview_content = ""
1452
- self.file_preview_max_lines = max_lines
1453
-
1454
- if self.rich_available:
1455
- # DON'T start the live preview here - wait for first content
1456
- pass
1457
- else:
1458
- # For non-rich mode, just print a header
1459
- print(f"\n{title_prefix} Generating {filename}...")
1460
- print("=" * 80)
1461
-
1462
- def update_file_preview(self, content_chunk: str) -> None:
1463
- """
1464
- Update the live file preview with new content.
1465
-
1466
- Args:
1467
- content_chunk: New content to append to the preview
1468
- """
1469
- self.file_preview_content += content_chunk
1470
-
1471
- if self.rich_available:
1472
- # Only process if we have a filename set (preview has been started)
1473
- if not self.file_preview_filename:
1474
- return
1475
-
1476
- # Check if enough time has passed for throttling
1477
- current_time = time.time()
1478
- time_since_last_update = current_time - self._last_preview_update_time
1479
-
1480
- # Start the live preview on first content if not already started
1481
- if self.file_preview_live is None and self.file_preview_content:
1482
- preview = self._generate_file_preview_panel("📄")
1483
- self.file_preview_live = Live(
1484
- preview,
1485
- console=self.console,
1486
- refresh_per_second=4,
1487
- transient=False, # Keep False to prevent double rendering
1488
- )
1489
- self.file_preview_live.start()
1490
- self._last_preview_update_time = current_time
1491
- elif (
1492
- self.file_preview_live
1493
- and time_since_last_update >= self._preview_update_interval
1494
- ):
1495
- try:
1496
- # Update existing live display with new content
1497
- preview = self._generate_file_preview_panel("📄")
1498
- # Just update, don't force refresh
1499
- self.file_preview_live.update(preview)
1500
- self._last_preview_update_time = current_time
1501
- except Exception:
1502
- # If update fails, continue accumulating content
1503
- # (silently ignore preview update failures)
1504
- pass
1505
- else:
1506
- # For non-rich mode, print new content directly
1507
- print(content_chunk, end="", flush=True)
1508
-
1509
- def stop_file_preview(self) -> None:
1510
- """Stop the live file preview and show final summary."""
1511
- if self.rich_available:
1512
- # Only stop if it was started
1513
- if self.file_preview_live:
1514
- try:
1515
- self.file_preview_live.stop()
1516
- except Exception:
1517
- pass
1518
- finally:
1519
- self.file_preview_live = None
1520
-
1521
- # Show completion message only if we generated content
1522
- if self.file_preview_content:
1523
- total_lines = len(self.file_preview_content.splitlines())
1524
- self.console.print(
1525
- f"[green]✅ Generated {self.file_preview_filename} ({total_lines} lines)[/green]\n"
1526
- )
1527
- else:
1528
- print("\n" + "=" * 80)
1529
- total_lines = len(self.file_preview_content.splitlines())
1530
- print(f"✅ Generated {self.file_preview_filename} ({total_lines} lines)\n")
1531
-
1532
- # Reset state - IMPORTANT: Clear filename first to prevent updates
1533
- self.file_preview_filename = ""
1534
- self.file_preview_content = ""
1535
-
1536
- def _generate_file_preview_panel(self, title_prefix: str) -> Panel:
1537
- """
1538
- Generate a Rich Panel with the current file preview content.
1539
-
1540
- Args:
1541
- title_prefix: Emoji/prefix for the title
1542
-
1543
- Returns:
1544
- Rich Panel with syntax-highlighted content
1545
- """
1546
- lines = self.file_preview_content.splitlines()
1547
- total_lines = len(lines)
1548
-
1549
- # Truncate extremely long lines to prevent display issues
1550
- truncated_lines = []
1551
- for line in lines:
1552
- if len(line) > MAX_DISPLAY_LINE_LENGTH:
1553
- truncated_lines.append(line[:MAX_DISPLAY_LINE_LENGTH] + "...")
1554
- else:
1555
- truncated_lines.append(line)
1556
-
1557
- # Show last N lines
1558
- if total_lines <= self.file_preview_max_lines:
1559
- preview_lines = truncated_lines
1560
- line_info = f"All {total_lines} lines"
1561
- else:
1562
- preview_lines = truncated_lines[-self.file_preview_max_lines :]
1563
- line_info = f"Last {self.file_preview_max_lines} of {total_lines} lines"
1564
-
1565
- # Determine syntax highlighting
1566
- ext = (
1567
- self.file_preview_filename.split(".")[-1]
1568
- if "." in self.file_preview_filename
1569
- else "txt"
1570
- )
1571
- syntax_map = {
1572
- "py": "python",
1573
- "js": "javascript",
1574
- "ts": "typescript",
1575
- "jsx": "jsx",
1576
- "tsx": "tsx",
1577
- "json": "json",
1578
- "md": "markdown",
1579
- "yml": "yaml",
1580
- "yaml": "yaml",
1581
- "toml": "toml",
1582
- "ini": "ini",
1583
- "sh": "bash",
1584
- "bash": "bash",
1585
- "ps1": "powershell",
1586
- "sql": "sql",
1587
- "html": "html",
1588
- "css": "css",
1589
- "xml": "xml",
1590
- "c": "c",
1591
- "cpp": "cpp",
1592
- "java": "java",
1593
- "go": "go",
1594
- "rs": "rust",
1595
- }
1596
- syntax_lang = syntax_map.get(ext.lower(), "text")
1597
-
1598
- # Create syntax-highlighted preview
1599
- preview_content = (
1600
- "\n".join(preview_lines) if preview_lines else "[dim]Generating...[/dim]"
1601
- )
1602
-
1603
- if preview_lines:
1604
- # Calculate starting line number for the preview
1605
- if total_lines <= self.file_preview_max_lines:
1606
- start_line = 1
1607
- else:
1608
- start_line = total_lines - self.file_preview_max_lines + 1
1609
-
1610
- syntax = Syntax(
1611
- preview_content,
1612
- syntax_lang,
1613
- theme="monokai",
1614
- line_numbers=True,
1615
- start_line=start_line,
1616
- word_wrap=False, # Prevent line wrapping that causes display issues
1617
- )
1618
- else:
1619
- syntax = preview_content
1620
-
1621
- return Panel(
1622
- syntax,
1623
- title=f"{title_prefix} {self.file_preview_filename} ({line_info})",
1624
- border_style="cyan",
1625
- padding=(1, 2),
1626
- )
1627
-
1628
-
1629
- class SilentConsole(OutputHandler):
1630
- """
1631
- A silent console that suppresses all output for JSON-only mode.
1632
- Provides the same interface as AgentConsole but with no-op methods.
1633
- Implements OutputHandler for silent/suppressed output.
1634
- """
1635
-
1636
- def __init__(self, silence_final_answer: bool = False):
1637
- """Initialize the silent console.
1638
-
1639
- Args:
1640
- silence_final_answer: If True, suppress even the final answer (for JSON-only mode)
1641
- """
1642
- self.streaming_buffer = "" # Maintain compatibility
1643
- self.silence_final_answer = silence_final_answer
1644
-
1645
- # Implementation of OutputHandler abstract methods - all no-ops
1646
- def print_final_answer(
1647
- self, answer: str, streaming: bool = True # pylint: disable=unused-argument
1648
- ) -> None:
1649
- """
1650
- Print the final answer.
1651
- Only suppressed if silence_final_answer is True.
1652
-
1653
- Args:
1654
- answer: The final answer to display
1655
- streaming: Not used (kept for compatibility)
1656
- """
1657
- if self.silence_final_answer:
1658
- return # Completely silent
1659
-
1660
- # Print the final answer directly
1661
- print(f"\n🧠 gaia: {answer}")
1662
-
1663
- def display_stats(self, stats: Dict[str, Any]) -> None:
1664
- """
1665
- Display stats even in silent mode (since explicitly requested).
1666
- Uses the same Rich table format as AgentConsole.
1667
-
1668
- Args:
1669
- stats: Dictionary containing performance statistics
1670
- """
1671
- if not stats:
1672
- return
1673
-
1674
- # Check if we have query-level stats or LLM-level stats
1675
- has_query_stats = any(
1676
- key in stats for key in ["duration", "steps_taken", "total_tokens"]
1677
- )
1678
- has_llm_stats = any(
1679
- key in stats for key in ["time_to_first_token", "tokens_per_second"]
1680
- )
1681
-
1682
- # Skip if there's no meaningful stats
1683
- if not has_query_stats and not has_llm_stats:
1684
- return
1685
-
1686
- # Use Rich table format (same as AgentConsole)
1687
- from rich.console import Console
1688
- from rich.panel import Panel
1689
- from rich.table import Table
1690
-
1691
- console = Console()
1692
-
1693
- title = "📊 Query Stats" if has_query_stats else "🚀 LLM Performance Stats"
1694
- table = Table(
1695
- title=title,
1696
- show_header=True,
1697
- header_style="bold cyan",
1698
- )
1699
- table.add_column("Metric", style="dim")
1700
- table.add_column("Value", justify="right")
1701
-
1702
- # Add query-level stats (timing and steps)
1703
- if "duration" in stats and stats["duration"] is not None:
1704
- table.add_row("Duration", f"{stats['duration']:.2f}s")
1705
-
1706
- if "steps_taken" in stats and stats["steps_taken"] is not None:
1707
- table.add_row("Steps", f"{stats['steps_taken']}")
1708
-
1709
- # Add LLM performance stats (timing)
1710
- if "time_to_first_token" in stats and stats["time_to_first_token"] is not None:
1711
- table.add_row("Time to First Token", f"{stats['time_to_first_token']:.2f}s")
1712
-
1713
- if "tokens_per_second" in stats and stats["tokens_per_second"] is not None:
1714
- table.add_row("Tokens/Second", f"{stats['tokens_per_second']:.1f}")
1715
-
1716
- # Add token usage stats (always show in consistent format)
1717
- if "input_tokens" in stats and stats["input_tokens"] is not None:
1718
- table.add_row("Input Tokens", f"{stats['input_tokens']:,}")
1719
-
1720
- if "output_tokens" in stats and stats["output_tokens"] is not None:
1721
- table.add_row("Output Tokens", f"{stats['output_tokens']:,}")
1722
-
1723
- if "total_tokens" in stats and stats["total_tokens"] is not None:
1724
- table.add_row("Total Tokens", f"{stats['total_tokens']:,}")
1725
-
1726
- # Print the table in a panel
1727
- console.print(Panel(table, border_style="blue"))
1728
-
1729
- # All other abstract methods as no-ops
1730
- def print_processing_start(self, query: str, max_steps: int):
1731
- """No-op implementation."""
1732
-
1733
- def print_step_header(self, step_num: int, step_limit: int):
1734
- """No-op implementation."""
1735
-
1736
- def print_state_info(self, state_message: str):
1737
- """No-op implementation."""
1738
-
1739
- def print_thought(self, thought: str):
1740
- """No-op implementation."""
1741
-
1742
- def print_goal(self, goal: str):
1743
- """No-op implementation."""
1744
-
1745
- def print_plan(self, plan: List[Any], current_step: int = None):
1746
- """No-op implementation."""
1747
-
1748
- def print_step_paused(self, description: str):
1749
- """No-op implementation."""
1750
-
1751
- def print_checklist(self, items: List[Any], current_idx: int):
1752
- """No-op implementation."""
1753
-
1754
- def print_checklist_reasoning(self, reasoning: str):
1755
- """No-op implementation."""
1756
-
1757
- def print_command_executing(self, command: str):
1758
- """No-op implementation."""
1759
-
1760
- def print_agent_selected(self, agent_name: str, language: str, project_type: str):
1761
- """No-op implementation."""
1762
-
1763
- def print_tool_usage(self, tool_name: str):
1764
- """No-op implementation."""
1765
-
1766
- def print_tool_complete(self):
1767
- """No-op implementation."""
1768
-
1769
- def pretty_print_json(self, data: Dict[str, Any], title: str = None):
1770
- """No-op implementation."""
1771
-
1772
- def print_error(self, error_message: str):
1773
- """No-op implementation."""
1774
-
1775
- def print_warning(self, warning_message: str):
1776
- """No-op implementation."""
1777
-
1778
- def print_info(self, message: str):
1779
- """No-op implementation."""
1780
-
1781
- def start_progress(self, message: str):
1782
- """No-op implementation."""
1783
-
1784
- def stop_progress(self):
1785
- """No-op implementation."""
1786
-
1787
- def print_repeated_tool_warning(self):
1788
- """No-op implementation."""
1789
-
1790
- def print_completion(self, steps_taken: int, steps_limit: int):
1791
- """No-op implementation."""
1792
-
1793
- def print_success(self, message: str):
1794
- """No-op implementation."""
1795
-
1796
- def print_file_created(self, filename: str, size: int = 0, extension: str = ""):
1797
- """No-op implementation."""
1798
-
1799
- def print_file_modified(self, filename: str, size: int = 0):
1800
- """No-op implementation."""
1801
-
1802
- def print_file_deleted(self, filename: str):
1803
- """No-op implementation."""
1804
-
1805
- def print_file_moved(self, src_filename: str, dest_filename: str):
1806
- """No-op implementation."""
1807
-
1808
- def print_model_loading(self, model_name: str):
1809
- """No-op implementation."""
1810
-
1811
- def print_model_ready(self, model_name: str, already_loaded: bool = False):
1812
- """No-op implementation."""
1813
-
1814
- def print_extraction_start(self, image_num: int, page_num: int, mime_type: str):
1815
- """No-op implementation."""
1816
-
1817
- def print_extraction_complete(
1818
- self, chars: int, image_num: int, elapsed_seconds: float, size_kb: float
1819
- ):
1820
- """No-op implementation."""
1821
-
1822
- def print_ready_for_input(self):
1823
- """No-op implementation."""
1824
-
1825
- def print_processing_step(
1826
- self, step_num: int, total_steps: int, step_name: str, status: str = "running"
1827
- ):
1828
- """No-op implementation."""
1829
-
1830
- def print_processing_pipeline_start(self, filename: str, total_steps: int):
1831
- """No-op implementation."""
1832
-
1833
- def print_processing_pipeline_complete(
1834
- self,
1835
- filename: str,
1836
- success: bool,
1837
- elapsed_seconds: float,
1838
- patient_name: str = None,
1839
- is_duplicate: bool = False,
1840
- ):
1841
- """No-op implementation."""
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import json
5
+ import threading
6
+ import time
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ # Import Rich library for pretty printing and syntax highlighting
11
+ try:
12
+ from rich import print as rprint
13
+ from rich.console import Console
14
+ from rich.live import Live
15
+ from rich.panel import Panel
16
+ from rich.spinner import Spinner
17
+ from rich.syntax import Syntax
18
+ from rich.table import Table
19
+
20
+ RICH_AVAILABLE = True
21
+ except ImportError:
22
+ RICH_AVAILABLE = False
23
+ print(
24
+ "Rich library not found. Install with 'uv pip install rich' for syntax highlighting."
25
+ )
26
+
27
+ # Display configuration constants
28
+ MAX_DISPLAY_LINE_LENGTH = 120
29
+
30
+
31
+ # ANSI Color Codes for fallback when Rich is not available
32
+ ANSI_RESET = "\033[0m"
33
+ ANSI_BOLD = "\033[1m"
34
+ ANSI_DIM = "\033[90m" # Dark Gray
35
+ ANSI_RED = "\033[91m"
36
+ ANSI_GREEN = "\033[92m"
37
+ ANSI_YELLOW = "\033[93m"
38
+ ANSI_BLUE = "\033[94m"
39
+ ANSI_MAGENTA = "\033[95m"
40
+ ANSI_CYAN = "\033[96m"
41
+
42
+
43
+ class OutputHandler(ABC):
44
+ """
45
+ Abstract base class for handling agent output.
46
+
47
+ Defines the minimal interface that agents use to report their progress.
48
+ Each implementation handles the output differently:
49
+ - AgentConsole: Rich console output for CLI
50
+ - SilentConsole: Suppressed output for testing
51
+ - SSEOutputHandler: Server-Sent Events for API streaming
52
+
53
+ This interface focuses on WHAT agents need to report, not HOW
54
+ each handler chooses to display it.
55
+ """
56
+
57
+ # === Core Progress/State Methods (Required) ===
58
+
59
+ @abstractmethod
60
+ def print_processing_start(self, query: str, max_steps: int):
61
+ """Print processing start message."""
62
+ ...
63
+
64
+ @abstractmethod
65
+ def print_step_header(self, step_num: int, step_limit: int):
66
+ """Print step header."""
67
+ ...
68
+
69
+ @abstractmethod
70
+ def print_state_info(self, state_message: str):
71
+ """Print current execution state."""
72
+ ...
73
+
74
+ @abstractmethod
75
+ def print_thought(self, thought: str):
76
+ """Print agent's reasoning/thought."""
77
+ ...
78
+
79
+ @abstractmethod
80
+ def print_goal(self, goal: str):
81
+ """Print agent's current goal."""
82
+ ...
83
+
84
+ @abstractmethod
85
+ def print_plan(self, plan: List[Any], current_step: int = None):
86
+ """Print agent's plan with optional current step highlight."""
87
+ ...
88
+
89
+ # === Tool Execution Methods (Required) ===
90
+
91
+ @abstractmethod
92
+ def print_tool_usage(self, tool_name: str):
93
+ """Print tool being called."""
94
+ ...
95
+
96
+ @abstractmethod
97
+ def print_tool_complete(self):
98
+ """Print tool completion."""
99
+ ...
100
+
101
+ @abstractmethod
102
+ def pretty_print_json(self, data: Dict[str, Any], title: str = None):
103
+ """Print JSON data (tool args/results)."""
104
+ ...
105
+
106
+ # === Status Messages (Required) ===
107
+
108
+ @abstractmethod
109
+ def print_error(self, error_message: str):
110
+ """Print error message."""
111
+ ...
112
+
113
+ @abstractmethod
114
+ def print_warning(self, warning_message: str):
115
+ """Print warning message."""
116
+ ...
117
+
118
+ @abstractmethod
119
+ def print_info(self, message: str):
120
+ """Print informational message."""
121
+ ...
122
+
123
+ # === Progress Indicators (Required) ===
124
+
125
+ @abstractmethod
126
+ def start_progress(self, message: str):
127
+ """Start progress indicator."""
128
+ ...
129
+
130
+ @abstractmethod
131
+ def stop_progress(self):
132
+ """Stop progress indicator."""
133
+ ...
134
+
135
+ # === Completion Methods (Required) ===
136
+
137
+ @abstractmethod
138
+ def print_final_answer(self, answer: str):
139
+ """Print final answer/result."""
140
+ ...
141
+
142
+ @abstractmethod
143
+ def print_repeated_tool_warning(self):
144
+ """Print warning about repeated tool calls (loop detection)."""
145
+ ...
146
+
147
+ @abstractmethod
148
+ def print_completion(self, steps_taken: int, steps_limit: int):
149
+ """Print completion summary."""
150
+ ...
151
+
152
+ @abstractmethod
153
+ def print_step_paused(self, description: str):
154
+ """Print step paused message."""
155
+ ...
156
+
157
+ @abstractmethod
158
+ def print_command_executing(self, command: str):
159
+ """Print command executing message."""
160
+ ...
161
+
162
+ @abstractmethod
163
+ def print_agent_selected(self, agent_name: str, language: str, project_type: str):
164
+ """Print agent selected message."""
165
+ ...
166
+
167
+ # === Optional Methods (with default no-op implementations) ===
168
+
169
+ def print_prompt(
170
+ self, prompt: str, title: str = "Prompt"
171
+ ): # pylint: disable=unused-argument
172
+ """Print prompt (for debugging). Optional - default no-op."""
173
+ ...
174
+
175
+ def print_response(
176
+ self, response: str, title: str = "Response"
177
+ ): # pylint: disable=unused-argument
178
+ """Print response (for debugging). Optional - default no-op."""
179
+ ...
180
+
181
+ def print_streaming_text(
182
+ self, text_chunk: str, end_of_stream: bool = False
183
+ ): # pylint: disable=unused-argument
184
+ """Print streaming text. Optional - default no-op."""
185
+ ...
186
+
187
+ def display_stats(self, stats: Dict[str, Any]): # pylint: disable=unused-argument
188
+ """Display performance statistics. Optional - default no-op."""
189
+ ...
190
+
191
+ def print_header(self, text: str): # pylint: disable=unused-argument
192
+ """Print header. Optional - default no-op."""
193
+ ...
194
+
195
+ def print_separator(self, length: int = 50): # pylint: disable=unused-argument
196
+ """Print separator. Optional - default no-op."""
197
+ ...
198
+
199
+ def print_tool_info(
200
+ self, name: str, params_str: str, description: str
201
+ ): # pylint: disable=unused-argument
202
+ """Print tool info. Optional - default no-op."""
203
+ ...
204
+
205
+
206
+ class ProgressIndicator:
207
+ """A simple progress indicator that shows a spinner or dots animation."""
208
+
209
+ def __init__(self, message="Processing"):
210
+ """Initialize the progress indicator.
211
+
212
+ Args:
213
+ message: The message to display before the animation
214
+ """
215
+ self.message = message
216
+ self.is_running = False
217
+ self.thread = None
218
+ self.spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
219
+ self.dot_chars = [".", "..", "..."]
220
+ self.spinner_idx = 0
221
+ self.dot_idx = 0
222
+ self.rich_spinner = None
223
+ if RICH_AVAILABLE:
224
+ self.rich_spinner = Spinner("dots", text=message)
225
+ self.live = None
226
+
227
+ def _animate(self):
228
+ """Animation loop that runs in a separate thread."""
229
+ while self.is_running:
230
+ if RICH_AVAILABLE:
231
+ # Rich handles the animation internally
232
+ time.sleep(0.1)
233
+ else:
234
+ # Simple terminal-based animation
235
+ self.dot_idx = (self.dot_idx + 1) % len(self.dot_chars)
236
+ self.spinner_idx = (self.spinner_idx + 1) % len(self.spinner_chars)
237
+
238
+ # Determine if we should use Unicode spinner or simple dots
239
+ try:
240
+ # Try to print a Unicode character to see if the terminal supports it
241
+ print(self.spinner_chars[0], end="", flush=True)
242
+ print(
243
+ "\b", end="", flush=True
244
+ ) # Backspace to remove the test character
245
+
246
+ # If we got here, Unicode is supported
247
+ print(
248
+ f"\r{self.message} {self.spinner_chars[self.spinner_idx]}",
249
+ end="",
250
+ flush=True,
251
+ )
252
+ except (UnicodeError, OSError):
253
+ # Fallback to simple dots
254
+ print(
255
+ f"\r{self.message}{self.dot_chars[self.dot_idx]}",
256
+ end="",
257
+ flush=True,
258
+ )
259
+
260
+ time.sleep(0.1)
261
+
262
+ def start(self, message=None):
263
+ """Start the progress indicator.
264
+
265
+ Args:
266
+ message: Optional new message to display
267
+ """
268
+ if message:
269
+ self.message = message
270
+
271
+ if self.is_running:
272
+ return
273
+
274
+ self.is_running = True
275
+
276
+ if RICH_AVAILABLE:
277
+ if self.rich_spinner:
278
+ self.rich_spinner.text = self.message
279
+ # Use transient=True to auto-clear when done
280
+ self.live = Live(
281
+ self.rich_spinner, refresh_per_second=10, transient=True
282
+ )
283
+ self.live.start()
284
+ else:
285
+ self.thread = threading.Thread(target=self._animate)
286
+ self.thread.daemon = True
287
+ self.thread.start()
288
+
289
+ def stop(self):
290
+ """Stop the progress indicator."""
291
+ if not self.is_running:
292
+ return
293
+
294
+ self.is_running = False
295
+
296
+ if RICH_AVAILABLE and self.live:
297
+ self.live.stop()
298
+ elif self.thread:
299
+ self.thread.join(timeout=0.2)
300
+ # Clear the animation line
301
+ print("\r" + " " * (len(self.message) + 5) + "\r", end="", flush=True)
302
+
303
+
304
+ class AgentConsole(OutputHandler):
305
+ """
306
+ A class to handle all display-related functionality for the agent.
307
+ Provides rich text formatting and progress indicators when available.
308
+ Implements OutputHandler for CLI-based output.
309
+ """
310
+
311
+ def __init__(self):
312
+ """Initialize the AgentConsole with appropriate display capabilities."""
313
+ self.rich_available = RICH_AVAILABLE
314
+ self.console = Console() if self.rich_available else None
315
+ self.progress = ProgressIndicator()
316
+ self.rprint = rprint
317
+ self.Panel = Panel
318
+ self.streaming_buffer = "" # Buffer for accumulating streaming text
319
+ self.file_preview_live: Optional[Live] = None
320
+ self.file_preview_content = ""
321
+ self.file_preview_filename = ""
322
+ self.file_preview_max_lines = 15
323
+ self._paused_preview = False # Track if preview was paused for progress
324
+ self._last_preview_update_time = 0 # Throttle preview updates
325
+ self._preview_update_interval = 0.25 # Minimum seconds between updates
326
+
327
+ def print(self, *args, **kwargs):
328
+ """
329
+ Print method that delegates to Rich Console or standard print.
330
+
331
+ This allows code to call console.print() directly on AgentConsole instances.
332
+
333
+ Args:
334
+ *args: Arguments to print
335
+ **kwargs: Keyword arguments (style, etc.) for Rich Console
336
+ """
337
+ if self.rich_available and self.console:
338
+ self.console.print(*args, **kwargs)
339
+ else:
340
+ # Fallback to standard print
341
+ print(*args, **kwargs)
342
+
343
+ # Implementation of OutputHandler abstract methods
344
+
345
+ def pretty_print_json(self, data: Dict[str, Any], title: str = None) -> None:
346
+ """
347
+ Pretty print JSON data with syntax highlighting if Rich is available.
348
+ If data contains a "command" field, shows it prominently.
349
+
350
+ Args:
351
+ data: Dictionary data to print
352
+ title: Optional title for the panel
353
+ """
354
+
355
+ def _safe_default(obj: Any) -> Any:
356
+ """
357
+ JSON serializer fallback that handles common non-serializable types like numpy scalars/arrays.
358
+ """
359
+ try:
360
+ import numpy as np # Local import to avoid hard dependency at module import time
361
+
362
+ if isinstance(obj, np.generic):
363
+ return obj.item()
364
+ if isinstance(obj, np.ndarray):
365
+ return obj.tolist()
366
+ except Exception:
367
+ pass
368
+
369
+ for caster in (float, int, str):
370
+ try:
371
+ return caster(obj)
372
+ except Exception:
373
+ continue
374
+ return "<non-serializable>"
375
+
376
+ if self.rich_available:
377
+ # Check if this is a command execution result
378
+ if "command" in data and "stdout" in data:
379
+ # Show command execution in a special format
380
+ command = data.get("command", "")
381
+ stdout = data.get("stdout", "")
382
+ stderr = data.get("stderr", "")
383
+ return_code = data.get("return_code", 0)
384
+
385
+ # Build preview text
386
+ preview = f"$ {command}\n\n"
387
+ if stdout:
388
+ preview += stdout[:500] # First 500 chars
389
+ if len(stdout) > 500:
390
+ preview += "\n... (output truncated)"
391
+ if stderr:
392
+ preview += f"\n\nSTDERR:\n{stderr[:200]}"
393
+ if return_code != 0:
394
+ preview += f"\n\n[Return code: {return_code}]"
395
+
396
+ self.console.print(
397
+ Panel(
398
+ preview,
399
+ title=title or "Command Output",
400
+ border_style="blue",
401
+ expand=False,
402
+ )
403
+ )
404
+ else:
405
+ # Regular JSON output
406
+ # Convert to formatted JSON string with safe fallback for non-serializable types (e.g., numpy.float32)
407
+ print(data)
408
+ try:
409
+ json_str = json.dumps(data, indent=2)
410
+ except TypeError:
411
+ json_str = json.dumps(data, indent=2, default=_safe_default)
412
+
413
+ # Create a syntax object with JSON highlighting
414
+ syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
415
+ # Create a panel with a title if provided
416
+ if title:
417
+ self.console.print(Panel(syntax, title=title, border_style="blue"))
418
+ else:
419
+ self.console.print(syntax)
420
+ else:
421
+ # Fallback to standard pretty printing without highlighting
422
+ if title:
423
+ print(f"\n--- {title} ---")
424
+ # Check if this is a command execution
425
+ if "command" in data and "stdout" in data:
426
+ print(f"\n$ {data.get('command', '')}")
427
+ stdout = data.get("stdout", "")
428
+ if stdout:
429
+ print(stdout[:500])
430
+ if len(stdout) > 500:
431
+ print("... (output truncated)")
432
+ else:
433
+ try:
434
+ print(json.dumps(data, indent=2))
435
+ except TypeError:
436
+ print(json.dumps(data, indent=2, default=_safe_default))
437
+
438
+ def print_header(self, text: str) -> None:
439
+ """
440
+ Print a header with appropriate styling.
441
+
442
+ Args:
443
+ text: The header text to display
444
+ """
445
+ if self.rich_available:
446
+ self.console.print(f"\n[bold blue]{text}[/bold blue]")
447
+ else:
448
+ print(f"\n{text}")
449
+
450
+ def print_step_paused(self, description: str) -> None:
451
+ """
452
+ Print step paused message.
453
+
454
+ Args:
455
+ description: Description of the step being paused after
456
+ """
457
+ if self.rich_available:
458
+ self.console.print(
459
+ f"\n[bold yellow]⏸️ Paused after step:[/bold yellow] {description}"
460
+ )
461
+ self.console.print("Press Enter to continue, or 'n'/'q' to stop...")
462
+ else:
463
+ print(f"\n⏸️ Paused after step: {description}")
464
+ print("Press Enter to continue, or 'n'/'q' to stop...")
465
+
466
+ def print_processing_start(self, query: str, max_steps: int) -> None:
467
+ """
468
+ Print the initial processing message.
469
+
470
+ Args:
471
+ query: The user query being processed
472
+ max_steps: Maximum number of steps allowed (kept for API compatibility)
473
+ """
474
+ if self.rich_available:
475
+ self.console.print(f"\n[bold blue]🤖 Processing:[/bold blue] '{query}'")
476
+ self.console.print("=" * 50)
477
+ self.console.print()
478
+ else:
479
+ print(f"\n🤖 Processing: '{query}'")
480
+ print("=" * 50)
481
+ print()
482
+
483
+ def print_separator(self, length: int = 50) -> None:
484
+ """
485
+ Print a separator line.
486
+
487
+ Args:
488
+ length: Length of the separator line
489
+ """
490
+ if self.rich_available:
491
+ self.console.print("=" * length, style="dim")
492
+ else:
493
+ print("=" * length)
494
+
495
+ def print_step_header(self, step_num: int, step_limit: int) -> None:
496
+ """
497
+ Print a step header.
498
+
499
+ Args:
500
+ step_num: Current step number
501
+ step_limit: Maximum number of steps (unused, kept for compatibility)
502
+ """
503
+ _ = step_limit # Mark as intentionally unused
504
+ if self.rich_available:
505
+ self.console.print(
506
+ f"\n[bold cyan]📝 Step {step_num}:[/bold cyan] Thinking...",
507
+ highlight=False,
508
+ )
509
+ else:
510
+ print(f"\n📝 Step {step_num}: Thinking...")
511
+
512
+ def print_thought(self, thought: str) -> None:
513
+ """
514
+ Print the agent's thought with appropriate styling.
515
+
516
+ Args:
517
+ thought: The thought to display
518
+ """
519
+ if self.rich_available:
520
+ self.console.print(f"[bold green]🧠 Thought:[/bold green] {thought}")
521
+ else:
522
+ print(f"🧠 Thought: {thought}")
523
+
524
+ def print_goal(self, goal: str) -> None:
525
+ """
526
+ Print the agent's goal with appropriate styling.
527
+
528
+ Args:
529
+ goal: The goal to display
530
+ """
531
+ if self.rich_available:
532
+ self.console.print(f"[bold yellow]🎯 Goal:[/bold yellow] {goal}")
533
+ else:
534
+ print(f"🎯 Goal: {goal}")
535
+
536
+ def print_plan(self, plan: List[Any], current_step: int = None) -> None:
537
+ """
538
+ Print the agent's plan with appropriate styling.
539
+
540
+ Args:
541
+ plan: List of plan steps
542
+ current_step: Optional index of the current step being executed (0-based)
543
+ """
544
+ if self.rich_available:
545
+ self.console.print("\n[bold magenta]📋 Plan:[/bold magenta]")
546
+ for i, step in enumerate(plan):
547
+ step_text = step
548
+ # Convert dict steps to string representation if needed
549
+ if isinstance(step, dict):
550
+ if "tool" in step and "tool_args" in step:
551
+ args_str = json.dumps(step["tool_args"], sort_keys=True)
552
+ step_text = f"Use tool '{step['tool']}' with args: {args_str}"
553
+ else:
554
+ step_text = json.dumps(step)
555
+
556
+ # Highlight the current step being executed
557
+ if current_step is not None and i == current_step:
558
+ self.console.print(
559
+ f" [dim]{i+1}.[/dim] [bold green]►[/bold green] [bold yellow]{step_text}[/bold yellow] [bold green]◄[/bold green] [cyan](current step)[/cyan]"
560
+ )
561
+ else:
562
+ self.console.print(f" [dim]{i+1}.[/dim] {step_text}")
563
+ # Add an extra newline for better readability
564
+ self.console.print("")
565
+ else:
566
+ print("\n📋 Plan:")
567
+ for i, step in enumerate(plan):
568
+ step_text = step
569
+ # Convert dict steps to string representation if needed
570
+ if isinstance(step, dict):
571
+ if "tool" in step and "tool_args" in step:
572
+ args_str = json.dumps(step["tool_args"], sort_keys=True)
573
+ step_text = f"Use tool '{step['tool']}' with args: {args_str}"
574
+ else:
575
+ step_text = json.dumps(step)
576
+
577
+ # Highlight the current step being executed
578
+ if current_step is not None and i == current_step:
579
+ print(f" {i+1}. ► {step_text} ◄ (current step)")
580
+ else:
581
+ print(f" {i+1}. {step_text}")
582
+
583
+ def print_plan_progress(
584
+ self, current_step: int, total_steps: int, completed_steps: int = None
585
+ ):
586
+ """
587
+ Print progress in plan execution
588
+
589
+ Args:
590
+ current_step: Current step being executed (1-based)
591
+ total_steps: Total number of steps in the plan
592
+ completed_steps: Optional number of already completed steps
593
+ """
594
+ if completed_steps is None:
595
+ completed_steps = current_step - 1
596
+
597
+ progress_str = f"[Step {current_step}/{total_steps}]"
598
+ progress_bar = ""
599
+
600
+ # Create a simple progress bar
601
+ if total_steps > 0:
602
+ bar_width = 20
603
+ completed_chars = int((completed_steps / total_steps) * bar_width)
604
+ current_char = 1 if current_step <= total_steps else 0
605
+ remaining_chars = bar_width - completed_chars - current_char
606
+
607
+ progress_bar = (
608
+ "█" * completed_chars + "▶" * current_char + "░" * remaining_chars
609
+ )
610
+
611
+ if self.rich_available:
612
+ self.rprint(f"[cyan]{progress_str}[/cyan] {progress_bar}")
613
+ else:
614
+ print(f"{progress_str} {progress_bar}")
615
+
616
+ def print_checklist(self, items: List[Any], current_idx: int) -> None:
617
+ """Print the checklist with current item highlighted.
618
+
619
+ Args:
620
+ items: List of checklist items (must have .description attribute)
621
+ current_idx: Index of the item currently being executed (0-based)
622
+ """
623
+ if self.rich_available:
624
+ self.console.print("\n[bold magenta]📋 EXECUTION PLAN[/bold magenta]")
625
+ self.console.print("=" * 60, style="dim")
626
+
627
+ for i, item in enumerate(items):
628
+ desc = getattr(item, "description", str(item))
629
+
630
+ if i < current_idx:
631
+ # Completed
632
+ self.console.print(f" [green]✓ {desc}[/green]")
633
+ elif i == current_idx:
634
+ # Current
635
+ self.console.print(f" [bold blue]➜ {desc}[/bold blue]")
636
+ else:
637
+ # Pending
638
+ self.console.print(f" [dim]○ {desc}[/dim]")
639
+
640
+ self.console.print("=" * 60, style="dim")
641
+ self.console.print("")
642
+ else:
643
+ print("\n" + "=" * 60)
644
+ print(f"{ANSI_BOLD}📋 EXECUTION PLAN{ANSI_RESET}")
645
+ print("=" * 60)
646
+
647
+ for i, item in enumerate(items):
648
+ desc = getattr(item, "description", str(item))
649
+ if i < current_idx:
650
+ print(f" {ANSI_GREEN}✓ {desc}{ANSI_RESET}")
651
+ elif i == current_idx:
652
+ print(f" {ANSI_BLUE}{ANSI_BOLD}➜ {desc}{ANSI_RESET}")
653
+ else:
654
+ print(f" {ANSI_DIM}○ {desc}{ANSI_RESET}")
655
+
656
+ print("=" * 60 + "\n")
657
+
658
+ def print_checklist_reasoning(self, reasoning: str) -> None:
659
+ """
660
+ Print checklist reasoning.
661
+
662
+ Args:
663
+ reasoning: The reasoning text to display
664
+ """
665
+ if self.rich_available:
666
+ self.console.print("\n[bold]📝 CHECKLIST REASONING[/bold]")
667
+ self.console.print("=" * 60, style="dim")
668
+ self.console.print(f"{reasoning}")
669
+ self.console.print("=" * 60, style="dim")
670
+ self.console.print("")
671
+ else:
672
+ print("\n" + "=" * 60)
673
+ print(f"{ANSI_BOLD}📝 CHECKLIST REASONING{ANSI_RESET}")
674
+ print("=" * 60)
675
+ print(f"{reasoning}")
676
+ print("=" * 60 + "\n")
677
+
678
+ def print_command_executing(self, command: str) -> None:
679
+ """
680
+ Print command executing message.
681
+
682
+ Args:
683
+ command: The command being executed
684
+ """
685
+ if self.rich_available:
686
+ self.console.print(f"\n[bold]Executing Command:[/bold] {command}")
687
+ else:
688
+ print(f"\nExecuting Command: {command}")
689
+
690
+ def print_agent_selected(
691
+ self, agent_name: str, language: str, project_type: str
692
+ ) -> None:
693
+ """
694
+ Print agent selected message.
695
+
696
+ Args:
697
+ agent_name: The name of the selected agent
698
+ language: The detected programming language
699
+ project_type: The detected project type
700
+ """
701
+ if self.rich_available:
702
+ self.console.print(
703
+ f"[bold]🤖 Agent Selected:[/bold] [blue]{agent_name}[/blue] (language={language}, project_type={project_type})\n"
704
+ )
705
+ else:
706
+ print(
707
+ f"{ANSI_BOLD}🤖 Agent Selected:{ANSI_RESET} {ANSI_BLUE}{agent_name}{ANSI_RESET} (language={language}, project_type={project_type})\n"
708
+ )
709
+
710
+ def print_tool_usage(self, tool_name: str) -> None:
711
+ """
712
+ Print tool usage information with user-friendly descriptions.
713
+
714
+ Args:
715
+ tool_name: Name of the tool being used
716
+ """
717
+ # Map tool names to user-friendly action descriptions
718
+ tool_descriptions = {
719
+ # RAG Tools
720
+ "list_indexed_documents": "📚 Checking which documents are currently indexed",
721
+ "query_documents": "🔍 Searching through indexed documents for relevant information",
722
+ "query_specific_file": "📄 Searching within a specific document",
723
+ "search_indexed_chunks": "🔎 Performing exact text search in indexed content",
724
+ "index_document": "📥 Adding document to the knowledge base",
725
+ "index_directory": "📁 Indexing all documents in a directory",
726
+ "dump_document": "📝 Exporting document content for analysis",
727
+ "summarize_document": "📋 Creating a summary of the document",
728
+ "rag_status": "ℹ️ Retrieving RAG system status",
729
+ # File System Tools
730
+ "search_file": "🔍 Searching for files on your system",
731
+ "search_directory": "📂 Looking for directories on your system",
732
+ "search_file_content": "📝 Searching for content within files",
733
+ "read_file": "📖 Reading file contents",
734
+ "write_file": "✏️ Writing content to a file",
735
+ "add_watch_directory": "👁️ Starting to monitor a directory for changes",
736
+ # Shell Tools
737
+ "run_shell_command": "💻 Executing shell command",
738
+ # Default for unknown tools
739
+ "default": "🔧 Executing operation",
740
+ }
741
+
742
+ # Get the description or use the tool name if not found
743
+ action_desc = tool_descriptions.get(tool_name, tool_descriptions["default"])
744
+
745
+ if self.rich_available:
746
+ self.console.print(f"\n[bold blue]{action_desc}[/bold blue]")
747
+ if action_desc == tool_descriptions["default"]:
748
+ # If using default, also show the tool name
749
+ self.console.print(f" [dim]Tool: {tool_name}[/dim]")
750
+ else:
751
+ print(f"\n{action_desc}")
752
+ if action_desc == tool_descriptions["default"]:
753
+ print(f" Tool: {tool_name}")
754
+
755
+ def print_tool_complete(self) -> None:
756
+ """Print that tool execution is complete."""
757
+ if self.rich_available:
758
+ self.console.print("[green]✅ Tool execution complete[/green]")
759
+ else:
760
+ print("✅ Tool execution complete")
761
+
762
+ def print_error(self, error_message: str) -> None:
763
+ """
764
+ Print an error message with appropriate styling.
765
+
766
+ Args:
767
+ error_message: The error message to display
768
+ """
769
+ # Handle None error messages
770
+ if error_message is None:
771
+ error_message = "Unknown error occurred (received None)"
772
+
773
+ if self.rich_available:
774
+ self.console.print(
775
+ Panel(str(error_message), title="⚠️ Error", border_style="red")
776
+ )
777
+ else:
778
+ print(f"\n⚠️ ERROR: {error_message}\n")
779
+
780
+ def print_info(self, message: str) -> None:
781
+ """
782
+ Print an information message.
783
+
784
+ Args:
785
+ message: The information message to display
786
+ """
787
+ if self.rich_available:
788
+ self.console.print() # Add newline before
789
+ self.console.print(Panel(message, title="ℹ️ Info", border_style="blue"))
790
+ else:
791
+ print(f"\nℹ️ INFO: {message}\n")
792
+
793
+ def print_success(self, message: str) -> None:
794
+ """
795
+ Print a success message.
796
+
797
+ Args:
798
+ message: The success message to display
799
+ """
800
+ if self.rich_available:
801
+ self.console.print() # Add newline before
802
+ self.console.print(Panel(message, title="✅ Success", border_style="green"))
803
+ else:
804
+ print(f"\n✅ SUCCESS: {message}\n")
805
+
806
+ def print_diff(self, diff: str, filename: str) -> None:
807
+ """
808
+ Print a code diff with syntax highlighting.
809
+
810
+ Args:
811
+ diff: The diff content to display
812
+ filename: Name of the file being changed
813
+ """
814
+ if self.rich_available:
815
+ from rich.syntax import Syntax
816
+
817
+ self.console.print() # Add newline before
818
+ diff_panel = Panel(
819
+ Syntax(diff, "diff", theme="monokai", line_numbers=True),
820
+ title=f"🔧 Changes to {filename}",
821
+ border_style="yellow",
822
+ )
823
+ self.console.print(diff_panel)
824
+ else:
825
+ print(f"\n🔧 DIFF for {filename}:")
826
+ print("=" * 50)
827
+ print(diff)
828
+ print("=" * 50 + "\n")
829
+
830
+ def print_repeated_tool_warning(self) -> None:
831
+ """Print a warning about repeated tool calls."""
832
+ message = "Detected repetitive tool call pattern. Agent execution paused to avoid an infinite loop. Try adjusting your prompt or agent configuration if this persists."
833
+
834
+ if self.rich_available:
835
+ self.console.print(
836
+ Panel(
837
+ f"[bold yellow]{message}[/bold yellow]",
838
+ title="⚠️ Warning",
839
+ border_style="yellow",
840
+ padding=(1, 2),
841
+ highlight=True,
842
+ )
843
+ )
844
+ else:
845
+ print(f"\n⚠️ WARNING: {message}\n")
846
+
847
+ def print_final_answer(
848
+ self, answer: str, streaming: bool = True # pylint: disable=unused-argument
849
+ ) -> None:
850
+ """
851
+ Print the final answer with appropriate styling.
852
+
853
+ Args:
854
+ answer: The final answer to display
855
+ streaming: Not used (kept for compatibility)
856
+ """
857
+ if self.rich_available:
858
+ self.console.print() # Add newline before
859
+ self.console.print(
860
+ Panel(answer, title="✅ Final Answer", border_style="green")
861
+ )
862
+ else:
863
+ print(f"\n✅ FINAL ANSWER: {answer}\n")
864
+
865
+ def print_completion(self, steps_taken: int, steps_limit: int) -> None:
866
+ """
867
+ Print completion information.
868
+
869
+ Args:
870
+ steps_taken: Number of steps taken
871
+ steps_limit: Maximum number of steps allowed
872
+ """
873
+ self.print_separator()
874
+
875
+ if steps_taken < steps_limit:
876
+ # Completed successfully before hitting limit - clean message
877
+ message = "✨ Processing complete!"
878
+ else:
879
+ # Hit the limit - show ratio to indicate incomplete
880
+ message = f"⚠️ Processing stopped after {steps_taken}/{steps_limit} steps"
881
+
882
+ if self.rich_available:
883
+ self.console.print(f"[bold blue]{message}[/bold blue]")
884
+ else:
885
+ print(message)
886
+
887
+ def print_prompt(self, prompt: str, title: str = "Prompt") -> None:
888
+ """
889
+ Print a prompt with appropriate styling for debugging.
890
+
891
+ Args:
892
+ prompt: The prompt to display
893
+ title: Optional title for the panel
894
+ """
895
+ if self.rich_available:
896
+ from rich.syntax import Syntax
897
+
898
+ # Use plain text instead of markdown to avoid any parsing issues
899
+ # and ensure the full content is displayed
900
+ syntax = Syntax(prompt, "text", theme="monokai", line_numbers=False)
901
+
902
+ # Use expand=False to prevent Rich from trying to fit to terminal width
903
+ # This ensures the full prompt is shown even if it's very long
904
+ self.console.print(
905
+ Panel(
906
+ syntax,
907
+ title=f"🔍 {title}",
908
+ border_style="cyan",
909
+ padding=(1, 2),
910
+ expand=False,
911
+ )
912
+ )
913
+ else:
914
+ print(f"\n🔍 {title}:\n{'-' * 80}\n{prompt}\n{'-' * 80}\n")
915
+
916
+ def display_stats(self, stats: Dict[str, Any]) -> None:
917
+ """
918
+ Display LLM performance statistics or query execution stats.
919
+
920
+ Args:
921
+ stats: Dictionary containing performance statistics
922
+ Can include: duration, steps_taken, total_tokens (query stats)
923
+ Or: time_to_first_token, tokens_per_second, etc. (LLM stats)
924
+ """
925
+ if not stats:
926
+ return
927
+
928
+ # Check if we have query-level stats or LLM-level stats
929
+ has_query_stats = any(
930
+ key in stats for key in ["duration", "steps_taken", "total_tokens"]
931
+ )
932
+ has_llm_stats = any(
933
+ key in stats for key in ["time_to_first_token", "tokens_per_second"]
934
+ )
935
+
936
+ # Skip if there's no meaningful stats
937
+ if not has_query_stats and not has_llm_stats:
938
+ return
939
+
940
+ # Create a table for the stats
941
+ title = "📊 Query Stats" if has_query_stats else "🚀 LLM Performance Stats"
942
+ table = Table(
943
+ title=title,
944
+ show_header=True,
945
+ header_style="bold cyan",
946
+ )
947
+ table.add_column("Metric", style="dim")
948
+ table.add_column("Value", justify="right")
949
+
950
+ # Add query-level stats (timing and steps)
951
+ if "duration" in stats and stats["duration"] is not None:
952
+ table.add_row("Duration", f"{stats['duration']:.2f}s")
953
+
954
+ if "steps_taken" in stats and stats["steps_taken"] is not None:
955
+ table.add_row("Steps", f"{stats['steps_taken']}")
956
+
957
+ # Add LLM performance stats (timing)
958
+ if "time_to_first_token" in stats and stats["time_to_first_token"] is not None:
959
+ table.add_row("Time to First Token", f"{stats['time_to_first_token']:.2f}s")
960
+
961
+ if "tokens_per_second" in stats and stats["tokens_per_second"] is not None:
962
+ table.add_row("Tokens/Second", f"{stats['tokens_per_second']:.1f}")
963
+
964
+ # Add token usage stats (always show in consistent format)
965
+ if "input_tokens" in stats and stats["input_tokens"] is not None:
966
+ table.add_row("Input Tokens", f"{stats['input_tokens']:,}")
967
+
968
+ if "output_tokens" in stats and stats["output_tokens"] is not None:
969
+ table.add_row("Output Tokens", f"{stats['output_tokens']:,}")
970
+
971
+ if "total_tokens" in stats and stats["total_tokens"] is not None:
972
+ table.add_row("Total Tokens", f"{stats['total_tokens']:,}")
973
+
974
+ # Print the table in a panel
975
+ self.console.print(Panel(table, border_style="blue"))
976
+
977
+ def start_progress(self, message: str) -> None:
978
+ """
979
+ Start the progress indicator.
980
+
981
+ Args:
982
+ message: Message to display with the indicator
983
+ """
984
+ # If file preview is active, pause it temporarily
985
+ self._paused_preview = False
986
+ if self.file_preview_live is not None:
987
+ try:
988
+ self.file_preview_live.stop()
989
+ self._paused_preview = True
990
+ self.file_preview_live = None
991
+ # Small delay to ensure clean transition
992
+ time.sleep(0.05)
993
+ except Exception:
994
+ pass
995
+
996
+ self.progress.start(message)
997
+
998
+ def stop_progress(self) -> None:
999
+ """Stop the progress indicator."""
1000
+ self.progress.stop()
1001
+
1002
+ # Ensure clean line separation after progress stops
1003
+ if self.rich_available:
1004
+ # Longer delay to ensure the transient display is FULLY cleared
1005
+ time.sleep(0.15)
1006
+ # Explicitly move to a new line
1007
+ print() # Use print() instead of console.print() to avoid Live display conflicts
1008
+
1009
+ # NOTE: Do NOT create Live display here - let update_file_preview() handle it
1010
+ # This prevents double panels from appearing when both stop_progress and update_file_preview execute
1011
+
1012
+ # Reset the paused flag
1013
+ if hasattr(self, "_paused_preview"):
1014
+ self._paused_preview = False
1015
+
1016
+ def print_state_info(self, state_message: str):
1017
+ """
1018
+ Print the current execution state
1019
+
1020
+ Args:
1021
+ state_message: Message describing the current state
1022
+ """
1023
+ if self.rich_available:
1024
+ self.console.print(
1025
+ self.Panel(
1026
+ f"🔄 [bold cyan]{state_message}[/bold cyan]",
1027
+ border_style="cyan",
1028
+ padding=(0, 1),
1029
+ )
1030
+ )
1031
+ else:
1032
+ print(f"🔄 STATE: {state_message}")
1033
+
1034
+ def print_warning(self, warning_message: str):
1035
+ """
1036
+ Print a warning message
1037
+
1038
+ Args:
1039
+ warning_message: Warning message to display
1040
+ """
1041
+ if self.rich_available:
1042
+ self.console.print() # Add newline before
1043
+ self.console.print(
1044
+ self.Panel(
1045
+ f"⚠️ [bold yellow] {warning_message} [/bold yellow]",
1046
+ border_style="yellow",
1047
+ padding=(0, 1),
1048
+ )
1049
+ )
1050
+ else:
1051
+ print(f"⚠️ WARNING: {warning_message}")
1052
+
1053
+ def print_streaming_text(
1054
+ self, text_chunk: str, end_of_stream: bool = False
1055
+ ) -> None:
1056
+ """
1057
+ Print text content as it streams in, without newlines between chunks.
1058
+
1059
+ Args:
1060
+ text_chunk: The chunk of text from the stream
1061
+ end_of_stream: Whether this is the last chunk
1062
+ """
1063
+ # Accumulate text in the buffer
1064
+ self.streaming_buffer += text_chunk
1065
+
1066
+ # Print the chunk directly to console
1067
+ if self.rich_available:
1068
+ # Use low-level print to avoid adding newlines
1069
+ print(text_chunk, end="", flush=True)
1070
+ else:
1071
+ print(text_chunk, end="", flush=True)
1072
+
1073
+ # If this is the end of the stream, add a newline
1074
+ if end_of_stream:
1075
+ print()
1076
+
1077
+ def get_streaming_buffer(self) -> str:
1078
+ """
1079
+ Get the accumulated streaming text and reset buffer.
1080
+
1081
+ Returns:
1082
+ The complete accumulated text from streaming
1083
+ """
1084
+ result = self.streaming_buffer
1085
+ self.streaming_buffer = "" # Reset buffer
1086
+ return result
1087
+
1088
+ def print_response(self, response: str, title: str = "Response") -> None:
1089
+ """
1090
+ Print an LLM response with appropriate styling.
1091
+
1092
+ Args:
1093
+ response: The response text to display
1094
+ title: Optional title for the panel
1095
+ """
1096
+ if self.rich_available:
1097
+ from rich.syntax import Syntax
1098
+
1099
+ syntax = Syntax(response, "markdown", theme="monokai", line_numbers=False)
1100
+ self.console.print(
1101
+ Panel(syntax, title=f"🤖 {title}", border_style="green", padding=(1, 2))
1102
+ )
1103
+ else:
1104
+ print(f"\n🤖 {title}:\n{'-' * 80}\n{response}\n{'-' * 80}\n")
1105
+
1106
+ def print_tool_info(self, name: str, params_str: str, description: str) -> None:
1107
+ """
1108
+ Print information about a tool with appropriate styling.
1109
+
1110
+ Args:
1111
+ name: Name of the tool
1112
+ params_str: Formatted string of parameters
1113
+ description: Tool description
1114
+ """
1115
+ if self.rich_available:
1116
+ self.console.print(
1117
+ f"[bold cyan]📌 {name}[/bold cyan]([italic]{params_str}[/italic])"
1118
+ )
1119
+ self.console.print(f" [dim]{description}[/dim]")
1120
+ else:
1121
+ print(f"\n📌 {name}({params_str})")
1122
+ print(f" {description}")
1123
+
1124
+ # === File Watcher Output Methods ===
1125
+
1126
+ def print_file_created(
1127
+ self, filename: str, size: int = 0, extension: str = ""
1128
+ ) -> None:
1129
+ """
1130
+ Print file created notification with styling.
1131
+
1132
+ Args:
1133
+ filename: Name of the file
1134
+ size: Size in bytes
1135
+ extension: File extension
1136
+ """
1137
+ if self.rich_available:
1138
+ self.console.print(
1139
+ f"\n[bold green]📄 New file detected:[/bold green] [cyan]{filename}[/cyan]"
1140
+ )
1141
+ size_str = self._format_file_size(size)
1142
+ self.console.print(f" [dim]Size:[/dim] {size_str}")
1143
+ self.console.print(f" [dim]Type:[/dim] {extension or 'unknown'}")
1144
+ else:
1145
+ print(f"\n📄 New file detected: {filename}")
1146
+ print(f" Size: {size} bytes")
1147
+ print(f" Type: {extension or 'unknown'}")
1148
+
1149
+ def print_file_modified(self, filename: str) -> None:
1150
+ """
1151
+ Print file modified notification.
1152
+
1153
+ Args:
1154
+ filename: Name of the file
1155
+ """
1156
+ if self.rich_available:
1157
+ self.console.print(
1158
+ f"\n[bold yellow]✏️ File modified:[/bold yellow] [cyan]{filename}[/cyan]"
1159
+ )
1160
+ else:
1161
+ print(f"\n✏️ File modified: {filename}")
1162
+
1163
+ def print_file_deleted(self, filename: str) -> None:
1164
+ """
1165
+ Print file deleted notification.
1166
+
1167
+ Args:
1168
+ filename: Name of the file
1169
+ """
1170
+ if self.rich_available:
1171
+ self.console.print(
1172
+ f"\n[bold red]🗑️ File deleted:[/bold red] [cyan]{filename}[/cyan]"
1173
+ )
1174
+ else:
1175
+ print(f"\n🗑️ File deleted: {filename}")
1176
+
1177
+ def print_file_moved(self, src_filename: str, dest_filename: str) -> None:
1178
+ """
1179
+ Print file moved notification.
1180
+
1181
+ Args:
1182
+ src_filename: Original filename
1183
+ dest_filename: New filename
1184
+ """
1185
+ if self.rich_available:
1186
+ self.console.print(
1187
+ f"\n[bold magenta]📦 File moved:[/bold magenta] "
1188
+ f"[cyan]{src_filename}[/cyan] → [cyan]{dest_filename}[/cyan]"
1189
+ )
1190
+ else:
1191
+ print(f"\n📦 File moved: {src_filename} → {dest_filename}")
1192
+
1193
+ def _format_file_size(self, size_bytes: int) -> str:
1194
+ """Format file size in human-readable format."""
1195
+ if size_bytes < 1024:
1196
+ return f"{size_bytes} B"
1197
+ elif size_bytes < 1024 * 1024:
1198
+ return f"{size_bytes / 1024:.1f} KB"
1199
+ elif size_bytes < 1024 * 1024 * 1024:
1200
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
1201
+ else:
1202
+ return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
1203
+
1204
+ # === VLM/Model Progress Output Methods ===
1205
+
1206
+ def print_model_loading(self, model_name: str) -> None:
1207
+ """
1208
+ Print model loading progress.
1209
+
1210
+ Args:
1211
+ model_name: Name of the model being loaded
1212
+ """
1213
+ if self.rich_available:
1214
+ self.console.print(
1215
+ f"[bold blue]🔄 Loading model:[/bold blue] [cyan]{model_name}[/cyan]..."
1216
+ )
1217
+ else:
1218
+ print(f"🔄 Loading model: {model_name}...")
1219
+
1220
+ def print_model_ready(self, model_name: str, already_loaded: bool = False) -> None:
1221
+ """
1222
+ Print model ready notification.
1223
+
1224
+ Args:
1225
+ model_name: Name of the model
1226
+ already_loaded: If True, model was already loaded
1227
+ """
1228
+ status = "ready" if already_loaded else "loaded"
1229
+ if self.rich_available:
1230
+ self.console.print(
1231
+ f"[bold green]✅ Model {status}:[/bold green] [cyan]{model_name}[/cyan]"
1232
+ )
1233
+ else:
1234
+ print(f"✅ Model {status}: {model_name}")
1235
+
1236
+ # === Download Progress Methods ===
1237
+
1238
+ def print_download_start(self, model_name: str) -> None:
1239
+ """
1240
+ Print download starting notification.
1241
+
1242
+ Args:
1243
+ model_name: Name of the model being downloaded
1244
+ """
1245
+ if self.rich_available and self.console:
1246
+ self.console.print()
1247
+ self.console.print(
1248
+ f"[bold blue]📥 Downloading:[/bold blue] [cyan]{model_name}[/cyan]"
1249
+ )
1250
+ else:
1251
+ rprint(f"\n📥 Downloading: {model_name}")
1252
+
1253
+ def print_download_progress(
1254
+ self,
1255
+ percent: int,
1256
+ bytes_downloaded: int,
1257
+ bytes_total: int,
1258
+ speed_mbps: float = 0.0,
1259
+ ) -> None:
1260
+ """
1261
+ Print download progress with a progress bar that updates in place.
1262
+
1263
+ Args:
1264
+ percent: Download percentage (0-100)
1265
+ bytes_downloaded: Bytes downloaded so far
1266
+ bytes_total: Total bytes to download
1267
+ speed_mbps: Download speed in MB/s (optional)
1268
+ """
1269
+ import sys
1270
+
1271
+ # Format sizes
1272
+ if bytes_total > 1024**3: # > 1 GB
1273
+ dl_str = f"{bytes_downloaded / 1024**3:.2f} GB"
1274
+ total_str = f"{bytes_total / 1024**3:.2f} GB"
1275
+ elif bytes_total > 1024**2: # > 1 MB
1276
+ dl_str = f"{bytes_downloaded / 1024**2:.0f} MB"
1277
+ total_str = f"{bytes_total / 1024**2:.0f} MB"
1278
+ else:
1279
+ dl_str = f"{bytes_downloaded / 1024:.0f} KB"
1280
+ total_str = f"{bytes_total / 1024:.0f} KB"
1281
+
1282
+ # Progress bar characters
1283
+ bar_width = 25
1284
+ filled = int(bar_width * percent / 100)
1285
+ bar = "━" * filled + "─" * (bar_width - filled)
1286
+
1287
+ # Build progress line with optional speed
1288
+ progress_line = f" [{bar}] {percent:3d}% {dl_str} / {total_str}"
1289
+ if speed_mbps > 0.1:
1290
+ progress_line += f" @ {speed_mbps:.0f} MB/s"
1291
+
1292
+ # Update in place with carriage return
1293
+ sys.stdout.write(f"\r{progress_line:<80}")
1294
+ sys.stdout.flush()
1295
+
1296
+ def print_download_complete(self, model_name: str = None) -> None:
1297
+ """
1298
+ Print download complete notification.
1299
+
1300
+ Args:
1301
+ model_name: Optional name of the downloaded model
1302
+ """
1303
+ if self.rich_available and self.console:
1304
+ self.console.print() # Newline after progress bar
1305
+ if model_name:
1306
+ self.console.print(
1307
+ f" [green]✅ Downloaded successfully:[/green] [cyan]{model_name}[/cyan]"
1308
+ )
1309
+ else:
1310
+ self.console.print(" [green]✅ Download complete[/green]")
1311
+ else:
1312
+ rprint()
1313
+ msg = (
1314
+ f" ✅ Downloaded: {model_name}"
1315
+ if model_name
1316
+ else " ✅ Download complete"
1317
+ )
1318
+ rprint(msg)
1319
+
1320
+ def print_download_error(self, error_message: str, model_name: str = None) -> None:
1321
+ """
1322
+ Print download error notification.
1323
+
1324
+ Args:
1325
+ error_message: Error description
1326
+ model_name: Optional name of the model that failed
1327
+ """
1328
+ if self.rich_available and self.console:
1329
+ self.console.print() # Newline after progress bar
1330
+ if model_name:
1331
+ self.console.print(
1332
+ f" [red]❌ Download failed for {model_name}:[/red] {error_message}"
1333
+ )
1334
+ else:
1335
+ self.console.print(f" [red]❌ Download failed:[/red] {error_message}")
1336
+ else:
1337
+ rprint()
1338
+ msg = (
1339
+ f" ❌ Download failed for {model_name}: {error_message}"
1340
+ if model_name
1341
+ else f" ❌ Download failed: {error_message}"
1342
+ )
1343
+ rprint(msg)
1344
+
1345
+ def print_download_skipped(
1346
+ self, model_name: str, reason: str = "already downloaded"
1347
+ ) -> None:
1348
+ """
1349
+ Print download skipped notification.
1350
+
1351
+ Args:
1352
+ model_name: Name of the model that was skipped
1353
+ reason: Reason for skipping
1354
+ """
1355
+ if self.rich_available and self.console:
1356
+ self.console.print(
1357
+ f"[green]✅[/green] [cyan]{model_name}[/cyan] [dim]({reason})[/dim]"
1358
+ )
1359
+ else:
1360
+ rprint(f"✅ {model_name} ({reason})")
1361
+
1362
+ def print_extraction_start(
1363
+ self, image_num: int, page_num: int, mime_type: str
1364
+ ) -> None:
1365
+ """
1366
+ Print VLM extraction starting notification.
1367
+
1368
+ Args:
1369
+ image_num: Image number being processed
1370
+ page_num: Page number (for PDFs)
1371
+ mime_type: MIME type of the image
1372
+ """
1373
+ if self.rich_available:
1374
+ self.console.print(
1375
+ f" [dim]🔍 VLM extracting from image {image_num} "
1376
+ f"on page {page_num} ({mime_type})...[/dim]"
1377
+ )
1378
+ else:
1379
+ print(
1380
+ f" 🔍 VLM extracting from image {image_num} "
1381
+ f"on page {page_num} ({mime_type})..."
1382
+ )
1383
+
1384
+ def print_extraction_complete(
1385
+ self, chars: int, image_num: int, elapsed_seconds: float, size_kb: float
1386
+ ) -> None:
1387
+ """
1388
+ Print VLM extraction complete notification.
1389
+
1390
+ Args:
1391
+ chars: Number of characters extracted
1392
+ image_num: Image number processed
1393
+ elapsed_seconds: Time taken for extraction
1394
+ size_kb: Image size in KB
1395
+ """
1396
+ if self.rich_available:
1397
+ self.console.print(
1398
+ f" [green]✅ Extracted {chars} chars from image {image_num} "
1399
+ f"in {elapsed_seconds:.2f}s ({size_kb:.0f}KB image)[/green]"
1400
+ )
1401
+ else:
1402
+ print(
1403
+ f" ✅ Extracted {chars} chars from image {image_num} "
1404
+ f"in {elapsed_seconds:.2f}s ({size_kb:.0f}KB image)"
1405
+ )
1406
+
1407
+ def print_ready_for_input(self) -> None:
1408
+ """
1409
+ Print a visual separator indicating ready for user input.
1410
+
1411
+ Used after file processing completes to show the user
1412
+ that the system is ready for commands.
1413
+ """
1414
+ if self.rich_available:
1415
+ self.console.print()
1416
+ self.console.print("─" * 80, style="dim")
1417
+ self.console.print("> ", end="", style="bold green")
1418
+ else:
1419
+ print()
1420
+ print("─" * 80)
1421
+ print("> ", end="")
1422
+
1423
+ # === Processing Pipeline Progress Methods ===
1424
+
1425
+ def print_processing_step(
1426
+ self,
1427
+ step_num: int,
1428
+ total_steps: int,
1429
+ step_name: str,
1430
+ status: str = "running",
1431
+ ) -> None:
1432
+ """
1433
+ Print a processing step indicator with progress bar.
1434
+
1435
+ Args:
1436
+ step_num: Current step number (1-based)
1437
+ total_steps: Total number of steps
1438
+ step_name: Human-readable name of the current step
1439
+ status: Step status - 'running', 'complete', 'error'
1440
+ """
1441
+ # Create a simple progress bar
1442
+ progress_width = 20
1443
+ completed = int((step_num - 1) / total_steps * progress_width)
1444
+ current = 1 if step_num <= total_steps else 0
1445
+ remaining = progress_width - completed - current
1446
+
1447
+ if status == "complete":
1448
+ bar = "█" * progress_width
1449
+ elif status == "error":
1450
+ bar = "█" * completed + "✗" + "░" * remaining
1451
+ else:
1452
+ bar = "█" * completed + "▶" * current + "░" * remaining
1453
+
1454
+ # Status icon
1455
+ icons = {
1456
+ "running": "⏳",
1457
+ "complete": "✅",
1458
+ "error": "❌",
1459
+ }
1460
+ icon = icons.get(status, "")
1461
+
1462
+ if self.rich_available:
1463
+ # Style based on status
1464
+ if status == "complete":
1465
+ style = "green"
1466
+ elif status == "error":
1467
+ style = "red"
1468
+ else:
1469
+ style = "cyan"
1470
+
1471
+ self.console.print(
1472
+ f" [{style}]{icon} [{step_num}/{total_steps}][/{style}] "
1473
+ f"[dim]{bar}[/dim] [bold]{step_name}[/bold]"
1474
+ )
1475
+ else:
1476
+ print(f" {icon} [{step_num}/{total_steps}] {bar} {step_name}")
1477
+
1478
+ def print_processing_pipeline_start(self, filename: str, total_steps: int) -> None:
1479
+ """
1480
+ Print the start of a processing pipeline.
1481
+
1482
+ Args:
1483
+ filename: Name of the file being processed
1484
+ total_steps: Total number of processing steps
1485
+ """
1486
+ if self.rich_available:
1487
+ self.console.print()
1488
+ self.console.print(
1489
+ f"[bold cyan]⚙️ Processing Pipeline[/bold cyan] "
1490
+ f"[dim]({total_steps} steps)[/dim]"
1491
+ )
1492
+ self.console.print(f" [dim]File:[/dim] [cyan]{filename}[/cyan]")
1493
+ else:
1494
+ print(f"\n⚙️ Processing Pipeline ({total_steps} steps)")
1495
+ print(f" File: {filename}")
1496
+
1497
+ def print_processing_pipeline_complete(
1498
+ self,
1499
+ filename: str, # pylint: disable=unused-argument
1500
+ success: bool,
1501
+ elapsed_seconds: float,
1502
+ patient_name: str = None,
1503
+ is_duplicate: bool = False,
1504
+ ) -> None:
1505
+ """
1506
+ Print the completion of a processing pipeline.
1507
+
1508
+ Args:
1509
+ filename: Name of the file processed (kept for API consistency)
1510
+ success: Whether processing was successful
1511
+ elapsed_seconds: Total processing time
1512
+ patient_name: Optional patient name for success message
1513
+ is_duplicate: Whether this was a duplicate file (skipped)
1514
+ """
1515
+ if self.rich_available:
1516
+ if is_duplicate:
1517
+ msg = f"[bold yellow]⚡ Duplicate skipped[/bold yellow] in {elapsed_seconds:.1f}s"
1518
+ if patient_name:
1519
+ msg += f" → [cyan]{patient_name}[/cyan] (already processed)"
1520
+ self.console.print(msg)
1521
+ elif success:
1522
+ msg = f"[bold green]✅ Pipeline complete[/bold green] in {elapsed_seconds:.1f}s"
1523
+ if patient_name:
1524
+ msg += f" → [cyan]{patient_name}[/cyan]"
1525
+ self.console.print(msg)
1526
+ else:
1527
+ self.console.print(
1528
+ f"[bold red]❌ Pipeline failed[/bold red] after {elapsed_seconds:.1f}s"
1529
+ )
1530
+ else:
1531
+ if is_duplicate:
1532
+ msg = f"⚡ Duplicate skipped in {elapsed_seconds:.1f}s"
1533
+ if patient_name:
1534
+ msg += f" → {patient_name} (already processed)"
1535
+ print(msg)
1536
+ elif success:
1537
+ msg = f"✅ Pipeline complete in {elapsed_seconds:.1f}s"
1538
+ if patient_name:
1539
+ msg += f" → {patient_name}"
1540
+ print(msg)
1541
+ else:
1542
+ print(f"❌ Pipeline failed after {elapsed_seconds:.1f}s")
1543
+
1544
+ # === File Preview Methods ===
1545
+
1546
+ def start_file_preview(
1547
+ self, filename: str, max_lines: int = 15, title_prefix: str = "📄"
1548
+ ) -> None:
1549
+ """
1550
+ Start a live streaming file preview window.
1551
+
1552
+ Args:
1553
+ filename: Name of the file being generated
1554
+ max_lines: Maximum number of lines to show (default: 15)
1555
+ title_prefix: Emoji/prefix for the title (default: 📄)
1556
+ """
1557
+ # CRITICAL: Stop progress indicator if running to prevent overlapping Live displays
1558
+ if self.progress.is_running:
1559
+ self.stop_progress()
1560
+
1561
+ # Stop any existing preview first to prevent stacking
1562
+ if self.file_preview_live is not None:
1563
+ try:
1564
+ self.file_preview_live.stop()
1565
+ except Exception:
1566
+ pass # Ignore errors if already stopped
1567
+ finally:
1568
+ self.file_preview_live = None
1569
+ # Small delay to ensure display cleanup
1570
+ time.sleep(0.1)
1571
+ # Ensure we're on a new line after stopping the previous preview
1572
+ if self.rich_available:
1573
+ self.console.print()
1574
+
1575
+ # Reset state for new file
1576
+ self.file_preview_filename = filename
1577
+ self.file_preview_content = ""
1578
+ self.file_preview_max_lines = max_lines
1579
+
1580
+ if self.rich_available:
1581
+ # DON'T start the live preview here - wait for first content
1582
+ pass
1583
+ else:
1584
+ # For non-rich mode, just print a header
1585
+ print(f"\n{title_prefix} Generating {filename}...")
1586
+ print("=" * 80)
1587
+
1588
+ def update_file_preview(self, content_chunk: str) -> None:
1589
+ """
1590
+ Update the live file preview with new content.
1591
+
1592
+ Args:
1593
+ content_chunk: New content to append to the preview
1594
+ """
1595
+ self.file_preview_content += content_chunk
1596
+
1597
+ if self.rich_available:
1598
+ # Only process if we have a filename set (preview has been started)
1599
+ if not self.file_preview_filename:
1600
+ return
1601
+
1602
+ # Check if enough time has passed for throttling
1603
+ current_time = time.time()
1604
+ time_since_last_update = current_time - self._last_preview_update_time
1605
+
1606
+ # Start the live preview on first content if not already started
1607
+ if self.file_preview_live is None and self.file_preview_content:
1608
+ preview = self._generate_file_preview_panel("📄")
1609
+ self.file_preview_live = Live(
1610
+ preview,
1611
+ console=self.console,
1612
+ refresh_per_second=4,
1613
+ transient=False, # Keep False to prevent double rendering
1614
+ )
1615
+ self.file_preview_live.start()
1616
+ self._last_preview_update_time = current_time
1617
+ elif (
1618
+ self.file_preview_live
1619
+ and time_since_last_update >= self._preview_update_interval
1620
+ ):
1621
+ try:
1622
+ # Update existing live display with new content
1623
+ preview = self._generate_file_preview_panel("📄")
1624
+ # Just update, don't force refresh
1625
+ self.file_preview_live.update(preview)
1626
+ self._last_preview_update_time = current_time
1627
+ except Exception:
1628
+ # If update fails, continue accumulating content
1629
+ # (silently ignore preview update failures)
1630
+ pass
1631
+ else:
1632
+ # For non-rich mode, print new content directly
1633
+ print(content_chunk, end="", flush=True)
1634
+
1635
+ def stop_file_preview(self) -> None:
1636
+ """Stop the live file preview and show final summary."""
1637
+ if self.rich_available:
1638
+ # Only stop if it was started
1639
+ if self.file_preview_live:
1640
+ try:
1641
+ self.file_preview_live.stop()
1642
+ except Exception:
1643
+ pass
1644
+ finally:
1645
+ self.file_preview_live = None
1646
+
1647
+ # Show completion message only if we generated content
1648
+ if self.file_preview_content:
1649
+ total_lines = len(self.file_preview_content.splitlines())
1650
+ self.console.print(
1651
+ f"[green]✅ Generated {self.file_preview_filename} ({total_lines} lines)[/green]\n"
1652
+ )
1653
+ else:
1654
+ print("\n" + "=" * 80)
1655
+ total_lines = len(self.file_preview_content.splitlines())
1656
+ print(f"✅ Generated {self.file_preview_filename} ({total_lines} lines)\n")
1657
+
1658
+ # Reset state - IMPORTANT: Clear filename first to prevent updates
1659
+ self.file_preview_filename = ""
1660
+ self.file_preview_content = ""
1661
+
1662
+ def _generate_file_preview_panel(self, title_prefix: str) -> Panel:
1663
+ """
1664
+ Generate a Rich Panel with the current file preview content.
1665
+
1666
+ Args:
1667
+ title_prefix: Emoji/prefix for the title
1668
+
1669
+ Returns:
1670
+ Rich Panel with syntax-highlighted content
1671
+ """
1672
+ lines = self.file_preview_content.splitlines()
1673
+ total_lines = len(lines)
1674
+
1675
+ # Truncate extremely long lines to prevent display issues
1676
+ truncated_lines = []
1677
+ for line in lines:
1678
+ if len(line) > MAX_DISPLAY_LINE_LENGTH:
1679
+ truncated_lines.append(line[:MAX_DISPLAY_LINE_LENGTH] + "...")
1680
+ else:
1681
+ truncated_lines.append(line)
1682
+
1683
+ # Show last N lines
1684
+ if total_lines <= self.file_preview_max_lines:
1685
+ preview_lines = truncated_lines
1686
+ line_info = f"All {total_lines} lines"
1687
+ else:
1688
+ preview_lines = truncated_lines[-self.file_preview_max_lines :]
1689
+ line_info = f"Last {self.file_preview_max_lines} of {total_lines} lines"
1690
+
1691
+ # Determine syntax highlighting
1692
+ ext = (
1693
+ self.file_preview_filename.split(".")[-1]
1694
+ if "." in self.file_preview_filename
1695
+ else "txt"
1696
+ )
1697
+ syntax_map = {
1698
+ "py": "python",
1699
+ "js": "javascript",
1700
+ "ts": "typescript",
1701
+ "jsx": "jsx",
1702
+ "tsx": "tsx",
1703
+ "json": "json",
1704
+ "md": "markdown",
1705
+ "yml": "yaml",
1706
+ "yaml": "yaml",
1707
+ "toml": "toml",
1708
+ "ini": "ini",
1709
+ "sh": "bash",
1710
+ "bash": "bash",
1711
+ "ps1": "powershell",
1712
+ "sql": "sql",
1713
+ "html": "html",
1714
+ "css": "css",
1715
+ "xml": "xml",
1716
+ "c": "c",
1717
+ "cpp": "cpp",
1718
+ "java": "java",
1719
+ "go": "go",
1720
+ "rs": "rust",
1721
+ }
1722
+ syntax_lang = syntax_map.get(ext.lower(), "text")
1723
+
1724
+ # Create syntax-highlighted preview
1725
+ preview_content = (
1726
+ "\n".join(preview_lines) if preview_lines else "[dim]Generating...[/dim]"
1727
+ )
1728
+
1729
+ if preview_lines:
1730
+ # Calculate starting line number for the preview
1731
+ if total_lines <= self.file_preview_max_lines:
1732
+ start_line = 1
1733
+ else:
1734
+ start_line = total_lines - self.file_preview_max_lines + 1
1735
+
1736
+ syntax = Syntax(
1737
+ preview_content,
1738
+ syntax_lang,
1739
+ theme="monokai",
1740
+ line_numbers=True,
1741
+ start_line=start_line,
1742
+ word_wrap=False, # Prevent line wrapping that causes display issues
1743
+ )
1744
+ else:
1745
+ syntax = preview_content
1746
+
1747
+ return Panel(
1748
+ syntax,
1749
+ title=f"{title_prefix} {self.file_preview_filename} ({line_info})",
1750
+ border_style="cyan",
1751
+ padding=(1, 2),
1752
+ )
1753
+
1754
+
1755
+ class SilentConsole(OutputHandler):
1756
+ """
1757
+ A silent console that suppresses all output for JSON-only mode.
1758
+ Provides the same interface as AgentConsole but with no-op methods.
1759
+ Implements OutputHandler for silent/suppressed output.
1760
+ """
1761
+
1762
+ def __init__(self, silence_final_answer: bool = False):
1763
+ """Initialize the silent console.
1764
+
1765
+ Args:
1766
+ silence_final_answer: If True, suppress even the final answer (for JSON-only mode)
1767
+ """
1768
+ self.streaming_buffer = "" # Maintain compatibility
1769
+ self.silence_final_answer = silence_final_answer
1770
+
1771
+ # Implementation of OutputHandler abstract methods - all no-ops
1772
+ def print_final_answer(
1773
+ self, answer: str, streaming: bool = True # pylint: disable=unused-argument
1774
+ ) -> None:
1775
+ """
1776
+ Print the final answer.
1777
+ Only suppressed if silence_final_answer is True.
1778
+
1779
+ Args:
1780
+ answer: The final answer to display
1781
+ streaming: Not used (kept for compatibility)
1782
+ """
1783
+ if self.silence_final_answer:
1784
+ return # Completely silent
1785
+
1786
+ # Print the final answer directly
1787
+ print(f"\n🧠 gaia: {answer}")
1788
+
1789
+ def display_stats(self, stats: Dict[str, Any]) -> None:
1790
+ """
1791
+ Display stats even in silent mode (since explicitly requested).
1792
+ Uses the same Rich table format as AgentConsole.
1793
+
1794
+ Args:
1795
+ stats: Dictionary containing performance statistics
1796
+ """
1797
+ if not stats:
1798
+ return
1799
+
1800
+ # Check if we have query-level stats or LLM-level stats
1801
+ has_query_stats = any(
1802
+ key in stats for key in ["duration", "steps_taken", "total_tokens"]
1803
+ )
1804
+ has_llm_stats = any(
1805
+ key in stats for key in ["time_to_first_token", "tokens_per_second"]
1806
+ )
1807
+
1808
+ # Skip if there's no meaningful stats
1809
+ if not has_query_stats and not has_llm_stats:
1810
+ return
1811
+
1812
+ # Use Rich table format (same as AgentConsole)
1813
+ from rich.console import Console
1814
+ from rich.panel import Panel
1815
+ from rich.table import Table
1816
+
1817
+ console = Console()
1818
+
1819
+ title = "📊 Query Stats" if has_query_stats else "🚀 LLM Performance Stats"
1820
+ table = Table(
1821
+ title=title,
1822
+ show_header=True,
1823
+ header_style="bold cyan",
1824
+ )
1825
+ table.add_column("Metric", style="dim")
1826
+ table.add_column("Value", justify="right")
1827
+
1828
+ # Add query-level stats (timing and steps)
1829
+ if "duration" in stats and stats["duration"] is not None:
1830
+ table.add_row("Duration", f"{stats['duration']:.2f}s")
1831
+
1832
+ if "steps_taken" in stats and stats["steps_taken"] is not None:
1833
+ table.add_row("Steps", f"{stats['steps_taken']}")
1834
+
1835
+ # Add LLM performance stats (timing)
1836
+ if "time_to_first_token" in stats and stats["time_to_first_token"] is not None:
1837
+ table.add_row("Time to First Token", f"{stats['time_to_first_token']:.2f}s")
1838
+
1839
+ if "tokens_per_second" in stats and stats["tokens_per_second"] is not None:
1840
+ table.add_row("Tokens/Second", f"{stats['tokens_per_second']:.1f}")
1841
+
1842
+ # Add token usage stats (always show in consistent format)
1843
+ if "input_tokens" in stats and stats["input_tokens"] is not None:
1844
+ table.add_row("Input Tokens", f"{stats['input_tokens']:,}")
1845
+
1846
+ if "output_tokens" in stats and stats["output_tokens"] is not None:
1847
+ table.add_row("Output Tokens", f"{stats['output_tokens']:,}")
1848
+
1849
+ if "total_tokens" in stats and stats["total_tokens"] is not None:
1850
+ table.add_row("Total Tokens", f"{stats['total_tokens']:,}")
1851
+
1852
+ # Print the table in a panel
1853
+ console.print(Panel(table, border_style="blue"))
1854
+
1855
+ # All other abstract methods as no-ops
1856
+ def print_processing_start(self, query: str, max_steps: int):
1857
+ """No-op implementation."""
1858
+
1859
+ def print_step_header(self, step_num: int, step_limit: int):
1860
+ """No-op implementation."""
1861
+
1862
+ def print_state_info(self, state_message: str):
1863
+ """No-op implementation."""
1864
+
1865
+ def print_thought(self, thought: str):
1866
+ """No-op implementation."""
1867
+
1868
+ def print_goal(self, goal: str):
1869
+ """No-op implementation."""
1870
+
1871
+ def print_plan(self, plan: List[Any], current_step: int = None):
1872
+ """No-op implementation."""
1873
+
1874
+ def print_step_paused(self, description: str):
1875
+ """No-op implementation."""
1876
+
1877
+ def print_checklist(self, items: List[Any], current_idx: int):
1878
+ """No-op implementation."""
1879
+
1880
+ def print_checklist_reasoning(self, reasoning: str):
1881
+ """No-op implementation."""
1882
+
1883
+ def print_command_executing(self, command: str):
1884
+ """No-op implementation."""
1885
+
1886
+ def print_agent_selected(self, agent_name: str, language: str, project_type: str):
1887
+ """No-op implementation."""
1888
+
1889
+ def print_tool_usage(self, tool_name: str):
1890
+ """No-op implementation."""
1891
+
1892
+ def print_tool_complete(self):
1893
+ """No-op implementation."""
1894
+
1895
+ def pretty_print_json(self, data: Dict[str, Any], title: str = None):
1896
+ """No-op implementation."""
1897
+
1898
+ def print_error(self, error_message: str):
1899
+ """No-op implementation."""
1900
+
1901
+ def print_warning(self, warning_message: str):
1902
+ """No-op implementation."""
1903
+
1904
+ def print_info(self, message: str):
1905
+ """No-op implementation."""
1906
+
1907
+ def start_progress(self, message: str):
1908
+ """No-op implementation."""
1909
+
1910
+ def stop_progress(self):
1911
+ """No-op implementation."""
1912
+
1913
+ def print_repeated_tool_warning(self):
1914
+ """No-op implementation."""
1915
+
1916
+ def print_completion(self, steps_taken: int, steps_limit: int):
1917
+ """No-op implementation."""
1918
+
1919
+ def print_success(self, message: str):
1920
+ """No-op implementation."""
1921
+
1922
+ def print_file_created(self, filename: str, size: int = 0, extension: str = ""):
1923
+ """No-op implementation."""
1924
+
1925
+ def print_file_modified(self, filename: str, size: int = 0):
1926
+ """No-op implementation."""
1927
+
1928
+ def print_file_deleted(self, filename: str):
1929
+ """No-op implementation."""
1930
+
1931
+ def print_file_moved(self, src_filename: str, dest_filename: str):
1932
+ """No-op implementation."""
1933
+
1934
+ def print_model_loading(self, model_name: str):
1935
+ """No-op implementation."""
1936
+
1937
+ def print_model_ready(self, model_name: str, already_loaded: bool = False):
1938
+ """No-op implementation."""
1939
+
1940
+ def print_extraction_start(self, image_num: int, page_num: int, mime_type: str):
1941
+ """No-op implementation."""
1942
+
1943
+ def print_extraction_complete(
1944
+ self, chars: int, image_num: int, elapsed_seconds: float, size_kb: float
1945
+ ):
1946
+ """No-op implementation."""
1947
+
1948
+ def print_ready_for_input(self):
1949
+ """No-op implementation."""
1950
+
1951
+ def print_processing_step(
1952
+ self, step_num: int, total_steps: int, step_name: str, status: str = "running"
1953
+ ):
1954
+ """No-op implementation."""
1955
+
1956
+ def print_processing_pipeline_start(self, filename: str, total_steps: int):
1957
+ """No-op implementation."""
1958
+
1959
+ def print_processing_pipeline_complete(
1960
+ self,
1961
+ filename: str,
1962
+ success: bool,
1963
+ elapsed_seconds: float,
1964
+ patient_name: str = None,
1965
+ is_duplicate: bool = False,
1966
+ ):
1967
+ """No-op implementation."""