0006 Event Bus Consumer Integration via openedx-events#
Status#
Provisional
Context#
openedx-ai-extensions exposes AI orchestration workflows that can be triggered
by different elements of the UI and also from pluggable extensions. Work is underway
to make it trigger from xblocks. Up to now, triggering an orchestration run required
a direct Python API call—coupling the calling code to the internal structure of this
package.
We want a decoupled, standards-compliant mechanism so that:
Any plugin or service can request an AI orchestration run without importing internal modules of
openedx-ai-extensions.The same handler can be invoked both in-process (direct Django signal, same LMS/CMS worker) and cross-service (via the Open edX Event Bus, e.g. Redis Streams).
The integration follows the openedx-events contract so it is consistent with the rest of the platform event infrastructure.
Decision#
openedx-ai-extensions owns and publishes an OpenEdxPublicSignal that any
consumer can fire to trigger an AI orchestration run.
Note
The signal is defined in this repository as a pragmatic starting point.
The long-term intent is to move AI_ORCHESTRATION_REQUESTED (and its
accompanying data class) to the openedx-events repository, so that any package
wishing to send the event does not need to take a dependency on
openedx-ai-extensions itself. Until that migration happens, callers must
import from openedx_ai_extensions.events.
Signal definition#
A new events sub-package is introduced inside openedx_ai_extensions:
openedx_ai_extensions/
├── events/
│ ├── __init__.py
│ ├── data.py ← attr data-class for the event payload
│ └── signals.py ← OpenEdxPublicSignal declaration
Data class (events/data.py):
@attr.s(frozen=True)
class AIOrchestrationRequestData:
user_id = attr.ib(type=int)
course_id = attr.ib(type=str, default=None)
location_id = attr.ib(type=str, default=None)
ui_slot_selector_id = attr.ib(type=str, default=None)
user_input = attr.ib(type=dict, factory=attr.Factory(dict))
action = attr.ib(type=str, default="run")
Signal (events/signals.py):
AI_ORCHESTRATION_REQUESTED = OpenEdxPublicSignal(
event_type="org.openedx.ai_extensions.orchestration.requested.v1",
data={"ai_orchestration_request": AIOrchestrationRequestData},
)
How a caller fires the event#
Any application that wants to trigger an AI orchestration run imports only the
public signal and data class—no internal openedx-ai-extensions modules:
from openedx_ai_extensions.events.signals import AI_ORCHESTRATION_REQUESTED
from openedx_ai_extensions.events.data import AIOrchestrationRequestData
AI_ORCHESTRATION_REQUESTED.send_event(
ai_orchestration_request=AIOrchestrationRequestData(
course_id="course-v1:edunext+01+2025",
ui_slot_selector_id="BADGES_GENERATOR_VIA_EVENT_BUS",
user_id=4,
)
)
Signal receiver#
A new receivers.py module inside openedx_ai_extensions subscribes to the
signal using Django’s @receiver decorator. The receiver follows the same
context-scoping process used elsewhere in the package: it builds a context dict
from the event payload, passes it to AIWorkflowScope.get_profile() to resolve
the matching workflow profile, and then calls execute() on the result:
@receiver(AI_ORCHESTRATION_REQUESTED)
def handle_ai_orchestration_requested(sender, ai_orchestration_request, **kwargs):
user = User.objects.get(id=ai_orchestration_request.user_id)
context = {
"course_id": ai_orchestration_request.course_id,
"location_id": ai_orchestration_request.location_id,
"ui_slot_selector_id": ai_orchestration_request.ui_slot_selector_id,
}
workflow = AIWorkflowScope.get_profile(**context)
workflow.execute(
user_input=ai_orchestration_request.user_input,
action=ai_orchestration_request.action,
user=user,
running_context=context,
)
The receiver is registered by importing openedx_ai_extensions.receivers inside
OpenedxAIExtensionsConfig.ready():
def ready(self):
import openedx_ai_extensions.receivers # noqa: F401
Event Bus consumer configuration#
The event bus consumer settings are not active by default. To enable them,
set the following flag in your environment (e.g. in lms.env.yml or a Tutor
plugin):
AI_EXTENSIONS_ENABLE_EVENT_BUS_CONSUMER = True
When that flag is present and True, plugin_settings injects the necessary
configuration so that the Open edX platform’s event bus worker picks up the signal
automatically:
settings.EVENT_BUS_CONSUMER = "edx_event_bus_redis.RedisEventConsumer"
settings.EVENT_BUS_CONSUMER_CONFIG = {
"org.openedx.ai_extensions.orchestration.requested.v1": {
"ai-orchestration-requests": {
"group_id": "ai-extensions-orchestrator",
"enabled": True,
}
}
}
Dependency#
openedx-events is added as an explicit dependency in requirements/base.in
(it was previously an implicit transitive dependency through event-tracking).
Files changed#
The following files are introduced or modified as part of this decision (shipped in a separate implementation PR):
File |
Change |
|---|---|
|
New – |
|
New – |
|
New – |
|
Modified – add |
Consequences#
Any plugin in the same Django process (e.g.
openedx-ai-badges) can trigger AI orchestration with a singlesend_event()call without coupling itself to internal APIs.The same receiver is transparently invoked when the event arrives over the Redis event bus, enabling cross-service triggering without any additional code.
openedx-ai-extensionsis the current owner of the signal definition and the sole consumer; other packages only need to import the publiceventssubpackage. Once the signal is migrated toopenedx-events, callers will no longer need a dependency onopenedx-ai-extensionsto fire the event—only this package (as the consumer/orchestrator) will retain that dependency.The chosen event type
org.openedx.ai_extensions.orchestration.requested.v1follows the Open edX event naming convention and is versioned from the start.Operators who do not need cross-service triggering can leave
EVENT_BUS_CONSUMER_CONFIGdisabled; in-process signals work without any message broker.