kalytera 0.1.0__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.
- agentiq/__init__.py +6 -0
- agentiq/analyzer.py +306 -0
- agentiq/config.py +5 -0
- agentiq/dashboard.py +1343 -0
- agentiq/judge.py +258 -0
- agentiq/prompts.py +138 -0
- agentiq/tracer.py +141 -0
- kalytera-0.1.0.dist-info/METADATA +350 -0
- kalytera-0.1.0.dist-info/RECORD +10 -0
- kalytera-0.1.0.dist-info/WHEEL +4 -0
agentiq/__init__.py
ADDED
agentiq/analyzer.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentiq/analyzer.py — hourly loss pattern detection job.
|
|
3
|
+
Runs after the judge; never in the trace path.
|
|
4
|
+
|
|
5
|
+
Public API:
|
|
6
|
+
run_analysis(agent_id, db) — detect patterns for one agent, write LossPattern rows
|
|
7
|
+
run_all(db) — run for every agent with new EvalResults since last run
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
from typing import Any, Dict, Generator, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
MIN_FAILURE_COUNT = 5 # only surface patterns with ≥5 failures
|
|
18
|
+
ANALYSIS_WINDOW_DAYS = 7 # look back this many days for current failure rate
|
|
19
|
+
WORSENING_WINDOW_DAYS = 7 # compare current vs prior window to flag worsening
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Public API
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
def run_analysis(agent_id: str, db: Any) -> int:
|
|
27
|
+
"""
|
|
28
|
+
Detect patterns for one agent and upsert LossPattern rows.
|
|
29
|
+
Returns the number of patterns written. Idempotent.
|
|
30
|
+
"""
|
|
31
|
+
from db.models import EvalResult
|
|
32
|
+
|
|
33
|
+
start_time = datetime.now(timezone.utc)
|
|
34
|
+
logger.info("[analyzer] start agent=%s", agent_id)
|
|
35
|
+
|
|
36
|
+
all_failures = _fetch_failures(agent_id, db)
|
|
37
|
+
total_evals = _count_total_evals(agent_id, db)
|
|
38
|
+
total_failures = len(all_failures)
|
|
39
|
+
|
|
40
|
+
if total_failures == 0:
|
|
41
|
+
logger.info("[analyzer] done agent=%s failures=0 patterns=0", agent_id)
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
patterns_written = 0
|
|
45
|
+
for ptype, pvalue, group in _group_failures(all_failures):
|
|
46
|
+
if len(group) < MIN_FAILURE_COUNT:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
total_for_group = _total_for_group(ptype, pvalue, agent_id, total_evals, db)
|
|
50
|
+
failure_rate = len(group) / total_for_group if total_for_group > 0 else 0.0
|
|
51
|
+
pct_of_all = len(group) / total_failures if total_failures > 0 else 0.0
|
|
52
|
+
root_cause = _most_common_reason(group)
|
|
53
|
+
is_worsening = _check_worsening(ptype, pvalue, agent_id, db)
|
|
54
|
+
|
|
55
|
+
_upsert_pattern(
|
|
56
|
+
agent_id=agent_id,
|
|
57
|
+
pattern_type=ptype,
|
|
58
|
+
pattern_value=pvalue,
|
|
59
|
+
failure_count=len(group),
|
|
60
|
+
total_count=total_for_group,
|
|
61
|
+
failure_rate=round(failure_rate, 4),
|
|
62
|
+
pct_of_all_failures=round(pct_of_all, 4),
|
|
63
|
+
root_cause=root_cause,
|
|
64
|
+
is_worsening=is_worsening,
|
|
65
|
+
first_seen=min(r.evaluated_at for r in group),
|
|
66
|
+
last_seen=max(r.evaluated_at for r in group),
|
|
67
|
+
db=db,
|
|
68
|
+
)
|
|
69
|
+
patterns_written += 1
|
|
70
|
+
|
|
71
|
+
elapsed = (datetime.now(timezone.utc) - start_time).total_seconds()
|
|
72
|
+
logger.info(
|
|
73
|
+
"[analyzer] done agent=%s failures=%d patterns=%d elapsed=%.2fs",
|
|
74
|
+
agent_id, total_failures, patterns_written, elapsed,
|
|
75
|
+
)
|
|
76
|
+
return patterns_written
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_all(db: Any) -> Dict[str, int]:
|
|
80
|
+
"""
|
|
81
|
+
Run pattern detection for every agent that has EvalResult rows.
|
|
82
|
+
Returns {agent_id: patterns_written}.
|
|
83
|
+
"""
|
|
84
|
+
from db.models import EvalResult
|
|
85
|
+
|
|
86
|
+
start_time = datetime.now(timezone.utc)
|
|
87
|
+
logger.info("[analyzer] run_all start")
|
|
88
|
+
|
|
89
|
+
agent_ids: List[str] = [
|
|
90
|
+
row[0] for row in db.query(EvalResult.agent_id).distinct().all()
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
results: Dict[str, int] = {}
|
|
94
|
+
for agent_id in agent_ids:
|
|
95
|
+
try:
|
|
96
|
+
results[agent_id] = run_analysis(agent_id, db)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.error("[analyzer] agent=%s error: %s", agent_id, exc)
|
|
99
|
+
results[agent_id] = 0
|
|
100
|
+
|
|
101
|
+
elapsed = (datetime.now(timezone.utc) - start_time).total_seconds()
|
|
102
|
+
logger.info(
|
|
103
|
+
"[analyzer] run_all done agents=%d total_patterns=%d elapsed=%.2fs",
|
|
104
|
+
len(agent_ids), sum(results.values()), elapsed,
|
|
105
|
+
)
|
|
106
|
+
return results
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Private helpers — pure functions (easy to test without DB)
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _group_failures(
|
|
114
|
+
failures: List[Any],
|
|
115
|
+
) -> Generator[Tuple[str, str, List[Any]], None, None]:
|
|
116
|
+
"""
|
|
117
|
+
Yield (pattern_type, pattern_value, group) for two pattern types:
|
|
118
|
+
- workflow_step: grouped by failure_step
|
|
119
|
+
- failure_type: grouped by failure_type string
|
|
120
|
+
"""
|
|
121
|
+
by_step: Dict[str, List[Any]] = {}
|
|
122
|
+
by_type: Dict[str, List[Any]] = {}
|
|
123
|
+
|
|
124
|
+
for row in failures:
|
|
125
|
+
if row.failure_step is not None:
|
|
126
|
+
key = f"step_{row.failure_step}"
|
|
127
|
+
by_step.setdefault(key, []).append(row)
|
|
128
|
+
if row.failure_type:
|
|
129
|
+
by_type.setdefault(row.failure_type, []).append(row)
|
|
130
|
+
|
|
131
|
+
for pvalue, group in by_step.items():
|
|
132
|
+
yield "workflow_step", pvalue, group
|
|
133
|
+
|
|
134
|
+
for pvalue, group in by_type.items():
|
|
135
|
+
yield "failure_type", pvalue, group
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _most_common_reason(group: List[Any]) -> Optional[str]:
|
|
139
|
+
"""Return the most frequent non-empty failure_reason in the group."""
|
|
140
|
+
reasons = [r.failure_reason for r in group if r.failure_reason]
|
|
141
|
+
if not reasons:
|
|
142
|
+
return None
|
|
143
|
+
return Counter(reasons).most_common(1)[0][0]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _check_worsening(
|
|
147
|
+
pattern_type: str,
|
|
148
|
+
pattern_value: str,
|
|
149
|
+
agent_id: str,
|
|
150
|
+
db: Any,
|
|
151
|
+
) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
Compare failure rate in current 7-day window vs prior 7-day window.
|
|
154
|
+
Returns True if the current rate is higher (pattern is getting worse).
|
|
155
|
+
"""
|
|
156
|
+
from db.models import EvalResult
|
|
157
|
+
|
|
158
|
+
now = datetime.now(timezone.utc)
|
|
159
|
+
current_start = now - timedelta(days=WORSENING_WINDOW_DAYS)
|
|
160
|
+
prior_start = current_start - timedelta(days=WORSENING_WINDOW_DAYS)
|
|
161
|
+
|
|
162
|
+
def failure_rate_in_window(start: datetime, end: datetime) -> float:
|
|
163
|
+
total = (
|
|
164
|
+
db.query(EvalResult)
|
|
165
|
+
.filter(
|
|
166
|
+
EvalResult.agent_id == agent_id,
|
|
167
|
+
EvalResult.evaluated_at >= start,
|
|
168
|
+
EvalResult.evaluated_at < end,
|
|
169
|
+
)
|
|
170
|
+
.count()
|
|
171
|
+
)
|
|
172
|
+
if total == 0:
|
|
173
|
+
return 0.0
|
|
174
|
+
failed = _count_group_in_window(pattern_type, pattern_value, agent_id, start, end, db)
|
|
175
|
+
return failed / total
|
|
176
|
+
|
|
177
|
+
current_rate = failure_rate_in_window(current_start, now)
|
|
178
|
+
prior_rate = failure_rate_in_window(prior_start, current_start)
|
|
179
|
+
return current_rate > prior_rate
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _count_group_in_window(
|
|
183
|
+
pattern_type: str,
|
|
184
|
+
pattern_value: str,
|
|
185
|
+
agent_id: str,
|
|
186
|
+
start: datetime,
|
|
187
|
+
end: datetime,
|
|
188
|
+
db: Any,
|
|
189
|
+
) -> int:
|
|
190
|
+
from db.models import EvalResult
|
|
191
|
+
|
|
192
|
+
q = (
|
|
193
|
+
db.query(EvalResult)
|
|
194
|
+
.filter(
|
|
195
|
+
EvalResult.agent_id == agent_id,
|
|
196
|
+
EvalResult.passed == False, # noqa: E712
|
|
197
|
+
EvalResult.evaluated_at >= start,
|
|
198
|
+
EvalResult.evaluated_at < end,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
if pattern_type == "workflow_step":
|
|
202
|
+
step_num = int(pattern_value.split("_")[1])
|
|
203
|
+
q = q.filter(EvalResult.failure_step == step_num)
|
|
204
|
+
elif pattern_type == "failure_type":
|
|
205
|
+
q = q.filter(EvalResult.failure_type == pattern_value)
|
|
206
|
+
return q.count()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _upsert_pattern(
|
|
210
|
+
*,
|
|
211
|
+
agent_id: str,
|
|
212
|
+
pattern_type: str,
|
|
213
|
+
pattern_value: str,
|
|
214
|
+
failure_count: int,
|
|
215
|
+
total_count: int,
|
|
216
|
+
failure_rate: float,
|
|
217
|
+
pct_of_all_failures: float,
|
|
218
|
+
root_cause: Optional[str],
|
|
219
|
+
is_worsening: bool,
|
|
220
|
+
first_seen: datetime,
|
|
221
|
+
last_seen: datetime,
|
|
222
|
+
db: Any,
|
|
223
|
+
) -> None:
|
|
224
|
+
from db.models import LossPattern
|
|
225
|
+
|
|
226
|
+
existing = (
|
|
227
|
+
db.query(LossPattern)
|
|
228
|
+
.filter(
|
|
229
|
+
LossPattern.agent_id == agent_id,
|
|
230
|
+
LossPattern.pattern_type == pattern_type,
|
|
231
|
+
LossPattern.pattern_value == pattern_value,
|
|
232
|
+
)
|
|
233
|
+
.first()
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if existing is not None:
|
|
237
|
+
existing.failure_count = failure_count
|
|
238
|
+
existing.total_count = total_count
|
|
239
|
+
existing.failure_rate = failure_rate
|
|
240
|
+
existing.pct_of_all_failures = pct_of_all_failures
|
|
241
|
+
existing.root_cause = root_cause
|
|
242
|
+
existing.is_worsening = is_worsening
|
|
243
|
+
existing.last_seen = last_seen
|
|
244
|
+
# first_seen is immutable once set
|
|
245
|
+
else:
|
|
246
|
+
db.add(LossPattern(
|
|
247
|
+
id=str(uuid.uuid4()),
|
|
248
|
+
agent_id=agent_id,
|
|
249
|
+
pattern_type=pattern_type,
|
|
250
|
+
pattern_value=pattern_value,
|
|
251
|
+
failure_count=failure_count,
|
|
252
|
+
total_count=total_count,
|
|
253
|
+
failure_rate=failure_rate,
|
|
254
|
+
pct_of_all_failures=pct_of_all_failures,
|
|
255
|
+
root_cause=root_cause,
|
|
256
|
+
is_worsening=is_worsening,
|
|
257
|
+
first_seen=first_seen,
|
|
258
|
+
last_seen=last_seen,
|
|
259
|
+
))
|
|
260
|
+
|
|
261
|
+
db.commit()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _fetch_failures(agent_id: str, db: Any) -> List[Any]:
|
|
265
|
+
from db.models import EvalResult
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
db.query(EvalResult)
|
|
269
|
+
.filter(
|
|
270
|
+
EvalResult.agent_id == agent_id,
|
|
271
|
+
EvalResult.passed == False, # noqa: E712
|
|
272
|
+
EvalResult.eval_error == False, # noqa: E712
|
|
273
|
+
)
|
|
274
|
+
.all()
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _count_total_evals(agent_id: str, db: Any) -> int:
|
|
279
|
+
from db.models import EvalResult
|
|
280
|
+
|
|
281
|
+
return db.query(EvalResult).filter(EvalResult.agent_id == agent_id).count()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _total_for_group(
|
|
285
|
+
pattern_type: str,
|
|
286
|
+
pattern_value: str,
|
|
287
|
+
agent_id: str,
|
|
288
|
+
total_evals: int,
|
|
289
|
+
db: Any,
|
|
290
|
+
) -> int:
|
|
291
|
+
from db.models import EvalResult
|
|
292
|
+
|
|
293
|
+
if pattern_type == "failure_type":
|
|
294
|
+
return total_evals
|
|
295
|
+
if pattern_type == "workflow_step":
|
|
296
|
+
step_num = int(pattern_value.split("_")[1])
|
|
297
|
+
return (
|
|
298
|
+
db.query(EvalResult)
|
|
299
|
+
.filter(
|
|
300
|
+
EvalResult.agent_id == agent_id,
|
|
301
|
+
EvalResult.failure_step == step_num,
|
|
302
|
+
)
|
|
303
|
+
.count()
|
|
304
|
+
or total_evals
|
|
305
|
+
)
|
|
306
|
+
return total_evals
|