|
|
@@ -0,0 +1,548 @@
|
|
|
+'use strict';
|
|
|
+
|
|
|
+const LEDGER_FIELDS = Object.freeze(['roomCount', 'area', 'budget', 'region']);
|
|
|
+
|
|
|
+const FACT_CLAIM_TYPES = Object.freeze({
|
|
|
+ CUSTOMER_ECHO: 'customer_echo',
|
|
|
+ METHODOLOGY: 'methodology',
|
|
|
+ PROPERTY_DYNAMIC: 'property_dynamic',
|
|
|
+ POLICY_RISK: 'policy_risk',
|
|
|
+ OTHER: 'other',
|
|
|
+});
|
|
|
+
|
|
|
+const ROOM_VALUE = Object.freeze({
|
|
|
+ 一: 1,
|
|
|
+ 二: 2,
|
|
|
+ 两: 2,
|
|
|
+ 三: 3,
|
|
|
+ 四: 4,
|
|
|
+ 五: 5,
|
|
|
+ 六: 6,
|
|
|
+ 七: 7,
|
|
|
+ 八: 8,
|
|
|
+ 九: 9,
|
|
|
+ 十: 10,
|
|
|
+});
|
|
|
+
|
|
|
+const FIELD_PROFILE_KEYS = Object.freeze({
|
|
|
+ roomCount: /(?:^|\.)(?:rooms?|roomcount|bedrooms?|layout|layouttype|roomtype|layout_type)$/i,
|
|
|
+ area: /(?:^|\.)(?:area|areamin|areamax|areasqm|buildingareasqm|area_range)$/i,
|
|
|
+ budget: /(?:^|\.)(?:budget|budgetwan|budgetmin|budgetmax|budgetrange|budget_range)$/i,
|
|
|
+ region: /(?:^|\.)(?:region|district|preferredregion|targetregion|target_region|board)$/i,
|
|
|
+});
|
|
|
+
|
|
|
+const SUCCESS_STATUS = /^(?:ok|success|succeeded|complete|completed|done|accepted|created|booked|scheduled|sent|delivered)$/i;
|
|
|
+const FAILURE_STATUS = /^(?:fail|failed|failure|error|errored|rejected|cancelled|canceled|timeout|timed_out)$/i;
|
|
|
+const VIEWING_REQUEST = /(?:想|要|准备|打算|约|安排|周[末六日]|今天|明天|后天)[^。!?]{0,14}(?:看房|看这套|去看|带看|还能?看|可以看)|(?:看房|带看|还能?看)[^。!?]{0,12}(?:时间|安排|方便|吗|么)|(?:什么时候|哪天|几点)[^。!?]{0,10}(?:来?看|看房)/;
|
|
|
+const DEFINITE_VIEWING_AVAILABILITY = /^(?:好的?[,,。!!\s]*|可以的?[,,。!!\s]*|没问题[,,。!!\s]*|能安排[,,。!!\s]*|看房没问题)|(?:今天|明天|后天|周[一二三四五六日天末]).{0,18}(?:可以的?(?:[,,。!!]|$)|能安排|可以安排|有空|有时间|时间没问题|能看|可以看)/;
|
|
|
+const SCOPED_VIEWING_BOUNDARY = /(?:具体|实际).{0,10}(?:时间|时段|安排).{0,16}(?:还要|需要|得).{0,12}(?:确认|对上|协调)|(?:先作为意向|先定想看的房子)|(?:业主|房源).{0,10}(?:时间|档期).{0,10}(?:确认|对上|协调)/;
|
|
|
+const TENTATIVE_VIEWING_PROPOSAL = /(?:今天|明天|后天|周[一二三四五六日天末])?(?:可以)?先作为(?:看房)?意向|先定(?:下)?想看的房子和(?:大概)?时段|具体(?:房源和)?(?:时间|时段|安排).{0,12}(?:还要|需要|得).{0,10}(?:确认|对上|协调)/;
|
|
|
+const PERSONAL_SCHEDULE_CONFIRMATION = /(?:我|我这边|这边).{0,6}(?:倒是|其实|目前|明天|今天|后天)?\s*(?:有空|有时间|时间没问题)|(?:今天|明天|后天).{0,10}(?:我|我这边|这边).{0,6}(?:有空|有时间|时间没问题)/;
|
|
|
+const FIRST_PERSON_CAPABILITY = /(?:我|我们|我这边|这边).{0,10}(?:都能看到|能看到|看得到|能查到|查得到|能调到|调得到|能找到|找得到|可以查|可以调).{0,18}(?:房源|房子|挂牌|成交|户型图|平面图|图片|照片|视频|媒体|资料)|(?:房源|房子|挂牌|成交|户型图|平面图|图片|照片|视频|媒体|资料).{0,10}(?:我|我们|我这边|这边).{0,10}(?:都能看到|能看到|看得到|能查到|查得到|能调到|调得到|能找到|找得到|可以查|可以调)/;
|
|
|
+const MARKET_DISTRIBUTION = /(?:这个|该)?(?:总价段|预算(?:里|下|范围)).{0,24}(?:三房|两房|户型|面积).{0,24}(?:通常|一般|多在|多落在|大多|普遍|偏紧凑|偏实用)|(?:三房|两房|户型|面积).{0,24}(?:在这个|该)(?:总价段|预算(?:里|下|范围)).{0,24}(?:通常|一般|多在|多落在|大多|普遍|偏紧凑|偏实用)|(?:通常|一般|大多|普遍|多在|多落在).{0,28}(?:\d{2,4}\s*(?:平|㎡)|三房|两房).{0,18}(?:预算|总价段)/;
|
|
|
+const ROOM_AREA_DISTRIBUTION = /(?:三房|两房|[2-4]房).{0,24}(?:通常|一般|大多|常见|多在|先看|参考|大概).{0,24}(?:\d{2,4}\s*(?:到|至|-|~|~)\s*\d{2,4}\s*(?:平|㎡|平方米)|\d{2,4}\s*(?:多)?平)|(?:三房|两房|[2-4]房).{0,40}\d{2,4}\s*(?:多)?平.{0,30}\d{2,4}\s*(?:多)?平.{0,20}(?:紧凑|宽松|舒展|舒服)|\d{2,4}\s*(?:到|至|-|~|~)\s*\d{2,4}\s*(?:平|㎡|平方米).{0,28}(?:三房|两房|[2-4]房|紧凑|宽松)/;
|
|
|
+const MARKET_PRICE_AREA_OUTCOME = /(?:预算|总价|附近|区域|片区|现在|当前).{0,20}(?:[\d一二两三四五六七八九十百千万亿]+(?:\.\d+)?\s*(?:万|万元)?).{0,18}(?:只能买|能买到|可以买到|买到).{0,14}(?:[\d一二两三四五六七八九十百千万亿]+(?:\.\d+)?\s*(?:平|㎡|平方米))/;
|
|
|
+const EXPLICIT_AREA_RANGE = /\d{2,4}(?:\.\d+)?\s*(?:到|至|-|~|~)\s*\d{2,4}(?:\.\d+)?\s*(?:平|㎡|平方米)/;
|
|
|
+const UNSOURCED_ROOM_ASSUMPTION = /(?:紧凑|宽松|舒适|实用)?\s*(?:三房|三室|3室)|(?:三房|三室|3室).{0,18}(?:紧凑|宽松|舒适|实用|90\s*(?:到|至|-|—)\s*120\s*(?:平|㎡)|90多平|110平)/;
|
|
|
+const UNSOURCED_BUDGET_FEASIBILITY = /(?:预算|总价|\d{2,4}\s*万).{0,16}(?:没问题|肯定够|够用|可选不少|选择很多|能挑不少|可以挑不少|很充足|很宽裕)|(?:没问题|肯定够|够用|可选不少|选择很多|能挑不少|可以挑不少|很充足|很宽裕).{0,16}(?:预算|总价|\d{2,4}\s*万)/;
|
|
|
+const FIRST_PERSON_INVENTORY_STATE = /(?:我这边|我们|这边|手上|手头|我.{0,10}(?:这边|区域|板块)).{0,24}(?:有|已有|没有|没|还没|暂无|暂时没|没筛出|没找到|没捋出).{0,20}(?:(?:特别)?合适|匹配|在售|可看|几套|多套|少量)?(?:房源|房子|户型)|(?:我这边|我们|这边|手上|手头|我.{0,10}(?:这边|区域|板块)).{0,20}(?:房源|房子|户型).{0,12}(?:有|没有|没|还没|暂无)|(?:我这边|我们|这边|手上|手头|我.{0,10}(?:这边|区域|板块)).{0,24}(?:合适|匹配|符合|筛到|整理出).{0,16}(?:房源|房子|候选)/;
|
|
|
+const SAFE_METHOD_CUES = /(?:要|得|需要|应该|可以|建议|先|主要)(?:把|从|看|结合|比较|确认|对准|分清)|(?:不能|不宜|不好|先不)(?:只看|直接|马上|下结论|报数|判断)|(?:关键|重点|主要).{0,12}(?:看|比较|取舍)|(?:还要|仍要|具体).{0,12}(?:看|结合|确认|对上)/;
|
|
|
+const DEFINITE_FACT_CUES = /(?:已确认|确定有|当前有|完全匹配|一定|保证|肯定|都能|普遍|大多|多在|多落在|通常|一般).{0,24}(?:\d|价格|房源|面积|三房|两房|在售|学区|产权|可看|能看)/;
|
|
|
+const UNSOURCED_GENERIC_PRICING_METHOD = /(?:价格|费用|收费).{0,20}(?:套餐|账号|席位|人数|数量).{0,20}(?:结合|取决于|要看|来定)|(?:套餐|账号|席位|人数|数量).{0,20}(?:决定|影响).{0,12}(?:价格|费用|收费)/;
|
|
|
+const POLICY_RISK_TOPIC = /(?:税费|契税|个税|增值税|满五|唯一住房|学区|学位|入学|划片|户籍|招生政策|产权|权属|合同|贷款|利率|资格|报名)/;
|
|
|
+const POLICY_RISK_CONCLUSION = /(?:税费|契税|个税|增值税).{0,24}(?:免|不用交|不需要交|只剩|是|为|合计|一共|总共|大概|约|\d+(?:\.\d+)?\s*[%%])|(?:满五|唯一住房).{0,30}(?:个税|增值税|契税).{0,18}(?:免|不用交|不需要交|按差额)|(?:能上|可以上|可以入学|保证入学|属于.{0,8}学区|对口.{0,8}(?:小学|中学|初中|学校)|学位稳|入学没问题)|(?:产权|权属|合同|贷款|利率|资格).{0,18}(?:没问题|(?<!不)确定|就是|一定|可以签|符合|通过)/;
|
|
|
+const POLICY_RISK_ASSERTION = /(?:保证|肯定|一定|绝对|(?<!不)确定|明确|没问题|无风险|不存在风险|可以签|符合|通过|能上|可以上|可以入学|免征|不用交|不需要交)/;
|
|
|
+const POLICY_RISK_SCOPED_UNCERTAINTY = /^(?:(?:当前|目前|暂时|现阶段)?(?:尚无|暂无|没有|尚未|还未|还没有|未能|无法|不能|没法).{0,16}(?:可确认|已确认|确定|明确|核实|核验|核对|复核)?(?:的)?(?:税费|契税|个税|增值税|学区|学位|入学|划片|产权|权属|合同|贷款|利率|资格|政策).{0,10}(?:结论|结果|依据|信息|状态)?|(?:税费|契税|个税|增值税|学区|学位|入学|划片|产权|权属|合同|贷款|利率|资格|政策)(?:状态|结论|结果|依据|信息)?(?:现在|目前|当前)?(?:还|仍)?(?:不确定|待确认|待核验))$/;
|
|
|
+const POLICY_RISK_METHOD = /(?:要|得|需要|应当|先|最终要|具体以|取决于|核对|准备|要备|对上|结合|落到).{0,32}(?:学校|户籍|划片|当期政策|招生政策|房产证|不动产权证|户口本|房本|产权材料|总价|面积|卖方情况)|(?:学校|户籍|划片|当期政策|招生政策|房产证|不动产权证|户口本|房本|产权材料).{0,32}(?:要|得|需要|核对|准备|为准|确认|落到)/;
|
|
|
+const PROPERTY_DYNAMIC_CLAIM = /(?:不同|各个).{0,8}(?:片区|板块|小区).{0,18}(?:价格|总价|单价|行情).{0,12}(?:差别|差异|不一样|波动)|(?:价格|总价|单价|行情).{0,18}(?:不同|各个).{0,8}(?:片区|板块|小区).{0,12}(?:差别|差异|不一样|波动)|(?:两房|三房|户型|房源).{0,20}(?:是不是都|一般|通常|大多|普遍|多在|都要).{0,18}(?:\d|价格|总价|面积)|(?:是不是都|一般|通常|大多|普遍|多在|都要).{0,20}(?:\d|价格|总价|面积|两房|三房)|(?:选择|房源).{0,6}(?:挺多|不少|很多)|(?:有|找到|筛到).{0,8}(?:合适|匹配).{0,6}(?:房源|房子)|^(?:当前|目前|现在)?(?:还有|有)(?:合适|匹配)的(?:[,,。]|$)|(?:第[一二三123]套|这套|该套|具体房源|当前房源).{0,24}(?:在售|停售|下架|可售|价格|总价|单价|面积|户型|安静|清静|不吵|靠河|临路|楼层|匹配|合适|对得上|优先看)/;
|
|
|
+const PROPERTY_MARKET_OUTCOME = /(?:地铁房|靠地铁|高层|中低楼层|低楼层|楼层).{0,24}(?:投资|放租|出租|租金|升值|保值|转手|流通).{0,12}(?:稳|好|容易|慢|快|难)|(?:投资|放租|出租|租金|升值|保值|转手|流通).{0,24}(?:稳|好|容易|慢|快|难).{0,12}(?:地铁房|靠地铁|高层|中低楼层|低楼层|楼层)/;
|
|
|
+const PROPERTY_NEGOTIATION_CLAIM = /(?:价格|报价|价位).{0,16}(?:有得谈|有商量空间|能谈|可以谈|可谈|谈得动)|(?:有得谈|有商量空间|能谈|可以谈|可谈|谈得动).{0,16}(?:价格|报价|价位)/;
|
|
|
+const PROPERTY_COMPARATIVE_JUDGEMENT = /(?:楼层|高层|低楼层|中高楼层).{0,32}(?:采光|视野|上下|进出|通风|转手|出租).{0,16}(?:更好|更差|更方便|更省事|省事些|更难|更慢|更快|更稳)/;
|
|
|
+const FACT_METHOD_CUES = /(?:筛|筛选|排序|排在前面|优先保留|不单看|取舍|权衡|先把|先从|按顺序|理清|对齐|定方向|付款方式|付款节奏|报价思路|谈判时|按经验|别急着|不急着|先不报|不猜价格|价格留到|逐项核|逐项看|核对|准备|要备|圈着看|留意|作为参考|怎么判断|如何判断)/;
|
|
|
+const CUSTOMER_ATTRIBUTION_CUES = /(?:记下|记住|记录|按(?:您|你|你们)(?:说的|提的|的)?|(?:您|你|你们)(?:说的|提的|确认的|想要的|在意的|担心的)|先按|这个范围|这个方向|都记下|不急着定)/;
|
|
|
+const CUSTOMER_ECHO_UNSAFE_ADDITION = /(?:选择|房源).{0,6}(?:挺多|不少|很多)|(?:有|找到|筛到).{0,8}(?:合适|匹配).{0,6}(?:房源|房子)|(?:够用|够买|没问题|充足|宽裕|好找)|(?:这套|该套|具体房源).{0,18}(?:很|更|最|确实|就是).{0,8}(?:安静|清静|靠河|不临路|采光好|户型好)/;
|
|
|
+const CUSTOMER_DIMENSIONS = Object.freeze({
|
|
|
+ budget: /(?:预算|总价|价格上限|压力|首付|月供)/,
|
|
|
+ roomCount: /(?:两房|二房|2房|三房|3房|四房|4房|户型)/,
|
|
|
+ transit: /(?:通勤|公司|上班|软件园|地铁|步行|走路|分钟)/,
|
|
|
+ quiet: /(?:怕吵|安静|清静|噪音|噪声|不吵)/,
|
|
|
+ floor: /(?:楼层|中间楼层|中层|低层|高层)/,
|
|
|
+ river: /(?:靠河|沿河|河景|河边)/,
|
|
|
+ propertyType: /(?:新房|二手房)/,
|
|
|
+ payment: /(?:按揭|现金|付款方式|付款节奏)/,
|
|
|
+ school: /(?:学校|小学|初中|入学|学区|学位|片区|小区)/,
|
|
|
+});
|
|
|
+
|
|
|
+function normalizeText(value = '') {
|
|
|
+ return String(value ?? '').trim();
|
|
|
+}
|
|
|
+
|
|
|
+function roleOf(message = {}) {
|
|
|
+ return message.role || (message.direction === 'outbound' ? 'assistant' : 'user');
|
|
|
+}
|
|
|
+
|
|
|
+function toolSucceeded(trace = {}) {
|
|
|
+ const status = String(trace.status || trace.result?.status || '');
|
|
|
+ if (trace.isError === true || trace.error || trace.result?.ok === false || trace.result?.success === false || FAILURE_STATUS.test(status)) return false;
|
|
|
+ return trace.ok === true || trace.success === true || trace.result?.ok === true || trace.result?.success === true || SUCCESS_STATUS.test(status);
|
|
|
+}
|
|
|
+
|
|
|
+function roomNumber(value = '') {
|
|
|
+ const token = String(value || '').trim();
|
|
|
+ return Number(token) || ROOM_VALUE[token] || 0;
|
|
|
+}
|
|
|
+
|
|
|
+function extractRoomCount(text = '') {
|
|
|
+ const value = normalizeText(text);
|
|
|
+ const matches = [...value.matchAll(/([1-9一二三四五六七八九十两])\s*(?:室|房|居)/g)];
|
|
|
+ for (const match of matches.reverse()) {
|
|
|
+ const prefix = value.slice(Math.max(0, Number(match.index || 0) - 10), Number(match.index || 0));
|
|
|
+ if (/(?:不要|不考虑|不是|排除|别看|不想要)\s*$/.test(prefix)) continue;
|
|
|
+ const count = roomNumber(match[1]);
|
|
|
+ if (count) return count;
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+function extractArea(text = '') {
|
|
|
+ const value = normalizeText(text);
|
|
|
+ const range = value.match(/(\d{2,4}(?:\.\d+)?)\s*(?:-|—|~|~|到|至)\s*(\d{2,4}(?:\.\d+)?)\s*(?:平|平方米|㎡)/);
|
|
|
+ if (range) return { min: Number(range[1]), max: Number(range[2]) };
|
|
|
+ const single = value.match(/(\d{2,4}(?:\.\d+)?)\s*(?:平|平方米|㎡)/);
|
|
|
+ return single ? { value: Number(single[1]) } : null;
|
|
|
+}
|
|
|
+
|
|
|
+function extractBudget(text = '') {
|
|
|
+ const value = normalizeText(text);
|
|
|
+ if (!/(?:预算|总价|房价|价位|万\s*(?:以内|以下|封顶|左右|上下|以上|起))/.test(value)) return null;
|
|
|
+ const range = value.match(/(\d{2,4}(?:\.\d+)?)\s*(?:-|—|~|~|到|至)\s*(\d{2,4}(?:\.\d+)?)\s*万/);
|
|
|
+ if (range) return { minWan: Number(range[1]), maxWan: Number(range[2]) };
|
|
|
+ const single = value.match(/(\d{2,4}(?:\.\d+)?)\s*万/);
|
|
|
+ if (!single) return null;
|
|
|
+ const amount = Number(single[1]);
|
|
|
+ if (/(?:以内|以下|封顶|上限)/.test(value)) return { maxWan: amount };
|
|
|
+ if (/(?:以上|起)/.test(value)) return { minWan: amount };
|
|
|
+ return { valueWan: amount };
|
|
|
+}
|
|
|
+
|
|
|
+function extractRegion(text = '') {
|
|
|
+ const value = normalizeText(text);
|
|
|
+ const fixed = [...value.matchAll(/(新北区|武进区|天宁区|钟楼区|新北|武进|天宁|钟楼)/g)].at(-1)?.[1] || '';
|
|
|
+ if (fixed) return fixed.endsWith('区') ? fixed : `${fixed}区`;
|
|
|
+ const board = [...value.matchAll(/([\p{Script=Han}]{2,8}板块)/gu)].at(-1)?.[1] || '';
|
|
|
+ if (board && !/(?:学区|小区)/.test(board)) return board;
|
|
|
+ const contextual = value.match(/(?:考虑在|想在|想买|想看|区域|位置|倾向|优先)\s*([\p{Script=Han}]{2,8}区)/u)?.[1] || '';
|
|
|
+ return /(?:小区|学区)/.test(contextual) ? '' : contextual;
|
|
|
+}
|
|
|
+
|
|
|
+function sourceRecord(field, value, sourceType, sourceRef, text = '') {
|
|
|
+ return {
|
|
|
+ field,
|
|
|
+ value,
|
|
|
+ sourceType,
|
|
|
+ sourceRef: String(sourceRef || ''),
|
|
|
+ text: normalizeText(text).slice(0, 180),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function addTextFields(output, text, sourceType, sourceRef) {
|
|
|
+ const extractors = {
|
|
|
+ roomCount: extractRoomCount,
|
|
|
+ area: extractArea,
|
|
|
+ budget: extractBudget,
|
|
|
+ region: extractRegion,
|
|
|
+ };
|
|
|
+ for (const [field, extract] of Object.entries(extractors)) {
|
|
|
+ const value = extract(text);
|
|
|
+ if (value !== null && value !== '' && value !== 0) output.push(sourceRecord(field, value, sourceType, sourceRef, text));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function flattenProfile(value, prefix = '', output = []) {
|
|
|
+ if (!value || typeof value !== 'object') return output;
|
|
|
+ for (const [key, child] of Object.entries(value)) {
|
|
|
+ if (['labels', 'caseMetadata', 'case_metadata', 'goldenCase', 'golden_case'].includes(key)) continue;
|
|
|
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
|
|
|
+ if (child && typeof child === 'object' && !Array.isArray(child)) {
|
|
|
+ if (Object.prototype.hasOwnProperty.call(child, 'value')) output.push({ path: fieldPath, value: child.value });
|
|
|
+ flattenProfile(child, fieldPath, output);
|
|
|
+ } else if (child !== undefined && child !== null && child !== '') {
|
|
|
+ output.push({ path: fieldPath, value: child });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return output;
|
|
|
+}
|
|
|
+
|
|
|
+function addToolFields(output, toolTrace = []) {
|
|
|
+ for (const [index, trace] of successfulToolEvidence(toolTrace).entries()) {
|
|
|
+ for (const item of flattenProfile(trace.result || {})) {
|
|
|
+ for (const field of LEDGER_FIELDS) {
|
|
|
+ if (!FIELD_PROFILE_KEYS[field].test(item.path)) continue;
|
|
|
+ const text = normalizeText(item.value);
|
|
|
+ let value = item.value;
|
|
|
+ if (field === 'roomCount') value = extractRoomCount(text) || roomNumber(text);
|
|
|
+ if (field === 'area') value = extractArea(text) || (Number.isFinite(Number(item.value)) ? { value: Number(item.value) } : null);
|
|
|
+ if (field === 'budget') value = extractBudget(text) || (Number.isFinite(Number(item.value)) ? { valueWan: Number(item.value) } : null);
|
|
|
+ if (field === 'region') value = text;
|
|
|
+ if (value !== null && value !== '' && value !== 0) {
|
|
|
+ output.push(sourceRecord(field, value, 'tool', `${trace.tool || trace.name || 'tool'}:${index}:${item.path}`, text));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function addProfileFields(output, profile = {}) {
|
|
|
+ for (const item of flattenProfile(profile)) {
|
|
|
+ for (const field of LEDGER_FIELDS) {
|
|
|
+ if (!FIELD_PROFILE_KEYS[field].test(item.path)) continue;
|
|
|
+ const text = normalizeText(item.value);
|
|
|
+ let value = item.value;
|
|
|
+ if (field === 'roomCount') value = extractRoomCount(text) || roomNumber(text);
|
|
|
+ if (field === 'area') value = extractArea(text) || (Number.isFinite(Number(item.value)) ? { value: Number(item.value) } : null);
|
|
|
+ if (field === 'budget') value = extractBudget(text) || (Number.isFinite(Number(item.value)) ? { valueWan: Number(item.value) } : null);
|
|
|
+ if (field === 'region') value = text;
|
|
|
+ if (value !== null && value !== '' && value !== 0) output.push(sourceRecord(field, value, 'profile', item.path, text));
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function successfulToolEvidence(toolTrace = []) {
|
|
|
+ return (toolTrace || []).filter(toolSucceeded);
|
|
|
+}
|
|
|
+
|
|
|
+function marketEvidencePresent(input = {}) {
|
|
|
+ if (input.capabilities?.marketEvidence === true) return true;
|
|
|
+ const citations = (input.citations || []).map(item => JSON.stringify(item || {})).join('\n');
|
|
|
+ if (/(?:成交|挂牌|价格|单价).{0,24}(?:面积|户型|三房|两房)|(?:分布|样本|中位数|平均)/.test(citations)) return true;
|
|
|
+ return successfulToolEvidence(input.toolTrace).some(trace => {
|
|
|
+ const name = String(trace.tool || trace.name || '');
|
|
|
+ const result = trace.result || {};
|
|
|
+ const rows = result.items || result.properties || result.matches || result.rows || result.nearItems || result.data?.items;
|
|
|
+ return /(?:propert(?:y|ies)|market|price|房源|市场|价格)/i.test(name)
|
|
|
+ && ((Array.isArray(rows) && rows.length > 1) || /(?:distribution|median|average|分布|中位数|平均)/i.test(JSON.stringify(result)));
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function areaDistributionEvidencePresent(input = {}) {
|
|
|
+ if (input.capabilities?.areaDistributionEvidence === true) return true;
|
|
|
+ const knowledge = [input.knowledgeEvidence, input.knowledgeFacts, input.knowledgeContext]
|
|
|
+ .flatMap(value => Array.isArray(value) ? value : value ? [value] : [])
|
|
|
+ .map(value => typeof value === 'string' ? value : JSON.stringify(value || {}));
|
|
|
+ const citations = (input.citations || []).map(item => JSON.stringify(item || {}));
|
|
|
+ const toolEvidence = successfulToolEvidence(input.toolTrace).map(trace => JSON.stringify(trace.result || {}));
|
|
|
+ if ([...knowledge, ...citations, ...toolEvidence].some(text => (
|
|
|
+ EXPLICIT_AREA_RANGE.test(text) && /(?:三房|两房|[2-4]房|户型|面积|room|layout|area)/i.test(text)
|
|
|
+ ))) return true;
|
|
|
+ return successfulToolEvidence(input.toolTrace).some(trace => {
|
|
|
+ const result = trace.result || {};
|
|
|
+ const rows = result.items || result.properties || result.matches || result.rows || result.nearItems || result.data?.items;
|
|
|
+ if (!Array.isArray(rows) || rows.length < 2) return false;
|
|
|
+ const areas = rows.map(item => Number(item?.area ?? item?.areaSqm ?? item?.buildingAreaSqm)).filter(Number.isFinite);
|
|
|
+ return areas.length >= 2 && /(?:propert(?:y|ies)|market|房源|市场)/i.test(String(trace.tool || trace.name || ''));
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function inventoryStatusEvidencePresent(input = {}) {
|
|
|
+ if (input.capabilities?.inventoryStatus === true) return true;
|
|
|
+ return (input.toolTrace || []).filter(trace => (
|
|
|
+ trace && trace.isError !== true && trace.error === undefined
|
|
|
+ && trace.result?.ok !== false && trace.result?.success !== false
|
|
|
+ )).some(trace => {
|
|
|
+ const result = trace.result || {};
|
|
|
+ const rows = result.items || result.properties || result.matches || result.rows || result.data?.items;
|
|
|
+ const explicitStatus = ['available', 'hasInventory', 'inventoryStatus', 'listingStatus', 'total'].some(key => (
|
|
|
+ Object.prototype.hasOwnProperty.call(result, key) || Object.prototype.hasOwnProperty.call(result.data || {}, key)
|
|
|
+ ));
|
|
|
+ return /(?:propert(?:y|ies)|listing|inventory|房源|在售)/i.test(String(trace.tool || trace.name || ''))
|
|
|
+ && (Array.isArray(rows) || explicitStatus);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function buildResponseProvenanceLedger(input = {}) {
|
|
|
+ const records = [];
|
|
|
+ addProfileFields(records, input.profile?.profile || input.profile || {});
|
|
|
+ const userMessages = (input.messages || []).filter(message => roleOf(message) === 'user');
|
|
|
+ userMessages.forEach((message, index) => addTextFields(records, message.content || message.text || '', 'history', `user:${index}`));
|
|
|
+ addTextFields(records, input.inboundContent || input.inbound || '', 'current_inbound', 'current');
|
|
|
+ addToolFields(records, input.toolTrace || []);
|
|
|
+ const priorities = { current_inbound: 4, history: 3, profile: 2, tool: 1 };
|
|
|
+ const fields = {};
|
|
|
+ for (const field of LEDGER_FIELDS) {
|
|
|
+ const sources = records.filter(record => record.field === field);
|
|
|
+ const selected = [...sources].sort((left, right) => priorities[right.sourceType] - priorities[left.sourceType]).at(-1) || null;
|
|
|
+ if (sources.length) {
|
|
|
+ const current = sources.filter(record => record.sourceType === 'current_inbound').at(-1)
|
|
|
+ || sources.filter(record => record.sourceType === 'history').at(-1)
|
|
|
+ || sources.filter(record => record.sourceType === 'profile').at(-1)
|
|
|
+ || selected;
|
|
|
+ fields[field] = { value: current.value, sources };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const successful = successfulToolEvidence(input.toolTrace);
|
|
|
+ const capabilities = {
|
|
|
+ appointment: input.capabilities?.appointment === true || successful.some(trace => /(?:book|schedule|appointment|viewing|带看|看房|预约|排期)/i.test(String(trace.tool || trace.name || ''))),
|
|
|
+ propertyAccess: input.capabilities?.propertyAccess === true || successful.some(trace => /(?:propert(?:y|ies)|listing|房源|楼盘)/i.test(String(trace.tool || trace.name || ''))),
|
|
|
+ mediaAccess: input.capabilities?.mediaAccess === true || successful.some(trace => /(?:media|photo|image|video|floor.?plan|户型图|图片|照片|视频|媒体)/i.test(`${trace.tool || trace.name || ''}\n${JSON.stringify(trace.result || {})}`)),
|
|
|
+ marketEvidence: marketEvidencePresent(input),
|
|
|
+ areaDistributionEvidence: areaDistributionEvidencePresent(input),
|
|
|
+ inventoryStatus: inventoryStatusEvidencePresent(input),
|
|
|
+ };
|
|
|
+ return {
|
|
|
+ version: '1.0',
|
|
|
+ fields,
|
|
|
+ capabilities,
|
|
|
+ sourceCount: records.length,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function ledgerFieldValue(ledger = {}, field = '') {
|
|
|
+ return ledger.fields?.[field]?.value;
|
|
|
+}
|
|
|
+
|
|
|
+function fieldHasSource(ledger = {}, field = '') {
|
|
|
+ return Array.isArray(ledger.fields?.[field]?.sources) && ledger.fields[field].sources.length > 0;
|
|
|
+}
|
|
|
+
|
|
|
+function provenanceInstruction(ledger = {}) {
|
|
|
+ const known = LEDGER_FIELDS.filter(field => fieldHasSource(ledger, field)).map(field => {
|
|
|
+ const entry = ledger.fields[field];
|
|
|
+ const sources = [...new Set(entry.sources.map(item => item.sourceType))].join('/');
|
|
|
+ return `${field}=${JSON.stringify(entry.value)}(${sources})`;
|
|
|
+ });
|
|
|
+ return `字段来源账本:${known.join(';') || '暂无已确认预算、区域、房间数或面积'}。只使用账本已有字段;roomCount 来源不等于面积分布来源,没有 areaDistributionEvidence 时不得生成三房面积区间;没有市场证据时不得生成预算对应面积或户型分布。`;
|
|
|
+}
|
|
|
+
|
|
|
+function customerFactEchoClaim(claim = '', input = {}) {
|
|
|
+ const text = normalizeText(claim);
|
|
|
+ if (!text || CUSTOMER_ECHO_UNSAFE_ADDITION.test(text)) return false;
|
|
|
+ const customerText = [
|
|
|
+ input.inboundContent || input.inbound,
|
|
|
+ ...(input.messages || [])
|
|
|
+ .filter(message => roleOf(message) === 'user')
|
|
|
+ .map(message => message.content || message.text || ''),
|
|
|
+ ].filter(Boolean).join('\n');
|
|
|
+ if (!customerText || !CUSTOMER_ATTRIBUTION_CUES.test(text)) return false;
|
|
|
+ const dimensions = Object.values(CUSTOMER_DIMENSIONS).filter(pattern => pattern.test(text));
|
|
|
+ if (!dimensions.length || !dimensions.every(pattern => pattern.test(customerText))) return false;
|
|
|
+ const numbers = text.match(/\d+(?:\.\d+)?/g) || [];
|
|
|
+ return numbers.every(number => normalizeText(customerText).includes(number));
|
|
|
+}
|
|
|
+
|
|
|
+function methodologyClaim(claim = '') {
|
|
|
+ const text = normalizeText(claim);
|
|
|
+ if (!text || PROPERTY_DYNAMIC_CLAIM.test(text) || POLICY_RISK_CONCLUSION.test(text)) return false;
|
|
|
+ if (safeMethodologyClaim(text)) return true;
|
|
|
+ return FACT_METHOD_CUES.test(text)
|
|
|
+ && !/(?:已确认|确定有|当前有|完全匹配|保证|肯定|一定).{0,24}(?:房源|价格|在售|学区|税费|产权)/.test(text);
|
|
|
+}
|
|
|
+
|
|
|
+function classifyFactClaim(claim = '', input = {}) {
|
|
|
+ const text = normalizeText(claim);
|
|
|
+ if (!text) return { type: FACT_CLAIM_TYPES.OTHER, requiresExternalEvidence: false };
|
|
|
+ const inbound = normalizeText(input.inboundContent || input.inbound || '');
|
|
|
+ if (POLICY_RISK_TOPIC.test(text)) {
|
|
|
+ if (POLICY_RISK_CONCLUSION.test(text) || POLICY_RISK_ASSERTION.test(text)) {
|
|
|
+ return { type: FACT_CLAIM_TYPES.POLICY_RISK, requiresExternalEvidence: true };
|
|
|
+ }
|
|
|
+ if (POLICY_RISK_SCOPED_UNCERTAINTY.test(text) && !POLICY_RISK_ASSERTION.test(text)) {
|
|
|
+ return { type: FACT_CLAIM_TYPES.METHODOLOGY, requiresExternalEvidence: false };
|
|
|
+ }
|
|
|
+ if (POLICY_RISK_METHOD.test(text) || /(?:入学|学位|学区).{0,24}(?:学校|户籍|划片|当期政策|核对|确认|准备|落到)/.test(text)) {
|
|
|
+ return { type: FACT_CLAIM_TYPES.METHODOLOGY, requiresExternalEvidence: false };
|
|
|
+ }
|
|
|
+ return { type: FACT_CLAIM_TYPES.POLICY_RISK, requiresExternalEvidence: true };
|
|
|
+ }
|
|
|
+ if (UNSOURCED_BUDGET_FEASIBILITY.test(text)
|
|
|
+ || FIRST_PERSON_INVENTORY_STATE.test(text)
|
|
|
+ || PROPERTY_DYNAMIC_CLAIM.test(text)
|
|
|
+ || PROPERTY_MARKET_OUTCOME.test(text)
|
|
|
+ || PROPERTY_NEGOTIATION_CLAIM.test(text)
|
|
|
+ || (/(?:价格|报价|价位).{0,12}(?:能不能谈|可以谈吗|能低吗|空间)/.test(inbound) && /^(?:能谈|可以谈|有得谈)(?:[,,。]|$)/.test(text))
|
|
|
+ || PROPERTY_COMPARATIVE_JUDGEMENT.test(text)
|
|
|
+ || MARKET_DISTRIBUTION.test(text)
|
|
|
+ || MARKET_PRICE_AREA_OUTCOME.test(text)
|
|
|
+ || ROOM_AREA_DISTRIBUTION.test(text)) {
|
|
|
+ return { type: FACT_CLAIM_TYPES.PROPERTY_DYNAMIC, requiresExternalEvidence: true };
|
|
|
+ }
|
|
|
+ if (customerFactEchoClaim(text, input)) {
|
|
|
+ return { type: FACT_CLAIM_TYPES.CUSTOMER_ECHO, requiresExternalEvidence: false };
|
|
|
+ }
|
|
|
+ const customerText = [input.inboundContent || input.inbound, ...(input.messages || [])
|
|
|
+ .filter(message => roleOf(message) === 'user')
|
|
|
+ .map(message => message.content || message.text || '')].filter(Boolean).join('\n');
|
|
|
+ if (/(?:明白|这个范围|补个|发个|小区名|大概位置|对上是哪一套|先看片区|不急着定)/.test(text)
|
|
|
+ && /(?:靠河|中间楼层|房源编号|小区|片区|位置)/.test(customerText)
|
|
|
+ && !PROPERTY_DYNAMIC_CLAIM.test(text)) {
|
|
|
+ return { type: FACT_CLAIM_TYPES.CUSTOMER_ECHO, requiresExternalEvidence: false };
|
|
|
+ }
|
|
|
+ if (methodologyClaim(text)) {
|
|
|
+ return { type: FACT_CLAIM_TYPES.METHODOLOGY, requiresExternalEvidence: false };
|
|
|
+ }
|
|
|
+ return { type: FACT_CLAIM_TYPES.OTHER, requiresExternalEvidence: false };
|
|
|
+}
|
|
|
+
|
|
|
+function safeMethodologyClaim(claim = '') {
|
|
|
+ const text = normalizeText(claim);
|
|
|
+ if (!text || DEFINITE_FACT_CUES.test(text) || MARKET_DISTRIBUTION.test(text) || UNSOURCED_GENERIC_PRICING_METHOD.test(text)) return false;
|
|
|
+ return SAFE_METHOD_CUES.test(text)
|
|
|
+ || /(?:面积跨度|面积差别|面积大小).{0,24}(?:取决于|结合|要看).{0,24}(?:小区|得房率|户型|布局)/.test(text);
|
|
|
+}
|
|
|
+
|
|
|
+function evaluateResponseProvenance(reply = '', input = {}) {
|
|
|
+ const content = normalizeText(reply);
|
|
|
+ const inbound = normalizeText(input.inboundContent || input.inbound || '');
|
|
|
+ const ledger = input.constraintLedger || buildResponseProvenanceLedger(input);
|
|
|
+ const violations = [];
|
|
|
+ const personalScheduleClaim = PERSONAL_SCHEDULE_CONFIRMATION.test(content);
|
|
|
+ const definiteViewingClaim = DEFINITE_VIEWING_AVAILABILITY.test(content);
|
|
|
+ const tentativeViewing = TENTATIVE_VIEWING_PROPOSAL.test(content)
|
|
|
+ || (!personalScheduleClaim && SCOPED_VIEWING_BOUNDARY.test(content));
|
|
|
+ if (VIEWING_REQUEST.test(inbound)
|
|
|
+ && (personalScheduleClaim || definiteViewingClaim)
|
|
|
+ && !tentativeViewing
|
|
|
+ && ledger.capabilities.appointment !== true) {
|
|
|
+ violations.push({ code: 'appointment_without_evidence', detail: '看房可用性缺少 appointment evidence' });
|
|
|
+ }
|
|
|
+ if (FIRST_PERSON_INVENTORY_STATE.test(content) && ledger.capabilities.inventoryStatus !== true) {
|
|
|
+ violations.push({ code: 'inventory_state_without_evidence', detail: '第一人称房源库存或匹配状态缺少 inventory evidence' });
|
|
|
+ }
|
|
|
+ if (FIRST_PERSON_CAPABILITY.test(content)) {
|
|
|
+ const media = /(?:户型图|平面图|图片|照片|视频|媒体|资料)/.test(content);
|
|
|
+ if ((media && !ledger.capabilities.mediaAccess) || (!media && !ledger.capabilities.propertyAccess)) {
|
|
|
+ violations.push({ code: 'capability_without_evidence', detail: '第一人称房源或媒体访问能力缺少 capability evidence' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const roomCount = Number(ledgerFieldValue(ledger, 'roomCount') || 0);
|
|
|
+ if (UNSOURCED_ROOM_ASSUMPTION.test(content) && roomCount !== 3 && !ledger.capabilities.propertyAccess) {
|
|
|
+ violations.push({ code: 'room_count_without_source', detail: '回复使用三房或三房面积区间,但 roomCount 没有对应来源' });
|
|
|
+ }
|
|
|
+ if ((MARKET_DISTRIBUTION.test(content) || MARKET_PRICE_AREA_OUTCOME.test(content)) && !ledger.capabilities.marketEvidence) {
|
|
|
+ violations.push({ code: 'market_distribution_without_evidence', detail: '预算、户型或面积分布缺少市场证据' });
|
|
|
+ }
|
|
|
+ if (ROOM_AREA_DISTRIBUTION.test(content) && !ledger.capabilities.areaDistributionEvidence) {
|
|
|
+ violations.push({ code: 'area_distribution_without_evidence', detail: 'roomCount 来源不能替代面积区间或面积分布证据' });
|
|
|
+ }
|
|
|
+ if (UNSOURCED_BUDGET_FEASIBILITY.test(content) && !ledger.capabilities.marketEvidence) {
|
|
|
+ violations.push({ code: 'budget_feasibility_without_evidence', detail: '预算记录不能直接推导市场可行性' });
|
|
|
+ }
|
|
|
+ return { passed: violations.length === 0, ledger, violations };
|
|
|
+}
|
|
|
+
|
|
|
+function stableIndex(seed = '', count = 1) {
|
|
|
+ let value = 2166136261;
|
|
|
+ for (const char of String(seed || '')) {
|
|
|
+ value ^= char.charCodeAt(0);
|
|
|
+ value = Math.imul(value, 16777619);
|
|
|
+ }
|
|
|
+ return count > 0 ? (value >>> 0) % count : 0;
|
|
|
+}
|
|
|
+
|
|
|
+function safeRewriteForViolations(input = {}, violations = [], seed = '') {
|
|
|
+ const inbound = normalizeText(input.inboundContent || input.inbound || '');
|
|
|
+ const ledger = input.constraintLedger || buildResponseProvenanceLedger(input);
|
|
|
+ const codes = new Set(violations.map(item => item.code));
|
|
|
+ const choose = (key, values) => values[stableIndex(`${seed}\n${key}`, values.length)];
|
|
|
+ if (codes.has('appointment_without_evidence')) {
|
|
|
+ return choose('appointment', [
|
|
|
+ '看房可以先定意向,具体时间还要和房源、业主时段对上。您哪天更方便?',
|
|
|
+ '先把想看的房子和大概时段定下来,具体安排还要再确认。您上午还是下午方便?',
|
|
|
+ '看房时间要结合具体房源和双方时段。您先说哪天方便。',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ if (codes.has('inventory_state_without_evidence')) {
|
|
|
+ return choose('inventory-state', [
|
|
|
+ '现有信息还不足以判断哪些房源合适。您先说最想看的区域或预算。',
|
|
|
+ '目前只能先补齐找房条件,不能先下库存结论。您更想先定区域还是预算?',
|
|
|
+ '先不判断手上有没有合适房源。您把区域和预算里更重要的一项告诉我。',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ if (codes.has('capability_without_evidence')) {
|
|
|
+ if (/(?:户型图|平面图|图片|照片|视频|媒体)/.test(inbound)) {
|
|
|
+ return choose('media-capability', [
|
|
|
+ '先对准具体哪套,再看对应的户型或视频资料。您发链接或截图给我。',
|
|
|
+ '户型和视频资料要先对应到具体房源。您把房源号或截图发我一个。',
|
|
|
+ '先确定您指的是哪套,资料才不会对错。发个链接或截图就行。',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ return choose('property-capability', [
|
|
|
+ '先对准您说的具体房源,再判断哪些信息能直接比较。您发小区名或房源号都行。',
|
|
|
+ '具体哪套还没对上,先不把房源信息说满。您给我小区名或房源号。',
|
|
|
+ '要先知道您指的是哪套。发个小区名、房源号或截图中的一个就行。',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ if (codes.has('area_distribution_without_evidence')) {
|
|
|
+ return '三房面积跨度取决于小区、得房率和户型,现在线索还不足以报固定区间。您更看重总价还是空间?';
|
|
|
+ }
|
|
|
+ if (codes.has('market_distribution_without_evidence')) {
|
|
|
+ const budget = ledgerFieldValue(ledger, 'budget');
|
|
|
+ const roomCount = Number(ledgerFieldValue(ledger, 'roomCount') || 0);
|
|
|
+ const known = [budget ? '预算' : '', roomCount ? `${roomCount}房` : ''].filter(Boolean).join('和');
|
|
|
+ return choose('market-distribution', [
|
|
|
+ `${known || '当前条件'}先保留,区域和具体房源没定前先不估面积分布。您重点看哪个区域?`,
|
|
|
+ `${known || '现有方向'}先按这个理解;没有区域和市场样本,现在不先下面积结论。您想看哪个片区?`,
|
|
|
+ `先保留${known || '您刚说的条件'},面积要结合区域和具体户型看。您优先考虑哪个区域?`,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ if (codes.has('budget_feasibility_without_evidence')) {
|
|
|
+ const budget = ledgerFieldValue(ledger, 'budget') || {};
|
|
|
+ const amount = Number(budget.maxWan ?? budget.valueWan ?? budget.minWan ?? 0);
|
|
|
+ return amount
|
|
|
+ ? `预算先按${amount}万以内记。您更想先定区域还是户型?`
|
|
|
+ : '预算先按您刚说的范围记。您更想先定区域还是户型?';
|
|
|
+ }
|
|
|
+ if (codes.has('room_count_without_source')) {
|
|
|
+ if (/(?:多大面积|多少平|平方|面积)/.test(inbound)) {
|
|
|
+ return choose('generic-area', [
|
|
|
+ '面积要先看您说的是哪种户型。您目前主要考虑几房?',
|
|
|
+ '不同户型的面积差别很大,先确认房间数更准确。您想看几房?',
|
|
|
+ '现在还没有房间数,先不套面积区间。您主要考虑几房?',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ return choose('good-layout', [
|
|
|
+ '好房型先看房间数和实际布局。您目前主要考虑几房?',
|
|
|
+ '房型好不好要结合您的使用需求,先从房间数开始。您想看几房?',
|
|
|
+ '先不默认具体户型。您更需要几房?',
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+}
|
|
|
+
|
|
|
+function enforceResponseProvenance(final = {}, input = {}) {
|
|
|
+ const reply = normalizeText(final.reply || '');
|
|
|
+ if (!reply) return final;
|
|
|
+ const assessment = evaluateResponseProvenance(reply, input);
|
|
|
+ if (assessment.passed) return { ...final, constraintLedger: assessment.ledger };
|
|
|
+ const rewritten = safeRewriteForViolations({ ...input, constraintLedger: assessment.ledger }, assessment.violations, input.conversationKey || 'default');
|
|
|
+ if (!rewritten) return { ...final, constraintLedger: assessment.ledger };
|
|
|
+ return {
|
|
|
+ ...final,
|
|
|
+ reply: rewritten,
|
|
|
+ constraintLedger: assessment.ledger,
|
|
|
+ reason: `${normalizeText(final.reason)}${final.reason ? ';' : ''}字段来源账本已阻止无来源字段、能力或市场分布进入客户回复。`,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+ FACT_CLAIM_TYPES,
|
|
|
+ LEDGER_FIELDS,
|
|
|
+ buildResponseProvenanceLedger,
|
|
|
+ classifyFactClaim,
|
|
|
+ customerFactEchoClaim,
|
|
|
+ enforceResponseProvenance,
|
|
|
+ evaluateResponseProvenance,
|
|
|
+ extractArea,
|
|
|
+ extractBudget,
|
|
|
+ extractRegion,
|
|
|
+ extractRoomCount,
|
|
|
+ fieldHasSource,
|
|
|
+ ledgerFieldValue,
|
|
|
+ areaDistributionEvidencePresent,
|
|
|
+ inventoryStatusEvidencePresent,
|
|
|
+ marketEvidencePresent,
|
|
|
+ methodologyClaim,
|
|
|
+ provenanceInstruction,
|
|
|
+ safeMethodologyClaim,
|
|
|
+ safeRewriteForViolations,
|
|
|
+ stableIndex,
|
|
|
+};
|