diff --git a/app/src/main/kotlin/com/cellclaw/provider/AnthropicProvider.kt b/app/src/main/kotlin/com/cellclaw/provider/AnthropicProvider.kt index 975bdc6..baf9c89 100644 --- a/app/src/main/kotlin/com/cellclaw/provider/AnthropicProvider.kt +++ b/app/src/main/kotlin/com/cellclaw/provider/AnthropicProvider.kt @@ -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" diff --git a/app/src/main/kotlin/com/cellclaw/provider/Provider.kt b/app/src/main/kotlin/com/cellclaw/provider/Provider.kt index ffb1f6e..0b138fc 100644 --- a/app/src/main/kotlin/com/cellclaw/provider/Provider.kt +++ b/app/src/main/kotlin/com/cellclaw/provider/Provider.kt @@ -18,7 +18,6 @@ interface Provider { fun stream(request: CompletionRequest): Flow } -@Serializable data class CompletionRequest( val systemPrompt: String, val messages: List, diff --git a/app/src/main/kotlin/com/cellclaw/tools/LocationTool.kt b/app/src/main/kotlin/com/cellclaw/tools/LocationTool.kt index 162c1c3..426954e 100644 --- a/app/src/main/kotlin/com/cellclaw/tools/LocationTool.kt +++ b/app/src/main/kotlin/com/cellclaw/tools/LocationTool.kt @@ -33,7 +33,7 @@ class LocationTool @Inject constructor( val client = LocationServices.getFusedLocationProviderClient(context) val cancellationToken = CancellationTokenSource() - val location = suspendCancellableCoroutine { cont -> + val location = suspendCancellableCoroutine { 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) ?: "") diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..762a11a --- /dev/null +++ b/gradlew @@ -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 \ + "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9dc57ba --- /dev/null +++ b/gradlew.bat @@ -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