Skip to content

Add project deviceFormFactor setting #263

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

Merged
merged 1 commit into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 7 additions & 2 deletions arbigent-core-model/src/commonMain/kotlin/ArbigentResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public data class ArbigentAgentResults(
public data class ArbigentAgentResult(
public val goal: String,
public val maxStep: Int = 10,
public val deviceFormFactor: ArbigentScenarioDeviceFormFactor = ArbigentScenarioDeviceFormFactor.Mobile,
public val deviceFormFactor: ArbigentScenarioDeviceFormFactor = ArbigentScenarioDeviceFormFactor.Unspecified,
public val isGoalAchieved: Boolean,
public val steps: List<ArbigentAgentTaskStepResult>,
public val deviceName: String,
Expand Down Expand Up @@ -114,6 +114,11 @@ public sealed interface ArbigentScenarioDeviceFormFactor {
@SerialName("Tv")
public data object Tv : ArbigentScenarioDeviceFormFactor

@Serializable
@SerialName("Unspecified")
public data object Unspecified : ArbigentScenarioDeviceFormFactor

public fun isMobile(): Boolean = this == Mobile
public fun isTv(): Boolean = this is Tv
}
public fun isUnspecified(): Boolean = this is Unspecified
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public data class ArbigentProjectSettings(
public val mcpJson: String = DefaultMcpJson,
@YamlMultiLineStringStyle(MultiLineStringStyle.Literal)
public val appUiStructure: String = "",
public val defaultDeviceFormFactor: ArbigentScenarioDeviceFormFactor = ArbigentScenarioDeviceFormFactor.Unspecified,
) {
public companion object {
public const val DefaultMcpJson: String = "{}"
Expand Down Expand Up @@ -163,16 +164,33 @@ public fun List<ArbigentScenarioContent>.createArbigentScenario(
val dependencyScenario = first { it.id == dependency }
dfs(dependencyScenario)
}
// Determine which device form factor to use
val effectiveDeviceFormFactor = if (nodeScenario.deviceFormFactor is ArbigentScenarioDeviceFormFactor.Unspecified) {
if (projectSettings.defaultDeviceFormFactor is ArbigentScenarioDeviceFormFactor.Unspecified) {
ArbigentScenarioDeviceFormFactor.Mobile
} else {
// If the scenario is from the YAML file and doesn't specify a device form factor,
// use Mobile as the default (for backward compatibility)
if (nodeScenario.id == "default-not-using-project") {
ArbigentScenarioDeviceFormFactor.Mobile
} else {
projectSettings.defaultDeviceFormFactor
}
}
} else {
nodeScenario.deviceFormFactor
}

result.add(
ArbigentAgentTask(
scenarioId = nodeScenario.id,
goal = nodeScenario.goal,
maxStep = nodeScenario.maxStep,
deviceFormFactor = nodeScenario.deviceFormFactor,
deviceFormFactor = effectiveDeviceFormFactor,
agentConfig = AgentConfigBuilder(
prompt = projectSettings.prompt,
scenarioType = nodeScenario.type,
deviceFormFactor = nodeScenario.deviceFormFactor,
deviceFormFactor = effectiveDeviceFormFactor,
initializationMethods = nodeScenario.initializationMethods.ifEmpty { listOf(nodeScenario.initializeMethods) },
imageAssertions = ArbigentImageAssertions(
nodeScenario.imageAssertions,
Expand All @@ -195,13 +213,30 @@ public fun List<ArbigentScenarioContent>.createArbigentScenario(
}
dfs(scenario)
arbigentDebugLog("executing:$result")
// Determine which device form factor to use for the scenario
val effectiveScenarioDeviceFormFactor = if (scenario.deviceFormFactor is ArbigentScenarioDeviceFormFactor.Unspecified) {
if (projectSettings.defaultDeviceFormFactor is ArbigentScenarioDeviceFormFactor.Unspecified) {
ArbigentScenarioDeviceFormFactor.Mobile
} else {
// If the scenario is from the YAML file and doesn't specify a device form factor,
// use Mobile as the default (for backward compatibility)
if (scenario.id == "default-not-using-project") {
ArbigentScenarioDeviceFormFactor.Mobile
} else {
projectSettings.defaultDeviceFormFactor
}
}
} else {
scenario.deviceFormFactor
}

return ArbigentScenario(
id = scenario.id,
agentTasks = result,
maxRetry = scenario.maxRetry,
maxStepCount = scenario.maxStep,
tags = scenario.tags,
deviceFormFactor = scenario.deviceFormFactor,
deviceFormFactor = effectiveScenarioDeviceFormFactor,
isLeaf = this.none { it.dependencyId == scenario.id },
cacheOptions = scenario.cacheOptions
)
Expand Down Expand Up @@ -237,7 +272,7 @@ public class ArbigentScenarioContent @OptIn(ExperimentalUuidApi::class) construc
public val maxRetry: Int = 3,
public val maxStep: Int = 10,
public val tags: ArbigentContentTags = setOf(),
public val deviceFormFactor: ArbigentScenarioDeviceFormFactor = ArbigentScenarioDeviceFormFactor.Mobile,
public val deviceFormFactor: ArbigentScenarioDeviceFormFactor = ArbigentScenarioDeviceFormFactor.Unspecified,
// This is no longer used and will be removed in the future.
public val cleanupData: CleanupData = CleanupData.Noop,
public val imageAssertionHistoryCount: Int = 1,
Expand Down
72 changes: 72 additions & 0 deletions arbigent-core/src/test/java/ArbigentProjectFileContentTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,76 @@ Previous steps:
assertNotNull(scenarioBAiOptions, "Scenario B aiOptions should not be null")
assertEquals(0.5, scenarioBAiOptions.temperature!!, "Scenario B temperature should be 0.5")
}

private val projectWithDeviceFormFactor = ArbigentProjectSerializer().load(
"""
settings:
defaultDeviceFormFactor:
type: "Tv"
scenarios:
- id: "default-form-factor"
goal: "Test default form factor"
deviceFormFactor:
type: "Unspecified"
- id: "custom-form-factor"
goal: "Test custom form factor"
deviceFormFactor:
type: "Mobile"
- id: "default-not-using-project"
goal: "Test not using project default"
"""
)

@Test
fun testDefaultDeviceFormFactor() {
// Test project-level defaultDeviceFormFactor
val projectSettings = projectWithDeviceFormFactor.settings
assertEquals(
io.github.takahirom.arbigent.result.ArbigentScenarioDeviceFormFactor.Tv,
projectSettings.defaultDeviceFormFactor,
"Project defaultDeviceFormFactor should be Tv"
)

// Test scenario using project default
val scenarioUsingDefault = projectWithDeviceFormFactor.scenarioContents.createArbigentScenario(
projectSettings = projectSettings,
scenario = projectWithDeviceFormFactor.scenarioContents[0],
aiFactory = { FakeAi() },
deviceFactory = { FakeDevice() },
aiDecisionCache = AiDecisionCacheStrategy.InMemory().toCache()
)
assertEquals(
io.github.takahirom.arbigent.result.ArbigentScenarioDeviceFormFactor.Tv,
scenarioUsingDefault.deviceFormFactor,
"Scenario using project default should have Tv form factor"
)

// Test scenario with custom form factor (should override project default)
val scenarioWithCustom = projectWithDeviceFormFactor.scenarioContents.createArbigentScenario(
projectSettings = projectSettings,
scenario = projectWithDeviceFormFactor.scenarioContents[1],
aiFactory = { FakeAi() },
deviceFactory = { FakeDevice() },
aiDecisionCache = AiDecisionCacheStrategy.InMemory().toCache()
)
assertEquals(
io.github.takahirom.arbigent.result.ArbigentScenarioDeviceFormFactor.Mobile,
scenarioWithCustom.deviceFormFactor,
"Scenario with custom form factor should have Mobile form factor"
)

// Test scenario not using project default (should use Mobile as default)
val scenarioNotUsingDefault = projectWithDeviceFormFactor.scenarioContents.createArbigentScenario(
projectSettings = projectSettings,
scenario = projectWithDeviceFormFactor.scenarioContents[2],
aiFactory = { FakeAi() },
deviceFactory = { FakeDevice() },
aiDecisionCache = AiDecisionCacheStrategy.InMemory().toCache()
)
assertEquals(
io.github.takahirom.arbigent.result.ArbigentScenarioDeviceFormFactor.Mobile,
scenarioNotUsingDefault.deviceFormFactor,
"Scenario not using project default should have Mobile form factor"
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.takahirom.arbigent.ui

import io.github.takahirom.arbigent.*
import io.github.takahirom.arbigent.result.ArbigentScenarioDeviceFormFactor
import io.github.takahirom.arbigent.result.StepFeedback
import io.github.takahirom.arbigent.result.StepFeedbackEvent
import kotlinx.coroutines.*
Expand Down Expand Up @@ -74,6 +75,7 @@ class ArbigentAppStateHolder(
val aiOptionsFlow = MutableStateFlow<ArbigentAiOptions?>(null)
val mcpJsonFlow = MutableStateFlow("{}")
val appUiStructureFlow = MutableStateFlow("")
val defaultDeviceFormFactorFlow = MutableStateFlow<ArbigentScenarioDeviceFormFactor>(ArbigentScenarioDeviceFormFactor.Unspecified)
val decisionCache = cacheStrategyFlow
.map {
val decisionCacheStrategy = it.aiDecisionCacheStrategy
Expand Down Expand Up @@ -146,7 +148,9 @@ class ArbigentAppStateHolder(
prompt = promptFlow.value,
cacheStrategy = cacheStrategyFlow.value,
aiOptions = aiOptionsFlow.value,
mcpJson = mcpJsonFlow.value
mcpJson = mcpJsonFlow.value,
appUiStructure = appUiStructureFlow.value,
defaultDeviceFormFactor = defaultDeviceFormFactorFlow.value
),
initialScenarios = allScenarioStateHoldersStateFlow.value.map { scenario ->
scenario.createScenario(allScenarioStateHoldersStateFlow.value)
Expand All @@ -173,7 +177,9 @@ class ArbigentAppStateHolder(
[email protected],
[email protected],
[email protected],
[email protected]
[email protected],
[email protected],
[email protected]
),
scenario = createArbigentScenarioContent(),
aiFactory = aiFactory,
Expand Down Expand Up @@ -266,7 +272,8 @@ class ArbigentAppStateHolder(
cacheStrategyFlow.value,
aiOptionsFlow.value,
mcpJsonFlow.value,
appUiStructureFlow.value
appUiStructureFlow.value,
defaultDeviceFormFactorFlow.value
),
scenarioContents = sortedScenarios.map {
it.createArbigentScenarioContent()
Expand Down Expand Up @@ -395,6 +402,10 @@ class ArbigentAppStateHolder(
appUiStructureFlow.value = structure
}

fun onDefaultDeviceFormFactorChanged(formFactor: ArbigentScenarioDeviceFormFactor) {
defaultDeviceFormFactorFlow.value = formFactor
}

fun scenarioCountById(newScenarioId: String): Int {
return allScenarioStateHoldersStateFlow.value.count { it.id == newScenarioId }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ constructor(
val initializationMethodStateFlow: StateFlow<List<ArbigentScenarioContent.InitializationMethod>> =
_initializationMethodStateFlow
val deviceFormFactorStateFlow: MutableStateFlow<ArbigentScenarioDeviceFormFactor> =
MutableStateFlow(ArbigentScenarioDeviceFormFactor.Mobile)
MutableStateFlow(ArbigentScenarioDeviceFormFactor.Unspecified)
fun deviceFormFactor() = deviceFormFactorStateFlow.value
val scenarioTypeStateFlow: MutableStateFlow<ArbigentScenarioType> =
MutableStateFlow(ArbigentScenarioType.Scenario)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.ui.window.DialogWindow
import io.github.takahirom.arbigent.AiDecisionCacheStrategy
import io.github.takahirom.arbigent.BuildConfig
import io.github.takahirom.arbigent.UserPromptTemplate
import io.github.takahirom.arbigent.result.ArbigentScenarioDeviceFormFactor
import androidx.compose.foundation.ExperimentalFoundationApi
import org.jetbrains.jewel.ui.component.IconActionButton
import org.jetbrains.jewel.ui.component.TextArea
Expand All @@ -30,6 +31,7 @@ import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.TextField
import org.jetbrains.jewel.ui.component.Slider
import org.jetbrains.jewel.ui.component.Checkbox
import org.jetbrains.jewel.ui.component.RadioButtonRow
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.Alignment
import io.github.takahirom.arbigent.ArbigentAiOptions
Expand Down Expand Up @@ -81,6 +83,63 @@ fun ProjectSettingsDialog(appStateHolder: ArbigentAppStateHolder, onCloseRequest
decorationBoxModifier = Modifier.padding(horizontal = 8.dp),
)

GroupHeader("Default Device Form Factor")
val defaultDeviceFormFactor by appStateHolder.defaultDeviceFormFactorFlow.collectAsState()

// Display the current value
val formFactorName = when {
defaultDeviceFormFactor.isMobile() -> "Mobile"
defaultDeviceFormFactor.isTv() -> "TV"
else -> "Unspecified"
}

// Create a mutable state to track the selected option
var selectedOption by remember { mutableStateOf(formFactorName) }

Column(
modifier = Modifier.padding(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
RadioButtonRow(
text = "Mobile",
selected = selectedOption == "Mobile",
onClick = {
selectedOption = "Mobile"
// Update the defaultDeviceFormFactor using the provided method
appStateHolder.onDefaultDeviceFormFactorChanged(ArbigentScenarioDeviceFormFactor.Mobile)
}
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
RadioButtonRow(
text = "TV",
selected = selectedOption == "TV",
onClick = {
selectedOption = "TV"
// Update the defaultDeviceFormFactor using the provided method
appStateHolder.onDefaultDeviceFormFactorChanged(ArbigentScenarioDeviceFormFactor.Tv)
}
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
RadioButtonRow(
text = "Unspecified",
selected = selectedOption == "Unspecified",
onClick = {
selectedOption = "Unspecified"
// Update the defaultDeviceFormFactor using the provided method
appStateHolder.onDefaultDeviceFormFactorChanged(ArbigentScenarioDeviceFormFactor.Unspecified)
}
)
}
}

GroupHeader("AI Options")
val aiOptions by appStateHolder.aiOptionsFlow.collectAsState()
val currentOptions = aiOptions ?: ArbigentAiOptions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,18 @@ private fun ScenarioFundamentalOptions(
}
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
RadioButtonRow(
text = "Unspecified",
selected = inputActionType.isUnspecified(),
onClick = {
updatedScenarioStateHolder.deviceFormFactorStateFlow.value =
ArbigentScenarioDeviceFormFactor.Unspecified
}
)
}
}
// Max retry and step count
Column(
Expand Down
Loading