@genkit-ai/dotprompt
Version:
Genkit AI framework `.prompt` file format and management library.
151 lines (143 loc) • 4.81 kB
text/typescript
/**
* 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 { PromptAction } from '@genkit-ai/ai';
import { GenkitError } from '@genkit-ai/core';
import { logger } from '@genkit-ai/core/logging';
import { lookupAction } from '@genkit-ai/core/registry';
import { existsSync, readdir, readFileSync } from 'fs';
import { basename, join, resolve } from 'path';
import { Dotprompt } from './prompt.js';
import { definePartial } from './template.js';
export function registryDefinitionKey(
name: string,
variant?: string,
ns?: string
) {
// "ns/prompt.variant" where ns and variant are optional
return `${ns ? `${ns}/` : ''}${name}${variant ? `.${variant}` : ''}`;
}
export function registryLookupKey(name: string, variant?: string, ns?: string) {
return `/prompt/${registryDefinitionKey(name, variant, ns)}`;
}
export async function lookupPrompt(
name: string,
variant?: string,
dir: string = './prompts'
): Promise<Dotprompt> {
let registryPrompt =
(await lookupAction(registryLookupKey(name, variant))) ||
(await lookupAction(registryLookupKey(name, variant, 'dotprompt')));
if (registryPrompt) {
return Dotprompt.fromAction(registryPrompt as PromptAction);
} else {
// Handle the case where initialization isn't complete
// or a file was added after the prompt folder was loaded.
return maybeLoadPrompt(dir, name, variant);
}
}
async function maybeLoadPrompt(
dir: string,
name: string,
variant?: string
): Promise<Dotprompt> {
const expectedFileName = `${name}${variant ? `.${variant}` : ''}.prompt`;
const promptFolder = resolve(dir);
const promptExists = existsSync(join(promptFolder, expectedFileName));
if (promptExists) {
return loadPrompt(promptFolder, expectedFileName);
} else {
throw new GenkitError({
source: 'dotprompt',
status: 'NOT_FOUND',
message: `Could not find '${expectedFileName}' in the prompts folder.`,
});
}
}
export async function loadPromptFolder(
dir: string = './prompts'
): Promise<void> {
const promptsPath = resolve(dir);
return new Promise<void>((resolve, reject) => {
if (existsSync(promptsPath)) {
readdir(
promptsPath,
{
withFileTypes: true,
recursive: true,
},
(err, dirEnts) => {
if (err) {
reject(err);
} else {
dirEnts.forEach(async (dirEnt) => {
if (dirEnt.isFile() && dirEnt.name.endsWith('.prompt')) {
if (dirEnt.name.startsWith('_')) {
console.log(dirEnt.name);
const partialName = dirEnt.name.substring(
1,
dirEnt.name.length - 7
);
definePartial(
partialName,
readFileSync(join(dirEnt.path, dirEnt.name), {
encoding: 'utf8',
})
);
logger.debug(
`Registered Dotprompt partial "${partialName}" from "${join(dirEnt.path, dirEnt.name)}"`
);
} else {
// If this prompt is in a subdirectory, we need to include that
// in the namespace to prevent naming conflicts.
let prefix = '';
if (promptsPath !== dirEnt.path) {
prefix = dirEnt.path
.replace(`${promptsPath}/`, '')
.replace(/\//g, '-');
}
loadPrompt(dirEnt.path, dirEnt.name, prefix);
}
}
});
resolve();
}
}
);
} else {
resolve();
}
});
}
export function loadPrompt(
path: string,
filename: string,
prefix = ''
): Dotprompt {
let name = `${prefix ? `${prefix}-` : ''}${basename(filename, '.prompt')}`;
let variant: string | null = null;
if (name.includes('.')) {
const parts = name.split('.');
name = parts[0];
variant = parts[1];
}
const source = readFileSync(join(path, filename), 'utf8');
const prompt = Dotprompt.parse(name, source);
if (variant) {
prompt.variant = variant;
}
prompt.define({ ns: `dotprompt` });
return prompt;
}