supyagent 0.1.0__py3-none-any.whl → 0.2.1__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 CHANGED
@@ -17,16 +17,18 @@ from rich.table import Table
17
17
  from typing import Any
18
18
 
19
19
  from supyagent.core.agent import Agent
20
+ from supyagent.core.config import ConfigManager, load_config
20
21
  from supyagent.core.executor import ExecutionRunner
21
22
  from supyagent.core.registry import AgentRegistry
22
23
  from supyagent.core.session_manager import SessionManager
24
+ from supyagent.default_tools import install_default_tools, list_default_tools
23
25
  from supyagent.models.agent_config import AgentNotFoundError, load_agent_config
24
26
 
25
27
  console = Console()
26
28
 
27
29
 
28
30
  @click.group()
29
- @click.version_option(version="0.1.0", prog_name="supyagent")
31
+ @click.version_option(version="0.2.1", prog_name="supyagent")
30
32
  def cli():
31
33
  """
32
34
  Supyagent - LLM agents powered by supypowers.
@@ -36,12 +38,82 @@ def cli():
36
38
  Quick start:
37
39
 
38
40
  \b
41
+ supyagent init # Set up default tools
42
+ supyagent config set # Configure API keys
39
43
  supyagent new myagent # Create an agent
40
44
  supyagent chat myagent # Start chatting
41
45
  """
42
46
  pass
43
47
 
44
48
 
49
+ @cli.command()
50
+ @click.option(
51
+ "--tools-dir",
52
+ "-t",
53
+ default="supypowers",
54
+ help="Directory for tools (default: supypowers/)",
55
+ )
56
+ @click.option(
57
+ "--force",
58
+ "-f",
59
+ is_flag=True,
60
+ help="Overwrite existing files",
61
+ )
62
+ def init(tools_dir: str, force: bool):
63
+ """
64
+ Initialize supyagent in the current directory.
65
+
66
+ This sets up:
67
+ - Default tools in supypowers/ (shell commands, file operations)
68
+ - agents/ directory for agent configurations
69
+
70
+ \b
71
+ Examples:
72
+ supyagent init
73
+ supyagent init --tools-dir my_tools
74
+ """
75
+ console.print("[bold]Initializing supyagent...[/bold]")
76
+ console.print()
77
+
78
+ # Create agents directory
79
+ agents_dir = Path("agents")
80
+ if not agents_dir.exists():
81
+ agents_dir.mkdir(parents=True)
82
+ console.print(f" [green]✓[/green] Created {agents_dir}/")
83
+ else:
84
+ console.print(f" [dim]○[/dim] {agents_dir}/ already exists")
85
+
86
+ # Install default tools
87
+ tools_path = Path(tools_dir)
88
+
89
+ if force:
90
+ # Remove and reinstall
91
+ import shutil
92
+
93
+ if tools_path.exists():
94
+ shutil.rmtree(tools_path)
95
+
96
+ if tools_path.exists() and any(tools_path.glob("*.py")):
97
+ console.print(f" [dim]○[/dim] {tools_dir}/ already has tools")
98
+ else:
99
+ count = install_default_tools(tools_path)
100
+ console.print(
101
+ f" [green]✓[/green] Installed {count} default tools to {tools_dir}/"
102
+ )
103
+
104
+ # Show available tools
105
+ console.print()
106
+ console.print("[bold]Available tools:[/bold]")
107
+ for tool in list_default_tools():
108
+ console.print(f" • [cyan]{tool['name']}[/cyan]: {tool['description']}")
109
+
110
+ console.print()
111
+ console.print("[bold]Next steps:[/bold]")
112
+ console.print(" 1. Configure your API key: [cyan]supyagent config set[/cyan]")
113
+ console.print(" 2. Create an agent: [cyan]supyagent new myagent[/cyan]")
114
+ console.print(" 3. Start chatting: [cyan]supyagent chat myagent[/cyan]")
115
+
116
+
45
117
  @cli.command()
46
118
  @click.argument("name")
47
119
  @click.option(
@@ -175,6 +247,9 @@ def chat(agent_name: str, new_session: bool, session_id: str | None):
175
247
  By default, resumes the most recent session. Use --new to start fresh,
176
248
  or --session <id> to resume a specific session.
177
249
  """
250
+ # Load global config (API keys) into environment
251
+ load_config()
252
+
178
253
  # Load agent config
179
254
  try:
180
255
  config = load_agent_config(agent_name)
@@ -625,6 +700,9 @@ def run(
625
700
  echo "text" | supyagent run summarizer
626
701
  supyagent run api-caller '{"endpoint": "/users"}' --secrets API_KEY=xxx
627
702
  """
703
+ # Load global config (API keys) into environment
704
+ load_config()
705
+
628
706
  # Load agent config
629
707
  try:
630
708
  config = load_agent_config(agent_name)
@@ -742,6 +820,9 @@ def batch(
742
820
  supyagent batch summarizer inputs.jsonl --output results.jsonl
743
821
  supyagent batch summarizer data.csv --format csv
744
822
  """
823
+ # Load global config (API keys) into environment
824
+ load_config()
825
+
745
826
  # Load agent config
746
827
  try:
747
828
  config = load_agent_config(agent_name)
@@ -879,6 +960,9 @@ def plan(task: str, planner: str, new_session: bool):
879
960
  supyagent plan "Create a Python library for data validation"
880
961
  supyagent plan "Write a blog post about AI" --planner my-planner
881
962
  """
963
+ # Load global config (API keys) into environment
964
+ load_config()
965
+
882
966
  # Load planner config
883
967
  try:
884
968
  config = load_agent_config(planner)
@@ -942,5 +1026,171 @@ def cleanup():
942
1026
  console.print(f"[green]✓[/green] Cleaned up {count} instance(s)")
943
1027
 
944
1028
 
1029
+ # =============================================================================
1030
+ # Config Commands
1031
+ # =============================================================================
1032
+
1033
+
1034
+ @cli.group()
1035
+ def config():
1036
+ """Manage API keys and global configuration."""
1037
+ pass
1038
+
1039
+
1040
+ @config.command("set")
1041
+ @click.argument("key_name", required=False)
1042
+ @click.option(
1043
+ "--value",
1044
+ "-v",
1045
+ help="Set value directly (use with caution - visible in shell history)",
1046
+ )
1047
+ def config_set(key_name: str | None, value: str | None):
1048
+ """
1049
+ Set an API key.
1050
+
1051
+ If KEY_NAME is not provided, shows an interactive menu of common keys.
1052
+
1053
+ \b
1054
+ Examples:
1055
+ supyagent config set # Interactive menu
1056
+ supyagent config set OPENAI_API_KEY # Set specific key
1057
+ supyagent config set MY_KEY -v "value" # Set with value (not recommended)
1058
+ """
1059
+ config_mgr = ConfigManager()
1060
+
1061
+ if value:
1062
+ if not key_name:
1063
+ console.print("[red]Error:[/red] KEY_NAME required when using --value")
1064
+ sys.exit(1)
1065
+ config_mgr.set(key_name, value)
1066
+ console.print(f"[green]✓[/green] Saved {key_name}")
1067
+ else:
1068
+ config_mgr.set_interactive(key_name)
1069
+
1070
+
1071
+ @config.command("list")
1072
+ def config_list():
1073
+ """List all configured API keys."""
1074
+ config_mgr = ConfigManager()
1075
+ config_mgr.show_status()
1076
+
1077
+
1078
+ @config.command("delete")
1079
+ @click.argument("key_name")
1080
+ def config_delete(key_name: str):
1081
+ """Delete a stored API key."""
1082
+ config_mgr = ConfigManager()
1083
+
1084
+ if config_mgr.delete(key_name):
1085
+ console.print(f"[green]✓[/green] Deleted {key_name}")
1086
+ else:
1087
+ console.print(f"[yellow]Key not found:[/yellow] {key_name}")
1088
+
1089
+
1090
+ @config.command("import")
1091
+ @click.argument("file_path", type=click.Path(exists=True))
1092
+ @click.option(
1093
+ "--filter",
1094
+ "-f",
1095
+ "key_filter",
1096
+ help="Only import keys matching this prefix (e.g., 'OPENAI')",
1097
+ )
1098
+ def config_import(file_path: str, key_filter: str | None):
1099
+ """
1100
+ Import API keys from a .env file.
1101
+
1102
+ The file should contain KEY=VALUE pairs, one per line.
1103
+ Lines starting with # are ignored.
1104
+
1105
+ \b
1106
+ Examples:
1107
+ supyagent config import .env
1108
+ supyagent config import secrets.env --filter OPENAI
1109
+ """
1110
+ config_mgr = ConfigManager()
1111
+
1112
+ try:
1113
+ # If filter is specified, we need custom handling
1114
+ if key_filter:
1115
+ from pathlib import Path
1116
+ import re
1117
+
1118
+ path = Path(file_path)
1119
+ pattern = re.compile(r"^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.+)$")
1120
+ imported = 0
1121
+
1122
+ with open(path) as f:
1123
+ for line in f:
1124
+ line = line.strip()
1125
+ if not line or line.startswith("#"):
1126
+ continue
1127
+
1128
+ match = pattern.match(line)
1129
+ if match:
1130
+ name, value = match.groups()
1131
+ if name.startswith(key_filter.upper()):
1132
+ if (value.startswith('"') and value.endswith('"')) or (
1133
+ value.startswith("'") and value.endswith("'")
1134
+ ):
1135
+ value = value[1:-1]
1136
+ config_mgr.set(name, value)
1137
+ console.print(f" [green]✓[/green] {name}")
1138
+ imported += 1
1139
+ else:
1140
+ imported = config_mgr.set_from_file(file_path)
1141
+
1142
+ if imported == 0:
1143
+ console.print("[yellow]No keys found in file[/yellow]")
1144
+ else:
1145
+ console.print(f"\n[green]✓[/green] Imported {imported} key(s)")
1146
+
1147
+ except FileNotFoundError:
1148
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
1149
+ sys.exit(1)
1150
+ except Exception as e:
1151
+ console.print(f"[red]Error:[/red] {e}")
1152
+ sys.exit(1)
1153
+
1154
+
1155
+ @config.command("export")
1156
+ @click.argument("file_path", type=click.Path())
1157
+ @click.option("--force", "-f", is_flag=True, help="Overwrite existing file")
1158
+ def config_export(file_path: str, force: bool):
1159
+ """
1160
+ Export stored API keys to a .env file.
1161
+
1162
+ \b
1163
+ Example:
1164
+ supyagent config export backup.env
1165
+ """
1166
+ config_mgr = ConfigManager()
1167
+ path = Path(file_path)
1168
+
1169
+ if path.exists() and not force:
1170
+ console.print(f"[red]Error:[/red] File exists: {file_path}")
1171
+ console.print("Use --force to overwrite")
1172
+ sys.exit(1)
1173
+
1174
+ keys = config_mgr._load_keys()
1175
+
1176
+ if not keys:
1177
+ console.print("[yellow]No keys to export[/yellow]")
1178
+ return
1179
+
1180
+ with open(path, "w") as f:
1181
+ f.write("# Supyagent API Keys\n")
1182
+ f.write("# Generated export - keep this file secure!\n\n")
1183
+ for name, value in sorted(keys.items()):
1184
+ f.write(f"{name}={value}\n")
1185
+
1186
+ # Set restrictive permissions
1187
+ try:
1188
+ path.chmod(0o600)
1189
+ except OSError:
1190
+ pass
1191
+
1192
+ console.print(f"[green]✓[/green] Exported {len(keys)} key(s) to {file_path}")
1193
+
1194
+
945
1195
  if __name__ == "__main__":
946
1196
  cli()
@@ -1,6 +1,7 @@
1
1
  """Core module for supyagent."""
2
2
 
3
3
  from supyagent.core.agent import Agent
4
+ from supyagent.core.config import ConfigManager, load_config
4
5
  from supyagent.core.context import DelegationContext
5
6
  from supyagent.core.credentials import CredentialManager
6
7
  from supyagent.core.delegation import DelegationManager
@@ -12,10 +13,12 @@ from supyagent.core.session_manager import SessionManager
12
13
  __all__ = [
13
14
  "Agent",
14
15
  "AgentRegistry",
16
+ "ConfigManager",
15
17
  "CredentialManager",
16
18
  "DelegationContext",
17
19
  "DelegationManager",
18
20
  "ExecutionRunner",
19
21
  "LLMClient",
20
22
  "SessionManager",
23
+ "load_config",
21
24
  ]
@@ -0,0 +1,352 @@
1
+ """
2
+ Configuration manager for global settings like LLM API keys.
3
+
4
+ Stores encrypted configuration in ~/.supyagent/config/ that is shared
5
+ across all agents and projects.
6
+ """
7
+
8
+ import getpass
9
+ import json
10
+ import os
11
+ import re
12
+ from pathlib import Path
13
+
14
+ from cryptography.fernet import Fernet, InvalidToken
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.prompt import Confirm
18
+ from rich.table import Table
19
+
20
+ console = Console()
21
+
22
+ # Common LLM provider API key names
23
+ KNOWN_LLM_KEYS = {
24
+ "OPENAI_API_KEY": "OpenAI (GPT-4, GPT-3.5)",
25
+ "ANTHROPIC_API_KEY": "Anthropic (Claude)",
26
+ "GOOGLE_API_KEY": "Google (Gemini)",
27
+ "AZURE_API_KEY": "Azure OpenAI",
28
+ "AZURE_API_BASE": "Azure OpenAI endpoint",
29
+ "COHERE_API_KEY": "Cohere",
30
+ "HUGGINGFACE_API_KEY": "Hugging Face",
31
+ "REPLICATE_API_KEY": "Replicate",
32
+ "TOGETHER_API_KEY": "Together AI",
33
+ "GROQ_API_KEY": "Groq",
34
+ "MISTRAL_API_KEY": "Mistral AI",
35
+ "PERPLEXITY_API_KEY": "Perplexity AI",
36
+ "DEEPSEEK_API_KEY": "DeepSeek",
37
+ "FIREWORKS_API_KEY": "Fireworks AI",
38
+ "OLLAMA_API_BASE": "Ollama (local) base URL",
39
+ }
40
+
41
+
42
+ class ConfigManager:
43
+ """
44
+ Manages global configuration including LLM API keys.
45
+
46
+ Configuration is stored encrypted in ~/.supyagent/config/
47
+ and automatically loaded into environment variables when
48
+ agents are run.
49
+
50
+ Directory structure:
51
+ ~/.supyagent/config/.key # Encryption key
52
+ ~/.supyagent/config/keys.enc # Encrypted API keys
53
+ """
54
+
55
+ def __init__(self, base_dir: Path | None = None):
56
+ """
57
+ Initialize the config manager.
58
+
59
+ Args:
60
+ base_dir: Base directory for config storage.
61
+ Defaults to ~/.supyagent/config/
62
+ """
63
+ if base_dir is None:
64
+ base_dir = Path.home() / ".supyagent" / "config"
65
+
66
+ self.base_dir = base_dir
67
+ self.base_dir.mkdir(parents=True, exist_ok=True)
68
+ self._fernet = self._get_fernet()
69
+ self._cache: dict[str, str] | None = None
70
+
71
+ def _get_fernet(self) -> Fernet:
72
+ """Get or create the encryption key."""
73
+ key_file = self.base_dir / ".key"
74
+
75
+ if key_file.exists():
76
+ key = key_file.read_bytes()
77
+ else:
78
+ key = Fernet.generate_key()
79
+ key_file.write_bytes(key)
80
+ try:
81
+ key_file.chmod(0o600)
82
+ except OSError:
83
+ pass
84
+
85
+ return Fernet(key)
86
+
87
+ def _keys_path(self) -> Path:
88
+ """Get path to the encrypted keys file."""
89
+ return self.base_dir / "keys.enc"
90
+
91
+ def _load_keys(self) -> dict[str, str]:
92
+ """Load and decrypt stored keys."""
93
+ if self._cache is not None:
94
+ return self._cache
95
+
96
+ path = self._keys_path()
97
+ if not path.exists():
98
+ self._cache = {}
99
+ return {}
100
+
101
+ try:
102
+ encrypted = path.read_bytes()
103
+ decrypted = self._fernet.decrypt(encrypted)
104
+ keys = json.loads(decrypted)
105
+ self._cache = keys
106
+ return keys
107
+ except (InvalidToken, json.JSONDecodeError):
108
+ self._cache = {}
109
+ return {}
110
+
111
+ def _save_keys(self, keys: dict[str, str]) -> None:
112
+ """Encrypt and save keys."""
113
+ encrypted = self._fernet.encrypt(json.dumps(keys).encode())
114
+ path = self._keys_path()
115
+ path.write_bytes(encrypted)
116
+ try:
117
+ path.chmod(0o600)
118
+ except OSError:
119
+ pass
120
+ self._cache = keys
121
+
122
+ def get(self, name: str) -> str | None:
123
+ """
124
+ Get a config value.
125
+
126
+ Checks environment first, then stored config.
127
+
128
+ Args:
129
+ name: Key name
130
+
131
+ Returns:
132
+ Value or None
133
+ """
134
+ # Environment takes precedence
135
+ if name in os.environ:
136
+ return os.environ[name]
137
+
138
+ keys = self._load_keys()
139
+ return keys.get(name)
140
+
141
+ def set(self, name: str, value: str) -> None:
142
+ """
143
+ Set a config value.
144
+
145
+ Args:
146
+ name: Key name
147
+ value: Key value
148
+ """
149
+ keys = self._load_keys()
150
+ keys[name] = value
151
+ self._save_keys(keys)
152
+
153
+ def delete(self, name: str) -> bool:
154
+ """
155
+ Delete a stored key.
156
+
157
+ Args:
158
+ name: Key name
159
+
160
+ Returns:
161
+ True if deleted, False if not found
162
+ """
163
+ keys = self._load_keys()
164
+ if name in keys:
165
+ del keys[name]
166
+ self._save_keys(keys)
167
+ return True
168
+ return False
169
+
170
+ def list_keys(self) -> list[str]:
171
+ """List all stored key names."""
172
+ return list(self._load_keys().keys())
173
+
174
+ def load_into_environment(self) -> int:
175
+ """
176
+ Load all stored keys into environment variables.
177
+
178
+ Only sets variables that aren't already in the environment.
179
+
180
+ Returns:
181
+ Number of keys loaded
182
+ """
183
+ keys = self._load_keys()
184
+ loaded = 0
185
+
186
+ for name, value in keys.items():
187
+ if name not in os.environ:
188
+ os.environ[name] = value
189
+ loaded += 1
190
+
191
+ return loaded
192
+
193
+ def set_interactive(self, name: str | None = None) -> bool:
194
+ """
195
+ Interactively prompt user to set a key.
196
+
197
+ Args:
198
+ name: Key name, or None to show a menu of common keys
199
+
200
+ Returns:
201
+ True if key was set
202
+ """
203
+ if name is None:
204
+ # Show menu of common keys
205
+ console.print()
206
+ console.print("[bold]Common LLM API Keys:[/bold]")
207
+ console.print()
208
+
209
+ items = list(KNOWN_LLM_KEYS.items())
210
+ for i, (key, desc) in enumerate(items, 1):
211
+ status = "[green]✓[/green]" if self.get(key) else "[dim]○[/dim]"
212
+ console.print(f" {status} [{i}] {key}")
213
+ console.print(f" [dim]{desc}[/dim]")
214
+
215
+ console.print()
216
+ console.print(f" [0] Enter custom key name")
217
+ console.print()
218
+
219
+ choice = input("Select key to set (number or name): ").strip()
220
+
221
+ if choice == "0":
222
+ name = input("Enter key name: ").strip()
223
+ if not name:
224
+ return False
225
+ elif choice.isdigit():
226
+ idx = int(choice) - 1
227
+ if 0 <= idx < len(items):
228
+ name = items[idx][0]
229
+ else:
230
+ console.print("[red]Invalid selection[/red]")
231
+ return False
232
+ else:
233
+ # Treat as key name
234
+ name = choice.upper()
235
+
236
+ # Get the value
237
+ description = KNOWN_LLM_KEYS.get(name, "API key")
238
+ console.print()
239
+ console.print(
240
+ Panel(
241
+ f"[bold]{name}[/bold]\n{description}",
242
+ title="🔑 Set API Key",
243
+ border_style="blue",
244
+ )
245
+ )
246
+
247
+ value = getpass.getpass("Enter value (or press Enter to cancel): ")
248
+
249
+ if not value:
250
+ console.print("[dim]Cancelled[/dim]")
251
+ return False
252
+
253
+ self.set(name, value)
254
+ console.print(f"[green]✓[/green] Saved {name}")
255
+ return True
256
+
257
+ def set_from_file(self, file_path: str | Path) -> int:
258
+ """
259
+ Load keys from a .env file.
260
+
261
+ File format (one per line):
262
+ KEY_NAME=value
263
+ # comments are ignored
264
+ export KEY_NAME=value # export prefix is stripped
265
+
266
+ Args:
267
+ file_path: Path to .env file
268
+
269
+ Returns:
270
+ Number of keys imported
271
+ """
272
+ path = Path(file_path)
273
+ if not path.exists():
274
+ raise FileNotFoundError(f"File not found: {file_path}")
275
+
276
+ imported = 0
277
+ pattern = re.compile(r"^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.+)$")
278
+
279
+ with open(path) as f:
280
+ for line in f:
281
+ line = line.strip()
282
+
283
+ # Skip empty lines and comments
284
+ if not line or line.startswith("#"):
285
+ continue
286
+
287
+ match = pattern.match(line)
288
+ if match:
289
+ name, value = match.groups()
290
+
291
+ # Strip quotes if present
292
+ if (value.startswith('"') and value.endswith('"')) or \
293
+ (value.startswith("'") and value.endswith("'")):
294
+ value = value[1:-1]
295
+
296
+ self.set(name, value)
297
+ imported += 1
298
+
299
+ return imported
300
+
301
+ def show_status(self) -> None:
302
+ """Display current configuration status."""
303
+ keys = self._load_keys()
304
+
305
+ if not keys:
306
+ console.print("[dim]No API keys configured[/dim]")
307
+ console.print()
308
+ console.print("Run [cyan]supyagent config set[/cyan] to add keys")
309
+ return
310
+
311
+ table = Table(title="Configured API Keys")
312
+ table.add_column("Key", style="cyan")
313
+ table.add_column("Provider")
314
+ table.add_column("Status")
315
+
316
+ for name in sorted(keys.keys()):
317
+ provider = KNOWN_LLM_KEYS.get(name, "Custom")
318
+ # Show if it's overridden by environment
319
+ if name in os.environ and os.environ[name] != keys[name]:
320
+ status = "[yellow]env override[/yellow]"
321
+ else:
322
+ status = "[green]stored[/green]"
323
+
324
+ table.add_row(name, provider, status)
325
+
326
+ console.print(table)
327
+ console.print()
328
+ console.print(f"[dim]Config location: {self.base_dir}[/dim]")
329
+
330
+
331
+ # Global config manager instance
332
+ _config_manager: ConfigManager | None = None
333
+
334
+
335
+ def get_config_manager() -> ConfigManager:
336
+ """Get the global config manager instance."""
337
+ global _config_manager
338
+ if _config_manager is None:
339
+ _config_manager = ConfigManager()
340
+ return _config_manager
341
+
342
+
343
+ def load_config() -> int:
344
+ """
345
+ Load global config into environment.
346
+
347
+ Call this at the start of any agent execution.
348
+
349
+ Returns:
350
+ Number of keys loaded
351
+ """
352
+ return get_config_manager().load_into_environment()