iconsulting/packages/services/evolution-service/src/evolution/evolution.service.ts

322 lines
9.9 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 { 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 };
}
}