django-explain-errors 0.2.0__tar.gz → 0.3.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 (29) hide show
  1. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/LICENSE +1 -1
  2. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/MANIFEST.in +0 -1
  3. {django_explain_errors-0.2.0/django_explain_errors.egg-info → django_explain_errors-0.3.0}/PKG-INFO +104 -4
  4. django_explain_errors-0.3.0/README.md +204 -0
  5. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0/django_explain_errors.egg-info}/PKG-INFO +104 -4
  6. django_explain_errors-0.3.0/django_explain_errors.egg-info/SOURCES.txt +24 -0
  7. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/django_explain_errors.egg-info/requires.txt +3 -0
  8. django_explain_errors-0.3.0/explain_errors/management/__init__.py +0 -0
  9. django_explain_errors-0.3.0/explain_errors/management/commands/__init__.py +0 -0
  10. django_explain_errors-0.3.0/explain_errors/management/commands/build_error_index.py +18 -0
  11. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/explain_errors/middleware.py +39 -5
  12. django_explain_errors-0.3.0/explain_errors/rag/__init__.py +0 -0
  13. django_explain_errors-0.3.0/explain_errors/rag/indexer.py +198 -0
  14. django_explain_errors-0.3.0/explain_errors/rag/retriever.py +143 -0
  15. django_explain_errors-0.3.0/explain_errors/rag/store.py +93 -0
  16. django_explain_errors-0.3.0/explain_errors/sanitize.py +44 -0
  17. django_explain_errors-0.3.0/explain_errors/throttle.py +29 -0
  18. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/setup.py +6 -1
  19. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/tests/test_middleware.py +32 -2
  20. django_explain_errors-0.3.0/tests/test_rag.py +391 -0
  21. django_explain_errors-0.3.0/tests/test_sanitize.py +74 -0
  22. django_explain_errors-0.3.0/tests/test_throttle.py +48 -0
  23. django_explain_errors-0.2.0/README.md +0 -107
  24. django_explain_errors-0.2.0/README.rst +0 -107
  25. django_explain_errors-0.2.0/django_explain_errors.egg-info/SOURCES.txt +0 -13
  26. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/django_explain_errors.egg-info/dependency_links.txt +0 -0
  27. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/django_explain_errors.egg-info/top_level.txt +0 -0
  28. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/explain_errors/__init__.py +0 -0
  29. {django_explain_errors-0.2.0 → django_explain_errors-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Mike
3
+ Copyright (c) 2024 Mike
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,3 +1,2 @@
1
1
  include README.md
2
- include README.rst
3
2
  include LICENSE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-explain-errors
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
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
5
  Home-page: https://github.com/topunix/django-explain-errors
6
6
  Author: topunix
@@ -30,6 +30,8 @@ Requires-Dist: Django>=4.2
30
30
  Requires-Dist: openai>=1.0
31
31
  Requires-Dist: python-dotenv>=1.0
32
32
  Requires-Dist: asgiref>=3.6
33
+ Provides-Extra: rag
34
+ Requires-Dist: sqlite-vec>=0.1.0; extra == "rag"
33
35
  Dynamic: author
34
36
  Dynamic: author-email
35
37
  Dynamic: classifier
@@ -38,22 +40,26 @@ Dynamic: description-content-type
38
40
  Dynamic: home-page
39
41
  Dynamic: license
40
42
  Dynamic: license-file
43
+ Dynamic: provides-extra
41
44
  Dynamic: requires-dist
42
45
  Dynamic: requires-python
43
46
  Dynamic: summary
44
47
 
45
48
  # Django Explain Errors Middleware
46
49
 
47
- This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It uses an environment variable to securely manage the OpenAI API key.
50
+ This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It can optionally ground explanations in your own project source code using a local vector index (RAG), so explanations reference the actual code that failed instead of staying generic.
48
51
 
49
- The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type.
52
+ The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type. Tracebacks are sanitized before leaving the process, and API calls are rate limited. It uses an environment variable to securely manage the OpenAI API key.
50
53
 
51
54
  ## Features
52
55
 
53
56
  - Captures Django errors and exceptions
54
57
  - Uses OpenAI to explain the error
55
- - Securely manages the OpenAI API key using environment variables
58
+ - Optional codebase-aware explanations (RAG) backed by a local sqlite-vec index (see the RAG section below)
59
+ - Redacts secrets, tokens, and emails from tracebacks before sending
60
+ - Rate limits API calls with a configurable sliding window
56
61
  - Works with both sync (WSGI) and async (ASGI) views
62
+ - Securely manages the OpenAI API key using environment variables
57
63
 
58
64
  ## Installation
59
65
 
@@ -115,6 +121,100 @@ No additional settings are needed. Place the middleware last in `MIDDLEWARE` for
115
121
  | `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
116
122
  | `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
117
123
 
124
+ ## Codebase-aware explanations (RAG)
125
+
126
+ By default, explanations are generated from the traceback alone. With the
127
+ optional RAG (retrieval-augmented generation) layer enabled, the middleware
128
+ also retrieves the most relevant chunks of your own project's source code
129
+ from a local vector index and includes them in the prompt, so explanations
130
+ can reference your actual functions and classes instead of guessing at them.
131
+
132
+ This feature is opt-in and adds no dependencies or behavior unless enabled.
133
+
134
+ ### Install the extra
135
+
136
+ ```bash
137
+ pip install django-explain-errors[rag]
138
+ ```
139
+
140
+ This pulls in [sqlite-vec](https://github.com/asg017/sqlite-vec), a
141
+ single-file, no-server vector store. The core package stays dependency-light
142
+ if you don't need RAG.
143
+
144
+ ### Build the index
145
+
146
+ Add `explain_errors` to `INSTALLED_APPS` (needed for Django to discover the
147
+ management command), then run:
148
+
149
+ ```bash
150
+ python manage.py build_error_index
151
+ ```
152
+
153
+ This walks your project, chunks Python files by top-level function/class
154
+ (and other text files by fixed-size line windows), embeds each chunk with
155
+ the OpenAI embeddings API, and writes them to a local index file. Re-run it
156
+ whenever your source changes meaningfully — indexing is not automatic.
157
+ Rebuilding is idempotent: it builds into a temp file and atomically replaces
158
+ the previous index.
159
+
160
+ ### Enable it
161
+
162
+ ```python
163
+ # settings.py
164
+
165
+ EXPLAIN_ERRORS_RAG_ENABLED = True
166
+ ```
167
+
168
+ ### Settings
169
+
170
+ | Setting | Default | Description |
171
+ | ------- | ------- | ----------- |
172
+ | `EXPLAIN_ERRORS_RAG_ENABLED` | `False` | Master switch for the RAG layer. |
173
+ | `EXPLAIN_ERRORS_RAG_INDEX_PATH` | `<BASE_DIR>/.explain_errors_index.db` | Path to the local vector index file. |
174
+ | `EXPLAIN_ERRORS_RAG_TOP_K` | `4` | Number of chunks retrieved and injected into the prompt. |
175
+ | `EXPLAIN_ERRORS_RAG_EMBED_MODEL` | `"text-embedding-3-small"` | OpenAI embedding model used for indexing and retrieval. |
176
+ | `EXPLAIN_ERRORS_RAG_INCLUDE` | `None` (defaults to `BASE_DIR`) | List of directories to index. |
177
+ | `EXPLAIN_ERRORS_RAG_EXCLUDE` | migrations, venvs, `node_modules`, static, media, `.git` | Directory names to skip while indexing. |
178
+ | `EXPLAIN_ERRORS_RAG_MAX_PROMPT_CHARS` | `6000` | Combined character budget for the traceback + retrieved source sections of the prompt. |
179
+
180
+ Every chunk of source code and every retrieval query is passed through the
181
+ same `sanitize_traceback()` redaction used for tracebacks, so secrets in
182
+ your source files are never sent to OpenAI or written to the index.
183
+
184
+ If RAG is enabled but the index is missing, `sqlite-vec` isn't installed, or
185
+ retrieval fails for any reason, the middleware logs a warning and falls back
186
+ to the traceback-only prompt — it never breaks error reporting.
187
+
188
+ RAG-grounded explanations tend to be longer than traceback-only ones. Consider raising `OPENAI_MAX_TOKENS` (for example to 500) when RAG is enabled so explanations are not truncated.
189
+
190
+ ### .gitignore
191
+
192
+ The index file is a local build artifact, not something to commit. Add it
193
+ to your project's `.gitignore`:
194
+
195
+ ```
196
+ .explain_errors_index.db
197
+ ```
198
+
199
+ (Adjust the path if you set `EXPLAIN_ERRORS_RAG_INDEX_PATH` to something
200
+ else.)
201
+
202
+ ### Before / after
203
+
204
+ **Without RAG** — traceback only:
205
+
206
+ > Your `ValueError` is raised because the value passed to `foo()` couldn't
207
+ > be converted to an integer. Check where `foo()` is called and make sure
208
+ > you're passing a numeric string.
209
+
210
+ **With RAG** — grounded in the actual function:
211
+
212
+ > In `myapp/utils.py`, `foo()` calls `int(value)` on line 12 without a
213
+ > `try`/`except`, so any non-numeric `value` raises `ValueError` straight
214
+ > through to the caller. Since `foo()` is called from `myapp/views.py` with
215
+ > unvalidated form input, add validation there or wrap the `int()` call in
216
+ > `foo()` with a clear error message.
217
+
118
218
  ## Example
119
219
 
120
220
  Here is an example of how to use the middleware in a Django project:
@@ -0,0 +1,204 @@
1
+ # Django Explain Errors Middleware
2
+
3
+ This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It can optionally ground explanations in your own project source code using a local vector index (RAG), so explanations reference the actual code that failed instead of staying generic.
4
+
5
+ The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type. Tracebacks are sanitized before leaving the process, and API calls are rate limited. It uses an environment variable to securely manage the OpenAI API key.
6
+
7
+ ## Features
8
+
9
+ - Captures Django errors and exceptions
10
+ - Uses OpenAI to explain the error
11
+ - Optional codebase-aware explanations (RAG) backed by a local sqlite-vec index (see the RAG section below)
12
+ - Redacts secrets, tokens, and emails from tracebacks before sending
13
+ - Rate limits API calls with a configurable sliding window
14
+ - Works with both sync (WSGI) and async (ASGI) views
15
+ - Securely manages the OpenAI API key using environment variables
16
+
17
+ ## Installation
18
+
19
+ 1. Install django-explain-errors by running:
20
+ ```bash
21
+ pip install django-explain-errors
22
+ ```
23
+
24
+ 2. **Add the middleware to your Django project**:
25
+
26
+ - Open your `settings.py` file and add the middleware to the `MIDDLEWARE` list. Ensure that the middleware is added last in the list:
27
+
28
+ ```python
29
+ MIDDLEWARE = [
30
+ ...
31
+ 'explain_errors.middleware.ExplainErrorsMiddleware',
32
+ ]
33
+ ```
34
+
35
+ 3. **Set up environment variables**:
36
+
37
+ - Create a `.env` file in your project's root directory and add your OpenAI API key. Alternatively, you can set the API key in `settings.py`:
38
+
39
+ ```plaintext
40
+ OPENAI_API_KEY=your_openai_api_key_here
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ 1. **Ensure DEBUG is set to True**:
46
+
47
+ Open your `settings.py` file and set:
48
+
49
+ ```python
50
+ DEBUG = True
51
+ ```
52
+
53
+ 2. **Trigger an error in your Django application**:
54
+
55
+ The middleware will capture the error, send it to OpenAI for explanation, and print the explanation to stdout. When an exception is caught, it returns a JSON `500` response containing the error message and the explanation.
56
+
57
+ ## Async Support
58
+
59
+ 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:
60
+
61
+ - Under WSGI (for example `runserver` with sync views), requests flow through the synchronous handler.
62
+ - Under ASGI (for example with async views), requests are awaited through the async handler. The blocking OpenAI call is offloaded with `asgiref.sync.sync_to_async` so the event loop is not blocked.
63
+
64
+ No additional settings are needed. Place the middleware last in `MIDDLEWARE` for both modes.
65
+
66
+ ## Configuration
67
+
68
+ | Setting / variable | Required | Description |
69
+ | ------------------ | -------- | ----------- |
70
+ | `OPENAI_API_KEY` (env or settings) | Yes, when `DEBUG=True` | API key used to authenticate with OpenAI. Read first from the environment, then from `settings`. |
71
+ | `DEBUG` | Yes | The middleware is only active when `DEBUG=True`. When `False`, requests pass through untouched. |
72
+ | `OPENAI_MODEL` | No | Model used for explanations. Defaults to `gpt-4o-mini`. |
73
+ | `OPENAI_MAX_TOKENS` | No | Maximum tokens in the explanation. Defaults to `150`. |
74
+ | `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
75
+ | `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
76
+
77
+ ## Codebase-aware explanations (RAG)
78
+
79
+ By default, explanations are generated from the traceback alone. With the
80
+ optional RAG (retrieval-augmented generation) layer enabled, the middleware
81
+ also retrieves the most relevant chunks of your own project's source code
82
+ from a local vector index and includes them in the prompt, so explanations
83
+ can reference your actual functions and classes instead of guessing at them.
84
+
85
+ This feature is opt-in and adds no dependencies or behavior unless enabled.
86
+
87
+ ### Install the extra
88
+
89
+ ```bash
90
+ pip install django-explain-errors[rag]
91
+ ```
92
+
93
+ This pulls in [sqlite-vec](https://github.com/asg017/sqlite-vec), a
94
+ single-file, no-server vector store. The core package stays dependency-light
95
+ if you don't need RAG.
96
+
97
+ ### Build the index
98
+
99
+ Add `explain_errors` to `INSTALLED_APPS` (needed for Django to discover the
100
+ management command), then run:
101
+
102
+ ```bash
103
+ python manage.py build_error_index
104
+ ```
105
+
106
+ This walks your project, chunks Python files by top-level function/class
107
+ (and other text files by fixed-size line windows), embeds each chunk with
108
+ the OpenAI embeddings API, and writes them to a local index file. Re-run it
109
+ whenever your source changes meaningfully — indexing is not automatic.
110
+ Rebuilding is idempotent: it builds into a temp file and atomically replaces
111
+ the previous index.
112
+
113
+ ### Enable it
114
+
115
+ ```python
116
+ # settings.py
117
+
118
+ EXPLAIN_ERRORS_RAG_ENABLED = True
119
+ ```
120
+
121
+ ### Settings
122
+
123
+ | Setting | Default | Description |
124
+ | ------- | ------- | ----------- |
125
+ | `EXPLAIN_ERRORS_RAG_ENABLED` | `False` | Master switch for the RAG layer. |
126
+ | `EXPLAIN_ERRORS_RAG_INDEX_PATH` | `<BASE_DIR>/.explain_errors_index.db` | Path to the local vector index file. |
127
+ | `EXPLAIN_ERRORS_RAG_TOP_K` | `4` | Number of chunks retrieved and injected into the prompt. |
128
+ | `EXPLAIN_ERRORS_RAG_EMBED_MODEL` | `"text-embedding-3-small"` | OpenAI embedding model used for indexing and retrieval. |
129
+ | `EXPLAIN_ERRORS_RAG_INCLUDE` | `None` (defaults to `BASE_DIR`) | List of directories to index. |
130
+ | `EXPLAIN_ERRORS_RAG_EXCLUDE` | migrations, venvs, `node_modules`, static, media, `.git` | Directory names to skip while indexing. |
131
+ | `EXPLAIN_ERRORS_RAG_MAX_PROMPT_CHARS` | `6000` | Combined character budget for the traceback + retrieved source sections of the prompt. |
132
+
133
+ Every chunk of source code and every retrieval query is passed through the
134
+ same `sanitize_traceback()` redaction used for tracebacks, so secrets in
135
+ your source files are never sent to OpenAI or written to the index.
136
+
137
+ If RAG is enabled but the index is missing, `sqlite-vec` isn't installed, or
138
+ retrieval fails for any reason, the middleware logs a warning and falls back
139
+ to the traceback-only prompt — it never breaks error reporting.
140
+
141
+ RAG-grounded explanations tend to be longer than traceback-only ones. Consider raising `OPENAI_MAX_TOKENS` (for example to 500) when RAG is enabled so explanations are not truncated.
142
+
143
+ ### .gitignore
144
+
145
+ The index file is a local build artifact, not something to commit. Add it
146
+ to your project's `.gitignore`:
147
+
148
+ ```
149
+ .explain_errors_index.db
150
+ ```
151
+
152
+ (Adjust the path if you set `EXPLAIN_ERRORS_RAG_INDEX_PATH` to something
153
+ else.)
154
+
155
+ ### Before / after
156
+
157
+ **Without RAG** — traceback only:
158
+
159
+ > Your `ValueError` is raised because the value passed to `foo()` couldn't
160
+ > be converted to an integer. Check where `foo()` is called and make sure
161
+ > you're passing a numeric string.
162
+
163
+ **With RAG** — grounded in the actual function:
164
+
165
+ > In `myapp/utils.py`, `foo()` calls `int(value)` on line 12 without a
166
+ > `try`/`except`, so any non-numeric `value` raises `ValueError` straight
167
+ > through to the caller. Since `foo()` is called from `myapp/views.py` with
168
+ > unvalidated form input, add validation there or wrap the `int()` call in
169
+ > `foo()` with a clear error message.
170
+
171
+ ## Example
172
+
173
+ Here is an example of how to use the middleware in a Django project:
174
+
175
+ ```python
176
+ # settings.py
177
+
178
+ DEBUG = True
179
+
180
+ MIDDLEWARE = [
181
+ ...
182
+ 'explain_errors.middleware.ExplainErrorsMiddleware',
183
+ ]
184
+
185
+ # .env
186
+
187
+ OPENAI_API_KEY=your_openai_api_key_here
188
+ ```
189
+
190
+ When an error occurs, you will see an explanation printed to stdout.
191
+
192
+ ## License
193
+
194
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
195
+
196
+ ## Contributing
197
+
198
+ Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
199
+
200
+ ## Acknowledgements
201
+
202
+ - [Django](https://www.djangoproject.com/)
203
+ - [OpenAI](https://www.openai.com/)
204
+ - [python-dotenv](https://github.com/theskumar/python-dotenv)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-explain-errors
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
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
5
  Home-page: https://github.com/topunix/django-explain-errors
6
6
  Author: topunix
@@ -30,6 +30,8 @@ Requires-Dist: Django>=4.2
30
30
  Requires-Dist: openai>=1.0
31
31
  Requires-Dist: python-dotenv>=1.0
32
32
  Requires-Dist: asgiref>=3.6
33
+ Provides-Extra: rag
34
+ Requires-Dist: sqlite-vec>=0.1.0; extra == "rag"
33
35
  Dynamic: author
34
36
  Dynamic: author-email
35
37
  Dynamic: classifier
@@ -38,22 +40,26 @@ Dynamic: description-content-type
38
40
  Dynamic: home-page
39
41
  Dynamic: license
40
42
  Dynamic: license-file
43
+ Dynamic: provides-extra
41
44
  Dynamic: requires-dist
42
45
  Dynamic: requires-python
43
46
  Dynamic: summary
44
47
 
45
48
  # Django Explain Errors Middleware
46
49
 
47
- This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It uses an environment variable to securely manage the OpenAI API key.
50
+ This Django middleware captures errors and exceptions, sends them to OpenAI for explanation, and prints the explanation to stdout when debug mode is enabled. It can optionally ground explanations in your own project source code using a local vector index (RAG), so explanations reference the actual code that failed instead of staying generic.
48
51
 
49
- The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type.
52
+ The middleware supports both synchronous (WSGI) and asynchronous (ASGI) views. It auto-detects the view chain at startup and routes requests through the matching sync or async path, so no extra configuration is required to use it under either server type. Tracebacks are sanitized before leaving the process, and API calls are rate limited. It uses an environment variable to securely manage the OpenAI API key.
50
53
 
51
54
  ## Features
52
55
 
53
56
  - Captures Django errors and exceptions
54
57
  - Uses OpenAI to explain the error
55
- - Securely manages the OpenAI API key using environment variables
58
+ - Optional codebase-aware explanations (RAG) backed by a local sqlite-vec index (see the RAG section below)
59
+ - Redacts secrets, tokens, and emails from tracebacks before sending
60
+ - Rate limits API calls with a configurable sliding window
56
61
  - Works with both sync (WSGI) and async (ASGI) views
62
+ - Securely manages the OpenAI API key using environment variables
57
63
 
58
64
  ## Installation
59
65
 
@@ -115,6 +121,100 @@ No additional settings are needed. Place the middleware last in `MIDDLEWARE` for
115
121
  | `OPENAI_TIMEOUT` | No | Request timeout in seconds for the OpenAI client. Defaults to `10`. |
116
122
  | `OPENAI_MAX_TRACEBACK_CHARS` | No | Traceback is trimmed to its last N characters before being sent. Defaults to `3000`. |
117
123
 
124
+ ## Codebase-aware explanations (RAG)
125
+
126
+ By default, explanations are generated from the traceback alone. With the
127
+ optional RAG (retrieval-augmented generation) layer enabled, the middleware
128
+ also retrieves the most relevant chunks of your own project's source code
129
+ from a local vector index and includes them in the prompt, so explanations
130
+ can reference your actual functions and classes instead of guessing at them.
131
+
132
+ This feature is opt-in and adds no dependencies or behavior unless enabled.
133
+
134
+ ### Install the extra
135
+
136
+ ```bash
137
+ pip install django-explain-errors[rag]
138
+ ```
139
+
140
+ This pulls in [sqlite-vec](https://github.com/asg017/sqlite-vec), a
141
+ single-file, no-server vector store. The core package stays dependency-light
142
+ if you don't need RAG.
143
+
144
+ ### Build the index
145
+
146
+ Add `explain_errors` to `INSTALLED_APPS` (needed for Django to discover the
147
+ management command), then run:
148
+
149
+ ```bash
150
+ python manage.py build_error_index
151
+ ```
152
+
153
+ This walks your project, chunks Python files by top-level function/class
154
+ (and other text files by fixed-size line windows), embeds each chunk with
155
+ the OpenAI embeddings API, and writes them to a local index file. Re-run it
156
+ whenever your source changes meaningfully — indexing is not automatic.
157
+ Rebuilding is idempotent: it builds into a temp file and atomically replaces
158
+ the previous index.
159
+
160
+ ### Enable it
161
+
162
+ ```python
163
+ # settings.py
164
+
165
+ EXPLAIN_ERRORS_RAG_ENABLED = True
166
+ ```
167
+
168
+ ### Settings
169
+
170
+ | Setting | Default | Description |
171
+ | ------- | ------- | ----------- |
172
+ | `EXPLAIN_ERRORS_RAG_ENABLED` | `False` | Master switch for the RAG layer. |
173
+ | `EXPLAIN_ERRORS_RAG_INDEX_PATH` | `<BASE_DIR>/.explain_errors_index.db` | Path to the local vector index file. |
174
+ | `EXPLAIN_ERRORS_RAG_TOP_K` | `4` | Number of chunks retrieved and injected into the prompt. |
175
+ | `EXPLAIN_ERRORS_RAG_EMBED_MODEL` | `"text-embedding-3-small"` | OpenAI embedding model used for indexing and retrieval. |
176
+ | `EXPLAIN_ERRORS_RAG_INCLUDE` | `None` (defaults to `BASE_DIR`) | List of directories to index. |
177
+ | `EXPLAIN_ERRORS_RAG_EXCLUDE` | migrations, venvs, `node_modules`, static, media, `.git` | Directory names to skip while indexing. |
178
+ | `EXPLAIN_ERRORS_RAG_MAX_PROMPT_CHARS` | `6000` | Combined character budget for the traceback + retrieved source sections of the prompt. |
179
+
180
+ Every chunk of source code and every retrieval query is passed through the
181
+ same `sanitize_traceback()` redaction used for tracebacks, so secrets in
182
+ your source files are never sent to OpenAI or written to the index.
183
+
184
+ If RAG is enabled but the index is missing, `sqlite-vec` isn't installed, or
185
+ retrieval fails for any reason, the middleware logs a warning and falls back
186
+ to the traceback-only prompt — it never breaks error reporting.
187
+
188
+ RAG-grounded explanations tend to be longer than traceback-only ones. Consider raising `OPENAI_MAX_TOKENS` (for example to 500) when RAG is enabled so explanations are not truncated.
189
+
190
+ ### .gitignore
191
+
192
+ The index file is a local build artifact, not something to commit. Add it
193
+ to your project's `.gitignore`:
194
+
195
+ ```
196
+ .explain_errors_index.db
197
+ ```
198
+
199
+ (Adjust the path if you set `EXPLAIN_ERRORS_RAG_INDEX_PATH` to something
200
+ else.)
201
+
202
+ ### Before / after
203
+
204
+ **Without RAG** — traceback only:
205
+
206
+ > Your `ValueError` is raised because the value passed to `foo()` couldn't
207
+ > be converted to an integer. Check where `foo()` is called and make sure
208
+ > you're passing a numeric string.
209
+
210
+ **With RAG** — grounded in the actual function:
211
+
212
+ > In `myapp/utils.py`, `foo()` calls `int(value)` on line 12 without a
213
+ > `try`/`except`, so any non-numeric `value` raises `ValueError` straight
214
+ > through to the caller. Since `foo()` is called from `myapp/views.py` with
215
+ > unvalidated form input, add validation there or wrap the `int()` call in
216
+ > `foo()` with a clear error message.
217
+
118
218
  ## Example
119
219
 
120
220
  Here is an example of how to use the middleware in a Django project:
@@ -0,0 +1,24 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ setup.py
5
+ django_explain_errors.egg-info/PKG-INFO
6
+ django_explain_errors.egg-info/SOURCES.txt
7
+ django_explain_errors.egg-info/dependency_links.txt
8
+ django_explain_errors.egg-info/requires.txt
9
+ django_explain_errors.egg-info/top_level.txt
10
+ explain_errors/__init__.py
11
+ explain_errors/middleware.py
12
+ explain_errors/sanitize.py
13
+ explain_errors/throttle.py
14
+ explain_errors/management/__init__.py
15
+ explain_errors/management/commands/__init__.py
16
+ explain_errors/management/commands/build_error_index.py
17
+ explain_errors/rag/__init__.py
18
+ explain_errors/rag/indexer.py
19
+ explain_errors/rag/retriever.py
20
+ explain_errors/rag/store.py
21
+ tests/test_middleware.py
22
+ tests/test_rag.py
23
+ tests/test_sanitize.py
24
+ tests/test_throttle.py
@@ -2,3 +2,6 @@ Django>=4.2
2
2
  openai>=1.0
3
3
  python-dotenv>=1.0
4
4
  asgiref>=3.6
5
+
6
+ [rag]
7
+ sqlite-vec>=0.1.0
@@ -0,0 +1,18 @@
1
+ from django.core.management.base import BaseCommand
2
+
3
+ from explain_errors.rag.indexer import build_index
4
+
5
+
6
+ class Command(BaseCommand):
7
+ help = (
8
+ "Build (or rebuild) the local vector index used to ground "
9
+ "explain_errors explanations in project source code."
10
+ )
11
+
12
+ def handle(self, *args, **options):
13
+ result = build_index()
14
+ self.stdout.write(
15
+ f"explain_errors: scanned {result['files_scanned']} files, "
16
+ f"embedded {result['chunks_embedded']} chunks.\n"
17
+ f"Index written to {result['index_path']}"
18
+ )
@@ -1,13 +1,20 @@
1
1
  import os
2
2
  import asyncio
3
+ import logging
3
4
  import traceback
4
5
 
5
6
  from openai import OpenAI
6
- from dotenv import load_dotenv
7
+ from dotenv import load_dotenv, find_dotenv
7
8
  from django.conf import settings
8
9
  from django.http import JsonResponse
9
10
  from asgiref.sync import sync_to_async
10
11
 
12
+ from .sanitize import sanitize_traceback
13
+ from .throttle import SlidingWindowThrottle
14
+ from .rag.retriever import format_chunks_for_prompt, retrieve_chunks
15
+
16
+ logger = logging.getLogger(__name__)
17
+
11
18
 
12
19
  class ExplainErrorsMiddleware:
13
20
  """
@@ -22,12 +29,16 @@ class ExplainErrorsMiddleware:
22
29
  def __init__(self, get_response):
23
30
  self.get_response = get_response
24
31
  self._is_async = asyncio.iscoroutinefunction(get_response)
25
- self.api_called = False
26
32
  self.openai_client = None
33
+ self.throttle = None
27
34
 
28
35
  if settings.DEBUG:
36
+ max_calls = getattr(settings, "EXPLAIN_ERRORS_MAX_CALLS", 5)
37
+ window_seconds = getattr(settings, "EXPLAIN_ERRORS_WINDOW_SECONDS", 60)
38
+ self.throttle = SlidingWindowThrottle(max_calls, window_seconds)
39
+
29
40
  # Load environment variables from .env file
30
- load_dotenv()
41
+ load_dotenv(find_dotenv(usecwd=True))
31
42
  # Get the OpenAI API key from environment variable (or settings)
32
43
  openai_api_key = os.getenv(
33
44
  "OPENAI_API_KEY", getattr(settings, "OPENAI_API_KEY", None)
@@ -74,16 +85,40 @@ class ExplainErrorsMiddleware:
74
85
  return None
75
86
 
76
87
  explanation = None
77
- if not self.api_called:
88
+ if self.throttle.allow():
78
89
  # Get the exception traceback, trimmed to the most recent frames to
79
90
  # cap token usage and stay within the model's context window.
80
91
  tb = traceback.format_exc()
81
92
  max_tb_chars = getattr(settings, "OPENAI_MAX_TRACEBACK_CHARS", 3000)
82
93
  if len(tb) > max_tb_chars:
83
94
  tb = "...(truncated)...\n" + tb[-max_tb_chars:]
95
+ # Sanitize the exact payload that ships, after truncation so we
96
+ # don't waste work redacting frames that get discarded.
97
+ tb = sanitize_traceback(tb)
98
+
84
99
  # Construct the prompt
85
100
  prompt = f"Explain the following Django error in simple terms:\n\n{tb}"
86
101
 
102
+ # RAG: ground the explanation in the user's own project source.
103
+ # Opt-in and must never break the traceback-only path, so any
104
+ # failure here is logged and swallowed.
105
+ if getattr(settings, "EXPLAIN_ERRORS_RAG_ENABLED", False):
106
+ try:
107
+ chunks = retrieve_chunks(exception)
108
+ max_prompt_chars = getattr(
109
+ settings, "EXPLAIN_ERRORS_RAG_MAX_PROMPT_CHARS", 6000
110
+ )
111
+ remaining_chars = max(max_prompt_chars - len(tb), 0)
112
+ rag_section = format_chunks_for_prompt(chunks, remaining_chars)
113
+ if rag_section:
114
+ prompt += f"\n\n{rag_section}"
115
+ except Exception as exc:
116
+ logger.warning(
117
+ "explain_errors: RAG retrieval failed, falling back to "
118
+ "traceback-only prompt: %s",
119
+ exc,
120
+ )
121
+
87
122
  try:
88
123
  # Call OpenAI API
89
124
  response = self.openai_client.chat.completions.create(
@@ -98,7 +133,6 @@ class ExplainErrorsMiddleware:
98
133
 
99
134
  # Print the explanation to stdout
100
135
  print("Error Explanation by OpenAI:\n", explanation)
101
- self.api_called = True # Set flag after the call
102
136
  except Exception as e:
103
137
  # If the OpenAI call fails, surface the failure but still return
104
138
  # a 500 so the request lifecycle completes cleanly.