File size: 3,537 Bytes
1d9bd9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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"  # fail-safe: no prior

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()