Extension Development Guide#

This guide provides a quickstart for developers who want to extend the Open edX AI infrastructure by creating custom plugins. We use the openedx-ai-badges project as a reference implementation.

What you will achieve#

  • Understand the core file structure required for an AI infrastructure plugin.

  • Implement a functional entry point following the openedx-ai-badges pattern.

Prerequisites#

Project Structure#

A standard AI extension repository follows a modular layout designed to integrate with both the Open edX backend and the Micro-Frontend (MFE) architecture. While the reference implementation (openedx-ai-badges) includes all layers, the structure is adaptable to your specific needs:

  • backend/: The Django application containing custom processors, orchestrators, and profiles.

  • frontend/: React components and styles that are registered and rendered inside UI slots exposed by the target MFE.

  • tutor/: The Tutor plugin files required for environment configuration and automated deployment.

Note

Modular Flexibility: You do not need to implement all three components. For example, if your feature only requires new data transformation logic, you might only need the backend/ directory. Similarly, a plugin might only provide a specialized UI for existing core logic. The framework allows you to mix and match components based on the specific requirements of your AI workflow. This quickstart, however, follows a full reference extension that includes both backend and frontend pieces, so the steps in the rest of this guide describe that combined implementation path.

Backend Implementation#

The backend logic of an AI plugin is primarily driven by the Workflow Profile. The framework uses these JSON blueprints to dynamically discover and load your custom logic.

1. Define the Workflow Profile#

The Profile is a JSON file that acts as the blueprint for your feature. It maps orchestrators, processors, and UI components into a single configuration.

Create your profile (e.g., workflows/profiles/badges_base.json):

{
  "orchestrator_class": "openedx_ai_badges.workflows.orchestrators.BadgeOrchestrator",
  "processor_config": {
      "BadgeProcessor": {
          "function": "generate_badgeclass",
          "provider": "openai"
      }
  },
  "actuator_config": {
    "UIComponents": {
      "request": {
        "component": "AIRequestBadgesComponent",
        "config": {
          "buttonText": "Generate Badge"
        }
      }
    }
  }
}

2. Register Profiles in Settings#

For the framework to discover your JSON profiles, you must add your plugin’s directory to the WORKFLOW_TEMPLATE_DIRS setting.

In your plugin’s settings (e.g., settings/common.py):

def plugin_settings(settings):
    if not hasattr(settings, "WORKFLOW_TEMPLATE_DIRS"):
        settings.WORKFLOW_TEMPLATE_DIRS = []

    # Add the path to your plugin's profiles
    badges_workflow_dir = BASE_DIR / "workflows" / "profiles"
    if badges_workflow_dir not in settings.WORKFLOW_TEMPLATE_DIRS:
        settings.WORKFLOW_TEMPLATE_DIRS.append(badges_workflow_dir)

3. Plugin Configuration (apps.py)#

Ensure your plugin is correctly registered in Open edX so that the settings above are processed:

In your plugin’s app configuration (for example, backend/openedx_ai_badges/apps.py):

from django.apps import AppConfig
from edx_django_utils.plugins.constants import PluginSettings

class YourPluginConfig(AppConfig):
    name = "your_plugin_name"

    plugin_app = {
        PluginSettings.CONFIG: {
            "lms.djangoapp": { "common": { "relative_path": "settings.common" } },
            "cms.djangoapp": { "common": { "relative_path": "settings.common" } },
        },
    }

4. Implement a Custom Orchestrator#

The orchestrator_class in your Profile JSON uses a full Python path to locate and instantiate your logic. By inheriting from core classes like SessionBasedOrchestrator, you can customize the workflow execution.

# backend/openedx_ai_badges/workflows/orchestrators.py
from openedx_ai_extensions.workflows.orchestrators.session_based_orchestrator import SessionBasedOrchestrator

class BadgeOrchestrator(SessionBasedOrchestrator):
    """
    Custom orchestrator that manages the badge generation sequence.
    """
    def run(self, input_data):
        # Your custom flow logic here
         return {
             "response": badge_data,
             "status": "completed",
         }

5. Processor Configuration Mapping#

For an Orchestrator to use one or more Processors, they must be declared in the processor_config section of the Profile JSON. This mapping connects the logic with specific AI providers and functions.

"processor_config": {
    "BadgeProcessor": {
        "function": "generate_badgeclass",
        "provider": "openai"
    }
}

Within your Orchestrator logic, you can then retrieve and use these processors:

def run(self, input_data):
    # Retrieve the processor as configured in the JSON profile
    badge_processor = BadgeProcessor(self.profile.processor_config)
    llm_result = badge_processor.process(context=str(course_context))
    badge = json.loads(llm_result.get("response", "{}"))
    # ... continue workflow

Note

The process function will call the function defined in the processor_config.

Note

For practical examples of how to implement and combine orchestrators and processors, refer to:

Frontend Implementation#

The frontend integration is governed by a central Extension Registry. There are two primary ways to implement your plugin’s user interface, depending on whether you want the core framework to manage the execution flow or you prefer to handle it manually.

Pattern A: Using ConfigurableAIAssistance#

This is the recommended pattern for standard AI interactions. The core provides a component called ConfigurableAIAssistance that acts as a dynamic frontend orchestrator. It reads the profile’s actuator_config.UIComponents section and renders the request and response UI components declared there.

See the ConfigurableAIAssistance implementation in openedx-ai-extensions for a complete example of that rendering flow.

  • How it works: At runtime, this component fetches the configuration from the backend (based on the AI Profile). It looks for the component names defined in the profile’s actuator_config and resolves them using the frontend registry.

  • Backend Control: The backend dictates which components to render for the “request” and “response” phases by sending their registered names in the API response.

Pattern B: Independent Custom Components#

Use this pattern if you want to manage the UI and the service calls yourself, bypassing the core’s automated flow. This is useful for complex features where the UI needs to do more than just a simple request/response.

  • How it works: You create a component that calls the AI services directly using the utilities provided by the core library.

  • Why the Profile is still required: Even if you manage the UI manually, the Backend still requires a Profile to identify the correct orchestrator_class and processor_config when your component triggers the workflow API.

6. Implementing UI Components#

Create your React components in the frontend/src/components directory.

Example for Pattern A (Managed by Core): Your component will receives params from the Profile JSON.

// frontend/src/components/MyRequestComponent.tsx

const MyRequestComponent = ({ customMessage, buttonText, isLoading }) => (
    <div className="ai-request-wrapper">
        {customMessage}
        <button onClick={onAskAI} disabled={isLoading}>
            {buttonText}
        </button>
    </div>
);

export default MyRequestComponent;

Example for Pattern B (Manually Managed): Your component uses core services to build context and call the backend.

// frontend/src/components/MyIndependentTabComponent.tsx

import { services } from '@openedx/openedx-ai-extensions-ui';

const MyIndependentTabComponent = ({ uiSlotSelectorId, courseId, locationId }) => {
  const handleAction = async () => {
    const context = services.prepareContextData({ uiSlotSelectorId, courseId, locationId });
    const response = await services.callWorkflowService({
      context,
      payload: {
        action: 'run',
      },
    });
    // Handle custom UI behavior with the response
  };

  return <button onClick={handleAction}>Custom Action</button>;
};

7. Component Registration#

The @openedx/openedx-ai-extensions-ui package maintains a registry that maps strings to React components. For Pattern A, the name registered here must match the name used in the Profile JSON.

In your frontend/src/index.tsx (or your plugin’s entry point):

import { registerComponents, REGISTRY_NAMES } from '@openedx/openedx-ai-extensions-ui';
import MyRequestComponent from './components/MyRequestComponent';
import MyIndependentTabComponent from './components/MyIndependentTabComponent';

// Register workflow UI components so ConfigurableAIAssistance can resolve them by name.
registerComponents({
  MyRequestComponent,
});

// Register a specialized entry (for example, a new tab in AI Settings).
registerComponents(REGISTRY_NAMES.SETTINGS, {
  id: 'my-plugin-id',
  label: 'My AI Plugin',
  component: MyIndependentTabComponent,
});

export { MyRequestComponent, MyIndependentTabComponent };

Note

If the backend returns a component name that is not found in the registry, ConfigurableAIAssistance will display a debug alert listing all currently registered components.

The openedx-ai-badges frontend entry point shows a complete example of this registration pattern.

8. Mount the UI in the target MFE#

Registering a component only makes it available to the registry. You still need to import your plugin package into the target micro-frontend and place the appropriate component in the desired UI slot.

For Pattern A, the MFE renders ConfigurableAIAssistance in the slot. Your plugin package is imported so its registration side effects run before the component asks the backend which UI components to render.

import { ConfigurableAIAssistance } from '@openedx/openedx-ai-extensions-ui';
import '@openedx/openedx-ai-badges-ui';

export default function SomeSlotContainer(props) {
  return <ConfigurableAIAssistance {...props} />;
}

For Pattern B, import your custom component into the MFE and render that component directly in the slot, passing whatever props it needs.

Use the slot system provided by the frontend plugin framework to place the component in the appropriate location in the MFE.

Activation and Usage#

After implementing the backend logic, defining the Profile JSON, registering your frontend components, and mounting the UI in the target MFE:

  1. Deploy: Install your plugin in your Open edX environment.

  2. Configure Profile: In Django Admin, create an AI Workflow Profile and select your JSON file as the Base filepath.

  3. Set Scope: Create an AI Workflow Scope to map your profile to the LMS or Studio, as explained in the Usage Guide.

Next Steps#