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.
Files changed (64) hide show
  1. agentmetrics_server-0.1.0/.gitignore +62 -0
  2. agentmetrics_server-0.1.0/PKG-INFO +147 -0
  3. agentmetrics_server-0.1.0/README.md +124 -0
  4. agentmetrics_server-0.1.0/alembic/env.py +45 -0
  5. agentmetrics_server-0.1.0/alembic/script.py.mako +25 -0
  6. agentmetrics_server-0.1.0/alembic/versions/001_initial_schema.py +64 -0
  7. agentmetrics_server-0.1.0/alembic/versions/002_placeholder.py +20 -0
  8. agentmetrics_server-0.1.0/alembic/versions/003_password_reset.py +41 -0
  9. agentmetrics_server-0.1.0/alembic/versions/004_data_integrity.py +34 -0
  10. agentmetrics_server-0.1.0/alembic/versions/005_agents_metadata.py +65 -0
  11. agentmetrics_server-0.1.0/alembic/versions/006_steps_table.py +59 -0
  12. agentmetrics_server-0.1.0/alembic/versions/007_metrics_aggregation.py +80 -0
  13. agentmetrics_server-0.1.0/alembic/versions/008_recommendations_alerts.py +100 -0
  14. agentmetrics_server-0.1.0/alembic/versions/009_org_settings.py +27 -0
  15. agentmetrics_server-0.1.0/alembic/versions/010_schema_fixes.py +70 -0
  16. agentmetrics_server-0.1.0/alembic/versions/011_perf_indexes.py +26 -0
  17. agentmetrics_server-0.1.0/alembic/versions/012_v2_schema_promotion.py +68 -0
  18. agentmetrics_server-0.1.0/alembic/versions/013_remove_supabase_add_password.py +45 -0
  19. agentmetrics_server-0.1.0/alembic/versions/014_schema_hardening.py +43 -0
  20. agentmetrics_server-0.1.0/alembic/versions/015_api_key_auth.py +27 -0
  21. agentmetrics_server-0.1.0/alembic.ini +38 -0
  22. agentmetrics_server-0.1.0/app/__init__.py +0 -0
  23. agentmetrics_server-0.1.0/app/config.py +26 -0
  24. agentmetrics_server-0.1.0/app/core/__init__.py +0 -0
  25. agentmetrics_server-0.1.0/app/core/activity_store.py +59 -0
  26. agentmetrics_server-0.1.0/app/core/pricing.py +191 -0
  27. agentmetrics_server-0.1.0/app/core/rate_limit.py +49 -0
  28. agentmetrics_server-0.1.0/app/database.py +29 -0
  29. agentmetrics_server-0.1.0/app/db_compat.py +73 -0
  30. agentmetrics_server-0.1.0/app/deps.py +63 -0
  31. agentmetrics_server-0.1.0/app/hooks.py +99 -0
  32. agentmetrics_server-0.1.0/app/main.py +299 -0
  33. agentmetrics_server-0.1.0/app/models/__init__.py +5 -0
  34. agentmetrics_server-0.1.0/app/models/event.py +57 -0
  35. agentmetrics_server-0.1.0/app/models/metrics.py +73 -0
  36. agentmetrics_server-0.1.0/app/models/organization.py +20 -0
  37. agentmetrics_server-0.1.0/app/routers/__init__.py +0 -0
  38. agentmetrics_server-0.1.0/app/routers/activity.py +122 -0
  39. agentmetrics_server-0.1.0/app/routers/agents.py +141 -0
  40. agentmetrics_server-0.1.0/app/routers/alerts.py +163 -0
  41. agentmetrics_server-0.1.0/app/routers/audit.py +149 -0
  42. agentmetrics_server-0.1.0/app/routers/auth.py +84 -0
  43. agentmetrics_server-0.1.0/app/routers/events.py +165 -0
  44. agentmetrics_server-0.1.0/app/routers/fleet.py +287 -0
  45. agentmetrics_server-0.1.0/app/routers/recommendations.py +66 -0
  46. agentmetrics_server-0.1.0/app/routers/runs.py +100 -0
  47. agentmetrics_server-0.1.0/app/routers/slo.py +150 -0
  48. agentmetrics_server-0.1.0/app/routers/stats.py +283 -0
  49. agentmetrics_server-0.1.0/app/schemas/__init__.py +0 -0
  50. agentmetrics_server-0.1.0/app/schemas/activity.py +30 -0
  51. agentmetrics_server-0.1.0/app/schemas/agent.py +140 -0
  52. agentmetrics_server-0.1.0/app/schemas/auth.py +20 -0
  53. agentmetrics_server-0.1.0/app/schemas/event.py +95 -0
  54. agentmetrics_server-0.1.0/app/services/__init__.py +0 -0
  55. agentmetrics_server-0.1.0/app/services/agent_service.py +465 -0
  56. agentmetrics_server-0.1.0/app/services/aggregation_service.py +351 -0
  57. agentmetrics_server-0.1.0/app/services/alert_service.py +343 -0
  58. agentmetrics_server-0.1.0/app/services/email_service.py +184 -0
  59. agentmetrics_server-0.1.0/app/services/recommendation_service.py +261 -0
  60. agentmetrics_server-0.1.0/app/worker.py +164 -0
  61. agentmetrics_server-0.1.0/cli.py +144 -0
  62. agentmetrics_server-0.1.0/daemon.py +377 -0
  63. agentmetrics_server-0.1.0/pyproject.toml +58 -0
  64. 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
+ [![PyPI](https://img.shields.io/pypi/v/agentmetrics-server?color=6366f1&label=pypi&logo=python&logoColor=white)](https://pypi.org/project/agentmetrics-server)
27
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-3776AB?logo=python&logoColor=white)](https://pypi.org/project/agentmetrics-server)
28
+ [![License: MIT](https://img.shields.io/badge/license-MIT-6366f1)](../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
+ [![PyPI](https://img.shields.io/pypi/v/agentmetrics-server?color=6366f1&label=pypi&logo=python&logoColor=white)](https://pypi.org/project/agentmetrics-server)
4
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-3776AB?logo=python&logoColor=white)](https://pypi.org/project/agentmetrics-server)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-6366f1)](../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")