Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/build_debug.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Build Debug APK

on:
push:
branches: [ "main" ]
paths:
- 'Android/**'
pull_request:
branches: [ "main" ]
paths:
- 'Android/**'
workflow_dispatch:

jobs:
build_debug:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./Android/src
steps:
- name: Checkout the source code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build Debug APK
run: ./gradlew assembleDebug
- name: Upload Debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug
path: Android/src/app/build/outputs/apk/debug/app-debug.apk
31 changes: 31 additions & 0 deletions .github/workflows/release_apk.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Release APK

on:
push:
tags:
- 'v*'

jobs:
release:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./Android/src
steps:
- name: Checkout the source code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build Release APK
run: ./gradlew assembleRelease
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
files: Android/src/app/build/outputs/apk/release/app-release-unsigned.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 4 additions & 0 deletions Android/src/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ dependencies {
implementation(libs.firebase.messaging)
implementation(libs.androidx.exifinterface)
implementation(libs.moshi.kotlin)
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
kapt(libs.hilt.android.compiler)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.ai.edge.gallery.customtasks.codingagent

import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Code
import androidx.compose.runtime.Composable
import com.google.ai.edge.gallery.customtasks.common.CustomTask
import com.google.ai.edge.gallery.customtasks.common.CustomTaskData
import com.google.ai.edge.gallery.data.BuiltInTaskId
import com.google.ai.edge.gallery.data.Category
import com.google.ai.edge.gallery.data.Model
import com.google.ai.edge.gallery.data.Task
import com.google.ai.edge.gallery.ui.llmchat.LlmChatModelHelper
import com.google.ai.edge.litertlm.Contents
import com.google.ai.edge.litertlm.tool
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import kotlinx.coroutines.CoroutineScope
import javax.inject.Inject

private const val CODING_AGENT_SYSTEM_PROMPT = """
You are an expert software engineering agent. You help the user with coding tasks in their GitHub repository.
You have access to tools to read files, write files, list directories, and create pull requests.

CRITICAL RULES:
1. ALWAYS start by proposing a step-by-step plan.
2. User MUST approve any file modifications (write_file) via the diff viewer.
3. Be concise and professional.
4. If a tool fails, analyze the error and propose an alternative.
5. Before editing, check if you are on the correct branch (avoid main).

Your available tools are:
- read_file(path: String)
- write_file(path: String, content: String, commit_message: String)
- list_directory(path: String)
- create_pull_request(title: String, body: String, head_branch: String, base_branch: String)
- run_command(command: String)
"""

class CodingAgentTask @Inject constructor(
val codingTools: CodingTools
) : CustomTask {

override val task = Task(
id = BuiltInTaskId.LLM_CODING_AGENT,
label = "Agentic Coding",
description = "AI-assisted software development on-device.",
shortDescription = "Expert coding assistant",
category = Category.LLM,
icon = Icons.Outlined.Code,
models = mutableListOf(),
handleModelConfigChangesInTask = true,
defaultSystemPrompt = CODING_AGENT_SYSTEM_PROMPT
)

override fun initializeModelFn(
context: Context,
coroutineScope: CoroutineScope,
model: Model,
systemInstruction: Contents?,
onDone: (String) -> Unit
) {
LlmChatModelHelper.initialize(
context = context,
model = model,
taskId = task.id,
supportImage = false,
supportAudio = false,
onDone = onDone,
systemInstruction = systemInstruction ?: Contents.of(task.defaultSystemPrompt),
tools = listOf(tool(codingTools)),
enableConversationConstrainedDecoding = true
)
}

override fun cleanUpModelFn(
context: Context,
coroutineScope: CoroutineScope,
model: Model,
onDone: () -> Unit
) {
LlmChatModelHelper.cleanUp(model = model, onDone = onDone)
}

@Composable
override fun MainScreen(data: Any) {
val customTaskData = data as CustomTaskData
CodingWorkspaceScreen(
task = task,
modelManagerViewModel = customTaskData.modelManagerViewModel,
bottomPadding = customTaskData.bottomPadding,
setAppBarControlsDisabled = customTaskData.setAppBarControlsDisabled,
setTopBarVisible = customTaskData.setTopBarVisible,
setCustomNavigateUpCallback = customTaskData.setCustomNavigateUpCallback
)
}
}

@Module
@InstallIn(SingletonComponent::class)
object CodingAgentTaskModule {
@Provides
@IntoSet
fun provideCodingAgentTask(codingTools: CodingTools): CustomTask {
return CodingAgentTask(codingTools)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.ai.edge.gallery.customtasks.codingagent

import android.util.Base64
import android.util.Log
import com.google.ai.edge.gallery.data.DataStoreRepository
import com.google.ai.edge.gallery.network.GitHubCreatePullRequest
import com.google.ai.edge.gallery.network.GitHubService
import com.google.ai.edge.gallery.network.GitHubUpdateFileRequest
import com.google.ai.edge.gallery.data.codingagent.Project
import com.google.ai.edge.litertlm.Tool
import com.google.ai.edge.litertlm.ToolParam
import com.google.ai.edge.litertlm.ToolSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import javax.inject.Inject

private const val TAG = "AGCodingTools"

class CodingTools @Inject constructor(
private val gitHubService: GitHubService,
private val dataStoreRepository: DataStoreRepository
) : ToolSet {
var currentProject: Project? = null

@Tool(description = "Reads the content of a file from the current repository.")
fun read_file(@ToolParam(description = "The path to the file.") path: String): String {
val project = currentProject ?: return "Error: No project selected."
val token = dataStoreRepository.readSecret("github_token") ?: return "Error: Not logged in to GitHub."
val parts = project.githubRepoUrl.split("/")

return runBlocking(Dispatchers.IO) {
try {
val response = gitHubService.getFileContent("token $token", parts[0], parts[1], path, project.branch)
if (response.isSuccessful) {
val fileContent = response.body()
if (fileContent?.encoding == "base64" && fileContent.content != null) {
String(Base64.decode(fileContent.content.replace("\n", ""), Base64.DEFAULT))
} else {
fileContent?.content ?: "Error: File content is empty."
}
} else {
"Error: Failed to read file: ${response.message()}"
}
} catch (e: Exception) {
"Error: ${e.message}"
}
}
}

@Tool(description = "Writes content to a file in the current repository. This creates a commit.")
fun write_file(
@ToolParam(description = "The path to the file.") path: String,
@ToolParam(description = "The new content of the file.") content: String,
@ToolParam(description = "The commit message.") commit_message: String
): String {
val project = currentProject ?: return "Error: No project selected."
val token = dataStoreRepository.readSecret("github_token") ?: return "Error: Not logged in to GitHub."
val parts = project.githubRepoUrl.split("/")

return runBlocking(Dispatchers.IO) {
try {
// First get the SHA of the file if it exists
val getFileResponse = gitHubService.getFileContent("token $token", parts[0], parts[1], path, project.branch)
val sha = if (getFileResponse.isSuccessful) getFileResponse.body()?.sha else null

val base64Content = Base64.encodeToString(content.toByteArray(), Base64.NO_WRAP)
val request = GitHubUpdateFileRequest(
message = commit_message,
content = base64Content,
sha = sha,
branch = project.branch
)

val response = gitHubService.updateFile("token $token", parts[0], parts[1], path, request)
if (response.isSuccessful) {
"Success: File updated. Commit SHA: ${response.body()?.commit?.sha}"
} else {
"Error: Failed to write file: ${response.message()}"
}
} catch (e: Exception) {
"Error: ${e.message}"
}
}
}

@Tool(description = "Lists files and directories in a given path.")
fun list_directory(@ToolParam(description = "The path to list.") path: String): String {
val project = currentProject ?: return "Error: No project selected."
val token = dataStoreRepository.readSecret("github_token") ?: return "Error: Not logged in to GitHub."
val parts = project.githubRepoUrl.split("/")

return runBlocking(Dispatchers.IO) {
try {
// GitHub's trees API is better for recursive listing, but for a single directory we can use contents API
val response = gitHubService.getTree("token $token", parts[0], parts[1], project.branch)
if (response.isSuccessful) {
val tree = response.body()?.tree ?: emptyList()
val filtered = tree.filter { it.path.startsWith(path) && it.path.removePrefix(path).removePrefix("/").split("/").size == 1 }
filtered.joinToString("\n") { "${it.type}: ${it.path}" }
} else {
"Error: Failed to list directory: ${response.message()}"
}
} catch (e: Exception) {
"Error: ${e.message}"
}
}
}

@Tool(description = "Creates a pull request.")
fun create_pull_request(
@ToolParam(description = "PR Title.") title: String,
@ToolParam(description = "PR Body.") body: String,
@ToolParam(description = "Head branch (source).") head_branch: String,
@ToolParam(description = "Base branch (target).") base_branch: String
): String {
val project = currentProject ?: return "Error: No project selected."
val token = dataStoreRepository.readSecret("github_token") ?: return "Error: Not logged in to GitHub."
val parts = project.githubRepoUrl.split("/")

return runBlocking(Dispatchers.IO) {
try {
val request = GitHubCreatePullRequest(title, body, head_branch, base_branch)
val response = gitHubService.createPullRequest("token $token", parts[0], parts[1], request)
if (response.isSuccessful) {
"Success: PR created at ${response.body()?.html_url}"
} else {
"Error: Failed to create PR: ${response.message()}"
}
} catch (e: Exception) {
"Error: ${e.message}"
}
}
}

@Tool(description = "Runs a shell command (Simulated).")
fun run_command(@ToolParam(description = "Command to run.") command: String): String {
return "Simulation: Command '$command' executed successfully (mock output). Note: Real command execution is not supported in this environment yet."
}
}
Loading
Loading