Skip to content

Commit 98e9c03

Browse files
committed
feat(ui, engine): Massive UI Integration and 'Glass Sandwich' Tamagotchi prompt context optimization. Fixed autoregressive telemetry loops.
1 parent 9ba69c2 commit 98e9c03

21 files changed

Lines changed: 749 additions & 213 deletions

app/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ dependencies {
112112

113113
implementation("androidx.core:core-ktx:1.13.1")
114114

115+
// UI & Layouts
116+
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
117+
implementation("androidx.recyclerview:recyclerview:1.3.2")
118+
115119
// Coroutines - 1.10.2 for Koog/Ktor 3.x compatibility
116120
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
117121
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")

app/src/main/kotlin/com/gemma/api/ApiServer.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,20 @@ class ApiServer(
3131
post("/api/generate") {
3232
try {
3333
val request = call.receiveText()
34-
@Suppress("UNCHECKED_CAST")
35-
val json = gson.fromJson(request, Map::class.java) as Map<String, Any>
36-
val prompt = json["prompt"] as? String ?: ""
37-
val sessionId = json["session_id"] as? String ?: UUID.randomUUID().toString()
34+
val parsed = gson.fromJson(request, Map::class.java)
35+
if (parsed !is Map<*, *>) {
36+
call.respondText("""{"error": "Invalid JSON format"}""", ContentType.Application.Json, HttpStatusCode.BadRequest)
37+
return@post
38+
}
39+
val prompt = parsed["prompt"] as? String ?: ""
40+
val sessionId = parsed["session_id"] as? String ?: UUID.randomUUID().toString()
3841

3942
Timber.d("API: ${prompt.take(30)}...")
4043

4144
// DELEGATE TO GEMMA SERVICE (Orchestrator)
4245
// Storage is handled atomically inside processQuery
4346
val aiResponse = withContext(Dispatchers.Default) {
44-
gemmaService.processQuery(prompt, sessionId)
47+
gemmaService.processQuery(prompt, sessionId) ?: "Error: No response generated"
4548
}
4649

4750
val jsonResponse = gson.toJson(mapOf(

app/src/main/kotlin/com/gemma/api/GemmaAccessibilityService.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class GemmaAccessibilityService : AccessibilityService() {
8181
}
8282
} finally {
8383
queue.forEach { toRecycle.add(it) }
84-
toRecycle.forEach { try { it.recycle() } catch (_: Exception) {} }
84+
toRecycle.forEach { try { it.recycle() } catch (e: IllegalStateException) { Timber.v("Failed to recycle node: ${e.message}") } }
8585
}
8686

8787
return if (sb.isEmpty()) "[[SCREEN: no readable content]]"
@@ -213,7 +213,7 @@ class GemmaAccessibilityService : AccessibilityService() {
213213
} finally {
214214
// Drain remaining queue and recycle all
215215
queue.forEach { toRecycle.add(it) }
216-
toRecycle.forEach { try { it.recycle() } catch (_: Exception) {} }
216+
toRecycle.forEach { try { it.recycle() } catch (e: IllegalStateException) { Timber.v("Failed to recycle node: ${e.message}") } }
217217
}
218218
return found
219219
}
@@ -245,7 +245,7 @@ class GemmaAccessibilityService : AccessibilityService() {
245245
}
246246
} finally {
247247
queue.forEach { toRecycle.add(it) }
248-
toRecycle.forEach { try { it.recycle() } catch (_: Exception) {} }
248+
toRecycle.forEach { try { it.recycle() } catch (e: IllegalStateException) { Timber.v("Failed to recycle node: ${e.message}") } }
249249
}
250250
return result
251251
}
@@ -297,7 +297,7 @@ class GemmaAccessibilityService : AccessibilityService() {
297297
}
298298
} finally {
299299
queue.forEach { toRecycle.add(it) }
300-
toRecycle.forEach { try { it.recycle() } catch (_: Exception) {} }
300+
toRecycle.forEach { try { it.recycle() } catch (e: IllegalStateException) { Timber.v("Failed to recycle node: ${e.message}") } }
301301
}
302302
return result
303303
}

app/src/main/kotlin/com/gemma/api/GemmaService.kt

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.CoroutineScope
1212
import kotlinx.coroutines.Dispatchers
1313
import kotlinx.coroutines.SupervisorJob
1414
import kotlinx.coroutines.launch
15+
import kotlinx.coroutines.withContext
1516
import kotlinx.coroutines.cancel
1617
import kotlinx.coroutines.delay
1718
import kotlinx.coroutines.Job
@@ -44,9 +45,23 @@ import com.gemma.api.mcp.MCPServer
4445
*/
4546
class GemmaService : Service(), AgentPlatformCallbacks {
4647

48+
inner class LocalBinder : android.os.Binder() {
49+
fun getService(): GemmaService = this@GemmaService
50+
}
51+
52+
// UI streaming interface for the native chat activity
53+
interface UiCallback {
54+
fun onMessageAdded(message: String, isUser: Boolean, isComplete: Boolean = true)
55+
fun onThinkingStateChanged(isThinking: Boolean)
56+
}
57+
58+
var uiCallback: UiCallback? = null
59+
60+
private val serviceScope = CoroutineScope(Dispatchers.Default + Job())
61+
4762
// Mandatory Service implementation
4863
override fun onBind(intent: Intent?): IBinder? {
49-
return null
64+
return LocalBinder()
5065
}
5166

5267
// IO dispatcher: inference + DB work must not compete with the UI render thread.
@@ -698,20 +713,57 @@ class GemmaService : Service(), AgentPlatformCallbacks {
698713
}
699714
}
700715

701-
suspend fun processQuery(userPrompt: String, sessionId: String, isDream: Boolean = false): String {
716+
/**
717+
* Called directly by MainActivity to process native chat interface queries.
718+
* It uses the same backend engine but avoids spinning up unnecessary Overlay/Audio managers.
719+
*/
720+
fun processQueryFromUi(prompt: String) {
721+
if (!::koogAgent.isInitialized || !koogAgent.isReady) {
722+
uiCallback?.onMessageAdded("Agent is still initializing. Please wait a moment and try again.", isUser = false)
723+
return
724+
}
725+
726+
// Let UI know we accepted it
727+
uiCallback?.onMessageAdded(prompt, isUser = true)
728+
uiCallback?.onThinkingStateChanged(true)
729+
730+
serviceScope.launch {
731+
try {
732+
// Pass it through the core pipeline without triggering TTS audio unless explicitly asked
733+
// processQuery signature: suspend fun processQuery(userPrompt: String, sessionId: String? = null): String
734+
val response = processQuery(prompt, null, false)
735+
736+
withContext(Dispatchers.Main) {
737+
uiCallback?.onThinkingStateChanged(false)
738+
uiCallback?.onMessageAdded(response ?: "Error: Did not generate response.", isUser = false)
739+
}
740+
} catch (e: Exception) {
741+
Timber.e(e, "UI processing failure")
742+
withContext(Dispatchers.Main) {
743+
uiCallback?.onThinkingStateChanged(false)
744+
uiCallback?.onMessageAdded("Error: ${e.message}", isUser = false)
745+
}
746+
}
747+
}
748+
}
749+
750+
/**
751+
* Core orchestrator: Context gathering + LLM reasoning + Tool execution
752+
*/
753+
suspend fun processQuery(userPrompt: String, sessionId: String? = null, isDream: Boolean = false): String? {
702754
if (!::koogAgent.isInitialized || !koogAgent.isReady) {
703755
responseNotificationManager.showResponse("⚠️ Agent still starting up... try again in a moment")
704756
return "Agent is still initializing. Please wait a moment and try again."
705757
}
706758

707759
if (!isDream) markActivity()
708760

709-
return kotlinx.coroutines.withTimeout(120000) {
761+
return kotlinx.coroutines.withTimeoutOrNull(120000) {
710762
koogAgent.processUserMessage(
711763
message = userPrompt,
712-
sessionId = sessionId,
764+
sessionId = sessionId ?: java.util.UUID.randomUUID().toString(),
713765
isDream = isDream
714-
)
766+
) ?: "Error: Agent returned null."
715767
}
716768
}
717769

@@ -861,6 +913,7 @@ class GemmaService : Service(), AgentPlatformCallbacks {
861913
private var currentFrame = 0
862914
private var animationJob: Job? = null
863915
private var lastActivityTime = System.currentTimeMillis()
916+
private var lastKvFlushTime = System.currentTimeMillis()
864917

865918
private fun startAnimationLoop() {
866919
animationJob?.cancel()
@@ -870,6 +923,16 @@ class GemmaService : Service(), AgentPlatformCallbacks {
870923
val frame = getNextAnimationFrame()
871924
updateNotification("✧💭 $frame")
872925
}
926+
927+
val now = System.currentTimeMillis()
928+
if (now - lastActivityTime > 15 * 60 * 1000 && now - lastKvFlushTime > 15 * 60 * 1000) {
929+
lastKvFlushTime = now
930+
if (::koogAgent.isInitialized) {
931+
Timber.d("GemmaService: Triggering 15-min inactivity KV Cache Flush")
932+
koogAgent.sendSystemEvent(KoogAgent.SystemEventType.KV_CACHE_FLUSH)
933+
}
934+
}
935+
873936
delay(500) // 500ms per frame
874937
}
875938
}
@@ -927,7 +990,7 @@ class GemmaService : Service(), AgentPlatformCallbacks {
927990
// Checkpoint agent state in case process is killed next
928991
if (::koogAgent.isInitialized) {
929992
try {
930-
kotlinx.coroutines.runBlocking {
993+
kotlinx.coroutines.runBlocking(Dispatchers.IO) {
931994
kotlinx.coroutines.withTimeoutOrNull(3000L) {
932995
koogAgent.checkpoint()
933996
}
@@ -960,7 +1023,7 @@ class GemmaService : Service(), AgentPlatformCallbacks {
9601023
private fun performCriticalCleanup() {
9611024
try {
9621025
// Use runBlocking to ensure cleanup completes (Kimi's fix for GlobalScope leak)
963-
kotlinx.coroutines.runBlocking {
1026+
kotlinx.coroutines.runBlocking(Dispatchers.IO) {
9641027
kotlinx.coroutines.withTimeoutOrNull(5000L) {
9651028
// Shutdown agent (closes event queue and checkpoints)
9661029
if (::koogAgent.isInitialized) {

0 commit comments

Comments
 (0)