Skip to content

Commit eeb9038

Browse files
authored
feat(js): added id and metadata to executable prompts (#3084)
1 parent 92fe6ba commit eeb9038

File tree

6 files changed

+103
-22
lines changed

6 files changed

+103
-22
lines changed

js/ai/src/prompt.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ export interface ExecutablePrompt<
147147
O extends z.ZodTypeAny = z.ZodTypeAny,
148148
CustomOptions extends z.ZodTypeAny = z.ZodTypeAny,
149149
> {
150+
/** Prompt reference. */
151+
ref: { name: string; metadata?: Record<string, any> };
152+
150153
/**
151154
* Generates a response by rendering the prompt template with given user input and then calling the model.
152155
*
@@ -234,7 +237,8 @@ export function definePrompt<
234237
return definePromptAsync(
235238
registry,
236239
`${options.name}${options.variant ? `.${options.variant}` : ''}`,
237-
Promise.resolve(options)
240+
Promise.resolve(options),
241+
options.metadata
238242
);
239243
}
240244

@@ -245,7 +249,8 @@ function definePromptAsync<
245249
>(
246250
registry: Registry,
247251
name: string,
248-
optionsPromise: PromiseLike<PromptConfig<I, O, CustomOptions>>
252+
optionsPromise: PromiseLike<PromptConfig<I, O, CustomOptions>>,
253+
metadata?: Record<string, any>
249254
): ExecutablePrompt<z.infer<I>, O, CustomOptions> {
250255
const promptCache = {} as PromptCache;
251256

@@ -399,11 +404,13 @@ function definePromptAsync<
399404
}
400405
) as Promise<ExecutablePromptAction<I>>;
401406

402-
const executablePrompt = wrapInExecutablePrompt(
407+
const executablePrompt = wrapInExecutablePrompt({
403408
registry,
409+
name,
404410
renderOptionsFn,
405-
rendererAction
406-
);
411+
rendererAction,
412+
metadata,
413+
});
407414

408415
return executablePrompt;
409416
}
@@ -436,57 +443,64 @@ function wrapInExecutablePrompt<
436443
I extends z.ZodTypeAny = z.ZodTypeAny,
437444
O extends z.ZodTypeAny = z.ZodTypeAny,
438445
CustomOptions extends z.ZodTypeAny = z.ZodTypeAny,
439-
>(
440-
registry: Registry,
446+
>(wrapOpts: {
447+
registry: Registry;
448+
name: string;
441449
renderOptionsFn: (
442450
input: z.infer<I>,
443451
renderOptions: PromptGenerateOptions<O, CustomOptions> | undefined
444-
) => Promise<GenerateOptions>,
445-
rendererAction: Promise<PromptAction<I>>
446-
) {
452+
) => Promise<GenerateOptions>;
453+
rendererAction: Promise<PromptAction<I>>;
454+
metadata?: Record<string, any>;
455+
}) {
447456
const executablePrompt = (async (
448457
input?: I,
449458
opts?: PromptGenerateOptions<O, CustomOptions>
450459
): Promise<GenerateResponse<z.infer<O>>> => {
451460
return await runInNewSpan(
452-
registry,
461+
wrapOpts.registry,
453462
{
454463
metadata: {
455-
name: (await rendererAction).__action.name,
464+
name: (await wrapOpts.rendererAction).__action.name,
456465
input,
457466
},
458467
labels: {
459468
[SPAN_TYPE_ATTR]: 'dotprompt',
460469
},
461470
},
462471
async (metadata) => {
463-
const output = await generate(registry, {
464-
...(await renderOptionsFn(input, opts)),
472+
const output = await generate(wrapOpts.registry, {
473+
...(await wrapOpts.renderOptionsFn(input, opts)),
465474
});
466475
metadata.output = output;
467476
return output;
468477
}
469478
);
470479
}) as ExecutablePrompt<z.infer<I>, O, CustomOptions>;
471480

481+
executablePrompt.ref = { name: wrapOpts.name, metadata: wrapOpts.metadata };
482+
472483
executablePrompt.render = async (
473484
input?: I,
474485
opts?: PromptGenerateOptions<O, CustomOptions>
475486
): Promise<GenerateOptions<O, CustomOptions>> => {
476487
return {
477-
...(await renderOptionsFn(input, opts)),
488+
...(await wrapOpts.renderOptionsFn(input, opts)),
478489
} as GenerateOptions<O, CustomOptions>;
479490
};
480491

481492
executablePrompt.stream = (
482493
input?: I,
483494
opts?: PromptGenerateOptions<O, CustomOptions>
484495
): GenerateStreamResponse<z.infer<O>> => {
485-
return generateStream(registry, renderOptionsFn(input, opts));
496+
return generateStream(
497+
wrapOpts.registry,
498+
wrapOpts.renderOptionsFn(input, opts)
499+
);
486500
};
487501

488502
executablePrompt.asTool = async (): Promise<ToolAction<I, O>> => {
489-
return (await rendererAction) as unknown as ToolAction<I, O>;
503+
return (await wrapOpts.rendererAction) as unknown as ToolAction<I, O>;
490504
};
491505
return executablePrompt;
492506
}

js/genkit/src/genkit.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ export class Genkit implements HasRegistry {
280280
return (await promise)(input, opts);
281281
}) as ExecutablePrompt<z.infer<I>, O, CustomOptions>;
282282

283+
executablePrompt.ref = { name };
284+
283285
executablePrompt.render = async (
284286
input?: I,
285287
opts?: PromptGenerateOptions<O, CustomOptions>

js/genkit/tests/prompts_test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,36 @@ describe('definePrompt', () => {
7272
defineEchoModel(ai);
7373
});
7474

75+
it('should define the prompt', async () => {
76+
const prompt = ai.definePrompt({
77+
name: 'hi',
78+
metadata: { foo: 'bar' },
79+
input: {
80+
schema: z.object({
81+
name: z.string(),
82+
}),
83+
},
84+
messages: async (input) => {
85+
return [
86+
{
87+
role: 'user',
88+
content: [{ text: `hi ${input.name}` }],
89+
},
90+
];
91+
},
92+
});
93+
94+
assert.deepStrictEqual(prompt.ref, {
95+
name: 'hi',
96+
metadata: { foo: 'bar' },
97+
});
98+
99+
const lookedUpPrompt = ai.prompt('hi');
100+
// This is a known limitation -- prompt lookup is async under the hood,
101+
// so we can't actually get the metadata...
102+
assert.deepStrictEqual(lookedUpPrompt.ref, { name: 'hi' }); // ideally metadatashould be: { foo: 'bar' }
103+
});
104+
75105
it('should apply middleware to a prompt call', async () => {
76106
const prompt = ai.definePrompt({
77107
name: 'hi',

js/plugins/mcp/src/util/prompts.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,20 @@ function createExecutablePrompt<
9696
options?: PromptGenerateOptions<any, any>;
9797
}
9898
): ExecutablePrompt<z.infer<I>, O, CustomOptions> {
99-
const callPrompt = async (
99+
const callPrompt = (async (
100100
input?: z.infer<I>,
101101
opts?: PromptGenerateOptions<O, CustomOptions>
102102
): Promise<GenerateResponse<z.infer<O>>> => {
103103
logger.debug(`[MCP] Calling MCP prompt ${params.name}/${prompt.name}`);
104104
return params.ai.generate(callPrompt.render(input, opts));
105+
}) as ExecutablePrompt<z.infer<I>, O, CustomOptions>;
106+
107+
callPrompt.ref = {
108+
name: prompt.name,
109+
metadata: {
110+
description: prompt.description,
111+
arguments: prompt.arguments,
112+
},
105113
};
106114

107115
callPrompt.stream = (
@@ -137,7 +145,7 @@ function createExecutablePrompt<
137145
});
138146
};
139147

140-
return callPrompt as ExecutablePrompt<z.infer<I>, O, CustomOptions>;
148+
return callPrompt;
141149
}
142150

143151
/**

js/plugins/mcp/tests/host_test.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,14 @@ describe('createMcpHost', () => {
300300
// Add a prompt to the first transport
301301
fakeTransport.prompts.push({
302302
name: 'testPrompt1',
303+
arguments: [
304+
{
305+
name: 'foo',
306+
description: 'foo arg',
307+
required: false,
308+
},
309+
],
310+
description: 'descr',
303311
});
304312
let activePrompts = await clientHost.getActivePrompts(ai);
305313
assert.strictEqual(activePrompts.length, 1);
@@ -326,17 +334,36 @@ describe('createMcpHost', () => {
326334
});
327335

328336
activePrompts = await clientHost.getActivePrompts(ai);
329-
assert.strictEqual(activePrompts.length, 2);
337+
assert.deepStrictEqual(activePrompts[0].ref.metadata, {
338+
arguments: [
339+
{
340+
description: 'foo arg',
341+
name: 'foo',
342+
required: false,
343+
},
344+
],
345+
description: 'descr',
346+
});
347+
assert.deepStrictEqual(
348+
activePrompts.map((p) => p.ref.name),
349+
['testPrompt1', 'testPrompt2']
350+
);
330351

331352
// Disable the first server
332353
await clientHost.disable('test-server');
333354
activePrompts = await clientHost.getActivePrompts(ai);
334-
assert.strictEqual(activePrompts.length, 1);
355+
assert.deepStrictEqual(
356+
activePrompts.map((p) => p.ref.name),
357+
['testPrompt2']
358+
);
335359

336360
// Enable the first server again
337361
await clientHost.enable('test-server');
338362
activePrompts = await clientHost.getActivePrompts(ai);
339-
assert.strictEqual(activePrompts.length, 2);
363+
assert.deepStrictEqual(
364+
activePrompts.map((p) => p.ref.name),
365+
['testPrompt1', 'testPrompt2']
366+
);
340367
});
341368

342369
it('should execute prompt', async () => {

js/testapps/flow-simple-ai/photo.mp4

3.63 MB
Binary file not shown.

0 commit comments

Comments
 (0)