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,672 @@
1
+ """
2
+ Persona Template Validation and Application system for Omnimancer.
3
+
4
+ This module provides robust validation and application logic for agent persona
5
+ templates with comprehensive error handling and rollback capabilities.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional, Set
12
+
13
+ from omnimancer.core.models import ConfigTemplate, ConfigTemplateManager
14
+
15
+ from ..provider_registry import ProviderRegistry
16
+ from .config import AgentConfig
17
+ from .persona import (
18
+ AgentPersona,
19
+ PersonaCapability,
20
+ PersonaCategory,
21
+ PersonaConfiguration,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ValidationSeverity(Enum):
28
+ """Severity levels for validation issues."""
29
+
30
+ INFO = "info"
31
+ WARNING = "warning"
32
+ ERROR = "error"
33
+ CRITICAL = "critical"
34
+
35
+
36
+ class ValidationCategory(Enum):
37
+ """Categories of validation issues."""
38
+
39
+ TEMPLATE_STRUCTURE = "template_structure"
40
+ REQUIRED_FIELDS = "required_fields"
41
+ MODEL_AVAILABILITY = "model_availability"
42
+ TOOL_COMPATIBILITY = "tool_compatibility"
43
+ PROVIDER_COMPATIBILITY = "provider_compatibility"
44
+ CONFIGURATION_CONSTRAINTS = "configuration_constraints"
45
+ RESOURCE_REQUIREMENTS = "resource_requirements"
46
+
47
+
48
+ @dataclass
49
+ class ValidationIssue:
50
+ """Represents a validation issue found during template validation."""
51
+
52
+ severity: ValidationSeverity
53
+ category: ValidationCategory
54
+ message: str
55
+ field_path: str = ""
56
+ suggestion: str = ""
57
+ fix_action: Optional[str] = None
58
+ details: Dict[str, Any] = field(default_factory=dict)
59
+
60
+ def to_dict(self) -> Dict[str, Any]:
61
+ """Convert to dictionary representation."""
62
+ return {
63
+ "severity": self.severity.value,
64
+ "category": self.category.value,
65
+ "message": self.message,
66
+ "field_path": self.field_path,
67
+ "suggestion": self.suggestion,
68
+ "fix_action": self.fix_action,
69
+ "details": self.details,
70
+ }
71
+
72
+
73
+ @dataclass
74
+ class ValidationResult:
75
+ """Result of template validation."""
76
+
77
+ is_valid: bool
78
+ issues: List[ValidationIssue] = field(default_factory=list)
79
+ warnings: List[ValidationIssue] = field(default_factory=list)
80
+ errors: List[ValidationIssue] = field(default_factory=list)
81
+ critical_issues: List[ValidationIssue] = field(default_factory=list)
82
+
83
+ def add_issue(self, issue: ValidationIssue) -> None:
84
+ """Add a validation issue."""
85
+ self.issues.append(issue)
86
+
87
+ if issue.severity == ValidationSeverity.WARNING:
88
+ self.warnings.append(issue)
89
+ elif issue.severity == ValidationSeverity.ERROR:
90
+ self.errors.append(issue)
91
+ self.is_valid = False
92
+ elif issue.severity == ValidationSeverity.CRITICAL:
93
+ self.critical_issues.append(issue)
94
+ self.is_valid = False
95
+
96
+ def has_blocking_issues(self) -> bool:
97
+ """Check if there are blocking issues that prevent template use."""
98
+ return len(self.errors) > 0 or len(self.critical_issues) > 0
99
+
100
+ def get_summary(self) -> str:
101
+ """Get a summary of validation results."""
102
+ if self.is_valid and not self.warnings:
103
+ return "Template validation passed with no issues."
104
+
105
+ summary_parts = []
106
+ if self.critical_issues:
107
+ summary_parts.append(f"{len(self.critical_issues)} critical issues")
108
+ if self.errors:
109
+ summary_parts.append(f"{len(self.errors)} errors")
110
+ if self.warnings:
111
+ summary_parts.append(f"{len(self.warnings)} warnings")
112
+
113
+ return f"Template validation found: {', '.join(summary_parts)}"
114
+
115
+
116
+ class PersonaValidator:
117
+ """
118
+ Validates persona templates for integrity, compatibility, and correctness.
119
+
120
+ This class provides comprehensive validation of agent persona templates,
121
+ checking everything from basic structure to provider compatibility.
122
+ """
123
+
124
+ def __init__(
125
+ self,
126
+ template_manager: Optional[ConfigTemplateManager] = None,
127
+ provider_registry: Optional[ProviderRegistry] = None,
128
+ agent_config: Optional[AgentConfig] = None,
129
+ ):
130
+ """
131
+ Initialize the validator.
132
+
133
+ Args:
134
+ template_manager: Configuration template manager
135
+ provider_registry: Provider registry for compatibility checks
136
+ agent_config: Agent configuration for validation context
137
+ """
138
+ self.template_manager = template_manager or ConfigTemplateManager()
139
+ self.provider_registry = provider_registry or ProviderRegistry()
140
+ self.agent_config = agent_config
141
+
142
+ # Required fields for different validation levels
143
+ self.required_persona_fields = {
144
+ "id",
145
+ "name",
146
+ "description",
147
+ "category",
148
+ }
149
+
150
+ self.required_configuration_fields = {
151
+ "template_id",
152
+ "primary_provider",
153
+ }
154
+
155
+ # Cache for expensive validation operations
156
+ self._provider_cache: Dict[str, bool] = {}
157
+ self._model_cache: Dict[str, bool] = {}
158
+
159
+ def validate_persona(self, persona: AgentPersona) -> ValidationResult:
160
+ """
161
+ Validate a complete persona instance.
162
+
163
+ Args:
164
+ persona: The persona to validate
165
+
166
+ Returns:
167
+ ValidationResult with all found issues
168
+ """
169
+ result = ValidationResult(is_valid=True)
170
+
171
+ # Validate basic persona structure
172
+ self._validate_persona_structure(persona, result)
173
+
174
+ # Validate persona configuration
175
+ if persona.configuration:
176
+ self._validate_persona_configuration(persona.configuration, result)
177
+
178
+ # Validate template compatibility
179
+ if persona.configuration.template_id:
180
+ self._validate_template_compatibility(
181
+ persona.configuration.template_id,
182
+ persona.configuration,
183
+ result,
184
+ )
185
+ else:
186
+ result.add_issue(
187
+ ValidationIssue(
188
+ severity=ValidationSeverity.ERROR,
189
+ category=ValidationCategory.REQUIRED_FIELDS,
190
+ message="Persona configuration is missing",
191
+ field_path="configuration",
192
+ suggestion="Ensure persona has a valid configuration object",
193
+ )
194
+ )
195
+
196
+ # Validate capabilities
197
+ self._validate_capabilities(persona.capabilities, result)
198
+
199
+ # Validate provider compatibility
200
+ if persona.configuration:
201
+ self._validate_provider_compatibility(persona.configuration, result)
202
+
203
+ return result
204
+
205
+ def validate_template(self, template_id: str) -> ValidationResult:
206
+ """
207
+ Validate a configuration template.
208
+
209
+ Args:
210
+ template_id: ID of the template to validate
211
+
212
+ Returns:
213
+ ValidationResult with all found issues
214
+ """
215
+ result = ValidationResult(is_valid=True)
216
+
217
+ try:
218
+ template = self.template_manager.get_template(template_id)
219
+ if not template:
220
+ result.add_issue(
221
+ ValidationIssue(
222
+ severity=ValidationSeverity.CRITICAL,
223
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
224
+ message=f"Template '{template_id}' not found",
225
+ suggestion="Check that the template ID is correct and the template exists",
226
+ )
227
+ )
228
+ return result
229
+
230
+ # Validate template structure
231
+ self._validate_template_structure(template, result)
232
+
233
+ # Validate provider configurations
234
+ self._validate_template_providers(template, result)
235
+
236
+ # Validate recommended models
237
+ self._validate_template_models(template, result)
238
+
239
+ except Exception as e:
240
+ result.add_issue(
241
+ ValidationIssue(
242
+ severity=ValidationSeverity.CRITICAL,
243
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
244
+ message=f"Failed to load template '{template_id}': {str(e)}",
245
+ suggestion="Check template file for syntax errors or corruption",
246
+ )
247
+ )
248
+
249
+ return result
250
+
251
+ def validate_configuration(self, config: PersonaConfiguration) -> ValidationResult:
252
+ """
253
+ Validate a persona configuration.
254
+
255
+ Args:
256
+ config: The configuration to validate
257
+
258
+ Returns:
259
+ ValidationResult with all found issues
260
+ """
261
+ result = ValidationResult(is_valid=True)
262
+ self._validate_persona_configuration(config, result)
263
+ return result
264
+
265
+ def _validate_persona_structure(
266
+ self, persona: AgentPersona, result: ValidationResult
267
+ ) -> None:
268
+ """Validate basic persona structure."""
269
+ for field in self.required_persona_fields:
270
+ if not hasattr(persona, field) or not getattr(persona, field):
271
+ result.add_issue(
272
+ ValidationIssue(
273
+ severity=ValidationSeverity.ERROR,
274
+ category=ValidationCategory.REQUIRED_FIELDS,
275
+ message=f"Required persona field '{field}' is missing or empty",
276
+ field_path=field,
277
+ suggestion=f"Provide a valid value for the '{field}' field",
278
+ )
279
+ )
280
+
281
+ # Validate ID format
282
+ if hasattr(persona, "id") and persona.id:
283
+ if not persona.id.isidentifier():
284
+ result.add_issue(
285
+ ValidationIssue(
286
+ severity=ValidationSeverity.ERROR,
287
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
288
+ message=f"Persona ID '{persona.id}' is not a valid identifier",
289
+ field_path="id",
290
+ suggestion="Use only letters, numbers, and underscores for persona IDs",
291
+ )
292
+ )
293
+
294
+ # Validate category
295
+ if hasattr(persona, "category") and persona.category:
296
+ if not isinstance(persona.category, PersonaCategory):
297
+ result.add_issue(
298
+ ValidationIssue(
299
+ severity=ValidationSeverity.WARNING,
300
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
301
+ message=f"Persona category should be a PersonaCategory enum",
302
+ field_path="category",
303
+ suggestion="Use PersonaCategory enum values",
304
+ )
305
+ )
306
+
307
+ def _validate_persona_configuration(
308
+ self, config: PersonaConfiguration, result: ValidationResult
309
+ ) -> None:
310
+ """Validate persona configuration."""
311
+ for field in self.required_configuration_fields:
312
+ if not hasattr(config, field) or not getattr(config, field):
313
+ result.add_issue(
314
+ ValidationIssue(
315
+ severity=ValidationSeverity.ERROR,
316
+ category=ValidationCategory.REQUIRED_FIELDS,
317
+ message=f"Required configuration field '{field}' is missing or empty",
318
+ field_path=f"configuration.{field}",
319
+ suggestion=f"Provide a valid value for the '{field}' field",
320
+ )
321
+ )
322
+
323
+ # Validate numeric constraints
324
+ if (
325
+ hasattr(config, "temperature_override")
326
+ and config.temperature_override is not None
327
+ ):
328
+ if not 0.0 <= config.temperature_override <= 2.0:
329
+ result.add_issue(
330
+ ValidationIssue(
331
+ severity=ValidationSeverity.ERROR,
332
+ category=ValidationCategory.CONFIGURATION_CONSTRAINTS,
333
+ message=f"Temperature override {config.temperature_override} is outside valid range (0.0-2.0)",
334
+ field_path="configuration.temperature_override",
335
+ suggestion="Set temperature between 0.0 and 2.0",
336
+ )
337
+ )
338
+
339
+ if (
340
+ hasattr(config, "max_tokens_override")
341
+ and config.max_tokens_override is not None
342
+ ):
343
+ if config.max_tokens_override <= 0:
344
+ result.add_issue(
345
+ ValidationIssue(
346
+ severity=ValidationSeverity.ERROR,
347
+ category=ValidationCategory.CONFIGURATION_CONSTRAINTS,
348
+ message=f"Max tokens override {config.max_tokens_override} must be positive",
349
+ field_path="configuration.max_tokens_override",
350
+ suggestion="Set max tokens to a positive integer",
351
+ )
352
+ )
353
+
354
+ if hasattr(config, "timeout_override") and config.timeout_override is not None:
355
+ if config.timeout_override <= 0:
356
+ result.add_issue(
357
+ ValidationIssue(
358
+ severity=ValidationSeverity.ERROR,
359
+ category=ValidationCategory.CONFIGURATION_CONSTRAINTS,
360
+ message=f"Timeout override {config.timeout_override} must be positive",
361
+ field_path="configuration.timeout_override",
362
+ suggestion="Set timeout to a positive number of seconds",
363
+ )
364
+ )
365
+
366
+ def _validate_template_compatibility(
367
+ self,
368
+ template_id: str,
369
+ config: PersonaConfiguration,
370
+ result: ValidationResult,
371
+ ) -> None:
372
+ """Validate that configuration is compatible with its template."""
373
+ try:
374
+ template = self.template_manager.get_template(template_id)
375
+ if not template:
376
+ result.add_issue(
377
+ ValidationIssue(
378
+ severity=ValidationSeverity.ERROR,
379
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
380
+ message=f"Referenced template '{template_id}' not found",
381
+ field_path="configuration.template_id",
382
+ suggestion="Check that the template ID is correct",
383
+ )
384
+ )
385
+ return
386
+
387
+ # Check if primary provider exists in template
388
+ if config.primary_provider not in template.provider_configs:
389
+ result.add_issue(
390
+ ValidationIssue(
391
+ severity=ValidationSeverity.ERROR,
392
+ category=ValidationCategory.PROVIDER_COMPATIBILITY,
393
+ message=f"Primary provider '{config.primary_provider}' not available in template '{template_id}'",
394
+ field_path="configuration.primary_provider",
395
+ suggestion=f"Use one of: {', '.join(template.provider_configs.keys())}",
396
+ )
397
+ )
398
+
399
+ # Check fallback providers
400
+ if hasattr(config, "fallback_providers") and config.fallback_providers:
401
+ for provider in config.fallback_providers:
402
+ if provider not in template.provider_configs:
403
+ result.add_issue(
404
+ ValidationIssue(
405
+ severity=ValidationSeverity.WARNING,
406
+ category=ValidationCategory.PROVIDER_COMPATIBILITY,
407
+ message=f"Fallback provider '{provider}' not available in template '{template_id}'",
408
+ field_path="configuration.fallback_providers",
409
+ suggestion=f"Remove '{provider}' or use one of: {', '.join(template.provider_configs.keys())}",
410
+ )
411
+ )
412
+
413
+ except Exception as e:
414
+ result.add_issue(
415
+ ValidationIssue(
416
+ severity=ValidationSeverity.WARNING,
417
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
418
+ message=f"Could not validate template compatibility: {str(e)}",
419
+ field_path="configuration.template_id",
420
+ )
421
+ )
422
+
423
+ def _validate_capabilities(
424
+ self, capabilities: Set[PersonaCapability], result: ValidationResult
425
+ ) -> None:
426
+ """Validate persona capabilities."""
427
+ if not capabilities:
428
+ result.add_issue(
429
+ ValidationIssue(
430
+ severity=ValidationSeverity.WARNING,
431
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
432
+ message="Persona has no defined capabilities",
433
+ field_path="capabilities",
434
+ suggestion="Define at least one capability for the persona",
435
+ )
436
+ )
437
+
438
+ # Check for conflicting capabilities
439
+ conflicting_pairs = [
440
+ (PersonaCapability.FAST_RESPONSE, PersonaCapability.LARGE_CONTEXT),
441
+ (
442
+ PersonaCapability.COST_EFFICIENT,
443
+ PersonaCapability.HIGH_TEMPERATURE,
444
+ ),
445
+ ]
446
+
447
+ for cap1, cap2 in conflicting_pairs:
448
+ if cap1 in capabilities and cap2 in capabilities:
449
+ result.add_issue(
450
+ ValidationIssue(
451
+ severity=ValidationSeverity.WARNING,
452
+ category=ValidationCategory.CONFIGURATION_CONSTRAINTS,
453
+ message=f"Conflicting capabilities: {cap1.value} and {cap2.value}",
454
+ field_path="capabilities",
455
+ suggestion=f"Consider removing one of the conflicting capabilities",
456
+ )
457
+ )
458
+
459
+ def _validate_provider_compatibility(
460
+ self, config: PersonaConfiguration, result: ValidationResult
461
+ ) -> None:
462
+ """Validate provider compatibility."""
463
+ # Check if primary provider is available
464
+ if not self._is_provider_available(config.primary_provider):
465
+ result.add_issue(
466
+ ValidationIssue(
467
+ severity=ValidationSeverity.ERROR,
468
+ category=ValidationCategory.PROVIDER_COMPATIBILITY,
469
+ message=f"Primary provider '{config.primary_provider}' is not available",
470
+ field_path="configuration.primary_provider",
471
+ suggestion="Check provider installation and API key configuration",
472
+ fix_action=f"install_{config.primary_provider}_provider",
473
+ )
474
+ )
475
+
476
+ # Check fallback providers
477
+ if hasattr(config, "fallback_providers") and config.fallback_providers:
478
+ unavailable_providers = []
479
+ for provider in config.fallback_providers:
480
+ if not self._is_provider_available(provider):
481
+ unavailable_providers.append(provider)
482
+
483
+ if unavailable_providers:
484
+ result.add_issue(
485
+ ValidationIssue(
486
+ severity=ValidationSeverity.WARNING,
487
+ category=ValidationCategory.PROVIDER_COMPATIBILITY,
488
+ message=f"Fallback providers not available: {', '.join(unavailable_providers)}",
489
+ field_path="configuration.fallback_providers",
490
+ suggestion="Remove unavailable providers or install/configure them",
491
+ details={"unavailable_providers": unavailable_providers},
492
+ )
493
+ )
494
+
495
+ def _validate_template_structure(
496
+ self, template: ConfigTemplate, result: ValidationResult
497
+ ) -> None:
498
+ """Validate template structure."""
499
+ if not template.provider_configs:
500
+ result.add_issue(
501
+ ValidationIssue(
502
+ severity=ValidationSeverity.CRITICAL,
503
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
504
+ message="Template has no provider configurations",
505
+ suggestion="Add at least one provider configuration to the template",
506
+ )
507
+ )
508
+
509
+ def _validate_template_providers(
510
+ self, template: ConfigTemplate, result: ValidationResult
511
+ ) -> None:
512
+ """Validate template provider configurations."""
513
+ for provider_id, provider_config in template.provider_configs.items():
514
+ if not isinstance(provider_config, dict):
515
+ result.add_issue(
516
+ ValidationIssue(
517
+ severity=ValidationSeverity.ERROR,
518
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
519
+ message=f"Provider config for '{provider_id}' is not a dictionary",
520
+ field_path=f"provider_configs.{provider_id}",
521
+ suggestion="Ensure provider configuration is a valid dictionary",
522
+ )
523
+ )
524
+ continue
525
+
526
+ # Check for required provider config fields
527
+ required_fields = ["temperature", "max_tokens"]
528
+ for field in required_fields:
529
+ if field not in provider_config:
530
+ result.add_issue(
531
+ ValidationIssue(
532
+ severity=ValidationSeverity.WARNING,
533
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
534
+ message=f"Provider '{provider_id}' missing recommended field '{field}'",
535
+ field_path=f"provider_configs.{provider_id}.{field}",
536
+ suggestion=f"Add '{field}' to provider configuration",
537
+ )
538
+ )
539
+
540
+ def _validate_template_models(
541
+ self, template: ConfigTemplate, result: ValidationResult
542
+ ) -> None:
543
+ """Validate template recommended models."""
544
+ if (
545
+ not hasattr(template, "recommended_models")
546
+ or not template.recommended_models
547
+ ):
548
+ result.add_issue(
549
+ ValidationIssue(
550
+ severity=ValidationSeverity.WARNING,
551
+ category=ValidationCategory.TEMPLATE_STRUCTURE,
552
+ message="Template has no recommended models",
553
+ field_path="recommended_models",
554
+ suggestion="Add recommended models for better user experience",
555
+ )
556
+ )
557
+ return
558
+
559
+ for provider, model in template.recommended_models.items():
560
+ if not self._is_model_available(provider, model):
561
+ result.add_issue(
562
+ ValidationIssue(
563
+ severity=ValidationSeverity.WARNING,
564
+ category=ValidationCategory.MODEL_AVAILABILITY,
565
+ message=f"Recommended model '{model}' for provider '{provider}' may not be available",
566
+ field_path=f"recommended_models.{provider}",
567
+ suggestion="Verify model availability or update recommendation",
568
+ )
569
+ )
570
+
571
+ def _is_provider_available(self, provider_id: str) -> bool:
572
+ """Check if a provider is available."""
573
+ if provider_id in self._provider_cache:
574
+ return self._provider_cache[provider_id]
575
+
576
+ try:
577
+ # Check if provider is registered
578
+ provider_info = self.provider_registry.get_provider_info(provider_id)
579
+ available = provider_info is not None
580
+
581
+ # Additional checks could be added here (API keys, installation, etc.)
582
+
583
+ self._provider_cache[provider_id] = available
584
+ return available
585
+ except Exception:
586
+ self._provider_cache[provider_id] = False
587
+ return False
588
+
589
+ def _is_model_available(self, provider_id: str, model_id: str) -> bool:
590
+ """Check if a model is available for a provider."""
591
+ cache_key = f"{provider_id}:{model_id}"
592
+ if cache_key in self._model_cache:
593
+ return self._model_cache[cache_key]
594
+
595
+ try:
596
+ # This would need integration with actual model availability checking
597
+ # For now, assume basic models are available
598
+ basic_models = {
599
+ "claude": [
600
+ "claude-3-sonnet-20240229",
601
+ "claude-3-haiku-20240307",
602
+ ],
603
+ "openai": ["gpt-4", "gpt-3.5-turbo"],
604
+ "gemini": ["gemini-pro"],
605
+ "perplexity": ["pplx-7b-online", "pplx-70b-online"],
606
+ }
607
+
608
+ available = (
609
+ provider_id in basic_models and model_id in basic_models[provider_id]
610
+ )
611
+
612
+ self._model_cache[cache_key] = available
613
+ return available
614
+ except Exception:
615
+ self._model_cache[cache_key] = False
616
+ return False
617
+
618
+ def clear_cache(self) -> None:
619
+ """Clear validation caches."""
620
+ self._provider_cache.clear()
621
+ self._model_cache.clear()
622
+
623
+ def get_validation_report(self, result: ValidationResult) -> str:
624
+ """Generate a formatted validation report."""
625
+ if not result.issues:
626
+ return "✅ Validation passed with no issues."
627
+
628
+ report_lines = [f"📋 Validation Report: {result.get_summary()}", ""]
629
+
630
+ if result.critical_issues:
631
+ report_lines.append("🚨 Critical Issues:")
632
+ for issue in result.critical_issues:
633
+ report_lines.append(f" • {issue.message}")
634
+ if issue.suggestion:
635
+ report_lines.append(f" 💡 {issue.suggestion}")
636
+ report_lines.append("")
637
+
638
+ if result.errors:
639
+ report_lines.append("❌ Errors:")
640
+ for issue in result.errors:
641
+ report_lines.append(f" • {issue.message}")
642
+ if issue.suggestion:
643
+ report_lines.append(f" 💡 {issue.suggestion}")
644
+ report_lines.append("")
645
+
646
+ if result.warnings:
647
+ report_lines.append("⚠️ Warnings:")
648
+ for issue in result.warnings:
649
+ report_lines.append(f" • {issue.message}")
650
+ if issue.suggestion:
651
+ report_lines.append(f" 💡 {issue.suggestion}")
652
+ report_lines.append("")
653
+
654
+ return "\n".join(report_lines)
655
+
656
+
657
+ # Global validator instance
658
+ _global_validator: Optional[PersonaValidator] = None
659
+
660
+
661
+ def get_persona_validator() -> PersonaValidator:
662
+ """Get the global persona validator instance."""
663
+ global _global_validator
664
+ if _global_validator is None:
665
+ _global_validator = PersonaValidator()
666
+ return _global_validator
667
+
668
+
669
+ def set_persona_validator(validator: PersonaValidator) -> None:
670
+ """Set the global persona validator instance."""
671
+ global _global_validator
672
+ _global_validator = validator