django-explain-errors 0.5.0__tar.gz → 0.6.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.
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/PKG-INFO +37 -9
- django_explain_errors-0.5.0/django_explain_errors.egg-info/PKG-INFO → django_explain_errors-0.6.0/README.md +35 -51
- django_explain_errors-0.5.0/README.md → django_explain_errors-0.6.0/django_explain_errors.egg-info/PKG-INFO +79 -1
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/django_explain_errors.egg-info/SOURCES.txt +1 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/middleware.py +40 -2
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/setup.py +2 -8
- django_explain_errors-0.6.0/tests/test_language.py +158 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/LICENSE +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/MANIFEST.in +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/django_explain_errors.egg-info/dependency_links.txt +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/django_explain_errors.egg-info/requires.txt +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/django_explain_errors.egg-info/top_level.txt +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/__init__.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/client.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/management/__init__.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/management/commands/__init__.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/management/commands/build_error_index.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/rag/__init__.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/rag/indexer.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/rag/retriever.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/rag/store.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/sanitize.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/throttle.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/setup.cfg +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_build_error_index.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_client.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_middleware.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_rag.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_sanitize.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_signals.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_throttle.py +0 -0
- {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_truncation.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-explain-errors
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Django middleware that
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.
|
|
5
5
|
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
6
|
Author: topunix
|
|
7
7
|
Author-email: topunixguy@gmail.com
|
|
@@ -9,12 +9,6 @@ License: MIT
|
|
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
|
10
10
|
Classifier: Environment :: Web Environment
|
|
11
11
|
Classifier: Framework :: Django
|
|
12
|
-
Classifier: Framework :: Django :: 4.2
|
|
13
|
-
Classifier: Framework :: Django :: 5.0
|
|
14
|
-
Classifier: Framework :: Django :: 5.1
|
|
15
|
-
Classifier: Framework :: Django :: 5.2
|
|
16
|
-
Classifier: Framework :: Django :: 6.0
|
|
17
|
-
Classifier: Framework :: Django :: 6.1
|
|
18
12
|
Classifier: Intended Audience :: Developers
|
|
19
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
20
14
|
Classifier: Operating System :: OS Independent
|
|
@@ -86,6 +80,8 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
|
86
80
|
- Explains errors using OpenAI, Anthropic's Claude models, or any other
|
|
87
81
|
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`
|
|
88
82
|
- Optional codebase-aware explanations (RAG) backed by a local sqlite-vec index (see the RAG section below)
|
|
83
|
+
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
|
|
84
|
+
identifiers, and code kept in English
|
|
89
85
|
- Redacts secrets, tokens, and emails from tracebacks before sending
|
|
90
86
|
- Rate limits API calls with a configurable sliding window
|
|
91
87
|
- Works with both sync (WSGI) and async (ASGI) views
|
|
@@ -185,7 +181,7 @@ developer is actually investigating.
|
|
|
185
181
|
| `OPENAI_API_KEY` (env or settings) | Yes, unless `OPENAI_BASE_URL` points at an endpoint that does not authenticate | API key used to authenticate with the configured endpoint. Read first from the environment, then from `settings`. |
|
|
186
182
|
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
187
183
|
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
|
|
188
|
-
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000
|
|
184
|
+
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000`; scales up automatically when `EXPLAIN_ERRORS_LANGUAGE` is set (see below), unless you set this explicitly, which always overrides the scaling. |
|
|
189
185
|
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
|
|
190
186
|
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
|
|
191
187
|
| `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE` | No | When `True` (the default), the middleware prints the explanation to stdout and returns `None`, so exception handling continues normally and Django renders its standard debug page. Set to `False` to instead return a JSON 500 response, which ends exception handling early (see Compatibility above). |
|
|
@@ -195,6 +191,7 @@ developer is actually investigating.
|
|
|
195
191
|
| `EXPLAIN_ERRORS_REDACT_PATTERNS` | No | Extra regex pattern strings (each passed to `re.compile`) applied to the traceback, appended after the built-in secret/token/email patterns. An invalid pattern is skipped with a warning rather than raising. Defaults to `[]`. |
|
|
196
192
|
| `EXPLAIN_ERRORS_REDACT_DISABLE_DEFAULTS` | No | When `True`, skips the built-in secret/token/email redaction patterns entirely and redacts only what `EXPLAIN_ERRORS_REDACT_PATTERNS` specifies. Turning this on removes the default protection against leaking secrets and PII in tracebacks. Defaults to `False`. |
|
|
197
193
|
| `EXPLAIN_ERRORS_REDACT_REPLACEMENT` | No | Replacement string substituted for anything matched by the redaction patterns. Defaults to `"[REDACTED]"`. |
|
|
194
|
+
| `EXPLAIN_ERRORS_LANGUAGE` | No | Language the explanation prose is written in, as a plain name or code (for example `"Spanish"` or `"es"`). Defaults to `None`, meaning English. Exception names, identifiers, code, file paths, and tracebacks always stay in English regardless of this setting. |
|
|
198
195
|
|
|
199
196
|
## Using local models (Ollama)
|
|
200
197
|
|
|
@@ -250,6 +247,37 @@ and rejected, so a missing `OPENAI_API_KEY` shows up as an opaque `401 Unauthori
|
|
|
250
247
|
than a clear configuration error. If you see a 401 with `OPENAI_BASE_URL` pointed at a remote
|
|
251
248
|
provider, check that `OPENAI_API_KEY` — not a provider-specific variable — is actually set.
|
|
252
249
|
|
|
250
|
+
## Explanation language
|
|
251
|
+
|
|
252
|
+
By default, explanations are written in English. Set `EXPLAIN_ERRORS_LANGUAGE` to
|
|
253
|
+
read them in another language instead:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
EXPLAIN_ERRORS_LANGUAGE = "Spanish" # or the code form, "es"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
This is independent of Django's own `LANGUAGE_CODE`, which controls the language your
|
|
260
|
+
site serves to its users, not the language you read explanations in. Regardless of
|
|
261
|
+
`EXPLAIN_ERRORS_LANGUAGE`, exception type names, Django and Python identifiers, code,
|
|
262
|
+
file paths, and tracebacks are always kept in English, so they stay greppable and
|
|
263
|
+
matchable against documentation and search results. Only the explanatory prose is
|
|
264
|
+
translated.
|
|
265
|
+
|
|
266
|
+
**Known limitation:** small local models behind `OPENAI_BASE_URL` (see "Using local
|
|
267
|
+
models" above) tend to degrade sharply outside English. Output quality with
|
|
268
|
+
`EXPLAIN_ERRORS_LANGUAGE` set does not transfer uniformly across providers — it is
|
|
269
|
+
generally solid against OpenAI and Anthropic's APIs, but a small local model that
|
|
270
|
+
writes fluent English explanations may produce broken or mixed-language output once
|
|
271
|
+
asked to switch languages.
|
|
272
|
+
|
|
273
|
+
When a language is configured, the token ceiling (`OPENAI_MAX_TOKENS`) is scaled up
|
|
274
|
+
automatically so explanations in languages that tokenize less efficiently than English
|
|
275
|
+
aren't cut off mid-sentence. This scaling is deliberately generous — billing follows
|
|
276
|
+
tokens actually generated, so unused headroom costs nothing — rather than precise: the
|
|
277
|
+
underlying tokens-per-word figures are estimates, not direct measurements, and the
|
|
278
|
+
default model (`gpt-4o-mini`) uses the `o200k_base` tokenizer, which handles non-Latin
|
|
279
|
+
scripts considerably better than the `cl100k_base`-era ratios these estimates lean on.
|
|
280
|
+
|
|
253
281
|
## Codebase-aware explanations (RAG)
|
|
254
282
|
|
|
255
283
|
By default, explanations are generated from the traceback alone. With the
|
|
@@ -1,53 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: django-explain-errors
|
|
3
|
-
Version: 0.5.0
|
|
4
|
-
Summary: Django middleware that captures errors and exceptions, sends them to OpenAI for a detailed explanation, and prints the explanation to stdout when debug mode is enabled. Supports both sync and async views.
|
|
5
|
-
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
|
-
Author: topunix
|
|
7
|
-
Author-email: topunixguy@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Classifier: Development Status :: 4 - Beta
|
|
10
|
-
Classifier: Environment :: Web Environment
|
|
11
|
-
Classifier: Framework :: Django
|
|
12
|
-
Classifier: Framework :: Django :: 4.2
|
|
13
|
-
Classifier: Framework :: Django :: 5.0
|
|
14
|
-
Classifier: Framework :: Django :: 5.1
|
|
15
|
-
Classifier: Framework :: Django :: 5.2
|
|
16
|
-
Classifier: Framework :: Django :: 6.0
|
|
17
|
-
Classifier: Framework :: Django :: 6.1
|
|
18
|
-
Classifier: Intended Audience :: Developers
|
|
19
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
-
Classifier: Operating System :: OS Independent
|
|
21
|
-
Classifier: Programming Language :: Python
|
|
22
|
-
Classifier: Programming Language :: Python :: 3
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
-
Classifier: Topic :: Internet :: WWW/HTTP
|
|
29
|
-
Requires-Python: >=3.9
|
|
30
|
-
Description-Content-Type: text/markdown
|
|
31
|
-
License-File: LICENSE
|
|
32
|
-
Requires-Dist: Django>=4.2
|
|
33
|
-
Requires-Dist: openai<4.0,>=1.0
|
|
34
|
-
Requires-Dist: python-dotenv>=1.0
|
|
35
|
-
Requires-Dist: asgiref>=3.6
|
|
36
|
-
Provides-Extra: rag
|
|
37
|
-
Requires-Dist: sqlite-vec>=0.1.0; extra == "rag"
|
|
38
|
-
Dynamic: author
|
|
39
|
-
Dynamic: author-email
|
|
40
|
-
Dynamic: classifier
|
|
41
|
-
Dynamic: description
|
|
42
|
-
Dynamic: description-content-type
|
|
43
|
-
Dynamic: home-page
|
|
44
|
-
Dynamic: license
|
|
45
|
-
Dynamic: license-file
|
|
46
|
-
Dynamic: provides-extra
|
|
47
|
-
Dynamic: requires-dist
|
|
48
|
-
Dynamic: requires-python
|
|
49
|
-
Dynamic: summary
|
|
50
|
-
|
|
51
1
|
# Django Explain Errors Middleware
|
|
52
2
|
|
|
53
3
|
This Django middleware captures unhandled errors and exceptions, sends them
|
|
@@ -86,6 +36,8 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
|
86
36
|
- Explains errors using OpenAI, Anthropic's Claude models, or any other
|
|
87
37
|
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`
|
|
88
38
|
- Optional codebase-aware explanations (RAG) backed by a local sqlite-vec index (see the RAG section below)
|
|
39
|
+
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
|
|
40
|
+
identifiers, and code kept in English
|
|
89
41
|
- Redacts secrets, tokens, and emails from tracebacks before sending
|
|
90
42
|
- Rate limits API calls with a configurable sliding window
|
|
91
43
|
- Works with both sync (WSGI) and async (ASGI) views
|
|
@@ -185,7 +137,7 @@ developer is actually investigating.
|
|
|
185
137
|
| `OPENAI_API_KEY` (env or settings) | Yes, unless `OPENAI_BASE_URL` points at an endpoint that does not authenticate | API key used to authenticate with the configured endpoint. Read first from the environment, then from `settings`. |
|
|
186
138
|
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
187
139
|
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
|
|
188
|
-
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000
|
|
140
|
+
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000`; scales up automatically when `EXPLAIN_ERRORS_LANGUAGE` is set (see below), unless you set this explicitly, which always overrides the scaling. |
|
|
189
141
|
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
|
|
190
142
|
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
|
|
191
143
|
| `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE` | No | When `True` (the default), the middleware prints the explanation to stdout and returns `None`, so exception handling continues normally and Django renders its standard debug page. Set to `False` to instead return a JSON 500 response, which ends exception handling early (see Compatibility above). |
|
|
@@ -195,6 +147,7 @@ developer is actually investigating.
|
|
|
195
147
|
| `EXPLAIN_ERRORS_REDACT_PATTERNS` | No | Extra regex pattern strings (each passed to `re.compile`) applied to the traceback, appended after the built-in secret/token/email patterns. An invalid pattern is skipped with a warning rather than raising. Defaults to `[]`. |
|
|
196
148
|
| `EXPLAIN_ERRORS_REDACT_DISABLE_DEFAULTS` | No | When `True`, skips the built-in secret/token/email redaction patterns entirely and redacts only what `EXPLAIN_ERRORS_REDACT_PATTERNS` specifies. Turning this on removes the default protection against leaking secrets and PII in tracebacks. Defaults to `False`. |
|
|
197
149
|
| `EXPLAIN_ERRORS_REDACT_REPLACEMENT` | No | Replacement string substituted for anything matched by the redaction patterns. Defaults to `"[REDACTED]"`. |
|
|
150
|
+
| `EXPLAIN_ERRORS_LANGUAGE` | No | Language the explanation prose is written in, as a plain name or code (for example `"Spanish"` or `"es"`). Defaults to `None`, meaning English. Exception names, identifiers, code, file paths, and tracebacks always stay in English regardless of this setting. |
|
|
198
151
|
|
|
199
152
|
## Using local models (Ollama)
|
|
200
153
|
|
|
@@ -250,6 +203,37 @@ and rejected, so a missing `OPENAI_API_KEY` shows up as an opaque `401 Unauthori
|
|
|
250
203
|
than a clear configuration error. If you see a 401 with `OPENAI_BASE_URL` pointed at a remote
|
|
251
204
|
provider, check that `OPENAI_API_KEY` — not a provider-specific variable — is actually set.
|
|
252
205
|
|
|
206
|
+
## Explanation language
|
|
207
|
+
|
|
208
|
+
By default, explanations are written in English. Set `EXPLAIN_ERRORS_LANGUAGE` to
|
|
209
|
+
read them in another language instead:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
EXPLAIN_ERRORS_LANGUAGE = "Spanish" # or the code form, "es"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
This is independent of Django's own `LANGUAGE_CODE`, which controls the language your
|
|
216
|
+
site serves to its users, not the language you read explanations in. Regardless of
|
|
217
|
+
`EXPLAIN_ERRORS_LANGUAGE`, exception type names, Django and Python identifiers, code,
|
|
218
|
+
file paths, and tracebacks are always kept in English, so they stay greppable and
|
|
219
|
+
matchable against documentation and search results. Only the explanatory prose is
|
|
220
|
+
translated.
|
|
221
|
+
|
|
222
|
+
**Known limitation:** small local models behind `OPENAI_BASE_URL` (see "Using local
|
|
223
|
+
models" above) tend to degrade sharply outside English. Output quality with
|
|
224
|
+
`EXPLAIN_ERRORS_LANGUAGE` set does not transfer uniformly across providers — it is
|
|
225
|
+
generally solid against OpenAI and Anthropic's APIs, but a small local model that
|
|
226
|
+
writes fluent English explanations may produce broken or mixed-language output once
|
|
227
|
+
asked to switch languages.
|
|
228
|
+
|
|
229
|
+
When a language is configured, the token ceiling (`OPENAI_MAX_TOKENS`) is scaled up
|
|
230
|
+
automatically so explanations in languages that tokenize less efficiently than English
|
|
231
|
+
aren't cut off mid-sentence. This scaling is deliberately generous — billing follows
|
|
232
|
+
tokens actually generated, so unused headroom costs nothing — rather than precise: the
|
|
233
|
+
underlying tokens-per-word figures are estimates, not direct measurements, and the
|
|
234
|
+
default model (`gpt-4o-mini`) uses the `o200k_base` tokenizer, which handles non-Latin
|
|
235
|
+
scripts considerably better than the `cl100k_base`-era ratios these estimates lean on.
|
|
236
|
+
|
|
253
237
|
## Codebase-aware explanations (RAG)
|
|
254
238
|
|
|
255
239
|
By default, explanations are generated from the traceback alone. With the
|
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-explain-errors
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.
|
|
5
|
+
Home-page: https://github.com/topunix/django-explain-errors
|
|
6
|
+
Author: topunix
|
|
7
|
+
Author-email: topunixguy@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: Django>=4.2
|
|
27
|
+
Requires-Dist: openai<4.0,>=1.0
|
|
28
|
+
Requires-Dist: python-dotenv>=1.0
|
|
29
|
+
Requires-Dist: asgiref>=3.6
|
|
30
|
+
Provides-Extra: rag
|
|
31
|
+
Requires-Dist: sqlite-vec>=0.1.0; extra == "rag"
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: license
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
Dynamic: provides-extra
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
1
45
|
# Django Explain Errors Middleware
|
|
2
46
|
|
|
3
47
|
This Django middleware captures unhandled errors and exceptions, sends them
|
|
@@ -36,6 +80,8 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
|
36
80
|
- Explains errors using OpenAI, Anthropic's Claude models, or any other
|
|
37
81
|
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`
|
|
38
82
|
- Optional codebase-aware explanations (RAG) backed by a local sqlite-vec index (see the RAG section below)
|
|
83
|
+
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
|
|
84
|
+
identifiers, and code kept in English
|
|
39
85
|
- Redacts secrets, tokens, and emails from tracebacks before sending
|
|
40
86
|
- Rate limits API calls with a configurable sliding window
|
|
41
87
|
- Works with both sync (WSGI) and async (ASGI) views
|
|
@@ -135,7 +181,7 @@ developer is actually investigating.
|
|
|
135
181
|
| `OPENAI_API_KEY` (env or settings) | Yes, unless `OPENAI_BASE_URL` points at an endpoint that does not authenticate | API key used to authenticate with the configured endpoint. Read first from the environment, then from `settings`. |
|
|
136
182
|
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
137
183
|
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
|
|
138
|
-
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000
|
|
184
|
+
| `OPENAI_MAX_TOKENS` | No | Ceiling on tokens generated for the explanation, not a target — the system prompt itself asks for a concise answer. Defaults to `1000`; scales up automatically when `EXPLAIN_ERRORS_LANGUAGE` is set (see below), unless you set this explicitly, which always overrides the scaling. |
|
|
139
185
|
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
|
|
140
186
|
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
|
|
141
187
|
| `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE` | No | When `True` (the default), the middleware prints the explanation to stdout and returns `None`, so exception handling continues normally and Django renders its standard debug page. Set to `False` to instead return a JSON 500 response, which ends exception handling early (see Compatibility above). |
|
|
@@ -145,6 +191,7 @@ developer is actually investigating.
|
|
|
145
191
|
| `EXPLAIN_ERRORS_REDACT_PATTERNS` | No | Extra regex pattern strings (each passed to `re.compile`) applied to the traceback, appended after the built-in secret/token/email patterns. An invalid pattern is skipped with a warning rather than raising. Defaults to `[]`. |
|
|
146
192
|
| `EXPLAIN_ERRORS_REDACT_DISABLE_DEFAULTS` | No | When `True`, skips the built-in secret/token/email redaction patterns entirely and redacts only what `EXPLAIN_ERRORS_REDACT_PATTERNS` specifies. Turning this on removes the default protection against leaking secrets and PII in tracebacks. Defaults to `False`. |
|
|
147
193
|
| `EXPLAIN_ERRORS_REDACT_REPLACEMENT` | No | Replacement string substituted for anything matched by the redaction patterns. Defaults to `"[REDACTED]"`. |
|
|
194
|
+
| `EXPLAIN_ERRORS_LANGUAGE` | No | Language the explanation prose is written in, as a plain name or code (for example `"Spanish"` or `"es"`). Defaults to `None`, meaning English. Exception names, identifiers, code, file paths, and tracebacks always stay in English regardless of this setting. |
|
|
148
195
|
|
|
149
196
|
## Using local models (Ollama)
|
|
150
197
|
|
|
@@ -200,6 +247,37 @@ and rejected, so a missing `OPENAI_API_KEY` shows up as an opaque `401 Unauthori
|
|
|
200
247
|
than a clear configuration error. If you see a 401 with `OPENAI_BASE_URL` pointed at a remote
|
|
201
248
|
provider, check that `OPENAI_API_KEY` — not a provider-specific variable — is actually set.
|
|
202
249
|
|
|
250
|
+
## Explanation language
|
|
251
|
+
|
|
252
|
+
By default, explanations are written in English. Set `EXPLAIN_ERRORS_LANGUAGE` to
|
|
253
|
+
read them in another language instead:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
EXPLAIN_ERRORS_LANGUAGE = "Spanish" # or the code form, "es"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
This is independent of Django's own `LANGUAGE_CODE`, which controls the language your
|
|
260
|
+
site serves to its users, not the language you read explanations in. Regardless of
|
|
261
|
+
`EXPLAIN_ERRORS_LANGUAGE`, exception type names, Django and Python identifiers, code,
|
|
262
|
+
file paths, and tracebacks are always kept in English, so they stay greppable and
|
|
263
|
+
matchable against documentation and search results. Only the explanatory prose is
|
|
264
|
+
translated.
|
|
265
|
+
|
|
266
|
+
**Known limitation:** small local models behind `OPENAI_BASE_URL` (see "Using local
|
|
267
|
+
models" above) tend to degrade sharply outside English. Output quality with
|
|
268
|
+
`EXPLAIN_ERRORS_LANGUAGE` set does not transfer uniformly across providers — it is
|
|
269
|
+
generally solid against OpenAI and Anthropic's APIs, but a small local model that
|
|
270
|
+
writes fluent English explanations may produce broken or mixed-language output once
|
|
271
|
+
asked to switch languages.
|
|
272
|
+
|
|
273
|
+
When a language is configured, the token ceiling (`OPENAI_MAX_TOKENS`) is scaled up
|
|
274
|
+
automatically so explanations in languages that tokenize less efficiently than English
|
|
275
|
+
aren't cut off mid-sentence. This scaling is deliberately generous — billing follows
|
|
276
|
+
tokens actually generated, so unused headroom costs nothing — rather than precise: the
|
|
277
|
+
underlying tokens-per-word figures are estimates, not direct measurements, and the
|
|
278
|
+
default model (`gpt-4o-mini`) uses the `o200k_base` tokenizer, which handles non-Latin
|
|
279
|
+
scripts considerably better than the `cl100k_base`-era ratios these estimates lean on.
|
|
280
|
+
|
|
203
281
|
## Codebase-aware explanations (RAG)
|
|
204
282
|
|
|
205
283
|
By default, explanations are generated from the traceback alone. With the
|
|
@@ -25,6 +25,30 @@ SYSTEM_PROMPT = (
|
|
|
25
25
|
"why, and how to fix it. Be concrete and skip preamble."
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
# Appended to SYSTEM_PROMPT only when EXPLAIN_ERRORS_LANGUAGE is set, so the
|
|
29
|
+
# default (unset) prompt stays byte-identical to what ships today.
|
|
30
|
+
LANGUAGE_CLAUSE_TEMPLATE = (
|
|
31
|
+
" Answer in {language}. Keep exception type names, Django and Python "
|
|
32
|
+
"identifiers, attribute names, settings names, code, file paths, and "
|
|
33
|
+
"tracebacks in English."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Some languages tokenize far worse than English for equivalent content, so
|
|
37
|
+
# the fixed EXPLANATION_WORD_BUDGET can consume proportionally more of
|
|
38
|
+
# DEFAULT_MAX_TOKENS. The ceiling is billed on tokens generated, not on the
|
|
39
|
+
# cap itself, so unused headroom is free — there is no reason to ration it
|
|
40
|
+
# per language, and a per-language table would silently truncate every
|
|
41
|
+
# language it didn't anticipate. Apply the worst-case multiplier from the
|
|
42
|
+
# languages estimated (see the report in docs/tasks/explain-errors-language.md)
|
|
43
|
+
# to any configured language, known or not.
|
|
44
|
+
LANGUAGE_MAX_TOKENS_MULTIPLIER = 3.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _default_max_tokens_for_language(language):
|
|
48
|
+
if not language:
|
|
49
|
+
return DEFAULT_MAX_TOKENS
|
|
50
|
+
return int(DEFAULT_MAX_TOKENS * LANGUAGE_MAX_TOKENS_MULTIPLIER)
|
|
51
|
+
|
|
28
52
|
|
|
29
53
|
class ExplainErrorsMiddleware:
|
|
30
54
|
"""
|
|
@@ -49,7 +73,21 @@ class ExplainErrorsMiddleware:
|
|
|
49
73
|
|
|
50
74
|
# Configurable via settings, with sensible defaults.
|
|
51
75
|
self.model = getattr(settings, "OPENAI_MODEL", "gpt-4o-mini")
|
|
52
|
-
self.
|
|
76
|
+
self.language = getattr(settings, "EXPLAIN_ERRORS_LANGUAGE", None)
|
|
77
|
+
|
|
78
|
+
configured_max_tokens = getattr(settings, "OPENAI_MAX_TOKENS", None)
|
|
79
|
+
if configured_max_tokens is not None:
|
|
80
|
+
self.max_tokens = configured_max_tokens
|
|
81
|
+
else:
|
|
82
|
+
self.max_tokens = _default_max_tokens_for_language(self.language)
|
|
83
|
+
|
|
84
|
+
if self.language:
|
|
85
|
+
self.system_prompt = SYSTEM_PROMPT + LANGUAGE_CLAUSE_TEMPLATE.format(
|
|
86
|
+
language=self.language
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
self.system_prompt = SYSTEM_PROMPT
|
|
90
|
+
|
|
53
91
|
timeout = getattr(settings, "OPENAI_TIMEOUT", 10)
|
|
54
92
|
|
|
55
93
|
self.openai_client = get_openai_client(timeout=timeout)
|
|
@@ -128,7 +166,7 @@ class ExplainErrorsMiddleware:
|
|
|
128
166
|
response = self.openai_client.chat.completions.create(
|
|
129
167
|
model=self.model,
|
|
130
168
|
messages=[
|
|
131
|
-
{"role": "system", "content":
|
|
169
|
+
{"role": "system", "content": self.system_prompt},
|
|
132
170
|
{"role": "user", "content": prompt},
|
|
133
171
|
],
|
|
134
172
|
max_tokens=self.max_tokens,
|
|
@@ -10,9 +10,9 @@ with open('README.md', encoding='utf-8') as f:
|
|
|
10
10
|
|
|
11
11
|
setup(
|
|
12
12
|
name='django-explain-errors',
|
|
13
|
-
version='0.
|
|
13
|
+
version='0.6.0',
|
|
14
14
|
packages=find_packages(exclude=['tests', 'tests.*']),
|
|
15
|
-
description='Django middleware that
|
|
15
|
+
description='Django middleware that explains unhandled exceptions in DEBUG using an LLM, optionally grounded in your own project source via a local vector index. Works with OpenAI, Claude, or any OpenAI-compatible endpoint including local models.',
|
|
16
16
|
long_description_content_type='text/markdown',
|
|
17
17
|
long_description=long_description,
|
|
18
18
|
python_requires='>=3.9',
|
|
@@ -35,12 +35,6 @@ setup(
|
|
|
35
35
|
'Development Status :: 4 - Beta',
|
|
36
36
|
'Environment :: Web Environment',
|
|
37
37
|
'Framework :: Django',
|
|
38
|
-
'Framework :: Django :: 4.2',
|
|
39
|
-
'Framework :: Django :: 5.0',
|
|
40
|
-
'Framework :: Django :: 5.1',
|
|
41
|
-
'Framework :: Django :: 5.2',
|
|
42
|
-
'Framework :: Django :: 6.0',
|
|
43
|
-
'Framework :: Django :: 6.1',
|
|
44
38
|
'Intended Audience :: Developers',
|
|
45
39
|
'License :: OSI Approved :: MIT License',
|
|
46
40
|
'Operating System :: OS Independent',
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
from django.test import RequestFactory, SimpleTestCase, override_settings
|
|
4
|
+
|
|
5
|
+
from explain_errors.middleware import (
|
|
6
|
+
DEFAULT_MAX_TOKENS,
|
|
7
|
+
EXPLANATION_WORD_BUDGET,
|
|
8
|
+
LANGUAGE_MAX_TOKENS_MULTIPLIER,
|
|
9
|
+
SYSTEM_PROMPT,
|
|
10
|
+
ExplainErrorsMiddleware,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _mock_openai():
|
|
15
|
+
patcher = patch("openai.OpenAI")
|
|
16
|
+
mock_cls = patcher.start()
|
|
17
|
+
client = MagicMock()
|
|
18
|
+
client.chat.completions.create.return_value = MagicMock(
|
|
19
|
+
choices=[
|
|
20
|
+
MagicMock(message=MagicMock(content="Mocked explanation."), finish_reason="stop")
|
|
21
|
+
]
|
|
22
|
+
)
|
|
23
|
+
mock_cls.return_value = client
|
|
24
|
+
return patcher, client
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@override_settings(DEBUG=True, OPENAI_API_KEY="test-key")
|
|
28
|
+
class LanguageUnsetTest(SimpleTestCase):
|
|
29
|
+
"""Case 1: no EXPLAIN_ERRORS_LANGUAGE must not change existing output."""
|
|
30
|
+
|
|
31
|
+
def setUp(self):
|
|
32
|
+
self.factory = RequestFactory()
|
|
33
|
+
self.patcher, self.client = _mock_openai()
|
|
34
|
+
self.addCleanup(self.patcher.stop)
|
|
35
|
+
|
|
36
|
+
def test_system_prompt_byte_identical_when_language_unset(self):
|
|
37
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
38
|
+
mw.process_exception(self.factory.get("/"), Exception("x"))
|
|
39
|
+
|
|
40
|
+
messages = self.client.chat.completions.create.call_args.kwargs["messages"]
|
|
41
|
+
self.assertEqual(messages[0]["content"], SYSTEM_PROMPT)
|
|
42
|
+
|
|
43
|
+
def test_user_prompt_byte_identical_when_language_unset(self):
|
|
44
|
+
# Invariant 10: the language clause belongs in the system message
|
|
45
|
+
# only. Confirm the user-message prompt is unaffected.
|
|
46
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
47
|
+
request = self.factory.get("/")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
raise ValueError("boom")
|
|
51
|
+
except ValueError as exc:
|
|
52
|
+
mw.process_exception(request, exc)
|
|
53
|
+
|
|
54
|
+
messages = self.client.chat.completions.create.call_args.kwargs["messages"]
|
|
55
|
+
# Re-derive what the traceback-only prompt should look like: same
|
|
56
|
+
# approach as tests/test_rag.py's byte-identical check.
|
|
57
|
+
expected_prefix = "Explain the following Django error in simple terms:\n\n"
|
|
58
|
+
self.assertTrue(messages[1]["content"].startswith(expected_prefix))
|
|
59
|
+
self.assertIn("ValueError", messages[1]["content"])
|
|
60
|
+
self.assertIn("boom", messages[1]["content"])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@override_settings(
|
|
64
|
+
DEBUG=True, OPENAI_API_KEY="test-key", EXPLAIN_ERRORS_LANGUAGE="Spanish"
|
|
65
|
+
)
|
|
66
|
+
class LanguageConfiguredTest(SimpleTestCase):
|
|
67
|
+
"""Cases 2 & 3: the language clause and identifier-preservation
|
|
68
|
+
instruction appear in the system prompt when configured."""
|
|
69
|
+
|
|
70
|
+
def setUp(self):
|
|
71
|
+
self.factory = RequestFactory()
|
|
72
|
+
self.patcher, self.client = _mock_openai()
|
|
73
|
+
self.addCleanup(self.patcher.stop)
|
|
74
|
+
|
|
75
|
+
def test_language_clause_appears_in_system_prompt(self):
|
|
76
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
77
|
+
mw.process_exception(self.factory.get("/"), Exception("x"))
|
|
78
|
+
|
|
79
|
+
messages = self.client.chat.completions.create.call_args.kwargs["messages"]
|
|
80
|
+
system_prompt = messages[0]["content"]
|
|
81
|
+
self.assertIn("Spanish", system_prompt)
|
|
82
|
+
self.assertTrue(system_prompt.startswith(SYSTEM_PROMPT))
|
|
83
|
+
|
|
84
|
+
def test_identifier_preservation_instruction_appears(self):
|
|
85
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
86
|
+
mw.process_exception(self.factory.get("/"), Exception("x"))
|
|
87
|
+
|
|
88
|
+
system_prompt = self.client.chat.completions.create.call_args.kwargs["messages"][
|
|
89
|
+
0
|
|
90
|
+
]["content"]
|
|
91
|
+
self.assertIn("Django and Python", system_prompt)
|
|
92
|
+
self.assertIn("English", system_prompt)
|
|
93
|
+
|
|
94
|
+
def test_user_prompt_still_byte_identical_when_language_configured(self):
|
|
95
|
+
# Invariant 10: configuring a language must not touch the
|
|
96
|
+
# traceback-only user-message prompt.
|
|
97
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
98
|
+
request = self.factory.get("/")
|
|
99
|
+
|
|
100
|
+
with patch(
|
|
101
|
+
"explain_errors.middleware.sanitize_traceback",
|
|
102
|
+
side_effect=lambda tb: tb,
|
|
103
|
+
):
|
|
104
|
+
try:
|
|
105
|
+
raise ValueError("boom")
|
|
106
|
+
except ValueError as exc:
|
|
107
|
+
mw.process_exception(request, exc)
|
|
108
|
+
|
|
109
|
+
messages = self.client.chat.completions.create.call_args.kwargs["messages"]
|
|
110
|
+
self.assertTrue(
|
|
111
|
+
messages[1]["content"].startswith(
|
|
112
|
+
"Explain the following Django error in simple terms:\n\n"
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
self.assertNotIn("Spanish", messages[1]["content"])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@override_settings(DEBUG=True, OPENAI_API_KEY="test-key")
|
|
119
|
+
class MaxTokensLanguageScalingTest(SimpleTestCase):
|
|
120
|
+
"""Case 4: the scaled ceiling covers the word budget, and applies
|
|
121
|
+
uniformly to any configured language rather than only ones anticipated
|
|
122
|
+
in advance — that's the no-cliff property a per-language table would
|
|
123
|
+
have broken."""
|
|
124
|
+
|
|
125
|
+
def setUp(self):
|
|
126
|
+
self.patcher, self.client = _mock_openai()
|
|
127
|
+
self.addCleanup(self.patcher.stop)
|
|
128
|
+
|
|
129
|
+
def test_default_ceiling_used_when_language_unset(self):
|
|
130
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
131
|
+
self.assertEqual(mw.max_tokens, DEFAULT_MAX_TOKENS)
|
|
132
|
+
|
|
133
|
+
def test_scaled_ceiling_covers_word_budget(self):
|
|
134
|
+
scaled = int(DEFAULT_MAX_TOKENS * LANGUAGE_MAX_TOKENS_MULTIPLIER)
|
|
135
|
+
self.assertGreaterEqual(scaled, EXPLANATION_WORD_BUDGET * 4)
|
|
136
|
+
|
|
137
|
+
def test_scaled_ceiling_applies_for_an_estimated_language(self):
|
|
138
|
+
with override_settings(EXPLAIN_ERRORS_LANGUAGE="Japanese"):
|
|
139
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
140
|
+
self.assertEqual(
|
|
141
|
+
mw.max_tokens, int(DEFAULT_MAX_TOKENS * LANGUAGE_MAX_TOKENS_MULTIPLIER)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def test_scaled_ceiling_applies_for_a_language_not_in_the_estimate(self):
|
|
145
|
+
# No per-language table to fall through: Chinese was never
|
|
146
|
+
# estimated, and still gets the same scaled ceiling as Japanese.
|
|
147
|
+
with override_settings(EXPLAIN_ERRORS_LANGUAGE="Chinese"):
|
|
148
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
149
|
+
self.assertEqual(
|
|
150
|
+
mw.max_tokens, int(DEFAULT_MAX_TOKENS * LANGUAGE_MAX_TOKENS_MULTIPLIER)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def test_explicit_openai_max_tokens_overrides_language_scaling(self):
|
|
154
|
+
with override_settings(
|
|
155
|
+
EXPLAIN_ERRORS_LANGUAGE="Hindi", OPENAI_MAX_TOKENS=42
|
|
156
|
+
):
|
|
157
|
+
mw = ExplainErrorsMiddleware(lambda r: None)
|
|
158
|
+
self.assertEqual(mw.max_tokens, 42)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|