-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathteaching_angle_generator.py
More file actions
235 lines (193 loc) · 7.11 KB
/
teaching_angle_generator.py
File metadata and controls
235 lines (193 loc) · 7.11 KB
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# teaching_angle_generator.py
# Dynamically generates a list of teaching angles/lenses for classroom sessions.
#
# Mirrors the pattern in story_genre_generator.py:
# 1. LLM generates 50 angles at high temperature
# 2. Python picks a random angle from the list
# 3. The list is cached to disk and refreshed when exhausted or stale
#
# Key functions:
# generate_teaching_angles(basil_assessment) -> list[str]
# pick_angle(basil_assessment) -> str
import os
import json
import random
from datetime import datetime
from openai import OpenAI
from config import (
TASK_AGENT_MODEL,
PROMPT_TEACHING_ANGLE_GENERATOR,
TEACHING_ANGLES_FILE,
CLASSROOM_DIR,
)
from file_lock_utils import get_lock
from llm_client import create_smart_client
client = create_smart_client()
FALLBACK_ANGLES = [
"historical origins",
"scientific principles behind",
"cultural traditions around the world",
"famous people and pioneers in",
"comparison across countries",
"everyday applications of",
"how it's made or built",
"the math and numbers behind",
"geography and places related to",
"artistic expression in",
"inventions and discoveries in",
"philosophical questions about",
"fun facts and world records",
"how it connects to faith and values",
"environmental and ecological impact",
"careers and jobs related to",
"how it has changed over time",
"mysteries and unsolved questions in",
"the future of",
"how it works at a microscopic level",
"stories and legends about",
"economics and trade related to",
"health and the human body connections",
"music and sound connections",
"architecture and design in",
"language and vocabulary of",
"animals and nature connections",
"technology and innovation in",
"sports and competition in",
"food and cooking connections",
]
def _load_prompt_template() -> str:
"""Load the teaching angle generator prompt template."""
if os.path.exists(PROMPT_TEACHING_ANGLE_GENERATOR):
with open(PROMPT_TEACHING_ANGLE_GENERATOR, "r") as f:
return f.read()
raise FileNotFoundError(
f"Teaching angle generator prompt not found: {PROMPT_TEACHING_ANGLE_GENERATOR}"
)
def _load_cached_angles() -> dict:
"""Load cached angles from disk. Returns full data dict or empty dict."""
if not os.path.exists(TEACHING_ANGLES_FILE):
return {}
try:
with open(TEACHING_ANGLES_FILE, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return {}
def _save_angles(angles: list, age_band: int):
"""Save angles list to the cache file."""
data = {
"generated_at": datetime.now().isoformat(),
"age_band": age_band,
"angles": angles,
}
os.makedirs(os.path.dirname(TEACHING_ANGLES_FILE), exist_ok=True)
with open(TEACHING_ANGLES_FILE, "w") as f:
json.dump(data, f, indent=2)
def generate_teaching_angles(
basil_assessment: dict,
recent_angles: list = None,
n: int = 50,
) -> list:
"""
Generate a diverse list of teaching angles/lenses via LLM.
Args:
basil_assessment: Dict with age_band, capabilities, etc.
recent_angles: List of recently used angle names to avoid.
n: Number of angles to request.
Returns:
List of angle name strings (deduplicated, filtered).
"""
age_band = basil_assessment.get("age_band", 0)
if recent_angles:
recent_text = ", ".join(recent_angles[-30:])
else:
recent_text = "(none)"
template = _load_prompt_template()
prompt = template.format(recent_angles=recent_text)
system_msg = f"Generate exactly {n} teaching angles. Output valid JSON only."
try:
response = client.chat.completions.create(
model=TASK_AGENT_MODEL,
messages=[
{"role": "system", "content": system_msg},
{"role": "user", "content": prompt},
],
temperature=1.0,
max_tokens=2000,
)
raw_output = response.choices[0].message.content.strip()
if raw_output.startswith("```"):
lines = raw_output.split("\n")
raw_output = "\n".join(lines[1:-1])
data = json.loads(raw_output)
angles_list = data.get("angles", [])
seen = set()
recent_lower = set(a.lower().strip() for a in (recent_angles or []))
filtered = []
for angle in angles_list:
if isinstance(angle, str):
angle = angle.strip()
if not angle:
continue
key = angle.lower()
if key in seen or key in recent_lower:
continue
seen.add(key)
filtered.append(angle)
print(f"[TeachingAngle] Generated {len(filtered)} angles for age_band={age_band}")
_save_angles(filtered, age_band)
return filtered
except json.JSONDecodeError as e:
print(f"[TeachingAngle] JSON parse error: {e}")
return FALLBACK_ANGLES[:]
except Exception as e:
print(f"[TeachingAngle] Error: {e}")
return FALLBACK_ANGLES[:]
def pick_angle(basil_assessment: dict, used_angles_this_session: list = None) -> str:
"""
Pick a random teaching angle for the current classroom session.
Loads the cached angle list, regenerating it if empty or missing.
Avoids angles already used in this session (if provided).
Args:
basil_assessment: Dict with age_band, capabilities, etc.
used_angles_this_session: Angles already tried this session (for retries).
Returns:
An angle string, e.g. "historical origins".
"""
used_this_session = set(
a.lower().strip() for a in (used_angles_this_session or [])
)
with get_lock(TEACHING_ANGLES_FILE):
cached = _load_cached_angles()
angles = cached.get("angles", [])
available = [a for a in angles if a.lower().strip() not in used_this_session]
if len(available) < 5:
print(f"[TeachingAngle] Only {len(available)} angles available, regenerating...")
angles = generate_teaching_angles(
basil_assessment,
recent_angles=list(used_this_session),
)
available = [a for a in angles if a.lower().strip() not in used_this_session]
if not available:
available = [
a for a in FALLBACK_ANGLES
if a.lower().strip() not in used_this_session
]
if not available:
available = FALLBACK_ANGLES[:]
chosen = random.choice(available)
return chosen
if __name__ == "__main__":
print("Testing Teaching Angle Generator...")
print()
test_assessment = {
"age_band": 2,
"capabilities": ["Some word production", "Basic pattern recognition"],
}
angles = generate_teaching_angles(test_assessment)
print(f"\nGenerated {len(angles)} angles:")
for i, angle in enumerate(angles, 1):
print(f" {i:>3}. {angle}")
print(f"\n--- Random picks ---")
for _ in range(5):
angle = pick_angle(test_assessment)
print(f" Picked: {angle}")