Skip to content

Commit cc96afa

Browse files
committed
refactoring of question context prompt
1 parent d9f8b64 commit cc96afa

File tree

6 files changed

+592
-270
lines changed

6 files changed

+592
-270
lines changed
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
"""
2+
Refactored JSON to prompt parser using improved, clearer structure.
3+
"""
4+
5+
from typing import List, Optional, Dict, Any, Union
6+
from .prompt_context_templates import PromptFormatter
7+
8+
# Definitions questionSubmissionSummary type
9+
class StudentLatestSubmission:
10+
def __init__(
11+
self,
12+
universalResponseAreaId: Optional[str] = None,
13+
answer: Optional[str] = None,
14+
submission: Optional[str] = None,
15+
feedback: Optional[str] = None,
16+
rawResponse: Optional[dict] = None,
17+
):
18+
self.universalResponseAreaId = universalResponseAreaId
19+
self.answer = answer
20+
self.submission = submission
21+
self.feedback = feedback
22+
self.rawResponse = rawResponse
23+
24+
class StudentWorkResponseArea:
25+
def __init__(
26+
self,
27+
publishedPartId: Optional[str] = None,
28+
publishedPartPosition: Optional[int] = None,
29+
publishedResponseAreaId: Optional[str] = None,
30+
publishedResponseAreaPosition: Optional[int] = None,
31+
responseAreaUniversalId: Optional[str] = None,
32+
publishedResponseAreaPreResponseText: Optional[str] = None,
33+
publishedResponseType: Optional[str] = None,
34+
publishedResponseConfig: Optional[dict] = None,
35+
totalSubmissions: Optional[int] = None,
36+
totalWrongSubmissions: Optional[int] = None,
37+
latestSubmission: Optional[StudentLatestSubmission] = None,
38+
):
39+
self.publishedPartId = publishedPartId
40+
self.publishedPartPosition = publishedPartPosition
41+
self.publishedResponseAreaId = publishedResponseAreaId
42+
self.publishedResponseAreaPosition = publishedResponseAreaPosition
43+
self.responseAreaUniversalId = responseAreaUniversalId
44+
self.publishedResponseAreaPreResponseText = publishedResponseAreaPreResponseText
45+
self.publishedResponseType = publishedResponseType
46+
self.publishedResponseConfig = publishedResponseConfig
47+
self.latestSubmission = StudentLatestSubmission(**latestSubmission) if latestSubmission else None
48+
self.totalSubmissions = totalSubmissions
49+
self.totalWrongSubmissions = totalWrongSubmissions
50+
51+
# questionInformation type
52+
class ResponseAreaDetails:
53+
def __init__(
54+
self,
55+
id: Optional[str] = None,
56+
position: Optional[int] = None,
57+
universalResponseAreaId: Optional[str] = None,
58+
preResponseText: Optional[str] = None,
59+
responseType: Optional[str] = None,
60+
answer: Optional[dict] = None,
61+
Response: Optional[dict] = None,
62+
):
63+
self.id = id
64+
self.position = position
65+
self.universalResponseAreaId = universalResponseAreaId
66+
self.preResponseText = preResponseText
67+
self.responseType = responseType
68+
self.answer = answer
69+
self.Response = Response
70+
71+
class PartDetails:
72+
def __init__(
73+
self,
74+
publishedPartId: Optional[str] = None,
75+
publishedPartPosition: Optional[int] = None,
76+
publishedPartContent: Optional[str] = None,
77+
publishedPartAnswerContent: Optional[str] = None,
78+
publishedWorkedSolutionSections: Optional[List[dict]] = [],
79+
publishedResponseAreas: Optional[List[Optional[ResponseAreaDetails]]] = [],
80+
):
81+
self.publishedPartId = publishedPartId
82+
self.publishedPartPosition = publishedPartPosition
83+
self.publishedPartContent = publishedPartContent
84+
self.publishedPartAnswerContent = publishedPartAnswerContent
85+
self.publishedWorkedSolutionSections = publishedWorkedSolutionSections
86+
self.publishedResponseAreas = [ResponseAreaDetails(**publishedResponseArea) for publishedResponseArea in publishedResponseAreas]
87+
88+
class QuestionDetails:
89+
def __init__(
90+
self,
91+
setNumber: Optional[int] = None,
92+
setName: Optional[str] = None,
93+
setDescription: Optional[str] = None,
94+
questionNumber: Optional[int] = None,
95+
questionTitle: Optional[str] = None,
96+
questionGuidance: Optional[str] = None,
97+
questionContent: Optional[str] = None,
98+
durationLowerBound: Optional[int] = None,
99+
durationUpperBound: Optional[int] = None,
100+
parts: Optional[List[PartDetails]] = [],
101+
):
102+
self.setNumber = setNumber
103+
self.setName = setName
104+
self.setDescription = setDescription
105+
self.questionNumber = questionNumber
106+
self.questionTitle = questionTitle
107+
self.questionGuidance = questionGuidance
108+
self.questionContent = questionContent
109+
self.durationLowerBound = durationLowerBound
110+
self.durationUpperBound = durationUpperBound
111+
self.parts = [PartDetails(**part) for part in parts]
112+
113+
# questionAccessInformation type
114+
class CurrentPart:
115+
def __init__(
116+
self,
117+
id: str = None,
118+
position: int = None,
119+
universalPartId: Optional[str] = None,
120+
timeTakenPart: Optional[str] = None,
121+
markedDonePart: Optional[str] = None
122+
):
123+
self.id = id
124+
self.position = position
125+
self.universalPartId = universalPartId
126+
self.timeTakenPart = timeTakenPart
127+
self.markedDonePart = markedDonePart
128+
129+
class QuestionAccessInformation:
130+
def __init__(
131+
self,
132+
estimatedMinimumTime: Optional[str] = None,
133+
estimaredMaximumTime: Optional[str] = None,
134+
timeTaken: Optional[str] = None,
135+
accessStatus: Optional[str] = None,
136+
markedDone: Optional[str] = None,
137+
currentPart: Optional[Dict[str, Union[str, int]]] = {},
138+
):
139+
self.estimatedMinimumTime = estimatedMinimumTime
140+
self.estimaredMaximumTime = estimaredMaximumTime
141+
self.timeTaken = timeTaken
142+
self.accessStatus = accessStatus
143+
self.markedDone = markedDone
144+
self.currentPart = CurrentPart(**currentPart)
145+
146+
147+
def parse_json_to_structured_prompt(
148+
question_submission_summary: Optional[List[StudentWorkResponseArea]],
149+
question_information: Optional[QuestionDetails],
150+
question_access_information: Optional[QuestionAccessInformation]
151+
) -> Optional[str]:
152+
"""
153+
Parse JSON data into a well-structured, LLM-friendly prompt.
154+
155+
Args:
156+
question_submission_summary: Student's work and submissions
157+
question_information: Question details and structure
158+
question_access_information: Current progress and timing info
159+
160+
Returns:
161+
Formatted prompt string or error message
162+
"""
163+
164+
if not question_information:
165+
return PromptFormatter.format_error_message()
166+
167+
# Convert to proper objects
168+
submission_summary = [StudentWorkResponseArea(**summary) for summary in question_submission_summary]
169+
question_info = QuestionDetails(**question_information)
170+
access_info = QuestionAccessInformation(**question_access_information) if question_access_information else None
171+
172+
# TODO: EXPERIMENTAL - Remove later
173+
# if question_info.setNumber is not None:
174+
# if (question_info.setNumber + 1) % 2 != 0:
175+
# return PromptFormatter.format_no_context_message()
176+
177+
# Build prompt sections
178+
sections = []
179+
180+
# 1. Question Header
181+
current_part_letter = None
182+
if access_info and access_info.currentPart:
183+
current_part_letter = PromptFormatter.get_part_letter(access_info.currentPart.position)
184+
185+
set_info = {
186+
'number': question_info.setNumber,
187+
'name': question_info.setName
188+
}
189+
190+
question_data = {
191+
'number': question_info.questionNumber,
192+
'title': question_info.questionTitle,
193+
'guidance': question_info.questionGuidance,
194+
'content': question_info.questionContent,
195+
'duration_lower': question_info.durationLowerBound,
196+
'duration_upper': question_info.durationUpperBound
197+
}
198+
199+
sections.append(PromptFormatter.format_question_header(
200+
set_info,
201+
question_data,
202+
current_part_letter
203+
))
204+
205+
# 2. Progress Summary (if available)
206+
if access_info:
207+
progress_section = PromptFormatter.format_progress_summary(
208+
access_info.timeTaken,
209+
access_info.accessStatus,
210+
access_info.markedDone
211+
)
212+
if progress_section:
213+
sections.append(progress_section)
214+
215+
# 3. Parts Details
216+
for part in question_info.parts:
217+
sections.append(_format_single_part(
218+
part,
219+
access_info.currentPart if access_info else None,
220+
submission_summary
221+
))
222+
223+
# 4. Combine into final prompt
224+
return PromptFormatter.format_complete_prompt(sections)
225+
226+
227+
def _format_single_part(
228+
part: PartDetails,
229+
current_part: Optional[CurrentPart],
230+
submissions: List[StudentWorkResponseArea]
231+
) -> str:
232+
"""Format a single part with all its components."""
233+
234+
if not part:
235+
return ""
236+
237+
part_sections = []
238+
part_letter = PromptFormatter.get_part_letter(part.publishedPartPosition)
239+
240+
# Determine if this is the current part
241+
is_current = current_part and current_part.id == part.publishedPartId
242+
time_on_part = current_part.timeTakenPart if is_current and current_part else None
243+
244+
# 1. Part Header
245+
part_sections.append(PromptFormatter.format_part_header(
246+
part_letter,
247+
is_current,
248+
time_on_part
249+
))
250+
251+
# 2. Part Content
252+
part_sections.append(PromptFormatter.format_part_content(part.publishedPartContent))
253+
254+
# 3. Response Areas
255+
response_areas = []
256+
for response_area in part.publishedResponseAreas:
257+
student_work = _extract_student_work_for_area(response_area, submissions)
258+
response_areas.append(PromptFormatter.format_single_response_area(
259+
response_area.position,
260+
response_area.preResponseText,
261+
response_area.answer,
262+
student_work
263+
))
264+
265+
if response_areas:
266+
part_sections.append(PromptFormatter.format_response_areas(response_areas))
267+
268+
# 4. Final Part Answer
269+
part_sections.append(PromptFormatter.format_part_answer(part.publishedPartAnswerContent))
270+
271+
# 5. Worked Solutions
272+
solutions_data = []
273+
if part.publishedWorkedSolutionSections:
274+
for ws in part.publishedWorkedSolutionSections:
275+
solutions_data.append({
276+
'title': ws.get('title', ''),
277+
'content': ws.get('content', ''),
278+
'position': ws.get('position', 0)
279+
})
280+
281+
part_sections.append(PromptFormatter.format_worked_solutions(solutions_data))
282+
283+
return "\n".join(part_sections) + "\n---\n"
284+
285+
286+
def _extract_student_work_for_area(
287+
response_area: ResponseAreaDetails,
288+
submissions: List[StudentWorkResponseArea]
289+
) -> Dict[str, Any]:
290+
"""Extract student work data for a specific response area."""
291+
292+
for submission in submissions:
293+
if (submission.publishedResponseAreaId == response_area.id and
294+
submission.latestSubmission):
295+
296+
return {
297+
'has_submissions': True,
298+
'latest_response': submission.latestSubmission.submission,
299+
'latest_feedback': submission.latestSubmission.feedback,
300+
'total_submissions': submission.totalSubmissions,
301+
'total_wrong': submission.totalWrongSubmissions
302+
}
303+
304+
return {'has_submissions': False}
305+
306+
307+
# Convenience function that maintains the original interface
308+
def parse_json_to_prompt(
309+
questionSubmissionSummary: Optional[List[StudentWorkResponseArea]],
310+
questionInformation: Optional[QuestionDetails],
311+
questionAccessInformation: Optional[QuestionAccessInformation]
312+
) -> Optional[str]:
313+
"""
314+
Legacy wrapper for backward compatibility.
315+
Recommended to use parse_json_to_structured_prompt for new code.
316+
"""
317+
return parse_json_to_structured_prompt(
318+
questionSubmissionSummary,
319+
questionInformation,
320+
questionAccessInformation
321+
)

0 commit comments

Comments
 (0)