@@ -43,14 +43,19 @@ import com.blurr.voice.v2.AgentService
4343import com.blurr.voice.v2.llm.GeminiApi
4444import com.google.ai.client.generativeai.type.TextPart
4545import com.google.firebase.Firebase
46+ import com.google.firebase.Timestamp
4647import com.google.firebase.analytics.FirebaseAnalytics
4748import com.google.firebase.analytics.analytics
49+ import com.google.firebase.auth.auth
50+ import com.google.firebase.firestore.FieldValue
51+ import com.google.firebase.firestore.firestore
4852import kotlinx.coroutines.CoroutineScope
4953import kotlinx.coroutines.Dispatchers
5054import kotlinx.coroutines.SupervisorJob
5155import kotlinx.coroutines.cancel
5256import kotlinx.coroutines.delay
5357import kotlinx.coroutines.launch
58+ import kotlinx.coroutines.tasks.await
5459import kotlinx.coroutines.withContext
5560
5661data class ModelDecision (
@@ -85,6 +90,11 @@ class ConversationalAgentService : Service() {
8590 private val usedMemories = mutableSetOf<String >() // Track memories already used in this conversation
8691 private lateinit var firebaseAnalytics: FirebaseAnalytics
8792 private val eyes by lazy { Eyes (this ) }
93+
94+ // Firebase instances for conversation tracking
95+ private val db = Firebase .firestore
96+ private val auth = Firebase .auth
97+ private var conversationId: String? = null // Track current conversation session
8898
8999
90100 companion object {
@@ -165,11 +175,13 @@ class ConversationalAgentService : Service() {
165175
166176 // Track conversation initiation
167177 firebaseAnalytics.logEvent(" conversation_initiated" , null )
178+ trackConversationStart()
168179
169180 serviceScope.launch {
170181 if (conversationHistory.size == 1 ) {
171182 val greeting = getPersonalizedGreeting()
172183 conversationHistory = addResponse(" model" , greeting, conversationHistory)
184+ trackMessage(" model" , greeting, " greeting" )
173185 speakAndThenListen(greeting)
174186 }
175187 }
@@ -236,7 +248,8 @@ class ConversationalAgentService : Service() {
236248 if (sttErrorAttempts >= maxSttErrorAttempts) {
237249 firebaseAnalytics.logEvent(" conversation_ended_stt_errors" , null )
238250 val exitMessage = " I'm having trouble understanding you clearly. Please try calling later!"
239- gracefulShutdown(exitMessage)
251+ trackMessage(" model" , exitMessage, " error_message" )
252+ gracefulShutdown(exitMessage, " stt_errors" )
240253 } else {
241254 speakAndThenListen(" I'm sorry, I didn't catch that. Could you please repeat?" )
242255 }
@@ -326,6 +339,9 @@ class ConversationalAgentService : Service() {
326339 updateSystemPromptWithAgentStatus()
327340
328341 conversationHistory = addResponse(" user" , userInput, conversationHistory)
342+
343+ // Track user message in Firebase
344+ trackMessage(" user" , userInput, " input" )
329345
330346 // Track user input
331347 val inputBundle = android.os.Bundle ().apply {
@@ -338,7 +354,8 @@ class ConversationalAgentService : Service() {
338354 try {
339355 if (userInput.equals(" stop" , ignoreCase = true ) || userInput.equals(" exit" , ignoreCase = true )) {
340356 firebaseAnalytics.logEvent(" conversation_ended_by_command" , null )
341- gracefulShutdown(" Goodbye!" )
357+ trackMessage(" model" , " Goodbye!" , " farewell" )
358+ gracefulShutdown(" Goodbye!" , " command" )
342359 return @launch
343360 }
344361
@@ -395,6 +412,7 @@ class ConversationalAgentService : Service() {
395412 " Clarification needed for task: ${decision.instruction} " ,
396413 conversationHistory
397414 )
415+ trackMessage(" model" , questionToAsk, " clarification" )
398416 speakAndThenListen(questionToAsk, false )
399417 } else {
400418 Log .d(
@@ -407,7 +425,8 @@ class ConversationalAgentService : Service() {
407425
408426 val originalInstruction = decision.instruction
409427 AgentService .start(applicationContext, originalInstruction)
410- gracefulShutdown(decision.reply)
428+ trackMessage(" model" , decision.reply, " task_confirmation" )
429+ gracefulShutdown(decision.reply, " task_executed" )
411430 }
412431 } else {
413432 Log .d(
@@ -419,7 +438,8 @@ class ConversationalAgentService : Service() {
419438 firebaseAnalytics.logEvent(" task_executed_max_clarification" , taskBundle)
420439
421440 AgentService .start(applicationContext, decision.instruction)
422- gracefulShutdown(decision.reply)
441+ trackMessage(" model" , decision.reply, " task_confirmation" )
442+ gracefulShutdown(decision.reply, " task_executed" )
423443 }
424444 }else {
425445 Log .w(" ConvAgent" , " User has no tasks remaining. Denying request." )
@@ -429,6 +449,7 @@ class ConversationalAgentService : Service() {
429449
430450 val upgradeMessage = " ${getPersonalizedGreeting()} You've used all your free tasks for the month. Please upgrade in the app to unlock more. We can still talk in voice mode."
431451 conversationHistory = addResponse(" model" , upgradeMessage, conversationHistory)
452+ trackMessage(" model" , upgradeMessage, " freemium_limit" )
432453 speakAndThenListen(upgradeMessage)
433454 }
434455 }
@@ -443,9 +464,12 @@ class ConversationalAgentService : Service() {
443464
444465 if (AgentService .isRunning) {
445466 AgentService .stop(applicationContext)
446- gracefulShutdown(decision.reply)
467+ trackMessage(" model" , decision.reply, " kill_task_response" )
468+ gracefulShutdown(decision.reply, " task_killed" )
447469 } else {
448- speakAndThenListen(" There was no automation running, but I can help with something else." )
470+ val noTaskMessage = " There was no automation running, but I can help with something else."
471+ trackMessage(" model" , noTaskMessage, " kill_task_response" )
472+ speakAndThenListen(noTaskMessage)
449473 }
450474 }
451475 else -> { // Default to "Reply"
@@ -459,9 +483,11 @@ class ConversationalAgentService : Service() {
459483 if (decision.shouldEnd) {
460484 Log .d(" ConvAgent" , " Model decided to end the conversation." )
461485 firebaseAnalytics.logEvent(" conversation_ended_by_model" , null )
462- gracefulShutdown(decision.reply)
486+ trackMessage(" model" , decision.reply, " farewell" )
487+ gracefulShutdown(decision.reply, " model_ended" )
463488 } else {
464489 conversationHistory = addResponse(" model" , rawModelResponse, conversationHistory)
490+ trackMessage(" model" , decision.reply, " reply" )
465491 speakAndThenListen(decision.reply)
466492 }
467493 }
@@ -909,7 +935,7 @@ class ConversationalAgentService : Service() {
909935 }
910936 }
911937
912- private suspend fun gracefulShutdown (exitMessage : String? = null) {
938+ private suspend fun gracefulShutdown (exitMessage : String? = null, endReason : String = "graceful" ) {
913939 // Track graceful shutdown
914940 val shutdownBundle = android.os.Bundle ().apply {
915941 putBoolean(" had_exit_message" , exitMessage != null )
@@ -920,6 +946,10 @@ class ConversationalAgentService : Service() {
920946 }
921947 firebaseAnalytics.logEvent(" conversation_ended_gracefully" , shutdownBundle)
922948
949+ // Track conversation end in Firebase
950+
951+ trackConversationEnd(endReason)
952+
923953 visualFeedbackManager.hideTtsWave()
924954 visualFeedbackManager.hideTranscription()
925955 visualFeedbackManager.hideSpeakingOverlay()
@@ -953,6 +983,9 @@ class ConversationalAgentService : Service() {
953983 }
954984 firebaseAnalytics.logEvent(" conversation_ended_instantly" , instantShutdownBundle)
955985
986+ // Track conversation end in Firebase
987+ trackConversationEnd(" instant" )
988+
956989 Log .d(" ConvAgent" , " Instant shutdown triggered by user." )
957990 speechCoordinator.stopSpeaking()
958991 speechCoordinator.stopListening()
@@ -971,13 +1004,131 @@ class ConversationalAgentService : Service() {
9711004
9721005 stopSelf()
9731006 }
1007+
1008+ /* *
1009+ * Tracks the conversation start in Firebase by creating a new conversation entry.
1010+ * This method is inspired by AgentService's Firebase operations.
1011+ */
1012+ private fun trackConversationStart () {
1013+ val currentUser = auth.currentUser
1014+ if (currentUser == null ) {
1015+ Log .w(" ConvAgent" , " Cannot track conversation, user is not logged in." )
1016+ return
1017+ }
1018+
1019+ // Generate a unique conversation ID
1020+ conversationId = " ${System .currentTimeMillis()} _${currentUser.uid.take(8 )} "
1021+
1022+ serviceScope.launch {
1023+ try {
1024+ val conversationEntry = hashMapOf(
1025+ " conversationId" to conversationId,
1026+ " startedAt" to Timestamp .now(),
1027+ " endedAt" to null ,
1028+ " messageCount" to 0 ,
1029+ " textModeUsed" to false ,
1030+ " clarificationAttempts" to 0 ,
1031+ " sttErrorAttempts" to 0 ,
1032+ " endReason" to null , // "graceful", "instant", "command", "model", "stt_errors"
1033+ " tasksRequested" to 0 ,
1034+ " tasksExecuted" to 0
1035+ )
1036+
1037+ // Append the conversation to the user's conversationHistory array
1038+ db.collection(" users" ).document(currentUser.uid)
1039+ .update(" conversationHistory" , FieldValue .arrayUnion(conversationEntry))
1040+ .await()
1041+
1042+ Log .d(" ConvAgent" , " Successfully tracked conversation start in Firebase for user ${currentUser.uid} : $conversationId " )
1043+ } catch (e: Exception ) {
1044+ Log .e(" ConvAgent" , " Failed to track conversation start in Firebase" , e)
1045+ // Don't fail the conversation if Firebase tracking fails
1046+ }
1047+ }
1048+ }
1049+
1050+ /* *
1051+ * Tracks individual messages in the conversation.
1052+ * Fire and forget operation.
1053+ */
1054+ private fun trackMessage (role : String , message : String , messageType : String = "text") {
1055+ val currentUser = auth.currentUser
1056+ if (currentUser == null || conversationId == null ) {
1057+ return
1058+ }
1059+
1060+ serviceScope.launch {
1061+ try {
1062+ val messageEntry = hashMapOf(
1063+ " conversationId" to conversationId,
1064+ " role" to role, // "user" or "model"
1065+ " message" to message.take(500 ), // Limit message length for storage
1066+ " messageType" to messageType, // "text", "task", "clarification"
1067+ " timestamp" to Timestamp .now(),
1068+ " inputMode" to if (isTextModeActive) " text" else " voice"
1069+ )
1070+
1071+ // Append the message to the user's messageHistory array
1072+ db.collection(" users" ).document(currentUser.uid)
1073+ .update(" messageHistory" , FieldValue .arrayUnion(messageEntry))
1074+ .await()
1075+
1076+ Log .d(" ConvAgent" , " Successfully tracked message in Firebase: $role - ${message.take(50 )} ..." )
1077+ } catch (e: Exception ) {
1078+ Log .e(" ConvAgent" , " Failed to track message in Firebase" , e)
1079+ }
1080+ }
1081+ }
1082+
1083+ /* *
1084+ * Updates the conversation completion status in Firebase.
1085+ * Fire and forget operation.
1086+ */
1087+ private fun trackConversationEnd (endReason : String , tasksRequested : Int = 0, tasksExecuted : Int = 0) {
1088+ val currentUser = auth.currentUser
1089+ if (currentUser == null || conversationId == null ) {
1090+ return
1091+ }
1092+
1093+ serviceScope.launch {
1094+ try {
1095+ val completionEntry = hashMapOf(
1096+ " conversationId" to conversationId,
1097+ " endedAt" to Timestamp .now(),
1098+ " messageCount" to conversationHistory.size,
1099+ " textModeUsed" to isTextModeActive,
1100+ " clarificationAttempts" to clarificationAttempts,
1101+ " sttErrorAttempts" to sttErrorAttempts,
1102+ " endReason" to endReason,
1103+ " tasksRequested" to tasksRequested,
1104+ " tasksExecuted" to tasksExecuted,
1105+ " status" to " completed"
1106+ )
1107+
1108+ // Append the completion status to the user's conversationHistory array
1109+ db.collection(" users" ).document(currentUser.uid)
1110+ .update(" conversationHistory" , FieldValue .arrayUnion(completionEntry))
1111+ .await()
1112+
1113+ Log .d(" ConvAgent" , " Successfully tracked conversation end in Firebase: $conversationId ($endReason )" )
1114+ } catch (e: Exception ) {
1115+ Log .e(" ConvAgent" , " Failed to track conversation end in Firebase" , e)
1116+ }
1117+ }
1118+ }
1119+
9741120 override fun onDestroy () {
9751121 super .onDestroy()
9761122 Log .d(" ConvAgent" , " Service onDestroy" )
9771123
9781124 // Track service destruction
9791125 firebaseAnalytics.logEvent(" conversational_agent_destroyed" , null )
9801126
1127+ // Track conversation end if not already tracked
1128+ if (conversationId != null ) {
1129+ trackConversationEnd(" service_destroyed" )
1130+ }
1131+
9811132 removeClarificationQuestions()
9821133 serviceScope.cancel()
9831134 ttsManager.setCaptionsEnabled(false )
0 commit comments