From 1f1f308b3f48ce8d57c63022b82156bf49c0fc8b Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 16 Oct 2024 18:40:50 -0400 Subject: [PATCH] [Vertex AI] Add code snippets for use in docs (#13653) --- .../Snippets/FirebaseAppSnippetsUtil.swift | 46 ++++++++ .../Snippets/FunctionCallingSnippets.swift | 109 ++++++++++++++++++ .../Snippets/StructuredOutputSnippets.swift | 95 +++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 FirebaseVertexAI/Tests/Unit/Snippets/FirebaseAppSnippetsUtil.swift create mode 100644 FirebaseVertexAI/Tests/Unit/Snippets/FunctionCallingSnippets.swift create mode 100644 FirebaseVertexAI/Tests/Unit/Snippets/StructuredOutputSnippets.swift diff --git a/FirebaseVertexAI/Tests/Unit/Snippets/FirebaseAppSnippetsUtil.swift b/FirebaseVertexAI/Tests/Unit/Snippets/FirebaseAppSnippetsUtil.swift new file mode 100644 index 00000000000..013b0dcab6d --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/Snippets/FirebaseAppSnippetsUtil.swift @@ -0,0 +1,46 @@ +// Copyright 2024 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. + +import FirebaseCore +import Foundation +import XCTest + +extension FirebaseApp { + static let projectIDEnvVar = "PROJECT_ID" + static let appIDEnvVar = "APP_ID" + static let apiKeyEnvVar = "API_KEY" + + static func configureForSnippets() throws { + let environment = ProcessInfo.processInfo.environment + guard let projectID = environment[projectIDEnvVar] else { + throw XCTSkip("No Firebase Project ID specified in environment variable \(projectIDEnvVar).") + } + guard let appID = environment[appIDEnvVar] else { + throw XCTSkip("No Google App ID specified in environment variable \(appIDEnvVar).") + } + guard let apiKey = environment[apiKeyEnvVar] else { + throw XCTSkip("No API key specified in environment variable \(apiKeyEnvVar).") + } + + let options = FirebaseOptions(googleAppID: appID, gcmSenderID: "") + options.projectID = projectID + options.apiKey = apiKey + + FirebaseApp.configure(options: options) + guard FirebaseApp.isDefaultAppConfigured() else { + XCTFail("Default Firebase app not configured.") + return + } + } +} diff --git a/FirebaseVertexAI/Tests/Unit/Snippets/FunctionCallingSnippets.swift b/FirebaseVertexAI/Tests/Unit/Snippets/FunctionCallingSnippets.swift new file mode 100644 index 00000000000..60d4438cad5 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/Snippets/FunctionCallingSnippets.swift @@ -0,0 +1,109 @@ +// Copyright 2024 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. + +import FirebaseCore +import FirebaseVertexAI +import XCTest + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +final class FunctionCallingSnippets: XCTestCase { + override func setUpWithError() throws { + try FirebaseApp.configureForSnippets() + } + + override func tearDown() async throws { + if let app = FirebaseApp.app() { + await app.delete() + } + } + + func testFunctionCalling() async throws { + // This function calls a hypothetical external API that returns + // a collection of weather information for a given location on a given date. + func fetchWeather(city: String, state: String, date: String) -> JSONObject { + // TODO(developer): Write a standard function that would call an external weather API. + + // For demo purposes, this hypothetical response is hardcoded here in the expected format. + return [ + "temperature": .number(38), + "chancePrecipitation": .string("56%"), + "cloudConditions": .string("partlyCloudy"), + ] + } + + let fetchWeatherTool = FunctionDeclaration( + name: "fetchWeather", + description: "Get the weather conditions for a specific city on a specific date.", + parameters: [ + "location": .object( + properties: [ + "city": .string(description: "The city of the location."), + "state": .string(description: "The US state of the location."), + ], + description: """ + The name of the city and its state for which to get the weather. Only cities in the + USA are supported. + """ + ), + "date": .string( + description: """ + The date for which to get the weather. Date must be in the format: YYYY-MM-DD. + """ + ), + ] + ) + + // Initialize the Vertex AI service and the generative model. + // Use a model that supports function calling, like a Gemini 1.5 model. + let model = VertexAI.vertexAI().generativeModel( + modelName: "gemini-1.5-flash", + // Provide the function declaration to the model. + tools: [.functionDeclarations([fetchWeatherTool])] + ) + + let chat = model.startChat() + let prompt = "What was the weather in Boston on October 17, 2024?" + + // Send the user's question (the prompt) to the model using multi-turn chat. + let response = try await chat.sendMessage(prompt) + + var functionResponses = [FunctionResponsePart]() + + // When the model responds with one or more function calls, invoke the function(s). + for functionCall in response.functionCalls { + if functionCall.name == "fetchWeather" { + // TODO(developer): Handle invalid arguments. + guard case let .object(location) = functionCall.args["location"] else { fatalError() } + guard case let .string(city) = location["city"] else { fatalError() } + guard case let .string(state) = location["state"] else { fatalError() } + guard case let .string(date) = functionCall.args["date"] else { fatalError() } + + functionResponses.append(FunctionResponsePart( + name: functionCall.name, + response: fetchWeather(city: city, state: state, date: date) + )) + } + // TODO(developer): Handle other potential function calls, if any. + } + + // Send the response(s) from the function back to the model so that the model can use it + // to generate its final response. + let finalResponse = try await chat.sendMessage( + [ModelContent(role: "function", parts: functionResponses)] + ) + + // Log the text response. + print(finalResponse.text ?? "No text in response.") + } +} diff --git a/FirebaseVertexAI/Tests/Unit/Snippets/StructuredOutputSnippets.swift b/FirebaseVertexAI/Tests/Unit/Snippets/StructuredOutputSnippets.swift new file mode 100644 index 00000000000..4a1046083a2 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/Snippets/StructuredOutputSnippets.swift @@ -0,0 +1,95 @@ +// Copyright 2024 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. + +import FirebaseCore +import FirebaseVertexAI +import XCTest + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +final class StructuredOutputSnippets: XCTestCase { + override func setUpWithError() throws { + try FirebaseApp.configureForSnippets() + } + + override func tearDown() async throws { + if let app = FirebaseApp.app() { + await app.delete() + } + } + + func testStructuredOutputJSONBasic() async throws { + // Provide a JSON schema object using a standard format. + // Later, pass this schema object into `responseSchema` in the generation config. + let jsonSchema = Schema.object( + properties: [ + "characters": Schema.array( + items: .object( + properties: [ + "name": .string(), + "age": .integer(), + "species": .string(), + "accessory": .enumeration(values: ["hat", "belt", "shoes"]), + ], + optionalProperties: ["accessory"] + ) + ), + ] + ) + + // Initialize the Vertex AI service and the generative model. + // Use a model that supports `responseSchema`, like one of the Gemini 1.5 models. + let model = VertexAI.vertexAI().generativeModel( + modelName: "gemini-1.5-flash", + // In the generation config, set the `responseMimeType` to `application/json` + // and pass the JSON schema object into `responseSchema`. + generationConfig: GenerationConfig( + responseMIMEType: "application/json", + responseSchema: jsonSchema + ) + ) + + let prompt = "For use in a children's card game, generate 10 animal-based characters." + + let response = try await model.generateContent(prompt) + print(response.text ?? "No text in response.") + } + + func testStructuredOutputEnumBasic() async throws { + // Provide an enum schema object using a standard format. + // Later, pass this schema object into `responseSchema` in the generation config. + let enumSchema = Schema.enumeration(values: ["drama", "comedy", "documentary"]) + + // Initialize the Vertex AI service and the generative model. + // Use a model that supports `responseSchema`, like one of the Gemini 1.5 models. + let model = VertexAI.vertexAI().generativeModel( + modelName: "gemini-1.5-flash", + // In the generation config, set the `responseMimeType` to `text/x.enum` + // and pass the enum schema object into `responseSchema`. + generationConfig: GenerationConfig( + responseMIMEType: "text/x.enum", + responseSchema: enumSchema + ) + ) + + let prompt = """ + The film aims to educate and inform viewers about real-life subjects, events, or people. + It offers a factual record of a particular topic by combining interviews, historical footage, + and narration. The primary purpose of a film is to present information and provide insights + into various aspects of reality. + """ + + let response = try await model.generateContent(prompt) + print(response.text ?? "No text in response.") + } +}