UNPKG

@microsoft/agents-activity

Version:

Microsoft 365 Agents SDK for JavaScript. Activity Protocol serialization and deserialization.

610 lines (526 loc) 17.4 kB
/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { v4 as uuid } from 'uuid' import { z } from 'zod' import { SemanticAction, semanticActionZodSchema } from './action/semanticAction' import { SuggestedActions, suggestedActionsZodSchema } from './action/suggestedActions' import { ActivityEventNames, activityEventNamesZodSchema } from './activityEventNames' import { ActivityImportance, activityImportanceZodSchema } from './activityImportance' import { ActivityTypes, activityTypesZodSchema } from './activityTypes' import { Attachment, attachmentZodSchema } from './attachment/attachment' import { AttachmentLayoutTypes, attachmentLayoutTypesZodSchema } from './attachment/attachmentLayoutTypes' import { ChannelAccount, channelAccountZodSchema } from './conversation/channelAccount' import { Channels } from './conversation/channels' import { ConversationAccount, conversationAccountZodSchema } from './conversation/conversationAccount' import { ConversationReference, conversationReferenceZodSchema } from './conversation/conversationReference' import { EndOfConversationCodes, endOfConversationCodesZodSchema } from './conversation/endOfConversationCodes' import { DeliveryModes, deliveryModesZodSchema } from './deliveryModes' import { Entity, entityZodSchema } from './entity/entity' import { Mention } from './entity/mention' import { InputHints, inputHintsZodSchema } from './inputHints' import { MessageReaction, messageReactionZodSchema } from './messageReaction' import { TextFormatTypes, textFormatTypesZodSchema } from './textFormatTypes' import { TextHighlight, textHighlightZodSchema } from './textHighlight' /** * Zod schema for validating an Activity object. */ export const activityZodSchema = z.object({ type: z.union([activityTypesZodSchema, z.string().min(1)]), text: z.string().optional(), id: z.string().min(1).optional(), channelId: z.string().min(1).optional(), from: channelAccountZodSchema.optional(), timestamp: z.union([z.date(), z.string().min(1).datetime().optional(), z.string().min(1).transform(s => new Date(s)).optional()]), localTimestamp: z.string().min(1).transform(s => new Date(s)).optional().or(z.date()).optional(), // z.string().min(1).transform(s => new Date(s)).optional(), localTimezone: z.string().min(1).optional(), callerId: z.string().min(1).optional(), serviceUrl: z.string().min(1).optional(), conversation: conversationAccountZodSchema.optional(), recipient: channelAccountZodSchema.optional(), textFormat: z.union([textFormatTypesZodSchema, z.string().min(1)]).optional(), attachmentLayout: z.union([attachmentLayoutTypesZodSchema, z.string().min(1)]).optional(), membersAdded: z.array(channelAccountZodSchema).optional(), membersRemoved: z.array(channelAccountZodSchema).optional(), reactionsAdded: z.array(messageReactionZodSchema).optional(), reactionsRemoved: z.array(messageReactionZodSchema).optional(), topicName: z.string().min(1).optional(), historyDisclosed: z.boolean().optional(), locale: z.string().min(1).optional(), speak: z.string().min(1).optional(), inputHint: z.union([inputHintsZodSchema, z.string().min(1)]).optional(), summary: z.string().min(1).optional(), suggestedActions: suggestedActionsZodSchema.optional(), attachments: z.array(attachmentZodSchema).optional(), entities: z.array(entityZodSchema.passthrough()).optional(), channelData: z.any().optional(), action: z.string().min(1).optional(), replyToId: z.string().min(1).optional(), label: z.string().min(1).optional(), valueType: z.string().min(1).optional(), value: z.unknown().optional(), name: z.union([activityEventNamesZodSchema, z.string().min(1)]).optional(), relatesTo: conversationReferenceZodSchema.optional(), code: z.union([endOfConversationCodesZodSchema, z.string().min(1)]).optional(), expiration: z.string().min(1).datetime().optional(), importance: z.union([activityImportanceZodSchema, z.string().min(1)]).optional(), deliveryMode: z.union([deliveryModesZodSchema, z.string().min(1)]).optional(), listenFor: z.array(z.string().min(1)).optional(), textHighlights: z.array(textHighlightZodSchema).optional(), semanticAction: semanticActionZodSchema.optional() }) /** * Represents an activity in a conversation. */ export class Activity { /** * The type of the activity. */ type: ActivityTypes | string /** * The text content of the activity. */ text?: string /** * The unique identifier of the activity. */ id?: string /** * The channel ID where the activity originated. */ channelId?: string /** * The account of the sender of the activity. */ from?: ChannelAccount /** * The timestamp of the activity. */ timestamp?: Date | string /** * The local timestamp of the activity. */ localTimestamp?: Date | string /** * The local timezone of the activity. */ localTimezone?: string /** * The caller ID of the activity. */ callerId?: string /** * The service URL of the activity. */ serviceUrl?: string /** * The conversation account associated with the activity. */ conversation?: ConversationAccount /** * The recipient of the activity. */ recipient?: ChannelAccount /** * The text format of the activity. */ textFormat?: TextFormatTypes | string /** * The attachment layout of the activity. */ attachmentLayout?: AttachmentLayoutTypes | string /** * The members added to the conversation. */ membersAdded?: ChannelAccount[] /** * The members removed from the conversation. */ membersRemoved?: ChannelAccount[] /** * The reactions added to the activity. */ reactionsAdded?: MessageReaction[] /** * The reactions removed from the activity. */ reactionsRemoved?: MessageReaction[] /** * The topic name of the activity. */ topicName?: string /** * Indicates whether the history is disclosed. */ historyDisclosed?: boolean /** * The locale of the activity. */ locale?: string /** * The speech text of the activity. */ speak?: string /** * The input hint for the activity. */ inputHint?: InputHints | string /** * The summary of the activity. */ summary?: string /** * The suggested actions for the activity. */ suggestedActions?: SuggestedActions /** * The attachments of the activity. */ attachments?: Attachment[] /** * The entities associated with the activity. */ entities?: Entity[] /** * The channel-specific data for the activity. */ channelData?: any /** * The action associated with the activity. */ action?: string /** * The ID of the activity being replied to. */ replyToId?: string /** * The label for the activity. */ label?: string /** * The value type of the activity. */ valueType?: string /** * The value associated with the activity. */ value?: unknown /** * The name of the activity event. */ name?: ActivityEventNames | string /** * The conversation reference for the activity. */ relatesTo?: ConversationReference /** * The end-of-conversation code for the activity. */ code?: EndOfConversationCodes | string /** * The expiration time of the activity. */ expiration?: string | Date /** * The importance of the activity. */ importance?: ActivityImportance | string /** * The delivery mode of the activity. */ deliveryMode?: DeliveryModes | string /** * The list of keywords to listen for in the activity. */ listenFor?: string[] /** * The text highlights in the activity. */ textHighlights?: TextHighlight[] /** * The semantic action associated with the activity. */ semanticAction?: SemanticAction /** * The raw timestamp of the activity. */ rawTimestamp?: string /** * The raw expiration time of the activity. */ rawExpiration?: string /** * The raw local timestamp of the activity. */ rawLocalTimestamp?: string /** * Additional properties of the activity. */ [x: string]: unknown /** * Creates a new Activity instance. * @param t The type of the activity. * @throws Will throw an error if the activity type is invalid. */ constructor (t: ActivityTypes | string) { if (t === undefined) { throw new Error('Invalid ActivityType: undefined') } if (t === null) { throw new Error('Invalid ActivityType: null') } if ((typeof t === 'string') && (t.length === 0)) { throw new Error('Invalid ActivityType: empty string') } this.type = t } /** * Creates an Activity instance from a JSON string. * @param json The JSON string representing the activity. * @returns The created Activity instance. */ static fromJson (json: string): Activity { return this.fromObject(JSON.parse(json)) } /** * Creates an Activity instance from an object. * @param o The object representing the activity. * @returns The created Activity instance. */ static fromObject (o: object): Activity { const parsedActivity = activityZodSchema.passthrough().parse(o) const activity = new Activity(parsedActivity.type) Object.assign(activity, parsedActivity) return activity } /** * Creates a continuation activity from a conversation reference. * @param reference The conversation reference. * @returns The created continuation activity. */ static getContinuationActivity (reference: ConversationReference): Activity { const continuationActivityObj = { type: ActivityTypes.Event, name: ActivityEventNames.ContinueConversation, id: uuid(), channelId: reference.channelId, locale: reference.locale, serviceUrl: reference.serviceUrl, conversation: reference.conversation, recipient: reference.agent, from: reference.user, relatesTo: reference } const continuationActivity: Activity = Activity.fromObject(continuationActivityObj) return continuationActivity } /** * Gets the appropriate reply-to ID for the activity. * @returns The reply-to ID, or undefined if not applicable. */ private getAppropriateReplyToId (): string | undefined { if ( this.type !== ActivityTypes.ConversationUpdate || (this.channelId !== Channels.Directline && this.channelId !== Channels.Webchat) ) { return this.id } return undefined } /** * Gets the conversation reference for the activity. * @returns The conversation reference. * @throws Will throw an error if required properties are undefined. */ public getConversationReference (): ConversationReference { if (this.recipient === null || this.recipient === undefined) { throw new Error('Activity Recipient undefined') } if (this.conversation === null || this.conversation === undefined) { throw new Error('Activity Conversation undefined') } if (this.channelId === null || this.channelId === undefined) { throw new Error('Activity ChannelId undefined') } return { activityId: this.getAppropriateReplyToId(), user: this.from, agent: this.recipient, conversation: this.conversation, channelId: this.channelId, locale: this.locale, serviceUrl: this.serviceUrl } } /** * Applies a conversation reference to the activity. * @param reference The conversation reference. * @param isIncoming Whether the activity is incoming. * @returns The updated activity. */ public applyConversationReference ( reference: ConversationReference, isIncoming = false ): Activity { this.channelId = reference.channelId this.locale ??= reference.locale this.serviceUrl = reference.serviceUrl this.conversation = reference.conversation if (isIncoming) { this.from = reference.user this.recipient = reference.agent ?? undefined if (reference.activityId) { this.id = reference.activityId } } else { this.from = reference.agent ?? undefined this.recipient = reference.user if (reference.activityId) { this.replyToId = reference.activityId } } return this } public clone (): Activity { const activityCopy = JSON.parse(JSON.stringify(this)) for (const key in activityCopy) { if (typeof activityCopy[key] === 'string' && !isNaN(Date.parse(activityCopy[key]))) { activityCopy[key] = new Date(activityCopy[key] as string) } } Object.setPrototypeOf(activityCopy, Activity.prototype) return activityCopy } /** * Gets the mentions in the activity. * @param activity The activity. * @returns The list of mentions. */ private getMentions (activity: Activity): Mention[] { const result: Mention[] = [] if (activity.entities !== undefined) { for (let i = 0; i < activity.entities.length; i++) { if (activity.entities[i].type.toLowerCase() === 'mention') { result.push(activity.entities[i] as unknown as Mention) } } } return result } /** * Normalizes mentions in the activity by removing mention tags and optionally removing recipient mention. * @param removeMention Whether to remove the recipient mention from the activity. */ public normalizeMentions (removeMention: boolean = false): void { if (this.type === ActivityTypes.Message) { if (removeMention) { // Strip recipient mention tags and text this.removeRecipientMention() // Strip entity.mention records for recipient id if (this.entities !== undefined && this.recipient?.id) { this.entities = this.entities.filter((entity) => { if (entity.type.toLowerCase() === 'mention') { const mention = entity as unknown as Mention return mention.mentioned.id !== this.recipient?.id } return true }) } } // Remove <at> </at> tags keeping the inner text if (this.text) { this.text = Activity.removeAt(this.text) } // Remove <at> </at> tags from mention records keeping the inner text if (this.entities !== undefined) { const mentions = this.getMentions(this) for (const mention of mentions) { if (mention.text) { mention.text = Activity.removeAt(mention.text)?.trim() } } } } } /** * Removes <at> </at> tags from the specified text. * @param text The text to process. * @returns The text with <at> </at> tags removed. */ private static removeAt (text: string): string { if (!text) { return text } let foundTag: boolean do { foundTag = false const iAtStart = text.toLowerCase().indexOf('<at') if (iAtStart >= 0) { const iAtEnd = text.indexOf('>', iAtStart) if (iAtEnd > 0) { const iAtClose = text.toLowerCase().indexOf('</at>', iAtEnd) if (iAtClose > 0) { // Replace </at> let followingText = text.substring(iAtClose + 5) // If first char of followingText is not whitespace, insert space if (followingText.length > 0 && !(/\s/.test(followingText[0]))) { followingText = ` ${followingText}` } text = text.substring(0, iAtClose) + followingText // Get tag content (text between <at...> and </at>) const tagContent = text.substring(iAtEnd + 1, iAtClose) // Replace <at ...> with just the tag content let prefixText = text.substring(0, iAtStart) // If prefixText is not empty and doesn't end with whitespace, add a space if (prefixText.length > 0 && !(/\s$/.test(prefixText))) { prefixText += ' ' } text = prefixText + tagContent + followingText // We found one, try again, there may be more foundTag = true } } } } while (foundTag) return text } /** * Removes the mention text for a given ID. * @param id The ID of the mention to remove. * @returns The updated text. */ private removeMentionText (id: string): string { const mentions = this.getMentions(this) const mentionsFiltered = mentions.filter((mention): boolean => mention.mentioned.id === id) if ((mentionsFiltered.length > 0) && this.text) { this.text = this.text.replace(mentionsFiltered[0].text, '').trim() } return this.text || '' } /** * Removes the recipient mention from the activity text. * @returns The updated text. */ public removeRecipientMention (): string { if ((this.recipient != null) && this.recipient.id) { return this.removeMentionText(this.recipient.id) } return '' } /** * Gets the conversation reference for a reply. * @param replyId The ID of the reply. * @returns The conversation reference. */ public getReplyConversationReference ( replyId: string ): ConversationReference { const reference: ConversationReference = this.getConversationReference() reference.activityId = replyId return reference } public toJsonString (): string { return JSON.stringify(this) } }