From 2b5754958316270978b29a7b2986e6dc16af3977 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Thu, 30 Oct 2025 12:11:32 -0700 Subject: [PATCH 01/14] add KSP processor for Firebase AI --- gradle/libs.versions.toml | 4 ++++ subprojects.cfg | 2 ++ 2 files changed, 6 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e8636054f4e..71e540e1676 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,7 @@ jsonassert = "1.5.0" kotest = "5.9.0" # Do not use 5.9.1 because it reverts the fix for https://github.com/kotest/kotest/issues/3981 kotestAssertionsCore = "5.8.1" kotlin = "2.0.21" +kotlinpoetKsp = "2.2.0" ktorVersion = "3.0.3" legacySupportV4 = "1.0.0" lifecycleProcess = "2.3.1" @@ -69,6 +70,7 @@ rxjava = "2.2.21" serialization = "1.7.3" slf4jNop = "2.0.17" spotless = "7.0.4" +symbolProcessingApi = "2.2.10-2.0.2" testServices = "1.6.0" truth = "1.4.4" truthProtoExtension = "1.0" @@ -142,6 +144,7 @@ kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin kotlin-coroutines-tasks = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoetKsp" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "coroutines" } @@ -203,6 +206,7 @@ rxandroid = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "rxandroi rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava" } slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4jNop" } spotless-plugin-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } +symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "symbolProcessingApi" } truth = { module = "com.google.truth:truth", version.ref = "truth" } truth-liteproto-extension = { module = "com.google.truth.extensions:truth-liteproto-extension", version.ref = "truth" } truth-proto-extension = { module = "com.google.truth.extensions:truth-proto-extension", version.ref = "truthProtoExtension" } diff --git a/subprojects.cfg b/subprojects.cfg index f8505ecf8e7..a167e6eb58c 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -74,3 +74,5 @@ transport:transport-runtime-testing # sdk #firebase-storage:test-app #appcheck:firebase-appcheck:test-app #firebase-appdistribution:test-app + +firebase-ai-ksp-processor # buildtools From 86430467c9e70f5289c6fd49b88607acf82a9b22 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Thu, 30 Oct 2025 12:11:51 -0700 Subject: [PATCH 02/14] add the code --- firebase-ai-ksp-processor/README.md | 13 + firebase-ai-ksp-processor/build.gradle.kts | 46 +++ firebase-ai-ksp-processor/gradle.properties | 1 + .../firebase/ai/annotations/Generable.kt | 21 ++ .../google/firebase/ai/annotations/Guide.kt | 28 ++ .../firebase/ai/ksp/SchemaSymbolProcessor.kt | 298 ++++++++++++++++++ .../ai/ksp/SchemaSymbolProcessorProvider.kt | 27 ++ ...ols.ksp.processing.SymbolProcessorProvider | 1 + 8 files changed, 435 insertions(+) create mode 100644 firebase-ai-ksp-processor/README.md create mode 100644 firebase-ai-ksp-processor/build.gradle.kts create mode 100644 firebase-ai-ksp-processor/gradle.properties create mode 100644 firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt create mode 100644 firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt create mode 100644 firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt create mode 100644 firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessorProvider.kt create mode 100644 firebase-ai-ksp-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider diff --git a/firebase-ai-ksp-processor/README.md b/firebase-ai-ksp-processor/README.md new file mode 100644 index 00000000000..961100598df --- /dev/null +++ b/firebase-ai-ksp-processor/README.md @@ -0,0 +1,13 @@ +To build run `./gradlew :publishToMavenLocal` + +To integrate: add the following to your app's gradle file: + +```declarative +plugins { + id("com.google.devtools.ksp") +} +dependencies { + implementation("com.google.firebase:firebase-ai-ksp-processor:1.0.0") + ksp("com.google.firebase:firebase-ai-ksp-processor:1.0.0") +} +``` diff --git a/firebase-ai-ksp-processor/build.gradle.kts b/firebase-ai-ksp-processor/build.gradle.kts new file mode 100644 index 00000000000..bd3304eb6ad --- /dev/null +++ b/firebase-ai-ksp-processor/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + kotlin("jvm") + id("java-library") + id("maven-publish") +} + +dependencies { + testImplementation(kotlin("test")) + implementation(libs.symbol.processing.api) + implementation(libs.kotlinpoet.ksp) +} + +tasks.test { useJUnitPlatform() } + +kotlin { jvmToolchain(21) } + +publishing { + publications { + create("mavenKotlin") { + from(components["kotlin"]) + groupId = "com.google.firebase" + artifactId = "firebase-ai-ksp-processor" + version = "1.0.0" + } + } + repositories { + maven { url = uri("m2/") } + mavenLocal() + } +} diff --git a/firebase-ai-ksp-processor/gradle.properties b/firebase-ai-ksp-processor/gradle.properties new file mode 100644 index 00000000000..7fc6f1ff272 --- /dev/null +++ b/firebase-ai-ksp-processor/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt new file mode 100644 index 00000000000..9cd1370b4d0 --- /dev/null +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.ai.annotations + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +public annotation class Generable() diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt new file mode 100644 index 00000000000..1a060721b00 --- /dev/null +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.ai.annotations + +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.SOURCE) +public annotation class Guide( + public val description: String = "", + public val minimum: Double = -1.0, + public val maximum: Double = -1.0, + public val minItems: Int = -1, + public val maxItems: Int = -1, + public val format: String = "", +) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt new file mode 100644 index 00000000000..75b453b862f --- /dev/null +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -0,0 +1,298 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.ai.ksp + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSVisitorVoid +import com.google.devtools.ksp.symbol.Modifier +import com.google.firebase.ai.annotations.Generable +import com.google.firebase.ai.annotations.Guide +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.writeTo + +public class SchemaSymbolProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, +) : SymbolProcessor { + override fun process(resolver: Resolver): List { + resolver + .getSymbolsWithAnnotation(Generable::class.qualifiedName.orEmpty()) + .filterIsInstance() + .map { it to SchemaSymbolProcessorVisitor(it, resolver) } + .forEach { it.second.visitClassDeclaration(it.first, Unit) } + + return emptyList() + } + + private inner class SchemaSymbolProcessorVisitor( + private val klass: KSClassDeclaration, + private val resolver: Resolver, + ) : KSVisitorVoid() { + private val numberTypes = setOf("kotlin.Int", "kotlin.Long", "kotlin.Double", "kotlin.Float") + private val baseKdocRegex = Regex("^\\s*(.*?)((@\\w* .*)|\\z)", RegexOption.DOT_MATCHES_ALL) + private val propertyKdocRegex = + Regex("\\s*@property (\\w*) (.*?)(?=@\\w*|\\z)", RegexOption.DOT_MATCHES_ALL) + + override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { + val isDataClass = classDeclaration.modifiers.contains(Modifier.DATA) + if (!isDataClass) { + logger.error("${classDeclaration.qualifiedName} is not a data class") + } + val generatedSchemaFile = generateFileSpec(classDeclaration) + generatedSchemaFile.writeTo( + codeGenerator, + Dependencies(true, classDeclaration.containingFile!!), + ) + } + + fun generateFileSpec(classDeclaration: KSClassDeclaration): FileSpec { + return FileSpec.builder( + classDeclaration.packageName.asString(), + "${classDeclaration.simpleName.asString()}GeneratedSchema", + ) + .addImport("com.google.firebase.ai.type", "Schema") + .addType( + TypeSpec.classBuilder("${classDeclaration.simpleName.asString()}GeneratedSchema") + .addType( + TypeSpec.companionObjectBuilder() + .addProperty( + PropertySpec.builder( + "SCHEMA", + ClassName("com.google.firebase.ai.type", "Schema"), + KModifier.PUBLIC, + ) + .mutable(false) + .initializer( + CodeBlock.builder() + .add( + generateCodeBlockForSchema(type = classDeclaration.asType(emptyList())) + ) + .build() + ) + .build() + ) + .build() + ) + .build() + ) + .build() + } + + @OptIn(KspExperimental::class) + fun generateCodeBlockForSchema( + name: String? = null, + description: String? = null, + type: KSType, + parentType: KSType? = null, + guideAnnotation: KSAnnotation? = null, + ): CodeBlock { + val parameterizedName = type.toTypeName() as? ParameterizedTypeName + val className = parameterizedName?.rawType ?: type.toClassName() + val kdocString = type.declaration.docString ?: "" + val baseKdoc = extractBaseKdoc(kdocString) + val propertyDocs = extractPropertyKdocs(kdocString) + val guideClassAnnotation = + type.annotations.firstOrNull() { + it.shortName.getShortName() == Guide::class.java.simpleName + } + val description = + getDescriptionFromAnnotations(guideAnnotation, guideClassAnnotation, description, baseKdoc) + val minimum = getDoubleFromAnnotation(guideAnnotation, "minimum") + val maximum = getDoubleFromAnnotation(guideAnnotation, "maximum") + val minItems = getIntFromAnnotation(guideAnnotation, "minItems") + val maxItems = getIntFromAnnotation(guideAnnotation, "maxItems") + val format = getStringFromAnnotation(guideAnnotation, "format") + val builder = CodeBlock.builder() + when (className.canonicalName) { + "kotlin.Int" -> { + builder.addStatement("Schema.integer(") + } + "kotlin.Long" -> { + builder.addStatement("Schema.long(") + } + "kotlin.Boolean" -> { + builder.addStatement("Schema.boolean(") + } + "kotlin.Float" -> { + builder.addStatement("Schema.float(") + } + "kotlin.Double" -> { + builder.addStatement("Schema.double(") + } + "kotlin.String" -> { + builder.addStatement("Schema.string(") + } + else -> { + if (className.canonicalName == "kotlin.collections.List") { + val listTypeParam = type.arguments.first().type!!.resolve() + val listParamCodeBlock = + generateCodeBlockForSchema(type = listTypeParam, parentType = type) + builder.addStatement("Schema.array(items = ").add(listParamCodeBlock).addStatement(",") + } else { + builder.addStatement("Schema.obj(properties = ") + val properties = + (type.declaration as KSClassDeclaration).getAllProperties().associate { property -> + val propertyName = property.simpleName.asString() + propertyName to + generateCodeBlockForSchema( + type = property.type.resolve(), + parentType = type, + description = propertyDocs[propertyName], + name = propertyName, + guideAnnotation = + property.annotations.firstOrNull() { + it.shortName.getShortName() == Guide::class.java.simpleName + }, + ) + } + builder.addStatement("mapOf(") + properties.entries.forEach { + builder.addStatement("%S to ", it.key).add(it.value).addStatement(", ") + } + builder.addStatement("),") + } + } + } + if (name != null) { + builder.addStatement("title = %S,", name) + } + if (description != null) { + builder.addStatement("description = %S,", description) + } + if ((minimum != null || maximum != null) && !numberTypes.contains(className.canonicalName)) { + logger.warn( + "${parentType?.toClassName()?.simpleName?.let { "$it." }}$name is not a number type, minimum and maximum are not valid parameters to specify in @Guide" + ) + } + if ( + (minItems != null || maxItems != null) && + className.canonicalName !== "kotlin.collections.List" + ) { + logger.warn( + "${parentType?.toClassName()?.simpleName?.let { "$it." }}$name is not a List type, minItems and maxItems are not valid parameters to specify in @Guide" + ) + } + if (format != null && className.canonicalName !== "kotlin.String") { + logger.warn( + "${parentType?.toClassName()?.simpleName?.let { "$it." }}$name is not a String type, format is not a valid parameter to specify in @Guide" + ) + } + if (minimum != null) { + builder.addStatement("minimum = %L,", minimum) + } + if (maximum != null) { + builder.addStatement("maximum = %L,", maximum) + } + if (minItems != null) { + builder.addStatement("minItems = %L,", minItems) + } + if (maxItems != null) { + builder.addStatement("maxItems = %L,", maxItems) + } + if (format != null) { + builder.addStatement("format = %S,", format) + } + builder.addStatement("nullable = %L)", className.isNullable) + return builder.build() + } + + private fun getDescriptionFromAnnotations( + guideAnnotation: KSAnnotation?, + guideClassAnnotation: KSAnnotation?, + description: String?, + baseKdoc: String?, + ): String? { + val guidePropertyDescription = getStringFromAnnotation(guideAnnotation, "description") + + val guideClassDescription = getStringFromAnnotation(guideClassAnnotation, "description") + + return guidePropertyDescription ?: guideClassDescription ?: description ?: baseKdoc + } + + private fun getDoubleFromAnnotation( + guideAnnotation: KSAnnotation?, + doubleName: String, + ): Double? { + val guidePropertyDoubleValue = + guideAnnotation + ?.arguments + ?.firstOrNull { it.name?.getShortName()?.equals(doubleName) == true } + ?.value as? Double + if (guidePropertyDoubleValue == null || guidePropertyDoubleValue == -1.0) { + return null + } + return guidePropertyDoubleValue + } + + private fun getIntFromAnnotation(guideAnnotation: KSAnnotation?, intName: String): Int? { + val guidePropertyIntValue = + guideAnnotation + ?.arguments + ?.firstOrNull { it.name?.getShortName()?.equals(intName) == true } + ?.value as? Int + if (guidePropertyIntValue == null || guidePropertyIntValue == -1) { + return null + } + return guidePropertyIntValue + } + + private fun getStringFromAnnotation( + guideAnnotation: KSAnnotation?, + stringName: String, + ): String? { + val guidePropertyStringValue = + guideAnnotation + ?.arguments + ?.firstOrNull { it.name?.getShortName()?.equals(stringName) == true } + ?.value as? String + if (guidePropertyStringValue.isNullOrEmpty()) { + return null + } + return guidePropertyStringValue + } + + private fun extractBaseKdoc(kdoc: String): String? { + return baseKdocRegex.matchEntire(kdoc)?.groups?.get(1)?.value?.trim().let { + if (it.isNullOrEmpty()) null else it + } + } + + private fun extractPropertyKdocs(kdoc: String): Map { + return propertyKdocRegex + .findAll(kdoc) + .map { it.groups[1]!!.value to it.groups[2]!!.value.replace("\n", "").trim() } + .toMap() + } + } +} diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessorProvider.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessorProvider.kt new file mode 100644 index 00000000000..2c8015bc8a9 --- /dev/null +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessorProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.ai.ksp + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +public class SchemaSymbolProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return SchemaSymbolProcessor(environment.codeGenerator, environment.logger) + } +} diff --git a/firebase-ai-ksp-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/firebase-ai-ksp-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000000..83d92f28c7e --- /dev/null +++ b/firebase-ai-ksp-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +com.google.firebase.ai.ksp.SchemaSymbolProcessorProvider \ No newline at end of file From 811122b1e449c408b589669175d1abd832dd5ef0 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Thu, 30 Oct 2025 12:43:51 -0700 Subject: [PATCH 03/14] Update firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt index 75b453b862f..61911cc2c1f 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -203,7 +203,7 @@ public class SchemaSymbolProcessor( "${parentType?.toClassName()?.simpleName?.let { "$it." }}$name is not a List type, minItems and maxItems are not valid parameters to specify in @Guide" ) } - if (format != null && className.canonicalName !== "kotlin.String") { + if (format != null && className.canonicalName != "kotlin.String") { logger.warn( "${parentType?.toClassName()?.simpleName?.let { "$it." }}$name is not a String type, format is not a valid parameter to specify in @Guide" ) From 0d5d3f750e252f0d5cf759372551ff003073d2b3 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Thu, 30 Oct 2025 12:44:00 -0700 Subject: [PATCH 04/14] Update firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt index 61911cc2c1f..e6a524a14c7 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -197,7 +197,7 @@ public class SchemaSymbolProcessor( } if ( (minItems != null || maxItems != null) && - className.canonicalName !== "kotlin.collections.List" + className.canonicalName != "kotlin.collections.List" ) { logger.warn( "${parentType?.toClassName()?.simpleName?.let { "$it." }}$name is not a List type, minItems and maxItems are not valid parameters to specify in @Guide" From e7a1ab82a463a10eb8f802d502281cea962cdbd3 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Thu, 30 Oct 2025 12:46:40 -0700 Subject: [PATCH 05/14] Update firebase-ai-ksp-processor/README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- firebase-ai-ksp-processor/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-ai-ksp-processor/README.md b/firebase-ai-ksp-processor/README.md index 961100598df..07cd50d27d2 100644 --- a/firebase-ai-ksp-processor/README.md +++ b/firebase-ai-ksp-processor/README.md @@ -2,7 +2,7 @@ To build run `./gradlew :publishToMavenLocal` To integrate: add the following to your app's gradle file: -```declarative +```kotlin plugins { id("com.google.devtools.ksp") } From 54e9466c667b5c92ab6ce051e2c2698fed61ab78 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Thu, 30 Oct 2025 12:46:48 -0700 Subject: [PATCH 06/14] Update firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../main/kotlin/com/google/firebase/ai/annotations/Generable.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt index 9cd1370b4d0..b4a5e652ae5 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt @@ -18,4 +18,4 @@ package com.google.firebase.ai.annotations @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) -public annotation class Generable() +public annotation class Generable From 368c2ecddd8e53fb11028639002bb26067a2e720 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Wed, 5 Nov 2025 08:04:51 -0800 Subject: [PATCH 07/14] add generated annotation to generated class --- .../kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt index e6a524a14c7..618918f9019 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -40,6 +40,7 @@ import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.writeTo +import javax.annotation.processing.Generated public class SchemaSymbolProcessor( private val codeGenerator: CodeGenerator, @@ -84,6 +85,7 @@ public class SchemaSymbolProcessor( .addImport("com.google.firebase.ai.type", "Schema") .addType( TypeSpec.classBuilder("${classDeclaration.simpleName.asString()}GeneratedSchema") + .addAnnotation(Generated::class) .addType( TypeSpec.companionObjectBuilder() .addProperty( From bf2aa4d092184a1a20b25e03b26d19e0c1816560 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Wed, 5 Nov 2025 11:47:47 -0800 Subject: [PATCH 08/14] improve file formatting and indentation --- .../firebase/ai/ksp/SchemaSymbolProcessor.kt | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt index 618918f9019..f5bf6fcccf3 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -138,31 +138,40 @@ public class SchemaSymbolProcessor( val builder = CodeBlock.builder() when (className.canonicalName) { "kotlin.Int" -> { - builder.addStatement("Schema.integer(") + builder.addStatement("Schema.integer(").indent() } "kotlin.Long" -> { - builder.addStatement("Schema.long(") + builder.addStatement("Schema.long(").indent() } "kotlin.Boolean" -> { - builder.addStatement("Schema.boolean(") + builder.addStatement("Schema.boolean(").indent() } "kotlin.Float" -> { - builder.addStatement("Schema.float(") + builder.addStatement("Schema.float(").indent() } "kotlin.Double" -> { - builder.addStatement("Schema.double(") + builder.addStatement("Schema.double(").indent() } "kotlin.String" -> { - builder.addStatement("Schema.string(") + builder.addStatement("Schema.string(").indent() } else -> { if (className.canonicalName == "kotlin.collections.List") { val listTypeParam = type.arguments.first().type!!.resolve() val listParamCodeBlock = generateCodeBlockForSchema(type = listTypeParam, parentType = type) - builder.addStatement("Schema.array(items = ").add(listParamCodeBlock).addStatement(",") + builder + .addStatement("Schema.array(") + .indent() + .addStatement("items = ") + .add(listParamCodeBlock) + .addStatement(",") } else { - builder.addStatement("Schema.obj(properties = ") + builder + .addStatement("Schema.obj(") + .indent() + .addStatement("properties = mapOf(") + .indent() val properties = (type.declaration as KSClassDeclaration).getAllProperties().associate { property -> val propertyName = property.simpleName.asString() @@ -178,11 +187,15 @@ public class SchemaSymbolProcessor( }, ) } - builder.addStatement("mapOf(") properties.entries.forEach { - builder.addStatement("%S to ", it.key).add(it.value).addStatement(", ") + builder + .addStatement("%S to ", it.key) + .indent() + .add(it.value) + .unindent() + .addStatement(", ") } - builder.addStatement("),") + builder.unindent().addStatement("),") } } } @@ -225,7 +238,7 @@ public class SchemaSymbolProcessor( if (format != null) { builder.addStatement("format = %S,", format) } - builder.addStatement("nullable = %L)", className.isNullable) + builder.addStatement("nullable = %L)", className.isNullable).unindent() return builder.build() } From 94a2e45711b754f8f2bd74faf87c0f105319a1b2 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Wed, 5 Nov 2025 11:53:32 -0800 Subject: [PATCH 09/14] move annotations to firebase-ai, and rename the artifact --- firebase-ai-ksp-processor/build.gradle.kts | 2 +- .../com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt | 6 +++--- .../kotlin/com/google/firebase/ai/annotations/Generable.kt | 0 .../main/kotlin/com/google/firebase/ai/annotations/Guide.kt | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename {firebase-ai-ksp-processor => firebase-ai}/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt (100%) rename {firebase-ai-ksp-processor => firebase-ai}/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt (100%) diff --git a/firebase-ai-ksp-processor/build.gradle.kts b/firebase-ai-ksp-processor/build.gradle.kts index bd3304eb6ad..16df3455759 100644 --- a/firebase-ai-ksp-processor/build.gradle.kts +++ b/firebase-ai-ksp-processor/build.gradle.kts @@ -35,7 +35,7 @@ publishing { create("mavenKotlin") { from(components["kotlin"]) groupId = "com.google.firebase" - artifactId = "firebase-ai-ksp-processor" + artifactId = "firebase-ai-processor" version = "1.0.0" } } diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt index f5bf6fcccf3..aee193f7302 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -48,7 +48,7 @@ public class SchemaSymbolProcessor( ) : SymbolProcessor { override fun process(resolver: Resolver): List { resolver - .getSymbolsWithAnnotation(Generable::class.qualifiedName.orEmpty()) + .getSymbolsWithAnnotation("com.google.firebase.ai.annotations.Generable") .filterIsInstance() .map { it to SchemaSymbolProcessorVisitor(it, resolver) } .forEach { it.second.visitClassDeclaration(it.first, Unit) } @@ -126,7 +126,7 @@ public class SchemaSymbolProcessor( val propertyDocs = extractPropertyKdocs(kdocString) val guideClassAnnotation = type.annotations.firstOrNull() { - it.shortName.getShortName() == Guide::class.java.simpleName + it.shortName.getShortName() == "Guide" } val description = getDescriptionFromAnnotations(guideAnnotation, guideClassAnnotation, description, baseKdoc) @@ -183,7 +183,7 @@ public class SchemaSymbolProcessor( name = propertyName, guideAnnotation = property.annotations.firstOrNull() { - it.shortName.getShortName() == Guide::class.java.simpleName + it.shortName.getShortName() == "Guide" }, ) } diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt similarity index 100% rename from firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt rename to firebase-ai/src/main/kotlin/com/google/firebase/ai/annotations/Generable.kt diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt similarity index 100% rename from firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt rename to firebase-ai/src/main/kotlin/com/google/firebase/ai/annotations/Guide.kt From d7ff3d44edd9e679ebd6f675fce6e544cee1d402 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Wed, 5 Nov 2025 12:31:19 -0800 Subject: [PATCH 10/14] fix imports in schema processor and update firebase-ai/api.txt --- .../firebase/ai/ksp/SchemaSymbolProcessor.kt | 2 -- firebase-ai/api.txt | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt index aee193f7302..812777445bd 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -28,8 +28,6 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSVisitorVoid import com.google.devtools.ksp.symbol.Modifier -import com.google.firebase.ai.annotations.Generable -import com.google.firebase.ai.annotations.Guide import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec diff --git a/firebase-ai/api.txt b/firebase-ai/api.txt index b5932bf9b0e..2b184a11b79 100644 --- a/firebase-ai/api.txt +++ b/firebase-ai/api.txt @@ -98,6 +98,28 @@ package com.google.firebase.ai { } +package com.google.firebase.ai.annotations { + + @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Generable { + } + + @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface Guide { + method public abstract String description() default ""; + method public abstract String format() default ""; + method public abstract int maxItems() default -1; + method public abstract double maximum() default -1.0; + method public abstract int minItems() default -1; + method public abstract double minimum() default -1.0; + property public abstract String description; + property public abstract String format; + property public abstract int maxItems; + property public abstract double maximum; + property public abstract int minItems; + property public abstract double minimum; + } + +} + package com.google.firebase.ai.java { public abstract class ChatFutures { From da74532ab21fc528bc9104ff8165522345908ca8 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Wed, 5 Nov 2025 14:08:42 -0800 Subject: [PATCH 11/14] format --- .../com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt index 812777445bd..f30f7a8c5e6 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -123,9 +123,7 @@ public class SchemaSymbolProcessor( val baseKdoc = extractBaseKdoc(kdocString) val propertyDocs = extractPropertyKdocs(kdocString) val guideClassAnnotation = - type.annotations.firstOrNull() { - it.shortName.getShortName() == "Guide" - } + type.annotations.firstOrNull() { it.shortName.getShortName() == "Guide" } val description = getDescriptionFromAnnotations(guideAnnotation, guideClassAnnotation, description, baseKdoc) val minimum = getDoubleFromAnnotation(guideAnnotation, "minimum") @@ -180,9 +178,7 @@ public class SchemaSymbolProcessor( description = propertyDocs[propertyName], name = propertyName, guideAnnotation = - property.annotations.firstOrNull() { - it.shortName.getShortName() == "Guide" - }, + property.annotations.firstOrNull() { it.shortName.getShortName() == "Guide" }, ) } properties.entries.forEach { From d32ec1f0a037ff03c0de9d170047b6d72dede6c4 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Wed, 5 Nov 2025 14:58:51 -0800 Subject: [PATCH 12/14] format --- firebase-ai/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index 21c55237ecf..fa57a850988 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased + - [feature] Added support for server templates via `TemplateGenerativeModel` and `TemplateImagenModel`. (#7503) From 8624f43ec18a77b3e2a1810a21bc50791b3d5c3b Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Thu, 6 Nov 2025 09:32:54 -0800 Subject: [PATCH 13/14] update readme for new artifact name --- firebase-ai-ksp-processor/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-ai-ksp-processor/README.md b/firebase-ai-ksp-processor/README.md index 07cd50d27d2..6460d9ff410 100644 --- a/firebase-ai-ksp-processor/README.md +++ b/firebase-ai-ksp-processor/README.md @@ -7,7 +7,7 @@ plugins { id("com.google.devtools.ksp") } dependencies { - implementation("com.google.firebase:firebase-ai-ksp-processor:1.0.0") - ksp("com.google.firebase:firebase-ai-ksp-processor:1.0.0") + implementation("com.google.firebase:firebase-ai:") + ksp("com.google.firebase:firebase-ai-processor:1.0.0") } ``` From 334f3a3c8bb33defeefed764dada9ee34dd6ec3b Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Tue, 11 Nov 2025 13:45:21 -0800 Subject: [PATCH 14/14] Add support for enum schema --- .../firebase/ai/ksp/SchemaSymbolProcessor.kt | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt index f30f7a8c5e6..8327f88eed1 100644 --- a/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt +++ b/firebase-ai-ksp-processor/src/main/kotlin/com/google/firebase/ai/ksp/SchemaSymbolProcessor.kt @@ -22,6 +22,7 @@ import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration @@ -151,17 +152,33 @@ public class SchemaSymbolProcessor( "kotlin.String" -> { builder.addStatement("Schema.string(").indent() } + "kotlin.collections.List" -> { + val listTypeParam = type.arguments.first().type!!.resolve() + val listParamCodeBlock = + generateCodeBlockForSchema(type = listTypeParam, parentType = type) + builder + .addStatement("Schema.array(") + .indent() + .addStatement("items = ") + .add(listParamCodeBlock) + .addStatement(",") + } else -> { - if (className.canonicalName == "kotlin.collections.List") { - val listTypeParam = type.arguments.first().type!!.resolve() - val listParamCodeBlock = - generateCodeBlockForSchema(type = listTypeParam, parentType = type) + if ((type.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS) { + val enumValues = + (type.declaration as KSClassDeclaration) + .declarations + .filterIsInstance(KSClassDeclaration::class.java) + .map { it.simpleName.asString() } + .toList() builder - .addStatement("Schema.array(") + .addStatement("Schema.enumeration(") + .indent() + .addStatement("values = listOf(") .indent() - .addStatement("items = ") - .add(listParamCodeBlock) - .addStatement(",") + .addStatement(enumValues.joinToString { "\"$it\"" }) + .unindent() + .addStatement("),") } else { builder .addStatement("Schema.obj(")