@xuda.io/ai_module 1.1.5589 → 1.1.5591

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 CHANGED
@@ -4,6 +4,7 @@ import { z } from 'zod';
4
4
  import fs from 'fs';
5
5
  import path from 'node:path';
6
6
  import { pathToFileURL } from 'node:url';
7
+ import { spawn } from 'node:child_process';
7
8
 
8
9
  import sharp from 'sharp';
9
10
 
@@ -24,11 +25,133 @@ import process from 'process';
24
25
  import { File } from 'node:buffer'; // Node 20+
25
26
  const { rm, readFile, unlink, writeFile } = fs.promises;
26
27
 
27
- global._conf = (
28
- await import(path.join(process.env.XUDA_HOME, process.env.XUDA_CONFIG), {
29
- with: { type: 'json' },
30
- })
31
- ).default;
28
+ const run_process = function (command, args, input, options = {}) {
29
+ return new Promise((resolve, reject) => {
30
+ const { onStdout, onStderr, ...spawn_options } = options;
31
+ const child = spawn(command, args, {
32
+ stdio: ['pipe', 'pipe', 'pipe'],
33
+ ...spawn_options,
34
+ });
35
+
36
+ let stdout = '';
37
+ let stderr = '';
38
+
39
+ child.stdout.on('data', (chunk) => {
40
+ const text = chunk.toString();
41
+ stdout += text;
42
+ if (onStdout) {
43
+ onStdout(text);
44
+ }
45
+ });
46
+
47
+ child.stderr.on('data', (chunk) => {
48
+ const text = chunk.toString();
49
+ stderr += text;
50
+ if (onStderr) {
51
+ onStderr(text);
52
+ }
53
+ });
54
+
55
+ child.on('error', reject);
56
+ child.on('close', (exit_code) => {
57
+ resolve({ exit_code, stdout, stderr });
58
+ });
59
+
60
+ if (input) {
61
+ child.stdin.write(input);
62
+ }
63
+ child.stdin.end();
64
+ });
65
+ };
66
+
67
+ const normalize_boolean = function (value) {
68
+ if (typeof value === 'string') {
69
+ return ['true', '1', 'yes', 'on'].includes(value.toLowerCase());
70
+ }
71
+
72
+ return Boolean(value);
73
+ };
74
+
75
+ const is_valid_codex_remote_host = function (ip) {
76
+ return typeof ip === 'string' && /^[a-zA-Z0-9.:%@_-]+$/.test(ip) && !ip.startsWith('-') && !ip.includes('..');
77
+ };
78
+
79
+ const get_codex_remote_host = function (ip) {
80
+ return ip.includes('@') ? ip : `root@${ip}`;
81
+ };
82
+
83
+ const get_codex_local_attachment_path = function (attachment) {
84
+ if (!attachment || typeof attachment !== 'object') return null;
85
+
86
+ return attachment.local_path || attachment.local_file_path || (attachment.is_local ? attachment.path || attachment.file_path || attachment.filename : null);
87
+ };
88
+
89
+ const get_codex_remote_attachment_path = function (attachment) {
90
+ if (!attachment) return null;
91
+ if (typeof attachment === 'string') return attachment;
92
+ if (typeof attachment !== 'object') return null;
93
+
94
+ return attachment.remote_path || (attachment.is_remote ? attachment.path || attachment.file_path || attachment.filename : null);
95
+ };
96
+
97
+ const is_codex_image_attachment = function (attachment_path) {
98
+ return typeof attachment_path === 'string' && /\.(png|jpe?g|webp|gif)$/i.test(attachment_path);
99
+ };
100
+
101
+ const parse_codex_jsonl = function (stdout) {
102
+ const events = [];
103
+ for (const line of stdout.split(/\r?\n/)) {
104
+ if (!line.trim()) continue;
105
+ try {
106
+ events.push(JSON.parse(line));
107
+ } catch (err) {
108
+ return null;
109
+ }
110
+ }
111
+ return events;
112
+ };
113
+
114
+ const create_codex_jsonl_stream_parser = function (onEvent) {
115
+ let buffer = '';
116
+
117
+ return function (chunk) {
118
+ buffer += chunk;
119
+ const lines = buffer.split(/\r?\n/);
120
+ buffer = lines.pop() || '';
121
+
122
+ for (const line of lines) {
123
+ if (!line.trim()) continue;
124
+ try {
125
+ onEvent(JSON.parse(line));
126
+ } catch (err) {
127
+ onEvent({
128
+ type: 'raw',
129
+ text: line,
130
+ });
131
+ }
132
+ }
133
+ };
134
+ };
135
+
136
+ const get_openai_codex_cli_command = function () {
137
+ return _conf.openai_codex_command || 'codex';
138
+ };
139
+
140
+ const get_openai_codex_exec_args = function () {
141
+ const args = ['exec', '--json'];
142
+ const bypass_approvals_and_sandbox = typeof _conf.openai_codex_bypass_approvals_and_sandbox === 'undefined' ? true : _conf.openai_codex_bypass_approvals_and_sandbox;
143
+ const sandbox = _conf.openai_codex_sandbox;
144
+
145
+ if (bypass_approvals_and_sandbox) {
146
+ args.push('--dangerously-bypass-approvals-and-sandbox');
147
+ } else if (sandbox) {
148
+ args.push('--sandbox', sandbox);
149
+ }
150
+
151
+ return args;
152
+ };
153
+
154
+ global._conf = JSON.parse(fs.readFileSync(path.join(process.env.XUDA_HOME, process.env.XUDA_CONFIG), 'utf8'));
32
155
 
33
156
  const _defaultAppConfig = {
34
157
  credential: admin.credential.cert(_conf.firebase_serviceAccount),
@@ -1082,6 +1205,259 @@ const check_studio_doc_tool = tool({
1082
1205
  },
1083
1206
  });
1084
1207
 
1208
+ export const execute_codex_request = async function (req_or_ip, prompt_arg, attachments_arg = []) {
1209
+ let emitToDashboard = function () {};
1210
+ let streamText = function () {};
1211
+ let stream_enabled = false;
1212
+
1213
+ try {
1214
+ const is_req_call = req_or_ip && typeof req_or_ip === 'object' && !Array.isArray(req_or_ip);
1215
+ const ip = is_req_call ? req_or_ip.ip : req_or_ip;
1216
+ const prompt = is_req_call ? req_or_ip.prompt : prompt_arg;
1217
+ let attachments = is_req_call ? req_or_ip.attachments || [] : attachments_arg;
1218
+ const uid = is_req_call ? req_or_ip.uid : undefined;
1219
+ const conversation_id = is_req_call ? req_or_ip.conversation_id || req_or_ip.conversation_doc?._id : undefined;
1220
+ const job_id = is_req_call ? prompt_arg || req_or_ip.job_id : undefined;
1221
+ stream_enabled = is_req_call ? req_or_ip.stream !== false && req_or_ip.stream !== 'false' : false;
1222
+ const prompt_conversation_item_id = is_req_call ? await _common.xuda_get_uuid('chat_conversation_item') : undefined;
1223
+ const response_conversation_item_id = is_req_call ? await _common.xuda_get_uuid('chat_conversation_item') : undefined;
1224
+
1225
+ let stream_delta_seq = 0;
1226
+ let stream_delta_text = '';
1227
+ let response_started = false;
1228
+
1229
+ emitToDashboard = (type, content, params) => {
1230
+ if (!stream_enabled || !uid) return;
1231
+
1232
+ const is_stream_delta = type === 'stream_delta';
1233
+ const is_stream_end = type === 'stream_end';
1234
+ const seq = is_stream_delta ? ++stream_delta_seq : undefined;
1235
+ if (is_stream_delta) {
1236
+ stream_delta_text += content || '';
1237
+ }
1238
+
1239
+ ws_dashboard_msa.emit_message_to_dashboard({
1240
+ service: type,
1241
+ to: uid,
1242
+ data: {
1243
+ conversation_id,
1244
+ job_id,
1245
+ delta: content,
1246
+ ...(seq ? { seq } : {}),
1247
+ ...(is_stream_end
1248
+ ? {
1249
+ stream_id: response_conversation_item_id,
1250
+ final_seq: stream_delta_seq,
1251
+ text: stream_delta_text,
1252
+ }
1253
+ : {}),
1254
+ prompt_conversation_item_id,
1255
+ response_conversation_item_id,
1256
+ params,
1257
+ },
1258
+ });
1259
+ };
1260
+
1261
+ const ensureResponseStarted = function () {
1262
+ if (!response_started) {
1263
+ response_started = true;
1264
+ emitToDashboard('response_start');
1265
+ }
1266
+ };
1267
+
1268
+ streamText = function (text, chunk_size = 280) {
1269
+ if (!stream_enabled || !text) return;
1270
+ ensureResponseStarted();
1271
+ for (let i = 0; i < text.length; i += chunk_size) {
1272
+ emitToDashboard('stream_delta', text.slice(i, i + chunk_size));
1273
+ }
1274
+ };
1275
+
1276
+ if (!is_valid_codex_remote_host(ip)) {
1277
+ return { code: -1, data: 'invalid remote ip' };
1278
+ }
1279
+
1280
+ if (typeof prompt !== 'string' || !prompt.trim()) {
1281
+ return { code: -2, data: 'prompt is mandatory field' };
1282
+ }
1283
+
1284
+ if (!Array.isArray(attachments)) {
1285
+ attachments = attachments ? [attachments] : [];
1286
+ }
1287
+
1288
+ const reasoning = [];
1289
+ let streamed_agent_messages = new Set();
1290
+ const pushReasoning = function (type, text, metadata = {}) {
1291
+ if (!text) return;
1292
+ reasoning.push({
1293
+ type,
1294
+ text,
1295
+ ...metadata,
1296
+ });
1297
+ };
1298
+
1299
+ const handleCodexEvent = function (event) {
1300
+ if (!event || typeof event !== 'object') return;
1301
+
1302
+ switch (event.type) {
1303
+ case 'thread.started':
1304
+ emitToDashboard('stream_phase', 'Starting Codex thread', { update: true, thread_id: event.thread_id });
1305
+ break;
1306
+ case 'turn.started':
1307
+ emitToDashboard('stream_phase', 'Reasoning through Codex request', { update: true });
1308
+ break;
1309
+ case 'item.started': {
1310
+ const item = event.item || {};
1311
+ if (item.type === 'command_execution') {
1312
+ const command = item.command || '';
1313
+ pushReasoning('command_started', command, { item_id: item.id });
1314
+ emitToDashboard('stream_phase', 'Running remote command', { update: true, command });
1315
+ }
1316
+ break;
1317
+ }
1318
+ case 'item.completed': {
1319
+ const item = event.item || {};
1320
+ if (item.type === 'agent_message') {
1321
+ const text = item.text || '';
1322
+ pushReasoning('agent_message', text, { item_id: item.id });
1323
+ emitToDashboard('stream_phase', text.trim(), { update: true, reasoning: true });
1324
+ if (!streamed_agent_messages.has(item.id)) {
1325
+ streamed_agent_messages.add(item.id);
1326
+ streamText(`${text.trim()}\n\n`);
1327
+ }
1328
+ }
1329
+
1330
+ if (item.type === 'command_execution') {
1331
+ const command = item.command || '';
1332
+ const output = (item.aggregated_output || '').trim();
1333
+ pushReasoning('command_completed', command, {
1334
+ item_id: item.id,
1335
+ exit_code: item.exit_code,
1336
+ status: item.status,
1337
+ output: output.length > 4000 ? `${output.slice(0, 4000)}...` : output,
1338
+ });
1339
+ emitToDashboard('stream_phase', item.exit_code === 0 ? 'Remote command completed' : 'Remote command failed', { update: true, command, exit_code: item.exit_code });
1340
+ }
1341
+ break;
1342
+ }
1343
+ case 'error':
1344
+ pushReasoning('error', event.message);
1345
+ emitToDashboard('stream_phase', event.message, { update: true, error: true });
1346
+ break;
1347
+ case 'turn.completed':
1348
+ emitToDashboard('stream_phase', 'Codex request completed', { update: true, usage: event.usage });
1349
+ break;
1350
+ case 'turn.failed':
1351
+ pushReasoning('error', event.error?.message || 'Codex request failed');
1352
+ emitToDashboard('stream_phase', 'Codex request failed', { update: true, error: true });
1353
+ break;
1354
+ default:
1355
+ break;
1356
+ }
1357
+ };
1358
+
1359
+ const image_paths = [];
1360
+ const local_attachments = [];
1361
+ const remote_attachments = [];
1362
+
1363
+ for (const attachment of attachments) {
1364
+ const attachment_path = get_codex_remote_attachment_path(attachment);
1365
+ const local_attachment_path = get_codex_local_attachment_path(attachment);
1366
+
1367
+ if (local_attachment_path && is_codex_image_attachment(local_attachment_path)) {
1368
+ image_paths.push(local_attachment_path);
1369
+ }
1370
+
1371
+ if (local_attachment_path) {
1372
+ local_attachments.push(local_attachment_path);
1373
+ }
1374
+
1375
+ if (attachment_path) {
1376
+ remote_attachments.push(attachment_path);
1377
+ }
1378
+ }
1379
+
1380
+ emitToDashboard('stream_start');
1381
+ emitToDashboard('stream_phase', 'Starting Codex request');
1382
+
1383
+ let codex_prompt = `You are OpenAI Codex running on the local Xuda server.
1384
+
1385
+ The target remote machine is ${get_codex_remote_host(ip)}.
1386
+ Execute shell commands on the remote machine through SSH, for example:
1387
+ ssh -o BatchMode=yes -o StrictHostKeyChecking=no ${get_codex_remote_host(ip)} "<command>"
1388
+
1389
+ Do not assume the current local filesystem is the remote machine. Use SSH for inspection, edits, installs, and command execution unless the user explicitly asks for local work.
1390
+
1391
+ User request:
1392
+ ${prompt}`;
1393
+
1394
+ if (local_attachments.length) {
1395
+ codex_prompt += `\n\nAttachments available to Codex on the local Xuda server:\n${local_attachments.map((attachment_path) => `- ${attachment_path}`).join('\n')}`;
1396
+ }
1397
+
1398
+ if (remote_attachments.length) {
1399
+ codex_prompt += `\n\nAttachments available on the remote machine:\n${remote_attachments.map((attachment_path) => `- ${attachment_path}`).join('\n')}`;
1400
+ }
1401
+
1402
+ const openai_codex_command = get_openai_codex_cli_command();
1403
+ const codex_args = get_openai_codex_exec_args();
1404
+ for (const image_path of image_paths) {
1405
+ codex_args.push('--image', image_path);
1406
+ }
1407
+ codex_args.push('-');
1408
+
1409
+ const ret = await run_process(openai_codex_command, codex_args, codex_prompt, {
1410
+ env: {
1411
+ ...process.env,
1412
+ OPENAI_API_KEY: _conf.OPENAI_API_KEY,
1413
+ },
1414
+ onStdout: create_codex_jsonl_stream_parser(handleCodexEvent),
1415
+ });
1416
+ const events = parse_codex_jsonl(ret.stdout);
1417
+
1418
+ if (ret.exit_code !== 0) {
1419
+ emitToDashboard('stream_phase', 'Codex request failed', { update: true });
1420
+ streamText(`Codex request failed: ${ret.stderr || ret.stdout || `exit code ${ret.exit_code}`}`);
1421
+ emitToDashboard('stream_end');
1422
+
1423
+ return {
1424
+ code: -3,
1425
+ data: {
1426
+ provider: 'openai_codex_cli',
1427
+ remote_host: get_codex_remote_host(ip),
1428
+ command: `${openai_codex_command} ${codex_args.filter((arg) => arg !== '-').join(' ')}`,
1429
+ exit_code: ret.exit_code,
1430
+ stdout: ret.stdout,
1431
+ stderr: ret.stderr,
1432
+ events,
1433
+ reasoning,
1434
+ },
1435
+ };
1436
+ }
1437
+
1438
+ emitToDashboard('stream_end');
1439
+
1440
+ return {
1441
+ code: 0,
1442
+ data: {
1443
+ provider: 'openai_codex_cli',
1444
+ remote_host: get_codex_remote_host(ip),
1445
+ command: `${openai_codex_command} ${codex_args.filter((arg) => arg !== '-').join(' ')}`,
1446
+ stdout: ret.stdout,
1447
+ stderr: ret.stderr,
1448
+ events,
1449
+ reasoning,
1450
+ },
1451
+ };
1452
+ } catch (err) {
1453
+ console.error(err);
1454
+ emitToDashboard('stream_phase', 'Codex request failed', { update: true });
1455
+ streamText(`Codex request failed: ${err.message}`);
1456
+ emitToDashboard('stream_end');
1457
+ return { code: -1, data: err.message };
1458
+ }
1459
+ };
1460
+
1085
1461
  export const get_chat_conversation = async function (req) {
1086
1462
  let { conversation_id, order = 'asc', limit = 9999, skip = 0, _id, uid } = req;
1087
1463
 
@@ -1100,6 +1476,7 @@ export const get_chat_conversation = async function (req) {
1100
1476
  conversation_doc = await db_module.get_app_couch_doc_native(reference_doc.shared_from_app_id, reference_doc.share_item_id);
1101
1477
  conversation_doc.reference_doc = reference_doc;
1102
1478
  }
1479
+ conversation_doc.request_started_at = conversation_doc.request_started_at || null;
1103
1480
  let conversation_items = await db_module.find_app_couch_query(account_profile_info.app_id, {
1104
1481
  selector: {
1105
1482
  docType: 'chat_conversation_item',
@@ -1602,6 +1979,7 @@ export const unarchive_ai_chat = async function (req) {
1602
1979
  conversation_doc.stat = 3;
1603
1980
  conversation_doc.ts = Date.now();
1604
1981
  const save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, conversation_doc);
1982
+ await update_conversation_items_stat(account_profile_info.app_id, 3, conversation_id);
1605
1983
 
1606
1984
  return save_ret;
1607
1985
  } catch (err) {
@@ -2262,7 +2640,7 @@ export const uninstall_ai_agent = async function (req) {
2262
2640
  }
2263
2641
  };
2264
2642
  export const unarchive_ai_agent = async function (req) {
2265
- let { agent_id, uid } = req;
2643
+ let { agent_id, uid, conversation_id } = req;
2266
2644
 
2267
2645
  const account_profile_info = await get_active_account_profile_info(uid);
2268
2646
 
@@ -2277,8 +2655,9 @@ export const unarchive_ai_agent = async function (req) {
2277
2655
  agent_doc.ts = Date.now();
2278
2656
 
2279
2657
  const save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, agent_doc);
2658
+ const updated_conversation_items = await update_conversation_stat(account_profile_info.app_id, agent_id, 3, conversation_id);
2280
2659
 
2281
- return save_ret;
2660
+ return { code: 1, data: { save_ret, updated_conversation_items } };
2282
2661
  } catch (err) {
2283
2662
  return { code: -9, data: err.message };
2284
2663
  }
@@ -2286,11 +2665,12 @@ export const unarchive_ai_agent = async function (req) {
2286
2665
 
2287
2666
  const update_conversation_items_stat = async function (app_id, stat, target_conversation_id) {
2288
2667
  const ts = Date.now();
2668
+ const stat_selector = stat === 5 ? { $lt: 4 } : 5;
2289
2669
  const items_ret = await db_module.find_app_couch_query(app_id, {
2290
2670
  selector: {
2291
2671
  docType: 'chat_conversation_item',
2292
2672
  conversation_id: target_conversation_id,
2293
- stat: { $lt: 4 },
2673
+ stat: stat_selector,
2294
2674
  },
2295
2675
  limit: 9999,
2296
2676
  });
@@ -3682,7 +4062,7 @@ export const create_openai_conversation = async function () {
3682
4062
  };
3683
4063
 
3684
4064
  export const create_conversation = async function (req, job_id, headers) {
3685
- const { profile_id, uid, prompt, perform_ai_execution = true, email_id, direction = 'out', from_mailbox, date_created, ai_model = _conf.default_ai_model } = req;
4065
+ const { profile_id, uid, prompt, perform_ai_execution = true, email_id, direction = 'out', from_mailbox, date_created, ai_model = _conf.default_ai_model, plan_mode = false } = req;
3686
4066
  let { reference_type, reference_id = '', conversation_type, email_recipient_type } = req;
3687
4067
 
3688
4068
  const account_profile_info = await get_active_account_profile_info(uid, profile_id);
@@ -3737,6 +4117,7 @@ export const create_conversation = async function (req, job_id, headers) {
3737
4117
  from_mailbox,
3738
4118
  email_recipient_type,
3739
4119
  model: ai_model,
4120
+ plan_mode: normalize_boolean(plan_mode),
3740
4121
  };
3741
4122
  const save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, conversation_doc);
3742
4123
 
@@ -3925,10 +4306,63 @@ export const submit_chat_conversation = async function (req, job_id, headers) {
3925
4306
  const account_profile_info = await get_active_account_profile_info(uid, profile_id);
3926
4307
 
3927
4308
  let conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
4309
+ const normalized_plan_mode = normalize_boolean(req.plan_mode ?? conversation_doc.plan_mode);
4310
+ const route_to_studio = conversation_doc.conversation_type === 'studio' || req.conversation_type === 'studio' || normalized_plan_mode;
4311
+
4312
+ if (route_to_studio && conversation_doc.conversation_type !== 'studio') {
4313
+ conversation_doc.conversation_type = 'studio';
4314
+ conversation_doc.plan_mode = normalized_plan_mode;
4315
+ await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
4316
+ }
4317
+
4318
+ req.plan_mode = normalized_plan_mode;
3928
4319
  req.conversation_doc = conversation_doc;
3929
4320
  req.metadata = metadata || {};
3930
4321
  let ret;
3931
- if (conversation_doc.conversation_type === 'studio') {
4322
+
4323
+ switch (conversation_doc.reference_type) {
4324
+ case 'contacts': {
4325
+ req._thread_reentry = Date.now() - conversation_doc.date_created_ts > 60000;
4326
+
4327
+ account_msa.ts_contact(uid, conversation_doc.reference_id);
4328
+
4329
+ switch (conversation_doc.conversation_type) {
4330
+ case 'note': {
4331
+ ret = await chat_note(req, job_id, headers);
4332
+ break;
4333
+ }
4334
+ case 'ai_chat': {
4335
+ ret = await ai_chat_conversation(req, job_id, headers);
4336
+ break;
4337
+ }
4338
+
4339
+ case 'email': {
4340
+ ret = await chat_email(req, job_id, headers);
4341
+ break;
4342
+ }
4343
+
4344
+ case 'chat': {
4345
+ ret = await contact_chat_conversation(req, job_id, headers);
4346
+ break;
4347
+ }
4348
+
4349
+ default:
4350
+ ret = { code: -44, data: 'invalid conversation_type ' + conversation_doc.conversation_type };
4351
+ break;
4352
+ }
4353
+
4354
+ break;
4355
+ }
4356
+
4357
+ case 'studio': {
4358
+ break;
4359
+ }
4360
+
4361
+ default:
4362
+ break;
4363
+ }
4364
+
4365
+ if (route_to_studio) {
3932
4366
  ret = await chat_studio(req, job_id, headers);
3933
4367
  } else if (conversation_doc.reference_type === 'contacts') {
3934
4368
  req._thread_reentry = Date.now() - conversation_doc.date_created_ts > 60000;
@@ -4251,7 +4685,7 @@ const chat_studio = async function (req, job_id, headers) {
4251
4685
  let { prompt, conversation_doc, attachments = [], plan_mode = false, stream = true } = req;
4252
4686
 
4253
4687
  const conversation_id = conversation_doc._id;
4254
- const normalized_plan_mode = typeof plan_mode === 'string' ? ['true', '1', 'yes', 'on'].includes(plan_mode.toLowerCase()) : Boolean(plan_mode);
4688
+ const normalized_plan_mode = normalize_boolean(plan_mode);
4255
4689
  const save_result_to_studio = normalized_plan_mode ? false : true;
4256
4690
  const plugin_name = '@xuda.io/xuda-library-plugin-studio-ai-app-builder';
4257
4691
  const plugin_method = normalized_plan_mode ? 'app_planner' : 'app_builder';
@@ -4259,6 +4693,7 @@ const chat_studio = async function (req, job_id, headers) {
4259
4693
 
4260
4694
  const prompt_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
4261
4695
  const response_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
4696
+ const request_started_at = Date.now();
4262
4697
 
4263
4698
  let stream_delta_seq = 0;
4264
4699
  let stream_delta_text = '';
@@ -4276,6 +4711,7 @@ const chat_studio = async function (req, job_id, headers) {
4276
4711
  conversation_id,
4277
4712
  job_id,
4278
4713
  delta: content,
4714
+ ...(type === 'stream_start' ? { request_started_at } : {}),
4279
4715
  ...(seq ? { seq } : {}),
4280
4716
  ...(is_stream_end
4281
4717
  ? {
@@ -4432,6 +4868,7 @@ ${conversation_history || `User (studio): ${prompt}`}
4432
4868
 
4433
4869
  try {
4434
4870
  emitToDashboard('stream_start');
4871
+ if (normalized_plan_mode) emitToDashboard('plan_sream_start');
4435
4872
  emitToDashboard('stream_phase', normalized_plan_mode ? 'Planning studio request' : 'Building studio request');
4436
4873
 
4437
4874
  const conversation_item_reference_id = await add_conversation_item(uid, profile_id, conversation_id, prompt, 'studio', undefined, { plan_mode: normalized_plan_mode });
@@ -4470,6 +4907,7 @@ ${conversation_history || `User (studio): ${prompt}`}
4470
4907
  conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
4471
4908
  conversation_doc.ts = Date.now();
4472
4909
  conversation_doc.stat = 2;
4910
+ conversation_doc.request_started_at = request_started_at;
4473
4911
  conversation_doc.job_id = job_id;
4474
4912
  conversation_doc.plan_mode = normalized_plan_mode;
4475
4913
  conversation_doc.target_app_id = target_app_id;
@@ -4544,6 +4982,7 @@ ${conversation_history || `User (studio): ${prompt}`}
4544
4982
 
4545
4983
  emitToDashboard('stream_phase', normalized_plan_mode ? 'Streaming studio plan' : 'Streaming studio result', { update: true });
4546
4984
  streamText(response_text);
4985
+ if (normalized_plan_mode) emitToDashboard('plan_sream_end');
4547
4986
  emitToDashboard('stream_end');
4548
4987
 
4549
4988
  conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
@@ -4590,6 +5029,7 @@ ${conversation_history || `User (studio): ${prompt}`}
4590
5029
  const error_message = get_error_message(err, 'studio request failed');
4591
5030
  emitToDashboard('stream_phase', 'Studio request failed', { update: true });
4592
5031
  streamText(`Studio request failed: ${error_message}`);
5032
+ if (normalized_plan_mode) emitToDashboard('plan_sream_end');
4593
5033
  emitToDashboard('stream_end');
4594
5034
 
4595
5035
  return { code: -16, data: error_message };
@@ -4930,6 +5370,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
4930
5370
 
4931
5371
  const prompt_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
4932
5372
  const response_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
5373
+ const request_started_at = Date.now();
4933
5374
 
4934
5375
  let out_conversation_item_obj = {
4935
5376
  _id: prompt_conversation_item_id,
@@ -4971,6 +5412,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
4971
5412
  conversation_id,
4972
5413
  job_id,
4973
5414
  delta: content,
5415
+ ...(type === 'stream_start' ? { request_started_at } : {}),
4974
5416
  ...(seq ? { seq } : {}),
4975
5417
  ...(is_stream_end
4976
5418
  ? {
@@ -5106,6 +5548,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
5106
5548
 
5107
5549
  conversation_doc.stat = 2;
5108
5550
  conversation_doc.ts = Date.now();
5551
+ conversation_doc.request_started_at = request_started_at;
5109
5552
  conversation_doc.ai_agents = ai_agents;
5110
5553
  conversation_doc.job_id = job_id;
5111
5554
  await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
package/index_ms.mjs CHANGED
@@ -17,6 +17,10 @@
17
17
 
18
18
 
19
19
 
20
+ export const execute_codex_request = async function (...args) {
21
+ return await broker.send_to_queue("execute_codex_request", ...args);
22
+ };
23
+
20
24
  export const get_chat_conversation = async function (...args) {
21
25
  return await broker.send_to_queue("get_chat_conversation", ...args);
22
26
  };
package/index_msa.mjs CHANGED
@@ -17,6 +17,10 @@
17
17
 
18
18
 
19
19
 
20
+ export const execute_codex_request = function (...args) {
21
+ broker.send_to_queue_async("execute_codex_request", ...args);
22
+ };
23
+
20
24
  export const get_chat_conversation = function (...args) {
21
25
  broker.send_to_queue_async("get_chat_conversation", ...args);
22
26
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5589",
3
+ "version": "1.1.5591",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",
@@ -27,7 +27,8 @@
27
27
  },
28
28
  "devDependencies": {},
29
29
  "scripts": {
30
- "pub": "npm version patch --force && npm publish"
30
+ "pub": "npm version patch --force && npm publish",
31
+ "test:codex": "node tests/execute-codex-request.mjs"
31
32
  },
32
33
  "author": "Xuda Llc",
33
34
  "license": "ISC"
@@ -0,0 +1,74 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const repoRoot = path.resolve(__dirname, '../../..');
8
+
9
+ const parseArgs = function (argv) {
10
+ const args = { attachments: [] };
11
+
12
+ for (let i = 0; i < argv.length; i++) {
13
+ const token = argv[i];
14
+ const next = argv[i + 1];
15
+
16
+ if (token === '--ip' && next) {
17
+ args.ip = next;
18
+ i += 1;
19
+ continue;
20
+ }
21
+
22
+ if ((token === '--prompt' || token === '--prompat') && next) {
23
+ args.prompt = next;
24
+ i += 1;
25
+ continue;
26
+ }
27
+
28
+ if ((token === '--attachment' || token === '--attach') && next) {
29
+ args.attachments.push(next);
30
+ i += 1;
31
+ continue;
32
+ }
33
+
34
+ if (token === '--help') {
35
+ args.help = true;
36
+ }
37
+ }
38
+
39
+ return args;
40
+ };
41
+
42
+ const usage = `
43
+ Usage:
44
+ node cpi/ai_module/tests/execute-codex-request.mjs [options]
45
+
46
+ Options:
47
+ --ip <host> Remote server IP/host. Default: 134.122.125.174
48
+ --prompt <text> Codex prompt. Default: create locksmith website
49
+ --attachment <path> Remote attachment path. Can be repeated.
50
+ --help Show this help.
51
+
52
+ Environment:
53
+ XUDA_HOME Defaults to repo root.
54
+ XUDA_CONFIG Defaults to config_dev.json.
55
+ `;
56
+
57
+ const args = parseArgs(process.argv.slice(2));
58
+
59
+ if (args.help) {
60
+ console.log(usage.trim());
61
+ process.exit(0);
62
+ }
63
+
64
+ process.env.XUDA_HOME ||= repoRoot;
65
+ process.env.XUDA_CONFIG ||= 'config_dev.json';
66
+
67
+ const ip = args.ip || '134.122.125.174';
68
+ const prompt = args.prompt || 'create locksmith website';
69
+ const attachments = args.attachments;
70
+
71
+ const aiModule = await import('../index.mjs');
72
+ const response = await aiModule.execute_codex_request(ip, prompt, attachments);
73
+
74
+ console.log(JSON.stringify(response, null, 2));