spring-projects / spring-ai

An Application Framework for AI Engineering
https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/index.html
Apache License 2.0
2.54k stars 615 forks source link

Function Calling with Kotlin Functions #922

Open jochenchrist opened 3 weeks ago

jochenchrist commented 3 weeks ago

Bug description When using a Kotlin function, the input type is not inferred. This causes an invalid_request_error error:

com.azure.core.exception.HttpResponseException: Status code 400, "{
  "error": {
    "message": "Invalid schema for function 'Weather': schema must be a JSON Schema of 'type: \"object\"', got 'type: \"None\"'.",
    "type": "invalid_request_error",
    "param": "tools[0].function.parameters",
    "code": "invalid_function_parameters"
  }
}"

Workaround is to specify the input type explictly withInputType (and use a custom object mapper)

Environment Latest Snapshot.

Steps to reproduce See example below

Expected behavior Spring AI should also work with Kotlin or update the documentation.

Minimal Complete Reproducible example


@Component
class MyAi(
  val functionCallbacks: List<FunctionCallback>,
) : ApplicationRunner {

  override fun run(args: ApplicationArguments) {

    val openAIClient = OpenAIClientBuilder()
      .credential(AzureKeyCredential(System.getenv("AZURE_OPENAI_API_KEY")))
      .endpoint(System.getenv("AZURE_OPENAI_ENDPOINT"))
      .buildClient()

    val openAIChatOptions = AzureOpenAiChatOptions.builder()
      .withDeploymentName("policy-ai-gpt-4o")
      .withTemperature(0.0f)
      .withFunctionCallbacks(functionCallbacks)
      .build()

    val chatModel = AzureOpenAiChatModel(openAIClient, openAIChatOptions)

    val response = chatModel.call(
      Prompt("Is it rainy in Paris?", AzureOpenAiChatOptions.builder().withFunction("Weather").build())
    )

    println(response.result.output)

  }
}

@Configuration
class AiFunctionCallbacks(val objectMapper: ObjectMapper) {

  @Bean
  fun weatherFunctionCallback(): FunctionCallback {
    return FunctionCallbackWrapper.builder { request: WeatherRequest ->
      WeatherResponse(rainy = false)
    }
      .withName("Weather")
      .withDescription("Get weather information for a city")
      .withObjectMapper(objectMapper)
//      .withInputType(WeatherRequest::class.java) // this is required, but should not
      .build()
  }

  data class WeatherRequest(
    val city: String?,
  )

  data class WeatherResponse(
    val rainy: Boolean?,
  )
}
KAMO030 commented 2 weeks ago

I reproduced the problem. The cause is that when inputType is not specified, the resolveInputType function uses reflection to get the generic type. However, when using a function with a trailing lambda, it is not possible to obtain the generic type declaration.

public FunctionCallbackWrapper<I, O> build() {
// ...
            if (this.inputType == null) {
                this.inputType = FunctionCallbackWrapper.resolveInputType(this.function);
            }
// ...
}

Currently, the issue can be resolved through the aforementioned method, or by passing in a specific implementation class. Alternatively, it could be addressed by adding support for reified generic function extensions in the library:

inline fun<reified I,O> functionCallbackWrapperBuild(
    noinline function:(I)->O
): FunctionCallbackWrapper.Builder<I,O> =
    FunctionCallbackWrapper.builder(function).withInputType(I::class.java)

or perhaps by import other reflection packages to obtain the Metadata info?