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/__init__.py +5 -0
- supyagent/__main__.py +8 -0
- supyagent/cli/__init__.py +1 -0
- supyagent/cli/main.py +946 -0
- supyagent/core/__init__.py +21 -0
- supyagent/core/agent.py +379 -0
- supyagent/core/context.py +158 -0
- supyagent/core/credentials.py +275 -0
- supyagent/core/delegation.py +286 -0
- supyagent/core/executor.py +232 -0
- supyagent/core/llm.py +73 -0
- supyagent/core/registry.py +238 -0
- supyagent/core/session_manager.py +233 -0
- supyagent/core/tools.py +235 -0
- supyagent/models/__init__.py +6 -0
- supyagent/models/agent_config.py +86 -0
- supyagent/models/session.py +43 -0
- supyagent/utils/__init__.py +1 -0
- supyagent/utils/paths.py +31 -0
- supyagent-0.1.0.dist-info/METADATA +328 -0
- supyagent-0.1.0.dist-info/RECORD +24 -0
- supyagent-0.1.0.dist-info/WHEEL +4 -0
- supyagent-0.1.0.dist-info/entry_points.txt +2 -0
- supyagent-0.1.0.dist-info/licenses/LICENSE +21 -0
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()
|