@aj-archipelago/cortex 1.3.54 → 1.3.55
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 +9 -0
- package/lib/entityConstants.js +2 -0
- package/lib/util.js +56 -1
- package/package.json +1 -1
- package/pathways/image_flux.js +1 -0
- package/pathways/system/entity/sys_entity_agent.js +5 -2
- package/pathways/system/entity/tools/sys_tool_image.js +13 -3
- package/server/plugins/replicateApiPlugin.js +28 -0
- package/helper-apps/cortex-file-handler/src/hashUtils.js +0 -91
package/config.js
CHANGED
|
@@ -344,6 +344,15 @@ var config = convict({
|
|
|
344
344
|
"Content-Type": "application/json"
|
|
345
345
|
},
|
|
346
346
|
},
|
|
347
|
+
"replicate-multi-image-kontext-max": {
|
|
348
|
+
"type": "REPLICATE-API",
|
|
349
|
+
"url": "https://api.replicate.com/v1/models/flux-kontext-apps/multi-image-kontext-max/predictions",
|
|
350
|
+
"headers": {
|
|
351
|
+
"Prefer": "wait",
|
|
352
|
+
"Authorization": "Token {{REPLICATE_API_KEY}}",
|
|
353
|
+
"Content-Type": "application/json"
|
|
354
|
+
},
|
|
355
|
+
},
|
|
347
356
|
"azure-video-translate": {
|
|
348
357
|
"type": "AZURE-VIDEO-TRANSLATE",
|
|
349
358
|
"url": "https://eastus.api.cognitive.microsoft.com/videotranslation",
|
package/lib/entityConstants.js
CHANGED
|
@@ -120,6 +120,8 @@ term~N (Match terms similar to "term", edit distance N)
|
|
|
120
120
|
|
|
121
121
|
AI_GROUNDING_INSTRUCTIONS: "# Grounding Responses\n\nIf you base part or all of your response on one or more search results, you MUST cite the source using a custom markdown directive of the form :cd_source[searchResultId]. There is NO other valid way to cite a source and a good UX depends on you using this directive correctly. Do not include other clickable links to the sourcewhen using the :cd_source[searchResultId] directive. Every search result has a unique searchResultId. You must include it verbatim, copied directly from the search results. Place the directives at the end of the phrase, sentence or paragraph that is grounded in that particular search result. If you are citing multiple search results, use multiple individual :cd_source[searchResultId] directives (e.g. :cd_source[searchResultId1] :cd_source[searchResultId2] :cd_source[searchResultId3] etc.)",
|
|
122
122
|
|
|
123
|
+
AI_AVAILABLE_FILES: "# Available Files\n\nThe following files are available for you to use in your tool calls or responses:\n{{{availableFiles}}}\n",
|
|
124
|
+
|
|
123
125
|
AI_MEMORY_INSTRUCTIONS: `# Memory Instructions
|
|
124
126
|
|
|
125
127
|
You have a memory system that contains important details, instructions, and context. Consult your memories when formulating a response to ensure your answers reflect previous learnings and context.
|
package/lib/util.js
CHANGED
|
@@ -310,6 +310,60 @@ function removeImageAndFileFromMessage(message) {
|
|
|
310
310
|
return modifiedMessage;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// Helper function to extract file URLs from a content object
|
|
314
|
+
function extractFileUrlsFromContent(contentObj) {
|
|
315
|
+
const urls = [];
|
|
316
|
+
if (contentObj.type === 'image_url' && contentObj.image_url?.url) {
|
|
317
|
+
urls.push(contentObj.image_url.url);
|
|
318
|
+
} else if (contentObj.type === 'file' && contentObj.file) {
|
|
319
|
+
urls.push(contentObj.file);
|
|
320
|
+
}
|
|
321
|
+
return urls;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function getAvailableFiles(chatHistory) {
|
|
325
|
+
const availableFiles = [];
|
|
326
|
+
|
|
327
|
+
if (!chatHistory || !Array.isArray(chatHistory)) {
|
|
328
|
+
return availableFiles;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const message of chatHistory) {
|
|
332
|
+
if (!message || !message.content) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Handle array content
|
|
337
|
+
if (Array.isArray(message.content)) {
|
|
338
|
+
for (const content of message.content) {
|
|
339
|
+
try {
|
|
340
|
+
const contentObj = typeof content === 'string' ? JSON.parse(content) : content;
|
|
341
|
+
availableFiles.push(...extractFileUrlsFromContent(contentObj));
|
|
342
|
+
} catch (e) {
|
|
343
|
+
// Not JSON or couldn't be parsed, continue
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Handle string content
|
|
349
|
+
else if (typeof message.content === 'string') {
|
|
350
|
+
try {
|
|
351
|
+
const contentObj = JSON.parse(message.content);
|
|
352
|
+
availableFiles.push(...extractFileUrlsFromContent(contentObj));
|
|
353
|
+
} catch (e) {
|
|
354
|
+
// Not JSON or couldn't be parsed, continue
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Handle object content
|
|
359
|
+
else if (typeof message.content === 'object') {
|
|
360
|
+
availableFiles.push(...extractFileUrlsFromContent(message.content));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return availableFiles;
|
|
365
|
+
}
|
|
366
|
+
|
|
313
367
|
export {
|
|
314
368
|
getUniqueId,
|
|
315
369
|
getSearchResultId,
|
|
@@ -322,5 +376,6 @@ export {
|
|
|
322
376
|
alignSubtitles,
|
|
323
377
|
getMediaChunks,
|
|
324
378
|
markCompletedForCleanUp,
|
|
325
|
-
removeOldImageAndFileContent
|
|
379
|
+
removeOldImageAndFileContent,
|
|
380
|
+
getAvailableFiles
|
|
326
381
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.55",
|
|
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
|
"private": false,
|
|
6
6
|
"repository": {
|
package/pathways/image_flux.js
CHANGED
|
@@ -5,7 +5,7 @@ const MAX_TOOL_CALLS = 50;
|
|
|
5
5
|
import { callPathway, callTool, say } from '../../../lib/pathwayTools.js';
|
|
6
6
|
import logger from '../../../lib/logger.js';
|
|
7
7
|
import { config } from '../../../config.js';
|
|
8
|
-
import { chatArgsHasImageUrl, removeOldImageAndFileContent } from '../../../lib/util.js';
|
|
8
|
+
import { chatArgsHasImageUrl, removeOldImageAndFileContent, getAvailableFiles } from '../../../lib/util.js';
|
|
9
9
|
import { Prompt } from '../../../server/prompt.js';
|
|
10
10
|
import { getToolsForEntity, loadEntityConfig } from './tools/shared/sys_entity_tools.js';
|
|
11
11
|
|
|
@@ -275,7 +275,7 @@ export default {
|
|
|
275
275
|
const instructionTemplates = entityInstructions ? (entityInstructions + '\n\n') : `{{renderTemplate AI_COMMON_INSTRUCTIONS}}\n\n{{renderTemplate AI_EXPERTISE}}\n\n`;
|
|
276
276
|
|
|
277
277
|
const promptMessages = [
|
|
278
|
-
{"role": "system", "content": `${promptPrefix}${instructionTemplates}{{renderTemplate AI_TOOLS}}\n\n{{renderTemplate AI_SEARCH_RULES}}\n\n{{renderTemplate AI_SEARCH_SYNTAX}}\n\n{{renderTemplate AI_GROUNDING_INSTRUCTIONS}}\n\n${memoryTemplates}{{renderTemplate AI_DATETIME}}`},
|
|
278
|
+
{"role": "system", "content": `${promptPrefix}${instructionTemplates}{{renderTemplate AI_TOOLS}}\n\n{{renderTemplate AI_SEARCH_RULES}}\n\n{{renderTemplate AI_SEARCH_SYNTAX}}\n\n{{renderTemplate AI_GROUNDING_INSTRUCTIONS}}\n\n${memoryTemplates}{{renderTemplate AI_AVAILABLE_FILES}}\n\n{{renderTemplate AI_DATETIME}}`},
|
|
279
279
|
"{{chatHistory}}",
|
|
280
280
|
];
|
|
281
281
|
|
|
@@ -294,6 +294,8 @@ export default {
|
|
|
294
294
|
args.chatHistory = args.chatHistory.slice(-20);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
const availableFiles = getAvailableFiles(args.chatHistory);
|
|
298
|
+
|
|
297
299
|
// remove old image and file content
|
|
298
300
|
const visionContentPresent = chatArgsHasImageUrl(args);
|
|
299
301
|
visionContentPresent && (args.chatHistory = removeOldImageAndFileContent(args.chatHistory));
|
|
@@ -327,6 +329,7 @@ export default {
|
|
|
327
329
|
let response = await runAllPrompts({
|
|
328
330
|
...args,
|
|
329
331
|
chatHistory: currentMessages,
|
|
332
|
+
availableFiles,
|
|
330
333
|
tools: entityToolsOpenAiFormat,
|
|
331
334
|
tool_choice: memoryLookupRequired ? "required" : "auto"
|
|
332
335
|
});
|
|
@@ -41,13 +41,17 @@ export default {
|
|
|
41
41
|
icon: "🔄",
|
|
42
42
|
function: {
|
|
43
43
|
name: "ModifyImage",
|
|
44
|
-
description: "Use when asked to modify, transform, or edit an existing image. This tool can apply various transformations like style changes, artistic effects, or specific modifications to an image that has been previously uploaded or generated.",
|
|
44
|
+
description: "Use when asked to modify, transform, or edit an existing image. This tool can apply various transformations like style changes, artistic effects, or specific modifications to an image that has been previously uploaded or generated. It takes up to two input images as a reference and outputs a new image based on the instructions.",
|
|
45
45
|
parameters: {
|
|
46
46
|
type: "object",
|
|
47
47
|
properties: {
|
|
48
48
|
inputImage: {
|
|
49
49
|
type: "string",
|
|
50
|
-
description: "The
|
|
50
|
+
description: "The first image URL copied exactly from an image_url field in your chat context."
|
|
51
|
+
},
|
|
52
|
+
inputImage2: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "The second input image URL copied exactly from an image_url field in your chat context if there is one."
|
|
51
55
|
},
|
|
52
56
|
detailedInstructions: {
|
|
53
57
|
type: "string",
|
|
@@ -77,6 +81,11 @@ export default {
|
|
|
77
81
|
model = "replicate-flux-kontext-max";
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
// If we have two input images, use the multi-image-kontext-max model
|
|
85
|
+
if (args.inputImage2) {
|
|
86
|
+
model = "replicate-multi-image-kontext-max";
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
pathwayResolver.tool = JSON.stringify({ toolUsed: "image" });
|
|
81
90
|
return await callPathway('image_flux', {
|
|
82
91
|
...args,
|
|
@@ -85,7 +94,8 @@ export default {
|
|
|
85
94
|
numberResults,
|
|
86
95
|
model,
|
|
87
96
|
stream: false,
|
|
88
|
-
input_image: args.inputImage
|
|
97
|
+
input_image: args.inputImage,
|
|
98
|
+
input_image_2: args.inputImage2,
|
|
89
99
|
});
|
|
90
100
|
|
|
91
101
|
} catch (e) {
|
|
@@ -98,11 +98,39 @@ class ReplicateApiPlugin extends ModelPlugin {
|
|
|
98
98
|
'5:4', '3:4', '4:3', '9:16', '9:21', 'match_input_image'
|
|
99
99
|
];
|
|
100
100
|
|
|
101
|
+
let safetyTolerance = combinedParameters.safety_tolerance || 3;
|
|
102
|
+
if(combinedParameters.input_image){
|
|
103
|
+
safetyTolerance = Math.min(safetyTolerance, 2);
|
|
104
|
+
}
|
|
105
|
+
|
|
101
106
|
requestParameters = {
|
|
102
107
|
input: {
|
|
103
108
|
prompt: modelPromptText,
|
|
104
109
|
input_image: combinedParameters.input_image,
|
|
105
110
|
aspect_ratio: validRatios.includes(combinedParameters.aspectRatio) ? combinedParameters.aspectRatio : "1:1",
|
|
111
|
+
safety_tolerance: safetyTolerance,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "replicate-multi-image-kontext-max": {
|
|
117
|
+
const validRatios = [
|
|
118
|
+
'1:1', '16:9', '21:9', '3:2', '2:3', '4:5',
|
|
119
|
+
'5:4', '3:4', '4:3', '9:16', '9:21', 'match_input_image'
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
let safetyTolerance = combinedParameters.safety_tolerance || 3;
|
|
123
|
+
if(combinedParameters.input_image_1 || combinedParameters.input_image) {
|
|
124
|
+
safetyTolerance = Math.min(safetyTolerance, 2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
requestParameters = {
|
|
128
|
+
input: {
|
|
129
|
+
prompt: modelPromptText,
|
|
130
|
+
input_image_1: combinedParameters.input_image_1 || combinedParameters.input_image,
|
|
131
|
+
input_image_2: combinedParameters.input_image_2,
|
|
132
|
+
aspect_ratio: validRatios.includes(combinedParameters.aspectRatio) ? combinedParameters.aspectRatio : "1:1",
|
|
133
|
+
safety_tolerance: safetyTolerance,
|
|
106
134
|
},
|
|
107
135
|
};
|
|
108
136
|
break;
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
getFileStoreMap,
|
|
8
|
-
removeFromFileStoreMap,
|
|
9
|
-
setFileStoreMap,
|
|
10
|
-
} from './redis.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Retrieve a hash entry from Redis and ensure that the referenced files
|
|
14
|
-
* still exist in at least one configured storage provider. If one copy is
|
|
15
|
-
* missing it will try to restore it from the other provider (when possible).
|
|
16
|
-
*
|
|
17
|
-
* If the entry is completely invalid (no files found) it is removed from
|
|
18
|
-
* the store and `null` is returned.
|
|
19
|
-
*
|
|
20
|
-
* The function also updates the timestamp of the entry so that active hashes
|
|
21
|
-
* stay fresh in Redis.
|
|
22
|
-
*
|
|
23
|
-
* @param {object} context – Azure Function context for logging
|
|
24
|
-
* @param {string} hash – The hash / key in the FileStoreMap
|
|
25
|
-
* @param {StorageService} storageService – An initialised StorageService instance
|
|
26
|
-
* @returns {object|null} The (possibly refreshed) entry or null when invalid
|
|
27
|
-
*/
|
|
28
|
-
export async function getValidHashEntry(context, hash, storageService) {
|
|
29
|
-
if (!hash) return null;
|
|
30
|
-
|
|
31
|
-
let entry = await getFileStoreMap(hash);
|
|
32
|
-
if (!entry) return null;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const primaryExists = entry?.url ? await storageService.fileExists(entry.url) : false;
|
|
36
|
-
const gcsExists = entry?.gcs ? await storageService.fileExists(entry.gcs) : false;
|
|
37
|
-
|
|
38
|
-
// If neither storage has the file, remove the entry and abort
|
|
39
|
-
if (!primaryExists && !gcsExists) {
|
|
40
|
-
await removeFromFileStoreMap(hash);
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Restore missing GCS copy when primary exists
|
|
45
|
-
if (primaryExists && !gcsExists) {
|
|
46
|
-
try {
|
|
47
|
-
entry = await storageService.ensureGCSUpload(context, entry);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
context.log(`getValidHashEntry: failed to restore GCS copy – ${err}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Restore missing primary copy when GCS exists and a primary provider is configured
|
|
54
|
-
if (!primaryExists && gcsExists && storageService.backupProvider?.isConfigured()) {
|
|
55
|
-
let tempDir;
|
|
56
|
-
let downloadedFile;
|
|
57
|
-
try {
|
|
58
|
-
tempDir = path.join(os.tmpdir(), `${uuidv4()}`);
|
|
59
|
-
fs.mkdirSync(tempDir);
|
|
60
|
-
downloadedFile = path.join(tempDir, path.basename(entry.gcs));
|
|
61
|
-
|
|
62
|
-
// Download from GCS, then upload to primary storage
|
|
63
|
-
await storageService.downloadFile(entry.gcs, downloadedFile);
|
|
64
|
-
const res = await storageService.uploadFile(context, downloadedFile, hash);
|
|
65
|
-
entry.url = res.url;
|
|
66
|
-
} catch (err) {
|
|
67
|
-
context.log(`getValidHashEntry: failed to restore primary copy – ${err}`);
|
|
68
|
-
} finally {
|
|
69
|
-
// Clean temp artefacts
|
|
70
|
-
try {
|
|
71
|
-
if (downloadedFile && fs.existsSync(downloadedFile)) {
|
|
72
|
-
fs.unlinkSync(downloadedFile);
|
|
73
|
-
}
|
|
74
|
-
if (tempDir && fs.existsSync(tempDir)) {
|
|
75
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
76
|
-
}
|
|
77
|
-
} catch (_) {
|
|
78
|
-
/* noop */
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Update timestamp so the entry stays fresh
|
|
84
|
-
await setFileStoreMap(hash, entry);
|
|
85
|
-
return entry;
|
|
86
|
-
} catch (err) {
|
|
87
|
-
context.log(`getValidHashEntry: error during validation – ${err}`);
|
|
88
|
-
await removeFromFileStoreMap(hash);
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|