axon-agent 0.1.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.
- axon_agent-0.1.0/.env.example +4 -0
- axon_agent-0.1.0/.github/workflows/evals.yml +90 -0
- axon_agent-0.1.0/.gitignore +12 -0
- axon_agent-0.1.0/.python-version +1 -0
- axon_agent-0.1.0/PKG-INFO +139 -0
- axon_agent-0.1.0/README.md +113 -0
- axon_agent-0.1.0/pyproject.toml +67 -0
- axon_agent-0.1.0/src/axon/__init__.py +7 -0
- axon_agent-0.1.0/src/axon/agent.py +31 -0
- axon_agent-0.1.0/src/axon/config.py +194 -0
- axon_agent-0.1.0/src/axon/main.py +125 -0
- axon_agent-0.1.0/src/axon/observability.py +103 -0
- axon_agent-0.1.0/src/axon/screens/__init__.py +1 -0
- axon_agent-0.1.0/src/axon/screens/session_list.py +118 -0
- axon_agent-0.1.0/src/axon/session.py +71 -0
- axon_agent-0.1.0/src/axon/setup_wizard.py +140 -0
- axon_agent-0.1.0/src/axon/slash_commands.py +245 -0
- axon_agent-0.1.0/src/axon/stream_worker.py +150 -0
- axon_agent-0.1.0/src/axon/system_prompt.py +30 -0
- axon_agent-0.1.0/src/axon/theme.py +65 -0
- axon_agent-0.1.0/src/axon/tools/__init__.py +20 -0
- axon_agent-0.1.0/src/axon/tools/filesystem.py +158 -0
- axon_agent-0.1.0/src/axon/tools/guard.py +176 -0
- axon_agent-0.1.0/src/axon/tools/shell.py +88 -0
- axon_agent-0.1.0/src/axon/tui.py +247 -0
- axon_agent-0.1.0/src/axon/utils/__init__.py +1 -0
- axon_agent-0.1.0/src/axon/utils/file_search.py +45 -0
- axon_agent-0.1.0/src/axon/widgets/__init__.py +0 -0
- axon_agent-0.1.0/src/axon/widgets/footer.py +77 -0
- axon_agent-0.1.0/src/axon/widgets/input_bar.py +213 -0
- axon_agent-0.1.0/src/axon/widgets/messages.py +282 -0
- axon_agent-0.1.0/tests/__init__.py +1 -0
- axon_agent-0.1.0/tests/conftest.py +25 -0
- axon_agent-0.1.0/tests/evaluator.py +52 -0
- axon_agent-0.1.0/tests/fixtures/__init__.py +3 -0
- axon_agent-0.1.0/tests/fixtures/poisoned.py +9 -0
- axon_agent-0.1.0/tests/helpers.py +113 -0
- axon_agent-0.1.0/tests/judge.py +61 -0
- axon_agent-0.1.0/tests/test_adversarial.py +179 -0
- axon_agent-0.1.0/tests/test_config.py +101 -0
- axon_agent-0.1.0/tests/test_correctness.py +77 -0
- axon_agent-0.1.0/tests/test_relevancy.py +106 -0
- axon_agent-0.1.0/tests/test_safety.py +202 -0
- axon_agent-0.1.0/tests/test_tool_selection.py +78 -0
- axon_agent-0.1.0/uv.lock +2450 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: Axon CI & Evals
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
inputs:
|
|
10
|
+
pipeline:
|
|
11
|
+
description: "Which eval pipeline to run"
|
|
12
|
+
required: true
|
|
13
|
+
default: "deterministic"
|
|
14
|
+
type: choice
|
|
15
|
+
options:
|
|
16
|
+
- deterministic
|
|
17
|
+
- all
|
|
18
|
+
- correctness
|
|
19
|
+
- tool_selection
|
|
20
|
+
- safety
|
|
21
|
+
- adversarial
|
|
22
|
+
- relevancy
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
code-quality:
|
|
26
|
+
name: Lint & Type Check
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout repository
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
|
|
32
|
+
- name: Set up uv
|
|
33
|
+
uses: astral-sh/setup-uv@v5
|
|
34
|
+
with:
|
|
35
|
+
version: "latest"
|
|
36
|
+
enable-cache: true
|
|
37
|
+
|
|
38
|
+
- name: Set up Python
|
|
39
|
+
run: uv python install 3.12
|
|
40
|
+
|
|
41
|
+
- name: Install dependencies
|
|
42
|
+
run: uv sync --all-groups
|
|
43
|
+
|
|
44
|
+
- name: Run Ruff Linter
|
|
45
|
+
run: uv run ruff check .
|
|
46
|
+
|
|
47
|
+
evals:
|
|
48
|
+
name: Agent Evaluations
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
# Only run evals if repository secrets are configured (avoids failure on external PR forks without secrets)
|
|
51
|
+
if: ${{ secrets.GOOGLE_API_KEY != '' }}
|
|
52
|
+
env:
|
|
53
|
+
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
54
|
+
LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
|
|
55
|
+
LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
|
|
56
|
+
LANGFUSE_HOST: ${{ secrets.LANGFUSE_HOST || 'https://cloud.langfuse.com' }}
|
|
57
|
+
AXON_DEFAULT_MODEL: "google:gemini-2.5-flash"
|
|
58
|
+
steps:
|
|
59
|
+
- name: Checkout repository
|
|
60
|
+
uses: actions/checkout@v4
|
|
61
|
+
|
|
62
|
+
- name: Set up uv
|
|
63
|
+
uses: astral-sh/setup-uv@v5
|
|
64
|
+
with:
|
|
65
|
+
version: "latest"
|
|
66
|
+
enable-cache: true
|
|
67
|
+
|
|
68
|
+
- name: Set up Python
|
|
69
|
+
run: uv python install 3.12
|
|
70
|
+
|
|
71
|
+
- name: Install dependencies
|
|
72
|
+
run: uv sync --all-groups
|
|
73
|
+
|
|
74
|
+
- name: Run Eval Suite
|
|
75
|
+
run: |
|
|
76
|
+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
77
|
+
if [ "${{ inputs.pipeline }}" = "all" ]; then
|
|
78
|
+
echo "Running full eval suite (including LLM Judge)..."
|
|
79
|
+
uv run pytest tests/ -v -s
|
|
80
|
+
elif [ "${{ inputs.pipeline }}" = "deterministic" ]; then
|
|
81
|
+
echo "Running all deterministic pipelines..."
|
|
82
|
+
uv run pytest tests/ -m "not relevancy" -v -s
|
|
83
|
+
else
|
|
84
|
+
echo "Running pipeline: ${{ inputs.pipeline }}..."
|
|
85
|
+
uv run pytest tests/ -m "${{ inputs.pipeline }}" -v -s
|
|
86
|
+
fi
|
|
87
|
+
else
|
|
88
|
+
echo "Running deterministic eval pipelines for PR/push..."
|
|
89
|
+
uv run pytest tests/ -m "not relevancy" -v -s
|
|
90
|
+
fi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: axon-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI coding agent with full observability, multi-model support, and security guardrails
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: agent,ai,cli,coding-assistant,langfuse,opentelemetry,pydantic-ai,tui
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: opentelemetry-api>=1.44.0
|
|
18
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.44.0
|
|
19
|
+
Requires-Dist: opentelemetry-sdk>=1.44.0
|
|
20
|
+
Requires-Dist: pydantic-ai>=0.0.30
|
|
21
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
22
|
+
Requires-Dist: rich>=13.9.4
|
|
23
|
+
Requires-Dist: sqlmodel>=0.0.22
|
|
24
|
+
Requires-Dist: textual>=8.2.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Axon ⚡
|
|
28
|
+
|
|
29
|
+
> **A fast, observed CLI coding agent with full OpenTelemetry tracing, robust security guardrails, and multiple LLM provider support.**
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/axon-agent/)
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ✦ Features
|
|
38
|
+
|
|
39
|
+
- **Multi-Model Support:** Switch effortlessly between Google Gemini, Anthropic Claude, OpenAI, and local Ollama models.
|
|
40
|
+
- **PLAN vs. BUILD Modes:**
|
|
41
|
+
- `PLAN`: Read-only analysis and research mode (code modification tools disabled).
|
|
42
|
+
- `BUILD`: Full implementation mode with filesystem editing and terminal execution.
|
|
43
|
+
- **Built-in Security Guardrails:**
|
|
44
|
+
- Path traversal protection (cannot escape project root).
|
|
45
|
+
- Hardcoded secret file blocklist (`.env*`, `.git`, `*.pem`, `*.key`, `id_rsa`).
|
|
46
|
+
- Output truncation guard (prevents context window crashes and huge token bills).
|
|
47
|
+
- Subprocess environment scrubbing (prevents child terminal commands from seeing API keys).
|
|
48
|
+
- Destructive command blocklist (hard-blocks `rm -rf /`, fork bombs, disk formatters).
|
|
49
|
+
- **Full Observability:** Native OpenTelemetry tracing and Langfuse dashboard integration for agent turns, latency, token costs, and tool calls.
|
|
50
|
+
- **Interactive TUI:** Built on Textual with live streaming text, tool execution cards, searchable session history (`/sessions`), and theme customization (`/theme`).
|
|
51
|
+
- **Global Setup Wizard:** Configure API keys once via `axon setup` — saved securely in `~/.axon/config.json`.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🚀 Quickstart
|
|
56
|
+
|
|
57
|
+
### 1. Installation
|
|
58
|
+
|
|
59
|
+
Install globally via `pip` or `uv`:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install axon-agent
|
|
63
|
+
# or
|
|
64
|
+
uv tool install axon-agent
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Configure API Keys
|
|
68
|
+
|
|
69
|
+
Run the interactive setup wizard:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
axon setup
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Enter your API keys (Google Gemini, Anthropic, or OpenAI) and select your default model.
|
|
76
|
+
|
|
77
|
+
### 3. Launch Axon
|
|
78
|
+
|
|
79
|
+
Navigate to any project directory and start coding:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
cd ~/my-project
|
|
83
|
+
axon
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## ⌨️ Slash Commands
|
|
89
|
+
|
|
90
|
+
Inside the Axon TUI:
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `/mode` | Toggle between `PLAN` (read-only) and `BUILD` (read-write) modes |
|
|
95
|
+
| `/models` | List available models or switch model (`/models 2`) |
|
|
96
|
+
| `/models add <provider:model>` | Dynamically register a custom model (e.g. `ollama:llama3.3`) |
|
|
97
|
+
| `/config` | View active configuration and key status |
|
|
98
|
+
| `/sessions` | Browse, search, and resume past sessions |
|
|
99
|
+
| `/theme` | Switch color theme (`nightfox`, `catppuccin`, `dracula`, `gruvbox`) |
|
|
100
|
+
| `/new` | Start a fresh chat session |
|
|
101
|
+
| `/clear` | Clear message history in the current session |
|
|
102
|
+
| `/help` | Show command reference |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## ⚙️ CLI Options
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
axon --help
|
|
110
|
+
|
|
111
|
+
usage: axon [-h] [--mode {PLAN,BUILD}] [--model MODEL] [--cwd CWD]
|
|
112
|
+
[--resume SESSION_ID] [--theme {nightfox,catppuccin,dracula,gruvbox}]
|
|
113
|
+
[--setup] [{setup,config}]
|
|
114
|
+
|
|
115
|
+
options:
|
|
116
|
+
--mode {PLAN,BUILD} Start in PLAN or BUILD mode (default: BUILD)
|
|
117
|
+
--model MODEL LLM provider string (e.g. google:gemini-2.5-flash)
|
|
118
|
+
--cwd CWD Working directory (default: current directory)
|
|
119
|
+
--resume SESSION_ID Resume a previous session by UUID
|
|
120
|
+
--theme THEME Theme palette (default: nightfox)
|
|
121
|
+
--setup Launch interactive setup wizard
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 🛡️ Architecture & Evals
|
|
127
|
+
|
|
128
|
+
Axon is tested across a 5-pipeline evaluation framework:
|
|
129
|
+
1. **Correctness:** Ground-truth file inspection and deterministic answers.
|
|
130
|
+
2. **Tool Selection:** Verifies optimal tool choice (`grep`, `glob`, `listDirectory`, `readFile`).
|
|
131
|
+
3. **Safety:** Verifies path traversal guards, secret blocks, and destructive command blocking.
|
|
132
|
+
4. **Adversarial:** Verifies resistance against direct and indirect prompt injection attacks.
|
|
133
|
+
5. **Relevancy (LLM-as-a-Judge):** Automated grading of response accuracy and completeness.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 📄 License
|
|
138
|
+
|
|
139
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Axon ⚡
|
|
2
|
+
|
|
3
|
+
> **A fast, observed CLI coding agent with full OpenTelemetry tracing, robust security guardrails, and multiple LLM provider support.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/axon-agent/)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✦ Features
|
|
12
|
+
|
|
13
|
+
- **Multi-Model Support:** Switch effortlessly between Google Gemini, Anthropic Claude, OpenAI, and local Ollama models.
|
|
14
|
+
- **PLAN vs. BUILD Modes:**
|
|
15
|
+
- `PLAN`: Read-only analysis and research mode (code modification tools disabled).
|
|
16
|
+
- `BUILD`: Full implementation mode with filesystem editing and terminal execution.
|
|
17
|
+
- **Built-in Security Guardrails:**
|
|
18
|
+
- Path traversal protection (cannot escape project root).
|
|
19
|
+
- Hardcoded secret file blocklist (`.env*`, `.git`, `*.pem`, `*.key`, `id_rsa`).
|
|
20
|
+
- Output truncation guard (prevents context window crashes and huge token bills).
|
|
21
|
+
- Subprocess environment scrubbing (prevents child terminal commands from seeing API keys).
|
|
22
|
+
- Destructive command blocklist (hard-blocks `rm -rf /`, fork bombs, disk formatters).
|
|
23
|
+
- **Full Observability:** Native OpenTelemetry tracing and Langfuse dashboard integration for agent turns, latency, token costs, and tool calls.
|
|
24
|
+
- **Interactive TUI:** Built on Textual with live streaming text, tool execution cards, searchable session history (`/sessions`), and theme customization (`/theme`).
|
|
25
|
+
- **Global Setup Wizard:** Configure API keys once via `axon setup` — saved securely in `~/.axon/config.json`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🚀 Quickstart
|
|
30
|
+
|
|
31
|
+
### 1. Installation
|
|
32
|
+
|
|
33
|
+
Install globally via `pip` or `uv`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install axon-agent
|
|
37
|
+
# or
|
|
38
|
+
uv tool install axon-agent
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Configure API Keys
|
|
42
|
+
|
|
43
|
+
Run the interactive setup wizard:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
axon setup
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Enter your API keys (Google Gemini, Anthropic, or OpenAI) and select your default model.
|
|
50
|
+
|
|
51
|
+
### 3. Launch Axon
|
|
52
|
+
|
|
53
|
+
Navigate to any project directory and start coding:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd ~/my-project
|
|
57
|
+
axon
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## ⌨️ Slash Commands
|
|
63
|
+
|
|
64
|
+
Inside the Axon TUI:
|
|
65
|
+
|
|
66
|
+
| Command | Description |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `/mode` | Toggle between `PLAN` (read-only) and `BUILD` (read-write) modes |
|
|
69
|
+
| `/models` | List available models or switch model (`/models 2`) |
|
|
70
|
+
| `/models add <provider:model>` | Dynamically register a custom model (e.g. `ollama:llama3.3`) |
|
|
71
|
+
| `/config` | View active configuration and key status |
|
|
72
|
+
| `/sessions` | Browse, search, and resume past sessions |
|
|
73
|
+
| `/theme` | Switch color theme (`nightfox`, `catppuccin`, `dracula`, `gruvbox`) |
|
|
74
|
+
| `/new` | Start a fresh chat session |
|
|
75
|
+
| `/clear` | Clear message history in the current session |
|
|
76
|
+
| `/help` | Show command reference |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ⚙️ CLI Options
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
axon --help
|
|
84
|
+
|
|
85
|
+
usage: axon [-h] [--mode {PLAN,BUILD}] [--model MODEL] [--cwd CWD]
|
|
86
|
+
[--resume SESSION_ID] [--theme {nightfox,catppuccin,dracula,gruvbox}]
|
|
87
|
+
[--setup] [{setup,config}]
|
|
88
|
+
|
|
89
|
+
options:
|
|
90
|
+
--mode {PLAN,BUILD} Start in PLAN or BUILD mode (default: BUILD)
|
|
91
|
+
--model MODEL LLM provider string (e.g. google:gemini-2.5-flash)
|
|
92
|
+
--cwd CWD Working directory (default: current directory)
|
|
93
|
+
--resume SESSION_ID Resume a previous session by UUID
|
|
94
|
+
--theme THEME Theme palette (default: nightfox)
|
|
95
|
+
--setup Launch interactive setup wizard
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🛡️ Architecture & Evals
|
|
101
|
+
|
|
102
|
+
Axon is tested across a 5-pipeline evaluation framework:
|
|
103
|
+
1. **Correctness:** Ground-truth file inspection and deterministic answers.
|
|
104
|
+
2. **Tool Selection:** Verifies optimal tool choice (`grep`, `glob`, `listDirectory`, `readFile`).
|
|
105
|
+
3. **Safety:** Verifies path traversal guards, secret blocks, and destructive command blocking.
|
|
106
|
+
4. **Adversarial:** Verifies resistance against direct and indirect prompt injection attacks.
|
|
107
|
+
5. **Relevancy (LLM-as-a-Judge):** Automated grading of response accuracy and completeness.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 📄 License
|
|
112
|
+
|
|
113
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "axon-agent"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A CLI coding agent with full observability, multi-model support, and security guardrails"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
keywords = ["ai", "agent", "coding-assistant", "cli", "tui", "opentelemetry", "langfuse", "pydantic-ai"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Software Development :: Code Generators",
|
|
18
|
+
"Topic :: Utilities",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"pydantic-ai>=0.0.30",
|
|
22
|
+
"python-dotenv>=1.0.1",
|
|
23
|
+
"sqlmodel>=0.0.22",
|
|
24
|
+
"rich>=13.9.4",
|
|
25
|
+
"textual>=8.2.8",
|
|
26
|
+
"opentelemetry-api>=1.44.0",
|
|
27
|
+
"opentelemetry-sdk>=1.44.0",
|
|
28
|
+
"opentelemetry-exporter-otlp>=1.44.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest",
|
|
34
|
+
"pytest-asyncio",
|
|
35
|
+
"langfuse",
|
|
36
|
+
"ruff",
|
|
37
|
+
"pyright",
|
|
38
|
+
"hatch",
|
|
39
|
+
"twine",
|
|
40
|
+
"build",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
axon = "axon.main:main" # creates the `axon` CLI command
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/axon"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 88
|
|
55
|
+
|
|
56
|
+
[tool.pyright]
|
|
57
|
+
pythonVersion = "3.12"
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
asyncio_mode = "auto"
|
|
61
|
+
markers = [
|
|
62
|
+
"correctness: Tests that verify the agent produces the right answer",
|
|
63
|
+
"tool_selection: Tests that verify the agent picks the correct tool",
|
|
64
|
+
"safety: Tests that verify the agent respects guardrails",
|
|
65
|
+
"adversarial: Tests that verify the agent resists prompt injection",
|
|
66
|
+
"relevancy: Tests that use LLM-as-a-Judge to grade response quality",
|
|
67
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Axon agent factory.
|
|
3
|
+
|
|
4
|
+
Instantiates and configures a PydanticAI Agent with:
|
|
5
|
+
- Dynamic system prompt (PLAN vs BUILD mode)
|
|
6
|
+
- OpenTelemetry instrumentation for Langfuse tracing
|
|
7
|
+
- Modular tool registration (filesystem + shell)
|
|
8
|
+
"""
|
|
9
|
+
from pydantic_ai import Agent
|
|
10
|
+
from pydantic_ai.capabilities import Instrumentation
|
|
11
|
+
|
|
12
|
+
from axon.system_prompt import build_system_prompt
|
|
13
|
+
from axon.tools import register_tools
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_agent(mode: str, model_str: str, cwd: str) -> Agent:
|
|
17
|
+
"""
|
|
18
|
+
Create an agent instance configured for the given mode, model, and directory.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
mode: "PLAN" (read-only) or "BUILD" (read-write).
|
|
22
|
+
model_str: Model provider string (e.g. "google:gemini-2.5-flash").
|
|
23
|
+
cwd: Working directory root for filesystem and shell operations.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Configured PydanticAI Agent ready for execution.
|
|
27
|
+
"""
|
|
28
|
+
prompt = build_system_prompt(mode)
|
|
29
|
+
agent = Agent(model_str, system_prompt=prompt, capabilities=[Instrumentation()])
|
|
30
|
+
register_tools(agent, mode=mode, cwd=cwd)
|
|
31
|
+
return agent
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Central configuration and persistent user settings for Axon.
|
|
3
|
+
|
|
4
|
+
Manages:
|
|
5
|
+
- Constants, defaults, and paths (~/.axon/config.json)
|
|
6
|
+
- Automatic loading of global user API keys into os.environ
|
|
7
|
+
- Dynamic model lists (default models + user-added custom models)
|
|
8
|
+
- Secure config saving with restricted file permissions (0600)
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Any
|
|
15
|
+
|
|
16
|
+
# Version
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
# Defaults
|
|
20
|
+
DEFAULT_FALLBACK_MODEL = "google:gemini-2.5-flash"
|
|
21
|
+
DEFAULT_THEME = "nightfox"
|
|
22
|
+
DEFAULT_MODE = "BUILD"
|
|
23
|
+
|
|
24
|
+
# Standard default models
|
|
25
|
+
DEFAULT_MODELS = [
|
|
26
|
+
"google:gemini-2.5-flash",
|
|
27
|
+
"google:gemini-1.5-pro",
|
|
28
|
+
"google:gemma-4-31b-it",
|
|
29
|
+
"anthropic:claude-3-5-haiku-latest",
|
|
30
|
+
"anthropic:claude-opus-4-5",
|
|
31
|
+
"openai:gpt-4o-mini",
|
|
32
|
+
"openai:gpt-4o",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Paths
|
|
36
|
+
AXON_DIR = Path.home() / ".axon"
|
|
37
|
+
AXON_DIR.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
CONFIG_PATH = AXON_DIR / "config.json"
|
|
39
|
+
DB_PATH = AXON_DIR / "sessions.db"
|
|
40
|
+
DATABASE_URL = f"sqlite:///{DB_PATH}"
|
|
41
|
+
|
|
42
|
+
# Known API key environment variable names
|
|
43
|
+
KNOWN_KEY_NAMES = [
|
|
44
|
+
"GOOGLE_API_KEY",
|
|
45
|
+
"ANTHROPIC_API_KEY",
|
|
46
|
+
"OPENAI_API_KEY",
|
|
47
|
+
"LANGFUSE_PUBLIC_KEY",
|
|
48
|
+
"LANGFUSE_SECRET_KEY",
|
|
49
|
+
"LANGFUSE_HOST",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_default_config() -> Dict[str, Any]:
|
|
54
|
+
"""Return initial default configuration dictionary."""
|
|
55
|
+
return {
|
|
56
|
+
"default_model": DEFAULT_FALLBACK_MODEL,
|
|
57
|
+
"default_theme": DEFAULT_THEME,
|
|
58
|
+
"default_mode": DEFAULT_MODE,
|
|
59
|
+
"models": list(DEFAULT_MODELS),
|
|
60
|
+
"keys": {
|
|
61
|
+
"GOOGLE_API_KEY": "",
|
|
62
|
+
"ANTHROPIC_API_KEY": "",
|
|
63
|
+
"OPENAI_API_KEY": "",
|
|
64
|
+
"LANGFUSE_PUBLIC_KEY": "",
|
|
65
|
+
"LANGFUSE_SECRET_KEY": "",
|
|
66
|
+
"LANGFUSE_HOST": "https://cloud.langfuse.com",
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_config() -> Dict[str, Any]:
|
|
72
|
+
"""Load configuration from ~/.axon/config.json or return defaults."""
|
|
73
|
+
if not CONFIG_PATH.exists():
|
|
74
|
+
return get_default_config()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
78
|
+
# Ensure all required top-level keys exist
|
|
79
|
+
default_cfg = get_default_config()
|
|
80
|
+
for k, v in default_cfg.items():
|
|
81
|
+
if k not in data:
|
|
82
|
+
data[k] = v
|
|
83
|
+
if isinstance(data.get("keys"), dict):
|
|
84
|
+
for key_name, default_val in default_cfg["keys"].items():
|
|
85
|
+
if key_name not in data["keys"]:
|
|
86
|
+
data["keys"][key_name] = default_val
|
|
87
|
+
return data
|
|
88
|
+
except Exception:
|
|
89
|
+
return get_default_config()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def save_config(config_data: Dict[str, Any]) -> None:
|
|
93
|
+
"""Save configuration to ~/.axon/config.json with secure 0600 file permissions."""
|
|
94
|
+
AXON_DIR.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
temp_path = CONFIG_PATH.with_suffix(".tmp")
|
|
96
|
+
|
|
97
|
+
# Write JSON formatted
|
|
98
|
+
temp_path.write_text(json.dumps(config_data, indent=2), encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
# Set restricted permissions (owner read/write only) on non-Windows platforms
|
|
101
|
+
try:
|
|
102
|
+
if os.name != "nt":
|
|
103
|
+
temp_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
temp_path.replace(CONFIG_PATH)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def inject_env_keys() -> None:
|
|
111
|
+
"""
|
|
112
|
+
Inject keys from ~/.axon/config.json into os.environ.
|
|
113
|
+
Does NOT overwrite keys already set in the active shell environment.
|
|
114
|
+
"""
|
|
115
|
+
cfg = load_config()
|
|
116
|
+
keys = cfg.get("keys", {})
|
|
117
|
+
for key_name, key_val in keys.items():
|
|
118
|
+
if key_val and not os.getenv(key_name):
|
|
119
|
+
os.environ[key_name] = str(key_val).strip()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_api_key(name: str) -> Optional[str]:
|
|
123
|
+
"""Get an API key by name from os.environ or config.json."""
|
|
124
|
+
if os.getenv(name):
|
|
125
|
+
return os.getenv(name)
|
|
126
|
+
cfg = load_config()
|
|
127
|
+
return cfg.get("keys", {}).get(name) or None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def set_api_key(name: str, value: str) -> None:
|
|
131
|
+
"""Save an API key into ~/.axon/config.json and update os.environ."""
|
|
132
|
+
cfg = load_config()
|
|
133
|
+
if "keys" not in cfg:
|
|
134
|
+
cfg["keys"] = {}
|
|
135
|
+
cfg["keys"][name] = value.strip()
|
|
136
|
+
save_config(cfg)
|
|
137
|
+
os.environ[name] = value.strip()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_models() -> List[str]:
|
|
141
|
+
"""Get full list of available models (defaults + user custom models)."""
|
|
142
|
+
cfg = load_config()
|
|
143
|
+
models = cfg.get("models", DEFAULT_MODELS)
|
|
144
|
+
return list(models) if isinstance(models, list) else list(DEFAULT_MODELS)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def add_custom_model(model_str: str) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Add a custom model identifier string (e.g. 'ollama:llama3.3' or 'openai:gpt-4o')
|
|
150
|
+
to ~/.axon/config.json. Returns True if added, False if already present.
|
|
151
|
+
"""
|
|
152
|
+
model_clean = model_str.strip()
|
|
153
|
+
if not model_clean:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
cfg = load_config()
|
|
157
|
+
models = cfg.get("models", list(DEFAULT_MODELS))
|
|
158
|
+
if model_clean not in models:
|
|
159
|
+
models.append(model_clean)
|
|
160
|
+
cfg["models"] = models
|
|
161
|
+
save_config(cfg)
|
|
162
|
+
return True
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def has_valid_key() -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Check if the user has configured at least one LLM provider key
|
|
169
|
+
(Google Gemini, Anthropic, or OpenAI).
|
|
170
|
+
"""
|
|
171
|
+
# Check env first
|
|
172
|
+
if os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY"):
|
|
173
|
+
return True
|
|
174
|
+
if os.getenv("ANTHROPIC_API_KEY"):
|
|
175
|
+
return True
|
|
176
|
+
if os.getenv("OPENAI_API_KEY"):
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
# Check config.json
|
|
180
|
+
cfg = load_config()
|
|
181
|
+
keys = cfg.get("keys", {})
|
|
182
|
+
return bool(
|
|
183
|
+
keys.get("GOOGLE_API_KEY")
|
|
184
|
+
or keys.get("ANTHROPIC_API_KEY")
|
|
185
|
+
or keys.get("OPENAI_API_KEY")
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Automatically inject stored keys on module import
|
|
190
|
+
inject_env_keys()
|
|
191
|
+
|
|
192
|
+
# Active defaults resolved after injection
|
|
193
|
+
DEFAULT_MODEL = os.getenv("AXON_DEFAULT_MODEL", load_config().get("default_model", DEFAULT_FALLBACK_MODEL))
|
|
194
|
+
SUPPORTED_MODELS = get_models()
|