Skip to content

Commit 0ea49e4

Browse files
Refactor: Update Settings screen to select a LLM Clients. Added Gemini client.Improve architecture in Demo Compose App
1 parent 45e4acb commit 0ea49e4

File tree

33 files changed

+461
-269
lines changed

33 files changed

+461
-269
lines changed

.github/workflows/demo-compose-app.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
- name: Configure Git
3737
run: |
3838
git config --global core.autocrlf input
39-
- uses: actions/checkout@v5
39+
- uses: actions/checkout@v6
4040
- name: Set up JDK ${{ env.JAVA_VERSION }}
4141
uses: actions/setup-java@v5
4242
with:
@@ -61,5 +61,3 @@ jobs:
6161
- name: Assemble target
6262
working-directory: ./examples/demo-compose-app
6363
run: ./gradlew ${{ matrix.gradle-tasks }} --stacktrace
64-
env:
65-
JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g"

examples/demo-compose-app/.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ indent_size = 2
1616

1717
[*.{kt,kts}]
1818
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
19+
ktlint_function_naming_ignore_when_annotated_with = Composable
1920

2021
# Disable wildcard imports entirely
2122
ij_kotlin_name_count_to_use_star_import = 2147483647

examples/demo-compose-app/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,15 @@ To build the application bundle:
3030
- find `.apk` file in `androidApp/build/outputs/apk/debug/app-debug.apk`
3131

3232
### Desktop
33-
Run the desktop application: `./gradlew :desktopApp:run`
34-
Run the desktop **hot reload** application: `./gradlew :desktopApp:hotRun`
33+
Run the desktop application: `./gradlew :desktopApp:run`
3534

3635
### iOS
3736
To run the application on iPhone device/simulator:
3837
- Open `iosApp/iosApp.xcproject` in Xcode and run standard configuration
3938
- Or use [Kotlin Multiplatform Mobile plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile) for Android Studio
4039

4140
### Web Distribution
42-
Build web pack: `./gradlew :webApp:jsBrowserDevelopmentWebpack`
41+
Build web pack: `./gradlew :webApp:jsBrowserDevelopmentWebpack`
4342
Deploy a dir `webApp/build/dist/composeWebCompatibility/productionExecutable` to a web server
4443

4544
### JS Browser

examples/demo-compose-app/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ plugins {
22
alias(libs.plugins.android.application).apply(false)
33
alias(libs.plugins.android.kmp.library).apply(false)
44
alias(libs.plugins.compose.compiler).apply(false)
5-
alias(libs.plugins.compose.hot.reload).apply(false)
65
alias(libs.plugins.compose.multiplatform).apply(false)
76
alias(libs.plugins.kotlin.android).apply(false)
87
alias(libs.plugins.kotlin.jvm).apply(false)
98
alias(libs.plugins.kotlin.multiplatform).apply(false)
109
alias(libs.plugins.ktlint)
1110
}
11+
12+
subprojects {
13+
apply(plugin = "org.jlleitschuh.gradle.ktlint")
14+
}

examples/demo-compose-app/commonApp/build.gradle.kts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
@file:OptIn(ExperimentalWasmDsl::class)
2-
31
import org.jetbrains.compose.reload.gradle.ComposeHotRun
4-
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
52
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
63

74
plugins {
@@ -15,11 +12,10 @@ plugins {
1512
kotlin {
1613
jvmToolchain(libs.versions.javaVersion.get().toInt())
1714

18-
android {
15+
androidLibrary {
1916
namespace = "com.jetbrains.example.koog.share.ui"
2017
compileSdk = 36
2118
minSdk = 23
22-
androidResources.enable = true
2319
}
2420

2521
jvm()
@@ -32,33 +28,38 @@ kotlin {
3228

3329
sourceSets {
3430
commonMain.dependencies {
35-
implementation(compose.animation)
36-
implementation(compose.animationGraphics)
37-
implementation(compose.components.resources)
38-
implementation(compose.components.uiToolingPreview)
39-
implementation(compose.foundation)
40-
implementation(compose.material3)
41-
implementation(compose.materialIconsExtended)
42-
implementation(compose.runtime)
43-
implementation(compose.ui)
44-
implementation(compose.uiUtil)
45-
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
46-
implementation(libs.jetbrains.navigation.compose)
31+
implementation(libs.jetbrains.compose.animation)
32+
implementation(libs.jetbrains.compose.animation.graphics)
33+
implementation(libs.jetbrains.compose.components.resources)
34+
implementation(libs.jetbrains.compose.foundation)
35+
implementation(libs.jetbrains.compose.material.icons.extended)
36+
implementation(libs.jetbrains.compose.material3)
37+
implementation(libs.jetbrains.compose.runtime)
38+
implementation(libs.jetbrains.compose.ui)
39+
implementation(libs.jetbrains.compose.ui.tooling.preview)
40+
implementation(libs.jetbrains.compose.ui.util)
41+
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
42+
implementation(libs.jetbrains.navigation3.ui)
4743
implementation(libs.koin.compose)
4844
implementation(libs.koog.agents.core)
4945
implementation(libs.koog.prompt.executor.llms.all)
5046
implementation(libs.kotlinx.coroutines.core)
5147
implementation(libs.kotlinx.datetime)
52-
implementation(libs.markdown.renderer)
5348
implementation(libs.kotlinx.serialization.core)
5449
implementation(libs.kotlinx.serialization.json)
50+
implementation(libs.markdown.renderer)
5551
implementation(project.dependencies.platform(libs.koin.bom))
5652
implementation(project.dependencies.platform(libs.ktor.bom))
5753
}
5854

55+
commonTest.dependencies {
56+
implementation(kotlin("test"))
57+
implementation(libs.jetbrains.compose.ui.test)
58+
}
59+
5960
androidMain.dependencies {
60-
implementation(compose.uiTooling)
6161
implementation(libs.androidx.datastore.preferences)
62+
implementation(libs.jetbrains.compose.ui.tooling)
6263
implementation(libs.kotlinx.coroutines.android)
6364
implementation(libs.ktor.client.okhttp)
6465
}

examples/demo-compose-app/commonApp/src/androidMain/kotlin/com/jetbrains/example/koog/compose/settings/DataStoreAppSettings.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ internal class DataStoreAppSettings(prefPathProvider: PrefPathProvider) : AppSet
1414
companion object {
1515
val OPENAI_TOKEN_KEY = stringPreferencesKey("openai_token")
1616
val ANTHROPIC_TOKEN_KEY = stringPreferencesKey("anthropic_token")
17+
val GEMINI_TOKEN_KEY = stringPreferencesKey("gemini_token")
18+
val SELECTED_PROVIDER_KEY = stringPreferencesKey("selected_provider")
1719
}
1820

1921
private val dataStore: DataStore<Preferences> by lazy {
@@ -25,14 +27,23 @@ internal class DataStoreAppSettings(prefPathProvider: PrefPathProvider) : AppSet
2527
override suspend fun getCurrentSettings(): AppSettingsData = dataStore.data.map { preferences ->
2628
AppSettingsData(
2729
openAiToken = preferences[OPENAI_TOKEN_KEY].orEmpty(),
28-
anthropicToken = preferences[ANTHROPIC_TOKEN_KEY].orEmpty()
30+
anthropicToken = preferences[ANTHROPIC_TOKEN_KEY].orEmpty(),
31+
geminiToken = preferences[GEMINI_TOKEN_KEY].orEmpty(),
32+
selectedOption = when (preferences[SELECTED_PROVIDER_KEY]) {
33+
SelectedOption.OpenAI.title -> SelectedOption.OpenAI
34+
SelectedOption.Anthropic.title -> SelectedOption.Anthropic
35+
SelectedOption.Gemini.title -> SelectedOption.Gemini
36+
else -> SelectedOption.OpenAI
37+
}
2938
)
3039
}.first()
3140

3241
override suspend fun setCurrentSettings(settings: AppSettingsData) {
3342
dataStore.edit { preferences ->
3443
preferences[OPENAI_TOKEN_KEY] = settings.openAiToken
3544
preferences[ANTHROPIC_TOKEN_KEY] = settings.anthropicToken
45+
preferences[GEMINI_TOKEN_KEY] = settings.geminiToken
46+
preferences[SELECTED_PROVIDER_KEY] = settings.selectedOption.title
3647
}
3748
}
3849
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.jetbrains.example.koog.compose
2+
3+
import androidx.navigation3.runtime.NavKey
4+
import com.jetbrains.example.koog.compose.screens.agentdemo.AgentDemoNavigationCallback
5+
import com.jetbrains.example.koog.compose.screens.settings.SettingsNavigationCallback
6+
import com.jetbrains.example.koog.compose.screens.start.StartNavigationCallback
7+
8+
internal class AppNavigation(private val backStack: MutableList<NavKey>) :
9+
AgentDemoNavigationCallback,
10+
SettingsNavigationCallback,
11+
StartNavigationCallback {
12+
13+
override fun goBack() {
14+
backStack.removeLastOrNull()
15+
}
16+
17+
override fun goSettings() {
18+
backStack.add(NavRoute.SettingsScreen)
19+
}
20+
21+
override fun goAgentDemo(agentDemoRoute: NavRoute.AgentDemoRoute) {
22+
backStack.add(agentDemoRoute)
23+
}
24+
}

examples/demo-compose-app/commonApp/src/commonMain/kotlin/com/jetbrains/example/koog/compose/ComposeApp.kt

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import androidx.compose.foundation.layout.fillMaxSize
44
import androidx.compose.material3.MaterialTheme
55
import androidx.compose.material3.Surface
66
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.mutableStateListOf
78
import androidx.compose.ui.Modifier
8-
import androidx.navigation.compose.NavHost
9-
import androidx.navigation.compose.composable
10-
import androidx.navigation.compose.rememberNavController
9+
import androidx.navigation3.runtime.NavKey
10+
import androidx.navigation3.runtime.entryProvider
11+
import androidx.navigation3.ui.NavDisplay
1112
import com.jetbrains.example.koog.compose.screens.agentdemo.AgentDemoScreen
1213
import com.jetbrains.example.koog.compose.screens.settings.SettingsScreen
1314
import com.jetbrains.example.koog.compose.screens.start.StartScreen
@@ -27,57 +28,55 @@ fun ComposeApp() = AppTheme {
2728
color = MaterialTheme.colorScheme.background
2829
) {
2930
val koin = getKoin()
30-
val navController = rememberNavController()
31-
NavHost(
32-
navController = navController,
33-
startDestination = NavRoute.StartScreen,
34-
) {
35-
composable<NavRoute.StartScreen> {
36-
StartScreen(
37-
onNavigateToSettings = {
38-
navController.navigate(NavRoute.SettingsScreen)
39-
},
40-
onNavigateToAgentDemo = { demoRoute ->
41-
navController.navigate(demoRoute)
42-
},
43-
viewModel = koin.get()
44-
)
45-
}
31+
val backStack = mutableStateListOf<NavKey>(NavRoute.StartScreen)
32+
val appNavigation = AppNavigation(backStack = backStack)
33+
NavDisplay(
34+
backStack = backStack,
35+
onBack = { backStack.removeLastOrNull() },
36+
entryProvider = entryProvider {
37+
entry<NavRoute.StartScreen> {
38+
StartScreen(
39+
viewModel = koin.get { parametersOf(appNavigation) }
40+
)
41+
}
4642

47-
composable<NavRoute.SettingsScreen> {
48-
SettingsScreen(
49-
onNavigateBack = {
50-
navController.popBackStack()
51-
},
52-
onSaveSettings = {
53-
navController.popBackStack()
54-
},
55-
viewModel = koin.get()
56-
)
57-
}
43+
entry<NavRoute.SettingsScreen> {
44+
SettingsScreen(
45+
viewModel = koin.get { parametersOf(appNavigation) }
46+
)
47+
}
5848

59-
composable<NavRoute.AgentDemoRoute.CalculatorScreen> {
60-
AgentDemoScreen(
61-
onNavigateBack = { navController.popBackStack() },
62-
viewModel = koin.get { parametersOf("calculator") }
63-
)
64-
}
49+
entry<NavRoute.AgentDemoRoute.CalculatorScreen> {
50+
AgentDemoScreen(
51+
viewModel = koin.get {
52+
parametersOf(
53+
appNavigation,
54+
"calculator",
55+
)
56+
}
57+
)
58+
}
6559

66-
composable<NavRoute.AgentDemoRoute.WeatherScreen> {
67-
AgentDemoScreen(
68-
onNavigateBack = { navController.popBackStack() },
69-
viewModel = koin.get { parametersOf("weather") }
70-
)
60+
entry<NavRoute.AgentDemoRoute.WeatherScreen> {
61+
AgentDemoScreen(
62+
viewModel = koin.get {
63+
parametersOf(
64+
appNavigation,
65+
"weather",
66+
)
67+
}
68+
)
69+
}
7170
}
72-
}
71+
)
7372
}
7473
}
7574

7675
/**
7776
* Navigation routes for the app
7877
*/
7978
@Serializable
80-
sealed interface NavRoute {
79+
sealed interface NavRoute : NavKey {
8180
@Serializable
8281
data object StartScreen : NavRoute
8382

examples/demo-compose-app/commonApp/src/commonMain/kotlin/com/jetbrains/example/koog/compose/KoinApp.kt

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package com.jetbrains.example.koog.compose
22

33
import ai.koog.prompt.executor.clients.LLMClient
4+
import ai.koog.prompt.executor.clients.anthropic.AnthropicLLMClient
5+
import ai.koog.prompt.executor.clients.anthropic.AnthropicModels
6+
import ai.koog.prompt.executor.clients.google.GoogleLLMClient
7+
import ai.koog.prompt.executor.clients.google.GoogleModels
48
import ai.koog.prompt.executor.clients.openai.OpenAILLMClient
59
import ai.koog.prompt.executor.clients.openai.OpenAIModels
610
import ai.koog.prompt.llm.LLModel
@@ -12,6 +16,7 @@ import com.jetbrains.example.koog.compose.screens.agentdemo.AgentDemoViewModel
1216
import com.jetbrains.example.koog.compose.screens.settings.SettingsViewModel
1317
import com.jetbrains.example.koog.compose.screens.start.StartViewModel
1418
import com.jetbrains.example.koog.compose.settings.AppSettings
19+
import com.jetbrains.example.koog.compose.settings.SelectedOption
1520
import org.koin.compose.KoinMultiplatformApplication
1621
import org.koin.core.annotation.KoinExperimentalAPI
1722
import org.koin.core.module.Module
@@ -29,19 +34,46 @@ fun KoinApp() = KoinMultiplatformApplication(
2934
factory<suspend () -> Pair<LLMClient, LLModel>> {
3035
{
3136
val appSettings: AppSettings = get()
32-
val openAiToken = appSettings.getCurrentSettings().openAiToken
33-
require(openAiToken.isNotEmpty()) { "OpenAI token is not configured." }
34-
Pair(OpenAILLMClient(openAiToken), OpenAIModels.Chat.GPT4o)
37+
val currentSettings = appSettings.getCurrentSettings()
38+
when (currentSettings.selectedOption) {
39+
SelectedOption.OpenAI -> {
40+
val openAiToken = appSettings.getCurrentSettings().openAiToken
41+
require(openAiToken.isNotEmpty()) { "OpenAI token is not configured." }
42+
Pair(OpenAILLMClient(openAiToken), OpenAIModels.Chat.GPT4o)
43+
}
44+
SelectedOption.Anthropic -> {
45+
val anthropicToken = appSettings.getCurrentSettings().anthropicToken
46+
require(anthropicToken.isNotEmpty()) { "Anthropic token is not configured." }
47+
Pair(AnthropicLLMClient(anthropicToken), AnthropicModels.Sonnet_4)
48+
}
49+
SelectedOption.Gemini -> {
50+
val geminiToken = appSettings.getCurrentSettings().geminiToken
51+
require(geminiToken.isNotEmpty()) { "Gemini token is not configured." }
52+
Pair(GoogleLLMClient(geminiToken), GoogleModels.Gemini2_5FlashLite)
53+
}
54+
}
3555
}
3656
}
3757
single<AgentProvider>(named("calculator")) { CalculatorAgentProvider(provideLLMClient = get()) }
3858
single<AgentProvider>(named("weather")) { WeatherAgentProvider(provideLLMClient = get()) }
39-
factory { SettingsViewModel(appSettings = get()) }
40-
factory { StartViewModel() }
4159
factory { params ->
42-
val agentProviderName: String = params.get()
60+
StartViewModel(
61+
navigationCallback = params[0],
62+
)
63+
}
64+
factory { params ->
65+
SettingsViewModel(
66+
navigationCallback = params[0],
67+
appSettings = get(),
68+
)
69+
}
70+
factory { params ->
71+
val agentProviderName: String = params[1]
4372
val agentProvider: AgentProvider = koin.get(named(agentProviderName))
44-
AgentDemoViewModel(agentProvider = agentProvider)
73+
AgentDemoViewModel(
74+
navigationCallback = params[0],
75+
agentProvider = agentProvider
76+
)
4577
}
4678
}
4779
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.jetbrains.example.koog.compose.screens.agentdemo
2+
3+
interface AgentDemoNavigationCallback {
4+
fun goBack()
5+
}

0 commit comments

Comments
 (0)