Skip to content

[FIX]: Enforce PromptTemplate construction invariants - #132

Open
spencrr wants to merge 1 commit into
microsoft:mainfrom
spencrr:dev/spencrr/fixup-template-init
Open

[FIX]: Enforce PromptTemplate construction invariants#132
spencrr wants to merge 1 commit into
microsoft:mainfrom
spencrr:dev/spencrr/fixup-template-init

Conversation

@spencrr

@spencrr spencrr commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes construction and equality invariants for PromptTemplate introduced in #120.

The dataclass-generated initializer exposed every field, including _template. A leading underscore is only a Python naming convention, so a caller could combine declared parameter metadata with an unrelated Jinja template and bypass PromptTemplate.from_yaml() validation:

PromptTemplate(
    name="x",
    description=None,
    parameter_keys=("declared",),
    _template=Template("{{ actual }}"),
).render(declared="provided")

This renders an empty string: argument validation succeeds against parameter_keys, while the injected template references a different variable and was compiled outside RAMPART's StrictUndefined environment.

This PR:

  • disables the generated dataclass initializer and explicitly rejects direct construction, including the otherwise-valid empty PromptTemplate() call left available by init=False alone;
  • creates instances only from a schema-validated YAML definition;
  • compiles the Jinja template inside the validated-definition factory so the declared parameters and compiled behavior cannot drift independently;
  • uses object.__new__() and object.__setattr__() for the narrow initialization phase required by a frozen, slotted dataclass;
  • disables generated structural equality, which previously treated templates with identical metadata but different bodies as equal because _template was excluded from comparison, while preserving identity-based hashing; and
  • marks PromptTemplate as final for static type checkers and IDEs.

PromptTemplate.from_yaml(path) remains the supported construction API and retains its Self return type. Separate instances now use identity equality:

first = PromptTemplate.from_yaml(path)
second = PromptTemplate.from_yaml(path)

assert first is not second
assert first != second
assert len({first, second}) == 2

Breaking changes

Direct PromptTemplate(...) construction is now rejected. Use PromptTemplate.from_yaml(path) instead. Equality between separate instances is now identity-based rather than metadata-based. Existing RAMPART production call sites already construct templates through from_yaml().

Checklist

  • pre-commit run --all-files passes
  • Tests added or updated for changes: direct construction, the former generated-constructor arguments, identity equality, and hashability
  • Documentation updated: factory-only construction and migration are documented above and in the API docstrings

@spencrr
spencrr requested a review from a team July 27, 2026 18:28
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant