@conversionpros/aiva 1.0.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.
Files changed (152) hide show
  1. package/README.md +148 -0
  2. package/auto-deploy.js +190 -0
  3. package/bin/aiva.js +81 -0
  4. package/cli-sync.js +126 -0
  5. package/d2a-prompt-template.txt +106 -0
  6. package/diagnostics-api.js +304 -0
  7. package/docs/ara-dedup-fix-scope.md +112 -0
  8. package/docs/ara-fix-round2-scope.md +61 -0
  9. package/docs/ara-greeting-fix-scope.md +70 -0
  10. package/docs/calendar-date-fix-scope.md +28 -0
  11. package/docs/getting-started.md +115 -0
  12. package/docs/network-architecture-rollout-scope.md +43 -0
  13. package/docs/scope-google-oauth-integration.md +351 -0
  14. package/docs/settings-page-scope.md +50 -0
  15. package/docs/xai-imagine-scope.md +116 -0
  16. package/docs/xai-voice-integration-scope.md +115 -0
  17. package/docs/xai-voice-tools-scope.md +165 -0
  18. package/email-router.js +512 -0
  19. package/follow-up-handler.js +606 -0
  20. package/gateway-monitor.js +158 -0
  21. package/google-email.js +379 -0
  22. package/google-oauth.js +310 -0
  23. package/grok-imagine.js +97 -0
  24. package/health-reporter.js +287 -0
  25. package/invisible-prefix-base.txt +206 -0
  26. package/invisible-prefix-owner.txt +26 -0
  27. package/invisible-prefix-slim.txt +10 -0
  28. package/invisible-prefix.txt +43 -0
  29. package/knowledge-base.js +472 -0
  30. package/lib/cli.js +19 -0
  31. package/lib/config.js +124 -0
  32. package/lib/health.js +57 -0
  33. package/lib/process.js +207 -0
  34. package/lib/server.js +42 -0
  35. package/lib/setup.js +472 -0
  36. package/meta-capi.js +206 -0
  37. package/meta-leads.js +411 -0
  38. package/notion-oauth.js +323 -0
  39. package/package.json +61 -0
  40. package/public/agent-config.html +241 -0
  41. package/public/aiva-avatar-anime.png +0 -0
  42. package/public/css/docs.css.bak +688 -0
  43. package/public/css/onboarding.css +543 -0
  44. package/public/diagrams/claude-subscription-pool.html +329 -0
  45. package/public/diagrams/claude-subscription-pool.png +0 -0
  46. package/public/docs-icon.png +0 -0
  47. package/public/escalation.html +237 -0
  48. package/public/group-config.html +300 -0
  49. package/public/icon-192.png +0 -0
  50. package/public/icon-512.png +0 -0
  51. package/public/icons/agents.svg +1 -0
  52. package/public/icons/attach.svg +1 -0
  53. package/public/icons/characters.svg +1 -0
  54. package/public/icons/chat.svg +1 -0
  55. package/public/icons/docs.svg +1 -0
  56. package/public/icons/heartbeat.svg +1 -0
  57. package/public/icons/messages.svg +1 -0
  58. package/public/icons/mic.svg +1 -0
  59. package/public/icons/notes.svg +1 -0
  60. package/public/icons/settings.svg +1 -0
  61. package/public/icons/tasks.svg +1 -0
  62. package/public/images/onboarding/p0-communication-layer.png +0 -0
  63. package/public/images/onboarding/p0-infinite-surface.png +0 -0
  64. package/public/images/onboarding/p0-learning-model.png +0 -0
  65. package/public/images/onboarding/p0-meet-aiva.png +0 -0
  66. package/public/images/onboarding/p4-contact-intelligence.png +0 -0
  67. package/public/images/onboarding/p4-context-compounds.png +0 -0
  68. package/public/images/onboarding/p4-message-router.png +0 -0
  69. package/public/images/onboarding/p4-per-contact-rules.png +0 -0
  70. package/public/images/onboarding/p4-send-messages.png +0 -0
  71. package/public/images/onboarding/p6-be-precise.png +0 -0
  72. package/public/images/onboarding/p6-review-escalations.png +0 -0
  73. package/public/images/onboarding/p6-voice-input.png +0 -0
  74. package/public/images/onboarding/p7-completion.png +0 -0
  75. package/public/index.html +11594 -0
  76. package/public/js/onboarding.js +699 -0
  77. package/public/manifest.json +24 -0
  78. package/public/messages-v2.html +2824 -0
  79. package/public/permission-approve.html.bak +107 -0
  80. package/public/permissions.html +150 -0
  81. package/public/styles/design-system.css +68 -0
  82. package/router-db.js +604 -0
  83. package/router-utils.js +28 -0
  84. package/router-v2/adapters/imessage.js +191 -0
  85. package/router-v2/adapters/quo.js +82 -0
  86. package/router-v2/adapters/whatsapp.js +192 -0
  87. package/router-v2/contact-manager.js +234 -0
  88. package/router-v2/conversation-engine.js +498 -0
  89. package/router-v2/data/knowledge-base.json +176 -0
  90. package/router-v2/data/router-v2.db +0 -0
  91. package/router-v2/data/router-v2.db-shm +0 -0
  92. package/router-v2/data/router-v2.db-wal +0 -0
  93. package/router-v2/data/router.db +0 -0
  94. package/router-v2/db.js +457 -0
  95. package/router-v2/escalation-bridge.js +540 -0
  96. package/router-v2/follow-up-engine.js +347 -0
  97. package/router-v2/index.js +441 -0
  98. package/router-v2/ingestion.js +213 -0
  99. package/router-v2/knowledge-base.js +231 -0
  100. package/router-v2/lead-qualifier.js +152 -0
  101. package/router-v2/learning-loop.js +202 -0
  102. package/router-v2/outbound-sender.js +160 -0
  103. package/router-v2/package.json +13 -0
  104. package/router-v2/permission-gate.js +86 -0
  105. package/router-v2/playbook.js +177 -0
  106. package/router-v2/prompts/base.js +52 -0
  107. package/router-v2/prompts/first-contact.js +38 -0
  108. package/router-v2/prompts/lead-qualification.js +37 -0
  109. package/router-v2/prompts/scheduling.js +72 -0
  110. package/router-v2/prompts/style-overrides.js +22 -0
  111. package/router-v2/scheduler.js +301 -0
  112. package/router-v2/scripts/migrate-v1-to-v2.js +215 -0
  113. package/router-v2/scripts/seed-faq.js +67 -0
  114. package/router-v2/seed-knowledge-base.js +39 -0
  115. package/router-v2/utils/ai.js +129 -0
  116. package/router-v2/utils/phone.js +52 -0
  117. package/router-v2/utils/response-validator.js +98 -0
  118. package/router-v2/utils/sanitize.js +222 -0
  119. package/router.js +5005 -0
  120. package/routes/google-calendar.js +186 -0
  121. package/scripts/deploy.sh +62 -0
  122. package/scripts/macos-calendar.sh +232 -0
  123. package/scripts/onboard-device.sh +466 -0
  124. package/server.js +5131 -0
  125. package/start.sh +24 -0
  126. package/templates/AGENTS.md +548 -0
  127. package/templates/IDENTITY.md +15 -0
  128. package/templates/docs-agents.html +132 -0
  129. package/templates/docs-app.html +130 -0
  130. package/templates/docs-home.html +83 -0
  131. package/templates/docs-imessage.html +121 -0
  132. package/templates/docs-tasks.html +123 -0
  133. package/templates/docs-tips.html +175 -0
  134. package/templates/getting-started.html +809 -0
  135. package/templates/invisible-prefix-base.txt +171 -0
  136. package/templates/invisible-prefix-owner.txt +282 -0
  137. package/templates/invisible-prefix.txt +338 -0
  138. package/templates/manifest.json +61 -0
  139. package/templates/memory-org/clients.md +7 -0
  140. package/templates/memory-org/credentials.md +9 -0
  141. package/templates/memory-org/devices.md +7 -0
  142. package/templates/updates.html +464 -0
  143. package/templates/workspace/AGENTS.md.tmpl +161 -0
  144. package/templates/workspace/HEARTBEAT.md.tmpl +17 -0
  145. package/templates/workspace/IDENTITY.md.tmpl +15 -0
  146. package/templates/workspace/MEMORY.md.tmpl +16 -0
  147. package/templates/workspace/SOUL.md.tmpl +51 -0
  148. package/templates/workspace/USER.md.tmpl +25 -0
  149. package/tts-proxy.js +96 -0
  150. package/voice-call-local.js +731 -0
  151. package/voice-call.js +732 -0
  152. package/wa-listener.js +354 -0
@@ -0,0 +1,498 @@
1
+ // ── Conversation Engine - Sonnet Context Assembly + Response ──
2
+ // The brain. Single AI call per message. Handles intent detection,
3
+ // response generation, and tool calls in one pass.
4
+ 'use strict';
5
+
6
+ const { getStmts, getSetting } = require('./db');
7
+ const { callSonnet } = require('./utils/ai');
8
+ const { buildBasePrompt } = require('./prompts/base');
9
+ const { buildSchedulingPrompt } = require('./prompts/scheduling');
10
+ const { buildLeadQualPrompt } = require('./prompts/lead-qualification');
11
+ const { buildFirstContactPrompt } = require('./prompts/first-contact');
12
+ const { buildStylePrompt } = require('./prompts/style-overrides');
13
+ const { buildPlaybookPrompt } = require('./playbook');
14
+ const { buildPreferencesPrompt } = require('./learning-loop');
15
+ const { checkToolPermission, canAutoReply, isBlocked } = require('./permission-gate');
16
+ const { getSchedulingRule } = require('./scheduler');
17
+ const { search: searchKnowledge } = require('./knowledge-base');
18
+ const { escalate, appendToEscalation, getActiveEscalation } = require('./escalation-bridge');
19
+ const { getContact, getOrCreateContact, updateContext, markIntroduced } = require('./contact-manager');
20
+ const { sendMessage } = require('./outbound-sender');
21
+ const { analyzeQualification, shouldEscalateForOutreach } = require('./lead-qualifier');
22
+ const { validateResponse } = require('./utils/response-validator');
23
+
24
+ /**
25
+ * Resolve the effective response mode for a contact using three-tier hierarchy:
26
+ * 1. Per-contact manual override (if set and not empty)
27
+ * 2. Per-channel default (imessage or whatsapp specific)
28
+ * 3. Global default (falls back to 'block')
29
+ * @param {Object} contact - Contact object with response_mode field
30
+ * @param {string} channel - Channel identifier ('imessage', 'whatsapp', etc.)
31
+ * @returns {string} The resolved response mode
32
+ */
33
+ function mv2ResolveEffectiveMode(contact, channel) {
34
+ // 1. Per-contact manual override
35
+ if (contact && contact.response_mode && contact.response_mode.trim() !== '') {
36
+ return contact.response_mode;
37
+ }
38
+ // 2. Per-channel default
39
+ if (channel === 'imessage') {
40
+ const imDefault = getSetting('imessageDefaultMode');
41
+ if (imDefault && imDefault.trim() !== '') return imDefault;
42
+ }
43
+ if (channel === 'whatsapp') {
44
+ const waDefault = getSetting('whatsappDefaultMode');
45
+ if (waDefault && waDefault.trim() !== '') return waDefault;
46
+ }
47
+ // 3. Global default
48
+ const globalDefault = getSetting('defaultResponseMode');
49
+ return (globalDefault && globalDefault.trim() !== '') ? globalDefault : 'block';
50
+ }
51
+
52
+ // Tool definitions for Sonnet
53
+ const TOOL_DEFINITIONS = [
54
+ {
55
+ type: 'function',
56
+ function: {
57
+ name: 'schedule_appointment',
58
+ description: 'MANDATORY tool for ALL scheduling actions. You MUST call this tool - never pretend to check availability or book without it.',
59
+ parameters: {
60
+ type: 'object',
61
+ properties: {
62
+ action: { type: 'string', enum: ['check_availability', 'book', 'reschedule', 'cancel'], description: 'Scheduling action' },
63
+ date: { type: 'string', description: 'Date in YYYY-MM-DD format (for check_availability)' },
64
+ startTime: { type: 'string', description: 'ISO datetime for appointment start (e.g. 2026-03-05T14:00:00-08:00)' },
65
+ endTime: { type: 'string', description: 'ISO datetime for appointment end (e.g. 2026-03-05T15:00:00-08:00)' },
66
+ duration: { type: 'number', description: 'Meeting duration in minutes (default 30)' },
67
+ summary: { type: 'string', description: 'Event title/summary' },
68
+ description: { type: 'string', description: 'Event description' },
69
+ attendeeEmail: { type: 'string', description: 'Email address of the attendee to invite' },
70
+ eventId: { type: 'string', description: 'Event ID for reschedule/cancel' },
71
+ newStart: { type: 'string', description: 'New start time for reschedule' },
72
+ newEnd: { type: 'string', description: 'New end time for reschedule' },
73
+ },
74
+ required: ['action'],
75
+ },
76
+ },
77
+ },
78
+ {
79
+ type: 'function',
80
+ function: {
81
+ name: 'search_knowledge_base',
82
+ description: 'Search FAQ and workspace files for information to answer a question.',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ query: { type: 'string', description: 'Search query' },
87
+ },
88
+ required: ['query'],
89
+ },
90
+ },
91
+ },
92
+ {
93
+ type: 'function',
94
+ function: {
95
+ name: 'escalate',
96
+ description: 'Escalate to the main agent when you cannot confidently answer. Do not acknowledge any delay to the contact.',
97
+ parameters: {
98
+ type: 'object',
99
+ properties: {
100
+ reason: { type: 'string', description: 'Why this needs escalation' },
101
+ isClientSupport: { type: 'boolean', description: 'True if this is a client reporting a bug/issue with their AIVA system' },
102
+ },
103
+ required: ['reason'],
104
+ },
105
+ },
106
+ },
107
+ ];
108
+
109
+ function log(msg, data) {
110
+ const ts = new Date().toISOString();
111
+ if (data) console.log(`[${ts}] [ENGINE] ${msg}`, JSON.stringify(data));
112
+ else console.log(`[${ts}] [ENGINE] ${msg}`);
113
+ }
114
+
115
+ /**
116
+ * Build the full system prompt with composable modules.
117
+ * Only injects modules when relevant - target ~800 tokens, max ~1500.
118
+ * @param {Object} contact - Contact record with context
119
+ * @param {Object} options - { conversationState, messageText }
120
+ * @returns {string}
121
+ */
122
+ function buildSystemPrompt(contact, options = {}) {
123
+ const parts = [];
124
+
125
+ // 1. Base prompt (always)
126
+ const awayMessage = getSetting('awayMessage');
127
+ parts.push(buildBasePrompt(contact, { awayMessage: awayMessage || undefined }));
128
+
129
+ // 2. Playbook voice + guardrails (always)
130
+ const playbookContext = {
131
+ topic: contact.context?.last_topic || '',
132
+ isLead: ['lead', 'qualified-lead'].includes(contact.category),
133
+ isPricing: /pric|cost|budget|rate|fee|charge|afford/i.test(options.messageText || ''),
134
+ isObjection: /expensive|too much|cheaper|can't afford|not sure/i.test(options.messageText || ''),
135
+ };
136
+ const playbook = buildPlaybookPrompt(null, playbookContext);
137
+ if (playbook) parts.push(playbook);
138
+
139
+ // 3. Learned preferences (if relevant matches exist)
140
+ const prefs = buildPreferencesPrompt(contact.phone, options.messageText || '');
141
+ if (prefs) parts.push(prefs);
142
+
143
+ // 4. Style override
144
+ const style = buildStylePrompt(contact);
145
+ if (style) parts.push(style);
146
+
147
+ // 5. First contact (if not introduced)
148
+ if (!contact.introduced) {
149
+ parts.push(buildFirstContactPrompt(contact));
150
+ }
151
+
152
+ // 6. Lead qualification (only for leads)
153
+ if (['lead', 'qualified-lead'].includes(contact.category)) {
154
+ parts.push(buildLeadQualPrompt(contact));
155
+ }
156
+
157
+ // 7. Scheduling context (only when relevant)
158
+ const state = options.conversationState?.state;
159
+ const isSchedulingRelevant = state === 'scheduling' ||
160
+ /schedul|book|appointment|meeting|call|availab|calendar|reschedul|cancel/i.test(options.messageText || '');
161
+ if (isSchedulingRelevant) {
162
+ const rule = getSchedulingRule(contact.category);
163
+ const stateData = state === 'scheduling' ? JSON.parse(options.conversationState?.state_data || '{}') : {};
164
+ parts.push(buildSchedulingPrompt(rule, stateData));
165
+ }
166
+
167
+ return parts.join('\n\n');
168
+ }
169
+
170
+ /**
171
+ * Build conversation history messages for the AI call.
172
+ * @param {string} phone
173
+ * @param {number} [limit=10]
174
+ * @returns {Array<{role: string, content: string}>}
175
+ */
176
+ function buildHistory(phone, limit = 10) {
177
+ const stmts = getStmts();
178
+ const messages = stmts.getRecentMessages.all(phone, limit);
179
+ // Messages come in DESC order, reverse for chronological
180
+ return messages.reverse().map(m => {
181
+ if (m.sent_by === 'contact') {
182
+ return { role: 'user', content: m.text };
183
+ }
184
+ if (m.sent_by === 'brandon') {
185
+ // Brandon's manual messages shown as assistant with attribution so AI knows
186
+ return { role: 'assistant', content: `[Brandon replied directly]: ${m.text}` };
187
+ }
188
+ return { role: 'assistant', content: m.text };
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Process a single inbound message through the conversation engine.
194
+ * This is the main entry point for message processing.
195
+ * @param {Object} message - Normalized inbound message
196
+ * @returns {Promise<void>}
197
+ */
198
+ async function processMessage(message) {
199
+ const { phone, text, channel } = message;
200
+
201
+ // Get or create contact
202
+ const contact = getOrCreateContact(phone, { source: channel });
203
+
204
+ // Resolve effective mode using three-tier hierarchy (FIRST - before permission checks)
205
+ const effectiveMode = mv2ResolveEffectiveMode(contact, channel);
206
+ log('Resolved mode', { phone, effectiveMode, contactMode: contact.response_mode, channel });
207
+
208
+ // Check for block mode
209
+ if (effectiveMode === 'block') {
210
+ log('Blocked by mode', { phone, effectiveMode });
211
+ return;
212
+ }
213
+
214
+ // Check for monitor-only mode
215
+ if (effectiveMode === 'monitor') {
216
+ log('Monitor only', { phone, effectiveMode });
217
+ return;
218
+ }
219
+
220
+ // Check if blocked by category
221
+ if (isBlocked(phone, contact)) {
222
+ log('Blocked contact', { phone });
223
+ return;
224
+ }
225
+
226
+ // Check auto-reply permission (only for 'auto' mode - escalate mode doesn't need it)
227
+ if (effectiveMode === 'auto' && !canAutoReply(phone)) {
228
+ log('Auto-reply disabled', { phone });
229
+ return;
230
+ }
231
+
232
+ // Handle escalate mode - directly escalate without AI processing
233
+ if (effectiveMode === 'escalate') {
234
+ log('Escalate mode - direct escalation', { phone });
235
+ const stmts = getStmts();
236
+ const recentMsgs = stmts.getRecentMessages.all(phone, 20);
237
+ await handleEscalation(contact, text, recentMsgs, 'Escalate mode - all messages for this contact are escalated', false);
238
+ runPostMessageProcessing(contact, phone, text, stmts);
239
+ return;
240
+ }
241
+
242
+ // Check for active escalation - append message
243
+ const activeEsc = getActiveEscalation(phone);
244
+ if (activeEsc) {
245
+ appendToEscalation(phone, text);
246
+ log('Appended to active escalation', { phone, escalationId: activeEsc.escalation_id });
247
+ return;
248
+ }
249
+
250
+ // Get conversation state
251
+ const stmts = getStmts();
252
+ stmts.clearExpiredStates.run();
253
+ const convState = stmts.getState.get(phone) || { state: 'idle', state_data: '{}' };
254
+
255
+ // Build system prompt
256
+ const systemPrompt = buildSystemPrompt(contact, {
257
+ conversationState: convState,
258
+ messageText: text,
259
+ });
260
+
261
+ // Build message history
262
+ const history = buildHistory(phone);
263
+ const messages = [
264
+ { role: 'system', content: systemPrompt },
265
+ ...history,
266
+ { role: 'user', content: text },
267
+ ];
268
+
269
+ // Filter tools based on contact's actual permissions
270
+ const allowedTools = TOOL_DEFINITIONS.filter(td => {
271
+ const toolName = td.function?.name;
272
+ if (!toolName) return false;
273
+ const perm = checkToolPermission(phone, toolName);
274
+ return perm.allowed;
275
+ });
276
+
277
+ // Build structured permission flags for the system prompt
278
+ const permissionFlags = {
279
+ CALENDAR_VIEW: allowedTools.some(t => t.function?.name === 'check_availability'),
280
+ CALENDAR_BOOK: allowedTools.some(t => t.function?.name === 'schedule_appointment'),
281
+ CALENDAR_MODIFY: allowedTools.some(t => t.function?.name === 'reschedule_appointment'),
282
+ REMINDERS: allowedTools.some(t => t.function?.name === 'create_reminder'),
283
+ TASKS: allowedTools.some(t => t.function?.name === 'create_task'),
284
+ FILE_SHARING: allowedTools.some(t => t.function?.name === 'send_file'),
285
+ MESSAGING: allowedTools.some(t => t.function?.name === 'send_message'),
286
+ KNOWLEDGE_BASE: true, // always enabled
287
+ ESCALATE: true, // always enabled
288
+ };
289
+
290
+ const flagBlock = Object.entries(permissionFlags)
291
+ .map(([key, enabled]) => `${key}: ${enabled ? 'ENABLED' : 'PROHIBITED'}`)
292
+ .join('\n');
293
+
294
+ const permissionNote = `
295
+
296
+ [CONTACT PERMISSIONS - STRICTLY ENFORCED]
297
+ ${flagBlock}
298
+
299
+ [PERMISSION RULES]
300
+ - ENABLED: You may use this capability freely
301
+ - PROHIBITED: You MUST NOT discuss, offer, promise, or imply this capability exists
302
+ - When a contact requests something PROHIBITED: use the escalate tool IMMEDIATELY
303
+ - Do NOT say "let me pass that along" or "I'll let Brandon know" for PROHIBITED items
304
+ - Do NOT acknowledge the request conversationally before escalating
305
+ - Simply use the escalate tool. The system handles the contact's experience.
306
+ - NEVER mention Brandon's name in relation to any PROHIBITED capability`;
307
+
308
+ // Append to system prompt
309
+ messages[0].content += permissionNote;
310
+
311
+ // Call Sonnet with only permitted tools
312
+ const callOpts = {
313
+ messages,
314
+ tools: allowedTools.length > 0 ? allowedTools : undefined,
315
+ maxTokens: 1000,
316
+ temperature: 0.7,
317
+ };
318
+
319
+ const result = await callSonnet(callOpts);
320
+
321
+ // Handle tool calls
322
+ if (result.toolCalls && result.toolCalls.length > 0) {
323
+ for (const toolCall of result.toolCalls) {
324
+ const fnName = toolCall.function?.name;
325
+ let args;
326
+ try {
327
+ args = JSON.parse(toolCall.function?.arguments || '{}');
328
+ } catch (parseErr) {
329
+ log('Failed to parse tool arguments', { phone, fnName, error: parseErr.message });
330
+ await handleEscalation(contact, text, stmts.getRecentMessages.all(phone, 20), 'Tool argument parse failure', false);
331
+ return;
332
+ }
333
+
334
+ // Permission check
335
+ const perm = checkToolPermission(phone, fnName);
336
+ if (perm.shouldEscalate) {
337
+ // Derive escalation type from tool name for contextual options
338
+ const toolTypeMap = {
339
+ schedule_appointment: 'scheduling', check_availability: 'scheduling',
340
+ reschedule_appointment: 'scheduling', cancel_appointment: 'scheduling',
341
+ create_task: 'task', create_reminder: 'task',
342
+ send_file: 'file_sharing', send_message: 'messaging',
343
+ };
344
+ await handleEscalation(contact, text, stmts.getRecentMessages.all(phone, 20), 'Permission denied for action', false, toolTypeMap[fnName] || null);
345
+ return;
346
+ }
347
+
348
+ if (fnName === 'escalate') {
349
+ const isClientSupport = args.isClientSupport || false;
350
+ await handleEscalation(contact, text, stmts.getRecentMessages.all(phone, 20), args.reason, isClientSupport);
351
+ runPostMessageProcessing(contact, phone, text, stmts);
352
+ return;
353
+ }
354
+
355
+ if (fnName === 'schedule_appointment') {
356
+ const scheduler = require('./scheduler');
357
+ const schedResult = await scheduler.handleToolCall(args, contact);
358
+
359
+ // Update state
360
+ stmts.upsertState.run(phone, 'scheduling', JSON.stringify(schedResult), null);
361
+
362
+ // Make a follow-up call with tool result
363
+ const followUpMessages = [
364
+ ...messages,
365
+ { role: 'assistant', content: null, tool_calls: [toolCall] },
366
+ { role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify(schedResult) },
367
+ ];
368
+
369
+ const followUp = await callSonnet({ messages: followUpMessages, maxTokens: 500 });
370
+ if (followUp.content) {
371
+ await sendResponse(phone, followUp.content, channel);
372
+ }
373
+ runPostMessageProcessing(contact, phone, text, stmts);
374
+ return;
375
+ }
376
+
377
+ if (fnName === 'search_knowledge_base') {
378
+ const kbResults = searchKnowledge(args.query);
379
+
380
+ // Make a follow-up call with search results
381
+ const followUpMessages = [
382
+ ...messages,
383
+ { role: 'assistant', content: null, tool_calls: [toolCall] },
384
+ { role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify(kbResults) },
385
+ ];
386
+
387
+ const followUp = await callSonnet({ messages: followUpMessages, maxTokens: 500 });
388
+ if (followUp.content) {
389
+ await sendResponse(phone, followUp.content, channel);
390
+ }
391
+ runPostMessageProcessing(contact, phone, text, stmts);
392
+ return;
393
+ }
394
+ }
395
+ }
396
+
397
+ // Direct response (no tool calls)
398
+ if (result.content) {
399
+ // Build permissions object from actual contact scopes
400
+ const permissions = {
401
+ canSchedule: allowedTools.some(t => t.function?.name === 'schedule_appointment'),
402
+ canCreateTask: allowedTools.some(t => t.function?.name === 'create_task'),
403
+ canCreateReminder: allowedTools.some(t => t.function?.name === 'create_reminder'),
404
+ canSendFile: allowedTools.some(t => t.function?.name === 'send_file'),
405
+ canSendMessage: allowedTools.some(t => t.function?.name === 'send_message'),
406
+ };
407
+
408
+ // Validate Sonnet's response against actual permissions
409
+ const validation = await validateResponse(result.content, permissions);
410
+
411
+ if (!validation.valid) {
412
+ // Sonnet overpromised - suppress response, send brief ack, escalate
413
+ log('Response overpromises capabilities - suppressing', { phone, capability: validation.reason });
414
+
415
+ const acks = [
416
+ "Let me check on that",
417
+ "Let me look into that",
418
+ "One sec, let me check",
419
+ "Give me a moment to check on that",
420
+ ];
421
+ const ack = acks[Math.floor(Math.random() * acks.length)];
422
+ await sendResponse(phone, ack, channel);
423
+
424
+ await handleEscalation(contact, text, stmts.getRecentMessages.all(phone, 20),
425
+ `Sonnet's response overpromised ${validation.reason} capability. Original response suppressed. Contact asked: "${text}"`, false, validation.reason);
426
+ } else {
427
+ await sendResponse(phone, result.content, channel);
428
+ }
429
+
430
+ // Update conversation state to active
431
+ stmts.upsertState.run(phone, 'active', '{}', null);
432
+
433
+ // Mark as introduced if first contact
434
+ if (!contact.introduced) {
435
+ markIntroduced(phone);
436
+ }
437
+ } else {
438
+ log('No response from Sonnet', { phone });
439
+ }
440
+
441
+ // Post-message: update context and run lead qualification (async, non-blocking)
442
+ runPostMessageProcessing(contact, phone, text, stmts);
443
+ }
444
+
445
+ /**
446
+ * Run post-message processing (context update + lead qualification).
447
+ * Extracted so all code paths can call it.
448
+ */
449
+ function runPostMessageProcessing(contact, phone, text, stmts) {
450
+ updateContextPostMessage(contact, text).catch(err => log('Context update error', { error: err.message }));
451
+
452
+ if (['lead', 'qualified-lead'].includes(contact.category)) {
453
+ const recentMsgs = stmts.getRecentMessages.all(phone, 15);
454
+ analyzeQualification(contact, recentMsgs.reverse()).catch(err => log('Lead qual error', { error: err.message }));
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Send a response to a contact and update state.
460
+ */
461
+ async function sendResponse(phone, text, channel) {
462
+ await sendMessage({
463
+ phone,
464
+ text,
465
+ channel,
466
+ sentBy: 'aiva',
467
+ stateAtTime: 'auto-reply',
468
+ });
469
+ }
470
+
471
+ /**
472
+ * Handle escalation (from escalate tool call or permission denial).
473
+ */
474
+ async function handleEscalation(contact, triggerMessage, recentMessages, reason, isClientSupport, escalationType = null) {
475
+ const context = getStmts().getContext.get(contact.phone) || {};
476
+
477
+ await escalate({
478
+ contact,
479
+ triggerMessage,
480
+ conversationHistory: recentMessages,
481
+ contactContext: context,
482
+ isClientSupport,
483
+ escalationType,
484
+ });
485
+
486
+ log('Escalated', { phone: contact.phone, reason, isClientSupport, escalationType });
487
+ }
488
+
489
+ /**
490
+ * Update contact context after processing a message.
491
+ */
492
+ async function updateContextPostMessage(contact, messageText) {
493
+ updateContext(contact.phone, {
494
+ last_topic: messageText.substring(0, 100),
495
+ });
496
+ }
497
+
498
+ module.exports = { processMessage, buildSystemPrompt, buildHistory, TOOL_DEFINITIONS, mv2ResolveEffectiveMode };
@@ -0,0 +1,176 @@
1
+ [
2
+ {
3
+ "category": "business_info",
4
+ "question": "What is Conversion Marketing Pros?",
5
+ "answer": "Conversion Marketing Pros, also called Conversion Pros or CMP, is a technology company based in Spokane, Washington. We build custom AI assistants, software, and websites for small to mid-size businesses. Our founder Brandon Burgan started the company to help business owners stop drowning in admin work and focus on what actually makes them money.",
6
+ "keywords": "conversion marketing pros cmp company about"
7
+ },
8
+ {
9
+ "category": "business_info",
10
+ "question": "What services does CMP offer?",
11
+ "answer": "We offer three main things: AIVA (your personal AI assistant that handles calls, texts, scheduling, and admin), custom software builds tailored to your business, and premium hand-coded websites. We also do auto-blogging for SEO, continuous care plans, and CRM builds. Everything we build, you own.",
12
+ "keywords": "services offer what do you do"
13
+ },
14
+ {
15
+ "category": "business_info",
16
+ "question": "Who is Brandon Burgan?",
17
+ "answer": "Brandon Burgan is the founder of Conversion Marketing Pros. He is based in Spokane, Washington and has been building software and AI solutions for small businesses. He started the company after building a proof of concept for Spokane House Plans and seeing how much AI could transform small business operations.",
18
+ "keywords": "brandon burgan founder owner who"
19
+ },
20
+ {
21
+ "category": "business_info",
22
+ "question": "Where is CMP located?",
23
+ "answer": "We are based in Spokane, Washington. We work with clients locally and remotely across the country.",
24
+ "keywords": "location where spokane washington address"
25
+ },
26
+ {
27
+ "category": "business_info",
28
+ "question": "What are your business hours?",
29
+ "answer": "Our team is generally available Monday through Friday during normal business hours, Pacific Time. AIVA, your AI assistant, is available 24/7 and can handle messages, scheduling, and questions any time of day or night.",
30
+ "keywords": "hours availability when open schedule business hours"
31
+ },
32
+ {
33
+ "category": "aiva_product",
34
+ "question": "What is AIVA?",
35
+ "answer": "AIVA is your AI-powered personal assistant. She handles your calls, texts, scheduling, follow-ups, and day-to-day admin so you can focus on running your business. Think of her as a chief of staff that never sleeps, never forgets, and costs a fraction of a real employee.",
36
+ "keywords": "aiva what is assistant ai"
37
+ },
38
+ {
39
+ "category": "aiva_product",
40
+ "question": "How does AIVA work?",
41
+ "answer": "AIVA runs on a dedicated device that we set up for you. She connects to your phone, calendar, and business tools. When someone texts or calls, AIVA can respond on your behalf, book appointments, answer questions about your business, and keep you in the loop on everything. You stay in control and can jump in any time.",
42
+ "keywords": "how does aiva work technology setup"
43
+ },
44
+ {
45
+ "category": "aiva_product",
46
+ "question": "What can AIVA do for my business?",
47
+ "answer": "AIVA can respond to leads and customer inquiries automatically, schedule and manage appointments, send follow-up messages, qualify leads through natural conversation, answer common questions about your business, and keep your calendar organized. She frees you up to focus on meeting clients and growing your business instead of being buried in admin.",
48
+ "keywords": "aiva features capabilities benefits business"
49
+ },
50
+ {
51
+ "category": "aiva_product",
52
+ "question": "How much does AIVA cost?",
53
+ "answer": "AIVA is $1,500 per month. Setup fees depend on your contract length. For a monthly plan, setup is $2,000. For a 6-month contract, setup is $1,500. For a 12-month contract, setup is $1,000. The longer you commit, the less you pay upfront.",
54
+ "keywords": "cost price pricing how much money subscription"
55
+ },
56
+ {
57
+ "category": "aiva_product",
58
+ "question": "What is included in the AIVA subscription?",
59
+ "answer": "Your $1,500 monthly subscription includes the AI assistant running 24/7, a dedicated device, automatic message handling, appointment scheduling, lead qualification, follow-up sequences, knowledge base for your business, and ongoing updates and improvements. Everything you need to run AIVA without lifting a finger.",
60
+ "keywords": "included subscription what do i get features"
61
+ },
62
+ {
63
+ "category": "aiva_product",
64
+ "question": "How long is the AIVA contract?",
65
+ "answer": "We offer three options: monthly (no long-term commitment), 6-month, and 12-month contracts. All contracts auto-renew for the same term length unless you give us 30 days notice before it ends. The longer contracts come with lower setup fees.",
66
+ "keywords": "contract length term commitment how long"
67
+ },
68
+ {
69
+ "category": "aiva_product",
70
+ "question": "What are the AIVA setup fees?",
71
+ "answer": "Setup fees cover your dedicated device and onboarding. Monthly plan is $2,000 setup. 6-month contract is $1,500 setup. 12-month contract is $1,000 setup. The setup includes getting your device configured, connecting your accounts, and a coaching session to get you started.",
72
+ "keywords": "setup fees onboarding initialization cost upfront"
73
+ },
74
+ {
75
+ "category": "services",
76
+ "question": "Do you build websites?",
77
+ "answer": "Yes! We build premium hand-coded websites. These are not template or drag-and-drop sites. They are fully coded for maximum performance, beautiful design, and perfect SEO scores. Websites typically ship in about 4 days. Website builds are a separate project with their own scope of work and pricing.",
78
+ "keywords": "website web design build site"
79
+ },
80
+ {
81
+ "category": "services",
82
+ "question": "Do you build CRMs or custom software?",
83
+ "answer": "Absolutely. Custom software is one of our biggest strengths. We build CRMs, project management tools, client portals, invoicing systems, and more. Everything is tailored to your specific business needs and you own the software when the project is complete. We embed AI into the foundation of everything we build.",
84
+ "keywords": "crm software custom build application"
85
+ },
86
+ {
87
+ "category": "services",
88
+ "question": "Do you work with my industry?",
89
+ "answer": "We work with small to mid-size businesses across all industries. We have built solutions for real estate, financial services, food service, pet care, home services, and more. Every new industry is an opportunity to build something great. If you have a business problem, we can probably solve it.",
90
+ "keywords": "industry vertical niche sector type of business"
91
+ },
92
+ {
93
+ "category": "services",
94
+ "question": "What makes CMP different from other agencies?",
95
+ "answer": "Three things. First, we embed AI into the foundation of everything we build, not as an afterthought. Second, you own everything we build for you. No lock-in, no hostage software. Third, we are a small team that actually cares about your results. We are not a big agency checking boxes. We solve real problems for real business owners.",
96
+ "keywords": "different unique why cmp competitive advantage"
97
+ },
98
+ {
99
+ "category": "services",
100
+ "question": "How much does a website cost?",
101
+ "answer": "A standard website build is $4,500. Additional front-end sites are $3,500 each. We also offer auto-blogging at $300 per month for ongoing SEO content. Website builds are separate from the AIVA subscription and come with their own scope of work.",
102
+ "keywords": "website cost price how much web"
103
+ },
104
+ {
105
+ "category": "services",
106
+ "question": "How much does custom software cost?",
107
+ "answer": "Software builds vary depending on scope and complexity. We will walk you through your specific needs, identify the biggest pain points, and put together a proposal. Everything breaks down into manageable monthly payments over time so it feels more like a subscription than a big upfront cost. We always start with your biggest problem first and prove the model before expanding.",
108
+ "keywords": "software cost price how much custom build"
109
+ },
110
+ {
111
+ "category": "common_questions",
112
+ "question": "How do I get started?",
113
+ "answer": "The best way to get started is to schedule a call with us. We will talk about your business, understand your pain points, and figure out the best solution. From there we put together a proposal and if it makes sense, we get moving. No pressure, no hard sell. We want it to be the right fit for both of us.",
114
+ "keywords": "get started begin sign up how start onboarding"
115
+ },
116
+ {
117
+ "category": "common_questions",
118
+ "question": "Can I cancel my AIVA subscription?",
119
+ "answer": "You can cancel with 30 days notice before your contract term ends. If you cancel early, the remaining balance on your contract is still owed. We do this because we invest real resources into setting up your device and system. That said, we also offer a 60-day satisfaction guarantee from final delivery, so there is built-in protection for you.",
120
+ "keywords": "cancel subscription stop end terminate"
121
+ },
122
+ {
123
+ "category": "common_questions",
124
+ "question": "Do you offer a trial or demo?",
125
+ "answer": "We do not offer a free trial, but we do have a 60-day satisfaction guarantee from final delivery. That means once everything is set up and running, you have 60 days to make sure you are happy with it. We are confident you will love it, and we stand behind our work.",
126
+ "keywords": "trial demo free test try"
127
+ },
128
+ {
129
+ "category": "common_questions",
130
+ "question": "What if I am not satisfied?",
131
+ "answer": "We offer a 60-day satisfaction guarantee from the date of final delivery. If you are not happy within that window, we will work with you to make it right. We want every client to feel great about their investment.",
132
+ "keywords": "not satisfied unhappy refund guarantee"
133
+ },
134
+ {
135
+ "category": "common_questions",
136
+ "question": "Who owns the code and intellectual property?",
137
+ "answer": "CMP owns the code during the life of the contract. Once you fully complete your contract, you receive the source code and it is yours to keep. This is how we protect our investment while making sure you end up owning everything we built for you.",
138
+ "keywords": "own code ip intellectual property ownership source"
139
+ },
140
+ {
141
+ "category": "common_questions",
142
+ "question": "How is payment handled?",
143
+ "answer": "Payment is due on the 1st of each month via Stripe. We accept credit card, ACH bank transfer, and Klarna. Cash payments require owner approval. There is a 3-day grace period for late payments, after which your service may be suspended.",
144
+ "keywords": "payment pay billing stripe how when"
145
+ },
146
+ {
147
+ "category": "common_questions",
148
+ "question": "Do I own the device AIVA runs on?",
149
+ "answer": "No, the device is owned and managed by CMP. We handle all maintenance, updates, and security on the device. This keeps everything running smoothly and means you never have to worry about the technical side. If your subscription ends, the device is recycled.",
150
+ "keywords": "device own hardware equipment mac"
151
+ },
152
+ {
153
+ "category": "common_questions",
154
+ "question": "What happens if I stop paying?",
155
+ "answer": "If payment is not received after the 3-day grace period, your AIVA service will be suspended. If the balance remains unpaid, the service will be terminated and the remaining contract balance is still owed. The device will be recycled. We always try to work with you before it gets to that point.",
156
+ "keywords": "stop paying late payment delinquent suspended"
157
+ },
158
+ {
159
+ "category": "common_questions",
160
+ "question": "Can AIVA be shared between multiple people?",
161
+ "answer": "Each AIVA subscription is for one person. Your assistant is personalized to you, your contacts, your calendar, and your business. Sharing would compromise the quality and personalization that makes AIVA effective.",
162
+ "keywords": "share multiple people users one person"
163
+ },
164
+ {
165
+ "category": "services",
166
+ "question": "Do you offer ongoing support after a build?",
167
+ "answer": "Yes. Software builds come with a 90-day warranty plus 90 days of bug fixes after delivery. We also offer continuous care plans at $300 per month for ongoing improvements and quarterly updates. We do not just build it and walk away.",
168
+ "keywords": "support warranty maintenance ongoing care after"
169
+ },
170
+ {
171
+ "category": "services",
172
+ "question": "Do you handle third-party API costs?",
173
+ "answer": "No. Any third-party API costs like MLS data feeds, payment processors, or other vendor subscriptions are set up under your own account. You pay the vendor directly. We build the integration, but you own the data relationship at cost with no markup from us. Think of it like a Costco membership. You get wholesale prices directly from the source.",
174
+ "keywords": "api costs third party vendor integration pricing"
175
+ }
176
+ ]
Binary file
Binary file