Agent/utils/tool-args-enrich.ts
2026-08-06 22:17:39 +08:00

292 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
import { inferTimetableQuery as sharedInfer, normalizeTimetableQuery as sharedNormalize } from './timetable-query.js';
import * as sessionContext from '../core/session-context.js';
export function inferTimetableQuery(text) {
return sharedInfer(text);
}
export function normalizeTimetableQuery(raw) {
return sharedNormalize(raw);
}
const DAY_TYPE_TO_QUERY = {
WORKDAY: '工作日',
WORK_DAY: '工作日',
HOLIDAY: '节假日',
WEEKEND: '周末',
workday: '工作日',
holiday: '节假日',
};
function resolveTimetableQuery(args, userInput) {
const aliases = ['fileName', 'name', 'timetableName'];
let q = '';
for (const k of ['query', ...aliases]) {
const v = String(args[k] ?? '').trim();
if (v) { q = v; break; }
}
if (!q && args.dayType != null) {
const key = String(args.dayType).trim();
q = DAY_TYPE_TO_QUERY[key] || DAY_TYPE_TO_QUERY[key.toUpperCase()] || '';
}
if (!q && userInput) q = inferTimetableQuery(userInput);
if (q) q = normalizeTimetableQuery(q);
return q;
}
const EXTRACTORS = {
timetableQuery: {
infer: inferTimetableQuery,
normalize: normalizeTimetableQuery,
resolve: resolveTimetableQuery,
},
userInputAsIs: {
infer: (text) => String(text || '').trim(),
},
};
const TOOL_PARAM_RULES = {
metro_load_timetable_full: [{
field: 'query',
aliases: ['fileName', 'name', 'timetableName'],
extract: 'timetableQuery',
}],
metro_search_tools: [{
field: 'query',
extract: 'userInputAsIs',
onlyIfEmpty: true,
}],
};
function readFieldValue(args, field, aliases = []) {
const keys = [field, ...aliases];
for (const k of keys) {
const v = String(args[k] ?? '').trim();
if (v) return v;
}
return '';
}
function applyExtractor(name, value, userInput, args) {
const ext = EXTRACTORS[name];
if (!ext) return value;
if (ext.resolve) return ext.resolve(args || {}, userInput);
let v = value;
if (!v && userInput && ext.infer) v = ext.infer(userInput);
if (v && ext.normalize) v = ext.normalize(v);
return v;
}
const SETUP_CONFIRM_TOOLS = new Set(['metro_setup_rest_rules', 'metro_setup_handover_rules']);
const RULES_CONFIRM_RE = /^(确认|可以|没问题|执行|好的|同意|按这个|就这样|确认执行|确认设置)([。.!\s]*)$/;
const RULES_CONFIRM_LOOSE_RE = /确认(执行|设置|应用|写入)?|按(这个|此)?(清单|方案|规则)?(执行|设置|应用)?/;
function findLastProposedRules(messages) {
if (!Array.isArray(messages)) return null;
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m?.role !== 'tool') continue;
try {
const parsed = typeof m.content === 'string' ? JSON.parse(m.content) : m.content;
const result = parsed?.result || parsed;
if (Array.isArray(result?.proposedRules) && result.proposedRules.length) {
return result.proposedRules;
}
} catch (_) { }
}
return null;
}
function applySetupConfirmFromDialog(out, currentUserInput, messages) {
const ui = String(currentUserInput || '').trim();
if (!ui) return;
const wantsConfirm = RULES_CONFIRM_RE.test(ui) || RULES_CONFIRM_LOOSE_RE.test(ui);
if (!wantsConfirm) return;
out.stage = 'write';
out.confirmed = true;
if (!Array.isArray(out.rules) || out.rules.length === 0) {
const pending = findLastProposedRules(messages);
if (pending?.length) out.rules = pending;
}
}
export function enrichToolArguments(toolName, args, userInput, context = {}) {
const out = { ...(args || {}) };
const ui = String(userInput || '').trim();
const dialog = [context.recentDialog, ui].filter(Boolean).join('\n');
if (ui) out._userContext = ui;
const ctx = sessionContext.getState();
const sessionSnapshot = {};
if (ctx.currentEmployeeId) sessionSnapshot.currentEmployeeId = ctx.currentEmployeeId;
if (ctx.currentEmployeeName) sessionSnapshot.currentEmployeeName = ctx.currentEmployeeName;
if (ctx.currentTeam) sessionSnapshot.currentTeam = ctx.currentTeam;
if (ctx.currentMonth) sessionSnapshot.currentMonth = ctx.currentMonth;
if (ctx.lastQueryType) sessionSnapshot.lastQueryType = ctx.lastQueryType;
if (Object.keys(sessionSnapshot).length > 0) {
out._sessionContext = sessionSnapshot;
}
sessionContext.updateFromUserInput(ui);
const rules = TOOL_PARAM_RULES[toolName];
if (rules) {
for (const rule of rules) {
let val = readFieldValue(out, rule.field, rule.aliases);
if (rule.onlyIfEmpty && val) {
out[rule.field] = val;
continue;
}
val = applyExtractor(rule.extract, val, ui, out);
if (val) out[rule.field] = val;
}
}
if (toolName === 'metro_load_timetable_full') {
if (out.query && out.dayType) delete out.dayType;
applyAutoConfirmFromDialog(out, dialog, ui);
}
if (SETUP_CONFIRM_TOOLS.has(toolName)) {
applySetupConfirmFromDialog(out, ui, context.messages);
}
const userYearText = buildUserYearSourceText(ui, context.messages);
stripInventedProfileYear(toolName, out, userYearText);
normalizeProfileEmployeeArgs(toolName, out, ui, dialog);
if (Array.isArray(out.rules)) {
out.rules = out.rules.map(normalizeRuleFields);
}
return out;
}
function normalizeProfileEmployeeArgs(toolName, out, userInput, dialog) {
if (!String(toolName || '').startsWith('profile_')) return;
const id = out.employeeId;
if (id != null && String(id).trim() && /[\u4e00-\u9fff]/.test(String(id))) {
if (!out.employeeName) out.employeeName = String(id).trim();
delete out.employeeId;
}
if (!out.employeeName && userInput) {
const m = String(dialog || userInput).match(
/(?:查看|查一下|查询|生成.*?|帮我看|看看)?\s*([一-龥·]{2,4})\s*(?:的|老师傅|师傅)?(?:数据|画像|报告|评估)?/
);
const m2 = String(userInput).match(/([一-龥·]{2,4})的(?:数据|画像|报告|评估|考试|公里)/);
if (m2) out.employeeName = m2[1];
}
if (out.month === 0 || out.month === '0') delete out.month;
}
function buildUserYearSourceText(userInput, messages) {
const parts = [];
if (Array.isArray(messages)) {
for (const m of messages) {
if (m?.role === 'user' && typeof m.content === 'string' && m.content.trim()) {
parts.push(m.content.trim());
}
}
}
const ui = String(userInput || '').trim();
if (ui && !parts.includes(ui)) parts.push(ui);
return parts.join('\n');
}
function stripInventedProfileYear(toolName, out, userText) {
if (!String(toolName || '').startsWith('profile_')) return;
if (out.year == null || out.year === '') return;
const y = Number(out.year);
if (!Number.isFinite(y)) {
delete out.year;
return;
}
const allowed = extractExplicitYears(userText);
if (!allowed.includes(y)) {
delete out.year;
}
}
export function extractExplicitYears(text) {
const years = [];
const s = String(text || '');
const re = /(20\d{2})\s*年(?:度)?/g;
let m;
while ((m = re.exec(s))) {
const y = parseInt(m[1], 10);
if (!years.includes(y)) years.push(y);
}
return years;
}
const FIELD_ALIASES = {
stationName: ['stationName', 'siteId', 'siteName', 'stationId', 'station', 'name'],
count: ['count', 'num', 'number', 'people', 'persons', 'amount'],
startTime: ['startTime', 'start', 'begin', 'from', 'start_time'],
endTime: ['endTime', 'end', 'to', 'until', 'end_time'],
reason: ['reason', 'type', 'category'],
shift: ['shift', 'shiftType', '班制'],
};
function normalizeRuleFields(rule) {
if (!rule || typeof rule !== 'object') return rule;
const out = {};
for (const [canonical, aliases] of Object.entries(FIELD_ALIASES)) {
for (const alias of aliases) {
if (rule[alias] !== undefined) {
out[canonical] = rule[alias];
break;
}
}
}
for (const [k, v] of Object.entries(rule)) {
if (!Object.values(FIELD_ALIASES).flat().includes(k)) {
out[k] = v;
}
}
return out;
}
const AUTO_CONFIRM_DENY_RE = /不要删|不删|保留(?:部分)?|取消删除|别删|无需删除/;
const AUTO_CONFIRM_YES_RE = /自动(?:确认)?删除|直接(?:帮您)?(?:确认)?删除|不用问|自己删除|自动处理调试车|确认删除调试车|加载完成(?:后|时).{0,16}删(?:除)?调试车|会直接.{0,12}删(?:除)?调试车|直接帮您确认删除/i;
function applyAutoConfirmFromDialog(out, dialog, currentUserInput) {
if (out.autoConfirmDebugTrain === true) return;
const deny = AUTO_CONFIRM_DENY_RE.test(String(currentUserInput || ''));
if (deny) {
out.autoConfirmDebugTrain = false;
return;
}
if (AUTO_CONFIRM_YES_RE.test(String(dialog || ''))) {
out.autoConfirmDebugTrain = true;
}
}
export function buildRecentDialogText(messages, maxItems = 14) {
return (messages || [])
.filter(m => m.role === 'user' || m.role === 'assistant')
.slice(-maxItems)
.map(m => String(m.content || '').trim())
.filter(Boolean)
.join('\n');
}