response-future-promise-verifier.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. 'use strict';
  2. const FUTURE_PROMISE_REASON_CODES = Object.freeze({
  3. NO_ACTION_CLAIMS: 'no_action_claims',
  4. SAFE_CONDITIONAL_OFFER: 'safe_conditional_offer',
  5. SAFE_MISSING_INPUT_REQUEST: 'safe_missing_input_request',
  6. FUTURE_ACTION_VERIFIED: 'future_action_verified',
  7. PERFORMED_ACTION_VERIFIED: 'performed_action_verified',
  8. IN_PROGRESS_ACTION_VERIFIED: 'in_progress_action_verified',
  9. FUTURE_ACTION_MISSING_EVIDENCE: 'future_action_missing_evidence',
  10. PERFORMED_ACTION_MISSING_EVIDENCE: 'performed_action_missing_evidence',
  11. IN_PROGRESS_ACTION_MISSING_EVIDENCE: 'in_progress_action_missing_evidence',
  12. ACTION_EVIDENCE_FAILED: 'action_evidence_failed',
  13. ACTION_EVIDENCE_UNRELATED: 'action_evidence_unrelated',
  14. });
  15. const ACTION_RULES = Object.freeze([
  16. { id: 'handoff', claim: /(?:转人工|转交|转给|转至|移交|升级|接管|安排.{0,6}(?:专员|顾问|主管|负责人|人工))/, evidence: /(?:handoff|transfer|escalat|assign.?human|human.?agent|转人工|转交|移交|升级|接管)/i },
  17. { id: 'callback', claim: /(?:回电|致电|电话联系|打电话|来电联系)/, evidence: /(?:callback|phone.?call|call.?customer|dial|回电|致电|电话联系)/i },
  18. { id: 'refund', claim: /(?:退款|退费|返款)/, evidence: /(?:refund|repay|退款|退费|返款)/i },
  19. { id: 'cancel', claim: /(?:取消|撤销|关闭)(?:订单|预约|申请|服务|合同)?/, evidence: /(?:cancel|revoke|close.?order|取消|撤销)/i },
  20. { id: 'book', claim: /(?:预约|预订|预定|安排看房|安排带看|排期)/, evidence: /(?:book|schedule|appointment|viewing|预约|预订|排期|带看)/i },
  21. { id: 'reserve', claim: /(?:保留|锁定|预留)(?:名额|库存|房源|档期|价格)?/, evidence: /(?:reserve|hold|lock|保留|锁定|预留)/i },
  22. { id: 'submit', claim: /(?:提交|登记|报备|发起|创建)(?:申请|工单|任务|订单|记录|报备)?/, evidence: /(?:submit|register|report|create.?(?:ticket|task|order)|ticket|提交|登记|报备|工单)/i },
  23. { id: 'notify', claim: /(?:通知|同步|告知|回复|答复|反馈|回信|发给您|发送给您|更新进度)/, evidence: /(?:notify|message|send.?text|send.?message|reply|outbound|deliver|通知|同步|告知|回复|发送)/i },
  24. { id: 'verify', claim: /(?:核实|核验|核对|查验|查询|查看|检查|确认|查清|了解清楚)/, evidence: /(?:verify|check|query|lookup|search|fetch|get.?detail|inspect|核实|核验|核对|查询|检查|确认)/i },
  25. { id: 'process', claim: /(?:处理|跟进|推进|办理|关注|解决)/, evidence: /(?:process|follow.?up|workflow|handle|resolve|task|处理|跟进|推进|办理|解决)/i },
  26. ]);
  27. const SCOPE_RULES = Object.freeze([
  28. { id: 'price', claim: /(?:价格|报价|费用|金额|最低价|最高价|首付|月供)/, evidence: /(?:价格|报价|费用|金额|最低价|最高价|首付|月供|price|quote|cost|amount)/i },
  29. { id: 'inventory', claim: /(?:库存|在售|名额|余量|房源)/, evidence: /(?:库存|在售|名额|余量|房源|inventory|stock|availability|listing|property)/i },
  30. { id: 'schedule', claim: /(?:档期|时间|日期|排期|看房|带看|预约)/, evidence: /(?:档期|时间|日期|排期|看房|带看|预约|schedule|time|date|viewing|appointment)/i },
  31. { id: 'school', claim: /(?:学校|学区|入学|学位)/, evidence: /(?:学校|学区|入学|学位|school|education|enrollment)/i },
  32. { id: 'rights', claim: /(?:产权|权属|合同|抵押|查封)/, evidence: /(?:产权|权属|合同|抵押|查封|title|ownership|contract|mortgage|lien)/i },
  33. { id: 'qualification', claim: /(?:资格|资质|政策|贷款|利率|税费)/, evidence: /(?:资格|资质|政策|贷款|利率|税费|qualification|policy|loan|rate|tax)/i },
  34. { id: 'order', claim: /(?:订单|申请|工单|任务|编号)/, evidence: /(?:订单|申请|工单|任务|编号|order|application|ticket|task|request.?id)/i },
  35. { id: 'refund', claim: /(?:退款|退费|返款)/, evidence: /(?:退款|退费|返款|refund|repay)/i },
  36. { id: 'contact', claim: /(?:电话|回电|手机号|联系人)/, evidence: /(?:电话|回电|手机号|联系人|phone|call|mobile|contact)/i },
  37. ]);
  38. const COMPLETED_CUE = /(?:已|已经|刚刚|刚才|已成功|已经成功).{0,8}(?:核实|核验|查验|查询|查看|检查|确认|通知|同步|告知|回复|答复|反馈|回信|回电|致电|电话联系|转人工|转交|转给|转至|移交|升级|接管|安排|退款|退费|返款|取消|撤销|关闭|预约|预订|预定|排期|保留|锁定|预留|提交|登记|报备|发起|创建|处理|跟进|推进|办理|解决)/;
  39. const IN_PROGRESS_CUE = /(?:正在|正为您|已经在|已开始|处理中|跟进中|办理中).{0,8}(?:核实|核验|查验|查询|查看|检查|确认|通知|同步|告知|回复|答复|反馈|回信|回电|致电|电话联系|转人工|转交|转给|转至|移交|升级|接管|安排|退款|退费|返款|取消|撤销|关闭|预约|预订|预定|排期|保留|锁定|预留|提交|登记|报备|发起|创建|处理|跟进|推进|办理|解决)|(?:处理|跟进|办理)中/;
  40. const FUTURE_CUE = /(?:^|(?:我|我们|这边|客服|专员|顾问|主管|负责人|人工|系统|平台))\s*(?:会|将|再|稍后|晚点|随后|之后|过会儿|马上|尽快|今天内|明天|我去|我们去)/;
  41. const ACTION_AFTER_CUE = /(?:核实|核验|查询|确认|处理|跟进|提交|转交|安排|办理)后.{0,16}(?:会|将|再|通知|同步|回复|答复|反馈|回电)/;
  42. const CONDITIONAL_OFFER = /(?:如果|如需|若|要是).{0,18}(?:需要|愿意|同意|方便|确认)|(?:是否|要不要|需不需要|需要我|要我|可否).{0,24}(?:核实|核验|查询|确认|通知|回电|转交|安排|处理|提交)|(?:我|我们|这边)?\s*(?:可以|可|能够|能).{0,18}(?:核实|核验|查询|确认|通知|回电|转交|安排|处理|提交).{0,18}(?:吗|呢|是否|要不要|需要)/;
  43. const MISSING_INPUT_REQUEST = /(?:请|麻烦|需要您|您先|等您|收到您).{0,28}(?:提供|告知|告诉|发送|发我|补充|确认|选择|回复).{0,40}(?:后|以后|之后|再)|(?:您|客户).{0,18}(?:提供|发送|发来|发我|补充|告知|确认|选择).{0,40}(?:后|以后|之后)/;
  44. const SAFE_NECESSITY = /(?:^|[,,])\s*(?:当前|目前|暂时|现阶段)?\s*(?:还|尚)?(?:需要|需|待|建议|最好|应当|必须).{0,8}(?:核实|核验|查询|确认|人工处理|转人工)/;
  45. const DECLARATIVE_VERIFICATION_NEED = /(?:需要|待)(?:由)?(?:人工|专员|顾问|主管|负责人)?(?:核验|核实|核对|确认|复核|查询|检查)/;
  46. const FIRST_PERSON_FUTURE_ACTION = /(?:我|我们|这边).{0,8}(?:会|将|稍后|晚点|随后|马上|尽快|现在|去)(?:.{0,12})(?:核验|核实|核对|确认|复核|查询|检查|回复|通知|转交|处理)/;
  47. const FIRST_PERSON_ACTIVE_ACTION = /(?:我|我们|这边).{0,8}(?:现在|正在|马上|立即|已开始).{0,12}(?:核验|核实|核对|确认|复核|查询|检查|回复|通知|回电|转交|转人工|处理|跟进|提交|退款|取消|预约|预订|保留)/;
  48. const DECLARATIVE_CONFIRMED_FACT = /^(?:当前)?(?:已确认|经核验|核验结果).{0,30}(?:\d|价格|费用|库存|名额|档期|日期|时间|资格|政策|产权|权属|税费|利率)/;
  49. const FAILURE_STATUS = /^(?:fail|failed|failure|error|errored|rejected|cancelled|canceled|timeout|timed_out)$/i;
  50. const SUCCESS_STATUS = /^(?:ok|success|succeeded|complete|completed|done|sent|delivered|processed|accepted|created|started|in_progress)$/i;
  51. function makeCheck(id, passed, detail, hard = true) {
  52. return { id, passed: Boolean(passed), weight: 1, hard: Boolean(hard), detail: String(detail || '') };
  53. }
  54. function asArray(value) {
  55. if (value === null || value === undefined) return [];
  56. return Array.isArray(value) ? value : [value];
  57. }
  58. function compactText(value) {
  59. if (value === null || value === undefined) return '';
  60. if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
  61. try {
  62. return JSON.stringify(value);
  63. } catch {
  64. return '';
  65. }
  66. }
  67. function explicitSuccess(value) {
  68. if (!value || typeof value !== 'object') return false;
  69. if (value.isError === true || value.error || FAILURE_STATUS.test(String(value.status || ''))) return false;
  70. if (value.success === true || value.ok === true || value.delivered === true || value.sent === true || value.isSendSuccess === true) return true;
  71. if (SUCCESS_STATUS.test(String(value.status || ''))) return true;
  72. if (value.result && typeof value.result === 'object') return explicitSuccess(value.result);
  73. return false;
  74. }
  75. function actionIdsFromText(value = '') {
  76. const text = String(value || '');
  77. return ACTION_RULES.filter(rule => rule.evidence.test(text)).map(rule => rule.id);
  78. }
  79. function normalizeActionId(value = '') {
  80. const normalized = String(value || '').trim().toLowerCase();
  81. if (!normalized) return '';
  82. if (ACTION_RULES.some(rule => rule.id === normalized)) return normalized;
  83. return actionIdsFromText(normalized)[0] || '';
  84. }
  85. function evidenceRef(source, index, actionIds, item = {}) {
  86. return {
  87. source,
  88. index,
  89. actionTypes: [...new Set(actionIds)],
  90. name: String(item.tool || item.name || item.type || item.action || source),
  91. };
  92. }
  93. function normalizeEvidenceItem(item, source, index, forcedAction) {
  94. if (!item || typeof item !== 'object') return null;
  95. const name = [item.type, item.action, item.category, item.tool, item.name].filter(Boolean).join(' ');
  96. const explicitAction = normalizeActionId(forcedAction || item.actionType || item.action || item.type || item.category);
  97. const actionIds = [...new Set([explicitAction, ...actionIdsFromText(name)].filter(Boolean))];
  98. return {
  99. source,
  100. success: explicitSuccess(item),
  101. actionIds,
  102. searchable: compactText({
  103. action: item.action,
  104. type: item.type,
  105. tool: item.tool,
  106. name: item.name,
  107. input: item.input,
  108. args: item.args,
  109. parameters: item.parameters,
  110. result: item.result,
  111. subject: item.subject,
  112. scope: item.scope,
  113. }),
  114. ref: evidenceRef(source, index, actionIds, item),
  115. };
  116. }
  117. function normalizeActionEvidence(input = {}) {
  118. const evidence = [];
  119. const append = (value, source, forcedAction) => {
  120. asArray(value).forEach((item, index) => {
  121. const normalized = normalizeEvidenceItem(item, source, index, forcedAction);
  122. if (normalized) evidence.push(normalized);
  123. });
  124. };
  125. append(input.toolTrace, 'tool', '');
  126. append(input.evidence, 'evidence', '');
  127. append(input.handoffEvidence, 'handoff', 'handoff');
  128. append(input.outboundEvidence, 'outbound', 'notify');
  129. return evidence;
  130. }
  131. function splitReply(reply = '') {
  132. const result = [];
  133. String(reply || '')
  134. .split(/[。!?!?;;\n]+/)
  135. .map(sentence => sentence.trim())
  136. .filter(Boolean)
  137. .forEach((sentence, sentenceIndex) => {
  138. sentence.split(/[,,]+/).map(clause => clause.trim()).filter(Boolean).forEach((clause, clauseIndex) => {
  139. result.push({ sentence, sentenceIndex, clause, clauseIndex });
  140. });
  141. });
  142. return result;
  143. }
  144. function claimKind(clause = '', sentence = '') {
  145. const value = String(clause || '').trim();
  146. if (FIRST_PERSON_ACTIVE_ACTION.test(value)) return 'in_progress';
  147. if (IN_PROGRESS_CUE.test(value)) return 'in_progress';
  148. if (COMPLETED_CUE.test(value)) return 'completed';
  149. if (FUTURE_CUE.test(value) || ACTION_AFTER_CUE.test(sentence)) return 'future';
  150. return '';
  151. }
  152. function safeReason(sentence = '', clause = '') {
  153. const value = `${sentence} ${clause}`;
  154. if (CONDITIONAL_OFFER.test(value)) return FUTURE_PROMISE_REASON_CODES.SAFE_CONDITIONAL_OFFER;
  155. if (MISSING_INPUT_REQUEST.test(value)) return FUTURE_PROMISE_REASON_CODES.SAFE_MISSING_INPUT_REQUEST;
  156. return '';
  157. }
  158. function claimActionIds(clause = '') {
  159. return ACTION_RULES.filter(rule => rule.claim.test(clause)).map(rule => rule.id);
  160. }
  161. function scopeRequirements(claim = '') {
  162. const numbers = String(claim || '').match(/\d+(?:\.\d+)?/g) || [];
  163. const scopes = SCOPE_RULES.filter(rule => rule.claim.test(claim));
  164. return { numbers: [...new Set(numbers)], scopes };
  165. }
  166. function evidenceCoversClaim(evidence, claim = '') {
  167. const { numbers, scopes } = scopeRequirements(claim);
  168. const searchable = String(evidence.searchable || '');
  169. if (!numbers.every(number => searchable.includes(number))) return false;
  170. return scopes.every(scope => scope.evidence.test(searchable));
  171. }
  172. function missingReason(kind) {
  173. if (kind === 'completed') return FUTURE_PROMISE_REASON_CODES.PERFORMED_ACTION_MISSING_EVIDENCE;
  174. if (kind === 'in_progress') return FUTURE_PROMISE_REASON_CODES.IN_PROGRESS_ACTION_MISSING_EVIDENCE;
  175. return FUTURE_PROMISE_REASON_CODES.FUTURE_ACTION_MISSING_EVIDENCE;
  176. }
  177. function verifiedReason(kind) {
  178. if (kind === 'completed') return FUTURE_PROMISE_REASON_CODES.PERFORMED_ACTION_VERIFIED;
  179. if (kind === 'in_progress') return FUTURE_PROMISE_REASON_CODES.IN_PROGRESS_ACTION_VERIFIED;
  180. return FUTURE_PROMISE_REASON_CODES.FUTURE_ACTION_VERIFIED;
  181. }
  182. function assessClaim(claim, evidence) {
  183. if (!claim.requiresEvidence) return claim;
  184. const sameAction = evidence.filter(item => item.actionIds.includes(claim.action));
  185. const successful = sameAction.filter(item => item.success);
  186. const matched = successful.filter(item => evidenceCoversClaim(item, claim.text));
  187. if (matched.length) {
  188. return {
  189. ...claim,
  190. evidenceMatched: true,
  191. evidenceRefs: matched.map(item => item.ref),
  192. reasonCode: verifiedReason(claim.kind),
  193. };
  194. }
  195. if (sameAction.some(item => !item.success)) {
  196. return { ...claim, reasonCode: FUTURE_PROMISE_REASON_CODES.ACTION_EVIDENCE_FAILED };
  197. }
  198. if (evidence.length > 0) {
  199. return { ...claim, reasonCode: FUTURE_PROMISE_REASON_CODES.ACTION_EVIDENCE_UNRELATED };
  200. }
  201. return { ...claim, reasonCode: missingReason(claim.kind) };
  202. }
  203. function extractActionClaims(reply = '') {
  204. const claims = [];
  205. for (const part of splitReply(reply)) {
  206. if (DECLARATIVE_CONFIRMED_FACT.test(part.clause) && !/(?:我|我们|这边)/.test(part.clause)) continue;
  207. const safe = safeReason(part.sentence, part.clause);
  208. const kind = claimKind(part.clause, part.sentence) || (safe ? 'future' : '');
  209. const declarativeNeed = DECLARATIVE_VERIFICATION_NEED.test(part.clause)
  210. && !FIRST_PERSON_FUTURE_ACTION.test(part.clause);
  211. if (!kind || SAFE_NECESSITY.test(part.clause) || declarativeNeed) continue;
  212. for (const action of claimActionIds(part.clause)) {
  213. claims.push({
  214. id: `claim_${claims.length + 1}`,
  215. sentenceIndex: part.sentenceIndex,
  216. clauseIndex: part.clauseIndex,
  217. text: part.clause,
  218. kind,
  219. action,
  220. conditional: Boolean(safe),
  221. requiresEvidence: !safe,
  222. evidenceMatched: false,
  223. evidenceRefs: [],
  224. reasonCode: safe || '',
  225. });
  226. }
  227. }
  228. return claims;
  229. }
  230. function verifyFuturePromises(input = {}) {
  231. const reply = String(input.reply ?? input.content ?? '').trim();
  232. const evidence = normalizeActionEvidence(input);
  233. const claims = extractActionClaims(reply).map(claim => assessClaim(claim, evidence));
  234. const futureClaims = claims.filter(claim => claim.kind === 'future' && claim.requiresEvidence);
  235. const performedClaims = claims.filter(claim => claim.kind !== 'future' && claim.requiresEvidence);
  236. const conditionalClaims = claims.filter(claim => !claim.requiresEvidence);
  237. const futurePassed = futureClaims.every(claim => claim.evidenceMatched);
  238. const performedPassed = performedClaims.every(claim => claim.evidenceMatched);
  239. const checks = [
  240. makeCheck('future_promise_evidence', futurePassed, `${futureClaims.filter(claim => claim.evidenceMatched).length}/${futureClaims.length} 个未来动作声明有同类型成功证据`),
  241. makeCheck('performed_action_evidence', performedPassed, `${performedClaims.filter(claim => claim.evidenceMatched).length}/${performedClaims.length} 个已执行或处理中声明有同类型成功证据`),
  242. makeCheck('conditional_proposal_safety', true, `${conditionalClaims.length} 个条件提议或缺失信息请求无需执行证据`, false),
  243. ];
  244. const failedClaimIds = claims.filter(claim => claim.requiresEvidence && !claim.evidenceMatched).map(claim => claim.id);
  245. const reasonCodes = claims.length
  246. ? [...new Set(claims.map(claim => claim.reasonCode).filter(Boolean))]
  247. : [FUTURE_PROMISE_REASON_CODES.NO_ACTION_CLAIMS];
  248. return {
  249. version: '1.0',
  250. passed: checks.every(check => check.passed || !check.hard),
  251. checks,
  252. claims,
  253. failedClaimIds,
  254. reasonCodes,
  255. };
  256. }
  257. module.exports = {
  258. FUTURE_PROMISE_REASON_CODES,
  259. extractActionClaims,
  260. normalizeActionEvidence,
  261. verifyFuturePromises,
  262. };