@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.
package/server/graphql.js CHANGED
@@ -15,7 +15,6 @@ import { WebSocketServer } from 'ws';
15
15
  import responseCachePlugin from '@apollo/server-plugin-response-cache';
16
16
  import { KeyvAdapter } from '@apollo/utils.keyvadapter';
17
17
  import cors from 'cors';
18
- import { v4 as uuidv4 } from 'uuid';
19
18
  import { buildModels, buildPathways } from '../config.js';
20
19
  import logger from '../lib/logger.js';
21
20
  import { buildModelEndpoints } from '../lib/requestExecutor.js';
@@ -23,8 +22,9 @@ import { startTestServer } from '../tests/helpers/server.js';
23
22
  import { requestState } from './requestState.js';
24
23
  import { cancelRequestResolver } from './resolver.js';
25
24
  import subscriptions from './subscriptions.js';
26
- import { getMessageTypeDefs, getPathwayTypeDef, userPathwayInputParameters } from './typeDef.js';
25
+ import { getMessageTypeDefs } from './typeDef.js';
27
26
  import { buildRestEndpoints } from './rest.js';
27
+ import { executeWorkspaceResolver, getExecuteWorkspaceTypeDefs } from './executeWorkspace.js';
28
28
 
29
29
  // Utility functions
30
30
  // Server plugins
@@ -74,22 +74,7 @@ const getTypedefs = (pathways, pathwayManager) => {
74
74
  cancelRequest(requestId: String!): Boolean
75
75
  }
76
76
 
77
- ${getPathwayTypeDef('ExecuteWorkspace', 'String')}
78
-
79
- type ExecuteWorkspaceResult {
80
- debug: String
81
- result: String
82
- resultData: String
83
- previousResult: String
84
- warnings: [String]
85
- errors: [String]
86
- contextId: String
87
- tool: String
88
- }
89
-
90
- extend type Query {
91
- executeWorkspace(userId: String!, pathwayName: String!, ${userPathwayInputParameters}): ExecuteWorkspaceResult
92
- }
77
+ ${getExecuteWorkspaceTypeDefs()}
93
78
 
94
79
  type RequestSubscription {
95
80
  requestId: String
@@ -130,171 +115,11 @@ const getResolvers = (config, pathways, pathwayManager) => {
130
115
 
131
116
  const pathwayManagerResolvers = pathwayManager?.getResolvers() || {};
132
117
 
133
- const executeWorkspaceResolver = async (_, args, contextValue, info) => {
134
- const startTime = Date.now();
135
- const requestId = uuidv4();
136
- const { userId, pathwayName, promptNames, ...pathwayArgs } = args;
137
-
138
- logger.info(`>>> [${requestId}] executeWorkspace started - userId: ${userId}, pathwayName: ${pathwayName}, promptNames: ${promptNames?.join(',') || 'none'}`);
139
-
140
- try {
141
- contextValue.config = config;
142
-
143
- // Get the base pathway from the user
144
- const pathways = await pathwayManager.getLatestPathways();
145
-
146
- if (!pathways[userId] || !pathways[userId][pathwayName]) {
147
- const error = new Error(`Pathway '${pathwayName}' not found for user '${userId}'`);
148
- logger.error(`!!! [${requestId}] ${error.message} - Available users: ${Object.keys(pathways).join(', ')}`);
149
- throw error;
150
- }
151
-
152
- const basePathway = pathways[userId][pathwayName];
153
- logger.debug(`[${requestId}] Found pathway: ${pathwayName} for user: ${userId}`);
154
-
155
- // If promptNames is specified, use getPathways to get individual pathways and execute in parallel
156
- if (promptNames && promptNames.length > 0) {
157
-
158
- // Check if the prompts are in legacy format (array of strings)
159
- // If so, we can't use promptNames filtering and need to ask user to republish
160
- if (pathwayManager.isLegacyPromptFormat(userId, pathwayName)) {
161
- const error = new Error(
162
- `The pathway '${pathwayName}' uses legacy prompt format (array of strings) which doesn't support the promptNames parameter. ` +
163
- `Please unpublish and republish your workspace to upgrade to the new format that supports named prompts.`
164
- );
165
- logger.error(`!!! [${requestId}] ${error.message}`);
166
- throw error;
167
- }
168
-
169
- // Handle wildcard case - execute all prompts in parallel
170
- if (promptNames.includes('*')) {
171
- logger.info(`[${requestId}] Executing all prompts in parallel (wildcard specified)`);
172
- const individualPathways = await pathwayManager.getPathways(basePathway);
173
-
174
- if (individualPathways.length === 0) {
175
- const error = new Error(`No prompts found in pathway '${pathwayName}'`);
176
- logger.error(`!!! [${requestId}] ${error.message}`);
177
- throw error;
178
- }
179
-
180
- // Execute all pathways in parallel
181
- logger.debug(`[${requestId}] Executing ${individualPathways.length} pathways in parallel`);
182
- const results = await Promise.all(
183
- individualPathways.map(async (pathway, index) => {
184
- try {
185
- logger.debug(`[${requestId}] Starting pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'}`);
186
- const pathwayContext = { ...contextValue, pathway };
187
- const result = await pathway.rootResolver(null, pathwayArgs, pathwayContext, info);
188
- logger.debug(`[${requestId}] Completed pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'}`);
189
- return {
190
- result: result.result,
191
- promptName: pathway.name || `prompt_${index + 1}`
192
- };
193
- } catch (error) {
194
- logger.error(`!!! [${requestId}] Error in pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'} - ${error.message}`);
195
- logger.debug(`[${requestId}] Error stack: ${error.stack}`);
196
- throw error;
197
- }
198
- })
199
- );
200
-
201
- const duration = Date.now() - startTime;
202
- logger.info(`<<< [${requestId}] executeWorkspace completed successfully in ${duration}ms - returned ${results.length} results`);
203
-
204
- // Return a single result with JSON stringified array of results
205
- return {
206
- debug: `Executed ${results.length} prompts in parallel`,
207
- result: JSON.stringify(results),
208
- resultData: null,
209
- previousResult: null,
210
- warnings: [],
211
- errors: [],
212
- contextId: requestId,
213
- tool: 'executeWorkspace'
214
- };
215
- } else {
216
- // Handle specific prompt names
217
- logger.info(`[${requestId}] Executing specific prompts: ${promptNames.join(', ')}`);
218
- const individualPathways = await pathwayManager.getPathways(basePathway, promptNames);
219
-
220
- if (individualPathways.length === 0) {
221
- const error = new Error(`No prompts found matching the specified names: ${promptNames.join(', ')}`);
222
- logger.error(`!!! [${requestId}] ${error.message}`);
223
- throw error;
224
- }
225
-
226
- // Execute all pathways in parallel
227
- logger.debug(`[${requestId}] Executing ${individualPathways.length} pathways in parallel`);
228
- const results = await Promise.all(
229
- individualPathways.map(async (pathway, index) => {
230
- try {
231
- logger.debug(`[${requestId}] Starting pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'}`);
232
- const pathwayContext = { ...contextValue, pathway };
233
- const result = await pathway.rootResolver(null, pathwayArgs, pathwayContext, info);
234
- logger.debug(`[${requestId}] Completed pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'}`);
235
- return {
236
- result: result.result,
237
- promptName: pathway.name || `prompt_${index + 1}`
238
- };
239
- } catch (error) {
240
- logger.error(`!!! [${requestId}] Error in pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'} - ${error.message}`);
241
- logger.debug(`[${requestId}] Error stack: ${error.stack}`);
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 (consistent with wildcard case)
251
- return {
252
- debug: `Executed ${results.length} specific prompts in parallel: ${promptNames.join(', ')}`,
253
- result: JSON.stringify(results),
254
- resultData: null,
255
- previousResult: null,
256
- warnings: [],
257
- errors: [],
258
- contextId: requestId,
259
- tool: 'executeWorkspace'
260
- };
261
- }
262
- }
263
-
264
- // Default behavior: execute all prompts in sequence
265
- logger.info(`[${requestId}] Executing prompts in sequence`);
266
- const userPathway = await pathwayManager.getPathway(userId, pathwayName);
267
- contextValue.pathway = userPathway;
268
-
269
- const result = await userPathway.rootResolver(null, pathwayArgs, contextValue, info);
270
- const duration = Date.now() - startTime;
271
- logger.info(`<<< [${requestId}] executeWorkspace completed successfully in ${duration}ms - returned 1 result`);
272
- return result; // Return single result directly
273
-
274
- } catch (error) {
275
- const duration = Date.now() - startTime;
276
- logger.error(`!!! [${requestId}] executeWorkspace failed after ${duration}ms`);
277
- logger.error(`!!! [${requestId}] Error type: ${error.constructor.name}`);
278
- logger.error(`!!! [${requestId}] Error message: ${error.message}`);
279
- logger.error(`!!! [${requestId}] Error stack: ${error.stack}`);
280
-
281
- // Log additional context for debugging "memory access out of bounds" errors
282
- if (error.message && error.message.includes('memory')) {
283
- logger.error(`!!! [${requestId}] MEMORY ERROR DETECTED - Additional context:`);
284
- logger.error(`!!! [${requestId}] - Node.js version: ${process.version}`);
285
- logger.error(`!!! [${requestId}] - Memory usage: ${JSON.stringify(process.memoryUsage())}`);
286
- logger.error(`!!! [${requestId}] - Args size estimate: ${JSON.stringify(args).length} chars`);
287
- logger.error(`!!! [${requestId}] - PathwayArgs keys: ${Object.keys(pathwayArgs).join(', ')}`);
288
- }
289
-
290
- throw error;
291
- }
292
- };
293
-
294
118
  const resolvers = {
295
119
  Query: {
296
120
  ...resolverFunctions,
297
- executeWorkspace: executeWorkspaceResolver
121
+ executeWorkspace: (parent, args, contextValue, info) =>
122
+ executeWorkspaceResolver(parent, args, contextValue, info, config, pathwayManager)
298
123
  },
299
124
  Mutation: {
300
125
  'cancelRequest': cancelRequestResolver,
@@ -90,7 +90,6 @@ test('parseJson should attempt to repair invalid JSON', async t => {
90
90
 
91
91
  const result = await parser.parseJson(invalidJson);
92
92
 
93
- console.log('parseJson result:', result); // For debugging
94
93
 
95
94
  t.not(result, null);
96
95
 
@@ -0,0 +1,256 @@
1
+ /**
2
+ * PathwayManager File Handling Tests
3
+ *
4
+ * This test suite validates the PathwayManager's file handling functionality, specifically
5
+ * testing how the manager processes and transforms pathway prompts that include file attachments.
6
+ *
7
+ * Key functionality tested:
8
+ * - File hash transformation from 'files' to 'fileHashes' property
9
+ * - Collection and deduplication of file hashes at the pathway level
10
+ * - Backward compatibility with legacy string-based prompts without file attachments
11
+ * - Handling of edge cases (null, undefined, empty file arrays)
12
+ * - Prompt object creation with file metadata
13
+ *
14
+ * The PathwayManager allows prompts to reference files by their hashes, which are then
15
+ * processed and made available to the execution context. This test suite ensures that
16
+ * file metadata is correctly preserved, transformed, and aggregated during pathway processing.
17
+ *
18
+ * Test scenarios covered:
19
+ * 1. Prompts with multiple file attachments
20
+ * 2. Prompts with empty or missing file arrays
21
+ * 3. Legacy string prompts (no file support)
22
+ * 4. Duplicate file hash deduplication
23
+ * 5. Null/undefined file handling
24
+ * 6. Direct prompt object creation with files
25
+ */
26
+
27
+ import test from 'ava';
28
+ import PathwayManager from '../../../lib/pathwayManager.js';
29
+
30
+ // Mock config for PathwayManager
31
+ const mockConfig = {
32
+ storageType: 'local',
33
+ filePath: './test-pathways.json',
34
+ publishKey: 'test-key'
35
+ };
36
+
37
+ // Mock base pathway
38
+ const mockBasePathway = {
39
+ name: 'base',
40
+ prompt: '{{text}}',
41
+ systemPrompt: '',
42
+ inputParameters: {},
43
+ typeDef: 'type Test { test: String }',
44
+ rootResolver: () => {},
45
+ resolver: () => {}
46
+ };
47
+
48
+ // Mock storage strategy
49
+ class MockStorageStrategy {
50
+ async load() {
51
+ return {};
52
+ }
53
+
54
+ async save(data) {
55
+ // Do nothing
56
+ }
57
+
58
+ async getLastModified() {
59
+ return Date.now();
60
+ }
61
+ }
62
+
63
+ test('pathwayManager handles prompt format with files correctly', async t => {
64
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
65
+
66
+ // Replace storage with mock
67
+ pathwayManager.storage = new MockStorageStrategy();
68
+
69
+ // Test prompts with files
70
+ const pathwayWithFiles = {
71
+ prompt: [
72
+ {
73
+ name: 'Analyze Document',
74
+ prompt: 'Please analyze the provided document',
75
+ files: ['abc123def456', 'def456ghi789']
76
+ },
77
+ {
78
+ name: 'Summarize Text',
79
+ prompt: 'Please summarize the text',
80
+ files: []
81
+ },
82
+ {
83
+ name: 'Simple Task',
84
+ prompt: 'Perform a simple task'
85
+ // No files property
86
+ }
87
+ ],
88
+ systemPrompt: 'You are a helpful assistant'
89
+ };
90
+
91
+ // Test transformPrompts method
92
+ const transformedPathway = await pathwayManager.transformPrompts(pathwayWithFiles);
93
+
94
+ // Verify the transformed pathway structure
95
+ t.truthy(transformedPathway);
96
+ t.true(Array.isArray(transformedPathway.prompt));
97
+ t.is(transformedPathway.prompt.length, 3);
98
+
99
+ // Verify first prompt with files
100
+ const firstPrompt = transformedPathway.prompt[0];
101
+ t.is(firstPrompt.name, 'Analyze Document');
102
+ t.truthy(firstPrompt.fileHashes);
103
+ t.deepEqual(firstPrompt.fileHashes, ['abc123def456', 'def456ghi789']);
104
+
105
+ // Verify second prompt with empty files
106
+ const secondPrompt = transformedPathway.prompt[1];
107
+ t.is(secondPrompt.name, 'Summarize Text');
108
+ t.falsy(secondPrompt.fileHashes); // Empty array results in no fileHashes property
109
+
110
+ // Verify third prompt without files property
111
+ const thirdPrompt = transformedPathway.prompt[2];
112
+ t.is(thirdPrompt.name, 'Simple Task');
113
+ t.falsy(thirdPrompt.fileHashes);
114
+
115
+ // Verify pathway-level file hashes collection
116
+ t.truthy(transformedPathway.fileHashes);
117
+ t.deepEqual(transformedPathway.fileHashes, ['abc123def456', 'def456ghi789']);
118
+ });
119
+
120
+ test('pathwayManager handles legacy string prompts correctly', async t => {
121
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
122
+
123
+ // Replace storage with mock
124
+ pathwayManager.storage = new MockStorageStrategy();
125
+
126
+ // Test legacy string prompts
127
+ const legacyPathway = {
128
+ prompt: [
129
+ 'Please analyze the data',
130
+ 'Summarize the findings'
131
+ ],
132
+ systemPrompt: 'You are a helpful assistant'
133
+ };
134
+
135
+ // Test transformPrompts method
136
+ const transformedPathway = await pathwayManager.transformPrompts(legacyPathway);
137
+
138
+ // Verify the transformed pathway structure
139
+ t.truthy(transformedPathway);
140
+ t.true(Array.isArray(transformedPathway.prompt));
141
+ t.is(transformedPathway.prompt.length, 2);
142
+
143
+ // Verify prompts don't have file hashes
144
+ transformedPathway.prompt.forEach(prompt => {
145
+ t.falsy(prompt.fileHashes);
146
+ });
147
+
148
+ // Verify no pathway-level file hashes
149
+ t.falsy(transformedPathway.fileHashes);
150
+ });
151
+
152
+ test('pathwayManager removes duplicate file hashes at pathway level', async t => {
153
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
154
+
155
+ // Replace storage with mock
156
+ pathwayManager.storage = new MockStorageStrategy();
157
+
158
+ // Test prompts with duplicate file hashes
159
+ const pathwayWithDuplicateFiles = {
160
+ prompt: [
161
+ {
162
+ name: 'First Task',
163
+ prompt: 'Analyze document 1',
164
+ files: ['abc123def456', 'def456ghi789']
165
+ },
166
+ {
167
+ name: 'Second Task',
168
+ prompt: 'Analyze document 2',
169
+ files: ['abc123def456', 'ghi789jkl012'] // abc123def456 is duplicate
170
+ }
171
+ ],
172
+ systemPrompt: 'You are a helpful assistant'
173
+ };
174
+
175
+ // Test transformPrompts method
176
+ const transformedPathway = await pathwayManager.transformPrompts(pathwayWithDuplicateFiles);
177
+
178
+ // Verify pathway-level file hashes are deduplicated
179
+ t.truthy(transformedPathway.fileHashes);
180
+ t.deepEqual(transformedPathway.fileHashes, ['abc123def456', 'def456ghi789', 'ghi789jkl012']);
181
+ t.is(transformedPathway.fileHashes.length, 3); // Should not have duplicates
182
+ });
183
+
184
+ test('pathwayManager handles null and undefined files gracefully', async t => {
185
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
186
+
187
+ // Replace storage with mock
188
+ pathwayManager.storage = new MockStorageStrategy();
189
+
190
+ // Test prompts with null/undefined files
191
+ const pathwayWithNullFiles = {
192
+ prompt: [
193
+ {
194
+ name: 'Task with null files',
195
+ prompt: 'Do something',
196
+ files: null
197
+ },
198
+ {
199
+ name: 'Task with undefined files',
200
+ prompt: 'Do something else'
201
+ // files property is undefined
202
+ }
203
+ ],
204
+ systemPrompt: 'You are a helpful assistant'
205
+ };
206
+
207
+ // Test transformPrompts method
208
+ const transformedPathway = await pathwayManager.transformPrompts(pathwayWithNullFiles);
209
+
210
+ // Verify the transformation handles null/undefined gracefully
211
+ t.truthy(transformedPathway);
212
+ t.true(Array.isArray(transformedPathway.prompt));
213
+
214
+ // Both prompts should have empty or undefined fileHashes
215
+ transformedPathway.prompt.forEach(prompt => {
216
+ t.true(!prompt.fileHashes || prompt.fileHashes.length === 0);
217
+ });
218
+
219
+ // No pathway-level file hashes should be set
220
+ t.falsy(transformedPathway.fileHashes);
221
+ });
222
+
223
+ test('pathwayManager _createPromptObject handles files correctly', t => {
224
+ const pathwayManager = new PathwayManager(mockConfig, mockBasePathway);
225
+
226
+ // Test with object prompt containing files
227
+ const promptWithFiles = {
228
+ name: 'Test Prompt',
229
+ prompt: 'Analyze this document',
230
+ files: ['file1hash', 'file2hash']
231
+ };
232
+
233
+ const createdPrompt = pathwayManager._createPromptObject(promptWithFiles, 'System prompt');
234
+
235
+ t.is(createdPrompt.name, 'Test Prompt');
236
+ t.truthy(createdPrompt.fileHashes);
237
+ t.deepEqual(createdPrompt.fileHashes, ['file1hash', 'file2hash']);
238
+
239
+ // Test with string prompt (no files)
240
+ const stringPrompt = 'Simple text prompt';
241
+ const createdStringPrompt = pathwayManager._createPromptObject(stringPrompt, 'System prompt', 'Default Name');
242
+
243
+ t.is(createdStringPrompt.name, 'Default Name');
244
+ t.falsy(createdStringPrompt.fileHashes);
245
+
246
+ // Test with object prompt without files
247
+ const promptWithoutFiles = {
248
+ name: 'No Files Prompt',
249
+ prompt: 'Simple task'
250
+ };
251
+
252
+ const createdPromptNoFiles = pathwayManager._createPromptObject(promptWithoutFiles, 'System prompt');
253
+
254
+ t.is(createdPromptNoFiles.name, 'No Files Prompt');
255
+ t.falsy(createdPromptNoFiles.fileHashes);
256
+ });