@microsoft/agents-hosting 1.1.4-geb1c05c291 → 1.2.0-alpha.19.g9aeee229e8

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.
Files changed (44) hide show
  1. package/dist/package.json +4 -4
  2. package/dist/src/activityHandler.js +7 -4
  3. package/dist/src/activityHandler.js.map +1 -1
  4. package/dist/src/app/index.d.ts +1 -0
  5. package/dist/src/app/index.js +1 -0
  6. package/dist/src/app/index.js.map +1 -1
  7. package/dist/src/app/streaming/streamingResponse.d.ts +138 -88
  8. package/dist/src/app/streaming/streamingResponse.js +241 -107
  9. package/dist/src/app/streaming/streamingResponse.js.map +1 -1
  10. package/dist/src/app/teamsAttachmentDownloader.d.ts +36 -0
  11. package/dist/src/app/teamsAttachmentDownloader.js +103 -0
  12. package/dist/src/app/teamsAttachmentDownloader.js.map +1 -0
  13. package/dist/src/auth/authConfiguration.d.ts +6 -0
  14. package/dist/src/auth/authConfiguration.js +15 -9
  15. package/dist/src/auth/authConfiguration.js.map +1 -1
  16. package/dist/src/auth/authProvider.d.ts +7 -1
  17. package/dist/src/auth/msalTokenProvider.d.ts +7 -1
  18. package/dist/src/auth/msalTokenProvider.js +48 -3
  19. package/dist/src/auth/msalTokenProvider.js.map +1 -1
  20. package/dist/src/cloudAdapter.d.ts +39 -14
  21. package/dist/src/cloudAdapter.js +52 -26
  22. package/dist/src/cloudAdapter.js.map +1 -1
  23. package/dist/src/connector-client/connectorClient.js +10 -9
  24. package/dist/src/connector-client/connectorClient.js.map +1 -1
  25. package/dist/src/errorHelper.d.ts +4 -0
  26. package/dist/src/errorHelper.js +588 -0
  27. package/dist/src/errorHelper.js.map +1 -0
  28. package/dist/src/index.d.ts +1 -0
  29. package/dist/src/index.js +3 -1
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/oauth/userTokenClient.js.map +1 -1
  32. package/package.json +4 -4
  33. package/src/activityHandler.ts +8 -5
  34. package/src/app/index.ts +1 -0
  35. package/src/app/streaming/streamingResponse.ts +252 -107
  36. package/src/app/teamsAttachmentDownloader.ts +110 -0
  37. package/src/auth/authConfiguration.ts +13 -2
  38. package/src/auth/authProvider.ts +8 -1
  39. package/src/auth/msalTokenProvider.ts +62 -3
  40. package/src/cloudAdapter.ts +56 -29
  41. package/src/connector-client/connectorClient.ts +11 -10
  42. package/src/errorHelper.ts +674 -0
  43. package/src/index.ts +1 -0
  44. 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.1.4-geb1c05c291",
4
+ "version": "1.2.0-alpha.19.g9aeee229e8",
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.2",
24
- "@microsoft/agents-activity": "1.1.4-geb1c05c291",
23
+ "@azure/msal-node": "^3.8.4",
24
+ "@microsoft/agents-activity": "1.2.0-alpha.19.g9aeee229e8",
25
25
  "axios": "^1.13.2",
26
- "jsonwebtoken": "^9.0.2",
26
+ "jsonwebtoken": "^9.0.3",
27
27
  "jwks-rsa": "^3.2.0",
28
28
  "object-path": "^0.11.8"
29
29
  },
@@ -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 new Error('Missing TurnContext parameter')
258
- if (!context.activity) throw new Error('TurnContext does not include an activity')
259
- if (!context.activity.type) throw new Error('Activity is missing its type')
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.onTurnActivity(context)
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
@@ -18,3 +18,4 @@ export * from './extensions'
18
18
  export * from './adaptiveCards'
19
19
  export * from './streaming/streamingResponse'
20
20
  export * from './streaming/citation'
21
+ export * from './teamsAttachmentDownloader'
@@ -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 = 1500
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
- * Creates a new StreamingResponse instance.
48
- *
49
- * @param {TurnContext} context - Context for the current turn of conversation with the user.
50
- * @returns {TurnContext} - The context for the current turn of conversation with the user.
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
- * Gets the stream ID of the current response.
58
- *
59
- * @returns {string | undefined} - The stream ID of the current response.
60
- *
61
- * @remarks
62
- * Assigned after the initial update is sent.
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
- * Gets the number of updates sent for the stream.
77
- *
78
- * @returns {number} - The number of updates sent for the stream.
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
- * Queues an informative update to be sent to the client.
93
- *
94
- * @param {string} text Text of the update to send.
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
- * Queues a chunk of partial message text to be sent to the client
115
- *
116
- * @param {string} text Partial text of the message to send.
117
- * @param {Citation[]} citations Citations to be included in the message.
118
- *
119
- * @remarks
120
- * The text we be sent as quickly as possible to the client. Chunks may be combined before
121
- * delivery to the client.
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
- * Ends the stream by sending the final message to the client.
141
- *
142
- * @returns {Promise<void>} - A promise representing the async operation
143
- */
144
- public endStream (): Promise<void> {
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
- throw new Error('The stream has already ended.')
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
- return this.waitForQueue()
215
+ await this.waitForQueue()
216
+ return StreamingResponseResult.Success
155
217
  }
156
218
 
157
219
  /**
158
- * Sets the attachments to attach to the final chunk.
159
- *
160
- * @param attachments List of attachments.
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
- * Sets the sensitivity label to attach to the final chunk.
168
- *
169
- * @param sensitivityLabel The sensitivty label.
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
- * Sets the citations for the full message.
177
- *
178
- * @param {Citation[]} citations Citations to be included in the message.
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
- * Sets the Feedback Loop in Teams that allows a user to
206
- * give thumbs up or down to a response.
207
- * Default is `false`.
208
- *
209
- * @param enableFeedbackLoop If true, the feedback loop is enabled.
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
- * Sets the type of UI to use for the feedback loop.
217
- *
218
- * @param feedbackLoopType The type of the feedback loop.
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
- * Sets the the Generated by AI label in Teams
226
- * Default is `false`.
227
- *
228
- * @param enableGeneratedByAILabel If true, the label is added.
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
- * Returns the most recently streamed message.
244
- *
245
- * @returns The streamed message.
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
- * Waits for the outgoing activity queue to be empty.
253
- *
254
- * @returns {Promise<void>} - A promise representing the async operation.
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
- * Queues the next chunk of text to be sent to the client.
262
- *
263
- * @private
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 Activity.fromObject({
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
- * Queues an activity to be sent to the client.
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
- * Sends any queued activities to the client until the queue is empty.
319
- *
320
- * @returns {Promise<void>} - A promise that will be resolved once the queue is empty.
321
- * @private
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
- * Sends an activity to the client and saves the stream ID returned.
345
- *
346
- * @param {Activity} activity - The activity to send.
347
- * @returns {Promise<void>} - A promise representing the async operation.
348
- * @private
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
- // Send activity
389
- const response = await this._context.sendActivity(activity)
390
- await new Promise((resolve) => setTimeout(resolve, this.delayInMs))
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
- // Save assigned stream ID
393
- if (!this._streamId) {
394
- this._streamId = response?.id
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
+ }