| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- 'use strict';
- /**
- * 智能问数智能体 · 入口与运行注册表
- * ----------------------------------------------------------------------------
- * startRun 立即返回运行记录(前端轮询),循环异步执行;成败都折叠进 rec,
- * 绝不向上抛(对齐 ai-fde 委派网关「永不抛出」的契约)。
- * 双模式:quick = 复用 nl-query 两段式(省预算);agent = harness 循环(默认)。
- * 运行态在内存(仿 syncState),终态落盘 data/agent/(trace.js)。
- */
- 'use strict';
- const path = require('path');
- const trace = require('./trace.js');
- const loop = require('./loop.js');
- const skills = require('./skills.js');
- const replay = require('./replay.js');
- const { mockReply } = require('./mock-model.js');
- const { pickRow } = require('./tools/lake.js');
- const nlQuery = require('../../nl-query.js');
- const runs = new Map(); // runId → rec
- const MAX_CACHED = 60;
- function isMock() { return process.env.NAPOS_AGENT_MOCK === '1'; }
- const corpusFile = (entity) => path.join(trace.AGENT_DIR, trace.sanitizeKey(entity), 'corpus.json');
- /** 快问自动分流:短问句且不含分析类词 → 快问(nl-query 两段式),否则深度分析 */
- function pickMode(q) {
- if (String(q || '').length > 18) return 'agent';
- if (/(为什么|怎么|为何|对比|环比|同比|变化|体检|对标|排名|归因|分析|趋势|影响|跨|哪家|哪些|问题)/.test(String(q || ''))) return 'agent';
- return 'quick';
- }
- function assertChatCfg(cfg) {
- for (const [k, label] of [['chatApiBase', '对话模型地址'], ['chatApiKey', '对话模型 Key'], ['chatModel', '对话模型']]) {
- if (!cfg[k]) {
- const e = new Error(`对话模型未配置(缺少${label}):请到「系统设置 → API 设置 → 对话模型」选择模型后点「保存设置」`);
- e.code = 'ASK_NO_MODEL';
- throw e;
- }
- }
- }
- function trimCache() {
- if (runs.size <= MAX_CACHED) return;
- for (const [id, rec] of runs) {
- if (rec.status !== 'running') runs.delete(id);
- if (runs.size <= MAX_CACHED) break;
- }
- }
- /**
- * 发起一次运行并立即返回记录。
- * @param {object} o { entity, question, mode: 'agent'|'quick', sessionId, cfg, svc }
- */
- function startRun(o) {
- const { cfg, svc } = o;
- const q = String(o.question || '').trim();
- if (!q) { const e = new Error('请输入问题'); e.code = 'ASK_EMPTY'; throw e; }
- if (!isMock()) assertChatCfg(cfg);
- const entity = trace.sanitizeKey(o.entity);
- if (!svc.store.getEntity(entity)) {
- const e = new Error('数据主体不存在,请刷新页面重试');
- e.code = 'ASK_NO_ENTITY';
- throw e;
- }
- const rec = trace.newRunRecord({
- entity,
- entityName: (svc.store.getEntity(entity) || {}).name,
- question: q,
- mode: o.mode,
- sessionId: o.sessionId ? trace.sanitizeKey(o.sessionId) : undefined,
- });
- const skill = skills.get(o.skill);
- if (skill) rec.skill = skill.key;
- // 自动分流:mode='auto'(前端默认档)按规则选快问/深度分析
- if (o.mode === 'auto') {
- rec.mode = pickMode(q);
- rec.autoPicked = true;
- }
- runs.set(rec.runId, rec);
- trimCache();
- trace.upsertSessionRun(entity, {
- sessionId: rec.sessionId, runId: rec.runId, question: q, mode: rec.mode, status: 'running', at: rec.startedAt,
- });
- // 异步执行:路由已返回,前端轮询 GET /api/agent/runs/:id
- (async () => {
- try {
- if (isMock()) {
- // mock 模式:quick/agent 都走脚本化假模型(快问=直接 final);auto 已折算进 rec.mode
- rec.result = rec.mode === 'quick'
- ? { answer: '(mock 模型)快问链路正常。', chart: null, appliedFrom: null, thought: '' }
- : await loop.runAgent({ rec, svc, cfg, mock: { next: (r) => mockReply(r) } });
- } else if (rec.mode === 'quick') {
- const history = trace.loadRecentConclusions(entity, rec.sessionId);
- const result = await nlQuery.ask({ q, entity, cfg, svc, history });
- rec.result = {
- answer: result.reply,
- chart: result.chart && result.chart.type !== 'none' ? result.chart : null,
- appliedFrom: result.applied || null,
- thought: '',
- // 快问同样可一键打开当期 HTML 分析报告(区间取本次查询钳制后的区间)
- reportUrl: result.applied && result.applied.from && result.applied.to
- ? `/api/reports/report-html?${new URLSearchParams({ entity, from: result.applied.from, to: result.applied.to }).toString()}`
- : null,
- // 图表数据源(快问不走工具步骤,行随结果携带;去 raw 控体积)
- chartRows: (result.rows || []).slice(0, 200).map(pickRow),
- };
- } else {
- rec.history = trace.loadRecentConclusions(entity, rec.sessionId);
- // 打法复用与 few-shot:真实模型模式下,从已验证语料命中相似问
- let plan = null;
- let fewShotsText = '';
- if (!o.skill && !isMock()) {
- try {
- plan = replay.findPlan(corpusFile(entity), q);
- if (!plan) {
- const shots = replay.fewShot(corpusFile(entity), q);
- if (shots.length) {
- fewShotsText = shots.map((s) => `- 问「${s.question}」→ ${s.steps.map((st) => st.tool).join(' → ')}`).join('\n');
- rec.fewShots = shots.map((s) => s.question);
- }
- }
- } catch { /* 语料读写失败不影响正常问答 */ }
- }
- if (plan) rec.replayFrom = plan.question;
- rec.result = await loop.runAgent({ rec, svc, cfg, mock: null, cloud: o.cloud || null, plan, fewShotsText });
- }
- rec.steps.push({ type: 'final', answer: rec.result.answer });
- rec.status = 'done';
- } catch (e) {
- if (e && e.code === 'AGENT_CANCELLED') rec.status = 'cancelled';
- else {
- rec.status = 'failed';
- rec.error = String((e && e.message) || e).slice(0, 500);
- }
- } finally {
- rec.finishedAt = new Date().toISOString();
- rec.cancelRequested = false;
- trace.pushEvent(rec, 'run.end', { status: rec.status, error: rec.error });
- // 成功且数字全部溯源的运行沉淀为已验证打法(相似问复用 / few-shot 语料)
- if (rec.status === 'done' && rec.mode === 'agent' && !isMock()
- && rec.provenance && rec.provenance.untracedCount === 0) {
- const steps = rec.steps
- .filter((s) => s.type === 'tool' && s.ok && replay.ALLOW_TOOLS.has(s.tool))
- .map((s) => ({ tool: s.tool, args: s.args }));
- if (steps.length && steps.length <= 8) {
- try { replay.record(corpusFile(entity), { question: q, steps }); } catch { /* 语料失败不影响主流程 */ }
- }
- }
- trace.persistRun(rec);
- trace.upsertSessionRun(entity, {
- sessionId: rec.sessionId, runId: rec.runId, question: q, mode: rec.mode, status: rec.status, at: rec.finishedAt,
- });
- }
- })();
- return rec;
- }
- function cancelRun(runId) {
- const rec = runs.get(String(runId || ''));
- if (rec && rec.status === 'running') rec.cancelRequested = true;
- return rec || null;
- }
- /** 内存优先,落盘兜底(服务重启后仍可回放历史运行) */
- function getRun(entity, runId) {
- const key = trace.sanitizeKey(entity);
- const id = String(runId || '').replace(/[^a-zA-Z0-9_-]/g, '');
- const rec = runs.get(id);
- if (rec && rec.entity === key) return rec;
- const saved = trace.loadRun(key, id);
- return saved && saved.entity === key ? saved : null;
- }
- function listRuns(entity, sessionId, limit = 20) {
- const key = trace.sanitizeKey(entity);
- const session = trace.loadSessions(key).find((s) => s.sessionId === sessionId);
- return session ? session.runs.slice(0, limit) : [];
- }
- function listSessions(entity) {
- const key = trace.sanitizeKey(entity);
- return trace.loadSessions(key).map((s) => {
- const last = (s.runs || [])[0] || {};
- return {
- sessionId: s.sessionId,
- title: s.title,
- createdAt: s.createdAt,
- updatedAt: s.updatedAt,
- runCount: (s.runs || []).length,
- lastQuestion: last.question || '',
- lastAt: last.at || s.updatedAt || s.createdAt || null,
- };
- });
- }
- function deleteSession(entity, sessionId) {
- const key = trace.sanitizeKey(entity);
- const id = String(sessionId || '').replace(/[^a-zA-Z0-9_-]/g, '');
- return trace.deleteSession(key, id);
- }
- /** 答案反馈:采纳 → 打法强制入语料;纠错 → 反馈日志(供评测提炼) */
- function feedback({ entity, runId, kind, note }) {
- const rec = getRun(entity, runId);
- if (!rec) return null;
- rec.feedback = { kind: kind === 'up' ? 'up' : 'down', note: String(note || '').slice(0, 300), at: new Date().toISOString() };
- trace.persistRun(rec);
- if (rec.feedback.kind === 'up') {
- const steps = rec.steps
- .filter((s) => s.type === 'tool' && s.ok && replay.ALLOW_TOOLS.has(s.tool))
- .map((s) => ({ tool: s.tool, args: s.args }));
- if (steps.length) {
- try { replay.record(corpusFile(rec.entity), { question: rec.question, steps }); } catch { /* ignore */ }
- }
- } else {
- trace.appendFeedbackLog(rec.entity, {
- at: rec.feedback.at, runId: rec.runId, question: rec.question, answer: (rec.result || {}).answer || '', note: rec.feedback.note,
- });
- }
- return rec;
- }
- module.exports = { startRun, cancelRun, getRun, listRuns, listSessions, deleteSession, feedback };
|