@aj-archipelago/cortex 1.0.24 → 1.0.25
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 +19 -0
- package/helper_apps/{MediaFileChunker → CortexFileHandler}/blobHandler.js +27 -1
- package/helper_apps/{MediaFileChunker → CortexFileHandler}/fileChunker.js +0 -1
- package/helper_apps/{MediaFileChunker → CortexFileHandler}/index.js +22 -2
- package/helper_apps/{MediaFileChunker → CortexFileHandler}/localFileHandler.js +38 -2
- package/helper_apps/{MediaFileChunker → CortexFileHandler}/package-lock.json +1 -24
- package/lib/keyValueStorageClient.js +2 -5
- package/lib/redisSubscription.js +49 -37
- package/lib/request.js +45 -11
- package/package.json +1 -1
- package/pathways/index.js +3 -1
- package/pathways/transcribe.js +4 -0
- package/pathways/vision.js +18 -0
- package/server/chunker.js +46 -6
- package/server/graphql.js +8 -1
- package/server/pathwayPrompter.js +4 -0
- package/server/pathwayResolver.js +10 -8
- package/server/plugins/modelPlugin.js +5 -2
- package/server/plugins/openAiChatPlugin.js +5 -3
- package/server/plugins/openAiVisionPlugin.js +35 -0
- package/server/plugins/openAiWhisperPlugin.js +26 -9
- package/server/typeDef.js +10 -6
- package/tests/main.test.js +157 -0
- package/tests/modelPlugin.test.js +1 -1
- package/helper_apps/HealthCheck/.funcignore +0 -10
- package/helper_apps/HealthCheck/host.json +0 -15
- package/helper_apps/HealthCheck/package-lock.json +0 -142
- package/helper_apps/HealthCheck/package.json +0 -14
- package/helper_apps/HealthCheck/src/functions/timerTrigger.js +0 -13
- package/helper_apps/HealthCheck/src/transcribeHealthCheck.js +0 -93
- package/helper_apps/WhisperX/.dockerignore +0 -27
- package/helper_apps/WhisperX/Dockerfile +0 -32
- package/helper_apps/WhisperX/app.py +0 -104
- package/helper_apps/WhisperX/docker-compose.debug.yml +0 -12
- package/helper_apps/WhisperX/docker-compose.yml +0 -10
- package/helper_apps/WhisperX/requirements.txt +0 -5
- /package/helper_apps/{MediaFileChunker → CortexFileHandler}/Dockerfile +0 -0
- /package/helper_apps/{MediaFileChunker → CortexFileHandler}/docHelper.js +0 -0
- /package/helper_apps/{MediaFileChunker → CortexFileHandler}/function.json +0 -0
- /package/helper_apps/{MediaFileChunker → CortexFileHandler}/helper.js +0 -0
- /package/helper_apps/{MediaFileChunker → CortexFileHandler}/package.json +0 -0
- /package/helper_apps/{MediaFileChunker → CortexFileHandler}/redis.js +0 -0
- /package/helper_apps/{MediaFileChunker → CortexFileHandler}/start.js +0 -0
package/config.js
CHANGED
|
@@ -9,6 +9,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
9
9
|
|
|
10
10
|
// Schema for config
|
|
11
11
|
var config = convict({
|
|
12
|
+
cortexId: {
|
|
13
|
+
format: String,
|
|
14
|
+
default: 'local',
|
|
15
|
+
env: 'CORTEX_ID'
|
|
16
|
+
},
|
|
12
17
|
basePathwayPath: {
|
|
13
18
|
format: String,
|
|
14
19
|
default: path.join(__dirname, 'pathways', 'basePathway.js'),
|
|
@@ -109,6 +114,20 @@ var config = convict({
|
|
|
109
114
|
},
|
|
110
115
|
"maxTokenLength": 8192,
|
|
111
116
|
},
|
|
117
|
+
"oai-gpt4-vision": {
|
|
118
|
+
"type": "OPENAI-VISION",
|
|
119
|
+
"url": "https://api.openai.com/v1/chat/completions",
|
|
120
|
+
"headers": {
|
|
121
|
+
"Authorization": "Bearer {{OPENAI_API_KEY}}",
|
|
122
|
+
"Content-Type": "application/json"
|
|
123
|
+
},
|
|
124
|
+
"params": {
|
|
125
|
+
"model": "gpt-4-vision-preview"
|
|
126
|
+
},
|
|
127
|
+
"requestsPerSecond": 1,
|
|
128
|
+
"maxTokenLength": 128000,
|
|
129
|
+
"supportsStreaming": true
|
|
130
|
+
},
|
|
112
131
|
},
|
|
113
132
|
env: 'CORTEX_MODELS'
|
|
114
133
|
},
|
|
@@ -145,6 +145,32 @@ async function uploadBlob(context, req, saveToLocal = false) {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Function to delete files that haven't been used in more than a month
|
|
149
|
+
async function cleanup() {
|
|
150
|
+
const { containerClient } = getBlobClient();
|
|
151
|
+
|
|
152
|
+
// List all the blobs in the container
|
|
153
|
+
const blobs = containerClient.listBlobsFlat();
|
|
154
|
+
|
|
155
|
+
// Calculate the date that is x month ago
|
|
156
|
+
const xMonthAgo = new Date();
|
|
157
|
+
xMonthAgo.setMonth(xMonthAgo.getMonth() - 1);
|
|
158
|
+
|
|
159
|
+
// Iterate through the blobs
|
|
160
|
+
for await (const blob of blobs) {
|
|
161
|
+
// Get the last modified date of the blob
|
|
162
|
+
const lastModified = blob.properties.lastModified;
|
|
163
|
+
|
|
164
|
+
// Compare the last modified date with one month ago
|
|
165
|
+
if (lastModified < xMonthAgo) {
|
|
166
|
+
// Delete the blob
|
|
167
|
+
const blockBlobClient = containerClient.getBlockBlobClient(blob.name);
|
|
168
|
+
await blockBlobClient.delete();
|
|
169
|
+
console.log(`Cleaned blob: ${blob.name}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
148
174
|
export {
|
|
149
|
-
saveFileToBlob, deleteBlob, uploadBlob
|
|
175
|
+
saveFileToBlob, deleteBlob, uploadBlob, cleanup
|
|
150
176
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { downloadFile, processYoutubeUrl, splitMediaFile } from './fileChunker.js';
|
|
2
|
-
import { saveFileToBlob, deleteBlob, uploadBlob } from './blobHandler.js';
|
|
2
|
+
import { saveFileToBlob, deleteBlob, uploadBlob, cleanup } from './blobHandler.js';
|
|
3
3
|
import { publishRequestProgress } from './redis.js';
|
|
4
4
|
import { deleteTempPath, ensureEncoded, isValidYoutubeUrl } from './helper.js';
|
|
5
|
-
import { moveFileToPublicFolder, deleteFolder } from './localFileHandler.js';
|
|
5
|
+
import { moveFileToPublicFolder, deleteFolder, cleanupLocal } from './localFileHandler.js';
|
|
6
6
|
import { documentToText, easyChunker } from './docHelper.js';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import os from 'os';
|
|
@@ -15,9 +15,29 @@ const useAzure = process.env.AZURE_STORAGE_CONNECTION_STRING ? true : false;
|
|
|
15
15
|
console.log(useAzure ? 'Using Azure Storage' : 'Using local file system');
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
let isCleanupRunning = false;
|
|
19
|
+
async function cleanupInactive(useAzure) {
|
|
20
|
+
try {
|
|
21
|
+
if (isCleanupRunning) { return; } //no need to cleanup every call
|
|
22
|
+
isCleanupRunning = true;
|
|
23
|
+
if (useAzure) {
|
|
24
|
+
await cleanup();
|
|
25
|
+
} else {
|
|
26
|
+
await cleanupLocal();
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.log('Error occurred during cleanup:', error);
|
|
30
|
+
} finally{
|
|
31
|
+
isCleanupRunning = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
18
36
|
async function main(context, req) {
|
|
19
37
|
context.log('Starting req processing..');
|
|
20
38
|
|
|
39
|
+
cleanupInactive(useAzure); //trigger & no need to wait for it
|
|
40
|
+
|
|
21
41
|
// Clean up blob when request delete which means processing marked completed
|
|
22
42
|
if (req.method.toLowerCase() === `delete`) {
|
|
23
43
|
const { requestId } = req.query;
|
|
@@ -4,7 +4,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
4
4
|
|
|
5
5
|
import { publicFolder, port, ipAddress } from "./start.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
async function moveFileToPublicFolder(chunkPath, requestId) {
|
|
9
8
|
// Use the filename with a UUID as the blob name
|
|
10
9
|
const filename = `${requestId}/${uuidv4()}_${basename(chunkPath)}`;
|
|
@@ -30,7 +29,44 @@ async function deleteFolder(requestId) {
|
|
|
30
29
|
console.log(`Cleaned folder: ${targetFolder}`);
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
async function cleanupLocal() {
|
|
33
|
+
try {
|
|
34
|
+
// Read the directory
|
|
35
|
+
const items = await fs.readdir(publicFolder);
|
|
36
|
+
|
|
37
|
+
// Calculate the date that is x months ago
|
|
38
|
+
const monthsAgo = new Date();
|
|
39
|
+
monthsAgo.setMonth(monthsAgo.getMonth() - 1);
|
|
40
|
+
|
|
41
|
+
// Iterate through the items
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
const itemPath = join(publicFolder, item);
|
|
44
|
+
|
|
45
|
+
// Get the stats of the item
|
|
46
|
+
const stats = await fs.stat(itemPath);
|
|
47
|
+
|
|
48
|
+
// Check if the item is a file or a directory
|
|
49
|
+
const isDirectory = stats.isDirectory();
|
|
50
|
+
|
|
51
|
+
// Compare the last modified date with three months ago
|
|
52
|
+
if (stats.mtime < monthsAgo) {
|
|
53
|
+
if (isDirectory) {
|
|
54
|
+
// If it's a directory, delete it recursively
|
|
55
|
+
await fs.rm(itemPath, { recursive: true });
|
|
56
|
+
console.log(`Cleaned directory: ${item}`);
|
|
57
|
+
} else {
|
|
58
|
+
// If it's a file, delete it
|
|
59
|
+
await fs.unlink(itemPath);
|
|
60
|
+
console.log(`Cleaned file: ${item}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(`Error cleaning up files: ${error}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
33
69
|
|
|
34
70
|
export {
|
|
35
|
-
moveFileToPublicFolder, deleteFolder
|
|
71
|
+
moveFileToPublicFolder, deleteFolder, cleanupLocal
|
|
36
72
|
};
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
"pdfjs-dist": "^3.9.179",
|
|
22
22
|
"public-ip": "^6.0.1",
|
|
23
23
|
"uuid": "^9.0.0",
|
|
24
|
-
"xlsx": "^0.18.5"
|
|
25
|
-
"ytdl-core": "git+ssh://git@github.com:khlevon/node-ytdl-core.git#v4.11.4-patch.2"
|
|
24
|
+
"xlsx": "^0.18.5"
|
|
26
25
|
}
|
|
27
26
|
},
|
|
28
27
|
"node_modules/@azure/abort-controller": {
|
|
@@ -2545,19 +2544,6 @@
|
|
|
2545
2544
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
2546
2545
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
2547
2546
|
"optional": true
|
|
2548
|
-
},
|
|
2549
|
-
"node_modules/ytdl-core": {
|
|
2550
|
-
"version": "0.0.0-development",
|
|
2551
|
-
"resolved": "git+ssh://git@github.com/khlevon/node-ytdl-core.git#87450450caabb91f81afa6e66758bf2f629664a1",
|
|
2552
|
-
"license": "MIT",
|
|
2553
|
-
"dependencies": {
|
|
2554
|
-
"m3u8stream": "^0.8.6",
|
|
2555
|
-
"miniget": "^4.2.2",
|
|
2556
|
-
"sax": "^1.1.3"
|
|
2557
|
-
},
|
|
2558
|
-
"engines": {
|
|
2559
|
-
"node": ">=12"
|
|
2560
|
-
}
|
|
2561
2547
|
}
|
|
2562
2548
|
},
|
|
2563
2549
|
"dependencies": {
|
|
@@ -4452,15 +4438,6 @@
|
|
|
4452
4438
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
4453
4439
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
4454
4440
|
"optional": true
|
|
4455
|
-
},
|
|
4456
|
-
"ytdl-core": {
|
|
4457
|
-
"version": "git+ssh://git@github.com/khlevon/node-ytdl-core.git#87450450caabb91f81afa6e66758bf2f629664a1",
|
|
4458
|
-
"from": "ytdl-core@git+ssh://git@github.com:khlevon/node-ytdl-core.git#v4.11.4-patch.2",
|
|
4459
|
-
"requires": {
|
|
4460
|
-
"m3u8stream": "^0.8.6",
|
|
4461
|
-
"miniget": "^4.2.2",
|
|
4462
|
-
"sax": "^1.1.3"
|
|
4463
|
-
}
|
|
4464
4441
|
}
|
|
4465
4442
|
}
|
|
4466
4443
|
}
|
|
@@ -2,10 +2,7 @@ import Keyv from 'keyv';
|
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
|
|
4
4
|
const storageConnectionString = config.get('storageConnectionString');
|
|
5
|
-
|
|
6
|
-
if (!config.get('storageConnectionString')) {
|
|
7
|
-
console.log('No storageConnectionString specified. Please set the storageConnectionString or STORAGE_CONNECTION_STRING environment variable if you need caching or stored context.')
|
|
8
|
-
}
|
|
5
|
+
const cortexId = config.get('cortexId');
|
|
9
6
|
|
|
10
7
|
// Create a keyv client to store data
|
|
11
8
|
const keyValueStorageClient = new Keyv(storageConnectionString, {
|
|
@@ -13,7 +10,7 @@ const keyValueStorageClient = new Keyv(storageConnectionString, {
|
|
|
13
10
|
abortConnect: false,
|
|
14
11
|
serialize: JSON.stringify,
|
|
15
12
|
deserialize: JSON.parse,
|
|
16
|
-
namespace:
|
|
13
|
+
namespace: `${cortexId}-cortex-context`
|
|
17
14
|
});
|
|
18
15
|
|
|
19
16
|
// Set values to keyv
|
package/lib/redisSubscription.js
CHANGED
|
@@ -3,48 +3,60 @@ import { config } from '../config.js';
|
|
|
3
3
|
import pubsub from '../server/pubsub.js';
|
|
4
4
|
|
|
5
5
|
const connectionString = config.get('storageConnectionString');
|
|
6
|
-
const client = new Redis(connectionString);
|
|
7
|
-
|
|
8
6
|
const channel = 'requestProgress';
|
|
7
|
+
let client;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
console.
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
client.on('connect', () => {
|
|
15
|
-
client.subscribe(channel, (error) => {
|
|
16
|
-
if (error) {
|
|
17
|
-
console.error(`Error subscribing to channel ${channel}: ${error}`);
|
|
18
|
-
} else {
|
|
19
|
-
console.log(`Subscribed to channel ${channel}`);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
client.on('message', (channel, message) => {
|
|
25
|
-
if (channel === 'requestProgress') {
|
|
26
|
-
console.log(`Received message from ${channel}: ${message}`);
|
|
27
|
-
let parsedMessage;
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
parsedMessage = JSON.parse(message);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
parsedMessage = message;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
handleMessage(parsedMessage);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const handleMessage = (data) => {
|
|
40
|
-
// Process the received data
|
|
41
|
-
console.log('Processing data:', data);
|
|
9
|
+
if (connectionString) {
|
|
10
|
+
console.log(`Using Redis subscription for channel ${channel}`);
|
|
42
11
|
try {
|
|
43
|
-
|
|
12
|
+
client = connectionString && new Redis(connectionString);
|
|
44
13
|
} catch (error) {
|
|
45
|
-
console.error(
|
|
14
|
+
console.error('Redis connection error: ', error);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (client) {
|
|
18
|
+
const channel = 'requestProgress';
|
|
19
|
+
|
|
20
|
+
client.on('error', (error) => {
|
|
21
|
+
console.error(`Redis client error: ${error}`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
client.on('connect', () => {
|
|
25
|
+
client.subscribe(channel, (error) => {
|
|
26
|
+
if (error) {
|
|
27
|
+
console.error(`Error subscribing to channel ${channel}: ${error}`);
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`Subscribed to channel ${channel}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
client.on('message', (channel, message) => {
|
|
35
|
+
if (channel === 'requestProgress') {
|
|
36
|
+
console.log(`Received message from ${channel}: ${message}`);
|
|
37
|
+
let parsedMessage;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
parsedMessage = JSON.parse(message);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
parsedMessage = message;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handleMessage(parsedMessage);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const handleMessage = (data) => {
|
|
50
|
+
// Process the received data
|
|
51
|
+
console.log('Processing data:', data);
|
|
52
|
+
try {
|
|
53
|
+
pubsub.publish('REQUEST_PROGRESS', { requestProgress: data });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`Error publishing data to pubsub: ${error}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
46
58
|
}
|
|
47
|
-
}
|
|
59
|
+
}
|
|
48
60
|
|
|
49
61
|
export {
|
|
50
62
|
client as subscriptionClient,
|
package/lib/request.js
CHANGED
|
@@ -3,37 +3,71 @@ import RequestMonitor from './requestMonitor.js';
|
|
|
3
3
|
import { config } from '../config.js';
|
|
4
4
|
import axios from 'axios';
|
|
5
5
|
import { setupCache } from 'axios-cache-interceptor';
|
|
6
|
+
import Redis from 'ioredis';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const connectionString = config.get('storageConnectionString');
|
|
8
9
|
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
if (!connectionString) {
|
|
11
|
+
console.log('No STORAGE_CONNECTION_STRING found in environment. Redis features (caching, pubsub, clustered limiters) disabled.')
|
|
12
|
+
} else {
|
|
13
|
+
console.log('Using Redis connection specified in STORAGE_CONNECTION_STRING.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let client;
|
|
17
|
+
|
|
18
|
+
if (connectionString) {
|
|
19
|
+
try {
|
|
20
|
+
client = new Redis(connectionString);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Redis connection error: ', error);
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
const cortexId = config.get('cortexId');
|
|
27
|
+
const connection = client && new Bottleneck.IORedisConnection({ client: client });
|
|
28
|
+
|
|
19
29
|
const limiters = {};
|
|
20
30
|
const monitors = {};
|
|
21
31
|
|
|
22
32
|
const buildLimiters = (config) => {
|
|
23
|
-
console.log(
|
|
33
|
+
console.log(`Building ${connection ? 'Redis clustered' : 'local'} model rate limiters for ${cortexId}...`);
|
|
24
34
|
for (const [name, model] of Object.entries(config.get('models'))) {
|
|
25
35
|
const rps = model.requestsPerSecond ?? 100;
|
|
26
|
-
|
|
36
|
+
let limiterOptions = {
|
|
27
37
|
minTime: 1000 / rps,
|
|
28
38
|
maxConcurrent: rps,
|
|
29
39
|
reservoir: rps, // Number of tokens available initially
|
|
30
40
|
reservoirRefreshAmount: rps, // Number of tokens added per interval
|
|
31
41
|
reservoirRefreshInterval: 1000, // Interval in milliseconds
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// If Redis connection exists, add id and connection to enable clustering
|
|
45
|
+
if (connection) {
|
|
46
|
+
limiterOptions.id = `${cortexId}-${name}-limiter`; // Unique id for each limiter
|
|
47
|
+
limiterOptions.connection = connection; // Shared Redis connection
|
|
48
|
+
limiterOptions.clearDatastore = true; // Clear Redis datastore on startup
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
limiters[name] = new Bottleneck(limiterOptions);
|
|
52
|
+
limiters[name].on('error', (err) => {
|
|
53
|
+
console.error(`Limiter error for ${cortexId}-${name}:`, err);
|
|
32
54
|
});
|
|
33
55
|
monitors[name] = new RequestMonitor();
|
|
34
56
|
}
|
|
35
57
|
}
|
|
36
58
|
|
|
59
|
+
let cortexAxios = axios;
|
|
60
|
+
|
|
61
|
+
if (config.get('enableCache')) {
|
|
62
|
+
// Setup cache
|
|
63
|
+
cortexAxios = setupCache(axios, {
|
|
64
|
+
// enable cache for all requests by default
|
|
65
|
+
methods: ['get', 'post', 'put', 'delete', 'patch'],
|
|
66
|
+
interpretHeader: false,
|
|
67
|
+
ttl: 1000 * 60 * 60 * 24 * 7, // 7 days
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
setInterval(() => {
|
|
38
72
|
const monitorKeys = Object.keys(monitors);
|
|
39
73
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
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/pathways/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import test_palm_chat from './test_palm_chat.js';
|
|
|
18
18
|
import transcribe from './transcribe.js';
|
|
19
19
|
import translate from './translate.js';
|
|
20
20
|
import embeddings from './embeddings.js';
|
|
21
|
+
import vision from './vision.js';
|
|
21
22
|
|
|
22
23
|
export {
|
|
23
24
|
edit,
|
|
@@ -39,5 +40,6 @@ export {
|
|
|
39
40
|
test_langchain,
|
|
40
41
|
test_palm_chat,
|
|
41
42
|
transcribe,
|
|
42
|
-
translate
|
|
43
|
+
translate,
|
|
44
|
+
vision,
|
|
43
45
|
};
|
package/pathways/transcribe.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Prompt } from '../server/prompt.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
prompt: [
|
|
5
|
+
new Prompt({ messages: [
|
|
6
|
+
"{{chatHistory}}",
|
|
7
|
+
]}),
|
|
8
|
+
],
|
|
9
|
+
inputParameters: {
|
|
10
|
+
chatHistory: [{role: '', content: []}],
|
|
11
|
+
contextId: ``,
|
|
12
|
+
},
|
|
13
|
+
max_tokens: 1024,
|
|
14
|
+
model: 'oai-gpt4-vision',
|
|
15
|
+
tokenRatio: 0.96,
|
|
16
|
+
useInputChunking: false,
|
|
17
|
+
enableDuplicateRequests: false,
|
|
18
|
+
}
|
package/server/chunker.js
CHANGED
|
@@ -11,12 +11,52 @@ const getLastNToken = (text, maxTokenLen) => {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const getFirstNToken = (text, maxTokenLen) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
if (Array.isArray(text)) {
|
|
15
|
+
return getFirstNTokenArray(text, maxTokenLen);
|
|
16
|
+
} else {
|
|
17
|
+
return getFirstNTokenSingle(text, maxTokenLen);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const getFirstNTokenSingle = (text, maxTokenLen) => {
|
|
22
|
+
const encoded = encode(text);
|
|
23
|
+
if (encoded.length > maxTokenLen) {
|
|
24
|
+
text = decode(encoded.slice(0, maxTokenLen + 1));
|
|
25
|
+
text = text.slice(0,text.search(/\s[^\s]*$/)); // skip potential partial word
|
|
26
|
+
}
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
function getFirstNTokenArray(content, tokensToKeep) {
|
|
32
|
+
let totalTokens = 0;
|
|
33
|
+
let result = [];
|
|
34
|
+
|
|
35
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
36
|
+
const message = content[i];
|
|
37
|
+
const messageTokens = encode(message).length;
|
|
38
|
+
|
|
39
|
+
if (totalTokens + messageTokens <= tokensToKeep) {
|
|
40
|
+
totalTokens += messageTokens;
|
|
41
|
+
result.unshift(message); // Add message to the start
|
|
42
|
+
} else {
|
|
43
|
+
try{
|
|
44
|
+
const messageObj = JSON.parse(message);
|
|
45
|
+
if(messageObj.type === "image_url"){
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}catch(e){
|
|
49
|
+
// ignore
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const remainingTokens = tokensToKeep - totalTokens;
|
|
53
|
+
const truncatedMessage = getFirstNToken(message, remainingTokens);
|
|
54
|
+
result.unshift(truncatedMessage); // Add truncated message to the start
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
20
60
|
}
|
|
21
61
|
|
|
22
62
|
const determineTextFormat = (text) => {
|
package/server/graphql.js
CHANGED
|
@@ -11,6 +11,7 @@ import { useServer } from 'graphql-ws/lib/use/ws';
|
|
|
11
11
|
import express from 'express';
|
|
12
12
|
import http from 'http';
|
|
13
13
|
import Keyv from 'keyv';
|
|
14
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
14
15
|
import cors from 'cors';
|
|
15
16
|
import { KeyvAdapter } from '@apollo/utils.keyvadapter';
|
|
16
17
|
import responseCachePlugin from '@apollo/server-plugin-response-cache';
|
|
@@ -40,6 +41,7 @@ const getPlugins = (config) => {
|
|
|
40
41
|
// TODO: custom cache key:
|
|
41
42
|
// https://www.apollographql.com/docs/apollo-server/performance/cache-backends#implementing-your-own-cache-backend
|
|
42
43
|
plugins.push(responseCachePlugin({ cache }));
|
|
44
|
+
console.log('Using Redis for GraphQL cache');
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
return { plugins, cache };
|
|
@@ -167,6 +169,11 @@ const build = async (config) => {
|
|
|
167
169
|
]),
|
|
168
170
|
});
|
|
169
171
|
|
|
172
|
+
// Healthcheck endpoint is valid regardless of auth
|
|
173
|
+
app.get('/healthcheck', (req, res) => {
|
|
174
|
+
res.status(200).send('OK');
|
|
175
|
+
});
|
|
176
|
+
|
|
170
177
|
// If CORTEX_API_KEY is set, we roll our own auth middleware - usually not used if you're being fronted by a proxy
|
|
171
178
|
const cortexApiKey = config.get('cortexApiKey');
|
|
172
179
|
if (cortexApiKey) {
|
|
@@ -202,7 +209,7 @@ const build = async (config) => {
|
|
|
202
209
|
next();
|
|
203
210
|
}
|
|
204
211
|
});
|
|
205
|
-
}
|
|
212
|
+
}
|
|
206
213
|
|
|
207
214
|
// Parse the body for REST endpoints
|
|
208
215
|
app.use(express.json());
|
|
@@ -14,6 +14,7 @@ import AzureCognitivePlugin from './plugins/azureCognitivePlugin.js';
|
|
|
14
14
|
import OpenAiEmbeddingsPlugin from './plugins/openAiEmbeddingsPlugin.js';
|
|
15
15
|
import OpenAIImagePlugin from './plugins/openAiImagePlugin.js';
|
|
16
16
|
import OpenAIDallE3Plugin from './plugins/openAiDallE3Plugin.js';
|
|
17
|
+
import OpenAIVisionPlugin from './plugins/openAiVisionPlugin.js';
|
|
17
18
|
|
|
18
19
|
class PathwayPrompter {
|
|
19
20
|
constructor(config, pathway, modelName, model) {
|
|
@@ -66,6 +67,9 @@ class PathwayPrompter {
|
|
|
66
67
|
case 'COHERE-SUMMARIZE':
|
|
67
68
|
plugin = new CohereSummarizePlugin(config, pathway, modelName, model);
|
|
68
69
|
break;
|
|
70
|
+
case 'OPENAI-VISION':
|
|
71
|
+
plugin = new OpenAIVisionPlugin(config, pathway, modelName, model);
|
|
72
|
+
break;
|
|
69
73
|
default:
|
|
70
74
|
throw new Error(`Unsupported model type: ${model.type}`);
|
|
71
75
|
}
|
|
@@ -95,6 +95,8 @@ class PathwayResolver {
|
|
|
95
95
|
try {
|
|
96
96
|
const incomingMessage = responseData;
|
|
97
97
|
|
|
98
|
+
let messageBuffer = '';
|
|
99
|
+
|
|
98
100
|
const processData = (data) => {
|
|
99
101
|
try {
|
|
100
102
|
//console.log(`\n\nReceived stream data for requestId ${this.requestId}`, data.toString());
|
|
@@ -108,27 +110,27 @@ class PathwayResolver {
|
|
|
108
110
|
// skip empty events
|
|
109
111
|
if (!(event.trim() === '')) {
|
|
110
112
|
//console.log(`Processing stream event for requestId ${this.requestId}`, event);
|
|
111
|
-
|
|
112
|
-
let message = event.replace(/^data: /, '');
|
|
113
|
+
messageBuffer += event.replace(/^data: /, '');
|
|
113
114
|
|
|
114
115
|
const requestProgress = {
|
|
115
116
|
requestId: this.requestId,
|
|
116
|
-
data:
|
|
117
|
+
data: messageBuffer,
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
// check for end of stream or in-stream errors
|
|
120
|
-
if (
|
|
121
|
+
if (messageBuffer.trim() === '[DONE]') {
|
|
121
122
|
requestProgress.progress = 1;
|
|
122
123
|
} else {
|
|
123
124
|
let parsedMessage;
|
|
124
125
|
try {
|
|
125
|
-
parsedMessage = JSON.parse(
|
|
126
|
+
parsedMessage = JSON.parse(messageBuffer);
|
|
127
|
+
messageBuffer = '';
|
|
126
128
|
} catch (error) {
|
|
127
|
-
|
|
129
|
+
// incomplete stream message, try to buffer more data
|
|
128
130
|
return;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
const streamError = parsedMessage
|
|
133
|
+
const streamError = parsedMessage?.error || parsedMessage?.choices?.[0]?.delta?.content?.error || parsedMessage?.choices?.[0]?.text?.error;
|
|
132
134
|
if (streamError) {
|
|
133
135
|
streamErrorOccurred = true;
|
|
134
136
|
console.error(`Stream error: ${streamError.message}`);
|
|
@@ -143,7 +145,7 @@ class PathwayResolver {
|
|
|
143
145
|
requestProgress: requestProgress
|
|
144
146
|
});
|
|
145
147
|
} catch (error) {
|
|
146
|
-
console.error('Could not publish the stream message',
|
|
148
|
+
console.error('Could not publish the stream message', messageBuffer, error);
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
151
|
}
|
|
@@ -74,13 +74,16 @@ class ModelPlugin {
|
|
|
74
74
|
const otherMessageTokens = totalTokenLength - currentTokenLength;
|
|
75
75
|
const tokensToKeep = targetTokenLength - (otherMessageTokens + emptyContentLength);
|
|
76
76
|
|
|
77
|
-
if (tokensToKeep <= 0) {
|
|
77
|
+
if (tokensToKeep <= 0 || Array.isArray(message?.content)) {
|
|
78
78
|
// If the message needs to be empty to make the target, remove it entirely
|
|
79
79
|
totalTokenLength -= currentTokenLength;
|
|
80
80
|
tokenLengths.splice(index, 1);
|
|
81
|
+
if(tokenLengths.length == 0){
|
|
82
|
+
throw new Error(`Unable to process your request as your single message content is too long. Please try again with a shorter message.`);
|
|
83
|
+
}
|
|
81
84
|
} else {
|
|
82
85
|
// Otherwise, update the message and token length
|
|
83
|
-
const truncatedContent = getFirstNToken(message
|
|
86
|
+
const truncatedContent = getFirstNToken(message?.content ?? message, tokensToKeep);
|
|
84
87
|
const truncatedMessage = { ...message, content: truncatedContent };
|
|
85
88
|
|
|
86
89
|
tokenLengths[index] = {
|