diff --git a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/SerpApiExample.kt b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/SerpApiExample.kt new file mode 100644 index 000000000..42f3f736a --- /dev/null +++ b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/SerpApiExample.kt @@ -0,0 +1,30 @@ +package com.xebia.functional.xef.auto.reasoning + +import com.xebia.functional.gpt4all.getOrThrow +import com.xebia.functional.xef.auto.ai +import com.xebia.functional.xef.reasoning.serpapi.SerpApiClient + +suspend fun main() { + ai { + val client = SerpApiClient() + + val searchData = SerpApiClient.SearchData( + search = "german+shepper", + location = "Villavicencio,+Meta,+Colombia", + language = "en", + region = "us", + googleDomain = "google.com" + ) + + val answer = client.search(searchData) + + answer.searchResults.forEach { + println( + "\n\uD83E\uDD16 Search Information:\n\n" + + "Title: ${it.title}\n" + + "Document: ${it.document}\n" + + "Source: ${it.source}\n" + ) + } + }.getOrThrow() +} diff --git a/integrations/gcp/src/commonMain/kotlin/com/xebia/functional/xef/gcp/GcpClient.kt b/integrations/gcp/src/commonMain/kotlin/com/xebia/functional/xef/gcp/GcpClient.kt index 39460372c..cb36c6876 100644 --- a/integrations/gcp/src/commonMain/kotlin/com/xebia/functional/xef/gcp/GcpClient.kt +++ b/integrations/gcp/src/commonMain/kotlin/com/xebia/functional/xef/gcp/GcpClient.kt @@ -1,6 +1,8 @@ package com.xebia.functional.xef.gcp import com.xebia.functional.xef.AIError +import com.xebia.functional.xef.auto.AutoClose +import com.xebia.functional.xef.auto.autoClose import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.plugins.* @@ -23,7 +25,7 @@ class GcpClient( private val projectId: String, val modelId: String, private val token: String -) : AutoCloseable { +) : AutoCloseable, AutoClose by autoClose() { private val http: HttpClient = HttpClient { install(HttpTimeout) { requestTimeoutMillis = 60_000 diff --git a/reasoning/build.gradle.kts b/reasoning/build.gradle.kts index c21e67ee7..452dc534f 100644 --- a/reasoning/build.gradle.kts +++ b/reasoning/build.gradle.kts @@ -8,9 +8,9 @@ repositories { plugins { base - alias(libs.plugins.kotlin.multiplatform) + id(libs.plugins.kotlin.multiplatform.get().pluginId) alias(libs.plugins.kotest.multiplatform) - alias(libs.plugins.kotlinx.serialization) + id(libs.plugins.kotlinx.serialization.get().pluginId) alias(libs.plugins.spotless) alias(libs.plugins.dokka) alias(libs.plugins.arrow.gradle.publish) @@ -68,6 +68,7 @@ kotlin { implementation(libs.okio) implementation(libs.klogging) implementation(libs.uuid) + implementation(libs.bundles.ktor.client) } } @@ -84,10 +85,15 @@ kotlin { implementation(libs.logback) implementation(projects.xefPdf) implementation(projects.xefFilesystem) + api(libs.ktor.client.cio) } } - val jsMain by getting + val jsMain by getting { + dependencies { + api(libs.ktor.client.js) + } + } val jvmTest by getting { dependencies { @@ -95,9 +101,29 @@ kotlin { } } - val linuxX64Main by getting - val macosX64Main by getting - val mingwX64Main by getting + val linuxX64Main by getting { + dependencies { + api(libs.ktor.client.cio) + } + } + + val macosX64Main by getting { + dependencies { + api(libs.ktor.client.cio) + } + } + + val macosArm64Main by getting { + dependencies { + api(libs.ktor.client.cio) + } + } + + val mingwX64Main by getting { + dependencies { + api(libs.ktor.client.winhttp) + } + } create("nativeMain") { dependsOn(commonMain) diff --git a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/serpapi/SerpApiClient.kt b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/serpapi/SerpApiClient.kt new file mode 100644 index 000000000..a3e5bc501 --- /dev/null +++ b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/serpapi/SerpApiClient.kt @@ -0,0 +1,91 @@ +package com.xebia.functional.xef.reasoning.serpapi + +import com.xebia.functional.xef.auto.AutoClose +import com.xebia.functional.xef.auto.autoClose +import com.xebia.functional.xef.env.getenv +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +class SerpApiClient : AutoCloseable, AutoClose by autoClose() { + + private val serpApiKey: String? + private val SERP_API_KEY_NOT_FOUND = "Missing SERP_API_KEY env var" + + init { + serpApiKey = + getenv("SERP_API_KEY") + ?: throw SerpApiClientException(HttpStatusCode.Unauthorized, SERP_API_KEY_NOT_FOUND) + + if (serpApiKey.isEmpty()) + throw SerpApiClientException(HttpStatusCode.Unauthorized, SERP_API_KEY_NOT_FOUND) + } + + private val http: HttpClient = HttpClient { + install(HttpTimeout) { + requestTimeoutMillis = 60_000 + connectTimeoutMillis = 60_000 + } + install(HttpRequestRetry) { + maxRetries = 5 + retryIf { _, response -> !response.status.isSuccess() } + retryOnExceptionIf { _, _ -> true } + delayMillis { retry -> retry * 3000L } + } + install(ContentNegotiation) { + json( + Json { + encodeDefaults = false + isLenient = true + ignoreUnknownKeys = true + } + ) + } + } + + data class SearchData( + val search: String, + val location: String? = null, + val language: String? = null, + val region: String? = null, + val googleDomain: String = "google.com" + ) + + @Serializable + data class SearchResults(@SerialName("organic_results") val searchResults: List) + + @Serializable + data class SearchResult( + val title: String, + @SerialName("snippet") val document: String, + @SerialName("link") val source: String + ) + + suspend fun search(searchData: SearchData): SearchResults { + + return http + .get( + "https://serpapi.com/search.json?q=${searchData.search}&location=${searchData.location}&hl=${searchData.language}&gl=${searchData.region}" + + "&google_domain=${searchData.googleDomain}&api_key=${serpApiKey}" + ) { + contentType(ContentType.Application.Json) + } + .body() + } + + class SerpApiClientException( + private val httpStatusCode: HttpStatusCode, + private val error: String + ) : IllegalStateException("$httpStatusCode: $error") + + override fun close() { + http.close() + } +}