Muse — a self-aware AI assistant that lives on your phone.
Muse pairs a local llama.cpp inference engine with memory, actions, LAN AI sharing, and a clean Jetpack Compose UI — so your data never leaves the device unless you choose to host it on your own network.
What is Muse?
Muse is an Android assistant powered by local GGUF language models. It ships with a full-featured chat UI, persistent memory, action parsing, device-stats monitoring, and a peer-to-peer LAN mode that lets one phone serve AI answers to others on the same network — no cloud required.
A proactive, personal AI.
Five pillars guide every design decision.
Tech stack
Modern Android, MVVM, fully Kotlin. Native inference via JNI + llama.cpp.
12 features, one app.
Hover a card for a glow preview. Click to jump to its detailed documentation.
Flow<String>.[ACTION:…] tags → reminders, alarms, timers, tasks.PrefsManager.Local LLM Inference
A streaming, model-aware native inference layer bridged to Kotlin via JNI.
1 Engine
The Kotlin LLMEngine interface exposes a single streaming method.
NativeEngine implements it by calling into jni_interface.cpp,
which drives llama.cpp.
interface LLMEngine { fun generateStream( prompt: String, opts: GenOptions, onStatus: (Status) -> Unit = {} ): Flow<String> }
Model families & prompt formats
- Gemma Instruct →
<start_of_turn>system / user / model - Qwen (ChatML) →
<|im_start|>system / user / assistant - LLaMA / Tiny →
### SYSTEM / ### CONVERSATION / ### RESPONSE - Universal fallback uses the
### SYSTEMformat - Detection uses word-boundary regex via
isGemmaInstruct()
Sampling options
GenOptions carries maxNewTokens, temperature,
topP, topK, repeatPenalty,
parseSpecial and addSpecial. The right preset is chosen via
chooseGenOptionsForModel(modelName) which reads the user's saved preset
from PresetStore.
Load lifecycle
ModelManager (Hilt-scoped singleton) owns loading and exposes
loadState: StateFlow<ModelLoadState> with states
Idle · Loading · Ready(modelName) · Error.
Model Download System
A foreground service that survives the system killing apps, with per-model notifications.
ModelDownloadServiceusesPARTIAL_WAKE_LOCK— never sleeps mid-download- HTTP
Rangeheader support — resumes from.partfiles - One notification per model (unique ID), shows speed, bytes, %
- Token expiry (401/403) → deletes
.partand prompts re-download - External GGUF import through Storage Access Framework
- After two failures, the app shows a copyable URL
- Checks available RAM and warns before starting oversized downloads
- Pause + Stop buttons in notification and UI
DownloadProgressBus— aSharedFlowbridging Service → ViewModel
Chat System
Streaming token UI, blank-token cutoff, think-tag stripping, ChatML artifact sanitization.
1 ChatViewModel state
chatMessages: StateFlow<List<ChatMessage>>currentStreamingReply: StateFlow<String?>— live token streamisGenerating: StateFlow<Boolean>promptEval: StateFlow<Pair<Int,Int>?>— shown in top barchatHistory: StateFlow<List<ChatSession>>modelLoadState— delegated fromModelManager
2 sendMessage() pipeline
// 1. Persist user msg to Room + state // 2. Route to LAN if receiver mode // 3. Run memory extraction // 4. Auto-name session (first 40 chars) // 5. Assemble context + notification block // 6. buildPromptForModel(...) // 7. chooseGenOptionsForModel(...) // 8. InferenceService.start() // foreground notif // 9. engine.generateStream() .takeWhile { !threeBlanksInARow() } // 10. stripThinkTags(tok) // per-token // 11. ActionParser + ActionExecutor // 12. Save bot message to Room // 13. InferenceService.stop()
3 Post-processing
stripThinkTags(text)— removes<think>…</think>; handles streaming unclosed tagschatMlStopIndex(text)— first ChatML / EOS marker locationstripChatMlArtifacts(text)— truncates at marker & regex-strips stray<|…|>
isModelAvailable() returns true if a local .gguf exists
or LAN receiver mode is active — so NewChatScreen never redirects users
who are connected to a host.Memory System
Selective fact extraction. What matters stays; chat transcripts don't linger.
processUserMessage(msg)— extracts and saves facts from user inputprocessAiReply(reply)— reads memory tags from AI responsesgetMemorySummary()— compact string injected into every prompt's system block- Stored as
UserMemoryRoom entities with anactive/inactiveflag - User can inspect & delete memories in
MemoryScreen - Master toggle:
memory_enabledpref (default on)
Actions System
The AI can do things, not just talk — entirely on-device.
ActionParser.parse(reply)— extracts[ACTION:…]tags, returns(cleanReply, List<Action>)ActionExecutor.execute(actions, sessionId, ctx)— runs them locally:- Create reminder / alarm
- Set timer
- Save task
- Results are pushed through
_actionEvent: SharedFlow<String>→ a toast
LAN AI Sharing
One phone loads a big model, the others query it over Wi-Fi. No cloud.
1 Protocol
Line-delimited TCP on port 8765.
2 Host device
LanHostService— foreground service, survives backgrounding- Notification: "N connected · X messages served · Code: XXXXXX"
onClientChangecallback → saves toPREF_CONNECTED_CLIENTS- ViewModel registers
OnSharedPreferenceChangeListenerfor zero-latency UI
3 Receiver device
connectReceiver()— auth test only, disconnects after OK- Connection-per-message on inference (no long-lived socket)
sendMessageViaLan()mirrorssendMessage()exactly — same memory, context, session naming, post-processing- Uses
buildPromptForModel(hostModelName)— format matches host's model family - Uses
chooseGenOptionsForModel(hostModelName)— same sampling params as local
4 Host discovery
scanForHosts() parallel-connects to all 254 IPs in the subnet with a
300 ms timeout per socket. Found hosts are shown as tappable rows on the Receive tab.
Device Stats Monitor
Tiny text in the chat top bar that shows you what your silicon is doing.
- Polls every 2 seconds
- CPU% from
/proc/stat - Temperature from
/sys/class/thermal - GPU% from
/sys/class/kgsl - Exposed as
statsFlow(): Flow<DeviceStats> - Rendered in
ChatScreen's top bar as tertiary 10 sp text — e.g.CPU 34% · 42°C
Backup & Restore
Full JSON dump that never needs storage permissions.
- Serializes
ChatSession,ChatMessage,UserMemory,UserProfilevia Gson - Writes to
cacheDir/exports/(must matchfile_provider_paths.xml) - Shares via
ACTION_SEND+FileProvider— no WRITE permission needed importBackup(uri)reads via SAFContentResolverand upserts recordsBackupStatecarries bothexportErrorandimportError
file_provider_paths.xml must be
<cache-path path="exports/">. Writing anywhere else silently crashes
FileProvider.getUriForFile().Settings System
Everything user-facing lives in PrefsManager.
- Backed by
SharedPreferences(file:app_prefs) - Top-level screen groups: Appearance · AI Behaviour · Data Management · LAN Hosting
AdvancedSettingsScreentoggles which rows show up in the main settings list
Database
Room. Four tables. Simple on purpose.
| Table | Purpose | Key DAO methods |
|---|---|---|
ChatSession | Conversation envelope — name, timestamps | getAllSessions() |
ChatMessage | Rows: user & bot messages with session FK | getMessagesForSession(id), getMessageCount() |
UserMemory | Facts with active flag | getAllActiveMemories(), activeMemoryCount() |
UserProfile | User name, assistant name, preferences | getProfile(), insertProfile() |
Permissions & Notifications
Only what the features actually need.
Manifest permissions
Notification channels
lan_host— LAN hosting service (IMPORTANCE_LOW)- Model download — one per model
- Inference service — running indicator
FileProvider
Authority: ${applicationId}.fileprovider — paths in
res/xml/file_provider_paths.xml.
Theme system
LucyColorPalette (not LucyColorScheme) accessed via
LucyColors.current. Both modes toggled through SettingsViewModel.themeMode.
Animation gallery
Everything in the app that moves is here — hover to see it live. All are CSS-driven,
GPU-friendly, and respect prefers-reduced-motion.
Pref keys quick reference
All persistent prefs are stored in the app_prefs SharedPreferences file.
| Key | Owner | Values |
|---|---|---|
lan_is_receiver | LanHostViewModel | "true" / "false" |
lan_is_hosting | LanHostViewModel | "true" / "false" |
lan_pairing_code | LanHostViewModel | 6-digit string |
lan_receiver_host_ip | LanHostViewModel | IP string |
lan_receiver_code | LanHostViewModel | 6-digit string |
lan_host_message_count | LanHostService | Int |
lan_host_connected_clients | LanHostService | Int |
memory_enabled | SettingsViewModel | "true" / "false" |
context_awareness_enabled | SettingsViewModel | "true" / "false" |
assistant_name | SettingsViewModel | String |