kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
core/llm/response_parser.py
CHANGED
|
@@ -96,7 +96,16 @@ class FileOperationParser:
|
|
|
96
96
|
re.DOTALL | re.IGNORECASE
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
# Special pattern for agent/skill file generation
|
|
100
|
+
# Uses @@@FILE/@@@END syntax to avoid XML tag conflicts
|
|
101
|
+
# Format: @@@FILE path/to/file.md\ncontent\n@@@END
|
|
102
|
+
# Path must contain / to be valid (prevents matching garbage like "@@@FILE blocks.")
|
|
103
|
+
self.agent_files_pattern = re.compile(
|
|
104
|
+
r'@@@FILE\s+(\S+/\S+)\n(.*?)@@@END',
|
|
105
|
+
re.DOTALL
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
logger.debug("File operation parser initialized with 15 operation patterns")
|
|
100
109
|
|
|
101
110
|
def parse_response(self, llm_response: str) -> List[Dict[str, Any]]:
|
|
102
111
|
"""Extract all file operations from LLM response.
|
|
@@ -109,7 +118,16 @@ class FileOperationParser:
|
|
|
109
118
|
"""
|
|
110
119
|
operations = []
|
|
111
120
|
|
|
112
|
-
# Parse
|
|
121
|
+
# FIRST: Parse @@@FILE blocks before any content stripping
|
|
122
|
+
# This protects inner content from being mangled by tag stripping
|
|
123
|
+
agent_files_ops, response_without_agent_files = self._parse_agent_files(llm_response)
|
|
124
|
+
operations.extend(agent_files_ops)
|
|
125
|
+
|
|
126
|
+
# Use the cleaned response for remaining parsing
|
|
127
|
+
llm_response = response_without_agent_files
|
|
128
|
+
|
|
129
|
+
# Parse operations that contain <content> blocks from full text first
|
|
130
|
+
# These need the full response to extract their content properly
|
|
113
131
|
operations.extend(self._parse_operations(
|
|
114
132
|
self.edit_pattern, self._parse_edit_block, llm_response, "edit"
|
|
115
133
|
))
|
|
@@ -121,40 +139,51 @@ class FileOperationParser:
|
|
|
121
139
|
llm_response, "create_overwrite"
|
|
122
140
|
))
|
|
123
141
|
operations.extend(self._parse_operations(
|
|
124
|
-
self.
|
|
142
|
+
self.append_pattern, self._parse_append_block, llm_response, "append"
|
|
125
143
|
))
|
|
126
144
|
operations.extend(self._parse_operations(
|
|
127
|
-
self.
|
|
145
|
+
self.insert_after_pattern, self._parse_insert_after_block,
|
|
146
|
+
llm_response, "insert_after"
|
|
128
147
|
))
|
|
129
148
|
operations.extend(self._parse_operations(
|
|
130
|
-
self.
|
|
149
|
+
self.insert_before_pattern, self._parse_insert_before_block,
|
|
150
|
+
llm_response, "insert_before"
|
|
131
151
|
))
|
|
152
|
+
|
|
153
|
+
# Strip <content>...</content> blocks to avoid parsing XML examples
|
|
154
|
+
# inside file content as actual commands (e.g., skill files with <read> examples)
|
|
155
|
+
text_without_content = re.sub(
|
|
156
|
+
r'<content>.*?</content>',
|
|
157
|
+
'',
|
|
158
|
+
llm_response,
|
|
159
|
+
flags=re.DOTALL | re.IGNORECASE
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Parse remaining operations from text with content blocks removed
|
|
132
163
|
operations.extend(self._parse_operations(
|
|
133
|
-
self.
|
|
134
|
-
llm_response, "copy_overwrite"
|
|
164
|
+
self.delete_pattern, self._parse_delete_block, text_without_content, "delete"
|
|
135
165
|
))
|
|
136
166
|
operations.extend(self._parse_operations(
|
|
137
|
-
self.
|
|
167
|
+
self.move_pattern, self._parse_move_block, text_without_content, "move"
|
|
138
168
|
))
|
|
139
169
|
operations.extend(self._parse_operations(
|
|
140
|
-
self.
|
|
141
|
-
llm_response, "insert_after"
|
|
170
|
+
self.copy_pattern, self._parse_copy_block, text_without_content, "copy"
|
|
142
171
|
))
|
|
143
172
|
operations.extend(self._parse_operations(
|
|
144
|
-
self.
|
|
145
|
-
|
|
173
|
+
self.copy_overwrite_pattern, self._parse_copy_overwrite_block,
|
|
174
|
+
text_without_content, "copy_overwrite"
|
|
146
175
|
))
|
|
147
176
|
operations.extend(self._parse_operations(
|
|
148
|
-
self.mkdir_pattern, self._parse_mkdir_block,
|
|
177
|
+
self.mkdir_pattern, self._parse_mkdir_block, text_without_content, "mkdir"
|
|
149
178
|
))
|
|
150
179
|
operations.extend(self._parse_operations(
|
|
151
|
-
self.rmdir_pattern, self._parse_rmdir_block,
|
|
180
|
+
self.rmdir_pattern, self._parse_rmdir_block, text_without_content, "rmdir"
|
|
152
181
|
))
|
|
153
182
|
operations.extend(self._parse_operations(
|
|
154
|
-
self.read_pattern, self._parse_read_block,
|
|
183
|
+
self.read_pattern, self._parse_read_block, text_without_content, "read"
|
|
155
184
|
))
|
|
156
185
|
operations.extend(self._parse_operations(
|
|
157
|
-
self.grep_pattern, self._parse_grep_block,
|
|
186
|
+
self.grep_pattern, self._parse_grep_block, text_without_content, "grep"
|
|
158
187
|
))
|
|
159
188
|
|
|
160
189
|
if operations:
|
|
@@ -162,6 +191,49 @@ class FileOperationParser:
|
|
|
162
191
|
|
|
163
192
|
return operations
|
|
164
193
|
|
|
194
|
+
def _parse_agent_files(self, llm_response: str) -> Tuple[List[Dict[str, Any]], str]:
|
|
195
|
+
"""Parse @@@FILE blocks and extract file operations.
|
|
196
|
+
|
|
197
|
+
Uses simple line-based syntax to avoid XML tag conflicts:
|
|
198
|
+
@@@FILE path/to/file.md
|
|
199
|
+
content with <create> <edit> examples - no conflicts
|
|
200
|
+
@@@END
|
|
201
|
+
|
|
202
|
+
This parser runs FIRST to protect inner content from tag stripping.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
llm_response: Raw LLM response text
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Tuple of (operations list, response with @@@FILE blocks removed)
|
|
209
|
+
"""
|
|
210
|
+
operations = []
|
|
211
|
+
cleaned_response = llm_response
|
|
212
|
+
|
|
213
|
+
for i, match in enumerate(self.agent_files_pattern.finditer(llm_response)):
|
|
214
|
+
filepath = match.group(1).strip()
|
|
215
|
+
content = match.group(2)
|
|
216
|
+
|
|
217
|
+
# Remove trailing newline if present (before @@@END)
|
|
218
|
+
if content.endswith('\n'):
|
|
219
|
+
content = content[:-1]
|
|
220
|
+
|
|
221
|
+
operations.append({
|
|
222
|
+
"type": "file_create",
|
|
223
|
+
"id": f"agent_file_{i}",
|
|
224
|
+
"file": filepath,
|
|
225
|
+
"content": content
|
|
226
|
+
})
|
|
227
|
+
logger.debug(f"Parsed agent file operation: {filepath}")
|
|
228
|
+
|
|
229
|
+
# Remove this block from the response
|
|
230
|
+
cleaned_response = cleaned_response.replace(match.group(0), '', 1)
|
|
231
|
+
|
|
232
|
+
if operations:
|
|
233
|
+
logger.info(f"Parsed {len(operations)} file operations from @@@FILE blocks")
|
|
234
|
+
|
|
235
|
+
return operations, cleaned_response
|
|
236
|
+
|
|
165
237
|
def _parse_operations(
|
|
166
238
|
self,
|
|
167
239
|
pattern: re.Pattern,
|
|
@@ -362,6 +434,8 @@ class FileOperationParser:
|
|
|
362
434
|
"""Parse <read> block."""
|
|
363
435
|
file_path = self._extract_tag("file", content).strip()
|
|
364
436
|
lines_spec = self._extract_tag("lines", content, required=False)
|
|
437
|
+
offset = self._extract_tag("offset", content, required=False)
|
|
438
|
+
limit = self._extract_tag("limit", content, required=False)
|
|
365
439
|
|
|
366
440
|
result = {
|
|
367
441
|
"type": "file_read",
|
|
@@ -370,6 +444,10 @@ class FileOperationParser:
|
|
|
370
444
|
|
|
371
445
|
if lines_spec:
|
|
372
446
|
result["lines"] = lines_spec.strip()
|
|
447
|
+
if offset:
|
|
448
|
+
result["offset"] = int(offset.strip())
|
|
449
|
+
if limit:
|
|
450
|
+
result["limit"] = int(limit.strip())
|
|
373
451
|
|
|
374
452
|
return result
|
|
375
453
|
|
|
@@ -422,11 +500,31 @@ class ResponseParser:
|
|
|
422
500
|
re.DOTALL | re.IGNORECASE
|
|
423
501
|
)
|
|
424
502
|
|
|
503
|
+
# Native-style tool_call tags: <tool_call>name</tool_call> or with JSON args
|
|
504
|
+
# Supports: <tool_call>search_nodes</tool_call>
|
|
505
|
+
# <tool_call>{"name": "search_nodes", "arguments": {...}}</tool_call>
|
|
506
|
+
self.tool_call_pattern = re.compile(
|
|
507
|
+
r'<tool_call>(.*?)</tool_call>',
|
|
508
|
+
re.DOTALL | re.IGNORECASE
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Question gate tags - suspend tool execution when present
|
|
512
|
+
self.question_pattern = re.compile(
|
|
513
|
+
r'<question>(.*?)</question>',
|
|
514
|
+
re.DOTALL | re.IGNORECASE
|
|
515
|
+
)
|
|
516
|
+
|
|
425
517
|
# File operations parser
|
|
426
518
|
self.file_ops_parser = FileOperationParser()
|
|
427
519
|
|
|
428
520
|
logger.info("Response parser initialized with comprehensive tag support + file operations")
|
|
429
521
|
|
|
522
|
+
async def initialize(self) -> bool:
|
|
523
|
+
"""Initialize the response parser."""
|
|
524
|
+
self.is_initialized = True
|
|
525
|
+
logger.debug("Response parser async initialization complete")
|
|
526
|
+
return True
|
|
527
|
+
|
|
430
528
|
def parse_response(self, raw_response: str) -> Dict[str, Any]:
|
|
431
529
|
"""Parse LLM response and extract all components.
|
|
432
530
|
|
|
@@ -449,13 +547,21 @@ class ResponseParser:
|
|
|
449
547
|
logger.warning(f"🔍 BUG-011 DIAGNOSTIC: Found {abs(orphaned_closes)} orphaned <think> tags (unclosed)")
|
|
450
548
|
|
|
451
549
|
# Extract all components
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
tool_calls = self._extract_tool_calls(raw_response)
|
|
550
|
+
# IMPORTANT: Parse @@@FILE blocks FIRST to get cleaned response
|
|
551
|
+
# This prevents XML examples inside @@@FILE from being executed
|
|
455
552
|
file_operations = self.file_ops_parser.parse_response(raw_response)
|
|
456
553
|
|
|
457
|
-
#
|
|
458
|
-
|
|
554
|
+
# Get response with @@@FILE blocks removed for other parsing
|
|
555
|
+
_, response_without_agent_files = self.file_ops_parser._parse_agent_files(raw_response)
|
|
556
|
+
|
|
557
|
+
# Extract other tools from the CLEANED response (not raw)
|
|
558
|
+
thinking_blocks = self._extract_thinking(response_without_agent_files)
|
|
559
|
+
terminal_commands = self._extract_terminal_commands(response_without_agent_files)
|
|
560
|
+
tool_calls = self._extract_tool_calls(response_without_agent_files)
|
|
561
|
+
question_content = self._extract_question(response_without_agent_files)
|
|
562
|
+
|
|
563
|
+
# Clean content (remove all tags) - use cleaned response
|
|
564
|
+
clean_content = self._clean_content(response_without_agent_files)
|
|
459
565
|
|
|
460
566
|
# DIAGNOSTIC: Verify defensive fix effectiveness
|
|
461
567
|
if '</think>' in clean_content or '<think>' in clean_content:
|
|
@@ -466,29 +572,36 @@ class ResponseParser:
|
|
|
466
572
|
elif orphaned_closes > 0:
|
|
467
573
|
logger.info(f"✅ BUG-011 SUCCESS: Defensive fix removed {orphaned_closes} orphaned tags")
|
|
468
574
|
|
|
469
|
-
#
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
575
|
+
# Count total tools
|
|
576
|
+
total_tools = len(terminal_commands) + len(tool_calls) + len(file_operations)
|
|
577
|
+
|
|
578
|
+
# Question gate: if question present, mark turn as completed but flag tools as pending
|
|
579
|
+
# This causes the system to stop and wait for user input
|
|
580
|
+
has_question = question_content is not None
|
|
581
|
+
|
|
582
|
+
# Determine if turn is completed
|
|
583
|
+
# Turn is completed if: no tools OR question present (tools suspended)
|
|
584
|
+
turn_completed = (total_tools == 0) or has_question
|
|
475
585
|
|
|
476
586
|
parsed = {
|
|
477
587
|
"raw": raw_response,
|
|
478
588
|
"content": clean_content,
|
|
479
589
|
"turn_completed": turn_completed,
|
|
590
|
+
"question_gate_active": has_question and total_tools > 0, # Tools suspended
|
|
480
591
|
"components": {
|
|
481
592
|
"thinking": thinking_blocks,
|
|
482
593
|
"terminal_commands": terminal_commands,
|
|
483
594
|
"tool_calls": tool_calls,
|
|
484
|
-
"file_operations": file_operations
|
|
595
|
+
"file_operations": file_operations,
|
|
596
|
+
"question": question_content
|
|
485
597
|
},
|
|
486
598
|
"metadata": {
|
|
487
599
|
"has_thinking": bool(thinking_blocks),
|
|
488
600
|
"has_terminal_commands": bool(terminal_commands),
|
|
489
601
|
"has_tool_calls": bool(tool_calls),
|
|
490
602
|
"has_file_operations": bool(file_operations),
|
|
491
|
-
"
|
|
603
|
+
"has_question": has_question,
|
|
604
|
+
"total_tools": total_tools,
|
|
492
605
|
"content_length": len(clean_content)
|
|
493
606
|
}
|
|
494
607
|
}
|
|
@@ -501,28 +614,53 @@ class ResponseParser:
|
|
|
501
614
|
|
|
502
615
|
def _extract_thinking(self, content: str) -> List[str]:
|
|
503
616
|
"""Extract thinking content blocks.
|
|
504
|
-
|
|
617
|
+
|
|
505
618
|
Args:
|
|
506
619
|
content: Raw response content
|
|
507
|
-
|
|
620
|
+
|
|
508
621
|
Returns:
|
|
509
622
|
List of thinking content strings
|
|
510
623
|
"""
|
|
511
624
|
matches = self.thinking_pattern.findall(content)
|
|
512
625
|
return [match.strip() for match in matches if match.strip()]
|
|
626
|
+
|
|
627
|
+
def _extract_question(self, content: str) -> Optional[str]:
|
|
628
|
+
"""Extract question gate content.
|
|
629
|
+
|
|
630
|
+
When a <question> tag is present, the agent is asking for user input
|
|
631
|
+
and all tool calls should be suspended until the user responds.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
content: Raw response content
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
Question content if found, None otherwise
|
|
638
|
+
"""
|
|
639
|
+
match = self.question_pattern.search(content)
|
|
640
|
+
if match:
|
|
641
|
+
return match.group(1).strip()
|
|
642
|
+
return None
|
|
513
643
|
|
|
514
644
|
def _extract_terminal_commands(self, content: str) -> List[Dict[str, Any]]:
|
|
515
645
|
"""Extract terminal command blocks.
|
|
516
|
-
|
|
646
|
+
|
|
517
647
|
Args:
|
|
518
648
|
content: Raw response content
|
|
519
|
-
|
|
649
|
+
|
|
520
650
|
Returns:
|
|
521
651
|
List of terminal command dictionaries
|
|
522
652
|
"""
|
|
653
|
+
# Strip <content> blocks to avoid parsing terminal examples inside file content
|
|
654
|
+
text_without_content = re.sub(
|
|
655
|
+
r'<content>.*?</content>',
|
|
656
|
+
'',
|
|
657
|
+
content,
|
|
658
|
+
flags=re.DOTALL | re.IGNORECASE
|
|
659
|
+
)
|
|
660
|
+
|
|
523
661
|
commands = []
|
|
524
|
-
matches = self.terminal_pattern.findall(
|
|
525
|
-
|
|
662
|
+
matches = self.terminal_pattern.findall(text_without_content)
|
|
663
|
+
|
|
526
664
|
for i, match in enumerate(matches):
|
|
527
665
|
command = match.strip()
|
|
528
666
|
if command:
|
|
@@ -532,49 +670,122 @@ class ResponseParser:
|
|
|
532
670
|
"command": command,
|
|
533
671
|
"raw": match
|
|
534
672
|
})
|
|
535
|
-
|
|
673
|
+
|
|
536
674
|
return commands
|
|
537
675
|
|
|
538
676
|
def _extract_tool_calls(self, content: str) -> List[Dict[str, Any]]:
|
|
539
|
-
"""Extract MCP tool call blocks.
|
|
540
|
-
|
|
677
|
+
"""Extract MCP tool call blocks from both <tool> and <tool_call> tags.
|
|
678
|
+
|
|
679
|
+
Supports:
|
|
680
|
+
- <tool name="tool_name" arg="value">content</tool>
|
|
681
|
+
- <tool_call>tool_name</tool_call>
|
|
682
|
+
- <tool_call>{"name": "tool_name", "arguments": {...}}</tool_call>
|
|
683
|
+
|
|
541
684
|
Args:
|
|
542
685
|
content: Raw response content
|
|
543
|
-
|
|
686
|
+
|
|
544
687
|
Returns:
|
|
545
688
|
List of tool call dictionaries
|
|
546
689
|
"""
|
|
547
690
|
tool_calls = []
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
691
|
+
tool_index = 0
|
|
692
|
+
|
|
693
|
+
# Extract <tool> style calls (attribute-based)
|
|
694
|
+
for match in self.tool_pattern.finditer(content):
|
|
695
|
+
attributes_str, tool_content = match.groups()
|
|
551
696
|
try:
|
|
552
|
-
# Parse tool attributes
|
|
553
697
|
tool_info = self._parse_tool_attributes(attributes_str)
|
|
554
|
-
|
|
555
|
-
# Build tool call
|
|
556
|
-
tool_call = {
|
|
698
|
+
tool_calls.append({
|
|
557
699
|
"type": "mcp_tool",
|
|
558
|
-
"id": f"mcp_tool_{
|
|
700
|
+
"id": f"mcp_tool_{tool_index}",
|
|
559
701
|
"name": tool_info.get("name", "unknown"),
|
|
560
702
|
"arguments": tool_info.get("arguments", {}),
|
|
561
703
|
"content": tool_content.strip(),
|
|
562
|
-
"raw":
|
|
563
|
-
}
|
|
564
|
-
|
|
704
|
+
"raw": match.group(0)
|
|
705
|
+
})
|
|
706
|
+
tool_index += 1
|
|
707
|
+
except Exception as e:
|
|
708
|
+
logger.warning(f"Failed to parse <tool> call: {e}")
|
|
709
|
+
tool_calls.append({
|
|
710
|
+
"type": "malformed_tool",
|
|
711
|
+
"id": f"malformed_{tool_index}",
|
|
712
|
+
"error": str(e),
|
|
713
|
+
"raw": match.group(0)
|
|
714
|
+
})
|
|
715
|
+
tool_index += 1
|
|
716
|
+
|
|
717
|
+
# Extract <tool_call> style calls (content-based)
|
|
718
|
+
for match in self.tool_call_pattern.finditer(content):
|
|
719
|
+
call_content = match.group(1).strip()
|
|
720
|
+
try:
|
|
721
|
+
tool_call = self._parse_tool_call_content(call_content, tool_index)
|
|
565
722
|
tool_calls.append(tool_call)
|
|
566
|
-
|
|
723
|
+
tool_index += 1
|
|
567
724
|
except Exception as e:
|
|
568
|
-
logger.warning(f"Failed to parse
|
|
569
|
-
# Add as malformed tool call for debugging
|
|
725
|
+
logger.warning(f"Failed to parse <tool_call> content: {e}")
|
|
570
726
|
tool_calls.append({
|
|
571
727
|
"type": "malformed_tool",
|
|
572
|
-
"id": f"malformed_{
|
|
728
|
+
"id": f"malformed_{tool_index}",
|
|
573
729
|
"error": str(e),
|
|
574
|
-
"raw":
|
|
730
|
+
"raw": match.group(0)
|
|
575
731
|
})
|
|
576
|
-
|
|
732
|
+
tool_index += 1
|
|
733
|
+
|
|
577
734
|
return tool_calls
|
|
735
|
+
|
|
736
|
+
def _parse_tool_call_content(self, content: str, index: int) -> Dict[str, Any]:
|
|
737
|
+
"""Parse content from <tool_call> tags.
|
|
738
|
+
|
|
739
|
+
Supports:
|
|
740
|
+
- Simple name: "search_nodes"
|
|
741
|
+
- JSON format: {"name": "search_nodes", "arguments": {"query": "test"}}
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
content: Content between <tool_call> tags
|
|
745
|
+
index: Tool index for ID generation
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
Parsed tool call dictionary
|
|
749
|
+
"""
|
|
750
|
+
content = content.strip()
|
|
751
|
+
|
|
752
|
+
# Try JSON format first
|
|
753
|
+
if content.startswith("{"):
|
|
754
|
+
try:
|
|
755
|
+
data = json.loads(content)
|
|
756
|
+
return {
|
|
757
|
+
"type": "mcp_tool",
|
|
758
|
+
"id": f"mcp_tool_{index}",
|
|
759
|
+
"name": data.get("name", "unknown"),
|
|
760
|
+
"arguments": data.get("arguments", {}),
|
|
761
|
+
"content": "",
|
|
762
|
+
"raw": f"<tool_call>{content}</tool_call>"
|
|
763
|
+
}
|
|
764
|
+
except json.JSONDecodeError:
|
|
765
|
+
pass
|
|
766
|
+
|
|
767
|
+
# Simple name format: just the tool name, maybe with inline args
|
|
768
|
+
# Handle: "search_nodes" or "search_nodes query=test"
|
|
769
|
+
parts = content.split(None, 1)
|
|
770
|
+
tool_name = parts[0] if parts else "unknown"
|
|
771
|
+
arguments = {}
|
|
772
|
+
|
|
773
|
+
# Parse inline arguments if present
|
|
774
|
+
if len(parts) > 1:
|
|
775
|
+
arg_str = parts[1]
|
|
776
|
+
# Try to parse key=value pairs
|
|
777
|
+
for pair in re.findall(r'(\w+)=(["\']?)([^"\'=\s]+)\2', arg_str):
|
|
778
|
+
key, _, value = pair
|
|
779
|
+
arguments[key] = self._convert_value(value)
|
|
780
|
+
|
|
781
|
+
return {
|
|
782
|
+
"type": "mcp_tool",
|
|
783
|
+
"id": f"mcp_tool_{index}",
|
|
784
|
+
"name": tool_name,
|
|
785
|
+
"arguments": arguments,
|
|
786
|
+
"content": "",
|
|
787
|
+
"raw": f"<tool_call>{content}</tool_call>"
|
|
788
|
+
}
|
|
578
789
|
|
|
579
790
|
def _parse_tool_attributes(self, attributes_str: str) -> Dict[str, Any]:
|
|
580
791
|
"""Parse tool tag attributes.
|
|
@@ -661,6 +872,13 @@ class ResponseParser:
|
|
|
661
872
|
# Remove tool tags but preserve content structure
|
|
662
873
|
cleaned = self.tool_pattern.sub('', cleaned)
|
|
663
874
|
|
|
875
|
+
# Remove <tool_call> tags
|
|
876
|
+
cleaned = self.tool_call_pattern.sub('', cleaned)
|
|
877
|
+
|
|
878
|
+
# Remove question tags but preserve content for display
|
|
879
|
+
# The question content stays visible, just the tags are removed
|
|
880
|
+
cleaned = self.question_pattern.sub(r'\1', cleaned)
|
|
881
|
+
|
|
664
882
|
# Remove file operation tags (all 14 types)
|
|
665
883
|
# Only successfully parsed tags are removed; malformed tags remain visible
|
|
666
884
|
cleaned = self.file_ops_parser.edit_pattern.sub('', cleaned)
|
core/llm/response_processor.py
CHANGED
|
@@ -33,6 +33,12 @@ class ResponseProcessor:
|
|
|
33
33
|
|
|
34
34
|
logger.info("Response processor initialized")
|
|
35
35
|
|
|
36
|
+
async def initialize(self) -> bool:
|
|
37
|
+
"""Initialize the response processor."""
|
|
38
|
+
self.is_initialized = True
|
|
39
|
+
logger.debug("Response processor async initialization complete")
|
|
40
|
+
return True
|
|
41
|
+
|
|
36
42
|
def process_response(self, raw_response: str) -> Dict[str, Any]:
|
|
37
43
|
"""Process raw LLM response.
|
|
38
44
|
|
|
@@ -89,14 +95,21 @@ class ResponseProcessor:
|
|
|
89
95
|
|
|
90
96
|
def _extract_terminal_commands(self, content: str) -> List[str]:
|
|
91
97
|
"""Extract terminal commands from content.
|
|
92
|
-
|
|
98
|
+
|
|
93
99
|
Args:
|
|
94
100
|
content: Raw content with potential terminal tags
|
|
95
|
-
|
|
101
|
+
|
|
96
102
|
Returns:
|
|
97
103
|
List of terminal commands
|
|
98
104
|
"""
|
|
99
|
-
|
|
105
|
+
# Strip <content> blocks to avoid parsing terminal examples inside file content
|
|
106
|
+
text_without_content = re.sub(
|
|
107
|
+
r'<content>.*?</content>',
|
|
108
|
+
'',
|
|
109
|
+
content,
|
|
110
|
+
flags=re.DOTALL | re.IGNORECASE
|
|
111
|
+
)
|
|
112
|
+
matches = self.terminal_pattern.findall(text_without_content)
|
|
100
113
|
return [match.strip() for match in matches if match.strip()]
|
|
101
114
|
|
|
102
115
|
def _extract_tool_calls(self, content: str) -> List[Dict[str, Any]]:
|
core/llm/tool_executor.py
CHANGED
|
@@ -264,7 +264,7 @@ class ToolExecutor:
|
|
|
264
264
|
|
|
265
265
|
try:
|
|
266
266
|
stdout, stderr = await asyncio.wait_for(
|
|
267
|
-
process.communicate(),
|
|
267
|
+
process.communicate(),
|
|
268
268
|
timeout=self.terminal_timeout
|
|
269
269
|
)
|
|
270
270
|
except asyncio.TimeoutError:
|
|
@@ -276,6 +276,11 @@ class ToolExecutor:
|
|
|
276
276
|
success=False,
|
|
277
277
|
error=f"Command timed out after {self.terminal_timeout} seconds"
|
|
278
278
|
)
|
|
279
|
+
except asyncio.CancelledError:
|
|
280
|
+
# Clean up subprocess on cancellation to avoid ResourceWarning
|
|
281
|
+
process.kill()
|
|
282
|
+
await process.wait()
|
|
283
|
+
raise # Re-raise to propagate cancellation
|
|
279
284
|
|
|
280
285
|
# Process results
|
|
281
286
|
stdout_text = stdout.decode('utf-8', errors='replace')
|
core/logging/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ from .setup import (
|
|
|
5
5
|
setup_from_config,
|
|
6
6
|
get_current_config,
|
|
7
7
|
is_configured,
|
|
8
|
+
set_level,
|
|
8
9
|
CompactFormatter,
|
|
9
10
|
LoggingSetup
|
|
10
11
|
)
|
|
@@ -14,6 +15,7 @@ __all__ = [
|
|
|
14
15
|
'setup_from_config',
|
|
15
16
|
'get_current_config',
|
|
16
17
|
'is_configured',
|
|
18
|
+
'set_level',
|
|
17
19
|
'CompactFormatter',
|
|
18
20
|
'LoggingSetup'
|
|
19
21
|
]
|
core/logging/setup.py
CHANGED
|
@@ -11,6 +11,8 @@ import threading
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Dict, Optional
|
|
13
13
|
|
|
14
|
+
from ..utils.config_utils import get_logs_dir
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class CompactFormatter(logging.Formatter):
|
|
16
18
|
"""Custom formatter that compacts level names and includes file location."""
|
|
@@ -37,12 +39,10 @@ class LoggingSetup:
|
|
|
37
39
|
"""Setup minimal logging before config system is available.
|
|
38
40
|
|
|
39
41
|
Args:
|
|
40
|
-
log_dir: Optional log directory, defaults to
|
|
42
|
+
log_dir: Optional log directory, defaults to project-specific logs
|
|
41
43
|
"""
|
|
42
44
|
if log_dir is None:
|
|
43
|
-
|
|
44
|
-
config_dir = get_config_directory()
|
|
45
|
-
log_dir = config_dir / "logs"
|
|
45
|
+
log_dir = get_logs_dir()
|
|
46
46
|
|
|
47
47
|
# Ensure log directory exists
|
|
48
48
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -96,7 +96,8 @@ class LoggingSetup:
|
|
|
96
96
|
|
|
97
97
|
# Extract configuration values
|
|
98
98
|
level = logging_config.get('level', 'INFO').upper()
|
|
99
|
-
|
|
99
|
+
default_log_path = get_logs_dir() / "kollabor.log"
|
|
100
|
+
log_file = logging_config.get('file') or str(default_log_path)
|
|
100
101
|
format_type = logging_config.get('format_type', 'compact')
|
|
101
102
|
custom_format = logging_config.get('format', None)
|
|
102
103
|
|
|
@@ -183,6 +184,28 @@ class LoggingSetup:
|
|
|
183
184
|
"""Check if logging has been configured."""
|
|
184
185
|
return self._configured
|
|
185
186
|
|
|
187
|
+
def set_level(self, level: str) -> None:
|
|
188
|
+
"""Set logging level at runtime (hot reload support).
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
level: Log level string (DEBUG, INFO, WARNING, ERROR)
|
|
192
|
+
"""
|
|
193
|
+
level = level.upper()
|
|
194
|
+
numeric_level = getattr(logging, level, logging.INFO)
|
|
195
|
+
|
|
196
|
+
# Update root logger
|
|
197
|
+
logging.getLogger().setLevel(numeric_level)
|
|
198
|
+
|
|
199
|
+
# Update all existing loggers
|
|
200
|
+
for logger_name in logging.Logger.manager.loggerDict:
|
|
201
|
+
existing_logger = logging.getLogger(logger_name)
|
|
202
|
+
existing_logger.setLevel(numeric_level)
|
|
203
|
+
|
|
204
|
+
# Update current config
|
|
205
|
+
self._current_config['level'] = level
|
|
206
|
+
|
|
207
|
+
logging.info(f"Logging level changed to {level}")
|
|
208
|
+
|
|
186
209
|
|
|
187
210
|
# Global instance
|
|
188
211
|
logging_setup = LoggingSetup()
|
|
@@ -205,4 +228,9 @@ def get_current_config() -> Dict[str, Any]:
|
|
|
205
228
|
|
|
206
229
|
def is_configured() -> bool:
|
|
207
230
|
"""Check if logging has been configured."""
|
|
208
|
-
return logging_setup.is_configured()
|
|
231
|
+
return logging_setup.is_configured()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def set_level(level: str) -> None:
|
|
235
|
+
"""Set logging level at runtime (hot reload support)."""
|
|
236
|
+
logging_setup.set_level(level)
|