zentest-sdk 0.4.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 (40) hide show
  1. zentest_sdk-0.4.0/CHANGELOG.md +109 -0
  2. zentest_sdk-0.4.0/MANIFEST.in +6 -0
  3. zentest_sdk-0.4.0/PKG-INFO +243 -0
  4. zentest_sdk-0.4.0/README.md +200 -0
  5. zentest_sdk-0.4.0/RELEASING.md +118 -0
  6. zentest_sdk-0.4.0/pyproject.toml +66 -0
  7. zentest_sdk-0.4.0/setup.cfg +4 -0
  8. zentest_sdk-0.4.0/zentest/__init__.py +36 -0
  9. zentest_sdk-0.4.0/zentest/_checks.py +1275 -0
  10. zentest_sdk-0.4.0/zentest/_version.py +3 -0
  11. zentest_sdk-0.4.0/zentest/adapters.py +86 -0
  12. zentest_sdk-0.4.0/zentest/assertions.py +159 -0
  13. zentest_sdk-0.4.0/zentest/cli.py +580 -0
  14. zentest_sdk-0.4.0/zentest/client.py +375 -0
  15. zentest_sdk-0.4.0/zentest/commands.py +269 -0
  16. zentest_sdk-0.4.0/zentest/declarative.py +249 -0
  17. zentest_sdk-0.4.0/zentest/errors.py +60 -0
  18. zentest_sdk-0.4.0/zentest/guard.py +237 -0
  19. zentest_sdk-0.4.0/zentest/judges.py +313 -0
  20. zentest_sdk-0.4.0/zentest/metrics/__init__.py +28 -0
  21. zentest_sdk-0.4.0/zentest/metrics/base.py +85 -0
  22. zentest_sdk-0.4.0/zentest/metrics/deterministic.py +192 -0
  23. zentest_sdk-0.4.0/zentest/metrics/judged.py +118 -0
  24. zentest_sdk-0.4.0/zentest/metrics/retrieval.py +149 -0
  25. zentest_sdk-0.4.0/zentest/offline.py +279 -0
  26. zentest_sdk-0.4.0/zentest/otel.py +162 -0
  27. zentest_sdk-0.4.0/zentest/py.typed +0 -0
  28. zentest_sdk-0.4.0/zentest/pytest_plugin.py +97 -0
  29. zentest_sdk-0.4.0/zentest/redteam.py +87 -0
  30. zentest_sdk-0.4.0/zentest/runner.py +315 -0
  31. zentest_sdk-0.4.0/zentest/simulate.py +170 -0
  32. zentest_sdk-0.4.0/zentest/sync.py +66 -0
  33. zentest_sdk-0.4.0/zentest/tracing.py +423 -0
  34. zentest_sdk-0.4.0/zentest/types.py +112 -0
  35. zentest_sdk-0.4.0/zentest_sdk.egg-info/PKG-INFO +243 -0
  36. zentest_sdk-0.4.0/zentest_sdk.egg-info/SOURCES.txt +38 -0
  37. zentest_sdk-0.4.0/zentest_sdk.egg-info/dependency_links.txt +1 -0
  38. zentest_sdk-0.4.0/zentest_sdk.egg-info/entry_points.txt +5 -0
  39. zentest_sdk-0.4.0/zentest_sdk.egg-info/requires.txt +22 -0
  40. zentest_sdk-0.4.0/zentest_sdk.egg-info/top_level.txt +1 -0
@@ -0,0 +1,109 @@
1
+ # Changelog
2
+
3
+ All notable changes to `zentest-sdk`, the Python SDK for the Zentest platform. The SDK implements the
4
+ [SDK contract v1](https://github.com/ITHyd/Zentest/blob/main/docs/specs/sdk-contract.md); every release so far is
5
+ additive within v1, so code written against an earlier release keeps working.
6
+
7
+ Versions follow [Semantic Versioning](https://semver.org/). Until 1.0, a minor release may add to the API but does not
8
+ remove or change what the contract marks as public (the adapter protocol, the `Verdict` shape and runner parity).
9
+ Release steps are in [RELEASING.md](https://github.com/ITHyd/Zentest/blob/main/sdk/RELEASING.md).
10
+
11
+ ## 0.4.0 (2026-10-19)
12
+
13
+ The first release published to PyPI (`pip install zentest-sdk`), built, tested and uploaded by an Azure Pipelines
14
+ release pipeline. Local evaluation: the SDK becomes a testing library
15
+ you can use without the platform, and that records on it when you want. Everything in 0.3.0 is unchanged.
16
+
17
+ ### Added
18
+ - `zentest.metrics`: a metrics library. Deterministic checks (`PIILeak`, `Forbidden`, `Contains`, `Regex`, `ExactMatch`,
19
+ `Format`, `ReadingLevel`, `Disclaimer`, `Escalation`, `ToolCorrectness`, `JudgeGaming`, and any platform check through
20
+ `Check`), deterministic retrieval metrics (`Groundedness`, `ContextPrecision`, `ContextRecall`) and judge metrics
21
+ (`Faithfulness`, `AnswerRelevancy`, `ContextRelevance` (the RAG triad), `Hallucination`, `Toxicity`, `Bias`, `GEval`).
22
+ Every score records `model_snapshot`, `prompt_hash` and `judge_version`.
23
+ - `zentest.judges`: the judge for judge metrics is pluggable. `PlatformJudge` scores through the platform's gateway
24
+ (`POST /api/sdk/evaluate`), `LocalJudge` calls Claude (`pip install "zentest-sdk[judge]"`) or an OpenRouter model with
25
+ your key, and `HeuristicJudge` makes no model call. Pick one with `judge=` or `ZENTEST_JUDGE`.
26
+ - `assert_test`, `evaluate` and `EvalReport`, plus a pytest plugin (auto-registered): a summary table,
27
+ `--zentest-report` JSON, `--zentest-judge`, and `--zentest-sync` to record the session as a platform run.
28
+ - `zentest test FILE`: declarative test files (cases, assertions, a Python or HTTP target, `repeats`, `pass_rate`),
29
+ with `--junit`, `--json-file`, `--baseline` (a case that passed before and fails now exits 1) and `--sync`.
30
+ `zentest init` scaffolds a test file and a pytest file.
31
+ - `zt.redteam.scan` and `zentest redteam`: generate the product's red-team suite from the platform's attack catalogue,
32
+ run it against your agent, and get the per-class and OWASP report.
33
+ - `zentest.guard`: runtime guardrails (prompt injection, PII in the input, topic denylist and any deterministic metric
34
+ on the output) with `block`, `redact`, `raise` or `log`, recorded as `guardrail` spans.
35
+ - `zentest.simulate`: seeded multi-turn conversations with a persona and a goal, scored per turn (the platform's
36
+ simulator script).
37
+ - `zentest.sync`: record local evaluations as a platform run (`POST /api/sdk/suites` finds or creates the suite).
38
+
39
+ ## 0.3.0 (2026-10-17)
40
+
41
+ Prepared for PyPI but not published; 0.1.0 to 0.3.0 were installed from the repository.
42
+
43
+ ### Added
44
+ - `gates.run` takes the run options `start` already had: `connection_id` (the HTTP connection an endpoint run calls when
45
+ the environment has several), `corpus` with `corpus_data_source` (UK-31: the retrieval corpus, scanned when the run
46
+ starts so contaminated cases are skipped), `osa_scope` (UK-10) and `trigger` (`"api"` or `"ci"`). `connection_id`
47
+ with an in-process `agent` raises `ValueError` before anything is created.
48
+ - `start` gained `connection_id` (several HTTP connections per environment; the run pins the one it calls) and `corpus`
49
+ with `corpus_data_source`.
50
+ - `Verdict.contamination` (UK-31): the cases found in the app's retrieval corpus, which were excluded, kept or skipped.
51
+ - The SDK runner also submits results on a time interval, not only every `batch_size` trials: `flush_every_s` on
52
+ `gates.run` and `Zentest(...)`, or `ZENTEST_FLUSH_EVERY_S` (default 30 s; `0` sends full batches only), so a slow
53
+ app's run shows progress in the console as it goes (SDK-07).
54
+ - CLI: `zentest run --junit PATH` writes the result as JUnit XML for CI test reports; a suite file can pin a
55
+ `connection`; a metric with no counted trials prints "no counted trials" instead of 0%.
56
+ - Packaging: published metadata (readme, licence, authors, classifiers, project URLs), a PEP 561 `py.typed` marker, this
57
+ changelog and a release workflow with PyPI trusted publishing.
58
+
59
+ ### Changed
60
+ - The version is written once, in `zentest/_version.py`; `pyproject.toml`, `zentest.__version__`, the `User-Agent` and
61
+ the SDK-19 minimum-version check all read it.
62
+ - The offline check library (`zentest/_checks.py`, a copy of the platform's) follows the platform: it gains the
63
+ special-category data check, and the platform-only RAG, language and attack checks are refused offline as a
64
+ configuration error instead of failing to import.
65
+
66
+ ## 0.2.0 (2026-10-15)
67
+
68
+ Completes contract v1 (SDK-01 to SDK-20). Installed from the repository (`pip install -e sdk`).
69
+
70
+ ### Added
71
+ - Tracing (SDK-15): `@zt.traced` and `with zt.span(...)` record zentest.span.v1 spans, one trace per trial, uploaded and
72
+ linked to the trial's result. A run whose agent records no spans warns when it starts (`gates.last_run.traced`).
73
+ - OpenTelemetry (SDK-16): `zentest.otel` (extra `otel`) exports existing GenAI or OpenInference spans and stamps them
74
+ with the trial's ids.
75
+ - Offline runs (SDK-08): `zentest export` and `zentest run --offline` score deterministic metrics with the platform's
76
+ own check library and decision rule; skipped judge metrics make the result `incomplete` (exit 2) unless
77
+ `--allow-incomplete`.
78
+ - The zentest CLI (SDK-08, SDK-09): `zentest run gate.zt` gates a CI pipeline on a platform verdict (exit 0 passed,
79
+ 1 not passed, 2 no verdict), with `--json`, `--json-file`, `--summary`, `zentest cancel`, the REQ-077 override chain
80
+ and OSA scope conditions (UK-10); a replayed idempotency key for another gate exits 2.
81
+ - Minimum-version check (SDK-19): one `GET /api/version` before the first call; `VersionError` with the upgrade command.
82
+ - Typed errors for every refusal: `ResidencyError`, `IdempotencyConflictError`, `MissingInputsError` (`.missing`),
83
+ `VersionError`, alongside the earlier ones.
84
+ - Per-trial `usage` and `metadata` (SDK-04), cut down rather than refused when too large; `packs` recorded on the run
85
+ and returned as `Verdict.packs` (SDK-10); `cancel` returns a `CancelResult` (SDK-18).
86
+ - Version attribution (AI-49): `AgentResponse(model=..., prompt_version=..., config_version=...)` and
87
+ `Verdict.attribution`.
88
+ - Client methods for the catalogue and case authoring (`products`, `versions`, `suites`, `suite_cases`, `add_cases`,
89
+ `update_case`), `results`, `power`, `override`, `span_schema`, `upload_spans`, `instrumentation`, `export_suite`,
90
+ `submit_corpus` and `submit_session`.
91
+ - Extras `otel`, `yaml` and `schema`; the core dependency stays `httpx` alone (SDK-20).
92
+ - Examples: `quickstart.py` and `mock_app.py` report attribution; `http_app_gate.py` wraps a login-protected chat API.
93
+
94
+ ### Changed
95
+ - `zt.wait()` accepts a `RunHandle` as well as a run id; `zt.cancel(run)` and `RunCancelledError` (SDK-17, SDK-18).
96
+
97
+ ## 0.1.0 (2026-10-13)
98
+
99
+ First version, installed from the repository.
100
+
101
+ ### Added
102
+ - `Zentest` client: API token and pinned residency region on every call, retries on 5xx and connection errors.
103
+ - Adapter protocol (SDK-02 to SDK-05): the agent's shape is detected from its type hint (`str`, `list[dict]` or
104
+ `AgentInput`), sync or async.
105
+ - The in-process SDK runner with batched, idempotent result submission, and the manual loop (`start`, `cases`,
106
+ `submit`, `finish`).
107
+ - `gates.run` with required `repeats` and the stated `deterministic` mode (SDK-10 to SDK-13), the typed `Verdict` and
108
+ the contract errors.
109
+ - Examples: `mock_app.py` and `quickstart.py` (both runners).
@@ -0,0 +1,6 @@
1
+ # The sdist is public (PyPI): the package (from pyproject.toml), the README (the PyPI page), the changelog and the
2
+ # release notes. The tests and examples stay in the repository: they name non-public apps and deployments, and the
3
+ # README links the examples on GitHub.
4
+ include CHANGELOG.md RELEASING.md
5
+ prune tests
6
+ prune examples
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: zentest-sdk
3
+ Version: 0.4.0
4
+ Summary: Zentest SDK: test your AI app in-process and gate releases on a judged verdict (SDK contract v1)
5
+ Author: Nxzen
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://github.com/ITHyd/Zentest
8
+ Project-URL: Repository, https://github.com/ITHyd/Zentest
9
+ Project-URL: Documentation, https://github.com/ITHyd/Zentest/blob/main/docs/specs/sdk-contract.md
10
+ Project-URL: Changelog, https://github.com/ITHyd/Zentest/blob/main/sdk/CHANGELOG.md
11
+ Project-URL: Issues, https://github.com/ITHyd/Zentest/issues
12
+ Keywords: llm,evaluation,testing,ai,release-gate,rag,opentelemetry
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: httpx>=0.27
27
+ Provides-Extra: otel
28
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
29
+ Provides-Extra: yaml
30
+ Requires-Dist: pyyaml>=6; extra == "yaml"
31
+ Provides-Extra: schema
32
+ Requires-Dist: jsonschema>=4; extra == "schema"
33
+ Provides-Extra: judge
34
+ Requires-Dist: anthropic>=0.100; extra == "judge"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=8; extra == "dev"
37
+ Requires-Dist: pyyaml>=6; extra == "dev"
38
+ Requires-Dist: opentelemetry-sdk>=1.20; extra == "dev"
39
+ Requires-Dist: jsonschema>=4; extra == "dev"
40
+ Requires-Dist: build>=1.2; extra == "dev"
41
+ Requires-Dist: twine>=6.1; extra == "dev"
42
+ Requires-Dist: packaging>=23; extra == "dev"
43
+
44
+ # zentest-sdk
45
+
46
+ The Python SDK for [Zentest](https://github.com/ITHyd/Zentest), Nxzen's platform for testing AI apps (chatbots, RAG,
47
+ voice agents). Run your app's test suite in your own process, or let the platform call its HTTP endpoint, and gate the
48
+ release on a judged verdict with a confidence interval. The behaviour is fixed by the
49
+ [SDK contract v1](https://github.com/ITHyd/Zentest/blob/main/docs/specs/sdk-contract.md) (SDK-01 to SDK-20).
50
+ A TypeScript SDK for Node 18+ implements the same contract in [`sdk-ts/`](https://github.com/ITHyd/Zentest/tree/main/sdk-ts)
51
+ (contract §2b); offline runs and the CLI are Python only.
52
+ You need a Zentest workspace and an API token (console: Settings → API tokens).
53
+
54
+ **Links:** [contract](https://github.com/ITHyd/Zentest/blob/main/docs/specs/sdk-contract.md) ·
55
+ [changelog](https://github.com/ITHyd/Zentest/blob/main/sdk/CHANGELOG.md) ·
56
+ [CI gate guide](https://github.com/ITHyd/Zentest/blob/main/docs/specs/ci-gate.md) ·
57
+ [span format](https://github.com/ITHyd/Zentest/blob/main/docs/specs/zentest-span-v1.md) ·
58
+ [examples](https://github.com/ITHyd/Zentest/tree/main/sdk/examples)
59
+
60
+ ## Install
61
+
62
+ Python 3.11 or later.
63
+
64
+ ```bash
65
+ pip install zentest-sdk # core: httpx only
66
+ pip install "zentest-sdk[otel]" # + OpenTelemetry exporter (zentest.otel)
67
+ pip install "zentest-sdk[yaml]" # + YAML suite files for the CLI
68
+ pip install "zentest-sdk[schema]" # + offline runs of a format metric with a JSON Schema
69
+ pip install "zentest-sdk[judge]" # + the local Claude judge (open-core mode, no platform)
70
+ ```
71
+
72
+ Install it into a virtual environment, then check it: `zentest --version` prints the installed version. Add only
73
+ the extras you need.
74
+
75
+ Working on the SDK itself? Install it from a checkout of the repository instead: `pip install -e sdk` (extras as
76
+ `pip install -e "sdk[otel,yaml]"`, and `sdk[dev]` for the tests).
77
+
78
+ **Dependencies (SDK-20).** The client is installed into your application's environment, so the floor is thin: `httpx`
79
+ and nothing else. Evaluation libraries stay on the platform; the deterministic checks used by offline runs are standard
80
+ library only. Extras: `otel` (`opentelemetry-sdk`), `yaml` (`pyyaml`), `schema` (`jsonschema`). The package ships type
81
+ hints (`py.typed`).
82
+
83
+ ## Evaluate in your own tests (0.4.0)
84
+
85
+ ```python
86
+ from zentest import LLMTestCase, assert_test
87
+ from zentest.metrics import PIILeak, Faithfulness, AnswerRelevancy, GEval
88
+
89
+ def test_burst_pipe():
90
+ case = LLMTestCase(input="Is a burst pipe covered?", actual_output=app(q), retrieval_context=chunks)
91
+ assert_test(case, [PIILeak(), Faithfulness(threshold=0.8), AnswerRelevancy(),
92
+ GEval("Plain English", "Short answer a customer understands")])
93
+ ```
94
+
95
+ - `pytest` prints a Zentest summary table; `--zentest-sync --zentest-product P --zentest-version V` records the session
96
+ as a platform run. Deterministic metrics always run before judge metrics.
97
+ - Judges: `ZENTEST_JUDGE=platform` (the platform's calibrated judge, needs `ZENTEST_API_KEY`), `local` (Claude with
98
+ `ANTHROPIC_API_KEY`, or OpenRouter with `OPENROUTER_API_KEY`) or `heuristic` (no model call). A local judge is not
99
+ calibrated: gate releases on the platform.
100
+ - `zentest init` then `zentest test zentest.yaml`: the same checks as a YAML file in Git (`--baseline`, `--junit`,
101
+ `--sync`).
102
+ - `zentest redteam --product P --version V --agent app:answer`: the product's red-team suite against your agent.
103
+ - `zentest.guard.Guard(...).protect(answer)`: block prompt injection and personal data in production.
104
+ - `zentest.simulate(answer, persona=..., goal=...)`: a seeded multi-turn conversation, scored per turn.
105
+
106
+ A worked example on a real app lives in the repository under `sdk/examples` (it runs with no network in mock mode).
107
+
108
+ ## Quick start (release gate)
109
+
110
+ ```python
111
+ from zentest import Zentest, AgentInput, AgentResponse
112
+
113
+ def agent(x: AgentInput) -> AgentResponse: # or (user_message: str) -> str, or (messages: list[dict]) -> str
114
+ reply, chunks = my_app.answer(x.messages[-1]["content"])
115
+ return AgentResponse(output=reply, retrieval_context=chunks,
116
+ model="claude-sonnet-4.5", prompt_version="r12", config_version="v6")
117
+
118
+ with Zentest(api_key="zt_live_…", base_url="http://localhost:8620", region="uk-south") as zt:
119
+ v = zt.gates.run("Claims Copilot", "v6", "Claims FAQ Golden Set", agent, repeats=3, deterministic=True)
120
+ print(v.passed, v.reason, v.interval)
121
+ ```
122
+
123
+ **Arguments**
124
+ - `repeats`: required, with no default. A single run gives no interval.
125
+ - `deterministic=True`: you are stating that the app runs with temperature 0 and a fixed seed. Without it the platform
126
+ refuses (`PrerequisiteError`).
127
+ - `agent=None`: the platform calls the product's HTTP connection instead of your code (the endpoint runner, below).
128
+ - `batch_size=50`, `flush_every_s=None`: results go to the platform every `batch_size` trials, or sooner once
129
+ `flush_every_s` seconds pass without a submission, so a slow app's run shows progress and time left in the console
130
+ as it goes. `None` takes the client's (`Zentest(flush_every_s=...)`, else `ZENTEST_FLUSH_EVERY_S`, else 30 s); `0`
131
+ sends full batches only.
132
+ - `packs=[...]`: compliance pack ids from the workspace's catalogue (`eu-ai-act`, `owasp-llm`, …; any other id is a
133
+ 422), recorded on the run and returned as `v.packs`.
134
+ - `connection_id="…"` (with `agent=None`): the HTTP connection the platform calls when the environment has several;
135
+ without it, the environment's default connection, else its only one.
136
+ - `corpus=[...]`, `corpus_data_source="synthetic" | "masked_production" | "production"`: your app's retrieval corpus
137
+ (chunks as strings or `{"text", "id"}`), scanned when the run starts. A test case found in it is skipped and reported on
138
+ `v.contamination`; too many and the run is refused (UK-31).
139
+ - `osa_scope={...}`: the three Online Safety Act scope conditions for this release; a change comes back as
140
+ `zt.gates.last_run.scope_alert` (UK-10). `trigger="ci"` marks a run started from a pipeline.
141
+ - `timeout=600`: seconds to wait for the verdict before `ZentestTimeout`.
142
+
143
+ **Version attribution ([contract §2a](https://github.com/ITHyd/Zentest/blob/main/docs/specs/sdk-contract.md)):** return `AgentResponse(output=…, model=…, prompt_version=…, config_version=…)`
144
+ from a shape C agent so every trial says what produced it. The values must equal what is registered on the version
145
+ under test. Attribution is required by default: on a product created from P2a on, a trial that reports nothing does not
146
+ count, and a run where nothing is reported does not pass (the verdict's reason says what to report). Products that
147
+ predate it keep counting such trials, as *declared*, until the owner turns attribution required on;
148
+ `v.attribution["coverage"]` shows the share the app reported. `usage` and `metadata` on the response are stored with
149
+ each trial too (credential-named keys such as `api_key`, `accessToken` or `clientSecret` as `[REDACTED]`, personal data
150
+ redacted like any output). They are optional and never fail a trial: nested usage (OpenAI's `prompt_tokens_details`)
151
+ is sent flat as `prompt_tokens_details.cached_tokens`, at most 32 values, and metadata over 16 KB as JSON is trimmed
152
+ key by key; each cut is a warning on `zt.gates.last_run.warnings`.
153
+
154
+ **Tracing (SDK-15):** `@zt.traced` / `with zt.span(name, kind=...)` record steps (tool calls, retrievals, LLM calls,
155
+ planning) as zentest.span.v1 spans. The SDK runner uploads one trace per trial and links it to the trial's result
156
+ (`zt.results(run)` gives `trace_id` and `session_id`; a trial whose spans the platform did not store has no
157
+ `trace_id`, and the run handle says which). Without spans the run still works, but the SDK warns when the run starts
158
+ that trajectory and tool-choice metrics are unavailable, and `zt.gates.last_run.traced` is `False`.
159
+
160
+ ```python
161
+ @zt.traced(kind="tool", name="lookup_claim")
162
+ def lookup(claim_id: str) -> dict: ...
163
+
164
+ with zt.span("answer", kind="llm", region="uk-south") as s:
165
+ s.set_usage(input_tokens=120, output_tokens=30, provider="anthropic")
166
+ s.set_output(text)
167
+ ```
168
+
169
+ **Already on OpenTelemetry (SDK-16):** keep your instrumentation (GenAI `gen_ai.*` or OpenInference) and add
170
+ `zentest.otel.install(zt, product="Claims Copilot", version="v6")`. Spans started inside a trial are stamped with the
171
+ trial's ids. A collector can instead export to `POST /v1/traces` with the API token and `X-Zentest-Product` (or the
172
+ resource attribute `zentest.product`), as **OTLP/JSON only**: set `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json`
173
+ (protobuf, the exporters' default, gets 415). `/v1/traces` is outside `/api`, so a proxy or ingress needs a route for
174
+ it; see [zentest-span-v1 §4a](https://github.com/ITHyd/Zentest/blob/main/docs/specs/zentest-span-v1.md).
175
+
176
+ **Endpoint runner (no code on your side):** the platform calls the product's HTTP connection (Connections in the
177
+ console, per environment). For the mock app:
178
+
179
+ | Field | Value | Why |
180
+ |---|---|---|
181
+ | endpoint | `http://127.0.0.1:8100/chat` | plain http only to localhost |
182
+ | secret_ref | `env://MOCK_APP_KEY` (dev) or `kv://<vault>/<name>` | the platform stores a reference, never the key |
183
+ | request_template | `{"message": "{{input}}", "session": "zt"}` | |
184
+ | output_path | `$.reply.text` | the answer |
185
+ | context_path | `$.sources[*].text` | **required** when the suite has a context-grounded judge metric (Faithfulness): without it the run is refused before it starts (NEW-43, `MissingInputsError`) |
186
+ | model_path, prompt_version_path, config_version_path | `$.meta.model`, `$.meta.prompt_version`, `$.meta.config_version` | attribution (§2a); a new product does not count unattributed trials |
187
+
188
+ **Manual loop:** `run = zt.start(...)`, then `zt.submit(run, case, output)` for each case in `zt.cases(run)`, then
189
+ `zt.finish(run)`. `zt.cancel(run)` stops a run and returns what completed (`trials_received`, kept).
190
+
191
+ **Catalogue and authoring:** `zt.products()`, `zt.versions(product)`, `zt.suites(product)`, `zt.suite_cases(suite_id)`,
192
+ `zt.add_cases(suite_id, [...])`, `zt.update_case(case_id, input=…)`, `zt.results(run)`, `zt.span_schema()`,
193
+ `zt.upload_spans(product, spans, otlp=…)`, `zt.instrumentation(product)`, `zt.export_suite(product, version, suite)`,
194
+ `zt.submit_corpus(run, chunks, data_source=…)` (UK-31 corpus scan before gating) and
195
+ `zt.submit_session(product, session_id, input, transcript=…)` (REQ-066 production sessions, a `sessions:write` token).
196
+
197
+ **Errors:** `PrerequisiteError`, `CalibrationError`, `ResidencyError`, `IdempotencyConflictError`,
198
+ `MissingInputsError` (`.missing` lists each gap), `VersionError`, `RunFailedError`, `RunCancelledError`, `AuthError`,
199
+ `NotFoundError`, `ZentestTimeout`; all are `ZentestError`.
200
+
201
+ **Minimum version (SDK-19):** the client reads `GET /api/version` once before its first call and raises `VersionError`
202
+ with the upgrade command when it is older than the platform's `min_sdk_version`.
203
+
204
+ **CLI** (`zentest`, [CI gate guide](https://github.com/ITHyd/Zentest/blob/main/docs/specs/ci-gate.md)): `zentest run gate.zt` (a platform gate in CI), `zentest cancel RUN_ID`, and
205
+ offline (SDK-08):
206
+
207
+ ```bash
208
+ zentest export --product "Claims Copilot" --version v6 --suite "Claims FAQ Golden Set" -o claims.ztx
209
+ zentest run --offline claims.ztx --agent my_app.gate:agent --repeats 3 # exit 0 passed, 1 not passed, 2 error
210
+ # or incomplete
211
+ ```
212
+
213
+ The export holds cases, metric configs and rubrics, never a connection or secret. It does hold every case input, and
214
+ once written it is outside the platform's retention and erasure: keep it where the suite's data may live, delete it
215
+ after the job, and export again after an erasure. Offline, deterministic metrics are scored with the platform's own
216
+ check library and the platform's decision rule. **Offline mode cannot** score judge metrics (they need the platform's
217
+ judge gateway: calibration, family guard, residency), apply the rubric guards, span-level checks, the power check or
218
+ the override chain, or produce an evidence pack; nothing is uploaded.
219
+
220
+ **Incomplete runs exit 2 by default.** When judge metrics were skipped the result is `status: "incomplete"` (exit 2):
221
+ a deterministic-only pass is not a release verdict. A definite not-pass still exits 1 (blocked by a gating
222
+ deterministic metric, or not passed with every trial scored). `--allow-incomplete` accepts the deterministic-only
223
+ result (exit 0/1). A trial with no metric scorable offline is not passed, and a suite with no deterministic metric has
224
+ no offline verdict (`passed: null`, exit 2 even with `--allow-incomplete`). `--strict` is the default and still
225
+ accepted.
226
+
227
+ **Examples**
228
+ - [`examples/mock_app.py`](https://github.com/ITHyd/Zentest/blob/main/sdk/examples/mock_app.py): a local stand-in app that reports attribution (`meta`).
229
+ - [`examples/quickstart.py`](https://github.com/ITHyd/Zentest/blob/main/sdk/examples/quickstart.py): runs it through both runners, with tracing.
230
+ - [`examples/http_app_gate.py`](https://github.com/ITHyd/Zentest/blob/main/sdk/examples/http_app_gate.py): wraps any login-protected chat API as an SDK agent from environment variables (logs in,
231
+ caches and refreshes the JWT, threads conversation ids, reads answer and context with JSONPaths, reports attribution).
232
+ `python sdk/examples/http_app_gate.py --smoke "question"` checks the configuration with one call.
233
+
234
+ **Tests** (from a checkout of the repository): `cd sdk && python3 -m pytest -q tests` (no network:
235
+ httpx.MockTransport; the span and OTel tests also run the platform's own validator from `backend/` when it is
236
+ importable). `pip install -e "sdk[dev]"` adds `build` and `twine`, which the packaging tests use to build and check the
237
+ release artifacts.
238
+
239
+ ## Versions and licence
240
+
241
+ `zentest.__version__` is the installed version; [CHANGELOG.md](https://github.com/ITHyd/Zentest/blob/main/sdk/CHANGELOG.md)
242
+ lists what each release changed. Releases are additive within contract v1, and the platform tells an SDK that is too
243
+ old to upgrade (`VersionError`, above). Copyright Nxzen; proprietary licence (see the package metadata).
@@ -0,0 +1,200 @@
1
+ # zentest-sdk
2
+
3
+ The Python SDK for [Zentest](https://github.com/ITHyd/Zentest), Nxzen's platform for testing AI apps (chatbots, RAG,
4
+ voice agents). Run your app's test suite in your own process, or let the platform call its HTTP endpoint, and gate the
5
+ release on a judged verdict with a confidence interval. The behaviour is fixed by the
6
+ [SDK contract v1](https://github.com/ITHyd/Zentest/blob/main/docs/specs/sdk-contract.md) (SDK-01 to SDK-20).
7
+ A TypeScript SDK for Node 18+ implements the same contract in [`sdk-ts/`](https://github.com/ITHyd/Zentest/tree/main/sdk-ts)
8
+ (contract §2b); offline runs and the CLI are Python only.
9
+ You need a Zentest workspace and an API token (console: Settings → API tokens).
10
+
11
+ **Links:** [contract](https://github.com/ITHyd/Zentest/blob/main/docs/specs/sdk-contract.md) ·
12
+ [changelog](https://github.com/ITHyd/Zentest/blob/main/sdk/CHANGELOG.md) ·
13
+ [CI gate guide](https://github.com/ITHyd/Zentest/blob/main/docs/specs/ci-gate.md) ·
14
+ [span format](https://github.com/ITHyd/Zentest/blob/main/docs/specs/zentest-span-v1.md) ·
15
+ [examples](https://github.com/ITHyd/Zentest/tree/main/sdk/examples)
16
+
17
+ ## Install
18
+
19
+ Python 3.11 or later.
20
+
21
+ ```bash
22
+ pip install zentest-sdk # core: httpx only
23
+ pip install "zentest-sdk[otel]" # + OpenTelemetry exporter (zentest.otel)
24
+ pip install "zentest-sdk[yaml]" # + YAML suite files for the CLI
25
+ pip install "zentest-sdk[schema]" # + offline runs of a format metric with a JSON Schema
26
+ pip install "zentest-sdk[judge]" # + the local Claude judge (open-core mode, no platform)
27
+ ```
28
+
29
+ Install it into a virtual environment, then check it: `zentest --version` prints the installed version. Add only
30
+ the extras you need.
31
+
32
+ Working on the SDK itself? Install it from a checkout of the repository instead: `pip install -e sdk` (extras as
33
+ `pip install -e "sdk[otel,yaml]"`, and `sdk[dev]` for the tests).
34
+
35
+ **Dependencies (SDK-20).** The client is installed into your application's environment, so the floor is thin: `httpx`
36
+ and nothing else. Evaluation libraries stay on the platform; the deterministic checks used by offline runs are standard
37
+ library only. Extras: `otel` (`opentelemetry-sdk`), `yaml` (`pyyaml`), `schema` (`jsonschema`). The package ships type
38
+ hints (`py.typed`).
39
+
40
+ ## Evaluate in your own tests (0.4.0)
41
+
42
+ ```python
43
+ from zentest import LLMTestCase, assert_test
44
+ from zentest.metrics import PIILeak, Faithfulness, AnswerRelevancy, GEval
45
+
46
+ def test_burst_pipe():
47
+ case = LLMTestCase(input="Is a burst pipe covered?", actual_output=app(q), retrieval_context=chunks)
48
+ assert_test(case, [PIILeak(), Faithfulness(threshold=0.8), AnswerRelevancy(),
49
+ GEval("Plain English", "Short answer a customer understands")])
50
+ ```
51
+
52
+ - `pytest` prints a Zentest summary table; `--zentest-sync --zentest-product P --zentest-version V` records the session
53
+ as a platform run. Deterministic metrics always run before judge metrics.
54
+ - Judges: `ZENTEST_JUDGE=platform` (the platform's calibrated judge, needs `ZENTEST_API_KEY`), `local` (Claude with
55
+ `ANTHROPIC_API_KEY`, or OpenRouter with `OPENROUTER_API_KEY`) or `heuristic` (no model call). A local judge is not
56
+ calibrated: gate releases on the platform.
57
+ - `zentest init` then `zentest test zentest.yaml`: the same checks as a YAML file in Git (`--baseline`, `--junit`,
58
+ `--sync`).
59
+ - `zentest redteam --product P --version V --agent app:answer`: the product's red-team suite against your agent.
60
+ - `zentest.guard.Guard(...).protect(answer)`: block prompt injection and personal data in production.
61
+ - `zentest.simulate(answer, persona=..., goal=...)`: a seeded multi-turn conversation, scored per turn.
62
+
63
+ A worked example on a real app lives in the repository under `sdk/examples` (it runs with no network in mock mode).
64
+
65
+ ## Quick start (release gate)
66
+
67
+ ```python
68
+ from zentest import Zentest, AgentInput, AgentResponse
69
+
70
+ def agent(x: AgentInput) -> AgentResponse: # or (user_message: str) -> str, or (messages: list[dict]) -> str
71
+ reply, chunks = my_app.answer(x.messages[-1]["content"])
72
+ return AgentResponse(output=reply, retrieval_context=chunks,
73
+ model="claude-sonnet-4.5", prompt_version="r12", config_version="v6")
74
+
75
+ with Zentest(api_key="zt_live_…", base_url="http://localhost:8620", region="uk-south") as zt:
76
+ v = zt.gates.run("Claims Copilot", "v6", "Claims FAQ Golden Set", agent, repeats=3, deterministic=True)
77
+ print(v.passed, v.reason, v.interval)
78
+ ```
79
+
80
+ **Arguments**
81
+ - `repeats`: required, with no default. A single run gives no interval.
82
+ - `deterministic=True`: you are stating that the app runs with temperature 0 and a fixed seed. Without it the platform
83
+ refuses (`PrerequisiteError`).
84
+ - `agent=None`: the platform calls the product's HTTP connection instead of your code (the endpoint runner, below).
85
+ - `batch_size=50`, `flush_every_s=None`: results go to the platform every `batch_size` trials, or sooner once
86
+ `flush_every_s` seconds pass without a submission, so a slow app's run shows progress and time left in the console
87
+ as it goes. `None` takes the client's (`Zentest(flush_every_s=...)`, else `ZENTEST_FLUSH_EVERY_S`, else 30 s); `0`
88
+ sends full batches only.
89
+ - `packs=[...]`: compliance pack ids from the workspace's catalogue (`eu-ai-act`, `owasp-llm`, …; any other id is a
90
+ 422), recorded on the run and returned as `v.packs`.
91
+ - `connection_id="…"` (with `agent=None`): the HTTP connection the platform calls when the environment has several;
92
+ without it, the environment's default connection, else its only one.
93
+ - `corpus=[...]`, `corpus_data_source="synthetic" | "masked_production" | "production"`: your app's retrieval corpus
94
+ (chunks as strings or `{"text", "id"}`), scanned when the run starts. A test case found in it is skipped and reported on
95
+ `v.contamination`; too many and the run is refused (UK-31).
96
+ - `osa_scope={...}`: the three Online Safety Act scope conditions for this release; a change comes back as
97
+ `zt.gates.last_run.scope_alert` (UK-10). `trigger="ci"` marks a run started from a pipeline.
98
+ - `timeout=600`: seconds to wait for the verdict before `ZentestTimeout`.
99
+
100
+ **Version attribution ([contract §2a](https://github.com/ITHyd/Zentest/blob/main/docs/specs/sdk-contract.md)):** return `AgentResponse(output=…, model=…, prompt_version=…, config_version=…)`
101
+ from a shape C agent so every trial says what produced it. The values must equal what is registered on the version
102
+ under test. Attribution is required by default: on a product created from P2a on, a trial that reports nothing does not
103
+ count, and a run where nothing is reported does not pass (the verdict's reason says what to report). Products that
104
+ predate it keep counting such trials, as *declared*, until the owner turns attribution required on;
105
+ `v.attribution["coverage"]` shows the share the app reported. `usage` and `metadata` on the response are stored with
106
+ each trial too (credential-named keys such as `api_key`, `accessToken` or `clientSecret` as `[REDACTED]`, personal data
107
+ redacted like any output). They are optional and never fail a trial: nested usage (OpenAI's `prompt_tokens_details`)
108
+ is sent flat as `prompt_tokens_details.cached_tokens`, at most 32 values, and metadata over 16 KB as JSON is trimmed
109
+ key by key; each cut is a warning on `zt.gates.last_run.warnings`.
110
+
111
+ **Tracing (SDK-15):** `@zt.traced` / `with zt.span(name, kind=...)` record steps (tool calls, retrievals, LLM calls,
112
+ planning) as zentest.span.v1 spans. The SDK runner uploads one trace per trial and links it to the trial's result
113
+ (`zt.results(run)` gives `trace_id` and `session_id`; a trial whose spans the platform did not store has no
114
+ `trace_id`, and the run handle says which). Without spans the run still works, but the SDK warns when the run starts
115
+ that trajectory and tool-choice metrics are unavailable, and `zt.gates.last_run.traced` is `False`.
116
+
117
+ ```python
118
+ @zt.traced(kind="tool", name="lookup_claim")
119
+ def lookup(claim_id: str) -> dict: ...
120
+
121
+ with zt.span("answer", kind="llm", region="uk-south") as s:
122
+ s.set_usage(input_tokens=120, output_tokens=30, provider="anthropic")
123
+ s.set_output(text)
124
+ ```
125
+
126
+ **Already on OpenTelemetry (SDK-16):** keep your instrumentation (GenAI `gen_ai.*` or OpenInference) and add
127
+ `zentest.otel.install(zt, product="Claims Copilot", version="v6")`. Spans started inside a trial are stamped with the
128
+ trial's ids. A collector can instead export to `POST /v1/traces` with the API token and `X-Zentest-Product` (or the
129
+ resource attribute `zentest.product`), as **OTLP/JSON only**: set `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json`
130
+ (protobuf, the exporters' default, gets 415). `/v1/traces` is outside `/api`, so a proxy or ingress needs a route for
131
+ it; see [zentest-span-v1 §4a](https://github.com/ITHyd/Zentest/blob/main/docs/specs/zentest-span-v1.md).
132
+
133
+ **Endpoint runner (no code on your side):** the platform calls the product's HTTP connection (Connections in the
134
+ console, per environment). For the mock app:
135
+
136
+ | Field | Value | Why |
137
+ |---|---|---|
138
+ | endpoint | `http://127.0.0.1:8100/chat` | plain http only to localhost |
139
+ | secret_ref | `env://MOCK_APP_KEY` (dev) or `kv://<vault>/<name>` | the platform stores a reference, never the key |
140
+ | request_template | `{"message": "{{input}}", "session": "zt"}` | |
141
+ | output_path | `$.reply.text` | the answer |
142
+ | context_path | `$.sources[*].text` | **required** when the suite has a context-grounded judge metric (Faithfulness): without it the run is refused before it starts (NEW-43, `MissingInputsError`) |
143
+ | model_path, prompt_version_path, config_version_path | `$.meta.model`, `$.meta.prompt_version`, `$.meta.config_version` | attribution (§2a); a new product does not count unattributed trials |
144
+
145
+ **Manual loop:** `run = zt.start(...)`, then `zt.submit(run, case, output)` for each case in `zt.cases(run)`, then
146
+ `zt.finish(run)`. `zt.cancel(run)` stops a run and returns what completed (`trials_received`, kept).
147
+
148
+ **Catalogue and authoring:** `zt.products()`, `zt.versions(product)`, `zt.suites(product)`, `zt.suite_cases(suite_id)`,
149
+ `zt.add_cases(suite_id, [...])`, `zt.update_case(case_id, input=…)`, `zt.results(run)`, `zt.span_schema()`,
150
+ `zt.upload_spans(product, spans, otlp=…)`, `zt.instrumentation(product)`, `zt.export_suite(product, version, suite)`,
151
+ `zt.submit_corpus(run, chunks, data_source=…)` (UK-31 corpus scan before gating) and
152
+ `zt.submit_session(product, session_id, input, transcript=…)` (REQ-066 production sessions, a `sessions:write` token).
153
+
154
+ **Errors:** `PrerequisiteError`, `CalibrationError`, `ResidencyError`, `IdempotencyConflictError`,
155
+ `MissingInputsError` (`.missing` lists each gap), `VersionError`, `RunFailedError`, `RunCancelledError`, `AuthError`,
156
+ `NotFoundError`, `ZentestTimeout`; all are `ZentestError`.
157
+
158
+ **Minimum version (SDK-19):** the client reads `GET /api/version` once before its first call and raises `VersionError`
159
+ with the upgrade command when it is older than the platform's `min_sdk_version`.
160
+
161
+ **CLI** (`zentest`, [CI gate guide](https://github.com/ITHyd/Zentest/blob/main/docs/specs/ci-gate.md)): `zentest run gate.zt` (a platform gate in CI), `zentest cancel RUN_ID`, and
162
+ offline (SDK-08):
163
+
164
+ ```bash
165
+ zentest export --product "Claims Copilot" --version v6 --suite "Claims FAQ Golden Set" -o claims.ztx
166
+ zentest run --offline claims.ztx --agent my_app.gate:agent --repeats 3 # exit 0 passed, 1 not passed, 2 error
167
+ # or incomplete
168
+ ```
169
+
170
+ The export holds cases, metric configs and rubrics, never a connection or secret. It does hold every case input, and
171
+ once written it is outside the platform's retention and erasure: keep it where the suite's data may live, delete it
172
+ after the job, and export again after an erasure. Offline, deterministic metrics are scored with the platform's own
173
+ check library and the platform's decision rule. **Offline mode cannot** score judge metrics (they need the platform's
174
+ judge gateway: calibration, family guard, residency), apply the rubric guards, span-level checks, the power check or
175
+ the override chain, or produce an evidence pack; nothing is uploaded.
176
+
177
+ **Incomplete runs exit 2 by default.** When judge metrics were skipped the result is `status: "incomplete"` (exit 2):
178
+ a deterministic-only pass is not a release verdict. A definite not-pass still exits 1 (blocked by a gating
179
+ deterministic metric, or not passed with every trial scored). `--allow-incomplete` accepts the deterministic-only
180
+ result (exit 0/1). A trial with no metric scorable offline is not passed, and a suite with no deterministic metric has
181
+ no offline verdict (`passed: null`, exit 2 even with `--allow-incomplete`). `--strict` is the default and still
182
+ accepted.
183
+
184
+ **Examples**
185
+ - [`examples/mock_app.py`](https://github.com/ITHyd/Zentest/blob/main/sdk/examples/mock_app.py): a local stand-in app that reports attribution (`meta`).
186
+ - [`examples/quickstart.py`](https://github.com/ITHyd/Zentest/blob/main/sdk/examples/quickstart.py): runs it through both runners, with tracing.
187
+ - [`examples/http_app_gate.py`](https://github.com/ITHyd/Zentest/blob/main/sdk/examples/http_app_gate.py): wraps any login-protected chat API as an SDK agent from environment variables (logs in,
188
+ caches and refreshes the JWT, threads conversation ids, reads answer and context with JSONPaths, reports attribution).
189
+ `python sdk/examples/http_app_gate.py --smoke "question"` checks the configuration with one call.
190
+
191
+ **Tests** (from a checkout of the repository): `cd sdk && python3 -m pytest -q tests` (no network:
192
+ httpx.MockTransport; the span and OTel tests also run the platform's own validator from `backend/` when it is
193
+ importable). `pip install -e "sdk[dev]"` adds `build` and `twine`, which the packaging tests use to build and check the
194
+ release artifacts.
195
+
196
+ ## Versions and licence
197
+
198
+ `zentest.__version__` is the installed version; [CHANGELOG.md](https://github.com/ITHyd/Zentest/blob/main/sdk/CHANGELOG.md)
199
+ lists what each release changed. Releases are additive within contract v1, and the platform tells an SDK that is too
200
+ old to upgrade (`VersionError`, above). Copyright Nxzen; proprietary licence (see the package metadata).