UNPKG

17.7 kBPlain TextView Raw
1/**
2 * Copyright (c) Microsoft Corporation. All rights reserved.
3 * Licensed under the MIT License.
4 */
5import * as CLM from '@conversationlearner/models'
6import { CLDebug } from './CLDebug'
7import * as Request from 'request'
8import * as constants from './constants'
9import { IActionResult } from './CLRunner'
10
11type HTTP_METHOD = 'GET' | 'PUT' | 'POST' | 'DELETE'
12const requestMethodMap = new Map<HTTP_METHOD, typeof Request.get | typeof Request.post>([
13 ['GET', Request.get],
14 ['PUT', Request.put],
15 ['POST', Request.post],
16 ['DELETE', Request.delete]
17])
18
19export interface ICLClientOptions {
20 CONVERSATION_LEARNER_SERVICE_URI: string
21 // This should only set when directly targeting cognitive services ppe environment.
22 APIM_SUBSCRIPTION_KEY: string | undefined
23 LUIS_AUTHORING_KEY: string | undefined
24 LUIS_SUBSCRIPTION_KEY?: string
25}
26
27export class CLClient {
28 private options: ICLClientOptions
29
30 constructor(options: ICLClientOptions) {
31 this.options = options;
32
33 if (options.APIM_SUBSCRIPTION_KEY === undefined) {
34 options.APIM_SUBSCRIPTION_KEY = options.LUIS_AUTHORING_KEY;
35 }
36 }
37
38 public ValidationError(): string | null {
39 if (typeof this.options.CONVERSATION_LEARNER_SERVICE_URI !== 'string' || this.options.CONVERSATION_LEARNER_SERVICE_URI.length === 0) {
40 return `CONVERSATION_LEARNER_SERVICE_URI must be a non-empty string. You passed: ${this.options.CONVERSATION_LEARNER_SERVICE_URI}`
41 }
42
43 if (typeof this.options.LUIS_AUTHORING_KEY !== 'string' || this.options.LUIS_AUTHORING_KEY.length === 0) {
44 return `LUIS_AUTHORING_KEY must be a non-empty string. You passed: ${this.options.LUIS_AUTHORING_KEY}`
45 }
46 return null;
47 }
48
49 public LuisAuthoringKey(): string | undefined {
50 return this.options.LUIS_AUTHORING_KEY;
51 }
52
53 private BuildURL(baseUri: string, apiPath: string, query?: string) {
54 let uri = baseUri + (!baseUri.endsWith('/') ? '/' : '') + apiPath
55 if (query) {
56 uri += `?${query}`
57 }
58 return uri
59 }
60
61 private MakeURL(apiPath: string, query?: string) {
62 return this.BuildURL(this.options.CONVERSATION_LEARNER_SERVICE_URI, apiPath, query)
63 }
64
65 private MakeSessionURL(apiPath: string, query?: string) {
66 // check if request is bypassing cognitive services APIM
67 if (!this.options.CONVERSATION_LEARNER_SERVICE_URI.includes('api.cognitive.microsoft.com')) {
68 // In this case we are not chaning the serviceUrl and it stays the same,
69 // for example: https://localhost:37936/api/v1/ -> https://localhost:37936/api/v1/
70 return this.MakeURL(apiPath, query)
71 }
72
73 // The base uri for session API in cognitive services APIM is in the form of '<service url>/conversationlearner/session/v1.0/'
74 // Session API are the following api:
75 // 1) POST /app/<appId>/session
76 // 2) PUT /app/<appId>/session/extract
77 // 3) PUT /app/<appId>/session/score
78 // 4) DELETE /app/<appId>/session
79 let baseUri = this.options.CONVERSATION_LEARNER_SERVICE_URI.endsWith('/') ?
80 this.options.CONVERSATION_LEARNER_SERVICE_URI :
81 `${this.options.CONVERSATION_LEARNER_SERVICE_URI}/`
82 const apimVersionSuffix = '/v1.0/'
83 if (baseUri.endsWith(apimVersionSuffix)) {
84 // In this case, serviceurl has api version information in it; "session" will be inserted before /v1.0
85 // this means that https://westus.api.cognitive.microsoft.com/conversationlearner/v1.0/ becomes
86 // https://westus.api.cognitive.microsoft.com/conversationlearner/session/v1.0/
87 baseUri = `${baseUri.substring(0, baseUri.lastIndexOf(apimVersionSuffix))}/session${apimVersionSuffix}`
88 }
89 else {
90 // When api version information is not part of the serviceUrl, we simply add /session/ to end of the api
91 // example: https://westus.api.cognitive.microsoft.com/conversationlearner/ -> https://westus.api.cognitive.microsoft.com/conversationlearner/session/
92 baseUri += 'session/'
93 }
94 return this.BuildURL(baseUri, apiPath, query)
95 }
96
97 private send<T>(method: HTTP_METHOD, url: string, body?: any): Promise<T> {
98 return new Promise((resolve, reject) => {
99 const requestData = {
100 url,
101 headers: {
102 [constants.luisAuthoringKeyHeader]: this.options.LUIS_AUTHORING_KEY,
103 [constants.luisSubscriptionKeyHeader]: this.options.LUIS_SUBSCRIPTION_KEY,
104 // This is only used when directly targeting service. In future APIM will provide user/subscription id associated from LUIS key
105 [constants.apimSubscriptionIdHeader]: this.options.LUIS_AUTHORING_KEY,
106 [constants.apimSubscriptionKeyHeader]: this.options.APIM_SUBSCRIPTION_KEY
107 },
108 json: true,
109 body
110 }
111
112 CLDebug.LogRequest(method, url, requestData)
113 const requestMethod = requestMethodMap.get(method)
114 if (!requestMethod) {
115 throw new Error(`Request method not found for http verb: ${method}`)
116 }
117
118 requestMethod(requestData, (error, response, responseBody) => {
119 if (error) {
120 reject(error)
121 } else if (response.statusCode && response.statusCode >= 300) {
122 reject(response)
123 } else {
124 resolve(responseBody)
125 }
126 })
127 })
128 }
129
130 //==============================================================================
131 // App
132 //=============================================================================
133 /**
134 * Retrieve information about a specific application
135 * If the app ID isn't found in the set of (non-archived) apps,
136 * returns 404 error ("not found")
137 */
138 public GetApp(appId: string): Promise<CLM.AppBase> {
139 let apiPath = `app/${appId}`
140 return this.send('GET', this.MakeURL(apiPath))
141 }
142
143 public GetAppSource(appId: string, packageId: string): Promise<CLM.AppDefinition> {
144 let apiPath = `app/${appId}/source?package=${packageId}`
145 return this.send('GET', this.MakeURL(apiPath))
146 }
147
148 public async PostAppSource(appId: string, appDefinition: CLM.AppDefinition): Promise<void> {
149 let apiPath = `app/${appId}/source`
150 await this.send('POST', this.MakeURL(apiPath), appDefinition);
151 }
152
153 /** Retrieve a list of (active) applications */
154 public GetApps(query: string): Promise<CLM.AppList> {
155 let apiPath = `apps`
156 return this.send('GET', this.MakeURL(apiPath, query))
157 }
158
159 /** Create a new application */
160 public CopyApps(srcUserId: string, destUserId: string, appId: string, luisSubscriptionKey: string): Promise<string> {
161 const apiPath = `apps/copy?srcUserId=${srcUserId}&destUserId=${destUserId}&appId=${appId}&luisSubscriptionKey=${luisSubscriptionKey}`
162 return this.send('POST', this.MakeURL(apiPath))
163 }
164
165 /**
166 * Archive an existing application
167 * Note: "deleting" an application doesn't destroy it, but rather archives
168 * it for a period (eg 30 days). During the archive period, the application
169 * can be restored with the next API call. At the end of the archive period,
170 * the application is destroyed.
171 */
172 public ArchiveApp(appId: string): Promise<string> {
173 let apiPath = `app/${appId}`
174 return this.send('DELETE', this.MakeURL(apiPath))
175 }
176
177 /**
178 * Create a new application
179 */
180 // TODO: Fix API to return full object
181 public async AddApp(app: CLM.AppBase, query: string): Promise<string> {
182 const apiPath = `app`
183 // Note: This isn't an actual AppBase, but just { appId, packageId }
184 const appResponse = await this.send<CLM.AppBase>('POST', this.MakeURL(apiPath, query), app)
185 return appResponse.appId
186 }
187
188 /** Creates a new package tag */
189 public PublishApp(appId: string, tagName: string): Promise<CLM.PackageReference> {
190 let apiPath = `app/${appId}/publish?version=${tagName}`
191 return this.send('PUT', this.MakeURL(apiPath))
192 }
193
194 /** Sets a package tags as the live version */
195 public PublishProdPackage(appId: string, packageId: string): Promise<string> {
196 let apiPath = `app/${appId}/publish/${packageId}`
197 return this.send('POST', this.MakeURL(apiPath))
198 }
199
200 //==============================================================================
201 // Entity
202 //=============================================================================
203 /**
204 * Retrieves definitions of ALL entities in the latest package
205 * (or the specified package, if provided). To retrieve just the IDs
206 * of all entities, see the GetEntityIds method
207 */
208 public GetEntities(appId: string, query?: string): Promise<CLM.EntityList> {
209 let apiPath = `app/${appId}/entities`
210 return this.send('GET', this.MakeURL(apiPath, query))
211 }
212
213 //=============================================================================
214 // Log Dialogs
215 //=============================================================================
216 /**
217 * Retrieves the contents of many/all logDialogs.
218 * To retrieve just a list of IDs of all logDialogs,
219 * see the GET GetLogDialogIds method.
220 */
221 public GetLogDialogs(appId: string, packageIds: string[]): Promise<CLM.LogDialogList> {
222 const packages = packageIds.map(p => `package=${p}`).join("&")
223 const apiPath = `app/${appId}/logdialogs?includeDefinitions=false&${packages}`
224 return this.send('GET', this.MakeURL(apiPath))
225 }
226
227 /** Runs entity extraction (prediction). */
228 public LogDialogExtract(
229 appId: string,
230 logDialogId: string,
231 turnIndex: string,
232 userInput: CLM.UserInput
233 ): Promise<CLM.ExtractResponse> {
234 let apiPath = `app/${appId}/logdialog/${logDialogId}/extractor/${turnIndex}`
235 // Always retrieve entity list
236 let query = 'includeDefinitions=true'
237 return this.send('PUT', this.MakeURL(apiPath, query), userInput)
238 }
239
240
241 //=============================================================================
242 // Train Dialogs
243 //=============================================================================
244 /**
245 * Retrieves information about a specific trainDialog in the current package
246 * (or the specified package, if provided)
247 */
248 public GetTrainDialog(appId: string, trainDialogId: string, includeDefinitions: boolean = false): Promise<CLM.TrainDialog> {
249 let query = `includeDefinitions=${includeDefinitions}`
250 let apiPath = `app/${appId}/traindialog/${trainDialogId}`
251 return this.send('GET', this.MakeURL(apiPath, query))
252 }
253
254 /** Runs entity extraction (prediction). */
255 public TrainDialogExtract(
256 appId: string,
257 trainDialogId: string,
258 turnIndex: string,
259 userInput: CLM.UserInput
260 ): Promise<CLM.ExtractResponse> {
261 let apiPath = `app/${appId}/traindialog/${trainDialogId}/extractor/${turnIndex}`
262 // Always retrieve entity list
263 let query = 'includeDefinitions=true'
264 return this.send('PUT', this.MakeURL(apiPath, query), userInput)
265 }
266
267 /**
268 * Returns a 409 if text variation conflicts with existing labels, otherwise 200
269 * filteredDialog is dialog to ignore when checking for conflicts
270 */
271 public TrainDialogValidateTextVariation(appId: string, trainDialogId: string, textVariation: CLM.TextVariation, excludeConflictCheckId: string): Promise<null> {
272 let apiPath = `app/${appId}/traindialog/${trainDialogId}/extractor/textvariation`
273 // Note: service can take a list of filteredDialogs, but we just use one for now
274 let query = excludeConflictCheckId ? `filteredDialogs=${excludeConflictCheckId}` : undefined
275 return this.send('POST', this.MakeURL(apiPath, query), textVariation)
276 }
277
278 //=============================================================================
279 // Session
280 //=============================================================================
281
282 /** Creates a new session and a corresponding logDialog */
283 public StartSession(appId: string, sessionCreateParams: CLM.SessionCreateParams): Promise<CLM.Session> {
284 let apiPath = `app/${appId}/session`
285 return this.send('POST', this.MakeSessionURL(apiPath), sessionCreateParams)
286 }
287
288 /** Gets information about a session */
289 // TODO: move this to session API path next time that the API definition gets updated
290 public GetSession(appId: string, sessionId: string): Promise<CLM.Session> {
291 let apiPath = `app/${appId}/session/${sessionId}`
292 return this.send('GET', this.MakeURL(apiPath))
293 }
294
295 /** Runs entity extraction (prediction). */
296 public SessionExtract(appId: string, sessionId: string, userInput: CLM.UserInput): Promise<CLM.ExtractResponse> {
297 let apiPath = `app/${appId}/session/${sessionId}/extractor`
298
299 // Always retrieve entity list
300 let query = 'includeDefinitions=true'
301
302 return this.send('PUT', this.MakeSessionURL(apiPath, query), userInput)
303 }
304
305 /** Take a turn and returns chosen action */
306 public SessionScore(appId: string, sessionId: string, scorerInput: CLM.ScoreInput): Promise<CLM.ScoreResponse> {
307 let apiPath = `app/${appId}/session/${sessionId}/scorer`
308 return this.send('PUT', this.MakeSessionURL(apiPath), scorerInput)
309 }
310
311 public SessionLogicResult(appId: string, sessionId: string, actionId: string, actionResult: IActionResult) {
312 let apiPath = `app/${appId}/session/${sessionId}/scorerSteps/action/${actionId}/logicResult`
313 return this.send('PUT', this.MakeSessionURL(apiPath), { logicResult: actionResult.logicResult })
314 }
315
316 /** End a session. */
317 public EndSession(appId: string, sessionId: string): Promise<string> {
318 let apiPath = `app/${appId}/session/${sessionId}`
319 //TODO: remove this when redundant query parameter is removed
320 let query = 'saveDialog=false'
321 return this.send('DELETE', this.MakeSessionURL(apiPath, query))
322 }
323
324 //=============================================================================
325 // Teach
326 //=============================================================================
327
328 /** Creates a new teaching session and a corresponding trainDialog */
329 public StartTeach(appId: string, createTeachParams: CLM.CreateTeachParams): Promise<CLM.TeachResponse> {
330 let apiPath = `app/${appId}/teach`
331 return this.send('POST', this.MakeURL(apiPath), createTeachParams)
332 }
333
334 /**
335 * Runs entity extraction (prediction).
336 * If a more recent version of the package is available on
337 * the server, the session will first migrate to that newer version. This
338 * doesn't affect the trainDialog maintained.
339 */
340 public TeachExtract(appId: string, teachId: string, userInput: CLM.UserInput, excludeConflictCheckId: string | null): Promise<CLM.ExtractResponse> {
341 let apiPath = `app/${appId}/teach/${teachId}/extractor`
342 // Note: service can take a list of filteredDialogs, but we just use one for now
343 let query = `includeDefinitions=true`
344 if (excludeConflictCheckId) {
345 query += `&filteredDialogs=${excludeConflictCheckId}`
346 }
347 return this.send('PUT', this.MakeURL(apiPath, query), { text: userInput.text })
348 }
349
350 /**
351 * Uploads a labeled entity extraction instance
352 * ie "commits" an entity extraction label, appending it to the teach session's
353 * trainDialog, and advancing the dialog. This may yield produce a new package.
354 */
355 public TeachExtractFeedback(appId: string, teachId: string, extractorStep: CLM.TrainExtractorStep): Promise<CLM.TeachResponse> {
356 let apiPath = `app/${appId}/teach/${teachId}/extractor`
357 return this.send('POST', this.MakeURL(apiPath), extractorStep)
358 }
359
360 /**
361 * Takes a turn and return distribution over actions.
362 * If a more recent version of the package is
363 * available on the server, the session will first migrate to that newer version.
364 * This doesn't affect the trainDialog maintained by the teaching session.
365 */
366 public TeachScore(appId: string, teachId: string, scorerInput: CLM.ScoreInput): Promise<CLM.ScoreResponse> {
367 let apiPath = `app/${appId}/teach/${teachId}/scorer`
368 return this.send('PUT', this.MakeURL(apiPath), scorerInput)
369 }
370
371 /**
372 * Uploads a labeled scorer step instance
373 * – ie "commits" a scorer label, appending it to the teach session's
374 * trainDialog, and advancing the dialog. This may yield produce a new package.
375 */
376 public TeachScoreFeedback(appId: string, teachId: string, scorerResponse: CLM.TrainScorerStep): Promise<CLM.TeachResponse> {
377 let apiPath = `app/${appId}/teach/${teachId}/scorer`
378 return this.send('POST', this.MakeURL(apiPath), scorerResponse)
379 }
380
381 /**
382 * Ends a teach.
383 * For Teach sessions, does NOT delete the associated trainDialog.
384 * To delete the associated trainDialog, call DELETE on the trainDialog.
385 */
386 public EndTeach(appId: string, teachId: string, save: boolean): Promise<CLM.TrainResponse> {
387 const query = `saveDialog=${save}`
388 let apiPath = `app/${appId}/teach/${teachId}`
389 return this.send('DELETE', this.MakeURL(apiPath, query))
390 }
391}