@toothfairyai/cli 1.0.3 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toothfairyai/cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.7",
4
4
  "description": "Command-line interface for ToothFairy AI API",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -43,7 +43,8 @@
43
43
  "dotenv": "^16.0.3",
44
44
  "js-yaml": "^4.1.0",
45
45
  "ora": "^5.4.1",
46
- "table": "^6.8.1"
46
+ "table": "^6.8.1",
47
+ "eventsource": "^2.0.2"
47
48
  },
48
49
  "devDependencies": {
49
50
  "eslint": "^8.32.0",
package/src/api.js CHANGED
@@ -1,14 +1,15 @@
1
- const axios = require("axios");
1
+ const axios = require('axios');
2
2
 
3
3
  class ToothFairyAPI {
4
- constructor(baseUrl, aiUrl, apiKey, workspaceId, verbose = false) {
4
+ constructor(baseUrl, aiUrl, aiStreamUrl, apiKey, workspaceId, verbose = false) {
5
5
  this.baseUrl = baseUrl;
6
6
  this.aiUrl = aiUrl;
7
+ this.aiStreamUrl = aiStreamUrl;
7
8
  this.workspaceId = workspaceId;
8
9
  this.verbose = verbose;
9
10
  this.headers = {
10
- "Content-Type": "application/json",
11
- "x-api-key": apiKey,
11
+ 'Content-Type': 'application/json',
12
+ 'x-api-key': apiKey,
12
13
  };
13
14
  }
14
15
 
@@ -19,46 +20,46 @@ class ToothFairyAPI {
19
20
  headers: this.headers,
20
21
  };
21
22
 
22
- if (method === "POST" || method === "PUT") {
23
+ if (method === 'POST' || method === 'PUT') {
23
24
  if (data) {
24
25
  data = { workspaceid: this.workspaceId, ...data };
25
26
  }
26
27
  config.data = data;
27
- } else if (method === "GET" && data) {
28
+ } else if (method === 'GET' && data) {
28
29
  // For GET requests with data, add as query parameters
29
30
  const params = new URLSearchParams(data);
30
31
  config.url += `?${params.toString()}`;
31
32
  }
32
33
 
33
34
  if (this.verbose) {
34
- const chalk = require("chalk");
35
- console.error(chalk.dim("\n--- API Request Debug ---"));
35
+ const chalk = require('chalk');
36
+ console.error(chalk.dim('\n--- API Request Debug ---'));
36
37
  console.error(chalk.dim(`Method: ${method}`));
37
38
  console.error(chalk.dim(`URL: ${config.url}`));
38
39
  console.error(chalk.dim(`Headers: ${JSON.stringify(config.headers, null, 2)}`));
39
40
  if (config.data) {
40
41
  console.error(chalk.dim(`Data: ${JSON.stringify(config.data, null, 2)}`));
41
42
  }
42
- console.error(chalk.dim("----------------------\n"));
43
+ console.error(chalk.dim('----------------------\n'));
43
44
  }
44
45
 
45
46
  try {
46
47
  const response = await axios(config);
47
48
 
48
49
  if (this.verbose) {
49
- const chalk = require("chalk");
50
- console.error(chalk.dim("\n--- API Response Debug ---"));
50
+ const chalk = require('chalk');
51
+ console.error(chalk.dim('\n--- API Response Debug ---'));
51
52
  console.error(chalk.dim(`Status: ${response.status} ${response.statusText}`));
52
53
  console.error(chalk.dim(`Response Headers: ${JSON.stringify(response.headers, null, 2)}`));
53
54
  console.error(chalk.dim(`Response Data: ${JSON.stringify(response.data, null, 2)}`));
54
- console.error(chalk.dim("------------------------\n"));
55
+ console.error(chalk.dim('------------------------\n'));
55
56
  }
56
57
 
57
58
  return response.data;
58
59
  } catch (error) {
59
60
  if (this.verbose) {
60
- const chalk = require("chalk");
61
- console.error(chalk.red("\n--- API Error Debug ---"));
61
+ const chalk = require('chalk');
62
+ console.error(chalk.red('\n--- API Error Debug ---'));
62
63
  console.error(chalk.red(`Error: ${error.message}`));
63
64
  if (error.response) {
64
65
  console.error(chalk.red(`Status: ${error.response.status} ${error.response.statusText}`));
@@ -68,13 +69,13 @@ class ToothFairyAPI {
68
69
  if (error.request) {
69
70
  console.error(chalk.red(`Request Config: ${JSON.stringify(error.config, null, 2)}`));
70
71
  }
71
- console.error(chalk.red("---------------------\n"));
72
+ console.error(chalk.red('---------------------\n'));
72
73
  }
73
74
 
74
75
  if (error.response) {
75
76
  throw new Error(
76
77
  `HTTP ${error.response.status}: ${
77
- error.response.data.message || "API request failed"
78
+ error.response.data.message || 'API request failed'
78
79
  }`
79
80
  );
80
81
  }
@@ -83,34 +84,34 @@ class ToothFairyAPI {
83
84
  }
84
85
 
85
86
  async createChat(chatData) {
86
- return this._makeRequest("POST", "chat/create", chatData);
87
+ return this._makeRequest('POST', 'chat/create', chatData);
87
88
  }
88
89
 
89
90
  async updateChat(chatData) {
90
- return this._makeRequest("POST", "chat/update", chatData);
91
+ return this._makeRequest('POST', 'chat/update', chatData);
91
92
  }
92
93
 
93
94
  async getChat(chatId) {
94
- return this._makeRequest("GET", `chat/get/${chatId}`);
95
+ return this._makeRequest('GET', `chat/get/${chatId}`);
95
96
  }
96
97
 
97
98
  async createMessage(messageData) {
98
- return this._makeRequest("POST", "chat_message/create", messageData);
99
+ return this._makeRequest('POST', 'chat_message/create', messageData);
99
100
  }
100
101
 
101
102
  async getMessage(messageId) {
102
- return this._makeRequest("GET", `chat_message/get/${messageId}`);
103
+ return this._makeRequest('GET', `chat_message/get/${messageId}`);
103
104
  }
104
105
 
105
106
  async getAllChats() {
106
- return this._makeRequest("GET", "chat/list", {
107
+ return this._makeRequest('GET', 'chat/list', {
107
108
  workspaceid: this.workspaceId,
108
109
  });
109
110
  }
110
111
 
111
112
  async getAgentResponse(agentData) {
112
113
  const config = {
113
- method: "POST",
114
+ method: 'POST',
114
115
  url: `${this.aiUrl}/chatter`,
115
116
  headers: this.headers,
116
117
  data: { workspaceid: this.workspaceId, ...agentData },
@@ -123,7 +124,7 @@ class ToothFairyAPI {
123
124
  if (error.response) {
124
125
  throw new Error(
125
126
  `HTTP ${error.response.status}: ${
126
- error.response.data.message || "Agent request failed"
127
+ error.response.data.message || 'Agent request failed'
127
128
  }`
128
129
  );
129
130
  }
@@ -145,14 +146,14 @@ class ToothFairyAPI {
145
146
  customerId ||
146
147
  `cli-user-${
147
148
  Math.abs(
148
- message.split("").reduce((a, b) => {
149
+ message.split('').reduce((a, b) => {
149
150
  a = (a << 5) - a + b.charCodeAt(0);
150
151
  return a & a;
151
152
  }, 0)
152
153
  ) % 10000
153
154
  }`;
154
- phoneNumber = phoneNumber || "+1234567890";
155
- providerId = providerId || "default-sms-provider";
155
+ phoneNumber = phoneNumber || '+1234567890';
156
+ providerId = providerId || 'default-sms-provider';
156
157
 
157
158
  const chatData = {
158
159
  name: customerId,
@@ -176,8 +177,8 @@ class ToothFairyAPI {
176
177
  const messageData = {
177
178
  chatID: createdChat.id,
178
179
  text: message,
179
- role: "user",
180
- userID: "CLI",
180
+ role: 'user',
181
+ userID: 'CLI',
181
182
  };
182
183
  const createdMessage = await this.createMessage(messageData);
183
184
  // console.log(`Message created: ${createdMessage.id}`);;
@@ -188,7 +189,7 @@ class ToothFairyAPI {
188
189
  {
189
190
  text: createdMessage.text,
190
191
  role: createdMessage.role,
191
- userID: createdMessage.userID || "System User",
192
+ userID: createdMessage.userID || 'System User',
192
193
  },
193
194
  ],
194
195
  agentid: agentId,
@@ -209,7 +210,7 @@ class ToothFairyAPI {
209
210
 
210
211
  async searchDocuments(text, topK = 10, metadata = null) {
211
212
  if (topK < 1 || topK > 50) {
212
- throw new Error("topK must be between 1 and 50");
213
+ throw new Error('topK must be between 1 and 50');
213
214
  }
214
215
 
215
216
  const searchData = {
@@ -222,7 +223,7 @@ class ToothFairyAPI {
222
223
  }
223
224
 
224
225
  const config = {
225
- method: "POST",
226
+ method: 'POST',
226
227
  url: `${this.aiUrl}/searcher`,
227
228
  headers: this.headers,
228
229
  data: { workspaceid: this.workspaceId, ...searchData },
@@ -235,13 +236,247 @@ class ToothFairyAPI {
235
236
  if (error.response) {
236
237
  throw new Error(
237
238
  `HTTP ${error.response.status}: ${
238
- error.response.data.message || "Search request failed"
239
+ error.response.data.message || 'Search request failed'
239
240
  }`
240
241
  );
241
242
  }
242
243
  throw error;
243
244
  }
244
245
  }
246
+
247
+ /**
248
+ * Send a message to an agent and get a streaming response.
249
+ *
250
+ * This method handles the complete workflow with Server-Sent Events (SSE):
251
+ * 1. Creates a chat
252
+ * 2. Creates a message
253
+ * 3. Streams the agent response in real-time
254
+ *
255
+ * @param {string} message - The message to send to the agent
256
+ * @param {string} agentId - The ID of the agent to send the message to
257
+ * @param {string|null} phoneNumber - Phone number for SMS channel
258
+ * @param {string|null} customerId - Customer identifier
259
+ * @param {string|null} providerId - SMS provider ID
260
+ * @param {Object} customerInfo - Additional customer information
261
+ * @param {Function} onEvent - Callback function called for each event
262
+ * @returns {Promise<void>} - Promise resolves when streaming is complete
263
+ *
264
+ * Event Types Explained:
265
+ * - 'status': Connection status updates ('connected', 'complete')
266
+ * - 'progress': Agent processing status updates:
267
+ * * 'init': Agent initialization started
268
+ * * 'initial_setup_completed': Basic setup finished
269
+ * * 'tools_processing_completed': Tools processing finished
270
+ * * 'replying': Agent is generating response (text streaming starts)
271
+ * * 'updating_memory': Agent is updating conversation memory
272
+ * * 'memory_updated': Memory update completed
273
+ * - 'data': Actual response text chunks (progressive text building)
274
+ * - 'complete': Final response with all metadata
275
+ * - 'error': Error occurred during streaming
276
+ *
277
+ * The onEvent callback receives: (eventType, eventData) => {}
278
+ */
279
+ async sendMessageToAgentStream(
280
+ message,
281
+ agentId,
282
+ phoneNumber = null,
283
+ customerId = null,
284
+ providerId = null,
285
+ customerInfo = {},
286
+ onEvent
287
+ ) {
288
+ try {
289
+ // Use defaults for optional parameters
290
+ customerId =
291
+ customerId ||
292
+ `cli-user-${
293
+ Math.abs(
294
+ message.split('').reduce((a, b) => {
295
+ a = (a << 5) - a + b.charCodeAt(0);
296
+ return a & a;
297
+ }, 0)
298
+ ) % 10000
299
+ }`;
300
+ phoneNumber = phoneNumber || '+1234567890';
301
+ providerId = providerId || 'default-sms-provider';
302
+
303
+ // Create chat first
304
+ const chatData = {
305
+ name: customerId,
306
+ primaryRole: agentId,
307
+ externalParticipantId: phoneNumber,
308
+ channelSettings: {
309
+ sms: {
310
+ isEnabled: true,
311
+ recipient: phoneNumber,
312
+ providerID: providerId,
313
+ },
314
+ },
315
+ customerId: customerId,
316
+ customerInfo: customerInfo,
317
+ isAIReplying: true,
318
+ };
319
+
320
+ const createdChat = await this.createChat(chatData);
321
+ console.debug(`Chat created: ${createdChat.id}`);
322
+
323
+ // Create message
324
+ const messageData = {
325
+ chatID: createdChat.id,
326
+ text: message,
327
+ role: 'user',
328
+ userID: 'CLI',
329
+ };
330
+ const createdMessage = await this.createMessage(messageData);
331
+ console.debug(`Message created: ${createdMessage.id}`);
332
+
333
+ // Prepare agent data for streaming
334
+ const agentData = {
335
+ workspaceid: this.workspaceId,
336
+ messages: [
337
+ {
338
+ text: createdMessage.text,
339
+ role: createdMessage.role,
340
+ userID: createdMessage.userID || 'System User',
341
+ },
342
+ ],
343
+ agentid: agentId,
344
+ };
345
+
346
+ // Stream the agent response using the dedicated streaming URL
347
+ const streamUrl = `${this.aiStreamUrl}/agent`; // Using streaming URL for /agent endpoint
348
+
349
+
350
+ return new Promise((resolve, reject) => {
351
+ // For POST requests with EventSource, we need to use a different approach
352
+ // EventSource doesn't support POST requests directly, so we'll use axios with streaming
353
+ const config = {
354
+ method: 'POST',
355
+ url: streamUrl,
356
+ headers: {
357
+ ...this.headers,
358
+ 'Accept': 'text/event-stream',
359
+ 'Cache-Control': 'no-cache',
360
+ },
361
+ data: agentData,
362
+ responseType: 'stream',
363
+ timeout: 300000, // 5 minute timeout
364
+ };
365
+
366
+ axios(config)
367
+ .then(response => {
368
+ let buffer = '';
369
+
370
+ response.data.on('data', (chunk) => {
371
+ buffer += chunk.toString();
372
+
373
+ // Process complete lines
374
+ const lines = buffer.split('\n');
375
+ buffer = lines.pop() || ''; // Keep the incomplete line in buffer
376
+
377
+ for (const line of lines) {
378
+ if (line.trim() === '') continue;
379
+
380
+ if (line.startsWith('data: ')) {
381
+ const dataStr = line.slice(6); // Remove 'data: ' prefix
382
+
383
+ try {
384
+ const eventData = JSON.parse(dataStr);
385
+
386
+ // Determine event type based on the data structure
387
+ if (eventData.status) {
388
+ if (eventData.status === 'connected') {
389
+ onEvent('status', eventData);
390
+ } else if (eventData.status === 'complete') {
391
+ onEvent('status', eventData);
392
+ } else if (eventData.status === 'inProgress') {
393
+ // Parse metadata to understand what's happening
394
+ let metadata = {};
395
+ if (eventData.metadata) {
396
+ try {
397
+ metadata = JSON.parse(eventData.metadata);
398
+ } catch (e) {
399
+ metadata = { raw_metadata: eventData.metadata };
400
+ }
401
+ }
402
+
403
+ // Determine progress type
404
+ if (metadata.agent_processing_status) {
405
+ const processingStatus = metadata.agent_processing_status;
406
+ onEvent('progress', {
407
+ ...eventData,
408
+ processing_status: processingStatus,
409
+ metadata_parsed: metadata
410
+ });
411
+ } else {
412
+ onEvent('progress', { ...eventData, metadata_parsed: metadata });
413
+ }
414
+
415
+ } else if (eventData.status === 'fulfilled') {
416
+ // Final response with complete data
417
+ onEvent('complete', eventData);
418
+ }
419
+
420
+ } else if (eventData.text && eventData.type === 'message') {
421
+ // This is streaming text data
422
+ onEvent('data', eventData);
423
+
424
+ } else if (eventData.type === 'message' && eventData.images !== undefined) {
425
+ // Additional message metadata (images, files, etc.)
426
+ onEvent('metadata', eventData);
427
+
428
+ } else if (eventData.type === 'message' && eventData.callbackMetadata) {
429
+ // Callback metadata with function details and execution plan
430
+ onEvent('callback', eventData);
431
+
432
+ } else {
433
+ // Generic event data
434
+ onEvent('unknown', eventData);
435
+ }
436
+
437
+ } catch (e) {
438
+ console.error(`Failed to parse SSE data: ${dataStr}, error: ${e.message}`);
439
+ onEvent('error', {
440
+ error: 'json_decode_error',
441
+ raw_data: dataStr,
442
+ message: e.message
443
+ });
444
+ }
445
+ }
446
+ }
447
+ });
448
+
449
+ response.data.on('end', () => {
450
+ resolve();
451
+ });
452
+
453
+ response.data.on('error', (error) => {
454
+ onEvent('error', {
455
+ error: 'stream_error',
456
+ message: error.message
457
+ });
458
+ reject(error);
459
+ });
460
+
461
+ })
462
+ .catch(error => {
463
+ onEvent('error', {
464
+ error: 'request_error',
465
+ message: error.message
466
+ });
467
+ reject(error);
468
+ });
469
+ });
470
+
471
+ } catch (error) {
472
+ console.error(`Error in sendMessageToAgentStream: ${error.message}`);
473
+ onEvent('error', {
474
+ error: 'setup_error',
475
+ message: error.message
476
+ });
477
+ throw error;
478
+ }
479
+ }
245
480
  }
246
481
 
247
482
  module.exports = ToothFairyAPI;
package/src/config.js CHANGED
@@ -4,9 +4,10 @@ const os = require('os');
4
4
  const yaml = require('js-yaml');
5
5
 
6
6
  class ToothFairyConfig {
7
- constructor(baseUrl, aiUrl, apiKey, workspaceId) {
7
+ constructor(baseUrl, aiUrl, aiStreamUrl, apiKey, workspaceId) {
8
8
  this.baseUrl = baseUrl;
9
9
  this.aiUrl = aiUrl;
10
+ this.aiStreamUrl = aiStreamUrl;
10
11
  this.apiKey = apiKey;
11
12
  this.workspaceId = workspaceId;
12
13
  }
@@ -15,6 +16,7 @@ class ToothFairyConfig {
15
16
  return new ToothFairyConfig(
16
17
  process.env.TF_BASE_URL || 'https://api.toothfairyai.com',
17
18
  process.env.TF_AI_URL || 'https://ai.toothfairyai.com',
19
+ process.env.TF_AI_STREAM_URL || 'https://ais.toothfairyai.com',
18
20
  process.env.TF_API_KEY || '',
19
21
  process.env.TF_WORKSPACE_ID || ''
20
22
  );
@@ -37,6 +39,7 @@ class ToothFairyConfig {
37
39
  return new ToothFairyConfig(
38
40
  data.base_url || 'https://api.toothfairyai.com',
39
41
  data.ai_url || 'https://ai.toothfairyai.com',
42
+ data.ai_stream_url || 'https://ais.toothfairyai.com',
40
43
  data.api_key || '',
41
44
  data.workspace_id || ''
42
45
  );
@@ -50,6 +53,7 @@ class ToothFairyConfig {
50
53
  return {
51
54
  base_url: this.baseUrl,
52
55
  ai_url: this.aiUrl,
56
+ ai_stream_url: this.aiStreamUrl,
53
57
  api_key: this.apiKey,
54
58
  workspace_id: this.workspaceId
55
59
  };