Skip to content

Commit c4c46e9

Browse files
committed
feat: keep history in short answer XBlock
1 parent 50937c0 commit c4c46e9

File tree

7 files changed

+177
-70
lines changed

7 files changed

+177
-70
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
@@ -84,10 +84,13 @@ class CodingAIEvalXBlock(AIEvalXBlock):
8484
scope=Scope.settings,
8585
)
8686

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

9396
editable_fields = AIEvalXBlock.editable_fields + (
@@ -97,6 +100,13 @@ class CodingAIEvalXBlock(AIEvalXBlock):
97100
"language",
98101
)
99102

103+
def __init__(self, *args, **kwargs):
104+
super().__init__(*args, **kwargs)
105+
if self.messages:
106+
self.sessions = [self.messages]
107+
self.messages = {}
108+
self.save()
109+
100110
def resource_string(self, path):
101111
"""Handy helper for getting resources from our kit."""
102112
data = pkg_resources.resource_string(__name__, path)
@@ -130,9 +140,9 @@ def student_view(self, context=None):
130140
js_data = {
131141
"monaco_html": monaco_html,
132142
"question": self.question,
133-
"code": self.messages[USER_RESPONSE],
134-
"ai_evaluation": self.messages[AI_EVALUATION],
135-
"code_exec_result": self.messages[CODE_EXEC_RESULT],
143+
"code": self.sessions[-1][USER_RESPONSE],
144+
"ai_evaluation": self.sessions[-1][AI_EVALUATION],
145+
"code_exec_result": self.sessions[-1][CODE_EXEC_RESULT],
136146
"marked_html": marked_html,
137147
"language": self.language,
138148
}
@@ -237,9 +247,9 @@ def get_response(self, data, suffix=""): # pylint: disable=unused-argument
237247
raise JsonHandlerError(500, "A probem occurred. Please retry.") from e
238248

239249
if response:
240-
self.messages[USER_RESPONSE] = data["code"]
241-
self.messages[AI_EVALUATION] = response
242-
self.messages[CODE_EXEC_RESULT] = {
250+
self.sessions[-1][USER_RESPONSE] = data["code"]
251+
self.sessions[-1][AI_EVALUATION] = response
252+
self.sessions[-1][CODE_EXEC_RESULT] = {
243253
"stdout": data["stdout"],
244254
"stderr": data["stderr"],
245255
}
@@ -261,7 +271,11 @@ def reset_handler(self, data, suffix=""): # pylint: disable=unused-argument
261271
"""
262272
Reset the Xblock.
263273
"""
264-
self.messages = {USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}}
274+
self.sessions.append({
275+
USER_RESPONSE: "",
276+
AI_EVALUATION: "",
277+
CODE_EXEC_RESULT: {},
278+
})
265279
return {"message": "reset successful."}
266280

267281
@XBlock.json_handler

ai_eval/export.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@
4141
class DataExportXBlock(XBlock):
4242
icon_class = "problem"
4343
display_name = String(
44-
display_name=_("(Display name)"),
45-
help=_("Title to display"),
46-
default=_("Data export"),
44+
default=_("AI XBlocks data export"),
4745
scope=Scope.settings
4846
)
4947
active_export_task_id = String(
@@ -132,7 +130,6 @@ def raise_error(self, code, message):
132130
self.last_export_result = {
133131
'error': message,
134132
}
135-
self.display_data = None
136133
raise JsonHandlerError(code, message)
137134

138135
@XBlock.json_handler
@@ -145,9 +142,18 @@ def delete_export(self, data, suffix=''):
145142
return self._get_status()
146143

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

152158
@XBlock.json_handler
153159
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,14 +242,19 @@ 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}",
@@ -241,10 +264,16 @@ def get_response(self, data, suffix=""): # pylint: disable=unused-argument
241264
raise JsonHandlerError(500, str(e)) from e
242265
raise JsonHandlerError(500, "A probem occurred. Please retry.") from e
243266

244-
if text:
245-
self.messages[self.USER_KEY].append(user_submission)
246-
self.messages[self.LLM_KEY].append(text)
247-
return {"response": text}
267+
if response:
268+
self.sessions[-1].append({
269+
"source": "user",
270+
"content": user_submission,
271+
})
272+
self.sessions[-1].append({
273+
"source": "llm",
274+
"content": response,
275+
})
276+
return {"response": response}
248277

249278
raise JsonHandlerError(500, "A probem occurred. The LLM sent an empty response.")
250279

@@ -255,8 +284,8 @@ def reset(self, data, suffix=""):
255284
"""
256285
if not self.allow_reset:
257286
raise JsonHandlerError(403, "Reset is disabled.")
258-
self.messages = {self.USER_KEY: [], self.LLM_KEY: []}
259287
self.thread_map = {}
288+
self.sessions.append([])
260289
return {}
261290

262291
@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)