8000 feat(py/dotprompt): render and compile implementations by yesudeep · Pull Request #263 · google/dotprompt · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(py/dotprompt): render and compile implementations #263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions js/src/dotprompt.ts
8000
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ import * as Handlebars from 'handlebars';
import * as builtinHelpers from './helpers';
import { parseDocument, toMessages } from './parse';
import { picoschema } from './picoschema';
import {
type DataArgument,
type JSONSchema,
type ParsedPrompt,
type PromptFunction,
type PromptMetadata,
type PromptStore,
type RenderedPrompt,
type Schema,
type SchemaResolver,
type ToolDefinition,
type ToolResolver,
import type {
DataArgument,
JSONSchema,
ParsedPrompt,
PromptFunction,
PromptMetadata,
PromptStore,
RenderedPrompt,
Schema,
SchemaResolver,
ToolDefinition,
ToolResolver,
} from './types';
import { removeUndefinedFields } from './util';

Expand Down Expand Up @@ -196,6 +196,7 @@ export class Dotprompt {
}
);

// Create an instance of a PromptFunction.
const renderFunc = async (
data: DataArgument,
options?: PromptMetadata<ModelConfig>
Expand Down Expand Up @@ -223,7 +224,10 @@ export class Dotprompt {
messages: toMessages<ModelConfig>(renderedString, data),
};
};

// Add the parsed source to the prompt function as a property.
(renderFunc as PromptFunction<ModelConfig>).prompt = parsedSource;

return renderFunc as PromptFunction<ModelConfig>;
}

Expand Down
48 changes: 41 additions & 7 deletions python/dotpromptz/src/dotpromptz/dotprompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
import anyio

from dotpromptz.helpers import BUILTIN_HELPERS
from dotpromptz.parse import parse_document
from dotpromptz.parse import parse_document, to_messages
from dotpromptz.picoschema import picoschema_to_json_schema
from dotpromptz.resolvers import resolve_json_schema, resolve_partial, resolve_tool
from dotpromptz.typing import (
Expand All @@ -64,7 +64,7 @@
VariablesT,
)
from dotpromptz.util import remove_undefined_fields
from handlebarrz import EscapeFunction, Handlebars, HelperFn
from handlebarrz import Context, EscapeFunction, Handlebars, HelperFn, RuntimeOptions

# Pre-compiled regex for finding partial references in handlebars templates

Expand Down Expand Up @@ -117,7 +117,7 @@ def _identify_partials(template: str) -> set[str]:
return set(_PARTIAL_PATTERN.findall(template))


class CompiledRenderer(PromptFunction[ModelConfigT]):
class RenderFunc(PromptFunction[ModelConfigT]):
"""A compiled prompt function with the prompt as a property.

This is the Python equivalent of the renderFunc nested function
Expand Down Expand Up @@ -151,9 +151,42 @@ async def __call__(
Returns:
The rendered prompt.
"""
# Discard the input schema as once rendered it doesn't make sense.
merged_metadata: PromptMetadata[ModelConfigT] = await self._dotprompt.render_metadata(self.prompt, options)
merged_metadata.input = None

# Prepare input data, merging defaults from options if available.
context: Context = {
**((options.input.default or {}) if options and options.input else {}),
**(data.input if data.input is not None else {}),
}

# Prepare runtime options.
# TODO: options are currently ignored; need to add support for it.
runtime_options: RuntimeOptions = {
'data': {
'metadata': {
'prompt': merged_metadata.model_dump(exclude_none=True, by_alias=True),
'docs': data.docs,
'messages': data.messages,
},
**(data.context or {}),
},
}

# Render the string.
render_string = self._handlebars.compile(self.prompt.template)
rendered_string = render_string(context, runtime_options)

# Parse the rendered string into messages.
messages = to_messages(rendered_string, data)

# Construct and return the final RenderedPrompt.
# TODO: Stub
return RenderedPrompt[ModelConfigT](messages=[])
return RenderedPrompt[ModelConfigT](
# Spread the metadata fields into the RenderedPrompt constructor.
**merged_metadata.model_dump(exclude_none=True, by_alias=True),
messages=messages,
)


class Dotprompt:
Expand Down Expand Up @@ -294,7 +327,7 @@ async def compile(

# Resolve partials before compiling.
await self._resolve_partials(prompt.template)
return CompiledRenderer(self, self._handlebars, prompt)
return RenderFunc(self, self._handlebars, prompt)

async def render_metadata(
self,
Expand Down Expand Up @@ -453,12 +486,13 @@ async def _resolve_tools(self, metadata: PromptMetadata[ModelConfigT]) -> Prompt
# Found locally.
out.tool_defs.append(self._tools[name])
elif have_resolver:
# Resolve from the tool resolver.
# Resolve using the tool resolver.
to_resolve.append(name)
else:
# Unregistered tool.
unregistered_names.append(name)

# Resolve all the tools to be resolved using the resolver.
if to_resolve:

async def resolve_and_append(tool_name: str) -> None:
Expand Down
56 changes: 47 additions & 9 deletions python/handlebarrz/src/handlebarrz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ def format_name(params, hash, ctx):
```
"""

from __future__ import annotations

import json
import sys # noqa
from collections.abc import Callable
from pathlib import Path
from typing import Any
from typing import Any, TypedDict

import structlog

Expand All @@ -91,6 +93,22 @@ def format_name(params, hash, ctx):

HelperFn = Callable[[list[Any], dict[str, Any], dict[str, Any]], str]
NativeHelperFn = Callable[[str, str, str], str]
Context = dict[str, Any]


class RuntimeOptions(TypedDict):
"""Options for the runtime of a Handlebars template.

These options are used to configure the runtime behavior of a Handlebars
template. They can be passed to the compiled template function to customize
the rendering process.
"""

data: dict[str, Any] | None
# TODO: Add other options based on supported features.


CompiledRenderer = Callable[[Context, RuntimeOptions | None], str]


class EscapeFunction(StrEnum):
Expand Down Expand Up @@ -427,7 +445,7 @@ def unregister_template(self, name: str) -> None:
self._template.unregister_template(name)
logger.debug({'event': 'template_unregistered', 'name': name})

def render(self, name: str, data: dict[str, Any]) -> str:
def render(self, name: str, data: dict[str, Any], options: RuntimeOptions | None = None) -> str:
"""Render a template with the given data.

Renders a previously registered template using the provided data
Expand All @@ -437,6 +455,7 @@ def render(self, name: str, data: dict[str, Any]) -> str:
Args:
name: The name of the template to render
data: The data to render the template with
options: Additional options for the template.

Returns:
str: The rendered template string
Expand All @@ -445,6 +464,8 @@ def render(self, name: str, data: dict[str, Any]) -> str:
ValueError: If the template does not exist or there is a rendering
error.
"""
# TODO: options is currently ignored; need to add support for it.

try:
result = self._template.render(name, json.dumps(data))
logger.debug({'event': 'template_rendered', 'name': name})
Expand All @@ -457,7 +478,7 @@ def render(self, name: str, data: dict[str, Any]) -> str:
})
raise

def render_template(self, template_string: str, data: dict[str, Any]) -> str:
def render_template(self, template_string: str, data: dict[str, Any], options: RuntimeOptions | None = None) -> str:
"""Render a template string directly without registering it.

Parses and renders the template string in one step. This is useful for
Expand All @@ -467,6 +488,7 @@ def render_template(self, template_string: str, data: dict[str, Any]) -> str:
Args:
template_string: The template string to render
data: The data to render the template with
options: Additional options for the template.

Returns:
Rendered template string.
Expand All @@ -475,8 +497,15 @@ def render_template(self, template_string: str, data: dict[str, Any]) -> str:
ValueError: If there is a syntax error in the template or a
rendering error.
"""
# TODO: options is currently ignored; need to add support for it.
try:
result = self._template.render_template(template_string, json.dumps(data))
# Serialize options if provided, focusing on the '@data' part
options_json = None
if options:
# Pass the whole options dict as JSON
options_json = json.dumps(options)

result = self._template.render_template(template_string, json.dumps(data), options_json)
logger.debug({'event': 'template_string_rendered'})
return result
except ValueError as e:
Expand All @@ -486,7 +515,7 @@ def render_template(self, template_string: str, data: dict[str, Any]) -> str:
})
raise

def compile(self, template_string: str) -> Callable[[dict[str, Any]], str]:
def compile(self, template_string: str) -> CompiledRenderer:
"""Compile a template string into a reusable function.

This method provides an interface similar to Handlebars.js's `compile`.
Expand All @@ -502,8 +531,8 @@ def compile(self, template_string: str) -> Callable[[dict[str, Any]], str]:
template_string: The Handlebars template string to compile.

Returns:
A callable function that takes a data dictionary and returns the
rendered string.
A callable function that takes a data dictionary and some runtime
options and returns the rendered string.

Raises:
ValueError: If there is a syntax error during the initial parse
Expand All @@ -512,8 +541,17 @@ def compile(self, template_string: str) -> Callable[[dict[str, Any]], str]:
called.
"""

def compiled(data: dict[str, Any]) -> str:
return self.render_template(template_string, data)
def compiled(context: Context, options: RuntimeOptions | None = None) -> str:
"""Compiled template function.

Args:
context: The data to render the template with.
options: Additional options for the template.

Returns:
The rendered template string.
"""
return self.render_template(template_string, context, options)

return compiled

Expand Down
3 changes: 2 additions & 1 deletion python/handlebarrz/src/handlebarrz/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"""Stub type annotations for native Handlebars."""

from collections.abc import Callable
from typing import Any

def html_escape(text: str) -> str: ...
def no_escape(text: str) -> str: ...
Expand Down Expand Up @@ -52,7 +53,7 @@ class HandlebarrzTemplate:

# Rendering.
def render(self, name: str, data_json: str) -> str: ...
def render_template(self, template_str: str, data_json: str) -> str: ...
def render_template(self, template_str: str, data_json: str, options_json: str | None = None) -> str: ...

# Extra helper registration.
def register_extra_helpers(self) -> None: ...
26 changes: 22 additions & 4 deletions python/handlebarrz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,8 @@ impl HandlebarrzTemplate {
/// # Arguments
///
/// * `template_string` - The template source code.
/// * `data` - The data to use for rendering (as JSON).
/// * `data_json` - The data to use for rendering (as JSON).
/// * `options_json` - Optional. If provided, the data will be merged with this JSON object.
///
/// # Raises
///
Expand All @@ -459,11 +460,28 @@ impl HandlebarrzTemplate {
/// # Returns
///
/// Rendered template as a string.
#[pyo3(text_signature = "($self, template_string, data)")]
fn render_template(&self, template_string: &str, data: &str) -> PyResult<String> {
let data: Value = serde_json::from_str(data)
#[pyo3(text_signature = "($self, template_string, data_json, options_json = None)")]
fn render_template(
&self,
template_string: &str,
data_json: &str,
_options_json: Option<&str>,
) -> PyResult<String> {
let data: Value = serde_json::from_str(data_json)
.map_err(|e| PyValueError::new_err(format!("invalid JSON: {}", e)))?;

// TODO: Implement setting the data attribute of runtime options.
// if let Some(options_str) = options_json {
// let options_data: Value = serde_json::from_str(options_str)
// .map_err(|e| PyValueError::new_err(format!("invalid options JSON: {}", e)))?;

// if let (Some(data_map), Some(_options_map)) =
// (data.as_object_mut(), options_data.as_object())
// {
// data_map.insert("@data".to_string(), options_data.clone());
// }
// }

self.registry
.render_template(template_string, &data)
.map_err(|e| PyValueError::new_err(e.to_string()))
Expand Down
Loading
Loading
0