From 34ac31910d5371f6b8ca4e566f12b6ef375dccb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 24 Dec 2024 17:11:18 +0100 Subject: [PATCH] Kotlin updates (#2006) * Update Kotlin documentation This commit updates the Kotlin documentation to showcase the new Kotlin reflection capabilities allowing to identify required properties. * Update Kotlin integration tests This commit updates the Kotlin integration tests to leverage the new Kotlin reflection capabilities allowing to identify required properties. It also adds a missing jackson-module-kotlin dependency and refine test implementation to be closer to the Java ones and use more idiomatic Kotlin code. --- .../modules/ROOT/pages/api/chat/openai-chat.adoc | 14 ++++++-------- spring-ai-spring-boot-autoconfigure/pom.xml | 6 ++++++ .../ollama/tool/FunctionCallbackContextKotlinIT.kt | 9 +++++---- .../ollama/tool/FunctionCallbackKotlinIT.kt | 11 +++++------ .../ollama/tool/MockKotlinWeatherService.kt | 11 ++++------- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc index 6fb7cc247..262db5a46 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc @@ -376,14 +376,14 @@ Kotlin:: [source,kotlin] ---- data class MathReasoning( - @get:JsonProperty(required = true, value = "steps") val steps: Steps, - @get:JsonProperty(required = true, value = "final_answer") val finalAnswer: String) { + val steps: Steps, + @get:JsonProperty(value = "final_answer") val finalAnswer: String) { - data class Steps(@get:JsonProperty(required = true, value = "items") val items: Array) { + data class Steps(val items: Array) { data class Items( - @get:JsonProperty(required = true, value = "explanation") val explanation: String, - @get:JsonProperty(required = true, value = "output") val output: String) + val explanation: String, + val output: String) } } @@ -405,9 +405,7 @@ val mathReasoning = outputConverter.convert(content) ====== -- -NOTE: Ensure you use the `@JsonProperty(required = true,...)` annotation (`@get:JsonProperty(required = true,...)` with Kotlin in order to generate the annotation on the related getters, see link:https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets[related documentation]). -This is crucial for generating a schema that accurately marks fields as `required`. -Although this is optional for JSON Schema, OpenAI link:https://platform.openai.com/docs/guides/structured-outputs/all-fields-must-be-required[mandates] it for the structured response to function correctly. +NOTE: Although this is optional for JSON Schema, OpenAI link:https://platform.openai.com/docs/guides/structured-outputs/all-fields-must-be-required#all-fields-must-be-required[mandates] required fields for the structured response to function correctly. Kotlin reflection is used to infer which property are required or not based on the nullability of types and default values of parameters, so for most use case `@get:JsonProperty(required = true)` is not needed. `@get:JsonProperty(value = "custom_name")` can be useful to customize the property name. Make sure to generate the annotation on the related getters with this `@get:` syntax, see link:https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets[related documentation]. ==== Configuring via Application Properties diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml index d2abc8de5..92ac01362 100644 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -578,6 +578,12 @@ test + + com.fasterxml.jackson.module + jackson-module-kotlin + test + + diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackContextKotlinIT.kt b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackContextKotlinIT.kt index 296488562..884d40d03 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackContextKotlinIT.kt +++ b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackContextKotlinIT.kt @@ -39,7 +39,7 @@ class FunctionCallbackResolverKotlinIT : BaseOllamaIT() { companion object { - private val MODEL_NAME = OllamaModel.LLAMA3_2.getName(); + private val MODEL_NAME = "qwen2.5:3b"; @JvmStatic @BeforeAll @@ -72,7 +72,7 @@ class FunctionCallbackResolverKotlinIT : BaseOllamaIT() { val response = chatModel .call(Prompt(listOf(userMessage), OllamaOptions.builder().function("weatherInfo").build())) - logger.info("Response: " + response) + logger.info("Response: $response") assertThat(response.getResult().output.text).contains("30", "10", "15") } @@ -93,10 +93,11 @@ class FunctionCallbackResolverKotlinIT : BaseOllamaIT() { .build() val response = chatModel.call(Prompt(listOf(userMessage), functionOptions)); + val output = response.getResult().output.text - logger.info("Response: " + response.getResult().getOutput().getText()); + logger.info("Response: $output"); - assertThat(response.getResult().output.text).contains("30", "10", "15"); + assertThat(output).contains("30", "10", "15"); } } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackKotlinIT.kt b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackKotlinIT.kt index 129d3619a..691c22bf9 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackKotlinIT.kt +++ b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackKotlinIT.kt @@ -28,7 +28,6 @@ import org.springframework.ai.chat.prompt.Prompt import org.springframework.ai.model.function.FunctionCallback import org.springframework.ai.model.function.FunctionCallingOptions import org.springframework.ai.ollama.OllamaChatModel -import org.springframework.ai.ollama.api.OllamaModel import org.springframework.ai.ollama.api.OllamaOptions import org.springframework.boot.autoconfigure.AutoConfigurations import org.springframework.boot.test.context.runner.ApplicationContextRunner @@ -39,7 +38,7 @@ class FunctionCallbackKotlinIT : BaseOllamaIT() { companion object { - private val MODEL_NAME = OllamaModel.LLAMA3_2.getName(); + private val MODEL_NAME = "qwen2.5:3b"; @JvmStatic @BeforeAll @@ -72,7 +71,7 @@ class FunctionCallbackKotlinIT : BaseOllamaIT() { val response = chatModel .call(Prompt(listOf(userMessage), OllamaOptions.builder().function("WeatherInfo").build())) - logger.info("Response: " + response) + logger.info("Response: $response") assertThat(response.getResult().output.text).contains("30", "10", "15") } @@ -93,10 +92,10 @@ class FunctionCallbackKotlinIT : BaseOllamaIT() { .build() val response = chatModel.call(Prompt(listOf(userMessage), functionOptions)); + val output = response.getResult().output.text + logger.info("Response: $output"); - logger.info("Response: " + response.getResult().getOutput().getText()); - - assertThat(response.getResult().output.text).contains("30", "10", "15"); + assertThat(output).contains("30", "10", "15"); } } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/MockKotlinWeatherService.kt b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/MockKotlinWeatherService.kt index ee223a21c..a0811bdb7 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/MockKotlinWeatherService.kt +++ b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/MockKotlinWeatherService.kt @@ -61,19 +61,16 @@ enum class Unit(val unitName: String) { @JsonInclude(Include.NON_NULL) @JsonClassDescription("Weather API request") data class KotlinRequest( - @get:JsonProperty(required = true, value = "location") + @get:JsonPropertyDescription("The city and state e.g. San Francisco, CA") - val location: String = "", + val location: String, - @get:JsonProperty(required = true, value = "lat") @get:JsonPropertyDescription("The city latitude") - val lat: Double = 0.0, + val lat: Double, - @get:JsonProperty(required = true, value = "lon") @get:JsonPropertyDescription("The city longitude") - val lon: Double = 0.0, + val lon: Double, - @get:JsonProperty(required = true, value = "unit") @get:JsonPropertyDescription("Temperature unit") val unit: Unit = Unit.C )