forked from palindromed/Bot-HandOff
-
Notifications
You must be signed in to change notification settings - Fork 0
/
handoff.ts
169 lines (144 loc) · 6.39 KB
/
handoff.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import * as builder from 'botbuilder';
import { Express } from 'express';
import { defaultProvider } from './provider';
// Options for state of a conversation
// Customer talking to bot, waiting for next available agent or talking to an agent
export enum ConversationState {
Bot,
Waiting,
Agent,
Watch
}
// What an entry in the customer transcript will have
export interface TranscriptLine {
timestamp: any,
from: string,
text: string
}
// What is stored in a conversation. Agent only included if customer is talking to an agent
export interface Conversation {
customer: builder.IAddress,
agent?: builder.IAddress,
state: ConversationState,
transcript: TranscriptLine[]
};
// Used in getConversation in provider. Gives context to the search and changes behavior
export interface By {
bestChoice?: true,
agentConversationId?: string,
customerConversationId?: string,
customerName?: string
}
export interface Provider {
init();
// Update
addToTranscript: (by: By, text: string) => boolean;
connectCustomerToAgent: (by: By, nextState: ConversationState, agentAddress: builder.IAddress) => Conversation;
connectCustomerToBot: (by: By) => boolean;
queueCustomerForAgent: (by: By) => boolean;
// Get
getConversation: (by: By, customerAddress?: builder.IAddress) => Conversation;
currentConversations: () => Conversation[];
}
export class Handoff {
// if customizing, pass in your own check for isAgent and your own versions of methods in defaultProvider
constructor(
private bot: builder.UniversalBot,
public isAgent: (session: builder.Session) => boolean,
private provider = defaultProvider
) {
this.provider.init();
}
public routingMiddleware() {
return {
botbuilder: (session: builder.Session, next: Function) => {
// Pass incoming messages to routing method
if (session.message.type === 'message') {
this.routeMessage(session, next);
}
},
send: (event: builder.IEvent, next: Function) => {
// Messages sent from the bot do not need to be routed
const message = event as builder.IMessage;
const customerConversation = this.getConversation({ customerConversationId: event.address.conversation.id });
// send message to agent observing conversation
if (customerConversation.state === ConversationState.Watch) {
this.bot.send(new builder.Message().address(customerConversation.agent).text(message.text));
}
this.trancribeMessageFromBot(message, next);
}
}
}
private routeMessage(
session: builder.Session,
next: Function
) {
if (this.isAgent(session)) {
this.routeAgentMessage(session)
} else {
this.routeCustomerMessage(session, next);
}
}
private routeAgentMessage(session: builder.Session) {
const message = session.message;
const conversation = this.getConversation({ agentConversationId: message.address.conversation.id });
// if the agent is not in conversation, no further routing is necessary
if (!conversation)
return;
// if the agent is observing a customer, no need to route message
if (conversation.state !== ConversationState.Agent)
return;
// send text that agent typed to the customer they are in conversation with
this.bot.send(new builder.Message().address(conversation.customer).text(message.text));
}
private routeCustomerMessage(session: builder.Session, next: Function) {
const message = session.message;
// method will either return existing conversation or a newly created conversation if this is first time we've heard from customer
const conversation = this.getConversation({ customerConversationId: message.address.conversation.id }, message.address);
this.addToTranscript({ customerConversationId: conversation.customer.conversation.id }, message.text);
switch (conversation.state) {
case ConversationState.Bot:
return next();
case ConversationState.Waiting:
session.send("Connecting you to the next available agent.");
return;
case ConversationState.Watch:
this.bot.send(new builder.Message().address(conversation.agent).text(message.text));
return next();
case ConversationState.Agent:
if (!conversation.agent) {
session.send("No agent address present while customer in state Agent");
console.log("No agent address present while customer in state Agent");
return;
}
this.bot.send(new builder.Message().address(conversation.agent).text(message.text));
return;
}
}
// These methods are wrappers around provider which handles data
private trancribeMessageFromBot(message: builder.IMessage, next: Function) {
this.provider.addToTranscript({ customerConversationId: message.address.conversation.id }, message.text);
next();
}
public getCustomerTranscript(by: By, session: builder.Session) {
const customerConversation = this.getConversation(by);
if (customerConversation) {
customerConversation.transcript.forEach(transcriptLine =>
session.send(transcriptLine.text));
} else {
session.send('No Transcript to show. Try entering a username or try again when connected to a customer');
}
}
public connectCustomerToAgent = (by: By, nextState: ConversationState, agentAddress: builder.IAddress) =>
this.provider.connectCustomerToAgent(by, nextState, agentAddress);
public connectCustomerToBot = (by: By) =>
this.provider.connectCustomerToBot(by);
public queueCustomerForAgent = (by: By) =>
this.provider.queueCustomerForAgent(by);
public addToTranscript = (by: By, text: string) =>
this.provider.addToTranscript(by, text);
public getConversation = (by: By, customerAddress?: builder.IAddress) =>
this.provider.getConversation(by, customerAddress);
public currentConversations = () =>
this.provider.currentConversations();
};