@aj-archipelago/cortex 1.3.63 → 1.3.65

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.
@@ -0,0 +1,297 @@
1
+ import test from 'ava';
2
+ import OpenAIVisionPlugin from '../../../server/plugins/openAiVisionPlugin.js';
3
+ import Gemini15VisionPlugin from '../../../server/plugins/gemini15VisionPlugin.js';
4
+ import { PathwayResolver } from '../../../server/pathwayResolver.js';
5
+ import { config } from '../../../config.js';
6
+ import { requestState } from '../../../server/requestState.js';
7
+
8
+ // Mock logger to prevent issues in tests
9
+ const mockLogger = {
10
+ debug: () => {},
11
+ info: () => {},
12
+ warn: () => {},
13
+ error: () => {}
14
+ };
15
+
16
+ // Mock the logger module globally
17
+ global.logger = mockLogger;
18
+
19
+ function createResolverWithPlugin(pluginClass, modelName = 'test-model') {
20
+ const pluginToModelType = {
21
+ OpenAIVisionPlugin: 'OPENAI-VISION',
22
+ Gemini15VisionPlugin: 'GEMINI-1.5-VISION'
23
+ };
24
+
25
+ const modelType = pluginToModelType[pluginClass.name];
26
+ if (!modelType) {
27
+ throw new Error(`Unknown plugin class: ${pluginClass.name}`);
28
+ }
29
+
30
+ const pathway = {
31
+ name: 'test-pathway',
32
+ model: modelName,
33
+ prompt: 'test prompt',
34
+ toolCallback: () => {} // Mock tool callback
35
+ };
36
+
37
+ const model = {
38
+ name: modelName,
39
+ type: modelType
40
+ };
41
+
42
+ const resolver = new PathwayResolver({
43
+ config,
44
+ pathway,
45
+ args: {},
46
+ endpoints: { [modelName]: model }
47
+ });
48
+
49
+ resolver.modelExecutor.plugin = new pluginClass(pathway, model);
50
+ return resolver;
51
+ }
52
+
53
+ test('OpenAIVisionPlugin - filters undefined tool calls from buffer', async t => {
54
+ const resolver = createResolverWithPlugin(OpenAIVisionPlugin);
55
+ const plugin = resolver.modelExecutor.plugin;
56
+
57
+ // Simulate the scenario where tool calls start at index 1, leaving index 0 undefined
58
+ plugin.toolCallsBuffer[0] = undefined; // This is what causes the issue
59
+ plugin.toolCallsBuffer[1] = {
60
+ id: 'call_1_1234567890',
61
+ type: 'function',
62
+ function: {
63
+ name: 'test_tool',
64
+ arguments: '{"param": "value"}'
65
+ }
66
+ };
67
+
68
+ // Mock the tool callback to capture what gets passed
69
+ let capturedToolCalls = null;
70
+ plugin.pathwayToolCallback = (args, message, resolver) => {
71
+ capturedToolCalls = message.tool_calls;
72
+ };
73
+
74
+ // Mock requestProgress and pathwayResolver
75
+ const requestProgress = { progress: 0, started: true };
76
+ const pathwayResolver = { args: {} };
77
+
78
+ // Mock requestState to return our resolver
79
+ requestState[plugin.requestId] = { pathwayResolver };
80
+
81
+ // Simulate a tool_calls finish reason
82
+ const event = {
83
+ data: JSON.stringify({
84
+ choices: [{
85
+ finish_reason: 'tool_calls'
86
+ }]
87
+ })
88
+ };
89
+
90
+ // Process the stream event
91
+ plugin.processStreamEvent(event, requestProgress);
92
+
93
+ // Verify that undefined elements were filtered out
94
+ t.truthy(capturedToolCalls, 'Tool callback should have been called');
95
+ t.is(capturedToolCalls.length, 1, 'Should have filtered out undefined elements');
96
+ t.is(capturedToolCalls[0].function.name, 'test_tool', 'Valid tool call should be preserved');
97
+
98
+ // Clean up
99
+ delete requestState[plugin.requestId];
100
+ });
101
+
102
+ test('OpenAIVisionPlugin - handles empty buffer gracefully', async t => {
103
+ const resolver = createResolverWithPlugin(OpenAIVisionPlugin);
104
+ const plugin = resolver.modelExecutor.plugin;
105
+
106
+ // Empty buffer
107
+ plugin.toolCallsBuffer = [];
108
+
109
+ // Mock the tool callback
110
+ let callbackCalled = false;
111
+ plugin.pathwayToolCallback = () => {
112
+ callbackCalled = true;
113
+ };
114
+
115
+ // Mock requestProgress and pathwayResolver
116
+ const requestProgress = { progress: 0, started: true };
117
+ const pathwayResolver = { args: {} };
118
+
119
+ // Mock requestState
120
+ requestState[plugin.requestId] = { pathwayResolver };
121
+
122
+ // Simulate a tool_calls finish reason
123
+ const event = {
124
+ data: JSON.stringify({
125
+ choices: [{
126
+ finish_reason: 'tool_calls'
127
+ }]
128
+ })
129
+ };
130
+
131
+ // Process the stream event
132
+ plugin.processStreamEvent(event, requestProgress);
133
+
134
+ // Verify that callback was not called with empty buffer
135
+ t.falsy(callbackCalled, 'Tool callback should not be called with empty buffer');
136
+
137
+ // Clean up
138
+ delete requestState[plugin.requestId];
139
+ });
140
+
141
+ test('OpenAIVisionPlugin - filters invalid tool calls', async t => {
142
+ const resolver = createResolverWithPlugin(OpenAIVisionPlugin);
143
+ const plugin = resolver.modelExecutor.plugin;
144
+
145
+ // Create buffer with mixed valid and invalid tool calls
146
+ plugin.toolCallsBuffer[0] = undefined;
147
+ plugin.toolCallsBuffer[1] = {
148
+ id: 'call_1_1234567890',
149
+ type: 'function',
150
+ function: {
151
+ name: 'valid_tool',
152
+ arguments: '{"param": "value"}'
153
+ }
154
+ };
155
+ plugin.toolCallsBuffer[2] = {
156
+ id: 'call_2_1234567890',
157
+ type: 'function',
158
+ function: {
159
+ name: '', // Invalid: empty name
160
+ arguments: '{"param": "value"}'
161
+ }
162
+ };
163
+ plugin.toolCallsBuffer[3] = {
164
+ id: 'call_3_1234567890',
165
+ type: 'function',
166
+ function: {
167
+ name: 'another_valid_tool',
168
+ arguments: '{"param": "value"}'
169
+ }
170
+ };
171
+
172
+ // Mock the tool callback
173
+ let capturedToolCalls = null;
174
+ plugin.pathwayToolCallback = (args, message, resolver) => {
175
+ capturedToolCalls = message.tool_calls;
176
+ };
177
+
178
+ // Mock requestProgress and pathwayResolver
179
+ const requestProgress = { progress: 0, started: true };
180
+ const pathwayResolver = { args: {} };
181
+
182
+ // Mock requestState
183
+ requestState[plugin.requestId] = { pathwayResolver };
184
+
185
+ // Simulate a tool_calls finish reason
186
+ const event = {
187
+ data: JSON.stringify({
188
+ choices: [{
189
+ finish_reason: 'tool_calls'
190
+ }]
191
+ })
192
+ };
193
+
194
+ // Process the stream event
195
+ plugin.processStreamEvent(event, requestProgress);
196
+
197
+ // Verify that only valid tool calls were passed
198
+ t.truthy(capturedToolCalls, 'Tool callback should have been called');
199
+ t.is(capturedToolCalls.length, 2, 'Should have filtered out invalid elements');
200
+ t.is(capturedToolCalls[0].function.name, 'valid_tool', 'First valid tool call should be preserved');
201
+ t.is(capturedToolCalls[1].function.name, 'another_valid_tool', 'Second valid tool call should be preserved');
202
+
203
+ // Clean up
204
+ delete requestState[plugin.requestId];
205
+ });
206
+
207
+ test('Gemini15VisionPlugin - filters undefined tool calls from buffer', async t => {
208
+ const resolver = createResolverWithPlugin(Gemini15VisionPlugin);
209
+ const plugin = resolver.modelExecutor.plugin;
210
+
211
+ // Simulate buffer with undefined elements (though less likely with push method)
212
+ plugin.toolCallsBuffer = [
213
+ undefined,
214
+ {
215
+ id: 'call_1_1234567890',
216
+ type: 'function',
217
+ function: {
218
+ name: 'test_tool',
219
+ arguments: '{"param": "value"}'
220
+ }
221
+ }
222
+ ];
223
+
224
+ // Mock the tool callback
225
+ let capturedToolCalls = null;
226
+ plugin.pathwayToolCallback = (args, message, resolver) => {
227
+ capturedToolCalls = message.tool_calls;
228
+ };
229
+
230
+ // Mock requestProgress and pathwayResolver
231
+ const requestProgress = { progress: 0, started: true };
232
+ const pathwayResolver = { args: {} };
233
+
234
+ // Mock requestState
235
+ requestState[plugin.requestId] = { pathwayResolver };
236
+
237
+ // Simulate a tool_calls finish reason
238
+ const eventData = {
239
+ candidates: [{
240
+ finishReason: 'STOP'
241
+ }]
242
+ };
243
+
244
+ // Set hadToolCalls to true to trigger tool_calls finish reason
245
+ plugin.hadToolCalls = true;
246
+
247
+ // Process the stream event
248
+ plugin.processStreamEvent({ data: JSON.stringify(eventData) }, requestProgress);
249
+
250
+ // Verify that undefined elements were filtered out
251
+ t.truthy(capturedToolCalls, 'Tool callback should have been called');
252
+ t.is(capturedToolCalls.length, 1, 'Should have filtered out undefined elements');
253
+ t.is(capturedToolCalls[0].function.name, 'test_tool', 'Valid tool call should be preserved');
254
+
255
+ // Clean up
256
+ delete requestState[plugin.requestId];
257
+ });
258
+
259
+ test('Gemini15VisionPlugin - handles empty buffer gracefully', async t => {
260
+ const resolver = createResolverWithPlugin(Gemini15VisionPlugin);
261
+ const plugin = resolver.modelExecutor.plugin;
262
+
263
+ // Empty buffer
264
+ plugin.toolCallsBuffer = [];
265
+
266
+ // Mock the tool callback
267
+ let callbackCalled = false;
268
+ plugin.pathwayToolCallback = () => {
269
+ callbackCalled = true;
270
+ };
271
+
272
+ // Mock requestProgress and pathwayResolver
273
+ const requestProgress = { progress: 0, started: true };
274
+ const pathwayResolver = { args: {} };
275
+
276
+ // Mock requestState
277
+ requestState[plugin.requestId] = { pathwayResolver };
278
+
279
+ // Simulate a tool_calls finish reason
280
+ const eventData = {
281
+ candidates: [{
282
+ finishReason: 'STOP'
283
+ }]
284
+ };
285
+
286
+ // Set hadToolCalls to true
287
+ plugin.hadToolCalls = true;
288
+
289
+ // Process the stream event
290
+ plugin.processStreamEvent({ data: JSON.stringify(eventData) }, requestProgress);
291
+
292
+ // Verify that callback was not called with empty buffer
293
+ t.falsy(callbackCalled, 'Tool callback should not be called with empty buffer');
294
+
295
+ // Clean up
296
+ delete requestState[plugin.requestId];
297
+ });
@@ -0,0 +1,170 @@
1
+ import test from 'ava';
2
+ import sinon from 'sinon';
3
+ import { getResolvers } from '../../../server/graphql.js';
4
+
5
+ // Mock logger to avoid actual logging during tests
6
+ const mockLogger = {
7
+ info: sinon.stub(),
8
+ debug: sinon.stub(),
9
+ error: sinon.stub(),
10
+ warn: sinon.stub()
11
+ };
12
+
13
+ // Mock config
14
+ const mockConfig = {
15
+ get: sinon.stub().returns('test-value')
16
+ };
17
+
18
+ test.beforeEach(t => {
19
+ // Reset stubs before each test
20
+ sinon.restore();
21
+ mockLogger.info.resetHistory();
22
+ mockLogger.debug.resetHistory();
23
+ mockLogger.error.resetHistory();
24
+ mockLogger.warn.resetHistory();
25
+ });
26
+
27
+ test('executeWorkspace throws error for legacy format with promptNames', async t => {
28
+ // Mock pathwayManager
29
+ const mockPathwayManager = {
30
+ getLatestPathways: sinon.stub().resolves({
31
+ 'test-user': {
32
+ 'test-pathway': {
33
+ prompt: ['legacy string prompt 1', 'legacy string prompt 2'], // Legacy format
34
+ systemPrompt: 'Test system prompt'
35
+ }
36
+ }
37
+ }),
38
+ isLegacyPromptFormat: sinon.stub().returns(true), // Mock returns true for legacy format
39
+ getResolvers: sinon.stub().returns({ Mutation: {} }) // Mock getResolvers method
40
+ };
41
+
42
+ // Get the resolvers function (need to mock the import first)
43
+ const resolvers = getResolvers(mockConfig, {}, mockPathwayManager);
44
+ const executeWorkspaceResolver = resolvers.Query.executeWorkspace;
45
+
46
+ // Mock GraphQL context and info
47
+ const mockContextValue = { config: mockConfig };
48
+ const mockInfo = {};
49
+
50
+ // Test arguments - userId, pathwayName, promptNames are the key ones
51
+ const args = {
52
+ userId: 'test-user',
53
+ pathwayName: 'test-pathway',
54
+ promptNames: ['specific-prompt'], // This triggers the check
55
+ text: 'test input'
56
+ };
57
+
58
+ // Execute the resolver and expect it to throw
59
+ const error = await t.throwsAsync(async () => {
60
+ await executeWorkspaceResolver(null, args, mockContextValue, mockInfo);
61
+ });
62
+
63
+ // Verify the error message
64
+ t.truthy(error);
65
+ t.true(error.message.includes('legacy prompt format'));
66
+ t.true(error.message.includes('unpublish and republish'));
67
+ t.true(error.message.includes('promptNames parameter'));
68
+ t.true(error.message.includes('test-pathway')); // Should include the pathway name
69
+
70
+ // Verify that the pathwayManager methods were called correctly
71
+ t.true(mockPathwayManager.getLatestPathways.calledOnce);
72
+ t.true(mockPathwayManager.isLegacyPromptFormat.calledOnce);
73
+ t.true(mockPathwayManager.isLegacyPromptFormat.calledWith('test-user', 'test-pathway'));
74
+ });
75
+
76
+ test('executeWorkspace does not throw for new format with promptNames', async t => {
77
+ // Mock pathwayManager with new format
78
+ const mockPathwayManager = {
79
+ getLatestPathways: sinon.stub().resolves({
80
+ 'test-user': {
81
+ 'test-pathway': {
82
+ prompt: [
83
+ { name: 'Prompt 1', prompt: 'New format prompt 1' },
84
+ { name: 'Prompt 2', prompt: 'New format prompt 2' }
85
+ ], // New format
86
+ systemPrompt: 'Test system prompt'
87
+ }
88
+ }
89
+ }),
90
+ isLegacyPromptFormat: sinon.stub().returns(false), // Mock returns false for new format
91
+ getPathways: sinon.stub().resolves([
92
+ {
93
+ name: 'specific-prompt',
94
+ prompt: [/* mock prompt object */],
95
+ rootResolver: sinon.stub().resolves({ result: 'test result' })
96
+ }
97
+ ]),
98
+ getResolvers: sinon.stub().returns({ Mutation: {} }) // Mock getResolvers method
99
+ };
100
+
101
+ const resolvers = getResolvers(mockConfig, {}, mockPathwayManager);
102
+ const executeWorkspaceResolver = resolvers.Query.executeWorkspace;
103
+
104
+ const mockContextValue = { config: mockConfig };
105
+ const mockInfo = {};
106
+
107
+ const args = {
108
+ userId: 'test-user',
109
+ pathwayName: 'test-pathway',
110
+ promptNames: ['specific-prompt'],
111
+ text: 'test input'
112
+ };
113
+
114
+ // This should not throw an error for new format
115
+ const result = await executeWorkspaceResolver(null, args, mockContextValue, mockInfo);
116
+
117
+ // Should return results without error
118
+ t.truthy(result);
119
+ t.is(typeof result, 'object');
120
+ t.false(Array.isArray(result));
121
+
122
+ // Verify that the pathwayManager methods were called correctly
123
+ t.true(mockPathwayManager.getLatestPathways.calledOnce);
124
+ t.true(mockPathwayManager.isLegacyPromptFormat.calledOnce);
125
+ t.true(mockPathwayManager.getPathways.calledOnce);
126
+ });
127
+
128
+ test('executeWorkspace does not check format when promptNames not provided', async t => {
129
+ // Mock pathwayManager with legacy format
130
+ const mockPathwayManager = {
131
+ getLatestPathways: sinon.stub().resolves({
132
+ 'test-user': {
133
+ 'test-pathway': {
134
+ prompt: ['legacy string prompt 1', 'legacy string prompt 2'], // Legacy format
135
+ systemPrompt: 'Test system prompt'
136
+ }
137
+ }
138
+ }),
139
+ isLegacyPromptFormat: sinon.stub(), // Should not be called
140
+ getPathway: sinon.stub().resolves({
141
+ rootResolver: sinon.stub().resolves({ result: 'test result' })
142
+ }),
143
+ getResolvers: sinon.stub().returns({ Mutation: {} }) // Mock getResolvers method
144
+ };
145
+
146
+ const resolvers = getResolvers(mockConfig, {}, mockPathwayManager);
147
+ const executeWorkspaceResolver = resolvers.Query.executeWorkspace;
148
+
149
+ const mockContextValue = { config: mockConfig };
150
+ const mockInfo = {};
151
+
152
+ const args = {
153
+ userId: 'test-user',
154
+ pathwayName: 'test-pathway',
155
+ // No promptNames provided - should use default behavior
156
+ text: 'test input'
157
+ };
158
+
159
+ // This should not throw an error even with legacy format when promptNames not provided
160
+ const result = await executeWorkspaceResolver(null, args, mockContextValue, mockInfo);
161
+
162
+ // Should return results without error
163
+ t.truthy(result);
164
+ t.is(typeof result, 'object');
165
+ t.false(Array.isArray(result));
166
+
167
+ // Verify that isLegacyPromptFormat was NOT called since promptNames wasn't provided
168
+ t.false(mockPathwayManager.isLegacyPromptFormat.called);
169
+ t.true(mockPathwayManager.getPathway.calledOnce);
170
+ });