diff --git a/genkit-tools/cli/config/firebase.index.ts.template b/genkit-tools/cli/config/firebase.index.ts.template deleted file mode 100644 index f8c8217bf..000000000 --- a/genkit-tools/cli/config/firebase.index.ts.template +++ /dev/null @@ -1,64 +0,0 @@ -import * as z from "zod"; - -// Import the Genkit core libraries and plugins. -import {generate} from "@genkit-ai/ai"; -import {genkit} from "@genkit-ai/core"; -$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 - ], - // Log debug output to tbe console. - logLevel: "debug", - // Perform OpenTelemetry instrumentation and enable trace collection. - enableTracingAndMetrics: true, -}); - -// Define a simple flow that prompts an LLM to generate menu suggestions. -export const menuSuggestionFlow = onFlow( - ai, - { - name: "menuSuggestionFlow", - inputSchema: z.string(), - 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 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/genkit-tools/cli/config/main.go.template b/genkit-tools/cli/config/main.go.template deleted file mode 100644 index 2def542f7..000000000 --- a/genkit-tools/cli/config/main.go.template +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "log" - - // Import the Genkit core libraries. - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - $GENKIT_MODEL_IMPORT -) - -func $GENKIT_FUNC_NAME() { - ctx := context.Background() - - $GENKIT_MODEL_INIT - - // Define a simple flow that prompts an LLM to generate menu suggestions. - genkit.DefineFlow("menuSuggestionFlow", func(ctx context.Context, input string) (string, error) { - $GENKIT_MODEL_LOOKUP - if m == nil { - return "", errors.New("menuSuggestionFlow: failed to find model") - } - - // Construct a request and send it to the model API. - resp, err := m.Generate(ctx, - ai.NewGenerateRequest( - &ai.GenerationCommonConfig{Temperature: 1}, - ai.NewUserTextMessage(fmt.Sprintf(`Suggest an item for the menu of a %s themed restaurant`, input))), - nil) - if err != nil { - return "", err - } - - // 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. - text := resp.Text() - return text, nil - }) - - // Initialize Genkit and start a flow server. This call must come last, - // after all of your plug-in configuration and flow definitions. When you - // pass a nil configuration to Init, Genkit starts a local flow server, - // which you can interact with using the developer UI. - if err := genkit.Init(ctx, nil); err != nil { - log.Fatal(err) - } -} diff --git a/genkit-tools/cli/config/nextjs.genkit-tools.config.js.template b/genkit-tools/cli/config/nextjs.genkit-tools.config.js.template deleted file mode 100644 index 0b4456805..000000000 --- a/genkit-tools/cli/config/nextjs.genkit-tools.config.js.template +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - runner: { - mode: 'harness', - files: [$GENKIT_HARNESS_FILES], - }, -}; diff --git a/genkit-tools/cli/config/nextjs.genkit.ts.template b/genkit-tools/cli/config/nextjs.genkit.ts.template deleted file mode 100644 index c1c05d8f8..000000000 --- a/genkit-tools/cli/config/nextjs.genkit.ts.template +++ /dev/null @@ -1,51 +0,0 @@ -'use server' - -import * as z from 'zod'; - -// Import the Genkit core libraries and plugins. -import { generate } from '@genkit-ai/ai'; -import { genkit } from 'genkit'; -$GENKIT_CONFIG_IMPORTS -$GENKIT_MODEL_IMPORT - -const ai = genkit({ - plugins: [ -$GENKIT_CONFIG_PLUGINS - ], - // Log debug output to the console. - logLevel: "debug", - // Perform OpenTelemetry instrumentation and enable trace collection. - enableTracingAndMetrics: true, -}); - -// Define a simple flow that prompts an LLM to generate menu suggestions. -const menuSuggestionFlow = ai.defineFlow( - { - name: 'menuSuggestionFlow', - inputSchema: z.string(), - outputSchema: z.string(), - }, - async (subject) => { - // Construct a request and send it to the model API. - const llmResponse = await generate({ - prompt: `Suggest an item for the menu of a ${subject} themed restaurant`, - model: $GENKIT_MODEL, - 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; - } -); - -export async function callMenuSuggestionFlow() { - // Invoke the flow. The value you pass as the second parameter must conform to - // your flow's input schema. - const flowResponse = await menuSuggestionFlow('banana'); - console.log(flowResponse); -} diff --git a/genkit-tools/cli/config/nodejs.index.ts.template b/genkit-tools/cli/config/nodejs.index.ts.template deleted file mode 100644 index 28388b239..000000000 --- a/genkit-tools/cli/config/nodejs.index.ts.template +++ /dev/null @@ -1,44 +0,0 @@ -import * as z from 'zod'; - -// Import the Genkit core libraries and plugins. -import { generate } from '@genkit-ai/ai'; -import { genkit } from 'genkit'; -$GENKIT_CONFIG_IMPORTS -$GENKIT_MODEL_IMPORT - -const ai = genkit({ - plugins: [ -$GENKIT_CONFIG_PLUGINS - ], - // Log debug output to tbe console. - logLevel: 'debug', - // Perform OpenTelemetry instrumentation and enable trace collection. - enableTracingAndMetrics: true, - flowServer: { - runInEnv: 'prod', - }, -}); - -// Define a simple flow that prompts an LLM to generate menu suggestions. -export const menuSuggestionFlow = ai.defineFlow( - { - name: 'menuSuggestionFlow', - inputSchema: z.string(), - outputSchema: z.string(), - }, - async (subject) => { - // Construct a request and send it to the model API. - const llmResponse = await generate({ - prompt: `Suggest an item for the menu of a ${subject} themed restaurant`, - model: $GENKIT_MODEL, - 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/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index 98f5f3e3b..033064b5a 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -28,7 +28,6 @@ import { evalFlow } from './commands/eval-flow'; import { evalRun } from './commands/eval-run'; import { flowBatchRun } from './commands/flow-batch-run'; import { flowRun } from './commands/flow-run'; -import { init } from './commands/init'; import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; import { uiStart } from './commands/ui-start'; import { uiStop } from './commands/ui-stop'; @@ -48,7 +47,6 @@ const commands: Command[] = [ evalExtractData, evalRun, evalFlow, - init, config, ]; diff --git a/genkit-tools/cli/src/commands/init.ts b/genkit-tools/cli/src/commands/init.ts deleted file mode 100644 index a58d65707..000000000 --- a/genkit-tools/cli/src/commands/init.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * 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 { Runtime } from '@genkit-ai/tools-common/manager'; -import { detectRuntime, logger } from '@genkit-ai/tools-common/utils'; -import { Command } from 'commander'; -import * as inquirer from 'inquirer'; -import { initGo } from './init/init-go'; -import { initNodejs } from './init/init-nodejs'; - -export type Platform = 'firebase' | 'other'; -export type ModelProvider = 'googleai' | 'vertexai' | 'ollama' | 'none'; -export type WriteMode = 'keep' | 'overwrite' | 'merge'; - -export interface InitOptions { - // Deployment platform. - platform: Platform; - // Model provider. - model: ModelProvider; - // Path to local Genkit dist archive. - distArchive: string; - // Non-interactive mode. - nonInteractive: boolean; -} - -/** Supported runtimes for the init command. */ -const supportedRuntimes: Record = { - nodejs: 'Node.js', - go: 'Go (preview)', -}; - -export const init = new Command('init') - .description('initialize a project directory with Genkit') - .option( - '-p, --platform ', - 'Deployment platform (firebase, googlecloud, or other)' - ) - .option( - '-m, --model ', - 'Model provider (googleai, vertexai, ollama, or none)' - ) - .option( - '--non-interactive', - 'Run init in non-interactive mode (experimental)' - ) - .option( - '-d, --dist-archive ', - 'Path to local Genkit dist archive' - ) - .action(async (options: InitOptions) => { - var isNew = false; - var runtime = await detectRuntime(process.cwd()); - if (!runtime) { - logger.info('No runtime was detected in the current directory.'); - const answer = await inquirer.prompt<{ runtime: Runtime }>([ - { - type: 'list', - name: 'runtime', - message: 'Select a runtime to initialize a Genkit project:', - choices: Object.keys(supportedRuntimes).map((runtime) => ({ - name: supportedRuntimes[runtime], - value: runtime, - })), - }, - ]); - runtime = answer.runtime; - isNew = true; - } else if (!supportedRuntimes[runtime]) { - logger.error( - `The runtime could not be detected or is not supported. Supported runtimes: ${Object.keys(supportedRuntimes)}` - ); - process.exit(1); - } - try { - switch (runtime) { - case 'nodejs': - await initNodejs(options, isNew); - break; - case 'go': - await initGo(options, isNew); - break; - } - } catch (err) { - logger.error(err); - process.exit(1); - } - logger.info('Genkit successfully initialized.'); - }); - -/** - * Displays info about the selected model. - * @param model selected model - */ -export function showModelInfo(model: ModelProvider) { - switch (model) { - case 'vertexai': - logger.info( - `Run the following command to enable Vertex AI in your Google Cloud project:\n\n gcloud services enable aiplatform.googleapis.com\n` - ); - break; - case 'ollama': - logger.info( - `If you don't have Ollama already installed and configured, refer to https://developers.google.com/genkit/plugins/ollama\n` - ); - break; - } -} - -/** - * Shows a confirmation prompt. - */ -export async function confirm(args: { - default?: boolean; - message?: string; -}): Promise { - const message = args.message ?? `Do you wish to continue?`; - const answer = await inquirer.prompt({ - type: 'confirm', - name: 'confirm', - message, - default: args.default, - }); - return answer.confirm; -} diff --git a/genkit-tools/cli/src/commands/init/init-go.ts b/genkit-tools/cli/src/commands/init/init-go.ts deleted file mode 100644 index 03d276efe..000000000 --- a/genkit-tools/cli/src/commands/init/init-go.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * 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 { InitEvent, logger, record } from '@genkit-ai/tools-common/utils'; -import { execSync } from 'child_process'; -import fs from 'fs'; -import * as inquirer from 'inquirer'; -import ora from 'ora'; -import path from 'path'; -import { InitOptions, ModelProvider, confirm, showModelInfo } from '../init'; - -interface ModelOption { - // Label for prompt option. - label: string; - // Package name. - package: string; - // Init call. - init: string; - // Model lookup call. - lookup: string; -} - -/** Path to Genkit sample template. */ -const templatePath = '../../../config/main.go.template'; - -/** Model to plugin name. */ -const modelOptions: Record = { - googleai: { - label: 'Google AI', - package: 'github.com/firebase/genkit/go/plugins/googleai', - init: `// Initialize the Google AI plugin. When you pass an empty string for the -\t// apiKey parameter, the Google AI plugin will use the value from the -\t// GOOGLE_GENAI_API_KEY environment variable, which is the recommended -\t// practice. -\tif err := googleai.Init(ctx, nil); err != nil { -\t\tlog.Fatal(err) -\t}`, - lookup: `// The Google AI API provides access to several generative models. Here, -\t\t// we specify gemini-1.5-flash. -\t\tm := googleai.Model("gemini-1.5-flash")`, - }, - vertexai: { - label: 'Google Cloud Vertex AI', - package: 'github.com/firebase/genkit/go/plugins/vertexai', - init: `// Initialize the Vertex AI plugin. When you pass an empty string for the -\t// projectID parameter, the Vertex AI plugin will use the value from the -\t// GCLOUD_PROJECT environment variable. When you pass an empty string for -\t// the location parameter, the plugin uses the default value, us-central1. -\tif err := vertexai.Init(ctx, nil); err != nil { -\t\tlog.Fatal(err) -\t}`, - lookup: `// The Vertex AI API provides access to several generative models. Here, -\t\t// we specify gemini-1.5-flash. -\t\tm := vertexai.Model("gemini-1.5-flash")`, - }, - ollama: { - label: 'Ollama (e.g. Gemma)', - package: 'github.com/firebase/genkit/go/plugins/ollama', - init: `// Initialize the Ollama plugin. -\terr := ollama.Init(ctx, -\t\t// The address of your Ollama API server. This is often a different host -\t\t// from your app backend (which runs Genkit), in order to run Ollama on -\t\t// a GPU-accelerated machine. -\t\t"http://127.0.0.1:11434") -\tif err != nil { -\t\tlog.Fatal(err) -\t} -\t// The models you want to use. These must already be downloaded and -\t// available to the Ollama server. -\tollama.DefineModel(ollama.ModelDefinition{Name: "gemma"}, nil)`, - lookup: `// Ollama provides an interface to many open generative models. Here, -\t\t// we specify Google's Gemma model, which we configured the Ollama -\t\t// plugin to provide, above. -\t\tm := ollama.Model("gemma")`, - }, - none: { - label: 'None', - package: '', - init: 'nil // TODO: Initialize a model.', - lookup: 'nil // TODO: Look up a model.', - }, -}; - -/** Supported platform to plugin name. */ -const platformOptions: Record = { - googlecloud: 'Google Cloud', - other: 'Other platforms', -}; - -/** Packages required to use Genkit. */ -const corePackages = [ - 'github.com/firebase/genkit/go/ai', - 'github.com/firebase/genkit/go/genkit', -]; - -/** - * Initializes a Genkit Go project. - * - * @param options command-line arguments - * @param isNew whether the project directory should be newly created - */ -export async function initGo(options: InitOptions, isNew: boolean) { - let { platform, model } = options; - - // Validate CLI arguments. - const supportedPlatforms = Object.keys(platformOptions); - if (platform && !supportedPlatforms.includes(platform)) { - throw new Error( - `\`${platform}\` is not a supported platform for Go. Supported platforms: ${supportedPlatforms}` - ); - } - const supportedModels = Object.keys(modelOptions) as ModelProvider[]; - if (model && !supportedModels.includes(model)) { - throw new Error( - `\`${model}\` is not a supported model provider for Go. Supported model providers: ${supportedModels}` - ); - } - - // Prompt for left-over arguments. - if (!model) { - const answer = await inquirer.prompt<{ model: ModelProvider }>([ - { - type: 'list', - name: 'model', - message: 'Select a model provider:', - choices: supportedModels.map((model) => ({ - name: modelOptions[model].label, - value: model, - })), - }, - ]); - model = answer.model; - } - - // Compile Go packages list. - const packages = [...corePackages]; - if (modelOptions[model]?.package) { - packages.push(modelOptions[model].package); - } - - // Initialize and configure. - if (isNew) { - const answer = await inquirer.prompt<{ module: string }>([ - { - type: 'input', - name: 'module', - message: - 'Enter the Go module name (e.g. github.com/user/genkit-go-app):', - }, - ]); - try { - execSync(`go mod init ${answer.module}`, { stdio: 'ignore' }); - } catch (err) { - logger.error(`Failed to initialize Go project: ${err}`); - process.exit(1); - } - } - installPackages(packages); - if ( - options.nonInteractive || - (await confirm({ - message: 'Would you like to generate a sample flow?', - default: true, - })) - ) { - await generateSampleFile(model); - } - - showModelInfo(model); - - // Record event. - await record(new InitEvent('go')); -} - -/** - * Installs Go Packages. - */ -function installPackages(packages: string[]) { - const spinner = ora('Installing Go packages').start(); - try { - execSync(`go get ${packages.map((p) => p + '@latest').join(' ')}`, { - stdio: 'ignore', - }); - spinner.succeed('Successfully installed Go packages'); - } catch (err) { - spinner.fail(`Error installing packages: ${err}`); - process.exit(1); - } -} - -/** - * Generates a sample main.go file. - */ -async function generateSampleFile(model: ModelProvider) { - let filename = 'main.go'; - let samplePath = path.join(process.cwd(), filename); - let write = true; - if (fs.existsSync(samplePath)) { - filename = 'genkit.go'; - samplePath = path.join(process.cwd(), filename); - if (fs.existsSync(samplePath)) { - write = await confirm({ - message: `${filename} already exists. Would you like to overwrite it?`, - default: false, - }); - } - } - const spinner = ora('Generating sample file').start(); - try { - const fullPath = path.join(__dirname, templatePath); - let sample = fs.readFileSync(fullPath, 'utf8'); - const modelOption = modelOptions[model]; - sample = sample - .replace( - '$GENKIT_FUNC_NAME', - filename === 'genkit.go' ? 'initGenkit' : 'main' - ) - .replace( - '$GENKIT_MODEL_IMPORT', - modelOption.package - ? `\n\t// Import the ${modelOption.label} plugin.\n\t"${modelOption.package}"` - : '' - ) - .replace('$GENKIT_MODEL_INIT', modelOption.init) - .replace('$GENKIT_MODEL_LOOKUP', modelOption.lookup); - if (write) { - fs.writeFileSync(samplePath, sample, 'utf8'); - spinner.succeed(`Successfully generated sample file (${filename})`); - } else { - spinner.warn('Skipped generating sample file'); - } - } catch (err) { - spinner.fail(`Failed to generate sample file: ${err}`); - process.exit(1); - } -} diff --git a/genkit-tools/cli/src/commands/init/init-nodejs.ts b/genkit-tools/cli/src/commands/init/init-nodejs.ts deleted file mode 100644 index 76a2e8553..000000000 --- a/genkit-tools/cli/src/commands/init/init-nodejs.ts +++ /dev/null @@ -1,625 +0,0 @@ -/** - * 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 { InitEvent, record } from '@genkit-ai/tools-common/utils'; -import { exec } from 'child_process'; -import extract from 'extract-zip'; -import fs from 'fs'; -import * as inquirer from 'inquirer'; -import ora from 'ora'; -import path from 'path'; -import { promisify } from 'util'; -import { - InitOptions, - ModelProvider, - Platform, - WriteMode, - confirm, - showModelInfo, -} from '../init'; - -type SampleTarget = 'firebase' | 'nodejs' | 'nextjs'; - -const execAsync = promisify(exec); - -interface PluginInfo { - // 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?: string; - // Model name as a string reference. - modelStr?: string; -} - -interface PromptOption { - // Label for prompt option. - label: string; - // Plugin name. - plugin?: string; -} - -interface ImportOptions { - // Spacing around brackets in import - spacer: ' ' | ''; - // Single or double quotes for import - quotes: '"' | "'"; -} - -/** Model to plugin name. */ -const modelOptions: Record = { - googleai: { label: 'Google AI', plugin: '@genkit-ai/googleai' }, - vertexai: { - label: 'Google Cloud Vertex AI', - plugin: '@genkit-ai/vertexai', - }, - ollama: { label: 'Ollama (e.g. Gemma)', plugin: 'genkitx-ollama' }, - none: { label: 'None', plugin: undefined }, -}; - -/** Supported platform to plugin name. */ -const platformOptions: Record = { - firebase: { label: 'Firebase', plugin: '@genkit-ai/firebase' }, - other: { label: 'Other platforms', plugin: undefined }, -}; - -/** Plugin name to descriptor. */ -const pluginToInfo: Record = { - '@genkit-ai/firebase': { - imports: 'firebase', - init: ` - // Load the Firebase plugin, which provides integrations with several - // Firebase services. - firebase()`.trimStart(), - }, - '@genkit-ai/google-cloud': { - imports: 'googleCloud', - init: 'googleCloud()', - }, - '@genkit-ai/vertexai': { - imports: 'vertexAI', - modelImportComment: ` -// Import models from the Vertex AI plugin. The Vertex AI API provides access to -// several generative models. Here, we import Gemini 1.5 Flash.`.trimStart(), - init: ` - // Load the Vertex AI plugin. You can optionally specify your project ID - // by passing in a config object; if you don't, the Vertex AI plugin uses - // the value from the GCLOUD_PROJECT environment variable. - vertexAI({ location: 'us-central1' })`.trimStart(), - model: 'gemini15Flash', - }, - 'genkitx-ollama': { - imports: 'ollama', - init: ` - ollama({ - // Ollama provides an interface to many open generative models. Here, - // we specify Google's Gemma model. The models you specify must already be - // downloaded and available to the Ollama server. - models: [{ name: 'gemma' }], - // The address of your Ollama API server. This is often a different host - // from your app backend (which runs Genkit), in order to run Ollama on - // a GPU-accelerated machine. - serverAddress: 'http://127.0.0.1:11434', - })`.trimStart(), - modelStr: "'ollama/gemma'", - }, - '@genkit-ai/googleai': { - imports: 'googleAI', - modelImportComment: ` -// Import models from the Google AI plugin. The Google AI API provides access to -// several generative models. Here, we import Gemini 1.5 Flash.`.trimStart(), - init: ` - // Load the Google AI plugin. You can optionally specify your API key - // by passing in a config object; if you don't, the Google AI plugin uses - // the value from the GOOGLE_GENAI_API_KEY environment variable, which is - // the recommended practice. - googleAI()`.trimStart(), - model: 'gemini15Flash', - }, -}; - -/** Platform to sample flow template paths. */ -const sampleTemplatePaths: Record = { - firebase: '../../../config/firebase.index.ts.template', - nodejs: '../../../config/nodejs.index.ts.template', - nextjs: '../../../config/nextjs.genkit.ts.template', -}; - -const nextjsToolsConfigTemplatePath = - '../../../config/nextjs.genkit-tools.config.js.template'; - -/** External packages required to use Genkit. */ -const externalPackages = ['zod', 'express']; - -/** External dev packages required to use Genkit. */ -const externalDevPackages = ['typescript']; - -/** Internal packages required to use Genkit. */ -const internalPackages = [ - '@genkit-ai/core', - '@genkit-ai/ai', - '@genkit-ai/dotprompt', -]; - -const platformImportOptions: Record = { - firebase: { spacer: '', quotes: '"' }, - other: { spacer: ' ', quotes: "'" }, -}; - -/** - * Initializes a Genkit Node.js project. - * - * @param options command-line arguments - * @param isNew whether the project directory should be newly created - */ -export async function initNodejs(options: InitOptions, isNew: boolean) { - let { platform, model, distArchive } = options; - - // Validate CLI arguments. - const supportedPlatforms = Object.keys(platformOptions) as Platform[]; - if (platform && !supportedPlatforms.includes(platform)) { - throw new Error( - `\`${platform}\` is not a supported platform for Node.js. Supported platforms: ${supportedPlatforms}` - ); - } - const supportedModels = Object.keys(modelOptions) as ModelProvider[]; - if (model && !supportedModels.includes(model)) { - throw new Error( - `\`${model}\` is not a supported model provider for Node.js. Supported model providers: ${supportedModels}` - ); - } - - platform = platform || 'other'; - - let sampleTarget: SampleTarget = 'nodejs'; - if (platform === 'firebase') { - sampleTarget = 'firebase'; - } else { - if ( - isFirebaseProject() && - (await confirm({ - message: - 'Detected a Firebase project. Would you like to configure Genkit for Firebase?', - default: true, - })) - ) { - sampleTarget = 'firebase'; - } else if ( - isNextJsProject() && - (await confirm({ - message: - 'Detected a Next.js project. Would you like to configure Genkit for Next.js?', - default: true, - })) - ) { - sampleTarget = 'nextjs'; - } - } - - if (!model) { - const answer = await inquirer.prompt<{ model: ModelProvider }>([ - { - type: 'list', - name: 'model', - message: 'Select a model provider:', - choices: supportedModels.map((model) => ({ - name: modelOptions[model].label, - value: model, - })), - }, - ]); - model = answer.model; - } - - // Compile plugins list. - const plugins: string[] = []; - if (platformOptions[platform]?.plugin) { - plugins.push(platformOptions[platform].plugin!); - } - if (modelOptions[model]?.plugin) { - plugins.push(modelOptions[model].plugin!); - } - - // Compile NPM packages list. - const packages = [...externalPackages]; - if (!distArchive) { - packages.push(...internalPackages); - packages.push(...plugins); - } - - // Initialize and configure. - if (isNew) { - const spinner = ora('Initializing NPM project').start(); - try { - await execAsync('npm init -y'); - spinner.succeed('Successfully initialized NPM project'); - } catch (err) { - spinner.fail(`Failed to initialize NPM project: ${err}`); - process.exit(1); - } - } - await installNpmPackages(packages, externalDevPackages, distArchive); - if (!fs.existsSync('src')) { - fs.mkdirSync('src'); - } - await updateTsConfig(options.nonInteractive || isNew); - await updatePackageJson(options.nonInteractive || isNew); - if ( - options.nonInteractive || - (await confirm({ - message: 'Would you like to generate a sample flow?', - default: true, - })) - ) { - generateSampleFile( - platform, - sampleTarget, - modelOptions[model].plugin, - plugins - ); - } - generateToolsConfig(sampleTarget); - showModelInfo(model); - - // Record event. - await record(new InitEvent(sampleTarget)); -} - -/** - * Updates tsconfig.json with required flags for Genkit. - */ -async function updateTsConfig(nonInteractive: boolean) { - const tsConfigPath = path.join(process.cwd(), 'tsconfig.json'); - let existingTsConfig = undefined; - if (fs.existsSync(tsConfigPath)) { - existingTsConfig = JSON.parse(fs.readFileSync(tsConfigPath, 'utf-8')); - } - let choice: WriteMode = 'overwrite'; - if (!nonInteractive && existingTsConfig) { - choice = await promptWriteMode( - 'Would you like to update your tsconfig.json with suggested settings?' - ); - } - const tsConfig = { - compileOnSave: true, - include: ['src'], - compilerOptions: { - module: 'commonjs', - noImplicitReturns: true, - outDir: 'lib', - sourceMap: true, - strict: true, - target: 'es2017', - skipLibCheck: true, - esModuleInterop: true, - }, - }; - const spinner = ora('Updating tsconfig.json').start(); - let newTsConfig = {}; - switch (choice) { - case 'overwrite': - newTsConfig = { - ...existingTsConfig, - ...tsConfig, - compilerOptions: { - ...existingTsConfig?.compilerOptions, - ...tsConfig.compilerOptions, - }, - }; - break; - case 'merge': - newTsConfig = { - ...tsConfig, - ...existingTsConfig, - compilerOptions: { - ...tsConfig.compilerOptions, - ...existingTsConfig?.compilerOptions, - }, - }; - break; - case 'keep': - spinner.warn('Skipped updating tsconfig.json'); - return; - } - try { - fs.writeFileSync(tsConfigPath, JSON.stringify(newTsConfig, null, 2)); - spinner.succeed('Successfully updated tsconfig.json'); - } catch (err) { - spinner.fail(`Failed to update tsconfig.json: ${err}`); - process.exit(1); - } -} - -/** - * Installs and saves NPM packages to package.json. - * @param packages List of NPM packages to install. - * @param devPackages List of NPM dev packages to install. - */ -async function installNpmPackages( - packages: string[], - devPackages?: string[], - distArchive?: string -): Promise { - const spinner = ora('Installing NPM packages').start(); - try { - if (packages.length) { - await execAsync(`npm install ${packages.join(' ')} --save`); - } - if (devPackages?.length) { - await execAsync(`npm install ${devPackages.join(' ')} --save-dev`); - } - if (distArchive) { - const distDir = 'genkit-dist'; - const outputPath = path.join(process.cwd(), distDir); - if (!fs.existsSync(distDir)) { - fs.mkdirSync(distDir); - } - await extract(distArchive, { dir: outputPath }); - await execAsync(`npm install ${outputPath}/*.tgz --save`); - } - spinner.succeed('Successfully installed NPM packages'); - } catch (err) { - spinner.fail(`Failed to install NPM packages: ${err}`); - process.exit(1); - } -} - -/** - * Generates a sample index.ts file. - * @param platform Deployment platform. - * @param sampleTarget Sample target. - * @param modelPlugin Model plugin name. - */ -function generateSampleFile( - platform: Platform, - sampleTarget: SampleTarget, - modelPlugin: string | undefined, - configPlugins: string[] -) { - const modelImport = - modelPlugin && pluginToInfo[modelPlugin].model - ? '\n' + - generateImportStatement( - pluginToInfo[modelPlugin].model!, - modelPlugin, - platformImportOptions[platform] - ) + - '\n' - : ''; - const modelImportComment = - modelPlugin && pluginToInfo[modelPlugin].modelImportComment - ? `\n${pluginToInfo[modelPlugin].modelImportComment}` - : ''; - const commentedModelImport = `${modelImportComment}${modelImport}`; - const templatePath = path.join(__dirname, sampleTemplatePaths[sampleTarget]); - let template = fs.readFileSync(templatePath, 'utf8'); - const sample = renderConfig( - configPlugins, - platform, - template - .replace('$GENKIT_MODEL_IMPORT\n', commentedModelImport) - .replace( - '$GENKIT_MODEL', - modelPlugin - ? pluginToInfo[modelPlugin].model || - pluginToInfo[modelPlugin].modelStr || - '' - : "'' /* TODO: Set a model. */" - ) - ); - const spinner = ora('Generating sample file').start(); - try { - let samplePath = 'src/index.ts'; - if (sampleTarget === 'nextjs') { - if (fs.existsSync('src/app')) { - samplePath = 'src/app/genkit.ts'; - } else if (fs.existsSync('app')) { - samplePath = 'app/genkit.ts'; - } else { - throw new Error( - 'Unable to resolve source folder (app or src/app) of you next.js app.' - ); - } - } - fs.writeFileSync(path.join(process.cwd(), samplePath), sample, 'utf8'); - spinner.succeed(`Successfully generated sample file (${samplePath})`); - } catch (err) { - spinner.fail(`Failed to generate sample file: ${err}`); - process.exit(1); - } -} - -/** - * Updates package.json with Genkit-expected fields. - */ -async function updatePackageJson(nonInteractive: boolean) { - const packageJsonPath = path.join(process.cwd(), 'package.json'); - // package.json should exist before reaching this point. - if (!fs.existsSync(packageJsonPath)) { - throw new Error('Failed to find package.json.'); - } - const existingPackageJson = JSON.parse( - fs.readFileSync(packageJsonPath, 'utf8') - ); - const choice = nonInteractive - ? 'overwrite' - : await promptWriteMode( - 'Would you like to update your package.json with suggested settings?' - ); - const packageJson = { - main: 'lib/index.js', - scripts: { - start: 'node lib/index.js', - build: 'tsc', - 'build:watch': 'tsc --watch', - dev: 'export GENKIT_RUNTIME_ID=$(openssl rand -hex 8) && npm start', - }, - }; - const spinner = ora('Updating package.json').start(); - let newPackageJson = {}; - switch (choice) { - case 'overwrite': - newPackageJson = { - ...existingPackageJson, - ...packageJson, - scripts: { - ...existingPackageJson.scripts, - ...packageJson.scripts, - }, - }; - break; - case 'merge': - newPackageJson = { - ...packageJson, - ...existingPackageJson, - // Main will always be overwritten to match tsconfig. - main: packageJson.main, - scripts: { - ...packageJson.scripts, - ...existingPackageJson.scripts, - }, - }; - break; - case 'keep': - spinner.warn('Skipped updating package.json'); - return; - } - try { - fs.writeFileSync(packageJsonPath, JSON.stringify(newPackageJson, null, 2)); - spinner.succeed('Successfully updated package.json'); - } catch (err) { - spinner.fail(`Failed to update package.json: ${err}`); - process.exit(1); - } -} - -function renderConfig( - pluginNames: string[], - platform: Platform, - template: string -): string { - const imports = pluginNames - .map((pluginName) => - generateImportStatement( - pluginToInfo[pluginName].imports, - pluginName, - platformImportOptions[platform] - ) - ) - .join('\n'); - const plugins = - pluginNames - .map((pluginName) => ` ${pluginToInfo[pluginName].init},`) - .join('\n') || ' /* Add your plugins here. */'; - return template - .replace('$GENKIT_CONFIG_IMPORTS', imports) - .replace('$GENKIT_CONFIG_PLUGINS', plugins); -} - -function generateImportStatement( - imports: string, - name: string, - opts: ImportOptions -): string { - return `import {${opts.spacer}${imports}${opts.spacer}} from ${opts.quotes}${name}${opts.quotes};`; -} - -/** - * Generates an appropriate tools config for the given platform. - * @param platform platform - */ -function generateToolsConfig(sampleTarget: SampleTarget) { - if (sampleTarget === 'nextjs') { - const templatePath = path.join(__dirname, nextjsToolsConfigTemplatePath); - let template = fs.readFileSync(templatePath, 'utf8'); - if (fs.existsSync('src/app')) { - template = template.replace('$GENKIT_HARNESS_FILES', `'./src/app/*.ts'`); - } else if (fs.existsSync('app')) { - template = template.replace('$GENKIT_HARNESS_FILES', `'./app/*.ts'`); - } else { - throw new Error( - 'Unable to resolve source folder (app or src/app) of you next.js app.' - ); - } - const spinner = ora('Updating genkit-tools.conf.js').start(); - try { - fs.writeFileSync( - path.join(process.cwd(), 'genkit-tools.conf.js'), - template, - 'utf8' - ); - spinner.succeed('Successfully updated genkit-tools.conf.js'); - } catch (err) { - spinner.fail(`Failed to update genkit-tools.conf.js: ${err}`); - process.exit(1); - } - } -} - -/** - * Detects whether the project directory is a Next.js app. - */ -function isNextJsProject(projectDir: string = process.cwd()): boolean { - const hasNextConfig = fs.existsSync(path.join(projectDir, 'next.config.js')); - let packageJson; - try { - packageJson = JSON.parse( - fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8') - ); - } catch (error) { - return false; - } - const hasNextDependency = - packageJson.dependencies && packageJson.dependencies.next; - return hasNextConfig || hasNextDependency; -} - -/** - * Detects whether the project directory is a Firebase app. - */ -function isFirebaseProject(projectDir: string = process.cwd()): boolean { - const filename = 'firebase.json'; - return ( - fs.existsSync(path.join(projectDir, filename)) || - fs.existsSync(path.join(projectDir, '..', filename)) - ); -} - -/** - * Prompts for what type of write to perform when there is a conflict. - */ -export async function promptWriteMode( - message: string, - defaultOption: WriteMode = 'merge' -): Promise { - const answers = await inquirer.prompt([ - { - type: 'list', - name: 'option', - message, - choices: [ - { name: 'Set if unset', value: 'merge' }, - { name: 'Overwrite', value: 'overwrite' }, - { name: 'Keep unchanged', value: 'keep' }, - ], - default: defaultOption, - }, - ]); - return answers.option; -}