diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 6de417594..ab72f7429 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -291,6 +291,7 @@ dependencies { googleImplementation(libs.firebase.ai) googleImplementation(libs.firebase.ai.ondevice) googleImplementation(libs.mlkit.translate) + googleImplementation(libs.mlkit.genai.prompt) googleImplementation(libs.androidx.appfunctions) googleImplementation(libs.androidx.appfunctions.service) diff --git a/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FdroidAiModule.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FdroidAiModule.kt index 53edf9aca..2920bf22d 100644 --- a/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FdroidAiModule.kt +++ b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FdroidAiModule.kt @@ -18,6 +18,8 @@ package org.meshtastic.app.di import org.koin.core.annotation.Module import org.koin.core.annotation.Single +import org.meshtastic.feature.discovery.ai.AlgorithmicSummaryProvider +import org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider import org.meshtastic.feature.docs.ai.AIDocAssistant import org.meshtastic.feature.docs.ai.KeywordFallbackAssistant import org.meshtastic.feature.docs.translation.DocTranslationService @@ -28,5 +30,7 @@ import org.meshtastic.feature.docs.translation.NoOpDocTranslator class FdroidAiModule { @Single fun aiDocAssistant(fallback: KeywordFallbackAssistant): AIDocAssistant = fallback + @Single fun discoverySummaryAiProvider(fallback: AlgorithmicSummaryProvider): DiscoverySummaryAiProvider = fallback + @Single fun docTranslationService(): DocTranslationService = NoOpDocTranslator() } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleAiModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleAiModule.kt index 0828a9493..121164eca 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleAiModule.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleAiModule.kt @@ -22,8 +22,11 @@ import okio.Path.Companion.toOkioPath import org.koin.core.annotation.Module import org.koin.core.annotation.Single import org.meshtastic.app.ai.GeminiNanoDocAssistant +import org.meshtastic.app.discovery.GeminiNanoSummaryProvider import org.meshtastic.app.translation.MlKitDocTranslator import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.feature.discovery.DiscoverySummaryGenerator +import org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider import org.meshtastic.feature.docs.ai.AIDocAssistant import org.meshtastic.feature.docs.data.DocBundleLoader import org.meshtastic.feature.docs.data.KeywordSearchEngine @@ -44,6 +47,10 @@ class GoogleAiModule { nodeRepository: NodeRepository, ): AIDocAssistant = GeminiNanoDocAssistant(searchEngine, bundleLoader, nodeRepository) + @Single + fun discoverySummaryAiProvider(generator: DiscoverySummaryGenerator): DiscoverySummaryAiProvider = + GeminiNanoSummaryProvider(generator) + @Single fun docTranslationCache(context: Context): DocTranslationCache = DocTranslationCache(cacheDir = context.cacheDir.toOkioPath(), fileSystem = FileSystem.SYSTEM) diff --git a/feature/discovery/src/androidMain/kotlin/org/meshtastic/feature/discovery/ai/GeminiNanoSummaryProvider.kt b/androidApp/src/google/kotlin/org/meshtastic/app/discovery/GeminiNanoSummaryProvider.kt similarity index 90% rename from feature/discovery/src/androidMain/kotlin/org/meshtastic/feature/discovery/ai/GeminiNanoSummaryProvider.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/discovery/GeminiNanoSummaryProvider.kt index 6fc800ef4..50395fc72 100644 --- a/feature/discovery/src/androidMain/kotlin/org/meshtastic/feature/discovery/ai/GeminiNanoSummaryProvider.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/discovery/GeminiNanoSummaryProvider.kt @@ -14,26 +14,29 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.discovery.ai +package org.meshtastic.app.discovery import co.touchlab.kermit.Logger import com.google.mlkit.genai.prompt.Generation import com.google.mlkit.genai.prompt.GenerativeModel import com.google.mlkit.genai.prompt.TextPart import com.google.mlkit.genai.prompt.generateContentRequest -import org.koin.core.annotation.Single import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity import org.meshtastic.feature.discovery.DiscoverySummaryGenerator +import org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider /** - * Android provider that uses Gemini Nano via ML Kit GenAI Prompt API for on-device AI summaries. + * Google-flavor provider that uses Gemini Nano via the ML Kit GenAI Prompt API for on-device AI summaries. + * + * Lives in the Google flavor source set (not the shared `:feature:discovery` module) so the proprietary ML Kit GenAI + * dependency never reaches the F-Droid build. The F-Droid flavor binds + * [org.meshtastic.feature.discovery.ai. AlgorithmicSummaryProvider] instead. * * Falls back to [DiscoverySummaryGenerator] when: * - The on-device model is unavailable (unsupported hardware or not downloaded) * - Generation fails for any reason */ -@Single(binds = [DiscoverySummaryAiProvider::class]) class GeminiNanoSummaryProvider(private val generator: DiscoverySummaryGenerator) : DiscoverySummaryAiProvider { private val log = Logger.withTag("GeminiNanoSummary") diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt index c47a1d4b0..3e7826074 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt @@ -87,6 +87,8 @@ import org.meshtastic.desktop.stub.NoopMeshLocationManager import org.meshtastic.desktop.stub.NoopMeshWorkerManager import org.meshtastic.desktop.stub.NoopPhoneLocationProvider import org.meshtastic.desktop.stub.NoopPlatformAnalytics +import org.meshtastic.feature.discovery.ai.AlgorithmicSummaryProvider +import org.meshtastic.feature.discovery.ai.DiscoverySummaryAiProvider import org.meshtastic.feature.docs.ai.AIDocAssistant import org.meshtastic.feature.docs.ai.KeywordFallbackAssistant import org.meshtastic.feature.docs.translation.DocTranslationService @@ -227,6 +229,7 @@ private fun desktopPlatformStubsModule() = module { // AI assistant: keyword-only fallback on desktop (no on-device model) single { get() } + single { get() } single { NoOpDocTranslator() } // Desktop uses the real ApiService implementation (no flavor stub needed) diff --git a/feature/discovery/build.gradle.kts b/feature/discovery/build.gradle.kts index 8fa4407bc..bc26d4d78 100644 --- a/feature/discovery/build.gradle.kts +++ b/feature/discovery/build.gradle.kts @@ -51,7 +51,5 @@ kotlin { } commonTest.dependencies { implementation(projects.core.testing) } - - androidMain.dependencies { implementation(libs.mlkit.genai.prompt) } } } diff --git a/feature/discovery/src/jvmMain/kotlin/org/meshtastic/feature/discovery/ai/AlgorithmicSummaryProvider.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ai/AlgorithmicSummaryProvider.kt similarity index 76% rename from feature/discovery/src/jvmMain/kotlin/org/meshtastic/feature/discovery/ai/AlgorithmicSummaryProvider.kt rename to feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ai/AlgorithmicSummaryProvider.kt index 3fa5a96b5..c8d89eb62 100644 --- a/feature/discovery/src/jvmMain/kotlin/org/meshtastic/feature/discovery/ai/AlgorithmicSummaryProvider.kt +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ai/AlgorithmicSummaryProvider.kt @@ -21,8 +21,14 @@ import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity import org.meshtastic.core.database.entity.DiscoverySessionEntity import org.meshtastic.feature.discovery.DiscoverySummaryGenerator -/** JVM/Desktop fallback that delegates to the algorithmic [DiscoverySummaryGenerator]. */ -@Single(binds = [DiscoverySummaryAiProvider::class]) +/** + * Algorithmic [DiscoverySummaryAiProvider] that delegates to the deterministic [DiscoverySummaryGenerator]. + * + * Used wherever no on-device AI model is available: Desktop, iOS, and the Android F-Droid flavor. Registered with + * `binds = []` so it is injectable by concrete type but is not auto-bound to [DiscoverySummaryAiProvider]; each + * platform binds the interface explicitly (the Google flavor binds the Gemini Nano provider instead). + */ +@Single(binds = []) class AlgorithmicSummaryProvider(private val generator: DiscoverySummaryGenerator) : DiscoverySummaryAiProvider { override val isAvailable: Boolean = true