Skip to content

Commit 8df07ce

Browse files
authored
feat(firebase-ai): add function calling example (#2678)
1 parent 3723e86 commit 8df07ce

File tree

4 files changed

+145
-28
lines changed

4 files changed

+145
-28
lines changed

firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.google.firebase.quickstart.ai
22

3+
import com.google.firebase.ai.type.FunctionDeclaration
34
import com.google.firebase.ai.type.GenerativeBackend
45
import com.google.firebase.ai.type.ResponseModality
6+
import com.google.firebase.ai.type.Schema
7+
import com.google.firebase.ai.type.Tool
58
import com.google.firebase.ai.type.content
69
import com.google.firebase.ai.type.generationConfig
710
import com.google.firebase.quickstart.ai.ui.navigation.Category
@@ -11,15 +14,15 @@ val FIREBASE_AI_SAMPLES = listOf(
1114
Sample(
1215
title = "Travel tips",
1316
description = "The user wants the model to help a new traveler" +
14-
" with travel tips",
17+
" with travel tips",
1518
navRoute = "chat",
1619
categories = listOf(Category.TEXT),
1720
systemInstructions = content {
1821
text(
1922
"You are a Travel assistant. You will answer" +
20-
" questions the user asks based on the information listed" +
21-
" in Relevant Information. Do not hallucinate. Do not use" +
22-
" the internet."
23+
" questions the user asks based on the information listed" +
24+
" in Relevant Information. Do not hallucinate. Do not use" +
25+
" the internet."
2326
)
2427
},
2528
chatHistory = listOf(
@@ -31,7 +34,7 @@ val FIREBASE_AI_SAMPLES = listOf(
3134
role = "model"
3235
text(
3336
"You should book flights a couple of months ahead of time." +
34-
" It will be cheaper and more flexible for you."
37+
" It will be cheaper and more flexible for you."
3538
)
3639
},
3740
content {
@@ -42,8 +45,8 @@ val FIREBASE_AI_SAMPLES = listOf(
4245
role = "model"
4346
text(
4447
"If you are traveling outside your own country, make sure" +
45-
" your passport is up-to-date and valid for more" +
46-
" than 6 months during your travel."
48+
" your passport is up-to-date and valid for more" +
49+
" than 6 months during your travel."
4750
)
4851
}
4952
),
@@ -57,8 +60,8 @@ val FIREBASE_AI_SAMPLES = listOf(
5760
systemInstructions = content {
5861
text(
5962
"You are a chatbot for the county's performing and fine arts" +
60-
" program. You help students decide what course they will" +
61-
" take during the summer."
63+
" program. You help students decide what course they will" +
64+
" take during the summer."
6265
)
6366
},
6467
initialPrompt = content {
@@ -75,14 +78,14 @@ val FIREBASE_AI_SAMPLES = listOf(
7578
content("model") {
7679
text(
7780
"Of course! Click on the attach button" +
78-
" below and choose an audio file for me to summarize."
81+
" below and choose an audio file for me to summarize."
7982
)
8083
}
8184
),
8285
initialPrompt = content {
8386
text(
8487
"I have attached the audio file. Please analyze it and summarize the contents" +
85-
" of the audio as bullet points."
88+
" of the audio as bullet points."
8689
)
8790
}
8891
),
@@ -114,8 +117,8 @@ val FIREBASE_AI_SAMPLES = listOf(
114117
)
115118
text(
116119
"Write a short, engaging blog post based on this picture." +
117-
" It should include a description of the meal in the" +
118-
" photo and talk about my journey meal prepping."
120+
" It should include a description of the meal in the" +
121+
" photo and talk about my journey meal prepping."
119122
)
120123
}
121124
),
@@ -139,8 +142,8 @@ val FIREBASE_AI_SAMPLES = listOf(
139142
initialPrompt = content {
140143
text(
141144
"Hi, can you create a 3d rendered image of a pig " +
142-
"with wings and a top hat flying over a happy " +
143-
"futuristic scifi city with lots of greenery?"
145+
"with wings and a top hat flying over a happy " +
146+
"futuristic scifi city with lots of greenery?"
144147
)
145148
},
146149
generationConfig = generationConfig {
@@ -165,7 +168,7 @@ val FIREBASE_AI_SAMPLES = listOf(
165168
)
166169
text(
167170
"The first document is from 2013, and the second document is" +
168-
" from 2023. How did the standard deduction evolve?"
171+
" from 2023. How did the standard deduction evolve?"
169172
)
170173
}
171174
),
@@ -182,9 +185,9 @@ val FIREBASE_AI_SAMPLES = listOf(
182185
)
183186
text(
184187
"Generate 5-10 hashtags that relate to the video content." +
185-
" Try to use more popular and engaging terms," +
186-
" e.g. #Viral. Do not add content not related to" +
187-
" the video.\n Start the output with 'Tags:'"
188+
" Try to use more popular and engaging terms," +
189+
" e.g. #Viral. Do not add content not related to" +
190+
" the video.\n Start the output with 'Tags:'"
188191
)
189192
}
190193
),
@@ -198,16 +201,44 @@ val FIREBASE_AI_SAMPLES = listOf(
198201
content("model") {
199202
text(
200203
"Sure! Click on the attach button below and choose a" +
201-
" video file for me to describe."
204+
" video file for me to describe."
202205
)
203206
}
204207
),
205208
initialPrompt = content {
206209
text(
207210
"I have attached the video file. Provide a description of" +
208-
" the video. The description should also contain" +
209-
" anything important which people say in the video."
211+
" the video. The description should also contain" +
212+
" anything important which people say in the video."
210213
)
211214
}
212-
)
215+
),
216+
Sample(
217+
title = "Weather Chat",
218+
description = "Use function calling to get the weather conditions" +
219+
" for a specific US city on a specific date.",
220+
navRoute = "chat",
221+
categories = listOf(Category.TEXT, Category.FUNCTION_CALLING),
222+
tools = listOf(
223+
Tool.functionDeclarations(
224+
listOf(
225+
FunctionDeclaration(
226+
"fetchWeather",
227+
"Get the weather conditions for a specific US city on a specific date.",
228+
mapOf(
229+
"city" to Schema.string("The US city of the location."),
230+
"state" to Schema.string("The US state of the location."),
231+
"date" to Schema.string(
232+
"The date for which to get the weather." +
233+
" Date must be in the format: YYYY-MM-DD."
234+
),
235+
),
236+
)
237+
)
238+
)
239+
),
240+
initialPrompt = content {
241+
text("What was the weather in Boston, MA on October 17, 2024?")
242+
}
243+
),
213244
)

firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.google.firebase.quickstart.ai.feature.text
22

33
import android.graphics.BitmapFactory
4+
import android.util.Log
45
import androidx.compose.runtime.mutableStateListOf
56
import androidx.compose.runtime.toMutableStateList
67
import androidx.lifecycle.SavedStateHandle
@@ -12,13 +13,17 @@ import com.google.firebase.ai.Chat
1213
import com.google.firebase.ai.ai
1314
import com.google.firebase.ai.type.Content
1415
import com.google.firebase.ai.type.FileDataPart
15-
import com.google.firebase.ai.type.GenerativeBackend
16+
import com.google.firebase.ai.type.FunctionResponsePart
17+
import com.google.firebase.ai.type.GenerateContentResponse
1618
import com.google.firebase.ai.type.TextPart
1719
import com.google.firebase.ai.type.asTextOrNull
20+
import com.google.firebase.ai.type.content
1821
import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES
22+
import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository
1923
import kotlinx.coroutines.flow.MutableStateFlow
2024
import kotlinx.coroutines.flow.StateFlow
2125
import kotlinx.coroutines.launch
26+
import kotlinx.serialization.json.jsonPrimitive
2227

2328
class ChatViewModel(
2429
savedStateHandle: SavedStateHandle
@@ -61,7 +66,8 @@ class ChatViewModel(
6166
).generativeModel(
6267
modelName = sample.modelName ?: "gemini-2.5-flash",
6368
systemInstruction = sample.systemInstructions,
64-
generationConfig = sample.generationConfig
69+
generationConfig = sample.generationConfig,
70+
tools = sample.tools
6571
)
6672
chat = generativeModel.startChat(sample.chatHistory)
6773

@@ -86,7 +92,15 @@ class ChatViewModel(
8692
_isLoading.value = true
8793
try {
8894
val response = chat.sendMessage(prompt)
89-
_messageList.add(response.candidates.first().content)
95+
if (response.functionCalls.isEmpty()) {
96+
// Samples without function calling can simply display
97+
// the response in the UI
98+
_messageList.add(response.candidates.first().content)
99+
} else {
100+
// Samples WITH function calling need to perform
101+
// additional handling
102+
handleFunctionCalls(response)
103+
}
90104
_errorMessage.value = null // clear errors
91105
} catch (e: Exception) {
92106
_errorMessage.value = e.localizedMessage
@@ -112,6 +126,47 @@ class ChatViewModel(
112126
_attachmentsList.add(Attachment(fileName ?: "Unnamed file"))
113127
}
114128

129+
/**
130+
* Only used by samples with function calling
131+
*/
132+
private suspend fun handleFunctionCalls(
133+
response: GenerateContentResponse
134+
) {
135+
response.functionCalls.forEach { functionCall ->
136+
Log.d(
137+
"ChatViewModel", "Model responded with function call:" +
138+
functionCall.name
139+
)
140+
when (functionCall.name) {
141+
"fetchWeather" -> {
142+
// Handle the call to fetchWeather()
143+
val city = functionCall.args["city"]!!.jsonPrimitive.content
144+
val state = functionCall.args["city"]!!.jsonPrimitive.content
145+
val date = functionCall.args["date"]!!.jsonPrimitive.content
146+
147+
val functionResponse = WeatherRepository
148+
.fetchWeather(city, state, date)
149+
150+
// Send the response(s) from the function back to the model
151+
// so that the model can use it to generate its final response.
152+
val finalResponse = chat.sendMessage(content("function") {
153+
part(FunctionResponsePart("fetchWeather", functionResponse))
154+
})
155+
156+
Log.d("ChatViewModel", "Model responded with: ${finalResponse.text}")
157+
_messageList.add(finalResponse.candidates.first().content)
158+
}
159+
160+
else -> {
161+
Log.d(
162+
"ChatViewModel", "Model responded with unknown" +
163+
" function call: ${functionCall.name}"
164+
)
165+
}
166+
}
167+
}
168+
}
169+
115170
private fun decodeBitmapFromImage(input: ByteArray) =
116171
BitmapFactory.decodeByteArray(input, 0, input.size)
117172
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.google.firebase.quickstart.ai.feature.text.functioncalling
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.withContext
5+
import kotlinx.serialization.json.JsonObject
6+
import kotlinx.serialization.json.JsonPrimitive
7+
8+
/**
9+
* Hypothetical repository that calls an external weather API.
10+
*/
11+
class WeatherRepository {
12+
13+
companion object {
14+
suspend fun fetchWeather(
15+
city: String, state: String, date: String
16+
): JsonObject = withContext(Dispatchers.IO) {
17+
// For demo purposes, this hypothetical response is
18+
// hardcoded here in the expected format.
19+
return@withContext JsonObject(
20+
mapOf(
21+
"temperature" to JsonPrimitive(38),
22+
"chancePrecipitation" to JsonPrimitive("56%"),
23+
"cloudConditions" to JsonPrimitive("partlyCloudy")
24+
)
25+
)
26+
}
27+
}
28+
}

firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.google.firebase.quickstart.ai.ui.navigation
33
import com.google.firebase.ai.type.Content
44
import com.google.firebase.ai.type.GenerationConfig
55
import com.google.firebase.ai.type.GenerativeBackend
6+
import com.google.firebase.ai.type.Tool
67
import java.util.UUID
78

89
enum class Category(
@@ -12,7 +13,8 @@ enum class Category(
1213
IMAGE("Image"),
1314
VIDEO("Video"),
1415
AUDIO("Audio"),
15-
DOCUMENT("Document")
16+
DOCUMENT("Document"),
17+
FUNCTION_CALLING("Function calling"),
1618
}
1719

1820
data class Sample(
@@ -27,5 +29,6 @@ data class Sample(
2729
val initialPrompt: Content? = null,
2830
val systemInstructions: Content? = null,
2931
val generationConfig: GenerationConfig? = null,
30-
val chatHistory: List<Content> = emptyList()
32+
val chatHistory: List<Content> = emptyList(),
33+
val tools: List<Tool>? = null
3134
)

0 commit comments

Comments
 (0)