pub struct Engine { /* private fields */ }Expand description
The core NLG engine. Holds a language implementation, template registry,
and immutable configuration. All per-render mutable state lives in
Session, which callers pass into render methods.
Implementations§
Source§impl Engine
impl Engine
Sourcepub fn new(language: impl Language + 'static) -> Self
pub fn new(language: impl Language + 'static) -> Self
Create a new engine with the given language implementation.
Sourcepub fn language_preference(self, lang: impl Into<String>) -> Self
pub fn language_preference(self, lang: impl Into<String>) -> Self
Set the BCP-47 language code that variant selection should prefer.
When templates are registered with Engine::register_template_with_language,
the engine picks variants whose language matches this preference; if
none match, it falls back to language-untagged variants, then to any
registered variant.
Sourcepub fn set_language_preference(&mut self, lang: impl Into<String>)
pub fn set_language_preference(&mut self, lang: impl Into<String>)
Update the BCP-47 language code used to prefer language-tagged variants without rebuilding the engine or dropping registered templates.
Sourcepub fn style_preference(self, style: impl Into<String>) -> Self
pub fn style_preference(self, style: impl Into<String>) -> Self
Set the free-form style tag that variant selection should prefer.
Style tags are application-defined strings such as "executive",
"technical", or "customer". The fallback chain mirrors language:
preferred style first, then unstyled variants, then any registered
variant for the already-selected language bucket.
Sourcepub fn set_style_preference(&mut self, style: impl Into<String>)
pub fn set_style_preference(&mut self, style: impl Into<String>)
Update the style tag used to prefer style-tagged variants without rebuilding the engine or dropping registered templates.
Sourcepub fn strictness(self, strictness: Strictness) -> Self
pub fn strictness(self, strictness: Strictness) -> Self
Set the strictness mode for missing slot handling.
Sourcepub fn variation(self, variation: Variation) -> Self
pub fn variation(self, variation: Variation) -> Self
Set the variation strategy for template selection.
Sourcepub fn salience_thresholds(self, thresholds: SalienceThresholds) -> Self
pub fn salience_thresholds(self, thresholds: SalienceThresholds) -> Self
Set the thresholds for automatic salience derivation from context.
Sourcepub fn style_profile(self, profile: StyleProfile) -> Self
pub fn style_profile(self, profile: StyleProfile) -> Self
Apply a StyleProfile — a declarative voice
configuration that biases the engine’s existing rendering decisions
toward a target register without breaking determinism. Setting the
neutral profile (StyleProfile::neutral()) is byte-for-byte
equivalent to never calling this method.
Sourcepub fn current_style_profile(&self) -> &StyleProfile
pub fn current_style_profile(&self) -> &StyleProfile
Read the currently-applied StyleProfile.
Returns the neutral profile if none was explicitly set.
Sourcepub fn refine(self, config: RefineConfig) -> Self
pub fn refine(self, config: RefineConfig) -> Self
Apply a RefineConfig — opts the engine
into the retrospective refine pass on DocumentPlan::render.
RefineConfig::off() (the default) is a no-op; the render path
produces byte-identical output to its non-refined form when the
config is off.
Sourcepub fn current_refine_config(&self) -> &RefineConfig
pub fn current_refine_config(&self) -> &RefineConfig
Read the currently-applied RefineConfig.
Sourcepub fn register_entity(&mut self, descriptor: EntityDescriptor)
pub fn register_entity(&mut self, descriptor: EntityDescriptor)
Register an entity descriptor for referring-expression generation
(REG). When the engine produces a Full form reference via the
{name|refer} pipe, it consults registered entities — if the
target shares its type with other registered entities, the Dale
& Reiter incremental algorithm selects the shortest set of
distinguishing attributes to include as premodifiers.
Entities not registered here still render with just type + name.
§Example
use prosaic_core::{Context, Engine, EntityDescriptor, Value};
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new());
engine.register_entity(
EntityDescriptor::new("UserService", "class")
.with_attribute("layer", "domain"),
);
engine.register_entity(
EntityDescriptor::new("AuthService", "class")
.with_attribute("layer", "infra"),
);
engine.register_template("t", "{name|refer} was modified").unwrap();
let mut ctx = Context::new();
ctx.insert("entity_type", Value::String("class".into()));
ctx.insert("name", Value::String("UserService".into()));
let mut session = prosaic_core::Session::new();
assert_eq!(
engine.render(&mut session, "t", &ctx).unwrap(),
"The domain class UserService was modified."
);Sourcepub fn attribute_preference(self, order: Vec<String>) -> Self
pub fn attribute_preference(self, order: Vec<String>) -> Self
Set the preferred attribute walking order for REG. Attributes earlier in the list are tried first; unknown attributes are ignored. Attributes not mentioned here still participate but fall to the end, in the order they were registered on the target entity.
Typical use: prefer semantic attributes (“layer”, “scope”) over physical ones (“color”) when disambiguating code entities.
Sourcepub fn reg_algorithm(self, algo: RegAlgorithm) -> Self
pub fn reg_algorithm(self, algo: RegAlgorithm) -> Self
Select the REG algorithm used when rendering Full-form references via
{name|refer}.
The default is RegAlgorithm::DaleReiter, which selects
distinguishing unary attributes only. Use RegAlgorithm::GraphBased
to enable the Krahmer 2003 greedy algorithm, which also considers
labeled relations registered via
EntityDescriptor::with_relation.
Sourcepub fn reference_time(self, unix_secs: i64) -> Self
pub fn reference_time(self, unix_secs: i64) -> Self
Override the engine’s “now” reference for relative-time rendering. Useful for tests and for rendering a report “as of” a specific point in time. The value is seconds since Unix epoch.
If not set, the engine reads SystemTime::now() on each call to
the {timestamp|relative} pipe.
§Example
use prosaic_core::{Context, Engine, Value};
use prosaic_grammar_en::English;
let now = 1_700_000_000;
let mut engine = Engine::new(English::new()).reference_time(now);
engine.register_template("t", "The change landed {ts|relative}").unwrap();
let mut ctx = Context::new();
ctx.insert("ts", Value::Number(now - 86400 - 3600));
let mut session = prosaic_core::Session::new();
assert_eq!(engine.render(&mut session, "t", &ctx).unwrap(), "The change landed yesterday.");Sourcepub fn register_partial(
&mut self,
name: &str,
source: &str,
) -> Result<(), ProsaicError>
pub fn register_partial( &mut self, name: &str, source: &str, ) -> Result<(), ProsaicError>
Register a reusable template fragment under name. Inside any
template, {>name} expands inline to the partial’s content at
render time. Partials share the same template syntax as the rest
of the engine — slots, pipes, conditionals, and nested partials
all work inside a partial.
Example:
engine.register_partial(
"impact_tail",
"{?consumer_count}, affecting {consumer_count} \
{consumer_count|pluralize:consumer}{/?}",
)?;
engine.register_template("code.modified", "{name|refer} was modified{>impact_tail}")?;
engine.register_template("code.renamed", "{name|refer} was renamed to {new_name}{>impact_tail}")?;Sourcepub fn smart_quotes(self, enabled: bool) -> Self
pub fn smart_quotes(self, enabled: bool) -> Self
Enable typographic (“smart”) quote substitution on rendered output.
Straight " becomes curly \u{201C}/\u{201D}; straight '
becomes \u{2018}/\u{2019}; apostrophes inside words (Alice’s,
it’s) become U+2019. Off by default — opt in for human-readable
prose output, leave disabled for code-like outputs.
§Example
use prosaic_core::{Context, Engine, Value};
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new()).smart_quotes(true);
engine.register_template("t", r#"Alice said "hello""#).unwrap();
let mut session = prosaic_core::Session::new();
let out = engine.render(&mut session, "t", Context::new()).unwrap();
assert!(out.contains('\u{201C}'));
assert!(out.contains('\u{201D}'));Sourcepub fn max_sentence_length(self, max_chars: usize) -> Self
pub fn max_sentence_length(self, max_chars: usize) -> Self
Cap the character length of any single rendered sentence. When a
sentence exceeds max_chars, the engine splits it at the latest
natural boundary (subordinate clauses introduced by “which”,
“affecting”, “impacting”, “requiring”; list prefixes like
“including”; em-dashes; explicit sentence breaks) and wraps the
remainder as a follow-up sentence with a light grammatical fix-up.
If no natural boundary exists inside the budget the sentence passes through unchanged — we never chop mid-word.
§Example
use prosaic_core::{Context, Engine};
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new()).max_sentence_length(60);
engine.register_template(
"t",
"The class UserService was renamed to AccountService, \
which impacts 6 consumers",
).unwrap();
let mut session = prosaic_core::Session::new();
let out = engine.render(&mut session, "t", Context::new()).unwrap();
assert!(out.contains("This impacts 6 consumers"));Sourcepub fn sentence_rhythm(self, enabled: bool) -> Self
pub fn sentence_rhythm(self, enabled: bool) -> Self
Toggle the sentence-rhythm cadence penalty layered on top of
choose-best scoring (Variation::Seeded/Random). Enabled by
default — the penalty discourages picking template variants whose
length closely matches recently emitted sentences, biasing prose
toward higher length variance / burstiness.
Disable this when downstream tooling needs the historical scoring behaviour (word-repetition only) — e.g. golden-output regressions taken before the rhythm pass landed.
Sourcepub fn register_antonym(&mut self, negative: &str, positive: &str)
pub fn register_antonym(&mut self, negative: &str, positive: &str)
Register a positive-framing antonym for a negative verb phrase.
The {phrase|negated} pipe will prefer the registered positive
form (e.g. “remained unchanged”) over the default “not {phrase}”
fallback (“was not modified”).
Matching is case-insensitive.
Sourcepub fn register_synonyms(&mut self, group: &[&str])
pub fn register_synonyms(&mut self, group: &[&str])
Register a group of synonym words for elegant variation. The
{word|syn} pipe will look up the input word in the registered
groups and pick whichever synonym from the group has appeared
least recently in output. Ties break toward registration order.
Example:
engine.register_synonyms(&["consumer", "dependent", "caller"]);
// Template: "{count} {consumer|syn}{count|pluralize:}"
// First render emits "consumer(s)"; next "dependent(s)"; next "caller(s)".Sourcepub fn with_faithfulness_gate(self, threshold: f32) -> Self
pub fn with_faithfulness_gate(self, threshold: f32) -> Self
Enable a runtime faithfulness gate on every render* call.
When set, each rendered output is scored against its input Context
and the selected template’s literal tokens using PARENT-style
precision + polarity checking. If the score’s precision falls below
threshold OR polarity tokens mismatch between source and output,
the render returns ProsaicError::FaithfulnessRejection and the
session state is restored as if the render had not occurred.
threshold is typically 1.0 (strict: every content token in the
output must be sourced from the context or template literals).
Values below 1.0 tolerate a fraction of unentailed tokens — useful
when the engine legitimately emits hedged phrasings that introduce
words not present in the input (e.g. “approximately”, “likely”).
Default: no gate (all renders pass).
§Example
use prosaic_core::{Context, Engine, ProsaicError, Value};
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new())
.with_faithfulness_gate(1.0);
engine.register_template("t", "{name} was modified").unwrap();
let mut ctx = Context::new();
ctx.insert("name", Value::String("UserService".into()));
let mut session = prosaic_core::Session::new();
// "modified" is in the template literal — renders faithfully.
assert!(engine.render(&mut session, "t", &ctx).is_ok());Sourcepub fn register_template(
&mut self,
key: &str,
source: &str,
) -> Result<(), ProsaicError>
pub fn register_template( &mut self, key: &str, source: &str, ) -> Result<(), ProsaicError>
Register a template string under a key with Medium salience. Multiple templates registered under the same key become alternatives for variation at that salience level.
§Example
use prosaic_core::{Context, Engine, Session, Value};
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new());
engine.register_template(
"count.items",
"You have {n} {n|pluralize:item}",
).unwrap();
let mut ctx = Context::new();
ctx.insert("n", Value::Number(3));
let mut session = Session::new();
assert_eq!(engine.render(&mut session, "count.items", &ctx).unwrap(), "You have 3 items.");Sourcepub fn register_template_at(
&mut self,
key: &str,
source: &str,
salience: Salience,
) -> Result<(), ProsaicError>
pub fn register_template_at( &mut self, key: &str, source: &str, salience: Salience, ) -> Result<(), ProsaicError>
Register a template at a specific salience level. The engine selects templates at the salience matching the rendered event’s magnitude.
Sourcepub fn register_template_with_language(
&mut self,
key: &str,
source: &str,
language: Option<&str>,
) -> Result<(), ProsaicError>
pub fn register_template_with_language( &mut self, key: &str, source: &str, language: Option<&str>, ) -> Result<(), ProsaicError>
Register a template variant tagged with a BCP-47 language code.
Variants registered with None language are language-agnostic
fallbacks. The engine’s Engine::language_preference biases
variant selection: when a preference is set and any variant
matches it, the engine picks among matching variants only.
Sourcepub fn register_template_with_style(
&mut self,
key: &str,
source: &str,
style: Option<&str>,
) -> Result<(), ProsaicError>
pub fn register_template_with_style( &mut self, key: &str, source: &str, style: Option<&str>, ) -> Result<(), ProsaicError>
Register a template variant tagged with a free-form style.
Variants registered with None style are unstyled fallbacks. The
engine’s Engine::style_preference biases variant selection
after language filtering and before salience filtering.
Sourcepub fn register_template_with_language_and_style(
&mut self,
key: &str,
source: &str,
language: Option<&str>,
style: Option<&str>,
) -> Result<(), ProsaicError>
pub fn register_template_with_language_and_style( &mut self, key: &str, source: &str, language: Option<&str>, style: Option<&str>, ) -> Result<(), ProsaicError>
Register a template variant tagged with both language and style.
Sourcepub fn register_template_with_language_at(
&mut self,
key: &str,
source: &str,
salience: Salience,
language: Option<&str>,
) -> Result<(), ProsaicError>
pub fn register_template_with_language_at( &mut self, key: &str, source: &str, salience: Salience, language: Option<&str>, ) -> Result<(), ProsaicError>
Salience-aware companion to Engine::register_template_with_language.
Sourcepub fn register_template_with_style_at(
&mut self,
key: &str,
source: &str,
salience: Salience,
style: Option<&str>,
) -> Result<(), ProsaicError>
pub fn register_template_with_style_at( &mut self, key: &str, source: &str, salience: Salience, style: Option<&str>, ) -> Result<(), ProsaicError>
Salience-aware companion to Engine::register_template_with_style.
Sourcepub fn register_template_with_language_and_style_at(
&mut self,
key: &str,
source: &str,
salience: Salience,
language: Option<&str>,
style: Option<&str>,
) -> Result<(), ProsaicError>
pub fn register_template_with_language_and_style_at( &mut self, key: &str, source: &str, salience: Salience, language: Option<&str>, style: Option<&str>, ) -> Result<(), ProsaicError>
Register a template variant with explicit salience, language, and style tags.
Sourcepub fn register_template_with_schema<T>(
&mut self,
key: &str,
source: &str,
) -> Result<(), ProsaicError>where
T: HasProsaicSchema + IntoContext,
pub fn register_template_with_schema<T>(
&mut self,
key: &str,
source: &str,
) -> Result<(), ProsaicError>where
T: HasProsaicSchema + IntoContext,
Register a template and cross-check every slot’s inferred type
against the static schema of T, a type that implements
IntoContext + HasProsaicSchema.
Slot types inferred from pipe chains (e.g. {count|pluralize:item}
implies count: Number) must be compatible with T’s schema. Use
this when loading templates dynamically (from JSON, disk, etc.) and
you want the same strong guarantees the prosaic_template! macro
provides at compile time.
Sourcepub fn has_template(&self, key: &str) -> bool
pub fn has_template(&self, key: &str) -> bool
Check whether a template is registered under the given key.
Pure read, no mutation. Useful for callers that want to skip rendering when no template matches (e.g. tracing bridges where not every event type has a registered template).
§Example
use prosaic_core::Engine;
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new());
engine.register_template("greet", "Hello {name}").unwrap();
assert!(engine.has_template("greet"));
assert!(!engine.has_template("farewell"));Sourcepub fn context_salience(&self, ctx: &Context) -> Salience
pub fn context_salience(&self, ctx: &Context) -> Salience
Compute the salience for a context using this engine’s thresholds.
Sourcepub fn render(
&self,
session: &mut Session,
key: &str,
context: impl IntoContext,
) -> Result<String, ProsaicError>
pub fn render( &self, session: &mut Session, key: &str, context: impl IntoContext, ) -> Result<String, ProsaicError>
Render a registered template with the given context.
The session tracks discourse state across calls: entity mentions,
template history, word frequency. Each call benefits from context
established by previous calls. Use session.reset() between unrelated
sequences.
Render is transactional: if any step fails (missing slot in Strict mode, unknown pipe, etc.), the discourse state is rolled back to what it was before the call. A caller that catches the error sees no residue from the failed render in subsequent output.
§Example
use prosaic_core::{Context, Engine, Session, Value, Variation};
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new()).variation(Variation::Fixed);
engine.register_template("greet", "Hello {name}").unwrap();
let mut session = Session::new();
let mut ctx = Context::new();
ctx.insert("name", Value::String("world".into()));
assert_eq!(engine.render(&mut session, "greet", &ctx).unwrap(), "Hello world");Sourcepub fn score_variants(
&self,
session: &mut Session,
key: &str,
context: impl IntoContext,
) -> Result<Vec<VariantScore>, ProsaicError>
pub fn score_variants( &self, session: &mut Session, key: &str, context: impl IntoContext, ) -> Result<Vec<VariantScore>, ProsaicError>
Score every registered variant for key against context, without
committing any render. Returns one VariantScore per variant
that matches the context’s salience bucket, with the
choose-best score the engine would compute and a selected flag
marking which one render() would currently emit.
Useful for template-author diagnostics (“why does variant B always win?”) and vocab-module lints. Does not mutate discourse state — the function snapshots and restores state around candidate rendering.
Sourcepub fn render_inline(
&self,
session: &mut Session,
source: &str,
context: impl IntoContext,
) -> Result<String, ProsaicError>
pub fn render_inline( &self, session: &mut Session, source: &str, context: impl IntoContext, ) -> Result<String, ProsaicError>
Render a one-off template string (not registered) with the given context.
Inline templates are state-isolated: they do not participate in discourse tracking (no connectives, no entity mentions, no list-style cycle advancement, no plural-focus flag changes) and do not consume template-variant / round-robin counters. The only side effect on the real session is that output words are recorded for repetition scoring, and only when the render succeeds — a failed inline render leaves the session exactly as it was before the call.
Implementation: render into a cloned session and discard it. On success, record output words on the caller’s session.
Sourcepub fn render_batch(
&self,
session: &mut Session,
events: &[(&str, Context)],
) -> Result<String, ProsaicError>
pub fn render_batch( &self, session: &mut Session, events: &[(&str, Context)], ) -> Result<String, ProsaicError>
Render a batch of events as a cohesive paragraph.
Each event is rendered sequentially through render(), which means
the discourse system produces natural cross-sentence flow via
referring expressions, connectives, template anti-repeat, and
list style cycling.
Additionally, consecutive events sharing a template key but with different entities are aggregated by combining subjects: “UserService and AuthService were renamed” instead of two sentences.
§Example — clause reduction across same-entity events
use prosaic_core::{Context, Engine, Value};
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new());
engine.register_template("renamed", "{name|refer} was renamed").unwrap();
engine.register_template("modified", "{name|refer} was modified").unwrap();
engine.register_template("moved", "{name|refer} was moved").unwrap();
let mut ctx = Context::new();
ctx.insert("entity_type", Value::String("class".into()));
ctx.insert("name", Value::String("UserService".into()));
let events: Vec<(&str, Context)> = vec![
("renamed", ctx.clone()),
("modified", ctx.clone()),
("moved", ctx.clone()),
];
let mut session = prosaic_core::Session::new();
let out = engine.render_batch(&mut session, &events).unwrap();
assert_eq!(
out,
"The class UserService was renamed, modified, and moved."
);Sourcepub fn render_batch_with_relations(
&self,
session: &mut Session,
events: &[(&str, Context, Option<RstRelation>)],
) -> Result<String, ProsaicError>
pub fn render_batch_with_relations( &self, session: &mut Session, events: &[(&str, Context, Option<RstRelation>)], ) -> Result<String, ProsaicError>
Render a batch of events where each event carries an optional RST relation describing its rhetorical link to the preceding event.
When a relation is present on events[i] (i ≥ 1), the corresponding
discourse marker is prepended to that sentence instead of a plain
space — e.g. “However, the class Foo was modified.” — and the leading
determiner of the rendered sentence is lowercased so the marker’s
capitalisation leads naturally.
When all relations are None, this method delegates to
Engine::render_batch so aggregation (clause-reduction,
subject-aggregation) still applies.
Sourcepub fn render_explained(
&self,
session: &mut Session,
key: &str,
context: impl IntoContext,
) -> Result<RenderExplanation, ProsaicError>
pub fn render_explained( &self, session: &mut Session, key: &str, context: impl IntoContext, ) -> Result<RenderExplanation, ProsaicError>
Render a template and return both the output and a
RenderExplanation describing the decisions the engine made.
Functionally equivalent to render() — discourse state advances
the same way; any error rolls back the same way — but each step’s
diagnostic is also captured. Use this for debugging template
behavior: “why did variant B win?”, “was this entity reference a
pronoun or short-name?”, “did the length budget split the
output?”.
Sourcepub fn render_iter<'a>(
&'a self,
session: &'a mut Session,
events: &'a [(&'a str, Context)],
) -> RenderIter<'a> ⓘ
pub fn render_iter<'a>( &'a self, session: &'a mut Session, events: &'a [(&'a str, Context)], ) -> RenderIter<'a> ⓘ
Iterator form of Engine::render_batch. Yields each sentence
(or aggregated run) as it is produced, so callers concerned with
time-to-first-sentence can stream output instead of waiting for
the full batch. Each .next() call produces one sentence —
which may correspond to multiple events (when aggregation or
clause reduction fires) — and returns None once the events
are exhausted.
Errors are terminal. If any render inside the run fails, the
iterator yields Some(Err(_)) exactly once and then returns None
on every subsequent call. This keeps semantics predictable even
when the failing event sits inside an aggregated, gapped, or
same-entity run whose earlier sentences already mutated session
state — replaying the run after partial success would compound
pronoun / anti-repetition state in unsafe ways. If you need
error-skipping behaviour, validate templates and contexts up
front (e.g. with Engine::score_variants) rather than relying
on the iterator to recover.
Sourcepub fn new_session(&self) -> Session
pub fn new_session(&self) -> Session
Create a new Session compatible with this engine.
Sugar for Session::new(). Equivalent, but clarifies intent at call
sites where a reader might wonder which session type to construct.
§Example
use prosaic_core::{Context, Engine, Value};
use prosaic_grammar_en::English;
let mut engine = Engine::new(English::new());
engine.register_template("hello", "Hello {name}!").unwrap();
let mut session = engine.new_session();
let mut ctx = Context::new();
ctx.insert("name", Value::String("world".into()));
assert_eq!(engine.render(&mut session, "hello", &ctx).unwrap(), "Hello world!");