@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
text/typescript
/**
* 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 };
}
}
);
}
});
}