@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.
Files changed (43) hide show
  1. package/config.js +19 -0
  2. package/helper_apps/{MediaFileChunker → CortexFileHandler}/blobHandler.js +27 -1
  3. package/helper_apps/{MediaFileChunker → CortexFileHandler}/fileChunker.js +0 -1
  4. package/helper_apps/{MediaFileChunker → CortexFileHandler}/index.js +22 -2
  5. package/helper_apps/{MediaFileChunker → CortexFileHandler}/localFileHandler.js +38 -2
  6. package/helper_apps/{MediaFileChunker → CortexFileHandler}/package-lock.json +1 -24
  7. package/lib/keyValueStorageClient.js +2 -5
  8. package/lib/redisSubscription.js +49 -37
  9. package/lib/request.js +45 -11
  10. package/package.json +1 -1
  11. package/pathways/index.js +3 -1
  12. package/pathways/transcribe.js +4 -0
  13. package/pathways/vision.js +18 -0
  14. package/server/chunker.js +46 -6
  15. package/server/graphql.js +8 -1
  16. package/server/pathwayPrompter.js +4 -0
  17. package/server/pathwayResolver.js +10 -8
  18. package/server/plugins/modelPlugin.js +5 -2
  19. package/server/plugins/openAiChatPlugin.js +5 -3
  20. package/server/plugins/openAiVisionPlugin.js +35 -0
  21. package/server/plugins/openAiWhisperPlugin.js +26 -9
  22. package/server/typeDef.js +10 -6
  23. package/tests/main.test.js +157 -0
  24. package/tests/modelPlugin.test.js +1 -1
  25. package/helper_apps/HealthCheck/.funcignore +0 -10
  26. package/helper_apps/HealthCheck/host.json +0 -15
  27. package/helper_apps/HealthCheck/package-lock.json +0 -142
  28. package/helper_apps/HealthCheck/package.json +0 -14
  29. package/helper_apps/HealthCheck/src/functions/timerTrigger.js +0 -13
  30. package/helper_apps/HealthCheck/src/transcribeHealthCheck.js +0 -93
  31. package/helper_apps/WhisperX/.dockerignore +0 -27
  32. package/helper_apps/WhisperX/Dockerfile +0 -32
  33. package/helper_apps/WhisperX/app.py +0 -104
  34. package/helper_apps/WhisperX/docker-compose.debug.yml +0 -12
  35. package/helper_apps/WhisperX/docker-compose.yml +0 -10
  36. package/helper_apps/WhisperX/requirements.txt +0 -5
  37. /package/helper_apps/{MediaFileChunker → CortexFileHandler}/Dockerfile +0 -0
  38. /package/helper_apps/{MediaFileChunker → CortexFileHandler}/docHelper.js +0 -0
  39. /package/helper_apps/{MediaFileChunker → CortexFileHandler}/function.json +0 -0
  40. /package/helper_apps/{MediaFileChunker → CortexFileHandler}/helper.js +0 -0
  41. /package/helper_apps/{MediaFileChunker → CortexFileHandler}/package.json +0 -0
  42. /package/helper_apps/{MediaFileChunker → CortexFileHandler}/redis.js +0 -0
  43. /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
  }
@@ -3,7 +3,6 @@ import path from 'path';
3
3
  import ffmpeg from 'fluent-ffmpeg';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
  import os from 'os';
6
- import ytdl from 'ytdl-core';
7
6
  import { promisify } from 'util';
8
7
  import axios from 'axios';
9
8
  import { ensureEncoded } from './helper.js';
@@ -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: 'cortex-context'
13
+ namespace: `${cortexId}-cortex-context`
17
14
  });
18
15
 
19
16
  // Set values to keyv
@@ -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
- client.on('error', (error) => {
11
- console.error(`Redis client error: ${error}`);
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
- pubsub.publish('REQUEST_PROGRESS', { requestProgress: data });
12
+ client = connectionString && new Redis(connectionString);
44
13
  } catch (error) {
45
- console.error(`Error publishing data to pubsub: ${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
- let cortexAxios = axios;
8
+ const connectionString = config.get('storageConnectionString');
8
9
 
9
- if (config.get('enableCache')) {
10
- // Setup cache
11
- cortexAxios = setupCache(axios, {
12
- // enable cache for all requests by default
13
- methods: ['get', 'post', 'put', 'delete', 'patch'],
14
- interpretHeader: false,
15
- ttl: 1000 * 60 * 60 * 24 * 7, // 7 days
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('Building limiters...');
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
- limiters[name] = new Bottleneck({
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.24",
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
  };
@@ -6,6 +6,10 @@ export default {
6
6
  language: ``,
7
7
  responseFormat: `text`,
8
8
  wordTimestamped: false,
9
+ highlightWords: false,
10
+ maxLineWidth: 0,
11
+ maxLineCount: 0,
12
+ maxWordsPerLine: 0,
9
13
  },
10
14
  timeout: 3600, // in seconds
11
15
  enableDuplicateRequests: false,
@@ -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
- const encoded = encode(text);
15
- if (encoded.length > maxTokenLen) {
16
- text = decode(encoded.slice(0, maxTokenLen + 1));
17
- text = text.slice(0,text.search(/\s[^\s]*$/)); // skip potential partial word
18
- }
19
- return text;
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: message,
117
+ data: messageBuffer,
117
118
  }
118
119
 
119
120
  // check for end of stream or in-stream errors
120
- if (message.trim() === '[DONE]') {
121
+ if (messageBuffer.trim() === '[DONE]') {
121
122
  requestProgress.progress = 1;
122
123
  } else {
123
124
  let parsedMessage;
124
125
  try {
125
- parsedMessage = JSON.parse(message);
126
+ parsedMessage = JSON.parse(messageBuffer);
127
+ messageBuffer = '';
126
128
  } catch (error) {
127
- console.error('Could not JSON parse stream message', message, error);
129
+ // incomplete stream message, try to buffer more data
128
130
  return;
129
131
  }
130
132
 
131
- const streamError = parsedMessage.error || parsedMessage?.choices?.[0]?.delta?.content?.error || parsedMessage?.choices?.[0]?.text?.error;
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', message, error);
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.content, tokensToKeep);
86
+ const truncatedContent = getFirstNToken(message?.content ?? message, tokensToKeep);
84
87
  const truncatedMessage = { ...message, content: truncatedContent };
85
88
 
86
89
  tokenLengths[index] = {