@aj-archipelago/cortex 1.0.4 → 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 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
@@ -3,6 +3,7 @@ import convict from 'convict';
3
3
  import HandleBars from './lib/handleBars.js';
4
4
  import fs from 'fs';
5
5
  import { fileURLToPath, pathToFileURL } from 'url';
6
+ import GcpAuthTokenHelper from './lib/gcpAuthTokenHelper.js';
6
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
 
@@ -57,7 +58,8 @@ var config = convict({
57
58
  cortexApiKey: {
58
59
  format: String,
59
60
  default: null,
60
- env: 'CORTEX_API_KEY'
61
+ env: 'CORTEX_API_KEY',
62
+ sensitive: true
61
63
  },
62
64
  defaultModelName: {
63
65
  format: String,
@@ -77,6 +79,7 @@ var config = convict({
77
79
  "params": {
78
80
  "model": "{{openaiDefaultModel}}"
79
81
  },
82
+ "requestsPerSecond": 2,
80
83
  },
81
84
  "oai-whisper": {
82
85
  "type": "OPENAI_WHISPER",
@@ -117,6 +120,12 @@ var config = convict({
117
120
  default: 'null',
118
121
  env: 'WHISPER_MEDIA_API_URL'
119
122
  },
123
+ gcpServiceAccountKey: {
124
+ format: String,
125
+ default: null,
126
+ env: 'GCP_SERVICE_ACCOUNT_KEY',
127
+ sensitive: true
128
+ },
120
129
  });
121
130
 
122
131
  // Read in environment variables and set up service configuration
@@ -135,6 +144,11 @@ if (configFile && fs.existsSync(configFile)) {
135
144
  }
136
145
  }
137
146
 
147
+ if (config.get('gcpServiceAccountKey')) {
148
+ const gcpAuthTokenHelper = new GcpAuthTokenHelper(config.getProperties());
149
+ config.set('gcpAuthTokenHelper', gcpAuthTokenHelper);
150
+ }
151
+
138
152
  // Build and load pathways to config
139
153
  const buildPathways = async (config) => {
140
154
  const { pathwaysPath, corePathwaysPath, basePathwayPath } = config.getProperties();
@@ -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
- const result = await this.pathwayPrompter.execute(text, { ...parameters, ...this.savedContext }, prompt, this);
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.role === 'system') {
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
- return prompt(parameters);
153
+ return prompt(parameters);
154
154
  } else {
155
- return prompt;
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
- role: message.role,
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
- //simples form string single or list return
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 (data && data.messages && data.messages.length > 1) {
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
  }
@@ -93,17 +115,20 @@ class OpenAIWhisperPlugin extends ModelPlugin {
93
115
 
94
116
  // Execute the request to the OpenAI Whisper API
95
117
  async execute(text, parameters, prompt, pathwayResolver) {
118
+ const { responseFormat } = parameters;
96
119
  const url = this.requestUrl(text);
97
120
  const params = {};
98
121
  const { modelPromptText } = this.getCompiledPrompt(text, parameters, prompt);
99
122
 
100
123
  const processChunk = async (chunk) => {
101
124
  try {
102
- const { language } = parameters;
125
+ const { language, responseFormat } = parameters;
126
+ const response_format = responseFormat || 'text';
127
+
103
128
  const formData = new FormData();
104
129
  formData.append('file', fs.createReadStream(chunk));
105
130
  formData.append('model', this.model.params.model);
106
- formData.append('response_format', 'text');
131
+ formData.append('response_format', response_format);
107
132
  language && formData.append('language', language);
108
133
  modelPromptText && formData.append('prompt', modelPromptText);
109
134
 
@@ -114,7 +139,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
114
139
  }
115
140
  }
116
141
 
117
- let result = ``;
142
+ let result = [];
118
143
  let { file } = parameters;
119
144
  let totalCount = 0;
120
145
  let completedCount = 0;
@@ -151,7 +176,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
151
176
 
152
177
  // sequential processing of chunks
153
178
  for (const chunk of chunks) {
154
- result += await processChunk(chunk);
179
+ result.push(await processChunk(chunk));
155
180
  sendProgress();
156
181
  }
157
182
 
@@ -184,7 +209,11 @@ class OpenAIWhisperPlugin extends ModelPlugin {
184
209
  console.error("An error occurred while deleting:", error);
185
210
  }
186
211
  }
187
- return result;
212
+
213
+ if (['srt','vtt'].includes(responseFormat)) { // align subtitles for formats
214
+ return alignSubtitles(result);
215
+ }
216
+ return result.join(` `);
188
217
  }
189
218
  }
190
219