@microsoft/agents-copilotstudio-client 1.5.0-beta.6.ga236d9a19c → 1.5.1

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.
@@ -9,7 +9,8 @@ import { Activity, Attachment, ConversationAccount } from '@microsoft/agents-act
9
9
  import { Observable, BehaviorSubject, type Subscriber } from 'rxjs'
10
10
 
11
11
  import { CopilotStudioClient } from './copilotStudioClient'
12
- import { debug } from '@microsoft/agents-activity/logger'
12
+ import { debug, trace } from '@microsoft/agents-telemetry'
13
+ import { CopilotStudioClientTraceDefinitions } from './observability'
13
14
 
14
15
  const logger = debug('copilot-studio:webchat')
15
16
 
@@ -256,144 +257,175 @@ export class CopilotStudioWebChat {
256
257
  client: CopilotStudioClient,
257
258
  settings?: CopilotStudioWebChatSettings
258
259
  ): CopilotStudioWebChatConnection {
259
- logger.info('--> Creating connection between Copilot Studio and WebChat ...')
260
+ const managed = trace(CopilotStudioClientTraceDefinitions.createConnection)
261
+ managed.record({ showTyping: settings?.showTyping })
260
262
 
261
- const normalizedConversationId =
263
+ try {
264
+ logger.info('--> Creating connection between Copilot Studio and WebChat ...')
265
+
266
+ const normalizedConversationId =
262
267
  settings?.conversationId && settings.conversationId.trim() !== ''
263
268
  ? settings.conversationId.trim()
264
269
  : undefined
265
- const shouldStart = settings?.startConversation ?? !normalizedConversationId
266
-
267
- let sequence = 0
268
- let activitySubscriber: Subscriber<Partial<Activity>> | undefined
269
- let conversation: ConversationAccount | undefined
270
- let activeConversationId: string | undefined = normalizedConversationId
271
- let ended = false
272
- let started = false
273
-
274
- const connectionStatus$ = new BehaviorSubject(0)
275
- const activity$ = createObservable<Partial<Activity>>(async (subscriber) => {
276
- activitySubscriber = subscriber
277
-
278
- const handleAcknowledgementOnce = once(async (): Promise<void> => {
279
- connectionStatus$.next(2)
280
- await Promise.resolve() // Webchat requires an extra tick to process the connection status change
281
- })
282
-
283
- // When resuming (shouldStart === false), transition straight to connected
284
- if (!shouldStart || started) {
285
- await handleAcknowledgementOnce()
286
- return
287
- }
288
- started = true
270
+ const shouldStart = settings?.startConversation ?? !normalizedConversationId
271
+
272
+ let sequence = 0
273
+ let activitySubscriber: Subscriber<Partial<Activity>> | undefined
274
+ let conversation: ConversationAccount | undefined
275
+ let activeConversationId: string | undefined = normalizedConversationId
276
+ let ended = false
277
+ let started = false
278
+
279
+ const connectionStatus$ = new BehaviorSubject(0)
280
+ const activity$ = createObservable<Partial<Activity>>(async (subscriber) => {
281
+ try {
282
+ activitySubscriber = subscriber
283
+
284
+ const handleAcknowledgementOnce = once(async (): Promise<void> => {
285
+ connectionStatus$.next(2)
286
+ await Promise.resolve() // Webchat requires an extra tick to process the connection status change
287
+ })
288
+
289
+ // When resuming (shouldStart === false), transition straight to connected
290
+ if (!shouldStart || started) {
291
+ await handleAcknowledgementOnce()
292
+ return
293
+ }
294
+ started = true
289
295
 
290
- logger.debug('--> Connection established.')
291
- notifyTyping()
296
+ logger.debug('--> Connection established.')
297
+ notifyTyping()
292
298
 
293
- for await (const activity of client.startConversationStreaming()) {
294
- delete activity.replyToId
295
- if (!conversation && activity.conversation) {
296
- conversation = activity.conversation
297
- }
298
- if (activity.conversation?.id) {
299
- activeConversationId = activity.conversation.id
299
+ for await (const activity of client.startConversationStreaming()) {
300
+ delete activity.replyToId
301
+ if (!conversation && activity.conversation) {
302
+ conversation = activity.conversation
303
+ }
304
+ if (activity.conversation?.id) {
305
+ activeConversationId = activity.conversation.id
306
+ }
307
+ await handleAcknowledgementOnce()
308
+ notifyActivity(activity)
309
+ managed.actions.receivedFromCopilot(activity)
310
+ }
311
+ // If no activities received from bot, we should still acknowledge.
312
+ await handleAcknowledgementOnce()
313
+ } catch (error) {
314
+ throw managed.fail(error)
315
+ } finally {
316
+ managed.end()
300
317
  }
301
- await handleAcknowledgementOnce()
302
- notifyActivity(activity)
303
- }
304
- // If no activities received from bot, we should still acknowledge.
305
- await handleAcknowledgementOnce()
306
- })
307
-
308
- const notifyActivity = (activity: Partial<Activity>) => {
309
- const newActivity = {
310
- ...activity,
311
- timestamp: new Date().toISOString(),
312
- channelData: {
313
- ...activity.channelData,
314
- 'webchat:sequence-id': sequence,
315
- },
316
- }
317
- sequence++
318
- logger.debug(`Notify '${newActivity.type}' activity to WebChat:`, newActivity)
319
- activitySubscriber?.next(newActivity)
320
- }
318
+ })
321
319
 
322
- const notifyTyping = () => {
323
- if (!settings?.showTyping) {
324
- return
320
+ const notifyActivity = (activity: Partial<Activity>) => {
321
+ const newActivity = {
322
+ ...activity,
323
+ timestamp: new Date().toISOString(),
324
+ channelData: {
325
+ ...activity.channelData,
326
+ 'webchat:sequence-id': sequence,
327
+ },
328
+ }
329
+ sequence++
330
+ logger.debug(`Notify '${newActivity.type}' activity to WebChat:`, newActivity)
331
+ activitySubscriber?.next(newActivity)
325
332
  }
326
333
 
327
- const from = conversation
328
- ? { id: conversation.id, name: conversation.name }
329
- : { id: 'agent', name: 'Agent' }
330
- notifyActivity({ type: 'typing', from })
331
- }
332
-
333
- return {
334
- connectionStatus$,
335
- activity$,
336
-
337
- get conversationId () {
338
- return activeConversationId
339
- },
340
-
341
- postActivity (activity: Activity) {
342
- logger.info('--> Preparing to send activity to Copilot Studio ...')
343
-
344
- if (!activity) {
345
- throw new Error('Activity cannot be null.')
334
+ const notifyTyping = () => {
335
+ if (!settings?.showTyping) {
336
+ return
346
337
  }
347
338
 
348
- if (ended) {
349
- throw new Error('Connection has been ended.')
350
- }
339
+ const from = conversation
340
+ ? { id: conversation.id, name: conversation.name }
341
+ : { id: 'agent', name: 'Agent' }
342
+ notifyActivity({ type: 'typing', from })
343
+ }
351
344
 
352
- if (!activitySubscriber) {
353
- throw new Error('Activity subscriber is not initialized.')
354
- }
345
+ return {
346
+ connectionStatus$,
347
+ activity$,
348
+
349
+ get conversationId () {
350
+ return activeConversationId
351
+ },
355
352
 
356
- return createObservable<string>(async (subscriber) => {
353
+ postActivity (activity: Activity) {
357
354
  try {
358
- logger.info('--> Sending activity to Copilot Studio ...')
359
- const newActivity = Activity.fromObject({
360
- ...activity,
361
- id: uuid(),
362
- attachments: await processAttachments(activity)
363
- })
355
+ logger.info('--> Preparing to send activity to Copilot Studio ...')
364
356
 
365
- notifyActivity(newActivity)
366
- notifyTyping()
357
+ if (!activity) {
358
+ throw new Error('Activity cannot be null.')
359
+ }
367
360
 
368
- // Notify WebChat immediately that the message was sent
369
- subscriber.next(newActivity.id!)
361
+ if (ended) {
362
+ throw new Error('Connection has been ended.')
363
+ }
370
364
 
371
- // Stream the agent's response, passing activeConversationId for URL routing
372
- for await (const responseActivity of client.sendActivityStreaming(newActivity, activeConversationId)) {
373
- if (!activeConversationId && responseActivity.conversation?.id) {
374
- activeConversationId = responseActivity.conversation.id
375
- }
376
- notifyActivity(responseActivity)
377
- logger.info('<-- Activity received correctly from Copilot Studio.')
365
+ if (!activitySubscriber) {
366
+ throw new Error('Activity subscriber is not initialized.')
378
367
  }
379
368
 
380
- subscriber.complete()
369
+ const result = createObservable<string>(async (subscriber) => {
370
+ try {
371
+ logger.info('--> Sending activity to Copilot Studio ...')
372
+ const newActivity = Activity.fromObject({
373
+ ...activity,
374
+ id: uuid(),
375
+ attachments: await processAttachments(activity)
376
+ })
377
+
378
+ notifyActivity(newActivity)
379
+ managed.actions.sentToWebChat(newActivity)
380
+ notifyTyping()
381
+
382
+ // Notify WebChat immediately that the message was sent
383
+ subscriber.next(newActivity.id!)
384
+
385
+ // Stream the agent's response, passing activeConversationId for URL routing
386
+ for await (const responseActivity of client.sendActivityStreaming(newActivity, activeConversationId)) {
387
+ if (!activeConversationId && responseActivity.conversation?.id) {
388
+ activeConversationId = responseActivity.conversation.id
389
+ }
390
+ notifyActivity(responseActivity)
391
+ managed.actions.receivedFromCopilot(responseActivity)
392
+ logger.info('<-- Activity received correctly from Copilot Studio.')
393
+ }
394
+
395
+ subscriber.complete()
396
+ } catch (error) {
397
+ logger.error('Error sending Activity to Copilot Studio:', error)
398
+ subscriber.error(error)
399
+ managed.fail(error)
400
+ } finally {
401
+ managed.end()
402
+ }
403
+ })
404
+
405
+ return result
381
406
  } catch (error) {
382
- logger.error('Error sending Activity to Copilot Studio:', error)
383
- subscriber.error(error)
407
+ throw managed.fail(error)
408
+ } finally {
409
+ managed.end()
384
410
  }
385
- })
386
- },
387
-
388
- end () {
389
- logger.info('--> Ending connection between Copilot Studio and WebChat ...')
390
- ended = true
391
- connectionStatus$.complete()
392
- if (activitySubscriber) {
393
- activitySubscriber.complete()
394
- activitySubscriber = undefined
395
- }
396
- },
411
+ },
412
+
413
+ end () {
414
+ logger.info('--> Ending connection between Copilot Studio and WebChat ...')
415
+ ended = true
416
+ connectionStatus$.complete()
417
+ if (activitySubscriber) {
418
+ activitySubscriber.complete()
419
+ activitySubscriber = undefined
420
+ }
421
+ // End the connection span
422
+ managed.end()
423
+ },
424
+ }
425
+ } catch (error) {
426
+ throw managed.fail(error)
427
+ } finally {
428
+ managed.end()
397
429
  }
398
430
  }
399
431
  }
@@ -0,0 +1,5 @@
1
+ // Copyright (c) Microsoft Corporation. All rights reserved.
2
+ // Licensed under the MIT License.
3
+
4
+ export * from './metrics'
5
+ export * from './traces'
@@ -0,0 +1,63 @@
1
+ // Copyright (c) Microsoft Corporation. All rights reserved.
2
+ // Licensed under the MIT License.
3
+
4
+ import { metric, MetricNames } from '@microsoft/agents-telemetry'
5
+
6
+ export const CopilotStudioClientMetrics = {
7
+ // Counters
8
+ activitiesReceivedCounter: metric.counter(MetricNames.CSC_ACTIVITIES_RECEIVED, {
9
+ unit: 'activities',
10
+ description: 'Total number of activities received by the Copilot Studio client'
11
+ }),
12
+
13
+ activitiesSentCounter: metric.counter(MetricNames.CSC_ACTIVITIES_SENT, {
14
+ unit: 'activities',
15
+ description: 'Total number of activities sent to Copilot Studio'
16
+ }),
17
+
18
+ conversationsStartedCounter: metric.counter(MetricNames.CSC_CONVERSATIONS_STARTED, {
19
+ unit: 'conversations',
20
+ description: 'Total number of conversations started with Copilot Studio'
21
+ }),
22
+
23
+ webchatConnectionsCounter: metric.counter(MetricNames.CSC_WEBCHAT_CONNECTIONS, {
24
+ unit: 'connections',
25
+ description: 'Total number of webchat connections created with Copilot Studio'
26
+ }),
27
+
28
+ requestsCounter: metric.counter(MetricNames.CSC_REQUEST_COUNT, {
29
+ unit: 'requests',
30
+ description: 'Total number of HTTP/SSE requests made to Copilot Studio'
31
+ }),
32
+
33
+ requestsErrorCounter: metric.counter(MetricNames.CSC_REQUEST_ERRORS, {
34
+ unit: 'requests',
35
+ description: 'Total number of failed requests to Copilot Studio'
36
+ }),
37
+
38
+ executeStreamingCounter: metric.counter(MetricNames.CSC_EXECUTE_STREAMING, {
39
+ unit: 'operations',
40
+ description: 'Total number of execute streaming operations'
41
+ }),
42
+
43
+ subscribeAsyncCounter: metric.counter(MetricNames.CSC_SUBSCRIBE_ASYNC, {
44
+ unit: 'operations',
45
+ description: 'Total number of subscribeAsync operations'
46
+ }),
47
+
48
+ subscribeEventCounter: metric.counter(MetricNames.CSC_SUBSCRIBE_EVENT, {
49
+ unit: 'events',
50
+ description: 'Total number of events received via subscribeAsync'
51
+ }),
52
+
53
+ // Duration Histograms
54
+ streamDuration: metric.histogram(MetricNames.CSC_STREAM_DURATION, {
55
+ unit: 'ms',
56
+ description: 'Duration of SSE stream sessions in milliseconds'
57
+ }),
58
+
59
+ requestDuration: metric.histogram(MetricNames.CSC_REQUEST_DURATION, {
60
+ unit: 'ms',
61
+ description: 'Duration of requests to Copilot Studio in milliseconds'
62
+ })
63
+ }
@@ -0,0 +1,171 @@
1
+ import { Activity } from '@microsoft/agents-activity'
2
+ import { trace, SpanNames } from '@microsoft/agents-telemetry'
3
+ import { CopilotStudioClientMetrics } from './metrics'
4
+ import { SubscribeEvent } from '../subscribeEvent'
5
+
6
+ export const CopilotStudioClientTraceDefinitions = {
7
+ createConnection: trace.define({
8
+ name: SpanNames.COPILOT_CREATE_CONNECTION,
9
+ record: {
10
+ showTyping: false,
11
+ },
12
+ actions: ({ span }) => ({
13
+ receivedFromCopilot (activity: Activity) {
14
+ span.addEvent('activity.received.from.copilot.studio', {
15
+ 'copilot.webchat.activity.type': activity.type ?? 'unknown',
16
+ 'copilot.webchat.activity.conversation_id': activity.conversation?.id ?? 'unknown'
17
+ })
18
+ },
19
+ sentToWebChat (activity: Activity) {
20
+ span.addEvent('activity.sent.to.webchat', {
21
+ 'copilot.webchat.activity.type': activity.type ?? 'unknown',
22
+ 'copilot.webchat.activity.conversation_id': activity.conversation?.id ?? 'unknown'
23
+ })
24
+ },
25
+ }),
26
+ end ({ span, record }) {
27
+ const attributes = {
28
+ 'copilot.webchat.show_typing': record.showTyping ?? 'unknown'
29
+ }
30
+
31
+ span.setAttributes(attributes)
32
+ CopilotStudioClientMetrics.webchatConnectionsCounter.add(1, attributes)
33
+ }
34
+ }),
35
+ postRequest: trace.define({
36
+ name: SpanNames.COPILOT_POST_REQUEST,
37
+ record: {
38
+ url: '',
39
+ method: ''
40
+ },
41
+ actions: ({ span }) => ({
42
+ receivedFromCopilot (activity: Activity) {
43
+ const attributes = {
44
+ 'copilot.activity.type': activity.type ?? 'unknown',
45
+ 'copilot.activity.conversation_id': activity.conversation?.id ?? 'unknown'
46
+ }
47
+
48
+ span.addEvent('activity.received', {
49
+ 'copilot.post_request.activity.type': attributes['copilot.activity.type'],
50
+ 'copilot.post_request.activity.conversation_id': attributes['copilot.activity.conversation_id']
51
+ })
52
+ CopilotStudioClientMetrics.activitiesReceivedCounter.add(1, attributes)
53
+ }
54
+ }),
55
+ end ({ span, record, duration, error }) {
56
+ const attributes = {
57
+ 'copilot.post_request.url': record.url ?? 'unknown',
58
+ 'copilot.post_request.method': record.method ?? 'unknown'
59
+ }
60
+ const metricAttributes = {
61
+ operation: 'postRequestAsync',
62
+ 'copilot.post_request.url': attributes['copilot.post_request.url'],
63
+ 'copilot.post_request.method': attributes['copilot.post_request.method']
64
+ }
65
+
66
+ span.setAttributes(attributes)
67
+ CopilotStudioClientMetrics.requestsCounter.add(1, metricAttributes)
68
+ CopilotStudioClientMetrics.streamDuration.record(duration, metricAttributes)
69
+
70
+ if (error) {
71
+ CopilotStudioClientMetrics.requestsErrorCounter.add(1, {
72
+ ...metricAttributes,
73
+ 'error.type': error instanceof Error ? error.name : typeof error,
74
+ })
75
+ }
76
+ }
77
+ }),
78
+ startConversation: trace.define({
79
+ name: SpanNames.COPILOT_START_CONVERSATION,
80
+ record: {
81
+ shouldEmitStartEvent: false,
82
+ },
83
+ end ({ span, record, duration }) {
84
+ const attributes = record.shouldEmitStartEvent
85
+ ? { 'copilot.emit_start_event': true }
86
+ : { 'copilot.request': true }
87
+ const metricAttributes = {
88
+ operation: 'startConversationStreaming',
89
+ ...attributes,
90
+ }
91
+
92
+ span.setAttributes(attributes)
93
+ CopilotStudioClientMetrics.conversationsStartedCounter.add(1, metricAttributes)
94
+ CopilotStudioClientMetrics.requestDuration.record(duration, metricAttributes)
95
+ }
96
+ }),
97
+ sendActivity: trace.define({
98
+ name: SpanNames.COPILOT_SEND_ACTIVITY,
99
+ record: {
100
+ activity: Activity.fromObject({ type: 'unknown' }),
101
+ },
102
+ end ({ span, record, duration }) {
103
+ const attributes = {
104
+ 'copilot.activity.type': record.activity.type ?? 'unknown',
105
+ 'copilot.activity.conversation_id': record.activity.conversation?.id ?? 'unknown'
106
+ }
107
+ const metricAttributes = {
108
+ operation: 'sendActivityStreaming',
109
+ ...attributes,
110
+ }
111
+
112
+ span.setAttributes(attributes)
113
+ CopilotStudioClientMetrics.activitiesSentCounter.add(1, attributes)
114
+ CopilotStudioClientMetrics.requestDuration.record(duration, metricAttributes)
115
+ }
116
+ }),
117
+ executeStreaming: trace.define({
118
+ name: SpanNames.COPILOT_EXECUTE_STREAMING,
119
+ record: {
120
+ activity: Activity.fromObject({ type: 'unknown' }),
121
+ conversationId: 'unknown'
122
+ },
123
+ end ({ span, record, duration }) {
124
+ const attributes = {
125
+ 'copilot.activity.type': record.activity.type ?? 'unknown',
126
+ 'copilot.activity.conversation_id': record.conversationId ?? 'unknown'
127
+ }
128
+ const metricAttributes = {
129
+ operation: 'executeStreaming',
130
+ ...attributes,
131
+ }
132
+
133
+ span.setAttributes(attributes)
134
+ CopilotStudioClientMetrics.executeStreamingCounter.add(1, attributes)
135
+ CopilotStudioClientMetrics.requestDuration.record(duration, metricAttributes)
136
+ }
137
+ }),
138
+ subscribeAsync: trace.define({
139
+ name: SpanNames.COPILOT_SUBSCRIBE_ASYNC,
140
+ record: {
141
+ conversationId: 'unknown',
142
+ lastReceivedEventId: 'unknown'
143
+ },
144
+ actions: ({ span }) => ({
145
+ eventReceivedFromCopilot (event: SubscribeEvent) {
146
+ const attributes = {
147
+ 'copilot.subscribe_async.event.id': event.eventId ?? 'unknown',
148
+ 'copilot.subscribe_async.event.activity.type': event.activity.type ?? 'unknown',
149
+ }
150
+
151
+ span.addEvent('event.received', attributes)
152
+ CopilotStudioClientMetrics.subscribeEventCounter.add(1, attributes)
153
+ }
154
+ }),
155
+ end ({ span, record, duration }) {
156
+ const attributes = {
157
+ 'copilot.subscribe_async.conversation_id': record.conversationId ?? 'unknown',
158
+ 'copilot.subscribe_async.last_received_event_id': record.lastReceivedEventId ?? 'unknown'
159
+ }
160
+ const metricAttributes = {
161
+ operation: 'subscribeAsync',
162
+ 'copilot.conversation_id': attributes['copilot.subscribe_async.conversation_id'],
163
+ 'copilot.last_received_event_id': attributes['copilot.subscribe_async.last_received_event_id']
164
+ }
165
+
166
+ span.setAttributes(attributes)
167
+ CopilotStudioClientMetrics.subscribeAsyncCounter.add(1, metricAttributes)
168
+ CopilotStudioClientMetrics.streamDuration.record(duration, metricAttributes)
169
+ }
170
+ })
171
+ }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { AgentType } from './agentType'
7
7
  import { ConnectionSettings } from './connectionSettings'
8
- import { debug } from '@microsoft/agents-activity/logger'
8
+ import { debug } from '@microsoft/agents-telemetry'
9
9
  import { PowerPlatformCloud } from './powerPlatformCloud'
10
10
  import { PrebuiltBotStrategy } from './strategies/prebuiltBotStrategy'
11
11
  import { PublishedBotStrategy } from './strategies/publishedBotStrategy'