Skip to content
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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ dependencies {
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.turbine)
testImplementation(libs.mockk)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.room.testing)
androidTestImplementation(libs.androidx.test.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.jdluu.flexinsight.data.repository.FlexRepository
import com.jdluu.flexinsight.data.repository.RoutineRepository
import com.jdluu.flexinsight.data.repository.StatsRepository
import com.jdluu.flexinsight.data.repository.WorkoutRepository
import com.jdluu.flexinsight.domain.ai.AiContextProvider
import kotlinx.coroutines.flow.first
import java.text.SimpleDateFormat
import java.util.Date
Expand All @@ -35,7 +36,7 @@ class HevyAiDataAccessor @Inject constructor(
private val userPreferencesManager: UserPreferencesManager,
private val apiKeyManager: ApiKeyManager,
private val healthConnectRepository: HealthConnectRepository
) {
) : AiContextProvider {

data class ContextSnapshot(
val text: String,
Expand All @@ -46,7 +47,7 @@ class HevyAiDataAccessor @Inject constructor(
val usesLiveExerciseHistory: Boolean = false
)

suspend fun buildContext(userQuery: String? = null): ContextSnapshot {
override suspend fun buildContext(userQuery: String?): ContextSnapshot {
val hasApiKey = apiKeyManager.hasApiKey()
val workoutCount = try {
flexRepository.getWorkoutCount().first()
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/jdluu/flexinsight/di/AiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.jdluu.flexinsight.di

import com.jdluu.flexinsight.data.ai.FlexAIClient
import com.jdluu.flexinsight.data.ai.GeminiNanoClient
import com.jdluu.flexinsight.data.ai.HevyAiDataAccessor
import com.jdluu.flexinsight.domain.ai.AiContextProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -17,4 +19,10 @@ abstract class AiModule {
abstract fun bindFlexAIClient(
geminiNanoClient: GeminiNanoClient
): FlexAIClient

@Binds
@Singleton
abstract fun bindAiContextProvider(
hevyAiDataAccessor: HevyAiDataAccessor
): AiContextProvider
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.jdluu.flexinsight.domain.ai

import com.jdluu.flexinsight.data.ai.HevyAiDataAccessor

/**
* Source of AI prompt context built from user workout data.
*
* Extracted from [HevyAiDataAccessor] so domain code depends on an abstraction
* rather than the Hevy-specific implementation.
*/
interface AiContextProvider {
/**
* Builds the system prompt context snapshot.
*
* @param userQuery When set, triggers live Hevy API exercise-history lookups for relevant lifts.
*/
suspend fun buildContext(userQuery: String? = null): HevyAiDataAccessor.ContextSnapshot
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.jdluu.flexinsight.domain.usecase

import com.jdluu.flexinsight.data.ai.HevyAiDataAccessor
import com.jdluu.flexinsight.domain.ai.AiContextProvider
import javax.inject.Inject

/**
Expand All @@ -9,9 +10,9 @@ import javax.inject.Inject
* @param userQuery When set, triggers live Hevy API exercise-history lookups for relevant lifts.
*/
class BuildAiContextUseCase @Inject constructor(
private val hevyAiDataAccessor: HevyAiDataAccessor
private val aiContextProvider: AiContextProvider
) {
suspend operator fun invoke(userQuery: String? = null): HevyAiDataAccessor.ContextSnapshot {
return hevyAiDataAccessor.buildContext(userQuery)
return aiContextProvider.buildContext(userQuery)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.jdluu.flexinsight.domain.usecase

import com.jdluu.flexinsight.data.ai.HevyAiDataAccessor
import com.jdluu.flexinsight.domain.ai.AiContextProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Test

class BuildAiContextUseCaseTest {

private fun snapshot(text: String, usesLive: Boolean = false) = HevyAiDataAccessor.ContextSnapshot(
text = text,
hasWorkoutData = true,
hasApiKey = true,
workoutCount = 42,
usesLiveExerciseHistory = usesLive
)

private class FakeContextProvider : AiContextProvider {
var lastQuery: String? = null
lateinit var response: HevyAiDataAccessor.ContextSnapshot

override suspend fun buildContext(userQuery: String?): HevyAiDataAccessor.ContextSnapshot {
lastQuery = userQuery
return response
}
}

@Test
fun `delegates to provider with null query by default`() = runTest {
val provider = FakeContextProvider()
val expected = snapshot("context text")
provider.response = expected

val result = BuildAiContextUseCase(provider)()

assertEquals(expected, result)
assertEquals(null, provider.lastQuery)
}

@Test
fun `forwards non-null user query to provider`() = runTest {
val provider = FakeContextProvider()
val expected = snapshot("live history context", usesLive = true)
provider.response = expected

val result = BuildAiContextUseCase(provider)(userQuery = "how is my bench?")

assertSame(expected, result)
assertEquals("how is my bench?", provider.lastQuery)
}
}
Loading
Loading