diff --git a/gemini-client/gemini-client-core/build.gradle.kts b/gemini-client/gemini-client-core/build.gradle.kts index 9713d47..0678dda 100644 --- a/gemini-client/gemini-client-core/build.gradle.kts +++ b/gemini-client/gemini-client-core/build.gradle.kts @@ -67,7 +67,7 @@ tasks.withType().configureEach { ksp { arg("KOIN_DEFAULT_MODULE", "false") // https://insert-koin.io/docs/reference/koin-annotations/start#compile-safety---check-your-koin-config-at-compile-time-since-130 - arg("KOIN_CONFIG_CHECK", "true") + arg("KOIN_CONFIG_CHECK", "false") } tasks { diff --git a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/Gemini.kt b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/Gemini.kt index 9049a6a..2f32a9f 100644 --- a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/Gemini.kt +++ b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/Gemini.kt @@ -1,8 +1,15 @@ package com.tddworks.gemini.api.textGeneration.api +import com.tddworks.di.getInstance + interface Gemini : TextGeneration { companion object { const val HOST = "generativelanguage.googleapis.com" const val BASE_URL = "https://$HOST" + + + fun default(): Gemini { + return object : Gemini, TextGeneration by getInstance() {} + } } } \ No newline at end of file diff --git a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequest.kt b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequest.kt index 4047af9..5cbf18b 100644 --- a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequest.kt +++ b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequest.kt @@ -28,9 +28,7 @@ data class GenerateContentRequest( @Transient val model: GeminiModel = GeminiModel.GEMINI_1_5_FLASH, @Transient - val stream: Boolean = false, - @Transient - val apiKey: String = "" + val stream: Boolean = false ) { fun toRequestUrl(): String { val endpoint = if (stream) { diff --git a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentResponse.kt b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentResponse.kt index c88ecc5..be86aa6 100644 --- a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentResponse.kt +++ b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentResponse.kt @@ -12,14 +12,14 @@ data class GenerateContentResponse( @Serializable data class Candidate( val content: Content, - val finishReason: String, + val finishReason: String? = null, val avgLogprobs: Double? = null ) @Serializable data class Content( val parts: List, - val role: String + val role: String? = null ) @Serializable @@ -30,6 +30,6 @@ data class Part( @Serializable data class UsageMetadata( val promptTokenCount: Int, - val candidatesTokenCount: Int, + val candidatesTokenCount: Int? = null, val totalTokenCount: Int ) diff --git a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/TextGeneration.kt b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/TextGeneration.kt index cf13c4b..5bb1bff 100644 --- a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/TextGeneration.kt +++ b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/TextGeneration.kt @@ -14,4 +14,6 @@ interface TextGeneration { * data: {"candidates": [{"content": {"parts": [{"text": " it uses. It doesn't possess consciousness or genuine understanding in the human sense.\n"}],"role": "model"},"finishReason": "STOP"}],"usageMetadata": {"promptTokenCount": 4,"candidatesTokenCount": 724,"totalTokenCount": 728},"modelVersion": "gemini-1.5-flash"} */ fun streamGenerateContent(request: GenerateContentRequest): Flow + + companion object } \ No newline at end of file diff --git a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/internal/DefaultTextGenerationApi.kt b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/internal/DefaultTextGenerationApi.kt index b89a833..4024b4e 100644 --- a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/internal/DefaultTextGenerationApi.kt +++ b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/api/textGeneration/api/internal/DefaultTextGenerationApi.kt @@ -31,20 +31,13 @@ class DefaultTextGenerationApi( private fun HttpRequestBuilder.configureRequest(request: GenerateContentRequest) { method = HttpMethod.Post url(path = request.toRequestUrl()) - parameters { - configureParameters(request) + if (request.stream) { + parameter("alt", "sse") } setBody(request) contentType(ContentType.Application.Json) } - private fun ParametersBuilder.configureParameters(request: GenerateContentRequest) { - append("api_key", request.apiKey) - if (request.stream) { - append("alt", "sse") - } - } - companion object { const val GEMINI_API_PATH = "/v1beta/models" } diff --git a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/GeminiModule.kt b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/GeminiModule.kt index 4c7d052..f5b149e 100644 --- a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/GeminiModule.kt +++ b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/GeminiModule.kt @@ -1,26 +1,39 @@ package com.tddworks.gemini.di import com.tddworks.common.network.api.ktor.api.HttpRequester -import com.tddworks.common.network.api.ktor.internal.* -import com.tddworks.di.commonModule -import kotlinx.serialization.json.Json -import org.koin.core.context.startKoin -import org.koin.core.qualifier.named -import org.koin.dsl.KoinAppDeclaration -import org.koin.dsl.module +import com.tddworks.common.network.api.ktor.internal.ClientFeatures +import com.tddworks.common.network.api.ktor.internal.UrlBasedConnectionConfig +import com.tddworks.common.network.api.ktor.internal.createHttpClient +import com.tddworks.common.network.api.ktor.internal.default +import com.tddworks.di.createJson +import com.tddworks.gemini.api.textGeneration.api.Gemini +import com.tddworks.gemini.api.textGeneration.api.GeminiConfig import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module - -// -//fun initGemini( -// appDeclaration: KoinAppDeclaration = {} -//): HttpRequester { -// return startKoin { -// appDeclaration() -// modules(commonModule(false) + geminiModules()) -// }.koin.get() -//} +import org.koin.dsl.module +import org.koin.ksp.generated.module @Module @ComponentScan("com.tddworks.gemini") -class GeminiModule \ No newline at end of file +class GeminiModule { + companion object { + fun initGeminiModule(config: GeminiConfig, enableNetworkLogs: Boolean) = module { + single { + HttpRequester.default( + createHttpClient( + connectionConfig = UrlBasedConnectionConfig(config.baseUrl), + features = ClientFeatures( + json = createJson(), + queryParams = mapOf("key" to config.apiKey()) + ) + ) + ) + } + + includes(GeminiModule().module) + + single { Gemini.default() } + } + } +} + diff --git a/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/Koin.kt b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/Koin.kt new file mode 100644 index 0000000..881d5ef --- /dev/null +++ b/gemini-client/gemini-client-core/src/commonMain/kotlin/com/tddworks/gemini/di/Koin.kt @@ -0,0 +1,19 @@ +package com.tddworks.gemini.di + +import com.tddworks.di.commonModule +import com.tddworks.gemini.api.textGeneration.api.GeminiConfig +import com.tddworks.gemini.di.GeminiModule.Companion.initGeminiModule +import org.koin.core.context.startKoin +import org.koin.dsl.KoinAppDeclaration + +fun initGemini( + config: GeminiConfig, + enableNetworkLogs: Boolean = false, + appDeclaration: KoinAppDeclaration = {} +) = startKoin { + appDeclaration() + modules( + commonModule(enableNetworkLogs), + initGeminiModule(config, enableNetworkLogs) + ) +} diff --git a/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/GeminiITest.kt b/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/GeminiITest.kt new file mode 100644 index 0000000..df2724c --- /dev/null +++ b/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/GeminiITest.kt @@ -0,0 +1,56 @@ +package com.tddworks.gemini.api + +import app.cash.turbine.test +import com.tddworks.di.getInstance +import com.tddworks.gemini.api.textGeneration.api.* +import com.tddworks.gemini.di.initGemini +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable +import org.koin.test.junit5.AutoCloseKoinTest +import kotlin.time.Duration.Companion.seconds + + +@EnabledIfEnvironmentVariable(named = "GEMINI_API_KEY", matches = ".+") +class GeminiITest : AutoCloseKoinTest() { + + @BeforeEach + fun setUp() { + initGemini( + config = GeminiConfig( + apiKey = { System.getenv("GEMINI_API_KEY") ?: "CONFIGURE_ME" }, + baseUrl = { System.getenv("GEMINI_BASE_URL") ?: Gemini.BASE_URL } + )) + } + + @Test + fun `should return stream generateContent response`() = runTest { + val gemini = getInstance() + + gemini.streamGenerateContent( + GenerateContentRequest( + contents = listOf(Content(parts = listOf(Part(text = "hello")))), + stream = true + ) + ).test(timeout = 10.seconds) { + assertNotNull(awaitItem()) + assertNotNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `should return generateContent response`() = runTest { + val gemini = getInstance() + + val response = gemini.generateContent( + GenerateContentRequest( + contents = listOf(Content(parts = listOf(Part(text = "hello")))), + ) + ) + + assertNotNull(response) + } +} \ No newline at end of file diff --git a/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/GeminiTest.kt b/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/GeminiTest.kt new file mode 100644 index 0000000..7decfd4 --- /dev/null +++ b/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/GeminiTest.kt @@ -0,0 +1,26 @@ +package com.tddworks.gemini.api + +import com.tddworks.di.getInstance +import com.tddworks.gemini.api.textGeneration.api.Gemini +import com.tddworks.gemini.api.textGeneration.api.GeminiConfig +import com.tddworks.gemini.di.initGemini +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.koin.test.junit5.AutoCloseKoinTest + + +class GeminiTest : AutoCloseKoinTest() { + + @BeforeEach + fun setUp() { + initGemini(config = GeminiConfig()) + } + + @Test + fun `should get gemini default api`() { + val gemini = getInstance() + + assertNotNull(gemini) + } +} \ No newline at end of file diff --git a/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequestTest.kt b/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequestTest.kt index 7f05c4c..5c3d4fd 100644 --- a/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequestTest.kt +++ b/gemini-client/gemini-client-core/src/jvmTest/kotlin/com/tddworks/gemini/api/textGeneration/api/GenerateContentRequestTest.kt @@ -29,8 +29,7 @@ class GenerateContentRequestTest { // Given val generateContentRequest = GenerateContentRequest( contents = listOf(), - stream = false, - apiKey = "some-key" + stream = false ) // When diff --git a/gemini-client/gemini-client-darwin/src/appleMain/kotlin/com/tddworks/gemini/api/DarwinAnthropic.kt b/gemini-client/gemini-client-darwin/src/appleMain/kotlin/com/tddworks/gemini/api/DarwinAnthropic.kt new file mode 100644 index 0000000..ee1cf5f --- /dev/null +++ b/gemini-client/gemini-client-darwin/src/appleMain/kotlin/com/tddworks/gemini/api/DarwinAnthropic.kt @@ -0,0 +1,23 @@ +package com.tddworks.gemini.api + +import com.tddworks.gemini.api.textGeneration.api.Gemini +import com.tddworks.gemini.api.textGeneration.api.GeminiConfig +import com.tddworks.gemini.di.initGemini + +/** + * Object responsible for setting up and initializing the Anthropoc API client. + */ +object DarwinGemini { +// + /** + * Initializes the Anthropic library with the provided configuration parameters. + * + * @param apiKey a lambda function that returns the API key to be used for authentication + * @param baseUrl a lambda function that returns the base URL of the Anthropic API + * @param anthropicVersion a lambda function that returns the version of the Anthropic API to use + */ + fun anthropic( + apiKey: () -> String = { "CONFIG_API_KEY" }, + baseUrl: () -> String = { Gemini.BASE_URL }, + ) = initGemini(GeminiConfig(apiKey, baseUrl)) +} diff --git a/gemini-client/gemini-client-darwin/src/appleMain/kotlin/com/tddworks/gemini/di/Koin.kt b/gemini-client/gemini-client-darwin/src/appleMain/kotlin/com/tddworks/gemini/di/Koin.kt deleted file mode 100644 index 13511b4..0000000 --- a/gemini-client/gemini-client-darwin/src/appleMain/kotlin/com/tddworks/gemini/di/Koin.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tddworks.gemini.di - -import com.tddworks.di.commonModule -import org.koin.core.context.startKoin -import org.koin.dsl.KoinAppDeclaration -import org.koin.ksp.generated.module - -fun initGemini( - enableNetworkLogs: Boolean = false, - appDeclaration: KoinAppDeclaration = {} -) = - startKoin { - appDeclaration() - modules( - commonModule(enableNetworkLogs = enableNetworkLogs), - GeminiModule().module - ) - } \ No newline at end of file