From 03851734223fc0dd283d6996bd179e0b709fa4fa Mon Sep 17 00:00:00 2001 From: mikolaj Date: Mon, 6 Apr 2026 21:02:56 +0200 Subject: [PATCH 1/3] Bump version to 0.0.4 and update dependencies --- .../calculator/CalculatorAgentProvider.kt | 3 - .../koogedge/agents/chat/ChatAgentProvider.kt | 1 - .../agents/weather/WeatherAgentProvider.kt | 4 -- .../koogedge/agents/weather/WeatherTools.kt | 62 +++++++------------ gradle/libs.versions.toml | 16 ++--- gradle/wrapper/gradle-wrapper.properties | 2 +- koog-edge/build.gradle.kts | 2 +- 7 files changed, 34 insertions(+), 56 deletions(-) diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt index 45b6d59..7402a52 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt @@ -3,9 +3,6 @@ package io.github.lemcoder.koogedge.agents.calculator import ai.koog.agents.core.agent.AIAgent import ai.koog.agents.core.agent.config.AIAgentConfig import ai.koog.agents.core.agent.functionalStrategy -import ai.koog.agents.core.dsl.extension.asAssistantMessage -import ai.koog.agents.core.dsl.extension.executeTool -import ai.koog.agents.core.dsl.extension.requestLLM import ai.koog.agents.core.environment.result import ai.koog.agents.core.tools.ToolRegistry import ai.koog.prompt.dsl.prompt diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/chat/ChatAgentProvider.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/chat/ChatAgentProvider.kt index 6c91e30..a69b7bc 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/chat/ChatAgentProvider.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/chat/ChatAgentProvider.kt @@ -3,7 +3,6 @@ package io.github.lemcoder.koogedge.agents.chat import ai.koog.agents.core.agent.AIAgent import ai.koog.agents.core.agent.config.AIAgentConfig import ai.koog.agents.core.agent.functionalStrategy -import ai.koog.agents.core.dsl.extension.requestLLM import ai.koog.prompt.dsl.prompt import ai.koog.prompt.executor.llms.SingleLLMPromptExecutor import io.github.lemcoder.koog.edge.cactus.CactusLLMParams diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt index 7f927b6..17ab8ff 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt @@ -3,10 +3,6 @@ package io.github.lemcoder.koogedge.agents.weather import ai.koog.agents.core.agent.AIAgent import ai.koog.agents.core.agent.config.AIAgentConfig import ai.koog.agents.core.agent.functionalStrategy -import ai.koog.agents.core.dsl.extension.asAssistantMessage -import ai.koog.agents.core.dsl.extension.executeTool -import ai.koog.agents.core.dsl.extension.requestLLM -import ai.koog.agents.core.dsl.extension.sendToolResult import ai.koog.agents.core.tools.ToolRegistry import ai.koog.prompt.dsl.prompt import ai.koog.prompt.executor.llms.SingleLLMPromptExecutor diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherTools.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherTools.kt index 5e6f1ae..3d87812 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherTools.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherTools.kt @@ -2,23 +2,20 @@ package io.github.lemcoder.koogedge.agents.weather import ai.koog.agents.core.tools.Tool import ai.koog.agents.core.tools.annotations.LLMDescription -import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimePeriod -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.offsetAt -import kotlinx.datetime.plus -import kotlinx.datetime.toInstant -import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter /** Tools for the weather agent */ object WeatherTools { private val openMeteoClient = OpenMeteoClient() - private val UTC_ZONE = TimeZone.UTC + private val UTC_ZONE: ZoneId = ZoneOffset.UTC /** Granularity options for weather forecasts */ @Serializable @@ -54,25 +51,18 @@ object WeatherTools { override suspend fun execute(args: Args): Result { val zoneId = try { - TimeZone.of(args.timezone) + ZoneId.of(args.timezone) } catch (_: Exception) { UTC_ZONE } - val now = Clock.System.now() - val localDateTime = now.toLocalDateTime(zoneId) - val offset = zoneId.offsetAt(now) - - val time = localDateTime.time - val timeStr = - "${time.hour.toString().padStart(2, '0')}:${ - time.minute.toString().padStart(2, '0') - }:${time.second.toString().padStart(2, '0')}" + val now = ZonedDateTime.now(zoneId) + val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss") return Result( - datetime = "${localDateTime.date}T$timeStr$offset", - date = localDateTime.date.toString(), - time = timeStr, + datetime = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + date = now.toLocalDate().toString(), + time = now.toLocalTime().format(timeFormatter), timezone = zoneId.id, ) } @@ -112,25 +102,21 @@ object WeatherTools { LocalDate.parse(args.date) } catch (_: Exception) { // Use current date if parsing fails - Clock.System.now().toLocalDateTime(UTC_ZONE).date + LocalDate.now(UTC_ZONE) } } else { - Clock.System.now().toLocalDateTime(UTC_ZONE).date + LocalDate.now(UTC_ZONE) } - // Convert to LocalDateTime to handle hours and minutes - val baseDateTime = - LocalDateTime(baseDate.year, baseDate.month, baseDate.dayOfMonth, 0, 0) - val baseInstant = baseDateTime.toInstant(UTC_ZONE) - - val period = - DateTimePeriod(days = args.days, hours = args.hours, minutes = args.minutes) - - val newInstant = baseInstant.plus(period, UTC_ZONE) - val resultDate = newInstant.toLocalDateTime(UTC_ZONE).date.toString() + val baseDateTime = baseDate.atStartOfDay(UTC_ZONE) + val resultDateTime = + baseDateTime + .plusDays(args.days.toLong()) + .plusHours(args.hours.toLong()) + .plusMinutes(args.minutes.toLong()) return Result( - date = resultDate, + date = resultDateTime.toLocalDate().toString(), originalDate = args.date, daysAdded = args.days, hoursAdded = args.hours, @@ -167,7 +153,7 @@ object WeatherTools { ) override suspend fun execute(args: Args): Result { - val date = Clock.System.now().toString() + val date = Instant.now().toString() // Search for the location val locations = openMeteoClient.searchLocation(args.location) if (locations.isEmpty()) { @@ -206,7 +192,7 @@ object WeatherTools { val daily = forecast.daily ?: return "No daily forecast data available" val startDate = - date.ifBlank { Clock.System.now().toLocalDateTime(UTC_ZONE).date.toString() } + date.ifBlank { LocalDate.now(UTC_ZONE).toString() } val startIndex = daily.time.indexOfFirst { it >= startDate }.coerceAtLeast(0) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 70edc79..e5765ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,17 @@ [versions] compileSdk = "36" -koogEdge = "0.0.2" +koogEdge = "0.0.3" minSdk = "31" -agp = "9.0.0" -kotlin = "2.3.0" -coreKtx = "1.17.0" +agp = "9.1.0" +kotlin = "2.3.20" +coreKtx = "1.18.0" lifecycleRuntimeKtx = "2.10.0" -activityCompose = "1.12.2" -composeBom = "2026.01.00" +activityCompose = "1.13.0" +composeBom = "2026.03.01" -koog = "0.6.0" -leap = "0.9.2" # TODO Update to latest version and migrate +koog = "0.7.3" +leap = "0.9.7" # TODO Update to latest version and migrate cactus = "1.3.0-beta" kotlinx-serialization = "1.10.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 63bc03a..0079868 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Oct 13 23:26:58 CEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/koog-edge/build.gradle.kts b/koog-edge/build.gradle.kts index d2d08a2..2a05a1f 100644 --- a/koog-edge/build.gradle.kts +++ b/koog-edge/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "io.github.lemcoder" -version = "0.0.3" +version = "0.0.4" kotlin { jvmToolchain(17) From c28148cdf903e84d4a6c19726a7a0b1119fc2436 Mon Sep 17 00:00:00 2001 From: mikolaj Date: Mon, 6 Apr 2026 23:04:00 +0200 Subject: [PATCH 2/3] Refactor Leap models and update LLM client implementations --- android/build.gradle.kts | 3 +- .../calculator/CalculatorAgentProvider.kt | 5 +- .../lemcoder/koogedge/agents/common/Common.kt | 5 +- .../agents/weather/WeatherAgentProvider.kt | 5 +- .../lemcoder/koog/edge/cactus/CactusModels.kt | 21 +++++- .../cactus/internal/CactusLocalLLMClient.kt | 8 +-- .../converter/CactusMessageConverter.kt | 2 +- .../lemcoder/koog/edge/leap/LeapLLMClient.kt | 4 +- .../lemcoder/koog/edge/leap/LeapModels.kt | 9 ++- .../edge/leap/internal/LeapLocalLLMClient.kt | 67 ++++++++++++++----- .../edge/leap/internal/LeapModelLoader.kt | 11 ++- .../converter/LeapMessageConverter.kt | 4 +- 12 files changed, 109 insertions(+), 35 deletions(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index a4a6b7c..988133e 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -57,7 +57,8 @@ dependencies { implementation(libs.koog.agents) implementation(libs.leap.sdk) implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.koog.edge) +// implementation(libs.koog.edge) + implementation(project(":koog-edge")) implementation(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.json) } \ No newline at end of file diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt index 7402a52..0066fdd 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt @@ -12,6 +12,7 @@ import android.util.Log import io.github.lemcoder.koog.edge.cactus.CactusLLMParams import io.github.lemcoder.koog.edge.cactus.CactusModels import io.github.lemcoder.koog.edge.cactus.getCactusLLMClient +import io.github.lemcoder.koog.edge.leap.LeapModels import io.github.lemcoder.koog.edge.leap.getLeapLLMClient import io.github.lemcoder.koogedge.App import io.github.lemcoder.koogedge.agents.common.AgentProvider @@ -75,12 +76,12 @@ internal class CalculatorAgentProvider : AgentProvider { prompt("test", params = CactusLLMParams(maxTokens = 512)) { system(calculatorSystemPrompt) }, - model = CactusModels.Chat.Qwen3_0_6B, + model = LeapModels.Chat.LFM2_1_2B_Instruct, maxAgentIterations = 10, ) return AIAgent( - promptExecutor = cactusExecutor, + promptExecutor = leapExecutor, strategy = strategy, agentConfig = agentConfig, toolRegistry = toolRegistry, diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/common/Common.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/common/Common.kt index 4c9e477..dfc65b1 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/common/Common.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/common/Common.kt @@ -1,3 +1,6 @@ package io.github.lemcoder.koogedge.agents.common -const val modelsPath = "/tmp/models" +import io.github.lemcoder.koogedge.App + +val modelsPath: String = App.context.filesDir.absolutePath + diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt index 17ab8ff..672df8c 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt @@ -9,6 +9,7 @@ import ai.koog.prompt.executor.llms.SingleLLMPromptExecutor import ai.koog.prompt.message.Message import io.github.lemcoder.koog.edge.cactus.CactusModels import io.github.lemcoder.koog.edge.cactus.getCactusLLMClient +import io.github.lemcoder.koog.edge.leap.LeapModels import io.github.lemcoder.koog.edge.leap.getLeapLLMClient import io.github.lemcoder.koogedge.App import io.github.lemcoder.koogedge.agents.common.AgentProvider @@ -59,13 +60,13 @@ internal class WeatherAgentProvider : AgentProvider { .trimIndent() ) }, - model = CactusModels.Chat.Qwen3_0_6B, + model = LeapModels.Chat.LFM2_1_2B_Instruct, maxAgentIterations = 50, ) // Return the agent return AIAgent( - promptExecutor = cactusExecutor, + promptExecutor = leapExecutor, strategy = strategy, agentConfig = agentConfig, toolRegistry = toolRegistry, diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/CactusModels.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/CactusModels.kt index 50a7cc7..0743d67 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/CactusModels.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/CactusModels.kt @@ -52,7 +52,7 @@ import io.github.lemcoder.koog.edge.provider.LocalLLMProvider * isDownloaded=false, quantization=8) */ sealed interface CactusModels : LocalModel { - data object Chat : LocalModel { + data object Chat : CactusModels { val FunctionGemma_270M = LLModel( provider = LocalLLMProvider, @@ -140,6 +140,25 @@ sealed interface CactusModels : LocalModel { capabilities = listOf(LLMCapability.Tools, LLMCapability.Completion), contextLength = 16_384, ) + private var customModels: List = emptyList() + override val models: List + get() = listOf( + FunctionGemma_270M, + LFM2_1_2B_Tool, + Qwen3_0_6B, + Qwen3_1_7B, + LFM2_1_2B, + LFM2_1_2B_RAG, + LFM2_350M, + LFM2_700M, + Qwen3_0_6B_Pro, + Qwen3_1_7B_Pro, + FunctionGemma_270M_Pro, + ) + customModels + + override fun addCustomModel(model: LLModel) { + customModels = customModels + model + } } } diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/CactusLocalLLMClient.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/CactusLocalLLMClient.kt index 2929a4f..ea1c50b 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/CactusLocalLLMClient.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/CactusLocalLLMClient.kt @@ -23,19 +23,19 @@ import io.github.lemcoder.koog.edge.cactus.internal.converter.koogToCactusMessag import io.github.lemcoder.koog.edge.cactus.internal.converter.koogToCactusToolConverter import io.github.lemcoder.koog.edge.log.KoogEdgeLog import io.github.lemcoder.koog.edge.provider.LocalLLMProvider -import kotlinx.datetime.Clock +import kotlin.time.Clock -class CactusLocalLLMClient(private val modelLoader: LocalModelLoader) : LLMClient { +class CactusLocalLLMClient(private val modelLoader: LocalModelLoader) : LLMClient() { override suspend fun execute( prompt: Prompt, model: LLModel, tools: List, ): List { KoogEdgeLog.w { "Executing prompt: $prompt with tools: $tools and model: $model" } - require(model.capabilities.contains(LLMCapability.Completion)) { + require(model.capabilities?.contains(LLMCapability.Completion) == true) { "Model ${model.id} does not support chat completions" } - require(model.capabilities.contains(LLMCapability.Tools) || tools.isEmpty()) { + require(model.capabilities?.contains(LLMCapability.Tools) == true || tools.isEmpty()) { "Model ${model.id} does not support tools" } diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/converter/CactusMessageConverter.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/converter/CactusMessageConverter.kt index afe5874..4c72be0 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/converter/CactusMessageConverter.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/converter/CactusMessageConverter.kt @@ -7,10 +7,10 @@ import com.cactus.ToolCall import io.github.lemcoder.koog.edge.util.Converter import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid -import kotlinx.datetime.Clock import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put +import kotlin.time.Clock @OptIn(ExperimentalUuidApi::class) internal val cactusToKoogToolCallResponseConverter = diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapLLMClient.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapLLMClient.kt index 9055b8b..f19f744 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapLLMClient.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapLLMClient.kt @@ -1,7 +1,9 @@ package io.github.lemcoder.koog.edge.leap import ai.koog.prompt.executor.clients.LLMClient +import ai.koog.prompt.executor.clients.LLMClientAPI import io.github.lemcoder.koog.edge.leap.internal.LeapLocalLLMClient import io.github.lemcoder.koog.edge.leap.internal.LeapModelLoader -fun getLeapLLMClient(modelPath: String): LLMClient = LeapLocalLLMClient(LeapModelLoader(modelPath)) +fun getLeapLLMClient(modelPath: String): LLMClient = + LeapLocalLLMClient(LeapModelLoader(modelPath)) diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapModels.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapModels.kt index 88b286f..f8b87c9 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapModels.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapModels.kt @@ -6,7 +6,7 @@ import io.github.lemcoder.koog.edge.LocalModel import io.github.lemcoder.koog.edge.provider.LocalLLMProvider sealed interface LeapModels : LocalModel { - data object Chat : LocalModel { + data object Chat : LeapModels { val LFM2_1_2B_Tool = LLModel( provider = LocalLLMProvider, @@ -30,6 +30,13 @@ sealed interface LeapModels : LocalModel { capabilities = listOf(LLMCapability.Tools, LLMCapability.Completion), contextLength = 32_768, ) + private var customModels: List = emptyList() + override val models: List + get() = listOf(LFM2_1_2B_Tool, LFM2_1_2B_Instruct, LFM2_1_2B_Thinking) + customModels + + override fun addCustomModel(model: LLModel) { + customModels = customModels + model + } } } diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapLocalLLMClient.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapLocalLLMClient.kt index 29f1c33..8e16218 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapLocalLLMClient.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapLocalLLMClient.kt @@ -4,9 +4,11 @@ import ai.koog.agents.core.tools.ToolDescriptor import ai.koog.prompt.dsl.ModerationResult import ai.koog.prompt.dsl.Prompt import ai.koog.prompt.executor.clients.LLMClient +import ai.koog.prompt.executor.clients.LLMClientAPI import ai.koog.prompt.llm.LLMCapability import ai.koog.prompt.llm.LLMProvider import ai.koog.prompt.llm.LLModel +import ai.koog.prompt.message.ContentPart import ai.koog.prompt.message.Message import ai.koog.prompt.message.ResponseMetaInfo import ai.koog.prompt.streaming.StreamFrame @@ -21,19 +23,19 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow -import kotlinx.datetime.Clock +import kotlin.time.Clock -internal open class LeapLocalLLMClient(private val modelLoader: LeapModelLoader) : LLMClient { +internal class LeapLocalLLMClient(private val modelLoader: LeapModelLoader) : LLMClient() { override suspend fun execute( prompt: Prompt, model: LLModel, tools: List, ): List { KoogEdgeLog.w { "Executing prompt: $prompt with tools: $tools and model: $model" } - require(model.capabilities.contains(LLMCapability.Completion)) { + require(model.capabilities?.contains(LLMCapability.Completion) == true) { "Model ${model.id} does not support chat completions" } - require(model.capabilities.contains(LLMCapability.Tools) || tools.isEmpty()) { + require(model.capabilities?.contains(LLMCapability.Tools) == true || tools.isEmpty()) { "Model ${model.id} does not support tools" } @@ -74,10 +76,23 @@ internal open class LeapLocalLLMClient(private val modelLoader: LeapModelLoader) frames.forEach { frame -> KoogEdgeLog.warning("Received frame: $frame") when (frame) { - is StreamFrame.Append -> responseText.append(frame.text) is StreamFrame.End -> finishReason = frame.finishReason - is StreamFrame.ToolCall -> - toolCalls.add(frame.toMessageResponse() as Message.Tool.Call) + is StreamFrame.TextComplete -> responseText.append(frame.text) + is StreamFrame.TextDelta -> responseText.append(frame.text) + is StreamFrame.ToolCallComplete -> { + toolCalls += + Message.Tool.Call( + id = frame.id, + tool = frame.name, + content = frame.content, + metaInfo = ResponseMetaInfo.create(Clock.System), + ) + } + is StreamFrame.ReasoningComplete, + is StreamFrame.ReasoningDelta, + is StreamFrame.ToolCallDelta -> { + // Ignored for now in non-streaming execute() aggregation. + } } } } @@ -113,10 +128,10 @@ internal open class LeapLocalLLMClient(private val modelLoader: LeapModelLoader) tools: List, ): Flow = flow { KoogEdgeLog.w { "Executing prompt: $prompt with tools: $tools and model: $model" } - require(model.capabilities.contains(LLMCapability.Completion)) { + require(model.capabilities?.contains(LLMCapability.Completion) == true) { "Model ${model.id} does not support chat completions" } - require(model.capabilities.contains(LLMCapability.Tools) || tools.isEmpty()) { + require(model.capabilities?.contains(LLMCapability.Tools) == true || tools.isEmpty()) { "Model ${model.id} does not support tools" } val leapLLModel = getLeapLLModelById(model.id) @@ -161,18 +176,36 @@ internal open class LeapLocalLLMClient(private val modelLoader: LeapModelLoader) private fun StreamFrame.toMessageResponse(): Message.Response { val metaInfo = ResponseMetaInfo(timestamp = Clock.System.now()) return when (this) { - is StreamFrame.Append -> - Message.Assistant(content = this.text, metaInfo = metaInfo, finishReason = "") + is StreamFrame.ReasoningComplete -> + Message.Reasoning( + encrypted = encrypted, + parts = text.map { ContentPart.Text(it) }, + summary = summary?.map { ContentPart.Text(it) }, + metaInfo = metaInfo, + ) + + is StreamFrame.TextComplete -> Message.Assistant(content = text, metaInfo = metaInfo) + + is StreamFrame.ToolCallComplete -> + Message.Tool.Call(id = id, tool = name, content = content, metaInfo = metaInfo) - is StreamFrame.End -> - Message.Assistant(content = "", metaInfo = metaInfo, finishReason = this.finishReason) + is StreamFrame.ReasoningDelta -> + Message.Reasoning( + parts = listOf(ContentPart.Text(text.orEmpty())), + summary = summary?.let { listOf(ContentPart.Text(it)) }, + metaInfo = metaInfo, + ) - is StreamFrame.ToolCall -> + is StreamFrame.TextDelta -> Message.Assistant(content = text, metaInfo = metaInfo) + + is StreamFrame.ToolCallDelta -> Message.Tool.Call( - id = this.id, - tool = this.name, - content = this.content, + id = id, + tool = name.orEmpty(), + content = content.orEmpty(), metaInfo = metaInfo, ) + + is StreamFrame.End -> Message.Assistant(content = "", metaInfo = metaInfo, finishReason = finishReason) } } diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapModelLoader.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapModelLoader.kt index 4f4dadc..d4ba5f7 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapModelLoader.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapModelLoader.kt @@ -15,6 +15,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem internal class LeapModelLoader( private val modelsPath: String, @@ -29,6 +31,10 @@ internal class LeapModelLoader( override suspend fun loadModel(model: LLModel): ModelRunner? = withContext(Dispatchers.IO) { mutex.withLock { + val modelPath = "${modelsPath}/${model.id}" + KoogEdgeLog.w { "Loading model ${model.id} from path: $model at $modelPath" } + SystemFileSystem.delete(Path(modelPath), false) + if (loadingJob?.isActive == true) { throw IllegalStateException("A model is already loading") } @@ -37,10 +43,11 @@ internal class LeapModelLoader( try { currentRunner = downloader.loadModel( - modelSlug = model.id, + modelName = model.id, quantizationSlug = - "Q_4_0", // Load 4-bit quantized models by default + "Q4_K_M", // Load 4-bit quantized models by default modelLoadingOptions = options, + forceDownload = true ) } catch (e: LeapModelLoadingException) { KoogEdgeLog.error("Error loading model: ${e.message}", e) diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/converter/LeapMessageConverter.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/converter/LeapMessageConverter.kt index 9c2eabc..52d3f56 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/converter/LeapMessageConverter.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/converter/LeapMessageConverter.kt @@ -39,13 +39,13 @@ internal val koogToLeapMessageConverter = internal val messageResponseToStreamFrameConverter = Converter> { response -> when (response) { - is MessageResponse.Chunk -> listOf(Append(text = response.text)) + is MessageResponse.Chunk -> listOf(TextComplete(text = response.text)) is MessageResponse.Complete -> listOf(End()) is MessageResponse.FunctionCalls -> response.functionCalls.map { firstCall -> - ToolCall( + ToolCallComplete( id = null, name = firstCall.name, content = firstCall.arguments.toJsonObjectString(), From 52ac5130b6effeb738ca36a789a0588b02cb777a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:21:45 +0000 Subject: [PATCH 3/3] chore: reformat Kotlin code with ktfmt --- .../calculator/CalculatorAgentProvider.kt | 1 - .../lemcoder/koogedge/agents/common/Common.kt | 1 - .../agents/weather/WeatherAgentProvider.kt | 1 - .../koogedge/agents/weather/WeatherTools.kt | 7 +++-- .../lemcoder/koog/edge/cactus/CactusModels.kt | 27 ++++++++++--------- .../converter/CactusMessageConverter.kt | 2 +- .../lemcoder/koog/edge/leap/LeapLLMClient.kt | 4 +-- .../edge/leap/internal/LeapLocalLLMClient.kt | 6 ++--- .../edge/leap/internal/LeapModelLoader.kt | 2 +- 9 files changed, 23 insertions(+), 28 deletions(-) diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt index 0066fdd..27a8e07 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/calculator/CalculatorAgentProvider.kt @@ -10,7 +10,6 @@ import ai.koog.prompt.executor.llms.SingleLLMPromptExecutor import ai.koog.prompt.message.Message import android.util.Log import io.github.lemcoder.koog.edge.cactus.CactusLLMParams -import io.github.lemcoder.koog.edge.cactus.CactusModels import io.github.lemcoder.koog.edge.cactus.getCactusLLMClient import io.github.lemcoder.koog.edge.leap.LeapModels import io.github.lemcoder.koog.edge.leap.getLeapLLMClient diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/common/Common.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/common/Common.kt index dfc65b1..9ea1e8f 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/common/Common.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/common/Common.kt @@ -3,4 +3,3 @@ package io.github.lemcoder.koogedge.agents.common import io.github.lemcoder.koogedge.App val modelsPath: String = App.context.filesDir.absolutePath - diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt index 672df8c..b890aa2 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherAgentProvider.kt @@ -7,7 +7,6 @@ import ai.koog.agents.core.tools.ToolRegistry import ai.koog.prompt.dsl.prompt import ai.koog.prompt.executor.llms.SingleLLMPromptExecutor import ai.koog.prompt.message.Message -import io.github.lemcoder.koog.edge.cactus.CactusModels import io.github.lemcoder.koog.edge.cactus.getCactusLLMClient import io.github.lemcoder.koog.edge.leap.LeapModels import io.github.lemcoder.koog.edge.leap.getLeapLLMClient diff --git a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherTools.kt b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherTools.kt index 3d87812..6c069bc 100644 --- a/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherTools.kt +++ b/android/src/main/kotlin/io/github/lemcoder/koogedge/agents/weather/WeatherTools.kt @@ -2,14 +2,14 @@ package io.github.lemcoder.koogedge.agents.weather import ai.koog.agents.core.tools.Tool import ai.koog.agents.core.tools.annotations.LLMDescription -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.time.Instant import java.time.LocalDate import java.time.ZoneId import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable /** Tools for the weather agent */ object WeatherTools { @@ -191,8 +191,7 @@ object WeatherTools { private fun formatDailyForecast(forecast: WeatherForecast, date: String): String { val daily = forecast.daily ?: return "No daily forecast data available" - val startDate = - date.ifBlank { LocalDate.now(UTC_ZONE).toString() } + val startDate = date.ifBlank { LocalDate.now(UTC_ZONE).toString() } val startIndex = daily.time.indexOfFirst { it >= startDate }.coerceAtLeast(0) diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/CactusModels.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/CactusModels.kt index 0743d67..e161b1d 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/CactusModels.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/CactusModels.kt @@ -142,19 +142,20 @@ sealed interface CactusModels : LocalModel { ) private var customModels: List = emptyList() override val models: List - get() = listOf( - FunctionGemma_270M, - LFM2_1_2B_Tool, - Qwen3_0_6B, - Qwen3_1_7B, - LFM2_1_2B, - LFM2_1_2B_RAG, - LFM2_350M, - LFM2_700M, - Qwen3_0_6B_Pro, - Qwen3_1_7B_Pro, - FunctionGemma_270M_Pro, - ) + customModels + get() = + listOf( + FunctionGemma_270M, + LFM2_1_2B_Tool, + Qwen3_0_6B, + Qwen3_1_7B, + LFM2_1_2B, + LFM2_1_2B_RAG, + LFM2_350M, + LFM2_700M, + Qwen3_0_6B_Pro, + Qwen3_1_7B_Pro, + FunctionGemma_270M_Pro, + ) + customModels override fun addCustomModel(model: LLModel) { customModels = customModels + model diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/converter/CactusMessageConverter.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/converter/CactusMessageConverter.kt index 4c72be0..55ade9b 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/converter/CactusMessageConverter.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/cactus/internal/converter/CactusMessageConverter.kt @@ -5,12 +5,12 @@ import ai.koog.prompt.message.ResponseMetaInfo import com.cactus.ChatMessage import com.cactus.ToolCall import io.github.lemcoder.koog.edge.util.Converter +import kotlin.time.Clock import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put -import kotlin.time.Clock @OptIn(ExperimentalUuidApi::class) internal val cactusToKoogToolCallResponseConverter = diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapLLMClient.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapLLMClient.kt index f19f744..9055b8b 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapLLMClient.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/LeapLLMClient.kt @@ -1,9 +1,7 @@ package io.github.lemcoder.koog.edge.leap import ai.koog.prompt.executor.clients.LLMClient -import ai.koog.prompt.executor.clients.LLMClientAPI import io.github.lemcoder.koog.edge.leap.internal.LeapLocalLLMClient import io.github.lemcoder.koog.edge.leap.internal.LeapModelLoader -fun getLeapLLMClient(modelPath: String): LLMClient = - LeapLocalLLMClient(LeapModelLoader(modelPath)) +fun getLeapLLMClient(modelPath: String): LLMClient = LeapLocalLLMClient(LeapModelLoader(modelPath)) diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapLocalLLMClient.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapLocalLLMClient.kt index 8e16218..4413a08 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapLocalLLMClient.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapLocalLLMClient.kt @@ -4,7 +4,6 @@ import ai.koog.agents.core.tools.ToolDescriptor import ai.koog.prompt.dsl.ModerationResult import ai.koog.prompt.dsl.Prompt import ai.koog.prompt.executor.clients.LLMClient -import ai.koog.prompt.executor.clients.LLMClientAPI import ai.koog.prompt.llm.LLMCapability import ai.koog.prompt.llm.LLMProvider import ai.koog.prompt.llm.LLModel @@ -19,11 +18,11 @@ import io.github.lemcoder.koog.edge.leap.internal.converter.leapFunctionConverte import io.github.lemcoder.koog.edge.leap.internal.converter.messageResponseToStreamFrameConverter import io.github.lemcoder.koog.edge.log.KoogEdgeLog import io.github.lemcoder.koog.edge.provider.LocalLLMProvider +import kotlin.time.Clock import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow -import kotlin.time.Clock internal class LeapLocalLLMClient(private val modelLoader: LeapModelLoader) : LLMClient() { override suspend fun execute( @@ -206,6 +205,7 @@ private fun StreamFrame.toMessageResponse(): Message.Response { metaInfo = metaInfo, ) - is StreamFrame.End -> Message.Assistant(content = "", metaInfo = metaInfo, finishReason = finishReason) + is StreamFrame.End -> + Message.Assistant(content = "", metaInfo = metaInfo, finishReason = finishReason) } } diff --git a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapModelLoader.kt b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapModelLoader.kt index d4ba5f7..d38c47c 100644 --- a/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapModelLoader.kt +++ b/koog-edge/src/commonMain/kotlin/io/github/lemcoder/koog/edge/leap/internal/LeapModelLoader.kt @@ -47,7 +47,7 @@ internal class LeapModelLoader( quantizationSlug = "Q4_K_M", // Load 4-bit quantized models by default modelLoadingOptions = options, - forceDownload = true + forceDownload = true, ) } catch (e: LeapModelLoadingException) { KoogEdgeLog.error("Error loading model: ${e.message}", e)