Skip to content

Commit 0550af5

Browse files
authored
1 parent 18ec3a6 commit 0550af5

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

ai-logic/firebase-ai/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- [feature] Added automatic function calling support for `LiveGenerativeModel`. (#8223)
4+
35
# 17.13.0
46

57
- [feature] Expanded `SpeechConfig` to support `MultiSpeakerVoiceConfig` and `LanguageCode`.

ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import com.google.firebase.FirebaseApp
2020
import com.google.firebase.ai.common.APIController
2121
import com.google.firebase.ai.common.AppCheckHeaderProvider
2222
import com.google.firebase.ai.common.JSON
23+
import com.google.firebase.ai.type.AutoFunctionDeclaration
2324
import com.google.firebase.ai.type.Content
25+
import com.google.firebase.ai.type.FirebaseAutoFunctionException
26+
import com.google.firebase.ai.type.FunctionCallPart
27+
import com.google.firebase.ai.type.FunctionResponsePart
2428
import com.google.firebase.ai.type.GenerativeBackend
2529
import com.google.firebase.ai.type.LiveClientSetupMessage
2630
import com.google.firebase.ai.type.LiveGenerationConfig
@@ -40,8 +44,11 @@ import io.ktor.websocket.readBytes
4044
import kotlin.coroutines.CoroutineContext
4145
import kotlinx.coroutines.channels.ClosedReceiveChannelException
4246
import kotlinx.serialization.ExperimentalSerializationApi
47+
import kotlinx.serialization.InternalSerializationApi
4348
import kotlinx.serialization.encodeToString
4449
import kotlinx.serialization.json.JsonObject
50+
import kotlinx.serialization.json.JsonPrimitive
51+
import kotlinx.serialization.json.jsonObject
4552

4653
/**
4754
* Represents a multimodal model (like Gemini) capable of real-time content generation based on
@@ -140,7 +147,9 @@ internal constructor(
140147
session = webSession,
141148
blockingDispatcher = blockingDispatcher,
142149
firebaseApp = firebaseApp,
143-
connectionFactory = connectFactory
150+
connectionFactory = connectFactory,
151+
hasFunction = ::hasFunction,
152+
executeFunction = ::executeFunction
144153
)
145154
} catch (e: ClosedReceiveChannelException) {
146155
val reason = webSession?.closeReason?.await()
@@ -150,6 +159,55 @@ internal constructor(
150159
}
151160
}
152161

162+
internal fun hasFunction(call: FunctionCallPart): Boolean {
163+
return tools
164+
.flatMap { it.autoFunctionDeclarations ?: emptyList() }
165+
.firstOrNull { it.name == call.name && it.functionReference != null } != null
166+
}
167+
168+
@OptIn(InternalSerializationApi::class)
169+
internal suspend fun executeFunction(call: FunctionCallPart): FunctionResponsePart {
170+
if (tools.isEmpty()) {
171+
throw RuntimeException("No registered tools")
172+
}
173+
val tool = tools.flatMap { it.autoFunctionDeclarations ?: emptyList() }
174+
val declaration =
175+
tool.firstOrNull { it.name == call.name }
176+
?: throw RuntimeException("No registered function named ${call.name}")
177+
return executeFunction<Any, Any>(
178+
call,
179+
declaration as AutoFunctionDeclaration<Any, Any>,
180+
JsonObject(call.args).toString()
181+
)
182+
}
183+
184+
@OptIn(InternalSerializationApi::class)
185+
internal suspend fun <I : Any, O : Any> executeFunction(
186+
functionCall: FunctionCallPart,
187+
functionDeclaration: AutoFunctionDeclaration<I, O>,
188+
parameter: String
189+
): FunctionResponsePart {
190+
val inputDeserializer = functionDeclaration.inputSchema.getSerializer()
191+
val input = JSON.decodeFromString(inputDeserializer, parameter)
192+
val functionReference =
193+
functionDeclaration.functionReference
194+
?: throw RuntimeException("Function reference for ${functionDeclaration.name} is missing")
195+
try {
196+
val output = functionReference.invoke(input)
197+
val outputSerializer = functionDeclaration.outputSchema?.getSerializer()
198+
if (outputSerializer != null) {
199+
return FunctionResponsePart.from(
200+
JSON.encodeToJsonElement(outputSerializer, output).jsonObject
201+
)
202+
.normalizeAgainstCall(functionCall)
203+
}
204+
return (output as FunctionResponsePart).normalizeAgainstCall(functionCall)
205+
} catch (e: FirebaseAutoFunctionException) {
206+
return FunctionResponsePart.from(JsonObject(mapOf("error" to JsonPrimitive(e.message))))
207+
.normalizeAgainstCall(functionCall)
208+
}
209+
}
210+
153211
private companion object {
154212
private val TAG = LiveGenerativeModel::class.java.simpleName
155213
}

ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ internal constructor(
7575
private val firebaseApp: FirebaseApp,
7676
private val connectionFactory:
7777
(suspend (SessionResumptionConfig?) -> DefaultClientWebSocketSession)? =
78-
null
78+
null,
79+
private val hasFunction: ((FunctionCallPart) -> Boolean)? = null,
80+
private val executeFunction: (suspend (FunctionCallPart) -> FunctionResponsePart)? = null
7981
) {
8082
/**
8183
* Coroutine scope that we batch data on for network related behavior.
@@ -580,6 +582,14 @@ internal constructor(
580582
// It's fine to suspend here since you can't have a function call running concurrently
581583
// with an audio response
582584
sendFunctionResponse(it.functionCalls.map(functionCallHandler).toList())
585+
} else if (
586+
hasFunction != null &&
587+
executeFunction != null &&
588+
it.functionCalls.all { f -> hasFunction.invoke(f) }
589+
) {
590+
sendFunctionResponse(
591+
it.functionCalls.map { call -> executeFunction.invoke(call) }.toList()
592+
)
583593
} else {
584594
Log.w(
585595
TAG,

0 commit comments

Comments
 (0)