'use strict'; /** * 智能问数智能体 · 运行留痕与会话索引 * ---------------------------------------------------------------------------- * 文件布局(data/agent/,GENPIC_AGENT_DIR 环境变量可覆盖——smoke 隔离用): * /runs/.json 单次运行全量(步骤/事件/结论,成败都留痕) * /sessions.json 会话索引(标题=首问,runs 摘要,双上限防膨胀) * 内存中的 run 注册表在 index.js;这里只管磁盘侧。历史结论回灌(压缩: * 只带问答文字,不带工具观察)供多轮追问。 */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const AGENT_DIR = process.env.GENPIC_AGENT_DIR ? path.resolve(process.env.GENPIC_AGENT_DIR) : path.join(__dirname, '..', '..', 'data', 'agent'); const MAX_SESSIONS = 30; const RUNS_PER_SESSION = 30; function readJsonSafe(file, fallback) { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return fallback; } } function writeJsonAtomic(file, obj) { fs.mkdirSync(path.dirname(file), { recursive: true }); const temp = file + '.tmp'; fs.writeFileSync(temp, JSON.stringify(obj), 'utf8'); fs.renameSync(temp, file); } function sanitizeKey(v) { return String(v || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64) || 'default'; } function newId(prefix) { return `${prefix}-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`; } // ---------- 运行记录 ---------- function newRunRecord({ entity, entityName, question, mode, sessionId }) { return { runId: newId('run'), entity: sanitizeKey(entity), entityName: entityName || entity, sessionId: sessionId || newId('s'), mode: mode === 'quick' ? 'quick' : 'agent', question: String(question || '').slice(0, 500), status: 'running', cancelRequested: false, steps: [], events: [], verdicts: [], provenance: null, error: null, usage: { llmCalls: 0, toolCalls: 0 }, result: null, startedAt: new Date().toISOString(), finishedAt: null, }; } function pushEvent(rec, type, payload = {}) { rec.events.push({ t: new Date().toISOString(), type, ...payload }); if (rec.events.length > 400) rec.events.splice(0, rec.events.length - 400); } function runFile(entityKey, runId) { return path.join(AGENT_DIR, sanitizeKey(entityKey), 'runs', `${String(runId).replace(/[^a-zA-Z0-9_-]/g, '')}.json`); } function persistRun(rec) { try { writeJsonAtomic(runFile(rec.entity, rec.runId), rec); } catch { /* 留痕失败不影响主流程 */ } } function loadRun(entityKey, runId) { return readJsonSafe(runFile(entityKey, runId), null); } // ---------- 会话索引 ---------- function sessionsFile(entityKey) { return path.join(AGENT_DIR, sanitizeKey(entityKey), 'sessions.json'); } function loadSessions(entityKey) { return readJsonSafe(sessionsFile(entityKey), { sessions: [] }).sessions; } function saveSessions(entityKey, sessions) { writeJsonAtomic(sessionsFile(entityKey), { sessions }); } /** 把一次运行登记进会话索引(会话不存在则以该问题为标题创建;双上限裁剪) */ function upsertSessionRun(entityKey, { sessionId, runId, question, mode, status, at }) { const sessions = loadSessions(entityKey); let session = sessions.find((s) => s.sessionId === sessionId); if (!session) { session = { sessionId, title: String(question || '新会话').slice(0, 40), createdAt: at, runs: [] }; sessions.unshift(session); } if (!Array.isArray(session.runs)) session.runs = []; session.updatedAt = at; const entry = { runId, question: String(question || '').slice(0, 200), mode, status, at }; const idx = session.runs.findIndex((r) => r.runId === runId); if (idx >= 0) session.runs[idx] = entry; else session.runs.unshift(entry); if (session.runs.length > RUNS_PER_SESSION) session.runs.length = RUNS_PER_SESSION; saveSessions(entityKey, sessions.slice(0, MAX_SESSIONS)); } /** 删除会话:摘除索引并清理该会话的全部运行文件 */ function deleteSession(entityKey, sessionId) { const sessions = loadSessions(entityKey); const idx = sessions.findIndex((s) => s.sessionId === sessionId); if (idx < 0) return false; const [gone] = sessions.splice(idx, 1); saveSessions(entityKey, sessions); for (const r of gone.runs || []) { try { fs.unlinkSync(runFile(entityKey, r.runId)); } catch { /* 已不存在 */ } } return true; } /** * 压缩历史:取该会话最近若干次成功运行的「问 / 答」文字回灌为新运行的上文。 * 工具观察一律不带(防上下文随轮次膨胀),模型需要的数据重新取。 */ function loadRecentConclusions(entityKey, sessionId, limit = 4) { const session = loadSessions(entityKey).find((s) => s.sessionId === sessionId); if (!session) return []; const done = session.runs.filter((r) => r.status === 'done').slice(0, limit); const out = []; for (const r of done.reverse()) { const rec = loadRun(entityKey, r.runId); if (!rec) continue; const answer = rec.result && rec.result.answer; if (!answer) continue; out.push({ role: 'user', content: rec.question }); out.push({ role: 'assistant', content: String(answer).slice(0, 1500) }); } return out; } /** 纠错反馈日志(问数答案「纠错」入口):cap 100,供金标准评测人工提炼 */ function feedbackFile(entityKey) { return path.join(AGENT_DIR, sanitizeKey(entityKey), 'feedback.json'); } function appendFeedbackLog(entityKey, entry) { let items = []; try { items = JSON.parse(fs.readFileSync(feedbackFile(entityKey), 'utf8')).items || []; } catch { /* 首条 */ } items.unshift({ ...entry }); if (items.length > 100) items.length = 100; writeJsonAtomic(feedbackFile(entityKey), { items }); } module.exports = { AGENT_DIR, newRunRecord, pushEvent, persistRun, loadRun, loadSessions, upsertSessionRun, deleteSession, loadRecentConclusions, appendFeedbackLog, newId, sanitizeKey, };