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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ *.pyc
8
+ .DS_Store
9
+ .claude
10
+ relay_preset/
11
+ .env
@@ -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