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.
Files changed (32) hide show
  1. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/PKG-INFO +37 -9
  2. django_explain_errors-0.5.0/django_explain_errors.egg-info/PKG-INFO → django_explain_errors-0.6.0/README.md +35 -51
  3. django_explain_errors-0.5.0/README.md → django_explain_errors-0.6.0/django_explain_errors.egg-info/PKG-INFO +79 -1
  4. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/django_explain_errors.egg-info/SOURCES.txt +1 -0
  5. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/middleware.py +40 -2
  6. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/setup.py +2 -8
  7. django_explain_errors-0.6.0/tests/test_language.py +158 -0
  8. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/LICENSE +0 -0
  9. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/MANIFEST.in +0 -0
  10. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/django_explain_errors.egg-info/dependency_links.txt +0 -0
  11. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/django_explain_errors.egg-info/requires.txt +0 -0
  12. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/django_explain_errors.egg-info/top_level.txt +0 -0
  13. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/__init__.py +0 -0
  14. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/client.py +0 -0
  15. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/management/__init__.py +0 -0
  16. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/management/commands/__init__.py +0 -0
  17. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/management/commands/build_error_index.py +0 -0
  18. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/rag/__init__.py +0 -0
  19. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/rag/indexer.py +0 -0
  20. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/rag/retriever.py +0 -0
  21. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/rag/store.py +0 -0
  22. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/sanitize.py +0 -0
  23. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/explain_errors/throttle.py +0 -0
  24. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/setup.cfg +0 -0
  25. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_build_error_index.py +0 -0
  26. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_client.py +0 -0
  27. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_middleware.py +0 -0
  28. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_rag.py +0 -0
  29. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_sanitize.py +0 -0
  30. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_signals.py +0 -0
  31. {django_explain_errors-0.5.0 → django_explain_errors-0.6.0}/tests/test_throttle.py +0 -0
  32. {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.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.
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
@@ -21,6 +21,7 @@ explain_errors/rag/retriever.py
21
21
  explain_errors/rag/store.py
22
22
  tests/test_build_error_index.py
23
23
  tests/test_client.py
24
+ tests/test_language.py
24
25
  tests/test_middleware.py
25
26
  tests/test_rag.py
26
27
  tests/test_sanitize.py
@@ -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.max_tokens = getattr(settings, "OPENAI_MAX_TOKENS", DEFAULT_MAX_TOKENS)
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": SYSTEM_PROMPT},
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.5.0',
13
+ version='0.6.0',
14
14
  packages=find_packages(exclude=['tests', 'tests.*']),
15
- description='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.',
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)