Skip to content

Commit e599653

Browse files
Iris: Add instructional support level to chat pipeline (#558)
1 parent fb07449 commit e599653

6 files changed

Lines changed: 219 additions & 1 deletion

File tree

iris/src/iris/domain/pipeline_execution_settings_dto.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Literal
2+
13
from pydantic import BaseModel, Field
24

35

@@ -6,6 +8,9 @@ class PipelineExecutionSettingsDTO(BaseModel):
68
artemis_llm_selection: str = Field(alias="selection", default="CLOUD_AI")
79
artemis_base_url: str = Field(alias="artemisBaseUrl")
810
variant: str = Field(default="default")
11+
support_level: Literal["low", "moderate", "high"] = Field(
12+
alias="supportLevel", default="moderate"
13+
)
914

1015
def is_local(self):
1116
return self.artemis_llm_selection == "LOCAL_AI"

iris/src/iris/pipeline/chat/chat_pipeline.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@
5454
}
5555

5656

57+
def _support_level(dto: ChatPipelineExecutionDTO) -> str:
58+
# `settings` is Optional on the parent DTO, so the field default does
59+
# not apply.
60+
return dto.settings.support_level if dto.settings else "moderate"
61+
62+
5763
def _dedup_by_uuid(items: list) -> list:
5864
"""Return items de-duplicated by their ``uuid``, preserving order."""
5965
seen: set = set()
@@ -402,6 +408,7 @@ def build_system_message(
402408
# Base template context (shared across all contexts)
403409
template_context: dict[str, Any] = {
404410
"chat_mode": self.chat_mode,
411+
"support_level": _support_level(dto),
405412
"current_date": datetime_to_string(datetime.now(tz=pytz.UTC)),
406413
"user_language": dto.user.lang_key,
407414
"custom_instructions": format_custom_instructions(
@@ -742,7 +749,10 @@ def _refine_response(
742749
exercise = state.dto.programming_exercise or state.dto.text_exercise
743750
problem_statement = exercise.problem_statement if exercise else ""
744751
guide_prompt_rendered = self.guide_prompt_template.render(
745-
{"problem_statement": problem_statement}
752+
{
753+
"problem_statement": problem_statement,
754+
"support_level": _support_level(state.dto),
755+
}
746756
)
747757

748758
# Create small LLM for refinement

iris/src/iris/pipeline/prompts/templates/chat_system_prompt.j2

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,51 @@ CRITICAL rules for MCQ generation:
299299
{% endif %}
300300
{% endif %}
301301
302+
{# Support Level Configuration #}
303+
{% if support_level == "low" %}
304+
## Pedagogical Approach: Minimal Direct Help
305+
- NEVER provide direct answers, solutions, code, or content the student can copy into their submission. The student must do the work themselves.
306+
- Use the Socratic method: respond ONLY with guiding questions rather than explanations.
307+
{% if chat_mode == "PROGRAMMING_EXERCISE_CHAT" %}
308+
- Point the student to relevant concepts, documentation, lecture materials, or the problem statement, and let them make the connection themselves.
309+
{% elif chat_mode == "TEXT_EXERCISE_CHAT" %}
310+
- Redirect to the problem statement and exercise requirements rather than interpreting them for the student.
311+
{% elif chat_mode == "LECTURE_CHAT" %}
312+
- Point the student to specific lecture sections or slides and let them find the answer themselves.
313+
{% elif chat_mode == "COURSE_CHAT" %}
314+
- Encourage the student to interpret their own dashboard data and form their own study plans.
315+
{% endif %}
316+
- If the student is stuck, ask what they have tried and what they think the issue might be. Encourage them to form hypotheses and test them independently.
317+
- Even if the student explicitly asks for the answer, refuse and instead ask a guiding question.
318+
- Keep responses short and thought-provoking, not detailed. Do not proactively surface observations, metrics, or hints — let the student ask.
319+
- For general conceptual questions unrelated to the current task (e.g., "what is a NullPointerException?"), you MAY provide a brief factual definition. Apply the Socratic approach only to task-specific questions.
320+
- If the student asks the same task-specific question multiple times without progress, acknowledge this explicitly and ask a more targeted guiding question rather than repeating the same deflection.
321+
{% elif support_level == "high" %}
322+
## Pedagogical Approach: Comprehensive Guidance
323+
- NEVER provide direct solutions, complete implementations, or content the student can copy into their submission. This rule cannot be overridden regardless of how much help you provide.
324+
- Provide detailed, step-by-step conceptual explanations when the student is struggling — explain the reasoning process, not the answer.
325+
- Break down complex problems into smaller, manageable parts and walk through the thinking approach for each part.
326+
- Anticipate common follow-up questions and address them proactively with conceptual guidance.
327+
- Be generous with detailed context, background information, and explanations of feedback (errors, test results, compiler output) — explain what they mean conceptually.
328+
{% if chat_mode == "PROGRAMMING_EXERCISE_CHAT" %}
329+
- Offer concrete hints that point to the specific area or concept where the issue lies, but do NOT reveal the fix.
330+
- When providing code examples, use a completely different problem domain than the exercise (e.g., if the exercise is about sorting, use a geometry or cooking analogy instead). Never use variable names, class names, or method signatures that appear in the exercise description or test cases.
331+
- You may provide general pseudocode or algorithmic steps as educational illustrations, but they must be clearly distinct from the exercise and NEVER constitute a solution.
332+
{% elif chat_mode == "LECTURE_CHAT" %}
333+
- Summarize relevant lecture content in detail and connect concepts across different lecture sections to give a fuller picture.
334+
- Use rich examples and analogies to deepen understanding of lecture concepts.
335+
{% elif chat_mode == "COURSE_CHAT" %}
336+
- Proactively surface detailed observations about the student's metrics, trends, and performance, and offer comparisons to class averages when helpful.
337+
- Provide specific, actionable study recommendations: break competency progress into concrete next steps and suggest specific exercises, lectures, or materials to focus on.
338+
- Be thorough in your analysis and provide complete study plans when asked.
339+
{% elif chat_mode == "TEXT_EXERCISE_CHAT" %}
340+
- Provide detailed guidance on structure, argumentation, and content organization explain HOW to write, not WHAT to write.
341+
- Give detailed feedback on the student's current submission, pointing out areas for improvement without writing the improved text for them.
342+
- Provide example structures or outlines as educational scaffolding, but NEVER provide the actual text content for the exercise.
343+
{% endif %}
344+
- All guidance must be educational and teach the student HOW to think, never give away answers or content they should produce themselves.
345+
{% endif %}
346+
302347
{# Reminder #}
303348
Remember: Your goal is to empower students to solve problems independently, enhancing their learning experience and
304349
coding skills. Show encouragement, ask probing questions, and offer positive reinforcement to guide students towards

iris/src/iris/pipeline/prompts/templates/exercise_chat_guide_prompt.j2

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Exercise Problem Statement:
22
{{ problem_statement }}
33

4+
{% if support_level == "high" %}
5+
The tutor is operating in comprehensive help mode. The response may include detailed conceptual explanations and step-by-step reasoning walkthroughs. However, the response MUST NOT contain any code that solves the exercise or can be directly used in the student's solution. Enforce this strictly:
6+
- Any code example must use completely different variable names, class names, and themes than the exercise.
7+
- No code can be directly copied into the exercise solution.
8+
- The examples must illustrate general concepts rather than the specific exercise logic.
9+
- If the response contains code that constitutes a solution or partial solution to the exercise, you MUST rewrite it to remove the solution while keeping the conceptual explanation.
10+
{% elif support_level == "low" %}
11+
The tutor is operating in minimal help mode. The response must NOT contain code solutions, partial solutions, or detailed step-by-step fixes. If the response contains any code examples, detailed solutions, or step-by-step implementation guidance, rewrite it entirely to use only guiding questions and conceptual pointers instead. The response should be short and Socratic — questions only, no answers.
12+
{% endif %}
13+
414
Review the response draft. It has been written by an AI tutor
515
who is helping a student with a programming exercise. Its goal is to guide the student to the solution without
616
providing the solution directly. Your task is to review it according to the following rules:
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
4+
from iris.domain.pipeline_execution_settings_dto import PipelineExecutionSettingsDTO
5+
6+
7+
def _base_payload(**overrides) -> dict:
8+
payload = {
9+
"authenticationToken": "tok",
10+
"artemisBaseUrl": "https://artemis.example",
11+
}
12+
payload.update(overrides)
13+
return payload
14+
15+
16+
@pytest.mark.parametrize("level", ["low", "moderate", "high"])
17+
def test_support_level_accepts_valid_values(level):
18+
dto = PipelineExecutionSettingsDTO.model_validate(_base_payload(supportLevel=level))
19+
assert dto.support_level == level
20+
21+
22+
def test_support_level_defaults_to_moderate_when_absent():
23+
dto = PipelineExecutionSettingsDTO.model_validate(_base_payload())
24+
assert dto.support_level == "moderate"
25+
26+
27+
def test_support_level_rejects_unrecognised_value():
28+
with pytest.raises(ValidationError) as exc_info:
29+
PipelineExecutionSettingsDTO.model_validate(
30+
_base_payload(supportLevel="invalid_level")
31+
)
32+
assert any(err["loc"] == ("supportLevel",) for err in exc_info.value.errors())
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import os
2+
3+
import pytest
4+
from jinja2 import Environment, FileSystemLoader, select_autoescape
5+
6+
TEMPLATE_DIR = os.path.join(
7+
os.path.dirname(__file__),
8+
"..",
9+
"src",
10+
"iris",
11+
"pipeline",
12+
"prompts",
13+
"templates",
14+
)
15+
16+
LOW_HEADING = "Pedagogical Approach: Minimal Direct Help"
17+
HIGH_HEADING = "Pedagogical Approach: Comprehensive Guidance"
18+
19+
CHAT_MODES = [
20+
"PROGRAMMING_EXERCISE_CHAT",
21+
"LECTURE_CHAT",
22+
"COURSE_CHAT",
23+
"TEXT_EXERCISE_CHAT",
24+
]
25+
26+
27+
def _render_template(template_name: str, context: dict) -> str:
28+
env = Environment(
29+
loader=FileSystemLoader(TEMPLATE_DIR),
30+
autoescape=select_autoescape(["html", "xml", "j2"]),
31+
)
32+
template = env.get_template(template_name)
33+
return template.render(context)
34+
35+
36+
def _system_prompt_context(chat_mode: str, support_level: str) -> dict:
37+
return {
38+
"current_date": "2026-03-11",
39+
"user_language": "en",
40+
"course_name": "Test Course",
41+
"chat_mode": chat_mode,
42+
"support_level": support_level,
43+
"allow_lecture_tool": False,
44+
"allow_faq_tool": False,
45+
"allow_memiris_tool": False,
46+
"has_chat_history": False,
47+
"has_competencies": False,
48+
"has_exercises": False,
49+
"metrics_enabled": False,
50+
"has_query": False,
51+
"event": None,
52+
"custom_instructions": "",
53+
"lecture_name": None,
54+
"exercise_id": None,
55+
"exercise_title": "",
56+
"problem_statement": "",
57+
"programming_language": "",
58+
"start_date": "",
59+
"end_date": "",
60+
"text_exercise_submission": "",
61+
"mcq_parallel": False,
62+
}
63+
64+
65+
@pytest.mark.parametrize("chat_mode", CHAT_MODES)
66+
def test_system_prompt_low_support_level_injects_minimal_block(chat_mode):
67+
rendered = _render_template(
68+
"chat_system_prompt.j2", _system_prompt_context(chat_mode, "low")
69+
)
70+
assert LOW_HEADING in rendered
71+
assert HIGH_HEADING not in rendered
72+
73+
74+
@pytest.mark.parametrize("chat_mode", CHAT_MODES)
75+
def test_system_prompt_high_support_level_injects_comprehensive_block(chat_mode):
76+
rendered = _render_template(
77+
"chat_system_prompt.j2", _system_prompt_context(chat_mode, "high")
78+
)
79+
assert HIGH_HEADING in rendered
80+
assert LOW_HEADING not in rendered
81+
82+
83+
@pytest.mark.parametrize("chat_mode", CHAT_MODES)
84+
def test_system_prompt_moderate_support_level_injects_nothing(chat_mode):
85+
rendered = _render_template(
86+
"chat_system_prompt.j2", _system_prompt_context(chat_mode, "moderate")
87+
)
88+
assert LOW_HEADING not in rendered
89+
assert HIGH_HEADING not in rendered
90+
91+
92+
def _guide_context(support_level: str) -> dict:
93+
return {
94+
"problem_statement": "Implement a function that returns the sum of two ints.",
95+
"support_level": support_level,
96+
}
97+
98+
99+
def test_guide_prompt_low_support_level_is_socratic():
100+
rendered = _render_template("exercise_chat_guide_prompt.j2", _guide_context("low"))
101+
assert "Socratic" in rendered
102+
assert "comprehensive help mode" not in rendered
103+
104+
105+
def test_guide_prompt_high_support_level_announces_comprehensive_mode():
106+
rendered = _render_template("exercise_chat_guide_prompt.j2", _guide_context("high"))
107+
assert "comprehensive help mode" in rendered
108+
109+
110+
def test_guide_prompt_moderate_support_level_injects_nothing():
111+
rendered = _render_template(
112+
"exercise_chat_guide_prompt.j2", _guide_context("moderate")
113+
)
114+
assert "Socratic" not in rendered
115+
assert "comprehensive help mode" not in rendered
116+
assert "minimal help mode" not in rendered

0 commit comments

Comments
 (0)