@aj-archipelago/cortex 1.3.66 → 1.3.67

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.
@@ -338,36 +338,58 @@ class PathwayManager {
338
338
 
339
339
  /**
340
340
  * Creates a Prompt object from a prompt item and system prompt.
341
- * @param {(string|Object)} promptItem - The prompt item (string or {name, prompt} object).
341
+ * @param {(string|Object)} promptItem - The prompt item (string or {name, prompt, files} object).
342
342
  * @param {string} systemPrompt - The system prompt to prepend.
343
343
  * @param {string} [defaultName] - Default name to use if promptItem is a string or if the object doesn't have a name.
344
344
  * @returns {Prompt} A new Prompt object.
345
345
  */
346
346
  _createPromptObject(promptItem, systemPrompt, defaultName = null) {
347
- // Handle both old format (strings) and new format (objects with name and prompt)
347
+ // Handle both old format (strings) and new format (objects with name, prompt, and files)
348
348
  const promptText = typeof promptItem === 'string' ? promptItem : promptItem.prompt;
349
349
  const promptName = typeof promptItem === 'string' ? defaultName : (promptItem.name || defaultName);
350
+ const promptFiles = typeof promptItem === 'string' ? [] : (promptItem.files || []);
351
+ const cortexPathwayName = typeof promptItem === 'string' ? null : (promptItem.cortexPathwayName || null);
350
352
 
351
- const messages = [
352
- // Add the original prompt as a user message
353
- { "role": "user", "content": `{{text}}\n\n${promptText}` },
354
- ];
353
+ const messages = [];
355
354
 
356
355
  // Only include system message if systemPrompt has content
357
356
  if (systemPrompt && systemPrompt.trim() !== "") {
358
357
  messages.unshift({ "role": "system", "content": systemPrompt });
359
358
  }
360
359
 
361
- return new Prompt({
360
+ // If there are files, include chatHistory to get the file content, then add user message
361
+ if (promptFiles.length > 0) {
362
+ // Add chatHistory which will contain the resolved file content
363
+ messages.push("{{chatHistory}}");
364
+ // Add the user text and prompt as a separate user message
365
+ messages.push({ "role": "user", "content": `{{text}}\n\n${promptText}` });
366
+ } else {
367
+ // No files, just add the user message with text and prompt
368
+ messages.push({ "role": "user", "content": `{{text}}\n\n${promptText}` });
369
+ }
370
+
371
+ const prompt = new Prompt({
362
372
  name: promptName,
363
373
  messages: messages
364
374
  });
375
+
376
+ // Store file hashes for later resolution
377
+ if (promptFiles.length > 0) {
378
+ prompt.fileHashes = promptFiles;
379
+ }
380
+
381
+ // Preserve cortexPathwayName if present
382
+ if (cortexPathwayName) {
383
+ prompt.cortexPathwayName = cortexPathwayName;
384
+ }
385
+
386
+ return prompt;
365
387
  }
366
388
 
367
389
  /**
368
390
  * Transforms the prompts in a pathway to include the system prompt.
369
391
  * @param {Object} pathway - The pathway object to transform.
370
- * @param {(string[]|Object[])} pathway.prompt - Array of user prompts (strings) or prompt objects with {name, prompt} properties.
392
+ * @param {(string[]|Object[])} pathway.prompt - Array of user prompts (strings) or prompt objects with {name, prompt, files} properties.
371
393
  * @param {string} pathway.systemPrompt - The system prompt to prepend to each user prompt.
372
394
  * @returns {Object} A new pathway object with transformed prompts.
373
395
  */
@@ -379,6 +401,16 @@ class PathwayManager {
379
401
  // Transform each prompt in the array
380
402
  newPathway.prompt = prompt.map(p => this._createPromptObject(p, systemPrompt));
381
403
 
404
+ // Collect all file hashes from all prompts
405
+ const allFileHashes = newPathway.prompt
406
+ .filter(p => p.fileHashes && p.fileHashes.length > 0)
407
+ .flatMap(p => p.fileHashes);
408
+
409
+ // Store file hashes at pathway level for later resolution
410
+ if (allFileHashes.length > 0) {
411
+ newPathway.fileHashes = [...new Set(allFileHashes)]; // Remove duplicates
412
+ }
413
+
382
414
  return newPathway;
383
415
  }
384
416
 
@@ -426,6 +458,8 @@ class PathwayManager {
426
458
  input PromptInput {
427
459
  name: String!
428
460
  prompt: String!
461
+ files: [String!]
462
+ cortexPathwayName: String
429
463
  }
430
464
 
431
465
  input PathwayInput {
package/lib/util.js CHANGED
@@ -412,6 +412,61 @@ function getAvailableFiles(chatHistory) {
412
412
  return availableFiles;
413
413
  }
414
414
 
415
+ /**
416
+ * Convert file hashes to content format suitable for LLM processing
417
+ * @param {Array<string>} fileHashes - Array of file hashes to resolve
418
+ * @param {Object} config - Configuration object with file service endpoints
419
+ * @returns {Promise<Array<string>>} Array of stringified file content objects
420
+ */
421
+ async function resolveFileHashesToContent(fileHashes, config) {
422
+ if (!fileHashes || fileHashes.length === 0) return [];
423
+
424
+ const fileContentPromises = fileHashes.map(async (hash) => {
425
+ try {
426
+ // Use the existing file handler (cortex-file-handler) to resolve file hashes
427
+ const fileHandlerUrl = config?.get?.('whisperMediaApiUrl');
428
+
429
+ if (fileHandlerUrl && fileHandlerUrl !== 'null') {
430
+ // Make request to file handler to get file content by hash
431
+ const response = await axios.get(fileHandlerUrl, {
432
+ params: { hash: hash, checkHash: true }
433
+ });
434
+ if (response.status === 200) {
435
+ const fileData = response.data;
436
+ const fileUrl = fileData.shortLivedUrl || fileData.url;
437
+ const convertedUrl = fileData.converted?.url;
438
+ const convertedGcsUrl = fileData.converted?.gcs;
439
+
440
+ return JSON.stringify({
441
+ type: "image_url",
442
+ url: convertedUrl,
443
+ image_url: { url: convertedUrl },
444
+ gcs: convertedGcsUrl || fileData.gcs, // Add GCS URL for Gemini models
445
+ originalFilename: fileData.filename,
446
+ hash: hash
447
+ });
448
+ }
449
+ }
450
+
451
+ // Fallback: create a placeholder that indicates file resolution is needed
452
+ return JSON.stringify({
453
+ type: "file_hash",
454
+ hash: hash,
455
+ _cortex_needs_resolution: true
456
+ });
457
+ } catch (error) {
458
+ // Return error indicator
459
+ return JSON.stringify({
460
+ type: "file_error",
461
+ hash: hash,
462
+ error: error.message
463
+ });
464
+ }
465
+ });
466
+
467
+ return Promise.all(fileContentPromises);
468
+ }
469
+
415
470
  export {
416
471
  getUniqueId,
417
472
  getSearchResultId,
@@ -426,5 +481,6 @@ export {
426
481
  getMediaChunks,
427
482
  markCompletedForCleanUp,
428
483
  removeOldImageAndFileContent,
429
- getAvailableFiles
484
+ getAvailableFiles,
485
+ resolveFileHashesToContent
430
486
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.3.66",
3
+ "version": "1.3.67",
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": {
@@ -79,9 +79,6 @@ export default {
79
79
  try {
80
80
  let currentMessages = JSON.parse(JSON.stringify(args.chatHistory));
81
81
 
82
- console.log("currentMessages", currentMessages);
83
- console.log("args", args);
84
-
85
82
  let response = await runAllPrompts({
86
83
  ...args,
87
84
  chatHistory: currentMessages,
@@ -0,0 +1,381 @@
1
+ // executeWorkspace.js
2
+ // Handles the executeWorkspace GraphQL query resolver and related functionality
3
+ //
4
+ // This module contains the implementation of the executeWorkspace resolver, which is responsible
5
+ // for executing user-defined pathways (workspaces) with various execution modes:
6
+ // - Sequential execution of all prompts (default)
7
+ // - Parallel execution of specific named prompts
8
+ // - Parallel execution of all prompts (wildcard mode)
9
+ //
10
+ // The resolver supports both legacy pathway formats and new dynamic pathways with cortexPathwayName.
11
+
12
+ import { v4 as uuidv4 } from 'uuid';
13
+ import logger from '../lib/logger.js';
14
+ import { callPathway } from '../lib/pathwayTools.js';
15
+ import { getPathwayTypeDef, userPathwayInputParameters } from './typeDef.js';
16
+
17
+ // Helper function to resolve file hashes and add them to chatHistory
18
+ const resolveAndAddFileContent = async (pathways, pathwayArgs, requestId, config) => {
19
+ let fileContentAdded = false;
20
+
21
+ // Check if any pathway has file hashes
22
+ const pathwaysWithFiles = Array.isArray(pathways) ? pathways : [pathways];
23
+
24
+ for (const pathway of pathwaysWithFiles) {
25
+ if (pathway.fileHashes && pathway.fileHashes.length > 0) {
26
+ try {
27
+ const { resolveFileHashesToContent } = await import('../lib/util.js');
28
+ const fileContent = await resolveFileHashesToContent(pathway.fileHashes, config);
29
+
30
+ // Add file content to chatHistory if not already present (only do this once)
31
+ if (!fileContentAdded) {
32
+ // Initialize chatHistory if it doesn't exist
33
+ if (!pathwayArgs.chatHistory) {
34
+ pathwayArgs.chatHistory = [];
35
+ }
36
+
37
+ // Find the last user message or create one
38
+ let lastUserMessage = null;
39
+ for (let i = pathwayArgs.chatHistory.length - 1; i >= 0; i--) {
40
+ if (pathwayArgs.chatHistory[i].role === 'user') {
41
+ lastUserMessage = pathwayArgs.chatHistory[i];
42
+ break;
43
+ }
44
+ }
45
+
46
+ if (!lastUserMessage) {
47
+ lastUserMessage = {
48
+ role: 'user',
49
+ content: []
50
+ };
51
+ pathwayArgs.chatHistory.push(lastUserMessage);
52
+ }
53
+
54
+ // Ensure content is an array
55
+ if (!Array.isArray(lastUserMessage.content)) {
56
+ lastUserMessage.content = [
57
+ JSON.stringify({
58
+ type: "text",
59
+ text: lastUserMessage.content || ""
60
+ })
61
+ ];
62
+ }
63
+
64
+ // Add file content
65
+ lastUserMessage.content.push(...fileContent);
66
+ fileContentAdded = true;
67
+ }
68
+ } catch (error) {
69
+ logger.error(`[${requestId}] Failed to resolve file hashes for pathway ${pathway.name || 'unnamed'}: ${error.message}`);
70
+ // Continue execution without files
71
+ }
72
+
73
+ // Only process files once for multiple pathways
74
+ if (fileContentAdded) break;
75
+ }
76
+ }
77
+
78
+ return fileContentAdded;
79
+ };
80
+
81
+ // Helper function to execute pathway with cortex pathway name or fallback to legacy
82
+ const executePathwayWithFallback = async (pathway, pathwayArgs, contextValue, info, requestId, originalPrompt = null, config) => {
83
+ const cortexPathwayName = (originalPrompt && typeof originalPrompt === 'object' && originalPrompt.cortexPathwayName)
84
+ ? originalPrompt.cortexPathwayName
85
+ : null;
86
+
87
+ if (cortexPathwayName) {
88
+ // Use the specific cortex pathway
89
+ // Transform parameters for cortex pathway
90
+ const cortexArgs = {
91
+ model: pathway.model || pathwayArgs.model || "labeeb-agent", // Use pathway model or default
92
+ chatHistory: [],
93
+ systemPrompt: pathway.systemPrompt
94
+ };
95
+
96
+ // If we have existing chatHistory, use it as base
97
+ if (pathwayArgs.chatHistory && pathwayArgs.chatHistory.length > 0) {
98
+ cortexArgs.chatHistory = JSON.parse(JSON.stringify(pathwayArgs.chatHistory));
99
+ }
100
+
101
+ // If we have text parameter, we need to add it to the chatHistory
102
+ if (pathwayArgs.text) {
103
+ // Find the last user message or create a new one
104
+ let lastUserMessage = null;
105
+ for (let i = cortexArgs.chatHistory.length - 1; i >= 0; i--) {
106
+ if (cortexArgs.chatHistory[i].role === 'user') {
107
+ lastUserMessage = cortexArgs.chatHistory[i];
108
+ break;
109
+ }
110
+ }
111
+
112
+ if (lastUserMessage) {
113
+ // Ensure content is an array
114
+ if (!Array.isArray(lastUserMessage.content)) {
115
+ lastUserMessage.content = [JSON.stringify({
116
+ type: "text",
117
+ text: lastUserMessage.content || ""
118
+ })];
119
+ }
120
+
121
+ // Add the text parameter as a text content item
122
+ const textFromPrompt = originalPrompt?.prompt || pathwayArgs.text;
123
+ lastUserMessage.content.unshift(JSON.stringify({
124
+ type: "text",
125
+ text: `${pathwayArgs.text}\n\n${textFromPrompt}`
126
+ }));
127
+ } else {
128
+ // Create new user message with text
129
+ const textFromPrompt = originalPrompt?.prompt || pathwayArgs.text;
130
+ cortexArgs.chatHistory.push({
131
+ role: 'user',
132
+ content: [JSON.stringify({
133
+ type: "text",
134
+ text: `${pathwayArgs.text}\n\n${textFromPrompt}`
135
+ })]
136
+ });
137
+ }
138
+ }
139
+
140
+ // Create a pathwayResolver to capture extended data like artifacts
141
+ const { PathwayResolver } = await import('./pathwayResolver.js');
142
+ const cortexPathway = config.get(`pathways.${cortexPathwayName}`);
143
+ if (!cortexPathway) {
144
+ throw new Error(`Cortex pathway ${cortexPathwayName} not found`);
145
+ }
146
+
147
+ const pathwayResolver = new PathwayResolver({
148
+ config,
149
+ pathway: cortexPathway,
150
+ args: cortexArgs
151
+ });
152
+
153
+ const result = await callPathway(cortexPathwayName, cortexArgs, pathwayResolver);
154
+
155
+ // Extract resultData from pathwayResolver (includes artifacts and other extended data)
156
+ const resultData = pathwayResolver.pathwayResultData
157
+ ? JSON.stringify(pathwayResolver.pathwayResultData)
158
+ : null;
159
+
160
+ // Return result with extended data
161
+ return {
162
+ result,
163
+ resultData,
164
+ warnings: pathwayResolver.warnings,
165
+ errors: pathwayResolver.errors
166
+ };
167
+ } else {
168
+ // Fallback to original pathway execution for legacy prompts
169
+ const pathwayContext = { ...contextValue, pathway };
170
+ return await pathway.rootResolver(null, pathwayArgs, pathwayContext, info);
171
+ }
172
+ };
173
+
174
+ // Main executeWorkspace resolver
175
+ export const executeWorkspaceResolver = async (_, args, contextValue, info, config, pathwayManager) => {
176
+ const startTime = Date.now();
177
+ const requestId = uuidv4();
178
+ const { userId, pathwayName, promptNames, ...pathwayArgs } = args;
179
+
180
+ logger.info(`>>> [${requestId}] executeWorkspace started - userId: ${userId}, pathwayName: ${pathwayName}, promptNames: ${promptNames?.join(',') || 'none'}`);
181
+
182
+ try {
183
+ contextValue.config = config;
184
+
185
+ // Get the base pathway from the user
186
+ const pathways = await pathwayManager.getLatestPathways();
187
+
188
+ if (!pathways[userId] || !pathways[userId][pathwayName]) {
189
+ const error = new Error(`Pathway '${pathwayName}' not found for user '${userId}'`);
190
+ logger.error(`!!! [${requestId}] ${error.message} - Available users: ${Object.keys(pathways).join(', ')}`);
191
+ throw error;
192
+ }
193
+
194
+ const basePathway = pathways[userId][pathwayName];
195
+
196
+ // If promptNames is specified, use getPathways to get individual pathways and execute in parallel
197
+ if (promptNames && promptNames.length > 0) {
198
+
199
+ // Check if the prompts are in legacy format (array of strings)
200
+ // If so, we can't use promptNames filtering and need to ask user to republish
201
+ if (pathwayManager.isLegacyPromptFormat(userId, pathwayName)) {
202
+ const error = new Error(
203
+ `The pathway '${pathwayName}' uses legacy prompt format (array of strings) which doesn't support the promptNames parameter. ` +
204
+ `Please unpublish and republish your workspace to upgrade to the new format that supports named prompts.`
205
+ );
206
+ logger.error(`!!! [${requestId}] ${error.message}`);
207
+ throw error;
208
+ }
209
+
210
+ // Handle wildcard case - execute all prompts in parallel
211
+ if (promptNames.includes('*')) {
212
+ logger.info(`[${requestId}] Executing all prompts in parallel (wildcard specified)`);
213
+ const individualPathways = await pathwayManager.getPathways(basePathway);
214
+
215
+ if (individualPathways.length === 0) {
216
+ const error = new Error(`No prompts found in pathway '${pathwayName}'`);
217
+ logger.error(`!!! [${requestId}] ${error.message}`);
218
+ throw error;
219
+ }
220
+
221
+ // Resolve file content for any pathways that have file hashes
222
+ await resolveAndAddFileContent(individualPathways, pathwayArgs, requestId, config);
223
+
224
+ // Execute all pathways in parallel
225
+ const results = await Promise.all(
226
+ individualPathways.map(async (pathway, index) => {
227
+ try {
228
+ // Check if the prompt has a cortexPathwayName (new format)
229
+ const originalPrompt = basePathway.prompt[index];
230
+
231
+ const result = await executePathwayWithFallback(pathway, pathwayArgs, contextValue, info, requestId, originalPrompt, config);
232
+
233
+ return {
234
+ result: result.result,
235
+ resultData: result.resultData,
236
+ warnings: result.warnings,
237
+ errors: result.errors,
238
+ promptName: pathway.name || `prompt_${index + 1}`
239
+ };
240
+ } catch (error) {
241
+ logger.error(`!!! [${requestId}] Error in pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'} - ${error.message}`);
242
+ throw error;
243
+ }
244
+ })
245
+ );
246
+
247
+ const duration = Date.now() - startTime;
248
+ logger.info(`<<< [${requestId}] executeWorkspace completed successfully in ${duration}ms - returned ${results.length} results`);
249
+
250
+ // Return a single result with JSON stringified array of results
251
+ return {
252
+ debug: `Executed ${results.length} prompts in parallel`,
253
+ result: JSON.stringify(results),
254
+ resultData: null,
255
+ previousResult: null,
256
+ warnings: [],
257
+ errors: [],
258
+ contextId: requestId,
259
+ tool: 'executeWorkspace'
260
+ };
261
+ } else {
262
+ // Handle specific prompt names
263
+ logger.info(`[${requestId}] Executing specific prompts: ${promptNames.join(', ')}`);
264
+ const individualPathways = await pathwayManager.getPathways(basePathway, promptNames);
265
+
266
+ if (individualPathways.length === 0) {
267
+ const error = new Error(`No prompts found matching the specified names: ${promptNames.join(', ')}`);
268
+ logger.error(`!!! [${requestId}] ${error.message}`);
269
+ throw error;
270
+ }
271
+
272
+ // Resolve file content for any pathways that have file hashes
273
+ await resolveAndAddFileContent(individualPathways, pathwayArgs, requestId, config);
274
+
275
+ // Execute all pathways in parallel
276
+ const results = await Promise.all(
277
+ individualPathways.map(async (pathway, index) => {
278
+ try {
279
+ // Find the original prompt by name to get the cortexPathwayName
280
+ const originalPrompt = basePathway.prompt.find(p =>
281
+ (typeof p === 'object' && p.name === pathway.name) ||
282
+ (typeof p === 'string' && pathway.name === `prompt_${basePathway.prompt.indexOf(p)}`)
283
+ );
284
+
285
+ const result = await executePathwayWithFallback(pathway, pathwayArgs, contextValue, info, requestId, originalPrompt, config);
286
+
287
+ return {
288
+ result: result.result,
289
+ resultData: result.resultData,
290
+ warnings: result.warnings,
291
+ errors: result.errors,
292
+ promptName: pathway.name || `prompt_${index + 1}`
293
+ };
294
+ } catch (error) {
295
+ logger.error(`!!! [${requestId}] Error in pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'} - ${error.message}`);
296
+ throw error;
297
+ }
298
+ })
299
+ );
300
+
301
+ const duration = Date.now() - startTime;
302
+ logger.info(`<<< [${requestId}] executeWorkspace completed successfully in ${duration}ms - returned ${results.length} results`);
303
+
304
+ // Return a single result with JSON stringified array of results (consistent with wildcard case)
305
+ return {
306
+ debug: `Executed ${results.length} specific prompts in parallel: ${promptNames.join(', ')}`,
307
+ result: JSON.stringify(results),
308
+ resultData: null,
309
+ previousResult: null,
310
+ warnings: [],
311
+ errors: [],
312
+ contextId: requestId,
313
+ tool: 'executeWorkspace'
314
+ };
315
+ }
316
+ }
317
+
318
+ // Default behavior: execute all prompts in sequence
319
+ logger.info(`[${requestId}] Executing prompts in sequence`);
320
+ const userPathway = await pathwayManager.getPathway(userId, pathwayName);
321
+ contextValue.pathway = userPathway;
322
+
323
+ // Handle file hashes if present in the pathway
324
+ await resolveAndAddFileContent(userPathway, pathwayArgs, requestId, config);
325
+
326
+ // Check if any prompt has cortexPathwayName (for dynamic pathways)
327
+ let result;
328
+ if (userPathway.prompt && Array.isArray(userPathway.prompt)) {
329
+ const firstPrompt = userPathway.prompt[0];
330
+
331
+ result = await executePathwayWithFallback(userPathway, pathwayArgs, contextValue, info, requestId, firstPrompt, config);
332
+ } else {
333
+ // No prompt array, use legacy execution
334
+ result = await userPathway.rootResolver(null, pathwayArgs, contextValue, info);
335
+ }
336
+ const duration = Date.now() - startTime;
337
+ logger.info(`<<< [${requestId}] executeWorkspace completed successfully in ${duration}ms - returned 1 result`);
338
+ return result; // Return single result directly
339
+
340
+ } catch (error) {
341
+ const duration = Date.now() - startTime;
342
+ logger.error(`!!! [${requestId}] executeWorkspace failed after ${duration}ms`);
343
+ logger.error(`!!! [${requestId}] Error type: ${error.constructor.name}`);
344
+ logger.error(`!!! [${requestId}] Error message: ${error.message}`);
345
+ logger.error(`!!! [${requestId}] Error stack: ${error.stack}`);
346
+
347
+ // Log additional context for debugging "memory access out of bounds" errors
348
+ if (error.message && error.message.includes('memory')) {
349
+ logger.error(`!!! [${requestId}] MEMORY ERROR DETECTED - Additional context:`);
350
+ logger.error(`!!! [${requestId}] - Node.js version: ${process.version}`);
351
+ logger.error(`!!! [${requestId}] - Memory usage: ${JSON.stringify(process.memoryUsage())}`);
352
+ logger.error(`!!! [${requestId}] - Args size estimate: ${JSON.stringify(args).length} chars`);
353
+ logger.error(`!!! [${requestId}] - PathwayArgs keys: ${Object.keys(pathwayArgs).join(', ')}`);
354
+ }
355
+
356
+ throw error;
357
+ }
358
+ };
359
+
360
+ // Type definitions for executeWorkspace
361
+ export const getExecuteWorkspaceTypeDefs = () => {
362
+ return `
363
+ ${getPathwayTypeDef('ExecuteWorkspace', 'String')}
364
+
365
+ type ExecuteWorkspaceResult {
366
+ debug: String
367
+ result: String
368
+ resultData: String
369
+ previousResult: String
370
+ warnings: [String]
371
+ errors: [String]
372
+ contextId: String
373
+ tool: String
374
+ }
375
+
376
+ extend type Query {
377
+ executeWorkspace(userId: String!, pathwayName: String!, ${userPathwayInputParameters}): ExecuteWorkspaceResult
378
+ }
379
+ `;
380
+ };
381
+