@aj-archipelago/cortex 1.3.35 → 1.3.36

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.
Files changed (47) hide show
  1. package/README.md +9 -9
  2. package/config/default.example.json +0 -20
  3. package/config.js +160 -6
  4. package/lib/pathwayTools.js +79 -1
  5. package/lib/requestExecutor.js +3 -1
  6. package/lib/util.js +7 -0
  7. package/package.json +1 -1
  8. package/pathways/basePathway.js +2 -0
  9. package/pathways/call_tools.js +379 -0
  10. package/pathways/system/entity/memory/shared/sys_memory_helpers.js +1 -1
  11. package/pathways/system/entity/memory/sys_search_memory.js +2 -2
  12. package/pathways/system/entity/sys_entity_agent.js +289 -0
  13. package/pathways/system/entity/sys_generator_memory.js +1 -1
  14. package/pathways/system/entity/sys_generator_results.js +1 -1
  15. package/pathways/system/entity/sys_get_entities.js +19 -0
  16. package/pathways/system/entity/tools/shared/sys_entity_tools.js +150 -0
  17. package/pathways/system/entity/tools/sys_tool_bing_search.js +147 -0
  18. package/pathways/system/entity/tools/sys_tool_callmodel.js +62 -0
  19. package/pathways/system/entity/tools/sys_tool_coding.js +53 -0
  20. package/pathways/system/entity/tools/sys_tool_codingagent.js +100 -0
  21. package/pathways/system/entity/tools/sys_tool_cognitive_search.js +231 -0
  22. package/pathways/system/entity/tools/sys_tool_image.js +57 -0
  23. package/pathways/system/entity/tools/sys_tool_readfile.js +119 -0
  24. package/pathways/system/entity/tools/sys_tool_reasoning.js +75 -0
  25. package/pathways/system/entity/tools/sys_tool_remember.js +59 -0
  26. package/pathways/vision.js +1 -1
  27. package/server/modelExecutor.js +4 -12
  28. package/server/pathwayResolver.js +53 -40
  29. package/server/plugins/azureBingPlugin.js +42 -4
  30. package/server/plugins/azureCognitivePlugin.js +40 -12
  31. package/server/plugins/claude3VertexPlugin.js +67 -18
  32. package/server/plugins/modelPlugin.js +3 -2
  33. package/server/plugins/openAiReasoningPlugin.js +3 -3
  34. package/server/plugins/openAiReasoningVisionPlugin.js +48 -0
  35. package/server/plugins/openAiVisionPlugin.js +192 -7
  36. package/tests/agentic.test.js +256 -0
  37. package/tests/call_tools.test.js +216 -0
  38. package/tests/claude3VertexToolConversion.test.js +78 -0
  39. package/tests/mocks.js +11 -3
  40. package/tests/multimodal_conversion.test.js +1 -1
  41. package/tests/openAiToolPlugin.test.js +242 -0
  42. package/pathways/test_palm_chat.js +0 -31
  43. package/server/plugins/palmChatPlugin.js +0 -233
  44. package/server/plugins/palmCodeCompletionPlugin.js +0 -45
  45. package/server/plugins/palmCompletionPlugin.js +0 -135
  46. package/tests/palmChatPlugin.test.js +0 -219
  47. package/tests/palmCompletionPlugin.test.js +0 -58
@@ -0,0 +1,48 @@
1
+ import OpenAIVisionPlugin from './openAiVisionPlugin.js';
2
+
3
+ class OpenAIReasoningVisionPlugin extends OpenAIVisionPlugin {
4
+
5
+ async tryParseMessages(messages) {
6
+ const parsedMessages = await super.tryParseMessages(messages);
7
+
8
+ let newMessages = [];
9
+
10
+ newMessages = parsedMessages.map(message => ({
11
+ role: message.role === 'system' ? 'developer' : message.role,
12
+ content: message.content
13
+ })).filter(message => ['user', 'assistant', 'developer', 'tool'].includes(message.role));
14
+
15
+ return newMessages;
16
+ }
17
+
18
+ async getRequestParameters(text, parameters, prompt) {
19
+ const requestParameters = await super.getRequestParameters(text, parameters, prompt);
20
+
21
+ const modelMaxReturnTokens = this.getModelMaxReturnTokens();
22
+ const maxTokensPrompt = this.promptParameters.max_tokens;
23
+ const maxTokensModel = this.getModelMaxTokenLength() * (1 - this.getPromptTokenRatio());
24
+
25
+ const maxTokens = maxTokensPrompt || maxTokensModel;
26
+
27
+ delete requestParameters.max_tokens;
28
+ requestParameters.max_completion_tokens = maxTokens ? Math.min(maxTokens, modelMaxReturnTokens) : modelMaxReturnTokens;
29
+ requestParameters.temperature = 1;
30
+
31
+ if (this.promptParameters.reasoningEffort) {
32
+ const effort = this.promptParameters.reasoningEffort.toLowerCase();
33
+ if (['high', 'medium', 'low'].includes(effort)) {
34
+ requestParameters.reasoning_effort = effort;
35
+ } else {
36
+ requestParameters.reasoning_effort = 'low';
37
+ }
38
+ }
39
+
40
+ if (this.promptParameters.responseFormat) {
41
+ requestParameters.response_format = this.promptParameters.responseFormat;
42
+ }
43
+
44
+ return requestParameters;
45
+ }
46
+ }
47
+
48
+ export default OpenAIReasoningVisionPlugin;
@@ -1,5 +1,6 @@
1
1
  import OpenAIChatPlugin from './openAiChatPlugin.js';
2
2
  import logger from '../../lib/logger.js';
3
+ import { requestState } from '../requestState.js';
3
4
 
4
5
  function safeJsonParse(content) {
5
6
  try {
@@ -15,14 +16,31 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
15
16
  constructor(pathway, model) {
16
17
  super(pathway, model);
17
18
  this.isMultiModal = true;
19
+ this.pathwayToolCallback = pathway.toolCallback;
20
+ this.toolCallsBuffer = [];
21
+ this.contentBuffer = ''; // Initialize content buffer
18
22
  }
19
23
 
20
24
  async tryParseMessages(messages) {
21
25
  return await Promise.all(messages.map(async message => {
22
26
  try {
27
+ // Handle tool-related message types
23
28
  if (message.role === "tool") {
24
- return message;
29
+ return {
30
+ role: message.role,
31
+ content: message.content,
32
+ tool_call_id: message.tool_call_id
33
+ };
25
34
  }
35
+
36
+ if (message.role === "assistant" && message.tool_calls) {
37
+ return {
38
+ role: message.role,
39
+ content: message.content,
40
+ tool_calls: message.tool_calls
41
+ };
42
+ }
43
+
26
44
  if (Array.isArray(message.content)) {
27
45
  message.content = await Promise.all(message.content.map(async item => {
28
46
  const parsedItem = safeJsonParse(item);
@@ -36,7 +54,7 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
36
54
  if (url && await this.validateImageUrl(url)) {
37
55
  return {type: parsedItem.type, image_url: {url}};
38
56
  }
39
- return { type: 'text', text: 'Image skipped: unsupported format' };
57
+ return { type: 'text', text: typeof item === 'string' ? item : JSON.stringify(item) };
40
58
  }
41
59
 
42
60
  return parsedItem;
@@ -90,14 +108,19 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
90
108
  logger.info(`[request sent containing ${length} ${units}]`);
91
109
  logger.verbose(`${this.shortenContent(content)}`);
92
110
  }
93
-
94
111
  if (stream) {
95
112
  logger.info(`[response received as an SSE stream]`);
96
113
  } else {
97
- const responseText = this.parseResponse(responseData);
98
- const { length, units } = this.getLength(responseText);
99
- logger.info(`[response received containing ${length} ${units}]`);
100
- logger.verbose(`${this.shortenContent(responseText)}`);
114
+ const parsedResponse = this.parseResponse(responseData);
115
+
116
+ if (typeof parsedResponse === 'string') {
117
+ const { length, units } = this.getLength(parsedResponse);
118
+ logger.info(`[response received containing ${length} ${units}]`);
119
+ logger.verbose(`${this.shortenContent(parsedResponse)}`);
120
+ } else {
121
+ logger.info(`[response received containing object]`);
122
+ logger.verbose(`${JSON.stringify(parsedResponse)}`);
123
+ }
101
124
  }
102
125
 
103
126
  prompt && prompt.debugInfo && (prompt.debugInfo += `\n${JSON.stringify(data)}`);
@@ -109,6 +132,15 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
109
132
 
110
133
  requestParameters.messages = await this.tryParseMessages(requestParameters.messages);
111
134
 
135
+ // Add tools support if provided in parameters
136
+ if (parameters.tools) {
137
+ requestParameters.tools = parameters.tools;
138
+ }
139
+
140
+ if (parameters.tool_choice) {
141
+ requestParameters.tool_choice = parameters.tool_choice;
142
+ }
143
+
112
144
  const modelMaxReturnTokens = this.getModelMaxReturnTokens();
113
145
  const maxTokensPrompt = this.promptParameters.max_tokens;
114
146
  const maxTokensModel = this.getModelMaxTokenLength() * (1 - this.getPromptTokenRatio());
@@ -138,6 +170,159 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
138
170
  return this.executeRequest(cortexRequest);
139
171
  }
140
172
 
173
+ // Override parseResponse to handle tool calls
174
+ parseResponse(data) {
175
+ if (!data) return "";
176
+ const { choices } = data;
177
+ if (!choices || !choices.length) {
178
+ return data;
179
+ }
180
+
181
+ // if we got a choices array back with more than one choice, return the whole array
182
+ if (choices.length > 1) {
183
+ return choices;
184
+ }
185
+
186
+ const choice = choices[0];
187
+ const message = choice.message;
188
+
189
+ // Handle tool calls in the response
190
+ if (message.tool_calls) {
191
+ return {
192
+ role: message.role,
193
+ content: message.content || "",
194
+ tool_calls: message.tool_calls
195
+ };
196
+ }
197
+
198
+ return message.content || "";
199
+ }
200
+
201
+ processStreamEvent(event, requestProgress) {
202
+ // check for end of stream or in-stream errors
203
+ if (event.data.trim() === '[DONE]') {
204
+ requestProgress.progress = 1;
205
+ // Clear buffers when stream is done
206
+ this.toolCallsBuffer = [];
207
+ this.contentBuffer = ''; // Clear content buffer
208
+ } else {
209
+ let parsedMessage;
210
+ try {
211
+ parsedMessage = JSON.parse(event.data);
212
+ requestProgress.data = event.data;
213
+ } catch (error) {
214
+ // Clear buffers on error
215
+ this.toolCallsBuffer = [];
216
+ this.contentBuffer = '';
217
+ throw new Error(`Could not parse stream data: ${error}`);
218
+ }
219
+
220
+ // error can be in different places in the message
221
+ const streamError = parsedMessage?.error || parsedMessage?.choices?.[0]?.delta?.content?.error || parsedMessage?.choices?.[0]?.text?.error;
222
+ if (streamError) {
223
+ // Clear buffers on error
224
+ this.toolCallsBuffer = [];
225
+ this.contentBuffer = '';
226
+ throw new Error(streamError);
227
+ }
228
+
229
+ const delta = parsedMessage?.choices?.[0]?.delta;
230
+
231
+ // Accumulate content
232
+ if (delta?.content) {
233
+ this.contentBuffer += delta.content;
234
+ }
235
+
236
+ // Handle tool calls in streaming response
237
+ if (delta?.tool_calls) {
238
+ // Accumulate tool call deltas into the buffer
239
+ delta.tool_calls.forEach((toolCall) => {
240
+ const index = toolCall.index;
241
+ if (!this.toolCallsBuffer[index]) {
242
+ this.toolCallsBuffer[index] = {
243
+ id: toolCall.id || '',
244
+ type: toolCall.type || 'function',
245
+ function: {
246
+ name: toolCall.function?.name || '',
247
+ arguments: toolCall.function?.arguments || ''
248
+ }
249
+ };
250
+ } else {
251
+ if (toolCall.function?.name) {
252
+ this.toolCallsBuffer[index].function.name += toolCall.function.name;
253
+ }
254
+ if (toolCall.function?.arguments) {
255
+ this.toolCallsBuffer[index].function.arguments += toolCall.function.arguments;
256
+ }
257
+ }
258
+ });
259
+ }
260
+
261
+ // finish reason can be in different places in the message
262
+ const finishReason = parsedMessage?.choices?.[0]?.finish_reason || parsedMessage?.candidates?.[0]?.finishReason;
263
+ if (finishReason) {
264
+ const pathwayResolver = requestState[this.requestId]?.pathwayResolver; // Get resolver
265
+
266
+ switch (finishReason.toLowerCase()) {
267
+ case 'tool_calls':
268
+ // Process complete tool calls when we get the finish reason
269
+ if (this.pathwayToolCallback && this.toolCallsBuffer.length > 0) {
270
+ const toolMessage = {
271
+ role: 'assistant',
272
+ content: delta?.content || '',
273
+ tool_calls: this.toolCallsBuffer,
274
+ };
275
+ this.pathwayToolCallback(pathwayResolver?.args, toolMessage, pathwayResolver);
276
+ }
277
+ // Don't set progress to 1 for tool calls to keep stream open
278
+ // Clear tool buffer after processing, but keep content buffer
279
+ this.toolCallsBuffer = [];
280
+ break;
281
+ case 'safety':
282
+ const safetyRatings = JSON.stringify(parsedMessage?.candidates?.[0]?.safetyRatings) || '';
283
+ logger.warn(`Request ${this.requestId} was blocked by the safety filter. ${safetyRatings}`);
284
+ requestProgress.data = `\n\nResponse blocked by safety filter: ${safetyRatings}`;
285
+ requestProgress.progress = 1;
286
+ // Clear buffers on finish
287
+ this.toolCallsBuffer = [];
288
+ this.contentBuffer = '';
289
+ break;
290
+ default: // Includes 'stop' and other normal finish reasons
291
+ // Look to see if we need to add citations to the response
292
+ if (pathwayResolver && this.contentBuffer) {
293
+ const regex = /:cd_source\[(.*?)\]/g;
294
+ let match;
295
+ const foundIds = [];
296
+ while ((match = regex.exec(this.contentBuffer)) !== null) {
297
+ // Ensure the capture group exists and is not empty
298
+ if (match[1] && match[1].trim()) {
299
+ foundIds.push(match[1].trim());
300
+ }
301
+ }
302
+
303
+ if (foundIds.length > 0) {
304
+ const {searchResults, tool} = pathwayResolver;
305
+ logger.info(`Found referenced searchResultIds: ${foundIds.join(', ')}`);
306
+
307
+ if (searchResults) {
308
+ const toolObj = typeof tool === 'string' ? JSON.parse(tool) : (tool || {});
309
+ toolObj.citations = searchResults
310
+ .filter(result => foundIds.includes(result.searchResultId));
311
+ pathwayResolver.tool = JSON.stringify(toolObj);
312
+ }
313
+ }
314
+ }
315
+ requestProgress.progress = 1;
316
+ // Clear buffers on finish
317
+ this.toolCallsBuffer = [];
318
+ this.contentBuffer = '';
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ return requestProgress;
324
+ }
325
+
141
326
  }
142
327
 
143
328
  export default OpenAIVisionPlugin;
@@ -0,0 +1,256 @@
1
+ // agentic.test.js
2
+ // Tests for the agentic entity system
3
+
4
+ import test from 'ava';
5
+ import serverFactory from '../index.js';
6
+ import { createClient } from 'graphql-ws';
7
+ import ws from 'ws';
8
+
9
+ let testServer;
10
+ let wsClient;
11
+
12
+ test.before(async () => {
13
+ process.env.CORTEX_ENABLE_REST = 'true';
14
+ const { server, startServer } = await serverFactory();
15
+ startServer && await startServer();
16
+ testServer = server;
17
+
18
+ // Create WebSocket client for subscriptions
19
+ wsClient = createClient({
20
+ url: 'ws://localhost:4000/graphql',
21
+ webSocketImpl: ws,
22
+ retryAttempts: 3,
23
+ connectionParams: {},
24
+ on: {
25
+ error: (error) => {
26
+ console.error('WS connection error:', error);
27
+ }
28
+ }
29
+ });
30
+
31
+ // Test the connection by making a simple subscription
32
+ try {
33
+ await new Promise((resolve, reject) => {
34
+ const subscription = wsClient.subscribe(
35
+ {
36
+ query: `
37
+ subscription TestConnection {
38
+ requestProgress(requestIds: ["test"]) {
39
+ requestId
40
+ }
41
+ }
42
+ `
43
+ },
44
+ {
45
+ next: () => {
46
+ resolve();
47
+ },
48
+ error: reject,
49
+ complete: () => {
50
+ resolve();
51
+ }
52
+ }
53
+ );
54
+
55
+ // Add a timeout to avoid hanging
56
+ setTimeout(() => {
57
+ resolve();
58
+ }, 2000);
59
+ });
60
+ } catch (error) {
61
+ console.error('Failed to establish WebSocket connection:', error);
62
+ throw error;
63
+ }
64
+ });
65
+
66
+ test.after.always('cleanup', async () => {
67
+ if (wsClient) {
68
+ wsClient.dispose();
69
+ }
70
+ if (testServer) {
71
+ await testServer.stop();
72
+ }
73
+ });
74
+
75
+ // Helper function to collect subscription events
76
+ async function collectSubscriptionEvents(subscription, timeout = 30000) {
77
+ const events = [];
78
+
79
+ return new Promise((resolve, reject) => {
80
+ const timeoutId = setTimeout(() => {
81
+ if (events.length > 0) {
82
+ resolve(events);
83
+ } else {
84
+ reject(new Error('Subscription timed out with no events'));
85
+ }
86
+ }, timeout);
87
+
88
+ const unsubscribe = wsClient.subscribe(
89
+ {
90
+ query: subscription.query,
91
+ variables: subscription.variables
92
+ },
93
+ {
94
+ next: (event) => {
95
+ events.push(event);
96
+ if (event?.data?.requestProgress?.progress === 1) {
97
+ clearTimeout(timeoutId);
98
+ unsubscribe();
99
+ resolve(events);
100
+ }
101
+ },
102
+ error: (error) => {
103
+ clearTimeout(timeoutId);
104
+ reject(error);
105
+ },
106
+ complete: () => {
107
+ clearTimeout(timeoutId);
108
+ resolve(events);
109
+ }
110
+ }
111
+ );
112
+ });
113
+ }
114
+
115
+ // Test basic single-step task
116
+ test.serial('sys_entity_agent handles single-step task', async (t) => {
117
+ t.timeout(30000); // 30 second timeout
118
+ const response = await testServer.executeOperation({
119
+ query: `
120
+ query TestAgentSingleStep(
121
+ $text: String!,
122
+ $chatHistory: [MultiMessage]!
123
+ ) {
124
+ sys_entity_agent(
125
+ text: $text,
126
+ chatHistory: $chatHistory,
127
+ stream: true
128
+ ) {
129
+ result
130
+ contextId
131
+ tool
132
+ warnings
133
+ errors
134
+ }
135
+ }
136
+ `,
137
+ variables: {
138
+ text: 'What is the current time?',
139
+ chatHistory: [{
140
+ role: "user",
141
+ content: ["What is the current time?"]
142
+ }]
143
+ }
144
+ });
145
+
146
+ console.log('Single-step Agent Response:', JSON.stringify(response, null, 2));
147
+
148
+ // Check for successful response
149
+ t.falsy(response.body?.singleResult?.errors, 'Should not have GraphQL errors');
150
+ const requestId = response.body?.singleResult?.data?.sys_entity_agent?.result;
151
+ t.truthy(requestId, 'Should have a requestId in the result field');
152
+
153
+ // Collect events
154
+ const events = await collectSubscriptionEvents({
155
+ query: `
156
+ subscription OnRequestProgress($requestId: String!) {
157
+ requestProgress(requestIds: [$requestId]) {
158
+ requestId
159
+ progress
160
+ data
161
+ info
162
+ }
163
+ }
164
+ `,
165
+ variables: { requestId }
166
+ });
167
+
168
+ console.log(`Received ${events.length} events for single-step task`);
169
+ t.true(events.length > 0, 'Should have received events');
170
+
171
+ // Verify we got a completion event
172
+ const completionEvent = events.find(event =>
173
+ event.data.requestProgress.progress === 1
174
+ );
175
+ t.truthy(completionEvent, 'Should have received a completion event');
176
+ });
177
+
178
+ // Test multi-step task with tool usage
179
+ test.serial('sys_entity_agent handles multi-step task with tools', async (t) => {
180
+ t.timeout(60000); // 60 second timeout for multi-step task
181
+ const response = await testServer.executeOperation({
182
+ query: `
183
+ query TestAgentMultiStep(
184
+ $text: String!,
185
+ $chatHistory: [MultiMessage]!
186
+ ) {
187
+ sys_entity_agent(
188
+ text: $text,
189
+ chatHistory: $chatHistory,
190
+ stream: true
191
+ ) {
192
+ result
193
+ contextId
194
+ tool
195
+ warnings
196
+ errors
197
+ }
198
+ }
199
+ `,
200
+ variables: {
201
+ text: 'Research the latest developments in renewable energy and summarize the key trends.',
202
+ chatHistory: [{
203
+ role: "user",
204
+ content: ["Research the latest developments in renewable energy and summarize the key trends."]
205
+ }]
206
+ }
207
+ });
208
+
209
+ console.log('Multi-step Agent Response:', JSON.stringify(response, null, 2));
210
+
211
+ // Check for successful response
212
+ t.falsy(response.body?.singleResult?.errors, 'Should not have GraphQL errors');
213
+ const requestId = response.body?.singleResult?.data?.sys_entity_agent?.result;
214
+ t.truthy(requestId, 'Should have a requestId in the result field');
215
+
216
+ // Collect events with a longer timeout since this is a multi-step operation
217
+ const events = await collectSubscriptionEvents({
218
+ query: `
219
+ subscription OnRequestProgress($requestId: String!) {
220
+ requestProgress(requestIds: [$requestId]) {
221
+ requestId
222
+ progress
223
+ data
224
+ info
225
+ }
226
+ }
227
+ `,
228
+ variables: { requestId }
229
+ }, 60000);
230
+
231
+ console.log(`Received ${events.length} events for multi-step task`);
232
+ t.true(events.length > 0, 'Should have received events');
233
+
234
+ // Verify we got a completion event
235
+ const completionEvent = events.find(event =>
236
+ event.data.requestProgress.progress === 1
237
+ );
238
+ t.truthy(completionEvent, 'Should have received a completion event');
239
+
240
+ // Check for tool usage in the events
241
+ let foundToolUsage = false;
242
+ for (const event of events) {
243
+ if (event.data.requestProgress.info) {
244
+ try {
245
+ const info = JSON.parse(event.data.requestProgress.info);
246
+ if (info.toolUsed) {
247
+ foundToolUsage = true;
248
+ break;
249
+ }
250
+ } catch (e) {
251
+ // Some info might not be JSON, which is fine
252
+ }
253
+ }
254
+ }
255
+ t.true(foundToolUsage, 'Should have used tools during execution');
256
+ });