From e809740fdb5813b7f2ecf2bc498f7823289b9463 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 8 Feb 2026 17:01:56 -0800 Subject: [PATCH] feat(agents): add run_professional_assessment tool with payment gate + artifact persistence Replaces ad-hoc assessment flow with structured pipeline: - Code-level payment verification (checks PAID ASSESSMENT order) - Info completeness validation (age, nationality, education, work exp) - Assessment expert invocation with result parsing - Automatic persistence as UserArtifact (assessment_report type) - 30-day dedup (existing report within 30 days returns cached) - Frontend rendering for all status codes (completed, payment_required, info_incomplete, already_assessed, error) - System prompt updated to mandate new tool for paid assessments - Post-assessment auto-generation of checklist + timeline Co-Authored-By: Claude Opus 4.6 --- .../prompts/coordinator-system-prompt.ts | 109 +++++++--- .../agents/tools/coordinator-tools.ts | 32 ++- .../agents/tools/tool-execution-queue.ts | 1 + .../claude/tools/immigration-tools.service.ts | 198 ++++++++++++++++++ .../presentation/components/MessageBubble.tsx | 102 +++++++++ 5 files changed, 414 insertions(+), 28 deletions(-) diff --git a/packages/services/conversation-service/src/infrastructure/agents/prompts/coordinator-system-prompt.ts b/packages/services/conversation-service/src/infrastructure/agents/prompts/coordinator-system-prompt.ts index 11675ff..4a6b212 100644 --- a/packages/services/conversation-service/src/infrastructure/agents/prompts/coordinator-system-prompt.ts +++ b/packages/services/conversation-service/src/infrastructure/agents/prompts/coordinator-system-prompt.ts @@ -208,23 +208,18 @@ ${companyName} 是${companyDescription}。 - 用自然对话语言呈现,不要照搬【政策要点】【申请条件】这种格式标题 - 如果用户想了解更多,他们会追问 -## 2.2 评估专家 Agent(invoke_assessment_expert) +## 2.2 评估专家 Agent(invoke_assessment_expert)— 内部辅助 **角色**:用户资格与适配度评估专家 -**何时调用**: -- 已收集到足够的用户信息(**至少**:年龄、国籍/户籍、最高学历、工作年限),可以进行初步评估时 -- 用户明确要求评估自己的资格时 -- 在信息收集阶段后,主动为用户提供评估建议时 -**重要规则**: -- 不要在信息严重不足时调用评估——给出不准确的评估比不给评估更糟糕 -- 如果只收集到部分信息,可以先说明还需要哪些信息,收集完再评估 -- 评估结果是咨询的关键转折点,要认真呈现 +**重要:正式付费评估请使用 run_professional_assessment 工具**,它会自动验证支付和信息完整性,并保存评估报告。 +invoke_assessment_expert 仅用于内部快速分析,结果不保存,不作为正式评估报告。 -**输入要求**: -- userInfo:已收集到的所有用户信息(JSON 格式) -- targetCategories:如果用户已表达对特定类别的兴趣,指定目标类别 -- conversationContext:最近 2-3 轮对话的摘要,帮助评估专家理解上下文 +**调用时机**(仅限内部参考): +- 已收集到部分信息,需要初步判断方向(非正式评估) +- 协调器内部需要快速了解用户匹配度,辅助对话策略 + +**⚠️ 禁止使用 invoke_assessment_expert 作为正式评估工具。所有面向用户的付费评估必须使用 run_professional_assessment。** **输出处理**: - 评估专家会返回各类别的评估结果(分数、信心度、亮点、隐忧)和整体推荐 @@ -486,6 +481,28 @@ ${companyName} 是${companyDescription}。 - 时间预估要合理,基于政策专家提供的审批周期数据 - 同标题时间线会自动更新 +## 3.5 专业评估工具 + +### run_professional_assessment +- **用途**:执行完整的专业移民资格评估(付费服务,99元) +- **与 invoke_assessment_expert 的区别**: + - run_professional_assessment:**正式评估**,代码级验证付费,自动保存报告,有状态返回 + - invoke_assessment_expert:**内部辅助**,不验证付费,不保存,仅供内部参考 +- **适用场景**:用户确认要做专业评估,且已表明愿意付费时 +- **输入要求**: + - userInfo:必须包含 age、nationality、education_level、work_experience_years + - targetCategories:用户感兴趣的类别(可选) + - conversationContext:最近对话摘要(可选) +- **返回的 status 值及处理方式**: + - **payment_required**:用户未付费 → 说明费用,用户确认后调用 generate_payment + - **info_incomplete**:信息不完整 → 根据 missingFieldLabels 向用户询问缺失信息 + - **already_assessed**:30天内已评估 → 展示已有报告,告知用户 + - **completed**:评估成功 → 呈现评估结果,然后自动调用 manage_checklist + create_timeline + - **assessment_error**:服务异常 → 向用户道歉并建议稍后重试 +- **⚠️ 重要**: + - 所有面向用户的正式评估**必须**用此工具,不要用 invoke_assessment_expert 绕过付费验证 + - 评估完成后,**必须自动**为用户创建材料准备清单和申请时间线 + --- # ═══════════════════════════════════════════════════════════════ @@ -598,17 +615,54 @@ ${companyName} 是${companyDescription}。 1. **确认用户已付费**:评估服务收费99元人民币(每用户一次性收取)。如果用户尚未付费,简洁说明费用和评估内容: - "专业评估需要99元(一次性,后续咨询不再收费)。评估会帮您分析各个类别的适配度、给出路径推荐和注意事项。确认后我帮您生成支付链接。" - 用户确认后调用 generate_payment 生成支付链接 - - **在用户完成支付前,不要调用 invoke_assessment_expert** - 如果用户犹豫,不要追问,说"没关系,您可以先继续了解,随时可以做评估",然后正常对话 -2. 调用 invoke_memory_manager (summarize_profile) 生成用户画像 -3. 确认已收集信息的完整性,如有关键缺失,先补充 -4. 告知用户即将进行评估:"您的评估费已确认,根据您分享的信息,我现在为您做专业的移民资格评估。" +2. 确认已收集信息的完整性:age、nationality、education_level、work_experience_years 是必须的 +3. 告知用户即将进行评估:"根据您分享的信息,我现在为您做专业的移民资格评估。" -**调用评估专家**: -- 将完整的 userInfo 传给 invoke_assessment_expert +**调用正式评估工具**: +- **必须使用 run_professional_assessment**(不要使用 invoke_assessment_expert) +- 将完整的 userInfo 传入,确保至少包含 age、nationality、education_level、work_experience_years - 如果用户表达了特定类别偏好,在 targetCategories 中指定 - 在 conversationContext 中提供最近 2-3 轮的对话摘要 +**处理返回结果**: + +run_professional_assessment 会返回带 status 字段的结果,按以下方式处理: + +1. **status: 'payment_required'** → 用户尚未付费 + - 告知用户需要先完成支付:"专业评估需要支付99元的评估费用。" + - 如果 hasPendingOrder 为 true,提醒用户已有未支付订单 + - 用户确认后调用 generate_payment 生成支付链接 + - **不要自行调用 invoke_assessment_expert 绕过付费** + +2. **status: 'info_incomplete'** → 信息不完整 + - 根据 missingFieldLabels 列出缺失信息 + - 自然地向用户询问这些信息 + - 收集完成后重新调用 run_professional_assessment + +3. **status: 'already_assessed'** → 已有有效评估(30天内) + - 告知用户已有评估报告 + - 展示 existingReport 中的结果 + - 如果用户有新信息想更新,告知30天后可重新评估 + +4. **status: 'completed'** → 评估成功完成 + - 按下方「评估结果的呈现」框架呈现结果 + - 评估结果已自动保存,用户下次咨询可以调出 + - **评估完成后自动执行后续步骤**(见下方) + +5. **status: 'assessment_error' / 'payment_service_unavailable'** → 服务异常 + - 向用户道歉并建议稍后重试 + - 不要尝试用 invoke_assessment_expert 替代 + +**评估完成后的自动后续**: + +当 run_professional_assessment 返回 status: 'completed' 后,你应该: +1. 先向用户呈现评估结果(按下方框架) +2. 在呈现结果的同时或之后,自动调用以下工具为用户创建实用资料: + a. **manage_checklist** — 根据推荐的首选类别创建「材料准备清单」 + b. **create_timeline** — 根据推荐路径创建「申请时间规划」 +3. 在回复中告知用户:"我已经为您生成了材料准备清单和申请时间规划,供您参考。" + **评估结果的呈现**: 评估是咨询的关键节点,呈现方式至关重要。 @@ -824,12 +878,12 @@ ${companyName} 是${companyDescription}。 **可以并行调用的场景**: - invoke_policy_expert + invoke_memory_manager(获取政策信息的同时保存用户信息) -- invoke_assessment_expert + invoke_policy_expert(评估的同时获取推荐类别的详细政策) +- run_professional_assessment + invoke_policy_expert(评估的同时获取推荐类别的详细政策) - invoke_case_analyst + invoke_policy_expert(获取案例的同时获取对应政策) - save_user_memory + search_knowledge(保存信息的同时搜索知识) **不应该并行调用的场景**: -- invoke_assessment_expert 需要依赖 invoke_memory_manager (summarize_profile) 的输出时 +- run_professional_assessment 需要依赖 invoke_memory_manager (summarize_profile) 的输出时 - 不要同时调用 3 个以上的 Agent(增加延迟和成本,影响用户体验) **效率优化**: @@ -1143,7 +1197,7 @@ ${categoriesList} - 无论是初步评估还是深度评估,都需要付费。没有免费评估。 - 在线咨询问答是免费的(回答政策问题、解释类别区别等),但一旦涉及针对用户个人情况的评估分析,就需要付费。 - 当用户要求评估时,应先说明收费:如"针对您个人情况的评估需要支付99元的评估费用,这是一次性的,后续可以无限次咨询。确认后我帮您生成支付链接。" -- 用户付费后才能调用 invoke_assessment_expert 进行评估。如果用户尚未付费,不要进行评估。 +- 用户付费后才能调用 run_professional_assessment 进行评估。如果用户尚未付费,不要进行评估。run_professional_assessment 会自动验证支付状态。 - **绝对禁止说"免费评估"、"初步评估不收费"等话**。 我们的服务涵盖: @@ -1338,11 +1392,12 @@ ${categoriesList} 对话经过多轮后,已收集到:年龄35,清华大学硕士,计算机专业,8年工作经验,互联网行业,年收入80万人民币。 推荐处理: -1. 调用 invoke_memory_manager (summarize_profile) 生成画像 -2. 并行调用 invoke_assessment_expert + invoke_policy_expert(获取评估 + 最推荐类别的政策) -3. 清晰地呈现评估结果 -4. 推荐最适合的1-2条路径 -5. 引导用户下一步 +1. 确认用户已付费(如未付费,先引导付费) +2. 调用 run_professional_assessment(工具自动验证付费和信息完整性) +3. 并行调用 invoke_policy_expert 获取推荐类别的详细政策 +4. 清晰地呈现评估结果 +5. 自动调用 manage_checklist + create_timeline 创建后续资料 +6. 引导用户下一步 ## 11.5 场景:用户表达购买意向 diff --git a/packages/services/conversation-service/src/infrastructure/agents/tools/coordinator-tools.ts b/packages/services/conversation-service/src/infrastructure/agents/tools/coordinator-tools.ts index 286c04b..9b5662a 100644 --- a/packages/services/conversation-service/src/infrastructure/agents/tools/coordinator-tools.ts +++ b/packages/services/conversation-service/src/infrastructure/agents/tools/coordinator-tools.ts @@ -431,7 +431,7 @@ export const DIRECT_TOOLS: ToolDefinition[] = [ properties: { artifactType: { type: 'string', - enum: ['document', 'checklist', 'timeline'], + enum: ['document', 'checklist', 'timeline', 'assessment_report'], description: '工件类型(可选,不传则查询全部)', }, }, @@ -519,6 +519,36 @@ export const DIRECT_TOOLS: ToolDefinition[] = [ }, isConcurrencySafe: true, }, + // ========== 专业评估工具 ========== + { + name: 'run_professional_assessment', + description: + '执行专业移民资格评估(付费服务)。自动验证支付状态和信息完整性。' + + '用户已付费且基本信息齐备时使用。评估结果自动保存。' + + '如果用户未付费或信息不完整,工具会返回相应状态码。', + input_schema: { + type: 'object', + properties: { + userInfo: { + type: 'object', + description: + '用户信息键值对,必须包含 age、nationality、education_level、work_experience_years。' + + '如 {"age": "35", "nationality": "中国大陆", "education_level": "硕士", "work_experience_years": "10"}', + }, + targetCategories: { + type: 'array', + items: { type: 'string' }, + description: '重点评估的类别列表(可选,不传则评估所有6个类别)', + }, + conversationContext: { + type: 'string', + description: '最近几轮对话的简要摘要,帮助评估专家理解背景', + }, + }, + required: ['userInfo'], + }, + isConcurrencySafe: false, // 调用评估专家 + 写操作 + }, ]; // ============================================================ diff --git a/packages/services/conversation-service/src/infrastructure/agents/tools/tool-execution-queue.ts b/packages/services/conversation-service/src/infrastructure/agents/tools/tool-execution-queue.ts index 47afee6..797d9bb 100644 --- a/packages/services/conversation-service/src/infrastructure/agents/tools/tool-execution-queue.ts +++ b/packages/services/conversation-service/src/infrastructure/agents/tools/tool-execution-queue.ts @@ -277,4 +277,5 @@ export const TOOL_CONCURRENCY_MAP: Record = { generate_payment: false, // 创建支付订单 collect_assessment_info: false, // 写操作 cancel_order: false, // 取消订单 + run_professional_assessment: false, // 调用评估专家 + 写操作 }; diff --git a/packages/services/conversation-service/src/infrastructure/claude/tools/immigration-tools.service.ts b/packages/services/conversation-service/src/infrastructure/claude/tools/immigration-tools.service.ts index ee58909..97e0fb7 100644 --- a/packages/services/conversation-service/src/infrastructure/claude/tools/immigration-tools.service.ts +++ b/packages/services/conversation-service/src/infrastructure/claude/tools/immigration-tools.service.ts @@ -6,6 +6,7 @@ import { TenantContextService } from '@iconsulting/shared'; import { ConversationContext } from '../claude-agent.service'; import { KnowledgeClientService } from '../../knowledge/knowledge-client.service'; import { PaymentClientService } from '../../payment/payment-client.service'; +import { AssessmentExpertService } from '../../agents/specialists/assessment-expert.service'; import { ConversationORM } from '../../database/postgres/entities/conversation.orm'; import { UserArtifactORM } from '../../database/postgres/entities/user-artifact.orm'; @@ -30,6 +31,7 @@ export class ImmigrationToolsService { private artifactRepo: Repository, private tenantContext: TenantContextService, @Optional() private paymentClient?: PaymentClientService, + @Optional() private assessmentExpert?: AssessmentExpertService, ) {} /** @@ -366,6 +368,9 @@ export class ImmigrationToolsService { case 'query_user_artifacts': return this.queryUserArtifacts(input, context); + case 'run_professional_assessment': + return this.runProfessionalAssessment(input, context); + default: return { error: `Unknown tool: ${toolName}` }; } @@ -1415,4 +1420,197 @@ export class ImmigrationToolsService { : '暂无已保存的工件', }; } + + /** + * Run professional assessment — 完整的付费评估管线 + * + * Pipeline: + * 1. Check existing assessment (avoid duplicate within 30 days) + * 2. Validate payment (ASSESSMENT order with PAID/COMPLETED status) + * 3. Validate info completeness (age, nationality, education_level, work_experience_years) + * 4. Call AssessmentExpertService.executeAssessment() + * 5. Persist result as UserArtifact (artifactType: 'assessment_report') + */ + private async runProfessionalAssessment( + input: Record, + context: ConversationContext, + ): Promise { + const { userInfo, targetCategories, conversationContext } = input as { + userInfo: Record; + targetCategories?: string[]; + conversationContext?: string; + }; + + const tenantId = this.tenantContext.getCurrentTenantId() || ''; + const userId = context.userId; + + console.log(`[Tool:run_professional_assessment] User ${userId} - Starting assessment pipeline`); + + // ── Step 1: Check existing assessment ── + const existingReport = await this.artifactRepo.findOne({ + where: { userId, tenantId, artifactType: 'assessment_report' }, + order: { updatedAt: 'DESC' }, + }); + + if (existingReport) { + const daysSinceReport = (Date.now() - existingReport.updatedAt.getTime()) / (1000 * 60 * 60 * 24); + + if (daysSinceReport < 30) { + let parsedContent: unknown = null; + try { parsedContent = JSON.parse(existingReport.content); } catch { /* leave null */ } + + console.log(`[Tool:run_professional_assessment] Existing report found (${daysSinceReport.toFixed(1)} days old)`); + + return { + status: 'already_assessed', + message: `您在 ${Math.floor(daysSinceReport)} 天前已完成专业评估。如需重新评估,请30天后再试,或联系顾问手动更新。`, + existingReport: parsedContent, + artifactId: existingReport.id, + assessedAt: existingReport.updatedAt.toISOString(), + _ui_hint: '前端已渲染评估报告卡片', + }; + } + console.log(`[Tool:run_professional_assessment] Existing report is ${daysSinceReport.toFixed(1)} days old, allowing re-assessment`); + } + + // ── Step 2: Validate payment ── + if (!this.paymentClient) { + return { + status: 'payment_service_unavailable', + message: '支付服务暂时不可用,请稍后再试。', + }; + } + + const orders = await this.paymentClient.getUserOrders(userId); + const paidAssessmentOrder = orders.find( + (o) => o.serviceType === 'ASSESSMENT' && (o.status === 'PAID' || o.status === 'COMPLETED'), + ); + + if (!paidAssessmentOrder) { + const pendingOrder = orders.find( + (o) => o.serviceType === 'ASSESSMENT' && (o.status === 'CREATED' || o.status === 'PENDING_PAYMENT'), + ); + + return { + status: 'payment_required', + message: '专业评估服务需要支付99元评估费(一次性,终身有效)。请先完成支付后再进行评估。', + hasPendingOrder: !!pendingOrder, + pendingOrderId: pendingOrder?.id || null, + _ui_hint: '提示用户付费,可使用 generate_payment 工具生成支付链接', + }; + } + + // ── Step 3: Validate info completeness ── + const REQUIRED_FIELDS = ['age', 'nationality', 'education_level', 'work_experience_years']; + const FIELD_LABELS: Record = { + age: '年龄', + nationality: '国籍/户籍', + education_level: '最高学历', + work_experience_years: '工作年限', + }; + + const missingFields: string[] = []; + const collectedFields: string[] = []; + + for (const field of REQUIRED_FIELDS) { + if (userInfo[field] !== undefined && userInfo[field] !== null && userInfo[field] !== '') { + collectedFields.push(field); + } else { + missingFields.push(field); + } + } + + if (missingFields.length > 0) { + return { + status: 'info_incomplete', + message: `还需要以下信息才能进行准确评估:${missingFields.map(f => FIELD_LABELS[f] || f).join('、')}`, + missingFields, + missingFieldLabels: missingFields.map(f => FIELD_LABELS[f] || f), + collectedFields, + _ui_hint: '提示用户提供缺失信息', + }; + } + + // ── Step 4: Run assessment expert ── + if (!this.assessmentExpert) { + return { + status: 'assessment_error', + message: '评估服务暂时不可用,请稍后再试。', + }; + } + + console.log(`[Tool:run_professional_assessment] Payment verified, info complete. Running assessment...`); + + let assessmentResult: string; + try { + assessmentResult = await this.assessmentExpert.executeAssessment({ + userInfo, + targetCategories, + conversationContext, + }); + } catch (error) { + console.error('[Tool:run_professional_assessment] Assessment expert failed:', error); + return { + status: 'assessment_error', + message: '评估过程中出现错误,请稍后重试。如问题持续,请联系客服。', + }; + } + + // Parse the JSON result from assessment expert + let parsedResult: unknown; + try { + parsedResult = typeof assessmentResult === 'string' + ? JSON.parse(assessmentResult) + : assessmentResult; + } catch { + console.error('[Tool:run_professional_assessment] Failed to parse assessment result'); + parsedResult = null; + } + + // ── Step 5: Persist as artifact ── + const contentToSave = parsedResult ? JSON.stringify(parsedResult) : assessmentResult; + const title = '移民资格评估报告'; + + if (existingReport) { + // Update existing report (re-assessment after 30+ days) + existingReport.content = contentToSave; + existingReport.conversationId = context.conversationId; + await this.artifactRepo.save(existingReport); + + console.log(`[Tool:run_professional_assessment] Updated existing report: ${existingReport.id}`); + + return { + status: 'completed', + assessment: parsedResult || assessmentResult, + artifactId: existingReport.id, + isReassessment: true, + paidOrderId: paidAssessmentOrder.id, + assessedAt: new Date().toISOString(), + _ui_hint: '前端已渲染评估报告卡片。评估完成后建议自动创建材料清单和时间线。', + }; + } + + const artifact = this.artifactRepo.create({ + tenantId, + userId, + conversationId: context.conversationId, + artifactType: 'assessment_report', + title, + documentType: 'report', + content: contentToSave, + }); + const saved = await this.artifactRepo.save(artifact); + + console.log(`[Tool:run_professional_assessment] Assessment saved: ${saved.id}`); + + return { + status: 'completed', + assessment: parsedResult || assessmentResult, + artifactId: saved.id, + isReassessment: false, + paidOrderId: paidAssessmentOrder.id, + assessedAt: new Date().toISOString(), + _ui_hint: '前端已渲染评估报告卡片。评估完成后建议自动创建材料清单和时间线。', + }; + } } diff --git a/packages/web-client/src/features/chat/presentation/components/MessageBubble.tsx b/packages/web-client/src/features/chat/presentation/components/MessageBubble.tsx index 8368c3e..41419fb 100644 --- a/packages/web-client/src/features/chat/presentation/components/MessageBubble.tsx +++ b/packages/web-client/src/features/chat/presentation/components/MessageBubble.tsx @@ -398,6 +398,108 @@ function ToolCallResult({ } } + if (toolCall.name === 'run_professional_assessment') { + const result = toolCall.result as { + status?: string; + message?: string; + assessment?: unknown; + existingReport?: unknown; + artifactId?: string; + assessedAt?: string; + isReassessment?: boolean; + missingFields?: string[]; + missingFieldLabels?: string[]; + collectedFields?: string[]; + hasPendingOrder?: boolean; + pendingOrderId?: string; + }; + + if (result.status === 'completed' || result.status === 'already_assessed') { + const assessmentData = result.status === 'completed' + ? result.assessment + : result.existingReport; + const data = typeof assessmentData === 'string' + ? (() => { try { return JSON.parse(assessmentData); } catch { return null; } })() + : assessmentData; + + return ( +
+ {result.status === 'already_assessed' && ( +
+
+ + + 已有评估报告({result.assessedAt ? new Date(result.assessedAt).toLocaleDateString('zh-CN') : ''}) + +
+
+ )} + {result.isReassessment && ( +
+
+ + 评估报告已更新 +
+
+ )} + {data?.assessments && Array.isArray(data.assessments) ? ( + + ) : ( +
+

{result.message || '评估已完成'}

+
+ )} +
+ ); + } + + if (result.status === 'payment_required') { + return ( +
+
+ + 需要支付评估费用 +
+

{result.message}

+ {result.hasPendingOrder && ( +

+ 您有一个待支付的订单,请完成支付后重试。 +

+ )} +
+ ); + } + + if (result.status === 'info_incomplete') { + return ( +
+
+ + 信息待补充 +
+

{result.message}

+
+ {result.missingFieldLabels?.map((label, i) => ( + + {label} + + ))} +
+
+ ); + } + + // assessment_error / payment_service_unavailable / other + return ( +
+
+ + {result.message || '评估服务暂时不可用'} +
+
+ ); + } + if (toolCall.name === 'cancel_order') { const result = toolCall.result as { success?: boolean;