aigc_prompt_composer
aigc_prompt_composer is an extensible, pure Dart toolkit for building
deterministic prompts from reusable, localized fragments. It works in Flutter,
server, and command-line Dart applications without depending on Flutter.
The package deliberately focuses on composition. It contains no model clients, networking, persistence, user interface, analytics, billing, or vendor SDKs.
Features
- User-defined section identifiers instead of a closed section enum.
- Stable section and fragment ordering with explicit tie-breakers.
- Fragment deduplication by identifier or resolved content.
{{variable}}substitution with fragment, recipe, and operation precedence.- Required and optional sections, single-fragment constraints, and conflicts.
- Locale selection with parent-locale and configured fallback chains.
- Immutable models with JSON serialization round trips.
- Plain-text and structured JSON renderers.
- Custom validator and renderer extension points.
- Strict composition with typed, recoverable validation exceptions.
- Lazy and materialized batch composition with complete strict-failure results.
Installation
Add the package to pubspec.yaml:
dependencies:
aigc_prompt_composer: ^0.1.1
Then run:
dart pub get
Quick start
import 'package:aigc_prompt_composer/aigc_prompt_composer.dart';
void main() {
final schema = PromptSchema(
id: 'image.v1',
version: '1',
defaultLocale: 'en',
sections: <PromptSectionDefinition>[
PromptSectionDefinition(
id: 'subject',
order: 10,
required: true,
localizedHeaders: const <String, String>{'en': 'Subject'},
),
PromptSectionDefinition(
id: 'lighting',
order: 20,
localizedHeaders: const <String, String>{'en': 'Lighting'},
),
],
);
final recipe = PromptRecipe(
schemaId: schema.id,
variables: const <String, Object?>{'material': 'frosted glass'},
fragments: <PromptFragment>[
PromptFragment.text(
id: 'main-subject',
sectionId: 'subject',
content: 'A {{material}} sculpture on a stone plinth',
),
PromptFragment.text(
id: 'soft-light',
sectionId: 'lighting',
content: 'Soft window light with gentle shadows',
),
],
);
final result = PromptComposer().compose(schema: schema, recipe: recipe);
final text = const PlainTextPromptRenderer().render(result);
print(text);
}
The resulting text is deterministic:
Subject:
A frosted glass sculpture on a stone plinth
Lighting:
Soft window light with gentle shadows
Ordering and deduplication
Sections are sorted by PromptSectionDefinition.order, then by section id.
Fragments are sorted by their section, PromptFragment.order, fragment id, and
finally original position. This produces stable output even when collections
arrive in a different order.
PromptCompositionOptions.deduplication accepts:
FragmentDeduplication.noneFragmentDeduplication.byIdFragmentDeduplication.byResolvedContent
Resolved-content deduplication is scoped to a section.
Variables
Fragments use {{name}} placeholders. Values are merged in this order, with
later layers taking precedence:
PromptFragment.variablesPromptRecipe.variablesPromptCompositionOptions.variables
Missing variables produce an error issue. The placeholder is preserved by
default; use MissingVariableBehavior.emptyString to remove it from output.
Locales and fallback
The requested locale is selected from composition options, the recipe, then the
schema default. Locale tags are compared case-insensitively, _ and - are
treated equally, and parent tags are tried from most to least specific.
For example, zh-Hant-HK tries zh-Hant-HK, zh-Hant, then zh. Operation
fallbacks are tried next, followed by schema fallbacks and the schema default.
If nothing matches, the lexicographically first available locale is selected so
the result remains deterministic. A fallback emits an informational issue.
Validation
ComposedPrompt.issues contains machine-readable diagnostics. Built-in checks
cover schema identity and version, duplicate identifiers, unknown sections,
required sections, section multiplicity, missing localized content, unresolved
variables, and section or fragment conflicts.
Composition returns the best available output alongside diagnostics. Check
ComposedPrompt.isValid or ComposedPrompt.hasErrors before sending output to
another system.
Compare built-in codes with PromptIssueCodes constants. The complete catalog
and severity contract are documented in Validation.
Use issuesWhere() or hasIssue() for exact, composable diagnostic queries:
final missingSubject = result.hasIssue(
code: PromptIssueCodes.requiredSectionMissing,
sectionId: 'subject',
);
final warnings = result.issuesWhere(
severity: PromptIssueSeverity.warning,
);
Add application rules by implementing PromptValidator:
final class MaximumLengthValidator implements PromptValidator {
MaximumLengthValidator(this.maximumLength);
final int maximumLength;
@override
Iterable<PromptValidationIssue> validate(PromptValidationContext context) {
final length = context.sections
.expand((section) => section.fragments)
.fold<int>(0, (sum, fragment) => sum + fragment.content.length);
if (length <= maximumLength) return const <PromptValidationIssue>[];
return <PromptValidationIssue>[
PromptValidationIssue(
code: 'maximum_length_exceeded',
message: 'Resolved content is longer than allowed.',
severity: PromptIssueSeverity.error,
details: <String, Object?>{'maximumLength': maximumLength},
),
];
}
}
Pass validators to PromptComposer(validators: [...]).
Strict composition
Use composeOrThrow() when invalid output must never reach a renderer or
downstream system:
try {
final prompt = PromptComposer().composeOrThrow(
schema: schema,
recipe: recipe,
);
print(const PlainTextPromptRenderer().render(prompt));
} on PromptCompositionException catch (error) {
for (final issue in error.errors) {
print('${issue.code}: ${issue.message}');
}
}
Only error-severity diagnostics throw. Warnings and informational diagnostics
remain available through warningIssues and infoIssues. The exception keeps
the complete partial prompt, allowing callers to log or inspect the exact
composition result without recomputing it.
Batch composition
Use composeEach() to process large recipe iterables lazily without retaining
the complete batch. Composition starts when the iterable is consumed, preserves
recipe order, and does not cache results:
final prompts = PromptComposer().composeEach(
schema: schema,
recipes: recipes,
options: PromptCompositionOptions(locale: 'en'),
);
final renderer = const PlainTextPromptRenderer();
for (final prompt in prompts) {
print(renderer.render(prompt));
}
Use composeAll() when callers need an immutable, reusable list. It accepts
the same shared schema and operation options:
final prompts = PromptComposer().composeAll(
schema: schema,
recipes: recipes,
options: PromptCompositionOptions(locale: 'en'),
);
composeAllOrThrow() composes the complete batch before throwing. A
PromptBatchCompositionException exposes every result through prompts, the
zero-based invalidIndexes, and the flattened error diagnostics through
errors. This allows callers to retain successful results and retry only the
invalid inputs.
Rendering
Use PlainTextPromptRenderer for display-ready text. Use
StructuredJsonPromptRenderer when another component needs typed section,
fragment, locale, metadata, and diagnostic data.
Implement PromptRenderer<T> to render any application-specific output type.
Serialization
All configuration and result models provide toJson() and fromJson().
Values are JSON-compatible maps rather than encoded strings, so callers remain
in control of storage and transport.
Top-level payloads include formatVersion. See
Serialization for version and compatibility rules.
Examples
Design references:
Development
dart pub get
dart format --set-exit-if-changed .
dart analyze --fatal-infos
dart test
dart doc
dart pub publish --dry-run
Additional quality checks:
dart test --coverage=coverage
dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib
dart run tool/check_coverage.dart coverage/lcov.info 90
dart run tool/consumer_smoke_test.dart
dart run benchmark/composition_benchmark.dart
License
BSD 3-Clause. See LICENSE.
Libraries
- aigc_prompt_composer
- Extensible building blocks for deterministic prompt composition.