detectkit 0.3.13__tar.gz → 0.3.15__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.3.13/detectkit.egg-info → detectkit-0.3.15}/PKG-INFO +1 -1
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/__init__.py +1 -1
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/channels/base.py +6 -3
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/orchestrator.py +130 -30
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/database/internal_tables.py +4 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/orchestration/task_manager.py +15 -10
- {detectkit-0.3.13 → detectkit-0.3.15/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.3.13 → detectkit-0.3.15}/pyproject.toml +1 -1
- {detectkit-0.3.13 → detectkit-0.3.15}/LICENSE +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/MANIFEST.in +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/README.md +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/cli/main.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/config/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/config/profile.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/config/project_config.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/config/validator.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/core/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/core/interval.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/core/models.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/database/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/database/manager.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/database/tables.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/detectors/base.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit/utils/stats.py +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/requirements.txt +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/setup.cfg +0 -0
- {detectkit-0.3.13 → detectkit-0.3.15}/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.3.
|
|
7
|
+
__version__ = "0.3.14"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -144,10 +144,13 @@ class BaseAlertChannel(ABC):
|
|
|
144
144
|
if isinstance(ts, np.datetime64):
|
|
145
145
|
ts = ts.astype(datetime)
|
|
146
146
|
|
|
147
|
-
#
|
|
148
|
-
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
|
|
147
|
+
# Convert naive UTC timestamp to target timezone if specified
|
|
149
148
|
if alert_data.timezone:
|
|
150
|
-
|
|
149
|
+
from zoneinfo import ZoneInfo
|
|
150
|
+
ts = ts.replace(tzinfo=ZoneInfo("UTC")).astimezone(ZoneInfo(alert_data.timezone))
|
|
151
|
+
ts_str = f"{ts.strftime('%Y-%m-%d %H:%M:%S')} ({alert_data.timezone})"
|
|
152
|
+
else:
|
|
153
|
+
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
|
|
151
154
|
|
|
152
155
|
# Format confidence interval
|
|
153
156
|
if alert_data.confidence_lower is not None and alert_data.confidence_upper is not None:
|
|
@@ -9,9 +9,10 @@ Handles:
|
|
|
9
9
|
- Coordinating alert sending
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import json
|
|
12
13
|
from dataclasses import dataclass
|
|
13
14
|
from datetime import datetime, timezone
|
|
14
|
-
from typing import Dict, List, Optional
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
15
16
|
from detectkit.utils.datetime_utils import now_utc, now_utc_naive, to_naive_utc, to_aware_utc
|
|
16
17
|
|
|
17
18
|
import numpy as np
|
|
@@ -20,6 +21,48 @@ from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
|
|
|
20
21
|
from detectkit.core.interval import Interval
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _parse_detection_metadata(metadata: Any) -> Dict:
|
|
25
|
+
"""Parse detection_metadata stored as dict or JSON string into a dict."""
|
|
26
|
+
if metadata is None:
|
|
27
|
+
return {}
|
|
28
|
+
if isinstance(metadata, dict):
|
|
29
|
+
return metadata
|
|
30
|
+
if isinstance(metadata, (bytes, bytearray)):
|
|
31
|
+
try:
|
|
32
|
+
metadata = metadata.decode("utf-8")
|
|
33
|
+
except Exception:
|
|
34
|
+
return {}
|
|
35
|
+
if isinstance(metadata, str):
|
|
36
|
+
if not metadata:
|
|
37
|
+
return {}
|
|
38
|
+
try:
|
|
39
|
+
parsed = json.loads(metadata)
|
|
40
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
41
|
+
except (ValueError, TypeError):
|
|
42
|
+
return {}
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _direction_from_metadata(metadata: Any, is_anomaly: bool) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Resolve alert direction ("up"/"down"/"none") from detector metadata.
|
|
49
|
+
|
|
50
|
+
Detectors authoritatively write direction as "below"/"above" in
|
|
51
|
+
detection_metadata. This is the source of truth — confidence-bound
|
|
52
|
+
reconstruction does not work for one-sided detectors (e.g. ManualBounds
|
|
53
|
+
with only upper_bound set).
|
|
54
|
+
"""
|
|
55
|
+
if not is_anomaly:
|
|
56
|
+
return "none"
|
|
57
|
+
parsed = _parse_detection_metadata(metadata)
|
|
58
|
+
raw = parsed.get("direction")
|
|
59
|
+
if raw == "below":
|
|
60
|
+
return "down"
|
|
61
|
+
if raw == "above":
|
|
62
|
+
return "up"
|
|
63
|
+
return "none"
|
|
64
|
+
|
|
65
|
+
|
|
23
66
|
@dataclass
|
|
24
67
|
class AlertConditions:
|
|
25
68
|
"""Alert conditions configuration."""
|
|
@@ -488,26 +531,19 @@ class AlertOrchestrator:
|
|
|
488
531
|
# Convert to DetectionRecord format
|
|
489
532
|
detection_records = []
|
|
490
533
|
for det in recent_detections:
|
|
534
|
+
metadata_list = det.get("detection_metadata_list") or [None] * len(det["detector_ids"])
|
|
491
535
|
# Group has multiple detectors per timestamp
|
|
492
536
|
for i in range(len(det["detector_ids"])):
|
|
493
|
-
# Parse detection metadata
|
|
494
|
-
try:
|
|
495
|
-
import json
|
|
496
|
-
metadata = json.loads(det["detector_params_list"][i])
|
|
497
|
-
except:
|
|
498
|
-
metadata = {}
|
|
499
|
-
|
|
500
|
-
# Determine direction
|
|
501
537
|
value = det["value"]
|
|
502
538
|
conf_lower = det["confidence_lowers"][i]
|
|
503
539
|
conf_upper = det["confidence_uppers"][i]
|
|
540
|
+
is_anomaly = det["is_anomaly_flags"][i]
|
|
504
541
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
direction = "none"
|
|
542
|
+
# Use detector-authoritative direction from detection_metadata
|
|
543
|
+
# (works for one-sided detectors like ManualBounds where
|
|
544
|
+
# confidence_lower/upper may be None).
|
|
545
|
+
metadata = _parse_detection_metadata(metadata_list[i])
|
|
546
|
+
direction = _direction_from_metadata(metadata, is_anomaly)
|
|
511
547
|
|
|
512
548
|
record = DetectionRecord(
|
|
513
549
|
timestamp=np.datetime64(det["timestamp"]),
|
|
@@ -515,12 +551,12 @@ class AlertOrchestrator:
|
|
|
515
551
|
detector_id=det["detector_ids"][i],
|
|
516
552
|
detector_params=det["detector_params_list"][i],
|
|
517
553
|
value=value,
|
|
518
|
-
is_anomaly=
|
|
554
|
+
is_anomaly=is_anomaly,
|
|
519
555
|
confidence_lower=conf_lower,
|
|
520
556
|
confidence_upper=conf_upper,
|
|
521
557
|
direction=direction,
|
|
522
558
|
severity=0.0, # Not used for recovery check
|
|
523
|
-
detection_metadata=metadata
|
|
559
|
+
detection_metadata=metadata,
|
|
524
560
|
)
|
|
525
561
|
detection_records.append(record)
|
|
526
562
|
|
|
@@ -528,18 +564,64 @@ class AlertOrchestrator:
|
|
|
528
564
|
detections_by_time = self._group_by_timestamp(detection_records)
|
|
529
565
|
timestamps_sorted = sorted(detections_by_time.keys(), reverse=True)
|
|
530
566
|
|
|
531
|
-
#
|
|
532
|
-
#
|
|
533
|
-
#
|
|
534
|
-
#
|
|
567
|
+
# Direction-aware recovery: only block recovery on anomalies that
|
|
568
|
+
# match the alert's direction condition. For a "down"-only alert an
|
|
569
|
+
# "up" anomaly already means the alert condition no longer holds, so
|
|
570
|
+
# it should count as recovery.
|
|
571
|
+
direction_condition = self.conditions.direction
|
|
535
572
|
latest_ts = timestamps_sorted[0]
|
|
536
573
|
latest_detections = detections_by_time[latest_ts]
|
|
537
574
|
latest_anomalies = [d for d in latest_detections if d.is_anomaly]
|
|
538
|
-
if len(latest_anomalies) > 0:
|
|
539
|
-
# At least one detector still considers this point anomalous — no recovery
|
|
540
|
-
return False
|
|
541
575
|
|
|
542
|
-
|
|
576
|
+
if direction_condition == "down":
|
|
577
|
+
blocking = [d for d in latest_anomalies if d.direction == "down"]
|
|
578
|
+
elif direction_condition == "up":
|
|
579
|
+
blocking = [d for d in latest_anomalies if d.direction == "up"]
|
|
580
|
+
elif direction_condition == "same":
|
|
581
|
+
trigger_direction = self._get_alert_trigger_direction(last_alert_timestamp)
|
|
582
|
+
if trigger_direction is None:
|
|
583
|
+
# Unknown trigger direction → fall back to conservative behavior
|
|
584
|
+
blocking = latest_anomalies
|
|
585
|
+
else:
|
|
586
|
+
blocking = [d for d in latest_anomalies if d.direction == trigger_direction]
|
|
587
|
+
else: # "any" and unknown — preserve historical behavior
|
|
588
|
+
blocking = latest_anomalies
|
|
589
|
+
|
|
590
|
+
return len(blocking) == 0
|
|
591
|
+
|
|
592
|
+
def _get_alert_trigger_direction(
|
|
593
|
+
self, last_alert_timestamp: datetime
|
|
594
|
+
) -> Optional[str]:
|
|
595
|
+
"""
|
|
596
|
+
Resolve the direction of the anomaly that triggered the last alert.
|
|
597
|
+
|
|
598
|
+
Used by direction="same" recovery logic. Loads the detection record
|
|
599
|
+
at last_alert_timestamp and returns the dominant anomaly direction
|
|
600
|
+
from detection_metadata.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
"up", "down", or None if not resolvable.
|
|
604
|
+
"""
|
|
605
|
+
if not self.internal:
|
|
606
|
+
return None
|
|
607
|
+
|
|
608
|
+
trigger_detections = self.internal.get_recent_detections(
|
|
609
|
+
metric_name=self.metric_name,
|
|
610
|
+
last_point=last_alert_timestamp,
|
|
611
|
+
num_points=1,
|
|
612
|
+
)
|
|
613
|
+
if not trigger_detections:
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
det = trigger_detections[0]
|
|
617
|
+
metadata_list = det.get("detection_metadata_list") or [None] * len(det["detector_ids"])
|
|
618
|
+
for i in range(len(det["detector_ids"])):
|
|
619
|
+
if not det["is_anomaly_flags"][i]:
|
|
620
|
+
continue
|
|
621
|
+
direction = _direction_from_metadata(metadata_list[i], True)
|
|
622
|
+
if direction in ("up", "down"):
|
|
623
|
+
return direction
|
|
624
|
+
return None
|
|
543
625
|
|
|
544
626
|
def should_send_recovery(
|
|
545
627
|
self,
|
|
@@ -601,19 +683,37 @@ class AlertOrchestrator:
|
|
|
601
683
|
if not detections:
|
|
602
684
|
return None
|
|
603
685
|
|
|
604
|
-
# Use the latest (newest) detection point for recovery
|
|
686
|
+
# Use the latest (newest) detection point for recovery timestamp/value.
|
|
605
687
|
# detections are sorted oldest→newest by _load_recent_detections.
|
|
606
688
|
latest = detections[-1]
|
|
607
689
|
|
|
690
|
+
# Use detector info and CI from the last anomalous detection
|
|
691
|
+
# (the one that triggered the alert), since normal points have
|
|
692
|
+
# detector_name="unknown" and confidence=None.
|
|
693
|
+
last_anomalous = next(
|
|
694
|
+
(d for d in reversed(detections) if d.is_anomaly),
|
|
695
|
+
None,
|
|
696
|
+
)
|
|
697
|
+
if last_anomalous:
|
|
698
|
+
recovery_detector_name = last_anomalous.detector_name
|
|
699
|
+
recovery_detector_params = last_anomalous.detector_params
|
|
700
|
+
recovery_ci_lower = last_anomalous.confidence_lower
|
|
701
|
+
recovery_ci_upper = last_anomalous.confidence_upper
|
|
702
|
+
else:
|
|
703
|
+
recovery_detector_name = latest.detector_name
|
|
704
|
+
recovery_detector_params = latest.detector_params
|
|
705
|
+
recovery_ci_lower = latest.confidence_lower
|
|
706
|
+
recovery_ci_upper = latest.confidence_upper
|
|
707
|
+
|
|
608
708
|
return AlertData(
|
|
609
709
|
metric_name=self.metric_name,
|
|
610
710
|
timestamp=latest.timestamp,
|
|
611
711
|
timezone=self.timezone_display,
|
|
612
712
|
value=latest.value,
|
|
613
|
-
confidence_lower=
|
|
614
|
-
confidence_upper=
|
|
615
|
-
detector_name=
|
|
616
|
-
detector_params=
|
|
713
|
+
confidence_lower=recovery_ci_lower,
|
|
714
|
+
confidence_upper=recovery_ci_upper,
|
|
715
|
+
detector_name=recovery_detector_name,
|
|
716
|
+
detector_params=recovery_detector_params,
|
|
617
717
|
direction="none",
|
|
618
718
|
severity=0.0,
|
|
619
719
|
detection_metadata={},
|
|
@@ -467,6 +467,7 @@ class InternalTablesManager:
|
|
|
467
467
|
- detector_ids: List of detector IDs for this timestamp
|
|
468
468
|
- detector_names: List of detector names
|
|
469
469
|
- detector_params_list: List of detector params (JSON strings)
|
|
470
|
+
- detection_metadata_list: List of detection metadata (JSON strings)
|
|
470
471
|
- is_anomaly_flags: List of is_anomaly bools
|
|
471
472
|
- confidence_lowers: List of lower confidence bounds
|
|
472
473
|
- confidence_uppers: List of upper confidence bounds
|
|
@@ -531,6 +532,7 @@ class InternalTablesManager:
|
|
|
531
532
|
detector_id,
|
|
532
533
|
detector_name,
|
|
533
534
|
detector_params,
|
|
535
|
+
detection_metadata,
|
|
534
536
|
is_anomaly,
|
|
535
537
|
confidence_lower,
|
|
536
538
|
confidence_upper,
|
|
@@ -570,6 +572,7 @@ class InternalTablesManager:
|
|
|
570
572
|
"detector_ids": [],
|
|
571
573
|
"detector_names": [],
|
|
572
574
|
"detector_params_list": [],
|
|
575
|
+
"detection_metadata_list": [],
|
|
573
576
|
"is_anomaly_flags": [],
|
|
574
577
|
"confidence_lowers": [],
|
|
575
578
|
"confidence_uppers": [],
|
|
@@ -579,6 +582,7 @@ class InternalTablesManager:
|
|
|
579
582
|
grouped[ts_key]["detector_ids"].append(row["detector_id"])
|
|
580
583
|
grouped[ts_key]["detector_names"].append(row["detector_name"])
|
|
581
584
|
grouped[ts_key]["detector_params_list"].append(row["detector_params"])
|
|
585
|
+
grouped[ts_key]["detection_metadata_list"].append(row.get("detection_metadata"))
|
|
582
586
|
grouped[ts_key]["is_anomaly_flags"].append(row["is_anomaly"])
|
|
583
587
|
grouped[ts_key]["confidence_lowers"].append(row["confidence_lower"])
|
|
584
588
|
grouped[ts_key]["confidence_uppers"].append(row["confidence_upper"])
|
|
@@ -23,6 +23,8 @@ from detectkit.alerting.orchestrator import (
|
|
|
23
23
|
AlertConditions,
|
|
24
24
|
AlertOrchestrator,
|
|
25
25
|
DetectionRecord,
|
|
26
|
+
_direction_from_metadata,
|
|
27
|
+
_parse_detection_metadata,
|
|
26
28
|
)
|
|
27
29
|
from detectkit.config.metric_config import MetricConfig
|
|
28
30
|
from detectkit.core.interval import Interval
|
|
@@ -737,7 +739,9 @@ class TaskManager:
|
|
|
737
739
|
if flag
|
|
738
740
|
]
|
|
739
741
|
|
|
740
|
-
# Determine direction and severity for the most severe detector
|
|
742
|
+
# Determine direction and severity for the most severe detector.
|
|
743
|
+
# Direction is read from detector metadata (authoritative for all
|
|
744
|
+
# detectors including ManualBounds where confidence bounds may be None).
|
|
741
745
|
direction = "none"
|
|
742
746
|
severity = 0.0
|
|
743
747
|
confidence_lower = None
|
|
@@ -745,6 +749,9 @@ class TaskManager:
|
|
|
745
749
|
detector_name = "unknown"
|
|
746
750
|
detector_id = "unknown"
|
|
747
751
|
detector_params = "{}"
|
|
752
|
+
metadata: dict = {}
|
|
753
|
+
|
|
754
|
+
metadata_list = row.get("detection_metadata_list") or [None] * len(row["detector_ids"])
|
|
748
755
|
|
|
749
756
|
if is_anomaly and anomaly_indices:
|
|
750
757
|
# Get data from first anomalous detector
|
|
@@ -755,14 +762,12 @@ class TaskManager:
|
|
|
755
762
|
confidence_lower = row["confidence_lowers"][first_idx]
|
|
756
763
|
confidence_upper = row["confidence_uppers"][first_idx]
|
|
757
764
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
direction = "up"
|
|
765
|
-
severity = (value - confidence_upper) / max(abs(confidence_upper), 1e-10)
|
|
765
|
+
metadata = _parse_detection_metadata(metadata_list[first_idx])
|
|
766
|
+
direction = _direction_from_metadata(metadata, True)
|
|
767
|
+
try:
|
|
768
|
+
severity = float(metadata.get("severity", 0.0) or 0.0)
|
|
769
|
+
except (TypeError, ValueError):
|
|
770
|
+
severity = 0.0
|
|
766
771
|
|
|
767
772
|
records.append(
|
|
768
773
|
DetectionRecord(
|
|
@@ -776,7 +781,7 @@ class TaskManager:
|
|
|
776
781
|
confidence_upper=confidence_upper,
|
|
777
782
|
direction=direction,
|
|
778
783
|
severity=severity,
|
|
779
|
-
detection_metadata=
|
|
784
|
+
detection_metadata=metadata,
|
|
780
785
|
)
|
|
781
786
|
)
|
|
782
787
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|