| """Round-0 intent classifier (the 'router' as a state-setter). Labels each query AUTHORITY |
| (wants the leading/landmark case on a doctrine -> apply the authority prior) vs SPECIFIC (a |
| particular case / fact-pattern / narrow holding -> no prior). DeepSeek, parallel, cached to JSON. |
| |
| Usage: THEMIS_QFILE=authority_queries.tsv OUT=intent_authority.json python classify_intent.py |
| Reads .env for DEEPSEEK_API_KEY. Cached by qid so re-runs are free; delete the OUT file to refresh. |
| """ |
| import os, json, sys, time, concurrent.futures as cf |
| import requests |
|
|
| HERE = os.path.dirname(os.path.abspath(__file__)) |
| def _load_env(p): |
| if os.path.exists(p): |
| for l in open(p): |
| l = l.strip() |
| if l and not l.startswith("#") and "=" in l: |
| k, v = l.split("=", 1); os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) |
| _load_env(os.path.join(HERE, "..", "scripts", ".env")) |
| KEY = os.environ["DEEPSEEK_API_KEY"] |
| HDR = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"} |
| QFILE = os.environ.get("THEMIS_QFILE", "authority_queries.tsv") |
| OUT = os.environ.get("OUT", "intent.json") |
|
|
| SYS = ("You route a legal search query for an Indian Supreme Court case-law engine. " |
| "Decide what the user is after:\n" |
| "AUTHORITY = they want the leading / landmark / controlling case(s) on a legal PRINCIPLE, " |
| "doctrine, right, or test (e.g. 'is privacy a fundamental right', 'test for sedition', " |
| "'doctrine of basic structure').\n" |
| "SPECIFIC = they want a particular named case, a narrow fact-pattern match, a specific " |
| "statutory provision's application, or a procedural/factual lookup where the single most " |
| "authoritative landmark is NOT necessarily the right answer.\n" |
| "Reply with EXACTLY one word: AUTHORITY or SPECIFIC.") |
|
|
| def classify(text): |
| for attempt in range(3): |
| try: |
| r = requests.post("https://api.deepseek.com/chat/completions", headers=HDR, timeout=40, |
| json={"model": "deepseek-chat", "temperature": 0, "max_tokens": 4, |
| "messages": [{"role": "system", "content": SYS}, {"role": "user", "content": text}]}) |
| if r.status_code == 200: |
| t = r.json()["choices"][0]["message"]["content"].strip().upper() |
| return "AUTHORITY" if "AUTHORITY" in t else "SPECIFIC" |
| except Exception: |
| time.sleep(2 * (attempt + 1)) |
| return "SPECIFIC" |
|
|
| def main(): |
| rows = [] |
| for l in open(QFILE, encoding="utf-8"): |
| qid, intent, text = l.rstrip("\n").split("\t", 2); rows.append((qid, text)) |
| cache = json.load(open(OUT)) if os.path.exists(OUT) else {} |
| todo = [(qid, text) for qid, text in rows if qid not in cache] |
| print(f"{len(rows)} queries, {len(todo)} to classify ({len(cache)} cached)", flush=True) |
| t0 = time.time() |
| with cf.ThreadPoolExecutor(max_workers=24) as ex: |
| futs = {ex.submit(classify, text): qid for qid, text in todo} |
| done = 0 |
| for f in cf.as_completed(futs): |
| cache[futs[f]] = f.result(); done += 1 |
| if done % 50 == 0: |
| json.dump(cache, open(OUT, "w")); print(f" {done}/{len(todo)} {time.time()-t0:.0f}s", flush=True) |
| json.dump(cache, open(OUT, "w")) |
| n_auth = sum(1 for v in cache.values() if v == "AUTHORITY") |
| print(f"done {len(cache)} -> {OUT} | AUTHORITY={n_auth} ({100*n_auth/len(cache):.0f}%) SPECIFIC={len(cache)-n_auth}", flush=True) |
|
|
| if __name__ == "__main__": |
| main() |
|
|