@aj-archipelago/cortex 1.1.0-beta.0 → 1.1.0

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/config.js CHANGED
@@ -174,6 +174,12 @@ var config = convict({
174
174
  sensitive: true,
175
175
  env: 'STORAGE_CONNECTION_STRING'
176
176
  },
177
+ redisEncryptionKey: {
178
+ format: String,
179
+ default: null,
180
+ env: 'REDIS_ENCRYPTION_KEY',
181
+ sensitive: true
182
+ },
177
183
  dalleImageApiUrl: {
178
184
  format: String,
179
185
  default: 'null',
package/lib/crypto.js ADDED
@@ -0,0 +1,46 @@
1
+ // This file is used to encrypt and decrypt data using the crypto library
2
+ import logger from './logger.js';
3
+ import crypto from 'crypto';
4
+
5
+ // Encryption function
6
+ function encrypt(text, key) {
7
+ if (!key) { return text; }
8
+ try {
9
+ key = tryBufferKey(key);
10
+ let iv = crypto.randomBytes(16);
11
+ let cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
12
+ let encrypted = cipher.update(text, 'utf8', 'hex');
13
+ encrypted += cipher.final('hex');
14
+ return iv.toString('hex') + ':' + encrypted;
15
+ } catch (error) {
16
+ logger.error(`Encryption failed: ${error.message}`);
17
+ return null;
18
+ }
19
+ }
20
+
21
+ // Decryption function
22
+ function decrypt(message, key) {
23
+ if (!key) { return message; }
24
+ try {
25
+ key = tryBufferKey(key);
26
+ let parts = message.split(':');
27
+ let iv = Buffer.from(parts.shift(), 'hex');
28
+ let encrypted = parts.join(':');
29
+ let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
30
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
31
+ decrypted += decipher.final('utf8');
32
+ return decrypted;
33
+ } catch (error) {
34
+ logger.error(`Decryption failed: ${error.message}`);
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function tryBufferKey(key) {
40
+ if (key.length === 64) {
41
+ return Buffer.from(key, 'hex');
42
+ }
43
+ return key;
44
+ }
45
+
46
+ export { encrypt, decrypt };
@@ -1,15 +1,32 @@
1
1
  import Keyv from 'keyv';
2
2
  import { config } from '../config.js';
3
+ import { encrypt, decrypt } from './crypto.js';
4
+ import logger from './logger.js';
3
5
 
4
6
  const storageConnectionString = config.get('storageConnectionString');
5
7
  const cortexId = config.get('cortexId');
8
+ const redisEncryptionKey = config.get('redisEncryptionKey');
6
9
 
7
10
  // Create a keyv client to store data
8
11
  const keyValueStorageClient = new Keyv(storageConnectionString, {
9
12
  ssl: true,
10
13
  abortConnect: false,
11
- serialize: JSON.stringify,
12
- deserialize: JSON.parse,
14
+ serialize: (data) => redisEncryptionKey ? encrypt(JSON.stringify(data), redisEncryptionKey) : JSON.stringify(data),
15
+ deserialize: (data) => {
16
+ try {
17
+ // Try to parse the data normally
18
+ return JSON.parse(data);
19
+ } catch (error) {
20
+ // If it fails, the data may be encrypted so attempt to decrypt it if we have a key
21
+ try {
22
+ return JSON.parse(decrypt(data, redisEncryptionKey));
23
+ } catch (decryptError) {
24
+ // If decryption also fails, log an error and return an empty object
25
+ logger.error(`Failed to parse or decrypt stored key value data: ${decryptError}`);
26
+ return {};
27
+ }
28
+ }
29
+ },
13
30
  namespace: `${cortexId}-cortex-context`
14
31
  });
15
32
 
package/lib/logger.js CHANGED
@@ -1,22 +1,6 @@
1
1
  // logger.js
2
2
  import winston from 'winston';
3
3
 
4
- const format = winston.format.combine(
5
- //winston.format.timestamp(),
6
- winston.format.colorize({ all: true }),
7
- winston.format.simple()
8
- );
9
-
10
- const transports = [
11
- new winston.transports.Console({ format })
12
- ];
13
-
14
- const logger = winston.createLogger({
15
- level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
16
- format: format,
17
- transports: transports
18
- });
19
-
20
4
  winston.addColors({
21
5
  debug: 'green',
22
6
  verbose: 'blue',
@@ -26,4 +10,39 @@ winston.addColors({
26
10
  error: 'red'
27
11
  });
28
12
 
13
+ const debugFormat = winston.format.combine(
14
+ winston.format.colorize({ all: true }),
15
+ winston.format.cli()
16
+ );
17
+
18
+ const prodFormat = winston.format.combine(
19
+ winston.format.simple()
20
+ );
21
+
22
+ const transports = process.env.NODE_ENV === 'production' ?
23
+ new winston.transports.Console({ level: 'info', format: prodFormat }) :
24
+ new winston.transports.Console({ level: 'debug', format: debugFormat });
25
+
26
+ const logger = winston.createLogger({ transports });
27
+
28
+ // Function to obscure sensitive URL parameters
29
+ export const obscureUrlParams = url => {
30
+ try {
31
+ const urlObject = new URL(url);
32
+ urlObject.searchParams.forEach((value, name) => {
33
+ if (/token|key|password|secret|auth|apikey|access|passwd|credential/i.test(name)) {
34
+ urlObject.searchParams.set(name, '******');
35
+ }
36
+ });
37
+ return urlObject.toString();
38
+ } catch (e) {
39
+ if (e instanceof TypeError) {
40
+ logger.error('Error obscuring URL parameters - invalid URL.');
41
+ return url;
42
+ } else {
43
+ throw e;
44
+ }
45
+ }
46
+ };
47
+
29
48
  export default logger;
@@ -3,90 +3,110 @@ import { config } from '../config.js';
3
3
  import pubsub from '../server/pubsub.js';
4
4
  import { requestState } from '../server/requestState.js';
5
5
  import logger from '../lib/logger.js';
6
+ import { encrypt, decrypt } from '../lib/crypto.js';
6
7
 
7
8
  const connectionString = config.get('storageConnectionString');
8
- const channels = ['requestProgress', 'requestProgressSubscriptions'];
9
- let client;
9
+ const redisEncryptionKey = config.get('redisEncryptionKey');
10
+ const requestProgressChannel = 'requestProgress';
11
+ const requestProgressSubscriptionsChannel = 'requestProgressSubscriptions';
12
+
13
+ let subscriptionClient;
14
+ let publisherClient;
10
15
 
11
16
  if (connectionString) {
12
- logger.info(`Using Redis subscription for channel(s) ${channels.join(', ')}`);
17
+ logger.info(`Using Redis subscription for channel(s) ${requestProgressChannel}, ${requestProgressSubscriptionsChannel}`);
18
+ try {
19
+ subscriptionClient = connectionString && new Redis(connectionString);
20
+ } catch (error) {
21
+ logger.error(`Redis connection error: ${error}`);
22
+ }
23
+
24
+ logger.info(`Using Redis publish for channel(s) ${requestProgressChannel}, ${requestProgressSubscriptionsChannel}`);
13
25
  try {
14
- client = connectionString && new Redis(connectionString);
26
+ publisherClient = connectionString && new Redis(connectionString);
15
27
  } catch (error) {
16
- logger.error(`Redis connection error: ${JSON.stringify(error)}`);
28
+ logger.error(`Redis connection error: ${error}`);
29
+ }
30
+
31
+ if (redisEncryptionKey) {
32
+ logger.info('Using encryption for Redis');
33
+ } else {
34
+ logger.warn('REDIS_ENCRYPTION_KEY not set. Data stored in Redis will not be encrypted.');
17
35
  }
18
36
 
19
- if (client) {
37
+ if (subscriptionClient) {
20
38
 
21
- client.on('error', (error) => {
22
- logger.error(`Redis client error: ${JSON.stringify(error)}`);
39
+ subscriptionClient.on('error', (error) => {
40
+ logger.error(`Redis subscriptionClient error: ${error}`);
23
41
  });
24
42
 
25
- client.on('connect', () => {
26
- client.subscribe('requestProgress', (error) => {
27
- if (error) {
28
- logger.error(`Error subscribing to redis channel requestProgress: ${JSON.stringify(error)}`);
29
- } else {
30
- logger.info(`Subscribed to channel requestProgress`);
31
- }
32
- });
33
- client.subscribe('requestProgressSubscriptions', (error) => {
34
- if (error) {
35
- logger.error(`Error subscribing to redis channel requestProgressSubscriptions: ${JSON.stringify(error)}`);
36
- } else {
37
- logger.info(`Subscribed to channel requestProgressSubscriptions`);
38
- }
43
+ subscriptionClient.on('connect', () => {
44
+ const channels = [requestProgressChannel, requestProgressSubscriptionsChannel];
45
+
46
+ channels.forEach(channel => {
47
+ subscriptionClient.subscribe(channel, (error) => {
48
+ if (error) {
49
+ logger.error(`Error subscribing to redis channel ${channel}: ${error}`);
50
+ } else {
51
+ logger.info(`Subscribed to channel ${channel}`);
52
+ }
53
+ });
39
54
  });
40
55
  });
41
56
 
42
- client.on('message', (channel, message) => {
43
- if (channel === 'requestProgress') {
44
- logger.debug(`Received message from ${channel}: ${message}`);
45
- let parsedMessage;
57
+ subscriptionClient.on('message', (channel, message) => {
58
+ logger.debug(`Received message from ${channel}: ${message}`);
59
+
60
+ let decryptedMessage = message;
46
61
 
62
+ if (channel === requestProgressChannel && redisEncryptionKey) {
47
63
  try {
48
- parsedMessage = JSON.parse(message);
64
+ decryptedMessage = decrypt(message, redisEncryptionKey);
49
65
  } catch (error) {
50
- parsedMessage = message;
66
+ logger.error(`Error decrypting message: ${error}`);
51
67
  }
68
+ }
52
69
 
53
- pubsubHandleMessage(parsedMessage);
54
- } else {
55
- if (channel === 'requestProgressSubscriptions') {
56
- logger.debug(`Received message from ${channel}: ${message}`);
57
- let parsedMessage;
58
-
59
- try {
60
- parsedMessage = JSON.parse(message);
61
- } catch (error) {
62
- parsedMessage = message;
63
- }
70
+ let parsedMessage = decryptedMessage;
71
+ try {
72
+ parsedMessage = JSON.parse(decryptedMessage);
73
+ } catch (error) {
74
+ logger.error(`Error parsing message: ${error}`);
75
+ }
64
76
 
77
+ switch(channel) {
78
+ case requestProgressChannel:
79
+ pubsubHandleMessage(parsedMessage);
80
+ break;
81
+ case requestProgressSubscriptionsChannel:
65
82
  handleSubscription(parsedMessage);
66
- }
83
+ break;
84
+ default:
85
+ logger.error(`Unsupported channel: ${channel}`);
86
+ break;
67
87
  }
68
88
  });
69
- }
70
- }
71
-
72
-
73
- let publisherClient;
74
-
75
- if (connectionString) {
76
- logger.info(`Using Redis publish for channel(s) ${channels.join(', ')}`);
77
- publisherClient = Redis.createClient(connectionString);
89
+ }
78
90
  } else {
79
- logger.info(`Using pubsub publish for channel ${channels[0]}`);
91
+ // No Redis connection, use pubsub for communication
92
+ logger.info(`Using pubsub publish for channel ${requestProgressChannel}`);
80
93
  }
81
94
 
82
95
  async function publishRequestProgress(data) {
83
96
  if (publisherClient) {
84
97
  try {
85
- const message = JSON.stringify(data);
86
- logger.debug(`Publishing message ${message} to channel ${channels[0]}`);
87
- await publisherClient.publish(channels[0], message);
98
+ let message = JSON.stringify(data);
99
+ if (redisEncryptionKey) {
100
+ try {
101
+ message = encrypt(message, redisEncryptionKey);
102
+ } catch (error) {
103
+ logger.error(`Error encrypting message: ${error}`);
104
+ }
105
+ }
106
+ logger.debug(`Publishing message ${message} to channel ${requestProgressChannel}`);
107
+ await publisherClient.publish(requestProgressChannel, message);
88
108
  } catch (error) {
89
- logger.error(`Error publishing message: ${JSON.stringify(error)}`);
109
+ logger.error(`Error publishing message: ${error}`);
90
110
  }
91
111
  } else {
92
112
  pubsubHandleMessage(data);
@@ -97,10 +117,10 @@ async function publishRequestProgressSubscription(data) {
97
117
  if (publisherClient) {
98
118
  try {
99
119
  const message = JSON.stringify(data);
100
- logger.debug(`Publishing message ${message} to channel ${channels[1]}`);
101
- await publisherClient.publish(channels[1], message);
120
+ logger.debug(`Publishing message ${message} to channel ${requestProgressSubscriptionsChannel}`);
121
+ await publisherClient.publish(requestProgressSubscriptionsChannel, message);
102
122
  } catch (error) {
103
- logger.error(`Error publishing message: ${JSON.stringify(error)}`);
123
+ logger.error(`Error publishing message: ${error}`);
104
124
  }
105
125
  } else {
106
126
  handleSubscription(data);
@@ -113,7 +133,7 @@ function pubsubHandleMessage(data){
113
133
  try {
114
134
  pubsub.publish('REQUEST_PROGRESS', { requestProgress: data });
115
135
  } catch (error) {
116
- logger.error(`Error publishing data to pubsub: ${JSON.stringify(error)}`);
136
+ logger.error(`Error publishing data to pubsub: ${error}`);
117
137
  }
118
138
  }
119
139
 
@@ -129,7 +149,6 @@ function handleSubscription(data){
129
149
  }
130
150
  }
131
151
 
132
-
133
152
  export {
134
- client as subscriptionClient, publishRequestProgress, publishRequestProgressSubscription
153
+ subscriptionClient, publishRequestProgress, publishRequestProgressSubscription
135
154
  };
package/lib/request.js CHANGED
@@ -241,7 +241,7 @@ const request = async (params, model, requestId, pathway) => {
241
241
  const response = await postRequest(params, model, requestId, pathway);
242
242
  const { error, data, cached } = response;
243
243
  if (cached) {
244
- console.info(`<<< [${requestId}] served with cached response.`);
244
+ logger.info(`<<< [${requestId}] served with cached response.`);
245
245
  }
246
246
  if (error && error.length > 0) {
247
247
  const lastError = error[error.length - 1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.1.0-beta.0",
3
+ "version": "1.1.0",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
package/server/parser.js CHANGED
@@ -1,3 +1,5 @@
1
+ import logger from '../lib/logger.js';
2
+
1
3
  //simply trim and parse with given regex
2
4
  const regexParser = (text, regex) => {
3
5
  return text.trim().split(regex).map(s => s.trim()).filter(s => s.length);
@@ -25,7 +27,7 @@ const parseNumberedObjectList = (text, format) => {
25
27
  }
26
28
  result.push(obj);
27
29
  } catch (e) {
28
- console.warn(`Failed to parse value in parseNumberedObjectList, value: ${value}, fields: ${fields}`);
30
+ logger.warn(`Failed to parse value in parseNumberedObjectList, value: ${value}, fields: ${fields}`);
29
31
  }
30
32
  }
31
33
 
@@ -229,7 +229,7 @@ class PathwayResolver {
229
229
  // Add a warning and log it
230
230
  logWarning(warning) {
231
231
  this.warnings.push(warning);
232
- console.warn(warning);
232
+ logger.warn(warning);
233
233
  }
234
234
 
235
235
  // Here we choose how to handle long input - either summarize or chunk
@@ -4,7 +4,7 @@ import HandleBars from '../../lib/handleBars.js';
4
4
  import { request } from '../../lib/request.js';
5
5
  import { encode } from 'gpt-3-encoder';
6
6
  import { getFirstNToken } from '../chunker.js';
7
- import logger from '../../lib/logger.js';
7
+ import logger, { obscureUrlParams } from '../../lib/logger.js';
8
8
 
9
9
  const DEFAULT_MAX_TOKENS = 4096;
10
10
  const DEFAULT_MAX_RETURN_TOKENS = 256;
@@ -227,7 +227,7 @@ class ModelPlugin {
227
227
  const header = '>'.repeat(logMessage.length);
228
228
  logger.info(`${header}`);
229
229
  logger.info(`${logMessage}`);
230
- logger.info(`>>> Making API request to ${url}`);
230
+ logger.info(`>>> Making API request to ${obscureUrlParams(url)}`);
231
231
  }
232
232
 
233
233
  logAIRequestFinished() {
@@ -24,7 +24,7 @@ class OpenAIImagePlugin extends ModelPlugin {
24
24
  requestDurationEstimator.startRequest(requestId);
25
25
  id = (await this.executeRequest(url, data, {}, { ...this.model.headers }, {}, requestId, pathway))?.id;
26
26
  } catch (error) {
27
- const errMsg = `Error generating image: ${error?.message || JSON.stringify(error)}`;
27
+ const errMsg = `Error generating image: ${error?.message || error}`;
28
28
  logger.error(errMsg);
29
29
  return errMsg;
30
30
  }
@@ -224,7 +224,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
224
224
  // result = await Promise.all(mediaSplit.chunks.map(processChunk));
225
225
 
226
226
  } catch (error) {
227
- const errMsg = `Transcribe error: ${error?.message || JSON.stringify(error)}`;
227
+ const errMsg = `Transcribe error: ${error?.message || error}`;
228
228
  logger.error(errMsg);
229
229
  return errMsg;
230
230
  }