@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 +6 -0
- package/lib/crypto.js +46 -0
- package/lib/keyValueStorageClient.js +19 -2
- package/lib/logger.js +35 -16
- package/lib/redisSubscription.js +79 -60
- package/lib/request.js +1 -1
- package/package.json +1 -1
- package/server/parser.js +3 -1
- package/server/pathwayResolver.js +1 -1
- package/server/plugins/modelPlugin.js +2 -2
- package/server/plugins/openAiImagePlugin.js +1 -1
- package/server/plugins/openAiWhisperPlugin.js +1 -1
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:
|
|
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;
|
package/lib/redisSubscription.js
CHANGED
|
@@ -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
|
|
9
|
-
|
|
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) ${
|
|
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
|
-
|
|
26
|
+
publisherClient = connectionString && new Redis(connectionString);
|
|
15
27
|
} catch (error) {
|
|
16
|
-
logger.error(`Redis connection 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 (
|
|
37
|
+
if (subscriptionClient) {
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
logger.error(`Redis
|
|
39
|
+
subscriptionClient.on('error', (error) => {
|
|
40
|
+
logger.error(`Redis subscriptionClient error: ${error}`);
|
|
23
41
|
});
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
64
|
+
decryptedMessage = decrypt(message, redisEncryptionKey);
|
|
49
65
|
} catch (error) {
|
|
50
|
-
|
|
66
|
+
logger.error(`Error decrypting message: ${error}`);
|
|
51
67
|
}
|
|
68
|
+
}
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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: ${
|
|
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 ${
|
|
101
|
-
await publisherClient.publish(
|
|
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: ${
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 ||
|
|
227
|
+
const errMsg = `Transcribe error: ${error?.message || error}`;
|
|
228
228
|
logger.error(errMsg);
|
|
229
229
|
return errMsg;
|
|
230
230
|
}
|