kollabor 0.4.9__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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Message Display Service for LLM responses.
|
|
2
|
+
|
|
3
|
+
Handles unified message display coordination, eliminating duplicated
|
|
4
|
+
display logic throughout the LLM service. Follows KISS principle with
|
|
5
|
+
single responsibility for message display orchestration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessageDisplayService:
|
|
15
|
+
"""Unified service for coordinating LLM message display.
|
|
16
|
+
|
|
17
|
+
Eliminates code duplication by providing a single point of control
|
|
18
|
+
for all message display operations including thinking duration,
|
|
19
|
+
assistant responses, and tool execution results.
|
|
20
|
+
|
|
21
|
+
Follows KISS principle: Single responsibility for message display coordination.
|
|
22
|
+
Implements DRY principle: Eliminates ~90 lines of duplicated display code.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, renderer):
|
|
26
|
+
"""Initialize message display service.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
renderer: Terminal renderer with message_coordinator
|
|
30
|
+
"""
|
|
31
|
+
self.renderer = renderer
|
|
32
|
+
self.message_coordinator = renderer.message_coordinator
|
|
33
|
+
self._streaming_active = False
|
|
34
|
+
|
|
35
|
+
logger.info("Message display service initialized")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def display_thinking_and_response(self,
|
|
39
|
+
thinking_duration: float,
|
|
40
|
+
response: str,
|
|
41
|
+
show_thinking_threshold: float = 0.1) -> None:
|
|
42
|
+
"""Display thinking duration and assistant response atomically.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
thinking_duration: Time spent thinking in seconds
|
|
46
|
+
response: Assistant response content
|
|
47
|
+
show_thinking_threshold: Minimum duration to show thinking message
|
|
48
|
+
"""
|
|
49
|
+
# Use the unified display method for consistency
|
|
50
|
+
self.display_complete_response(
|
|
51
|
+
thinking_duration=thinking_duration,
|
|
52
|
+
response=response,
|
|
53
|
+
tool_results=None,
|
|
54
|
+
original_tools=None,
|
|
55
|
+
show_thinking_threshold=show_thinking_threshold
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def display_tool_results(self, tool_results: List[Any], original_tools: List[Dict] = None) -> None:
|
|
59
|
+
"""Display tool execution results with consistent formatting.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
tool_results: List of tool execution result objects
|
|
63
|
+
original_tools: List of original tool data for command extraction
|
|
64
|
+
"""
|
|
65
|
+
for i, result in enumerate(tool_results):
|
|
66
|
+
# Get original tool data for display
|
|
67
|
+
tool_data = original_tools[i] if original_tools and i < len(original_tools) else {}
|
|
68
|
+
|
|
69
|
+
# Format tool execution display with consistent styling
|
|
70
|
+
tool_display = self._format_tool_header(result, tool_data)
|
|
71
|
+
result_display = self._format_tool_result(result)
|
|
72
|
+
|
|
73
|
+
# Build combined tool display message
|
|
74
|
+
combined_output = [tool_display, result_display]
|
|
75
|
+
|
|
76
|
+
# Add actual output if appropriate
|
|
77
|
+
if self._should_show_output(result):
|
|
78
|
+
output_lines = self._format_tool_output(result)
|
|
79
|
+
combined_output.extend(output_lines)
|
|
80
|
+
|
|
81
|
+
# Create message sequence for this tool
|
|
82
|
+
tool_messages = [
|
|
83
|
+
("error" if not result.success else "system",
|
|
84
|
+
"\n".join(combined_output), {})
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# Add spacing between tools if multiple
|
|
88
|
+
if len(tool_results) > 1 and i < len(tool_results) - 1:
|
|
89
|
+
tool_messages.append(("system", "", {}))
|
|
90
|
+
|
|
91
|
+
# Display tool messages using coordinator
|
|
92
|
+
self.message_coordinator.display_message_sequence(tool_messages)
|
|
93
|
+
|
|
94
|
+
logger.debug(f"Displayed {len(tool_results)} tool results")
|
|
95
|
+
|
|
96
|
+
def display_user_message(self, message: str) -> None:
|
|
97
|
+
"""Display user message through coordinator.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
message: User's input message
|
|
101
|
+
"""
|
|
102
|
+
# Don't display user messages in pipe mode
|
|
103
|
+
if getattr(self.renderer, 'pipe_mode', False):
|
|
104
|
+
logger.debug(f"Suppressing user message in pipe mode: {len(message)} chars")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
message_sequence = [("user", message, {})]
|
|
108
|
+
self.message_coordinator.display_message_sequence(message_sequence)
|
|
109
|
+
logger.debug(f"Displayed user message: {len(message)} chars")
|
|
110
|
+
|
|
111
|
+
def display_system_message(self, message: str) -> None:
|
|
112
|
+
"""Display system message through coordinator.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
message: System message to display
|
|
116
|
+
"""
|
|
117
|
+
message_sequence = [("system", message, {})]
|
|
118
|
+
self.message_coordinator.display_message_sequence(message_sequence)
|
|
119
|
+
logger.debug(f"Displayed system message: {message[:50]}...")
|
|
120
|
+
|
|
121
|
+
def display_error_message(self, error: str) -> None:
|
|
122
|
+
"""Display error message through coordinator.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
error: Error message to display
|
|
126
|
+
"""
|
|
127
|
+
message_sequence = [("error", f"Error: {error}", {})]
|
|
128
|
+
self.message_coordinator.display_message_sequence(message_sequence)
|
|
129
|
+
logger.debug(f"Displayed error message: {error[:50]}...")
|
|
130
|
+
|
|
131
|
+
def display_cancellation_message(self) -> None:
|
|
132
|
+
"""Display request cancellation message."""
|
|
133
|
+
# Don't display cancellation message in pipe mode (it's expected during cleanup)
|
|
134
|
+
pipe_mode = getattr(self.renderer, 'pipe_mode', False)
|
|
135
|
+
|
|
136
|
+
if hasattr(self.renderer, 'pipe_mode') and pipe_mode:
|
|
137
|
+
logger.debug("Suppressing cancellation message in pipe mode")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
message_sequence = [
|
|
141
|
+
("system", "Request cancelled", {}),
|
|
142
|
+
("system", "", {}) # Empty line for spacing
|
|
143
|
+
]
|
|
144
|
+
self.message_coordinator.display_message_sequence(message_sequence)
|
|
145
|
+
logger.debug("Displayed cancellation message")
|
|
146
|
+
|
|
147
|
+
def _format_tool_header(self, result, tool_data: Dict = None) -> str:
|
|
148
|
+
"""Format tool execution header with consistent styling.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
result: Tool execution result
|
|
152
|
+
tool_data: Original tool data for command/name extraction
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Formatted tool header string
|
|
156
|
+
"""
|
|
157
|
+
if result.tool_type == "terminal":
|
|
158
|
+
# Extract actual command from original tool data
|
|
159
|
+
command = tool_data.get("command", "unknown") if tool_data else result.tool_id
|
|
160
|
+
return f"\033[1;33m⟣\033[0m terminal({command})"
|
|
161
|
+
elif result.tool_type == "mcp_tool":
|
|
162
|
+
# Extract tool name and arguments from original tool data
|
|
163
|
+
tool_name = tool_data.get("name", "unknown") if tool_data else result.tool_id
|
|
164
|
+
arguments = tool_data.get("arguments", {}) if tool_data else {}
|
|
165
|
+
return f"\033[1;33m⟣\033[0m {tool_name}({arguments})"
|
|
166
|
+
elif result.tool_type.startswith("file_"):
|
|
167
|
+
# Extract filename/path from file operation data
|
|
168
|
+
display_info = self._extract_file_display_info(tool_data, result.tool_type)
|
|
169
|
+
return f"\033[1;33m⟣\033[0m {result.tool_type}({display_info})"
|
|
170
|
+
else:
|
|
171
|
+
return f"\033[1;33m⟣\033[0m {result.tool_type}({result.tool_id})"
|
|
172
|
+
|
|
173
|
+
def _extract_file_display_info(self, tool_data: Dict, tool_type: str) -> str:
|
|
174
|
+
"""Extract display information from file operation data.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
tool_data: Original tool data
|
|
178
|
+
tool_type: Type of file operation
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Filename or path to display
|
|
182
|
+
"""
|
|
183
|
+
if not tool_data:
|
|
184
|
+
return "unknown"
|
|
185
|
+
|
|
186
|
+
# Most file operations use 'file' key
|
|
187
|
+
if "file" in tool_data:
|
|
188
|
+
return tool_data["file"]
|
|
189
|
+
|
|
190
|
+
# Move/copy operations use 'from' and 'to'
|
|
191
|
+
if "from" in tool_data and "to" in tool_data:
|
|
192
|
+
return f"{tool_data['from']} → {tool_data['to']}"
|
|
193
|
+
|
|
194
|
+
# mkdir/rmdir use 'path'
|
|
195
|
+
if "path" in tool_data:
|
|
196
|
+
return tool_data["path"]
|
|
197
|
+
|
|
198
|
+
return "unknown"
|
|
199
|
+
|
|
200
|
+
def _format_tool_result(self, result) -> str:
|
|
201
|
+
"""Format tool execution result summary.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
result: Tool execution result
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Formatted result summary string
|
|
208
|
+
"""
|
|
209
|
+
if result.success:
|
|
210
|
+
# Count output characteristics for summary
|
|
211
|
+
output_lines = result.output.count('\n') + 1 if result.output else 0
|
|
212
|
+
output_chars = len(result.output) if result.output else 0
|
|
213
|
+
|
|
214
|
+
if result.tool_type == "terminal" and result.output:
|
|
215
|
+
return f"\033[32m ▮ Read {output_lines} lines ({output_chars} chars)\033[0m"
|
|
216
|
+
elif result.tool_type == "file_read" and result.output:
|
|
217
|
+
# Extract line count from output message "✓ Read X lines from..."
|
|
218
|
+
import re
|
|
219
|
+
match = re.search(r'Read (\d+) lines', result.output)
|
|
220
|
+
if match:
|
|
221
|
+
line_count = match.group(1)
|
|
222
|
+
return f"\033[32m ▮ Read {line_count} lines\033[0m"
|
|
223
|
+
return f"\033[32m ▮ Success\033[0m"
|
|
224
|
+
elif result.tool_type == "mcp_tool" and result.output:
|
|
225
|
+
preview = result.output[:50] + "..." if len(result.output) > 50 else result.output
|
|
226
|
+
return f"\033[32m ▮ {preview}\033[0m"
|
|
227
|
+
else:
|
|
228
|
+
return f"\033[32m ▮ Success\033[0m"
|
|
229
|
+
else:
|
|
230
|
+
return f"\033[31m ▮ Error: {result.error}\033[0m"
|
|
231
|
+
|
|
232
|
+
def _should_show_output(self, result) -> bool:
|
|
233
|
+
"""Determine if tool output should be displayed inline.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
result: Tool execution result
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
True if output should be shown
|
|
240
|
+
"""
|
|
241
|
+
return (result.success and
|
|
242
|
+
result.output and
|
|
243
|
+
len(result.output) < 500)
|
|
244
|
+
|
|
245
|
+
def _format_tool_output(self, result) -> List[str]:
|
|
246
|
+
"""Format tool output for inline display.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
result: Tool execution result
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of formatted output lines
|
|
253
|
+
"""
|
|
254
|
+
# Special formatting for file_edit with diff info
|
|
255
|
+
if (result.tool_type == "file_edit" and
|
|
256
|
+
hasattr(result, 'metadata') and
|
|
257
|
+
result.metadata and
|
|
258
|
+
'diff_info' in result.metadata):
|
|
259
|
+
return self._format_edit_diff(result)
|
|
260
|
+
|
|
261
|
+
# Default formatting for other outputs
|
|
262
|
+
output_lines = result.output.strip().split('\n')
|
|
263
|
+
formatted_lines = []
|
|
264
|
+
|
|
265
|
+
# Show first 20 lines with indentation
|
|
266
|
+
for line in output_lines[:20]:
|
|
267
|
+
formatted_lines.append(f" {line}")
|
|
268
|
+
|
|
269
|
+
# Add truncation message if needed
|
|
270
|
+
if len(output_lines) > 20:
|
|
271
|
+
remaining = len(output_lines) - 20
|
|
272
|
+
formatted_lines.append(f" ... ({remaining} more lines)")
|
|
273
|
+
|
|
274
|
+
return formatted_lines
|
|
275
|
+
|
|
276
|
+
def _format_edit_diff(self, result) -> List[str]:
|
|
277
|
+
"""Format file edit as a pretty condensed diff.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
result: Tool execution result with diff_info
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List of formatted diff lines
|
|
284
|
+
"""
|
|
285
|
+
diff_info = result.metadata.get('diff_info', {})
|
|
286
|
+
find_text = diff_info.get('find', '')
|
|
287
|
+
replace_text = diff_info.get('replace', '')
|
|
288
|
+
count = diff_info.get('count', 0)
|
|
289
|
+
|
|
290
|
+
formatted_lines = []
|
|
291
|
+
|
|
292
|
+
# Show the first line of output (✅ Replaced...)
|
|
293
|
+
first_line = result.output.split('\n')[0]
|
|
294
|
+
formatted_lines.append(f" {first_line}")
|
|
295
|
+
|
|
296
|
+
# Add pretty diff visualization
|
|
297
|
+
formatted_lines.append("")
|
|
298
|
+
|
|
299
|
+
# Removed lines (red with -)
|
|
300
|
+
removed_lines = find_text.split('\n')
|
|
301
|
+
for line in removed_lines[:3]: # Show max 3 lines
|
|
302
|
+
formatted_lines.append(f" \033[31m│- {line}\033[0m")
|
|
303
|
+
|
|
304
|
+
if len(removed_lines) > 3:
|
|
305
|
+
formatted_lines.append(f" \033[31m│ ... ({len(removed_lines) - 3} more lines)\033[0m")
|
|
306
|
+
|
|
307
|
+
# Separator
|
|
308
|
+
formatted_lines.append(" \033[90m│\033[0m")
|
|
309
|
+
|
|
310
|
+
# Added lines (green with +)
|
|
311
|
+
added_lines = replace_text.split('\n')
|
|
312
|
+
for line in added_lines[:3]: # Show max 3 lines
|
|
313
|
+
formatted_lines.append(f" \033[32m│+ {line}\033[0m")
|
|
314
|
+
|
|
315
|
+
if len(added_lines) > 3:
|
|
316
|
+
formatted_lines.append(f" \033[32m│ ... ({len(added_lines) - 3} more lines)\033[0m")
|
|
317
|
+
|
|
318
|
+
formatted_lines.append("")
|
|
319
|
+
|
|
320
|
+
# Add backup info if present
|
|
321
|
+
output_lines = result.output.split('\n')
|
|
322
|
+
for line in output_lines[1:]: # Skip first line (already shown)
|
|
323
|
+
if line.strip():
|
|
324
|
+
formatted_lines.append(f" {line}")
|
|
325
|
+
|
|
326
|
+
return formatted_lines
|
|
327
|
+
|
|
328
|
+
def display_generating_progress(self, estimated_tokens: int) -> None:
|
|
329
|
+
"""Display generating progress with token estimate.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
estimated_tokens: Estimated number of tokens being generated
|
|
333
|
+
"""
|
|
334
|
+
if estimated_tokens > 0:
|
|
335
|
+
self.renderer.update_thinking(True, f"Generating... ({estimated_tokens} tokens)")
|
|
336
|
+
else:
|
|
337
|
+
self.renderer.update_thinking(True, "Generating...")
|
|
338
|
+
logger.debug(f"Displaying generating progress: {estimated_tokens} tokens")
|
|
339
|
+
|
|
340
|
+
def clear_thinking_display(self) -> None:
|
|
341
|
+
"""Clear thinking/generating display."""
|
|
342
|
+
self.renderer.update_thinking(False)
|
|
343
|
+
logger.debug("Cleared thinking display")
|
|
344
|
+
|
|
345
|
+
def start_streaming_response(self) -> None:
|
|
346
|
+
"""Start a streaming response session.
|
|
347
|
+
|
|
348
|
+
This method initializes streaming mode, disabling atomic batching
|
|
349
|
+
for the duration of the response to allow real-time display.
|
|
350
|
+
"""
|
|
351
|
+
self._streaming_active = True
|
|
352
|
+
logger.debug("Started streaming response session")
|
|
353
|
+
|
|
354
|
+
def end_streaming_response(self) -> None:
|
|
355
|
+
"""End a streaming response session.
|
|
356
|
+
|
|
357
|
+
This method disables streaming mode and returns to normal
|
|
358
|
+
atomic batching behavior.
|
|
359
|
+
"""
|
|
360
|
+
self._streaming_active = False
|
|
361
|
+
logger.debug("Ended streaming response session")
|
|
362
|
+
|
|
363
|
+
def is_streaming_active(self) -> bool:
|
|
364
|
+
"""Check if streaming mode is currently active.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
True if streaming is active, False otherwise
|
|
368
|
+
"""
|
|
369
|
+
return self._streaming_active
|
|
370
|
+
|
|
371
|
+
def display_complete_response(self,
|
|
372
|
+
thinking_duration: float,
|
|
373
|
+
response: str,
|
|
374
|
+
tool_results: List[Any] = None,
|
|
375
|
+
original_tools: List[Dict] = None,
|
|
376
|
+
show_thinking_threshold: float = 0.1,
|
|
377
|
+
skip_response_content: bool = False) -> None:
|
|
378
|
+
"""Display complete response with thinking, content, and tools atomically.
|
|
379
|
+
|
|
380
|
+
This unified method ensures that thinking duration, assistant response,
|
|
381
|
+
and tool execution results all display together in a single atomic
|
|
382
|
+
operation, preventing commands from appearing after the response.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
thinking_duration: Time spent thinking in seconds
|
|
386
|
+
response: Assistant response content
|
|
387
|
+
tool_results: List of tool execution result objects (optional)
|
|
388
|
+
original_tools: List of original tool data for command extraction (optional)
|
|
389
|
+
show_thinking_threshold: Minimum duration to show thinking message
|
|
390
|
+
skip_response_content: Skip displaying response content (for streaming mode)
|
|
391
|
+
"""
|
|
392
|
+
message_sequence = []
|
|
393
|
+
pipe_mode = getattr(self.renderer, 'pipe_mode', False)
|
|
394
|
+
|
|
395
|
+
# Add thinking duration if meaningful (suppress in pipe mode)
|
|
396
|
+
if thinking_duration > show_thinking_threshold and not pipe_mode:
|
|
397
|
+
thought_message = f"Thought for {thinking_duration:.1f} seconds"
|
|
398
|
+
message_sequence.append(("system", thought_message, {}))
|
|
399
|
+
|
|
400
|
+
# Add assistant response if present and not skipped (for streaming mode)
|
|
401
|
+
if response.strip() and not skip_response_content:
|
|
402
|
+
message_sequence.append(("assistant", response, {}))
|
|
403
|
+
|
|
404
|
+
# Add tool results if present (suppress in pipe mode)
|
|
405
|
+
if tool_results and not pipe_mode:
|
|
406
|
+
for i, result in enumerate(tool_results):
|
|
407
|
+
# Get original tool data for display
|
|
408
|
+
tool_data = original_tools[i] if original_tools and i < len(original_tools) else {}
|
|
409
|
+
|
|
410
|
+
# Format tool execution display with consistent styling
|
|
411
|
+
tool_display = self._format_tool_header(result, tool_data)
|
|
412
|
+
result_display = self._format_tool_result(result)
|
|
413
|
+
|
|
414
|
+
# Build combined tool display message
|
|
415
|
+
combined_output = [tool_display, result_display]
|
|
416
|
+
|
|
417
|
+
# Add actual output if appropriate
|
|
418
|
+
if self._should_show_output(result):
|
|
419
|
+
output_lines = self._format_tool_output(result)
|
|
420
|
+
combined_output.extend(output_lines)
|
|
421
|
+
|
|
422
|
+
# Add tool message to sequence
|
|
423
|
+
message_sequence.append((
|
|
424
|
+
"error" if not result.success else "system",
|
|
425
|
+
"\n".join(combined_output),
|
|
426
|
+
{}
|
|
427
|
+
))
|
|
428
|
+
|
|
429
|
+
# Add spacing between tools if multiple
|
|
430
|
+
if len(tool_results) > 1 and i < len(tool_results) - 1:
|
|
431
|
+
message_sequence.append(("system", "", {}))
|
|
432
|
+
|
|
433
|
+
# Display everything atomically to prevent race conditions
|
|
434
|
+
if message_sequence:
|
|
435
|
+
self.message_coordinator.display_message_sequence(message_sequence)
|
|
436
|
+
logger.debug(f"Displayed complete response with {len(message_sequence)} messages atomically")
|
|
437
|
+
|
|
438
|
+
def get_display_stats(self) -> Dict[str, int]:
|
|
439
|
+
"""Get display operation statistics.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Dictionary with display operation counts
|
|
443
|
+
"""
|
|
444
|
+
# This could be enhanced with actual counters if needed
|
|
445
|
+
return {
|
|
446
|
+
"messages_displayed": 0, # Placeholder - could track actual counts
|
|
447
|
+
"tool_results_displayed": 0,
|
|
448
|
+
"thinking_displays": 0,
|
|
449
|
+
"streaming_sessions": 1 if self._streaming_active else 0
|
|
450
|
+
}
|
core/llm/model_router.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Model routing system for intelligent model selection.
|
|
2
|
+
|
|
3
|
+
Routes queries to appropriate models based on task requirements
|
|
4
|
+
and configured model capabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelRouter:
|
|
14
|
+
"""Intelligent model routing based on query analysis.
|
|
15
|
+
|
|
16
|
+
Routes different types of queries to appropriate models
|
|
17
|
+
for optimal performance and cost efficiency.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config):
|
|
21
|
+
"""Initialize model router.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config: Configuration manager
|
|
25
|
+
"""
|
|
26
|
+
self.config = config
|
|
27
|
+
|
|
28
|
+
# Model configurations
|
|
29
|
+
self.models = {
|
|
30
|
+
"fast": config.get("core.llm.models.fast", {
|
|
31
|
+
"name": "qwen/qwen3-0.6b",
|
|
32
|
+
"api_url": "http://localhost:1234",
|
|
33
|
+
"temperature": 0.3,
|
|
34
|
+
"max_tokens": 500
|
|
35
|
+
}),
|
|
36
|
+
"reasoning": config.get("core.llm.models.reasoning", {
|
|
37
|
+
"name": "qwen/qwen3-4b",
|
|
38
|
+
"api_url": "http://localhost:1234",
|
|
39
|
+
"temperature": 0.7,
|
|
40
|
+
"max_tokens": 2000
|
|
41
|
+
}),
|
|
42
|
+
"coding": config.get("core.llm.models.coding", {
|
|
43
|
+
"name": "qwen/qwen3-4b",
|
|
44
|
+
"api_url": "http://localhost:1234",
|
|
45
|
+
"temperature": 0.5,
|
|
46
|
+
"max_tokens": 3000
|
|
47
|
+
}),
|
|
48
|
+
"documentation": config.get("core.llm.models.documentation", {
|
|
49
|
+
"name": "qwen/qwen3-4b",
|
|
50
|
+
"api_url": "http://localhost:1234",
|
|
51
|
+
"temperature": 0.6,
|
|
52
|
+
"max_tokens": 2000
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Default model
|
|
57
|
+
self.default_model = "reasoning"
|
|
58
|
+
|
|
59
|
+
logger.info(f"Model router initialized with {len(self.models)} model types")
|
|
60
|
+
|
|
61
|
+
def analyze_query(self, query: str) -> str:
|
|
62
|
+
"""Analyze query to determine appropriate model type.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
query: User query to analyze
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Model type to use
|
|
69
|
+
"""
|
|
70
|
+
query_lower = query.lower()
|
|
71
|
+
|
|
72
|
+
# Quick responses
|
|
73
|
+
if len(query) < 50 and "?" in query:
|
|
74
|
+
if any(word in query_lower for word in ["what", "when", "where", "who"]):
|
|
75
|
+
return "fast"
|
|
76
|
+
|
|
77
|
+
# Coding tasks
|
|
78
|
+
if any(word in query_lower for word in ["code", "function", "class", "debug", "implement", "fix"]):
|
|
79
|
+
return "coding"
|
|
80
|
+
|
|
81
|
+
# Documentation tasks
|
|
82
|
+
if any(word in query_lower for word in ["document", "explain", "describe", "readme", "comment"]):
|
|
83
|
+
return "documentation"
|
|
84
|
+
|
|
85
|
+
# Complex reasoning
|
|
86
|
+
if any(word in query_lower for word in ["analyze", "compare", "evaluate", "design", "architect"]):
|
|
87
|
+
return "reasoning"
|
|
88
|
+
|
|
89
|
+
# Short greetings or simple responses
|
|
90
|
+
if len(query) < 20:
|
|
91
|
+
return "fast"
|
|
92
|
+
|
|
93
|
+
# Default to reasoning for everything else
|
|
94
|
+
return self.default_model
|
|
95
|
+
|
|
96
|
+
def get_model_config(self, model_type: Optional[str] = None) -> Dict[str, Any]:
|
|
97
|
+
"""Get configuration for a specific model type.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
model_type: Type of model (fast, reasoning, coding, documentation)
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Model configuration dictionary
|
|
104
|
+
"""
|
|
105
|
+
if model_type is None:
|
|
106
|
+
model_type = self.default_model
|
|
107
|
+
|
|
108
|
+
if model_type not in self.models:
|
|
109
|
+
logger.warning(f"Unknown model type: {model_type}, using default")
|
|
110
|
+
model_type = self.default_model
|
|
111
|
+
|
|
112
|
+
return self.models[model_type]
|
|
113
|
+
|
|
114
|
+
def route_query(self, query: str, force_model: Optional[str] = None) -> Dict[str, Any]:
|
|
115
|
+
"""Route a query to the appropriate model.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
query: User query to route
|
|
119
|
+
force_model: Optional model type to force
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Routing decision with model configuration
|
|
123
|
+
"""
|
|
124
|
+
# Allow forcing a specific model
|
|
125
|
+
if force_model and force_model in self.models:
|
|
126
|
+
model_type = force_model
|
|
127
|
+
reason = "forced"
|
|
128
|
+
else:
|
|
129
|
+
model_type = self.analyze_query(query)
|
|
130
|
+
reason = "analyzed"
|
|
131
|
+
|
|
132
|
+
model_config = self.get_model_config(model_type)
|
|
133
|
+
|
|
134
|
+
routing_decision = {
|
|
135
|
+
"model_type": model_type,
|
|
136
|
+
"model_config": model_config,
|
|
137
|
+
"reason": reason,
|
|
138
|
+
"query_length": len(query),
|
|
139
|
+
"query_complexity": self._assess_complexity(query)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
logger.info(f"Routed query to {model_type} model ({reason})")
|
|
143
|
+
return routing_decision
|
|
144
|
+
|
|
145
|
+
def _assess_complexity(self, query: str) -> str:
|
|
146
|
+
"""Assess query complexity.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
query: Query to assess
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Complexity level (simple, moderate, complex)
|
|
153
|
+
"""
|
|
154
|
+
# Simple heuristics for complexity
|
|
155
|
+
word_count = len(query.split())
|
|
156
|
+
has_code = "```" in query
|
|
157
|
+
has_multiple_questions = query.count("?") > 1
|
|
158
|
+
|
|
159
|
+
if word_count < 10 and not has_code:
|
|
160
|
+
return "simple"
|
|
161
|
+
elif word_count > 50 or has_code or has_multiple_questions:
|
|
162
|
+
return "complex"
|
|
163
|
+
else:
|
|
164
|
+
return "moderate"
|
|
165
|
+
|
|
166
|
+
def update_model_config(self, model_type: str, config: Dict[str, Any]) -> bool:
|
|
167
|
+
"""Update configuration for a model type.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
model_type: Type of model to update
|
|
171
|
+
config: New configuration
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if update successful
|
|
175
|
+
"""
|
|
176
|
+
if model_type not in self.models:
|
|
177
|
+
logger.warning(f"Creating new model type: {model_type}")
|
|
178
|
+
|
|
179
|
+
self.models[model_type] = config
|
|
180
|
+
logger.info(f"Updated configuration for {model_type} model")
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
def list_available_models(self) -> List[Dict[str, Any]]:
|
|
184
|
+
"""List all available model configurations.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of model configurations
|
|
188
|
+
"""
|
|
189
|
+
models = []
|
|
190
|
+
for model_type, config in self.models.items():
|
|
191
|
+
models.append({
|
|
192
|
+
"type": model_type,
|
|
193
|
+
"name": config.get("name", "unknown"),
|
|
194
|
+
"api_url": config.get("api_url", "unknown"),
|
|
195
|
+
"is_default": model_type == self.default_model
|
|
196
|
+
})
|
|
197
|
+
return models
|
|
198
|
+
|
|
199
|
+
def set_default_model(self, model_type: str) -> bool:
|
|
200
|
+
"""Set the default model type.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
model_type: Model type to set as default
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if successful
|
|
207
|
+
"""
|
|
208
|
+
if model_type not in self.models:
|
|
209
|
+
logger.error(f"Cannot set unknown model type as default: {model_type}")
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
self.default_model = model_type
|
|
213
|
+
logger.info(f"Set default model to: {model_type}")
|
|
214
|
+
return True
|