@microsoft/agents-copilotstudio-client
Version:
Microsoft Copilot Studio Client for JavaScript. Copilot Studio Client.
225 lines (196 loc) • 8.4 kB
text/typescript
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ConnectionSettings } from './connectionSettings'
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
import { getCopilotStudioConnectionUrl, getTokenAudience } from './powerPlatformEnvironment'
import { Activity, ActivityTypes, ConversationAccount } from '@microsoft/agents-activity'
import { ExecuteTurnRequest } from './executeTurnRequest'
import { debug } from '@microsoft/agents-activity/logger'
import { version } from '../package.json'
import os from 'os'
const logger = debug('copilot-studio:client')
interface streamRead {
done: boolean,
value: string
}
/**
* Client for interacting with Microsoft Copilot Studio services.
* Provides functionality to start conversations and send messages to Copilot Studio bots.
*/
export class CopilotStudioClient {
/** Header key for conversation ID. */
private static readonly conversationIdHeaderKey: string = 'x-ms-conversationid'
/** Island Header key */
private static readonly islandExperimentalUrlHeaderKey: string = 'x-ms-d2e-experimental'
/** The ID of the current conversation. */
private conversationId: string = ''
/** The connection settings for the client. */
private readonly settings: ConnectionSettings
/** The Axios instance used for HTTP requests. */
private readonly client: AxiosInstance
/**
* Returns the scope URL needed to connect to Copilot Studio from the connection settings.
* This is used for authentication token audience configuration.
* @param settings Copilot Studio connection settings.
* @returns The scope URL for token audience.
*/
static scopeFromSettings: (settings: ConnectionSettings) => string = getTokenAudience
/**
* Creates an instance of CopilotStudioClient.
* @param settings The connection settings.
* @param token The authentication token.
*/
constructor (settings: ConnectionSettings, token: string) {
this.settings = settings
this.client = axios.create()
this.client.defaults.headers.common.Authorization = `Bearer ${token}`
this.client.defaults.headers.common['User-Agent'] = CopilotStudioClient.getProductInfo()
}
private async postRequestAsync (axiosConfig: AxiosRequestConfig): Promise<Activity[]> {
const activities: Activity[] = []
logger.debug(`>>> SEND TO ${axiosConfig.url}`)
const response = await this.client(axiosConfig)
if (this.settings.useExperimentalEndpoint && !this.settings.directConnectUrl?.trim()) {
const islandExperimentalUrl = response.headers?.[CopilotStudioClient.islandExperimentalUrlHeaderKey]
if (islandExperimentalUrl) {
this.settings.directConnectUrl = islandExperimentalUrl
logger.debug(`Island Experimental URL: ${islandExperimentalUrl}`)
}
}
this.conversationId = response.headers?.[CopilotStudioClient.conversationIdHeaderKey] ?? ''
if (this.conversationId) {
logger.debug(`Conversation ID: ${this.conversationId}`)
}
const sanitizedHeaders = { ...response.headers }
delete sanitizedHeaders['Authorization']
delete sanitizedHeaders[CopilotStudioClient.conversationIdHeaderKey]
logger.debug('Headers received:', sanitizedHeaders)
const stream = response.data
const reader = stream.pipeThrough(new TextDecoderStream()).getReader()
let result: string = ''
const results: string[] = []
const processEvents = async ({ done, value }: streamRead): Promise<string[]> => {
if (done) {
logger.debug('Stream complete')
result += value
results.push(result)
return results
}
logger.info('Agent is typing ...')
result += value
return await processEvents(await reader.read())
}
const events: string[] = await reader.read().then(processEvents)
events.forEach(event => {
const values: string[] = event.toString().split('\n')
const validEvents = values.filter(e => e.substring(0, 4) === 'data' && e !== 'data: end\r')
validEvents.forEach(ve => {
try {
const act = Activity.fromJson(ve.substring(5, ve.length))
if (act.type === ActivityTypes.Message) {
activities.push(act)
if (!this.conversationId.trim()) {
// Did not get it from the header.
this.conversationId = act.conversation?.id ?? ''
logger.debug(`Conversation ID: ${this.conversationId}`)
}
} else {
logger.debug(`Activity type: ${act.type}`)
}
} catch (e) {
logger.error('Error: ', e)
throw e
}
})
})
return activities
}
/**
* Appends this package.json version to the User-Agent header.
* - For browser environments, it includes the user agent of the browser.
* - For Node.js environments, it includes the Node.js version, platform, architecture, and release.
* @returns A string containing the product information, including version and user agent.
*/
private static getProductInfo (): string {
const versionString = `CopilotStudioClient.agents-sdk-js/${version}`
let userAgent: string
if (typeof window !== 'undefined' && window.navigator) {
userAgent = `${versionString} ${navigator.userAgent}`
} else {
userAgent = `${versionString} nodejs/${process.version} ${os.platform()}-${os.arch()}/${os.release()}`
}
logger.debug(`User-Agent: ${userAgent}`)
return userAgent
}
/**
* Starts a new conversation with the Copilot Studio service.
* @param emitStartConversationEvent Whether to emit a start conversation event. Defaults to true.
* @returns A promise that resolves to the initial activity of the conversation.
*/
public async startConversationAsync (emitStartConversationEvent: boolean = true): Promise<Activity> {
const uriStart: string = getCopilotStudioConnectionUrl(this.settings)
const body = { emitStartConversationEvent }
const config: AxiosRequestConfig = {
method: 'post',
url: uriStart,
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
},
data: body,
responseType: 'stream',
adapter: 'fetch'
}
logger.info('Starting conversation ...')
const values = await this.postRequestAsync(config)
const act = values[0]
logger.info(`Conversation '${act.conversation?.id}' started. Received ${values.length} activities.`, values)
return act
}
/**
* Sends a question to the Copilot Studio service and retrieves the response activities.
* @param question The question to ask.
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
* @returns A promise that resolves to an array of activities containing the responses.
*/
public async askQuestionAsync (question: string, conversationId: string = this.conversationId) {
const conversationAccount: ConversationAccount = {
id: conversationId
}
const activityObj = {
type: 'message',
text: question,
conversation: conversationAccount
}
const activity = Activity.fromObject(activityObj)
return this.sendActivity(activity)
}
/**
* Sends an activity to the Copilot Studio service and retrieves the response activities.
* @param activity The activity to send.
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
* @returns A promise that resolves to an array of activities containing the responses.
*/
public async sendActivity (activity: Activity, conversationId: string = this.conversationId) {
const localConversationId = activity.conversation?.id ?? conversationId
const uriExecute = getCopilotStudioConnectionUrl(this.settings, localConversationId)
const qbody: ExecuteTurnRequest = new ExecuteTurnRequest(activity)
const config: AxiosRequestConfig = {
method: 'post',
url: uriExecute,
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
},
data: qbody,
responseType: 'stream',
adapter: 'fetch'
}
logger.info('Sending activity...', activity)
const values = await this.postRequestAsync(config)
logger.info(`Received ${values.length} activities.`, values)
return values
}
}