Files
X-Financial/web/src/composables/workbenchAiMode/workbenchAiIntentPlannerModel.js
caoxiaozhu 59353308a2 feat(web): AI 意图规划置信度阈值与动作策略细化
- workbenchAiIntentPlannerModel 新增 WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD 与 isLowConfidenceTravelApplicationPlan,shouldRequestWorkbenchAiIntentPlan 增加业务关键词前置过滤
- resolveExecutableTravelApplicationPlan 区分 requestedSubmit 与提交确认(submitRequiresConfirmation),autoSubmit 不再直接置真
- workbenchIntentActionPolicy 改用 policyDecision 路由(need_confirmation/query_candidates),透传 riskLevel/requiresSelection/requiresConfirmation
- workbenchIntentFrameModel 补充 query 动作识别,usePersonalWorkbenchAiMode/useWorkbenchAiActionRouter/useWorkbenchAiApplicationPreviewFlow 接入低置信度与确认流程
- 更新 intent-planner-model/intent-frame-model/application-gate-model/fast-preview 测试
2026-06-25 10:55:49 +08:00

306 lines
10 KiB
JavaScript
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.
import { resolveInlineTravelApplicationRequest } from './workbenchAiApplicationGateModel.js'
export const WORKBENCH_AI_INTENT_SOURCE_MODEL = 'llm_function_call'
export const WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK = 'rule_fallback'
export const WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD = 0.6
export const WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW = 'build_application_preview'
export const WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS = 'validate_required_fields'
export const WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT = 'save_application_draft'
export const WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK = 'run_duplicate_precheck'
export const WORKBENCH_AI_STEP_SUBMIT_APPLICATION = 'submit_application'
const TRAVEL_APPLICATION_INTENT = 'create_travel_application'
function normalizePromptAction(prompt = '') {
const compact = String(prompt || '').replace(/\s+/g, '')
if (/直接提交|提交申请|确认提交|提交审批/.test(compact)) {
return 'submit'
}
if (/保存草稿|保存|存草稿|先保存/.test(compact)) {
return 'save_draft'
}
return 'preview'
}
function normalizePlannerSource(value = '') {
return String(value || '').trim() === WORKBENCH_AI_INTENT_SOURCE_MODEL
? WORKBENCH_AI_INTENT_SOURCE_MODEL
: WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK
}
function normalizeSlotKey(key = '') {
const normalized = String(key || '').trim()
if (['time_range', 'business_time', 'occurred_date', 'application_time'].includes(normalized)) {
return 'timeRange'
}
if (['transport_mode', 'transportType', 'transport_type', 'trafficMode'].includes(normalized)) {
return 'transportMode'
}
if (['business_reason', 'businessPurpose', 'purpose'].includes(normalized)) {
return 'reason'
}
return normalized
}
function normalizeSlots(rawSlots = {}) {
if (!rawSlots || typeof rawSlots !== 'object') {
return {}
}
return Object.entries(rawSlots).reduce((slots, [key, value]) => {
const normalizedKey = normalizeSlotKey(key)
const normalizedValue = String(value || '').trim()
if (normalizedKey && normalizedValue) {
slots[normalizedKey] = normalizedValue
}
return slots
}, {})
}
const ONTOLOGY_FIELD_ALIASES = {
business_time: 'time_range',
occurred_date: 'time_range',
application_time: 'time_range',
transportType: 'transport_mode',
transport_type: 'transport_mode',
trafficMode: 'transport_mode',
business_reason: 'reason',
businessPurpose: 'reason',
purpose: 'reason'
}
const SUPPORTED_ONTOLOGY_FIELDS = new Set([
'expense_type',
'time_range',
'location',
'reason',
'amount',
'transport_mode',
'attachments',
'customer_name',
'merchant_name',
'department_name',
'employee_name',
'employee_no'
])
function normalizeOntologyFields(rawFields = {}) {
if (!rawFields || typeof rawFields !== 'object') {
return {}
}
return Object.entries(rawFields).reduce((fields, [key, value]) => {
const normalizedKey = ONTOLOGY_FIELD_ALIASES[String(key || '').trim()] || String(key || '').trim()
const normalizedValue = String(value || '').trim()
if (SUPPORTED_ONTOLOGY_FIELDS.has(normalizedKey) && normalizedValue) {
fields[normalizedKey] = normalizedValue
}
return fields
}, {})
}
function buildApplicationSteps(requestedAction = 'preview') {
const steps = [
WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW,
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS
]
if (requestedAction === 'submit') {
steps.push(
WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK,
WORKBENCH_AI_STEP_SUBMIT_APPLICATION
)
} else if (requestedAction === 'save_draft') {
steps.push(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT)
}
return steps
}
function normalizeServerApplicationSteps(rawSteps = []) {
if (!Array.isArray(rawSteps)) {
return []
}
const mappedSteps = rawSteps
.map((step) => String(step?.action_type || step?.actionType || '').trim())
.map((actionType) => {
if (actionType === WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW) {
return WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW
}
if (actionType === WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS) {
return WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS
}
if (actionType === WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT) {
return WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT
}
if (actionType === WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK) {
return WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK
}
if (actionType === WORKBENCH_AI_STEP_SUBMIT_APPLICATION) {
return WORKBENCH_AI_STEP_SUBMIT_APPLICATION
}
return ''
})
.filter(Boolean)
return [...new Set(mappedSteps)]
}
function findModelTravelApplicationTask(rawPlan = {}) {
const tasks = Array.isArray(rawPlan?.tasks) ? rawPlan.tasks : []
return tasks.find((task) => {
const taskType = String(task?.task_type || task?.taskType || '').trim()
const assignedAgent = String(task?.assigned_agent || task?.assignedAgent || '').trim()
return taskType === 'expense_application' || assignedAgent === 'application_assistant'
}) || null
}
function resolveCandidateFlows(rawPlan = {}) {
const pendingFlow = rawPlan?.pending_flow_confirmation || rawPlan?.pendingFlowConfirmation || {}
const pendingCandidates = pendingFlow?.candidate_flows || pendingFlow?.candidateFlows
const rootCandidates = rawPlan?.candidate_flows || rawPlan?.candidateFlows
if (Array.isArray(pendingCandidates)) {
return pendingCandidates
}
return Array.isArray(rootCandidates) ? rootCandidates : []
}
function findSingleApplicationCandidateFlow(rawPlan = {}) {
const pendingFlow = rawPlan?.pending_flow_confirmation || rawPlan?.pendingFlowConfirmation || {}
if (String(pendingFlow?.status || '').trim() !== 'pending') {
return null
}
const candidateFlows = resolveCandidateFlows(rawPlan)
if (candidateFlows.length !== 1) {
return null
}
const [candidate] = candidateFlows
const flowId = String(candidate?.flow_id || candidate?.flowId || '').trim()
const label = String(candidate?.label || '').trim()
if (flowId === 'travel_application' && /先发起出差申请/.test(label)) {
return candidate
}
return null
}
export function normalizeWorkbenchAiIntentPlan(rawPlan = {}, options = {}) {
const prompt = String(options.prompt || rawPlan?.sourceText || rawPlan?.source_text || '').trim()
const task = findModelTravelApplicationTask(rawPlan)
if (!task) {
const candidateFlow = findSingleApplicationCandidateFlow(rawPlan)
if (!candidateFlow) {
return null
}
const ontologyFields = normalizeOntologyFields(candidateFlow.ontology_fields || candidateFlow.ontologyFields)
const requestedAction = normalizePromptAction(prompt)
return {
source: normalizePlannerSource(rawPlan.planning_source || rawPlan.planningSource),
intent: TRAVEL_APPLICATION_INTENT,
requestedAction,
confidence: Number(candidateFlow.confidence || rawPlan.confidence || 0),
sourceText: prompt,
ontologyFields,
slots: normalizeSlots(ontologyFields),
missingFields: Array.isArray(candidateFlow.missing_fields || candidateFlow.missingFields)
? candidateFlow.missing_fields || candidateFlow.missingFields
: [],
steps: buildApplicationSteps(requestedAction)
}
}
const rawOntologyFields = task.ontology_fields || task.ontologyFields || rawPlan.slots
const ontologyFields = normalizeOntologyFields(rawOntologyFields)
const requestedAction = String(
task.requested_action ||
task.requestedAction ||
rawPlan.requested_action ||
rawPlan.requestedAction ||
''
).trim() || normalizePromptAction(prompt)
const serverSteps = normalizeServerApplicationSteps(task.action_steps || task.actionSteps)
return {
source: normalizePlannerSource(rawPlan.planning_source || rawPlan.planningSource),
intent: TRAVEL_APPLICATION_INTENT,
requestedAction,
confidence: Number(task.confidence || rawPlan.confidence || 0),
sourceText: prompt,
ontologyFields,
slots: normalizeSlots(ontologyFields),
missingFields: Array.isArray(task.missing_fields || task.missingFields)
? task.missing_fields || task.missingFields
: [],
steps: serverSteps.length ? serverSteps : buildApplicationSteps(requestedAction)
}
}
export function buildRuleFallbackWorkbenchAiIntentPlan(prompt = '') {
const request = resolveInlineTravelApplicationRequest(prompt)
if (!request) {
return null
}
const requestedAction = request.requestedSubmit ? 'submit' : normalizePromptAction(prompt)
return {
source: WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK,
intent: TRAVEL_APPLICATION_INTENT,
requestedAction,
confidence: 0.72,
sourceText: request.sourceText,
ontologyFields: {},
slots: {},
missingFields: [],
steps: buildApplicationSteps(requestedAction)
}
}
export function shouldRequestWorkbenchAiIntentPlan(prompt = '') {
const compact = String(prompt || '').replace(/\s+/g, '')
if (!compact) {
return false
}
if (compact.length < 2 || /^[\d\s.,,。:;!?-]+$/.test(compact)) {
return false
}
if (!WORKBENCH_AI_BUSINESS_KEYWORD_PATTERN.test(compact)) {
return false
}
return true
}
const WORKBENCH_AI_BUSINESS_KEYWORD_PATTERN = (
/报销|报账|出差|差旅|申请|审批|审核|报销单|申请单|草稿|删除|提交|保存|查|看|找|列出|发起|新建|创建|驳回|退回|通过|多少|标准|制度|规则|政策/
)
export function resolveExecutableTravelApplicationPlan(plan = null) {
if (!plan || plan.intent !== TRAVEL_APPLICATION_INTENT) {
return null
}
if (!Array.isArray(plan.steps) || !plan.steps.includes(WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW)) {
return null
}
const requestedSubmit = plan.steps.includes(WORKBENCH_AI_STEP_SUBMIT_APPLICATION)
return {
expenseType: 'travel',
expenseTypeLabel: '差旅费',
sourceText: String(plan.sourceText || '').trim(),
ontologyFields: normalizeOntologyFields(plan.ontologyFields || {}),
autoSubmit: false,
autoSaveDraft: plan.steps.includes(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT),
requestedSubmit,
submitRequiresConfirmation: requestedSubmit
}
}
export function isLowConfidenceTravelApplicationPlan(plan = null) {
if (!plan || plan.intent !== TRAVEL_APPLICATION_INTENT) {
return false
}
if (plan.source === WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK) {
return false
}
if (plan.requestedAction === 'submit' || plan.requestedAction === 'save_draft') {
return false
}
const confidence = Number(plan.confidence)
if (!Number.isFinite(confidence)) {
return false
}
return confidence < WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD
}