diff --git a/bases/rsptx/assignment_server_api/routers/peer.py b/bases/rsptx/assignment_server_api/routers/peer.py index dd36e0bf9..bf9a3cc62 100644 --- a/bases/rsptx/assignment_server_api/routers/peer.py +++ b/bases/rsptx/assignment_server_api/routers/peer.py @@ -221,9 +221,11 @@ async def get_peer_dashboard( "message": "enableNext", "broadcast": True, "course_name": course.course_name, + "div_id": current_question.name, } r.publish("peermessages", json.dumps(mess)) r.hset(f"{course.course_name}_state", "current_phase", json.dumps(mess)) + r.delete(f"assignment_{assignment_id}_state") except Exception as e: rslogger.error(f"Error initializing Redis: {e}") @@ -1105,6 +1107,7 @@ async def get_num_answers( div_id: str = Body(...), start_time: str = Body(...), course_name: str = Body(...), + message_start_time: Optional[str] = Body(None), user=Depends(auth_manager), course=None, ): @@ -1127,8 +1130,14 @@ async def get_num_answers( div_id, course_name, start_time_dt, exclude_sid=user.username ) - # Count messages sent - message_count = await count_peer_messages(div_id, course_name, start_time_dt) + # Messages are counted from the start of the question, not the current + # vote, so the count doesn't reset to 0 when vote 2 starts + mess_start_dt = ( + parse(message_start_time).replace(tzinfo=None) + if message_start_time + else start_time_dt + ) + message_count = await count_peer_messages(div_id, course_name, mess_start_dt) return JSONResponse( content={"count": answer_count, "mess_count": message_count} @@ -1256,6 +1265,10 @@ async def publish_message( "enableFaceChat", ): r.hset(f"{course.course_name}_state", "current_phase", json.dumps(data)) + if data.get("message") in ("enableVote", "enableNext") and data.get( + "assignment_id" + ): + r.delete(f"assignment_{data['assignment_id']}_state") # Track message count for text messages if data.get("type") == "text": @@ -1479,6 +1492,8 @@ async def get_async_explainer( all_msgs = [] last_per_sid = {} for row in messages: + if row.event == "sendmessage" and row.act.startswith("to:llm:"): + continue if row.event == "reflection": msg = row.act else: @@ -1807,7 +1822,7 @@ async def get_async_llm_reflection( f"never use CS terms or file-system terminology while in the analogy — stay fully in '{theme_label}' language.\n\n" ) - base_rules = ( + common_rules = ( "only speak in lower case.\n" "you are a student talking to another student during peer instruction.\n" "you are both looking at the same multiple choice question with code and answers.\n" @@ -1823,21 +1838,10 @@ async def get_async_llm_reflection( "never say something is right or wrong.\n" "STRICT RULE: every message must begin with one of these allowed openers: a question word ('what', 'where', 'how', 'do', 'can', 'if', 'wait', 'hmm'), or the word 'so' followed immediately by a scenario observation (not 'so exactly' or 'so right'). never begin with 'yeah', 'right', 'exactly', 'correct', 'yes', 'yep', 'true', 'good', 'nice', 'great', 'perfect', 'cool', 'totally', 'sure', 'got it', or any variation. these imply the student is correct. check your first word before every message.\n" "never confirm or explain the code outcome after the student says something — only ask them to keep tracing or elaborate.\n" - "if the student introduces a new assumption or claim, do not build on it as if it is correct — instead use the analogy to make them test that assumption themselves. for example if they claim 'it creates the directory' ask them through the analogy: 'does trying to walk to a floor that doesn't exist create it, or does something else happen?' never validate the assumption, never deny it — just ask them to examine it through the scenario.\n" - "if an analogy scenario was established, always bring follow-up questions back through that scenario — do not abandon it for direct code talk. never use CS terms (like 'staging', 'commit', 'directory', 'repository', 'file path') in a message that is otherwise in analogy vocabulary — stay fully in the theme language until you are explicitly bridging back.\n" "each follow-up must move the conversation forward — do not ask the same question twice in different words. if the student answered your last question, accept that and go one level deeper or bridge back to the actual problem.\n" - "when referencing a structural detail of the scenario in a follow-up, briefly restate that detail in the same message — do not assume the student remembers the exact structure from the first message. for example: 'remember in that tree the grandparent has two children — user and shared. you are currently under user...' then ask your question.\n" - "never say things like 'let's focus on the code' or 'going back to the code' — always route through the scenario instead.\n" - "if the student themselves references the analogy, use that as an opening to deepen it — never redirect away from it.\n" - "once the student has traced through the analogy and seems to understand the structure, explicitly bridge back to the question — ask them to apply that same reasoning to the actual problem values or code.\n" - "do not let the analogy float indefinitely without connecting it back to the question. the goal is for the student to say 'oh so in the question that means...' — guide them there.\n" "never use phrases like 'not quite' or 'not exactly' or 'almost' or 'close' or 'not yet' or any phrase that implies the student is incorrect.\n" "never react to whether the student's answer is correct or incorrect — only ask them to explain their reasoning.\n" - "never use the analogy to imply the student's answer is wrong.\n" "never end a message with a rhetorical question whose obvious answer signals the student is wrong — like 'does that room just appear?' or 'is there really a backup area there?' — these tell the student they are wrong.\n" - "instead: have the student trace through the scenario step by step. ask where they end up after each step, or ask them to walk you through what they think each part of the command does in the scenario. keep questions open — 'where does that put you?' not 'does that even exist?'\n" - "the student should discover whether their answer is right or wrong by tracing through the analogy themselves.\n" - "never connect the analogy conclusion back to a specific answer choice — do not say things like 'answer B says you need to be in X — does that match up with needing to be in the new server?' because this confirms the answer without saying it. if the student has traced through the analogy and reached a conclusion ask them what that tells them about the problem and let THEM connect it back to their choice — never make that connection for them.\n" "do not pretend to have picked an answer yourself.\n" "never mention a choice letter as the correct answer.\n" "if the question includes code never clearly describe the final result or fully state what it prints.\n" @@ -1854,11 +1858,34 @@ async def get_async_llm_reflection( "do not make up information that is not in the question.\n" "if you are unsure about something say so honestly instead of guessing.\n" "if the other student mentions the same answer more than once or sounds confident in their answer you must tell them to go ahead and vote again — this overrides everything else.\n" - "do not ask another question or continue the analogy after they have confirmed their answer — just tell them to vote.\n" + "do not ask another question or continue reasoning after they have confirmed their answer — just tell them to vote.\n" "do not continue reasoning after telling them to vote again.\n" "focus on getting them to think through the problem not on changing their mind.\n\n" ) + # Analogy-specific rules only apply when the student picked a theme + analogy_flow_rules = ( + "if the student introduces a new assumption or claim, do not build on it as if it is correct — instead use the analogy to make them test that assumption themselves. for example if they claim 'it creates the directory' ask them through the analogy: 'does trying to walk to a floor that doesn't exist create it, or does something else happen?' never validate the assumption, never deny it — just ask them to examine it through the scenario.\n" + "if an analogy scenario was established, always bring follow-up questions back through that scenario — do not abandon it for direct code talk. never use CS terms (like 'staging', 'commit', 'directory', 'repository', 'file path') in a message that is otherwise in analogy vocabulary — stay fully in the theme language until you are explicitly bridging back.\n" + "when referencing a structural detail of the scenario in a follow-up, briefly restate that detail in the same message — do not assume the student remembers the exact structure from the first message. for example: 'remember in that tree the grandparent has two children — user and shared. you are currently under user...' then ask your question.\n" + "never say things like 'let's focus on the code' or 'going back to the code' — always route through the scenario instead.\n" + "if the student themselves references the analogy, use that as an opening to deepen it — never redirect away from it.\n" + "once the student has traced through the analogy and seems to understand the structure, explicitly bridge back to the question — ask them to apply that same reasoning to the actual problem values or code.\n" + "do not let the analogy float indefinitely without connecting it back to the question. the goal is for the student to say 'oh so in the question that means...' — guide them there.\n" + "never use the analogy to imply the student's answer is wrong.\n" + "have the student trace through the scenario step by step. ask where they end up after each step, or ask them to walk you through what they think each part of the command does in the scenario. keep questions open — 'where does that put you?' not 'does that even exist?'\n" + "the student should discover whether their answer is right or wrong by tracing through the analogy themselves.\n" + "never connect the analogy conclusion back to a specific answer choice — do not say things like 'answer B says you need to be in X — does that match up with needing to be in the new server?' because this confirms the answer without saying it. if the student has traced through the analogy and reached a conclusion ask them what that tells them about the problem and let THEM connect it back to their choice — never make that connection for them.\n\n" + ) + + generic_flow_rules = ( + "stay grounded in the actual question and code — do not invent metaphors analogies or hypothetical scenarios. talk directly about the code and what it does.\n" + "if the student introduces a new assumption or claim, do not build on it as if it is correct — instead ask them to test that assumption by tracing through the actual code or question themselves. never validate the assumption never deny it — just ask them to examine it.\n" + "have the student trace through the code step by step. ask what each line does and where they end up. keep questions open — 'what does that line do?' not 'does that even work?'.\n" + "the student should discover whether their answer is right or wrong by tracing through the code themselves.\n" + "never connect a conclusion back to a specific answer choice — if the student reaches a conclusion ask them what that tells them about the problem and let THEM connect it back to their choice.\n\n" + ) + context_suffix = "" if question: context_suffix += f"question:\n{question}\n\n" @@ -1869,7 +1896,8 @@ async def get_async_llm_reflection( if selected: context_suffix += f"the other student chose: {selected}\n\n" - sys_content = analogy_preamble + base_rules + context_suffix + flow_rules = analogy_flow_rules if theme_id else generic_flow_rules + sys_content = analogy_preamble + common_rules + flow_rules + context_suffix system_msg = {"role": "system", "content": sys_content} if not messages: diff --git a/components/rsptx/db/models.py b/components/rsptx/db/models.py index ef25b2ab3..ff3e52903 100644 --- a/components/rsptx/db/models.py +++ b/components/rsptx/db/models.py @@ -688,7 +688,7 @@ class AssignmentQuestion(Base, IdMixin): activities_required = Column( Integer ) # only reading assignments will have this populated - async_mode = Column(String(20), server_default="standard") + async_mode = Column(String(20), server_default="llm") AssignmentQuestionValidator: TypeAlias = sqlalchemy_to_pydantic(AssignmentQuestion) # type: ignore diff --git a/components/rsptx/templates/assignment/instructor/peer_dashboard.html b/components/rsptx/templates/assignment/instructor/peer_dashboard.html index 06b9f0351..032b15a22 100644 --- a/components/rsptx/templates/assignment/instructor/peer_dashboard.html +++ b/components/rsptx/templates/assignment/instructor/peer_dashboard.html @@ -456,7 +456,7 @@