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 { 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('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 { 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 { 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; return data.success ? data.data : null; } /** * RAG 检索并格式化为提示词上下文 */ async retrieveForPrompt(params: { query: string; userId?: string; category?: string; }): Promise { 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 { 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; 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 { 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; return data.success ? data.data : null; } /** * 搜索用户相关记忆 */ async searchUserMemories(params: { userId: string; query: string; limit?: number; }): Promise { 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; return data.success ? data.data : []; } /** * 获取用户最重要的记忆 */ async getUserTopMemories(userId: string, limit = 5): Promise { 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; return data.success ? data.data : []; } /** * 搜索相关系统经验 */ async searchExperiences(params: { query: string; experienceType?: string; category?: string; activeOnly?: boolean; limit?: number; }): Promise { 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; return data.success ? data.data : []; } /** * 初始化用户节点(Neo4j) */ async initializeUser(userId: string): Promise { 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 { 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; return data.success ? data.data : null; } // ============================================================ // Convenience Methods (for Specialist Agents) // ============================================================ /** * 简便搜索方法 — 供专家 Agent 使用 * 封装 retrieveForPrompt */ async search(query: string, category?: string): Promise { const result = await this.retrieveForPrompt({ query, category }); return result || ''; } /** * 简便获取用户上下文方法 — 供专家 Agent 使用 * 如果提供 userId 则搜索用户记忆,否则做通用知识检索 */ async getUserContext(query: string, userId?: string): Promise { 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 }, }; } }