geekydraco commited on
Commit
3581a58
·
verified ·
1 Parent(s): 9b8b84a

Upload 54 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. README.md +442 -6
  3. configs/config.yaml +105 -0
  4. data/indices/ast_index.pkl +3 -0
  5. data/indices/semantic_index.faiss +3 -0
  6. data/indices/semantic_index.metadata.pkl +3 -0
  7. models/checkpoints/lora_finetuned/README.md +207 -0
  8. models/checkpoints/lora_finetuned/adapter_config.json +50 -0
  9. models/checkpoints/lora_finetuned/adapter_model.safetensors +3 -0
  10. models/checkpoints/lora_finetuned/added_tokens.json +24 -0
  11. models/checkpoints/lora_finetuned/merges.txt +0 -0
  12. models/checkpoints/lora_finetuned/special_tokens_map.json +31 -0
  13. models/checkpoints/lora_finetuned/tokenizer.json +0 -0
  14. models/checkpoints/lora_finetuned/tokenizer_config.json +207 -0
  15. models/checkpoints/lora_finetuned/vocab.json +0 -0
  16. requirements.txt +10 -0
  17. src/__pycache__/config.cpython-312.pyc +0 -0
  18. src/config.py +163 -0
  19. src/data/data_loader.py +95 -0
  20. src/evaluation/__init__.py +11 -0
  21. src/evaluation/comparator.py +74 -0
  22. src/evaluation/metrics.py +258 -0
  23. src/evaluation/rag_evaluation.py +132 -0
  24. src/evaluation/visualization.py +76 -0
  25. src/generators/__init__.py +11 -0
  26. src/generators/__pycache__/__init__.cpython-312.pyc +0 -0
  27. src/generators/__pycache__/commit_generator.cpython-312.pyc +0 -0
  28. src/generators/__pycache__/doc_generator.cpython-312.pyc +0 -0
  29. src/generators/__pycache__/program_generator.cpython-312.pyc +0 -0
  30. src/generators/__pycache__/text_to_mongo_generator.cpython-312.pyc +0 -0
  31. src/generators/commit_generator.py +26 -0
  32. src/generators/doc_generator.py +47 -0
  33. src/generators/llm_baseline.py +85 -0
  34. src/generators/program_generator.py +70 -0
  35. src/generators/text_to_mongo_generator.py +125 -0
  36. src/models/__pycache__/load_model.cpython-312.pyc +0 -0
  37. src/models/load_model.py +263 -0
  38. src/plots/baseline_vs_finetuned_latency.png +0 -0
  39. src/plots/baseline_vs_finetuned_text_to_mongo.png +0 -0
  40. src/plots/text_to_mongo_baseline_metrics.png +0 -0
  41. src/plots/text_to_mongo_finetuned_metrics.png +0 -0
  42. src/rag/__pycache__/rag_pipeline.cpython-312.pyc +0 -0
  43. src/rag/rag_pipeline.py +166 -0
  44. src/vectorDB/__init__.py +13 -0
  45. src/vectorDB/__pycache__/__init__.cpython-312.pyc +0 -0
  46. src/vectorDB/__pycache__/ast_index.cpython-312.pyc +0 -0
  47. src/vectorDB/__pycache__/embedder.cpython-312.pyc +0 -0
  48. src/vectorDB/__pycache__/hybrid_retriever.cpython-312.pyc +0 -0
  49. src/vectorDB/__pycache__/index_manager.cpython-312.pyc +0 -0
  50. src/vectorDB/__pycache__/semantic_index.cpython-312.pyc +0 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ data/indices/semantic_index.faiss filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,10 +1,446 @@
1
  ---
2
- title: CodeGen Emasters IIITH
3
- emoji: 📉
4
- colorFrom: yellow
5
- colorTo: gray
6
- sdk: static
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Text to MongoDB Generator
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 5.9.1
8
+ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # CodeGen: Hybrid RAG Pipeline
13
+
14
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue)](https://www.python.org/)
15
+ [![PyTorch](https://img.shields.io/badge/PyTorch-2.5.1-red)](https://pytorch.org/)
16
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
+
18
+ A capstone project (eMasters CS: AI & ML, Group 2) that fine-tunes a small open-source code language model — **Qwen2.5-Coder-0.5B-Instruct** — with **LoRA**, builds a **hybrid semantic + AST/structural retrieval (RAG)** layer on top of it, and evaluates the result against the un-adapted base model and an independently invoked LLM baseline, across text-to-SQL, documentation-generation, program-synthesis, and commit-message-generation tasks.
19
+
20
+ ---
21
+
22
+ ## Table of Contents
23
+
24
+ - [Executive Summary](#-executive-summary)
25
+ - [Key Capabilities](#-key-capabilities)
26
+ - [System Architecture](#-system-architecture)
27
+ - [Pipeline Stages](#-pipeline-stages)
28
+ - [Benchmark Results](#-benchmark-results)
29
+ - [Dataset](#-dataset)
30
+ - [Installation](#-installation)
31
+ - [Quick Start](#-quick-start)
32
+ - [Repository Layout](#-repository-layout)
33
+ - [Configuration](#-configuration)
34
+ - [Requirements](#-requirements)
35
+ - [License](#-license)
36
+
37
+ ---
38
+
39
+ ## Executive Summary
40
+
41
+ CodeGen combines **Retrieval-Augmented Generation (RAG)** and **parameter-efficient LoRA fine-tuning** on top of a compact instruction-tuned code model to support three developer-facing capabilities: natural-language-to-SQL translation, automatic source-code documentation generation, and program/commit-message generation. The primary evaluation dataset is the **Spider** cross-domain text-to-SQL benchmark (1,034 dev-split examples), supplemented by small hand-curated example sets for documentation, program-synthesis, and commit-message generation.
42
+
43
+ Retrieval embeddings are produced by the small code LM itself (no second embedding model needs to be downloaded), and are combined with an AST/structural fingerprint index so retrieval can be reranked on structure, not just vector similarity.
44
+
45
+ ---
46
+
47
+ ## Key Capabilities
48
+
49
+ | Capability | Description |
50
+ |---|---|
51
+ | **Multi-Task Generation** | Program synthesis, docstring generation, Text-to-SQL translation, commit-message synthesis |
52
+ | **Code-LM Self-Embeddings** | Retrieval embeddings mean-pooled from the base model's own hidden states — no separate embedding model required (an external `BAAI/bge-small-en-v1.5` path is also available and config-switchable) |
53
+ | **Hybrid Retrieval** | FAISS (`IndexFlatIP`) dense semantic search fused with an AST/structural fingerprint reranker (α = 0.7 dense / 0.3 structural) |
54
+ | **LoRA Fine-Tuning** | PEFT LoRA adapter (r=16, α=32) on the attention projections, with a custom training loop (gradient checkpointing, prompt-token label masking, OOM-safe stepping) |
55
+ | **Small-LM vs. LLM Harness** | Pluggable large-LLM baseline generator supporting local, OpenAI-compatible, and Gemini backends |
56
+ | **Comprehensive Evaluation** | BLEU, ROUGE-1/L, token-F1, CodeBERTScore, SQL Exact Match, SQL Execution Accuracy, RAG gain-over-baseline, top-K sweep |
57
+
58
+ ---
59
+
60
+ ## System Architecture
61
+
62
+ ```
63
+ ┌─────────────────────────────────────────────────────────────────┐
64
+ │ 1. Configuration & Data │
65
+ │ • Load config.yaml (paths, model, LoRA, retrieval, eval) │
66
+ │ • Convert & normalize Spider (dev.json + tables.json + SQLite) │
67
+ │ • Build retrieval corpus (1,034 question/SQL pairs) │
68
+ └──────────────────────────┬──────────────────────────────────────┘
69
+ │
70
+ ┌──────────────────────────▼──────────────────────────────────────┐
71
+ │ 2. Model Loading & LoRA Fine-Tuning │
72
+ │ • Load Qwen2.5-Coder-0.5B-Instruct (tokenizer + base weights) │
73
+ │ • Attach LoRA adapter, train on illustrative task examples │
74
+ │ • Save / reload adapter checkpoint │
75
+ └──────────────────────────┬──────────────────────────────────────┘
76
+ │
77
+ ┌──────────────────────────▼──────────────────────────────────────┐
78
+ │ 3. Generation Tasks │
79
+ │ • Text-to-SQL • Documentation • Program Synthesis │
80
+ │ • Commit Messages, evaluated for base model vs. LoRA model │
81
+ └──────────────────────────┬──────────────────────────────────────┘
82
+ │
83
+ ┌──────────────────────────▼──────────────────────────────────────┐
84
+ │ 4. Hybrid Indexing & RAG │
85
+ │ • CodeLMEmbedder → FAISS semantic index (IndexFlatIP) │
86
+ │ • ASTIndexer → structural fingerprints (Python AST / SQL regex)│
87
+ │ • HybridRetriever → dense + structural blended top-K search │
88
+ │ • RAGPipeline → context injection + generation │
89
+ └──────────────────────────┬──────────────────────────────────────┘
90
+ │
91
+ ┌──────────────────────────▼──────────────────────────────────────┐
92
+ │ 5. Evaluation & Comparison │
93
+ │ • Base vs. LoRA vs. RAG vs. LLM-baseline (ModelComparator) │
94
+ │ • RAG gain-over-baseline & top-K sweep (RAGEvaluator) │
95
+ │ • Charts (matplotlib) + JSON metrics persisted to output/ │
96
+ └─────────────────────────────────────────────────────────────────┘
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Pipeline Stages
102
+
103
+ The pipeline is run through a set of Jupyter notebooks rather than a single CLI entry point:
104
+
105
+ | Notebook | Stage |
106
+ |---|---|
107
+ | `baseline_model.ipynb` | Loads the **un-adapted base model**, runs text-to-SQL and documentation generation, saves metrics to `outputs/generated/baseline_model_results.json` |
108
+ | `finetuned_model.ipynb` | Loads/trains the **LoRA adapter**, re-runs the same two tasks, saves metrics to `outputs/generated/finetuned_model_results.json` |
109
+ | `spider_eval.ipynb` | Spider-specific SQL execution evaluation |
110
+ | `eval.ipynb` | RAG-gain measurement, top-K sweep, small-LM-vs-LLM comparison |
111
+ | `main.ipynb` | Complete End-to-End pipeline |
112
+
113
+ Supporting production code lives in `src/` (see [Repository Layout](#-repository-layout)) and is imported by the notebooks — it is not currently exposed as a standalone CLI script.
114
+
115
+ ### Task-Specific Generators (`src/generators/`)
116
+
117
+ | Task | Module | Benchmark used | Primary Metrics |
118
+ |---|---|---|---|
119
+ | **Text-to-SQL** | `text_to_sql.py`, `sql_generator.py` | Spider (100-sample subset of the 1,034-question dev split) | Exact Match, Execution Accuracy, BLEU, CodeBERTScore |
120
+ | **Documentation Generation** | `doc_gen.py` | 3 hand-authored illustrative examples | BLEU, ROUGE, CodeBERTScore, F1 |
121
+ | **Program Synthesis** | `program_generator.py` | 3 hand-authored illustrative examples | Qualitative + shared metric suite |
122
+ | **Commit Message Generation** | `commit_gen.py` | 3 hand-authored illustrative examples | Qualitative + shared metric suite |
123
+ | **LLM Baseline** | `llm_baseline.py` | Same prompts as program synthesis | Comparative scoring against the small LM |
124
+
125
+ ### Hybrid Indexing & Retrieval (`src/embeddings/`)
126
+
127
+ | Component | Technology | Purpose |
128
+ |---|---|---|
129
+ | `code_lm_embedder.py` | Mean-pooled hidden states of the base model itself | Retrieval embeddings without a second model download |
130
+ | `embedding.py` | `BAAI/bge-small-en-v1.5` (external, config-switchable) | Alternative embedding backend |
131
+ | `indexing.py` | FAISS `IndexFlatIP` | Dense semantic vector store (cosine similarity via normalized inner product) |
132
+ | `ast_indexing.py` | Python `ast` module (Python code) / regex-based structural extractor (SQL) | Structural fingerprints — function/class names, or referenced tables/aggregates/clauses |
133
+ | `hybrid_retriever.py` | Blended reranker | Over-fetches `k × 4` from FAISS, reranks with `α * dense + (1-α) * structural` |
134
+
135
+ ### Evaluation & Persistence (`src/evaluation/`)
136
+
137
+ | Component | Function |
138
+ |---|---|
139
+ | `metrics.py` | BLEU, ROUGE, token-F1, CodeBERTScore, execution accuracy primitives |
140
+ | `evaluation.py` | Task-level evaluation engine |
141
+ | `sql_eval.py` | SQL normalization and Spider-style execution comparison |
142
+ | `comparator.py` | Base vs. LoRA vs. RAG side-by-side comparison |
143
+ | `rag_evaluation.py` | RAG gain-over-baseline measurement and top-K sweep |
144
+ | `visualization.py` | Matplotlib bar-chart generation, saved to `output/plots/` |
145
+
146
+ ---
147
+
148
+ ## Benchmark Results
149
+
150
+ Numbers below are taken directly from `outputs/generated/baseline_model_results.json` and `finetuned_model_results.json` (100-sample Spider text-to-SQL subset, 3-sample documentation-generation set).
151
+
152
+ ### Baseline (un-adapted) vs. LoRA Fine-Tuned
153
+
154
+ | Task | Metric | Base Model | LoRA Fine-Tuned |
155
+ |---|---|:---:|:---:|
156
+ | **Text-to-SQL** | BLEU | 0.2329 | **0.2467** |
157
+ | | CodeBERTScore | 0.9740 | **0.9759** |
158
+ | | F1 | 0.4642 | **0.4879** |
159
+ | | ROUGE-1 | 0.4378 | **0.4602** |
160
+ | | ROUGE-L | 0.4274 | **0.4503** |
161
+ | | Exact Match | 7% | **8%** |
162
+ | | Execution Accuracy | 11% | **14%** |
163
+ | **Documentation Generation** | BLEU | 0.0241 | **0.0528** |
164
+ | | CodeBERTScore | 0.9250 | **0.9447** |
165
+ | | F1 | 0.1181 | **0.2022** |
166
+ | | ROUGE-1 | 0.0831 | **0.1615** |
167
+ | | ROUGE-L | 0.0719 | **0.1494** |
168
+
169
+ LoRA fine-tuning (trained on only 9 illustrative prompt/completion examples — 2.16M trainable params, 0.44% of the model) improves every metric on both tasks. Gains are proportionally larger on documentation generation.
170
+
171
+ ### RAG, Top-K Sweep, and LLM Baseline (5-question SQL slice)
172
+
173
+ - On the small 5-question evaluation slice used for this stage, the **RAG-augmented pipeline underperformed the no-RAG LoRA baseline** on every metric — a genuine, reproducible negative result, not a placeholder. See [Known Limitations](#-known-limitations--future-work) for why.
174
+ - A **top-K sweep** over K ∈ {1, 2, 3, 5, 8} shows the gain is least negative around **K = 2–3** and degrades further at K = 8.
175
+ - The **LLM-baseline comparison** fell back to the same local Qwen2.5-Coder-0.5B model in this run because no external API credential was resolved at runtime — see the security note below and Known Limitations.
176
+ - For reference, the rule-based Spider baseline scores **12.28% exact match / 33.66% execution accuracy** over the full 1,034-question dev set.
177
+
178
+ Plots for all of the above are saved under `output/plots/` (`scores_baseline_text_to_sql_vs_doc_generation.png`, `scores_finetuned_text_to_sql_vs_doc_generation.png`, `scores_model_comparison_checkpoint2.png`, `scores_model_comparison_full.png`, `topk_gain_sweep.png`).
179
+
180
+
181
+ ## Dataset
182
+
183
+ This project uses the **[Spider](https://yale-lily.github.io/spider)** dataset — a large-scale, cross-domain text-to-SQL benchmark — as its primary evaluation dataset.
184
+
185
+ > **Note:** The Spider dataset is **not included in this repository**. GitHub's file/repo upload limits make it impractical to commit the raw dataset (question/SQL pairs, table schemas, and per-database SQLite files), so it must be downloaded separately before running the pipeline.
186
+
187
+ ### Download
188
+
189
+ 1. Download the dataset from Google Drive: **[Spider dataset (Google Drive)](https://drive.google.com/file/d/1403EGqzIDoHMdQF4c9Bkyl7dZLZ5Wt6J/view)**
190
+ - Alternatively, see the [official Spider dataset page](https://yale-lily.github.io/spider) for the source release and licensing terms.
191
+ 2. Unzip the download and place its contents under `data/raw/Spider/` at the repository root, so that `dev.json`, `tables.json`, and the per-database SQLite files sit alongside each other, e.g.:
192
+
193
+ ```
194
+ data/raw/Spider/
195
+ ├── dev.json
196
+ ├── tables.json
197
+ └── database/
198
+ ├── concert_singer/
199
+ │ └── concert_singer.sqlite
200
+ └── ...
201
+ ```
202
+
203
+ 3. Run `src/scripts/convert_spider_to_jsonl.py` to convert the raw files into the normalized `data/processed/spider_eval.jsonl` and retrieval corpus used by the notebooks (see [Quick Start](#-quick-start)).
204
+
205
+ `data/raw/`, `data/processed/`, and `data/indices/` are excluded via `.gitignore` for the same reason — they're generated/downloaded locally rather than versioned.
206
+
207
+ ---
208
+
209
+ ## Installation
210
+
211
+ ### Prerequisites
212
+
213
+ ```bash
214
+ Python 3.10 or higher
215
+ A CUDA-capable GPU is optional — the code auto-detects CUDA > MPS (Apple Silicon) > CPU
216
+ Recommended: 16 GB RAM, 8 GB+ VRAM if using GPU
217
+ ```
218
+
219
+ ### Step 1: Clone Repository
220
+
221
+ ```bash
222
+ git clone https://github.com/Rithusravya/Emasters_Group-2_CapstoneProject.git
223
+ cd Emasters_Group-2_CapstoneProject
224
+ ```
225
+
226
+ ### Step 2: Create Virtual Environment
227
+
228
+ ```bash
229
+ python -m venv venv
230
+ source venv/bin/activate # macOS/Linux
231
+ # OR
232
+ venv\Scripts\activate # Windows
233
+ ```
234
+
235
+ ### Step 3: Install Dependencies
236
+
237
+ ```bash
238
+ pip install --upgrade pip
239
+ pip install -r requirements.txt
240
+ ```
241
+
242
+ ### Step 4: Verify Installation
243
+
244
+ ```bash
245
+ python -c "import torch; print(f'CUDA Available: {torch.cuda.is_available()}')"
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Quick Start
251
+
252
+ This project is driven through notebooks, not a CLI. Typical order:
253
+
254
+ 1. **Download & prepare data** — download the Spider dataset as described in [Dataset](#-dataset) and place it under `data/raw/Spider/`. Then run `src/scripts/convert_spider_to_jsonl.py` to convert the raw Spider files into `data/processed/spider_eval.jsonl` and a retrieval corpus; `src/scripts/prepare_processed_data.py` builds the MongoDB-style variant.
255
+ 2. **Baseline evaluation** — open `baseline_model.ipynb` and run all cells to evaluate the un-adapted base model on text-to-SQL and documentation generation.
256
+ 3. **LoRA fine-tuning + evaluation** — open `finetuned_model.ipynb` to train (or reload) the LoRA adapter at `models/checkpoints/lora_finetuned/` and re-run the same evaluations.
257
+ 4. **RAG / top-K / LLM comparison** — open `eval.ipynb` to build the FAISS + AST indices, measure RAG gain, sweep top-K, and compare against the LLM baseline.
258
+ 5. **Spider execution evaluation** — open `spider_eval.ipynb` for SQL-execution-based scoring against the SQLite databases.
259
+
260
+ Programmatic usage of the core pipeline, e.g. for text-to-SQL generation:
261
+
262
+ ```python
263
+ from src.config import load_config
264
+ from src.models.load_model import ModelLoader
265
+ from src.generators.program_generator import GenerationPipeline
266
+ from src.generators.text_to_sql import TextToSQLGenerator
267
+
268
+ config = load_config() # reads configs/config.yaml
269
+ loader = ModelLoader(config)
270
+ models, tokenizer = loader.load_models(lora_path=config.lora.output_dir)
271
+
272
+ pipeline = GenerationPipeline(models["lora"] or models["base"], tokenizer, config.generation)
273
+ sql_gen = TextToSQLGenerator(pipeline)
274
+
275
+ result = sql_gen.generate_queries(
276
+ question="How many singers are there?",
277
+ schema="CREATE TABLE singer (singer_id INT, name TEXT, age INT)",
278
+ )
279
+ print(result["sql_query"])
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Repository Layout
285
+
286
+ ```
287
+ Emasters_Group-2_CapstoneProject/
288
+ ├── configs/
289
+ │ └── config.yaml # Project, model, LoRA, retrieval, eval & output paths
290
+ ├── data/
291
+ │ ├── raw/ # Raw Spider files (dev.json, tables.json, SQLite DBs)
292
+ │ ├── processed/ # Normalized JSONL (spider_eval, retrieval_corpus, etc.)
293
+ │ └── indices/ # Saved FAISS index, AST store, FAISS metadata
294
+ ├── models/
295
+ │ └── checkpoints/lora_finetuned/ # Saved LoRA adapter + tokenizer files
296
+ ├── src/
297
+ │ ├── config.py # YAML → dataclass config loader
298
+ │ ├── data/
299
+ │ │ └── data_loader.py # Generic JSON/JSONL dataset loader (Spider, etc.)
300
+ │ ├── models/
301
+ │ │ └── load_model.py # Base model + LoRA loading, training loop, base-vs-LoRA generation
302
+ │ ├── generators/
303
+ │ │ ├── program_generator.py # Core GenerationPipeline + prompt builder
304
+ │ │ ├── text_to_sql.py # Text-to-SQL generator
305
+ │ │ ├── sql_generator.py # SQL prompt/parsing helpers
306
+ │ │ ├── doc_gen.py # Documentation generator
307
+ │ │ ├── commit_gen.py # Commit-message generator
308
+ │ │ └── llm_baseline.py # Pluggable local/OpenAI/Gemini LLM baseline
309
+ │ ├── embeddings/
310
+ │ │ ├── code_lm_embedder.py # Embeddings from the small code LM's own hidden states
311
+ │ │ ├── embedding.py # External embedding-model backend (BGE)
312
+ │ │ ├── indexing.py # FAISS semantic index manager
313
+ │ │ ├── ast_indexing.py # AST / SQL structural fingerprint indexer
314
+ │ │ └── hybrid_retriever.py # Dense + structural blended retrieval
315
+ │ ├── rag/
316
+ │ │ └── rag_pipeline.py # Context retrieval, prompt building, RAG generation
317
+ │ ├── evaluation/
318
+ │ │ ├── metrics.py # BLEU, ROUGE, F1, CodeBERTScore, execution accuracy
319
+ │ │ ├── evaluation.py # Task-level evaluation engine
320
+ │ │ ├── sql_eval.py # Spider SQL normalization & execution comparison
321
+ │ │ ├── comparator.py # Base vs. LoRA vs. RAG comparison
322
+ │ │ ├── rag_evaluation.py # RAG gain-over-baseline + top-K sweep
323
+ │ │ └── visualization.py # Chart generation
324
+ │ └── scripts/
325
+ │ ├── convert_spider_to_jsonl.py # Raw Spider → normalized JSONL
326
+ │ └── prepare_processed_data.py # Build derived/MongoDB-style datasets
327
+ ├── outputs/generated/ # Per-task generated samples & timestamped run results
328
+ ├── output/plots/ # Saved comparison charts
329
+ ├── backup/ # Earlier snapshot of evaluation/rag/embeddings modules
330
+ ├── baseline_model.ipynb # Base model evaluation
331
+ ├── finetuned_model.ipynb # LoRA fine-tuning + evaluation
332
+ ├── spider_eval.ipynb # Spider execution-based SQL evaluation
333
+ ├── eval.ipynb # RAG gain / top-K sweep / LLM comparison
334
+ ├── main.ipynb # Colab entry point
335
+ ├── requirements.txt
336
+ └── README.md
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Configuration (`configs/config.yaml`)
342
+
343
+ Key fields actually read by `src/config.py`:
344
+
345
+ ```yaml
346
+ model_name: "Qwen/Qwen2.5-Coder-0.5B-Instruct"
347
+ lora_adapter_path: "models/checkpoints/lora_finetuned"
348
+ embedding_model: "BAAI/bge-small-en-v1.5" # used only when embedding.source == "external"
349
+
350
+ embedding:
351
+ source: "code_lm" # "code_lm" (default, self-embeddings) or "external"
352
+
353
+ retrieval:
354
+ hybrid: true
355
+ structural_weight_alpha: 0.7
356
+ structural_language: "sql"
357
+ topk_sweep: [1, 2, 3, 5, 8]
358
+
359
+ llm_baseline:
360
+ backend: "local" # "local" or an external provider ("openai" / "gemini")
361
+ local_model: "Qwen/Qwen2.5-Coder-0.5B-Instruct"
362
+ api_model: "gpt-5.4-mini"
363
+ max_new_tokens: 256
364
+ temperature: 0.2
365
+
366
+ lora:
367
+ r: 16
368
+ alpha: 32
369
+ dropout: 0.05
370
+ target_modules: ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
371
+ batch_size: 4
372
+ learning_rate: 0.0003
373
+ epochs: 5
374
+ output_dir: "models/checkpoints/lora_finetuned"
375
+
376
+ indexing:
377
+ top_k: 3
378
+ vector_dim: 1024 # must match the embedding model's output dimension
379
+
380
+ outputs:
381
+ logs_dir: "output/logs"
382
+ metrics_path: "output/metrics/evaluation_metrics.json"
383
+ plots_dir: "output/plots"
384
+ ```
385
+
386
+ ---
387
+
388
+ ## Requirements
389
+
390
+ | Package | Version | Purpose |
391
+ |---|---|---|
392
+ | `torch` | 2.5.1 | Core deep learning framework |
393
+ | `transformers` | 4.46.3 | Pretrained model + tokenizer loading (Qwen2.5-Coder) |
394
+ | `peft` | 0.13.2 | LoRA fine-tuning |
395
+ | `datasets` | 3.1.0 | Dataset utilities |
396
+ | `faiss-cpu` | 1.9.0 | Dense vector similarity search |
397
+ | `tree-sitter` / `tree-sitter-python` | 0.23.2 / 0.23.6 | (Available for AST-based parsing) |
398
+ | `codebleu` | 0.7.0 | CodeBLEU metric |
399
+ | `codebert-score` | 0.3.13 | CodeBERTScore semantic similarity metric |
400
+ | `evaluate` | 0.4.3 | Hugging Face evaluation metrics |
401
+ | `rouge-score` | 0.1.2 | ROUGE metrics |
402
+ | `nltk` | 3.9.1 | Text preprocessing / BLEU support |
403
+ | `PyYAML` | 6.0.2 | Config parsing |
404
+ | `matplotlib` / `seaborn` | 3.10.0 / 0.13.2 | Plotting |
405
+ | `numpy` | 2.1.3 | Numerical computing |
406
+
407
+ See `requirements.txt` for the full pinned list.
408
+
409
+ ---
410
+
411
+ ## Contributing
412
+
413
+ 1. **Fork** the repository
414
+ 2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
415
+ 3. **Commit** your changes (`git commit -m 'Add amazing feature'`)
416
+ 4. **Push** to the branch (`git push origin feature/amazing-feature`)
417
+ 5. **Open** a Pull Request
418
+
419
+ ---
420
+
421
+ ## License
422
+
423
+ This project is licensed under the **MIT License** — see the `LICENSE` file for details.
424
+
425
+ ---
426
+
427
+ ## Citation
428
+
429
+ ```bibtex
430
+ @software{codegen_capstone_2026,
431
+ title = {CodeGen: Small Code-LM Fine-Tuning and Hybrid RAG Pipeline},
432
+ author = {Emasters Group 2},
433
+ year = {2026},
434
+ url = {https://github.com/Rithusravya/Emasters_Group-2_CapstoneProject}
435
+ }
436
+ ```
437
+
438
+ ---
439
+
440
+ <div align="center">
441
+
442
+ **Built by IIIT-H eMasters Group 2**
443
+
444
+ [⬆ Back to Top](#-codegen-small-code-lm-fine-tuning--hybrid-rag-pipeline)
445
+
446
+ </div>
configs/config.yaml ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # General & System Configurations
3
+ # ============================================================
4
+ project_name: "Emasters_Group-2_CapstoneProject"
5
+ seed: 42
6
+ device: "auto" # Options: 'auto', 'cuda', 'mps', 'cpu'
7
+ dtype: "float16" # Options: 'float16', 'bfloat16', 'float32'
8
+
9
+ # ============================================================
10
+ # Model & Tokenizer Configs
11
+ # ============================================================
12
+ model_name: "Qwen/Qwen2.5-Coder-0.5B-Instruct"
13
+ lora_adapter_path: "models/checkpoints/lora_finetuned"
14
+
15
+ # ============================================================
16
+ # Embedding Model Configs
17
+ # ============================================================
18
+ embedding_model: "BAAI/bge-small-en-v1.5" # used only when embedding.source == "external"
19
+ embedding:
20
+ source: "external" # Options: 'code_lm' or 'external'
21
+ external_model: "BAAI/bge-small-en-v1.5"
22
+ max_length: 512
23
+ batch_size: 32
24
+
25
+ # ============================================================
26
+ # Retrieval Settings (semantic + AST/structural hybrid search)
27
+ # ============================================================
28
+ retrieval:
29
+ hybrid: true
30
+ structural_weight_alpha: 0.7
31
+ structural_language: "mongodb"
32
+ topk_sweep: [1, 2, 3, 5, 8]
33
+
34
+ # ============================================================
35
+ # Large-LLM baseline (for "small code LM vs. LLM" comparison)
36
+ # ============================================================
37
+ llm_baseline:
38
+ backend: "api" # Options: 'local' or 'api'
39
+ local_model: "Qwen/Qwen2.5-Coder-0.5B-Instruct"
40
+ api_model: "gemini-3.6-flash"
41
+ api_key_env: "GEMINI_API_KEY"
42
+ max_new_tokens: 256
43
+ temperature: 0.2
44
+
45
+ # ============================================================
46
+ # Dataset Paths
47
+ # ============================================================
48
+ data_paths:
49
+ raw_dir: "data/raw"
50
+ spider: "spider_real_execution_mongo.json"
51
+
52
+ # ============================================================
53
+ # FAISS Vector & AST Index Paths
54
+ # ============================================================
55
+ indices_paths:
56
+ faiss_index: "data/indices/faiss_semantic.index"
57
+ ast_store: "data/indices/ast_store.pkl"
58
+
59
+ # ============================================================
60
+ # Text Generation Parameters
61
+ # ============================================================
62
+ generation:
63
+ max_length: 512
64
+ temperature: 0.2
65
+ top_k: 50
66
+ top_p: 0.95
67
+ num_return_sequences: 1
68
+
69
+ # ============================================================
70
+ # Evaluation Settings
71
+ # ============================================================
72
+ evaluation:
73
+ device: "cpu" # CodeBERTScore device
74
+ bertscore_model: "microsoft/codebert-base"
75
+ bertscore_local_path: "models/codebert-base"
76
+ timeout_seconds: 3
77
+
78
+ # ============================================================
79
+ # LoRA Training Settings
80
+ # ============================================================
81
+ lora:
82
+ r: 16
83
+ alpha: 32
84
+ dropout: 0.05
85
+ batch_size: 1
86
+ learning_rate: 0.0002
87
+ epochs: 1
88
+ output_dir: "models/checkpoints/lora_finetuned"
89
+ trainable_fraction: 1.0 # Fraction of LoRA parameters to train (0.0 to 1.0)
90
+
91
+ # ============================================================
92
+ # Indexing Settings
93
+ # ============================================================
94
+ indexing:
95
+ top_k: 3
96
+ vector_dim: 1024 # Must match embedding model's output dimension
97
+
98
+ # ============================================================
99
+ # Outputs & Evaluation Paths
100
+ # ============================================================
101
+ outputs:
102
+ logs_dir: "output/logs"
103
+ metrics_path: "output/metrics/evaluation_metrics.json"
104
+ plots_dir: "output/plots"
105
+ generated_dir: "output/generated"
data/indices/ast_index.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c5f4bae7dc5a9a4ef572e9f0f64123668a95382b038132bddcd11473f779db90
3
+ size 361374
data/indices/semantic_index.faiss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03db2b2dd28bcb7e5165ce11e60a976728796f48a7a13b3eaf259519c507ef16
3
+ size 1711149
data/indices/semantic_index.metadata.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bed7ff41bd109624a645eb1e91cf612c3d4811f4eb62ba7bd787c1078659f0a9
3
+ size 329781
models/checkpoints/lora_finetuned/README.md ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: Qwen/Qwen2.5-Coder-0.5B-Instruct
3
+ library_name: peft
4
+ pipeline_tag: text-generation
5
+ tags:
6
+ - base_model:adapter:Qwen/Qwen2.5-Coder-0.5B-Instruct
7
+ - lora
8
+ - transformers
9
+ ---
10
+
11
+ # Model Card for Model ID
12
+
13
+ <!-- Provide a quick summary of what the model is/does. -->
14
+
15
+
16
+
17
+ ## Model Details
18
+
19
+ ### Model Description
20
+
21
+ <!-- Provide a longer summary of what this model is. -->
22
+
23
+
24
+
25
+ - **Developed by:** [More Information Needed]
26
+ - **Funded by [optional]:** [More Information Needed]
27
+ - **Shared by [optional]:** [More Information Needed]
28
+ - **Model type:** [More Information Needed]
29
+ - **Language(s) (NLP):** [More Information Needed]
30
+ - **License:** [More Information Needed]
31
+ - **Finetuned from model [optional]:** [More Information Needed]
32
+
33
+ ### Model Sources [optional]
34
+
35
+ <!-- Provide the basic links for the model. -->
36
+
37
+ - **Repository:** [More Information Needed]
38
+ - **Paper [optional]:** [More Information Needed]
39
+ - **Demo [optional]:** [More Information Needed]
40
+
41
+ ## Uses
42
+
43
+ <!-- Address questions around how the model is intended to be used, including the foreseeable users of the model and those affected by the model. -->
44
+
45
+ ### Direct Use
46
+
47
+ <!-- This section is for the model use without fine-tuning or plugging into a larger ecosystem/app. -->
48
+
49
+ [More Information Needed]
50
+
51
+ ### Downstream Use [optional]
52
+
53
+ <!-- This section is for the model use when fine-tuned for a task, or when plugged into a larger ecosystem/app -->
54
+
55
+ [More Information Needed]
56
+
57
+ ### Out-of-Scope Use
58
+
59
+ <!-- This section addresses misuse, malicious use, and uses that the model will not work well for. -->
60
+
61
+ [More Information Needed]
62
+
63
+ ## Bias, Risks, and Limitations
64
+
65
+ <!-- This section is meant to convey both technical and sociotechnical limitations. -->
66
+
67
+ [More Information Needed]
68
+
69
+ ### Recommendations
70
+
71
+ <!-- This section is meant to convey recommendations with respect to the bias, risk, and technical limitations. -->
72
+
73
+ Users (both direct and downstream) should be made aware of the risks, biases and limitations of the model. More information needed for further recommendations.
74
+
75
+ ## How to Get Started with the Model
76
+
77
+ Use the code below to get started with the model.
78
+
79
+ [More Information Needed]
80
+
81
+ ## Training Details
82
+
83
+ ### Training Data
84
+
85
+ <!-- This should link to a Dataset Card, perhaps with a short stub of information on what the training data is all about as well as documentation related to data pre-processing or additional filtering. -->
86
+
87
+ [More Information Needed]
88
+
89
+ ### Training Procedure
90
+
91
+ <!-- This relates heavily to the Technical Specifications. Content here should link to that section when it is relevant to the training procedure. -->
92
+
93
+ #### Preprocessing [optional]
94
+
95
+ [More Information Needed]
96
+
97
+
98
+ #### Training Hyperparameters
99
+
100
+ - **Training regime:** [More Information Needed] <!--fp32, fp16 mixed precision, bf16 mixed precision, bf16 non-mixed precision, fp16 non-mixed precision, fp8 mixed precision -->
101
+
102
+ #### Speeds, Sizes, Times [optional]
103
+
104
+ <!-- This section provides information about throughput, start/end time, checkpoint size if relevant, etc. -->
105
+
106
+ [More Information Needed]
107
+
108
+ ## Evaluation
109
+
110
+ <!-- This section describes the evaluation protocols and provides the results. -->
111
+
112
+ ### Testing Data, Factors & Metrics
113
+
114
+ #### Testing Data
115
+
116
+ <!-- This should link to a Dataset Card if possible. -->
117
+
118
+ [More Information Needed]
119
+
120
+ #### Factors
121
+
122
+ <!-- These are the things the evaluation is disaggregating by, e.g., subpopulations or domains. -->
123
+
124
+ [More Information Needed]
125
+
126
+ #### Metrics
127
+
128
+ <!-- These are the evaluation metrics being used, ideally with a description of why. -->
129
+
130
+ [More Information Needed]
131
+
132
+ ### Results
133
+
134
+ [More Information Needed]
135
+
136
+ #### Summary
137
+
138
+
139
+
140
+ ## Model Examination [optional]
141
+
142
+ <!-- Relevant interpretability work for the model goes here -->
143
+
144
+ [More Information Needed]
145
+
146
+ ## Environmental Impact
147
+
148
+ <!-- Total emissions (in grams of CO2eq) and additional considerations, such as electricity usage, go here. Edit the suggested text below accordingly -->
149
+
150
+ Carbon emissions can be estimated using the [Machine Learning Impact calculator](https://mlco2.github.io/impact#compute) presented in [Lacoste et al. (2019)](https://arxiv.org/abs/1910.09700).
151
+
152
+ - **Hardware Type:** [More Information Needed]
153
+ - **Hours used:** [More Information Needed]
154
+ - **Cloud Provider:** [More Information Needed]
155
+ - **Compute Region:** [More Information Needed]
156
+ - **Carbon Emitted:** [More Information Needed]
157
+
158
+ ## Technical Specifications [optional]
159
+
160
+ ### Model Architecture and Objective
161
+
162
+ [More Information Needed]
163
+
164
+ ### Compute Infrastructure
165
+
166
+ [More Information Needed]
167
+
168
+ #### Hardware
169
+
170
+ [More Information Needed]
171
+
172
+ #### Software
173
+
174
+ [More Information Needed]
175
+
176
+ ## Citation [optional]
177
+
178
+ <!-- If there is a paper or blog post introducing the model, the APA and Bibtex information for that should go in this section. -->
179
+
180
+ **BibTeX:**
181
+
182
+ [More Information Needed]
183
+
184
+ **APA:**
185
+
186
+ [More Information Needed]
187
+
188
+ ## Glossary [optional]
189
+
190
+ <!-- If relevant, include terms and calculations in this section that can help readers understand the model or model card. -->
191
+
192
+ [More Information Needed]
193
+
194
+ ## More Information [optional]
195
+
196
+ [More Information Needed]
197
+
198
+ ## Model Card Authors [optional]
199
+
200
+ [More Information Needed]
201
+
202
+ ## Model Card Contact
203
+
204
+ [More Information Needed]
205
+ ### Framework versions
206
+
207
+ - PEFT 0.20.0
models/checkpoints/lora_finetuned/adapter_config.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alora_invocation_tokens": null,
3
+ "alpha_pattern": {},
4
+ "arrow_config": null,
5
+ "auto_mapping": null,
6
+ "base_model_name_or_path": "Qwen/Qwen2.5-Coder-0.5B-Instruct",
7
+ "bias": "none",
8
+ "corda_config": null,
9
+ "ensure_weight_tying": false,
10
+ "eva_config": null,
11
+ "exclude_modules": null,
12
+ "fan_in_fan_out": false,
13
+ "inference_mode": true,
14
+ "init_lora_weights": true,
15
+ "layer_replication": null,
16
+ "layers_pattern": null,
17
+ "layers_to_transform": null,
18
+ "loftq_config": {},
19
+ "lora_alpha": 32,
20
+ "lora_bias": false,
21
+ "lora_dropout": 0.05,
22
+ "lora_ga_config": null,
23
+ "megatron_config": null,
24
+ "megatron_core": "megatron.core",
25
+ "modules_to_save": null,
26
+ "monteclora_config": null,
27
+ "peft_type": "LORA",
28
+ "peft_version": "0.20.0",
29
+ "qalora_group_size": 16,
30
+ "r": 16,
31
+ "rank_pattern": {},
32
+ "revision": null,
33
+ "target_modules": [
34
+ "gate_proj",
35
+ "q_proj",
36
+ "up_proj",
37
+ "k_proj",
38
+ "down_proj",
39
+ "v_proj",
40
+ "o_proj"
41
+ ],
42
+ "target_parameters": null,
43
+ "task_type": "CAUSAL_LM",
44
+ "trainable_token_indices": null,
45
+ "use_bdlora": null,
46
+ "use_dora": false,
47
+ "use_qalora": false,
48
+ "use_rslora": false,
49
+ "velora_config": null
50
+ }
models/checkpoints/lora_finetuned/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a72b6b7afbe198b677f5df314bcfe5d5452d142d433fe9b6bc1215431fe297ae
3
+ size 35237104
models/checkpoints/lora_finetuned/added_tokens.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "</tool_call>": 151658,
3
+ "<tool_call>": 151657,
4
+ "<|box_end|>": 151649,
5
+ "<|box_start|>": 151648,
6
+ "<|endoftext|>": 151643,
7
+ "<|file_sep|>": 151664,
8
+ "<|fim_middle|>": 151660,
9
+ "<|fim_pad|>": 151662,
10
+ "<|fim_prefix|>": 151659,
11
+ "<|fim_suffix|>": 151661,
12
+ "<|im_end|>": 151645,
13
+ "<|im_start|>": 151644,
14
+ "<|image_pad|>": 151655,
15
+ "<|object_ref_end|>": 151647,
16
+ "<|object_ref_start|>": 151646,
17
+ "<|quad_end|>": 151651,
18
+ "<|quad_start|>": 151650,
19
+ "<|repo_name|>": 151663,
20
+ "<|video_pad|>": 151656,
21
+ "<|vision_end|>": 151653,
22
+ "<|vision_pad|>": 151654,
23
+ "<|vision_start|>": 151652
24
+ }
models/checkpoints/lora_finetuned/merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
models/checkpoints/lora_finetuned/special_tokens_map.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<|im_start|>",
4
+ "<|im_end|>",
5
+ "<|object_ref_start|>",
6
+ "<|object_ref_end|>",
7
+ "<|box_start|>",
8
+ "<|box_end|>",
9
+ "<|quad_start|>",
10
+ "<|quad_end|>",
11
+ "<|vision_start|>",
12
+ "<|vision_end|>",
13
+ "<|vision_pad|>",
14
+ "<|image_pad|>",
15
+ "<|video_pad|>"
16
+ ],
17
+ "eos_token": {
18
+ "content": "<|im_end|>",
19
+ "lstrip": false,
20
+ "normalized": false,
21
+ "rstrip": false,
22
+ "single_word": false
23
+ },
24
+ "pad_token": {
25
+ "content": "<|endoftext|>",
26
+ "lstrip": false,
27
+ "normalized": false,
28
+ "rstrip": false,
29
+ "single_word": false
30
+ }
31
+ }
models/checkpoints/lora_finetuned/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
models/checkpoints/lora_finetuned/tokenizer_config.json ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "151643": {
6
+ "content": "<|endoftext|>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151644": {
14
+ "content": "<|im_start|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151645": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151646": {
30
+ "content": "<|object_ref_start|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151647": {
38
+ "content": "<|object_ref_end|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151648": {
46
+ "content": "<|box_start|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151649": {
54
+ "content": "<|box_end|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151650": {
62
+ "content": "<|quad_start|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151651": {
70
+ "content": "<|quad_end|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151652": {
78
+ "content": "<|vision_start|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151653": {
86
+ "content": "<|vision_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151654": {
94
+ "content": "<|vision_pad|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151655": {
102
+ "content": "<|image_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151656": {
110
+ "content": "<|video_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151657": {
118
+ "content": "<tool_call>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": false
124
+ },
125
+ "151658": {
126
+ "content": "</tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151659": {
134
+ "content": "<|fim_prefix|>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151660": {
142
+ "content": "<|fim_middle|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151661": {
150
+ "content": "<|fim_suffix|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151662": {
158
+ "content": "<|fim_pad|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151663": {
166
+ "content": "<|repo_name|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151664": {
174
+ "content": "<|file_sep|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ }
181
+ },
182
+ "additional_special_tokens": [
183
+ "<|im_start|>",
184
+ "<|im_end|>",
185
+ "<|object_ref_start|>",
186
+ "<|object_ref_end|>",
187
+ "<|box_start|>",
188
+ "<|box_end|>",
189
+ "<|quad_start|>",
190
+ "<|quad_end|>",
191
+ "<|vision_start|>",
192
+ "<|vision_end|>",
193
+ "<|vision_pad|>",
194
+ "<|image_pad|>",
195
+ "<|video_pad|>"
196
+ ],
197
+ "bos_token": null,
198
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- message.content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n",
199
+ "clean_up_tokenization_spaces": false,
200
+ "eos_token": "<|im_end|>",
201
+ "errors": "replace",
202
+ "model_max_length": 32768,
203
+ "pad_token": "<|endoftext|>",
204
+ "split_special_tokens": false,
205
+ "tokenizer_class": "Qwen2Tokenizer",
206
+ "unk_token": null
207
+ }
models/checkpoints/lora_finetuned/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ spaces
2
+ gradio==5.9.1
3
+ torch==2.5.1
4
+ transformers==4.46.3
5
+ peft==0.20.0
6
+ faiss-cpu==1.9.0.post1
7
+ numpy==2.1.3
8
+ PyYAML==6.0.2
9
+ huggingface-hub==0.26.2
10
+ tqdm==4.67.1
src/__pycache__/config.cpython-312.pyc ADDED
Binary file (10.7 kB). View file
 
src/config.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from dataclasses import dataclass, field, fields
3
+ from pathlib import Path
4
+ from typing import List, Optional, Any, Type, TypeVar
5
+ import yaml
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ def _build(dataclass_type: Type[T], data: dict) -> T:
13
+ valid_keys = {f.name for f in fields(dataclass_type)}
14
+ filtered = {k: v for k, v in data.items() if k in valid_keys}
15
+ return dataclass_type(**filtered)
16
+
17
+
18
+ @dataclass
19
+ class PathConfig:
20
+ data_dir: Path = Path("data/raw")
21
+ checkpoints_dir: Path = Path("models/checkpoints")
22
+ lora_save_path: Path = Path("models/checkpoints/lora_finetuned")
23
+ faiss_index_path: Path = Path("data/indices/faiss_semantic.index")
24
+ ast_store_path: Path = Path("data/indices/ast_store.pkl")
25
+
26
+ @dataclass
27
+ class ModelConfig:
28
+ name_or_path: str = "Qwen/Qwen2.5-Coder-0.5B-Instruct"
29
+ embedding_name: str = "BAAI/bge-small-en-v1.5"
30
+
31
+ @dataclass
32
+ class LoRAConfig:
33
+ r: int = 16
34
+ alpha: int = 32
35
+ target_modules: List[str] = field(
36
+ default_factory=lambda: ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
37
+ )
38
+ dropout: float = 0.05
39
+ batch_size: int = 1
40
+ learning_rate: float = 2e-4
41
+ epochs: int = 1
42
+ output_dir: str = "models/checkpoints/lora_finetuned"
43
+ trainable_fraction: float = 1.0
44
+
45
+ @dataclass
46
+ class EmbeddingConfig:
47
+ source: str = "external" # 'code_lm' or 'external'
48
+ external_model: str = "BAAI/bge-small-en-v1.5"
49
+ max_length: int = 512
50
+ batch_size: int = 32
51
+
52
+ @dataclass
53
+ class RetrievalConfig:
54
+ hybrid: bool = True
55
+ structural_language: str = "sql"
56
+ structural_weight_alpha: float = 0.7
57
+ topk_sweep: List[int] = field(default_factory=lambda: [1, 2, 3, 5, 8])
58
+
59
+ @dataclass
60
+ class LLMBaselineConfig:
61
+ backend: str = "api" # 'local' or 'api'
62
+ local_model: str = "Qwen/Qwen2.5-Coder-0.5B-Instruct"
63
+ api_model: str = "gemini-3.6-flash"
64
+ api_key_env: str = "GEMINI_API_KEY"
65
+ max_new_tokens: int = 256
66
+ temperature: float = 0.2
67
+
68
+ @dataclass
69
+ class GenerationConfig:
70
+ max_length: int = 512
71
+ temperature: float = 0.2
72
+ top_k: int = 50
73
+ top_p: float = 0.95
74
+ num_return_sequences: int = 1
75
+
76
+ @dataclass
77
+ class EvaluationConfig:
78
+ device: str = "cpu"
79
+ bertscore_model: str = "microsoft/codebert-base"
80
+ bertscore_local_path: str = "models/codebert-base"
81
+ timeout_seconds: int = 3
82
+
83
+ def resolve_bertscore_model(self, base_dir: Optional[Path] = None) -> str:
84
+ candidates = []
85
+ local = Path(self.bertscore_local_path)
86
+ if base_dir is not None:
87
+ candidates.append(Path(base_dir) / self.bertscore_local_path)
88
+ candidates.append(local)
89
+ # Fallback: resolve relative to this config.py's own directory (the
90
+ # project root), since that's where `models/codebert-base` is
91
+ # expected to live per the project layout.
92
+ candidates.append(Path(__file__).resolve().parent / self.bertscore_local_path)
93
+
94
+ for candidate in candidates:
95
+ if candidate.is_dir() and (candidate / "config.json").exists():
96
+ return str(candidate.resolve())
97
+
98
+ logger.info(
99
+ f"Local CodeBERT checkpoint not found at '{self.bertscore_local_path}' "
100
+ f"(tried: {[str(c) for c in candidates]}); falling back to "
101
+ f"downloading '{self.bertscore_model}' from the Hugging Face Hub."
102
+ )
103
+ return self.bertscore_model
104
+
105
+ @dataclass
106
+ class OutputConfig:
107
+ logs_dir: str = "output/logs"
108
+ metrics_path: str = "output/metrics/evaluation_metrics.json"
109
+ plots_dir: str = "output/plots"
110
+ generated_dir: str = "outputs/generated"
111
+
112
+ @dataclass
113
+ class IndexingConfig:
114
+ top_k: int = 3
115
+ vector_dim: int = 1024
116
+
117
+ class PipelineConfig:
118
+ def __init__(self, yaml_path: Optional[str] = None):
119
+ if yaml_path is None:
120
+ base_path = Path(__file__).resolve().parent
121
+ yaml_path = base_path / "configs" / "config.yaml"
122
+
123
+ self.yaml_path = Path(yaml_path)
124
+ self._load_yaml()
125
+
126
+ def _load_yaml(self):
127
+ if self.yaml_path.exists():
128
+ with open(self.yaml_path, "r", encoding="utf-8") as f:
129
+ data = yaml.safe_load(f) or {}
130
+ else:
131
+ logger.warning(f"Config not found at {self.yaml_path}. Using defaults.")
132
+ data = {}
133
+
134
+ path_data = data.get('data_paths', {})
135
+ idx_data = data.get('indices_paths', {})
136
+ self.paths = PathConfig(
137
+ data_dir=Path(path_data.get('raw_dir', "data/raw")),
138
+ faiss_index_path=Path(idx_data.get('faiss_index', "data/indices/faiss_semantic.index")),
139
+ ast_store_path=Path(idx_data.get('ast_store', "data/indices/ast_store.pkl"))
140
+ )
141
+
142
+ self.model = ModelConfig(
143
+ name_or_path=data.get('model_name', "Qwen/Qwen2.5-Coder-0.5B-Instruct"),
144
+ embedding_name=data.get('embedding_model', "BAAI/bge-small-en-v1.5")
145
+ )
146
+
147
+ self.lora = _build(LoRAConfig, data.get('lora', {}))
148
+ self.embedding = _build(EmbeddingConfig, data.get('embedding', {}))
149
+ self.retrieval = _build(RetrievalConfig, data.get('retrieval', {}))
150
+ self.llm_baseline = _build(LLMBaselineConfig, data.get('llm_baseline', {}))
151
+ self.generation = _build(GenerationConfig, data.get('generation', {}))
152
+ self.evaluation = _build(EvaluationConfig, data.get('evaluation', {}))
153
+ self.outputs = _build(OutputConfig, data.get('outputs', {}))
154
+ self.indexing = _build(IndexingConfig, data.get('indexing', {}))
155
+
156
+ # Top-level attributes for backward compatibility
157
+ self.model_name = self.model.name_or_path
158
+ self.embedding_model = self.model.embedding_name
159
+ self.data_paths = self.paths
160
+ self.indices_paths = self.paths
161
+
162
+ def load_config(yaml_path: Optional[str] = None) -> PipelineConfig:
163
+ return PipelineConfig(yaml_path=yaml_path)
src/data/data_loader.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class DatasetLoader:
10
+ def __init__(self, target_filename="qwen_spider_mongodb_conversion.json"):
11
+ self.target_filename = target_filename
12
+ self.data = None
13
+ self.file_path = None
14
+
15
+ def scan_and_find_file(self, search_dirs=None):
16
+ """Scans directories recursively to find the target JSON file."""
17
+ if search_dirs is None:
18
+ search_dirs = [
19
+ Path.cwd(),
20
+ Path.cwd().parent,
21
+ Path.cwd() / "data",
22
+ Path.cwd() / "data" / "raw",
23
+ Path.cwd() / "data" / "raw" / "Spider",
24
+ Path.cwd() / "outputs",
25
+ ]
26
+
27
+ search_dirs = list({d.resolve() for d in search_dirs if d.exists()})
28
+
29
+ for directory in search_dirs:
30
+ for path in directory.rglob(self.target_filename):
31
+ if path.is_file():
32
+ logger.info(f"✅ Found {self.target_filename} at: {path}")
33
+ return path
34
+
35
+ logger.warning(f"File not found in predefined paths. Scanning entire {Path.cwd()}...")
36
+ for path in Path.cwd().rglob(self.target_filename):
37
+ if path.is_file():
38
+ logger.info(f"✅ Found {self.target_filename} at: {path}")
39
+ return path
40
+
41
+ raise FileNotFoundError(f"❌ Could not find {self.target_filename} in any scanned directories.")
42
+
43
+ def load_data(self, file_path=None):
44
+ """Loads the JSON data and parses it into a list of dictionaries."""
45
+ if file_path is None:
46
+ file_path = self.scan_and_find_file()
47
+ else:
48
+ file_path = Path(file_path)
49
+ if not file_path.exists():
50
+ raise FileNotFoundError(f"❌ Provided path does not exist: {file_path}")
51
+
52
+ self.file_path = file_path
53
+ logger.info(f"📂 Loading data from {file_path}...")
54
+
55
+ with open(file_path, "r", encoding="utf-8") as f:
56
+ raw_data = json.load(f)
57
+
58
+ if "detailed_results" in raw_data:
59
+ self.data = raw_data["detailed_results"]
60
+ elif isinstance(raw_data, list):
61
+ self.data = raw_data
62
+ else:
63
+ self.data = [raw_data]
64
+
65
+ # Normalize: flatten generated_mongodb_query if it's a list
66
+ for item in self.data:
67
+ if "generated_mongodb_query" in item and isinstance(item["generated_mongodb_query"], list) and len(item["generated_mongodb_query"]) > 0:
68
+ item["generated_mongodb_query"] = item["generated_mongodb_query"][0]
69
+
70
+ logger.info(f"✅ Successfully loaded {len(self.data)} records.")
71
+ return self.data
72
+
73
+ def get_training_data(self):
74
+ """Returns data formatted for LoRA training (Question -> MongoDB Query)."""
75
+ if self.data is None:
76
+ self.load_data()
77
+
78
+ training_data = []
79
+ for item in self.data:
80
+ question = item.get("question", "")
81
+ mongodb_query = item.get("generated_mongodb_query", "")
82
+
83
+ # Flatten if still a list (safety net)
84
+ if isinstance(mongodb_query, list) and len(mongodb_query) > 0:
85
+ mongodb_query = mongodb_query[0]
86
+
87
+ if question and mongodb_query:
88
+ training_data.append({
89
+ "question": question,
90
+ "generated_mongodb_query": mongodb_query,
91
+ "database_id": item.get("database_id", "")
92
+ })
93
+
94
+ logger.info(f"✅ Prepared {len(training_data)} training examples.")
95
+ return training_data
src/evaluation/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from evaluation.metrics import EvaluationMetrics
2
+ from evaluation.comparator import ModelComparator
3
+ from evaluation.rag_evaluation import RAGEvaluator
4
+ from evaluation.visualization import ResultVisualizer
5
+
6
+ __all__ = [
7
+ "EvaluationMetrics",
8
+ "ModelComparator",
9
+ "RAGEvaluator",
10
+ "ResultVisualizer"
11
+ ]
src/evaluation/comparator.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import List, Dict, Any, Optional
4
+ from evaluation.metrics import EvaluationMetrics
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def _default_bertscore_model() -> str:
10
+ local_dir = Path("models/codebert-base")
11
+ if local_dir.is_dir() and (local_dir / "config.json").exists():
12
+ return str(local_dir.resolve())
13
+ return "microsoft/codebert-base"
14
+
15
+
16
+ class ModelComparator:
17
+ """
18
+ Compares generation outputs across 4 categories:
19
+ 1. Base_Model (Small Code LM)
20
+ 2. LoRA_Model (Fine-tuned)
21
+ 3. RAG_Pipeline (Retrieval Augmented)
22
+ 4. LLM_Baseline (OpenAI/Gemini API)
23
+ """
24
+ def __init__(self, device: str = "cpu", bertscore_model: Optional[str] = None):
25
+ self.device = device
26
+ self.bertscore_model = bertscore_model or _default_bertscore_model()
27
+ logger.info(f"ModelComparator using BERTScore/CodeBERTScore backbone: {self.bertscore_model}")
28
+ self.metrics = EvaluationMetrics()
29
+
30
+ def compare(
31
+ self,
32
+ references: List[str],
33
+ base_preds: List[str],
34
+ lora_preds: List[str],
35
+ rag_preds: Optional[List[str]] = None,
36
+ llm_preds: Optional[List[str]] = None,
37
+ test_cases: Optional[List[str]] = None,
38
+ task_type: str = "sql" # "sql" or "doc"
39
+ ) -> Dict[str, Dict[str, float]]:
40
+
41
+ results = {}
42
+ categories = {
43
+ "Base_Model": base_preds,
44
+ "LoRA_Model": lora_preds
45
+ }
46
+ if rag_preds: categories["RAG_Pipeline"] = rag_preds
47
+ if llm_preds: categories["LLM_Baseline"] = llm_preds
48
+
49
+ for name, preds in categories.items():
50
+ logger.info(f"Calculating metrics for {name}...")
51
+ cat_metrics = {}
52
+
53
+ # Text-based metrics (BLEU, ROUGE, BERTScore)
54
+ cat_metrics["BLEU"] = self.metrics.compute_bleu(references, preds)
55
+ rouge_scores = self.metrics.compute_rouge(references, preds)
56
+ cat_metrics.update(rouge_scores)
57
+ cat_metrics["BERTScore"] = self.metrics.compute_bertscore(
58
+ references, preds, device=self.device, model_type=self.bertscore_model
59
+ )
60
+
61
+ # Exact & Response Accuracy
62
+ # exact_matches = [self.metrics.compute_exact_match(r, p) for r, p in zip(references, preds)]
63
+ # cat_metrics["Exact_Match_Accuracy"] = round(sum(exact_matches) / len(exact_matches), 4) if exact_matches else 0.0
64
+
65
+ resp_acc = [self.metrics.compute_response_accuracy(r, p) for r, p in zip(references, preds)]
66
+ cat_metrics["Response_Accuracy"] = round(sum(resp_acc) / len(resp_acc), 4) if resp_acc else 0.0
67
+
68
+ # SQL Execution Accuracy (if task is SQL and test cases/DBs are provided)
69
+ if task_type == "sql" and test_cases:
70
+ pass
71
+
72
+ results[name] = cat_metrics
73
+
74
+ return results
src/evaluation/metrics.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import torch
3
+ import logging
4
+ from typing import List, Dict, Optional
5
+ from nltk.translate.bleu_score import corpus_bleu, SmoothingFunction
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ _BERTSCORE_NUM_LAYERS_OVERRIDES = {
10
+ "models/codebert-base": 10,
11
+ "models/codebert-base-mlm": 10
12
+ }
13
+
14
+
15
+ def _resolve_bertscore_num_layers(model_type: str) -> Optional[int]:
16
+ try:
17
+ from bert_score.utils import model2layers
18
+ except Exception:
19
+ model2layers = {}
20
+
21
+ if model_type in model2layers:
22
+ return model2layers[model_type]
23
+
24
+ short_name = model_type.split("/")[-1]
25
+ if short_name in model2layers:
26
+ return model2layers[short_name]
27
+
28
+ if model_type in _BERTSCORE_NUM_LAYERS_OVERRIDES:
29
+ return _BERTSCORE_NUM_LAYERS_OVERRIDES[model_type]
30
+
31
+ name_lower = model_type.replace("\\", "/").lower()
32
+ for key in sorted(_BERTSCORE_NUM_LAYERS_OVERRIDES, key=len, reverse=True):
33
+ key_short = key.split("/")[-1].lower()
34
+ if key_short in name_lower:
35
+ return _BERTSCORE_NUM_LAYERS_OVERRIDES[key]
36
+
37
+ return None
38
+
39
+
40
+ class EvaluationMetrics:
41
+
42
+ @staticmethod
43
+ def _tokenize_sql(sql: str) -> List[str]:
44
+ """Basic SQL tokenization for BLEU/ROUGE."""
45
+ if not sql: return []
46
+ sql = sql.lower().strip()
47
+ sql = re.sub(r'[^a-z0-9\s_\*\(\)\,\.\'\"\=\>\<\!\+\-\%]', ' ', sql)
48
+ return sql.split()
49
+
50
+ @staticmethod
51
+ def compute_bleu(references: List[str], predictions: List[str]) -> float:
52
+ try:
53
+ refs = [[EvaluationMetrics._tokenize_sql(r)] for r in references]
54
+ preds = [EvaluationMetrics._tokenize_sql(p) for p in predictions]
55
+ smoother = SmoothingFunction().method1
56
+ score = corpus_bleu(refs, preds, smoothing_function=smoother)
57
+ return round(score, 4)
58
+ except Exception as e:
59
+ logger.error(f"BLEU calculation failed: {e}")
60
+ return 0.0
61
+
62
+ @staticmethod
63
+ def compute_rouge(references: List[str], predictions: List[str]) -> Dict[str, float]:
64
+ try:
65
+ from rouge_score import rouge_scorer
66
+ scorer = rouge_scorer.RougeScorer(['rouge1', 'rougeL'], use_stemmer=True)
67
+ rouge1_scores, rougeL_scores = [], []
68
+
69
+ for ref, pred in zip(references, predictions):
70
+ scores = scorer.score(ref, pred)
71
+ rouge1_scores.append(scores['rouge1'].fmeasure)
72
+ rougeL_scores.append(scores['rougeL'].fmeasure)
73
+
74
+ return {
75
+ "ROUGE-1": round(sum(rouge1_scores) / len(rouge1_scores), 4) if rouge1_scores else 0.0,
76
+ "ROUGE-L": round(sum(rougeL_scores) / len(rougeL_scores), 4) if rougeL_scores else 0.0
77
+ }
78
+ except Exception as e:
79
+ logger.error(f"ROUGE calculation failed: {e}")
80
+ return {"ROUGE-1": 0.0, "ROUGE-L": 0.0}
81
+
82
+ @staticmethod
83
+ def compute_bertscore(references: List[str], predictions: List[str], device: str = "cpu", model_type: str = "models/codebert-base") -> float:
84
+ """CodeBERTScore: BERTScore F1 computed with a code-aware encoder (CodeBERT by default).
85
+
86
+ NOTE: `model_type` values that aren't in bert_score's built-in
87
+ `model2layers` table (which is the case for both "microsoft/codebert-base"
88
+ and "FacebookAI/roberta-base") need an explicit `num_layers`, or the
89
+ underlying `bert_score.score()` call raises a KeyError. See
90
+ `_resolve_bertscore_num_layers` above -- this is what was previously
91
+ making CodeBERTScore silently return 0.0 for every model category.
92
+ """
93
+ try:
94
+ from bert_score import score
95
+ valid_pairs = [(r, p) for r, p in zip(references, predictions) if r.strip() and p.strip()]
96
+ if not valid_pairs:
97
+ return 0.0
98
+ refs, preds = zip(*valid_pairs)
99
+
100
+ num_layers = _resolve_bertscore_num_layers(model_type)
101
+ score_kwargs = {"num_layers": num_layers} if num_layers is not None else {}
102
+
103
+ with torch.no_grad():
104
+ P, R, F1 = score(
105
+ list(preds), list(refs),
106
+ model_type=model_type,
107
+ verbose=False,
108
+ device=device,
109
+ **score_kwargs,
110
+ )
111
+ return round(F1.mean().item(), 4)
112
+ except Exception as e:
113
+ logger.error(f"BERTScore ({model_type}) calculation failed: {e}")
114
+ return 0.0
115
+
116
+ @staticmethod
117
+ def _find_top_level_groups(s: str) -> List[tuple]:
118
+ """Locates every top-level `{...}`/`[...]` span in `s`, skipping over
119
+ brace/bracket characters that appear inside double-quoted string
120
+ literals (so e.g. a `$regex: "a{2,3}"` value doesn't confuse the
121
+ bracket matching)."""
122
+ groups = []
123
+ depth = 0
124
+ start = None
125
+ in_string = False
126
+ i, n = 0, len(s)
127
+ while i < n:
128
+ ch = s[i]
129
+ if in_string:
130
+ if ch == '\\':
131
+ i += 2
132
+ continue
133
+ if ch == '"':
134
+ in_string = False
135
+ i += 1
136
+ continue
137
+ if ch == '"':
138
+ in_string = True
139
+ i += 1
140
+ continue
141
+ if ch in '{[':
142
+ if depth == 0:
143
+ start = i
144
+ depth += 1
145
+ elif ch in '}]':
146
+ depth -= 1
147
+ if depth == 0 and start is not None:
148
+ groups.append((start, i + 1))
149
+ start = None
150
+ i += 1
151
+ return groups
152
+
153
+ @staticmethod
154
+ def _split_top_level(inner: str, sep: str) -> List[str]:
155
+ """Splits `inner` on `sep`, but only at bracket-depth 0 and never
156
+ inside a double-quoted string, so nested `{...}`/`[...]`/`(...)`
157
+ and comma-containing string values aren't split incorrectly."""
158
+ parts, depth, current = [], 0, []
159
+ in_string = False
160
+ i = 0
161
+ while i < len(inner):
162
+ ch = inner[i]
163
+ if in_string:
164
+ current.append(ch)
165
+ if ch == '\\' and i + 1 < len(inner):
166
+ current.append(inner[i + 1])
167
+ i += 1
168
+ elif ch == '"':
169
+ in_string = False
170
+ i += 1
171
+ continue
172
+ if ch == '"':
173
+ in_string = True
174
+ current.append(ch)
175
+ i += 1
176
+ continue
177
+ if ch in '{[(':
178
+ depth += 1
179
+ elif ch in '}])':
180
+ depth -= 1
181
+ if ch == sep and depth == 0:
182
+ parts.append(''.join(current))
183
+ current = []
184
+ else:
185
+ current.append(ch)
186
+ i += 1
187
+ parts.append(''.join(current))
188
+ return [p for p in parts if p.strip() != '']
189
+
190
+ @classmethod
191
+ def _canon_group(cls, s: str) -> str:
192
+ s = s.strip()
193
+ if s.startswith('{') and s.endswith('}'):
194
+ inner = s[1:-1]
195
+ items = cls._split_top_level(inner, ',')
196
+ kvs = []
197
+ for item in items:
198
+ if ':' not in item:
199
+ return s # malformed for our purposes -- bail out safely
200
+ k, v = item.split(':', 1)
201
+ kvs.append((k.strip(), cls._canon_group(v.strip())))
202
+ kvs.sort(key=lambda kv: kv[0])
203
+ return '{' + ','.join(f'{k}:{v}' for k, v in kvs) + '}'
204
+ elif s.startswith('[') and s.endswith(']'):
205
+ inner = s[1:-1]
206
+ items = cls._split_top_level(inner, ',')
207
+ return '[' + ','.join(cls._canon_group(i.strip()) for i in items) + ']'
208
+ return s
209
+
210
+ @classmethod
211
+ def _canonicalize_mql(cls, s: str) -> str:
212
+ try:
213
+ if s.count('{') != s.count('}') or s.count('[') != s.count(']'):
214
+ return s
215
+ groups = cls._find_top_level_groups(s)
216
+ if not groups:
217
+ return s
218
+ out, last = [], 0
219
+ for gstart, gend in groups:
220
+ out.append(s[last:gstart])
221
+ out.append(cls._canon_group(s[gstart:gend]))
222
+ last = gend
223
+ out.append(s[last:])
224
+ return ''.join(out)
225
+ except Exception:
226
+ return s
227
+
228
+ # @classmethod
229
+ # def compute_exact_match(cls, gold: str, pred: str) -> float:
230
+ # if not gold or not pred:
231
+ # return 0.0
232
+
233
+ # def normalize(s):
234
+ # s = s.strip().rstrip(';')
235
+ # s = s.replace("'", '"') # Normalize single quotes to double quotes
236
+ # s = cls._canonicalize_mql(s) # Order-independent object-key comparison
237
+ # s = re.sub(r'\s+', '', s) # Remove all remaining whitespace
238
+ # return s.lower()
239
+
240
+ # return 1.0 if normalize(gold) == normalize(pred) else 0.0
241
+
242
+ @staticmethod
243
+ def _tokenize_mql(query: str) -> List[str]:
244
+ """Tokenizer optimized for MongoDB Query Language (MQL) brackets and punctuation."""
245
+ if not query: return []
246
+ # Add spaces around brackets, commas, colons, and dots
247
+ query = re.sub(r'([(){}[\],:.])', r' \1 ', query)
248
+ return [tok.lower() for tok in query.split() if tok.strip()]
249
+
250
+ @staticmethod
251
+ def compute_response_accuracy(gold: str, pred: str) -> float:
252
+ """Semantic response accuracy (recall-based token overlap)."""
253
+ if not gold or not pred: return 0.0
254
+ g_tokens = set(EvaluationMetrics._tokenize_mql(gold))
255
+ p_tokens = set(EvaluationMetrics._tokenize_mql(pred))
256
+ if not g_tokens: return 0.0
257
+ overlap = len(g_tokens.intersection(p_tokens))
258
+ return round(overlap / len(g_tokens), 4)
src/evaluation/rag_evaluation.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import List, Dict, Any
3
+ import matplotlib.pyplot as plt
4
+ from pathlib import Path
5
+ from evaluation.metrics import EvaluationMetrics
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class RAGEvaluator:
10
+ """Evaluates RAG pipeline improvements and optimizes Top-K retrieval."""
11
+
12
+ def __init__(self, comparator):
13
+ self.comparator = comparator
14
+ self.metrics = EvaluationMetrics()
15
+ logger.info(f"RAGEvaluator using BERTScore/CodeBERTScore backbone: {getattr(comparator, 'bertscore_model', 'models/codebert-base')}")
16
+
17
+ def measure_rag_improvement(self, rag_pipeline, references, queries, lora_preds, top_k=3):
18
+ logger.info(f"Measuring RAG improvement over baseline LoRA (top_k={top_k})...")
19
+ rag_preds = []
20
+ for q in queries:
21
+ out, _ = rag_pipeline.generate_with_rag(q, top_k=top_k)
22
+ rag_preds.append(out)
23
+
24
+ bertscore_model = getattr(self.comparator, "bertscore_model", "models/codebert-base")
25
+
26
+ rag_metrics = {
27
+ "BLEU": self.metrics.compute_bleu(references, rag_preds),
28
+ "BERTScore": self.metrics.compute_bertscore(references, rag_preds, device=self.comparator.device, model_type=bertscore_model)
29
+ # "Exact_Match_Accuracy": round(sum([self.metrics.compute_exact_match(r, p) for r, p in zip(references, rag_preds)]) / len(references), 4)
30
+ }
31
+
32
+ baseline_metrics = {
33
+ "BLEU": self.metrics.compute_bleu(references, lora_preds),
34
+ "BERTScore": self.metrics.compute_bertscore(references, lora_preds, device=self.comparator.device, model_type=bertscore_model)
35
+ # "Exact_Match_Accuracy": round(sum([self.metrics.compute_exact_match(r, p) for r, p in zip(references, lora_preds)]) / len(references), 4)
36
+ }
37
+
38
+ gain = {k: round(rag_metrics[k] - baseline_metrics[k], 4) for k in rag_metrics}
39
+
40
+ return {
41
+ "metrics": {"RAG_Pipeline": rag_metrics, "Baseline_LoRA": baseline_metrics},
42
+ "gain_over_baseline": gain,
43
+ "baseline_used_for_gain": "Baseline_LoRA"
44
+ }
45
+
46
+ def sweep_top_k(self, rag_pipeline, references, queries, baseline_preds, k_values=[1, 2, 3, 5, 8], verbose=True):
47
+ sweep_results = {}
48
+
49
+ # Pre-compute baseline metrics once to save time
50
+ baseline_bleu = self.metrics.compute_bleu(references, baseline_preds)
51
+ baseline_rouge = self.metrics.compute_rouge(references, baseline_preds).get("ROUGE-1", 0.0)
52
+
53
+ for k in k_values:
54
+ logger.info(f"Sweeping top_k={k}...")
55
+ preds = []
56
+ for q in queries:
57
+ out, _ = rag_pipeline.generate_with_rag(q, top_k=k)
58
+ preds.append(out)
59
+
60
+ # Use BLEU and ROUGE-1 for sweeping
61
+ bleu = self.metrics.compute_bleu(references, preds)
62
+ rouge1 = self.metrics.compute_rouge(references, preds).get("ROUGE-1", 0.0)
63
+
64
+ metrics = {"BLEU": bleu, "ROUGE-1": rouge1}
65
+ gain = {
66
+ "BLEU": round(bleu - baseline_bleu, 4),
67
+ "ROUGE-1": round(rouge1 - baseline_rouge, 4)
68
+ }
69
+
70
+ sweep_results[k] = {"metrics": metrics, "gain_over_baseline": gain, "predictions": preds}
71
+
72
+ if verbose:
73
+ print("\n" + self.format_sweep_table(sweep_results))
74
+
75
+ return sweep_results
76
+
77
+ @staticmethod
78
+ def format_sweep_table(sweep_results: Dict[int, Dict[str, Any]]) -> str:
79
+ ks = list(sweep_results.keys())
80
+ if not ks:
81
+ return "(no sweep results to display)"
82
+
83
+ columns = [
84
+ ("Top-K", lambda k, r: str(k)),
85
+ ("BLEU", lambda k, r: f"{r['metrics'].get('BLEU', 0.0):.4f}"),
86
+ ("ROUGE-1", lambda k, r: f"{r['metrics'].get('ROUGE-1', 0.0):.4f}"),
87
+ ("BLEU Gain", lambda k, r: f"{r['gain_over_baseline'].get('BLEU', 0.0):+.4f}"),
88
+ ("ROUGE-1 Gain", lambda k, r: f"{r['gain_over_baseline'].get('ROUGE-1', 0.0):+.4f}"),
89
+ ]
90
+
91
+ rows = [[render(k, sweep_results[k]) for _, render in columns] for k in ks]
92
+ headers = [name for name, _ in columns]
93
+
94
+ widths = [
95
+ max(len(headers[i]), *(len(row[i]) for row in rows))
96
+ for i in range(len(columns))
97
+ ]
98
+
99
+ def fmt_row(cells):
100
+ return "| " + " | ".join(c.center(w) for c, w in zip(cells, widths)) + " |"
101
+
102
+ separator = "+-" + "-+-".join("-" * w for w in widths) + "-+"
103
+
104
+ lines = ["Top-K Retrieval Sweep Results", separator, fmt_row(headers), separator]
105
+ lines += [fmt_row(row) for row in rows]
106
+ lines.append(separator)
107
+ return "\n".join(lines)
108
+
109
+ def best_k(self, sweep_results, metric="BLEU"):
110
+ best_k_val = -999
111
+ best_k = None
112
+ for k, res in sweep_results.items():
113
+ val = res["gain_over_baseline"].get(metric, -999)
114
+ if val > best_k_val:
115
+ best_k_val = val
116
+ best_k = k
117
+ return best_k if best_k is not None else list(sweep_results.keys())[0]
118
+
119
+ def plot_gain_vs_k(self, sweep_results, save_path="output/plots/topk_gain_sweep.png"):
120
+ Path(save_path).parent.mkdir(parents=True, exist_ok=True)
121
+ ks = list(sweep_results.keys())
122
+ bleu_gains = [sweep_results[k]["gain_over_baseline"].get("BLEU", 0) for k in ks]
123
+
124
+ plt.figure(figsize=(8, 5))
125
+ plt.plot(ks, bleu_gains, marker='o', linestyle='-', color='b')
126
+ plt.title("RAG Gain over Baseline by Top-K")
127
+ plt.xlabel("Top-K Retrieved Contexts")
128
+ plt.ylabel("BLEU Score Gain")
129
+ plt.grid(True)
130
+ plt.savefig(save_path)
131
+ plt.close()
132
+ logger.info(f"✅ Saved Top-K sweep plot to {save_path}")
src/evaluation/visualization.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import matplotlib.pyplot as plt
3
+ import seaborn as sns
4
+ import pandas as pd
5
+ from pathlib import Path
6
+ from typing import Dict
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class ResultVisualizer:
11
+ """Visualizes evaluation metrics across Base, LoRA, RAG, and LLM categories."""
12
+
13
+ def __init__(self, output_dir: str = "output/plots"):
14
+ self.output_dir = Path(output_dir)
15
+ self.output_dir.mkdir(parents=True, exist_ok=True)
16
+ sns.set_theme(style="whitegrid", palette="muted")
17
+
18
+ def plot_comparison(self, eval_results: Dict[str, Dict[str, float]], save_name: str = "model_comparison.png", bertscore_model_label: str = None):
19
+ # Flatten dictionary for seaborn
20
+ data = []
21
+ for model_name, metrics in eval_results.items():
22
+ for metric_name, value in metrics.items():
23
+ data.append({"Model": model_name, "Metric": metric_name, "Score": value})
24
+
25
+ df = pd.DataFrame(data)
26
+
27
+ if df.empty:
28
+ logger.warning("No data to plot.")
29
+ return
30
+
31
+ plt.figure(figsize=(14, 8))
32
+ sns.barplot(x="Metric", y="Score", hue="Model", data=df)
33
+
34
+ plt.title("Comprehensive Model Comparison: Base vs LoRA vs RAG vs LLM", fontsize=16, fontweight='bold')
35
+ if bertscore_model_label:
36
+ plt.suptitle(f"BERTScore backbone: {bertscore_model_label}", fontsize=9, y=0.93, color="gray")
37
+ plt.ylabel("Score", fontsize=12)
38
+ plt.xlabel("Evaluation Metric", fontsize=12)
39
+ plt.xticks(rotation=45, ha='right')
40
+ plt.legend(title="Model Category", bbox_to_anchor=(1.05, 1), loc='upper left')
41
+ plt.tight_layout()
42
+
43
+ save_path = self.output_dir / save_name
44
+ plt.savefig(save_path, dpi=300)
45
+ plt.close()
46
+ logger.info(f"✅ Saved comparison plot to {save_path}")
47
+
48
+ def plot_radar_chart(self, eval_results: Dict[str, Dict[str, float]], save_name: str = "radar_comparison.png"):
49
+ """Generates a radar chart for a holistic view of model capabilities."""
50
+ import numpy as np
51
+
52
+ categories = list(next(iter(eval_results.values())).keys())
53
+ N = len(categories)
54
+ if N == 0: return
55
+
56
+ # Compute the angle for each axis
57
+ angles = [n / float(N) * 2 * 3.14159 for n in range(N)]
58
+ angles += angles[:1]
59
+
60
+ fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
61
+
62
+ for model_name, metrics in eval_results.items():
63
+ values = list(metrics.values())
64
+ values += values[:1]
65
+ ax.plot(angles, values, linewidth=2, label=model_name)
66
+ ax.fill(angles, values, alpha=0.15)
67
+
68
+ ax.set_xticks(angles[:-1])
69
+ ax.set_xticklabels(categories, fontsize=10)
70
+ plt.title("Model Capability Radar", size=15, y=1.1, fontweight='bold')
71
+ plt.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1))
72
+
73
+ save_path = self.output_dir / save_name
74
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
75
+ plt.close()
76
+ logger.info(f"✅ Saved radar chart to {save_path}")
src/generators/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from generators.program_generator import GenerationPipeline
2
+ from generators.doc_generator import DocGenerator
3
+ from generators.commit_generator import CommitMessageGenerator
4
+ from generators.text_to_mongo_generator import TextToMongoGenerator
5
+
6
+ __all__ = [
7
+ "GenerationPipeline",
8
+ "DocGenerator",
9
+ "CommitMessageGenerator",
10
+ "TextToMongoGenerator"
11
+ ]
src/generators/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (489 Bytes). View file
 
src/generators/__pycache__/commit_generator.cpython-312.pyc ADDED
Binary file (1.62 kB). View file
 
src/generators/__pycache__/doc_generator.cpython-312.pyc ADDED
Binary file (2.58 kB). View file
 
src/generators/__pycache__/program_generator.cpython-312.pyc ADDED
Binary file (3.74 kB). View file
 
src/generators/__pycache__/text_to_mongo_generator.cpython-312.pyc ADDED
Binary file (6.15 kB). View file
 
src/generators/commit_generator.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+ class CommitMessageGenerator:
6
+ """
7
+ Generates conventional commit messages based on git diffs.
8
+ """
9
+ def __init__(self, pipeline):
10
+ self.pipeline = pipeline
11
+
12
+ def _build_prompt(self, diff: str) -> str:
13
+ prompt = (
14
+ "You are an expert software developer. Generate a concise, conventional commit message "
15
+ "for the following git diff. The message should start with a standard type "
16
+ "(feat, fix, docs, style, refactor, test, chore) followed by a colon and a short, "
17
+ "imperative summary of the changes.\n\n"
18
+ f"Diff:\n{diff}\n\n"
19
+ "Commit Message:"
20
+ )
21
+ return prompt
22
+
23
+ def generate_commit_msg(self, diff: str) -> str:
24
+ """Generates a commit message for the provided git diff."""
25
+ prompt = self._build_prompt(diff)
26
+ return self.pipeline.generate(prompt)
src/generators/doc_generator.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+ class DocGenerator:
6
+ """
7
+ Generates documentation/docstrings for code snippets using 5-shot prompting.
8
+ """
9
+ def __init__(self, pipeline):
10
+ self.pipeline = pipeline
11
+
12
+ def _build_prompt(self, code_snippet: str) -> str:
13
+ # 5 high-quality examples for few-shot learning
14
+ examples = [
15
+ {
16
+ "code": "def add(a, b):\n return a + b",
17
+ "doc": "Adds two numbers together and returns their sum."
18
+ },
19
+ {
20
+ "code": "import os\ndef list_files(directory):\n return os.listdir(directory)",
21
+ "doc": "Returns a list containing the names of all files and directories in the specified directory path."
22
+ },
23
+ {
24
+ "code": "class Dog:\n def __init__(self, name):\n self.name = name\n def bark(self):\n return 'Woof!'",
25
+ "doc": "A simple Dog class that initializes with a name and provides a method to make the dog bark."
26
+ },
27
+ {
28
+ "code": "def factorial(n):\n if n == 0:\n return 1\n return n * factorial(n-1)",
29
+ "doc": "Calculates the factorial of a given non-negative integer n using a recursive approach."
30
+ },
31
+ {
32
+ "code": "import json\ndef parse_json(file_path):\n with open(file_path, 'r') as f:\n return json.load(f)",
33
+ "doc": "Reads a JSON file from the specified file path and returns the parsed content as a Python dictionary."
34
+ }
35
+ ]
36
+
37
+ prompt = "You are an expert programmer. Generate a concise, accurate, and professional docstring/description for the given code.\n\n"
38
+ for ex in examples:
39
+ prompt += f"Code:\n{ex['code']}\nDescription: {ex['doc']}\n\n"
40
+
41
+ prompt += f"Code:\n{code_snippet}\nDescription:"
42
+ return prompt
43
+
44
+ def generate_docstring(self, code_snippet: str) -> str:
45
+ """Generates a docstring for the provided code snippet."""
46
+ prompt = self._build_prompt(code_snippet)
47
+ return self.pipeline.generate(prompt)
src/generators/llm_baseline.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import time
4
+ from typing import Optional
5
+ from google import genai
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class LargeLLMGenerator:
10
+ """
11
+ Wrapper for Google Gemini API to serve as an upper-bound baseline for fast, reliable generation.
12
+ """
13
+ def __init__(self, config=None, backend: str = "api", api_model: str = "gemini-3.6-flash", local_model_name: str = None):
14
+ self.backend = backend
15
+ self.api_model = api_model
16
+ self.local_model_name = local_model_name
17
+ self.client = None
18
+ self.config = config
19
+ self.api_key_env_name = "GEMINI_API_KEY"
20
+
21
+ # Override with config if provided
22
+ if config and hasattr(config, "llm_baseline"):
23
+ llm_cfg = config.llm_baseline
24
+ self.backend = getattr(llm_cfg, "backend", self.backend)
25
+ self.api_model = getattr(llm_cfg, "api_model", self.api_model)
26
+ self.local_model_name = getattr(llm_cfg, "local_model", self.local_model_name)
27
+ self.api_key_env_name = getattr(llm_cfg, "api_key_env", None) or self.api_key_env_name
28
+
29
+ self._initialize_client()
30
+
31
+ def _initialize_client(self):
32
+ if self.backend == "api":
33
+ self._init_gemini()
34
+ else:
35
+ logger.info(f"Using local/off-the-shelf LLM backend: {self.local_model_name}")
36
+
37
+ def _init_gemini(self):
38
+ try:
39
+ api_key = os.getenv(self.api_key_env_name)
40
+
41
+ if not api_key:
42
+ logger.error(
43
+ f"API key not found. Set the '{self.api_key_env_name}' environment "
44
+ "variable before initializing LargeLLMGenerator."
45
+ )
46
+ return
47
+
48
+ # Initialize the modern google-genai client
49
+ self.client = genai.Client(api_key=api_key)
50
+ logger.info(f"✅ Initialized Gemini client for model: {self.api_model}")
51
+ except ImportError:
52
+ logger.error("google-genai package not installed. Run: pip install google-genai")
53
+ except Exception as e:
54
+ logger.error(f"Failed to initialize Gemini client: {e}")
55
+
56
+ def generate(self, prompt: str, max_tokens: int = 512, temperature: float = 0.2) -> str:
57
+ if self.backend == "api":
58
+ return self._generate_api(prompt, max_tokens, temperature)
59
+ else:
60
+ logger.warning("Local LLM generation not fully implemented in this baseline wrapper. Returning empty string.")
61
+ return ""
62
+
63
+ def _generate_api(self, prompt: str, max_tokens: int, temperature: float) -> str:
64
+ if not self.client:
65
+ return "[API Client Not Initialized]"
66
+ try:
67
+ start_time = time.time()
68
+
69
+ # Generate content using the google-genai client
70
+ response = self.client.models.generate_content(
71
+ model=self.api_model,
72
+ contents=prompt,
73
+ config={
74
+ "max_output_tokens": max_tokens,
75
+ "temperature": temperature
76
+ }
77
+ )
78
+ text = response.text.strip()
79
+
80
+ latency = round((time.time() - start_time) * 1000, 2)
81
+ logger.info(f"Gemini API ({self.api_model}) generated {len(text)} chars in {latency}ms")
82
+ return text
83
+ except Exception as e:
84
+ logger.error(f"API Generation failed: {e}")
85
+ return f"[API Error: {str(e)}]"
src/generators/program_generator.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch
3
+ import os
4
+ os.environ.setdefault("PYTORCH_ENABLE_MPS_FALLBACK", "1")
5
+
6
+ from typing import Any
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class GenerationPipeline:
12
+ def __init__(self, model: Any, tokenizer: Any, config: Any = None):
13
+ self.model = model
14
+ self.tokenizer = tokenizer
15
+ self.config = config
16
+
17
+ def _build_generation_kwargs(self) -> dict:
18
+ """Builds generation kwargs based on config or defaults."""
19
+ # Safely extract token IDs as standard Python ints to prevent PyTorch warnings
20
+ eos_id = self.tokenizer.eos_token_id
21
+ if isinstance(eos_id, torch.Tensor):
22
+ eos_id = eos_id.item()
23
+
24
+ kwargs = {
25
+ "max_new_tokens": 128, # Hard cap: MongoDB queries are short
26
+ "temperature": 0.2,
27
+ "top_p": 0.95,
28
+ "top_k": 50,
29
+ "do_sample": True,
30
+ "pad_token_id": eos_id,
31
+ "eos_token_id": eos_id,
32
+ "repetition_penalty": 1.2,
33
+ "no_repeat_ngram_size": 3, # CRITICAL: Prevents repeating blocks of numbers/text
34
+ }
35
+ if self.config:
36
+ kwargs["max_new_tokens"] = min(getattr(self.config, "max_length", 256), 128)
37
+ kwargs["temperature"] = getattr(self.config, "temperature", 0.2)
38
+ kwargs["top_p"] = getattr(self.config, "top_p", 0.95)
39
+ kwargs["top_k"] = getattr(self.config, "top_k", 50)
40
+ return kwargs
41
+
42
+ def generate(self, prompt: str) -> str:
43
+ """Generates text from a given prompt."""
44
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
45
+ with torch.no_grad():
46
+ outputs = self.model.generate(**inputs, **self._build_generation_kwargs())
47
+
48
+ generated_text = self.tokenizer.decode(
49
+ outputs[0][inputs["input_ids"].shape[1]:],
50
+ skip_special_tokens=True
51
+ )
52
+ generated_text = generated_text.strip()
53
+
54
+ generated_text = self._clean_output(generated_text)
55
+ return generated_text
56
+
57
+ @staticmethod
58
+ def _clean_output(text: str) -> str:
59
+ """Remove hallucinated refusals or trailing junk after the actual answer."""
60
+ # Common patterns that indicate the model went off-track
61
+ stop_patterns = [
62
+ "I'm sorry", "I can't", "I cannot", "I am sorry",
63
+ "I apologize", "As an AI", "As a language model",
64
+ "```", "\n\n", "###", "Human:", "User:", "Assistant:"
65
+ ]
66
+ for pattern in stop_patterns:
67
+ idx = text.find(pattern)
68
+ if idx > 0: # Only cut if it's not at the very start
69
+ text = text[:idx]
70
+ return text.strip()
src/generators/text_to_mongo_generator.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import List, Dict, Any
6
+ from tqdm import tqdm
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class TextToMongoGenerator:
12
+ def __init__(self, pipeline, config: Any = None):
13
+ self.pipeline = pipeline
14
+ self.config = config
15
+
16
+ def _build_prompt(self, question: str, schema: str = "") -> str:
17
+ prompt = "### Task: Generate MongoDB Query (MQL)\n"
18
+ if schema:
19
+ prompt += f"### Collection Schema / Structure:\n{schema}\n\n"
20
+ prompt += f"### Question:\n{question}\n\n### MongoDB Query:\n db."
21
+ return prompt
22
+
23
+ def generate_single(self, question: str, schema: str = "") -> Dict[str, Any]:
24
+ """Generates a single MongoDB query and measures latency."""
25
+ prompt = self._build_prompt(question, schema)
26
+ start_time = time.perf_counter()
27
+ raw_output = self.pipeline.generate(prompt)
28
+ end_time = time.perf_counter()
29
+ latency_ms = (end_time - start_time) * 1000
30
+
31
+ # Clean up hallucinations and repetition loops
32
+ generated_query = self._clean_mongo_output(raw_output)
33
+
34
+ return {
35
+ "generated_query": generated_query,
36
+ "latency_ms": round(latency_ms, 2)
37
+ }
38
+
39
+ @staticmethod
40
+ def clean_mongo_output(raw_output: str) -> str:
41
+ stripped = raw_output.strip()
42
+ if stripped.lower().startswith("db."):
43
+ # Model re-emitted the prefix itself; don't duplicate it.
44
+ restored = stripped
45
+ else:
46
+ restored = "db." + stripped
47
+
48
+ cleaned = (
49
+ restored.replace("```javascript", "")
50
+ .replace("```json", "")
51
+ .replace("```js", "")
52
+ .replace("```", "")
53
+ .strip()
54
+ )
55
+
56
+ # Stop at common hallucination markers
57
+ stop_markers = ["--->", "\n\n", "\nUser:", "\nQuestion:", "```", "###", "Human:"]
58
+ for marker in stop_markers:
59
+ idx = cleaned.find(marker)
60
+ if idx != -1:
61
+ cleaned = cleaned[:idx]
62
+
63
+ # Extract the first valid MongoDB query line
64
+ lines = cleaned.split('\n')
65
+ for line in lines:
66
+ line = line.strip()
67
+ if line.startswith("db."):
68
+ return line.rstrip(';').strip()
69
+
70
+ # Fallback: find "db." anywhere in the text
71
+ if "db." in cleaned:
72
+ idx = cleaned.find("db.")
73
+ end_idx = len(cleaned)
74
+ for marker in stop_markers + ["\n"]:
75
+ pos = cleaned.find(marker, idx)
76
+ if pos != -1:
77
+ end_idx = min(end_idx, pos)
78
+ return cleaned[idx:end_idx].strip().rstrip(';')
79
+
80
+ return cleaned.strip()
81
+
82
+ _clean_mongo_output = clean_mongo_output
83
+
84
+ def generate_batch(self, dataset: List[Dict[str, Any]], output_path: str = None) -> List[Dict[str, Any]]:
85
+ results = []
86
+ logger.info(f"🚀 Starting batch MongoDB query generation for {len(dataset)} examples...")
87
+
88
+ for item in tqdm(dataset, desc="Generating MongoDB Queries", unit="query"):
89
+ query_id = item.get("query_id", f"sample_{len(results)}")
90
+ database_id = item.get("database_id", "unknown")
91
+ question = item.get("question", "")
92
+ schema = item.get("schema", "")
93
+ gold_query = item.get("generated_mongodb_query", item.get("gold_query", ""))
94
+ if isinstance(gold_query, list) and len(gold_query) > 0:
95
+ gold_query = gold_query[0]
96
+
97
+ if not question:
98
+ continue
99
+
100
+ gen_result = self.generate_single(question, schema)
101
+
102
+ result_entry = {
103
+ "query_id": query_id,
104
+ "database_id": database_id,
105
+ "question": question,
106
+ "gold_query": gold_query,
107
+ "generated_query": gen_result["generated_query"],
108
+ "latency_ms": gen_result["latency_ms"],
109
+ }
110
+ results.append(result_entry)
111
+
112
+ if output_path:
113
+ self.save_results(results, output_path)
114
+
115
+ return results
116
+
117
+ def save_results(self, results: List[Dict[str, Any]], output_path: str):
118
+ path = Path(output_path)
119
+ path.parent.mkdir(parents=True, exist_ok=True)
120
+
121
+ output_data = {"results": results}
122
+ with open(path, "w", encoding="utf-8") as f:
123
+ json.dump(output_data, f, indent=4, ensure_ascii=False)
124
+
125
+ logger.info(f"✅ Successfully saved {len(results)} generated queries to {path}")
src/models/__pycache__/load_model.cpython-312.pyc ADDED
Binary file (15.7 kB). View file
 
src/models/load_model.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import torch
4
+ from pathlib import Path
5
+ from tqdm import tqdm
6
+ from peft import LoraConfig, get_peft_model, PeftModel
7
+ from transformers import AutoModelForCausalLM, AutoTokenizer
8
+ from torch.utils.data import DataLoader, Dataset
9
+
10
+ os.environ["PYTORCH_MPS_HIGH_WATERMARK_RATIO"] = "0.0"
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class SpiderDataset(Dataset):
15
+ """Formats Spider JSON data into causal LM training examples (Question -> MongoDB Query)."""
16
+
17
+ def __init__(self, data, tokenizer, max_length=512):
18
+ self.data = data
19
+ self.tokenizer = tokenizer
20
+ self.max_length = max_length
21
+
22
+ def __len__(self):
23
+ return len(self.data)
24
+
25
+ def __getitem__(self, idx):
26
+ item = self.data[idx]
27
+ question = item.get("question", "")
28
+ mongodb_query = item.get("generated_mongodb_query", "")
29
+
30
+ # Handle if stored as a list
31
+ if isinstance(mongodb_query, list) and len(mongodb_query) > 0:
32
+ mongodb_query = mongodb_query[0]
33
+
34
+ prompt = f"### Task: Generate MongoDB Query (MQL)\n### Question:\n{question}\n\n### MongoDB Query:\n"
35
+
36
+ prompt_enc = self.tokenizer(prompt, truncation=True, max_length=self.max_length, return_tensors="pt")
37
+ completion_enc = self.tokenizer(mongodb_query, truncation=True, max_length=self.max_length, return_tensors="pt")
38
+
39
+ input_ids = torch.cat([prompt_enc.input_ids[0], completion_enc.input_ids[0]], dim=0)
40
+ attention_mask = torch.cat([prompt_enc.attention_mask[0], completion_enc.attention_mask[0]], dim=0)
41
+
42
+ prompt_len = prompt_enc.input_ids.shape[1]
43
+ labels = torch.cat([torch.full((prompt_len,), -100, dtype=torch.long), completion_enc.input_ids[0]], dim=0)
44
+
45
+ return {
46
+ "input_ids": input_ids,
47
+ "attention_mask": attention_mask,
48
+ "labels": labels
49
+ }
50
+
51
+ class ModelLoader:
52
+ def __init__(self, config, device_override=None):
53
+ self.config = config
54
+ self.device = device_override or self._detect_device()
55
+ if self.device == "cuda":
56
+ self.dtype = torch.float16
57
+ elif self.device == "mps":
58
+ self.dtype = torch.float16
59
+ else:
60
+ self.dtype = torch.float32
61
+ self.tokenizer = None
62
+ self.base_model = None
63
+ self.lora_model = None
64
+
65
+ logger.info(f"🖥️ Initialized ModelLoader | Device: {self.device} | Dtype: {self.dtype}")
66
+
67
+ def _detect_device(self):
68
+ if torch.cuda.is_available():
69
+ return "cuda"
70
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
71
+ return "mps"
72
+ return "cpu"
73
+
74
+ def load_tokenizer(self):
75
+ model_name = getattr(self.config, "model_name", "Qwen/Qwen2.5-Coder-0.5B-Instruct")
76
+ logger.info(f"🔄 Loading tokenizer: {model_name}")
77
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
78
+ if self.tokenizer.pad_token is None:
79
+ self.tokenizer.pad_token = self.tokenizer.eos_token
80
+ return self.tokenizer
81
+
82
+ def load_base_model(self):
83
+ model_name = getattr(self.config, "model_name", "Qwen/Qwen2.5-Coder-0.5B-Instruct")
84
+ logger.info(f"🔄 Loading base model: {model_name}")
85
+
86
+ if self.device == "cuda":
87
+ self.base_model = AutoModelForCausalLM.from_pretrained(
88
+ model_name,
89
+ torch_dtype=self.dtype,
90
+ trust_remote_code=True,
91
+ low_cpu_mem_usage=True,
92
+ device_map="auto"
93
+ )
94
+ else:
95
+ self.base_model = AutoModelForCausalLM.from_pretrained(
96
+ model_name,
97
+ torch_dtype=self.dtype,
98
+ trust_remote_code=True,
99
+ low_cpu_mem_usage=True
100
+ )
101
+ self.base_model.to(self.device)
102
+
103
+ self.base_model.eval()
104
+ return self.base_model
105
+
106
+ def setup_lora(self, trainable_fraction=1.0, r=16, alpha=32, dropout=0.05):
107
+ if self.base_model is None:
108
+ self.load_base_model()
109
+
110
+ target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
111
+
112
+ peft_config = LoraConfig(
113
+ r=r,
114
+ lora_alpha=alpha,
115
+ target_modules=target_modules,
116
+ lora_dropout=dropout,
117
+ bias="none",
118
+ task_type="CAUSAL_LM"
119
+ )
120
+
121
+ self.lora_model = get_peft_model(self.base_model, peft_config)
122
+
123
+ if trainable_fraction < 1.0:
124
+ self._apply_trainable_fraction(trainable_fraction)
125
+ else:
126
+ logger.info("✅ LoRA configured with 100% trainable parameters.")
127
+
128
+ self.lora_model.print_trainable_parameters()
129
+ return self.lora_model
130
+
131
+ def _apply_trainable_fraction(self, fraction):
132
+ lora_params = [(n, p) for n, p in self.lora_model.named_parameters() if "lora_" in n and p.requires_grad]
133
+ total_lora = len(lora_params)
134
+ num_to_freeze = int(total_lora * (1.0 - fraction))
135
+
136
+ if num_to_freeze > 0:
137
+ for i, (name, param) in enumerate(lora_params):
138
+ if i < num_to_freeze:
139
+ param.requires_grad = False
140
+
141
+ trainable_count = sum(1 for p in self.lora_model.parameters() if p.requires_grad)
142
+ logger.info(f"🔒 Froze {num_to_freeze}/{total_lora} LoRA parameters. Remaining trainable: {trainable_count} ({fraction*100}%).")
143
+
144
+ def autodetect_saved_model(self, output_dir):
145
+ output_dir = Path(output_dir)
146
+ if output_dir.exists() and (output_dir / "adapter_config.json").exists():
147
+ logger.info(f"✅ Found saved LoRA adapter at {output_dir}. Loading...")
148
+ if self.base_model is None:
149
+ self.load_base_model()
150
+ # NOTE (ZeroGPU compatibility): peft's from_pretrained() infers the load
151
+ # device via infer_device(), NOT from the base model's actual .device.
152
+ # On HF Spaces ZeroGPU, `spaces` monkey-patches torch.cuda.is_available()
153
+ # to report True even before a real GPU is attached to the process, so
154
+ # infer_device() always says "cuda" here -- which makes peft try to load
155
+ # the adapter's .safetensors weights with a raw, direct CUDA file load.
156
+ # That bypasses spaces' safe .to("cuda") staging and crashes with
157
+ # "No CUDA GPUs are available". Forcing torch_device="cpu" here loads the
158
+ # adapter safely on CPU; the single .to(self.device) call right after is
159
+ # a normal nn.Module.to() move, which spaces *does* stage/support.
160
+ self.lora_model = PeftModel.from_pretrained(self.base_model, str(output_dir), torch_device="cpu")
161
+ self.lora_model.to(self.device)
162
+ self.lora_model.eval()
163
+ return self.lora_model
164
+ logger.info(f"ℹ️ No saved adapter found at {output_dir}.")
165
+ return None
166
+
167
+ def train_lora(self, train_data, output_dir, epochs=3, batch_size=1, grad_accum=4, lr=2e-4, max_length=512):
168
+ lr = float(lr) # Ensure lr is a float to prevent PyTorch optimizer TypeError
169
+ if self.lora_model is None:
170
+ self.setup_lora()
171
+
172
+ dataset = SpiderDataset(train_data, self.tokenizer, max_length)
173
+
174
+ def collate_fn(batch):
175
+ max_len = max(len(b["input_ids"]) for b in batch)
176
+ input_ids, attention_mask, labels = [], [], []
177
+ pad_id = self.tokenizer.pad_token_id
178
+
179
+ for b in batch:
180
+ pad_len = max_len - len(b["input_ids"])
181
+ input_ids.append(b["input_ids"].tolist() + [pad_id] * pad_len)
182
+ attention_mask.append(b["attention_mask"].tolist() + [0] * pad_len)
183
+ labels.append(b["labels"].tolist() + [-100] * pad_len)
184
+
185
+ return {
186
+ "input_ids": torch.tensor(input_ids, dtype=torch.long).to(self.device),
187
+ "attention_mask": torch.tensor(attention_mask, dtype=torch.long).to(self.device),
188
+ "labels": torch.tensor(labels, dtype=torch.long).to(self.device)
189
+ }
190
+
191
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
192
+
193
+ optimizer = torch.optim.AdamW(
194
+ filter(lambda p: p.requires_grad, self.lora_model.parameters()),
195
+ lr=lr
196
+ )
197
+
198
+ self.lora_model.train()
199
+
200
+ logger.info(f"🚀 Starting LoRA training | Device: {self.device} | Epochs: {epochs} | Batch: {batch_size} | Grad Accum: {grad_accum} | LR: {lr}")
201
+
202
+ for epoch in range(epochs):
203
+ epoch_loss = 0.0
204
+ progress_bar = tqdm(loader, desc=f"Epoch {epoch+1}/{epochs}", leave=False)
205
+
206
+ optimizer.zero_grad()
207
+
208
+ for step, batch in enumerate(progress_bar):
209
+ outputs = self.lora_model(**batch)
210
+ loss = outputs.loss / grad_accum
211
+ loss.backward()
212
+ epoch_loss += loss.item() * grad_accum
213
+
214
+ if (step + 1) % grad_accum == 0:
215
+ optimizer.step()
216
+ optimizer.zero_grad()
217
+
218
+ progress_bar.set_postfix({"loss": f"{loss.item() * grad_accum:.4f}"})
219
+
220
+ # Clear MPS cache periodically to prevent memory leaks on Mac
221
+ if self.device == "mps" and (step + 1) % 10 == 0:
222
+ torch.mps.empty_cache()
223
+
224
+ optimizer.step()
225
+ optimizer.zero_grad()
226
+
227
+ avg_loss = epoch_loss / len(loader)
228
+ logger.info(f"✅ Epoch {epoch+1} completed | Avg Loss: {avg_loss:.4f}")
229
+
230
+ output_dir = Path(output_dir)
231
+ output_dir.mkdir(parents=True, exist_ok=True)
232
+ self.lora_model.save_pretrained(str(output_dir))
233
+ self.tokenizer.save_pretrained(str(output_dir))
234
+ logger.info(f"💾 LoRA adapter saved to {output_dir}")
235
+
236
+ self.lora_model.eval()
237
+ return self.lora_model
238
+
239
+ def load_models(self, lora_path=None):
240
+ """Returns a dictionary of models and the tokenizer."""
241
+ if self.tokenizer is None:
242
+ self.load_tokenizer()
243
+ if self.base_model is None:
244
+ self.load_base_model()
245
+
246
+ models = {"base": self.base_model, "lora": None}
247
+
248
+ if self.lora_model is not None:
249
+ logger.info("Reusing already-loaded LoRA model in memory.")
250
+ models["lora"] = self.lora_model
251
+ elif lora_path:
252
+ lora_path = Path(lora_path)
253
+ if lora_path.exists() and (lora_path / "adapter_config.json").exists():
254
+ logger.info(f"Loading LoRA model from {lora_path}")
255
+ self.lora_model = PeftModel.from_pretrained(self.base_model, str(lora_path))
256
+ self.lora_model.to(self.device)
257
+ self.lora_model.eval()
258
+ models["lora"] = self.lora_model
259
+ else:
260
+ logger.warning(f"LoRA path {lora_path} does not exist or is invalid. Returning base model as 'lora'.")
261
+ models["lora"] = self.base_model
262
+
263
+ return models, self.tokenizer
src/plots/baseline_vs_finetuned_latency.png ADDED
src/plots/baseline_vs_finetuned_text_to_mongo.png ADDED
src/plots/text_to_mongo_baseline_metrics.png ADDED
src/plots/text_to_mongo_finetuned_metrics.png ADDED
src/rag/__pycache__/rag_pipeline.cpython-312.pyc ADDED
Binary file (9.05 kB). View file
 
src/rag/rag_pipeline.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch
3
+ from typing import Any, List, Tuple, Dict, Optional
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ METADATA_TEXT_FIELDS = ["generated_mongodb_query", "mongodb_query", "code", "sql", "SQL", "question", "text", "schema"]
8
+
9
+ class RAGPipeline:
10
+ def __init__(
11
+ self,
12
+ retriever: Any,
13
+ generator: Optional[Any] = None,
14
+ model: Optional[Any] = None,
15
+ tokenizer: Optional[Any] = None,
16
+ config: Any = None
17
+ ):
18
+ """
19
+ Args:
20
+ retriever: An instance of IndexManager or HybridRetriever that has a `.search(query, k)` method.
21
+ generator: An instance of GenerationPipeline (has `.generate(prompt)`).
22
+ If provided, `model` and `tokenizer` are ignored.
23
+ model: HuggingFace model (used if generator is None).
24
+ tokenizer: HuggingFace tokenizer (used if generator is None).
25
+ config: Generation configuration object.
26
+ """
27
+ self.retriever = retriever
28
+ self.generator = generator
29
+ self.model = model
30
+ self.tokenizer = tokenizer
31
+ self.config = config
32
+
33
+ if self.generator is None and (self.model is None or self.tokenizer is None):
34
+ raise ValueError("Either `generator` or both `model` and `tokenizer` must be provided.")
35
+
36
+ def retrieve_context(self, query: str, top_k: int = 3) -> List[Dict[str, Any]]:
37
+ """
38
+ Retrieves relevant context using the hybrid retriever.
39
+ Returns a list of metadata dictionaries.
40
+ """
41
+ try:
42
+ # The new HybridRetriever/IndexManager returns List[Dict] or List[Tuple[Dict, float]]
43
+ results = self.retriever.search(query, k=top_k)
44
+
45
+ # Handle different return formats gracefully
46
+ if results and isinstance(results[0], tuple):
47
+ return [metadata for metadata, score in results]
48
+ elif results and isinstance(results[0], dict):
49
+ return results
50
+ else:
51
+ return []
52
+ except Exception as e:
53
+ logger.error(f"Error during context retrieval: {e}")
54
+ return []
55
+
56
+ @staticmethod
57
+ def _metadata_to_text(metadata: Any) -> str:
58
+ """Extracts the most relevant text field from a metadata dictionary."""
59
+ if not isinstance(metadata, dict):
60
+ return str(metadata)
61
+
62
+ for field in METADATA_TEXT_FIELDS:
63
+ if metadata.get(field):
64
+ return str(metadata[field])
65
+
66
+ # Fallback: concatenate all string values
67
+ return " | ".join([f"{k}: {v}" for k, v in metadata.items() if isinstance(v, str)])
68
+
69
+ def build_prompt(self, query: str, context_items: List[Dict[str, Any]]) -> str:
70
+ context_str = ""
71
+ for i, item in enumerate(context_items):
72
+ mongodb_query = (
73
+ item.get('generated_mongodb_query', '')
74
+ or item.get('mongodb_query', '')
75
+ )
76
+ question = item.get('question', '')
77
+
78
+ if isinstance(mongodb_query, list) and len(mongodb_query) > 0:
79
+ mongodb_query = mongodb_query[0]
80
+
81
+ if mongodb_query:
82
+ context_str += f"--- Example {i + 1} ---\n"
83
+ if question:
84
+ context_str += f"Question: {question}\n"
85
+ context_str += f"MongoDB Query: {mongodb_query}\n\n"
86
+
87
+ prompt = (
88
+ "You are an expert MongoDB query generator. Given a question, output ONLY the MongoDB query.\n"
89
+ "Do not include explanations, apologies, or extra text.\n\n"
90
+ f"Context Examples:\n{context_str}\n"
91
+ f"### Task: Generate MongoDB Query (MQL)\n"
92
+ f"### Question:\n{query}\n\n"
93
+ f"### MongoDB Query:\ndb."
94
+ )
95
+ return prompt
96
+
97
+ def _build_generation_kwargs(self) -> dict:
98
+ eos_id = self.tokenizer.eos_token_id if self.tokenizer else None
99
+ if isinstance(eos_id, torch.Tensor):
100
+ eos_id = eos_id.item()
101
+
102
+ kwargs = {
103
+ "max_new_tokens": 128,
104
+ "temperature": 0.2,
105
+ "top_p": 0.95,
106
+ "top_k": 50,
107
+ "do_sample": True,
108
+ "pad_token_id": eos_id,
109
+ "eos_token_id": eos_id,
110
+ "repetition_penalty": 1.2,
111
+ "no_repeat_ngram_size": 3,
112
+ }
113
+ if self.config:
114
+ kwargs["max_new_tokens"] = min(getattr(self.config, "max_length", 256), 128)
115
+ kwargs["temperature"] = getattr(self.config, "temperature", 0.2)
116
+ kwargs["top_p"] = getattr(self.config, "top_p", 0.95)
117
+ kwargs["top_k"] = getattr(self.config, "top_k", 50)
118
+ return kwargs
119
+
120
+ @staticmethod
121
+ def _clean_mongo_output(raw_output: str) -> str:
122
+ stripped = raw_output.strip()
123
+ if stripped.lower().startswith("db."):
124
+ restored = stripped
125
+ else:
126
+ restored = "db." + stripped
127
+
128
+ cleaned = restored.replace("```javascript", "").replace("```json", "").replace("```", "").strip()
129
+ stop_markers = ["--->", "\n\n", "\nUser:", "\nQuestion:", "```", "###", "Human:"]
130
+ for marker in stop_markers:
131
+ idx = cleaned.find(marker)
132
+ if idx != -1:
133
+ cleaned = cleaned[:idx]
134
+ lines = cleaned.split('\n')
135
+ for line in lines:
136
+ if line.strip().startswith("db."):
137
+ return line.strip().rstrip(';')
138
+ if "db." in cleaned:
139
+ idx = cleaned.find("db.")
140
+ return cleaned[idx:].split('\n')[0].strip().rstrip(';')
141
+ return cleaned.strip()
142
+
143
+ # Update generate_with_rag to clean the output before returning:
144
+ def generate_with_rag(self, query: str, top_k: int = 3) -> Tuple[str, List[Dict[str, Any]]]:
145
+ context_metadata = self.retrieve_context(query, top_k=top_k)
146
+ prompt = self.build_prompt(query, context_metadata)
147
+
148
+ if self.generator:
149
+ generated_text = self.generator.generate(prompt)
150
+ else:
151
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
152
+ gen_kwargs = self._build_generation_kwargs()
153
+ with torch.no_grad():
154
+ outputs = self.model.generate(**inputs, **gen_kwargs)
155
+ generated_text = self.tokenizer.decode(
156
+ outputs[0][inputs["input_ids"].shape[1]:],
157
+ skip_special_tokens=True
158
+ ).strip()
159
+
160
+ generated_text = self._clean_mongo_output(generated_text)
161
+ return generated_text, context_metadata
162
+
163
+ def generate_program(self, prompt: str, top_k: int = 3) -> str:
164
+ """Wrapper for simple program generation using RAG."""
165
+ generated_text, _ = self.generate_with_rag(query=prompt, top_k=top_k)
166
+ return generated_text
src/vectorDB/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from vectorDB.embedder import HFEmbedder
2
+ from vectorDB.semantic_index import SemanticIndex
3
+ from vectorDB.ast_index import ASTIndex
4
+ from vectorDB.hybrid_retriever import HybridRetriever
5
+ from vectorDB.index_manager import IndexManager
6
+
7
+ __all__ = [
8
+ "HFEmbedder",
9
+ "SemanticIndex",
10
+ "ASTIndex",
11
+ "HybridRetriever",
12
+ "IndexManager"
13
+ ]
src/vectorDB/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (496 Bytes). View file
 
src/vectorDB/__pycache__/ast_index.cpython-312.pyc ADDED
Binary file (10.3 kB). View file
 
src/vectorDB/__pycache__/embedder.cpython-312.pyc ADDED
Binary file (3.7 kB). View file
 
src/vectorDB/__pycache__/hybrid_retriever.cpython-312.pyc ADDED
Binary file (4.58 kB). View file
 
src/vectorDB/__pycache__/index_manager.cpython-312.pyc ADDED
Binary file (7.58 kB). View file
 
src/vectorDB/__pycache__/semantic_index.cpython-312.pyc ADDED
Binary file (7.48 kB). View file