django-explain-errors 0.6.0__tar.gz → 0.8.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.6.0/django_explain_errors.egg-info → django_explain_errors-0.8.0}/PKG-INFO +242 -66
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/README.md +241 -65
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0/django_explain_errors.egg-info}/PKG-INFO +242 -66
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/django_explain_errors.egg-info/SOURCES.txt +8 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/client.py +3 -2
- django_explain_errors-0.8.0/explain_errors/debug_page.py +172 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/management/commands/build_error_index.py +2 -2
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/middleware.py +77 -17
- django_explain_errors-0.8.0/explain_errors/tracebacks.py +105 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/setup.py +2 -2
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_build_error_index.py +1 -1
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_client.py +3 -2
- django_explain_errors-0.8.0/tests/test_debug_page.py +402 -0
- django_explain_errors-0.8.0/tests/test_eval_fixtures.py +110 -0
- django_explain_errors-0.8.0/tests/test_eval_judge.py +393 -0
- django_explain_errors-0.8.0/tests/test_eval_run.py +389 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_middleware.py +47 -1
- django_explain_errors-0.8.0/tests/test_print_stdout.py +130 -0
- django_explain_errors-0.8.0/tests/test_tracebacks.py +129 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/LICENSE +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/MANIFEST.in +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/django_explain_errors.egg-info/dependency_links.txt +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/django_explain_errors.egg-info/requires.txt +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/django_explain_errors.egg-info/top_level.txt +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/__init__.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/management/__init__.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/management/commands/__init__.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/rag/__init__.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/rag/indexer.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/rag/retriever.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/rag/store.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/sanitize.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/explain_errors/throttle.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/setup.cfg +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_language.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_rag.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_sanitize.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_signals.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_throttle.py +0 -0
- {django_explain_errors-0.6.0 → django_explain_errors-0.8.0}/tests/test_truncation.py +0 -0
{django_explain_errors-0.6.0/django_explain_errors.egg-info → django_explain_errors-0.8.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-explain-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
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
|
|
@@ -45,16 +45,23 @@ Dynamic: summary
|
|
|
45
45
|
# Django Explain Errors Middleware
|
|
46
46
|
|
|
47
47
|
This Django middleware captures unhandled errors and exceptions, sends them
|
|
48
|
-
to a language model for explanation, and
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
to a language model for explanation, and, when debug mode is enabled, shows
|
|
49
|
+
the explanation on Django's debug page directly under the exception
|
|
50
|
+
headline, as well as printing it to stdout. It works with the OpenAI API
|
|
51
|
+
out of the box, with Anthropic's Claude models through Anthropic's
|
|
52
|
+
OpenAI-compatible endpoint, and with any other OpenAI-compatible endpoint
|
|
53
|
+
(Ollama, LM Studio, Azure, or a corporate gateway) by setting a base URL,
|
|
54
|
+
so explanations can run entirely on a local model if you prefer not to send
|
|
55
|
+
code off your machine.
|
|
56
|
+
|
|
57
|
+
Enabling RAG is strongly recommended. With it, the middleware retrieves the
|
|
58
|
+
relevant parts of your own project source from a local vector index, so
|
|
59
|
+
explanations name the actual file and function that failed instead of
|
|
60
|
+
guessing. In the package's eval harness, RAG-grounded explanations won 35
|
|
61
|
+
of 43 blind comparisons, with the gap concentrated in pointing at the right
|
|
62
|
+
fix location and not inventing details (see "Does RAG actually help?"
|
|
63
|
+
below). It is off by default because it needs an optional extra and a
|
|
64
|
+
one-time index build.
|
|
58
65
|
|
|
59
66
|
The middleware supports both synchronous (WSGI) and asynchronous (ASGI)
|
|
60
67
|
views. It auto-detects the view chain at startup and routes requests through
|
|
@@ -62,11 +69,17 @@ the matching path, so no extra configuration is required for either server
|
|
|
62
69
|
type. Tracebacks are sanitized before leaving the process, and API calls are
|
|
63
70
|
rate limited.
|
|
64
71
|
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
*Real `gpt-4o-mini` output with RAG enabled, recorded by the eval harness
|
|
75
|
+
on its `missing_fk` fixture.*
|
|
76
|
+
|
|
65
77
|
## Scope
|
|
66
78
|
|
|
67
79
|
This package explains errors for a person, not for a coding agent to consume
|
|
68
|
-
programmatically. The explanation is written for a human reader,
|
|
69
|
-
|
|
80
|
+
programmatically. The explanation is written for a human reader, on Django's
|
|
81
|
+
debug page in the browser or in a terminal, and the output format assumes
|
|
82
|
+
that reader.
|
|
70
83
|
|
|
71
84
|
If a coding agent is doing the debugging, it does not need this. Agents read tracebacks directly,
|
|
72
85
|
and tools that expose live runtime state (debugger-over-MCP servers, `mcp-django`) serve that case
|
|
@@ -77,9 +90,16 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
|
|
|
77
90
|
## Features
|
|
78
91
|
|
|
79
92
|
- Captures Django errors and exceptions
|
|
93
|
+
- Shows the explanation on Django's debug page, directly under the
|
|
94
|
+
exception headline (`EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`, on by default)
|
|
95
|
+
- Prints the explanation to stdout, for terminal workflows and logs
|
|
96
|
+
(`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
|
|
97
|
+
- Optional JSON 500 response instead of the debug page
|
|
98
|
+
(`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE = False`)
|
|
80
99
|
- Explains errors using OpenAI, Anthropic's Claude models, or any other
|
|
81
100
|
OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`
|
|
82
|
-
-
|
|
101
|
+
- Codebase-aware explanations via RAG over a local sqlite-vec index
|
|
102
|
+
(recommended; see "Codebase-aware explanations" below)
|
|
83
103
|
- Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
|
|
84
104
|
identifiers, and code kept in English
|
|
85
105
|
- Redacts secrets, tokens, and emails from tracebacks before sending
|
|
@@ -96,13 +116,16 @@ pip install django-explain-errors
|
|
|
96
116
|
|
|
97
117
|
2. **Add the middleware to your Django project**:
|
|
98
118
|
|
|
99
|
-
- Open your `settings.py` file and
|
|
119
|
+
- Open your `settings.py` file and register the middleware only when `DEBUG` is on (see
|
|
120
|
+
Production Safety below for why):
|
|
100
121
|
|
|
101
122
|
```python
|
|
102
123
|
MIDDLEWARE = [
|
|
103
124
|
...
|
|
104
|
-
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
105
125
|
]
|
|
126
|
+
|
|
127
|
+
if DEBUG:
|
|
128
|
+
MIDDLEWARE.append('explain_errors.middleware.ExplainErrorsMiddleware')
|
|
106
129
|
```
|
|
107
130
|
|
|
108
131
|
In the default preserve mode, `process_exception` returns `None`, so exception handling
|
|
@@ -116,15 +139,39 @@ pip install django-explain-errors
|
|
|
116
139
|
|
|
117
140
|
3. **Set up environment variables**:
|
|
118
141
|
|
|
119
|
-
- Create a `.env` file in your project's root directory and add your
|
|
142
|
+
- Create a `.env` file in your project's root directory and add your API key. Alternatively, you can set it in `settings.py`:
|
|
120
143
|
|
|
121
144
|
```plaintext
|
|
122
|
-
OPENAI_API_KEY=
|
|
145
|
+
OPENAI_API_KEY=your_api_key_here
|
|
123
146
|
```
|
|
124
147
|
|
|
125
148
|
The API key is not required if you set `OPENAI_BASE_URL` to a local
|
|
126
149
|
server such as Ollama, which does not authenticate requests.
|
|
127
150
|
|
|
151
|
+
## Production Safety
|
|
152
|
+
|
|
153
|
+
This middleware is intended for local development only. When active, it sends exception
|
|
154
|
+
tracebacks to the configured LLM provider, which may include source code, file paths, and
|
|
155
|
+
local variable values. Keep it out of production.
|
|
156
|
+
|
|
157
|
+
**Primary safeguard:** register the middleware only when `DEBUG` is on, or only in your
|
|
158
|
+
development settings module:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# settings.py
|
|
162
|
+
if DEBUG:
|
|
163
|
+
MIDDLEWARE.append("explain_errors.middleware.ExplainErrorsMiddleware")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
See Installation above for where the middleware can sit in `MIDDLEWARE`.
|
|
167
|
+
|
|
168
|
+
**Secondary safeguard:** run Django's deployment checks in CI against your production settings.
|
|
169
|
+
This fails the build if `DEBUG = True` (`security.W018`):
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
python manage.py check --deploy --fail-level WARNING
|
|
173
|
+
```
|
|
174
|
+
|
|
128
175
|
## Usage
|
|
129
176
|
|
|
130
177
|
1. **Ensure DEBUG is set to True**:
|
|
@@ -137,17 +184,59 @@ pip install django-explain-errors
|
|
|
137
184
|
|
|
138
185
|
2. **Trigger an error in your Django application**:
|
|
139
186
|
|
|
140
|
-
The middleware captures the error, sends it to the configured model for explanation,
|
|
187
|
+
The middleware captures the error, sends it to the configured model for explanation, prints the explanation to stdout, and adds it as a banner on Django's debug page. By default (`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE=True`), exception handling then continues normally, so Django (or whatever else is watching, such as `runserver_plus` or Sentry, see Compatibility below) renders its usual response. The only change is the banner on Django's own debug page. Set `EXPLAIN_ERRORS_INJECT_DEBUG_PAGE = False` to remove it, or `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE = False` to instead get a JSON `500` response containing the error message and the explanation.
|
|
188
|
+
|
|
189
|
+
In the terminal, the explanation is printed with the model that produced it:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
Error explanation (gpt-4o-mini):
|
|
193
|
+
The view raised a ValueError because ...
|
|
194
|
+
```
|
|
141
195
|
|
|
142
196
|
## Async Support
|
|
143
197
|
|
|
144
198
|
The middleware exposes both `sync_capable = True` and `async_capable = True`. At initialization it inspects `get_response` to decide whether it is part of a sync or async chain:
|
|
145
199
|
|
|
146
200
|
- Under WSGI (for example `runserver` with sync views), requests flow through the synchronous handler.
|
|
147
|
-
- Under ASGI (for example with async views), requests are awaited through the async handler. The blocking
|
|
201
|
+
- Under ASGI (for example with async views), requests are awaited through the async handler. The blocking model API call is offloaded with `asgiref.sync.sync_to_async` so the event loop is not blocked.
|
|
148
202
|
|
|
149
203
|
No additional settings are needed. See Installation above for where to place the middleware in `MIDDLEWARE`.
|
|
150
204
|
|
|
205
|
+
## Debug page injection
|
|
206
|
+
|
|
207
|
+
When Django renders its debug page for a 500, `explain_errors` also inserts a banner
|
|
208
|
+
containing the explanation directly under the exception headline, in addition to printing
|
|
209
|
+
it to stdout.
|
|
210
|
+
|
|
211
|
+
Controlled by `EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`, default `True`. Injection happens only
|
|
212
|
+
when all of the following hold:
|
|
213
|
+
|
|
214
|
+
- `DEBUG` is `True`
|
|
215
|
+
- `EXPLAIN_ERRORS_INJECT_DEBUG_PAGE` is `True` (the default)
|
|
216
|
+
- `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE` is `True` (the default). With
|
|
217
|
+
`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE=False`, the JSON 500 path runs instead and there is
|
|
218
|
+
no debug page to inject into.
|
|
219
|
+
- an explanation was actually produced (not throttled, and the API call succeeded)
|
|
220
|
+
|
|
221
|
+
The banner appears only on the HTML debug page, styled inline (bordered box, light
|
|
222
|
+
background) so it reads on Django's page. The explanation is HTML-escaped first, then a small
|
|
223
|
+
Markdown subset (inline code, code blocks, bold, lists) is rendered as HTML. Nothing from the
|
|
224
|
+
model is ever marked safe.
|
|
225
|
+
|
|
226
|
+
**Fails open.** Any problem building or inserting the banner (a missing request, an
|
|
227
|
+
unexpected debug page layout, anything else) logs one warning and leaves Django's normal
|
|
228
|
+
debug page untouched. This code can never turn a working debug page into a broken one.
|
|
229
|
+
|
|
230
|
+
**Custom exception reporters.** If your project already sets a custom
|
|
231
|
+
`DEFAULT_EXCEPTION_REPORTER`, or something earlier in the request sets
|
|
232
|
+
`request.exception_reporter_class`, `explain_errors` leaves it alone and skips injection
|
|
233
|
+
(logged at debug level) rather than overriding it.
|
|
234
|
+
|
|
235
|
+
**Upgrading.** Turning this on, or upgrading to a version where it defaults on, changes
|
|
236
|
+
what Django's debug page looks like: a new section appears above the request metadata
|
|
237
|
+
table. If you rely on the debug page's exact markup (a scraper, a screenshot test), account
|
|
238
|
+
for this.
|
|
239
|
+
|
|
151
240
|
## Compatibility
|
|
152
241
|
|
|
153
242
|
How this middleware interacts with other error-handling and debugging tools, in the default
|
|
@@ -155,10 +244,10 @@ preserve mode and with `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE=False`:
|
|
|
155
244
|
|
|
156
245
|
| Package | Preserve mode (default) | `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE=False` |
|
|
157
246
|
| ------- | ------------------------ | ------------------------------------------- |
|
|
158
|
-
| Django Debug Toolbar | Works — Django renders its
|
|
247
|
+
| Django Debug Toolbar | Works — Django renders its debug page (with the explanation banner, unless `EXPLAIN_ERRORS_INJECT_DEBUG_PAGE = False`), and Debug Toolbar injects into it. | Broken — the JSON 500 response has no HTML to inject into. |
|
|
159
248
|
| Sentry, Rollbar | Work — the exception propagates and Django re-raises it, so `got_request_exception` fires. | Broken — returning a response ends exception handling before `got_request_exception` fires. |
|
|
160
249
|
| Django REST Framework | Partial — only exceptions DRF does not already handle itself reach this middleware. | Partial, same reason. |
|
|
161
|
-
| Silk and other profiling panels | Timings are inflated by the
|
|
250
|
+
| Silk and other profiling panels | Timings are inflated by the model API call, since `process_exception` blocks the request path. | Same. |
|
|
162
251
|
| CORS, GZip, WhiteNoise | No interaction. | No interaction. |
|
|
163
252
|
| `runserver_plus` / Werkzeug debugger | Works — returning `None` re-raises the original exception, and `django-extensions` replaces Django's debug-page renderer with one that re-raises instead, so Werkzeug's WSGI wrapper catches it and shows the interactive debugger. | Broken — the JSON 500 response ends exception handling before it reaches `runserver_plus`'s exception hook, so the Werkzeug debugger never appears. |
|
|
164
253
|
|
|
@@ -182,9 +271,11 @@ developer is actually investigating.
|
|
|
182
271
|
| `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
|
|
183
272
|
| `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
|
|
184
273
|
| `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. |
|
|
185
|
-
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for
|
|
186
|
-
| `OPENAI_MAX_TRACEBACK_CHARS` | No |
|
|
187
|
-
| `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE` | No | When `True` (the default), the middleware
|
|
274
|
+
| `OPENAI_TIMEOUT` | No | Request timeout in seconds for model API calls. Defaults to `10`. |
|
|
275
|
+
| `OPENAI_MAX_TRACEBACK_CHARS` | No | Total character budget for the traceback sent to the model. Application frames (your own code, as opposed to Django, the standard library, or installed packages) are always kept; library frames fill whatever budget remains, nearest the raise point first, with an `... N library frames omitted ...` line where frames are dropped. If the application frames alone exceed the budget, falls back to keeping the last N characters of the raw traceback. Defaults to `3000`. |
|
|
276
|
+
| `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE` | No | When `True` (the default), the middleware returns `None` (after printing the explanation to stdout, unless `EXPLAIN_ERRORS_PRINT_STDOUT = False`), so exception handling continues normally and Django renders its debug page (with the explanation banner, controlled by `EXPLAIN_ERRORS_INJECT_DEBUG_PAGE`). Set to `False` to instead return a JSON 500 response, which ends exception handling early (see Compatibility above). |
|
|
277
|
+
| `EXPLAIN_ERRORS_INJECT_DEBUG_PAGE` | No | When `True` (the default), also injects the explanation as a banner into Django's debug page, in addition to stdout. Requires `EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE=True` (the default); see Debug Page Injection above. |
|
|
278
|
+
| `EXPLAIN_ERRORS_PRINT_STDOUT` | No | When `True` (the default), prints the explanation to stdout. Set to `False` to suppress it, for example when `EXPLAIN_ERRORS_INJECT_DEBUG_PAGE` already shows it in the browser. The failure message (when the model API call itself errors) always prints. For requests where nobody views the debug page (API clients, which get Django's plain-text error response, and `fetch`/HTMX requests, whose HTML response is never displayed), stdout is the only channel that shows the explanation, so turn this off only in browser-first workflows. |
|
|
188
279
|
| `OPENAI_BASE_URL` (env or settings) | No | Base URL for any OpenAI-compatible API (for example Ollama at `http://localhost:11434/v1`). When set, a missing API key is replaced with a placeholder since local servers do not require one. |
|
|
189
280
|
| `EXPLAIN_ERRORS_MAX_CALLS` | No | Together with `EXPLAIN_ERRORS_WINDOW_SECONDS`, caps API spend to at most this many explanations within a rolling window; once the cap is hit, further errors in that window are not sent for explanation until an earlier call ages out. Defaults to `5`. |
|
|
190
281
|
| `EXPLAIN_ERRORS_WINDOW_SECONDS` | No | Length in seconds of the rolling window `EXPLAIN_ERRORS_MAX_CALLS` is measured against. Defaults to `60` (with the defaults, at most 5 explanations per 60-second window). |
|
|
@@ -256,6 +347,13 @@ read them in another language instead:
|
|
|
256
347
|
EXPLAIN_ERRORS_LANGUAGE = "Spanish" # or the code form, "es"
|
|
257
348
|
```
|
|
258
349
|
|
|
350
|
+
There is no fixed list of supported languages. `EXPLAIN_ERRORS_LANGUAGE` accepts
|
|
351
|
+
any language the configured model can write, because the setting adds one clause
|
|
352
|
+
to the system prompt (see `LANGUAGE_CLAUSE_TEMPLATE` in
|
|
353
|
+
`explain_errors/middleware.py`), not a translation catalog with its own
|
|
354
|
+
maintained language list. How well it works varies by model; see the known
|
|
355
|
+
limitation below.
|
|
356
|
+
|
|
259
357
|
This is independent of Django's own `LANGUAGE_CODE`, which controls the language your
|
|
260
358
|
site serves to its users, not the language you read explanations in. Regardless of
|
|
261
359
|
`EXPLAIN_ERRORS_LANGUAGE`, exception type names, Django and Python identifiers, code,
|
|
@@ -270,13 +368,18 @@ generally solid against OpenAI and Anthropic's APIs, but a small local model tha
|
|
|
270
368
|
writes fluent English explanations may produce broken or mixed-language output once
|
|
271
369
|
asked to switch languages.
|
|
272
370
|
|
|
273
|
-
When a language is configured
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
371
|
+
When a language is configured and `OPENAI_MAX_TOKENS` isn't set explicitly, the
|
|
372
|
+
token ceiling defaults to 3,000 instead of 1,000, a flat 3x multiplier applied the
|
|
373
|
+
same way regardless of how well or poorly a given language is known to tokenize, so
|
|
374
|
+
explanations in languages that use more tokens per word than English aren't cut off
|
|
375
|
+
mid-sentence. This is a ceiling, not a target: billing follows tokens actually
|
|
376
|
+
generated, so the extra headroom costs nothing if unused. Setting `OPENAI_MAX_TOKENS`
|
|
377
|
+
explicitly always overrides this scaling, at any value, including one lower than the
|
|
378
|
+
unscaled 1,000 default. The multiplier itself is deliberately generous rather than
|
|
379
|
+
precise: the underlying tokens-per-word figures are estimates, not direct
|
|
380
|
+
measurements, and the default model (`gpt-4o-mini`) uses the `o200k_base` tokenizer,
|
|
381
|
+
which handles non-Latin scripts considerably better than the `cl100k_base`-era ratios
|
|
382
|
+
these estimates lean on.
|
|
280
383
|
|
|
281
384
|
## Codebase-aware explanations (RAG)
|
|
282
385
|
|
|
@@ -286,7 +389,9 @@ also retrieves the most relevant chunks of your own project's source code
|
|
|
286
389
|
from a local vector index and includes them in the prompt, so explanations
|
|
287
390
|
can reference your actual functions and classes instead of guessing at them.
|
|
288
391
|
|
|
289
|
-
|
|
392
|
+
Recommended for most projects. It is opt-in only because setup takes three
|
|
393
|
+
steps (install the extra, build the index, enable it), and it adds no
|
|
394
|
+
dependencies or behavior until you do.
|
|
290
395
|
|
|
291
396
|
### Install the extra
|
|
292
397
|
|
|
@@ -309,7 +414,7 @@ python manage.py build_error_index
|
|
|
309
414
|
|
|
310
415
|
This walks your project, chunks Python files by top-level function/class
|
|
311
416
|
(and other text files by fixed-size line windows), embeds each chunk with
|
|
312
|
-
the
|
|
417
|
+
the configured endpoint's embeddings API, and writes them to a local index file. Re-run it
|
|
313
418
|
whenever your source changes meaningfully. Indexing is not automatic.
|
|
314
419
|
Rebuilding is idempotent: it builds into a temp file and atomically replaces
|
|
315
420
|
the previous index.
|
|
@@ -329,15 +434,14 @@ EXPLAIN_ERRORS_RAG_ENABLED = True
|
|
|
329
434
|
| `EXPLAIN_ERRORS_RAG_ENABLED` | `False` | Master switch for the RAG layer. |
|
|
330
435
|
| `EXPLAIN_ERRORS_RAG_INDEX_PATH` | `<BASE_DIR>/.explain_errors_index.db` | Path to the local vector index file. |
|
|
331
436
|
| `EXPLAIN_ERRORS_RAG_TOP_K` | `4` | Number of chunks retrieved and injected into the prompt. |
|
|
332
|
-
| `EXPLAIN_ERRORS_RAG_EMBED_MODEL` | `"text-embedding-3-small"` |
|
|
437
|
+
| `EXPLAIN_ERRORS_RAG_EMBED_MODEL` | `"text-embedding-3-small"` | Embedding model used for indexing and retrieval. Must exist on the configured endpoint. |
|
|
333
438
|
| `EXPLAIN_ERRORS_RAG_INCLUDE` | `None` (defaults to `BASE_DIR`) | List of directories to index. |
|
|
334
439
|
| `EXPLAIN_ERRORS_RAG_EXCLUDE` | migrations, venvs, `node_modules`, static, media, `.git` | Directory names to skip while indexing. |
|
|
335
440
|
| `EXPLAIN_ERRORS_RAG_MAX_PROMPT_CHARS` | `6000` | Combined character budget for the traceback + retrieved source sections of the prompt. |
|
|
336
441
|
|
|
337
442
|
Every chunk of source code and every retrieval query is passed through the
|
|
338
443
|
same `sanitize_traceback()` redaction used for tracebacks, which strips
|
|
339
|
-
patterns that look like secrets, tokens, and emails before anything is sent
|
|
340
|
-
to OpenAI or written to the index. It's a pattern-based filter, not a
|
|
444
|
+
patterns that look like secrets, tokens, and emails before anything is sent to the model API or written to the index. It's a pattern-based filter, not a
|
|
341
445
|
guarantee: it catches recognizable secret shapes, not arbitrary sensitive
|
|
342
446
|
data that doesn't match one.
|
|
343
447
|
|
|
@@ -361,40 +465,112 @@ else.)
|
|
|
361
465
|
|
|
362
466
|
### Before / after
|
|
363
467
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
**With RAG**, grounded in the actual function:
|
|
468
|
+
A real result from the eval harness (`missing_fk`, one of the fixtures in
|
|
469
|
+
`evals/fixtures.py`): a view creates a new `Post` without setting the
|
|
470
|
+
required `author` foreign key. The traceback the model actually received
|
|
471
|
+
was already truncated to `OPENAI_MAX_TRACEBACK_CHARS`, so it contains no
|
|
472
|
+
application code at all, only Django/SQLite internals:
|
|
371
473
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
474
|
+
This example was recorded before 0.7.0, when tracebacks were trimmed to
|
|
475
|
+
their last N characters. Since 0.7.0 your application's frames are always
|
|
476
|
+
kept, so a traceback-only explanation would now see `clone_latest_post`
|
|
477
|
+
too. The measured results in "Does RAG actually help?" below were run
|
|
478
|
+
after that change.
|
|
377
479
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
480
|
+
```
|
|
481
|
+
...(truncated)...
|
|
482
|
+
File ".../django/db/models/base.py", line 904, in save
|
|
483
|
+
self.save_base(
|
|
484
|
+
...
|
|
485
|
+
File ".../django/db/backends/utils.py", line 105, in _execute
|
|
486
|
+
return self.cursor.execute(sql, params)
|
|
487
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
488
|
+
File ".../django/db/backends/sqlite3/base.py", line 359, in execute
|
|
489
|
+
return super().execute(query, params)
|
|
490
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
491
|
+
django.db.utils.IntegrityError: NOT NULL constraint failed: blog_post.author_id
|
|
492
|
+
```
|
|
386
493
|
|
|
387
|
-
|
|
388
|
-
...
|
|
389
|
-
'explain_errors.middleware.ExplainErrorsMiddleware',
|
|
390
|
-
]
|
|
494
|
+
**Without RAG**, traceback only:
|
|
391
495
|
|
|
392
|
-
|
|
496
|
+
> The error "django.db.utils.IntegrityError: NOT NULL constraint failed: blog_post.author_id" occurs when you attempt to save a model instance but the `author_id` field does not receive a value, even though it requires one (is defined as `NOT NULL`).
|
|
497
|
+
>
|
|
498
|
+
> This typically indicates that the `author` field in your `Post` model (or similar) is mandatory, but you did not provide an `author` when creating or updating the post.
|
|
499
|
+
>
|
|
500
|
+
> To fix this:
|
|
501
|
+
>
|
|
502
|
+
> 1. Ensure that you are assigning a valid user or author instance to the `author` field before saving the `Post` model. For example:
|
|
503
|
+
> ```python
|
|
504
|
+
> post.author = some_user_instance
|
|
505
|
+
> ```
|
|
506
|
+
>
|
|
507
|
+
> 2. If you intended for `author_id` to be optional, adjust the model definition to allow null values by modifying the field:
|
|
508
|
+
> ```python
|
|
509
|
+
> author = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
|
|
510
|
+
> ```
|
|
511
|
+
>
|
|
512
|
+
> 3. If the migration is necessary (if you did change the model), run:
|
|
513
|
+
> ```bash
|
|
514
|
+
> python manage.py makemigrations
|
|
515
|
+
> python manage.py migrate
|
|
516
|
+
> ```
|
|
517
|
+
>
|
|
518
|
+
> Make sure that every time you save a `Post`, it has a valid author assigned.
|
|
393
519
|
|
|
394
|
-
|
|
395
|
-
```
|
|
520
|
+
**With RAG**, grounded in the actual function:
|
|
396
521
|
|
|
397
|
-
|
|
522
|
+
> The error `django.db.utils.IntegrityError: NOT NULL constraint failed: blog_post.author_id` occurs because when you're creating a new `Post` instance in `clone_latest_post`, you're not providing an `author` for the new draft. The `author` field in the `Post` model is a ForeignKey that cannot be null, and thus must be populated.
|
|
523
|
+
>
|
|
524
|
+
> To fix this, modify the `clone_latest_post` function to include an `author` when creating the draft. You could retrieve the author from the latest post and use it like so:
|
|
525
|
+
>
|
|
526
|
+
> ```python
|
|
527
|
+
> def clone_latest_post(request):
|
|
528
|
+
> """Duplicate the most recent post as a new draft, ready for editing."""
|
|
529
|
+
> latest = Post.objects.order_by("-id").first()
|
|
530
|
+
> draft = Post.objects.create(
|
|
531
|
+
> title=f"Copy of {latest.title}",
|
|
532
|
+
> slug=f"copy-of-{latest.slug}",
|
|
533
|
+
> body=latest.body,
|
|
534
|
+
> author=latest.author, # Add this line to specify the author
|
|
535
|
+
> )
|
|
536
|
+
> return HttpResponse(f"Created draft #{draft.id}")
|
|
537
|
+
> ```
|
|
538
|
+
>
|
|
539
|
+
> This ensures the `draft` has a valid `author`, satisfying the NOT NULL constraint.
|
|
540
|
+
|
|
541
|
+
### Does RAG actually help?
|
|
542
|
+
|
|
543
|
+
To check whether RAG-grounded explanations are actually better, not just
|
|
544
|
+
longer, the package ships an eval harness (`evals/`): fifteen deliberately
|
|
545
|
+
broken Django views, each explained twice (once from the traceback alone,
|
|
546
|
+
once with RAG enabled) and judged by a separate model, blind to which
|
|
547
|
+
explanation is which, against the error's known cause and correct fix
|
|
548
|
+
location. Which side the judge sees as "A" is randomized per comparison so
|
|
549
|
+
position can't bias the result.
|
|
550
|
+
|
|
551
|
+
Across three runs (45 judged comparisons, 2 judge failures, 43 scored),
|
|
552
|
+
RAG-on won 35, RAG-off 5, and 3 tied. The gap isn't spread evenly across
|
|
553
|
+
everything the judge checks. It's concentrated in whether the explanation
|
|
554
|
+
names the right file and function, and whether it invents details along
|
|
555
|
+
the way: on `points_to_fix_location`, RAG-on answered yes in 26 of the
|
|
556
|
+
group-A comparisons against RAG-off's 13; on `no_fabrication`, 26 against
|
|
557
|
+
17. Without source access, `gpt-4o-mini` tends to invent a
|
|
558
|
+
plausible-sounding function name or parameter rather than say it doesn't
|
|
559
|
+
know; given the actual code via RAG, it mostly does not.
|
|
560
|
+
|
|
561
|
+
Three limitations are worth knowing before trusting this uncritically: RAG
|
|
562
|
+
can anchor on the wrong retrieved chunk, as it did in one fixture
|
|
563
|
+
(`missing_post_key`) where the fix got redirected to a retrieved template
|
|
564
|
+
instead of the view; the judge is shown the failing function's own
|
|
565
|
+
source, which is the same source RAG-on's retriever draws from, so part
|
|
566
|
+
of RAG-on's `no_fabrication` advantage may be judge and generator
|
|
567
|
+
overlapping on material RAG-off never sees rather than RAG-on being more
|
|
568
|
+
careful; and claim statuses are spot-checked, not exhaustively audited --
|
|
569
|
+
a script that flagged 14 of 363 claims on one run, all correct on manual
|
|
570
|
+
inspection, is a sample that turned up no false positive, not a proof
|
|
571
|
+
that none exists. Full per-fixture results, the judge prompt, and how to
|
|
572
|
+
reproduce this (about $1.37 for a `--runs 3` pass, most of it judge cost)
|
|
573
|
+
are in [`evals/README.md`](evals/README.md).
|
|
398
574
|
|
|
399
575
|
## License
|
|
400
576
|
|
|
@@ -407,5 +583,5 @@ Contributions are welcome! Please open an issue or submit a pull request for any
|
|
|
407
583
|
## Acknowledgements
|
|
408
584
|
|
|
409
585
|
- [Django](https://www.djangoproject.com/)
|
|
410
|
-
- [OpenAI](https://
|
|
586
|
+
- [OpenAI Python SDK](https://github.com/openai/openai-python)
|
|
411
587
|
- [python-dotenv](https://github.com/theskumar/python-dotenv)
|