learningequality / studio

Content curation tools for Kolibri
https://studio.learningequality.org/
MIT License
116 stars 163 forks source link

Development of Flexible API Layer #4275

Closed akash5100 closed 1 year ago

akash5100 commented 1 year ago

Creating a Flexible API layer in the contentcuration app in Studio

Overview

Within the contentcuration app in Studio, we want to build an API layer that acts as a communication bridge with different backends like Docker Images, Google Cloud Platform's Vertex AI, and VM instances, cloud storage services, etc. The goal is to make sure this API layer can work with these backends, regardless of where or how they do the job. As long as the input and output formats stay the same, this setup provides flexibility in choosing and using backend resources.

Description and outcomes

The stand-alone deployed backend service(s) will not have direct access to contentcuration models or the database for that matter, so this API layer facilitates access to these resources by receiving and returning a standardized requests and responses, irrespective of the backend interacted with.

The Architecture

Screenshot 2023-09-11 at 14 50 06

The main components of this API layer are

  1. Backend Interface
  2. Adapter

Creating the Backend Interface

The core Backend class will be an abstract interface that defines the operations that all backends must support. It also implements the Singleton pattern, and provides a make_request method to forward requests to the chosen Backend, whose formats can be specified using the request and response methods.

ABSTRACT CLASS Backend:
    _instance = None # Private variable to hold the instance

    ABSTRACT METHOD connect()
        # Provides blue print to connect
        pass

    ABSTRACT METHOD make_request(params)
        # provide blue print to make request
        pass

        ABSTRACT METHOD request(params)
        # provide blue print for the request object
        pass

    ABSTRACT METHOD response(params)
        # provides blue print for the response object
        pass

    CLASS METHOD get_instance(cls)
        IF cls._instance is None:
            cls._instance = cls._create_instance()
        return cls._instance

    CLASS METHOD _create_instance(cls)
        raise NotImplementedError # concrete class must implement

Different backends can now be created by implementing the base Backend class:

# Implement CONCRETE CLASS using ABSTRACT Backend class
CLASS GCS IMPLEMENTS Backend:
    METHOD make_request(params):
        # make request to Google Cloud Storage services

    METHOD connect(params):
        # Implement the connect method for GCS

    CLASS METHOD _create_instance(cls)
        # initialize a GCS Backend instance

CLASS ML IMPLEMENTS Backend:
    METHOD make_request(params):
        # make request to DeepLearning models hosted as service

    METHOD connect(params):
        # Implement the connect method for hosted ML service

    CLASS METHOD _create_instance(cls)
        # initialize a ML Backend instance

CLASS OtherBackend IMPLEMENTS Backend:
    ...
    [you get the idea]

To create an instance of a backend, using the ML class as an example, use the get_instance() method:

>>> backend = ML.get_instance()

To centralize the creation of Backend instances based on specific Django settings(e.g. dev vs. production environments), create a base BackendFactory abstract class. This should follow the Factory Design Pattern.

# Abstract class for the Factory to instantiate the Backend based on Django Settings
ABSTRACT CLASS BackendFactory:
    ABSTRACT METHOD create_backend(self) -> Backend
        pass

The BackendFactory's create_backend method optionally allows a Backend instance to be injected into the factory instead of relying solely on Django settings. This is particularly useful if we want to explicitly specify the backend to use.

Creating Adapter that accepts any Backend

The Adapter class can be initialized with a Backend instance which provides a make_request method that forwards requests to the chosen Backend while adhering to its specific request and response formats.

CLASS Adapter:

  METHOD __init__(self, backend)
    # Initialize the Backend with BackendFactory
    SET backend = backend

  METHOD request(self):
    # something
    return self.backend.request()

  METHOD response(self):
    # something
    return self.backend.response()

With this Adapter class in place, we can create Adapter that are able interact with any backend we need.

CLASS Recommendation INHERITS ADAPTER:
    METHOD generateEmbeddings(self, params) -> Boolean
        # [ Implementation ]

    METHOD getRecommendation(self, params) -> Array
        # [ Implementation ]

CLASS Transcription INHERITS ADAPTER:
    METHOD generateCaption(self, params) -> Array
        # [ Implementation ]

CLASS OtherAdapter INHERITS ADAPTER:
    METHOD someOperation(self, params) -> Any
        # Operation that any backend wants

Below is a sample use case, using the ML backend as an example:

>>> backend = ML.get_instance()
>>> adapter =  Transcription(backend)

To access specific methods within the adapter:

>>> adapter.generateCaption(...)

Resources

OOP Design patterns

Acceptance Criteria

  1. A module with an appropriate name is created for the mini-library within the contentcuration app in Studio.
  2. Appropriate folders are created in the created module to clearly distinguish the files(if necessary).
  3. A base abstract Backend class that defines the operation that all backends must support is created.
  4. The Backend class includes relevant abstract methods that allow for specific implementations for the various backends.
  5. The Backend class implements the Singleton pattern.
  6. A base abstract Backend Factory class that centralizes the creation of Backend instances based on Django settings is created.
  7. All Backend instances specified in the BackendFactory follow the singleton pattern, ensuring only one instance of each backend can exist.
  8. A base Adapter class is established, which can be initialized by any Backend.
  9. The Adapter class includes methods that are relevant for interaction with the initialized Backend.
  10. Tests are written to validate the correctness of the base logic implementation.
  11. Documentation is updated to include information about the new mini-library, its usage, and its purpose.

Out of scope

### Tasks
akolson commented 1 year ago

With a considerable amount of actual development work already done by @akash5100 prior to today's iteration planning, @ozer550 has unassigned himself so Akash can proceed to complete the issue.