agentmetrics-server 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.
- agentmetrics_server-0.1.0/.gitignore +62 -0
- agentmetrics_server-0.1.0/PKG-INFO +147 -0
- agentmetrics_server-0.1.0/README.md +124 -0
- agentmetrics_server-0.1.0/alembic/env.py +45 -0
- agentmetrics_server-0.1.0/alembic/script.py.mako +25 -0
- agentmetrics_server-0.1.0/alembic/versions/001_initial_schema.py +64 -0
- agentmetrics_server-0.1.0/alembic/versions/002_placeholder.py +20 -0
- agentmetrics_server-0.1.0/alembic/versions/003_password_reset.py +41 -0
- agentmetrics_server-0.1.0/alembic/versions/004_data_integrity.py +34 -0
- agentmetrics_server-0.1.0/alembic/versions/005_agents_metadata.py +65 -0
- agentmetrics_server-0.1.0/alembic/versions/006_steps_table.py +59 -0
- agentmetrics_server-0.1.0/alembic/versions/007_metrics_aggregation.py +80 -0
- agentmetrics_server-0.1.0/alembic/versions/008_recommendations_alerts.py +100 -0
- agentmetrics_server-0.1.0/alembic/versions/009_org_settings.py +27 -0
- agentmetrics_server-0.1.0/alembic/versions/010_schema_fixes.py +70 -0
- agentmetrics_server-0.1.0/alembic/versions/011_perf_indexes.py +26 -0
- agentmetrics_server-0.1.0/alembic/versions/012_v2_schema_promotion.py +68 -0
- agentmetrics_server-0.1.0/alembic/versions/013_remove_supabase_add_password.py +45 -0
- agentmetrics_server-0.1.0/alembic/versions/014_schema_hardening.py +43 -0
- agentmetrics_server-0.1.0/alembic/versions/015_api_key_auth.py +27 -0
- agentmetrics_server-0.1.0/alembic.ini +38 -0
- agentmetrics_server-0.1.0/app/__init__.py +0 -0
- agentmetrics_server-0.1.0/app/config.py +26 -0
- agentmetrics_server-0.1.0/app/core/__init__.py +0 -0
- agentmetrics_server-0.1.0/app/core/activity_store.py +59 -0
- agentmetrics_server-0.1.0/app/core/pricing.py +191 -0
- agentmetrics_server-0.1.0/app/core/rate_limit.py +49 -0
- agentmetrics_server-0.1.0/app/database.py +29 -0
- agentmetrics_server-0.1.0/app/db_compat.py +73 -0
- agentmetrics_server-0.1.0/app/deps.py +63 -0
- agentmetrics_server-0.1.0/app/hooks.py +99 -0
- agentmetrics_server-0.1.0/app/main.py +299 -0
- agentmetrics_server-0.1.0/app/models/__init__.py +5 -0
- agentmetrics_server-0.1.0/app/models/event.py +57 -0
- agentmetrics_server-0.1.0/app/models/metrics.py +73 -0
- agentmetrics_server-0.1.0/app/models/organization.py +20 -0
- agentmetrics_server-0.1.0/app/routers/__init__.py +0 -0
- agentmetrics_server-0.1.0/app/routers/activity.py +122 -0
- agentmetrics_server-0.1.0/app/routers/agents.py +141 -0
- agentmetrics_server-0.1.0/app/routers/alerts.py +163 -0
- agentmetrics_server-0.1.0/app/routers/audit.py +149 -0
- agentmetrics_server-0.1.0/app/routers/auth.py +84 -0
- agentmetrics_server-0.1.0/app/routers/events.py +165 -0
- agentmetrics_server-0.1.0/app/routers/fleet.py +287 -0
- agentmetrics_server-0.1.0/app/routers/recommendations.py +66 -0
- agentmetrics_server-0.1.0/app/routers/runs.py +100 -0
- agentmetrics_server-0.1.0/app/routers/slo.py +150 -0
- agentmetrics_server-0.1.0/app/routers/stats.py +283 -0
- agentmetrics_server-0.1.0/app/schemas/__init__.py +0 -0
- agentmetrics_server-0.1.0/app/schemas/activity.py +30 -0
- agentmetrics_server-0.1.0/app/schemas/agent.py +140 -0
- agentmetrics_server-0.1.0/app/schemas/auth.py +20 -0
- agentmetrics_server-0.1.0/app/schemas/event.py +95 -0
- agentmetrics_server-0.1.0/app/services/__init__.py +0 -0
- agentmetrics_server-0.1.0/app/services/agent_service.py +465 -0
- agentmetrics_server-0.1.0/app/services/aggregation_service.py +351 -0
- agentmetrics_server-0.1.0/app/services/alert_service.py +343 -0
- agentmetrics_server-0.1.0/app/services/email_service.py +184 -0
- agentmetrics_server-0.1.0/app/services/recommendation_service.py +261 -0
- agentmetrics_server-0.1.0/app/worker.py +164 -0
- agentmetrics_server-0.1.0/cli.py +144 -0
- agentmetrics_server-0.1.0/daemon.py +377 -0
- agentmetrics_server-0.1.0/pyproject.toml +58 -0
- agentmetrics_server-0.1.0/requirements.txt +15 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Generated — dashboard SPA copied here during server build
|
|
2
|
+
api/app/static/
|
|
3
|
+
|
|
4
|
+
# Python
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*.pyo
|
|
8
|
+
.venv/
|
|
9
|
+
.env
|
|
10
|
+
*.egg-info/
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
htmlcov/
|
|
17
|
+
.coverage
|
|
18
|
+
coverage.xml
|
|
19
|
+
|
|
20
|
+
# Node / JS
|
|
21
|
+
node_modules/
|
|
22
|
+
.next/
|
|
23
|
+
.turbo/
|
|
24
|
+
dist/
|
|
25
|
+
build/
|
|
26
|
+
*.tsbuildinfo
|
|
27
|
+
.pnpm-store/
|
|
28
|
+
|
|
29
|
+
# Env files
|
|
30
|
+
.env
|
|
31
|
+
.env.local
|
|
32
|
+
.env.production
|
|
33
|
+
.env.*.local
|
|
34
|
+
api/.env.local
|
|
35
|
+
dashboard/.env.local
|
|
36
|
+
|
|
37
|
+
# Build artifacts inside packages
|
|
38
|
+
packages/python/dist/
|
|
39
|
+
packages/python/*.egg-info/
|
|
40
|
+
packages/js/dist/
|
|
41
|
+
packages/js/node_modules/
|
|
42
|
+
|
|
43
|
+
# Internal docs — never public
|
|
44
|
+
.internal/
|
|
45
|
+
PLAN.md
|
|
46
|
+
CODE.md
|
|
47
|
+
|
|
48
|
+
# OS
|
|
49
|
+
.DS_Store
|
|
50
|
+
Thumbs.db
|
|
51
|
+
|
|
52
|
+
# IDE
|
|
53
|
+
.vscode/
|
|
54
|
+
.idea/
|
|
55
|
+
*.swp
|
|
56
|
+
|
|
57
|
+
# Docker
|
|
58
|
+
*.log
|
|
59
|
+
.internal
|
|
60
|
+
|
|
61
|
+
# Local data (SQLite DB when running without Docker)
|
|
62
|
+
data/
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentmetrics-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AgentMetrics - self-hosted AI agent observability server
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: alembic>=1.13.1
|
|
8
|
+
Requires-Dist: apscheduler==3.10.4
|
|
9
|
+
Requires-Dist: email-validator==2.1.1
|
|
10
|
+
Requires-Dist: fastapi>=0.115.0
|
|
11
|
+
Requires-Dist: httpx==0.27.0
|
|
12
|
+
Requires-Dist: passlib[bcrypt]==1.7.4
|
|
13
|
+
Requires-Dist: pydantic-settings>=2.6.0
|
|
14
|
+
Requires-Dist: pydantic>=2.10.0
|
|
15
|
+
Requires-Dist: python-jose[cryptography]==3.3.0
|
|
16
|
+
Requires-Dist: python-multipart==0.0.9
|
|
17
|
+
Requires-Dist: requests==2.32.3
|
|
18
|
+
Requires-Dist: sqlalchemy>=2.0.30
|
|
19
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
20
|
+
Provides-Extra: postgres
|
|
21
|
+
Requires-Dist: psycopg2-binary>=2.9.9; extra == 'postgres'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# agentmetrics-server
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/agentmetrics-server)
|
|
27
|
+
[](https://pypi.org/project/agentmetrics-server)
|
|
28
|
+
[](../LICENSE)
|
|
29
|
+
|
|
30
|
+
Self-hosted AgentMetrics API server. Install it once and every agent instrumented with the AgentMetrics SDK reports to your own dashboard showing cost, latency, token usage, tool calls, and failures, with no cloud account and no data leaving your infrastructure.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install agentmetrics-server
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python 3.11 or later.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
Start the server in the foreground:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
agentmetrics-server
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Dashboard → **http://localhost:3099**
|
|
53
|
+
API → **http://localhost:8099**
|
|
54
|
+
|
|
55
|
+
Data is stored in `agentmetrics.db` in the current directory by default.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Always running
|
|
60
|
+
|
|
61
|
+
For continuous 24/7 observation across reboots and crashes, install as a system service instead of running in the foreground:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
agentmetrics install
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This registers the server with systemd on Linux, launchd on macOS, or Task Scheduler on Windows, starts it immediately, and configures it to come back automatically after any restart or crash. The database is stored in a persistent OS data directory so it is never lost across reinstalls.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
agentmetrics install # install and start with defaults
|
|
71
|
+
agentmetrics install --port 9000 # custom port
|
|
72
|
+
agentmetrics install --db postgresql://user:pass@localhost/mydb
|
|
73
|
+
|
|
74
|
+
agentmetrics start # start a stopped service
|
|
75
|
+
agentmetrics stop # stop without uninstalling
|
|
76
|
+
agentmetrics restart # restart
|
|
77
|
+
agentmetrics status # show service state and HTTP health check
|
|
78
|
+
agentmetrics uninstall # remove the service
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Platform behaviour
|
|
82
|
+
|
|
83
|
+
| Platform | Mechanism | Restarts on crash | Starts on boot |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| Linux (non-root) | systemd user service | yes | yes |
|
|
86
|
+
| Linux (root) | systemd system service | yes | yes |
|
|
87
|
+
| macOS | launchd user agent | yes | yes |
|
|
88
|
+
| Windows | Task Scheduler (ONLOGON) | no | yes (at logon) |
|
|
89
|
+
|
|
90
|
+
On Linux, the service unit is written to `~/.config/systemd/user/agentmetrics.service` when run as a normal user, or `/etc/systemd/system/agentmetrics.service` when run as root. Logs are available via `journalctl --user -u agentmetrics -f`.
|
|
91
|
+
|
|
92
|
+
On macOS, the plist is written to `~/Library/LaunchAgents/com.agentmetrics.server.plist` and logs go to `~/Library/Logs/AgentMetrics/`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
All options can be passed as flags or set via environment variables.
|
|
99
|
+
|
|
100
|
+
| Flag | Env var | Default | Description |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| `--port` | `PORT` | `8099` | Port to bind |
|
|
103
|
+
| `--host` | — | `0.0.0.0` | Host to bind |
|
|
104
|
+
| `--db` | `DATABASE_URL` | `sqlite:///./agentmetrics.db` | Database URL |
|
|
105
|
+
| `--open` | — | off | Open browser on startup |
|
|
106
|
+
|
|
107
|
+
### SQLite (default)
|
|
108
|
+
|
|
109
|
+
No setup needed. The database is created automatically in the current directory, or in the OS data directory when running as a service.
|
|
110
|
+
|
|
111
|
+
### PostgreSQL
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install "agentmetrics-server[postgres]"
|
|
115
|
+
agentmetrics-server --db postgresql://user:pass@localhost/agentmetrics
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or as a persistent service:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
agentmetrics install --db postgresql://user:pass@localhost/agentmetrics
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Foreground options
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
agentmetrics-server --port 9000
|
|
130
|
+
agentmetrics-server --host 127.0.0.1 # localhost only
|
|
131
|
+
agentmetrics-server --db postgresql://user:pass@localhost/mydb
|
|
132
|
+
agentmetrics-server --open # open dashboard in browser on start
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Health check
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
GET /health → {"status": "ok"}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
[MIT](../LICENSE)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# agentmetrics-server
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/agentmetrics-server)
|
|
4
|
+
[](https://pypi.org/project/agentmetrics-server)
|
|
5
|
+
[](../LICENSE)
|
|
6
|
+
|
|
7
|
+
Self-hosted AgentMetrics API server. Install it once and every agent instrumented with the AgentMetrics SDK reports to your own dashboard showing cost, latency, token usage, tool calls, and failures, with no cloud account and no data leaving your infrastructure.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install agentmetrics-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.11 or later.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
Start the server in the foreground:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
agentmetrics-server
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Dashboard → **http://localhost:3099**
|
|
30
|
+
API → **http://localhost:8099**
|
|
31
|
+
|
|
32
|
+
Data is stored in `agentmetrics.db` in the current directory by default.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Always running
|
|
37
|
+
|
|
38
|
+
For continuous 24/7 observation across reboots and crashes, install as a system service instead of running in the foreground:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
agentmetrics install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This registers the server with systemd on Linux, launchd on macOS, or Task Scheduler on Windows, starts it immediately, and configures it to come back automatically after any restart or crash. The database is stored in a persistent OS data directory so it is never lost across reinstalls.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
agentmetrics install # install and start with defaults
|
|
48
|
+
agentmetrics install --port 9000 # custom port
|
|
49
|
+
agentmetrics install --db postgresql://user:pass@localhost/mydb
|
|
50
|
+
|
|
51
|
+
agentmetrics start # start a stopped service
|
|
52
|
+
agentmetrics stop # stop without uninstalling
|
|
53
|
+
agentmetrics restart # restart
|
|
54
|
+
agentmetrics status # show service state and HTTP health check
|
|
55
|
+
agentmetrics uninstall # remove the service
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Platform behaviour
|
|
59
|
+
|
|
60
|
+
| Platform | Mechanism | Restarts on crash | Starts on boot |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| Linux (non-root) | systemd user service | yes | yes |
|
|
63
|
+
| Linux (root) | systemd system service | yes | yes |
|
|
64
|
+
| macOS | launchd user agent | yes | yes |
|
|
65
|
+
| Windows | Task Scheduler (ONLOGON) | no | yes (at logon) |
|
|
66
|
+
|
|
67
|
+
On Linux, the service unit is written to `~/.config/systemd/user/agentmetrics.service` when run as a normal user, or `/etc/systemd/system/agentmetrics.service` when run as root. Logs are available via `journalctl --user -u agentmetrics -f`.
|
|
68
|
+
|
|
69
|
+
On macOS, the plist is written to `~/Library/LaunchAgents/com.agentmetrics.server.plist` and logs go to `~/Library/Logs/AgentMetrics/`.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
All options can be passed as flags or set via environment variables.
|
|
76
|
+
|
|
77
|
+
| Flag | Env var | Default | Description |
|
|
78
|
+
|---|---|---|---|
|
|
79
|
+
| `--port` | `PORT` | `8099` | Port to bind |
|
|
80
|
+
| `--host` | — | `0.0.0.0` | Host to bind |
|
|
81
|
+
| `--db` | `DATABASE_URL` | `sqlite:///./agentmetrics.db` | Database URL |
|
|
82
|
+
| `--open` | — | off | Open browser on startup |
|
|
83
|
+
|
|
84
|
+
### SQLite (default)
|
|
85
|
+
|
|
86
|
+
No setup needed. The database is created automatically in the current directory, or in the OS data directory when running as a service.
|
|
87
|
+
|
|
88
|
+
### PostgreSQL
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install "agentmetrics-server[postgres]"
|
|
92
|
+
agentmetrics-server --db postgresql://user:pass@localhost/agentmetrics
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or as a persistent service:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
agentmetrics install --db postgresql://user:pass@localhost/agentmetrics
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Foreground options
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
agentmetrics-server --port 9000
|
|
107
|
+
agentmetrics-server --host 127.0.0.1 # localhost only
|
|
108
|
+
agentmetrics-server --db postgresql://user:pass@localhost/mydb
|
|
109
|
+
agentmetrics-server --open # open dashboard in browser on start
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Health check
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
GET /health → {"status": "ok"}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
[MIT](../LICENSE)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from logging.config import fileConfig
|
|
4
|
+
|
|
5
|
+
from alembic import context
|
|
6
|
+
from sqlalchemy import engine_from_config, pool
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
9
|
+
|
|
10
|
+
from app.config import settings
|
|
11
|
+
from app.database import Base
|
|
12
|
+
from app.models import Event, Organization # noqa: F401
|
|
13
|
+
|
|
14
|
+
config = context.config
|
|
15
|
+
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
|
16
|
+
|
|
17
|
+
if config.config_file_name is not None:
|
|
18
|
+
fileConfig(config.config_file_name)
|
|
19
|
+
|
|
20
|
+
target_metadata = Base.metadata
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_migrations_offline() -> None:
|
|
24
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
25
|
+
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
|
|
26
|
+
with context.begin_transaction():
|
|
27
|
+
context.run_migrations()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_migrations_online() -> None:
|
|
31
|
+
connectable = engine_from_config(
|
|
32
|
+
config.get_section(config.config_ini_section, {}),
|
|
33
|
+
prefix="sqlalchemy.",
|
|
34
|
+
poolclass=pool.NullPool,
|
|
35
|
+
)
|
|
36
|
+
with connectable.connect() as connection:
|
|
37
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
38
|
+
with context.begin_transaction():
|
|
39
|
+
context.run_migrations()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if context.is_offline_mode():
|
|
43
|
+
run_migrations_offline()
|
|
44
|
+
else:
|
|
45
|
+
run_migrations_online()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
${imports if imports else ""}
|
|
13
|
+
|
|
14
|
+
revision: str = ${repr(up_revision)}
|
|
15
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
16
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
17
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
${upgrades if upgrades else "pass"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def downgrade() -> None:
|
|
25
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""initial schema
|
|
2
|
+
|
|
3
|
+
Revision ID: 001
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2026-03-26
|
|
6
|
+
"""
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy.dialects import postgresql
|
|
12
|
+
|
|
13
|
+
revision: str = "001"
|
|
14
|
+
down_revision: str | None = None
|
|
15
|
+
branch_labels: str | Sequence[str] | None = None
|
|
16
|
+
depends_on: str | Sequence[str] | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade() -> None:
|
|
20
|
+
op.create_table(
|
|
21
|
+
"organizations",
|
|
22
|
+
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
|
23
|
+
sa.Column("email", sa.String(255), unique=True, nullable=False),
|
|
24
|
+
sa.Column("company_name", sa.String(255), nullable=False),
|
|
25
|
+
sa.Column("hashed_password", sa.String(255), nullable=False),
|
|
26
|
+
sa.Column("plan", sa.String(50), server_default="free", nullable=False),
|
|
27
|
+
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
|
28
|
+
)
|
|
29
|
+
op.create_index("ix_organizations_email", "organizations", ["email"])
|
|
30
|
+
|
|
31
|
+
op.create_table(
|
|
32
|
+
"api_keys",
|
|
33
|
+
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
|
34
|
+
sa.Column("org_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False),
|
|
35
|
+
sa.Column("key_hash", sa.String(255), unique=True, nullable=False),
|
|
36
|
+
sa.Column("name", sa.String(255), server_default="Default"),
|
|
37
|
+
sa.Column("active", sa.Boolean, server_default="true", nullable=False),
|
|
38
|
+
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
|
39
|
+
)
|
|
40
|
+
op.create_index("ix_api_keys_org_id", "api_keys", ["org_id"])
|
|
41
|
+
|
|
42
|
+
op.create_table(
|
|
43
|
+
"events",
|
|
44
|
+
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
|
45
|
+
sa.Column("org_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
46
|
+
sa.Column("trace_id", sa.String(255), nullable=False),
|
|
47
|
+
sa.Column("agent_id", sa.String(255), nullable=False),
|
|
48
|
+
sa.Column("status", sa.String(50), nullable=False),
|
|
49
|
+
sa.Column("duration_ms", sa.Float, nullable=True),
|
|
50
|
+
sa.Column("cost_usd", sa.Float, server_default="0"),
|
|
51
|
+
sa.Column("model", sa.String(100), nullable=True),
|
|
52
|
+
sa.Column("input_tokens", sa.Float, nullable=True),
|
|
53
|
+
sa.Column("output_tokens", sa.Float, nullable=True),
|
|
54
|
+
sa.Column("error_message", sa.Text, nullable=True),
|
|
55
|
+
sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
|
56
|
+
)
|
|
57
|
+
op.create_index("ix_events_timestamp", "events", ["timestamp"])
|
|
58
|
+
op.create_index("ix_events_org_agent_ts", "events", ["org_id", "agent_id", "timestamp"])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def downgrade() -> None:
|
|
62
|
+
op.drop_table("events")
|
|
63
|
+
op.drop_table("api_keys")
|
|
64
|
+
op.drop_table("organizations")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""placeholder migration (fills gap between 001 and 003)
|
|
2
|
+
|
|
3
|
+
Revision ID: 002
|
|
4
|
+
Revises: 001
|
|
5
|
+
Create Date: 2026-03-27
|
|
6
|
+
"""
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
revision: str = "002"
|
|
10
|
+
down_revision: str | None = "001"
|
|
11
|
+
branch_labels: str | Sequence[str] | None = None
|
|
12
|
+
depends_on: str | Sequence[str] | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def upgrade() -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def downgrade() -> None:
|
|
20
|
+
pass
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""supabase auth migration
|
|
2
|
+
|
|
3
|
+
Revision ID: 003
|
|
4
|
+
Revises: 002
|
|
5
|
+
Create Date: 2026-03-29
|
|
6
|
+
"""
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
|
|
12
|
+
revision: str = "003"
|
|
13
|
+
down_revision: str | None = "002"
|
|
14
|
+
branch_labels: str | Sequence[str] | None = None
|
|
15
|
+
depends_on: str | Sequence[str] | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def upgrade() -> None:
|
|
19
|
+
# Drop custom auth columns no longer needed
|
|
20
|
+
op.drop_column("organizations", "hashed_password")
|
|
21
|
+
|
|
22
|
+
# Add Supabase user ID for JWT-based auth
|
|
23
|
+
op.add_column(
|
|
24
|
+
"organizations",
|
|
25
|
+
sa.Column("supabase_user_id", sa.String(255), nullable=True),
|
|
26
|
+
)
|
|
27
|
+
op.create_index(
|
|
28
|
+
"ix_organizations_supabase_user_id",
|
|
29
|
+
"organizations",
|
|
30
|
+
["supabase_user_id"],
|
|
31
|
+
unique=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def downgrade() -> None:
|
|
36
|
+
op.drop_index("ix_organizations_supabase_user_id", table_name="organizations")
|
|
37
|
+
op.drop_column("organizations", "supabase_user_id")
|
|
38
|
+
op.add_column(
|
|
39
|
+
"organizations",
|
|
40
|
+
sa.Column("hashed_password", sa.String(255), nullable=False, server_default=""),
|
|
41
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""data integrity: FK on events, indexes, token type fix
|
|
2
|
+
|
|
3
|
+
Revision ID: 004
|
|
4
|
+
Revises: 003
|
|
5
|
+
Create Date: 2026-03-31
|
|
6
|
+
"""
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from alembic import op
|
|
10
|
+
|
|
11
|
+
revision: str = "004"
|
|
12
|
+
down_revision: str | None = "003"
|
|
13
|
+
branch_labels: str | Sequence[str] | None = None
|
|
14
|
+
depends_on: str | Sequence[str] | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def upgrade() -> None:
|
|
18
|
+
# Add FK from events.org_id → organizations.id with cascade delete
|
|
19
|
+
op.create_foreign_key(
|
|
20
|
+
"fk_events_org_id",
|
|
21
|
+
"events",
|
|
22
|
+
"organizations",
|
|
23
|
+
["org_id"],
|
|
24
|
+
["id"],
|
|
25
|
+
ondelete="CASCADE",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Add standalone index on events.org_id for fast per-org queries
|
|
29
|
+
op.create_index("ix_events_org_id", "events", ["org_id"])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def downgrade() -> None:
|
|
33
|
+
op.drop_index("ix_events_org_id", table_name="events")
|
|
34
|
+
op.drop_constraint("fk_events_org_id", "events", type_="foreignkey")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""agents table + metadata JSONB on events
|
|
2
|
+
|
|
3
|
+
Revision ID: 005
|
|
4
|
+
Revises: 004
|
|
5
|
+
Create Date: 2026-03-31
|
|
6
|
+
"""
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy import inspect
|
|
12
|
+
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
|
13
|
+
|
|
14
|
+
revision: str = "005"
|
|
15
|
+
down_revision: str | None = "004"
|
|
16
|
+
branch_labels: str | Sequence[str] | None = None
|
|
17
|
+
depends_on: str | Sequence[str] | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _tables():
|
|
21
|
+
return inspect(op.get_bind()).get_table_names()
|
|
22
|
+
|
|
23
|
+
def _columns(table):
|
|
24
|
+
return [c["name"] for c in inspect(op.get_bind()).get_columns(table)]
|
|
25
|
+
|
|
26
|
+
def _indexes(table):
|
|
27
|
+
return [i["name"] for i in inspect(op.get_bind()).get_indexes(table)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def upgrade() -> None:
|
|
31
|
+
tables = _tables()
|
|
32
|
+
|
|
33
|
+
if "agents" not in tables:
|
|
34
|
+
op.create_table(
|
|
35
|
+
"agents",
|
|
36
|
+
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
|
37
|
+
sa.Column("org_id", UUID(as_uuid=True), sa.ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False),
|
|
38
|
+
sa.Column("agent_id", sa.String(255), nullable=False),
|
|
39
|
+
sa.Column("display_name", sa.String(255), nullable=True),
|
|
40
|
+
sa.Column("description", sa.Text, nullable=True),
|
|
41
|
+
sa.Column("environment", sa.String(50), nullable=True),
|
|
42
|
+
sa.Column("tags", JSONB, nullable=True),
|
|
43
|
+
sa.Column("first_seen", sa.DateTime(timezone=True), server_default=sa.text("now()")),
|
|
44
|
+
sa.Column("last_seen", sa.DateTime(timezone=True), server_default=sa.text("now()")),
|
|
45
|
+
)
|
|
46
|
+
if "ix_agents_org_agent" not in _indexes("agents"):
|
|
47
|
+
op.create_index("ix_agents_org_agent", "agents", ["org_id", "agent_id"], unique=True)
|
|
48
|
+
|
|
49
|
+
existing_cols = _columns("events")
|
|
50
|
+
for col, typ in [
|
|
51
|
+
("metadata", JSONB),
|
|
52
|
+
("step_count", sa.Integer),
|
|
53
|
+
("tool_calls", sa.Integer),
|
|
54
|
+
("environment", sa.String(50)),
|
|
55
|
+
("version", sa.String(50)),
|
|
56
|
+
]:
|
|
57
|
+
if col not in existing_cols:
|
|
58
|
+
op.add_column("events", sa.Column(col, typ, nullable=True))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def downgrade() -> None:
|
|
62
|
+
for col in ("version", "environment", "tool_calls", "step_count", "metadata"):
|
|
63
|
+
op.drop_column("events", col)
|
|
64
|
+
op.drop_index("ix_agents_org_agent", table_name="agents")
|
|
65
|
+
op.drop_table("agents")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""steps table: per-step tracing within an agent run
|
|
2
|
+
|
|
3
|
+
Revision ID: 006
|
|
4
|
+
Revises: 005
|
|
5
|
+
Create Date: 2026-03-31
|
|
6
|
+
"""
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy import inspect
|
|
12
|
+
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
|
13
|
+
|
|
14
|
+
revision: str = "006"
|
|
15
|
+
down_revision: str | None = "005"
|
|
16
|
+
branch_labels: str | Sequence[str] | None = None
|
|
17
|
+
depends_on: str | Sequence[str] | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _tables():
|
|
21
|
+
return inspect(op.get_bind()).get_table_names()
|
|
22
|
+
|
|
23
|
+
def _indexes(table):
|
|
24
|
+
return [i["name"] for i in inspect(op.get_bind()).get_indexes(table)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def upgrade() -> None:
|
|
28
|
+
if "steps" not in _tables():
|
|
29
|
+
op.create_table(
|
|
30
|
+
"steps",
|
|
31
|
+
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
|
32
|
+
sa.Column("event_id", UUID(as_uuid=True), sa.ForeignKey("events.id", ondelete="CASCADE"), nullable=False),
|
|
33
|
+
sa.Column("org_id", UUID(as_uuid=True), sa.ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False),
|
|
34
|
+
sa.Column("trace_id", sa.String(255), nullable=False),
|
|
35
|
+
sa.Column("step_name", sa.String(255), nullable=False),
|
|
36
|
+
sa.Column("step_type", sa.String(50), nullable=True),
|
|
37
|
+
sa.Column("status", sa.String(50), nullable=False),
|
|
38
|
+
sa.Column("duration_ms", sa.Float, nullable=True),
|
|
39
|
+
sa.Column("model", sa.String(100), nullable=True),
|
|
40
|
+
sa.Column("input_tokens", sa.Integer, nullable=True),
|
|
41
|
+
sa.Column("output_tokens", sa.Integer, nullable=True),
|
|
42
|
+
sa.Column("cost_usd", sa.Float, default=0.0),
|
|
43
|
+
sa.Column("error_message", sa.Text, nullable=True),
|
|
44
|
+
sa.Column("metadata", JSONB, nullable=True),
|
|
45
|
+
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
|
46
|
+
sa.Column("ended_at", sa.DateTime(timezone=True), nullable=True),
|
|
47
|
+
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()")),
|
|
48
|
+
)
|
|
49
|
+
indexes = _indexes("steps")
|
|
50
|
+
if "ix_steps_event_id" not in indexes:
|
|
51
|
+
op.create_index("ix_steps_event_id", "steps", ["event_id"])
|
|
52
|
+
if "ix_steps_org_trace" not in indexes:
|
|
53
|
+
op.create_index("ix_steps_org_trace", "steps", ["org_id", "trace_id"])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def downgrade() -> None:
|
|
57
|
+
op.drop_index("ix_steps_org_trace", table_name="steps")
|
|
58
|
+
op.drop_index("ix_steps_event_id", table_name="steps")
|
|
59
|
+
op.drop_table("steps")
|