322 lines
9.9 KiB
TypeScript
322 lines
9.9 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { Repository, MoreThan, LessThan } from 'typeorm';
|
||
import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service';
|
||
import { ConversationORM } from '../infrastructure/database/entities/conversation.orm';
|
||
import { MessageORM } from '../infrastructure/database/entities/message.orm';
|
||
import { SystemExperienceORM } from '../infrastructure/database/entities/system-experience.orm';
|
||
import { v4 as uuidv4 } from 'uuid';
|
||
|
||
/**
|
||
* 进化任务结果
|
||
*/
|
||
export interface EvolutionTaskResult {
|
||
taskId: string;
|
||
status: 'success' | 'partial' | 'failed';
|
||
conversationsAnalyzed: number;
|
||
experiencesExtracted: number;
|
||
knowledgeGapsFound: number;
|
||
errors: string[];
|
||
}
|
||
|
||
/**
|
||
* 进化服务
|
||
* 负责系统的自我学习和进化
|
||
*/
|
||
@Injectable()
|
||
export class EvolutionService {
|
||
constructor(
|
||
@InjectRepository(ConversationORM)
|
||
private conversationRepo: Repository<ConversationORM>,
|
||
@InjectRepository(MessageORM)
|
||
private messageRepo: Repository<MessageORM>,
|
||
@InjectRepository(SystemExperienceORM)
|
||
private experienceRepo: Repository<SystemExperienceORM>,
|
||
private experienceExtractor: ExperienceExtractorService,
|
||
) {}
|
||
|
||
/**
|
||
* 执行进化任务 - 分析最近的对话并提取经验
|
||
*/
|
||
async runEvolutionTask(options?: {
|
||
hoursBack?: number;
|
||
limit?: number;
|
||
minMessageCount?: number;
|
||
}): Promise<EvolutionTaskResult> {
|
||
const taskId = uuidv4();
|
||
const hoursBack = options?.hoursBack || 24;
|
||
const limit = options?.limit || 50;
|
||
const minMessageCount = options?.minMessageCount || 4;
|
||
|
||
const result: EvolutionTaskResult = {
|
||
taskId,
|
||
status: 'success',
|
||
conversationsAnalyzed: 0,
|
||
experiencesExtracted: 0,
|
||
knowledgeGapsFound: 0,
|
||
errors: [],
|
||
};
|
||
|
||
console.log(`[Evolution] Starting task ${taskId}`);
|
||
|
||
try {
|
||
// 1. 获取待分析的对话
|
||
const cutoffTime = new Date();
|
||
cutoffTime.setHours(cutoffTime.getHours() - hoursBack);
|
||
|
||
const conversations = await this.conversationRepo.find({
|
||
where: {
|
||
status: 'ENDED',
|
||
createdAt: MoreThan(cutoffTime),
|
||
messageCount: MoreThan(minMessageCount),
|
||
},
|
||
order: { createdAt: 'DESC' },
|
||
take: limit,
|
||
});
|
||
|
||
console.log(`[Evolution] Found ${conversations.length} conversations to analyze`);
|
||
|
||
// 2. 分析每个对话
|
||
const allKnowledgeGaps: string[] = [];
|
||
|
||
for (const conversation of conversations) {
|
||
try {
|
||
// 获取对话消息
|
||
const messages = await this.messageRepo.find({
|
||
where: { conversationId: conversation.id },
|
||
order: { createdAt: 'ASC' },
|
||
});
|
||
|
||
// 分析对话
|
||
const analysis = await this.experienceExtractor.analyzeConversation({
|
||
conversationId: conversation.id,
|
||
messages: messages.map(m => ({
|
||
role: m.role as 'user' | 'assistant',
|
||
content: m.content,
|
||
})),
|
||
category: conversation.category,
|
||
hasConverted: conversation.hasConverted,
|
||
rating: conversation.rating,
|
||
});
|
||
|
||
// 保存提取的经验
|
||
for (const exp of analysis.experiences) {
|
||
await this.saveExperience({
|
||
experienceType: exp.type,
|
||
content: exp.content,
|
||
scenario: exp.scenario,
|
||
confidence: exp.confidence,
|
||
relatedCategory: exp.relatedCategory,
|
||
sourceConversationId: conversation.id,
|
||
});
|
||
result.experiencesExtracted++;
|
||
}
|
||
|
||
// 收集知识缺口
|
||
allKnowledgeGaps.push(...analysis.knowledgeGaps);
|
||
|
||
result.conversationsAnalyzed++;
|
||
} catch (error) {
|
||
result.errors.push(`Conversation ${conversation.id}: ${(error as Error).message}`);
|
||
}
|
||
}
|
||
|
||
// 3. 汇总知识缺口
|
||
const uniqueGaps = [...new Set(allKnowledgeGaps)];
|
||
result.knowledgeGapsFound = uniqueGaps.length;
|
||
|
||
// 可以在这里调用知识服务创建待处理的知识缺口任务
|
||
|
||
console.log(`[Evolution] Task ${taskId} completed:`, result);
|
||
|
||
if (result.errors.length > 0) {
|
||
result.status = 'partial';
|
||
}
|
||
|
||
return result;
|
||
} catch (error) {
|
||
console.error(`[Evolution] Task ${taskId} failed:`, error);
|
||
result.status = 'failed';
|
||
result.errors.push((error as Error).message);
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存经验(带去重逻辑)
|
||
*/
|
||
private async saveExperience(params: {
|
||
experienceType: string;
|
||
content: string;
|
||
scenario: string;
|
||
confidence: number;
|
||
relatedCategory?: string;
|
||
sourceConversationId: string;
|
||
}): Promise<void> {
|
||
// 查找相似经验
|
||
const existingExperiences = await this.experienceRepo.find({
|
||
where: {
|
||
experienceType: params.experienceType,
|
||
relatedCategory: params.relatedCategory,
|
||
},
|
||
});
|
||
|
||
// 简单的相似度检查(实际应该用向量相似度)
|
||
const similar = existingExperiences.find(
|
||
exp => this.simpleSimilarity(exp.content, params.content) > 0.8,
|
||
);
|
||
|
||
if (similar) {
|
||
// 合并到现有经验
|
||
if (!similar.sourceConversationIds.includes(params.sourceConversationId)) {
|
||
similar.sourceConversationIds.push(params.sourceConversationId);
|
||
similar.confidence = Math.min(100, similar.confidence + 5);
|
||
await this.experienceRepo.save(similar);
|
||
}
|
||
} else {
|
||
// 创建新经验
|
||
const newExperience = this.experienceRepo.create({
|
||
id: uuidv4(),
|
||
experienceType: params.experienceType,
|
||
content: params.content,
|
||
scenario: params.scenario,
|
||
confidence: params.confidence,
|
||
relatedCategory: params.relatedCategory,
|
||
sourceConversationIds: [params.sourceConversationId],
|
||
verificationStatus: 'PENDING',
|
||
isActive: false,
|
||
});
|
||
await this.experienceRepo.save(newExperience);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 简单的文本相似度计算
|
||
*/
|
||
private simpleSimilarity(a: string, b: string): number {
|
||
const aWords = new Set(a.toLowerCase().split(/\s+/));
|
||
const bWords = new Set(b.toLowerCase().split(/\s+/));
|
||
const intersection = [...aWords].filter(x => bWords.has(x)).length;
|
||
const union = new Set([...aWords, ...bWords]).size;
|
||
return intersection / union;
|
||
}
|
||
|
||
/**
|
||
* 获取进化统计信息
|
||
*/
|
||
async getEvolutionStatistics(): Promise<{
|
||
totalExperiences: number;
|
||
pendingExperiences: number;
|
||
approvedExperiences: number;
|
||
activeExperiences: number;
|
||
recentConversationsAnalyzed: number;
|
||
topExperienceTypes: Array<{ type: string; count: number }>;
|
||
}> {
|
||
const [total, pending, approved, active] = await Promise.all([
|
||
this.experienceRepo.count(),
|
||
this.experienceRepo.count({ where: { verificationStatus: 'PENDING' } }),
|
||
this.experienceRepo.count({ where: { verificationStatus: 'APPROVED' } }),
|
||
this.experienceRepo.count({ where: { isActive: true } }),
|
||
]);
|
||
|
||
// 获取最近分析的对话数(过去7天)
|
||
const weekAgo = new Date();
|
||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||
const recentConversations = await this.conversationRepo.count({
|
||
where: {
|
||
status: 'ENDED',
|
||
updatedAt: MoreThan(weekAgo),
|
||
},
|
||
});
|
||
|
||
// 获取经验类型分布
|
||
const typeDistribution = await this.experienceRepo
|
||
.createQueryBuilder('exp')
|
||
.select('exp.experienceType', 'type')
|
||
.addSelect('COUNT(*)', 'count')
|
||
.groupBy('exp.experienceType')
|
||
.orderBy('count', 'DESC')
|
||
.limit(5)
|
||
.getRawMany();
|
||
|
||
return {
|
||
totalExperiences: total,
|
||
pendingExperiences: pending,
|
||
approvedExperiences: approved,
|
||
activeExperiences: active,
|
||
recentConversationsAnalyzed: recentConversations,
|
||
topExperienceTypes: typeDistribution.map(t => ({
|
||
type: t.type,
|
||
count: parseInt(t.count),
|
||
})),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取系统健康报告
|
||
*/
|
||
async getSystemHealthReport(): Promise<{
|
||
overall: 'healthy' | 'warning' | 'critical';
|
||
metrics: Array<{
|
||
name: string;
|
||
value: number;
|
||
threshold: number;
|
||
status: 'good' | 'warning' | 'critical';
|
||
}>;
|
||
recommendations: string[];
|
||
}> {
|
||
const stats = await this.getEvolutionStatistics();
|
||
const metrics: Array<{
|
||
name: string;
|
||
value: number;
|
||
threshold: number;
|
||
status: 'good' | 'warning' | 'critical';
|
||
}> = [];
|
||
const recommendations: string[] = [];
|
||
|
||
// 检查待验证经验堆积
|
||
const pendingRatio = stats.pendingExperiences / Math.max(1, stats.totalExperiences);
|
||
metrics.push({
|
||
name: '待验证经验比例',
|
||
value: Math.round(pendingRatio * 100),
|
||
threshold: 50,
|
||
status: pendingRatio > 0.5 ? 'warning' : 'good',
|
||
});
|
||
if (pendingRatio > 0.5) {
|
||
recommendations.push('待验证经验过多,建议及时审核');
|
||
}
|
||
|
||
// 检查活跃经验数量
|
||
metrics.push({
|
||
name: '活跃经验数量',
|
||
value: stats.activeExperiences,
|
||
threshold: 10,
|
||
status: stats.activeExperiences < 10 ? 'warning' : 'good',
|
||
});
|
||
if (stats.activeExperiences < 10) {
|
||
recommendations.push('活跃经验较少,系统学习能力有限');
|
||
}
|
||
|
||
// 检查最近分析的对话
|
||
metrics.push({
|
||
name: '近7天分析对话数',
|
||
value: stats.recentConversationsAnalyzed,
|
||
threshold: 50,
|
||
status: stats.recentConversationsAnalyzed < 50 ? 'warning' : 'good',
|
||
});
|
||
|
||
// 计算总体健康状态
|
||
const criticalCount = metrics.filter(m => m.status === 'critical').length;
|
||
const warningCount = metrics.filter(m => m.status === 'warning').length;
|
||
|
||
let overall: 'healthy' | 'warning' | 'critical' = 'healthy';
|
||
if (criticalCount > 0) {
|
||
overall = 'critical';
|
||
} else if (warningCount > 1) {
|
||
overall = 'warning';
|
||
}
|
||
|
||
return { overall, metrics, recommendations };
|
||
}
|
||
}
|