donb-hf commited on
Commit
c1e9b4d
Β·
verified Β·
1 Parent(s): 1701e56

add lesson_graph.py

Browse files
Files changed (1) hide show
  1. lesson_graph.py +243 -0
lesson_graph.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import networkx as nx
2
+ import matplotlib.pyplot as plt
3
+ from io import BytesIO
4
+ from PIL import Image
5
+ import matplotlib.patches as mpatches
6
+ import mplcursors
7
+ import json
8
+ from typing import Dict, Tuple, Any, NamedTuple, Optional
9
+ from enum import Enum
10
+
11
+ class NodeType(Enum):
12
+ USER = "User"
13
+ SUBJECT = "Subject"
14
+ GRADE_LEVEL = "Grade Level"
15
+ LEARNING_OBJECTIVE = "Learning Objective"
16
+ ACTIVITY = "Activity"
17
+ ASSESSMENT = "Assessment"
18
+ RESOURCE = "Resource"
19
+ SCHOOL_BOARD = "School Board"
20
+
21
+ class ResetState(NamedTuple):
22
+ teacher_name: str
23
+ subject: str
24
+ grade_level: str
25
+ learning_objective: str
26
+ activity: str
27
+ assessment: str
28
+ resource: str
29
+ school_board: str
30
+ message: str
31
+
32
+ class LessonGraph:
33
+ INITIAL_STATE: Dict[str, str] = {
34
+ "teacher_name": "",
35
+ "subject": "",
36
+ "grade_level": "",
37
+ "learning_objective": "",
38
+ "activity": "",
39
+ "assessment": "",
40
+ "resource": "",
41
+ "school_board": ""
42
+ }
43
+
44
+ REQUIRED_FIELDS = ["teacher_name", "subject", "grade_level"]
45
+
46
+ COLOR_MAP: Dict[NodeType, str] = {
47
+ NodeType.USER: "#FF9999",
48
+ NodeType.SUBJECT: "#66B2FF",
49
+ NodeType.GRADE_LEVEL: "#99FF99",
50
+ NodeType.LEARNING_OBJECTIVE: "#FFCC99",
51
+ NodeType.ACTIVITY: "#FF99FF",
52
+ NodeType.ASSESSMENT: "#FFFF99",
53
+ NodeType.RESOURCE: "#99FFFF",
54
+ NodeType.SCHOOL_BOARD: "#CCCCCC"
55
+ }
56
+
57
+ def __init__(self):
58
+ self.graph = nx.DiGraph()
59
+ self.inputs = self.INITIAL_STATE.copy()
60
+
61
+ def validate_required_fields(self):
62
+ """
63
+ Validate that all required fields are filled.
64
+ Raises a ValueError if any required field is empty.
65
+ """
66
+ missing_fields = [field for field in self.REQUIRED_FIELDS if not self.inputs.get(field)]
67
+ if missing_fields:
68
+ raise ValueError(f"The following required fields are missing: {', '.join(missing_fields)}")
69
+
70
+ def add_lesson_plan(self, **kwargs) -> Tuple[str, Image.Image]:
71
+ """
72
+ Add nodes and edges to the lesson plan graph for the given inputs.
73
+ Returns a search string and the graph image.
74
+ """
75
+ self.graph.clear() # Clear previous graph
76
+ self.inputs.update(kwargs)
77
+
78
+ # Validate only required fields
79
+ self.validate_required_fields()
80
+
81
+ # Define node details and add them to the graph
82
+ nodes = {
83
+ self.inputs["teacher_name"]: {"type": NodeType.USER, "role": "Teacher"},
84
+ self.inputs["subject"]: {"type": NodeType.SUBJECT, "description": "Core subject area"},
85
+ self.inputs["grade_level"]: {"type": NodeType.GRADE_LEVEL, "description": "Target grade for the lesson"},
86
+ }
87
+
88
+ # Add optional nodes if they exist
89
+ optional_nodes = {
90
+ "learning_objective": NodeType.LEARNING_OBJECTIVE,
91
+ "activity": NodeType.ACTIVITY,
92
+ "assessment": NodeType.ASSESSMENT,
93
+ "resource": NodeType.RESOURCE,
94
+ "school_board": NodeType.SCHOOL_BOARD
95
+ }
96
+
97
+ for field, node_type in optional_nodes.items():
98
+ if self.inputs.get(field):
99
+ nodes[self.inputs[field]] = {"type": node_type, "description": f"Optional {node_type.value}"}
100
+
101
+ # Add nodes to the graph
102
+ for node, attributes in nodes.items():
103
+ self.graph.add_node(node, **attributes)
104
+
105
+ # Define the relationships between nodes
106
+ edges = [
107
+ (self.inputs["teacher_name"], self.inputs["subject"], {"relationship": "TEACHES"}),
108
+ (self.inputs["subject"], self.inputs["grade_level"], {"relationship": "HAS_GRADE"})
109
+ ]
110
+
111
+ # Add optional edges
112
+ if self.inputs.get("learning_objective"):
113
+ edges.extend([
114
+ (self.inputs["subject"], self.inputs["learning_objective"], {"relationship": "COVERS"}),
115
+ (self.inputs["learning_objective"], self.inputs["school_board"], {"relationship": "ALIGNS_WITH"}) if self.inputs.get("school_board") else None
116
+ ])
117
+
118
+ if self.inputs.get("activity") and self.inputs.get("learning_objective"):
119
+ edges.append((self.inputs["activity"], self.inputs["learning_objective"], {"relationship": "ACHIEVES"}))
120
+
121
+ if self.inputs.get("activity") and self.inputs.get("resource"):
122
+ edges.append((self.inputs["activity"], self.inputs["resource"], {"relationship": "REQUIRES"}))
123
+
124
+ if self.inputs.get("learning_objective") and self.inputs.get("assessment"):
125
+ edges.append((self.inputs["learning_objective"], self.inputs["assessment"], {"relationship": "EVALUATED_BY"}))
126
+
127
+ if self.inputs.get("school_board"):
128
+ edges.append((self.inputs["teacher_name"], self.inputs["school_board"], {"relationship": "BELONGS_TO"}))
129
+
130
+ # Remove None entries from edges list
131
+ edges = [edge for edge in edges if edge is not None]
132
+ self.graph.add_edges_from(edges)
133
+
134
+ # Generate the search string for content discovery
135
+ search_string = f"{self.inputs['subject']} {self.inputs['grade_level']} {self.inputs.get('learning_objective', '')} {self.inputs.get('activity', '')} {self.inputs.get('resource', '')}".strip()
136
+
137
+ # Get the graph image
138
+ image = self.draw_graph()
139
+
140
+ # Return the search string and the graph image
141
+ return search_string, image
142
+
143
+ def draw_graph(self) -> Image.Image:
144
+ """
145
+ Visualize the graph using Matplotlib, handling layout, labels, and interactivity.
146
+ """
147
+ fig, ax = plt.subplots(figsize=(14, 10))
148
+ pos = nx.spring_layout(self.graph, k=0.9, iterations=50)
149
+
150
+ self._draw_nodes(ax, pos)
151
+ self._draw_edges(ax, pos)
152
+ self._add_legend(ax)
153
+
154
+ plt.title("Your Educational Landscape", fontsize=16)
155
+ plt.axis('off')
156
+ plt.tight_layout()
157
+
158
+ self._add_interactivity()
159
+
160
+ # Save the plot to a BytesIO object
161
+ buf = BytesIO()
162
+ plt.savefig(buf, format="png", dpi=300, bbox_inches="tight")
163
+ buf.seek(0)
164
+ plt.close(fig)
165
+
166
+ return Image.open(buf)
167
+
168
+ def _draw_nodes(self, ax, pos):
169
+ node_colors = [self.COLOR_MAP[self.graph.nodes[node]['type']] for node in self.graph.nodes()]
170
+ nx.draw_networkx_nodes(self.graph, pos, node_color=node_colors, node_size=3000, alpha=0.8, ax=ax)
171
+ nx.draw_networkx_labels(self.graph, pos, font_size=10, font_weight="bold", ax=ax)
172
+
173
+ def _draw_edges(self, ax, pos):
174
+ nx.draw_networkx_edges(self.graph, pos, edge_color='gray', arrows=True, arrowsize=20, ax=ax)
175
+ edge_labels = nx.get_edge_attributes(self.graph, 'relationship')
176
+ nx.draw_networkx_edge_labels(self.graph, pos, edge_labels=edge_labels, font_size=8, ax=ax)
177
+
178
+ def _add_legend(self, ax):
179
+ legend_elements = [mpatches.Patch(color=color, label=node_type.value) for node_type, color in self.COLOR_MAP.items()]
180
+ ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(1, 1), title="Node Types")
181
+
182
+ def _add_interactivity(self):
183
+ cursor = mplcursors.cursor(hover=True)
184
+ @cursor.connect("add")
185
+ def on_add(sel):
186
+ node = list(self.graph.nodes())[sel.target.index]
187
+ node_data = self.graph.nodes[node]
188
+ sel.annotation.set_text(f"Node: {node}\nType: {node_data['type'].value}\n{node_data.get('description', '')}")
189
+
190
+ def reset_state(self) -> ResetState:
191
+ """
192
+ Resets all input states to their default values and clears the graph.
193
+ Returns a named tuple of the cleared input values and a status message.
194
+ """
195
+ self.inputs = self.INITIAL_STATE.copy()
196
+ self.graph.clear()
197
+ return ResetState(**self.inputs, message="Landscape cleared. You can start a new lesson plan.")
198
+
199
+ def graph_to_json(self) -> str:
200
+ """
201
+ Converts the current lesson plan graph into a JSON string format and returns the result.
202
+ """
203
+ try:
204
+ graph_data = {
205
+ "nodes": [
206
+ {
207
+ "id": node,
208
+ "type": self.graph.nodes[node]["type"].value,
209
+ "description": self.graph.nodes[node].get("description", "")
210
+ }
211
+ for node in self.graph.nodes()
212
+ ],
213
+ "edges": [
214
+ {
215
+ "source": u,
216
+ "target": v,
217
+ "relationship": self.graph.edges[u, v]["relationship"]
218
+ }
219
+ for u, v in self.graph.edges()
220
+ ]
221
+ }
222
+ return json.dumps(graph_data, indent=4)
223
+ except (KeyError, TypeError) as e:
224
+ return f"An error occurred while converting the graph to JSON: {str(e)}"
225
+
226
+ def process_inputs(self, *args) -> Tuple[str, Optional[Image.Image]]:
227
+ """
228
+ Process input arguments and create a lesson plan.
229
+ Returns a tuple of search string and graph image, or error message and None.
230
+ """
231
+ try:
232
+ self.inputs.update(dict(zip(self.INITIAL_STATE.keys(), args)))
233
+ return self.add_lesson_plan(**self.inputs)
234
+ except ValueError as e:
235
+ return str(e), None
236
+
237
+ @property
238
+ def is_empty(self) -> bool:
239
+ """Check if all inputs are empty."""
240
+ return all(value == "" for value in self.inputs.values())
241
+
242
+ def __repr__(self) -> str:
243
+ return f"LessonGraph(inputs={self.inputs}, graph_size={len(self.graph)})"