| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- 'use strict';
- /**
- * 智能问数智能体 · 运行留痕与会话索引
- * ----------------------------------------------------------------------------
- * 文件布局(data/agent/,GENPIC_AGENT_DIR 环境变量可覆盖——smoke 隔离用):
- * <entityKey>/runs/<runId>.json 单次运行全量(步骤/事件/结论,成败都留痕)
- * <entityKey>/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;
- }
- module.exports = {
- AGENT_DIR, newRunRecord, pushEvent, persistRun, loadRun,
- loadSessions, upsertSessionRun, deleteSession, loadRecentConclusions, newId, sanitizeKey,
- };
|