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/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
+ };