resource.ts•8.21 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 {
action,
Action,
ActionContext,
GenkitError,
isAction,
z,
} from '@genkit-ai/core';
import { Registry } from '@genkit-ai/core/registry';
import uriTemplate from 'uri-templates';
import { PartSchema } from './model-types.js';
/**
* Options for defining a resource.
*/
export interface ResourceOptions {
/**
* Resource name. If not specified, uri or template will be used as name.
*/
name?: string;
/**
* The URI of the resource. Can contain template variables.
*/
uri?: string;
/**
* The URI template (ex. `my://resource/{id}`). See RFC6570 for specification.
*/
template?: string;
/**
* A description of the resource.
*/
description?: string;
/**
* Resource metadata.
*/
metadata?: Record<string, any>;
}
export const ResourceInputSchema = z.object({
uri: z.string(),
});
export type ResourceInput = z.infer<typeof ResourceInputSchema>;
export const ResourceOutputSchema = z.object({
content: z.array(PartSchema),
});
export type ResourceOutput = z.infer<typeof ResourceOutputSchema>;
/**
* A function that returns parts for a given resource.
*/
export type ResourceFn = (
input: ResourceInput,
ctx: ActionContext
) => ResourceOutput | Promise<ResourceOutput>;
/**
* A resource action.
*/
export interface ResourceAction
extends Action<typeof ResourceInputSchema, typeof ResourceOutputSchema> {
matches(input: ResourceInput): boolean;
}
/**
* A reference to a resource in the form of a name or a ResourceAction.
*/
export type ResourceArgument = ResourceAction | string;
export async function resolveResources(
registry: Registry,
resources?: ResourceArgument[]
): Promise<ResourceAction[]> {
if (!resources || resources.length === 0) {
return [];
}
return await Promise.all(
resources.map(async (ref): Promise<ResourceAction> => {
if (typeof ref === 'string') {
return await lookupResourceByName(registry, ref);
} else if (isAction(ref)) {
return ref;
}
throw new Error('Resources must be strings, or actions');
})
);
}
export async function lookupResourceByName(
registry: Registry,
name: string
): Promise<ResourceAction> {
const resource =
(await registry.lookupAction(name)) ||
(await registry.lookupAction(`/resource/${name}`)) ||
(await registry.lookupAction(`/dynamic-action-provider/${name}`));
if (!resource) {
throw new Error(`Resource ${name} not found`);
}
return resource as ResourceAction;
}
/**
* Defines a resource.
*
* @param registry The registry to register the resource with.
* @param opts The resource options.
* @param fn The resource function.
* @returns The resource action.
*/
export function defineResource(
registry: Registry,
opts: ResourceOptions,
fn: ResourceFn
): ResourceAction {
const action = dynamicResource(opts, fn);
action.matches = createMatcher(opts.uri, opts.template);
registry.registerAction('resource', action);
return action;
}
/**
* A dynamic action with a `resource` type. Dynamic resources are detached actions -- not associated with any registry.
*/
export type DynamicResourceAction = ResourceAction & {
__action: {
metadata: {
type: 'resource';
};
};
/** @deprecated no-op, for backwards compatibility only. */
attach(registry: Registry): ResourceAction;
matches(input: ResourceInput): boolean;
};
/**
* Finds a matching resource in the registry. If not found returns undefined.
*/
export async function findMatchingResource(
registry: Registry,
resources: ResourceAction[],
input: ResourceInput
): Promise<ResourceAction | undefined> {
// First look in any resources explicitly listed in the generate request
for (const res of resources) {
if (res.matches(input)) {
return res;
}
}
// Then search the registry
for (const registryKey of Object.keys(
await registry.listResolvableActions()
)) {
// We decided not to look in DAP actions because they might be slow.
// DAP actions with resources will only be found if they are listed in the
// resources section, and then they will be found above.
if (registryKey.startsWith('/resource/')) {
const resource = (await registry.lookupAction(
registryKey
)) as ResourceAction;
if (resource.matches(input)) {
return resource;
}
}
}
return undefined;
}
/** Checks whether provided object is a dynamic resource. */
export function isDynamicResourceAction(t: unknown): t is ResourceAction {
return isAction(t) && !t.__registry;
}
/**
* Defines a dynamic resource. Dynamic resources are just like regular resources but will not be
* registered in the Genkit registry and can be defined dynamically at runtime.
*/
export function resource(
opts: ResourceOptions,
fn: ResourceFn
): ResourceAction {
return dynamicResource(opts, fn);
}
/**
* Defines a dynamic resource. Dynamic resources are just like regular resources but will not be
* registered in the Genkit registry and can be defined dynamically at runtime.
*
* @deprecated renamed to {@link resource}.
*/
export function dynamicResource(
opts: ResourceOptions,
fn: ResourceFn
): DynamicResourceAction {
const uri = opts.uri ?? opts.template;
if (!uri) {
throw new GenkitError({
status: 'INVALID_ARGUMENT',
message: `must specify either url or template options`,
});
}
const matcher = createMatcher(opts.uri, opts.template);
const act = action(
{
actionType: 'resource',
name: opts.name ?? uri,
description: opts.description,
inputSchema: ResourceInputSchema,
outputSchema: ResourceOutputSchema,
metadata: {
resource: {
uri: opts.uri,
template: opts.template,
},
...opts.metadata,
type: 'resource',
dynamic: true,
},
},
async (input, ctx) => {
const templateMatch = matcher(input);
if (!templateMatch) {
throw new GenkitError({
status: 'INVALID_ARGUMENT',
message: `input ${input} did not match template ${uri}`,
});
}
const parts = await fn(input, ctx);
parts.content.map((p) => {
if (!p.metadata) {
p.metadata = {};
}
if (p.metadata?.resource) {
if (!(p.metadata as any).resource.parent) {
(p.metadata as any).resource.parent = {
uri: input.uri,
};
if (opts.template) {
(p.metadata as any).resource.parent.template = opts.template;
}
}
} else {
(p.metadata as any).resource = {
uri: input.uri,
};
if (opts.template) {
(p.metadata as any).resource.template = opts.template;
}
}
return p;
});
return parts;
}
) as DynamicResourceAction;
act.matches = matcher;
act.attach = (_: Registry) => act;
return act;
}
function createMatcher(
uriOpt: string | undefined,
templateOpt: string | undefined
): (input: ResourceInput) => boolean {
// TODO: normalize resource URI during comparisons
// foo://bar?baz=1&qux=2 and foo://bar?qux=2&baz=1 are equivalent URIs but would not match.
if (uriOpt) {
return (input: ResourceInput) => input.uri === uriOpt;
}
if (templateOpt) {
const template = uriTemplate(templateOpt);
return (input: ResourceInput) => template!.fromUri(input.uri) !== undefined;
}
throw new GenkitError({
status: 'INVALID_ARGUMENT',
message: 'must specify either url or template options',
});
}