@microsoft/agents-hosting 1.1.4-geb1c05c291 → 1.2.0-alpha.18.g3c104e426a
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/package.json +4 -4
- package/dist/src/activityHandler.js +7 -4
- package/dist/src/activityHandler.js.map +1 -1
- package/dist/src/app/index.d.ts +1 -0
- package/dist/src/app/index.js +1 -0
- package/dist/src/app/index.js.map +1 -1
- package/dist/src/app/streaming/streamingResponse.d.ts +138 -88
- package/dist/src/app/streaming/streamingResponse.js +241 -107
- package/dist/src/app/streaming/streamingResponse.js.map +1 -1
- package/dist/src/app/teamsAttachmentDownloader.d.ts +36 -0
- package/dist/src/app/teamsAttachmentDownloader.js +103 -0
- package/dist/src/app/teamsAttachmentDownloader.js.map +1 -0
- package/dist/src/auth/authProvider.d.ts +7 -1
- package/dist/src/auth/msalTokenProvider.js +4 -1
- package/dist/src/auth/msalTokenProvider.js.map +1 -1
- package/dist/src/cloudAdapter.d.ts +39 -14
- package/dist/src/cloudAdapter.js +52 -26
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/connector-client/connectorClient.js +10 -9
- package/dist/src/connector-client/connectorClient.js.map +1 -1
- package/dist/src/errorHelper.d.ts +4 -0
- package/dist/src/errorHelper.js +588 -0
- package/dist/src/errorHelper.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/package.json +4 -4
- package/src/activityHandler.ts +8 -5
- package/src/app/index.ts +1 -0
- package/src/app/streaming/streamingResponse.ts +252 -107
- package/src/app/teamsAttachmentDownloader.ts +110 -0
- package/src/auth/authProvider.ts +8 -1
- package/src/auth/msalTokenProvider.ts +5 -1
- package/src/cloudAdapter.ts +56 -29
- package/src/connector-client/connectorClient.ts +11 -10
- package/src/errorHelper.ts +674 -0
- package/src/index.ts +1 -0
- package/src/oauth/userTokenClient.ts +2 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@microsoft/agents-hosting",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0-alpha.18.g3c104e426a",
|
|
5
5
|
"homepage": "https://github.com/microsoft/Agents-for-js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"types": "dist/src/index.d.ts",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@azure/core-auth": "^1.10.1",
|
|
23
|
-
"@azure/msal-node": "^3.8.
|
|
24
|
-
"@microsoft/agents-activity": "1.
|
|
23
|
+
"@azure/msal-node": "^3.8.4",
|
|
24
|
+
"@microsoft/agents-activity": "1.2.0-alpha.18.g3c104e426a",
|
|
25
25
|
"axios": "^1.13.2",
|
|
26
|
-
"jsonwebtoken": "^9.0.
|
|
26
|
+
"jsonwebtoken": "^9.0.3",
|
|
27
27
|
"jwks-rsa": "^3.2.0",
|
|
28
28
|
"object-path": "^0.11.8"
|
|
29
29
|
},
|
package/src/activityHandler.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { debug } from '@microsoft/agents-activity/logger'
|
|
6
6
|
import { TurnContext } from './turnContext'
|
|
7
|
-
import { Activity, ActivityTypes, Channels } from '@microsoft/agents-activity'
|
|
7
|
+
import { Activity, ActivityTypes, Channels, ExceptionHelper } from '@microsoft/agents-activity'
|
|
8
|
+
import { Errors } from './errorHelper'
|
|
8
9
|
import { StatusCodes } from './statusCodes'
|
|
9
10
|
import { InvokeResponse } from './invoke/invokeResponse'
|
|
10
11
|
import { InvokeException } from './invoke/invokeException'
|
|
@@ -254,11 +255,13 @@ export class ActivityHandler {
|
|
|
254
255
|
* @throws Error if context is missing, activity is missing, or activity type is missing
|
|
255
256
|
*/
|
|
256
257
|
async run (context: TurnContext): Promise<void> {
|
|
257
|
-
if (!context) throw
|
|
258
|
-
if (!context.activity) throw
|
|
259
|
-
if (!context.activity.type) throw
|
|
258
|
+
if (!context) throw ExceptionHelper.generateException(Error, Errors.MissingTurnContext)
|
|
259
|
+
if (!context.activity) throw ExceptionHelper.generateException(Error, Errors.TurnContextMissingActivity)
|
|
260
|
+
if (!context.activity.type) throw ExceptionHelper.generateException(Error, Errors.ActivityMissingType)
|
|
260
261
|
|
|
261
|
-
await this.
|
|
262
|
+
await this.handle(context, 'Turn', async () => {
|
|
263
|
+
await this.onTurnActivity(context)
|
|
264
|
+
})
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
/**
|
package/src/app/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Activity, addAIToActivity, Attachment, Entity, ClientCitation, SensitivityUsageInfo } from '@microsoft/agents-activity'
|
|
6
|
+
import { Activity, addAIToActivity, Attachment, Entity, ClientCitation, SensitivityUsageInfo, DeliveryModes, Channels } from '@microsoft/agents-activity'
|
|
7
7
|
import { TurnContext } from '../../turnContext'
|
|
8
8
|
import { Citation } from './citation'
|
|
9
9
|
import { CitationUtil } from './citationUtil'
|
|
@@ -11,6 +11,28 @@ import { debug } from '@microsoft/agents-activity/logger'
|
|
|
11
11
|
|
|
12
12
|
const logger = debug('agents:streamingResponse')
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Results for streaming response operations.
|
|
16
|
+
*/
|
|
17
|
+
export enum StreamingResponseResult {
|
|
18
|
+
/**
|
|
19
|
+
* The operation was successful.
|
|
20
|
+
*/
|
|
21
|
+
Success = 'success',
|
|
22
|
+
/**
|
|
23
|
+
* The stream has already ended.
|
|
24
|
+
*/
|
|
25
|
+
AlreadyEnded = 'alreadyEnded',
|
|
26
|
+
/**
|
|
27
|
+
* The user canceled the streaming response.
|
|
28
|
+
*/
|
|
29
|
+
UserCanceled = 'userCanceled',
|
|
30
|
+
/**
|
|
31
|
+
* An error occurred during the streaming response.
|
|
32
|
+
*/
|
|
33
|
+
Error = 'error'
|
|
34
|
+
}
|
|
35
|
+
|
|
14
36
|
/**
|
|
15
37
|
* A helper class for streaming responses to the client.
|
|
16
38
|
*
|
|
@@ -29,7 +51,11 @@ export class StreamingResponse {
|
|
|
29
51
|
private _message: string = ''
|
|
30
52
|
private _attachments?: Attachment[]
|
|
31
53
|
private _ended = false
|
|
32
|
-
private _delayInMs =
|
|
54
|
+
private _delayInMs = 250
|
|
55
|
+
private _isStreamingChannel: boolean = true
|
|
56
|
+
private _finalMessage?: Activity
|
|
57
|
+
private _canceled = false
|
|
58
|
+
private _userCanceled = false
|
|
33
59
|
|
|
34
60
|
// Queue for outgoing activities
|
|
35
61
|
private _queue: Array<() => Activity> = []
|
|
@@ -44,23 +70,24 @@ export class StreamingResponse {
|
|
|
44
70
|
private _sensitivityLabel?: SensitivityUsageInfo
|
|
45
71
|
|
|
46
72
|
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
* Creates a new StreamingResponse instance.
|
|
74
|
+
*
|
|
75
|
+
* @param {TurnContext} context - Context for the current turn of conversation with the user.
|
|
76
|
+
* @returns {TurnContext} - The context for the current turn of conversation with the user.
|
|
77
|
+
*/
|
|
52
78
|
public constructor (context: TurnContext) {
|
|
53
79
|
this._context = context
|
|
80
|
+
this.loadDefaults(context.activity)
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
* Gets the stream ID of the current response.
|
|
85
|
+
*
|
|
86
|
+
* @returns {string | undefined} - The stream ID of the current response.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* Assigned after the initial update is sent.
|
|
90
|
+
*/
|
|
64
91
|
public get streamId (): string | undefined {
|
|
65
92
|
return this._streamId
|
|
66
93
|
}
|
|
@@ -73,27 +100,42 @@ export class StreamingResponse {
|
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
* Gets the number of updates sent for the stream.
|
|
104
|
+
*
|
|
105
|
+
* @returns {number} - The number of updates sent for the stream.
|
|
106
|
+
*/
|
|
80
107
|
public get updatesSent (): number {
|
|
81
108
|
return this._nextSequence - 1
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
/**
|
|
85
112
|
* Gets the delay in milliseconds between chunks.
|
|
113
|
+
* @remarks
|
|
114
|
+
* Teams default: 1000 ms
|
|
115
|
+
* Web Chat / Direct Line default: 500 ms
|
|
116
|
+
* Other channels: 250 ms
|
|
86
117
|
*/
|
|
87
118
|
public get delayInMs (): number {
|
|
88
119
|
return this._delayInMs
|
|
89
120
|
}
|
|
90
121
|
|
|
91
122
|
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
* Gets whether the channel supports streaming.
|
|
124
|
+
*/
|
|
125
|
+
public get isStreamingChannel (): boolean {
|
|
126
|
+
return this._isStreamingChannel
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Queues an informative update to be sent to the client.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} text Text of the update to send.
|
|
133
|
+
*/
|
|
96
134
|
public queueInformativeUpdate (text: string): void {
|
|
135
|
+
if (!this.isStreamingChannel || !text.trim() || this._canceled) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
97
139
|
if (this._ended) {
|
|
98
140
|
throw new Error('The stream has already ended.')
|
|
99
141
|
}
|
|
@@ -111,17 +153,21 @@ export class StreamingResponse {
|
|
|
111
153
|
}
|
|
112
154
|
|
|
113
155
|
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
* Queues a chunk of partial message text to be sent to the client
|
|
157
|
+
*
|
|
158
|
+
* @param {string} text Partial text of the message to send.
|
|
159
|
+
* @param {Citation[]} citations Citations to be included in the message.
|
|
160
|
+
*
|
|
161
|
+
* @remarks
|
|
162
|
+
* The text we be sent as quickly as possible to the client. Chunks may be combined before
|
|
163
|
+
* delivery to the client.
|
|
164
|
+
*
|
|
165
|
+
*/
|
|
124
166
|
public queueTextChunk (text: string, citations?: Citation[]): void {
|
|
167
|
+
if (!text.trim() || this._canceled) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
125
171
|
if (this._ended) {
|
|
126
172
|
throw new Error('The stream has already ended.')
|
|
127
173
|
}
|
|
@@ -132,51 +178,92 @@ export class StreamingResponse {
|
|
|
132
178
|
// If there are citations, modify the content so that the sources are numbers instead of [doc1], [doc2], etc.
|
|
133
179
|
this._message = CitationUtil.formatCitationsResponse(this._message)
|
|
134
180
|
|
|
181
|
+
if (!this.isStreamingChannel) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
135
185
|
// Queue the next chunk
|
|
136
186
|
this.queueNextChunk()
|
|
137
187
|
}
|
|
138
188
|
|
|
139
189
|
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
public endStream (): Promise<
|
|
190
|
+
* Ends the stream by sending the final message to the client.
|
|
191
|
+
*
|
|
192
|
+
* @returns {Promise<StreamingResponseResult>} - StreamingResponseResult with the result of the streaming response.
|
|
193
|
+
*/
|
|
194
|
+
public async endStream (): Promise<StreamingResponseResult> {
|
|
145
195
|
if (this._ended) {
|
|
146
|
-
|
|
196
|
+
return StreamingResponseResult.AlreadyEnded
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (this._canceled) {
|
|
200
|
+
return this._userCanceled ? StreamingResponseResult.UserCanceled : StreamingResponseResult.Error
|
|
147
201
|
}
|
|
148
202
|
|
|
149
203
|
// Queue final message
|
|
150
204
|
this._ended = true
|
|
205
|
+
|
|
206
|
+
if (!this.isStreamingChannel) {
|
|
207
|
+
await this.sendActivity(this.createFinalMessage())
|
|
208
|
+
return StreamingResponseResult.Success
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Queue final message
|
|
151
212
|
this.queueNextChunk()
|
|
152
213
|
|
|
153
214
|
// Wait for the queue to drain
|
|
154
|
-
|
|
215
|
+
await this.waitForQueue()
|
|
216
|
+
return StreamingResponseResult.Success
|
|
155
217
|
}
|
|
156
218
|
|
|
157
219
|
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
220
|
+
* Resets the streaming response to its initial state.
|
|
221
|
+
* If the stream is still running, this will wait for completion.
|
|
222
|
+
*/
|
|
223
|
+
public async reset () : Promise<void> {
|
|
224
|
+
await this.waitForQueue()
|
|
225
|
+
|
|
226
|
+
this._queueSync = undefined
|
|
227
|
+
this._queue = []
|
|
228
|
+
this._chunkQueued = false
|
|
229
|
+
this._ended = false
|
|
230
|
+
this._canceled = false
|
|
231
|
+
this._userCanceled = false
|
|
232
|
+
this._message = ''
|
|
233
|
+
this._nextSequence = 1
|
|
234
|
+
this._streamId = undefined
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Set Activity that will be (optionally) used for the final streaming message.
|
|
239
|
+
*/
|
|
240
|
+
public setFinalMessage (activity: Activity): void {
|
|
241
|
+
this._finalMessage = activity
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Sets the attachments to attach to the final chunk.
|
|
246
|
+
*
|
|
247
|
+
* @param attachments List of attachments.
|
|
248
|
+
*/
|
|
162
249
|
public setAttachments (attachments: Attachment[]): void {
|
|
163
250
|
this._attachments = attachments
|
|
164
251
|
}
|
|
165
252
|
|
|
166
253
|
/**
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
254
|
+
* Sets the sensitivity label to attach to the final chunk.
|
|
255
|
+
*
|
|
256
|
+
* @param sensitivityLabel The sensitivty label.
|
|
257
|
+
*/
|
|
171
258
|
public setSensitivityLabel (sensitivityLabel: SensitivityUsageInfo): void {
|
|
172
259
|
this._sensitivityLabel = sensitivityLabel
|
|
173
260
|
}
|
|
174
261
|
|
|
175
262
|
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
263
|
+
* Sets the citations for the full message.
|
|
264
|
+
*
|
|
265
|
+
* @param {Citation[]} citations Citations to be included in the message.
|
|
266
|
+
*/
|
|
180
267
|
public setCitations (citations: Citation[]): void {
|
|
181
268
|
if (citations.length > 0) {
|
|
182
269
|
if (!this._citations) {
|
|
@@ -202,31 +289,31 @@ export class StreamingResponse {
|
|
|
202
289
|
}
|
|
203
290
|
|
|
204
291
|
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
292
|
+
* Sets the Feedback Loop in Teams that allows a user to
|
|
293
|
+
* give thumbs up or down to a response.
|
|
294
|
+
* Default is `false`.
|
|
295
|
+
*
|
|
296
|
+
* @param enableFeedbackLoop If true, the feedback loop is enabled.
|
|
297
|
+
*/
|
|
211
298
|
public setFeedbackLoop (enableFeedbackLoop: boolean): void {
|
|
212
299
|
this._enableFeedbackLoop = enableFeedbackLoop
|
|
213
300
|
}
|
|
214
301
|
|
|
215
302
|
/**
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
303
|
+
* Sets the type of UI to use for the feedback loop.
|
|
304
|
+
*
|
|
305
|
+
* @param feedbackLoopType The type of the feedback loop.
|
|
306
|
+
*/
|
|
220
307
|
public setFeedbackLoopType (feedbackLoopType: 'default' | 'custom'): void {
|
|
221
308
|
this._feedbackLoopType = feedbackLoopType
|
|
222
309
|
}
|
|
223
310
|
|
|
224
311
|
/**
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
312
|
+
* Sets the the Generated by AI label in Teams
|
|
313
|
+
* Default is `false`.
|
|
314
|
+
*
|
|
315
|
+
* @param enableGeneratedByAILabel If true, the label is added.
|
|
316
|
+
*/
|
|
230
317
|
public setGeneratedByAILabel (enableGeneratedByAILabel: boolean): void {
|
|
231
318
|
this._enableGeneratedByAILabel = enableGeneratedByAILabel
|
|
232
319
|
}
|
|
@@ -240,28 +327,28 @@ export class StreamingResponse {
|
|
|
240
327
|
}
|
|
241
328
|
|
|
242
329
|
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
330
|
+
* Returns the most recently streamed message.
|
|
331
|
+
*
|
|
332
|
+
* @returns The streamed message.
|
|
333
|
+
*/
|
|
247
334
|
public getMessage (): string {
|
|
248
335
|
return this._message
|
|
249
336
|
}
|
|
250
337
|
|
|
251
338
|
/**
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
339
|
+
* Waits for the outgoing activity queue to be empty.
|
|
340
|
+
*
|
|
341
|
+
* @returns {Promise<void>} - A promise representing the async operation.
|
|
342
|
+
*/
|
|
256
343
|
private waitForQueue (): Promise<void> {
|
|
257
344
|
return this._queueSync || Promise.resolve()
|
|
258
345
|
}
|
|
259
346
|
|
|
260
347
|
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
348
|
+
* Queues the next chunk of text to be sent to the client.
|
|
349
|
+
*
|
|
350
|
+
* @private
|
|
351
|
+
*/
|
|
265
352
|
private queueNextChunk (): void {
|
|
266
353
|
// Are we already waiting to send a chunk?
|
|
267
354
|
if (this._chunkQueued) {
|
|
@@ -274,16 +361,7 @@ export class StreamingResponse {
|
|
|
274
361
|
this._chunkQueued = false
|
|
275
362
|
if (this._ended) {
|
|
276
363
|
// Send final message
|
|
277
|
-
return
|
|
278
|
-
type: 'message',
|
|
279
|
-
text: this._message || 'end of stream response',
|
|
280
|
-
attachments: this._attachments,
|
|
281
|
-
entities: [{
|
|
282
|
-
type: 'streaminfo',
|
|
283
|
-
streamType: 'final',
|
|
284
|
-
streamSequence: this._nextSequence++
|
|
285
|
-
}]
|
|
286
|
-
})
|
|
364
|
+
return this.createFinalMessage()
|
|
287
365
|
} else {
|
|
288
366
|
// Send typing activity
|
|
289
367
|
return Activity.fromObject({
|
|
@@ -300,8 +378,8 @@ export class StreamingResponse {
|
|
|
300
378
|
}
|
|
301
379
|
|
|
302
380
|
/**
|
|
303
|
-
|
|
304
|
-
|
|
381
|
+
* Queues an activity to be sent to the client.
|
|
382
|
+
*/
|
|
305
383
|
private queueActivity (factory: () => Activity): void {
|
|
306
384
|
this._queue.push(factory)
|
|
307
385
|
|
|
@@ -315,11 +393,11 @@ export class StreamingResponse {
|
|
|
315
393
|
}
|
|
316
394
|
|
|
317
395
|
/**
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
396
|
+
* Sends any queued activities to the client until the queue is empty.
|
|
397
|
+
*
|
|
398
|
+
* @returns {Promise<void>} - A promise that will be resolved once the queue is empty.
|
|
399
|
+
* @private
|
|
400
|
+
*/
|
|
323
401
|
private async drainQueue (): Promise<void> {
|
|
324
402
|
// eslint-disable-next-line no-async-promise-executor
|
|
325
403
|
return new Promise<void>(async (resolve, reject) => {
|
|
@@ -341,12 +419,38 @@ export class StreamingResponse {
|
|
|
341
419
|
}
|
|
342
420
|
|
|
343
421
|
/**
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
422
|
+
* Creates the final message to be sent at the end of the stream.
|
|
423
|
+
*/
|
|
424
|
+
private createFinalMessage (): Activity {
|
|
425
|
+
const activity = this._finalMessage ?? new Activity('message')
|
|
426
|
+
activity.type = 'message'
|
|
427
|
+
|
|
428
|
+
if (!this._finalMessage) {
|
|
429
|
+
activity.text = this._message || 'end of stream response'
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
activity.entities ??= []
|
|
433
|
+
activity.attachments = this._attachments
|
|
434
|
+
this._nextSequence++ // Increment sequence for final message, even if not streaming.
|
|
435
|
+
|
|
436
|
+
if (this.isStreamingChannel) {
|
|
437
|
+
activity.entities.push({
|
|
438
|
+
type: 'streaminfo',
|
|
439
|
+
streamType: 'final',
|
|
440
|
+
streamSequence: this._nextSequence
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return activity
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Sends an activity to the client and saves the stream ID returned.
|
|
449
|
+
*
|
|
450
|
+
* @param {Activity} activity - The activity to send.
|
|
451
|
+
* @returns {Promise<void>} - A promise representing the async operation.
|
|
452
|
+
* @private
|
|
453
|
+
*/
|
|
350
454
|
private async sendActivity (activity: Activity): Promise<void> {
|
|
351
455
|
// Set activity ID to the assigned stream ID
|
|
352
456
|
if (this._streamId) {
|
|
@@ -385,13 +489,54 @@ export class StreamingResponse {
|
|
|
385
489
|
}
|
|
386
490
|
}
|
|
387
491
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
492
|
+
try {
|
|
493
|
+
const response = await this._context.sendActivity(activity)
|
|
494
|
+
if (!this._streamId) {
|
|
495
|
+
this._streamId = response?.id
|
|
496
|
+
}
|
|
497
|
+
await new Promise((resolve) => setTimeout(resolve, this.delayInMs))
|
|
498
|
+
} catch (error) {
|
|
499
|
+
const { message } = error as Error
|
|
500
|
+
this._canceled = true
|
|
501
|
+
this._queueSync = undefined
|
|
502
|
+
this._queue = []
|
|
503
|
+
|
|
504
|
+
// MS Teams code list: https://learn.microsoft.com/en-us/microsoftteams/platform/bots/streaming-ux?tabs=jsts#error-codes
|
|
505
|
+
if (message.includes('ContentStreamNotAllowed')) {
|
|
506
|
+
logger.warn('Streaming content is not allowed by the client side.', { originalError: message })
|
|
507
|
+
this._userCanceled = true
|
|
508
|
+
} else if (message.includes('BadArgument') && message.toLowerCase().includes('streaming api is not enabled')) {
|
|
509
|
+
logger.warn('Interaction does not support streaming. Defaulting to non-streaming response.', { originalError: message })
|
|
510
|
+
this._canceled = false
|
|
511
|
+
this._isStreamingChannel = false
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
391
515
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Loads default values for the streaming response.
|
|
518
|
+
*/
|
|
519
|
+
private loadDefaults (activity: Activity) {
|
|
520
|
+
if (!activity) {
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (activity.deliveryMode === DeliveryModes.ExpectReplies) {
|
|
525
|
+
this._isStreamingChannel = false
|
|
526
|
+
} else if (Channels.Msteams === activity.channelId) {
|
|
527
|
+
if (activity.isAgenticRequest()) {
|
|
528
|
+
// Agentic requests do not support streaming responses at this time.
|
|
529
|
+
// TODO: Enable streaming for agentic requests when supported.
|
|
530
|
+
this._isStreamingChannel = false
|
|
531
|
+
} else {
|
|
532
|
+
this._isStreamingChannel = true
|
|
533
|
+
this._delayInMs = 1000
|
|
534
|
+
}
|
|
535
|
+
} else if (Channels.Webchat === activity.channelId || Channels.Directline === activity.channelId) {
|
|
536
|
+
this._isStreamingChannel = true
|
|
537
|
+
this._delayInMs = 500
|
|
538
|
+
} else {
|
|
539
|
+
this._isStreamingChannel = false
|
|
395
540
|
}
|
|
396
541
|
}
|
|
397
542
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Attachment, Channels } from '@microsoft/agents-activity'
|
|
7
|
+
import { debug } from '@microsoft/agents-activity/logger'
|
|
8
|
+
import { ConnectorClient } from '../connector-client'
|
|
9
|
+
import { InputFile, InputFileDownloader } from './inputFileDownloader'
|
|
10
|
+
import { TurnContext } from '../turnContext'
|
|
11
|
+
import { TurnState } from './turnState'
|
|
12
|
+
import axios, { AxiosInstance } from 'axios'
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
|
|
15
|
+
const logger = debug('agents:teamsAttachmentDownloader')
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Downloads attachments from Teams using the bots access token.
|
|
19
|
+
*/
|
|
20
|
+
export class TeamsAttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
|
|
21
|
+
private _httpClient: AxiosInstance
|
|
22
|
+
private _stateKey: string
|
|
23
|
+
|
|
24
|
+
public constructor (stateKey: string = 'inputFiles') {
|
|
25
|
+
this._httpClient = axios.create()
|
|
26
|
+
this._stateKey = stateKey
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Download any files relative to the current user's input.
|
|
31
|
+
*
|
|
32
|
+
* @param {TurnContext} context Context for the current turn of conversation.
|
|
33
|
+
* @returns {Promise<InputFile[]>} Promise that resolves to an array of downloaded input files.
|
|
34
|
+
*/
|
|
35
|
+
public async downloadFiles (context: TurnContext): Promise<InputFile[]> {
|
|
36
|
+
if (context.activity.channelId !== Channels.Msteams && context.activity.channelId !== Channels.M365Copilot) {
|
|
37
|
+
return Promise.resolve([])
|
|
38
|
+
}
|
|
39
|
+
// Filter out HTML attachments
|
|
40
|
+
const attachments = context.activity.attachments?.filter((a) => a.contentType && !a.contentType.startsWith('text/html'))
|
|
41
|
+
if (!attachments || attachments.length === 0) {
|
|
42
|
+
return Promise.resolve([])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const connectorClient : ConnectorClient = context.turnState.get<ConnectorClient>(context.adapter.ConnectorClientKey)
|
|
46
|
+
this._httpClient.defaults.headers = connectorClient.axiosInstance.defaults.headers
|
|
47
|
+
|
|
48
|
+
const files: InputFile[] = []
|
|
49
|
+
for (const attachment of attachments) {
|
|
50
|
+
const file = await this.downloadFile(attachment)
|
|
51
|
+
if (file) {
|
|
52
|
+
files.push(file)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return files
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @private
|
|
61
|
+
* @param {Attachment} attachment - Attachment to download.
|
|
62
|
+
* @returns {Promise<InputFile>} - Promise that resolves to the downloaded input file.
|
|
63
|
+
*/
|
|
64
|
+
private async downloadFile (attachment: Attachment): Promise<InputFile | undefined> {
|
|
65
|
+
let inputFile: InputFile | undefined
|
|
66
|
+
|
|
67
|
+
if (attachment.contentUrl && attachment.contentUrl.startsWith('https://')) {
|
|
68
|
+
try {
|
|
69
|
+
const contentSchema = z.object({ downloadUrl: z.string().url() })
|
|
70
|
+
const parsed = contentSchema.safeParse(attachment.content)
|
|
71
|
+
const downloadUrl = parsed.success ? parsed.data.downloadUrl : attachment.contentUrl
|
|
72
|
+
const response = await this._httpClient.get(downloadUrl, { responseType: 'arraybuffer' })
|
|
73
|
+
|
|
74
|
+
const content = Buffer.from(response.data, 'binary')
|
|
75
|
+
const contentType = response.headers['content-type'] || 'application/octet-stream'
|
|
76
|
+
inputFile = { content, contentType, contentUrl: attachment.contentUrl }
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.error(`Failed to download Teams attachment: ${error}`)
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
if (!attachment.content) {
|
|
83
|
+
logger.error('Attachment missing content')
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
if (!(attachment.content instanceof ArrayBuffer) && !Buffer.isBuffer(attachment.content)) {
|
|
87
|
+
logger.error('Attachment content is not ArrayBuffer or Buffer')
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
inputFile = {
|
|
91
|
+
content: Buffer.from(attachment.content as ArrayBuffer),
|
|
92
|
+
contentType: attachment.contentType,
|
|
93
|
+
contentUrl: attachment.contentUrl
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return inputFile
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Downloads files from the attachments in the current turn context and stores them in state.
|
|
101
|
+
*
|
|
102
|
+
* @param context The turn context containing the activity with attachments.
|
|
103
|
+
* @param state The turn state to store the files in.
|
|
104
|
+
* @returns A promise that resolves when the downloaded files are stored.
|
|
105
|
+
*/
|
|
106
|
+
public async downloadAndStoreFiles (context: TurnContext, state: TState): Promise<void> {
|
|
107
|
+
const files = await this.downloadFiles(context)
|
|
108
|
+
state.setValue(this._stateKey, files)
|
|
109
|
+
}
|
|
110
|
+
}
|