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,870 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Persona Architecture for Omnimancer.
|
|
3
|
+
|
|
4
|
+
This module provides the architectural design for mapping configuration templates
|
|
5
|
+
to agent personas with clear interfaces and data models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
14
|
+
|
|
15
|
+
from omnimancer.core.models import ConfigTemplate, ConfigTemplateManager
|
|
16
|
+
|
|
17
|
+
from .config import AgentConfig, ProviderConfig, ProviderType
|
|
18
|
+
from .status_core import AgentStatus
|
|
19
|
+
from .status_manager import UnifiedStatusManager as AgentStatusManager
|
|
20
|
+
from .status_manager import (
|
|
21
|
+
get_status_manager,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PersonaCapability(Enum):
|
|
28
|
+
"""Capabilities that agent personas can have."""
|
|
29
|
+
|
|
30
|
+
TOOL_CALLING = "tool_calling"
|
|
31
|
+
CODE_GENERATION = "code_generation"
|
|
32
|
+
FILE_OPERATIONS = "file_operations"
|
|
33
|
+
WEB_SEARCH = "web_search"
|
|
34
|
+
LARGE_CONTEXT = "large_context"
|
|
35
|
+
RESEARCH = "research"
|
|
36
|
+
CREATIVE_WRITING = "creative_writing"
|
|
37
|
+
HIGH_TEMPERATURE = "high_temperature"
|
|
38
|
+
FAST_RESPONSE = "fast_response"
|
|
39
|
+
COST_EFFICIENT = "cost_efficient"
|
|
40
|
+
BALANCED = "balanced"
|
|
41
|
+
GENERAL_PURPOSE = "general_purpose"
|
|
42
|
+
MULTIMODAL = "multimodal"
|
|
43
|
+
FUNCTION_CALLING = "function_calling"
|
|
44
|
+
REASONING = "reasoning"
|
|
45
|
+
MATH = "math"
|
|
46
|
+
ANALYSIS = "analysis"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PersonaCategory(Enum):
|
|
50
|
+
"""Categories for organizing agent personas."""
|
|
51
|
+
|
|
52
|
+
DEVELOPMENT = "development"
|
|
53
|
+
RESEARCH = "research"
|
|
54
|
+
CREATIVE = "creative"
|
|
55
|
+
PRODUCTIVITY = "productivity"
|
|
56
|
+
ANALYSIS = "analysis"
|
|
57
|
+
GENERAL = "general"
|
|
58
|
+
CUSTOM = "custom"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PersonaStatus(Enum):
|
|
62
|
+
"""Status of agent personas."""
|
|
63
|
+
|
|
64
|
+
AVAILABLE = "available"
|
|
65
|
+
ACTIVE = "active"
|
|
66
|
+
DISABLED = "disabled"
|
|
67
|
+
ERROR = "error"
|
|
68
|
+
LOADING = "loading"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class PersonaMetadata:
|
|
73
|
+
"""Metadata structure for UI display and management."""
|
|
74
|
+
|
|
75
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
76
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
77
|
+
version: str = "1.0.0"
|
|
78
|
+
author: str = "Omnimancer"
|
|
79
|
+
tags: List[str] = field(default_factory=list)
|
|
80
|
+
usage_count: int = 0
|
|
81
|
+
last_used: Optional[datetime] = None
|
|
82
|
+
rating: Optional[float] = None
|
|
83
|
+
user_notes: str = ""
|
|
84
|
+
is_builtin: bool = True
|
|
85
|
+
is_custom: bool = False
|
|
86
|
+
|
|
87
|
+
def update_usage(self) -> None:
|
|
88
|
+
"""Update usage statistics."""
|
|
89
|
+
self.usage_count += 1
|
|
90
|
+
self.last_used = datetime.now()
|
|
91
|
+
self.updated_at = datetime.now()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class PersonaConfiguration:
|
|
96
|
+
"""Configuration mapping for agent personas."""
|
|
97
|
+
|
|
98
|
+
template_id: str # Maps to ConfigTemplate
|
|
99
|
+
primary_provider: str # Primary provider from template
|
|
100
|
+
fallback_providers: List[str] = field(default_factory=list) # Fallback providers
|
|
101
|
+
|
|
102
|
+
# Override settings (optional)
|
|
103
|
+
temperature_override: Optional[float] = None
|
|
104
|
+
max_tokens_override: Optional[int] = None
|
|
105
|
+
timeout_override: Optional[int] = None
|
|
106
|
+
|
|
107
|
+
# Omnimancer-specific settings
|
|
108
|
+
tools_enabled: bool = True
|
|
109
|
+
web_search_enabled: bool = False
|
|
110
|
+
file_operations_enabled: bool = True
|
|
111
|
+
approval_required: bool = True
|
|
112
|
+
auto_approve_safe_operations: bool = False
|
|
113
|
+
|
|
114
|
+
def get_template(self, template_manager: ConfigTemplateManager) -> ConfigTemplate:
|
|
115
|
+
"""Get the underlying configuration template."""
|
|
116
|
+
return template_manager.get_template(self.template_id)
|
|
117
|
+
|
|
118
|
+
def get_primary_provider_config(
|
|
119
|
+
self, template_manager: ConfigTemplateManager
|
|
120
|
+
) -> Dict[str, Any]:
|
|
121
|
+
"""Get provider configuration for the primary provider."""
|
|
122
|
+
template = self.get_template(template_manager)
|
|
123
|
+
if self.primary_provider not in template.provider_configs:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Provider {self.primary_provider} not found in template {self.template_id}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
config = template.provider_configs[self.primary_provider].copy()
|
|
129
|
+
|
|
130
|
+
# Apply overrides
|
|
131
|
+
if self.temperature_override is not None:
|
|
132
|
+
config["temperature"] = self.temperature_override
|
|
133
|
+
if self.max_tokens_override is not None:
|
|
134
|
+
config["max_tokens"] = self.max_tokens_override
|
|
135
|
+
if self.timeout_override is not None:
|
|
136
|
+
config["timeout"] = self.timeout_override
|
|
137
|
+
|
|
138
|
+
return config
|
|
139
|
+
|
|
140
|
+
def to_provider_config(
|
|
141
|
+
self, provider_id: str, template_manager: ConfigTemplateManager
|
|
142
|
+
) -> ProviderConfig:
|
|
143
|
+
"""Convert to ProviderConfig for AgentConfig."""
|
|
144
|
+
template = self.get_template(template_manager)
|
|
145
|
+
provider_config = self.get_primary_provider_config(template_manager)
|
|
146
|
+
|
|
147
|
+
# Map provider name to ProviderType
|
|
148
|
+
provider_type_mapping = {
|
|
149
|
+
"claude": ProviderType.ANTHROPIC,
|
|
150
|
+
"openai": ProviderType.OPENAI,
|
|
151
|
+
"gemini": ProviderType.GOOGLE,
|
|
152
|
+
"perplexity": ProviderType.PERPLEXITY,
|
|
153
|
+
"ollama": ProviderType.OPENROUTER, # Map to OpenRouter as fallback
|
|
154
|
+
"xai": ProviderType.XAI,
|
|
155
|
+
"mistral": ProviderType.MISTRAL,
|
|
156
|
+
"cohere": ProviderType.OPENROUTER, # Map to OpenRouter as fallback
|
|
157
|
+
"claude-code": ProviderType.CLAUDE_CODE,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
provider_type = provider_type_mapping.get(
|
|
161
|
+
self.primary_provider, ProviderType.ANTHROPIC
|
|
162
|
+
)
|
|
163
|
+
model_id = template.recommended_models.get(self.primary_provider, "default")
|
|
164
|
+
|
|
165
|
+
return ProviderConfig(
|
|
166
|
+
provider_type=provider_type,
|
|
167
|
+
enabled=True,
|
|
168
|
+
model=model_id,
|
|
169
|
+
temperature=provider_config.get("temperature", 0.7),
|
|
170
|
+
max_tokens=provider_config.get("max_tokens", 4096),
|
|
171
|
+
timeout_seconds=provider_config.get("timeout", 30),
|
|
172
|
+
custom_headers={},
|
|
173
|
+
provider_specific=provider_config.get("provider_specific", {}),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class AgentPersona(ABC):
|
|
178
|
+
"""
|
|
179
|
+
Abstract base class for agent personas.
|
|
180
|
+
|
|
181
|
+
Defines the interface and common functionality for all agent personas.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
persona_id: str,
|
|
187
|
+
name: str,
|
|
188
|
+
description: str,
|
|
189
|
+
category: PersonaCategory = PersonaCategory.GENERAL,
|
|
190
|
+
template_manager: Optional[ConfigTemplateManager] = None,
|
|
191
|
+
):
|
|
192
|
+
"""
|
|
193
|
+
Initialize agent persona.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
persona_id: Unique identifier for the persona
|
|
197
|
+
name: Human-readable name
|
|
198
|
+
description: Description of the persona's purpose
|
|
199
|
+
category: Category for organization
|
|
200
|
+
template_manager: Configuration template manager
|
|
201
|
+
"""
|
|
202
|
+
self.id = persona_id
|
|
203
|
+
self.name = name
|
|
204
|
+
self.description = description
|
|
205
|
+
self.category = category
|
|
206
|
+
self.status = PersonaStatus.AVAILABLE
|
|
207
|
+
self.icon = self._get_default_icon()
|
|
208
|
+
self.template_manager = template_manager or ConfigTemplateManager()
|
|
209
|
+
|
|
210
|
+
# Core properties
|
|
211
|
+
self.capabilities: Set[PersonaCapability] = set()
|
|
212
|
+
self.configuration: Optional[PersonaConfiguration] = None
|
|
213
|
+
self.metadata = PersonaMetadata()
|
|
214
|
+
|
|
215
|
+
# State management
|
|
216
|
+
self._is_active = False
|
|
217
|
+
self._session_data: Dict[str, Any] = {}
|
|
218
|
+
self._event_listeners: List[Callable] = []
|
|
219
|
+
|
|
220
|
+
@abstractmethod
|
|
221
|
+
def _get_default_icon(self) -> str:
|
|
222
|
+
"""Get default icon for this persona type."""
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
@abstractmethod
|
|
226
|
+
def _setup_capabilities(self) -> Set[PersonaCapability]:
|
|
227
|
+
"""Set up persona-specific capabilities."""
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
@abstractmethod
|
|
231
|
+
def _setup_configuration(self) -> PersonaConfiguration:
|
|
232
|
+
"""Set up persona-specific configuration."""
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
def get_template(self) -> Optional[ConfigTemplate]:
|
|
236
|
+
"""Get the underlying configuration template."""
|
|
237
|
+
if not self.configuration:
|
|
238
|
+
return None
|
|
239
|
+
return self.configuration.get_template(self.template_manager)
|
|
240
|
+
|
|
241
|
+
def get_primary_provider_config(self) -> Optional[Dict[str, Any]]:
|
|
242
|
+
"""Get provider configuration for the primary provider."""
|
|
243
|
+
if not self.configuration:
|
|
244
|
+
return None
|
|
245
|
+
return self.configuration.get_primary_provider_config(self.template_manager)
|
|
246
|
+
|
|
247
|
+
def initialize(self) -> None:
|
|
248
|
+
"""Initialize the persona with capabilities and configuration."""
|
|
249
|
+
self.capabilities = self._setup_capabilities()
|
|
250
|
+
self.configuration = self._setup_configuration()
|
|
251
|
+
self.status = PersonaStatus.AVAILABLE
|
|
252
|
+
|
|
253
|
+
def activate(self) -> bool:
|
|
254
|
+
"""
|
|
255
|
+
Activate the persona.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
True if activation was successful
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
self.status = PersonaStatus.LOADING
|
|
262
|
+
self._is_active = True
|
|
263
|
+
self.metadata.update_usage()
|
|
264
|
+
self.status = PersonaStatus.ACTIVE
|
|
265
|
+
|
|
266
|
+
# Notify listeners
|
|
267
|
+
for listener in self._event_listeners:
|
|
268
|
+
try:
|
|
269
|
+
listener("persona_activated", {"persona_id": self.id})
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"Error in persona event listener: {e}")
|
|
272
|
+
|
|
273
|
+
return True
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Failed to activate persona {self.id}: {e}")
|
|
276
|
+
self.status = PersonaStatus.ERROR
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
def deactivate(self) -> bool:
|
|
280
|
+
"""
|
|
281
|
+
Deactivate the persona.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
True if deactivation was successful
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
self._is_active = False
|
|
288
|
+
self.status = PersonaStatus.AVAILABLE
|
|
289
|
+
self._session_data.clear()
|
|
290
|
+
|
|
291
|
+
# Notify listeners
|
|
292
|
+
for listener in self._event_listeners:
|
|
293
|
+
try:
|
|
294
|
+
listener("persona_deactivated", {"persona_id": self.id})
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Error in persona event listener: {e}")
|
|
297
|
+
|
|
298
|
+
return True
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"Failed to deactivate persona {self.id}: {e}")
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def is_active(self) -> bool:
|
|
305
|
+
"""Check if persona is currently active."""
|
|
306
|
+
return self._is_active and self.status == PersonaStatus.ACTIVE
|
|
307
|
+
|
|
308
|
+
def get_session_data(self, key: str, default: Any = None) -> Any:
|
|
309
|
+
"""Get session-specific data."""
|
|
310
|
+
return self._session_data.get(key, default)
|
|
311
|
+
|
|
312
|
+
def set_session_data(self, key: str, value: Any) -> None:
|
|
313
|
+
"""Set session-specific data."""
|
|
314
|
+
self._session_data[key] = value
|
|
315
|
+
|
|
316
|
+
def add_event_listener(self, listener: Callable) -> None:
|
|
317
|
+
"""Add event listener for persona events."""
|
|
318
|
+
self._event_listeners.append(listener)
|
|
319
|
+
|
|
320
|
+
def remove_event_listener(self, listener: Callable) -> None:
|
|
321
|
+
"""Remove event listener."""
|
|
322
|
+
if listener in self._event_listeners:
|
|
323
|
+
self._event_listeners.remove(listener)
|
|
324
|
+
|
|
325
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
326
|
+
"""Convert persona to dictionary representation."""
|
|
327
|
+
return {
|
|
328
|
+
"id": self.id,
|
|
329
|
+
"name": self.name,
|
|
330
|
+
"description": self.description,
|
|
331
|
+
"category": self.category.value,
|
|
332
|
+
"status": self.status.value,
|
|
333
|
+
"icon": self.icon,
|
|
334
|
+
"capabilities": [cap.value for cap in self.capabilities],
|
|
335
|
+
"configuration": asdict(self.configuration),
|
|
336
|
+
"metadata": asdict(self.metadata),
|
|
337
|
+
"is_active": self.is_active,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@classmethod
|
|
341
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AgentPersona":
|
|
342
|
+
"""Create persona from dictionary representation."""
|
|
343
|
+
# This would be implemented by specific persona types
|
|
344
|
+
raise NotImplementedError("Subclasses must implement from_dict")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class CodingPersona(AgentPersona):
|
|
348
|
+
"""Agent persona optimized for software development."""
|
|
349
|
+
|
|
350
|
+
def __init__(self, template_manager: Optional[ConfigTemplateManager] = None):
|
|
351
|
+
super().__init__(
|
|
352
|
+
persona_id="coding",
|
|
353
|
+
name="Coding Agent",
|
|
354
|
+
description="Optimized for software development with code-capable models and dev tools",
|
|
355
|
+
category=PersonaCategory.DEVELOPMENT,
|
|
356
|
+
template_manager=template_manager,
|
|
357
|
+
)
|
|
358
|
+
self.initialize()
|
|
359
|
+
|
|
360
|
+
def _get_default_icon(self) -> str:
|
|
361
|
+
return "💻"
|
|
362
|
+
|
|
363
|
+
def _setup_capabilities(self) -> Set[PersonaCapability]:
|
|
364
|
+
return {
|
|
365
|
+
PersonaCapability.TOOL_CALLING,
|
|
366
|
+
PersonaCapability.CODE_GENERATION,
|
|
367
|
+
PersonaCapability.FILE_OPERATIONS,
|
|
368
|
+
PersonaCapability.FUNCTION_CALLING,
|
|
369
|
+
PersonaCapability.REASONING,
|
|
370
|
+
PersonaCapability.ANALYSIS,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
def _setup_configuration(self) -> PersonaConfiguration:
|
|
374
|
+
return PersonaConfiguration(
|
|
375
|
+
template_id="coding",
|
|
376
|
+
primary_provider="claude",
|
|
377
|
+
fallback_providers=[
|
|
378
|
+
"openai",
|
|
379
|
+
"perplexity",
|
|
380
|
+
"claude-code",
|
|
381
|
+
"ollama",
|
|
382
|
+
],
|
|
383
|
+
tools_enabled=True,
|
|
384
|
+
file_operations_enabled=True,
|
|
385
|
+
approval_required=True,
|
|
386
|
+
auto_approve_safe_operations=True,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class ResearchPersona(AgentPersona):
|
|
391
|
+
"""Agent persona configured for research with web search capabilities."""
|
|
392
|
+
|
|
393
|
+
def __init__(self, template_manager: Optional[ConfigTemplateManager] = None):
|
|
394
|
+
super().__init__(
|
|
395
|
+
persona_id="research",
|
|
396
|
+
name="Research Agent",
|
|
397
|
+
description="Configured for research with web search and large context models",
|
|
398
|
+
category=PersonaCategory.RESEARCH,
|
|
399
|
+
template_manager=template_manager,
|
|
400
|
+
)
|
|
401
|
+
self.initialize()
|
|
402
|
+
|
|
403
|
+
def _get_default_icon(self) -> str:
|
|
404
|
+
return "🔍"
|
|
405
|
+
|
|
406
|
+
def _setup_capabilities(self) -> Set[PersonaCapability]:
|
|
407
|
+
return {
|
|
408
|
+
PersonaCapability.WEB_SEARCH,
|
|
409
|
+
PersonaCapability.LARGE_CONTEXT,
|
|
410
|
+
PersonaCapability.RESEARCH,
|
|
411
|
+
PersonaCapability.ANALYSIS,
|
|
412
|
+
PersonaCapability.REASONING,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
def _setup_configuration(self) -> PersonaConfiguration:
|
|
416
|
+
return PersonaConfiguration(
|
|
417
|
+
template_id="research",
|
|
418
|
+
primary_provider="perplexity",
|
|
419
|
+
fallback_providers=["claude", "gemini", "openai", "xai"],
|
|
420
|
+
web_search_enabled=True,
|
|
421
|
+
tools_enabled=True,
|
|
422
|
+
file_operations_enabled=True,
|
|
423
|
+
approval_required=True,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class CreativePersona(AgentPersona):
|
|
428
|
+
"""Agent persona set up for creative writing."""
|
|
429
|
+
|
|
430
|
+
def __init__(self, template_manager: Optional[ConfigTemplateManager] = None):
|
|
431
|
+
super().__init__(
|
|
432
|
+
persona_id="creative",
|
|
433
|
+
name="Creative Agent",
|
|
434
|
+
description="Set up for creative writing with high-temperature creative models",
|
|
435
|
+
category=PersonaCategory.CREATIVE,
|
|
436
|
+
template_manager=template_manager,
|
|
437
|
+
)
|
|
438
|
+
self.initialize()
|
|
439
|
+
|
|
440
|
+
def _get_default_icon(self) -> str:
|
|
441
|
+
return "🎨"
|
|
442
|
+
|
|
443
|
+
def _setup_capabilities(self) -> Set[PersonaCapability]:
|
|
444
|
+
return {
|
|
445
|
+
PersonaCapability.CREATIVE_WRITING,
|
|
446
|
+
PersonaCapability.HIGH_TEMPERATURE,
|
|
447
|
+
PersonaCapability.LARGE_CONTEXT,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
def _setup_configuration(self) -> PersonaConfiguration:
|
|
451
|
+
return PersonaConfiguration(
|
|
452
|
+
template_id="creative",
|
|
453
|
+
primary_provider="claude",
|
|
454
|
+
fallback_providers=["openai", "mistral", "cohere", "gemini"],
|
|
455
|
+
tools_enabled=False,
|
|
456
|
+
web_search_enabled=False,
|
|
457
|
+
file_operations_enabled=True,
|
|
458
|
+
approval_required=False,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class PerformancePersona(AgentPersona):
|
|
463
|
+
"""Agent persona optimized for speed and cost efficiency."""
|
|
464
|
+
|
|
465
|
+
def __init__(self, template_manager: Optional[ConfigTemplateManager] = None):
|
|
466
|
+
super().__init__(
|
|
467
|
+
persona_id="performance",
|
|
468
|
+
name="Performance Agent",
|
|
469
|
+
description="Optimized for speed and cost efficiency",
|
|
470
|
+
category=PersonaCategory.PRODUCTIVITY,
|
|
471
|
+
template_manager=template_manager,
|
|
472
|
+
)
|
|
473
|
+
self.initialize()
|
|
474
|
+
|
|
475
|
+
def _get_default_icon(self) -> str:
|
|
476
|
+
return "⚡"
|
|
477
|
+
|
|
478
|
+
def _setup_capabilities(self) -> Set[PersonaCapability]:
|
|
479
|
+
return {
|
|
480
|
+
PersonaCapability.FAST_RESPONSE,
|
|
481
|
+
PersonaCapability.COST_EFFICIENT,
|
|
482
|
+
PersonaCapability.TOOL_CALLING,
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
def _setup_configuration(self) -> PersonaConfiguration:
|
|
486
|
+
return PersonaConfiguration(
|
|
487
|
+
template_id="performance",
|
|
488
|
+
primary_provider="openai",
|
|
489
|
+
fallback_providers=["claude", "gemini", "ollama", "claude-code"],
|
|
490
|
+
timeout_override=15,
|
|
491
|
+
tools_enabled=True,
|
|
492
|
+
web_search_enabled=False,
|
|
493
|
+
file_operations_enabled=True,
|
|
494
|
+
approval_required=True,
|
|
495
|
+
auto_approve_safe_operations=True,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class GeneralPersona(AgentPersona):
|
|
500
|
+
"""Agent persona with balanced configuration for general-purpose use."""
|
|
501
|
+
|
|
502
|
+
def __init__(self, template_manager: Optional[ConfigTemplateManager] = None):
|
|
503
|
+
super().__init__(
|
|
504
|
+
persona_id="general",
|
|
505
|
+
name="General Agent",
|
|
506
|
+
description="Balanced configuration for general-purpose use",
|
|
507
|
+
category=PersonaCategory.GENERAL,
|
|
508
|
+
template_manager=template_manager,
|
|
509
|
+
)
|
|
510
|
+
self.initialize()
|
|
511
|
+
|
|
512
|
+
def _get_default_icon(self) -> str:
|
|
513
|
+
return "🤖"
|
|
514
|
+
|
|
515
|
+
def _setup_capabilities(self) -> Set[PersonaCapability]:
|
|
516
|
+
return {
|
|
517
|
+
PersonaCapability.BALANCED,
|
|
518
|
+
PersonaCapability.GENERAL_PURPOSE,
|
|
519
|
+
PersonaCapability.TOOL_CALLING,
|
|
520
|
+
PersonaCapability.REASONING,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
def _setup_configuration(self) -> PersonaConfiguration:
|
|
524
|
+
return PersonaConfiguration(
|
|
525
|
+
template_id="general",
|
|
526
|
+
primary_provider="claude",
|
|
527
|
+
fallback_providers=["openai", "gemini", "perplexity", "ollama"],
|
|
528
|
+
tools_enabled=True,
|
|
529
|
+
web_search_enabled=False,
|
|
530
|
+
file_operations_enabled=True,
|
|
531
|
+
approval_required=True,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class PersonaManager:
|
|
536
|
+
"""
|
|
537
|
+
Manages agent personas and their lifecycle.
|
|
538
|
+
|
|
539
|
+
Handles persona registration, activation, deactivation, and integration
|
|
540
|
+
with the existing configuration system.
|
|
541
|
+
"""
|
|
542
|
+
|
|
543
|
+
def __init__(
|
|
544
|
+
self,
|
|
545
|
+
agent_config: Optional[AgentConfig] = None,
|
|
546
|
+
status_manager: Optional[AgentStatusManager] = None,
|
|
547
|
+
template_manager: Optional[ConfigTemplateManager] = None,
|
|
548
|
+
):
|
|
549
|
+
"""
|
|
550
|
+
Initialize persona manager.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
agent_config: Agent configuration instance
|
|
554
|
+
status_manager: Agent status manager for tracking
|
|
555
|
+
template_manager: Configuration template manager
|
|
556
|
+
"""
|
|
557
|
+
self.agent_config = agent_config
|
|
558
|
+
self.status_manager = status_manager or get_status_manager()
|
|
559
|
+
self.template_manager = template_manager or ConfigTemplateManager()
|
|
560
|
+
|
|
561
|
+
# Persona storage
|
|
562
|
+
self.personas: Dict[str, AgentPersona] = {}
|
|
563
|
+
self.active_persona: Optional[AgentPersona] = None
|
|
564
|
+
|
|
565
|
+
# Built-in personas
|
|
566
|
+
self._builtin_personas = {
|
|
567
|
+
"coding": CodingPersona,
|
|
568
|
+
"research": ResearchPersona,
|
|
569
|
+
"creative": CreativePersona,
|
|
570
|
+
"performance": PerformancePersona,
|
|
571
|
+
"general": GeneralPersona,
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# Custom personas (loaded from configuration)
|
|
575
|
+
self.custom_personas: Dict[str, Dict[str, Any]] = {}
|
|
576
|
+
|
|
577
|
+
# Event listeners
|
|
578
|
+
self._event_listeners: List[Callable] = []
|
|
579
|
+
|
|
580
|
+
# Initialize built-in personas
|
|
581
|
+
self._initialize_builtin_personas()
|
|
582
|
+
|
|
583
|
+
def _initialize_builtin_personas(self) -> None:
|
|
584
|
+
"""Initialize all built-in personas."""
|
|
585
|
+
for persona_id, persona_class in self._builtin_personas.items():
|
|
586
|
+
try:
|
|
587
|
+
persona = persona_class(template_manager=self.template_manager)
|
|
588
|
+
self.personas[persona_id] = persona
|
|
589
|
+
persona.add_event_listener(self._handle_persona_event)
|
|
590
|
+
logger.debug(f"Initialized built-in persona: {persona_id}")
|
|
591
|
+
except Exception as e:
|
|
592
|
+
logger.error(f"Failed to initialize persona {persona_id}: {e}")
|
|
593
|
+
|
|
594
|
+
def _handle_persona_event(self, event_type: str, data: Dict[str, Any]) -> None:
|
|
595
|
+
"""Handle persona events."""
|
|
596
|
+
for listener in self._event_listeners:
|
|
597
|
+
try:
|
|
598
|
+
listener(event_type, data)
|
|
599
|
+
except Exception as e:
|
|
600
|
+
logger.error(f"Error in persona manager event listener: {e}")
|
|
601
|
+
|
|
602
|
+
def get_persona(self, persona_id: str) -> Optional[AgentPersona]:
|
|
603
|
+
"""Get persona by ID."""
|
|
604
|
+
return self.personas.get(persona_id)
|
|
605
|
+
|
|
606
|
+
def get_all_personas(self) -> Dict[str, AgentPersona]:
|
|
607
|
+
"""Get all registered personas."""
|
|
608
|
+
return self.personas.copy()
|
|
609
|
+
|
|
610
|
+
def get_personas_by_category(self, category: PersonaCategory) -> List[AgentPersona]:
|
|
611
|
+
"""Get personas filtered by category."""
|
|
612
|
+
return [p for p in self.personas.values() if p.category == category]
|
|
613
|
+
|
|
614
|
+
def get_available_personas(self) -> List[AgentPersona]:
|
|
615
|
+
"""Get personas that are available for activation."""
|
|
616
|
+
return [
|
|
617
|
+
p
|
|
618
|
+
for p in self.personas.values()
|
|
619
|
+
if p.status in [PersonaStatus.AVAILABLE, PersonaStatus.ACTIVE]
|
|
620
|
+
]
|
|
621
|
+
|
|
622
|
+
def activate_persona(self, persona_id: str) -> bool:
|
|
623
|
+
"""
|
|
624
|
+
Activate a persona.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
persona_id: ID of persona to activate
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
True if activation was successful
|
|
631
|
+
"""
|
|
632
|
+
persona = self.personas.get(persona_id)
|
|
633
|
+
if not persona:
|
|
634
|
+
logger.error(f"Persona not found: {persona_id}")
|
|
635
|
+
return False
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
# Deactivate current persona if any
|
|
639
|
+
if self.active_persona and self.active_persona != persona:
|
|
640
|
+
self.deactivate_persona()
|
|
641
|
+
|
|
642
|
+
# Activate new persona
|
|
643
|
+
if persona.activate():
|
|
644
|
+
self.active_persona = persona
|
|
645
|
+
|
|
646
|
+
# Update agent configuration if available
|
|
647
|
+
if self.agent_config:
|
|
648
|
+
self._apply_persona_configuration(persona)
|
|
649
|
+
|
|
650
|
+
# Update status manager
|
|
651
|
+
if self.status_manager:
|
|
652
|
+
try:
|
|
653
|
+
import asyncio
|
|
654
|
+
|
|
655
|
+
# Try to get running loop, if not available, just skip status update
|
|
656
|
+
loop = asyncio.get_running_loop()
|
|
657
|
+
loop.create_task(
|
|
658
|
+
self.status_manager.set_agent_status(
|
|
659
|
+
agent_id=persona_id,
|
|
660
|
+
status=AgentStatus.ENABLED,
|
|
661
|
+
metadata={"persona_name": persona.name},
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
except RuntimeError:
|
|
665
|
+
# No event loop running, skip async status update
|
|
666
|
+
logger.debug(
|
|
667
|
+
f"No event loop available for status update of persona {persona_id}"
|
|
668
|
+
)
|
|
669
|
+
pass
|
|
670
|
+
|
|
671
|
+
logger.info(f"Activated persona: {persona_id} ({persona.name})")
|
|
672
|
+
return True
|
|
673
|
+
|
|
674
|
+
return False
|
|
675
|
+
except Exception as e:
|
|
676
|
+
logger.error(f"Failed to activate persona {persona_id}: {e}")
|
|
677
|
+
return False
|
|
678
|
+
|
|
679
|
+
def deactivate_persona(self) -> bool:
|
|
680
|
+
"""
|
|
681
|
+
Deactivate the currently active persona.
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
True if deactivation was successful
|
|
685
|
+
"""
|
|
686
|
+
if not self.active_persona:
|
|
687
|
+
return True
|
|
688
|
+
|
|
689
|
+
try:
|
|
690
|
+
persona = self.active_persona
|
|
691
|
+
if persona.deactivate():
|
|
692
|
+
self.active_persona = None
|
|
693
|
+
|
|
694
|
+
# Update status manager
|
|
695
|
+
if self.status_manager:
|
|
696
|
+
try:
|
|
697
|
+
import asyncio
|
|
698
|
+
|
|
699
|
+
# Try to get running loop, if not available, just skip status update
|
|
700
|
+
loop = asyncio.get_running_loop()
|
|
701
|
+
loop.create_task(
|
|
702
|
+
self.status_manager.set_agent_status(
|
|
703
|
+
agent_id=persona.id,
|
|
704
|
+
status=AgentStatus.DISABLED,
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
except RuntimeError:
|
|
708
|
+
# No event loop running, skip async status update
|
|
709
|
+
logger.debug(
|
|
710
|
+
f"No event loop available for status update of persona {persona.id}"
|
|
711
|
+
)
|
|
712
|
+
pass
|
|
713
|
+
|
|
714
|
+
logger.info(f"Deactivated persona: {persona.id}")
|
|
715
|
+
return True
|
|
716
|
+
|
|
717
|
+
return False
|
|
718
|
+
except Exception as e:
|
|
719
|
+
logger.error(f"Failed to deactivate persona: {e}")
|
|
720
|
+
return False
|
|
721
|
+
|
|
722
|
+
def _apply_persona_configuration(self, persona: AgentPersona) -> None:
|
|
723
|
+
"""Apply persona configuration to agent config."""
|
|
724
|
+
if not self.agent_config or not persona.configuration:
|
|
725
|
+
return
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
# Convert persona configuration to provider config
|
|
729
|
+
provider_config = persona.configuration.to_provider_config(
|
|
730
|
+
persona.id, self.template_manager
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# Add to agent configuration
|
|
734
|
+
self.agent_config.add_provider_config(
|
|
735
|
+
f"persona_{persona.id}", provider_config
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
logger.debug(f"Applied configuration for persona: {persona.id}")
|
|
739
|
+
except Exception as e:
|
|
740
|
+
logger.error(f"Failed to apply persona configuration: {e}")
|
|
741
|
+
|
|
742
|
+
def register_custom_persona(self, persona_data: Dict[str, Any]) -> bool:
|
|
743
|
+
"""
|
|
744
|
+
Register a custom persona from configuration data.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
persona_data: Persona configuration dictionary
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
True if registration was successful
|
|
751
|
+
"""
|
|
752
|
+
try:
|
|
753
|
+
# Validate required fields
|
|
754
|
+
required_fields = ["id", "name", "description", "configuration"]
|
|
755
|
+
for field in required_fields:
|
|
756
|
+
if field not in persona_data:
|
|
757
|
+
raise ValueError(f"Missing required field: {field}")
|
|
758
|
+
|
|
759
|
+
# Create custom persona (this would need a CustomPersona class)
|
|
760
|
+
# For now, we'll store the data for later implementation
|
|
761
|
+
persona_id = persona_data["id"]
|
|
762
|
+
self.custom_personas[persona_id] = persona_data
|
|
763
|
+
|
|
764
|
+
logger.info(f"Registered custom persona: {persona_id}")
|
|
765
|
+
return True
|
|
766
|
+
except Exception as e:
|
|
767
|
+
logger.error(f"Failed to register custom persona: {e}")
|
|
768
|
+
return False
|
|
769
|
+
|
|
770
|
+
def get_persona_recommendations(self, context: str) -> List[AgentPersona]:
|
|
771
|
+
"""
|
|
772
|
+
Get persona recommendations based on context.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
context: Context description for recommendations
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
List of recommended personas
|
|
779
|
+
"""
|
|
780
|
+
# Simple keyword-based recommendations
|
|
781
|
+
# In a real implementation, this could use ML or more sophisticated matching
|
|
782
|
+
context_lower = context.lower()
|
|
783
|
+
recommendations = []
|
|
784
|
+
|
|
785
|
+
# Coding-related keywords
|
|
786
|
+
if any(
|
|
787
|
+
word in context_lower
|
|
788
|
+
for word in [
|
|
789
|
+
"code",
|
|
790
|
+
"programming",
|
|
791
|
+
"development",
|
|
792
|
+
"bug",
|
|
793
|
+
"function",
|
|
794
|
+
]
|
|
795
|
+
):
|
|
796
|
+
coding_persona = self.get_persona("coding")
|
|
797
|
+
if coding_persona:
|
|
798
|
+
recommendations.append(coding_persona)
|
|
799
|
+
|
|
800
|
+
# Research-related keywords
|
|
801
|
+
if any(
|
|
802
|
+
word in context_lower
|
|
803
|
+
for word in ["research", "search", "find", "information", "study"]
|
|
804
|
+
):
|
|
805
|
+
research_persona = self.get_persona("research")
|
|
806
|
+
if research_persona:
|
|
807
|
+
recommendations.append(research_persona)
|
|
808
|
+
|
|
809
|
+
# Creative-related keywords
|
|
810
|
+
if any(
|
|
811
|
+
word in context_lower
|
|
812
|
+
for word in ["write", "story", "creative", "poem", "novel"]
|
|
813
|
+
):
|
|
814
|
+
creative_persona = self.get_persona("creative")
|
|
815
|
+
if creative_persona:
|
|
816
|
+
recommendations.append(creative_persona)
|
|
817
|
+
|
|
818
|
+
# Performance-related keywords
|
|
819
|
+
if any(
|
|
820
|
+
word in context_lower for word in ["fast", "quick", "speed", "efficient"]
|
|
821
|
+
):
|
|
822
|
+
performance_persona = self.get_persona("performance")
|
|
823
|
+
if performance_persona:
|
|
824
|
+
recommendations.append(performance_persona)
|
|
825
|
+
|
|
826
|
+
# Default to general if no specific matches
|
|
827
|
+
if not recommendations:
|
|
828
|
+
general_persona = self.get_persona("general")
|
|
829
|
+
if general_persona:
|
|
830
|
+
recommendations.append(general_persona)
|
|
831
|
+
|
|
832
|
+
return recommendations
|
|
833
|
+
|
|
834
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
835
|
+
"""Get persona manager statistics."""
|
|
836
|
+
return {
|
|
837
|
+
"total_personas": len(self.personas),
|
|
838
|
+
"builtin_personas": len(self._builtin_personas),
|
|
839
|
+
"custom_personas": len(self.custom_personas),
|
|
840
|
+
"active_persona": (self.active_persona.id if self.active_persona else None),
|
|
841
|
+
"available_personas": len(self.get_available_personas()),
|
|
842
|
+
"categories": list(set(p.category.value for p in self.personas.values())),
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
def add_event_listener(self, listener: Callable) -> None:
|
|
846
|
+
"""Add event listener for persona manager events."""
|
|
847
|
+
self._event_listeners.append(listener)
|
|
848
|
+
|
|
849
|
+
def remove_event_listener(self, listener: Callable) -> None:
|
|
850
|
+
"""Remove event listener."""
|
|
851
|
+
if listener in self._event_listeners:
|
|
852
|
+
self._event_listeners.remove(listener)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
# Global instance for easy access
|
|
856
|
+
_global_persona_manager: Optional[PersonaManager] = None
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def get_persona_manager() -> PersonaManager:
|
|
860
|
+
"""Get the global persona manager instance."""
|
|
861
|
+
global _global_persona_manager
|
|
862
|
+
if _global_persona_manager is None:
|
|
863
|
+
_global_persona_manager = PersonaManager()
|
|
864
|
+
return _global_persona_manager
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def set_persona_manager(manager: PersonaManager) -> None:
|
|
868
|
+
"""Set the global persona manager instance."""
|
|
869
|
+
global _global_persona_manager
|
|
870
|
+
_global_persona_manager = manager
|