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,715 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Agent Configuration Schema and Storage Layer.
|
|
3
|
+
|
|
4
|
+
This module defines the data model for custom agent configurations and provides
|
|
5
|
+
persistent storage functionality for user-created agent profiles.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional, Set
|
|
16
|
+
|
|
17
|
+
from jsonschema import ValidationError, validate
|
|
18
|
+
|
|
19
|
+
from omnimancer.core.models import ConfigTemplateManager
|
|
20
|
+
|
|
21
|
+
from .config import (
|
|
22
|
+
ProviderType,
|
|
23
|
+
)
|
|
24
|
+
from .persona import PersonaCapability, PersonaCategory, PersonaMetadata
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CustomAgentStatus(Enum):
|
|
30
|
+
"""Status of custom agent configurations."""
|
|
31
|
+
|
|
32
|
+
DRAFT = "draft"
|
|
33
|
+
ACTIVE = "active"
|
|
34
|
+
DISABLED = "disabled"
|
|
35
|
+
ARCHIVED = "archived"
|
|
36
|
+
ERROR = "error"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AgentTool(Enum):
|
|
40
|
+
"""Available tools that can be enabled for custom agents."""
|
|
41
|
+
|
|
42
|
+
FILE_OPERATIONS = "file_operations"
|
|
43
|
+
PROGRAM_EXECUTOR = "program_executor"
|
|
44
|
+
WEB_CLIENT = "web_client"
|
|
45
|
+
MCP_TOOLS = "mcp_tools"
|
|
46
|
+
SEARCH = "search"
|
|
47
|
+
CODE_ANALYSIS = "code_analysis"
|
|
48
|
+
SYSTEM_INFO = "system_info"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ModelSettings:
|
|
53
|
+
"""Model configuration settings for custom agents."""
|
|
54
|
+
|
|
55
|
+
provider_type: ProviderType
|
|
56
|
+
model_name: str
|
|
57
|
+
temperature: float = 0.7
|
|
58
|
+
max_tokens: Optional[int] = None
|
|
59
|
+
top_p: Optional[float] = None
|
|
60
|
+
frequency_penalty: Optional[float] = None
|
|
61
|
+
presence_penalty: Optional[float] = None
|
|
62
|
+
stop_sequences: List[str] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
def __post_init__(self):
|
|
65
|
+
"""Validate model settings after initialization."""
|
|
66
|
+
if not (0.0 <= self.temperature <= 2.0):
|
|
67
|
+
raise ValueError("Temperature must be between 0.0 and 2.0")
|
|
68
|
+
if self.top_p is not None and not (0.0 <= self.top_p <= 1.0):
|
|
69
|
+
raise ValueError("Top-p must be between 0.0 and 1.0")
|
|
70
|
+
if self.max_tokens is not None and self.max_tokens < 1:
|
|
71
|
+
raise ValueError("Max tokens must be positive")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ContextParameters:
|
|
76
|
+
"""Context and behavior parameters for custom agents."""
|
|
77
|
+
|
|
78
|
+
system_prompt: str = ""
|
|
79
|
+
context_window_size: int = 32000
|
|
80
|
+
conversation_memory: bool = True
|
|
81
|
+
memory_limit: int = 50 # Number of messages to remember
|
|
82
|
+
user_context: str = "" # Additional user-specific context
|
|
83
|
+
response_format: str = "default" # default, json, markdown
|
|
84
|
+
|
|
85
|
+
def __post_init__(self):
|
|
86
|
+
"""Validate context parameters after initialization."""
|
|
87
|
+
if self.context_window_size < 1000:
|
|
88
|
+
raise ValueError("Context window size must be at least 1000")
|
|
89
|
+
if self.memory_limit < 1:
|
|
90
|
+
raise ValueError("Memory limit must be positive")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class BehaviorRules:
|
|
95
|
+
"""Behavior rules and constraints for custom agents."""
|
|
96
|
+
|
|
97
|
+
max_response_length: Optional[int] = None
|
|
98
|
+
require_confirmation_for: List[str] = field(
|
|
99
|
+
default_factory=lambda: ["file_delete", "system_commands"]
|
|
100
|
+
)
|
|
101
|
+
auto_approve_patterns: List[str] = field(default_factory=list)
|
|
102
|
+
forbidden_operations: List[str] = field(default_factory=list)
|
|
103
|
+
custom_restrictions: List[str] = field(default_factory=list)
|
|
104
|
+
reasoning_style: str = "balanced" # concise, balanced, verbose, step_by_step
|
|
105
|
+
creativity_level: str = "medium" # low, medium, high
|
|
106
|
+
safety_level: str = "standard" # strict, standard, permissive
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class CustomAgentConfig:
|
|
111
|
+
"""Complete configuration for a custom agent."""
|
|
112
|
+
|
|
113
|
+
# Identity and metadata
|
|
114
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
115
|
+
name: str = "Untitled Agent"
|
|
116
|
+
description: str = ""
|
|
117
|
+
category: PersonaCategory = PersonaCategory.CUSTOM
|
|
118
|
+
|
|
119
|
+
# Base template information
|
|
120
|
+
base_template_id: Optional[str] = None # Template to inherit from
|
|
121
|
+
|
|
122
|
+
# Configuration components
|
|
123
|
+
model_settings: ModelSettings = field(
|
|
124
|
+
default_factory=lambda: ModelSettings(
|
|
125
|
+
provider_type=ProviderType.ANTHROPIC,
|
|
126
|
+
model_name="claude-3-haiku-20240307",
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
context_parameters: ContextParameters = field(default_factory=ContextParameters)
|
|
130
|
+
behavior_rules: BehaviorRules = field(default_factory=BehaviorRules)
|
|
131
|
+
|
|
132
|
+
# Capabilities and tools
|
|
133
|
+
capabilities: Set[PersonaCapability] = field(
|
|
134
|
+
default_factory=lambda: {PersonaCapability.GENERAL_PURPOSE}
|
|
135
|
+
)
|
|
136
|
+
enabled_tools: Set[AgentTool] = field(
|
|
137
|
+
default_factory=lambda: {
|
|
138
|
+
AgentTool.FILE_OPERATIONS,
|
|
139
|
+
AgentTool.PROGRAM_EXECUTOR,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Security and permissions (inherit from base if not overridden)
|
|
144
|
+
security_overrides: Optional[Dict[str, Any]] = None
|
|
145
|
+
agent_overrides: Optional[Dict[str, Any]] = None
|
|
146
|
+
|
|
147
|
+
# Metadata
|
|
148
|
+
metadata: PersonaMetadata = field(
|
|
149
|
+
default_factory=lambda: PersonaMetadata(is_builtin=False, is_custom=True)
|
|
150
|
+
)
|
|
151
|
+
status: CustomAgentStatus = CustomAgentStatus.DRAFT
|
|
152
|
+
|
|
153
|
+
# Version and tracking
|
|
154
|
+
version: str = "1.0.0"
|
|
155
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
156
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
157
|
+
|
|
158
|
+
def __post_init__(self):
|
|
159
|
+
"""Validate configuration after initialization."""
|
|
160
|
+
self._validate_config()
|
|
161
|
+
|
|
162
|
+
# Ensure metadata is properly configured for custom agents
|
|
163
|
+
self.metadata.is_custom = True
|
|
164
|
+
self.metadata.is_builtin = False
|
|
165
|
+
if not self.metadata.author:
|
|
166
|
+
self.metadata.author = "User"
|
|
167
|
+
|
|
168
|
+
def _validate_config(self):
|
|
169
|
+
"""Validate the complete configuration."""
|
|
170
|
+
if not self.name.strip():
|
|
171
|
+
raise ValueError("Agent name cannot be empty")
|
|
172
|
+
|
|
173
|
+
if len(self.name) > 100:
|
|
174
|
+
raise ValueError("Agent name too long (max 100 characters)")
|
|
175
|
+
|
|
176
|
+
if len(self.description) > 500:
|
|
177
|
+
raise ValueError("Agent description too long (max 500 characters)")
|
|
178
|
+
|
|
179
|
+
# Validate tool dependencies
|
|
180
|
+
if AgentTool.MCP_TOOLS in self.enabled_tools:
|
|
181
|
+
# MCP tools require web client for most operations
|
|
182
|
+
if AgentTool.WEB_CLIENT not in self.enabled_tools:
|
|
183
|
+
logger.warning("MCP tools work best with web client enabled")
|
|
184
|
+
|
|
185
|
+
# Validate capability consistency
|
|
186
|
+
if PersonaCapability.WEB_SEARCH in self.capabilities:
|
|
187
|
+
if AgentTool.WEB_CLIENT not in self.enabled_tools:
|
|
188
|
+
raise ValueError("Web search capability requires web client tool")
|
|
189
|
+
|
|
190
|
+
if PersonaCapability.FILE_OPERATIONS in self.capabilities:
|
|
191
|
+
if AgentTool.FILE_OPERATIONS not in self.enabled_tools:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
"File operations capability requires file operations tool"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def inherit_from_template(self, template_manager: ConfigTemplateManager) -> None:
|
|
197
|
+
"""Inherit settings from a base template."""
|
|
198
|
+
if not self.base_template_id:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
template = template_manager.get_template(self.base_template_id)
|
|
203
|
+
|
|
204
|
+
# Inherit capabilities if none specified
|
|
205
|
+
if not self.capabilities or self.capabilities == {
|
|
206
|
+
PersonaCapability.GENERAL_PURPOSE
|
|
207
|
+
}:
|
|
208
|
+
# Map template capabilities to persona capabilities
|
|
209
|
+
template_caps = template.metadata.get("capabilities", [])
|
|
210
|
+
self.capabilities = set()
|
|
211
|
+
for cap_name in template_caps:
|
|
212
|
+
try:
|
|
213
|
+
self.capabilities.add(PersonaCapability(cap_name))
|
|
214
|
+
except ValueError:
|
|
215
|
+
logger.warning(f"Unknown capability in template: {cap_name}")
|
|
216
|
+
|
|
217
|
+
# Inherit model settings if using default
|
|
218
|
+
if (
|
|
219
|
+
self.model_settings.provider_type == ProviderType.ANTHROPIC
|
|
220
|
+
and self.model_settings.model_name == "claude-3-haiku-20240307"
|
|
221
|
+
):
|
|
222
|
+
|
|
223
|
+
# Use template's recommended model
|
|
224
|
+
primary_provider = template.metadata.get("primary_provider", "claude")
|
|
225
|
+
if primary_provider in template.recommended_models:
|
|
226
|
+
self.model_settings.model_name = template.recommended_models[
|
|
227
|
+
primary_provider
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
# Use template's provider configuration
|
|
231
|
+
if primary_provider in template.provider_configs:
|
|
232
|
+
provider_config = template.provider_configs[primary_provider]
|
|
233
|
+
self.model_settings.temperature = provider_config.get(
|
|
234
|
+
"temperature", 0.7
|
|
235
|
+
)
|
|
236
|
+
self.model_settings.max_tokens = provider_config.get("max_tokens")
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.warning(
|
|
240
|
+
f"Failed to inherit from template {self.base_template_id}: {e}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def update_timestamp(self):
|
|
244
|
+
"""Update the last modified timestamp."""
|
|
245
|
+
self.updated_at = datetime.now()
|
|
246
|
+
self.metadata.updated_at = self.updated_at
|
|
247
|
+
|
|
248
|
+
def increment_usage(self):
|
|
249
|
+
"""Increment usage statistics."""
|
|
250
|
+
self.metadata.update_usage()
|
|
251
|
+
self.update_timestamp()
|
|
252
|
+
|
|
253
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
254
|
+
"""Convert to dictionary for serialization."""
|
|
255
|
+
data = asdict(self)
|
|
256
|
+
|
|
257
|
+
# Convert enums to string values
|
|
258
|
+
data["category"] = self.category.value
|
|
259
|
+
data["status"] = self.status.value
|
|
260
|
+
data["model_settings"][
|
|
261
|
+
"provider_type"
|
|
262
|
+
] = self.model_settings.provider_type.value
|
|
263
|
+
data["capabilities"] = [cap.value for cap in self.capabilities]
|
|
264
|
+
data["enabled_tools"] = [tool.value for tool in self.enabled_tools]
|
|
265
|
+
|
|
266
|
+
# Handle datetime serialization
|
|
267
|
+
data["created_at"] = self.created_at.isoformat()
|
|
268
|
+
data["updated_at"] = self.updated_at.isoformat()
|
|
269
|
+
data["metadata"]["created_at"] = self.metadata.created_at.isoformat()
|
|
270
|
+
data["metadata"]["updated_at"] = self.metadata.updated_at.isoformat()
|
|
271
|
+
if self.metadata.last_used:
|
|
272
|
+
data["metadata"]["last_used"] = self.metadata.last_used.isoformat()
|
|
273
|
+
|
|
274
|
+
return data
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def from_dict(cls, data: Dict[str, Any]) -> "CustomAgentConfig":
|
|
278
|
+
"""Create instance from dictionary."""
|
|
279
|
+
# Convert string values back to enums
|
|
280
|
+
data = data.copy() # Don't modify original
|
|
281
|
+
|
|
282
|
+
# Handle PersonaCategory
|
|
283
|
+
if "category" in data:
|
|
284
|
+
data["category"] = PersonaCategory(data["category"])
|
|
285
|
+
|
|
286
|
+
# Handle CustomAgentStatus
|
|
287
|
+
if "status" in data:
|
|
288
|
+
data["status"] = CustomAgentStatus(data["status"])
|
|
289
|
+
|
|
290
|
+
# Handle ModelSettings
|
|
291
|
+
if "model_settings" in data:
|
|
292
|
+
model_data = data["model_settings"].copy()
|
|
293
|
+
model_data["provider_type"] = ProviderType(model_data["provider_type"])
|
|
294
|
+
data["model_settings"] = ModelSettings(**model_data)
|
|
295
|
+
|
|
296
|
+
# Handle ContextParameters
|
|
297
|
+
if "context_parameters" in data:
|
|
298
|
+
data["context_parameters"] = ContextParameters(**data["context_parameters"])
|
|
299
|
+
|
|
300
|
+
# Handle BehaviorRules
|
|
301
|
+
if "behavior_rules" in data:
|
|
302
|
+
data["behavior_rules"] = BehaviorRules(**data["behavior_rules"])
|
|
303
|
+
|
|
304
|
+
# Handle capabilities set
|
|
305
|
+
if "capabilities" in data:
|
|
306
|
+
data["capabilities"] = {
|
|
307
|
+
PersonaCapability(cap) for cap in data["capabilities"]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Handle enabled_tools set
|
|
311
|
+
if "enabled_tools" in data:
|
|
312
|
+
data["enabled_tools"] = {AgentTool(tool) for tool in data["enabled_tools"]}
|
|
313
|
+
|
|
314
|
+
# Handle datetime fields
|
|
315
|
+
for field_name in ["created_at", "updated_at"]:
|
|
316
|
+
if field_name in data:
|
|
317
|
+
data[field_name] = datetime.fromisoformat(data[field_name])
|
|
318
|
+
|
|
319
|
+
# Handle metadata
|
|
320
|
+
if "metadata" in data:
|
|
321
|
+
metadata_data = data["metadata"].copy()
|
|
322
|
+
for field_name in ["created_at", "updated_at"]:
|
|
323
|
+
if field_name in metadata_data:
|
|
324
|
+
metadata_data[field_name] = datetime.fromisoformat(
|
|
325
|
+
metadata_data[field_name]
|
|
326
|
+
)
|
|
327
|
+
if "last_used" in metadata_data and metadata_data["last_used"]:
|
|
328
|
+
metadata_data["last_used"] = datetime.fromisoformat(
|
|
329
|
+
metadata_data["last_used"]
|
|
330
|
+
)
|
|
331
|
+
data["metadata"] = PersonaMetadata(**metadata_data)
|
|
332
|
+
|
|
333
|
+
return cls(**data)
|
|
334
|
+
|
|
335
|
+
def get_display_summary(self) -> Dict[str, Any]:
|
|
336
|
+
"""Get summary for display in listings."""
|
|
337
|
+
return {
|
|
338
|
+
"id": self.id,
|
|
339
|
+
"name": self.name,
|
|
340
|
+
"description": self.description[:100]
|
|
341
|
+
+ ("..." if len(self.description) > 100 else ""),
|
|
342
|
+
"category": self.category.value,
|
|
343
|
+
"status": self.status.value,
|
|
344
|
+
"model": f"{self.model_settings.provider_type.value}:{self.model_settings.model_name}",
|
|
345
|
+
"capabilities_count": len(self.capabilities),
|
|
346
|
+
"tools_count": len(self.enabled_tools),
|
|
347
|
+
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
348
|
+
"last_used": (
|
|
349
|
+
self.metadata.last_used.strftime("%Y-%m-%d %H:%M")
|
|
350
|
+
if self.metadata.last_used
|
|
351
|
+
else "Never"
|
|
352
|
+
),
|
|
353
|
+
"usage_count": self.metadata.usage_count,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class CustomAgentValidator:
|
|
358
|
+
"""Validates custom agent configurations."""
|
|
359
|
+
|
|
360
|
+
def __init__(self):
|
|
361
|
+
self.schema = self._build_schema()
|
|
362
|
+
|
|
363
|
+
def _build_schema(self) -> Dict[str, Any]:
|
|
364
|
+
"""Build JSON schema for validation."""
|
|
365
|
+
return {
|
|
366
|
+
"type": "object",
|
|
367
|
+
"properties": {
|
|
368
|
+
"id": {"type": "string", "pattern": r"^[a-f0-9\-]{36}$"},
|
|
369
|
+
"name": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
370
|
+
"description": {"type": "string", "maxLength": 500},
|
|
371
|
+
"category": {
|
|
372
|
+
"type": "string",
|
|
373
|
+
"enum": [cat.value for cat in PersonaCategory],
|
|
374
|
+
},
|
|
375
|
+
"base_template_id": {"type": ["string", "null"]},
|
|
376
|
+
"model_settings": {
|
|
377
|
+
"type": "object",
|
|
378
|
+
"properties": {
|
|
379
|
+
"provider_type": {
|
|
380
|
+
"type": "string",
|
|
381
|
+
"enum": [pt.value for pt in ProviderType],
|
|
382
|
+
},
|
|
383
|
+
"model_name": {"type": "string", "minLength": 1},
|
|
384
|
+
"temperature": {
|
|
385
|
+
"type": "number",
|
|
386
|
+
"minimum": 0.0,
|
|
387
|
+
"maximum": 2.0,
|
|
388
|
+
},
|
|
389
|
+
"max_tokens": {
|
|
390
|
+
"type": ["integer", "null"],
|
|
391
|
+
"minimum": 1,
|
|
392
|
+
},
|
|
393
|
+
"top_p": {
|
|
394
|
+
"type": ["number", "null"],
|
|
395
|
+
"minimum": 0.0,
|
|
396
|
+
"maximum": 1.0,
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
"required": ["provider_type", "model_name", "temperature"],
|
|
400
|
+
"additionalProperties": True,
|
|
401
|
+
},
|
|
402
|
+
"context_parameters": {
|
|
403
|
+
"type": "object",
|
|
404
|
+
"properties": {
|
|
405
|
+
"context_window_size": {
|
|
406
|
+
"type": "integer",
|
|
407
|
+
"minimum": 1000,
|
|
408
|
+
},
|
|
409
|
+
"memory_limit": {"type": "integer", "minimum": 1},
|
|
410
|
+
"system_prompt": {"type": "string"},
|
|
411
|
+
"response_format": {
|
|
412
|
+
"type": "string",
|
|
413
|
+
"enum": ["default", "json", "markdown"],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
"additionalProperties": True,
|
|
417
|
+
},
|
|
418
|
+
"capabilities": {
|
|
419
|
+
"type": "array",
|
|
420
|
+
"items": {
|
|
421
|
+
"type": "string",
|
|
422
|
+
"enum": [cap.value for cap in PersonaCapability],
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
"enabled_tools": {
|
|
426
|
+
"type": "array",
|
|
427
|
+
"items": {
|
|
428
|
+
"type": "string",
|
|
429
|
+
"enum": [tool.value for tool in AgentTool],
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
"status": {
|
|
433
|
+
"type": "string",
|
|
434
|
+
"enum": [status.value for status in CustomAgentStatus],
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
"required": [
|
|
438
|
+
"id",
|
|
439
|
+
"name",
|
|
440
|
+
"model_settings",
|
|
441
|
+
"capabilities",
|
|
442
|
+
"enabled_tools",
|
|
443
|
+
],
|
|
444
|
+
"additionalProperties": True,
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
def validate(self, config_data: Dict[str, Any]) -> None:
|
|
448
|
+
"""Validate configuration data."""
|
|
449
|
+
try:
|
|
450
|
+
validate(instance=config_data, schema=self.schema)
|
|
451
|
+
except ValidationError as e:
|
|
452
|
+
raise ValueError(f"Invalid agent configuration: {e.message}")
|
|
453
|
+
|
|
454
|
+
def validate_config(self, config: CustomAgentConfig) -> None:
|
|
455
|
+
"""Validate a CustomAgentConfig instance."""
|
|
456
|
+
config_dict = config.to_dict()
|
|
457
|
+
self.validate(config_dict)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class AgentRepository:
|
|
461
|
+
"""Repository for CRUD operations on custom agent configurations."""
|
|
462
|
+
|
|
463
|
+
def __init__(self, storage_dir: Optional[Path] = None):
|
|
464
|
+
"""
|
|
465
|
+
Initialize repository.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
storage_dir: Directory to store agent configs (defaults to ~/.omnimancer/custom_agents)
|
|
469
|
+
"""
|
|
470
|
+
self.storage_dir = storage_dir or (
|
|
471
|
+
Path.home() / ".omnimancer" / "custom_agents"
|
|
472
|
+
)
|
|
473
|
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
474
|
+
|
|
475
|
+
self.validator = CustomAgentValidator()
|
|
476
|
+
self._cache: Dict[str, CustomAgentConfig] = {}
|
|
477
|
+
self._cache_loaded = False
|
|
478
|
+
|
|
479
|
+
def _get_config_path(self, agent_id: str) -> Path:
|
|
480
|
+
"""Get file path for agent configuration."""
|
|
481
|
+
return self.storage_dir / f"{agent_id}.json"
|
|
482
|
+
|
|
483
|
+
def _load_cache(self) -> None:
|
|
484
|
+
"""Load all configurations into cache."""
|
|
485
|
+
if self._cache_loaded:
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
self._cache.clear()
|
|
489
|
+
|
|
490
|
+
for config_file in self.storage_dir.glob("*.json"):
|
|
491
|
+
try:
|
|
492
|
+
agent_config = self._load_from_file(config_file)
|
|
493
|
+
self._cache[agent_config.id] = agent_config
|
|
494
|
+
except Exception as e:
|
|
495
|
+
logger.warning(f"Failed to load agent config from {config_file}: {e}")
|
|
496
|
+
|
|
497
|
+
self._cache_loaded = True
|
|
498
|
+
|
|
499
|
+
def _load_from_file(self, file_path: Path) -> CustomAgentConfig:
|
|
500
|
+
"""Load configuration from file."""
|
|
501
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
502
|
+
data = json.load(f)
|
|
503
|
+
|
|
504
|
+
# Validate data structure
|
|
505
|
+
self.validator.validate(data)
|
|
506
|
+
|
|
507
|
+
return CustomAgentConfig.from_dict(data)
|
|
508
|
+
|
|
509
|
+
def _save_to_file(self, config: CustomAgentConfig) -> None:
|
|
510
|
+
"""Save configuration to file."""
|
|
511
|
+
# Validate before saving
|
|
512
|
+
self.validator.validate_config(config)
|
|
513
|
+
|
|
514
|
+
config_path = self._get_config_path(config.id)
|
|
515
|
+
config_data = config.to_dict()
|
|
516
|
+
|
|
517
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
518
|
+
json.dump(config_data, f, indent=2, ensure_ascii=False)
|
|
519
|
+
|
|
520
|
+
# Update cache
|
|
521
|
+
self._cache[config.id] = config
|
|
522
|
+
|
|
523
|
+
def create(self, config: CustomAgentConfig) -> CustomAgentConfig:
|
|
524
|
+
"""Create a new agent configuration."""
|
|
525
|
+
self._load_cache()
|
|
526
|
+
|
|
527
|
+
if config.id in self._cache:
|
|
528
|
+
raise ValueError(f"Agent with ID {config.id} already exists")
|
|
529
|
+
|
|
530
|
+
# Ensure timestamps are set
|
|
531
|
+
config.created_at = datetime.now()
|
|
532
|
+
config.updated_at = datetime.now()
|
|
533
|
+
config.metadata.created_at = config.created_at
|
|
534
|
+
config.metadata.updated_at = config.updated_at
|
|
535
|
+
|
|
536
|
+
self._save_to_file(config)
|
|
537
|
+
logger.info(f"Created custom agent: {config.name} ({config.id})")
|
|
538
|
+
|
|
539
|
+
return config
|
|
540
|
+
|
|
541
|
+
def get(self, agent_id: str) -> Optional[CustomAgentConfig]:
|
|
542
|
+
"""Get agent configuration by ID."""
|
|
543
|
+
self._load_cache()
|
|
544
|
+
return self._cache.get(agent_id)
|
|
545
|
+
|
|
546
|
+
def get_by_name(self, name: str) -> Optional[CustomAgentConfig]:
|
|
547
|
+
"""Get agent configuration by name."""
|
|
548
|
+
self._load_cache()
|
|
549
|
+
for config in self._cache.values():
|
|
550
|
+
if config.name == name:
|
|
551
|
+
return config
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
def list_all(self) -> List[CustomAgentConfig]:
|
|
555
|
+
"""Get all agent configurations."""
|
|
556
|
+
self._load_cache()
|
|
557
|
+
return list(self._cache.values())
|
|
558
|
+
|
|
559
|
+
def list_by_category(self, category: PersonaCategory) -> List[CustomAgentConfig]:
|
|
560
|
+
"""Get agent configurations by category."""
|
|
561
|
+
self._load_cache()
|
|
562
|
+
return [
|
|
563
|
+
config for config in self._cache.values() if config.category == category
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
def list_by_status(self, status: CustomAgentStatus) -> List[CustomAgentConfig]:
|
|
567
|
+
"""Get agent configurations by status."""
|
|
568
|
+
self._load_cache()
|
|
569
|
+
return [config for config in self._cache.values() if config.status == status]
|
|
570
|
+
|
|
571
|
+
def update(self, config: CustomAgentConfig) -> CustomAgentConfig:
|
|
572
|
+
"""Update existing agent configuration."""
|
|
573
|
+
self._load_cache()
|
|
574
|
+
|
|
575
|
+
if config.id not in self._cache:
|
|
576
|
+
raise ValueError(f"Agent with ID {config.id} does not exist")
|
|
577
|
+
|
|
578
|
+
config.update_timestamp()
|
|
579
|
+
self._save_to_file(config)
|
|
580
|
+
logger.info(f"Updated custom agent: {config.name} ({config.id})")
|
|
581
|
+
|
|
582
|
+
return config
|
|
583
|
+
|
|
584
|
+
def delete(self, agent_id: str) -> bool:
|
|
585
|
+
"""Delete agent configuration."""
|
|
586
|
+
self._load_cache()
|
|
587
|
+
|
|
588
|
+
if agent_id not in self._cache:
|
|
589
|
+
return False
|
|
590
|
+
|
|
591
|
+
config_path = self._get_config_path(agent_id)
|
|
592
|
+
if config_path.exists():
|
|
593
|
+
config_path.unlink()
|
|
594
|
+
|
|
595
|
+
config = self._cache.pop(agent_id)
|
|
596
|
+
logger.info(f"Deleted custom agent: {config.name} ({agent_id})")
|
|
597
|
+
|
|
598
|
+
return True
|
|
599
|
+
|
|
600
|
+
def search(
|
|
601
|
+
self, query: str, fields: Optional[List[str]] = None
|
|
602
|
+
) -> List[CustomAgentConfig]:
|
|
603
|
+
"""Search agent configurations by query."""
|
|
604
|
+
self._load_cache()
|
|
605
|
+
|
|
606
|
+
if not fields:
|
|
607
|
+
fields = ["name", "description"]
|
|
608
|
+
|
|
609
|
+
query_lower = query.lower()
|
|
610
|
+
results = []
|
|
611
|
+
|
|
612
|
+
for config in self._cache.values():
|
|
613
|
+
match = False
|
|
614
|
+
|
|
615
|
+
if "name" in fields and query_lower in config.name.lower():
|
|
616
|
+
match = True
|
|
617
|
+
if "description" in fields and query_lower in config.description.lower():
|
|
618
|
+
match = True
|
|
619
|
+
if "capabilities" in fields:
|
|
620
|
+
for cap in config.capabilities:
|
|
621
|
+
if query_lower in cap.value.lower():
|
|
622
|
+
match = True
|
|
623
|
+
break
|
|
624
|
+
|
|
625
|
+
if match:
|
|
626
|
+
results.append(config)
|
|
627
|
+
|
|
628
|
+
return results
|
|
629
|
+
|
|
630
|
+
def export_config(self, agent_id: str, export_path: Path) -> bool:
|
|
631
|
+
"""Export agent configuration to file."""
|
|
632
|
+
config = self.get(agent_id)
|
|
633
|
+
if not config:
|
|
634
|
+
return False
|
|
635
|
+
|
|
636
|
+
export_data = config.to_dict()
|
|
637
|
+
|
|
638
|
+
with open(export_path, "w", encoding="utf-8") as f:
|
|
639
|
+
json.dump(export_data, f, indent=2, ensure_ascii=False)
|
|
640
|
+
|
|
641
|
+
return True
|
|
642
|
+
|
|
643
|
+
def import_config(
|
|
644
|
+
self, import_path: Path, new_id: bool = True
|
|
645
|
+
) -> Optional[CustomAgentConfig]:
|
|
646
|
+
"""Import agent configuration from file."""
|
|
647
|
+
with open(import_path, "r", encoding="utf-8") as f:
|
|
648
|
+
data = json.load(f)
|
|
649
|
+
|
|
650
|
+
# Validate structure
|
|
651
|
+
self.validator.validate(data)
|
|
652
|
+
|
|
653
|
+
config = CustomAgentConfig.from_dict(data)
|
|
654
|
+
|
|
655
|
+
# Optionally generate new ID to avoid conflicts
|
|
656
|
+
if new_id:
|
|
657
|
+
config.id = str(uuid.uuid4())
|
|
658
|
+
config.created_at = datetime.now()
|
|
659
|
+
config.updated_at = datetime.now()
|
|
660
|
+
|
|
661
|
+
return self.create(config)
|
|
662
|
+
|
|
663
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
664
|
+
"""Get repository statistics."""
|
|
665
|
+
self._load_cache()
|
|
666
|
+
|
|
667
|
+
total_count = len(self._cache)
|
|
668
|
+
status_counts = {}
|
|
669
|
+
category_counts = {}
|
|
670
|
+
provider_counts = {}
|
|
671
|
+
|
|
672
|
+
for config in self._cache.values():
|
|
673
|
+
# Count by status
|
|
674
|
+
status = config.status.value
|
|
675
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
676
|
+
|
|
677
|
+
# Count by category
|
|
678
|
+
category = config.category.value
|
|
679
|
+
category_counts[category] = category_counts.get(category, 0) + 1
|
|
680
|
+
|
|
681
|
+
# Count by provider
|
|
682
|
+
provider = config.model_settings.provider_type.value
|
|
683
|
+
provider_counts[provider] = provider_counts.get(provider, 0) + 1
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
"total_agents": total_count,
|
|
687
|
+
"status_distribution": status_counts,
|
|
688
|
+
"category_distribution": category_counts,
|
|
689
|
+
"provider_distribution": provider_counts,
|
|
690
|
+
"storage_directory": str(self.storage_dir),
|
|
691
|
+
"last_updated": (
|
|
692
|
+
max([c.updated_at for c in self._cache.values()]).isoformat()
|
|
693
|
+
if self._cache
|
|
694
|
+
else None
|
|
695
|
+
),
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
def cleanup_orphaned_files(self) -> int:
|
|
699
|
+
"""Clean up orphaned configuration files."""
|
|
700
|
+
self._load_cache()
|
|
701
|
+
|
|
702
|
+
valid_ids = set(self._cache.keys())
|
|
703
|
+
removed_count = 0
|
|
704
|
+
|
|
705
|
+
for config_file in self.storage_dir.glob("*.json"):
|
|
706
|
+
file_id = config_file.stem
|
|
707
|
+
if file_id not in valid_ids:
|
|
708
|
+
try:
|
|
709
|
+
config_file.unlink()
|
|
710
|
+
removed_count += 1
|
|
711
|
+
logger.info(f"Removed orphaned config file: {config_file}")
|
|
712
|
+
except Exception as e:
|
|
713
|
+
logger.warning(f"Failed to remove orphaned file {config_file}: {e}")
|
|
714
|
+
|
|
715
|
+
return removed_count
|