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:
hailin 2026-02-08 17:01:56 -08:00
parent 95f36752c9
commit e809740fdb
5 changed files with 414 additions and 28 deletions

View File

@ -208,23 +208,18 @@ ${companyName} 是${companyDescription}。
-
-
## 2.2 Agentinvoke_assessment_expert
## 2.2 Agentinvoke_assessment_expert
****
****
- ****/
-
-
****
-
-
-
**使 run_professional_assessment **
invoke_assessment_expert
****
- userInfoJSON
- 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 agenationalityeducation_levelwork_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. agenationalityeducation_levelwork_experience_years
3. "根据您分享的信息,我现在为您做专业的移民资格评估。"
****
- userInfo invoke_assessment_expert
****
- **使 run_professional_assessment**使 invoke_assessment_expert
- userInfo agenationalityeducation_levelwork_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}
35880
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

View File

@ -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, // 调用评估专家 + 写操作
},
];
// ============================================================

View File

@ -277,4 +277,5 @@ export const TOOL_CONCURRENCY_MAP: Record<string, boolean> = {
generate_payment: false, // 创建支付订单
collect_assessment_info: false, // 写操作
cancel_order: false, // 取消订单
run_professional_assessment: false, // 调用评估专家 + 写操作
};

View File

@ -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<UserArtifactORM>,
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<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: '前端已渲染评估报告卡片。评估完成后建议自动创建材料清单和时间线。',
};
}
}

View File

@ -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') {
const result = toolCall.result as {
success?: boolean;