feat(web): AI 工作台意图规划与规划思考模型
- 新增 workbenchAiIntentPlannerModel,基于 LLM function_call 解析建单/草稿/提交意图,区分 model 与 rule_fallback 来源 - 新增 workbenchAiPlanningThinkingModel 合并规划思考事件流,按 eventId 去重合并 - application gate/preview 模型接入意图规划,usePersonalWorkbenchAiMode/useWorkbenchAiStewardFlow/useWorkbenchAiActionRouter 链路适配,支持上下文提交 - steward 服务与 stewardPlanModel 适配新动作结构,receipt-folder-view 微调样式 - 新增 intent-planner-model/application-context-submit/steward-actions-service 测试,更新 gate-model/action-router/plan-message-copy/fast-preview 测试
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
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_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.autoSubmit ? '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
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return {
|
||||
expenseType: 'travel',
|
||||
expenseTypeLabel: '差旅费',
|
||||
sourceText: String(plan.sourceText || '').trim(),
|
||||
ontologyFields: normalizeOntologyFields(plan.ontologyFields || {}),
|
||||
autoSubmit: plan.steps.includes(WORKBENCH_AI_STEP_SUBMIT_APPLICATION),
|
||||
autoSaveDraft: plan.steps.includes(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user