@aj-archipelago/cortex 1.0.18 → 1.0.20

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
@@ -95,6 +95,7 @@ var config = convict({
95
95
  "api-key": "{{AZURE_COGNITIVE_API_KEY}}",
96
96
  "Content-Type": "application/json"
97
97
  },
98
+ "requestsPerSecond": 6
98
99
  },
99
100
  "oai-embeddings": {
100
101
  "type": "OPENAI-EMBEDDINGS",
@@ -148,6 +149,11 @@ var config = convict({
148
149
  sensitive: true,
149
150
  env: 'STORAGE_CONNECTION_STRING'
150
151
  },
152
+ dalleImageApiUrl: {
153
+ format: String,
154
+ default: 'null',
155
+ env: 'DALLE_IMAGE_API_URL'
156
+ },
151
157
  whisperMediaApiUrl: {
152
158
  format: String,
153
159
  default: 'null',
@@ -30,17 +30,49 @@ export async function xlsxToText(filePath) {
30
30
  return finalText;
31
31
  }
32
32
 
33
- export async function pdfToText(filePath) {
33
+ async function pdfToText(filePath) {
34
34
  const pdf = await pdfjsLib.getDocument(filePath).promise;
35
+ const meta = await pdf.getMetadata();
36
+
37
+ // Check if pdf is scanned
38
+ if (meta && meta.metadata && meta.metadata._metadataMap && meta.metadata._metadataMap.has('dc:format')) {
39
+ const format = meta.metadata._metadataMap.get('dc:format');
40
+ if (format && format._value && format._value.toLowerCase() === 'application/pdf; version=1.3') {
41
+ throw new Error('Scanned PDFs are not supported');
42
+ }
43
+ }
44
+
45
+ // Check if pdf is encrypted
46
+ if (pdf._pdfInfo && pdf._pdfInfo.encrypt) {
47
+ throw new Error('Encrypted PDFs are not supported');
48
+ }
49
+
50
+ // Check if pdf is password protected
51
+ if (pdf._passwordNeeded) {
52
+ throw new Error('Password protected PDFs are not supported');
53
+ }
54
+
35
55
  let finalText = '';
56
+ let ocrNeeded = true; // Initialize the variable as true
36
57
 
37
- for(let i = 1; i <= pdf.numPages; i++) {
58
+ for (let i = 1; i <= pdf.numPages; i++) {
38
59
  const page = await pdf.getPage(i);
60
+ const operatorList = await page.getOperatorList();
61
+
62
+ // Check if there are any fonts used in the PDF
63
+ if (operatorList.fnArray.some(fn => fn === pdfjsLib.OPS.setFont)) {
64
+ ocrNeeded = false; // Set ocrNeeded to false if fonts are found
65
+ }
66
+
39
67
  const textContent = await page.getTextContent();
40
68
  const strings = textContent.items.map(item => item.str);
41
69
  finalText += strings.join(' ') + '\n';
42
70
  }
43
71
 
72
+ if (ocrNeeded) {
73
+ throw new Error('OCR might be needed for this document!');
74
+ }
75
+
44
76
  return finalText.trim();
45
77
  }
46
78
 
@@ -150,6 +150,9 @@ async function main(context, req) {
150
150
  }
151
151
  } catch (error) {
152
152
  console.error("An error occurred:", error);
153
+ context.res.status(500);
154
+ context.res.body = error.message || error;
155
+ return;
153
156
  } finally {
154
157
  try {
155
158
  (isYoutubeUrl) && (await deleteTempPath(file));
@@ -159,13 +162,11 @@ async function main(context, req) {
159
162
  }
160
163
  }
161
164
 
162
-
163
165
  console.log(`result: ${result}`);
164
-
165
166
  context.res = {
166
- // status: 200, /* Defaults to 200 */
167
167
  body: result
168
168
  };
169
+
169
170
  }
170
171
 
171
172
 
@@ -1,4 +1,5 @@
1
1
  // pathwayTools.js
2
+ import { encode , decode } from 'gpt-3-encoder';
2
3
 
3
4
  // callPathway - call a pathway from another pathway
4
5
  const callPathway = async (config, pathwayName, args) => {
@@ -12,4 +13,12 @@ const callPathway = async (config, pathwayName, args) => {
12
13
  return data?.result;
13
14
  };
14
15
 
15
- export { callPathway };
16
+ const gpt3Encode = (text) => {
17
+ return encode(text);
18
+ }
19
+
20
+ const gpt3Decode = (text) => {
21
+ return decode(text);
22
+ }
23
+
24
+ export { callPathway, gpt3Encode, gpt3Decode };
package/lib/request.js CHANGED
@@ -169,7 +169,8 @@ const postRequest = async ({ url, data, params, headers, cache }, model, request
169
169
  try {
170
170
  const response = await Promise.race(promises);
171
171
 
172
- if (response.status === 200) {
172
+ // if response status is 2xx
173
+ if (response.status >= 200 && response.status < 300) {
173
174
  return response;
174
175
  } else {
175
176
  throw new Error(`Received error response: ${response.status}`);
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A class to get request durations and estimate their average.
3
+ */
4
+ export default class RequestDurationEstimator {
5
+ // Initializing the class with given number of durations to track.
6
+ constructor(n = 10) {
7
+ this.n = n; // Number of last durations to consider
8
+ this.durations = []; // List to keep track of last n durations
9
+ }
10
+
11
+ /**
12
+ * Private method to add a request duration to the durations list.
13
+ * If the list is full (n durations already), the oldest duration is removed.
14
+ * @param {number} duration - The duration of the request
15
+ */
16
+ #add(duration) {
17
+ this.durations.push(duration);
18
+ // Remove the oldest duration if we have stored n durations
19
+ if (this.durations.length > this.n) {
20
+ this.durations.shift();
21
+ }
22
+ }
23
+
24
+ /**
25
+ * To be invoked when a request starts.
26
+ * If there is an ongoing request, it ends that request.
27
+ * @param {string} requestId - The ID of the request
28
+ */
29
+ startRequest(requestId) {
30
+ // If there is an ongoing request, end it
31
+ if (this.requestId) {
32
+ this.endRequest();
33
+ }
34
+
35
+ // Store the starting details of the new request
36
+ this.requestId = requestId;
37
+ this.startTime = Date.now();
38
+ }
39
+
40
+ /**
41
+ * To be invoked when a request ends.
42
+ * Calculates the duration of the request and adds it to the durations list.
43
+ */
44
+ endRequest() {
45
+ // If there is an ongoing request, add its duration to the durations list
46
+ if (this.requestId) {
47
+ this.#add(Date.now() - this.startTime);
48
+ this.requestId = null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Calculate and return the average of the request durations.
54
+ * @return {number} The average request duration
55
+ */
56
+ getAverage() {
57
+ // If no duration is stored, return 0
58
+ if (!this.durations.length) {
59
+ return 0;
60
+ }
61
+
62
+ // Calculate the sum of the durations and divide by the number of durations to get the average
63
+ return this.durations.reduce((a, b) => a + b) / this.durations.length;
64
+ }
65
+
66
+ /**
67
+ * Calculate the percentage completion of the current request based on the average of past durations.
68
+ * @return {number} The estimated percent completion of the ongoing request
69
+ */
70
+ calculatePercentComplete() {
71
+ // If no duration is stored, return 0
72
+ if (!this.durations.length) {
73
+ return 0;
74
+ }
75
+
76
+
77
+ // Calculate the duration of the current request
78
+ const duration = Date.now() - this.startTime;
79
+ // Get the average of the durations
80
+ const average = this.getAverage();
81
+ // Calculate the percentage completion
82
+ let percentComplete = duration / average;
83
+
84
+ if (percentComplete > .8) {
85
+ percentComplete = 0.8;
86
+ }
87
+
88
+ return percentComplete;
89
+ }
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
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
  "repository": {
6
6
  "type": "git",
@@ -26,5 +26,7 @@ export default {
26
26
  // args: the input arguments to the pathway
27
27
  // runAllPrompts: a function that runs all prompts in the pathway and returns the result
28
28
  executePathway: undefined,
29
+ // Set the temperature to 0 to favor more deterministic output when generating entity extraction.
30
+ temperature: undefined,
29
31
  };
30
32
 
@@ -1,5 +1,5 @@
1
1
  export default {
2
- prompt: `{{text}}`,
2
+ prompt: `{{{text}}}`,
3
3
  model: 'azure-cognitive',
4
4
  inputParameters: {
5
5
  inputVector: ``,
@@ -0,0 +1,4 @@
1
+ export default {
2
+ prompt:["{{text}}"],
3
+ model: 'azure-dalle',
4
+ }
package/server/graphql.js CHANGED
@@ -72,6 +72,7 @@ const getTypedefs = (pathways) => {
72
72
  type RequestSubscription {
73
73
  requestId: String
74
74
  progress: Float
75
+ status: String
75
76
  data: String
76
77
  }
77
78
 
@@ -12,16 +12,20 @@ import CohereGeneratePlugin from './plugins/cohereGeneratePlugin.js';
12
12
  import CohereSummarizePlugin from './plugins/cohereSummarizePlugin.js';
13
13
  import AzureCognitivePlugin from './plugins/azureCognitivePlugin.js';
14
14
  import OpenAiEmbeddingsPlugin from './plugins/openAiEmbeddingsPlugin.js';
15
+ import OpenAIImagePlugin from './plugins/openAiImagePlugin.js';
15
16
 
16
17
  class PathwayPrompter {
17
18
  constructor(config, pathway, modelName, model) {
18
-
19
+
19
20
  let plugin;
20
21
 
21
22
  switch (model.type) {
22
23
  case 'OPENAI-CHAT':
23
24
  plugin = new OpenAIChatPlugin(config, pathway, modelName, model);
24
25
  break;
26
+ case 'OPENAI-IMAGE':
27
+ plugin = new OpenAIImagePlugin(config, pathway, modelName, model);
28
+ break;
25
29
  case 'OPENAI-CHAT-EXTENSION':
26
30
  plugin = new OpenAIChatExtensionPlugin(config, pathway, modelName, model);
27
31
  break;
@@ -10,6 +10,8 @@ import { getv, setv } from '../lib/keyValueStorageClient.js';
10
10
  import { requestState } from './requestState.js';
11
11
  import { callPathway } from '../lib/pathwayTools.js';
12
12
 
13
+ const modelTypesExcludedFromProgressUpdates = ['OPENAI-IMAGE'];
14
+
13
15
  class PathwayResolver {
14
16
  constructor({ config, pathway, args }) {
15
17
  this.config = config;
@@ -78,13 +80,17 @@ class PathwayResolver {
78
80
  if (args.async || typeof responseData === 'string') {
79
81
  const { completedCount, totalCount } = requestState[this.requestId];
80
82
  requestState[this.requestId].data = responseData;
81
- pubsub.publish('REQUEST_PROGRESS', {
82
- requestProgress: {
83
- requestId: this.requestId,
84
- progress: completedCount / totalCount,
85
- data: JSON.stringify(responseData),
86
- }
87
- });
83
+
84
+ // if model type is OPENAI-IMAGE
85
+ if (!modelTypesExcludedFromProgressUpdates.includes(this.model.type)) {
86
+ pubsub.publish('REQUEST_PROGRESS', {
87
+ requestProgress: {
88
+ requestId: this.requestId,
89
+ progress: completedCount / totalCount,
90
+ data: JSON.stringify(responseData),
91
+ }
92
+ });
93
+ }
88
94
  } else {
89
95
  try {
90
96
  const incomingMessage = responseData;
@@ -204,7 +210,7 @@ class PathwayResolver {
204
210
  async promptAndParse(args) {
205
211
  // Get saved context from contextId or change contextId if needed
206
212
  const { contextId } = args;
207
- this.savedContextId = contextId ? contextId : null;
213
+ this.savedContextId = contextId ? contextId : uuidv4();
208
214
  this.savedContext = contextId ? (getv && (await getv(contextId)) || {}) : {};
209
215
 
210
216
  // Save the context before processing the request
@@ -290,7 +296,7 @@ class PathwayResolver {
290
296
  text = await this.summarizeIfEnabled({ text, ...parameters }); // summarize if flag enabled
291
297
  const chunks = this.processInputText(text);
292
298
 
293
- const anticipatedRequestCount = chunks.length * this.prompts.length;
299
+ let anticipatedRequestCount = chunks.length * this.prompts.length
294
300
 
295
301
  if ((requestState[this.requestId] || {}).canceled) {
296
302
  throw new Error('Request canceled');
@@ -155,11 +155,12 @@ class AzureCognitivePlugin extends ModelPlugin {
155
155
  const extension = path.extname(file).toLowerCase();
156
156
  if (!DIRECT_FILE_EXTENSIONS.includes(extension)) {
157
157
  try {
158
- const {data} = await axios.get(API_URL, { params: { uri: file, requestId, save: true } });
159
- url = data[0]
158
+ const { data } = await axios.get(API_URL, { params: { uri: file, requestId, save: true } });
159
+ url = data[0];
160
160
  } catch (error) {
161
- console.log(`Error converting file ${file} to txt:`, error);
162
- throw error;
161
+ console.error(`Error converting file ${file} to txt:`, error);
162
+ await this.markCompletedForCleanUp(requestId);
163
+ throw Error(error?.response?.data || error?.message || error);
163
164
  }
164
165
  }
165
166
 
@@ -197,6 +197,13 @@ class ModelPlugin {
197
197
  }
198
198
  });
199
199
 
200
+ // Clean up any null messages if they exist
201
+ expandedMessages.forEach((message) => {
202
+ if (typeof message === 'object' && message.content === null) {
203
+ message.content = '';
204
+ }
205
+ });
206
+
200
207
  return expandedMessages;
201
208
  }
202
209
 
@@ -57,7 +57,7 @@ class OpenAIChatPlugin extends ModelPlugin {
57
57
  if (isPalmFormat) {
58
58
  const context = modelPrompt.context || '';
59
59
  const examples = modelPrompt.examples || [];
60
- requestMessages = this.convertPalmToOpenAIMessages(context, examples, expandedMessages);
60
+ requestMessages = this.convertPalmToOpenAIMessages(context, examples, modelPromptMessages);
61
61
  }
62
62
 
63
63
  // Check if the token length exceeds the model's max token length
@@ -0,0 +1,85 @@
1
+ // OpenAIImagePlugin.js
2
+ import FormData from 'form-data';
3
+ import { config } from '../../config.js';
4
+ import ModelPlugin from './modelPlugin.js';
5
+ import pubsub from '../pubsub.js';
6
+ import axios from 'axios';
7
+ import RequestDurationEstimator from '../../lib/requestDurationEstimator.js';
8
+
9
+ const API_URL = config.get('dalleImageApiUrl'); // URL for the DALL-E API
10
+ const requestDurationEstimator = new RequestDurationEstimator(10);
11
+
12
+ class OpenAIImagePlugin extends ModelPlugin {
13
+ constructor(config, pathway, modelName, model) {
14
+ super(config, pathway, modelName, model);
15
+ }
16
+
17
+ // Implement the method to call the DALL-E API
18
+ async execute(text, parameters, _, pathwayResolver) {
19
+ const url = this.requestUrl(text);
20
+ const data = JSON.stringify({ prompt: text });
21
+
22
+ let id;
23
+ const { requestId, pathway } = pathwayResolver;
24
+
25
+ try {
26
+ requestDurationEstimator.startRequest(requestId);
27
+ id = (await this.executeRequest(url, data, {}, { ...this.model.headers }, {}, requestId, pathway))?.id;
28
+ } catch (error) {
29
+ const errMsg = `Error generating image: ${error?.message || JSON.stringify(error)}`;
30
+ console.error(errMsg);
31
+ return errMsg;
32
+ }
33
+
34
+ if (!parameters.async) {
35
+ return await this.getStatus(text, id, requestId);
36
+ }
37
+ else {
38
+ this.getStatus(text, id, requestId);
39
+ }
40
+ }
41
+
42
+ async getStatus(text, id, requestId) {
43
+ // get the post URL which is used to send the request
44
+ const url = this.requestUrl(text);
45
+
46
+ // conver it to the GET URL which is used to check the status
47
+ const statusUrl = url.replace("images/generations:submit", `operations/images/${id}`);
48
+ let status;
49
+ let attemptCount = 0;
50
+ let data = null;
51
+
52
+ do {
53
+ const response = (await axios.get(statusUrl, { cache: false, headers: { ...this.model.headers } })).data;
54
+ status = response.status;
55
+ let progress =
56
+ requestDurationEstimator.calculatePercentComplete();
57
+
58
+ if (status === "succeeded") {
59
+ progress = 1;
60
+ data = JSON.stringify(response);
61
+ }
62
+
63
+ pubsub.publish('REQUEST_PROGRESS', {
64
+ requestProgress: {
65
+ requestId,
66
+ status,
67
+ progress,
68
+ data,
69
+ }
70
+ });
71
+
72
+ if (status === "succeeded") {
73
+ requestDurationEstimator.endRequest();
74
+ break;
75
+ }
76
+ // sleep for 5 seconds
77
+ await new Promise(resolve => setTimeout(resolve, 2000));
78
+ }
79
+ while (status !== "succeeded" && attemptCount++ < 30);
80
+
81
+ return data;
82
+ }
83
+ }
84
+
85
+ export default OpenAIImagePlugin;
@@ -21,7 +21,7 @@ const pipeline = promisify(stream.pipeline);
21
21
  const API_URL = config.get('whisperMediaApiUrl');
22
22
  const WHISPER_TS_API_URL = config.get('whisperTSApiUrl');
23
23
 
24
- function alignSubtitles(subtitles) {
24
+ function alignSubtitles(subtitles, format) {
25
25
  const result = [];
26
26
  const offset = 1000 * 60 * 10; // 10 minutes for each chunk
27
27
 
@@ -39,7 +39,7 @@ function alignSubtitles(subtitles) {
39
39
  const subtitle = subtitles[i];
40
40
  result.push(...shiftSubtitles(subtitle, i * offset));
41
41
  }
42
- return subsrt.build(result);
42
+ return subsrt.build(result, { format: format === 'vtt' ? 'vtt' : 'srt' });
43
43
  }
44
44
 
45
45
  function generateUniqueFilename(extension) {
@@ -234,7 +234,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
234
234
  }
235
235
 
236
236
  if (['srt','vtt'].includes(responseFormat) || wordTimestamped) { // align subtitles for formats
237
- return alignSubtitles(result);
237
+ return alignSubtitles(result, responseFormat);
238
238
  }
239
239
  return result.join(` `);
240
240
  }
@@ -0,0 +1,59 @@
1
+ import test from 'ava';
2
+ import RequestDurationEstimator from '../lib/requestDurationEstimator.js';
3
+
4
+ test('add and get average request duration', async (t) => {
5
+ const estimator = new RequestDurationEstimator(5);
6
+
7
+ estimator.startRequest('req1');
8
+ await new Promise(resolve => setTimeout(() => {
9
+ estimator.endRequest();
10
+
11
+ const average = estimator.calculatePercentComplete();
12
+
13
+ // An average should be calculated after the first completed request
14
+ t.not(average, 0);
15
+ resolve();
16
+ }, 1000));
17
+ });
18
+
19
+ test('add more requests than size of durations array', (t) => {
20
+ const estimator = new RequestDurationEstimator(5);
21
+
22
+ for (let i = 0; i < 10; i++) {
23
+ estimator.startRequest(`req${i}`);
24
+ estimator.endRequest();
25
+ }
26
+
27
+ // Array size should not exceed maximum length (5 in this case)
28
+ t.is(estimator.durations.length, 5);
29
+ });
30
+
31
+ test('calculate percent complete of current request based on average of past durations', async (t) => {
32
+ const estimator = new RequestDurationEstimator(5);
33
+
34
+ for (let i = 0; i < 4; i++) {
35
+ estimator.startRequest(`req${i}`);
36
+ // wait 1 second
37
+ await new Promise(resolve => setTimeout(resolve, 1000));
38
+ estimator.endRequest();
39
+ }
40
+
41
+ estimator.startRequest('req5');
42
+
43
+ await new Promise(resolve => setTimeout(() => {
44
+ const percentComplete = estimator.calculatePercentComplete();
45
+
46
+ // Depending on how fast the operations are,
47
+ // the percentage may not be exactly 50%, but
48
+ // we'll affirm it should be at least partially complete.
49
+ t.true(percentComplete > 0);
50
+ resolve();
51
+ }, 500));
52
+ });
53
+
54
+ test('calculate percent complete based on average of past durations', async (t) => {
55
+ const estimator = new RequestDurationEstimator(5);
56
+ estimator.durations = [1000, 2000, 3000];
57
+ const average = estimator.getAverage();
58
+ t.is(average, 2000);
59
+ });