iconsulting/packages/services/conversation-service/src/infrastructure/knowledge/knowledge-client.service.ts

298 lines
8.3 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, OnModuleInit, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CircuitBreaker } from '../common/circuit-breaker';
/**
* 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 readonly logger = new Logger(KnowledgeClientService.name);
private baseUrl: string;
private readonly circuitBreaker: CircuitBreaker;
constructor(private configService: ConfigService) {
this.circuitBreaker = new CircuitBreaker({
name: 'knowledge-service',
failureThreshold: 5,
resetTimeoutMs: 60_000,
});
}
onModuleInit() {
this.baseUrl = this.configService.get<string>('KNOWLEDGE_SERVICE_URL') || 'http://knowledge-service:3003';
this.logger.log(`Initialized with base URL: ${this.baseUrl}`);
}
/**
* 受熔断器保护的 fetch — 连续 5 次失败后熔断 60s
* 熔断期间直接返回 null不发起网络请求
*/
private protectedFetch(url: string, init?: RequestInit): Promise<Response | null> {
return this.circuitBreaker.execute(
async () => {
const resp = await fetch(url, init);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp;
},
null,
);
}
/**
* RAG 知识检索
*/
async retrieveKnowledge(params: {
query: string;
userId?: string;
category?: string;
includeMemories?: boolean;
includeExperiences?: boolean;
}): Promise<RAGResult | null> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/knowledge/retrieve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!resp) return null;
const data = (await resp.json()) as ApiResponse<RAGResult>;
return data.success ? data.data : null;
}
/**
* RAG 检索并格式化为提示词上下文
*/
async retrieveForPrompt(params: {
query: string;
userId?: string;
category?: string;
}): Promise<string | null> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/knowledge/retrieve/prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!resp) return null;
const data = (await resp.json()) as ApiResponse<{ context: string }>;
return data.success ? data.data.context : null;
}
/**
* 检查是否离题
*/
async checkOffTopic(query: string): Promise<OffTopicResult> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/knowledge/check-off-topic`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
if (!resp) return { isOffTopic: false, confidence: 0 };
const data = (await resp.json()) as ApiResponse<OffTopicResult>;
return data.success ? data.data : { isOffTopic: false, confidence: 0 };
}
/**
* 保存用户记忆
*/
async saveUserMemory(params: {
userId: string;
memoryType: 'FACT' | 'PREFERENCE' | 'INTENT';
content: string;
importance?: number;
sourceConversationId?: string;
relatedCategory?: string;
}): Promise<UserMemory | null> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/memory/user`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!resp) return null;
const data = (await resp.json()) as ApiResponse<UserMemory>;
return data.success ? data.data : null;
}
/**
* 搜索用户相关记忆
*/
async searchUserMemories(params: {
userId: string;
query: string;
limit?: number;
}): Promise<UserMemory[]> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/memory/user/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!resp) return [];
const data = (await resp.json()) as ApiResponse<UserMemory[]>;
return data.success ? data.data : [];
}
/**
* 获取用户最重要的记忆
*/
async getUserTopMemories(userId: string, limit = 5): Promise<UserMemory[]> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/memory/user/${userId}/top?limit=${limit}`);
if (!resp) return [];
const data = (await resp.json()) as ApiResponse<UserMemory[]>;
return data.success ? data.data : [];
}
/**
* 搜索相关系统经验
*/
async searchExperiences(params: {
query: string;
experienceType?: string;
category?: string;
activeOnly?: boolean;
limit?: number;
}): Promise<SystemExperience[]> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/memory/experience/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!resp) return [];
const data = (await resp.json()) as ApiResponse<SystemExperience[]>;
return data.success ? data.data : [];
}
/**
* 初始化用户节点Neo4j
*/
async initializeUser(userId: string): Promise<boolean> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/memory/user`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId,
memoryType: 'FACT',
content: '用户首次访问系统',
importance: 10,
}),
});
return resp !== null;
}
/**
* 保存系统经验
* 用于将评估门控的失败教训沉淀为全局经验,影响未来所有对话
*/
async saveExperience(params: {
experienceType: string;
content: string;
scenario: string;
sourceConversationId: string;
confidence?: number;
relatedCategory?: string;
}): Promise<SystemExperience | null> {
const resp = await this.protectedFetch(`${this.baseUrl}/api/v1/memory/experience`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!resp) return null;
const data = (await resp.json()) as ApiResponse<SystemExperience>;
return data.success ? data.data : null;
}
// ============================================================
// Convenience Methods (for Specialist Agents)
// ============================================================
/**
* 简便搜索方法 — 供专家 Agent 使用
* 封装 retrieveForPrompt
*/
async search(query: string, category?: string): Promise<string> {
const result = await this.retrieveForPrompt({ query, category });
return result || '';
}
/**
* 简便获取用户上下文方法 — 供专家 Agent 使用
* 如果提供 userId 则搜索用户记忆,否则做通用知识检索
*/
async getUserContext(query: string, userId?: string): Promise<string> {
if (!userId) {
// 无 userId 时做通用检索
return await this.search(query);
}
const memories = await this.searchUserMemories({ userId, query, limit: 5 });
if (memories.length === 0) return '';
return memories.map(m => `[${m.memoryType}] (重要度:${m.importance}) ${m.content}`).join('\n');
}
/**
* 获取熔断器状态 — 供 Admin API 使用
*/
getCircuitBreakerStatus() {
return {
name: 'knowledge-service',
state: this.circuitBreaker.getState(),
failureCount: this.circuitBreaker.getFailureCount(),
config: { failureThreshold: 5, resetTimeoutMs: 60_000 },
};
}
}