@xuda.io/ai_module 1.1.5571 → 1.1.5573

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 +387 -24
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -3,6 +3,7 @@ console.log('AI Module loaded...');
3
3
  import { z } from 'zod';
4
4
  import fs from 'fs';
5
5
  import path from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
6
7
 
7
8
  import sharp from 'sharp';
8
9
 
@@ -3567,6 +3568,46 @@ function getFirstNWords(text, n = 10) {
3567
3568
  return words.slice(0, n).join(' ');
3568
3569
  }
3569
3570
 
3571
+ const get_error_message = function (err, fallback = 'Unknown error') {
3572
+ if (!err) return fallback;
3573
+
3574
+ if (typeof err === 'string') {
3575
+ return err;
3576
+ }
3577
+
3578
+ if (err instanceof Error) {
3579
+ return err.message || fallback;
3580
+ }
3581
+
3582
+ if (typeof err?.message === 'string' && err.message) {
3583
+ return err.message;
3584
+ }
3585
+
3586
+ if (typeof err?.data === 'string' && err.data) {
3587
+ return err.data;
3588
+ }
3589
+
3590
+ try {
3591
+ const serialized = JSON.stringify(err);
3592
+ if (serialized && serialized !== '{}') {
3593
+ return serialized;
3594
+ }
3595
+ } catch (error) {}
3596
+
3597
+ return String(err);
3598
+ };
3599
+
3600
+ const get_plugin_import_specifier = function (app_id, plugin_name, resource, dev = true) {
3601
+ const absolute_path = dev ? path.join(process.env.XUDA_HOME, 'plugins', plugin_name, resource) : path.join(_conf.plugins_drive_path, app_id, 'node_modules', plugin_name, resource);
3602
+
3603
+ const file_url = pathToFileURL(absolute_path);
3604
+ if (dev) {
3605
+ file_url.searchParams.set('ts', `${Date.now()}`);
3606
+ }
3607
+
3608
+ return file_url.href;
3609
+ };
3610
+
3570
3611
  export const create_openai_conversation = async function () {
3571
3612
  return await client.conversations.create();
3572
3613
  };
@@ -3609,7 +3650,7 @@ export const create_conversation = async function (req, job_id, headers) {
3609
3650
  title: conversation_type !== 'email' ? getFirstNWords(prompt, 10) : prompt,
3610
3651
  date_created_ts: date_created ? new Date(date_created).getTime() : d,
3611
3652
  ts: d,
3612
- stat: !conversation_type || conversation_type === 'ai_chat' ? 1 : 3,
3653
+ stat: !conversation_type || ['ai_chat', 'studio'].includes(conversation_type) ? 1 : 3,
3613
3654
  uid,
3614
3655
  messages: [],
3615
3656
  reference_type,
@@ -3728,6 +3769,11 @@ const process_conversation = async function (uid, conversation_id, account_profi
3728
3769
  break;
3729
3770
  }
3730
3771
 
3772
+ case 'studio': {
3773
+ category_info = await categorize_ai_prompt(uid, prompt, account_profile_info);
3774
+ break;
3775
+ }
3776
+
3731
3777
  case 'email': {
3732
3778
  category_info = await categorize_email_subject(uid, prompt, account_profile_info);
3733
3779
  break;
@@ -3812,7 +3858,9 @@ export const submit_chat_conversation = async function (req, job_id, headers) {
3812
3858
  let conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
3813
3859
  req.conversation_doc = conversation_doc;
3814
3860
  let ret;
3815
- if (conversation_doc.reference_type === 'contacts') {
3861
+ if (conversation_doc.conversation_type === 'studio') {
3862
+ ret = await chat_studio(req, job_id, headers);
3863
+ } else if (conversation_doc.reference_type === 'contacts') {
3816
3864
  req._thread_reentry = Date.now() - conversation_doc.date_created_ts > 60000;
3817
3865
 
3818
3866
  account_msa.ts_contact(uid, conversation_doc.reference_id);
@@ -4127,6 +4175,336 @@ const chat_email = async function (req, job_id, headers) {
4127
4175
  }
4128
4176
  };
4129
4177
 
4178
+ const chat_studio = async function (req, job_id, headers) {
4179
+ const { uid, profile_id } = req;
4180
+ const account_profile_info = await get_active_account_profile_info(uid, profile_id);
4181
+ let { prompt, conversation_doc, attachments = [], plan_mode = false, stream = true } = req;
4182
+
4183
+ const conversation_id = conversation_doc._id;
4184
+ const normalized_plan_mode = typeof plan_mode === 'string' ? ['true', '1', 'yes', 'on'].includes(plan_mode.toLowerCase()) : Boolean(plan_mode);
4185
+ const save_result_to_studio = normalized_plan_mode ? false : true;
4186
+ const plugin_name = '@xuda.io/xuda-library-plugin-studio-ai-app-builder';
4187
+ const plugin_method = normalized_plan_mode ? 'app_planner' : 'app_builder';
4188
+ const target_app_id = (await _common.get_project_app_id(req.app_id || conversation_doc.reference_id || account_profile_info.app_id, true)) || account_profile_info.app_id;
4189
+
4190
+ const prompt_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
4191
+ const response_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
4192
+
4193
+ const emitToDashboard = (type, content, params) => {
4194
+ ws_dashboard_msa.emit_message_to_dashboard({
4195
+ service: type,
4196
+ to: uid,
4197
+ data: {
4198
+ conversation_id,
4199
+ job_id,
4200
+ delta: content,
4201
+ prompt_conversation_item_id,
4202
+ response_conversation_item_id,
4203
+ params,
4204
+ },
4205
+ });
4206
+ };
4207
+
4208
+ let response_started = false;
4209
+ const ensureResponseStarted = function () {
4210
+ if (!response_started) {
4211
+ response_started = true;
4212
+ emitToDashboard('response_start');
4213
+ }
4214
+ };
4215
+
4216
+ const emitStreamDelta = function (text) {
4217
+ if (!text) return;
4218
+ ensureResponseStarted();
4219
+ emitToDashboard('stream_delta', text);
4220
+ };
4221
+
4222
+ const streamText = function (text, chunk_size = 280) {
4223
+ if (!stream || !text) return;
4224
+ for (let i = 0; i < text.length; i += chunk_size) {
4225
+ emitStreamDelta(text.slice(i, i + chunk_size));
4226
+ }
4227
+ };
4228
+
4229
+ const emitThinkingPhase = function (content) {
4230
+ if (!content) return;
4231
+ emitToDashboard('stream_phase', content.trim(), { update: true });
4232
+ };
4233
+
4234
+ const formatConversationHistoryItem = function (item) {
4235
+ const role = item.role === 'assistant' ? 'Assistant' : 'User';
4236
+ const type = item.conversation_type || item.type || 'message';
4237
+ const file_name = item?.file?.filename ? ` file=${item.file.filename}` : '';
4238
+ let text = (item.text || item.prompt || '').trim();
4239
+ if (text.length > 2000) {
4240
+ text = `${text.slice(0, 2000)}...`;
4241
+ }
4242
+
4243
+ return `${role} (${type}${file_name}): ${text}`;
4244
+ };
4245
+
4246
+ const get_studio_context_prompt = async function () {
4247
+ const ret = await db_module.find_app_couch_query(account_profile_info.app_id, {
4248
+ selector: {
4249
+ docType: 'chat_conversation_item',
4250
+ conversation_id,
4251
+ stat: 3,
4252
+ },
4253
+ limit: 12,
4254
+ sort: [{ date_created_ts: 'desc' }],
4255
+ });
4256
+
4257
+ const items = (ret.docs || []).reverse().filter((item) => item.text || item.prompt || item?.file?.filename);
4258
+ const conversation_history = items.map(formatConversationHistoryItem).join('\n\n');
4259
+
4260
+ return `You are working inside Xuda Studio.
4261
+
4262
+ Use the conversation history below as the main source of context for the current studio task.
4263
+ The last user message is the latest request that should be handled now.
4264
+ When plan_mode is true, create a plan only and do not save studio docs.
4265
+ When plan_mode is false, generate the studio docs and save them into the project.
4266
+
4267
+ Conversation history:
4268
+ ${conversation_history || `User (studio): ${prompt}`}
4269
+ `;
4270
+ };
4271
+
4272
+ const get_studio_result_message = function (studio_result) {
4273
+ if (normalized_plan_mode) {
4274
+ if (typeof studio_result === 'string') {
4275
+ return studio_result;
4276
+ }
4277
+
4278
+ const approval_units = studio_result?.approval_units || [];
4279
+ const build_order = studio_result?.build_order || [];
4280
+ const acceptance_checks = studio_result?.acceptance_checks || [];
4281
+
4282
+ const lines = [`Studio plan ready.`, ``, `Thinking:`, `- Mapped the request into ${approval_units.length} planned Xuda unit${approval_units.length === 1 ? '' : 's'}.`, `- Kept this turn in planning mode, so no studio docs were saved.`];
4283
+
4284
+ if (studio_result?.overview) {
4285
+ lines.push('', 'Overview', studio_result.overview);
4286
+ }
4287
+
4288
+ if (approval_units.length) {
4289
+ lines.push('', 'Planned units');
4290
+ for (const unit of approval_units.slice(0, 12)) {
4291
+ lines.push(`- ${unit.unit_type}: ${unit.unit_name}${unit.folder_name ? ` (${unit.folder_name})` : ''}`);
4292
+ }
4293
+ }
4294
+
4295
+ if (build_order.length) {
4296
+ lines.push('', 'Build order');
4297
+ for (const step of build_order.slice(0, 8)) {
4298
+ lines.push(`- ${step.step}. ${step.block_ref}: ${step.deliverable}`);
4299
+ }
4300
+ }
4301
+
4302
+ if (acceptance_checks.length) {
4303
+ lines.push('', 'Acceptance checks');
4304
+ for (const check of acceptance_checks.slice(0, 8)) {
4305
+ lines.push(`- ${check}`);
4306
+ }
4307
+ }
4308
+
4309
+ return lines.join('\n');
4310
+ }
4311
+
4312
+ const docs = Array.isArray(studio_result?.docs) ? studio_result.docs.filter(Boolean) : _.castArray(studio_result).filter(Boolean);
4313
+ const grouped_docs = _.countBy(docs, (doc) => doc?.properties?.menuType || 'unknown');
4314
+ const grouped_docs_summary = Object.entries(grouped_docs)
4315
+ .map(([type, count]) => `${count} ${type}`)
4316
+ .join(', ');
4317
+
4318
+ const lines = [save_result_to_studio ? `Studio build completed and saved.` : `Studio build completed.`, ``, `Thinking:`, `- Turned the request into ${docs.length} studio doc${docs.length === 1 ? '' : 's'}.`];
4319
+
4320
+ if (grouped_docs_summary) {
4321
+ lines.push(`- Generated ${grouped_docs_summary}.`);
4322
+ }
4323
+
4324
+ lines.push(save_result_to_studio ? `- Saved the generated docs into the current studio project.` : `- Returned the generated docs without saving them.`);
4325
+
4326
+ if (docs.length) {
4327
+ lines.push('', 'Created units');
4328
+ for (const doc of docs.slice(0, 12)) {
4329
+ const menu_type = doc?.properties?.menuType || 'doc';
4330
+ const menu_name = doc?.properties?.menuName || doc?.properties?.menuTitle || doc?._id || 'Unnamed';
4331
+ lines.push(`- ${menu_type}: ${menu_name}${doc?._id ? ` [${doc._id}]` : ''}`);
4332
+ }
4333
+ }
4334
+
4335
+ return lines.join('\n');
4336
+ };
4337
+
4338
+ let thinking_interval;
4339
+ let thinking_index = 0;
4340
+ const thinking_messages = normalized_plan_mode
4341
+ ? ['Thinking through the request and mapping it into Xuda units...\n\n', 'Checking which folders, tables, globals, and runtime blocks are needed...\n\n', 'Drafting a studio floor plan for approval...\n\n']
4342
+ : ['Thinking through the request and deciding what should be created in Studio...\n\n', 'Checking which Xuda docs are required before building...\n\n', 'Preparing the app builder plugin to generate and save the result...\n\n'];
4343
+
4344
+ try {
4345
+ emitToDashboard('stream_start');
4346
+ emitToDashboard('stream_phase', normalized_plan_mode ? 'Planning studio request' : 'Building studio request');
4347
+
4348
+ const conversation_item_reference_id = await add_conversation_item(uid, profile_id, conversation_id, prompt, 'studio', undefined, { plan_mode: normalized_plan_mode });
4349
+
4350
+ let out_conversation_item_obj = {
4351
+ _id: prompt_conversation_item_id,
4352
+ stat: 3,
4353
+ docType: 'chat_conversation_item',
4354
+ uid,
4355
+ conversation_type: 'studio',
4356
+ type: 'studio',
4357
+ date_created_ts: Date.now(),
4358
+ ts: Date.now(),
4359
+ conversation_id,
4360
+ text: prompt,
4361
+ reference_id: conversation_doc.reference_id,
4362
+ conversation_item_reference_id,
4363
+ direction: 'out',
4364
+ role: 'user',
4365
+ read: { [uid]: Date.now() },
4366
+ rtl: _common.detectRTL(prompt),
4367
+ job_id,
4368
+ plan_mode: normalized_plan_mode,
4369
+ studio_method: plugin_method,
4370
+ target_app_id,
4371
+ };
4372
+
4373
+ const out_conversation_item_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, out_conversation_item_obj);
4374
+ if (out_conversation_item_save_ret.code < 0) throw new Error(out_conversation_item_save_ret.data);
4375
+
4376
+ if (attachments?.length) {
4377
+ emitToDashboard('stream_phase', 'Analyzing attachments', { update: true });
4378
+ await attachment_handler(uid, account_profile_info.app_id, conversation_id, attachments, account_profile_info);
4379
+ }
4380
+
4381
+ conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
4382
+ conversation_doc.ts = Date.now();
4383
+ conversation_doc.stat = 2;
4384
+ conversation_doc.job_id = job_id;
4385
+ conversation_doc.plan_mode = normalized_plan_mode;
4386
+ conversation_doc.target_app_id = target_app_id;
4387
+ await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
4388
+
4389
+ emitToDashboard('stream_phase', normalized_plan_mode ? 'Preparing studio plan' : 'Preparing studio build', { update: true });
4390
+ emitThinkingPhase(thinking_messages[thinking_index]);
4391
+
4392
+ if (thinking_messages.length > 1) {
4393
+ thinking_interval = setInterval(async () => {
4394
+ if (thinking_index >= thinking_messages.length - 1) {
4395
+ clearInterval(thinking_interval);
4396
+ thinking_interval = undefined;
4397
+ return;
4398
+ }
4399
+
4400
+ thinking_index += 1;
4401
+ try {
4402
+ if (job_id) {
4403
+ const job_info = await jobs_ms.get_job_info({ job_id });
4404
+ if (job_info.code < 0 || job_info.data.stat === 4) {
4405
+ return;
4406
+ }
4407
+ }
4408
+ } catch (error) {}
4409
+
4410
+ emitThinkingPhase(thinking_messages[thinking_index]);
4411
+
4412
+ if (thinking_index >= thinking_messages.length - 1) {
4413
+ clearInterval(thinking_interval);
4414
+ thinking_interval = undefined;
4415
+ }
4416
+ }, 1800);
4417
+ }
4418
+
4419
+ const studio_prompt = await get_studio_context_prompt();
4420
+ const plugin_ret = await run_plugin(
4421
+ target_app_id,
4422
+ uid,
4423
+ plugin_name,
4424
+ plugin_method,
4425
+ {
4426
+ prompt: studio_prompt,
4427
+ save: save_result_to_studio,
4428
+ },
4429
+ true,
4430
+ {
4431
+ ...req,
4432
+ prompt: studio_prompt,
4433
+ plan_mode: normalized_plan_mode,
4434
+ target_app_id,
4435
+ },
4436
+ true,
4437
+ {
4438
+ conversation_id,
4439
+ conversation_type: 'studio',
4440
+ plan_mode: normalized_plan_mode,
4441
+ },
4442
+ );
4443
+
4444
+ clearInterval(thinking_interval);
4445
+ thinking_interval = undefined;
4446
+
4447
+ if (!plugin_ret || plugin_ret.code < 0) {
4448
+ throw new Error(get_error_message(plugin_ret?.data || plugin_ret, 'studio plugin failed'));
4449
+ }
4450
+
4451
+ const studio_result = plugin_ret.data;
4452
+ const response_text = get_studio_result_message(studio_result);
4453
+
4454
+ emitToDashboard('stream_phase', normalized_plan_mode ? 'Streaming studio plan' : 'Streaming studio result', { update: true });
4455
+ streamText(response_text);
4456
+ emitToDashboard('stream_end');
4457
+
4458
+ conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
4459
+ conversation_doc.ts = Date.now();
4460
+ conversation_doc.stat = 3;
4461
+ conversation_doc.process_stat = 'full';
4462
+ await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
4463
+
4464
+ const in_conversation_item_obj = {
4465
+ _id: response_conversation_item_id,
4466
+ stat: 3,
4467
+ docType: 'chat_conversation_item',
4468
+ uid,
4469
+ conversation_type: 'studio',
4470
+ type: 'studio',
4471
+ date_created_ts: Date.now(),
4472
+ ts: Date.now(),
4473
+ conversation_id,
4474
+ text: response_text,
4475
+ reference_id: conversation_doc.reference_id,
4476
+ direction: 'in',
4477
+ role: 'assistant',
4478
+ read: { [uid]: Date.now() },
4479
+ rtl: _common.detectRTL(response_text),
4480
+ job_id,
4481
+ plan_mode: normalized_plan_mode,
4482
+ studio_method: plugin_method,
4483
+ studio_result,
4484
+ target_app_id,
4485
+ prompt_conversation_item_id,
4486
+ };
4487
+
4488
+ return await db_module.save_app_couch_doc_native(account_profile_info.app_id, in_conversation_item_obj);
4489
+ } catch (err) {
4490
+ if (thinking_interval) {
4491
+ clearInterval(thinking_interval);
4492
+ }
4493
+
4494
+ conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
4495
+ conversation_doc.ts = Date.now();
4496
+ conversation_doc.stat = 3;
4497
+ await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
4498
+
4499
+ const error_message = get_error_message(err, 'studio request failed');
4500
+ emitToDashboard('stream_phase', 'Studio request failed', { update: true });
4501
+ streamText(`Studio request failed: ${error_message}`);
4502
+ emitToDashboard('stream_end');
4503
+
4504
+ return { code: -16, data: error_message };
4505
+ }
4506
+ };
4507
+
4130
4508
  const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_type, prompt_suggestion_activated, chat_suggestion_activated, gtp_token, uid, account_profile_info, job_id, headers, context, app_id }) {
4131
4509
  let tools = [];
4132
4510
  let tool_resources = {};
@@ -4166,7 +4544,7 @@ const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_t
4166
4544
  eligible_agent = false;
4167
4545
  break;
4168
4546
  }
4169
- continue;
4547
+
4170
4548
  try {
4171
4549
  debugger;
4172
4550
  const api_allow_methods = Object.keys(_conf.cpi_methods);
@@ -4176,6 +4554,7 @@ const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_t
4176
4554
  if (!method_prop) continue;
4177
4555
  if (method_prop.private) continue;
4178
4556
  if (!method_prop.description) continue;
4557
+ if (!method_prop.mcp) continue;
4179
4558
 
4180
4559
  let inputSchema = {};
4181
4560
 
@@ -5161,7 +5540,7 @@ const add_conversation_item = async function (uid, profile_id, conversation_id,
5161
5540
  } catch (err) {
5162
5541
  throw err;
5163
5542
  }
5164
- if (contact_id) {
5543
+ if (conversation_doc.reference_type === 'contacts' && contact_id) {
5165
5544
  try {
5166
5545
  let contact_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
5167
5546
  const item = await client.conversations.items.create(contact_doc.contact_reference_conversation_id, {
@@ -5466,17 +5845,10 @@ setTimeout(async () => {
5466
5845
  const run_plugin = async function (app_id, uid, plugin_name, method, prop_data, dev = true, req = {}, testing = true, agent_info = {}) {
5467
5846
  const db_module = await import(`${module_path}/db_module/index.mjs`);
5468
5847
  debugger;
5469
- const get_path = function (resource) {
5470
- if (dev) {
5471
- return `../../plugins/${plugin_name}/${resource}`;
5472
- }
5473
-
5474
- return `${_conf.plugins_drive_path}/${app_id}/node_modules/${plugin_name}/${resource}`;
5475
- };
5476
5848
  const get_plugin_resource = function (plugin_name, plugin_resource) {
5477
5849
  return new Promise(async (resolve, reject) => {
5478
5850
  try {
5479
- const plugin_resource_res = await import(`${get_path(plugin_resource)}`);
5851
+ const plugin_resource_res = await import(get_plugin_import_specifier(app_id, plugin_name, plugin_resource, dev));
5480
5852
  resolve(plugin_resource_res);
5481
5853
  } catch (err) {
5482
5854
  console.error(err);
@@ -5559,8 +5931,7 @@ const run_plugin = async function (app_id, uid, plugin_name, method, prop_data,
5559
5931
  };
5560
5932
 
5561
5933
  const reject_local = function (e) {
5562
- reject({ code: -1, data: e });
5563
- // reject(e.message);
5934
+ reject(new Error(get_error_message(e, 'plugin execution failed')));
5564
5935
  };
5565
5936
 
5566
5937
  try {
@@ -5603,17 +5974,10 @@ const get_plugin_tool = async function (app_id, uid, plugin_name, method, prop_d
5603
5974
 
5604
5975
  init(undefined, undefined, _, true, undefined, undefined, undefined, undefined, undefined, true, z);
5605
5976
 
5606
- const get_path = function (resource) {
5607
- if (dev) {
5608
- return `../../plugins/${plugin_name}/${resource}`;
5609
- }
5610
-
5611
- return `${_conf.plugins_drive_path}/${app_id}/node_modules/${plugin_name}/${resource}`;
5612
- };
5613
5977
  const get_plugin_resource = function (plugin_name, plugin_resource) {
5614
5978
  return new Promise(async (resolve, reject) => {
5615
5979
  try {
5616
- const plugin_resource_res = await import(`${get_path(plugin_resource)}`);
5980
+ const plugin_resource_res = await import(get_plugin_import_specifier(app_id, plugin_name, plugin_resource, dev));
5617
5981
  resolve(plugin_resource_res);
5618
5982
  } catch (err) {
5619
5983
  console.error(err);
@@ -5691,8 +6055,7 @@ const get_plugin_tool = async function (app_id, uid, plugin_name, method, prop_d
5691
6055
  };
5692
6056
 
5693
6057
  const reject_local = function (e) {
5694
- reject({ code: -1, data: e });
5695
- // reject(e.message);
6058
+ reject(new Error(get_error_message(e, 'plugin execution failed')));
5696
6059
  };
5697
6060
 
5698
6061
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5571",
3
+ "version": "1.1.5573",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",