@microsoft/agents-copilotstudio-client 1.1.0-alpha.5 → 1.1.0-alpha.58
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 +3 -3
- package/dist/src/browser.mjs +6 -8
- package/dist/src/browser.mjs.map +4 -4
- package/dist/src/copilotStudioClient.d.ts +16 -8
- package/dist/src/copilotStudioClient.js +88 -97
- package/dist/src/copilotStudioClient.js.map +1 -1
- package/dist/src/copilotStudioWebChat.js +80 -11
- package/dist/src/copilotStudioWebChat.js.map +1 -1
- package/package.json +3 -3
- package/src/copilotStudioClient.ts +92 -110
- package/src/copilotStudioWebChat.ts +88 -13
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createEventSource, EventSourceClient } from 'eventsource-client'
|
|
6
7
|
import { ConnectionSettings } from './connectionSettings'
|
|
7
|
-
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
|
8
8
|
import { getCopilotStudioConnectionUrl, getTokenAudience } from './powerPlatformEnvironment'
|
|
9
9
|
import { Activity, ActivityTypes, ConversationAccount } from '@microsoft/agents-activity'
|
|
10
10
|
import { ExecuteTurnRequest } from './executeTurnRequest'
|
|
@@ -14,11 +14,6 @@ import os from 'os'
|
|
|
14
14
|
|
|
15
15
|
const logger = debug('copilot-studio:client')
|
|
16
16
|
|
|
17
|
-
interface streamRead {
|
|
18
|
-
done: boolean,
|
|
19
|
-
value: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
17
|
/**
|
|
23
18
|
* Client for interacting with Microsoft Copilot Studio services.
|
|
24
19
|
* Provides functionality to start conversations and send messages to Copilot Studio bots.
|
|
@@ -33,8 +28,8 @@ export class CopilotStudioClient {
|
|
|
33
28
|
private conversationId: string = ''
|
|
34
29
|
/** The connection settings for the client. */
|
|
35
30
|
private readonly settings: ConnectionSettings
|
|
36
|
-
/** The
|
|
37
|
-
private readonly
|
|
31
|
+
/** The authenticaton token. */
|
|
32
|
+
private readonly token: string
|
|
38
33
|
|
|
39
34
|
/**
|
|
40
35
|
* Returns the scope URL needed to connect to Copilot Studio from the connection settings.
|
|
@@ -51,79 +46,70 @@ export class CopilotStudioClient {
|
|
|
51
46
|
*/
|
|
52
47
|
constructor (settings: ConnectionSettings, token: string) {
|
|
53
48
|
this.settings = settings
|
|
54
|
-
this.
|
|
55
|
-
this.client.defaults.headers.common.Authorization = `Bearer ${token}`
|
|
56
|
-
this.client.defaults.headers.common['User-Agent'] = CopilotStudioClient.getProductInfo()
|
|
49
|
+
this.token = token
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (islandExperimentalUrl) {
|
|
69
|
-
this.settings.directConnectUrl = islandExperimentalUrl
|
|
70
|
-
logger.debug(`Island Experimental URL: ${islandExperimentalUrl}`)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
this.conversationId = response.headers?.[CopilotStudioClient.conversationIdHeaderKey] ?? ''
|
|
75
|
-
if (this.conversationId) {
|
|
76
|
-
logger.debug(`Conversation ID: ${this.conversationId}`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const sanitizedHeaders = { ...response.headers }
|
|
80
|
-
delete sanitizedHeaders['Authorization']
|
|
81
|
-
delete sanitizedHeaders[CopilotStudioClient.conversationIdHeaderKey]
|
|
82
|
-
logger.debug('Headers received:', sanitizedHeaders)
|
|
52
|
+
/**
|
|
53
|
+
* Streams activities from the Copilot Studio service using eventsource-client.
|
|
54
|
+
* @param url The connection URL for Copilot Studio.
|
|
55
|
+
* @param body Optional. The request body (for POST).
|
|
56
|
+
* @param method Optional. The HTTP method (default: POST).
|
|
57
|
+
* @returns An async generator yielding the Agent's Activities.
|
|
58
|
+
*/
|
|
59
|
+
private async * postRequestAsync (url: string, body?: any, method: string = 'POST'): AsyncGenerator<Activity> {
|
|
60
|
+
logger.debug(`>>> SEND TO ${url}`)
|
|
83
61
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
62
|
+
const eventSource: EventSourceClient = createEventSource({
|
|
63
|
+
url,
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${this.token}`,
|
|
66
|
+
'User-Agent': CopilotStudioClient.getProductInfo(),
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
Accept: 'text/event-stream'
|
|
69
|
+
},
|
|
70
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
71
|
+
method,
|
|
72
|
+
fetch: async (url, init) => {
|
|
73
|
+
const response = await fetch(url, init)
|
|
74
|
+
this.processResponseHeaders(response.headers)
|
|
75
|
+
return response
|
|
95
76
|
}
|
|
96
|
-
|
|
97
|
-
result += value
|
|
98
|
-
|
|
99
|
-
return await processEvents(await reader.read())
|
|
100
|
-
}
|
|
77
|
+
})
|
|
101
78
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
79
|
+
try {
|
|
80
|
+
for await (const { data, event } of eventSource) {
|
|
81
|
+
if (data && event === 'activity') {
|
|
82
|
+
try {
|
|
83
|
+
const activity = Activity.fromJson(data)
|
|
84
|
+
switch (activity.type) {
|
|
85
|
+
case ActivityTypes.Message:
|
|
86
|
+
if (!this.conversationId.trim()) { // Did not get it from the header.
|
|
87
|
+
this.conversationId = activity.conversation?.id ?? ''
|
|
88
|
+
logger.debug(`Conversation ID: ${this.conversationId}`)
|
|
89
|
+
}
|
|
90
|
+
yield activity
|
|
91
|
+
break
|
|
92
|
+
default:
|
|
93
|
+
logger.debug(`Activity type: ${activity.type}`)
|
|
94
|
+
yield activity
|
|
95
|
+
break
|
|
116
96
|
}
|
|
117
|
-
}
|
|
118
|
-
logger.
|
|
97
|
+
} catch (error) {
|
|
98
|
+
logger.error('Failed to parse activity:', error)
|
|
119
99
|
}
|
|
120
|
-
}
|
|
121
|
-
logger.
|
|
122
|
-
|
|
100
|
+
} else if (event === 'end') {
|
|
101
|
+
logger.debug('Stream complete')
|
|
102
|
+
break
|
|
123
103
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
104
|
+
|
|
105
|
+
if (eventSource.readyState === 'closed') {
|
|
106
|
+
logger.debug('Connection closed')
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
eventSource.close()
|
|
112
|
+
}
|
|
127
113
|
}
|
|
128
114
|
|
|
129
115
|
/**
|
|
@@ -146,41 +132,50 @@ export class CopilotStudioClient {
|
|
|
146
132
|
return userAgent
|
|
147
133
|
}
|
|
148
134
|
|
|
135
|
+
private processResponseHeaders (responseHeaders: Headers): void {
|
|
136
|
+
if (this.settings.useExperimentalEndpoint && !this.settings.directConnectUrl?.trim()) {
|
|
137
|
+
const islandExperimentalUrl = responseHeaders?.get(CopilotStudioClient.islandExperimentalUrlHeaderKey)
|
|
138
|
+
if (islandExperimentalUrl) {
|
|
139
|
+
this.settings.directConnectUrl = islandExperimentalUrl
|
|
140
|
+
logger.debug(`Island Experimental URL: ${islandExperimentalUrl}`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.conversationId = responseHeaders?.get(CopilotStudioClient.conversationIdHeaderKey) ?? ''
|
|
145
|
+
if (this.conversationId) {
|
|
146
|
+
logger.debug(`Conversation ID: ${this.conversationId}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const sanitizedHeaders = new Headers()
|
|
150
|
+
responseHeaders.forEach((value, key) => {
|
|
151
|
+
if (key.toLowerCase() !== 'authorization' && key.toLowerCase() !== CopilotStudioClient.conversationIdHeaderKey.toLowerCase()) {
|
|
152
|
+
sanitizedHeaders.set(key, value)
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
logger.debug('Headers received:', sanitizedHeaders)
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
/**
|
|
150
159
|
* Starts a new conversation with the Copilot Studio service.
|
|
151
160
|
* @param emitStartConversationEvent Whether to emit a start conversation event. Defaults to true.
|
|
152
|
-
* @returns
|
|
161
|
+
* @returns An async generator yielding the Agent's Activities.
|
|
153
162
|
*/
|
|
154
|
-
public async startConversationAsync (emitStartConversationEvent: boolean = true):
|
|
163
|
+
public async * startConversationAsync (emitStartConversationEvent: boolean = true): AsyncGenerator<Activity> {
|
|
155
164
|
const uriStart: string = getCopilotStudioConnectionUrl(this.settings)
|
|
156
165
|
const body = { emitStartConversationEvent }
|
|
157
166
|
|
|
158
|
-
const config: AxiosRequestConfig = {
|
|
159
|
-
method: 'post',
|
|
160
|
-
url: uriStart,
|
|
161
|
-
headers: {
|
|
162
|
-
Accept: 'text/event-stream',
|
|
163
|
-
'Content-Type': 'application/json',
|
|
164
|
-
},
|
|
165
|
-
data: body,
|
|
166
|
-
responseType: 'stream',
|
|
167
|
-
adapter: 'fetch'
|
|
168
|
-
}
|
|
169
|
-
|
|
170
167
|
logger.info('Starting conversation ...')
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
logger.info(`Conversation '${act.conversation?.id}' started. Received ${values.length} activities.`, values)
|
|
174
|
-
return act
|
|
168
|
+
|
|
169
|
+
yield * this.postRequestAsync(uriStart, body, 'POST')
|
|
175
170
|
}
|
|
176
171
|
|
|
177
172
|
/**
|
|
178
173
|
* Sends a question to the Copilot Studio service and retrieves the response activities.
|
|
179
174
|
* @param question The question to ask.
|
|
180
175
|
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
|
|
181
|
-
* @returns
|
|
176
|
+
* @returns An async generator yielding the Agent's Activities.
|
|
182
177
|
*/
|
|
183
|
-
public async askQuestionAsync (question: string, conversationId: string = this.conversationId) {
|
|
178
|
+
public async * askQuestionAsync (question: string, conversationId: string = this.conversationId) : AsyncGenerator<Activity> {
|
|
184
179
|
const conversationAccount: ConversationAccount = {
|
|
185
180
|
id: conversationId
|
|
186
181
|
}
|
|
@@ -191,34 +186,21 @@ export class CopilotStudioClient {
|
|
|
191
186
|
}
|
|
192
187
|
const activity = Activity.fromObject(activityObj)
|
|
193
188
|
|
|
194
|
-
|
|
189
|
+
yield * this.sendActivity(activity)
|
|
195
190
|
}
|
|
196
191
|
|
|
197
192
|
/**
|
|
198
193
|
* Sends an activity to the Copilot Studio service and retrieves the response activities.
|
|
199
194
|
* @param activity The activity to send.
|
|
200
195
|
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
|
|
201
|
-
* @returns
|
|
196
|
+
* @returns An async generator yielding the Agent's Activities.
|
|
202
197
|
*/
|
|
203
|
-
public async sendActivity (activity: Activity, conversationId: string = this.conversationId) {
|
|
198
|
+
public async * sendActivity (activity: Activity, conversationId: string = this.conversationId) : AsyncGenerator<Activity> {
|
|
204
199
|
const localConversationId = activity.conversation?.id ?? conversationId
|
|
205
200
|
const uriExecute = getCopilotStudioConnectionUrl(this.settings, localConversationId)
|
|
206
201
|
const qbody: ExecuteTurnRequest = new ExecuteTurnRequest(activity)
|
|
207
202
|
|
|
208
|
-
const config: AxiosRequestConfig = {
|
|
209
|
-
method: 'post',
|
|
210
|
-
url: uriExecute,
|
|
211
|
-
headers: {
|
|
212
|
-
Accept: 'text/event-stream',
|
|
213
|
-
'Content-Type': 'application/json',
|
|
214
|
-
},
|
|
215
|
-
data: qbody,
|
|
216
|
-
responseType: 'stream',
|
|
217
|
-
adapter: 'fetch'
|
|
218
|
-
}
|
|
219
203
|
logger.info('Sending activity...', activity)
|
|
220
|
-
|
|
221
|
-
logger.info(`Received ${values.length} activities.`, values)
|
|
222
|
-
return values
|
|
204
|
+
yield * this.postRequestAsync(uriExecute, qbody, 'POST')
|
|
223
205
|
}
|
|
224
206
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { v4 as uuid } from 'uuid'
|
|
7
7
|
|
|
8
|
-
import { Activity, ConversationAccount } from '@microsoft/agents-activity'
|
|
8
|
+
import { Activity, Attachment, ConversationAccount } from '@microsoft/agents-activity'
|
|
9
9
|
import { Observable, BehaviorSubject, type Subscriber } from 'rxjs'
|
|
10
10
|
|
|
11
11
|
import { CopilotStudioClient } from './copilotStudioClient'
|
|
@@ -218,10 +218,12 @@ export class CopilotStudioWebChat {
|
|
|
218
218
|
|
|
219
219
|
logger.debug('--> Connection established.')
|
|
220
220
|
notifyTyping()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
|
|
222
|
+
for await (const activity of client.startConversationAsync()) {
|
|
223
|
+
delete activity.replyToId
|
|
224
|
+
conversation = activity.conversation
|
|
225
|
+
notifyActivity(activity)
|
|
226
|
+
}
|
|
225
227
|
})
|
|
226
228
|
|
|
227
229
|
const notifyActivity = (activity: Partial<Activity>) => {
|
|
@@ -230,9 +232,10 @@ export class CopilotStudioWebChat {
|
|
|
230
232
|
timestamp: new Date().toISOString(),
|
|
231
233
|
channelData: {
|
|
232
234
|
...activity.channelData,
|
|
233
|
-
'webchat:sequence-id': sequence
|
|
235
|
+
'webchat:sequence-id': sequence,
|
|
234
236
|
},
|
|
235
237
|
}
|
|
238
|
+
sequence++
|
|
236
239
|
logger.debug(`Notify '${newActivity.type}' activity to WebChat:`, newActivity)
|
|
237
240
|
activitySubscriber?.next(newActivity)
|
|
238
241
|
}
|
|
@@ -264,22 +267,26 @@ export class CopilotStudioWebChat {
|
|
|
264
267
|
|
|
265
268
|
return createObservable<string>(async (subscriber) => {
|
|
266
269
|
try {
|
|
267
|
-
const id = uuid()
|
|
268
|
-
|
|
269
270
|
logger.info('--> Sending activity to Copilot Studio ...')
|
|
271
|
+
const newActivity = Activity.fromObject({
|
|
272
|
+
...activity,
|
|
273
|
+
id: uuid(),
|
|
274
|
+
attachments: await processAttachments(activity)
|
|
275
|
+
})
|
|
270
276
|
|
|
271
|
-
notifyActivity(
|
|
277
|
+
notifyActivity(newActivity)
|
|
272
278
|
notifyTyping()
|
|
273
279
|
|
|
274
|
-
|
|
280
|
+
// Notify WebChat immediately that the message was sent
|
|
281
|
+
subscriber.next(newActivity.id!)
|
|
275
282
|
|
|
276
|
-
|
|
283
|
+
// Stream the agent's response, but don't block the UI
|
|
284
|
+
for await (const responseActivity of client.sendActivity(newActivity)) {
|
|
277
285
|
notifyActivity(responseActivity)
|
|
286
|
+
logger.info('<-- Activity received correctly from Copilot Studio.')
|
|
278
287
|
}
|
|
279
288
|
|
|
280
|
-
subscriber.next(id)
|
|
281
289
|
subscriber.complete()
|
|
282
|
-
logger.info('--> Activity received correctly from Copilot Studio.')
|
|
283
290
|
} catch (error) {
|
|
284
291
|
logger.error('Error sending Activity to Copilot Studio:', error)
|
|
285
292
|
subscriber.error(error)
|
|
@@ -299,6 +306,74 @@ export class CopilotStudioWebChat {
|
|
|
299
306
|
}
|
|
300
307
|
}
|
|
301
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Processes activity attachments.
|
|
311
|
+
* @param activity The activity to process for attachments.
|
|
312
|
+
* @returns A promise that resolves to the activity with all attachments converted.
|
|
313
|
+
*/
|
|
314
|
+
async function processAttachments (activity: Activity): Promise<Attachment[]> {
|
|
315
|
+
if (activity.type !== 'message' || !activity.attachments?.length) {
|
|
316
|
+
return activity.attachments || []
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const attachments: Attachment[] = []
|
|
320
|
+
for (const attachment of activity.attachments) {
|
|
321
|
+
const processed = await processBlobAttachment(attachment)
|
|
322
|
+
attachments.push(processed)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return attachments
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Processes a blob attachment to convert its content URL to a data URL.
|
|
330
|
+
* @param attachment The attachment to process.
|
|
331
|
+
* @returns A promise that resolves to the processed attachment.
|
|
332
|
+
*/
|
|
333
|
+
async function processBlobAttachment (attachment: Attachment): Promise<Attachment> {
|
|
334
|
+
let newContentUrl = attachment.contentUrl
|
|
335
|
+
if (!newContentUrl?.startsWith('blob:')) {
|
|
336
|
+
return attachment
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const response = await fetch(newContentUrl)
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
throw new Error(`Failed to fetch blob URL: ${response.status} ${response.statusText}`)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const blob = await response.blob()
|
|
346
|
+
const arrayBuffer = await blob.arrayBuffer()
|
|
347
|
+
const base64 = arrayBufferToBase64(arrayBuffer)
|
|
348
|
+
newContentUrl = `data:${blob.type};base64,${base64}`
|
|
349
|
+
} catch (error) {
|
|
350
|
+
newContentUrl = attachment.contentUrl
|
|
351
|
+
logger.error('Error processing blob attachment:', newContentUrl, error)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return { ...attachment, contentUrl: newContentUrl }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Converts an ArrayBuffer to a base64 string.
|
|
359
|
+
* @param buffer The ArrayBuffer to convert.
|
|
360
|
+
* @returns The base64 encoded string.
|
|
361
|
+
*/
|
|
362
|
+
function arrayBufferToBase64 (buffer: ArrayBuffer): string {
|
|
363
|
+
// Node.js environment
|
|
364
|
+
const BufferClass = typeof globalThis.Buffer === 'function' ? globalThis.Buffer : undefined
|
|
365
|
+
if (BufferClass && typeof BufferClass.from === 'function') {
|
|
366
|
+
return BufferClass.from(buffer).toString('base64')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Browser environment
|
|
370
|
+
let binary = ''
|
|
371
|
+
for (const byte of new Uint8Array(buffer)) {
|
|
372
|
+
binary += String.fromCharCode(byte)
|
|
373
|
+
}
|
|
374
|
+
return btoa(binary)
|
|
375
|
+
}
|
|
376
|
+
|
|
302
377
|
/**
|
|
303
378
|
* Creates an RxJS Observable that wraps an asynchronous function execution.
|
|
304
379
|
*
|