API reference¶
AdaptiveRanker is the package-root API. It fits from completed learning
attempts, ranks an application-approved candidate set, and accepts feedback.
Supporting modules add durable audit records, catalog validation, and a
reference controlled-pilot adapter.
Use the production-serving guide for the ordinary request loop and the pilot integration contract when a next-item choice is part of an efficacy study.
At a glance¶
| Need | API | Result |
|---|---|---|
| Fit a learning policy | AdaptiveRanker().fit(events, ...) |
The fitted ranker |
| Rank safe exercises | ranker.recommend(user_id, ids) |
AdaptiveLearningRecommendation values |
| Adapt after an attempt | ranker.observe(...) |
Updates the learner state |
| Keep a reproducible serving record | ranker.recommend_and_log(...) |
Ranked items and immutable LoggedDecision |
| Attach a delayed score safely | ranker.observe_decision(...) |
Durable DecisionOutcome and normally a learner update |
| Validate authoring data | validate_learning_catalog(frame) |
Normalized exercises and all diagnostics |
| Run a reference experiment adapter | AdaptivePracticePilot |
Assignment, decisions, lifecycle evidence, analysis frames |
All timestamps must be finite, non-negative numeric values in one consistent
application-defined unit. Outcomes are binary 0 or 1; do not turn missing,
abandoned, or unscored work into 0.
AdaptiveRanker¶
Construct and fit¶
ranker = AdaptiveRanker(
kt_backbone="empirical", # optional AdaptiveRankerConfig override
decision_store=store, # optional durable store
)
ranker.fit(
events,
user_col="user_id",
item_col="item_id",
outcome_col="outcome",
timestamp_col="timestamp",
category_col=None,
difficulty_col=None,
catalog=None,
catalog_item_col="item_id",
catalog_category_col="category_id",
catalog_difficulty_col="difficulty",
prerequisite_by_concept=None,
)
Pass either an AdaptiveRankerConfig as the first argument or its fields as
keyword overrides; unknown names raise TypeError. fit returns the same
ranker and expects a pandas DataFrame of completed attempts with learner, item,
binary outcome, and timestamp columns.
A canonical catalog is the preferred source of item category and difficulty. It
validates historical coverage and registers catalog-only items for future
serving. Its default columns are item_id, category_id, and difficulty;
use the three catalog_*_col arguments for a different source schema.
prerequisite_by_concept constrains ranking, but it does not replace the
application's item-level eligibility.
ranker = AdaptiveRanker().fit(
completed_attempts,
catalog=exercise_catalog,
prerequisite_by_concept={"advanced": ["fundamentals"]},
)
Use ranker.is_fitted after a successful fit. ranker.learning_readiness()
returns data support, outcome balance, metadata coverage, selected tracer, and
recommended next steps; it is rollout evidence, not a model-quality guarantee.
ranker.diagnostics() additionally reports adaptive, policy, semantic, and
decision-store state.
Sparse learning data automatically uses Orchid's empirical learner. For a first
frozen pilot, explicitly set kt_backbone="empirical".
recommend¶
recommendations = ranker.recommend(
user_id,
candidate_item_ids,
top_k=3,
concept_goal=None,
context_hash=None,
)
Ranks the supplied candidate set, returning
AdaptiveLearningRecommendation values in descending order.
| Field | Meaning |
|---|---|
item_id |
Candidate in ranking order |
score |
Relative score within this request |
outcome_probability |
Estimated probability of a positive next attempt |
difficulty, category_id, competence |
Available learning context |
item_support, concept_support, feedback_supported |
Evidence/support indicators |
prerequisites_met, recent_repetition |
Constraint and sequencing signals |
Scores are for ranking the current candidates, not globally calibrated learning
values. outcome_probability estimates the next practice result, not retained
mastery. An explicit empty sequence returns [] and never means “all known
items.” candidate_item_ids=None only permits a configured candidate generator
or explicit catalog fallback.
The application owns eligibility: remove unavailable, completed,
accommodation-inappropriate, prerequisite-blocked, and assessment-only items
before calling recommend. PilotCatalog enforces a published version of
these controls for the reference pilot.
observe and register_items¶
ranker.observe(
user_id=learner_id,
item_id=exercise_id,
outcome=1,
timestamp=attempt_time,
update_global=True,
)
ranker.register_items(catalog)
observe advances the live learner state from a completed attempt. Set
update_global=False only where a learner may adapt while shared aggregate
statistics must remain frozen, such as treatment traffic in a controlled pilot.
register_items adds catalog items absent from fitting history with an OOV
prior, making them available for recommendation and feedback immediately.
Refit later to learn item-specific parameters from their accumulated outcomes.
Durable decisions and delayed outcomes¶
Use a decision store whenever a served exercise must be reproducible or evaluated later.
from orchid_ranker.decision_store import SQLiteDecisionStore
store = SQLiteDecisionStore("orchid.sqlite")
ranker = AdaptiveRanker(decision_store=store).fit(completed_attempts)
LoggedDecision records the full candidate list, chosen item, score/probability
evidence, policy identity, and immutable application metadata.
DecisionOutcome links one later result to one decision. Its apply_state
flag says whether that event may change adaptive state; update_global says
whether a permitted update may change shared aggregate statistics.
recommend_and_log¶
ranked, decision = ranker.recommend_and_log(
user_id=learner_id,
candidate_item_ids=eligible_ids,
timestamp=request_time,
top_k=1,
exploration=0.0,
decision_id=request_id,
policy_version="course-2026.1",
decision_metadata={"catalog_version": "2026.1"},
)
This is the serving method for auditable decisions. It requires a non-empty
explicit candidate set and returns the selected item first plus an immutable
LoggedDecision.
| Parameter | Operational meaning |
|---|---|
decision_id |
Stable application request ID. An exact retry returns the saved choice; reuse with different inputs raises ValueError. |
exploration |
Epsilon-uniform sampling among candidates that survive configured serving constraints. Start pilots at 0.0. |
decision_metadata |
JSON-compatible application context retained immutably and included in idempotency checks. |
allow_unsupported_feedback |
Use only if an external system supplies the complete feedback path for an unsupported item. |
min_*, max_*, require_prerequisites |
Support, predicted-outcome, difficulty, and prerequisite guards applied before sampling. |
Persist the decision before rendering the item. Do not change the candidate list, model/catalog identity, or metadata on a delivery retry.
observe_decision, persistence, and recovery¶
linked = ranker.observe_decision(
decision.decision_id,
outcome=correct,
timestamp=scored_at,
outcome_event_id=lms_score_event_id,
)
observe_decision first durably attaches the outcome, then applies it to the
live learner state. A decision accepts one immutable outcome. Repeating the
same payload is safe; a conflicting payload raises ValueError. Pass an
LMS-global outcome_event_id whenever it exists: Orchid enforces uniqueness
across decisions, not merely within one decision.
persist_decision_outcome(...) accepts the same outcome, reward, timestamp,
category, event ID, apply_state, and update_global arguments but does not
change state. Use it when a lifecycle adapter must persist score evidence
before projecting it into a learner model.
| Method | Use |
|---|---|
ranker.replay_pending_outcomes() |
Continue from the same restored model baseline. Applies durable, uncheckpointed outcomes in learner-time order. |
ranker.replay_all_outcomes_from_baseline() |
Rebuild a newly fitted/restored baseline. Do not call it on a live projection or outcomes will be applied twice. |
ranker.decision_log_frame(completed_only=False) |
Export decisions joined with outcomes, event IDs, and state-update flags. |
InMemoryDecisionStore is thread-safe but process-local.
SQLiteDecisionStore is transactional, uses WAL mode, and is appropriate for
a single-host service; close it explicitly or use it as a context manager.
Both implement DecisionOutcomeStore; a custom backend must support immutable,
idempotent decision/outcome creation plus outcome-application checkpoints.
Catalog validation¶
from orchid_ranker.learning_catalog import (
LearningCatalogSchema,
validate_learning_catalog,
)
validation = validate_learning_catalog(
authoring_catalog,
schema=LearningCatalogSchema(),
require_complete_metadata=True,
)
validation.raise_for_errors()
validate_learning_catalog never mutates the DataFrame and returns all
findings rather than failing at the first one. It validates canonical exercise
identities (item_id, optionally content_version), metadata,
duplicate/conflicting versions, prerequisites, dangling references, and cycles.
| Input/result | Use |
|---|---|
LearningCatalogSchema |
Maps source columns. item_id_col is required; set an optional column to None only where it does not exist upstream. |
require_complete_metadata=True |
Makes absent curriculum, skill/category, difficulty, assessment, and prerequisite metadata blocking errors. Use for a pilot import gate. |
allow_external_prerequisites=True |
Permits prerequisite IDs owned by another catalog; otherwise dangling IDs are errors. |
LearningCatalogValidation.exercises |
Normalized immutable CanonicalExercise values. |
.diagnostics, .errors, .warnings |
Actionable findings, including source row indices. |
.is_valid, .raise_for_errors() |
Boolean gate or concise exception after inspection. |
This is an import-quality gate, not a serving eligibility engine. Use
PilotCatalog when delivery must be checked against a frozen catalog.
Controlled pilot API¶
orchid_ranker.pilot is a reference single-host LMS adapter. It creates an
immutable experiment manifest, deterministic sticky assignment, append-only
delivery evidence, and analysis exports. It is not an LMS connector and does
not replace a study design, power calculation, or governance process.
Build a frozen pilot¶
from orchid_ranker.pilot import (
AdaptivePracticePilot,
PilotCatalog,
PilotRequest,
SQLiteExperimentAssignmentStore,
SQLitePilotLifecycleStore,
)
catalog = PilotCatalog.from_frame(
authoring_catalog,
catalog_version="networking-2026.1",
)
database = "networking-pilot.sqlite"
pilot = AdaptivePracticePilot(
ranker,
catalog,
experiment_id="networking-routing",
model_artifact_id="empirical-2026-08-30",
authored_policy_version="routing-static-v1",
eligibility_rule_version="v1",
treatment_fraction=0.5,
randomization_salt="deployment-secret",
assignment_store=SQLiteExperimentAssignmentStore(database),
lifecycle_store=SQLitePilotLifecycleStore(database),
initial_mode="aa",
)
PilotCatalog.from_frame requires one active content version per item_id.
It validates content_version, course/module, a skill or category, difficulty,
assessment flag, prerequisites, availability, required flag, and unique
authored sequence position. eligible_items(...) rejects—not silently
drops—an LMS candidate that is outside scope, unavailable, completed,
assessment-only, prerequisite-blocked, or inconsistent with a required item.
AdaptivePracticePilot requires a fitted empirical baseline without a CQL
overlay. It persists an ExperimentManifest with catalog digest/version, model
artifact/config identity, authored policy, eligibility version, and allocation
configuration. Reusing an experiment ID with a changed artifact or catalog
fails rather than mixing evidence.
For durable use, put SQLiteDecisionStore,
SQLiteExperimentAssignmentStore, and SQLitePilotLifecycleStore on the same
database path. The in-memory variants are for tests and one-process prototypes.
Serve an LMS request¶
served = pilot.serve(PilotRequest(
request_id=lms_request_id,
user_id=learner_id,
course_id="networking",
module_id="routing",
course_run_id=cohort_id,
timestamp=request_time,
completed_item_ids=tuple(completed_ids),
candidate_item_ids=tuple(exact_lms_eligible_ids),
stratum=baseline_band,
))
candidate_item_ids is required and must be the exact non-null LMS-approved
set. request_id is namespaced by experiment and course run, preventing
cross-experiment collisions. Assignment is stable per learner and stratified
by the supplied stratum.
serve returns a PilotDecision:
| Field | Meaning |
|---|---|
decision |
Immutable LoggedDecision; render decision.chosen_item_id. |
arm |
Sticky randomized assignment (control or treatment). |
effective_arm |
Actually delivered arm after applying the current mode. |
mode |
Mode recorded on this decision. |
chosen_content_version |
Exact revision the LMS must render. |
reason_code, eligible_item_ids |
Why it was chosen and the exact candidates. |
Every decision automatically records an immutable explanation snapshot. Shadow decisions also record the Orchid proposal; halted decisions record a kill-switch fallback event.
Modes and operation events¶
pilot.set_mode(
"shadow",
event_id=change_ticket_id,
timestamp=changed_at,
reason="validated A/A delivery joins",
)
| Mode | Actual delivery |
|---|---|
"aa" |
Authored control for both assignments; assignments and lifecycle evidence are still audited. |
"shadow" |
Authored control; Orchid proposal is computed and recorded but not rendered. |
"active" |
Frozen Orchid treatment only for assigned treatment learners; assigned controls remain authored. |
"halted" |
All future requests use authored control with KILL_SWITCH evidence. A halted experiment cannot be re-enabled. |
set_mode is idempotent for the same event payload and records an operation
event. A transition cannot move backward in time; use a new experiment_id to
restart after a halt.
Record actual delivery and score it¶
pilot.record_rendered(
served.decision.decision_id,
event_id=lms_render_event_id,
item_id=served.decision.chosen_item_id,
content_version=served.chosen_content_version,
timestamp=rendered_at,
)
pilot.record_submitted(
served.decision.decision_id,
event_id=lms_submission_event_id,
item_id=served.decision.chosen_item_id,
content_version=served.chosen_content_version,
timestamp=submitted_at,
)
outcome = pilot.record_scored(
served.decision.decision_id,
outcome_event_id=lms_score_event_id,
item_id=served.decision.chosen_item_id,
content_version=served.chosen_content_version,
outcome=correct,
timestamp=scored_at,
)
These calls are append-only and idempotent for the same event ID/payload. The
adapter enforces decision → rendered → submitted → scored, the selected
item/version, and nondecreasing timestamps. record_scored first stores its
score event, then persists/uses its outcome. It requires an LMS-global
outcome_event_id.
Only active effective-treatment scores update learner-local state, and those
updates leave aggregate empirical statistics frozen. Control, A/A, shadow, and
halted scores remain durable audit evidence with apply_state=False.
| Recovery method | Purpose |
|---|---|
pilot.recover_scored_events() |
Finish the outcome projection after a crash between score-event persistence and application. |
pilot.rebuild_state_from_baseline() |
Persist score evidence and rebuild treatment learner state from a freshly restored/fitted ranker baseline. Do not call on a live projection. |
pilot.observe_decision(...) |
Compatibility method for a score that already has a submission event. New integrations should call record_scored. |
Import delayed assessments and export evidence¶
import_delayed_assessments requires a pandas DataFrame with
assessment_event_id, user_id, course_run_id, assessment_form_version,
timestamp, score, and independent. independent must be literal True;
the method rejects an assessment for a learner/course run without pilot
participation and never feeds it into adaptive state.
Use assessment_frame() for imported assessments,
decision_frame(completed_only=False) for joined delivery decisions/outcomes,
and analysis_frame() for decisions plus rendered/submitted/scored IDs and
timestamps, fallback/shadow/explanation evidence, and matching assessments.
Offline policy and evaluation APIs¶
The base adaptive policy is the recommended first deployment. Offline CQL is an opt-in promotion path, not a shortcut around a controlled learning study.
report = ranker.fit_policy(
earlier_completed_decisions,
evaluation_decisions=later_completed_decisions,
)
fit_policy trains tabular CQL from a training decision log and promotes it
only if a strictly later, disjoint evaluation log passes the
user-cluster-bootstrap rollout gate. By default it requires at least 30
evaluation events and 30 users. fit_policy_rolling(...) creates the same
strict chronological split from a trailing source window.
Use ope_report(...) or bootstrap_ope_report(...) for logged-policy
evaluation and shadow_report() to summarize traffic, outcome coverage,
calibration/drift, and, when applicable, policy evidence. These reports require
complete candidate lists, propensities, and linked outcomes; they do not prove
a causal learning benefit on their own.