Skip to content

Commit a8c0131

Browse files
committed
- Replace OpenAI API with Gemini API for generating Kotlin topic details.
1 parent b5fc0c5 commit a8c0131

File tree

5 files changed

+93
-100
lines changed

5 files changed

+93
-100
lines changed

build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ plugins {
77
alias(libs.plugins.composeCompiler) apply false
88
alias(libs.plugins.kotlinMultiplatform) apply false
99
alias(libs.plugins.serialization) apply false
10-
}
10+
alias(libs.plugins.googleSecrets) apply false
11+
}
12+

composeApp/build.gradle.kts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ plugins {
1414
alias(libs.plugins.composeCompiler)
1515
alias(libs.plugins.serialization)
1616
alias(libs.plugins.ktlint)
17+
alias(libs.plugins.googleSecrets)
1718
}
1819

1920
ktlint {
@@ -74,6 +75,7 @@ kotlin {
7475
implementation(libs.androidx.activity.compose)
7576
implementation(libs.koin.android)
7677
implementation(libs.koin.androidx.compose)
78+
implementation(libs.generativeai)
7779
}
7880
commonMain.dependencies {
7981
implementation(compose.runtime)
@@ -164,11 +166,11 @@ fun ApplicationDefaultConfig.setupBuildConfigFields(
164166
) {
165167
fun secret(key: String): String = System.getenv(key) ?: properties.getProperty(key, "")
166168

167-
if (secret("OPEN_API_KEY").isEmpty()) {
168-
error("OPEN_API_KEY not set in local.properties")
169+
if (secret("GEMINI_API_KEY").isEmpty()) {
170+
error("GEMINI_API_KEY not set in local.properties")
169171
}
170172

171-
buildConfigField(type = "String", name = "OPEN_API_KEY", value = "\"${secret("OPEN_API_KEY")}\"")
173+
buildConfigField(type = "String", name = "GEMINI_API_KEY", value = "\"${secret("GEMINI_API_KEY")}\"")
172174
}
173175

174176
fun getLocalProperties(): Properties {

composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/Platform.android.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ class AndroidPlatform : Platform {
88

99
actual fun getPlatform(): Platform = AndroidPlatform()
1010

11-
actual fun getOpenApiKey() = BuildConfig.OPEN_API_KEY
11+
actual fun getOpenApiKey() = BuildConfig.GEMINI_API_KEY
Lines changed: 79 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,115 @@
11
package com.developersbreach.kotlindictionarymultiplatform.core.network
22

33
import com.developersbreach.kotlindictionarymultiplatform.Log
4-
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.ChatCompletionRequest
5-
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.ChatCompletionResponse
6-
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.ChatMessage
7-
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.FunctionDefinition
84
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.KotlinTopicDetails
95
import io.ktor.client.HttpClient
106
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
11-
import io.ktor.client.request.header
127
import io.ktor.client.request.post
138
import io.ktor.client.request.setBody
14-
import io.ktor.client.statement.HttpResponse
159
import io.ktor.client.statement.bodyAsText
1610
import io.ktor.http.ContentType
17-
import io.ktor.http.HttpHeaders
1811
import io.ktor.http.contentType
1912
import io.ktor.serialization.kotlinx.json.json
2013
import kotlinx.serialization.json.Json
21-
import kotlinx.serialization.json.decodeFromJsonElement
2214
import kotlinx.serialization.json.jsonObject
15+
import kotlinx.serialization.decodeFromString
16+
import kotlinx.serialization.json.addJsonObject
17+
import kotlinx.serialization.json.buildJsonObject
18+
import kotlinx.serialization.json.jsonArray
19+
import kotlinx.serialization.json.jsonPrimitive
20+
import kotlinx.serialization.json.put
21+
import kotlinx.serialization.json.putJsonArray
2322

2423
object KtorHttpClient {
25-
private val json = Json { ignoreUnknownKeys = true }
24+
25+
private val json = Json {
26+
ignoreUnknownKeys = true
27+
prettyPrint = true
28+
}
2629

2730
private val client = HttpClient {
2831
install(ContentNegotiation) {
2932
json()
3033
}
3134
}
3235

33-
private val functionSchema = json.parseToJsonElement(
34-
// Paste the full JSON schema for your function here
35-
"""
36-
{
37-
"type": "object",
38-
"properties": {
39-
"topicId": { "type": "string" },
40-
"topicName": { "type": "string" },
41-
"intro": { "type": "string" },
42-
"syntax": {
43-
"type": "object",
44-
"properties": {
45-
"signature": { "type": "string" },
46-
"notes": { "type": "string" }
47-
},
48-
"required": ["signature"]
49-
},
50-
"sections": {
51-
"type": "array",
52-
"items": {
53-
"type": "object",
54-
"properties": {
55-
"heading": { "type": "string" },
56-
"content": { "type": "string" },
57-
"codeExamples": {
58-
"type": "array",
59-
"items": {
60-
"type": "object",
61-
"properties": {
62-
"description": { "type": "string" },
63-
"code": { "type": "string" },
64-
"language": { "type": "string" }
65-
},
66-
"required": ["code"]
67-
}
68-
}
69-
},
70-
"required": ["heading","content"]
71-
}
72-
},
73-
"pitfalls": { "type": "array", "items": { "type": "string" } },
74-
"relatedTopics": { "type": "array", "items": { "type": "string" } },
75-
"metadata": { "type": "object", "additionalProperties": true }
76-
},
77-
"required": ["topicId","topicName","intro","syntax","sections"]
78-
}
79-
""".trimIndent(),
80-
)
81-
82-
private val functionDef = FunctionDefinition(
83-
name = "generate_kotlin_topic_details",
84-
description = "Return a fully-featured Kotlin documentation object for a given topic",
85-
parameters = functionSchema,
86-
)
87-
88-
/**
89-
* Calls the OpenAI ChatCompletion with function-calling to get topic details.
90-
* @param topicId the topic identifier, e.g. "variables".
91-
* @param apiKey your OpenAI API key.
92-
*/
9336
suspend fun generateTopicDetails(
9437
topicId: String,
9538
apiKey: String,
9639
): KotlinTopicDetails {
97-
// Prepare messages
98-
val messages = listOf(
99-
ChatMessage("system", "You are a Kotlin documentation generator."),
100-
ChatMessage("user", "Generate full Kotlin documentation for topic \"$topicId\"."),
101-
)
40+
// Gemini-style prompt
41+
val prompt = """
42+
You are a Kotlin documentation generator.
43+
Generate a JSON object for the topic "$topicId" with the following structure:
44+
45+
{
46+
"topicId": "...",
47+
"topicName": "...",
48+
"intro": "...",
49+
"syntax": {
50+
"signature": "...",
51+
"notes": "..."
52+
},
53+
"sections": [
54+
{
55+
"heading": "...",
56+
"content": "...",
57+
"codeExamples": [
58+
{
59+
"description": "...",
60+
"code": "...",
61+
"language": "kotlin"
62+
}
63+
]
64+
}
65+
],
66+
"pitfalls": ["..."],
67+
"relatedTopics": ["..."],
68+
"metadata": {}
69+
}
70+
71+
Respond only with pure JSON. No explanation or markdown.
72+
""".trimIndent()
10273

103-
// Build request body
104-
val request = ChatCompletionRequest(
105-
model = "gpt-4o-mini",
106-
messages = messages,
107-
functions = listOf(functionDef),
108-
functionCall = mapOf("name" to functionDef.name),
109-
)
74+
val requestBody = buildJsonObject {
75+
putJsonArray("contents") {
76+
addJsonObject {
77+
putJsonArray("parts") {
78+
addJsonObject {
79+
put("text", prompt)
80+
}
81+
}
82+
}
83+
}
84+
}
11085

111-
// Execute HTTP request
112-
val response: HttpResponse = client.post("https://api.openai.com/v1/chat/completions") {
113-
header(HttpHeaders.Authorization, "Bearer $apiKey")
86+
val response = client.post("https://generativelanguage.googleapis.com/v1/models/gemini-2.0-flash:generateContent?key=$apiKey") {
11487
contentType(ContentType.Application.Json)
115-
setBody(json.encodeToString(ChatCompletionRequest.serializer(), request))
88+
setBody(requestBody)
11689
}
11790

118-
val text = response.bodyAsText()
119-
Log.i("RawResponse", "RAW RESPONSE:\n$text")
91+
val responseBody = response.bodyAsText()
92+
Log.i("GeminiRawResponse", responseBody)
93+
94+
// Parse root object
95+
val root = json.parseToJsonElement(responseBody).jsonObject
96+
val candidates = root["candidates"]?.jsonArray ?: error("Missing candidates")
97+
val firstCandidate = candidates.first().jsonObject
98+
val content = firstCandidate["content"]?.jsonObject ?: error("Missing content")
99+
val partsArray = content["parts"]?.jsonArray ?: error("Missing parts array")
100+
val part = partsArray.first().jsonObject
101+
val rawJson = part["text"]?.jsonPrimitive?.content ?: error("Missing text in part")
102+
Log.i("RawJson", rawJson)
120103

121-
// Parse response
122-
val chatResp = json.decodeFromString(ChatCompletionResponse.serializer(), text)
123-
Log.i("ChatResponse", "$chatResp")
124-
val funcCall = chatResp.choices?.first()?.message?.functionCall ?: error("No function call in response")
104+
// Trim whitespace, remove code fences if any
105+
val cleanJson = rawJson.trim()
106+
.removePrefix("```json\n")
107+
.removePrefix("```json")
108+
.removePrefix("json\n")
109+
.removePrefix("json")
110+
.removeSuffix("```")
111+
.trim()
125112

126-
// The arguments field is a JSON string: parse and decode into our DTO
127-
val argsJson = json.parseToJsonElement(funcCall.arguments)
128-
return json.decodeFromJsonElement(argsJson.jsonObject)
113+
return json.decodeFromString(KotlinTopicDetails.serializer(), cleanJson)
129114
}
130115
}

gradle/libs.versions.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ androidx-material = "1.12.0"
1313
androidx-test-junit = "1.2.1"
1414
arrowFxCoroutines = "2.1.0"
1515
compose-multiplatform = "1.7.3"
16+
generativeai = "0.9.0"
1617
junit = "4.13.2"
1718
kermit = "2.0.4"
1819
kotlin = "2.1.10"
@@ -22,11 +23,13 @@ ktor-bom = "3.0.1"
2223
koin = "4.0.4"
2324
navigation-compose = "2.9.0"
2425
ktlint = "12.2.0"
26+
secrets = "2.0.1"
2527

2628
[libraries]
2729
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
2830
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowFxCoroutines" }
2931
arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrowFxCoroutines" }
32+
generativeai = { module = "com.google.ai.client.generativeai:generativeai", version.ref = "generativeai" }
3033
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
3134
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
3235
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
@@ -64,4 +67,5 @@ composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-mu
6467
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
6568
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
6669
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
67-
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
70+
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
71+
googleSecrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }

0 commit comments

Comments
 (0)