Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #114: Implement answer classification controller #211

Merged
merged 21 commits into from
Oct 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e19af2b
Introduce first pass interface for ExplorationProgressController.
BenHenning Sep 25, 2019
43766b0
Fill in the stubbed logic for ExplorationProgressController. Still no
BenHenning Sep 26, 2019
c8eaa66
Fix lateinit issue in ExplorationProgressController due to wrongly or…
BenHenning Sep 27, 2019
a0eb3ce
Fix a variaty of issues in the exp progress controller, properly hook…
BenHenning Sep 29, 2019
1ea9d01
Merge branch 'develop' into introduce-exploration-progress-controller
BenHenning Sep 29, 2019
41141b6
Created a separate ExplorationRetriever, hooked up
BenHenning Oct 1, 2019
ccbac0e
Merge branch 'develop' into introduce-exploration-progress-controller
BenHenning Oct 1, 2019
5f326fc
Change the locking mechanism for ExplorationProgressController to work
BenHenning Oct 3, 2019
b5b7485
Finish tests for ExplorationProgressController and add test classific…
BenHenning Oct 4, 2019
a3c4667
Merge branch 'develop' into introduce-exploration-progress-controller
BenHenning Oct 4, 2019
e2dfd5a
Merge branch 'introduce-exploration-progress-controller' into impleme…
BenHenning Oct 6, 2019
340b22b
First iteration at implementing real answer classification for the Oppia
BenHenning Oct 6, 2019
f0bfc44
Bind all text input rules, add numeric input rules, and add support for
BenHenning Oct 6, 2019
d5c313e
Add numbers with units classification support.
BenHenning Oct 7, 2019
54ef1c2
Add multiple choice input classification support. Fix some of the
BenHenning Oct 7, 2019
e8eae1b
Add item selection classification support. Clean up all rule classifier
BenHenning Oct 7, 2019
ffb324c
Add fractions input classification support.
BenHenning Oct 7, 2019
24935cc
Add Continue module. Resolve some TODOs, add TODOs to test classifiers,
BenHenning Oct 7, 2019
2d7a58a
Add thorough tests for AnswerClassificationController.
BenHenning Oct 7, 2019
4bac447
Consolidate generic rule classifiers into a single class.
BenHenning Oct 7, 2019
2e95ce7
Merge branch 'develop' into implement-answer-classification-controller
BenHenning Oct 7, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@ import dagger.BindsInstance
import dagger.Component
import org.oppia.app.activity.ActivityComponent
import org.oppia.data.backends.gae.NetworkModule
import org.oppia.domain.classify.InteractionsModule
import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.util.logging.LoggerModule
import org.oppia.util.threading.DispatcherModule
import javax.inject.Provider
import javax.inject.Singleton

/** Root Dagger component for the application. All application-scoped modules should be included in this component. */
@Singleton
@Component(modules = [ApplicationModule::class, DispatcherModule::class, NetworkModule::class, LoggerModule::class])
@Component(modules = [
ApplicationModule::class, DispatcherModule::class, NetworkModule::class, LoggerModule::class,
ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, InteractionsModule::class
])
interface ApplicationComponent {
@Component.Builder
interface Builder {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package org.oppia.domain.classify

import org.oppia.app.model.AnswerGroup
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 java.lang.IllegalStateException
import javax.inject.Inject

// TODO(#59): Restrict the visibility of this class to only other controllers.
Expand All @@ -14,66 +15,46 @@ import javax.inject.Inject
*
* 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.

class AnswerClassificationController @Inject constructor(
private val interactionClassifiers: Map<String, @JvmSuppressWildcards InteractionClassifier>
) {
/**
* 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(currentState: State, answer: InteractionObject): Outcome {
return when (currentState.name) {
// Exp 5
"Welcome!" -> simulateMultipleChoiceForWelcomeStateExp5(currentState, answer)
"What language" -> simulateTextInputForWhatLanguageStateExp5(currentState, answer)
"Numeric input" -> simulateNumericInputForNumericInputStateExp5(currentState, answer)
"Things you can do" -> currentState.interaction.defaultOutcome
// Exp 6
"First State" -> currentState.interaction.defaultOutcome
"So what can I tell you" -> simulateMultipleChoiceForWelcomeStateExp6(currentState, answer)
"Example1" -> currentState.interaction.defaultOutcome // TextInput with ignored answer.
"Example3" -> currentState.interaction.defaultOutcome
"End Card" -> currentState.interaction.defaultOutcome
else -> throw Exception("Cannot submit answer to unexpected state: ${currentState.name}.")
internal fun classify(interaction: Interaction, answer: InteractionObject): Outcome {
val interactionClassifier = checkNotNull(interactionClassifiers[interaction.id]) {
"Encountered unknown interaction type: ${interaction.id}, expected one of: ${interactionClassifiers.keys}"
}
// TODO(#207): Add support for additional classification types.
return classifyAnswer(
answer, interaction.answerGroupsList, interaction.defaultOutcome, interactionClassifier, interaction.id)
}

private fun simulateMultipleChoiceForWelcomeStateExp5(currentState: State, answer: InteractionObject): Outcome {
return when {
answer.objectTypeCase != InteractionObject.ObjectTypeCase.NON_NEGATIVE_INT ->
throw Exception("Expected non-negative int answer, not $answer.")
answer.nonNegativeInt == 0 -> currentState.interaction.answerGroupsList[0].outcome
answer.nonNegativeInt == 2 -> currentState.interaction.answerGroupsList[1].outcome
else -> currentState.interaction.defaultOutcome
// Based on the Oppia web version:
// https://github.com/oppia/oppia/blob/edb62f/core/templates/dev/head/pages/exploration-player-page/services/answer-classification.service.ts#L57.
private fun classifyAnswer(
answer: InteractionObject, answerGroups: List<AnswerGroup>, defaultOutcome: Outcome,
interactionClassifier: InteractionClassifier, interactionId: String
): Outcome {
for (answerGroup in answerGroups) {
for (ruleSpec in answerGroup.ruleSpecsList) {
val ruleClassifier = checkNotNull(interactionClassifier.getRuleClassifier(ruleSpec.ruleType)) {
"Expected interaction $interactionId to have classifier for rule type: ${ruleSpec.ruleType}, but only" +
" has: ${interactionClassifier.getRuleTypes()}"
}
try {
if (ruleClassifier.matches(answer, ruleSpec.inputMap)) {
// Explicit classification matched.
return answerGroup.outcome
}
} catch (e: Exception) {
throw IllegalStateException("Failed when classifying answer $answer for interaction $interactionId", e)
}
}
}
}

private fun simulateTextInputForWhatLanguageStateExp5(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(6).outcome
else -> currentState.interaction.defaultOutcome
}
}

private fun simulateNumericInputForNumericInputStateExp5(currentState: State, answer: InteractionObject): Outcome {
return when {
answer.objectTypeCase != InteractionObject.ObjectTypeCase.SIGNED_INT ->
throw Exception("Expected signed int answer, not $answer.")
answer.signedInt == 121 -> currentState.interaction.answerGroupsList.first().outcome
else -> currentState.interaction.defaultOutcome
}
}

private fun simulateMultipleChoiceForWelcomeStateExp6(currentState: State, answer: InteractionObject): Outcome {
return when {
answer.objectTypeCase != InteractionObject.ObjectTypeCase.NON_NEGATIVE_INT ->
throw Exception("Expected non-negative int answer, not $answer.")
answer.nonNegativeInt == 3 -> currentState.interaction.answerGroupsList[1].outcome
answer.nonNegativeInt == 0 -> currentState.interaction.answerGroupsList[2].outcome
else -> currentState.interaction.defaultOutcome
}
// Default outcome classification.
return defaultOutcome
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.oppia.domain.classify

/** A general-purpose [InteractionClassifier] that utilizes a Dagger-bound [RuleClassifier] map. */
internal class GenericInteractionClassifier(
private val ruleClassifiers: Map<String, RuleClassifier>
): InteractionClassifier {
override fun getRuleTypes(): Set<String> {
return ruleClassifiers.keys
}

override fun getRuleClassifier(ruleType: String): RuleClassifier? {
return ruleClassifiers[ruleType]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.oppia.domain.classify

/**
* An answer classifier for a specific interaction type. Instances of this classifier should be bound to a map of
* interaction IDs to classifier instances so that they can be used by the [AnswerClassificationController].
*/
interface InteractionClassifier {
/** Returns a set of rule types that this interaction supports classifying. */
fun getRuleTypes(): Set<String>

/** Returns the [RuleClassifier] corresponding to the specified rule type. */
fun getRuleClassifier(ruleType: String): RuleClassifier?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.oppia.domain.classify

import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import dagger.multibindings.StringKey
import org.oppia.domain.classify.rules.ContinueRules
import org.oppia.domain.classify.rules.FractionInputRules
import org.oppia.domain.classify.rules.ItemSelectionInputRules
import org.oppia.domain.classify.rules.MultipleChoiceInputRules
import org.oppia.domain.classify.rules.NumberWithUnitsRules
import org.oppia.domain.classify.rules.NumericInputRules
import org.oppia.domain.classify.rules.TextInputRules

/** Module that provides a map of [InteractionClassifier]s. */
@Module
class InteractionsModule {
@Provides
@IntoMap
@StringKey("Continue")
fun provideContinueInteractionClassifier(
@ContinueRules ruleClassifiers: Map<String, @JvmSuppressWildcards RuleClassifier>
): InteractionClassifier {
return GenericInteractionClassifier(ruleClassifiers)
}

@Provides
@IntoMap
@StringKey("FractionInput")
fun provideFractionInputInteractionClassifier(
@FractionInputRules ruleClassifiers: Map<String, @JvmSuppressWildcards RuleClassifier>
): InteractionClassifier {
return GenericInteractionClassifier(ruleClassifiers)
}

@Provides
@IntoMap
@StringKey("ItemSelectionInput")
fun provideItemSelectionInputInteractionClassifier(
@ItemSelectionInputRules ruleClassifiers: Map<String, @JvmSuppressWildcards RuleClassifier>
): InteractionClassifier {
return GenericInteractionClassifier(ruleClassifiers)
}

@Provides
@IntoMap
@StringKey("MultipleChoiceInput")
fun provideMultipleChoiceInputInteractionClassifier(
@MultipleChoiceInputRules ruleClassifiers: Map<String, @JvmSuppressWildcards RuleClassifier>
): InteractionClassifier {
return GenericInteractionClassifier(ruleClassifiers)
}

@Provides
@IntoMap
@StringKey("NumberWithUnits")
fun provideNumberWithUnitsInteractionClassifier(
@NumberWithUnitsRules ruleClassifiers: Map<String, @JvmSuppressWildcards RuleClassifier>
): InteractionClassifier {
return GenericInteractionClassifier(ruleClassifiers)
}

@Provides
@IntoMap
@StringKey("NumericInput")
fun provideNumericInputInteractionClassifier(
@NumericInputRules ruleClassifiers: Map<String, @JvmSuppressWildcards RuleClassifier>
): InteractionClassifier {
return GenericInteractionClassifier(ruleClassifiers)
}

@Provides
@IntoMap
@StringKey("TextInput")
fun provideTextInputInteractionClassifier(
@TextInputRules ruleClassifiers: Map<String, @JvmSuppressWildcards RuleClassifier>
): InteractionClassifier {
return GenericInteractionClassifier(ruleClassifiers)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.oppia.domain.classify

import org.oppia.app.model.InteractionObject

/** An answer classifier for a specific interaction rule. */
interface RuleClassifier {
/**
* Returns whether the specified answer matches the rule's parameter inputs per this rule's classification strategy.
*/
fun matches(answer: InteractionObject, inputs: Map<String, InteractionObject>): Boolean
}
Loading