backend-manager 4.0.4 → 4.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "4.0.4",
3
+ "version": "4.0.6",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -51,8 +51,8 @@
51
51
  "chalk": "^4.1.2",
52
52
  "cors": "^2.8.5",
53
53
  "dotenv": "^16.4.5",
54
- "express": "^4.21.0",
55
- "firebase-admin": "^12.5.0",
54
+ "express": "^4.21.1",
55
+ "firebase-admin": "^12.6.0",
56
56
  "firebase-functions": "^6.0.1",
57
57
  "fs-jetpack": "^5.1.0",
58
58
  "glob": "^11.0.0",
@@ -13,7 +13,10 @@ Module.prototype.main = function () {
13
13
 
14
14
  return new Promise(async function(resolve, reject) {
15
15
  // Load libraries
16
- _ = Manager.require('lodash')
16
+ _ = Manager.require('lodash');
17
+
18
+ // Set defaults
19
+ payload.data.payload.update = payload.data.payload.update || false;
17
20
 
18
21
  // Perform checks
19
22
  if (!payload.user.roles.admin) {
@@ -29,7 +32,7 @@ Module.prototype.main = function () {
29
32
 
30
33
  // Only update if requested
31
34
  if (payload.data.payload.update) {
32
- await self.updateStats(data)
35
+ await self.updateStats(data, payload.data.payload.update)
33
36
  .catch(e => data = e)
34
37
  }
35
38
 
@@ -52,7 +55,7 @@ Module.prototype.main = function () {
52
55
 
53
56
  return resolve({data: data})
54
57
  })
55
- .catch(function (e) {
58
+ .catch((e) => {
56
59
  return reject(assistant.errorify(`Failed to get: ${e}`, {code: 500}));
57
60
  })
58
61
  });
@@ -72,7 +75,7 @@ Module.prototype.fixStats = function (data) {
72
75
  }
73
76
 
74
77
  // TODO: ADD https://firebase.google.com/docs/firestore/query-data/aggregation-queries#pricing
75
- Module.prototype.updateStats = function (existingData) {
78
+ Module.prototype.updateStats = function (existingData, update) {
76
79
  const self = this;
77
80
 
78
81
  return new Promise(async function(resolve, reject) {
@@ -82,43 +85,57 @@ Module.prototype.updateStats = function (existingData) {
82
85
  const sessionsOnline = self.libraries.admin.database().ref(`sessions/online`);
83
86
 
84
87
  let error = null;
85
- let update = {
86
- app: _.get(self.Manager.config, 'app.id', null),
88
+ let newData = {
89
+ app: self.Manager.config?.app?.id || null,
87
90
  };
88
91
 
89
- // Fix broken stats
90
- if (!_.get(existingData, 'users.total', null)) {
92
+ // Fix user stats
93
+ if (
94
+ !existingData?.users?.total
95
+ || update === true
96
+ || update?.users
97
+ ) {
91
98
  await self.getAllUsers()
92
99
  .then(r => {
93
- _.set(update, 'users.total', r.length)
100
+ _.set(newData, 'users.total', r.length)
94
101
  })
95
102
  .catch(e => {
96
103
  error = new Error(`Failed fixing stats: ${e}`);
97
104
  })
98
105
  }
99
106
 
107
+ // Reject if error
100
108
  if (error) {
101
109
  return reject(error);
102
110
  }
103
111
 
104
112
  // Fetch new notification stats
105
- await self.getAllNotifications()
106
- .then(r => {
107
- _.set(update, 'notifications.total', r)
108
- })
109
- .catch(e => {
110
- error = new Error(`Failed getting notifications: ${e}`);
111
- })
113
+ if (
114
+ update === true || update?.notifications
115
+ ) {
116
+ await self.getAllNotifications()
117
+ .then(r => {
118
+ _.set(newData, 'notifications.total', r)
119
+ })
120
+ .catch(e => {
121
+ error = new Error(`Failed getting notifications: ${e}`);
122
+ })
123
+ }
112
124
 
113
125
  // Fetch new subscription stats
114
- await self.getAllSubscriptions()
115
- .then(r => {
116
- _.set(update, 'subscriptions', r)
117
- })
118
- .catch(e => {
119
- error = new Error(`Failed getting subscriptions: ${e}`);
120
- })
126
+ if (
127
+ update === true || update?.subscriptions
128
+ ) {
129
+ await self.getAllSubscriptions()
130
+ .then(r => {
131
+ _.set(newData, 'subscriptions', r)
132
+ })
133
+ .catch(e => {
134
+ error = new Error(`Failed getting subscriptions: ${e}`);
135
+ })
136
+ }
121
137
 
138
+ // Reject if error
122
139
  if (error) {
123
140
  return reject(error);
124
141
  }
@@ -129,37 +146,46 @@ Module.prototype.updateStats = function (existingData) {
129
146
  .then((snap) => {
130
147
  const data = snap.val() || {};
131
148
  const keys = Object.keys(data);
132
- const existing = _.get(update, 'users.online', 0)
133
- _.set(update, 'users.online', existing + keys.length)
149
+ const existing = newData?.users?.online || 0;
150
+
151
+ // Set new value
152
+ _.set(newData, 'users.online', existing + keys.length)
134
153
  })
135
154
  .catch(e => {
136
155
  error = new Error(`Failed getting online users: ${e}`);
137
156
  })
138
157
  }
139
158
 
140
- // Count users online (in old gathering)
141
- await _countUsersOnline(gatheringOnline);
159
+ // Fetch new user stats
160
+ if (
161
+ update === true || update?.online
162
+ ) {
163
+ // Count users online (in old gathering)
164
+ await _countUsersOnline(gatheringOnline);
142
165
 
143
- // Count users online (in new session)
144
- await _countUsersOnline(sessionsApp);
166
+ // Count users online (in new session)
167
+ await _countUsersOnline(sessionsApp);
145
168
 
146
- // Count users online (in new session)
147
- await _countUsersOnline(sessionsOnline);
169
+ // Count users online (in new session)
170
+ await _countUsersOnline(sessionsOnline);
171
+ }
148
172
 
173
+ // Reject if error
149
174
  if (error) {
150
175
  return reject(error);
151
176
  }
152
177
 
153
178
  // Set metadata
154
- update.metadata = self.Manager.Metadata().set({tag: 'admin:get-stats'})
179
+ newData.metadata = self.Manager.Metadata().set({tag: 'admin:get-stats'})
155
180
 
156
- // Update stats
181
+ // newData stats
157
182
  await stats
158
- .set(update, { merge: true })
159
- .catch(function (e) {
183
+ .set(newData, { merge: true })
184
+ .catch((e) => {
160
185
  return reject(new Error(`Failed getting stats: ${e}`));
161
186
  })
162
187
 
188
+ // Return
163
189
  return resolve();
164
190
  });
165
191
  }
@@ -208,10 +234,10 @@ Module.prototype.getAllSubscriptions = function () {
208
234
  snapshot
209
235
  .forEach((doc, i) => {
210
236
  const data = doc.data();
211
- const planId = _.get(data, 'plan.id', 'basic');
212
- const frequency = _.get(data, 'plan.payment.frequency', 'unknown');
213
- const isAdmin = _.get(data, 'roles.admin', false);
214
- const isVip = _.get(data, 'roles.vip', false);
237
+ const planId = data?.plan?.id || 'basic';
238
+ const frequency = data?.plan?.payment?.frequency || 'unknown';
239
+ const isAdmin = data?.roles?.admin || false;
240
+ const isVip = data?.roles?.vip || false;
215
241
 
216
242
  if (!stats.plans[planId]) {
217
243
  stats.plans[planId] = {
@@ -20,7 +20,7 @@ Module.prototype.main = function () {
20
20
  return reject(assistant.errorify(`Admin required.`, {code: 401}));
21
21
  }
22
22
 
23
- const productId = _.get(payload, 'data.payload.payload.details.productIdGlobal');
23
+ const productId = payload?.data?.payload?.payload?.details?.productIdGlobal;
24
24
  if (!productId) {
25
25
  return reject(assistant.errorify(`No productId`, {code: 400}));
26
26
  }
@@ -224,36 +224,46 @@ OpenAI.prototype.request = function (options) {
224
224
  const assistant = self.assistant;
225
225
 
226
226
  return new Promise(async function(resolve, reject) {
227
+ // Deep merge options
227
228
  options = _.merge({}, options);
228
229
 
230
+ // Set defaults
229
231
  options.model = typeof options.model === 'undefined' ? DEFAULT_MODEL : options.model;
232
+ options.response = typeof options.response === 'undefined' ? undefined : options.response;
230
233
  options.timeout = typeof options.timeout === 'undefined' ? 120000 : options.timeout;
231
234
  options.moderate = typeof options.moderate === 'undefined' ? true : options.moderate;
232
235
  options.log = typeof options.log === 'undefined' ? false : options.log;
233
236
  options.user = options.user || assistant.getUser();
234
237
 
238
+ // Format retries
235
239
  options.retries = typeof options.retries === 'undefined' ? 0 : options.retries;
236
240
  options.retryTriggers = typeof options.retryTriggers === 'undefined' ? ['network', 'parse'] : options.retryTriggers;
237
241
 
242
+ // Format other options
243
+ options.temperature = typeof options.temperature === 'undefined' ? 0.7 : options.temperature;
244
+ options.maxTokens = typeof options.maxTokens === 'undefined' ? 512 : options.maxTokens;
245
+
246
+ // Custom options
247
+ options.dedupeConsecutiveRoles = typeof options.dedupeConsecutiveRoles === 'undefined' ? true : options.dedupeConsecutiveRoles;
248
+
249
+ // Format prompt
238
250
  options.prompt = options.prompt || {};
239
251
  options.prompt.path = options.prompt.path || '';
240
- options.prompt.text = options.prompt.text || options.prompt.content || '';
252
+ options.prompt.content = options.prompt.content || options.prompt.content || '';
241
253
  options.prompt.settings = options.prompt.settings || {};
242
254
 
255
+ // Format message
243
256
  options.message = options.message || {};
244
257
  options.message.path = options.message.path || '';
245
- options.message.text = options.message.text || options.message.content || '';
258
+ options.message.content = options.message.content || options.message.content || '';
246
259
  options.message.settings = options.message.settings || {};
247
260
  options.message.images = options.message.images || [];
248
261
 
262
+ // Format history
249
263
  options.history = options.history || {};
250
264
  options.history.messages = options.history.messages || [];
251
265
  options.history.limit = typeof options.history.limit === 'undefined' ? 5 : options.history.limit;
252
266
 
253
- options.response = typeof options.response === 'undefined' ? undefined : options.response;
254
- options.temperature = typeof options.temperature === 'undefined' ? 0.7 : options.temperature;
255
- options.maxTokens = typeof options.maxTokens === 'undefined' ? 512 : options.maxTokens;
256
-
257
267
  let attempt = 0;
258
268
 
259
269
  function _log() {
@@ -269,9 +279,9 @@ OpenAI.prototype.request = function (options) {
269
279
  // console.log('*** input.content', input.content.slice(0, 50));
270
280
  // console.log('*** input.path', input.path);
271
281
 
272
- let text = '';
282
+ let content = '';
273
283
 
274
- // Load text
284
+ // Load content
275
285
  if (input.path) {
276
286
  const exists = jetpack.exists(input.path);
277
287
 
@@ -284,15 +294,15 @@ OpenAI.prototype.request = function (options) {
284
294
  }
285
295
 
286
296
  try {
287
- text = jetpack.read(input.path);
297
+ content = jetpack.read(input.path);
288
298
  } catch (e) {
289
299
  return new Error(`Error reading file ${input.path}: ${e}`);
290
300
  }
291
301
  } else {
292
- text = input.text;
302
+ content = input.content;
293
303
  }
294
304
 
295
- return powertools.template(text, input.settings).trim();
305
+ return powertools.template(content, input.settings).trim();
296
306
  }
297
307
 
298
308
  // Log
@@ -345,51 +355,63 @@ OpenAI.prototype.request = function (options) {
345
355
  body: {},
346
356
  }
347
357
 
358
+ // Format depending on mode
348
359
  if (mode === 'chatgpt') {
349
- request.url = 'https://api.openai.com/v1/chat/completions';
350
-
351
- // Get history
360
+ // Get history with respect to the message limit
352
361
  const history = options.history.messages.slice(-options.history.limit);
353
362
 
354
- // Add prompt to history
363
+ // Add prompt to beginning of history
355
364
  history.unshift({
356
365
  role: 'system',
357
- text: prompt,
366
+ content: prompt,
358
367
  images: [],
359
368
  });
360
369
 
361
- // Set last history item
370
+ // Get last history item
362
371
  const lastHistory = history[history.length - 1];
363
372
 
364
- // If message is different than last message in history, add it
365
- if (lastHistory?.text !== message) {
366
- history.push({
367
- role: 'user',
368
- text: message,
369
- images: options.message.images,
370
- });
373
+ // Remove last message from history
374
+ if (
375
+ options.dedupeConsecutiveRoles
376
+ && lastHistory?.role === 'user'
377
+ ) {
378
+ history.pop();
371
379
  }
372
380
 
381
+ // Add message to history
382
+ history.push({
383
+ role: 'user',
384
+ content: message,
385
+ images: options.message.images,
386
+ });
387
+
373
388
  // Format history
374
389
  history.map((m) => {
375
- m.role = m.role || 'system';
390
+ const originalContent = m.content;
391
+ const originalImages = m.images;
376
392
 
393
+ // Set properties
394
+ m.role = m.role || 'system';
377
395
  m.content = [];
396
+ m.images = [];
378
397
 
379
- // Set content
380
- if (m.text) {
398
+ // Format content
399
+ if (originalContent) {
381
400
  m.content.push({
382
401
  type: 'text',
383
- text: m.text,
402
+ text: originalContent,
384
403
  })
385
404
  }
386
405
 
387
- // Set images
388
- m.images = m.images || [];
406
+ // Format images
407
+ if (originalImages) {
408
+ originalImages.forEach((i) => {
409
+ // Skip if no URL
410
+ if (!i.url) {
411
+ return
412
+ }
389
413
 
390
- // Loop through and add
391
- m.images.forEach((i) => {
392
- if (i.url) {
414
+ // Add image
393
415
  m.content.push({
394
416
  type: 'image_url',
395
417
  image_url: {
@@ -397,12 +419,15 @@ OpenAI.prototype.request = function (options) {
397
419
  detail: i.detail || 'low',
398
420
  }
399
421
  });
400
- }
401
- }),
422
+ });
423
+ }
402
424
 
403
- // Delete text and images
404
- delete m.text;
405
- delete m.images;
425
+ // Delete any field except for role, content, images
426
+ Object.keys(m).forEach((key) => {
427
+ if (!['role', 'content', 'images'].includes(key)) {
428
+ delete m[key];
429
+ }
430
+ });
406
431
  })
407
432
 
408
433
  // Log message
@@ -410,16 +435,20 @@ OpenAI.prototype.request = function (options) {
410
435
  _log('Message', m.role, m.content);
411
436
  });
412
437
 
438
+ // Set request
439
+ request.url = 'https://api.openai.com/v1/chat/completions';
413
440
  request.body = {
414
441
  model: options.model,
415
442
  response_format: responseFormat,
416
443
  messages: history,
417
444
  temperature: options.temperature,
418
- max_tokens: options.maxTokens,
445
+ // max_tokens: options.maxTokens,
446
+ max_completion_tokens: options.maxTokens,
419
447
  user: user,
420
448
  }
421
449
  resultPath = 'choices[0].message.content';
422
450
  } else if (mode === 'moderation') {
451
+ // Set request
423
452
  request.url = 'https://api.openai.com/v1/moderations';
424
453
  request.body = {
425
454
  input: message,
@@ -431,8 +460,40 @@ OpenAI.prototype.request = function (options) {
431
460
  // Request
432
461
  await fetch(request.url, request)
433
462
  .then(async (r) => {
463
+ // Log
464
+ // _log('Response RAW', JSON.stringify(r));
465
+ // {
466
+ // "id": "chatcmpl-AGKe03mwx644T6db3QRoXFz0aFuil",
467
+ // "object": "chat.completion",
468
+ // "created": 1728455968,
469
+ // "model": "gpt-4o-mini-2024-07-18",
470
+ // "choices": [{
471
+ // "index": 0,
472
+ // "message": {
473
+ // "role": "assistant",
474
+ // "content": "{\n \"message\": \"We offer several pricing plans:\\n\\n1. **Basic Plan**: Free\\n - Chatsy branding on chat\\n - 1 chatbot\\n - 5 knowledge base FAQs per chatbot\\n - English only\\n\\n2. **Premium Plan**: $19/month\\n - Chatsy branding removed\\n - 1 chatbot\\n - 10 knowledge base FAQs per chatbot\\n - English only\\n\\n3. **Pro Plan**: $29/month\\n - Chatsy branding removed\\n - 3 chatbots\\n - 10 knowledge base FAQs per chatbot\\n - Automatically chats in the language of your customers\\n\\n4. **Pro Plan**: $49/month\\n - Chatsy branding removed\\n - 10 chatbots\\n - 10 knowledge base FAQs per chatbot\\n - Automatically chats in the language of your customers\\n\\nLet me know if you need more details or assistance with anything else!\",\n \"user\": {\n \"name\": \"\"\n },\n \"scores\": {\n \"questionRelevancy\": 1\n }\n}",
475
+ // "refusal": null
476
+ // },
477
+ // "logprobs": null,
478
+ // "finish_reason": "stop"
479
+ // }],
480
+ // "usage": {
481
+ // "prompt_tokens": 1306,
482
+ // "completion_tokens": 231,
483
+ // "total_tokens": 1537,
484
+ // "prompt_tokens_details": {
485
+ // "cached_tokens": 1024
486
+ // },
487
+ // "completion_tokens_details": {
488
+ // "reasoning_tokens": 0
489
+ // }
490
+ // },
491
+ // "system_fingerprint": "fp_e2bde53e6e"
492
+ // }
493
+
434
494
  // Set token counts
435
- self.tokens.input.count += r?.usage?.prompt_tokens || 0;
495
+ self.tokens.input.count += (r?.usage?.prompt_tokens || 0)
496
+ - (r?.usage?.prompt_tokens_details?.cached_tokens || 0);
436
497
  self.tokens.output.count += r?.usage?.completion_tokens || 0;
437
498
  self.tokens.total.count = self.tokens.input.count + self.tokens.output.count;
438
499