506 lines
14 KiB
TypeScript
506 lines
14 KiB
TypeScript
import { prisma } from '../config/database.js';
|
||
import { LLMFactory } from '../adapters/LLMFactory.js';
|
||
import { Message, ModelType, StreamChunk } from '../adapters/types.js';
|
||
import { agentService } from './agentService.js';
|
||
import * as knowledgeBaseService from './knowledgeBaseService.js';
|
||
|
||
interface CreateConversationData {
|
||
userId: string;
|
||
projectId: string;
|
||
agentId: string;
|
||
title?: string;
|
||
}
|
||
|
||
interface SendMessageData {
|
||
conversationId: string;
|
||
content: string;
|
||
modelType: ModelType;
|
||
knowledgeBaseIds?: string[];
|
||
}
|
||
|
||
export class ConversationService {
|
||
/**
|
||
* 创建新对话
|
||
*/
|
||
async createConversation(data: CreateConversationData) {
|
||
const { userId, projectId, agentId, title } = data;
|
||
|
||
// 验证智能体是否存在
|
||
const agent = agentService.getAgentById(agentId);
|
||
if (!agent) {
|
||
throw new Error('智能体不存在');
|
||
}
|
||
|
||
// 验证项目是否存在
|
||
const project = await prisma.project.findFirst({
|
||
where: {
|
||
id: projectId,
|
||
userId: userId,
|
||
deletedAt: null,
|
||
},
|
||
});
|
||
|
||
if (!project) {
|
||
throw new Error('项目不存在或无权访问');
|
||
}
|
||
|
||
// 创建对话
|
||
const conversation = await prisma.conversation.create({
|
||
data: {
|
||
userId,
|
||
projectId,
|
||
agentId,
|
||
title: title || `与${agent.name}的对话`,
|
||
metadata: {
|
||
agentName: agent.name,
|
||
agentCategory: agent.category,
|
||
},
|
||
},
|
||
});
|
||
|
||
return conversation;
|
||
}
|
||
|
||
/**
|
||
* 获取对话列表
|
||
*/
|
||
async getConversations(userId: string, projectId?: string) {
|
||
const where: any = {
|
||
userId,
|
||
deletedAt: null,
|
||
};
|
||
|
||
if (projectId) {
|
||
where.projectId = projectId;
|
||
}
|
||
|
||
const conversations = await prisma.conversation.findMany({
|
||
where,
|
||
include: {
|
||
project: {
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
},
|
||
},
|
||
_count: {
|
||
select: {
|
||
messages: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: {
|
||
updatedAt: 'desc',
|
||
},
|
||
});
|
||
|
||
return conversations;
|
||
}
|
||
|
||
/**
|
||
* 获取对话详情(包含消息)
|
||
*/
|
||
async getConversationById(conversationId: string, userId: string) {
|
||
const conversation = await prisma.conversation.findFirst({
|
||
where: {
|
||
id: conversationId,
|
||
userId,
|
||
deletedAt: null,
|
||
},
|
||
include: {
|
||
project: {
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
background: true,
|
||
researchType: true,
|
||
},
|
||
},
|
||
messages: {
|
||
orderBy: {
|
||
createdAt: 'asc',
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!conversation) {
|
||
throw new Error('对话不存在或无权访问');
|
||
}
|
||
|
||
return conversation;
|
||
}
|
||
|
||
/**
|
||
* 组装上下文消息
|
||
*/
|
||
private async assembleContext(
|
||
conversationId: string,
|
||
agentId: string,
|
||
projectBackground: string,
|
||
userInput: string,
|
||
knowledgeBaseContext?: string
|
||
): Promise<Message[]> {
|
||
console.log('🔧 [assembleContext] 开始组装上下文', {
|
||
conversationId,
|
||
agentId,
|
||
hasKnowledgeBaseContext: !!knowledgeBaseContext,
|
||
knowledgeBaseContextLength: knowledgeBaseContext?.length || 0
|
||
});
|
||
|
||
// 获取系统Prompt
|
||
const systemPrompt = agentService.getSystemPrompt(agentId);
|
||
|
||
// 获取历史消息(最近100条,约50轮对话)
|
||
// DeepSeek-V3支持64K tokens,实际可容纳100-200轮对话
|
||
const historyMessages = await prisma.message.findMany({
|
||
where: {
|
||
conversationId,
|
||
},
|
||
orderBy: {
|
||
createdAt: 'desc',
|
||
},
|
||
take: 100,
|
||
});
|
||
|
||
// 反转顺序(最早的在前)
|
||
historyMessages.reverse();
|
||
|
||
// 判断是否是第一条消息
|
||
const isFirstMessage = historyMessages.length === 0;
|
||
console.log(`📜 [assembleContext] 历史消息数: ${historyMessages.length}, 是否首次: ${isFirstMessage}`);
|
||
|
||
// 渲染用户Prompt
|
||
let userPromptContent: string;
|
||
|
||
if (isFirstMessage) {
|
||
// 第一条消息:使用完整模板(包含项目背景)
|
||
userPromptContent = agentService.renderUserPrompt(agentId, {
|
||
projectBackground,
|
||
userInput,
|
||
knowledgeBaseContext,
|
||
});
|
||
console.log(`📝 [assembleContext] 首次消息,使用完整模板,长度: ${userPromptContent.length}`);
|
||
console.log(`📋 [assembleContext] userPromptContent完整内容:\n${userPromptContent}`);
|
||
console.log(`🔍 [assembleContext] 是否包含"参考文献": ${userPromptContent.includes('参考文献')}`);
|
||
console.log(`🔍 [assembleContext] 是否包含知识库内容: ${userPromptContent.includes('阿尔兹海默症')}`);
|
||
} else {
|
||
// 后续消息:只发送用户输入和知识库上下文(如果有)
|
||
if (knowledgeBaseContext) {
|
||
userPromptContent = `${userInput}\n\n## 参考文献(来自知识库)\n${knowledgeBaseContext}`;
|
||
console.log(`📝 [assembleContext] 后续消息+知识库,总长度: ${userPromptContent.length}`);
|
||
console.log(`📋 [assembleContext] userPromptContent预览:\n${userPromptContent.substring(0, 300)}...`);
|
||
} else {
|
||
userPromptContent = userInput;
|
||
console.log(`📝 [assembleContext] 后续消息,仅用户输入: ${userPromptContent}`);
|
||
}
|
||
}
|
||
|
||
// 组装消息数组
|
||
const messages: Message[] = [
|
||
{
|
||
role: 'system',
|
||
content: systemPrompt,
|
||
},
|
||
];
|
||
|
||
// 添加历史消息
|
||
for (const msg of historyMessages) {
|
||
messages.push({
|
||
role: msg.role as 'user' | 'assistant',
|
||
content: msg.content,
|
||
});
|
||
}
|
||
|
||
// 添加当前用户输入
|
||
messages.push({
|
||
role: 'user',
|
||
content: userPromptContent,
|
||
});
|
||
|
||
console.log(`✅ [assembleContext] 组装完成,消息总数: ${messages.length}`);
|
||
return messages;
|
||
}
|
||
|
||
/**
|
||
* 发送消息(非流式)
|
||
*/
|
||
async sendMessage(data: SendMessageData, userId: string) {
|
||
const { conversationId, content, modelType, knowledgeBaseIds } = data;
|
||
|
||
// 获取对话信息
|
||
const conversation = await this.getConversationById(conversationId, userId);
|
||
|
||
// 获取知识库上下文(如果有@知识库)
|
||
let knowledgeBaseContext = '';
|
||
if (knowledgeBaseIds && knowledgeBaseIds.length > 0) {
|
||
const knowledgeResults: string[] = [];
|
||
|
||
// 对每个知识库进行检索
|
||
for (const kbId of knowledgeBaseIds) {
|
||
try {
|
||
const searchResult = await knowledgeBaseService.searchKnowledgeBase(
|
||
userId,
|
||
kbId,
|
||
content,
|
||
3 // 每个知识库返回3个最相关的段落
|
||
);
|
||
|
||
// 格式化检索结果
|
||
if (searchResult.records && searchResult.records.length > 0) {
|
||
const kbInfo = await prisma.knowledgeBase.findUnique({
|
||
where: { id: kbId },
|
||
select: { name: true },
|
||
});
|
||
|
||
knowledgeResults.push(
|
||
`【知识库:${kbInfo?.name || '未命名'}】\n` +
|
||
searchResult.records
|
||
.map((record: any, index: number) => {
|
||
const score = (record.score * 100).toFixed(1);
|
||
return `${index + 1}. [相关度${score}%] ${record.segment.content}`;
|
||
})
|
||
.join('\n\n')
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error(`Failed to search knowledge base ${kbId}:`, error);
|
||
// 检索失败不阻止对话,继续处理
|
||
}
|
||
}
|
||
|
||
if (knowledgeResults.length > 0) {
|
||
knowledgeBaseContext = knowledgeResults.join('\n\n---\n\n');
|
||
}
|
||
}
|
||
|
||
// 组装上下文
|
||
const messages = await this.assembleContext(
|
||
conversationId,
|
||
conversation.agentId,
|
||
conversation.project?.background || '',
|
||
content,
|
||
knowledgeBaseContext
|
||
);
|
||
|
||
// 获取LLM适配器
|
||
const adapter = LLMFactory.getAdapter(modelType);
|
||
|
||
// 获取智能体配置的模型参数
|
||
const agent = agentService.getAgentById(conversation.agentId);
|
||
const modelConfig = agent?.models?.[modelType];
|
||
|
||
// 调用LLM
|
||
const response = await adapter.chat(messages, {
|
||
temperature: modelConfig?.temperature,
|
||
maxTokens: modelConfig?.maxTokens,
|
||
topP: modelConfig?.topP,
|
||
});
|
||
|
||
// 保存用户消息
|
||
const userMessage = await prisma.message.create({
|
||
data: {
|
||
conversationId,
|
||
role: 'user',
|
||
content,
|
||
metadata: {
|
||
knowledgeBaseIds,
|
||
},
|
||
},
|
||
});
|
||
|
||
// 保存助手回复
|
||
const assistantMessage = await prisma.message.create({
|
||
data: {
|
||
conversationId,
|
||
role: 'assistant',
|
||
content: response.content,
|
||
model: response.model,
|
||
tokens: response.usage?.totalTokens,
|
||
metadata: {
|
||
usage: response.usage,
|
||
finishReason: response.finishReason,
|
||
},
|
||
},
|
||
});
|
||
|
||
// 更新对话的最后更新时间
|
||
await prisma.conversation.update({
|
||
where: { id: conversationId },
|
||
data: { updatedAt: new Date() },
|
||
});
|
||
|
||
return {
|
||
userMessage,
|
||
assistantMessage,
|
||
usage: response.usage,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 发送消息(流式)
|
||
*/
|
||
async *sendMessageStream(
|
||
data: SendMessageData,
|
||
userId: string
|
||
): AsyncGenerator<StreamChunk, void, unknown> {
|
||
const { conversationId, content, modelType, knowledgeBaseIds } = data;
|
||
|
||
// 获取对话信息
|
||
const conversation = await this.getConversationById(conversationId, userId);
|
||
|
||
// 获取知识库上下文(如果有@知识库)
|
||
console.log('📚 [sendMessageStream] 开始处理知识库', { knowledgeBaseIds });
|
||
let knowledgeBaseContext = '';
|
||
if (knowledgeBaseIds && knowledgeBaseIds.length > 0) {
|
||
const knowledgeResults: string[] = [];
|
||
|
||
// 对每个知识库进行检索
|
||
for (const kbId of knowledgeBaseIds) {
|
||
try {
|
||
console.log(`🔎 [sendMessageStream] 检索知识库 ${kbId}`);
|
||
const searchResult = await knowledgeBaseService.searchKnowledgeBase(
|
||
userId,
|
||
kbId,
|
||
content,
|
||
3 // 每个知识库返回3个最相关的段落
|
||
);
|
||
|
||
console.log(`✅ [sendMessageStream] 检索结果`, {
|
||
kbId,
|
||
recordCount: searchResult.records?.length || 0
|
||
});
|
||
|
||
// 格式化检索结果
|
||
if (searchResult.records && searchResult.records.length > 0) {
|
||
const kbInfo = await prisma.knowledgeBase.findUnique({
|
||
where: { id: kbId },
|
||
select: { name: true },
|
||
});
|
||
|
||
const formattedResult = `【知识库:${kbInfo?.name || '未命名'}】\n` +
|
||
searchResult.records
|
||
.map((record: any, index: number) => {
|
||
const score = (record.score * 100).toFixed(1);
|
||
return `${index + 1}. [相关度${score}%] ${record.segment.content}`;
|
||
})
|
||
.join('\n\n');
|
||
|
||
console.log(`📄 [sendMessageStream] 格式化结果长度: ${formattedResult.length} 字符`);
|
||
knowledgeResults.push(formattedResult);
|
||
} else {
|
||
console.warn(`⚠️ [sendMessageStream] 知识库 ${kbId} 没有检索到记录`);
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ [sendMessageStream] 检索知识库失败 ${kbId}:`, error);
|
||
// 检索失败不阻止对话,继续处理
|
||
}
|
||
}
|
||
|
||
if (knowledgeResults.length > 0) {
|
||
knowledgeBaseContext = knowledgeResults.join('\n\n---\n\n');
|
||
console.log(`💾 [sendMessageStream] 知识库上下文总长度: ${knowledgeBaseContext.length} 字符`);
|
||
console.log(`📋 [sendMessageStream] 知识库上下文预览:\n${knowledgeBaseContext.substring(0, 500)}...`);
|
||
} else {
|
||
console.warn('⚠️ [sendMessageStream] 没有构建任何知识库上下文');
|
||
}
|
||
} else {
|
||
console.log('ℹ️ [sendMessageStream] 未选择知识库');
|
||
}
|
||
|
||
// 组装上下文
|
||
const messages = await this.assembleContext(
|
||
conversationId,
|
||
conversation.agentId,
|
||
conversation.project?.background || '',
|
||
content,
|
||
knowledgeBaseContext
|
||
);
|
||
|
||
// 获取LLM适配器
|
||
const adapter = LLMFactory.getAdapter(modelType);
|
||
|
||
// 获取智能体配置的模型参数
|
||
const agent = agentService.getAgentById(conversation.agentId);
|
||
const modelConfig = agent?.models?.[modelType];
|
||
|
||
// 保存用户消息
|
||
await prisma.message.create({
|
||
data: {
|
||
conversationId,
|
||
role: 'user',
|
||
content,
|
||
metadata: {
|
||
knowledgeBaseIds,
|
||
},
|
||
},
|
||
});
|
||
|
||
// 用于累积完整的回复内容
|
||
let fullContent = '';
|
||
let usage: any = null;
|
||
|
||
// 流式调用LLM
|
||
for await (const chunk of adapter.chatStream(messages, {
|
||
temperature: modelConfig?.temperature,
|
||
maxTokens: modelConfig?.maxTokens,
|
||
topP: modelConfig?.topP,
|
||
})) {
|
||
fullContent += chunk.content;
|
||
|
||
if (chunk.usage) {
|
||
usage = chunk.usage;
|
||
}
|
||
|
||
yield chunk;
|
||
}
|
||
|
||
// 流式输出完成后,保存助手回复
|
||
await prisma.message.create({
|
||
data: {
|
||
conversationId,
|
||
role: 'assistant',
|
||
content: fullContent,
|
||
model: modelType,
|
||
tokens: usage?.totalTokens,
|
||
metadata: {
|
||
usage,
|
||
},
|
||
},
|
||
});
|
||
|
||
// 更新对话的最后更新时间
|
||
await prisma.conversation.update({
|
||
where: { id: conversationId },
|
||
data: { updatedAt: new Date() },
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 删除对话(软删除)
|
||
*/
|
||
async deleteConversation(conversationId: string, userId: string) {
|
||
const conversation = await prisma.conversation.findFirst({
|
||
where: {
|
||
id: conversationId,
|
||
userId,
|
||
deletedAt: null,
|
||
},
|
||
});
|
||
|
||
if (!conversation) {
|
||
throw new Error('对话不存在或无权访问');
|
||
}
|
||
|
||
await prisma.conversation.update({
|
||
where: { id: conversationId },
|
||
data: { deletedAt: new Date() },
|
||
});
|
||
|
||
return { success: true };
|
||
}
|
||
}
|
||
|
||
export const conversationService = new ConversationService();
|
||
|