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 <noreply@anthropic.com>
This commit is contained in:
parent
95f36752c9
commit
e809740fdb
|
|
@ -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元人民币(每用户一次性收取)。如果用户尚未付费,简洁说明费用和评估内容:
|
1. **确认用户已付费**:评估服务收费99元人民币(每用户一次性收取)。如果用户尚未付费,简洁说明费用和评估内容:
|
||||||
- "专业评估需要99元(一次性,后续咨询不再收费)。评估会帮您分析各个类别的适配度、给出路径推荐和注意事项。确认后我帮您生成支付链接。"
|
- "专业评估需要99元(一次性,后续咨询不再收费)。评估会帮您分析各个类别的适配度、给出路径推荐和注意事项。确认后我帮您生成支付链接。"
|
||||||
- 用户确认后调用 generate_payment 生成支付链接
|
- 用户确认后调用 generate_payment 生成支付链接
|
||||||
- **在用户完成支付前,不要调用 invoke_assessment_expert**
|
|
||||||
- 如果用户犹豫,不要追问,说"没关系,您可以先继续了解,随时可以做评估",然后正常对话
|
- 如果用户犹豫,不要追问,说"没关系,您可以先继续了解,随时可以做评估",然后正常对话
|
||||||
2. 调用 invoke_memory_manager (summarize_profile) 生成用户画像
|
2. 确认已收集信息的完整性:age、nationality、education_level、work_experience_years 是必须的
|
||||||
3. 确认已收集信息的完整性,如有关键缺失,先补充
|
3. 告知用户即将进行评估:"根据您分享的信息,我现在为您做专业的移民资格评估。"
|
||||||
4. 告知用户即将进行评估:"您的评估费已确认,根据您分享的信息,我现在为您做专业的移民资格评估。"
|
|
||||||
|
|
||||||
**调用评估专家**:
|
**调用正式评估工具**:
|
||||||
- 将完整的 userInfo 传给 invoke_assessment_expert
|
- **必须使用 run_professional_assessment**(不要使用 invoke_assessment_expert)
|
||||||
|
- 将完整的 userInfo 传入,确保至少包含 age、nationality、education_level、work_experience_years
|
||||||
- 如果用户表达了特定类别偏好,在 targetCategories 中指定
|
- 如果用户表达了特定类别偏好,在 targetCategories 中指定
|
||||||
- 在 conversationContext 中提供最近 2-3 轮的对话摘要
|
- 在 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_policy_expert + invoke_memory_manager(获取政策信息的同时保存用户信息)
|
||||||
- invoke_assessment_expert + invoke_policy_expert(评估的同时获取推荐类别的详细政策)
|
- run_professional_assessment + invoke_policy_expert(评估的同时获取推荐类别的详细政策)
|
||||||
- invoke_case_analyst + invoke_policy_expert(获取案例的同时获取对应政策)
|
- invoke_case_analyst + invoke_policy_expert(获取案例的同时获取对应政策)
|
||||||
- save_user_memory + search_knowledge(保存信息的同时搜索知识)
|
- save_user_memory + search_knowledge(保存信息的同时搜索知识)
|
||||||
|
|
||||||
**不应该并行调用的场景**:
|
**不应该并行调用的场景**:
|
||||||
- invoke_assessment_expert 需要依赖 invoke_memory_manager (summarize_profile) 的输出时
|
- run_professional_assessment 需要依赖 invoke_memory_manager (summarize_profile) 的输出时
|
||||||
- 不要同时调用 3 个以上的 Agent(增加延迟和成本,影响用户体验)
|
- 不要同时调用 3 个以上的 Agent(增加延迟和成本,影响用户体验)
|
||||||
|
|
||||||
**效率优化**:
|
**效率优化**:
|
||||||
|
|
@ -1143,7 +1197,7 @@ ${categoriesList}
|
||||||
- 无论是初步评估还是深度评估,都需要付费。没有免费评估。
|
- 无论是初步评估还是深度评估,都需要付费。没有免费评估。
|
||||||
- 在线咨询问答是免费的(回答政策问题、解释类别区别等),但一旦涉及针对用户个人情况的评估分析,就需要付费。
|
- 在线咨询问答是免费的(回答政策问题、解释类别区别等),但一旦涉及针对用户个人情况的评估分析,就需要付费。
|
||||||
- 当用户要求评估时,应先说明收费:如"针对您个人情况的评估需要支付99元的评估费用,这是一次性的,后续可以无限次咨询。确认后我帮您生成支付链接。"
|
- 当用户要求评估时,应先说明收费:如"针对您个人情况的评估需要支付99元的评估费用,这是一次性的,后续可以无限次咨询。确认后我帮您生成支付链接。"
|
||||||
- 用户付费后才能调用 invoke_assessment_expert 进行评估。如果用户尚未付费,不要进行评估。
|
- 用户付费后才能调用 run_professional_assessment 进行评估。如果用户尚未付费,不要进行评估。run_professional_assessment 会自动验证支付状态。
|
||||||
- **绝对禁止说"免费评估"、"初步评估不收费"等话**。
|
- **绝对禁止说"免费评估"、"初步评估不收费"等话**。
|
||||||
|
|
||||||
我们的服务涵盖:
|
我们的服务涵盖:
|
||||||
|
|
@ -1338,11 +1392,12 @@ ${categoriesList}
|
||||||
对话经过多轮后,已收集到:年龄35,清华大学硕士,计算机专业,8年工作经验,互联网行业,年收入80万人民币。
|
对话经过多轮后,已收集到:年龄35,清华大学硕士,计算机专业,8年工作经验,互联网行业,年收入80万人民币。
|
||||||
|
|
||||||
推荐处理:
|
推荐处理:
|
||||||
1. 调用 invoke_memory_manager (summarize_profile) 生成画像
|
1. 确认用户已付费(如未付费,先引导付费)
|
||||||
2. 并行调用 invoke_assessment_expert + invoke_policy_expert(获取评估 + 最推荐类别的政策)
|
2. 调用 run_professional_assessment(工具自动验证付费和信息完整性)
|
||||||
3. 清晰地呈现评估结果
|
3. 并行调用 invoke_policy_expert 获取推荐类别的详细政策
|
||||||
4. 推荐最适合的1-2条路径
|
4. 清晰地呈现评估结果
|
||||||
5. 引导用户下一步
|
5. 自动调用 manage_checklist + create_timeline 创建后续资料
|
||||||
|
6. 引导用户下一步
|
||||||
|
|
||||||
## 11.5 场景:用户表达购买意向
|
## 11.5 场景:用户表达购买意向
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -431,7 +431,7 @@ export const DIRECT_TOOLS: ToolDefinition[] = [
|
||||||
properties: {
|
properties: {
|
||||||
artifactType: {
|
artifactType: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['document', 'checklist', 'timeline'],
|
enum: ['document', 'checklist', 'timeline', 'assessment_report'],
|
||||||
description: '工件类型(可选,不传则查询全部)',
|
description: '工件类型(可选,不传则查询全部)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -519,6 +519,36 @@ export const DIRECT_TOOLS: ToolDefinition[] = [
|
||||||
},
|
},
|
||||||
isConcurrencySafe: true,
|
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, // 调用评估专家 + 写操作
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -277,4 +277,5 @@ export const TOOL_CONCURRENCY_MAP: Record<string, boolean> = {
|
||||||
generate_payment: false, // 创建支付订单
|
generate_payment: false, // 创建支付订单
|
||||||
collect_assessment_info: false, // 写操作
|
collect_assessment_info: false, // 写操作
|
||||||
cancel_order: false, // 取消订单
|
cancel_order: false, // 取消订单
|
||||||
|
run_professional_assessment: false, // 调用评估专家 + 写操作
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { TenantContextService } from '@iconsulting/shared';
|
||||||
import { ConversationContext } from '../claude-agent.service';
|
import { ConversationContext } from '../claude-agent.service';
|
||||||
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
|
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
|
||||||
import { PaymentClientService } from '../../payment/payment-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 { ConversationORM } from '../../database/postgres/entities/conversation.orm';
|
||||||
import { UserArtifactORM } from '../../database/postgres/entities/user-artifact.orm';
|
import { UserArtifactORM } from '../../database/postgres/entities/user-artifact.orm';
|
||||||
|
|
||||||
|
|
@ -30,6 +31,7 @@ export class ImmigrationToolsService {
|
||||||
private artifactRepo: Repository<UserArtifactORM>,
|
private artifactRepo: Repository<UserArtifactORM>,
|
||||||
private tenantContext: TenantContextService,
|
private tenantContext: TenantContextService,
|
||||||
@Optional() private paymentClient?: PaymentClientService,
|
@Optional() private paymentClient?: PaymentClientService,
|
||||||
|
@Optional() private assessmentExpert?: AssessmentExpertService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -366,6 +368,9 @@ export class ImmigrationToolsService {
|
||||||
case 'query_user_artifacts':
|
case 'query_user_artifacts':
|
||||||
return this.queryUserArtifacts(input, context);
|
return this.queryUserArtifacts(input, context);
|
||||||
|
|
||||||
|
case 'run_professional_assessment':
|
||||||
|
return this.runProfessionalAssessment(input, context);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { error: `Unknown tool: ${toolName}` };
|
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<string, unknown>,
|
||||||
|
context: ConversationContext,
|
||||||
|
): Promise<unknown> {
|
||||||
|
const { userInfo, targetCategories, conversationContext } = input as {
|
||||||
|
userInfo: Record<string, unknown>;
|
||||||
|
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<string, string> = {
|
||||||
|
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: '前端已渲染评估报告卡片。评估完成后建议自动创建材料清单和时间线。',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="mt-3">
|
||||||
|
{result.status === 'already_assessed' && (
|
||||||
|
<div className="mb-2 px-3 py-2 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4 text-blue-500" />
|
||||||
|
<span className="text-xs text-blue-700">
|
||||||
|
已有评估报告({result.assessedAt ? new Date(result.assessedAt).toLocaleDateString('zh-CN') : ''})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result.isReassessment && (
|
||||||
|
<div className="mb-2 px-3 py-2 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||||
|
<span className="text-xs text-green-700">评估报告已更新</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data?.assessments && Array.isArray(data.assessments) ? (
|
||||||
|
<AssessmentResultCard data={data} />
|
||||||
|
) : (
|
||||||
|
<div className="p-3 bg-secondary-50 rounded-lg border border-secondary-200">
|
||||||
|
<p className="text-sm text-secondary-600">{result.message || '评估已完成'}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 'payment_required') {
|
||||||
|
return (
|
||||||
|
<div className="mt-3 p-3 bg-amber-50 rounded-lg border border-amber-200">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<ShoppingBag className="w-4 h-4 text-amber-600" />
|
||||||
|
<span className="text-sm font-medium text-amber-800">需要支付评估费用</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-amber-700">{result.message}</p>
|
||||||
|
{result.hasPendingOrder && (
|
||||||
|
<p className="mt-1 text-xs text-amber-600">
|
||||||
|
您有一个待支付的订单,请完成支付后重试。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 'info_incomplete') {
|
||||||
|
return (
|
||||||
|
<div className="mt-3 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<AlertCircle className="w-4 h-4 text-blue-500" />
|
||||||
|
<span className="text-sm font-medium text-blue-800">信息待补充</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-blue-700 mb-2">{result.message}</p>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{result.missingFieldLabels?.map((label, i) => (
|
||||||
|
<span key={i} className="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// assessment_error / payment_service_unavailable / other
|
||||||
|
return (
|
||||||
|
<div className="mt-3 p-3 bg-red-50 rounded-lg border border-red-200">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<XCircle className="w-4 h-4 text-red-500" />
|
||||||
|
<span className="text-sm text-red-600">{result.message || '评估服务暂时不可用'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (toolCall.name === 'cancel_order') {
|
if (toolCall.name === 'cancel_order') {
|
||||||
const result = toolCall.result as {
|
const result = toolCall.result as {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue