ADR 031 · Accepted
Tagged Prompt Snippet Library
Context
Consuming extensions — first nr_repurpose — assemble prompts from recurring building blocks: a persona, a tone of voice, a target audience, an image style, a layout instruction. Editors want to manage these fragments centrally, once, instead of re-typing them into every extension's own configuration.
The existing PromptTemplate entity does not fit this need. It is a heavyweight complete prompt: it binds a feature, carries model parameters (temperature, max tokens, top-p), supports versioning with parent/variant relations, and tracks usage performance. A persona like "You are Nova, a friendly expert." has none of these concerns — it is a fragment that only becomes a prompt when a consumer composes it with its own instructions. Forcing fragments into PromptTemplate would either bloat every fragment record with irrelevant model fields or fork the template semantics depending on a "fragment" flag.
A second question is how consumers select fragments. A fixed category enum (like Task categories) would require an nr-llm release every time a consuming extension introduces a new fragment kind, which contradicts the goal of nr-llm being a shared foundation that consumers extend without touching it.
Decision
Introduce a separate, lightweight PromptSnippet entity (table tx_nrllm_promptsnippet) next to — not on top of — PromptTemplate:
Fragments, not templates. A snippet is identifier + name + description + fragment text. No model parameters, no versioning, no performance tracking. PromptTemplate stays untouched.
Free-form CSV tags instead of a category enum. Snippets carry a comma-separated tags field. Consumers query PromptSnippetRepository::findActiveByTag(), which matches tags as exact, case-insensitive tokens — style never matches lifestyle. The tag vocabulary is a convention between editors and consumers (established so far: audience, tone_of_voice, persona, layout, style), documented in the TCA field description and the administration guide. New fragment kinds need no nr-llm release.
JSON metadata side-channel. An optional metadata JSON object carries consumer-specific settings (e.g. {"voice": "nova"} on persona snippets so speech features can pick a matching TTS voice). getMetadataArray() returns [] for empty or invalid JSON — bad editor input must never break a consumer.
Composition stays in nr-llm. PromptSnippetComposer renders an ordered label-to-snippet map into labeled prompt blocks (LABEL: + fragment text, blank-line separated), so all consumers produce uniformly structured prompt sections.
Editing via FormEngine. The backend module gets a "Snippets" list following the established Providers/Models/Tasks pattern; create/edit links into FormEngine, no custom forms.
Consequences
Editors manage personas, tones, audiences, styles, and layouts once, centrally; every consuming extension reads the same library.
The free-tag model keeps nr-llm release-independent from consumer vocabulary — at the cost of no referential integrity: a typo in a tag silently yields an empty query result. The documented convention and the tag badges in the list view mitigate this.
Token matching is implemented over the CSV field in PHP, not SQL LIKE, guaranteeing exact-token semantics on every database platform. The snippet library is small (tens of records), so loading active snippets for tag filtering is not a performance concern.
Two prompt-related entities now coexist. The split is intentional (template = complete prompt, snippet = fragment) and documented here, in the administration guide, and in both entities' PHPDoc.