amul-ai-eval / scripts /build_kcc_testcases.py
bpHigh's picture
HF Space: add charts tab
74e6b83
Raw
History Blame Contribute Delete
5.83 kB
"""Convert a curated KCC Animal Husbandry slice into CeRAI DataPoints.json.
KCC is field data: questions and answers are often in Romanized Hindi /
mixed-script — *we keep those rows*, that's actually closer to how a farmer
would phrase a query to Amul AI. We do strip out:
- rows whose answer is essentially a phone number (very common — "call
1800-180-5141 for cattle queries")
- empty / single-word queries
- non-dairy/animal-husbandry rows (broad keyword filter, English + romanized
Hindi terms)
Reads: data/processed/kcc_animal_husbandry.parquet
Writes: data/processed/kcc_dairy_datapoints.json
data/processed/kcc_dairy_plan.json
Usage:
python scripts/build_kcc_testcases.py --max-rows 20
"""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
import pandas as pd
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "data" / "processed" / "kcc_animal_husbandry.parquet"
OUT_DATAPOINTS = ROOT / "data" / "processed" / "kcc_dairy_datapoints.json"
OUT_PLAN = ROOT / "data" / "processed" / "kcc_dairy_plan.json"
# Broad dairy / animal-husbandry vocabulary spanning English + romanized Hindi.
# `\b` word boundaries so we don't false-match substrings.
KEYWORDS = re.compile(
r"\b("
# English
r"cow|cows|bull|bulls|buffalo|buffaloes|calf|calves|cattle|bovine|"
r"dairy|milk|milking|milker|udder|teat|mastitis|colostrum|"
r"lactation|heifer|holstein|jersey|gir|sahiwal|murrah|"
r"ghee|curd|butterfat|fodder|silage|chara|deworming|insemination|ai|"
r"fmd|brucellosis|theileria|hoof|"
# Romanized Hindi / common KCC terms
r"pasu|pashu|gay|gaay|bhains|bhainsh|doodh|dudh|dudhha|dugdh|"
r"bachhda|bachhra|bachra|bachhdi|"
r"thaan|thanela|paseena|thanaila|"
r"ghas|charae|chare|charaa|"
r"hara|sukha|"
r"khurpaka|munhpaka|"
r"sankarn|sangraman|sankran|"
r"khali|khaali"
r")\b",
re.IGNORECASE,
)
# Match if the answer is mostly digits/punctuation/whitespace — i.e. a bare
# phone number, an extension code, etc.
PHONE_ONLY = re.compile(r"^[\s\d\-+()/.,]*$")
# Match an isolated phone number anywhere — KCC has lots of "call XXXXXXX"
# answers; we drop those too.
HAS_LONG_NUMBER = re.compile(r"\b\d{6,}\b")
METRICS_AND_STRATEGY: dict[str, tuple[str, list[str]]] = {
"14": ("Lexical_Diversity", ["18"]),
"38": ("BLEU", ["42"]),
"39": ("ROUGE", ["42"]),
"40": ("METEOR", ["42"]),
"41": ("Turn_Around_Time", ["25"]),
"47": ("Error_Rate", ["13"]),
}
METRICS = {met_id: meta[0] for met_id, meta in METRICS_AND_STRATEGY.items()}
SYSTEM_PROMPT = (
"You are an expert assistant for Indian dairy and animal-husbandry farmers. "
"The farmer may write in English, Hindi, or romanized Hindi (Hinglish). "
"Answer the question accurately and concisely."
)
def _looks_like_real_answer(ans: str) -> bool:
a = ans.strip()
if len(a) < 30:
return False
if PHONE_ONLY.match(a):
return False
if HAS_LONG_NUMBER.search(a):
return False
if " " not in a:
return False
return True
def _looks_like_real_query(q: str) -> bool:
return len(q.strip()) >= 8
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--max-rows", type=int, default=20)
parser.add_argument("--seed", type=int, default=11)
args = parser.parse_args()
df = pd.read_parquet(SRC)
n0 = len(df)
df = df.dropna(subset=["QueryText", "KccAns"])
df = df[df["QueryText"].apply(_looks_like_real_query)]
df = df[df["KccAns"].apply(_looks_like_real_answer)]
df = df[df["QueryText"].astype(str).str.contains(KEYWORDS, regex=True)
| df["KccAns"].astype(str).str.contains(KEYWORDS, regex=True)]
df = df.drop_duplicates(subset=["QueryText"]).reset_index(drop=True)
print(f"From {n0:,} rows ➜ {len(df):,} after filters (dairy-relevant, "
f"non-phone answer, dedup)")
if len(df) == 0:
raise SystemExit("filter is too strict; relax KEYWORDS or thresholds.")
df = df.sample(min(args.max_rows, len(df)), random_state=args.seed).reset_index(drop=True)
base_cases = []
for i, row in df.iterrows():
q = str(row["QueryText"]).strip()
a = str(row["KccAns"]).strip()
base_cases.append({
"PROMPT_ID": f"KCC-AH-{i:04d}",
"LLM_AS_JUDGE": "No",
"SYSTEM_PROMPT": SYSTEM_PROMPT,
"PROMPT": q,
"EXPECTED_OUTPUT": a,
"DOMAIN": "animal_husbandry",
})
# One testcase row per (prompt, metric) — see the long comment in
# build_cerai_testcases.py. Same prompt across metric copies, distinct
# strategy per copy. Executor's cache dedupes API calls by user_prompt.
datapoints: dict[str, dict] = {}
for met_id, (_, strategy_ids) in METRICS_AND_STRATEGY.items():
suffixed = []
for case in base_cases:
c = dict(case)
c["PROMPT_ID"] = f"{case['PROMPT_ID']}-M{met_id}"
c["STRATEGY"] = strategy_ids
suffixed.append(c)
datapoints[met_id] = {"cases": suffixed}
plan = {
"T_AmulKCC": {
"TestPlan_name": "Amul_KCC_AnimalHusbandry",
"metrics": METRICS,
}
}
OUT_DATAPOINTS.write_text(json.dumps(datapoints, indent=2, ensure_ascii=False))
OUT_PLAN.write_text(json.dumps(plan, indent=2, ensure_ascii=False))
print(f"Wrote {OUT_DATAPOINTS} ({len(base_cases)} base cases x {len(METRICS)} metrics)")
print(f"Wrote {OUT_PLAN}")
print()
print("--- 4 sample cases ---")
for c in base_cases[:4]:
print(f"Q: {c['PROMPT']}")
print(f"A: {c['EXPECTED_OUTPUT']}")
print()
if __name__ == "__main__":
main()