@igoruehara/canvas-flow 0.1.13 → 0.1.14

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.
@@ -1006,11 +1006,15 @@ export declare class RunnerController {
1006
1006
  } | {
1007
1007
  ok: boolean;
1008
1008
  received: number;
1009
+ synced: number;
1010
+ syncResults: any[];
1009
1011
  ignored: boolean;
1010
1012
  results?: undefined;
1011
1013
  } | {
1012
1014
  ok: boolean;
1013
1015
  received: any;
1016
+ synced: number;
1017
+ syncResults: any[];
1014
1018
  results: any[];
1015
1019
  ignored?: undefined;
1016
1020
  }>;
@@ -1033,11 +1037,15 @@ export declare class RunnerController {
1033
1037
  } | {
1034
1038
  ok: boolean;
1035
1039
  received: number;
1040
+ synced: number;
1041
+ syncResults: any[];
1036
1042
  ignored: boolean;
1037
1043
  results?: undefined;
1038
1044
  } | {
1039
1045
  ok: boolean;
1040
1046
  received: any;
1047
+ synced: number;
1048
+ syncResults: any[];
1041
1049
  results: any[];
1042
1050
  ignored?: undefined;
1043
1051
  }>;
@@ -132,6 +132,8 @@ export declare class RunnerService implements OnModuleInit, OnModuleDestroy {
132
132
  private refreshOpenAIClient;
133
133
  private getOpenAIClient;
134
134
  private normalizeFlowLlmProvider;
135
+ private isRuntimeLlmProviderConfigured;
136
+ private resolveRuntimeLlmProvider;
135
137
  private getOpenAIClientForProvider;
136
138
  private getChatModelForProvider;
137
139
  private flowLlmProvider;
@@ -1600,10 +1602,20 @@ export declare class RunnerService implements OnModuleInit, OnModuleDestroy {
1600
1602
  private normalizeFlowAttachmentFiles;
1601
1603
  private enrichFlowReplyDataWithAttachmentUrls;
1602
1604
  private extractFlowReplyData;
1605
+ private extractMetaWhatsappMessageText;
1606
+ private normalizeWhatsappPhone;
1607
+ private inferWhatsappHistoryRole;
1603
1608
  private extractMetaWhatsappMessages;
1604
1609
  private extractBlipWhatsappMessages;
1605
1610
  private extractSinchWhatsappMessages;
1606
1611
  private extractWhatsappMessages;
1612
+ private pushMetaWhatsappSyncMessage;
1613
+ private getMetaWhatsappMessageArray;
1614
+ private extractMetaWhatsappHistoryEvents;
1615
+ private extractMetaWhatsappSyncEvents;
1616
+ private extractWhatsappSyncEvents;
1617
+ private buildWhatsappSyncDedupeKey;
1618
+ private persistWhatsappSyncEvents;
1607
1619
  private getAssistantText;
1608
1620
  private whatsappDeliveryKey;
1609
1621
  private buildWhatsappDedupeKey;
@@ -1738,22 +1750,30 @@ export declare class RunnerService implements OnModuleInit, OnModuleDestroy {
1738
1750
  runWhatsappWebhook(flowId: string, payload: any): Promise<{
1739
1751
  ok: boolean;
1740
1752
  received: number;
1753
+ synced: number;
1754
+ syncResults: any[];
1741
1755
  ignored: boolean;
1742
1756
  results?: undefined;
1743
1757
  } | {
1744
1758
  ok: boolean;
1745
1759
  received: any;
1760
+ synced: number;
1761
+ syncResults: any[];
1746
1762
  results: any[];
1747
1763
  ignored?: undefined;
1748
1764
  }>;
1749
1765
  runWhatsappMainWebhook(agentId: string, payload: any): Promise<{
1750
1766
  ok: boolean;
1751
1767
  received: number;
1768
+ synced: number;
1769
+ syncResults: any[];
1752
1770
  ignored: boolean;
1753
1771
  results?: undefined;
1754
1772
  } | {
1755
1773
  ok: boolean;
1756
1774
  received: any;
1775
+ synced: number;
1776
+ syncResults: any[];
1757
1777
  results: any[];
1758
1778
  ignored?: undefined;
1759
1779
  }>;
@@ -173,12 +173,37 @@ let RunnerService = class RunnerService {
173
173
  return 'bedrock';
174
174
  return '';
175
175
  }
176
+ isRuntimeLlmProviderConfigured(settings, provider) {
177
+ if (provider === 'openai')
178
+ return Boolean(String(settings.openai?.apiKey || '').trim());
179
+ if (provider === 'azure') {
180
+ return Boolean(String(settings.azureOpenai?.endpoint || '').trim() &&
181
+ String(settings.azureOpenai?.apiKey || '').trim());
182
+ }
183
+ if (provider === 'gemini')
184
+ return Boolean(String(settings.gemini?.apiKey || '').trim());
185
+ if (provider === 'claude')
186
+ return Boolean(String(settings.claude?.apiKey || '').trim());
187
+ if (provider === 'grok')
188
+ return Boolean(String(settings.grok?.apiKey || '').trim());
189
+ if (provider === 'bedrock') {
190
+ return Boolean(String(settings.bedrock?.apiKey || '').trim() &&
191
+ String(settings.bedrock?.baseUrl || '').trim());
192
+ }
193
+ return false;
194
+ }
195
+ resolveRuntimeLlmProvider(settings, provider) {
196
+ const requested = this.normalizeFlowLlmProvider(provider);
197
+ if (requested && this.isRuntimeLlmProviderConfigured(settings, requested))
198
+ return requested;
199
+ return this.normalizeFlowLlmProvider(settings?.llmProvider || '') || requested || undefined;
200
+ }
176
201
  async getOpenAIClientForProvider(provider, agentId) {
177
202
  const normalized = this.normalizeFlowLlmProvider(provider);
178
203
  if (!normalized && !agentId)
179
204
  return await this.getOpenAIClient();
180
205
  const settings = await this.getProviderSettings(agentId);
181
- const runtime = this.providerConfigService.toOpenAIRuntimeConfig(settings, normalized);
206
+ const runtime = this.providerConfigService.toOpenAIRuntimeConfig(settings, this.resolveRuntimeLlmProvider(settings, normalized));
182
207
  return (0, openai_provider_1.createOpenAIClient)(this.configService, runtime);
183
208
  }
184
209
  async getChatModelForProvider(provider, model, agentId) {
@@ -186,7 +211,7 @@ let RunnerService = class RunnerService {
186
211
  if (!normalized && !agentId)
187
212
  return await this.getChatModel(model);
188
213
  const settings = await this.getProviderSettings(agentId);
189
- const runtime = this.providerConfigService.toOpenAIRuntimeConfig(settings, normalized);
214
+ const runtime = this.providerConfigService.toOpenAIRuntimeConfig(settings, this.resolveRuntimeLlmProvider(settings, normalized));
190
215
  return (0, openai_provider_1.getOpenAIChatModel)(this.configService, model, runtime);
191
216
  }
192
217
  flowLlmProvider(config, fallback) {
@@ -10392,6 +10417,10 @@ let RunnerService = class RunnerService {
10392
10417
  'businessAccountId',
10393
10418
  'phoneNumberId',
10394
10419
  'accessToken',
10420
+ 'embeddedSignupAppId',
10421
+ 'embeddedSignupConfigId',
10422
+ 'embeddedSignupAppSecret',
10423
+ 'embeddedSignupSolutionId',
10395
10424
  'blipContractId',
10396
10425
  'blipAuthorizationKey',
10397
10426
  'sinchProjectId',
@@ -10407,6 +10436,10 @@ let RunnerService = class RunnerService {
10407
10436
  return true;
10408
10437
  if (String(whatsapp.deliveryMode || 'provider') === 'apiResponse')
10409
10438
  return true;
10439
+ if (String(whatsapp.onboardingMode || 'manual') !== 'manual')
10440
+ return true;
10441
+ if (whatsapp.coexistenceEnabled === true || whatsapp.syncMessageEchoes === false || whatsapp.syncHistory === true)
10442
+ return true;
10410
10443
  if (whatsapp.autoReply === false)
10411
10444
  return true;
10412
10445
  if (String(whatsapp.sinchApiMode || 'conversation') === 'relay' || String(whatsapp.sinchApiMode || '') === 'broker')
@@ -12073,23 +12106,50 @@ let RunnerService = class RunnerService {
12073
12106
  const parsed = this.parsePossibleJsonValue(raw);
12074
12107
  return this.isPlainObject(parsed) ? await this.enrichFlowReplyDataWithAttachmentUrls(parsed, config, flowId) : undefined;
12075
12108
  }
12109
+ extractMetaWhatsappMessageText(message, flowReplyData) {
12110
+ return (message?.text?.body ||
12111
+ message?.button?.payload ||
12112
+ message?.button?.text ||
12113
+ message?.interactive?.button_reply?.id ||
12114
+ message?.interactive?.button_reply?.title ||
12115
+ message?.interactive?.list_reply?.id ||
12116
+ message?.interactive?.list_reply?.title ||
12117
+ (flowReplyData ? JSON.stringify(flowReplyData) : '') ||
12118
+ message?.interactive?.nfm_reply?.body ||
12119
+ message?.image?.caption ||
12120
+ message?.document?.caption ||
12121
+ message?.audio?.id ||
12122
+ message?.video?.caption ||
12123
+ message?.contacts?.[0]?.name?.formatted_name ||
12124
+ '');
12125
+ }
12126
+ normalizeWhatsappPhone(value) {
12127
+ return String(value || '').replace(/[^\d]/g, '');
12128
+ }
12129
+ inferWhatsappHistoryRole(message, customerPhone, businessPhone) {
12130
+ const from = this.normalizeWhatsappPhone(message?.from);
12131
+ const to = this.normalizeWhatsappPhone(message?.to || message?.recipient_id);
12132
+ const customer = this.normalizeWhatsappPhone(customerPhone);
12133
+ const business = this.normalizeWhatsappPhone(businessPhone);
12134
+ if (customer && from === customer)
12135
+ return 'user';
12136
+ if (customer && to === customer)
12137
+ return 'assistant';
12138
+ if (business && from === business)
12139
+ return 'assistant';
12140
+ return message?.from_me === true || message?.fromMe === true ? 'assistant' : 'user';
12141
+ }
12076
12142
  async extractMetaWhatsappMessages(payload, config, flowId) {
12077
12143
  const messages = [];
12078
12144
  for (const entry of payload?.entry || []) {
12079
12145
  for (const change of entry?.changes || []) {
12146
+ const field = String(change?.field || '');
12147
+ if (['smb_message_echoes', 'history', 'smb_app_state_sync'].includes(field))
12148
+ continue;
12080
12149
  const value = change?.value || {};
12081
12150
  for (const message of value?.messages || []) {
12082
12151
  const flowReplyData = await this.extractFlowReplyData(message, config, flowId);
12083
- const text = message?.text?.body ||
12084
- message?.button?.payload ||
12085
- message?.button?.text ||
12086
- message?.interactive?.button_reply?.id ||
12087
- message?.interactive?.button_reply?.title ||
12088
- message?.interactive?.list_reply?.id ||
12089
- message?.interactive?.list_reply?.title ||
12090
- (flowReplyData ? JSON.stringify(flowReplyData) : '') ||
12091
- message?.interactive?.nfm_reply?.body ||
12092
- '';
12152
+ const text = this.extractMetaWhatsappMessageText(message, flowReplyData);
12093
12153
  if (!text)
12094
12154
  continue;
12095
12155
  messages.push({
@@ -12158,6 +12218,216 @@ let RunnerService = class RunnerService {
12158
12218
  return this.extractSinchWhatsappMessages(payload);
12159
12219
  return await this.extractMetaWhatsappMessages(payload, config, flowId);
12160
12220
  }
12221
+ pushMetaWhatsappSyncMessage(events, params) {
12222
+ const message = params.message || {};
12223
+ const value = params.value || {};
12224
+ const metadata = value?.metadata || {};
12225
+ const text = this.extractMetaWhatsappMessageText(message);
12226
+ if (!text)
12227
+ return;
12228
+ const customerPhone = String(params.customerPhone ||
12229
+ message?.to ||
12230
+ message?.recipient_id ||
12231
+ message?.customer_phone_number ||
12232
+ message?.customerPhoneNumber ||
12233
+ message?.contacts?.[0]?.wa_id ||
12234
+ message?.from ||
12235
+ '').trim();
12236
+ const role = params.role || this.inferWhatsappHistoryRole(message, customerPhone, metadata?.display_phone_number);
12237
+ const from = role === 'assistant'
12238
+ ? customerPhone || String(message?.to || message?.recipient_id || message?.from || '').trim()
12239
+ : String(message?.from || customerPhone || '').trim();
12240
+ if (!from)
12241
+ return;
12242
+ events.push({
12243
+ provider: 'meta',
12244
+ syncKind: params.kind,
12245
+ role,
12246
+ from,
12247
+ text,
12248
+ messageId: message?.id,
12249
+ timestamp: message?.timestamp,
12250
+ phoneNumberId: metadata?.phone_number_id,
12251
+ displayPhoneNumber: metadata?.display_phone_number,
12252
+ raw: message,
12253
+ });
12254
+ }
12255
+ getMetaWhatsappMessageArray(value) {
12256
+ if (Array.isArray(value?.messages))
12257
+ return value.messages;
12258
+ if (Array.isArray(value?.message_echoes))
12259
+ return value.message_echoes;
12260
+ if (Array.isArray(value?.smb_message_echoes))
12261
+ return value.smb_message_echoes;
12262
+ if (Array.isArray(value?.data?.messages))
12263
+ return value.data.messages;
12264
+ if (this.isPlainObject(value?.message))
12265
+ return [value.message];
12266
+ return [];
12267
+ }
12268
+ extractMetaWhatsappHistoryEvents(events, historySource, value) {
12269
+ const histories = Array.isArray(historySource) ? historySource : [historySource].filter(Boolean);
12270
+ histories.forEach((history) => {
12271
+ const threads = Array.isArray(history?.threads) ? history.threads : [];
12272
+ threads.forEach((thread) => {
12273
+ const customerPhone = String(thread?.id || thread?.wa_id || thread?.phone || '').trim();
12274
+ const messages = Array.isArray(thread?.messages) ? thread.messages : [];
12275
+ messages.forEach((message) => {
12276
+ this.pushMetaWhatsappSyncMessage(events, {
12277
+ message,
12278
+ kind: 'history',
12279
+ value,
12280
+ customerPhone,
12281
+ });
12282
+ });
12283
+ });
12284
+ });
12285
+ }
12286
+ extractMetaWhatsappSyncEvents(payload, config) {
12287
+ const whatsapp = config?.whatsapp || {};
12288
+ const syncEchoes = whatsapp.syncMessageEchoes !== false;
12289
+ const syncHistory = whatsapp.syncHistory === true;
12290
+ const events = [];
12291
+ if (syncEchoes && String(payload?.event || '') === 'smb_message_echoes') {
12292
+ const data = payload?.data || {};
12293
+ this.getMetaWhatsappMessageArray(data).forEach((message) => {
12294
+ this.pushMetaWhatsappSyncMessage(events, {
12295
+ message,
12296
+ kind: 'message_echo',
12297
+ value: data,
12298
+ role: 'assistant',
12299
+ });
12300
+ });
12301
+ }
12302
+ if (syncHistory && String(payload?.event || '') === 'history') {
12303
+ const data = payload?.data || {};
12304
+ this.extractMetaWhatsappHistoryEvents(events, data?.history || data, data);
12305
+ }
12306
+ for (const entry of payload?.entry || []) {
12307
+ for (const change of entry?.changes || []) {
12308
+ const field = String(change?.field || '');
12309
+ const value = change?.value || {};
12310
+ if (field === 'smb_message_echoes' && syncEchoes) {
12311
+ this.getMetaWhatsappMessageArray(value).forEach((message) => {
12312
+ this.pushMetaWhatsappSyncMessage(events, {
12313
+ message,
12314
+ kind: 'message_echo',
12315
+ value,
12316
+ role: 'assistant',
12317
+ });
12318
+ });
12319
+ }
12320
+ if (field === 'history' && syncHistory) {
12321
+ this.extractMetaWhatsappHistoryEvents(events, value?.history || value, value);
12322
+ }
12323
+ }
12324
+ }
12325
+ return events;
12326
+ }
12327
+ async extractWhatsappSyncEvents(payload, config) {
12328
+ const provider = this.normalizeWhatsappProvider(config);
12329
+ if (provider !== 'meta')
12330
+ return [];
12331
+ return this.extractMetaWhatsappSyncEvents(payload, config);
12332
+ }
12333
+ buildWhatsappSyncDedupeKey(flowRecord, event) {
12334
+ const providerMessageId = String(event?.messageId || '').trim();
12335
+ const fallback = providerMessageId || (0, crypto_1.createHash)('sha1')
12336
+ .update(JSON.stringify({
12337
+ kind: event?.syncKind,
12338
+ role: event?.role,
12339
+ from: event?.from,
12340
+ text: event?.text,
12341
+ timestamp: event?.timestamp,
12342
+ }))
12343
+ .digest('hex');
12344
+ return [
12345
+ flowRecord?.organizationId || 'global',
12346
+ flowRecord?.agentId || 'default-agent',
12347
+ flowRecord?._id || 'flow',
12348
+ event?.provider || 'whatsapp',
12349
+ event?.syncKind || 'sync',
12350
+ fallback,
12351
+ ].join(':');
12352
+ }
12353
+ async persistWhatsappSyncEvents(flowRecord, flowId, events) {
12354
+ const results = [];
12355
+ for (const event of events) {
12356
+ const conversationId = `whatsapp-${event.from}`;
12357
+ const conversationOwnerId = `whatsapp:${event.from}`;
12358
+ const dedupeKey = this.buildWhatsappSyncDedupeKey(flowRecord, event);
12359
+ const dedupe = await this.sqsTransitionService.tryStartMessageDedupe({
12360
+ dedupeKey,
12361
+ organizationId: flowRecord?.organizationId,
12362
+ agentId: flowRecord?.agentId,
12363
+ flowId,
12364
+ conversationId,
12365
+ channel: 'whatsapp',
12366
+ provider: event.provider || 'meta',
12367
+ providerMessageId: event.messageId,
12368
+ });
12369
+ if (!dedupe.acquired) {
12370
+ results.push({
12371
+ from: event.from,
12372
+ messageId: event.messageId,
12373
+ syncKind: event.syncKind,
12374
+ duplicate: true,
12375
+ skipped: true,
12376
+ status: dedupe.status,
12377
+ });
12378
+ continue;
12379
+ }
12380
+ try {
12381
+ await this.memoryService.addTurn({
12382
+ agentId: flowRecord?.agentId,
12383
+ conversationId,
12384
+ role: event.role === 'assistant' ? 'assistant' : 'user',
12385
+ content: event.text,
12386
+ metadata: {
12387
+ kind: 'message',
12388
+ source: 'whatsapp_coexistence',
12389
+ syncKind: event.syncKind,
12390
+ organizationId: flowRecord?.organizationId,
12391
+ flowId,
12392
+ entryFlowId: flowId,
12393
+ activeFlowId: flowId,
12394
+ conversationOwnerId,
12395
+ whatsapp: {
12396
+ provider: event.provider || 'meta',
12397
+ from: event.from,
12398
+ messageId: event.messageId,
12399
+ phoneNumberId: event.phoneNumberId,
12400
+ displayPhoneNumber: event.displayPhoneNumber,
12401
+ timestamp: event.timestamp,
12402
+ syncKind: event.syncKind,
12403
+ },
12404
+ },
12405
+ });
12406
+ await this.sqsTransitionService.completeMessageDedupe(dedupeKey);
12407
+ results.push({
12408
+ from: event.from,
12409
+ messageId: event.messageId,
12410
+ syncKind: event.syncKind,
12411
+ role: event.role,
12412
+ synced: true,
12413
+ });
12414
+ }
12415
+ catch (error) {
12416
+ await this.sqsTransitionService.failMessageDedupe(dedupeKey, error);
12417
+ (0, observability_1.logEvent)('error', 'whatsapp.sync.failed', {
12418
+ flowId,
12419
+ agentId: flowRecord?.agentId,
12420
+ conversationId,
12421
+ provider: event.provider,
12422
+ providerMessageId: event.messageId,
12423
+ syncKind: event.syncKind,
12424
+ error: (0, observability_1.getErrorDetails)(error),
12425
+ });
12426
+ throw error;
12427
+ }
12428
+ }
12429
+ return results;
12430
+ }
12161
12431
  getAssistantText(messages) {
12162
12432
  return messages
12163
12433
  .filter((message) => message.role === 'assistant' && message.text)
@@ -14043,8 +14313,18 @@ let RunnerService = class RunnerService {
14043
14313
  const versionInfo = await this.canvasFlowService.resolveFlowVersionAsync(flowRecord, payload?.flowVersion || payload?.version || releaseFlowVersion);
14044
14314
  const config = await this.resolveRuntimeFlowConfig(versionInfo.config, flowRecord?.agentId, flowRecord?.organizationId);
14045
14315
  const messages = await this.extractWhatsappMessages(payload, config, String(flowRecord?._id || flowId));
14316
+ const syncEvents = await this.extractWhatsappSyncEvents(payload, config);
14317
+ const syncResults = syncEvents.length
14318
+ ? await this.persistWhatsappSyncEvents(flowRecord, String(flowRecord?._id || flowId), syncEvents)
14319
+ : [];
14046
14320
  if (!messages.length) {
14047
- return { ok: true, received: 0, ignored: true };
14321
+ return {
14322
+ ok: true,
14323
+ received: 0,
14324
+ synced: syncResults.length,
14325
+ syncResults,
14326
+ ignored: syncResults.length === 0,
14327
+ };
14048
14328
  }
14049
14329
  const results = [];
14050
14330
  for (const message of messages) {
@@ -14228,6 +14508,8 @@ let RunnerService = class RunnerService {
14228
14508
  return {
14229
14509
  ok: true,
14230
14510
  received: messages.length,
14511
+ synced: syncResults.length,
14512
+ syncResults,
14231
14513
  results,
14232
14514
  };
14233
14515
  }