Skip to content

feat(firebase-ai): add function calling example #2678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.google.firebase.quickstart.ai

import com.google.firebase.ai.type.FunctionDeclaration
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ResponseModality
import com.google.firebase.ai.type.Schema
import com.google.firebase.ai.type.Tool
import com.google.firebase.ai.type.content
import com.google.firebase.ai.type.generationConfig
import com.google.firebase.quickstart.ai.ui.navigation.Category
Expand All @@ -11,15 +14,15 @@ val FIREBASE_AI_SAMPLES = listOf(
Sample(
title = "Travel tips",
description = "The user wants the model to help a new traveler" +
" with travel tips",
" with travel tips",
navRoute = "chat",
categories = listOf(Category.TEXT),
systemInstructions = content {
text(
"You are a Travel assistant. You will answer" +
" questions the user asks based on the information listed" +
" in Relevant Information. Do not hallucinate. Do not use" +
" the internet."
" questions the user asks based on the information listed" +
" in Relevant Information. Do not hallucinate. Do not use" +
" the internet."
)
},
chatHistory = listOf(
Expand All @@ -31,7 +34,7 @@ val FIREBASE_AI_SAMPLES = listOf(
role = "model"
text(
"You should book flights a couple of months ahead of time." +
" It will be cheaper and more flexible for you."
" It will be cheaper and more flexible for you."
)
},
content {
Expand All @@ -42,8 +45,8 @@ val FIREBASE_AI_SAMPLES = listOf(
role = "model"
text(
"If you are traveling outside your own country, make sure" +
" your passport is up-to-date and valid for more" +
" than 6 months during your travel."
" your passport is up-to-date and valid for more" +
" than 6 months during your travel."
)
}
),
Expand All @@ -57,8 +60,8 @@ val FIREBASE_AI_SAMPLES = listOf(
systemInstructions = content {
text(
"You are a chatbot for the county's performing and fine arts" +
" program. You help students decide what course they will" +
" take during the summer."
" program. You help students decide what course they will" +
" take during the summer."
)
},
initialPrompt = content {
Expand All @@ -75,14 +78,14 @@ val FIREBASE_AI_SAMPLES = listOf(
content("model") {
text(
"Of course! Click on the attach button" +
" below and choose an audio file for me to summarize."
" below and choose an audio file for me to summarize."
)
}
),
initialPrompt = content {
text(
"I have attached the audio file. Please analyze it and summarize the contents" +
" of the audio as bullet points."
" of the audio as bullet points."
)
}
),
Expand Down Expand Up @@ -114,8 +117,8 @@ val FIREBASE_AI_SAMPLES = listOf(
)
text(
"Write a short, engaging blog post based on this picture." +
" It should include a description of the meal in the" +
" photo and talk about my journey meal prepping."
" It should include a description of the meal in the" +
" photo and talk about my journey meal prepping."
)
}
),
Expand All @@ -139,8 +142,8 @@ val FIREBASE_AI_SAMPLES = listOf(
initialPrompt = content {
text(
"Hi, can you create a 3d rendered image of a pig " +
"with wings and a top hat flying over a happy " +
"futuristic scifi city with lots of greenery?"
"with wings and a top hat flying over a happy " +
"futuristic scifi city with lots of greenery?"
)
},
generationConfig = generationConfig {
Expand All @@ -165,7 +168,7 @@ val FIREBASE_AI_SAMPLES = listOf(
)
text(
"The first document is from 2013, and the second document is" +
" from 2023. How did the standard deduction evolve?"
" from 2023. How did the standard deduction evolve?"
)
}
),
Expand All @@ -182,9 +185,9 @@ val FIREBASE_AI_SAMPLES = listOf(
)
text(
"Generate 5-10 hashtags that relate to the video content." +
" Try to use more popular and engaging terms," +
" e.g. #Viral. Do not add content not related to" +
" the video.\n Start the output with 'Tags:'"
" Try to use more popular and engaging terms," +
" e.g. #Viral. Do not add content not related to" +
" the video.\n Start the output with 'Tags:'"
)
}
),
Expand All @@ -198,16 +201,44 @@ val FIREBASE_AI_SAMPLES = listOf(
content("model") {
text(
"Sure! Click on the attach button below and choose a" +
" video file for me to describe."
" video file for me to describe."
)
}
),
initialPrompt = content {
text(
"I have attached the video file. Provide a description of" +
" the video. The description should also contain" +
" anything important which people say in the video."
" the video. The description should also contain" +
" anything important which people say in the video."
)
}
)
),
Sample(
title = "Weather Chat",
description = "Use function calling to get the weather conditions" +
" for a specific US city on a specific date.",
navRoute = "chat",
categories = listOf(Category.TEXT, Category.FUNCTION_CALLING),
tools = listOf(
Tool.functionDeclarations(
listOf(
FunctionDeclaration(
"fetchWeather",
"Get the weather conditions for a specific US city on a specific date.",
mapOf(
"city" to Schema.string("The US city of the location."),
Copy link
Contributor

@marinacoelho marinacoelho Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid confusion on what string should be sent as "city", "state" and "date", I'd send an empty string on this code sample and add a code comment on the line above, with something along the lines of // Replace this empty string with the US city of choice. (Same for state and date).

"state" to Schema.string("The US state of the location."),
"date" to Schema.string(
"The date for which to get the weather." +
" Date must be in the format: YYYY-MM-DD."
),
),
)
)
)
),
initialPrompt = content {
text("What was the weather in Boston, MA on October 17, 2024?")
}
),
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.google.firebase.quickstart.ai.feature.text

import android.graphics.BitmapFactory
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.SavedStateHandle
Expand All @@ -12,13 +13,17 @@ import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.FileDataPart
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.FunctionResponsePart
import com.google.firebase.ai.type.GenerateContentResponse
import com.google.firebase.ai.type.TextPart
import com.google.firebase.ai.type.asTextOrNull
import com.google.firebase.ai.type.content
import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES
import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.jsonPrimitive

class ChatViewModel(
savedStateHandle: SavedStateHandle
Expand Down Expand Up @@ -61,7 +66,8 @@ class ChatViewModel(
).generativeModel(
modelName = sample.modelName ?: "gemini-2.5-flash",
systemInstruction = sample.systemInstructions,
generationConfig = sample.generationConfig
generationConfig = sample.generationConfig,
tools = sample.tools
)
chat = generativeModel.startChat(sample.chatHistory)

Expand All @@ -86,7 +92,15 @@ class ChatViewModel(
_isLoading.value = true
try {
val response = chat.sendMessage(prompt)
_messageList.add(response.candidates.first().content)
if (response.functionCalls.isEmpty()) {
// Samples without function calling can simply display
// the response in the UI
_messageList.add(response.candidates.first().content)
} else {
// Samples WITH function calling need to perform
// additional handling
handleFunctionCalls(response)
}
_errorMessage.value = null // clear errors
} catch (e: Exception) {
_errorMessage.value = e.localizedMessage
Expand All @@ -112,6 +126,47 @@ class ChatViewModel(
_attachmentsList.add(Attachment(fileName ?: "Unnamed file"))
}

/**
* Only used by samples with function calling
*/
private suspend fun handleFunctionCalls(
response: GenerateContentResponse
) {
response.functionCalls.forEach { functionCall ->
Log.d(
"ChatViewModel", "Model responded with function call:" +
functionCall.name
)
when (functionCall.name) {
"fetchWeather" -> {
// Handle the call to fetchWeather()
val city = functionCall.args["city"]!!.jsonPrimitive.content
val state = functionCall.args["city"]!!.jsonPrimitive.content
val date = functionCall.args["date"]!!.jsonPrimitive.content

val functionResponse = WeatherRepository
.fetchWeather(city, state, date)

// Send the response(s) from the function back to the model
// so that the model can use it to generate its final response.
val finalResponse = chat.sendMessage(content("function") {
part(FunctionResponsePart("fetchWeather", functionResponse))
})

Log.d("ChatViewModel", "Model responded with: ${finalResponse.text}")
_messageList.add(finalResponse.candidates.first().content)
}

else -> {
Log.d(
"ChatViewModel", "Model responded with unknown" +
" function call: ${functionCall.name}"
)
}
}
}
}

private fun decodeBitmapFromImage(input: ByteArray) =
BitmapFactory.decodeByteArray(input, 0, input.size)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.google.firebase.quickstart.ai.feature.text.functioncalling

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

/**
* Hypothetical repository that calls an external weather API.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more accessible option (language-wise) is "Example repository".

*/
class WeatherRepository {

companion object {
suspend fun fetchWeather(
city: String, state: String, date: String
): JsonObject = withContext(Dispatchers.IO) {
// For demo purposes, this hypothetical response is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, I'd rephrase it to something along the lines of // This response is a hardcoded example of the expected format, and should be used for demo purposes only

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marinacoelho I copied this from the docs, but we could discuss changing the docs too if it makes more sense

// hardcoded here in the expected format.
return@withContext JsonObject(
mapOf(
"temperature" to JsonPrimitive(38),
"chancePrecipitation" to JsonPrimitive("56%"),
"cloudConditions" to JsonPrimitive("partlyCloudy")
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.google.firebase.quickstart.ai.ui.navigation
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerationConfig
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.Tool
import java.util.UUID

enum class Category(
Expand All @@ -12,7 +13,8 @@ enum class Category(
IMAGE("Image"),
VIDEO("Video"),
AUDIO("Audio"),
DOCUMENT("Document")
DOCUMENT("Document"),
FUNCTION_CALLING("Function calling"),
}

data class Sample(
Expand All @@ -27,5 +29,6 @@ data class Sample(
val initialPrompt: Content? = null,
val systemInstructions: Content? = null,
val generationConfig: GenerationConfig? = null,
val chatHistory: List<Content> = emptyList()
val chatHistory: List<Content> = emptyList(),
val tools: List<Tool>? = null
)
Loading