detectkit 0.42.0__tar.gz → 0.44.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.
- {detectkit-0.42.0/detectkit.egg-info → detectkit-0.44.0}/PKG-INFO +1 -1
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/__init__.py +1 -1
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/autotuner.py +30 -2
- detectkit-0.44.0/detectkit/autotune/runner.py +102 -0
- detectkit-0.44.0/detectkit/cli/_output.py +112 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/CLAUDE.section.md +3 -1
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/autotune.md +10 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/cli.md +15 -3
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/autotune.py +17 -69
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/tune.py +4 -0
- detectkit-0.44.0/detectkit/tuning/assets/tune.js +187 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/tuning/payload.py +31 -17
- detectkit-0.44.0/detectkit/tuning/server.py +512 -0
- {detectkit-0.42.0 → detectkit-0.44.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit.egg-info/SOURCES.txt +1 -0
- detectkit-0.42.0/detectkit/cli/_output.py +0 -50
- detectkit-0.42.0/detectkit/tuning/assets/tune.js +0 -176
- detectkit-0.42.0/detectkit/tuning/server.py +0 -233
- {detectkit-0.42.0 → detectkit-0.44.0}/LICENSE +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/MANIFEST.in +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/README.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/core/models.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/pyproject.toml +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/requirements.txt +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/setup.cfg +0 -0
- {detectkit-0.42.0 → detectkit-0.44.0}/setup.py +0 -0
|
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
|
|
|
4
4
|
A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.44.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -6,7 +6,9 @@ command handles loading, persistence, config emission and candidate cleanup.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import contextlib
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Callable, Iterator
|
|
10
12
|
from datetime import datetime, timedelta
|
|
11
13
|
from typing import Any
|
|
12
14
|
|
|
@@ -27,6 +29,31 @@ from detectkit.detectors.factory import DetectorFactory
|
|
|
27
29
|
|
|
28
30
|
_ALERT_WINDOW_GRID = (1, 2, 3, 4, 5)
|
|
29
31
|
|
|
32
|
+
# The windowed detectors emit a one-time "seasonality group can't fill this
|
|
33
|
+
# window → falls back to global" warning per *instance*. A tune builds dozens of
|
|
34
|
+
# throwaway candidate detectors, so that warning would flood the terminal and
|
|
35
|
+
# bury the structured decision log. The engine already surfaces an under-fill of
|
|
36
|
+
# the *chosen* seasonality as a `window` advisory in its own log, so the raw
|
|
37
|
+
# per-candidate warnings are pure noise here.
|
|
38
|
+
_WINDOWED_LOGGER = "detectkit.detectors.statistical._windowed"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@contextlib.contextmanager
|
|
42
|
+
def _quiet_per_candidate_warnings() -> Iterator[None]:
|
|
43
|
+
"""Silence the windowed detectors' per-instance under-fill warning for a tune.
|
|
44
|
+
|
|
45
|
+
Scoped + restored, so a real ``dtk run`` (which builds one detector and *does*
|
|
46
|
+
want the warning) is unaffected — only the candidate sweep inside the engine
|
|
47
|
+
is quieted.
|
|
48
|
+
"""
|
|
49
|
+
logger = logging.getLogger(_WINDOWED_LOGGER)
|
|
50
|
+
prev_level = logger.level
|
|
51
|
+
logger.setLevel(logging.ERROR)
|
|
52
|
+
try:
|
|
53
|
+
yield
|
|
54
|
+
finally:
|
|
55
|
+
logger.setLevel(prev_level)
|
|
56
|
+
|
|
30
57
|
|
|
31
58
|
def _ts_to_dt(ts64: np.datetime64) -> datetime:
|
|
32
59
|
ms = int(ts64.astype("datetime64[ms]").astype(np.int64))
|
|
@@ -196,4 +223,5 @@ def run_autotune_engine(
|
|
|
196
223
|
settings=settings,
|
|
197
224
|
on_stage=on_stage,
|
|
198
225
|
)
|
|
199
|
-
|
|
226
|
+
with _quiet_per_candidate_warnings():
|
|
227
|
+
return tuner.tune()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Shared autotune entry point reused by ``dtk autotune`` and ``dtk tune``.
|
|
2
|
+
|
|
3
|
+
The pure ``run_autotune_engine`` operates on an in-memory ``data`` dict, a
|
|
4
|
+
``GroundTruth`` and a ``TuneSettings``. Both callers need the same plumbing to
|
|
5
|
+
get there from a metric's ``AutoTuneConfig`` + labels: resolve the scoring
|
|
6
|
+
metric, cap the training history, build the settings, project the labels onto
|
|
7
|
+
the grid, and run the engine. That plumbing used to live privately in
|
|
8
|
+
``cli/commands/autotune.py``; it lives here so the ``dtk tune`` server can reuse
|
|
9
|
+
it verbatim for its server-side **Autotune** mode (no browser port of the
|
|
10
|
+
scoring/CV/seasonality search). Neither this module nor the engine touches the
|
|
11
|
+
database or the filesystem — the caller loads the datapoints and decides what to
|
|
12
|
+
persist.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from detectkit.autotune._types import ScoringMetric
|
|
22
|
+
from detectkit.autotune.autotuner import run_autotune_engine
|
|
23
|
+
from detectkit.autotune.labels import IncidentLabels
|
|
24
|
+
from detectkit.autotune.result import AutoTuneResult
|
|
25
|
+
from detectkit.autotune.settings import TuneSettings
|
|
26
|
+
from detectkit.config.metric_config import AutoTuneConfig
|
|
27
|
+
|
|
28
|
+
# Cap the scored training span when the user doesn't pin max_history, so tuning
|
|
29
|
+
# stays responsive on very long histories (recent data is the most relevant).
|
|
30
|
+
DEFAULT_TRAIN_CAP = 50_000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_scoring(scoring_override: str | None, autotune_cfg: AutoTuneConfig) -> ScoringMetric:
|
|
34
|
+
"""Resolve the scoring metric: CLI override > config > MCC default.
|
|
35
|
+
|
|
36
|
+
Raises ``ValueError`` on an unknown name (callers translate as they see fit).
|
|
37
|
+
"""
|
|
38
|
+
value = scoring_override or autotune_cfg.scoring_metric or ScoringMetric.MCC.value
|
|
39
|
+
try:
|
|
40
|
+
return ScoringMetric(value)
|
|
41
|
+
except ValueError as exc:
|
|
42
|
+
allowed = ", ".join(m.value for m in ScoringMetric)
|
|
43
|
+
raise ValueError(f"Invalid scoring metric '{value}'. Allowed: {allowed}") from exc
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_settings(*, scoring: ScoringMetric, autotune_cfg: AutoTuneConfig) -> TuneSettings:
|
|
47
|
+
"""Map an ``AutoTuneConfig`` block (+ resolved scoring) to ``TuneSettings``."""
|
|
48
|
+
return TuneSettings(
|
|
49
|
+
metric=scoring,
|
|
50
|
+
beta=autotune_cfg.beta,
|
|
51
|
+
fold_count=autotune_cfg.folds,
|
|
52
|
+
stability_lambda=autotune_cfg.stability_lambda,
|
|
53
|
+
allowed_detector_types=autotune_cfg.detector_types,
|
|
54
|
+
allowed_seasonality=autotune_cfg.seasonality_candidates,
|
|
55
|
+
force_seasonality=autotune_cfg.force_seasonality,
|
|
56
|
+
fixed_params=dict(autotune_cfg.fixed_params),
|
|
57
|
+
max_history=autotune_cfg.max_history,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cap_history(data: dict[str, np.ndarray], max_history: int | None) -> dict[str, np.ndarray]:
|
|
62
|
+
"""Keep the most recent ``max_history`` (or the default cap) points."""
|
|
63
|
+
n = len(data["timestamp"])
|
|
64
|
+
cap = max_history if max_history is not None else DEFAULT_TRAIN_CAP
|
|
65
|
+
if n <= cap:
|
|
66
|
+
return data
|
|
67
|
+
return {
|
|
68
|
+
"timestamp": data["timestamp"][-cap:],
|
|
69
|
+
"value": data["value"][-cap:],
|
|
70
|
+
"seasonality_data": data["seasonality_data"][-cap:],
|
|
71
|
+
"seasonality_columns": data["seasonality_columns"],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def autotune_from_data(
|
|
76
|
+
*,
|
|
77
|
+
metric_name: str,
|
|
78
|
+
data: dict[str, np.ndarray],
|
|
79
|
+
labels: IncidentLabels,
|
|
80
|
+
interval_seconds: int,
|
|
81
|
+
autotune_cfg: AutoTuneConfig,
|
|
82
|
+
scoring_override: str | None = None,
|
|
83
|
+
on_stage: Callable[[str, str], None] | None = None,
|
|
84
|
+
) -> AutoTuneResult:
|
|
85
|
+
"""Run the full autotune engine on an in-memory series + labels.
|
|
86
|
+
|
|
87
|
+
Caps the history, resolves the scoring metric, projects ``labels`` onto the
|
|
88
|
+
(capped) grid as ground truth, builds the settings and runs the engine.
|
|
89
|
+
Pure: no DB, no filesystem, no lock — the caller decides what to persist.
|
|
90
|
+
"""
|
|
91
|
+
data = cap_history(data, autotune_cfg.max_history)
|
|
92
|
+
scoring = resolve_scoring(scoring_override, autotune_cfg)
|
|
93
|
+
ground_truth = labels.to_ground_truth(data["timestamp"], interval_seconds)
|
|
94
|
+
settings = build_settings(scoring=scoring, autotune_cfg=autotune_cfg)
|
|
95
|
+
return run_autotune_engine(
|
|
96
|
+
metric_name=metric_name,
|
|
97
|
+
data=data,
|
|
98
|
+
ground_truth=ground_truth,
|
|
99
|
+
interval_seconds=interval_seconds,
|
|
100
|
+
settings=settings,
|
|
101
|
+
on_stage=on_stage,
|
|
102
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Shared CLI output helpers so every command renders in one house style.
|
|
2
|
+
|
|
3
|
+
Mirrors the load → detect → alert pipeline's tree look (``┌─ / │ / └─``) used by
|
|
4
|
+
``dtk run`` so the maintenance commands (``dtk clean``, ``dtk unlock``) match it
|
|
5
|
+
instead of each inventing its own formatting.
|
|
6
|
+
|
|
7
|
+
House conventions:
|
|
8
|
+
- A metric *with* something to report is a tree: a cyan-bold ``┌─ <name>``
|
|
9
|
+
header followed by one child line per item (``│ `` for all but the last,
|
|
10
|
+
``└─ `` for the last).
|
|
11
|
+
- A metric with *nothing* to do is a single ``•`` line.
|
|
12
|
+
- A per-metric error is a red ``✗`` line (to stderr).
|
|
13
|
+
- The final summary is a cyan-bold ``Done. …`` line.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
# Autotune-engine stage name → display title for the streamed run-log tree.
|
|
23
|
+
# Most stages render as ``stage.upper()``; only the two underscored names need a
|
|
24
|
+
# space so the header reads cleanly (``DETECTOR SELECT`` / ``GRID SEARCH``).
|
|
25
|
+
AUTOTUNE_STAGE_TITLES = {
|
|
26
|
+
"labels": "LABELS",
|
|
27
|
+
"seasonality": "SEASONALITY",
|
|
28
|
+
"detector_select": "DETECTOR SELECT",
|
|
29
|
+
"grid_search": "GRID SEARCH",
|
|
30
|
+
"window": "WINDOW",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def echo_block(
|
|
35
|
+
title: str,
|
|
36
|
+
children: list[str],
|
|
37
|
+
*,
|
|
38
|
+
warnings: list[str] | None = None,
|
|
39
|
+
echo: Callable[[str], None] = click.echo,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Print a cyan-bold ``┌─ title`` header with ``│``/``└─`` child lines.
|
|
42
|
+
|
|
43
|
+
The injectable core of the house tree style (``dtk run``'s load/detect/alert
|
|
44
|
+
blocks): ``warnings`` render as yellow ``│`` continuation lines above the
|
|
45
|
+
children, the last child gets the ``└─`` elbow. ``echo`` defaults to
|
|
46
|
+
``click.echo`` but can be any line sink, so the tune server can stream the
|
|
47
|
+
same blocks through its own output callback. ``children`` must be non-empty.
|
|
48
|
+
"""
|
|
49
|
+
echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
|
|
50
|
+
for warning in warnings or []:
|
|
51
|
+
echo(click.style(f" │ ⚠ {warning}", fg="yellow", bold=True))
|
|
52
|
+
last = len(children) - 1
|
|
53
|
+
for i, child in enumerate(children):
|
|
54
|
+
prefix = " └─ " if i == last else " │ "
|
|
55
|
+
echo(f"{prefix}{child}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def echo_tree(name: str, children: list[str], *, warnings: list[str] | None = None) -> None:
|
|
59
|
+
"""Print a ``┌─ name`` header with ``│``/``└─`` child lines.
|
|
60
|
+
|
|
61
|
+
``warnings`` (if any) are rendered as yellow ``│`` continuation lines above
|
|
62
|
+
the children. ``children`` must be non-empty (a metric with no items should
|
|
63
|
+
use :func:`echo_noop` instead). Thin wrapper over :func:`echo_block` (the
|
|
64
|
+
CLI-default ``click.echo`` sink).
|
|
65
|
+
"""
|
|
66
|
+
echo_block(name, children, warnings=warnings)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class StageLogRenderer:
|
|
70
|
+
"""Stream ``(stage, line)`` engine progress as the run-log tree.
|
|
71
|
+
|
|
72
|
+
Opens a cyan-bold ``┌─ TITLE`` header the first time each stage appears and
|
|
73
|
+
prints every subsequent line for that stage as a ``│ `` child — the same
|
|
74
|
+
look as ``dtk run``'s load/detect/alert blocks. ``titles`` maps an engine
|
|
75
|
+
stage name to its display title (unmapped names fall back to ``upper()``);
|
|
76
|
+
``echo`` is injectable so one renderer drives both the CLI (``click.echo``)
|
|
77
|
+
and the ``dtk tune`` server (its own output callback). Build a fresh renderer
|
|
78
|
+
per run so the first stage re-opens its header.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
titles: dict[str, str] | None = None,
|
|
85
|
+
echo: Callable[[str], None] = click.echo,
|
|
86
|
+
) -> None:
|
|
87
|
+
self._open: str | None = None
|
|
88
|
+
self._titles = titles or {}
|
|
89
|
+
self._echo = echo
|
|
90
|
+
|
|
91
|
+
def __call__(self, stage: str, line: str) -> None:
|
|
92
|
+
if self._open != stage:
|
|
93
|
+
title = self._titles.get(stage, stage.upper())
|
|
94
|
+
self._echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
|
|
95
|
+
self._open = stage
|
|
96
|
+
self._echo(f" │ {line}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def echo_noop(name: str, reason: str) -> None:
|
|
100
|
+
"""A metric with nothing to do — a single ``•`` line."""
|
|
101
|
+
click.echo(f" • {name}: {reason}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def echo_error(name: str, message: str) -> None:
|
|
105
|
+
"""A per-metric failure — a red ``✗`` line on stderr."""
|
|
106
|
+
click.echo(click.style(f" ✗ {name}: {message}", fg="red"), err=True)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def echo_done(summary: str) -> None:
|
|
110
|
+
"""The closing ``Done. …`` summary (cyan, bold), preceded by a blank line."""
|
|
111
|
+
click.echo()
|
|
112
|
+
click.echo(click.style(f"Done. {summary}", fg="cyan", bold=True))
|
|
@@ -54,7 +54,9 @@ version — **read the relevant one on demand** instead of guessing:
|
|
|
54
54
|
`dtk autotune`, and explains the chosen, annotated config (an automatic,
|
|
55
55
|
cross-validated search). For **hands-on** tuning instead, `dtk tune` opens a
|
|
56
56
|
browser view where you drag the detector's knobs against the real series and
|
|
57
|
-
write the result back in place
|
|
57
|
+
write the result back in place — and its **Autotune** mode can run that same
|
|
58
|
+
search server-side and re-seed the knobs without leaving the cockpit (see
|
|
59
|
+
`cli.md`).
|
|
58
60
|
- **Hit a detectkit bug, or have feedback** — once you've ruled out a local
|
|
59
61
|
config fix (see the gotchas below), use the **`dtk-feedback`** skill to file a
|
|
60
62
|
redacted bug report, feature request, or comment as a GitHub issue on the
|
|
@@ -17,6 +17,16 @@ same windowed detectors and `detector_id` identity). The fastest path is the
|
|
|
17
17
|
> `metrics/.history/<metric>/` first). Use `autotune` to search automatically and
|
|
18
18
|
> emit a new file; use `tune` to dial a detector in by eye and commit it. See
|
|
19
19
|
> `cli.md`.
|
|
20
|
+
>
|
|
21
|
+
> **You can also run this engine *inside* `dtk tune`** — its **Autotune** mode runs
|
|
22
|
+
> the same search **server-side** (using the incidents you've marked as ground
|
|
23
|
+
> truth), re-seeds the knobs with the winner, and lets you **Apply** in place. That's
|
|
24
|
+
> the same computation as this command, with two differences: it tunes on the
|
|
25
|
+
> **window currently shown** in the cockpit (the **Points shown** trim — the series
|
|
26
|
+
> you see and score) rather than the full capped history, and it edits the metric YAML
|
|
27
|
+
> in place (advisory — no run record / `__tuned_<id>.yml` / persisted detections)
|
|
28
|
+
> rather than emitting a new tuned file. Reach for `dtk autotune` when you want the
|
|
29
|
+
> audited run + tuned file + persisted winner detections.
|
|
20
30
|
|
|
21
31
|
## What it searches
|
|
22
32
|
|
|
@@ -88,7 +88,8 @@ cockpit**: ONE chart (the windshield) fills the view, the live metrics ride
|
|
|
88
88
|
**pinned in a HUD over the chart** (the speedometer), and every control lives in an
|
|
89
89
|
**always-visible, mode-aware side rail** beside the chart (Tune shows the detector
|
|
90
90
|
knobs + effective config + Apply, Review the verdict actions, Label the capture
|
|
91
|
-
tools + incident list + Save
|
|
91
|
+
tools + incident list + Save, Autotune the search button + winning config), while
|
|
92
|
+
the controls that aren't detector-specific —
|
|
92
93
|
the **Points shown** data window, the alert rule (**direction** + **consecutive
|
|
93
94
|
anomalies**) and the **y = 0** toggle — stay visible in every mode. The chart is
|
|
94
95
|
**zoomable** (scroll/drag + navigator strip) with a **"Points shown"** trim slider.
|
|
@@ -103,11 +104,22 @@ and which interactions are armed on the one chart: **Tune** (band leads; inciden
|
|
|
103
104
|
recede to read-only context; hover a point for its window), **Review** (the fired
|
|
104
105
|
alerts lead, band ghosts — click an alert marker to cycle its verdict un-reviewed →
|
|
105
106
|
**valid** (green) → **false alarm** (slate); **Confirm all unreviewed valid** does
|
|
106
|
-
the lot),
|
|
107
|
+
the lot), **Label** (band hides; **mark the real incidents** by drag, **Lasso
|
|
107
108
|
anomalies** — loop a cloud of anomaly dots, each consecutive run, gaps bridged up to
|
|
108
109
|
`consecutive_anomalies`, becomes one span — or **Threshold capture** every span past
|
|
109
110
|
a horizontal line, each widened to a full interval so the alert lands inside; the
|
|
110
|
-
painted window saves as `capture_windows`)
|
|
111
|
+
painted window saves as `capture_windows`), and **Autotune** (**Run autotune**
|
|
112
|
+
launches the **real** `dtk autotune` engine **server-side** over the **window
|
|
113
|
+
currently shown** (the **Points shown** trim — the same series you see and score, not
|
|
114
|
+
the full history), using your marked incidents as ground truth, then **re-seeds every
|
|
115
|
+
knob** with the winner and shows the score + decision log; the band leads like Tune;
|
|
116
|
+
each run also streams a structured `LABELS → … → RESULT` log to the terminal, like
|
|
117
|
+
`dtk run`). The Autotune mode is **advisory** — it computes + re-seeds only and persists nothing (no
|
|
118
|
+
run record / `__tuned_<id>.yml` / detections, so `dtk tune` stays lock-free); review
|
|
119
|
+
the band and **Apply** to write it back. It honours the metric's `autotune:` block,
|
|
120
|
+
is supervised when incidents are marked (also picks `consecutive_anomalies`) else
|
|
121
|
+
unsupervised, and needs the live server (unavailable under `--no-serve`).
|
|
122
|
+
**Confirming an alert valid IS marking an
|
|
111
123
|
incident**: the confirmed streak becomes a first-class **ground-truth incident** that
|
|
112
124
|
shows in the Marked-incidents list (a read-only "✓ confirmed alert" row; its ✕
|
|
113
125
|
un-confirms the alert), counts toward recall + correct, and is written on Save — so a
|
|
@@ -20,7 +20,6 @@ import numpy as np
|
|
|
20
20
|
from detectkit.autotune import (
|
|
21
21
|
AutoTuneError,
|
|
22
22
|
ScoringMetric,
|
|
23
|
-
TuneSettings,
|
|
24
23
|
compute_run_id,
|
|
25
24
|
emit_tuned_config,
|
|
26
25
|
parse_labels_file,
|
|
@@ -31,7 +30,15 @@ from detectkit.autotune.labels import (
|
|
|
31
30
|
IncidentLabels,
|
|
32
31
|
parse_incident_labels,
|
|
33
32
|
)
|
|
34
|
-
from detectkit.
|
|
33
|
+
from detectkit.autotune.runner import build_settings, cap_history, resolve_scoring
|
|
34
|
+
from detectkit.cli._output import (
|
|
35
|
+
AUTOTUNE_STAGE_TITLES,
|
|
36
|
+
StageLogRenderer,
|
|
37
|
+
echo_block,
|
|
38
|
+
echo_done,
|
|
39
|
+
echo_error,
|
|
40
|
+
echo_noop,
|
|
41
|
+
)
|
|
35
42
|
from detectkit.cli.commands.run import find_project_root, parse_date, select_metrics
|
|
36
43
|
from detectkit.config.metric_config import AutoTuneConfig, MetricConfig
|
|
37
44
|
from detectkit.config.profile import ProfilesConfig
|
|
@@ -41,32 +48,6 @@ from detectkit.detectors.base import DetectionResult
|
|
|
41
48
|
from detectkit.detectors.factory import DetectorFactory
|
|
42
49
|
from detectkit.utils.json_utils import json_dumps_sorted
|
|
43
50
|
|
|
44
|
-
# Cap the scored training span when the user doesn't pin max_history, so tuning
|
|
45
|
-
# stays responsive on very long histories (recent data is the most relevant).
|
|
46
|
-
_DEFAULT_TRAIN_CAP = 50_000
|
|
47
|
-
|
|
48
|
-
_STAGE_TITLES = {
|
|
49
|
-
"labels": "LABELS",
|
|
50
|
-
"seasonality": "SEASONALITY",
|
|
51
|
-
"detector_select": "DETECTOR SELECT",
|
|
52
|
-
"grid_search": "GRID SEARCH",
|
|
53
|
-
"window": "WINDOW",
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class _StageRenderer:
|
|
58
|
-
"""Streams engine progress as the run-log tree (header per stage + │ lines)."""
|
|
59
|
-
|
|
60
|
-
def __init__(self) -> None:
|
|
61
|
-
self._open: str | None = None
|
|
62
|
-
|
|
63
|
-
def __call__(self, stage: str, line: str) -> None:
|
|
64
|
-
if self._open != stage:
|
|
65
|
-
title = _STAGE_TITLES.get(stage, stage.upper())
|
|
66
|
-
click.echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
|
|
67
|
-
self._open = stage
|
|
68
|
-
click.echo(f" │ {line}")
|
|
69
|
-
|
|
70
51
|
|
|
71
52
|
def _results_to_batch(results: list[DetectionResult]) -> dict[str, np.ndarray]:
|
|
72
53
|
"""Convert detect() output into the ``save_detections`` batch shape."""
|
|
@@ -88,12 +69,11 @@ def _results_to_batch(results: list[DetectionResult]) -> dict[str, np.ndarray]:
|
|
|
88
69
|
|
|
89
70
|
|
|
90
71
|
def _resolve_scoring(scoring_override: str | None, autotune_cfg: AutoTuneConfig) -> ScoringMetric:
|
|
91
|
-
|
|
72
|
+
"""CLI wrapper over the shared resolver: bad name → a click usage error."""
|
|
92
73
|
try:
|
|
93
|
-
return
|
|
74
|
+
return resolve_scoring(scoring_override, autotune_cfg)
|
|
94
75
|
except ValueError as exc:
|
|
95
|
-
|
|
96
|
-
raise click.BadParameter(f"Invalid scoring metric '{value}'. Allowed: {allowed}") from exc
|
|
76
|
+
raise click.BadParameter(str(exc)) from exc
|
|
97
77
|
|
|
98
78
|
|
|
99
79
|
_LABELS_GLOBS = ("*.yml", "*.yaml", "*.json")
|
|
@@ -216,34 +196,6 @@ def _prompt_labels(*, interval_seconds: int) -> IncidentLabels:
|
|
|
216
196
|
return parse_incident_labels({"incidents": incidents}, interval_seconds=interval_seconds)
|
|
217
197
|
|
|
218
198
|
|
|
219
|
-
def _build_settings(*, scoring: ScoringMetric, autotune_cfg: AutoTuneConfig) -> TuneSettings:
|
|
220
|
-
return TuneSettings(
|
|
221
|
-
metric=scoring,
|
|
222
|
-
beta=autotune_cfg.beta,
|
|
223
|
-
fold_count=autotune_cfg.folds,
|
|
224
|
-
stability_lambda=autotune_cfg.stability_lambda,
|
|
225
|
-
allowed_detector_types=autotune_cfg.detector_types,
|
|
226
|
-
allowed_seasonality=autotune_cfg.seasonality_candidates,
|
|
227
|
-
force_seasonality=autotune_cfg.force_seasonality,
|
|
228
|
-
fixed_params=dict(autotune_cfg.fixed_params),
|
|
229
|
-
max_history=autotune_cfg.max_history,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _cap_history(data: dict[str, np.ndarray], max_history: int | None) -> dict[str, np.ndarray]:
|
|
234
|
-
"""Keep the most recent ``max_history`` (or default-cap) points."""
|
|
235
|
-
n = len(data["timestamp"])
|
|
236
|
-
cap = max_history if max_history is not None else _DEFAULT_TRAIN_CAP
|
|
237
|
-
if n <= cap:
|
|
238
|
-
return data
|
|
239
|
-
return {
|
|
240
|
-
"timestamp": data["timestamp"][-cap:],
|
|
241
|
-
"value": data["value"][-cap:],
|
|
242
|
-
"seasonality_data": data["seasonality_data"][-cap:],
|
|
243
|
-
"seasonality_columns": data["seasonality_columns"],
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
199
|
def _load_project(
|
|
248
200
|
profile: str | None,
|
|
249
201
|
) -> tuple[Path, ProjectConfig, InternalTablesManager, Any] | None:
|
|
@@ -368,7 +320,7 @@ def _tune_one(
|
|
|
368
320
|
return False
|
|
369
321
|
|
|
370
322
|
try:
|
|
371
|
-
data =
|
|
323
|
+
data = cap_history(data, autotune_cfg.max_history)
|
|
372
324
|
click.echo(
|
|
373
325
|
f" Training span: {len(data['timestamp']):,} points " f"(interval {interval_seconds}s)"
|
|
374
326
|
)
|
|
@@ -384,14 +336,14 @@ def _tune_one(
|
|
|
384
336
|
ground_truth = labels.to_ground_truth(data["timestamp"], interval_seconds)
|
|
385
337
|
click.echo(f" Labels: {label_source}")
|
|
386
338
|
|
|
387
|
-
settings =
|
|
339
|
+
settings = build_settings(scoring=scoring, autotune_cfg=autotune_cfg)
|
|
388
340
|
result = run_autotune_engine(
|
|
389
341
|
metric_name=name,
|
|
390
342
|
data=data,
|
|
391
343
|
ground_truth=ground_truth,
|
|
392
344
|
interval_seconds=interval_seconds,
|
|
393
345
|
settings=settings,
|
|
394
|
-
on_stage=
|
|
346
|
+
on_stage=StageLogRenderer(titles=AUTOTUNE_STAGE_TITLES),
|
|
395
347
|
)
|
|
396
348
|
|
|
397
349
|
run_id = compute_run_id(result)
|
|
@@ -467,9 +419,7 @@ def _finalize(
|
|
|
467
419
|
|
|
468
420
|
if dry_run:
|
|
469
421
|
children.append("dry-run: no config written, no detections persisted")
|
|
470
|
-
|
|
471
|
-
for i, child in enumerate(children):
|
|
472
|
-
click.echo(f" {'└─' if i == len(children) - 1 else '│ '} {child}")
|
|
422
|
+
echo_block("RESULT", children)
|
|
473
423
|
return
|
|
474
424
|
|
|
475
425
|
# Persist the run record (the audit trail).
|
|
@@ -551,9 +501,7 @@ def _finalize(
|
|
|
551
501
|
f"persisted winner, pruned {pruned} superseded run(s)"
|
|
552
502
|
)
|
|
553
503
|
children.append(f"Re-run with: dtk run --select {name}__tuned_{run_id}")
|
|
554
|
-
|
|
555
|
-
for i, child in enumerate(children):
|
|
556
|
-
click.echo(f" {'└─' if i == len(children) - 1 else '│ '} {child}")
|
|
504
|
+
echo_block("RESULT", children)
|
|
557
505
|
|
|
558
506
|
|
|
559
507
|
def _prune_prior_winners(
|
|
@@ -143,6 +143,10 @@ def run_tune(
|
|
|
143
143
|
metric_name=name,
|
|
144
144
|
incidents_dir=incidents_dir,
|
|
145
145
|
interval_seconds=interval_seconds,
|
|
146
|
+
# Enable the server-side Autotune mode: the config (autotune block, interval)
|
|
147
|
+
# + a DB handle to reload the metric's history for the engine.
|
|
148
|
+
metric_config=config,
|
|
149
|
+
internal_manager=internal_manager,
|
|
146
150
|
)
|
|
147
151
|
if applied is None:
|
|
148
152
|
echo_noop(name, "tuning cancelled — metric unchanged")
|