, M : MLModel, P : Any>(
+ private val mlModel: M,
+ private val collector: PredictionCollector,
+) : SinglePrediction {
+ override fun predict(): P {
+ val prediction = mlModel.predict(collector.usableDescription)
+ collector.submitPrediction(prediction)
+ return prediction
+ }
+
+ override fun cancelPrediction() {
+ collector.submitPrediction(null)
+ }
+}
+
+/**
+ * A [NestableMLSession] of utilizing [mlModel].
+ * The session's structure is collected by [collector] after [onLastNestedSessionCreated],
+ * and all nested sessions' structures are collected.
+ */
+@ApiStatus.Internal
+class MLModelPredictionBranching, M : MLModel, P : Any>(
+ private val mlModel: M,
+ private val collector: NestableStructureCollector
+) : NestableMLSession {
+ override fun createNestedSession(levelMainEnvironment: Environment): Session
{
+ val nestedLevelScheme = collector.levelPositioning.lowerTiers.first()
+ verifyTiersInMain(nestedLevelScheme.main, levelMainEnvironment.tiers)
+ val levelAdditionalTiers = nestedLevelScheme.additional
+
+ return if (collector.levelPositioning.lowerTiers.size == 1) {
+ val nestedCollector = collector.nestPrediction(levelMainEnvironment, levelAdditionalTiers)
+ MLModelPrediction(mlModel, nestedCollector)
+ }
+ else {
+ assert(collector.levelPositioning.lowerTiers.size > 1)
+ val nestedCollector = collector.nestBranch(levelMainEnvironment, levelAdditionalTiers)
+ MLModelPredictionBranching(mlModel, nestedCollector)
+ }
+ }
+
+ override fun onLastNestedSessionCreated() {
+ collector.onLastNestedCollectorCreated()
+ }
+}
+
+private fun verifyTiersInMain(expected: Set>, actual: Set<*>) {
+ require(expected == actual) {
+ "Tier set in the main environment is not like it was declared. " +
+ "Declared $expected, " +
+ "but given $actual"
+ }
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/MLTask.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/MLTask.kt
new file mode 100644
index 000000000000..ea25b50ad652
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/MLTask.kt
@@ -0,0 +1,129 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.platform.ml.*
+import com.intellij.platform.ml.impl.apiPlatform.MLApiPlatform
+import com.intellij.platform.ml.impl.apiPlatform.ReplaceableIJPlatform
+import com.intellij.platform.ml.impl.session.AdditionalTierScheme
+import com.intellij.platform.ml.impl.session.Level
+import com.intellij.platform.ml.impl.session.MainTierScheme
+import org.jetbrains.annotations.ApiStatus
+
+
+/**
+ * Is a declaration of a place in code, where a classical machine learning approach
+ * is desired to be applied.
+ *
+ * The proper way to create new tasks is to create a static object-inheritor of this class
+ * (see inheritors of this class to find all implemented tasks in your project).
+ *
+ * @param name The unique name of an ML Task
+ * @param levels The main tiers of the task that will be provided within the task's application place
+ * @param predictionClass The class of an object, that will serve as "prediction"
+ * @param T The type of prediction
+ */
+@ApiStatus.Internal
+abstract class MLTask protected constructor(
+ val name: String,
+ val levels: List>>,
+ val predictionClass: Class
+)
+
+/**
+ * A method of approaching an ML task.
+ * Usually, it is inferencing an ML model and collecting logs.
+ *
+ * Each [MLTaskApproach] is initialized once by the corresponding [MLTaskApproachInitializer],
+ * then the [apiPlatform] is fixed.
+ *
+ * @see [com.intellij.platform.ml.impl.approach.LogDrivenModelInference] for currently used approach.
+ */
+@ApiStatus.Internal
+interface MLTaskApproach {
+ /**
+ * The task this approach is solving.
+ * Each approach is dedicated to one and only task, and it is aware of it.
+ */
+ val task: MLTask
+
+ /**
+ * The platform, this approach is called within, that was provided by [MLTaskApproachInitializer]
+ */
+ val apiPlatform: MLApiPlatform
+
+ /**
+ * A static declaration of the features, used in the approach.
+ */
+ val approachDeclaration: Declaration
+
+ /**
+ * Acquire the ML model and start the session.
+ *
+ * @return [Session.StartOutcome.Failure] if something went wrong during the start, [Session.StartOutcome.Success]
+ * which contains the started session otherwise.
+ */
+ fun startSession(permanentSessionEnvironment: Environment): Session.StartOutcome
+
+ data class Declaration(
+ val sessionFeatures: Map>>,
+ val levelsScheme: List
+ )
+
+ companion object {
+ fun findMlApproach(task: MLTask
, apiPlatform: MLApiPlatform = ReplaceableIJPlatform): MLTaskApproach
{
+ return apiPlatform.accessApproachFor(task)
+ }
+
+ fun
startMLSession(task: MLTask
,
+ permanentSessionEnvironment: Environment,
+ apiPlatform: MLApiPlatform = ReplaceableIJPlatform): Session.StartOutcome
{
+ val approach = findMlApproach(task, apiPlatform)
+ return approach.startSession(permanentSessionEnvironment)
+ }
+
+ fun
MLTask
.startMLSession(permanentSessionEnvironment: Environment): Session.StartOutcome
{
+ return startMLSession(this, permanentSessionEnvironment)
+ }
+
+ fun
MLTask
.startMLSession(permanentTierInstances: Iterable>): Session.StartOutcome {
+ return this.startMLSession(Environment.of(permanentTierInstances))
+ }
+
+ fun
MLTask
.startMLSession(vararg permanentTierInstances: TierInstance<*>): Session.StartOutcome
{
+ return this.startMLSession(Environment.of(*permanentTierInstances))
+ }
+ }
+}
+
+/**
+ * Initializes an [MLTaskApproach]
+ */
+@ApiStatus.Internal
+interface MLTaskApproachInitializer
{
+ /**
+ * The task, that the created [MLTaskApproach] is dedicated to solve.
+ */
+ val task: MLTask
+
+ /**
+ * Initializes the approach.
+ * It is called only once during the application's runtime.
+ * So it is crucial that this function will accept the [MLApiPlatform] you want it to.
+ *
+ * To access the API in order to build event validator statically,
+ * FUS uses the actual [com.intellij.platform.ml.impl.apiPlatform.IJPlatform], which could be problematic if you
+ * want to test FUS logs.
+ * So make sure that you will replace it with your test platform in
+ * time via [com.intellij.platform.ml.impl.apiPlatform.ReplaceableIJPlatform.replacingWith].
+ */
+ fun initializeApproachWithin(apiPlatform: MLApiPlatform): MLTaskApproach
+
+ companion object {
+ val EP_NAME = ExtensionPointName>("com.intellij.platform.ml.impl.approach")
+ }
+}
+
+typealias LevelScheme = Level, PerTier>
+
+typealias LevelTiers = Level>, Set>>
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/CodeLikePrinter.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/CodeLikePrinter.kt
new file mode 100644
index 000000000000..34ca84d2781c
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/CodeLikePrinter.kt
@@ -0,0 +1,40 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.apiPlatform
+
+import com.intellij.platform.ml.FeatureDeclaration
+import com.intellij.platform.ml.FeatureValueType
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Prints code-like features representation, when they have to be logged for an API user's
+ * convenience (so they can just copy-paste the logged features into their code).
+ */
+@ApiStatus.Internal
+class CodeLikePrinter {
+ private val > FeatureValueType.Enum.codeLikeType: String
+ get() = this.enumClass.name
+
+ private fun FeatureValueType.makeCodeLikeString(name: String): String = when (this) {
+ FeatureValueType.Boolean -> "FeatureDeclaration.boolean(\"$name\")"
+ FeatureValueType.Class -> "FeatureDeclaration.aClass(\"$name\")"
+ FeatureValueType.Double -> "FeatureDeclaration.double(\"$name\")"
+ is FeatureValueType.Enum<*> -> "FeatureDeclaration.enum<${this.codeLikeType}>(\"$name\")"
+ FeatureValueType.Float -> "FeatureDeclaration.float(\"${name}\")"
+ FeatureValueType.Int -> "FeatureDeclaration.int(\"${name}\")"
+ FeatureValueType.Long -> "FeatureDeclaration.long(\"${name}\")"
+ is FeatureValueType.Nullable<*> -> "${this.baseType.makeCodeLikeString(name)}.nullable()"
+ is FeatureValueType.Categorical -> {
+ val possibleValuesSerialized = possibleValues.joinToString(", ") { "\"$it\"" }
+ "FeatureDeclaration.categorical(\"$name\", setOf(${possibleValuesSerialized}))"
+ }
+ FeatureValueType.Version -> "FeatureDeclaration.version(\"${name}\")"
+ }
+
+ fun printCodeLikeString(featureDeclaration: FeatureDeclaration): String {
+ return featureDeclaration.type.makeCodeLikeString(featureDeclaration.name)
+ }
+
+ fun printCodeLikeString(featureDeclarations: Collection>): String {
+ return featureDeclarations.joinToString(", ") { printCodeLikeString(it) }
+ }
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/IJPlatform.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/IJPlatform.kt
new file mode 100644
index 000000000000..2405379f1b5e
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/IJPlatform.kt
@@ -0,0 +1,186 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.apiPlatform
+
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.platform.ml.EnvironmentExtender
+import com.intellij.platform.ml.Feature
+import com.intellij.platform.ml.ObsoleteTierDescriptor
+import com.intellij.platform.ml.TierDescriptor
+import com.intellij.platform.ml.impl.MLTaskApproachInitializer
+import com.intellij.platform.ml.impl.apiPlatform.MLApiPlatform.ExtensionController
+import com.intellij.platform.ml.impl.apiPlatform.ReplaceableIJPlatform.replacingWith
+import com.intellij.platform.ml.impl.logger.MLEvent
+import com.intellij.platform.ml.impl.monitoring.MLApiStartupListener
+import com.intellij.platform.ml.impl.monitoring.MLTaskGroupListener
+import com.intellij.util.application
+import com.intellij.util.messages.Topic
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.annotations.NonNls
+
+@ApiStatus.Internal
+fun interface MessagingProvider {
+ fun provide(collector: (T) -> Unit)
+
+ companion object {
+ inline fun > createTopic(displayName: @NonNls String): Topic {
+ return Topic.create(displayName, P::class.java)
+ }
+
+ fun > collect(topic: Topic): List {
+ val collected = mutableListOf()
+ application.messageBus.syncPublisher(topic).provide { collected.add(it) }
+ return collected
+ }
+ }
+}
+
+fun interface MLTaskListenerProvider : MessagingProvider {
+ companion object {
+ val TOPIC = MessagingProvider.createTopic("ml.task")
+ }
+}
+
+fun interface MLEventProvider : MessagingProvider {
+ companion object {
+ val TOPIC = MessagingProvider.createTopic("ml.event")
+ }
+}
+
+fun interface MLApiStartupListenerProvider : MessagingProvider {
+ companion object {
+ val TOPIC = MessagingProvider.createTopic("ml.startup")
+ }
+}
+
+/**
+ * A representation of the "real-life" [MLApiPlatform], whose content is
+ * the content of the corresponding Extension Points.
+ * It is used at the API's entry point, unless it is not replaced by another.
+ *
+ * It shouldn't be used due to low testability.
+ * Use [ReplaceableIJPlatform] instead.
+ */
+@ApiStatus.Internal
+private data object IJPlatform : MLApiPlatform() {
+ override val tierDescriptors: List
+ get() = TierDescriptor.EP_NAME.extensionList
+
+ override val environmentExtenders: List>
+ get() = EnvironmentExtender.EP_NAME.extensionList
+
+ override val taskApproaches: List>
+ get() = MLTaskApproachInitializer.EP_NAME.extensionList
+
+ override val taskListeners: List
+ get() = MessagingProvider.collect(MLTaskListenerProvider.TOPIC)
+
+ override val events: List
+ get() = MessagingProvider.collect(MLEventProvider.TOPIC)
+
+ override val startupListeners: List
+ get() = MessagingProvider.collect(MLApiStartupListenerProvider.TOPIC)
+
+ override fun addStartupListener(listener: MLApiStartupListener): ExtensionController {
+ val connection = application.messageBus.connect()
+ connection.subscribe(MLApiStartupListenerProvider.TOPIC, MLApiStartupListenerProvider { collector -> collector(listener) })
+ return ExtensionController { connection.disconnect() }
+ }
+
+ override fun addTaskListener(taskListener: MLTaskGroupListener): ExtensionController {
+ val connection = application.messageBus.connect()
+ connection.subscribe(MLTaskListenerProvider.TOPIC, MLTaskListenerProvider { it(taskListener) })
+ return ExtensionController { connection.disconnect() }
+ }
+
+ override fun addEvent(event: MLEvent): ExtensionController {
+ val connection = application.messageBus.connect()
+ connection.subscribe(MLEventProvider.TOPIC, MLEventProvider { it(event) })
+ return ExtensionController { connection.disconnect() }
+ }
+
+ override fun manageNonDeclaredFeatures(descriptor: ObsoleteTierDescriptor, nonDeclaredFeatures: Set) {
+ if (!Registry.`is`("ml.description.logMissing")) return
+ val printer = CodeLikePrinter()
+ val codeLikeMissingDeclaration = printer.printCodeLikeString(nonDeclaredFeatures.map { it.declaration })
+ thisLogger().info("${descriptor::class.java} is missing declaration: setOf($codeLikeMissingDeclaration)")
+ }
+}
+
+/**
+ * Also a "real-life" [MLApiPlatform], but it can be replaced with another one any time.
+ *
+ * We always want to test [com.intellij.platform.ml.impl.MLTaskApproach]es.
+ * But after they are initialized by [com.intellij.platform.ml.impl.MLTaskApproachInitializer],
+ * the passed [MLApiPlatform] could already spread all the way within the API.
+ * But the user-defined instances of the api could be overridden for testing sake.
+ *
+ * To replace all [TierDescriptor], [EnvironmentExtender] and [MLTaskApproachInitializer]
+ * to test your code, you may call [replacingWith] and pass the desired environment,
+ * that contains all the objects you need for your test.
+ */
+@ApiStatus.Internal
+object ReplaceableIJPlatform : MLApiPlatform() {
+ private var replacement: MLApiPlatform? = null
+
+ private val platform: MLApiPlatform
+ get() = replacement ?: IJPlatform
+
+
+ override val tierDescriptors: List
+ get() = platform.tierDescriptors
+
+ override val environmentExtenders: List>
+ get() = platform.environmentExtenders
+
+ override val taskApproaches: List>
+ get() = platform.taskApproaches
+
+
+ override val taskListeners: List
+ get() = platform.taskListeners
+
+ override val events: List
+ get() = platform.events
+
+ override val startupListeners: List
+ get() = platform.startupListeners
+
+
+ override fun addStartupListener(listener: MLApiStartupListener): ExtensionController {
+ return extend(listener) { platform -> platform.addStartupListener(listener) }
+ }
+
+ override fun addTaskListener(taskListener: MLTaskGroupListener): ExtensionController {
+ return extend(taskListener) { platform -> platform.addTaskListener(taskListener) }
+ }
+
+ override fun addEvent(event: MLEvent): ExtensionController {
+ return extend(event) { platform -> platform.addEvent(event) }
+ }
+
+ override fun manageNonDeclaredFeatures(descriptor: ObsoleteTierDescriptor, nonDeclaredFeatures: Set) =
+ platform.manageNonDeclaredFeatures(descriptor, nonDeclaredFeatures)
+
+ private fun extend(obj: T, method: (MLApiPlatform) -> ExtensionController): ExtensionController {
+ val initialPlatform = platform
+ method(initialPlatform)
+ return ExtensionController {
+ require(initialPlatform == platform) {
+ "$obj should be removed within the same platform it was added in." +
+ "It was added in $initialPlatform, but removed from $platform"
+ }
+ }
+ }
+
+ fun replacingWith(apiPlatform: MLApiPlatform, action: () -> T): T {
+ val oldApiPlatform = replacement
+ return try {
+ replacement = apiPlatform
+ action()
+ }
+ finally {
+ replacement = oldApiPlatform
+ }
+ }
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/MLApiPlatform.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/MLApiPlatform.kt
new file mode 100644
index 000000000000..c411bc92ccdc
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/apiPlatform/MLApiPlatform.kt
@@ -0,0 +1,264 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.apiPlatform
+
+import com.intellij.platform.ml.*
+import com.intellij.platform.ml.impl.MLTask
+import com.intellij.platform.ml.impl.MLTaskApproach
+import com.intellij.platform.ml.impl.MLTaskApproachInitializer
+import com.intellij.platform.ml.impl.logger.MLEvent
+import com.intellij.platform.ml.impl.logger.MLEventsLogger
+import com.intellij.platform.ml.impl.monitoring.*
+import com.intellij.platform.ml.impl.monitoring.MLApproachListener.Companion.asJoinedListener
+import com.intellij.platform.ml.impl.monitoring.MLTaskGroupListener.Companion.onAttemptedToStartSession
+import com.intellij.platform.ml.impl.monitoring.MLTaskGroupListener.Companion.targetedApproaches
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Represents an environment, that provides extendable parts of the ML API.
+ *
+ * Each entity inside the API could access the platform, it is running within,
+ * as everything happens after [com.intellij.platform.ml.impl.MLTaskApproachInitializer.initializeApproachWithin],
+ * where the platform is acknowledged.
+ *
+ * All usages of the ij platform functionality (extension points, registry keys, etc.) shall be
+ * accessed via this class.
+ */
+@ApiStatus.Internal
+abstract class MLApiPlatform {
+ private val finishedInitialization = lazy { MLApiPlatformInitializationProcess() }
+ private var initializationStage: InitializationStage = InitializationStage.NotStarted
+
+ /**
+ * Each [MLTaskApproach] is initialized only once during the application's lifetime.
+ * This function keeps track of all approaches that were initialized already, and initializes
+ * them when they are first needed.
+ */
+ fun accessApproachFor(task: MLTask
): MLTaskApproach
{
+ return finishedInitialization.value.getApproachFor(task)
+ }
+
+
+ /**
+ * The extendable static state of the platform that must be fixed to create FUS group's validator.
+ *
+ * These values must be static, as they define the FUS scheme
+ */
+ val staticState: StaticState
+ get() = StaticState(tierDescriptors, environmentExtenders, taskApproaches)
+
+ /**
+ * The descriptors that are available in the platform.
+ * This value is interchangeable during the application runtime,
+ * see [staticState].
+ */
+ abstract val tierDescriptors: List
+
+ /**
+ * The complete list of environment extenders, available in the platform.
+ * This value is interchangeable during the application runtime,
+ * see [staticState].
+ */
+ abstract val environmentExtenders: List>
+
+ /**
+ * The complete list of the approaches for ML tasks, available in the platform.
+ * This value is interchangeable during the application runtime,
+ * see [staticState].
+ */
+ abstract val taskApproaches: List>
+
+
+ /**
+ * All the objects, that are listening execution of ML tasks.
+ * The collection is mutable, so new listeners could be added via [addTaskListener].
+ *
+ * This value is mutable, new listeners could be added anytime.
+ */
+ abstract val taskListeners: List
+
+ /**
+ * Adds another provider for ML tasks' execution process monitoring dynamically.
+ * The event could be removed via the corresponding [ExtensionController.removeExtension] call.
+ * See [taskListeners].
+ */
+ abstract fun addTaskListener(taskListener: MLTaskGroupListener): ExtensionController
+
+ /**
+ * ML events that will be written to FUS logs.
+ * As FUS is initialized only once, on the application's startup, they all must be registered
+ * before that via [addMLEventBeforeFusInitialized].
+ *
+ * This value could be mutable, however, only during a short period of time: after the application's startup,
+ * and before FUS logs initialization.
+ */
+ abstract val events: List
+
+ /**
+ * Adds another ML event dynamically.
+ * The event could be removed via the corresponding [ExtensionController.removeExtension] call.
+ */
+ fun addMLEventBeforeFusInitialized(event: MLEvent): ExtensionController {
+ when (val stage = initializationStage) {
+ is InitializationStage.Failed ->
+ throw Exception("Initialization of ML Api Platform has failed, events could not be added.", stage.asException)
+ is InitializationStage.PotentiallySuccessful -> {
+ require(stage.order <= InitializationStage.InitializingApproaches.order) {
+ "FUS group initialization has already been started, not allowed to register more ML Events"
+ }
+ return addEvent(event)
+ }
+ }
+ }
+
+ /**
+ * The complete list of the listeners that are listening to the process of an MLApiPlatform's initialization.
+ * The initialization is performed in [finishedInitialization]'s init block.
+ *
+ * This value could be mutable, so
+ * additional listeners could be added via [addStartupListener].
+ *
+ * If a listener was added after a certain initialization stage,
+ * only callbacks of those stages will be triggered later that have not happened yet.
+ */
+ abstract val startupListeners: List
+
+ /**
+ * Adds another startup listener.
+ * The listener could be removed via the corresponding [ExtensionController.removeExtension] call.
+ */
+ abstract fun addStartupListener(listener: MLApiStartupListener): ExtensionController
+
+
+ /**
+ * Declares how the computed but non-declared features will be handled.
+ */
+ abstract fun manageNonDeclaredFeatures(descriptor: ObsoleteTierDescriptor, nonDeclaredFeatures: Set)
+
+
+ internal abstract fun addEvent(event: MLEvent): ExtensionController
+
+ fun interface ExtensionController {
+ fun removeExtension()
+ }
+
+ data class StaticState(
+ val tierDescriptors: List,
+ val environmentExtenders: List>,
+ val taskApproaches: List>,
+ )
+
+ private sealed class InitializationStage(val callListener: (MLApiStartupProcessListener) -> Unit) {
+ sealed class PotentiallySuccessful(val order: Int, callListener: (MLApiStartupProcessListener) -> Unit) : InitializationStage(callListener)
+
+ class Failed(lastStage: InitializationStage, nextStage: InitializationStage, exception: Throwable, callListener: (MLApiStartupProcessListener) -> Unit) : InitializationStage(callListener) {
+ val asException = Exception("Failed to proceed from the initialization stage $lastStage to $nextStage", exception)
+ }
+
+ data object NotStarted : PotentiallySuccessful(0, {})
+ data object InitializingApproaches : PotentiallySuccessful(1, { it.onStartedInitializingApproaches() })
+ data class InitializingFUS(val initializedApproaches: Collection>) : PotentiallySuccessful(2, {
+ it.onStartedInitializingFus(initializedApproaches)
+ })
+
+ data object Finished : PotentiallySuccessful(3, { it.onFinished() })
+ }
+
+ private inner class MLApiPlatformInitializationProcess {
+ val approachPerTask: Map, MLTaskApproach<*>>
+ private val completeInitializersList: List> = taskApproaches.toMutableList()
+
+ init {
+ require(initializationStage == InitializationStage.NotStarted) { "ML API Platform's initialization should not be run twice" }
+
+ fun currentStartupListeners(): List = startupListeners.map { it.onBeforeStarted(this@MLApiPlatform) }
+
+ fun proceedToNextStage(nextStage: InitializationStage.PotentiallySuccessful, action: () -> T): T {
+ return try {
+ action().also {
+ currentStartupListeners().forEach { nextStage.callListener(it) }
+ initializationStage = nextStage
+ }
+ }
+ catch (ex: Throwable) {
+ val failure = InitializationStage.Failed(initializationStage, nextStage, ex) { it.onFailed(ex) }
+ initializationStage = failure
+ throw failure.asException
+ }
+ }
+
+ proceedToNextStage(InitializationStage.InitializingApproaches) {}
+
+ val initializedApproachPerTask = mutableListOf>()
+
+ approachPerTask = proceedToNextStage(InitializationStage.InitializingFUS(initializedApproachPerTask)) {
+ completeInitializersList.validate()
+
+ fun initializeApproach(approachInitializer: MLTaskApproachInitializer) {
+ initializedApproachPerTask.add(InitializerAndApproach(
+ approachInitializer,
+ approachInitializer.initializeApproachWithin(this@MLApiPlatform)
+ ))
+ }
+ completeInitializersList.forEach { initializeApproach(it) }
+ initializedApproachPerTask.associate { it.initializer.task to it.approach }
+ }
+
+ proceedToNextStage(InitializationStage.Finished) {
+ MLEventsLogger.Manager.ensureInitialized(okIfInitializing = true, this@MLApiPlatform)
+ }
+ }
+
+ fun getApproachFor(task: MLTask
): MLTaskApproach
{
+ val taskApproach = requireNotNull(approachPerTask[task]) {
+ val mainMessage = "No approach for task $task was found"
+ val lateRegistrationMessage = getLateApproachRegistrationAssumption(task)
+ if (lateRegistrationMessage != null) "$mainMessage. $lateRegistrationMessage" else mainMessage
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return taskApproach as MLTaskApproach
+ }
+
+ private fun getLateApproachRegistrationAssumption(task: MLTask<*>): String? {
+ val currentInitializersList = taskApproaches.toMutableList()
+ if (completeInitializersList == currentInitializersList) return null
+ val taskApproaches = currentInitializersList.filter { it.task == task }
+ if (taskApproaches.isEmpty()) return null
+ require(taskApproaches.size == 1) { "More than one approach for task $task: $taskApproaches" }
+ return "Approach ${taskApproaches.first()} for task ${task.name} was registered after the ML API Platform was initialized"
+ }
+
+ private fun List>.validate() {
+ val duplicateInitializerPerTask = this.groupBy { it.task }.filter { it.value.size > 1 }
+ require(duplicateInitializerPerTask.isEmpty()) {
+ "Found more than one approach for the following tasks: ${duplicateInitializerPerTask}"
+ }
+ }
+ }
+
+ companion object {
+ fun MLApiPlatform.getDescriptorsOfTiers(tiers: Set>): PerTier> {
+ val descriptorsPerTier = tierDescriptors.groupBy { it.tier }
+ return tiers.associateWith { descriptorsPerTier[it] ?: emptyList() }
+ }
+
+ fun MLApiPlatform.ensureApproachesInitialized() {
+ when (val stage = initializationStage) {
+ is InitializationStage.Failed -> throw Exception("Unable to ensure that approaches are initialized", stage.asException)
+ InitializationStage.NotStarted -> finishedInitialization.value
+ InitializationStage.InitializingApproaches -> throw Exception("Recursion detected while initializing approaches")
+ is InitializationStage.InitializingFUS -> return
+ InitializationStage.Finished -> return
+ }
+ }
+
+ fun MLApiPlatform.getJoinedListenerForTask(taskApproach: MLTaskApproach,
+ permanentSessionEnvironment: Environment): MLApproachListener {
+ val relevantGroupListeners = taskListeners.filter { taskApproach.javaClass in it.targetedApproaches }
+ val approachListeners = relevantGroupListeners.mapNotNull {
+ it.onAttemptedToStartSession(taskApproach, permanentSessionEnvironment)
+ }
+ return approachListeners.asJoinedListener()
+ }
+ }
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/AnalysisMethod.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/AnalysisMethod.kt
new file mode 100644
index 000000000000..e156dc93a663
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/AnalysisMethod.kt
@@ -0,0 +1,31 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.approach
+
+import com.intellij.platform.ml.FeatureDeclaration
+import com.intellij.platform.ml.PerTier
+import com.intellij.platform.ml.impl.model.MLModel
+import com.intellij.platform.ml.impl.session.AnalysedRootContainer
+import com.intellij.platform.ml.impl.session.DescribedRootContainer
+import org.jetbrains.annotations.ApiStatus
+import java.util.concurrent.CompletableFuture
+
+/**
+ * Represents the method, that is utilized for the [LogDrivenModelInference]'s analysis.
+ */
+@ApiStatus.Internal
+interface AnalysisMethod, P : Any> {
+ /**
+ * Static declaration of the features, that are used in the session tree's analysis.
+ */
+ val structureAnalysisDeclaration: PerTier>>
+
+ /**
+ * Static declaration of the session's entities, that are not tiers.
+ */
+ val sessionAnalysisDeclaration: Map>>
+
+ /**
+ * Perform the completed session's analysis.
+ */
+ fun analyseTree(treeRoot: DescribedRootContainer): CompletableFuture>
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/GroupedAnalysis.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/GroupedAnalysis.kt
new file mode 100644
index 000000000000..708827a6b63c
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/GroupedAnalysis.kt
@@ -0,0 +1,62 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.approach
+
+import com.intellij.platform.ml.Feature
+import com.intellij.platform.ml.FeatureDeclaration
+import com.intellij.platform.ml.impl.model.MLModel
+import com.intellij.platform.ml.impl.session.DescribedRootContainer
+import com.intellij.platform.ml.impl.session.analysis.*
+import org.jetbrains.annotations.ApiStatus
+import java.util.concurrent.CompletableFuture
+
+/**
+ * The session's assembled analysis declaration.
+ */
+@ApiStatus.Internal
+data class GroupedAnalysisDeclaration(
+ val structureAnalysis: StructureAnalysisDeclaration,
+ val mlModelAnalysis: Set>
+)
+
+/**
+ * The session's assembled analysis itself.
+ */
+@ApiStatus.Internal
+data class GroupedAnalysis, P : Any>(
+ val structureAnalysis: StructureAnalysis,
+ val mlModelAnalysis: Set
+)
+
+/**
+ * Analyzes both structure and ML model.
+ */
+@ApiStatus.Internal
+class JoinedGroupedSessionAnalyser, P : Any>(
+ private val structureAnalysers: Collection>,
+ private val mlModelAnalysers: Collection>,
+) : SessionAnalyser, M, P> {
+ override val analysisDeclaration = GroupedAnalysisDeclaration(
+ structureAnalysis = SessionStructureAnalysisJoiner().joinDeclarations(structureAnalysers.map { it.analysisDeclaration }),
+ mlModelAnalysis = MLModelAnalysisJoiner().joinDeclarations(mlModelAnalysers.map { it.analysisDeclaration })
+ )
+
+ override fun analyse(sessionTreeRoot: DescribedRootContainer): CompletableFuture> {
+ val joinedStructureAnalyser = JoinedSessionAnalyser(
+ structureAnalysers, SessionStructureAnalysisJoiner()
+ )
+ val joinedMLModelAnalyser = JoinedSessionAnalyser(
+ mlModelAnalysers, MLModelAnalysisJoiner()
+ )
+ val structureAnalysis = joinedStructureAnalyser.analyse(sessionTreeRoot)
+ val mlModelAnalysis = joinedMLModelAnalyser.analyse(sessionTreeRoot)
+
+ val futureGroupAnalysis = CompletableFuture.allOf(structureAnalysis, mlModelAnalysis)
+ val completeGroupAnalysis = CompletableFuture>()
+
+ futureGroupAnalysis.thenRun {
+ completeGroupAnalysis.complete(GroupedAnalysis(structureAnalysis.get(), mlModelAnalysis.get()))
+ }
+
+ return completeGroupAnalysis
+ }
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/LanguageSpecific.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/LanguageSpecific.kt
new file mode 100644
index 000000000000..329d9d0a11b7
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/LanguageSpecific.kt
@@ -0,0 +1,36 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.approach
+
+import com.intellij.lang.Language
+import com.intellij.platform.ml.Feature
+import com.intellij.platform.ml.FeatureDeclaration
+import com.intellij.platform.ml.impl.model.MLModel
+import com.intellij.platform.ml.impl.session.DescribedRootContainer
+import com.intellij.platform.ml.impl.session.analysis.MLModelAnalyser
+import org.jetbrains.annotations.ApiStatus
+import java.util.concurrent.CompletableFuture
+
+/**
+ * Something, that is dedicated for one language only.
+ */
+@ApiStatus.Internal
+interface LanguageSpecific {
+ val languageId: String
+}
+
+/**
+ * The analyzer, that adds information about ML model's language to logs.
+ */
+@ApiStatus.Internal
+class ModelLanguageAnalyser : MLModelAnalyser
+ where M : MLModel,
+ M : LanguageSpecific {
+
+ private val LANGUAGE_ID = FeatureDeclaration.categorical("language_id", Language.getRegisteredLanguages().map { it.id }.toSet())
+
+ override val analysisDeclaration = setOf(LANGUAGE_ID)
+
+ override fun analyse(sessionTreeRoot: DescribedRootContainer): CompletableFuture> = CompletableFuture.completedFuture(
+ setOf(LANGUAGE_ID with sessionTreeRoot.root.languageId)
+ )
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/LogDrivenModelInference.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/LogDrivenModelInference.kt
new file mode 100644
index 000000000000..68d72236808f
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/LogDrivenModelInference.kt
@@ -0,0 +1,231 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.approach
+
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.platform.ml.*
+import com.intellij.platform.ml.ScopeEnvironment.Companion.accessibleSafelyByOrNull
+import com.intellij.platform.ml.impl.*
+import com.intellij.platform.ml.impl.apiPlatform.MLApiPlatform
+import com.intellij.platform.ml.impl.apiPlatform.MLApiPlatform.Companion.getDescriptorsOfTiers
+import com.intellij.platform.ml.impl.apiPlatform.MLApiPlatform.Companion.getJoinedListenerForTask
+import com.intellij.platform.ml.impl.environment.ExtendedEnvironment
+import com.intellij.platform.ml.impl.model.MLModel
+import com.intellij.platform.ml.impl.monitoring.MLApproachListener
+import com.intellij.platform.ml.impl.monitoring.MLSessionListener
+import com.intellij.platform.ml.impl.session.*
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * The main way to apply classical machine learning approaches: run the ML model, collect the logs, retrain the model, repeat.
+ *
+ * @param task The task that is solved by this approach.
+ * @param apiPlatform The platform, that the approach will be running within.
+ */
+@ApiStatus.Internal
+abstract class LogDrivenModelInference, P : Any>(
+ override val task: MLTask,
+ override val apiPlatform: MLApiPlatform
+) : MLTaskApproach
{
+ /**
+ * The method that is used to analyze sessions.
+ *
+ * [StructureAndModelAnalysis] is currently used analysis method
+ * that is dedicated to analyze session's tree-like structure,
+ * and the ML model.
+ */
+ abstract val analysisMethod: AnalysisMethod
+
+ /**
+ * Provides an ML model to use during session's lifetime.
+ */
+ abstract val mlModelProvider: MLModel.Provider
+
+ /**
+ * Declares features, that are not used by the ML model, but must be computed anyway,
+ * so they make it to logs.
+ *
+ * A feature cannot be simultaneously declared as "not used description" and as used by the [mlModelProvider]'s
+ * provided model.
+ * If a feature is not declared as "not used but still computed" or as "used by the model", then it will be computed.
+ *
+ * It must contain explicitly declared selectors for each tier used in [task], as well as in [additionallyDescribedTiers].
+ */
+ abstract val notUsedDescription: PerTier
+
+ /**
+ * Performs description's computation.
+ * Could perform caching mechanisms to avoid recomputing features every time.
+ */
+ abstract val descriptionComputer: DescriptionComputer
+
+ /**
+ * Tiers that do not make a part of te [task], but they could be described and passed to the ML model.
+ *
+ * The size of this list must correspond to the number of levels in the solved [task].
+ */
+ abstract val additionallyDescribedTiers: List>>
+
+ private val levels: List by lazy {
+ (task.levels zip additionallyDescribedTiers).map { Level(it.first, it.second) }
+ }
+
+ private val approachValidation: Unit by lazy { validateApproach() }
+
+ override fun startSession(permanentSessionEnvironment: Environment): Session.StartOutcome {
+ return startSessionMonitoring(permanentSessionEnvironment)
+ }
+
+ private fun startSessionMonitoring(permanentSessionEnvironment: Environment): Session.StartOutcome
{
+ val approachListener = apiPlatform.getJoinedListenerForTask(this, permanentSessionEnvironment)
+ try {
+ return acquireModelAndStartSession(permanentSessionEnvironment, approachListener)
+ }
+ catch (e: Throwable) {
+ approachListener.onFailedToStartSessionWithException(e)
+ return Session.StartOutcome.UncaughtException(e)
+ }
+ }
+
+ private fun acquireModelAndStartSession(permanentSessionEnvironment: Environment,
+ approachListener: MLApproachListener): Session.StartOutcome {
+ approachValidation
+
+ val extendedPermanentSessionEnvironment = ExtendedEnvironment(
+ apiPlatform.environmentExtenders,
+ permanentSessionEnvironment,
+ mlModelProvider.requiredTiers
+ )
+
+ val mlModel: M = run {
+ val mlModelProviderEnvironment = extendedPermanentSessionEnvironment.accessibleSafelyByOrNull(mlModelProvider)
+ if (mlModelProviderEnvironment == null) {
+ val failure = InsufficientEnvironmentForModelProviderOutcome
(mlModelProvider.requiredTiers,
+ extendedPermanentSessionEnvironment.tiers)
+ approachListener.onFailedToStartSession(failure)
+ return failure
+ }
+ val nullableMlModel = mlModelProvider.provideModel(levels, mlModelProviderEnvironment)
+ if (nullableMlModel == null) {
+ val failure = ModelNotAcquiredOutcome
()
+ approachListener.onFailedToStartSession(failure)
+ return failure
+ }
+ nullableMlModel
+ }
+
+ var sessionListener: MLSessionListener? = null
+
+ val analyseThenLogStructure = SessionTreeHandler, M, P> { treeRoot ->
+ sessionListener?.onSessionDescriptionFinished(treeRoot)
+ analysisMethod.analyseTree(treeRoot).thenApplyAsync { analysedSession ->
+ sessionListener?.onSessionAnalysisFinished(analysedSession)
+ }.exceptionally {
+ thisLogger().error(it)
+ }
+ }
+
+ val session = if (levels.size == 1) {
+ val collector = SolitaryLeafCollector(
+ apiPlatform, levels.first(), descriptionComputer, notUsedDescription,
+ permanentSessionEnvironment, levels.first().additional, mlModel
+ )
+ collector.handleCollectedTree(analyseThenLogStructure)
+ MLModelPrediction(mlModel, collector)
+ }
+ else {
+ val collector = RootCollector(
+ apiPlatform, levels, descriptionComputer, notUsedDescription,
+ permanentSessionEnvironment, levels.first().additional, mlModel
+ )
+ collector.handleCollectedTree(analyseThenLogStructure)
+ MLModelPredictionBranching(mlModel, collector)
+ }
+
+ sessionListener = approachListener.onStartedSession(session)
+
+ return Session.StartOutcome.Success(session)
+ }
+
+ override val approachDeclaration: MLTaskApproach.Declaration
+ get() {
+ approachValidation
+
+ return MLTaskApproach.Declaration(
+ sessionFeatures = analysisMethod.sessionAnalysisDeclaration,
+ levelsScheme = levels.map { levelTiers ->
+ Level(
+ buildMainTiersScheme(levelTiers.main, apiPlatform),
+ buildAdditionalTiersScheme(levelTiers.additional, apiPlatform),
+ )
+ }
+ )
+ }
+
+ private fun validateApproach() {
+ require(task.levels.size == additionallyDescribedTiers.size) {
+ "Task $task has ${task.levels.size} levels, when 'additionallyDescribedTiers' has ${additionallyDescribedTiers.size}"
+ }
+
+ require(levels.isNotEmpty()) { "Task must declare at least one level" }
+
+ val maybeDuplicatedTaskTiers = levels.flatMap { it.main + it.additional }
+ val taskTiers = maybeDuplicatedTaskTiers.toSet()
+
+ require(maybeDuplicatedTaskTiers.size == taskTiers.size) {
+ "There are duplicated tiers in the declaration: ${maybeDuplicatedTaskTiers - taskTiers}"
+ }
+ require(notUsedDescription.keys == taskTiers) {
+ "Selectors for those and only those tiers must be represented in the 'notUsedDescription' that are present in the task. " +
+ "Missing: ${taskTiers - notUsedDescription.keys}, " +
+ "Redundant: ${notUsedDescription.keys - taskTiers}"
+ }
+ }
+
+ private fun buildTierDescriptionDeclaration(tierDescriptors: Collection): Set> {
+ return tierDescriptors.flatMap {
+ if (it is ObsoleteTierDescriptor) it.partialDescriptionDeclaration else it.descriptionDeclaration
+ }.toSet()
+ }
+
+ private fun buildMainTiersScheme(tiers: Set>, apiEnvironment: MLApiPlatform): PerTier {
+ val tiersDescriptors = apiEnvironment.getDescriptorsOfTiers(tiers)
+
+ return tiers.associateWith { tier ->
+ val tierDescriptors = tiersDescriptors.getValue(tier)
+ val descriptionDeclaration = buildTierDescriptionDeclaration(tierDescriptors)
+ val analysisDeclaration = analysisMethod.structureAnalysisDeclaration[tier] ?: emptySet()
+ MainTierScheme(descriptionDeclaration, analysisDeclaration)
+ }
+ }
+
+ private fun buildAdditionalTiersScheme(tiers: Set>, apiEnvironment: MLApiPlatform): PerTier {
+ val tiersDescriptors = apiEnvironment.getDescriptorsOfTiers(tiers)
+
+ return tiers.associateWith { tier ->
+ val tierDescriptors = tiersDescriptors.getValue(tier)
+ val descriptionDeclaration = buildTierDescriptionDeclaration(tierDescriptors)
+ AdditionalTierScheme(descriptionDeclaration)
+ }
+ }
+}
+
+/**
+ * An exception that indicates that for some reason, it was not possible to provide an ML model
+ * when calling [com.intellij.platform.ml.impl.model.MLModel.Provider.provideModel].
+ * The session's start is considered as failed.
+ */
+@ApiStatus.Internal
+class ModelNotAcquiredOutcome : Session.StartOutcome.Failure
() {
+ override val failureDetails: String = "ML Model was not provided"
+}
+
+/**
+ * There were not enough tiers to satisfy [MLModel.Provider]'s requirements, so it could not provide the model.
+ */
+@ApiStatus.Internal
+class InsufficientEnvironmentForModelProviderOutcome
(
+ expectedTiers: Set>,
+ existingTiers: Set>
+) : Session.StartOutcome.Failure() {
+ override val failureDetails: String = "ML Model could not be provided: environment is not sufficient. Missing: ${expectedTiers - existingTiers}"
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/StructureAndModelAnalysis.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/StructureAndModelAnalysis.kt
new file mode 100644
index 000000000000..14cd0c5eece6
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/StructureAndModelAnalysis.kt
@@ -0,0 +1,81 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.approach
+
+import com.intellij.platform.ml.FeatureDeclaration
+import com.intellij.platform.ml.PerTierInstance
+import com.intellij.platform.ml.impl.model.MLModel
+import com.intellij.platform.ml.impl.session.*
+import com.intellij.platform.ml.impl.session.analysis.MLModelAnalyser
+import com.intellij.platform.ml.impl.session.analysis.StructureAnalyser
+import com.intellij.platform.ml.impl.session.analysis.StructureAnalysisDeclaration
+import org.jetbrains.annotations.ApiStatus
+import java.util.concurrent.CompletableFuture
+
+/**
+ * An analysis method, that asynchronously analyzes session structure after
+ * it finished, and the ML model that has been used during the session.
+ *
+ * @param structureAnalysers Analyzing session's structure - main tier instances.
+ * @param mlModelAnalysers Analyzing the ML model which was producing predictions during the session.
+ * @param sessionAnalysisKeyModel Key that will be used in logs, to write ML model's features into.
+ */
+@ApiStatus.Internal
+class StructureAndModelAnalysis, P : Any>(
+ structureAnalysers: Collection>,
+ mlModelAnalysers: Collection>,
+ private val sessionAnalysisKeyModel: String = DEFAULT_SESSION_KEY_ML_MODEL
+) : AnalysisMethod {
+ private val groupedAnalyser = JoinedGroupedSessionAnalyser(structureAnalysers, mlModelAnalysers)
+
+ override val structureAnalysisDeclaration: StructureAnalysisDeclaration
+ get() = groupedAnalyser.analysisDeclaration.structureAnalysis
+
+ override val sessionAnalysisDeclaration: Map>> = mapOf(
+ sessionAnalysisKeyModel to groupedAnalyser.analysisDeclaration.mlModelAnalysis
+ )
+
+ override fun analyseTree(treeRoot: DescribedRootContainer): CompletableFuture> {
+ return groupedAnalyser.analyse(treeRoot).thenApply {
+ buildAnalysedSessionTree(treeRoot, it) as AnalysedRootContainer
+ }
+ }
+
+ private fun buildAnalysedSessionTree(tree: DescribedSessionTree, analysis: GroupedAnalysis): AnalysedSessionTree {
+ val treeAnalysisPerInstance: PerTierInstance = tree.level.main.entries
+ .associate { (tierInstance, data) ->
+ tierInstance to AnalysedTierData(data.description,
+ analysis.structureAnalysis[tree]?.get(tierInstance.tier) ?: emptySet())
+ }
+
+ val analysedLevel = Level(
+ main = treeAnalysisPerInstance,
+ additional = tree.level.additional
+ )
+
+ return when (tree) {
+ is SessionTree.Branching -> {
+ SessionTree.Branching(analysedLevel,
+ tree.children.map { buildAnalysedSessionTree(it, analysis) })
+ }
+ is SessionTree.Leaf -> {
+ SessionTree.Leaf(analysedLevel, tree.prediction)
+ }
+ is SessionTree.ComplexRoot -> {
+ SessionTree.ComplexRoot(mapOf(sessionAnalysisKeyModel to analysis.mlModelAnalysis),
+ analysedLevel,
+ tree.children.map { buildAnalysedSessionTree(it, analysis) }
+ )
+ }
+ is SessionTree.SolitaryLeaf -> {
+ SessionTree.SolitaryLeaf(mapOf(sessionAnalysisKeyModel to analysis.mlModelAnalysis),
+ analysedLevel,
+ tree.prediction
+ )
+ }
+ }
+ }
+
+ companion object {
+ const val DEFAULT_SESSION_KEY_ML_MODEL: String = "ml_model"
+ }
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/Versioned.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/Versioned.kt
new file mode 100644
index 000000000000..3be44a757368
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/approach/Versioned.kt
@@ -0,0 +1,37 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.approach
+
+import com.intellij.openapi.util.Version
+import com.intellij.platform.ml.Feature
+import com.intellij.platform.ml.FeatureDeclaration
+import com.intellij.platform.ml.impl.model.MLModel
+import com.intellij.platform.ml.impl.session.DescribedRootContainer
+import com.intellij.platform.ml.impl.session.analysis.MLModelAnalyser
+import org.jetbrains.annotations.ApiStatus
+import java.util.concurrent.CompletableFuture
+
+/**
+ * Something, that has versions.
+ */
+@ApiStatus.Internal
+interface Versioned {
+ val version: Version?
+}
+
+/**
+ * Adds model's version to the ML logs.
+ */
+@ApiStatus.Internal
+class ModelVersionAnalyser : MLModelAnalyser
+ where M : MLModel,
+ M : Versioned {
+ companion object {
+ val VERSION = FeatureDeclaration.version("version").nullable()
+ }
+
+ override val analysisDeclaration = setOf(VERSION)
+
+ override fun analyse(sessionTreeRoot: DescribedRootContainer): CompletableFuture> = CompletableFuture.completedFuture(
+ setOf(VERSION with sessionTreeRoot.root.version)
+ )
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/EnvironmentResolver.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/EnvironmentResolver.kt
new file mode 100644
index 000000000000..ffaee813b55e
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/EnvironmentResolver.kt
@@ -0,0 +1,35 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.environment
+
+import com.intellij.platform.ml.EnvironmentExtender
+import com.intellij.platform.ml.Tier
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * There was circle within available extenders' set's requirements.
+ * Meaning that to create some tier, we must eventually run the extender itself.
+ */
+@ApiStatus.Internal
+class CircularRequirementException(val extensionPath: List>) : IllegalArgumentException() {
+ override val message: String = "A circular resolve path found among EnvironmentExtenders: ${serializePath()}"
+
+ private fun serializePath(): String {
+ val extensions = extensionPath.map { extender -> "[$extender] -> ${extender.extendingTier.name}" }
+ return extensions.joinToString(" - ")
+ }
+}
+
+/**
+ * An algorithm for resolving order of [EnvironmentExtender]s' execution.
+ */
+@ApiStatus.Internal
+interface EnvironmentResolver {
+ /**
+ * @return The order, which guarantees that for each extender, all the requirements will be fulfilled by the previously runned
+ * extenders.
+ * But it still could happen that an [EnvironmentExtender] will not return the tier it extends, then some subsequent
+ * extenders' requirements could not be satisfied.
+ * @throws CircularRequirementException
+ */
+ fun resolve(extenderPerTier: Map, EnvironmentExtender<*>>): List>
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/ExtendedEnvironment.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/ExtendedEnvironment.kt
new file mode 100644
index 000000000000..2f74d5f15166
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/ExtendedEnvironment.kt
@@ -0,0 +1,139 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.environment
+
+import com.intellij.platform.ml.*
+import com.intellij.platform.ml.EnvironmentExtender.Companion.extendTierInstance
+import com.intellij.platform.ml.ScopeEnvironment.Companion.accessibleSafelyByOrNull
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * An environment that is built in to fulfill a [TierRequester]'s requirements.
+ *
+ * When built, it accepts all available extenders, and it is trying to resolve their
+ * order to extend particular tiers.
+ *
+ * If there are some main tiers passed to build the extended environment, they will
+ * not be overridden.
+ *
+ * Among the available extenders, passed to the constructor, there could be some that
+ * extend the same tier.
+ * This could signify that an instance of the same tier can be mined from
+ * different sets of objects (requirements).
+ * But if there is more than one extender, that could potentially be run that will
+ * extend a tier of the same instance, then [IllegalArgumentException] will be thrown
+ * telling, that there is an ambiguity.
+ *
+ * When we have determined which extenders could potentially be run, we try to determine
+ * the order with topological sort: [com.intellij.platform.ml.impl.environment.TopologicalSortingResolver].
+ * If there is a circle in the requirements, it will throw [com.intellij.platform.ml.impl.environment.CircularRequirementException].
+ */
+@ApiStatus.Internal
+class ExtendedEnvironment : Environment {
+ private val storage: Environment
+
+ /**
+ * @param environmentExtenders Extenders, that will be used to extend the [tiersToExtend].
+ * @param mainEnvironment Tiers that are already determined and shall not be replaced.
+ * @param tiersToExtend Tiers that should be put to the extended environment via [environmentExtenders].
+ * It could not be guaranteed that all desired tiers will be extended.
+ *
+ * @return An environment that contains all tiers from [mainEnvironment] plus
+ * some tiers from [tiersToExtend], if it was possible to extend them.
+ */
+ constructor(environmentExtenders: List>,
+ mainEnvironment: Environment,
+ tiersToExtend: Set>) {
+ require(tiersToExtend.all { it !in mainEnvironment })
+ val nonOverridingExtenders = environmentExtenders.filter { it.extendingTier !in mainEnvironment }
+ storage = buildExtendedEnvironment(
+ tiersToExtend + mainEnvironment.tiers,
+ nonOverridingExtenders + mainEnvironment.separateIntoExtenders()
+ )
+ }
+
+ /**
+ * @param environmentExtenders Extenders that will be utilized to build the extended environment.
+ * @param mainEnvironment An already existing environment, instances from which shall not be overridden.
+ *
+ * @return An environment that contains all tiers from [mainEnvironment] plus
+ * all tiers that it was possible to acquire via [environmentExtenders].
+ */
+ constructor(environmentExtenders: List>,
+ mainEnvironment: Environment) {
+ val nonOverridingExtenders = environmentExtenders.filter { it.extendingTier !in mainEnvironment }
+ storage = buildExtendedEnvironment(
+ nonOverridingExtenders.map { it.extendingTier }.toSet() + mainEnvironment.tiers,
+ nonOverridingExtenders + mainEnvironment.separateIntoExtenders()
+ )
+ }
+
+ override val tiers: Set>
+ get() = storage.tiers
+
+ override fun getInstance(tier: Tier): T {
+ return storage.getInstance(tier)
+ }
+
+ companion object {
+ private val ENVIRONMENT_RESOLVER = TopologicalSortingResolver()
+
+ /**
+ * Creates an [Environment] that contents tiers from [tiers], that were successfully extended by [extenders]
+ */
+ private fun buildExtendedEnvironment(tiers: Set>,
+ extenders: List>): Environment {
+ val validatedExtendersPerTier = validateExtenders(tiers, extenders)
+ val extensionOrder = ENVIRONMENT_RESOLVER.resolve(validatedExtendersPerTier)
+ val storage = TierInstanceStorage()
+
+ extensionOrder.map {
+ val safelyAccessibleEnvironment = storage.accessibleSafelyByOrNull(it) ?: return@map
+ it.extendTierInstance(safelyAccessibleEnvironment)?.let { extendedTierInstance ->
+ storage.putTierInstance(extendedTierInstance)
+ }
+ }
+ return storage
+ }
+
+ private fun validateExtenders(tiers: Set>, extenders: List>): Map, EnvironmentExtender<*>> {
+ val extendableTiers: Set> = extenders.map { it.extendingTier }.toSet()
+
+ val runnableExtenders = extenders
+ .filter { desiredExtender ->
+ desiredExtender.requiredTiers.all { requirementForDesiredExtender -> requirementForDesiredExtender in extendableTiers }
+ }
+
+ val ambiguouslyExtendableTiers: MutableList, List>>> = mutableListOf()
+ val extendersPerTier: Map, EnvironmentExtender<*>> = runnableExtenders
+ .groupBy { it.extendingTier }
+ .mapNotNull { (tier, tierExtenders) ->
+ if (tierExtenders.size > 1) {
+ ambiguouslyExtendableTiers.add(tier to tierExtenders)
+ null
+ }
+ else
+ tierExtenders.first()
+ }
+ .associateBy { it.extendingTier }
+ .filterKeys { it in tiers }
+
+ require(ambiguouslyExtendableTiers.isEmpty()) { "Some tiers could be extended ambiguously: $ambiguouslyExtendableTiers" }
+
+ return extendersPerTier
+ }
+ }
+}
+
+private fun Environment.separateIntoExtenders(): List> {
+ class ContainingExtender(private val tier: Tier) : EnvironmentExtender {
+ override val extendingTier: Tier = tier
+
+ override fun extend(environment: Environment): T {
+ return this@separateIntoExtenders[tier]
+ }
+
+ override val requiredTiers: Set> = emptySet()
+ }
+
+ return this.tiers.map { tier -> ContainingExtender(tier) }
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/TopologicalSortingResolver.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/TopologicalSortingResolver.kt
new file mode 100644
index 000000000000..857fdd553727
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/environment/TopologicalSortingResolver.kt
@@ -0,0 +1,49 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.environment
+
+import com.intellij.platform.ml.EnvironmentExtender
+import com.intellij.platform.ml.Tier
+import org.jetbrains.annotations.ApiStatus
+
+private typealias Node = EnvironmentExtender<*>
+
+/**
+ * Resolves order using topological sort
+ */
+@ApiStatus.Internal
+class TopologicalSortingResolver : EnvironmentResolver {
+
+ override fun resolve(extenderPerTier: Map, EnvironmentExtender<*>>): List> {
+ val graph: Map> = extenderPerTier.values
+ .associateWith { desiredExtender ->
+ desiredExtender.requiredTiers.map { requirementForDesiredExtender -> extenderPerTier.getValue(requirementForDesiredExtender) }
+ }
+
+ val reverseTopologicalOrder: MutableList = mutableListOf()
+ val resolveStatus: MutableMap = mutableMapOf()
+
+ fun Node.resolve(path: List) {
+ when (resolveStatus[this]) {
+ ResolveState.STARTED -> throw CircularRequirementException(path + this)
+ ResolveState.RESOLVED -> return
+ null -> {
+ resolveStatus[this] = ResolveState.STARTED
+ for (nextNode in graph.getValue(this)) {
+ nextNode.resolve(path + this)
+ }
+ resolveStatus[this] = ResolveState.RESOLVED
+ reverseTopologicalOrder.add(this)
+ }
+ }
+ }
+
+ graph.keys.forEach { it.resolve(emptyList()) }
+
+ return reverseTopologicalOrder
+ }
+
+ private enum class ResolveState {
+ STARTED,
+ RESOLVED
+ }
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/logger/FusSessionEventBuilder.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/logger/FusSessionEventBuilder.kt
new file mode 100644
index 000000000000..f2a13c183247
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/logger/FusSessionEventBuilder.kt
@@ -0,0 +1,49 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.logger
+
+import com.intellij.internal.statistic.eventLog.events.EventPair
+import com.intellij.internal.statistic.eventLog.events.ObjectDescription
+import com.intellij.internal.statistic.eventLog.events.ObjectEventData
+import com.intellij.platform.ml.impl.MLTaskApproach
+import com.intellij.platform.ml.impl.session.AnalysedRootContainer
+import com.intellij.platform.ml.impl.session.AnalysedSessionTree
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Represents FUS fields of a session's subtree.
+ */
+@ApiStatus.Internal
+abstract class SessionFields : ObjectDescription() {
+ fun buildObjectEventData(sessionStructure: AnalysedSessionTree
) = ObjectEventData(buildEventPairs(sessionStructure))
+
+ abstract fun buildEventPairs(sessionStructure: AnalysedSessionTree
): List>
+}
+
+/**
+ * Represents a logging scheme for the FUS event.
+ *
+ * @param P The type of the ML task's prediction
+ */
+@ApiStatus.Internal
+interface FusSessionEventBuilder {
+ /**
+ * Configuration of a [FusSessionEventBuilder], that builds it when accepts approach's declaration.
+ */
+ interface FusScheme
{
+ fun createEventBuilder(approachDeclaration: MLTaskApproach.Declaration): FusSessionEventBuilder
+ }
+
+ /**
+ * Builds declaration of all features, that will be logged for the session tiers' description and analysis.
+ * It is required because FUS logs validators are built 'statically'.
+ */
+ fun buildFusDeclaration(): SessionFields
+
+ /**
+ * Builds a concrete FUS record, that contains fields that were built by [buildFusDeclaration].
+ *
+ * @param sessionStructure A session tree that been already analyzed and is ready to be logged.
+ * @param sessionFields Session fields that were built by [buildFusDeclaration] earlier.
+ */
+ fun buildRecord(sessionStructure: AnalysedRootContainer
, sessionFields: SessionFields
): Array>
+}
diff --git a/platform/ml-impl/src/com/intellij/platform/ml/impl/logger/InplaceFeaturesScheme.kt b/platform/ml-impl/src/com/intellij/platform/ml/impl/logger/InplaceFeaturesScheme.kt
new file mode 100644
index 000000000000..24c34cc656a7
--- /dev/null
+++ b/platform/ml-impl/src/com/intellij/platform/ml/impl/logger/InplaceFeaturesScheme.kt
@@ -0,0 +1,390 @@
+// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.platform.ml.impl.logger
+
+import com.intellij.internal.statistic.eventLog.FeatureUsageData
+import com.intellij.internal.statistic.eventLog.events.*
+import com.intellij.platform.ml.*
+import com.intellij.platform.ml.impl.*
+import com.intellij.platform.ml.impl.session.*
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * The currently used FUS logging scheme.
+ * Inplace means that the features are logged beside the tiers' instances and
+ * are not compressed in any way.
+ *
+ * An example of a FUS record could be found in the test resources:
+ * [testResources/ml_logs.js](community/platform/ml-impl/testResources/ml_logs.js)
+ */
+@ApiStatus.Internal
+class InplaceFeaturesScheme internal constructor(
+ private val predictionValidationRule: List,
+ private val predictionTransform: (P?) -> String,
+ private val approachDeclaration: MLTaskApproach.Declaration
+) : FusSessionEventBuilder {
+ class FusScheme
(
+ private val predictionValidationRule: List,
+ private val predictionTransform: (P?) -> String
+ ) : FusSessionEventBuilder.FusScheme {
+ override fun createEventBuilder(approachDeclaration: MLTaskApproach.Declaration): FusSessionEventBuilder
= InplaceFeaturesScheme(
+ predictionValidationRule,
+ predictionTransform,
+ approachDeclaration
+ )
+
+ companion object {
+ val DOUBLE: FusScheme = FusScheme(listOf("{regexp#float}")) { it.toString() }
+ }
+ }
+
+ override fun buildFusDeclaration(): SessionFields {
+ require(approachDeclaration.levelsScheme.isNotEmpty())
+ return if (approachDeclaration.levelsScheme.size == 1)
+ PredictionSessionFields(approachDeclaration.levelsScheme.first(), predictionValidationRule, predictionTransform,
+ approachDeclaration.sessionFeatures)
+ else
+ NestableSessionFields(approachDeclaration.levelsScheme.first(), approachDeclaration.levelsScheme.drop(1), predictionValidationRule,
+ predictionTransform, approachDeclaration.sessionFeatures)
+ }
+
+ override fun buildRecord(sessionStructure: AnalysedRootContainer
, sessionFields: SessionFields
): Array> {
+ return sessionFields.buildEventPairs(sessionStructure).toTypedArray()
+ }
+}
+
+private class PredictionField(
+ override val name: String,
+ override val validationRule: List,
+ val transform: (T?) -> String
+) : PrimitiveEventField() {
+ override fun addData(fuData: FeatureUsageData, value: T?) {
+ fuData.addData(name, transform(value))
+ }
+}
+
+private data class StringField(override val name: String, private val possibleValues: Set) : PrimitiveEventField() {
+ override fun addData(fuData: FeatureUsageData, value: String) {
+ fuData.addData(name, value)
+ }
+
+ override val validationRule = listOf(
+ "{enum:${possibleValues.joinToString("|")}}"
+ )
+}
+
+private data class VersionField(override val name: String) : PrimitiveEventField() {
+ override val validationRule: List
+ get() = listOf("{regexp#version}")
+
+ override fun addData(fuData: FeatureUsageData, value: String?) {
+ fuData.addVersionByString(value)
+ }
+}
+
+private fun FeatureDeclaration<*>.toEventField(): EventField<*> {
+ return when (val valueType = type) {
+ is FeatureValueType.Enum<*> -> EnumEventField(name, valueType.enumClass, Enum<*>::name)
+ is FeatureValueType.Int -> IntEventField(name)
+ is FeatureValueType.Long -> LongEventField(name)
+ is FeatureValueType.Class -> ClassEventField(name)
+ is FeatureValueType.Boolean -> BooleanEventField(name)
+ is FeatureValueType.Double -> DoubleEventField(name)
+ is FeatureValueType.Float -> FloatEventField(name)
+ is FeatureValueType.Nullable -> FeatureDeclaration(name, valueType.baseType).toEventField()
+ is FeatureValueType.Categorical -> StringField(name, valueType.possibleValues)
+ is FeatureValueType.Version -> VersionField(name)
+ }
+}
+
+private fun > Feature.Enum.toEventPair(): EventPair<*> {
+ return EnumEventField(declaration.name, valueType.enumClass, Enum<*>::name) with value
+}
+
+private fun Feature.Nullable.toEventPair(): EventPair<*>? {
+ return value?.let {
+ baseType.instantiate(this.declaration.name, it).toEventPair()
+ }
+}
+
+private fun Feature.toEventPair(): EventPair<*>? {
+ return when (this) {
+ is Feature.TypedFeature<*> -> typedToEventPair()
+ }
+}
+
+private fun Feature.TypedFeature.typedToEventPair(): EventPair<*>? {
+ return when (this) {
+ is Feature.Boolean -> BooleanEventField(declaration.name) with this.value
+ is Feature.Categorical -> StringField(declaration.name, this.valueType.possibleValues) with this.value
+ is Feature.Class -> ClassEventField(declaration.name) with this.value
+ is Feature.Double -> DoubleEventField(declaration.name) with this.value
+ is Feature.Enum<*> -> toEventPair()
+ is Feature.Float -> FloatEventField(declaration.name) with this.value
+ is Feature.Int -> IntEventField(declaration.name) with this.value
+ is Feature.Long -> LongEventField(declaration.name) with this.value
+ is Feature.Nullable<*> -> toEventPair()
+ is Feature.Version -> VersionField(declaration.name) with this.value.toString()
+ }
+}
+
+private class FeatureSet(featuresDeclarations: Set>) : ObjectDescription() {
+ init {
+ for (featureDeclaration in featuresDeclarations) {
+ field(featureDeclaration.toEventField())
+ }
+ }
+
+ fun toObjectEventData(features: Set) = ObjectEventData(features.mapNotNull { it.toEventPair() })
+}
+
+private fun Set.toObjectEventData() = FeatureSet(this.map { it.declaration }.toSet()).toObjectEventData(this)
+
+private data class TierDescriptionFields(
+ val used: FeatureSet,
+ val notUsed: FeatureSet,
+) : ObjectDescription() {
+ private val fieldUsed = ObjectEventField("used", used)
+ private val fieldNotUsed = ObjectEventField("not_used", notUsed)
+ private val fieldAmountUsedNonDeclaredFeatures = IntEventField("n_used_non_declared")
+ private val fieldAmountNotUsedNonDeclaredFeatures = IntEventField("n_not_used_non_declared")
+
+ init {
+ field(fieldUsed)
+ field(fieldNotUsed)
+ field(fieldAmountUsedNonDeclaredFeatures)
+ field(fieldAmountNotUsedNonDeclaredFeatures)
+ }
+
+ fun buildEventPairs(descriptionPartition: DescriptionPartition): List> {
+ val result = mutableListOf>(
+ fieldUsed with descriptionPartition.declared.used.toObjectEventData(),
+ fieldNotUsed with descriptionPartition.declared.notUsed.toObjectEventData(),
+ )
+ descriptionPartition.nonDeclared.used.let {
+ if (it.isNotEmpty()) result += fieldAmountUsedNonDeclaredFeatures with it.size
+ }
+ descriptionPartition.nonDeclared.notUsed.let {
+ if (it.isNotEmpty()) result += fieldAmountUsedNonDeclaredFeatures with it.size
+ }
+ return result
+ }
+
+ fun buildObjectEventData(descriptionPartition: DescriptionPartition) = ObjectEventData(
+ buildEventPairs(descriptionPartition)
+ )
+}
+
+private data class AdditionalTierFields(val description: TierDescriptionFields) : ObjectDescription() {
+ private val fieldInstanceId = IntEventField("id")
+ private val fieldDescription = ObjectEventField("description", description)
+
+ constructor(descriptionFeatures: Set>)
+ : this(TierDescriptionFields(used = FeatureSet(descriptionFeatures),
+ notUsed = FeatureSet(descriptionFeatures)))
+
+ init {
+ field(fieldInstanceId)
+ field(fieldDescription)
+ }
+
+ fun buildObjectEventData(tierInstance: TierInstance<*>,
+ descriptionPartition: DescriptionPartition) = ObjectEventData(
+ fieldInstanceId with tierInstance.instance.hashCode(),
+ fieldDescription with this.description.buildObjectEventData(descriptionPartition),
+ )
+}
+
+private data class MainTierFields(
+ val description: TierDescriptionFields,
+ val analysis: FeatureSet,
+) : ObjectDescription() {
+ private val fieldInstanceId = IntEventField("id")
+ private val fieldDescription = ObjectEventField("description", description)
+ private val fieldAnalysis = ObjectEventField("analysis", analysis)
+
+ constructor(descriptionFeatures: Set>, analysisFeatures: Set>)
+ : this(TierDescriptionFields(used = FeatureSet(descriptionFeatures),
+ notUsed = FeatureSet(descriptionFeatures)),
+ FeatureSet(analysisFeatures))
+
+ init {
+ field(fieldInstanceId)
+ field(fieldDescription)
+ field(fieldAnalysis)
+ }
+
+ fun buildObjectEventData(tierInstance: TierInstance<*>,
+ descriptionPartition: DescriptionPartition,
+ analysis: Set) = ObjectEventData(
+ fieldInstanceId with tierInstance.instance.hashCode(),
+ fieldDescription with this.description.buildObjectEventData(descriptionPartition),
+ fieldAnalysis with this.analysis.toObjectEventData(analysis)
+ )
+}
+
+private data class SessionAnalysisFields(
+ val featuresPerKey: Map>>
+) : SessionFields() {
+ val fieldsPerKey: Map = featuresPerKey.entries.associate { (key, keyFeatures) ->
+ key to ObjectEventField(key, FeatureSet(keyFeatures))
+ }
+
+ init {
+ fieldsPerKey.values.forEach { field(it) }
+ }
+
+ override fun buildEventPairs(sessionStructure: AnalysedSessionTree): List> {
+ require(sessionStructure is SessionTree.RootContainer)
+ return sessionStructure.root.entries.map { (key, keyFeatures) ->
+ val keyFeaturesDeclaration = requireNotNull(featuresPerKey[key]) {
+ "Key $key was not declared as session features key, declared keys: ${featuresPerKey.keys}"
+ }
+ val objectEventField = fieldsPerKey.getValue(key)
+ val keyFeatureSet = FeatureSet(keyFeaturesDeclaration)
+ objectEventField with keyFeatureSet.toObjectEventData(keyFeatures)
+ }
+ }
+}
+
+private class MainTierSet(mainTierScheme: PerTier) : SessionFields() {
+ val tiersDeclarations: PerTier = mainTierScheme.entries.associate { (tier, tierScheme) ->
+ tier to MainTierFields(tierScheme.description, tierScheme.analysis)
+ }
+ val fieldPerTier: PerTier = tiersDeclarations.entries.associate { (tier, tierFields) ->
+ tier to ObjectEventField(tier.name, tierFields)
+ }
+
+ init {
+ fieldPerTier.values.forEach { field(it) }
+ }
+
+ override fun buildEventPairs(sessionStructure: AnalysedSessionTree): List> {
+ val level = sessionStructure.level.main
+ return level.entries.map { (tierInstance, data) ->
+ val tierField = requireNotNull(fieldPerTier[tierInstance.tier]) {
+ "Tier ${tierInstance.tier} is now allowed here: only ${fieldPerTier.keys} are registered"
+ }
+ val tierDeclaration = tiersDeclarations.getValue(tierInstance.tier)
+ tierField with tierDeclaration.buildObjectEventData(tierInstance, data.description, data.analysis)
+ }
+ }
+}
+
+private class AdditionalTierSet(additionalTierScheme: PerTier) : SessionFields() {
+ val tiersDeclarations: PerTier = additionalTierScheme.entries.associate { (tier, tierScheme) ->
+ tier to AdditionalTierFields(tierScheme.description)
+ }
+ val fieldPerTier: PerTier = tiersDeclarations.entries.associate { (tier, tierFields) ->
+ tier to ObjectEventField(tier.name, tierFields)
+ }
+
+ init {
+ fieldPerTier.values.forEach { field(it) }
+ }
+
+ override fun buildEventPairs(sessionStructure: AnalysedSessionTree): List> {
+ val level = sessionStructure.level.additional
+ return level.entries.map { (tierInstance, data) ->
+ val tierField = requireNotNull(fieldPerTier[tierInstance.tier]) {
+ "Tier ${tierInstance.tier} is now allowed here: only ${fieldPerTier.keys} are registered"
+ }
+ val tierDeclaration = tiersDeclarations.getValue(tierInstance.tier)
+ tierField with tierDeclaration.buildObjectEventData(tierInstance, data.description)
+ }
+ }
+}
+
+private data class PredictionSessionFields(
+ val declarationMainTierSet: MainTierSet
,
+ val declarationAdditionalTierSet: AdditionalTierSet
,
+ val predictionValidationRule: List,
+ val predictionTransform: (P?) -> String,
+ val sessionAnalysisFields: SessionAnalysisFields?
+) : SessionFields
() {
+ private val fieldMainInstances = ObjectEventField("main", declarationMainTierSet)
+ private val fieldAdditionalInstances = ObjectEventField("additional", declarationAdditionalTierSet)
+ private val fieldPrediction = PredictionField("prediction", predictionValidationRule, predictionTransform)
+ private val fieldSessionAnalysis = sessionAnalysisFields?.let { ObjectEventField("session", sessionAnalysisFields) }
+
+ constructor(levelScheme: LevelScheme,
+ predictionValidationRule: List