sales-champion-generation-plan-smoke-test.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. 'use strict';
  2. const assert = require('assert/strict');
  3. const { planResponse, plannerInstruction } = require('../mcp/src/core/response-quality-planner');
  4. const { planSalesMove } = require('../mcp/src/core/agent-sales-move-planner');
  5. const {
  6. buildResponseGenerationStrategy,
  7. responseGenerationInstruction,
  8. } = require('../mcp/src/core/response-generation-strategy');
  9. const { QiweiAgentRuntime } = require('../mcp/src/core/agent-runtime');
  10. function buildPlan({ inboundContent, knownFields = [], missingFields = [], profile = {}, citations = [], toolTrace = [] }) {
  11. const responsePlan = planResponse({ inboundContent, profile, citations, toolTrace });
  12. const salesMovePlan = planSalesMove({
  13. scenario: responsePlan.scenario,
  14. riskLevel: responsePlan.requireHumanHandoff ? 'high' : 'none',
  15. riskTypes: responsePlan.riskTypes,
  16. requiresHuman: responsePlan.requireHumanHandoff,
  17. knownFields,
  18. missingFields,
  19. });
  20. const generationPlan = buildResponseGenerationStrategy({
  21. inboundContent,
  22. responsePlan,
  23. salesMovePlan,
  24. profile,
  25. citations,
  26. toolTrace,
  27. });
  28. return {
  29. responsePlan,
  30. salesMovePlan,
  31. generationPlan,
  32. instruction: responseGenerationInstruction(generationPlan),
  33. };
  34. }
  35. async function main() {
  36. const comparison = buildPlan({
  37. inboundContent: '两套差20万,我先看哪套?',
  38. knownFields: ['daily_commute', 'option_a_price', 'option_b_price', 'option_tradeoffs'],
  39. missingFields: ['living_room_is_hard_requirement', 'floor_preference', 'viewing_time'],
  40. profile: { daily_commute: true },
  41. });
  42. assert.equal(comparison.generationPlan.customerQuestion.text, '两套差20万,我先看哪套?');
  43. assert.equal(comparison.generationPlan.directAnswer.position, 'opening');
  44. assert.equal(comparison.generationPlan.directAnswer.answerBeforeQuestion, true);
  45. assert.equal(comparison.generationPlan.judgment.mode, 'bounded_tradeoff');
  46. assert.equal(comparison.generationPlan.singleNextStep.maxCount, 1);
  47. assert.equal(comparison.generationPlan.singleNextStep.maxQuestions, 1);
  48. assert.equal(comparison.generationPlan.singleNextStep.targetField, 'living_room_is_hard_requirement');
  49. assert.match(comparison.instruction, /先给有依据的结论或倾向/);
  50. assert.match(comparison.instruction, /倾向、取舍或理由/);
  51. assert(comparison.instruction.indexOf('1. 先答') < comparison.instruction.indexOf('2. 再判断'));
  52. assert(comparison.instruction.indexOf('2. 再判断') < comparison.instruction.indexOf('3. 唯一步'));
  53. const knownConstraints = buildPlan({
  54. inboundContent: '中上楼层可以,就按前面的条件看。',
  55. knownFields: ['budget', 'region', 'layout', 'floor'],
  56. missingFields: ['budget', 'region', 'layout', 'commute_priority'],
  57. });
  58. assert.equal(knownConstraints.salesMovePlan.singleNextStep.targetField, 'commute_priority');
  59. assert(!['budget', 'region', 'layout'].includes(knownConstraints.salesMovePlan.singleNextStep.targetField));
  60. assert.equal(knownConstraints.generationPlan.singleNextStep.maxQuestions, 1);
  61. const missingEvidence = buildPlan({
  62. inboundContent: '这套现在挂牌价是多少,值不值?',
  63. knownFields: ['purpose'],
  64. missingFields: ['property_id', 'price_evidence'],
  65. });
  66. assert.equal(missingEvidence.generationPlan.directAnswer.evidenceBoundary, 'direction_with_explicit_unknowns');
  67. assert.equal(missingEvidence.generationPlan.judgment.evidenceLimited, true);
  68. assert.match(missingEvidence.instruction, /先给当前能成立的方向判断/);
  69. assert.match(missingEvidence.instruction, /不编造具体产品或库存事实/);
  70. const riskBoundary = buildPlan({
  71. inboundContent: '这个房子的产权和税费能确认吗?',
  72. knownFields: ['property_id'],
  73. missingFields: ['ownership_evidence', 'tax_evidence'],
  74. });
  75. assert.equal(riskBoundary.salesMovePlan.primaryMove, 'handoff');
  76. assert.equal(riskBoundary.generationPlan.singleNextStep.kind, 'human_boundary');
  77. assert.equal(riskBoundary.generationPlan.singleNextStep.evidenceRequired, true);
  78. assert.equal(riskBoundary.generationPlan.constraints.preserveFactAndActionGates, true);
  79. assert.match(riskBoundary.instruction, /没有匹配凭证时/);
  80. assert.match(plannerInstruction(riskBoundary.responsePlan), /只有存在真实交接凭证时/);
  81. const noReply = buildPlan({ inboundContent: '好的' });
  82. assert.equal(noReply.generationPlan.directAnswer.required, false);
  83. assert.equal(noReply.generationPlan.singleNextStep.maxCount, 0);
  84. let capturedSystem = '';
  85. const runtime = new QiweiAgentRuntime({
  86. config: {
  87. provider: 'fixture',
  88. model: 'fixture',
  89. maxToolRounds: 1,
  90. qualityPassScore: 82,
  91. requiredProfileFields: ['living_room_is_hard_requirement', 'floor_preference', 'viewing_time'],
  92. salesGuidelines: [],
  93. businessGoal: '先解决客户当前选择,再推进一个决定性输入',
  94. stagePlaybook: '比较阶段先给倾向与理由',
  95. },
  96. knowledge: { contextText: () => '', search: () => [] },
  97. modelClient: {
  98. async complete(messages) {
  99. capturedSystem = String(messages.find(message => message.role === 'system')?.content || '');
  100. return {
  101. content: JSON.stringify({
  102. reply: '按您每天通勤,先看离地铁近的那套。多出的20万如果能换来稳定通勤,优先级通常高于暂时更大的客厅;客厅空间是不是硬要求?',
  103. confidence: 0.92,
  104. intent: 'compare_options',
  105. reason: '先给通勤取舍,再确认一个决定性条件。',
  106. requiresHuman: false,
  107. profileUpdates: {},
  108. tasks: [],
  109. alerts: [],
  110. }),
  111. };
  112. },
  113. },
  114. });
  115. const runtimeResult = await runtime.run({
  116. conversation: { id: 'sc-gap-01-fixture', mode: 'review' },
  117. messages: [
  118. { direction: 'inbound', content: '我每天通勤,A离地铁近但客厅小,B离地铁远但客厅大。' },
  119. { direction: 'inbound', content: '两套差20万,我先看哪套?' },
  120. ],
  121. profile: {
  122. daily_commute: true,
  123. option_a_price: 'customer_provided',
  124. option_b_price: 'customer_provided',
  125. option_tradeoffs: 'customer_provided',
  126. },
  127. inboundContent: '两套差20万,我先看哪套?',
  128. });
  129. assert.match(capturedSystem, /客户本轮真正要解决的问题:两套差20万,我先看哪套/);
  130. assert(capturedSystem.indexOf('1. 先答') < capturedSystem.indexOf('3. 唯一步'));
  131. assert.equal(runtimeResult.responseGenerationPlan.directAnswer.required, true);
  132. assert.equal(runtimeResult.responseGenerationPlan.singleNextStep.targetField, 'living_room_is_hard_requirement');
  133. assert.match(runtimeResult.content, /^按您每天通勤,先看/);
  134. process.stdout.write(`${JSON.stringify({
  135. status: 'ok',
  136. cases: 5,
  137. fixtureRuntimeIntegrations: 1,
  138. assertions: 33,
  139. contract: ['customerQuestion', 'directAnswer', 'judgment', 'singleNextStep'],
  140. }, null, 2)}\n`);
  141. }
  142. main().catch(error => {
  143. process.stderr.write(`${error.stack || error.message}\n`);
  144. process.exit(1);
  145. });