290 lines
7.5 KiB
TypeScript
290 lines
7.5 KiB
TypeScript
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||
import { ConfigService } from '@nestjs/config';
|
||
|
||
/**
|
||
* RAG 检索结果
|
||
*/
|
||
export interface RAGResult {
|
||
content: string;
|
||
sources: Array<{
|
||
articleId: string;
|
||
title: string;
|
||
similarity: number;
|
||
}>;
|
||
userMemories?: string[];
|
||
systemExperiences?: string[];
|
||
}
|
||
|
||
/**
|
||
* 离题检测结果
|
||
*/
|
||
export interface OffTopicResult {
|
||
isOffTopic: boolean;
|
||
confidence: number;
|
||
reason?: string;
|
||
}
|
||
|
||
/**
|
||
* 用户记忆
|
||
*/
|
||
export interface UserMemory {
|
||
id: string;
|
||
userId: string;
|
||
memoryType: 'FACT' | 'PREFERENCE' | 'INTENT';
|
||
content: string;
|
||
importance: number;
|
||
createdAt: string;
|
||
}
|
||
|
||
/**
|
||
* 系统经验
|
||
*/
|
||
export interface SystemExperience {
|
||
id: string;
|
||
experienceType: string;
|
||
content: string;
|
||
scenario: string;
|
||
confidence: number;
|
||
relatedCategory?: string;
|
||
}
|
||
|
||
/**
|
||
* API 响应包装
|
||
*/
|
||
interface ApiResponse<T> {
|
||
success: boolean;
|
||
data: T;
|
||
message?: string;
|
||
}
|
||
|
||
/**
|
||
* Knowledge Service 客户端
|
||
* 封装对 knowledge-service 的 HTTP 调用
|
||
*/
|
||
@Injectable()
|
||
export class KnowledgeClientService implements OnModuleInit {
|
||
private baseUrl: string;
|
||
|
||
constructor(private configService: ConfigService) {}
|
||
|
||
onModuleInit() {
|
||
this.baseUrl = this.configService.get<string>('KNOWLEDGE_SERVICE_URL') || 'http://knowledge-service:3003';
|
||
console.log(`[KnowledgeClient] Initialized with base URL: ${this.baseUrl}`);
|
||
}
|
||
|
||
/**
|
||
* RAG 知识检索
|
||
*/
|
||
async retrieveKnowledge(params: {
|
||
query: string;
|
||
userId?: string;
|
||
category?: string;
|
||
includeMemories?: boolean;
|
||
includeExperiences?: boolean;
|
||
}): Promise<RAGResult | null> {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/api/v1/knowledge/retrieve`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(params),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`[KnowledgeClient] RAG retrieve failed: ${response.status}`);
|
||
return null;
|
||
}
|
||
|
||
const data = (await response.json()) as ApiResponse<RAGResult>;
|
||
return data.success ? data.data : null;
|
||
} catch (error) {
|
||
console.error('[KnowledgeClient] RAG retrieve error:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* RAG 检索并格式化为提示词上下文
|
||
*/
|
||
async retrieveForPrompt(params: {
|
||
query: string;
|
||
userId?: string;
|
||
category?: string;
|
||
}): Promise<string | null> {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/api/v1/knowledge/retrieve/prompt`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(params),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`[KnowledgeClient] RAG retrieve/prompt failed: ${response.status}`);
|
||
return null;
|
||
}
|
||
|
||
const data = (await response.json()) as ApiResponse<{ context: string }>;
|
||
return data.success ? data.data.context : null;
|
||
} catch (error) {
|
||
console.error('[KnowledgeClient] RAG retrieve/prompt error:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查是否离题
|
||
*/
|
||
async checkOffTopic(query: string): Promise<OffTopicResult> {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/api/v1/knowledge/check-off-topic`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ query }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`[KnowledgeClient] checkOffTopic failed: ${response.status}`);
|
||
return { isOffTopic: false, confidence: 0 };
|
||
}
|
||
|
||
const data = (await response.json()) as ApiResponse<OffTopicResult>;
|
||
return data.success ? data.data : { isOffTopic: false, confidence: 0 };
|
||
} catch (error) {
|
||
console.error('[KnowledgeClient] checkOffTopic error:', error);
|
||
return { isOffTopic: false, confidence: 0 };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存用户记忆
|
||
*/
|
||
async saveUserMemory(params: {
|
||
userId: string;
|
||
memoryType: 'FACT' | 'PREFERENCE' | 'INTENT';
|
||
content: string;
|
||
importance?: number;
|
||
sourceConversationId?: string;
|
||
relatedCategory?: string;
|
||
}): Promise<UserMemory | null> {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/api/v1/memory/user`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(params),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`[KnowledgeClient] saveUserMemory failed: ${response.status}`);
|
||
return null;
|
||
}
|
||
|
||
const data = (await response.json()) as ApiResponse<UserMemory>;
|
||
return data.success ? data.data : null;
|
||
} catch (error) {
|
||
console.error('[KnowledgeClient] saveUserMemory error:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 搜索用户相关记忆
|
||
*/
|
||
async searchUserMemories(params: {
|
||
userId: string;
|
||
query: string;
|
||
limit?: number;
|
||
}): Promise<UserMemory[]> {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/api/v1/memory/user/search`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(params),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`[KnowledgeClient] searchUserMemories failed: ${response.status}`);
|
||
return [];
|
||
}
|
||
|
||
const data = (await response.json()) as ApiResponse<UserMemory[]>;
|
||
return data.success ? data.data : [];
|
||
} catch (error) {
|
||
console.error('[KnowledgeClient] searchUserMemories error:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户最重要的记忆
|
||
*/
|
||
async getUserTopMemories(userId: string, limit = 5): Promise<UserMemory[]> {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/api/v1/memory/user/${userId}/top?limit=${limit}`);
|
||
|
||
if (!response.ok) {
|
||
console.error(`[KnowledgeClient] getUserTopMemories failed: ${response.status}`);
|
||
return [];
|
||
}
|
||
|
||
const data = (await response.json()) as ApiResponse<UserMemory[]>;
|
||
return data.success ? data.data : [];
|
||
} catch (error) {
|
||
console.error('[KnowledgeClient] getUserTopMemories error:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 搜索相关系统经验
|
||
*/
|
||
async searchExperiences(params: {
|
||
query: string;
|
||
experienceType?: string;
|
||
category?: string;
|
||
activeOnly?: boolean;
|
||
limit?: number;
|
||
}): Promise<SystemExperience[]> {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/api/v1/memory/experience/search`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(params),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`[KnowledgeClient] searchExperiences failed: ${response.status}`);
|
||
return [];
|
||
}
|
||
|
||
const data = (await response.json()) as ApiResponse<SystemExperience[]>;
|
||
return data.success ? data.data : [];
|
||
} catch (error) {
|
||
console.error('[KnowledgeClient] searchExperiences error:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化用户节点(Neo4j)
|
||
*/
|
||
async initializeUser(userId: string): Promise<boolean> {
|
||
try {
|
||
// 通过保存一个初始记忆来初始化用户
|
||
const response = await fetch(`${this.baseUrl}/api/v1/memory/user`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
userId,
|
||
memoryType: 'FACT',
|
||
content: '用户首次访问系统',
|
||
importance: 10,
|
||
}),
|
||
});
|
||
|
||
return response.ok;
|
||
} catch (error) {
|
||
console.error('[KnowledgeClient] initializeUser error:', error);
|
||
return false;
|
||
}
|
||
}
|
||
}
|