Spaces:
Paused
Paused
Download sensorflow/hillclimb/evaluate.py from whassan/SensorflowStudio: direct link, hf CLI and curl.
- Browser
- Download file 8.15 kB
-
https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/hillclimb/evaluate.py
- Command line
-
hf download hf://spaces/whassan/SensorflowStudio/sensorflow/hillclimb/evaluate.py
-
curl -L -o evaluate.py https://huggingface.co/spaces/whassan/SensorflowStudio/resolve/main/sensorflow/hillclimb/evaluate.py
8.15 kB
| """Evaluation engine: rubric-driven, evidence-based scoring of free-text answers. | |
| Rules enforced here: | |
| - NO SCORE WITHOUT EVIDENCE: every score above 1 must reference specific user | |
| statements (quotes extracted from the answer). No evidence => score 1. | |
| - ANTI-GAMING: score derives ONLY from concept coverage, tradeoff mentions and | |
| quantified results — never from answer length. A long waffle that covers no | |
| rubric concepts scores below a short precise answer. | |
| - LLM output (when available) must validate against the EvaluationResult | |
| contract AND its evidence quotes must actually appear in the user's answer; | |
| otherwise the deterministic rule-based result is used. An LLM may never | |
| raise the score more than one point above the rule-based floor. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import re | |
| from typing import List, Optional, Tuple | |
| from sensorflow.hillclimb import coach, llm | |
| from sensorflow.hillclimb.content import Exercise, RubricItem | |
| from sensorflow.hillclimb.models import EvaluationResult | |
| QUANT_RE = re.compile( | |
| r"\d+(?:\.\d+)?\s*(?:%|percent|ms\b|s\b|x\b|qps\b|rps\b|fps\b|gb\b|tb\b|pb\b|" | |
| r"days?\b|hours?\b|weeks?\b|engineers?\b|gpus?\b|nodes?\b|replicas?\b|frames?\b)|" | |
| r"\bfrom\s+\d+(?:\.\d+)?\s+to\s+\d+(?:\.\d+)?|\bp9[059]\b|\d+(?:\.\d+)?\s*sigma", | |
| re.IGNORECASE, | |
| ) | |
| TRADEOFF_MARKERS = [ | |
| "trade-off", "tradeoff", "trade off", "at the cost of", "sacrific", "downside", | |
| "at the expense", " versus ", " vs ", "on the other hand", "the cost is", | |
| "we give up", "gives up", "in exchange", "instead of", "weighed against", | |
| ] | |
| MISCONCEPTION_PATTERNS: List[Tuple[str, str]] = [ | |
| (r"accuracy\s+is\s+the\s+best|just\s+use\s+accuracy", | |
| "Accuracy is a poor headline metric for imbalanced detection problems; prefer precision/recall/AP."), | |
| (r"average\s+latency\s+is\s+(fine|enough|what matters)", | |
| "Serving systems are judged by tail latency (p99), not the average."), | |
| (r"more\s+data\s+always\s+(helps|fixes|improves)", | |
| "More data does not fix distribution mismatch, label noise, or serving skew."), | |
| (r"offline\s+(metrics?|eval(uation)?)\s+(is|are)\s+ground\s+truth", | |
| "Offline evaluation is a proxy under its own sampling; it is not ground truth for live behavior."), | |
| (r"definitely\s+a\s+(true\s+)?regression|certainly\s+regressed", | |
| "Claiming certainty before significance testing / measurement validation is premature."), | |
| ] | |
| def split_sentences(text: str) -> List[str]: | |
| parts = re.split(r"(?<=[.!?])\s+|\n+", text.strip()) | |
| return [p.strip() for p in parts if p.strip()] | |
| def _clip(sentence: str, limit: int = 220) -> str: | |
| return sentence if len(sentence) <= limit else sentence[:limit].rsplit(" ", 1)[0] + "…" | |
| def _find_sentence(sentences: List[str], predicate) -> Optional[str]: | |
| for s in sentences: | |
| if predicate(s): | |
| return s | |
| return None | |
| def _criterion_evidence(item: RubricItem, answer_lower: str, sentences: List[str]) -> Optional[str]: | |
| """Return an evidence quote (user's own sentence) if the criterion is met.""" | |
| if item.check == "quantified": | |
| hit = _find_sentence(sentences, lambda s: bool(QUANT_RE.search(s))) | |
| return hit | |
| if item.check == "tradeoff": | |
| return _find_sentence(sentences, lambda s: any(m in s.lower() for m in TRADEOFF_MARKERS)) | |
| for kw in item.keywords: | |
| if kw.lower() in answer_lower: | |
| hit = _find_sentence(sentences, lambda s: kw.lower() in s.lower()) | |
| if hit: | |
| return hit | |
| return None | |
| def rule_based_evaluate(exercise: Exercise, answer: str) -> EvaluationResult: | |
| sentences = split_sentences(answer) | |
| answer_lower = answer.lower() | |
| covered: List[Tuple[RubricItem, str]] = [] | |
| missed: List[RubricItem] = [] | |
| evidence: List[str] = [] | |
| for item in exercise.evaluation_rubric: | |
| quote = _criterion_evidence(item, answer_lower, sentences) | |
| if quote: | |
| covered.append((item, quote)) | |
| clipped = _clip(quote) | |
| if clipped not in evidence: | |
| evidence.append(clipped) | |
| else: | |
| missed.append(item) | |
| total_weight = sum(i.weight for i in exercise.evaluation_rubric) or 1.0 | |
| covered_weight = sum(i.weight for i, _ in covered) | |
| coverage = covered_weight / total_weight | |
| # Coverage → score. Deliberately independent of answer length. | |
| if coverage >= 0.85: | |
| score = 5 | |
| elif coverage >= 0.65: | |
| score = 4 | |
| elif coverage >= 0.45: | |
| score = 3 | |
| elif coverage >= 0.20: | |
| score = 2 | |
| else: | |
| score = 1 | |
| # No score without evidence. | |
| if not evidence: | |
| score = 1 | |
| misconceptions = [msg for pat, msg in MISCONCEPTION_PATTERNS if re.search(pat, answer_lower)] | |
| strengths = [f"{i.criterion} — \"{_clip(q, 140)}\"" for i, q in covered[:5]] | |
| weaknesses = [i.criterion for i in missed if i.check == "keywords"][:5] | |
| missing_evidence = [] | |
| for i in missed: | |
| if i.check == "quantified": | |
| missing_evidence.append("No quantified claims: no numbers, thresholds, or before/after comparisons anywhere in the answer.") | |
| elif i.check == "tradeoff": | |
| missing_evidence.append("No explicit tradeoff stated: nothing about what your approach sacrifices.") | |
| else: | |
| missing_evidence.append(f"Not addressed: {i.criterion.lower()}.") | |
| confidence = round(min(0.9, 0.35 + 0.07 * len(covered)), 2) | |
| result = EvaluationResult( | |
| competency=exercise.competency_id, | |
| score=score, | |
| confidence=confidence, | |
| evidence=evidence, | |
| strengths=strengths, | |
| weaknesses=weaknesses, | |
| missing_evidence=missing_evidence[:6], | |
| misconceptions=misconceptions, | |
| evaluator="rule_based", | |
| ) | |
| result.recommended_action = coach.recommended_action(exercise, result) | |
| result.follow_up_question = coach.pick_follow_up(exercise, result) | |
| return result | |
| def _normalize(s: str) -> str: | |
| return re.sub(r"\s+", " ", s.lower()).strip(" .\"'…") | |
| def _llm_result_valid(candidate: EvaluationResult, answer: str) -> bool: | |
| """LLM evidence must quote the user's actual statements.""" | |
| if not candidate.evidence: | |
| return False | |
| normalized_answer = _normalize(answer) | |
| for quote in candidate.evidence: | |
| if _normalize(quote) not in normalized_answer: | |
| return False | |
| return True | |
| def evaluate_response(exercise: Exercise, answer: str, use_llm: bool = True) -> EvaluationResult: | |
| rule_result = rule_based_evaluate(exercise, answer) | |
| if not (use_llm and llm.llm_enabled()): | |
| return rule_result | |
| rubric_lines = "\n".join(f"- {i.criterion}" for i in exercise.evaluation_rubric) | |
| prompt = ( | |
| "You are grading an engineering-manager interview answer against a rubric.\n" | |
| f"SCENARIO:\n{exercise.scenario}\n\nRUBRIC:\n{rubric_lines}\n\n" | |
| f"ANSWER:\n{answer}\n\n" | |
| "Respond with ONLY a JSON object with keys: competency (string), score (int 1-5), " | |
| "confidence (0-1), evidence (array of EXACT quotes copied verbatim from the answer), " | |
| "strengths (array), weaknesses (array), missing_evidence (array), misconceptions (array), " | |
| "recommended_action (string), follow_up_question (string). " | |
| "Every point awarded must be justified by a quote in evidence. Do not reward verbosity." | |
| ) | |
| candidate = llm.generate_json(prompt, EvaluationResult, timeout=25.0) | |
| if candidate is None or not _llm_result_valid(candidate, answer): | |
| return rule_result | |
| candidate.competency = exercise.competency_id | |
| candidate.evaluator = "llm" | |
| # Anti-gaming guard: the LLM may refine, but never inflate more than one | |
| # point above the deterministic evidence-based floor. | |
| if candidate.score > rule_result.score + 1: | |
| candidate.score = rule_result.score + 1 | |
| if not candidate.recommended_action: | |
| candidate.recommended_action = coach.recommended_action(exercise, candidate) | |
| if not candidate.follow_up_question: | |
| candidate.follow_up_question = coach.pick_follow_up(exercise, candidate) | |
| return candidate | |