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,364 @@
|
|
|
1
|
+
"""Response processing and formatting for LLM outputs.
|
|
2
|
+
|
|
3
|
+
Handles response parsing, formatting, and special tag processing
|
|
4
|
+
for LLM responses including thinking tags and final responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResponseProcessor:
|
|
16
|
+
"""Process and format LLM responses.
|
|
17
|
+
|
|
18
|
+
Handles parsing of special tags, response formatting,
|
|
19
|
+
and content extraction from raw LLM outputs.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
"""Initialize response processor."""
|
|
24
|
+
# Regex patterns for tag extraction
|
|
25
|
+
self.thinking_pattern = re.compile(
|
|
26
|
+
r'<think>(.*?)</think>',
|
|
27
|
+
re.DOTALL | re.IGNORECASE
|
|
28
|
+
)
|
|
29
|
+
self.terminal_pattern = re.compile(
|
|
30
|
+
r'<terminal>(.*?)</terminal>',
|
|
31
|
+
re.DOTALL | re.IGNORECASE
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger.info("Response processor initialized")
|
|
35
|
+
|
|
36
|
+
def process_response(self, raw_response: str) -> Dict[str, Any]:
|
|
37
|
+
"""Process raw LLM response.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
raw_response: Raw response from LLM
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Processed response with extracted components
|
|
44
|
+
"""
|
|
45
|
+
# Extract components
|
|
46
|
+
thinking_content = self._extract_thinking(raw_response)
|
|
47
|
+
terminal_commands = self._extract_terminal_commands(raw_response)
|
|
48
|
+
|
|
49
|
+
# Clean main content
|
|
50
|
+
clean_content = self._clean_content(raw_response)
|
|
51
|
+
|
|
52
|
+
# Determine response type
|
|
53
|
+
response_type = self._determine_response_type(
|
|
54
|
+
raw_response,
|
|
55
|
+
thinking_content,
|
|
56
|
+
terminal_commands
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
processed = {
|
|
60
|
+
"type": response_type,
|
|
61
|
+
"content": clean_content,
|
|
62
|
+
"raw": raw_response,
|
|
63
|
+
"components": {
|
|
64
|
+
"thinking": thinking_content,
|
|
65
|
+
"terminal_commands": terminal_commands
|
|
66
|
+
},
|
|
67
|
+
"metadata": {
|
|
68
|
+
"has_thinking": bool(thinking_content),
|
|
69
|
+
"has_terminal_commands": bool(terminal_commands),
|
|
70
|
+
"word_count": len(clean_content.split()),
|
|
71
|
+
"char_count": len(clean_content)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.debug(f"Processed response: type={response_type}, components={len(processed['components'])}")
|
|
76
|
+
return processed
|
|
77
|
+
|
|
78
|
+
def _extract_thinking(self, content: str) -> List[str]:
|
|
79
|
+
"""Extract thinking tags from content.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
content: Raw content with potential thinking tags
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of thinking content blocks
|
|
86
|
+
"""
|
|
87
|
+
matches = self.thinking_pattern.findall(content)
|
|
88
|
+
return [match.strip() for match in matches if match.strip()]
|
|
89
|
+
|
|
90
|
+
def _extract_terminal_commands(self, content: str) -> List[str]:
|
|
91
|
+
"""Extract terminal commands from content.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
content: Raw content with potential terminal tags
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of terminal commands
|
|
98
|
+
"""
|
|
99
|
+
matches = self.terminal_pattern.findall(content)
|
|
100
|
+
return [match.strip() for match in matches if match.strip()]
|
|
101
|
+
|
|
102
|
+
def _extract_tool_calls(self, content: str) -> List[Dict[str, Any]]:
|
|
103
|
+
"""Extract tool calls from content.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
content: Raw content with potential tool calls
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of parsed tool calls
|
|
110
|
+
"""
|
|
111
|
+
tool_calls = []
|
|
112
|
+
matches = self.tool_call_pattern.findall(content)
|
|
113
|
+
|
|
114
|
+
for match in matches:
|
|
115
|
+
try:
|
|
116
|
+
# Try to parse as JSON
|
|
117
|
+
tool_data = json.loads(match)
|
|
118
|
+
tool_calls.append(tool_data)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
# Parse as plain text command
|
|
121
|
+
lines = match.strip().split('\n')
|
|
122
|
+
if lines:
|
|
123
|
+
tool_calls.append({
|
|
124
|
+
"command": lines[0],
|
|
125
|
+
"raw": match
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return tool_calls
|
|
129
|
+
|
|
130
|
+
def _extract_code_blocks(self, content: str) -> List[Dict[str, str]]:
|
|
131
|
+
"""Extract code blocks from content.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
content: Raw content with potential code blocks
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of code blocks with language and content
|
|
138
|
+
"""
|
|
139
|
+
code_blocks = []
|
|
140
|
+
matches = self.code_block_pattern.findall(content)
|
|
141
|
+
|
|
142
|
+
for language, code in matches:
|
|
143
|
+
code_blocks.append({
|
|
144
|
+
"language": language or "plain",
|
|
145
|
+
"code": code.strip()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return code_blocks
|
|
149
|
+
|
|
150
|
+
def _clean_content(self, content: str) -> str:
|
|
151
|
+
"""Remove special tags from content.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
content: Raw content with tags
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Cleaned content without special tags
|
|
158
|
+
"""
|
|
159
|
+
# Remove thinking tags
|
|
160
|
+
cleaned = self.thinking_pattern.sub('', content)
|
|
161
|
+
|
|
162
|
+
# Remove final response tags but keep content
|
|
163
|
+
final_match = self.final_response_pattern.search(cleaned)
|
|
164
|
+
if final_match:
|
|
165
|
+
cleaned = self.final_response_pattern.sub(final_match.group(1), cleaned)
|
|
166
|
+
|
|
167
|
+
# Remove tool call tags
|
|
168
|
+
cleaned = self.tool_call_pattern.sub('', cleaned)
|
|
169
|
+
|
|
170
|
+
# Clean up extra whitespace
|
|
171
|
+
cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
|
|
172
|
+
cleaned = cleaned.strip()
|
|
173
|
+
|
|
174
|
+
return cleaned
|
|
175
|
+
|
|
176
|
+
def _determine_response_type(self, raw: str, thinking: List[str],
|
|
177
|
+
final: Optional[str], tools: List[Dict]) -> str:
|
|
178
|
+
"""Determine the type of response.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
raw: Raw response content
|
|
182
|
+
thinking: Extracted thinking content
|
|
183
|
+
final: Extracted final response
|
|
184
|
+
tools: Extracted tool calls
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Response type identifier
|
|
188
|
+
"""
|
|
189
|
+
if final:
|
|
190
|
+
return "final"
|
|
191
|
+
elif tools:
|
|
192
|
+
return "tool_execution"
|
|
193
|
+
elif thinking:
|
|
194
|
+
return "reasoning"
|
|
195
|
+
elif "?" in raw:
|
|
196
|
+
return "question"
|
|
197
|
+
elif len(raw) < 100:
|
|
198
|
+
return "brief"
|
|
199
|
+
else:
|
|
200
|
+
return "standard"
|
|
201
|
+
|
|
202
|
+
def format_for_display(self, processed_response: Dict[str, Any]) -> str:
|
|
203
|
+
"""Format processed response for display.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
processed_response: Processed response data
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Formatted string for display
|
|
210
|
+
"""
|
|
211
|
+
components = processed_response.get("components", {})
|
|
212
|
+
|
|
213
|
+
# Build display string
|
|
214
|
+
display_parts = []
|
|
215
|
+
|
|
216
|
+
# Add thinking if present (dimmed)
|
|
217
|
+
thinking = components.get("thinking", [])
|
|
218
|
+
if thinking:
|
|
219
|
+
for thought in thinking:
|
|
220
|
+
display_parts.append(f"[dim]{thought}[/dim]")
|
|
221
|
+
display_parts.append("")
|
|
222
|
+
|
|
223
|
+
# Add main content or final response
|
|
224
|
+
if components.get("final_response"):
|
|
225
|
+
display_parts.append(components["final_response"])
|
|
226
|
+
else:
|
|
227
|
+
display_parts.append(processed_response.get("content", ""))
|
|
228
|
+
|
|
229
|
+
return "\n".join(display_parts)
|
|
230
|
+
|
|
231
|
+
def extract_metrics(self, processed_response: Dict[str, Any]) -> Dict[str, Any]:
|
|
232
|
+
"""Extract metrics from processed response.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
processed_response: Processed response data
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Response metrics
|
|
239
|
+
"""
|
|
240
|
+
metadata = processed_response.get("metadata", {})
|
|
241
|
+
components = processed_response.get("components", {})
|
|
242
|
+
|
|
243
|
+
metrics = {
|
|
244
|
+
"response_type": processed_response.get("type", "unknown"),
|
|
245
|
+
"word_count": metadata.get("word_count", 0),
|
|
246
|
+
"char_count": metadata.get("char_count", 0),
|
|
247
|
+
"thinking_blocks": len(components.get("thinking", [])),
|
|
248
|
+
"code_blocks": len(components.get("code_blocks", [])),
|
|
249
|
+
"tool_calls": len(components.get("tool_calls", [])),
|
|
250
|
+
"has_final_response": metadata.get("has_final_response", False),
|
|
251
|
+
"complexity": self._assess_complexity(processed_response)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return metrics
|
|
255
|
+
|
|
256
|
+
def _assess_complexity(self, processed_response: Dict[str, Any]) -> str:
|
|
257
|
+
"""Assess response complexity.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
processed_response: Processed response data
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Complexity level (simple, moderate, complex)
|
|
264
|
+
"""
|
|
265
|
+
metadata = processed_response.get("metadata", {})
|
|
266
|
+
|
|
267
|
+
# Calculate complexity score
|
|
268
|
+
score = 0
|
|
269
|
+
|
|
270
|
+
if metadata.get("word_count", 0) > 200:
|
|
271
|
+
score += 2
|
|
272
|
+
elif metadata.get("word_count", 0) > 100:
|
|
273
|
+
score += 1
|
|
274
|
+
|
|
275
|
+
if metadata.get("has_thinking"):
|
|
276
|
+
score += 2
|
|
277
|
+
|
|
278
|
+
if metadata.get("has_code"):
|
|
279
|
+
score += 1
|
|
280
|
+
|
|
281
|
+
if metadata.get("has_tool_calls"):
|
|
282
|
+
score += 1
|
|
283
|
+
|
|
284
|
+
# Map score to complexity level
|
|
285
|
+
if score >= 4:
|
|
286
|
+
return "complex"
|
|
287
|
+
elif score >= 2:
|
|
288
|
+
return "moderate"
|
|
289
|
+
else:
|
|
290
|
+
return "simple"
|
|
291
|
+
|
|
292
|
+
def merge_streaming_chunks(self, chunks: List[str]) -> str:
|
|
293
|
+
"""Merge streaming response chunks.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
chunks: List of response chunks
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Merged response content
|
|
300
|
+
"""
|
|
301
|
+
# Join chunks
|
|
302
|
+
merged = "".join(chunks)
|
|
303
|
+
|
|
304
|
+
# Fix any broken tags from chunking
|
|
305
|
+
merged = self._fix_broken_tags(merged)
|
|
306
|
+
|
|
307
|
+
return merged
|
|
308
|
+
|
|
309
|
+
def _fix_broken_tags(self, content: str) -> str:
|
|
310
|
+
"""Fix broken tags from streaming.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
content: Content with potentially broken tags
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Content with fixed tags
|
|
317
|
+
"""
|
|
318
|
+
# Fix broken thinking tags
|
|
319
|
+
content = re.sub(r'<thi\s*nk>', '<think>', content, flags=re.IGNORECASE)
|
|
320
|
+
content = re.sub(r'</thi\s*nk>', '</think>', content, flags=re.IGNORECASE)
|
|
321
|
+
|
|
322
|
+
# Fix broken final response tags
|
|
323
|
+
content = re.sub(r'<final_res\s*ponse>', '<final_response>', content, flags=re.IGNORECASE)
|
|
324
|
+
content = re.sub(r'</final_res\s*ponse>', '</final_response>', content, flags=re.IGNORECASE)
|
|
325
|
+
|
|
326
|
+
return content
|
|
327
|
+
|
|
328
|
+
def validate_response(self, response: str) -> Tuple[bool, List[str]]:
|
|
329
|
+
"""Validate response format and content.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
response: Response to validate
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Tuple of (is_valid, list_of_issues)
|
|
336
|
+
"""
|
|
337
|
+
issues = []
|
|
338
|
+
|
|
339
|
+
# Check for unclosed tags
|
|
340
|
+
if '<think>' in response and '</think>' not in response:
|
|
341
|
+
issues.append("Unclosed thinking tag")
|
|
342
|
+
|
|
343
|
+
if '<final_response>' in response and '</final_response>' not in response:
|
|
344
|
+
issues.append("Unclosed final response tag")
|
|
345
|
+
|
|
346
|
+
# Check for empty response
|
|
347
|
+
if not response.strip():
|
|
348
|
+
issues.append("Empty response")
|
|
349
|
+
|
|
350
|
+
# Check for truncation indicators
|
|
351
|
+
if response.endswith('...') and len(response) > 1000:
|
|
352
|
+
issues.append("Response appears truncated")
|
|
353
|
+
|
|
354
|
+
# Check for malformed JSON in tool calls
|
|
355
|
+
tool_matches = self.tool_call_pattern.findall(response)
|
|
356
|
+
for match in tool_matches:
|
|
357
|
+
if match.strip().startswith('{'):
|
|
358
|
+
try:
|
|
359
|
+
json.loads(match)
|
|
360
|
+
except json.JSONDecodeError:
|
|
361
|
+
issues.append("Malformed JSON in tool call")
|
|
362
|
+
|
|
363
|
+
is_valid = len(issues) == 0
|
|
364
|
+
return is_valid, issues
|