Currently confirmed offline model download and communications are working. Next step is to build the JSON schema for local models to understand tool calling
This commit is contained in:
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2026-02-25T23:24:39.552459762Z">
|
<DropdownSelection timestamp="2026-02-28T03:12:27.182833316Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/michael/.android/avd/Pixel_8_API_35.avd" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=461ed66e" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.mmanningau.alice
|
package net.mmanningau.alice
|
||||||
|
|
||||||
|
import com.llamatik.library.platform.LlamaBridge // The correct Llamatik import!
|
||||||
import dev.langchain4j.data.message.AiMessage
|
import dev.langchain4j.data.message.AiMessage
|
||||||
import dev.langchain4j.data.message.ChatMessage
|
import dev.langchain4j.data.message.ChatMessage
|
||||||
import dev.langchain4j.data.message.SystemMessage
|
import dev.langchain4j.data.message.SystemMessage
|
||||||
@@ -8,9 +9,10 @@ import dev.langchain4j.model.chat.ChatLanguageModel
|
|||||||
import dev.langchain4j.model.output.Response
|
import dev.langchain4j.model.output.Response
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
import dev.langchain4j.agent.tool.ToolSpecification
|
||||||
|
|
||||||
class LlamaCppAdapter(private val modelPath: String) : ChatLanguageModel {
|
class LlamaCppAdapter(private val modelPath: String) : ChatLanguageModel {
|
||||||
|
|
||||||
// We will initialize the native C++ engine here once the model is downloaded
|
|
||||||
private var isEngineLoaded = false
|
private var isEngineLoaded = false
|
||||||
|
|
||||||
private fun getOrInitEngine() {
|
private fun getOrInitEngine() {
|
||||||
@@ -19,35 +21,43 @@ class LlamaCppAdapter(private val modelPath: String) : ChatLanguageModel {
|
|||||||
if (!modelFile.exists()) {
|
if (!modelFile.exists()) {
|
||||||
throw IllegalStateException("Model file not found at: $modelPath. Please download a model first.")
|
throw IllegalStateException("Model file not found at: $modelPath. Please download a model first.")
|
||||||
}
|
}
|
||||||
// TODO: In the next step, we will call Llamatik's native init function here
|
|
||||||
// to load the .gguf file into the device's memory.
|
// Boot the native C++ backend via Llamatik's Kotlin bridge
|
||||||
|
LlamaBridge.initGenerateModel(modelPath)
|
||||||
isEngineLoaded = true
|
isEngineLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun generate(messages: List<ChatMessage>): Response<AiMessage> {
|
override fun generate(messages: List<ChatMessage>): Response<AiMessage> {
|
||||||
// Ensure the engine is booted
|
|
||||||
getOrInitEngine()
|
getOrInitEngine()
|
||||||
|
|
||||||
// 1. Translation IN: Convert LangChain4j messages into a raw text prompt format
|
// 1. Translation IN: Format specifically for Qwen 2.5 (ChatML)
|
||||||
// Local models usually need a specific prompt template (like ChatML).
|
|
||||||
// We will build a basic conversational wrapper here.
|
|
||||||
val promptBuilder = java.lang.StringBuilder()
|
val promptBuilder = java.lang.StringBuilder()
|
||||||
for (message in messages) {
|
for (message in messages) {
|
||||||
when (message) {
|
when (message) {
|
||||||
is SystemMessage -> promptBuilder.append("System: ${message.text()}\n")
|
is SystemMessage -> promptBuilder.append("<|im_start|>system\n${message.text()}<|im_end|>\n")
|
||||||
is UserMessage -> promptBuilder.append("User: ${message.text()}\n")
|
is UserMessage -> promptBuilder.append("<|im_start|>user\n${message.text()}<|im_end|>\n")
|
||||||
is AiMessage -> promptBuilder.append("Alice: ${message.text()}\n")
|
is AiMessage -> promptBuilder.append("<|im_start|>assistant\n${message.text()}<|im_end|>\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
promptBuilder.append("Alice: ")
|
// Prompt the AI to start generating its response
|
||||||
|
promptBuilder.append("<|im_start|>assistant\n")
|
||||||
|
|
||||||
// 2. Execution: (Placeholder until we wire the specific Llamatik execution call)
|
// 2. Execution: Run it on the local hardware using Llamatik
|
||||||
// val responseText = LlamatikEngine.generate(promptBuilder.toString())
|
val responseText = LlamaBridge.generate(promptBuilder.toString())
|
||||||
val responseText = "This is a placeholder response from the local Llama.cpp engine!"
|
|
||||||
|
|
||||||
// 3. Translation OUT: Wrap the raw string back into LangChain4j's format
|
// 3. Translation OUT: Clean up any trailing ChatML tags the engine might leave behind
|
||||||
val finalAiMessage = AiMessage(responseText)
|
val cleanResponse = responseText.replace("<|im_end|>", "").trim()
|
||||||
return Response.from(finalAiMessage)
|
|
||||||
|
return Response.from(AiMessage(cleanResponse))
|
||||||
|
}
|
||||||
|
// This catches LangChain4j when it tries to send tools to our local engine
|
||||||
|
override fun generate(
|
||||||
|
messages: List<ChatMessage>,
|
||||||
|
toolSpecifications: List<ToolSpecification>
|
||||||
|
): Response<AiMessage> {
|
||||||
|
// For Phase 1, we simply ignore the tools and route it to our standard text generator
|
||||||
|
// so we can prove the local GPU engine is successfully generating tokens!
|
||||||
|
return generate(messages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import java.time.Duration
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
object LlmManager {
|
object LlmManager {
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ object LlmManager {
|
|||||||
|
|
||||||
// Initialization now makes the dao optional so the UI can safely call it!
|
// Initialization now makes the dao optional so the UI can safely call it!
|
||||||
fun initialize(
|
fun initialize(
|
||||||
dao: ChatDao?, mode: String, url: String, modelName: String, apiKey: String, systemPrompt: String
|
context: Context,dao: ChatDao?, mode: String, url: String, modelName: String, apiKey: String, systemPrompt: String
|
||||||
) {
|
) {
|
||||||
// Only update the DAO if one was passed in (like on app boot)
|
// Only update the DAO if one was passed in (like on app boot)
|
||||||
if (dao != null) {
|
if (dao != null) {
|
||||||
@@ -50,8 +51,10 @@ object LlmManager {
|
|||||||
.logRequests(true)
|
.logRequests(true)
|
||||||
.logResponses(true)
|
.logResponses(true)
|
||||||
.build()
|
.build()
|
||||||
} else {
|
} else if (mode == "Local") {
|
||||||
chatModel = null // MLC Engine goes here later!
|
// NEW: Grab the absolute path from the registry and boot the middleman!
|
||||||
|
val fullPath = ModelRegistry.getModelPath(context, modelName)
|
||||||
|
chatModel = LlamaCppAdapter(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Database Startup Logic
|
// Database Startup Logic
|
||||||
@@ -102,7 +105,7 @@ object LlmManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun chat(userText: String): String {
|
fun chat(userText: String): String {
|
||||||
if (currentMode == "MLC") return "System: MLC LLM On-Device engine is selected but not yet installed."
|
if (currentMode == "Local" && chatModel == null) return "System: Llamatik Ollama On-Device engine is selected but not yet installed."
|
||||||
val currentModel = chatModel ?: return "Error: LLM engine not initialized."
|
val currentModel = chatModel ?: return "Error: LLM engine not initialized."
|
||||||
|
|
||||||
// If the history size is 1, it means only the System prompt exists. This is the first message!
|
// If the history size is 1, it means only the System prompt exists. This is the first message!
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ fun MainChatScreen() {
|
|||||||
val response = LlmManager.chat(userText)
|
val response = LlmManager.chat(userText)
|
||||||
messages = messages + ChatMessage(response, false)
|
messages = messages + ChatMessage(response, false)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
messages = messages + ChatMessage("Connection Error: Is the local LLM server running?", false)
|
messages = messages + ChatMessage("System Error: ${e.message}", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,7 +387,7 @@ fun SettingsScreen(onBackClicked: () -> Unit) {
|
|||||||
.putString("systemPrompt", systemPrompt)
|
.putString("systemPrompt", systemPrompt)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
LlmManager.initialize(null, llmMode, llmUrl, modelName, apiKey, systemPrompt)
|
LlmManager.initialize(context, null, llmMode, llmUrl, modelName, apiKey, systemPrompt)
|
||||||
SkillManager.updateDirectory(skillsPath)
|
SkillManager.updateDirectory(skillsPath)
|
||||||
|
|
||||||
onBackClicked()
|
onBackClicked()
|
||||||
@@ -481,6 +481,13 @@ fun ModelManagerScreen(onBackClicked: () -> Unit) {
|
|||||||
// Save the exact filename so LlmManager knows which one to boot up
|
// Save the exact filename so LlmManager knows which one to boot up
|
||||||
prefs.edit().putString("modelName", model.fileName).apply()
|
prefs.edit().putString("modelName", model.fileName).apply()
|
||||||
activeModelName = model.fileName
|
activeModelName = model.fileName
|
||||||
|
|
||||||
|
// NEW: Hot-reload the LlmManager instantly!
|
||||||
|
val mode = prefs.getString("llmMode", "Local") ?: "Local"
|
||||||
|
val url = prefs.getString("llmUrl", "") ?: ""
|
||||||
|
val apiKey = prefs.getString("apiKey", "") ?: ""
|
||||||
|
val prompt = prefs.getString("systemPrompt", "You are a helpful AI assistant.") ?: "You are a helpful AI assistant."
|
||||||
|
LlmManager.initialize(context, null, mode, url, model.fileName, apiKey, prompt)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
|||||||
@@ -38,6 +38,6 @@ class AliceApp : Application() {
|
|||||||
).allowMainThreadQueries().build() // We use allowMainThreadQueries for immediate boot loading
|
).allowMainThreadQueries().build() // We use allowMainThreadQueries for immediate boot loading
|
||||||
|
|
||||||
// Pass the DAO into the manager!
|
// Pass the DAO into the manager!
|
||||||
LlmManager.initialize(db.chatDao(), savedMode, savedUrl, savedModel, savedApiKey, savedSystemPrompt)
|
LlmManager.initialize(this,db.chatDao(), savedMode, savedUrl, savedModel, savedApiKey, savedSystemPrompt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user