UI and Skills settings update to now allow for on the fly model changes and the skills directory to be changed. Both confirmed as working
This commit is contained in:
@@ -11,58 +11,54 @@ import java.time.Duration
|
||||
|
||||
object LlmManager {
|
||||
|
||||
private const val LOCAL_LLM_URL = "http://10.0.2.2:11434/v1"
|
||||
// 1. We make the model variable so we can rebuild it when settings change
|
||||
private var chatModel: ChatLanguageModel? = null
|
||||
|
||||
private val chatModel: ChatLanguageModel = OpenAiChatModel.builder()
|
||||
.baseUrl(LOCAL_LLM_URL)
|
||||
.apiKey("dummy-key")
|
||||
.modelName("qwen3:8b") // Ensure this matches your Ollama model!
|
||||
.timeout(Duration.ofMinutes(3))
|
||||
.maxRetries(0)
|
||||
.logRequests(true)
|
||||
.logResponses(true)
|
||||
.build()
|
||||
|
||||
// We manually maintain the conversation state now
|
||||
private val chatHistory = mutableListOf<ChatMessage>(
|
||||
SystemMessage("You are Alice, a highly capable local AI assistant. You provide concise, direct answers.")
|
||||
)
|
||||
|
||||
fun chat(userText: String): String {
|
||||
// 1. Add user's new message to memory
|
||||
chatHistory.add(UserMessage(userText))
|
||||
// 2. The UI will call this when you hit "Save"
|
||||
fun initialize(url: String, modelName: String) {
|
||||
chatModel = OpenAiChatModel.builder()
|
||||
.baseUrl(url)
|
||||
.apiKey("dummy-key")
|
||||
.modelName(modelName)
|
||||
.timeout(Duration.ofMinutes(3))
|
||||
.maxRetries(0)
|
||||
.logRequests(true)
|
||||
.logResponses(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
// 2. Fetch all current tools dynamically from the folder
|
||||
fun chat(userText: String): String {
|
||||
// Safety check in case the model hasn't been built yet
|
||||
val currentModel = chatModel ?: return "Error: LLM engine not initialized. Please check Settings."
|
||||
|
||||
chatHistory.add(UserMessage(userText))
|
||||
val toolSpecs = SkillManager.loadSkills()
|
||||
|
||||
// 3. Send the entire history and the tools to the LLM
|
||||
var response = chatModel.generate(chatHistory, toolSpecs)
|
||||
// Use the active model
|
||||
var response = currentModel.generate(chatHistory, toolSpecs)
|
||||
var aiMessage: AiMessage = response.content()
|
||||
chatHistory.add(aiMessage)
|
||||
|
||||
// 4. THE EXECUTION LOOP
|
||||
// If the LLM decides it needs to run a tool, it will set hasToolExecutionRequests() to true.
|
||||
while (aiMessage.hasToolExecutionRequests()) {
|
||||
|
||||
for (request in aiMessage.toolExecutionRequests()) {
|
||||
val toolName = request.name()
|
||||
val arguments = request.arguments()
|
||||
|
||||
// Send the request across the bridge to Python!
|
||||
val toolResult = SkillManager.executeSkill(toolName, arguments)
|
||||
|
||||
// Package the result and add it to the memory
|
||||
val toolMessage = ToolExecutionResultMessage(request.id(), toolName, toolResult)
|
||||
chatHistory.add(toolMessage)
|
||||
}
|
||||
|
||||
// Ping the LLM again with the new tool results so it can formulate a final answer
|
||||
response = chatModel.generate(chatHistory, toolSpecs)
|
||||
response = currentModel.generate(chatHistory, toolSpecs)
|
||||
aiMessage = response.content()
|
||||
chatHistory.add(aiMessage)
|
||||
}
|
||||
|
||||
// 5. Return the final conversational text to the UI
|
||||
return aiMessage.text()
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
@@ -18,9 +20,12 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
// 1. A simple data class to hold our messages
|
||||
data class ChatMessage(val text: String, val isUser: Boolean)
|
||||
|
||||
@@ -72,7 +77,7 @@ fun MainChatScreen() {
|
||||
|
||||
// Navigation item for Skills Library
|
||||
NavigationDrawerItem(
|
||||
label = { Text("Skills Library") },
|
||||
label = { Text("Settings & Skills") },
|
||||
selected = currentScreen == "Skills",
|
||||
icon = { Icon(Icons.Default.Build, contentDescription = "Skills") },
|
||||
onClick = {
|
||||
@@ -176,7 +181,7 @@ fun MainChatScreen() {
|
||||
|
||||
} else if (currentScreen == "Skills") {
|
||||
// LOAD THE NEW SCREEN
|
||||
SkillsLibraryScreen(
|
||||
SettingsScreen(
|
||||
onBackClicked = { currentScreen = "Chat" }
|
||||
)
|
||||
}
|
||||
@@ -212,14 +217,20 @@ fun ChatBubble(message: ChatMessage) {
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SkillsLibraryScreen(onBackClicked: () -> Unit) {
|
||||
// Grab the path from our Manager
|
||||
val skillsPath = SkillManager.skillsDirectory?.absolutePath ?: "Path unavailable"
|
||||
fun SettingsScreen(onBackClicked: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val prefs = context.getSharedPreferences("AlicePrefs", Context.MODE_PRIVATE)
|
||||
|
||||
// Load state from SharedPreferences
|
||||
var llmMode by remember { mutableStateOf(prefs.getString("llmMode", "Remote") ?: "Remote") }
|
||||
var llmUrl by remember { mutableStateOf(prefs.getString("llmUrl", "http://10.0.2.2:11434/v1") ?: "") }
|
||||
var modelName by remember { mutableStateOf(prefs.getString("modelName", "llama3.2") ?: "") }
|
||||
var skillsPath by remember { mutableStateOf(prefs.getString("skillsPath", SkillManager.skillsDirectory?.absolutePath ?: "") ?: "") }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Skills Library") },
|
||||
title = { Text("Settings & Skills") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClicked) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
@@ -232,38 +243,88 @@ fun SkillsLibraryScreen(onBackClicked: () -> Unit) {
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
// We use verticalScroll so the keyboard doesn't hide the text fields!
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = "Local Skill Directory",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text("LLM Engine Configuration", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Displays the actual folder path on the device
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = skillsPath,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
|
||||
// The Toggle!
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = llmMode == "Remote",
|
||||
onClick = {
|
||||
llmMode = "Remote"
|
||||
llmUrl = "http://10.0.2.2:11434/v1" // Auto-fill standard Ollama host
|
||||
}
|
||||
)
|
||||
Text("Remote Host (Ollama)")
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
RadioButton(
|
||||
selected = llmMode == "Local",
|
||||
onClick = {
|
||||
llmMode = "Local"
|
||||
llmUrl = "http://localhost:8080/v1" // Auto-fill standard Llama.cpp host
|
||||
}
|
||||
)
|
||||
Text("On-Device")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Drop your Python scripts (script.py) and JSON configurations (manifest.json) into subfolders within this directory to expand Alice's capabilities.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = llmUrl,
|
||||
onValueChange = { llmUrl = it },
|
||||
label = { Text("LLM Server URL") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = modelName,
|
||||
onValueChange = { modelName = it },
|
||||
label = { Text("Model Name (e.g., llama3.2)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Text("Skills Library Path", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = skillsPath,
|
||||
onValueChange = { skillsPath = it },
|
||||
label = { Text("Local Directory Path") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
// 1. Save all inputs to device memory
|
||||
prefs.edit()
|
||||
.putString("llmMode", llmMode)
|
||||
.putString("llmUrl", llmUrl)
|
||||
.putString("modelName", modelName)
|
||||
.putString("skillsPath", skillsPath)
|
||||
.apply()
|
||||
|
||||
// 2. Hot-reload the engines with the new settings
|
||||
LlmManager.initialize(llmUrl, modelName)
|
||||
SkillManager.updateDirectory(skillsPath)
|
||||
|
||||
// 3. Return to chat
|
||||
onBackClicked()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text("Save and Apply")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,15 @@ object SkillManager {
|
||||
skillsDirectory = skillsDir
|
||||
}
|
||||
|
||||
fun updateDirectory(newPath: String) {
|
||||
val newDir = File(newPath)
|
||||
if (!newDir.exists()) {
|
||||
newDir.mkdirs() // Create it if the user typed a new path
|
||||
}
|
||||
skillsDirectory = newDir
|
||||
Log.i("AliceSkills", "Skills directory updated to: ${newDir.absolutePath}")
|
||||
}
|
||||
|
||||
// 1. THE PARSER: Reads the folders and creates LangChain4j Tool Rules
|
||||
fun loadSkills(): List<ToolSpecification> {
|
||||
val toolSpecs = mutableListOf<ToolSpecification>()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.mmanningau.alice
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.chaquo.python.Python
|
||||
import com.chaquo.python.android.AndroidPlatform
|
||||
|
||||
@@ -12,7 +13,23 @@ class alice : Application() {
|
||||
if (!Python.isStarted()) {
|
||||
Python.start(AndroidPlatform(this))
|
||||
}
|
||||
// ADD THIS: Create the skills folder and resolve the path immediately
|
||||
SkillManager.initialize(this)
|
||||
// 1. Grab saved settings (or defaults if it's the first time booting)
|
||||
val prefs = getSharedPreferences("AlicePrefs", Context.MODE_PRIVATE)
|
||||
val savedUrl = prefs.getString("llmUrl", "http://10.0.2.2:11434/v1") ?: "http://10.0.2.2:11434/v1"
|
||||
val savedModel = prefs.getString("modelName", "llama3.2") ?: "llama3.2"
|
||||
val savedSkillsPath = prefs.getString("skillsPath", "") ?: ""
|
||||
|
||||
// 2. Initialize the Skills folder
|
||||
SkillManager.initialize(this) // This creates the safe default
|
||||
|
||||
if (savedSkillsPath.isNotBlank()) {
|
||||
SkillManager.updateDirectory(savedSkillsPath) // Override if the user saved a custom one
|
||||
} else {
|
||||
// If it's the first boot, save the default path so the UI can display it
|
||||
prefs.edit().putString("skillsPath", SkillManager.skillsDirectory?.absolutePath).apply()
|
||||
}
|
||||
|
||||
// 3. Boot the LLM Engine
|
||||
LlmManager.initialize(savedUrl, savedModel)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user