@microsoft/agents-copilotstudio-client 1.1.0-alpha.9.g154c2c8a32 → 1.1.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.
- 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 +30 -6
- package/dist/src/copilotStudioClient.js +126 -102
- 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 +132 -115
- 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,43 +132,83 @@ 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
|
|
163
|
+
public async * startConversationStreaming (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
|
-
|
|
174
|
-
|
|
168
|
+
|
|
169
|
+
yield * this.postRequestAsync(uriStart, body, 'POST')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Sends an activity to the Copilot Studio service and retrieves the response activities.
|
|
174
|
+
* @param activity The activity to send.
|
|
175
|
+
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
|
|
176
|
+
* @returns An async generator yielding the Agent's Activities.
|
|
177
|
+
*/
|
|
178
|
+
public async * sendActivityStreaming (activity: Activity, conversationId: string = this.conversationId) : AsyncGenerator<Activity> {
|
|
179
|
+
const localConversationId = activity.conversation?.id ?? conversationId
|
|
180
|
+
const uriExecute = getCopilotStudioConnectionUrl(this.settings, localConversationId)
|
|
181
|
+
const qbody: ExecuteTurnRequest = new ExecuteTurnRequest(activity)
|
|
182
|
+
|
|
183
|
+
logger.info('Sending activity...', activity)
|
|
184
|
+
yield * this.postRequestAsync(uriExecute, qbody, 'POST')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Starts a new conversation with the Copilot Studio service.
|
|
189
|
+
* @param emitStartConversationEvent Whether to emit a start conversation event. Defaults to true.
|
|
190
|
+
* @returns A promise yielding an array of activities.
|
|
191
|
+
* @deprecated Use startConversationStreaming instead.
|
|
192
|
+
*/
|
|
193
|
+
public async startConversationAsync (emitStartConversationEvent: boolean = true): Promise<Activity[]> {
|
|
194
|
+
const result: Activity[] = []
|
|
195
|
+
for await (const value of this.startConversationStreaming(emitStartConversationEvent)) {
|
|
196
|
+
result.push(value)
|
|
197
|
+
}
|
|
198
|
+
return result
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
/**
|
|
178
202
|
* Sends a question to the Copilot Studio service and retrieves the response activities.
|
|
179
203
|
* @param question The question to ask.
|
|
180
204
|
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
|
|
181
|
-
* @returns A promise
|
|
205
|
+
* @returns A promise yielding an array of activities.
|
|
206
|
+
* @deprecated Use sendActivityStreaming instead.
|
|
182
207
|
*/
|
|
183
|
-
public async askQuestionAsync (question: string, conversationId
|
|
208
|
+
public async askQuestionAsync (question: string, conversationId?: string) : Promise<Activity[]> {
|
|
209
|
+
const localConversationId = conversationId?.trim() ? conversationId : this.conversationId
|
|
184
210
|
const conversationAccount: ConversationAccount = {
|
|
185
|
-
id:
|
|
211
|
+
id: localConversationId
|
|
186
212
|
}
|
|
187
213
|
const activityObj = {
|
|
188
214
|
type: 'message',
|
|
@@ -191,34 +217,25 @@ export class CopilotStudioClient {
|
|
|
191
217
|
}
|
|
192
218
|
const activity = Activity.fromObject(activityObj)
|
|
193
219
|
|
|
194
|
-
|
|
220
|
+
const result: Activity[] = []
|
|
221
|
+
for await (const value of this.sendActivityStreaming(activity, conversationId)) {
|
|
222
|
+
result.push(value)
|
|
223
|
+
}
|
|
224
|
+
return result
|
|
195
225
|
}
|
|
196
226
|
|
|
197
227
|
/**
|
|
198
228
|
* Sends an activity to the Copilot Studio service and retrieves the response activities.
|
|
199
229
|
* @param activity The activity to send.
|
|
200
230
|
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
|
|
201
|
-
* @returns A promise
|
|
231
|
+
* @returns A promise yielding an array of activities.
|
|
232
|
+
* @deprecated Use sendActivityStreaming instead.
|
|
202
233
|
*/
|
|
203
|
-
public async sendActivity (activity: Activity, conversationId: string = this.conversationId) {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
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'
|
|
234
|
+
public async sendActivity (activity: Activity, conversationId: string = this.conversationId) : Promise<Activity[]> {
|
|
235
|
+
const result: Activity[] = []
|
|
236
|
+
for await (const value of this.sendActivityStreaming(activity, conversationId)) {
|
|
237
|
+
result.push(value)
|
|
218
238
|
}
|
|
219
|
-
|
|
220
|
-
const values = await this.postRequestAsync(config)
|
|
221
|
-
logger.info(`Received ${values.length} activities.`, values)
|
|
222
|
-
return values
|
|
239
|
+
return result
|
|
223
240
|
}
|
|
224
241
|
}
|
|
@@ -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.startConversationStreaming()) {
|
|
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.sendActivityStreaming(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
|
*
|