fix(ai): recognize conversational document filters
This commit is contained in:
@@ -1,43 +1,5 @@
|
||||
import { buildDraftAssociationQueryPayload } from '../views/scripts/travelReimbursementExpenseQueryModel.js'
|
||||
|
||||
const CITY_NAMES = [
|
||||
'北京',
|
||||
'上海',
|
||||
'广州',
|
||||
'深圳',
|
||||
'武汉',
|
||||
'南京',
|
||||
'杭州',
|
||||
'成都',
|
||||
'重庆',
|
||||
'西安',
|
||||
'天津',
|
||||
'苏州',
|
||||
'长沙',
|
||||
'郑州',
|
||||
'青岛',
|
||||
'厦门',
|
||||
'宁波',
|
||||
'无锡',
|
||||
'合肥',
|
||||
'福州',
|
||||
'昆明',
|
||||
'大连',
|
||||
'沈阳',
|
||||
'济南',
|
||||
'哈尔滨',
|
||||
'长春',
|
||||
'南昌',
|
||||
'太原',
|
||||
'贵阳',
|
||||
'南宁',
|
||||
'石家庄',
|
||||
'兰州',
|
||||
'银川',
|
||||
'西宁',
|
||||
'海口',
|
||||
'拉萨'
|
||||
]
|
||||
import { extractKnownCityNames } from './knownCityNames.js'
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '')
|
||||
@@ -102,11 +64,7 @@ function extractDateTokens(text) {
|
||||
}
|
||||
|
||||
function extractCityTokens(text) {
|
||||
const compact = normalizeText(text)
|
||||
if (!compact) {
|
||||
return []
|
||||
}
|
||||
return CITY_NAMES.filter((city) => compact.includes(city))
|
||||
return extractKnownCityNames(text)
|
||||
}
|
||||
|
||||
function collectFieldSignals(ocrDocuments = []) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { compactText, formatDate, normalizeText, parseDate } from './aiDocumentQueryText.js'
|
||||
import { extractKnownCityNames } from './knownCityNames.js'
|
||||
|
||||
const STATUS_FILTERS = [
|
||||
{ label: '草稿', keys: ['draft'], pattern: /草稿|未提交/ },
|
||||
@@ -202,9 +203,13 @@ function normalizeKeywordCandidate(value = '') {
|
||||
function resolveKeywordFilter(prompt) {
|
||||
const text = normalizeText(prompt)
|
||||
const compact = compactText(prompt)
|
||||
const explicitMatch = text.match(/(?:关于|有关|包含|含有|关键词|关键字|事由(?:是|为|包含|含有)?)[::\s]*(?<keyword>[\u4e00-\u9fa5A-Za-z0-9_-]{2,32})/u)
|
||||
const explicitMatch = text.match(/(?:关于|有关|包含|含有|关键词|关键字|事由(?:是|为|包含|含有)?)[::\s“”"']*(?<keyword>[\u4e00-\u9fa5A-Za-z0-9_-]{2,32})/u)
|
||||
const relatedMatch = compact.match(/(?<keyword>[\u4e00-\u9fa5A-Za-z0-9_-]{2,32})相关(?:的)?(?:单据|单子|申请单|报销单|审核单|审批单)/u)
|
||||
const keyword = normalizeKeywordCandidate(explicitMatch?.groups?.keyword || relatedMatch?.groups?.keyword || '')
|
||||
const cityNames = extractKnownCityNames(text)
|
||||
const cityKeyword = cityNames.length === 1 ? cityNames[0] : ''
|
||||
const keyword = normalizeKeywordCandidate(
|
||||
explicitMatch?.groups?.keyword || relatedMatch?.groups?.keyword || cityKeyword
|
||||
)
|
||||
if (!keyword || /^(现在|当前|哪些|审核|审批|申请|报销|单据|单子)$/u.test(keyword)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ const APPROVAL_PATTERN =
|
||||
/待我审核|待审|审核|审批|审核意见|审批意见|审批通过|审批驳回|驳回|退回|审核中心|审批中心|领导审批|财务审核|处理意见/
|
||||
const KNOWLEDGE_PATTERN =
|
||||
/制度|政策|标准|规则|规定|流程|口径|依据|上限|额度|补贴|住宿标准|差旅标准|报销标准|票据要求|可不可以|能不能|怎么规定|如何计算|怎么算/
|
||||
const DOCUMENT_QUERY_ACTION_PATTERN = /查询|查找|查看|筛选|过滤|列出|检索|找出|搜索|搜一下/
|
||||
const DOCUMENT_QUERY_TARGET_PATTERN = /单据|单子|申请单|报销单|审核单|审批单|待办|草稿/
|
||||
const EXPENSE_OPERATION_PATTERN = /发起报销|报销单|票据|发票|火车票|高铁票|机票|的士票|草稿|归集|上传|关联单据|继续下一步/
|
||||
const CURRENT_CLAIM_RISK_PATTERN = /这张|当前|本单|该单|单据|风险|超标|异常|重复|待补/
|
||||
const FINANCE_OPERATING_PATTERN = buildKeywordPattern([
|
||||
@@ -123,6 +125,15 @@ export function hasReimbursementIntentSignal(rawText) {
|
||||
return EXPENSE_PATTERN.test(normalizeText(rawText))
|
||||
}
|
||||
|
||||
export function hasDocumentQueryIntentSignal(rawText) {
|
||||
const text = normalizeText(rawText)
|
||||
return Boolean(
|
||||
text &&
|
||||
DOCUMENT_QUERY_ACTION_PATTERN.test(text) &&
|
||||
DOCUMENT_QUERY_TARGET_PATTERN.test(text)
|
||||
)
|
||||
}
|
||||
|
||||
export function hasExpenseApplicationIntentSignal(rawText) {
|
||||
const text = normalizeText(rawText)
|
||||
if (!text) {
|
||||
@@ -172,6 +183,16 @@ export function inferAssistantScopeTarget(rawText, options = {}) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (hasDocumentQueryIntentSignal(text)) {
|
||||
if (APPROVAL_PATTERN.test(text) && /待我审核|待审|审核单|审批单|待办/.test(text)) {
|
||||
return ASSISTANT_SCOPE_SESSION_APPROVAL
|
||||
}
|
||||
if (/申请单|申请类单据/.test(text) && !/报销单|报销类单据/.test(text)) {
|
||||
return ASSISTANT_SCOPE_SESSION_APPLICATION
|
||||
}
|
||||
return ASSISTANT_SCOPE_SESSION_EXPENSE
|
||||
}
|
||||
|
||||
if (hasAmbiguousTravelFlowIntent(text)) {
|
||||
return ASSISTANT_SCOPE_SESSION_STEWARD
|
||||
}
|
||||
|
||||
46
web/src/utils/knownCityNames.js
Normal file
46
web/src/utils/knownCityNames.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export const KNOWN_CITY_NAMES = [
|
||||
'北京',
|
||||
'上海',
|
||||
'广州',
|
||||
'深圳',
|
||||
'武汉',
|
||||
'南京',
|
||||
'杭州',
|
||||
'成都',
|
||||
'重庆',
|
||||
'西安',
|
||||
'天津',
|
||||
'苏州',
|
||||
'长沙',
|
||||
'郑州',
|
||||
'青岛',
|
||||
'厦门',
|
||||
'宁波',
|
||||
'无锡',
|
||||
'合肥',
|
||||
'福州',
|
||||
'昆明',
|
||||
'大连',
|
||||
'沈阳',
|
||||
'济南',
|
||||
'哈尔滨',
|
||||
'长春',
|
||||
'南昌',
|
||||
'太原',
|
||||
'贵阳',
|
||||
'南宁',
|
||||
'石家庄',
|
||||
'兰州',
|
||||
'银川',
|
||||
'西宁',
|
||||
'海口',
|
||||
'拉萨'
|
||||
]
|
||||
|
||||
export function extractKnownCityNames(value = '') {
|
||||
const compact = String(value || '').replace(/\s+/g, '')
|
||||
if (!compact) {
|
||||
return []
|
||||
}
|
||||
return KNOWN_CITY_NAMES.filter((city) => compact.includes(city))
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { hasDocumentQueryIntentSignal } from '../../utils/assistantSessionScope.js'
|
||||
|
||||
export const GUIDED_FLOW_MODE_NONE = ''
|
||||
export const GUIDED_FLOW_MODE_REIMBURSEMENT = 'reimbursement_guide'
|
||||
export const GUIDED_FLOW_MODE_STATUS_QUERY = 'status_query_guide'
|
||||
@@ -505,7 +507,7 @@ export function shouldConfirmGuidedInterruption(text, state) {
|
||||
if (!normalized || NO_ATTACHMENT_TEXT_PATTERN.test(normalized)) {
|
||||
return false
|
||||
}
|
||||
return INTERRUPTION_PATTERN.test(normalized)
|
||||
return INTERRUPTION_PATTERN.test(normalized) || hasDocumentQueryIntentSignal(normalized)
|
||||
}
|
||||
|
||||
export function buildGuidedInterruptionText(text) {
|
||||
|
||||
@@ -153,6 +153,29 @@ test('AI document query combines natural-language filters', () => {
|
||||
assert.match(buildAiDocumentQueryConditionSummary(intent), /金额:不少于1000元/)
|
||||
})
|
||||
|
||||
test('AI document query recognizes colloquial travel city filtering commands', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('出差上海的单据请筛选一下', { today })
|
||||
const records = filterAiDocumentQueryRecords(claims, intent)
|
||||
|
||||
assert.equal(intent?.source, 'accessible')
|
||||
assert.equal(intent?.expenseTypeFilter?.label, '差旅费')
|
||||
assert.equal(intent?.keywordFilter?.label, '上海')
|
||||
assert.deepEqual(
|
||||
records.map((record) => record.documentNo),
|
||||
['CL-20260221001', 'AP-20260220001']
|
||||
)
|
||||
})
|
||||
|
||||
test('AI document query recognizes quoted keywords generated by the guided query flow', () => {
|
||||
const intent = resolveAiDocumentQueryIntent(
|
||||
'帮我查询地点或事由包含“上海电力”的报销单据状态,筛选最近的 5 条记录',
|
||||
{ today }
|
||||
)
|
||||
|
||||
assert.equal(intent?.documentType, 'reimbursement')
|
||||
assert.equal(intent?.keywordFilter?.label, '上海电力')
|
||||
})
|
||||
|
||||
test('AI document query filters draft candidates by relative day', () => {
|
||||
const intent = resolveAiDocumentQueryIntent('我的 3天前 草稿单据', { today: '2026-06-24' })
|
||||
const records = filterAiDocumentQueryRecords([
|
||||
|
||||
@@ -181,6 +181,14 @@ test('assistant session scope guard keeps business boundaries isolated', () => {
|
||||
resolveAssistantScopeGuard('帮我查询待我审核的单据', SESSION_TYPE_EXPENSE).targetSessionType,
|
||||
SESSION_TYPE_APPROVAL
|
||||
)
|
||||
assert.equal(
|
||||
resolveAssistantScopeGuard('出差上海的单据请筛选一下', SESSION_TYPE_EXPENSE),
|
||||
null
|
||||
)
|
||||
assert.equal(
|
||||
resolveAssistantScopeGuard('出差上海的单据请筛选一下', SESSION_TYPE_APPLICATION).targetSessionType,
|
||||
SESSION_TYPE_EXPENSE
|
||||
)
|
||||
assert.equal(
|
||||
resolveAssistantScopeGuard('差旅住宿标准是多少', SESSION_TYPE_EXPENSE).targetSessionType,
|
||||
SESSION_TYPE_KNOWLEDGE
|
||||
@@ -414,6 +422,7 @@ test('guided reimbursement interrupts suspicious questions before expensive flow
|
||||
const state = selectGuidedExpenseType(createGuidedReimbursementState(), 'transport')
|
||||
assert.equal(shouldConfirmGuidedInterruption('送客户去机场', state), false)
|
||||
assert.equal(shouldConfirmGuidedInterruption('帮我查询一下上周的报销状态?', state), true)
|
||||
assert.equal(shouldConfirmGuidedInterruption('出差上海的单据请筛选一下', state), true)
|
||||
assert.deepEqual(
|
||||
buildGuidedInterruptionActions().map((action) => action.action_type),
|
||||
[GUIDED_ACTION_CONTINUE_FILLING, GUIDED_ACTION_PROCESS_INTERRUPTION]
|
||||
|
||||
Reference in New Issue
Block a user