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.
Files changed (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. system_prompt/example_with_trender.md +47 -0
@@ -0,0 +1,414 @@
1
+ """Conversation management for LLM core service.
2
+
3
+ Manages conversation state, history, context windows,
4
+ and message threading for LLM interactions.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional
12
+ from uuid import uuid4
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ConversationManager:
18
+ """Manage conversation state and history.
19
+
20
+ Handles message storage, context windows, conversation threading,
21
+ and session management for LLM interactions.
22
+ """
23
+
24
+ def __init__(self, config, conversation_logger=None):
25
+ """Initialize conversation manager.
26
+
27
+ Args:
28
+ config: Configuration manager
29
+ conversation_logger: Optional conversation logger instance
30
+ """
31
+ self.config = config
32
+ self.conversation_logger = conversation_logger
33
+
34
+ # Conversation state
35
+ self.current_session_id = str(uuid4())
36
+ self.messages = []
37
+ self.message_index = {} # uuid -> message lookup
38
+ self.context_window = []
39
+
40
+ # Configuration
41
+ self.max_history = config.get("core.llm.max_history", 50)
42
+ self.max_context_tokens = config.get("core.llm.max_context_tokens", 4000)
43
+ self.save_conversations = config.get("core.llm.save_conversations", True)
44
+
45
+ # Conversation storage
46
+ from ..utils.config_utils import get_config_directory
47
+ config_dir = get_config_directory()
48
+ self.conversations_dir = config_dir / "conversations"
49
+ self.conversations_dir.mkdir(parents=True, exist_ok=True)
50
+
51
+ # Current conversation metadata
52
+ self.current_parent_uuid = None # Track parent UUID for message threading
53
+
54
+ self.conversation_metadata = {
55
+ "started_at": datetime.now().isoformat(),
56
+ "message_count": 0,
57
+ "turn_count": 0,
58
+ "topics": [],
59
+ "model_used": None
60
+ }
61
+
62
+ logger.info(f"Conversation manager initialized with session: {self.current_session_id}")
63
+
64
+ def add_message(self, role: str, content: str,
65
+ parent_uuid: Optional[str] = None,
66
+ metadata: Optional[Dict[str, Any]] = None) -> str:
67
+ """Add a message to the conversation.
68
+
69
+ Args:
70
+ role: Message role (user, assistant, system)
71
+ content: Message content
72
+ parent_uuid: UUID of parent message for threading
73
+ metadata: Optional message metadata
74
+
75
+ Returns:
76
+ UUID of the added message
77
+ """
78
+ message_uuid = str(uuid4())
79
+ timestamp = datetime.now().isoformat()
80
+
81
+ # Update current_parent_uuid for next message
82
+ if parent_uuid:
83
+ self.current_parent_uuid = parent_uuid
84
+
85
+ message = {
86
+ "uuid": message_uuid,
87
+ "role": role,
88
+ "content": content,
89
+ "timestamp": timestamp,
90
+ "parent_uuid": parent_uuid or self.current_parent_uuid,
91
+ "metadata": metadata or {},
92
+ "session_id": self.current_session_id
93
+ }
94
+
95
+ # Add to messages list
96
+ self.messages.append(message)
97
+ self.message_index[message_uuid] = message
98
+
99
+ # Update context window
100
+ self._update_context_window()
101
+
102
+ # Update metadata
103
+ self.conversation_metadata["message_count"] += 1
104
+ if role == "user":
105
+ self.conversation_metadata["turn_count"] += 1
106
+
107
+ # Log to conversation logger if available
108
+ if self.conversation_logger:
109
+ self.conversation_logger.log_message(
110
+ role=role,
111
+ content=content,
112
+ parent_uuid=parent_uuid,
113
+ metadata=metadata
114
+ )
115
+
116
+ # Auto-save if configured
117
+ if self.save_conversations and len(self.messages) % 10 == 0:
118
+ self.save_conversation()
119
+
120
+ logger.debug(f"Added {role} message: {message_uuid}")
121
+ return message_uuid
122
+
123
+ def get_context_messages(self, max_messages: Optional[int] = None) -> List[Dict[str, Any]]:
124
+ """Get messages for LLM context.
125
+
126
+ Args:
127
+ max_messages: Maximum number of messages to return
128
+
129
+ Returns:
130
+ List of messages for context
131
+ """
132
+ if max_messages:
133
+ return self.messages[-max_messages:]
134
+ return self.context_window
135
+
136
+ def _update_context_window(self):
137
+ """Update the context window with recent messages."""
138
+ # Simple sliding window for now
139
+ # TODO: Implement token counting for precise context management
140
+ self.context_window = self.messages[-self.max_history:]
141
+
142
+ # Ensure we have system message if it exists
143
+ system_messages = [m for m in self.messages if m["role"] == "system"]
144
+ if system_messages and system_messages[0] not in self.context_window:
145
+ # Prepend system message
146
+ self.context_window = [system_messages[0]] + self.context_window
147
+
148
+ def _get_last_message_uuid(self) -> Optional[str]:
149
+ """Get UUID of the last message."""
150
+ if self.messages:
151
+ return self.messages[-1]["uuid"]
152
+ return None
153
+
154
+ def get_message_thread(self, message_uuid: str) -> List[Dict[str, Any]]:
155
+ """Get the thread of messages leading to a specific message.
156
+
157
+ Args:
158
+ message_uuid: UUID of the target message
159
+
160
+ Returns:
161
+ List of messages in the thread
162
+ """
163
+ thread = []
164
+ current_uuid = message_uuid
165
+
166
+ while current_uuid:
167
+ if current_uuid in self.message_index:
168
+ message = self.message_index[current_uuid]
169
+ thread.insert(0, message)
170
+ current_uuid = message.get("parent_uuid")
171
+ else:
172
+ break
173
+
174
+ return thread
175
+
176
+ def search_messages(self, query: str, role: Optional[str] = None) -> List[Dict[str, Any]]:
177
+ """Search messages by content.
178
+
179
+ Args:
180
+ query: Search query
181
+ role: Optional role filter
182
+
183
+ Returns:
184
+ List of matching messages
185
+ """
186
+ results = []
187
+ query_lower = query.lower()
188
+
189
+ for message in self.messages:
190
+ if role and message["role"] != role:
191
+ continue
192
+
193
+ if query_lower in message["content"].lower():
194
+ results.append(message)
195
+
196
+ return results
197
+
198
+ def get_conversation_summary(self) -> Dict[str, Any]:
199
+ """Get summary of current conversation.
200
+
201
+ Returns:
202
+ Conversation summary statistics
203
+ """
204
+ user_messages = [m for m in self.messages if m["role"] == "user"]
205
+ assistant_messages = [m for m in self.messages if m["role"] == "assistant"]
206
+
207
+ # Extract topics from messages
208
+ topics = self._extract_topics()
209
+
210
+ summary = {
211
+ "session_id": self.current_session_id,
212
+ "total_messages": len(self.messages),
213
+ "user_messages": len(user_messages),
214
+ "assistant_messages": len(assistant_messages),
215
+ "turn_count": self.conversation_metadata["turn_count"],
216
+ "started_at": self.conversation_metadata["started_at"],
217
+ "duration": self._calculate_duration(),
218
+ "topics": topics,
219
+ "average_message_length": self._calculate_avg_message_length(),
220
+ "context_usage": f"{len(self.context_window)}/{self.max_history}"
221
+ }
222
+
223
+ return summary
224
+
225
+ def _extract_topics(self) -> List[str]:
226
+ """Extract main topics from conversation."""
227
+ # Simple keyword extraction for now
228
+ # TODO: Implement more sophisticated topic extraction
229
+ topics = []
230
+
231
+ # Common technical keywords to look for
232
+ keywords = ["error", "bug", "feature", "implement", "fix", "create",
233
+ "update", "delete", "configure", "install", "debug"]
234
+
235
+ all_content = " ".join([m["content"] for m in self.messages])
236
+ all_content_lower = all_content.lower()
237
+
238
+ for keyword in keywords:
239
+ if keyword in all_content_lower:
240
+ topics.append(keyword)
241
+
242
+ return topics[:5] # Return top 5 topics
243
+
244
+ def _calculate_duration(self) -> str:
245
+ """Calculate conversation duration."""
246
+ if not self.messages:
247
+ return "0m"
248
+
249
+ start = datetime.fromisoformat(self.messages[0]["timestamp"])
250
+ end = datetime.fromisoformat(self.messages[-1]["timestamp"])
251
+ duration = end - start
252
+
253
+ minutes = duration.total_seconds() / 60
254
+ if minutes < 60:
255
+ return f"{int(minutes)}m"
256
+ else:
257
+ hours = minutes / 60
258
+ return f"{hours:.1f}h"
259
+
260
+ def _calculate_avg_message_length(self) -> int:
261
+ """Calculate average message length."""
262
+ if not self.messages:
263
+ return 0
264
+
265
+ total_length = sum(len(m["content"]) for m in self.messages)
266
+ return total_length // len(self.messages)
267
+
268
+ def save_conversation(self, filename: Optional[str] = None) -> Path:
269
+ """Save current conversation to file.
270
+
271
+ Args:
272
+ filename: Optional custom filename
273
+
274
+ Returns:
275
+ Path to saved conversation file
276
+ """
277
+ if not filename:
278
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
279
+ filename = f"conversation_{self.current_session_id[:8]}_{timestamp}.json"
280
+
281
+ filepath = self.conversations_dir / filename
282
+
283
+ conversation_data = {
284
+ "metadata": self.conversation_metadata,
285
+ "summary": self.get_conversation_summary(),
286
+ "messages": self.messages
287
+ }
288
+
289
+ with open(filepath, 'w') as f:
290
+ json.dump(conversation_data, f, indent=2)
291
+
292
+ logger.info(f"Saved conversation to: {filepath}")
293
+ return filepath
294
+
295
+ def load_conversation(self, filepath: Path) -> bool:
296
+ """Load a conversation from file.
297
+
298
+ Args:
299
+ filepath: Path to conversation file
300
+
301
+ Returns:
302
+ True if loaded successfully
303
+ """
304
+ try:
305
+ with open(filepath, 'r') as f:
306
+ data = json.load(f)
307
+
308
+ self.messages = data.get("messages", [])
309
+ self.conversation_metadata = data.get("metadata", {})
310
+
311
+ # Rebuild message index
312
+ self.message_index = {m["uuid"]: m for m in self.messages}
313
+
314
+ # Update context window
315
+ self._update_context_window()
316
+
317
+ logger.info(f"Loaded conversation from: {filepath}")
318
+ return True
319
+
320
+ except Exception as e:
321
+ logger.error(f"Failed to load conversation: {e}")
322
+ return False
323
+
324
+ def clear_conversation(self):
325
+ """Clear current conversation and start fresh."""
326
+ # Save current conversation if it has messages
327
+ if self.messages and self.save_conversations:
328
+ self.save_conversation()
329
+
330
+ # Reset state
331
+ self.current_session_id = str(uuid4())
332
+ self.messages = []
333
+ self.message_index = {}
334
+ self.context_window = []
335
+
336
+ # Reset metadata
337
+ self.current_parent_uuid = None # Track parent UUID for message threading
338
+
339
+ self.conversation_metadata = {
340
+ "started_at": datetime.now().isoformat(),
341
+ "message_count": 0,
342
+ "turn_count": 0,
343
+ "topics": [],
344
+ "model_used": None
345
+ }
346
+
347
+ logger.info(f"Cleared conversation, new session: {self.current_session_id}")
348
+
349
+ def export_for_training(self) -> List[Dict[str, str]]:
350
+ """Export conversation in format suitable for model training.
351
+
352
+ Returns:
353
+ List of message pairs for training
354
+ """
355
+ training_data = []
356
+
357
+ for i in range(0, len(self.messages) - 1, 2):
358
+ if (self.messages[i]["role"] == "user" and
359
+ i + 1 < len(self.messages) and
360
+ self.messages[i + 1]["role"] == "assistant"):
361
+
362
+ training_data.append({
363
+ "instruction": self.messages[i]["content"],
364
+ "response": self.messages[i + 1]["content"]
365
+ })
366
+
367
+ return training_data
368
+
369
+ def get_conversation_stats(self) -> Dict[str, Any]:
370
+ """Get detailed conversation statistics.
371
+
372
+ Returns:
373
+ Detailed statistics about the conversation
374
+ """
375
+ stats = {
376
+ "session": {
377
+ "id": self.current_session_id,
378
+ "started": self.conversation_metadata["started_at"],
379
+ "duration": self._calculate_duration()
380
+ },
381
+ "messages": {
382
+ "total": len(self.messages),
383
+ "by_role": {},
384
+ "average_length": self._calculate_avg_message_length(),
385
+ "shortest": min((len(m["content"]) for m in self.messages), default=0),
386
+ "longest": max((len(m["content"]) for m in self.messages), default=0)
387
+ },
388
+ "context": {
389
+ "window_size": len(self.context_window),
390
+ "max_size": self.max_history,
391
+ "utilization": f"{(len(self.context_window) / self.max_history * 100):.1f}%"
392
+ },
393
+ "threading": {
394
+ "unique_threads": len(set(m.get("parent_uuid") for m in self.messages)),
395
+ "max_thread_depth": self._calculate_max_thread_depth()
396
+ }
397
+ }
398
+
399
+ # Count messages by role
400
+ for message in self.messages:
401
+ role = message["role"]
402
+ stats["messages"]["by_role"][role] = stats["messages"]["by_role"].get(role, 0) + 1
403
+
404
+ return stats
405
+
406
+ def _calculate_max_thread_depth(self) -> int:
407
+ """Calculate maximum thread depth in conversation."""
408
+ max_depth = 0
409
+
410
+ for message in self.messages:
411
+ depth = len(self.get_message_thread(message["uuid"]))
412
+ max_depth = max(max_depth, depth)
413
+
414
+ return max_depth