supyagent 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.

Potentially problematic release.


This version of supyagent might be problematic. Click here for more details.

supyagent/cli/main.py ADDED
@@ -0,0 +1,946 @@
1
+ """
2
+ CLI entry point for supyagent.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import click
11
+ from rich.console import Console
12
+ from rich.markdown import Markdown
13
+ from rich.panel import Panel
14
+ from rich.progress import Progress, SpinnerColumn, TextColumn
15
+ from rich.table import Table
16
+
17
+ from typing import Any
18
+
19
+ from supyagent.core.agent import Agent
20
+ from supyagent.core.executor import ExecutionRunner
21
+ from supyagent.core.registry import AgentRegistry
22
+ from supyagent.core.session_manager import SessionManager
23
+ from supyagent.models.agent_config import AgentNotFoundError, load_agent_config
24
+
25
+ console = Console()
26
+
27
+
28
+ @click.group()
29
+ @click.version_option(version="0.1.0", prog_name="supyagent")
30
+ def cli():
31
+ """
32
+ Supyagent - LLM agents powered by supypowers.
33
+
34
+ Create and interact with AI agents that can use tools.
35
+
36
+ Quick start:
37
+
38
+ \b
39
+ supyagent new myagent # Create an agent
40
+ supyagent chat myagent # Start chatting
41
+ """
42
+ pass
43
+
44
+
45
+ @cli.command()
46
+ @click.argument("name")
47
+ @click.option(
48
+ "--type",
49
+ "-t",
50
+ "agent_type",
51
+ type=click.Choice(["interactive", "execution"]),
52
+ default="interactive",
53
+ help="Type of agent to create",
54
+ )
55
+ def new(name: str, agent_type: str):
56
+ """
57
+ Create a new agent from template.
58
+
59
+ NAME is the agent name (will create agents/NAME.yaml)
60
+ """
61
+ agents_dir = Path("agents")
62
+ agents_dir.mkdir(exist_ok=True)
63
+
64
+ agent_path = agents_dir / f"{name}.yaml"
65
+
66
+ if agent_path.exists():
67
+ if not click.confirm(f"Agent '{name}' already exists. Overwrite?"):
68
+ return
69
+
70
+ # Create template based on type
71
+ if agent_type == "interactive":
72
+ template = f"""name: {name}
73
+ description: An interactive AI assistant
74
+ version: "1.0"
75
+ type: interactive
76
+
77
+ model:
78
+ provider: anthropic/claude-3-5-sonnet-20241022
79
+ temperature: 0.7
80
+ max_tokens: 4096
81
+
82
+ system_prompt: |
83
+ You are a helpful AI assistant named {name}.
84
+
85
+ You have access to various tools via supypowers. Use them when needed
86
+ to help accomplish tasks.
87
+
88
+ Be concise, helpful, and accurate.
89
+
90
+ tools:
91
+ allow:
92
+ - "*" # Allow all tools (customize as needed)
93
+
94
+ limits:
95
+ max_tool_calls_per_turn: 10
96
+ """
97
+ else:
98
+ template = f"""name: {name}
99
+ description: An execution agent for automated tasks
100
+ version: "1.0"
101
+ type: execution
102
+
103
+ model:
104
+ provider: anthropic/claude-3-5-sonnet-20241022
105
+ temperature: 0.3 # Lower temperature for consistency
106
+ max_tokens: 2048
107
+
108
+ system_prompt: |
109
+ You are a task execution agent. Process the input and produce the output.
110
+ Be precise and follow instructions exactly.
111
+ Do not engage in conversation - just output the result.
112
+
113
+ tools:
114
+ allow: [] # Execution agents often don't need tools
115
+
116
+ limits:
117
+ max_tool_calls_per_turn: 5
118
+ """
119
+
120
+ agent_path.write_text(template)
121
+
122
+ console.print(f"[green]✓[/green] Created agent: [cyan]{agent_path}[/cyan]")
123
+ console.print()
124
+ console.print("Next steps:")
125
+ console.print(f" 1. Edit [cyan]{agent_path}[/cyan] to customize")
126
+ console.print(f" 2. Run: [cyan]supyagent chat {name}[/cyan]")
127
+
128
+
129
+ @cli.command("list")
130
+ def list_agents():
131
+ """List all available agents."""
132
+ agents_dir = Path("agents")
133
+
134
+ if not agents_dir.exists():
135
+ console.print("[yellow]No agents directory found.[/yellow]")
136
+ console.print("Create an agent with: [cyan]supyagent new <name>[/cyan]")
137
+ return
138
+
139
+ yaml_files = sorted(agents_dir.glob("*.yaml"))
140
+
141
+ if not yaml_files:
142
+ console.print("[yellow]No agents found.[/yellow]")
143
+ console.print("Create an agent with: [cyan]supyagent new <name>[/cyan]")
144
+ return
145
+
146
+ console.print("[bold]Available agents:[/bold]\n")
147
+
148
+ for yaml_file in yaml_files:
149
+ name = yaml_file.stem
150
+ try:
151
+ config = load_agent_config(name)
152
+ agent_type = f"[dim]({config.type})[/dim]"
153
+ desc = (
154
+ config.description[:50] + "..."
155
+ if len(config.description) > 50
156
+ else config.description
157
+ )
158
+ console.print(f" [cyan]{name}[/cyan] {agent_type}")
159
+ if desc:
160
+ console.print(f" [dim]{desc}[/dim]")
161
+ except Exception as e:
162
+ console.print(f" [red]{name}[/red] [dim](invalid: {e})[/dim]")
163
+
164
+
165
+ @cli.command()
166
+ @click.argument("agent_name")
167
+ @click.option("--new", "new_session", is_flag=True, help="Start a new session")
168
+ @click.option("--session", "session_id", help="Resume a specific session by ID")
169
+ def chat(agent_name: str, new_session: bool, session_id: str | None):
170
+ """
171
+ Start an interactive chat session with an agent.
172
+
173
+ AGENT_NAME is the name of the agent to chat with.
174
+
175
+ By default, resumes the most recent session. Use --new to start fresh,
176
+ or --session <id> to resume a specific session.
177
+ """
178
+ # Load agent config
179
+ try:
180
+ config = load_agent_config(agent_name)
181
+ except AgentNotFoundError as e:
182
+ console.print(f"[red]Error:[/red] {e}")
183
+ console.print(f"\nAvailable agents:")
184
+ agents_dir = Path("agents")
185
+ if agents_dir.exists():
186
+ for f in agents_dir.glob("*.yaml"):
187
+ console.print(f" - {f.stem}")
188
+ else:
189
+ console.print(
190
+ " [dim](none - create one with 'supyagent new <name>')[/dim]"
191
+ )
192
+ sys.exit(1)
193
+
194
+ # Initialize session manager
195
+ session_mgr = SessionManager()
196
+
197
+ # Determine which session to use
198
+ session = None
199
+ if session_id:
200
+ # Resume specific session
201
+ session = session_mgr.load_session(agent_name, session_id)
202
+ if not session:
203
+ console.print(f"[red]Error:[/red] Session '{session_id}' not found")
204
+ console.print("\nAvailable sessions:")
205
+ for s in session_mgr.list_sessions(agent_name):
206
+ console.print(f" - {s.session_id}: {s.title or '(untitled)'}")
207
+ sys.exit(1)
208
+ elif not new_session:
209
+ # Try to resume current session
210
+ session = session_mgr.get_current_session(agent_name)
211
+
212
+ # Initialize agent
213
+ try:
214
+ agent = Agent(config, session=session, session_manager=session_mgr)
215
+ except Exception as e:
216
+ console.print(f"[red]Error initializing agent:[/red] {e}")
217
+ sys.exit(1)
218
+
219
+ # Print welcome
220
+ console.print()
221
+
222
+ if session and session.messages:
223
+ # Resuming existing session
224
+ console.print(
225
+ Panel(
226
+ f"[bold]Resuming session[/bold] [cyan]{agent.session.meta.session_id}[/cyan]\n\n"
227
+ f"[dim]{len(session.messages)} messages in history[/dim]\n"
228
+ f"Model: [cyan]{config.model.provider}[/cyan]\n\n"
229
+ f"[dim]Type /help for commands, /quit to exit[/dim]",
230
+ title=f"💬 {config.name}",
231
+ border_style="cyan",
232
+ )
233
+ )
234
+ else:
235
+ # New session
236
+ console.print(
237
+ Panel(
238
+ f"[bold]New session[/bold] [cyan]{agent.session.meta.session_id}[/cyan]\n\n"
239
+ f"[dim]{config.description or 'No description'}[/dim]\n"
240
+ f"Model: [cyan]{config.model.provider}[/cyan]\n"
241
+ f"Tools: [cyan]{len(agent.tools)} available[/cyan]\n\n"
242
+ f"[dim]Type /help for commands, /quit to exit[/dim]",
243
+ title=f"💬 {config.name}",
244
+ border_style="green",
245
+ )
246
+ )
247
+ console.print()
248
+
249
+ # Chat loop
250
+ while True:
251
+ try:
252
+ # Get user input
253
+ user_input = console.input("[bold blue]You>[/bold blue] ")
254
+
255
+ if not user_input.strip():
256
+ continue
257
+
258
+ # Handle commands
259
+ if user_input.startswith("/"):
260
+ cmd_parts = user_input[1:].split()
261
+ cmd = cmd_parts[0].lower() if cmd_parts else ""
262
+
263
+ if cmd in ("quit", "exit", "q"):
264
+ console.print("[dim]Goodbye![/dim]")
265
+ break
266
+
267
+ elif cmd in ("help", "h", "?"):
268
+ console.print(
269
+ "\n[bold]Available commands:[/bold]\n"
270
+ " /help Show this help\n"
271
+ " /tools List available tools\n"
272
+ " /creds [action] Manage credentials (list|set|delete)\n"
273
+ " /sessions List all sessions\n"
274
+ " /session <id> Switch to another session\n"
275
+ " /new Start a new session\n"
276
+ " /history [n] Show last n messages (default: 10)\n"
277
+ " /export [file] Export conversation to markdown\n"
278
+ " /model [name] Show or change model\n"
279
+ " /clear Clear screen\n"
280
+ " /quit Exit the chat\n"
281
+ )
282
+ continue
283
+
284
+ elif cmd == "tools":
285
+ tools = agent.get_available_tools()
286
+ if tools:
287
+ console.print("\n[bold]Available tools:[/bold]")
288
+ for tool in tools:
289
+ console.print(f" - {tool}")
290
+ console.print()
291
+ else:
292
+ console.print("[dim]No tools available[/dim]")
293
+ continue
294
+
295
+ elif cmd == "sessions":
296
+ sessions = session_mgr.list_sessions(agent_name)
297
+ if not sessions:
298
+ console.print("[dim]No sessions found[/dim]")
299
+ else:
300
+ table = Table(title="Sessions")
301
+ table.add_column("ID", style="cyan")
302
+ table.add_column("Title")
303
+ table.add_column("Updated", style="dim")
304
+ table.add_column("", style="green")
305
+
306
+ current_id = agent.session.meta.session_id
307
+ for s in sessions:
308
+ marker = "← current" if s.session_id == current_id else ""
309
+ title = s.title or "(untitled)"
310
+ updated = s.updated_at.strftime("%Y-%m-%d %H:%M")
311
+ table.add_row(s.session_id, title, updated, marker)
312
+
313
+ console.print(table)
314
+ continue
315
+
316
+ elif cmd == "session":
317
+ if len(cmd_parts) < 2:
318
+ console.print("[yellow]Usage: /session <id>[/yellow]")
319
+ continue
320
+
321
+ target_id = cmd_parts[1]
322
+ new_sess = session_mgr.load_session(agent_name, target_id)
323
+ if not new_sess:
324
+ console.print(f"[red]Session '{target_id}' not found[/red]")
325
+ continue
326
+
327
+ # Reinitialize agent with new session
328
+ agent = Agent(config, session=new_sess, session_manager=session_mgr)
329
+ session_mgr._set_current(agent_name, target_id)
330
+ console.print(f"[green]Switched to session {target_id}[/green]")
331
+ continue
332
+
333
+ elif cmd == "new":
334
+ # Start a fresh session
335
+ agent = Agent(config, session=None, session_manager=session_mgr)
336
+ console.print(
337
+ f"[green]Started new session {agent.session.meta.session_id}[/green]"
338
+ )
339
+ continue
340
+
341
+ elif cmd == "history":
342
+ n = 10
343
+ if len(cmd_parts) > 1:
344
+ try:
345
+ n = int(cmd_parts[1])
346
+ except ValueError:
347
+ pass
348
+
349
+ messages = agent.session.messages[-n:]
350
+ if not messages:
351
+ console.print("[dim]No messages in history[/dim]")
352
+ else:
353
+ console.print(f"\n[bold]Last {len(messages)} messages:[/bold]")
354
+ for msg in messages:
355
+ if msg.type == "user":
356
+ preview = (msg.content or "")[:80]
357
+ if len(msg.content or "") > 80:
358
+ preview += "..."
359
+ console.print(f" [blue]You:[/blue] {preview}")
360
+ elif msg.type == "assistant":
361
+ preview = (msg.content or "")[:80]
362
+ if len(msg.content or "") > 80:
363
+ preview += "..."
364
+ console.print(
365
+ f" [green]{config.name}:[/green] {preview}"
366
+ )
367
+ elif msg.type == "tool_result":
368
+ console.print(f" [dim]Tool: {msg.name}[/dim]")
369
+ console.print()
370
+ continue
371
+
372
+ elif cmd == "clear":
373
+ console.clear()
374
+ continue
375
+
376
+ elif cmd == "creds":
377
+ action = cmd_parts[1] if len(cmd_parts) > 1 else "list"
378
+ cred_name = cmd_parts[2] if len(cmd_parts) > 2 else None
379
+
380
+ if action == "list":
381
+ creds = agent.credential_manager.list_credentials(agent_name)
382
+ if not creds:
383
+ console.print("[dim]No stored credentials[/dim]")
384
+ else:
385
+ console.print("\n[bold]Stored credentials:[/bold]")
386
+ for c in creds:
387
+ console.print(f" - {c}")
388
+ console.print()
389
+ elif action == "set" and cred_name:
390
+ result = agent.credential_manager.prompt_for_credential(
391
+ cred_name, "Manually setting credential"
392
+ )
393
+ if result:
394
+ value, persist = result
395
+ agent.credential_manager.set(
396
+ agent_name, cred_name, value, persist
397
+ )
398
+ console.print(
399
+ f"[green]Credential {cred_name} saved[/green]"
400
+ )
401
+ else:
402
+ console.print("[dim]Cancelled[/dim]")
403
+ elif action == "delete" and cred_name:
404
+ if agent.credential_manager.delete(agent_name, cred_name):
405
+ console.print(
406
+ f"[green]Credential {cred_name} deleted[/green]"
407
+ )
408
+ else:
409
+ console.print(
410
+ f"[yellow]Credential {cred_name} not found[/yellow]"
411
+ )
412
+ else:
413
+ console.print(
414
+ "[yellow]Usage: /creds [list|set|delete] [name][/yellow]"
415
+ )
416
+ continue
417
+
418
+ elif cmd == "export":
419
+ filename = (
420
+ cmd_parts[1]
421
+ if len(cmd_parts) > 1
422
+ else f"{agent_name}_{agent.session.meta.session_id}.md"
423
+ )
424
+
425
+ lines = [f"# Conversation with {config.name}", ""]
426
+ for msg in agent.session.messages:
427
+ if msg.type == "user":
428
+ lines.append(f"**You:** {msg.content}\n")
429
+ elif msg.type == "assistant":
430
+ lines.append(f"**{config.name}:** {msg.content}\n")
431
+
432
+ with open(filename, "w") as f:
433
+ f.write("\n".join(lines))
434
+ console.print(f"[green]Exported to {filename}[/green]")
435
+ continue
436
+
437
+ elif cmd == "model":
438
+ if len(cmd_parts) > 1:
439
+ new_model = cmd_parts[1]
440
+ agent.llm.change_model(new_model)
441
+ console.print(f"[green]Model changed to {new_model}[/green]")
442
+ else:
443
+ console.print(f"Current model: [cyan]{agent.llm.model}[/cyan]")
444
+ continue
445
+
446
+ else:
447
+ console.print(f"[yellow]Unknown command: /{cmd}[/yellow]")
448
+ continue
449
+
450
+ # Send message to agent
451
+ with console.status("[bold green]Thinking...[/bold green]"):
452
+ try:
453
+ response = agent.send_message(user_input)
454
+ except Exception as e:
455
+ console.print(f"\n[red]Error:[/red] {e}\n")
456
+ continue
457
+
458
+ # Display response
459
+ console.print()
460
+ console.print(f"[bold green]{config.name}>[/bold green]")
461
+ console.print(Markdown(response))
462
+ console.print()
463
+
464
+ except KeyboardInterrupt:
465
+ console.print("\n[dim]Use /quit to exit[/dim]")
466
+ except EOFError:
467
+ console.print("\n[dim]Goodbye![/dim]")
468
+ break
469
+
470
+
471
+ @cli.command()
472
+ @click.argument("agent_name")
473
+ def sessions(agent_name: str):
474
+ """List all sessions for an agent."""
475
+ session_mgr = SessionManager()
476
+ session_list = session_mgr.list_sessions(agent_name)
477
+
478
+ if not session_list:
479
+ console.print(f"[dim]No sessions found for '{agent_name}'[/dim]")
480
+ console.print(
481
+ f"\nStart a session with: [cyan]supyagent chat {agent_name}[/cyan]"
482
+ )
483
+ return
484
+
485
+ # Get current session
486
+ current = session_mgr.get_current_session(agent_name)
487
+ current_id = current.meta.session_id if current else None
488
+
489
+ table = Table(title=f"Sessions for {agent_name}")
490
+ table.add_column("ID", style="cyan")
491
+ table.add_column("Title")
492
+ table.add_column("Created", style="dim")
493
+ table.add_column("Updated", style="dim")
494
+ table.add_column("", style="green")
495
+
496
+ for s in session_list:
497
+ marker = "← current" if s.session_id == current_id else ""
498
+ title = s.title or "(untitled)"
499
+ created = s.created_at.strftime("%Y-%m-%d %H:%M")
500
+ updated = s.updated_at.strftime("%Y-%m-%d %H:%M")
501
+ table.add_row(s.session_id, title, created, updated, marker)
502
+
503
+ console.print(table)
504
+ console.print()
505
+ console.print(
506
+ "[dim]Resume a session:[/dim] supyagent chat " + agent_name + " --session <id>"
507
+ )
508
+
509
+
510
+ @cli.command()
511
+ @click.argument("agent_name")
512
+ def show(agent_name: str):
513
+ """Show details about an agent."""
514
+ try:
515
+ config = load_agent_config(agent_name)
516
+ except AgentNotFoundError as e:
517
+ console.print(f"[red]Error:[/red] {e}")
518
+ sys.exit(1)
519
+
520
+ console.print(f"\n[bold cyan]{config.name}[/bold cyan] v{config.version}")
521
+ console.print(f"[dim]{config.description}[/dim]\n")
522
+
523
+ console.print(f"[bold]Type:[/bold] {config.type}")
524
+ console.print(f"[bold]Model:[/bold] {config.model.provider}")
525
+ console.print(f"[bold]Temperature:[/bold] {config.model.temperature}")
526
+ console.print(f"[bold]Max Tokens:[/bold] {config.model.max_tokens}")
527
+
528
+ if config.tools.allow:
529
+ console.print(f"\n[bold]Allowed Tools:[/bold]")
530
+ for pattern in config.tools.allow:
531
+ console.print(f" - {pattern}")
532
+
533
+ if config.tools.deny:
534
+ console.print(f"\n[bold]Denied Tools:[/bold]")
535
+ for pattern in config.tools.deny:
536
+ console.print(f" - {pattern}")
537
+
538
+ if config.delegates:
539
+ console.print(f"\n[bold]Delegates:[/bold]")
540
+ for delegate in config.delegates:
541
+ console.print(f" - {delegate}")
542
+
543
+ console.print(f"\n[bold]System Prompt:[/bold]")
544
+ console.print(Panel(config.system_prompt, border_style="dim"))
545
+
546
+
547
+ def parse_secrets(secrets: tuple[str, ...]) -> dict[str, str]:
548
+ """
549
+ Parse secrets from KEY=VALUE pairs or .env files.
550
+
551
+ Args:
552
+ secrets: Tuple of "KEY=VALUE" strings or file paths
553
+
554
+ Returns:
555
+ Dict of secret key -> value
556
+ """
557
+ result: dict[str, str] = {}
558
+
559
+ for secret in secrets:
560
+ if "=" in secret:
561
+ # KEY=VALUE format
562
+ key, value = secret.split("=", 1)
563
+ result[key.strip()] = value
564
+ elif os.path.isfile(secret):
565
+ # .env file format
566
+ with open(secret) as f:
567
+ for line in f:
568
+ line = line.strip()
569
+ if line and not line.startswith("#") and "=" in line:
570
+ key, value = line.split("=", 1)
571
+ result[key.strip()] = value.strip()
572
+
573
+ return result
574
+
575
+
576
+ @cli.command()
577
+ @click.argument("agent_name")
578
+ @click.argument("task", required=False)
579
+ @click.option(
580
+ "--input",
581
+ "-i",
582
+ "input_file",
583
+ type=click.Path(),
584
+ help="Read task from file (use '-' for stdin)",
585
+ )
586
+ @click.option(
587
+ "--output",
588
+ "-o",
589
+ "output_format",
590
+ type=click.Choice(["raw", "json", "markdown"]),
591
+ default="raw",
592
+ help="Output format",
593
+ )
594
+ @click.option(
595
+ "--secrets",
596
+ "-s",
597
+ multiple=True,
598
+ help="Secrets as KEY=VALUE or path to .env file",
599
+ )
600
+ @click.option(
601
+ "--quiet",
602
+ "-q",
603
+ is_flag=True,
604
+ help="Only output the result, no status messages",
605
+ )
606
+ def run(
607
+ agent_name: str,
608
+ task: str | None,
609
+ input_file: str | None,
610
+ output_format: str,
611
+ secrets: tuple[str, ...],
612
+ quiet: bool,
613
+ ):
614
+ """
615
+ Run an agent in execution mode (non-interactive).
616
+
617
+ AGENT_NAME is the agent to run.
618
+ TASK is the task description or JSON input (optional if using --input or stdin).
619
+
620
+ \b
621
+ Examples:
622
+ supyagent run summarizer "Summarize this text..."
623
+ supyagent run summarizer --input document.txt
624
+ supyagent run summarizer --input document.txt --output json
625
+ echo "text" | supyagent run summarizer
626
+ supyagent run api-caller '{"endpoint": "/users"}' --secrets API_KEY=xxx
627
+ """
628
+ # Load agent config
629
+ try:
630
+ config = load_agent_config(agent_name)
631
+ except AgentNotFoundError as e:
632
+ console.print(f"[red]Error:[/red] {e}", err=True)
633
+ sys.exit(1)
634
+
635
+ # Warn if using interactive agent in execution mode
636
+ if config.type != "execution" and not quiet:
637
+ console.print(
638
+ f"[yellow]Note:[/yellow] '{agent_name}' is an interactive agent. "
639
+ "Consider using 'chat' for interactive use.",
640
+ err=True,
641
+ )
642
+
643
+ # Parse secrets
644
+ secrets_dict = parse_secrets(secrets)
645
+
646
+ # Get task content
647
+ task_content: str | dict[str, Any]
648
+
649
+ if input_file:
650
+ if input_file == "-":
651
+ task_content = sys.stdin.read().strip()
652
+ else:
653
+ input_path = Path(input_file)
654
+ if not input_path.exists():
655
+ console.print(
656
+ f"[red]Error:[/red] File not found: {input_file}", err=True
657
+ )
658
+ sys.exit(1)
659
+ task_content = input_path.read_text().strip()
660
+ elif task:
661
+ # Try to parse as JSON, otherwise use as string
662
+ try:
663
+ task_content = json.loads(task)
664
+ except json.JSONDecodeError:
665
+ task_content = task
666
+ else:
667
+ # Check if there's stdin input
668
+ if not sys.stdin.isatty():
669
+ task_content = sys.stdin.read().strip()
670
+ else:
671
+ console.print(
672
+ "[red]Error:[/red] No task provided. "
673
+ "Use positional argument, --input, or pipe to stdin.",
674
+ err=True,
675
+ )
676
+ sys.exit(1)
677
+
678
+ if not task_content:
679
+ console.print("[red]Error:[/red] Empty task", err=True)
680
+ sys.exit(1)
681
+
682
+ # Run the agent
683
+ runner = ExecutionRunner(config)
684
+
685
+ if not quiet:
686
+ console.print(f"[dim]Running {agent_name}...[/dim]", err=True)
687
+
688
+ result = runner.run(task_content, secrets=secrets_dict, output_format=output_format)
689
+
690
+ # Output result
691
+ if output_format == "json":
692
+ click.echo(json.dumps(result, indent=2))
693
+ elif result["ok"]:
694
+ click.echo(result["data"])
695
+ else:
696
+ console.print(f"[red]Error:[/red] {result['error']}", err=True)
697
+ sys.exit(1)
698
+
699
+
700
+ @cli.command()
701
+ @click.argument("agent_name")
702
+ @click.argument("input_file", type=click.Path(exists=True))
703
+ @click.option(
704
+ "--output",
705
+ "-o",
706
+ "output_file",
707
+ type=click.Path(),
708
+ help="Output file (default: stdout)",
709
+ )
710
+ @click.option(
711
+ "--format",
712
+ "-f",
713
+ "input_format",
714
+ type=click.Choice(["jsonl", "csv"]),
715
+ default="jsonl",
716
+ help="Input file format",
717
+ )
718
+ @click.option(
719
+ "--secrets",
720
+ "-s",
721
+ multiple=True,
722
+ help="Secrets as KEY=VALUE or path to .env file",
723
+ )
724
+ def batch(
725
+ agent_name: str,
726
+ input_file: str,
727
+ output_file: str | None,
728
+ input_format: str,
729
+ secrets: tuple[str, ...],
730
+ ):
731
+ """
732
+ Run an agent on multiple inputs from a file.
733
+
734
+ \b
735
+ Input formats:
736
+ - jsonl: One JSON object per line
737
+ - csv: CSV with headers, each row becomes a dict
738
+
739
+ \b
740
+ Examples:
741
+ supyagent batch summarizer inputs.jsonl
742
+ supyagent batch summarizer inputs.jsonl --output results.jsonl
743
+ supyagent batch summarizer data.csv --format csv
744
+ """
745
+ # Load agent config
746
+ try:
747
+ config = load_agent_config(agent_name)
748
+ except AgentNotFoundError as e:
749
+ console.print(f"[red]Error:[/red] {e}", err=True)
750
+ sys.exit(1)
751
+
752
+ # Parse secrets
753
+ secrets_dict = parse_secrets(secrets)
754
+
755
+ # Load inputs
756
+ inputs: list[dict[str, Any] | str] = []
757
+
758
+ if input_format == "jsonl":
759
+ with open(input_file) as f:
760
+ for line in f:
761
+ line = line.strip()
762
+ if line:
763
+ try:
764
+ inputs.append(json.loads(line))
765
+ except json.JSONDecodeError:
766
+ inputs.append(line)
767
+ elif input_format == "csv":
768
+ import csv
769
+
770
+ with open(input_file) as f:
771
+ reader = csv.DictReader(f)
772
+ inputs = list(reader)
773
+
774
+ if not inputs:
775
+ console.print("[yellow]No inputs found in file[/yellow]")
776
+ return
777
+
778
+ # Process
779
+ runner = ExecutionRunner(config)
780
+ results: list[dict[str, Any]] = []
781
+
782
+ with Progress(
783
+ SpinnerColumn(),
784
+ TextColumn("[progress.description]{task.description}"),
785
+ console=console,
786
+ ) as progress:
787
+ task = progress.add_task(
788
+ f"Processing {len(inputs)} items...", total=len(inputs)
789
+ )
790
+
791
+ for item in inputs:
792
+ result = runner.run(item, secrets=secrets_dict, output_format="json")
793
+ results.append(result)
794
+ progress.advance(task)
795
+
796
+ # Count successes/failures
797
+ successes = sum(1 for r in results if r["ok"])
798
+ failures = len(results) - successes
799
+
800
+ # Output
801
+ output_content = "\n".join(json.dumps(r) for r in results)
802
+
803
+ if output_file:
804
+ with open(output_file, "w") as f:
805
+ f.write(output_content + "\n")
806
+ console.print(
807
+ f"[green]✓[/green] Processed {len(results)} items "
808
+ f"({successes} succeeded, {failures} failed)"
809
+ )
810
+ console.print(f" Results written to [cyan]{output_file}[/cyan]")
811
+ else:
812
+ click.echo(output_content)
813
+
814
+
815
+ @cli.command()
816
+ def agents():
817
+ """List all registered agent instances."""
818
+ registry = AgentRegistry()
819
+ instances = registry.list_all()
820
+
821
+ if not instances:
822
+ console.print("[dim]No active agent instances[/dim]")
823
+ return
824
+
825
+ # Build a table
826
+ table = Table(title="Agent Instances")
827
+ table.add_column("ID", style="cyan")
828
+ table.add_column("Agent")
829
+ table.add_column("Status")
830
+ table.add_column("Parent")
831
+ table.add_column("Created")
832
+
833
+ for inst in instances:
834
+ status_style = {
835
+ "active": "green",
836
+ "completed": "dim",
837
+ "failed": "red",
838
+ }.get(inst.status, "")
839
+
840
+ parent = inst.parent_id if inst.parent_id else "-"
841
+ created = inst.created_at.strftime("%Y-%m-%d %H:%M")
842
+
843
+ table.add_row(
844
+ inst.instance_id,
845
+ inst.name,
846
+ f"[{status_style}]{inst.status}[/{status_style}]",
847
+ parent,
848
+ created,
849
+ )
850
+
851
+ console.print(table)
852
+
853
+
854
+ @cli.command()
855
+ @click.argument("task")
856
+ @click.option(
857
+ "--planner",
858
+ "-p",
859
+ default="planner",
860
+ help="Planning agent to use (default: planner)",
861
+ )
862
+ @click.option(
863
+ "--new",
864
+ "-n",
865
+ "new_session",
866
+ is_flag=True,
867
+ help="Start a new session",
868
+ )
869
+ def plan(task: str, planner: str, new_session: bool):
870
+ """
871
+ Run a task through the planning agent for orchestration.
872
+
873
+ The planning agent will break down the task and delegate to
874
+ specialist agents as needed.
875
+
876
+ \b
877
+ Examples:
878
+ supyagent plan "Build a web scraper for news articles"
879
+ supyagent plan "Create a Python library for data validation"
880
+ supyagent plan "Write a blog post about AI" --planner my-planner
881
+ """
882
+ # Load planner config
883
+ try:
884
+ config = load_agent_config(planner)
885
+ except AgentNotFoundError as e:
886
+ console.print(f"[red]Error:[/red] {e}")
887
+ return
888
+
889
+ if not config.delegates:
890
+ console.print(
891
+ f"[yellow]Warning:[/yellow] Agent '{planner}' has no delegates configured. "
892
+ "It will handle the task directly."
893
+ )
894
+
895
+ # Show plan info
896
+ console.print(
897
+ Panel(
898
+ f"[bold]Planning Agent:[/bold] {planner}\n"
899
+ f"[bold]Delegates:[/bold] {', '.join(config.delegates) if config.delegates else 'None'}\n"
900
+ f"[bold]Task:[/bold] {task}",
901
+ title="🎯 Plan Execution",
902
+ border_style="blue",
903
+ )
904
+ )
905
+ console.print()
906
+
907
+ # Create agent with registry for tracking
908
+ registry = AgentRegistry()
909
+ agent = Agent(config, registry=registry)
910
+
911
+ # Execute the task
912
+ try:
913
+ response = agent.send_message(task)
914
+ console.print(Markdown(response))
915
+ except KeyboardInterrupt:
916
+ console.print("\n[yellow]Cancelled[/yellow]")
917
+ except Exception as e:
918
+ console.print(f"[red]Error:[/red] {e}")
919
+
920
+ # Show summary of agent activity
921
+ children = registry.list_children(agent.instance_id) if agent.instance_id else []
922
+ if children:
923
+ console.print()
924
+ console.print(
925
+ Panel(
926
+ "\n".join(f"• {c.name} [{c.status}]" for c in children),
927
+ title="Delegated Agents",
928
+ border_style="dim",
929
+ )
930
+ )
931
+
932
+
933
+ @cli.command()
934
+ def cleanup():
935
+ """Clean up completed/failed agent instances from the registry."""
936
+ registry = AgentRegistry()
937
+ count = registry.cleanup_completed()
938
+
939
+ if count == 0:
940
+ console.print("[dim]No instances to clean up[/dim]")
941
+ else:
942
+ console.print(f"[green]✓[/green] Cleaned up {count} instance(s)")
943
+
944
+
945
+ if __name__ == "__main__":
946
+ cli()