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.
- {provekit-0.2.0 → provekit-0.5.0}/PKG-INFO +24 -1
- provekit-0.5.0/provekit/__init__.py +18 -0
- provekit-0.5.0/provekit/auto.py +11 -0
- provekit-0.5.0/provekit/config.py +68 -0
- provekit-0.5.0/provekit/eval.py +74 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/main.py +9 -1
- provekit-0.5.0/provekit/mcp.py +83 -0
- provekit-0.5.0/provekit/migrations/versions/a1b2c3d4e5f6_feedback_sessions.py +49 -0
- provekit-0.5.0/provekit/migrations/versions/b2c3d4e5f6a7_datasets.py +54 -0
- provekit-0.5.0/provekit/migrations/versions/c3d4e5f6a7b8_experiments.py +59 -0
- provekit-0.5.0/provekit/migrations/versions/d4e5f6a7b8c9_alerts.py +40 -0
- provekit-0.5.0/provekit/migrations/versions/e5f6a7b8c9d0_admin.py +30 -0
- provekit-0.5.0/provekit/models.py +188 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/observability.py +2 -2
- provekit-0.5.0/provekit/routers/admin.py +82 -0
- provekit-0.5.0/provekit/routers/alerts.py +124 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/routers/auth.py +3 -1
- provekit-0.5.0/provekit/routers/datasets.py +151 -0
- provekit-0.5.0/provekit/routers/experiments.py +141 -0
- provekit-0.5.0/provekit/routers/metrics.py +143 -0
- provekit-0.5.0/provekit/routers/projects.py +140 -0
- provekit-0.5.0/provekit/routers/traces.py +275 -0
- provekit-0.5.0/provekit/scorers.py +63 -0
- provekit-0.5.0/provekit/services/limits.py +73 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/services/otel.py +25 -0
- provekit-0.5.0/provekit/services/redact.py +48 -0
- provekit-0.5.0/provekit/services/sealing.py +49 -0
- provekit-0.5.0/provekit/services/share.py +54 -0
- provekit-0.5.0/provekit/services/workspace.py +61 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/trace.py +152 -12
- {provekit-0.2.0 → provekit-0.5.0}/provekit.egg-info/PKG-INFO +24 -1
- {provekit-0.2.0 → provekit-0.5.0}/provekit.egg-info/SOURCES.txt +33 -3
- provekit-0.5.0/provekit.egg-info/entry_points.txt +2 -0
- provekit-0.5.0/provekit.egg-info/requires.txt +52 -0
- {provekit-0.2.0 → provekit-0.5.0}/pyproject.toml +41 -6
- provekit-0.5.0/tests/test_admin.py +72 -0
- provekit-0.5.0/tests/test_alerts.py +55 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_auth.py +4 -2
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_cov_limits_auth.py +13 -30
- provekit-0.5.0/tests/test_coverage_extra.py +295 -0
- provekit-0.5.0/tests/test_datasets.py +65 -0
- provekit-0.5.0/tests/test_eval.py +73 -0
- provekit-0.5.0/tests/test_experiments.py +51 -0
- provekit-0.5.0/tests/test_feedback.py +65 -0
- provekit-0.5.0/tests/test_hardening.py +49 -0
- provekit-0.5.0/tests/test_key_read_api.py +81 -0
- provekit-0.5.0/tests/test_mcp.py +61 -0
- provekit-0.5.0/tests/test_metrics.py +74 -0
- provekit-0.5.0/tests/test_projects.py +79 -0
- provekit-0.5.0/tests/test_redact.py +56 -0
- provekit-0.5.0/tests/test_scorers.py +32 -0
- provekit-0.5.0/tests/test_share.py +58 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_trace_sdk.py +78 -0
- provekit-0.2.0/provekit/config.py +0 -77
- provekit-0.2.0/provekit/models.py +0 -100
- provekit-0.2.0/provekit/routers/traces.py +0 -125
- provekit-0.2.0/provekit/services/__init__.py +0 -0
- provekit-0.2.0/provekit/services/limits.py +0 -109
- provekit-0.2.0/provekit/services/masking.py +0 -45
- provekit-0.2.0/provekit/services/runstore.py +0 -93
- provekit-0.2.0/provekit/services/sealing.py +0 -106
- provekit-0.2.0/provekit/services/workspace.py +0 -27
- provekit-0.2.0/provekit.egg-info/requires.txt +0 -26
- provekit-0.2.0/tests/test_runstore.py +0 -64
- {provekit-0.2.0 → provekit-0.5.0}/README.md +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/database.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/migrations/env.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/migrations/script.py.mako +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/migrations/versions/9d2c06bfa4e6_baseline.py +0 -0
- {provekit-0.2.0/provekit → provekit-0.5.0/provekit/routers}/__init__.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/routers/apikeys.py +0 -0
- {provekit-0.2.0/provekit/routers → provekit-0.5.0/provekit/services}/__init__.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/services/apikey.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/services/auth.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/services/deploy.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/services/email.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit/services/netguard.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit.egg-info/dependency_links.txt +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/provekit.egg-info/top_level.txt +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/setup.cfg +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_account_recovery.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_apikeys.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_auth_hardening.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_autoinstrument.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_netguard.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_observability.py +0 -0
- {provekit-0.2.0 → provekit-0.5.0}/tests/test_otel.py +0 -0
- {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.
|
|
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')
|