omnimancer-cli 0.1.0__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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversation persistence manager for Omnimancer.
|
|
3
|
+
|
|
4
|
+
This module handles saving, loading, and managing conversation histories
|
|
5
|
+
including serialization, file management, and conversation organization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from ..utils.errors import ConversationError
|
|
13
|
+
from .models import ChatContext, ChatMessage, MessageRole
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConversationManager:
|
|
17
|
+
"""
|
|
18
|
+
Manages conversation persistence and file operations.
|
|
19
|
+
|
|
20
|
+
This class handles saving conversations to files, loading them back,
|
|
21
|
+
and managing conversation file organization.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, storage_path):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the conversation manager.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
storage_path: Base path for storing conversation files (str or Path)
|
|
30
|
+
"""
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
# Always ensure Path object and expand user home directory
|
|
34
|
+
if isinstance(storage_path, str):
|
|
35
|
+
self.storage_path = Path(storage_path).expanduser()
|
|
36
|
+
else:
|
|
37
|
+
self.storage_path = Path(storage_path).expanduser()
|
|
38
|
+
self.conversations_dir = self.storage_path / "conversations"
|
|
39
|
+
self._ensure_directories()
|
|
40
|
+
|
|
41
|
+
def _ensure_directories(self) -> None:
|
|
42
|
+
"""Ensure required directories exist."""
|
|
43
|
+
self.conversations_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
def save_conversation(
|
|
46
|
+
self,
|
|
47
|
+
context: ChatContext,
|
|
48
|
+
filename: Optional[str] = None,
|
|
49
|
+
metadata: Optional[Dict] = None,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Save a conversation to a file.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
context: Chat context to save
|
|
56
|
+
filename: Optional filename (auto-generated if not provided)
|
|
57
|
+
metadata: Optional metadata to include
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Filename of the saved conversation
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ConversationError: If saving fails
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
# Generate filename if not provided
|
|
67
|
+
if not filename:
|
|
68
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
69
|
+
filename = f"conversation_{timestamp}.json"
|
|
70
|
+
|
|
71
|
+
# Ensure .json extension
|
|
72
|
+
if not filename.endswith(".json"):
|
|
73
|
+
filename += ".json"
|
|
74
|
+
|
|
75
|
+
# Prepare conversation data
|
|
76
|
+
conversation_data = {
|
|
77
|
+
"version": "1.0",
|
|
78
|
+
"created_at": datetime.now().isoformat(),
|
|
79
|
+
"session_id": context.session_id,
|
|
80
|
+
"current_model": context.current_model,
|
|
81
|
+
"max_context_length": context.max_context_length,
|
|
82
|
+
"metadata": metadata or {},
|
|
83
|
+
"messages": [
|
|
84
|
+
{
|
|
85
|
+
"role": msg.role.value,
|
|
86
|
+
"content": msg.content,
|
|
87
|
+
"timestamp": msg.timestamp.isoformat(),
|
|
88
|
+
"model_used": msg.model_used,
|
|
89
|
+
}
|
|
90
|
+
for msg in context.messages
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Save to file
|
|
95
|
+
file_path = self.conversations_dir / filename
|
|
96
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
97
|
+
json.dump(conversation_data, f, indent=2, ensure_ascii=False)
|
|
98
|
+
|
|
99
|
+
return filename
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
raise ConversationError(f"Failed to save conversation: {e}")
|
|
103
|
+
|
|
104
|
+
def load_conversation(self, filename: str) -> ChatContext:
|
|
105
|
+
"""
|
|
106
|
+
Load a conversation from a file.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
filename: Name of the conversation file
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Loaded ChatContext
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ConversationError: If loading fails
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
# Ensure .json extension
|
|
119
|
+
if not filename.endswith(".json"):
|
|
120
|
+
filename += ".json"
|
|
121
|
+
|
|
122
|
+
file_path = self.conversations_dir / filename
|
|
123
|
+
|
|
124
|
+
if not file_path.exists():
|
|
125
|
+
raise ConversationError(f"Conversation file not found: {filename}")
|
|
126
|
+
|
|
127
|
+
# Load conversation data
|
|
128
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
129
|
+
conversation_data = json.load(f)
|
|
130
|
+
|
|
131
|
+
# Validate version
|
|
132
|
+
version = conversation_data.get("version", "1.0")
|
|
133
|
+
if version != "1.0":
|
|
134
|
+
raise ConversationError(
|
|
135
|
+
f"Unsupported conversation file version: {version}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Reconstruct messages
|
|
139
|
+
messages = []
|
|
140
|
+
for msg_data in conversation_data.get("messages", []):
|
|
141
|
+
message = ChatMessage(
|
|
142
|
+
role=MessageRole(msg_data["role"]),
|
|
143
|
+
content=msg_data["content"],
|
|
144
|
+
timestamp=datetime.fromisoformat(msg_data["timestamp"]),
|
|
145
|
+
model_used=msg_data["model_used"],
|
|
146
|
+
)
|
|
147
|
+
messages.append(message)
|
|
148
|
+
|
|
149
|
+
# Create ChatContext
|
|
150
|
+
context = ChatContext(
|
|
151
|
+
messages=messages,
|
|
152
|
+
current_model=conversation_data.get("current_model", ""),
|
|
153
|
+
session_id=conversation_data.get("session_id", ""),
|
|
154
|
+
max_context_length=conversation_data.get("max_context_length", 4000),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return context
|
|
158
|
+
|
|
159
|
+
except ConversationError:
|
|
160
|
+
raise
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise ConversationError(f"Failed to load conversation: {e}")
|
|
163
|
+
|
|
164
|
+
def list_conversations(self) -> List[Dict[str, str]]:
|
|
165
|
+
"""
|
|
166
|
+
List all available conversation files.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of conversation info dictionaries
|
|
170
|
+
"""
|
|
171
|
+
conversations = []
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
for file_path in self.conversations_dir.glob("*.json"):
|
|
175
|
+
try:
|
|
176
|
+
# Get basic file info
|
|
177
|
+
stat = file_path.stat()
|
|
178
|
+
|
|
179
|
+
# Try to get conversation metadata
|
|
180
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
181
|
+
data = json.load(f)
|
|
182
|
+
|
|
183
|
+
conversations.append(
|
|
184
|
+
{
|
|
185
|
+
"filename": file_path.name,
|
|
186
|
+
"created_at": data.get("created_at", ""),
|
|
187
|
+
"session_id": data.get("session_id", ""),
|
|
188
|
+
"current_model": data.get("current_model", ""),
|
|
189
|
+
"message_count": len(data.get("messages", [])),
|
|
190
|
+
"file_size": stat.st_size,
|
|
191
|
+
"modified_at": datetime.fromtimestamp(
|
|
192
|
+
stat.st_mtime
|
|
193
|
+
).isoformat(),
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
except Exception:
|
|
198
|
+
# Skip files that can't be read
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
# Sort by creation date (newest first)
|
|
202
|
+
conversations.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
|
203
|
+
|
|
204
|
+
except Exception:
|
|
205
|
+
# Return empty list if directory can't be read
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
return conversations
|
|
209
|
+
|
|
210
|
+
def delete_conversation(self, filename: str) -> bool:
|
|
211
|
+
"""
|
|
212
|
+
Delete a conversation file.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
filename: Name of the conversation file
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if deleted successfully, False otherwise
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
# Ensure .json extension
|
|
222
|
+
if not filename.endswith(".json"):
|
|
223
|
+
filename += ".json"
|
|
224
|
+
|
|
225
|
+
file_path = self.conversations_dir / filename
|
|
226
|
+
|
|
227
|
+
if file_path.exists():
|
|
228
|
+
file_path.unlink()
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
except Exception:
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
def get_conversation_info(self, filename: str) -> Optional[Dict]:
|
|
237
|
+
"""
|
|
238
|
+
Get information about a specific conversation.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
filename: Name of the conversation file
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Conversation info dictionary or None if not found
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
# Ensure .json extension
|
|
248
|
+
if not filename.endswith(".json"):
|
|
249
|
+
filename += ".json"
|
|
250
|
+
|
|
251
|
+
file_path = self.conversations_dir / filename
|
|
252
|
+
|
|
253
|
+
if not file_path.exists():
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
257
|
+
data = json.load(f)
|
|
258
|
+
|
|
259
|
+
stat = file_path.stat()
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"filename": filename,
|
|
263
|
+
"created_at": data.get("created_at", ""),
|
|
264
|
+
"session_id": data.get("session_id", ""),
|
|
265
|
+
"current_model": data.get("current_model", ""),
|
|
266
|
+
"message_count": len(data.get("messages", [])),
|
|
267
|
+
"file_size": stat.st_size,
|
|
268
|
+
"modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
|
269
|
+
"metadata": data.get("metadata", {}),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
except Exception:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def export_conversation(
|
|
276
|
+
self, filename: str, export_format: str = "json"
|
|
277
|
+
) -> Optional[str]:
|
|
278
|
+
"""
|
|
279
|
+
Export a conversation to different formats.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
filename: Name of the conversation file
|
|
283
|
+
export_format: Export format ("json", "txt", "md")
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Path to exported file or None if failed
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
context = self.load_conversation(filename)
|
|
290
|
+
|
|
291
|
+
base_name = filename.replace(".json", "")
|
|
292
|
+
|
|
293
|
+
if export_format == "txt":
|
|
294
|
+
return self._export_to_txt(context, base_name)
|
|
295
|
+
elif export_format == "md":
|
|
296
|
+
return self._export_to_markdown(context, base_name)
|
|
297
|
+
elif export_format == "json":
|
|
298
|
+
# Already in JSON format
|
|
299
|
+
return str(self.conversations_dir / filename)
|
|
300
|
+
else:
|
|
301
|
+
raise ConversationError(f"Unsupported export format: {export_format}")
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
raise ConversationError(f"Failed to export conversation: {e}")
|
|
305
|
+
|
|
306
|
+
def _export_to_txt(self, context: ChatContext, base_name: str) -> str:
|
|
307
|
+
"""Export conversation to plain text format."""
|
|
308
|
+
export_path = self.conversations_dir / f"{base_name}.txt"
|
|
309
|
+
|
|
310
|
+
with open(export_path, "w", encoding="utf-8") as f:
|
|
311
|
+
f.write(f"Omnimancer Conversation Export\n")
|
|
312
|
+
f.write(f"Session ID: {context.session_id}\n")
|
|
313
|
+
f.write(f"Model: {context.current_model}\n")
|
|
314
|
+
f.write(f"Messages: {len(context.messages)}\n")
|
|
315
|
+
f.write("=" * 50 + "\n\n")
|
|
316
|
+
|
|
317
|
+
for msg in context.messages:
|
|
318
|
+
f.write(f"[{msg.timestamp.strftime('%Y-%m-%d %H:%M:%S')}] ")
|
|
319
|
+
f.write(f"{msg.role.value.upper()}")
|
|
320
|
+
if msg.model_used:
|
|
321
|
+
f.write(f" ({msg.model_used})")
|
|
322
|
+
f.write(":\n")
|
|
323
|
+
f.write(f"{msg.content}\n\n")
|
|
324
|
+
|
|
325
|
+
return str(export_path)
|
|
326
|
+
|
|
327
|
+
def _export_to_markdown(self, context: ChatContext, base_name: str) -> str:
|
|
328
|
+
"""Export conversation to Markdown format."""
|
|
329
|
+
export_path = self.conversations_dir / f"{base_name}.md"
|
|
330
|
+
|
|
331
|
+
with open(export_path, "w", encoding="utf-8") as f:
|
|
332
|
+
f.write(f"# Omnimancer Conversation\n\n")
|
|
333
|
+
f.write(f"**Session ID:** {context.session_id}\n")
|
|
334
|
+
f.write(f"**Model:** {context.current_model}\n")
|
|
335
|
+
f.write(f"**Messages:** {len(context.messages)}\n\n")
|
|
336
|
+
f.write("---\n\n")
|
|
337
|
+
|
|
338
|
+
for msg in context.messages:
|
|
339
|
+
timestamp = msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
340
|
+
role = msg.role.value.title()
|
|
341
|
+
|
|
342
|
+
if msg.role == MessageRole.USER:
|
|
343
|
+
f.write(f"## 👤 {role} ({timestamp})\n\n")
|
|
344
|
+
elif msg.role == MessageRole.ASSISTANT:
|
|
345
|
+
model_info = f" - {msg.model_used}" if msg.model_used else ""
|
|
346
|
+
f.write(f"## 🤖 {role}{model_info} ({timestamp})\n\n")
|
|
347
|
+
else:
|
|
348
|
+
f.write(f"## ⚙️ {role} ({timestamp})\n\n")
|
|
349
|
+
|
|
350
|
+
f.write(f"{msg.content}\n\n")
|
|
351
|
+
|
|
352
|
+
return str(export_path)
|