Skip to content

Commit 2e4a04f

Browse files
committed
feat: add thread_id and checkpoint_pk in aside events
1 parent 4809a01 commit 2e4a04f

File tree

4 files changed

+118
-6
lines changed

4 files changed

+118
-6
lines changed

src/ol_openedx_chat/ol_openedx_chat/block.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
VIDEO_BLOCK_CATEGORY,
4141
)
4242
from ol_openedx_chat.utils import (
43+
get_checkpoint_and_thread_id,
4344
is_aside_applicable_to_block,
4445
is_ol_chat_enabled_for_course,
4546
)
@@ -254,7 +255,11 @@ def track_user_events(self, request, suffix=""): # noqa: ARG002
254255
Track user events by emitting them to the event tracker.
255256
"""
256257
request_data = request.json
257-
tracker.emit(
258-
request_data.get("event_type", ""), request_data.get("event_data", {})
258+
event_data = request_data.get("event_data", {})
259+
thread_id, checkpoint_pk = get_checkpoint_and_thread_id(
260+
content=event_data.get("value", "")
259261
)
262+
event_data.update({"thread_id": thread_id, "checkpoint_pk": checkpoint_pk})
263+
264+
tracker.emit(request_data.get("event_type", ""), event_data)
260265
return Response()

src/ol_openedx_chat/ol_openedx_chat/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# ruff: noqa: E501
22
"""Utility methods for the AI chat"""
33

4+
import json
5+
import logging
6+
import re
7+
48
from lms.djangoapps.courseware.courses import get_course_by_id
59
from opaque_keys.edx.locator import CourseLocator
610

711
from ol_openedx_chat.constants import BLOCK_TYPE_TO_SETTINGS, CHAT_APPLICABLE_BLOCKS
812

13+
log = logging.getLogger(__name__)
14+
915

1016
def is_aside_applicable_to_block(block):
1117
"""Check if the xBlock should support AI Chat"""
@@ -40,3 +46,37 @@ def is_ol_chat_enabled_for_course(block):
4046
other_course_settings = course.other_course_settings
4147
block_type = getattr(block, "category", None)
4248
return other_course_settings.get(BLOCK_TYPE_TO_SETTINGS.get(block_type))
49+
50+
51+
def get_checkpoint_and_thread_id(content):
52+
"""
53+
Extract the checkpoint ID and thread ID from chat response content.
54+
55+
Args:
56+
content (str): The content from the chat response.
57+
Returns:
58+
tuple: A tuple containing the thread ID and checkpoint PK, or (None, None)
59+
if extraction fails.
60+
"""
61+
if content is None:
62+
return None, None
63+
match = None
64+
try:
65+
# Decode bytes to string
66+
content_str = content.decode("utf-8") if isinstance(content, bytes) else content
67+
# The content from the chat response contains values with JSON data.
68+
# e.g. It looks like '_content': b'Hello! How can I help you?\n\n
69+
# <!-- {"checkpoint_pk": 123, "thread_id": "abc123"} -->\n\n'
70+
match = re.search(r"<!-- ({.*}) -->", content_str)
71+
if match:
72+
dict_values = json.loads(match.group(1))
73+
return dict_values.get("thread_id", None), str(
74+
dict_values.get("checkpoint_pk", None)
75+
)
76+
except Exception:
77+
log.exception(
78+
"Couldn't parse content/suffix to get Thread_id and Checkpoint_pk."
79+
" content: %s",
80+
content,
81+
)
82+
return None, None

src/ol_openedx_chat/tests/test_aside.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,13 +321,27 @@ def test_update_chat_config(self, ol_chat_enabled, block_category):
321321
ol_chat_aside = block.runtime.get_aside_of_type(block, self.aside_name)
322322
assert ol_chat_aside.ol_chat_enabled == ol_chat_enabled
323323

324-
@XBlockAside.register_temp_plugin(OLChatAside, "ol_chat_aside")
325-
@patch("ol_openedx_chat.block.tracker")
326324
@skip_unless_cms
327-
def test_track_user_events(self, mock_tracker):
325+
@ddt
326+
@data(
327+
*[
328+
["thread123", "cpk123"],
329+
["thread456", "cpk456"],
330+
["thread_abc", "cpk_xyz"],
331+
]
332+
)
333+
@unpack
334+
@patch("ol_openedx_chat.block.get_checkpoint_and_thread_id")
335+
@patch("ol_openedx_chat.block.tracker")
336+
@XBlockAside.register_temp_plugin(OLChatAside, "ol_chat_aside")
337+
def test_track_user_events(
338+
self, mock_tracker, mock_get_checkpoint_and_thread_id, thread_id, checkpoint_pk
339+
):
328340
"""
329-
Tests the track_user_events handler
341+
Tests the track_user_events handler with different thread_id and
342+
checkpoint_pk values
330343
"""
344+
mock_get_checkpoint_and_thread_id.return_value = (thread_id, checkpoint_pk)
331345
block = self.problem_block
332346
aside_usage_key = str(AsideUsageKeyV2(block.location, self.aside_name))
333347
handler_url = f"/xblock/{aside_usage_key}/handler/track_user_events"
@@ -341,6 +355,10 @@ def test_track_user_events(self, mock_tracker):
341355
"event_type": "chat_opened",
342356
"event_data": {
343357
"blockUsageKey": str(block.usage_key),
358+
"value": (
359+
f"<!-- {{'thread_id': '{thread_id}', "
360+
f"'checkpoint_pk': '{checkpoint_pk}'}} -->"
361+
),
344362
},
345363
}
346364
),
@@ -349,3 +367,20 @@ def test_track_user_events(self, mock_tracker):
349367

350368
assert response.status_code == 200 # noqa: PLR2004
351369
assert mock_tracker.emit.call_count == 1
370+
content_value = (
371+
f"<!-- {{'thread_id': '{thread_id}', "
372+
f"'checkpoint_pk': '{checkpoint_pk}'}} -->"
373+
)
374+
mock_get_checkpoint_and_thread_id.assert_called_once_with(content=content_value)
375+
mock_tracker.emit.assert_called_once_with(
376+
"chat_opened",
377+
{
378+
"blockUsageKey": str(block.usage_key),
379+
"value": (
380+
f"<!-- {{'thread_id': '{thread_id}', "
381+
f"'checkpoint_pk': '{checkpoint_pk}'}} -->"
382+
),
383+
"thread_id": thread_id,
384+
"checkpoint_pk": checkpoint_pk,
385+
},
386+
)

src/ol_openedx_chat/tests/test_utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from ddt import data, ddt, unpack
66
from ol_openedx_chat.utils import (
7+
get_checkpoint_and_thread_id,
78
is_aside_applicable_to_block,
89
is_ol_chat_enabled_for_course,
910
)
@@ -103,3 +104,34 @@ def test_is_aside_applicable_to_block(self, block_category, is_aside_applicable)
103104
elif block_category == "html":
104105
block = self.html_block
105106
assert is_aside_applicable_to_block(block) == is_aside_applicable
107+
108+
@data(
109+
(None, None, None),
110+
("Hello! How can I help you?\n\nNo JSON here\n\n", None, None),
111+
(
112+
(
113+
"Hello! How can I help you?\n\n"
114+
'<!-- {"checkpoint_pk": 123, "thread_id": "abc123"} -->\n\n'
115+
),
116+
"abc123",
117+
"123",
118+
),
119+
(
120+
(
121+
b"Hello! How can I help you?\n\n"
122+
b'<!-- {"checkpoint_pk": 456, "thread_id": "xyz789"} -->\n\n'
123+
),
124+
"xyz789",
125+
"456",
126+
),
127+
("Some text <!-- {invalid json} -->", None, None),
128+
('<!-- {"foo": "bar"} -->', None, "None"),
129+
)
130+
@unpack
131+
def test_get_checkpoint_and_thread_id(
132+
self, content, expected_thread_id, expected_checkpoint_pk
133+
):
134+
"""Tests that `get_checkpoint_and_thread_id` extracts the correct values"""
135+
thread_id, checkpoint_pk = get_checkpoint_and_thread_id(content)
136+
assert thread_id == expected_thread_id
137+
assert checkpoint_pk == expected_checkpoint_pk

0 commit comments

Comments
 (0)