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 Axios instance used for HTTP requests . */
37
- private readonly client : AxiosInstance
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 . client = axios . create ( )
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
- private async postRequestAsync ( axiosConfig : AxiosRequestConfig ) : Promise < Activity [ ] > {
60
- const activities : Activity [ ] = [ ]
61
-
62
- logger . debug ( `>>> SEND TO ${ axiosConfig . url } ` )
63
-
64
- const response = await this . client ( axiosConfig )
65
-
66
- if ( this . settings . useExperimentalEndpoint && ! this . settings . directConnectUrl ?. trim ( ) ) {
67
- const islandExperimentalUrl = response . headers ?. [ CopilotStudioClient . islandExperimentalUrlHeaderKey ]
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 stream = response . data
85
- const reader = stream . pipeThrough ( new TextDecoderStream ( ) ) . getReader ( )
86
- let result : string = ''
87
- const results : string [ ] = [ ]
88
-
89
- const processEvents = async ( { done, value } : streamRead ) : Promise < string [ ] > => {
90
- if ( done ) {
91
- logger . debug ( 'Stream complete' )
92
- result += value
93
- results . push ( result )
94
- return results
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
- logger . info ( 'Agent is typing ...' )
97
- result += value
98
-
99
- return await processEvents ( await reader . read ( ) )
100
- }
77
+ } )
101
78
102
- const events : string [ ] = await reader . read ( ) . then ( processEvents )
103
-
104
- events . forEach ( event => {
105
- const values : string [ ] = event . toString ( ) . split ( '\n' )
106
- const validEvents = values . filter ( e => e . substring ( 0 , 4 ) === 'data' && e !== 'data: end\r' )
107
- validEvents . forEach ( ve => {
108
- try {
109
- const act = Activity . fromJson ( ve . substring ( 5 , ve . length ) )
110
- if ( act . type === ActivityTypes . Message ) {
111
- activities . push ( act )
112
- if ( ! this . conversationId . trim ( ) ) {
113
- // Did not get it from the header.
114
- this . conversationId = act . conversation ?. id ?? ''
115
- logger . debug ( `Conversation ID: ${ this . conversationId } ` )
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
- } else {
118
- logger . debug ( `Activity type: ${ act . type } ` )
97
+ } catch ( error ) {
98
+ logger . error ( 'Failed to parse activity:' , error )
119
99
}
120
- } catch ( e ) {
121
- logger . error ( 'Error: ' , e )
122
- throw e
100
+ } else if ( event === 'end' ) {
101
+ logger . debug ( 'Stream complete' )
102
+ break
123
103
}
124
- } )
125
- } )
126
- return activities
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 A promise that resolves to the initial activity of the conversation .
161
+ * @returns An async generator yielding the Agent's Activities .
153
162
*/
154
- public async startConversationAsync ( emitStartConversationEvent : boolean = true ) : Promise < Activity > {
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
- const values = await this . postRequestAsync ( config )
172
- const act = values [ 0 ]
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 A promise that resolves to an array of activities containing the responses .
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
- return this . sendActivity ( activity )
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 A promise that resolves to an array of activities containing the responses .
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
- const values = await this . postRequestAsync ( config )
221
- logger . info ( `Received ${ values . length } activities.` , values )
222
- return values
204
+ yield * this . postRequestAsync ( uriExecute , qbody , 'POST' )
223
205
}
224
206
}
0 commit comments