django-explain-errors 0.9.0__tar.gz → 0.9.1__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 (40) hide show
  1. {django_explain_errors-0.9.0/django_explain_errors.egg-info → django_explain_errors-0.9.1}/PKG-INFO +29 -36
  2. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/README.md +28 -35
  3. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1/django_explain_errors.egg-info}/PKG-INFO +29 -36
  4. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/rag/indexer.py +14 -8
  5. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/setup.py +1 -1
  6. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_rag.py +143 -0
  7. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/LICENSE +0 -0
  8. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/MANIFEST.in +0 -0
  9. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/django_explain_errors.egg-info/SOURCES.txt +0 -0
  10. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/django_explain_errors.egg-info/dependency_links.txt +0 -0
  11. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/django_explain_errors.egg-info/requires.txt +0 -0
  12. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/django_explain_errors.egg-info/top_level.txt +0 -0
  13. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/__init__.py +0 -0
  14. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/client.py +0 -0
  15. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/debug_page.py +0 -0
  16. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/management/__init__.py +0 -0
  17. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/management/commands/__init__.py +0 -0
  18. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/management/commands/build_error_index.py +0 -0
  19. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/middleware.py +0 -0
  20. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/rag/__init__.py +0 -0
  21. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/rag/retriever.py +0 -0
  22. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/rag/store.py +0 -0
  23. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/sanitize.py +0 -0
  24. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/throttle.py +0 -0
  25. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/explain_errors/tracebacks.py +0 -0
  26. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/setup.cfg +0 -0
  27. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_build_error_index.py +0 -0
  28. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_client.py +0 -0
  29. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_debug_page.py +0 -0
  30. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_eval_fixtures.py +0 -0
  31. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_eval_judge.py +0 -0
  32. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_eval_run.py +0 -0
  33. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_language.py +0 -0
  34. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_middleware.py +0 -0
  35. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_print_stdout.py +0 -0
  36. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_sanitize.py +0 -0
  37. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_signals.py +0 -0
  38. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_throttle.py +0 -0
  39. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_tracebacks.py +0 -0
  40. {django_explain_errors-0.9.0 → django_explain_errors-0.9.1}/tests/test_truncation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-explain-errors
3
- Version: 0.9.0
3
+ Version: 0.9.1
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
@@ -42,37 +42,31 @@ Dynamic: requires-dist
42
42
  Dynamic: requires-python
43
43
  Dynamic: summary
44
44
 
45
- # Django Explain Errors Middleware
46
-
47
- When debug mode is on, this Django middleware sends each unhandled exception
48
- to a language model and shows its explanation of what went wrong and how to
49
- fix it on Django's debug page, directly under the exception headline. The
50
- explanation is also printed to stdout. The middleware works with the OpenAI
51
- API 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, so
54
- 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.
65
-
66
- The middleware supports both synchronous (WSGI) and asynchronous (ASGI)
67
- views. It auto-detects the view chain at startup and routes requests through
68
- the matching path, so no extra configuration is required for either server
69
- type. Tracebacks are sanitized before leaving the process, and API calls are
70
- rate limited.
71
-
72
- ![Django debug page with the explanation banner under the exception headline](https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png)
73
-
74
- *Real `gpt-4o-mini` output with RAG enabled, recorded by the eval harness
75
- on its `missing_fk` fixture.*
45
+ # Django Explain Errors
46
+
47
+ Explains Django exceptions on the debug page, pointing at the real fix in your own source code.
48
+
49
+ <p align="center">
50
+ <a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/v/django-explain-errors" alt="PyPI version"></a>
51
+ <a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/pyversions/django-explain-errors" alt="Python versions"></a>
52
+ <a href="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml"><img src="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests"></a>
53
+ <a href="https://github.com/topunix/django-explain-errors/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/django-explain-errors" alt="License"></a>
54
+ </p>
55
+
56
+ <p align="center">
57
+ <img src="https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png" width="800"
58
+ alt="Django debug page with the explanation banner under the exception headline">
59
+ </p>
60
+
61
+ *The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
62
+ failing function (`clone_latest_post`) and gives the one-line fix. Real `gpt-4o-mini` output from
63
+ the eval harness.*
64
+
65
+ - **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
66
+ - Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
67
+ - Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with code and identifiers kept in English
68
+ - Sync and async views, traceback redaction, rate limiting
69
+ - Local development only: requires `DEBUG = True`
76
70
 
77
71
  ## Scope
78
72
 
@@ -85,8 +79,6 @@ If a coding agent is doing the debugging, it does not need this. Agents read tra
85
79
  and tools that expose live runtime state (debugger-over-MCP servers, `mcp-django`) serve that case
86
80
  better. This package is not trying to compete there.
87
81
 
88
- Local development only. It requires `DEBUG = True` and is inert otherwise.
89
-
90
82
  ## Features
91
83
 
92
84
  - Captures Django errors and exceptions
@@ -98,8 +90,9 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
98
90
  (`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
99
91
  - Optional JSON 500 response instead of the debug page
100
92
  (`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE = False`)
101
- - Explains errors using OpenAI, Anthropic's Claude models, or any other
102
- OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`
93
+ - Explains errors using OpenAI out of the box, Anthropic's Claude models, or any other
94
+ OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`,
95
+ including fully local models, so no code has to leave your machine
103
96
  - Codebase-aware explanations via RAG over a local sqlite-vec index
104
97
  (recommended; see "Codebase-aware explanations" below)
105
98
  - Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
@@ -1,34 +1,28 @@
1
- # Django Explain Errors Middleware
2
-
3
- When debug mode is on, this Django middleware sends each unhandled exception
4
- to a language model and shows its explanation of what went wrong and how to
5
- fix it on Django's debug page, directly under the exception headline. The
6
- explanation is also printed to stdout. The middleware works with the OpenAI
7
- API out of the box, with Anthropic's Claude models through Anthropic's
8
- OpenAI-compatible endpoint, and with any other OpenAI-compatible endpoint
9
- (Ollama, LM Studio, Azure, or a corporate gateway) by setting a base URL, so
10
- explanations can run entirely on a local model if you prefer not to send
11
- code off your machine.
12
-
13
- Enabling RAG is strongly recommended. With it, the middleware retrieves the
14
- relevant parts of your own project source from a local vector index, so
15
- explanations name the actual file and function that failed instead of
16
- guessing. In the package's eval harness, RAG-grounded explanations won 35
17
- of 43 blind comparisons, with the gap concentrated in pointing at the right
18
- fix location and not inventing details (see "Does RAG actually help?"
19
- below). It is off by default because it needs an optional extra and a
20
- one-time index build.
21
-
22
- The middleware supports both synchronous (WSGI) and asynchronous (ASGI)
23
- views. It auto-detects the view chain at startup and routes requests through
24
- the matching path, so no extra configuration is required for either server
25
- type. Tracebacks are sanitized before leaving the process, and API calls are
26
- rate limited.
27
-
28
- ![Django debug page with the explanation banner under the exception headline](https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png)
29
-
30
- *Real `gpt-4o-mini` output with RAG enabled, recorded by the eval harness
31
- on its `missing_fk` fixture.*
1
+ # Django Explain Errors
2
+
3
+ Explains Django exceptions on the debug page, pointing at the real fix in your own source code.
4
+
5
+ <p align="center">
6
+ <a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/v/django-explain-errors" alt="PyPI version"></a>
7
+ <a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/pyversions/django-explain-errors" alt="Python versions"></a>
8
+ <a href="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml"><img src="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests"></a>
9
+ <a href="https://github.com/topunix/django-explain-errors/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/django-explain-errors" alt="License"></a>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <img src="https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png" width="800"
14
+ alt="Django debug page with the explanation banner under the exception headline">
15
+ </p>
16
+
17
+ *The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
18
+ failing function (`clone_latest_post`) and gives the one-line fix. Real `gpt-4o-mini` output from
19
+ the eval harness.*
20
+
21
+ - **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
22
+ - Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
23
+ - Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with code and identifiers kept in English
24
+ - Sync and async views, traceback redaction, rate limiting
25
+ - Local development only: requires `DEBUG = True`
32
26
 
33
27
  ## Scope
34
28
 
@@ -41,8 +35,6 @@ If a coding agent is doing the debugging, it does not need this. Agents read tra
41
35
  and tools that expose live runtime state (debugger-over-MCP servers, `mcp-django`) serve that case
42
36
  better. This package is not trying to compete there.
43
37
 
44
- Local development only. It requires `DEBUG = True` and is inert otherwise.
45
-
46
38
  ## Features
47
39
 
48
40
  - Captures Django errors and exceptions
@@ -54,8 +46,9 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
54
46
  (`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
55
47
  - Optional JSON 500 response instead of the debug page
56
48
  (`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE = False`)
57
- - Explains errors using OpenAI, Anthropic's Claude models, or any other
58
- OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`
49
+ - Explains errors using OpenAI out of the box, Anthropic's Claude models, or any other
50
+ OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`,
51
+ including fully local models, so no code has to leave your machine
59
52
  - Codebase-aware explanations via RAG over a local sqlite-vec index
60
53
  (recommended; see "Codebase-aware explanations" below)
61
54
  - Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-explain-errors
3
- Version: 0.9.0
3
+ Version: 0.9.1
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
@@ -42,37 +42,31 @@ Dynamic: requires-dist
42
42
  Dynamic: requires-python
43
43
  Dynamic: summary
44
44
 
45
- # Django Explain Errors Middleware
46
-
47
- When debug mode is on, this Django middleware sends each unhandled exception
48
- to a language model and shows its explanation of what went wrong and how to
49
- fix it on Django's debug page, directly under the exception headline. The
50
- explanation is also printed to stdout. The middleware works with the OpenAI
51
- API 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, so
54
- 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.
65
-
66
- The middleware supports both synchronous (WSGI) and asynchronous (ASGI)
67
- views. It auto-detects the view chain at startup and routes requests through
68
- the matching path, so no extra configuration is required for either server
69
- type. Tracebacks are sanitized before leaving the process, and API calls are
70
- rate limited.
71
-
72
- ![Django debug page with the explanation banner under the exception headline](https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png)
73
-
74
- *Real `gpt-4o-mini` output with RAG enabled, recorded by the eval harness
75
- on its `missing_fk` fixture.*
45
+ # Django Explain Errors
46
+
47
+ Explains Django exceptions on the debug page, pointing at the real fix in your own source code.
48
+
49
+ <p align="center">
50
+ <a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/v/django-explain-errors" alt="PyPI version"></a>
51
+ <a href="https://pypi.org/project/django-explain-errors/"><img src="https://img.shields.io/pypi/pyversions/django-explain-errors" alt="Python versions"></a>
52
+ <a href="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml"><img src="https://github.com/topunix/django-explain-errors/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests"></a>
53
+ <a href="https://github.com/topunix/django-explain-errors/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/django-explain-errors" alt="License"></a>
54
+ </p>
55
+
56
+ <p align="center">
57
+ <img src="https://raw.githubusercontent.com/topunix/django-explain-errors/main/docs/images/debug-page-banner.png" width="800"
58
+ alt="Django debug page with the explanation banner under the exception headline">
59
+ </p>
60
+
61
+ *The traceback held only Django and SQLite internals. With RAG enabled, the explanation names the
62
+ failing function (`clone_latest_post`) and gives the one-line fix. Real `gpt-4o-mini` output from
63
+ the eval harness.*
64
+
65
+ - **Strongly recommended:** enable RAG over your project, so explanations name the actual file and function instead of guessing ([measured in the eval harness](#does-rag-actually-help)). It is off by default only because [setup](#codebase-aware-explanations-rag) takes three steps.
66
+ - Works with OpenAI, Claude, or any OpenAI-compatible endpoint, including local models via Ollama
67
+ - Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with code and identifiers kept in English
68
+ - Sync and async views, traceback redaction, rate limiting
69
+ - Local development only: requires `DEBUG = True`
76
70
 
77
71
  ## Scope
78
72
 
@@ -85,8 +79,6 @@ If a coding agent is doing the debugging, it does not need this. Agents read tra
85
79
  and tools that expose live runtime state (debugger-over-MCP servers, `mcp-django`) serve that case
86
80
  better. This package is not trying to compete there.
87
81
 
88
- Local development only. It requires `DEBUG = True` and is inert otherwise.
89
-
90
82
  ## Features
91
83
 
92
84
  - Captures Django errors and exceptions
@@ -98,8 +90,9 @@ Local development only. It requires `DEBUG = True` and is inert otherwise.
98
90
  (`EXPLAIN_ERRORS_PRINT_STDOUT`, on by default)
99
91
  - Optional JSON 500 response instead of the debug page
100
92
  (`EXPLAIN_ERRORS_PRESERVE_DEBUG_PAGE = False`)
101
- - Explains errors using OpenAI, Anthropic's Claude models, or any other
102
- OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`
93
+ - Explains errors using OpenAI out of the box, Anthropic's Claude models, or any other
94
+ OpenAI-compatible endpoint (Ollama, LM Studio, Azure, gateways) via `OPENAI_BASE_URL`,
95
+ including fully local models, so no code has to leave your machine
103
96
  - Codebase-aware explanations via RAG over a local sqlite-vec index
104
97
  (recommended; see "Codebase-aware explanations" below)
105
98
  - Explanations in your language via `EXPLAIN_ERRORS_LANGUAGE`, with exception names,
@@ -1,5 +1,6 @@
1
1
  """File discovery, chunking, and embedding for the RAG source index."""
2
2
  import ast
3
+ import contextlib
3
4
  import os
4
5
 
5
6
  from django.conf import settings
@@ -167,16 +168,21 @@ def build_index():
167
168
  os.remove(tmp_path)
168
169
 
169
170
  dimensions = len(embeddings[0]) if embeddings else 1536
170
- with VectorStore(tmp_path) as store:
171
- store.create(dimensions)
172
- store.add(
173
- (file_path, start_line, end_line, sanitized_text, embedding)
174
- for (file_path, start_line, end_line, _text), sanitized_text, embedding in zip(
175
- raw_chunks, sanitized_texts, embeddings
171
+ try:
172
+ with VectorStore(tmp_path) as store:
173
+ store.create(dimensions)
174
+ store.add(
175
+ (file_path, start_line, end_line, sanitized_text, embedding)
176
+ for (file_path, start_line, end_line, _text), sanitized_text, embedding in zip(
177
+ raw_chunks, sanitized_texts, embeddings
178
+ )
176
179
  )
177
- )
178
180
 
179
- os.replace(tmp_path, index_path)
181
+ os.replace(tmp_path, index_path)
182
+ except BaseException:
183
+ with contextlib.suppress(OSError):
184
+ os.remove(tmp_path)
185
+ raise
180
186
 
181
187
  return {
182
188
  "files_scanned": len(files),
@@ -10,7 +10,7 @@ with open('README.md', encoding='utf-8') as f:
10
10
 
11
11
  setup(
12
12
  name='django-explain-errors',
13
- version='0.9.0',
13
+ version='0.9.1',
14
14
  packages=find_packages(exclude=['tests', 'tests.*', 'evals', 'evals.*']),
15
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',
@@ -174,6 +174,76 @@ class IndexerBuildTest(SimpleTestCase):
174
174
  ]
175
175
  self.assertEqual(leftovers, [])
176
176
 
177
+ def test_failed_rebuild_keeps_previous_index_and_removes_temp_file(self):
178
+ with tempfile.TemporaryDirectory() as tmp:
179
+ with open(os.path.join(tmp, "app.py"), "w") as f:
180
+ f.write(SAMPLE_MODULE)
181
+
182
+ index_path = os.path.join(tmp, "index.db")
183
+
184
+ with override_settings(
185
+ EXPLAIN_ERRORS_RAG_INCLUDE=[tmp],
186
+ EXPLAIN_ERRORS_RAG_INDEX_PATH=index_path,
187
+ ):
188
+ with patch(
189
+ "explain_errors.rag.indexer.get_openai_client",
190
+ return_value=_mock_embed_client([0.1, 0.2, 0.3]),
191
+ ):
192
+ build_index()
193
+
194
+ with open(index_path, "rb") as f:
195
+ original_bytes = f.read()
196
+ with VectorStore(index_path) as store:
197
+ original_count = store.count()
198
+ self.assertGreater(original_count, 0)
199
+
200
+ with patch.object(
201
+ VectorStore, "add", side_effect=RuntimeError("add failed")
202
+ ):
203
+ with self.assertRaisesRegex(RuntimeError, "add failed"):
204
+ build_index()
205
+
206
+ with open(index_path, "rb") as f:
207
+ self.assertEqual(f.read(), original_bytes)
208
+ with VectorStore(index_path) as store:
209
+ self.assertEqual(store.count(), original_count)
210
+
211
+ leftovers = [
212
+ name for name in os.listdir(tmp) if name.startswith("index.db.tmp-")
213
+ ]
214
+ self.assertEqual(leftovers, [])
215
+
216
+ def test_build_index_redacts_secrets_in_stored_chunk_text(self):
217
+ with tempfile.TemporaryDirectory() as tmp:
218
+ src = os.path.join(tmp, "src")
219
+ os.makedirs(src)
220
+ with open(os.path.join(src, "config.py"), "w") as f:
221
+ f.write(
222
+ "def load():\n"
223
+ " api_key = 'sk-index-secret-value'\n"
224
+ " return api_key\n"
225
+ )
226
+
227
+ index_path = os.path.join(tmp, "index.db")
228
+ embed_client = _mock_embed_client([0.1, 0.2, 0.3])
229
+
230
+ with override_settings(
231
+ EXPLAIN_ERRORS_RAG_INCLUDE=[src],
232
+ EXPLAIN_ERRORS_RAG_INDEX_PATH=index_path,
233
+ ):
234
+ with patch(
235
+ "explain_errors.rag.indexer.get_openai_client",
236
+ return_value=embed_client,
237
+ ):
238
+ build_index()
239
+
240
+ with VectorStore(index_path) as store:
241
+ rows = store.query([0.1, 0.2, 0.3], 10)
242
+
243
+ self.assertEqual(len(rows), 1)
244
+ self.assertNotIn("sk-index-secret-value", rows[0]["chunk_text"])
245
+ self.assertIn("[REDACTED]", rows[0]["chunk_text"])
246
+
177
247
 
178
248
  def _raise_json_error():
179
249
  return json.loads("{not valid json")
@@ -192,6 +262,35 @@ class RetrieverFrameExtractionTest(SimpleTestCase):
192
262
  self.assertTrue(any(name.endswith("test_rag.py") for name in filenames))
193
263
  self.assertFalse(any("json" in os.path.basename(name) for name in filenames))
194
264
 
265
+ def test_extract_project_frames_excludes_site_packages_frame(self):
266
+ with tempfile.TemporaryDirectory() as tmp:
267
+ site_dir = os.path.join(tmp, "venv-lib", "site-packages")
268
+ os.makedirs(site_dir)
269
+ lib_path = os.path.join(site_dir, "thirdparty.py")
270
+ app_path = os.path.join(tmp, "app.py")
271
+ with open(lib_path, "w") as f:
272
+ f.write("def boom():\n raise ValueError('lib failure')\n")
273
+ with open(app_path, "w") as f:
274
+ f.write("def call(fn):\n return fn()\n")
275
+
276
+ namespaces = {}
277
+ for path in (lib_path, app_path):
278
+ ns = {}
279
+ with open(path) as f:
280
+ exec(compile(f.read(), path, "exec"), ns)
281
+ namespaces[path] = ns
282
+
283
+ # site-packages sits inside the include dir, so only the
284
+ # site-packages rule can exclude it.
285
+ with override_settings(EXPLAIN_ERRORS_RAG_INCLUDE=[tmp]):
286
+ try:
287
+ namespaces[app_path]["call"](namespaces[lib_path]["boom"])
288
+ except ValueError as exc:
289
+ frames = extract_project_frames(exc)
290
+
291
+ filenames = [frame.filename for frame in frames]
292
+ self.assertEqual(filenames, [app_path])
293
+
195
294
  def test_extract_project_frames_empty_without_traceback(self):
196
295
  self.assertEqual(extract_project_frames(ValueError("no traceback")), [])
197
296
 
@@ -229,6 +328,50 @@ class RetrieverTopKTest(SimpleTestCase):
229
328
  self.assertEqual(retrieve_chunks(ValueError("boom")), [])
230
329
 
231
330
 
331
+ class RetrieverQuerySanitizationTest(SimpleTestCase):
332
+
333
+ @requires_sqlite_vec
334
+ def test_query_text_is_sanitized_before_embedding(self):
335
+ with tempfile.TemporaryDirectory() as tmp:
336
+ module_path = os.path.join(tmp, "leaky.py")
337
+ with open(module_path, "w") as f:
338
+ f.write(
339
+ "def fail():\n"
340
+ " password = 'hunter2-source-secret'; raise ValueError(\n"
341
+ " 'token=msg-secret-value')\n"
342
+ )
343
+
344
+ index_path = os.path.join(tmp, "index.db")
345
+ with VectorStore(index_path) as store:
346
+ store.create(3)
347
+ store.add([("leaky.py", 1, 3, "chunk", [1.0, 0.0, 0.0])])
348
+
349
+ namespace = {}
350
+ exec(compile(open(module_path).read(), module_path, "exec"), namespace)
351
+ try:
352
+ namespace["fail"]()
353
+ except ValueError as exc:
354
+ captured = exc
355
+
356
+ embed_client = _mock_embed_client([1.0, 0.0, 0.0])
357
+ with override_settings(
358
+ EXPLAIN_ERRORS_RAG_ENABLED=True,
359
+ EXPLAIN_ERRORS_RAG_INDEX_PATH=index_path,
360
+ EXPLAIN_ERRORS_RAG_INCLUDE=[tmp],
361
+ ):
362
+ with patch(
363
+ "explain_errors.rag.retriever.get_openai_client",
364
+ return_value=embed_client,
365
+ ):
366
+ retrieve_chunks(captured)
367
+
368
+ embed_client.embeddings.create.assert_called_once()
369
+ (query_text,) = embed_client.embeddings.create.call_args.kwargs["input"]
370
+ self.assertNotIn("msg-secret-value", query_text)
371
+ self.assertNotIn("hunter2-source-secret", query_text)
372
+ self.assertIn("[REDACTED]", query_text)
373
+
374
+
232
375
  @override_settings(DEBUG=True, OPENAI_API_KEY="test-key")
233
376
  class MiddlewareRagIntegrationTest(SimpleTestCase):
234
377