-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsubject_generator.py
More file actions
289 lines (239 loc) · 9.11 KB
/
Copy pathsubject_generator.py
File metadata and controls
289 lines (239 loc) · 9.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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# subject_generator.py
# Dynamically generates a list of age-appropriate school subjects per session.
#
# Key function:
# generate_subject_candidates(basil_assessment, recent_subjects, n_min, n_max) -> list[str]
import os
import json
from datetime import datetime
from openai import OpenAI
from config import TASK_AGENT_MODEL, PROMPT_SUBJECT_GENERATOR, SUBJECTS_FILE
from curriculum_manager import normalize_subject
from llm_client import create_smart_client
client = create_smart_client()
# Age band to human-ish age descriptor
AGE_BAND_TO_DESC = {
0: "newborn (0-6 months old)",
1: "infant (6-12 months old)",
2: "toddler (12-18 months old)",
3: "toddler (18-24 months old)",
4: "preschool age (2-3 years old)",
5: "kindergarten age (4-5 years old)",
6: "early elementary age (6-8 years old)",
7: "late elementary / middle school age (9-12 years old)",
}
def load_prompt_template() -> str:
"""Load the subject generator prompt template."""
if os.path.exists(PROMPT_SUBJECT_GENERATOR):
with open(PROMPT_SUBJECT_GENERATOR, "r") as f:
return f.read()
raise FileNotFoundError(f"Subject generator prompt not found: {PROMPT_SUBJECT_GENERATOR}")
def generate_subject_candidates(
basil_assessment: dict,
recent_subjects: list,
n_min: int = 20,
n_max: int = 50,
) -> list:
"""
Generate a list of age-appropriate school subjects for this session.
Args:
basil_assessment: Dict with age_band, capabilities, etc.
recent_subjects: List of recently used subject names to avoid
n_min: Minimum number of candidates to return (will retry if needed)
n_max: Maximum number of candidates to request from LLM
Returns:
List of subject name strings (deduplicated, filtered, display versions)
"""
age_band = basil_assessment.get("age_band", 0)
age_desc = AGE_BAND_TO_DESC.get(age_band, AGE_BAND_TO_DESC[0])
# Format capabilities
capabilities = basil_assessment.get("capabilities", [])
if isinstance(capabilities, list):
capabilities_text = "\n".join(f"- {c}" for c in capabilities)
else:
capabilities_text = str(capabilities)
# Normalize recent subjects for comparison
recent_normalized = set(normalize_subject(s) for s in recent_subjects)
# Format recent subjects for prompt
if recent_subjects:
recent_subjects_text = ", ".join(recent_subjects[-25:]) # Last 25 for prompt
else:
recent_subjects_text = "(none)"
# First attempt
candidates = _call_subject_generator(
age_desc=age_desc,
capabilities=capabilities_text,
recent_subjects_text=recent_subjects_text,
n_max=n_max,
)
# Filter out subjects that match recent subjects (normalized comparison)
filtered = []
seen_normalized = set()
for subj in candidates:
normalized = normalize_subject(subj)
# Skip if matches recent subject
if normalized in recent_normalized:
continue
# Skip if duplicate within this batch
if normalized in seen_normalized:
continue
seen_normalized.add(normalized)
filtered.append(subj)
# Check if we have enough candidates
if len(filtered) < n_min:
print(f"[Subject Generator] Only {len(filtered)} candidates after filtering, retrying with broader request...")
# Retry with instruction to be more general
retry_candidates = _call_subject_generator(
age_desc=age_desc,
capabilities=capabilities_text,
recent_subjects_text=recent_subjects_text,
n_max=n_max,
broader=True,
)
# Add new candidates that aren't duplicates
for subj in retry_candidates:
normalized = normalize_subject(subj)
if normalized not in recent_normalized and normalized not in seen_normalized:
seen_normalized.add(normalized)
filtered.append(subj)
# If still not enough, relax blacklist (use only last 10)
if len(filtered) < n_min and len(recent_subjects) > 10:
print(f"[Subject Generator] Still only {len(filtered)} candidates, relaxing blacklist...")
recent_relaxed = set(normalize_subject(s) for s in recent_subjects[-10:])
# Re-filter with relaxed blacklist
for subj in candidates:
normalized = normalize_subject(subj)
if normalized not in recent_relaxed and normalized not in seen_normalized:
seen_normalized.add(normalized)
filtered.append(subj)
print(f"[Subject Generator] Generated {len(filtered)} subject candidates for age_band={age_band}")
# Save candidates to JSON for debugging visibility
try:
data = {
"generated_at": datetime.now().isoformat(),
"age_band": age_band,
"candidates": filtered,
}
os.makedirs(os.path.dirname(SUBJECTS_FILE), exist_ok=True)
with open(SUBJECTS_FILE, "w") as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"[Subject Generator] Warning: Failed to save candidates to JSON: {e}")
return filtered
def _call_subject_generator(
age_desc: str,
capabilities: str,
recent_subjects_text: str,
n_max: int,
broader: bool = False,
) -> list:
"""
Call the LLM to generate subject candidates.
Args:
age_desc: Human-readable age description
capabilities: Formatted capabilities text
recent_subjects_text: Comma-separated recent subjects
n_max: Max subjects to request
broader: If True, request broader/more general subjects
Returns:
List of subject name strings (raw from LLM, not filtered)
"""
template = load_prompt_template()
prompt = template.format(
age_desc=age_desc,
capabilities=capabilities,
recent_subjects=recent_subjects_text,
)
# Add broader instruction if needed
if broader:
prompt += "\n\nIMPORTANT: Be more general and creative. Include broader subject categories and less common but still child-appropriate subjects."
system_msg = f"Generate exactly {n_max} school subjects. 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=0.9, # Higher temp for variety
max_tokens=1500,
)
raw_output = response.choices[0].message.content.strip()
# Parse JSON, handling potential markdown code blocks
if raw_output.startswith("```"):
lines = raw_output.split("\n")
raw_output = "\n".join(lines[1:-1])
data = json.loads(raw_output)
# Extract subject names
subjects_list = data.get("subjects", [])
names = []
for item in subjects_list:
if isinstance(item, dict) and "name" in item:
name = item["name"].strip()
if name:
names.append(name)
elif isinstance(item, str):
name = item.strip()
if name:
names.append(name)
return names
except json.JSONDecodeError as e:
print(f"[Subject Generator] JSON parse error: {e}")
print(f"[Subject Generator] Raw output: {raw_output[:500]}...")
return _fallback_subjects()
except Exception as e:
print(f"[Subject Generator] Error: {e}")
return _fallback_subjects()
def _fallback_subjects() -> list:
"""Return fallback subjects if generation fails."""
return [
"Language Arts",
"Reading",
"Writing",
"Mathematics",
"Numbers",
"Shapes",
"Science",
"Nature",
"Animals",
"Weather",
"Art",
"Music",
"Health",
"Safety",
"Feelings",
"Manners",
"Colors",
"Counting",
"Patterns",
"Stories",
"Bible Stories",
"Saints",
"Prayer",
"Angels",
]
if __name__ == "__main__":
# Test the subject generator
print("Testing Subject Generator...")
print()
# Test with different age bands
for age_band in [0, 2, 4]:
print(f"=== Age Band {age_band} ===")
test_assessment = {
"age_band": age_band,
"capabilities": [
"Basic pattern recognition",
"Some word production",
],
}
recent = ["Mathematics", "Science", "Reading"]
candidates = generate_subject_candidates(
basil_assessment=test_assessment,
recent_subjects=recent,
n_min=10,
n_max=25,
)
print(f"Generated {len(candidates)} candidates:")
for i, subj in enumerate(candidates, 1):
print(f" {i:>3}. {subj}")
print()