iconsulting/packages/services/conversation-service/src/infrastructure/claude/tools/immigration-tools.service.ts

845 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ConversationContext } from '../claude-agent.service';
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
export interface Tool {
name: string;
description: string;
input_schema: {
type: string;
properties: Record<string, unknown>;
required?: string[];
};
}
@Injectable()
export class ImmigrationToolsService {
constructor(
private knowledgeClient: KnowledgeClientService,
private configService: ConfigService,
) {}
/**
* Get all available tools for the agent
*/
getTools(): Tool[] {
return [
{
name: 'search_knowledge',
description: '搜索香港移民相关知识库,获取最新的政策信息和常见问题解答',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索查询内容',
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '移民类别代码(可选)',
},
},
required: ['query'],
},
},
{
name: 'check_off_topic',
description: '检查用户的问题是否与香港移民相关,用于判断是否需要拒绝回答',
input_schema: {
type: 'object',
properties: {
question: {
type: 'string',
description: '用户的问题',
},
},
required: ['question'],
},
},
{
name: 'collect_assessment_info',
description: '收集用户的个人信息用于移民资格评估',
input_schema: {
type: 'object',
properties: {
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '用户感兴趣的移民类别',
},
age: {
type: 'number',
description: '用户年龄',
},
education: {
type: 'string',
description: '最高学历',
},
university: {
type: 'string',
description: '毕业院校',
},
yearsOfExperience: {
type: 'number',
description: '工作年限',
},
currentJobTitle: {
type: 'string',
description: '当前职位',
},
industry: {
type: 'string',
description: '所属行业',
},
annualIncome: {
type: 'number',
description: '年收入(人民币)',
},
hasHKEmployer: {
type: 'boolean',
description: '是否有香港雇主',
},
},
required: ['category'],
},
},
{
name: 'generate_payment',
description: '为付费评估服务生成支付二维码',
input_schema: {
type: 'object',
properties: {
serviceType: {
type: 'string',
enum: ['ASSESSMENT'],
description: '服务类型',
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '移民类别',
},
paymentMethod: {
type: 'string',
enum: ['ALIPAY', 'WECHAT', 'CREDIT_CARD'],
description: '支付方式',
},
},
required: ['serviceType', 'category', 'paymentMethod'],
},
},
{
name: 'save_user_memory',
description: '保存用户的重要信息到长期记忆,以便后续对话中记住用户情况',
input_schema: {
type: 'object',
properties: {
memoryType: {
type: 'string',
enum: ['FACT', 'PREFERENCE', 'INTENT'],
description: '记忆类型FACT-用户陈述的事实PREFERENCE-用户偏好INTENT-用户意图',
},
content: {
type: 'string',
description: '要记住的内容',
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '相关的移民类别(可选)',
},
},
required: ['memoryType', 'content'],
},
},
{
name: 'get_user_context',
description: '获取用户的历史背景信息和之前的对话记忆,帮助提供更个性化的建议',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '当前问题,用于检索相关记忆',
},
},
required: ['query'],
},
},
// ========== 实时工具 ==========
{
name: 'get_current_datetime',
description: '获取当前的日期和时间,用于回答与时间相关的问题',
input_schema: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: '时区,默认为 Asia/Hong_Kong',
},
format: {
type: 'string',
enum: ['full', 'date', 'time'],
description: '返回格式full-完整日期时间date-仅日期time-仅时间',
},
},
},
},
{
name: 'web_search',
description: '搜索互联网获取最新信息,用于查询移民政策变更、新闻动态等实时内容',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索关键词',
},
site: {
type: 'string',
description: '限定搜索网站,如 immd.gov.hk香港入境处',
},
language: {
type: 'string',
enum: ['zh-CN', 'zh-TW', 'en'],
description: '搜索语言,默认 zh-CN',
},
},
required: ['query'],
},
},
{
name: 'get_exchange_rate',
description: '查询实时货币汇率,用于投资移民金额换算等场景',
input_schema: {
type: 'object',
properties: {
from: {
type: 'string',
description: '源货币代码,如 CNY人民币、USD美元',
},
to: {
type: 'string',
description: '目标货币代码,如 HKD港币',
},
amount: {
type: 'number',
description: '要转换的金额(可选)',
},
},
required: ['from', 'to'],
},
},
{
name: 'fetch_immigration_news',
description: '获取香港移民相关的最新新闻和政策公告',
input_schema: {
type: 'object',
properties: {
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS', 'ALL'],
description: '移民类别ALL表示所有类别',
},
limit: {
type: 'number',
description: '返回条数默认5条',
},
},
},
},
];
}
/**
* Execute a tool and return the result
*/
async executeTool(
toolName: string,
input: Record<string, unknown>,
context: ConversationContext,
): Promise<unknown> {
console.log(`[Tool] Executing ${toolName} with input:`, JSON.stringify(input));
switch (toolName) {
case 'search_knowledge':
return this.searchKnowledge(input, context);
case 'check_off_topic':
return this.checkOffTopic(input);
case 'collect_assessment_info':
return this.collectAssessmentInfo(input, context);
case 'generate_payment':
return this.generatePayment(input, context);
case 'save_user_memory':
return this.saveUserMemory(input, context);
case 'get_user_context':
return this.getUserContext(input, context);
case 'get_current_datetime':
return this.getCurrentDatetime(input);
case 'web_search':
return this.webSearch(input);
case 'get_exchange_rate':
return this.getExchangeRate(input);
case 'fetch_immigration_news':
return this.fetchImmigrationNews(input);
default:
return { error: `Unknown tool: ${toolName}` };
}
}
/**
* Search knowledge base - 调用 knowledge-service RAG API
*/
private async searchKnowledge(
input: Record<string, unknown>,
context: ConversationContext,
): Promise<unknown> {
const { query, category } = input as { query: string; category?: string };
console.log(`[Tool:search_knowledge] Query: "${query}", Category: ${category || 'all'}`);
// 调用 knowledge-service RAG API
const result = await this.knowledgeClient.retrieveKnowledge({
query,
userId: context.userId,
category,
includeMemories: true,
includeExperiences: true,
});
if (result && result.content) {
return {
success: true,
content: result.content,
sources: result.sources,
userContext: result.userMemories,
relatedExperiences: result.systemExperiences,
message: `找到 ${result.sources?.length || 0} 条相关知识`,
};
}
// 降级:返回基础响应
return {
success: false,
content: null,
message: '知识库暂无相关内容,请基于内置知识回答',
};
}
/**
* Check if question is off-topic - 调用 knowledge-service API
*/
private async checkOffTopic(input: Record<string, unknown>): Promise<unknown> {
const { question } = input as { question: string };
console.log(`[Tool:check_off_topic] Checking: "${question}"`);
// 调用 knowledge-service 离题检测 API
const result = await this.knowledgeClient.checkOffTopic(question);
if (result.isOffTopic) {
return {
isOffTopic: true,
confidence: result.confidence,
suggestion: result.reason || '这个问题似乎与香港移民无关。作为移民咨询顾问,我专注于香港各类移民政策的咨询。请问您有香港移民相关的问题吗?',
};
}
return {
isOffTopic: false,
confidence: result.confidence,
suggestion: null,
};
}
/**
* Collect assessment info - 收集评估信息并保存到记忆
*/
private async collectAssessmentInfo(
input: Record<string, unknown>,
context: ConversationContext,
): Promise<unknown> {
const info = input as {
category: string;
age?: number;
education?: string;
university?: string;
yearsOfExperience?: number;
currentJobTitle?: string;
industry?: string;
annualIncome?: number;
hasHKEmployer?: boolean;
};
console.log(`[Tool:collect_assessment_info] User ${context.userId} - Category: ${info.category}`);
// 保存收集到的信息到用户记忆
const memoryContent = Object.entries(info)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => `${k}: ${v}`)
.join(', ');
if (memoryContent) {
await this.knowledgeClient.saveUserMemory({
userId: context.userId,
memoryType: 'FACT',
content: `用户评估信息 - ${memoryContent}`,
sourceConversationId: context.conversationId,
relatedCategory: info.category,
importance: 80,
});
}
return {
success: true,
collectedInfo: info,
message: '已记录您的信息。如需完整评估,请选择付费评估服务。',
nextStep: 'payment',
};
}
/**
* Generate payment QR code
*/
private async generatePayment(
input: Record<string, unknown>,
context: ConversationContext,
): Promise<unknown> {
const { serviceType, category, paymentMethod } = input as {
serviceType: string;
category: string;
paymentMethod: string;
};
console.log(
`[Tool:generate_payment] User ${context.userId} - ${serviceType} for ${category} via ${paymentMethod}`,
);
// 记录用户付费意向
await this.knowledgeClient.saveUserMemory({
userId: context.userId,
memoryType: 'INTENT',
content: `用户希望购买${category}类别的${serviceType}服务,选择${paymentMethod}支付`,
sourceConversationId: context.conversationId,
relatedCategory: category,
importance: 90,
});
// TODO: 调用 payment-service 生成实际支付
const priceMap: Record<string, number> = {
QMAS: 99,
GEP: 99,
IANG: 79,
TTPS: 99,
CIES: 199,
TECHTAS: 99,
};
const price = priceMap[category] || 99;
return {
success: true,
orderId: `ORD_${Date.now()}`,
amount: price,
currency: 'CNY',
paymentMethod,
qrCodeUrl: `https://placeholder-payment-qr.com/${paymentMethod.toLowerCase()}/${Date.now()}`,
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
message: `请扫描二维码支付 ¥${price} 完成${category}类别的移民资格评估服务`,
};
}
/**
* Save user memory - 调用 knowledge-service Memory API
*/
private async saveUserMemory(
input: Record<string, unknown>,
context: ConversationContext,
): Promise<unknown> {
const { memoryType, content, category } = input as {
memoryType: 'FACT' | 'PREFERENCE' | 'INTENT';
content: string;
category?: string;
};
console.log(
`[Tool:save_user_memory] User ${context.userId} - Type: ${memoryType}, Content: "${content}"`,
);
// 调用 knowledge-service Memory API
const memory = await this.knowledgeClient.saveUserMemory({
userId: context.userId,
memoryType,
content,
sourceConversationId: context.conversationId,
relatedCategory: category,
importance: memoryType === 'FACT' ? 70 : memoryType === 'INTENT' ? 80 : 60,
});
if (memory) {
return {
success: true,
memoryId: memory.id,
message: '已记住您的信息,下次对话时我会记得',
};
}
return {
success: false,
message: '记忆保存失败,但不影响当前对话',
};
}
/**
* Get user context - 获取用户历史背景
*/
private async getUserContext(
input: Record<string, unknown>,
context: ConversationContext,
): Promise<unknown> {
const { query } = input as { query: string };
console.log(`[Tool:get_user_context] User ${context.userId} - Query: "${query}"`);
// 并行获取相关记忆和重要记忆
const [relevantMemories, topMemories] = await Promise.all([
this.knowledgeClient.searchUserMemories({
userId: context.userId,
query,
limit: 3,
}),
this.knowledgeClient.getUserTopMemories(context.userId, 5),
]);
const allMemories = [...relevantMemories];
// 添加不重复的重要记忆
for (const mem of topMemories) {
if (!allMemories.find(m => m.id === mem.id)) {
allMemories.push(mem);
}
}
if (allMemories.length > 0) {
return {
success: true,
memories: allMemories.map(m => ({
type: m.memoryType,
content: m.content,
importance: m.importance,
})),
message: `找到 ${allMemories.length} 条用户背景信息`,
};
}
return {
success: true,
memories: [],
message: '这是新用户,暂无历史信息',
};
}
// ========== 实时工具实现 ==========
/**
* Get current datetime - 获取当前日期时间
*/
private getCurrentDatetime(input: Record<string, unknown>): unknown {
const { timezone = 'Asia/Hong_Kong', format = 'full' } = input as {
timezone?: string;
format?: 'full' | 'date' | 'time';
};
console.log(`[Tool:get_current_datetime] Timezone: ${timezone}, Format: ${format}`);
const now = new Date();
const options: Intl.DateTimeFormatOptions = {
timeZone: timezone,
};
let result: string;
switch (format) {
case 'date':
options.year = 'numeric';
options.month = 'long';
options.day = 'numeric';
options.weekday = 'long';
result = now.toLocaleDateString('zh-CN', options);
break;
case 'time':
options.hour = '2-digit';
options.minute = '2-digit';
options.second = '2-digit';
options.hour12 = false;
result = now.toLocaleTimeString('zh-CN', options);
break;
case 'full':
default:
options.year = 'numeric';
options.month = 'long';
options.day = 'numeric';
options.weekday = 'long';
options.hour = '2-digit';
options.minute = '2-digit';
options.second = '2-digit';
options.hour12 = false;
result = now.toLocaleString('zh-CN', options);
break;
}
return {
success: true,
datetime: result,
timezone,
timestamp: now.toISOString(),
message: `当前${timezone}时间: ${result}`,
};
}
/**
* Web search - 搜索互联网获取最新信息
* 使用 Google Custom Search API 或其他搜索服务
*/
private async webSearch(input: Record<string, unknown>): Promise<unknown> {
const { query, site, language = 'zh-CN' } = input as {
query: string;
site?: string;
language?: string;
};
console.log(`[Tool:web_search] Query: "${query}", Site: ${site || 'any'}, Language: ${language}`);
// 构建搜索查询
let searchQuery = query;
if (site) {
searchQuery = `site:${site} ${query}`;
}
// 尝试使用 Google Custom Search API
const googleApiKey = this.configService.get<string>('GOOGLE_SEARCH_API_KEY');
const googleCseId = this.configService.get<string>('GOOGLE_CSE_ID');
if (googleApiKey && googleCseId) {
try {
const url = new URL('https://www.googleapis.com/customsearch/v1');
url.searchParams.set('key', googleApiKey);
url.searchParams.set('cx', googleCseId);
url.searchParams.set('q', searchQuery);
url.searchParams.set('lr', `lang_${language.split('-')[0]}`);
url.searchParams.set('num', '5');
const response = await fetch(url.toString());
if (response.ok) {
const data = (await response.json()) as { items?: Array<{ title: string; link: string; snippet: string }> };
const results = (data.items || []).map((item: { title: string; link: string; snippet: string }) => ({
title: item.title,
url: item.link,
snippet: item.snippet,
}));
return {
success: true,
query: searchQuery,
results,
resultCount: results.length,
message: `找到 ${results.length} 条相关结果`,
};
}
} catch (error) {
console.error('[Tool:web_search] Google API error:', error);
}
}
// 降级:返回建议用户自行搜索的信息
return {
success: false,
query: searchQuery,
results: [],
message: '搜索服务暂时不可用。建议您访问以下官方网站获取最新信息:',
suggestedSites: [
{ name: '香港入境事务处', url: 'https://www.immd.gov.hk' },
{ name: '香港人才服务窗口', url: 'https://www.talentservicewindow.gov.hk' },
{ name: '投资推广署', url: 'https://www.investhk.gov.hk' },
],
};
}
/**
* Get exchange rate - 获取实时汇率
* 使用免费的汇率 API
*/
private async getExchangeRate(input: Record<string, unknown>): Promise<unknown> {
const { from, to, amount } = input as {
from: string;
to: string;
amount?: number;
};
console.log(`[Tool:get_exchange_rate] From: ${from}, To: ${to}, Amount: ${amount || 'N/A'}`);
// 使用 Exchange Rate API (免费层)
const apiKey = this.configService.get<string>('EXCHANGE_RATE_API_KEY');
const baseUrl = apiKey
? `https://v6.exchangerate-api.com/v6/${apiKey}/pair/${from}/${to}`
: `https://api.exchangerate-api.com/v4/latest/${from}`;
try {
const response = await fetch(baseUrl);
if (response.ok) {
const data = (await response.json()) as {
conversion_rate?: number;
rates?: Record<string, number>;
time_last_update_utc?: string;
};
let rate: number | undefined;
if (apiKey) {
// v6 API response
rate = data.conversion_rate;
} else {
// v4 API response (free, no key needed)
rate = data.rates?.[to];
}
if (rate) {
const convertedAmount = amount ? amount * rate : null;
return {
success: true,
from,
to,
rate,
amount: amount || null,
convertedAmount,
lastUpdate: data.time_last_update_utc || new Date().toISOString(),
message: amount
? `${amount} ${from} = ${convertedAmount?.toFixed(2)} ${to} (汇率: ${rate.toFixed(4)})`
: `1 ${from} = ${rate.toFixed(4)} ${to}`,
};
}
}
} catch (error) {
console.error('[Tool:get_exchange_rate] API error:', error);
}
// 降级:使用固定参考汇率
const fallbackRates: Record<string, Record<string, number>> = {
CNY: { HKD: 1.08, USD: 0.14 },
USD: { HKD: 7.78, CNY: 7.24 },
HKD: { CNY: 0.93, USD: 0.13 },
};
const fallbackRate = fallbackRates[from]?.[to];
if (fallbackRate) {
const convertedAmount = amount ? amount * fallbackRate : null;
return {
success: true,
from,
to,
rate: fallbackRate,
amount: amount || null,
convertedAmount,
isEstimate: true,
message: `参考汇率 (可能有偏差): 1 ${from}${fallbackRate} ${to}${amount ? `${amount} ${from}${convertedAmount?.toFixed(2)} ${to}` : ''}`,
};
}
return {
success: false,
message: `暂不支持 ${from}${to} 的汇率查询`,
};
}
/**
* Fetch immigration news - 获取移民相关新闻
* 从官方渠道或新闻源获取
*/
private async fetchImmigrationNews(input: Record<string, unknown>): Promise<unknown> {
const { category = 'ALL', limit = 5 } = input as {
category?: string;
limit?: number;
};
console.log(`[Tool:fetch_immigration_news] Category: ${category}, Limit: ${limit}`);
// 尝试从 knowledge-service 获取最新新闻(如果有此功能)
try {
const response = await fetch(
`${this.configService.get<string>('KNOWLEDGE_SERVICE_URL') || 'http://knowledge-service:3003'}/api/v1/news/immigration?category=${category}&limit=${limit}`,
);
if (response.ok) {
const data = (await response.json()) as { success?: boolean; data?: Array<unknown> };
if (data.success && data.data) {
return {
success: true,
category,
news: data.data,
message: `获取到 ${data.data.length}${category === 'ALL' ? '' : category + '相关'}移民新闻`,
};
}
}
} catch {
console.log('[Tool:fetch_immigration_news] Knowledge service news not available');
}
// 降级:返回静态参考信息和官方渠道
const categoryNews: Record<string, Array<{ title: string; summary: string; date: string; source: string }>> = {
QMAS: [
{
title: '优才计划持续接受申请',
summary: '香港优才计划无配额限制,全年接受申请。申请人需符合基本资格要求,并通过计分制评核。',
date: '2024-01',
source: '香港入境事务处',
},
],
TTPS: [
{
title: '高才通计划最新动态',
summary: '高端人才通行证计划持续运作A/B/C三类申请通道开放。',
date: '2024-01',
source: '香港入境事务处',
},
],
CIES: [
{
title: '新资本投资者入境计划',
summary: '投资门槛为3000万港币需投资于许可资产类别。',
date: '2024-03',
source: '香港入境事务处',
},
],
};
const newsItems = category === 'ALL'
? Object.values(categoryNews).flat()
: categoryNews[category] || [];
return {
success: true,
category,
news: newsItems.slice(0, limit),
isStatic: true,
message: newsItems.length > 0
? `以下是${category === 'ALL' ? '' : category + '类别'}的参考信息。如需最新资讯,请访问香港入境事务处官网。`
: '暂无相关新闻,建议访问香港入境事务处官网获取最新信息。',
officialSources: [
{ name: '香港入境事务处', url: 'https://www.immd.gov.hk/hks/services/visas/admission_talents.html' },
{ name: '香港人才服务窗口', url: 'https://www.talentservicewindow.gov.hk' },
],
};
}
}