detectkit 0.46.0__tar.gz → 0.47.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.46.0/detectkit.egg-info → detectkit-0.47.0}/PKG-INFO +5 -1
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/__init__.py +1 -1
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/base.py +17 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_base.py +5 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_decision.py +2 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_recovery.py +1 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/alerting.md +1 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/cli.md +40 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/metrics.md +16 -0
- detectkit-0.47.0/detectkit/cli/commands/osi.py +245 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/main.py +8 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/metric_config.py +76 -1
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_alert_step.py +1 -0
- detectkit-0.47.0/detectkit/semantic/__init__.py +41 -0
- detectkit-0.47.0/detectkit/semantic/errors.py +31 -0
- detectkit-0.47.0/detectkit/semantic/exporter.py +102 -0
- detectkit-0.47.0/detectkit/semantic/importer.py +281 -0
- detectkit-0.47.0/detectkit/semantic/osi_model.py +221 -0
- detectkit-0.47.0/detectkit/semantic/query_gen.py +229 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/payload.py +20 -0
- {detectkit-0.46.0 → detectkit-0.47.0/detectkit.egg-info}/PKG-INFO +5 -1
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/SOURCES.txt +7 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/requires.txt +5 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/pyproject.toml +7 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/LICENSE +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/MANIFEST.in +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/README.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/core/models.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/requirements.txt +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/setup.cfg +0 -0
- {detectkit-0.46.0 → detectkit-0.47.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detectkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.47.0
|
|
4
4
|
Summary: Metric monitoring with automatic anomaly detection
|
|
5
5
|
Author: detectkit team
|
|
6
6
|
License: MIT
|
|
@@ -47,12 +47,15 @@ Requires-Dist: timesfm>=0.1.0; extra == "timesfm"
|
|
|
47
47
|
Provides-Extra: advanced-detectors
|
|
48
48
|
Requires-Dist: prophet>=1.1.0; extra == "advanced-detectors"
|
|
49
49
|
Requires-Dist: timesfm>=0.1.0; extra == "advanced-detectors"
|
|
50
|
+
Provides-Extra: osi
|
|
51
|
+
Requires-Dist: sqlglot>=20.0; extra == "osi"
|
|
50
52
|
Provides-Extra: all
|
|
51
53
|
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
|
|
52
54
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
53
55
|
Requires-Dist: pymysql>=1.0.0; extra == "all"
|
|
54
56
|
Requires-Dist: prophet>=1.1.0; extra == "all"
|
|
55
57
|
Requires-Dist: timesfm>=0.1.0; extra == "all"
|
|
58
|
+
Requires-Dist: sqlglot>=20.0; extra == "all"
|
|
56
59
|
Provides-Extra: dev
|
|
57
60
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
58
61
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -61,6 +64,7 @@ Requires-Dist: requests-mock>=1.12; extra == "dev"
|
|
|
61
64
|
Requires-Dist: black>=23.0; extra == "dev"
|
|
62
65
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
63
66
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
67
|
+
Requires-Dist: sqlglot>=20.0; extra == "dev"
|
|
64
68
|
Provides-Extra: integration
|
|
65
69
|
Requires-Dist: testcontainers[clickhouse,mysql,postgres]>=4.0; extra == "integration"
|
|
66
70
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "integration"
|
|
@@ -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.47.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -72,6 +72,13 @@ class AlertData:
|
|
|
72
72
|
error_message: str | None = None
|
|
73
73
|
description: str | None = None
|
|
74
74
|
mentions: list[str] = field(default_factory=list)
|
|
75
|
+
# OSI-compatible AI grounding: the metric's alternative names. Exposed to
|
|
76
|
+
# message templates as the OPT-IN ``{synonyms}`` / ``{synonyms_line}``
|
|
77
|
+
# variables (via :meth:`build_context`) so a custom template can surface an
|
|
78
|
+
# "Also known as: …" line — but the DEFAULT rendering deliberately does NOT
|
|
79
|
+
# show it, so existing alerts are byte-identical. Stamped by the orchestrator
|
|
80
|
+
# from ``MetricConfig.ai_context.synonyms``; defaults to empty.
|
|
81
|
+
ai_synonyms: list[str] = field(default_factory=list)
|
|
75
82
|
project_name: str | None = None
|
|
76
83
|
# Optional actionable links surfaced in the message. ``dashboard_url`` is the
|
|
77
84
|
# headline link (rendered natively as a clickable title/link on
|
|
@@ -416,6 +423,14 @@ class BaseAlertChannel(ABC):
|
|
|
416
423
|
# Format description line (empty string if no description)
|
|
417
424
|
description_line = f"{alert_data.description}\n" if alert_data.description else ""
|
|
418
425
|
|
|
426
|
+
# OSI ai_context synonyms — the metric's alternative names. Provided as
|
|
427
|
+
# OPT-IN template variables only (``{synonyms}`` / ``{synonyms_line}``);
|
|
428
|
+
# the default templates do NOT reference them, so a metric without
|
|
429
|
+
# ai_context — and every existing alert — renders unchanged. A custom
|
|
430
|
+
# template can drop ``{synonyms_line}`` in to show "Also known as: …".
|
|
431
|
+
synonyms = ", ".join(alert_data.ai_synonyms) if alert_data.ai_synonyms else ""
|
|
432
|
+
synonyms_line = f"Also known as: {synonyms}\n" if synonyms else ""
|
|
433
|
+
|
|
419
434
|
# Format mentions
|
|
420
435
|
mentions_str = self.format_mentions(alert_data.mentions)
|
|
421
436
|
mentions_line = f"\n{mentions_str}" if mentions_str else ""
|
|
@@ -482,6 +497,8 @@ class BaseAlertChannel(ABC):
|
|
|
482
497
|
"error_message": alert_data.error_message or "",
|
|
483
498
|
"description": alert_data.description or "",
|
|
484
499
|
"description_line": description_line,
|
|
500
|
+
"synonyms": synonyms,
|
|
501
|
+
"synonyms_line": synonyms_line,
|
|
485
502
|
"dashboard_url": dashboard_url,
|
|
486
503
|
"dashboard_line": dashboard_line,
|
|
487
504
|
"help_url": help_url,
|
|
@@ -36,6 +36,7 @@ class _OrchestratorBase:
|
|
|
36
36
|
links: dict[str, str] | None = None,
|
|
37
37
|
project_name: str | None = None,
|
|
38
38
|
help_url: str | None = None,
|
|
39
|
+
ai_synonyms: list[str] | None = None,
|
|
39
40
|
):
|
|
40
41
|
self.metric_name = metric_name
|
|
41
42
|
self.interval = interval
|
|
@@ -57,6 +58,10 @@ class _OrchestratorBase:
|
|
|
57
58
|
# defaulting to the official docs; None when opted out). Stamped onto every
|
|
58
59
|
# AlertData so channels render a guide link for non-operator stakeholders.
|
|
59
60
|
self.help_url = help_url
|
|
61
|
+
# OSI ai_context synonyms (the metric's alternative names). Stamped onto
|
|
62
|
+
# every AlertData so channels can render an "Also known as" identity line.
|
|
63
|
+
# Empty list when the metric has no ai_context — renders unchanged.
|
|
64
|
+
self.ai_synonyms = ai_synonyms or []
|
|
60
65
|
|
|
61
66
|
@staticmethod
|
|
62
67
|
def _group_by_timestamp(
|
|
@@ -280,6 +280,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
280
280
|
consecutive_count=consecutive_count,
|
|
281
281
|
description=self.description,
|
|
282
282
|
mentions=self.mentions,
|
|
283
|
+
ai_synonyms=self.ai_synonyms,
|
|
283
284
|
dashboard_url=self.dashboard_url,
|
|
284
285
|
links=self.links,
|
|
285
286
|
project_name=self.project_name,
|
|
@@ -345,6 +346,7 @@ class _DecisionMixin(_OrchestratorBase):
|
|
|
345
346
|
is_no_data=True,
|
|
346
347
|
description=self.description,
|
|
347
348
|
mentions=self.mentions,
|
|
349
|
+
ai_synonyms=self.ai_synonyms,
|
|
348
350
|
dashboard_url=self.dashboard_url,
|
|
349
351
|
links=self.links,
|
|
350
352
|
project_name=self.project_name,
|
|
@@ -188,6 +188,7 @@ class _RecoveryMixin(_OrchestratorBase):
|
|
|
188
188
|
is_recovery=True,
|
|
189
189
|
description=self.description,
|
|
190
190
|
mentions=self.mentions,
|
|
191
|
+
ai_synonyms=self.ai_synonyms,
|
|
191
192
|
dashboard_url=self.dashboard_url,
|
|
192
193
|
links=self.links,
|
|
193
194
|
project_name=self.project_name,
|
|
@@ -40,6 +40,7 @@ version — **read the relevant one on demand** instead of guessing:
|
|
|
40
40
|
| Alert rules (quorum/direction/consecutive), cooldown, recovery, templates | `.claude/rules/detectkit/alerting.md` |
|
|
41
41
|
| Auto-tuning a detector (`dtk autotune`), labels file, the `autotune:` block, `_dtk_autotune_runs` | `.claude/rules/detectkit/autotune.md` |
|
|
42
42
|
| Manually/interactively tuning a detector in the browser and writing it back in place (`dtk tune`) | `.claude/rules/detectkit/cli.md` |
|
|
43
|
+
| OSI (Open Semantic Interchange) interop: import an OSI metric into a native metric / export back (`dtk osi`) | `.claude/rules/detectkit/cli.md` |
|
|
43
44
|
|
|
44
45
|
### Skills
|
|
45
46
|
|
|
@@ -297,6 +297,7 @@ referenced by path). Key variables:
|
|
|
297
297
|
| Variable | Meaning |
|
|
298
298
|
|---|---|
|
|
299
299
|
| `{metric_name}`, `{description}` / `{description_line}` | identity |
|
|
300
|
+
| `{synonyms}` / `{synonyms_line}` | alt names from `ai_context.synonyms` (opt-in; not in default templates — add `{synonyms_line}` for an "Also known as: …" line) |
|
|
300
301
|
| `{project_name}` / `{project_name_prefix}` | project label (`"[name] "` prefix, or `""`) |
|
|
301
302
|
| `{timestamp}`, `{timezone}` | when (display tz via `alerting.timezone`, default UTC) |
|
|
302
303
|
| `{value}` / `{value_display}` | metric value (`value_display` is NaN-safe) |
|
|
@@ -15,6 +15,8 @@ Run all commands from a project directory (the one containing
|
|
|
15
15
|
| `dtk test-alert <metric>` | Send a mock alert to the metric's channels |
|
|
16
16
|
| `dtk unlock --select <sel>` | Clear a stuck pipeline lock |
|
|
17
17
|
| `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
|
|
18
|
+
| `dtk osi import <model>` | Scaffold a native metric from an OSI semantic-model metric (see "OSI interop") |
|
|
19
|
+
| `dtk osi export` | Publish metrics into an OSI fragment (config in `custom_extensions[detectkit]`) |
|
|
18
20
|
| `dtk --version` | Show installed detectkit version |
|
|
19
21
|
|
|
20
22
|
## Selectors (`--select` / `-s`)
|
|
@@ -193,6 +195,44 @@ actually delete.
|
|
|
193
195
|
unless `-y/--yes`; refuses to run if the project defines no metrics or configs
|
|
194
196
|
fail to parse (so a wrong directory can't wipe valid data).
|
|
195
197
|
|
|
198
|
+
## OSI interop (`dtk osi`)
|
|
199
|
+
|
|
200
|
+
`dtk osi` converts between [Open Semantic Interchange](https://github.com/open-semantic-interchange/OSI)
|
|
201
|
+
(OSI) semantic models and native detectkit metrics. It's a **separate, additive**
|
|
202
|
+
layer: it never runs the pipeline, takes no lock, and the converter package
|
|
203
|
+
(`detectkit/semantic/`) is not imported by load/detect/alert — so it can't affect
|
|
204
|
+
a running project. OSI is treated as an *interchange* format (define the KPI once,
|
|
205
|
+
consume in BI + AI), not an execution engine. OSI adoption is still early, so the
|
|
206
|
+
broadly-useful piece today is `ai_context` (see the metrics rule) — the converters
|
|
207
|
+
are a forward bridge for teams that already run a governed semantic layer.
|
|
208
|
+
|
|
209
|
+
- `dtk osi import <model.osi.yml> --metric <name> --interval <grain>` — the
|
|
210
|
+
"enhanced init": resolve one OSI metric and **scaffold a normal native metric**
|
|
211
|
+
(SQL `query`, interval, a starter detector, the metric's `ai_context`). Review
|
|
212
|
+
it, then commit like any hand-written metric. Targets:
|
|
213
|
+
- `--target clickhouse` (default) — a direct `toStartOfInterval(...) GROUP BY`
|
|
214
|
+
query from the dataset's physical `source` (ANSI→ClickHouse via **sqlglot**,
|
|
215
|
+
the optional `[osi]` extra: `pip install 'detectkit[osi]'`).
|
|
216
|
+
- `--target cube --cube <name> --time-field <dim>` — a Cube **SQL-API**
|
|
217
|
+
`MEASURE(...)` query, so the metric runs through Cube and the alert matches a
|
|
218
|
+
Cube dashboard's number by construction. Point its `profile` at a Postgres
|
|
219
|
+
connection on Cube's SQL port.
|
|
220
|
+
- Only provably per-bucket-additive measures compile (`SUM`/`COUNT`/`COUNT(DISTINCT)`/
|
|
221
|
+
`AVG`/`MIN`/`MAX` + ratios like `SUM(x)/NULLIF(COUNT(DISTINCT y),0)`). Window
|
|
222
|
+
functions / non-aggregates / unknown aggregates are **refused** with a message
|
|
223
|
+
to use `query_file:` — never a plausible-but-wrong series.
|
|
224
|
+
- `dtk osi compile <model> --metric <name> --interval <g>` prints just the SQL
|
|
225
|
+
(for review). `dtk osi export [--select <sel>]` writes an OSI fragment carrying
|
|
226
|
+
a lossless snapshot of the config in `custom_extensions[detectkit]` + the
|
|
227
|
+
`ai_context` — a **one-way carrier** (`import` does not reconstruct from it; the
|
|
228
|
+
metric YAML stays the source of truth).
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
dtk osi compile model.osi.yml -m total_sales -i 1h # preview the SQL
|
|
232
|
+
dtk osi import model.osi.yml -m total_sales -i 1h -o metrics/ # scaffold a metric
|
|
233
|
+
dtk osi export -o semantic/detectkit.osi.yml # publish back to OSI
|
|
234
|
+
```
|
|
235
|
+
|
|
196
236
|
## Common workflows
|
|
197
237
|
|
|
198
238
|
```bash
|
|
@@ -13,6 +13,9 @@ description: API p95 latency # optional, shown in alerts
|
|
|
13
13
|
profile: prod # optional, overrides project default_profile
|
|
14
14
|
enabled: true # optional, false → skipped by `dtk run`
|
|
15
15
|
tags: [critical, api] # optional, used by `--select tag:<t>`
|
|
16
|
+
ai_context: # optional — OSI-compatible grounding (descriptive only)
|
|
17
|
+
instructions: "p95 API latency, ms; user-facing"
|
|
18
|
+
synonyms: ["api latency", "response time"]
|
|
16
19
|
|
|
17
20
|
interval: 5min # required — point spacing on the time grid
|
|
18
21
|
|
|
@@ -63,6 +66,19 @@ false_alert_budget: 0.3 # optional — `dtk tune` target false-alert rate
|
|
|
63
66
|
project-wide default; unset → project, then a built-in `0.5`. Tuning-only — it never
|
|
64
67
|
affects the load/detect/alert pipeline.
|
|
65
68
|
|
|
69
|
+
`ai_context` is **OSI-compatible grounding** you can add to any metric with **no
|
|
70
|
+
OSI model needed** — the metric's business meaning (`instructions`), alternative
|
|
71
|
+
names (`synonyms`) and example values (`examples`), mirroring the
|
|
72
|
+
[Open Semantic Interchange](https://github.com/open-semantic-interchange/OSI)
|
|
73
|
+
`ai_context` shape so a metric's meaning is portable to/from an OSI semantic model.
|
|
74
|
+
Accepts a bare string (→ `instructions`) or the full mapping. It is **purely
|
|
75
|
+
descriptive**: it never affects load/detect/alert or the detector id, and it does
|
|
76
|
+
**not** change any default-rendered alert. The `synonyms` are exposed to alert
|
|
77
|
+
templates as the **opt-in** `{synonyms}` / `{synonyms_line}` variables (a custom
|
|
78
|
+
`template` can add an "Also known as: …" line), and the whole block is carried in
|
|
79
|
+
the `dtk tune` cockpit payload as read-only grounding. Omit it and everything
|
|
80
|
+
renders as before.
|
|
81
|
+
|
|
66
82
|
## `interval` (required)
|
|
67
83
|
|
|
68
84
|
Point spacing. String (`"30s"`, `"1min"`, `"5min"`, `"10min"`, `"1hour"`,
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""``dtk osi`` — Open Semantic Interchange interop (import / export / compile).
|
|
2
|
+
|
|
3
|
+
A self-contained, additive command group. It never runs the pipeline, takes no
|
|
4
|
+
lock, and touches no internal table:
|
|
5
|
+
|
|
6
|
+
- ``dtk osi import`` scaffolds a native detectkit metric from an OSI model metric
|
|
7
|
+
(review the output, then commit it like any hand-written metric).
|
|
8
|
+
- ``dtk osi export`` publishes detectkit metrics into an OSI fragment (a one-way
|
|
9
|
+
carrier: the full config rides in ``custom_extensions[detectkit]``; ``import``
|
|
10
|
+
does not reconstruct from it).
|
|
11
|
+
- ``dtk osi compile`` prints just the SQL an import would generate (for review).
|
|
12
|
+
|
|
13
|
+
Needs the optional ``[osi]`` extra (sqlglot) only for the ClickHouse target.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group()
|
|
27
|
+
def osi() -> None:
|
|
28
|
+
"""Open Semantic Interchange (OSI) interop: import / export / compile metrics."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _split_csv(value: str | None) -> list[str] | None:
|
|
32
|
+
if not value:
|
|
33
|
+
return None
|
|
34
|
+
items = [v.strip() for v in value.split(",") if v.strip()]
|
|
35
|
+
return items or None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _dump_metric_yaml(body: dict[str, Any], header: str) -> str:
|
|
39
|
+
"""Render a metric body as YAML with a ``#`` provenance header (mirrors config_emitter)."""
|
|
40
|
+
text = yaml.safe_dump(body, sort_keys=False, allow_unicode=True, width=100)
|
|
41
|
+
return header + text
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _import_header(result: Any, model_file: Path) -> str:
|
|
45
|
+
lines = [
|
|
46
|
+
"# Generated by `dtk osi import` — review before committing.",
|
|
47
|
+
f"# Source OSI model: {model_file}",
|
|
48
|
+
f"# Metric: {result.metric_name} · target: {result.target} · "
|
|
49
|
+
f"sql-fingerprint: {result.fingerprint}",
|
|
50
|
+
"# detectkit resumes from the last datapoint, so if you re-import and this",
|
|
51
|
+
"# fingerprint changes, backfill/clean to avoid mixing definitions.",
|
|
52
|
+
]
|
|
53
|
+
for w in result.warnings:
|
|
54
|
+
lines.append(f"# NOTE: {w}")
|
|
55
|
+
return "\n".join(lines) + "\n"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@osi.command(name="import")
|
|
59
|
+
@click.argument("model_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
60
|
+
@click.option("--metric", "-m", required=True, help="OSI metric name to import")
|
|
61
|
+
@click.option("--interval", "-i", required=True, help="Series grain (e.g. 1h, 10min, 3600)")
|
|
62
|
+
@click.option(
|
|
63
|
+
"--target",
|
|
64
|
+
type=click.Choice(["clickhouse", "cube"]),
|
|
65
|
+
default="clickhouse",
|
|
66
|
+
show_default=True,
|
|
67
|
+
help="clickhouse = direct SQL from dataset.source; cube = Cube SQL-API (MEASURE)",
|
|
68
|
+
)
|
|
69
|
+
@click.option("--dataset", help="OSI dataset to build FROM (default: the model's only dataset)")
|
|
70
|
+
@click.option("--time-field", help="Time grain column / dimension (default: a field with is_time)")
|
|
71
|
+
@click.option("--where", help="Extra SQL predicate (ANSI) added to the WHERE clause")
|
|
72
|
+
@click.option("--cube", help="Cube name (cube target)")
|
|
73
|
+
@click.option("--cube-measure", help="Cube measure name (cube target; default: the metric name)")
|
|
74
|
+
@click.option("--time-dimension", help="Cube time dimension (cube target)")
|
|
75
|
+
@click.option("--seasonality", help="Comma-separated time-derived seasonality columns")
|
|
76
|
+
@click.option("--detector", default="mad", show_default=True, help="Starter detector type")
|
|
77
|
+
@click.option(
|
|
78
|
+
"--out",
|
|
79
|
+
"-o",
|
|
80
|
+
type=click.Path(path_type=Path),
|
|
81
|
+
help="Write the metric YAML here (a dir → <name>.yml). Default: print to stdout.",
|
|
82
|
+
)
|
|
83
|
+
@click.option("--force", is_flag=True, help="Overwrite an existing --out file")
|
|
84
|
+
def import_(
|
|
85
|
+
model_file: Path,
|
|
86
|
+
metric: str,
|
|
87
|
+
interval: str,
|
|
88
|
+
target: str,
|
|
89
|
+
dataset: str | None,
|
|
90
|
+
time_field: str | None,
|
|
91
|
+
where: str | None,
|
|
92
|
+
cube: str | None,
|
|
93
|
+
cube_measure: str | None,
|
|
94
|
+
time_dimension: str | None,
|
|
95
|
+
seasonality: str | None,
|
|
96
|
+
detector: str,
|
|
97
|
+
out: Path | None,
|
|
98
|
+
force: bool,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Scaffold a native detectkit metric from an OSI MODEL_FILE metric."""
|
|
101
|
+
from detectkit.semantic import OsiError, import_osi_metric, load_osi_models
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
models = load_osi_models(model_file)
|
|
105
|
+
result = import_osi_metric(
|
|
106
|
+
models=models,
|
|
107
|
+
metric_name=metric,
|
|
108
|
+
interval=interval,
|
|
109
|
+
target=target,
|
|
110
|
+
dataset=dataset,
|
|
111
|
+
time_field=time_field,
|
|
112
|
+
where=where,
|
|
113
|
+
cube=cube,
|
|
114
|
+
cube_measure=cube_measure,
|
|
115
|
+
time_dimension=time_dimension,
|
|
116
|
+
seasonality=_split_csv(seasonality),
|
|
117
|
+
detector_type=detector,
|
|
118
|
+
)
|
|
119
|
+
except OsiError as exc:
|
|
120
|
+
raise click.ClickException(str(exc)) from exc
|
|
121
|
+
|
|
122
|
+
rendered = _dump_metric_yaml(result.metric, _import_header(result, model_file))
|
|
123
|
+
|
|
124
|
+
if out is None:
|
|
125
|
+
click.echo(rendered)
|
|
126
|
+
click.echo("# (printed only — pass --out to write a metric file)", err=True)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
dest = out / f"{result.metric_name}.yml" if out.is_dir() else out
|
|
130
|
+
if dest.exists() and not force:
|
|
131
|
+
raise click.ClickException(f"{dest} exists — pass --force to overwrite")
|
|
132
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
dest.write_text(rendered)
|
|
134
|
+
click.echo(f"Wrote {dest}")
|
|
135
|
+
for w in result.warnings:
|
|
136
|
+
click.echo(f" note: {w}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@osi.command()
|
|
140
|
+
@click.argument("model_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
141
|
+
@click.option("--metric", "-m", required=True, help="OSI metric name")
|
|
142
|
+
@click.option("--interval", "-i", required=True, help="Series grain (e.g. 1h, 10min, 3600)")
|
|
143
|
+
@click.option("--target", type=click.Choice(["clickhouse", "cube"]), default="clickhouse")
|
|
144
|
+
@click.option("--dataset", help="OSI dataset to build FROM")
|
|
145
|
+
@click.option("--time-field", help="Time grain column / dimension")
|
|
146
|
+
@click.option("--where", help="Extra SQL predicate (ANSI)")
|
|
147
|
+
@click.option("--cube", help="Cube name (cube target)")
|
|
148
|
+
@click.option("--cube-measure", help="Cube measure name (cube target)")
|
|
149
|
+
@click.option("--time-dimension", help="Cube time dimension (cube target)")
|
|
150
|
+
def compile( # noqa: A001 - mirrors the `dtk osi compile` verb
|
|
151
|
+
model_file: Path,
|
|
152
|
+
metric: str,
|
|
153
|
+
interval: str,
|
|
154
|
+
target: str,
|
|
155
|
+
dataset: str | None,
|
|
156
|
+
time_field: str | None,
|
|
157
|
+
where: str | None,
|
|
158
|
+
cube: str | None,
|
|
159
|
+
cube_measure: str | None,
|
|
160
|
+
time_dimension: str | None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Print ONLY the SQL an import would generate (no file written)."""
|
|
163
|
+
from detectkit.semantic import OsiError, import_osi_metric, load_osi_models
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
models = load_osi_models(model_file)
|
|
167
|
+
result = import_osi_metric(
|
|
168
|
+
models=models,
|
|
169
|
+
metric_name=metric,
|
|
170
|
+
interval=interval,
|
|
171
|
+
target=target,
|
|
172
|
+
dataset=dataset,
|
|
173
|
+
time_field=time_field,
|
|
174
|
+
where=where,
|
|
175
|
+
cube=cube,
|
|
176
|
+
cube_measure=cube_measure,
|
|
177
|
+
time_dimension=time_dimension,
|
|
178
|
+
)
|
|
179
|
+
except OsiError as exc:
|
|
180
|
+
raise click.ClickException(str(exc)) from exc
|
|
181
|
+
click.echo(result.sql)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@osi.command()
|
|
185
|
+
@click.option(
|
|
186
|
+
"--select", "-s", "selector", help="Metric selector (name / path / tag:<t>); default: all"
|
|
187
|
+
)
|
|
188
|
+
@click.option("--model-name", default="detectkit_export", show_default=True, help="OSI model name")
|
|
189
|
+
@click.option(
|
|
190
|
+
"--out",
|
|
191
|
+
"-o",
|
|
192
|
+
type=click.Path(path_type=Path),
|
|
193
|
+
help="Write the OSI YAML here. Default: print to stdout.",
|
|
194
|
+
)
|
|
195
|
+
@click.option("--force", is_flag=True, help="Overwrite an existing --out file")
|
|
196
|
+
def export(selector: str | None, model_name: str, out: Path | None, force: bool) -> None:
|
|
197
|
+
"""Export native detectkit metrics into an OSI fragment (config in custom_extensions)."""
|
|
198
|
+
from detectkit.cli.commands.run import find_project_root, select_metrics
|
|
199
|
+
from detectkit.config.validator import validate_project_metrics
|
|
200
|
+
from detectkit.semantic import export_models
|
|
201
|
+
|
|
202
|
+
root = find_project_root()
|
|
203
|
+
if root is None:
|
|
204
|
+
raise click.ClickException(
|
|
205
|
+
"not inside a detectkit project (no detectkit_project.yml found)"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
pairs = select_metrics(selector, root) if selector else validate_project_metrics(root)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
raise click.ClickException(str(exc)) from exc
|
|
212
|
+
configs = [cfg for _, cfg in pairs]
|
|
213
|
+
if not configs:
|
|
214
|
+
raise click.ClickException("no metrics selected")
|
|
215
|
+
|
|
216
|
+
doc = export_models(configs, model_name=model_name)
|
|
217
|
+
rendered = yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100)
|
|
218
|
+
header = (
|
|
219
|
+
f"# Generated by `dtk osi export` — {len(configs)} metric(s).\n"
|
|
220
|
+
"# The portable measure is a placeholder; the exact detectkit definition\n"
|
|
221
|
+
"# (detectors, alerting, query) rides in custom_extensions[detectkit].\n"
|
|
222
|
+
"# One-way carrier: `dtk osi import` does not reconstruct from this block —\n"
|
|
223
|
+
"# keep your metric YAML as the source of truth.\n"
|
|
224
|
+
)
|
|
225
|
+
rendered = header + rendered
|
|
226
|
+
|
|
227
|
+
if out is None:
|
|
228
|
+
click.echo(rendered)
|
|
229
|
+
return
|
|
230
|
+
if out.exists() and not force:
|
|
231
|
+
raise click.ClickException(f"{out} exists — pass --force to overwrite")
|
|
232
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
out.write_text(rendered)
|
|
234
|
+
click.echo(f"Wrote {out} ({len(configs)} metric(s))", err=False)
|
|
235
|
+
click.echo(
|
|
236
|
+
" note: one-way carrier — `dtk osi import` does not reconstruct from "
|
|
237
|
+
"custom_extensions[detectkit]; keep your metric YAML as the source of truth.",
|
|
238
|
+
err=True,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def run_osi() -> None:
|
|
243
|
+
"""Entry shim (kept symmetric with other commands; the group is wired in main)."""
|
|
244
|
+
osi(standalone_mode=True) # pragma: no cover
|
|
245
|
+
sys.exit(0) # pragma: no cover
|
|
@@ -515,5 +515,13 @@ def clean(select: str, orphaned_metrics: bool, execute: bool, yes: bool, profile
|
|
|
515
515
|
)
|
|
516
516
|
|
|
517
517
|
|
|
518
|
+
# OSI (Open Semantic Interchange) interop — a self-contained command group
|
|
519
|
+
# (`dtk osi import` / `export` / `compile`). Isolated from the load/detect/alert
|
|
520
|
+
# pipeline; its module only imports click/yaml at load time (sqlglot stays lazy).
|
|
521
|
+
from detectkit.cli.commands.osi import osi as _osi_group # noqa: E402
|
|
522
|
+
|
|
523
|
+
cli.add_command(_osi_group)
|
|
524
|
+
|
|
525
|
+
|
|
518
526
|
if __name__ == "__main__":
|
|
519
527
|
cli()
|
|
@@ -507,6 +507,59 @@ class AutoTuneConfig(BaseModel):
|
|
|
507
507
|
return self
|
|
508
508
|
|
|
509
509
|
|
|
510
|
+
class AiContextConfig(BaseModel):
|
|
511
|
+
"""OSI-compatible AI grounding for a metric (purely descriptive).
|
|
512
|
+
|
|
513
|
+
Mirrors the OSI ``ai_context`` shape (``instructions`` / ``synonyms`` /
|
|
514
|
+
``examples``) verbatim, so a metric's business meaning, alternative names and
|
|
515
|
+
example values are portable to and from an OSI semantic model (Open Semantic
|
|
516
|
+
Interchange). It is surfaced to humans and agents — synonyms ride into alerts
|
|
517
|
+
as an "Also known as" line, the whole block is baked into the ``dtk tune``
|
|
518
|
+
cockpit payload, and it is shipped to the assistant via ``dtk init-claude`` —
|
|
519
|
+
but it **never** affects the load/detect/alert pipeline or the detector id. A
|
|
520
|
+
metric with no ``ai_context`` behaves exactly as before.
|
|
521
|
+
|
|
522
|
+
Accepts either the full mapping or a bare string (lifted to ``instructions``):
|
|
523
|
+
|
|
524
|
+
```yaml
|
|
525
|
+
ai_context: "Revenue recognized at order completion, net of refunds."
|
|
526
|
+
# or
|
|
527
|
+
ai_context:
|
|
528
|
+
instructions: "Revenue recognized at order completion, net of refunds."
|
|
529
|
+
synonyms: ["total revenue", "gross sales"]
|
|
530
|
+
examples: ["12030.50", "9821.00"]
|
|
531
|
+
```
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
instructions: str | None = Field(
|
|
535
|
+
default=None,
|
|
536
|
+
description="Business meaning / how to interpret the metric (grounds humans + LLMs)",
|
|
537
|
+
)
|
|
538
|
+
synonyms: list[str] = Field(
|
|
539
|
+
default_factory=list,
|
|
540
|
+
description="Alternative names for the metric (grounding + the alert 'Also known as' line)",
|
|
541
|
+
)
|
|
542
|
+
examples: list[str] = Field(
|
|
543
|
+
default_factory=list,
|
|
544
|
+
description="Illustrative example values or phrasings (grounding only)",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
@field_validator("synonyms", "examples")
|
|
548
|
+
@classmethod
|
|
549
|
+
def _clean_str_list(cls, v: list[str]) -> list[str]:
|
|
550
|
+
"""Drop blank/whitespace-only entries (trimming each), preserving order and
|
|
551
|
+
de-duplicating, so a stray empty YAML entry can't leak a bare comma into the
|
|
552
|
+
rendered "Also known as" line."""
|
|
553
|
+
seen: set[str] = set()
|
|
554
|
+
cleaned: list[str] = []
|
|
555
|
+
for item in v:
|
|
556
|
+
s = str(item).strip()
|
|
557
|
+
if s and s not in seen:
|
|
558
|
+
seen.add(s)
|
|
559
|
+
cleaned.append(s)
|
|
560
|
+
return cleaned
|
|
561
|
+
|
|
562
|
+
|
|
510
563
|
class MetricConfig(BaseModel):
|
|
511
564
|
"""
|
|
512
565
|
Configuration for a single metric.
|
|
@@ -602,7 +655,7 @@ class MetricConfig(BaseModel):
|
|
|
602
655
|
"""Normalize alerting to list. Accepts single dict/AlertConfig (backward compat) or list."""
|
|
603
656
|
if v is None:
|
|
604
657
|
return None
|
|
605
|
-
if isinstance(v,
|
|
658
|
+
if isinstance(v, dict | AlertConfig):
|
|
606
659
|
return [v]
|
|
607
660
|
return v
|
|
608
661
|
|
|
@@ -612,6 +665,19 @@ class MetricConfig(BaseModel):
|
|
|
612
665
|
autotune: AutoTuneConfig | None = Field(
|
|
613
666
|
default=None, description="Optional auto-tuning constraints (for `dtk autotune`)"
|
|
614
667
|
)
|
|
668
|
+
# OSI-compatible AI grounding (instructions/synonyms/examples). Descriptive
|
|
669
|
+
# only: surfaced in alerts (synonyms → "Also known as"), the `dtk tune`
|
|
670
|
+
# cockpit and assistant grounding, but never touches load/detect/alert or the
|
|
671
|
+
# detector id. Mirrors OSI's `ai_context` so a metric's meaning is portable to
|
|
672
|
+
# and from an OSI semantic model. Accepts a bare string (→ instructions).
|
|
673
|
+
ai_context: AiContextConfig | None = Field(
|
|
674
|
+
default=None,
|
|
675
|
+
description=(
|
|
676
|
+
"Optional OSI-compatible AI grounding (instructions/synonyms/examples). "
|
|
677
|
+
"Descriptive only — surfaced in alerts, the tune cockpit and assistant "
|
|
678
|
+
"grounding; never affects load/detect/alert or the detector id."
|
|
679
|
+
),
|
|
680
|
+
)
|
|
615
681
|
# Per-metric false-alert-rate (FDR) budget for manual tuning, overriding the
|
|
616
682
|
# project-wide `false_alert_budget`. The `dtk tune` cockpit flags — non
|
|
617
683
|
# intrusively — when the share of fired alerts that don't overlap a real
|
|
@@ -637,6 +703,15 @@ class MetricConfig(BaseModel):
|
|
|
637
703
|
raise ValueError("false_alert_budget must be a fraction in (0, 1] (e.g. 0.3 = 30%)")
|
|
638
704
|
return v
|
|
639
705
|
|
|
706
|
+
@field_validator("ai_context", mode="before")
|
|
707
|
+
@classmethod
|
|
708
|
+
def _accept_ai_context_string(cls, v: Any) -> Any:
|
|
709
|
+
"""Allow ``ai_context: "free text"`` as shorthand for ``{instructions: ...}``,
|
|
710
|
+
mirroring OSI's ``ai_context`` which is itself either a string or a struct."""
|
|
711
|
+
if isinstance(v, str):
|
|
712
|
+
return {"instructions": v}
|
|
713
|
+
return v
|
|
714
|
+
|
|
640
715
|
# Parsed interval (computed from string/int)
|
|
641
716
|
_interval: Interval | None = None
|
|
642
717
|
|
|
@@ -68,6 +68,7 @@ class _AlertStepMixin(_TaskManagerBase):
|
|
|
68
68
|
alert_config=alerting_config,
|
|
69
69
|
description=config.description,
|
|
70
70
|
mentions=alerting_config.mentions,
|
|
71
|
+
ai_synonyms=(config.ai_context.synonyms if config.ai_context else None),
|
|
71
72
|
dashboard_url=alerting_config.dashboard_url,
|
|
72
73
|
links=alerting_config.links,
|
|
73
74
|
project_name=getattr(self.project_config, "name", None),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""OSI (Open Semantic Interchange) interop for detectkit.
|
|
2
|
+
|
|
3
|
+
An **isolated, additive** layer: nothing in the load/detect/alert pipeline imports
|
|
4
|
+
it, so it can never affect a running project. It powers the ``dtk osi`` command
|
|
5
|
+
group only (import an OSI metric into a native detectkit metric; export detectkit
|
|
6
|
+
metrics into an OSI fragment). sqlglot is an optional dependency (the ``[osi]``
|
|
7
|
+
extra), imported lazily inside :mod:`detectkit.semantic.query_gen`.
|
|
8
|
+
|
|
9
|
+
See ``.claude/rules/architecture.md`` ("Manual semantic interop") and
|
|
10
|
+
``project_osi_detectkit_integration`` for the design.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from detectkit.semantic.errors import (
|
|
16
|
+
OsiDependencyMissing,
|
|
17
|
+
OsiError,
|
|
18
|
+
OsiParseError,
|
|
19
|
+
OsiUnsupportedMetric,
|
|
20
|
+
)
|
|
21
|
+
from detectkit.semantic.exporter import export_metric_to_osi, export_models
|
|
22
|
+
from detectkit.semantic.importer import ImportResult, import_osi_metric
|
|
23
|
+
from detectkit.semantic.osi_model import (
|
|
24
|
+
DETECTKIT_VENDOR,
|
|
25
|
+
OsiSemanticModel,
|
|
26
|
+
load_osi_models,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DETECTKIT_VENDOR",
|
|
31
|
+
"ImportResult",
|
|
32
|
+
"OsiDependencyMissing",
|
|
33
|
+
"OsiError",
|
|
34
|
+
"OsiParseError",
|
|
35
|
+
"OsiSemanticModel",
|
|
36
|
+
"OsiUnsupportedMetric",
|
|
37
|
+
"export_metric_to_osi",
|
|
38
|
+
"export_models",
|
|
39
|
+
"import_osi_metric",
|
|
40
|
+
"load_osi_models",
|
|
41
|
+
]
|