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 ADDED
@@ -0,0 +1,6 @@
1
+ from agentiq.tracer import init, trace, watch
2
+
3
+ # Standard SDK convention alias — agentiq.configure() == agentiq.init()
4
+ configure = init
5
+
6
+ __all__ = ["init", "configure", "trace", "watch"]
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
agentiq/config.py ADDED
@@ -0,0 +1,5 @@
1
+ import os
2
+
3
+ DEFAULT_ENDPOINT: str = os.getenv("AGENTIQ_API_ENDPOINT", "http://localhost:8000")
4
+ REQUEST_TIMEOUT: float = 5.0
5
+ ERROR_LOG_PATH: str = os.path.expanduser("~/.agentiq/errors.log")