We’ve been working on openedx-ai-extensions, a standalone plugin that exposes LLM capabilities to the platform. As part of that work, we investigated whether a plugin can register a new XBlock runtime service — so that any XBlock could call self.runtime.service(self, “ai_extensions”) without importing plugin internals directly.
The short answer we found: there is currently no supported way to do this.
Service registration is entirely hardcoded inside openedx-platform’s runtime classes — either as an explicit Python dictionary (legacy runtime) or a hardcoded if/elif chain (modern
XBlockRuntime). Neither the XBlock library nor openedx-platform exposes an entry point, Django setting, filter, or signal that a plugin can use to inject a new service.
We explored three concrete approaches and documented them in ADR-0005 of our repo:
Monkey-patching Runtime.service; plugin-only, no platform changes, but an anti-pattern we’re not willing to ship.
A new openedx.xblock_service entry-point group; consistent with existing openedx.* entry points, but requires upstream platform changes we can’t own from this project.
An XBLOCK_EXTRA_SERVICES Django setting; analogous to XBLOCK_EXTRA_MIXINS, but same constraint.
Open-edx filters; consistent with openedx practices. Requires a new filter and modifications to the runtimes
We’re not pursuing any of these at this time. Beyond scope and timeline constraints on our end, we’re also aware that ADR-0006 (Role of XBlocks) points toward reducing XBlock’s dependence on runtime services. As such, any upstream proposal would need to make a case to the community.
So the question we’d like to bring to the community is:
Are we interested in having a plugin based approach to extending the services that the runtimes can expose to Xblocks?
If so, does any of the approaches above feel like the right direction? Are we missing an even better option?
Happy to hear if others have hit this wall, if there are approaches we missed, or if there’s existing discussion we should be reading.
What sort of functionality would your new service have, and how would you expect other XBlocks to make use of it?
If other XBlocks are going to be built to require the new AI service, then I would lean towards entry points because we’d want to express an actual pip installation dependency. Or possibly even creating a new platform-level service, if it’s going to be broadly needed.
FWIW, XBlock runtime services were broadly intended to be pluggable at some point, as this comment in the Service docstring illustrates:
But back then, we also had the notion that XBlock could run in other learning platforms. Once it became clear that they’d only run on our platform, pluggable services became less important as a goal, because most of the service layer was tied to platform anyway. The other consideration is that the service layer abstraction is most useful where there are plausibly different, pluggable implementations of the same service that might exist (e.g. on different LMS systems). If an XBlock really just needs a piece of functionality that your library can provide, it may be simpler if those XBlocks just include your library as an explicit dependency.
What we want to do is to expose some third party service call capabilities. In this case that third party is a LLM service. Naturally, the xblock could directly call the service, but the extension layer provided by the ai-extensions plugin has build in capabilities for author based configuration of how the LLMs are used and audit for the llm responses.
Also, having each xblock pin their own required version of the library would soon get into conflicting library versions such as OpenAI or a router like litellm. We already saw this with one xblock and the ai-extension plugin, so it might only get worse.
One of the use cases we are looking into is ORA grading. We have seen apetite for having a llm grading step that gives learners immediate feedback. This has even been explored by rg in an fork of ora2. This implementation pins the use of openai, requires a hard fork of ora and lacks a lot of the configuration that ai-extensions provides.
How we are envisioning it, the xblock would declare that it wants the ai_service @XBlock.wants("ai_extensions") and if it finds it, then it would present the UI for LLM grading. If the service is not present, then the xblock would still have all the other ORA2 capabilities. That in practice means that if an installation decides to add the ai-extensions plugin, then ora gets new options, but if the installation does not globally decide to add ai, then ora is what is has always been. By using the service instead of a hard ai library requirement we avoid having a dependency in an xblock that forces the whole platform to get AI connection.
There are alternatives for sure. Using pip install extras or even try catching some import, but having the xblock service looks cleaner. It would also allow for a different ai-plugin to implement the service differently and still get ORA to cooperate without having to fork.
This is just an initial idea, but here is how I think about it so far.
If we are trying to have a xblock such as ORA have a grading step that uses AI, we would need to have the user_input and some instructions for the AI. This instructions include the prompt, but we have seen that we need more than just a prompt. In the ai-extensions project this normally includes a list of classes that know how to call the llm, extract content from the definition of the course and naturally the custom prompt that the course authors wrote for this. In there, we call this definition the ai_workflow_profile and the individual classes are called orchestrators and processors.
The simplest way to call the optional ai_service from an xblock would be to let the author configure and store select the profile they want to run when authoring the unit/component in studio and during runtime have something like:
I’m oversimplifying because we’d like to use async tasks eventually, but the gist of the idea is to completely abstract away the use of comercial llm services, api_keys, streaming, self-hosting models and even the act of writing and storing the llm prompt.
An even more abstract way of handling it would be to rely on the ai_workflow_scope model (not related to the xblock.scopes). This just has a way of resolving which ai_workflow_profile best matches the context of the calling function by using the course_id, location_id and a ui_placement_id. Then the selection of the profile happens at the ai-extensions code. We initially thought this is how we wanted to handle it, but I’m thinking now that we better select the profile directly given how the whole thing is to let authors have control.
I suppose that whenever we are working on having LLM feedback at ORA, we are going to need specific orchestrators and processors for this. Most of what we will require from authors is that they either select an existing prompt or write one for their own cases.