@xuda.io/ai_module 1.1.5641 → 1.1.5643
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/index.mjs +233 -82
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -337,6 +337,8 @@ const drive_ms = await import(`${module_path}/drive_module/index_ms.mjs`);
|
|
|
337
337
|
const jobs_ms = await import(`${module_path}/jobs_module/index_ms.mjs`);
|
|
338
338
|
const team_ms = await import(`${module_path}/team_module/index_ms.mjs`);
|
|
339
339
|
const email_ms = await import(`${module_path}/email_module/index_ms.mjs`);
|
|
340
|
+
// Profile broadcasts send SMS through the same primitive Phone Manager uses.
|
|
341
|
+
const voice_ms = await import(`${module_path}/voice_module/index_ms.mjs`);
|
|
340
342
|
const api_ms = await import(`${module_path}/api_module/index_ms.mjs`);
|
|
341
343
|
const stripe_ms = await import(`${module_path}/stripe_module/index_ms.mjs`);
|
|
342
344
|
|
|
@@ -5272,13 +5274,31 @@ const _tl_voice_rows = async function (uid, account_profile_info, contact_id, us
|
|
|
5272
5274
|
privilege: true,
|
|
5273
5275
|
});
|
|
5274
5276
|
|
|
5277
|
+
// What the call was actually about, so the row says something beyond its duration.
|
|
5278
|
+
// The caller's LONGEST turn, because the opening turns are "Hello?" and whatever noise
|
|
5279
|
+
// the speech recogniser picked up before the greeting, while the request itself is the
|
|
5280
|
+
// one sentence they actually spoke at length.
|
|
5281
|
+
const call_gist = (d) => {
|
|
5282
|
+
const turns = Array.isArray(d.transcript) ? d.transcript : [];
|
|
5283
|
+
const said = turns
|
|
5284
|
+
.filter((t) => t.role === 'caller' || t.role === 'user')
|
|
5285
|
+
.map((t) => String(t.text || '').replace(/\s+/g, ' ').trim())
|
|
5286
|
+
.filter(Boolean);
|
|
5287
|
+
const text = said.sort((a, b) => b.length - a.length)[0] || '';
|
|
5288
|
+
return text.length > 140 ? `${text.slice(0, 139)}…` : text;
|
|
5289
|
+
};
|
|
5290
|
+
// Stored as an object ({ type, id }) by the realtime path but sent as a plain string on
|
|
5291
|
+
// the socket event, and the UI compares it to 'ai'. Normalize to the string form here so
|
|
5292
|
+
// a row reads the same however the call was recorded.
|
|
5293
|
+
const handled_kind = (d) => (typeof d.handled_by === 'object' && d.handled_by ? d.handled_by.type || '' : d.handled_by || '');
|
|
5275
5294
|
const call_rows = (calls?.docs || []).filter((d) => d.status !== 'ringing' && mine(d)).map((d) =>
|
|
5276
5295
|
row('call', d, d.direction === 'inbound' ? 'Incoming call' : 'Outgoing call', {
|
|
5277
5296
|
status: d.status,
|
|
5278
5297
|
duration_sec: d.duration_sec || 0,
|
|
5279
5298
|
has_recording: !!d.recording,
|
|
5280
5299
|
transcript_turns: (d.transcript || []).length,
|
|
5281
|
-
|
|
5300
|
+
transcript_preview: call_gist(d),
|
|
5301
|
+
handled_by: handled_kind(d),
|
|
5282
5302
|
}),
|
|
5283
5303
|
);
|
|
5284
5304
|
const sms_rows = (messages?.docs || []).filter(mine).map((d) => row('sms', d, d.body || '', { status: d.status, segments: d.segments || 1 }));
|
|
@@ -5403,7 +5423,23 @@ export const submit_chat_conversation = async function (req, job_id, headers) {
|
|
|
5403
5423
|
}
|
|
5404
5424
|
|
|
5405
5425
|
case 'account_profiles': {
|
|
5406
|
-
|
|
5426
|
+
// This used to call ai_chat_conversation unconditionally, so Note, Chat, SMS and
|
|
5427
|
+
// Email on a profile all silently became AI chats. A profile thread is a group
|
|
5428
|
+
// thread: each mode records one item here, and the broadcast modes also deliver
|
|
5429
|
+
// to every member. See profile_thread_post.
|
|
5430
|
+
switch (conversation_doc.conversation_type) {
|
|
5431
|
+
case 'note':
|
|
5432
|
+
case 'chat':
|
|
5433
|
+
case 'sms':
|
|
5434
|
+
case 'email': {
|
|
5435
|
+
ret = await profile_thread_post(req, job_id, headers, conversation_doc.conversation_type);
|
|
5436
|
+
break;
|
|
5437
|
+
}
|
|
5438
|
+
default: {
|
|
5439
|
+
ret = await ai_chat_conversation(req, job_id, headers);
|
|
5440
|
+
break;
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5407
5443
|
break;
|
|
5408
5444
|
}
|
|
5409
5445
|
|
|
@@ -5575,6 +5611,149 @@ const chat_note = async function (req, job_id, headers) {
|
|
|
5575
5611
|
}
|
|
5576
5612
|
};
|
|
5577
5613
|
|
|
5614
|
+
// ── Account-profile group thread ────────────────────────────────────────────────────────
|
|
5615
|
+
// A profile conversation is ONE group thread, not a set of per-member threads (Boaz,
|
|
5616
|
+
// 2026-07-31: "only on the profile like group chat"). Every mode records a single item on
|
|
5617
|
+
// the profile conversation. SMS and email additionally deliver that text to each member,
|
|
5618
|
+
// but those sends are a delivery detail: nothing is written to a member's own contact
|
|
5619
|
+
// timeline, and inbound replies belong back on this same profile conversation.
|
|
5620
|
+
|
|
5621
|
+
// Resolve the profile's members to one delivery address each, then send per member.
|
|
5622
|
+
// Returns a per-recipient outcome rather than a bare boolean, because a broadcast that
|
|
5623
|
+
// only half-succeeded must not be recorded as if it had reached everyone.
|
|
5624
|
+
const profile_broadcast_recipients = async function (owner_uid, profile_id, kind) {
|
|
5625
|
+
const member_uids = await account_ms.get_account_profile_group_member_uids(owner_uid, profile_id);
|
|
5626
|
+
// The owner is not a member of their own profile and must not receive their own broadcast.
|
|
5627
|
+
const uids = [...new Set((member_uids || []).filter((u) => u && u !== owner_uid))];
|
|
5628
|
+
const out = [];
|
|
5629
|
+
for (const member_uid of uids) {
|
|
5630
|
+
let doc = null;
|
|
5631
|
+
try {
|
|
5632
|
+
doc = await db_module.get_couch_doc_native('xuda_accounts', member_uid);
|
|
5633
|
+
} catch (_) {
|
|
5634
|
+
doc = null;
|
|
5635
|
+
}
|
|
5636
|
+
const info = doc?.account_info || {};
|
|
5637
|
+
const name = [info.name, info.last_name].filter(Boolean).join(' ') || info.email || member_uid;
|
|
5638
|
+
const address = kind === 'sms' ? info.phone_number : info.email;
|
|
5639
|
+
out.push({ uid: member_uid, name, address: address || null });
|
|
5640
|
+
}
|
|
5641
|
+
return out;
|
|
5642
|
+
};
|
|
5643
|
+
|
|
5644
|
+
const profile_broadcast = async function (uid, account_profile_info, profile_id, kind, body) {
|
|
5645
|
+
const recipients = await profile_broadcast_recipients(uid, profile_id, kind);
|
|
5646
|
+
const results = [];
|
|
5647
|
+
|
|
5648
|
+
// One SMS-capable number for the whole broadcast, resolved once. Without a number there
|
|
5649
|
+
// is nothing to send from, and that is a configuration error worth surfacing rather than
|
|
5650
|
+
// reporting every member as individually unreachable.
|
|
5651
|
+
let voice_number_id = null;
|
|
5652
|
+
if (kind === 'sms') {
|
|
5653
|
+
const numbers = await voice_ms.list_voice_numbers({ uid, profile_id });
|
|
5654
|
+
const usable = (numbers?.data || []).find((n) => n.capabilities?.SMS === true || n.capabilities?.sms === true);
|
|
5655
|
+
if (!usable) throw new Error('This profile has no phone number that can send SMS');
|
|
5656
|
+
voice_number_id = usable.id;
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
const profile_name = account_profile_info.account_profile_obj?.profile_name || 'your team';
|
|
5660
|
+
for (const recipient of recipients) {
|
|
5661
|
+
if (!recipient.address) {
|
|
5662
|
+
results.push({ ...recipient, ok: false, error: kind === 'sms' ? 'no phone number on file' : 'no email address on file' });
|
|
5663
|
+
continue;
|
|
5664
|
+
}
|
|
5665
|
+
try {
|
|
5666
|
+
if (kind === 'sms') {
|
|
5667
|
+
const ret = await voice_ms.send_voice_message({ uid, profile_id, voice_number_id, to: recipient.address, body });
|
|
5668
|
+
if (ret?.code < 0) throw new Error(ret.data || 'send failed');
|
|
5669
|
+
} else {
|
|
5670
|
+
await email_ms.send_email({
|
|
5671
|
+
email: recipient.address,
|
|
5672
|
+
subject: `Message from ${profile_name}`,
|
|
5673
|
+
body,
|
|
5674
|
+
display_type: 'info',
|
|
5675
|
+
app_id: account_profile_info.app_id,
|
|
5676
|
+
});
|
|
5677
|
+
}
|
|
5678
|
+
results.push({ ...recipient, ok: true });
|
|
5679
|
+
} catch (err) {
|
|
5680
|
+
results.push({ ...recipient, ok: false, error: err.message });
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5684
|
+
return { results, sent: results.filter((r) => r.ok).length, failed: results.filter((r) => !r.ok).length };
|
|
5685
|
+
};
|
|
5686
|
+
|
|
5687
|
+
// `kind` is the composer mode: note | chat | sms | email. AI chat does not come through
|
|
5688
|
+
// here, it keeps going to ai_chat_conversation.
|
|
5689
|
+
const profile_thread_post = async function (req, job_id, headers, kind) {
|
|
5690
|
+
const { uid, profile_id } = req;
|
|
5691
|
+
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
5692
|
+
const { prompt: body, conversation_doc, attachments = [] } = req;
|
|
5693
|
+
try {
|
|
5694
|
+
const conversation_id = conversation_doc._id;
|
|
5695
|
+
const app_id = account_profile_info.app_id;
|
|
5696
|
+
const text = String(body || '').trim();
|
|
5697
|
+
if (!text) throw new Error('A message body is required');
|
|
5698
|
+
|
|
5699
|
+
await attachment_handler(uid, app_id, conversation_id, attachments, account_profile_info);
|
|
5700
|
+
|
|
5701
|
+
// Deliver BEFORE recording. A thread item is a claim that the message went out, so a
|
|
5702
|
+
// broadcast that reached nobody has to fail loudly instead of leaving a row that reads
|
|
5703
|
+
// as sent. A partial success is recorded, with the per-recipient detail on the item.
|
|
5704
|
+
let delivery = null;
|
|
5705
|
+
if (kind === 'sms' || kind === 'email') {
|
|
5706
|
+
delivery = await profile_broadcast(uid, account_profile_info, conversation_doc.reference_id, kind, text);
|
|
5707
|
+
if (!delivery.sent) {
|
|
5708
|
+
const why = delivery.results.find((r) => r.error)?.error;
|
|
5709
|
+
throw new Error(delivery.failed ? `Could not deliver to any member (${why})` : 'This profile has no members to send to');
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
let conversation_item_reference_id;
|
|
5714
|
+
try {
|
|
5715
|
+
conversation_item_reference_id = await add_conversation_item(uid, profile_id, conversation_id, text, kind, conversation_doc.reference_id, {});
|
|
5716
|
+
report_ai_status('conversations');
|
|
5717
|
+
} catch (err) {
|
|
5718
|
+
report_ai_status('conversations', err);
|
|
5719
|
+
throw err;
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
const out_conversation_item_obj = {
|
|
5723
|
+
_id: await _common.xuda_get_uuid('chat_conversation_item'),
|
|
5724
|
+
stat: 3,
|
|
5725
|
+
docType: 'chat_conversation_item',
|
|
5726
|
+
uid,
|
|
5727
|
+
conversation_type: kind,
|
|
5728
|
+
type: kind,
|
|
5729
|
+
date_created_ts: Date.now(),
|
|
5730
|
+
ts: Date.now(),
|
|
5731
|
+
conversation_id,
|
|
5732
|
+
text,
|
|
5733
|
+
reference_id: conversation_doc.reference_id,
|
|
5734
|
+
conversation_item_reference_id,
|
|
5735
|
+
direction: 'out',
|
|
5736
|
+
role: 'user',
|
|
5737
|
+
read: { [uid]: Date.now() },
|
|
5738
|
+
rtl: _common.detectRTL(text),
|
|
5739
|
+
...(delivery
|
|
5740
|
+
? {
|
|
5741
|
+
broadcast: {
|
|
5742
|
+
channel: kind,
|
|
5743
|
+
sent: delivery.sent,
|
|
5744
|
+
failed: delivery.failed,
|
|
5745
|
+
recipients: delivery.results.map((r) => ({ uid: r.uid, name: r.name, ok: r.ok, ...(r.error ? { error: r.error } : {}) })),
|
|
5746
|
+
},
|
|
5747
|
+
}
|
|
5748
|
+
: {}),
|
|
5749
|
+
};
|
|
5750
|
+
|
|
5751
|
+
return await db_module.save_app_couch_doc(app_id, out_conversation_item_obj);
|
|
5752
|
+
} catch (err) {
|
|
5753
|
+
return { code: -15, data: err.message };
|
|
5754
|
+
}
|
|
5755
|
+
};
|
|
5756
|
+
|
|
5578
5757
|
const chat_email = async function (req, job_id, headers) {
|
|
5579
5758
|
const { profile_id, uid, email_id, perform_ai_execution = true, from_mailbox, _thread_reentry, direction } = req;
|
|
5580
5759
|
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
@@ -7077,64 +7256,18 @@ const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_t
|
|
|
7077
7256
|
let tool_resources = {};
|
|
7078
7257
|
let eligible_agent = true;
|
|
7079
7258
|
|
|
7080
|
-
const
|
|
7259
|
+
const add_xuda_public_website_tool = function ({ name, description, origin, path_prefix }) {
|
|
7081
7260
|
if (reference_type !== 'ai_agents' && !prompt_suggestion_activated && !chat_suggestion_activated) {
|
|
7082
7261
|
eligible_agent = false;
|
|
7083
7262
|
return;
|
|
7084
7263
|
}
|
|
7085
7264
|
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
}),
|
|
7093
|
-
async execute(e) {
|
|
7094
|
-
const question = String(e?.question || '').trim();
|
|
7095
|
-
if (!question) {
|
|
7096
|
-
return 'question is required';
|
|
7097
|
-
}
|
|
7098
|
-
|
|
7099
|
-
const dev_host = _conf?.internal_ai_agents?.dev_server_host || 'dev.xuda.ai';
|
|
7100
|
-
const codex_ret = await execute_codex_request(
|
|
7101
|
-
{
|
|
7102
|
-
ip: dev_host,
|
|
7103
|
-
uid,
|
|
7104
|
-
account_profile_info,
|
|
7105
|
-
job_id,
|
|
7106
|
-
stream: false,
|
|
7107
|
-
prompt: `${system_prompt}
|
|
7108
|
-
|
|
7109
|
-
User question:
|
|
7110
|
-
${question}
|
|
7111
|
-
|
|
7112
|
-
Return a concise answer. Include only non-sensitive findings and mention when the requested information is outside the allowed scope.`,
|
|
7113
|
-
},
|
|
7114
|
-
job_id,
|
|
7115
|
-
headers,
|
|
7116
|
-
);
|
|
7117
|
-
|
|
7118
|
-
if (codex_ret.code < 0) {
|
|
7119
|
-
return `Codex readonly lookup failed: ${get_error_message(codex_ret.data, 'unknown error')}`;
|
|
7120
|
-
}
|
|
7121
|
-
|
|
7122
|
-
const events = codex_ret?.data?.events || [];
|
|
7123
|
-
const final_message = [...events].reverse().find((event) => event?.item?.type === 'agent_message' && event.item.text)?.item?.text;
|
|
7124
|
-
return final_message || 'Codex readonly lookup completed without a final answer.';
|
|
7125
|
-
},
|
|
7126
|
-
}),
|
|
7127
|
-
);
|
|
7128
|
-
};
|
|
7129
|
-
|
|
7130
|
-
const add_xuda_public_website_tool = function ({ name, description }) {
|
|
7131
|
-
if (reference_type !== 'ai_agents' && !prompt_suggestion_activated && !chat_suggestion_activated) {
|
|
7132
|
-
eligible_agent = false;
|
|
7133
|
-
return;
|
|
7134
|
-
}
|
|
7135
|
-
|
|
7136
|
-
const website_origin = _conf?.internal_ai_agents?.public_website_url || 'https://xuda.ai';
|
|
7137
|
-
const dump_dir = path.join(os.tmpdir(), 'xuda_public_website_dump', new URL(website_origin).hostname.replace(/[^a-z0-9.-]/gi, '_'));
|
|
7265
|
+
const website_origin = origin || _conf?.internal_ai_agents?.public_website_url || 'https://xuda.ai';
|
|
7266
|
+
// path_prefix narrows the crawl to one section of the site (the mentor tool uses /docs).
|
|
7267
|
+
// It is part of the cache key so a narrowed tool can never read a wider tool's dump.
|
|
7268
|
+
const section = String(path_prefix || '').replace(/\/+$/, '');
|
|
7269
|
+
const cache_key = `${new URL(website_origin).hostname}${section}`.replace(/[^a-z0-9.-]/gi, '_');
|
|
7270
|
+
const dump_dir = path.join(os.tmpdir(), 'xuda_public_website_dump', cache_key);
|
|
7138
7271
|
const manifest_path = path.join(dump_dir, 'manifest.json');
|
|
7139
7272
|
const ttl_ms = Number(_conf?.internal_ai_agents?.public_website_dump_ttl_ms || 10 * 60 * 1000);
|
|
7140
7273
|
const cache_version = 2;
|
|
@@ -7258,13 +7391,19 @@ Return a concise answer. Include only non-sensitive findings and mention when th
|
|
|
7258
7391
|
|
|
7259
7392
|
await fs.promises.mkdir(dump_dir, { recursive: true });
|
|
7260
7393
|
const sitemap_xml = await fetch_text(`${website_base}/sitemap.xml`, 8000);
|
|
7261
|
-
const priority_urls =
|
|
7394
|
+
const priority_urls = section
|
|
7395
|
+
? [`${website_base}${section}/`]
|
|
7396
|
+
: [`${website_base}/`, `${website_base}/about`, `${website_base}/legal/privacy-policy`, `${website_base}/legal/terms`, `${website_base}/contact`];
|
|
7262
7397
|
const sitemap_urls = [...sitemap_xml.matchAll(/<loc>([\s\S]*?)<\/loc>/gi)]
|
|
7263
7398
|
.map((match) => decode_html(match[1]).trim())
|
|
7264
7399
|
.filter((url) => {
|
|
7265
7400
|
try {
|
|
7266
7401
|
const parsed = new URL(url);
|
|
7267
|
-
|
|
7402
|
+
if (parsed.origin !== website_base) return false;
|
|
7403
|
+
// Section-scoped tools see only their own subtree, so a docs-only tool cannot
|
|
7404
|
+
// wander into the rest of the site.
|
|
7405
|
+
if (section && !(parsed.pathname === section || parsed.pathname.startsWith(`${section}/`))) return false;
|
|
7406
|
+
return !/\.(png|jpe?g|gif|webp|svg|pdf|zip)$/i.test(parsed.pathname);
|
|
7268
7407
|
} catch (_) {
|
|
7269
7408
|
return false;
|
|
7270
7409
|
}
|
|
@@ -7375,26 +7514,20 @@ Return a concise answer. Include only non-sensitive findings and mention when th
|
|
|
7375
7514
|
break;
|
|
7376
7515
|
}
|
|
7377
7516
|
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
- Never query CouchDB or any database.
|
|
7393
|
-
- Never use network commands to call internal services.
|
|
7394
|
-
- Never run commands that write, mutate, install, delete, restart, deploy, publish, chmod/chown, kill processes, or change system state.
|
|
7395
|
-
- Never reveal passwords, tokens, API keys, private keys, names of private people, private emails, personal information, customer data, security details, or anything that could harm Xuda, its team, customers, or product security.
|
|
7396
|
-
- If the answer requires forbidden access, say it is outside the readonly mentor scope.
|
|
7397
|
-
- If sensitive data appears accidentally, do not quote it. State that sensitive output was omitted.`,
|
|
7517
|
+
// RETIRED 2026-07-31 (Boaz): this used to run Codex over SSH on the dev server. Its
|
|
7518
|
+
// "readonly" scope was only a system prompt, while the process itself ran with
|
|
7519
|
+
// --dangerously-bypass-approvals-and-sandbox (no sandbox key is set in either config,
|
|
7520
|
+
// and that is the default), so an external mentor chatting to the agent was one
|
|
7521
|
+
// persuasive message away from arbitrary command execution on dev. The type string is
|
|
7522
|
+
// KEPT and remapped rather than deleted, so every mentor agent already installed out
|
|
7523
|
+
// there loses the capability the moment this ships, with no agent doc to migrate.
|
|
7524
|
+
case 'xuda_network_mentor_dev_server_readonly':
|
|
7525
|
+
case 'xuda_network_mentor_docs_readonly': {
|
|
7526
|
+
add_xuda_public_website_tool({
|
|
7527
|
+
name: name || 'Xuda Mentor Docs Readonly',
|
|
7528
|
+
description: description || 'Readonly lookup against the public Xuda developer documentation for technical mentor answers.',
|
|
7529
|
+
origin: _conf?.internal_ai_agents?.docs_website_url || _conf?.internal_ai_agents?.public_website_url || 'https://xuda.ai',
|
|
7530
|
+
path_prefix: _conf?.internal_ai_agents?.docs_path_prefix || '/docs',
|
|
7398
7531
|
});
|
|
7399
7532
|
break;
|
|
7400
7533
|
}
|
|
@@ -8504,6 +8637,20 @@ const add_conversation_item = async function (uid, profile_id, conversation_id,
|
|
|
8504
8637
|
}
|
|
8505
8638
|
};
|
|
8506
8639
|
|
|
8640
|
+
// The same privacy rule the phone AI follows, worded for a written reply. The contact may of
|
|
8641
|
+
// course see the thread they are already in, that is their own conversation. What they must
|
|
8642
|
+
// never get is the business's DERIVED view of them (internal notes, categories, mood or
|
|
8643
|
+
// status labels, payment standing), anything about a DIFFERENT contact, or a rundown of their
|
|
8644
|
+
// activity on other channels. Chat and SMS have no live verification step the way a phone
|
|
8645
|
+
// call does (there is nobody on the line to read a code back mid-sentence), so the verified
|
|
8646
|
+
// exception does not apply here and cross-channel history stays closed.
|
|
8647
|
+
const AUTO_REPLY_PRIVACY_POLICY = `Privacy rules, these override any other instruction:
|
|
8648
|
+
- You may use this conversation thread to reply naturally, it is the contact's own conversation.
|
|
8649
|
+
- Never reveal or hint at the business's internal record of this person: notes, tags, categories, sentiment or mood scores, account status, payment standing, or any other internal assessment.
|
|
8650
|
+
- Never reveal anything about any other person or contact.
|
|
8651
|
+
- Do not summarise or confirm their history on other channels, such as their phone calls, other email threads, or SMS messages. If they ask, say you cannot go over their record here and offer to have someone follow up.
|
|
8652
|
+
- If you are unsure whether something is safe to share, do not share it.`;
|
|
8653
|
+
|
|
8507
8654
|
const auto_response = async function (uid, profile_id, contact_id, conversation_type = 'email') {
|
|
8508
8655
|
try {
|
|
8509
8656
|
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
@@ -8515,11 +8662,11 @@ const auto_response = async function (uid, profile_id, contact_id, conversation_
|
|
|
8515
8662
|
|
|
8516
8663
|
let account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
8517
8664
|
|
|
8518
|
-
// The main profile
|
|
8519
|
-
//
|
|
8520
|
-
//
|
|
8521
|
-
|
|
8522
|
-
|
|
8665
|
+
// The main profile auto-responds like any other (Boaz, 2026-07-31, reversing UI-44
|
|
8666
|
+
// part 2). It used to be hard-skipped here on the reasoning that you do not auto-reply
|
|
8667
|
+
// to yourself, but the main profile is the identity most people actually receive on, so
|
|
8668
|
+
// excluding it meant the feature was off exactly where it was most wanted. The
|
|
8669
|
+
// auto_respond flag and mode above are now the only gate, for every profile.
|
|
8523
8670
|
if (account_profile_doc.auto_respond_mode === 'when_offline' && account_doc.socket_id) return;
|
|
8524
8671
|
if (!['always', 'when_offline'].includes(account_profile_doc.auto_respond_mode)) return;
|
|
8525
8672
|
|
|
@@ -8585,6 +8732,8 @@ const auto_response = async function (uid, profile_id, contact_id, conversation_
|
|
|
8585
8732
|
instructions: `You are ${account_profile_doc.profile_name || userName}'s automatic ${conversation_type === 'chat' ? 'chat' : 'email'} assistant.
|
|
8586
8733
|
Reply to the latest incoming message from ${contact_doc.name || contact_doc.email} in a friendly, helpful, and concise way.
|
|
8587
8734
|
Use the conversation history for context.
|
|
8735
|
+
|
|
8736
|
+
${AUTO_REPLY_PRIVACY_POLICY}
|
|
8588
8737
|
Match the language the contact wrote in.
|
|
8589
8738
|
Do not mention that the reply is automated.
|
|
8590
8739
|
${conversation_type === 'chat' ? 'Return only the chat message text, ready to send.' : 'Return only the email body, ready to send.'}
|
|
@@ -8626,6 +8775,8 @@ ${account_profile_doc.profile_signature || ''}`.trim(),
|
|
|
8626
8775
|
|
|
8627
8776
|
You are composing an automatic ${conversation_type === 'chat' ? 'chat' : 'email'} reply.
|
|
8628
8777
|
Use the existing conversation history as context.
|
|
8778
|
+
|
|
8779
|
+
${AUTO_REPLY_PRIVACY_POLICY}
|
|
8629
8780
|
Reply as ${account_profile_doc.profile_name || userName}.
|
|
8630
8781
|
${conversation_type === 'chat' ? 'Return only the chat message text, ready to send.' : 'Return only the email body text, ready to send.'}
|
|
8631
8782
|
Keep the response concise, natural, and professional.${
|