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,787 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core engine for Omnimancer CLI.
|
|
3
|
+
|
|
4
|
+
This module provides the main engine class that coordinates between
|
|
5
|
+
providers, configuration, and chat management.
|
|
6
|
+
|
|
7
|
+
Version: 1.0.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from ..core.models import ChatResponse, EnhancedModelInfo, ModelInfo
|
|
14
|
+
from ..providers.base import BaseProvider
|
|
15
|
+
from ..ui.progress_indicator import OperationType, get_progress_indicator
|
|
16
|
+
from ..utils.errors import ConfigurationError
|
|
17
|
+
from .chat_manager import ChatManager
|
|
18
|
+
from .config_manager import ConfigManager
|
|
19
|
+
from .conversation_manager import ConversationManager
|
|
20
|
+
from .health_monitor import HealthMonitor
|
|
21
|
+
from .provider_initializer import ProviderInitializer
|
|
22
|
+
from .provider_registry import ProviderRegistry
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CoreEngine:
|
|
28
|
+
"""
|
|
29
|
+
Core engine that coordinates all Omnimancer functionality.
|
|
30
|
+
|
|
31
|
+
This class manages providers, configuration, chat sessions,
|
|
32
|
+
and provides the main interface for the CLI.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config_manager: ConfigManager):
|
|
36
|
+
"""
|
|
37
|
+
Initialize the core engine.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config_manager: Configuration manager instance
|
|
41
|
+
"""
|
|
42
|
+
self.config_manager = config_manager
|
|
43
|
+
self.chat_manager = ChatManager()
|
|
44
|
+
|
|
45
|
+
# Get storage path from config or use default
|
|
46
|
+
config = config_manager.get_config()
|
|
47
|
+
storage_path = getattr(config, "storage_path", "~/.omnimancer")
|
|
48
|
+
self.conversation_manager = ConversationManager(storage_path)
|
|
49
|
+
|
|
50
|
+
self.health_monitor = HealthMonitor()
|
|
51
|
+
self.provider_initializer = ProviderInitializer()
|
|
52
|
+
|
|
53
|
+
# Initialize provider registry
|
|
54
|
+
self.provider_registry = ProviderRegistry()
|
|
55
|
+
|
|
56
|
+
# Initialize MCP manager
|
|
57
|
+
mcp_config = getattr(config, "mcp", None)
|
|
58
|
+
if mcp_config:
|
|
59
|
+
from ..mcp.manager import MCPManager
|
|
60
|
+
|
|
61
|
+
self.mcp_manager = MCPManager(mcp_config)
|
|
62
|
+
else:
|
|
63
|
+
self.mcp_manager = None
|
|
64
|
+
|
|
65
|
+
self.providers: Dict[str, BaseProvider] = {}
|
|
66
|
+
self.current_provider: Optional[BaseProvider] = None
|
|
67
|
+
self._initialized = False
|
|
68
|
+
|
|
69
|
+
# Initialize agent engine for autonomous operations
|
|
70
|
+
self.agent_engine = None
|
|
71
|
+
|
|
72
|
+
async def initialize_providers(self) -> None:
|
|
73
|
+
"""Initialize all configured providers."""
|
|
74
|
+
try:
|
|
75
|
+
config = self.config_manager.get_config()
|
|
76
|
+
|
|
77
|
+
# Use the optimized provider initializer with config_manager for API key decryption
|
|
78
|
+
self.providers = await self.provider_initializer.initialize_providers(
|
|
79
|
+
config.providers, self.config_manager
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Register providers with the provider registry for catalog management
|
|
83
|
+
from ..providers.factory import ProviderFactory
|
|
84
|
+
|
|
85
|
+
factory = ProviderFactory()
|
|
86
|
+
available_providers = factory.get_available_providers()
|
|
87
|
+
|
|
88
|
+
# Register all available providers (not just configured ones) for catalog management
|
|
89
|
+
for provider_name in available_providers:
|
|
90
|
+
try:
|
|
91
|
+
# Just register the provider name, the registry will handle class loading
|
|
92
|
+
self.provider_registry.register_provider(provider_name, None)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.warning(f"Failed to register provider {provider_name}: {e}")
|
|
95
|
+
|
|
96
|
+
# Set default provider
|
|
97
|
+
if config.default_provider and config.default_provider in self.providers:
|
|
98
|
+
self.current_provider = self.providers[config.default_provider]
|
|
99
|
+
elif self.providers:
|
|
100
|
+
# Use first available provider as default
|
|
101
|
+
self.current_provider = next(iter(self.providers.values()))
|
|
102
|
+
|
|
103
|
+
# Initialize agent engine after providers are ready
|
|
104
|
+
self._initialize_agent_engine()
|
|
105
|
+
|
|
106
|
+
self._initialized = True
|
|
107
|
+
logger.info(f"Initialized {len(self.providers)} providers")
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Failed to initialize providers: {e}")
|
|
111
|
+
raise ConfigurationError(f"Provider initialization failed: {e}")
|
|
112
|
+
|
|
113
|
+
async def switch_model(
|
|
114
|
+
self, provider_name: str, model_name: Optional[str] = None
|
|
115
|
+
) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Switch to a different provider/model.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
provider_name: Name of the provider to switch to
|
|
121
|
+
model_name: Optional model name to use
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if switch was successful, False otherwise
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
if provider_name not in self.providers:
|
|
128
|
+
raise ConfigurationError(f"Provider '{provider_name}' is not available")
|
|
129
|
+
|
|
130
|
+
provider = self.providers[provider_name]
|
|
131
|
+
|
|
132
|
+
# Switch model if specified
|
|
133
|
+
if model_name:
|
|
134
|
+
# Validate model is available for this provider
|
|
135
|
+
available_models = provider.get_available_models()
|
|
136
|
+
model_names = [m.name for m in available_models]
|
|
137
|
+
|
|
138
|
+
# Also check custom models for this provider
|
|
139
|
+
custom_models = self.config_manager.get_custom_models()
|
|
140
|
+
custom_model_names = [
|
|
141
|
+
m.name for m in custom_models if m.provider == provider_name
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
# Combine both lists
|
|
145
|
+
all_model_names = model_names + custom_model_names
|
|
146
|
+
|
|
147
|
+
if model_name not in all_model_names:
|
|
148
|
+
raise ConfigurationError(
|
|
149
|
+
f"Model '{model_name}' not available for provider '{provider_name}'"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
provider.model = model_name
|
|
153
|
+
|
|
154
|
+
# Switch to the provider
|
|
155
|
+
self.current_provider = provider
|
|
156
|
+
|
|
157
|
+
# Update chat manager with new model
|
|
158
|
+
self.chat_manager.set_current_model(provider.model)
|
|
159
|
+
|
|
160
|
+
logger.info(
|
|
161
|
+
f"Switched to provider: {provider_name}, model: {provider.model}"
|
|
162
|
+
)
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Failed to switch model: {e}")
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
async def send_message(self, message: str) -> ChatResponse:
|
|
170
|
+
"""
|
|
171
|
+
Send a message using the current provider.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
message: Message to send
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Chat response from the provider
|
|
178
|
+
"""
|
|
179
|
+
if not self.current_provider:
|
|
180
|
+
return ChatResponse(
|
|
181
|
+
content="",
|
|
182
|
+
model_used="",
|
|
183
|
+
tokens_used=0,
|
|
184
|
+
error="No provider available. Please configure a provider first.",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
# Get progress indicator
|
|
189
|
+
progress = get_progress_indicator()
|
|
190
|
+
|
|
191
|
+
# Get current chat context
|
|
192
|
+
if progress and progress.enabled:
|
|
193
|
+
progress.start_operation(
|
|
194
|
+
"engine_context",
|
|
195
|
+
OperationType.ANALYZE,
|
|
196
|
+
"Getting chat context",
|
|
197
|
+
)
|
|
198
|
+
context = self.chat_manager.get_current_context()
|
|
199
|
+
if progress and progress.enabled:
|
|
200
|
+
progress.complete_operation("engine_context", "completed")
|
|
201
|
+
|
|
202
|
+
# Send message to provider
|
|
203
|
+
if progress and progress.enabled:
|
|
204
|
+
progress.start_operation(
|
|
205
|
+
"engine_provider",
|
|
206
|
+
OperationType.NETWORK,
|
|
207
|
+
f"Sending to {self.current_provider.get_provider_name()}",
|
|
208
|
+
)
|
|
209
|
+
response = await self.current_provider.send_message(message, context)
|
|
210
|
+
if progress and progress.enabled:
|
|
211
|
+
progress.complete_operation(
|
|
212
|
+
"engine_provider",
|
|
213
|
+
"completed" if response.is_success else "failed",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Always add user message to chat history
|
|
217
|
+
self.chat_manager.add_user_message(message)
|
|
218
|
+
|
|
219
|
+
# Only add assistant message if response was successful
|
|
220
|
+
if response.is_success:
|
|
221
|
+
if progress and progress.enabled:
|
|
222
|
+
progress.start_operation(
|
|
223
|
+
"engine_history",
|
|
224
|
+
OperationType.WRITE,
|
|
225
|
+
"Updating chat history",
|
|
226
|
+
)
|
|
227
|
+
self.chat_manager.add_assistant_message(
|
|
228
|
+
response.content, self.current_provider.model
|
|
229
|
+
)
|
|
230
|
+
if progress and progress.enabled:
|
|
231
|
+
progress.complete_operation("engine_history", "completed")
|
|
232
|
+
|
|
233
|
+
return response
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.error(f"Failed to send message: {e}")
|
|
237
|
+
return ChatResponse(
|
|
238
|
+
content="",
|
|
239
|
+
model_used="",
|
|
240
|
+
tokens_used=0,
|
|
241
|
+
error=f"Failed to send message: {str(e)}",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def get_available_models(self) -> List[ModelInfo]:
|
|
245
|
+
"""Get all available models from all providers."""
|
|
246
|
+
all_models = []
|
|
247
|
+
|
|
248
|
+
for provider in self.providers.values():
|
|
249
|
+
try:
|
|
250
|
+
models = provider.get_available_models()
|
|
251
|
+
# Convert EnhancedModelInfo to ModelInfo if needed
|
|
252
|
+
for model in models:
|
|
253
|
+
if isinstance(model, EnhancedModelInfo):
|
|
254
|
+
all_models.append(model.to_model_info())
|
|
255
|
+
else:
|
|
256
|
+
all_models.append(model)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.warning(
|
|
259
|
+
f"Failed to get models from provider {provider.get_provider_name()}: {e}"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return all_models
|
|
263
|
+
|
|
264
|
+
def get_all_models(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
265
|
+
"""Get all models organized by provider (for CLI display)."""
|
|
266
|
+
result = {}
|
|
267
|
+
|
|
268
|
+
for provider_name, provider in self.providers.items():
|
|
269
|
+
try:
|
|
270
|
+
models = provider.get_available_models()
|
|
271
|
+
result[provider_name] = []
|
|
272
|
+
|
|
273
|
+
for model in models:
|
|
274
|
+
model_dict = {
|
|
275
|
+
"name": model.name,
|
|
276
|
+
"provider": model.provider,
|
|
277
|
+
"supports_tools": getattr(model, "supports_tools", False),
|
|
278
|
+
"supports_multimodal": getattr(
|
|
279
|
+
model, "supports_multimodal", False
|
|
280
|
+
),
|
|
281
|
+
"available": getattr(model, "available", True),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# Add enhanced info if available
|
|
285
|
+
if isinstance(model, EnhancedModelInfo):
|
|
286
|
+
model_dict.update(
|
|
287
|
+
{
|
|
288
|
+
"swe_score": model.swe_score,
|
|
289
|
+
"cost_display": model.get_cost_display(),
|
|
290
|
+
"latest_version": model.latest_version,
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
result[provider_name].append(model_dict)
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.warning(
|
|
298
|
+
f"Failed to get models from provider {provider_name}: {e}"
|
|
299
|
+
)
|
|
300
|
+
result[provider_name] = []
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
def get_current_config(self) -> Dict[str, Any]:
|
|
305
|
+
"""Get current configuration for display."""
|
|
306
|
+
try:
|
|
307
|
+
config = self.config_manager.get_config()
|
|
308
|
+
return {
|
|
309
|
+
"default_provider": config.default_provider,
|
|
310
|
+
"providers": {
|
|
311
|
+
name: {
|
|
312
|
+
"model": provider_config.model,
|
|
313
|
+
"api_key": (
|
|
314
|
+
f"{provider_config.api_key[:8]}***"
|
|
315
|
+
if provider_config.api_key
|
|
316
|
+
else "Not set"
|
|
317
|
+
),
|
|
318
|
+
}
|
|
319
|
+
for name, provider_config in config.providers.items()
|
|
320
|
+
},
|
|
321
|
+
"current_provider": (
|
|
322
|
+
self.current_provider.get_provider_name()
|
|
323
|
+
if self.current_provider
|
|
324
|
+
else None
|
|
325
|
+
),
|
|
326
|
+
"current_model": (
|
|
327
|
+
self.current_provider.model if self.current_provider else None
|
|
328
|
+
),
|
|
329
|
+
}
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(f"Failed to get current config: {e}")
|
|
332
|
+
return {"error": str(e)}
|
|
333
|
+
|
|
334
|
+
def get_current_model_info(self) -> Optional[Dict[str, Any]]:
|
|
335
|
+
"""Get information about the current model."""
|
|
336
|
+
if not self.current_provider:
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
model_info = self.current_provider.get_model_info()
|
|
341
|
+
return {
|
|
342
|
+
"name": model_info.name,
|
|
343
|
+
"provider": model_info.provider,
|
|
344
|
+
"supports_tools": getattr(model_info, "supports_tools", False),
|
|
345
|
+
"supports_multimodal": getattr(
|
|
346
|
+
model_info, "supports_multimodal", False
|
|
347
|
+
),
|
|
348
|
+
"available": getattr(model_info, "available", True),
|
|
349
|
+
}
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.warning(f"Failed to get current model info: {e}")
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
def get_conversation_summary(self) -> Dict[str, Any]:
|
|
355
|
+
"""Get summary of current conversation."""
|
|
356
|
+
try:
|
|
357
|
+
context = self.chat_manager.get_current_context()
|
|
358
|
+
return {
|
|
359
|
+
"message_count": len(context.messages),
|
|
360
|
+
"current_model": (
|
|
361
|
+
self.current_provider.model if self.current_provider else None
|
|
362
|
+
),
|
|
363
|
+
"session_id": context.session_id,
|
|
364
|
+
}
|
|
365
|
+
except Exception as e:
|
|
366
|
+
logger.error(f"Failed to get conversation summary: {e}")
|
|
367
|
+
return {"error": str(e)}
|
|
368
|
+
|
|
369
|
+
async def validate_current_provider(self) -> bool:
|
|
370
|
+
"""Validate that the current provider is working."""
|
|
371
|
+
if not self.current_provider:
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
return await self.current_provider.validate_credentials()
|
|
376
|
+
except Exception as e:
|
|
377
|
+
logger.error(f"Provider validation failed: {e}")
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
async def check_provider_health(
|
|
381
|
+
self, provider_name: Optional[str] = None, force: bool = False
|
|
382
|
+
) -> Dict[str, Any]:
|
|
383
|
+
"""
|
|
384
|
+
Check health status of providers using the optimized health monitor.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
provider_name: Name of specific provider to check, or None for all
|
|
388
|
+
force: Force check even if cached result is available
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Dictionary with health status information
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
config = self.config_manager.get_config()
|
|
395
|
+
|
|
396
|
+
if provider_name:
|
|
397
|
+
# Check specific provider
|
|
398
|
+
if provider_name not in config.providers:
|
|
399
|
+
return {
|
|
400
|
+
provider_name: {
|
|
401
|
+
"status": "error",
|
|
402
|
+
"message": f"Provider {provider_name} not configured",
|
|
403
|
+
"available": False,
|
|
404
|
+
"credentials_valid": False,
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
provider_config = config.providers[provider_name]
|
|
409
|
+
status = await self.health_monitor.check_provider_health(
|
|
410
|
+
provider_name, provider_config, force=force
|
|
411
|
+
)
|
|
412
|
+
return {provider_name: status}
|
|
413
|
+
else:
|
|
414
|
+
# Check all providers
|
|
415
|
+
return await self.health_monitor.check_all_providers_health(
|
|
416
|
+
config.providers, force=force
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.error(f"Error checking provider health: {e}")
|
|
421
|
+
if provider_name:
|
|
422
|
+
return {
|
|
423
|
+
provider_name: {
|
|
424
|
+
"status": "error",
|
|
425
|
+
"message": f"Health check failed: {str(e)}",
|
|
426
|
+
"available": False,
|
|
427
|
+
"credentials_valid": False,
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else:
|
|
431
|
+
return {"error": f"Health check failed: {str(e)}"}
|
|
432
|
+
|
|
433
|
+
def save_conversation(self, name: str) -> str:
|
|
434
|
+
"""Save current conversation."""
|
|
435
|
+
try:
|
|
436
|
+
context = self.chat_manager.get_current_context()
|
|
437
|
+
return self.conversation_manager.save_conversation(context, name)
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"Failed to save conversation: {e}")
|
|
440
|
+
raise
|
|
441
|
+
|
|
442
|
+
def list_conversations(self) -> List[Dict[str, Any]]:
|
|
443
|
+
"""List saved conversations."""
|
|
444
|
+
try:
|
|
445
|
+
return self.conversation_manager.list_conversations()
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.error(f"Failed to list conversations: {e}")
|
|
448
|
+
return []
|
|
449
|
+
|
|
450
|
+
def load_conversation(self, filename: str) -> bool:
|
|
451
|
+
"""Load a saved conversation."""
|
|
452
|
+
try:
|
|
453
|
+
context = self.conversation_manager.load_conversation(filename)
|
|
454
|
+
self.chat_manager.current_context = context
|
|
455
|
+
return True
|
|
456
|
+
except Exception as e:
|
|
457
|
+
logger.error(f"Failed to load conversation: {e}")
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
async def initialize_mcp(self) -> None:
|
|
461
|
+
"""Initialize MCP (Model Context Protocol) servers."""
|
|
462
|
+
try:
|
|
463
|
+
if self.mcp_manager:
|
|
464
|
+
await self.mcp_manager.initialize_servers()
|
|
465
|
+
logger.info("MCP servers initialized successfully")
|
|
466
|
+
else:
|
|
467
|
+
logger.info("MCP manager not configured - skipping MCP initialization")
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.error(f"Failed to initialize MCP: {e}")
|
|
470
|
+
raise
|
|
471
|
+
|
|
472
|
+
async def shutdown_mcp(self) -> None:
|
|
473
|
+
"""Shutdown MCP servers gracefully."""
|
|
474
|
+
try:
|
|
475
|
+
if self.mcp_manager:
|
|
476
|
+
await self.mcp_manager.shutdown()
|
|
477
|
+
logger.info("MCP servers shutdown successfully")
|
|
478
|
+
else:
|
|
479
|
+
logger.info("MCP manager not configured - skipping MCP shutdown")
|
|
480
|
+
except Exception as e:
|
|
481
|
+
logger.error(f"Failed to shutdown MCP: {e}")
|
|
482
|
+
# Don't raise during shutdown
|
|
483
|
+
|
|
484
|
+
def _get_models_list(self) -> str:
|
|
485
|
+
"""Get a basic models list as fallback."""
|
|
486
|
+
models = self.get_available_models()
|
|
487
|
+
if not models:
|
|
488
|
+
return "No models available."
|
|
489
|
+
|
|
490
|
+
lines = []
|
|
491
|
+
for model in models:
|
|
492
|
+
lines.append(f"- {model.name} ({model.provider})")
|
|
493
|
+
|
|
494
|
+
return "\n".join(lines)
|
|
495
|
+
|
|
496
|
+
async def _get_tools_list(self) -> str:
|
|
497
|
+
"""Get formatted list of available MCP tools."""
|
|
498
|
+
if not self.mcp_manager or not self.mcp_manager.initialized:
|
|
499
|
+
return "MCP is not initialized. No tools available."
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
# Get available tools from MCP manager
|
|
503
|
+
tools = await self.mcp_manager.get_available_tools()
|
|
504
|
+
|
|
505
|
+
if not tools:
|
|
506
|
+
return "No MCP tools available."
|
|
507
|
+
|
|
508
|
+
# Group tools by server
|
|
509
|
+
tools_by_server = {}
|
|
510
|
+
for tool in tools:
|
|
511
|
+
server_name = getattr(tool, "server_name", "Unknown")
|
|
512
|
+
if server_name not in tools_by_server:
|
|
513
|
+
tools_by_server[server_name] = []
|
|
514
|
+
tools_by_server[server_name].append(tool)
|
|
515
|
+
|
|
516
|
+
# Format output
|
|
517
|
+
lines = []
|
|
518
|
+
lines.append("Available MCP Tools:")
|
|
519
|
+
lines.append("=" * 50)
|
|
520
|
+
|
|
521
|
+
for server_name, server_tools in tools_by_server.items():
|
|
522
|
+
lines.append(f"\n📡 {server_name} ({len(server_tools)} tools)")
|
|
523
|
+
lines.append("-" * 30)
|
|
524
|
+
|
|
525
|
+
for tool in server_tools:
|
|
526
|
+
name = getattr(tool, "name", "Unknown")
|
|
527
|
+
description = getattr(tool, "description", "No description")
|
|
528
|
+
lines.append(f" 🔧 {name}")
|
|
529
|
+
if description and description != "No description":
|
|
530
|
+
lines.append(f" {description}")
|
|
531
|
+
|
|
532
|
+
lines.append(
|
|
533
|
+
f"\nTotal: {len(tools)} tools across {len(tools_by_server)} servers"
|
|
534
|
+
)
|
|
535
|
+
return "\n".join(lines)
|
|
536
|
+
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.error(f"Error getting tools list: {e}")
|
|
539
|
+
return f"Error retrieving tools list: {str(e)}"
|
|
540
|
+
|
|
541
|
+
async def _handle_mcp_command(self, command_obj) -> str:
|
|
542
|
+
"""Handle MCP management commands."""
|
|
543
|
+
if not self.mcp_manager:
|
|
544
|
+
return "MCP is not configured for this installation."
|
|
545
|
+
|
|
546
|
+
# Extract arguments from Command object or treat as string
|
|
547
|
+
if hasattr(command_obj, "args") and command_obj.args:
|
|
548
|
+
args = " ".join(command_obj.args)
|
|
549
|
+
elif isinstance(command_obj, str):
|
|
550
|
+
args = command_obj
|
|
551
|
+
else:
|
|
552
|
+
args = ""
|
|
553
|
+
|
|
554
|
+
args = args.strip()
|
|
555
|
+
if not args:
|
|
556
|
+
args = "status"
|
|
557
|
+
|
|
558
|
+
command_parts = args.split()
|
|
559
|
+
command = command_parts[0].lower()
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
if command == "status":
|
|
563
|
+
return await self._mcp_status()
|
|
564
|
+
elif command == "reload":
|
|
565
|
+
return await self._mcp_reload()
|
|
566
|
+
elif command == "connect":
|
|
567
|
+
server_name = command_parts[1] if len(command_parts) > 1 else None
|
|
568
|
+
return await self._mcp_connect(server_name)
|
|
569
|
+
elif command == "disconnect":
|
|
570
|
+
server_name = command_parts[1] if len(command_parts) > 1 else None
|
|
571
|
+
return await self._mcp_disconnect(server_name)
|
|
572
|
+
elif command == "health":
|
|
573
|
+
return await self._mcp_health()
|
|
574
|
+
elif command == "servers":
|
|
575
|
+
return self._mcp_servers()
|
|
576
|
+
elif command == "tools":
|
|
577
|
+
server_name = command_parts[1] if len(command_parts) > 1 else None
|
|
578
|
+
return await self._mcp_tools(server_name)
|
|
579
|
+
else:
|
|
580
|
+
return self._mcp_help()
|
|
581
|
+
|
|
582
|
+
except Exception as e:
|
|
583
|
+
logger.error(f"Error handling MCP command '{command}': {e}")
|
|
584
|
+
return f"Error executing MCP command: {str(e)}"
|
|
585
|
+
|
|
586
|
+
async def _mcp_status(self) -> str:
|
|
587
|
+
"""Get MCP system status."""
|
|
588
|
+
if not self.mcp_manager:
|
|
589
|
+
return "MCP is not configured."
|
|
590
|
+
|
|
591
|
+
status_info = []
|
|
592
|
+
status_info.append("MCP System Status")
|
|
593
|
+
status_info.append("=" * 40)
|
|
594
|
+
|
|
595
|
+
# Basic status
|
|
596
|
+
status_info.append(f"Enabled: {'Yes' if self.mcp_manager.is_enabled else 'No'}")
|
|
597
|
+
status_info.append(
|
|
598
|
+
f"Initialized: {'Yes' if self.mcp_manager.initialized else 'No'}"
|
|
599
|
+
)
|
|
600
|
+
status_info.append(
|
|
601
|
+
f"Connected Servers: {self.mcp_manager.connected_server_count}"
|
|
602
|
+
)
|
|
603
|
+
status_info.append(f"Total Tools: {self.mcp_manager.total_tool_count}")
|
|
604
|
+
|
|
605
|
+
# Degradation status
|
|
606
|
+
degradation = self.mcp_manager.get_degradation_status()
|
|
607
|
+
status_info.append(f"Degradation Level: {degradation['degradation_level']}")
|
|
608
|
+
|
|
609
|
+
if degradation["functionality_impact"]:
|
|
610
|
+
status_info.append("\nFunctionality Impact:")
|
|
611
|
+
for impact in degradation["functionality_impact"]:
|
|
612
|
+
status_info.append(f" • {impact}")
|
|
613
|
+
|
|
614
|
+
return "\n".join(status_info)
|
|
615
|
+
|
|
616
|
+
async def _mcp_reload(self) -> str:
|
|
617
|
+
"""Reload MCP servers."""
|
|
618
|
+
if not self.mcp_manager:
|
|
619
|
+
return "MCP is not configured."
|
|
620
|
+
|
|
621
|
+
try:
|
|
622
|
+
await self.mcp_manager.reload_servers()
|
|
623
|
+
return "MCP servers reloaded successfully."
|
|
624
|
+
except Exception as e:
|
|
625
|
+
return f"Error reloading MCP servers: {str(e)}"
|
|
626
|
+
|
|
627
|
+
async def _mcp_connect(self, server_name: str = None) -> str:
|
|
628
|
+
"""Connect to MCP server(s)."""
|
|
629
|
+
if not self.mcp_manager:
|
|
630
|
+
return "MCP is not configured."
|
|
631
|
+
|
|
632
|
+
try:
|
|
633
|
+
if server_name:
|
|
634
|
+
# For specific server, we'd need a method to connect individual servers
|
|
635
|
+
return f"Connecting to specific server '{server_name}' is not yet implemented. Use reload to reconnect all servers."
|
|
636
|
+
else:
|
|
637
|
+
await self.mcp_manager.initialize_servers()
|
|
638
|
+
return "Attempted to connect to all MCP servers."
|
|
639
|
+
except Exception as e:
|
|
640
|
+
return f"Error connecting to MCP servers: {str(e)}"
|
|
641
|
+
|
|
642
|
+
async def _mcp_disconnect(self, server_name: str = None) -> str:
|
|
643
|
+
"""Disconnect from MCP server(s)."""
|
|
644
|
+
if not self.mcp_manager:
|
|
645
|
+
return "MCP is not configured."
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
if server_name:
|
|
649
|
+
success = await self.mcp_manager.shutdown_servers(server_name)
|
|
650
|
+
if success:
|
|
651
|
+
return f"Disconnected from server '{server_name}'."
|
|
652
|
+
else:
|
|
653
|
+
return (
|
|
654
|
+
f"Server '{server_name}' was not connected or does not exist."
|
|
655
|
+
)
|
|
656
|
+
else:
|
|
657
|
+
await self.mcp_manager.shutdown()
|
|
658
|
+
return "Disconnected from all MCP servers."
|
|
659
|
+
except Exception as e:
|
|
660
|
+
return f"Error disconnecting from MCP servers: {str(e)}"
|
|
661
|
+
|
|
662
|
+
async def _mcp_health(self) -> str:
|
|
663
|
+
"""Get MCP health status."""
|
|
664
|
+
if not self.mcp_manager:
|
|
665
|
+
return "MCP is not configured."
|
|
666
|
+
|
|
667
|
+
try:
|
|
668
|
+
# Get both server status and health check
|
|
669
|
+
server_status = self.mcp_manager.get_server_status()
|
|
670
|
+
health_status = await self.mcp_manager.health_check()
|
|
671
|
+
|
|
672
|
+
health_info = []
|
|
673
|
+
health_info.append("MCP Server Health")
|
|
674
|
+
health_info.append("=" * 40)
|
|
675
|
+
|
|
676
|
+
for server_name, status in server_status.items():
|
|
677
|
+
health_check = health_status.get(server_name, False)
|
|
678
|
+
health_icon = "🟢" if health_check else "🔴"
|
|
679
|
+
|
|
680
|
+
health_info.append(f"\n{health_icon} {server_name}")
|
|
681
|
+
health_info.append(
|
|
682
|
+
f" Enabled: {'Yes' if status['enabled'] else 'No'}"
|
|
683
|
+
)
|
|
684
|
+
health_info.append(
|
|
685
|
+
f" Connected: {'Yes' if status['connected'] else 'No'}"
|
|
686
|
+
)
|
|
687
|
+
health_info.append(f" Healthy: {'Yes' if health_check else 'No'}")
|
|
688
|
+
health_info.append(f" Tools: {status['tool_count']}")
|
|
689
|
+
health_info.append(f" Command: {status['command']}")
|
|
690
|
+
|
|
691
|
+
overall_health = health_status.get("overall_healthy", False)
|
|
692
|
+
health_info.append(
|
|
693
|
+
f"\nOverall Health: {'🟢 Healthy' if overall_health else '🔴 Issues detected'}"
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
return "\n".join(health_info)
|
|
697
|
+
|
|
698
|
+
except Exception as e:
|
|
699
|
+
return f"Error getting health status: {str(e)}"
|
|
700
|
+
|
|
701
|
+
def _mcp_servers(self) -> str:
|
|
702
|
+
"""List MCP servers."""
|
|
703
|
+
if not self.mcp_manager:
|
|
704
|
+
return "MCP is not configured."
|
|
705
|
+
|
|
706
|
+
try:
|
|
707
|
+
server_status = self.mcp_manager.get_server_status()
|
|
708
|
+
|
|
709
|
+
servers_info = []
|
|
710
|
+
servers_info.append("MCP Servers")
|
|
711
|
+
servers_info.append("=" * 30)
|
|
712
|
+
|
|
713
|
+
for server_name, status in server_status.items():
|
|
714
|
+
icon = "🟢" if status["connected"] else "🔴"
|
|
715
|
+
servers_info.append(f"{icon} {server_name}")
|
|
716
|
+
servers_info.append(f" Tools: {status['tool_count']}")
|
|
717
|
+
if status["args"]:
|
|
718
|
+
servers_info.append(f" Args: {' '.join(status['args'])}")
|
|
719
|
+
|
|
720
|
+
return "\n".join(servers_info)
|
|
721
|
+
|
|
722
|
+
except Exception as e:
|
|
723
|
+
return f"Error listing servers: {str(e)}"
|
|
724
|
+
|
|
725
|
+
async def _mcp_tools(self, server_name: str = None) -> str:
|
|
726
|
+
"""List tools from specific server or all servers."""
|
|
727
|
+
if not self.mcp_manager:
|
|
728
|
+
return "MCP is not configured."
|
|
729
|
+
|
|
730
|
+
try:
|
|
731
|
+
if server_name:
|
|
732
|
+
tools = self.mcp_manager.get_tools_by_server(server_name)
|
|
733
|
+
if not tools:
|
|
734
|
+
return f"No tools found for server '{server_name}' or server not connected."
|
|
735
|
+
|
|
736
|
+
tools_info = []
|
|
737
|
+
tools_info.append(f"Tools from {server_name}")
|
|
738
|
+
tools_info.append("=" * 40)
|
|
739
|
+
|
|
740
|
+
for tool in tools:
|
|
741
|
+
name = getattr(tool, "name", "Unknown")
|
|
742
|
+
description = getattr(tool, "description", "No description")
|
|
743
|
+
tools_info.append(f"🔧 {name}")
|
|
744
|
+
if description:
|
|
745
|
+
tools_info.append(f" {description}")
|
|
746
|
+
|
|
747
|
+
return "\n".join(tools_info)
|
|
748
|
+
else:
|
|
749
|
+
# Return summary of all tools
|
|
750
|
+
return await self._get_tools_list()
|
|
751
|
+
|
|
752
|
+
except Exception as e:
|
|
753
|
+
return f"Error listing tools: {str(e)}"
|
|
754
|
+
|
|
755
|
+
def _mcp_help(self) -> str:
|
|
756
|
+
"""Show MCP command help."""
|
|
757
|
+
help_text = """
|
|
758
|
+
MCP Commands:
|
|
759
|
+
=============
|
|
760
|
+
|
|
761
|
+
/mcp status - Show MCP system status
|
|
762
|
+
/mcp health - Show server health status
|
|
763
|
+
/mcp servers - List all configured servers
|
|
764
|
+
/mcp tools - List all available tools
|
|
765
|
+
/mcp tools <server> - List tools from specific server
|
|
766
|
+
/mcp reload - Reload MCP configuration
|
|
767
|
+
/mcp connect [server] - Connect to server(s)
|
|
768
|
+
/mcp disconnect [server] - Disconnect from server(s)
|
|
769
|
+
|
|
770
|
+
Examples:
|
|
771
|
+
/mcp status
|
|
772
|
+
/mcp tools filesystem
|
|
773
|
+
/mcp health
|
|
774
|
+
"""
|
|
775
|
+
return help_text.strip()
|
|
776
|
+
|
|
777
|
+
def _initialize_agent_engine(self) -> None:
|
|
778
|
+
"""Initialize the agent engine for autonomous operations."""
|
|
779
|
+
try:
|
|
780
|
+
from .agent_engine import AgentEngine
|
|
781
|
+
|
|
782
|
+
self.agent_engine = AgentEngine(self.config_manager)
|
|
783
|
+
logger.info("Agent engine initialized successfully")
|
|
784
|
+
except Exception as e:
|
|
785
|
+
logger.warning(f"Failed to initialize agent engine: {e}")
|
|
786
|
+
# Don't fail completely if agent engine can't be initialized
|
|
787
|
+
self.agent_engine = None
|