diff --git a/backend/openedx_ai_extensions/admin.py b/backend/openedx_ai_extensions/admin.py
index 7a99403e..a28a958a 100644
--- a/backend/openedx_ai_extensions/admin.py
+++ b/backend/openedx_ai_extensions/admin.py
@@ -516,7 +516,7 @@ def debug_thread_view(self, request):
session_data["remote_thread_error"] = str(e)
try:
- session_data["combined_thread"] = session.get_combined_thread()
+ session_data["combined_thread"] = session.get_debug_thread()
except Exception as e: # pylint: disable=broad-exception-caught
_logger.exception(
"Error building combined thread for session %s", session.id
diff --git a/backend/openedx_ai_extensions/templates/admin/debug_thread.html b/backend/openedx_ai_extensions/templates/admin/debug_thread.html
index fbdd2bc5..95a3d601 100644
--- a/backend/openedx_ai_extensions/templates/admin/debug_thread.html
+++ b/backend/openedx_ai_extensions/templates/admin/debug_thread.html
@@ -5,28 +5,28 @@
{% block content %}
Debug Thread
diff --git a/backend/openedx_ai_extensions/workflows/models.py b/backend/openedx_ai_extensions/workflows/models.py
index 06a867ed..ffe57586 100644
--- a/backend/openedx_ai_extensions/workflows/models.py
+++ b/backend/openedx_ai_extensions/workflows/models.py
@@ -571,3 +571,23 @@ def get_combined_thread(self): # pylint: disable=too-many-statements
combined.insert(insert_at, entry)
return combined
+
+ def get_debug_thread(self):
+ """
+ Fetch the debug thread for this session by delegating to its orchestrator.
+
+ This allows different orchestrator types (threaded vs one-shot) to
+ provide their own relevant debug information.
+ """
+ context = {
+ "course_id": str(self.course_id) if self.course_id else None,
+ "location_id": str(self.location_id) if self.location_id else None,
+ }
+
+ orchestrator = BaseOrchestrator.get_orchestrator(
+ workflow=self.scope,
+ user=self.user,
+ context=context,
+ )
+
+ return orchestrator.get_debug_messages()
diff --git a/backend/openedx_ai_extensions/workflows/orchestrators/base_orchestrator.py b/backend/openedx_ai_extensions/workflows/orchestrators/base_orchestrator.py
index b65eeb57..11180be2 100644
--- a/backend/openedx_ai_extensions/workflows/orchestrators/base_orchestrator.py
+++ b/backend/openedx_ai_extensions/workflows/orchestrators/base_orchestrator.py
@@ -94,6 +94,16 @@ def _emit_workflow_event(self, event_name: str) -> None:
def run(self, input_data):
raise NotImplementedError("Subclasses must implement run method")
+ def get_debug_messages(self) -> list:
+ """
+ Return a list of messages for debugging this orchestrator's execution.
+
+ Default implementation returns an empty list. Subclasses (especially
+ session-based ones) should override this to provide relevant
+ interaction history or state.
+ """
+ return []
+
@classmethod
def get_orchestrator(cls, *, workflow, user, context):
"""
diff --git a/backend/openedx_ai_extensions/workflows/orchestrators/direct_orchestrator.py b/backend/openedx_ai_extensions/workflows/orchestrators/direct_orchestrator.py
index 4b31d36f..667d4b4a 100644
--- a/backend/openedx_ai_extensions/workflows/orchestrators/direct_orchestrator.py
+++ b/backend/openedx_ai_extensions/workflows/orchestrators/direct_orchestrator.py
@@ -311,3 +311,28 @@ def save(self, input_data):
'status': 'completed',
'response': collection_url,
}
+
+ def get_debug_messages(self) -> list:
+ """
+ Return debug messages including question slots data.
+ """
+ messages = super().get_debug_messages()
+
+ metadata = self.session.metadata or {}
+ question_slots = metadata.get("question_slots")
+ if question_slots:
+ messages.append({
+ "role": "assistant",
+ "content": f"Stored Question Slots: {json.dumps(question_slots, indent=2)}",
+ "source": "metadata",
+ })
+
+ collection_url = metadata.get("collection_url")
+ if collection_url:
+ messages.append({
+ "role": "assistant",
+ "content": f"Saved Collection URL: {collection_url}",
+ "source": "metadata",
+ })
+
+ return messages
diff --git a/backend/openedx_ai_extensions/workflows/orchestrators/flashcards_orchestrator.py b/backend/openedx_ai_extensions/workflows/orchestrators/flashcards_orchestrator.py
index 4e717f1a..82574b28 100644
--- a/backend/openedx_ai_extensions/workflows/orchestrators/flashcards_orchestrator.py
+++ b/backend/openedx_ai_extensions/workflows/orchestrators/flashcards_orchestrator.py
@@ -162,3 +162,20 @@ def get_current_session_response(self, _):
'cards': None,
'status': 'no_flashcards',
}
+
+ def get_debug_messages(self) -> list:
+ """
+ Return debug messages including flashcard data.
+ """
+ messages = super().get_debug_messages()
+
+ metadata = self.session.metadata or {}
+ cards = metadata.get("cards")
+ if cards:
+ messages.append({
+ "role": "assistant",
+ "content": f"Stored Flashcards: {json.dumps(cards, indent=2)}",
+ "source": "metadata",
+ })
+
+ return messages
diff --git a/backend/openedx_ai_extensions/workflows/orchestrators/session_based_orchestrator.py b/backend/openedx_ai_extensions/workflows/orchestrators/session_based_orchestrator.py
index b6b43df0..459ab18b 100644
--- a/backend/openedx_ai_extensions/workflows/orchestrators/session_based_orchestrator.py
+++ b/backend/openedx_ai_extensions/workflows/orchestrators/session_based_orchestrator.py
@@ -135,6 +135,60 @@ def _get_submission_processor(self):
self.profile.processor_config, self.session
)
+ def get_debug_messages(self) -> list:
+ """
+ Return debug messages from the session's thread and metadata.
+ """
+ import json # pylint: disable=import-outside-toplevel
+
+ # 1. Start with the combined thread if available (for threaded workflows)
+ messages = self.session.get_combined_thread() or []
+
+ # 2. Enrich with task status and results from metadata (for non-threaded workflows)
+ metadata = self.session.metadata or {}
+
+ # Add task status if present and not already represented
+ task_status = metadata.get("task_status")
+ if task_status and not any(m.get("role") == "system" and task_status in m.get("content", "") for m in messages):
+ messages.append({
+ "role": "system",
+ "content": f"Task Status: {task_status}",
+ "source": "metadata",
+ })
+
+ # Add task status message if present
+ task_status_msg = metadata.get("task_status_message")
+ if task_status_msg:
+ messages.append({
+ "role": "system",
+ "content": f"Status Message: {task_status_msg}",
+ "source": "metadata",
+ })
+
+ # Add task result if present and not already represented
+ task_result = metadata.get("task_result")
+ if task_result:
+ # Check if task_result content is already in the thread to avoid duplication
+ # (though for non-threaded it likely won't be)
+ result_str = json.dumps(task_result, indent=2)
+ if not any(result_str[:200] in str(m.get("content", "")) for m in messages):
+ messages.append({
+ "role": "assistant",
+ "content": result_str,
+ "source": "metadata",
+ })
+
+ # Add task error if present
+ task_error = metadata.get("task_error")
+ if task_error:
+ messages.append({
+ "role": "error",
+ "content": task_error,
+ "source": "metadata",
+ })
+
+ return messages
+
def run(self, input_data):
raise NotImplementedError("Subclasses must implement run method")
diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py
index 3385c390..799ec20c 100644
--- a/backend/tests/test_models.py
+++ b/backend/tests/test_models.py
@@ -559,6 +559,93 @@ def test_combined_thread_function_call_fields_propagated( # pylint: disable=unu
# content is still present as a human-readable fallback
assert "get_weather" in item["content"]
+ @patch("openedx_ai_extensions.workflows.orchestrators.BaseOrchestrator.get_orchestrator")
+ def test_get_debug_thread_delegates_to_orchestrator(self, mock_get_orchestrator, session_with_ids):
+ """Verify that get_debug_thread calls the orchestrator's get_debug_messages."""
+ mock_orchestrator = Mock()
+ mock_orchestrator.get_debug_messages.return_value = [{"role": "system", "content": "test"}]
+ mock_get_orchestrator.return_value = mock_orchestrator
+
+ result = session_with_ids.get_debug_thread()
+
+ assert result == [{"role": "system", "content": "test"}]
+ mock_get_orchestrator.assert_called_once()
+ mock_orchestrator.get_debug_messages.assert_called_once()
+
+ def test_session_based_orchestrator_debug_messages_with_metadata(self, session_no_ids):
+ """Test that SessionBasedOrchestrator synthesizes messages from metadata."""
+ # pylint: disable=import-outside-toplevel
+ from openedx_ai_extensions.workflows.orchestrators.session_based_orchestrator import SessionBasedOrchestrator
+
+ session_no_ids.metadata = {
+ "task_status": "completed",
+ "task_result": {"answer": "42"},
+ "task_status_message": "All done"
+ }
+ session_no_ids.save()
+
+ orchestrator = SessionBasedOrchestrator(
+ workflow=session_no_ids.scope,
+ user=session_no_ids.user,
+ context={}
+ )
+ # Ensure it uses the session we just updated
+ orchestrator.session = session_no_ids
+
+ messages = orchestrator.get_debug_messages()
+
+ # Should have messages for status, status message, and result
+ assert any("Task Status: completed" in m["content"] for m in messages)
+ assert any("Status Message: All done" in m["content"] for m in messages)
+ assert any("42" in m["content"] for m in messages)
+ assert all(m["source"] == "metadata" for m in messages)
+
+ def test_flashcards_orchestrator_debug_messages(self, session_no_ids):
+ """Test that FlashCardsOrchestrator includes cards in debug messages."""
+ # pylint: disable=import-outside-toplevel
+ from openedx_ai_extensions.workflows.orchestrators.flashcards_orchestrator import FlashCardsOrchestrator
+
+ session_no_ids.metadata = {
+ "cards": [{"question": "Q1", "answer": "A1"}]
+ }
+ session_no_ids.save()
+
+ orchestrator = FlashCardsOrchestrator(
+ workflow=session_no_ids.scope,
+ user=session_no_ids.user,
+ context={}
+ )
+ orchestrator.session = session_no_ids
+
+ messages = orchestrator.get_debug_messages()
+
+ assert any("Stored Flashcards" in m["content"] for m in messages)
+ assert any("Q1" in m["content"] for m in messages)
+
+ def test_educator_assistant_orchestrator_debug_messages(self, session_no_ids):
+ """Test that EducatorAssistantOrchestrator includes question slots."""
+ # pylint: disable=import-outside-toplevel
+ from openedx_ai_extensions.workflows.orchestrators.direct_orchestrator import EducatorAssistantOrchestrator
+
+ session_no_ids.metadata = {
+ "question_slots": [{"versions": [{"q": "v1"}]}],
+ "collection_url": "http://example.com"
+ }
+ session_no_ids.save()
+
+ orchestrator = EducatorAssistantOrchestrator(
+ workflow=session_no_ids.scope,
+ user=session_no_ids.user,
+ context={}
+ )
+ orchestrator.session = session_no_ids
+
+ messages = orchestrator.get_debug_messages()
+
+ assert any("Stored Question Slots" in m["content"] for m in messages)
+ assert any("Saved Collection URL" in m["content"] for m in messages)
+ assert any("http://example.com" in m["content"] for m in messages)
+
# ==========================================================================
# AIWorkflowScope Resolution (multi-scope per location)