prosaic_core/lib.rs
1//! General-purpose natural language generation from structured data.
2//!
3//! Takes structured events and produces **natural-sounding** prose, not
4//! just grammatically correct output. The engine tracks discourse state
5//! across calls, so multiple renders flow together like human-written
6//! prose — using pronouns, varying phrasing, matching verbosity to
7//! impact, and structuring multi-paragraph narratives.
8//!
9//! English, Spanish, and German grammars ship out of the box via the
10//! `prosaic-grammar-en`, `-es`, and `-de` sibling crates. Add more
11//! languages by implementing the [`Language`] trait.
12//!
13//! # Quick start
14//!
15//! ```
16//! use prosaic_core::{Context, Engine, Session, Strictness, Value, Variation};
17//! use prosaic_grammar_en::English;
18//!
19//! let mut engine = Engine::new(English::new())
20//! .strictness(Strictness::Strict)
21//! .variation(Variation::Fixed);
22//!
23//! engine.register_template(
24//! "entity.renamed",
25//! "{old_name|refer} was renamed to {new_name}",
26//! ).unwrap();
27//!
28//! let mut ctx = Context::new();
29//! ctx.insert("entity_type", Value::String("class".into()));
30//! ctx.insert("old_name", Value::String("Foo".into()));
31//! ctx.insert("new_name", Value::String("Foobar".into()));
32//!
33//! let mut session = Session::new();
34//! let sentence = engine.render(&mut session, "entity.renamed", &ctx).unwrap();
35//! assert_eq!(sentence, "The class Foo was renamed to Foobar.");
36//! ```
37//!
38//! # Type-aware template validation
39//!
40//! Context types that derive `IntoContext` also get a `HasProsaicSchema`
41//! impl for free. Pair it with the `context:` argument of
42//! `prosaic_template!` to validate slot types at compile time:
43//!
44//! ```no_run
45//! use prosaic_core::{Engine, Session};
46//! use prosaic_derive::{IntoContext, prosaic_template};
47//! use prosaic_grammar_en::English;
48//!
49//! #[derive(IntoContext)]
50//! struct RenameCtx {
51//! old_name: String,
52//! new_name: String,
53//! consumer_count: i64,
54//! }
55//!
56//! // Compile error if `consumer_count` were declared as `String`:
57//! let tpl = prosaic_template! {
58//! template: "{old_name} → {new_name} ({consumer_count|pluralize:consumer})",
59//! slots: [old_name, new_name, consumer_count],
60//! context: RenameCtx,
61//! };
62//!
63//! let mut engine = Engine::new(English::new());
64//! engine.register_template("rename", tpl).unwrap();
65//! ```
66//!
67//! For templates loaded dynamically (JSON manifests, on-disk sources),
68//! use [`Engine::register_template_with_schema`] to get the same check
69//! at registration time.
70//!
71//! # Feature flags
72//!
73//! - `std` (default): `std::error::Error` on `ProsaicError`, `SystemTime::now()`
74//! fallbacks. Disable for `no_std + alloc` targets.
75//! - `time` (default): `{ts|relative}` and `{ts|since_last}` pipes.
76//! - `polish` (default): sentence-length budgeting and smart quotes.
77//! - `reg` (default): referring expression generation (Dale-Reiter + graph-based).
78//! - `serde` (off): `Serialize`/`Deserialize` on public types.
79//! - `parallel` (off): `DocumentPlan::render_parallel` via rayon.
80//!
81//! # `no_std` support
82//!
83//! Disable the `std` feature to compile under `no_std + alloc`:
84//!
85//! ```toml
86//! prosaic-core = { version = "0.2", default-features = false }
87//! ```
88//!
89//! Without the `std` feature:
90//! - `Variation::Random` falls back to `Variation::Fixed` (variant 0).
91//! - `{ts|relative}` and `{ts|since_last}` require `engine.reference_time()`.
92//! - `ProsaicError` does not implement `std::error::Error`.
93
94#![cfg_attr(not(feature = "std"), no_std)]
95
96extern crate alloc;
97
98pub mod agreement;
99mod antonyms;
100mod builder;
101mod collections;
102mod context;
103mod discourse;
104mod document;
105mod engine;
106mod error;
107mod faithfulness;
108mod hedge;
109mod language;
110#[cfg(feature = "polish")]
111mod length;
112mod proportion;
113#[cfg(feature = "polish")]
114mod punctuation;
115mod quantify;
116mod refine;
117mod refine_diagnosers;
118mod refine_score;
119#[cfg(feature = "reg")]
120mod reg;
121pub mod rst;
122mod salience;
123mod session;
124mod style;
125mod synonyms;
126mod template;
127#[cfg(feature = "time")]
128mod time;
129
130pub use agreement::{
131 AgreementFeatures, AgreementPerson, Animacy, Case, Definiteness, Gender,
132 Number as GrammaticalNumber,
133};
134pub use context::{Context, EntityValue, HasProsaicSchema, IntoValue, Value, entity};
135pub use faithfulness::{FaithfulnessScore, PolarityDrift, score_faithfulness};
136pub use language::{
137 Aspect, Conjunction, Language, Mood, Person, PluralCategory, Tense, VerbForm, Voice,
138 english_verb_phrase,
139};
140// assert_faithful! is exported via #[macro_export] in faithfulness.rs
141pub use antonyms::{AntonymRegistry, insert_not};
142pub use builder::{Clause, Sentence, Subject, named, subject};
143pub use context::IntoContext;
144pub use discourse::{Cf, DiscourseState, ListStyle, ReferenceForm, Transition};
145pub use document::{
146 DocumentPlan, GroupingStrategy, Paragraph, RhetoricalCategory, default_classifier,
147};
148#[cfg(feature = "reg")]
149pub use engine::RegAlgorithm;
150pub use engine::{Engine, RenderExplanation, RenderIter, Strictness, VariantScore, Variation};
151pub use error::ProsaicError;
152pub use hedge::{HedgeMode, hedge};
153#[cfg(feature = "polish")]
154pub use length::split_long;
155pub use proportion::english_proportion;
156pub use prosaic_common::{ValueType, pipe_spec, schema_lookup, types_compatible};
157#[cfg(feature = "polish")]
158pub use punctuation::{em_dash_nested_parentheticals, smart_quotes};
159pub use quantify::{QuantifyMode, quantify};
160pub use refine::{
161 Diagnoser, Diagnostic, RefineConfig, RefineConstraint, RefineOutcome, RefineWeights,
162 RenderedDocument, RenderedParagraph, RenderedSentence, UsedConnective, UsedListStyle,
163};
164pub use refine_diagnosers::{
165 ConnectiveFamilySaturation, DocumentScopeRhythm, ListStyleFatigue, ParagraphOpenerMonotony,
166 ProfileDistributionDrift, RstRelationImbalance,
167};
168pub use refine_score::score_document;
169#[cfg(feature = "reg")]
170pub use reg::{
171 EntityDescriptor, EntityRegistry, SubgraphDescription, distinguishing_attributes,
172 distinguishing_subgraph,
173};
174
175// Implementation details consumed by the `prosaic_template!` macro at
176// expansion time. Not intended for direct use — the public contract goes
177// through `ValueType` and `HasProsaicSchema`.
178#[doc(hidden)]
179pub use prosaic_common::{PIPE_SPECS, PipeSpec};
180pub use rst::RstRelation;
181pub use salience::{Salience, SalienceThresholds};
182pub use session::Session;
183pub use style::{
184 ConnectivePreferences, HedgingCalibration, LengthDistribution, ListStyleBias, PronounDensity,
185 SalienceBias, StyleProfile, StyleProfileBuilder, StyleProfileError, Verbosity,
186};
187pub use synonyms::SynonymRegistry;
188pub use template::{BareSegment, Pipe, PipeArg, Template};
189#[cfg(feature = "time")]
190pub use time::format_relative;
191
192#[cfg(test)]
193mod common_reexport_tests {
194 //! Sanity checks that `prosaic-common`'s public surface is visible
195 //! through `prosaic-core`'s re-exports. Each test invokes a re-exported
196 //! fn through a non-trivial code path so a broken re-export or a
197 //! behavioural regression in `prosaic-common` surfaces here.
198
199 use super::*;
200
201 #[test]
202 fn pipe_specs_length_matches_registry() {
203 assert_eq!(PIPE_SPECS.len(), 20);
204 }
205
206 #[test]
207 fn pipe_spec_lookup_round_trips_through_reexport() {
208 let p = pipe_spec("pluralize").expect("pluralize must resolve via re-export");
209 assert_eq!(p.input, ValueType::Number);
210 assert_eq!(p.output, ValueType::String);
211 }
212
213 #[test]
214 fn types_compatible_via_reexport_rejects_mismatches() {
215 // Any + concrete → compatible; distinct concretes → not.
216 assert!(types_compatible(ValueType::Any, ValueType::Number));
217 assert!(!types_compatible(ValueType::Number, ValueType::List));
218 }
219
220 #[test]
221 fn schema_lookup_via_reexport_finds_keys() {
222 let schema: &[(&str, ValueType)] = &[("x", ValueType::Number)];
223 assert_eq!(schema_lookup(schema, "x"), Some(ValueType::Number));
224 assert_eq!(schema_lookup(schema, "missing"), None);
225 }
226
227 #[test]
228 fn pipe_spec_struct_is_constructible_via_reexport() {
229 // Confirms the struct re-export is usable as a type, not just a name.
230 let p = PipeSpec {
231 name: "test",
232 input: ValueType::Any,
233 output: ValueType::String,
234 };
235 assert_eq!(p.name, "test");
236 assert_eq!(p.input, ValueType::Any);
237 }
238}