Skip to content

Commit

Permalink
Created a separate ExplorationRetriever, hooked up
Browse files Browse the repository at this point in the history
AnswerClassificationController, and attempted to make
ExplorationProgressController thread-safe.

The thread-safety led to significant interface changes in the progress
controller, and led to discovering some issues with the mediator live data
approach to interop coroutines and LiveData. This locking mechanism will
need to change since the optimal solution requires resolving #90.
  • Loading branch information
BenHenning committed Oct 1, 2019
1 parent 1ea9d01 commit 41141b6
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 248 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,59 @@ package org.oppia.domain.classify
import org.oppia.app.model.Interaction
import org.oppia.app.model.InteractionObject
import org.oppia.app.model.Outcome
import org.oppia.app.model.State
import javax.inject.Inject

// TODO(#59): Restrict the visibility of this class to only other controllers.
/**
* Controller responsible for classifying user answers to a specific outcome based on Oppia's interaction rule engine.
* This controller is not meant to be interacted with directly by the UI. Instead, UIs wanting to submit answers should
* do so via various progress controllers, like [StoryProgressController].
* do so via various progress controllers, like [org.oppia.domain.topic.StoryProgressController].
*
* This controller should only be interacted with via background threads.
*/
class AnswerClassificationController @Inject constructor() {
// TODO(#114): Add support for classifying answers based on an actual exploration. Also, classify() should take an
// Interaction, not a State.

/**
* Classifies the specified answer in the context of the specified [Interaction] and returns the [Outcome] that best
* matches the learner's answer.
*/
internal fun classify(interaction: Interaction, answer: InteractionObject): Outcome {
// Assume only the default outcome is returned currently since this stubbed implementation is not actually used by
// downstream stubbed progress controllers.
return interaction.defaultOutcome
internal fun classify(currentState: State, answer: InteractionObject): Outcome {
return when (currentState.name) {
"Welcome!" -> simulateMultipleChoiceForWelcomeState(currentState, answer)
"What language" -> simulateTextInputForWhatLanguageState(currentState, answer)
"Numeric input" -> simulateNumericInputForNumericInputState(currentState, answer)
"Things you can do" -> currentState.interaction.defaultOutcome
else -> throw Exception("Cannot submit answer to unexpected state: ${currentState.name}.")
}
}

private fun simulateMultipleChoiceForWelcomeState(currentState: State, answer: InteractionObject): Outcome {
return when {
answer.objectTypeCase != InteractionObject.ObjectTypeCase.NON_NEGATIVE_INT ->
throw Exception("Expected int answer, not $answer.")
answer.nonNegativeInt == 0 -> currentState.interaction.answerGroupsList.first().outcome
else -> currentState.interaction.defaultOutcome
}
}

private fun simulateTextInputForWhatLanguageState(currentState: State, answer: InteractionObject): Outcome {
return when {
answer.objectTypeCase != InteractionObject.ObjectTypeCase.NORMALIZED_STRING ->
throw Exception("Expected string answer, not $answer.")
answer.normalizedString.toLowerCase() == "finnish" -> currentState.interaction.getAnswerGroups(7).outcome
else -> currentState.interaction.defaultOutcome
}
}

private fun simulateNumericInputForNumericInputState(currentState: State, answer: InteractionObject): Outcome {
return when {
answer.objectTypeCase != InteractionObject.ObjectTypeCase.NON_NEGATIVE_INT ->
throw Exception("Expected int answer, not $answer.")
answer.nonNegativeInt == 121 -> currentState.interaction.answerGroupsList.first().outcome
else -> currentState.interaction.defaultOutcome
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import org.oppia.util.data.DataProviders
import javax.inject.Inject

private const val EXPLORATION_DATA_PROVIDER_ID = "ExplorationDataProvider"
private const val START_PLAYING_EXPLORATION_RESULT_DATA_PROVIDER_ID = "StartPlayingExplorationResultDataProvider"
private const val STOP_PLAYING_EXPLORATION_RESULT_DATA_PROVIDER_ID = "StopPlayingExplorationResultDataProvider"

/**
* Controller for loading explorations by ID, or beginning to play an exploration.
Expand Down Expand Up @@ -34,17 +36,26 @@ class ExplorationDataController @Inject constructor(
*
* This must be called only if no active exploration is being played. The previous exploration must have first been
* stopped using [stopPlayingExploration] otherwise an exception will be thrown.
*
* @return a one-time [LiveData] to observe whether initiating the play request succeeded. The exploration may still
* fail to load, but this provides early-failure detection.
*/
fun startPlayingExploration(explorationId: String) {
explorationProgressController.beginExploration(explorationId)
fun startPlayingExploration(explorationId: String): LiveData<AsyncResult<Any?>> {
val operation = explorationProgressController.beginExplorationAsync(explorationId)
val dataProvider = dataProviders.createDeferredDataProviderAsync(
START_PLAYING_EXPLORATION_RESULT_DATA_PROVIDER_ID, operation)
return dataProviders.convertToLiveData(dataProvider)
}

/**
* Finishes the most recent exploration started by [startPlayingExploration]. This method should only be called if an
* active exploration is being played, otherwise an exception will be thrown.
*/
fun stopPlayingExploration() {
explorationProgressController.finishExploration()
fun stopPlayingExploration(): LiveData<AsyncResult<Any?>> {
val operation = explorationProgressController.finishExplorationAsync()
val dataProvider = dataProviders.createDeferredDataProviderAsync(
STOP_PLAYING_EXPLORATION_RESULT_DATA_PROVIDER_ID, operation)
return dataProviders.convertToLiveData(dataProvider)
}

@Suppress("RedundantSuspendModifier") // DataProviders expects this function to be a suspend function.
Expand Down
Loading

0 comments on commit 41141b6

Please sign in to comment.