A/B Tests
A/B tests between registry models, their predictions and experiments.
Access it as client.ab_tests on a Strongly client, or the same path on AsyncStrongly with await. All methods exist on both with identical signatures.
Quick start
from strongly import Strongly
client = Strongly()
# List (auto-paginates as you iterate) # filters: status, strategy, model_id, workspace_id, search, sort
for a/b in client.ab_tests.list():
print(a/b.id)
Methods
Core
list
list(*, status: str | None = None, strategy: str | None = None, model_id: str | None = None, workspace_id: str | None = None, search: str | None = None, sort: str | None = None, limit: int | None = None) -> SyncPaginator[ABTest]
Return the A/B tests you can see (newest update first).
Parameters
status(str | None, optional):registered,deploying,running,stoppedorfailed.strategy(str | None, optional):weighted_random,feature_based,multi_armed_banditorcanary.model_id(str | None, optional): Only tests with a variant serving this registry model.workspace_id(str | None, optional): Only tests in this workspace.search(str | None, optional): Matches name and description.sort(str | None, optional):-updatedAt(default),createdAt,name,strategy,-metrics.totalRequests.limit(int | None, optional): Maximum number of tests (default: all).
create
create(*, name: str, strategy: str, variants: Sequence[Mapping[str, Any]], description: str | None = None, tags: Sequence[str] | None = None, workspace_id: str | None = None, sticky_routing: bool | None = None, feature_rules: Sequence[Mapping[str, Any]] | None = None, bandit_config: Mapping[str, Any] | None = None, canary_config: Mapping[str, Any] | None = None) -> ABTest
Create an A/B test (status registered; deploy starts serving it).
Parameters
name(str): Test name.strategy(str):weighted_random(each variant gets its weight's share),feature_based(the first matching rule by priority picks the variant, else the control),multi_armed_bandit(routes by the rewards recorded withprediction_feedback) orcanary(the target variant gets the canary's current percentage of traffic; once deployed, the platform advances it through its stages while it stays as healthy as the control, or rolls it back to 0% with the reason, shown incanary_config.last_check).variants(Sequence[Mapping[str, Any]]): At least two{"variantId", "modelId", "weight"?, "isControl"?}` dicts, each serving a registry model. `weightis a 0-1 fraction (default an equal share); forweighted_randomthe weights must sum to 1. The first variant is the control unless one is marked.description, tags, workspace_id: Metadata.sticky_routing(bool | None, optional): Keep an entity on one variant for the test's life, keyed on theentity_idsent with eachpredict(a prediction without one is then refused). A canary moves an entity to the target only as its share grows, and back on a rollback.feature_rules(Sequence[Mapping[str, Any]] | None, optional): Forfeature_based, required (at least one):{"featureName", "operator", "value", "targetVariantId", "priority"?}`. Operators `equals`, `not_equals`, `in,not_in,greater_than,less_than,contains,regex;valueis a list forin/not_in, a number forgreater_than/less_than, a valid pattern forregex.priority(lower first) defaults to the order given. A rule serving could not apply is refused, naming it.bandit_config(Mapping[str, Any] | None, optional): Formulti_armed_bandit:{"algorithm": "epsilon_greedy" | "thompson_sampling" | "ucb1" | "linucb", "epsilon", "explorationBonus" (LinUCB's alpha), "contextFeatures", "rewardMetric"}`. `linucbroutes by the request'scontextFeatures: numeric input features every variant model declares as numbers, and every variant model must have Record inputs on (LinUCB learns from each prediction's recorded inputs).canary_config(Mapping[str, Any] | None, optional): Forcanary:{"controlVariantId", "targetVariantId", "stages", "currentPercentage", "targetPercentage", "bakeMinutes", "errorRateDelta", "latencyP95DeltaMs", "minRequestsForDecision"}. A stage advances only afterbakeMinutesandminRequestsForDecisioncanary predictions, judged on the current stage's traffic against the control.
retrieve
retrieve(test_id: str) -> ABTest
Return an A/B test.
Its variants and their counters, strategy config and deployment (status).
update
update(test_id: str, *, name: str | None = None, description: str | None = None, tags: Sequence[str] | None = None) -> ABTest
Change an A/B test's name, description or tags and return it.
delete
delete(test_id: str) -> None
Delete an A/B test (stopped first when running).
Its recorded predictions are kept for their retention period.
Lifecycle & actions
start
start(test_id: str) -> ABTest
Start a stopped A/B test again (re-deploys it). Returns the test.
stop
stop(test_id: str) -> ABTest
Stop routing (status stopped); predictions, counters and experiments are kept.
deploy
deploy(test_id: str) -> ABTest
Start routing predictions to the variants' models (status running).
The variants' models must be deployed. Returns the test.
Other
analyze_experiment
analyze_experiment(experiment_id: str) -> dict[str, Any]
Analyze an experiment: each treatment against the control on the primary metric.
Returns controlStats, treatmentStats (mean, sample size, p-value, confidence
interval, relative improvement, significance), recommendation (treatment,
control or inconclusive), winnerVariantId and reason. A winner needs
min_sample_per_variant observations in every variant and a significant difference of
at least minimum_detectable_effect relative to the control's mean; reason says
which setting left the result inconclusive. relativeImprovement is None when the
control's mean is 0. A running experiment concludes when a sequential test is
significant or its sample or duration limit is reached.
conclude_experiment
conclude_experiment(experiment_id: str) -> ABTestExperiment
End a running experiment as a finished run (status completed).
create_experiment
create_experiment(test_id: str, *, name: str, control_variant_id: str, treatment_variant_ids: Sequence[str], primary_metric: str, description: str | None = None, hypothesis: str | None = None, confidence_level: float | None = None, minimum_detectable_effect: float | None = None, min_sample_per_variant: int | None = None, max_sample_per_variant: int | None = None, max_duration_days: float | None = None) -> ABTestExperiment
Create an experiment (status draft) and return its id.
It compares each treatment variant with the control on the primary metric over the predictions the test routes while it runs.
Parameters
primary_metric(str):success_rate(higher is better),accuracy(each prediction against its actual: alabelsent withprediction_feedbackor the model's actuals, higher is better),latency(lower is better) orcustom(the reward fromprediction_feedback, higher is better).confidence_level(float | None, optional): Between 0 and 1 (default 0.95).minimum_detectable_effect(float | None, optional): Between 0 and 1 (default 0.05).min_sample_per_variant(int | None, optional): Samples per variant before a result is meaningful (default 100).max_sample_per_variant, max_duration_days: Conclude the experiment once every variant has this many samples, or after this many days (default and maximum 83, within the 90-day prediction retention).
delete_experiment
delete_experiment(experiment_id: str) -> None
Delete an experiment and its results (the test's predictions are kept).
list_experiments
list_experiments(test_id: str, *, search: str | None = None, sort: str | None = None, limit: int | None = None) -> SyncPaginator[ABTestExperiment]
Return the test's experiments (newest first) with their status and latest results.
Parameters
sort(str | None, optional):-createdAt(default),name,status,startedAt.
metrics
metrics(test_id: str) -> ABTestMetrics
Return the test's totals since it was created, overall and per variant.
predict
predict(test_id: str, input_data: Any, *, entity_id: str | None = None) -> dict[str, Any]
Predict through a running A/B test.
The response is the serving model's output plus prediction_id and the variant_id
that served it; pass prediction_id to prediction_feedback.
entity_id is the user or entity the prediction is for. A test with
sticky_routing requires it (the entity keeps its variant); a
linucb test also needs each of its context features in
input_data as one number.
prediction_feedback
prediction_feedback(prediction_id: str, *, reward: float | None = None, label: str | None = None) -> dict[str, Any]
Record the outcome of a prediction a test routed.
Parameters
reward(float | None, optional): 0 to 1, kept on the prediction (sending it again replaces it, and a bandit counts only the difference). Amulti_armed_bandittest learns from it (linucbover the prediction's recorded inputs, so the variant model needs Record inputs on) and an experiment withprimary_metric="custom"measures it.label(str | None, optional): The prediction's actual (its true outcome), recorded with the model's actuals.
predictions
predictions(test_id: str, *, variant_id: str | None = None, search: str | None = None, sort: str | None = None, limit: int | None = None) -> SyncPaginator[PredictionRecord]
Return the predictions the test routed (newest first).
The variant that served each and why, latency, success, and its actual and reward when recorded.
Parameters
variant_id(str | None, optional): Only one variant's predictions.search(str | None, optional): Matches prediction id, entity id, variant, routing reason or error, or the prediction exactly.sort(str | None, optional):-timestamp(default),latencyMs,variantId,success,confidence.limit(int | None, optional): Maximum number of predictions (default: all).
start_experiment
start_experiment(experiment_id: str) -> ABTestExperiment
Start a draft experiment: it counts the predictions from now until it ends.
stop_experiment
stop_experiment(experiment_id: str) -> ABTestExperiment
Cancel a draft or running experiment (status cancelled).
traffic
traffic(test_id: str) -> ABTestTraffic
Return requests, errors and latency per variant over time.
update_variant
update_variant(test_id: str, variant_id: str, *, weight: float | None = None, enabled: bool | None = None) -> dict[str, Any]
Update a variant: its share of traffic, whether it is enabled, or both.
Parameters
weight(float | None): The enabled variant's share of traffic (a 0-1 fraction) in aweighted_randomtest; the other enabled variants are rescaled to fill the rest.enabled(bool | None):Falsedisables the variant (it receives no traffic); at least two stay enabled.