Package Exports
- @superbuilders/qti-assessment-item-generator/compiler
- @superbuilders/qti-assessment-item-generator/compiler/qti-constants
- @superbuilders/qti-assessment-item-generator/compiler/response-processor
- @superbuilders/qti-assessment-item-generator/core
- @superbuilders/qti-assessment-item-generator/core/content
- @superbuilders/qti-assessment-item-generator/core/feedback
- @superbuilders/qti-assessment-item-generator/core/interactions
- @superbuilders/qti-assessment-item-generator/core/item
- @superbuilders/qti-assessment-item-generator/examples
- @superbuilders/qti-assessment-item-generator/package.json
- @superbuilders/qti-assessment-item-generator/qti-validation/utils
- @superbuilders/qti-assessment-item-generator/structured
- @superbuilders/qti-assessment-item-generator/structured/ai-context-builder
- @superbuilders/qti-assessment-item-generator/structured/differentiator
- @superbuilders/qti-assessment-item-generator/widgets/collections
- @superbuilders/qti-assessment-item-generator/widgets/registry
- @superbuilders/qti-assessment-item-generator/widgets/types
- @superbuilders/qti-assessment-item-generator/widgets/widget-generator
Readme
@superbuilders/qti-assessment-item-generator
A robust toolkit for generating and compiling QTI 3.0 assessment items from various sources using a structured AI pipeline.
- Runtime: Bun
- Package Manager: Bun
Install
bun add @superbuilders/qti-assessment-item-generatorCore Functionality
Full Pipeline: Generating QTI from an AI Context Envelope
The primary workflow involves creating an AiContextEnvelope object and passing it to the generateFromEnvelope function. This envelope is a self-contained representation of the source content, including primary HTML or JSON, supplementary vector graphics, and any raster images.
This approach gives you full control and supports completely offline generation by allowing you to provide raster images as in-memory binary payloads or data: URLs, eliminating all network dependencies.
import fs from "fs/promises";
import OpenAI from "openai";
import * as logger from "@superbuilders/slog";
import { generateFromEnvelope } from "@superbuilders/qti-assessment-item-generator/structured";
import { compile } from "@superbuilders/qti-assessment-item-generator/compiler";
import { widgetCollections } from "@superbuilders/qti-assessment-item-generator/widgets/collections";
// ---
// In an offline pipeline, you load all assets from a local, trusted source.
// ---
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const WIDGETS = widgetCollections["fourth-grade-math"]; // Choose a collection
// 1. Load primary HTML and supplementary SVG content from disk.
const html = await fs.readFile("/abs/path/to/item.html", "utf8");
const svgText = await fs.readFile("/abs/path/to/diagram.svg", "utf8");
// 2. Load a raster image (e.g., a screenshot) and create a payload.
const screenshotBytes = await fs.readFile("/abs/path/to/screenshot.png");
const screenshotPayload = {
  data: await new Blob([screenshotBytes]).arrayBuffer(),
  mimeType: "image/png",
};
// You can also include images as data URLs.
const extraImageBytes = await fs.readFile("/path/to/figure.png");
const extraImageDataUrl = `data:image/png;base64,${extraImageBytes.toString("base64")}`;
// 3. Manually build the offline envelope. No network calls will be made by the library.
const envelope = {
  primaryContent: html,
  supplementaryContent: [svgText], // Vector graphics go here as raw text
  multimodalImageUrls: [extraImageDataUrl], // Raster images can be data: URLs
  multimodalImagePayloads: [screenshotPayload], // Or raw binary payloads
  pdfPayloads: []
};
// 4. Generate the structured item.
const structuredItem = await generateFromEnvelope(openai, logger, envelope, WIDGETS);
// 5. Compile to QTI XML.
const xml = await compile(structuredItem, WIDGETS);
console.log(xml);AI-Powered Item Differentiation
Generate unique variations of an existing assessment item using a robust, two-shot pipeline that separates content planning from visual generation. This process is fully independent of the AiContextEnvelope used for initial item creation.
import OpenAI from "openai";
import * as logger from "@superbuilders/slog";
import { widgetCollections } from "@superbuilders/qti-assessment-item-generator/widgets/collections";
import { differentiateAssessmentItem } from "@superbuilders/qti-assessment-item-generator/structured/differentiator";
import type { AssessmentItemInput } from "@superbuilders/qti-assessment-item-generator/core/item";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const WIDGETS = widgetCollections["fourth-grade-math"]; // choose a collection matching your item
// Assume `sourceItem` is a valid AssessmentItemInput object you already have.
const sourceItem: AssessmentItemInput = { /* ... */ };
// To generate 3 new variations of the source item; Shot 2 will generate widgets via LLM using authoritative schemas:
const items = await differentiateAssessmentItem(openai, logger, sourceItem, 3, WIDGETS);
// `items` is now an array of up to 3 valid AssessmentItemInput objects.
console.log(`Generated ${items.length} new items with regenerated widgets.`);Standalone Widget Compilation
Compile a widget configuration directly into an SVG or HTML string without using the AI pipeline. This is useful for rendering visual aids independently.
import { generateWidget } from "@superbuilders/qti-assessment-item-generator/widgets/widget-generator";
import { widgetCollections } from "@superbuilders/qti-assessment-item-generator/widgets/collections";
import { BarChartPropsSchema, type BarChartProps } from "@superbuilders/qti-assessment-item-generator/widgets/registry";
const WIDGETS = widgetCollections["fourth-grade-math"]; // select a collection
// 1. Define the properties for the widget you want to compile.
const barChartProps: BarChartProps = {
  type: "barChart",
  width: 480,
  height: 340,
  title: "Games Won Last Summer",
  xAxisLabel: "Team",
  yAxis: {
    label: "Games Won",
    min: 0,
    max: 16,
    tickInterval: 2
  },
  data: [
    { label: "Lions", value: 14, state: "normal" },
    { label: "Tigers", value: 2, state: "normal" },
    { label: "Bears", value: 7, state: "normal" }
  ],
  barColor: "#4285F4"
};
// 2. Validate the configuration against the specific Zod schema
const validationResult = BarChartPropsSchema.safeParse(barChartProps);
if (!validationResult.success) {
  throw validationResult.error;
}
// 3. Call the generator with a collection and a widget type key
const svgString = await generateWidget(WIDGETS, "barChart", validationResult.data);
console.log(svgString);API Reference
structured
structured/ai-context-builder
- buildPerseusEnvelope(perseusJson, fetch?) => Promise<AiContextEnvelope>
- buildMathacademyEnvelope(html, screenshotUrl?, fetch?) => Promise<AiContextEnvelope>- Helpers to construct AiContextEnvelopeobjects from common content sources. Returned envelopes includeprimaryContent,supplementaryContent,multimodalImageUrls,multimodalImagePayloads, andpdfPayloads(empty by default).
 
- Helpers to construct 
widgets/collections
- widgetCollections— a map of built-in collections:- "all",- "science",- "simple-visual",- "fourth-grade-math",- "teks-math-4"
- Each collection exposes name,widgets, andwidgetTypeKeysused by the APIs above.
 
- generateFromEnvelope(openai, logger, envelope, widgetCollection) => Promise<AssessmentItemInput>- Converts an AiContextEnvelopeinto anAssessmentItemInputusing the provided widget collection.
- widgetCollectionmust be one of the exported collections from- widgets/collections.
 
- Converts an 
structured/differentiator
- differentiateAssessmentItem(openai, logger, item, n, widgetCollection) => Promise<AssessmentItemInput[]>- Generates nnew variations of a structuredAssessmentItemInputvia a two-shot process (plan + widgets) using the providedwidgetCollection.
- Independent of AiContextEnvelope.
 
- Generates 
compiler
- compile(item: AssessmentItemInput, widgetCollection) => Promise<string>- Compiles a structured AssessmentItemInputinto QTI 3.0 XML. AwidgetCollectionis required for schema enforcement and widget content generation.
 
- Compiles a structured 
widgets/widget-generator
- generateWidget(widgetCollection, widgetType, data) => Promise<string>- Renders a single widget given a collection, a widget type key (e.g., "barChart"), and pre-validated data.
 
- Renders a single widget given a collection, a widget type key (e.g., 
Widget Schemas and Types
You can import individual Zod schemas and TypeScript types for each domain directly from the @superbuilders/qti-assessment-item-generator/core entry point. This is ideal for programmatically creating or validating configurations.
Breaking Change:
Old Import:
import { type AssessmentItemInput } from "@superbuilders/qti-assessment-item-generator/compiler/schemas";New Import:
import { type AssessmentItemInput } from "@superbuilders/qti-assessment-item-generator/core/item";You can also import content, feedback, and interaction types:
import { generateWidget } from "@superbuilders/qti-assessment-item-generator/widgets/widget-generator";
import { compile } from "@superbuilders/qti-assessment-item-generator/compiler";
import { widgetCollections } from "@superbuilders/qti-assessment-item-generator/widgets/collections";
import {
  type AssessmentItemInput,
  createDynamicAssessmentItemSchema
} from "@superbuilders/qti-assessment-item-generator/core/item";
import {
  BarChartPropsSchema,
  type BarChartProps,
  type WidgetInput,
  type Widget
} from "@superbuilders/qti-assessment-item-generator/widgets/registry";
// Example of creating a specific widget with full type-safety
const myBarChart: BarChartProps = {
  type: "barChart",
  width: 480,
  height: 340,
  title: "Games Won Last Summer",
  xAxisLabel: "Team",
  yAxis: {
    label: "Games Won",
    min: 0,
    max: 16,
    tickInterval: 2
  },
  data: [
    { label: "Lions", value: 14, state: "normal" },
    { label: "Tigers", value: 2, state: "normal" },
    { label: "Bears", value: 7, state: "normal" }
  ],
  barColor: "#4285F4"
};
// Validate the configuration against the specific Zod schema
const validationResult = BarChartPropsSchema.safeParse(myBarChart);
if (!validationResult.success) {
  throw validationResult.error;
}
const WIDGETS = widgetCollections["fourth-grade-math"];
const svg = await generateWidget(WIDGETS, "barChart", validationResult.data);Image and Content Policy
The AiContextEnvelope object separates content by type to ensure proper processing:
- primaryContent: The main HTML or Perseus JSON content for the assessment item.
- supplementaryContent: An array of strings, where each string is the full text content of a vector graphic (e.g., an entire- .svgor- .xmlfile).
- multimodalImageUrls: An array of strings for raster images. Each URL must be an absolute- http/- httpsURL or a- data:URL.
- multimodalImagePayloads: An array of- RasterImagePayloadobjects for providing raster images as in-memory binary data. This is the recommended approach for fully offline workflows. Each payload must contain the raw- ArrayBufferdata and its corresponding MIME type (e.g.,- image/png,- image/jpeg).
- pdfPayloads: An array of PDF payloads- { name: string; data: ArrayBuffer }for including PDFs in the AI context. May be empty.
Configuration
- OPENAI_API_KEY: An OpenAI API key must be available as an environment variable.
Troubleshooting
- Error: primaryContent cannot be empty: You called- generateFromEnvelopewith an- AiContextEnvelopethat had no primary content. Ensure the- primaryContentstring is not empty.
- Error: Model returned no parsed content: The OpenAI API call succeeded but returned an empty or unparsable response. Check your API key, model access, and inspect the logs for more details.
- Error: Slot declaration mismatch detected...: This is an internal AI error where the generated plan is inconsistent with the generated content. This error is typically non-retriable for the given input.
- Error: ...must have at least 2 choices...: An interaction like a multiple-choice or ordering question was generated with fewer than two choices, which is invalid.
- Error: pipe characters banned...or- Error: caret characters banned...: The compiler detected a- |or- ^in plain text. This is a strict rule. Use a- tableRichcontent block to create tables, and use MathML (- <msup>) for exponents.
- Error: duplicate response identifier found...: The compiler found the same- responseIdentifierused for multiple interactions or input fields within the same item. All response identifiers must be unique.
- Error: duplicate choice identifiers within interaction...: The compiler found the same- identifierused for multiple choices within a single interaction. Choice identifiers must be unique within their parent interaction.
License
0BSD