UNPKG

@openfloor/protocol

Version:

Open Floor Protocol implementation for JavaScript/TypeScript - enables interoperable multi-agent conversations

466 lines 15.8 kB
/** * @fileoverview Dialog Event implementation for the Open Floor Protocol * Implements the Dialog Event Object Specification Version 1.0.2 * @author Open Voice Interoperability Initiative * @version 0.0.1 * @license Apache-2.0 */ import { parseIsoDuration, millisecondsToIsoDuration, resolveJsonPath, isValidConfidence, isValidEncoding, createValidationError } from './utils'; /** * Represents a time span for a dialog event or token according to the Open Floor specification. * Time spans can be absolute (using Date objects) or relative (using duration offsets). * * @example * ```typescript * // Absolute time span * const span1 = new Span({ startTime: new Date(), endTime: new Date(Date.now() + 5000) }); * * // Relative time span * const span2 = new Span({ startOffset: 1000, endOffset: 5000 }); * ``` */ export class Span { startTime; startOffset; endTime; endOffset; /** * Creates a new Span instance * @param options - Span configuration options * @throws Error if invalid combination of time parameters is provided */ constructor(options = {}) { const { startTime, startOffset, endTime, endOffset } = options; if (!startTime && startOffset === undefined) { this.startTime = new Date(); } else { if (startTime !== undefined) this.startTime = startTime; if (startOffset !== undefined) this.startOffset = startOffset; } if (endTime !== undefined) this.endTime = endTime; if (endOffset !== undefined) this.endOffset = endOffset; if (this.startTime && this.startOffset !== undefined) { throw new Error(createValidationError('Span', 'both startTime and startOffset', 'either startTime or startOffset, not both')); } if (this.endTime && this.endOffset !== undefined) { throw new Error(createValidationError('Span', 'both endTime and endOffset', 'either endTime or endOffset, not both')); } if (!this.startTime && this.startOffset === undefined) { throw new Error(createValidationError('Span', 'neither startTime nor startOffset', 'either startTime or startOffset')); } } /** * Convert to plain object for JSON serialization */ toObject() { const result = {}; if (this.startTime) { result.startTime = this.startTime.toISOString(); } if (this.startOffset !== undefined) { result.startOffset = millisecondsToIsoDuration(this.startOffset); } if (this.endTime) { result.endTime = this.endTime.toISOString(); } if (this.endOffset !== undefined) { result.endOffset = millisecondsToIsoDuration(this.endOffset); } return result; } /** * Convert to JSON string */ toJSON() { return JSON.stringify(this.toObject()); } /** * Creates a Span instance from a dictionary with automatic type conversion */ static fromObject(data) { const options = {}; if (typeof data.startTime === 'string') { options.startTime = new Date(data.startTime); } else if (data.startTime instanceof Date) { options.startTime = data.startTime; } if (typeof data.endTime === 'string') { options.endTime = new Date(data.endTime); } else if (data.endTime instanceof Date) { options.endTime = data.endTime; } if (typeof data.startOffset === 'string') { options.startOffset = parseIsoDuration(data.startOffset); } else if (typeof data.startOffset === 'number') { options.startOffset = data.startOffset; } if (typeof data.endOffset === 'string') { options.endOffset = parseIsoDuration(data.endOffset); } else if (typeof data.endOffset === 'number') { options.endOffset = data.endOffset; } return new Span(options); } } /** * Represents a single token in a dialog event feature with optional metadata. * Tokens are the fundamental units of information within features. * * @example * ```typescript * // Simple text token * const token1 = new Token({ value: "Hello world" }); * * // Token with confidence and span * const token2 = new Token({ * value: "Hello", * confidence: 0.95, * span: { startOffset: 0, endOffset: 1000 } * }); * * // Token with links to other features * const token3 = new Token({ * value: { intent: "greeting" }, * links: ["$.textFeature.tokens[0].value"] * }); * ``` */ export class Token { value; valueUrl; span; confidence; links; /** * Creates a new Token instance * @param options - Token configuration options * @throws Error if invalid configuration is provided */ constructor(options = {}) { const { value, valueUrl, span, confidence, links = [] } = options; this.value = value; if (valueUrl !== undefined) this.valueUrl = valueUrl; if (span !== undefined) this.span = new Span(span); if (confidence !== undefined) this.confidence = confidence; this.links = Object.freeze([...links]); if (this.value === undefined && !this.valueUrl) { throw new Error(createValidationError('Token', 'neither value nor valueUrl', 'either value or valueUrl')); } if (this.value !== undefined && this.valueUrl) { throw new Error(createValidationError('Token', 'both value and valueUrl', 'either value or valueUrl, not both')); } if (this.confidence !== undefined && !isValidConfidence(this.confidence)) { throw new Error(createValidationError('Token.confidence', this.confidence, 'number between 0 and 1')); } } /** * Convert to plain object for JSON serialization */ toObject() { const result = {}; if (this.value !== undefined) { result.value = this.value; } if (this.valueUrl) { result.valueUrl = this.valueUrl; } if (this.span) { result.span = this.span.toObject(); } if (this.confidence !== undefined) { result.confidence = this.confidence; } if (this.links.length > 0) { result.links = [...this.links]; } return result; } /** * Convert to JSON string */ toJSON() { return JSON.stringify(this.toObject()); } /** * Creates a Token instance from a dictionary with nested object conversion */ static fromObject(data) { const options = { ...data }; if (data.span && typeof data.span === 'object') { options.span = Span.fromObject(data.span).toObject(); } return new Token(options); } /** * Resolves JSON Path links within a dialog event to find linked values * @param dialogEvent - The parent dialog event to search within * @returns Array of [path, value] pairs for matched links */ getLinkedValues(dialogEvent) { const values = []; const featuresDict = Object.fromEntries(Object.entries(dialogEvent.features).map(([key, feature]) => [key, feature.toObject()])); for (const linkPath of this.links) { try { const results = resolveJsonPath(linkPath, featuresDict); for (const result of results) { values.push([linkPath, result]); } } catch (error) { console.warn(`Failed to resolve JSON path "${linkPath}":`, error); } } return values; } } /** * Represents a feature in a dialog event according to the Open Floor specification. * Features contain tokens and metadata about different aspects of the dialog event. * * @example * ```typescript * // Text feature with multiple tokens * const feature = new Feature({ * mimeType: 'text/plain', * tokens: [ * { value: 'Hello' }, * { value: 'world' } * ] * }); * * // Feature with alternatives and language * const feature2 = new Feature({ * mimeType: 'text/plain', * lang: 'en-US', * tokens: [{ value: 'Hello', confidence: 0.95 }], * alternates: [[{ value: 'Hi', confidence: 0.85 }]] * }); * ``` */ export class Feature { mimeType; tokens; alternates; lang; encoding; tokenSchema; /** * Creates a new Feature instance * @param options - Feature configuration options * @throws Error if invalid configuration is provided */ constructor(options) { const { mimeType, tokens, alternates = [], lang, encoding, tokenSchema } = options; if (!mimeType) throw new Error('Feature.mimeType is required'); if (!tokens) throw new Error('Feature.tokens is required'); this.mimeType = mimeType; this.tokens = Object.freeze(tokens.map(token => new Token(token))); this.alternates = Object.freeze(alternates.map(arr => Object.freeze(arr.map(token => new Token(token))))); if (lang !== undefined) this.lang = lang; if (encoding !== undefined) this.encoding = encoding; if (tokenSchema !== undefined) this.tokenSchema = tokenSchema; if (encoding && !isValidEncoding(encoding)) { throw new Error(createValidationError('Feature.encoding', encoding, '"ISO-8859-1", "iso-8859-1", "UTF-8", or "utf-8"')); } } /** * Convert to plain object for JSON serialization */ toObject() { const result = { mimeType: this.mimeType, tokens: this.tokens.map(token => token.toObject()) }; if (this.alternates.length > 0) { result.alternates = this.alternates.map(alt => alt.map(token => token.toObject())); } if (this.lang) { result.lang = this.lang; } if (this.encoding) { result.encoding = this.encoding; } if (this.tokenSchema) { result.tokenSchema = this.tokenSchema; } return result; } /** * Convert to JSON string */ toJSON() { return JSON.stringify(this.toObject()); } /** * Creates a Feature instance from a dictionary with nested object conversion */ static fromObject(data) { const options = { mimeType: data.mimeType, tokens: Array.isArray(data.tokens) ? data.tokens.map(t => Token.fromObject(t).toObject()) : [], ...(data.alternates !== undefined ? { alternates: data.alternates.map((alt) => (Array.isArray(alt) ? alt.map(tokenData => Token.fromObject(tokenData).toObject()) : [])) } : {}), ...(data.lang !== undefined ? { lang: data.lang } : {}), ...(data.encoding !== undefined ? { encoding: data.encoding } : {}), ...(data.tokenSchema !== undefined ? { tokenSchema: data.tokenSchema } : {}) }; return new Feature(options); } } /** * Specialized text feature with convenient string value handling. * Automatically sets mimeType to 'text/plain' and provides a values convenience property. * * @example * ```typescript * // Create from string values * const textFeature = new TextFeature({ values: ['Hello', 'world'] }); * * // Create with full token options * const textFeature2 = new TextFeature({ * tokens: [{ value: 'Hello', confidence: 0.95 }], * lang: 'en-US' * }); * ``` */ export class TextFeature extends Feature { /** * Creates a new TextFeature instance * @param options - TextFeature configuration options */ constructor(options = {}) { const { values, mimeType = 'text/plain', ...rest } = options; // If values provided, convert to tokens const tokens = values ? values.map(value => ({ value })) : rest.tokens || []; super({ mimeType, tokens, ...rest }); } } /** * Represents a dialog event according to the Open Floor specification. * Dialog events capture linguistic events with features, timing, and speaker information. * * @example * ```typescript * // Simple text dialog event * const event = new DialogEvent({ * speakerUri: 'tag:example.com,2025:user1', * features: { * text: { mimeType: 'text/plain', tokens: [{ value: 'Hello world' }] } * } * }); * * // Dialog event with timing and context * const event2 = new DialogEvent({ * speakerUri: 'tag:example.com,2025:agent1', * span: { startTime: new Date() }, * features: { * text: { mimeType: 'text/plain', tokens: [{ value: 'How can I help?' }] } * }, * context: 'Greeting response' * }); * ``` */ export class DialogEvent { id; speakerUri; span; features; previousId; context; /** * Creates a new DialogEvent instance * @param options - DialogEvent configuration options * @throws Error if required parameters are missing */ constructor(options) { const { id, speakerUri, span, features, previousId, context } = options; if (!id) throw new Error('DialogEvent.id is required'); if (!speakerUri) throw new Error('DialogEvent.speakerUri is required'); if (!span) throw new Error('DialogEvent.span is required'); if (!features) throw new Error('DialogEvent.features is required'); this.id = id; this.speakerUri = speakerUri; this.span = new Span(span); this.features = new Map(Object.entries(features).map(([k, v]) => [k, new Feature(v)])); if (previousId !== undefined) this.previousId = previousId; if (context !== undefined) this.context = context; } /** * Convert to plain object for JSON serialization */ toObject() { const result = { id: this.id, speakerUri: this.speakerUri, span: this.span.toObject(), features: Object.fromEntries(Array.from(this.features.entries()).map(([name, feature]) => [name, feature.toObject()])) }; if (this.previousId) { result.previousId = this.previousId; } if (this.context) { result.context = this.context; } return result; } /** * Convert to JSON string */ toJSON() { return JSON.stringify(this.toObject()); } /** * Creates a DialogEvent instance from a dictionary with nested object conversion */ static fromObject(data) { const options = { speakerUri: data.speakerUri, span: Span.fromObject(data.span), features: Object.fromEntries(Object.entries(data.features) .filter(([_, v]) => { if (!v || typeof v !== 'object') return false; const rec = v; return typeof rec.mimeType === 'string'; }) .map(([k, v]) => [k, Feature.fromObject(v).toObject()])), ...(data.id !== undefined ? { id: data.id } : {}), ...(data.previousId !== undefined ? { previousId: data.previousId } : {}), ...(data.context !== undefined ? { context: data.context } : {}) }; return new DialogEvent(options); } } //# sourceMappingURL=dialog-event.js.map