UNPKG

13.7 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
33export default function createClient(tokenOrCfg) {
34 let cfg = _.defaults(
35 {},
36 _.isString(tokenOrCfg) ? { token: tokenOrCfg } : tokenOrCfg,
37 DEFAULTS
38 );
39
40 // Initialization check
41 if (!_.has(cfg, 'token') || !_.isString(cfg.token)) {
42 throw new CraftAiBadRequestError('Bad Request, unable to create a client with no or invalid token provided.');
43 }
44 try {
45 const { owner, platform, project } = jwtDecode(cfg.token);
46
47 // Keep the provided values
48 cfg.owner = cfg.owner || owner;
49 cfg.project = cfg.project || project;
50 cfg.url = cfg.url || platform;
51 }
52 catch (e) {
53 throw new CraftAiCredentialsError();
54 }
55 if (!_.has(cfg, 'url') || !isUrl(cfg.url)) {
56 throw new CraftAiBadRequestError('Bad Request, unable to create a client with no or invalid url provided.');
57 }
58 if (!_.has(cfg, 'project') || !_.isString(cfg.project)) {
59 throw new CraftAiBadRequestError('Bad Request, unable to create a client with no or invalid project provided.');
60 }
61 else {
62 const splittedProject = cfg.project.split('/');
63 if (splittedProject.length >= 2) {
64 cfg.owner = splittedProject[0];
65 cfg.project = splittedProject[1];
66 }
67 }
68 if (!_.has(cfg, 'owner') || !_.isString(cfg.owner)) {
69 throw new CraftAiBadRequestError('Bad Request, unable to create a client with no or invalid owner provided.');
70 }
71 if (cfg.proxy != null && !isUrl(cfg.proxy)) {
72 throw new CraftAiBadRequestError('Bad Request, unable to create a client with an invalid proxy url provided.');
73 }
74
75 debug(`Creating a client instance for project '${cfg.owner}/${cfg.project}' on '${cfg.url}'.`);
76
77 const request = createRequest(cfg);
78
79 // 'Public' attributes & methods
80 let instance = {
81 cfg: cfg,
82 createAgent: function(configuration, id = undefined) {
83 if (_.isUndefined(configuration) || !_.isObject(configuration)) {
84 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to create an agent with no or invalid configuration provided.'));
85 }
86
87 if (!_.isUndefined(id) && !AGENT_ID_ALLOWED_REGEXP.test(id)) {
88 return Promise.reject(new CraftAiBadRequestError(`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.`));
89 }
90
91 return request({
92 method: 'POST',
93 path: '/agents',
94 body: {
95 id: id,
96 configuration: configuration
97 }
98 })
99 .then(({ body }) => {
100 debug(`Agent '${body.id}' created.`);
101 return body;
102 });
103 },
104 getAgent: function(agentId) {
105 if (!AGENT_ID_ALLOWED_REGEXP.test(agentId)) {
106 return Promise.reject(new CraftAiBadRequestError('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.'));
107 }
108
109 return request({
110 method: 'GET',
111 path: `/agents/${agentId}`
112 })
113 .then(({ body }) => body);
114 },
115 listAgents: function(agentId) {
116 return request({
117 method: 'GET',
118 path: '/agents'
119 })
120 .then(({ body }) => body.agentsList);
121 },
122 deleteAgent: function(agentId) {
123 if (_.isUndefined(agentId)) {
124 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to delete an agent with no agentId provided.'));
125 }
126
127 return request({
128 method: 'DELETE',
129 path: `/agents/${agentId}`
130 })
131 .then(({ body }) => {
132 debug(`Agent '${agentId}' deleted`);
133 return body;
134 });
135 },
136 destroyAgent: function(agentId) {
137 deprecation('client.destroyAgent', 'client.deleteAgent');
138 return this.deleteAgent(agentId);
139 },
140 getAgentContext: function(agentId, t = undefined) {
141 if (_.isUndefined(agentId)) {
142 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to get the agent context with no agentId provided.'));
143 }
144 let posixTimestamp = Time(t).timestamp;
145 if (_.isUndefined(posixTimestamp)) {
146 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to get the agent context with an invalid timestamp provided.'));
147 }
148
149 return request({
150 method: 'GET',
151 path: `/agents/${agentId}/context/state`,
152 query: {
153 t: posixTimestamp
154 }
155 })
156 .then(({ body }) => body);
157 },
158 addAgentContextOperations: function(agentId, operations) {
159 if (_.isUndefined(agentId)) {
160 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to add agent context operations with no agentId provided.'));
161 }
162 if (!_.isArray(operations)) {
163 // Only one given operation
164 operations = [operations];
165 }
166 operations = _.compact(operations);
167
168 if (!operations.length) {
169 const message = `No operation to add to the agent ${cfg.owner}/${cfg.project}/${agentId} context.`;
170
171 debug(message);
172
173 return Promise.resolve({ message });
174 }
175
176 return _(operations)
177 .map(({ context, timestamp }) => ({
178 context: context,
179 timestamp: Time(timestamp).timestamp
180 }))
181 .orderBy('timestamp')
182 .chunk(cfg.operationsChunksSize)
183 .reduce((p, chunk) => p.then(
184 () => request({
185 method: 'POST',
186 path: `/agents/${agentId}/context`,
187 body: chunk
188 })
189 ),
190 Promise.resolve())
191 .then(() => {
192 const message = `Successfully added ${operations.length} operation(s) to the agent ${cfg.owner}/${cfg.project}/${agentId} context.`;
193 debug(message);
194 return { message };
195 });
196 },
197 getAgentContextOperations: function(agentId, start = undefined, end = undefined) {
198 if (_.isUndefined(agentId)) {
199 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to get agent context operations with no agentId provided.'));
200 }
201 let startTimestamp;
202 if (start) {
203 startTimestamp = Time(start).timestamp;
204 if (_.isUndefined(startTimestamp)) {
205 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to get agent context operations with an invalid \'start\' timestamp provided.'));
206 }
207 }
208 let endTimestamp;
209 if (end) {
210 endTimestamp = Time(end).timestamp;
211 if (_.isUndefined(endTimestamp)) {
212 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to get agent context operations with an invalid \'end\' timestamp provided.'));
213 }
214 }
215
216 const requestFollowingPages = ({ operations, nextPageUrl }) => {
217 if (!nextPageUrl) {
218 return Promise.resolve(operations);
219 }
220 return request({ url: nextPageUrl }, this)
221 .then(({ body, nextPageUrl }) => requestFollowingPages({
222 operations: operations.concat(body),
223 nextPageUrl
224 }));
225 };
226
227 return request({
228 method: 'GET',
229 path: `/agents/${agentId}/context`,
230 query: {
231 start: startTimestamp,
232 end: endTimestamp
233 }
234 })
235 .then(({ body, nextPageUrl }) => requestFollowingPages({
236 operations: body,
237 nextPageUrl
238 }));
239 },
240 getAgentStateHistory: function(agentId, start = undefined, end = undefined) {
241 if (_.isUndefined(agentId)) {
242 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to get agent state history with no agentId provided.'));
243 }
244 let startTimestamp;
245 if (start) {
246 startTimestamp = Time(start).timestamp;
247 if (_.isUndefined(startTimestamp)) {
248 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to get agent state history with an invalid \'start\' timestamp provided.'));
249 }
250 }
251 let endTimestamp;
252 if (end) {
253 endTimestamp = Time(end).timestamp;
254 if (_.isUndefined(endTimestamp)) {
255 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to get agent state history with an invalid \'end\' timestamp provided.'));
256 }
257 }
258
259 const requestFollowingPages = ({ stateHistory, nextPageUrl }) => {
260 if (!nextPageUrl) {
261 return Promise.resolve(stateHistory);
262 }
263 return request({ url: nextPageUrl })
264 .then(({ body, nextPageUrl }) => requestFollowingPages({
265 stateHistory: stateHistory.concat(body),
266 nextPageUrl
267 }));
268 };
269
270 return request({
271 method: 'GET',
272 path: `/agents/${agentId}/context/state/history`,
273 query: {
274 start: startTimestamp,
275 end: endTimestamp
276 }
277 })
278 .then(({ body, nextPageUrl }) => requestFollowingPages({
279 stateHistory: body,
280 nextPageUrl
281 }));
282 },
283 getAgentInspectorUrl: function(agentId, t = undefined) {
284 deprecation('client.getAgentInspectorUrl', 'client.getSharedAgentInspectorUrl');
285 return this.getSharedAgentInspectorUrl(agentId, t);
286 },
287 getSharedAgentInspectorUrl: function(agentId, t = undefined) {
288 return request({
289 method: 'GET',
290 path: `/agents/${agentId}/shared`
291 })
292 .then(({ body }) => {
293 if (_.isUndefined(t)) {
294 return body.shortUrl;
295 }
296 else {
297 let posixTimestamp = Time(t).timestamp;
298 return `${body.shortUrl}?t=${posixTimestamp}`;
299 }
300 });
301 },
302 deleteSharedAgentInspectorUrl: function(agentId) {
303 return request({
304 method: 'DELETE',
305 path: `/agents/${agentId}/shared`
306 })
307 .then(() => {
308 debug(`Delete shared inspector link for agent "${agentId}".`);
309 });
310 },
311 getAgentDecisionTree: function(agentId, t = undefined, version = DEFAULT_DECISION_TREE_VERSION) {
312 if (_.isUndefined(agentId)) {
313 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to retrieve an agent decision tree with no agentId provided.'));
314 }
315 let posixTimestamp = Time(t).timestamp;
316 if (_.isUndefined(posixTimestamp)) {
317 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to retrieve an agent decision tree with an invalid timestamp provided.'));
318 }
319
320 const agentDecisionTreeRequest = () => request({
321 method: 'GET',
322 path: `/agents/${agentId}/decision/tree`,
323 query: {
324 t: posixTimestamp
325 },
326 headers: {
327 'x-craft-ai-tree-version': version
328 }
329 })
330 .then(({ body }) => body);
331
332 if (!cfg.decisionTreeRetrievalTimeout) {
333 // Don't retry
334 return agentDecisionTreeRequest();
335 }
336 else {
337 const start = Date.now();
338 return Promise.race([
339 agentDecisionTreeRequest()
340 .catch((error) => {
341 const requestDuration = Date.now() - start;
342 const expectedRetryDuration = requestDuration + 2000; // Let's add some margin
343 const timeoutBeforeRetrying = cfg.decisionTreeRetrievalTimeout - requestDuration - expectedRetryDuration;
344 if (error instanceof CraftAiLongRequestTimeOutError && timeoutBeforeRetrying > 0) {
345 // First timeout, let's retry once near the end of the set timeout
346 return resolveAfterTimeout(timeoutBeforeRetrying)
347 .then(() => agentDecisionTreeRequest());
348 }
349 else {
350 return Promise.reject(error);
351 }
352 }),
353 resolveAfterTimeout(cfg.decisionTreeRetrievalTimeout)
354 .then(() => {
355 throw new CraftAiLongRequestTimeOutError();
356 })
357 ]);
358 }
359 },
360 computeAgentDecision: function(agentId, t, ...contexts) {
361 if (_.isUndefined(agentId)) {
362 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to compute an agent decision with no agentId provided.'));
363 }
364 let posixTimestamp = Time(t).timestamp;
365 if (_.isUndefined(posixTimestamp)) {
366 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to compute an agent decision with no or invalid timestamp provided.'));
367 }
368 if (_.isUndefined(contexts) || _.size(contexts) === 0) {
369 return Promise.reject(new CraftAiBadRequestError('Bad Request, unable to compute an agent decision with no context provided.'));
370 }
371
372 return request({
373 method: 'GET',
374 path: `/agents/${agentId}/decision/tree`,
375 query: {
376 t: posixTimestamp
377 }
378 })
379 .then(({ body }) => {
380 let decision = decide(body, ...contexts);
381 decision.timestamp = posixTimestamp;
382 return decision;
383 });
384 }
385 };
386
387 return instance;
388}