diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..ff0c8cceb9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- genkit: support for new unified plugin during init diff --git a/src/init/features/genkit/index.ts b/src/init/features/genkit/index.ts index 591356fc59e..2c3c53ad930 100644 --- a/src/init/features/genkit/index.ts +++ b/src/init/features/genkit/index.ts @@ -39,6 +39,7 @@ import { interface GenkitInfo { genkitVersion: string; cliVersion: string; + genaiVersion: string; vertexVersion: string; googleAiVersion: string; templateVersion: string; @@ -47,7 +48,7 @@ interface GenkitInfo { // This is the next breaking change version past the latest template. const UNKNOWN_VERSION_TOO_HIGH = "2.0.0"; -const MIN_VERSION = "0.6.0"; +const MIN_VERSION = "1.0.0-rc.1"; // This is the latest template. It is the default. const LATEST_TEMPLATE = "1.0.0"; @@ -92,6 +93,7 @@ async function getGenkitInfo(): Promise { const genkitVersion = await getPackageVersion("genkit", "GENKIT_DEV_VERSION"); const cliVersion = await getPackageVersion("genkit-cli", "GENKIT_CLI_DEV_VERSION"); + const genaiVersion = await getPackageVersion("@genkit-ai/google-genai", "GENKIT_GENAI_VERSION"); const vertexVersion = await getPackageVersion("@genkit-ai/vertexai", "GENKIT_VERTEX_VERSION"); const googleAiVersion = await getPackageVersion("@genkit-ai/googleai", "GENKIT_GOOGLEAI_VERSION"); @@ -110,11 +112,11 @@ async function getGenkitInfo(): Promise { if (!continueInstall) { stopInstall = true; } - } else if (semver.gte(genkitVersion, "1.0.0-rc.1")) { - // 1.0.0-rc.1 < 1.0.0 - templateVersion = "1.0.0"; + } else if (semver.gte(genkitVersion, "1.17.0") && semver.gte(genaiVersion, "0.0.2-rc.1")) { + // Unified plugin template + templateVersion = "1.17.0"; } else if (semver.gte(genkitVersion, MIN_VERSION)) { - templateVersion = "0.9.0"; + templateVersion = "1.0.0"; } else { throw new FirebaseError( `The requested version of Genkit (${genkitVersion}) is no ` + @@ -127,6 +129,7 @@ async function getGenkitInfo(): Promise { cliVersion, vertexVersion, googleAiVersion, + genaiVersion, templateVersion, stopInstall, }; @@ -241,21 +244,23 @@ export async function ensureVertexApiEnabled(options: Options): Promise { } interface PluginInfo { + // The name of the plugin + plugin: string; // Imported items from `name` (can be comma list). imports: string; // Comment for 'the model import line. modelImportComment?: string; // Initializer call. init: string; - // Model name as an imported reference. + // Model definition model?: string; - // Model name as a string reference. - modelStr?: string; } -interface PromptOption { +interface ModelOption { // Label for prompt option. label: string; + // Provider (e.g. googleAI, vertexAI) + provider?: string; // Plugin name. plugin?: string; // Package including version @@ -263,33 +268,81 @@ interface PromptOption { } /** Model to plugin name. */ -function getModelOptions(genkitInfo: GenkitInfo): Record { - const modelOptions: Record = { - vertexai: { - label: "Google Cloud Vertex AI", - plugin: "@genkit-ai/vertexai", - package: `@genkit-ai/vertexai@${genkitInfo.vertexVersion}`, - }, - googleai: { - label: "Google AI", - plugin: "@genkit-ai/googleai", - package: `@genkit-ai/googleai@${genkitInfo.googleAiVersion}`, - }, - none: { label: "None", plugin: undefined, package: undefined }, - }; +function getModelOptions(genkitInfo: GenkitInfo): Record { + let modelOptions: Record; + if (semver.gte(genkitInfo.templateVersion, "1.17.0")) { + modelOptions = { + vertexai: { + label: "Google Cloud Vertex AI", + provider: "vertexai", + plugin: "@genkit-ai/google-genai", + package: `@genkit-ai/google-genai@${genkitInfo.genaiVersion}`, + }, + googleai: { + label: "Google AI", + provider: "googleai", + plugin: "@genkit-ai/google-genai", + package: `@genkit-ai/google-genai@${genkitInfo.genaiVersion}`, + }, + none: { label: "None" }, + }; + } else { + modelOptions = { + vertexai: { + label: "Google Cloud Vertex AI", + plugin: "@genkit-ai/vertexai", + package: `@genkit-ai/vertexai@${genkitInfo.vertexVersion}`, + }, + googleai: { + label: "Google AI", + plugin: "@genkit-ai/googleai", + package: `@genkit-ai/googleai@${genkitInfo.googleAiVersion}`, + }, + none: { label: "None" }, + }; + } + return modelOptions; } /** Plugin name to descriptor. */ const pluginToInfo: Record = { "@genkit-ai/firebase": { + plugin: "@genkit-ai/firebase", imports: "firebase", init: ` // Load the Firebase plugin, which provides integrations with several // Firebase services. firebase()`.trimStart(), }, + "@genkit-ai/google-genai(vertexai)": { + plugin: "@genkit-ai/google-genai", + imports: "vertexAI", + modelImportComment: ` +// Import vertexAI provider from the unified plugin. The Vertex AI API provides +// access to many models.`, + init: ` // Load the VertexAI provider. You can optionally specify your location + // and projectID by passing in a config object; if you don't, the provider + // uses the value from environment variables like GCLOUD_PROJECT and GCLOUD_LOCATION. + // If you want to use Vertex Express Mode, you can specify apiKey instead. + vertexAI({location: "global"})`, + model: 'vertexAI.model("gemini-2.5-flash")', + }, + "@genkit-ai/google-genai(googleai)": { + plugin: "@genkit-ai/google-genai", + imports: "googleAI", + modelImportComment: ` +// Import googleAI provider from the unified plugin. The Gemini Developer API +// provides access to several generative models.`, + init: ` // Load the GoogleAI provider. You can optionally specify your API key by + // passing in a config object; if you don't, the provider uses the value + // from the GOOGLE_GENAI_API_KEY environment variable, which is the + // recommended practice. + googleAI()`, + model: 'googleAI.model("gemini-2.5-flash")', + }, "@genkit-ai/vertexai": { + plugin: "@genkit-ai/vertexai", imports: "vertexAI", modelImportComment: ` // Import models from the Vertex AI plugin. The Vertex AI API provides access to @@ -302,6 +355,7 @@ const pluginToInfo: Record = { model: "gemini20Flash", }, "@genkit-ai/googleai": { + plugin: "@genkit-ai/googleai", imports: "googleAI", modelImportComment: ` // Import models from the Google AI plugin. The Google AI API provides access to @@ -316,6 +370,20 @@ const pluginToInfo: Record = { }, }; +function getPluginInfo(option?: ModelOption): PluginInfo { + if (option?.provider && option.plugin) { + return pluginToInfo[`${option.plugin}(${option.provider})`]; + } + if (option?.plugin) { + return pluginToInfo[option.plugin]; + } + return { + plugin: "", + imports: "", + init: "", + }; +} + /** Basic packages required to use Genkit. */ function getBasePackages(genkitVersion: string): string[] { const basePackages = ["express", `genkit@${genkitVersion}`]; @@ -352,13 +420,8 @@ export async function genkitSetup( } // Compile plugins list. - const plugins: string[] = []; const pluginPackages: string[] = []; pluginPackages.push(`@genkit-ai/firebase@${genkitInfo.genkitVersion}`); - - if (modelOptions[model]?.plugin) { - plugins.push(modelOptions[model].plugin || ""); - } if (modelOptions[model]?.package) { pluginPackages.push(modelOptions[model].package || ""); } @@ -393,8 +456,7 @@ export async function genkitSetup( })); generateSampleFile( - modelOptions[model].plugin, - plugins, + modelOptions[model], projectDir, genkitInfo.templateVersion, enableTelemetry, @@ -515,25 +577,25 @@ async function installNpmPackages( /** * Generates a sample index.ts file. - * @param modelPlugin Model plugin name. - * @param configPlugins config plugins. + * @param modelOption Information about the model/plugin + * @param projectDir Where to put the sample + * @param templateVersion Which template the use + * @param enableTelemetry If telemetry is enabled or not. */ function generateSampleFile( - modelPlugin: string | undefined, - configPlugins: string[], + modelOption: ModelOption | undefined, projectDir: string, templateVersion: string, enableTelemetry: boolean, ): void { let modelImport = ""; - if (modelPlugin && pluginToInfo[modelPlugin].model) { - const modelInfo = pluginToInfo[modelPlugin].model || ""; - modelImport = "\n" + generateImportStatement(modelInfo, modelPlugin) + "\n"; + const pluginInfo = getPluginInfo(modelOption); + if (pluginInfo.imports) { + modelImport = "\n" + generateImportStatement(pluginInfo) + "\n"; } let modelImportComment = ""; - if (modelPlugin && pluginToInfo[modelPlugin].modelImportComment) { - const comment = pluginToInfo[modelPlugin].modelImportComment || ""; - modelImportComment = `\n${comment}`; + if (pluginInfo.modelImportComment) { + modelImportComment = `\n${pluginInfo.modelImportComment}`; } const commentedModelImport = `${modelImportComment}${modelImport}`; const templatePath = path.join( @@ -542,15 +604,10 @@ function generateSampleFile( ); const template = fs.readFileSync(templatePath, "utf8"); const sample = renderConfig( - configPlugins, + pluginInfo, template .replace("$GENKIT_MODEL_IMPORT\n", commentedModelImport) - .replace( - "$GENKIT_MODEL", - modelPlugin - ? pluginToInfo[modelPlugin].model || pluginToInfo[modelPlugin].modelStr || "" - : "'' /* TODO: Set a model. */", - ), + .replace("$GENKIT_MODEL", pluginInfo.model ?? "'' /* TODO: Set a model. */"), enableTelemetry, ); logLabeledBullet("genkit", "Generating sample file"); @@ -640,21 +697,19 @@ async function updatePackageJson(nonInteractive: boolean, projectDir: string): P } } -function renderConfig(pluginNames: string[], template: string, enableTelemetry: boolean): string { - const imports = pluginNames - .map((pluginName) => generateImportStatement(pluginToInfo[pluginName].imports, pluginName)) - .join("\n"); - const plugins = - pluginNames.map((pluginName) => ` ${pluginToInfo[pluginName].init},`).join("\n") || - " /* Add your plugins here. */"; +function renderConfig(pluginInfo: PluginInfo, template: string, enableTelemetry: boolean): string { + const plugins = pluginInfo.init || " /* Add your plugins here. */"; return template - .replace("$GENKIT_CONFIG_IMPORTS", imports) + .replace("$GENKIT_CONFIG_IMPORTS", generateImportStatement(pluginInfo)) .replace("$GENKIT_CONFIG_PLUGINS", plugins) .replaceAll("$TELEMETRY_COMMENT", enableTelemetry ? "" : "// "); } -function generateImportStatement(imports: string, name: string): string { - return `import {${imports}} from "${name}";`; +function generateImportStatement(pluginInfo: PluginInfo): string { + if (pluginInfo.imports && pluginInfo.plugin) { + return `import {${pluginInfo.imports}} from "${pluginInfo.plugin}";`; + } + return ""; } /** diff --git a/templates/genkit/firebase.0.9.0.template b/templates/genkit/firebase.0.9.0.template deleted file mode 100644 index 0ce0f1a7a57..00000000000 --- a/templates/genkit/firebase.0.9.0.template +++ /dev/null @@ -1,57 +0,0 @@ -// Import the Genkit core libraries and plugins. -import {genkit, z} from "genkit"; -$GENKIT_CONFIG_IMPORTS -$GENKIT_MODEL_IMPORT - -// From the Firebase plugin, import the functions needed to deploy flows using -// Cloud Functions. -import {firebaseAuth} from "@genkit-ai/firebase/auth"; -import {onFlow} from "@genkit-ai/firebase/functions"; - -const ai = genkit({ - plugins: [ -$GENKIT_CONFIG_PLUGINS - ], -}); - -// Define a simple flow that prompts an LLM to generate menu suggestions. -export const menuSuggestionFlow = onFlow( - ai, - { - name: "menuSuggestionFlow", - inputSchema: z.string().describe("A restaurant theme").default("seafood"), - outputSchema: z.string(), - authPolicy: firebaseAuth((user) => { - // By default, the firebaseAuth policy requires that all requests have an - // `Authorization: Bearer` header containing the user's Firebase - // Authentication ID token. All other requests are rejected with error - // 403. If your app client uses the Cloud Functions for Firebase callable - // functions feature, the library automatically attaches this header to - // requests. - - // You should also set additional policy requirements as appropriate for - // your app. For example: - // if (!user.email_verified) { - // throw new Error("Verified email required to run flow"); - // } - }), - }, - async (subject) => { - // Construct a request and send it to the model API. - const prompt = - `Suggest an item for the menu of a ${subject} themed restaurant`; - const llmResponse = await ai.generate({ - model: $GENKIT_MODEL, - prompt: prompt, - config: { - temperature: 1, - }, - }); - - // Handle the response from the model API. In this sample, we just - // convert it to a string, but more complicated flows might coerce the - // response into structured output or chain the response into another - // LLM call, etc. - return llmResponse.text; - } -); diff --git a/templates/genkit/firebase.1.17.0.template b/templates/genkit/firebase.1.17.0.template new file mode 100644 index 00000000000..7f24f182c9c --- /dev/null +++ b/templates/genkit/firebase.1.17.0.template @@ -0,0 +1,72 @@ +import {genkit, z} from "genkit"; +$GENKIT_CONFIG_IMPORTS + +// Cloud Functions for Firebase supports Genkit natively. The onCallGenkit function creates a callable +// function from a Genkit action. It automatically implements streaming if your flow does. +// The https library also has other utility methods such as hasClaim, which verifies that +// a caller's token has a specific claim (optionally matching a specific value) +import { onCallGenkit, hasClaim } from "firebase-functions/https"; + +// Gemini Developer API models and Vertex Express Mode models depend on an API key. +// API keys should be stored in Cloud Secret Manager so that access to these +// sensitive values can be controlled. defineSecret does this for you automatically. +// If you are using Google Developer API (googleAI) you can get an API key at https://aistudio.google.com/app/apikey +// If you are using Vertex Express Mode (vertexAI with apiKey) you can get an API key +// from the Vertex AI Studio Express Mode setup. +import { defineSecret } from "firebase-functions/params"; +const apiKey = defineSecret("GOOGLE_GENAI_API_KEY"); + +// The Firebase telemetry plugin exports a combination of metrics, traces, and logs to Google Cloud +// Observability. See https://firebase.google.com/docs/genkit/observability/telemetry-collection. +$TELEMETRY_COMMENTimport {enableFirebaseTelemetry} from "@genkit-ai/firebase"; +$TELEMETRY_COMMENTenableFirebaseTelemetry(); + +const ai = genkit({ + plugins: [ +$GENKIT_CONFIG_PLUGINS + ], +}); + +// Define a simple flow that prompts an LLM to generate menu suggestions. +const menuSuggestionFlow = ai.defineFlow({ + name: "menuSuggestionFlow", + inputSchema: z.string().describe("A restaurant theme").default("seafood"), + outputSchema: z.string(), + streamSchema: z.string(), + }, async (subject, { sendChunk }) => { + // Construct a request and send it to the model API. + const prompt = + `Suggest an item for the menu of a ${subject} themed restaurant`; + const { response, stream } = ai.generateStream({ + model: $GENKIT_MODEL, + prompt: prompt, + config: { + temperature: 1, + }, + }); + + for await (const chunk of stream) { + sendChunk(chunk.text); + } + + // Handle the response from the model API. In this sample, we just + // convert it to a string, but more complicated flows might coerce the + // response into structured output or chain the response into another + // LLM call, etc. + return (await response).text; + } +); + +export const menuSuggestion = onCallGenkit({ + // Uncomment to enable AppCheck. This can reduce costs by ensuring only your Verified + // app users can use your API. Read more at https://firebase.google.com/docs/app-check/cloud-functions + // enforceAppCheck: true, + + // authPolicy can be any callback that accepts an AuthData (a uid and tokens dictionary) and the + // request data. The isSignedIn() and hasClaim() helpers can be used to simplify. The following + // will require the user to have the email_verified claim, for example. + // authPolicy: hasClaim("email_verified"), + + // Grant access to the API key to this function: + secrets: [apiKey], +}, menuSuggestionFlow);