Skip to content

Commit

Permalink
Lifting definePrompt to genkit core (#11)
Browse files Browse the repository at this point in the history
- Moving definePrompt into ai package
- Reimagining dotprompt to follow the new contract
- Adding renderPrompt veneer to facilitate using prompts in code
- Updating tests and samples to use defineDotprompt.
- Adding new docs for prompts and updating dotprompt
  • Loading branch information
maxl0rd authored May 3, 2024
1 parent 59a38e7 commit 6a66966
Show file tree
Hide file tree
Showing 20 changed files with 422 additions and 319 deletions.
11 changes: 3 additions & 8 deletions docs/dotprompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
Firebase Genkit provides the Dotprompt library and text format to help you write
and organize your generative AI prompts.

Prompt manipulation is the primary way that you, as an app developer, influence
the output of generative AI models. For example, when using LLMs, you can craft
prompts that influence the tone, format, length, and other characteristics of
the models’ responses.

Dotprompt is designed around the premise that _prompts are code_. You write and
maintain your prompts in specially-formatted files called dotprompt files, track
changes to them using the same version control system that you use for your
Expand Down Expand Up @@ -282,15 +277,15 @@ are a few other ways to load and define prompts:

- `loadPromptFile`: Load a prompt from a file in the prompt directory.
- `loadPromptUrl`: Load a prompt from a URL.
- `definePrompt`: Define a prompt in code.
- `defineDotprompt`: Define a prompt in code.

Examples:

```ts
import {
loadPromptFile,
loadPromptUrl,
definePrompt,
defineDotprompt,
} from '@genkit-ai/dotprompt';
import { z } from 'zod';
Expand All @@ -301,7 +296,7 @@ const myPrompt = await loadPromptFile('./path/to/my_prompt.prompt');
const myPrompt = await loadPromptUrl('https://example.com/my_prompt.prompt');
// Define a prompt in code
const myPrompt = definePrompt(
const myPrompt = defineDotprompt(
{
model: 'vertexai/gemini-1.0-pro',
input: {
Expand Down
106 changes: 106 additions & 0 deletions docs/prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Prompts

Prompt manipulation is the primary way that you, as an app developer, influence
the output of generative AI models. For example, when using LLMs, you can craft
prompts that influence the tone, format, length, and other characteristics of
the models’ responses.

Genkit is designed around the premise that _prompts are code_. You write and
maintain your prompts in source files, track changes to them using the same version
control system that you use for your code, and you deploy them along with the code
that calls your generative AI models.

Most developers will find that the included [Dotprompt](./dotprompt.md) library
meets their needs for working with prompts in Genkit. However, alternative
approaches are also supported by working with prompts directly.

## Defining prompts

Genkit's `generate()` helper function accepts string prompts, and you can
call models this way for straight-forward use cases.

```ts
import { generate } from '@genkit-ai/ai';

generate({
model: 'googleai/gemini-pro',
prompt: 'You are a helpful AI assistant named Walt.',
});
```

In most cases, you will need to include some customer provided inputs in your prompt.
You could define a function to render them like this.

```ts
function helloPrompt(name: string) {
return `You are a helpful AI assistant named Walt. Say hello to ${name}.`;
}

generate({
model: 'googleai/gemini-pro',
prompt: helloPrompt('Fred'),
});
```

One shortcoming of defining prompts in your code is that testing requires executing
them as part of a flow. To faciliate more rapid iteration, Genkit provides a facility
to define your prompts and run them in the Developer UI.

Use the `definePrompt` function to register your prompts with Genkit.

```ts
import { definePrompt } from '@genkit-ai/ai';
import z from 'zod';

export const helloPrompt = definePrompt(
{
name: 'helloPrompt',
inputSchema: z.object({ name: z.string() }),
},
async (input) => {
const promptText = `You are a helpful AI assistant named Walt.
Say hello to ${input.name}.`;

return {
messages: [{ role: 'user', content: [{ text: promptText }] }],
config: { temperature: 0.3 }
});
}
);
```

A prompt action defines a function that returns a `GenerateRequest` object
which can be used with any model. Optionally, you can also define an input schema
for the prompt, which is analagous to the input schema for a flow.
Prompts can also define any of the common model configuration options, such as
temperature or number of output tokens.

You can use this prompt in your code with the `renderPrompt()` helper function.
Provide the input variables expected by the prompt, and the model to call.

```javascript
import { generate, render } from '@genkit-ai/ai';

generate(
renderPrompt({
prompt: helloPrompt,
input: { name: 'Fred' },
model: 'googleai/gemini-pro',
})
);
```

In the Genkit Developer UI, you can run any prompt you have defined in this way.
This allows you to experiment with individual prompts outside of the scope of
the flows in which they might be used.

## Dotprompt

Genkit includes the [Dotprompt](./dotprompt.md) library which adds additional
functionality to prompts.

- Loading prompts from `.prompt` source files
- Handlebars-based templates
- Support for multi-turn prompt templates and multimedia content
- Concise input and output schema definitions
- Fluent usage with `generate()`
2 changes: 2 additions & 0 deletions js/ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export {
Message,
generate,
generateStream,
toGenerateRequest,
} from './generate.js';
export { PromptAction, definePrompt, renderPrompt } from './prompt.js';
export {
IndexerAction,
IndexerInfo,
Expand Down
102 changes: 102 additions & 0 deletions js/ai/src/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* 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 { Action, action, JSONSchema7 } from '@genkit-ai/core';
import { lookupAction, registerAction } from '@genkit-ai/core/registry';
import { setCustomMetadataAttributes } from '@genkit-ai/core/tracing';
import z from 'zod';
import { GenerateOptions } from './generate';
import { GenerateRequest, GenerateRequestSchema, ModelArgument } from './model';

export type PromptFn<I extends z.ZodTypeAny = z.ZodTypeAny> = (
input: z.infer<I>
) => Promise<GenerateRequest>;

export type PromptAction<I extends z.ZodTypeAny = z.ZodTypeAny> = Action<
I,
typeof GenerateRequestSchema
> & {
__action: {
metadata: {
type: 'prompt';
};
};
};

export function definePrompt<I extends z.ZodTypeAny>(
{
name,
description,
inputSchema,
inputJsonSchema,
metadata,
}: {
name: string;
description?: string;
inputSchema?: I;
inputJsonSchema?: JSONSchema7;
metadata?: Record<string, any>;
},
fn: PromptFn<I>
): PromptAction<I> {
const a = action(
{
name,
description,
inputSchema,
inputJsonSchema,
metadata: { ...(metadata || { prompt: {} }), type: 'prompt' },
},
(i: I): Promise<GenerateRequest> => {
setCustomMetadataAttributes({ subtype: 'prompt' });
return fn(i);
}
);
registerAction('prompt', name, a);
return a as PromptAction<I>;
}

/**
* A veneer for rendering a prompt action to GenerateOptions.
*/

export type PromptArgument<I extends z.ZodTypeAny = z.ZodTypeAny> =
| string
| PromptAction<I>;

export async function renderPrompt<
I extends z.ZodTypeAny = z.ZodTypeAny,
CustomOptions extends z.ZodTypeAny = z.ZodTypeAny,
>(params: {
prompt: PromptArgument<I>;
input: z.infer<I>;
model: ModelArgument<CustomOptions>;
config?: z.infer<CustomOptions>;
}): Promise<GenerateOptions> {
let prompt: PromptAction<I>;
if (typeof params.prompt === 'string') {
prompt = await lookupAction(`/prompt/${params.prompt}`);
} else {
prompt = params.prompt as PromptAction<I>;
}
const rendered = await prompt(params.input);
return {
model: params.model,
config: { ...(rendered.config || {}), ...params.config },
history: rendered.messages.slice(0, rendered.messages.length - 1),
prompt: rendered.messages[rendered.messages.length - 1].content,
};
}
34 changes: 8 additions & 26 deletions js/dotprompt/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,13 @@
import { readFileSync } from 'fs';
import { basename } from 'path';

import z from 'zod';

import { registerAction } from '@genkit-ai/core/registry';

import { PromptMetadata } from './metadata.js';
import { Prompt, PromptAction, PromptGenerateOptions } from './prompt.js';
import { defineDotprompt, Dotprompt } from './prompt.js';
import { lookupPrompt } from './registry.js';

export { Prompt, PromptAction, PromptGenerateOptions };
export { defineDotprompt, Dotprompt };

export function loadPromptFile(path: string): Prompt {
return Prompt.parse(
export function loadPromptFile(path: string): Dotprompt {
return Dotprompt.parse(
basename(path).split('.')[0],
readFileSync(path, 'utf-8')
);
Expand All @@ -37,29 +32,16 @@ export function loadPromptFile(path: string): Prompt {
export async function loadPromptUrl(
name: string,
url: string
): Promise<Prompt> {
): Promise<Dotprompt> {
const fetch = (await import('node-fetch')).default;
const response = await fetch(url);
const text = await response.text();
return Prompt.parse(name, text);
return Dotprompt.parse(name, text);
}

export async function prompt<Variables = unknown>(
name: string,
options?: { variant?: string }
): Promise<Prompt<Variables>> {
return (await lookupPrompt(name, options?.variant)) as Prompt<Variables>;
}

export function definePrompt<V extends z.ZodTypeAny = z.ZodTypeAny>(
options: PromptMetadata<V>,
template: string
): Prompt<z.infer<V>> {
const prompt = new Prompt(options, template);
registerAction(
'prompt',
`${prompt.name}${prompt.variant ? `.${prompt.variant}` : ''}`,
prompt.action()
);
return prompt;
): Promise<Dotprompt<Variables>> {
return (await lookupPrompt(name, options?.variant)) as Dotprompt<Variables>;
}
Loading

0 comments on commit 6a66966

Please sign in to comment.