Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 44 additions & 16 deletions bases/rsptx/assignment_server_api/routers/peer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down Expand Up @@ -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,
):
Expand All @@ -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}
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion components/rsptx/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment thread
sethbern marked this conversation as resolved.


AssignmentQuestionValidator: TypeAlias = sqlalchemy_to_pydantic(AssignmentQuestion) # type: ignore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ <h3>Question {{ current_qnum }} of {{ num_questions }}</h3>
}

async function updateCount() {
let data = { div_id: "{{ current_question.name }}", start_time: startTime, course_name: "{{ course.course_name }}" };
let data = { div_id: "{{ current_question.name }}", start_time: startTime, course_name: "{{ course.course_name }}", message_start_time: startTime };
if (startTime2 !== null) data.start_time = startTime2;
let jsheaders = new Headers({ "Content-type": "application/json; charset=utf-8", Accept: "application/json" });
let resp = await fetch(new Request("/assignment/peer/api/num_answers", { method: "POST", headers: jsheaders, body: JSON.stringify(data) }));
Expand Down Expand Up @@ -497,6 +497,7 @@ <h3>Question {{ current_qnum }} of {{ num_questions }}</h3>
<script>
var user = "{{ user.username }}";
var currentQuestion = "{{ current_question.name }}";
var assignment_id = {{ assignment_id }};
var voteNum = 1;

document.addEventListener("DOMContentLoaded", function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@
\( {{ latex_macros|safe }} \)
</div>

<h2>Peer Instruction Question (After Class)</h2>
<h2>Peer Instruction (After Class): {{ assignment_name }}</h2>

{% if not all_done %}
<div id="stepBanner" style="
Expand Down Expand Up @@ -412,7 +412,7 @@ <h3>Congratulations, you have completed this assignment!</h3>
<textarea id="messageText" placeholder="Write your reasoning here…" disabled></textarea>
<div class="pi-reasoning-footer">
<button id="submitReflection" type="button" class="btn btn-primary" disabled>
{% if llm_enabled %}Continue to discussion →{% else %}Submit and see responses →{% endif %}
{% if llm_enabled %}Continue to discussion →{% else %}Submit{% endif %}
</button>
</div>
</div>
Expand Down Expand Up @@ -1527,6 +1527,9 @@ <h3>Congratulations, you have completed this assignment!</h3>
document.querySelectorAll(".tm-opt").forEach(o => o.classList.remove("selected"));
btn.classList.add("selected");
window._selectedTheme = t.id;
// Drop any cached mapping from a previous theme so the backend
// regenerates it for the newly-chosen theme
window._analogyMapping = "";
ttLabelEl.textContent = t.label;
themeTriggerBtn.classList.remove("placeholder", "invalid");
closeThemeMenu();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ <h2>Peer Instruction: {{ assignment_name }}</h2>
</div>

<div id="pi-student-interface">
<div id="imessage"></div>
<div id="imessage"><h3>Time to make your 1st vote!</h3><p>Answer the question and click Submit.</p></div>

<div class="row">
<div class="col-md-6">
Expand Down
11 changes: 11 additions & 0 deletions components/rsptx/templates/staticAssets/js/peer.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,20 @@ function connect(event) {
// _catchup flag tells the handlers to skip side effects that should
// only happen on a live message (vote counting, page navigation).
ws.onopen = async function () {
if (typeof window.PI_ASYNC_MODE !== "undefined") return;
try {
let urlParams = new URLSearchParams(window.location.search);
let assignmentId = urlParams.get('assignment_id');
let resp = await fetch(`/assignment/peer/api/current_state?assignment_id=${assignmentId}`);
if (!resp.ok) return;
let phase = await resp.json();
if (phase && phase.message) {
// Ignore a stored phase that belongs to a different question since
// it's leftover state from a previous session, not the current one.
if (typeof currentQuestion !== "undefined" && phase.div_id && phase.div_id !== currentQuestion) {
console.log(`Catch-up: ignoring stale phase for ${phase.div_id}`);
return;
}
console.log(`Catch-up: re-applying phase ${phase.message}`);
phase._catchup = true;
ws.onmessage({ data: JSON.stringify(phase) });
Expand Down Expand Up @@ -639,6 +646,8 @@ function startVote2(event) {
message: "enableVote",
broadcast: true,
course_name: eBookConfig.course,
assignment_id: typeof assignment_id !== "undefined" ? assignment_id : null,
div_id: currentQuestion,
};
//ws.send(JSON.stringify(mess));
publishMessage(mess);
Expand Down Expand Up @@ -685,6 +694,8 @@ function enableNext() {
message: "enableNext",
broadcast: true,
course_name: eBookConfig.course,
assignment_id: typeof assignment_id !== "undefined" ? assignment_id : null,
div_id: currentQuestion,
};
if (typeof voteNum !== "undefined" && voteNum < 2) {
logPeerEvent({
Expand Down
35 changes: 35 additions & 0 deletions migrations/versions/d8e4f2b6c1a7_default_async_mode_to_llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""default assignment_questions.async_mode to llm

Revision ID: d8e4f2b6c1a7
Revises: b7f3c1a9d2e4
Create Date: 2026-07-10 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = 'd8e4f2b6c1a7'
down_revision: Union[str, None] = 'b7f3c1a9d2e4'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# New peer questions default to the generic LLM mode. Existing rows keep
# whatever mode they already have (standard/llm/analogies).
op.alter_column(
'assignment_questions',
'async_mode',
server_default=sa.text("'llm'"),
)
Comment thread
sethbern marked this conversation as resolved.


def downgrade() -> None:
op.alter_column(
'assignment_questions',
'async_mode',
server_default=sa.text("'standard'"),
)
Loading