Rushabh147 Claude Sonnet 4.6 commited on
Commit
b89e6d6
·
0 Parent(s):

Initial deploy to HF Spaces (clean history, LFS for all binaries)

Browse files
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.index filter=lfs diff=lfs merge=lfs -text
2
+ *.parquet filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ data/raw/
5
+ data/processed/
6
+ data/eda/
7
+ data/codet5p-ft/
8
+ # data/index/ is excluded globally but force-included below for HF Spaces deploy
9
+ data/index/
10
+ !data/index/
11
+ !data/index/code.index
12
+ !data/index/corpus.parquet
13
+ !data/index/embed_model.txt
14
+ *.parquet
15
+ !data/index/*.parquet
16
+ *.index
17
+ !data/index/*.index
18
+ .ipynb_checkpoints/
19
+ .claude/settings.local.json
CLAUDE.md ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code Generation Assistant — Claude Context
2
+
3
+ RAG-based Python code generation assistant using CodeSearchNet.
4
+ Compares baseline, RAG, fine-tuned, and agentic approaches.
5
+
6
+ ## Environment
7
+
8
+ - macOS, no NVIDIA GPU. All local runs must stay small and CPU-friendly.
9
+ - Python 3.9; virtual environment at `.venv/` (never touch system Python).
10
+ - Always activate with `.venv/bin/python` (or `.venv/bin/<tool>`); don't use bare `python`.
11
+
12
+ ## Pipeline run order
13
+
14
+ ```bash
15
+ # 1. Install (one-time)
16
+ python3 -m venv .venv
17
+ .venv/bin/pip install -r requirements.txt
18
+
19
+ # 2. Smoke test (synthetic data, fast)
20
+ # Set use_sample: true in config.yaml first.
21
+ .venv/bin/python scripts/01_prepare_data.py
22
+ .venv/bin/python scripts/02_run_eda.py
23
+
24
+ # 3. Real subset (set use_sample: false, keep max_rows: 5000 in config.yaml)
25
+ .venv/bin/python scripts/01_prepare_data.py # downloads CodeSearchNet ~457k rows, caps at max_rows
26
+ .venv/bin/python scripts/03_build_index.py # downloads all-MiniLM-L6-v2, embeds corpus, writes FAISS index
27
+
28
+ # 4. Launch UI (downloads Qwen2.5-Coder-1.5B-Instruct ~3 GB on first run)
29
+ .venv/bin/python app/gradio_app.py # serves at http://127.0.0.1:7860
30
+ ```
31
+
32
+ ## config.yaml key settings
33
+
34
+ | Key | Default | Notes |
35
+ |-----|---------|-------|
36
+ | `data.use_sample` | `false` | Set `true` for offline/CI smoke tests |
37
+ | `data.sample_size` | 200 | Rows generated when `use_sample: true` |
38
+ | `data.max_rows` | 5000 | Caps real HF data for local runs (0 = no cap) |
39
+ | `models.embed_model` | `sentence-transformers/all-MiniLM-L6-v2` | Retrieval embedder |
40
+ | `models.gen_model` | `Qwen/Qwen2.5-Coder-1.5B-Instruct` | Code LLM |
41
+ | `models.top_k` | 3 | Retrieved examples per query |
42
+
43
+ ## What each script does
44
+
45
+ - `scripts/01_prepare_data.py` — load raw dataset (HF or synthetic) → clean → train/val/test split → `data/processed/`
46
+ - `scripts/02_run_eda.py` — compute stats + plots from training split → `data/eda/`
47
+ - `scripts/03_build_index.py` — embed training corpus with MiniLM → FAISS index → `data/index/`
48
+ - `scripts/04_run_eval.py` — retrieval metrics (recall@k, MRR) + pass@1 baseline vs RAG
49
+ - `scripts/05_finetune.py` — fine-tune CodeT5+ on docstring→code (Colab only; too slow locally)
50
+
51
+ ## Source layout
52
+
53
+ ```
54
+ src/config.py loads config.yaml into a SimpleNamespace
55
+ src/data/load.py HF dataset fetch + max_rows cap
56
+ src/data/clean.py filtering funnel (word count, tokens, dedup, etc.)
57
+ src/data/make_sample.py synthetic 200-row sample for smoke tests
58
+ src/eda/analyze.py stats + matplotlib/seaborn plots
59
+ src/rag/embedder.py CodeIndex: SentenceTransformer + FAISS (build/save/load/retrieve)
60
+ src/rag/generator.py CodeAssistant: Qwen LLM wrapper, baseline + RAG prompt builders
61
+ src/eval/ functional_eval.py, retrieval_eval.py, sandbox.py
62
+ src/agent/repair_loop.py generate → run → self-repair loop
63
+ src/finetune/train_codet5.py (Colab only)
64
+ app/gradio_app.py Gradio chat UI (main local + HF Spaces deploy target)
65
+ app/api.py FastAPI REST service (uvicorn)
66
+ app/streamlit_app.py Streamlit UI
67
+ ```
68
+
69
+ ## HuggingFace downloads (one-time, cached in ~/.cache/huggingface/)
70
+
71
+ | Asset | Size | When |
72
+ |-------|------|------|
73
+ | `code_search_net` dataset | ~2 GB | `01_prepare_data.py` with `use_sample: false` |
74
+ | `sentence-transformers/all-MiniLM-L6-v2` | ~90 MB | `03_build_index.py` (first run) |
75
+ | `Qwen/Qwen2.5-Coder-1.5B-Instruct` | ~3 GB | `app/gradio_app.py` (first run) |
76
+
77
+ ## Data directories (excluded from git)
78
+
79
+ ```
80
+ data/raw/ raw parquet from HF
81
+ data/processed/ train/val/test.parquet + cleaning_funnel.csv
82
+ data/eda/ PNG plots + eda_stats.json
83
+ data/index/ code.index (FAISS) + corpus.parquet + embed_model.txt
84
+ ```
85
+
86
+ ## Deployment options
87
+
88
+ ```bash
89
+ # Gradio (local or push to HF Spaces as app.py)
90
+ .venv/bin/python app/gradio_app.py
91
+
92
+ # FastAPI
93
+ .venv/bin/uvicorn app.api:app --host 0.0.0.0 --port 8000
94
+
95
+ # Streamlit
96
+ .venv/bin/streamlit run app/streamlit_app.py
97
+
98
+ # Docker
99
+ docker build -t cga . && docker run -p 8000:8000 cga
100
+ ```
101
+
102
+ ## Full-dataset training / eval
103
+
104
+ Do NOT run locally — use Colab:
105
+ - `scripts/04_run_eval.py` on full CodeSearchNet is slow; fine for small subsets.
106
+ - `scripts/05_finetune.py` (CodeT5+) requires a GPU.
107
+ - The notebook (`notebooks/`) is for Colab EDA, training, and reporting eval numbers.
108
+
109
+ ## Known warnings (non-fatal)
110
+
111
+ - `urllib3 NotOpenSSLWarning` — macOS LibreSSL vs OpenSSL; safe to ignore.
112
+ - `Some parameters are on the meta device` — CPU offload of Qwen weights; expected on macOS without GPU.
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Container for the FastAPI service. Build: docker build -t code-gen-assistant .
2
+ # Run: docker run -p 8000:8000 code-gen-assistant
3
+ # For generated-code execution safety, run with --network none if you don't need
4
+ # the model to download at runtime (bake the index/model into the image instead).
5
+ FROM python:3.11-slim
6
+
7
+ WORKDIR /app
8
+
9
+ # System deps for tokenizers / faiss wheels.
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ build-essential git && rm -rf /var/lib/apt/lists/*
12
+
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ COPY . .
17
+
18
+ EXPOSE 8000
19
+ CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "8000"]
README.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Code Generation Assistant
3
+ sdk: gradio
4
+ app_file: app/gradio_app.py
5
+ pinned: false
6
+ ---
7
+
8
+ # Code Generation Assistant
9
+
10
+ Generate Python code from natural-language descriptions, grounded in
11
+ **CodeSearchNet** via retrieval (RAG), with functional evaluation and a
12
+ deployable chat interface.
13
+
14
+ ## Approaches compared
15
+ 1. **Baseline** - frozen code LLM, zero/few-shot
16
+ 2. **RAG** - retrieve similar CodeSearchNet examples, condition the LLM
17
+ 3. **Fine-tuned** - CodeT5+ trained on `docstring -> code`
18
+ 4. **Agentic** - generate -> run -> read error -> repair loop
19
+
20
+ ## Evaluation
21
+ - **CodeBLEU** (similarity to reference) - in the notebook
22
+ - **pass@k** on HumanEval / MBPP (functional correctness) - `src/eval/functional_eval.py`
23
+ - **recall@k / MRR** (retrieval quality) - `src/eval/retrieval_eval.py`
24
+
25
+ > CodeSearchNet ships no unit tests, so pass@k is measured on HumanEval/MBPP
26
+ > (which do), while CodeSearchNet powers retrieval + the similarity metric.
27
+
28
+ ## Pipeline (run in order)
29
+ ```bash
30
+ pip install -r requirements.txt
31
+ python scripts/01_prepare_data.py # load -> clean -> split
32
+ python scripts/02_run_eda.py # stats + plots
33
+ python scripts/03_build_index.py # embed corpus -> FAISS index (persisted)
34
+ python scripts/04_run_eval.py # retrieval + functional pass@1 (baseline vs RAG)
35
+ python scripts/05_finetune.py # (optional) fine-tune CodeT5+
36
+ ```
37
+
38
+ ## Project layout
39
+ ```
40
+ config.yaml # single source of truth (models, paths, thresholds)
41
+ src/
42
+ data/ load.py clean.py make_sample.py # Phase 1
43
+ eda/ analyze.py # Phase 1
44
+ rag/ embedder.py generator.py # Phase 3 + 5 (CodeAssistant)
45
+ eval/ functional_eval.py retrieval_eval.py sandbox.py # Phase 2
46
+ agent/ repair_loop.py # Phase 6
47
+ finetune/ train_codet5.py # Phase 4
48
+ app/
49
+ api.py # FastAPI REST service
50
+ gradio_app.py # Gradio chat UI (Hugging Face Spaces)
51
+ streamlit_app.py # Streamlit chat UI
52
+ scripts/ # numbered phase entrypoints
53
+ notebooks/ # experimentation notebook
54
+ Dockerfile # container for the API
55
+ ```
56
+
57
+ ## Is the notebook the right vehicle?
58
+ The notebook is for **experimentation, EDA, and reporting eval numbers** - keep
59
+ it for your capstone appendix. For anything you *deploy*, the logic belongs in
60
+ the `src/` package (importable, testable, version-controlled). The apps in
61
+ `app/` all import the same `CodeAssistant`, so there is one implementation, not
62
+ three copies. Workflow: prototype in the notebook -> harden into `src/` ->
63
+ push to GitHub -> deploy an app.
64
+
65
+ ## Deployment interfaces
66
+
67
+ **1. Gradio on Hugging Face Spaces (easiest).** Copy `app/gradio_app.py` to a new
68
+ Gradio Space as `app.py`, add `requirements.txt`, pick a GPU tier. Public chat UI
69
+ in minutes, no servers to manage.
70
+
71
+ **2. FastAPI (production / integration).**
72
+ ```bash
73
+ uvicorn app.api:app --host 0.0.0.0 --port 8000 # docs at /docs
74
+ curl -X POST localhost:8000/generate -H 'Content-Type: application/json' \
75
+ -d '{"intent": "function to check if a number is prime", "mode": "rag"}'
76
+ ```
77
+ Use this when another system (an IDE plugin, a CI bot, a web frontend) needs to
78
+ call the assistant programmatically. Endpoints: `GET /health`, `POST /generate`
79
+ (supports `mode`, `repair`, `return_sources`).
80
+
81
+ **3. Streamlit.** `streamlit run app/streamlit_app.py` - deploys free to
82
+ Streamlit Community Cloud from a GitHub repo.
83
+
84
+ **4. Docker (any cloud).** `docker build -t cga . && docker run -p 8000:8000 cga`.
85
+
86
+ ## Security note
87
+ Generated code is executed (for pass@k and the repair loop) via a subprocess +
88
+ timeout in `src/eval/sandbox.py`. That guards against hangs but is **not** a
89
+ security sandbox. For public deployment, run execution inside a container with
90
+ `--network none` and dropped privileges, or disable the repair feature.
app/api.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI service - the production interface.
2
+
3
+ Exposes the CodeAssistant over REST so any client (web app, CI bot, IDE plugin)
4
+ can call it. Run locally:
5
+ uvicorn app.api:app --host 0.0.0.0 --port 8000
6
+ Then POST to /generate. Interactive docs at /docs.
7
+
8
+ The model loads once at startup (lifespan) and is reused across requests.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import sys
14
+ from contextlib import asynccontextmanager
15
+ from pathlib import Path
16
+
17
+ from fastapi import FastAPI
18
+ from pydantic import BaseModel, Field
19
+
20
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
21
+
22
+ # Allow tests / CI to skip the heavy model load.
23
+ _STATE: dict = {"assistant": None}
24
+
25
+
26
+ def get_assistant():
27
+ return _STATE["assistant"]
28
+
29
+
30
+ @asynccontextmanager
31
+ async def lifespan(app: FastAPI):
32
+ if os.environ.get("CGA_SKIP_MODEL") != "1":
33
+ from src.rag.generator import CodeAssistant
34
+ _STATE["assistant"] = CodeAssistant.from_config(with_index=True)
35
+ yield
36
+ _STATE["assistant"] = None
37
+
38
+
39
+ app = FastAPI(title="Code Generation Assistant", version="1.0", lifespan=lifespan)
40
+
41
+
42
+ class GenerateRequest(BaseModel):
43
+ intent: str = Field(..., description="Natural-language description of the code")
44
+ mode: str = Field("rag", description="'rag' or 'baseline'")
45
+ repair: bool = Field(False, description="Run the agentic repair loop")
46
+ return_sources: bool = Field(True)
47
+
48
+
49
+ class GenerateResponse(BaseModel):
50
+ code: str
51
+ mode: str
52
+ sources: list = []
53
+
54
+
55
+ @app.get("/health")
56
+ def health():
57
+ return {"status": "ok", "model_loaded": _STATE["assistant"] is not None}
58
+
59
+
60
+ @app.post("/generate", response_model=GenerateResponse)
61
+ def generate(req: GenerateRequest):
62
+ assistant = get_assistant()
63
+ if assistant is None:
64
+ return GenerateResponse(code="# model not loaded", mode=req.mode)
65
+
66
+ if req.repair:
67
+ from src.agent.repair_loop import make_repair_generator, repair_loop
68
+ gen_fn = make_repair_generator(assistant)
69
+ trace = repair_loop(req.intent, gen_fn,
70
+ check_program_fn=lambda c: c, max_iters=3)
71
+ return GenerateResponse(code=trace.final_code, mode=f"{req.mode}+repair")
72
+
73
+ result = assistant.generate(req.intent, mode=req.mode,
74
+ return_sources=req.return_sources)
75
+ if isinstance(result, tuple):
76
+ code, sources = result
77
+ return GenerateResponse(code=code, mode=req.mode, sources=sources)
78
+ return GenerateResponse(code=result, mode=req.mode)
app/gradio_app.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio chat UI - the easiest deploy target (Hugging Face Spaces).
2
+
3
+ To deploy on HF Spaces:
4
+ 1. Create a new Space (SDK: Gradio).
5
+ 2. Push this file as `app.py` at the repo root, plus requirements.txt.
6
+ 3. Pick a GPU tier if you want the 7B model; the 1.5B runs on CPU (slowly).
7
+
8
+ Locally: python app/gradio_app.py
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import gradio as gr
16
+
17
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
18
+ from src.rag.generator import CodeAssistant # noqa: E402
19
+
20
+ assistant = CodeAssistant.from_config(with_index=True)
21
+
22
+
23
+ def respond(message, history, mode, use_repair):
24
+ if use_repair:
25
+ from src.agent.repair_loop import make_repair_generator, repair_loop
26
+ gen_fn = make_repair_generator(assistant)
27
+ trace = repair_loop(message, gen_fn, check_program_fn=lambda c: c, max_iters=3)
28
+ code, note = trace.final_code, f"(repair: {'passed' if trace.success else 'best effort'} in {trace.iterations} iters)"
29
+ return f"```python\n{code}\n```\n\n_{note}_"
30
+
31
+ result = assistant.generate(message, mode=mode, return_sources=True)
32
+ if isinstance(result, tuple):
33
+ code, sources = result
34
+ src_md = "\n".join(f"- ({s['score']:.2f}) {s['docstring']}" for s in sources)
35
+ return f"```python\n{code}\n```\n\n**Retrieved examples:**\n{src_md}"
36
+ return f"```python\n{result}\n```"
37
+
38
+
39
+ with gr.Blocks(title="Code Generation Assistant") as demo:
40
+ gr.Markdown("# Code Generation Assistant\nDescribe what you want; it writes Python, grounded in CodeSearchNet.")
41
+ with gr.Row():
42
+ mode = gr.Radio(["rag", "baseline"], value="rag", label="Mode")
43
+ use_repair = gr.Checkbox(label="Agentic repair (run + self-fix)", value=False)
44
+ gr.ChatInterface(
45
+ fn=respond,
46
+ additional_inputs=[mode, use_repair],
47
+ examples=[["Write a function to check if a number is prime."],
48
+ ["Parse a CSV file and return a list of dicts."]],
49
+ )
50
+
51
+ if __name__ == "__main__":
52
+ demo.launch(server_name="0.0.0.0")
app/streamlit_app.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamlit chat UI - alternative front-end (Streamlit Community Cloud).
2
+
3
+ Run: streamlit run app/streamlit_app.py
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import streamlit as st
11
+
12
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
13
+
14
+
15
+ @st.cache_resource
16
+ def get_assistant():
17
+ from src.rag.generator import CodeAssistant
18
+ return CodeAssistant.from_config(with_index=True)
19
+
20
+
21
+ st.set_page_config(page_title="Code Generation Assistant", page_icon="</>")
22
+ st.title("Code Generation Assistant")
23
+
24
+ with st.sidebar:
25
+ mode = st.radio("Mode", ["rag", "baseline"], index=0)
26
+ use_repair = st.checkbox("Agentic repair (run + self-fix)", value=False)
27
+
28
+ assistant = get_assistant()
29
+
30
+ if "messages" not in st.session_state:
31
+ st.session_state.messages = []
32
+
33
+ for m in st.session_state.messages:
34
+ with st.chat_message(m["role"]):
35
+ st.markdown(m["content"])
36
+
37
+ if prompt := st.chat_input("Describe the function you want..."):
38
+ st.session_state.messages.append({"role": "user", "content": prompt})
39
+ with st.chat_message("user"):
40
+ st.markdown(prompt)
41
+
42
+ with st.chat_message("assistant"), st.spinner("Generating..."):
43
+ if use_repair:
44
+ from src.agent.repair_loop import make_repair_generator, repair_loop
45
+ gen_fn = make_repair_generator(assistant)
46
+ trace = repair_loop(prompt, gen_fn, check_program_fn=lambda c: c, max_iters=3)
47
+ code = trace.final_code
48
+ st.code(code, language="python")
49
+ st.caption(f"Repair: {'passed' if trace.success else 'best effort'} in {trace.iterations} iters")
50
+ answer = code
51
+ else:
52
+ result = assistant.generate(prompt, mode=mode, return_sources=True)
53
+ if isinstance(result, tuple):
54
+ code, sources = result
55
+ st.code(code, language="python")
56
+ with st.expander("Retrieved examples"):
57
+ for s in sources:
58
+ st.write(f"({s['score']:.2f}) {s['docstring']}")
59
+ else:
60
+ code = result
61
+ st.code(code, language="python")
62
+ answer = code
63
+ st.session_state.messages.append({"role": "assistant", "content": f"```python\n{answer}\n```"})
config.yaml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Code Generation Assistant - Project Configuration
3
+ # ============================================================
4
+ # Central config so every phase reads the same settings.
5
+ # Change `use_sample: true` to test the pipeline on synthetic
6
+ # data; set false to pull the real CodeSearchNet from HuggingFace.
7
+
8
+ data:
9
+ # HuggingFace dataset id. The canonical id is "code_search_net".
10
+ # If that fails to load, try the community mirror:
11
+ # "code-search-net/code_search_net"
12
+ hf_dataset_id: "code_search_net"
13
+ # Languages to include. Start with python only; add "java" etc. later.
14
+ languages:
15
+ - python
16
+ # When true, the pipeline uses a small synthetic sample instead of
17
+ # downloading the real dataset (useful for offline testing / CI).
18
+ use_sample: false
19
+ sample_size: 200 # rows generated when use_sample is true
20
+ max_rows: 5000 # cap on real HF data (0 = no cap); keeps local runs fast
21
+
22
+ cleaning:
23
+ min_doc_words: 3 # drop docstrings shorter than this (words)
24
+ max_doc_words: 120 # drop overly long docstrings (likely noise)
25
+ min_code_chars: 20 # drop trivially short functions
26
+ max_code_tokens: 512 # drop functions longer than this (token budget)
27
+ drop_exact_duplicates: true
28
+ drop_non_ascii_docs: true # drop docstrings that are mostly non-ASCII
29
+ # Substrings that flag low-quality / autogenerated docstrings.
30
+ doc_blocklist:
31
+ - "todo"
32
+ - "fixme"
33
+ - "auto-generated"
34
+ - "autogenerated"
35
+ - "do not edit"
36
+
37
+ split:
38
+ train: 0.8
39
+ val: 0.1
40
+ test: 0.1
41
+ seed: 42
42
+
43
+ models:
44
+ embed_model: "sentence-transformers/all-MiniLM-L6-v2"
45
+ gen_model: "Qwen/Qwen2.5-Coder-1.5B-Instruct"
46
+ top_k: 3
47
+
48
+ paths:
49
+ data_dir: "data"
50
+ raw_dir: "data/raw"
51
+ processed_dir: "data/processed"
52
+ eda_dir: "data/eda"
53
+ index_dir: "data/index"
data/index/code.index ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:44f5653ddb9f84a940c75aa9c483df8a8193252695529e011d49a1821d73fa7a
3
+ size 5343789
data/index/corpus.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dfbd0e7044327f45bdb99a3a4d0ac0aa60b635f88cb709eaa5b8f9ba5d39f3e2
3
+ size 1783627
data/index/embed_model.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ sentence-transformers/all-MiniLM-L6-v2
notebooks/Code_Generation_Assistant.ipynb ADDED
@@ -0,0 +1,652 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "43fed051",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Code Generation Assistant \n",
9
+ "\n",
10
+ "**Generate Python code from natural-language descriptions, grounded in CodeSearchNet.**\n",
11
+ "\n",
12
+ "This notebook runs the core vertical slice of the capstone top-to-bottom:\n",
13
+ "\n",
14
+ "1. **Phase 1** - load + clean CodeSearchNet, EDA\n",
15
+ "2. **Phase 3** - embed the corpus + build a FAISS retrieval index\n",
16
+ "3. **Phase 5** - RAG: retrieve similar examples and condition a code LLM\n",
17
+ "4. **Eval** - baseline (no retrieval) vs RAG, scored with CodeBLEU\n",
18
+ "5. **Interactive** - ask it to write code\n",
19
+ "6. **Phase 4 (optional)** - fine-tune CodeT5+ on docstring->code\n",
20
+ "\n",
21
+ "> CodeSearchNet was built for code *search*, so it ships `(docstring, code)` pairs\n",
22
+ "> and **no unit tests**. We treat the docstring summary as the intent and the\n",
23
+ "> function body as the target. Because it is natively a retrieval corpus, RAG is\n",
24
+ "> the most natural architecture here.\n",
25
+ "\n",
26
+ "**Runtime:** set `Runtime -> Change runtime type -> T4 GPU`. No API key required -\n",
27
+ "generation uses a small local model (`Qwen2.5-Coder-1.5B-Instruct`)."
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "markdown",
32
+ "id": "06dd65ca",
33
+ "metadata": {},
34
+ "source": [
35
+ "## 0. Setup\n",
36
+ "\n",
37
+ "Installs everything. `codebleu` is optional (it builds tree-sitter parsers); if it\n",
38
+ "fails the eval falls back to a token-overlap metric so the notebook still runs."
39
+ ]
40
+ },
41
+ {
42
+ "cell_type": "code",
43
+ "execution_count": null,
44
+ "id": "2733fd6d",
45
+ "metadata": {},
46
+ "outputs": [],
47
+ "source": [
48
+ "!pip -q install datasets transformers accelerate sentence-transformers faiss-cpu pandas matplotlib seaborn\n",
49
+ "# codebleu needs a tree-sitter parser to actually run; install it too (optional - has a fallback)\n",
50
+ "!pip -q install codebleu tree-sitter tree-sitter-python || echo \"codebleu/parser install failed - will use fallback metric\""
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "id": "1125e98a",
57
+ "metadata": {},
58
+ "outputs": [],
59
+ "source": [
60
+ "import torch\n",
61
+ "print(\"CUDA available:\", torch.cuda.is_available())\n",
62
+ "print(\"Device:\", torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU (generation will be slow)\")"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "markdown",
67
+ "id": "88a40082",
68
+ "metadata": {},
69
+ "source": [
70
+ "## 1. Config\n",
71
+ "\n",
72
+ "One place for every knob. `MAX_ROWS` keeps the Colab run fast - raise it (or set to\n",
73
+ "`None`) for a fuller run. `python` only to start; depth over breadth."
74
+ ]
75
+ },
76
+ {
77
+ "cell_type": "code",
78
+ "execution_count": null,
79
+ "id": "894bfb5f",
80
+ "metadata": {},
81
+ "outputs": [],
82
+ "source": [
83
+ "from dataclasses import dataclass, field\n",
84
+ "from typing import Tuple\n",
85
+ "\n",
86
+ "@dataclass\n",
87
+ "class Config:\n",
88
+ " # data\n",
89
+ " candidate_dataset_ids: Tuple[str, ...] = (\n",
90
+ " \"code-search-net/code_search_net\", # parquet mirror (most reliable)\n",
91
+ " \"code_search_net\", # canonical (may need older datasets)\n",
92
+ " )\n",
93
+ " language: str = \"python\"\n",
94
+ " max_rows: int = 8000 # subset for speed; set None for full split\n",
95
+ " # cleaning\n",
96
+ " min_doc_words: int = 3\n",
97
+ " max_doc_words: int = 120\n",
98
+ " min_code_chars: int = 20\n",
99
+ " max_code_tokens: int = 400\n",
100
+ " doc_blocklist: Tuple[str, ...] = (\"todo\", \"fixme\", \"auto-generated\",\n",
101
+ " \"autogenerated\", \"do not edit\")\n",
102
+ " # split\n",
103
+ " seed: int = 42\n",
104
+ " train: float = 0.8\n",
105
+ " val: float = 0.1\n",
106
+ " # models\n",
107
+ " embed_model: str = \"sentence-transformers/all-MiniLM-L6-v2\"\n",
108
+ " gen_model: str = \"Qwen/Qwen2.5-Coder-1.5B-Instruct\"\n",
109
+ " top_k: int = 3\n",
110
+ "\n",
111
+ "CFG = Config()\n",
112
+ "CFG"
113
+ ]
114
+ },
115
+ {
116
+ "cell_type": "markdown",
117
+ "id": "4382632d",
118
+ "metadata": {},
119
+ "source": [
120
+ "## 2. Phase 1a - Load CodeSearchNet\n",
121
+ "\n",
122
+ "Tries the parquet mirror first, then the canonical id. If both fail on your\n",
123
+ "`datasets` version, run `!pip install \"datasets<3\"` and re-run, or download the\n",
124
+ "raw release from the CodeSearchNet GitHub repo."
125
+ ]
126
+ },
127
+ {
128
+ "cell_type": "code",
129
+ "execution_count": null,
130
+ "id": "158ab53b",
131
+ "metadata": {},
132
+ "outputs": [],
133
+ "source": [
134
+ "from datasets import load_dataset\n",
135
+ "import pandas as pd\n",
136
+ "\n",
137
+ "USE_COLS = {\n",
138
+ " \"func_documentation_string\": \"docstring\",\n",
139
+ " \"func_code_string\": \"code\",\n",
140
+ " \"language\": \"language\",\n",
141
+ " \"repository_name\": \"repo\",\n",
142
+ " \"func_code_url\": \"url\",\n",
143
+ "}\n",
144
+ "\n",
145
+ "def load_codesearchnet(cfg):\n",
146
+ " last_err = None\n",
147
+ " for ds_id in cfg.candidate_dataset_ids:\n",
148
+ " try:\n",
149
+ " print(f\"[load] trying '{ds_id}' ({cfg.language}) ...\")\n",
150
+ " ds = load_dataset(ds_id, cfg.language, split=\"train\", trust_remote_code=True)\n",
151
+ " if cfg.max_rows:\n",
152
+ " ds = ds.select(range(min(cfg.max_rows, len(ds))))\n",
153
+ " df = ds.to_pandas()\n",
154
+ " keep = [c for c in USE_COLS if c in df.columns]\n",
155
+ " df = df[keep].rename(columns=USE_COLS)\n",
156
+ " for col in USE_COLS.values():\n",
157
+ " if col not in df.columns:\n",
158
+ " df[col] = \"\"\n",
159
+ " print(f\"[load] OK - {len(df):,} rows from '{ds_id}'\")\n",
160
+ " return df\n",
161
+ " except Exception as e:\n",
162
+ " print(f\"[load] failed: {e}\")\n",
163
+ " last_err = e\n",
164
+ " raise RuntimeError(f\"All dataset ids failed. Last error: {last_err}\")\n",
165
+ "\n",
166
+ "raw = load_codesearchnet(CFG)\n",
167
+ "raw.head(2)"
168
+ ]
169
+ },
170
+ {
171
+ "cell_type": "markdown",
172
+ "id": "32dba780",
173
+ "metadata": {},
174
+ "source": [
175
+ "## 3. Phase 1b - Clean & filter\n",
176
+ "\n",
177
+ "CodeSearchNet is noisy. We keep only the **summary first line** of each docstring\n",
178
+ "as the intent (the rest is usually `:param:`/`:return:` boilerplate), then apply\n",
179
+ "quality filters and dedup. The **funnel** logs how many rows each filter removes -\n",
180
+ "keep it for your write-up."
181
+ ]
182
+ },
183
+ {
184
+ "cell_type": "code",
185
+ "execution_count": null,
186
+ "id": "2aa0af5e",
187
+ "metadata": {},
188
+ "outputs": [],
189
+ "source": [
190
+ "import re\n",
191
+ "\n",
192
+ "WORD_RE = re.compile(r\"\\b\\w+\\b\")\n",
193
+ "\n",
194
+ "def first_line(t):\n",
195
+ " return t.strip().split(\"\\n\")[0].strip() if isinstance(t, str) else \"\"\n",
196
+ "\n",
197
+ "def word_count(t):\n",
198
+ " return len(WORD_RE.findall(t)) if isinstance(t, str) else 0\n",
199
+ "\n",
200
+ "def ascii_ratio(t):\n",
201
+ " if not t:\n",
202
+ " return 1.0\n",
203
+ " return sum(1 for ch in t if ord(ch) < 128) / len(t)\n",
204
+ "\n",
205
+ "def approx_tokens(c):\n",
206
+ " return len(re.findall(r\"\\w+|[^\\s\\w]\", c)) if isinstance(c, str) else 0\n",
207
+ "\n",
208
+ "def clean(df, cfg):\n",
209
+ " funnel = [(\"raw\", len(df))]\n",
210
+ " df = df.copy()\n",
211
+ " df[\"docstring\"] = df[\"docstring\"].map(first_line)\n",
212
+ " df[\"code\"] = df[\"code\"].fillna(\"\").astype(str)\n",
213
+ "\n",
214
+ " df = df[(df[\"docstring\"].str.len() > 0) & (df[\"code\"].str.len() > 0)]\n",
215
+ " funnel.append((\"non_empty\", len(df)))\n",
216
+ "\n",
217
+ " wc = df[\"docstring\"].map(word_count)\n",
218
+ " df = df[(wc >= cfg.min_doc_words) & (wc <= cfg.max_doc_words)]\n",
219
+ " funnel.append((\"doc_word_window\", len(df)))\n",
220
+ "\n",
221
+ " df = df[df[\"code\"].str.len() >= cfg.min_code_chars]\n",
222
+ " funnel.append((\"min_code_chars\", len(df)))\n",
223
+ "\n",
224
+ " df = df[df[\"code\"].map(approx_tokens) <= cfg.max_code_tokens]\n",
225
+ " funnel.append((\"max_code_tokens\", len(df)))\n",
226
+ "\n",
227
+ " pat = \"|\".join(re.escape(t) for t in cfg.doc_blocklist)\n",
228
+ " df = df[~df[\"docstring\"].str.lower().str.contains(pat, regex=True)]\n",
229
+ " funnel.append((\"doc_blocklist\", len(df)))\n",
230
+ "\n",
231
+ " df = df[df[\"docstring\"].map(ascii_ratio) >= 0.9]\n",
232
+ " funnel.append((\"ascii_docs\", len(df)))\n",
233
+ "\n",
234
+ " df = df.drop_duplicates(subset=[\"code\"]).drop_duplicates(subset=[\"docstring\"])\n",
235
+ " funnel.append((\"dedup\", len(df)))\n",
236
+ "\n",
237
+ " funnel_df = pd.DataFrame(funnel, columns=[\"step\", \"rows_remaining\"])\n",
238
+ " return df.reset_index(drop=True), funnel_df\n",
239
+ "\n",
240
+ "clean_df, funnel = clean(raw, CFG)\n",
241
+ "print(funnel.to_string(index=False))\n",
242
+ "print(\"\\nClean rows:\", len(clean_df))\n",
243
+ "clean_df.head(2)"
244
+ ]
245
+ },
246
+ {
247
+ "cell_type": "markdown",
248
+ "id": "20747f0a",
249
+ "metadata": {},
250
+ "source": [
251
+ "## 4. Phase 1c - EDA\n",
252
+ "\n",
253
+ "Quick look at the cleaned corpus: docstring length, code length, and the cleaning\n",
254
+ "funnel. Save these for the report appendix."
255
+ ]
256
+ },
257
+ {
258
+ "cell_type": "code",
259
+ "execution_count": null,
260
+ "id": "f684c430",
261
+ "metadata": {},
262
+ "outputs": [],
263
+ "source": [
264
+ "import matplotlib.pyplot as plt\n",
265
+ "import seaborn as sns\n",
266
+ "sns.set_theme(style=\"whitegrid\")\n",
267
+ "\n",
268
+ "doc_words = clean_df[\"docstring\"].map(word_count)\n",
269
+ "code_lines = clean_df[\"code\"].str.count(\"\\n\") + 1\n",
270
+ "\n",
271
+ "fig, axes = plt.subplots(1, 3, figsize=(16, 4))\n",
272
+ "sns.histplot(doc_words, bins=40, ax=axes[0]); axes[0].set(title=\"Docstring length (words)\", xlabel=\"words\")\n",
273
+ "sns.histplot(code_lines.clip(upper=80), bins=40, ax=axes[1]); axes[1].set(title=\"Code length (lines, clipped 80)\", xlabel=\"lines\")\n",
274
+ "axes[2].barh(funnel[\"step\"], funnel[\"rows_remaining\"]); axes[2].invert_yaxis(); axes[2].set(title=\"Cleaning funnel\")\n",
275
+ "plt.tight_layout(); plt.show()\n",
276
+ "\n",
277
+ "print({\n",
278
+ " \"rows\": len(clean_df),\n",
279
+ " \"doc_words_median\": int(doc_words.median()),\n",
280
+ " \"code_lines_median\": int(code_lines.median()),\n",
281
+ "})"
282
+ ]
283
+ },
284
+ {
285
+ "cell_type": "markdown",
286
+ "id": "db098ca9",
287
+ "metadata": {},
288
+ "source": [
289
+ "## 5. Train / val / test split\n",
290
+ "\n",
291
+ "The **train** pool doubles as the retrieval corpus for RAG. We evaluate on **test**\n",
292
+ "so retrieved examples never leak the answer."
293
+ ]
294
+ },
295
+ {
296
+ "cell_type": "code",
297
+ "execution_count": null,
298
+ "id": "0c18c3e2",
299
+ "metadata": {},
300
+ "outputs": [],
301
+ "source": [
302
+ "def split(df, cfg):\n",
303
+ " df = df.sample(frac=1.0, random_state=cfg.seed).reset_index(drop=True)\n",
304
+ " n = len(df); n_tr = int(n * cfg.train); n_va = int(n * cfg.val)\n",
305
+ " return (df.iloc[:n_tr].reset_index(drop=True),\n",
306
+ " df.iloc[n_tr:n_tr+n_va].reset_index(drop=True),\n",
307
+ " df.iloc[n_tr+n_va:].reset_index(drop=True))\n",
308
+ "\n",
309
+ "train_df, val_df, test_df = split(clean_df, CFG)\n",
310
+ "print(f\"train={len(train_df)} val={len(val_df)} test={len(test_df)}\")"
311
+ ]
312
+ },
313
+ {
314
+ "cell_type": "markdown",
315
+ "id": "b2d3b684",
316
+ "metadata": {},
317
+ "source": [
318
+ "## 6. Phase 3 - Embeddings + FAISS index\n",
319
+ "\n",
320
+ "Embed each docstring in the train pool and build a cosine-similarity index\n",
321
+ "(`IndexFlatIP` on L2-normalised vectors). The default embedder is small and fast;\n",
322
+ "for a stronger code-aware corpus, swap `embed_model` to\n",
323
+ "`Salesforce/codet5p-110m-embedding` (ties into the CodeT5 family)."
324
+ ]
325
+ },
326
+ {
327
+ "cell_type": "code",
328
+ "execution_count": null,
329
+ "id": "68145a4c",
330
+ "metadata": {},
331
+ "outputs": [],
332
+ "source": [
333
+ "from sentence_transformers import SentenceTransformer\n",
334
+ "import faiss\n",
335
+ "import numpy as np\n",
336
+ "\n",
337
+ "embedder = SentenceTransformer(CFG.embed_model)\n",
338
+ "corpus = train_df.reset_index(drop=True)\n",
339
+ "\n",
340
+ "corpus_emb = embedder.encode(\n",
341
+ " corpus[\"docstring\"].tolist(),\n",
342
+ " batch_size=64, show_progress_bar=True,\n",
343
+ " convert_to_numpy=True, normalize_embeddings=True,\n",
344
+ ").astype(\"float32\")\n",
345
+ "\n",
346
+ "index = faiss.IndexFlatIP(corpus_emb.shape[1])\n",
347
+ "index.add(corpus_emb)\n",
348
+ "print(\"Indexed vectors:\", index.ntotal, \"| dim:\", corpus_emb.shape[1])"
349
+ ]
350
+ },
351
+ {
352
+ "cell_type": "code",
353
+ "execution_count": null,
354
+ "id": "386988c0",
355
+ "metadata": {},
356
+ "outputs": [],
357
+ "source": [
358
+ "def retrieve(query, k=None):\n",
359
+ " k = k or CFG.top_k\n",
360
+ " q = embedder.encode([query], convert_to_numpy=True,\n",
361
+ " normalize_embeddings=True).astype(\"float32\")\n",
362
+ " scores, idx = index.search(q, k)\n",
363
+ " out = corpus.iloc[idx[0]].copy()\n",
364
+ " out[\"score\"] = scores[0]\n",
365
+ " return out\n",
366
+ "\n",
367
+ "# sanity check\n",
368
+ "retrieve(\"read a json file from disk and return a dict\")[[\"docstring\", \"score\"]]"
369
+ ]
370
+ },
371
+ {
372
+ "cell_type": "markdown",
373
+ "id": "45bad6b2",
374
+ "metadata": {},
375
+ "source": [
376
+ "## 7. Phase 5a - Load the code LLM\n",
377
+ "\n",
378
+ "`Qwen2.5-Coder-1.5B-Instruct` fits on a free T4. For higher quality (and a Colab Pro\n",
379
+ "GPU) bump `gen_model` to `Qwen/Qwen2.5-Coder-7B-Instruct`."
380
+ ]
381
+ },
382
+ {
383
+ "cell_type": "code",
384
+ "execution_count": null,
385
+ "id": "c42beea4",
386
+ "metadata": {},
387
+ "outputs": [],
388
+ "source": [
389
+ "from transformers import AutoTokenizer, AutoModelForCausalLM\n",
390
+ "\n",
391
+ "tok = AutoTokenizer.from_pretrained(CFG.gen_model)\n",
392
+ "model = AutoModelForCausalLM.from_pretrained(\n",
393
+ " CFG.gen_model, torch_dtype=\"auto\", device_map=\"auto\"\n",
394
+ ")\n",
395
+ "\n",
396
+ "def chat_generate(messages, max_new_tokens=320):\n",
397
+ " text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n",
398
+ " inputs = tok(text, return_tensors=\"pt\").to(model.device)\n",
399
+ " out = model.generate(**inputs, max_new_tokens=max_new_tokens,\n",
400
+ " do_sample=False, pad_token_id=tok.eos_token_id)\n",
401
+ " new = out[0][inputs.input_ids.shape[1]:]\n",
402
+ " return tok.decode(new, skip_special_tokens=True)\n",
403
+ "\n",
404
+ "def extract_code(text):\n",
405
+ " \"\"\"Strip markdown fences if the model wrapped the code.\"\"\"\n",
406
+ " m = re.search(r\"```(?:python)?\\n(.*?)```\", text, re.DOTALL)\n",
407
+ " return m.group(1).strip() if m else text.strip()\n",
408
+ "\n",
409
+ "print(\"Model loaded:\", CFG.gen_model)"
410
+ ]
411
+ },
412
+ {
413
+ "cell_type": "markdown",
414
+ "id": "2622d4fd",
415
+ "metadata": {},
416
+ "source": [
417
+ "## 8. Phase 5b - Baseline vs RAG prompts\n",
418
+ "\n",
419
+ "Same model, two prompting strategies. The RAG prompt injects the top-k retrieved\n",
420
+ "`(docstring, code)` pairs as dynamic few-shot context."
421
+ ]
422
+ },
423
+ {
424
+ "cell_type": "code",
425
+ "execution_count": null,
426
+ "id": "54db7627",
427
+ "metadata": {},
428
+ "outputs": [],
429
+ "source": [
430
+ "SYS = (\"You are an expert Python coding assistant. Write a single, correct, \"\n",
431
+ " \"self-contained Python function for the request. Output only code.\")\n",
432
+ "\n",
433
+ "def baseline_messages(intent):\n",
434
+ " return [{\"role\": \"system\", \"content\": SYS},\n",
435
+ " {\"role\": \"user\", \"content\": f\"# Task: {intent}\"}]\n",
436
+ "\n",
437
+ "def rag_messages(intent, k=None):\n",
438
+ " ex = retrieve(intent, k)\n",
439
+ " blocks = [f\"# Task: {r.docstring}\\n{r.code}\" for _, r in ex.iterrows()]\n",
440
+ " context = \"\\n\\n\".join(blocks)\n",
441
+ " user = (f\"Here are similar reference examples:\\n\\n{context}\\n\\n\"\n",
442
+ " f\"# Now write a function for this task:\\n# Task: {intent}\")\n",
443
+ " return [{\"role\": \"system\", \"content\": SYS},\n",
444
+ " {\"role\": \"user\", \"content\": user}]\n",
445
+ "\n",
446
+ "demo = \"Write a function that returns the n-th Fibonacci number.\"\n",
447
+ "print(\"=== BASELINE ===\")\n",
448
+ "print(extract_code(chat_generate(baseline_messages(demo))))\n",
449
+ "print(\"\\n=== RAG ===\")\n",
450
+ "print(extract_code(chat_generate(rag_messages(demo))))"
451
+ ]
452
+ },
453
+ {
454
+ "cell_type": "markdown",
455
+ "id": "f2f6fda3",
456
+ "metadata": {},
457
+ "source": [
458
+ "## 9. Eval - CodeBLEU, baseline vs RAG\n",
459
+ "\n",
460
+ "We score generated code against the reference on held-out **test** rows. CodeBLEU\n",
461
+ "weights AST + data-flow match, not just text overlap. If `codebleu` did not install,\n",
462
+ "we fall back to a token-overlap F1 so the cell still runs.\n",
463
+ "\n",
464
+ "> Caveat: CodeSearchNet has no unit tests, so this measures *similarity to the\n",
465
+ "> reference*, not functional correctness. For pass@k, add a HumanEval/MBPP harness\n",
466
+ "> (Phase 2) - flagged in the next-steps cell."
467
+ ]
468
+ },
469
+ {
470
+ "cell_type": "code",
471
+ "execution_count": null,
472
+ "id": "8530179f",
473
+ "metadata": {},
474
+ "outputs": [],
475
+ "source": [
476
+ "# Try CodeBLEU; fall back to token-F1 if the metric OR its parser is unavailable.\n",
477
+ "score, METRIC = None, None\n",
478
+ "try:\n",
479
+ " from codebleu import calc_codebleu\n",
480
+ " # actually CALL it once - this is what needs the tree-sitter parser\n",
481
+ " _ = calc_codebleu([\"def f(): return 1\"], [\"def f(): return 1\"], lang=\"python\")\n",
482
+ " def score(ref, hyp):\n",
483
+ " return calc_codebleu([ref], [hyp], lang=\"python\")[\"codebleu\"]\n",
484
+ " METRIC = \"CodeBLEU\"\n",
485
+ "except Exception as e:\n",
486
+ " print(\"CodeBLEU unavailable, using token-F1 fallback:\", e)\n",
487
+ " def _toks(s):\n",
488
+ " return set(re.findall(r\"\\w+\", s))\n",
489
+ " def score(ref, hyp):\n",
490
+ " a, b = _toks(ref), _toks(hyp)\n",
491
+ " if not a or not b:\n",
492
+ " return 0.0\n",
493
+ " inter = len(a & b)\n",
494
+ " p, rec = inter / len(b), inter / len(a)\n",
495
+ " return 0.0 if p + rec == 0 else 2 * p * rec / (p + rec)\n",
496
+ " METRIC = \"token-F1 (fallback)\"\n",
497
+ "print(\"Using metric:\", METRIC)"
498
+ ]
499
+ },
500
+ {
501
+ "cell_type": "code",
502
+ "execution_count": null,
503
+ "id": "b1f22892",
504
+ "metadata": {},
505
+ "outputs": [],
506
+ "source": [
507
+ "N_EVAL = 15 # keep small on free Colab; raise for the real run\n",
508
+ "sample = test_df.sample(min(N_EVAL, len(test_df)), random_state=CFG.seed)\n",
509
+ "\n",
510
+ "rows = []\n",
511
+ "for _, r in sample.iterrows():\n",
512
+ " base = extract_code(chat_generate(baseline_messages(r.docstring)))\n",
513
+ " rag = extract_code(chat_generate(rag_messages(r.docstring)))\n",
514
+ " rows.append({\"baseline\": score(r.code, base), \"rag\": score(r.code, rag)})\n",
515
+ "\n",
516
+ "res = pd.DataFrame(rows)\n",
517
+ "print(f\"Mean {METRIC} over {len(res)} test tasks:\")\n",
518
+ "print(res.mean().round(4).to_string())"
519
+ ]
520
+ },
521
+ {
522
+ "cell_type": "markdown",
523
+ "id": "86b042c5",
524
+ "metadata": {},
525
+ "source": [
526
+ "## 10. Interactive - ask it to write code\n",
527
+ "\n",
528
+ "Edit the string and run. This uses the RAG pipeline and shows the retrieved\n",
529
+ "examples so the grounding is visible."
530
+ ]
531
+ },
532
+ {
533
+ "cell_type": "code",
534
+ "execution_count": null,
535
+ "id": "13cf8cc1",
536
+ "metadata": {},
537
+ "outputs": [],
538
+ "source": [
539
+ "def ask(intent, show_sources=True):\n",
540
+ " if show_sources:\n",
541
+ " print(\"Retrieved examples:\")\n",
542
+ " for _, r in retrieve(intent).iterrows():\n",
543
+ " print(f\" - ({r.score:.2f}) {r.docstring}\")\n",
544
+ " print(\"-\" * 50)\n",
545
+ " print(extract_code(chat_generate(rag_messages(intent))))\n",
546
+ "\n",
547
+ "ask(\"Write a function to check whether a string is a valid IPv4 address.\")"
548
+ ]
549
+ },
550
+ {
551
+ "cell_type": "markdown",
552
+ "id": "61adc276",
553
+ "metadata": {},
554
+ "source": [
555
+ "## 11. (Optional) Phase 4 - Fine-tune CodeT5+\n",
556
+ "\n",
557
+ "A compact demonstration of the fine-tuning arm: train `codet5p-220m` on a small\n",
558
+ "`docstring -> code` subset for a few steps so you can see the loop work, then\n",
559
+ "generate. For the real capstone result, raise `subset`/`epochs` and run on a Pro\n",
560
+ "GPU. **This section is slow - skip on a first pass.**"
561
+ ]
562
+ },
563
+ {
564
+ "cell_type": "code",
565
+ "execution_count": null,
566
+ "id": "c2f5217e",
567
+ "metadata": {},
568
+ "outputs": [],
569
+ "source": [
570
+ "# Set to True to run fine-tuning.\n",
571
+ "RUN_FINETUNE = False\n",
572
+ "\n",
573
+ "if RUN_FINETUNE:\n",
574
+ " from transformers import (AutoTokenizer, AutoModelForSeq2SeqLM,\n",
575
+ " Seq2SeqTrainer, Seq2SeqTrainingArguments,\n",
576
+ " DataCollatorForSeq2Seq)\n",
577
+ " from datasets import Dataset\n",
578
+ "\n",
579
+ " ck = \"Salesforce/codet5p-220m\"\n",
580
+ " t5_tok = AutoTokenizer.from_pretrained(ck)\n",
581
+ " t5 = AutoModelForSeq2SeqLM.from_pretrained(ck)\n",
582
+ "\n",
583
+ " subset = train_df.head(2000)\n",
584
+ " def to_features(batch):\n",
585
+ " x = t5_tok(batch[\"docstring\"], max_length=64, truncation=True, padding=\"max_length\")\n",
586
+ " y = t5_tok(text_target=batch[\"code\"], max_length=256, truncation=True, padding=\"max_length\")\n",
587
+ " x[\"labels\"] = y[\"input_ids\"]\n",
588
+ " return x\n",
589
+ "\n",
590
+ " hf = Dataset.from_pandas(subset[[\"docstring\", \"code\"]]).map(\n",
591
+ " to_features, batched=True, remove_columns=[\"docstring\", \"code\"])\n",
592
+ "\n",
593
+ " args = Seq2SeqTrainingArguments(\n",
594
+ " output_dir=\"codet5p-ft\", per_device_train_batch_size=8,\n",
595
+ " num_train_epochs=1, learning_rate=5e-5, logging_steps=20,\n",
596
+ " fp16=torch.cuda.is_available(), report_to=\"none\", save_strategy=\"no\")\n",
597
+ "\n",
598
+ " trainer = Seq2SeqTrainer(\n",
599
+ " model=t5, args=args, train_dataset=hf,\n",
600
+ " data_collator=DataCollatorForSeq2Seq(t5_tok, model=t5))\n",
601
+ " trainer.train()\n",
602
+ "\n",
603
+ " def t5_generate(intent):\n",
604
+ " ids = t5_tok(intent, return_tensors=\"pt\").input_ids.to(t5.device)\n",
605
+ " out = t5.generate(ids, max_length=256)\n",
606
+ " return t5_tok.decode(out[0], skip_special_tokens=True)\n",
607
+ "\n",
608
+ " print(t5_generate(\"Return the factorial of a non-negative integer n.\"))\n",
609
+ "else:\n",
610
+ " print(\"Fine-tuning skipped. Set RUN_FINETUNE = True to run it.\")"
611
+ ]
612
+ },
613
+ {
614
+ "cell_type": "markdown",
615
+ "id": "ed8b964b",
616
+ "metadata": {},
617
+ "source": [
618
+ "## 12. Next steps + deploying to VS Code\n",
619
+ "\n",
620
+ "**What's still to add for the full capstone:**\n",
621
+ "- **Phase 2 functional eval:** wire up HumanEval / MBPP for real `pass@k` (they ship\n",
622
+ " unit tests, unlike CodeSearchNet). This is the metric graders trust most.\n",
623
+ "- **Phase 6 agentic loop:** generate -> run in a sandbox -> read traceback -> repair.\n",
624
+ "- **Retrieval quality:** measure recall@k / MRR on the search task to justify the embedder.\n",
625
+ "\n",
626
+ "**Lifting this into VS Code for deployment:**\n",
627
+ "1. The functions here map cleanly onto the repo modules: `clean()` -> `src/data/clean.py`,\n",
628
+ " `retrieve()` + index build -> `src/rag/retriever.py`, `chat_generate()`/prompts ->\n",
629
+ " `src/rag/generator.py`.\n",
630
+ "2. Persist the FAISS index (`faiss.write_index(index, \"index.faiss\")`) and the corpus\n",
631
+ " so you don't rebuild on every start.\n",
632
+ "3. Wrap `ask()` in a **Streamlit** app (`app.py`) for the Phase 7 chat UI:\n",
633
+ " `streamlit run app.py`.\n",
634
+ "4. Keep `config.yaml` as the single source of truth across notebook and app."
635
+ ]
636
+ }
637
+ ],
638
+ "metadata": {
639
+ "colab": {
640
+ "provenance": []
641
+ },
642
+ "kernelspec": {
643
+ "display_name": "Python 3",
644
+ "name": "python3"
645
+ },
646
+ "language_info": {
647
+ "name": "python"
648
+ }
649
+ },
650
+ "nbformat": 4,
651
+ "nbformat_minor": 5
652
+ }
requirements.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Phase 1: data + EDA ---
2
+ datasets>=2.18
3
+ pandas>=2.0
4
+ pyarrow>=14.0
5
+ matplotlib>=3.7
6
+ seaborn>=0.13
7
+ pyyaml>=6.0
8
+
9
+ # --- Phase 2: evaluation ---
10
+ numpy>=1.24
11
+ codebleu>=0.7
12
+ tree-sitter>=0.21
13
+ tree-sitter-python>=0.21
14
+
15
+ # --- Phase 3: retrieval ---
16
+ sentence-transformers>=2.7
17
+ faiss-cpu>=1.8
18
+
19
+ # --- Phase 4-6: generation, fine-tuning, agent ---
20
+ transformers>=4.40
21
+ torch>=2.2
22
+ accelerate>=0.30
23
+ peft>=0.10
24
+
25
+ # --- Deployment ---
26
+ fastapi>=0.110
27
+ uvicorn>=0.29
28
+ pydantic>=2.0
29
+ gradio>=4.0
30
+ streamlit>=1.33
scripts/01_prepare_data.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 1 entrypoint: load -> clean -> split -> save processed parquet files.
2
+
3
+ Usage:
4
+ python scripts/01_prepare_data.py
5
+ Outputs train/val/test parquet files + a cleaning_funnel.csv into data/processed/.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
13
+ from src.config import load_config
14
+ from src.data.clean import clean, split
15
+ from src.data.load import load_raw
16
+
17
+
18
+ def main():
19
+ cfg = load_config()
20
+ print("=" * 60)
21
+ print("PHASE 1: DATA PREPARATION")
22
+ print("=" * 60)
23
+
24
+ raw = load_raw(cfg)
25
+ cleaned, funnel = clean(raw, cfg)
26
+
27
+ print("\nCleaning funnel:")
28
+ print(funnel.to_string(index=False))
29
+
30
+ splits = split(cleaned, cfg)
31
+ out = Path(cfg.paths.processed_dir)
32
+ for name, part in splits.items():
33
+ path = out / f"{name}.parquet"
34
+ part.to_parquet(path, index=False)
35
+ print(f" saved {name}: {len(part):,} rows -> {path}")
36
+
37
+ funnel.to_csv(out / "cleaning_funnel.csv", index=False)
38
+ print(f"\nDone. Processed data in {out}")
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
scripts/02_run_eda.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 1 EDA entrypoint: analyse the processed training split.
2
+
3
+ Usage:
4
+ python scripts/02_run_eda.py
5
+ Reads data/processed/train.parquet, writes plots + eda_stats.json to data/eda/.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import pandas as pd
14
+
15
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
16
+ from src.config import load_config
17
+ from src.eda.analyze import run_eda
18
+
19
+
20
+ def main():
21
+ cfg = load_config()
22
+ print("=" * 60)
23
+ print("PHASE 1: EXPLORATORY DATA ANALYSIS")
24
+ print("=" * 60)
25
+
26
+ train_path = Path(cfg.paths.processed_dir) / "train.parquet"
27
+ funnel_path = Path(cfg.paths.processed_dir) / "cleaning_funnel.csv"
28
+ if not train_path.exists():
29
+ sys.exit("train.parquet not found. Run scripts/01_prepare_data.py first.")
30
+
31
+ df = pd.read_parquet(train_path)
32
+ funnel = pd.read_csv(funnel_path) if funnel_path.exists() else None
33
+
34
+ stats = run_eda(df, cfg, funnel)
35
+ print(json.dumps({k: v for k, v in stats.items() if k != "plots"}, indent=2))
36
+ print(f"\nPlots saved to {cfg.paths.eda_dir}")
37
+
38
+
39
+ if __name__ == "__main__":
40
+ main()
scripts/03_build_index.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 3 entrypoint: build + persist the FAISS retrieval index.
2
+
3
+ Usage: python scripts/03_build_index.py
4
+ Reads data/processed/train.parquet, writes the index to data/index/.
5
+ """
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
10
+ from src.rag.embedder import build_index_from_processed
11
+
12
+ if __name__ == "__main__":
13
+ print("=" * 60, "\nPHASE 3: BUILD RETRIEVAL INDEX\n", "=" * 60, sep="")
14
+ build_index_from_processed()
scripts/04_run_eval.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 2 entrypoint: run the eval suite and print a comparison table.
2
+
3
+ Usage: python scripts/04_run_eval.py
4
+ Loads the index + assistant, then reports:
5
+ - retrieval recall@k / MRR
6
+ - functional pass@1 on HumanEval (baseline vs RAG)
7
+ """
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ import pandas as pd
12
+
13
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
14
+ from src.config import load_config
15
+ from src.eval.functional_eval import evaluate
16
+ from src.eval.retrieval_eval import evaluate_cross_modal
17
+ from src.rag.generator import CodeAssistant
18
+
19
+
20
+ def main():
21
+ cfg = load_config()
22
+ print("=" * 60, "\nPHASE 2: EVALUATION\n", "=" * 60, sep="")
23
+
24
+ assistant = CodeAssistant.from_config(cfg, with_index=True)
25
+
26
+ # 1. Cross-modal retrieval quality on held-out test pairs.
27
+ test = pd.read_parquet(Path(cfg.paths.processed_dir) / "test.parquet")
28
+ pairs = test[["docstring", "code"]].dropna().sample(
29
+ n=min(500, len(test)), random_state=42
30
+ ).reset_index(drop=True)
31
+ print("\n[retrieval]", evaluate_cross_modal(assistant.index.embedder, pairs))
32
+
33
+ # 2. Functional pass@1: baseline vs RAG on HumanEval.
34
+ LIMIT = 12 # raise for the real run
35
+ base = evaluate(lambda i: assistant.generate(i, mode="baseline"),
36
+ "humaneval", limit=LIMIT)
37
+ rag = evaluate(lambda i: assistant.generate(i, mode="rag"),
38
+ "humaneval", limit=LIMIT)
39
+
40
+ table = pd.DataFrame([
41
+ {"system": "baseline", "pass@1": base.pass_at_1},
42
+ {"system": "rag", "pass@1": rag.pass_at_1},
43
+ ])
44
+ print(f"\n[functional] HumanEval pass@1 over {LIMIT} problems:")
45
+ print(table.to_string(index=False))
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
scripts/05_finetune.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 4 entrypoint: fine-tune CodeT5+.
2
+
3
+ Usage: python scripts/05_finetune.py
4
+ Trains on data/processed/train.parquet, saves to data/codet5p-ft/.
5
+ """
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
10
+ from src.finetune.train_codet5 import finetune
11
+
12
+ if __name__ == "__main__":
13
+ print("=" * 60, "\nPHASE 4: FINE-TUNE CODET5+\n", "=" * 60, sep="")
14
+ finetune(subset_size=5000, epochs=1)
scripts/retrieval_only_eval.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cross-modal retrieval eval — no LLM generation, finishes in seconds.
2
+
3
+ Runs twice:
4
+ 1. Raw code (inflated — docstring is embedded inside func_code_string).
5
+ 2. Docstrings stripped from candidate code (cleaner semantic signal).
6
+
7
+ Usage: python scripts/retrieval_only_eval.py
8
+ """
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import pandas as pd
13
+
14
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
15
+ from src.config import load_config
16
+ from src.eval.retrieval_eval import evaluate_cross_modal
17
+ from src.rag.embedder import CodeIndex
18
+
19
+ cfg = load_config()
20
+
21
+ print("[load] reading test split ...")
22
+ test = pd.read_parquet(Path(cfg.paths.processed_dir) / "test.parquet")
23
+ pairs = (
24
+ test[["docstring", "code"]]
25
+ .dropna()
26
+ .sample(n=min(500, len(test)), random_state=42)
27
+ .reset_index(drop=True)
28
+ )
29
+ print(f" {len(pairs)} pairs")
30
+
31
+ print("[load] loading embedder ...")
32
+ idx = CodeIndex.load(cfg.paths.index_dir)
33
+
34
+ print()
35
+ for strip in (False, True):
36
+ label = "stripped (leakage-free)" if strip else "raw code (⚠ lexical leakage)"
37
+ r = evaluate_cross_modal(idx.embedder, pairs, k_values=(1, 5, 10),
38
+ strip_code_docstrings=strip)
39
+ print(f"\n=== {label} ===")
40
+ print(f" N : {r['n_pairs']}")
41
+ print(f" MRR : {r['mrr']:.4f}")
42
+ for k in (1, 5, 10):
43
+ print(f" R@{k:2d} : {r[f'recall@{k}']:.4f}")
scripts/test_repair_loop.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Throwaway script: exercise the agentic repair loop on a tricky problem.
2
+
3
+ The intent is chosen to be genuinely hard for a small model: balanced-bracket
4
+ checking with all three delimiter types. The check_fn appends a strict test
5
+ harness so the sandbox can report a traceback on failure.
6
+
7
+ Run:
8
+ python scripts/test_repair_loop.py
9
+ Expected output: iteration trace showing whether the model self-corrects.
10
+ """
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ sys.path.append(str(Path(__file__).resolve().parents[1]))
15
+ from src.agent.repair_loop import make_repair_generator, repair_loop
16
+ from src.rag.generator import CodeAssistant
17
+
18
+ # ── test harness appended to every generated function ─────────────────────
19
+ _TEST_HARNESS = '''
20
+
21
+ # --- auto-injected test harness ---
22
+ assert is_balanced("") is True, "empty string should be balanced"
23
+ assert is_balanced("()") is True, "simple parens"
24
+ assert is_balanced("([{}])") is True, "nested mix"
25
+ assert is_balanced("([)]") is False, "wrong order"
26
+ assert is_balanced("(") is False, "unclosed"
27
+ assert is_balanced("{[}]") is False, "interleaved wrong"
28
+ assert is_balanced("((()))") is True, "deep nesting"
29
+ print("all tests passed")
30
+ '''
31
+
32
+ INTENT = (
33
+ "Write a Python function called `is_balanced` that returns True if the input "
34
+ "string has balanced parentheses (), square brackets [], and curly braces {}, "
35
+ "and False otherwise. Use a stack."
36
+ )
37
+
38
+
39
+ def check_fn(code: str) -> str:
40
+ """Append the test harness to the generated function."""
41
+ return code + _TEST_HARNESS
42
+
43
+
44
+ def main():
45
+ print("=" * 60)
46
+ print("REPAIR LOOP TEST")
47
+ print("Intent:", INTENT[:80] + "...")
48
+ print("=" * 60)
49
+
50
+ print("\n[setup] Loading CodeAssistant (model + index)...")
51
+ assistant = CodeAssistant.from_config(with_index=True)
52
+ generate_fn = make_repair_generator(assistant)
53
+
54
+ print("[repair] Starting loop (max_iters=3)...\n")
55
+ trace = repair_loop(INTENT, generate_fn, check_fn, max_iters=3, timeout=10.0)
56
+
57
+ print("\n" + "=" * 60)
58
+ print("ITERATION TRACE")
59
+ print("=" * 60)
60
+ for i, (code, error) in enumerate(trace.history, 1):
61
+ print(f"\n--- Iteration {i} ---")
62
+ print("Generated code:")
63
+ print(code)
64
+ if error:
65
+ print(f"\nError:\n {error}")
66
+ else:
67
+ print("\nResult: PASSED")
68
+
69
+ print("\n" + "=" * 60)
70
+ print(f"Outcome: {'SUCCESS' if trace.success else 'BEST EFFORT (did not pass)'}")
71
+ print(f"Iterations used: {trace.iterations}")
72
+ print("=" * 60)
73
+ if trace.success:
74
+ print("\nFinal passing code:")
75
+ print(trace.final_code)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
src/__init__.py ADDED
File without changes
src/agent/__init__.py ADDED
File without changes
src/agent/repair_loop.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 6: agentic generate -> execute -> repair loop.
2
+
3
+ Model-agnostic: takes a `generate_fn(intent, feedback) -> code` callable, so it
4
+ works with CodeAssistant or any other generator (and is unit-testable with a mock).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Callable
12
+
13
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
14
+ from src.eval.sandbox import run_code # noqa: E402
15
+
16
+
17
+ @dataclass
18
+ class RepairTrace:
19
+ final_code: str
20
+ success: bool
21
+ iterations: int
22
+ history: list = field(default_factory=list) # list of (code, error)
23
+
24
+
25
+ def repair_loop(
26
+ intent: str,
27
+ generate_fn: Callable[[str, str | None], str],
28
+ check_program_fn: Callable[[str], str],
29
+ max_iters: int = 3,
30
+ timeout: float = 8.0,
31
+ ) -> RepairTrace:
32
+ """Iteratively generate and self-correct.
33
+
34
+ generate_fn(intent, feedback) -> candidate code
35
+ feedback is None on the first call, else the previous error string.
36
+ check_program_fn(code) -> a runnable program string (code + a smoke
37
+ test or the harness test) used to decide pass/fail.
38
+ """
39
+ feedback = None
40
+ history = []
41
+ for i in range(1, max_iters + 1):
42
+ code = generate_fn(intent, feedback)
43
+ result = run_code(check_program_fn(code), timeout=timeout)
44
+ history.append((code, result.error))
45
+ if result.ok:
46
+ return RepairTrace(code, True, i, history)
47
+ feedback = result.error # feed the traceback back in
48
+ return RepairTrace(history[-1][0], False, max_iters, history)
49
+
50
+
51
+ def make_repair_generator(assistant):
52
+ """Adapt a CodeAssistant into a generate_fn for the repair loop."""
53
+ def generate_fn(intent: str, feedback: str | None) -> str:
54
+ if feedback:
55
+ intent = (f"{intent}\n\n# Your previous attempt failed with this error:\n"
56
+ f"# {feedback}\n# Fix it and return the corrected function.")
57
+ return assistant.generate(intent, mode="rag")
58
+ return generate_fn
src/config.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load the project YAML config into a simple attribute-style object."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+
8
+ import yaml
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+
12
+
13
+ def _to_namespace(obj):
14
+ if isinstance(obj, dict):
15
+ return SimpleNamespace(**{k: _to_namespace(v) for k, v in obj.items()})
16
+ if isinstance(obj, list):
17
+ return [_to_namespace(v) for v in obj]
18
+ return obj
19
+
20
+
21
+ def load_config(path: str | os.PathLike | None = None) -> SimpleNamespace:
22
+ """Read config.yaml and return a nested namespace (cfg.data.languages, ...)."""
23
+ path = Path(path) if path else PROJECT_ROOT / "config.yaml"
24
+ with open(path, "r", encoding="utf-8") as f:
25
+ raw = yaml.safe_load(f)
26
+ cfg = _to_namespace(raw)
27
+ # Resolve paths relative to project root and ensure they exist.
28
+ for attr in ("data_dir", "raw_dir", "processed_dir", "eda_dir", "index_dir"):
29
+ abspath = PROJECT_ROOT / getattr(cfg.paths, attr)
30
+ setattr(cfg.paths, attr, str(abspath))
31
+ abspath.mkdir(parents=True, exist_ok=True)
32
+ return cfg
33
+
34
+
35
+ if __name__ == "__main__":
36
+ c = load_config()
37
+ print("Languages:", c.data.languages)
38
+ print("Use sample:", c.data.use_sample)
39
+ print("Processed dir:", c.paths.processed_dir)
src/data/__init__.py ADDED
File without changes
src/data/clean.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 1b: clean and filter the raw (docstring, code) pairs.
2
+
3
+ CodeSearchNet is noisy. A clean, filtered subset trains and retrieves better
4
+ than the raw dump. Every filter records how many rows it removed so you can
5
+ report the funnel in your EDA / write-up.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import pandas as pd
14
+
15
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
16
+ from src.config import load_config # noqa: E402
17
+
18
+ _WORD_RE = re.compile(r"\b\w+\b")
19
+
20
+
21
+ def _first_line(text: str) -> str:
22
+ """CodeSearchNet docstrings often have a summary first line + details.
23
+ For NL->code we keep the summary line (the actual 'intent')."""
24
+ return text.strip().split("\n")[0].strip() if isinstance(text, str) else ""
25
+
26
+
27
+ def _word_count(text: str) -> int:
28
+ return len(_WORD_RE.findall(text)) if isinstance(text, str) else 0
29
+
30
+
31
+ def _ascii_ratio(text: str) -> float:
32
+ if not text:
33
+ return 1.0
34
+ ascii_chars = sum(1 for ch in text if ord(ch) < 128)
35
+ return ascii_chars / len(text)
36
+
37
+
38
+ def _approx_tokens(code: str) -> int:
39
+ """Cheap proxy for token count (whitespace + punctuation split)."""
40
+ return len(re.findall(r"\w+|[^\s\w]", code)) if isinstance(code, str) else 0
41
+
42
+
43
+ def clean(df: pd.DataFrame, cfg=None) -> tuple[pd.DataFrame, pd.DataFrame]:
44
+ """Return (cleaned_df, funnel_df). funnel_df logs rows removed per step."""
45
+ cfg = cfg or load_config()
46
+ cc = cfg.cleaning
47
+ funnel = [("raw", len(df))]
48
+ df = df.copy()
49
+
50
+ # Use only the summary line of each docstring as the NL intent.
51
+ df["docstring"] = df["docstring"].map(_first_line)
52
+ df["code"] = df["code"].fillna("").astype(str)
53
+
54
+ # 1. Drop empty docstring or code.
55
+ df = df[(df["docstring"].str.len() > 0) & (df["code"].str.len() > 0)]
56
+ funnel.append(("non_empty", len(df)))
57
+
58
+ # 2. Docstring word-count window.
59
+ wc = df["docstring"].map(_word_count)
60
+ df = df[(wc >= cc.min_doc_words) & (wc <= cc.max_doc_words)]
61
+ funnel.append(("doc_word_window", len(df)))
62
+
63
+ # 3. Minimum code length.
64
+ df = df[df["code"].str.len() >= cc.min_code_chars]
65
+ funnel.append(("min_code_chars", len(df)))
66
+
67
+ # 4. Maximum code tokens (budget for the generator's context).
68
+ df = df[df["code"].map(_approx_tokens) <= cc.max_code_tokens]
69
+ funnel.append(("max_code_tokens", len(df)))
70
+
71
+ # 5. Blocklisted / autogenerated docstrings.
72
+ pattern = "|".join(re.escape(t) for t in cc.doc_blocklist)
73
+ if pattern:
74
+ df = df[~df["docstring"].str.lower().str.contains(pattern, regex=True)]
75
+ funnel.append(("doc_blocklist", len(df)))
76
+
77
+ # 6. Drop mostly-non-ASCII docstrings (non-English noise).
78
+ if cc.drop_non_ascii_docs:
79
+ df = df[df["docstring"].map(_ascii_ratio) >= 0.9]
80
+ funnel.append(("ascii_docs", len(df)))
81
+
82
+ # 7. Exact duplicate removal (same code or same docstring).
83
+ if cc.drop_exact_duplicates:
84
+ df = df.drop_duplicates(subset=["code"]).drop_duplicates(subset=["docstring"])
85
+ funnel.append(("dedup", len(df)))
86
+
87
+ df = df.reset_index(drop=True)
88
+ funnel_df = pd.DataFrame(funnel, columns=["step", "rows_remaining"])
89
+ funnel_df["removed"] = funnel_df["rows_remaining"].shift(1).fillna(
90
+ funnel_df["rows_remaining"].iloc[0]
91
+ ).astype(int) - funnel_df["rows_remaining"]
92
+ return df, funnel_df
93
+
94
+
95
+ def split(df: pd.DataFrame, cfg=None) -> dict[str, pd.DataFrame]:
96
+ """Random train/val/test split per the config ratios."""
97
+ cfg = cfg or load_config()
98
+ df = df.sample(frac=1.0, random_state=cfg.split.seed).reset_index(drop=True)
99
+ n = len(df)
100
+ n_train = int(n * cfg.split.train)
101
+ n_val = int(n * cfg.split.val)
102
+ return {
103
+ "train": df.iloc[:n_train].reset_index(drop=True),
104
+ "val": df.iloc[n_train:n_train + n_val].reset_index(drop=True),
105
+ "test": df.iloc[n_train + n_val:].reset_index(drop=True),
106
+ }
107
+
108
+
109
+ if __name__ == "__main__":
110
+ from src.data.load import load_raw
111
+
112
+ cfg = load_config()
113
+ raw = load_raw(cfg)
114
+ cleaned, funnel = clean(raw, cfg)
115
+ print(funnel.to_string(index=False))
116
+ print("cleaned rows:", len(cleaned))
src/data/load.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 1a: load raw CodeSearchNet (or the synthetic sample).
2
+
3
+ We normalise everything to a pandas DataFrame with two key columns:
4
+ - docstring : natural-language description (model INPUT)
5
+ - code : function body (model TARGET)
6
+ plus useful metadata (language, repo, url) for traceability.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import pandas as pd
14
+
15
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
16
+ from src.config import load_config # noqa: E402
17
+ from src.data.make_sample import make_sample # noqa: E402
18
+
19
+ # Map CodeSearchNet's verbose column names to our canonical names.
20
+ _COLUMN_MAP = {
21
+ "func_documentation_string": "docstring",
22
+ "func_code_string": "code",
23
+ "language": "language",
24
+ "repository_name": "repo",
25
+ "func_code_url": "url",
26
+ }
27
+
28
+
29
+ def _from_huggingface(cfg) -> pd.DataFrame:
30
+ """Stream CodeSearchNet per-language from HuggingFace and concatenate."""
31
+ from datasets import load_dataset
32
+
33
+ max_rows = getattr(cfg.data, "max_rows", 0)
34
+ frames = []
35
+ for lang in cfg.data.languages:
36
+ print(f"[load] downloading CodeSearchNet '{lang}' ...")
37
+ # CodeSearchNet ships train/validation/test; we pull all and re-split later.
38
+ ds = load_dataset(cfg.data.hf_dataset_id, lang)
39
+ for split in ds.keys():
40
+ df = ds[split].to_pandas()
41
+ keep = [c for c in _COLUMN_MAP if c in df.columns]
42
+ df = df[keep].rename(columns=_COLUMN_MAP)
43
+ frames.append(df)
44
+ out = pd.concat(frames, ignore_index=True)
45
+ print(f"[load] total raw rows: {len(out):,}")
46
+ if max_rows and max_rows > 0 and len(out) > max_rows:
47
+ out = out.sample(n=max_rows, random_state=42).reset_index(drop=True)
48
+ print(f"[load] capped to {max_rows:,} rows (max_rows setting)")
49
+ return out
50
+
51
+
52
+ def _from_sample(cfg) -> pd.DataFrame:
53
+ print(f"[load] using synthetic sample (n={cfg.data.sample_size})")
54
+ df = make_sample(cfg.data.sample_size, cfg.split.seed)
55
+ keep = [c for c in _COLUMN_MAP if c in df.columns]
56
+ return df[keep].rename(columns=_COLUMN_MAP)
57
+
58
+
59
+ def load_raw(cfg=None) -> pd.DataFrame:
60
+ cfg = cfg or load_config()
61
+ if cfg.data.use_sample:
62
+ df = _from_sample(cfg)
63
+ else:
64
+ try:
65
+ df = _from_huggingface(cfg)
66
+ except Exception as e: # noqa: BLE001
67
+ print(
68
+ f"[load] HuggingFace load failed ({e}).\n"
69
+ f" Tip: try hf_dataset_id: 'code-search-net/code_search_net' "
70
+ f"in config.yaml, or set use_sample: true.",
71
+ file=sys.stderr,
72
+ )
73
+ raise
74
+ # Guarantee the columns downstream code expects, even if metadata missing.
75
+ for col in ("docstring", "code", "language", "repo", "url"):
76
+ if col not in df.columns:
77
+ df[col] = ""
78
+ return df
79
+
80
+
81
+ if __name__ == "__main__":
82
+ df = load_raw()
83
+ print(df.shape)
84
+ print(df.head(3).to_string())
src/data/make_sample.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate a small synthetic dataset in CodeSearchNet's schema.
2
+
3
+ This lets you test the full pipeline (clean -> EDA -> later phases) without
4
+ downloading the real ~2M-row dataset. The schema matches the HuggingFace
5
+ `code_search_net` columns we actually use downstream.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import random
10
+
11
+ import pandas as pd
12
+
13
+ # A handful of realistic (docstring, code) templates plus some deliberately
14
+ # "dirty" rows so the cleaning step has something to remove.
15
+ _GOOD = [
16
+ (
17
+ "Return the factorial of a non-negative integer n.",
18
+ "def factorial(n):\n if n < 0:\n raise ValueError('n must be >= 0')\n result = 1\n for i in range(2, n + 1):\n result *= i\n return result",
19
+ ),
20
+ (
21
+ "Compute the nth Fibonacci number using iteration.",
22
+ "def fib(n):\n a, b = 0, 1\n for _ in range(n):\n a, b = b, a + b\n return a",
23
+ ),
24
+ (
25
+ "Check whether a given string is a palindrome, ignoring case.",
26
+ "def is_palindrome(s):\n s = s.lower()\n return s == s[::-1]",
27
+ ),
28
+ (
29
+ "Read a JSON file from disk and return the parsed dictionary.",
30
+ "import json\n\ndef read_json(path):\n with open(path) as f:\n return json.load(f)",
31
+ ),
32
+ (
33
+ "Return a list of unique elements preserving original order.",
34
+ "def dedupe(items):\n seen = set()\n out = []\n for x in items:\n if x not in seen:\n seen.add(x)\n out.append(x)\n return out",
35
+ ),
36
+ (
37
+ "Flatten a nested list of arbitrary depth into a single list.",
38
+ "def flatten(lst):\n out = []\n for x in lst:\n if isinstance(x, list):\n out.extend(flatten(x))\n else:\n out.append(x)\n return out",
39
+ ),
40
+ ]
41
+
42
+ # Rows that should be filtered out by the cleaning rules.
43
+ _DIRTY = [
44
+ ("", "def noop():\n pass"), # empty docstring
45
+ ("ok", "def f():\n return 1"), # too-short docstring
46
+ ("TODO: write this later", "def g():\n pass"), # blocklisted
47
+ ("auto-generated do not edit", "def h():\n pass"), # blocklisted
48
+ ("Returns x.", "x"), # too-short code
49
+ ("说明:返回输入值的两倍。", "def dbl(x):\n return x * 2"), # non-ascii doc
50
+ ]
51
+
52
+
53
+ def make_sample(n: int = 200, seed: int = 42) -> pd.DataFrame:
54
+ """Build a DataFrame with the CodeSearchNet columns we rely on."""
55
+ rng = random.Random(seed)
56
+ rows = []
57
+ for i in range(n):
58
+ # ~15% dirty rows so cleaning has work to do.
59
+ if rng.random() < 0.15:
60
+ doc, code = rng.choice(_DIRTY)
61
+ else:
62
+ doc, code = rng.choice(_GOOD)
63
+ rows.append(
64
+ {
65
+ "repository_name": f"acme/repo{i % 10}",
66
+ "func_path_in_repository": f"src/module_{i}.py",
67
+ "func_name": (code.split("(")[0].replace("def ", "").strip()
68
+ if code.startswith("def ") else f"sym_{i}"),
69
+ "language": "python",
70
+ "func_code_string": code,
71
+ "func_documentation_string": doc,
72
+ "func_code_url": f"https://github.com/acme/repo{i % 10}/blob/main/src/module_{i}.py",
73
+ }
74
+ )
75
+ return pd.DataFrame(rows)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ df = make_sample()
80
+ print(df.shape)
81
+ print(df.head(3).to_string())
src/eda/__init__.py ADDED
File without changes
src/eda/analyze.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 1c: exploratory data analysis.
2
+
3
+ Produces (a) a stats dict you can dump to JSON for the report, and
4
+ (b) PNG plots saved to the eda dir. Keep these in your capstone appendix.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import matplotlib
14
+
15
+ matplotlib.use("Agg") # headless backend for servers/CI
16
+ import matplotlib.pyplot as plt # noqa: E402
17
+ import pandas as pd # noqa: E402
18
+ import seaborn as sns # noqa: E402
19
+
20
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
21
+ from src.config import load_config # noqa: E402
22
+
23
+ sns.set_theme(style="whitegrid")
24
+ _WORD_RE = re.compile(r"\b\w+\b")
25
+
26
+
27
+ def _doc_words(s: str) -> int:
28
+ return len(_WORD_RE.findall(s))
29
+
30
+
31
+ def compute_stats(df: pd.DataFrame) -> dict:
32
+ doc_words = df["docstring"].map(_doc_words)
33
+ code_lines = df["code"].str.count("\n") + 1
34
+ code_chars = df["code"].str.len()
35
+ return {
36
+ "n_rows": int(len(df)),
37
+ "languages": df["language"].value_counts().to_dict(),
38
+ "docstring_words": {
39
+ "mean": round(float(doc_words.mean()), 2),
40
+ "median": int(doc_words.median()),
41
+ "p95": int(doc_words.quantile(0.95)),
42
+ "max": int(doc_words.max()),
43
+ },
44
+ "code_lines": {
45
+ "mean": round(float(code_lines.mean()), 2),
46
+ "median": int(code_lines.median()),
47
+ "p95": int(code_lines.quantile(0.95)),
48
+ "max": int(code_lines.max()),
49
+ },
50
+ "code_chars": {
51
+ "mean": round(float(code_chars.mean()), 2),
52
+ "median": int(code_chars.median()),
53
+ },
54
+ }
55
+
56
+
57
+ def make_plots(df: pd.DataFrame, out_dir: str, funnel: pd.DataFrame | None = None):
58
+ out = Path(out_dir)
59
+ out.mkdir(parents=True, exist_ok=True)
60
+ saved = []
61
+
62
+ # Docstring length distribution.
63
+ fig, ax = plt.subplots(figsize=(7, 4))
64
+ sns.histplot(df["docstring"].map(_doc_words), bins=40, ax=ax)
65
+ ax.set(title="Docstring length (words)", xlabel="words", ylabel="count")
66
+ p = out / "docstring_length.png"
67
+ fig.tight_layout(); fig.savefig(p, dpi=120); plt.close(fig); saved.append(str(p))
68
+
69
+ # Code length distribution (lines).
70
+ fig, ax = plt.subplots(figsize=(7, 4))
71
+ sns.histplot((df["code"].str.count("\n") + 1).clip(upper=80), bins=40, ax=ax)
72
+ ax.set(title="Code length (lines, clipped at 80)", xlabel="lines", ylabel="count")
73
+ p = out / "code_length.png"
74
+ fig.tight_layout(); fig.savefig(p, dpi=120); plt.close(fig); saved.append(str(p))
75
+
76
+ # Language distribution.
77
+ fig, ax = plt.subplots(figsize=(7, 4))
78
+ df["language"].value_counts().plot(kind="bar", ax=ax)
79
+ ax.set(title="Rows per language", xlabel="language", ylabel="count")
80
+ p = out / "language_distribution.png"
81
+ fig.tight_layout(); fig.savefig(p, dpi=120); plt.close(fig); saved.append(str(p))
82
+
83
+ # Cleaning funnel (if provided).
84
+ if funnel is not None:
85
+ fig, ax = plt.subplots(figsize=(7, 4))
86
+ ax.barh(funnel["step"], funnel["rows_remaining"])
87
+ ax.invert_yaxis()
88
+ ax.set(title="Cleaning funnel (rows remaining)", xlabel="rows")
89
+ p = out / "cleaning_funnel.png"
90
+ fig.tight_layout(); fig.savefig(p, dpi=120); plt.close(fig); saved.append(str(p))
91
+
92
+ return saved
93
+
94
+
95
+ def run_eda(df: pd.DataFrame, cfg=None, funnel: pd.DataFrame | None = None) -> dict:
96
+ cfg = cfg or load_config()
97
+ stats = compute_stats(df)
98
+ plots = make_plots(df, cfg.paths.eda_dir, funnel)
99
+ stats["plots"] = plots
100
+ with open(Path(cfg.paths.eda_dir) / "eda_stats.json", "w") as f:
101
+ json.dump(stats, f, indent=2)
102
+ return stats
103
+
104
+
105
+ if __name__ == "__main__":
106
+ from src.data.clean import clean
107
+ from src.data.load import load_raw
108
+
109
+ cfg = load_config()
110
+ cleaned, funnel = clean(load_raw(cfg), cfg)
111
+ print(json.dumps(run_eda(cleaned, cfg, funnel), indent=2))
src/eval/__init__.py ADDED
File without changes
src/eval/functional_eval.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 2: functional correctness eval (pass@k) on HumanEval / MBPP.
2
+
3
+ Unlike CodeBLEU (similarity), this measures whether generated code actually RUNS
4
+ and PASSES unit tests - the claim that carries a capstone defense.
5
+
6
+ generate_fn(intent) -> code is injected, so this is decoupled from the model and
7
+ unit-testable with a mock.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Callable
15
+
16
+ import numpy as np
17
+
18
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
19
+ from src.eval.sandbox import run_code # noqa: E402
20
+
21
+
22
+ def pass_at_k(n: int, c: int, k: int) -> float:
23
+ """Unbiased pass@k estimator (Codex paper). n samples, c correct."""
24
+ if n - c < k:
25
+ return 1.0
26
+ return 1.0 - float(np.prod(1.0 - k / np.arange(n - c + 1, n + 1)))
27
+
28
+
29
+ # ---- per-benchmark program builders ------------------------------------
30
+ def humaneval_program(problem, candidate_code: str) -> str:
31
+ # candidate_code already defines the full function (entry_point).
32
+ return f"{candidate_code}\n\n{problem['test']}\n\ncheck({problem['entry_point']})\n"
33
+
34
+
35
+ def mbpp_program(problem, candidate_code: str) -> str:
36
+ setup = problem.get("test_setup_code", "") or ""
37
+ tests = "\n".join(problem.get("test_list", []))
38
+ return f"{candidate_code}\n\n{setup}\n{tests}\n"
39
+
40
+
41
+ # hf_id is a tuple of candidate IDs tried in order; newer datasets versions
42
+ # require the namespaced form, older ones or mirrors may still use the bare id.
43
+ _BENCH = {
44
+ "humaneval": {
45
+ "hf_id": ("openai/openai_humaneval", "openai_humaneval"), "split": "test",
46
+ "intent_col": "prompt", "program": humaneval_program,
47
+ },
48
+ "mbpp": {
49
+ "hf_id": ("google-research-datasets/mbpp", "mbpp"), "split": "test",
50
+ "intent_col": "text", "program": mbpp_program,
51
+ },
52
+ }
53
+
54
+
55
+ @dataclass
56
+ class EvalResult:
57
+ benchmark: str
58
+ n_problems: int
59
+ n_samples: int
60
+ pass_at_1: float
61
+ pass_at_k: float
62
+ k: int
63
+
64
+
65
+ def evaluate(
66
+ generate_fn: Callable[[str], str],
67
+ benchmark: str = "humaneval",
68
+ limit: int | None = 20,
69
+ n_samples: int = 1,
70
+ k: int = 1,
71
+ timeout: float = 8.0,
72
+ ) -> EvalResult:
73
+ """Run pass@k. Use n_samples>1 + a sampling generate_fn for k>1."""
74
+ from datasets import load_dataset
75
+
76
+ spec = _BENCH[benchmark]
77
+ hf_ids = spec["hf_id"]
78
+ ds = None
79
+ for hf_id in hf_ids:
80
+ try:
81
+ ds = load_dataset(hf_id, split=spec["split"], trust_remote_code=True)
82
+ break
83
+ except Exception: # noqa: BLE001
84
+ continue
85
+ if ds is None:
86
+ raise RuntimeError(
87
+ f"Could not load benchmark '{benchmark}' from any of {hf_ids}. "
88
+ "Check your HuggingFace token and dataset availability."
89
+ )
90
+ if limit:
91
+ ds = ds.select(range(min(limit, len(ds))))
92
+
93
+ p1_scores, pk_scores = [], []
94
+ for problem in ds:
95
+ intent = problem[spec["intent_col"]]
96
+ correct = 0
97
+ for _ in range(n_samples):
98
+ code = generate_fn(intent)
99
+ program = spec["program"](problem, code)
100
+ if run_code(program, timeout=timeout).ok:
101
+ correct += 1
102
+ p1_scores.append(pass_at_k(n_samples, correct, 1))
103
+ pk_scores.append(pass_at_k(n_samples, correct, k))
104
+
105
+ return EvalResult(
106
+ benchmark=benchmark, n_problems=len(ds), n_samples=n_samples,
107
+ pass_at_1=round(float(np.mean(p1_scores)), 4),
108
+ pass_at_k=round(float(np.mean(pk_scores)), 4), k=k,
109
+ )
src/eval/retrieval_eval.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 3 eval: cross-modal retrieval quality (recall@k, MRR).
2
+
3
+ Design: N held-out (docstring, code) pairs form a closed candidate pool.
4
+ Each query docstring is ranked against all N code candidates; the paired
5
+ code is the positive and the other N-1 are distractors. This tests the
6
+ embedder's ability to bridge natural-language intent → code, without the
7
+ confound of looking up exact code that is already in the FAISS index.
8
+
9
+ ⚠️ Leakage caveat: CodeSearchNet's func_code_string includes the Python
10
+ docstring verbatim inside the function body (the triple-quoted string right
11
+ after `def`). Embedding the raw code therefore lets the embedder trivially
12
+ find the match via lexical overlap — recall@1 ≈ 0.96 is an artefact, NOT a
13
+ measure of true code understanding.
14
+
15
+ Call with strip_code_docstrings=True to remove triple-quoted strings and
16
+ # comments from candidate code before embedding. That number (~0.3-0.5
17
+ recall@1) reflects the embedder's actual semantic matching ability.
18
+
19
+ Usage (standalone):
20
+ python scripts/retrieval_only_eval.py
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ import numpy as np
29
+ import pandas as pd
30
+
31
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
32
+
33
+ # Matches the first triple-quoted string in a Python function body.
34
+ _TRIPLE_QUOTE_RE = re.compile(r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'')
35
+ _COMMENT_RE = re.compile(r'#[^\n]*')
36
+
37
+
38
+ def _strip_code_docstring(code: str) -> str:
39
+ """Remove the first triple-quoted docstring and all # comments from Python code."""
40
+ code = _TRIPLE_QUOTE_RE.sub('', code, count=1)
41
+ code = _COMMENT_RE.sub('', code)
42
+ return code
43
+
44
+
45
+ def evaluate_cross_modal(
46
+ embedder,
47
+ pairs: pd.DataFrame,
48
+ k_values: tuple[int, ...] = (1, 5, 10),
49
+ batch_size: int = 64,
50
+ strip_code_docstrings: bool = False,
51
+ ) -> dict:
52
+ """Cross-modal retrieval eval: docstring queries → code candidates.
53
+
54
+ Args:
55
+ embedder: SentenceTransformer (or anything with .encode()).
56
+ pairs: DataFrame with 'docstring' and 'code' columns (N rows).
57
+ k_values: Recall cut-offs to report.
58
+ batch_size: Encoding batch size.
59
+ strip_code_docstrings: If True, remove triple-quoted docstrings and #
60
+ comments from candidate code before embedding.
61
+ Use this for a leakage-free signal; see module
62
+ docstring for why the raw number is inflated.
63
+
64
+ Returns dict with keys mrr, recall@k (for each k), n_pairs, stripped.
65
+ """
66
+ n = len(pairs)
67
+
68
+ candidates = pairs["code"].tolist()
69
+ if strip_code_docstrings:
70
+ candidates = [_strip_code_docstring(c) for c in candidates]
71
+
72
+ print(f"[eval] encoding {n} docstrings as queries ...")
73
+ q_emb = embedder.encode(
74
+ pairs["docstring"].tolist(),
75
+ batch_size=batch_size, show_progress_bar=True,
76
+ convert_to_numpy=True, normalize_embeddings=True,
77
+ ).astype("float32")
78
+
79
+ print(f"[eval] encoding {n} code candidates"
80
+ f"{' (docstrings stripped)' if strip_code_docstrings else ''} ...")
81
+ c_emb = embedder.encode(
82
+ candidates,
83
+ batch_size=batch_size, show_progress_bar=True,
84
+ convert_to_numpy=True, normalize_embeddings=True,
85
+ ).astype("float32")
86
+
87
+ # Cosine similarity matrix (N × N); both sides are already L2-normalised,
88
+ # so inner product == cosine similarity.
89
+ sim = q_emb @ c_emb.T # shape (N, N)
90
+
91
+ reciprocal_ranks: list[float] = []
92
+ hits: dict[int, int] = {k: 0 for k in k_values}
93
+
94
+ for i in range(n):
95
+ order = sim[i].argsort()[::-1]
96
+ rank = int(np.where(order == i)[0][0]) + 1 # 1-indexed
97
+ reciprocal_ranks.append(1.0 / rank)
98
+ for k in k_values:
99
+ if rank <= k:
100
+ hits[k] += 1
101
+
102
+ result: dict = {
103
+ "mrr": round(float(np.mean(reciprocal_ranks)), 4),
104
+ "n_pairs": n,
105
+ "stripped": strip_code_docstrings,
106
+ }
107
+ for k in k_values:
108
+ result[f"recall@{k}"] = round(hits[k] / n, 4)
109
+ return result
src/eval/sandbox.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run untrusted generated code in a subprocess with a timeout.
2
+
3
+ NOTE: this is isolation-by-subprocess + timeout, NOT a real security sandbox.
4
+ It protects against hangs and lets us capture tracebacks for the repair loop and
5
+ pass@k scoring. For production / public deployment, run inside a container with
6
+ no network and dropped privileges (see Dockerfile + README security note).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+
17
+ @dataclass
18
+ class ExecResult:
19
+ ok: bool
20
+ stdout: str
21
+ stderr: str
22
+ timed_out: bool = False
23
+
24
+ @property
25
+ def error(self) -> str:
26
+ """Short error summary for feeding back into a repair prompt."""
27
+ if self.timed_out:
28
+ return "Execution timed out."
29
+ return self.stderr.strip().splitlines()[-1] if self.stderr.strip() else ""
30
+
31
+
32
+ def run_code(program: str, timeout: float = 8.0) -> ExecResult:
33
+ """Execute a full Python program string; return pass/fail + captured output."""
34
+ with tempfile.TemporaryDirectory() as d:
35
+ path = Path(d) / "candidate.py"
36
+ path.write_text(program)
37
+ try:
38
+ proc = subprocess.run(
39
+ [sys.executable, str(path)],
40
+ capture_output=True, text=True, timeout=timeout,
41
+ )
42
+ return ExecResult(ok=proc.returncode == 0, stdout=proc.stdout,
43
+ stderr=proc.stderr)
44
+ except subprocess.TimeoutExpired as e:
45
+ return ExecResult(ok=False, stdout=e.stdout or "", stderr="Timeout",
46
+ timed_out=True)
src/finetune/__init__.py ADDED
File without changes
src/finetune/train_codet5.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 4: fine-tune CodeT5+ on docstring -> code.
2
+
3
+ This is the second experimental arm (a tuned small model) to compare against
4
+ frozen-LLM + RAG. Runs on a single mid-range GPU; raise subset/epochs for the
5
+ real result.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import pandas as pd
13
+
14
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
15
+ from src.config import load_config # noqa: E402
16
+
17
+ CHECKPOINT = "Salesforce/codet5p-220m"
18
+
19
+
20
+ def finetune(subset_size: int = 5000, epochs: int = 1, out_dir: str = "data/codet5p-ft",
21
+ cfg=None):
22
+ from datasets import Dataset
23
+ from transformers import (AutoModelForSeq2SeqLM, AutoTokenizer,
24
+ DataCollatorForSeq2Seq, Seq2SeqTrainer,
25
+ Seq2SeqTrainingArguments)
26
+ import torch
27
+
28
+ cfg = cfg or load_config()
29
+ train_path = Path(cfg.paths.processed_dir) / "train.parquet"
30
+ df = pd.read_parquet(train_path).head(subset_size)
31
+
32
+ tok = AutoTokenizer.from_pretrained(CHECKPOINT)
33
+ model = AutoModelForSeq2SeqLM.from_pretrained(CHECKPOINT)
34
+
35
+ def to_features(batch):
36
+ x = tok(batch["docstring"], max_length=64, truncation=True, padding="max_length")
37
+ y = tok(text_target=batch["code"], max_length=256, truncation=True,
38
+ padding="max_length")
39
+ x["labels"] = y["input_ids"]
40
+ return x
41
+
42
+ ds = Dataset.from_pandas(df[["docstring", "code"]]).map(
43
+ to_features, batched=True, remove_columns=["docstring", "code"])
44
+
45
+ args = Seq2SeqTrainingArguments(
46
+ output_dir=out_dir, per_device_train_batch_size=8, num_train_epochs=epochs,
47
+ learning_rate=5e-5, logging_steps=50, save_strategy="epoch",
48
+ fp16=torch.cuda.is_available(), report_to="none")
49
+
50
+ trainer = Seq2SeqTrainer(
51
+ model=model, args=args, train_dataset=ds,
52
+ data_collator=DataCollatorForSeq2Seq(tok, model=model))
53
+ trainer.train()
54
+ trainer.save_model(out_dir)
55
+ tok.save_pretrained(out_dir)
56
+ print(f"[finetune] saved to {out_dir}")
57
+ return out_dir
58
+
59
+
60
+ def make_t5_generate_fn(model_dir: str):
61
+ """Return generate_fn(intent)->code for plugging a tuned CodeT5+ into eval."""
62
+ from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
63
+
64
+ tok = AutoTokenizer.from_pretrained(model_dir)
65
+ model = AutoModelForSeq2SeqLM.from_pretrained(model_dir)
66
+ model.eval()
67
+
68
+ def generate_fn(intent: str) -> str:
69
+ ids = tok(intent, return_tensors="pt", truncation=True, max_length=64).input_ids
70
+ out = model.generate(ids.to(model.device), max_length=256)
71
+ return tok.decode(out[0], skip_special_tokens=True)
72
+
73
+ return generate_fn
src/rag/__init__.py ADDED
File without changes
src/rag/embedder.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 3: build / save / load the FAISS retrieval index.
2
+
3
+ The index plus the corpus DataFrame are persisted so deployment doesn't rebuild
4
+ embeddings on every start (rebuilding is the slow part).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
15
+ from src.config import load_config # noqa: E402
16
+
17
+
18
+ class CodeIndex:
19
+ """Wraps a sentence-transformer embedder + a FAISS cosine index."""
20
+
21
+ def __init__(self, embed_model: str):
22
+ from sentence_transformers import SentenceTransformer
23
+
24
+ self.embed_model = embed_model
25
+ self.embedder = SentenceTransformer(embed_model)
26
+ self.index = None
27
+ self.corpus: pd.DataFrame | None = None
28
+
29
+ def build(self, corpus: pd.DataFrame, text_col: str = "docstring", batch_size: int = 64):
30
+ import faiss
31
+
32
+ self.corpus = corpus.reset_index(drop=True)
33
+ emb = self.embedder.encode(
34
+ self.corpus[text_col].tolist(),
35
+ batch_size=batch_size, show_progress_bar=True,
36
+ convert_to_numpy=True, normalize_embeddings=True,
37
+ ).astype("float32")
38
+ self.index = faiss.IndexFlatIP(emb.shape[1])
39
+ self.index.add(emb)
40
+ return self
41
+
42
+ def retrieve(self, query: str, k: int = 3) -> pd.DataFrame:
43
+ if self.index is None or self.corpus is None:
44
+ raise RuntimeError("Index not built/loaded. Call build() or load().")
45
+ q = self.embedder.encode(
46
+ [query], convert_to_numpy=True, normalize_embeddings=True
47
+ ).astype("float32")
48
+ scores, idx = self.index.search(q, k)
49
+ out = self.corpus.iloc[idx[0]].copy()
50
+ out["score"] = scores[0]
51
+ return out
52
+
53
+ def save(self, out_dir: str):
54
+ import faiss
55
+
56
+ out = Path(out_dir)
57
+ out.mkdir(parents=True, exist_ok=True)
58
+ faiss.write_index(self.index, str(out / "code.index"))
59
+ self.corpus.to_parquet(out / "corpus.parquet", index=False)
60
+ (out / "embed_model.txt").write_text(self.embed_model)
61
+ print(f"[index] saved to {out}")
62
+
63
+ @classmethod
64
+ def load(cls, in_dir: str) -> "CodeIndex":
65
+ import faiss
66
+
67
+ in_dir = Path(in_dir)
68
+ embed_model = (in_dir / "embed_model.txt").read_text().strip()
69
+ obj = cls(embed_model)
70
+ obj.index = faiss.read_index(str(in_dir / "code.index"))
71
+ obj.corpus = pd.read_parquet(in_dir / "corpus.parquet")
72
+ print(f"[index] loaded {obj.index.ntotal} vectors from {in_dir}")
73
+ return obj
74
+
75
+
76
+ def build_index_from_processed(cfg=None) -> CodeIndex:
77
+ """Build the index from data/processed/train.parquet."""
78
+ cfg = cfg or load_config()
79
+ train_path = Path(cfg.paths.processed_dir) / "train.parquet"
80
+ if not train_path.exists():
81
+ sys.exit("train.parquet missing. Run scripts/01_prepare_data.py first.")
82
+ corpus = pd.read_parquet(train_path)
83
+ idx = CodeIndex(cfg.models.embed_model).build(corpus)
84
+ idx.save(cfg.paths.index_dir)
85
+ return idx
src/rag/generator.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 5: the CodeAssistant service - the heart of the deployable app.
2
+
3
+ Wraps a code LLM + an optional retrieval index and exposes:
4
+ - generate(intent, mode="baseline"|"rag")
5
+ - the prompt builders, so eval/agent code can reuse them.
6
+
7
+ Designed to be imported by the FastAPI / Gradio / Streamlit front-ends so all
8
+ surfaces share one implementation.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ sys.path.append(str(Path(__file__).resolve().parents[2]))
17
+ from src.config import load_config # noqa: E402
18
+ from src.rag.embedder import CodeIndex # noqa: E402
19
+
20
+ SYSTEM_PROMPT = (
21
+ "You are an expert Python coding assistant. Write a single, correct, "
22
+ "self-contained Python function for the request. Output only code."
23
+ )
24
+
25
+ _FENCE_RE = re.compile(r"```(?:python)?\n(.*?)```", re.DOTALL)
26
+
27
+
28
+ def extract_code(text: str) -> str:
29
+ """Strip markdown fences if the model wrapped its answer."""
30
+ m = _FENCE_RE.search(text)
31
+ return m.group(1).strip() if m else text.strip()
32
+
33
+
34
+ class CodeAssistant:
35
+ def __init__(self, gen_model: str, index: CodeIndex | None = None,
36
+ top_k: int = 3, device_map: str = "auto"):
37
+ from transformers import AutoModelForCausalLM, AutoTokenizer
38
+
39
+ self.gen_model = gen_model
40
+ self.index = index
41
+ self.top_k = top_k
42
+ self.tok = AutoTokenizer.from_pretrained(gen_model)
43
+ self.model = AutoModelForCausalLM.from_pretrained(
44
+ gen_model, dtype="auto", device_map=device_map
45
+ )
46
+
47
+ # ---- prompt builders ------------------------------------------------
48
+ def baseline_messages(self, intent: str):
49
+ return [{"role": "system", "content": SYSTEM_PROMPT},
50
+ {"role": "user", "content": f"# Task: {intent}"}]
51
+
52
+ def rag_messages(self, intent: str, k: int | None = None):
53
+ if self.index is None:
54
+ return self.baseline_messages(intent)
55
+ ex = self.index.retrieve(intent, k or self.top_k)
56
+ blocks = [f"# Task: {r.docstring}\n{r.code}" for _, r in ex.iterrows()]
57
+ context = "\n\n".join(blocks)
58
+ user = (f"Here are similar reference examples:\n\n{context}\n\n"
59
+ f"# Now write a function for this task:\n# Task: {intent}")
60
+ return [{"role": "system", "content": SYSTEM_PROMPT},
61
+ {"role": "user", "content": user}]
62
+
63
+ # ---- generation -----------------------------------------------------
64
+ def _generate(self, messages, max_new_tokens=320, temperature=0.0):
65
+ text = self.tok.apply_chat_template(
66
+ messages, tokenize=False, add_generation_prompt=True)
67
+ inputs = self.tok(text, return_tensors="pt").to(self.model.device)
68
+ do_sample = temperature and temperature > 0
69
+ kwargs = dict(max_new_tokens=max_new_tokens, do_sample=do_sample,
70
+ pad_token_id=self.tok.eos_token_id)
71
+ if do_sample:
72
+ kwargs["temperature"] = temperature
73
+ out = self.model.generate(**inputs, **kwargs)
74
+ new = out[0][inputs.input_ids.shape[1]:]
75
+ return self.tok.decode(new, skip_special_tokens=True)
76
+
77
+ def generate(self, intent: str, mode: str = "rag", max_new_tokens=320,
78
+ temperature=0.0, return_sources=False):
79
+ msgs = self.rag_messages(intent) if mode == "rag" else self.baseline_messages(intent)
80
+ code = extract_code(self._generate(msgs, max_new_tokens, temperature))
81
+ if return_sources and mode == "rag" and self.index is not None:
82
+ srcs = self.index.retrieve(intent, self.top_k)[["docstring", "score"]]
83
+ return code, srcs.to_dict("records")
84
+ return code
85
+
86
+ @classmethod
87
+ def from_config(cls, cfg=None, with_index: bool = True) -> "CodeAssistant":
88
+ cfg = cfg or load_config()
89
+ index = None
90
+ if with_index:
91
+ idx_dir = Path(cfg.paths.index_dir)
92
+ if (idx_dir / "code.index").exists():
93
+ index = CodeIndex.load(str(idx_dir))
94
+ else:
95
+ print("[assistant] no saved index found; running baseline-only.")
96
+ return cls(cfg.models.gen_model, index=index, top_k=cfg.models.top_k)