feat(discovery): wire Gemini Nano via ML Kit GenAI Prompt API

Replace the stub GeminiNanoSummaryProvider with a real implementation
that uses com.google.mlkit:genai-prompt:1.0.0-beta2 for on-device
AI-powered scan summaries on supported Android hardware.

Implementation:
- Generation.getClient() to obtain the GenerativeModel
- generateContentRequest with TextPart for structured prompts
- Temperature 0.3, topK 16, maxOutputTokens 200 for concise output
- Graceful fallback to DiscoverySummaryGenerator on any failure
- Lazy model initialization with error logging via Kermit

The existing buildSessionPrompt() and buildPresetPrompt() methods in
DiscoverySummaryGenerator provide the prompt text. On unsupported
devices or fdroid builds, the provider falls through to the
deterministic algorithmic summary seamlessly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-18 07:38:34 -05:00
parent 4bf8aaf0a9
commit f6bfefd439
4 changed files with 114 additions and 51 deletions

View File

@@ -39,9 +39,8 @@ import kotlin.test.assertTrue
/**
* Migration coverage for discovery tables (D011).
*
* Verifies that the discovery schema (version 38→39 auto-migration) creates
* the expected tables, supports CRUD operations, enforces foreign key cascade
* behavior, and respects column defaults.
* Verifies that the discovery schema (version 38→39 auto-migration) creates the expected tables, supports CRUD
* operations, enforces foreign key cascade behavior, and respects column defaults.
*/
@RunWith(AndroidJUnit4::class)
@Config(sdk = [34])
@@ -71,12 +70,13 @@ class DiscoveryMigrationTest {
@Test
fun discoverySessionTable_insertAndRetrieve() = runTest {
val session = DiscoverySessionEntity(
timestamp = 1_000_000L,
presetsScanned = "LONG_FAST,SHORT_FAST",
homePreset = "LONG_FAST",
completionStatus = "complete",
)
val session =
DiscoverySessionEntity(
timestamp = 1_000_000L,
presetsScanned = "LONG_FAST,SHORT_FAST",
homePreset = "LONG_FAST",
completionStatus = "complete",
)
val id = discoveryDao.insertSession(session)
assertTrue(id > 0, "Insert should return positive auto-generated ID")
val loaded = discoveryDao.getSession(id)
@@ -88,14 +88,15 @@ class DiscoveryMigrationTest {
@Test
fun discoveryPresetResultTable_insertAndRetrieve() = runTest {
val sessionId = discoveryDao.insertSession(testSession())
val result = DiscoveryPresetResultEntity(
sessionId = sessionId,
presetName = "LONG_FAST",
dwellDurationSeconds = 30,
uniqueNodes = 5,
directNeighborCount = 3,
meshNeighborCount = 2,
)
val result =
DiscoveryPresetResultEntity(
sessionId = sessionId,
presetName = "LONG_FAST",
dwellDurationSeconds = 30,
uniqueNodes = 5,
directNeighborCount = 3,
meshNeighborCount = 2,
)
val resultId = discoveryDao.insertPresetResult(result)
assertTrue(resultId > 0)
val results = discoveryDao.getPresetResults(sessionId)
@@ -108,17 +109,18 @@ class DiscoveryMigrationTest {
fun discoveredNodeTable_insertAndRetrieve() = runTest {
val sessionId = discoveryDao.insertSession(testSession())
val presetId = discoveryDao.insertPresetResult(testPresetResult(sessionId))
val node = DiscoveredNodeEntity(
presetResultId = presetId,
nodeNum = 12345,
shortName = "TST",
longName = "Test Node",
neighborType = "direct",
latitude = 37.7749,
longitude = -122.4194,
snr = 8.5f,
rssi = -65,
)
val node =
DiscoveredNodeEntity(
presetResultId = presetId,
nodeNum = 12345,
shortName = "TST",
longName = "Test Node",
neighborType = "direct",
latitude = 37.7749,
longitude = -122.4194,
snr = 8.5f,
rssi = -65,
)
val nodeId = discoveryDao.insertDiscoveredNode(node)
assertTrue(nodeId > 0)
val nodes = discoveryDao.getDiscoveredNodes(presetId)
@@ -134,11 +136,7 @@ class DiscoveryMigrationTest {
@Test
fun sessionEntity_defaultValues() = runTest {
// Insert with only required fields — verify defaults
val session = DiscoverySessionEntity(
timestamp = 1L,
presetsScanned = "A",
homePreset = "A",
)
val session = DiscoverySessionEntity(timestamp = 1L, presetsScanned = "A", homePreset = "A")
val id = discoveryDao.insertSession(session)
val loaded = discoveryDao.getSession(id)!!
assertEquals(0, loaded.totalUniqueNodes)
@@ -263,4 +261,3 @@ class DiscoveryMigrationTest {
// endregion
}