infer.online¶
Real-time prediction loop: poll ActivityWatch, predict, smooth, and report.
See Inference Contract for the
canonical pipeline order and how this module fits the online runtime path.
OnlinePredictor is implemented as a slotted dataclass while preserving
its constructor shape (model, metadata, then keyword-only options).
Model auto-resolution¶
--model-dir is optional. When omitted, the model is resolved
automatically using resolve_model_dir() from taskclf.infer.resolve:
- If
models/active.jsonexists and points to a valid, compatible bundle, use it. - Otherwise, scan
--models-dir(defaultmodels/) and select the best bundle by policy. - If no eligible model is found, exit with a descriptive error.
--model-dir always overrides auto-resolution when provided.
Model hot-reload¶
When --models-dir is provided (always the case with the default CLI),
the online loop watches models/active.json (see model bundle layout) for changes using mtime
polling (default interval: 60 seconds). When a change is detected:
- The new active bundle is resolved and loaded.
- The
OnlinePredictoris rebuilt with the new model. - If loading fails, the current model is kept and a warning is logged.
This allows retraining to promote a new model while the online loop is running, without requiring a restart.
Label queue integration¶
When label_queue_path is provided, the online loop auto-enqueues
predictions whose confidence falls below label_confidence_threshold
(default 0.55) into the ActiveLabelingQueue. Enqueued items surface
in taskclf labels show-queue and the web UI for
manual review.
Enable via CLI:
At shutdown, the loop prints how many buckets were enqueued during the session.
Persistent feature state¶
The online loop creates an OnlineFeatureState (see
infer.feature_state) that buffers recent
FeatureRow values across poll cycles. After each row is built from
the current poll window, it is pushed into the state and the corrected
rolling aggregates (15-minute app switch counts, rolling means, deltas,
session length) are overlaid onto the row before prediction. This
ensures features match the full history windows the model was trained on,
rather than being truncated to the narrow poll slice.
The feature state is preserved across model hot-reloads since it tracks feature history, not model state.
Missing-value handling¶
OnlinePredictor._encode_value() fills missing numeric values with 0.0,
matching the training and batch inference paths which use fillna(0).
Per-user reject thresholds¶
OnlinePredictor accepts an optional per_user_reject_thresholds dict
mapping user IDs to individual reject thresholds. When present and the
current row's user_id is found in the dict, the per-user threshold
overrides the global reject_threshold for the rejection decision.
Users not in the dict fall back to the global threshold.
Per-user thresholds are loaded from InferencePolicy.per_user_reject_thresholds
by resolve_inference_config() and threaded through ResolvedInferenceConfig.
Unknown-category handling¶
When a categorical value is not found in the fitted encoder's vocabulary,
_encode_value() checks whether the encoder contains an "__unknown__"
class (present when the model was trained with encode_categoricals
using min_category_freq / unknown_mask_rate). If so, the
__unknown__ code is returned; otherwise -1.0 is used as a legacy
fallback. This ensures that models trained with explicit unknown-category
exposure produce calibrated confidence on novel inputs rather than
defaulting to an out-of-vocabulary sentinel the model never learned.
taskclf.infer.online
¶
Real-time prediction loop: poll ActivityWatch, predict, smooth, and report.
The online predictor never retrains -- it loads a frozen model bundle and applies it to live data from a running ActivityWatch server instance.
OnlinePredictor
dataclass
¶
Stateful single-bucket predictor with rolling smoothing.
Maintains a buffer of recent raw predictions and a running list of
segments so that callers only need to feed one :class:FeatureRow
at a time.
Source code in src/taskclf/infer/online.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | |
predict_bucket(row)
¶
Predict a single bucket and return a full :class:WindowPrediction.
Pipeline: raw model proba -> calibrate -> reject check ->
rolling majority smoothing (on core labels) -> taxonomy resolve
-> assemble WindowPrediction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
FeatureRowBase
|
A validated :class: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
WindowPrediction
|
class: |
WindowPrediction
|
predictions, confidence, and rejection status. |
Source code in src/taskclf/infer/online.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
get_segments()
¶
Return the running segment list built from all predictions so far.
Applies hysteresis merging so segments shorter than
MIN_BLOCK_DURATION_SECONDS are absorbed by their neighbours.
Source code in src/taskclf/infer/online.py
run_online_loop(*, model_dir, models_dir=None, aw_host=DEFAULT_AW_HOST, poll_seconds=DEFAULT_POLL_SECONDS, smooth_window=DEFAULT_SMOOTH_WINDOW, title_salt=DEFAULT_TITLE_SALT, out_dir=Path(DEFAULT_OUT_DIR), bucket_seconds=DEFAULT_BUCKET_SECONDS, idle_gap_seconds=DEFAULT_IDLE_GAP_SECONDS, reject_threshold=DEFAULT_REJECT_THRESHOLD, taxonomy_path=None, calibrator_path=None, calibrator_store_path=None, label_queue_path=None, label_confidence_threshold=DEFAULT_LABEL_CONFIDENCE_THRESHOLD)
¶
Poll ActivityWatch, predict, smooth, and write results continuously.
Runs until interrupted with KeyboardInterrupt (Ctrl+C). On
shutdown, writes final segments and prints a summary.
Session state is tracked across poll cycles. A new session starts when the gap since the last observed event exceeds idle_gap_seconds.
When models_dir is provided, the loop watches
models/active.json for changes and hot-reloads the model bundle
without restarting. The swap only occurs after the new bundle loads
successfully; on failure the current model is kept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_dir
|
Path
|
Path to a trained model bundle directory. |
required |
models_dir
|
Path | None
|
Base directory for model bundles. When provided,
enables automatic model reload on |
None
|
aw_host
|
str
|
Base URL of the ActivityWatch server. |
DEFAULT_AW_HOST
|
poll_seconds
|
int
|
Seconds between polling iterations. |
DEFAULT_POLL_SECONDS
|
smooth_window
|
int
|
Rolling majority window size. |
DEFAULT_SMOOTH_WINDOW
|
title_salt
|
str
|
Salt for hashing window titles. |
DEFAULT_TITLE_SALT
|
out_dir
|
Path
|
Directory for |
Path(DEFAULT_OUT_DIR)
|
bucket_seconds
|
int
|
Width of each time bucket in seconds. |
DEFAULT_BUCKET_SECONDS
|
idle_gap_seconds
|
float
|
Minimum gap (seconds) that starts a new session. |
DEFAULT_IDLE_GAP_SECONDS
|
reject_threshold
|
float | None
|
If given, predictions with
|
DEFAULT_REJECT_THRESHOLD
|
taxonomy_path
|
Path | None
|
Optional path to a taxonomy YAML file. When provided, output labels are mapped to user-defined buckets. |
None
|
calibrator_path
|
Path | None
|
Optional path to a calibrator JSON file. When provided, raw model probabilities are calibrated before the reject decision. |
None
|
calibrator_store_path
|
Path | None
|
Optional path to a calibrator store directory. When provided, per-user calibration is applied. Takes precedence over calibrator_path. |
None
|
label_queue_path
|
Path | None
|
Optional path to the labeling queue JSON. When provided, low-confidence predictions are auto-enqueued for manual labeling. |
None
|
label_confidence_threshold
|
float
|
Predictions with confidence below this value are enqueued when label_queue_path is set. |
DEFAULT_LABEL_CONFIDENCE_THRESHOLD
|
Source code in src/taskclf/infer/online.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 | |
taskclf.infer.resolve
¶
Model resolution for inference: resolve --model-dir and hot-reload.
Bridges CLI arguments to the model registry, providing:
- :func:
resolve_model_dir— resolve an optional--model-dirto a concrete :class:~pathlib.Pathusing the active pointer / best-model selection fallback. Deprecated — prefer :func:resolve_inference_config. - :func:
resolve_inference_config— resolve the full inference configuration (model + calibrator + threshold) from an :class:~taskclf.core.inference_policy.InferencePolicy. - :class:
ActiveModelReloader— lightweight mtime-based watcher that detectsactive.jsonchanges and reloads the model bundle for long-running online inference loops. Deprecated — prefer :class:InferencePolicyReloader. - :class:
InferencePolicyReloader— watchesinference_policy.json(falling back toactive.json) and reloads the full inference config on change.
ModelResolutionError
dataclass
¶
Bases: Exception
Raised when no model can be resolved for inference.
Source code in src/taskclf/infer/resolve.py
ActiveModelReloader
dataclass
¶
Watch active.json and reload the model bundle on change.
.. deprecated::
Use :class:InferencePolicyReloader instead. This class only
reloads the model bundle; it does not update the calibrator
store or reject threshold when the policy changes.
Designed for the online inference loop: polls the file's mtime at a configurable interval and, when a change is detected, loads the new bundle. The caller only swaps to the new model after a successful load — on failure the current model is kept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
models_dir
|
Path
|
Directory containing |
required |
check_interval_s
|
float
|
Minimum seconds between mtime checks. |
60.0
|
Source code in src/taskclf/infer/resolve.py
check_reload()
¶
Check whether active.json changed and reload if so.
Returns the new (model, metadata, cat_encoders) tuple when a
reload succeeds, or None when no reload is needed or the
reload fails (a warning is logged on failure).
Source code in src/taskclf/infer/resolve.py
ResolvedInferenceConfig
dataclass
¶
Fully resolved inference configuration ready for use.
Produced by :func:resolve_inference_config. Contains all
loaded artifacts so callers do not need to perform additional I/O.
Source code in src/taskclf/infer/resolve.py
InferencePolicyReloader
dataclass
¶
Watch inference_policy.json and reload the full config on change.
Falls back to watching active.json when no policy file exists.
Designed for the online inference loop: polls file mtimes at a
configurable interval and returns a :class:ResolvedInferenceConfig
when a reload is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
models_dir
|
Path
|
Directory containing policy / active pointer files. |
required |
check_interval_s
|
float
|
Minimum seconds between mtime checks. |
60.0
|
Source code in src/taskclf/infer/resolve.py
check_reload()
¶
Check whether the policy/active file changed and reload if so.
Returns a :class:ResolvedInferenceConfig when a reload
succeeds, or None when no reload is needed or the reload
fails.
Source code in src/taskclf/infer/resolve.py
resolve_model_dir(model_dir, models_dir, policy=None)
¶
Resolve the model directory for inference.
.. deprecated::
Use :func:resolve_inference_config instead. This function
only resolves the model bundle path; it does not load the
calibrator store or reject threshold from the inference policy.
Resolution precedence:
- If model_dir is provided, validate that it exists and return it.
- Otherwise, delegate to :func:
~taskclf.model_registry.resolve_active_modelwhich readsactive.jsonor falls back to best-model selection. - If no eligible model is found, raise :class:
ModelResolutionErrorwith a descriptive message including exclusion reasons.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_dir
|
str | None
|
Explicit |
required |
models_dir
|
Path
|
Base directory containing promoted model bundles. |
required |
policy
|
SelectionPolicy | None
|
Selection policy override (defaults to policy v1). |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to the resolved model bundle directory. |
Raises:
| Type | Description |
|---|---|
ModelResolutionError
|
When no model can be resolved. |
Source code in src/taskclf/infer/resolve.py
resolve_inference_config(models_dir, *, model_dir_override=None, reject_threshold_override=None, calibrator_store_override=None, calibrator_path_override=None)
¶
Resolve the full inference configuration from policy or fallback.
Resolution precedence:
- Explicit model_dir_override — bypasses policy; uses override flags for threshold and calibrator.
models/inference_policy.json— loads model, calibrator store, and threshold from the policy. Explicit overrides still take precedence for individual fields.models/active.json+ code defaults — deprecated legacy fallback.- Best-model selection + code defaults — no-config fallback.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
models_dir
|
Path
|
The |
required |
model_dir_override
|
str | None
|
Explicit |
None
|
reject_threshold_override
|
float | None
|
Explicit threshold that overrides the policy value. |
None
|
calibrator_store_override
|
Path | None
|
Explicit calibrator store path that overrides the policy value. |
None
|
calibrator_path_override
|
Path | None
|
Explicit single-calibrator JSON path (lowest calibrator precedence). |
None
|
Returns:
| Type | Description |
|---|---|
ResolvedInferenceConfig
|
A fully resolved :class: |
Raises:
| Type | Description |
|---|---|
ModelResolutionError
|
When no model can be resolved. |
Source code in src/taskclf/infer/resolve.py
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 | |