UNPKG

18.5 kBJavaScriptView Raw
1import _ from 'lodash';
2import createRequest from './request';
3import Debug from 'debug';
4import { decide } from './interpreter';
5import DEFAULTS from './defaults';
6import jwtDecode from 'jwt-decode';
7import Time from './time';
8import {
9 AGENT_ID_ALLOWED_REGEXP,
10 AGENT_ID_MAX_LENGTH,
11 DEFAULT_DECISION_TREE_VERSION,
12 deprecation
13} from './constants';
14import {
15 CraftAiBadRequestError,
16 CraftAiCredentialsError,
17 CraftAiLongRequestTimeOutError
18} from './errors';
19
20let debug = Debug('craft-ai:client');
21
22function resolveAfterTimeout(timeout) {
23 return new Promise((resolve) => setTimeout(() => resolve(), timeout));
24}
25
26// A very simple regex, helps detect some issues.
27const SIMPLE_HTTP_URL_REGEX = /^https?:\/\/.*$/;
28
29function isUrl(url) {
30 return SIMPLE_HTTP_URL_REGEX.test(url);
31}
32
33const isUnvalidId = (id) =>
34 !_.isUndefined(id) && !AGENT_ID_ALLOWED_REGEXP.test(id);
35const isUnvalidConfiguration = (configuration) =>
36 _.isUndefined(configuration) || !_.isObject(configuration);
37const areUnvalidOperations = (operations) =>
38 _.isUndefined(operations) || !_.isArray(operations);
39
40function checkBulkParameters(bulkArray) {
41 if (_.isUndefined(bulkArray)) {
42 throw new CraftAiBadRequestError(
43 'Bad Request, unable to use bulk functionalities without list provided.'
44 );
45 }
46 if (!_.isArray(bulkArray)) {
47 throw new CraftAiBadRequestError(
48 'Bad Request, bulk inputs should be provided within an array.'
49 );
50 }
51 if (!bulkArray.length) {
52 throw new CraftAiBadRequestError(
53 'Bad Request, the array containing bulk inputs is empty.'
54 );
55 }
56}
57
58export default function createClient(tokenOrCfg) {
59 let cfg = _.defaults(
60 {},
61 _.isString(tokenOrCfg) ? { token: tokenOrCfg } : tokenOrCfg,
62 DEFAULTS
63 );
64
65 // Initialization check
66 if (!_.has(cfg, 'token') || !_.isString(cfg.token)) {
67 throw new CraftAiBadRequestError(
68 'Bad Request, unable to create a client with no or invalid token provided.'
69 );
70 }
71 try {
72 const { owner, platform, project } = jwtDecode(cfg.token);
73
74 // Keep the provided values
75 cfg.owner = cfg.owner || owner;
76 cfg.project = cfg.project || project;
77 cfg.url = cfg.url || platform;
78 }
79 catch (e) {
80 throw new CraftAiCredentialsError();
81 }
82 if (!_.has(cfg, 'url') || !isUrl(cfg.url)) {
83 throw new CraftAiBadRequestError(
84 'Bad Request, unable to create a client with no or invalid url provided.'
85 );
86 }
87 if (!_.has(cfg, 'project') || !_.isString(cfg.project)) {
88 throw new CraftAiBadRequestError(
89 'Bad Request, unable to create a client with no or invalid project provided.'
90 );
91 }
92 else {
93 const splittedProject = cfg.project.split('/');
94 if (splittedProject.length >= 2) {
95 cfg.owner = splittedProject[0];
96 cfg.project = splittedProject[1];
97 }
98 }
99 if (!_.has(cfg, 'owner') || !_.isString(cfg.owner)) {
100 throw new CraftAiBadRequestError(
101 'Bad Request, unable to create a client with no or invalid owner provided.'
102 );
103 }
104 if (cfg.proxy != null && !isUrl(cfg.proxy)) {
105 throw new CraftAiBadRequestError(
106 'Bad Request, unable to create a client with an invalid proxy url provided.'
107 );
108 }
109
110 debug(
111 `Creating a client instance for project '${cfg.owner}/${cfg.project}' on '${
112 cfg.url
113 }'.`
114 );
115
116 const request = createRequest(cfg);
117
118 // 'Public' attributes & methods
119 let instance = {
120 cfg: cfg,
121 createAgent: function(configuration, id = undefined) {
122 if (isUnvalidConfiguration(configuration)) {
123 return Promise.reject(
124 new CraftAiBadRequestError(
125 'Bad Request, unable to create an agent with no or invalid configuration provided.'
126 )
127 );
128 }
129
130 if (isUnvalidId(id)) {
131 return Promise.reject(
132 new CraftAiBadRequestError(
133 `Bad Request, unable to create an agent with invalid agent id. It must only contain characters in 'a-zA-Z0-9_-' and must be a string between 1 and ${AGENT_ID_MAX_LENGTH} characters.`
134 )
135 );
136 }
137
138 return request({
139 method: 'POST',
140 path: '/agents',
141 body: {
142 id: id,
143 configuration: configuration
144 }
145 })
146 .then(({ body }) => {
147 debug(`Agent '${body.id}' created.`);
148 return body;
149 });
150 },
151 createAgentBulk: function(agentsList) {
152 checkBulkParameters(agentsList);
153
154 return request({
155 method: 'POST',
156 path: '/bulk/agents',
157 body: agentsList
158 })
159 .then(({ body }) => body);
160 },
161 getAgent: function(agentId) {
162 if (!AGENT_ID_ALLOWED_REGEXP.test(agentId)) {
163 return Promise.reject(
164 new CraftAiBadRequestError(
165 'Bad Request, unable to get an agent with invalid agent id. It must only contain characters in \'a-zA-Z0-9_-\' and cannot be the empty string.'
166 )
167 );
168 }
169
170 return request({
171 method: 'GET',
172 path: `/agents/${agentId}`
173 })
174 .then(({ body }) => body);
175 },
176 listAgents: function(agentId) {
177 return request({
178 method: 'GET',
179 path: '/agents'
180 })
181 .then(({ body }) => body.agentsList);
182 },
183 deleteAgent: function(agentId) {
184 if (_.isUndefined(agentId)) {
185 return Promise.reject(
186 new CraftAiBadRequestError(
187 'Bad Request, unable to delete an agent with no agentId provided.'
188 )
189 );
190 }
191
192 return request({
193 method: 'DELETE',
194 path: `/agents/${agentId}`
195 })
196 .then(({ body }) => {
197 debug(`Agent '${agentId}' deleted`);
198 return body;
199 });
200 },
201 deleteAgentBulk: function(agentsList) {
202 checkBulkParameters(agentsList);
203
204 return request({
205 method: 'DELETE',
206 path: '/bulk/agents',
207 body: agentsList
208 })
209 .then(({ body }) => body);
210 },
211 destroyAgent: function(agentId) {
212 deprecation('client.destroyAgent', 'client.deleteAgent');
213 return this.deleteAgent(agentId);
214 },
215 getAgentContext: function(agentId, t = undefined) {
216 if (_.isUndefined(agentId)) {
217 return Promise.reject(
218 new CraftAiBadRequestError(
219 'Bad Request, unable to get the agent context with no agentId provided.'
220 )
221 );
222 }
223 let posixTimestamp = Time(t).timestamp;
224 if (_.isUndefined(posixTimestamp)) {
225 return Promise.reject(
226 new CraftAiBadRequestError(
227 'Bad Request, unable to get the agent context with an invalid timestamp provided.'
228 )
229 );
230 }
231
232 return request({
233 method: 'GET',
234 path: `/agents/${agentId}/context/state`,
235 query: {
236 t: posixTimestamp
237 }
238 })
239 .then(({ body }) => body);
240 },
241 addAgentContextOperations: function(agentId, operations) {
242 if (_.isUndefined(agentId)) {
243 return Promise.reject(
244 new CraftAiBadRequestError(
245 'Bad Request, unable to add agent context operations with no agentId provided.'
246 )
247 );
248 }
249 if (!_.isArray(operations)) {
250 // Only one given operation
251 operations = [operations];
252 }
253 operations = _.compact(operations);
254
255 if (!operations.length) {
256 const message = `No operation to add to the agent ${cfg.owner}/${
257 cfg.project
258 }/${agentId} context.`;
259
260 debug(message);
261
262 return Promise.resolve({ message });
263 }
264
265 return _(operations)
266 .map(({ context, timestamp }) => ({
267 context: context,
268 timestamp: Time(timestamp).timestamp
269 }))
270 .orderBy('timestamp')
271 .chunk(cfg.operationsChunksSize)
272 .reduce(
273 (p, chunk) =>
274 p.then(() =>
275 request({
276 method: 'POST',
277 path: `/agents/${agentId}/context`,
278 body: chunk
279 })
280 ),
281 Promise.resolve()
282 )
283 .then(() => {
284 const message = `Successfully added ${
285 operations.length
286 } operation(s) to the agent ${cfg.owner}/${
287 cfg.project
288 }/${agentId} context.`;
289 debug(message);
290 return { message };
291 });
292 },
293 addAgentContextOperationsBulk: function(agentsOperationsList) {
294 checkBulkParameters(agentsOperationsList);
295 agentsOperationsList.map(({ id, operations }) => {
296 if (areUnvalidOperations(operations)) {
297 throw new CraftAiBadRequestError(
298 `Bad Request, unable to handle operations for agent ${id}. Operations should be provided within an array.`
299 );
300 }
301 });
302
303 let chunkedData = [];
304 let currentChunk = [];
305 let currentChunkSize = 0;
306
307 for (let agent of agentsOperationsList) {
308 if (agent.operations && _.isArray(agent.operations)) {
309 if (
310 currentChunkSize + agent.operations.length >
311 cfg.operationsChunksSize &&
312 currentChunkSize.length
313 ) {
314 chunkedData.push(currentChunk);
315 currentChunkSize = 0;
316 currentChunk = [];
317 }
318
319 if (agent.operations.length > cfg.operationsChunksSize) {
320 chunkedData.push([agent]);
321 currentChunkSize = 0;
322 }
323 else {
324 currentChunkSize += agent.operations.length;
325 currentChunk.push(agent);
326 }
327 }
328 }
329
330 if (currentChunk.length) {
331 chunkedData.push(currentChunk);
332 }
333
334 return Promise.all(
335 chunkedData.map((chunk) => {
336 if (chunk.length > 1) {
337 return request({
338 method: 'POST',
339 path: '/bulk/context',
340 body: chunk
341 })
342 .then(({ body }) => body);
343 }
344 else {
345 return this.addAgentContextOperations(
346 chunk[0].id,
347 chunk[0].operations
348 )
349 .then(({ message }) => [
350 { id: chunk[0].id, status: 201, message }
351 ]);
352 }
353 })
354 )
355 .then(_.flattenDeep);
356 },
357 getAgentContextOperations: function(
358 agentId,
359 start = undefined,
360 end = undefined
361 ) {
362 if (_.isUndefined(agentId)) {
363 return Promise.reject(
364 new CraftAiBadRequestError(
365 'Bad Request, unable to get agent context operations with no agentId provided.'
366 )
367 );
368 }
369 let startTimestamp;
370 if (start) {
371 startTimestamp = Time(start).timestamp;
372 if (_.isUndefined(startTimestamp)) {
373 return Promise.reject(
374 new CraftAiBadRequestError(
375 'Bad Request, unable to get agent context operations with an invalid \'start\' timestamp provided.'
376 )
377 );
378 }
379 }
380 let endTimestamp;
381 if (end) {
382 endTimestamp = Time(end).timestamp;
383 if (_.isUndefined(endTimestamp)) {
384 return Promise.reject(
385 new CraftAiBadRequestError(
386 'Bad Request, unable to get agent context operations with an invalid \'end\' timestamp provided.'
387 )
388 );
389 }
390 }
391
392 const requestFollowingPages = ({ operations, nextPageUrl }) => {
393 if (!nextPageUrl) {
394 return Promise.resolve(operations);
395 }
396 return request({ url: nextPageUrl }, this)
397 .then(
398 ({ body, nextPageUrl }) =>
399 requestFollowingPages({
400 operations: operations.concat(body),
401 nextPageUrl
402 })
403 );
404 };
405
406 return request({
407 method: 'GET',
408 path: `/agents/${agentId}/context`,
409 query: {
410 start: startTimestamp,
411 end: endTimestamp
412 }
413 })
414 .then(({ body, nextPageUrl }) =>
415 requestFollowingPages({
416 operations: body,
417 nextPageUrl
418 })
419 );
420 },
421 getAgentStateHistory: function(
422 agentId,
423 start = undefined,
424 end = undefined
425 ) {
426 if (_.isUndefined(agentId)) {
427 return Promise.reject(
428 new CraftAiBadRequestError(
429 'Bad Request, unable to get agent state history with no agentId provided.'
430 )
431 );
432 }
433 let startTimestamp;
434 if (start) {
435 startTimestamp = Time(start).timestamp;
436 if (_.isUndefined(startTimestamp)) {
437 return Promise.reject(
438 new CraftAiBadRequestError(
439 'Bad Request, unable to get agent state history with an invalid \'start\' timestamp provided.'
440 )
441 );
442 }
443 }
444 let endTimestamp;
445 if (end) {
446 endTimestamp = Time(end).timestamp;
447 if (_.isUndefined(endTimestamp)) {
448 return Promise.reject(
449 new CraftAiBadRequestError(
450 'Bad Request, unable to get agent state history with an invalid \'end\' timestamp provided.'
451 )
452 );
453 }
454 }
455
456 const requestFollowingPages = ({ stateHistory, nextPageUrl }) => {
457 if (!nextPageUrl) {
458 return Promise.resolve(stateHistory);
459 }
460 return request({ url: nextPageUrl })
461 .then(({ body, nextPageUrl }) =>
462 requestFollowingPages({
463 stateHistory: stateHistory.concat(body),
464 nextPageUrl
465 })
466 );
467 };
468
469 return request({
470 method: 'GET',
471 path: `/agents/${agentId}/context/state/history`,
472 query: {
473 start: startTimestamp,
474 end: endTimestamp
475 }
476 })
477 .then(({ body, nextPageUrl }) =>
478 requestFollowingPages({
479 stateHistory: body,
480 nextPageUrl
481 })
482 );
483 },
484 getAgentInspectorUrl: function(agentId, t = undefined) {
485 deprecation(
486 'client.getAgentInspectorUrl',
487 'client.getSharedAgentInspectorUrl'
488 );
489 return this.getSharedAgentInspectorUrl(agentId, t);
490 },
491 getSharedAgentInspectorUrl: function(agentId, t = undefined) {
492 return request({
493 method: 'GET',
494 path: `/agents/${agentId}/shared`
495 })
496 .then(({ body }) => {
497 if (_.isUndefined(t)) {
498 return body.shortUrl;
499 }
500 else {
501 let posixTimestamp = Time(t).timestamp;
502 return `${body.shortUrl}?t=${posixTimestamp}`;
503 }
504 });
505 },
506 deleteSharedAgentInspectorUrl: function(agentId) {
507 return request({
508 method: 'DELETE',
509 path: `/agents/${agentId}/shared`
510 })
511 .then(() => {
512 debug(`Delete shared inspector link for agent '${agentId}'.`);
513 });
514 },
515 getAgentDecisionTree: function(
516 agentId,
517 t = undefined,
518 version = DEFAULT_DECISION_TREE_VERSION
519 ) {
520 if (_.isUndefined(agentId)) {
521 return Promise.reject(
522 new CraftAiBadRequestError(
523 'Bad Request, unable to retrieve an agent decision tree with no agentId provided.'
524 )
525 );
526 }
527 let posixTimestamp = Time(t).timestamp;
528 if (_.isUndefined(posixTimestamp)) {
529 return Promise.reject(
530 new CraftAiBadRequestError(
531 'Bad Request, unable to retrieve an agent decision tree with an invalid timestamp provided.'
532 )
533 );
534 }
535
536 const agentDecisionTreeRequest = () =>
537 request({
538 method: 'GET',
539 path: `/agents/${agentId}/decision/tree`,
540 query: {
541 t: posixTimestamp
542 },
543 headers: {
544 'x-craft-ai-tree-version': version
545 }
546 })
547 .then(({ body }) => body);
548
549 if (!cfg.decisionTreeRetrievalTimeout) {
550 // Don't retry
551 return agentDecisionTreeRequest();
552 }
553 else {
554 const start = Date.now();
555 return Promise.race([
556 agentDecisionTreeRequest()
557 .catch((error) => {
558 const requestDuration = Date.now() - start;
559 const expectedRetryDuration = requestDuration + 2000; // Let's add some margin
560 const timeoutBeforeRetrying =
561 cfg.decisionTreeRetrievalTimeout -
562 requestDuration -
563 expectedRetryDuration;
564 if (
565 error instanceof CraftAiLongRequestTimeOutError &&
566 timeoutBeforeRetrying > 0
567 ) {
568 // First timeout, let's retry once near the end of the set timeout
569 return resolveAfterTimeout(timeoutBeforeRetrying)
570 .then(() =>
571 agentDecisionTreeRequest()
572 );
573 }
574 else {
575 return Promise.reject(error);
576 }
577 }),
578 resolveAfterTimeout(cfg.decisionTreeRetrievalTimeout)
579 .then(() => {
580 throw new CraftAiLongRequestTimeOutError();
581 })
582 ]);
583 }
584 },
585 getAgentDecisionTreeBulk: function(agentsList) {
586 checkBulkParameters(agentsList);
587
588 return request({
589 method: 'POST',
590 path: '/bulk/decision_tree',
591 body: agentsList
592 })
593 .then(({ body }) => body);
594 },
595 computeAgentDecision: function(agentId, t, ...contexts) {
596 if (_.isUndefined(agentId)) {
597 return Promise.reject(
598 new CraftAiBadRequestError(
599 'Bad Request, unable to compute an agent decision with no agentId provided.'
600 )
601 );
602 }
603 let posixTimestamp = Time(t).timestamp;
604 if (_.isUndefined(posixTimestamp)) {
605 return Promise.reject(
606 new CraftAiBadRequestError(
607 'Bad Request, unable to compute an agent decision with no or invalid timestamp provided.'
608 )
609 );
610 }
611 if (_.isUndefined(contexts) || _.size(contexts) === 0) {
612 return Promise.reject(
613 new CraftAiBadRequestError(
614 'Bad Request, unable to compute an agent decision with no context provided.'
615 )
616 );
617 }
618
619 return request({
620 method: 'GET',
621 path: `/agents/${agentId}/decision/tree`,
622 query: {
623 t: posixTimestamp
624 }
625 })
626 .then(({ body }) => {
627 let decision = decide(body, ...contexts);
628 decision.timestamp = posixTimestamp;
629 return decision;
630 });
631 }
632 };
633
634 return instance;
635}