amd-gaia 0.14.3__py3-none-any.whl → 0.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
- amd_gaia-0.15.1.dist-info/RECORD +178 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
- gaia/__init__.py +29 -29
- gaia/agents/__init__.py +19 -19
- gaia/agents/base/__init__.py +9 -9
- gaia/agents/base/agent.py +2177 -2177
- gaia/agents/base/api_agent.py +120 -120
- gaia/agents/base/console.py +1841 -1841
- gaia/agents/base/errors.py +237 -237
- gaia/agents/base/mcp_agent.py +86 -86
- gaia/agents/base/tools.py +83 -83
- gaia/agents/blender/agent.py +556 -556
- gaia/agents/blender/agent_simple.py +133 -135
- gaia/agents/blender/app.py +211 -211
- gaia/agents/blender/app_simple.py +41 -41
- gaia/agents/blender/core/__init__.py +16 -16
- gaia/agents/blender/core/materials.py +506 -506
- gaia/agents/blender/core/objects.py +316 -316
- gaia/agents/blender/core/rendering.py +225 -225
- gaia/agents/blender/core/scene.py +220 -220
- gaia/agents/blender/core/view.py +146 -146
- gaia/agents/chat/__init__.py +9 -9
- gaia/agents/chat/agent.py +835 -835
- gaia/agents/chat/app.py +1058 -1058
- gaia/agents/chat/session.py +508 -508
- gaia/agents/chat/tools/__init__.py +15 -15
- gaia/agents/chat/tools/file_tools.py +96 -96
- gaia/agents/chat/tools/rag_tools.py +1729 -1729
- gaia/agents/chat/tools/shell_tools.py +436 -436
- gaia/agents/code/__init__.py +7 -7
- gaia/agents/code/agent.py +549 -549
- gaia/agents/code/cli.py +377 -0
- gaia/agents/code/models.py +135 -135
- gaia/agents/code/orchestration/__init__.py +24 -24
- gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
- gaia/agents/code/orchestration/checklist_generator.py +713 -713
- gaia/agents/code/orchestration/factories/__init__.py +9 -9
- gaia/agents/code/orchestration/factories/base.py +63 -63
- gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
- gaia/agents/code/orchestration/factories/python_factory.py +106 -106
- gaia/agents/code/orchestration/orchestrator.py +841 -841
- gaia/agents/code/orchestration/project_analyzer.py +391 -391
- gaia/agents/code/orchestration/steps/__init__.py +67 -67
- gaia/agents/code/orchestration/steps/base.py +188 -188
- gaia/agents/code/orchestration/steps/error_handler.py +314 -314
- gaia/agents/code/orchestration/steps/nextjs.py +828 -828
- gaia/agents/code/orchestration/steps/python.py +307 -307
- gaia/agents/code/orchestration/template_catalog.py +469 -469
- gaia/agents/code/orchestration/workflows/__init__.py +14 -14
- gaia/agents/code/orchestration/workflows/base.py +80 -80
- gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
- gaia/agents/code/orchestration/workflows/python.py +94 -94
- gaia/agents/code/prompts/__init__.py +11 -11
- gaia/agents/code/prompts/base_prompt.py +77 -77
- gaia/agents/code/prompts/code_patterns.py +2036 -2036
- gaia/agents/code/prompts/nextjs_prompt.py +40 -40
- gaia/agents/code/prompts/python_prompt.py +109 -109
- gaia/agents/code/schema_inference.py +365 -365
- gaia/agents/code/system_prompt.py +41 -41
- gaia/agents/code/tools/__init__.py +42 -42
- gaia/agents/code/tools/cli_tools.py +1138 -1138
- gaia/agents/code/tools/code_formatting.py +319 -319
- gaia/agents/code/tools/code_tools.py +769 -769
- gaia/agents/code/tools/error_fixing.py +1347 -1347
- gaia/agents/code/tools/external_tools.py +180 -180
- gaia/agents/code/tools/file_io.py +845 -845
- gaia/agents/code/tools/prisma_tools.py +190 -190
- gaia/agents/code/tools/project_management.py +1016 -1016
- gaia/agents/code/tools/testing.py +321 -321
- gaia/agents/code/tools/typescript_tools.py +122 -122
- gaia/agents/code/tools/validation_parsing.py +461 -461
- gaia/agents/code/tools/validation_tools.py +806 -806
- gaia/agents/code/tools/web_dev_tools.py +1758 -1758
- gaia/agents/code/validators/__init__.py +16 -16
- gaia/agents/code/validators/antipattern_checker.py +241 -241
- gaia/agents/code/validators/ast_analyzer.py +197 -197
- gaia/agents/code/validators/requirements_validator.py +145 -145
- gaia/agents/code/validators/syntax_validator.py +171 -171
- gaia/agents/docker/__init__.py +7 -7
- gaia/agents/docker/agent.py +642 -642
- gaia/agents/emr/__init__.py +8 -8
- gaia/agents/emr/agent.py +1506 -1506
- gaia/agents/emr/cli.py +1322 -1322
- gaia/agents/emr/constants.py +475 -475
- gaia/agents/emr/dashboard/__init__.py +4 -4
- gaia/agents/emr/dashboard/server.py +1974 -1974
- gaia/agents/jira/__init__.py +11 -11
- gaia/agents/jira/agent.py +894 -894
- gaia/agents/jira/jql_templates.py +299 -299
- gaia/agents/routing/__init__.py +7 -7
- gaia/agents/routing/agent.py +567 -570
- gaia/agents/routing/system_prompt.py +75 -75
- gaia/agents/summarize/__init__.py +11 -0
- gaia/agents/summarize/agent.py +885 -0
- gaia/agents/summarize/prompts.py +129 -0
- gaia/api/__init__.py +23 -23
- gaia/api/agent_registry.py +238 -238
- gaia/api/app.py +305 -305
- gaia/api/openai_server.py +575 -575
- gaia/api/schemas.py +186 -186
- gaia/api/sse_handler.py +373 -373
- gaia/apps/__init__.py +4 -4
- gaia/apps/llm/__init__.py +6 -6
- gaia/apps/llm/app.py +173 -169
- gaia/apps/summarize/app.py +116 -633
- gaia/apps/summarize/html_viewer.py +133 -133
- gaia/apps/summarize/pdf_formatter.py +284 -284
- gaia/audio/__init__.py +2 -2
- gaia/audio/audio_client.py +439 -439
- gaia/audio/audio_recorder.py +269 -269
- gaia/audio/kokoro_tts.py +599 -599
- gaia/audio/whisper_asr.py +432 -432
- gaia/chat/__init__.py +16 -16
- gaia/chat/app.py +430 -430
- gaia/chat/prompts.py +522 -522
- gaia/chat/sdk.py +1228 -1225
- gaia/cli.py +5481 -5621
- gaia/database/__init__.py +10 -10
- gaia/database/agent.py +176 -176
- gaia/database/mixin.py +290 -290
- gaia/database/testing.py +64 -64
- gaia/eval/batch_experiment.py +2332 -2332
- gaia/eval/claude.py +542 -542
- gaia/eval/config.py +37 -37
- gaia/eval/email_generator.py +512 -512
- gaia/eval/eval.py +3179 -3179
- gaia/eval/groundtruth.py +1130 -1130
- gaia/eval/transcript_generator.py +582 -582
- gaia/eval/webapp/README.md +167 -167
- gaia/eval/webapp/package-lock.json +875 -875
- gaia/eval/webapp/package.json +20 -20
- gaia/eval/webapp/public/app.js +3402 -3402
- gaia/eval/webapp/public/index.html +87 -87
- gaia/eval/webapp/public/styles.css +3661 -3661
- gaia/eval/webapp/server.js +415 -415
- gaia/eval/webapp/test-setup.js +72 -72
- gaia/llm/__init__.py +9 -2
- gaia/llm/base_client.py +60 -0
- gaia/llm/exceptions.py +12 -0
- gaia/llm/factory.py +70 -0
- gaia/llm/lemonade_client.py +3236 -3221
- gaia/llm/lemonade_manager.py +294 -294
- gaia/llm/providers/__init__.py +9 -0
- gaia/llm/providers/claude.py +108 -0
- gaia/llm/providers/lemonade.py +120 -0
- gaia/llm/providers/openai_provider.py +79 -0
- gaia/llm/vlm_client.py +382 -382
- gaia/logger.py +189 -189
- gaia/mcp/agent_mcp_server.py +245 -245
- gaia/mcp/blender_mcp_client.py +138 -138
- gaia/mcp/blender_mcp_server.py +648 -648
- gaia/mcp/context7_cache.py +332 -332
- gaia/mcp/external_services.py +518 -518
- gaia/mcp/mcp_bridge.py +811 -550
- gaia/mcp/servers/__init__.py +6 -6
- gaia/mcp/servers/docker_mcp.py +83 -83
- gaia/perf_analysis.py +361 -0
- gaia/rag/__init__.py +10 -10
- gaia/rag/app.py +293 -293
- gaia/rag/demo.py +304 -304
- gaia/rag/pdf_utils.py +235 -235
- gaia/rag/sdk.py +2194 -2194
- gaia/security.py +163 -163
- gaia/talk/app.py +289 -289
- gaia/talk/sdk.py +538 -538
- gaia/testing/__init__.py +87 -87
- gaia/testing/assertions.py +330 -330
- gaia/testing/fixtures.py +333 -333
- gaia/testing/mocks.py +493 -493
- gaia/util.py +46 -46
- gaia/utils/__init__.py +33 -33
- gaia/utils/file_watcher.py +675 -675
- gaia/utils/parsing.py +223 -223
- gaia/version.py +100 -100
- amd_gaia-0.14.3.dist-info/RECORD +0 -168
- gaia/agents/code/app.py +0 -266
- gaia/llm/llm_client.py +0 -729
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
- {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
gaia/agents/base/console.py
CHANGED
|
@@ -1,1841 +1,1841 @@
|
|
|
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
|
+
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."""
|