Skip to content

Commit a023447

Browse files
committed
feat: preload previous response
1 parent 3cc0963 commit a023447

File tree

5 files changed

+148
-4
lines changed

5 files changed

+148
-4
lines changed

backend/openedx_ai_extensions/workflows/configs/default_cms.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"config": {
1818
"titleText": "AI Assistant",
1919
"buttonText": "Start",
20-
"customMessage": "Use an AI workflow to create multiple answer questions from this unit in a content library"
20+
"customMessage": "Use an AI workflow to create multiple answer questions from this unit in a content library",
21+
"preloadPreviousSession": true
2122
}
2223
},
2324
"response": {

backend/openedx_ai_extensions/workflows/orchestrators.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ def run(self, input_data):
113113
class EducatorAssistantOrchestrator(SessionBasedOrchestrator):
114114
"""Orchestrator for educator assistant workflows."""
115115

116+
def get_current_session_response(self, _):
117+
"""Retrieve the current session's LLM response."""
118+
submission = self.get_submission()
119+
if submission and submission.get("answer"):
120+
return {"response": submission["answer"]["collection_url"]}
121+
return {"response": None}
122+
116123
def run(self, input_data):
117124
# Prepare context
118125
context = {

backend/tests/test_submission_processor.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,53 @@ def test_process_retrieves_existing_submissions(
181181

182182
# Should return response with messages
183183
assert "response" in result or "error" in result
184+
185+
186+
@pytest.mark.django_db
187+
@patch("openedx_ai_extensions.processors.openedx.submission_processor.submissions_api")
188+
def test_process_respects_max_context_messages_limit(
189+
mock_submissions_api, user_session # pylint: disable=redefined-outer-name
190+
):
191+
"""
192+
Test that process() respects the max_context_messages configuration limit.
193+
"""
194+
# Create processor with max_context_messages=2
195+
config = {
196+
"SubmissionProcessor": {
197+
"max_context_messages": 2,
198+
}
199+
}
200+
processor = SubmissionProcessor(config=config, user_session=user_session)
201+
202+
# Mock multiple submissions exceeding the limit
203+
mock_submissions = [
204+
{
205+
"uuid": "submission-1",
206+
"answer": {"messages": [{"role": "user", "content": "Message 1"}]},
207+
"created_at": "2025-01-01T00:00:00Z",
208+
},
209+
{
210+
"uuid": "submission-2",
211+
"answer": {"messages": [{"role": "assistant", "content": "Response 1"}]},
212+
"created_at": "2025-01-01T00:01:00Z",
213+
},
214+
{
215+
"uuid": "submission-3",
216+
"answer": {"messages": [{"role": "user", "content": "Message 2"}]},
217+
"created_at": "2025-01-01T00:02:00Z",
218+
},
219+
{
220+
"uuid": "submission-4",
221+
"answer": {"messages": [{"role": "assistant", "content": "Response 2"}]},
222+
"created_at": "2025-01-01T00:03:00Z",
223+
},
224+
]
225+
mock_submissions_api.get_submissions.return_value = mock_submissions
226+
227+
result = processor.process(context={}, input_data=None)
228+
229+
# Verify get_submissions was called
230+
mock_submissions_api.get_submissions.assert_called_once()
231+
232+
# Should return response
233+
assert "response" in result or "error" in result

frontend/src/components/AIEducatorLibraryAssistComponent.jsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import PropTypes from 'prop-types';
33
import {
44
Button,
@@ -26,6 +26,7 @@ const AIEducatorLibraryAssistComponent = ({
2626
libraries: librariesProp,
2727
titleText,
2828
buttonText,
29+
preloadPreviousSession,
2930
customMessage,
3031
onSuccess,
3132
onError,
@@ -45,6 +46,9 @@ const AIEducatorLibraryAssistComponent = ({
4546
const [numberOfQuestions, setNumberOfQuestions] = useState(5);
4647
const [additionalInstructions, setAdditionalInstructions] = useState('');
4748

49+
// Track if we've already attempted to load previous session
50+
const hasLoadedSession = useRef(false);
51+
4852
/**
4953
* Fetch libraries from API
5054
* Only called when user opens the form
@@ -95,6 +99,53 @@ const AIEducatorLibraryAssistComponent = ({
9599
}
96100
}, [librariesProp]);
97101

102+
// Preload previous session if enabled
103+
useEffect(() => {
104+
const loadPreviousSession = async () => {
105+
if (!preloadPreviousSession || hasAsked || hasLoadedSession.current) {
106+
return;
107+
}
108+
109+
hasLoadedSession.current = true;
110+
setIsLoading(true);
111+
try {
112+
const contextData = prepareContextData({
113+
courseId,
114+
unitId,
115+
});
116+
117+
const data = await callWorkflowService({
118+
context: contextData,
119+
action: 'get_current_session_response',
120+
payload: {
121+
requestId: `ai-request-${Date.now()}`,
122+
courseId,
123+
},
124+
});
125+
126+
// Handle response - only set if there's actual data
127+
if (data.response && data.response !== null) {
128+
setResponse(data.response);
129+
setHasAsked(true);
130+
} else if (debug) {
131+
// No previous session or empty response - do nothing, show normal component
132+
// eslint-disable-next-line no-console
133+
console.log('No previous session found or empty response');
134+
}
135+
} catch (err) {
136+
// Silent fail - no previous session is not an error
137+
if (debug) {
138+
// eslint-disable-next-line no-console
139+
console.log('Error loading previous session:', err);
140+
}
141+
} finally {
142+
setIsLoading(false);
143+
}
144+
};
145+
146+
loadPreviousSession();
147+
}, [preloadPreviousSession, hasAsked, courseId, unitId, setResponse, setHasAsked, debug]);
148+
98149
// Early return after all hooks have been called
99150
if (hasAsked && !isLoading) {
100151
return null;
@@ -381,6 +432,7 @@ AIEducatorLibraryAssistComponent.propTypes = {
381432
),
382433
titleText: PropTypes.string,
383434
buttonText: PropTypes.string,
435+
preloadPreviousSession: PropTypes.bool,
384436
customMessage: PropTypes.string,
385437
onSuccess: PropTypes.func,
386438
onError: PropTypes.func,
@@ -392,6 +444,7 @@ AIEducatorLibraryAssistComponent.defaultProps = {
392444
titleText: 'AI Assistant',
393445
buttonText: 'Start',
394446
customMessage: 'Use an AI workflow to create multiple answer questions from this unit in a content library',
447+
preloadPreviousSession: false,
395448
onSuccess: null,
396449
onError: null,
397450
debug: false,

frontend/src/components/AIEducatorLibraryResponseComponent.jsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Button, Alert, Card } from '@openedx/paragon';
44
import {
55
Warning,
66
} from '@openedx/paragon/icons';
7+
import { prepareContextData, callWorkflowService } from '../services';
78

89
/**
910
* AI Response Component
@@ -18,6 +19,7 @@ const AIEducatorLibraryResponseComponent = ({
1819
customMessage,
1920
titleText,
2021
hyperlinkText,
22+
contextData,
2123
}) => {
2224
// Don't render if no response or error
2325
if (!response && !error) {
@@ -28,6 +30,35 @@ const AIEducatorLibraryResponseComponent = ({
2830
const baseUrl = window.location.origin;
2931
const hyperlinkUrl = `${baseUrl}/${response}`;
3032

33+
const handleClearSession = async () => {
34+
try {
35+
// Prepare context data
36+
const preparedContext = prepareContextData({
37+
...contextData,
38+
});
39+
40+
// Make API call
41+
await callWorkflowService({
42+
context: preparedContext,
43+
action: 'clear_session',
44+
payload: {
45+
requestId: `ai-request-${Date.now()}`,
46+
courseId: preparedContext.courseId || null,
47+
},
48+
});
49+
} catch (err) {
50+
// eslint-disable-next-line no-console
51+
console.error('[AISidebarResponse] Clear session error:', err);
52+
}
53+
};
54+
55+
const handleClearAndClose = async () => {
56+
await handleClearSession();
57+
if (onClear) {
58+
onClear();
59+
}
60+
};
61+
3162
return (
3263
<Card className="ai-educator-library-response mt-3 mb-3">
3364
<Card.Section>
@@ -74,12 +105,12 @@ const AIEducatorLibraryResponseComponent = ({
74105
</a>
75106
)}
76107
</div>
77-
{onClear && (
108+
{handleClearAndClose && (
78109
<div className="d-flex justify-content-end mt-3">
79110
<Button
80111
variant="outline-secondary"
81112
size="sm"
82-
onClick={onClear}
113+
onClick={handleClearAndClose}
83114
className="py-1 px-2"
84115
>
85116
Clear
@@ -103,6 +134,7 @@ AIEducatorLibraryResponseComponent.propTypes = {
103134
customMessage: PropTypes.string,
104135
titleText: PropTypes.string,
105136
hyperlinkText: PropTypes.string,
137+
contextData: PropTypes.shape({}),
106138
};
107139

108140
AIEducatorLibraryResponseComponent.defaultProps = {
@@ -114,6 +146,7 @@ AIEducatorLibraryResponseComponent.defaultProps = {
114146
customMessage: 'Question generation success.',
115147
titleText: 'AI Assistant',
116148
hyperlinkText: 'View content ›',
149+
contextData: {},
117150
};
118151

119152
export default AIEducatorLibraryResponseComponent;

0 commit comments

Comments
 (0)