File size: 4,312 Bytes
ede2fa4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""
Deterministic grader for the SQL Query Environment.

Compares the agent's query result against the expected result.
Produces a score between 0.0 and 1.0 with partial credit:

  0.0  β€” SQL error or no result returned
  0.1  β€” Query runs but returns wrong column count
  0.2  β€” Correct column count but wrong column names
  0.3  β€” Correct columns but wrong number of rows
  0.5  β€” Correct structure, some rows match
  0.7  β€” Most rows match (>= 70% of expected rows found)
  0.9  β€” All rows match but in wrong order
  1.0  β€” Exact match (columns, rows, order)
"""

from tasks import TaskDefinition


def _normalize_value(val) -> str:
    """Normalize a value for comparison (handle float rounding, casing)."""
    if val is None:
        return "NULL"
    if isinstance(val, float):
        return f"{val:.2f}"
    return str(val).strip().lower()


def _normalize_row(row: tuple) -> tuple:
    return tuple(_normalize_value(v) for v in row)


def grade_query(
    task: TaskDefinition,
    agent_rows: list[tuple] | None,
    agent_columns: list[str] | None,
    query_error: str | None,
) -> tuple[float, str]:
    """
    Grade the agent's SQL query result against the expected result.

    Returns:
        (score, feedback_string)
    """
    # SQL error β†’ 0.0
    if query_error is not None:
        return 0.0, f"SQL error: {query_error}"

    # No result (e.g. INSERT/UPDATE instead of SELECT) β†’ 0.0
    if agent_rows is None or agent_columns is None:
        return 0.0, "Query did not return any result rows. Expected a SELECT query."

    expected_cols = [c.lower() for c in task.expected_columns]
    actual_cols = [c.lower() for c in agent_columns]

    # Wrong column count β†’ 0.1
    if len(actual_cols) != len(expected_cols):
        return 0.1, (
            f"Wrong number of columns. Expected {len(expected_cols)} "
            f"({', '.join(expected_cols)}), got {len(actual_cols)} "
            f"({', '.join(actual_cols)})."
        )

    # Wrong column names β†’ 0.2
    if actual_cols != expected_cols:
        return 0.2, (
            f"Column names don't match. Expected {expected_cols}, got {actual_cols}. "
            f"Note: column names must match exactly."
        )

    # Columns match β€” now compare rows
    expected_normalized = [_normalize_row(r) for r in task.expected_rows]
    actual_normalized = [_normalize_row(r) for r in agent_rows]

    # Wrong row count β†’ partial credit based on matching rows
    if len(actual_normalized) != len(expected_normalized):
        # Count how many expected rows appear in actual
        expected_set = set(expected_normalized)
        actual_set = set(actual_normalized)
        matching = len(expected_set & actual_set)

        if matching == 0:
            return 0.3, (
                f"Correct columns but wrong number of rows. "
                f"Expected {len(expected_normalized)} rows, got {len(actual_normalized)}. "
                f"No matching rows found."
            )
        ratio = matching / len(expected_normalized)
        score = 0.3 + ratio * 0.4  # Scale from 0.3 to 0.7
        return round(score, 2), (
            f"Correct columns but wrong row count. "
            f"Expected {len(expected_normalized)} rows, got {len(actual_normalized)}. "
            f"{matching}/{len(expected_normalized)} expected rows found."
        )

    # Same row count β€” check content
    expected_set = set(expected_normalized)
    actual_set = set(actual_normalized)

    if expected_set != actual_set:
        # Some rows match
        matching = len(expected_set & actual_set)
        if matching == 0:
            return 0.5, (
                "Correct structure (columns and row count) but row values "
                "don't match any expected rows."
            )
        ratio = matching / len(expected_normalized)
        score = 0.5 + ratio * 0.2
        return round(score, 2), (
            f"Partially correct. {matching}/{len(expected_normalized)} rows match."
        )

    # All rows present β€” check order
    if actual_normalized != expected_normalized:
        return 0.9, (
            "All correct rows found but in wrong order. "
            "Check your ORDER BY clause."
        )

    # Perfect match!
    return 1.0, "Correct! Query returned the exact expected result."