@aj-archipelago/cortex 1.1.20 → 1.1.21

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.
@@ -0,0 +1,252 @@
1
+ import ModelPlugin from "./modelPlugin.js";
2
+ import fs from "fs";
3
+ import FormData from "form-data";
4
+ import logger from "../../lib/logger.js";
5
+ import {
6
+ alignSubtitles,
7
+ deleteTempPath,
8
+ downloadFile,
9
+ getMediaChunks,
10
+ } from "../../lib/util.js";
11
+ import CortexRequest from "../../lib/cortexRequest.js";
12
+ import { publishRequestProgress } from "../../lib/redisSubscription.js";
13
+
14
+ const OFFSET_CHUNK = 500; //seconds of each chunk offset, only used if helper does not provide
15
+
16
+ function convertToSrt(timestamps) {
17
+ let srt = "";
18
+ for (let i = 0; i < timestamps.length; i++) {
19
+ const _start = timestamps[i].start ?? timestamps[i].startTime;
20
+ const _end = timestamps[i].end ?? timestamps[i].endTime;
21
+ const _text = timestamps[i].word ?? timestamps[i].text;
22
+ const start = new Date(_start * 1000)
23
+ .toISOString()
24
+ .slice(11, -1)
25
+ .replace(".", ",");
26
+ const end = new Date(_end * 1000)
27
+ .toISOString()
28
+ .slice(11, -1)
29
+ .replace(".", ",");
30
+ srt += `${i + 1}\n${start} --> ${end}\n${_text}\n\n`;
31
+ }
32
+ return srt;
33
+ }
34
+
35
+ function convertToVtt(timestamps) {
36
+ let vtt = "WEBVTT\n\n";
37
+ for (let i = 0; i < timestamps.length; i++) {
38
+ const _start = timestamps[i].start ?? timestamps[i].startTime;
39
+ const _end = timestamps[i].end ?? timestamps[i].endTime;
40
+ const _text = timestamps[i].word ?? timestamps[i].text;
41
+ const start = new Date(_start * 1000)
42
+ .toISOString()
43
+ .slice(11, -1)
44
+ .replace(".", ",");
45
+ const end = new Date(_end * 1000)
46
+ .toISOString()
47
+ .slice(11, -1)
48
+ .replace(".", ",");
49
+ vtt += `${start} --> ${end}\n${_text}\n\n`;
50
+ }
51
+ return vtt;
52
+ }
53
+
54
+ class NeuralSpacePlugin extends ModelPlugin {
55
+ constructor(pathway, model) {
56
+ super(pathway, model);
57
+ this.pathwayResolver = null;
58
+ }
59
+
60
+ async execute(text, parameters, prompt, cortexRequest) {
61
+ const { responseFormat, file, language, wordTimestamped, maxLineWidth } =
62
+ parameters;
63
+
64
+ let chunks = [];
65
+ let offsets = [];
66
+
67
+ try {
68
+ const { pathwayResolver } = cortexRequest;
69
+
70
+ const { requestId } = pathwayResolver;
71
+
72
+ const mediaChunks = await getMediaChunks(file, requestId);
73
+
74
+ if (!mediaChunks || !mediaChunks.length) {
75
+ throw new Error(
76
+ `Error in getting chunks from media helper for file ${file}`
77
+ );
78
+ }
79
+
80
+ const uris = mediaChunks.map((chunk) => chunk?.uri || chunk);
81
+ offsets = mediaChunks.map(
82
+ (chunk, index) => chunk?.offset || index * OFFSET_CHUNK
83
+ );
84
+
85
+ let totalCount = uris.length * 2; // [download, request] jobs per chunk
86
+ let completedCount = 0;
87
+
88
+ const sendProgress = () => {
89
+ completedCount++;
90
+ if (completedCount >= totalCount) return;
91
+
92
+ const progress = completedCount / totalCount;
93
+ logger.info(`Progress for ${requestId}: ${progress}`);
94
+
95
+ publishRequestProgress({
96
+ requestId,
97
+ progress,
98
+ data: null,
99
+ });
100
+ };
101
+
102
+ for (let i = 0; i < uris.length; i++) {
103
+ const uri = uris[i];
104
+ try {
105
+ const chunk = await downloadFile(uri);
106
+ chunks.push(chunk);
107
+ sendProgress();
108
+ } catch (err) {
109
+ logger.error(`Error downloading chunk: ${err}`);
110
+ throw err;
111
+ }
112
+ }
113
+
114
+ const jobs = [];
115
+
116
+ for (const chunk of chunks) {
117
+ const cortexRequest = new CortexRequest({ pathwayResolver });
118
+ cortexRequest.url = this.requestUrl();
119
+
120
+ const formData = new FormData();
121
+ formData.append("files", fs.createReadStream(chunk));
122
+ const configObj = {
123
+ file_transcription: {
124
+ mode: "advanced",
125
+ },
126
+ };
127
+
128
+ //phrase/segment level
129
+ if ((responseFormat && !wordTimestamped) || maxLineWidth) {
130
+ configObj.speaker_diarization = {
131
+ // mode: "speakers",
132
+ // num_speakers: numSpeakers,
133
+ // overrides: {
134
+ // clustering: {
135
+ // threshold: clusteringThreshold,
136
+ // },
137
+ // },
138
+ };
139
+
140
+ configObj.subtitles_guidelines = {
141
+ line_count: 1
142
+ };
143
+ }
144
+
145
+ if (maxLineWidth) {
146
+ configObj.subtitles_guidelines = {
147
+ character_count: maxLineWidth,
148
+ };
149
+ }
150
+
151
+ if (language) {
152
+ configObj.file_transcription.language_id = language;
153
+ }
154
+ formData.append("config", JSON.stringify(configObj));
155
+
156
+ cortexRequest.data = formData;
157
+ cortexRequest.params = {};
158
+ cortexRequest.headers = {
159
+ ...cortexRequest.headers,
160
+ ...formData.getHeaders(),
161
+ };
162
+
163
+ const result = await this.executeRequest(cortexRequest);
164
+
165
+ const jobId = result?.data?.jobId;
166
+ if (!jobId) {
167
+ logger.error(`Error in creating job: ${JSON.stringify(result)}`);
168
+ return;
169
+ }
170
+ logger.info(`Job created successfully with ID: ${jobId}`);
171
+ jobs.push(jobId);
172
+ }
173
+
174
+ return await this.checkJobStatus(
175
+ jobs,
176
+ pathwayResolver,
177
+ sendProgress,
178
+ responseFormat,
179
+ offsets
180
+ );
181
+ } catch (error) {
182
+ logger.error(`Error occurred while executing: ${error}`);
183
+ throw error;
184
+ } finally {
185
+ for (const chunk of chunks) {
186
+ try {
187
+ await deleteTempPath(chunk);
188
+ } catch (error) {
189
+ // Ignore error
190
+ logger.error(`Error deleting temp file: ${error}`);
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ async checkJobStatus(
197
+ jobs,
198
+ pathwayResolver,
199
+ sendProgress,
200
+ responseFormat,
201
+ offsets
202
+ ) {
203
+ const textResults = [];
204
+ const timestampResults = [];
205
+ for (let i = 0; i < jobs.length; i++) {
206
+ const jobId = jobs[i];
207
+ const result = await this.getJobStatus(jobId, pathwayResolver);
208
+ const text = result.data.result.transcription.channels[0].transcript;
209
+ textResults.push(text);
210
+ timestampResults.push(
211
+ result.data.result.transcription?.segments?.length > 0 ?
212
+ result.data.result.transcription.segments :
213
+ result.data.result.transcription.channels[0].timestamps
214
+ );
215
+ sendProgress();
216
+ }
217
+
218
+ if (responseFormat) {
219
+ const output = timestampResults.map((t) =>
220
+ responseFormat === "srt" ? convertToSrt(t) : convertToVtt(t)
221
+ );
222
+ return alignSubtitles(output, responseFormat, offsets);
223
+ }
224
+
225
+ return textResults.join(" ").trim();
226
+ }
227
+
228
+ async getJobStatus(jobId, pathwayResolver) {
229
+ const cortexRequest = new CortexRequest({ pathwayResolver });
230
+ cortexRequest.url = `${this.requestUrl()}/${jobId}`;
231
+ cortexRequest.method = "GET";
232
+ const result = await this.executeRequest(cortexRequest);
233
+
234
+ const status = result?.data?.status;
235
+ if (!status) {
236
+ throw new Error(`Error in getting job status: ${result}`);
237
+ }
238
+
239
+ if (status === "Completed") {
240
+ return result;
241
+ }
242
+
243
+ if (status === "Failed") {
244
+ throw new Error(`Job failed with error: ${result.data.error}`);
245
+ } else {
246
+ await new Promise((resolve) => setTimeout(resolve, 5000));
247
+ return this.getJobStatus(jobId, pathwayResolver);
248
+ }
249
+ }
250
+ }
251
+
252
+ export default NeuralSpacePlugin;
@@ -4,17 +4,20 @@ import OpenAIChatPlugin from './openAiChatPlugin.js';
4
4
  class OpenAIVisionPlugin extends OpenAIChatPlugin {
5
5
 
6
6
  tryParseMessages(messages) {
7
- //check if elements of messages strings are JSON, if valid JSON parse them to obj
8
7
  messages.map(message => {
9
8
  try {
10
- // message.content can be array or string
11
9
  if (typeof message.content === 'string') {
12
10
  message.content = JSON.parse(message.content);
13
- } else if (Array.isArray(message.content)) {
11
+ }
12
+ if (Array.isArray(message.content)) {
14
13
  message.content = message.content.map(item => {
15
- const parsedItem = JSON.parse(item);
16
- const { type, text, image_url, url } = parsedItem;
17
- return { type, text, image_url: url || image_url};
14
+ if (typeof item === 'string') {
15
+ return { type: 'text', text: item };
16
+ } else {
17
+ const parsedItem = JSON.parse(item);
18
+ const { type, text, image_url, url } = parsedItem;
19
+ return { type, text, image_url: url || image_url };
20
+ }
18
21
  });
19
22
  }
20
23
  } catch (e) {
@@ -29,8 +32,16 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
29
32
 
30
33
  this.tryParseMessages(requestParameters.messages);
31
34
 
32
- if(this.promptParameters.max_tokens) {
33
- requestParameters.max_tokens = this.promptParameters.max_tokens;
35
+ const modelMaxReturnTokens = this.getModelMaxReturnTokens();
36
+ const maxTokensPrompt = this.promptParameters.max_tokens;
37
+ const maxTokensModel = this.getModelMaxTokenLength() * (1 - this.getPromptTokenRatio());
38
+
39
+ const maxTokens = maxTokensPrompt || maxTokensModel;
40
+
41
+ requestParameters.max_tokens = maxTokens ? Math.min(maxTokens, modelMaxReturnTokens) : modelMaxReturnTokens;
42
+
43
+ if (this.promptParameters.json) {
44
+ //requestParameters.response_format = { type: "json_object", }
34
45
  }
35
46
 
36
47
  return requestParameters;
@@ -1,24 +1,13 @@
1
1
  // openAiWhisperPlugin.js
2
2
  import ModelPlugin from './modelPlugin.js';
3
3
  import { config } from '../../config.js';
4
- import subsrt from 'subsrt';
5
4
  import FormData from 'form-data';
6
5
  import fs from 'fs';
7
- import { axios } from '../../lib/requestExecutor.js';
8
- import stream from 'stream';
9
- import os from 'os';
10
- import path from 'path';
11
- import http from 'http';
12
- import https from 'https';
13
- import { URL } from 'url';
14
- import { v4 as uuidv4 } from 'uuid';
15
- import { promisify } from 'util';
16
6
  import { publishRequestProgress } from '../../lib/redisSubscription.js';
17
7
  import logger from '../../lib/logger.js';
18
8
  import CortexRequest from '../../lib/cortexRequest.js';
19
- const pipeline = promisify(stream.pipeline);
9
+ import { downloadFile, deleteTempPath, convertSrtToText, alignSubtitles, getMediaChunks, markCompletedForCleanUp } from '../../lib/util.js';
20
10
 
21
- const API_URL = config.get('whisperMediaApiUrl');
22
11
  const WHISPER_TS_API_URL = config.get('whisperTSApiUrl');
23
12
  if(WHISPER_TS_API_URL){
24
13
  logger.info(`WHISPER API URL using ${WHISPER_TS_API_URL}`);
@@ -28,147 +17,11 @@ if(WHISPER_TS_API_URL){
28
17
 
29
18
  const OFFSET_CHUNK = 500; //seconds of each chunk offset, only used if helper does not provide
30
19
 
31
- async function deleteTempPath(path) {
32
- try {
33
- if (!path) {
34
- logger.warn('Temporary path is not defined.');
35
- return;
36
- }
37
- if (!fs.existsSync(path)) {
38
- logger.warn(`Temporary path ${path} does not exist.`);
39
- return;
40
- }
41
- const stats = fs.statSync(path);
42
- if (stats.isFile()) {
43
- fs.unlinkSync(path);
44
- logger.info(`Temporary file ${path} deleted successfully.`);
45
- } else if (stats.isDirectory()) {
46
- fs.rmSync(path, { recursive: true });
47
- logger.info(`Temporary folder ${path} and its contents deleted successfully.`);
48
- }
49
- } catch (err) {
50
- logger.error(`Error occurred while deleting the temporary path: ${err}`);
51
- }
52
- }
53
-
54
- function generateUniqueFilename(extension) {
55
- return `${uuidv4()}.${extension}`;
56
- }
57
-
58
- const downloadFile = async (fileUrl) => {
59
- const fileExtension = path.extname(fileUrl).slice(1);
60
- const uniqueFilename = generateUniqueFilename(fileExtension);
61
- const tempDir = os.tmpdir();
62
- const localFilePath = `${tempDir}/${uniqueFilename}`;
63
-
64
- // eslint-disable-next-line no-async-promise-executor
65
- return new Promise(async (resolve, reject) => {
66
- try {
67
- const parsedUrl = new URL(fileUrl);
68
- const protocol = parsedUrl.protocol === 'https:' ? https : http;
69
-
70
- const response = await new Promise((resolve, reject) => {
71
- protocol.get(parsedUrl, (res) => {
72
- if (res.statusCode === 200) {
73
- resolve(res);
74
- } else {
75
- reject(new Error(`HTTP request failed with status code ${res.statusCode}`));
76
- }
77
- }).on('error', reject);
78
- });
79
-
80
- await pipeline(response, fs.createWriteStream(localFilePath));
81
- logger.info(`Downloaded file to ${localFilePath}`);
82
- resolve(localFilePath);
83
- } catch (error) {
84
- fs.unlink(localFilePath, () => {
85
- reject(error);
86
- });
87
- //throw error;
88
- }
89
- });
90
- };
91
-
92
- // convert srt format to text
93
- function convertToText(str) {
94
- return str
95
- .split('\n')
96
- .filter(line => !line.match(/^\d+$/) && !line.match(/^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}$/) && line !== '')
97
- .join(' ');
98
- }
99
-
100
- function alignSubtitles(subtitles, format, offsets) {
101
- const result = [];
102
-
103
- function preprocessStr(str) {
104
- try{
105
- if(!str) return '';
106
- return str.trim().replace(/(\n\n)(?!\n)/g, '\n\n\n');
107
- }catch(e){
108
- logger.error(`An error occurred in content text preprocessing: ${e}`);
109
- return '';
110
- }
111
- }
112
-
113
- function shiftSubtitles(subtitle, shiftOffset) {
114
- const captions = subsrt.parse(preprocessStr(subtitle));
115
- const resynced = subsrt.resync(captions, { offset: shiftOffset });
116
- return resynced;
117
- }
118
-
119
- for (let i = 0; i < subtitles.length; i++) {
120
- result.push(...shiftSubtitles(subtitles[i], offsets[i]*1000)); // convert to milliseconds
121
- }
122
-
123
- try {
124
- //if content has needed html style tags, keep them
125
- for(const obj of result) {
126
- if(obj && obj.content){
127
- obj.text = obj.content;
128
- }
129
- }
130
- } catch (error) {
131
- logger.error(`An error occurred in content text parsing: ${error}`);
132
- }
133
-
134
- return subsrt.build(result, { format: format === 'vtt' ? 'vtt' : 'srt' });
135
- }
136
-
137
-
138
20
  class OpenAIWhisperPlugin extends ModelPlugin {
139
21
  constructor(pathway, model) {
140
22
  super(pathway, model);
141
23
  }
142
24
 
143
- async getMediaChunks(file, requestId) {
144
- try {
145
- if (API_URL) {
146
- //call helper api and get list of file uris
147
- const res = await axios.get(API_URL, { params: { uri: file, requestId } });
148
- return res.data;
149
- } else {
150
- logger.info(`No API_URL set, returning file as chunk`);
151
- return [file];
152
- }
153
- } catch (err) {
154
- logger.error(`Error getting media chunks list from api: ${err}`);
155
- throw err;
156
- }
157
- }
158
-
159
- async markCompletedForCleanUp(requestId) {
160
- try {
161
- if (API_URL) {
162
- //call helper api to mark processing as completed
163
- const res = await axios.delete(API_URL, { params: { requestId } });
164
- logger.info(`Marked request ${requestId} as completed:`, res.data);
165
- return res.data;
166
- }
167
- } catch (err) {
168
- logger.error(`Error marking request ${requestId} as completed: ${err}`);
169
- }
170
- }
171
-
172
25
  // Execute the request to the OpenAI Whisper API
173
26
  async execute(text, parameters, prompt, cortexRequest) {
174
27
  const { pathwayResolver } = cortexRequest;
@@ -252,7 +105,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
252
105
 
253
106
  if(!wordTimestamped && !responseFormat){
254
107
  //if no response format, convert to text
255
- return convertToText(res);
108
+ return convertSrtToText(res);
256
109
  }
257
110
  return res;
258
111
  }
@@ -324,7 +177,7 @@ let offsets = [];
324
177
  let uris = []
325
178
 
326
179
  try {
327
- const mediaChunks = await this.getMediaChunks(file, requestId);
180
+ const mediaChunks = await getMediaChunks(file, requestId);
328
181
 
329
182
  if (!mediaChunks || !mediaChunks.length) {
330
183
  throw new Error(`Error in getting chunks from media helper for file ${file}`);
@@ -363,14 +216,14 @@ try {
363
216
  }
364
217
  }
365
218
 
366
- await this.markCompletedForCleanUp(requestId);
219
+ await markCompletedForCleanUp(requestId);
367
220
 
368
221
  //check cleanup for whisper temp uploaded files url
369
222
  const regex = /whispertempfiles\/([a-z0-9-]+)/;
370
223
  const match = file.match(regex);
371
224
  if (match && match[1]) {
372
225
  const extractedValue = match[1];
373
- await this.markCompletedForCleanUp(extractedValue);
226
+ await markCompletedForCleanUp(extractedValue);
374
227
  logger.info(`Cleaned temp whisper file ${file} with request id ${extractedValue}`);
375
228
  }
376
229
 
@@ -100,7 +100,7 @@ class PalmChatPlugin extends ModelPlugin {
100
100
  throw new Error(`Prompt is too long to successfully call the model at ${tokenLength} tokens. The model will not be called.`);
101
101
  }
102
102
 
103
- // Ensure there are an even number of messages (PaLM requires an even number of messages)
103
+ // Ensure there are an odd number of messages for turn taking
104
104
  if (requestMessages.length % 2 === 0) {
105
105
  requestMessages = requestMessages.slice(1);
106
106
  }
@@ -1,6 +1,5 @@
1
1
  import { fulfillWithTimeout } from '../lib/promiser.js';
2
2
  import { PathwayResolver } from './pathwayResolver.js';
3
- import logger from '../lib/logger.js';
4
3
 
5
4
  // This resolver uses standard parameters required by Apollo server:
6
5
  // (parent, args, contextValue, info)
@@ -22,16 +21,16 @@ const rootResolver = async (parent, args, contextValue, info) => {
22
21
  try {
23
22
  result = await fulfillWithTimeout(pathway.resolver(parent, args, contextValue, info), pathway.timeout);
24
23
  } catch (error) {
25
- logger.error(`Request failed with error: ${error}`);
24
+ pathwayResolver.logError(error);
26
25
  result = error.message || error.toString();
27
26
  }
28
27
 
29
- const { warnings, previousResult, savedContextId, tool } = pathwayResolver;
28
+ const { warnings, errors, previousResult, savedContextId, tool } = pathwayResolver;
30
29
 
31
30
  // Add request parameters back as debug
32
31
  const debug = pathwayResolver.prompts.map(prompt => prompt.debugInfo || '').join('\n').trim();
33
32
 
34
- return { debug, result, warnings, previousResult, tool, contextId: savedContextId }
33
+ return { debug, result, warnings, errors, previousResult, tool, contextId: savedContextId }
35
34
  }
36
35
 
37
36
  // This resolver is used by the root resolver to process the request
package/server/typeDef.js CHANGED
@@ -50,6 +50,7 @@ const typeDef = (pathway) => {
50
50
  result: ${resultStr}
51
51
  previousResult: String
52
52
  warnings: [String]
53
+ errors: [String]
53
54
  contextId: String
54
55
  tool: String
55
56
  }`;