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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. 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