Skip to content

Commit 9b88303

Browse files
committed
chat: off-load model when activity is not in foreground #52
Add SmolLMManager.kt that takes responsibilities from ChatScreenViewMode.kt and manages the SmolLM instance efficiently across the app's life-cycle
1 parent d150f44 commit 9b88303

File tree

4 files changed

+268
-116
lines changed

4 files changed

+268
-116
lines changed

app/src/main/java/io/shubham0204/smollmandroid/data/ChatsDB.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@
1616

1717
package io.shubham0204.smollmandroid.data
1818

19+
import android.util.Log
1920
import io.objectbox.annotation.Entity
2021
import io.objectbox.annotation.Id
2122
import io.objectbox.kotlin.flow
22-
import io.shubham0204.smollmandroid.ui.screens.chat.LOGD
2323
import kotlinx.coroutines.Dispatchers
2424
import kotlinx.coroutines.ExperimentalCoroutinesApi
2525
import kotlinx.coroutines.flow.Flow
2626
import kotlinx.coroutines.flow.flowOn
2727
import org.koin.core.annotation.Single
2828
import java.util.Date
2929

30+
private const val LOGTAG = "[ChatDB-Kt]"
31+
private val LOGD: (String) -> Unit = { Log.d(LOGTAG, it) }
32+
3033
@Entity
3134
data class Chat(
3235
@Id var id: Long = 0,
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright (C) 2025 Shubham Panchal
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.shubham0204.smollmandroid.llm
18+
19+
import android.util.Log
20+
import io.shubham0204.smollm.SmolLM
21+
import io.shubham0204.smollmandroid.data.Chat
22+
import io.shubham0204.smollmandroid.data.MessagesDB
23+
import kotlinx.coroutines.CancellationException
24+
import kotlinx.coroutines.CoroutineScope
25+
import kotlinx.coroutines.Dispatchers
26+
import kotlinx.coroutines.Job
27+
import kotlinx.coroutines.launch
28+
import kotlinx.coroutines.withContext
29+
import org.koin.core.annotation.Single
30+
import kotlin.time.measureTime
31+
32+
private const val LOGTAG = "[SmolLMManager-Kt]"
33+
private val LOGD: (String) -> Unit = { Log.d(LOGTAG, it) }
34+
35+
@Single
36+
class SmolLMManager(
37+
private val messagesDB: MessagesDB,
38+
) {
39+
private val instance = SmolLM()
40+
private var responseGenerationJob: Job? = null
41+
private var chat: Chat? = null
42+
var isInstanceLoaded = false
43+
44+
data class SmolLMInitParams(
45+
val chat: Chat,
46+
val modelPath: String,
47+
val minP: Float,
48+
val temperature: Float,
49+
val storeChats: Boolean,
50+
val contextSize: Long,
51+
)
52+
53+
data class SmolLMResponse(
54+
val response: String,
55+
val generationSpeed: Float,
56+
val generationTimeSecs: Int,
57+
val contextLengthUsed: Int,
58+
)
59+
60+
fun create(
61+
initParams: SmolLMInitParams,
62+
onError: (Exception) -> Unit,
63+
onSuccess: () -> Unit,
64+
) {
65+
try {
66+
CoroutineScope(Dispatchers.Default).launch {
67+
chat = initParams.chat
68+
if (isInstanceLoaded) {
69+
close()
70+
}
71+
instance.create(
72+
initParams.modelPath,
73+
initParams.minP,
74+
initParams.temperature,
75+
initParams.storeChats,
76+
initParams.contextSize,
77+
)
78+
LOGD("Model loaded")
79+
if (initParams.chat.systemPrompt.isNotEmpty()) {
80+
instance.addSystemPrompt(initParams.chat.systemPrompt)
81+
LOGD("System prompt added")
82+
}
83+
if (!initParams.chat.isTask) {
84+
messagesDB.getMessagesForModel(initParams.chat.id).forEach { message ->
85+
if (message.isUserMessage) {
86+
instance.addUserMessage(message.message)
87+
LOGD("User message added: ${message.message}")
88+
} else {
89+
instance.addAssistantMessage(message.message)
90+
LOGD("Assistant message added: ${message.message}")
91+
}
92+
}
93+
}
94+
withContext(Dispatchers.Main) {
95+
isInstanceLoaded = true
96+
onSuccess()
97+
}
98+
}
99+
} catch (e: Exception) {
100+
onError(e)
101+
}
102+
}
103+
104+
fun getResponse(
105+
query: String,
106+
responseTransform: (String) -> String,
107+
onPartialResponseGenerated: (String) -> Unit,
108+
onSuccess: (SmolLMResponse) -> Unit,
109+
onCancelled: () -> Unit,
110+
onError: (Exception) -> Unit,
111+
) {
112+
try {
113+
assert(chat != null) { "Please call SmolLMManager.create() first." }
114+
responseGenerationJob =
115+
CoroutineScope(Dispatchers.Default).launch {
116+
var response = ""
117+
val duration =
118+
measureTime {
119+
instance.getResponse(query).collect { piece ->
120+
response += responseTransform(piece)
121+
withContext(Dispatchers.Main) {
122+
onPartialResponseGenerated(response)
123+
}
124+
}
125+
}
126+
// once the response is generated
127+
// add it to the messages database
128+
messagesDB.addAssistantMessage(chat!!.id, response)
129+
withContext(Dispatchers.Main) {
130+
onSuccess(
131+
SmolLMResponse(
132+
response = response,
133+
generationSpeed = instance.getResponseGenerationSpeed(),
134+
generationTimeSecs = duration.inWholeSeconds.toInt(),
135+
contextLengthUsed = instance.getContextLengthUsed(),
136+
),
137+
)
138+
}
139+
}
140+
} catch (e: CancellationException) {
141+
onCancelled()
142+
} catch (e: Exception) {
143+
onError(e)
144+
}
145+
}
146+
147+
fun stopResponseGeneration() {
148+
responseGenerationJob?.let {
149+
if (it.isActive) {
150+
it.cancel()
151+
}
152+
}
153+
}
154+
155+
fun close() {
156+
instance.close()
157+
isInstanceLoaded = false
158+
}
159+
}

app/src/main/java/io/shubham0204/smollmandroid/ui/screens/chat/ChatActivity.kt

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.content.Context
2222
import android.content.Intent
2323
import android.os.Bundle
2424
import android.text.Spanned
25+
import android.util.Log
2526
import android.widget.Toast
2627
import androidx.activity.ComponentActivity
2728
import androidx.activity.compose.setContent
@@ -105,6 +106,9 @@ import io.shubham0204.smollmandroid.ui.theme.SmolLMAndroidTheme
105106
import kotlinx.coroutines.launch
106107
import org.koin.android.ext.android.inject
107108

109+
private const val LOGTAG = "[ChatActivity-Kt]"
110+
private val LOGD: (String) -> Unit = { Log.d(LOGTAG, it) }
111+
108112
class ChatActivity : ComponentActivity() {
109113
private val viewModel: ChatScreenViewModel by inject()
110114

@@ -133,6 +137,23 @@ class ChatActivity : ComponentActivity() {
133137
}
134138
}
135139
}
140+
141+
/**
142+
* Load the model when the activity is visible to the user and
143+
* unload the model when the activity is not visible to the user.
144+
* see https://developer.android.com/guide/components/activities/activity-lifecycle
145+
*/
146+
override fun onStart() {
147+
super.onStart()
148+
viewModel.loadModel()
149+
LOGD("onStart() called - model loaded")
150+
}
151+
152+
override fun onStop() {
153+
super.onStop()
154+
viewModel.unloadModel()
155+
LOGD("onStop() called - model unloaded")
156+
}
136157
}
137158

138159
@OptIn(ExperimentalMaterial3Api::class)
@@ -219,9 +240,9 @@ fun ChatActivityScreenUI(
219240
) { innerPadding ->
220241
Column(
221242
modifier =
222-
Modifier
223-
.padding(innerPadding)
224-
.background(MaterialTheme.colorScheme.background),
243+
Modifier
244+
.padding(innerPadding)
245+
.background(MaterialTheme.colorScheme.background),
225246
) {
226247
if (currChat != null) {
227248
ScreenUI(viewModel, currChat!!)
@@ -269,9 +290,9 @@ private fun ColumnScope.MessagesList(
269290
LazyColumn(
270291
state = listState,
271292
modifier =
272-
Modifier
273-
.fillMaxSize()
274-
.weight(1f),
293+
Modifier
294+
.fillMaxSize()
295+
.weight(1f),
275296
) {
276297
itemsIndexed(messages) { i, chatMessage ->
277298
MessageListItem(
@@ -311,10 +332,10 @@ private fun ColumnScope.MessagesList(
311332
Row(
312333
verticalAlignment = Alignment.CenterVertically,
313334
modifier =
314-
Modifier
315-
.fillMaxWidth()
316-
.padding(horizontal = 16.dp, vertical = 8.dp)
317-
.animateItem(),
335+
Modifier
336+
.fillMaxWidth()
337+
.padding(horizontal = 16.dp, vertical = 8.dp)
338+
.animateItem(),
318339
) {
319340
Icon(
320341
modifier = Modifier.padding(8.dp),
@@ -325,9 +346,9 @@ private fun ColumnScope.MessagesList(
325346
Text(
326347
text = stringResource(R.string.chat_thinking),
327348
modifier =
328-
Modifier
329-
.fillMaxWidth()
330-
.padding(8.dp),
349+
Modifier
350+
.fillMaxWidth()
351+
.padding(8.dp),
331352
fontSize = 12.sp,
332353
)
333354
}
@@ -424,17 +445,17 @@ private fun LazyItemScope.MessageListItem(
424445
Row(
425446
modifier =
426447
Modifier
427-
.fillMaxWidth()
428-
.animateItem(),
448+
.fillMaxWidth()
449+
.animateItem(),
429450
horizontalArrangement = Arrangement.End,
430451
) {
431452
ChatMessageText(
432453
modifier =
433454
Modifier
434455
.padding(8.dp)
435456
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(16.dp))
436-
.padding(8.dp)
437-
.widthIn(max = 250.dp),
457+
.padding(8.dp)
458+
.widthIn(max = 250.dp),
438459
textColor = android.graphics.Color.WHITE,
439460
textSize = 16f,
440461
message = messageStr,
@@ -478,8 +499,8 @@ private fun MessageInput(
478499
TextField(
479500
modifier =
480501
Modifier
481-
.fillMaxWidth()
482-
.weight(1f),
502+
.fillMaxWidth()
503+
.weight(1f),
483504
value = questionText,
484505
onValueChange = { questionText = it },
485506
shape = RoundedCornerShape(16.dp),
@@ -489,7 +510,7 @@ private fun MessageInput(
489510
focusedIndicatorColor = Color.Transparent,
490511
unfocusedIndicatorColor = Color.Transparent,
491512
disabledIndicatorColor = Color.Transparent,
492-
),
513+
),
493514
placeholder = {
494515
Text(
495516
text = stringResource(R.string.chat_ask_question),
@@ -548,7 +569,7 @@ private fun TasksListBottomSheet(viewModel: ChatScreenViewModel) {
548569
Modifier
549570
.fillMaxWidth()
550571
.background(MaterialTheme.colorScheme.surfaceContainer, RoundedCornerShape(8.dp))
551-
.padding(8.dp),
572+
.padding(8.dp),
552573
horizontalAlignment = Alignment.CenterHorizontally,
553574
verticalArrangement = Arrangement.Center,
554575
) {
@@ -612,7 +633,7 @@ private fun SelectModelsList(viewModel: ChatScreenViewModel) {
612633
val context = LocalContext.current
613634
if (showSelectModelsListDialog) {
614635
val modelsList by
615-
viewModel.modelsRepository.getAvailableModels().collectAsState(emptyList())
636+
viewModel.modelsRepository.getAvailableModels().collectAsState(emptyList())
616637
SelectModelsList(
617638
onDismissRequest = { viewModel.hideSelectModelListDialog() },
618639
modelsList,

0 commit comments

Comments
 (0)