diff --git a/common/src/main/kotlin/com/google/ai/client/generativeai/common/APIController.kt b/common/src/main/kotlin/com/google/ai/client/generativeai/common/APIController.kt index 09daf5e4..bd18784e 100644 --- a/common/src/main/kotlin/com/google/ai/client/generativeai/common/APIController.kt +++ b/common/src/main/kotlin/com/google/ai/client/generativeai/common/APIController.kt @@ -64,12 +64,20 @@ val JSON = Json { * @property apiVersion the endpoint version to communicate with. * @property timeout the maximum amount of time for a request to take in the initial exchange. */ -class APIController( +class APIController +internal constructor( private val key: String, model: String, private val requestOptions: RequestOptions, - httpEngine: HttpClientEngine = OkHttp.create(), + httpEngine: HttpClientEngine ) { + + constructor( + key: String, + model: String, + requestOptions: RequestOptions + ) : this(key, model, requestOptions, OkHttp.create()) + private val model = fullModelName(model) private val client = diff --git a/common/src/test/java/com/google/ai/client/generativeai/common/util/tests.kt b/common/src/test/java/com/google/ai/client/generativeai/common/util/tests.kt index 2655d086..d753a9bf 100644 --- a/common/src/test/java/com/google/ai/client/generativeai/common/util/tests.kt +++ b/common/src/test/java/com/google/ai/client/generativeai/common/util/tests.kt @@ -18,8 +18,6 @@ package com.google.ai.client.generativeai.common.util -// import com.google.ai.client.generativeai.internal.util.send -// import com.google.ai.client.generativeai.type.RequestOptions import com.google.ai.client.generativeai.common.APIController import com.google.ai.client.generativeai.common.GenerateContentRequest import com.google.ai.client.generativeai.common.GenerateContentResponse diff --git a/generativeai/build.gradle.kts b/generativeai/build.gradle.kts index 39d5f72c..bac2aefa 100644 --- a/generativeai/build.gradle.kts +++ b/generativeai/build.gradle.kts @@ -73,15 +73,8 @@ android { } dependencies { - val ktorVersion = "2.3.2" + implementation(project(":common")) - implementation("io.ktor:ktor-client-okhttp:$ktorVersion") - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("io.ktor:ktor-client-logging:$ktorVersion") - - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") implementation("androidx.core:core-ktx:1.12.0") implementation("org.slf4j:slf4j-nop:2.0.9") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") @@ -92,10 +85,9 @@ dependencies { implementation("androidx.concurrent:concurrent-futures:1.2.0-alpha02") implementation("androidx.concurrent:concurrent-futures-ktx:1.2.0-alpha02") testImplementation("junit:junit:4.13.2") - testImplementation("io.kotest:kotest-assertions-core:4.0.7") - testImplementation("io.kotest:kotest-assertions-jvm:4.0.7") - testImplementation("io.kotest:kotest-assertions-json:4.0.7") - testImplementation("io.ktor:ktor-client-mock:$ktorVersion") + testImplementation("io.kotest:kotest-assertions-core:5.5.5") + testImplementation("io.kotest:kotest-assertions-core-jvm:5.5.5") + testImplementation("io.mockk:mockk:1.13.10") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/GenerativeModel.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/GenerativeModel.kt index 77c86cce..690f5be4 100644 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/GenerativeModel.kt +++ b/generativeai/src/main/java/com/google/ai/client/generativeai/GenerativeModel.kt @@ -17,9 +17,9 @@ package com.google.ai.client.generativeai import android.graphics.Bitmap -import com.google.ai.client.generativeai.internal.api.APIController -import com.google.ai.client.generativeai.internal.api.CountTokensRequest -import com.google.ai.client.generativeai.internal.api.GenerateContentRequest +import com.google.ai.client.generativeai.common.APIController +import com.google.ai.client.generativeai.common.CountTokensRequest +import com.google.ai.client.generativeai.common.GenerateContentRequest import com.google.ai.client.generativeai.internal.util.toInternal import com.google.ai.client.generativeai.internal.util.toPublic import com.google.ai.client.generativeai.type.Content @@ -71,7 +71,7 @@ internal constructor( generationConfig, safetySettings, requestOptions, - APIController(apiKey, modelName, requestOptions) + APIController(apiKey, modelName, requestOptions.toInternal()) ) /** @@ -97,8 +97,8 @@ internal constructor( fun generateContentStream(vararg prompt: Content): Flow = controller .generateContentStream(constructRequest(*prompt)) - .map { it.toPublic().validate() } .catch { throw GoogleGenerativeAIException.from(it) } + .map { it.toPublic().validate() } /** * Generates a response from the backend with the provided text represented [Content]. diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/APIController.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/APIController.kt deleted file mode 100644 index dc3c343f..00000000 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/APIController.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.internal.api - -import com.google.ai.client.generativeai.BuildConfig -import com.google.ai.client.generativeai.internal.util.decodeToFlow -import com.google.ai.client.generativeai.type.InvalidAPIKeyException -import com.google.ai.client.generativeai.type.RequestOptions -import com.google.ai.client.generativeai.type.ServerException -import com.google.ai.client.generativeai.type.UnsupportedUserLocationException -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.engine.HttpClientEngine -import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.HttpRequestBuilder -import io.ktor.client.request.header -import io.ktor.client.request.post -import io.ktor.client.request.preparePost -import io.ktor.client.request.setBody -import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.bodyAsChannel -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.timeout -import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json - -internal const val DOMAIN = "https://generativelanguage.googleapis.com" - -internal val JSON = Json { - ignoreUnknownKeys = true - prettyPrint = false -} - -/** - * Backend class for interfacing with the Gemini API. - * - * This class handles making HTTP requests to the API and streaming the responses back. - * - * @param httpEngine The HTTP client engine to be used for making requests. Defaults to CIO engine. - * Exposed primarily for DI in tests. - * @property key The API key used for authentication. - * @property model The model to use for generation. - * @property apiVersion the endpoint version to communicate with. - * @property timeout the maximum amount of time for a request to take in the initial exchange. - */ -internal class APIController( - private val key: String, - model: String, - private val requestOptions: RequestOptions, - httpEngine: HttpClientEngine = OkHttp.create(), -) { - private val model = fullModelName(model) - - private val client = - HttpClient(httpEngine) { - install(HttpTimeout) { - requestTimeoutMillis = requestOptions.timeout.inWholeMilliseconds - socketTimeoutMillis = 80_000 - } - install(ContentNegotiation) { json(JSON) } - } - - suspend fun generateContent(request: GenerateContentRequest): GenerateContentResponse = - client - .post("$DOMAIN/${requestOptions.apiVersion}/$model:generateContent") { - applyCommonConfiguration(request) - } - .also { validateResponse(it) } - .body() - - fun generateContentStream(request: GenerateContentRequest): Flow { - return client.postStream( - "$DOMAIN/${requestOptions.apiVersion}/$model:streamGenerateContent?alt=sse" - ) { - applyCommonConfiguration(request) - } - } - - suspend fun countTokens(request: CountTokensRequest): CountTokensResponse = - client - .post("$DOMAIN/${requestOptions.apiVersion}/$model:countTokens") { - applyCommonConfiguration(request) - } - .also { validateResponse(it) } - .body() - - private fun HttpRequestBuilder.applyCommonConfiguration(request: Request) { - when (request) { - is GenerateContentRequest -> setBody(request) - is CountTokensRequest -> setBody(request) - } - contentType(ContentType.Application.Json) - header("x-goog-api-key", key) - header("x-goog-api-client", "genai-android/${BuildConfig.VERSION_NAME}") - } -} - -/** - * Ensures the model name provided has a `models/` prefix - * - * Models must be prepended with the `models/` prefix when communicating with the backend. - */ -private fun fullModelName(name: String): String = name.takeIf { it.contains("/") } ?: "models/$name" - -/** - * Makes a POST request to the specified [url] and returns a [Flow] of deserialized response objects - * of type [R]. The response is expected to be a stream of JSON objects that are parsed in real-time - * as they are received from the server. - * - * This function is intended for internal use within the client that handles streaming responses. - * - * Example usage: - * ``` - * val client: HttpClient = HttpClient(CIO) - * val request: Request = GenerateContentRequest(...) - * val url: String = "http://example.com/stream" - * - * val responses: GenerateContentResponse = client.postStream(url) { - * setBody(request) - * contentType(ContentType.Application.Json) - * } - * responses.collect { - * println("Got a response: $it") - * } - * ``` - * - * @param R The type of the response object. - * @param url The URL to which the POST request will be made. - * @param config An optional [HttpRequestBuilder] callback for request configuration. - * @return A [Flow] of response objects of type [R]. - */ -private inline fun HttpClient.postStream( - url: String, - crossinline config: HttpRequestBuilder.() -> Unit = {} -): Flow = channelFlow { - launch(CoroutineName("postStream")) { - preparePost(url) { config() } - .execute { - validateResponse(it) - - val channel = it.bodyAsChannel() - val flow = JSON.decodeToFlow(channel) - - flow.collect { send(it) } - } - } -} - -private suspend fun validateResponse(response: HttpResponse) { - if (response.status != HttpStatusCode.OK) { - val text = response.bodyAsText() - val message = - try { - JSON.decodeFromString(text).error.message - } catch (e: Throwable) { - "Unexpected Response:\n$text" - } - if (message.contains("API key not valid")) { - throw InvalidAPIKeyException(message) - } - // TODO (b/325117891): Use a better method than string matching. - if (message == "User location is not supported for the API use.") { - throw UnsupportedUserLocationException() - } - throw ServerException(message) - } -} diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/Request.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/Request.kt deleted file mode 100644 index 44cbe85d..00000000 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/Request.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.internal.api - -import com.google.ai.client.generativeai.internal.api.client.GenerationConfig -import com.google.ai.client.generativeai.internal.api.shared.Content -import com.google.ai.client.generativeai.internal.api.shared.SafetySetting -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -internal sealed interface Request - -@Serializable -internal data class GenerateContentRequest( - val model: String, - val contents: List, - @SerialName("safety_settings") val safetySettings: List? = null, - @SerialName("generation_config") val generationConfig: GenerationConfig? = null, -) : Request - -@Serializable -internal data class CountTokensRequest(val model: String, val contents: List) : Request diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/Response.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/Response.kt deleted file mode 100644 index df142c4f..00000000 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/Response.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.internal.api - -import com.google.ai.client.generativeai.internal.api.server.Candidate -import com.google.ai.client.generativeai.internal.api.server.GRpcError -import com.google.ai.client.generativeai.internal.api.server.PromptFeedback -import kotlinx.serialization.Serializable - -internal sealed interface Response - -@Serializable -internal data class GenerateContentResponse( - val candidates: List? = null, - val promptFeedback: PromptFeedback? = null, -) : Response - -@Serializable internal data class CountTokensResponse(val totalTokens: Int) : Response - -@Serializable internal data class GRpcErrorResponse(val error: GRpcError) : Response diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/client/Types.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/client/Types.kt deleted file mode 100644 index f1f778bf..00000000 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/client/Types.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.internal.api.client - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class GenerationConfig( - val temperature: Float?, - @SerialName("top_p") val topP: Float?, - @SerialName("top_k") val topK: Int?, - @SerialName("candidate_count") val candidateCount: Int?, - @SerialName("max_output_tokens") val maxOutputTokens: Int?, - @SerialName("stop_sequences") val stopSequences: List? -) diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/server/Types.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/server/Types.kt deleted file mode 100644 index 21b8bd30..00000000 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/server/Types.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.internal.api.server - -import com.google.ai.client.generativeai.internal.api.shared.Content -import com.google.ai.client.generativeai.internal.api.shared.HarmCategory -import com.google.ai.client.generativeai.internal.util.FirstOrdinalSerializer -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames - -internal object BlockReasonSerializer : - KSerializer by FirstOrdinalSerializer(BlockReason::class) - -internal object HarmProbabilitySerializer : - KSerializer by FirstOrdinalSerializer(HarmProbability::class) - -internal object FinishReasonSerializer : - KSerializer by FirstOrdinalSerializer(FinishReason::class) - -@Serializable -internal data class PromptFeedback( - val blockReason: BlockReason? = null, - val safetyRatings: List? = null, -) - -@Serializable(BlockReasonSerializer::class) -internal enum class BlockReason { - UNKNOWN, - @SerialName("BLOCKED_REASON_UNSPECIFIED") UNSPECIFIED, - SAFETY, - OTHER -} - -@Serializable -internal data class Candidate( - val content: Content? = null, - val finishReason: FinishReason? = null, - val safetyRatings: List? = null, - val citationMetadata: CitationMetadata? = null -) - -@Serializable -internal data class CitationMetadata -@OptIn(ExperimentalSerializationApi::class) -constructor(@JsonNames("citations") val citationSources: List) - -@Serializable -internal data class CitationSources( - val startIndex: Int, - val endIndex: Int, - val uri: String, - val license: String -) - -@Serializable -internal data class SafetyRating( - val category: HarmCategory, - val probability: HarmProbability, - val blocked: Boolean? = null // TODO(): any reason not to default to false? -) - -@Serializable(HarmProbabilitySerializer::class) -internal enum class HarmProbability { - UNKNOWN, - @SerialName("HARM_PROBABILITY_UNSPECIFIED") UNSPECIFIED, - NEGLIGIBLE, - LOW, - MEDIUM, - HIGH -} - -@Serializable(FinishReasonSerializer::class) -internal enum class FinishReason { - UNKNOWN, - @SerialName("FINISH_REASON_UNSPECIFIED") UNSPECIFIED, - STOP, - MAX_TOKENS, - SAFETY, - RECITATION, - OTHER -} - -@Serializable -internal data class GRpcError( - val code: Int, - val message: String, -) diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/shared/Types.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/shared/Types.kt deleted file mode 100644 index 6202a619..00000000 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/shared/Types.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.internal.api.shared - -import com.google.ai.client.generativeai.internal.util.FirstOrdinalSerializer -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.EncodeDefault -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.JsonContentPolymorphicSerializer -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.jsonObject - -internal object HarmCategorySerializer : - KSerializer by FirstOrdinalSerializer(HarmCategory::class) - -@Serializable(HarmCategorySerializer::class) -internal enum class HarmCategory { - UNKNOWN, - @SerialName("HARM_CATEGORY_HARASSMENT") HARASSMENT, - @SerialName("HARM_CATEGORY_HATE_SPEECH") HATE_SPEECH, - @SerialName("HARM_CATEGORY_SEXUALLY_EXPLICIT") SEXUALLY_EXPLICIT, - @SerialName("HARM_CATEGORY_DANGEROUS_CONTENT") DANGEROUS_CONTENT -} - -typealias Base64 = String - -@ExperimentalSerializationApi -@Serializable -internal data class Content(@EncodeDefault val role: String? = "user", val parts: List) - -@Serializable(PartSerializer::class) internal sealed interface Part - -@Serializable internal data class TextPart(val text: String) : Part - -@Serializable internal data class BlobPart(@SerialName("inline_data") val inlineData: Blob) : Part - -@Serializable -internal data class Blob( - @SerialName("mime_type") val mimeType: String, - val data: Base64, -) - -@Serializable -internal data class SafetySetting(val category: HarmCategory, val threshold: HarmBlockThreshold) - -@Serializable -internal enum class HarmBlockThreshold { - @SerialName("HARM_BLOCK_THRESHOLD_UNSPECIFIED") UNSPECIFIED, - BLOCK_LOW_AND_ABOVE, - BLOCK_MEDIUM_AND_ABOVE, - BLOCK_ONLY_HIGH, - BLOCK_NONE, -} - -internal object PartSerializer : JsonContentPolymorphicSerializer(Part::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - val jsonObject = element.jsonObject - return when { - "text" in jsonObject -> TextPart.serializer() - "inlineData" in jsonObject -> BlobPart.serializer() - else -> throw SerializationException("Unknown Part type") - } - } -} diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/conversions.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/conversions.kt index df0d5285..e4b236b7 100644 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/conversions.kt +++ b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/conversions.kt @@ -19,24 +19,25 @@ package com.google.ai.client.generativeai.internal.util import android.graphics.Bitmap import android.graphics.BitmapFactory import android.util.Base64 -import com.google.ai.client.generativeai.internal.api.CountTokensResponse -import com.google.ai.client.generativeai.internal.api.GenerateContentResponse -import com.google.ai.client.generativeai.internal.api.client.GenerationConfig -import com.google.ai.client.generativeai.internal.api.server.BlockReason -import com.google.ai.client.generativeai.internal.api.server.Candidate -import com.google.ai.client.generativeai.internal.api.server.CitationSources -import com.google.ai.client.generativeai.internal.api.server.FinishReason -import com.google.ai.client.generativeai.internal.api.server.HarmProbability -import com.google.ai.client.generativeai.internal.api.server.PromptFeedback -import com.google.ai.client.generativeai.internal.api.server.SafetyRating -import com.google.ai.client.generativeai.internal.api.shared.Blob -import com.google.ai.client.generativeai.internal.api.shared.BlobPart -import com.google.ai.client.generativeai.internal.api.shared.Content -import com.google.ai.client.generativeai.internal.api.shared.HarmBlockThreshold -import com.google.ai.client.generativeai.internal.api.shared.HarmCategory -import com.google.ai.client.generativeai.internal.api.shared.Part -import com.google.ai.client.generativeai.internal.api.shared.SafetySetting -import com.google.ai.client.generativeai.internal.api.shared.TextPart +import com.google.ai.client.generativeai.common.CountTokensResponse +import com.google.ai.client.generativeai.common.GenerateContentResponse +import com.google.ai.client.generativeai.common.RequestOptions +import com.google.ai.client.generativeai.common.client.GenerationConfig +import com.google.ai.client.generativeai.common.server.BlockReason +import com.google.ai.client.generativeai.common.server.Candidate +import com.google.ai.client.generativeai.common.server.CitationSources +import com.google.ai.client.generativeai.common.server.FinishReason +import com.google.ai.client.generativeai.common.server.HarmProbability +import com.google.ai.client.generativeai.common.server.PromptFeedback +import com.google.ai.client.generativeai.common.server.SafetyRating +import com.google.ai.client.generativeai.common.shared.Blob +import com.google.ai.client.generativeai.common.shared.BlobPart +import com.google.ai.client.generativeai.common.shared.Content +import com.google.ai.client.generativeai.common.shared.HarmBlockThreshold +import com.google.ai.client.generativeai.common.shared.HarmCategory +import com.google.ai.client.generativeai.common.shared.Part +import com.google.ai.client.generativeai.common.shared.SafetySetting +import com.google.ai.client.generativeai.common.shared.TextPart import com.google.ai.client.generativeai.type.BlockThreshold import com.google.ai.client.generativeai.type.CitationMetadata import com.google.ai.client.generativeai.type.ImagePart @@ -46,6 +47,9 @@ import java.io.ByteArrayOutputStream private const val BASE_64_FLAGS = Base64.NO_WRAP +internal fun com.google.ai.client.generativeai.type.RequestOptions.toInternal() = + RequestOptions(timeout, apiVersion) + internal fun com.google.ai.client.generativeai.type.Content.toInternal() = Content(this.role, this.parts.map { it.toInternal() }) diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/ktor.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/ktor.kt deleted file mode 100644 index ae3434e3..00000000 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/ktor.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("DEPRECATION") // a replacement for our purposes has not been published yet - -package com.google.ai.client.generativeai.internal.util - -import io.ktor.utils.io.ByteChannel -import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.close -import io.ktor.utils.io.readUTF8Line -import io.ktor.utils.io.writeFully -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json - -/** - * Suspends and processes each line read from the [ByteReadChannel] until the channel is closed for - * read. - * - * This extension function facilitates processing the stream of lines in a manner that takes into - * account EOF/empty strings- and avoids calling [block] as such. - * - * Example usage: - * ``` - * val channel: ByteReadChannel = ByteReadChannel("Hello, World!") - * channel.onEachLine { - * println("Received line: $it") - * } - * ``` - * - * @param block A suspending function to process each line. - */ -internal suspend fun ByteReadChannel.onEachLine(block: suspend (String) -> Unit) { - while (!isClosedForRead) { - awaitContent() - val line = readUTF8Line()?.takeUnless { it.isEmpty() } ?: continue - block(line) - } -} - -/** - * Decodes a stream of JSON elements from the given [ByteReadChannel] into a [Flow] of objects of - * type [T]. - * - * This function takes in a stream of events, each with a set of named parts. Parts are separated by - * an HTTP \r\n newline, events are separated by a double HTTP \r\n\r\n newline. This function - * assumes every event will only contain a named "data" part with a JSON object. Each data JSON is - * decoded into an instance of [T] and emitted as it is read from the channel. - * - * Example usage: - * ``` - * val json = Json { ignoreUnknownKeys = true } // Create a Json instance with any configurations - * val channel: ByteReadChannel = ByteReadChannel("data: {\"name\":\"Alice\"}\r\n\r\ndata: {\"name\":\"Bob\"}]") - * - * json.decodeToFlow(channel).collect { person -> - * println(person.name) - * } - * ``` - * - * @param T The type of objects to decode from the JSON stream. - * @param channel The [ByteReadChannel] from which the JSON stream will be read. - * @return A [Flow] of objects of type [T]. - * @throws SerializationException in case of any decoding-specific error - * @throws IllegalArgumentException if the decoded input is not a valid instance of [T] - */ -internal inline fun Json.decodeToFlow(channel: ByteReadChannel): Flow = channelFlow { - channel.onEachLine { - val data = it.removePrefix("data:") - send(decodeFromString(data)) - } -} - -/** - * Writes the provided [bytes] to the channel and closes it. - * - * Just a wrapper around [writeFully] that closes the channel after writing is complete. - * - * @param bytes the data to send through the channel - */ -internal suspend fun ByteChannel.send(bytes: ByteArray) { - writeFully(bytes) - close() -} - -/** String separator used in SSE communication to signal the end of a message. */ -internal const val SSE_SEPARATOR = "\r\n\r\n" diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/serialization.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/serialization.kt deleted file mode 100644 index 76405fe3..00000000 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/util/serialization.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.internal.util - -import android.util.Log -import com.google.ai.client.generativeai.type.SerializationException -import kotlin.reflect.KClass -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder - -/** - * Serializer for enums that defaults to the first ordinal on unknown types. - * - * Convention is that the first enum be named `UNKNOWN`, but any name is valid. - * - * When an unknown enum value is found, the enum itself will be logged to stderr with a message - * about opening an issue on GitHub regarding the new enum value. - */ -internal class FirstOrdinalSerializer>(private val enumClass: KClass) : - KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("FirstOrdinalSerializer") - - override fun deserialize(decoder: Decoder): T { - val name = decoder.decodeString() - val values = enumClass.enumValues() - - return values.firstOrNull { it.serialName == name } - ?: values.first().also { printWarning(name) } - } - - private fun printWarning(name: String) { - Log.e( - "FirstOrdinalSerializer", - """ - |Unknown enum value found: $name" - |This usually means the backend was updated, and the SDK needs to be updated to match it. - |Check if there's a new version for the SDK, otherwise please open an issue on our - |GitHub to bring it to our attention: - |https://github.com/google/google-ai-android - """ - .trimMargin() - ) - } - - override fun serialize(encoder: Encoder, value: T) { - encoder.encodeString(value.serialName) - } -} - -/** - * Provides the name to be used in serialization for this enum value. - * - * By default an enum is serialized to its [name][Enum.name], and can be overwritten by providing a - * [SerialName] annotation. - */ -internal val > T.serialName: String - get() = declaringJavaClass.getField(name).getAnnotation()?.value ?: name - -/** - * Variant of [kotlin.enumValues] that provides support for [KClass] instances of enums. - * - * @throws SerializationException if the class is not a valid enum. Beyond runtime emily magic, this - * shouldn't really be possible. - */ -internal fun > KClass.enumValues(): Array = - java.enumConstants ?: throw SerializationException("$simpleName is not a valid enum type.") diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/type/Exceptions.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/type/Exceptions.kt index 5b451f01..5cfc197e 100644 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/type/Exceptions.kt +++ b/generativeai/src/main/java/com/google/ai/client/generativeai/type/Exceptions.kt @@ -17,12 +17,13 @@ package com.google.ai.client.generativeai.type import com.google.ai.client.generativeai.GenerativeModel -import io.ktor.serialization.JsonConvertException +import com.google.ai.client.generativeai.internal.util.toPublic import kotlinx.coroutines.TimeoutCancellationException /** Parent class for any errors that occur from [GenerativeModel]. */ sealed class GoogleGenerativeAIException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) { + companion object { /** @@ -34,12 +35,30 @@ sealed class GoogleGenerativeAIException(message: String, cause: Throwable? = nu fun from(cause: Throwable): GoogleGenerativeAIException = when (cause) { is GoogleGenerativeAIException -> cause - is JsonConvertException, - is kotlinx.serialization.SerializationException -> - SerializationException( - "Something went wrong while trying to deserialize a response from the server.", - cause - ) + is com.google.ai.client.generativeai.common.GoogleGenerativeAIException -> + when (cause) { + is com.google.ai.client.generativeai.common.SerializationException -> + SerializationException(cause.message ?: "", cause.cause) + is com.google.ai.client.generativeai.common.ServerException -> + ServerException(cause.message ?: "", cause.cause) + is com.google.ai.client.generativeai.common.InvalidAPIKeyException -> + InvalidAPIKeyException( + cause.message ?: "", + ) + is com.google.ai.client.generativeai.common.PromptBlockedException -> + PromptBlockedException(cause.response.toPublic(), cause.cause) + is com.google.ai.client.generativeai.common.UnsupportedUserLocationException -> + UnsupportedUserLocationException(cause.cause) + is com.google.ai.client.generativeai.common.InvalidStateException -> + InvalidStateException(cause.message ?: "", cause) + is com.google.ai.client.generativeai.common.ResponseStoppedException -> + ResponseStoppedException(cause.response.toPublic(), cause.cause) + is com.google.ai.client.generativeai.common.RequestTimeoutException -> + RequestTimeoutException(cause.message ?: "", cause.cause) + is com.google.ai.client.generativeai.common.UnknownException -> + UnknownException(cause.message ?: "", cause.cause) + else -> UnknownException(cause.message ?: "", cause) + } is TimeoutCancellationException -> RequestTimeoutException("The request failed to complete in the allotted time.") else -> UnknownException("Something unexpected happened.", cause) @@ -66,6 +85,7 @@ class InvalidAPIKeyException(message: String, cause: Throwable? = null) : * * @property response the full server response for the request. */ +// TODO(rlazo): Add secondary constructor to pass through the message? class PromptBlockedException(val response: GenerateContentResponse, cause: Throwable? = null) : GoogleGenerativeAIException( "Prompt was blocked: ${response.promptFeedback?.blockReason?.name}", @@ -79,6 +99,7 @@ class PromptBlockedException(val response: GenerateContentResponse, cause: Throw * [list of regions](https://ai.google.dev/available_regions#available_regions) (countries and * territories) where the API is available. */ +// TODO(rlazo): Add secondary constructor to pass through the message? class UnsupportedUserLocationException(cause: Throwable? = null) : GoogleGenerativeAIException("User location is not supported for the API use.", cause) diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/type/RequestOptions.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/type/RequestOptions.kt index cc9669d9..e25561fb 100644 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/type/RequestOptions.kt +++ b/generativeai/src/main/java/com/google/ai/client/generativeai/type/RequestOptions.kt @@ -16,7 +16,6 @@ package com.google.ai.client.generativeai.type -import io.ktor.client.plugins.HttpTimeout import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -31,10 +30,7 @@ import kotlin.time.toDuration class RequestOptions(val timeout: Duration, val apiVersion: String = "v1") { @JvmOverloads constructor( - timeout: Long? = HttpTimeout.INFINITE_TIMEOUT_MS, + timeout: Long? = Long.MAX_VALUE, apiVersion: String = "v1" - ) : this( - (timeout ?: HttpTimeout.INFINITE_TIMEOUT_MS).toDuration(DurationUnit.MILLISECONDS), - apiVersion - ) + ) : this((timeout ?: Long.MAX_VALUE).toDuration(DurationUnit.MILLISECONDS), apiVersion) } diff --git a/generativeai/src/test/java/com/google/ai/client/generativeai/GenerativeModelTests.kt b/generativeai/src/test/java/com/google/ai/client/generativeai/GenerativeModelTests.kt index 15145389..bfc0b24c 100644 --- a/generativeai/src/test/java/com/google/ai/client/generativeai/GenerativeModelTests.kt +++ b/generativeai/src/test/java/com/google/ai/client/generativeai/GenerativeModelTests.kt @@ -16,91 +16,119 @@ package com.google.ai.client.generativeai -import com.google.ai.client.generativeai.type.RequestOptions -import com.google.ai.client.generativeai.type.RequestTimeoutException -import com.google.ai.client.generativeai.util.commonTest -import com.google.ai.client.generativeai.util.createGenerativeModel -import com.google.ai.client.generativeai.util.createResponses -import com.google.ai.client.generativeai.util.doBlocking -import com.google.ai.client.generativeai.util.prepareStreamingResponse +import com.google.ai.client.generativeai.common.APIController +import com.google.ai.client.generativeai.common.GenerateContentRequest as GenerateContentRequest_Common +import com.google.ai.client.generativeai.common.GenerateContentResponse as GenerateContentResponse_Common +import com.google.ai.client.generativeai.common.InvalidAPIKeyException as InvalidAPIKeyException_Common +import com.google.ai.client.generativeai.common.UnsupportedUserLocationException as UnsupportedUserLocationException_Common +import com.google.ai.client.generativeai.common.server.Candidate as Candidate_Common +import com.google.ai.client.generativeai.common.shared.Content as Content_Common +import com.google.ai.client.generativeai.common.shared.TextPart as TextPart_Common +import com.google.ai.client.generativeai.type.Candidate +import com.google.ai.client.generativeai.type.Content +import com.google.ai.client.generativeai.type.GenerateContentResponse +import com.google.ai.client.generativeai.type.InvalidAPIKeyException +import com.google.ai.client.generativeai.type.PromptFeedback +import com.google.ai.client.generativeai.type.TextPart +import com.google.ai.client.generativeai.type.UnsupportedUserLocationException import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headersOf -import io.ktor.utils.io.ByteChannel -import io.ktor.utils.io.close -import io.ktor.utils.io.writeFully -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.withTimeout +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.equality.shouldBeEqualToUsingFields +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized internal class GenerativeModelTests { - private val testTimeout = 5.seconds + + private val apiKey: String = "api_key" + private val mockApiController = mockk() @Test - fun `(generateContentStream) emits responses as they come in`() = commonTest { - val response = createResponses("The", " world", " is", " a", " beautiful", " place!") - val bytes = prepareStreamingResponse(response) + fun `generateContent request succeeds`() = doBlocking { + val model = GenerativeModel("gemini-pro-1.0", apiKey, controller = mockApiController) + coEvery { + mockApiController.generateContent( + GenerateContentRequest_Common( + "gemini-pro-1.0", + contents = listOf(Content_Common(parts = listOf(TextPart_Common("Why's the sky blue?")))) + ) + ) + } returns + GenerateContentResponse_Common( + listOf( + Candidate_Common( + content = + Content_Common( + parts = listOf(TextPart_Common("I'm still learning how to answer this question")) + ), + finishReason = null, + safetyRatings = listOf(), + citationMetadata = null + ) + ) + ) + + val expectedResponse = + GenerateContentResponse( + listOf( + Candidate( + Content(parts = listOf(TextPart("I'm still learning how to answer this question"))), + safetyRatings = listOf(), + citationMetadata = listOf(), + finishReason = null + ) + ), + PromptFeedback(null, listOf()) + ) - bytes.forEach { channel.writeFully(it) } - val responses = model.generateContentStream() + val response = model.generateContent("Why's the sky blue?") - withTimeout(testTimeout) { - responses.collect { - it.candidates.isEmpty() shouldBe false - channel.close() - } - } + response.shouldBeEqualToUsingFields(expectedResponse, GenerateContentResponse::text) + response.candidates shouldHaveSize expectedResponse.candidates.size + response.candidates[0].shouldBeEqualToUsingFields( + expectedResponse.candidates[0], + Candidate::finishReason, + Candidate::citationMetadata, + Candidate::safetyRatings + ) } @Test - fun `(generateContent) respects a custom timeout`() = - commonTest(requestOptions = RequestOptions(2.seconds)) { - shouldThrow { - withTimeout(testTimeout) { model.generateContent("d") } - } - } -} + fun `generateContent throws exception`() = doBlocking { + val model = GenerativeModel("gemini-pro-1.0", apiKey, controller = mockApiController) + coEvery { + mockApiController.generateContent( + GenerateContentRequest_Common( + "gemini-pro-1.0", + contents = listOf(Content_Common(parts = listOf(TextPart_Common("Why's the sky blue?")))) + ) + ) + } throws InvalidAPIKeyException_Common("exception message") -@RunWith(Parameterized::class) -internal class ModelNamingTests(private val modelName: String, private val actualName: String) { + shouldThrow { model.generateContent("Why's the sky blue?") } + } @Test - fun `request should include right model name`() = doBlocking { - val channel = ByteChannel(autoFlush = true) - val mockEngine = MockEngine { - respond(channel, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) - } - prepareStreamingResponse(createResponses("Random")).forEach { channel.writeFully(it) } - val model = - createGenerativeModel(modelName, "super_cool_test_key", RequestOptions(), mockEngine) + fun `generateContentStream throws exception`() = doBlocking { + val model = GenerativeModel("gemini-pro-1.0", apiKey, controller = mockApiController) + coEvery { + mockApiController.generateContentStream( + GenerateContentRequest_Common( + "gemini-pro-1.0", + contents = listOf(Content_Common(parts = listOf(TextPart_Common("Why's the sky blue?")))) + ) + ) + } returns flow { throw UnsupportedUserLocationException_Common() } - withTimeout(5.seconds) { - model.generateContentStream().collect { - it.candidates.isEmpty() shouldBe false - channel.close() - } + shouldThrow { + model.generateContentStream("Why's the sky blue?").collect {} } - - mockEngine.requestHistory.first().url.encodedPath shouldContain actualName } +} - companion object { - @JvmStatic - @Parameterized.Parameters - fun data() = - listOf( - arrayOf("gemini-pro", "models/gemini-pro"), - arrayOf("x/gemini-pro", "x/gemini-pro"), - arrayOf("models/gemini-pro", "models/gemini-pro"), - arrayOf("/modelname", "/modelname"), - arrayOf("modifiedNaming/mymodel", "modifiedNaming/mymodel"), - ) - } +internal fun doBlocking(block: suspend CoroutineScope.() -> Unit) { + runBlocking(block = block) } diff --git a/generativeai/src/test/java/com/google/ai/client/generativeai/StreamingSnapshotTests.kt b/generativeai/src/test/java/com/google/ai/client/generativeai/StreamingSnapshotTests.kt deleted file mode 100644 index 4615bcdc..00000000 --- a/generativeai/src/test/java/com/google/ai/client/generativeai/StreamingSnapshotTests.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai - -import com.google.ai.client.generativeai.type.BlockReason -import com.google.ai.client.generativeai.type.FinishReason -import com.google.ai.client.generativeai.type.HarmCategory -import com.google.ai.client.generativeai.type.InvalidAPIKeyException -import com.google.ai.client.generativeai.type.PromptBlockedException -import com.google.ai.client.generativeai.type.ResponseStoppedException -import com.google.ai.client.generativeai.type.SerializationException -import com.google.ai.client.generativeai.type.ServerException -import com.google.ai.client.generativeai.util.goldenStreamingFile -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.shouldBe -import io.ktor.http.HttpStatusCode -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.withTimeout -import org.junit.Test - -internal class StreamingSnapshotTests { - private val testTimeout = 5.seconds - - @Test - fun `short reply`() = - goldenStreamingFile("success-basic-reply-short.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.isEmpty() shouldBe false - responseList.first().candidates.first().finishReason shouldBe FinishReason.STOP - responseList.first().candidates.first().content.parts.isEmpty() shouldBe false - responseList.first().candidates.first().safetyRatings.isEmpty() shouldBe false - } - } - - @Test - fun `long reply`() = - goldenStreamingFile("success-basic-reply-long.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.isEmpty() shouldBe false - responseList.forEach { - it.candidates.first().finishReason shouldBe FinishReason.STOP - it.candidates.first().content.parts.isEmpty() shouldBe false - it.candidates.first().safetyRatings.isEmpty() shouldBe false - } - } - } - - @Test - fun `unknown enum`() = - goldenStreamingFile("success-unknown-enum.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - responses.first { - it.candidates.any { it.safetyRatings.any { it.category == HarmCategory.UNKNOWN } } - } - } - } - - @Test - fun `quotes escaped`() = - goldenStreamingFile("success-quotes-escaped.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - val responseList = responses.toList() - - responseList.isEmpty() shouldBe false - responseList.first().text!!.contains("\"") - } - } - - @Test - fun `prompt blocked for safety`() = - goldenStreamingFile("failure-prompt-blocked-safety.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - val exception = shouldThrow { responses.collect() } - exception.response.promptFeedback?.blockReason shouldBe BlockReason.SAFETY - } - } - - @Test - fun `empty content`() = - goldenStreamingFile("failure-empty-content.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } - - @Test - fun `http errors`() = - goldenStreamingFile("failure-http-error.txt", HttpStatusCode.PreconditionFailed) { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } - - @Test - fun `stopped for safety`() = - goldenStreamingFile("failure-finish-reason-safety.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - val exception = shouldThrow { responses.collect() } - exception.response.candidates.first().finishReason shouldBe FinishReason.SAFETY - } - } - - @Test - fun `citation parsed correctly`() = - goldenStreamingFile("success-citations.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.any { it.candidates.any { it.citationMetadata.isNotEmpty() } } shouldBe true - } - } - - @Test - fun `citation returns correctly when using alternative name`() = - goldenStreamingFile("success-citations-altname.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.any { it.candidates.any { it.citationMetadata.isNotEmpty() } } shouldBe true - } - } - - @Test - fun `stopped for recitation`() = - goldenStreamingFile("failure-recitation-no-content.txt") { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { - val exception = shouldThrow { responses.collect() } - exception.response.candidates.first().finishReason shouldBe FinishReason.RECITATION - } - } - - @Test - fun `image rejected`() = - goldenStreamingFile("failure-image-rejected.txt", HttpStatusCode.BadRequest) { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } - - @Test - fun `unknown model`() = - goldenStreamingFile("failure-unknown-model.txt", HttpStatusCode.NotFound) { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } - - @Test - fun `invalid api key`() = - goldenStreamingFile("failure-api-key.txt", HttpStatusCode.BadRequest) { - val responses = model.generateContentStream() - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } -} diff --git a/generativeai/src/test/java/com/google/ai/client/generativeai/UnarySnapshotTests.kt b/generativeai/src/test/java/com/google/ai/client/generativeai/UnarySnapshotTests.kt deleted file mode 100644 index 3c15a9e5..00000000 --- a/generativeai/src/test/java/com/google/ai/client/generativeai/UnarySnapshotTests.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai - -import com.google.ai.client.generativeai.type.BlockReason -import com.google.ai.client.generativeai.type.FinishReason -import com.google.ai.client.generativeai.type.HarmCategory -import com.google.ai.client.generativeai.type.InvalidAPIKeyException -import com.google.ai.client.generativeai.type.PromptBlockedException -import com.google.ai.client.generativeai.type.ResponseStoppedException -import com.google.ai.client.generativeai.type.SerializationException -import com.google.ai.client.generativeai.type.ServerException -import com.google.ai.client.generativeai.type.UnsupportedUserLocationException -import com.google.ai.client.generativeai.util.goldenUnaryFile -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.should -import io.kotest.matchers.shouldBe -import io.ktor.http.HttpStatusCode -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.withTimeout -import org.junit.Test - -internal class UnarySnapshotTests { - private val testTimeout = 5.seconds - - @Test - fun `short reply`() = - goldenUnaryFile("success-basic-reply-short.json") { - withTimeout(testTimeout) { - val response = model.generateContent() - - response.candidates.isEmpty() shouldBe false - response.candidates.first().finishReason shouldBe FinishReason.STOP - response.candidates.first().content.parts.isEmpty() shouldBe false - response.candidates.first().safetyRatings.isEmpty() shouldBe false - } - } - - @Test - fun `long reply`() = - goldenUnaryFile("success-basic-reply-long.json") { - withTimeout(testTimeout) { - val response = model.generateContent() - - response.candidates.isEmpty() shouldBe false - response.candidates.first().finishReason shouldBe FinishReason.STOP - response.candidates.first().content.parts.isEmpty() shouldBe false - response.candidates.first().safetyRatings.isEmpty() shouldBe false - } - } - - @Test - fun `unknown enum`() = - goldenUnaryFile("success-unknown-enum.json") { - withTimeout(testTimeout) { - val response = model.generateContent() - - response.candidates.first { it.safetyRatings.any { it.category == HarmCategory.UNKNOWN } } - } - } - - @Test - fun `prompt blocked for safety`() = - goldenUnaryFile("failure-prompt-blocked-safety.json") { - withTimeout(testTimeout) { - shouldThrow { model.generateContent() } should - { - it.response.promptFeedback?.blockReason shouldBe BlockReason.SAFETY - } - } - } - - @Test - fun `empty content`() = - goldenUnaryFile("failure-empty-content.json") { - withTimeout(testTimeout) { shouldThrow { model.generateContent() } } - } - - @Test - fun `http error`() = - goldenUnaryFile("failure-http-error.json", HttpStatusCode.PreconditionFailed) { - withTimeout(testTimeout) { shouldThrow { model.generateContent() } } - } - - @Test - fun `user location error`() = - goldenUnaryFile("failure-unsupported-user-location.json", HttpStatusCode.PreconditionFailed) { - withTimeout(testTimeout) { - shouldThrow { model.generateContent() } - } - } - - @Test - fun `stopped for safety`() = - goldenUnaryFile("failure-finish-reason-safety.json") { - withTimeout(testTimeout) { - val exception = shouldThrow { model.generateContent() } - exception.response.candidates.first().finishReason shouldBe FinishReason.SAFETY - } - } - - @Test - fun `citation returns correctly`() = - goldenUnaryFile("success-citations.json") { - withTimeout(testTimeout) { - val response = model.generateContent() - - response.candidates.isEmpty() shouldBe false - response.candidates.first().citationMetadata.isNotEmpty() shouldBe true - } - } - - @Test - fun `citation returns correctly when using alternative name`() = - goldenUnaryFile("success-citations-altname.json") { - withTimeout(testTimeout) { - val response = model.generateContent() - - response.candidates.isEmpty() shouldBe false - response.candidates.first().citationMetadata.isNotEmpty() shouldBe true - } - } - - @Test - fun `invalid response`() = - goldenUnaryFile("failure-invalid-response.json") { - withTimeout(testTimeout) { shouldThrow { model.generateContent() } } - } - - @Test - fun `malformed content`() = - goldenUnaryFile("failure-malformed-content.json") { - withTimeout(testTimeout) { shouldThrow { model.generateContent() } } - } - - @Test - fun `invalid api key`() = - goldenUnaryFile("failure-api-key.json", HttpStatusCode.BadRequest) { - withTimeout(testTimeout) { shouldThrow { model.generateContent() } } - } - - @Test - fun `image rejected`() = - goldenUnaryFile("failure-image-rejected.json", HttpStatusCode.BadRequest) { - withTimeout(testTimeout) { shouldThrow { model.generateContent() } } - } - - @Test - fun `unknown model`() = - goldenUnaryFile("failure-unknown-model.json", HttpStatusCode.NotFound) { - withTimeout(testTimeout) { shouldThrow { model.generateContent() } } - } -} diff --git a/generativeai/src/test/java/com/google/ai/client/generativeai/internal/util/ConversionsTest.kt b/generativeai/src/test/java/com/google/ai/client/generativeai/internal/util/ConversionsTest.kt deleted file mode 100644 index 31255fbe..00000000 --- a/generativeai/src/test/java/com/google/ai/client/generativeai/internal/util/ConversionsTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.internal.util - -import com.google.ai.client.generativeai.internal.api.shared.Content -import com.google.ai.client.generativeai.internal.api.shared.TextPart -import com.google.ai.client.generativeai.type.content -import io.kotest.matchers.shouldBe -import org.junit.Test - -class ConversionsTest { - - @Test - fun `test content conversion toInternal (role not mentioned)`() { - val content = content { text("test") }.toInternal() - content.run { - // default role should be a "user" - role shouldBe "user" - - // only one part should be present - parts.size shouldBe 1 - parts[0].run { (this as TextPart).text shouldBe "test" } - } - } - - @Test - fun `test content conversion toInternal (role mentioned)`() { - val content = content(role = "model") { text("test") }.toInternal() - content.run { - // Role should be a "model" - role shouldBe "model" - - // only one part should be present - parts.size shouldBe 1 - parts[0].run { (this as TextPart).text shouldBe "test" } - } - } - - @Test - fun `test content conversion toPublic (role not mentioned)`() { - val content = Content(parts = listOf(TextPart("test"))).toPublic() - content.role shouldBe "user" - } - - @Test - fun `test content conversion toPublic (role mentioned)`() { - val content = Content(role = "model", parts = listOf(TextPart("test"))).toPublic() - content.role shouldBe "model" - } -} diff --git a/generativeai/src/test/java/com/google/ai/client/generativeai/util/kotlin.kt b/generativeai/src/test/java/com/google/ai/client/generativeai/util/kotlin.kt deleted file mode 100644 index 259f72eb..00000000 --- a/generativeai/src/test/java/com/google/ai/client/generativeai/util/kotlin.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.ai.client.generativeai.util - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.runBlocking - -/** - * Runs the given [block] using [runBlocking] on the current thread for side effect. - * - * Using this function is like [runBlocking] with default context (which runs the given block on the - * calling thread) but forces the return type to be `Unit`, which is helpful when implementing - * suspending tests as expression functions: - * ``` - * @Test - * fun myTest() = doBlocking {...} - * ``` - */ -internal fun doBlocking(block: suspend CoroutineScope.() -> Unit) { - runBlocking(block = block) -} diff --git a/generativeai/src/test/java/com/google/ai/client/generativeai/util/tests.kt b/generativeai/src/test/java/com/google/ai/client/generativeai/util/tests.kt deleted file mode 100644 index 90707336..00000000 --- a/generativeai/src/test/java/com/google/ai/client/generativeai/util/tests.kt +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("DEPRECATION") // a replacement for our purposes has not been published yet - -package com.google.ai.client.generativeai.util - -import com.google.ai.client.generativeai.GenerativeModel -import com.google.ai.client.generativeai.internal.api.APIController -import com.google.ai.client.generativeai.internal.api.GenerateContentRequest -import com.google.ai.client.generativeai.internal.api.GenerateContentResponse -import com.google.ai.client.generativeai.internal.api.JSON -import com.google.ai.client.generativeai.internal.api.server.Candidate -import com.google.ai.client.generativeai.internal.api.shared.Content -import com.google.ai.client.generativeai.internal.api.shared.TextPart -import com.google.ai.client.generativeai.internal.util.SSE_SEPARATOR -import com.google.ai.client.generativeai.internal.util.send -import com.google.ai.client.generativeai.type.RequestOptions -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headersOf -import io.ktor.utils.io.ByteChannel -import io.ktor.utils.io.close -import io.ktor.utils.io.writeFully -import java.io.File -import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString - -internal fun prepareStreamingResponse(response: List): List = - response.map { "data: ${JSON.encodeToString(it)}$SSE_SEPARATOR".toByteArray() } - -internal fun prepareResponse(response: GenerateContentResponse) = - JSON.encodeToString(response).toByteArray() - -internal fun createRequest(vararg text: String): GenerateContentRequest { - val contents = text.map { Content(parts = listOf(TextPart(it))) } - - return GenerateContentRequest("gemini", contents) -} - -internal fun createResponse(text: String) = createResponses(text).single() - -internal fun createResponses(vararg text: String): List { - val candidates = text.map { Candidate(Content(parts = listOf(TextPart(it)))) } - - return candidates.map { GenerateContentResponse(candidates = listOf(it)) } -} - -/** - * Wrapper around common instances needed in tests. - * - * @param channel A [ByteChannel] for sending responses through the mock HTTP engine - * @param model A [GenerativeModel] that consumes the [channel] - * @see commonTest - * @see send - */ -internal data class CommonTestScope(val channel: ByteChannel, val model: GenerativeModel) - -/** A test that runs under a [CommonTestScope]. */ -internal typealias CommonTest = suspend CommonTestScope.() -> Unit - -/** - * Common test block for providing a [CommonTestScope] during tests. - * - * Example usage: - * ``` - * @Test - * fun `(generateContent) generates a proper response`() = commonTest { - * val request = createRequest("say something nice") - * val response = createResponse("The world is a beautiful place!") - * - * channel.send(prepareResponse(response)) - * - * withTimeout(testTimeout) { - * val data = controller.generateContent(request) - * data.candidates.shouldNotBeEmpty() - * } - * } - * ``` - * - * @param status An optional [HttpStatusCode] to return as a response - * @param requestOptions Optional [RequestOptions] to utilize in the underlying controller - * @param block The test contents themselves, with the [CommonTestScope] implicitly provided - * @see CommonTestScope - */ -internal fun commonTest( - status: HttpStatusCode = HttpStatusCode.OK, - requestOptions: RequestOptions = RequestOptions(), - block: CommonTest -) = doBlocking { - val channel = ByteChannel(autoFlush = true) - val mockEngine = MockEngine { - respond(channel, status, headersOf(HttpHeaders.ContentType, "application/json")) - } - val model = createGenerativeModel("gemini-pro", "super_cool_test_key", requestOptions, mockEngine) - CommonTestScope(channel, model).block() -} - -/** Simple wrapper that guarantees the model and APIController are created using the same data */ -internal fun createGenerativeModel( - name: String, - apikey: String, - requestOptions: RequestOptions = RequestOptions(), - engine: MockEngine -) = - GenerativeModel( - name, - apikey, - controller = APIController("super_cool_test_key", name, requestOptions, engine) - ) - -/** - * A variant of [commonTest] for performing *streaming-based* snapshot tests. - * - * Loads the *Golden File* and automatically parses the messages from it; providing it to the - * channel. - * - * @param name The name of the *Golden File* to load - * @param httpStatusCode An optional [HttpStatusCode] to return as a response - * @param block The test contents themselves, with a [CommonTestScope] implicitly provided - * @see goldenUnaryFile - */ -internal fun goldenStreamingFile( - name: String, - httpStatusCode: HttpStatusCode = HttpStatusCode.OK, - block: CommonTest -) = doBlocking { - val goldenFile = loadGoldenFile("streaming/$name") - val messages = goldenFile.readLines().filter { it.isNotBlank() } - - commonTest(httpStatusCode) { - launch { - for (message in messages) { - channel.writeFully("$message$SSE_SEPARATOR".toByteArray()) - } - channel.close() - } - - block() - } -} - -/** - * A variant of [commonTest] for performing snapshot tests. - * - * Loads the *Golden File* and automatically provides it to the channel. - * - * @param name The name of the *Golden File* to load - * @param httpStatusCode An optional [HttpStatusCode] to return as a response - * @param block The test contents themselves, with a [CommonTestScope] implicitly provided - * @see goldenStreamingFile - */ -internal fun goldenUnaryFile( - name: String, - httpStatusCode: HttpStatusCode = HttpStatusCode.OK, - block: CommonTest -) = - commonTest(httpStatusCode) { - val goldenFile = loadGoldenFile("unary/$name") - val message = goldenFile.readText() - - channel.send(message.toByteArray()) - - block() - } - -/** - * Loads a *Golden File* from the resource directory. - * - * Expects golden files to live under `golden-files` in the resource files. - * - * @see goldenUnaryFile - */ -internal fun loadGoldenFile(path: String): File = loadResourceFile("golden-files/$path") - -/** Loads a file from the test resources directory. */ -internal fun loadResourceFile(path: String) = File("src/test/resources/$path") diff --git a/generativeai/src/test/resources/golden-files/streaming/failure-api-key.txt b/generativeai/src/test/resources/golden-files/streaming/failure-api-key.txt deleted file mode 100644 index ecf6f6b5..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/failure-api-key.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "error": { - "code": 400, - "message": "API key not valid. Please pass a valid API key.", - "status": "INVALID_ARGUMENT", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "API_KEY_INVALID", - "domain": "googleapis.com", - "metadata": { - "service": "generativelanguage.googleapis.com" - } - }, - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "Invalid API key: AIzv00G7VmUCUeC-5OglO3hcXM" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/streaming/failure-empty-content.txt b/generativeai/src/test/resources/golden-files/streaming/failure-empty-content.txt deleted file mode 100644 index 5762b515..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/failure-empty-content.txt +++ /dev/null @@ -1 +0,0 @@ -data: {"candidates": [{"content": {},"index": 0}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} diff --git a/generativeai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt b/generativeai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt deleted file mode 100644 index 05e09361..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt +++ /dev/null @@ -1,2 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": ""}],"role": "model"},"finishReason": "SAFETY","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "HIGH"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - diff --git a/generativeai/src/test/resources/golden-files/streaming/failure-http-error.txt b/generativeai/src/test/resources/golden-files/streaming/failure-http-error.txt deleted file mode 100644 index 8c75fd7b..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/failure-http-error.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "$grpcMessage", - "status": "FAILED_PRECONDITION", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::failed_precondition: User location is not supported for the API use. [google.rpc.error_details_ext] { message: \"User location is not supported for the API use.\" }" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/streaming/failure-image-rejected.txt b/generativeai/src/test/resources/golden-files/streaming/failure-image-rejected.txt deleted file mode 100644 index 8567086e..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/failure-image-rejected.txt +++ /dev/null @@ -1,7 +0,0 @@ -{ - "error": { - "code": 400, - "message": "Request contains an invalid argument.", - "status": "INVALID_ARGUMENT" - } -} diff --git a/generativeai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt b/generativeai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt deleted file mode 100644 index 58c914af..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt +++ /dev/null @@ -1,2 +0,0 @@ -data: {"promptFeedback": {"blockReason": "SAFETY","safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "HIGH"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - diff --git a/generativeai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt b/generativeai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt deleted file mode 100644 index 6d69b64e..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt +++ /dev/null @@ -1,6 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "PLACEHOLDER"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": "PLACEHOLDER"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "LOW"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 30,"endIndex": 179,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"finishReason": "RECITATION","index": 0}]} - diff --git a/generativeai/src/test/resources/golden-files/streaming/failure-unknown-model.txt b/generativeai/src/test/resources/golden-files/streaming/failure-unknown-model.txt deleted file mode 100644 index 60b3f55c..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/failure-unknown-model.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 404, - "message": "models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.", - "status": "NOT_FOUND", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::not_found: models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods. [google.rpc.error_details_ext] { message: \"models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.\" }" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt b/generativeai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt deleted file mode 100644 index 268f75d7..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt +++ /dev/null @@ -1,12 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "**Cats:**\n\n1. **Anatomy and Appearance:**\n - Cats have"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": " flexible bodies with a long tail, sharp retractable claws, and soft fur.\n - Their eyes are adapted for low-light conditions and have a vertical slit"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "-like pupil.\n - Cats come in a wide variety of breeds, each with distinct physical characteristics.\n\n2. **Behavior and Personality:**\n - Cats are known for their independence and solitary nature.\n - They are often described as aloof and mysterious, but they can also be affectionate and playful."}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "\n - Cats are territorial and communicate through body language, vocalizations, and scent marking.\n\n3. **Diet and Nutrition:**\n - Cats are obligate carnivores, meaning they require animal-based protein for survival.\n - Their diet should consist primarily of high-quality cat food that meets their nutritional needs.\n - Cats are prone to obesity, so portion control and regular exercise are important.\n\n4. **Health and Care:**\n - Cats require regular veterinary checkups, vaccinations, and parasite control.\n - They should be brushed regularly to prevent matting and shedding.\n - Providing"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": " a clean litter box and fresh water is essential for their well-being.\n\n5. **Lifespan:**\n - The average lifespan of a cat is 12-15 years, although some cats can live longer with proper care.\n\n**Dogs:**\n\n1. **Anatomy and Appearance:**\n - Dogs have a diverse range of sizes, shapes, and coat types depending on their breed.\n - They have strong jaws with sharp teeth adapted for chewing and tearing.\n - Dogs' ears are typically floppy or erect and can be used to express emotions.\n\n2. **Behavior and Personality:**\n - Dogs are known for their loyalty, companionship, and trainability.\n - They are social animals that thrive on human interaction and form strong bonds with their owners.\n - Dogs communicate through barking, whining, growling, and body language.\n\n3. **Diet and Nutrition:**\n - Dogs are omnivores and can eat a variety of foods, including meat, grains, fruits, and vegetables.\n - Their diet should be balanced and meet their nutritional requirements based on age, size, and activity level.\n - Obesity is a common problem in dogs, so portion control and exercise are important.\n\n4"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": ". **Health and Care:**\n - Dogs require regular veterinary checkups, vaccinations, and parasite control.\n - They should be brushed regularly to maintain a healthy coat and prevent shedding.\n - Providing adequate exercise, mental stimulation, and socialization is essential for their well-being.\n\n5. **Lifespan:**\n - The average lifespan of a dog varies depending on breed, size, and overall health.\n - Smaller breeds tend to live longer than larger breeds, with an average lifespan of 10-15 years."}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - diff --git a/generativeai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt b/generativeai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt deleted file mode 100644 index b3c07628..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt +++ /dev/null @@ -1,2 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "Cheyenne"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - diff --git a/generativeai/src/test/resources/golden-files/streaming/success-citations-altname.txt b/generativeai/src/test/resources/golden-files/streaming/success-citations-altname.txt deleted file mode 100644 index 4c682dc8..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/success-citations-altname.txt +++ /dev/null @@ -1,12 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - diff --git a/generativeai/src/test/resources/golden-files/streaming/success-citations.txt b/generativeai/src/test/resources/golden-files/streaming/success-citations.txt deleted file mode 100644 index 3bb76e3d..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/success-citations.txt +++ /dev/null @@ -1,12 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - diff --git a/generativeai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt b/generativeai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt deleted file mode 100644 index 0c48e4c4..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt +++ /dev/null @@ -1,7 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": " Pineapples and bananas are two different types of fruit. Pineapples grow on a"}]},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": " tropical plant with a rosette of long, pointed leaves. Bananas grow on a herbaceous"}]},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": " plant with large, broad leaves. The two plants are not related, and pin"}]},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "eapples do not grow on banana plants."}]},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}]} diff --git a/generativeai/src/test/resources/golden-files/streaming/success-unknown-enum.txt b/generativeai/src/test/resources/golden-files/streaming/success-unknown-enum.txt deleted file mode 100644 index 0f3da8eb..00000000 --- a/generativeai/src/test/resources/golden-files/streaming/success-unknown-enum.txt +++ /dev/null @@ -1,11 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "**Cats:**\n\n- **Physical Characteristics:**\n - Size: Cats come"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": " in a wide range of sizes, from small breeds like the Singapura to large breeds like the Maine Coon.\n - Fur: Cats have soft, furry coats"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": " that can vary in length and texture depending on the breed.\n - Eyes: Cats have large, expressive eyes that can be various colors, including green, blue, yellow, and hazel.\n - Ears: Cats have pointed, erect ears that are sensitive to sound.\n - Tail: Cats have long"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": ", flexible tails that they use for balance and communication.\n\n- **Behavior and Personality:**\n - Independent: Cats are often described as independent animals that enjoy spending time alone.\n - Affectionate: Despite their independent nature, cats can be very affectionate and form strong bonds with their owners.\n - Playful: Cats are naturally playful and enjoy engaging in activities such as chasing toys, climbing, and pouncing.\n - Curious: Cats are curious creatures that love to explore their surroundings.\n - Vocal: Cats communicate through a variety of vocalizations, including meows, purrs, hisses, and grow"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "ls.\n\n- **Health and Care:**\n - Diet: Cats are obligate carnivores, meaning they require animal-based protein for optimal health.\n - Grooming: Cats spend a significant amount of time grooming themselves to keep their fur clean and free of mats.\n - Exercise: Cats need regular exercise to stay healthy and active. This can be achieved through play sessions or access to outdoor space.\n - Veterinary Care: Regular veterinary checkups are essential for maintaining a cat's health and detecting any potential health issues early on.\n\n**Dogs:**\n\n- **Physical Characteristics:**\n - Size: Dogs come in a wide range of sizes, from small breeds like the Chihuahua to giant breeds like the Great Dane.\n - Fur: Dogs have fur coats that can vary in length, texture, and color depending on the breed.\n - Eyes: Dogs have expressive eyes that can be various colors, including brown, blue, green, and hazel.\n - Ears: Dogs have floppy or erect ears that are sensitive to sound.\n - Tail: Dogs have long, wagging tails that they use for communication and expressing emotions.\n\n- **Behavior and Personality:**\n - Loyal: Dogs are known for their loyalty and"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": " devotion to their owners.\n - Friendly: Dogs are generally friendly and outgoing animals that enjoy interacting with people and other animals.\n - Playful: Dogs are playful and energetic creatures that love to engage in activities such as fetching, running, and playing with toys.\n - Trainable: Dogs are highly trainable and can learn a variety of commands and tricks.\n - Vocal: Dogs communicate through a variety of vocalizations, including barking, howling, whining, and growling.\n\n- **Health and Care:**\n - Diet: Dogs are omnivores and can eat a variety of foods, including meat, vegetables, and grains.\n - Grooming: Dogs require regular grooming to keep their fur clean and free of mats. The frequency of grooming depends on the breed and coat type.\n - Exercise: Dogs need regular exercise to stay healthy and active. The amount of exercise required varies depending on the breed and age of the dog.\n - Veterinary Care: Regular veterinary checkups are essential for maintaining a dog's health and detecting any potential health issues early on."}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT_NEW_ENUM","probability": "NEGLIGIBLE_UNKNOWN_ENUM"}]}]} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-api-key.json b/generativeai/src/test/resources/golden-files/unary/failure-api-key.json deleted file mode 100644 index ecf6f6b5..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-api-key.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "error": { - "code": 400, - "message": "API key not valid. Please pass a valid API key.", - "status": "INVALID_ARGUMENT", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "API_KEY_INVALID", - "domain": "googleapis.com", - "metadata": { - "service": "generativelanguage.googleapis.com" - } - }, - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "Invalid API key: AIzv00G7VmUCUeC-5OglO3hcXM" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-empty-content.json b/generativeai/src/test/resources/golden-files/unary/failure-empty-content.json deleted file mode 100644 index 4e188966..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-empty-content.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "candidates": [ - { - "content": {}, - "index": 0 - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json b/generativeai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json deleted file mode 100644 index 111e33de..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "" - } - ], - "role": "model" - }, - "finishReason": "SAFETY", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "HIGH" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-http-error.json b/generativeai/src/test/resources/golden-files/unary/failure-http-error.json deleted file mode 100644 index c8b07a5b..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-http-error.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "$grpcMessage", - "status": "FAILED_PRECONDITION", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::failed_precondition: User location is not supported for the API use. [google.rpc.error_details_ext] { message: \"User location is not supported for the API use.\" }" - } - ] -} -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-image-rejected.json b/generativeai/src/test/resources/golden-files/unary/failure-image-rejected.json deleted file mode 100644 index 9dacdc71..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-image-rejected.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "Request contains an invalid argument.", - "status": "INVALID_ARGUMENT", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-invalid-response.json b/generativeai/src/test/resources/golden-files/unary/failure-invalid-response.json deleted file mode 100644 index 49d05e18..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-invalid-response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "this": [ - { - "is": { - "not": [ - { - "a": "valid" - } - ] - }, - "response": {} - } - ] -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-malformed-content.json b/generativeai/src/test/resources/golden-files/unary/failure-malformed-content.json deleted file mode 100644 index 737f2e08..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-malformed-content.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "candidates": [ - { - "content": { - "invalid-field": true - }, - "index": 0 - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json b/generativeai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json deleted file mode 100644 index 9d2abbb2..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "promptFeedback": { - "blockReason": "SAFETY", - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "HIGH" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-unknown-model.json b/generativeai/src/test/resources/golden-files/unary/failure-unknown-model.json deleted file mode 100644 index 60b3f55c..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-unknown-model.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 404, - "message": "models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.", - "status": "NOT_FOUND", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::not_found: models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods. [google.rpc.error_details_ext] { message: \"models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.\" }" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json b/generativeai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json deleted file mode 100644 index c4c2ace4..00000000 --- a/generativeai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "User location is not supported for the API use.", - "status": "FAILED_PRECONDITION", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::failed_precondition: User location is not supported for the API use. [google.rpc.error_details_ext] { message: \"User location is not supported for the API use.\" }" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/success-basic-reply-long.json b/generativeai/src/test/resources/golden-files/unary/success-basic-reply-long.json deleted file mode 100644 index 2ee66177..00000000 --- a/generativeai/src/test/resources/golden-files/unary/success-basic-reply-long.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "1. **Use Freshly Ground Coffee**:\n - Grind your coffee beans just before brewing to preserve their flavor and aroma.\n - Use a burr grinder for a consistent grind size.\n\n2. **Choose the Right Water**:\n - Use filtered or spring water for the best taste.\n - Avoid using tap water, as it may contain impurities that can affect the flavor.\n\n3. **Measure Accurately**:\n - Use a kitchen scale to measure your coffee and water precisely.\n - A general rule of thumb is to use 1:16 ratio of coffee to water (e.g., 15 grams of coffee to 240 grams of water).\n\n4. **Preheat Your Equipment**:\n - Preheat your coffee maker or espresso machine before brewing to ensure a consistent temperature.\n\n5. **Control the Water Temperature**:\n - The ideal water temperature for brewing coffee is between 195°F (90°C) and 205°F (96°C).\n - Too hot water can extract bitter flavors, while too cold water won't extract enough flavor.\n\n6. **Steep the Coffee**:\n - For drip coffee, let the water slowly drip through the coffee grounds for optimal extraction.\n - For espresso, maintain a steady pressure and flow rate during the extraction.\n\n7. **Clean Your Equipment**:\n - Regularly clean your coffee maker or espresso machine to remove any residual oils or coffee grounds that can affect the taste.\n\n8. **Experiment with Different Coffee Beans**:\n - Try different coffee beans from various regions and roasts to find your preferred flavor profile.\n\n9. **Store Coffee Properly**:\n - Store your coffee beans in an airtight container in a cool, dark place to preserve their freshness.\n\n10. **Enjoy Freshly Brewed Coffee**:\n - Drink your coffee as soon as possible after brewing to savor its peak flavor and aroma." - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/success-basic-reply-short.json b/generativeai/src/test/resources/golden-files/unary/success-basic-reply-short.json deleted file mode 100644 index 40a9a6da..00000000 --- a/generativeai/src/test/resources/golden-files/unary/success-basic-reply-short.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "Mountain View, California, United States" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/success-citations-altname.json b/generativeai/src/test/resources/golden-files/unary/success-citations-altname.json deleted file mode 100644 index 7adaad5f..00000000 --- a/generativeai/src/test/resources/golden-files/unary/success-citations-altname.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "placeholder" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ], - "citationMetadata": { - "citations": [ - { - "startIndex": 574, - "endIndex": 705, - "uri": "https://example.com/", - "license": "" - }, - { - "startIndex": 899, - "endIndex": 1026, - "uri": "https://example.com/", - "license": "" - } - ] - } - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/success-citations.json b/generativeai/src/test/resources/golden-files/unary/success-citations.json deleted file mode 100644 index 2a765ace..00000000 --- a/generativeai/src/test/resources/golden-files/unary/success-citations.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "placeholder" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ], - "citationMetadata": { - "citationSources": [ - { - "startIndex": 574, - "endIndex": 705, - "uri": "https://example.com/", - "license": "" - }, - { - "startIndex": 899, - "endIndex": 1026, - "uri": "https://example.com/", - "license": "" - } - ] - } - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/success-quote-reply.json b/generativeai/src/test/resources/golden-files/unary/success-quote-reply.json deleted file mode 100644 index f1e5331e..00000000 --- a/generativeai/src/test/resources/golden-files/unary/success-quote-reply.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "1. \"The greatest glory in living lies not in never falling, but in rising every time we fall.\" - Nelson Mandela\n2. \"The future belongs to those who believe in the beauty of their dreams.\" - Eleanor Roosevelt\n3. \"It does not matter how slow you go so long as you do not stop.\" - Confucius\n4. \"If you want to live a happy life, tie it to a goal, not to people or things.\" - Albert Einstein\n5. \"The only person you are destined to become is the person you decide to be.\" - Ralph Waldo Emerson\n6. \"It's not how much you have, but how much you enjoy that makes happiness.\" - Charles Spurgeon\n7. \"The greatest wealth is to live content with little.\" - Plato\n8. \"The only way to do great work is to love what you do.\" - Steve Jobs\n9. \"Don't be afraid to fail. Be afraid not to try.\" - Michael Jordan\n10. \"The best way to predict the future is to create it.\" - Abraham Lincoln" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/generativeai/src/test/resources/golden-files/unary/success-unknown-enum.json b/generativeai/src/test/resources/golden-files/unary/success-unknown-enum.json deleted file mode 100644 index b27a11ae..00000000 --- a/generativeai/src/test/resources/golden-files/unary/success-unknown-enum.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "1. **Use Freshly Ground Coffee**:\n - Grind your coffee beans just before brewing to preserve their flavor and aroma.\n - Use a burr grinder for a consistent grind size.\n\n\n2. **Choose the Right Water**:\n - Use filtered or spring water for the best taste.\n - Avoid using tap water, as it may contain impurities that can affect the flavor.\n\n\n3. **Measure Accurately**:\n - Use a kitchen scale to measure your coffee and water precisely.\n - A general rule of thumb is to use 1:16 ratio of coffee to water (e.g., 15 grams of coffee to 240 grams of water).\n\n\n4. **Preheat Your Equipment**:\n - Preheat your coffee maker or espresso machine before brewing to ensure a consistent temperature.\n\n\n5. **Control the Water Temperature**:\n - The ideal water temperature for brewing coffee is between 195°F (90°C) and 205°F (96°C).\n - Too hot water can extract bitter flavors, while too cold water won't extract enough flavor.\n\n\n6. **Steep the Coffee**:\n - For drip coffee, let the water slowly drip through the coffee grounds for optimal extraction.\n - For pour-over coffee, pour the water in a circular motion over the coffee grounds, allowing it to steep for 30-45 seconds before continuing.\n\n\n7. **Clean Your Equipment**:\n - Regularly clean your coffee maker or espresso machine to prevent the buildup of oils and residue that can affect the taste of your coffee.\n\n\n8. **Experiment with Different Coffee Beans**:\n - Try different coffee beans from various regions and roasts to find your preferred flavor profile.\n - Experiment with different grind sizes and brewing methods to optimize the flavor of your chosen beans.\n\n\n9. **Store Coffee Properly**:\n - Store your coffee beans in an airtight container in a cool, dark place to preserve their freshness and flavor.\n - Avoid storing coffee in the refrigerator or freezer, as this can cause condensation and affect the taste.\n\n\n10. **Enjoy Freshly Brewed Coffee**:\n - Drink your coffee as soon as possible after brewing to enjoy its peak flavor and aroma.\n - Coffee starts to lose its flavor and aroma within 30 minutes of brewing." - } - ] - }, - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT_ENUM_NEW", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT_LIKE_A_NEW_ENUM", - "probability": "NEGLIGIBLE_NEW_ENUM" - } - ] - } -}