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:
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@@ -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
13
.idea/deviceManager.xml
generated
Normal 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>
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
12
app/src/main/java/net/mmanningau/alice/AliceAgent.kt.kt
Normal file
12
app/src/main/java/net/mmanningau/alice/AliceAgent.kt.kt
Normal 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
|
||||
|
||||
}
|
||||
31
app/src/main/java/net/mmanningau/alice/LlmManager.kt
Normal file
31
app/src/main/java/net/mmanningau/alice/LlmManager.kt
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user