From 1ce565020c0e1045eeb697e64bd01b1da3649a0b Mon Sep 17 00:00:00 2001 From: takahirom Date: Tue, 22 Oct 2024 11:43:40 +0900 Subject: [PATCH] Fix naming --- gradle.properties | 5 +-- .../takahirom/roborazzi/RoborazziOptions.kt | 30 ++++++++-------- .../roborazzi/processOutputImageAndReport.kt | 10 +++--- .../{AiOptions.kt => AiCompareOptions.kt} | 34 +++++++++---------- .../takahirom/roborazzi/AiResultFactory.kt | 10 +++--- .../takahirom/roborazzi/CaptureResult.kt | 12 +++---- .../takahirom/roborazzi/CaptureResultTest.kt | 4 +-- .../github/takahirom/roborazzi/RoborazziAi.kt | 34 +++++++++++-------- .../takahirom/roborazzi/RoborazziIos.kt | 16 ++++----- .../takahirom/roborazzi/sample/AiTest.kt | 10 +++--- .../takahirom/roborazzi/sample/ManualTest.kt | 12 +++---- 11 files changed, 91 insertions(+), 86 deletions(-) rename include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/{AiOptions.kt => AiCompareOptions.kt} (64%) diff --git a/gradle.properties b/gradle.properties index a8697e5b..1c9a09f4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -58,5 +58,6 @@ org.jetbrains.compose.experimental.uikit.enabled=true kotlin.incremental.native=true # To debug -roborazzi.test.record=true -#roborazzi.test.verify=true +#roborazzi.test.record=true +roborazzi.test.verify=true +#roborazzi.test.compare=true diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt index 3148e03e..08ff4c30 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt @@ -100,7 +100,7 @@ data class RoborazziOptions( val outputDirectoryPath: String = roborazziSystemPropertyOutputDirectory(), val imageComparator: ImageComparator = DefaultImageComparator, val comparisonStyle: ComparisonStyle = ComparisonStyle.Grid(), - val aiOptions: AiOptions? = null, + val aiCompareOptions: AiCompareOptions? = null, val resultValidator: (result: ImageComparator.ComparisonResult) -> Boolean = DefaultResultValidator, ) { @@ -161,25 +161,27 @@ data class RoborazziOptions( override fun report(captureResult: CaptureResult, roborazziTaskType: RoborazziTaskType) { val aiResult = when (captureResult) { is CaptureResult.Changed -> { - captureResult.aiResult + captureResult.aiComparisonResult } is CaptureResult.Added -> { - captureResult.aiResult + captureResult.aiComparisonResult } else -> { null } } - aiResult?.aiAssertions?.forEach { aiAssertion -> - if (aiAssertion.fulfillmentPercent < aiAssertion.requiredFulfillmentPercent) { + aiResult?.aiConditionResults + ?.filter { conditionResult -> conditionResult.requiredFulfillmentPercent != null } + ?.forEach { conditionResult -> + if (conditionResult.fulfillmentPercent < conditionResult.requiredFulfillmentPercent!!) { throw AssertionError( "The generated image did not meet the required prompt fulfillment percentage.\n" + - "prompt:${aiAssertion.assertPrompt}\n" + - "aiAssertion.fulfillmentPercent:${aiAssertion.fulfillmentPercent}\n" + - "requiredFulfillmentPercent:${aiAssertion.requiredFulfillmentPercent}\n" + - "explanation:${aiAssertion.explanation}" + "prompt:${conditionResult.assertPrompt}\n" + + "aiAssertion.fulfillmentPercent:${conditionResult.fulfillmentPercent}\n" + + "requiredFulfillmentPercent:${conditionResult.requiredFulfillmentPercent}\n" + + "explanation:${conditionResult.explanation}" ) } } @@ -249,8 +251,8 @@ data class RoborazziOptions( ): RoborazziOptions { return copy( compareOptions = compareOptions.copy( - aiOptions = compareOptions.aiOptions!!.copy( - aiAssertions = compareOptions.aiOptions.aiAssertions + AiOptions.AiAssertion( + aiCompareOptions = compareOptions.aiCompareOptions!!.copy( + aiConditions = compareOptions.aiCompareOptions.aiConditions + AiCompareOptions.AiCondition( assertPrompt = assert, requiredFulfillmentPercent = requiredFulfillmentPercent ) @@ -259,11 +261,11 @@ data class RoborazziOptions( ) } - fun addedCompareAiAssertions(vararg assertions: AiOptions.AiAssertion): RoborazziOptions { + fun addedCompareAiAssertions(vararg assertions: AiCompareOptions.AiCondition): RoborazziOptions { return copy( compareOptions = compareOptions.copy( - aiOptions = compareOptions.aiOptions!!.copy( - aiAssertions = compareOptions.aiOptions.aiAssertions + assertions + aiCompareOptions = compareOptions.aiCompareOptions!!.copy( + aiConditions = compareOptions.aiCompareOptions.aiConditions + assertions ) ) ) diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt index 8dccebe7..dc5b430a 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/processOutputImageAndReport.kt @@ -122,9 +122,9 @@ fun processOutputImageAndReport( resizeScale = resizeScale, contextData = contextData ) - val aiOptions = compareOptions.aiOptions - val aiResult = if (aiOptions != null && aiOptions.aiAssertions.isNotEmpty()) { - val aiResult = aiCompareResultFactory?.invoke(comparisonFile.absolutePath, aiOptions) + val aiOptions = compareOptions.aiCompareOptions + val aiResult = if (aiOptions != null && aiOptions.aiConditions.isNotEmpty()) { + val aiResult = aiComparisonResultFactory?.invoke(comparisonFile.absolutePath, aiOptions) ?: throw NotImplementedError("aiCompareCanvasFactory is not implemented. Did you add roborazzi-ai dependency and (call loadRoboAi() or use RoborazziRule)?") aiResult } else { @@ -162,7 +162,7 @@ fun processOutputImageAndReport( goldenFile = goldenFile.absolutePath, timestampNs = System.nanoTime(), diffPercentage = diffPercentage, - aiResult = aiResult, + aiComparisonResult = aiResult, contextData = contextData, ) } else { @@ -171,7 +171,7 @@ fun processOutputImageAndReport( actualFile = actualFile.absolutePath, goldenFile = goldenFile.absolutePath, timestampNs = System.nanoTime(), - aiResult = aiResult, + aiComparisonResult = aiResult, contextData = contextData, ) } diff --git a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiOptions.kt b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiCompareOptions.kt similarity index 64% rename from include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiOptions.kt rename to include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiCompareOptions.kt index 37821f45..8589de97 100644 --- a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiOptions.kt +++ b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiCompareOptions.kt @@ -3,28 +3,26 @@ package com.github.takahirom.roborazzi /** * If you want to use AI to compare images, you can specify the model and prompt. */ -data class AiOptions( +data class AiCompareOptions( val aiModel: AiModel, - val aiAssertions: List = emptyList(), - val inputPrompt: (AiOptions) -> String = { aiOptions -> - buildString { - aiOptions.aiAssertions.forEachIndexed { index, aiAssertion -> - appendLine("Assertion ${index + 1}: ${aiAssertion.assertPrompt}\n") - } - } - }, - val template: String = """ -Evaluate the following assertion for fulfillment in the new image. + val aiConditions: List = emptyList(), + val systemPrompt: String = """Evaluate the following assertion for fulfillment in the new image. The evaluation should be based on the comparison between the original image on the left and the new image on the right, with differences highlighted in red in the center. Focus on whether the new image fulfills the requirement specified in the user input. Output: For each assertion: A fulfillment percentage from 0 to 100. -A brief explanation of how this percentage was determined. - -Assertions: +A brief explanation of how this percentage was determined.""", + val promptTemplate: String = """Assertions: INPUT_PROMPT -""" +""", + val inputPrompt: (AiCompareOptions) -> String = { aiOptions -> + buildString { + aiOptions.aiConditions.forEachIndexed { index, aiAssertion -> + appendLine("Assertion ${index + 1}: ${aiAssertion.assertPrompt}\n") + } + } + }, ) { interface AiModel { data class Gemini( @@ -35,14 +33,14 @@ INPUT_PROMPT /** * You can use this model if you want to use other models. */ - interface Manual : AiModel, AiCompareResultFactory + interface Manual : AiModel, AiComparisonResultFactory } - data class AiAssertion( + data class AiCondition( val assertPrompt: String, /** * If null, the AI result is not validated. But they are still included in the report. */ - val requiredFulfillmentPercent: Int + val requiredFulfillmentPercent: Int? ) } \ No newline at end of file diff --git a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiResultFactory.kt b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiResultFactory.kt index 505776a2..7306e17e 100644 --- a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiResultFactory.kt +++ b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/AiResultFactory.kt @@ -1,13 +1,13 @@ package com.github.takahirom.roborazzi -fun interface AiCompareResultFactory { +fun interface AiComparisonResultFactory { operator fun invoke( comparisonImageFilePath: String, - aiOptions: AiOptions - ): AiResult + aiCompareOptions: AiCompareOptions + ): AiComparisonResult } -var aiCompareResultFactory: AiCompareResultFactory? = - AiCompareResultFactory { comparisonImageFilePath, aiOptions -> +var aiComparisonResultFactory: AiComparisonResultFactory? = + AiComparisonResultFactory { comparisonImageFilePath, aiOptions -> throw NotImplementedError("aiCompareCanvasFactory is not implemented") } diff --git a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResult.kt b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResult.kt index 96389d57..ef7b6e88 100644 --- a/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResult.kt +++ b/include-build/roborazzi-core/src/commonMain/kotlin/com/github/takahirom/roborazzi/CaptureResult.kt @@ -59,7 +59,7 @@ sealed interface CaptureResult { @SerialName("timestamp") override val timestampNs: Long, @SerialName("ai_result") - val aiResult: AiResult?, + val aiComparisonResult: AiComparisonResult?, @SerialName("context_data") override val contextData: Map ) : CaptureResult { @@ -79,7 +79,7 @@ sealed interface CaptureResult { @SerialName("diff_percentage") val diffPercentage: Float?, @SerialName("ai_result") - val aiResult: AiResult?, + val aiComparisonResult: AiComparisonResult?, @SerialName("context_data") override val contextData: Map ) : CaptureResult { @@ -137,15 +137,15 @@ sealed interface CaptureResult { } @Serializable -data class AiResult( - val aiAssertions: List = emptyList() +data class AiComparisonResult( + val aiConditionResults: List = emptyList() ) @Serializable -data class AiAssertion( +data class AiConditionResult( val assertPrompt: String, @SerialName("required_fulfillment_percent") - val requiredFulfillmentPercent: Int, + val requiredFulfillmentPercent: Int?, val fulfillmentPercent: Int, val explanation: String?, ) \ No newline at end of file diff --git a/include-build/roborazzi-core/src/jvmTest/kotlin/io/github/takahirom/roborazzi/CaptureResultTest.kt b/include-build/roborazzi-core/src/jvmTest/kotlin/io/github/takahirom/roborazzi/CaptureResultTest.kt index 39a61db1..411e1fad 100644 --- a/include-build/roborazzi-core/src/jvmTest/kotlin/io/github/takahirom/roborazzi/CaptureResultTest.kt +++ b/include-build/roborazzi-core/src/jvmTest/kotlin/io/github/takahirom/roborazzi/CaptureResultTest.kt @@ -36,7 +36,7 @@ class CaptureResultTest { actualFile = "/actual_file", goldenFile = "/golden_file", timestampNs = 123456789, - aiResult = null, + aiComparisonResult = null, contextData = mapOf( "key" to 2, "keyDouble" to 2.5, @@ -48,7 +48,7 @@ class CaptureResultTest { actualFile = "/actual_file", timestampNs = 123456789, diffPercentage = 0.123f, - aiResult = null, + aiComparisonResult = null, contextData = mapOf("key" to Long.MAX_VALUE - 100), ), CaptureResult.Unchanged( diff --git a/roborazzi-ai/src/commonMain/kotlin/com/github/takahirom/roborazzi/RoborazziAi.kt b/roborazzi-ai/src/commonMain/kotlin/com/github/takahirom/roborazzi/RoborazziAi.kt index 9387631a..0cc05bbd 100644 --- a/roborazzi-ai/src/commonMain/kotlin/com/github/takahirom/roborazzi/RoborazziAi.kt +++ b/roborazzi-ai/src/commonMain/kotlin/com/github/takahirom/roborazzi/RoborazziAi.kt @@ -14,7 +14,7 @@ import kotlin.jvm.JvmName @InternalRoborazziApi val loaded = run { - aiCompareResultFactory = AiCompareResultFactory { comparisonImageFilePath, aiOptions -> + aiComparisonResultFactory = AiComparisonResultFactory { comparisonImageFilePath, aiOptions -> createAiResult(aiOptions, comparisonImageFilePath) } } @@ -22,7 +22,7 @@ val loaded = run { fun loadRoboAi() = loaded @Serializable -data class AssertionResult( +data class GeminiAiConditionResult( @SerialName("fulfillment_percent") val fulfillmentPercent: Int, val explanation: String?, @@ -32,14 +32,18 @@ expect fun readByteArrayFromFile(filePath: String): PlatformImage @InternalRoborazziApi fun createAiResult( - aiOptions: AiOptions, + aiCompareOptions: AiCompareOptions, comparisonImageFilePath: String, -): AiResult { - when (val aiModel = aiOptions.aiModel) { - is AiOptions.AiModel.Gemini -> { +): AiComparisonResult { + when (val aiModel = aiCompareOptions.aiModel) { + is AiCompareOptions.AiModel.Gemini -> { + val systemPrompt = aiCompareOptions.systemPrompt val generativeModel = GenerativeModel( modelName = aiModel.modelName, apiKey = aiModel.apiKey, + systemInstruction = content { + text(systemPrompt) + }, generationConfig = generationConfig { maxOutputTokens = 8192 responseMimeType = "application/json" @@ -69,9 +73,9 @@ fun createAiResult( }, ) - val template = aiOptions.template + val template = aiCompareOptions.promptTemplate - val inputPrompt = aiOptions.inputPrompt(aiOptions) + val inputPrompt = aiCompareOptions.inputPrompt(aiCompareOptions) val inputContent = content { image(readByteArrayFromFile(comparisonImageFilePath)) val prompt = template.replace("INPUT_PROMPT", inputPrompt) @@ -86,18 +90,18 @@ fun createAiResult( debugLog { "RoborazziAi: response: ${response.text}" } - val geminiResult = CaptureResults.json.decodeFromString>( + val geminiResult = CaptureResults.json.decodeFromString>( requireNotNull( response.text ) ) - return AiResult( - aiAssertions = aiOptions.aiAssertions.mapIndexed { index, it -> - val assertResult = geminiResult.getOrNull(index) ?: AssertionResult( + return AiComparisonResult( + aiConditionResults = aiCompareOptions.aiConditions.mapIndexed { index, it -> + val assertResult = geminiResult.getOrNull(index) ?: GeminiAiConditionResult( fulfillmentPercent = 0, explanation = "AI model did not return a result for this assertion" ) - AiAssertion( + AiConditionResult( assertPrompt = it.assertPrompt, requiredFulfillmentPercent = it.requiredFulfillmentPercent, fulfillmentPercent = assertResult.fulfillmentPercent, @@ -107,8 +111,8 @@ fun createAiResult( ) } - is AiOptions.AiModel.Manual -> { - return aiModel(comparisonImageFilePath, aiOptions) + is AiCompareOptions.AiModel.Manual -> { + return aiModel(comparisonImageFilePath, aiCompareOptions) } else -> { diff --git a/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt b/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt index 63dafb56..3999f746 100644 --- a/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt +++ b/roborazzi-compose-ios/src/iosMain/kotlin/io/github/takahirom/roborazzi/RoborazziIos.kt @@ -523,7 +523,7 @@ fun SemanticsNodeInteraction.captureRoboImage( actualFile = actualFilePath, goldenFile = goldenFilePath, timestampNs = getNanoTime(), - aiResult = null, + aiComparisonResult = null, contextData = emptyMap() ) writeJson(result, resultsDir, nameWithoutExtension) @@ -541,7 +541,7 @@ fun SemanticsNodeInteraction.captureRoboImage( goldenFile = goldenFilePath, timestampNs = getNanoTime(), diffPercentage = Float.NaN, - aiResult = null, + aiComparisonResult = null, contextData = emptyMap() ) writeJson(result, resultsDir, nameWithoutExtension) @@ -568,7 +568,7 @@ fun SemanticsNodeInteraction.captureRoboImage( actualFile = actualFilePath, goldenFile = goldenFilePath, timestampNs = getNanoTime(), - aiResult = null, + aiComparisonResult = null, contextData = emptyMap() ) writeJson(result, resultsDir, nameWithoutExtension) @@ -586,7 +586,7 @@ fun SemanticsNodeInteraction.captureRoboImage( goldenFile = goldenFilePath, timestampNs = getNanoTime(), diffPercentage = Float.NaN, - aiResult = null, + aiComparisonResult = null, contextData = emptyMap() ) writeJson(result, resultsDir, nameWithoutExtension) @@ -613,7 +613,7 @@ fun SemanticsNodeInteraction.captureRoboImage( actualFile = goldenFilePath, goldenFile = goldenFilePath, timestampNs = getNanoTime(), - aiResult = null, + aiComparisonResult = null, contextData = emptyMap() ) writeJson(result, resultsDir, nameWithoutExtension) @@ -631,7 +631,7 @@ fun SemanticsNodeInteraction.captureRoboImage( goldenFile = goldenFilePath, timestampNs = getNanoTime(), diffPercentage = Float.NaN, - aiResult = null, + aiComparisonResult = null, contextData = emptyMap() ) writeJson(result, resultsDir, nameWithoutExtension) @@ -658,7 +658,7 @@ fun SemanticsNodeInteraction.captureRoboImage( actualFile = goldenFilePath, goldenFile = goldenFilePath, timestampNs = getNanoTime(), - aiResult = null, + aiComparisonResult = null, contextData = emptyMap() ) writeJson(result, resultsDir, nameWithoutExtension) @@ -676,7 +676,7 @@ fun SemanticsNodeInteraction.captureRoboImage( goldenFile = goldenFilePath, timestampNs = getNanoTime(), diffPercentage = Float.NaN, - aiResult = null, + aiComparisonResult = null, contextData = emptyMap() ) writeJson(result, resultsDir, nameWithoutExtension) diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/AiTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/AiTest.kt index 7ee32760..9a423f88 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/AiTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/AiTest.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.github.takahirom.roborazzi.AiOptions +import com.github.takahirom.roborazzi.AiCompareOptions import com.github.takahirom.roborazzi.ROBORAZZI_DEBUG import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RoborazziOptions @@ -32,8 +32,8 @@ class AiTest { options = RoborazziRule.Options( roborazziOptions = RoborazziOptions( compareOptions = RoborazziOptions.CompareOptions( - aiOptions = AiOptions( - aiModel = AiOptions.AiModel.Gemini( + aiCompareOptions = AiCompareOptions( + aiModel = AiCompareOptions.AiModel.Gemini( apiKey = System.getenv("gemini_api_key") ?: "" ), ) @@ -52,11 +52,11 @@ class AiTest { onView(ViewMatchers.isRoot()) .captureRoboImage( roborazziOptions = provideRoborazziContext().options.addedCompareAiAssertions( - AiOptions.AiAssertion( + AiCompareOptions.AiCondition( assertPrompt = "it should have PREVIOUS button", requiredFulfillmentPercent = 90, ), - AiOptions.AiAssertion( + AiCompareOptions.AiCondition( assertPrompt = "it should show First Fragment", requiredFulfillmentPercent = 90, ) diff --git a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt index 64ca78f5..2be7a628 100644 --- a/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt +++ b/sample-android/src/test/java/com/github/takahirom/roborazzi/sample/ManualTest.kt @@ -19,7 +19,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 import com.dropbox.differ.ImageComparator import com.dropbox.differ.SimpleImageComparator -import com.github.takahirom.roborazzi.AiOptions +import com.github.takahirom.roborazzi.AiCompareOptions import com.github.takahirom.roborazzi.Dump import com.github.takahirom.roborazzi.RoboComponent import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers @@ -68,18 +68,18 @@ class ManualTest { roborazziOptions = RoborazziOptions( compareOptions = if (System.getenv("gemini_api_key")?.isNotBlank() == true) { RoborazziOptions.CompareOptions( - aiOptions = AiOptions( - aiAssertions = listOf( - AiOptions.AiAssertion( + aiCompareOptions = AiCompareOptions( + aiConditions = listOf( + AiCompareOptions.AiCondition( assertPrompt = "it should have PREVIOUS button", requiredFulfillmentPercent = 90, ), - AiOptions.AiAssertion( + AiCompareOptions.AiCondition( assertPrompt = "it should show First Fragment", requiredFulfillmentPercent = 90, ), ), - aiModel = AiOptions.AiModel.Gemini( + aiModel = AiCompareOptions.AiModel.Gemini( apiKey = System.getenv("gemini_api_key")!!, ), )