@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.
- package/README.md +9 -9
- package/config/default.example.json +0 -20
- package/config.js +160 -6
- package/lib/pathwayTools.js +79 -1
- package/lib/requestExecutor.js +3 -1
- package/lib/util.js +7 -0
- package/package.json +1 -1
- package/pathways/basePathway.js +2 -0
- package/pathways/call_tools.js +379 -0
- package/pathways/system/entity/memory/shared/sys_memory_helpers.js +1 -1
- package/pathways/system/entity/memory/sys_search_memory.js +2 -2
- package/pathways/system/entity/sys_entity_agent.js +289 -0
- package/pathways/system/entity/sys_generator_memory.js +1 -1
- package/pathways/system/entity/sys_generator_results.js +1 -1
- package/pathways/system/entity/sys_get_entities.js +19 -0
- package/pathways/system/entity/tools/shared/sys_entity_tools.js +150 -0
- package/pathways/system/entity/tools/sys_tool_bing_search.js +147 -0
- package/pathways/system/entity/tools/sys_tool_callmodel.js +62 -0
- package/pathways/system/entity/tools/sys_tool_coding.js +53 -0
- package/pathways/system/entity/tools/sys_tool_codingagent.js +100 -0
- package/pathways/system/entity/tools/sys_tool_cognitive_search.js +231 -0
- package/pathways/system/entity/tools/sys_tool_image.js +57 -0
- package/pathways/system/entity/tools/sys_tool_readfile.js +119 -0
- package/pathways/system/entity/tools/sys_tool_reasoning.js +75 -0
- package/pathways/system/entity/tools/sys_tool_remember.js +59 -0
- package/pathways/vision.js +1 -1
- package/server/modelExecutor.js +4 -12
- package/server/pathwayResolver.js +53 -40
- package/server/plugins/azureBingPlugin.js +42 -4
- package/server/plugins/azureCognitivePlugin.js +40 -12
- package/server/plugins/claude3VertexPlugin.js +67 -18
- package/server/plugins/modelPlugin.js +3 -2
- package/server/plugins/openAiReasoningPlugin.js +3 -3
- package/server/plugins/openAiReasoningVisionPlugin.js +48 -0
- package/server/plugins/openAiVisionPlugin.js +192 -7
- package/tests/agentic.test.js +256 -0
- package/tests/call_tools.test.js +216 -0
- package/tests/claude3VertexToolConversion.test.js +78 -0
- package/tests/mocks.js +11 -3
- package/tests/multimodal_conversion.test.js +1 -1
- package/tests/openAiToolPlugin.test.js +242 -0
- package/pathways/test_palm_chat.js +0 -31
- package/server/plugins/palmChatPlugin.js +0 -233
- package/server/plugins/palmCodeCompletionPlugin.js +0 -45
- package/server/plugins/palmCompletionPlugin.js +0 -135
- package/tests/palmChatPlugin.test.js +0 -219
- 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
|
|
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: '
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
});
|