Skip to content

Commit 567ce8d

Browse files
committed
LLM Heurists processing
1 parent 348a8ba commit 567ce8d

29 files changed

Lines changed: 3393 additions & 1001 deletions

app/proguard-rules.pro

Lines changed: 361 additions & 3 deletions
Large diffs are not rendered by default.
-636 Bytes
Binary file not shown.

app/src/main/assets/oui.csv

Lines changed: 621 additions & 621 deletions
Large diffs are not rendered by default.

app/src/main/java/com/flockyou/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,8 @@ fun AppNavigation(
343343
onNavigateToUltrasonicDetection = { navController.navigate("ultrasonic_detection") },
344344
onNavigateToSatelliteDetection = { navController.navigate("satellite_detection") },
345345
onNavigateToWifiSecurity = { navController.navigate("wifi_security") },
346-
onNavigateToServiceHealth = { navController.navigate("service_health") }
346+
onNavigateToServiceHealth = { navController.navigate("service_health") },
347+
onNavigateToActiveProbes = { navController.navigate("active_probes") }
347348
)
348349
}
349350
composable("map") {
@@ -362,7 +363,6 @@ fun AppNavigation(
362363
onNavigateToAllDetections = { navController.navigate("all_detections") },
363364
onNavigateToAiSettings = { navController.navigate("ai_settings") },
364365
onNavigateToServiceHealth = { navController.navigate("service_health") },
365-
onNavigateToFlipperSettings = { navController.navigate("flipper_settings") },
366366
onNavigateToTestMode = { navController.navigate("test_mode") }
367367
)
368368
}

app/src/main/java/com/flockyou/ai/DetectionAnalyzer.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ class DetectionAnalyzer @Inject constructor(
126126
private val llmEngineManager: LlmEngineManager,
127127
private val detectionRegistry: DetectionRegistry,
128128
private val feedbackTracker: AnalysisFeedbackTracker,
129-
private val ruleBasedAnalyzer: RuleBasedAnalyzer
129+
private val ruleBasedAnalyzer: RuleBasedAnalyzer,
130+
private val enrichedDataCache: EnrichedDataCache
130131
) {
131132
companion object {
132133
private const val TAG = "DetectionAnalyzer"
@@ -1282,8 +1283,10 @@ class DetectionAnalyzer @Inject constructor(
12821283
val shouldTryLlm = (activeEngine != LlmEngine.RULE_BASED) || anyLlmReady
12831284

12841285
if (shouldTryLlm) {
1285-
Log.d(TAG, "Attempting LLM analysis (activeEngine=$activeEngine, anyLlmReady=$anyLlmReady)")
1286-
val llmResult = llmEngineManager.analyzeDetection(detection)
1286+
// Retrieve enriched heuristics data for this detection if available
1287+
val enrichedData = enrichedDataCache.get(detection.id)
1288+
Log.d(TAG, "Attempting LLM analysis (activeEngine=$activeEngine, anyLlmReady=$anyLlmReady, hasEnrichedData=${enrichedData != null})")
1289+
val llmResult = llmEngineManager.analyzeDetection(detection, enrichedData)
12871290

12881291
if (llmResult.success) {
12891292
Log.i(TAG, "LLM analysis succeeded! Model: ${llmResult.modelUsed}, Response length: ${llmResult.analysis?.length ?: 0}")
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package com.flockyou.ai
2+
3+
import android.util.Log
4+
import java.util.concurrent.ConcurrentHashMap
5+
import javax.inject.Inject
6+
import javax.inject.Singleton
7+
8+
/**
9+
* In-memory cache for enriched detector data associated with detections.
10+
*
11+
* When monitors (Cellular, GNSS, WiFi, Ultrasonic, etc.) create detections, they also
12+
* store the detailed heuristics analysis here. When the LLM analyzes a detection, it
13+
* retrieves this enriched data to provide more accurate and contextual analysis.
14+
*
15+
* Features:
16+
* - Thread-safe using ConcurrentHashMap
17+
* - Time-based expiry (configurable TTL)
18+
* - Size-limited to prevent memory issues
19+
* - Automatic cleanup of expired entries
20+
*/
21+
@Singleton
22+
class EnrichedDataCache @Inject constructor() {
23+
24+
companion object {
25+
private const val TAG = "EnrichedDataCache"
26+
private const val DEFAULT_TTL_MS = 5 * 60 * 1000L // 5 minutes
27+
private const val MAX_CACHE_SIZE = 200
28+
private const val CLEANUP_THRESHOLD = 250
29+
}
30+
31+
private data class CacheEntry(
32+
val data: EnrichedDetectorData,
33+
val timestamp: Long = System.currentTimeMillis(),
34+
val ttlMs: Long = DEFAULT_TTL_MS
35+
) {
36+
fun isExpired(): Boolean = System.currentTimeMillis() - timestamp > ttlMs
37+
}
38+
39+
private val cache = ConcurrentHashMap<String, CacheEntry>()
40+
41+
/**
42+
* Store enriched data for a detection.
43+
*
44+
* @param detectionId The unique ID of the detection
45+
* @param data The enriched detector data (CellularAnomalyAnalysis, GnssAnomalyAnalysis, etc.)
46+
* @param ttlMs Optional custom TTL in milliseconds (default 5 minutes)
47+
*/
48+
fun put(detectionId: String, data: EnrichedDetectorData, ttlMs: Long = DEFAULT_TTL_MS) {
49+
// Clean up if cache is getting too large
50+
if (cache.size > CLEANUP_THRESHOLD) {
51+
cleanup()
52+
}
53+
54+
cache[detectionId] = CacheEntry(data, ttlMs = ttlMs)
55+
Log.d(TAG, "Cached enriched data for detection: $detectionId (type: ${data.javaClass.simpleName})")
56+
}
57+
58+
/**
59+
* Retrieve enriched data for a detection.
60+
* Returns null if not found or expired.
61+
*
62+
* @param detectionId The unique ID of the detection
63+
* @return The enriched data, or null if not found/expired
64+
*/
65+
fun get(detectionId: String): EnrichedDetectorData? {
66+
val entry = cache[detectionId]
67+
68+
if (entry == null) {
69+
Log.d(TAG, "No enriched data found for detection: $detectionId")
70+
return null
71+
}
72+
73+
if (entry.isExpired()) {
74+
cache.remove(detectionId)
75+
Log.d(TAG, "Enriched data expired for detection: $detectionId")
76+
return null
77+
}
78+
79+
Log.d(TAG, "Retrieved enriched data for detection: $detectionId (type: ${entry.data.javaClass.simpleName})")
80+
return entry.data
81+
}
82+
83+
/**
84+
* Store cellular anomaly analysis for a detection.
85+
*/
86+
fun putCellular(detectionId: String, analysis: com.flockyou.service.CellularMonitor.CellularAnomalyAnalysis) {
87+
put(detectionId, EnrichedDetectorData.Cellular(analysis))
88+
}
89+
90+
/**
91+
* Store GNSS anomaly analysis for a detection.
92+
*/
93+
fun putGnss(detectionId: String, analysis: com.flockyou.monitoring.GnssSatelliteMonitor.GnssAnomalyAnalysis) {
94+
put(detectionId, EnrichedDetectorData.Gnss(analysis))
95+
}
96+
97+
/**
98+
* Store ultrasonic beacon analysis for a detection.
99+
*/
100+
fun putUltrasonic(detectionId: String, analysis: com.flockyou.service.UltrasonicDetector.BeaconAnalysis) {
101+
put(detectionId, EnrichedDetectorData.Ultrasonic(analysis))
102+
}
103+
104+
/**
105+
* Store WiFi following analysis for a detection.
106+
*/
107+
fun putWifiFollowing(detectionId: String, analysis: com.flockyou.service.RogueWifiMonitor.FollowingNetworkAnalysis) {
108+
put(detectionId, EnrichedDetectorData.WifiFollowing(analysis))
109+
}
110+
111+
/**
112+
* Store satellite/NTN enriched data for a detection.
113+
*/
114+
fun putSatellite(
115+
detectionId: String,
116+
detectorType: String,
117+
metadata: Map<String, String>,
118+
signalCharacteristics: Map<String, String>,
119+
riskIndicators: List<String>,
120+
timestamp: Long = System.currentTimeMillis()
121+
) {
122+
put(detectionId, EnrichedDetectorData.Satellite(
123+
detectorType = detectorType,
124+
metadata = metadata,
125+
signalCharacteristics = signalCharacteristics,
126+
riskIndicators = riskIndicators,
127+
timestamp = timestamp
128+
))
129+
}
130+
131+
/**
132+
* Remove enriched data for a detection.
133+
*/
134+
fun remove(detectionId: String) {
135+
cache.remove(detectionId)
136+
}
137+
138+
/**
139+
* Clear all cached data.
140+
*/
141+
fun clear() {
142+
cache.clear()
143+
Log.d(TAG, "Cache cleared")
144+
}
145+
146+
/**
147+
* Get the current cache size.
148+
*/
149+
fun size(): Int = cache.size
150+
151+
/**
152+
* Clean up expired entries and trim to max size.
153+
*/
154+
fun cleanup() {
155+
val expiredCount = cache.entries.count { it.value.isExpired() }
156+
cache.entries.removeIf { it.value.isExpired() }
157+
158+
// If still over max size, remove oldest entries
159+
if (cache.size > MAX_CACHE_SIZE) {
160+
val toRemove = cache.entries
161+
.sortedBy { it.value.timestamp }
162+
.take(cache.size - MAX_CACHE_SIZE)
163+
.map { it.key }
164+
165+
toRemove.forEach { cache.remove(it) }
166+
Log.d(TAG, "Removed ${toRemove.size} oldest entries to maintain max size")
167+
}
168+
169+
if (expiredCount > 0) {
170+
Log.d(TAG, "Cleaned up $expiredCount expired entries, current size: ${cache.size}")
171+
}
172+
}
173+
174+
/**
175+
* Get statistics about the cache.
176+
*/
177+
fun getStats(): CacheStats {
178+
val now = System.currentTimeMillis()
179+
var expiredCount = 0
180+
var cellularCount = 0
181+
var gnssCount = 0
182+
var ultrasonicCount = 0
183+
var wifiCount = 0
184+
var satelliteCount = 0
185+
186+
cache.values.forEach { entry ->
187+
if (entry.isExpired()) expiredCount++
188+
when (entry.data) {
189+
is EnrichedDetectorData.Cellular -> cellularCount++
190+
is EnrichedDetectorData.Gnss -> gnssCount++
191+
is EnrichedDetectorData.Ultrasonic -> ultrasonicCount++
192+
is EnrichedDetectorData.WifiFollowing -> wifiCount++
193+
is EnrichedDetectorData.Satellite -> satelliteCount++
194+
}
195+
}
196+
197+
return CacheStats(
198+
totalEntries = cache.size,
199+
expiredEntries = expiredCount,
200+
cellularEntries = cellularCount,
201+
gnssEntries = gnssCount,
202+
ultrasonicEntries = ultrasonicCount,
203+
wifiEntries = wifiCount,
204+
satelliteEntries = satelliteCount
205+
)
206+
}
207+
208+
data class CacheStats(
209+
val totalEntries: Int,
210+
val expiredEntries: Int,
211+
val cellularEntries: Int,
212+
val gnssEntries: Int,
213+
val ultrasonicEntries: Int,
214+
val wifiEntries: Int,
215+
val satelliteEntries: Int
216+
)
217+
}

app/src/main/java/com/flockyou/ai/GeminiNanoClient.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -489,8 +489,14 @@ class GeminiNanoClient @Inject constructor(
489489
/**
490490
* Generate analysis for a detection using Gemini Nano.
491491
* All processing happens on-device via the NPU.
492+
*
493+
* @param detection The detection to analyze
494+
* @param enrichedData Optional enriched heuristics data for more accurate and contextual analysis
492495
*/
493-
suspend fun analyzeDetection(detection: Detection): AiAnalysisResult = withContext(Dispatchers.IO) {
496+
suspend fun analyzeDetection(
497+
detection: Detection,
498+
enrichedData: EnrichedDetectorData? = null
499+
): AiAnalysisResult = withContext(Dispatchers.IO) {
494500
val startTime = System.currentTimeMillis()
495501

496502
val model = generativeModel
@@ -505,9 +511,9 @@ class GeminiNanoClient @Inject constructor(
505511

506512
try {
507513
withTimeout(INFERENCE_TIMEOUT_MS) {
508-
// Build the prompt for surveillance device analysis
509-
val prompt = buildAnalysisPrompt(detection)
510-
Log.d(TAG, "Generating analysis with Gemini Nano (prompt length: ${prompt.length})")
514+
// Build the prompt for surveillance device analysis with enriched heuristics data
515+
val prompt = buildAnalysisPrompt(detection, enrichedData)
516+
Log.d(TAG, "Generating analysis with Gemini Nano (prompt length: ${prompt.length}, hasEnrichedData=${enrichedData != null})")
511517

512518
// Generate content using ML Kit GenAI
513519
val response = model.generateContent(prompt)
@@ -655,9 +661,10 @@ class GeminiNanoClient @Inject constructor(
655661
* Build a prompt for surveillance device analysis.
656662
* Gemini Nano is powerful enough for verbose prompts, but uses structured output for consistency.
657663
*/
658-
private fun buildAnalysisPrompt(detection: Detection): String {
664+
private fun buildAnalysisPrompt(detection: Detection, enrichedData: EnrichedDetectorData? = null): String {
659665
// Use structured output prompt for consistent, parseable results
660-
return PromptTemplates.buildStructuredOutputPrompt(detection, null)
666+
// Include enriched heuristics data when available for more accurate analysis
667+
return PromptTemplates.buildStructuredOutputPrompt(detection, enrichedData)
661668
}
662669

663670
/**

app/src/main/java/com/flockyou/ai/LlmEngineManager.kt

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,15 @@ class LlmEngineManager @Inject constructor(
355355
* Analyze a detection using the best available engine with automatic fallback.
356356
*
357357
* Includes timeout protection to prevent indefinite hangs during AI analysis.
358+
*
359+
* @param detection The detection to analyze
360+
* @param enrichedData Optional enriched heuristics data from the detector that created this detection.
361+
* When provided, enables more accurate and contextual LLM analysis.
358362
*/
359-
suspend fun analyzeDetection(detection: Detection): AiAnalysisResult = withContext(Dispatchers.IO) {
363+
suspend fun analyzeDetection(
364+
detection: Detection,
365+
enrichedData: EnrichedDetectorData? = null
366+
): AiAnalysisResult = withContext(Dispatchers.IO) {
360367
val startTime = System.currentTimeMillis()
361368

362369
// Ensure initialized
@@ -382,7 +389,7 @@ class LlmEngineManager @Inject constructor(
382389
// Wrap entire analysis in timeout to prevent indefinite hangs
383390
val result = withTimeoutOrNull(ANALYSIS_TIMEOUT_MS) {
384391
// Try current active engine first
385-
var analysisResult = tryAnalyzeWithEngine(currentEngine, detection)
392+
var analysisResult = tryAnalyzeWithEngine(currentEngine, detection, enrichedData)
386393
Log.d(TAG, " tryAnalyzeWithEngine($currentEngine) returned: success=${analysisResult?.success}, error=${analysisResult?.error}")
387394

388395
// If failed, try fallback chain
@@ -391,7 +398,7 @@ class LlmEngineManager @Inject constructor(
391398

392399
for (engine in getFallbackOrder(currentEngine)) {
393400
Log.d(TAG, " Trying fallback engine: $engine")
394-
analysisResult = tryAnalyzeWithEngine(engine, detection)
401+
analysisResult = tryAnalyzeWithEngine(engine, detection, enrichedData)
395402
Log.d(TAG, " Result: success=${analysisResult?.success}, error=${analysisResult?.error}")
396403
if (analysisResult != null && analysisResult.success) {
397404
Log.i(TAG, "Fallback to $engine succeeded!")
@@ -435,19 +442,24 @@ class LlmEngineManager @Inject constructor(
435442

436443
/**
437444
* Try to analyze with a specific engine.
445+
*
446+
* @param engine The LLM engine to use
447+
* @param detection The detection to analyze
448+
* @param enrichedData Optional enriched heuristics data for more accurate analysis
438449
*/
439450
private suspend fun tryAnalyzeWithEngine(
440451
engine: LlmEngine,
441-
detection: Detection
452+
detection: Detection,
453+
enrichedData: EnrichedDetectorData? = null
442454
): AiAnalysisResult? {
443455
return try {
444456
when (engine) {
445457
LlmEngine.GEMINI_NANO -> {
446458
val isReady = geminiNanoClient.isReady()
447-
Log.d(TAG, "tryAnalyzeWithEngine(GEMINI_NANO): isReady=$isReady")
459+
Log.d(TAG, "tryAnalyzeWithEngine(GEMINI_NANO): isReady=$isReady, hasEnrichedData=${enrichedData != null}")
448460
if (isReady) {
449-
Log.d(TAG, "Calling geminiNanoClient.analyzeDetection()...")
450-
val result = geminiNanoClient.analyzeDetection(detection)
461+
Log.d(TAG, "Calling geminiNanoClient.analyzeDetection() with enrichedData=${enrichedData?.javaClass?.simpleName}")
462+
val result = geminiNanoClient.analyzeDetection(detection, enrichedData)
451463
Log.d(TAG, "geminiNanoClient.analyzeDetection() returned: success=${result.success}, modelUsed=${result.modelUsed}, error=${result.error}")
452464
if (result.success) {
453465
recordSuccess(LlmEngine.GEMINI_NANO)
@@ -464,7 +476,8 @@ class LlmEngineManager @Inject constructor(
464476
}
465477
LlmEngine.MEDIAPIPE -> {
466478
if (mediaPipeLlmClient.isReady()) {
467-
val result = mediaPipeLlmClient.analyzeDetection(detection, _currentModel)
479+
Log.d(TAG, "Calling mediaPipeLlmClient.analyzeDetection() with enrichedData=${enrichedData?.javaClass?.simpleName}")
480+
val result = mediaPipeLlmClient.analyzeDetection(detection, _currentModel, enrichedData)
468481
if (result.success) {
469482
recordSuccess(LlmEngine.MEDIAPIPE)
470483
} else {

0 commit comments

Comments
 (0)