@xuda.io/ai_module 1.1.5589 → 1.1.5590

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,59 @@ 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
+ default:
4358
+ break;
4359
+ }
4360
+
4361
+ if (route_to_studio) {
3932
4362
  ret = await chat_studio(req, job_id, headers);
3933
4363
  } else if (conversation_doc.reference_type === 'contacts') {
3934
4364
  req._thread_reentry = Date.now() - conversation_doc.date_created_ts > 60000;
@@ -4251,7 +4681,7 @@ const chat_studio = async function (req, job_id, headers) {
4251
4681
  let { prompt, conversation_doc, attachments = [], plan_mode = false, stream = true } = req;
4252
4682
 
4253
4683
  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);
4684
+ const normalized_plan_mode = normalize_boolean(plan_mode);
4255
4685
  const save_result_to_studio = normalized_plan_mode ? false : true;
4256
4686
  const plugin_name = '@xuda.io/xuda-library-plugin-studio-ai-app-builder';
4257
4687
  const plugin_method = normalized_plan_mode ? 'app_planner' : 'app_builder';
@@ -4259,6 +4689,7 @@ const chat_studio = async function (req, job_id, headers) {
4259
4689
 
4260
4690
  const prompt_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
4261
4691
  const response_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
4692
+ const request_started_at = Date.now();
4262
4693
 
4263
4694
  let stream_delta_seq = 0;
4264
4695
  let stream_delta_text = '';
@@ -4276,6 +4707,7 @@ const chat_studio = async function (req, job_id, headers) {
4276
4707
  conversation_id,
4277
4708
  job_id,
4278
4709
  delta: content,
4710
+ ...(type === 'stream_start' ? { request_started_at } : {}),
4279
4711
  ...(seq ? { seq } : {}),
4280
4712
  ...(is_stream_end
4281
4713
  ? {
@@ -4432,6 +4864,7 @@ ${conversation_history || `User (studio): ${prompt}`}
4432
4864
 
4433
4865
  try {
4434
4866
  emitToDashboard('stream_start');
4867
+ if (normalized_plan_mode) emitToDashboard('plan_sream_start');
4435
4868
  emitToDashboard('stream_phase', normalized_plan_mode ? 'Planning studio request' : 'Building studio request');
4436
4869
 
4437
4870
  const conversation_item_reference_id = await add_conversation_item(uid, profile_id, conversation_id, prompt, 'studio', undefined, { plan_mode: normalized_plan_mode });
@@ -4470,6 +4903,7 @@ ${conversation_history || `User (studio): ${prompt}`}
4470
4903
  conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
4471
4904
  conversation_doc.ts = Date.now();
4472
4905
  conversation_doc.stat = 2;
4906
+ conversation_doc.request_started_at = request_started_at;
4473
4907
  conversation_doc.job_id = job_id;
4474
4908
  conversation_doc.plan_mode = normalized_plan_mode;
4475
4909
  conversation_doc.target_app_id = target_app_id;
@@ -4544,6 +4978,7 @@ ${conversation_history || `User (studio): ${prompt}`}
4544
4978
 
4545
4979
  emitToDashboard('stream_phase', normalized_plan_mode ? 'Streaming studio plan' : 'Streaming studio result', { update: true });
4546
4980
  streamText(response_text);
4981
+ if (normalized_plan_mode) emitToDashboard('plan_sream_end');
4547
4982
  emitToDashboard('stream_end');
4548
4983
 
4549
4984
  conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
@@ -4590,6 +5025,7 @@ ${conversation_history || `User (studio): ${prompt}`}
4590
5025
  const error_message = get_error_message(err, 'studio request failed');
4591
5026
  emitToDashboard('stream_phase', 'Studio request failed', { update: true });
4592
5027
  streamText(`Studio request failed: ${error_message}`);
5028
+ if (normalized_plan_mode) emitToDashboard('plan_sream_end');
4593
5029
  emitToDashboard('stream_end');
4594
5030
 
4595
5031
  return { code: -16, data: error_message };
@@ -4930,6 +5366,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
4930
5366
 
4931
5367
  const prompt_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
4932
5368
  const response_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
5369
+ const request_started_at = Date.now();
4933
5370
 
4934
5371
  let out_conversation_item_obj = {
4935
5372
  _id: prompt_conversation_item_id,
@@ -4971,6 +5408,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
4971
5408
  conversation_id,
4972
5409
  job_id,
4973
5410
  delta: content,
5411
+ ...(type === 'stream_start' ? { request_started_at } : {}),
4974
5412
  ...(seq ? { seq } : {}),
4975
5413
  ...(is_stream_end
4976
5414
  ? {
@@ -5106,6 +5544,7 @@ const ai_chat_conversation = async function (req, job_id, headers) {
5106
5544
 
5107
5545
  conversation_doc.stat = 2;
5108
5546
  conversation_doc.ts = Date.now();
5547
+ conversation_doc.request_started_at = request_started_at;
5109
5548
  conversation_doc.ai_agents = ai_agents;
5110
5549
  conversation_doc.job_id = job_id;
5111
5550
  await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
package/index_ms.mjs CHANGED
@@ -221,6 +221,10 @@ export const get_person_info = async function (...args) {
221
221
  return await broker.send_to_queue("get_person_info", ...args);
222
222
  };
223
223
 
224
+ export const execute_codex_request = async function (...args) {
225
+ return await broker.send_to_queue("execute_codex_request", ...args);
226
+ };
227
+
224
228
  export const open_ai_alive_check = async function (...args) {
225
229
  return await broker.send_to_queue("open_ai_alive_check", ...args);
226
230
  };
package/index_msa.mjs CHANGED
@@ -221,6 +221,10 @@ export const get_person_info = function (...args) {
221
221
  broker.send_to_queue_async("get_person_info", ...args);
222
222
  };
223
223
 
224
+ export const execute_codex_request = function (...args) {
225
+ broker.send_to_queue_async("execute_codex_request", ...args);
226
+ };
227
+
224
228
  export const open_ai_alive_check = function (...args) {
225
229
  broker.send_to_queue_async("open_ai_alive_check", ...args);
226
230
  };
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.5590",
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));