supyagent 0.1.0__tar.gz → 0.2.0__tar.gz

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.

Files changed (40) hide show
  1. {supyagent-0.1.0 → supyagent-0.2.0}/PKG-INFO +69 -30
  2. {supyagent-0.1.0 → supyagent-0.2.0}/README.md +68 -29
  3. {supyagent-0.1.0 → supyagent-0.2.0}/pyproject.toml +1 -1
  4. supyagent-0.2.0/scripts/release.sh +151 -0
  5. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/cli/main.py +180 -1
  6. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/__init__.py +3 -0
  7. supyagent-0.2.0/supyagent/core/config.py +352 -0
  8. {supyagent-0.1.0 → supyagent-0.2.0}/.gitignore +0 -0
  9. {supyagent-0.1.0 → supyagent-0.2.0}/LICENSE +0 -0
  10. {supyagent-0.1.0 → supyagent-0.2.0}/agents/assistant.yaml +0 -0
  11. {supyagent-0.1.0 → supyagent-0.2.0}/agents/coder.yaml +0 -0
  12. {supyagent-0.1.0 → supyagent-0.2.0}/agents/planner.yaml +0 -0
  13. {supyagent-0.1.0 → supyagent-0.2.0}/agents/researcher.yaml +0 -0
  14. {supyagent-0.1.0 → supyagent-0.2.0}/agents/summarizer.yaml +0 -0
  15. {supyagent-0.1.0 → supyagent-0.2.0}/agents/writer.yaml +0 -0
  16. {supyagent-0.1.0 → supyagent-0.2.0}/plans/initial_plan.md +0 -0
  17. {supyagent-0.1.0 → supyagent-0.2.0}/sprints/README.md +0 -0
  18. {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_1_foundation.md +0 -0
  19. {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_2_sessions.md +0 -0
  20. {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_3_repl.md +0 -0
  21. {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_4_execution.md +0 -0
  22. {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_5_multiagent.md +0 -0
  23. {supyagent-0.1.0 → supyagent-0.2.0}/sprints/sprint_6_polish.md +0 -0
  24. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/__init__.py +0 -0
  25. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/__main__.py +0 -0
  26. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/cli/__init__.py +0 -0
  27. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/agent.py +0 -0
  28. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/context.py +0 -0
  29. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/credentials.py +0 -0
  30. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/delegation.py +0 -0
  31. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/executor.py +0 -0
  32. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/llm.py +0 -0
  33. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/registry.py +0 -0
  34. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/session_manager.py +0 -0
  35. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/core/tools.py +0 -0
  36. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/models/__init__.py +0 -0
  37. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/models/agent_config.py +0 -0
  38. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/models/session.py +0 -0
  39. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/utils/__init__.py +0 -0
  40. {supyagent-0.1.0 → supyagent-0.2.0}/supyagent/utils/paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supyagent
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: LLM agents powered by supypowers - build AI agents with tool use, multi-agent orchestration, and secure credential management
5
5
  Project-URL: Homepage, https://github.com/ergodic-ai/supyagent
6
6
  Project-URL: Documentation, https://github.com/ergodic-ai/supyagent#readme
@@ -68,6 +68,9 @@ uv pip install supyagent
68
68
  ## Quick Start
69
69
 
70
70
  ```bash
71
+ # Set up your API key (stored securely)
72
+ supyagent config set ANTHROPIC_API_KEY
73
+
71
74
  # Create your first agent
72
75
  supyagent new myagent
73
76
 
@@ -133,39 +136,40 @@ supyagent plan "Build a Python library for data validation"
133
136
  # The planner will delegate to specialist agents (coder, writer, researcher)
134
137
  ```
135
138
 
136
- ## Agent Configuration
139
+ ## Configuration
137
140
 
138
- Agents are defined in YAML files in the `agents/` directory:
141
+ ### Setting Up API Keys
139
142
 
140
- ```yaml
141
- name: researcher
142
- description: An AI research assistant
143
- type: interactive # or "execution"
143
+ Supyagent securely stores your LLM API keys so you don't need to export them every time:
144
144
 
145
- model:
146
- provider: anthropic/claude-3-5-sonnet-20241022
147
- temperature: 0.7
148
- max_tokens: 4096
145
+ ```bash
146
+ # Interactive setup (recommended)
147
+ supyagent config set
148
+ # Shows a menu of common providers to choose from
149
149
 
150
- system_prompt: |
151
- You are a helpful research assistant...
150
+ # Set a specific key
151
+ supyagent config set ANTHROPIC_API_KEY
152
+ supyagent config set OPENAI_API_KEY
152
153
 
153
- tools:
154
- allow:
155
- - "*" # Allow all tools
156
- # or be specific:
157
- # - "web:*" # All functions in web.py
158
- # - "math:calc" # Specific function
159
- deny:
160
- - "dangerous:*" # Block specific tools
154
+ # Import from a .env file
155
+ supyagent config import .env
161
156
 
162
- # For multi-agent support
163
- delegates:
164
- - coder
165
- - writer
157
+ # Import only specific keys
158
+ supyagent config import .env --filter OPENAI
159
+
160
+ # List configured keys
161
+ supyagent config list
162
+
163
+ # Export keys to backup
164
+ supyagent config export backup.env
165
+
166
+ # Delete a key
167
+ supyagent config delete OPENAI_API_KEY
166
168
  ```
167
169
 
168
- ### LLM Providers
170
+ Keys are encrypted and stored in `~/.supyagent/config/`. They're automatically loaded when running any agent command.
171
+
172
+ ### Supported Providers
169
173
 
170
174
  Supyagent uses LiteLLM, supporting 100+ providers:
171
175
 
@@ -187,11 +191,36 @@ model:
187
191
  provider: gemini/gemini-pro
188
192
  ```
189
193
 
190
- Set API keys as environment variables:
194
+ ## Agent Configuration
191
195
 
192
- ```bash
193
- export OPENAI_API_KEY=sk-...
194
- export ANTHROPIC_API_KEY=sk-ant-...
196
+ Agents are defined in YAML files in the `agents/` directory:
197
+
198
+ ```yaml
199
+ name: researcher
200
+ description: An AI research assistant
201
+ type: interactive # or "execution"
202
+
203
+ model:
204
+ provider: anthropic/claude-3-5-sonnet-20241022
205
+ temperature: 0.7
206
+ max_tokens: 4096
207
+
208
+ system_prompt: |
209
+ You are a helpful research assistant...
210
+
211
+ tools:
212
+ allow:
213
+ - "*" # Allow all tools
214
+ # or be specific:
215
+ # - "web:*" # All functions in web.py
216
+ # - "math:calc" # Specific function
217
+ deny:
218
+ - "dangerous:*" # Block specific tools
219
+
220
+ # For multi-agent support
221
+ delegates:
222
+ - coder
223
+ - writer
195
224
  ```
196
225
 
197
226
  ## CLI Reference
@@ -223,6 +252,16 @@ export ANTHROPIC_API_KEY=sk-ant-...
223
252
  | `supyagent agents` | List active agent instances |
224
253
  | `supyagent cleanup` | Remove completed instances |
225
254
 
255
+ ### Config Commands
256
+
257
+ | Command | Description |
258
+ |---------|-------------|
259
+ | `supyagent config set [KEY]` | Set an API key (interactive menu if no key specified) |
260
+ | `supyagent config list` | List all configured keys |
261
+ | `supyagent config import <file>` | Import keys from .env file |
262
+ | `supyagent config export <file>` | Export keys to .env file |
263
+ | `supyagent config delete <key>` | Delete a stored key |
264
+
226
265
  ### In-Chat Commands
227
266
 
228
267
  While chatting, use these commands:
@@ -32,6 +32,9 @@ uv pip install supyagent
32
32
  ## Quick Start
33
33
 
34
34
  ```bash
35
+ # Set up your API key (stored securely)
36
+ supyagent config set ANTHROPIC_API_KEY
37
+
35
38
  # Create your first agent
36
39
  supyagent new myagent
37
40
 
@@ -97,39 +100,40 @@ supyagent plan "Build a Python library for data validation"
97
100
  # The planner will delegate to specialist agents (coder, writer, researcher)
98
101
  ```
99
102
 
100
- ## Agent Configuration
103
+ ## Configuration
101
104
 
102
- Agents are defined in YAML files in the `agents/` directory:
105
+ ### Setting Up API Keys
103
106
 
104
- ```yaml
105
- name: researcher
106
- description: An AI research assistant
107
- type: interactive # or "execution"
107
+ Supyagent securely stores your LLM API keys so you don't need to export them every time:
108
108
 
109
- model:
110
- provider: anthropic/claude-3-5-sonnet-20241022
111
- temperature: 0.7
112
- max_tokens: 4096
109
+ ```bash
110
+ # Interactive setup (recommended)
111
+ supyagent config set
112
+ # Shows a menu of common providers to choose from
113
113
 
114
- system_prompt: |
115
- You are a helpful research assistant...
114
+ # Set a specific key
115
+ supyagent config set ANTHROPIC_API_KEY
116
+ supyagent config set OPENAI_API_KEY
116
117
 
117
- tools:
118
- allow:
119
- - "*" # Allow all tools
120
- # or be specific:
121
- # - "web:*" # All functions in web.py
122
- # - "math:calc" # Specific function
123
- deny:
124
- - "dangerous:*" # Block specific tools
118
+ # Import from a .env file
119
+ supyagent config import .env
125
120
 
126
- # For multi-agent support
127
- delegates:
128
- - coder
129
- - writer
121
+ # Import only specific keys
122
+ supyagent config import .env --filter OPENAI
123
+
124
+ # List configured keys
125
+ supyagent config list
126
+
127
+ # Export keys to backup
128
+ supyagent config export backup.env
129
+
130
+ # Delete a key
131
+ supyagent config delete OPENAI_API_KEY
130
132
  ```
131
133
 
132
- ### LLM Providers
134
+ Keys are encrypted and stored in `~/.supyagent/config/`. They're automatically loaded when running any agent command.
135
+
136
+ ### Supported Providers
133
137
 
134
138
  Supyagent uses LiteLLM, supporting 100+ providers:
135
139
 
@@ -151,11 +155,36 @@ model:
151
155
  provider: gemini/gemini-pro
152
156
  ```
153
157
 
154
- Set API keys as environment variables:
158
+ ## Agent Configuration
155
159
 
156
- ```bash
157
- export OPENAI_API_KEY=sk-...
158
- export ANTHROPIC_API_KEY=sk-ant-...
160
+ Agents are defined in YAML files in the `agents/` directory:
161
+
162
+ ```yaml
163
+ name: researcher
164
+ description: An AI research assistant
165
+ type: interactive # or "execution"
166
+
167
+ model:
168
+ provider: anthropic/claude-3-5-sonnet-20241022
169
+ temperature: 0.7
170
+ max_tokens: 4096
171
+
172
+ system_prompt: |
173
+ You are a helpful research assistant...
174
+
175
+ tools:
176
+ allow:
177
+ - "*" # Allow all tools
178
+ # or be specific:
179
+ # - "web:*" # All functions in web.py
180
+ # - "math:calc" # Specific function
181
+ deny:
182
+ - "dangerous:*" # Block specific tools
183
+
184
+ # For multi-agent support
185
+ delegates:
186
+ - coder
187
+ - writer
159
188
  ```
160
189
 
161
190
  ## CLI Reference
@@ -187,6 +216,16 @@ export ANTHROPIC_API_KEY=sk-ant-...
187
216
  | `supyagent agents` | List active agent instances |
188
217
  | `supyagent cleanup` | Remove completed instances |
189
218
 
219
+ ### Config Commands
220
+
221
+ | Command | Description |
222
+ |---------|-------------|
223
+ | `supyagent config set [KEY]` | Set an API key (interactive menu if no key specified) |
224
+ | `supyagent config list` | List all configured keys |
225
+ | `supyagent config import <file>` | Import keys from .env file |
226
+ | `supyagent config export <file>` | Export keys to .env file |
227
+ | `supyagent config delete <key>` | Delete a stored key |
228
+
190
229
  ### In-Chat Commands
191
230
 
192
231
  While chatting, use these commands:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "supyagent"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "LLM agents powered by supypowers - build AI agents with tool use, multi-agent orchestration, and secure credential management"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,151 @@
1
+ #!/bin/bash
2
+ #
3
+ # Release script for supyagent
4
+ # Usage: ./scripts/release.sh [version]
5
+ #
6
+ # Examples:
7
+ # ./scripts/release.sh 0.2.0 # Release version 0.2.0
8
+ # ./scripts/release.sh patch # Bump patch version (0.1.0 -> 0.1.1)
9
+ # ./scripts/release.sh minor # Bump minor version (0.1.0 -> 0.2.0)
10
+ # ./scripts/release.sh major # Bump major version (0.1.0 -> 1.0.0)
11
+ #
12
+
13
+ set -e # Exit on error
14
+
15
+ # Colors for output
16
+ RED='\033[0;31m'
17
+ GREEN='\033[0;32m'
18
+ YELLOW='\033[1;33m'
19
+ BLUE='\033[0;34m'
20
+ NC='\033[0m' # No Color
21
+
22
+ # Change to project root
23
+ cd "$(dirname "$0")/.."
24
+ PROJECT_ROOT=$(pwd)
25
+
26
+ echo -e "${BLUE}📦 Supyagent Release Script${NC}"
27
+ echo "================================"
28
+ echo ""
29
+
30
+ # Get current version from pyproject.toml
31
+ CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
32
+ echo -e "Current version: ${YELLOW}${CURRENT_VERSION}${NC}"
33
+
34
+ # Determine new version
35
+ if [ -z "$1" ]; then
36
+ echo -e "${RED}Error: Version argument required${NC}"
37
+ echo ""
38
+ echo "Usage: $0 [version|patch|minor|major]"
39
+ echo ""
40
+ echo "Examples:"
41
+ echo " $0 0.2.0 # Set specific version"
42
+ echo " $0 patch # Bump patch (0.1.0 -> 0.1.1)"
43
+ echo " $0 minor # Bump minor (0.1.0 -> 0.2.0)"
44
+ echo " $0 major # Bump major (0.1.0 -> 1.0.0)"
45
+ exit 1
46
+ fi
47
+
48
+ # Parse version
49
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
50
+
51
+ case "$1" in
52
+ patch)
53
+ NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
54
+ ;;
55
+ minor)
56
+ NEW_VERSION="$MAJOR.$((MINOR + 1)).0"
57
+ ;;
58
+ major)
59
+ NEW_VERSION="$((MAJOR + 1)).0.0"
60
+ ;;
61
+ *)
62
+ NEW_VERSION="$1"
63
+ ;;
64
+ esac
65
+
66
+ echo -e "New version: ${GREEN}${NEW_VERSION}${NC}"
67
+ echo ""
68
+
69
+ # Confirm
70
+ read -p "Proceed with release? [y/N] " -n 1 -r
71
+ echo ""
72
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
73
+ echo "Cancelled."
74
+ exit 0
75
+ fi
76
+
77
+ echo ""
78
+ echo -e "${BLUE}Step 1: Update version in pyproject.toml${NC}"
79
+ sed -i.bak "s/^version = \".*\"/version = \"${NEW_VERSION}\"/" pyproject.toml
80
+ rm -f pyproject.toml.bak
81
+
82
+ # Also update version in CLI if it's hardcoded there
83
+ if grep -q "version=\"${CURRENT_VERSION}\"" supyagent/cli/main.py 2>/dev/null; then
84
+ sed -i.bak "s/version=\"${CURRENT_VERSION}\"/version=\"${NEW_VERSION}\"/" supyagent/cli/main.py
85
+ rm -f supyagent/cli/main.py.bak
86
+ fi
87
+
88
+ echo -e "${GREEN}✓${NC} Version updated to ${NEW_VERSION}"
89
+
90
+ echo ""
91
+ echo -e "${BLUE}Step 2: Run tests${NC}"
92
+ source .venv/bin/activate 2>/dev/null || true
93
+ python -m pytest tests/ -q
94
+ echo -e "${GREEN}✓${NC} All tests passed"
95
+
96
+ echo ""
97
+ echo -e "${BLUE}Step 3: Clean old builds${NC}"
98
+ rm -rf dist/ build/ *.egg-info
99
+ echo -e "${GREEN}✓${NC} Cleaned build directories"
100
+
101
+ echo ""
102
+ echo -e "${BLUE}Step 4: Build package${NC}"
103
+ python -m build
104
+ echo -e "${GREEN}✓${NC} Built successfully"
105
+
106
+ echo ""
107
+ echo -e "${BLUE}Step 5: Verify package${NC}"
108
+ twine check dist/*
109
+ echo -e "${GREEN}✓${NC} Package verified"
110
+
111
+ echo ""
112
+ echo -e "${BLUE}Step 6: Upload to PyPI${NC}"
113
+ # Load credentials from .env if present
114
+ if [ -f .env ]; then
115
+ export $(cat .env | grep -v '^#' | xargs)
116
+ fi
117
+ twine upload dist/*
118
+ echo -e "${GREEN}✓${NC} Uploaded to PyPI"
119
+
120
+ echo ""
121
+ echo -e "${BLUE}Step 7: Clear local caches${NC}"
122
+
123
+ # Clear pip cache
124
+ echo " Clearing pip cache..."
125
+ pip cache purge 2>/dev/null || true
126
+
127
+ # Clear uv cache for supyagent
128
+ echo " Clearing uv cache..."
129
+ uv cache clean supyagent 2>/dev/null || true
130
+
131
+ # Remove any local installs
132
+ echo " Removing local editable install..."
133
+ pip uninstall -y supyagent 2>/dev/null || true
134
+ uv pip uninstall supyagent 2>/dev/null || true
135
+
136
+ echo -e "${GREEN}✓${NC} Caches cleared"
137
+
138
+ echo ""
139
+ echo -e "${GREEN}========================================${NC}"
140
+ echo -e "${GREEN}🎉 Released supyagent ${NEW_VERSION}${NC}"
141
+ echo -e "${GREEN}========================================${NC}"
142
+ echo ""
143
+ echo "View on PyPI: https://pypi.org/project/supyagent/${NEW_VERSION}/"
144
+ echo ""
145
+ echo "To install the new version:"
146
+ echo " pip install supyagent==${NEW_VERSION}"
147
+ echo " # or"
148
+ echo " uv pip install supyagent==${NEW_VERSION}"
149
+ echo ""
150
+ echo "To reinstall locally for development:"
151
+ echo " uv pip install -e ."
@@ -17,6 +17,7 @@ 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
@@ -26,7 +27,7 @@ console = Console()
26
27
 
27
28
 
28
29
  @click.group()
29
- @click.version_option(version="0.1.0", prog_name="supyagent")
30
+ @click.version_option(version="0.2.0", prog_name="supyagent")
30
31
  def cli():
31
32
  """
32
33
  Supyagent - LLM agents powered by supypowers.
@@ -175,6 +176,9 @@ def chat(agent_name: str, new_session: bool, session_id: str | None):
175
176
  By default, resumes the most recent session. Use --new to start fresh,
176
177
  or --session <id> to resume a specific session.
177
178
  """
179
+ # Load global config (API keys) into environment
180
+ load_config()
181
+
178
182
  # Load agent config
179
183
  try:
180
184
  config = load_agent_config(agent_name)
@@ -625,6 +629,9 @@ def run(
625
629
  echo "text" | supyagent run summarizer
626
630
  supyagent run api-caller '{"endpoint": "/users"}' --secrets API_KEY=xxx
627
631
  """
632
+ # Load global config (API keys) into environment
633
+ load_config()
634
+
628
635
  # Load agent config
629
636
  try:
630
637
  config = load_agent_config(agent_name)
@@ -742,6 +749,9 @@ def batch(
742
749
  supyagent batch summarizer inputs.jsonl --output results.jsonl
743
750
  supyagent batch summarizer data.csv --format csv
744
751
  """
752
+ # Load global config (API keys) into environment
753
+ load_config()
754
+
745
755
  # Load agent config
746
756
  try:
747
757
  config = load_agent_config(agent_name)
@@ -879,6 +889,9 @@ def plan(task: str, planner: str, new_session: bool):
879
889
  supyagent plan "Create a Python library for data validation"
880
890
  supyagent plan "Write a blog post about AI" --planner my-planner
881
891
  """
892
+ # Load global config (API keys) into environment
893
+ load_config()
894
+
882
895
  # Load planner config
883
896
  try:
884
897
  config = load_agent_config(planner)
@@ -942,5 +955,171 @@ def cleanup():
942
955
  console.print(f"[green]✓[/green] Cleaned up {count} instance(s)")
943
956
 
944
957
 
958
+ # =============================================================================
959
+ # Config Commands
960
+ # =============================================================================
961
+
962
+
963
+ @cli.group()
964
+ def config():
965
+ """Manage API keys and global configuration."""
966
+ pass
967
+
968
+
969
+ @config.command("set")
970
+ @click.argument("key_name", required=False)
971
+ @click.option(
972
+ "--value",
973
+ "-v",
974
+ help="Set value directly (use with caution - visible in shell history)",
975
+ )
976
+ def config_set(key_name: str | None, value: str | None):
977
+ """
978
+ Set an API key.
979
+
980
+ If KEY_NAME is not provided, shows an interactive menu of common keys.
981
+
982
+ \b
983
+ Examples:
984
+ supyagent config set # Interactive menu
985
+ supyagent config set OPENAI_API_KEY # Set specific key
986
+ supyagent config set MY_KEY -v "value" # Set with value (not recommended)
987
+ """
988
+ config_mgr = ConfigManager()
989
+
990
+ if value:
991
+ if not key_name:
992
+ console.print("[red]Error:[/red] KEY_NAME required when using --value")
993
+ sys.exit(1)
994
+ config_mgr.set(key_name, value)
995
+ console.print(f"[green]✓[/green] Saved {key_name}")
996
+ else:
997
+ config_mgr.set_interactive(key_name)
998
+
999
+
1000
+ @config.command("list")
1001
+ def config_list():
1002
+ """List all configured API keys."""
1003
+ config_mgr = ConfigManager()
1004
+ config_mgr.show_status()
1005
+
1006
+
1007
+ @config.command("delete")
1008
+ @click.argument("key_name")
1009
+ def config_delete(key_name: str):
1010
+ """Delete a stored API key."""
1011
+ config_mgr = ConfigManager()
1012
+
1013
+ if config_mgr.delete(key_name):
1014
+ console.print(f"[green]✓[/green] Deleted {key_name}")
1015
+ else:
1016
+ console.print(f"[yellow]Key not found:[/yellow] {key_name}")
1017
+
1018
+
1019
+ @config.command("import")
1020
+ @click.argument("file_path", type=click.Path(exists=True))
1021
+ @click.option(
1022
+ "--filter",
1023
+ "-f",
1024
+ "key_filter",
1025
+ help="Only import keys matching this prefix (e.g., 'OPENAI')",
1026
+ )
1027
+ def config_import(file_path: str, key_filter: str | None):
1028
+ """
1029
+ Import API keys from a .env file.
1030
+
1031
+ The file should contain KEY=VALUE pairs, one per line.
1032
+ Lines starting with # are ignored.
1033
+
1034
+ \b
1035
+ Examples:
1036
+ supyagent config import .env
1037
+ supyagent config import secrets.env --filter OPENAI
1038
+ """
1039
+ config_mgr = ConfigManager()
1040
+
1041
+ try:
1042
+ # If filter is specified, we need custom handling
1043
+ if key_filter:
1044
+ from pathlib import Path
1045
+ import re
1046
+
1047
+ path = Path(file_path)
1048
+ pattern = re.compile(r"^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.+)$")
1049
+ imported = 0
1050
+
1051
+ with open(path) as f:
1052
+ for line in f:
1053
+ line = line.strip()
1054
+ if not line or line.startswith("#"):
1055
+ continue
1056
+
1057
+ match = pattern.match(line)
1058
+ if match:
1059
+ name, value = match.groups()
1060
+ if name.startswith(key_filter.upper()):
1061
+ if (value.startswith('"') and value.endswith('"')) or (
1062
+ value.startswith("'") and value.endswith("'")
1063
+ ):
1064
+ value = value[1:-1]
1065
+ config_mgr.set(name, value)
1066
+ console.print(f" [green]✓[/green] {name}")
1067
+ imported += 1
1068
+ else:
1069
+ imported = config_mgr.set_from_file(file_path)
1070
+
1071
+ if imported == 0:
1072
+ console.print("[yellow]No keys found in file[/yellow]")
1073
+ else:
1074
+ console.print(f"\n[green]✓[/green] Imported {imported} key(s)")
1075
+
1076
+ except FileNotFoundError:
1077
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
1078
+ sys.exit(1)
1079
+ except Exception as e:
1080
+ console.print(f"[red]Error:[/red] {e}")
1081
+ sys.exit(1)
1082
+
1083
+
1084
+ @config.command("export")
1085
+ @click.argument("file_path", type=click.Path())
1086
+ @click.option("--force", "-f", is_flag=True, help="Overwrite existing file")
1087
+ def config_export(file_path: str, force: bool):
1088
+ """
1089
+ Export stored API keys to a .env file.
1090
+
1091
+ \b
1092
+ Example:
1093
+ supyagent config export backup.env
1094
+ """
1095
+ config_mgr = ConfigManager()
1096
+ path = Path(file_path)
1097
+
1098
+ if path.exists() and not force:
1099
+ console.print(f"[red]Error:[/red] File exists: {file_path}")
1100
+ console.print("Use --force to overwrite")
1101
+ sys.exit(1)
1102
+
1103
+ keys = config_mgr._load_keys()
1104
+
1105
+ if not keys:
1106
+ console.print("[yellow]No keys to export[/yellow]")
1107
+ return
1108
+
1109
+ with open(path, "w") as f:
1110
+ f.write("# Supyagent API Keys\n")
1111
+ f.write("# Generated export - keep this file secure!\n\n")
1112
+ for name, value in sorted(keys.items()):
1113
+ f.write(f"{name}={value}\n")
1114
+
1115
+ # Set restrictive permissions
1116
+ try:
1117
+ path.chmod(0o600)
1118
+ except OSError:
1119
+ pass
1120
+
1121
+ console.print(f"[green]✓[/green] Exported {len(keys)} key(s) to {file_path}")
1122
+
1123
+
945
1124
  if __name__ == "__main__":
946
1125
  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()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes