errorsense 0.1.0__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.
- errorsense-0.1.0/.gitignore +11 -0
- errorsense-0.1.0/LICENSE +21 -0
- errorsense-0.1.0/PKG-INFO +213 -0
- errorsense-0.1.0/README.md +185 -0
- errorsense-0.1.0/design/ERRORSENSE.md +340 -0
- errorsense-0.1.0/errorsense/__init__.py +27 -0
- errorsense-0.1.0/errorsense/engine.py +452 -0
- errorsense-0.1.0/errorsense/llm.py +201 -0
- errorsense-0.1.0/errorsense/models.py +52 -0
- errorsense-0.1.0/errorsense/phase.py +192 -0
- errorsense-0.1.0/errorsense/presets/__init__.py +5 -0
- errorsense-0.1.0/errorsense/presets/http_gateway.py +72 -0
- errorsense-0.1.0/errorsense/ruleset.py +165 -0
- errorsense-0.1.0/errorsense/signal.py +100 -0
- errorsense-0.1.0/errorsense/skill.py +70 -0
- errorsense-0.1.0/errorsense/skills/http_classifier.md +29 -0
- errorsense-0.1.0/errorsense/skills/reclassification.md +9 -0
- errorsense-0.1.0/pyproject.toml +36 -0
- errorsense-0.1.0/tests/test_engine.py +281 -0
- errorsense-0.1.0/tests/test_ruleset.py +180 -0
- errorsense-0.1.0/tests/test_signal.py +97 -0
- errorsense-0.1.0/tests/test_tracker.py +133 -0
errorsense-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenGPU
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: errorsense
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Error classification engine. Rules for the obvious, AI for the ambiguous.
|
|
5
|
+
Project-URL: Homepage, https://github.com/opengpu/errorsense
|
|
6
|
+
Project-URL: Documentation, https://github.com/opengpu/errorsense#readme
|
|
7
|
+
Author-email: Can Atılgan <can@opengpu.network>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: circuit-breaker,error-classification,llm,observability
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
25
|
+
Provides-Extra: llm
|
|
26
|
+
Requires-Dist: httpx>=0.25; extra == 'llm'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# ErrorSense
|
|
30
|
+
|
|
31
|
+
Error classification engine. Rules for the obvious, LLM for the ambiguous.
|
|
32
|
+
|
|
33
|
+
Most errors are easy to classify — a 400 is a client error, a 502 is a server error. But some aren't — a 500 with "model not found" in the body is actually a client error, not a server failure. Your rules can't catch every edge case. An LLM can.
|
|
34
|
+
|
|
35
|
+
ErrorSense runs errors through a phase pipeline: fast deterministic rulesets first, LLM only when rulesets can't decide. Most errors never hit the LLM. The ones that do get classified correctly instead of falling through as "unknown."
|
|
36
|
+
|
|
37
|
+
**Use it for:** circuit breakers, alert routing, retry logic, error dashboards; anywhere you need to know *what kind* of error happened, not just *that* it happened.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install errorsense # core only (zero dependencies)
|
|
43
|
+
pip install errorsense[llm] # + LLM classification
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start — Use a Preset
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from errorsense.presets import http
|
|
50
|
+
from errorsense import LLMConfig, Signal
|
|
51
|
+
|
|
52
|
+
sense = http(llm=LLMConfig(api_key="your_api_key"))
|
|
53
|
+
|
|
54
|
+
results = sense.classify(Signal.from_http(status_code=400, body="bad request"))
|
|
55
|
+
results[0].label # "client"
|
|
56
|
+
|
|
57
|
+
results = sense.classify(Signal.from_http(status_code=502))
|
|
58
|
+
results[0].label # "server"
|
|
59
|
+
|
|
60
|
+
results = sense.classify(Signal.from_http(status_code=500, body="model not found"))
|
|
61
|
+
results[0].label # "client" (LLM figured it out)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The `http` preset gives you a 3-phase pipeline (rules → patterns → LLM) with 3 categories: `"client"`, `"server"`, `"undecided"`. Rulesets handle obvious cases instantly. LLM handles the ambiguous ones.
|
|
65
|
+
|
|
66
|
+
Don't want LLM? Use `http_no_llm()` — rulesets only, ambiguous errors come back as `"undecided"`.
|
|
67
|
+
|
|
68
|
+
## Build Your Own Pipeline
|
|
69
|
+
|
|
70
|
+
A pipeline is a list of phases. Each phase has rulesets (deterministic) or skills (LLM). You can mix both, use only rulesets, or use only skills.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from errorsense import ErrorSense, Phase, Ruleset, Skill, LLMConfig, Signal
|
|
74
|
+
|
|
75
|
+
# Rulesets + LLM
|
|
76
|
+
sense = ErrorSense(
|
|
77
|
+
categories=["transient", "permanent", "user"],
|
|
78
|
+
pipeline=[
|
|
79
|
+
Phase("codes", rulesets=[
|
|
80
|
+
Ruleset(field="error_code", match={
|
|
81
|
+
"ECONNRESET": "transient", "ETIMEOUT": "transient", "EPERM": "permanent",
|
|
82
|
+
}),
|
|
83
|
+
]),
|
|
84
|
+
Phase("patterns", rulesets=[
|
|
85
|
+
Ruleset(field="message", patterns=[
|
|
86
|
+
("transient", [r"timeout", r"connection reset", r"retry"]),
|
|
87
|
+
("permanent", [r"corruption", r"fatal"]),
|
|
88
|
+
]),
|
|
89
|
+
]),
|
|
90
|
+
Phase("llm", skills=[
|
|
91
|
+
Skill("my_classifier", path="./skills/my_classifier.md"),
|
|
92
|
+
], llm=LLMConfig(api_key="your_key")),
|
|
93
|
+
],
|
|
94
|
+
default="transient",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Rulesets only — no LLM needed
|
|
98
|
+
sense = ErrorSense(
|
|
99
|
+
categories=["client", "server"],
|
|
100
|
+
pipeline=[
|
|
101
|
+
Phase("rules", rulesets=[
|
|
102
|
+
Ruleset(field="status_code", match={"4xx": "client", 502: "server"}),
|
|
103
|
+
]),
|
|
104
|
+
],
|
|
105
|
+
default="server",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# LLM only — skip rulesets entirely
|
|
109
|
+
sense = ErrorSense(
|
|
110
|
+
categories=["client", "server"],
|
|
111
|
+
pipeline=[
|
|
112
|
+
Phase("llm", skills=[
|
|
113
|
+
Skill("my_classifier", path="./skills/my_classifier.md"),
|
|
114
|
+
], llm=LLMConfig(api_key="your_key")),
|
|
115
|
+
],
|
|
116
|
+
default="unknown",
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Phases run in order. First match wins. Rulesets are instant and free. LLM is the fallback.
|
|
121
|
+
|
|
122
|
+
## Rulesets
|
|
123
|
+
|
|
124
|
+
Each ruleset does one thing — `match=` for field matching or `patterns=` for regex:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
Ruleset(field="status_code", match={400: "client", 502: "server"}) # exact match
|
|
128
|
+
Ruleset(field="status_code", match={"4xx": "client", 503: "server"}) # range match
|
|
129
|
+
Ruleset(field="headers.content-type", match={"text/html": "server"}) # header match
|
|
130
|
+
Ruleset(field="body.error.type", match={"validation_error": "client"}) # JSON dot-path
|
|
131
|
+
Ruleset(field="body", patterns=[("server", [r"OOM"]), ("client", [r"invalid"])]) # regex
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Custom logic? Subclass:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
class VendorBugRuleset(Ruleset):
|
|
138
|
+
def classify(self, signal: Signal) -> SenseResult | None:
|
|
139
|
+
if signal.get("vendor") == "acme" and signal.get("code") == "X99":
|
|
140
|
+
return SenseResult(label="known_bug", confidence=1.0)
|
|
141
|
+
return None
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Skills
|
|
145
|
+
|
|
146
|
+
Skills are LLM instructions stored as `.md` files. Each skill teaches the LLM how to classify errors in a specific domain.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# Loads from errorsense/skills/http_classifier.md (built-in)
|
|
150
|
+
Skill("http_classifier")
|
|
151
|
+
|
|
152
|
+
# Loads from your own file
|
|
153
|
+
Skill("my_classifier", path="./skills/my_classifier.md")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## All Phases Mode
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# Default — stops at first match
|
|
160
|
+
results = sense.classify(signal)
|
|
161
|
+
|
|
162
|
+
# All phases run
|
|
163
|
+
results = sense.classify(signal, short_circuit=False)
|
|
164
|
+
|
|
165
|
+
# With LLM reasoning
|
|
166
|
+
results = sense.classify(signal, explain=True)
|
|
167
|
+
results[0].reason # "ECONNRESET indicates transient network failure"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Trailing (Stateful Error Tracking)
|
|
171
|
+
|
|
172
|
+
Track errors per key. When a threshold is hit, the LLM reviews the full error history.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from errorsense import TrailingConfig
|
|
176
|
+
|
|
177
|
+
sense = ErrorSense(
|
|
178
|
+
categories=["transient", "permanent", "user"],
|
|
179
|
+
pipeline=[...],
|
|
180
|
+
trailing=TrailingConfig(
|
|
181
|
+
threshold=3,
|
|
182
|
+
count_labels=["transient", "permanent"], # user errors don't count
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# In your error handler:
|
|
187
|
+
result = sense.trail("service-a", signal)
|
|
188
|
+
result.label # "transient"
|
|
189
|
+
result.at_threshold # True (3rd counted error)
|
|
190
|
+
result.reason # LLM review: "3 transient errors — all connection resets..."
|
|
191
|
+
|
|
192
|
+
# On success:
|
|
193
|
+
sense.reset("service-a")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**How it works:**
|
|
197
|
+
- Each `trail()` call classifies the signal normally through the pipeline
|
|
198
|
+
- Counted labels accumulate per key toward the threshold
|
|
199
|
+
- At threshold, the LLM reviews all recorded errors and gives its verdict
|
|
200
|
+
- If the review changes the label, the history entry is corrected and the count adjusts
|
|
201
|
+
- `review=False` in TrailingConfig disables LLM review (just counting)
|
|
202
|
+
|
|
203
|
+
**Manual review anytime:**
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
verdict = sense.review("service-a")
|
|
207
|
+
verdict.label # LLM's verdict on the full history
|
|
208
|
+
verdict.reason # explanation
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# ErrorSense
|
|
2
|
+
|
|
3
|
+
Error classification engine. Rules for the obvious, LLM for the ambiguous.
|
|
4
|
+
|
|
5
|
+
Most errors are easy to classify — a 400 is a client error, a 502 is a server error. But some aren't — a 500 with "model not found" in the body is actually a client error, not a server failure. Your rules can't catch every edge case. An LLM can.
|
|
6
|
+
|
|
7
|
+
ErrorSense runs errors through a phase pipeline: fast deterministic rulesets first, LLM only when rulesets can't decide. Most errors never hit the LLM. The ones that do get classified correctly instead of falling through as "unknown."
|
|
8
|
+
|
|
9
|
+
**Use it for:** circuit breakers, alert routing, retry logic, error dashboards; anywhere you need to know *what kind* of error happened, not just *that* it happened.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install errorsense # core only (zero dependencies)
|
|
15
|
+
pip install errorsense[llm] # + LLM classification
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start — Use a Preset
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from errorsense.presets import http
|
|
22
|
+
from errorsense import LLMConfig, Signal
|
|
23
|
+
|
|
24
|
+
sense = http(llm=LLMConfig(api_key="your_api_key"))
|
|
25
|
+
|
|
26
|
+
results = sense.classify(Signal.from_http(status_code=400, body="bad request"))
|
|
27
|
+
results[0].label # "client"
|
|
28
|
+
|
|
29
|
+
results = sense.classify(Signal.from_http(status_code=502))
|
|
30
|
+
results[0].label # "server"
|
|
31
|
+
|
|
32
|
+
results = sense.classify(Signal.from_http(status_code=500, body="model not found"))
|
|
33
|
+
results[0].label # "client" (LLM figured it out)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The `http` preset gives you a 3-phase pipeline (rules → patterns → LLM) with 3 categories: `"client"`, `"server"`, `"undecided"`. Rulesets handle obvious cases instantly. LLM handles the ambiguous ones.
|
|
37
|
+
|
|
38
|
+
Don't want LLM? Use `http_no_llm()` — rulesets only, ambiguous errors come back as `"undecided"`.
|
|
39
|
+
|
|
40
|
+
## Build Your Own Pipeline
|
|
41
|
+
|
|
42
|
+
A pipeline is a list of phases. Each phase has rulesets (deterministic) or skills (LLM). You can mix both, use only rulesets, or use only skills.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from errorsense import ErrorSense, Phase, Ruleset, Skill, LLMConfig, Signal
|
|
46
|
+
|
|
47
|
+
# Rulesets + LLM
|
|
48
|
+
sense = ErrorSense(
|
|
49
|
+
categories=["transient", "permanent", "user"],
|
|
50
|
+
pipeline=[
|
|
51
|
+
Phase("codes", rulesets=[
|
|
52
|
+
Ruleset(field="error_code", match={
|
|
53
|
+
"ECONNRESET": "transient", "ETIMEOUT": "transient", "EPERM": "permanent",
|
|
54
|
+
}),
|
|
55
|
+
]),
|
|
56
|
+
Phase("patterns", rulesets=[
|
|
57
|
+
Ruleset(field="message", patterns=[
|
|
58
|
+
("transient", [r"timeout", r"connection reset", r"retry"]),
|
|
59
|
+
("permanent", [r"corruption", r"fatal"]),
|
|
60
|
+
]),
|
|
61
|
+
]),
|
|
62
|
+
Phase("llm", skills=[
|
|
63
|
+
Skill("my_classifier", path="./skills/my_classifier.md"),
|
|
64
|
+
], llm=LLMConfig(api_key="your_key")),
|
|
65
|
+
],
|
|
66
|
+
default="transient",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Rulesets only — no LLM needed
|
|
70
|
+
sense = ErrorSense(
|
|
71
|
+
categories=["client", "server"],
|
|
72
|
+
pipeline=[
|
|
73
|
+
Phase("rules", rulesets=[
|
|
74
|
+
Ruleset(field="status_code", match={"4xx": "client", 502: "server"}),
|
|
75
|
+
]),
|
|
76
|
+
],
|
|
77
|
+
default="server",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# LLM only — skip rulesets entirely
|
|
81
|
+
sense = ErrorSense(
|
|
82
|
+
categories=["client", "server"],
|
|
83
|
+
pipeline=[
|
|
84
|
+
Phase("llm", skills=[
|
|
85
|
+
Skill("my_classifier", path="./skills/my_classifier.md"),
|
|
86
|
+
], llm=LLMConfig(api_key="your_key")),
|
|
87
|
+
],
|
|
88
|
+
default="unknown",
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Phases run in order. First match wins. Rulesets are instant and free. LLM is the fallback.
|
|
93
|
+
|
|
94
|
+
## Rulesets
|
|
95
|
+
|
|
96
|
+
Each ruleset does one thing — `match=` for field matching or `patterns=` for regex:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
Ruleset(field="status_code", match={400: "client", 502: "server"}) # exact match
|
|
100
|
+
Ruleset(field="status_code", match={"4xx": "client", 503: "server"}) # range match
|
|
101
|
+
Ruleset(field="headers.content-type", match={"text/html": "server"}) # header match
|
|
102
|
+
Ruleset(field="body.error.type", match={"validation_error": "client"}) # JSON dot-path
|
|
103
|
+
Ruleset(field="body", patterns=[("server", [r"OOM"]), ("client", [r"invalid"])]) # regex
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Custom logic? Subclass:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
class VendorBugRuleset(Ruleset):
|
|
110
|
+
def classify(self, signal: Signal) -> SenseResult | None:
|
|
111
|
+
if signal.get("vendor") == "acme" and signal.get("code") == "X99":
|
|
112
|
+
return SenseResult(label="known_bug", confidence=1.0)
|
|
113
|
+
return None
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Skills
|
|
117
|
+
|
|
118
|
+
Skills are LLM instructions stored as `.md` files. Each skill teaches the LLM how to classify errors in a specific domain.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Loads from errorsense/skills/http_classifier.md (built-in)
|
|
122
|
+
Skill("http_classifier")
|
|
123
|
+
|
|
124
|
+
# Loads from your own file
|
|
125
|
+
Skill("my_classifier", path="./skills/my_classifier.md")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## All Phases Mode
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# Default — stops at first match
|
|
132
|
+
results = sense.classify(signal)
|
|
133
|
+
|
|
134
|
+
# All phases run
|
|
135
|
+
results = sense.classify(signal, short_circuit=False)
|
|
136
|
+
|
|
137
|
+
# With LLM reasoning
|
|
138
|
+
results = sense.classify(signal, explain=True)
|
|
139
|
+
results[0].reason # "ECONNRESET indicates transient network failure"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Trailing (Stateful Error Tracking)
|
|
143
|
+
|
|
144
|
+
Track errors per key. When a threshold is hit, the LLM reviews the full error history.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from errorsense import TrailingConfig
|
|
148
|
+
|
|
149
|
+
sense = ErrorSense(
|
|
150
|
+
categories=["transient", "permanent", "user"],
|
|
151
|
+
pipeline=[...],
|
|
152
|
+
trailing=TrailingConfig(
|
|
153
|
+
threshold=3,
|
|
154
|
+
count_labels=["transient", "permanent"], # user errors don't count
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# In your error handler:
|
|
159
|
+
result = sense.trail("service-a", signal)
|
|
160
|
+
result.label # "transient"
|
|
161
|
+
result.at_threshold # True (3rd counted error)
|
|
162
|
+
result.reason # LLM review: "3 transient errors — all connection resets..."
|
|
163
|
+
|
|
164
|
+
# On success:
|
|
165
|
+
sense.reset("service-a")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**How it works:**
|
|
169
|
+
- Each `trail()` call classifies the signal normally through the pipeline
|
|
170
|
+
- Counted labels accumulate per key toward the threshold
|
|
171
|
+
- At threshold, the LLM reviews all recorded errors and gives its verdict
|
|
172
|
+
- If the review changes the label, the history entry is corrected and the count adjusts
|
|
173
|
+
- `review=False` in TrailingConfig disables LLM review (just counting)
|
|
174
|
+
|
|
175
|
+
**Manual review anytime:**
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
verdict = sense.review("service-a")
|
|
179
|
+
verdict.label # LLM's verdict on the full history
|
|
180
|
+
verdict.reason # explanation
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|