feat(agents): add query_user_profile tool for user info lookup
新增 query_user_profile 工具,让 AI agent 能回答用户关于自身信息的查询, 例如"这是我第几次咨询?"、"你记得我的信息吗?"、"我之前咨询过什么?" ## 问题背景 当用户问"这是我第几次跟你咨询?"时,AI 无法回答,因为没有任何工具 能查询用户的历史咨询数据。 ## 实现方案:双层设计 ### 第一层:被动上下文注入(Context Injector) - context-injector.service.ts 注入 ConversationORM repo + TenantContextService - buildConversationStatsBlock() 现在自动查询用户累计咨询次数 - 每次对话自动注入 `用户累计咨询次数: N 次(含本次对话)` - 简单问题("这是第几次?")AI 可直接从上下文回答,零工具调用 ### 第二层:主动工具调用(query_user_profile) 用户需要详细信息时,AI 调用此工具,返回完整档案: - 咨询统计:累计次数、首次/最近咨询时间、类别分布 - 最近对话:最近 10 个对话的标题、类别、阶段 - 用户画像:系统记忆中的事实(学历/年龄/职业)、偏好、意图 - 订单统计:总单数、已支付、待支付 ## 修改文件 - agents.module.ts: 添加 ConversationORM 到 TypeORM imports - coordinator-tools.ts: 新增 query_user_profile 工具定义(只读) - immigration-tools.service.ts: 注入 ConversationORM repo + TenantContextService, 实现 queryUserProfile() 方法(并行查询对话+记忆+订单) - coordinator-system-prompt.ts: 第3.3节添加工具文档和使用指引 - context-injector.service.ts: 注入 repo,conversation_stats 块添加累计咨询次数 ## 依赖关系 - 无循环依赖:直接使用 TypeORM Repository<ConversationORM>(数据访问层), 不依赖 ConversationService(避免 AgentsModule ↔ ConversationModule 循环) - TenantContextService 全局可用,确保多租户隔离 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
389f975e33
commit
43d4102e1f
|
|
@ -32,6 +32,7 @@ import { TokenUsageService } from '../claude/token-usage.service';
|
|||
import { ImmigrationToolsService } from '../claude/tools/immigration-tools.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TokenUsageORM } from '../database/postgres/entities/token-usage.orm';
|
||||
import { ConversationORM } from '../database/postgres/entities/conversation.orm';
|
||||
|
||||
// MCP Integration
|
||||
import { McpModule } from './mcp/mcp.module';
|
||||
|
|
@ -75,7 +76,7 @@ const AnthropicClientProvider = {
|
|||
imports: [
|
||||
ConfigModule,
|
||||
KnowledgeModule,
|
||||
TypeOrmModule.forFeature([TokenUsageORM, EvaluationRuleORM]),
|
||||
TypeOrmModule.forFeature([TokenUsageORM, EvaluationRuleORM, ConversationORM]),
|
||||
McpModule,
|
||||
PaymentModule,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@
|
|||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Not } from 'typeorm';
|
||||
import { TenantContextService } from '@iconsulting/shared';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import {
|
||||
ContextType,
|
||||
|
|
@ -27,6 +30,7 @@ import {
|
|||
} from '../types/context.types';
|
||||
import { ClaudeMessage } from '../types/agent.types';
|
||||
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
|
||||
import { ConversationORM } from '../../database/postgres/entities/conversation.orm';
|
||||
|
||||
@Injectable()
|
||||
export class ContextInjectorService {
|
||||
|
|
@ -36,6 +40,9 @@ export class ContextInjectorService {
|
|||
|
||||
constructor(
|
||||
private readonly knowledgeClient: KnowledgeClientService,
|
||||
@InjectRepository(ConversationORM)
|
||||
private readonly conversationRepo: Repository<ConversationORM>,
|
||||
private readonly tenantContext: TenantContextService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -163,12 +170,28 @@ export class ContextInjectorService {
|
|||
? Math.round((Date.now() - new Date(firstMsgTime).getTime()) / 60000)
|
||||
: 0;
|
||||
|
||||
// 查询用户累计咨询次数
|
||||
let totalConsultations = 0;
|
||||
try {
|
||||
const tenantId = this.tenantContext.getCurrentTenantId() || '';
|
||||
totalConsultations = await this.conversationRepo.count({
|
||||
where: {
|
||||
userId: ctx.userId,
|
||||
tenantId,
|
||||
status: Not('DELETED'),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to count consultations: ${err}`);
|
||||
}
|
||||
|
||||
const content = [
|
||||
`<conversation_stats>`,
|
||||
`对话统计: ${userMsgs} 条用户消息, ${assistantMsgs} 条助手消息, 持续 ${durationMin} 分钟`,
|
||||
`本次对话: ${userMsgs} 条用户消息, ${assistantMsgs} 条助手消息, 持续 ${durationMin} 分钟`,
|
||||
`当前阶段: ${ctx.consultingState?.currentStage || '未知'}`,
|
||||
totalConsultations > 0 ? `用户累计咨询次数: ${totalConsultations} 次(含本次对话)` : '',
|
||||
`</conversation_stats>`,
|
||||
].join('\n');
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
return {
|
||||
contextType: ContextType.CONVERSATION_STATS,
|
||||
|
|
|
|||
|
|
@ -417,6 +417,21 @@ ${companyName} 是${companyDescription}。
|
|||
- 只在用户确认愿意付费后才生成支付链接,绝不主动推送
|
||||
- 评估费99元是固定价格,不要给折扣或说免费
|
||||
|
||||
### query_user_profile
|
||||
- **用途**:查询用户在系统中的完整信息档案
|
||||
- **适用场景**:
|
||||
- 用户问"这是我第几次咨询?"、"我来过几次?"
|
||||
- 用户问"你记得我的信息吗?"、"你知道我是谁吗?"
|
||||
- 用户问"我之前咨询过什么?"、"我上次问了什么?"
|
||||
- 任何涉及用户历史、个人信息回顾的问题
|
||||
- **返回内容**:
|
||||
- 累计咨询次数和日期
|
||||
- 历史对话主题和类别分布
|
||||
- 系统记住的用户事实(学历、年龄、职业等)
|
||||
- 用户偏好和意图记录
|
||||
- 订单统计
|
||||
- **使用方式**:调用后用自然语言总结用户档案,不要直接输出原始数据
|
||||
|
||||
---
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -408,6 +408,18 @@ export const DIRECT_TOOLS: ToolDefinition[] = [
|
|||
},
|
||||
isConcurrencySafe: false, // 写操作
|
||||
},
|
||||
{
|
||||
name: 'query_user_profile',
|
||||
description:
|
||||
'查询用户在系统中的完整信息档案。包括累计咨询次数、首次咨询时间、' +
|
||||
'最近咨询时间、历史对话主题摘要、已收集的个人信息、用户记忆等。' +
|
||||
'当用户问"这是我第几次咨询"、"你记得我的信息吗"、"我之前咨询过什么"等问题时使用。',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
isConcurrencySafe: true, // 只读
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { Injectable, Optional } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Not } from 'typeorm';
|
||||
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 { ConversationORM } from '../../database/postgres/entities/conversation.orm';
|
||||
|
||||
export interface Tool {
|
||||
name: string;
|
||||
|
|
@ -19,6 +23,9 @@ export class ImmigrationToolsService {
|
|||
constructor(
|
||||
private knowledgeClient: KnowledgeClientService,
|
||||
private configService: ConfigService,
|
||||
@InjectRepository(ConversationORM)
|
||||
private conversationRepo: Repository<ConversationORM>,
|
||||
private tenantContext: TenantContextService,
|
||||
@Optional() private paymentClient?: PaymentClientService,
|
||||
) {}
|
||||
|
||||
|
|
@ -341,6 +348,9 @@ export class ImmigrationToolsService {
|
|||
case 'fetch_immigration_news':
|
||||
return this.fetchImmigrationNews(input);
|
||||
|
||||
case 'query_user_profile':
|
||||
return this.queryUserProfile(context);
|
||||
|
||||
default:
|
||||
return { error: `Unknown tool: ${toolName}` };
|
||||
}
|
||||
|
|
@ -1048,4 +1058,95 @@ export class ImmigrationToolsService {
|
|||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Query user profile — 查询用户在系统中的完整信息档案
|
||||
* 包括咨询次数、对话主题、用户记忆等
|
||||
*/
|
||||
private async queryUserProfile(
|
||||
context: ConversationContext,
|
||||
): Promise<unknown> {
|
||||
console.log(`[Tool:query_user_profile] User: ${context.userId}`);
|
||||
|
||||
const tenantId = this.tenantContext.getCurrentTenantId() || '';
|
||||
|
||||
// 并行查询:对话历史 + 用户记忆 + 订单历史
|
||||
const [conversations, topMemories, orders] = await Promise.all([
|
||||
// 查询该用户的所有非删除对话
|
||||
this.conversationRepo.find({
|
||||
where: {
|
||||
userId: context.userId,
|
||||
tenantId,
|
||||
status: Not('DELETED'),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
select: ['id', 'title', 'status', 'category', 'messageCount', 'consultingStage', 'createdAt', 'updatedAt'],
|
||||
}),
|
||||
// 获取用户最重要的记忆
|
||||
this.knowledgeClient.getUserTopMemories(context.userId, 15).catch(() => []),
|
||||
// 获取订单历史
|
||||
this.paymentClient?.getUserOrders(context.userId).catch(() => []) ?? Promise.resolve([]),
|
||||
]);
|
||||
|
||||
// 统计信息
|
||||
const totalConsultations = conversations.length;
|
||||
const firstConsultation = conversations.length > 0
|
||||
? conversations[conversations.length - 1].createdAt
|
||||
: null;
|
||||
const lastConsultation = conversations.length > 0
|
||||
? conversations[0].createdAt
|
||||
: null;
|
||||
|
||||
// 对话主题摘要(最近 10 个)
|
||||
const recentConversations = conversations.slice(0, 10).map(c => ({
|
||||
title: c.title,
|
||||
category: c.category || '未分类',
|
||||
stage: c.consultingStage || '未知',
|
||||
messageCount: c.messageCount,
|
||||
date: c.createdAt,
|
||||
}));
|
||||
|
||||
// 分类统计
|
||||
const categoryStats: Record<string, number> = {};
|
||||
for (const c of conversations) {
|
||||
const cat = c.category || '未分类';
|
||||
categoryStats[cat] = (categoryStats[cat] || 0) + 1;
|
||||
}
|
||||
|
||||
// 用户记忆分类
|
||||
const facts = topMemories.filter(m => m.memoryType === 'FACT');
|
||||
const preferences = topMemories.filter(m => m.memoryType === 'PREFERENCE');
|
||||
const intents = topMemories.filter(m => m.memoryType === 'INTENT');
|
||||
|
||||
// 订单统计
|
||||
const orderStats = {
|
||||
total: orders.length,
|
||||
paid: orders.filter((o: any) => o.status === 'PAID' || o.status === 'COMPLETED').length,
|
||||
pending: orders.filter((o: any) => o.status === 'CREATED' || o.status === 'PENDING_PAYMENT').length,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
profile: {
|
||||
// 咨询统计
|
||||
totalConsultations,
|
||||
currentConsultationNumber: totalConsultations, // "这是第 N 次咨询"
|
||||
firstConsultationDate: firstConsultation,
|
||||
lastConsultationDate: lastConsultation,
|
||||
categoryDistribution: categoryStats,
|
||||
|
||||
// 最近对话
|
||||
recentConversations,
|
||||
|
||||
// 用户画像(来自记忆系统)
|
||||
userFacts: facts.map(m => m.content),
|
||||
userPreferences: preferences.map(m => m.content),
|
||||
userIntents: intents.map(m => m.content),
|
||||
|
||||
// 订单统计
|
||||
orderStats,
|
||||
},
|
||||
message: `用户已累计咨询 ${totalConsultations} 次,系统记录了 ${topMemories.length} 条用户信息`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue