Skip to content

Commit 7df3fa9

Browse files
fix: add span for prompt template rendering (#2165)
Co-authored-by: Pavel Jbanov <[email protected]>
1 parent a759945 commit 7df3fa9

File tree

2 files changed

+140
-75
lines changed

2 files changed

+140
-75
lines changed

js/ai/src/prompt.ts

Lines changed: 100 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import { lazy } from '@genkit-ai/core/async';
2929
import { logger } from '@genkit-ai/core/logging';
3030
import type { Registry } from '@genkit-ai/core/registry';
3131
import { toJsonSchema } from '@genkit-ai/core/schema';
32-
import type { Message as DpMessage, PromptFunction } from 'dotprompt';
32+
import { SPAN_TYPE_ATTR, runInNewSpan } from '@genkit-ai/core/tracing';
33+
import { Message as DpMessage, PromptFunction } from 'dotprompt';
3334
import { existsSync, readFileSync, readdirSync } from 'fs';
3435
import { basename, join, resolve } from 'path';
3536
import type { DocumentData } from './document.js';
@@ -252,72 +253,87 @@ function definePromptAsync<
252253
input: z.infer<I>,
253254
renderOptions: PromptGenerateOptions<O, CustomOptions> | undefined
254255
): Promise<GenerateOptions> => {
255-
const messages: MessageData[] = [];
256-
renderOptions = { ...renderOptions }; // make a copy, we will be trimming
257-
const session = getCurrentSession(registry);
258-
const resolvedOptions = await optionsPromise;
259-
260-
// order of these matters:
261-
await renderSystemPrompt(
262-
registry,
263-
session,
264-
input,
265-
messages,
266-
resolvedOptions,
267-
promptCache,
268-
renderOptions
269-
);
270-
await renderMessages(
271-
registry,
272-
session,
273-
input,
274-
messages,
275-
resolvedOptions,
276-
renderOptions,
277-
promptCache
278-
);
279-
await renderUserPrompt(
256+
return await runInNewSpan(
280257
registry,
281-
session,
282-
input,
283-
messages,
284-
resolvedOptions,
285-
promptCache,
286-
renderOptions
287-
);
288-
289-
let docs: DocumentData[] | undefined;
290-
if (typeof resolvedOptions.docs === 'function') {
291-
docs = await resolvedOptions.docs(input, {
292-
state: session?.state,
293-
context: renderOptions?.context || getContext(registry) || {},
294-
});
295-
} else {
296-
docs = resolvedOptions.docs;
297-
}
298-
299-
const opts: GenerateOptions = stripUndefinedProps({
300-
model: resolvedOptions.model,
301-
maxTurns: resolvedOptions.maxTurns,
302-
messages,
303-
docs,
304-
tools: resolvedOptions.tools,
305-
returnToolRequests: resolvedOptions.returnToolRequests,
306-
toolChoice: resolvedOptions.toolChoice,
307-
context: resolvedOptions.context,
308-
output: resolvedOptions.output,
309-
use: resolvedOptions.use,
310-
...stripUndefinedProps(renderOptions),
311-
config: {
312-
...resolvedOptions?.config,
313-
...renderOptions?.config,
258+
{
259+
metadata: {
260+
name: 'render',
261+
input,
262+
},
263+
labels: {
264+
[SPAN_TYPE_ATTR]: 'promptTemplate',
265+
},
314266
},
315-
});
316-
// if config is empty and it was not explicitly passed in, we delete it, don't want {}
317-
if (Object.keys(opts.config).length === 0 && !renderOptions?.config) {
318-
delete opts.config;
319-
}
320-
return opts;
267+
async (metadata) => {
268+
const messages: MessageData[] = [];
269+
renderOptions = { ...renderOptions }; // make a copy, we will be trimming
270+
const session = getCurrentSession(registry);
271+
const resolvedOptions = await optionsPromise;
272+
273+
// order of these matters:
274+
await renderSystemPrompt(
275+
registry,
276+
session,
277+
input,
278+
messages,
279+
resolvedOptions,
280+
promptCache,
281+
renderOptions
282+
);
283+
await renderMessages(
284+
registry,
285+
session,
286+
input,
287+
messages,
288+
resolvedOptions,
289+
renderOptions,
290+
promptCache
291+
);
292+
await renderUserPrompt(
293+
registry,
294+
session,
295+
input,
296+
messages,
297+
resolvedOptions,
298+
promptCache,
299+
renderOptions
300+
);
301+
302+
let docs: DocumentData[] | undefined;
303+
if (typeof resolvedOptions.docs === 'function') {
304+
docs = await resolvedOptions.docs(input, {
305+
state: session?.state,
306+
context: renderOptions?.context || getContext(registry) || {},
307+
});
308+
} else {
309+
docs = resolvedOptions.docs;
310+
}
311+
312+
const opts: GenerateOptions = stripUndefinedProps({
313+
model: resolvedOptions.model,
314+
maxTurns: resolvedOptions.maxTurns,
315+
messages,
316+
docs,
317+
tools: resolvedOptions.tools,
318+
returnToolRequests: resolvedOptions.returnToolRequests,
319+
toolChoice: resolvedOptions.toolChoice,
320+
context: resolvedOptions.context,
321+
output: resolvedOptions.output,
322+
use: resolvedOptions.use,
323+
...stripUndefinedProps(renderOptions),
324+
config: {
325+
...resolvedOptions?.config,
326+
...renderOptions?.config,
327+
},
328+
});
329+
// if config is empty and it was not explicitly passed in, we delete it, don't want {}
330+
if (Object.keys(opts.config).length === 0 && !renderOptions?.config) {
331+
delete opts.config;
332+
}
333+
metadata.output = opts;
334+
return opts;
335+
}
336+
);
321337
};
322338
const rendererActionConfig = lazy(() =>
323339
optionsPromise.then((options: PromptConfig<I, O, CustomOptions>) => {
@@ -432,9 +448,25 @@ function wrapInExecutablePrompt<
432448
input?: I,
433449
opts?: PromptGenerateOptions<O, CustomOptions>
434450
): Promise<GenerateResponse<z.infer<O>>> => {
435-
return generate(registry, {
436-
...(await renderOptionsFn(input, opts)),
437-
});
451+
return await runInNewSpan(
452+
registry,
453+
{
454+
metadata: {
455+
name: (await rendererAction).__action.name,
456+
input,
457+
},
458+
labels: {
459+
[SPAN_TYPE_ATTR]: 'dotprompt',
460+
},
461+
},
462+
async (metadata) => {
463+
const output = await generate(registry, {
464+
...(await renderOptionsFn(input, opts)),
465+
});
466+
metadata.output = output;
467+
return output;
468+
}
469+
);
438470
}) as ExecutablePrompt<z.infer<I>, O, CustomOptions>;
439471

440472
executablePrompt.render = async (

js/genkit/src/genkit.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
type GenerateOptions,
3939
type GenerateRequest,
4040
type GenerateResponse,
41+
type GenerateResponseChunk,
4142
type GenerateResponseData,
4243
type GenerateStreamOptions,
4344
type GenerateStreamResponse,
@@ -110,11 +111,13 @@ import {
110111
type StreamingCallback,
111112
type z,
112113
} from '@genkit-ai/core';
114+
import { Channel } from '@genkit-ai/core/async';
113115
import type { HasRegistry } from '@genkit-ai/core/registry';
114116
import type { BaseEvalDataPointSchema } from './evaluator.js';
115117
import { logger } from './logging.js';
116118
import type { GenkitPlugin } from './plugin.js';
117119
import { Registry, type ActionType } from './registry.js';
120+
import { SPAN_TYPE_ATTR, runInNewSpan } from './tracing.js';
118121

119122
/**
120123
* @deprecated use `ai.definePrompt({messages: fn})`
@@ -254,6 +257,7 @@ export class Genkit implements HasRegistry {
254257
options?: { variant?: string }
255258
): ExecutablePrompt<z.infer<I>, O, CustomOptions> {
256259
return this.wrapExecutablePromptPromise(
260+
`${name}${options?.variant ? `.${options?.variant}` : ''}`,
257261
prompt(this.registry, name, {
258262
...options,
259263
dir: this.options.promptDir ?? './prompts',
@@ -265,7 +269,10 @@ export class Genkit implements HasRegistry {
265269
I extends z.ZodTypeAny = z.ZodTypeAny,
266270
O extends z.ZodTypeAny = z.ZodTypeAny,
267271
CustomOptions extends z.ZodTypeAny = z.ZodTypeAny,
268-
>(promise: Promise<ExecutablePrompt<z.infer<I>, O, CustomOptions>>) {
272+
>(
273+
name: string,
274+
promise: Promise<ExecutablePrompt<z.infer<I>, O, CustomOptions>>
275+
) {
269276
const executablePrompt = (async (
270277
input?: I,
271278
opts?: PromptGenerateOptions<O, CustomOptions>
@@ -286,13 +293,39 @@ export class Genkit implements HasRegistry {
286293
input?: I,
287294
opts?: PromptGenerateOptions<O, CustomOptions>
288295
): GenerateStreamResponse<O> => {
289-
return this.generateStream(
290-
promise.then((action) =>
291-
action.render(input, {
292-
...opts,
293-
})
294-
)
296+
let channel = new Channel<GenerateResponseChunk>();
297+
298+
const generated = runInNewSpan(
299+
this.registry,
300+
{
301+
metadata: {
302+
name,
303+
input,
304+
},
305+
labels: {
306+
[SPAN_TYPE_ATTR]: 'dotprompt',
307+
},
308+
},
309+
() =>
310+
generate<O, CustomOptions>(
311+
this.registry,
312+
promise.then((action) =>
313+
action.render(input, {
314+
...opts,
315+
onChunk: (chunk) => channel.send(chunk),
316+
})
317+
)
318+
)
319+
);
320+
generated.then(
321+
() => channel.close(),
322+
(err) => channel.error(err)
295323
);
324+
325+
return {
326+
response: generated,
327+
stream: channel,
328+
};
296329
};
297330

298331
executablePrompt.asTool = async (): Promise<ToolAction<I, O>> => {

0 commit comments

Comments
 (0)