Now have the message box - send to langchain4j routine set up and confirmed, even without an LLM to test against so we prove that the onscreen workflow is OK

This commit is contained in:
2026-02-26 10:33:33 +11:00
parent 3f281d0b5b
commit 7c1bc79fb2
8 changed files with 282 additions and 34 deletions

View File

@@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-02-25T23:24:39.552459762Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/michael/.android/avd/Pixel_8_API_35.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@@ -39,11 +39,12 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "17"
}
buildFeatures {
compose = true
@@ -68,10 +69,14 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
// LangChain4j Core Orchestration
implementation("dev.langchain4j:langchain4j:0.29.1")
// LangChain4j BOM (Bill of Materials)
// This ensures all langchain4j modules use the same, compatible version.
implementation(platform("dev.langchain4j:langchain4j-bom:0.36.2"))
// We use the OpenAI module because almost all local Android LLM runners
// (like Llama.cpp) host a local server that mimics the OpenAI API format.
implementation("dev.langchain4j:langchain4j-open-ai:0.29.1")
// Now, declare the modules you need without specifying the version.
implementation("dev.langchain4j:langchain4j")
implementation("dev.langchain4j:langchain4j-open-ai")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}

View File

@@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="alice"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"

View File

@@ -0,0 +1,12 @@
package net.mmanningau.alice
import dev.langchain4j.service.SystemMessage
// This interface defines our AI. LangChain4j will automatically implement it.
interface AliceAgent {
// The System Message sets the baseline behavior before the user even speaks.
@SystemMessage("You are Alice, a highly capable local AI assistant running on an Android device. You provide concise, direct answers.")
fun chat(userMessage: String): String
}

View File

@@ -0,0 +1,31 @@
package net.mmanningau.alice
import dev.langchain4j.model.chat.ChatLanguageModel
import dev.langchain4j.model.openai.OpenAiChatModel
import dev.langchain4j.service.AiServices
import java.time.Duration
object LlmManager {
// IMPORTANT FOR THE EMULATOR:
// 10.0.2.2 is a special IP that lets the Android Emulator talk to your Host PC's localhost.
// If you are running Llama.cpp or LM Studio on your Mac/PC at port 8080, use this:
private const val LOCAL_LLM_URL = "http://10.0.2.2:11434/v1"
// (Later, when the LLM is running directly ON the phone itself, this will change to "http://localhost:8080/v1")
// 1. We build the "Model" connection
private val chatModel: ChatLanguageModel = OpenAiChatModel.builder()
.baseUrl(LOCAL_LLM_URL)
.apiKey("dummy-key-not-needed") // Local servers ignore this, but the builder requires a string
.timeout(Duration.ofMinutes(3)) // Local inference can take a bit
.maxRetries(0) // ADD THIS LINE: Bypasses the broken Java 15 retry logger!
.logRequests(true) // Great for debugging in Logcat
.logResponses(true)
.build()
// 2. We build the "Agent" combining our Model and our AliceAgent Interface
val agent: AliceAgent = AiServices.builder(AliceAgent::class.java)
.chatLanguageModel(chatModel)
.build()
}

View File

@@ -1,47 +1,207 @@
package net.mmanningau.alice
package net.mmanningau.alice // Ensure this matches your package name!
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.foundation.background
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.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import net.mmanningau.alice.ui.theme.AliceTheme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers //Added so that Kotlin run the query in background threads
// 1. A simple data class to hold our messages
data class ChatMessage(val text: String, val isUser: Boolean)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AliceTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
MaterialTheme {
MainChatScreen()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainChatScreen() {
// State management for the drawer and the chat
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
var inputText by remember { mutableStateOf("") }
// We start with a dummy greeting message
var messages by remember {
mutableStateOf(listOf(ChatMessage("Hello! I am your local agent. How can I help?", false)))
}
// 2. The Slide-out Drawer Setup
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
Spacer(modifier = Modifier.height(16.dp))
Text(
"Alice Agent Configuration",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleLarge
)
Divider()
// Placeholder 1: Model Selection
NavigationDrawerItem(
label = { Text("Select LLM Model") },
selected = false,
icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
onClick = {
scope.launch { drawerState.close() }
// TODO: Navigate to Model Selection Screen
}
)
// Placeholder 2: Skills Menu
NavigationDrawerItem(
label = { Text("Skills Library") },
selected = false,
icon = { Icon(Icons.Default.Build, contentDescription = "Skills") },
onClick = {
scope.launch { drawerState.close() }
// TODO: Navigate to Skills Menu Screen
}
)
}
}
) {
// 3. The Main Screen Layout (Scaffold provides the top bar and body)
Scaffold(
topBar = {
TopAppBar(
title = { Text("Alice Agent") },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
// 4. The Chat Area
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// The scrolling list of messages
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 8.dp),
reverseLayout = true // Starts at the bottom like a real chat app
) {
// We reverse the list so the newest is at the bottom
items(messages.reversed()) { message ->
ChatBubble(message)
}
}
// 5. The Input Field
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Ask me to do something...") },
shape = RoundedCornerShape(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
FloatingActionButton(
onClick = {
if (inputText.isNotBlank()) {
val userText = inputText
// 1. Add user message to UI
messages = messages + ChatMessage(userText, true)
// 2. Clear input field
inputText = ""
// 3. Launch background thread to talk to LangChain4j
scope.launch(Dispatchers.IO) {
try {
// Send it to the local LLM!
val response = LlmManager.agent.chat(userText)
// Compose state automatically handles switching back to the main thread for UI updates
messages = messages + ChatMessage(response, false)
} catch (e: Exception) {
messages = messages + ChatMessage("Connection Error: Is the local LLM server running?", false)
}
}
}
},
containerColor = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(50)
) {
Icon(Icons.Default.Send, contentDescription = "Send")
}
}
}
}
}
}
// 6. A helper composable to draw the chat bubbles
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
fun ChatBubble(message: ChatMessage) {
val alignment = if (message.isUser) Alignment.CenterEnd else Alignment.CenterStart
val color = if (message.isUser) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondaryContainer
val textColor = if (message.isUser) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSecondaryContainer
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
AliceTheme {
Greeting("Android")
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
contentAlignment = alignment
) {
Surface(
shape = RoundedCornerShape(16.dp),
color = color,
modifier = Modifier.widthIn(max = 280.dp)
) {
Text(
text = message.text,
modifier = Modifier.padding(12.dp),
color = textColor
)
}
}
}

View File

@@ -0,0 +1,16 @@
package net.mmanningau.alice
import android.app.Application
import com.chaquo.python.Python
import com.chaquo.python.android.AndroidPlatform
class alice : Application() {
override fun onCreate() {
super.onCreate()
// This boots the Python environment the moment the app starts
if (!Python.isStarted()) {
Python.start(AndroidPlatform(this))
}
}
}