Getting Started

This guide provides the essential steps to integrate Project KARL into your Kotlin application. We will cover setting up your project, adding the necessary dependencies, and creating a minimal runnable example to demonstrate a basic KARL integration.

Prerequisites

Before integrating Project KARL, please ensure your development environment and project meet the following prerequisites:

  • Kotlin Version: Project KARL's core and implementation modules are compatible with Kotlin 1.9.23 or higher. Ensure your project is configured to use a compatible Kotlin version.

  • Java Development Kit (JDK): A JDK version of 11 or later is required to compile and run KARL, especially for its JVM-targeted modules (e.g., Desktop examples, KotlinDL backend).

  • Gradle: Your project should be using Gradle 7.3 or a more recent version for dependency management and building.

  • Integrated Development Environment (IDE): IntelliJ IDEA or Android Studio with the latest Kotlin plugin is highly recommended for the best development experience.

  • For UI Integration (:karl-compose-ui & :karl-example-desktop):

    • Jetpack Compose Multiplatform Plugin: Version 1.6.10 (compatible with Kotlin 1.9.23) or a compatible successor.

    • Relevant Jetpack Compose library dependencies (runtime, UI, foundation, material/material3).

  • For Specific Implementations:

    • :karl-room: Requires KSP (Kotlin Symbol Processing) plugin (e.g., version 1.9.23-1.0.19) and AndroidX Room dependencies (e.g., version 2.6.1).

    • :karl-kldl: Requires KotlinDL dependencies (e.g., version 0.5.2 for Kotlin 1.9.23, or a 0.6.0-alpha-x for Kotlin 2.0.x).

It is assumed that you are familiar with Kotlin programming and Gradle build system basics.

Adding KARL to Your Project

To integrate Project KARL, you'll need to configure your Gradle build scripts to include KARL's modules and their dependencies. KARL's modular design allows you to select only the components necessary for your application.

Step 1: Configure Repositories

First, ensure your project can resolve artifacts from standard repositories. This is typically configured in your root settings.gradle.kts file. Your setup should include repositories like mavenCentral(), google(), and the JetBrains Compose repository if you plan to use KARL's UI components or Jetpack Compose in general.

Example snippet for settings.gradle.kts:


              pluginManagement {
                  repositories {
                      gradlePluginPortal()
                      google()
                      mavenCentral()
                      maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
                  }
              }
              dependencyResolutionManagement {
                  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
                  repositories {
                      google()
                      mavenCentral()
                     maven("https.maven.pkg.jetbrains.space/public/p/compose/dev")
                      // TODO: Add repository for KARL artifacts once published
                    }
              }
            

For the most up-to-date repository configuration, please refer to the settings.gradle.kts

Step 2: Add Dependencies using Version Catalog (Recommended)

Project KARL utilizes Gradle's Version Catalog feature (a libs.versions.toml file) for consistent and centralized dependency management. This is the recommended approach.

You will need to define versions and aliases for KARL modules (e.g., karl-core, karl-kldl, karl-room) and their transitive dependencies (like Room, KotlinDL, Coroutines) in your project's gradle/libs.versions.toml file.

Key entries to add/ensure in your gradle/libs.versions.toml:

In the [versions] section, define a version for KARL and its key dependencies:


karl = "x.y.z"  # Replace with the latest KARL release version
room = "2.6.1"  # Example Room version
kotlinDL = "0.5.2" # Example KotlinDL version (ensure compatibility with Kotlin)


In the [libraries] section, define aliases for KARL modules and their dependencies:


karl-core = {group = "com.your-group-id", name ="karl-core", version.ref ="karl"}
karl-kldl = {group = "com.your-group-id", name ="karl-kldl", version.ref ="karl"}
karl-room = {group = "com.your-group-id", name ="karl-room", version.ref ="karl"}
androidx-room-runtime = {group = "androidx.room", name ="room-runtime", version.ref ="room"}
androidx-room-compiler = {group = "androidx.room", name ="room-compiler", version.ref ="room"}
# ... and other necessary libraries like coroutines, serialization, etc.

In the [plugins] section, define aliases for necessary Gradle plugins:


ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 
# Ensure ksp version is defined in [versions]

For a complete reference of required versions and aliases, please consult the authoritative
libs.versions.toml

Then, in your application module's build.gradle.kts (e.g., :app or :karl-example-desktop):

Apply necessary plugins:


plugins
alias(libs.plugins.kotlinMultiplatform) // Or kotlinJvm for a pure JVM module
alias(libs.plugins.ksp) // If using the :karl-room module
// Add Compose plugins if your application or KARL UI components are used
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.kotlinComposeCompiler) // If required by your Kotlin/Compose versions

Add KARL dependencies to the appropriate source set:


// ... your target configurations (jvm, androidTarget, etc.) ...
sourceSets {
  val main by getting { // Adjust to your module's main source set (e.g., commonMain, jvmMain)
    dependencies {
      implementation(libs.karl.core)
        implementation(libs.karl.kldl)      // Your chosen LearningEngine implementation
        implementation(libs.karl.room)      // Your chosen DataStorage implementation
        // implementation(libs.karl.composeUi) // If using KARL's UI components
              
        // Transitive dependencies for implementations (e.g., Room runtime)
        implementation(libs.androidx.room.runtime)
        implementation(libs.androidx.room.ktx)
    }
  }
  // KSP configuration for specific targets if applicable (e.g., jvmMain for Room)
  val jvmMain by getting {
    dependencies {
      kspJvm(libs.androidx.room.compiler)
    }
  }
}

For a fully configured example, please view the build.gradle.kts

Step 3: Sync Gradle Project

After configuring your build scripts, synchronize your project with Gradle. In IntelliJ IDEA or Android Studio, a Sync Now prompt typically appears, or you can trigger this action manually (e.g., via File > Sync Project with Gradle Files or the Gradle tool window).

Your First KARL Integration

This section outlines the core steps to integrate KARL into your application, initialize a KarlContainer, and perform a basic interaction. It assumes you have already set up your project dependencies as described above.

For a complete, runnable code example demonstrating these steps, please refer to our Example
Desktop Application

Core Steps:

Step 1: Implement Your Application's DataSource

KARL learns from user interactions that your application provides. To do this, you must implement the com.karl.core.data.DataSource interface. This implementation will be specific to your application, observing relevant user actions (e.g., button clicks, commands executed, settings changed) and transforming them into InteractionData objects for KARL.

Conceptual Structure of a DataSource:

// In your application's codebase
import com.karl.core.data.DataSource
import com.karl.core.models.InteractionData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
// ... other necessary imports for your event source (e.g., Flow)
              
class AppSpecificDataSource(private val userId: String /*... other dependencies ...*/) : DataSource {
  override fun observeInteractionData(
    onNewData: suspend (InteractionData) -> Unit,
    coroutineScope: CoroutineScope
    ): Job {
      println("DataSource: Starting observation for user $userId...")
      // 1. Subscribe to your application's internal event stream(s) (e.g., a Flow, listeners).
      // 2. When a relevant event occurs, transform it into an InteractionData object:
      // val interaction = InteractionData(type = "event_type", details = mapOf(...), ...)
      // 3. Call the onNewData callback:
      // coroutineScope.launch { onNewData(interaction) }
      // 4. Return the Job associated with the observation.
      TODO("Implement actual observation logic and return Job")
    }

See a concrete implementation in the ExampleDataSource.kt file within our example application

Step 2: Instantiate KARL Dependencies

In your application's setup logic (e.g., when a user session starts, or in your main application initialization), you'll need to create instances of:

  • Your chosen LearningEngine implementation (e.g., an instance of KLDLLearningEngine from the :karl-kldl module).

  • Your chosen DataStorage implementation (e.g., an instance of RoomDataStorage from the :karl-room module, which itself requires an initialized Room database instance).

  • Your custom DataSource implementation (from Step 1).

  • A CoroutineScope that is tied to the lifecycle of the user session or application component that will manage KARL. This scope is crucial for KARL's asynchronous operations and proper cleanup.

Conceptual Snippet:


// In your application's setup code
val userId = "currentUser123"
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) 
// Manage this scope's lifecycle!
              
// Note: Actual instantiation will involve 
// Constructors and Configuration specific to each implementation.
val learningEngine: LearningEngine = KLDLLearningEngineImpl(/* ...config... */)
val dataStorage: DataStorage = RoomDataStorageImpl(/* ...database instance, DAO... */)
val dataSource: DataSource = AppSpecificDataSource(userId /* ...other app dependencies... */)

For details on instantiating specific implementations like KLDLLearningEngine and setting up RoomDataStorage (including database creation), refer to the dependency setup section in our Example Application

Step 3: Build and Initialize the KarlContainer

With the dependencies ready, use the KarlAPI builder to construct and then initialize your KarlContainer instance. Initialization is an asynchronous operation and should typically be launched within your application-managed CoroutineScope.

Conceptual Snippet:


val karlContainer = Karl.forUser(userId)
.withLearningEngine(learningEngine)
.withDataStorage(dataStorage)
.withDataSource(dataSource)
.withCoroutineScope(applicationScope)
// .withInstructions(listOf(KarlInstruction.MinConfidence(0.7f))) // Optional
.buil
applicationScope.launch {
  try {
    karlContainer.initialize(learningEngine, dataStorage, dataSource, emptyList(), applicationScope)
      println("KARL Initialized for $userId.")
      // KARL is now ready and observing data from your DataSource.
  } catch (e: Exception) {
    println("Error initializing KARL: ${e.message}")
  }
}

Step 4: Getting Predictions and Managing Lifecycle

Once initialized, you can request predictions using karlContainer.getPrediction(). Remember to manage the container's lifecycle by calling karlContainer.saveState() (e.g., on app exit) and karlContainer.release() (when the container is no longer needed and its scope is about to be cancelled).

Conceptual Snippet:


// To get a prediction:
applicationScope.launch {
  val prediction = karlContainer.getPrediction()
  if (prediction != null) { /* Use the prediction */ }
}
// During application shutdown or when the user session ends:
fun onAppShutdown() {
  applicationScope.launch {
    karlContainer.saveState().join() // Ensure state is saved
    karlContainer.release()
  }
  applicationScope.cancel() // Cancel the main scope
}

This minimal overview focuses on the setup and core interaction loop. The TODO placeholders from the original example are now addressed by guiding the user to your full example application code for the concrete implementation details of the LearningEngine and DataStorage.

The full lifecycle management and interaction patterns are best understood by examining our Example
Desktop Application

Running the Example

To run the example above (once the TODO's are filled):

  1. Ensure all KARL module dependencies are correctly added to your application module.

  2. Place the KarlManager and AppSpecificDataSource (or similar logic) in your application's startup sequence (e.g., main function for a simple JVM app, Application class or ViewModel for Android, App composable for Jetpack Compose Desktop).

  3. Call karlManager.initializeKarl() at an appropriate point (e.g., application start).

  4. Call karlManager.cleanup() when your application is shutting down to ensure KARL's state is saved and resources are released.

  5. Compile and run your application. Observe the console output for logs from KARL components.

The :karl-example-desktop module in the Project KARL repository provides a more complete, runnable Jetpack Compose Desktop application demonstrating these steps. It is recommended to explore its source code for a practical reference.