|
37 | 37 |
|
38 | 38 | def parse_frontend_graph(data: dict) -> Graph: |
39 | 39 | """ |
40 | | - Parse pipe-delimited frontend graph format into Graph object. |
41 | | - |
| 40 | + Parse pipe-delimited frontend graph format into a Graph object (nodes + edges only). |
| 41 | +
|
42 | 42 | Frontend format: |
43 | 43 | - nodes: ["id|label|x|y", ...] |
44 | 44 | - edges: ["source|target|weight|label", ...] |
45 | | - - directed: boolean |
46 | | - - weighted: boolean |
47 | | - - multigraph: boolean |
48 | | - |
49 | | - Example: |
50 | | - { |
51 | | - "nodes": ["city1|New York|120|180"], |
52 | | - "edges": ["city1|city2|215|I-95 North"], |
53 | | - "directed": true, |
54 | | - "weighted": true, |
55 | | - "multigraph": false |
56 | | - } |
57 | | - |
| 45 | +
|
| 46 | + Note: directed/weighted/multigraph are NOT read from the data dict here. |
| 47 | + They come exclusively from EvaluationParams and are applied later via |
| 48 | + _apply_params_to_graph(). |
| 49 | +
|
58 | 50 | Args: |
59 | 51 | data: Dictionary with pipe-delimited node and edge strings |
60 | | - |
| 52 | +
|
61 | 53 | Returns: |
62 | | - Graph object with parsed nodes and edges |
| 54 | + Graph object with only nodes and edges populated. |
63 | 55 | """ |
64 | 56 | nodes = [] |
65 | 57 | edges = [] |
@@ -94,13 +86,9 @@ def parse_frontend_graph(data: dict) -> Graph: |
94 | 86 | ) |
95 | 87 | edges.append(edge) |
96 | 88 |
|
97 | | - return Graph( |
98 | | - nodes=nodes, |
99 | | - edges=edges, |
100 | | - directed=data.get("directed", False), |
101 | | - weighted=data.get("weighted", False), |
102 | | - multigraph=data.get("multigraph", False) |
103 | | - ) |
| 89 | + # directed/weighted/multigraph are intentionally NOT read from the data dict. |
| 90 | + # They come exclusively from EvaluationParams, applied via _apply_params_to_graph(). |
| 91 | + return Graph(nodes=nodes, edges=edges) |
104 | 92 |
|
105 | 93 |
|
106 | 94 | def is_frontend_format(data: dict) -> bool: |
@@ -134,6 +122,25 @@ def is_frontend_format(data: dict) -> bool: |
134 | 122 | return False |
135 | 123 |
|
136 | 124 |
|
| 125 | +# ============================================================================= |
| 126 | +# GRAPH HELPERS |
| 127 | +# ============================================================================= |
| 128 | + |
| 129 | +def _apply_params_to_graph(graph: Graph, params: EvaluationParams) -> Graph: |
| 130 | + """ |
| 131 | + Return a new Graph with directed/weighted/multigraph copied from EvaluationParams. |
| 132 | + This is the single place where these flags are stamped onto a graph object — |
| 133 | + they must never come from the student or teacher payload. |
| 134 | + """ |
| 135 | + return Graph( |
| 136 | + nodes=graph.nodes, |
| 137 | + edges=graph.edges, |
| 138 | + directed=params.directed, |
| 139 | + weighted=params.weighted, |
| 140 | + multigraph=params.multigraph, |
| 141 | + ) |
| 142 | + |
| 143 | + |
137 | 144 | # ============================================================================= |
138 | 145 | # FEEDBACK GENERATION HELPERS |
139 | 146 | # ============================================================================= |
@@ -615,59 +622,57 @@ def _ok() -> Result: |
615 | 622 | def _err(msg: str) -> Result: |
616 | 623 | return Result(is_correct=False, feedback_items=[("error", msg)]) |
617 | 624 |
|
618 | | - # ── parse & validate inputs ────────────────────────────────────────── |
| 625 | + # ── parse params FIRST — directed/weighted/multigraph live here ───── |
| 626 | + |
| 627 | + raw_params = _to_dictish(params) or {} |
| 628 | + try: |
| 629 | + p = EvaluationParams.model_validate(raw_params) |
| 630 | + except ValidationError as e: |
| 631 | + return _err( |
| 632 | + "Invalid params schema. Expected e.g. " |
| 633 | + "{'evaluation_type': 'connectivity'|'bipartite'|'graph_coloring'|...}. " |
| 634 | + f"Error: {e}" |
| 635 | + ) |
| 636 | + |
| 637 | + # ── parse response (student's graph) ───────────────────────────────── |
619 | 638 |
|
620 | | - # Parse response (student's graph) |
621 | 639 | response_dict = _to_dictish(response) or {} |
622 | | - |
623 | | - # Check if response contains frontend pipe-delimited format and convert |
| 640 | + |
624 | 641 | if is_frontend_format(response_dict): |
625 | 642 | parsed_graph = parse_frontend_graph(response_dict) |
626 | | - response_dict = {"graph": parsed_graph} |
627 | | - |
| 643 | + response_dict = {"graph": parsed_graph.model_dump()} |
| 644 | + |
628 | 645 | try: |
629 | 646 | resp = Response.model_validate(response_dict) |
630 | 647 | except ValidationError as e: |
631 | 648 | return _err(f"Invalid response schema: {e}") |
632 | 649 |
|
633 | | - # Parse answer (teacher's reference) |
| 650 | + # ── parse answer (teacher's reference) ─────────────────────────────── |
| 651 | + |
634 | 652 | answer_dict = _to_dictish(answer) or {} |
635 | | - |
636 | | - # Check if answer contains frontend pipe-delimited format and convert |
| 653 | + |
637 | 654 | if is_frontend_format(answer_dict): |
638 | 655 | parsed_graph = parse_frontend_graph(answer_dict) |
639 | | - answer_dict = {"graph": parsed_graph} |
640 | | - |
| 656 | + answer_dict = {"graph": parsed_graph.model_dump()} |
| 657 | + |
641 | 658 | try: |
642 | 659 | ans = Answer.model_validate(answer_dict) |
643 | 660 | except ValidationError as e: |
644 | 661 | return _err(f"Invalid answer schema: {e}") |
645 | 662 |
|
646 | | - raw_params = _to_dictish(params) or {} |
647 | | - try: |
648 | | - p = EvaluationParams.model_validate(raw_params) |
649 | | - except ValidationError as e: |
650 | | - return _err( |
651 | | - "Invalid params schema. Expected e.g. " |
652 | | - "{'evaluation_type': 'connectivity'|'bipartite'|'graph_coloring'|...}. " |
653 | | - f"Error: {e}" |
654 | | - f"response: {response}" |
655 | | - f"response_dict: {response_dict}" |
656 | | - f"answer: {answer}" |
657 | | - f"answer_dict: {answer_dict}" |
658 | | - f"params: {params}" |
659 | | - f"raw_params: {raw_params}" |
660 | | - ) |
| 663 | + # ── resolve graphs and stamp params flags ───────────────────────────── |
| 664 | + # directed/weighted/multigraph come exclusively from params — never from |
| 665 | + # the student or teacher payload. |
661 | 666 |
|
662 | | - # ── resolve graphs ─────────────────────────────────────────────────── |
663 | | - # student_graph (resp.graph) is always present — the student submits a graph. |
664 | | - # ans.graph is only present for isomorphism / subgraph checks where the |
665 | | - # teacher provides a reference graph. For all other eval types the teacher |
666 | | - # sets the expected property value directly in the answer (e.g. ans.is_connected). |
667 | 667 | student_graph: Graph = resp.graph |
668 | 668 | if student_graph is None: |
669 | 669 | return _err("response.graph is required — the student must submit a graph.") |
670 | 670 |
|
| 671 | + student_graph = _apply_params_to_graph(student_graph, p) |
| 672 | + |
| 673 | + if ans.graph is not None: |
| 674 | + ans = ans.model_copy(update={"graph": _apply_params_to_graph(ans.graph, p)}) |
| 675 | + |
671 | 676 | # ── helper: grade a simple boolean property ────────────────────────── |
672 | 677 | def _grade_bool( |
673 | 678 | label: str, |
|
0 commit comments