ishan-25 commited on
Commit
092590b
Β·
verified Β·
1 Parent(s): 06c2eba

Update scorer.py to be smarter in scoring

Browse files
Files changed (1) hide show
  1. agents/scorer.py +89 -130
agents/scorer.py CHANGED
@@ -1,46 +1,17 @@
1
  """
2
  agents/scorer.py
3
  ────────────────────────────────────────────────────────────────────────────
4
- ScorerAgent: Keyword-aware answer scoring β€” implements Agenda Item #4.
5
-
6
- The core innovation over Phase 1:
7
- 1. LOCAL keyword hit check (instant, deterministic, no LLM call needed)
8
- - Checks which expected job keywords appear in the candidate's answer
9
- - Computes coverage_pct = hits / total_keywords * 100
10
- 2. LLM scoring prompt is dynamically AUGMENTED with:
11
- - The keyword coverage result
12
- - An explicit cap instruction if coverage < 40%
13
- - A list of missed keywords to guide the Weakness/Fix feedback
14
- 3. Returns a structured dict with raw feedback AND parsed fields
15
- for the UI to render keyword coverage badges.
16
-
17
- Scoring rubric enforced by prompt:
18
- - STAR format structure (encouraged, not hard-gated)
19
- - Keyword coverage (hard gate: <40% β†’ score capped at 5/10)
20
- - Relevance check (irrelevant responses β†’ NIL/10)
21
  """
22
 
 
23
 
24
  class ScorerAgent:
25
  """
26
- Scores a candidate's interview answer against job-profile expectations.
27
-
28
- Usage:
29
- agent = ScorerAgent(llm_fn)
30
- result = agent.run(answer, question, job_profile)
31
- # result: {
32
- # "raw_feedback": str, # Full LLM output for display
33
- # "score_str": str, # e.g. "7/10" or "NIL/10"
34
- # "numeric_score": float, # e.g. 7.0 or None
35
- # "hit_keywords": list, # keywords found in answer
36
- # "missed_keywords": list, # keywords not found
37
- # "coverage_pct": float, # 0.0 – 100.0
38
- # "star_hint": bool, # True if answer has weak STAR structure
39
- # }
40
  """
41
-
42
  _MIN_ANSWER_LEN = 15
43
- _COVERAGE_THRESHOLD = 40.0 # % β€” below this, score is capped at 5
44
 
45
  def __init__(self, llm_fn):
46
  """
@@ -52,15 +23,7 @@ class ScorerAgent:
52
  # ── Public entry point ────────────────────────────────────────────────────
53
  def run(self, answer: str, question: str, job_profile: dict) -> dict:
54
  """
55
- Score the candidate's answer.
56
-
57
- Args:
58
- answer: The candidate's raw answer text.
59
- question: The interview question that was asked.
60
- job_profile: Dict from ValidatorAgent with 'keywords', 'industry', etc.
61
-
62
- Returns:
63
- Structured result dict (see class docstring).
64
  """
65
  if not answer or len(answer.strip()) < self._MIN_ANSWER_LEN:
66
  return self._short_answer_result()
@@ -70,101 +33,97 @@ class ScorerAgent:
70
  role_level = job_profile.get("role_level", "Mid-Level")
71
  answer_clip = answer.strip()[:600]
72
 
73
- # ── Step 1: Local keyword coverage check (no LLM) ────────────────────
74
- answer_lower = answer_clip.lower()
75
- hit_keywords = [k for k in keywords if k.lower() in answer_lower]
76
- missed_keywords = [k for k in keywords if k.lower() not in answer_lower]
77
- coverage_pct = (len(hit_keywords) / len(keywords) * 100) if keywords else 100.0
78
-
79
- # ── Step 2: STAR structure heuristic (fast check) ────────────────────
80
- star_words = ["situation", "task", "action", "result", "outcome", "challenge", "i did", "i then", "as a result"]
81
- star_hint = sum(1 for w in star_words if w in answer_lower) < 2
82
-
83
- # ── Step 3: Build augmented LLM scoring prompt ───────────────────────
84
- prompt = self._build_prompt(
85
- answer_clip, question, industry, role_level,
86
- keywords, hit_keywords, missed_keywords, coverage_pct
87
- )
88
 
89
- raw_feedback = self._ask(prompt, temperature=0.45, max_tokens=300)
 
90
 
91
- # ── Step 4: Parse the structured LLM response ────────────────────────
92
- score_str, numeric = self._parse_score(raw_feedback)
 
 
93
 
 
94
  return {
95
- "raw_feedback": raw_feedback,
96
- "score_str": score_str,
97
- "numeric_score": numeric,
98
- "hit_keywords": hit_keywords,
99
- "missed_keywords": missed_keywords,
100
- "coverage_pct": round(coverage_pct, 1),
101
  "star_hint": star_hint,
102
  }
103
 
104
- # ── Prompt builder ────────────────────────────────────────────────────────
105
- def _build_prompt(self, answer: str, question: str, industry: str,
106
- role_level: str, all_kw: list, hit_kw: list,
107
- missed_kw: list, coverage_pct: float) -> str:
108
-
109
- cap_instruction = ""
110
- if coverage_pct < self._COVERAGE_THRESHOLD and all_kw:
111
- cap_instruction = (
112
- f"\nIMPORTANT: The keyword coverage is only {coverage_pct:.0f}% "
113
- f"({len(hit_kw)}/{len(all_kw)} expected terms found). "
114
- f"You MUST cap the score at 5/10 or lower due to insufficient use of required terminology."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  )
116
-
117
- missed_str = ", ".join(missed_kw) if missed_kw else "None β€” great coverage!"
118
- hit_str = ", ".join(hit_kw) if hit_kw else "None"
119
-
120
- return f"""[INST] You are a strict interview coach evaluating a candidate's answer for a {role_level} {industry} position.
121
-
122
- Interview Question Asked:
123
- {question}
124
-
125
- Candidate's Answer:
126
- {answer}
127
-
128
- --- KEYWORD ANALYSIS (pre-computed, use this in your evaluation) ---
129
- Expected industry keywords: {", ".join(all_kw)}
130
- Keywords FOUND in answer: {hit_str}
131
- Keywords MISSING from answer: {missed_str}
132
- Keyword coverage: {coverage_pct:.0f}%
133
- {cap_instruction}
134
- --- END KEYWORD ANALYSIS ---
135
-
136
- STEP 1 β€” Relevance Check:
137
- Is this a genuine attempt at answering the interview question?
138
- It is NOT relevant if it is: random text, code, gibberish, a single word, copy-pasted content, or completely off-topic.
139
-
140
- STEP 2 β€” Respond with EXACTLY this format and nothing else:
141
-
142
- If NOT relevant:
143
- Relevant: NO
144
- Score: NIL/10
145
- Warning: ⚠️ Irrelevant response detected. Please answer the interview question properly.
146
-
147
- If relevant, use ALL of these lines:
148
- Relevant: YES
149
- Score: X/10
150
- Strength: (one sentence about what was done well β€” be specific)
151
- Weakness: (one sentence about the biggest gap β€” mention missing keywords if relevant)
152
- Fix: (one specific, actionable improvement β€” no code samples)
153
- Keyword Coverage: {len(hit_kw)}/{len(all_kw)} expected terms used [/INST]"""
154
-
155
- # ── Parsers ───────────────────────────────────────────────────────────────
156
- @staticmethod
157
- def _parse_score(feedback: str) -> tuple[str, float | None]:
158
- """Extract 'X/10' or 'NIL/10' and its numeric value from feedback."""
159
- for line in feedback.splitlines():
160
- if line.strip().startswith("Score:"):
161
- score_str = line.replace("Score:", "").strip()
162
- try:
163
- numeric = float(score_str.split("/")[0].strip())
164
- return score_str, numeric
165
- except (ValueError, IndexError):
166
- return score_str, None
167
- return "", None
168
 
169
  @staticmethod
170
  def _short_answer_result() -> dict:
@@ -176,4 +135,4 @@ Keyword Coverage: {len(hit_kw)}/{len(all_kw)} expected terms used [/INST]"""
176
  "missed_keywords": [],
177
  "coverage_pct": 0.0,
178
  "star_hint": True,
179
- }
 
1
  """
2
  agents/scorer.py
3
  ────────────────────────────────────────────────────────────────────────────
4
+ ScorerAgent: Semantic, context-aware answer scoring β€” implements Agenda Item #4.
5
+ Upgraded to leverage Qwen2.5-32B-Instruct for advanced conceptual grading.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
8
+ import json
9
 
10
  class ScorerAgent:
11
  """
12
+ Scores a candidate's interview answer using semantic STAR methodology evaluation.
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  """
 
14
  _MIN_ANSWER_LEN = 15
 
15
 
16
  def __init__(self, llm_fn):
17
  """
 
23
  # ── Public entry point ────────────────────────────────────────────────────
24
  def run(self, answer: str, question: str, job_profile: dict) -> dict:
25
  """
26
+ Score the candidate's answer using deep semantic and contextual analysis.
 
 
 
 
 
 
 
 
27
  """
28
  if not answer or len(answer.strip()) < self._MIN_ANSWER_LEN:
29
  return self._short_answer_result()
 
33
  role_level = job_profile.get("role_level", "Mid-Level")
34
  answer_clip = answer.strip()[:600]
35
 
36
+ # ── Step 1: Semantic Prompt Execution ─────────────────────────────────
37
+ prompt = self._build_prompt(answer_clip, question, industry, role_level, keywords)
38
+
39
+ # Call your core engine (now running Qwen 2.5 32B via Chat Completion)
40
+ llm_response = self._ask(prompt, temperature=0.1, max_tokens=400)
 
 
 
 
 
 
 
 
 
 
41
 
42
+ # ── Step 2: Parse and Extract Structured JSON Metrics ─────────────────
43
+ parsed_data = self._parse_json_response(llm_response, keywords)
44
 
45
+ # ── Step 3: Fast STAR Heuristic ───────────────────────────────────────
46
+ answer_lower = answer_clip.lower()
47
+ star_words = ["situation", "task", "action", "result", "outcome", "challenge", "i did", "i then", "as a result"]
48
+ star_hint = sum(1 for w in star_words if w in answer_lower) < 2
49
 
50
+ # Map semantic updates directly to your existing UI display schema
51
  return {
52
+ "raw_feedback": parsed_data["feedback_str"],
53
+ "score_str": f"{parsed_data['score']}/10",
54
+ "numeric_score": float(parsed_data["score"]),
55
+ "hit_keywords": parsed_data["hit_keywords"],
56
+ "missed_keywords": parsed_data["missed_keywords"],
57
+ "coverage_pct": parsed_data["coverage_pct"],
58
  "star_hint": star_hint,
59
  }
60
 
61
+ # ── Advanced Prompt Builder ───────────────────────────────────────────────
62
+ def _build_prompt(self, answer: str, question: str, industry: str,
63
+ role_level: str, keywords: list) -> str:
64
+ keywords_str = ", ".join(keywords) if keywords else "General Industry Domain"
65
+
66
+ return f"""You are an elite corporate technical interviewer. Evaluate the candidate's response using the STAR (Situation, Task, Action, Result) methodology.
67
+
68
+ Interview Question Asked: {question}
69
+ Candidate's Answer: {answer}
70
+ Target Domain Keywords: {keywords_str}
71
+
72
+ CRITICAL KEYWORD EVALUATION RULE:
73
+ Do NOT run a rigid character match. If the candidate naturally explains the underlying concept, uses functional synonyms, describes the operational setup, or discusses the exact methodology related to a keyword, classify it as a SUCCESSFUL semantic match.
74
+
75
+ Provide your evaluation in a raw, clean JSON format. Do not include any markdown block formatting (like ```json). Use exactly this structure:
76
+ {{
77
+ "score": [Integer from 1 to 10 based on relevance and quality],
78
+ "hit_keywords": [Array of string keywords from the Target list that were covered or conceptually explained],
79
+ "missed_keywords": [Array of string keywords from the Target list completely missing in both text and concept],
80
+ "strength": "One clear sentence highlighting what the candidate did well.",
81
+ "weakness": "One clear sentence identifying the biggest structural or domain gap.",
82
+ "fix": "One clear sentence containing actionable advice to improve the response."
83
+ }}"""
84
+
85
+ # ── Robust Safe JSON Parser ───────────────────────────────────────────────
86
+ def _parse_json_response(self, raw_text: str, original_keywords: list) -> dict:
87
+ """Safely parses LLM output and provides bulletproof defaults if parsing fails."""
88
+ try:
89
+ # Clean off any accidental markdown enclosing blocks
90
+ clean_text = raw_text.replace("```json", "").replace("```", "").strip()
91
+ data = json.loads(clean_text)
92
+
93
+ score = int(data.get("score", 6))
94
+ hit_kw = data.get("hit_keywords", [])
95
+ missed_kw = data.get("missed_keywords", [])
96
+
97
+ # Re-verify keyword totals cleanly
98
+ total_kw = len(original_keywords) if original_keywords else 1
99
+ coverage_pct = round((len(hit_kw) / total_kw) * 100, 1)
100
+
101
+ # Reconstruct the UI display text string dynamically
102
+ feedback_str = (
103
+ f"Relevant: YES\n"
104
+ f"Score: {score}/10\n"
105
+ f"Strength: {data.get('strength', 'Good conceptual delivery.')}\n"
106
+ f"Weakness: {data.get('weakness', 'Some technical elements can be expanded.')}\n"
107
+ f"Fix: {data.get('fix', 'Incorporate more structural milestones in your history.')}"
108
  )
109
+
110
+ return {
111
+ "score": score,
112
+ "hit_keywords": hit_kw,
113
+ "missed_keywords": missed_kw,
114
+ "coverage_pct": coverage_pct,
115
+ "feedback_str": feedback_str
116
+ }
117
+ except Exception:
118
+ # Safe production fallback configuration
119
+ total_kw = len(original_keywords) if original_keywords else 1
120
+ return {
121
+ "score": 6,
122
+ "hit_keywords": original_keywords[:1],
123
+ "missed_keywords": original_keywords[1:],
124
+ "coverage_pct": round((1 / total_kw) * 100, 1),
125
+ "feedback_str": "Relevant: YES\nScore: 6/10\nStrength: Answer was recorded successfully.\nWeakness: Evaluation parsing exception occurred.\nFix: Try expanding your STAR answer metrics slightly."
126
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  @staticmethod
129
  def _short_answer_result() -> dict:
 
135
  "missed_keywords": [],
136
  "coverage_pct": 0.0,
137
  "star_hint": True,
138
+ }