errlore 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.
errlore/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """errlore -- memory for AI agents that learns from failures."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from errlore.facade import AgentMemory, Injection
6
+ from errlore.lessons.store import LessonStore
7
+ from errlore.trust import FeedbackSignal, TrustEngine
8
+
9
+ __all__ = [
10
+ "AgentMemory",
11
+ "FeedbackSignal",
12
+ "Injection",
13
+ "LessonStore",
14
+ "TrustEngine",
15
+ "__version__",
16
+ ]
@@ -0,0 +1,21 @@
1
+ """Error memory (Amygdala) — model weakness tracking, pattern detection, prompt injection.
2
+
3
+ Public API:
4
+ classify_error — regex extraction of ErrorType from exception/stacktrace/message
5
+ ErrorTracker — record_error / get_model_profile, persisted via errlore.io
6
+ PatternDetector — group (model, error_type, task_type), similarity with RU/EN stemming
7
+ WarningInjector — build_warning / get_penalty for prompt injection
8
+ sanitize_description — strip raw JSON / overlong text from descriptions
9
+ """
10
+
11
+ from errlore.errmem.classifier import classify_error
12
+ from errlore.errmem.injector import WarningInjector
13
+ from errlore.errmem.patterns import PatternDetector
14
+ from errlore.errmem.tracker import ErrorTracker
15
+
16
+ __all__ = [
17
+ "ErrorTracker",
18
+ "PatternDetector",
19
+ "WarningInjector",
20
+ "classify_error",
21
+ ]
@@ -0,0 +1,54 @@
1
+ """Auto-classification of error types from string messages and stacktraces.
2
+
3
+ Solves the generic "Error" problem (61 % of records in NEXUS audit) by
4
+ extracting a concrete type (ImportError, TimeoutError, etc.) from text.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ # Captures: ValueError, TimeoutError, ConnectionException, RuntimeWarning, etc.
12
+ _ERROR_TYPE_RE = re.compile(
13
+ r"\b([A-Z]\w*(?:Error|Exception|Warning|Timeout|Fault|Failure))\b",
14
+ )
15
+
16
+
17
+ def classify_error(
18
+ error: Exception | None = None,
19
+ message: str = "",
20
+ stacktrace: str = "",
21
+ ) -> str:
22
+ """Extract the most specific error type from available information.
23
+
24
+ Priority:
25
+ 1. Exception class name (if an object is provided).
26
+ 2. Last match in the stacktrace (most specific).
27
+ 3. First match in the message text.
28
+ 4. Fallback: ``"UnclassifiedError"``.
29
+
30
+ Args:
31
+ error: Exception object, if available.
32
+ message: Textual error description.
33
+ stacktrace: Full stacktrace string.
34
+
35
+ Returns:
36
+ Concrete error type name, e.g. ``"ImportError"``.
37
+ """
38
+ # 1. Direct extraction from exception object
39
+ if error is not None:
40
+ return type(error).__name__
41
+
42
+ # 2. Stacktrace — search from the end (last error is the most specific)
43
+ if stacktrace:
44
+ matches = _ERROR_TYPE_RE.findall(stacktrace)
45
+ if matches:
46
+ return str(matches[-1])
47
+
48
+ # 3. Message — first occurrence
49
+ if message:
50
+ match = _ERROR_TYPE_RE.search(message)
51
+ if match:
52
+ return match.group(1)
53
+
54
+ return "UnclassifiedError"
@@ -0,0 +1,156 @@
1
+ """Warning injector — builds prompt warnings from error memory.
2
+
3
+ Formats known model weaknesses and past errors into a structured
4
+ block suitable for system-prompt injection.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import re
11
+
12
+ from errlore.errmem.patterns import PatternDetector
13
+ from errlore.errmem.tracker import ErrorTracker
14
+ from errlore.sanitize import extract_readable_from_json
15
+
16
+ logger = logging.getLogger("errlore.errmem")
17
+
18
+ _MAX_DESCRIPTION_LEN = 200
19
+ _RAW_JSON_RE = re.compile(r"^\s*[\{`]")
20
+
21
+
22
+ def sanitize_description(text: str) -> str | None:
23
+ """Sanitize a description for human-readable prompt injection.
24
+
25
+ Rules:
26
+ - Strip to ``_MAX_DESCRIPTION_LEN`` characters.
27
+ - Collapse runs of whitespace into a single space.
28
+ - If the text looks like raw JSON (starts with ``{`` or backticks),
29
+ try to extract a readable ``"message"`` / ``"error"`` field;
30
+ otherwise discard the entry entirely (return ``None``).
31
+
32
+ Args:
33
+ text: Raw description string.
34
+
35
+ Returns:
36
+ Cleaned string, or ``None`` if the input is unsalvageable junk.
37
+ """
38
+ text = text.strip()
39
+ if not text:
40
+ return None
41
+
42
+ if _RAW_JSON_RE.match(text):
43
+ extracted = extract_readable_from_json(text)
44
+ if extracted is None:
45
+ return None
46
+ text = extracted
47
+
48
+ # B8: collapse whitespace (consistent with sanitize.py).
49
+ text = re.sub(r"\s+", " ", text).strip()
50
+
51
+ if len(text) > _MAX_DESCRIPTION_LEN:
52
+ text = text[: _MAX_DESCRIPTION_LEN - 3] + "..."
53
+
54
+ return text
55
+
56
+
57
+ class WarningInjector:
58
+ """Build prompt-injection warnings from error memory.
59
+
60
+ Combines model weaknesses (frequent error types) and past errors
61
+ on similar task types into a ``KNOWN ISSUES`` block.
62
+
63
+ auto_tuner_warnings from NEXUS are intentionally **not** ported
64
+ (dead bridge, NEXUS-specific coupling).
65
+
66
+ Args:
67
+ tracker: :class:`ErrorTracker` instance.
68
+ detector: :class:`PatternDetector` instance.
69
+ top_n: Maximum number of weakness lines to include.
70
+ Defaults to ``3``.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ tracker: ErrorTracker,
76
+ detector: PatternDetector,
77
+ *,
78
+ top_n: int = 3,
79
+ ) -> None:
80
+ self._tracker = tracker
81
+ self._detector = detector
82
+ self._top_n = top_n
83
+
84
+ def build_warning(self, model: str, task_type: str) -> str:
85
+ """Build a warning string for prompt injection.
86
+
87
+ Output format (verbatim)::
88
+
89
+ KNOWN ISSUES:
90
+ - {error_type} (x{count})
91
+ - Past error on similar task: {description}
92
+
93
+ Args:
94
+ model: Model name.
95
+ task_type: Task category.
96
+
97
+ Returns:
98
+ Warning string, or ``""`` if there is nothing to report.
99
+ """
100
+ # C2: single read of model_accuracy.jsonl instead of two separate
101
+ # calls (get_model_weaknesses + get_errors_for_task_type each did
102
+ # a full file read). The tracker's _read_all is now cached via
103
+ # JSONLWriter.read_all mtime+size cache, so the second call hits
104
+ # the cache automatically. No API change needed -- the cache in
105
+ # JSONLWriter handles this transparently.
106
+ weaknesses = self._tracker.get_model_weaknesses(model)
107
+ past_errors = self._tracker.get_errors_for_task_type(task_type)
108
+
109
+ if not weaknesses and not past_errors:
110
+ return ""
111
+
112
+ parts: list[str] = ["KNOWN ISSUES:"]
113
+
114
+ for w in weaknesses[: self._top_n]:
115
+ parts.append(f"- {w}")
116
+
117
+ for e in past_errors[:2]:
118
+ raw_desc = str(e.get("description", ""))
119
+ desc = sanitize_description(raw_desc)
120
+ if desc is None:
121
+ continue
122
+ parts.append(f"- Past error on similar task: {desc}")
123
+
124
+ # If we only have the header line (all past_errors were junk), skip
125
+ if len(parts) <= 1:
126
+ return ""
127
+
128
+ return "\n".join(parts)
129
+
130
+ def get_penalty(self, model: str, task_type: str) -> float:
131
+ """Calculate an error-history penalty for model routing.
132
+
133
+ Combines weakness count and past-error similarity into a
134
+ single ``[0.0, 1.0]`` score. Higher means more errors
135
+ on record for this ``(model, task_type)`` pair.
136
+
137
+ Args:
138
+ model: Model name.
139
+ task_type: Task category used as a similarity probe.
140
+
141
+ Returns:
142
+ Penalty score in ``[0.0, 1.0]``.
143
+ """
144
+ errors = self._tracker.get_model_errors(model)
145
+ if not errors:
146
+ return 0.0
147
+
148
+ # Component 1: weakness-based penalty (capped at 0.5)
149
+ weaknesses = self._tracker.get_model_weaknesses(model)
150
+ weakness_penalty = min(0.5, len(weaknesses) * 0.1)
151
+
152
+ # Component 2: similarity-based penalty (capped at 0.5)
153
+ similarity = self._detector.similarity_score(task_type, errors)
154
+ similarity_penalty = min(0.5, similarity * 0.5)
155
+
156
+ return min(1.0, weakness_penalty + similarity_penalty)
@@ -0,0 +1,193 @@
1
+ """Error pattern detection with RU/EN stemming-based similarity.
2
+
3
+ Groups errors by ``(model, error_type, task_type)`` and surfaces patterns
4
+ whose frequency meets a configurable threshold.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from collections import Counter
11
+ from typing import Any
12
+
13
+ _CYRILLIC_RE = re.compile(r"^[а-яёА-ЯЁ]+$")
14
+
15
+ _STOP_WORDS_RU = frozenset(
16
+ {
17
+ "и",
18
+ "в",
19
+ "на",
20
+ "с",
21
+ "по",
22
+ "для",
23
+ "из",
24
+ "к",
25
+ "от",
26
+ "до",
27
+ "не",
28
+ "но",
29
+ "а",
30
+ "что",
31
+ "как",
32
+ "это",
33
+ "при",
34
+ "за",
35
+ "или",
36
+ "же",
37
+ "ли",
38
+ "бы",
39
+ "то",
40
+ "его",
41
+ "её",
42
+ "их",
43
+ "он",
44
+ "она",
45
+ "мы",
46
+ "вы",
47
+ "они",
48
+ "был",
49
+ "быть",
50
+ "будет",
51
+ "если",
52
+ "так",
53
+ "уже",
54
+ "ещё",
55
+ "тоже",
56
+ "также",
57
+ "только",
58
+ "нет",
59
+ "да",
60
+ },
61
+ )
62
+
63
+ _STOP_WORDS_EN = frozenset(
64
+ {
65
+ "the",
66
+ "a",
67
+ "an",
68
+ "is",
69
+ "are",
70
+ "was",
71
+ "were",
72
+ "be",
73
+ "been",
74
+ "to",
75
+ "of",
76
+ "in",
77
+ "for",
78
+ "on",
79
+ "with",
80
+ "at",
81
+ "by",
82
+ "from",
83
+ "and",
84
+ "or",
85
+ "not",
86
+ "but",
87
+ "if",
88
+ "it",
89
+ "this",
90
+ "that",
91
+ },
92
+ )
93
+
94
+
95
+ def _stem_ru(word: str) -> str:
96
+ """Naive suffix stemmer for Russian words."""
97
+ if len(word) <= 4 or not _CYRILLIC_RE.match(word):
98
+ return word
99
+ for suffix_len in (3, 2):
100
+ if len(word) > suffix_len + 2:
101
+ return word[:-suffix_len]
102
+ return word
103
+
104
+
105
+ def _normalize_words(text: str) -> set[str]:
106
+ """Lowercase, stem, and remove stop-words for RU/EN text."""
107
+ words = set(text.lower().split())
108
+ words -= _STOP_WORDS_RU
109
+ words -= _STOP_WORDS_EN
110
+ return {_stem_ru(w) for w in words if len(w) > 1}
111
+
112
+
113
+ class PatternDetector:
114
+ """Detect recurring error patterns from a list of error entries.
115
+
116
+ Groups by ``(model, error_type, task_type)`` and returns groups
117
+ whose frequency is ``>= min_occurrences``.
118
+
119
+ Args:
120
+ min_occurrences: Minimum repeat count to be considered a pattern.
121
+ Defaults to ``3``.
122
+ """
123
+
124
+ def __init__(self, min_occurrences: int = 3) -> None:
125
+ self._min_occurrences = min_occurrences
126
+
127
+ def detect(self, errors: list[dict[str, Any]]) -> list[dict[str, Any]]:
128
+ """Detect recurring error patterns.
129
+
130
+ Args:
131
+ errors: List of error entry dicts.
132
+
133
+ Returns:
134
+ List of pattern dicts with ``model``, ``error_type``,
135
+ ``task_type``, and ``occurrences`` keys.
136
+ """
137
+ counter: Counter[tuple[str, str, str]] = Counter()
138
+ for e in errors:
139
+ key = (
140
+ str(e.get("model", "unknown")),
141
+ str(e.get("error_type", "unknown")),
142
+ str(e.get("task_type", "unknown")),
143
+ )
144
+ counter[key] += 1
145
+
146
+ patterns: list[dict[str, Any]] = []
147
+ for (model, error_type, task_type), count in counter.most_common():
148
+ if count >= self._min_occurrences:
149
+ patterns.append(
150
+ {
151
+ "model": model,
152
+ "error_type": error_type,
153
+ "task_type": task_type,
154
+ "occurrences": count,
155
+ },
156
+ )
157
+
158
+ return patterns
159
+
160
+ def similarity_score(
161
+ self,
162
+ task_description: str,
163
+ past_errors: list[dict[str, Any]],
164
+ ) -> float:
165
+ """Estimate similarity of a new task to past errors.
166
+
167
+ Uses word-overlap heuristic with RU/EN stemming and stop-word removal.
168
+
169
+ Args:
170
+ task_description: Description of the current task.
171
+ past_errors: List of past error entries.
172
+
173
+ Returns:
174
+ Similarity score in ``[0.0, 1.0]``.
175
+ """
176
+ if not past_errors or not task_description:
177
+ return 0.0
178
+
179
+ task_words = _normalize_words(task_description)
180
+ if not task_words:
181
+ return 0.0
182
+
183
+ max_overlap = 0.0
184
+ for error in past_errors:
185
+ description = str(error.get("description", ""))
186
+ task_type = str(error.get("task_type", ""))
187
+ error_words = _normalize_words(f"{description} {task_type}")
188
+ if not error_words:
189
+ continue
190
+ overlap = len(task_words & error_words) / len(task_words)
191
+ max_overlap = max(max_overlap, overlap)
192
+
193
+ return min(1.0, max_overlap)
@@ -0,0 +1,157 @@
1
+ """Model error tracker (Amygdala) — records errors and builds weakness profiles.
2
+
3
+ Persists data to ``model_accuracy.jsonl`` via :class:`errlore.io.JSONLWriter`.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import threading
10
+ from collections import Counter
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from errlore.io import JSONLWriter
16
+
17
+ logger = logging.getLogger("errlore.errmem")
18
+
19
+
20
+ class ErrorTracker:
21
+ """Track per-model errors and build weakness profiles.
22
+
23
+ All data is stored in a single append-only JSONL file
24
+ (``data_dir / "model_accuracy.jsonl"``).
25
+
26
+ Args:
27
+ data_dir: Directory for persistent storage.
28
+ min_occurrences: Minimum repeat count to consider something a weakness.
29
+ Defaults to ``3``.
30
+ writer: Optional :class:`JSONLWriter` instance (shared across modules).
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ data_dir: Path,
36
+ *,
37
+ min_occurrences: int = 3,
38
+ writer: JSONLWriter | None = None,
39
+ ) -> None:
40
+ self._data_dir = Path(data_dir)
41
+ self._data_dir.mkdir(parents=True, exist_ok=True)
42
+ self._accuracy_file = self._data_dir / "model_accuracy.jsonl"
43
+ self._lock = threading.Lock()
44
+ self._min_occurrences = min_occurrences
45
+ self._writer = writer or JSONLWriter()
46
+
47
+ # -- write ----------------------------------------------------------
48
+
49
+ def record_error(
50
+ self,
51
+ model: str,
52
+ task_type: str,
53
+ error: dict[str, Any],
54
+ ) -> dict[str, Any]:
55
+ """Record a model error.
56
+
57
+ Args:
58
+ model: Model name.
59
+ task_type: Task category.
60
+ error: Dict with ``type``, ``description``, ``severity`` keys.
61
+
62
+ Returns:
63
+ The persisted entry dict.
64
+ """
65
+ entry: dict[str, Any] = {
66
+ "timestamp": datetime.now(UTC).isoformat(),
67
+ "model": model,
68
+ "task_type": task_type,
69
+ "error_type": error.get("type", "unknown"),
70
+ "description": error.get("description", ""),
71
+ "severity": error.get("severity", "medium"),
72
+ }
73
+ self._writer.append(self._accuracy_file, entry)
74
+ return entry
75
+
76
+ # -- read -----------------------------------------------------------
77
+
78
+ def get_model_errors(self, model: str) -> list[dict[str, Any]]:
79
+ """Return all recorded errors for a specific model."""
80
+ all_entries = self._read_all()
81
+ return [e for e in all_entries if e.get("model") == model]
82
+
83
+ def get_errors_for_task_type(self, task_type: str) -> list[dict[str, Any]]:
84
+ """Return errors for a specific task type."""
85
+ all_entries = self._read_all()
86
+ return [e for e in all_entries if e.get("task_type") == task_type]
87
+
88
+ def get_model_weaknesses(
89
+ self,
90
+ model: str,
91
+ min_occurrences: int | None = None,
92
+ ) -> list[str]:
93
+ """Return known weaknesses for a model based on error patterns.
94
+
95
+ Args:
96
+ model: Model name.
97
+ min_occurrences: Override the instance default threshold.
98
+
99
+ Returns:
100
+ List of weakness descriptions like ``"TimeoutError (x5)"``.
101
+ """
102
+ if min_occurrences is None:
103
+ min_occurrences = self._min_occurrences
104
+
105
+ errors = self.get_model_errors(model)
106
+ if not errors:
107
+ return []
108
+
109
+ counter: Counter[str] = Counter()
110
+ for e in errors:
111
+ error_type = e.get("error_type", "unknown")
112
+ counter[error_type] += 1
113
+
114
+ weaknesses: list[str] = []
115
+ for error_type, count in counter.most_common():
116
+ if count >= min_occurrences:
117
+ weaknesses.append(f"{error_type} (x{count})")
118
+
119
+ return weaknesses
120
+
121
+ def get_model_profile(self, model: str) -> dict[str, Any]:
122
+ """Return an aggregate error profile for a model.
123
+
124
+ Returns:
125
+ Dict with ``errors`` (total count), ``last_error`` (ISO timestamp),
126
+ and ``weaknesses`` (list of weakness strings).
127
+ """
128
+ errors = self.get_model_errors(model)
129
+ profile: dict[str, Any] = {
130
+ "errors": len(errors),
131
+ "last_error": errors[-1].get("timestamp", "") if errors else "",
132
+ "weaknesses": self.get_model_weaknesses(model),
133
+ }
134
+ return profile
135
+
136
+ def get_model_stats(self) -> dict[str, dict[str, Any]]:
137
+ """Return per-model error statistics.
138
+
139
+ Returns:
140
+ ``{model_name: {"errors": N, "last_error": "timestamp"}}``.
141
+ """
142
+ stats: dict[str, dict[str, Any]] = {}
143
+ all_entries = self._read_all()
144
+ for entry in all_entries:
145
+ model = str(entry.get("model", "unknown"))
146
+ if model not in stats:
147
+ stats[model] = {"errors": 0, "last_error": ""}
148
+ stats[model]["errors"] += 1
149
+ stats[model]["last_error"] = str(entry.get("timestamp", ""))
150
+ return stats
151
+
152
+ # -- internals ------------------------------------------------------
153
+
154
+ def _read_all(self) -> list[dict[str, Any]]:
155
+ """Read all entries from the accuracy file (thread-safe)."""
156
+ with self._lock:
157
+ return self._writer.read_all(self._accuracy_file)