| """Reward scoring for ADHD coaching environment. |
| |
| V2: Rubric-based scoring with tool calling + state awareness. |
| - Tool calling: 40% weight - penalizes wrong-domain tools |
| - State awareness: 30% weight - rewards state-responsive coaching |
| - ADHD relevance: 30% weight - rewards directive, low-cognitive-load responses |
| """ |
|
|
| from typing import Dict, Any, Optional |
| from models import ADHDAction |
|
|
|
|
| |
| ADHD_TOOLS = {"adhd_coach_tool"} |
|
|
|
|
| def score_tool_calling( |
| action: ADHDAction, |
| is_adhd_scenario: bool, |
| expected_tool: Optional[str] = None, |
| ) -> float: |
| """Score tool selection based on scenario type. |
| |
| ADHD scenario: |
| 1.0 - called adhd_coach_tool |
| 0.0 - no tools called |
| -0.5 - called a non-ADHD tool (wrong domain) |
| |
| Non-ADHD scenario: |
| -0.5 - called adhd_coach_tool (wrong domain) |
| 0.7 - called the expected non-ADHD tool |
| 0.5 - no tools called (neutral) |
| 0.5 - called some other non-ADHD tool (neutral) |
| """ |
| called = set(action.tool_calls) |
|
|
| if is_adhd_scenario: |
| if "adhd_coach_tool" in called: |
| return 1.0 |
| if not called: |
| return 0.0 |
| |
| return -0.5 |
| else: |
| |
| if "adhd_coach_tool" in called: |
| return -0.5 |
| if expected_tool and expected_tool in called: |
| return 0.7 |
| |
| return 0.5 |
|
|
|
|
| def score_state_awareness(action: ADHDAction, user_state: dict) -> float: |
| """Score whether response accounts for user state. |
| |
| 1.0 - mentions movement/stretching when sitting 60+ min or slouching |
| 1.0 - suggests simpler tasks when evening (hour >= 20) |
| 0.5 - generic response (default, neutral) |
| """ |
| msg = action.message.lower() |
| score = 0.5 |
|
|
| minutes_sitting = user_state.get("minutes_since_last_stood", 0) |
| position = user_state.get("position_in_chair", "normal") |
| time_str = user_state.get("time_of_day", "12:00") |
| hour = int(time_str.split(":")[0]) |
|
|
| movement_keywords = [ |
| "stand", "stretch", "walk", "move", "get up", "posture", |
| "take a break", "step away", "physical", |
| ] |
|
|
| |
| if minutes_sitting >= 60 or position == "slouching": |
| if any(kw in msg for kw in movement_keywords): |
| score = 1.0 |
|
|
| |
| evening_keywords = [ |
| "simple", "small", "easy", "quick", "short", "wind down", |
| "rest", "tomorrow", "lighter", |
| ] |
| if hour >= 20: |
| if any(kw in msg for kw in evening_keywords): |
| score = 1.0 |
|
|
| return score |
|
|
|
|
| def score_adhd_relevance(action: ADHDAction, is_adhd_scenario: bool) -> float: |
| """Score ADHD-specific response quality. |
| |
| For ADHD scenarios: rewards concise responses and reflective questions. |
| For non-ADHD: returns neutral 0.5. |
| """ |
| if not is_adhd_scenario: |
| return 0.5 |
|
|
| msg = action.message.strip() |
| if not msg: |
| return 0.0 |
|
|
| score = 0.5 |
| msg_lower = msg.lower() |
|
|
| |
| if "?" in msg: |
| question_words = ("what", "how") |
| reflective_words = ("specific", "detail", "details", "feeling", "think", "reflect", "explain") |
| if any(qw in msg_lower for qw in question_words) and any(rw in msg_lower for rw in reflective_words): |
| score += 0.15 |
|
|
| |
| word_count = len(msg.split()) |
| if 5 <= word_count <= 50: |
| score += 0.25 |
| elif word_count > 100: |
| score -= 0.25 |
|
|
| return max(0.0, min(1.0, score)) |
|
|
|
|
| def score_rubric( |
| action: ADHDAction, |
| scenario: str, |
| user_state: dict, |
| is_adhd_scenario: bool, |
| expected_tool: Optional[str] = None, |
| ) -> Dict[str, Any]: |
| """Combined rubric score with per-criterion breakdown. |
| |
| Weights: tool_calling 40% + state_awareness 30% + adhd_relevance 30% |
| Total clamped to 0.0-1.0. |
| """ |
| tool_score = score_tool_calling(action, is_adhd_scenario, expected_tool) |
| state_score = score_state_awareness(action, user_state) |
| relevance_score = score_adhd_relevance(action, is_adhd_scenario) |
|
|
| raw_total = (tool_score * 0.4) + (state_score * 0.3) + (relevance_score * 0.3) |
| total = max(0.0, min(1.0, raw_total)) |
|
|
| return { |
| "version": "v2.1", |
| "total_score": round(total, 3), |
| "criteria": { |
| "tool_calling": { |
| "score": tool_score, |
| "weight": 0.4, |
| "is_adhd_scenario": is_adhd_scenario, |
| "expected_tool": expected_tool, |
| "tools_called": action.tool_calls, |
| }, |
| "state_awareness": { |
| "score": state_score, |
| "weight": 0.3, |
| "user_state": user_state, |
| }, |
| "adhd_relevance": { |
| "score": relevance_score, |
| "weight": 0.3, |
| }, |
| }, |
| } |
|
|