askme-rag 0.1.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.
- askme_rag-0.1.1/PKG-INFO +245 -0
- askme_rag-0.1.1/README.md +231 -0
- askme_rag-0.1.1/pyproject.toml +30 -0
- askme_rag-0.1.1/setup.cfg +4 -0
- askme_rag-0.1.1/src/askme/__init__.py +0 -0
- askme_rag-0.1.1/src/askme/config.py +73 -0
- askme_rag-0.1.1/src/askme/file_processors.py +40 -0
- askme_rag-0.1.1/src/askme/list_files.py +49 -0
- askme_rag-0.1.1/src/askme/llm.py +104 -0
- askme_rag-0.1.1/src/askme/main.py +617 -0
- askme_rag-0.1.1/src/askme/models.py +16 -0
- askme_rag-0.1.1/src/askme/scanner.py +90 -0
- askme_rag-0.1.1/src/askme/sparse.py +107 -0
- askme_rag-0.1.1/src/askme/ui.py +168 -0
- askme_rag-0.1.1/src/askme/utils.py +69 -0
- askme_rag-0.1.1/src/askme/vector_db.py +433 -0
- askme_rag-0.1.1/src/askme_rag.egg-info/PKG-INFO +245 -0
- askme_rag-0.1.1/src/askme_rag.egg-info/SOURCES.txt +30 -0
- askme_rag-0.1.1/src/askme_rag.egg-info/dependency_links.txt +1 -0
- askme_rag-0.1.1/src/askme_rag.egg-info/entry_points.txt +2 -0
- askme_rag-0.1.1/src/askme_rag.egg-info/requires.txt +7 -0
- askme_rag-0.1.1/src/askme_rag.egg-info/top_level.txt +1 -0
- askme_rag-0.1.1/tests/test_config_defaults.py +81 -0
- askme_rag-0.1.1/tests/test_connection_checks.py +71 -0
- askme_rag-0.1.1/tests/test_file_processors.py +49 -0
- askme_rag-0.1.1/tests/test_history_context.py +122 -0
- askme_rag-0.1.1/tests/test_list_files.py +79 -0
- askme_rag-0.1.1/tests/test_main_module.py +388 -0
- askme_rag-0.1.1/tests/test_scanner_integration.py +129 -0
- askme_rag-0.1.1/tests/test_sparse_bm25.py +48 -0
- askme_rag-0.1.1/tests/test_utils_extra.py +65 -0
- askme_rag-0.1.1/tests/test_vector_db_extra.py +371 -0
askme_rag-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: askme-rag
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CLI RAG tool for local codebases
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: qdrant-client
|
|
8
|
+
Requires-Dist: openai
|
|
9
|
+
Requires-Dist: rich
|
|
10
|
+
Requires-Dist: python-dotenv
|
|
11
|
+
Requires-Dist: prompt-toolkit
|
|
12
|
+
Requires-Dist: python-docx
|
|
13
|
+
Requires-Dist: pypdf
|
|
14
|
+
|
|
15
|
+
# askme - Codebase RAG CLI
|
|
16
|
+
|
|
17
|
+
A Command Line Interface (CLI) tool for local Retrieval-Augmented Generation (RAG) over your codebase. It scans, indexes
|
|
18
|
+
and lets you chat with your project using local LLMs (via an OpenAI-compatible API) and a Qdrant vector database,
|
|
19
|
+
returning context-aware answers with source citations.
|
|
20
|
+
|
|
21
|
+
The Python package is named `askme` and exposes a single `askme` console script.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
### Key Features
|
|
26
|
+
|
|
27
|
+
- Local-first architecture
|
|
28
|
+
- Runs against any OpenAI-compatible LLM/embedding endpoint (e.g. Ollama) and a Qdrant instance you control.
|
|
29
|
+
- Code never has to leave your machine if you self-host the services.
|
|
30
|
+
- Hybrid retrieval (dense + sparse)
|
|
31
|
+
- Dense vectors are produced by the configured embedding model.
|
|
32
|
+
- Sparse vectors are computed locally with a built-in BM25 encoder (`src/askme/sparse.py`) for keyword/identifier
|
|
33
|
+
recall.
|
|
34
|
+
- Smart incremental indexing
|
|
35
|
+
- MD5 hashing of files detects changes, so only new or modified files are re-embedded.
|
|
36
|
+
- `/reindex` forces a full rebuild on demand.
|
|
37
|
+
- Source citations
|
|
38
|
+
- Answers include inline `[File: ...]` citations and a citation list, backed by chunk metadata stored in Qdrant.
|
|
39
|
+
- Conversation history
|
|
40
|
+
- Sessions are auto-saved as JSON in `.cfg/history/` and can be listed, loaded or deleted via `/history`.
|
|
41
|
+
- Rich interactive prompt
|
|
42
|
+
- Powered by `prompt-toolkit` with multiline and paste modes.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Technology Stack
|
|
47
|
+
|
|
48
|
+
- Language: Python 3.11+
|
|
49
|
+
- Vector Database: Qdrant (1.x; remote, via Docker, or local file-based)
|
|
50
|
+
- LLM / Embeddings: any OpenAI-compatible API (tested with Ollama)
|
|
51
|
+
- Key libraries:
|
|
52
|
+
- `qdrant-client` - Qdrant access (dense + sparse vectors).
|
|
53
|
+
- `openai` - client for the OpenAI-compatible LLM and embedding APIs.
|
|
54
|
+
- `rich` - terminal UI, panels, markdown rendering.
|
|
55
|
+
- `prompt-toolkit` - interactive prompt with multiline support.
|
|
56
|
+
- `python-dotenv` - environment variable loading.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Project Layout
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
src/askme/
|
|
64
|
+
config.py # ConfigManager - .cfg/settings.json and chat history
|
|
65
|
+
scanner.py # FileScanner - traversal, filtering, MD5 hashing
|
|
66
|
+
vector_db.py # VectorDBConnector - chunking, embeddings, Qdrant I/O
|
|
67
|
+
sparse.py # BM25SparseEncoder - local sparse vector generation
|
|
68
|
+
llm.py # LLMInterface - prompt building and chat completion
|
|
69
|
+
list_files.py # /index command helpers
|
|
70
|
+
ui.py # rich-based UI helpers
|
|
71
|
+
utils.py # shared utilities
|
|
72
|
+
models.py # data models
|
|
73
|
+
main.py # entry point and interactive loop
|
|
74
|
+
tests/ # pytest test suite
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Data Flow
|
|
78
|
+
|
|
79
|
+
`Codebase` -> `FileScanner (hash + filter)` -> `VectorDBConnector (chunk + dense embed + BM25 sparse)` ->
|
|
80
|
+
`Qdrant (hybrid index)` -> `LLMInterface (query + retrieved context)` -> `User`
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### Setup
|
|
85
|
+
|
|
86
|
+
#### Prerequisites
|
|
87
|
+
|
|
88
|
+
- Python 3.11+
|
|
89
|
+
- A Qdrant vector database, in one of two modes:
|
|
90
|
+
- `server` - a running Qdrant instance (local Docker or remote).
|
|
91
|
+
- `local` - a file-based store on disk, no server or Docker required (quick start).
|
|
92
|
+
- An OpenAI-compatible LLM and embeddings endpoint (e.g. Ollama).
|
|
93
|
+
|
|
94
|
+
#### 1. Start Qdrant (optional)
|
|
95
|
+
|
|
96
|
+
In `local` mode you can skip this step entirely - the index is stored on disk under
|
|
97
|
+
`qdrant_local_path` (default `./data/vector_store`) and persists between runs.
|
|
98
|
+
|
|
99
|
+
For `server` mode, run Qdrant locally via Docker:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
docker run -p 6333:6333 -p 6334:6334 \
|
|
103
|
+
-v $(pwd)/qdrant_storage:/qdrant/storage:z \
|
|
104
|
+
qdrant/qdrant
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
If you start in `server` mode but the server is unreachable, `askme` offers to fall back to `local` file-based mode
|
|
108
|
+
(default answer: yes) and remembers the choice in
|
|
109
|
+
`.cfg/settings.json`.
|
|
110
|
+
|
|
111
|
+
#### 2. Prepare models (example with Ollama)
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ollama pull llama3
|
|
115
|
+
ollama pull embeddinggemma:300m
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### 3. Install
|
|
119
|
+
|
|
120
|
+
Using `uv` (recommended for development):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv sync
|
|
124
|
+
uv run askme
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or with pip from the project root:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pip install .
|
|
131
|
+
askme
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Or directly from GitHub:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pip install git+https://github.com/varsey/codebase-rag.git
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Configuration
|
|
143
|
+
|
|
144
|
+
On the first run inside a project directory, `askme` prompts for configuration and stores it in `./.cfg/settings.json`.
|
|
145
|
+
Conversation histories are stored next to it under `./.cfg/history/`.
|
|
146
|
+
|
|
147
|
+
| Option | Default | Description |
|
|
148
|
+
|:--------------------|:--------------------------------------|:---------------------------------------------------------------------|
|
|
149
|
+
| `qdrant_mode` | `server` | Connection mode: `server` (remote/Docker) or `local` (file-based). |
|
|
150
|
+
| `qdrant_local_path` | `./data/vector_store` | On-disk path for the local file-based store (used in `local` mode). |
|
|
151
|
+
| `qdrant_host` | `localhost` | Hostname of the Qdrant service (used in `server` mode). |
|
|
152
|
+
| `qdrant_port` | `6333` | Port of the Qdrant service (used in `server` mode). |
|
|
153
|
+
| `llm_api_base` | `http://localhost:11434/v1` | Base URL of the OpenAI-compatible LLM API. |
|
|
154
|
+
| `llm_api_base_cert` | `` | Optional path to custom cert/CA bundle for LLM API TLS verification. |
|
|
155
|
+
| `vdb_api_base` | `http://localhost:11434/v1` | Base URL of the OpenAI-compatible embeddings API. |
|
|
156
|
+
| `api_key` | `sk-...` | API key passed to the OpenAI-compatible client. |
|
|
157
|
+
| `llm_model` | `llama3` | LLM model name. |
|
|
158
|
+
| `embedding_model` | `embeddinggemma:300m` | Embedding model name. |
|
|
159
|
+
| `chunk_size` | `750` | Chunk size in characters. |
|
|
160
|
+
| `chunk_overlap` | `250` | Overlap between chunks in characters. |
|
|
161
|
+
| `buffer_size` | `1048576` | Read buffer size for file scanning. |
|
|
162
|
+
| `top_n` | `10` | Number of chunks retrieved per query. |
|
|
163
|
+
| `collection_name` | auto-generated | Qdrant collection used for this project. |
|
|
164
|
+
| `file_extensions` | `.py, .md, .js, .ts, .go, .java, ...` | File types to index. |
|
|
165
|
+
| `excluded_dirs` | `.git, .venv, node_modules, ...` | Directories skipped during scanning. |
|
|
166
|
+
|
|
167
|
+
A real example lives in `.cfg/settings.json`.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### Usage
|
|
172
|
+
|
|
173
|
+
Inside the codebase you want to query:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
askme
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
On first launch the tool guides you through configuration, scans the project and builds the Qdrant collection.
|
|
180
|
+
Subsequent runs reuse the existing index and only re-embed changed files.
|
|
181
|
+
|
|
182
|
+
#### Interactive commands
|
|
183
|
+
|
|
184
|
+
- `/new` - start a fresh conversation (resets the context window).
|
|
185
|
+
- `/history` - list, load or delete saved sessions in `.cfg/history/`.
|
|
186
|
+
- `/reindex` - clear the collection and re-embed the codebase from scratch.
|
|
187
|
+
- `/index` - show the files currently indexed.
|
|
188
|
+
- `/multiline` or `/m` - toggle persistent multiline input (submit with Alt+Enter).
|
|
189
|
+
- `/paste` - one-shot multiline input for a single query.
|
|
190
|
+
- `/exit` or `/quit` - end the session.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### Storage Schema
|
|
195
|
+
|
|
196
|
+
Each point in the Qdrant collection holds a dense vector, a BM25 sparse vector and the following payload:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"path": "string (relative path to file)",
|
|
201
|
+
"content": "string (the actual code chunk)",
|
|
202
|
+
"hash": "string (MD5 hash of the original file)",
|
|
203
|
+
"chunk_index": "int",
|
|
204
|
+
"total_chunks": "int"
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### Development
|
|
211
|
+
|
|
212
|
+
- Install dev dependencies and run tests with `uv`:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
uv sync
|
|
216
|
+
uv run pytest
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- The test suite covers config defaults, connection checks, scanner behaviour, sparse BM25 encoding, history/context
|
|
220
|
+
handling and the main module wiring.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
### Known Limitations
|
|
225
|
+
|
|
226
|
+
- Very large files can be memory-heavy during scanning and embedding.
|
|
227
|
+
- Answer quality is bounded by the local LLM's context window.
|
|
228
|
+
- Only text-based source files are supported; binaries are skipped.
|
|
229
|
+
- Hybrid search quality depends on the corpus the BM25 encoder was fit on (the current project).
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Contributing
|
|
234
|
+
|
|
235
|
+
1. Fork the repository.
|
|
236
|
+
2. Create a feature branch (`git checkout -b feature/your-change`).
|
|
237
|
+
3. Keep changes focused and follow the existing code style.
|
|
238
|
+
4. Add or update tests under `tests/` and make sure `uv run pytest` passes.
|
|
239
|
+
5. Open a Pull Request with a clear description.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### License
|
|
244
|
+
|
|
245
|
+
MIT License - see the `LICENSE` file for details (or standard MIT terms if the file is missing).
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# askme - Codebase RAG CLI
|
|
2
|
+
|
|
3
|
+
A Command Line Interface (CLI) tool for local Retrieval-Augmented Generation (RAG) over your codebase. It scans, indexes
|
|
4
|
+
and lets you chat with your project using local LLMs (via an OpenAI-compatible API) and a Qdrant vector database,
|
|
5
|
+
returning context-aware answers with source citations.
|
|
6
|
+
|
|
7
|
+
The Python package is named `askme` and exposes a single `askme` console script.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
### Key Features
|
|
12
|
+
|
|
13
|
+
- Local-first architecture
|
|
14
|
+
- Runs against any OpenAI-compatible LLM/embedding endpoint (e.g. Ollama) and a Qdrant instance you control.
|
|
15
|
+
- Code never has to leave your machine if you self-host the services.
|
|
16
|
+
- Hybrid retrieval (dense + sparse)
|
|
17
|
+
- Dense vectors are produced by the configured embedding model.
|
|
18
|
+
- Sparse vectors are computed locally with a built-in BM25 encoder (`src/askme/sparse.py`) for keyword/identifier
|
|
19
|
+
recall.
|
|
20
|
+
- Smart incremental indexing
|
|
21
|
+
- MD5 hashing of files detects changes, so only new or modified files are re-embedded.
|
|
22
|
+
- `/reindex` forces a full rebuild on demand.
|
|
23
|
+
- Source citations
|
|
24
|
+
- Answers include inline `[File: ...]` citations and a citation list, backed by chunk metadata stored in Qdrant.
|
|
25
|
+
- Conversation history
|
|
26
|
+
- Sessions are auto-saved as JSON in `.cfg/history/` and can be listed, loaded or deleted via `/history`.
|
|
27
|
+
- Rich interactive prompt
|
|
28
|
+
- Powered by `prompt-toolkit` with multiline and paste modes.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### Technology Stack
|
|
33
|
+
|
|
34
|
+
- Language: Python 3.11+
|
|
35
|
+
- Vector Database: Qdrant (1.x; remote, via Docker, or local file-based)
|
|
36
|
+
- LLM / Embeddings: any OpenAI-compatible API (tested with Ollama)
|
|
37
|
+
- Key libraries:
|
|
38
|
+
- `qdrant-client` - Qdrant access (dense + sparse vectors).
|
|
39
|
+
- `openai` - client for the OpenAI-compatible LLM and embedding APIs.
|
|
40
|
+
- `rich` - terminal UI, panels, markdown rendering.
|
|
41
|
+
- `prompt-toolkit` - interactive prompt with multiline support.
|
|
42
|
+
- `python-dotenv` - environment variable loading.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Project Layout
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
src/askme/
|
|
50
|
+
config.py # ConfigManager - .cfg/settings.json and chat history
|
|
51
|
+
scanner.py # FileScanner - traversal, filtering, MD5 hashing
|
|
52
|
+
vector_db.py # VectorDBConnector - chunking, embeddings, Qdrant I/O
|
|
53
|
+
sparse.py # BM25SparseEncoder - local sparse vector generation
|
|
54
|
+
llm.py # LLMInterface - prompt building and chat completion
|
|
55
|
+
list_files.py # /index command helpers
|
|
56
|
+
ui.py # rich-based UI helpers
|
|
57
|
+
utils.py # shared utilities
|
|
58
|
+
models.py # data models
|
|
59
|
+
main.py # entry point and interactive loop
|
|
60
|
+
tests/ # pytest test suite
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Data Flow
|
|
64
|
+
|
|
65
|
+
`Codebase` -> `FileScanner (hash + filter)` -> `VectorDBConnector (chunk + dense embed + BM25 sparse)` ->
|
|
66
|
+
`Qdrant (hybrid index)` -> `LLMInterface (query + retrieved context)` -> `User`
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### Setup
|
|
71
|
+
|
|
72
|
+
#### Prerequisites
|
|
73
|
+
|
|
74
|
+
- Python 3.11+
|
|
75
|
+
- A Qdrant vector database, in one of two modes:
|
|
76
|
+
- `server` - a running Qdrant instance (local Docker or remote).
|
|
77
|
+
- `local` - a file-based store on disk, no server or Docker required (quick start).
|
|
78
|
+
- An OpenAI-compatible LLM and embeddings endpoint (e.g. Ollama).
|
|
79
|
+
|
|
80
|
+
#### 1. Start Qdrant (optional)
|
|
81
|
+
|
|
82
|
+
In `local` mode you can skip this step entirely - the index is stored on disk under
|
|
83
|
+
`qdrant_local_path` (default `./data/vector_store`) and persists between runs.
|
|
84
|
+
|
|
85
|
+
For `server` mode, run Qdrant locally via Docker:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
docker run -p 6333:6333 -p 6334:6334 \
|
|
89
|
+
-v $(pwd)/qdrant_storage:/qdrant/storage:z \
|
|
90
|
+
qdrant/qdrant
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If you start in `server` mode but the server is unreachable, `askme` offers to fall back to `local` file-based mode
|
|
94
|
+
(default answer: yes) and remembers the choice in
|
|
95
|
+
`.cfg/settings.json`.
|
|
96
|
+
|
|
97
|
+
#### 2. Prepare models (example with Ollama)
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ollama pull llama3
|
|
101
|
+
ollama pull embeddinggemma:300m
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### 3. Install
|
|
105
|
+
|
|
106
|
+
Using `uv` (recommended for development):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv sync
|
|
110
|
+
uv run askme
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or with pip from the project root:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pip install .
|
|
117
|
+
askme
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Or directly from GitHub:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install git+https://github.com/varsey/codebase-rag.git
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Configuration
|
|
129
|
+
|
|
130
|
+
On the first run inside a project directory, `askme` prompts for configuration and stores it in `./.cfg/settings.json`.
|
|
131
|
+
Conversation histories are stored next to it under `./.cfg/history/`.
|
|
132
|
+
|
|
133
|
+
| Option | Default | Description |
|
|
134
|
+
|:--------------------|:--------------------------------------|:---------------------------------------------------------------------|
|
|
135
|
+
| `qdrant_mode` | `server` | Connection mode: `server` (remote/Docker) or `local` (file-based). |
|
|
136
|
+
| `qdrant_local_path` | `./data/vector_store` | On-disk path for the local file-based store (used in `local` mode). |
|
|
137
|
+
| `qdrant_host` | `localhost` | Hostname of the Qdrant service (used in `server` mode). |
|
|
138
|
+
| `qdrant_port` | `6333` | Port of the Qdrant service (used in `server` mode). |
|
|
139
|
+
| `llm_api_base` | `http://localhost:11434/v1` | Base URL of the OpenAI-compatible LLM API. |
|
|
140
|
+
| `llm_api_base_cert` | `` | Optional path to custom cert/CA bundle for LLM API TLS verification. |
|
|
141
|
+
| `vdb_api_base` | `http://localhost:11434/v1` | Base URL of the OpenAI-compatible embeddings API. |
|
|
142
|
+
| `api_key` | `sk-...` | API key passed to the OpenAI-compatible client. |
|
|
143
|
+
| `llm_model` | `llama3` | LLM model name. |
|
|
144
|
+
| `embedding_model` | `embeddinggemma:300m` | Embedding model name. |
|
|
145
|
+
| `chunk_size` | `750` | Chunk size in characters. |
|
|
146
|
+
| `chunk_overlap` | `250` | Overlap between chunks in characters. |
|
|
147
|
+
| `buffer_size` | `1048576` | Read buffer size for file scanning. |
|
|
148
|
+
| `top_n` | `10` | Number of chunks retrieved per query. |
|
|
149
|
+
| `collection_name` | auto-generated | Qdrant collection used for this project. |
|
|
150
|
+
| `file_extensions` | `.py, .md, .js, .ts, .go, .java, ...` | File types to index. |
|
|
151
|
+
| `excluded_dirs` | `.git, .venv, node_modules, ...` | Directories skipped during scanning. |
|
|
152
|
+
|
|
153
|
+
A real example lives in `.cfg/settings.json`.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### Usage
|
|
158
|
+
|
|
159
|
+
Inside the codebase you want to query:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
askme
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
On first launch the tool guides you through configuration, scans the project and builds the Qdrant collection.
|
|
166
|
+
Subsequent runs reuse the existing index and only re-embed changed files.
|
|
167
|
+
|
|
168
|
+
#### Interactive commands
|
|
169
|
+
|
|
170
|
+
- `/new` - start a fresh conversation (resets the context window).
|
|
171
|
+
- `/history` - list, load or delete saved sessions in `.cfg/history/`.
|
|
172
|
+
- `/reindex` - clear the collection and re-embed the codebase from scratch.
|
|
173
|
+
- `/index` - show the files currently indexed.
|
|
174
|
+
- `/multiline` or `/m` - toggle persistent multiline input (submit with Alt+Enter).
|
|
175
|
+
- `/paste` - one-shot multiline input for a single query.
|
|
176
|
+
- `/exit` or `/quit` - end the session.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### Storage Schema
|
|
181
|
+
|
|
182
|
+
Each point in the Qdrant collection holds a dense vector, a BM25 sparse vector and the following payload:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"path": "string (relative path to file)",
|
|
187
|
+
"content": "string (the actual code chunk)",
|
|
188
|
+
"hash": "string (MD5 hash of the original file)",
|
|
189
|
+
"chunk_index": "int",
|
|
190
|
+
"total_chunks": "int"
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### Development
|
|
197
|
+
|
|
198
|
+
- Install dev dependencies and run tests with `uv`:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
uv sync
|
|
202
|
+
uv run pytest
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- The test suite covers config defaults, connection checks, scanner behaviour, sparse BM25 encoding, history/context
|
|
206
|
+
handling and the main module wiring.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### Known Limitations
|
|
211
|
+
|
|
212
|
+
- Very large files can be memory-heavy during scanning and embedding.
|
|
213
|
+
- Answer quality is bounded by the local LLM's context window.
|
|
214
|
+
- Only text-based source files are supported; binaries are skipped.
|
|
215
|
+
- Hybrid search quality depends on the corpus the BM25 encoder was fit on (the current project).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### Contributing
|
|
220
|
+
|
|
221
|
+
1. Fork the repository.
|
|
222
|
+
2. Create a feature branch (`git checkout -b feature/your-change`).
|
|
223
|
+
3. Keep changes focused and follow the existing code style.
|
|
224
|
+
4. Add or update tests under `tests/` and make sure `uv run pytest` passes.
|
|
225
|
+
5. Open a Pull Request with a clear description.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### License
|
|
230
|
+
|
|
231
|
+
MIT License - see the `LICENSE` file for details (or standard MIT terms if the file is missing).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "askme-rag"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "CLI RAG tool for local codebases"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"qdrant-client",
|
|
9
|
+
"openai",
|
|
10
|
+
"rich",
|
|
11
|
+
"python-dotenv",
|
|
12
|
+
"prompt-toolkit",
|
|
13
|
+
"python-docx",
|
|
14
|
+
"pypdf",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
askme = "askme.main:main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools>=61.0"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=9.0.2",
|
|
27
|
+
]
|
|
28
|
+
publish = [
|
|
29
|
+
"twine>=6.2.0",
|
|
30
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, Any, List
|
|
4
|
+
|
|
5
|
+
QDRANT_MODE_SERVER = "server"
|
|
6
|
+
QDRANT_MODE_LOCAL = "local"
|
|
7
|
+
DEFAULT_QDRANT_MODE = QDRANT_MODE_SERVER
|
|
8
|
+
DEFAULT_QDRANT_LOCAL_PATH = "./data/vector_store"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigManager:
|
|
12
|
+
def __init__(self, root_dir: Path = None):
|
|
13
|
+
self.root_dir = root_dir or Path.cwd()
|
|
14
|
+
self.cfg_path = self.root_dir / ".cfg"
|
|
15
|
+
self.config_file = self.cfg_path / "settings.json"
|
|
16
|
+
self.history_dir = self.cfg_path / "history"
|
|
17
|
+
self.ensure_dirs()
|
|
18
|
+
|
|
19
|
+
def ensure_dirs(self):
|
|
20
|
+
"""Create the .cfg folder and history subfolder if they don't exist."""
|
|
21
|
+
self.cfg_path.mkdir(exist_ok=True, parents=True)
|
|
22
|
+
self.history_dir.mkdir(exist_ok=True, parents=True)
|
|
23
|
+
|
|
24
|
+
def is_configured(self) -> bool:
|
|
25
|
+
"""Check if the tool is already configured."""
|
|
26
|
+
return self.config_file.exists()
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def get_qdrant_mode(settings: Dict[str, Any]) -> str:
|
|
30
|
+
"""Return the validated Qdrant connection mode ('server' or 'local')."""
|
|
31
|
+
mode = str(settings.get("qdrant_mode", DEFAULT_QDRANT_MODE)).lower()
|
|
32
|
+
return mode if mode in (QDRANT_MODE_SERVER, QDRANT_MODE_LOCAL) else DEFAULT_QDRANT_MODE
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def get_qdrant_local_path(settings: Dict[str, Any]) -> str:
|
|
36
|
+
"""Return the on-disk path used for the local file-based Qdrant store."""
|
|
37
|
+
return settings.get("qdrant_local_path", DEFAULT_QDRANT_LOCAL_PATH)
|
|
38
|
+
|
|
39
|
+
def save_config(self, settings: Dict[str, Any]):
|
|
40
|
+
"""Save user configuration to settings.json."""
|
|
41
|
+
with open(self.config_file, "w") as f:
|
|
42
|
+
json.dump(settings, f, indent=4)
|
|
43
|
+
|
|
44
|
+
def load_config(self) -> Dict[str, Any]:
|
|
45
|
+
"""Load configuration from settings.json."""
|
|
46
|
+
if not self.is_configured():
|
|
47
|
+
return {}
|
|
48
|
+
with open(self.config_file, "r") as f:
|
|
49
|
+
return json.load(f)
|
|
50
|
+
|
|
51
|
+
def save_chat_history(self, chat_id: str, history_data: List[Dict[str, str]]):
|
|
52
|
+
"""Save a conversation history to a JSON file in .cfg/history."""
|
|
53
|
+
history_file = self.history_dir / f"{chat_id}.json"
|
|
54
|
+
with open(history_file, "w") as f:
|
|
55
|
+
json.dump(history_data, f, indent=4)
|
|
56
|
+
|
|
57
|
+
def load_chat_history(self, chat_id: str) -> List[Dict[str, str]]:
|
|
58
|
+
"""Load a conversation history from a JSON file in .cfg/history."""
|
|
59
|
+
history_file = self.history_dir / f"{chat_id}.json"
|
|
60
|
+
if not history_file.exists():
|
|
61
|
+
return []
|
|
62
|
+
with open(history_file, "r") as f:
|
|
63
|
+
return json.load(f)
|
|
64
|
+
|
|
65
|
+
def list_histories(self):
|
|
66
|
+
"""List all conversation histories."""
|
|
67
|
+
return list(self.history_dir.glob("*.json"))
|
|
68
|
+
|
|
69
|
+
def delete_history(self, chat_id: str):
|
|
70
|
+
"""Delete a conversation history file."""
|
|
71
|
+
history_file = self.history_dir / f"{chat_id}.json"
|
|
72
|
+
if history_file.exists():
|
|
73
|
+
history_file.unlink()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import docx
|
|
5
|
+
from pypdf import PdfReader
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileProcessor(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def extract_text(self, file_path: Path) -> str:
|
|
11
|
+
"""Extract text content from the file."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def get_supported_extensions(self) -> set[str]:
|
|
16
|
+
"""Return a set of supported file extensions."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DocxProcessor(FileProcessor):
|
|
21
|
+
def extract_text(self, file_path: Path) -> str:
|
|
22
|
+
"""Extract text from a .docx file."""
|
|
23
|
+
doc = docx.Document(file_path)
|
|
24
|
+
return "\n".join([paragraph.text for paragraph in doc.paragraphs])
|
|
25
|
+
|
|
26
|
+
def get_supported_extensions(self) -> set[str]:
|
|
27
|
+
return {".docx"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PdfProcessor(FileProcessor):
|
|
31
|
+
def extract_text(self, file_path: Path) -> str:
|
|
32
|
+
"""Extract text from a .pdf file."""
|
|
33
|
+
reader = PdfReader(file_path)
|
|
34
|
+
text_parts = []
|
|
35
|
+
for page in reader.pages:
|
|
36
|
+
text_parts.append(page.extract_text() or "")
|
|
37
|
+
return "\n".join(text_parts)
|
|
38
|
+
|
|
39
|
+
def get_supported_extensions(self) -> set[str]:
|
|
40
|
+
return {".pdf"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from askme.config import ConfigManager
|
|
5
|
+
from askme.vector_db import VectorDBConnector
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def list_indexed_files():
|
|
9
|
+
"""
|
|
10
|
+
Retrieves and displays the list of currently indexed files in the vector database.
|
|
11
|
+
"""
|
|
12
|
+
# Load the configuration from settings.json
|
|
13
|
+
config_path = 'settings.json'
|
|
14
|
+
if not os.path.exists(config_path):
|
|
15
|
+
print(f"Error: {config_path} not found.")
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
with open(config_path, 'r') as config_file:
|
|
19
|
+
settings = json.load(config_file)
|
|
20
|
+
|
|
21
|
+
# Initialize the VectorDBConnector with the settings
|
|
22
|
+
# We use .get() for optional parameters to handle cases where they might be missing
|
|
23
|
+
vdb = VectorDBConnector(
|
|
24
|
+
host=settings.get("qdrant_host", "localhost"),
|
|
25
|
+
port=settings.get("qdrant_port", 6333),
|
|
26
|
+
collection_name=settings.get("collection_name", "codebase"),
|
|
27
|
+
vdb_api_base=settings.get("vdb_api_base", "http://localhost:11434/v1"),
|
|
28
|
+
model_name=settings.get("embedding_model", "embeddinggemma:300m"),
|
|
29
|
+
chunk_size=settings.get("chunk_size", 1000),
|
|
30
|
+
chunk_overlap=settings.get("chunk_overlap", 100),
|
|
31
|
+
mode=ConfigManager.get_qdrant_mode(settings),
|
|
32
|
+
local_path=ConfigManager.get_qdrant_local_path(settings)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Fetch the list of indexed files
|
|
36
|
+
# get_indexed_files returns a dict {path: hash}
|
|
37
|
+
indexed_files = vdb.get_indexed_files()
|
|
38
|
+
|
|
39
|
+
# Display the list of indexed files
|
|
40
|
+
if not indexed_files:
|
|
41
|
+
print("No files currently indexed.")
|
|
42
|
+
else:
|
|
43
|
+
print("Currently Indexed Files:")
|
|
44
|
+
# Sort paths for consistent output
|
|
45
|
+
for file_path in sorted(indexed_files.keys()):
|
|
46
|
+
print(f"- {file_path}")
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
list_indexed_files()
|