Fix compilation errors and add Gradle wrapper

- Fix SSE streaming: replace inline processSSE with readLine loop
  to allow suspend emit() calls inside the flow
- Remove @Serializable from CompletionRequest (ToolApiDefinition
  is not serializable)
- Fix LocationTool type inference for suspendCancellableCoroutine
- Add gradlew/gradlew.bat and gradle-wrapper.jar

Build: assembleDebug passes, APK generated (20.7MB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
JordanTheJet
2026-02-18 14:01:38 -05:00
parent e57292c25d
commit d191d60668
6 changed files with 220 additions and 100 deletions

View File

@@ -11,7 +11,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.BufferedReader
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -76,79 +76,94 @@ class AnthropicProvider @Inject constructor() : Provider {
var stopReason = StopReason.END_TURN
try {
reader.processSSE { event, data ->
when (event) {
"content_block_start" -> {
val block = json.parseToJsonElement(data).jsonObject
val contentBlock = block["content_block"]?.jsonObject ?: return@processSSE
when (contentBlock["type"]?.jsonPrimitive?.content) {
"tool_use" -> {
// Flush any accumulated text
if (currentText.isNotEmpty()) {
contentBlocks.add(ContentBlock.Text(currentText.toString()))
currentText = StringBuilder()
var sseEvent = ""
var sseData = StringBuilder()
while (true) {
val line = reader.readLine() ?: break
when {
line.startsWith("event: ") -> sseEvent = line.removePrefix("event: ").trim()
line.startsWith("data: ") -> sseData.append(line.removePrefix("data: "))
line.isBlank() -> {
if (sseEvent.isNotEmpty() && sseData.isNotEmpty()) {
val data = sseData.toString()
when (sseEvent) {
"content_block_start" -> {
val block = json.parseToJsonElement(data).jsonObject
val cb = block["content_block"]?.jsonObject
if (cb != null && cb["type"]?.jsonPrimitive?.content == "tool_use") {
if (currentText.isNotEmpty()) {
contentBlocks.add(ContentBlock.Text(currentText.toString()))
currentText = StringBuilder()
}
currentToolId = cb["id"]?.jsonPrimitive?.content ?: ""
currentToolName = cb["name"]?.jsonPrimitive?.content ?: ""
currentToolInput = StringBuilder()
emit(StreamEvent.ToolUseStart(currentToolId, currentToolName))
}
}
"content_block_delta" -> {
val block = json.parseToJsonElement(data).jsonObject
val delta = block["delta"]?.jsonObject
if (delta != null) {
when (delta["type"]?.jsonPrimitive?.content) {
"text_delta" -> {
val text = delta["text"]?.jsonPrimitive?.content ?: ""
currentText.append(text)
emit(StreamEvent.TextDelta(text))
}
"input_json_delta" -> {
val partial = delta["partial_json"]?.jsonPrimitive?.content ?: ""
currentToolInput.append(partial)
emit(StreamEvent.ToolUseInputDelta(partial))
}
}
}
}
"content_block_stop" -> {
if (currentToolName.isNotEmpty()) {
val inputJson = try {
json.parseToJsonElement(currentToolInput.toString()).jsonObject
} catch (_: Exception) {
JsonObject(emptyMap())
}
contentBlocks.add(
ContentBlock.ToolUse(currentToolId, currentToolName, inputJson)
)
currentToolId = ""
currentToolName = ""
currentToolInput = StringBuilder()
}
}
"message_delta" -> {
val block = json.parseToJsonElement(data).jsonObject
val delta = block["delta"]?.jsonObject
val reason = delta?.get("stop_reason")?.jsonPrimitive?.content
stopReason = when (reason) {
"tool_use" -> StopReason.TOOL_USE
"max_tokens" -> StopReason.MAX_TOKENS
else -> StopReason.END_TURN
}
}
"message_stop" -> {
if (currentText.isNotEmpty()) {
contentBlocks.add(ContentBlock.Text(currentText.toString()))
}
emit(StreamEvent.Complete(
CompletionResponse(contentBlocks, stopReason)
))
}
"error" -> {
val block = json.parseToJsonElement(data).jsonObject
val error = block["error"]?.jsonObject
val message = error?.get("message")?.jsonPrimitive?.content ?: data
emit(StreamEvent.Error(message))
}
currentToolId = contentBlock["id"]?.jsonPrimitive?.content ?: ""
currentToolName = contentBlock["name"]?.jsonPrimitive?.content ?: ""
currentToolInput = StringBuilder()
emit(StreamEvent.ToolUseStart(currentToolId, currentToolName))
}
}
}
"content_block_delta" -> {
val block = json.parseToJsonElement(data).jsonObject
val delta = block["delta"]?.jsonObject ?: return@processSSE
when (delta["type"]?.jsonPrimitive?.content) {
"text_delta" -> {
val text = delta["text"]?.jsonPrimitive?.content ?: ""
currentText.append(text)
emit(StreamEvent.TextDelta(text))
}
"input_json_delta" -> {
val partial = delta["partial_json"]?.jsonPrimitive?.content ?: ""
currentToolInput.append(partial)
emit(StreamEvent.ToolUseInputDelta(partial))
}
}
}
"content_block_stop" -> {
if (currentToolName.isNotEmpty()) {
val inputJson = try {
json.parseToJsonElement(currentToolInput.toString()).jsonObject
} catch (_: Exception) {
JsonObject(emptyMap())
}
contentBlocks.add(
ContentBlock.ToolUse(currentToolId, currentToolName, inputJson)
)
currentToolId = ""
currentToolName = ""
currentToolInput = StringBuilder()
}
}
"message_delta" -> {
val block = json.parseToJsonElement(data).jsonObject
val delta = block["delta"]?.jsonObject
val reason = delta?.get("stop_reason")?.jsonPrimitive?.content
stopReason = when (reason) {
"tool_use" -> StopReason.TOOL_USE
"max_tokens" -> StopReason.MAX_TOKENS
else -> StopReason.END_TURN
}
}
"message_stop" -> {
if (currentText.isNotEmpty()) {
contentBlocks.add(ContentBlock.Text(currentText.toString()))
}
emit(StreamEvent.Complete(
CompletionResponse(contentBlocks, stopReason)
))
}
"error" -> {
val block = json.parseToJsonElement(data).jsonObject
val error = block["error"]?.jsonObject
val message = error?.get("message")?.jsonPrimitive?.content ?: data
emit(StreamEvent.Error(message))
sseEvent = ""
sseData = StringBuilder()
}
}
}
@@ -278,29 +293,6 @@ class AnthropicProvider @Inject constructor() : Provider {
return CompletionResponse(blocks, stopReason, usage)
}
private inline fun BufferedReader.processSSE(handler: (event: String, data: String) -> Unit) {
var currentEvent = ""
var currentData = StringBuilder()
forEachLine { line ->
when {
line.startsWith("event: ") -> {
currentEvent = line.removePrefix("event: ").trim()
}
line.startsWith("data: ") -> {
currentData.append(line.removePrefix("data: "))
}
line.isBlank() -> {
if (currentEvent.isNotEmpty() && currentData.isNotEmpty()) {
handler(currentEvent, currentData.toString())
}
currentEvent = ""
currentData = StringBuilder()
}
}
}
}
companion object {
const val API_URL = "https://api.anthropic.com/v1/messages"
const val API_VERSION = "2023-06-01"

View File

@@ -18,7 +18,6 @@ interface Provider {
fun stream(request: CompletionRequest): Flow<StreamEvent>
}
@Serializable
data class CompletionRequest(
val systemPrompt: String,
val messages: List<Message>,

View File

@@ -33,7 +33,7 @@ class LocationTool @Inject constructor(
val client = LocationServices.getFusedLocationProviderClient(context)
val cancellationToken = CancellationTokenSource()
val location = suspendCancellableCoroutine { cont ->
val location = suspendCancellableCoroutine<android.location.Location?> { cont ->
client.getCurrentLocation(
Priority.PRIORITY_HIGH_ACCURACY,
cancellationToken.token
@@ -45,17 +45,20 @@ class LocationTool @Inject constructor(
cont.invokeOnCancellation { cancellationToken.cancel() }
} ?: return ToolResult.error("Could not get location")
val lat = location.latitude
val lon = location.longitude
val result = buildJsonObject {
put("latitude", location.latitude)
put("longitude", location.longitude)
put("latitude", lat)
put("longitude", lon)
put("accuracy_meters", location.accuracy.toDouble())
location.altitude.let { put("altitude", it) }
put("altitude", location.altitude)
if (geocode) {
try {
@Suppress("DEPRECATION")
val geocoder = Geocoder(context, Locale.getDefault())
val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
val addresses = geocoder.getFromLocation(lat, lon, 1)
addresses?.firstOrNull()?.let { addr ->
put("address", buildJsonObject {
put("street", addr.getAddressLine(0) ?: "")

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

66
gradlew vendored Normal file
View File

@@ -0,0 +1,66 @@
#!/bin/sh
##############################################################################
## Gradle wrapper script for POSIX compatible systems
##############################################################################
# Attempt to set APP_HOME
PRG="$0"
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME"
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH."
fi
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
exec "$JAVACMD" \
$DEFAULT_JVM_OPTS \
$JAVA_OPTS \
$GRADLE_OPTS \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"

60
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,60 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem Licensed under the Apache License, Version 2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %OS%=="Windows_NT" endlocal
:omega
@exit /b %ERRORLEVEL%
:fail
@exit /b 1