Skip to content

Commit bc83b80

Browse files
committed
feat: keep history in short answer XBlock
1 parent e68e1dc commit bc83b80

File tree

6 files changed

+135
-54
lines changed

6 files changed

+135
-54
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
}
@@ -228,9 +238,9 @@ def get_response(self, data, suffix=""): # pylint: disable=unused-argument
228238
raise JsonHandlerError(500, "A probem occured. Please retry.") from e
229239

230240
if response:
231-
self.messages[USER_RESPONSE] = data["code"]
232-
self.messages[AI_EVALUATION] = response
233-
self.messages[CODE_EXEC_RESULT] = {
241+
self.sessions[-1][USER_RESPONSE] = data["code"]
242+
self.sessions[-1][AI_EVALUATION] = response
243+
self.sessions[-1][CODE_EXEC_RESULT] = {
234244
"stdout": data["stdout"],
235245
"stderr": data["stderr"],
236246
}
@@ -253,7 +263,11 @@ def reset_handler(self, data, suffix=""): # pylint: disable=unused-argument
253263
"""
254264
Reset the Xblock.
255265
"""
256-
self.messages = {USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}}
266+
self.sessions.append({
267+
USER_RESPONSE: "",
268+
AI_EVALUATION: "",
269+
CODE_EXEC_RESULT: {},
270+
})
257271
return {"message": "reset successful."}
258272

259273
@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: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ class ShortAnswerAIEvalXBlock(AIEvalXBlock):
2727

2828
ATTACHMENT_PARALLEL_DOWNLOADS = 5
2929

30-
USER_KEY = "USER"
31-
LLM_KEY = "LLM"
32-
3330
display_name = String(
3431
display_name=_("Display Name"),
3532
help=_("Name of the component in the studio"),
@@ -90,10 +87,14 @@ class ShortAnswerAIEvalXBlock(AIEvalXBlock):
9087
resettable_editor=False,
9188
)
9289

90+
# XXX: Deprecated.
9391
messages = Dict(
94-
help=_("Dictionary with chat messages"),
9592
scope=Scope.user_state,
96-
default={USER_KEY: [], LLM_KEY: []},
93+
)
94+
95+
sessions = List(
96+
scope=Scope.user_state,
97+
default=[[]],
9798
)
9899

99100
editable_fields = AIEvalXBlock.editable_fields + (
@@ -105,6 +106,22 @@ class ShortAnswerAIEvalXBlock(AIEvalXBlock):
105106
"attachment_urls",
106107
)
107108

109+
def __init__(self, *args, **kwargs):
110+
super().__init__(*args, **kwargs)
111+
if self.messages:
112+
for user_msg, assistant_msg in zip(self.messages["USER"],
113+
self.messages["LLM"]):
114+
self.sessions[-1].append({
115+
"source": "user",
116+
"content": user_msg or ".",
117+
})
118+
self.sessions[-1].append({
119+
"source": "llm",
120+
"content": assistant_msg,
121+
})
122+
self.messages = {}
123+
self.save()
124+
108125
def validate_field_data(self, validation, data):
109126
"""
110127
Validate fields
@@ -154,7 +171,7 @@ def student_view(self, context=None):
154171

155172
js_data = {
156173
"question": self.question,
157-
"messages": self.messages,
174+
"messages": self.sessions[-1],
158175
"max_responses": self.max_responses,
159176
"marked_html": marked_html,
160177
}
@@ -202,10 +219,15 @@ def get_response(self, data, suffix=""): # pylint: disable=unused-argument
202219
# add previous messages
203220
# the first AI role is 'system' which defines the LLM's personnality and behavior.
204221
# subsequent roles are 'assistant' and 'user'
205-
for user_msg, assistant_msg in zip(self.messages[self.USER_KEY],
206-
self.messages[self.LLM_KEY]):
207-
messages.append({"content": user_msg or ".", "role": "user"})
208-
messages.append({"content": assistant_msg, "role": "assistant"})
222+
for message in self.sessions[-1]:
223+
if message["source"] == "user":
224+
role = "user"
225+
else:
226+
role = "assistant"
227+
messages.append({
228+
"role": role,
229+
"content": message["content"] or ".",
230+
})
209231
messages.append({"role": "user", "content": user_submission})
210232

211233
try:
@@ -218,8 +240,14 @@ def get_response(self, data, suffix=""): # pylint: disable=unused-argument
218240
raise JsonHandlerError(500, "A probem occured. Please retry.") from e
219241

220242
if response:
221-
self.messages[self.USER_KEY].append(user_submission)
222-
self.messages[self.LLM_KEY].append(response)
243+
self.sessions[-1].append({
244+
"source": "user",
245+
"content": user_submission,
246+
})
247+
self.sessions[-1].append({
248+
"source": "llm",
249+
"content": response,
250+
})
223251
return {"response": response}
224252

225253
raise JsonHandlerError(500, "A probem occured. The LLM sent an empty response.")
@@ -231,7 +259,7 @@ def reset(self, data, suffix=""):
231259
"""
232260
if not self.allow_reset:
233261
raise JsonHandlerError(403, "Reset is disabled.")
234-
self.messages = {self.USER_KEY: [], self.LLM_KEY: []}
262+
self.sessions.append([])
235263
return {}
236264

237265
@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(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) {

ai_eval/tasks.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,20 @@
99
from django.contrib.auth.models import User
1010
from xblock.fields import Scope
1111

12-
from . import ShortAnswerAIEvalXBlock
12+
from . import (
13+
CodingAIEvalXBlock,
14+
MultiAgentAIEvalXBlock,
15+
ShortAnswerAIEvalXBlock,
16+
)
1317

1418
logger = get_task_logger(__name__)
1519

20+
_SUPPORTED_BLOCKS = [
21+
CodingAIEvalXBlock,
22+
MultiAgentAIEvalXBlock,
23+
ShortAnswerAIEvalXBlock,
24+
]
25+
1626

1727
@shared_task()
1828
def export_data(course_id_str):
@@ -59,7 +69,7 @@ def _extract_all_data(course_id):
5969

6070
store = modulestore()
6171
for block in store.get_items(course_id):
62-
if isinstance(block, ShortAnswerAIEvalXBlock):
72+
if isinstance(block, _SUPPORTED_BLOCKS):
6373
yield from _extract_data(block)
6474

6575

@@ -76,25 +86,29 @@ def _extract_data(block):
7686
data.add_blocks_to_cache([block])
7787

7888
try:
79-
messages = data.get(DjangoKeyValueStore.Key(
89+
sessions = data.get(DjangoKeyValueStore.Key(
8090
scope=Scope.user_state,
8191
user_id=user.id,
8292
block_scope_id=block.location,
83-
field_name='messages'
93+
field_name='sessions'
8494
))
8595
except KeyError:
8696
continue
8797

88-
conversation = 1
89-
for user_message, llm_message in zip(messages['USER'], messages['LLM']):
90-
yield (section_name, subsection_name, unit_name,
91-
str(block.location), block.display_name,
92-
user.username, user.email or "", conversation,
93-
"user", user_message)
94-
yield (section_name, subsection_name, unit_name,
95-
str(block.location), block.display_name,
96-
user.username, user.email or "", conversation,
97-
"llm", llm_message)
98+
for i, conversation in enumerate(sessions, start=1):
99+
for message in conversation:
100+
yield (
101+
section_name,
102+
subsection_name,
103+
unit_name,
104+
str(block.location),
105+
block.display_name,
106+
user.username,
107+
user.email or "",
108+
i,
109+
message["source"],
110+
message["content"]
111+
)
98112

99113

100114
def _get_context(block):
@@ -110,4 +124,4 @@ def _get_context(block):
110124
section_name = block_names_by_type.get('chapter', '')
111125
subsection_name = block_names_by_type.get('sequential', '')
112126
unit_name = block_names_by_type.get('vertical', '')
113-
return section_name, subsection_name, unit_name
127+
return section_name, subsection_name, unit_name

0 commit comments

Comments
 (0)