Skip to content

Commit 0752865

Browse files
authored
fix(py/dotpromptz): register initial helpers and partials correctly (#260)
1 parent 5cc067d commit 0752865

File tree

5 files changed

+192
-150
lines changed

5 files changed

+192
-150
lines changed

js/src/dotprompt.ts

Lines changed: 124 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -88,22 +88,8 @@ export class Dotprompt {
8888
this.schemaResolver = options?.schemaResolver;
8989
this.partialResolver = options?.partialResolver;
9090

91-
for (const key in helpers) {
92-
this.defineHelper(key, helpers[key as keyof typeof helpers]);
93-
this.handlebars.registerHelper(key, helpers[key as keyof typeof helpers]);
94-
}
95-
96-
if (options?.helpers) {
97-
for (const key in options.helpers) {
98-
this.defineHelper(key, options.helpers[key]);
99-
}
100-
}
101-
102-
if (options?.partials) {
103-
for (const key in options.partials) {
104-
this.definePartial(key, options.partials[key]);
105-
}
106-
}
91+
this.registerInitialHelpers(options?.helpers);
92+
this.registerInitialPartials(options?.partials);
10793
}
10894

10995
/**
@@ -174,6 +160,106 @@ export class Dotprompt {
174160
return renderer(data, options);
175161
}
176162

163+
/**
164+
* Compiles a template into a reusable function for rendering prompts.
165+
*
166+
* @param source The template source or parsed prompt to compile
167+
* @param additionalMetadata Additional metadata to include in the compiled template
168+
* @return A promise resolving to a function for rendering the template
169+
*/
170+
async compile<
171+
Variables = Record<string, unknown>,
172+
ModelConfig = Record<string, unknown>,
173+
>(
174+
source: string | ParsedPrompt<ModelConfig>,
175+
additionalMetadata?: PromptMetadata<ModelConfig>
176+
): Promise<PromptFunction<ModelConfig>> {
177+
let parsedSource: ParsedPrompt<ModelConfig>;
178+
if (typeof source === 'string') {
179+
parsedSource = this.parse<ModelConfig>(source);
180+
} else {
181+
parsedSource = source;
182+
}
183+
184+
if (additionalMetadata) {
185+
parsedSource = { ...parsedSource, ...additionalMetadata };
186+
}
187+
188+
// Resolve all partials before compilation.
189+
await this.resolvePartials(parsedSource.template);
190+
191+
const renderString = this.handlebars.compile<Variables>(
192+
parsedSource.template,
193+
{
194+
knownHelpers: this.knownHelpers,
195+
knownHelpersOnly: true,
196+
noEscape: true,
197+
}
198+
);
199+
200+
const renderFunc = async (
201+
data: DataArgument,
202+
options?: PromptMetadata<ModelConfig>
203+
) => {
204+
// Discard the input schema as once rendered it doesn't make sense.
205+
const { input, ...mergedMetadata } =
206+
await this.renderMetadata(parsedSource);
207+
208+
const renderedString = renderString(
209+
{ ...(options?.input?.default || {}), ...data.input },
210+
{
211+
data: {
212+
metadata: {
213+
prompt: mergedMetadata,
214+
docs: data.docs,
215+
messages: data.messages,
216+
},
217+
...(data.context || {}),
218+
},
219+
}
220+
);
221+
222+
return {
223+
...mergedMetadata,
224+
messages: toMessages<ModelConfig>(renderedString, data),
225+
};
226+
};
227+
(renderFunc as PromptFunction<ModelConfig>).prompt = parsedSource;
228+
return renderFunc as PromptFunction<ModelConfig>;
229+
}
230+
231+
/**
232+
* Processes and resolves all metadata for a prompt template.
233+
*
234+
* @param source The template source or parsed prompt
235+
* @param additionalMetadata Additional metadata to include
236+
* @return A promise resolving to the fully processed metadata
237+
*/
238+
async renderMetadata<ModelConfig>(
239+
source: string | ParsedPrompt<ModelConfig>,
240+
additionalMetadata?: PromptMetadata<ModelConfig>
241+
): Promise<PromptMetadata<ModelConfig>> {
242+
let parsedSource: ParsedPrompt<ModelConfig>;
243+
if (typeof source === 'string') {
244+
parsedSource = this.parse<ModelConfig>(source);
245+
} else {
246+
parsedSource = source;
247+
}
248+
249+
const model =
250+
additionalMetadata?.model || parsedSource.model || this.defaultModel;
251+
252+
let modelConfig: ModelConfig | undefined;
253+
if (model && this.modelConfigs[model]) {
254+
modelConfig = this.modelConfigs[model] as ModelConfig;
255+
}
256+
257+
return this.resolveMetadata<ModelConfig>(
258+
modelConfig ? { config: modelConfig } : {},
259+
parsedSource,
260+
additionalMetadata
261+
);
262+
}
177263
/**
178264
* Processes schema definitions in picoschema format into standard JSON Schema.
179265
*
@@ -401,103 +487,37 @@ export class Dotprompt {
401487
}
402488

403489
/**
404-
* Compiles a template into a reusable function for rendering prompts.
405-
*
406-
* @param source The template source or parsed prompt to compile
407-
* @param additionalMetadata Additional metadata to include in the compiled template
408-
* @return A promise resolving to a function for rendering the template
490+
* Registers initial helpers from built-in helpers and options.
491+
* @private
409492
*/
410-
async compile<
411-
Variables = Record<string, unknown>,
412-
ModelConfig = Record<string, unknown>,
413-
>(
414-
source: string | ParsedPrompt<ModelConfig>,
415-
additionalMetadata?: PromptMetadata<ModelConfig>
416-
): Promise<PromptFunction<ModelConfig>> {
417-
let parsedSource: ParsedPrompt<ModelConfig>;
418-
if (typeof source === 'string') {
419-
parsedSource = this.parse<ModelConfig>(source);
420-
} else {
421-
parsedSource = source;
422-
}
423-
424-
if (additionalMetadata) {
425-
parsedSource = { ...parsedSource, ...additionalMetadata };
493+
private registerInitialHelpers(
494+
customHelpers?: Record<string, Handlebars.HelperDelegate>
495+
): void {
496+
// Register built-in helpers
497+
for (const key in helpers) {
498+
this.defineHelper(key, helpers[key as keyof typeof helpers]);
499+
this.handlebars.registerHelper(key, helpers[key as keyof typeof helpers]);
426500
}
427501

428-
// Resolve all partials before compilation.
429-
await this.resolvePartials(parsedSource.template);
430-
431-
const renderString = this.handlebars.compile<Variables>(
432-
parsedSource.template,
433-
{
434-
knownHelpers: this.knownHelpers,
435-
knownHelpersOnly: true,
436-
noEscape: true,
502+
// Register custom helpers from options
503+
if (customHelpers) {
504+
for (const key in customHelpers) {
505+
this.defineHelper(key, customHelpers[key]);
437506
}
438-
);
439-
440-
const renderFunc = async (
441-
data: DataArgument,
442-
options?: PromptMetadata<ModelConfig>
443-
) => {
444-
// Discard the input schema as once rendered it doesn't make sense.
445-
const { input, ...mergedMetadata } =
446-
await this.renderMetadata(parsedSource);
447-
448-
const renderedString = renderString(
449-
{ ...(options?.input?.default || {}), ...data.input },
450-
{
451-
data: {
452-
metadata: {
453-
prompt: mergedMetadata,
454-
docs: data.docs,
455-
messages: data.messages,
456-
},
457-
...(data.context || {}),
458-
},
459-
}
460-
);
461-
462-
return {
463-
...mergedMetadata,
464-
messages: toMessages<ModelConfig>(renderedString, data),
465-
};
466-
};
467-
(renderFunc as PromptFunction<ModelConfig>).prompt = parsedSource;
468-
return renderFunc as PromptFunction<ModelConfig>;
507+
}
469508
}
470509

471510
/**
472-
* Processes and resolves all metadata for a prompt template.
511+
* Registers initial partials from the options.
473512
*
474-
* @param source The template source or parsed prompt
475-
* @param additionalMetadata Additional metadata to include
476-
* @return A promise resolving to the fully processed metadata
513+
* @param partials The partials to register
514+
* @private
477515
*/
478-
async renderMetadata<ModelConfig>(
479-
source: string | ParsedPrompt<ModelConfig>,
480-
additionalMetadata?: PromptMetadata<ModelConfig>
481-
): Promise<PromptMetadata<ModelConfig>> {
482-
let parsedSource: ParsedPrompt<ModelConfig>;
483-
if (typeof source === 'string') {
484-
parsedSource = this.parse<ModelConfig>(source);
485-
} else {
486-
parsedSource = source;
487-
}
488-
489-
const model =
490-
additionalMetadata?.model || parsedSource.model || this.defaultModel;
491-
492-
let modelConfig: ModelConfig | undefined;
493-
if (model && this.modelConfigs[model]) {
494-
modelConfig = this.modelConfigs[model] as ModelConfig;
516+
private registerInitialPartials(partials?: Record<string, string>): void {
517+
if (partials) {
518+
for (const key in partials) {
519+
this.definePartial(key, partials[key]);
520+
}
495521
}
496-
497-
return this.resolveMetadata<ModelConfig>(
498-
modelConfig ? { config: modelConfig } : {},
499-
parsedSource,
500-
additionalMetadata
501-
);
502522
}
503523
}

python/dotpromptz/src/dotpromptz/dotprompt.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444

4545
import anyio
4646

47-
from dotpromptz.helpers import register_all_helpers
48-
from dotpromptz.parse import parse_document, to_messages
47+
from dotpromptz.helpers import BUILTIN_HELPERS
48+
from dotpromptz.parse import parse_document
4949
from dotpromptz.picoschema import picoschema_to_json_schema
5050
from dotpromptz.resolvers import resolve_json_schema, resolve_partial, resolve_tool
5151
from dotpromptz.typing import (
@@ -200,19 +200,11 @@ def __init__(
200200
self._partial_resolver: PartialResolver | None = partial_resolver
201201
self._store: PromptStore | None = None
202202

203-
self._register_initial_helpers()
204-
self._register_initial_partials()
205-
206-
def _register_initial_helpers(self) -> None:
207-
"""Register the initial helpers."""
208-
register_all_helpers(self._handlebars)
209-
for name, fn in self._helpers.items():
210-
self._handlebars.register_helper(name, fn)
211-
212-
def _register_initial_partials(self) -> None:
213-
"""Register the initial partials."""
214-
for name, source in self._partials.items():
215-
self._handlebars.register_partial(name, source)
203+
self._register_initial_helpers(
204+
builtin_helpers=BUILTIN_HELPERS,
205+
custom_helpers=helpers,
206+
)
207+
self._register_initial_partials(partials)
216208

217209
def define_helper(self, name: str, fn: HelperFn) -> Dotprompt:
218210
"""Define a helper function for the template.
@@ -538,3 +530,35 @@ async def _wrapped_schema_resolver(self, name: str) -> JsonSchema | None:
538530

539531
# TODO: Should we cache the resolved schema in self._schemas?
540532
return await resolve_json_schema(name, self._schema_resolver)
533+
534+
def _register_initial_helpers(
535+
self,
536+
builtin_helpers: dict[str, HelperFn] | None = None,
537+
custom_helpers: dict[str, HelperFn] | None = None,
538+
) -> None:
539+
"""Register the initial helpers.
540+
541+
Built-in helpers are registered first, then custom helpers are
542+
registered.
543+
544+
Args:
545+
builtin_helpers: Built-in helpers to register.
546+
custom_helpers: Custom helpers to register.
547+
"""
548+
if builtin_helpers is not None:
549+
for name, fn in builtin_helpers.items():
550+
self.define_helper(name, fn)
551+
552+
if custom_helpers is not None:
553+
for name, fn in custom_helpers.items():
554+
self.define_helper(name, fn)
555+
556+
def _register_initial_partials(self, partials: dict[str, str] | None = None) -> None:
557+
"""Register the initial partials.
558+
559+
Args:
560+
partials: Partials to register.
561+
"""
562+
if partials is not None:
563+
for name, source in partials.items():
564+
self.define_partial(name, source)

0 commit comments

Comments
 (0)