UNPKG

@genkit-ai/firebase

Version:

Genkit AI framework plugin for Firebase including Firestore trace/state store and deployment helpers for Cloud Functions for Firebase.

204 lines (192 loc) 6.5 kB
/** * 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. */ import { getApp, initializeServerApp, type FirebaseApp, type FirebaseOptions, type FirebaseServerApp, } from 'firebase/app'; import { executeMutation, executeQuery, getDataConnect, mutationRef, queryRef, } from 'firebase/data-connect'; import { readFileSync } from 'fs'; import { GenkitError, type Genkit, type JSONSchema7 } from 'genkit'; import { logger } from 'genkit/logging'; import { genkitPlugin, type GenkitPlugin } from 'genkit/plugin'; export interface DataConnectTool { name: string; type: 'query' | 'mutation'; description?: string; parameters: JSONSchema7; } export interface ToolsConfig { connector: string; location: string; service: string; tools: DataConnectTool[]; } export interface DataConnectToolsOptions { /** Provide a name for the plugin. All tools will have this prefix, e.g. `myname/myTool` */ name: string; /** Pass in tool definitions as generated from Data Connect's `llmTools` generator. */ config?: ToolsConfig; /** Path to the file output by the `llmTools` generator. */ configFile: string; /** * Your Firebase client SDK config or a FirebaseApp or a FirebaseServerApp. If not provided * the plugin will attempt to use `context.firebaseApp` for an in-context app or will fall * back the default app. */ firebaseApp?: FirebaseOptions | FirebaseApp; /** * How to handle errors coming from Data Connect tools: * * - `return`: (default) return the error to the model to allow it to decide how to proceed * - `throw`: throw an error, halting execution * * You may also supply a custom error handler. Whatever is returned from the handler will * be returned to the LLM. If you throw an exception from the handler, execution will be * halted with an error. */ onError?: | 'return' | 'throw' | ((tool: DataConnectTool, error: Error) => any | Promise<any>); } export function serverAppFromContext( context: Record<string, any>, config?: FirebaseOptions | FirebaseApp ): FirebaseServerApp | FirebaseApp { if ('firebaseApp' in context) return context.firebaseApp as FirebaseApp | FirebaseServerApp; try { if (!config) config = getApp(); } catch (e) { throw new GenkitError({ status: 'FAILED_PRECONDITION', message: `Must either supply a 'firebaseApp' option or have already initialized a default FirebaseApp when calling Data Connect tools.`, }); } return initializeServerApp(config, { authIdToken: context.auth?.rawToken, appCheckToken: context.app?.rawToken, }); } /** * dataConnectTools connects Genkit to your Data Connect operations by creating tools from * your connector's queries and mutations. This plugin is driven by the generated JSON file * from the `llmTools` option. You can generate this file using the `llmTools` option in * `connector.yaml`, for example: * * ```yaml * connectorId: tools * generate: * llmTools: * outputFile: ../../tools.json * ``` * * Once you have the tools file, you can use this function to register the tools as a Genkit * plugin: * * ```ts * const app = initializeFirebase({...}); * * const ai = genkit({ * plugins: [..., dataConnectTool({ * name: 'myTools', * configFile: 'tools.json', * sdk: app, * })] * }) * ``` * * **IMPORTANT:** This plugin relies on the *client SDKs* for Firebase and does not have * administrative access to Data Connect. To authenticate this plugin requires a `firebaseApp` * instance on `context` with appropriate privileges or the use of the `firebaseContext()` * context provider. * * @param options Configuration options for the plugin. * @returns A Genkit plugin. */ export function dataConnectTools( options: DataConnectToolsOptions ): GenkitPlugin { if (!options.config && !options.configFile) throw new GenkitError({ status: 'INVALID_ARGUMENT', message: 'Must supply `config` or `configFile` when initializing a Data Connect tools plugin.', }); if (!options.config) { try { options.config = JSON.parse( readFileSync(options.configFile, 'utf8') ) as ToolsConfig; } catch (e) { throw new GenkitError({ status: 'INVALID_ARGUMENT', message: `Could not parse Data Connect tools config from ${options.configFile}: ${(e as any).message}`, }); } } return genkitPlugin(options.name, (ai: Genkit) => { const config = options.config!; for (const tool of config.tools) { ai.defineTool( { name: `${options.name}/${tool.name}`, description: tool.description || '', inputJsonSchema: tool.parameters, }, async (input, { context }) => { const serverApp = serverAppFromContext(context, options.firebaseApp); const dc = getDataConnect(serverApp, { connector: config.connector, location: config.location, service: config.service, }); try { if (tool.type === 'query') { const { data } = await executeQuery( queryRef(dc, tool.name, input) ); return data; } const { data } = await executeMutation( mutationRef(dc, tool.name, input) ); logger.debug( `[dataConnectTools] ${tool.name}(${JSON.stringify(input)}) -> ${JSON.stringify(data)}` ); return data; } catch (e) { logger.info('[dataConnectTools] error on tool call:', e); if (options.onError === 'throw') throw e; if (typeof options.onError === 'function') return Promise.resolve(options.onError(tool, e as Error)); if (options.onError === 'return' || !options.onError) return { error: (e as any).message }; } } ); } }); }