Repository navigation
Add hybrid function calling sample - #2855
VinayGuthal wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new Hybrid Function Calling feature, adding the necessary ViewModel, UI screen, state representation, and navigation routing to support testing function calling tools across on-device, hybrid, and cloud inference modes. The feedback highlights opportunities to improve robustness and consistency, specifically by using safe casting to prevent potential exceptions during JSON parsing, ensuring the streaming generation path handles automatic function calling consistently with the unary path, and avoiding redundant model downloads in the UI when the selected option has not changed.
| val city = input["city"]?.jsonPrimitive?.content | ||
| val state = input["state"]?.jsonPrimitive?.content | ||
| val date = input["date"]?.jsonPrimitive?.content |
There was a problem hiding this comment.
Using .jsonPrimitive on a JsonElement that is not a primitive (e.g., if the model unexpectedly returns a nested JSON object or array) will throw an IllegalArgumentException. To make the input parsing robust against unexpected model outputs, use safe casting with as? JsonPrimitive instead.
| val city = input["city"]?.jsonPrimitive?.content | |
| val state = input["state"]?.jsonPrimitive?.content | |
| val date = input["date"]?.jsonPrimitive?.content | |
| val city = (input["city"] as? JsonPrimitive)?.content | |
| val state = (input["state"] as? JsonPrimitive)?.content | |
| val date = (input["date"] as? JsonPrimitive)?.content |
| if (toolOption == FunctionToolOption.MANUAL && detectedCalls.isNotEmpty()) { | ||
| val firstCall = detectedCalls.first() | ||
| val city = (firstCall.args["city"] as? JsonPrimitive)?.content ?: "Boston" | ||
| val state = (firstCall.args["state"] as? JsonPrimitive)?.content ?: "MA" | ||
| val date = (firstCall.args["date"] as? JsonPrimitive)?.content ?: "2024-10-17" | ||
| val weatherJson = WeatherRepository.fetchWeather(city, state, date) | ||
| executionResult = weatherJson.toString() | ||
|
|
||
| val followUp = model.generateContent( | ||
| content(role = "user") { text(promptText) }, | ||
| content(role = "model") { part(firstCall) }, | ||
| content(role = "user") { | ||
| part(FunctionResponsePart(firstCall.name, weatherJson, firstCall.id)) | ||
| } | ||
| ) | ||
| fullText = followUp.text.orEmpty() | ||
| source = followUp.inferenceSource | ||
| } |
There was a problem hiding this comment.
In the STREAMING generation method, there is no handling for FunctionToolOption.AUTO to capture and set the executionResult when a function call is detected. This causes an inconsistency with the UNARY method where the execution result is captured and displayed in the UI. Adding an else if block for AUTO will resolve this.
if (toolOption == FunctionToolOption.MANUAL && detectedCalls.isNotEmpty()) {
val firstCall = detectedCalls.first()
val city = (firstCall.args["city"] as? JsonPrimitive)?.content ?: "Boston"
val state = (firstCall.args["state"] as? JsonPrimitive)?.content ?: "MA"
val date = (firstCall.args["date"] as? JsonPrimitive)?.content ?: "2024-10-17"
val weatherJson = WeatherRepository.fetchWeather(city, state, date)
executionResult = weatherJson.toString()
val followUp = model.generateContent(
content(role = "user") { text(promptText) },
content(role = "model") { part(firstCall) },
content(role = "user") {
part(FunctionResponsePart(firstCall.name, weatherJson, firstCall.id))
}
)
fullText = followUp.text.orEmpty()
source = followUp.inferenceSource
} else if (toolOption == FunctionToolOption.AUTO && detectedCalls.isNotEmpty()) {
val firstCall = detectedCalls.first()
val city = (firstCall.args["city"] as? JsonPrimitive)?.content ?: "Boston"
val state = (firstCall.args["state"] as? JsonPrimitive)?.content ?: "MA"
val date = (firstCall.args["date"] as? JsonPrimitive)?.content ?: "2024-10-17"
executionResult = WeatherRepository.fetchWeather(city, state, date).toString()
}| onClick = { | ||
| selectedModelOption = modelOpt | ||
| viewModel.checkAndDownloadModel(modelOpt.option) | ||
| }, |
There was a problem hiding this comment.
To avoid redundant network requests and job cancellations when the user taps an already selected model option, add a check to ensure checkAndDownloadModel is only called when the selection actually changes.
onClick = {
if (selectedModelOption != modelOpt) {
selectedModelOption = modelOpt
viewModel.checkAndDownloadModel(modelOpt.option)
}
},
Description
Adds a new sample screen to test function calling tools across on-device, hybrid (
PREFER_ON_DEVICE), and cloud inference modes. This sample complements the function calling support added in firebase-android-sdk #8758.Features
FunctionDeclaration): DefinesfetchWeather(city, state, date), inspects returnedFunctionCallPartentries, executes the local tool, and sendsFunctionResponsePartto resolve the multi-turn exchange.AutoFunctionDeclaration): Validates automatic tool invocation handled directly by the SDK.Tool.googleSearch()): Demonstrates validation behavior and automatic cloud fallback when non-function tools are used with hybrid inference.ONLY_ON_DEVICE,PREFER_ON_DEVICE,ONLY_IN_CLOUD).generateContent) and streaming (generateContentStream) generation methods.ON_DEVICEvsIN_CLOUD).Summary of Changes
gradle/libs.versions.toml:firebase-aito18.2.0andfirebase-ai-ondeviceto16.0.0-beta08.firebase-ai/app:FunctionCallingViewModel.kt,FunctionCallingUiState.kt, andFunctionCallingScreen.ktundercom.google.firebase.quickstart.ai.FUNCTION_CALLINGinScreenType(Sample.kt).HYBRIDandTOOLS_FCcategories inFirebaseAISamples.kt.MainActivity.kt.Testing
./gradlew :app:assembleDebug./gradlew ktlintCheck