provekit 0.2.0__tar.gz → 0.5.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 (88) hide show
  1. {provekit-0.2.0 → provekit-0.5.0}/PKG-INFO +24 -1
  2. provekit-0.5.0/provekit/__init__.py +18 -0
  3. provekit-0.5.0/provekit/auto.py +11 -0
  4. provekit-0.5.0/provekit/config.py +68 -0
  5. provekit-0.5.0/provekit/eval.py +74 -0
  6. {provekit-0.2.0 → provekit-0.5.0}/provekit/main.py +9 -1
  7. provekit-0.5.0/provekit/mcp.py +83 -0
  8. provekit-0.5.0/provekit/migrations/versions/a1b2c3d4e5f6_feedback_sessions.py +49 -0
  9. provekit-0.5.0/provekit/migrations/versions/b2c3d4e5f6a7_datasets.py +54 -0
  10. provekit-0.5.0/provekit/migrations/versions/c3d4e5f6a7b8_experiments.py +59 -0
  11. provekit-0.5.0/provekit/migrations/versions/d4e5f6a7b8c9_alerts.py +40 -0
  12. provekit-0.5.0/provekit/migrations/versions/e5f6a7b8c9d0_admin.py +30 -0
  13. provekit-0.5.0/provekit/models.py +188 -0
  14. {provekit-0.2.0 → provekit-0.5.0}/provekit/observability.py +2 -2
  15. provekit-0.5.0/provekit/routers/admin.py +82 -0
  16. provekit-0.5.0/provekit/routers/alerts.py +124 -0
  17. {provekit-0.2.0 → provekit-0.5.0}/provekit/routers/auth.py +3 -1
  18. provekit-0.5.0/provekit/routers/datasets.py +151 -0
  19. provekit-0.5.0/provekit/routers/experiments.py +141 -0
  20. provekit-0.5.0/provekit/routers/metrics.py +143 -0
  21. provekit-0.5.0/provekit/routers/projects.py +140 -0
  22. provekit-0.5.0/provekit/routers/traces.py +275 -0
  23. provekit-0.5.0/provekit/scorers.py +63 -0
  24. provekit-0.5.0/provekit/services/limits.py +73 -0
  25. {provekit-0.2.0 → provekit-0.5.0}/provekit/services/otel.py +25 -0
  26. provekit-0.5.0/provekit/services/redact.py +48 -0
  27. provekit-0.5.0/provekit/services/sealing.py +49 -0
  28. provekit-0.5.0/provekit/services/share.py +54 -0
  29. provekit-0.5.0/provekit/services/workspace.py +61 -0
  30. {provekit-0.2.0 → provekit-0.5.0}/provekit/trace.py +152 -12
  31. {provekit-0.2.0 → provekit-0.5.0}/provekit.egg-info/PKG-INFO +24 -1
  32. {provekit-0.2.0 → provekit-0.5.0}/provekit.egg-info/SOURCES.txt +33 -3
  33. provekit-0.5.0/provekit.egg-info/entry_points.txt +2 -0
  34. provekit-0.5.0/provekit.egg-info/requires.txt +52 -0
  35. {provekit-0.2.0 → provekit-0.5.0}/pyproject.toml +41 -6
  36. provekit-0.5.0/tests/test_admin.py +72 -0
  37. provekit-0.5.0/tests/test_alerts.py +55 -0
  38. {provekit-0.2.0 → provekit-0.5.0}/tests/test_auth.py +4 -2
  39. {provekit-0.2.0 → provekit-0.5.0}/tests/test_cov_limits_auth.py +13 -30
  40. provekit-0.5.0/tests/test_coverage_extra.py +295 -0
  41. provekit-0.5.0/tests/test_datasets.py +65 -0
  42. provekit-0.5.0/tests/test_eval.py +73 -0
  43. provekit-0.5.0/tests/test_experiments.py +51 -0
  44. provekit-0.5.0/tests/test_feedback.py +65 -0
  45. provekit-0.5.0/tests/test_hardening.py +49 -0
  46. provekit-0.5.0/tests/test_key_read_api.py +81 -0
  47. provekit-0.5.0/tests/test_mcp.py +61 -0
  48. provekit-0.5.0/tests/test_metrics.py +74 -0
  49. provekit-0.5.0/tests/test_projects.py +79 -0
  50. provekit-0.5.0/tests/test_redact.py +56 -0
  51. provekit-0.5.0/tests/test_scorers.py +32 -0
  52. provekit-0.5.0/tests/test_share.py +58 -0
  53. {provekit-0.2.0 → provekit-0.5.0}/tests/test_trace_sdk.py +78 -0
  54. provekit-0.2.0/provekit/config.py +0 -77
  55. provekit-0.2.0/provekit/models.py +0 -100
  56. provekit-0.2.0/provekit/routers/traces.py +0 -125
  57. provekit-0.2.0/provekit/services/__init__.py +0 -0
  58. provekit-0.2.0/provekit/services/limits.py +0 -109
  59. provekit-0.2.0/provekit/services/masking.py +0 -45
  60. provekit-0.2.0/provekit/services/runstore.py +0 -93
  61. provekit-0.2.0/provekit/services/sealing.py +0 -106
  62. provekit-0.2.0/provekit/services/workspace.py +0 -27
  63. provekit-0.2.0/provekit.egg-info/requires.txt +0 -26
  64. provekit-0.2.0/tests/test_runstore.py +0 -64
  65. {provekit-0.2.0 → provekit-0.5.0}/README.md +0 -0
  66. {provekit-0.2.0 → provekit-0.5.0}/provekit/database.py +0 -0
  67. {provekit-0.2.0 → provekit-0.5.0}/provekit/migrations/env.py +0 -0
  68. {provekit-0.2.0 → provekit-0.5.0}/provekit/migrations/script.py.mako +0 -0
  69. {provekit-0.2.0 → provekit-0.5.0}/provekit/migrations/versions/9d2c06bfa4e6_baseline.py +0 -0
  70. {provekit-0.2.0/provekit → provekit-0.5.0/provekit/routers}/__init__.py +0 -0
  71. {provekit-0.2.0 → provekit-0.5.0}/provekit/routers/apikeys.py +0 -0
  72. {provekit-0.2.0/provekit/routers → provekit-0.5.0/provekit/services}/__init__.py +0 -0
  73. {provekit-0.2.0 → provekit-0.5.0}/provekit/services/apikey.py +0 -0
  74. {provekit-0.2.0 → provekit-0.5.0}/provekit/services/auth.py +0 -0
  75. {provekit-0.2.0 → provekit-0.5.0}/provekit/services/deploy.py +0 -0
  76. {provekit-0.2.0 → provekit-0.5.0}/provekit/services/email.py +0 -0
  77. {provekit-0.2.0 → provekit-0.5.0}/provekit/services/netguard.py +0 -0
  78. {provekit-0.2.0 → provekit-0.5.0}/provekit.egg-info/dependency_links.txt +0 -0
  79. {provekit-0.2.0 → provekit-0.5.0}/provekit.egg-info/top_level.txt +0 -0
  80. {provekit-0.2.0 → provekit-0.5.0}/setup.cfg +0 -0
  81. {provekit-0.2.0 → provekit-0.5.0}/tests/test_account_recovery.py +0 -0
  82. {provekit-0.2.0 → provekit-0.5.0}/tests/test_apikeys.py +0 -0
  83. {provekit-0.2.0 → provekit-0.5.0}/tests/test_auth_hardening.py +0 -0
  84. {provekit-0.2.0 → provekit-0.5.0}/tests/test_autoinstrument.py +0 -0
  85. {provekit-0.2.0 → provekit-0.5.0}/tests/test_netguard.py +0 -0
  86. {provekit-0.2.0 → provekit-0.5.0}/tests/test_observability.py +0 -0
  87. {provekit-0.2.0 → provekit-0.5.0}/tests/test_otel.py +0 -0
  88. {provekit-0.2.0 → provekit-0.5.0}/tests/test_traces_tree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: provekit
3
- Version: 0.2.0
3
+ Version: 0.5.0
4
4
  Summary: Drop-in agent tracing — add one decorator, get a project key, review every run your agent makes.
5
5
  Author: ProveKit contributors
6
6
  License: MIT
@@ -38,6 +38,29 @@ Requires-Dist: opentelemetry-api<2,>=1.20; extra == "trace"
38
38
  Requires-Dist: opentelemetry-sdk<2,>=1.20; extra == "trace"
39
39
  Requires-Dist: openinference-instrumentation-openai>=0.1; extra == "trace"
40
40
  Requires-Dist: openinference-instrumentation-anthropic>=0.1; extra == "trace"
41
+ Provides-Extra: http
42
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.40b0; extra == "http"
43
+ Requires-Dist: opentelemetry-instrumentation-requests>=0.40b0; extra == "http"
44
+ Requires-Dist: opentelemetry-instrumentation-urllib>=0.40b0; extra == "http"
45
+ Provides-Extra: trace-all
46
+ Requires-Dist: opentelemetry-api<2,>=1.20; extra == "trace-all"
47
+ Requires-Dist: opentelemetry-sdk<2,>=1.20; extra == "trace-all"
48
+ Requires-Dist: openinference-instrumentation-openai>=0.1; extra == "trace-all"
49
+ Requires-Dist: openinference-instrumentation-anthropic>=0.1; extra == "trace-all"
50
+ Requires-Dist: openinference-instrumentation-langchain>=0.1; extra == "trace-all"
51
+ Requires-Dist: openinference-instrumentation-llama-index>=0.1; extra == "trace-all"
52
+ Requires-Dist: openinference-instrumentation-crewai>=0.1; extra == "trace-all"
53
+ Requires-Dist: openinference-instrumentation-bedrock>=0.1; extra == "trace-all"
54
+ Requires-Dist: openinference-instrumentation-litellm>=0.1; extra == "trace-all"
55
+ Requires-Dist: openinference-instrumentation-groq>=0.1; extra == "trace-all"
56
+ Requires-Dist: openinference-instrumentation-mistralai>=0.1; extra == "trace-all"
57
+ Requires-Dist: openinference-instrumentation-dspy>=0.1; extra == "trace-all"
58
+ Requires-Dist: openinference-instrumentation-haystack>=0.1; extra == "trace-all"
59
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.40b0; extra == "trace-all"
60
+ Requires-Dist: opentelemetry-instrumentation-requests>=0.40b0; extra == "trace-all"
61
+ Requires-Dist: opentelemetry-instrumentation-urllib>=0.40b0; extra == "trace-all"
62
+ Provides-Extra: mcp
63
+ Requires-Dist: mcp>=1.2; extra == "mcp"
41
64
  Provides-Extra: dev
42
65
  Requires-Dist: pytest==9.1.1; extra == "dev"
43
66
  Requires-Dist: pytest-cov==6.0.0; extra == "dev"
@@ -0,0 +1,18 @@
1
+ """ProveKit — drop-in agent tracing. One SDK on the client captures the whole flow.
2
+
3
+ import provekit.trace as pk # the canonical import
4
+ pk.init() # or, zero-code: import provekit.auto
5
+
6
+ @pk.trace(name="my-agent") # optional: group one run under a named root
7
+ def run(q): ...
8
+
9
+ Everything beneath your code — LLM providers, agent frameworks, outbound HTTP — is
10
+ captured automatically. `init`, `configure`, and `span` are also re-exported here for
11
+ `import provekit as pk` convenience; the `trace` decorator lives on the `provekit.trace`
12
+ submodule (a same-named top-level export would shadow that module).
13
+ """
14
+ from provekit import scorers
15
+ from provekit.eval import evaluate
16
+ from provekit.trace import configure, init, span
17
+
18
+ __all__ = ["init", "configure", "span", "evaluate", "scorers"]
@@ -0,0 +1,11 @@
1
+ """Zero-code activation: `import provekit.auto` turns tracing on with no other calls.
2
+
3
+ import provekit.auto # reads PROVEKIT_API_KEY / PROVEKIT_ENDPOINT and instruments
4
+
5
+ Put this once at your program's entrypoint (before the libraries you want traced do their
6
+ work). Everything instrumented below it is then captured automatically. Fail-open: if the
7
+ env vars are unset or OpenTelemetry isn't installed, this is a silent no-op.
8
+ """
9
+ from provekit.trace import init
10
+
11
+ init()
@@ -0,0 +1,68 @@
1
+ """ProveKit configuration."""
2
+ from functools import lru_cache
3
+
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
9
+
10
+ database_url: str = "sqlite:///./provekit.db"
11
+ cors_origins: str = "http://localhost:3001,http://localhost:3000"
12
+
13
+ # Hosted mode: a login wall, secure cookies, and stricter outbound-URL guarding.
14
+ hosted: bool = False
15
+
16
+ # Signs session/reset/verify tokens (any string → a Fernet key). Optional for local
17
+ # SQLite (a key file is auto-generated); required for any other database.
18
+ secret_key: str = ""
19
+
20
+ # Optional OTLP/HTTP collector to mirror captured runs to another backend.
21
+ otel_export_url: str = ""
22
+
23
+ # Optional Sentry DSN for error reporting (off when unset).
24
+ sentry_dsn: str = ""
25
+
26
+ # Web app base URL (for links in emails).
27
+ web_base_url: str = "http://localhost:3001"
28
+
29
+ # Email (account recovery / verification). No SMTP host → links are logged, not sent.
30
+ smtp_host: str = ""
31
+ smtp_port: int = 587
32
+ smtp_user: str = ""
33
+ smtp_password: str = ""
34
+ smtp_from: str = ""
35
+ smtp_starttls: bool = True
36
+ require_email_verification: bool = False
37
+
38
+ # Optional Redis (shared rate-limit windows across workers). Unset → in-memory.
39
+ redis_url: str = ""
40
+
41
+ thread_pool_size: int = 200
42
+
43
+ # Mask PII (emails, cards, SSNs, phones, common secret keys) in captured span
44
+ # input/output/error before it's stored. Off by default; a safety net for hosted use.
45
+ redact_pii: bool = False
46
+
47
+ # Bootstrap platform operators: any account whose email is in this comma-list is treated
48
+ # as a superuser (admin console), even before the DB flag is set. e.g. "you@co.com".
49
+ superuser_emails: str = ""
50
+
51
+ @property
52
+ def superuser_email_list(self) -> list[str]:
53
+ return [e.strip().lower() for e in self.superuser_emails.split(",") if e.strip()]
54
+
55
+ # Quotas (protect shared infra). 0 disables a given limit.
56
+ ingest_rate_per_min: int = 600 # trace-ingest requests per project per minute
57
+ login_attempts_per_min: int = 10 # login attempts per email+IP per minute
58
+ runs_retention: int = 10000 # keep last N spans per project (older ones pruned)
59
+ max_body_bytes: int = 2_000_000 # reject request bodies larger than this
60
+
61
+ @property
62
+ def cors_origin_list(self) -> list[str]:
63
+ return [o.strip() for o in self.cors_origins.split(",") if o.strip()]
64
+
65
+
66
+ @lru_cache
67
+ def get_settings() -> Settings:
68
+ return Settings()
@@ -0,0 +1,74 @@
1
+ """Offline evaluation from the client: run a target over a dataset, score each output, and
2
+ record it as an experiment in your portal — the basis of a CI regression gate.
3
+
4
+ import provekit as pk
5
+
6
+ def target(question: str) -> str:
7
+ return my_agent(question)
8
+
9
+ summary = pk.evaluate("qa-golden", target, scorers=["exact_match", "contains"])
10
+ assert summary["mean_score"] >= 0.8 # fail the build on a regression
11
+
12
+ `scorers` are names from provekit.scorers (exact_match/contains/regex_match/json_valid) or
13
+ your own `fn(output, expected) -> float`. The dataset is a name or id from your portal.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+
19
+ import httpx
20
+
21
+ from . import scorers as _scorers
22
+ from . import trace as _trace
23
+
24
+ _TIMEOUT = 60
25
+
26
+
27
+ def _get(endpoint, headers, path):
28
+ r = httpx.get(f"{endpoint}{path}", headers=headers, timeout=_TIMEOUT, follow_redirects=False)
29
+ r.raise_for_status()
30
+ return r.json()
31
+
32
+
33
+ def _post(endpoint, headers, path, body):
34
+ r = httpx.post(f"{endpoint}{path}", headers=headers, json=body, timeout=_TIMEOUT, follow_redirects=False)
35
+ r.raise_for_status()
36
+ return r.json()
37
+
38
+
39
+ def _resolve_dataset_id(endpoint, headers, dataset) -> int:
40
+ if isinstance(dataset, int):
41
+ return dataset
42
+ for d in _get(endpoint, headers, "/v1/datasets"):
43
+ if d.get("name") == dataset:
44
+ return d["id"]
45
+ raise ValueError(f"dataset {dataset!r} not found")
46
+
47
+
48
+ def evaluate(dataset, target, scorers, *, name: str = "experiment") -> dict:
49
+ """Run `target(input)` over every item in `dataset`, score each output with `scorers`,
50
+ post the results as a new experiment, and return its summary (id, result_count,
51
+ scorer_means, mean_score). Raises if ProveKit isn't configured."""
52
+ if not _trace.configure():
53
+ raise RuntimeError("ProveKit not configured: set PROVEKIT_API_KEY and PROVEKIT_ENDPOINT")
54
+ endpoint, headers = _trace._endpoint, {"Authorization": f"Bearer {_trace._api_key}"}
55
+
56
+ dsid = _resolve_dataset_id(endpoint, headers, dataset)
57
+ items = _get(endpoint, headers, f"/v1/datasets/{dsid}/items")
58
+ exp = _post(endpoint, headers, "/v1/experiments", {"name": name, "dataset_id": dsid})
59
+ eid = exp["id"]
60
+
61
+ for it in items:
62
+ try:
63
+ output = target(it["input"])
64
+ except Exception as exc: # a target failure is a data point, not a crash
65
+ output = f"ERROR: {exc}"
66
+ if not isinstance(output, str):
67
+ output = json.dumps(output, default=str)
68
+ expected = it.get("expected", "")
69
+ scores = _scorers.run_scorers(scorers, output, expected)
70
+ _post(endpoint, headers, f"/v1/experiments/{eid}/results",
71
+ {"item_id": it.get("id"), "input": it["input"], "output": output,
72
+ "expected": expected, "scores": scores})
73
+
74
+ return _get(endpoint, headers, f"/v1/experiments/{eid}")
@@ -16,7 +16,7 @@ from .observability import (
16
16
  init_sentry,
17
17
  setup_logging,
18
18
  )
19
- from .routers import apikeys, auth, traces
19
+ from .routers import admin, alerts, apikeys, auth, datasets, experiments, metrics, projects, traces
20
20
 
21
21
  logging.basicConfig(level=logging.INFO)
22
22
  settings = get_settings()
@@ -81,6 +81,14 @@ app.include_router(apikeys.router)
81
81
  app.include_router(traces.router)
82
82
  app.include_router(traces.ws_router)
83
83
  app.include_router(traces.runs_router)
84
+ app.include_router(datasets.router)
85
+ app.include_router(datasets.key_router)
86
+ app.include_router(experiments.router)
87
+ app.include_router(experiments.key_router)
88
+ app.include_router(metrics.router)
89
+ app.include_router(alerts.router)
90
+ app.include_router(projects.router)
91
+ app.include_router(admin.router)
84
92
 
85
93
 
86
94
  @app.get("/")
@@ -0,0 +1,83 @@
1
+ """ProveKit MCP server — debug your traces from any MCP client (Claude Desktop, Cursor, …).
2
+
3
+ This is a *debug channel*, not a second client SDK: your agent still installs only the one
4
+ `provekit` tracing SDK. This server runs wherever you like (your laptop, CI) and reads the
5
+ already-captured traces from your portal over HTTP, authenticated by the same project key.
6
+
7
+ pip install "provekit[mcp]"
8
+ export PROVEKIT_API_KEY=pk_... # your project key
9
+ export PROVEKIT_ENDPOINT=https://provekit.your-co.com
10
+ provekit-mcp # speaks MCP over stdio
11
+
12
+ Then point an MCP client at the `provekit-mcp` command. It exposes tools to list recent
13
+ traces, surface failures, and pull a full span tree — so the assistant can reason over an
14
+ agent run and tell you what broke, with no code added to the traced app.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+
20
+ import httpx
21
+
22
+ _TIMEOUT = 15
23
+
24
+
25
+ def _base() -> tuple[str, dict]:
26
+ """Resolve the portal URL + auth header from the environment, or raise a clear error."""
27
+ key = os.environ.get("PROVEKIT_API_KEY")
28
+ endpoint = os.environ.get("PROVEKIT_ENDPOINT")
29
+ if not key or not endpoint:
30
+ raise RuntimeError(
31
+ "Set PROVEKIT_API_KEY (your pk_ project key) and PROVEKIT_ENDPOINT "
32
+ "(your portal URL) before starting the ProveKit MCP server."
33
+ )
34
+ return endpoint.rstrip("/"), {"Authorization": f"Bearer {key}"}
35
+
36
+
37
+ def _get(path: str, **params):
38
+ url, headers = _base()
39
+ r = httpx.get(f"{url}/v1{path}", headers=headers, params=params, timeout=_TIMEOUT,
40
+ follow_redirects=False)
41
+ r.raise_for_status()
42
+ return r.json()
43
+
44
+
45
+ # ---- the callable data layer (import-safe; used by the MCP tools and by tests) ----
46
+ def list_traces(limit: int = 20, status: str | None = None) -> list[dict]:
47
+ """Recent traces, newest first. Pass status='failed' to see only failed runs."""
48
+ return _get("/traces", limit=limit, **({"status": status} if status else {}))
49
+
50
+
51
+ def list_failures(limit: int = 20) -> list[dict]:
52
+ """Just the traces that ended in a failure — the ones worth debugging first."""
53
+ return list_traces(limit=limit, status="failed")
54
+
55
+
56
+ def get_trace(trace_id: str) -> list[dict]:
57
+ """Every span of one trace (the full nested flow), in start order."""
58
+ return _get(f"/traces/{trace_id}")
59
+
60
+
61
+ def build_server(): # pragma: no cover - needs the optional `mcp` dep; glue over the tested data layer
62
+ """Construct the FastMCP server with the trace tools registered. Imported lazily so the
63
+ module stays importable (and testable) without the optional `mcp` dependency. The three
64
+ data-layer functions above are registered directly as MCP tools."""
65
+ from mcp.server.fastmcp import FastMCP
66
+
67
+ server = FastMCP("provekit")
68
+ server.add_tool(list_traces, name="provekit_list_traces",
69
+ description="List recent ProveKit traces (agent runs). status='failed' filters to failures.")
70
+ server.add_tool(list_failures, name="provekit_list_failures",
71
+ description="List recent traces that ended in a failure — start debugging here.")
72
+ server.add_tool(get_trace, name="provekit_get_trace",
73
+ description="Fetch the full span tree of one trace by trace_id to inspect what happened.")
74
+ return server
75
+
76
+
77
+ def main() -> None: # pragma: no cover - blocks serving MCP over stdio; exercised by hand
78
+ """Console-script entrypoint: `provekit-mcp`. Speaks MCP over stdio."""
79
+ build_server().run()
80
+
81
+
82
+ if __name__ == "__main__": # pragma: no cover
83
+ main()
@@ -0,0 +1,49 @@
1
+ """feedback table + runs.session_id
2
+
3
+ Revision ID: a1b2c3d4e5f6
4
+ Revises: 9d2c06bfa4e6
5
+ Create Date: 2026-07-19 00:00:00.000000
6
+ """
7
+ from alembic import op
8
+ import sqlalchemy as sa
9
+
10
+
11
+ revision = 'a1b2c3d4e5f6'
12
+ down_revision = '9d2c06bfa4e6'
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ op.create_table('feedback',
19
+ sa.Column('id', sa.Integer(), nullable=False),
20
+ sa.Column('workspace_id', sa.Integer(), nullable=True),
21
+ sa.Column('trace_id', sa.String(length=32), nullable=False),
22
+ sa.Column('name', sa.String(length=120), nullable=False),
23
+ sa.Column('score', sa.Float(), nullable=True),
24
+ sa.Column('value', sa.String(length=200), nullable=False),
25
+ sa.Column('comment', sa.Text(), nullable=False),
26
+ sa.Column('source', sa.String(length=16), nullable=False),
27
+ sa.Column('created_at', sa.DateTime(), nullable=False),
28
+ sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], name=op.f('fk_feedback_workspace_id_workspaces')),
29
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_feedback'))
30
+ )
31
+ with op.batch_alter_table('feedback', schema=None) as batch_op:
32
+ batch_op.create_index(batch_op.f('ix_feedback_trace_id'), ['trace_id'], unique=False)
33
+ batch_op.create_index(batch_op.f('ix_feedback_workspace_id'), ['workspace_id'], unique=False)
34
+
35
+ with op.batch_alter_table('runs', schema=None) as batch_op:
36
+ batch_op.add_column(sa.Column('session_id', sa.String(length=64), nullable=False,
37
+ server_default=''))
38
+ batch_op.create_index(batch_op.f('ix_runs_session_id'), ['session_id'], unique=False)
39
+
40
+
41
+ def downgrade() -> None:
42
+ with op.batch_alter_table('runs', schema=None) as batch_op:
43
+ batch_op.drop_index(batch_op.f('ix_runs_session_id'))
44
+ batch_op.drop_column('session_id')
45
+
46
+ with op.batch_alter_table('feedback', schema=None) as batch_op:
47
+ batch_op.drop_index(batch_op.f('ix_feedback_workspace_id'))
48
+ batch_op.drop_index(batch_op.f('ix_feedback_trace_id'))
49
+ op.drop_table('feedback')
@@ -0,0 +1,54 @@
1
+ """datasets + dataset_items
2
+
3
+ Revision ID: b2c3d4e5f6a7
4
+ Revises: a1b2c3d4e5f6
5
+ Create Date: 2026-07-19 01:00:00.000000
6
+ """
7
+ from alembic import op
8
+ import sqlalchemy as sa
9
+
10
+
11
+ revision = 'b2c3d4e5f6a7'
12
+ down_revision = 'a1b2c3d4e5f6'
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ op.create_table('datasets',
19
+ sa.Column('id', sa.Integer(), nullable=False),
20
+ sa.Column('workspace_id', sa.Integer(), nullable=True),
21
+ sa.Column('name', sa.String(length=160), nullable=False),
22
+ sa.Column('description', sa.Text(), nullable=False),
23
+ sa.Column('created_at', sa.DateTime(), nullable=False),
24
+ sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], name=op.f('fk_datasets_workspace_id_workspaces')),
25
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_datasets'))
26
+ )
27
+ with op.batch_alter_table('datasets', schema=None) as batch_op:
28
+ batch_op.create_index(batch_op.f('ix_datasets_workspace_id'), ['workspace_id'], unique=False)
29
+
30
+ op.create_table('dataset_items',
31
+ sa.Column('id', sa.Integer(), nullable=False),
32
+ sa.Column('workspace_id', sa.Integer(), nullable=True),
33
+ sa.Column('dataset_id', sa.Integer(), nullable=False),
34
+ sa.Column('input', sa.Text(), nullable=False),
35
+ sa.Column('expected', sa.Text(), nullable=False),
36
+ sa.Column('meta', sa.JSON(), nullable=False),
37
+ sa.Column('created_at', sa.DateTime(), nullable=False),
38
+ sa.ForeignKeyConstraint(['dataset_id'], ['datasets.id'], name=op.f('fk_dataset_items_dataset_id_datasets')),
39
+ sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], name=op.f('fk_dataset_items_workspace_id_workspaces')),
40
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_dataset_items'))
41
+ )
42
+ with op.batch_alter_table('dataset_items', schema=None) as batch_op:
43
+ batch_op.create_index(batch_op.f('ix_dataset_items_dataset_id'), ['dataset_id'], unique=False)
44
+ batch_op.create_index(batch_op.f('ix_dataset_items_workspace_id'), ['workspace_id'], unique=False)
45
+
46
+
47
+ def downgrade() -> None:
48
+ with op.batch_alter_table('dataset_items', schema=None) as batch_op:
49
+ batch_op.drop_index(batch_op.f('ix_dataset_items_workspace_id'))
50
+ batch_op.drop_index(batch_op.f('ix_dataset_items_dataset_id'))
51
+ op.drop_table('dataset_items')
52
+ with op.batch_alter_table('datasets', schema=None) as batch_op:
53
+ batch_op.drop_index(batch_op.f('ix_datasets_workspace_id'))
54
+ op.drop_table('datasets')
@@ -0,0 +1,59 @@
1
+ """experiments + experiment_results
2
+
3
+ Revision ID: c3d4e5f6a7b8
4
+ Revises: b2c3d4e5f6a7
5
+ Create Date: 2026-07-19 02:00:00.000000
6
+ """
7
+ from alembic import op
8
+ import sqlalchemy as sa
9
+
10
+
11
+ revision = 'c3d4e5f6a7b8'
12
+ down_revision = 'b2c3d4e5f6a7'
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ op.create_table('experiments',
19
+ sa.Column('id', sa.Integer(), nullable=False),
20
+ sa.Column('workspace_id', sa.Integer(), nullable=True),
21
+ sa.Column('name', sa.String(length=160), nullable=False),
22
+ sa.Column('dataset_id', sa.Integer(), nullable=True),
23
+ sa.Column('created_at', sa.DateTime(), nullable=False),
24
+ sa.ForeignKeyConstraint(['dataset_id'], ['datasets.id'], name=op.f('fk_experiments_dataset_id_datasets')),
25
+ sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], name=op.f('fk_experiments_workspace_id_workspaces')),
26
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_experiments'))
27
+ )
28
+ with op.batch_alter_table('experiments', schema=None) as batch_op:
29
+ batch_op.create_index(batch_op.f('ix_experiments_dataset_id'), ['dataset_id'], unique=False)
30
+ batch_op.create_index(batch_op.f('ix_experiments_workspace_id'), ['workspace_id'], unique=False)
31
+
32
+ op.create_table('experiment_results',
33
+ sa.Column('id', sa.Integer(), nullable=False),
34
+ sa.Column('workspace_id', sa.Integer(), nullable=True),
35
+ sa.Column('experiment_id', sa.Integer(), nullable=False),
36
+ sa.Column('item_id', sa.Integer(), nullable=True),
37
+ sa.Column('input', sa.Text(), nullable=False),
38
+ sa.Column('output', sa.Text(), nullable=False),
39
+ sa.Column('expected', sa.Text(), nullable=False),
40
+ sa.Column('scores', sa.JSON(), nullable=False),
41
+ sa.Column('created_at', sa.DateTime(), nullable=False),
42
+ sa.ForeignKeyConstraint(['experiment_id'], ['experiments.id'], name=op.f('fk_experiment_results_experiment_id_experiments')),
43
+ sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], name=op.f('fk_experiment_results_workspace_id_workspaces')),
44
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_experiment_results'))
45
+ )
46
+ with op.batch_alter_table('experiment_results', schema=None) as batch_op:
47
+ batch_op.create_index(batch_op.f('ix_experiment_results_experiment_id'), ['experiment_id'], unique=False)
48
+ batch_op.create_index(batch_op.f('ix_experiment_results_workspace_id'), ['workspace_id'], unique=False)
49
+
50
+
51
+ def downgrade() -> None:
52
+ with op.batch_alter_table('experiment_results', schema=None) as batch_op:
53
+ batch_op.drop_index(batch_op.f('ix_experiment_results_workspace_id'))
54
+ batch_op.drop_index(batch_op.f('ix_experiment_results_experiment_id'))
55
+ op.drop_table('experiment_results')
56
+ with op.batch_alter_table('experiments', schema=None) as batch_op:
57
+ batch_op.drop_index(batch_op.f('ix_experiments_workspace_id'))
58
+ batch_op.drop_index(batch_op.f('ix_experiments_dataset_id'))
59
+ op.drop_table('experiments')
@@ -0,0 +1,40 @@
1
+ """alerts
2
+
3
+ Revision ID: d4e5f6a7b8c9
4
+ Revises: c3d4e5f6a7b8
5
+ Create Date: 2026-07-19 03:00:00.000000
6
+ """
7
+ from alembic import op
8
+ import sqlalchemy as sa
9
+
10
+
11
+ revision = 'd4e5f6a7b8c9'
12
+ down_revision = 'c3d4e5f6a7b8'
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ op.create_table('alerts',
19
+ sa.Column('id', sa.Integer(), nullable=False),
20
+ sa.Column('workspace_id', sa.Integer(), nullable=True),
21
+ sa.Column('name', sa.String(length=160), nullable=False),
22
+ sa.Column('metric', sa.String(length=32), nullable=False),
23
+ sa.Column('comparator', sa.String(length=4), nullable=False),
24
+ sa.Column('threshold', sa.Float(), nullable=False),
25
+ sa.Column('window_hours', sa.Integer(), nullable=False),
26
+ sa.Column('email', sa.String(length=255), nullable=False),
27
+ sa.Column('enabled', sa.Boolean(), nullable=False),
28
+ sa.Column('last_triggered_at', sa.DateTime(), nullable=True),
29
+ sa.Column('created_at', sa.DateTime(), nullable=False),
30
+ sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], name=op.f('fk_alerts_workspace_id_workspaces')),
31
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_alerts'))
32
+ )
33
+ with op.batch_alter_table('alerts', schema=None) as batch_op:
34
+ batch_op.create_index(batch_op.f('ix_alerts_workspace_id'), ['workspace_id'], unique=False)
35
+
36
+
37
+ def downgrade() -> None:
38
+ with op.batch_alter_table('alerts', schema=None) as batch_op:
39
+ batch_op.drop_index(batch_op.f('ix_alerts_workspace_id'))
40
+ op.drop_table('alerts')
@@ -0,0 +1,30 @@
1
+ """users.is_superuser + workspaces.retention/redact_pii
2
+
3
+ Revision ID: e5f6a7b8c9d0
4
+ Revises: d4e5f6a7b8c9
5
+ Create Date: 2026-07-19 04:00:00.000000
6
+ """
7
+ from alembic import op
8
+ import sqlalchemy as sa
9
+
10
+
11
+ revision = 'e5f6a7b8c9d0'
12
+ down_revision = 'd4e5f6a7b8c9'
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ with op.batch_alter_table('users', schema=None) as batch_op:
19
+ batch_op.add_column(sa.Column('is_superuser', sa.Boolean(), nullable=False, server_default=sa.false()))
20
+ with op.batch_alter_table('workspaces', schema=None) as batch_op:
21
+ batch_op.add_column(sa.Column('retention', sa.Integer(), nullable=False, server_default='0'))
22
+ batch_op.add_column(sa.Column('redact_pii', sa.Boolean(), nullable=False, server_default=sa.false()))
23
+
24
+
25
+ def downgrade() -> None:
26
+ with op.batch_alter_table('workspaces', schema=None) as batch_op:
27
+ batch_op.drop_column('redact_pii')
28
+ batch_op.drop_column('retention')
29
+ with op.batch_alter_table('users', schema=None) as batch_op:
30
+ batch_op.drop_column('is_superuser')