bot-handoff
Version:
Bot hand off module for the Microsoft Bot Framework. It allows you to transfer a customer from talking to a bot to talking to a human.
228 lines (176 loc) • 9.83 kB
text/typescript
import * as Promise from 'bluebird';
import * as builder from 'botbuilder';
import * as _ from 'lodash';
import { ConversationState, IConversation} from '../../../IConversation';
import { IHandoffMessage } from '../../../IHandoffMessage';
import { AgentAlreadyInConversationError} from '../../errors/AgentAlreadyInConversationError';
import { AgentNotInConversationError} from '../../errors/AgentNotInConversationError';
import { BotAttemptedToRecordMessageWhileAgentHasConnection} from '../../errors/BotAttemptedToRecordMessageWhileAgentHasConnection';
import { CustomerAlreadyConnectedException } from '../../errors/CustomerAlreadyConnectedException';
import { IProvider } from '../../IProvider';
import { AgentConvoIdToCustomerAddressProvider } from './AgentConvoIdToCustomerAddressProvider';
import { InMemoryConversationProvider } from './InMemoryConversationProvider';
type AgentToCustomerConversationMap = {
[s: string]: builder.IAddress
};
function ensureCustomerAddressDefinedOnHandoffMessage(msg: IHandoffMessage): void {
if (!msg.customerAddress) {
throw new Error('customer address must be defined on a Handoff message in this function');
}
}
function ensureAgentAddressDefinedOnHandoffMessage(msg: IHandoffMessage): void {
if (!msg.agentAddress) {
throw new Error('agent address must be defined');
}
}
function ensureCustomerAndAgentAddressDefined(customerAddress: builder.IAddress, agentAddress: builder.IAddress): void {
if (!agentAddress) {
throw new Error('agent address must be defined');
}
if (!customerAddress) {
throw new Error('customer address must be defined');
}
}
export class InMemoryProvider implements IProvider {
public conversations: {[s: string]: IConversation};
private agentConvoToCustomerAddressProvider: AgentConvoIdToCustomerAddressProvider;
private conversationProvider: InMemoryConversationProvider;
constructor() {
this.conversations = {};
this.agentConvoToCustomerAddressProvider = new AgentConvoIdToCustomerAddressProvider();
this.conversationProvider = new InMemoryConversationProvider(this.conversations);
}
public addBotMessageToTranscript(message: IHandoffMessage): Promise<IConversation> {
ensureCustomerAddressDefinedOnHandoffMessage(message);
const customerAddress = message.customerAddress;
const convo = this.conversationProvider.getConversationFromCustomerAddress(customerAddress);
if (convo && convo.conversationState === ConversationState.Agent) {
return Promise.reject(new BotAttemptedToRecordMessageWhileAgentHasConnection(customerAddress.conversation.id));
}
return this.addBotMessageToTranscriptIgnoringConversationState(message);
}
public addBotMessageToTranscriptIgnoringConversationState(message: IHandoffMessage): Promise<IConversation> {
ensureCustomerAddressDefinedOnHandoffMessage(message);
const customerAddress = message.customerAddress;
return Promise.resolve(
this.conversationProvider.addToTranscriptOrCreateNewConversation(customerAddress, message));
}
public addCustomerMessageToTranscript(message: IHandoffMessage): Promise<IConversation> {
ensureCustomerAddressDefinedOnHandoffMessage(message);
const customerAddress = message.customerAddress;
return Promise.resolve(
this.conversationProvider.addToTranscriptOrCreateNewConversation(customerAddress, message, customerAddress));
}
public addAgentMessageToTranscript(message: IHandoffMessage): Promise<IConversation> {
ensureAgentAddressDefinedOnHandoffMessage(message);
const agentAddress = message.agentAddress;
const customerAddress = this.agentConvoToCustomerAddressProvider.getCustomerAddress(agentAddress);
if (!customerAddress) {
const rejectionMessage = `no customer conversation found for agent with conversation id ${agentAddress.conversation.id}`;
return Promise.reject(new AgentNotInConversationError(agentAddress.conversation.id));
}
const convo = this.conversationProvider.addToTranscriptOrCreateNewConversation(customerAddress, message, agentAddress);
if (convo.conversationState !== ConversationState.Agent) {
try {
return Promise.resolve(this.conversationProvider.setConversationStateToAgent(customerAddress, agentAddress));
} catch (e) {
return Promise.reject(e);
}
} else {
return Promise.resolve(convo);
}
}
// CONNECT/DISCONNECT ACTIONS
public connectCustomerToAgent(customerAddress: builder.IAddress, agentAddress: builder.IAddress): Promise<IConversation> {
ensureCustomerAndAgentAddressDefined(customerAddress, agentAddress);
const customerConvoId: string = customerAddress.conversation.id;
const agentConvoId: string = agentAddress.conversation.id;
if (this.agentConversationAlreadyConnected(agentConvoId)) {
return Promise.reject(new AgentAlreadyInConversationError(agentConvoId));
}
if (this.customerIsConnectedToAgent(customerConvoId)) {
return Promise.reject(
new CustomerAlreadyConnectedException(`customer ${customerAddress.user.name} is already speaking to an agent`));
}
this.agentConvoToCustomerAddressProvider.linkCustomerAddressToAgentConvoId(agentConvoId, customerAddress);
try {
return Promise.resolve( this.conversationProvider.setConversationStateToAgent(customerAddress, agentAddress));
} catch (e) {
return Promise.reject(e);
}
}
public disconnectCustomerFromAgent(customerAddress: builder.IAddress, agentAddress: builder.IAddress): Promise<IConversation> {
const customerConvoId: string = customerAddress.conversation.id;
const agentConvoId: string = agentAddress.conversation.id;
this.agentConvoToCustomerAddressProvider.removeAgentConvoId(agentConvoId);
this.conversationProvider.unsetConversationStateToAgent(customerAddress);
try {
return Promise.resolve(this.getConversationFromCustomerAddress(customerAddress));
} catch (e) {
return Promise.reject(e);
}
}
// QUEUE/DEQUEUE ACTIONS
public queueCustomerForAgent(customerAddress: builder.IAddress): Promise<IConversation> {
try {
return Promise.resolve(this.conversationProvider.setConversationStateToWait(customerAddress));
} catch (e) {
return Promise.reject(e);
}
}
public dequeueCustomerForAgent(customerAddress: builder.IAddress): Promise<IConversation> {
const customerConvoId: string = customerAddress.conversation.id;
try {
return Promise.resolve(this.conversationProvider.unsetConversationWait(customerConvoId));
} catch (e) {
return Promise.reject(e);
}
}
// WATCH/UNWATCH ACTIONS
public watchConversation(customerAddress: builder.IAddress, agentAddress: builder.IAddress): Promise<IConversation> {
this.agentConvoToCustomerAddressProvider.linkCustomerAddressToAgentConvoId(agentAddress.conversation.id, customerAddress);
try {
return Promise.resolve(this.conversationProvider.setConversationStateToWatch(customerAddress, agentAddress));
} catch (e) {
return Promise.reject(e);
}
}
public unwatchConversation(customerAddress: builder.IAddress, agentAddress: builder.IAddress): Promise<IConversation> {
this.agentConvoToCustomerAddressProvider.removeAgentConvoId(agentAddress.conversation.id);
try {
return Promise.resolve(this.conversationProvider.unsetConversationToWatch(customerAddress));
} catch (e) {
return Promise.reject(e);
}
}
public getConversationFromCustomerAddress(customerAddress: builder.IAddress): Promise<IConversation> {
return Promise.resolve(this.conversationProvider.getConversationFromCustomerAddress(customerAddress));
}
public getConversationFromAgentAddress(agentAddress: builder.IAddress): Promise<IConversation> {
const customerAddress = this.agentConvoToCustomerAddressProvider.getCustomerAddress(agentAddress);
if (customerAddress) {
return this.getConversationFromCustomerAddress(customerAddress);
}
return Promise.resolve(undefined);
}
public getAllConversations(): Promise<IConversation[]> {
return Promise.resolve(_.reduce(this.conversations, (accumulator: IConversation[], currentConvo: IConversation) => {
accumulator.push(_.cloneDeep(currentConvo));
return accumulator;
//tslint:disable
}, []));
//tslint:enable
}
private agentConversationAlreadyConnected(agentConversationId: string): boolean {
const customerAddress = this.agentConvoToCustomerAddressProvider.getCustomerAddress(agentConversationId);
// if the customer address does not exist, there is no mapping from the agent conversationId to the customer, therefore there is no
// conversation between the agent and customer. If one does exist, it can be in a watching state. We only care if the fetched
// conversation is in an Agent state.
return !!customerAddress &&
this.conversationProvider.getConversationFromCustomerAddress(customerAddress).conversationState === ConversationState.Agent;
}
private customerIsConnectedToAgent(customerConvoId: string): boolean {
const convo = this.conversations[customerConvoId];
return convo.conversationState === ConversationState.Agent;
}
}