Skip to content

Commit 095e248

Browse files
committed
feat: moved configuration parameters into "params" from resposne
1 parent 81d01e6 commit 095e248

4 files changed

Lines changed: 85 additions & 72 deletions

File tree

evaluation_function/evaluation.py

Lines changed: 60 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,21 @@
3737

3838
def parse_frontend_graph(data: dict) -> Graph:
3939
"""
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+
4242
Frontend format:
4343
- nodes: ["id|label|x|y", ...]
4444
- 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+
5850
Args:
5951
data: Dictionary with pipe-delimited node and edge strings
60-
52+
6153
Returns:
62-
Graph object with parsed nodes and edges
54+
Graph object with only nodes and edges populated.
6355
"""
6456
nodes = []
6557
edges = []
@@ -94,13 +86,9 @@ def parse_frontend_graph(data: dict) -> Graph:
9486
)
9587
edges.append(edge)
9688

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)
10492

10593

10694
def is_frontend_format(data: dict) -> bool:
@@ -134,6 +122,25 @@ def is_frontend_format(data: dict) -> bool:
134122
return False
135123

136124

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+
137144
# =============================================================================
138145
# FEEDBACK GENERATION HELPERS
139146
# =============================================================================
@@ -615,59 +622,57 @@ def _ok() -> Result:
615622
def _err(msg: str) -> Result:
616623
return Result(is_correct=False, feedback_items=[("error", msg)])
617624

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) ─────────────────────────────────
619638

620-
# Parse response (student's graph)
621639
response_dict = _to_dictish(response) or {}
622-
623-
# Check if response contains frontend pipe-delimited format and convert
640+
624641
if is_frontend_format(response_dict):
625642
parsed_graph = parse_frontend_graph(response_dict)
626-
response_dict = {"graph": parsed_graph}
627-
643+
response_dict = {"graph": parsed_graph.model_dump()}
644+
628645
try:
629646
resp = Response.model_validate(response_dict)
630647
except ValidationError as e:
631648
return _err(f"Invalid response schema: {e}")
632649

633-
# Parse answer (teacher's reference)
650+
# ── parse answer (teacher's reference) ───────────────────────────────
651+
634652
answer_dict = _to_dictish(answer) or {}
635-
636-
# Check if answer contains frontend pipe-delimited format and convert
653+
637654
if is_frontend_format(answer_dict):
638655
parsed_graph = parse_frontend_graph(answer_dict)
639-
answer_dict = {"graph": parsed_graph}
640-
656+
answer_dict = {"graph": parsed_graph.model_dump()}
657+
641658
try:
642659
ans = Answer.model_validate(answer_dict)
643660
except ValidationError as e:
644661
return _err(f"Invalid answer schema: {e}")
645662

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.
661666

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).
667667
student_graph: Graph = resp.graph
668668
if student_graph is None:
669669
return _err("response.graph is required — the student must submit a graph.")
670670

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+
671676
# ── helper: grade a simple boolean property ──────────────────────────
672677
def _grade_bool(
673678
label: str,

evaluation_function/schemas/graph.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ class Config:
3232
class Graph(BaseModel):
3333
nodes: list[Node] = Field(..., description="List of nodes in the graph")
3434
edges: list[Edge] = Field(default_factory=list, description="List of edges")
35-
directed: bool = Field(False, description="Whether the graph is directed")
36-
weighted: bool = Field(False, description="Whether the graph is weighted (metadata only, not used in evaluation)")
37-
multigraph: bool = Field(False, description="Whether the graph allows multiple edges (metadata only, not used in evaluation)")
35+
# These flags are NOT part of the student/teacher payload schema.
36+
# They are set exclusively from EvaluationParams by _apply_params_to_graph()
37+
# at evaluation time so that algorithm functions can read them.
38+
directed: bool = Field(False, description="Set from EvaluationParams.directed at evaluation time — do not include in response/answer payloads")
39+
weighted: bool = Field(False, description="Set from EvaluationParams.weighted at evaluation time — do not include in response/answer payloads")
40+
multigraph: bool = Field(False, description="Set from EvaluationParams.multigraph at evaluation time — do not include in response/answer payloads")
3841

3942
class Config:
4043
extra = "allow"

evaluation_function/schemas/params.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ class TraversalParams(BaseModel):
7575
class EvaluationParams(BaseModel):
7676
evaluation_type: EvaluationType = Field(..., description="The type of evaluation to perform")
7777

78+
# Graph structure flags — sourced from params, not from the response/answer graph payload
79+
directed: bool = Field(False, description="Whether the graph is directed")
80+
weighted: bool = Field(False, description="Whether the graph is weighted")
81+
multigraph: bool = Field(False, description="Whether the graph allows multiple edges")
82+
7883
connectivity: Optional[ConnectivityParams] = None
7984
bipartite: Optional[BipartiteParams] = None
8085
graph_coloring: Optional[GraphColoringParams] = None

tests/test_evaluation_function_core.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818

1919
# ── helpers ──────────────────────────────────────────────────────────────
2020

21-
def _graph(nodes, edges, *, directed=False):
21+
def _graph(nodes, edges):
22+
"""Build a plain graph dict with only nodes and edges — no graph-level flags."""
2223
return {
2324
"nodes": [{"id": n} for n in nodes],
2425
"edges": [{"source": s, "target": t, **({"weight": w} if (w := e.get("weight")) is not None else {})}
2526
for e in edges for s, t in [(e["source"], e["target"])]],
26-
"directed": directed,
2727
}
2828

2929

@@ -53,15 +53,15 @@ def test_disconnected(self):
5353
assert r["is_correct"] is True
5454

5555
def test_strongly_connected(self):
56-
g = _graph(["A", "B"], [{"source": "A", "target": "B"}, {"source": "B", "target": "A"}], directed=True)
56+
g = _graph(["A", "B"], [{"source": "A", "target": "B"}, {"source": "B", "target": "A"}])
5757
r = _eval({"graph": g}, {"is_connected": True},
58-
{"evaluation_type": "connectivity", "connectivity": {"check_type": "strongly_connected"}})
58+
{"evaluation_type": "connectivity", "directed": True, "connectivity": {"check_type": "strongly_connected"}})
5959
assert r["is_correct"] is True
6060

6161
def test_weakly_connected(self):
62-
g = _graph(["A", "B"], [{"source": "A", "target": "B"}], directed=True)
62+
g = _graph(["A", "B"], [{"source": "A", "target": "B"}])
6363
r = _eval({"graph": g}, {"is_connected": True},
64-
{"evaluation_type": "connectivity", "connectivity": {"check_type": "weakly_connected"}})
64+
{"evaluation_type": "connectivity", "directed": True, "connectivity": {"check_type": "weakly_connected"}})
6565
assert r["is_correct"] is True
6666

6767
def test_missing_expected_value(self):
@@ -144,17 +144,17 @@ def test_wrong_cycle_answer(self):
144144
def test_directed_cycle(self):
145145
g = _graph(["A", "B", "C"], [
146146
{"source": "A", "target": "B"}, {"source": "B", "target": "C"}, {"source": "C", "target": "A"}
147-
], directed=True)
147+
])
148148
r = _eval({"graph": g}, {"has_cycle": True},
149-
{"evaluation_type": "cycle_detection"})
149+
{"evaluation_type": "cycle_detection", "directed": True})
150150
assert r["is_correct"] is True
151151

152152
def test_directed_dag(self):
153153
g = _graph(["A", "B", "C"], [
154154
{"source": "A", "target": "B"}, {"source": "A", "target": "C"}
155-
], directed=True)
155+
])
156156
r = _eval({"graph": g}, {"has_cycle": False},
157-
{"evaluation_type": "cycle_detection"})
157+
{"evaluation_type": "cycle_detection", "directed": True})
158158
assert r["is_correct"] is True
159159

160160

@@ -341,15 +341,15 @@ class TestDAG:
341341
def test_dag_correct(self):
342342
g = _graph(["A", "B", "C"], [
343343
{"source": "A", "target": "B"}, {"source": "A", "target": "C"}
344-
], directed=True)
345-
r = _eval({"graph": g}, {"is_dag": True}, {"evaluation_type": "dag"})
344+
])
345+
r = _eval({"graph": g}, {"is_dag": True}, {"evaluation_type": "dag", "directed": True})
346346
assert r["is_correct"] is True
347347

348348
def test_not_dag_has_cycle(self):
349349
g = _graph(["A", "B"], [
350350
{"source": "A", "target": "B"}, {"source": "B", "target": "A"}
351-
], directed=True)
352-
r = _eval({"graph": g}, {"is_dag": False}, {"evaluation_type": "dag"})
351+
])
352+
r = _eval({"graph": g}, {"is_dag": False}, {"evaluation_type": "dag", "directed": True})
353353
assert r["is_correct"] is True
354354

355355
def test_undirected_never_dag(self):

0 commit comments

Comments
 (0)