@microsoft/agents-hosting 0.5.1-g2e246ff274 → 0.5.12-g2d752e9b13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +4 -4
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
- package/dist/src/app/agentApplication.js +1 -1
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/oauth/authorization.d.ts +1 -0
- package/dist/src/app/oauth/authorization.js +3 -4
- package/dist/src/app/oauth/authorization.js.map +1 -1
- package/dist/src/app/streaming/citation.d.ts +25 -0
- package/dist/src/app/streaming/citation.js +7 -0
- package/dist/src/app/streaming/citation.js.map +1 -0
- package/dist/src/app/streaming/citationUtil.d.ts +31 -0
- package/dist/src/app/streaming/citationUtil.js +70 -0
- package/dist/src/app/streaming/citationUtil.js.map +1 -0
- package/dist/src/app/streaming/streamingResponse.d.ts +140 -0
- package/dist/src/app/streaming/streamingResponse.js +333 -0
- package/dist/src/app/streaming/streamingResponse.js.map +1 -0
- package/dist/src/cards/cardFactory.d.ts +2 -2
- package/dist/src/cards/cardFactory.js.map +1 -1
- package/dist/src/oauth/index.d.ts +1 -4
- package/dist/src/oauth/index.js +1 -4
- package/dist/src/oauth/index.js.map +1 -1
- package/dist/src/oauth/oAuthFlow.d.ts +9 -10
- package/dist/src/oauth/oAuthFlow.js +35 -38
- package/dist/src/oauth/oAuthFlow.js.map +1 -1
- package/dist/src/oauth/userTokenClient.d.ts +37 -7
- package/dist/src/oauth/userTokenClient.js +56 -15
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/oauth/userTokenClient.types.d.ts +147 -0
- package/dist/src/oauth/{signingResource.js → userTokenClient.types.js} +1 -1
- package/dist/src/oauth/userTokenClient.types.js.map +1 -0
- package/dist/src/turnContext.d.ts +3 -0
- package/dist/src/turnContext.js +5 -0
- package/dist/src/turnContext.js.map +1 -1
- package/package.json +3 -3
- package/src/app/adaptiveCards/adaptiveCardsActions.ts +4 -4
- package/src/app/agentApplication.ts +1 -1
- package/src/app/oauth/authorization.ts +6 -8
- package/src/app/streaming/citation.ts +29 -0
- package/src/app/streaming/citationUtil.ts +76 -0
- package/src/app/streaming/streamingResponse.ts +407 -0
- package/src/cards/cardFactory.ts +2 -3
- package/src/oauth/index.ts +1 -4
- package/src/oauth/oAuthFlow.ts +39 -45
- package/src/oauth/userTokenClient.ts +62 -19
- package/src/oauth/userTokenClient.types.ts +173 -0
- package/src/turnContext.ts +7 -1
- package/dist/src/oauth/oAuthCard.d.ts +0 -27
- package/dist/src/oauth/oAuthCard.js +0 -5
- package/dist/src/oauth/oAuthCard.js.map +0 -1
- package/dist/src/oauth/signingResource.d.ts +0 -43
- package/dist/src/oauth/signingResource.js.map +0 -1
- package/dist/src/oauth/tokenExchangeRequest.d.ts +0 -17
- package/dist/src/oauth/tokenExchangeRequest.js +0 -5
- package/dist/src/oauth/tokenExchangeRequest.js.map +0 -1
- package/dist/src/oauth/tokenResponse.d.ts +0 -29
- package/dist/src/oauth/tokenResponse.js +0 -25
- package/dist/src/oauth/tokenResponse.js.map +0 -1
- package/src/oauth/oAuthCard.ts +0 -30
- package/src/oauth/signingResource.ts +0 -48
- package/src/oauth/tokenExchangeRequest.ts +0 -20
- package/src/oauth/tokenResponse.ts +0 -43
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ClientCitation } from '@microsoft/agents-activity/src/entity/AIEntity'
|
|
7
|
+
|
|
8
|
+
// import { stringify } from 'yaml'
|
|
9
|
+
|
|
10
|
+
// import { Tokenizer } from './tokenizers'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Utility functions for manipulating text and citations.
|
|
14
|
+
*/
|
|
15
|
+
export class CitationUtil {
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* Clips the text to a maximum length in case it exceeds the limit.
|
|
19
|
+
* @param {string} text The text to clip.
|
|
20
|
+
* @param {number} maxLength The maximum length of the text to return, cutting off the last whole word.
|
|
21
|
+
* @returns {string} The modified text
|
|
22
|
+
*/
|
|
23
|
+
public static snippet (text: string, maxLength: number): string {
|
|
24
|
+
if (text.length <= maxLength) {
|
|
25
|
+
return text
|
|
26
|
+
}
|
|
27
|
+
let snippet = text.slice(0, maxLength)
|
|
28
|
+
snippet = snippet.slice(0, Math.min(snippet.length, snippet.lastIndexOf(' ')))
|
|
29
|
+
snippet += '...'
|
|
30
|
+
return snippet
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert citation tags `[doc(s)n]` to `[n]` where n is a number.
|
|
35
|
+
* @param {string} text The text to format.
|
|
36
|
+
* @returns {string} The formatted text.
|
|
37
|
+
*/
|
|
38
|
+
public static formatCitationsResponse (text: string): string {
|
|
39
|
+
return text.replace(/\[docs?(\d+)\]/gi, '[$1]')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the citations used in the text. This will remove any citations that are included in the citations array from the response but not referenced in the text.
|
|
44
|
+
* @param {string} text - The text to search for citation references, i.e. [1], [2], etc.
|
|
45
|
+
* @param {ClientCitation[]} citations - The list of citations to search for.
|
|
46
|
+
* @returns {ClientCitation[] | undefined} The list of citations used in the text.
|
|
47
|
+
*/
|
|
48
|
+
public static getUsedCitations (text: string, citations: ClientCitation[]): ClientCitation[] | undefined {
|
|
49
|
+
const regex = /\[(\d+)\]/gi
|
|
50
|
+
const matches = text.match(regex)
|
|
51
|
+
|
|
52
|
+
if (!matches) {
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Remove duplicates
|
|
57
|
+
const filteredMatches = new Set()
|
|
58
|
+
matches.forEach((match) => {
|
|
59
|
+
if (filteredMatches.has(match)) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
filteredMatches.add(match)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Add citations
|
|
67
|
+
const usedCitations: ClientCitation[] = []
|
|
68
|
+
filteredMatches.forEach((match) => {
|
|
69
|
+
const found = citations.find((citation) => `[${citation.position}]` === match)
|
|
70
|
+
if (found) {
|
|
71
|
+
usedCitations.push(found)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
return usedCitations
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Activity, addAIToActivity, Attachment, Entity } from '@microsoft/agents-activity'
|
|
7
|
+
import { TurnContext } from '../../turnContext'
|
|
8
|
+
import { Citation } from './citation'
|
|
9
|
+
import { CitationUtil } from './citationUtil'
|
|
10
|
+
import { debug } from '../../logger'
|
|
11
|
+
import { ClientCitation, SensitivityUsageInfo } from '@microsoft/agents-activity/src/entity/AIEntity'
|
|
12
|
+
|
|
13
|
+
const logger = debug('agents:streamingResponse')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A helper class for streaming responses to the client.
|
|
17
|
+
* @remarks
|
|
18
|
+
* This class is used to send a series of updates to the client in a single response. The expected
|
|
19
|
+
* sequence of calls is:
|
|
20
|
+
*
|
|
21
|
+
* `queueInformativeUpdate()`, `queueTextChunk()`, `queueTextChunk()`, ..., `endStream()`.
|
|
22
|
+
*
|
|
23
|
+
* Once `endStream()` is called, the stream is considered ended and no further updates can be sent.
|
|
24
|
+
*/
|
|
25
|
+
export class StreamingResponse {
|
|
26
|
+
private readonly _context: TurnContext
|
|
27
|
+
private _nextSequence: number = 1
|
|
28
|
+
private _streamId?: string
|
|
29
|
+
private _message: string = ''
|
|
30
|
+
private _attachments?: Attachment[]
|
|
31
|
+
private _ended = false
|
|
32
|
+
|
|
33
|
+
// Queue for outgoing activities
|
|
34
|
+
private _queue: Array<() => Activity> = []
|
|
35
|
+
private _queueSync: Promise<void> | undefined
|
|
36
|
+
private _chunkQueued = false
|
|
37
|
+
|
|
38
|
+
// Powered by AI feature flags
|
|
39
|
+
private _enableFeedbackLoop = false
|
|
40
|
+
private _feedbackLoopType?: 'default' | 'custom'
|
|
41
|
+
private _enableGeneratedByAILabel = false
|
|
42
|
+
private _citations?: ClientCitation[] = []
|
|
43
|
+
private _sensitivityLabel?: SensitivityUsageInfo
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new StreamingResponse instance.
|
|
47
|
+
* @param {TurnContext} context - Context for the current turn of conversation with the user.
|
|
48
|
+
* @returns {TurnContext} - The context for the current turn of conversation with the user.
|
|
49
|
+
*/
|
|
50
|
+
public constructor (context: TurnContext) {
|
|
51
|
+
this._context = context
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Gets the stream ID of the current response.
|
|
56
|
+
* @returns {string | undefined} - The stream ID of the current response.
|
|
57
|
+
* @remarks
|
|
58
|
+
* Assigned after the initial update is sent.
|
|
59
|
+
*/
|
|
60
|
+
public get streamId (): string | undefined {
|
|
61
|
+
return this._streamId
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets the citations of the current response.
|
|
66
|
+
*/
|
|
67
|
+
public get citations (): ClientCitation[] | undefined {
|
|
68
|
+
return this._citations
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Gets the number of updates sent for the stream.
|
|
73
|
+
* @returns {number} - The number of updates sent for the stream.
|
|
74
|
+
*/
|
|
75
|
+
public get updatesSent (): number {
|
|
76
|
+
return this._nextSequence - 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Queues an informative update to be sent to the client.
|
|
81
|
+
* @param {string} text Text of the update to send.
|
|
82
|
+
*/
|
|
83
|
+
public queueInformativeUpdate (text: string): void {
|
|
84
|
+
if (this._ended) {
|
|
85
|
+
throw new Error('The stream has already ended.')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Queue a typing activity
|
|
89
|
+
this.queueActivity(() => Activity.fromObject({
|
|
90
|
+
type: 'typing',
|
|
91
|
+
text,
|
|
92
|
+
channelData: {
|
|
93
|
+
streamType: 'informative',
|
|
94
|
+
streamSequence: this._nextSequence++
|
|
95
|
+
} as StreamingChannelData
|
|
96
|
+
}))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Queues a chunk of partial message text to be sent to the client
|
|
101
|
+
* @remarks
|
|
102
|
+
* The text we be sent as quickly as possible to the client. Chunks may be combined before
|
|
103
|
+
* delivery to the client.
|
|
104
|
+
* @param {string} text Partial text of the message to send.
|
|
105
|
+
* @param {Citation[]} citations Citations to be included in the message.
|
|
106
|
+
*/
|
|
107
|
+
public queueTextChunk (text: string, citations?: Citation[]): void {
|
|
108
|
+
if (this._ended) {
|
|
109
|
+
throw new Error('The stream has already ended.')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Update full message text
|
|
113
|
+
this._message += text
|
|
114
|
+
|
|
115
|
+
// If there are citations, modify the content so that the sources are numbers instead of [doc1], [doc2], etc.
|
|
116
|
+
this._message = CitationUtil.formatCitationsResponse(this._message)
|
|
117
|
+
|
|
118
|
+
// Queue the next chunk
|
|
119
|
+
this.queueNextChunk()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Ends the stream by sending the final message to the client.
|
|
124
|
+
* @returns {Promise<void>} - A promise representing the async operation
|
|
125
|
+
*/
|
|
126
|
+
public endStream (): Promise<void> {
|
|
127
|
+
if (this._ended) {
|
|
128
|
+
throw new Error('The stream has already ended.')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Queue final message
|
|
132
|
+
this._ended = true
|
|
133
|
+
this.queueNextChunk()
|
|
134
|
+
|
|
135
|
+
// Wait for the queue to drain
|
|
136
|
+
return this.waitForQueue()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Sets the attachments to attach to the final chunk.
|
|
141
|
+
* @param attachments List of attachments.
|
|
142
|
+
*/
|
|
143
|
+
public setAttachments (attachments: Attachment[]): void {
|
|
144
|
+
this._attachments = attachments
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sets the sensitivity label to attach to the final chunk.
|
|
149
|
+
* @param sensitivityLabel The sensitivty label.
|
|
150
|
+
*/
|
|
151
|
+
public setSensitivityLabel (sensitivityLabel: SensitivityUsageInfo): void {
|
|
152
|
+
this._sensitivityLabel = sensitivityLabel
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Sets the citations for the full message.
|
|
157
|
+
* @param {Citation[]} citations Citations to be included in the message.
|
|
158
|
+
*/
|
|
159
|
+
public setCitations (citations: Citation[]): void {
|
|
160
|
+
if (citations.length > 0) {
|
|
161
|
+
if (!this._citations) {
|
|
162
|
+
this._citations = []
|
|
163
|
+
}
|
|
164
|
+
let currPos = this._citations.length
|
|
165
|
+
|
|
166
|
+
for (const citation of citations) {
|
|
167
|
+
const clientCitation: ClientCitation = {
|
|
168
|
+
'@type': 'Claim',
|
|
169
|
+
position: currPos + 1,
|
|
170
|
+
appearance: {
|
|
171
|
+
'@type': 'DigitalDocument',
|
|
172
|
+
name: citation.title || `Document #${currPos + 1}`,
|
|
173
|
+
abstract: CitationUtil.snippet(citation.content, 477)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
currPos++
|
|
177
|
+
this._citations.push(clientCitation)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Sets the Feedback Loop in Teams that allows a user to
|
|
184
|
+
* give thumbs up or down to a response.
|
|
185
|
+
* Default is `false`.
|
|
186
|
+
* @param enableFeedbackLoop If true, the feedback loop is enabled.
|
|
187
|
+
*/
|
|
188
|
+
public setFeedbackLoop (enableFeedbackLoop: boolean): void {
|
|
189
|
+
this._enableFeedbackLoop = enableFeedbackLoop
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Sets the type of UI to use for the feedback loop.
|
|
194
|
+
* @param feedbackLoopType The type of the feedback loop.
|
|
195
|
+
*/
|
|
196
|
+
public setFeedbackLoopType (feedbackLoopType: 'default' | 'custom'): void {
|
|
197
|
+
this._feedbackLoopType = feedbackLoopType
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Sets the the Generated by AI label in Teams
|
|
202
|
+
* Default is `false`.
|
|
203
|
+
* @param enableGeneratedByAILabel If true, the label is added.
|
|
204
|
+
*/
|
|
205
|
+
public setGeneratedByAILabel (enableGeneratedByAILabel: boolean): void {
|
|
206
|
+
this._enableGeneratedByAILabel = enableGeneratedByAILabel
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Returns the most recently streamed message.
|
|
211
|
+
* @returns The streamed message.
|
|
212
|
+
*/
|
|
213
|
+
public getMessage (): string {
|
|
214
|
+
return this._message
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Waits for the outgoing activity queue to be empty.
|
|
219
|
+
* @returns {Promise<void>} - A promise representing the async operation.
|
|
220
|
+
*/
|
|
221
|
+
public waitForQueue (): Promise<void> {
|
|
222
|
+
return this._queueSync || Promise.resolve()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Queues the next chunk of text to be sent to the client.
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
private queueNextChunk (): void {
|
|
230
|
+
// Are we already waiting to send a chunk?
|
|
231
|
+
if (this._chunkQueued) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Queue a chunk of text to be sent
|
|
236
|
+
this._chunkQueued = true
|
|
237
|
+
this.queueActivity(() => {
|
|
238
|
+
this._chunkQueued = false
|
|
239
|
+
if (this._ended) {
|
|
240
|
+
// Send final message
|
|
241
|
+
return Activity.fromObject({
|
|
242
|
+
type: 'message',
|
|
243
|
+
text: this._message || 'end strean response',
|
|
244
|
+
attachments: this._attachments,
|
|
245
|
+
channelData: {
|
|
246
|
+
streamType: 'final',
|
|
247
|
+
streamSequence: this._nextSequence++
|
|
248
|
+
} as StreamingChannelData
|
|
249
|
+
})
|
|
250
|
+
} else {
|
|
251
|
+
// Send typing activity
|
|
252
|
+
return Activity.fromObject({
|
|
253
|
+
type: 'typing',
|
|
254
|
+
text: this._message,
|
|
255
|
+
channelData: {
|
|
256
|
+
streamType: 'streaming',
|
|
257
|
+
streamSequence: this._nextSequence++
|
|
258
|
+
} as StreamingChannelData
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Queues an activity to be sent to the client.
|
|
266
|
+
*/
|
|
267
|
+
private queueActivity (factory: () => Activity): void {
|
|
268
|
+
this._queue.push(factory)
|
|
269
|
+
|
|
270
|
+
// If there's no sync in progress, start one
|
|
271
|
+
if (!this._queueSync) {
|
|
272
|
+
this._queueSync = this.drainQueue().catch((err) => {
|
|
273
|
+
logger.error(`Error occured when sending activity while streaming: "${err}".`)
|
|
274
|
+
// throw err
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Sends any queued activities to the client until the queue is empty.
|
|
281
|
+
* @returns {Promise<void>} - A promise that will be resolved once the queue is empty.
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
private async drainQueue (): Promise<void> {
|
|
285
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
286
|
+
return new Promise<void>(async (resolve, reject) => {
|
|
287
|
+
try {
|
|
288
|
+
logger.debug(`Draining queue with ${this._queue.length} activities.`)
|
|
289
|
+
while (this._queue.length > 0) {
|
|
290
|
+
const factory = this._queue.shift()!
|
|
291
|
+
const activity = factory()
|
|
292
|
+
await this.sendActivity(activity)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
resolve()
|
|
296
|
+
} catch (err) {
|
|
297
|
+
reject(err)
|
|
298
|
+
} finally {
|
|
299
|
+
this._queueSync = undefined
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Sends an activity to the client and saves the stream ID returned.
|
|
306
|
+
* @param {Activity} activity - The activity to send.
|
|
307
|
+
* @returns {Promise<void>} - A promise representing the async operation.
|
|
308
|
+
* @private
|
|
309
|
+
*/
|
|
310
|
+
private async sendActivity (activity: Activity): Promise<void> {
|
|
311
|
+
// Set activity ID to the assigned stream ID
|
|
312
|
+
if (this._streamId) {
|
|
313
|
+
activity.id = this._streamId
|
|
314
|
+
activity.channelData = Object.assign({}, activity.channelData, { streamId: this._streamId })
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
activity.entities = [
|
|
318
|
+
{
|
|
319
|
+
type: 'streaminfo',
|
|
320
|
+
...activity.channelData
|
|
321
|
+
} as Entity
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
if (this._citations && this._citations.length > 0 && !this._ended) {
|
|
325
|
+
// Filter out the citations unused in content.
|
|
326
|
+
const currCitations = CitationUtil.getUsedCitations(this._message, this._citations) ?? undefined
|
|
327
|
+
activity.entities.push({
|
|
328
|
+
type: 'https://schema.org/Message',
|
|
329
|
+
'@type': 'Message',
|
|
330
|
+
'@context': 'https://schema.org',
|
|
331
|
+
'@id': '',
|
|
332
|
+
citation: currCitations
|
|
333
|
+
} as unknown as Entity)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Add in Powered by AI feature flags
|
|
337
|
+
if (this._ended) {
|
|
338
|
+
if (this._enableFeedbackLoop && this._feedbackLoopType) {
|
|
339
|
+
activity.channelData = Object.assign({}, activity.channelData, {
|
|
340
|
+
feedbackLoop: { type: this._feedbackLoopType }
|
|
341
|
+
})
|
|
342
|
+
} else {
|
|
343
|
+
activity.channelData = Object.assign({}, activity.channelData, {
|
|
344
|
+
feedbackLoopEnabled: this._enableFeedbackLoop
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Add in Generated by AI
|
|
349
|
+
if (this._enableGeneratedByAILabel) {
|
|
350
|
+
addAIToActivity(activity, this._citations, this._sensitivityLabel)
|
|
351
|
+
// activity.entities.push({
|
|
352
|
+
// type: 'https://schema.org/Message',
|
|
353
|
+
// '@type': 'Message',
|
|
354
|
+
// '@context': 'https://schema.org',
|
|
355
|
+
// '@id': '',
|
|
356
|
+
// additionalType: ['AIGeneratedContent'],
|
|
357
|
+
// citation: this._citations && this._citations.length > 0 ? this._citations : [],
|
|
358
|
+
// usageInfo: this._sensitivityLabel
|
|
359
|
+
// } as unknown as Entity)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Send activity
|
|
364
|
+
const response = await this._context.sendActivity(activity)
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, 1500))
|
|
366
|
+
|
|
367
|
+
// Save assigned stream ID
|
|
368
|
+
if (!this._streamId) {
|
|
369
|
+
this._streamId = response?.id
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @private
|
|
376
|
+
* Structure of the outgoing channelData field for streaming responses.
|
|
377
|
+
* @remarks
|
|
378
|
+
* The expected sequence of streamTypes is:
|
|
379
|
+
*
|
|
380
|
+
* `informative`, `streaming`, `streaming`, ..., `final`.
|
|
381
|
+
*
|
|
382
|
+
* Once a `final` message is sent, the stream is considered ended.
|
|
383
|
+
*/
|
|
384
|
+
interface StreamingChannelData {
|
|
385
|
+
/**
|
|
386
|
+
* The type of message being sent.
|
|
387
|
+
* @remarks
|
|
388
|
+
* `informative` - An informative update.
|
|
389
|
+
* `streaming` - A chunk of partial message text.
|
|
390
|
+
* `final` - The final message.
|
|
391
|
+
*/
|
|
392
|
+
streamType: 'informative' | 'streaming' | 'final';
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Sequence number of the message in the stream.
|
|
396
|
+
* @remarks
|
|
397
|
+
* Starts at 1 for the first message and increments from there.
|
|
398
|
+
*/
|
|
399
|
+
streamSequence: number;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* ID of the stream.
|
|
403
|
+
* @remarks
|
|
404
|
+
* Assigned after the initial update is sent.
|
|
405
|
+
*/
|
|
406
|
+
streamId?: string;
|
|
407
|
+
}
|
package/src/cards/cardFactory.ts
CHANGED
|
@@ -13,8 +13,7 @@ import { O365ConnectorCard } from './o365ConnectorCard'
|
|
|
13
13
|
import { ThumbnailCard } from './thumbnailCard'
|
|
14
14
|
import { VideoCard } from './videoCard'
|
|
15
15
|
import { CardImage } from './cardImage'
|
|
16
|
-
import { OAuthCard } from '../oauth/
|
|
17
|
-
import { SigningResource } from '../oauth/signingResource'
|
|
16
|
+
import { OAuthCard, SignInResource } from '../oauth/userTokenClient.types'
|
|
18
17
|
import { SigninCard } from './signinCard'
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -218,7 +217,7 @@ export class CardFactory {
|
|
|
218
217
|
* @param signingResource The signing resource.
|
|
219
218
|
* @returns The OAuth card attachment.
|
|
220
219
|
*/
|
|
221
|
-
static oauthCard (connectionName: string, title: string, text: string, signingResource:
|
|
220
|
+
static oauthCard (connectionName: string, title: string, text: string, signingResource: SignInResource) : Attachment {
|
|
222
221
|
const card: Partial<OAuthCard> = {
|
|
223
222
|
buttons: [{
|
|
224
223
|
type: ActionTypes.Signin,
|
package/src/oauth/index.ts
CHANGED
package/src/oauth/oAuthFlow.ts
CHANGED
|
@@ -4,20 +4,18 @@ import { debug } from './../logger'
|
|
|
4
4
|
import { ActivityTypes, Attachment } from '@microsoft/agents-activity'
|
|
5
5
|
import {
|
|
6
6
|
CardFactory,
|
|
7
|
-
AgentStatePropertyAccessor,
|
|
8
|
-
UserState,
|
|
9
7
|
TurnContext,
|
|
8
|
+
Storage,
|
|
10
9
|
MessageFactory,
|
|
11
|
-
TokenExchangeRequest,
|
|
12
|
-
UserTokenClient
|
|
13
10
|
} from '../'
|
|
14
|
-
import {
|
|
11
|
+
import { UserTokenClient } from './userTokenClient'
|
|
12
|
+
import { TokenExchangeRequest, TokenResponse } from './userTokenClient.types'
|
|
15
13
|
|
|
16
14
|
const logger = debug('agents:oauth-flow')
|
|
17
15
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
export interface FlowState {
|
|
17
|
+
flowStarted: boolean,
|
|
18
|
+
flowExpires: number
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
interface TokenVerifyState {
|
|
@@ -35,12 +33,7 @@ export class OAuthFlow {
|
|
|
35
33
|
/**
|
|
36
34
|
* The current state of the OAuth flow.
|
|
37
35
|
*/
|
|
38
|
-
state: FlowState
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The accessor for managing the flow state in user state.
|
|
42
|
-
*/
|
|
43
|
-
flowStateAccessor: AgentStatePropertyAccessor<FlowState | null>
|
|
36
|
+
state: FlowState
|
|
44
37
|
|
|
45
38
|
/**
|
|
46
39
|
* The ID of the token exchange request, used to deduplicate requests.
|
|
@@ -66,9 +59,8 @@ export class OAuthFlow {
|
|
|
66
59
|
* Creates a new instance of OAuthFlow.
|
|
67
60
|
* @param userState The user state.
|
|
68
61
|
*/
|
|
69
|
-
constructor (
|
|
70
|
-
this.state =
|
|
71
|
-
this.flowStateAccessor = userState.createProperty('flowState')
|
|
62
|
+
constructor (private storage: Storage, absOauthConnectionName: string, tokenClient?: UserTokenClient, cardTitle?: string, cardText?: string) {
|
|
63
|
+
this.state = { flowExpires: 0, flowStarted: false }
|
|
72
64
|
this.absOauthConnectionName = absOauthConnectionName
|
|
73
65
|
this.userTokenClient = tokenClient ?? null!
|
|
74
66
|
this.cardTitle = cardTitle ?? this.cardTitle
|
|
@@ -97,7 +89,7 @@ export class OAuthFlow {
|
|
|
97
89
|
* @param context The turn context.
|
|
98
90
|
* @returns A promise that resolves to the user token.
|
|
99
91
|
*/
|
|
100
|
-
public async beginFlow (context: TurnContext): Promise<TokenResponse> {
|
|
92
|
+
public async beginFlow (context: TurnContext): Promise<TokenResponse | undefined> {
|
|
101
93
|
this.state = await this.getUserState(context)
|
|
102
94
|
if (this.absOauthConnectionName === '') {
|
|
103
95
|
throw new Error('connectionName is not set')
|
|
@@ -105,27 +97,20 @@ export class OAuthFlow {
|
|
|
105
97
|
logger.info('Starting OAuth flow for connectionName:', this.absOauthConnectionName)
|
|
106
98
|
await this.initializeTokenClient(context)
|
|
107
99
|
|
|
108
|
-
const
|
|
109
|
-
|
|
100
|
+
const act = context.activity
|
|
101
|
+
const output = await this.userTokenClient.getTokenOrSignInResource(act.from?.id!, this.absOauthConnectionName, act.channelId!, act.getConversationReference(), act.relatesTo!, undefined!)
|
|
102
|
+
if (output && output.tokenResponse) {
|
|
110
103
|
this.state.flowStarted = false
|
|
111
104
|
this.state.flowExpires = 0
|
|
112
|
-
await this.
|
|
113
|
-
|
|
114
|
-
return tokenResponse
|
|
105
|
+
await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
|
|
106
|
+
return output.tokenResponse
|
|
115
107
|
}
|
|
116
|
-
|
|
117
|
-
const authConfig = context.adapter.authConfig
|
|
118
|
-
const signingResource = await this.userTokenClient.getSignInResource(authConfig.clientId!, this.absOauthConnectionName, context.activity.getConversationReference(), context.activity.relatesTo)
|
|
119
|
-
const oCard: Attachment = CardFactory.oauthCard(this.absOauthConnectionName, this.cardTitle, this.cardText, signingResource)
|
|
108
|
+
const oCard: Attachment = CardFactory.oauthCard(this.absOauthConnectionName, this.cardTitle, this.cardText, output.signInResource)
|
|
120
109
|
await context.sendActivity(MessageFactory.attachment(oCard))
|
|
121
110
|
this.state.flowStarted = true
|
|
122
111
|
this.state.flowExpires = Date.now() + 30000
|
|
123
|
-
await this.
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
token: undefined,
|
|
127
|
-
status: TokenRequestStatus.InProgress
|
|
128
|
-
}
|
|
112
|
+
await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
|
|
113
|
+
return undefined
|
|
129
114
|
}
|
|
130
115
|
|
|
131
116
|
/**
|
|
@@ -140,7 +125,7 @@ export class OAuthFlow {
|
|
|
140
125
|
logger.warn('Flow expired')
|
|
141
126
|
this.state!.flowStarted = false
|
|
142
127
|
await context.sendActivity(MessageFactory.text('Sign-in session expired. Please try again.'))
|
|
143
|
-
return {
|
|
128
|
+
return { token: undefined }
|
|
144
129
|
}
|
|
145
130
|
const contFlowActivity = context.activity
|
|
146
131
|
if (contFlowActivity.type === ActivityTypes.Message) {
|
|
@@ -161,22 +146,22 @@ export class OAuthFlow {
|
|
|
161
146
|
logger.info('Continuing OAuth flow with tokenExchange')
|
|
162
147
|
const tokenExchangeRequest = contFlowActivity.value as TokenExchangeRequest
|
|
163
148
|
if (this.tokenExchangeId === tokenExchangeRequest.id) { // dedupe
|
|
164
|
-
return {
|
|
149
|
+
return { token: undefined }
|
|
165
150
|
}
|
|
166
151
|
this.tokenExchangeId = tokenExchangeRequest.id!
|
|
167
152
|
const userTokenResp = await this.userTokenClient?.exchangeTokenAsync(contFlowActivity.from?.id!, this.absOauthConnectionName, contFlowActivity.channelId!, tokenExchangeRequest)
|
|
168
|
-
if (userTokenResp
|
|
153
|
+
if (userTokenResp && userTokenResp.token) {
|
|
169
154
|
logger.info('Token exchanged')
|
|
170
155
|
this.state!.flowStarted = false
|
|
171
|
-
await this.
|
|
156
|
+
await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
|
|
172
157
|
return userTokenResp
|
|
173
158
|
} else {
|
|
174
159
|
logger.warn('Token exchange failed')
|
|
175
160
|
this.state!.flowStarted = true
|
|
176
|
-
return {
|
|
161
|
+
return { token: undefined }
|
|
177
162
|
}
|
|
178
163
|
}
|
|
179
|
-
return {
|
|
164
|
+
return { token: undefined }
|
|
180
165
|
}
|
|
181
166
|
|
|
182
167
|
/**
|
|
@@ -189,7 +174,7 @@ export class OAuthFlow {
|
|
|
189
174
|
await this.initializeTokenClient(context)
|
|
190
175
|
await this.userTokenClient?.signOut(context.activity.from?.id as string, this.absOauthConnectionName, context.activity.channelId as string)
|
|
191
176
|
this.state!.flowExpires = 0
|
|
192
|
-
|
|
177
|
+
this.storage.write({ [this.getFlowStateKey(context)]: this.state })
|
|
193
178
|
logger.info('User signed out successfully')
|
|
194
179
|
}
|
|
195
180
|
|
|
@@ -199,10 +184,9 @@ export class OAuthFlow {
|
|
|
199
184
|
* @returns A promise that resolves to the user state.
|
|
200
185
|
*/
|
|
201
186
|
private async getUserState (context: TurnContext) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
187
|
+
const key = this.getFlowStateKey(context)
|
|
188
|
+
const data = await this.storage.read([key])
|
|
189
|
+
const userProfile: FlowState = data[key] ?? { flowStarted: false, flowExpires: 0 }
|
|
206
190
|
return userProfile
|
|
207
191
|
}
|
|
208
192
|
|
|
@@ -210,7 +194,17 @@ export class OAuthFlow {
|
|
|
210
194
|
if (this.userTokenClient === undefined || this.userTokenClient === null) {
|
|
211
195
|
const scope = 'https://api.botframework.com'
|
|
212
196
|
const accessToken = await context.adapter.authProvider.getAccessToken(context.adapter.authConfig, scope)
|
|
213
|
-
this.userTokenClient = new UserTokenClient(accessToken)
|
|
197
|
+
this.userTokenClient = new UserTokenClient(accessToken, context.adapter.authConfig.clientId!)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private getFlowStateKey (context: TurnContext): string {
|
|
202
|
+
const channelId = context.activity.channelId
|
|
203
|
+
const conversationId = context.activity.conversation?.id
|
|
204
|
+
const userId = context.activity.from?.id
|
|
205
|
+
if (!channelId || !conversationId || !userId) {
|
|
206
|
+
throw new Error('ChannelId, conversationId, and userId must be set in the activity')
|
|
214
207
|
}
|
|
208
|
+
return `oauth/${channelId}/${conversationId}/${userId}/flowState`
|
|
215
209
|
}
|
|
216
210
|
}
|