@aj-archipelago/cortex 1.1.33 → 1.1.35
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 +98 -1
- package/config/dynamicPathwaysConfig.example.json +4 -0
- package/config.js +83 -10
- package/helper-apps/cortex-autogen/function_app.py +7 -0
- package/helper-apps/cortex-autogen/myautogen.py +109 -20
- package/helper-apps/cortex-autogen/prompt_summary.txt +13 -4
- package/helper-apps/cortex-autogen/requirements.txt +1 -1
- package/helper-apps/cortex-file-handler/package-lock.json +387 -203
- package/helper-apps/cortex-file-handler/package.json +3 -3
- package/helper-apps/cortex-whisper-wrapper/.dockerignore +1 -0
- package/helper-apps/cortex-whisper-wrapper/app.py +3 -1
- package/helper-apps/cortex-whisper-wrapper/requirements.txt +1 -1
- package/lib/pathwayManager.js +422 -0
- package/lib/requestExecutor.js +19 -15
- package/lib/util.js +4 -1
- package/package.json +5 -1
- package/pathways/code_human_input.js +47 -0
- package/pathways/dynamic/pathways.json +1 -0
- package/pathways/flux_image.js +12 -0
- package/pathways/index.js +4 -0
- package/server/graphql.js +67 -37
- package/server/modelExecutor.js +4 -0
- package/server/pathwayResolver.js +1 -1
- package/server/plugins/claude3VertexPlugin.js +86 -79
- package/server/plugins/gemini15VisionPlugin.js +23 -12
- package/server/plugins/geminiVisionPlugin.js +32 -25
- package/server/plugins/modelPlugin.js +15 -2
- package/server/plugins/openAiChatPlugin.js +1 -1
- package/server/plugins/openAiVisionPlugin.js +19 -6
- package/server/plugins/runwareAIPlugin.js +81 -0
- package/server/rest.js +31 -13
- package/server/typeDef.js +33 -15
- package/tests/claude3VertexPlugin.test.js +1 -1
- package/tests/multimodal_conversion.test.js +328 -0
- package/tests/vision.test.js +20 -5
|
@@ -3,6 +3,11 @@ import mime from 'mime-types';
|
|
|
3
3
|
|
|
4
4
|
class GeminiVisionPlugin extends GeminiChatPlugin {
|
|
5
5
|
|
|
6
|
+
constructor(pathway, model) {
|
|
7
|
+
super(pathway, model);
|
|
8
|
+
this.isMultiModal = true;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
// Override the convertMessagesToGemini method to handle multimodal vision messages
|
|
7
12
|
// This function can operate on messages in Gemini native format or in OpenAI's format
|
|
8
13
|
// It will convert the messages to the Gemini format
|
|
@@ -15,40 +20,33 @@ class GeminiVisionPlugin extends GeminiChatPlugin {
|
|
|
15
20
|
modifiedMessages = messages;
|
|
16
21
|
} else {
|
|
17
22
|
messages.forEach(message => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// so we insert them as parts of the first user: role message
|
|
22
|
-
if (role === 'system') {
|
|
23
|
-
modifiedMessages.push({
|
|
24
|
-
role: 'user',
|
|
25
|
-
parts: [{ text: content }],
|
|
26
|
-
});
|
|
27
|
-
lastAuthor = 'user';
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
23
|
+
let role = message.role;
|
|
24
|
+
const { author, content } = message;
|
|
25
|
+
|
|
31
26
|
// Convert content to Gemini format, trying to maintain compatibility
|
|
32
|
-
const convertPartToGemini = (
|
|
27
|
+
const convertPartToGemini = (inputPart) => {
|
|
33
28
|
try {
|
|
34
|
-
const part = JSON.parse(
|
|
29
|
+
const part = typeof inputPart === 'string' ? JSON.parse(inputPart) : inputPart;
|
|
30
|
+
const {type, text, image_url, gcs} = part;
|
|
31
|
+
let fileUrl = gcs || image_url?.url;
|
|
32
|
+
|
|
35
33
|
if (typeof part === 'string') {
|
|
36
|
-
return { text:
|
|
37
|
-
} else if (
|
|
38
|
-
return { text:
|
|
39
|
-
} else if (
|
|
40
|
-
if (
|
|
34
|
+
return { text: text };
|
|
35
|
+
} else if (type === 'text') {
|
|
36
|
+
return { text: text };
|
|
37
|
+
} else if (type === 'image_url') {
|
|
38
|
+
if (fileUrl.startsWith('gs://')) {
|
|
41
39
|
return {
|
|
42
40
|
fileData: {
|
|
43
|
-
mimeType: mime.lookup(
|
|
44
|
-
fileUri:
|
|
41
|
+
mimeType: mime.lookup(fileUrl) || 'image/jpeg',
|
|
42
|
+
fileUri: fileUrl
|
|
45
43
|
}
|
|
46
44
|
};
|
|
47
45
|
} else {
|
|
48
46
|
return {
|
|
49
47
|
inlineData: {
|
|
50
48
|
mimeType: 'image/jpeg', // fixed for now as there's no MIME type in the request
|
|
51
|
-
data:
|
|
49
|
+
data: fileUrl.split('base64,')[1]
|
|
52
50
|
}
|
|
53
51
|
};
|
|
54
52
|
}
|
|
@@ -56,10 +54,11 @@ class GeminiVisionPlugin extends GeminiChatPlugin {
|
|
|
56
54
|
} catch (e) {
|
|
57
55
|
// this space intentionally left blank
|
|
58
56
|
}
|
|
59
|
-
return { text:
|
|
57
|
+
return inputPart ? { text: inputPart } : null;
|
|
60
58
|
};
|
|
61
|
-
|
|
59
|
+
|
|
62
60
|
const addPartToMessages = (geminiPart) => {
|
|
61
|
+
if (!geminiPart) { return; }
|
|
63
62
|
// Gemini requires alternating user: and model: messages
|
|
64
63
|
if ((role === lastAuthor || author === lastAuthor) && modifiedMessages.length > 0) {
|
|
65
64
|
modifiedMessages[modifiedMessages.length - 1].parts.push(geminiPart);
|
|
@@ -74,6 +73,14 @@ class GeminiVisionPlugin extends GeminiChatPlugin {
|
|
|
74
73
|
}
|
|
75
74
|
};
|
|
76
75
|
|
|
76
|
+
// Right now Gemini API has no direct translation for system messages,
|
|
77
|
+
// so we insert them as parts of the first user: role message
|
|
78
|
+
if (role === 'system') {
|
|
79
|
+
role = 'user';
|
|
80
|
+
addPartToMessages(convertPartToGemini(content));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
// Content can either be in the "vision" format (array) or in the "chat" format (string)
|
|
78
85
|
if (Array.isArray(content)) {
|
|
79
86
|
content.forEach(part => {
|
|
@@ -20,6 +20,7 @@ class ModelPlugin {
|
|
|
20
20
|
this.pathwayPrompt = pathway.prompt;
|
|
21
21
|
this.pathwayName = pathway.name;
|
|
22
22
|
this.promptParameters = {};
|
|
23
|
+
this.isMultiModal = false;
|
|
23
24
|
|
|
24
25
|
// Make all of the parameters defined on the pathway itself available to the prompt
|
|
25
26
|
for (const [k, v] of Object.entries(pathway)) {
|
|
@@ -205,6 +206,15 @@ class ModelPlugin {
|
|
|
205
206
|
message.content = '';
|
|
206
207
|
}
|
|
207
208
|
});
|
|
209
|
+
|
|
210
|
+
// Flatten content arrays for non-multimodal models
|
|
211
|
+
if (!this.isMultiModal) {
|
|
212
|
+
expandedMessages.forEach(message => {
|
|
213
|
+
if (Array.isArray(message?.content)) {
|
|
214
|
+
message.content = message.content.join("\n");
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
208
218
|
|
|
209
219
|
return expandedMessages;
|
|
210
220
|
}
|
|
@@ -288,8 +298,11 @@ class ModelPlugin {
|
|
|
288
298
|
return parsedData;
|
|
289
299
|
} catch (error) {
|
|
290
300
|
// Log the error and continue
|
|
291
|
-
logger.error(error.message || error);
|
|
292
|
-
|
|
301
|
+
logger.error(`Error in executeRequest for ${this.pathwayName}: ${error.message || error}`);
|
|
302
|
+
if (error.data) {
|
|
303
|
+
logger.error(`Additional error data: ${JSON.stringify(error.data)}`);
|
|
304
|
+
}
|
|
305
|
+
throw new Error(`Execution failed for ${this.pathwayName}: ${error.message || error}`);
|
|
293
306
|
}
|
|
294
307
|
}
|
|
295
308
|
|
|
@@ -112,7 +112,7 @@ class OpenAIChatPlugin extends ModelPlugin {
|
|
|
112
112
|
let totalUnits;
|
|
113
113
|
messages.forEach((message, index) => {
|
|
114
114
|
//message.content string or array
|
|
115
|
-
const content = Array.isArray(message.content) ? message.content.map(item => JSON.stringify(item)).join(', ') : message.content;
|
|
115
|
+
const content = message.content === undefined ? JSON.stringify(message) : (Array.isArray(message.content) ? message.content.map(item => JSON.stringify(item)).join(', ') : message.content);
|
|
116
116
|
const words = content.split(" ");
|
|
117
117
|
const { length, units } = this.getLength(content);
|
|
118
118
|
const preview = words.length < 41 ? content : words.slice(0, 20).join(" ") + " ... " + words.slice(-20).join(" ");
|
|
@@ -10,22 +10,35 @@ function safeJsonParse(content) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
class OpenAIVisionPlugin extends OpenAIChatPlugin {
|
|
13
|
+
|
|
14
|
+
constructor(pathway, model) {
|
|
15
|
+
super(pathway, model);
|
|
16
|
+
this.isMultiModal = true;
|
|
17
|
+
}
|
|
13
18
|
|
|
14
19
|
tryParseMessages(messages) {
|
|
15
20
|
return messages.map(message => {
|
|
16
21
|
try {
|
|
22
|
+
if (message.role === "tool") {
|
|
23
|
+
return message;
|
|
24
|
+
}
|
|
17
25
|
if (typeof message.content === 'string') {
|
|
18
26
|
message.content = safeJsonParse(message.content);
|
|
19
27
|
}
|
|
20
28
|
if (Array.isArray(message.content)) {
|
|
21
29
|
message.content = message.content.map(item => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const parsedItem = safeJsonParse(item);
|
|
31
|
+
|
|
32
|
+
if (typeof parsedItem === 'string') {
|
|
33
|
+
return { type: 'text', text: parsedItem };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof parsedItem === 'object' && parsedItem !== null && parsedItem.type === 'image_url') {
|
|
37
|
+
parsedItem.image_url.url = parsedItem.url || parsedItem.image_url.url;
|
|
38
|
+
return parsedItem;
|
|
28
39
|
}
|
|
40
|
+
|
|
41
|
+
return parsedItem;
|
|
29
42
|
});
|
|
30
43
|
}
|
|
31
44
|
} catch (e) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// RunwareAiPlugin.js
|
|
2
|
+
import ModelPlugin from "./modelPlugin.js";
|
|
3
|
+
import logger from "../../lib/logger.js";
|
|
4
|
+
import { config } from "../../config.js";
|
|
5
|
+
import { v4 as uuidv4 } from "uuid";
|
|
6
|
+
|
|
7
|
+
class RunwareAiPlugin extends ModelPlugin {
|
|
8
|
+
constructor(pathway, model) {
|
|
9
|
+
super(pathway, model);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Set up parameters specific to the Runware REST API
|
|
13
|
+
getRequestParameters(text, parameters, prompt) {
|
|
14
|
+
const combinedParameters = { ...this.promptParameters, ...parameters };
|
|
15
|
+
const { modelPromptText } = this.getCompiledPrompt(
|
|
16
|
+
text,
|
|
17
|
+
parameters,
|
|
18
|
+
prompt,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const requestParameters = {
|
|
22
|
+
data: [
|
|
23
|
+
{
|
|
24
|
+
taskType: "authentication",
|
|
25
|
+
apiKey: config.get("runwareAiApiKey"),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
taskType: "imageInference",
|
|
29
|
+
taskUUID: uuidv4(),
|
|
30
|
+
positivePrompt: modelPromptText,
|
|
31
|
+
width: combinedParameters.width,
|
|
32
|
+
height: combinedParameters.height,
|
|
33
|
+
modelId: "runware:100@1",
|
|
34
|
+
CFGScale: 4.0,
|
|
35
|
+
negative_prompt: combinedParameters.negativePrompt,
|
|
36
|
+
numberResults: combinedParameters.numberResults,
|
|
37
|
+
steps: combinedParameters.steps,
|
|
38
|
+
checkNSFW: false,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return requestParameters;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Execute the request to the Runware REST API
|
|
47
|
+
async execute(text, parameters, prompt, cortexRequest) {
|
|
48
|
+
const requestParameters = this.getRequestParameters(
|
|
49
|
+
text,
|
|
50
|
+
parameters,
|
|
51
|
+
prompt,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
cortexRequest.data = requestParameters.data;
|
|
55
|
+
cortexRequest.params = requestParameters.params;
|
|
56
|
+
|
|
57
|
+
return this.executeRequest(cortexRequest);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Parse the response from the Azure Translate API
|
|
61
|
+
parseResponse(data) {
|
|
62
|
+
if (data.data) {
|
|
63
|
+
return JSON.stringify(data.data);
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Override the logging function to display the request and response
|
|
69
|
+
logRequestData(data, responseData, prompt) {
|
|
70
|
+
const modelInput = data[1].positivePrompt;
|
|
71
|
+
|
|
72
|
+
logger.verbose(`${modelInput}`);
|
|
73
|
+
logger.verbose(`${this.parseResponse(responseData)}`);
|
|
74
|
+
|
|
75
|
+
prompt &&
|
|
76
|
+
prompt.debugInfo &&
|
|
77
|
+
(prompt.debugInfo += `\n${JSON.stringify(data)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default RunwareAiPlugin;
|
package/server/rest.js
CHANGED
|
@@ -137,13 +137,35 @@ const processIncomingStream = (requestId, res, jsonResponse, pathway) => {
|
|
|
137
137
|
if (subscription) {
|
|
138
138
|
try {
|
|
139
139
|
const subPromiseResult = await subscription;
|
|
140
|
-
subPromiseResult && pubsub.
|
|
140
|
+
if (subPromiseResult && pubsub.subscriptions?.[subPromiseResult]) {
|
|
141
|
+
pubsub.unsubscribe(subPromiseResult);
|
|
142
|
+
}
|
|
141
143
|
} catch (error) {
|
|
142
|
-
logger.
|
|
144
|
+
logger.warn(`Pubsub unsubscribe threw error: ${error}`);
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
const processStringData = (stringData) => {
|
|
150
|
+
if (progress === 1 && stringData.trim() === "[DONE]") {
|
|
151
|
+
fillJsonResponse(jsonResponse, stringData, "stop");
|
|
152
|
+
safeUnsubscribe();
|
|
153
|
+
finishStream(res, jsonResponse);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
chunkTextIntoTokens(stringData, false, useSingleTokenStream).forEach(token => {
|
|
158
|
+
fillJsonResponse(jsonResponse, token, null);
|
|
159
|
+
sendStreamData(jsonResponse);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (progress === 1) {
|
|
163
|
+
safeUnsubscribe();
|
|
164
|
+
finishStream(res, jsonResponse);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
147
169
|
if (data.requestProgress.requestId !== requestId) return;
|
|
148
170
|
|
|
149
171
|
logger.debug(`REQUEST_PROGRESS received progress: ${data.requestProgress.progress}, data: ${data.requestProgress.data}`);
|
|
@@ -152,6 +174,12 @@ const processIncomingStream = (requestId, res, jsonResponse, pathway) => {
|
|
|
152
174
|
|
|
153
175
|
try {
|
|
154
176
|
const messageJson = JSON.parse(progressData);
|
|
177
|
+
|
|
178
|
+
if (typeof messageJson === 'string') {
|
|
179
|
+
processStringData(messageJson);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
155
183
|
if (messageJson.error) {
|
|
156
184
|
logger.error(`Stream error REST: ${messageJson?.error?.message || 'unknown error'}`);
|
|
157
185
|
safeUnsubscribe();
|
|
@@ -178,17 +206,7 @@ const processIncomingStream = (requestId, res, jsonResponse, pathway) => {
|
|
|
178
206
|
} catch (error) {
|
|
179
207
|
logger.debug(`progressData not JSON: ${progressData}`);
|
|
180
208
|
if (typeof progressData === 'string') {
|
|
181
|
-
|
|
182
|
-
fillJsonResponse(jsonResponse, progressData, "stop");
|
|
183
|
-
safeUnsubscribe();
|
|
184
|
-
finishStream(res, jsonResponse);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
chunkTextIntoTokens(progressData, false, useSingleTokenStream).forEach(token => {
|
|
189
|
-
fillJsonResponse(jsonResponse, token, null);
|
|
190
|
-
sendStreamData(jsonResponse);
|
|
191
|
-
});
|
|
209
|
+
processStringData(progressData);
|
|
192
210
|
} else {
|
|
193
211
|
fillJsonResponse(jsonResponse, progressData, "stop");
|
|
194
212
|
sendStreamData(jsonResponse);
|
package/server/typeDef.js
CHANGED
|
@@ -28,7 +28,26 @@ const getGraphQlType = (value) => {
|
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const getMessageTypeDefs = () => {
|
|
32
|
+
const messageType = `input Message { role: String, content: String, name: String }`;
|
|
33
|
+
const multiMessageType = `input MultiMessage { role: String, content: [String], name: String }`;
|
|
34
|
+
|
|
35
|
+
return `${messageType}\n\n${multiMessageType}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getPathwayTypeDef = (name, returnType) => {
|
|
39
|
+
return `type ${name} {
|
|
40
|
+
debug: String
|
|
41
|
+
result: ${returnType}
|
|
42
|
+
previousResult: String
|
|
43
|
+
warnings: [String]
|
|
44
|
+
errors: [String]
|
|
45
|
+
contextId: String
|
|
46
|
+
tool: String
|
|
47
|
+
}`
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getPathwayTypeDefAndExtendQuery = (pathway) => {
|
|
32
51
|
const { name, objName, defaultInputParameters, inputParameters, format } = pathway;
|
|
33
52
|
|
|
34
53
|
const fields = format ? format.match(/\b(\w+)\b/g) : null;
|
|
@@ -36,24 +55,13 @@ const typeDef = (pathway) => {
|
|
|
36
55
|
|
|
37
56
|
const typeName = fields ? `${objName}Result` : `String`;
|
|
38
57
|
|
|
39
|
-
const messageType = `input Message { role: String, content: String, name: String }`;
|
|
40
|
-
const multiMessageType = `input MultiMessage { role: String, content: [String], name: String }`;
|
|
41
|
-
|
|
42
58
|
const type = fields ? `type ${typeName} {
|
|
43
59
|
${fieldsStr}
|
|
44
60
|
}` : ``;
|
|
45
61
|
|
|
46
|
-
const
|
|
62
|
+
const returnType = pathway.list ? `[${typeName}]` : typeName;
|
|
47
63
|
|
|
48
|
-
const responseType =
|
|
49
|
-
debug: String
|
|
50
|
-
result: ${resultStr}
|
|
51
|
-
previousResult: String
|
|
52
|
-
warnings: [String]
|
|
53
|
-
errors: [String]
|
|
54
|
-
contextId: String
|
|
55
|
-
tool: String
|
|
56
|
-
}`;
|
|
64
|
+
const responseType = getPathwayTypeDef(objName, returnType);
|
|
57
65
|
|
|
58
66
|
const params = { ...defaultInputParameters, ...inputParameters };
|
|
59
67
|
|
|
@@ -71,7 +79,7 @@ const typeDef = (pathway) => {
|
|
|
71
79
|
};
|
|
72
80
|
});
|
|
73
81
|
|
|
74
|
-
const gqlDefinition = `${
|
|
82
|
+
const gqlDefinition = `${type}\n\n${responseType}\n\nextend type Query {${name}(${paramsStr}): ${objName}}`;
|
|
75
83
|
|
|
76
84
|
return {
|
|
77
85
|
gqlDefinition,
|
|
@@ -79,6 +87,16 @@ const typeDef = (pathway) => {
|
|
|
79
87
|
};
|
|
80
88
|
};
|
|
81
89
|
|
|
90
|
+
const typeDef = (pathway) => {
|
|
91
|
+
return getPathwayTypeDefAndExtendQuery(pathway);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const userPathwayInputParameters = `text: String`;
|
|
95
|
+
|
|
96
|
+
|
|
82
97
|
export {
|
|
83
98
|
typeDef,
|
|
99
|
+
getMessageTypeDefs,
|
|
100
|
+
getPathwayTypeDef,
|
|
101
|
+
userPathwayInputParameters,
|
|
84
102
|
};
|
|
@@ -199,7 +199,7 @@ test('convertMessagesToClaudeVertex system message with user message', async (t)
|
|
|
199
199
|
test('convertMessagesToClaudeVertex user message with unsupported image type', async (t) => {
|
|
200
200
|
const plugin = new Claude3VertexPlugin(pathway, model);
|
|
201
201
|
// Test with unsupported image type
|
|
202
|
-
const messages = [{ role: 'user', content: { type: 'image_url', image_url: '
|
|
202
|
+
const messages = [{ role: 'user', content: { type: 'image_url', image_url: 'https://unec.edu.az/application/uploads/2014/12/pdf-sample.pdf' } }];
|
|
203
203
|
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
204
204
|
t.deepEqual(output, { system: '', modifiedMessages: [{role: 'user', content: [] }] });
|
|
205
205
|
});
|