@xuda.io/ai_module 1.1.5640 → 1.1.5641

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 (2) hide show
  1. package/index.mjs +167 -20
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -308,6 +308,30 @@ const { init, check, check_structure, get_zod_schema, get_fields_z_schema } = aw
308
308
  const { get_xuda_couch } = _common;
309
309
 
310
310
  const db_module = await import(`${module_path}/db_module/index.mjs`);
311
+
312
+ // Sort keys for the dashboard list tabs, per docType. Each entry is a fallback
313
+ // chain (first defined value wins) because the doc shapes differ: a chat has
314
+ // its own title + date_created_ts, a studio doc carries the name under
315
+ // properties.menuName and may only have ts. See
316
+ // db_module.find_app_couch_sorted_page.
317
+ // `shared` points at the owner's copy: an app/agent shared with you, or bought
318
+ // from the marketplace, is stored locally as a stub with no menuName, so a name
319
+ // sort has to read the source doc. Same two pointers the enrichment follows
320
+ // (get_ai_agents/get_apps: shared_from_app_id + share_item_id, then
321
+ // installed_from_app_id keyed by the doc's own _id).
322
+ const LIST_SORT_PATHS = {
323
+ chat: { created: ['date_created_ts', 'date_created', 'ts'], updated: ['ts', 'date_created_ts'], name: ['title'] },
324
+ studio: {
325
+ created: ['date_created_ts', 'date_created', 'ts'],
326
+ updated: ['ts', 'date_created_ts'],
327
+ name: ['properties.menuName', 'studio_meta.name'],
328
+ shared: [
329
+ { app_id: 'studio_meta.shared_from_app_id', doc_id: 'studio_meta.share_item_id' },
330
+ { app_id: 'studio_meta.installed_from_app_id', doc_id: '_id' },
331
+ ],
332
+ },
333
+ };
334
+
311
335
  const account_ms = await import(`${module_path}/account_module/index_ms.mjs`);
312
336
  const drive_ms = await import(`${module_path}/drive_module/index_ms.mjs`);
313
337
  const jobs_ms = await import(`${module_path}/jobs_module/index_ms.mjs`);
@@ -2073,6 +2097,14 @@ export const get_ai_chats = async function (req, job_id, headers) {
2073
2097
  };
2074
2098
  }
2075
2099
 
2100
+ // User-picked order from the dashboard list controls (sort_by date |
2101
+ // updated | name, sort_dir asc | desc). Null when none was asked for,
2102
+ // which leaves the default newest-first path below untouched. Only the
2103
+ // top-level chat list is sortable: with a reference_id these are the
2104
+ // items INSIDE one conversation, which must stay in thread order.
2105
+ const sorted = reference_id || conversation_id ? null : await db_module.find_app_couch_sorted_page(account_profile_info.app_id, opt, req, LIST_SORT_PATHS.chat);
2106
+ if (sorted) return sorted;
2107
+
2076
2108
  let ai_chats = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
2077
2109
  if (!limit || conversation_id) {
2078
2110
  ai_chats.total_docs = ai_chats.docs.length;
@@ -2243,9 +2275,19 @@ export const get_ai_chats = async function (req, job_id, headers) {
2243
2275
  doc.notifications = contact_chat_conversation_count_ret[doc._id] - contact_chat_conversation_read_ret[doc._id];
2244
2276
  doc.chats = doc.interactions;
2245
2277
 
2246
- doc.user_contact = await get_user_card({ uid, uid_query: doc.uid });
2278
+ // Who WROTE this row. A contact/profile timeline comes back from
2279
+ // get_data_view_conversation_items as a composite { conversation, conversation_item }
2280
+ // that carries no uid of its own, so `doc.uid` was undefined here. get_user_card then
2281
+ // built a selector with contact_uid dropped (JSON.stringify strips undefined) and
2282
+ // matched ANY contact, which is why every note on a contact page was bylined with the
2283
+ // first contact in the book, the person the note is ABOUT, instead of its author.
2284
+ const author_uid = doc.uid || doc.conversation_item?.uid || doc.conversation?.initiator_uid || doc.conversation?.uid;
2247
2285
 
2248
- doc.privilege = doc.uid === uid || doc?.account_profile_info?.uid === uid;
2286
+ if (author_uid) {
2287
+ doc.user_contact = await get_user_card({ uid, uid_query: author_uid });
2288
+ }
2289
+
2290
+ doc.privilege = author_uid === uid || doc?.account_profile_info?.uid === uid;
2249
2291
 
2250
2292
  if (reference_id && doc.reference_type === 'contact') {
2251
2293
  // only valid for contacts conversation
@@ -2254,6 +2296,17 @@ export const get_ai_chats = async function (req, job_id, headers) {
2254
2296
  docs.push(doc);
2255
2297
  }
2256
2298
 
2299
+ // Calls and SMS belong on the contact's timeline next to the notes rather than behind
2300
+ // their own tabs, so merge them in and order the whole thing by time. Only for a real
2301
+ // contact timeline: the account-wide lists have their own screens.
2302
+ if (reference_id && reference_type === 'contacts') {
2303
+ const voice_rows = await _tl_voice_rows(uid, account_profile_info, reference_id, docs[0]?.user_contact || null);
2304
+ if (voice_rows.length) {
2305
+ const row_ts = (r) => Number(r?.conversation_item?.date_created_ts || r?.conversation?.date_created_ts || r?.date_created_ts || 0);
2306
+ docs = [...docs, ...voice_rows].sort((a, b) => row_ts(a) - row_ts(b));
2307
+ }
2308
+ }
2309
+
2257
2310
  return { code: 2, data: { docs: [...requests_from.docs, ...docs], total_docs: ai_chats.total_docs + requests_from.total_docs } };
2258
2311
  } catch (err) {
2259
2312
  return { code: -2, data: err.message };
@@ -2974,17 +3027,24 @@ export const get_ai_agents = async function (req, job_id, headers) {
2974
3027
 
2975
3028
  let user_agents = { docs: [], total_docs: 0 };
2976
3029
  if (filter_type !== 'pending') {
2977
- user_agents = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
2978
- if (!limit || agent_id) {
2979
- user_agents.total_docs = user_agents.docs.length;
3030
+ // User-picked order from the dashboard list controls, see
3031
+ // db_module.find_app_couch_sorted_page. Null when none was asked for.
3032
+ const sorted = agent_id ? null : await db_module.find_app_couch_sorted_page(account_profile_info.app_id, opt, req, LIST_SORT_PATHS.studio);
3033
+ if (sorted) {
3034
+ user_agents = sorted;
2980
3035
  } else {
2981
- delete opt.sort;
2982
- delete opt.skip;
2983
- opt.limit = 9999;
2984
- opt.fields = ['_id'];
3036
+ user_agents = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
3037
+ if (!limit || agent_id) {
3038
+ user_agents.total_docs = user_agents.docs.length;
3039
+ } else {
3040
+ delete opt.sort;
3041
+ delete opt.skip;
3042
+ opt.limit = 9999;
3043
+ opt.fields = ['_id'];
2985
3044
 
2986
- const counter = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
2987
- user_agents.total_docs = counter.docs.length;
3045
+ const counter = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
3046
+ user_agents.total_docs = counter.docs.length;
3047
+ }
2988
3048
  }
2989
3049
  }
2990
3050
 
@@ -3460,17 +3520,25 @@ export const get_apps = async function (req, job_id, headers) {
3460
3520
 
3461
3521
  let user_apps = { docs: [], total_docs: 0 };
3462
3522
  if (filter_type !== 'pending') {
3463
- user_apps = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
3464
- if (!limit || mini_app_id) {
3465
- user_apps.total_docs = user_apps.docs.length;
3523
+ // User-picked order from the dashboard list controls, see
3524
+ // db_module.find_app_couch_sorted_page. Null when none was asked for,
3525
+ // which leaves the default newest-first path below untouched.
3526
+ const sorted = mini_app_id ? null : await db_module.find_app_couch_sorted_page(account_profile_info.app_id, opt, req, LIST_SORT_PATHS.studio);
3527
+ if (sorted) {
3528
+ user_apps = sorted;
3466
3529
  } else {
3467
- delete opt.sort;
3468
- delete opt.skip;
3469
- opt.limit = 9999;
3470
- opt.fields = ['_id'];
3530
+ user_apps = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
3531
+ if (!limit || mini_app_id) {
3532
+ user_apps.total_docs = user_apps.docs.length;
3533
+ } else {
3534
+ delete opt.sort;
3535
+ delete opt.skip;
3536
+ opt.limit = 9999;
3537
+ opt.fields = ['_id'];
3471
3538
 
3472
- const counter = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
3473
- user_apps.total_docs = counter.docs.length;
3539
+ const counter = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
3540
+ user_apps.total_docs = counter.docs.length;
3541
+ }
3474
3542
  }
3475
3543
  }
3476
3544
 
@@ -5143,6 +5211,85 @@ const process_conversation = async function (uid, conversation_id, account_profi
5143
5211
  // }
5144
5212
  };
5145
5213
 
5214
+ // --- contact timeline: calls and SMS -----------------------------------------------------
5215
+ // Phone activity lives in xuda_master as voice_call / voice_message docs keyed by
5216
+ // owner_uid + phone number, NOT by contact, so it can only be tied to a contact by matching
5217
+ // numbers. Digits only, comparing the last 10, so a number written +1 561 235 1675 on an
5218
+ // account matches +15612351675 on a call. Same rule as voice_module's own caller matching.
5219
+ const _tl_digits = (value) => String(value ?? '').replace(/\D+/g, '');
5220
+ const _tl_same_phone = (a, b) => {
5221
+ const x = _tl_digits(a);
5222
+ const y = _tl_digits(b);
5223
+ if (!x || !y) return false;
5224
+ if (x === y) return true;
5225
+ const n = Math.min(10, x.length, y.length);
5226
+ return n >= 7 && x.slice(-n) === y.slice(-n);
5227
+ };
5228
+
5229
+ // Every number this contact can be reached on. A contact linked to a Xuda user carries no
5230
+ // phone on its own doc at all: it lives on their account, which is why matching only the
5231
+ // contact doc finds nothing for exactly the contacts that matter most.
5232
+ const _tl_contact_phones = async function (app_id, contact_id) {
5233
+ const out = [];
5234
+ let contact = null;
5235
+ try {
5236
+ contact = await db_module.get_app_couch_doc_native(app_id, contact_id);
5237
+ for (const f of ['phone_e164', 'phone', 'telephone', 'phone_number', 'mobile', 'tel']) {
5238
+ if (contact?.[f]) out.push(contact[f]);
5239
+ }
5240
+ if (contact?.contact_uid) {
5241
+ const ret = await db_module.get_couch_doc('xuda_accounts', contact.contact_uid);
5242
+ const info = ret?.code >= 0 ? ret.data?.account_info || {} : {};
5243
+ if (info.phone_number) out.push(info.phone_number);
5244
+ if (info.tel) out.push(info.tel);
5245
+ }
5246
+ } catch (_) { /* no phone is a normal contact, not an error */ }
5247
+ return { phones: out.filter(Boolean), contact };
5248
+ };
5249
+
5250
+ // Calls and SMS as timeline rows, in the same composite shape the note and email rows use
5251
+ // ({ conversation, conversation_item, user_contact }) so the timeline renders them by
5252
+ // conversation_type without a second code path.
5253
+ const _tl_voice_rows = async function (uid, account_profile_info, contact_id, user_contact) {
5254
+ try {
5255
+ const { phones, contact } = await _tl_contact_phones(account_profile_info.app_id, contact_id);
5256
+ if (!phones.length) return [];
5257
+ const peer = (d) => (d.direction === 'inbound' ? d.from : d.to);
5258
+ // Byline the party who did the thing: an inbound call or SMS came FROM the contact, an
5259
+ // outbound one went out under this account. Bylining every call with the account name
5260
+ // read as if the business had called itself.
5261
+ const card_for = (d) => (d.direction === 'inbound' ? contact || user_contact : user_contact);
5262
+ const [calls, messages] = await Promise.all([
5263
+ db_module.find_couch_query('xuda_master', { selector: { docType: 'voice_call', owner_uid: uid }, limit: 500 }, false, true),
5264
+ db_module.find_couch_query('xuda_master', { selector: { docType: 'voice_message', owner_uid: uid }, limit: 500 }, false, true),
5265
+ ]);
5266
+ const mine = (d) => phones.some((p) => _tl_same_phone(p, peer(d)));
5267
+ const row = (type, doc, text, extra) => ({
5268
+ conversation: { _id: doc._id, docType: 'chat_conversation', conversation_type: type, reference_type: 'contacts', reference_id: contact_id, uid, prompt: text, title: text, date_created_ts: doc.created_ts, ts: doc.created_ts, stat: 3 },
5269
+ conversation_item: { _id: doc._id, docType: 'chat_conversation_item', conversation_type: type, type, text, direction: doc.direction, date_created_ts: doc.created_ts, ts: doc.created_ts, stat: 3, uid },
5270
+ user_contact: card_for(doc),
5271
+ voice: { kind: type, ...extra },
5272
+ privilege: true,
5273
+ });
5274
+
5275
+ const call_rows = (calls?.docs || []).filter((d) => d.status !== 'ringing' && mine(d)).map((d) =>
5276
+ row('call', d, d.direction === 'inbound' ? 'Incoming call' : 'Outgoing call', {
5277
+ status: d.status,
5278
+ duration_sec: d.duration_sec || 0,
5279
+ has_recording: !!d.recording,
5280
+ transcript_turns: (d.transcript || []).length,
5281
+ handled_by: d.handled_by || '',
5282
+ }),
5283
+ );
5284
+ const sms_rows = (messages?.docs || []).filter(mine).map((d) => row('sms', d, d.body || '', { status: d.status, segments: d.segments || 1 }));
5285
+ return [...call_rows, ...sms_rows];
5286
+ } catch (err) {
5287
+ // A phone lookup must never take the whole timeline down with it.
5288
+ console.error('[ai_module] contact voice timeline rows failed:', err.message);
5289
+ return [];
5290
+ }
5291
+ };
5292
+
5146
5293
  const contactGuardrailAgent = new Agent({
5147
5294
  name: 'Person Guardrail check',
5148
5295
  instructions: 'Check if the prompt about person.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5640",
3
+ "version": "1.1.5641",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",