Skip to content

Commit ed19550

Browse files
committed
feat: keep history in short answer XBlock
1 parent b736f41 commit ed19550

File tree

6 files changed

+196
-60
lines changed

6 files changed

+196
-60
lines changed

ai_eval/coding_ai_eval.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from web_fragments.fragment import Fragment
99
from xblock.core import XBlock
1010
from xblock.exceptions import JsonHandlerError
11-
from xblock.fields import Dict, Scope, String
11+
from xblock.fields import Dict, List, Scope, String
1212
from xblock.validation import ValidationMessage
1313

1414
from .base import AIEvalXBlock
@@ -83,10 +83,13 @@ class CodingAIEvalXBlock(AIEvalXBlock):
8383
scope=Scope.settings,
8484
)
8585

86-
messages = Dict(
86+
# XXX: deprecated
87+
messages = Dict(scope=Scope.user_state)
88+
89+
sessions = List(
8790
help=_("Dictionary with messages"),
8891
scope=Scope.user_state,
89-
default={USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}},
92+
default=[{USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}}],
9093
)
9194

9295
editable_fields = AIEvalXBlock.editable_fields + (
@@ -96,6 +99,13 @@ class CodingAIEvalXBlock(AIEvalXBlock):
9699
"language",
97100
)
98101

102+
def __init__(self, *args, **kwargs):
103+
super().__init__(*args, **kwargs)
104+
if self.messages:
105+
self.sessions = [self.messages]
106+
self.messages = {}
107+
self.save()
108+
99109
def resource_string(self, path):
100110
"""Handy helper for getting resources from our kit."""
101111
data = pkg_resources.resource_string(__name__, path)
@@ -129,9 +139,9 @@ def student_view(self, context=None):
129139
js_data = {
130140
"monaco_html": monaco_html,
131141
"question": self.question,
132-
"code": self.messages[USER_RESPONSE],
133-
"ai_evaluation": self.messages[AI_EVALUATION],
134-
"code_exec_result": self.messages[CODE_EXEC_RESULT],
142+
"code": self.sessions[-1][USER_RESPONSE],
143+
"ai_evaluation": self.sessions[-1][AI_EVALUATION],
144+
"code_exec_result": self.sessions[-1][CODE_EXEC_RESULT],
135145
"marked_html": marked_html,
136146
"language": self.language,
137147
}
@@ -234,9 +244,9 @@ def get_response(self, data, suffix=""): # pylint: disable=unused-argument
234244
raise JsonHandlerError(500, "A probem occured. Please retry.") from e
235245

236246
if response:
237-
self.messages[USER_RESPONSE] = data["code"]
238-
self.messages[AI_EVALUATION] = response
239-
self.messages[CODE_EXEC_RESULT] = {
247+
self.sessions[-1][USER_RESPONSE] = data["code"]
248+
self.sessions[-1][AI_EVALUATION] = response
249+
self.sessions[-1][CODE_EXEC_RESULT] = {
240250
"stdout": data["stdout"],
241251
"stderr": data["stderr"],
242252
}
@@ -258,7 +268,11 @@ def reset_handler(self, data, suffix=""): # pylint: disable=unused-argument
258268
"""
259269
Reset the Xblock.
260270
"""
261-
self.messages = {USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}}
271+
self.sessions.append({
272+
USER_RESPONSE: "",
273+
AI_EVALUATION: "",
274+
CODE_EXEC_RESULT: {},
275+
})
262276
return {"message": "reset successful."}
263277

264278
@XBlock.json_handler

ai_eval/export.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@
3838
class DataExportXBlock(XBlock):
3939
icon_class = "problem"
4040
display_name = String(
41-
display_name=_("(Display name)"),
42-
help=_("Title to display"),
43-
default=_("Data export"),
41+
default=_("AI XBlocks data export"),
4442
scope=Scope.settings
4543
)
4644
active_export_task_id = String(
@@ -129,7 +127,6 @@ def raise_error(self, code, message):
129127
self.last_export_result = {
130128
'error': message,
131129
}
132-
self.display_data = None
133130
raise JsonHandlerError(code, message)
134131

135132
@XBlock.json_handler
@@ -142,9 +139,18 @@ def delete_export(self, data, suffix=''):
142139
return self._get_status()
143140

144141
def _delete_export(self):
142+
if not self.last_export_result or 'error' in self.last_export_result:
143+
return
144+
filename = self.last_export_result['report_filename']
145+
from lms.djangoapps.instructor_task.models import ReportStore
146+
report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD')
147+
course_key = getattr(self.scope_ids.usage_id, 'course_key', None)
148+
path = report_store.path_to(course_key, filename)
149+
try:
150+
report_store.storage.delete(path)
151+
except NotImplementedError:
152+
pass
145153
self.last_export_result = None
146-
self.display_data = None
147-
self.active_export_task_id = ''
148154

149155
@XBlock.json_handler
150156
def start_export(self, data, suffix=''):

ai_eval/multiagent.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,17 @@ class MultiAgentAIEvalXBlock(AIEvalXBlock):
284284
default=False,
285285
)
286286

287+
# XXX: Deprecated.
287288
chat_history = List(
288289
scope=Scope.user_state,
289290
default=[],
290291
)
291292

293+
sessions = List(
294+
scope=Scope.user_state,
295+
default=[[]],
296+
)
297+
292298
editable_fields = AIEvalXBlock.editable_fields + (
293299
"scenario_data",
294300
"character_data",
@@ -302,6 +308,13 @@ class MultiAgentAIEvalXBlock(AIEvalXBlock):
302308
"blacklist",
303309
)
304310

311+
def __init__(self, *args, **kwargs):
312+
super().__init__(*args, **kwargs)
313+
if self.chat_history:
314+
self.sessions = [self.chat_history]
315+
self.chat_history = []
316+
self.save()
317+
305318
def studio_view(self, context):
306319
"""
307320
Render a form for editing this XBlock
@@ -352,7 +365,7 @@ def _llm_input(self, prompt, user_input):
352365
# history with an LLM completion, with each message having a "role"
353366
# of "user" or "assistant".
354367
for message in itertools.chain(initial_messages,
355-
self.chat_history,
368+
self.sessions[-1],
356369
[user_message]):
357370
if message["role"] == "assistant":
358371
agent = message["extra"].get("role") or ""
@@ -503,7 +516,7 @@ def student_view(self, context=None):
503516
main_data = self._get_character_data(main_name)
504517
break
505518
js_data = {
506-
"messages": self.chat_history,
519+
"messages": self.sessions[-1],
507520
"main_character_agent": main_agent,
508521
"main_character_data": {
509522
"name": main_data.get("name", main_name),
@@ -625,10 +638,10 @@ def get_response(self, data, suffix=""): # pylint: disable=unused-argument
625638
character_data = character_data.copy()
626639
character_data.setdefault("name", character_name)
627640

628-
self.chat_history.append({"role": "user", "content": user_input})
641+
self.sessions[-1].append({"role": "user", "content": user_input})
629642
extra = {"is_evaluator": is_evaluator, "role": agent,
630643
"character_data": character_data}
631-
self.chat_history.append({"role": "assistant", "content": message,
644+
self.sessions[-1].append({"role": "assistant", "content": message,
632645
"extra": extra})
633646
return {
634647
"message": message,
@@ -646,6 +659,6 @@ def reset(self, data, suffix=""):
646659
"""Reset the chat history."""
647660
if not self.allow_reset:
648661
raise JsonHandlerError(403, "Reset is disabled.")
649-
self.chat_history = []
662+
self.sessions.append([])
650663
self.finished = False
651664
return {}

ai_eval/shortanswer.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ class ShortAnswerAIEvalXBlock(AIEvalXBlock):
2727
Short Answer Xblock.
2828
"""
2929

30-
USER_KEY = "USER"
31-
LLM_KEY = "LLM"
3230
ATTACHMENT_PARALLEL_DOWNLOADS = 5
3331

3432
display_name = String(
@@ -84,19 +82,23 @@ class ShortAnswerAIEvalXBlock(AIEvalXBlock):
8482
default=False,
8583
)
8684

87-
messages = Dict(
88-
help=_("Dictionary with chat messages"),
89-
scope=Scope.user_state,
90-
default={USER_KEY: [], LLM_KEY: []},
91-
)
92-
9385
attachment_urls = List(
9486
display_name=_("Attachment URLs"),
9587
help=_("Attachments to include with the evaluation prompt"),
9688
scope=Scope.settings,
9789
resettable_editor=False,
9890
)
9991

92+
# XXX: Deprecated.
93+
messages = Dict(
94+
scope=Scope.user_state,
95+
)
96+
97+
sessions = List(
98+
scope=Scope.user_state,
99+
default=[[]],
100+
)
101+
100102
editable_fields = AIEvalXBlock.editable_fields + (
101103
"question",
102104
"evaluation_prompt",
@@ -106,6 +108,22 @@ class ShortAnswerAIEvalXBlock(AIEvalXBlock):
106108
"attachment_urls",
107109
)
108110

111+
def __init__(self, *args, **kwargs):
112+
super().__init__(*args, **kwargs)
113+
if self.messages:
114+
for user_msg, assistant_msg in zip(self.messages["USER"],
115+
self.messages["LLM"]):
116+
self.sessions[-1].append({
117+
"source": "user",
118+
"content": user_msg or ".",
119+
})
120+
self.sessions[-1].append({
121+
"source": "llm",
122+
"content": assistant_msg,
123+
})
124+
self.messages = {}
125+
self.save()
126+
109127
def validate_field_data(self, validation, data):
110128
"""
111129
Validate fields
@@ -155,7 +173,7 @@ def student_view(self, context=None):
155173

156174
js_data = {
157175
"question": self.question,
158-
"messages": self.messages,
176+
"messages": self.sessions[-1],
159177
"max_responses": self.max_responses,
160178
"marked_html": marked_html,
161179
}
@@ -224,25 +242,36 @@ def get_response(self, data, suffix=""): # pylint: disable=unused-argument
224242
# add previous messages
225243
# the first AI role is 'system' which defines the LLM's personnality and behavior.
226244
# subsequent roles are 'assistant' and 'user'
227-
for user_msg, assistant_msg in zip(self.messages[self.USER_KEY],
228-
self.messages[self.LLM_KEY]):
229-
messages.append({"content": user_msg or ".", "role": "user"})
230-
messages.append({"content": assistant_msg, "role": "assistant"})
245+
for message in self.sessions[-1]:
246+
if message["source"] == "user":
247+
role = "user"
248+
else:
249+
role = "assistant"
250+
messages.append({
251+
"role": role,
252+
"content": message["content"] or ".",
253+
})
231254
messages.append({"role": "user", "content": user_submission})
232255

233256
try:
234-
text = self.get_llm_response(messages, tag=current_tag)
257+
response = self.get_llm_response(messages, tag=current_tag)
235258
except Exception as e:
236259
logger.error(
237260
f"Failed while making LLM request using model {self.model}. Error: {e}",
238261
exc_info=True,
239262
)
240263
raise JsonHandlerError(500, "A probem occured. Please retry.") from e
241264

242-
if text:
243-
self.messages[self.USER_KEY].append(user_submission)
244-
self.messages[self.LLM_KEY].append(text)
245-
return {"response": text}
265+
if response:
266+
self.sessions[-1].append({
267+
"source": "user",
268+
"content": user_submission,
269+
})
270+
self.sessions[-1].append({
271+
"source": "llm",
272+
"content": response,
273+
})
274+
return {"response": response}
246275

247276
raise JsonHandlerError(500, "A probem occured. The LLM sent an empty response.")
248277

@@ -253,8 +282,8 @@ def reset(self, data, suffix=""):
253282
"""
254283
if not self.allow_reset:
255284
raise JsonHandlerError(403, "Reset is disabled.")
256-
self.messages = {self.USER_KEY: [], self.LLM_KEY: []}
257285
self.thread_map = {}
286+
self.sessions.append([])
258287
return {}
259288

260289
@staticmethod

ai_eval/static/js/src/shortanswer.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ function ShortAnswerAIEvalXBlock(runtime, element, data) {
88

99
const handleInit = function() {
1010
$("#question-text", element).html(MarkdownToHTML(data.question));
11-
for (var i = 0; i < data.messages.USER.length; i++) {
12-
this.insertUserMessage(data.messages.USER[i]);
13-
this.insertAIMessage(formatAIMessage(data.messages.LLM[i]));
11+
var userMessageCount = 0;
12+
for (var i = 0; i < data.messages.length; i++) {
13+
var message = data.messages[i];
14+
if (message.source == "user") {
15+
userMessageCount++;
16+
this.insertUserMessage(message.content);
17+
} else if (message.source == "llm") {
18+
this.insertAIMessage(formatAIMessage(message.content));
19+
}
1420
}
15-
this.enableInput(data.messages.USER.length < data.max_responses);
16-
this.enableReset(data.messages.USER.length > 0);
21+
this.enableInput(userMessageCount < data.max_responses);
22+
this.enableReset(userMessageCount > 0);
1723
};
1824

1925
const handleResponse = function(response) {

0 commit comments

Comments
 (0)