failure-forensics 0.1.1__py3-none-any.whl
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.
- failure_forensics/__init__.py +5 -0
- failure_forensics/ab_report.py +116 -0
- failure_forensics/alerts.py +100 -0
- failure_forensics/baseline.py +91 -0
- failure_forensics/config.py +36 -0
- failure_forensics/dashboard.py +104 -0
- failure_forensics/eval_collector.py +113 -0
- failure_forensics/forensics.py +143 -0
- failure_forensics/llm_analyzer.py +68 -0
- failure_forensics/logger.py +105 -0
- failure_forensics/pattern.py +175 -0
- failure_forensics/prompt_optimizer.py +82 -0
- failure_forensics/recommender.py +62 -0
- failure_forensics/regression_guard.py +80 -0
- failure_forensics/trace.py +72 -0
- failure_forensics/versioning.py +120 -0
- failure_forensics-0.1.1.dist-info/METADATA +269 -0
- failure_forensics-0.1.1.dist-info/RECORD +21 -0
- failure_forensics-0.1.1.dist-info/WHEEL +5 -0
- failure_forensics-0.1.1.dist-info/licenses/LICENSE +21 -0
- failure_forensics-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ab_report.py — İki prompt versiyonunu karşılaştıran A/B raporu üretir.
|
|
3
|
+
Adım bazında breakdown, kazanan versiyon, fark yüzdesi.
|
|
4
|
+
data/ab_report.json'a kaydeder ve terminale tablo olarak basar.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
11
|
+
from failure_forensics.versioning import compare_versions, version_stats
|
|
12
|
+
from failure_forensics.logger import read_logs
|
|
13
|
+
from failure_forensics.config import AB_REPORT_FILE
|
|
14
|
+
|
|
15
|
+
STEP_ORDER = ["retrieval", "reranking", "generation", "citation"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_ab_report(v1: str = "v1", v2: str = "v2") -> dict:
|
|
19
|
+
"""
|
|
20
|
+
v1 ve v2 versiyonları için A/B raporu üretir.
|
|
21
|
+
Raporu AB_REPORT_FILE'a kaydeder.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Tam rapor dict'i
|
|
25
|
+
"""
|
|
26
|
+
logs = read_logs()
|
|
27
|
+
comparison = compare_versions(v1, v2, logs)
|
|
28
|
+
stats = version_stats(logs)
|
|
29
|
+
|
|
30
|
+
v1_data = stats.get(v1, {})
|
|
31
|
+
v2_data = stats.get(v2, {})
|
|
32
|
+
|
|
33
|
+
# Adım bazında karşılaştırma
|
|
34
|
+
step_comparison = {}
|
|
35
|
+
all_steps = set(
|
|
36
|
+
list(v1_data.get("steps", {}).keys()) +
|
|
37
|
+
list(v2_data.get("steps", {}).keys())
|
|
38
|
+
)
|
|
39
|
+
for step in all_steps:
|
|
40
|
+
s1 = v1_data.get("steps", {}).get(step, {"total": 0, "failed": 0, "rate": 0.0})
|
|
41
|
+
s2 = v2_data.get("steps", {}).get(step, {"total": 0, "failed": 0, "rate": 0.0})
|
|
42
|
+
delta = s1["rate"] - s2["rate"]
|
|
43
|
+
step_winner = v1 if s1["rate"] < s2["rate"] else (v2 if s2["rate"] < s1["rate"] else "tie")
|
|
44
|
+
step_comparison[step] = {
|
|
45
|
+
v1: s1,
|
|
46
|
+
v2: s2,
|
|
47
|
+
"delta_pct": round(delta * 100, 2),
|
|
48
|
+
"step_winner": step_winner,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
report = {
|
|
52
|
+
"summary": {
|
|
53
|
+
"v1": v1,
|
|
54
|
+
"v2": v2,
|
|
55
|
+
"winner": comparison["winner"],
|
|
56
|
+
"v1_failure_rate": comparison["v1_rate"],
|
|
57
|
+
"v2_failure_rate": comparison["v2_rate"],
|
|
58
|
+
"improvement_pct": comparison["improvement_pct"],
|
|
59
|
+
"v1_runs": comparison["v1_runs"],
|
|
60
|
+
"v2_runs": comparison["v2_runs"],
|
|
61
|
+
},
|
|
62
|
+
"step_breakdown": step_comparison,
|
|
63
|
+
"raw_stats": {v1: v1_data, v2: v2_data},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Kaydet
|
|
67
|
+
os.makedirs(os.path.dirname(AB_REPORT_FILE), exist_ok=True)
|
|
68
|
+
with open(AB_REPORT_FILE, "w", encoding="utf-8") as f:
|
|
69
|
+
json.dump(report, f, indent=2, ensure_ascii=False)
|
|
70
|
+
|
|
71
|
+
return report
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def print_ab_report(v1: str = "v1", v2: str = "v2"):
|
|
75
|
+
"""Terminale formatlanmış A/B raporu tablo olarak basar."""
|
|
76
|
+
report = generate_ab_report(v1, v2)
|
|
77
|
+
summary = report["summary"]
|
|
78
|
+
steps = report["step_breakdown"]
|
|
79
|
+
|
|
80
|
+
print("\n" + "=" * 62)
|
|
81
|
+
print(" 📊 A/B RAPORU — Prompt Versiyon Karşılaştırması")
|
|
82
|
+
print("=" * 62)
|
|
83
|
+
|
|
84
|
+
# Özet
|
|
85
|
+
winner = summary["winner"]
|
|
86
|
+
winner_icon = "🏆"
|
|
87
|
+
print(f"\n Karşılaştırma : {v1} vs {v2}")
|
|
88
|
+
print(f" {v1} çalışma sayısı : {summary['v1_runs']}")
|
|
89
|
+
print(f" {v2} çalışma sayısı : {summary['v2_runs']}")
|
|
90
|
+
print(f"\n {'Metrik':<30} {v1:>10} {v2:>10} {'Fark':>10}")
|
|
91
|
+
print(f" {'-'*60}")
|
|
92
|
+
v1r = summary["v1_failure_rate"]
|
|
93
|
+
v2r = summary["v2_failure_rate"]
|
|
94
|
+
diff = (v1r - v2r) * 100
|
|
95
|
+
print(f" {'Genel Failure Rate':<30} {v1r:>9.1%} {v2r:>10.1%} {diff:>+9.1f}pp")
|
|
96
|
+
|
|
97
|
+
# Adım bazında tablo
|
|
98
|
+
print(f"\n {'Adım':<16} {v1+' fail':>12} {v2+' fail':>12} {'Kazanan':>10}")
|
|
99
|
+
print(f" {'-'*55}")
|
|
100
|
+
|
|
101
|
+
for step in STEP_ORDER:
|
|
102
|
+
if step not in steps:
|
|
103
|
+
continue
|
|
104
|
+
sc = steps[step]
|
|
105
|
+
r1 = sc[v1]["rate"]
|
|
106
|
+
r2 = sc[v2]["rate"]
|
|
107
|
+
sw = sc["step_winner"]
|
|
108
|
+
icon = winner_icon if sw != "tie" else "="
|
|
109
|
+
print(f" {step:<16} {r1:>11.1%} {r2:>12.1%} {icon} {sw:>6}")
|
|
110
|
+
|
|
111
|
+
print(f"\n {winner_icon} KAZANAN: {winner.upper()} "
|
|
112
|
+
f"— {summary['improvement_pct']:.1f} pp iyileşme")
|
|
113
|
+
print(f"\n 📁 Detaylı rapor: {AB_REPORT_FILE}")
|
|
114
|
+
print("=" * 62)
|
|
115
|
+
|
|
116
|
+
return report
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
alerts.py — Slack webhook bildirimleri.
|
|
3
|
+
Tetiklenme koşulları:
|
|
4
|
+
1. Failure rate eşiği aşıldığında
|
|
5
|
+
2. Anomali tespit edildiğinde
|
|
6
|
+
3. Belirli bir adım 3 üst üste başarısız olduğunda
|
|
7
|
+
|
|
8
|
+
Webhook URL boşsa konsola yazar.
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
16
|
+
from failure_forensics.config import SLACK_WEBHOOK_URL, FAILURE_RATE_THRESHOLD, CONSECUTIVE_FAILURE_THRESHOLD
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import requests as _requests
|
|
20
|
+
_HAS_REQUESTS = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
_HAS_REQUESTS = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _send(message: str, level: str = "WARNING"):
|
|
26
|
+
"""Mesajı Slack'e veya konsola gönderir."""
|
|
27
|
+
icon = {"WARNING": "⚠️", "CRITICAL": "🔴", "INFO": "ℹ️"}.get(level, "⚠️")
|
|
28
|
+
full_msg = f"{icon} *[Failure Forensics]* {message}"
|
|
29
|
+
|
|
30
|
+
if SLACK_WEBHOOK_URL and _HAS_REQUESTS:
|
|
31
|
+
try:
|
|
32
|
+
_requests.post(
|
|
33
|
+
SLACK_WEBHOOK_URL,
|
|
34
|
+
json={"text": full_msg},
|
|
35
|
+
timeout=5,
|
|
36
|
+
)
|
|
37
|
+
print(f"[ALERT → Slack] {full_msg}")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"[ALERT → Console (Slack failed: {e})] {full_msg}")
|
|
40
|
+
else:
|
|
41
|
+
ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
|
|
42
|
+
print(f"[ALERT {ts}] {full_msg}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_failure_rate(failure_rate: float, context: str = ""):
|
|
46
|
+
"""Failure rate eşiği aşılırsa alert gönderir."""
|
|
47
|
+
if failure_rate > FAILURE_RATE_THRESHOLD:
|
|
48
|
+
_send(
|
|
49
|
+
f"Failure rate eşiği aşıldı: {failure_rate:.1%} "
|
|
50
|
+
f"(eşik: {FAILURE_RATE_THRESHOLD:.1%}). {context}",
|
|
51
|
+
level="CRITICAL",
|
|
52
|
+
)
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_anomaly(anomaly_result: dict):
|
|
58
|
+
"""Anomali tespit edildiyse alert gönderir."""
|
|
59
|
+
if anomaly_result.get("anomaly"):
|
|
60
|
+
today = anomaly_result["today_rate"]
|
|
61
|
+
avg = anomaly_result["avg_7d"]
|
|
62
|
+
delta = anomaly_result["delta"]
|
|
63
|
+
_send(
|
|
64
|
+
f"Anomali tespit edildi! Bugün: {today:.1%}, "
|
|
65
|
+
f"7g ort: {avg:.1%}, fark: +{delta:.1%}",
|
|
66
|
+
level="CRITICAL",
|
|
67
|
+
)
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def check_consecutive_failures(logs: list[dict], step_name: str = None) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Belirli bir adımda (veya herhangi bir adımda) arka arkaya
|
|
75
|
+
CONSECUTIVE_FAILURE_THRESHOLD kadar hata varsa alert gönderir.
|
|
76
|
+
"""
|
|
77
|
+
if not logs:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
# İsteğe bağlı filtre
|
|
81
|
+
filtered = [r for r in logs if step_name is None or r.get("step_name") == step_name]
|
|
82
|
+
|
|
83
|
+
# Son N kaydı kontrol et
|
|
84
|
+
recent = filtered[-CONSECUTIVE_FAILURE_THRESHOLD:]
|
|
85
|
+
if len(recent) < CONSECUTIVE_FAILURE_THRESHOLD:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
if all(not r.get("success", True) for r in recent):
|
|
89
|
+
step_label = step_name or "herhangi bir adım"
|
|
90
|
+
_send(
|
|
91
|
+
f"'{step_label}' adımı {CONSECUTIVE_FAILURE_THRESHOLD} üst üste başarısız!",
|
|
92
|
+
level="CRITICAL",
|
|
93
|
+
)
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def send_summary(title: str, body: str):
|
|
99
|
+
"""Genel özet bildirimi gönderir."""
|
|
100
|
+
_send(f"*{title}*\n{body}", level="INFO")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
baseline.py — Günlük failure rate'i kaydeder, 7 günlük hareketli ortalama ve trend hesaplar.
|
|
3
|
+
Trend: IMPROVING / STABLE / DEGRADING
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
11
|
+
from failure_forensics.config import BASELINE_FILE, TREND_IMPROVING_THRESHOLD, TREND_DEGRADING_THRESHOLD
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load() -> dict:
|
|
15
|
+
os.makedirs(os.path.dirname(BASELINE_FILE), exist_ok=True)
|
|
16
|
+
if not os.path.exists(BASELINE_FILE):
|
|
17
|
+
return {}
|
|
18
|
+
with open(BASELINE_FILE, "r", encoding="utf-8") as f:
|
|
19
|
+
try:
|
|
20
|
+
return json.load(f)
|
|
21
|
+
except json.JSONDecodeError:
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _save(data: dict):
|
|
26
|
+
os.makedirs(os.path.dirname(BASELINE_FILE), exist_ok=True)
|
|
27
|
+
with open(BASELINE_FILE, "w", encoding="utf-8") as f:
|
|
28
|
+
json.dump(data, f, indent=2)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def record_daily_rate(date: str, failure_rate: float):
|
|
32
|
+
"""Bir günün failure rate'ini baseline dosyasına kaydeder."""
|
|
33
|
+
data = _load()
|
|
34
|
+
data[date] = round(failure_rate, 6)
|
|
35
|
+
_save(data)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_moving_average(days: int = 7) -> dict:
|
|
39
|
+
"""
|
|
40
|
+
Son N günün hareketli ortalamasını döner.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
{
|
|
44
|
+
"dates": [...],
|
|
45
|
+
"rates": [...],
|
|
46
|
+
"moving_avg": float,
|
|
47
|
+
"trend": "IMPROVING" | "STABLE" | "DEGRADING",
|
|
48
|
+
"trend_delta": float
|
|
49
|
+
}
|
|
50
|
+
"""
|
|
51
|
+
data = _load()
|
|
52
|
+
sorted_dates = sorted(data.keys())[-days:]
|
|
53
|
+
rates = [data[d] for d in sorted_dates]
|
|
54
|
+
|
|
55
|
+
moving_avg = sum(rates) / len(rates) if rates else 0.0
|
|
56
|
+
|
|
57
|
+
# Trend: ilk yarı ile ikinci yarıyı karşılaştır
|
|
58
|
+
if len(rates) >= 4:
|
|
59
|
+
mid = len(rates) // 2
|
|
60
|
+
first_half_avg = sum(rates[:mid]) / mid
|
|
61
|
+
second_half_avg = sum(rates[mid:]) / (len(rates) - mid)
|
|
62
|
+
trend_delta = second_half_avg - first_half_avg
|
|
63
|
+
elif len(rates) >= 2:
|
|
64
|
+
trend_delta = rates[-1] - rates[0]
|
|
65
|
+
else:
|
|
66
|
+
trend_delta = 0.0
|
|
67
|
+
|
|
68
|
+
if trend_delta <= TREND_IMPROVING_THRESHOLD:
|
|
69
|
+
trend = "IMPROVING"
|
|
70
|
+
elif trend_delta >= TREND_DEGRADING_THRESHOLD:
|
|
71
|
+
trend = "DEGRADING"
|
|
72
|
+
else:
|
|
73
|
+
trend = "STABLE"
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"dates": sorted_dates,
|
|
77
|
+
"rates": [round(r, 4) for r in rates],
|
|
78
|
+
"moving_avg": round(moving_avg, 4),
|
|
79
|
+
"trend": trend,
|
|
80
|
+
"trend_delta": round(trend_delta, 4),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def sync_from_pattern(daily_rates: dict):
|
|
85
|
+
"""
|
|
86
|
+
pattern.py'den gelen günlük failure rate'leri baseline'a yazar.
|
|
87
|
+
Her gün için record_daily_rate çağırır.
|
|
88
|
+
"""
|
|
89
|
+
for date, day_data in daily_rates.items():
|
|
90
|
+
rate = day_data.get("failure_rate", 0.0)
|
|
91
|
+
record_daily_rate(date, rate)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
config.py — Eşik değerleri, Slack webhook URL, adım bazında kalite eşikleri.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Failure rate eşiği — bu değeri aşarsa alert tetiklenir
|
|
6
|
+
FAILURE_RATE_THRESHOLD = 0.25
|
|
7
|
+
|
|
8
|
+
# Anomali eşiği — bugün 7 günlük ortalamayı bu kadar aşarsa ANOMALY flag'i
|
|
9
|
+
ANOMALY_THRESHOLD = 0.20
|
|
10
|
+
|
|
11
|
+
# Slack webhook URL — boş bırakılırsa konsola yazılır
|
|
12
|
+
SLACK_WEBHOOK_URL = ""
|
|
13
|
+
|
|
14
|
+
# Log dosyası
|
|
15
|
+
LOG_FILE = "data/logs/requests.jsonl"
|
|
16
|
+
|
|
17
|
+
# A/B rapor çıktısı
|
|
18
|
+
AB_REPORT_FILE = "data/ab_report.json"
|
|
19
|
+
|
|
20
|
+
# Baseline geçmiş dosyası
|
|
21
|
+
BASELINE_FILE = "data/baseline.json"
|
|
22
|
+
|
|
23
|
+
# Adım bazında kalite eşikleri (maksimum kabul edilebilir failure rate)
|
|
24
|
+
STEP_THRESHOLDS = {
|
|
25
|
+
"retrieval": 0.20,
|
|
26
|
+
"reranking": 0.15,
|
|
27
|
+
"generation": 0.15,
|
|
28
|
+
"citation": 0.10,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Trend hesabı için eşikler
|
|
32
|
+
TREND_IMPROVING_THRESHOLD = -0.05 # %5 iyileşme → IMPROVING
|
|
33
|
+
TREND_DEGRADING_THRESHOLD = 0.05 # %5 kötüleşme → DEGRADING
|
|
34
|
+
|
|
35
|
+
# Kaç üst üste hata ALERT tetikler
|
|
36
|
+
CONSECUTIVE_FAILURE_THRESHOLD = 3
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dashboard.py — Terminal ASCII dashboard.
|
|
3
|
+
Gösterir:
|
|
4
|
+
- Son 7 günün failure rate grafiği (ASCII bar chart)
|
|
5
|
+
- Adım bazında hata dağılımı
|
|
6
|
+
- Aktif anomaliler
|
|
7
|
+
- Son 5 başarısız run ve kök nedenleri
|
|
8
|
+
- Trend durumu (IMPROVING / STABLE / DEGRADING)
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
14
|
+
from failure_forensics.pattern import daily_failure_rates, detect_anomaly, step_breakdown_report
|
|
15
|
+
from failure_forensics.baseline import get_moving_average, sync_from_pattern
|
|
16
|
+
from failure_forensics.forensics import get_failed_runs
|
|
17
|
+
|
|
18
|
+
BAR_WIDTH = 30
|
|
19
|
+
TREND_ICONS = {
|
|
20
|
+
"IMPROVING": "📈 IMPROVING ↓ (failure azalıyor)",
|
|
21
|
+
"STABLE": "➡️ STABLE — (kararlı)",
|
|
22
|
+
"DEGRADING": "📉 DEGRADING ↑ (failure artıyor)",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _bar(rate: float, width: int = BAR_WIDTH) -> str:
|
|
27
|
+
"""0.0-1.0 arası failure rate'i ASCII bar'a dönüştürür."""
|
|
28
|
+
filled = int(rate * width)
|
|
29
|
+
empty = width - filled
|
|
30
|
+
color = "█" * filled + "░" * empty
|
|
31
|
+
return f"[{color}] {rate:.1%}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render(clear: bool = False):
|
|
35
|
+
"""Terminal dashboard'ı render eder."""
|
|
36
|
+
if clear:
|
|
37
|
+
os.system("cls" if os.name == "nt" else "clear")
|
|
38
|
+
|
|
39
|
+
daily = daily_failure_rates(7)
|
|
40
|
+
sync_from_pattern(daily)
|
|
41
|
+
anomaly = detect_anomaly(daily)
|
|
42
|
+
step_report = step_breakdown_report(daily)
|
|
43
|
+
baseline = get_moving_average(7)
|
|
44
|
+
failed_runs = get_failed_runs(5)
|
|
45
|
+
|
|
46
|
+
W = 65
|
|
47
|
+
print("\n" + "═" * W)
|
|
48
|
+
print(" 🔬 FAILURE FORENSICS — Terminal Dashboard")
|
|
49
|
+
print("═" * W)
|
|
50
|
+
|
|
51
|
+
# ── 1. 7 Günlük Failure Rate Grafiği ─────────────────────────
|
|
52
|
+
print("\n 📅 SON 7 GÜNÜN FAILURE RATE GRAFİĞİ")
|
|
53
|
+
print(" " + "─" * (W - 2))
|
|
54
|
+
if not daily:
|
|
55
|
+
print(" (henüz veri yok)")
|
|
56
|
+
else:
|
|
57
|
+
for date, data in sorted(daily.items()):
|
|
58
|
+
rate = data["failure_rate"]
|
|
59
|
+
bar = _bar(rate)
|
|
60
|
+
marker = " ⚠️ " if rate > 0.25 else " "
|
|
61
|
+
print(f" {date} {bar}{marker}")
|
|
62
|
+
|
|
63
|
+
# ── 2. Adım Bazında Hata Dağılımı ────────────────────────────
|
|
64
|
+
print(f"\n 🔍 ADIM BAZINDA HATA DAĞILIMI (son 7 gün)")
|
|
65
|
+
print(" " + "─" * (W - 2))
|
|
66
|
+
if not step_report:
|
|
67
|
+
print(" (veri yok)")
|
|
68
|
+
else:
|
|
69
|
+
for step, sd in sorted(step_report.items(), key=lambda x: -x[1]["rate"]):
|
|
70
|
+
rate = sd["rate"]
|
|
71
|
+
bar = _bar(rate, width=20)
|
|
72
|
+
print(f" {step:<12} {bar} ({sd['failed']}/{sd['total']} hatalı)")
|
|
73
|
+
|
|
74
|
+
# ── 3. Anomali Durumu ─────────────────────────────────────────
|
|
75
|
+
print(f"\n ⚡ ANOMALİ DURUMU")
|
|
76
|
+
print(" " + "─" * (W - 2))
|
|
77
|
+
print(f" {anomaly['message']}")
|
|
78
|
+
if anomaly["anomaly"]:
|
|
79
|
+
print(f" → Bugün: {anomaly['today_rate']:.1%} | 7g ort: {anomaly['avg_7d']:.1%} | Delta: +{anomaly['delta']:.1%}")
|
|
80
|
+
|
|
81
|
+
# ── 4. Trend ──────────────────────────────────────────────────
|
|
82
|
+
print(f"\n 📊 TREND & BASELINE (7 günlük hareketli ort.)")
|
|
83
|
+
print(" " + "─" * (W - 2))
|
|
84
|
+
trend = baseline.get("trend", "STABLE")
|
|
85
|
+
avg = baseline.get("moving_avg", 0.0)
|
|
86
|
+
delta = baseline.get("trend_delta", 0.0)
|
|
87
|
+
print(f" {TREND_ICONS.get(trend, trend)}")
|
|
88
|
+
print(f" Hareketli Ort: {avg:.1%} | Trend Delta: {delta:+.1%}")
|
|
89
|
+
|
|
90
|
+
# ── 5. Son 5 Başarısız Run ────────────────────────────────────
|
|
91
|
+
print(f"\n ❌ SON 5 BAŞARISIZ RUN & KÖK NEDENLER")
|
|
92
|
+
print(" " + "─" * (W - 2))
|
|
93
|
+
if not failed_runs:
|
|
94
|
+
print(" Başarısız run bulunamadı. 🎉")
|
|
95
|
+
else:
|
|
96
|
+
for i, run in enumerate(failed_runs[:5], 1):
|
|
97
|
+
rid = run["run_id"]
|
|
98
|
+
cat = run["category"]
|
|
99
|
+
steps = ", ".join(run["failed_steps"]) or "-"
|
|
100
|
+
print(f" {i}. run_id={rid} [{cat}]")
|
|
101
|
+
print(f" Başarısız adımlar: {steps}")
|
|
102
|
+
print(f" → {run['description'][:80]}")
|
|
103
|
+
|
|
104
|
+
print("\n" + "═" * W + "\n")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Katman 3 — eval_collector.py
|
|
3
|
+
Başarısız sorgulardan otomatik eval seti büyütme.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime, timezone, timedelta
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
12
|
+
from failure_forensics.logger import read_logs
|
|
13
|
+
from failure_forensics.forensics import ERROR_CATEGORY_MAP, STEP_CATEGORY_MAP
|
|
14
|
+
|
|
15
|
+
EVAL_CANDIDATES_FILE = "data/eval_candidates/candidates.jsonl"
|
|
16
|
+
|
|
17
|
+
def _ensure_dir():
|
|
18
|
+
os.makedirs(os.path.dirname(EVAL_CANDIDATES_FILE), exist_ok=True)
|
|
19
|
+
|
|
20
|
+
def collect_candidates():
|
|
21
|
+
"""Başarısız sorguları frekanslarına göre toplar ve candidates.jsonl dosyasına yazar."""
|
|
22
|
+
logs = read_logs()
|
|
23
|
+
|
|
24
|
+
# query -> { failure_count, root_cause_category, first_seen, last_seen }
|
|
25
|
+
candidates = {}
|
|
26
|
+
|
|
27
|
+
# run_id -> category mapping
|
|
28
|
+
# Hızlıca kategori bulmak için
|
|
29
|
+
run_categories = {}
|
|
30
|
+
|
|
31
|
+
# Önce tüm başarısız run'ların kategorilerini bul
|
|
32
|
+
runs = defaultdict(list)
|
|
33
|
+
for record in logs:
|
|
34
|
+
runs[record.get("run_id", "unknown")].append(record)
|
|
35
|
+
|
|
36
|
+
for rid, steps in runs.items():
|
|
37
|
+
failed_steps = [s for s in steps if not s.get("success", True)]
|
|
38
|
+
if failed_steps:
|
|
39
|
+
first_failure = failed_steps[0]
|
|
40
|
+
step_name = first_failure.get("step_name", "")
|
|
41
|
+
error_type = (first_failure.get("error_type") or "").lower()
|
|
42
|
+
|
|
43
|
+
category = "UNKNOWN"
|
|
44
|
+
for key, cat in ERROR_CATEGORY_MAP.items():
|
|
45
|
+
if key in error_type:
|
|
46
|
+
category = cat
|
|
47
|
+
break
|
|
48
|
+
if category == "UNKNOWN":
|
|
49
|
+
category = STEP_CATEGORY_MAP.get(step_name, "UNKNOWN")
|
|
50
|
+
|
|
51
|
+
run_categories[rid] = category
|
|
52
|
+
|
|
53
|
+
# Input summary'den query al
|
|
54
|
+
query = first_failure.get("input_summary", "").strip()
|
|
55
|
+
if not query or query.startswith("Simulated"): # Simulate'deki fake query'leri atla
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
ts = first_failure.get("timestamp", "")
|
|
59
|
+
|
|
60
|
+
# Query deduplication
|
|
61
|
+
if query not in candidates:
|
|
62
|
+
candidates[query] = {
|
|
63
|
+
"query": query,
|
|
64
|
+
"failure_count": 1,
|
|
65
|
+
"root_cause_category": category,
|
|
66
|
+
"first_seen": ts,
|
|
67
|
+
"last_seen": ts,
|
|
68
|
+
"auto_add_eligible": False
|
|
69
|
+
}
|
|
70
|
+
else:
|
|
71
|
+
candidates[query]["failure_count"] += 1
|
|
72
|
+
candidates[query]["last_seen"] = ts
|
|
73
|
+
if candidates[query]["failure_count"] >= 3:
|
|
74
|
+
candidates[query]["auto_add_eligible"] = True
|
|
75
|
+
|
|
76
|
+
# Dosyaya yaz
|
|
77
|
+
_ensure_dir()
|
|
78
|
+
with open(EVAL_CANDIDATES_FILE, "w", encoding="utf-8") as f:
|
|
79
|
+
for query, data in candidates.items():
|
|
80
|
+
if data["failure_count"] >= 2: # En az 2 kez başarısız olanları kaydet
|
|
81
|
+
f.write(json.dumps(data, ensure_ascii=False) + "\n")
|
|
82
|
+
|
|
83
|
+
def weekly_summary() -> dict:
|
|
84
|
+
"""Haftalık eval adayı özeti döner."""
|
|
85
|
+
collect_candidates() # Verileri güncelle
|
|
86
|
+
|
|
87
|
+
if not os.path.exists(EVAL_CANDIDATES_FILE):
|
|
88
|
+
return {"total_candidates": 0, "eligible_count": 0, "message": "Aday bulunamadı."}
|
|
89
|
+
|
|
90
|
+
candidates = []
|
|
91
|
+
with open(EVAL_CANDIDATES_FILE, "r", encoding="utf-8") as f:
|
|
92
|
+
for line in f:
|
|
93
|
+
if line.strip():
|
|
94
|
+
candidates.append(json.loads(line))
|
|
95
|
+
|
|
96
|
+
one_week_ago = datetime.now(timezone.utc) - timedelta(days=7)
|
|
97
|
+
|
|
98
|
+
recent_candidates = []
|
|
99
|
+
for c in candidates:
|
|
100
|
+
try:
|
|
101
|
+
ts = datetime.fromisoformat(c["last_seen"].replace("Z", "+00:00"))
|
|
102
|
+
if ts > one_week_ago:
|
|
103
|
+
recent_candidates.append(c)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
eligible_count = sum(1 for c in recent_candidates if c.get("auto_add_eligible"))
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"total_candidates": len(recent_candidates),
|
|
111
|
+
"eligible_count": eligible_count,
|
|
112
|
+
"message": f"Bu hafta {len(recent_candidates)} yeni eval adayı birikirdi, onaylamak ister misiniz? ({eligible_count} tanesi >=3 kez başarısız)"
|
|
113
|
+
}
|