UNPKG

@genkit-ai/core

Version:

Genkit AI framework core libraries.

256 lines (232 loc) 7.44 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 * as z from 'zod'; import { Action, ActionMetadata, ActionMetadataSchema, defineAction, } from './action.js'; import { GenkitError } from './error.js'; import { ActionMetadataRecord, ActionType, Registry } from './registry.js'; type DapValue = { [K in ActionType]?: Action<z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny>[]; }; class SimpleCache { private value: DapValue | undefined; private expiresAt: number | undefined; private ttlMillis: number; private dap: DynamicActionProviderAction | undefined; private dapFn: DapFn; private fetchPromise: Promise<DapValue> | null = null; constructor(config: DapConfig, dapFn: DapFn) { this.dapFn = dapFn; this.ttlMillis = !config.cacheConfig?.ttlMillis ? 3 * 1000 : config.cacheConfig?.ttlMillis; } setDap(dap: DynamicActionProviderAction) { this.dap = dap; } setValue(value: DapValue) { const dapId = this.dap?.__action?.name; if (dapId) { Object.entries(value).forEach(([actionType, actionList]) => { actionList?.forEach((action) => { action.__action.key = `/dynamic-action-provider/${dapId}:${actionType}/${action.__action.name}`; }); }); } this.value = value; this.expiresAt = Date.now() + this.ttlMillis; } /** * Gets or fetches the DAP data. * @param skipTrace Don't run the action. i.e. don't create a trace log. * @returns The DAP data */ async getOrFetch(params?: { skipTrace?: boolean }): Promise<DapValue> { const isStale = !this.value || !this.expiresAt || this.ttlMillis < 0 || Date.now() > this.expiresAt; if (!isStale) { return this.value!; } if (!this.fetchPromise) { this.fetchPromise = (async () => { try { if (this.dap && !params?.skipTrace) { await this.dap.run(); // calls setValue } else { this.setValue(await this.dapFn()); } if (!this.value) { throw new Error('value is undefined'); } return this.value; } catch (error) { console.error('Error fetching Dynamic Action Provider value:', error); this.invalidate(); throw error; // Rethrow to reject the fetchPromise } finally { // Allow new fetches after this one completes or fails. this.fetchPromise = null; } })(); } return await this.fetchPromise; } invalidate() { this.value = undefined; } } export interface DynamicRegistry { __cache: SimpleCache; invalidateCache(): void; getAction( actionType: string, actionName: string ): Promise<Action<z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny> | undefined>; listActionMetadata( actionType: string, actionName: string ): Promise<ActionMetadata[]>; getActionMetadataRecord(dapPrefix: string): Promise<ActionMetadataRecord>; } export type DynamicActionProviderAction = Action< z.ZodVoid, z.ZodArray<typeof ActionMetadataSchema>, z.ZodTypeAny > & DynamicRegistry & { __action: { metadata: { type: 'dynamic-action-provider'; }; }; }; export function isDynamicActionProvider( obj: Action<z.ZodTypeAny, z.ZodTypeAny> ): obj is DynamicActionProviderAction { return obj.__action?.metadata?.type == 'dynamic-action-provider'; } export interface DapConfig { name: string; description?: string; cacheConfig?: { // Negative = no caching // Zero or undefined = default (3000 milliseconds) // Positive number = how many milliseconds the cache is valid for ttlMillis: number | undefined; }; metadata?: Record<string, any>; } export type DapFn = () => Promise<DapValue>; export type DapMetadata = { [K in ActionType]?: ActionMetadata[]; }; function transformDapValue(value: DapValue): ActionMetadata[] { return Object.values(value).flatMap( (actions) => actions?.map((a) => a.__action) || [] ); } export function defineDynamicActionProvider( registry: Registry, config: DapConfig | string, fn: DapFn ): DynamicActionProviderAction { let cfg: DapConfig; if (typeof config == 'string') { cfg = { name: config }; } else { cfg = { ...config }; } const cache = new SimpleCache(cfg, fn); const a = defineAction( registry, { ...cfg, inputSchema: z.void(), outputSchema: z.array(ActionMetadataSchema), actionType: 'dynamic-action-provider', metadata: { ...(cfg.metadata || {}), type: 'dynamic-action-provider' }, }, async (_options) => { const dapValue = await fn(); cache.setValue(dapValue); return transformDapValue(dapValue); } ); implementDap(a as DynamicActionProviderAction, cache); return a as DynamicActionProviderAction; } function implementDap(dap: DynamicActionProviderAction, cache: SimpleCache) { cache.setDap(dap); dap.__cache = cache; dap.invalidateCache = () => { dap.__cache.invalidate(); }; dap.getAction = async (actionType: string, actionName: string) => { const result = await dap.__cache.getOrFetch(); if (result[actionType]) { return result[actionType].find((t) => t.__action.name == actionName); } return undefined; }; dap.listActionMetadata = async (actionType: string, actionName: string) => { const result = await dap.__cache.getOrFetch(); if (!result[actionType]) { return []; } // Match everything in the actionType const metadata = result[actionType].map((a) => a.__action); if (actionName == '*') { return metadata; } // Prefix matching if (actionName.endsWith('*')) { const prefix = actionName.slice(0, -1); return metadata.filter((m) => m.name.startsWith(prefix)); } // Single match or empty array return metadata.filter((m) => m.name == actionName); }; // This is called by listResolvableActions which is used by the // reflection API. dap.getActionMetadataRecord = async (dapPrefix: string) => { const dapActions = {} as ActionMetadataRecord; // We want to skip traces so we don't get a new action trace // every time the DevUI requests the list of actions. // This is ok, because the DevUI will show the actions, so // not having them in the trace is fine. const result = await dap.__cache.getOrFetch({ skipTrace: true }); for (const actions of Object.values(result)) { const metadataList = actions.map((a) => a.__action); for (const metadata of metadataList) { if (!metadata.name || !metadata.key) { throw new GenkitError({ status: 'INVALID_ARGUMENT', message: `Invalid metadata when listing dynamic actions from ${dapPrefix} - name required`, }); } dapActions[metadata.key] = metadata; } } return dapActions; }; }