'use strict'; const assert = require('assert/strict'); const { planResponse, plannerInstruction } = require('../mcp/src/core/response-quality-planner'); const { planSalesMove } = require('../mcp/src/core/agent-sales-move-planner'); const { buildResponseGenerationStrategy, responseGenerationInstruction, } = require('../mcp/src/core/response-generation-strategy'); const { QiweiAgentRuntime } = require('../mcp/src/core/agent-runtime'); function buildPlan({ inboundContent, knownFields = [], missingFields = [], profile = {}, citations = [], toolTrace = [] }) { const responsePlan = planResponse({ inboundContent, profile, citations, toolTrace }); const salesMovePlan = planSalesMove({ scenario: responsePlan.scenario, riskLevel: responsePlan.requireHumanHandoff ? 'high' : 'none', riskTypes: responsePlan.riskTypes, requiresHuman: responsePlan.requireHumanHandoff, knownFields, missingFields, }); const generationPlan = buildResponseGenerationStrategy({ inboundContent, responsePlan, salesMovePlan, profile, citations, toolTrace, }); return { responsePlan, salesMovePlan, generationPlan, instruction: responseGenerationInstruction(generationPlan), }; } async function main() { const comparison = buildPlan({ inboundContent: '两套差20万,我先看哪套?', knownFields: ['daily_commute', 'option_a_price', 'option_b_price', 'option_tradeoffs'], missingFields: ['living_room_is_hard_requirement', 'floor_preference', 'viewing_time'], profile: { daily_commute: true }, }); assert.equal(comparison.generationPlan.customerQuestion.text, '两套差20万,我先看哪套?'); assert.equal(comparison.generationPlan.directAnswer.position, 'opening'); assert.equal(comparison.generationPlan.directAnswer.answerBeforeQuestion, true); assert.equal(comparison.generationPlan.judgment.mode, 'bounded_tradeoff'); assert.equal(comparison.generationPlan.singleNextStep.maxCount, 1); assert.equal(comparison.generationPlan.singleNextStep.maxQuestions, 1); assert.equal(comparison.generationPlan.singleNextStep.targetField, 'living_room_is_hard_requirement'); assert.match(comparison.instruction, /先给有依据的结论或倾向/); assert.match(comparison.instruction, /倾向、取舍或理由/); assert(comparison.instruction.indexOf('1. 先答') < comparison.instruction.indexOf('2. 再判断')); assert(comparison.instruction.indexOf('2. 再判断') < comparison.instruction.indexOf('3. 唯一步')); const knownConstraints = buildPlan({ inboundContent: '中上楼层可以,就按前面的条件看。', knownFields: ['budget', 'region', 'layout', 'floor'], missingFields: ['budget', 'region', 'layout', 'commute_priority'], }); assert.equal(knownConstraints.salesMovePlan.singleNextStep.targetField, 'commute_priority'); assert(!['budget', 'region', 'layout'].includes(knownConstraints.salesMovePlan.singleNextStep.targetField)); assert.equal(knownConstraints.generationPlan.singleNextStep.maxQuestions, 1); const missingEvidence = buildPlan({ inboundContent: '这套现在挂牌价是多少,值不值?', knownFields: ['purpose'], missingFields: ['property_id', 'price_evidence'], }); assert.equal(missingEvidence.generationPlan.directAnswer.evidenceBoundary, 'direction_with_explicit_unknowns'); assert.equal(missingEvidence.generationPlan.judgment.evidenceLimited, true); assert.match(missingEvidence.instruction, /先给当前能成立的方向判断/); assert.match(missingEvidence.instruction, /不编造具体产品或库存事实/); const riskBoundary = buildPlan({ inboundContent: '这个房子的产权和税费能确认吗?', knownFields: ['property_id'], missingFields: ['ownership_evidence', 'tax_evidence'], }); assert.equal(riskBoundary.salesMovePlan.primaryMove, 'handoff'); assert.equal(riskBoundary.generationPlan.singleNextStep.kind, 'human_boundary'); assert.equal(riskBoundary.generationPlan.singleNextStep.evidenceRequired, true); assert.equal(riskBoundary.generationPlan.constraints.preserveFactAndActionGates, true); assert.match(riskBoundary.instruction, /没有匹配凭证时/); assert.match(plannerInstruction(riskBoundary.responsePlan), /只有存在真实交接凭证时/); const noReply = buildPlan({ inboundContent: '好的' }); assert.equal(noReply.generationPlan.directAnswer.required, false); assert.equal(noReply.generationPlan.singleNextStep.maxCount, 0); let capturedSystem = ''; const runtime = new QiweiAgentRuntime({ config: { provider: 'fixture', model: 'fixture', maxToolRounds: 1, qualityPassScore: 82, requiredProfileFields: ['living_room_is_hard_requirement', 'floor_preference', 'viewing_time'], salesGuidelines: [], businessGoal: '先解决客户当前选择,再推进一个决定性输入', stagePlaybook: '比较阶段先给倾向与理由', }, knowledge: { contextText: () => '', search: () => [] }, modelClient: { async complete(messages) { capturedSystem = String(messages.find(message => message.role === 'system')?.content || ''); return { content: JSON.stringify({ reply: '按您每天通勤,先看离地铁近的那套。多出的20万如果能换来稳定通勤,优先级通常高于暂时更大的客厅;客厅空间是不是硬要求?', confidence: 0.92, intent: 'compare_options', reason: '先给通勤取舍,再确认一个决定性条件。', requiresHuman: false, profileUpdates: {}, tasks: [], alerts: [], }), }; }, }, }); const runtimeResult = await runtime.run({ conversation: { id: 'sc-gap-01-fixture', mode: 'review' }, messages: [ { direction: 'inbound', content: '我每天通勤,A离地铁近但客厅小,B离地铁远但客厅大。' }, { direction: 'inbound', content: '两套差20万,我先看哪套?' }, ], profile: { daily_commute: true, option_a_price: 'customer_provided', option_b_price: 'customer_provided', option_tradeoffs: 'customer_provided', }, inboundContent: '两套差20万,我先看哪套?', }); assert.match(capturedSystem, /客户本轮真正要解决的问题:两套差20万,我先看哪套/); assert(capturedSystem.indexOf('1. 先答') < capturedSystem.indexOf('3. 唯一步')); assert.equal(runtimeResult.responseGenerationPlan.directAnswer.required, true); assert.equal(runtimeResult.responseGenerationPlan.singleNextStep.targetField, 'living_room_is_hard_requirement'); assert.match(runtimeResult.content, /^按您每天通勤,先看/); process.stdout.write(`${JSON.stringify({ status: 'ok', cases: 5, fixtureRuntimeIntegrations: 1, assertions: 33, contract: ['customerQuestion', 'directAnswer', 'judgment', 'singleNextStep'], }, null, 2)}\n`); } main().catch(error => { process.stderr.write(`${error.stack || error.message}\n`); process.exit(1); });