@aj-archipelago/cortex 1.0.3 → 1.0.5
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/README.md +1 -1
- package/config/default.example.json +18 -0
- package/config.js +25 -5
- package/graphql/pathwayPrompter.js +8 -0
- package/graphql/pathwayResolver.js +9 -1
- package/graphql/plugins/azureTranslatePlugin.js +22 -0
- package/graphql/plugins/modelPlugin.js +15 -42
- package/graphql/plugins/openAiChatPlugin.js +85 -2
- package/graphql/plugins/openAiCompletionPlugin.js +32 -2
- package/graphql/plugins/openAiWhisperPlugin.js +49 -13
- package/graphql/plugins/palmChatPlugin.js +229 -0
- package/graphql/plugins/palmCompletionPlugin.js +134 -0
- package/graphql/prompt.js +11 -4
- package/helper_apps/MediaFileChunker/Dockerfile +20 -0
- package/helper_apps/MediaFileChunker/fileChunker.js +50 -6
- package/helper_apps/MediaFileChunker/helper.js +13 -1
- package/helper_apps/MediaFileChunker/index.js +2 -4
- package/helper_apps/MediaFileChunker/package-lock.json +73 -18
- package/helper_apps/MediaFileChunker/package.json +2 -1
- package/lib/gcpAuthTokenHelper.js +37 -0
- package/package.json +3 -1
- package/pathways/completions.js +17 -0
- package/pathways/index.js +4 -2
- package/pathways/{lc_test.mjs → test_langchain.mjs} +1 -1
- package/pathways/test_oai_chat.js +18 -0
- package/pathways/test_oai_cmpl.js +13 -0
- package/pathways/test_palm_chat.js +31 -0
- package/pathways/transcribe.js +1 -0
- package/pathways/translate.js +2 -1
- package/tests/chunking.test.js +8 -6
- package/tests/modelPlugin.test.js +2 -14
- package/tests/openAiChatPlugin.test.js +125 -0
- package/tests/palmChatPlugin.test.js +256 -0
- package/tests/palmCompletionPlugin.test.js +87 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Just about anything! It's kind of an LLM swiss army knife. Here are some ideas:
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
19
|
* Simple architecture to build custom functional endpoints (called `pathways`), that implement common NL AI tasks. Default pathways include chat, summarization, translation, paraphrasing, completion, spelling and grammar correction, entity extraction, sentiment analysis, and bias analysis.
|
|
20
|
-
* Allows for building multi-model, multi-tool, multi-vendor, and model-agnostic pathways (choose the right model or combination of models and tools for the job, implement redundancy) with built-in support for OpenAI GPT-3, GPT-3.5 (chatGPT), and GPT-4 models - both from OpenAI directly and through Azure OpenAI, OpenAI Whisper, Azure Translator, LangChain.js and more.
|
|
20
|
+
* Allows for building multi-model, multi-tool, multi-vendor, and model-agnostic pathways (choose the right model or combination of models and tools for the job, implement redundancy) with built-in support for OpenAI GPT-3, GPT-3.5 (chatGPT), and GPT-4 models - both from OpenAI directly and through Azure OpenAI, PaLM Text and PaLM Chat from Google, OpenAI Whisper, Azure Translator, LangChain.js and more.
|
|
21
21
|
* Easy, templatized prompt definition with flexible support for most prompt engineering techniques and strategies ranging from simple single prompts to complex custom prompt chains with context continuity.
|
|
22
22
|
* Built in support for long-running, asynchronous operations with progress updates or streaming responses
|
|
23
23
|
* Integrated context persistence: have your pathways "remember" whatever you want and use it on the next request to the model
|
|
@@ -51,6 +51,24 @@
|
|
|
51
51
|
"requestsPerSecond": 10,
|
|
52
52
|
"maxTokenLength": 8192
|
|
53
53
|
},
|
|
54
|
+
"palm-text": {
|
|
55
|
+
"type": "PALM-COMPLETION",
|
|
56
|
+
"url": "https://us-central1-aiplatform.googleapis.com/v1/projects/project-id/locations/us-central1/publishers/google/models/text-bison@001:predict",
|
|
57
|
+
"headers": {
|
|
58
|
+
"Content-Type": "application/json"
|
|
59
|
+
},
|
|
60
|
+
"requestsPerSecond": 10,
|
|
61
|
+
"maxTokenLength": 2048
|
|
62
|
+
},
|
|
63
|
+
"palm-chat": {
|
|
64
|
+
"type": "PALM-CHAT",
|
|
65
|
+
"url": "https://us-central1-aiplatform.googleapis.com/v1/projects/project-id/locations/us-central1/publishers/google/models/chat-bison@001:predict",
|
|
66
|
+
"headers": {
|
|
67
|
+
"Content-Type": "application/json"
|
|
68
|
+
},
|
|
69
|
+
"requestsPerSecond": 10,
|
|
70
|
+
"maxTokenLength": 2048
|
|
71
|
+
},
|
|
54
72
|
"local-llama13B": {
|
|
55
73
|
"type": "LOCAL-CPP-MODEL",
|
|
56
74
|
"executablePath": "../llm/llama.cpp/main",
|
package/config.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
3
2
|
import convict from 'convict';
|
|
4
3
|
import HandleBars from './lib/handleBars.js';
|
|
5
4
|
import fs from 'fs';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
6
|
+
import GcpAuthTokenHelper from './lib/gcpAuthTokenHelper.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
9
|
|
|
7
10
|
// Schema for config
|
|
8
11
|
var config = convict({
|
|
@@ -55,7 +58,8 @@ var config = convict({
|
|
|
55
58
|
cortexApiKey: {
|
|
56
59
|
format: String,
|
|
57
60
|
default: null,
|
|
58
|
-
env: 'CORTEX_API_KEY'
|
|
61
|
+
env: 'CORTEX_API_KEY',
|
|
62
|
+
sensitive: true
|
|
59
63
|
},
|
|
60
64
|
defaultModelName: {
|
|
61
65
|
format: String,
|
|
@@ -75,6 +79,7 @@ var config = convict({
|
|
|
75
79
|
"params": {
|
|
76
80
|
"model": "{{openaiDefaultModel}}"
|
|
77
81
|
},
|
|
82
|
+
"requestsPerSecond": 2,
|
|
78
83
|
},
|
|
79
84
|
"oai-whisper": {
|
|
80
85
|
"type": "OPENAI_WHISPER",
|
|
@@ -115,6 +120,12 @@ var config = convict({
|
|
|
115
120
|
default: 'null',
|
|
116
121
|
env: 'WHISPER_MEDIA_API_URL'
|
|
117
122
|
},
|
|
123
|
+
gcpServiceAccountKey: {
|
|
124
|
+
format: String,
|
|
125
|
+
default: null,
|
|
126
|
+
env: 'GCP_SERVICE_ACCOUNT_KEY',
|
|
127
|
+
sensitive: true
|
|
128
|
+
},
|
|
118
129
|
});
|
|
119
130
|
|
|
120
131
|
// Read in environment variables and set up service configuration
|
|
@@ -133,21 +144,30 @@ if (configFile && fs.existsSync(configFile)) {
|
|
|
133
144
|
}
|
|
134
145
|
}
|
|
135
146
|
|
|
147
|
+
if (config.get('gcpServiceAccountKey')) {
|
|
148
|
+
const gcpAuthTokenHelper = new GcpAuthTokenHelper(config.getProperties());
|
|
149
|
+
config.set('gcpAuthTokenHelper', gcpAuthTokenHelper);
|
|
150
|
+
}
|
|
151
|
+
|
|
136
152
|
// Build and load pathways to config
|
|
137
153
|
const buildPathways = async (config) => {
|
|
138
154
|
const { pathwaysPath, corePathwaysPath, basePathwayPath } = config.getProperties();
|
|
139
155
|
|
|
156
|
+
const pathwaysURL = pathToFileURL(pathwaysPath).toString();
|
|
157
|
+
const corePathwaysURL = pathToFileURL(corePathwaysPath).toString();
|
|
158
|
+
const basePathwayURL = pathToFileURL(basePathwayPath).toString();
|
|
159
|
+
|
|
140
160
|
// Load cortex base pathway
|
|
141
|
-
const basePathway = await import(
|
|
161
|
+
const basePathway = await import(basePathwayURL).then(module => module.default);
|
|
142
162
|
|
|
143
163
|
// Load core pathways, default from the Cortex package
|
|
144
164
|
console.log('Loading core pathways from', corePathwaysPath)
|
|
145
|
-
let loadedPathways = await import(`${
|
|
165
|
+
let loadedPathways = await import(`${corePathwaysURL}/index.js`).then(module => module);
|
|
146
166
|
|
|
147
167
|
// Load custom pathways and override core pathways if same
|
|
148
168
|
if (pathwaysPath && fs.existsSync(pathwaysPath)) {
|
|
149
169
|
console.log('Loading custom pathways from', pathwaysPath)
|
|
150
|
-
const customPathways = await import(`${
|
|
170
|
+
const customPathways = await import(`${pathwaysURL}/index.js`).then(module => module);
|
|
151
171
|
loadedPathways = { ...loadedPathways, ...customPathways };
|
|
152
172
|
}
|
|
153
173
|
|
|
@@ -4,6 +4,8 @@ import OpenAICompletionPlugin from './plugins/openAiCompletionPlugin.js';
|
|
|
4
4
|
import AzureTranslatePlugin from './plugins/azureTranslatePlugin.js';
|
|
5
5
|
import OpenAIWhisperPlugin from './plugins/openAiWhisperPlugin.js';
|
|
6
6
|
import LocalModelPlugin from './plugins/localModelPlugin.js';
|
|
7
|
+
import PalmChatPlugin from './plugins/palmChatPlugin.js';
|
|
8
|
+
import PalmCompletionPlugin from './plugins/palmCompletionPlugin.js';
|
|
7
9
|
|
|
8
10
|
class PathwayPrompter {
|
|
9
11
|
constructor({ config, pathway }) {
|
|
@@ -33,6 +35,12 @@ class PathwayPrompter {
|
|
|
33
35
|
case 'LOCAL-CPP-MODEL':
|
|
34
36
|
plugin = new LocalModelPlugin(config, pathway);
|
|
35
37
|
break;
|
|
38
|
+
case 'PALM-CHAT':
|
|
39
|
+
plugin = new PalmChatPlugin(config, pathway);
|
|
40
|
+
break;
|
|
41
|
+
case 'PALM-COMPLETION':
|
|
42
|
+
plugin = new PalmCompletionPlugin(config, pathway);
|
|
43
|
+
break;
|
|
36
44
|
default:
|
|
37
45
|
throw new Error(`Unsupported model type: ${model.type}`);
|
|
38
46
|
}
|
|
@@ -289,7 +289,15 @@ class PathwayResolver {
|
|
|
289
289
|
if (requestState[this.requestId].canceled) {
|
|
290
290
|
return;
|
|
291
291
|
}
|
|
292
|
-
|
|
292
|
+
let result = '';
|
|
293
|
+
|
|
294
|
+
// If this text is empty, skip applying the prompt as it will likely be a nonsensical result
|
|
295
|
+
if (!/^\s*$/.test(text)) {
|
|
296
|
+
result = await this.pathwayPrompter.execute(text, { ...parameters, ...this.savedContext }, prompt, this);
|
|
297
|
+
} else {
|
|
298
|
+
result = text;
|
|
299
|
+
}
|
|
300
|
+
|
|
293
301
|
requestState[this.requestId].completedCount++;
|
|
294
302
|
|
|
295
303
|
const { completedCount, totalCount } = requestState[this.requestId];
|
|
@@ -35,6 +35,28 @@ class AzureTranslatePlugin extends ModelPlugin {
|
|
|
35
35
|
|
|
36
36
|
return this.executeRequest(url, data, params, headers, prompt);
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
// Parse the response from the Azure Translate API
|
|
40
|
+
parseResponse(data) {
|
|
41
|
+
if (Array.isArray(data) && data.length > 0 && data[0].translations) {
|
|
42
|
+
return data[0].translations[0].text.trim();
|
|
43
|
+
} else {
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Override the logging function to display the request and response
|
|
49
|
+
logRequestData(data, responseData, prompt) {
|
|
50
|
+
const separator = `\n=== ${this.pathwayName}.${this.requestCount++} ===\n`;
|
|
51
|
+
console.log(separator);
|
|
52
|
+
|
|
53
|
+
const modelInput = data[0].Text;
|
|
54
|
+
|
|
55
|
+
console.log(`\x1b[36m${modelInput}\x1b[0m`);
|
|
56
|
+
console.log(`\x1b[34m> ${this.parseResponse(responseData)}\x1b[0m`);
|
|
57
|
+
|
|
58
|
+
prompt && prompt.debugInfo && (prompt.debugInfo += `${separator}${JSON.stringify(data)}`);
|
|
59
|
+
}
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
export default AzureTranslatePlugin;
|
|
@@ -62,7 +62,7 @@ class ModelPlugin {
|
|
|
62
62
|
const message = tokenLengths[index].message;
|
|
63
63
|
|
|
64
64
|
// Skip system messages
|
|
65
|
-
if (message
|
|
65
|
+
if (message?.role === 'system') {
|
|
66
66
|
index++;
|
|
67
67
|
continue;
|
|
68
68
|
}
|
|
@@ -113,7 +113,7 @@ class ModelPlugin {
|
|
|
113
113
|
let output = "";
|
|
114
114
|
if (messages && messages.length) {
|
|
115
115
|
for (let message of messages) {
|
|
116
|
-
output += (message.role && (message.content || message.content === '')) ? `<|im_start|>${message.role}\n${message.content}\n<|im_end|>\n` : `${message}\n`;
|
|
116
|
+
output += ((message.author || message.role) && (message.content || message.content === '')) ? `<|im_start|>${(message.author || message.role)}\n${message.content}\n<|im_end|>\n` : `${message}\n`;
|
|
117
117
|
}
|
|
118
118
|
// you always want the assistant to respond next so add a
|
|
119
119
|
// directive for that
|
|
@@ -124,6 +124,7 @@ class ModelPlugin {
|
|
|
124
124
|
return output;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// compile the Prompt
|
|
127
128
|
getCompiledPrompt(text, parameters, prompt) {
|
|
128
129
|
const combinedParameters = { ...this.promptParameters, ...parameters };
|
|
129
130
|
const modelPrompt = this.getModelPrompt(prompt, parameters);
|
|
@@ -132,9 +133,9 @@ class ModelPlugin {
|
|
|
132
133
|
const modelPromptMessagesML = this.messagesToChatML(modelPromptMessages);
|
|
133
134
|
|
|
134
135
|
if (modelPromptMessagesML) {
|
|
135
|
-
return { modelPromptMessages, tokenLength: encode(modelPromptMessagesML).length };
|
|
136
|
+
return { modelPromptMessages, tokenLength: encode(modelPromptMessagesML).length, modelPrompt };
|
|
136
137
|
} else {
|
|
137
|
-
return { modelPromptText, tokenLength: encode(modelPromptText).length };
|
|
138
|
+
return { modelPromptText, tokenLength: encode(modelPromptText).length, modelPrompt };
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -147,12 +148,11 @@ class ModelPlugin {
|
|
|
147
148
|
return this.promptParameters.inputParameters?.tokenRatio ?? this.promptParameters.tokenRatio ?? DEFAULT_PROMPT_TOKEN_RATIO;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
|
|
151
151
|
getModelPrompt(prompt, parameters) {
|
|
152
152
|
if (typeof(prompt) === 'function') {
|
|
153
|
-
|
|
153
|
+
return prompt(parameters);
|
|
154
154
|
} else {
|
|
155
|
-
|
|
155
|
+
return prompt;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -160,20 +160,20 @@ class ModelPlugin {
|
|
|
160
160
|
if (!modelPrompt.messages) {
|
|
161
161
|
return null;
|
|
162
162
|
}
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
// First run handlebars compile on the pathway messages
|
|
165
165
|
const compiledMessages = modelPrompt.messages.map((message) => {
|
|
166
166
|
if (message.content) {
|
|
167
167
|
const compileText = HandleBars.compile(message.content);
|
|
168
168
|
return {
|
|
169
|
-
|
|
169
|
+
...message,
|
|
170
170
|
content: compileText({ ...combinedParameters, text }),
|
|
171
171
|
};
|
|
172
172
|
} else {
|
|
173
173
|
return message;
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
|
-
|
|
176
|
+
|
|
177
177
|
// Next add in any parameters that are referenced by name in the array
|
|
178
178
|
const expandedMessages = compiledMessages.flatMap((message) => {
|
|
179
179
|
if (typeof message === 'string') {
|
|
@@ -188,7 +188,7 @@ class ModelPlugin {
|
|
|
188
188
|
return [message];
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
return expandedMessages;
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -197,44 +197,17 @@ class ModelPlugin {
|
|
|
197
197
|
return generateUrl({ ...this.model, ...this.environmentVariables, ...this.config });
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
//
|
|
201
|
-
parseResponse(data) {
|
|
202
|
-
const { choices } = data;
|
|
203
|
-
if (!choices || !choices.length) {
|
|
204
|
-
if (Array.isArray(data) && data.length > 0 && data[0].translations) {
|
|
205
|
-
return data[0].translations[0].text.trim();
|
|
206
|
-
} else {
|
|
207
|
-
return data;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// if we got a choices array back with more than one choice, return the whole array
|
|
212
|
-
if (choices.length > 1) {
|
|
213
|
-
return choices;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// otherwise, return the first choice
|
|
217
|
-
const textResult = choices[0].text && choices[0].text.trim();
|
|
218
|
-
const messageResult = choices[0].message && choices[0].message.content && choices[0].message.content.trim();
|
|
219
|
-
|
|
220
|
-
return messageResult ?? textResult ?? null;
|
|
221
|
-
}
|
|
200
|
+
// Default response parsing
|
|
201
|
+
parseResponse(data) { return data; };
|
|
222
202
|
|
|
203
|
+
// Default simple logging
|
|
223
204
|
logRequestData(data, responseData, prompt) {
|
|
224
205
|
const separator = `\n=== ${this.pathwayName}.${this.requestCount++} ===\n`;
|
|
225
206
|
console.log(separator);
|
|
226
207
|
|
|
227
208
|
const modelInput = data.prompt || (data.messages && data.messages[0].content) || (data.length > 0 && data[0].Text) || null;
|
|
228
209
|
|
|
229
|
-
if (
|
|
230
|
-
data.messages.forEach((message, index) => {
|
|
231
|
-
const words = message.content.split(" ");
|
|
232
|
-
const tokenCount = encode(message.content).length;
|
|
233
|
-
const preview = words.length < 41 ? message.content : words.slice(0, 20).join(" ") + " ... " + words.slice(-20).join(" ");
|
|
234
|
-
|
|
235
|
-
console.log(`\x1b[36mMessage ${index + 1}: Role: ${message.role}, Tokens: ${tokenCount}, Content: "${preview}"\x1b[0m`);
|
|
236
|
-
});
|
|
237
|
-
} else {
|
|
210
|
+
if (modelInput) {
|
|
238
211
|
console.log(`\x1b[36m${modelInput}\x1b[0m`);
|
|
239
212
|
}
|
|
240
213
|
|
|
@@ -1,20 +1,64 @@
|
|
|
1
1
|
// OpenAIChatPlugin.js
|
|
2
2
|
import ModelPlugin from './modelPlugin.js';
|
|
3
|
+
import { encode } from 'gpt-3-encoder';
|
|
3
4
|
|
|
4
5
|
class OpenAIChatPlugin extends ModelPlugin {
|
|
5
6
|
constructor(config, pathway) {
|
|
6
7
|
super(config, pathway);
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
// convert to OpenAI messages array format if necessary
|
|
11
|
+
convertPalmToOpenAIMessages(context, examples, messages) {
|
|
12
|
+
let openAIMessages = [];
|
|
13
|
+
|
|
14
|
+
// Add context as a system message
|
|
15
|
+
if (context) {
|
|
16
|
+
openAIMessages.push({
|
|
17
|
+
role: 'system',
|
|
18
|
+
content: context,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Add examples to the messages array
|
|
23
|
+
examples.forEach(example => {
|
|
24
|
+
openAIMessages.push({
|
|
25
|
+
role: example.input.author || 'user',
|
|
26
|
+
content: example.input.content,
|
|
27
|
+
});
|
|
28
|
+
openAIMessages.push({
|
|
29
|
+
role: example.output.author || 'assistant',
|
|
30
|
+
content: example.output.content,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Add remaining messages to the messages array
|
|
35
|
+
messages.forEach(message => {
|
|
36
|
+
openAIMessages.push({
|
|
37
|
+
role: message.author,
|
|
38
|
+
content: message.content,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return openAIMessages;
|
|
43
|
+
}
|
|
44
|
+
|
|
9
45
|
// Set up parameters specific to the OpenAI Chat API
|
|
10
46
|
getRequestParameters(text, parameters, prompt) {
|
|
11
|
-
const { modelPromptText, modelPromptMessages, tokenLength } = this.getCompiledPrompt(text, parameters, prompt);
|
|
47
|
+
const { modelPromptText, modelPromptMessages, tokenLength, modelPrompt } = this.getCompiledPrompt(text, parameters, prompt);
|
|
12
48
|
const { stream } = parameters;
|
|
13
49
|
|
|
14
50
|
// Define the model's max token length
|
|
15
51
|
const modelTargetTokenLength = this.getModelMaxTokenLength() * this.getPromptTokenRatio();
|
|
16
52
|
|
|
17
53
|
let requestMessages = modelPromptMessages || [{ "role": "user", "content": modelPromptText }];
|
|
54
|
+
|
|
55
|
+
// Check if the messages are in Palm format and convert them to OpenAI format if necessary
|
|
56
|
+
const isPalmFormat = requestMessages.some(message => 'author' in message);
|
|
57
|
+
if (isPalmFormat) {
|
|
58
|
+
const context = modelPrompt.context || '';
|
|
59
|
+
const examples = modelPrompt.examples || [];
|
|
60
|
+
requestMessages = this.convertPalmToOpenAIMessages(context, examples, expandedMessages);
|
|
61
|
+
}
|
|
18
62
|
|
|
19
63
|
// Check if the token length exceeds the model's max token length
|
|
20
64
|
if (tokenLength > modelTargetTokenLength) {
|
|
@@ -25,7 +69,7 @@ class OpenAIChatPlugin extends ModelPlugin {
|
|
|
25
69
|
const requestParameters = {
|
|
26
70
|
messages: requestMessages,
|
|
27
71
|
temperature: this.temperature ?? 0.7,
|
|
28
|
-
stream
|
|
72
|
+
...(stream !== undefined ? { stream } : {}),
|
|
29
73
|
};
|
|
30
74
|
|
|
31
75
|
return requestParameters;
|
|
@@ -41,6 +85,45 @@ class OpenAIChatPlugin extends ModelPlugin {
|
|
|
41
85
|
const headers = this.model.headers || {};
|
|
42
86
|
return this.executeRequest(url, data, params, headers, prompt);
|
|
43
87
|
}
|
|
88
|
+
|
|
89
|
+
// Parse the response from the OpenAI Chat API
|
|
90
|
+
parseResponse(data) {
|
|
91
|
+
const { choices } = data;
|
|
92
|
+
if (!choices || !choices.length) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// if we got a choices array back with more than one choice, return the whole array
|
|
97
|
+
if (choices.length > 1) {
|
|
98
|
+
return choices;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// otherwise, return the first choice
|
|
102
|
+
const messageResult = choices[0].message && choices[0].message.content && choices[0].message.content.trim();
|
|
103
|
+
return messageResult ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Override the logging function to display the messages and responses
|
|
107
|
+
logRequestData(data, responseData, prompt) {
|
|
108
|
+
const separator = `\n=== ${this.pathwayName}.${this.requestCount++} ===\n`;
|
|
109
|
+
console.log(separator);
|
|
110
|
+
|
|
111
|
+
if (data && data.messages && data.messages.length > 1) {
|
|
112
|
+
data.messages.forEach((message, index) => {
|
|
113
|
+
const words = message.content.split(" ");
|
|
114
|
+
const tokenCount = encode(message.content).length;
|
|
115
|
+
const preview = words.length < 41 ? message.content : words.slice(0, 20).join(" ") + " ... " + words.slice(-20).join(" ");
|
|
116
|
+
|
|
117
|
+
console.log(`\x1b[36mMessage ${index + 1}: Role: ${message.role}, Tokens: ${tokenCount}, Content: "${preview}"\x1b[0m`);
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
console.log(`\x1b[36m${data.messages[0].content}\x1b[0m`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`\x1b[34m> ${this.parseResponse(responseData)}\x1b[0m`);
|
|
124
|
+
|
|
125
|
+
prompt && prompt.debugInfo && (prompt.debugInfo += `${separator}${JSON.stringify(data)}`);
|
|
126
|
+
}
|
|
44
127
|
}
|
|
45
128
|
|
|
46
129
|
export default OpenAIChatPlugin;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// OpenAICompletionPlugin.js
|
|
2
2
|
|
|
3
3
|
import ModelPlugin from './modelPlugin.js';
|
|
4
|
-
|
|
5
4
|
import { encode } from 'gpt-3-encoder';
|
|
6
5
|
|
|
7
6
|
// Helper function to truncate the prompt if it is too long
|
|
@@ -52,7 +51,7 @@ class OpenAICompletionPlugin extends ModelPlugin {
|
|
|
52
51
|
frequency_penalty: 0,
|
|
53
52
|
presence_penalty: 0,
|
|
54
53
|
stop: ["<|im_end|>"],
|
|
55
|
-
stream
|
|
54
|
+
...(stream !== undefined ? { stream } : {}),
|
|
56
55
|
};
|
|
57
56
|
} else {
|
|
58
57
|
|
|
@@ -83,8 +82,39 @@ class OpenAICompletionPlugin extends ModelPlugin {
|
|
|
83
82
|
const data = { ...(this.model.params || {}), ...requestParameters };
|
|
84
83
|
const params = {};
|
|
85
84
|
const headers = this.model.headers || {};
|
|
85
|
+
|
|
86
86
|
return this.executeRequest(url, data, params, headers, prompt);
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
// Parse the response from the OpenAI Completion API
|
|
90
|
+
parseResponse(data) {
|
|
91
|
+
const { choices } = data;
|
|
92
|
+
if (!choices || !choices.length) {
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// if we got a choices array back with more than one choice, return the whole array
|
|
97
|
+
if (choices.length > 1) {
|
|
98
|
+
return choices;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// otherwise, return the first choice
|
|
102
|
+
const textResult = choices[0].text && choices[0].text.trim();
|
|
103
|
+
return textResult ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Override the logging function to log the prompt and response
|
|
107
|
+
logRequestData(data, responseData, prompt) {
|
|
108
|
+
const separator = `\n=== ${this.pathwayName}.${this.requestCount++} ===\n`;
|
|
109
|
+
console.log(separator);
|
|
110
|
+
|
|
111
|
+
const modelInput = data.prompt;
|
|
112
|
+
|
|
113
|
+
console.log(`\x1b[36m${modelInput}\x1b[0m`);
|
|
114
|
+
console.log(`\x1b[34m> ${this.parseResponse(responseData)}\x1b[0m`);
|
|
115
|
+
|
|
116
|
+
prompt && prompt.debugInfo && (prompt.debugInfo += `${separator}${JSON.stringify(data)}`);
|
|
117
|
+
}
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
export default OpenAICompletionPlugin;
|
|
@@ -14,11 +14,33 @@ import http from 'http';
|
|
|
14
14
|
import https from 'https';
|
|
15
15
|
import url from 'url';
|
|
16
16
|
import { promisify } from 'util';
|
|
17
|
+
import subsrt from 'subsrt';
|
|
17
18
|
const pipeline = promisify(stream.pipeline);
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
const API_URL = config.get('whisperMediaApiUrl');
|
|
21
22
|
|
|
23
|
+
function alignSubtitles(subtitles) {
|
|
24
|
+
const result = [];
|
|
25
|
+
const offset = 1000 * 60 * 10; // 10 minutes for each chunk
|
|
26
|
+
|
|
27
|
+
function preprocessStr(str) {
|
|
28
|
+
return str.trim().replace(/(\n\n)(?!\n)/g, '\n\n\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shiftSubtitles(subtitle, shiftOffset) {
|
|
32
|
+
const captions = subsrt.parse(preprocessStr(subtitle));
|
|
33
|
+
const resynced = subsrt.resync(captions, { offset: shiftOffset });
|
|
34
|
+
return resynced;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < subtitles.length; i++) {
|
|
38
|
+
const subtitle = subtitles[i];
|
|
39
|
+
result.push(...shiftSubtitles(subtitle, i * offset));
|
|
40
|
+
}
|
|
41
|
+
return subsrt.build(result);
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
function generateUniqueFilename(extension) {
|
|
23
45
|
return `${uuidv4()}.${extension}`;
|
|
24
46
|
}
|
|
@@ -52,6 +74,7 @@ const downloadFile = async (fileUrl) => {
|
|
|
52
74
|
fs.unlink(localFilePath, () => {
|
|
53
75
|
reject(error);
|
|
54
76
|
});
|
|
77
|
+
throw error;
|
|
55
78
|
}
|
|
56
79
|
});
|
|
57
80
|
};
|
|
@@ -73,41 +96,50 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
73
96
|
}
|
|
74
97
|
} catch (err) {
|
|
75
98
|
console.log(`Error getting media chunks list from api:`, err);
|
|
99
|
+
throw err;
|
|
76
100
|
}
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
async markCompletedForCleanUp(requestId) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
try {
|
|
105
|
+
if (API_URL) {
|
|
106
|
+
//call helper api to mark processing as completed
|
|
107
|
+
const res = await axios.delete(API_URL, { params: { requestId } });
|
|
108
|
+
console.log(`Marked request ${requestId} as completed:`, res.data);
|
|
109
|
+
return res.data;
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.log(`Error marking request ${requestId} as completed:`, err);
|
|
85
113
|
}
|
|
86
114
|
}
|
|
87
115
|
|
|
88
116
|
// Execute the request to the OpenAI Whisper API
|
|
89
117
|
async execute(text, parameters, prompt, pathwayResolver) {
|
|
118
|
+
const { responseFormat } = parameters;
|
|
90
119
|
const url = this.requestUrl(text);
|
|
91
120
|
const params = {};
|
|
92
121
|
const { modelPromptText } = this.getCompiledPrompt(text, parameters, prompt);
|
|
93
122
|
|
|
94
123
|
const processChunk = async (chunk) => {
|
|
95
124
|
try {
|
|
96
|
-
const { language } = parameters;
|
|
125
|
+
const { language, responseFormat } = parameters;
|
|
126
|
+
const response_format = responseFormat || 'text';
|
|
127
|
+
|
|
97
128
|
const formData = new FormData();
|
|
98
129
|
formData.append('file', fs.createReadStream(chunk));
|
|
99
130
|
formData.append('model', this.model.params.model);
|
|
100
|
-
formData.append('response_format',
|
|
131
|
+
formData.append('response_format', response_format);
|
|
101
132
|
language && formData.append('language', language);
|
|
102
133
|
modelPromptText && formData.append('prompt', modelPromptText);
|
|
103
134
|
|
|
104
135
|
return this.executeRequest(url, formData, params, { ...this.model.headers, ...formData.getHeaders() });
|
|
105
136
|
} catch (err) {
|
|
106
137
|
console.log(err);
|
|
138
|
+
throw err;
|
|
107
139
|
}
|
|
108
140
|
}
|
|
109
141
|
|
|
110
|
-
let result =
|
|
142
|
+
let result = [];
|
|
111
143
|
let { file } = parameters;
|
|
112
144
|
let totalCount = 0;
|
|
113
145
|
let completedCount = 0;
|
|
@@ -144,7 +176,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
144
176
|
|
|
145
177
|
// sequential processing of chunks
|
|
146
178
|
for (const chunk of chunks) {
|
|
147
|
-
result
|
|
179
|
+
result.push(await processChunk(chunk));
|
|
148
180
|
sendProgress();
|
|
149
181
|
}
|
|
150
182
|
|
|
@@ -152,11 +184,11 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
152
184
|
// result = await Promise.all(mediaSplit.chunks.map(processChunk));
|
|
153
185
|
|
|
154
186
|
} catch (error) {
|
|
155
|
-
|
|
187
|
+
const errMsg = `Transcribe error: ${error?.message || JSON.stringify(error)}`;
|
|
188
|
+
console.error(errMsg);
|
|
189
|
+
return errMsg;
|
|
156
190
|
}
|
|
157
191
|
finally {
|
|
158
|
-
// isYoutubeUrl && (await deleteTempPath(file));
|
|
159
|
-
// folder && (await deleteTempPath(folder));
|
|
160
192
|
try {
|
|
161
193
|
for (const chunk of chunks) {
|
|
162
194
|
await deleteTempPath(chunk);
|
|
@@ -177,7 +209,11 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
177
209
|
console.error("An error occurred while deleting:", error);
|
|
178
210
|
}
|
|
179
211
|
}
|
|
180
|
-
|
|
212
|
+
|
|
213
|
+
if (['srt','vtt'].includes(responseFormat)) { // align subtitles for formats
|
|
214
|
+
return alignSubtitles(result);
|
|
215
|
+
}
|
|
216
|
+
return result.join(` `);
|
|
181
217
|
}
|
|
182
218
|
}
|
|
183
219
|
|