File size: 14,428 Bytes
8b02e7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
"""Configuration manager for runtime agent and routing rules management."""

import yaml
import re
from pathlib import Path
from typing import Any
from dataclasses import dataclass

from ..config import DelegationConfig, OrchestratorConfig, DelegationRule
from ..orchestrator import OrchestratorRegistry


@dataclass
class AgentStatus:
    """Agent status information."""

    name: str
    enabled: bool
    installed: bool
    config: OrchestratorConfig | None
    status_text: str
    status_icon: str

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for UI display."""
        return {
            "name": self.name,
            "enabled": self.enabled,
            "installed": self.installed,
            "status": self.status_text,
            "icon": self.status_icon,
        }


class ConfigurationManager:
    """Manages agent configuration and routing rules."""

    def __init__(
        self,
        orchestrators_path: Path = Path("config/orchestrators.yaml"),
        rules_path: Path = Path("config/delegation_rules.yaml"),
    ):
        """Initialize configuration manager.

        Args:
            orchestrators_path: Path to orchestrators YAML config
            rules_path: Path to delegation rules YAML config
        """
        self.orchestrators_path = orchestrators_path
        self.rules_path = rules_path
        self.registry = OrchestratorRegistry()

        # Store default configurations for reset functionality
        self.default_orchestrators: dict[str, Any] = {}
        self.default_rules: list[dict[str, Any]] = []

        # Load configurations
        self.load_configurations()

    def load_configurations(self) -> None:
        """Load orchestrator and delegation rule configurations."""
        # Load orchestrators
        with open(self.orchestrators_path) as f:
            orch_data = yaml.safe_load(f)
            self.orchestrators_config = orch_data.get("orchestrators", {})

            # Store defaults
            if not self.default_orchestrators:
                self.default_orchestrators = {
                    k: v.copy() for k, v in self.orchestrators_config.items()
                }

            # Register orchestrators
            for name, config in self.orchestrators_config.items():
                self.registry.register(OrchestratorConfig(**config))

        # Load delegation rules
        with open(self.rules_path) as f:
            rules_data = yaml.safe_load(f)
            self.primary_orchestrator = rules_data.get("orchestrator", "claude")
            self.rules_list = rules_data.get("rules", [])

            # Store defaults
            if not self.default_rules:
                self.default_rules = [r.copy() for r in self.rules_list]

    def get_agent_statuses(self) -> list[AgentStatus]:
        """Get status information for all agents.

        Returns:
            List of AgentStatus objects with installation and enablement info
        """
        # Validate which agents are installed
        installed_agents = self.registry.validate_all()

        statuses = []
        for name, config_dict in self.orchestrators_config.items():
            config = OrchestratorConfig(**config_dict)
            is_installed = installed_agents.get(name, False)
            is_enabled = config.enabled

            # Determine status
            if is_enabled and is_installed:
                status_text = "Active"
                status_icon = "🟒"
            elif is_enabled and not is_installed:
                status_text = "Not Installed"
                status_icon = "⚠️"
            else:
                status_text = "Disabled"
                status_icon = "πŸ”΄"

            statuses.append(AgentStatus(
                name=name,
                enabled=is_enabled,
                installed=is_installed,
                config=config,
                status_text=status_text,
                status_icon=status_icon,
            ))

        return statuses

    def toggle_agent(self, agent_name: str, enabled: bool) -> tuple[bool, str]:
        """Toggle an agent on or off.

        Args:
            agent_name: Name of the agent to toggle
            enabled: True to enable, False to disable

        Returns:
            tuple: (success, message)
        """
        if agent_name not in self.orchestrators_config:
            return False, f"Agent '{agent_name}' not found"

        # Check if this is the primary orchestrator
        if not enabled and agent_name == self.primary_orchestrator:
            return False, f"Cannot disable primary orchestrator '{agent_name}'. Please select a different primary orchestrator first."

        # Check if disabling would break any routing rules
        if not enabled:
            broken_rules = [
                rule for rule in self.rules_list
                if rule.get("delegate_to") == agent_name
            ]
            if broken_rules:
                rule_descriptions = [r.get("description", r.get("pattern")) for r in broken_rules]
                return False, f"Cannot disable '{agent_name}'. It is used in {len(broken_rules)} routing rule(s): {', '.join(rule_descriptions[:3])}"

        # Update configuration
        self.orchestrators_config[agent_name]["enabled"] = enabled

        # Update registry
        config = OrchestratorConfig(**self.orchestrators_config[agent_name])
        self.registry.register(config)

        return True, f"Agent '{agent_name}' {'enabled' if enabled else 'disabled'} successfully"

    def set_primary_orchestrator(self, agent_name: str) -> tuple[bool, str]:
        """Set the primary orchestrator.

        Args:
            agent_name: Name of the agent to set as primary

        Returns:
            tuple: (success, message)
        """
        if agent_name not in self.orchestrators_config:
            return False, f"Agent '{agent_name}' not found"

        config = self.orchestrators_config[agent_name]

        # Validate agent is enabled
        if not config.get("enabled", False):
            return False, f"Cannot set '{agent_name}' as primary orchestrator because it is disabled. Please enable it first."

        # Validate agent is installed
        installed = self.registry.validate_all()
        if not installed.get(agent_name, False):
            return False, f"Cannot set '{agent_name}' as primary orchestrator because it is not installed."

        self.primary_orchestrator = agent_name
        return True, f"Primary orchestrator set to '{agent_name}'"

    def validate_routing_rules(self, yaml_text: str) -> tuple[bool, str, list[dict[str, Any]] | None]:
        """Validate routing rules YAML.

        Args:
            yaml_text: YAML text to validate

        Returns:
            tuple: (is_valid, message, parsed_rules)
        """
        try:
            # Parse YAML
            data = yaml.safe_load(yaml_text)

            if not isinstance(data, list):
                return False, "Rules must be a list", None

            # Validate each rule
            for i, rule in enumerate(data):
                if not isinstance(rule, dict):
                    return False, f"Rule {i+1} must be a dictionary", None

                # Required fields
                if "pattern" not in rule:
                    return False, f"Rule {i+1} missing required field 'pattern'", None
                if "delegate_to" not in rule:
                    return False, f"Rule {i+1} missing required field 'delegate_to'", None

                # Validate regex pattern
                try:
                    re.compile(rule["pattern"])
                except re.error as e:
                    return False, f"Rule {i+1} has invalid regex pattern: {e}", None

                # Validate delegate_to exists
                delegate_to = rule["delegate_to"]
                if delegate_to not in self.orchestrators_config:
                    return False, f"Rule {i+1} delegates to unknown agent '{delegate_to}'", None

                # Validate delegate_to is enabled
                if not self.orchestrators_config[delegate_to].get("enabled", False):
                    return False, f"Rule {i+1} delegates to disabled agent '{delegate_to}'", None

                # Validate priority is a number
                if "priority" in rule and not isinstance(rule["priority"], (int, float)):
                    return False, f"Rule {i+1} priority must be a number", None

            return True, "βœ… Routing rules are valid", data

        except yaml.YAMLError as e:
            return False, f"YAML parsing error: {e}", None
        except Exception as e:
            return False, f"Validation error: {e}", None

    def preview_routing_rules(self, rules: list[dict[str, Any]]) -> str:
        """Generate a preview of how routing rules will affect delegation.

        Args:
            rules: List of routing rules

        Returns:
            Formatted preview text
        """
        if not rules:
            return "No routing rules defined. All tasks will go to the primary orchestrator."

        preview = "## Routing Rules Preview\n\n"
        preview += f"**Primary Orchestrator:** {self.primary_orchestrator}\n\n"

        # Sort by priority (highest first)
        sorted_rules = sorted(rules, key=lambda r: r.get("priority", 0), reverse=True)

        preview += "**Rules (by priority):**\n\n"
        for i, rule in enumerate(sorted_rules, 1):
            pattern = rule.get("pattern", "")
            delegate_to = rule.get("delegate_to", "")
            priority = rule.get("priority", 0)
            description = rule.get("description", "")

            preview += f"{i}. **Pattern:** `{pattern}`\n"
            preview += f"   **Delegates to:** {delegate_to}\n"
            preview += f"   **Priority:** {priority}\n"
            if description:
                preview += f"   **Description:** {description}\n"
            preview += "\n"

        # Add example queries
        preview += "\n**Example Query Matching:**\n\n"
        example_queries = [
            "Fix this security vulnerability",
            "Refactor the authentication module",
            "Create a pull request for this feature",
            "Run the test suite",
        ]

        for query in example_queries:
            matched = False
            for rule in sorted_rules:
                if re.search(rule["pattern"], query, re.IGNORECASE):
                    preview += f"- \"{query}\" β†’ **{rule['delegate_to']}** (matches: `{rule['pattern']}`)\n"
                    matched = True
                    break

            if not matched:
                preview += f"- \"{query}\" β†’ **{self.primary_orchestrator}** (no rule match)\n"

        return preview

    def save_configurations(self, rules_yaml: str | None = None) -> tuple[bool, str]:
        """Save configurations to YAML files.

        Args:
            rules_yaml: Optional YAML text for routing rules

        Returns:
            tuple: (success, message)
        """
        try:
            # Save orchestrators
            with open(self.orchestrators_path, "w") as f:
                yaml.dump(
                    {"orchestrators": self.orchestrators_config},
                    f,
                    default_flow_style=False,
                    sort_keys=False,
                )

            # Save delegation rules
            rules_data = {
                "orchestrator": self.primary_orchestrator,
            }

            if rules_yaml:
                # Validate and use provided rules
                is_valid, message, parsed_rules = self.validate_routing_rules(rules_yaml)
                if not is_valid:
                    return False, f"Cannot save: {message}"
                rules_data["rules"] = parsed_rules
                self.rules_list = parsed_rules or []
            else:
                rules_data["rules"] = self.rules_list

            with open(self.rules_path, "w") as f:
                # Add comment header
                f.write("# Delegation MCP Configuration\n")
                f.write("# Configure your primary orchestrator and delegation rules\n")
                f.write("# Note: Orchestrator definitions are in config/orchestrators.yaml\n\n")
                yaml.dump(rules_data, f, default_flow_style=False, sort_keys=False)

            return True, "βœ… Configuration saved successfully"

        except Exception as e:
            return False, f"Failed to save configuration: {e}"

    def reset_to_defaults(self) -> tuple[bool, str]:
        """Reset all configurations to defaults.

        Returns:
            tuple: (success, message)
        """
        try:
            # Reset orchestrators
            self.orchestrators_config = {
                k: v.copy() for k, v in self.default_orchestrators.items()
            }

            # Reset rules
            self.rules_list = [r.copy() for r in self.default_rules]
            self.primary_orchestrator = "claude"

            # Re-register orchestrators
            for name, config in self.orchestrators_config.items():
                self.registry.register(OrchestratorConfig(**config))

            # Save defaults
            success, message = self.save_configurations()
            if success:
                return True, "βœ… Configuration reset to defaults"
            return False, f"Failed to save defaults: {message}"

        except Exception as e:
            return False, f"Failed to reset configuration: {e}"

    def get_rules_yaml(self) -> str:
        """Get current routing rules as YAML text.

        Returns:
            YAML text of routing rules
        """
        return yaml.dump(self.rules_list, default_flow_style=False, sort_keys=False)

    def get_agent_capabilities(self, agent_name: str) -> dict[str, Any]:
        """Get agent capabilities from configuration.

        Args:
            agent_name: Name of the agent

        Returns:
            Dictionary of agent capabilities
        """
        if agent_name not in self.orchestrators_config:
            return {}

        config = self.orchestrators_config[agent_name]

        return {
            "command": config.get("command", ""),
            "args": config.get("args", []),
            "timeout": config.get("timeout", 300),
            "max_retries": config.get("max_retries", 3),
            "enabled": config.get("enabled", False),
        }