claude-mpm 0.3.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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (159) hide show
  1. claude_mpm/__init__.py +17 -0
  2. claude_mpm/__main__.py +14 -0
  3. claude_mpm/_version.py +32 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
  5. claude_mpm/agents/INSTRUCTIONS.md +375 -0
  6. claude_mpm/agents/__init__.py +118 -0
  7. claude_mpm/agents/agent_loader.py +621 -0
  8. claude_mpm/agents/agent_loader_integration.py +229 -0
  9. claude_mpm/agents/agents_metadata.py +204 -0
  10. claude_mpm/agents/base_agent.json +27 -0
  11. claude_mpm/agents/base_agent_loader.py +519 -0
  12. claude_mpm/agents/schema/agent_schema.json +160 -0
  13. claude_mpm/agents/system_agent_config.py +587 -0
  14. claude_mpm/agents/templates/__init__.py +101 -0
  15. claude_mpm/agents/templates/data_engineer_agent.json +46 -0
  16. claude_mpm/agents/templates/documentation_agent.json +45 -0
  17. claude_mpm/agents/templates/engineer_agent.json +49 -0
  18. claude_mpm/agents/templates/ops_agent.json +46 -0
  19. claude_mpm/agents/templates/qa_agent.json +45 -0
  20. claude_mpm/agents/templates/research_agent.json +49 -0
  21. claude_mpm/agents/templates/security_agent.json +46 -0
  22. claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
  23. claude_mpm/agents/templates/version_control_agent.json +46 -0
  24. claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
  25. claude_mpm/cli.py +655 -0
  26. claude_mpm/cli_main.py +13 -0
  27. claude_mpm/cli_module/__init__.py +15 -0
  28. claude_mpm/cli_module/args.py +222 -0
  29. claude_mpm/cli_module/commands.py +203 -0
  30. claude_mpm/cli_module/migration_example.py +183 -0
  31. claude_mpm/cli_module/refactoring_guide.md +253 -0
  32. claude_mpm/cli_old/__init__.py +1 -0
  33. claude_mpm/cli_old/ticket_cli.py +102 -0
  34. claude_mpm/config/__init__.py +5 -0
  35. claude_mpm/config/hook_config.py +42 -0
  36. claude_mpm/constants.py +150 -0
  37. claude_mpm/core/__init__.py +45 -0
  38. claude_mpm/core/agent_name_normalizer.py +248 -0
  39. claude_mpm/core/agent_registry.py +627 -0
  40. claude_mpm/core/agent_registry.py.bak +312 -0
  41. claude_mpm/core/agent_session_manager.py +273 -0
  42. claude_mpm/core/base_service.py +747 -0
  43. claude_mpm/core/base_service.py.bak +406 -0
  44. claude_mpm/core/config.py +334 -0
  45. claude_mpm/core/config_aliases.py +292 -0
  46. claude_mpm/core/container.py +347 -0
  47. claude_mpm/core/factories.py +281 -0
  48. claude_mpm/core/framework_loader.py +472 -0
  49. claude_mpm/core/injectable_service.py +206 -0
  50. claude_mpm/core/interfaces.py +539 -0
  51. claude_mpm/core/logger.py +468 -0
  52. claude_mpm/core/minimal_framework_loader.py +107 -0
  53. claude_mpm/core/mixins.py +150 -0
  54. claude_mpm/core/service_registry.py +299 -0
  55. claude_mpm/core/session_manager.py +190 -0
  56. claude_mpm/core/simple_runner.py +511 -0
  57. claude_mpm/core/tool_access_control.py +173 -0
  58. claude_mpm/hooks/README.md +243 -0
  59. claude_mpm/hooks/__init__.py +5 -0
  60. claude_mpm/hooks/base_hook.py +154 -0
  61. claude_mpm/hooks/builtin/__init__.py +1 -0
  62. claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
  63. claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
  64. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
  65. claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
  66. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
  67. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
  68. claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
  69. claude_mpm/hooks/hook_client.py +264 -0
  70. claude_mpm/hooks/hook_runner.py +370 -0
  71. claude_mpm/hooks/json_rpc_executor.py +259 -0
  72. claude_mpm/hooks/json_rpc_hook_client.py +319 -0
  73. claude_mpm/hooks/tool_call_interceptor.py +204 -0
  74. claude_mpm/init.py +246 -0
  75. claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
  76. claude_mpm/orchestration/__init__.py +6 -0
  77. claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
  78. claude_mpm/orchestration/archive/factory.py +215 -0
  79. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
  80. claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
  81. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
  82. claude_mpm/orchestration/archive/orchestrator.py +501 -0
  83. claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
  84. claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
  85. claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
  86. claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
  87. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
  88. claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
  89. claude_mpm/scripts/__init__.py +1 -0
  90. claude_mpm/scripts/ticket.py +269 -0
  91. claude_mpm/services/__init__.py +10 -0
  92. claude_mpm/services/agent_deployment.py +955 -0
  93. claude_mpm/services/agent_lifecycle_manager.py +948 -0
  94. claude_mpm/services/agent_management_service.py +596 -0
  95. claude_mpm/services/agent_modification_tracker.py +841 -0
  96. claude_mpm/services/agent_profile_loader.py +606 -0
  97. claude_mpm/services/agent_registry.py +677 -0
  98. claude_mpm/services/base_agent_manager.py +380 -0
  99. claude_mpm/services/framework_agent_loader.py +337 -0
  100. claude_mpm/services/framework_claude_md_generator/README.md +92 -0
  101. claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
  102. claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
  103. claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
  104. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
  105. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
  106. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
  107. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
  108. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
  109. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
  110. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
  111. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
  112. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
  113. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
  114. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
  115. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
  116. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
  117. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
  118. claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
  119. claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
  120. claude_mpm/services/framework_claude_md_generator.py +621 -0
  121. claude_mpm/services/hook_service.py +388 -0
  122. claude_mpm/services/hook_service_manager.py +223 -0
  123. claude_mpm/services/json_rpc_hook_manager.py +92 -0
  124. claude_mpm/services/parent_directory_manager/README.md +83 -0
  125. claude_mpm/services/parent_directory_manager/__init__.py +577 -0
  126. claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
  127. claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
  128. claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
  129. claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
  130. claude_mpm/services/parent_directory_manager/operations.py +186 -0
  131. claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
  132. claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
  133. claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
  134. claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
  135. claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
  136. claude_mpm/services/shared_prompt_cache.py +819 -0
  137. claude_mpm/services/ticket_manager.py +213 -0
  138. claude_mpm/services/ticket_manager_di.py +318 -0
  139. claude_mpm/services/ticketing_service_original.py +508 -0
  140. claude_mpm/services/version_control/VERSION +1 -0
  141. claude_mpm/services/version_control/__init__.py +70 -0
  142. claude_mpm/services/version_control/branch_strategy.py +670 -0
  143. claude_mpm/services/version_control/conflict_resolution.py +744 -0
  144. claude_mpm/services/version_control/git_operations.py +784 -0
  145. claude_mpm/services/version_control/semantic_versioning.py +703 -0
  146. claude_mpm/ui/__init__.py +1 -0
  147. claude_mpm/ui/rich_terminal_ui.py +295 -0
  148. claude_mpm/ui/terminal_ui.py +328 -0
  149. claude_mpm/utils/__init__.py +16 -0
  150. claude_mpm/utils/config_manager.py +468 -0
  151. claude_mpm/utils/import_migration_example.py +80 -0
  152. claude_mpm/utils/imports.py +182 -0
  153. claude_mpm/utils/path_operations.py +357 -0
  154. claude_mpm/utils/paths.py +289 -0
  155. claude_mpm-0.3.0.dist-info/METADATA +290 -0
  156. claude_mpm-0.3.0.dist-info/RECORD +159 -0
  157. claude_mpm-0.3.0.dist-info/WHEEL +5 -0
  158. claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
  159. claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,621 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unified Agent Loader System
4
+ ==========================
5
+
6
+ Provides unified loading of agent prompts from framework markdown files.
7
+ Integrates with SharedPromptCache for performance optimization.
8
+
9
+ Key Features:
10
+ - Loads agent prompts from framework/agent-roles/*.md files
11
+ - Handles base_agent.md prepending
12
+ - Provides backward-compatible get_*_agent_prompt() functions
13
+ - Uses SharedPromptCache for performance
14
+ - Special handling for ticketing agent's dynamic CLI help
15
+
16
+ For advanced agent management features (CRUD, versioning, section updates), use:
17
+ from claude_pm.agents.agent_loader_integration import get_enhanced_loader
18
+ from claude_pm.services.agent_management_service import AgentManager
19
+
20
+ Usage:
21
+ from claude_pm.agents.agent_loader import get_documentation_agent_prompt
22
+
23
+ # Get agent prompt from MD file
24
+ prompt = get_documentation_agent_prompt()
25
+ """
26
+
27
+ import logging
28
+ import os
29
+ from pathlib import Path
30
+ from typing import Optional, Dict, Any, Tuple, Union
31
+
32
+ from ..services.shared_prompt_cache import SharedPromptCache
33
+ from .base_agent_loader import prepend_base_instructions
34
+ # from ..services.task_complexity_analyzer import TaskComplexityAnalyzer, ComplexityLevel, ModelType
35
+ from ..utils.paths import PathResolver
36
+
37
+ # Temporary placeholders for missing module
38
+ class ComplexityLevel:
39
+ LOW = "LOW"
40
+ MEDIUM = "MEDIUM"
41
+ HIGH = "HIGH"
42
+
43
+ class ModelType:
44
+ HAIKU = "haiku"
45
+ SONNET = "sonnet"
46
+ OPUS = "opus"
47
+
48
+ # Module-level logger
49
+ logger = logging.getLogger(__name__)
50
+
51
+
52
+ def _get_framework_agent_roles_dir() -> Path:
53
+ """Get the framework agent-roles directory dynamically."""
54
+ # Use PathResolver for consistent path discovery
55
+ try:
56
+ framework_root = PathResolver.get_framework_root()
57
+
58
+ # Check if we're running from a wheel installation
59
+ try:
60
+ import claude_pm
61
+ package_path = Path(claude_pm.__file__).parent
62
+ path_str = str(package_path.resolve())
63
+ if 'site-packages' in path_str or 'dist-packages' in path_str:
64
+ # For wheel installations, check data directory
65
+ data_agent_roles = package_path / "data" / "framework" / "agent-roles"
66
+ if data_agent_roles.exists():
67
+ logger.debug(f"Using wheel installation agent-roles: {data_agent_roles}")
68
+ return data_agent_roles
69
+ except Exception:
70
+ pass
71
+
72
+ # Check framework structure
73
+ agent_roles_dir = framework_root / "framework" / "agent-roles"
74
+ if agent_roles_dir.exists():
75
+ logger.debug(f"Using framework agent-roles: {agent_roles_dir}")
76
+ return agent_roles_dir
77
+
78
+ # Try agents directory as fallback
79
+ agents_dir = PathResolver.get_agents_dir()
80
+ logger.debug(f"Using agents directory: {agents_dir}")
81
+ return agents_dir
82
+
83
+ except FileNotFoundError as e:
84
+ # Ultimate fallback
85
+ logger.warning(f"PathResolver could not find framework root: {e}")
86
+ fallback = Path(__file__).parent.parent.parent / "framework" / "agent-roles"
87
+ logger.warning(f"Using fallback agent-roles path: {fallback}")
88
+ return fallback
89
+
90
+
91
+ # Framework agent-roles directory (dynamically determined)
92
+ FRAMEWORK_AGENT_ROLES_DIR = _get_framework_agent_roles_dir()
93
+
94
+ # Cache prefix for agent prompts
95
+ AGENT_CACHE_PREFIX = "agent_prompt:"
96
+
97
+ # Agent name mappings (agent name -> MD file name)
98
+ AGENT_MAPPINGS = {
99
+ "documentation": "documentation-agent.md",
100
+ "version_control": "version-control-agent.md",
101
+ "qa": "qa-agent.md",
102
+ "research": "research-agent.md",
103
+ "ops": "ops-agent.md",
104
+ "security": "security-agent.md",
105
+ "engineer": "engineer-agent.md",
106
+ "data_engineer": "data-agent.md", # Note: data-agent.md maps to data_engineer
107
+ "pm": "pm-orchestrator-agent.md",
108
+ "orchestrator": "pm-orchestrator-agent.md",
109
+ "pm_orchestrator": "pm-orchestrator-agent.md"
110
+ }
111
+
112
+ # Model configuration thresholds
113
+ MODEL_THRESHOLDS = {
114
+ ModelType.HAIKU: {"min_complexity": 0, "max_complexity": 30},
115
+ ModelType.SONNET: {"min_complexity": 31, "max_complexity": 70},
116
+ ModelType.OPUS: {"min_complexity": 71, "max_complexity": 100}
117
+ }
118
+
119
+ # Default model for each agent type (fallback when dynamic selection is disabled)
120
+ DEFAULT_AGENT_MODELS = {
121
+ 'orchestrator': 'claude-4-opus',
122
+ 'pm': 'claude-4-opus',
123
+ 'pm_orchestrator': 'claude-4-opus',
124
+ 'engineer': 'claude-4-opus',
125
+ 'architecture': 'claude-4-opus',
126
+ 'documentation': 'claude-sonnet-4-20250514',
127
+ 'version_control': 'claude-sonnet-4-20250514',
128
+ 'qa': 'claude-sonnet-4-20250514',
129
+ 'research': 'claude-sonnet-4-20250514',
130
+ 'ops': 'claude-sonnet-4-20250514',
131
+ 'security': 'claude-sonnet-4-20250514',
132
+ 'data_engineer': 'claude-sonnet-4-20250514'
133
+ }
134
+
135
+ # Model name mappings for Claude API
136
+ MODEL_NAME_MAPPINGS = {
137
+ ModelType.HAIKU: "claude-3-haiku-20240307",
138
+ ModelType.SONNET: "claude-sonnet-4-20250514",
139
+ ModelType.OPUS: "claude-4-opus"
140
+ }
141
+
142
+
143
+ def load_agent_prompt_from_md(agent_name: str, force_reload: bool = False) -> Optional[str]:
144
+ """
145
+ Load agent prompt from framework markdown file.
146
+
147
+ Args:
148
+ agent_name: Agent name (e.g., 'documentation', 'ticketing')
149
+ force_reload: Force reload from file, bypassing cache
150
+
151
+ Returns:
152
+ str: Agent prompt content from MD file, or None if not found
153
+ """
154
+ try:
155
+ # Get cache instance
156
+ cache = SharedPromptCache.get_instance()
157
+ cache_key = f"{AGENT_CACHE_PREFIX}{agent_name}:md"
158
+
159
+ # Check cache first (unless force reload)
160
+ if not force_reload:
161
+ cached_content = cache.get(cache_key)
162
+ if cached_content is not None:
163
+ logger.debug(f"Agent prompt for '{agent_name}' loaded from cache")
164
+ return str(cached_content)
165
+
166
+ # Get MD file path
167
+ md_filename = AGENT_MAPPINGS.get(agent_name)
168
+ if not md_filename:
169
+ logger.warning(f"No MD file mapping found for agent: {agent_name}")
170
+ return None
171
+
172
+ # Always get fresh framework directory path to ensure we're using the right location
173
+ framework_agent_roles_dir = _get_framework_agent_roles_dir()
174
+ md_path = framework_agent_roles_dir / md_filename
175
+
176
+ # Check if file exists
177
+ if not md_path.exists():
178
+ logger.warning(f"Agent MD file not found: {md_path}")
179
+ return None
180
+
181
+ logger.debug(f"Loading agent prompt from: {md_path}")
182
+ content = md_path.read_text(encoding='utf-8')
183
+
184
+ # Cache the content with 1 hour TTL
185
+ cache.set(cache_key, content, ttl=3600)
186
+ logger.debug(f"Agent prompt for '{agent_name}' cached successfully")
187
+
188
+ return content
189
+
190
+ except Exception as e:
191
+ logger.error(f"Error loading agent prompt from MD for '{agent_name}': {e}")
192
+ return None
193
+
194
+
195
+
196
+
197
+ def _analyze_task_complexity(task_description: str, context_size: int = 0, **kwargs: Any) -> Dict[str, Any]:
198
+ """
199
+ Analyze task complexity using TaskComplexityAnalyzer.
200
+
201
+ Args:
202
+ task_description: Description of the task
203
+ context_size: Size of context in characters
204
+ **kwargs: Additional parameters for complexity analysis
205
+
206
+ Returns:
207
+ Dictionary containing complexity analysis results
208
+ """
209
+ # Temporary implementation until TaskComplexityAnalyzer is available
210
+ logger.warning("TaskComplexityAnalyzer not available, using default values")
211
+ return {
212
+ "complexity_score": 50,
213
+ "complexity_level": ComplexityLevel.MEDIUM,
214
+ "recommended_model": ModelType.SONNET,
215
+ "optimal_prompt_size": (700, 1000),
216
+ "error": "TaskComplexityAnalyzer module not available"
217
+ }
218
+
219
+
220
+ def _get_model_config(agent_name: str, complexity_analysis: Optional[Dict[str, Any]] = None) -> Tuple[str, Dict[str, Any]]:
221
+ """
222
+ Get model configuration based on agent type and task complexity.
223
+
224
+ Args:
225
+ agent_name: Name of the agent
226
+ complexity_analysis: Results from task complexity analysis
227
+
228
+ Returns:
229
+ Tuple of (selected_model, model_config)
230
+ """
231
+ # Check if dynamic model selection is enabled
232
+ enable_dynamic_selection = os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION', 'true').lower() == 'true'
233
+
234
+ # Debug logging
235
+ logger.debug(f"Environment ENABLE_DYNAMIC_MODEL_SELECTION: {os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION')}")
236
+ logger.debug(f"Enable dynamic selection: {enable_dynamic_selection}")
237
+
238
+ # Check for per-agent override in environment
239
+ agent_override_key = f"CLAUDE_PM_{agent_name.upper()}_MODEL_SELECTION"
240
+ agent_override = os.getenv(agent_override_key, '').lower()
241
+
242
+ if agent_override == 'true':
243
+ enable_dynamic_selection = True
244
+ elif agent_override == 'false':
245
+ enable_dynamic_selection = False
246
+
247
+ # Log model selection decision
248
+ logger.info(f"Model selection for {agent_name}: dynamic={enable_dynamic_selection}, "
249
+ f"complexity_available={complexity_analysis is not None}")
250
+
251
+ # Dynamic model selection based on complexity
252
+ if enable_dynamic_selection and complexity_analysis:
253
+ recommended_model = complexity_analysis.get('recommended_model', ModelType.SONNET)
254
+ selected_model = MODEL_NAME_MAPPINGS.get(recommended_model, DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514'))
255
+
256
+ model_config = {
257
+ "selection_method": "dynamic_complexity_based",
258
+ "complexity_score": complexity_analysis.get('complexity_score', 50),
259
+ "complexity_level": complexity_analysis.get('complexity_level', ComplexityLevel.MEDIUM).value,
260
+ "optimal_prompt_size": complexity_analysis.get('optimal_prompt_size', (700, 1000)),
261
+ "scoring_breakdown": complexity_analysis.get('scoring_breakdown', {}),
262
+ "analysis_details": complexity_analysis.get('analysis_details', {})
263
+ }
264
+
265
+ # Log metrics
266
+ logger.info(f"Dynamic model selection for {agent_name}: "
267
+ f"model={selected_model}, "
268
+ f"complexity_score={model_config['complexity_score']}, "
269
+ f"complexity_level={model_config['complexity_level']}")
270
+
271
+ # Track model selection metrics
272
+ log_model_selection(
273
+ agent_name=agent_name,
274
+ selected_model=selected_model,
275
+ complexity_score=model_config['complexity_score'],
276
+ selection_method=model_config['selection_method']
277
+ )
278
+
279
+ else:
280
+ # Use default model mapping
281
+ selected_model = DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514')
282
+ model_config = {
283
+ "selection_method": "default_mapping",
284
+ "reason": "dynamic_selection_disabled" if not enable_dynamic_selection else "no_complexity_analysis"
285
+ }
286
+
287
+ return selected_model, model_config
288
+
289
+
290
+ def get_agent_prompt(agent_name: str, force_reload: bool = False, return_model_info: bool = False, **kwargs: Any) -> Union[str, Tuple[str, str, Dict[str, Any]]]:
291
+ """
292
+ Get agent prompt from MD file with optional dynamic model selection.
293
+
294
+ Args:
295
+ agent_name: Agent name (e.g., 'documentation', 'ticketing')
296
+ force_reload: Force reload from source, bypassing cache
297
+ return_model_info: If True, returns tuple (prompt, model, config)
298
+ **kwargs: Additional arguments including:
299
+ - task_description: Description of the task for complexity analysis
300
+ - context_size: Size of context for complexity analysis
301
+ - enable_complexity_analysis: Override for complexity analysis
302
+ - Additional complexity factors (file_count, integration_points, etc.)
303
+
304
+ Returns:
305
+ str or tuple: Complete agent prompt with base instructions prepended,
306
+ or tuple of (prompt, selected_model, model_config) if return_model_info=True
307
+ """
308
+ # Load from MD file
309
+ prompt = load_agent_prompt_from_md(agent_name, force_reload)
310
+
311
+ if prompt is None:
312
+ raise ValueError(f"No agent prompt MD file found for: {agent_name}")
313
+
314
+ # Analyze task complexity if task description is provided
315
+ complexity_analysis = None
316
+ task_description = kwargs.get('task_description', '')
317
+ enable_analysis = kwargs.get('enable_complexity_analysis', True)
318
+
319
+ if task_description and enable_analysis:
320
+ # Remove already specified parameters from kwargs to avoid duplicates
321
+ analysis_kwargs = {k: v for k, v in kwargs.items()
322
+ if k not in ['task_description', 'context_size']}
323
+ complexity_analysis = _analyze_task_complexity(
324
+ task_description=task_description,
325
+ context_size=kwargs.get('context_size', 0),
326
+ **analysis_kwargs
327
+ )
328
+
329
+ # Get model configuration (always happens, even without complexity analysis)
330
+ selected_model, model_config = _get_model_config(agent_name, complexity_analysis)
331
+
332
+ # Always store model selection info in kwargs for potential use by callers
333
+ kwargs['_selected_model'] = selected_model
334
+ kwargs['_model_config'] = model_config
335
+
336
+ # Handle dynamic template formatting if needed
337
+ if "{dynamic_help}" in prompt:
338
+ try:
339
+ # Import CLI helper module to get dynamic help
340
+ from ..orchestration.ai_trackdown_tools import CLIHelpFormatter
341
+
342
+ # Create a CLI helper instance
343
+ cli_helper = CLIHelpFormatter()
344
+ help_content, _ = cli_helper.get_cli_help()
345
+ dynamic_help = cli_helper.format_help_for_prompt(help_content)
346
+ prompt = prompt.format(dynamic_help=dynamic_help)
347
+ except Exception as e:
348
+ logger.warning(f"Could not format dynamic help for ticketing agent: {e}")
349
+ # Remove the placeholder if we can't fill it
350
+ prompt = prompt.replace("{dynamic_help}", "")
351
+
352
+ # Add model selection metadata to prompt if dynamic selection is enabled
353
+ if selected_model and model_config.get('selection_method') == 'dynamic_complexity_based':
354
+ model_metadata = f"\n<!-- Model Selection: {selected_model} (Complexity: {model_config.get('complexity_level', 'UNKNOWN')}) -->\n"
355
+ prompt = model_metadata + prompt
356
+
357
+ # Prepend base instructions with dynamic template based on complexity
358
+ complexity_score = model_config.get('complexity_score', 50) if model_config else 50
359
+ final_prompt = prepend_base_instructions(prompt, complexity_score=complexity_score)
360
+
361
+ # Return model info if requested
362
+ if return_model_info:
363
+ return final_prompt, selected_model, model_config
364
+ else:
365
+ return final_prompt
366
+
367
+
368
+ # Backward-compatible functions
369
+ def get_documentation_agent_prompt() -> str:
370
+ """Get the complete Documentation Agent prompt with base instructions."""
371
+ prompt = get_agent_prompt("documentation", return_model_info=False)
372
+ assert isinstance(prompt, str), "Expected string when return_model_info=False"
373
+ return prompt
374
+
375
+
376
+
377
+ def get_version_control_agent_prompt() -> str:
378
+ """Get the complete Version Control Agent prompt with base instructions."""
379
+ prompt = get_agent_prompt("version_control", return_model_info=False)
380
+ assert isinstance(prompt, str), "Expected string when return_model_info=False"
381
+ return prompt
382
+
383
+
384
+ def get_qa_agent_prompt() -> str:
385
+ """Get the complete QA Agent prompt with base instructions."""
386
+ prompt = get_agent_prompt("qa", return_model_info=False)
387
+ assert isinstance(prompt, str), "Expected string when return_model_info=False"
388
+ return prompt
389
+
390
+
391
+ def get_research_agent_prompt() -> str:
392
+ """Get the complete Research Agent prompt with base instructions."""
393
+ prompt = get_agent_prompt("research", return_model_info=False)
394
+ assert isinstance(prompt, str), "Expected string when return_model_info=False"
395
+ return prompt
396
+
397
+
398
+ def get_ops_agent_prompt() -> str:
399
+ """Get the complete Ops Agent prompt with base instructions."""
400
+ prompt = get_agent_prompt("ops", return_model_info=False)
401
+ assert isinstance(prompt, str), "Expected string when return_model_info=False"
402
+ return prompt
403
+
404
+
405
+ def get_security_agent_prompt() -> str:
406
+ """Get the complete Security Agent prompt with base instructions."""
407
+ prompt = get_agent_prompt("security", return_model_info=False)
408
+ assert isinstance(prompt, str), "Expected string when return_model_info=False"
409
+ return prompt
410
+
411
+
412
+ def get_engineer_agent_prompt() -> str:
413
+ """Get the complete Engineer Agent prompt with base instructions."""
414
+ prompt = get_agent_prompt("engineer", return_model_info=False)
415
+ assert isinstance(prompt, str), "Expected string when return_model_info=False"
416
+ return prompt
417
+
418
+
419
+ def get_data_engineer_agent_prompt() -> str:
420
+ """Get the complete Data Engineer Agent prompt with base instructions."""
421
+ prompt = get_agent_prompt("data_engineer", return_model_info=False)
422
+ assert isinstance(prompt, str), "Expected string when return_model_info=False"
423
+ return prompt
424
+
425
+
426
+ def get_agent_prompt_with_model_info(agent_name: str, force_reload: bool = False, **kwargs: Any) -> Tuple[str, str, Dict[str, Any]]:
427
+ """
428
+ Get agent prompt with model selection information.
429
+
430
+ Args:
431
+ agent_name: Agent name (e.g., 'documentation', 'ticketing')
432
+ force_reload: Force reload from source, bypassing cache
433
+ **kwargs: Additional arguments for prompt generation and model selection
434
+
435
+ Returns:
436
+ Tuple of (prompt, selected_model, model_config)
437
+ """
438
+ # Use get_agent_prompt with return_model_info=True
439
+ result = get_agent_prompt(agent_name, force_reload, return_model_info=True, **kwargs)
440
+
441
+ # If result is a tuple, return it directly
442
+ if isinstance(result, tuple):
443
+ return result
444
+
445
+ # Fallback (shouldn't happen)
446
+ return result, DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514'), {"selection_method": "default"}
447
+
448
+
449
+ # Utility functions
450
+ def list_available_agents() -> Dict[str, Dict[str, Any]]:
451
+ """
452
+ List all available agents with their sources.
453
+
454
+ Returns:
455
+ dict: Agent information including MD file path
456
+ """
457
+ agents = {}
458
+
459
+ # Get fresh framework directory path
460
+ framework_agent_roles_dir = _get_framework_agent_roles_dir()
461
+
462
+ for agent_name, md_filename in AGENT_MAPPINGS.items():
463
+ md_path = framework_agent_roles_dir / md_filename
464
+
465
+ agents[agent_name] = {
466
+ "md_file": md_filename if md_path.exists() else None,
467
+ "md_path": str(md_path) if md_path.exists() else None,
468
+ "has_md": md_path.exists(),
469
+ "default_model": DEFAULT_AGENT_MODELS.get(agent_name, 'claude-sonnet-4-20250514')
470
+ }
471
+
472
+ return agents
473
+
474
+
475
+ def clear_agent_cache(agent_name: Optional[str] = None) -> None:
476
+ """
477
+ Clear cached agent prompts.
478
+
479
+ Args:
480
+ agent_name: Specific agent to clear, or None to clear all
481
+ """
482
+ try:
483
+ cache = SharedPromptCache.get_instance()
484
+
485
+ if agent_name:
486
+ cache_key = f"{AGENT_CACHE_PREFIX}{agent_name}:md"
487
+ cache.invalidate(cache_key)
488
+ logger.debug(f"Cache cleared for agent: {agent_name}")
489
+ else:
490
+ # Clear all agent caches
491
+ for name in AGENT_MAPPINGS:
492
+ cache_key = f"{AGENT_CACHE_PREFIX}{name}:md"
493
+ cache.invalidate(cache_key)
494
+ logger.debug("All agent caches cleared")
495
+
496
+ except Exception as e:
497
+ logger.error(f"Error clearing agent cache: {e}")
498
+
499
+
500
+ def validate_agent_files() -> Dict[str, Dict[str, Any]]:
501
+ """
502
+ Validate that all expected agent files exist.
503
+
504
+ Returns:
505
+ dict: Validation results for each agent
506
+ """
507
+ results = {}
508
+
509
+ # Get fresh framework directory path
510
+ framework_agent_roles_dir = _get_framework_agent_roles_dir()
511
+
512
+ for agent_name, md_filename in AGENT_MAPPINGS.items():
513
+ md_path = framework_agent_roles_dir / md_filename
514
+ results[agent_name] = {
515
+ "md_exists": md_path.exists(),
516
+ "md_path": str(md_path)
517
+ }
518
+
519
+ return results
520
+
521
+
522
+ def get_model_selection_metrics() -> Dict[str, Any]:
523
+ """
524
+ Get metrics about model selection usage.
525
+
526
+ Returns:
527
+ dict: Metrics including feature flag status and selection counts
528
+ """
529
+ # Check feature flag status
530
+ global_enabled = os.getenv('ENABLE_DYNAMIC_MODEL_SELECTION', 'true').lower() == 'true'
531
+
532
+ # Check per-agent overrides
533
+ agent_overrides = {}
534
+ for agent_name in AGENT_MAPPINGS.keys():
535
+ override_key = f"CLAUDE_PM_{agent_name.upper()}_MODEL_SELECTION"
536
+ override_value = os.getenv(override_key, '')
537
+ if override_value:
538
+ agent_overrides[agent_name] = override_value.lower() == 'true'
539
+
540
+ # Get cache instance to check for cached metrics
541
+ try:
542
+ cache = SharedPromptCache.get_instance()
543
+ selection_stats = cache.get("agent_loader:model_selection_stats") or {}
544
+ except Exception:
545
+ selection_stats = {}
546
+
547
+ return {
548
+ "feature_flag": {
549
+ "global_enabled": global_enabled,
550
+ "agent_overrides": agent_overrides
551
+ },
552
+ "model_thresholds": {
553
+ model_type.value: thresholds
554
+ for model_type, thresholds in MODEL_THRESHOLDS.items()
555
+ },
556
+ "default_models": DEFAULT_AGENT_MODELS,
557
+ "selection_stats": selection_stats
558
+ }
559
+
560
+
561
+ def log_model_selection(agent_name: str, selected_model: str, complexity_score: int, selection_method: str) -> None:
562
+ """
563
+ Log model selection for metrics tracking.
564
+
565
+ Args:
566
+ agent_name: Name of the agent
567
+ selected_model: Model that was selected
568
+ complexity_score: Complexity score from analysis
569
+ selection_method: Method used for selection
570
+ """
571
+ try:
572
+ # Get cache instance
573
+ cache = SharedPromptCache.get_instance()
574
+
575
+ # Get existing stats
576
+ stats_key = "agent_loader:model_selection_stats"
577
+ stats = cache.get(stats_key) or {
578
+ "total_selections": 0,
579
+ "by_model": {},
580
+ "by_agent": {},
581
+ "by_method": {},
582
+ "complexity_distribution": {
583
+ "0-30": 0,
584
+ "31-70": 0,
585
+ "71-100": 0
586
+ }
587
+ }
588
+
589
+ # Update stats
590
+ stats["total_selections"] += 1
591
+
592
+ # By model
593
+ if selected_model not in stats["by_model"]:
594
+ stats["by_model"][selected_model] = 0
595
+ stats["by_model"][selected_model] += 1
596
+
597
+ # By agent
598
+ if agent_name not in stats["by_agent"]:
599
+ stats["by_agent"][agent_name] = {}
600
+ if selected_model not in stats["by_agent"][agent_name]:
601
+ stats["by_agent"][agent_name][selected_model] = 0
602
+ stats["by_agent"][agent_name][selected_model] += 1
603
+
604
+ # By method
605
+ if selection_method not in stats["by_method"]:
606
+ stats["by_method"][selection_method] = 0
607
+ stats["by_method"][selection_method] += 1
608
+
609
+ # Complexity distribution
610
+ if complexity_score <= 30:
611
+ stats["complexity_distribution"]["0-30"] += 1
612
+ elif complexity_score <= 70:
613
+ stats["complexity_distribution"]["31-70"] += 1
614
+ else:
615
+ stats["complexity_distribution"]["71-100"] += 1
616
+
617
+ # Store updated stats with 24 hour TTL
618
+ cache.set(stats_key, stats, ttl=86400)
619
+
620
+ except Exception as e:
621
+ logger.warning(f"Failed to log model selection metrics: {e}")