Skip to content

Commit 988d626

Browse files
authored
Merge pull request #219 from joreilly/agent_updates
agent updates
2 parents aef4921 + b1ebbfa commit 988d626

File tree

23 files changed

+1423
-250
lines changed

23 files changed

+1423
-250
lines changed

composeApp/build.gradle.kts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
44
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
55
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
66
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
7+
import java.util.Properties
8+
import com.codingfeline.buildkonfig.compiler.FieldSpec
79

810
plugins {
911
alias(libs.plugins.kotlinMultiplatform)
@@ -13,6 +15,7 @@ plugins {
1315
alias(libs.plugins.kotlinx.serialization)
1416
alias(libs.plugins.ksp)
1517
alias(libs.plugins.kmpNativeCoroutines)
18+
alias(libs.plugins.buildkonfig)
1619
}
1720

1821
kotlin {
@@ -70,6 +73,7 @@ kotlin {
7073

7174
implementation(libs.kotlinx.coroutines)
7275
implementation(libs.kotlinx.datetime)
76+
7377
implementation(libs.bundles.ktor.common)
7478

7579
implementation(libs.voyager)
@@ -162,9 +166,6 @@ android {
162166
sourceCompatibility = JavaVersion.VERSION_17
163167
targetCompatibility = JavaVersion.VERSION_17
164168
}
165-
// dependencies {
166-
// debugImplementation(libs.compose.ui.tooling)
167-
// }
168169

169170
testOptions {
170171
unitTests {
@@ -187,28 +188,34 @@ compose.desktop {
187188
}
188189
}
189190

190-
//compose.experimental {
191-
// web.application {}
192-
//}
193-
194-
195191
kotlin.sourceSets.all {
196192
languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
197193
}
198194

199-
//configurations.configureEach {
200-
// exclude("androidx.window.core", "window-core")
201-
//}
202-
//
203195
configurations.all {
204196
// FIXME exclude netty from Koog dependencies?
205197
exclude(group = "io.netty", module = "*")
206198
}
207199

208-
// Explicitly exclude Ktor CIO engine on iOS/apple targets to avoid bringing non-supported engine
209-
// can be removed once https://github.com/JetBrains/koog/pull/869 is merged
210-
configurations.matching { it.name.contains("ios", ignoreCase = true) || it.name.contains("apple", ignoreCase = true) }.all {
211-
//exclude(group = "io.ktor", module = "ktor-client-cio")
212-
// Exclude kotlinx-datetime to avoid Clock type alias conflict with kotlin.time.Clock in Kotlin 2.2.21
213-
//exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-datetime")
214-
}
200+
buildkonfig {
201+
packageName = "dev.johnoreilly.climatetrace"
202+
203+
val localPropsFile = rootProject.file("local.properties")
204+
val localProperties = Properties()
205+
if (localPropsFile.exists()) {
206+
runCatching {
207+
localProperties.load(localPropsFile.inputStream())
208+
}.getOrElse {
209+
it.printStackTrace()
210+
}
211+
}
212+
defaultConfigs {
213+
buildConfigField(
214+
FieldSpec.Type.STRING,
215+
"GEMINI_API_KEY",
216+
localProperties["gemini_api_key"]?.toString() ?: ""
217+
)
218+
}
219+
220+
}
221+

composeApp/src/androidMain/kotlin/dev/johnoreilly/climatetrace/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import androidx.compose.runtime.setValue
2626
import androidx.compose.ui.Modifier
2727
import cafe.adriel.voyager.navigator.Navigator
2828
import dev.johnoreilly.climatetrace.di.initKoin
29-
import dev.johnoreilly.climatetrace.ui.AgentsScreen
29+
import dev.johnoreilly.climatetrace.ui.AgentScreen
3030
import dev.johnoreilly.climatetrace.ui.ClimateTraceScreen
3131
import dev.johnoreilly.wordmaster.androidApp.theme.ClimateTraceTheme
3232

@@ -71,7 +71,7 @@ fun AndroidApp() {
7171
Column(modifier = Modifier.padding(paddingValues)) {
7272
when (selectedIndex) {
7373
0 -> Navigator(screen = ClimateTraceScreen())
74-
else -> AgentsScreen()
74+
else -> AgentScreen()
7575
}
7676
}
7777
}

composeApp/src/androidMain/kotlin/dev/johnoreilly/climatetrace/agent/ClimateTraceAgent.android.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package dev.johnoreilly.climatetrace.agent
33
import ai.koog.prompt.executor.clients.google.GoogleModels
44
import ai.koog.prompt.executor.llms.all.simpleGoogleAIExecutor
55
import ai.koog.prompt.executor.model.PromptExecutor
6-
import ai.koog.prompt.llm.LLModel
6+
import dev.johnoreilly.climatetrace.BuildKonfig
77

88
actual fun getLLModel() = GoogleModels.Gemini2_5Flash
99

10-
actual fun getPromptExecutor(apiKey: String): PromptExecutor {
11-
return simpleGoogleAIExecutor(apiKey)
10+
actual fun getPromptExecutor(): PromptExecutor {
11+
return simpleGoogleAIExecutor(BuildKonfig.GEMINI_API_KEY)
1212
}

composeApp/src/commonMain/kotlin/App.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import androidx.compose.runtime.setValue
1717
import androidx.compose.ui.Modifier
1818
import cafe.adriel.voyager.navigator.Navigator
1919
import dev.johnoreilly.climatetrace.di.commonModule
20-
import dev.johnoreilly.climatetrace.ui.AgentsScreen
20+
import dev.johnoreilly.climatetrace.ui.AgentScreen
2121
import dev.johnoreilly.climatetrace.ui.ClimateTraceScreen
22+
import dev.johnoreilly.climatetrace.ui.theme.ClimateTraceTheme
2223
import org.jetbrains.compose.ui.tooling.preview.Preview
2324
import org.koin.compose.KoinApplication
2425

@@ -29,7 +30,7 @@ fun App() {
2930
KoinApplication(application = {
3031
modules(commonModule())
3132
}) {
32-
MaterialTheme {
33+
ClimateTraceTheme {
3334
var selectedIndex by remember { mutableIntStateOf(0) }
3435

3536
Scaffold(
@@ -53,7 +54,7 @@ fun App() {
5354
Column(Modifier.padding(paddingValues)) {
5455
when (selectedIndex) {
5556
0 -> Navigator(screen = ClimateTraceScreen())
56-
else -> AgentsScreen()
57+
else -> AgentScreen()
5758
}
5859
}
5960
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package dev.johnoreilly.climatetrace.agent
2+
3+
import ai.koog.agents.core.agent.AIAgent
4+
5+
/**
6+
* Interface for agent factory
7+
*/
8+
interface AgentProvider {
9+
val description: String
10+
11+
suspend fun provideAgent(
12+
onToolCallEvent: suspend (String) -> Unit,
13+
onErrorEvent: suspend (String) -> Unit,
14+
onAssistantMessage: suspend (String) -> String
15+
): AIAgent<String, String>
16+
}

composeApp/src/commonMain/kotlin/dev/johnoreilly/climatetrace/agent/ClimateTraceAgent.kt

Lines changed: 0 additions & 87 deletions
This file was deleted.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package dev.johnoreilly.climatetrace.agent
2+
3+
import ai.koog.agents.core.agent.AIAgent
4+
import ai.koog.agents.core.agent.config.AIAgentConfig
5+
import ai.koog.agents.core.agent.functionalStrategy
6+
import ai.koog.agents.core.dsl.extension.asAssistantMessage
7+
import ai.koog.agents.core.dsl.extension.containsToolCalls
8+
import ai.koog.agents.core.dsl.extension.executeMultipleTools
9+
import ai.koog.agents.core.dsl.extension.extractToolCalls
10+
import ai.koog.agents.core.dsl.extension.requestLLMMultiple
11+
import ai.koog.agents.core.dsl.extension.sendMultipleToolResults
12+
import ai.koog.agents.core.tools.ToolRegistry
13+
import ai.koog.agents.features.eventHandler.feature.EventHandler
14+
import ai.koog.prompt.dsl.prompt
15+
import ai.koog.prompt.executor.model.PromptExecutor
16+
import ai.koog.prompt.llm.LLModel
17+
import dev.johnoreilly.climatetrace.BuildKonfig
18+
import dev.johnoreilly.climatetrace.data.ClimateTraceRepository
19+
import kotlin.time.ExperimentalTime
20+
21+
22+
// TODO use Koin for these and inject?
23+
expect fun getLLModel(): LLModel
24+
expect fun getPromptExecutor(): PromptExecutor
25+
26+
class ClimateTraceAgentProvider(
27+
private val climateTraceRepository: ClimateTraceRepository
28+
) : AgentProvider {
29+
30+
override val description: String = "Hi, I'm a climate agent. I can provide climate emission information for different countries/years."
31+
32+
@OptIn(ExperimentalTime::class)
33+
override suspend fun provideAgent(
34+
onToolCallEvent: suspend (String) -> Unit,
35+
onErrorEvent: suspend (String) -> Unit,
36+
onAssistantMessage: suspend (String) -> String,
37+
): AIAgent<String, String> {
38+
39+
val toolRegistry = ToolRegistry {
40+
tool(CurrentDatetimeTool())
41+
tool(GetCountryTool(climateTraceRepository))
42+
tool(GetEmissionsTool(climateTraceRepository))
43+
tool(GetAssetEmissionsTool(climateTraceRepository))
44+
tool(GetPopulationTool(climateTraceRepository))
45+
46+
tool(ExitTool)
47+
}
48+
49+
val strategy = functionalStrategy<String, String> { initialInput ->
50+
var inputMessage = initialInput
51+
var lastAssistantMessage = ""
52+
53+
repeat(50) { // align with agentConfig.maxAgentIterations
54+
println("Calling LLM with Input = $inputMessage")
55+
var responses = requestLLMMultiple(inputMessage)
56+
57+
// Resolve tools until none left, mirroring graph strategy
58+
while (responses.containsToolCalls()) {
59+
val pendingCalls = extractToolCalls(responses)
60+
println("Pending Calls")
61+
println(pendingCalls.map { "${it.tool} ${it.content}" })
62+
63+
val results = executeMultipleTools(pendingCalls, parallelTools = true)
64+
65+
// Finish condition: if ExitTool is called, return its result directly
66+
if (results.size == 1 && results.first().tool == ExitTool.name) {
67+
return@functionalStrategy results.first().result!!.toString()
68+
}
69+
70+
// Send tool results back to LLM
71+
responses = sendMultipleToolResults(results)
72+
}
73+
74+
// No more tool calls: deliver assistant message to UI and get possible user follow-up
75+
lastAssistantMessage = responses.first().asAssistantMessage().content
76+
val userReply = onAssistantMessage(lastAssistantMessage)
77+
78+
// If user provides no reply, consider conversation finished and return assistant response
79+
if (userReply.isBlank()) {
80+
return@functionalStrategy lastAssistantMessage
81+
}
82+
83+
// Prepare for next loop iteration with user's reply
84+
inputMessage = userReply
85+
}
86+
87+
// Max iterations reached; return last assistant message
88+
lastAssistantMessage
89+
}
90+
91+
92+
val agentConfig = AIAgentConfig(
93+
prompt = prompt("climateTrace") {
94+
system(
95+
"""
96+
You an AI assistant specialising in providing information about global climate emissions.
97+
Use 3 letter country codes.
98+
The year is currently 2025.
99+
100+
Use the tools at your disposal to:
101+
1. Look up country codes from country names
102+
2. Get climate emission information.
103+
3. Get cause of emissions using asset emission information.
104+
4. Get population data.
105+
5. Get current date and time.
106+
107+
Pass the list of country codes and the year to the GetEmissionsTool tool to get climate emission information.
108+
Use units of millions for the emissions data.
109+
""".trimIndent(),
110+
)
111+
},
112+
model = getLLModel(),
113+
maxAgentIterations = 50
114+
)
115+
116+
// Return the agent
117+
return AIAgent(
118+
promptExecutor = getPromptExecutor(),
119+
strategy = strategy,
120+
agentConfig = agentConfig,
121+
toolRegistry = toolRegistry,
122+
) {
123+
install(EventHandler) {
124+
onToolCallStarting { ctx ->
125+
onToolCallEvent("Tool ${ctx.tool.name}, args ${ctx.toolArgs}")
126+
}
127+
128+
onAgentExecutionFailed { ctx ->
129+
onErrorEvent("${ctx.throwable.message}")
130+
}
131+
132+
onAgentCompleted { _ ->
133+
// Skip finish event handling
134+
}
135+
136+
}
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)