index.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. 'use strict';
  2. /**
  3. * 智能问数智能体 · 入口与运行注册表
  4. * ----------------------------------------------------------------------------
  5. * startRun 立即返回运行记录(前端轮询),循环异步执行;成败都折叠进 rec,
  6. * 绝不向上抛(对齐 ai-fde 委派网关「永不抛出」的契约)。
  7. * 双模式:quick = 复用 nl-query 两段式(省预算);agent = harness 循环(默认)。
  8. * 运行态在内存(仿 syncState),终态落盘 data/agent/(trace.js)。
  9. */
  10. const trace = require('./trace.js');
  11. const loop = require('./loop.js');
  12. const skills = require('./skills.js');
  13. const { mockReply } = require('./mock-model.js');
  14. const { pickRow } = require('./tools/lake.js');
  15. const nlQuery = require('../../nl-query.js');
  16. const runs = new Map(); // runId → rec
  17. const MAX_CACHED = 60;
  18. function isMock() { return process.env.NAPOS_AGENT_MOCK === '1'; }
  19. /** 快问自动分流:短问句且不含分析类词 → 快问(nl-query 两段式),否则深度分析 */
  20. function pickMode(q) {
  21. if (String(q || '').length > 18) return 'agent';
  22. if (/(为什么|怎么|为何|对比|环比|同比|变化|体检|对标|排名|归因|分析|趋势|影响|跨|哪家|哪些|问题)/.test(String(q || ''))) return 'agent';
  23. return 'quick';
  24. }
  25. function assertChatCfg(cfg) {
  26. for (const [k, label] of [['chatApiBase', '对话模型地址'], ['chatApiKey', '对话模型 Key'], ['chatModel', '对话模型']]) {
  27. if (!cfg[k]) {
  28. const e = new Error(`对话模型未配置(缺少${label}):请到「系统设置 → API 设置 → 对话模型」选择模型后点「保存设置」`);
  29. e.code = 'ASK_NO_MODEL';
  30. throw e;
  31. }
  32. }
  33. }
  34. function trimCache() {
  35. if (runs.size <= MAX_CACHED) return;
  36. for (const [id, rec] of runs) {
  37. if (rec.status !== 'running') runs.delete(id);
  38. if (runs.size <= MAX_CACHED) break;
  39. }
  40. }
  41. /**
  42. * 发起一次运行并立即返回记录。
  43. * @param {object} o { entity, question, mode: 'agent'|'quick', sessionId, cfg, svc }
  44. */
  45. function startRun(o) {
  46. const { cfg, svc } = o;
  47. const q = String(o.question || '').trim();
  48. if (!q) { const e = new Error('请输入问题'); e.code = 'ASK_EMPTY'; throw e; }
  49. if (!isMock()) assertChatCfg(cfg);
  50. const entity = trace.sanitizeKey(o.entity);
  51. if (!svc.store.getEntity(entity)) {
  52. const e = new Error('数据主体不存在,请刷新页面重试');
  53. e.code = 'ASK_NO_ENTITY';
  54. throw e;
  55. }
  56. const rec = trace.newRunRecord({
  57. entity,
  58. entityName: (svc.store.getEntity(entity) || {}).name,
  59. question: q,
  60. mode: o.mode,
  61. sessionId: o.sessionId ? trace.sanitizeKey(o.sessionId) : undefined,
  62. });
  63. const skill = skills.get(o.skill);
  64. if (skill) rec.skill = skill.key;
  65. // 自动分流:mode='auto'(前端默认档)按规则选快问/深度分析
  66. if (o.mode === 'auto') {
  67. rec.mode = pickMode(q);
  68. rec.autoPicked = true;
  69. }
  70. runs.set(rec.runId, rec);
  71. trimCache();
  72. trace.upsertSessionRun(entity, {
  73. sessionId: rec.sessionId, runId: rec.runId, question: q, mode: rec.mode, status: 'running', at: rec.startedAt,
  74. });
  75. // 异步执行:路由已返回,前端轮询 GET /api/agent/runs/:id
  76. (async () => {
  77. try {
  78. if (isMock()) {
  79. // mock 模式:quick/agent 都走脚本化假模型(快问=直接 final);auto 已折算进 rec.mode
  80. rec.result = rec.mode === 'quick'
  81. ? { answer: '(mock 模型)快问链路正常。', chart: null, appliedFrom: null, thought: '' }
  82. : await loop.runAgent({ rec, svc, cfg, mock: { next: (r) => mockReply(r) } });
  83. } else if (rec.mode === 'quick') {
  84. const history = trace.loadRecentConclusions(entity, rec.sessionId);
  85. const result = await nlQuery.ask({ q, entity, cfg, svc, history });
  86. rec.result = {
  87. answer: result.reply,
  88. chart: result.chart && result.chart.type !== 'none' ? result.chart : null,
  89. appliedFrom: result.applied || null,
  90. thought: '',
  91. // 快问同样可一键打开当期 HTML 分析报告(区间取本次查询钳制后的区间)
  92. reportUrl: result.applied && result.applied.from && result.applied.to
  93. ? `/api/reports/report-html?${new URLSearchParams({ entity, from: result.applied.from, to: result.applied.to }).toString()}`
  94. : null,
  95. // 图表数据源(快问不走工具步骤,行随结果携带;去 raw 控体积)
  96. chartRows: (result.rows || []).slice(0, 200).map(pickRow),
  97. };
  98. } else {
  99. rec.history = trace.loadRecentConclusions(entity, rec.sessionId);
  100. rec.result = await loop.runAgent({ rec, svc, cfg, mock: null, cloud: o.cloud || null });
  101. }
  102. rec.steps.push({ type: 'final', answer: rec.result.answer });
  103. rec.status = 'done';
  104. } catch (e) {
  105. if (e && e.code === 'AGENT_CANCELLED') rec.status = 'cancelled';
  106. else {
  107. rec.status = 'failed';
  108. rec.error = String((e && e.message) || e).slice(0, 500);
  109. }
  110. } finally {
  111. rec.finishedAt = new Date().toISOString();
  112. rec.cancelRequested = false;
  113. trace.pushEvent(rec, 'run.end', { status: rec.status, error: rec.error });
  114. trace.persistRun(rec);
  115. trace.upsertSessionRun(entity, {
  116. sessionId: rec.sessionId, runId: rec.runId, question: q, mode: rec.mode, status: rec.status, at: rec.finishedAt,
  117. });
  118. }
  119. })();
  120. return rec;
  121. }
  122. function cancelRun(runId) {
  123. const rec = runs.get(String(runId || ''));
  124. if (rec && rec.status === 'running') rec.cancelRequested = true;
  125. return rec || null;
  126. }
  127. /** 内存优先,落盘兜底(服务重启后仍可回放历史运行) */
  128. function getRun(entity, runId) {
  129. const key = trace.sanitizeKey(entity);
  130. const id = String(runId || '').replace(/[^a-zA-Z0-9_-]/g, '');
  131. const rec = runs.get(id);
  132. if (rec && rec.entity === key) return rec;
  133. const saved = trace.loadRun(key, id);
  134. return saved && saved.entity === key ? saved : null;
  135. }
  136. function listRuns(entity, sessionId, limit = 20) {
  137. const key = trace.sanitizeKey(entity);
  138. const session = trace.loadSessions(key).find((s) => s.sessionId === sessionId);
  139. return session ? session.runs.slice(0, limit) : [];
  140. }
  141. function listSessions(entity) {
  142. const key = trace.sanitizeKey(entity);
  143. return trace.loadSessions(key).map((s) => {
  144. const last = (s.runs || [])[0] || {};
  145. return {
  146. sessionId: s.sessionId,
  147. title: s.title,
  148. createdAt: s.createdAt,
  149. updatedAt: s.updatedAt,
  150. runCount: (s.runs || []).length,
  151. lastQuestion: last.question || '',
  152. lastAt: last.at || s.updatedAt || s.createdAt || null,
  153. };
  154. });
  155. }
  156. function deleteSession(entity, sessionId) {
  157. const key = trace.sanitizeKey(entity);
  158. const id = String(sessionId || '').replace(/[^a-zA-Z0-9_-]/g, '');
  159. return trace.deleteSession(key, id);
  160. }
  161. module.exports = { startRun, cancelRun, getRun, listRuns, listSessions, deleteSession };