node-red-contrib-ai-agent 0.4.0 → 0.5.0
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 +20 -12
- package/agent/ai-agent.html +95 -95
- package/agent/ai-agent.js +326 -326
- package/model/ai-model.html +260 -141
- package/orchestrator/orchestrator.html +2 -2
- package/orchestrator/orchestrator.js +98 -26
- package/package.json +4 -2
package/agent/ai-agent.js
CHANGED
|
@@ -1,326 +1,326 @@
|
|
|
1
|
-
const axios = require('axios');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Helper functions for AI Agent Node
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// Validate AI configuration
|
|
8
|
-
function validateAIConfig(aiagent) {
|
|
9
|
-
if (!aiagent) return 'AI configuration missing. Ensure an AI Model node is connected.';
|
|
10
|
-
if (!aiagent.model) return 'AI model not specified. Please configure the AI Model node with a valid model.';
|
|
11
|
-
if (!aiagent.apiKey) return 'API key not found. Please configure the AI Model node with a valid API key.';
|
|
12
|
-
return null; // No errors
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Creates and returns a message object
|
|
17
|
-
* @param {string} role - The role of the message (e.g., 'user', 'assistant', 'system')
|
|
18
|
-
* @param {string} content - The content of the message
|
|
19
|
-
* @returns {Object} - The message object
|
|
20
|
-
*/
|
|
21
|
-
function createMessage(role, content) {
|
|
22
|
-
return {
|
|
23
|
-
role: role,
|
|
24
|
-
content: content,
|
|
25
|
-
timestamp: new Date().toISOString(),
|
|
26
|
-
type: 'conversation'
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Prepares the prompt with context if memory is available
|
|
32
|
-
* @param {Object} node - The AI Agent node
|
|
33
|
-
* @param {Object} msg - The input message
|
|
34
|
-
* @param {string} inputText - The input text
|
|
35
|
-
* @returns {Object} - The prepared prompt object
|
|
36
|
-
*/
|
|
37
|
-
function preparePrompt(node, msg, inputText) {
|
|
38
|
-
const messages = [{ role: 'system', content: node.systemPrompt }];
|
|
39
|
-
let userMessage = null;
|
|
40
|
-
|
|
41
|
-
// Add context if using memory
|
|
42
|
-
if (msg.aimemory) {
|
|
43
|
-
if (!msg.aimemory.context) {
|
|
44
|
-
throw new Error('Memory not properly initialized. Ensure a memory node is connected.');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Ensure memory has required structure
|
|
48
|
-
msg.aimemory.context = msg.aimemory.context || [];
|
|
49
|
-
msg.aimemory.maxItems = msg.aimemory.maxItems || 1000;
|
|
50
|
-
|
|
51
|
-
// Add conversation context
|
|
52
|
-
messages.push(...msg.aimemory.context);
|
|
53
|
-
|
|
54
|
-
// Create and store user message for later context update
|
|
55
|
-
userMessage = createMessage('user', inputText);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Add current user input
|
|
59
|
-
messages.push({ role: 'user', content: inputText });
|
|
60
|
-
|
|
61
|
-
return { messages, userMessage };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Updates the conversation context with new messages
|
|
66
|
-
* @param {Object} msg - The input message
|
|
67
|
-
* @param {Object} userMessage - The user message
|
|
68
|
-
* @param {string} assistantResponse - The assistant response
|
|
69
|
-
*/
|
|
70
|
-
function updateContext(msg, userMessage, assistantResponse) {
|
|
71
|
-
if (!msg.aimemory?.context) return;
|
|
72
|
-
|
|
73
|
-
const assistantMessage = createMessage('assistant', assistantResponse);
|
|
74
|
-
const newContext = [...msg.aimemory.context, userMessage, assistantMessage];
|
|
75
|
-
const maxItems = msg.aimemory.maxItems || 1000;
|
|
76
|
-
|
|
77
|
-
msg.aimemory.context = newContext.slice(-maxItems);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Handles errors consistently
|
|
82
|
-
* @param {Object} node - The AI Agent node
|
|
83
|
-
* @param {Object} msg - The input message
|
|
84
|
-
* @param {Error} error - The error object
|
|
85
|
-
*/
|
|
86
|
-
function handleError(node, msg, error) {
|
|
87
|
-
const errorMsg = error.response?.data?.error?.message || error.message || 'Unknown error';
|
|
88
|
-
node.status({ fill: 'red', shape: 'ring', text: 'Error' });
|
|
89
|
-
node.error('AI Agent Error: ' + errorMsg, msg);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Formats tools for the OpenAI/OpenRouter API
|
|
94
|
-
* @param {Array} tools - Array of tool definitions
|
|
95
|
-
* @returns {Array} - Formatted tools for the API
|
|
96
|
-
*/
|
|
97
|
-
function formatToolsForAPI(tools) {
|
|
98
|
-
return tools.map(tool => {
|
|
99
|
-
const type = tool.type || 'function';
|
|
100
|
-
const fn = tool.function || {};
|
|
101
|
-
const name = fn.name || 'function';
|
|
102
|
-
const description = fn.description || 'function';
|
|
103
|
-
const parameters = fn.parameters || {};
|
|
104
|
-
return {
|
|
105
|
-
type: type,
|
|
106
|
-
function: {
|
|
107
|
-
name: name,
|
|
108
|
-
description: description,
|
|
109
|
-
parameters: parameters || {
|
|
110
|
-
type: 'object',
|
|
111
|
-
properties: {},
|
|
112
|
-
required: []
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Calls the AI with proper error handling
|
|
121
|
-
* @param {Object} node - The AI Agent node
|
|
122
|
-
* @param {Object} aiConfig - The AI configuration object
|
|
123
|
-
* @param {Array} messages - The messages to send to the AI
|
|
124
|
-
* @returns {Promise<string>} - The AI response
|
|
125
|
-
*/
|
|
126
|
-
async function callAI(node, aiConfig, messages) {
|
|
127
|
-
const hasTools = aiConfig.tools && Array.isArray(aiConfig.tools) && aiConfig.tools.length > 0;
|
|
128
|
-
const tools = hasTools ? aiConfig.tools : [];
|
|
129
|
-
const toolChoice = hasTools ? 'auto' : 'none';
|
|
130
|
-
|
|
131
|
-
node.warn(`Calling ${aiConfig.model} with ${tools.length} tools and ${toolChoice} tool choice`);
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
node.status({ fill: 'blue', shape: 'dot', text: `Calling ${aiConfig.model}...` });
|
|
135
|
-
|
|
136
|
-
// Prepare request payload
|
|
137
|
-
const requestPayload = {
|
|
138
|
-
model: aiConfig.model,
|
|
139
|
-
temperature: aiConfig.temperature,
|
|
140
|
-
// max_tokens: aiConfig.maxTokens,
|
|
141
|
-
messages: messages,
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
// Add tools if available
|
|
145
|
-
if (hasTools) {
|
|
146
|
-
node.warn('Adding tools: ' + JSON.stringify(tools, null, 2));
|
|
147
|
-
requestPayload.tools = formatToolsForAPI(tools);
|
|
148
|
-
requestPayload.tool_choice = toolChoice;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
node.warn(JSON.stringify(requestPayload, null, 2));
|
|
152
|
-
|
|
153
|
-
const response = await axios.post(
|
|
154
|
-
'https://openrouter.ai/api/v1/chat/completions',
|
|
155
|
-
requestPayload,
|
|
156
|
-
{
|
|
157
|
-
headers: {
|
|
158
|
-
'Authorization': `Bearer ${aiConfig.apiKey}`,
|
|
159
|
-
'Content-Type': 'application/json',
|
|
160
|
-
'HTTP-Referer': 'https://nodered.org/',
|
|
161
|
-
'X-Title': 'Node-RED AI Agent'
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
// Check if the response contains tool calls
|
|
167
|
-
const responseMessage = response.data.choices[0]?.message;
|
|
168
|
-
|
|
169
|
-
node.warn(JSON.stringify(responseMessage, null, 2));
|
|
170
|
-
|
|
171
|
-
if (responseMessage?.tool_calls && aiConfig.tools) {
|
|
172
|
-
// Process tool calls
|
|
173
|
-
if (node.warn) node.warn('Processing tool calls');
|
|
174
|
-
return await processToolCalls(node, responseMessage, aiConfig.tools, messages, aiConfig);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
node.warn('Processing response');
|
|
178
|
-
return responseMessage?.content?.trim() || '';
|
|
179
|
-
|
|
180
|
-
} catch (error) {
|
|
181
|
-
const errorMsg = error.response?.data?.error?.message || error.message;
|
|
182
|
-
throw new Error(`AI API Error: ${errorMsg}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Helper function to process tool calls from AI response
|
|
188
|
-
* @param {Object} node - The Node-RED node instance
|
|
189
|
-
* @param {Object} responseMessage - The AI response message containing tool calls
|
|
190
|
-
* @param {Array} tools - Array of available tools
|
|
191
|
-
* @param {Array} messages - Conversation messages
|
|
192
|
-
* @returns {Promise<string>} - Result of tool executions
|
|
193
|
-
*/
|
|
194
|
-
async function processToolCalls(node, responseMessage, tools, messages, aiConfig) {
|
|
195
|
-
try {
|
|
196
|
-
const toolCalls = responseMessage.tool_calls || [];
|
|
197
|
-
let toolResults = [];
|
|
198
|
-
if (node && node.warn) {
|
|
199
|
-
node.warn('Processing tool calls: ' + JSON.stringify(toolCalls, null, 2));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Process each tool call
|
|
203
|
-
for (const toolCall of toolCalls) {
|
|
204
|
-
const { id, function: fn } = toolCall;
|
|
205
|
-
const { name, arguments: args } = fn;
|
|
206
|
-
|
|
207
|
-
// Find the matching tool
|
|
208
|
-
const tool = tools.find(t => t.function?.name === name);
|
|
209
|
-
if (!tool) {
|
|
210
|
-
toolResults.push({
|
|
211
|
-
tool_call_id: id,
|
|
212
|
-
role: 'tool',
|
|
213
|
-
name,
|
|
214
|
-
content: JSON.stringify({ error: `Tool '${name}' not found` })
|
|
215
|
-
});
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Execute the tool
|
|
220
|
-
try {
|
|
221
|
-
const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
|
|
222
|
-
const result = await tool.execute(parsedArgs);
|
|
223
|
-
|
|
224
|
-
toolResults.push({
|
|
225
|
-
tool_call_id: id,
|
|
226
|
-
role: 'tool',
|
|
227
|
-
name,
|
|
228
|
-
content: typeof result === 'string' ? result : JSON.stringify(result)
|
|
229
|
-
});
|
|
230
|
-
} catch (error) {
|
|
231
|
-
toolResults.push({
|
|
232
|
-
tool_call_id: id,
|
|
233
|
-
role: 'tool',
|
|
234
|
-
name,
|
|
235
|
-
content: JSON.stringify({ error: error.message })
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Add tool results to the messages array
|
|
241
|
-
const updatedMessages = [...messages, responseMessage, ...toolResults];
|
|
242
|
-
|
|
243
|
-
// Make a new API call to let the AI process the tool results
|
|
244
|
-
const aiResponse = await callAI(node, { ...aiConfig, tools: null }, updatedMessages);
|
|
245
|
-
|
|
246
|
-
// Return the final AI response
|
|
247
|
-
return aiResponse;
|
|
248
|
-
} catch (error) {
|
|
249
|
-
return `Error processing tool calls: ${error.message}`;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
module.exports = function (RED) {
|
|
254
|
-
/**
|
|
255
|
-
* AI Agent Node
|
|
256
|
-
* @param {Object} config - The node configuration object
|
|
257
|
-
*/
|
|
258
|
-
function AiAgentNode(config) {
|
|
259
|
-
RED.nodes.createNode(this, config);
|
|
260
|
-
const node = this;
|
|
261
|
-
|
|
262
|
-
// Configuration
|
|
263
|
-
this.agentName = config.name || 'AI Agent';
|
|
264
|
-
this.systemPrompt = config.systemPrompt || 'You are a helpful AI assistant.';
|
|
265
|
-
this.responseType = config.responseType || 'text';
|
|
266
|
-
|
|
267
|
-
// Handle node cleanup
|
|
268
|
-
node.on('close', function (done) {
|
|
269
|
-
node.status({});
|
|
270
|
-
if (done) done();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Process incoming messages
|
|
274
|
-
node.on('input', async function (msg, send, done) {
|
|
275
|
-
node.status({ fill: 'blue', shape: 'dot', text: 'processing...' });
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
// 1. Validate AI configuration
|
|
279
|
-
const validationError = validateAIConfig(msg.aiagent);
|
|
280
|
-
if (validationError) {
|
|
281
|
-
throw new Error(validationError);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// 2. Get input
|
|
285
|
-
const input = msg.payload || {};
|
|
286
|
-
const inputText = typeof input === 'string' ? input : JSON.stringify(input);
|
|
287
|
-
|
|
288
|
-
// 3. Prepare prompt with context
|
|
289
|
-
const { messages, userMessage } = preparePrompt(node, msg, inputText);
|
|
290
|
-
|
|
291
|
-
// 4. Execute the prompt and get response
|
|
292
|
-
const response = await callAI(node, msg.aiagent, messages);
|
|
293
|
-
|
|
294
|
-
// 5. Update context if using memory
|
|
295
|
-
if (msg.aimemory && userMessage) {
|
|
296
|
-
updateContext(msg, userMessage, response);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// 6. Format and send response
|
|
300
|
-
msg.payload = node.responseType === 'object' ? {
|
|
301
|
-
agent: node.agentName,
|
|
302
|
-
type: 'ai',
|
|
303
|
-
input: input,
|
|
304
|
-
response: response,
|
|
305
|
-
timestamp: new Date().toISOString(),
|
|
306
|
-
context: {
|
|
307
|
-
conversationLength: msg.aimemory?.context?.length || 0,
|
|
308
|
-
lastInteraction: new Date().toISOString(),
|
|
309
|
-
...(msg.aimemory && { aimemory: msg.aimemory })
|
|
310
|
-
}
|
|
311
|
-
} : response;
|
|
312
|
-
|
|
313
|
-
send(msg);
|
|
314
|
-
node.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
315
|
-
|
|
316
|
-
} catch (error) {
|
|
317
|
-
handleError(node, msg, error);
|
|
318
|
-
} finally {
|
|
319
|
-
if (done) done();
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Register the node type
|
|
325
|
-
RED.nodes.registerType('ai-agent', AiAgentNode);
|
|
326
|
-
};
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper functions for AI Agent Node
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Validate AI configuration
|
|
8
|
+
function validateAIConfig(aiagent) {
|
|
9
|
+
if (!aiagent) return 'AI configuration missing. Ensure an AI Model node is connected.';
|
|
10
|
+
if (!aiagent.model) return 'AI model not specified. Please configure the AI Model node with a valid model.';
|
|
11
|
+
if (!aiagent.apiKey) return 'API key not found. Please configure the AI Model node with a valid API key.';
|
|
12
|
+
return null; // No errors
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates and returns a message object
|
|
17
|
+
* @param {string} role - The role of the message (e.g., 'user', 'assistant', 'system')
|
|
18
|
+
* @param {string} content - The content of the message
|
|
19
|
+
* @returns {Object} - The message object
|
|
20
|
+
*/
|
|
21
|
+
function createMessage(role, content) {
|
|
22
|
+
return {
|
|
23
|
+
role: role,
|
|
24
|
+
content: content,
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
type: 'conversation'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Prepares the prompt with context if memory is available
|
|
32
|
+
* @param {Object} node - The AI Agent node
|
|
33
|
+
* @param {Object} msg - The input message
|
|
34
|
+
* @param {string} inputText - The input text
|
|
35
|
+
* @returns {Object} - The prepared prompt object
|
|
36
|
+
*/
|
|
37
|
+
function preparePrompt(node, msg, inputText) {
|
|
38
|
+
const messages = [{ role: 'system', content: node.systemPrompt }];
|
|
39
|
+
let userMessage = null;
|
|
40
|
+
|
|
41
|
+
// Add context if using memory
|
|
42
|
+
if (msg.aimemory) {
|
|
43
|
+
if (!msg.aimemory.context) {
|
|
44
|
+
throw new Error('Memory not properly initialized. Ensure a memory node is connected.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Ensure memory has required structure
|
|
48
|
+
msg.aimemory.context = msg.aimemory.context || [];
|
|
49
|
+
msg.aimemory.maxItems = msg.aimemory.maxItems || 1000;
|
|
50
|
+
|
|
51
|
+
// Add conversation context
|
|
52
|
+
messages.push(...msg.aimemory.context);
|
|
53
|
+
|
|
54
|
+
// Create and store user message for later context update
|
|
55
|
+
userMessage = createMessage('user', inputText);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add current user input
|
|
59
|
+
messages.push({ role: 'user', content: inputText });
|
|
60
|
+
|
|
61
|
+
return { messages, userMessage };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Updates the conversation context with new messages
|
|
66
|
+
* @param {Object} msg - The input message
|
|
67
|
+
* @param {Object} userMessage - The user message
|
|
68
|
+
* @param {string} assistantResponse - The assistant response
|
|
69
|
+
*/
|
|
70
|
+
function updateContext(msg, userMessage, assistantResponse) {
|
|
71
|
+
if (!msg.aimemory?.context) return;
|
|
72
|
+
|
|
73
|
+
const assistantMessage = createMessage('assistant', assistantResponse);
|
|
74
|
+
const newContext = [...msg.aimemory.context, userMessage, assistantMessage];
|
|
75
|
+
const maxItems = msg.aimemory.maxItems || 1000;
|
|
76
|
+
|
|
77
|
+
msg.aimemory.context = newContext.slice(-maxItems);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handles errors consistently
|
|
82
|
+
* @param {Object} node - The AI Agent node
|
|
83
|
+
* @param {Object} msg - The input message
|
|
84
|
+
* @param {Error} error - The error object
|
|
85
|
+
*/
|
|
86
|
+
function handleError(node, msg, error) {
|
|
87
|
+
const errorMsg = error.response?.data?.error?.message || error.message || 'Unknown error';
|
|
88
|
+
node.status({ fill: 'red', shape: 'ring', text: 'Error' });
|
|
89
|
+
node.error('AI Agent Error: ' + errorMsg, msg);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Formats tools for the OpenAI/OpenRouter API
|
|
94
|
+
* @param {Array} tools - Array of tool definitions
|
|
95
|
+
* @returns {Array} - Formatted tools for the API
|
|
96
|
+
*/
|
|
97
|
+
function formatToolsForAPI(tools) {
|
|
98
|
+
return tools.map(tool => {
|
|
99
|
+
const type = tool.type || 'function';
|
|
100
|
+
const fn = tool.function || {};
|
|
101
|
+
const name = fn.name || 'function';
|
|
102
|
+
const description = fn.description || 'function';
|
|
103
|
+
const parameters = fn.parameters || {};
|
|
104
|
+
return {
|
|
105
|
+
type: type,
|
|
106
|
+
function: {
|
|
107
|
+
name: name,
|
|
108
|
+
description: description,
|
|
109
|
+
parameters: parameters || {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {},
|
|
112
|
+
required: []
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Calls the AI with proper error handling
|
|
121
|
+
* @param {Object} node - The AI Agent node
|
|
122
|
+
* @param {Object} aiConfig - The AI configuration object
|
|
123
|
+
* @param {Array} messages - The messages to send to the AI
|
|
124
|
+
* @returns {Promise<string>} - The AI response
|
|
125
|
+
*/
|
|
126
|
+
async function callAI(node, aiConfig, messages) {
|
|
127
|
+
const hasTools = aiConfig.tools && Array.isArray(aiConfig.tools) && aiConfig.tools.length > 0;
|
|
128
|
+
const tools = hasTools ? aiConfig.tools : [];
|
|
129
|
+
const toolChoice = hasTools ? 'auto' : 'none';
|
|
130
|
+
|
|
131
|
+
node.warn(`Calling ${aiConfig.model} with ${tools.length} tools and ${toolChoice} tool choice`);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
node.status({ fill: 'blue', shape: 'dot', text: `Calling ${aiConfig.model}...` });
|
|
135
|
+
|
|
136
|
+
// Prepare request payload
|
|
137
|
+
const requestPayload = {
|
|
138
|
+
model: aiConfig.model,
|
|
139
|
+
temperature: aiConfig.temperature,
|
|
140
|
+
// max_tokens: aiConfig.maxTokens,
|
|
141
|
+
messages: messages,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Add tools if available
|
|
145
|
+
if (hasTools) {
|
|
146
|
+
node.warn('Adding tools: ' + JSON.stringify(tools, null, 2));
|
|
147
|
+
requestPayload.tools = formatToolsForAPI(tools);
|
|
148
|
+
requestPayload.tool_choice = toolChoice;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
node.warn(JSON.stringify(requestPayload, null, 2));
|
|
152
|
+
|
|
153
|
+
const response = await axios.post(
|
|
154
|
+
'https://openrouter.ai/api/v1/chat/completions',
|
|
155
|
+
requestPayload,
|
|
156
|
+
{
|
|
157
|
+
headers: {
|
|
158
|
+
'Authorization': `Bearer ${aiConfig.apiKey}`,
|
|
159
|
+
'Content-Type': 'application/json',
|
|
160
|
+
'HTTP-Referer': 'https://nodered.org/',
|
|
161
|
+
'X-Title': 'Node-RED AI Agent'
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Check if the response contains tool calls
|
|
167
|
+
const responseMessage = response.data.choices[0]?.message;
|
|
168
|
+
|
|
169
|
+
node.warn(JSON.stringify(responseMessage, null, 2));
|
|
170
|
+
|
|
171
|
+
if (responseMessage?.tool_calls && aiConfig.tools) {
|
|
172
|
+
// Process tool calls
|
|
173
|
+
if (node.warn) node.warn('Processing tool calls');
|
|
174
|
+
return await processToolCalls(node, responseMessage, aiConfig.tools, messages, aiConfig);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
node.warn('Processing response');
|
|
178
|
+
return responseMessage?.content?.trim() || '';
|
|
179
|
+
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const errorMsg = error.response?.data?.error?.message || error.message;
|
|
182
|
+
throw new Error(`AI API Error: ${errorMsg}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Helper function to process tool calls from AI response
|
|
188
|
+
* @param {Object} node - The Node-RED node instance
|
|
189
|
+
* @param {Object} responseMessage - The AI response message containing tool calls
|
|
190
|
+
* @param {Array} tools - Array of available tools
|
|
191
|
+
* @param {Array} messages - Conversation messages
|
|
192
|
+
* @returns {Promise<string>} - Result of tool executions
|
|
193
|
+
*/
|
|
194
|
+
async function processToolCalls(node, responseMessage, tools, messages, aiConfig) {
|
|
195
|
+
try {
|
|
196
|
+
const toolCalls = responseMessage.tool_calls || [];
|
|
197
|
+
let toolResults = [];
|
|
198
|
+
if (node && node.warn) {
|
|
199
|
+
node.warn('Processing tool calls: ' + JSON.stringify(toolCalls, null, 2));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Process each tool call
|
|
203
|
+
for (const toolCall of toolCalls) {
|
|
204
|
+
const { id, function: fn } = toolCall;
|
|
205
|
+
const { name, arguments: args } = fn;
|
|
206
|
+
|
|
207
|
+
// Find the matching tool
|
|
208
|
+
const tool = tools.find(t => t.function?.name === name);
|
|
209
|
+
if (!tool) {
|
|
210
|
+
toolResults.push({
|
|
211
|
+
tool_call_id: id,
|
|
212
|
+
role: 'tool',
|
|
213
|
+
name,
|
|
214
|
+
content: JSON.stringify({ error: `Tool '${name}' not found` })
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Execute the tool
|
|
220
|
+
try {
|
|
221
|
+
const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
|
|
222
|
+
const result = await tool.execute(parsedArgs);
|
|
223
|
+
|
|
224
|
+
toolResults.push({
|
|
225
|
+
tool_call_id: id,
|
|
226
|
+
role: 'tool',
|
|
227
|
+
name,
|
|
228
|
+
content: typeof result === 'string' ? result : JSON.stringify(result)
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
toolResults.push({
|
|
232
|
+
tool_call_id: id,
|
|
233
|
+
role: 'tool',
|
|
234
|
+
name,
|
|
235
|
+
content: JSON.stringify({ error: error.message })
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Add tool results to the messages array
|
|
241
|
+
const updatedMessages = [...messages, responseMessage, ...toolResults];
|
|
242
|
+
|
|
243
|
+
// Make a new API call to let the AI process the tool results
|
|
244
|
+
const aiResponse = await callAI(node, { ...aiConfig, tools: null }, updatedMessages);
|
|
245
|
+
|
|
246
|
+
// Return the final AI response
|
|
247
|
+
return aiResponse;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return `Error processing tool calls: ${error.message}`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = function (RED) {
|
|
254
|
+
/**
|
|
255
|
+
* AI Agent Node
|
|
256
|
+
* @param {Object} config - The node configuration object
|
|
257
|
+
*/
|
|
258
|
+
function AiAgentNode(config) {
|
|
259
|
+
RED.nodes.createNode(this, config);
|
|
260
|
+
const node = this;
|
|
261
|
+
|
|
262
|
+
// Configuration
|
|
263
|
+
this.agentName = config.name || 'AI Agent';
|
|
264
|
+
this.systemPrompt = config.systemPrompt || 'You are a helpful AI assistant.';
|
|
265
|
+
this.responseType = config.responseType || 'text';
|
|
266
|
+
|
|
267
|
+
// Handle node cleanup
|
|
268
|
+
node.on('close', function (done) {
|
|
269
|
+
node.status({});
|
|
270
|
+
if (done) done();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Process incoming messages
|
|
274
|
+
node.on('input', async function (msg, send, done) {
|
|
275
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'processing...' });
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// 1. Validate AI configuration
|
|
279
|
+
const validationError = validateAIConfig(msg.aiagent);
|
|
280
|
+
if (validationError) {
|
|
281
|
+
throw new Error(validationError);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2. Get input
|
|
285
|
+
const input = msg.payload || {};
|
|
286
|
+
const inputText = typeof input === 'string' ? input : JSON.stringify(input);
|
|
287
|
+
|
|
288
|
+
// 3. Prepare prompt with context
|
|
289
|
+
const { messages, userMessage } = preparePrompt(node, msg, inputText);
|
|
290
|
+
|
|
291
|
+
// 4. Execute the prompt and get response
|
|
292
|
+
const response = await callAI(node, msg.aiagent, messages);
|
|
293
|
+
|
|
294
|
+
// 5. Update context if using memory
|
|
295
|
+
if (msg.aimemory && userMessage) {
|
|
296
|
+
updateContext(msg, userMessage, response);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 6. Format and send response
|
|
300
|
+
msg.payload = node.responseType === 'object' ? {
|
|
301
|
+
agent: node.agentName,
|
|
302
|
+
type: 'ai',
|
|
303
|
+
input: input,
|
|
304
|
+
response: response,
|
|
305
|
+
timestamp: new Date().toISOString(),
|
|
306
|
+
context: {
|
|
307
|
+
conversationLength: msg.aimemory?.context?.length || 0,
|
|
308
|
+
lastInteraction: new Date().toISOString(),
|
|
309
|
+
...(msg.aimemory && { aimemory: msg.aimemory })
|
|
310
|
+
}
|
|
311
|
+
} : response;
|
|
312
|
+
|
|
313
|
+
send(msg);
|
|
314
|
+
node.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
315
|
+
|
|
316
|
+
} catch (error) {
|
|
317
|
+
handleError(node, msg, error);
|
|
318
|
+
} finally {
|
|
319
|
+
if (done) done();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Register the node type
|
|
325
|
+
RED.nodes.registerType('ai-agent', AiAgentNode);
|
|
326
|
+
};
|