File size: 12,050 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
"""CLI for executing workflows."""

import asyncio
import click
import sys
from pathlib import Path
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
from rich.table import Table
from rich.panel import Panel
from rich.syntax import Syntax

from .workflow import WorkflowEngine, WorkflowDefinition
from .orchestrator import OrchestratorRegistry
from .config import DelegationConfig
from .logging_config import setup_logging
from .agent_discovery import AgentDiscovery


console = Console()


@click.group()
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
@click.option('--config', '-c', type=click.Path(exists=True), help='Config file path')
@click.pass_context
def cli(ctx, verbose, config):
    """Delegation MCP - Multi-Agent Workflow Orchestration."""
    ctx.ensure_object(dict)
    ctx.obj['verbose'] = verbose
    ctx.obj['config_path'] = Path(config) if config else Path("config/delegation_rules.yaml")

    # Setup logging
    import logging
    setup_logging(level=logging.DEBUG if verbose else logging.INFO, verbose=verbose)


@cli.command('list')
@click.pass_context
def list_workflows(ctx):
    """List all available workflows."""
    workflows_dir = Path("workflows")

    if not workflows_dir.exists():
        console.print("[red]❌ Workflows directory not found[/red]")
        sys.exit(1)

    console.print("\n[bold cyan]πŸ“š Available Workflows[/bold cyan]\n")

    table = Table(show_header=True, header_style="bold magenta")
    table.add_column("Name", style="cyan", no_wrap=True)
    table.add_column("Steps", justify="center", style="yellow")
    table.add_column("Agents", style="green")
    table.add_column("Category", style="blue")
    table.add_column("Difficulty", style="magenta")
    table.add_column("Duration", justify="right", style="yellow")

    for workflow_file in sorted(workflows_dir.glob("*.yaml")):
        try:
            workflow = WorkflowDefinition.from_yaml(workflow_file)
            agents = ", ".join(sorted(set(step.agent for step in workflow.steps)))
            category = workflow.metadata.get("category", "general")
            difficulty = workflow.metadata.get("difficulty", "intermediate")
            duration = workflow.metadata.get("estimated_duration", 0)

            table.add_row(
                workflow.name,
                str(len(workflow.steps)),
                agents,
                category,
                difficulty,
                f"{duration // 60}min"
            )
        except Exception as e:
            console.print(f"[red]⚠️  Failed to load {workflow_file.name}: {e}[/red]")

    console.print(table)
    console.print()


@cli.command()
@click.argument('workflow_name')
@click.pass_context
def show(ctx, workflow_name):
    """Show workflow details."""
    workflows_dir = Path("workflows")
    workflow = _find_workflow(workflow_name, workflows_dir)

    if not workflow:
        sys.exit(1)

    # Display workflow info
    console.print()
    console.print(Panel(
        f"[bold]{workflow.name}[/bold]\n\n{workflow.description}",
        title="πŸ“‹ Workflow Details",
        border_style="cyan"
    ))

    # Display steps
    console.print("\n[bold cyan]πŸ”„ Workflow Steps[/bold cyan]\n")

    for i, step in enumerate(workflow.steps, 1):
        console.print(f"[bold yellow]Step {i}:[/bold yellow] {step.id}")
        console.print(f"  [cyan]Agent:[/cyan] {step.agent}")
        console.print(f"  [green]Task:[/green] {step.task}")
        if step.output:
            console.print(f"  [blue]Output:[/blue] {step.output}")
        if step.condition:
            console.print(f"  [magenta]Condition:[/magenta] {step.condition}")
        console.print()

    # Display metadata
    if workflow.metadata:
        console.print("[bold cyan]πŸ“Š Metadata[/bold cyan]")
        for key, value in workflow.metadata.items():
            console.print(f"  [cyan]{key}:[/cyan] {value}")

    console.print()


@cli.command()
@click.argument('workflow_name')
@click.option('--context', '-c', multiple=True, help='Context variables (key=value)')
@click.option('--dry-run', is_flag=True, help='Show what would be executed without running')
@click.pass_context
def execute(ctx, workflow_name, context, dry_run):
    """Execute a workflow."""
    workflows_dir = Path("workflows")
    workflow = _find_workflow(workflow_name, workflows_dir)

    if not workflow:
        sys.exit(1)

    # Parse context
    context_dict = {}
    for ctx_item in context:
        if '=' in ctx_item:
            key, value = ctx_item.split('=', 1)
            context_dict[key.strip()] = value.strip()

    if dry_run:
        console.print("\n[bold yellow]πŸ” Dry Run Mode[/bold yellow]\n")
        console.print(f"[cyan]Workflow:[/cyan] {workflow.name}")
        console.print(f"[cyan]Steps:[/cyan] {len(workflow.steps)}")
        console.print(f"[cyan]Context:[/cyan] {context_dict}")
        console.print("\n[bold green]Would execute:[/bold green]\n")
        for i, step in enumerate(workflow.steps, 1):
            console.print(f"  {i}. [{step.agent}] {step.task}")
        console.print()
        return

    # Execute workflow
    asyncio.run(_execute_workflow(ctx, workflow, context_dict))


async def _execute_workflow(ctx, workflow, context):
    """Execute workflow with progress display."""
    # Setup
    config_path = ctx.obj['config_path']
    if config_path.exists():
        config = DelegationConfig.from_yaml(config_path)
    else:
        config = DelegationConfig(orchestrator="claude")

    registry = OrchestratorRegistry()
    for name, orch_config in config.orchestrators.items():
        registry.register(orch_config)

    engine = WorkflowEngine(registry)

    # Display header
    console.print()
    console.print(Panel(
        f"[bold]{workflow.name}[/bold]\n{workflow.description}",
        title="πŸš€ Executing Workflow",
        border_style="green"
    ))
    console.print()

    # Execute with progress
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        console=console,
    ) as progress:

        task = progress.add_task(
            f"[cyan]Executing {len(workflow.steps)} steps...",
            total=len(workflow.steps)
        )

        # We need to modify the engine to support progress callbacks
        # For now, just execute
        result = await engine.execute(workflow, initial_context=context)

        progress.update(task, completed=len(workflow.steps))

    # Display results
    console.print()
    if result.success:
        console.print(Panel(
            f"[bold green]βœ… Workflow Completed Successfully[/bold green]\n\n"
            f"Steps: {result.steps_completed}/{result.total_steps}\n"
            f"Duration: {result.duration:.2f}s",
            border_style="green"
        ))
    else:
        console.print(Panel(
            f"[bold red]❌ Workflow Failed[/bold red]\n\n"
            f"Steps: {result.steps_completed}/{result.total_steps}\n"
            f"Duration: {result.duration:.2f}s",
            border_style="red"
        ))

    # Display outputs
    if result.outputs:
        console.print("\n[bold cyan]πŸ“€ Outputs[/bold cyan]\n")
        for key, value in result.outputs.items():
            console.print(f"[bold]{key}:[/bold]")
            # Truncate long outputs
            display_value = str(value)
            if len(display_value) > 1000:
                display_value = display_value[:1000] + "\n... (truncated)"
            console.print(Panel(display_value, border_style="blue"))

    # Display errors
    if result.errors:
        console.print("\n[bold red]❌ Errors[/bold red]\n")
        for error in result.errors:
            console.print(f"  β€’ {error}")

    console.print()


@cli.command()
@click.argument('workflow_file', type=click.Path(exists=True))
@click.pass_context
def validate(ctx, workflow_file):
    """Validate a workflow file."""
    try:
        workflow = WorkflowDefinition.from_yaml(Path(workflow_file))
        console.print(f"[green]βœ… Workflow '{workflow.name}' is valid[/green]")
        console.print(f"   Steps: {len(workflow.steps)}")
        console.print(f"   Agents: {', '.join(set(step.agent for step in workflow.steps))}")
    except Exception as e:
        console.print(f"[red]❌ Invalid workflow: {e}[/red]")
        sys.exit(1)


@cli.command('discover-agents')
@click.option('--force-refresh', '-f', is_flag=True, help='Force re-discovery even if cache exists')
@click.option('--json', 'output_json', is_flag=True, help='Output results as JSON')
@click.pass_context
def discover_agents_cmd(ctx, force_refresh, output_json):
    """Discover available CLI agents on the system."""
    asyncio.run(_discover_agents(force_refresh, output_json))


async def _discover_agents(force_refresh, output_json):
    """Execute agent discovery."""
    console.print()
    console.print("[bold cyan]Discovering CLI Agents...[/bold cyan]\n")

    # Create discovery instance and run discovery
    discovery = AgentDiscovery()

    console.print("[cyan]Scanning system PATH...[/cyan]")
    discovered = await discovery.discover_agents(force_refresh=force_refresh)
    console.print()

    summary = discovery.get_discovery_summary()

    if output_json:
        # Output as JSON
        import json
        console.print(json.dumps(summary, indent=2))
        return

    # Display results in table format
    console.print()
    console.print(Panel(
        f"[bold]Agent Discovery Results[/bold]\n\n"
        f"Total agents scanned: {summary['total_agents']}\n"
        f"Available: [green]{summary['available']}[/green]\n"
        f"Unavailable: [red]{summary['unavailable']}[/red]",
        title="Summary",
        border_style="cyan"
    ))
    console.print()

    # Display available agents
    if summary['available_agents']:
        console.print("[bold green]Available Agents[/bold green]\n")
        table = Table(show_header=True, header_style="bold magenta")
        table.add_column("Agent", style="cyan", no_wrap=True)
        table.add_column("Version", style="yellow")
        table.add_column("Path", style="green")

        for agent in summary['available_agents']:
            table.add_row(
                agent['name'],
                agent['version'][:50] if agent['version'] else 'Unknown',
                agent['path']
            )

        console.print(table)
        console.print()

    # Display unavailable agents
    if summary['unavailable_agents']:
        console.print("[bold red]Unavailable Agents[/bold red]\n")
        for agent in summary['unavailable_agents']:
            console.print(f"[red]x[/red] [bold]{agent['name']}[/bold]")
            console.print(f"  {agent['error']}\n")

    console.print()
    console.print("[dim]Tip: Use --force-refresh to re-scan, or --json for machine-readable output[/dim]")
    console.print()


def _find_workflow(name, workflows_dir):
    """Find workflow by name or filename."""
    if not workflows_dir.exists():
        console.print("[red]❌ Workflows directory not found[/red]")
        return None

    # Try exact name match
    for workflow_file in workflows_dir.glob("*.yaml"):
        try:
            workflow = WorkflowDefinition.from_yaml(workflow_file)
            if workflow.name.lower() == name.lower():
                return workflow
        except Exception:
            continue

    # Try filename match
    workflow_path = workflows_dir / f"{name}.yaml"
    if workflow_path.exists():
        try:
            return WorkflowDefinition.from_yaml(workflow_path)
        except Exception as e:
            console.print(f"[red]❌ Failed to load workflow: {e}[/red]")
            return None

    console.print(f"[red]❌ Workflow '{name}' not found[/red]")
    console.print("\nTry: [cyan]delegation-workflow list[/cyan] to see available workflows")
    return None


def main():
    """Entry point."""
    cli(obj={})


if __name__ == '__main__':
    main()