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/compiler/schemas
- @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-generator
Core Functionality
Full Pipeline: Perseus JSON to QTI XML
This example demonstrates the primary workflow: converting a Perseus JSON object into a QTI 3.0 XML string. The buildPerseusEnvelope
helper automatically resolves web+graphie://
and standard https://
image URLs, fetches external SVG content, and embeds it for the AI.
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 { buildPerseusEnvelope } from "@superbuilders/qti-assessment-item-generator/structured/ai-context-builder";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
// Your Perseus-like JSON data
const perseusData = { /* ... your perseus item JSON ... */ };
// 1. Build the AI context envelope from the source data.
const envelope = await buildPerseusEnvelope(perseusData);
// 2. Generate the structured AssessmentItemInput object.
// The `widgetCollectionName` tells the AI which set of visual widgets to use.
const structuredItem = await generateFromEnvelope(openai, logger, envelope, "math-core");
// 3. Compile the structured item to QTI XML.
const xml = await compile(structuredItem);
console.log(xml);
Full Pipeline: HTML to QTI XML
This example converts raw HTML into a QTI 3.0 XML string. The buildHtmlEnvelope
helper is now async and fetches SVG content from external image URLs. Inline <svg>
embedded in the HTML remains as-is inside the primary HTML context and is not duplicated. <img>
src
URLs and the optional screenshotUrl
are validated: only absolute http
/https
URLs are accepted. Invalid <img>
src
values are skipped; an invalid screenshotUrl
will throw. Fetch timeout is 30 seconds.
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 { buildHtmlEnvelope } from "@superbuilders/qti-assessment-item-generator/structured/ai-context-builder";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const html = `
<div>
<p>Which of these is a prime number?</p>
<img src="https://example.com/some-diagram.png" alt="Diagram" />
</div>
`;
// Optional: provide a full screenshot for broader context.
const screenshotUrl = "https://example.com/item-screenshot.png";
// 1. Build the AI context envelope.
const envelope = await buildHtmlEnvelope(html, screenshotUrl);
// 2. Generate the structured AssessmentItemInput object.
const structuredItem = await generateFromEnvelope(openai, logger, envelope, "fourth-grade-math");
// 3. Compile the structured item to QTI XML.
const xml = await compile(structuredItem);
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 { differentiateAssessmentItem } from "@superbuilders/qti-assessment-item-generator/structured/differentiator";
import type { AssessmentItemInput } from "@superbuilders/qti-assessment-item-generator/compiler/schemas";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
// 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);
// `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 type { BarChartProps } from "@superbuilders/qti-assessment-item-generator/widgets/registry";
import { BarChartPropsSchema } from "@superbuilders/qti-assessment-item-generator/widgets/registry";
import type { Widget } from "@superbuilders/qti-assessment-item-generator/widgets/registry";
// 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. Pass the validated data to the generator (Widget union type)
const svgString = await generateWidget(validationResult.data as unknown as Widget);
console.log(svgString);
API Reference
structured
generateFromEnvelope(openai, logger, envelope, widgetCollectionName) => Promise<AssessmentItemInput>
- The main entry point for the AI pipeline. Converts an
AiContextEnvelope
into a structuredAssessmentItemInput
object. widgetCollectionName
is required and specifies which set of widgets the AI can use.
- The main entry point for the AI pipeline. Converts an
structured/ai-context-builder
buildPerseusEnvelope(perseusJson: unknown) => Promise<AiContextEnvelope>
- Creates an envelope from Perseus JSON. Resolves both
web+graphie://
and standardhttps://
image URLs, fetches external SVG content, and separates raster/vector URLs for the AI.
- Creates an envelope from Perseus JSON. Resolves both
buildHtmlEnvelope(html: string, screenshotUrl?: string) => Promise<AiContextEnvelope>
- Creates an envelope from raw HTML. Breaking change: now async. Fetches SVG content from external image URLs. Inline
<svg>
remains only in the primary HTML context (not extracted). Only absolutehttp
/https
URLs are accepted; invalid<img>
src
values are skipped, while an invalidscreenshotUrl
will throw. Fetch timeout is 30 seconds.
- Creates an envelope from raw HTML. Breaking change: now async. Fetches SVG content from external image URLs. Inline
structured/differentiator
differentiateAssessmentItem(openai, logger, item, n) => Promise<AssessmentItemInput[]>
- The main entry point for the AI differentiation pipeline. Generates
n
new variations of a structuredAssessmentItemInput
using a two-shot process: content planning (Shot 1) and widget generation via LLM using authoritative schemas (Shot 2). - This function is completely independent of
AiContextEnvelope
andwidgetCollectionName
.
- The main entry point for the AI differentiation pipeline. Generates
compiler
compile(item: AssessmentItemInput) => Promise<string>
- Compiles a structured
AssessmentItemInput
object into a valid QTI 3.0 XML string.
- Compiles a structured
widgets/widget-generator
generateWidget(widget: Widget) => Promise<string>
- Compiles a single widget's property object into an SVG or HTML string.
Widget Schemas and Types
You can import individual Zod schemas and TypeScript input
types for each widget, as well as a unified WidgetInput
type for any valid widget object. This is ideal for programmatically creating or validating widget configurations.
import { generateWidget } from "@superbuilders/qti-assessment-item-generator/widgets/widget-generator";
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;
}
// The `generateWidget` function accepts a `Widget` (runtime-inferred union)
const svg = await generateWidget(validationResult.data as unknown as Widget);
Image URL Policy
buildHtmlEnvelope
accepts only absolutehttp
/https
image URLs.<img>
elements with invalid/unsupportedsrc
values are skipped (not thrown).- If an optional
screenshotUrl
is provided but invalid or unsupported, it will throw. - Inline
<svg>
content embedded in HTML is left as-is inside the primary HTML context; only external.svg
image URLs are fetched and embedded as text.
Breaking Changes (v2.0.0)
This version includes breaking changes to the AI context pipeline:
AiContextEnvelope
interface: TheimageUrls
field has been renamed torasterImageUrls
, and a newvectorImageUrls
field has been added.buildHtmlEnvelope
function: Now async and performs network fetches for SVG content from<img>
sources.- Enhanced context: Both builders now provide richer context with explicit separation of raster and vector images, improving AI accuracy especially for widget mapping.
Migration Guide
Update
AiContextEnvelope
usage:// Before const envelope: AiContextEnvelope = { context: [...], imageUrls: [...] } // After const envelope: AiContextEnvelope = { context: [...], rasterImageUrls: [...], vectorImageUrls: [...] }
Make
buildHtmlEnvelope
calls async:// Before const envelope = buildHtmlEnvelope(html, screenshotUrl); // After const envelope = await buildHtmlEnvelope(html, screenshotUrl);
Configuration
OPENAI_API_KEY
: An OpenAI API key must be available as an environment variable.
Troubleshooting
Error: envelope context cannot be empty
: You calledgenerateFromEnvelope
with anAiContextEnvelope
that had no content. Ensure thecontext
array 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...
orError: caret characters banned...
: The compiler detected a|
or^
in plain text. This is a strict rule. Use adataTable
widget to create tables, and use MathML (<msup>
) for exponents.Error: duplicate response identifier found...
: The compiler found the sameresponseIdentifier
used 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 sameidentifier
used for multiple choices within a single interaction. Choice identifiers must be unique within their parent interaction.
License
0BSD