@peopl-health/nexus 1.0.2

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.
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Base Assistant class for consumer server extensions
3
+ * Provides common functionality for AI assistant implementations
4
+ */
5
+ class BaseAssistant {
6
+ constructor(config = {}) {
7
+ this.assistantId = config.assistantId;
8
+ this.llmClient = config.llmClient;
9
+ this.storage = config.storage;
10
+ this.nexus = config.nexus;
11
+ this.functions = new Map();
12
+ }
13
+
14
+ /**
15
+ * Register a function that can be called by the assistant
16
+ * @param {string} name - Function name
17
+ * @param {Function} handler - Function handler
18
+ * @param {Object} schema - Function schema for LLM
19
+ */
20
+ registerFunction(name, handler, schema) {
21
+ this.functions.set(name, { handler, schema });
22
+ }
23
+
24
+ /**
25
+ * Get all registered function schemas for LLM
26
+ * @returns {Array} Function schemas
27
+ */
28
+ getFunctionSchemas() {
29
+ return Array.from(this.functions.values()).map(f => f.schema);
30
+ }
31
+
32
+ /**
33
+ * Execute a function call from the assistant
34
+ * @param {string} name - Function name
35
+ * @param {Object} args - Function arguments
36
+ * @returns {Promise<any>} Function result
37
+ */
38
+ async executeFunction(name, args) {
39
+ const func = this.functions.get(name);
40
+ if (!func) {
41
+ throw new Error(`Unknown function: ${name}`);
42
+ }
43
+ return await func.handler(args);
44
+ }
45
+
46
+ /**
47
+ * Create a new thread for this assistant
48
+ * @param {string} userId - User identifier
49
+ * @param {Array} initialMessages - Initial messages
50
+ * @returns {Promise<Object>} Thread data
51
+ */
52
+ async createThread(userId, initialMessages = []) {
53
+ if (!this.llmClient) {
54
+ throw new Error('LLM client not configured');
55
+ }
56
+
57
+ const thread = await this.llmClient.beta.threads.create();
58
+
59
+ // Add initial messages if provided
60
+ for (const message of initialMessages) {
61
+ await this.llmClient.beta.threads.messages.create(
62
+ thread.id,
63
+ { role: "assistant", content: message }
64
+ );
65
+ }
66
+
67
+ const threadData = {
68
+ code: userId,
69
+ assistantId: this.assistantId,
70
+ threadId: thread.id,
71
+ active: true,
72
+ createdAt: new Date()
73
+ };
74
+
75
+ // Save to storage if available
76
+ if (this.storage) {
77
+ await this.storage.createThread(threadData);
78
+ }
79
+
80
+ return threadData;
81
+ }
82
+
83
+ /**
84
+ * Send message to assistant and handle response
85
+ * @param {string} userId - User identifier
86
+ * @param {string} message - User message
87
+ * @param {Object} options - Additional options
88
+ * @returns {Promise<string>} Assistant response
89
+ */
90
+ async sendMessage(userId, message, options = {}) {
91
+ if (!this.llmClient) {
92
+ throw new Error('LLM client not configured');
93
+ }
94
+
95
+ // Get or create thread
96
+ let threadData = null;
97
+ if (this.storage) {
98
+ threadData = await this.storage.getThread(userId);
99
+ }
100
+
101
+ if (!threadData) {
102
+ threadData = await this.createThread(userId);
103
+ }
104
+
105
+ // Add user message to thread
106
+ await this.llmClient.beta.threads.messages.create(
107
+ threadData.threadId,
108
+ { role: "user", content: message }
109
+ );
110
+
111
+ // Create run with function schemas if available
112
+ const runConfig = {
113
+ assistant_id: this.assistantId,
114
+ ...options
115
+ };
116
+
117
+ const functionSchemas = this.getFunctionSchemas();
118
+ if (functionSchemas.length > 0) {
119
+ runConfig.tools = functionSchemas;
120
+ }
121
+
122
+ const run = await this.llmClient.beta.threads.runs.create(
123
+ threadData.threadId,
124
+ runConfig
125
+ );
126
+
127
+ // Wait for completion and handle function calls
128
+ const result = await this.waitForCompletion(threadData.threadId, run.id);
129
+
130
+ if (result.status === 'completed') {
131
+ const messages = await this.llmClient.beta.threads.messages.list(
132
+ threadData.threadId,
133
+ { run_id: run.id }
134
+ );
135
+ return messages.data[0]?.content[0]?.text?.value || "";
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Wait for run completion and handle function calls
143
+ * @param {string} threadId - Thread ID
144
+ * @param {string} runId - Run ID
145
+ * @returns {Promise<Object>} Run result
146
+ */
147
+ async waitForCompletion(threadId, runId) {
148
+ const maxAttempts = 30;
149
+ let attempts = 0;
150
+
151
+ while (attempts < maxAttempts) {
152
+ const run = await this.llmClient.beta.threads.runs.retrieve(threadId, runId);
153
+
154
+ if (run.status === 'completed') {
155
+ return { status: 'completed', run };
156
+ } else if (run.status === 'requires_action') {
157
+ // Handle function calls
158
+ const toolOutputs = await this.handleRequiredActions(run);
159
+
160
+ if (toolOutputs.length > 0) {
161
+ await this.llmClient.beta.threads.runs.submitToolOutputs(
162
+ threadId,
163
+ runId,
164
+ { tool_outputs: toolOutputs }
165
+ );
166
+
167
+ // Continue waiting for completion
168
+ attempts = 0; // Reset attempts after submitting outputs
169
+ await this.delay(2000);
170
+ continue;
171
+ }
172
+ } else if (run.status === 'failed' || run.status === 'cancelled' || run.status === 'expired') {
173
+ return { status: 'failed', run };
174
+ }
175
+
176
+ await this.delay(2000);
177
+ attempts++;
178
+ }
179
+
180
+ throw new Error('Assistant run timeout');
181
+ }
182
+
183
+ /**
184
+ * Handle required actions (function calls)
185
+ * @param {Object} run - Run object with required actions
186
+ * @returns {Promise<Array>} Tool outputs
187
+ */
188
+ async handleRequiredActions(run) {
189
+ const toolCalls = run.required_action?.submit_tool_outputs?.tool_calls || [];
190
+ const toolOutputs = [];
191
+
192
+ for (const toolCall of toolCalls) {
193
+ try {
194
+ const functionName = toolCall.function.name;
195
+ const args = JSON.parse(toolCall.function.arguments);
196
+
197
+ const result = await this.executeFunction(functionName, args);
198
+
199
+ toolOutputs.push({
200
+ tool_call_id: toolCall.id,
201
+ output: JSON.stringify(result)
202
+ });
203
+ } catch (error) {
204
+ console.error(`Error executing function ${toolCall.function.name}:`, error);
205
+ toolOutputs.push({
206
+ tool_call_id: toolCall.id,
207
+ output: JSON.stringify({ error: error.message })
208
+ });
209
+ }
210
+ }
211
+
212
+ return toolOutputs;
213
+ }
214
+
215
+ /**
216
+ * Send a message through Nexus
217
+ * @param {string} to - Recipient
218
+ * @param {string} message - Message text
219
+ * @param {Object} options - Additional options
220
+ */
221
+ async sendNexusMessage(to, message, options = {}) {
222
+ if (!this.nexus) {
223
+ throw new Error('Nexus instance not provided');
224
+ }
225
+
226
+ return await this.nexus.sendMessage({
227
+ to,
228
+ message,
229
+ ...options
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Utility delay function
235
+ * @param {number} ms - Milliseconds to delay
236
+ */
237
+ delay(ms) {
238
+ return new Promise(resolve => setTimeout(resolve, ms));
239
+ }
240
+ }
241
+
242
+ module.exports = { BaseAssistant };
@@ -0,0 +1,111 @@
1
+ const { BaseAssistant } = require('./BaseAssistant');
2
+
3
+ /**
4
+ * Example Assistant implementation showing how to extend BaseAssistant
5
+ */
6
+ class ExampleAssistant extends BaseAssistant {
7
+ constructor(config) {
8
+ super(config);
9
+
10
+ // Register custom functions
11
+ this.registerFunction('get_weather', this.getWeather.bind(this), {
12
+ type: "function",
13
+ function: {
14
+ name: "get_weather",
15
+ description: "Get current weather for a location",
16
+ parameters: {
17
+ type: "object",
18
+ properties: {
19
+ location: {
20
+ type: "string",
21
+ description: "The city and state, e.g. San Francisco, CA"
22
+ }
23
+ },
24
+ required: ["location"]
25
+ }
26
+ }
27
+ });
28
+
29
+ this.registerFunction('schedule_reminder', this.scheduleReminder.bind(this), {
30
+ type: "function",
31
+ function: {
32
+ name: "schedule_reminder",
33
+ description: "Schedule a reminder for the user",
34
+ parameters: {
35
+ type: "object",
36
+ properties: {
37
+ message: {
38
+ type: "string",
39
+ description: "The reminder message"
40
+ },
41
+ datetime: {
42
+ type: "string",
43
+ description: "When to send the reminder (ISO format)"
44
+ }
45
+ },
46
+ required: ["message", "datetime"]
47
+ }
48
+ }
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Get weather information (mock implementation)
54
+ * @param {Object} args - Function arguments
55
+ * @returns {Promise<Object>} Weather data
56
+ */
57
+ async getWeather(args) {
58
+ const { location } = args;
59
+
60
+ // Mock weather data - replace with actual weather API
61
+ return {
62
+ location,
63
+ temperature: "22°C",
64
+ condition: "Sunny",
65
+ humidity: "65%",
66
+ timestamp: new Date().toISOString()
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Schedule a reminder (mock implementation)
72
+ * @param {Object} args - Function arguments
73
+ * @returns {Promise<Object>} Scheduling result
74
+ */
75
+ async scheduleReminder(args) {
76
+ const { message, datetime } = args;
77
+
78
+ // Mock scheduling - replace with actual scheduling logic
79
+ console.log(`Scheduling reminder: "${message}" for ${datetime}`);
80
+
81
+ return {
82
+ success: true,
83
+ reminderId: `reminder_${Date.now()}`,
84
+ scheduledFor: datetime,
85
+ message
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Handle incoming messages with custom logic
91
+ * @param {string} userId - User identifier
92
+ * @param {string} message - User message
93
+ * @returns {Promise<string>} Response
94
+ */
95
+ async handleMessage(userId, message) {
96
+ // Add custom pre-processing logic here
97
+ console.log(`ExampleAssistant handling message from ${userId}: ${message}`);
98
+
99
+ // Call parent method to handle LLM interaction
100
+ const response = await this.sendMessage(userId, message);
101
+
102
+ // Add custom post-processing logic here
103
+ if (response) {
104
+ console.log(`ExampleAssistant responding to ${userId}: ${response}`);
105
+ }
106
+
107
+ return response;
108
+ }
109
+ }
110
+
111
+ module.exports = { ExampleAssistant };
@@ -0,0 +1,7 @@
1
+ const { BaseAssistant } = require('./BaseAssistant');
2
+ const { ExampleAssistant } = require('./ExampleAssistant');
3
+
4
+ module.exports = {
5
+ BaseAssistant,
6
+ ExampleAssistant
7
+ };
@@ -0,0 +1,109 @@
1
+ const { Nexus } = require('@peopl/nexus');
2
+ require('dotenv').config();
3
+
4
+ async function main() {
5
+ const nexus = new Nexus();
6
+
7
+ try {
8
+ // Initialize with defaults (twilio provider, mongo storage, openai llm)
9
+ await nexus.initialize({
10
+ providerConfig: {
11
+ accountSid: process.env.TWILIO_ACCOUNT_SID,
12
+ authToken: process.env.TWILIO_AUTH_TOKEN,
13
+ phoneNumber: process.env.TWILIO_PHONE_NUMBER
14
+ },
15
+ // storage: 'mongo' is default
16
+ storageConfig: {
17
+ uri: process.env.MONGODB_URI,
18
+ dbName: 'nexus_basic'
19
+ },
20
+ // parser: 'MessageParser' is default
21
+ parserConfig: {
22
+ commandPrefixes: ['/', '!'],
23
+ keywords: ['help', 'support', 'info'],
24
+ flowTriggers: ['start survey', 'begin onboarding']
25
+ },
26
+ // llm: 'openai' is default (optional for basic usage)
27
+ llmConfig: {
28
+ apiKey: process.env.OPENAI_API_KEY
29
+ }
30
+ });
31
+
32
+ // Set up message handlers
33
+ nexus.onMessage(async (messageData) => {
34
+ console.log(`Received message from ${messageData.from}: ${messageData.message}`);
35
+
36
+ // Simple echo response
37
+ await nexus.sendMessage({
38
+ to: messageData.from,
39
+ message: `You said: ${messageData.message}`
40
+ });
41
+ });
42
+
43
+ nexus.onCommand(async (messageData) => {
44
+ const { command, args } = messageData.command;
45
+
46
+ switch (command) {
47
+ case 'help':
48
+ await nexus.sendMessage({
49
+ to: messageData.from,
50
+ message: 'Available commands:\n/help - Show this help\n/status - Check system status\n/echo [text] - Echo your text'
51
+ });
52
+ break;
53
+ case 'status':
54
+ await nexus.sendMessage({
55
+ to: messageData.from,
56
+ message: `System Status: ${nexus.isConnected() ? 'Connected' : 'Disconnected'}`
57
+ });
58
+ break;
59
+ case 'echo':
60
+ const text = args.join(' ') || 'Nothing to echo!';
61
+ await nexus.sendMessage({
62
+ to: messageData.from,
63
+ message: `Echo: ${text}`
64
+ });
65
+ break;
66
+ default:
67
+ await nexus.sendMessage({
68
+ to: messageData.from,
69
+ message: `Unknown command: ${command}. Type /help for available commands.`
70
+ });
71
+ }
72
+ });
73
+
74
+ nexus.onKeyword(async (messageData) => {
75
+ const keyword = messageData.keyword;
76
+
77
+ switch (keyword) {
78
+ case 'help':
79
+ case 'support':
80
+ await nexus.sendMessage({
81
+ to: messageData.from,
82
+ message: 'Hello! I can help you with:\n- Basic messaging\n- Commands (try /help)\n- Keyword responses\n\nWhat would you like to know?'
83
+ });
84
+ break;
85
+ case 'info':
86
+ await nexus.sendMessage({
87
+ to: messageData.from,
88
+ message: 'This is a basic Nexus messaging bot example. It demonstrates simple message handling without AI assistants.'
89
+ });
90
+ break;
91
+ }
92
+ });
93
+
94
+ console.log('Nexus basic example started successfully');
95
+ console.log('Send messages to your configured phone number to test');
96
+
97
+ } catch (error) {
98
+ console.error('Error starting Nexus:', error);
99
+ process.exit(1);
100
+ }
101
+ }
102
+
103
+ // Handle graceful shutdown
104
+ process.on('SIGINT', async () => {
105
+ console.log('\nShutting down gracefully...');
106
+ process.exit(0);
107
+ });
108
+
109
+ main().catch(console.error);
@@ -0,0 +1,206 @@
1
+ const { Nexus } = require('@peopl/nexus');
2
+ const { ExampleAssistant } = require('./assistants/ExampleAssistant');
3
+ const express = require('express');
4
+ require('dotenv').config();
5
+
6
+ const app = express();
7
+ app.use(express.json());
8
+ app.use(express.urlencoded({ extended: false }));
9
+
10
+
11
+ const nexus = new Nexus();
12
+ let assistants = {};
13
+
14
+ async function initializeNexus() {
15
+ await nexus.initialize({
16
+ provider: 'twilio',
17
+ providerConfig: {
18
+ accountSid: process.env.TWILIO_ACCOUNT_SID,
19
+ authToken: process.env.TWILIO_AUTH_TOKEN,
20
+ phoneNumber: process.env.TWILIO_PHONE_NUMBER
21
+ },
22
+ storageConfig: {
23
+ uri: process.env.MONGODB_URI,
24
+ dbName: 'nexus_messaging'
25
+ },
26
+ llmConfig: {
27
+ apiKey: process.env.OPENAI_API_KEY
28
+ }
29
+ });
30
+
31
+ assistants = {
32
+ sales: new ExampleAssistant({
33
+ assistantId: process.env.SALES_ASSISTANT_ID,
34
+ llmClient: nexus.getLLMProvider().getClient(),
35
+ storage: nexus.getStorage(),
36
+ nexus: nexus
37
+ }),
38
+ support: new ExampleAssistant({
39
+ assistantId: process.env.SUPPORT_ASSISTANT_ID,
40
+ llmClient: nexus.getLLMProvider().getClient(),
41
+ storage: nexus.getStorage(),
42
+ nexus: nexus
43
+ })
44
+ };
45
+
46
+ nexus.onMessage(async (messageData) => {
47
+ console.log('Received message:', messageData);
48
+
49
+ if (messageData.type === 'text') {
50
+ const response = await processTextMessage(messageData);
51
+ if (response) {
52
+ await nexus.sendMessage({
53
+ to: messageData.from,
54
+ message: response
55
+ });
56
+ }
57
+ }
58
+ });
59
+
60
+ nexus.onCommand(async (messageData) => {
61
+ const { command, args } = messageData.command;
62
+
63
+ switch (command) {
64
+ case 'help':
65
+ await nexus.sendMessage({
66
+ to: messageData.from,
67
+ message: `🤖 Available Commands:
68
+ /help - Show this help
69
+ /support - Connect to support assistant
70
+ /sales - Connect to sales assistant
71
+ /status - Check system status
72
+ /reset - Reset conversation`
73
+ });
74
+ break;
75
+
76
+ case 'support':
77
+ const supportResponse = await assistants.support.handleMessage(messageData.from, 'Hello, I need support.');
78
+ if (supportResponse) {
79
+ await nexus.sendMessage({
80
+ to: messageData.from,
81
+ message: supportResponse
82
+ });
83
+ }
84
+ break;
85
+
86
+ case 'sales':
87
+ const salesResponse = await assistants.sales.handleMessage(messageData.from, 'Hello, I\'m interested in your products.');
88
+ if (salesResponse) {
89
+ await nexus.sendMessage({
90
+ to: messageData.from,
91
+ message: salesResponse
92
+ });
93
+ }
94
+ break;
95
+
96
+ case 'status':
97
+ await nexus.sendMessage({
98
+ to: messageData.from,
99
+ message: `System Status: ${nexus.isConnected() ? '✅ Connected' : '❌ Disconnected'}`
100
+ });
101
+ break;
102
+
103
+ case 'reset':
104
+ if (nexus.getStorage()) {
105
+ await nexus.getStorage().clearThread(messageData.from);
106
+ }
107
+ await nexus.sendMessage({
108
+ to: messageData.from,
109
+ message: 'Conversation reset! You can start fresh now.'
110
+ });
111
+ break;
112
+
113
+ default:
114
+ await nexus.sendMessage({
115
+ to: messageData.from,
116
+ message: `Unknown command: /${command}. Type /help for available commands.`
117
+ });
118
+ }
119
+ });
120
+
121
+ console.log('🚀 Nexus consumer server initialized successfully');
122
+ }
123
+
124
+ // Webhook endpoints - provider agnostic
125
+ app.post('/webhook', async (req, res) => {
126
+ try {
127
+ await nexus.processMessage(req.body);
128
+ res.sendStatus(200);
129
+ } catch (error) {
130
+ console.error('Webhook error:', error);
131
+ res.sendStatus(500);
132
+ }
133
+ });
134
+
135
+ // API endpoints
136
+ app.post('/api/send-message', async (req, res) => {
137
+ try {
138
+ const { to, message, fileUrl, fileType } = req.body;
139
+
140
+ const result = await nexus.sendMessage({
141
+ to,
142
+ message,
143
+ fileUrl,
144
+ fileType
145
+ });
146
+
147
+ res.json({ success: true, result });
148
+ } catch (error) {
149
+ res.status(500).json({ success: false, error: error.message });
150
+ }
151
+ });
152
+
153
+ app.post('/api/send-template', async (req, res) => {
154
+ try {
155
+ const { to, contentSid, variables } = req.body;
156
+
157
+ const result = await nexus.sendMessage({
158
+ to,
159
+ contentSid,
160
+ variables
161
+ });
162
+
163
+ res.json({ success: true, result });
164
+ } catch (error) {
165
+ res.status(500).json({ success: false, error: error.message });
166
+ }
167
+ });
168
+
169
+ app.get('/api/status', (req, res) => {
170
+ res.json({
171
+ connected: nexus.isConnected(),
172
+ provider: process.env.MESSAGING_PROVIDER || 'twilio'
173
+ });
174
+ });
175
+
176
+ // Health check
177
+ app.get('/health', (req, res) => {
178
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
179
+ });
180
+
181
+ // Initialize and start server
182
+ async function start() {
183
+ try {
184
+ await initializeNexus();
185
+
186
+ const PORT = process.env.PORT || 3000;
187
+ app.listen(PORT, () => {
188
+ const provider = process.env.MESSAGING_PROVIDER || 'twilio';
189
+ console.log(`🚀 Consumer server running on port ${PORT}`);
190
+ console.log(`📱 Messaging provider: ${provider}`);
191
+ console.log(`🔗 Webhook URL: http://localhost:${PORT}/webhook`);
192
+ });
193
+ } catch (error) {
194
+ console.error('Failed to start server:', error);
195
+ process.exit(1);
196
+ }
197
+ }
198
+
199
+ // Graceful shutdown
200
+ process.on('SIGINT', async () => {
201
+ console.log('Shutting down gracefully...');
202
+ await nexus.disconnect();
203
+ process.exit(0);
204
+ });
205
+
206
+ start();