File size: 6,478 Bytes
481fbb6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/env python3
"""Generate high-quality, judgment-shaped "Tech Lead" QA from REAL GitHub
data (SWE-bench: real issues + fix patches + test patches + PR discussion),
via OpenRouter google/gemini-3.6-flash.

Design decisions baked in from this session's analysis:
- Targets are TIER-A/B *judgment* QA only (why / architecture / data-flow /
  contracts / conventions / impact-NARRATIVE), never Tier-C exact-recall
  ("list the precise files+lines") which a LoRA can only hallucinate.
- Answers are SHORT (a phrase/sentence) so they're compressible into a
  weight-delta and gradeable.
- Each example is keyed to doc_id = "{repo}@{base_commit[:10]}" so it can
  later be tied to a repo-state embedding for hypernetwork training.
- gemini-3.6-flash is a REASONING model: reasoning is mandatory and cannot
  be disabled, and it consumes completion tokens BEFORE content -- so
  max_tokens must be generous (default 3500) or content comes back empty.

Usage:
  python scripts/generate_techlead_qa.py --limit 3          # smoke test
  python scripts/generate_techlead_qa.py --max-instances 1200
"""
from __future__ import annotations
import argparse, hashlib, json, os, re, sys, time
from pathlib import Path
from typing import Dict, List
from openai import OpenAI

HERE = Path(__file__).resolve().parent; REPO_ROOT = HERE.parent
sys.path.insert(0, str(REPO_ROOT))
from memory_lora.data_paths import QNA_DIR, DOCS_DIR, CACHE_DIR, ensure_dirs  # noqa: E402

def _load_dotenv(p: Path):
    if p.exists():
        for line in p.read_text().splitlines():
            if "=" in line and not line.strip().startswith("#"):
                k, v = line.split("=", 1); os.environ.setdefault(k.strip(), v.strip())
_load_dotenv(REPO_ROOT / ".env")

MODEL = "google/gemini-3.6-flash"
BASE_URL = "https://openrouter.ai/api/v1"

SYSTEM = (
 "You are a staff engineer writing onboarding Q&A about a real code change. "
 "Given an issue, its fix patch, the test patch, and any discussion, produce "
 "a JSON array of 5-8 objects with keys 'aspect', 'question', 'answer'. "
 "aspect must be one of: architecture, data_flow, why, contracts, conventions, "
 "impact. Rules: (1) Questions test JUDGMENT and UNDERSTANDING, not memorized "
 "trivia. (2) Answers are SHORT -- one phrase or one sentence, copyable, no "
 "lists of file paths or line numbers. (3) 'why' questions explain the "
 "rationale/trade-off. 'impact' questions describe the KIND of thing affected "
 "(a behavior, a contract), NOT an exact file enumeration. (4) Ground every "
 "answer in the provided change; do not invent APIs. Output ONLY the JSON array."
)

def cache_key(*parts) -> str:
    return hashlib.sha256("||".join(parts).encode()).hexdigest()[:24]

def gen_qa(client, ctx: str, cache_dir: Path, retries=4) -> List[Dict]:
    key = cache_key(MODEL, ctx)
    cf = cache_dir / f"tlqa_{key}.json"
    if cf.exists():
        raw = cf.read_text()
    else:
        last = None
        for a in range(retries):
            try:
                r = client.chat.completions.create(
                    model=MODEL,
                    messages=[{"role": "system", "content": SYSTEM},
                              {"role": "user", "content": ctx}],
                    max_tokens=3500, temperature=0.6)
                raw = r.choices[0].message.content or ""
                cf.write_text(raw); break
            except Exception as e:  # noqa: BLE001
                last = e; time.sleep(2 ** a)
        else:
            print(f"    [warn] failed: {last}", flush=True); return []
    m = re.search(r"\[.*\]", raw, re.DOTALL)
    if not m: return []
    try:
        items = json.loads(m.group(0))
    except json.JSONDecodeError:
        return []
    out = []
    for it in items:
        q = (it.get("question") or "").strip()
        a = (it.get("answer") or "").strip()
        asp = (it.get("aspect") or "general").strip()
        if q and a and len(a) < 240:
            out.append({"aspect": asp, "question": q, "answer": a})
    return out

def build_ctx(row) -> str:
    def clip(s, n): return (s or "")[:n]
    return (f"REPO: {row['repo']}\n"
            f"ISSUE:\n{clip(row.get('problem_statement'),1500)}\n\n"
            f"DISCUSSION:\n{clip(row.get('hints_text'),800)}\n\n"
            f"FIX PATCH:\n{clip(row.get('patch'),2500)}\n\n"
            f"TEST PATCH:\n{clip(row.get('test_patch'),1200)}")

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--max-instances", type=int, default=1200)
    ap.add_argument("--limit", type=int, default=0, help="smoke-test cap")
    ap.add_argument("--split", default="test")
    args = ap.parse_args()
    ensure_dirs()
    api_key = os.environ.get("OPENROUTER_API_KEY")
    if not api_key: raise SystemExit("OPENROUTER_API_KEY not set")
    client = OpenAI(base_url=BASE_URL, api_key=api_key)

    from datasets import load_dataset
    ds = load_dataset("princeton-nlp/SWE-bench", split=args.split, streaming=True)

    n_max = args.limit if args.limit else args.max_instances
    qna_f = (QNA_DIR / "techlead_qa.jsonl").open("a")
    src_f = (DOCS_DIR / "techlead_sources.jsonl").open("a")
    n_docs = n_qa = 0
    t0 = time.time()
    for row in ds:
        if n_docs >= n_max: break
        doc_id = f"{row['repo']}@{row['base_commit'][:10]}"
        ctx = build_ctx(row)
        qas = gen_qa(client, ctx, CACHE_DIR)
        if not qas: continue
        # store the source context so a repo-state embedding can be built later
        src_f.write(json.dumps({"doc_id": doc_id, "repo": row["repo"],
                                "base_commit": row["base_commit"], "context": ctx}) + "\n")
        for qa in qas:
            qna_f.write(json.dumps({
                "doc_id": doc_id, "doc_version": row["base_commit"],
                "split": "train", "qna_split": "train",
                "aspect": qa["aspect"], "question": qa["question"],
                "prefix": f"Q: {qa['question']}\nA:", "target": " " + qa["answer"],
            }) + "\n")
            n_qa += 1
        n_docs += 1
        if n_docs % 10 == 0:
            rate = n_docs / max(1e-9, (time.time() - t0) / 60)
            print(f"  {n_docs} instances, {n_qa} QA ({rate:.1f} inst/min) latest={doc_id}", flush=True)
    qna_f.close(); src_f.close()
    print(f"\nDone. {n_docs} real instances -> {n_qa} judgment-QA pairs.", flush=True)
    print(f"  {QNA_DIR/'techlead_qa.jsonl'}\n  {DOCS_DIR/'techlead_sources.jsonl'}", flush=True)

if __name__ == "__main__":
    main()