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.none
  • FragmentDeduplication.byId
  • FragmentDeduplication.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:

  1. PromptFragment.variables
  2. PromptRecipe.variables
  3. PromptCompositionOptions.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.