@aj-archipelago/cortex 1.3.63 → 1.3.64

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.
@@ -145,7 +145,7 @@ Privacy is critical. If asked to forget or delete something, always comply affir
145
145
  AI_DATETIME: "# Time, Date, and Time Zone\n\nThe current time and date in GMT is {{now}}, but references like \"today\" or \"yesterday\" are relative to the user's time zone. If you remember the user's time zone, use it - it's possible that the day for the user is different than the day in GMT.",
146
146
 
147
147
  AI_STYLE_OPENAI: "oai-gpt41",
148
- AI_STYLE_OPENAI_RESEARCH: "oai-o3",
148
+ AI_STYLE_OPENAI_RESEARCH: "oai-gpt5",
149
149
  AI_STYLE_ANTHROPIC: "claude-4-sonnet-vertex",
150
150
  AI_STYLE_ANTHROPIC_RESEARCH: "claude-41-opus-vertex",
151
151
  AI_STYLE_XAI: "xai-grok-4-fast-reasoning",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.3.63",
3
+ "version": "1.3.64",
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": {
@@ -34,7 +34,7 @@ const normalizeMemoryFormat = async (args, content) => {
34
34
  formattedContent = [...validLines, ...formattedBlock.split('\n')];
35
35
  }
36
36
  } catch (error) {
37
- logger.warn('Error formatting invalid memory lines:', error);
37
+ logger.warn(`Error formatting invalid memory lines: ${error?.message || 'Unknown error'}`);
38
38
  }
39
39
  }
40
40
 
@@ -123,7 +123,7 @@ export default {
123
123
  return "";
124
124
 
125
125
  } catch (e) {
126
- logger.warn('sys_memory_required returned invalid JSON:', memoryRequired);
126
+ logger.warn(`sys_memory_required returned invalid JSON: ${JSON.stringify(memoryRequired)}`);
127
127
  return "";
128
128
  }
129
129
 
@@ -43,9 +43,12 @@ export default {
43
43
  // Handle both CortexResponse objects and plain message objects
44
44
  let tool_calls;
45
45
  if (message instanceof CortexResponse) {
46
- tool_calls = message.toolCalls || message.functionCall ? [message.functionCall] : null;
46
+ tool_calls = [...(message.toolCalls || [])];
47
+ if (message.functionCall) {
48
+ tool_calls.push(message.functionCall);
49
+ }
47
50
  } else {
48
- tool_calls = message.tool_calls;
51
+ tool_calls = [...(message.tool_calls || [])];
49
52
  }
50
53
 
51
54
  const pathwayResolver = resolver;
@@ -56,10 +59,20 @@ export default {
56
59
  const preToolCallMessages = JSON.parse(JSON.stringify(args.chatHistory || []));
57
60
  const finalMessages = JSON.parse(JSON.stringify(preToolCallMessages));
58
61
 
59
- if (tool_calls) {
62
+ if (tool_calls && tool_calls.length > 0) {
60
63
  if (pathwayResolver.toolCallCount < MAX_TOOL_CALLS) {
61
64
  // Execute tool calls in parallel but with isolated message histories
62
- const toolResults = await Promise.all(tool_calls.map(async (toolCall) => {
65
+ // Filter out any undefined or invalid tool calls
66
+ const invalidToolCalls = tool_calls.filter(tc => !tc || !tc.function || !tc.function.name);
67
+ if (invalidToolCalls.length > 0) {
68
+ logger.warn(`Found ${invalidToolCalls.length} invalid tool calls: ${JSON.stringify(invalidToolCalls, null, 2)}`);
69
+ // bail out if we're getting invalid tool calls
70
+ pathwayResolver.toolCallCount = MAX_TOOL_CALLS;
71
+ }
72
+
73
+ const validToolCalls = tool_calls.filter(tc => tc && tc.function && tc.function.name);
74
+
75
+ const toolResults = await Promise.all(validToolCalls.map(async (toolCall) => {
63
76
  try {
64
77
  if (!toolCall?.function?.arguments) {
65
78
  throw new Error('Invalid tool call structure: missing function arguments');
@@ -192,7 +205,7 @@ export default {
192
205
  }
193
206
 
194
207
  // Check if any tool calls failed
195
- const failedTools = toolResults.filter(result => !result.success);
208
+ const failedTools = toolResults.filter(result => result && !result.success);
196
209
  if (failedTools.length > 0) {
197
210
  logger.warn(`Some tool calls failed: ${failedTools.map(t => t.error).join(', ')}`);
198
211
  }
@@ -342,7 +342,7 @@ Here are the information sources that were found:
342
342
  }
343
343
 
344
344
  if (!args.voiceResponse) {
345
- const referencedSources = extractReferencedSources(result);
345
+ const referencedSources = extractReferencedSources(result.toString());
346
346
  searchResults = searchResults.length ? pruneSearchResults(searchResults, referencedSources) : [];
347
347
  }
348
348
 
@@ -464,11 +464,13 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
464
464
  const pathwayResolver = requestState[this.requestId]?.pathwayResolver;
465
465
 
466
466
  if (finishReason === 'tool_calls' && this.toolCallsBuffer.length > 0 && this.pathwayToolCallback && pathwayResolver) {
467
+ // Filter out undefined elements from the tool calls buffer
468
+ const validToolCalls = this.toolCallsBuffer.filter(tc => tc && tc.function && tc.function.name);
467
469
  // Execute tool callback and keep stream open
468
470
  const toolMessage = {
469
471
  role: 'assistant',
470
472
  content: this.contentBuffer || '',
471
- tool_calls: this.toolCallsBuffer,
473
+ tool_calls: validToolCalls,
472
474
  };
473
475
  this.pathwayToolCallback(pathwayResolver?.args, toolMessage, pathwayResolver);
474
476
  // Clear tool buffer after processing; keep content for citations/continuations
@@ -280,10 +280,12 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
280
280
  case 'tool_calls':
281
281
  // Process complete tool calls when we get the finish reason
282
282
  if (this.pathwayToolCallback && this.toolCallsBuffer.length > 0 && pathwayResolver) {
283
+ // Filter out undefined elements from the tool calls buffer
284
+ const validToolCalls = this.toolCallsBuffer.filter(tc => tc && tc.function && tc.function.name);
283
285
  const toolMessage = {
284
286
  role: 'assistant',
285
287
  content: delta?.content || '',
286
- tool_calls: this.toolCallsBuffer,
288
+ tool_calls: validToolCalls,
287
289
  };
288
290
  this.pathwayToolCallback(pathwayResolver?.args, toolMessage, pathwayResolver);
289
291
  }
@@ -7,7 +7,7 @@ export async function connectToSSEEndpoint(baseUrl, endpoint, payload, onEvent)
7
7
  let sawDone = false;
8
8
  const timeout = setTimeout(() => {
9
9
  reject(new Error('SSE timeout waiting for [DONE]'));
10
- }, 8000); // 8 second timeout
10
+ }, 20000); // 20 second timeout
11
11
 
12
12
  try {
13
13
  const instance = axios.create({
@@ -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
+ });