diff --git a/desktop/src/lib/__tests__/llm-integration.test.ts b/desktop/src/lib/__tests__/llm-integration.test.ts new file mode 100644 index 0000000..81015ba --- /dev/null +++ b/desktop/src/lib/__tests__/llm-integration.test.ts @@ -0,0 +1,228 @@ +/** + * LLM Integration Tests - Phase 2 Engine Upgrades + * + * Tests for LLM-powered features: + * - ReflectionEngine with LLM semantic analysis + * - ContextCompactor with LLM summarization + * - MemoryExtractor with LLM importance scoring + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + ReflectionEngine, + DEFAULT_REFLECTION_CONFIG, + type ReflectionConfig, +} from '../reflection-engine'; +import { + ContextCompactor, + DEFAULT_COMPACTION_CONFIG, + type CompactionConfig, +} from '../context-compactor'; +import { + MemoryExtractor, + DEFAULT_EXTRACTION_CONFIG, + type ExtractionConfig, +} from '../memory-extractor'; +import { + getLLMAdapter, + resetLLMAdapter, + type LLMProvider, +} from '../llm-service'; + +// === Mock LLM Adapter === + +const mockLLMAdapter = { + complete: vi.fn(), + isAvailable: vi.fn(() => true), + getProvider: vi.fn(() => 'mock' as LLMProvider), +}; + +vi.mock('../llm-service', () => ({ + getLLMAdapter: vi.fn(() => mockLLMAdapter), + resetLLMAdapter: vi.fn(), + llmReflect: vi.fn(async () => JSON.stringify({ + patterns: [ + { + observation: '用户经常询问代码优化问题', + frequency: 5, + sentiment: 'positive', + evidence: ['多次讨论性能优化'], + }, + ], + improvements: [ + { + area: '代码解释', + suggestion: '可以提供更详细的代码注释', + priority: 'medium', + }, + ], + identityProposals: [], + })), + llmCompact: vi.fn(async () => '[LLM摘要]\n讨论主题: 代码优化\n关键决策: 使用缓存策略\n待办事项: 完成性能测试'), + llmExtract: vi.fn(async () => JSON.stringify([ + { content: '用户偏好简洁的回答', type: 'preference', importance: 7, tags: ['style'] }, + { content: '项目使用 TypeScript', type: 'fact', importance: 6, tags: ['tech'] }, + ])), +})); + +// === ReflectionEngine Tests === + +describe('ReflectionEngine with LLM', () => { + let engine: ReflectionEngine; + + beforeEach(() => { + vi.clearAllMocks(); + engine = new ReflectionEngine({ useLLM: true }); + }); + + afterEach(() => { + engine?.updateConfig({ useLLM: false }); + }); + + it('should initialize with LLM config', () => { + const config = engine.getConfig(); + expect(config.useLLM).toBe(true); + }); + + it('should have llmFallbackToRules enabled by default', () => { + const config = engine.getConfig(); + expect(config.llmFallbackToRules).toBe(true); + }); + + it('should track conversations for reflection trigger', () => { + engine.recordConversation(); + engine.recordConversation(); + expect(engine.shouldReflect()).toBe(false); + + // After 5 conversations (default trigger) + for (let i = 0; i < 4; i++) { + engine.recordConversation(); + } + expect(engine.shouldReflect()).toBe(true); + }); + + it('should use LLM when enabled and available', async () => { + mockLLMAdapter.isAvailable.mockReturnValue(true); + + const result = await engine.reflect('test-agent', { forceLLM: true }); + + expect(result.patterns.length).toBeGreaterThan(0); + expect(result.timestamp).toBeDefined(); + }); + + it('should fallback to rules when LLM fails', async () => { + mockLLMAdapter.isAvailable.mockReturnValue(false); + + const result = await engine.reflect('test-agent'); + + // Should still work with rule-based approach + expect(result).toBeDefined(); + expect(result.timestamp).toBeDefined(); + }); +}); + +// === ContextCompactor Tests === + +describe('ContextCompactor with LLM', () => { + let compactor: ContextCompactor; + + beforeEach(() => { + vi.clearAllMocks(); + compactor = new ContextCompactor({ useLLM: true }); + }); + + it('should initialize with LLM config', () => { + const config = compactor.getConfig(); + expect(config.useLLM).toBe(true); + }); + + it('should have llmFallbackToRules enabled by default', () => { + const config = compactor.getConfig(); + expect(config.llmFallbackToRules).toBe(true); + }); + + it('should check threshold correctly', () => { + const messages = [ + { role: 'user', content: 'Hello'.repeat(1000) }, + { role: 'assistant', content: 'Response'.repeat(1000) }, + ]; + + const check = compactor.checkThreshold(messages); + expect(check.shouldCompact).toBe(false); + expect(check.urgency).toBe('none'); + }); + + it('should trigger soft threshold', () => { + // Create enough messages to exceed 15000 soft threshold but not 20000 hard threshold + // estimateTokens: CJK chars ~1.5 tokens each + // 20 messages × 600 CJK chars × 1.5 = ~18000 tokens (between soft and hard) + const messages = Array(20).fill(null).map((_, i) => ({ + role: i % 2 === 0 ? 'user' : 'assistant', + content: '测试内容'.repeat(150), // 600 CJK chars ≈ 900 tokens each + })); + + const check = compactor.checkThreshold(messages); + expect(check.shouldCompact).toBe(true); + expect(check.urgency).toBe('soft'); + }); +}); + +// === MemoryExtractor Tests === + +describe('MemoryExtractor with LLM', () => { + let extractor: MemoryExtractor; + + beforeEach(() => { + vi.clearAllMocks(); + extractor = new MemoryExtractor({ useLLM: true }); + }); + + it('should initialize with LLM config', () => { + // MemoryExtractor doesn't expose config directly, but we can test behavior + expect(extractor).toBeDefined(); + }); + + it('should skip extraction with too few messages', async () => { + const messages = [ + { role: 'user', content: 'Hi' }, + { role: 'assistant', content: 'Hello!' }, + ]; + + const result = await extractor.extractFromConversation(messages, 'test-agent'); + expect(result.saved).toBe(0); + }); + + it('should extract with enough messages', async () => { + const messages = [ + { role: 'user', content: '我喜欢简洁的回答' }, + { role: 'assistant', content: '好的,我会简洁一些' }, + { role: 'user', content: '我的项目使用 TypeScript' }, + { role: 'assistant', content: 'TypeScript 是个好选择' }, + { role: 'user', content: '继续' }, + { role: 'assistant', content: '继续...' }, + ]; + + const result = await extractor.extractFromConversation(messages, 'test-agent'); + expect(result.items.length).toBeGreaterThanOrEqual(0); + }); +}); + +// === Integration Test === + +describe('LLM Integration Full Flow', () => { + it('should work end-to-end with all engines', async () => { + // Setup all engines with LLM + const engine = new ReflectionEngine({ useLLM: true, llmFallbackToRules: true }); + const compactor = new ContextCompactor({ useLLM: true, llmFallbackToRules: true }); + const extractor = new MemoryExtractor({ useLLM: true, llmFallbackToRules: true }); + + // Verify they all have LLM support + expect(engine.getConfig().useLLM).toBe(true); + expect(compactor.getConfig().useLLM).toBe(true); + + // All should work without throwing + await expect(engine.reflect('test-agent')).resolves; + await expect(compactor.compact([], 'test-agent')).resolves; + await expect(extractor.extractFromConversation([], 'test-agent')).resolves; + }); +}); diff --git a/desktop/src/lib/agent-memory.ts b/desktop/src/lib/agent-memory.ts index 509b4cb..fbc90cc 100644 --- a/desktop/src/lib/agent-memory.ts +++ b/desktop/src/lib/agent-memory.ts @@ -10,7 +10,7 @@ // === Types === export type MemoryType = 'fact' | 'preference' | 'lesson' | 'context' | 'task'; -export type MemorySource = 'auto' | 'user' | 'reflection'; +export type MemorySource = 'auto' | 'user' | 'reflection' | 'llm-reflection'; export interface MemoryEntry { id: string; diff --git a/desktop/src/lib/context-compactor.ts b/desktop/src/lib/context-compactor.ts index 8413ec5..3c70a0e 100644 --- a/desktop/src/lib/context-compactor.ts +++ b/desktop/src/lib/context-compactor.ts @@ -8,12 +8,18 @@ * 4. Replace old messages with summary — user sees no interruption * * Phase 2 implementation: heuristic token estimation + rule-based summarization. - * Phase 3 upgrade: LLM-powered summarization + semantic importance scoring. + * Phase 4 upgrade: LLM-powered summarization + semantic importance scoring. * * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.3.1 */ import { getMemoryExtractor, type ConversationMessage } from './memory-extractor'; +import { + getLLMAdapter, + llmCompact, + type LLMServiceAdapter, + type LLMProvider, +} from './llm-service'; // === Types === @@ -24,6 +30,9 @@ export interface CompactionConfig { memoryFlushEnabled: boolean; // Extract memories before compacting (default true) keepRecentMessages: number; // Always keep this many recent messages (default 6) summaryMaxTokens: number; // Max tokens for the compaction summary (default 800) + useLLM: boolean; // Use LLM for high-quality summarization (Phase 4) + llmProvider?: LLMProvider; // Preferred LLM provider + llmFallbackToRules: boolean; // Fall back to rules if LLM fails } export interface CompactableMessage { @@ -59,6 +68,8 @@ export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = { memoryFlushEnabled: true, keepRecentMessages: 6, summaryMaxTokens: 800, + useLLM: false, + llmFallbackToRules: true, }; // === Token Estimation === @@ -103,9 +114,19 @@ export function estimateMessagesTokens(messages: CompactableMessage[]): number { export class ContextCompactor { private config: CompactionConfig; + private llmAdapter: LLMServiceAdapter | null = null; constructor(config?: Partial) { this.config = { ...DEFAULT_COMPACTION_CONFIG, ...config }; + + // Initialize LLM adapter if configured + if (this.config.useLLM) { + try { + this.llmAdapter = getLLMAdapter(); + } catch (error) { + console.warn('[ContextCompactor] Failed to initialize LLM adapter:', error); + } + } } /** @@ -154,12 +175,13 @@ export class ContextCompactor { * Execute compaction: summarize old messages, keep recent ones. * * Phase 2: Rule-based summarization (extract key points heuristically). - * Phase 3: LLM-powered summarization. + * Phase 4: LLM-powered summarization for higher quality summaries. */ async compact( messages: CompactableMessage[], agentId: string, - conversationId?: string + conversationId?: string, + options?: { forceLLM?: boolean } ): Promise { const tokensBeforeCompaction = estimateMessagesTokens(messages); const keepCount = Math.min(this.config.keepRecentMessages, messages.length); @@ -176,7 +198,22 @@ export class ContextCompactor { } // Step 2: Generate summary of old messages - const summary = this.generateSummary(oldMessages); + let summary: string; + if ((this.config.useLLM || options?.forceLLM) && this.llmAdapter?.isAvailable()) { + try { + console.log('[ContextCompactor] Using LLM-powered summarization'); + summary = await this.llmGenerateSummary(oldMessages); + } catch (error) { + console.error('[ContextCompactor] LLM summarization failed:', error); + if (!this.config.llmFallbackToRules) { + throw error; + } + console.log('[ContextCompactor] Falling back to rule-based summarization'); + summary = this.generateSummary(oldMessages); + } + } else { + summary = this.generateSummary(oldMessages); + } // Step 3: Build compacted message list const summaryMessage: CompactableMessage = { @@ -206,6 +243,30 @@ export class ContextCompactor { }; } + /** + * LLM-powered summary generation for high-quality compaction. + */ + private async llmGenerateSummary(messages: CompactableMessage[]): Promise { + if (messages.length === 0) return '[对话开始]'; + + // Build conversation text for LLM + const conversationText = messages + .filter(m => m.role === 'user' || m.role === 'assistant') + .map(m => `[${m.role === 'user' ? '用户' : '助手'}]: ${m.content}`) + .join('\n\n'); + + // Use llmCompact helper from llm-service + const llmSummary = await llmCompact(conversationText, this.llmAdapter!); + + // Enforce token limit + const summaryTokens = estimateTokens(llmSummary); + if (summaryTokens > this.config.summaryMaxTokens) { + return llmSummary.slice(0, this.config.summaryMaxTokens * 2) + '\n...(摘要已截断)'; + } + + return `[LLM摘要]\n${llmSummary}`; + } + /** * Phase 2: Rule-based summary generation. * Extracts key topics, decisions, and action items from old messages. diff --git a/desktop/src/lib/memory-extractor.ts b/desktop/src/lib/memory-extractor.ts index ad4e674..9ff362d 100644 --- a/desktop/src/lib/memory-extractor.ts +++ b/desktop/src/lib/memory-extractor.ts @@ -9,11 +9,20 @@ * * Also handles auto-updating USER.md with discovered preferences. * + * Phase 1: Rule-based extraction (pattern matching). + * Phase 4: LLM-powered semantic extraction with importance scoring. + * * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.2 */ import { getMemoryManager, type MemoryType } from './agent-memory'; import { getAgentIdentityManager } from './agent-identity'; +import { + getLLMAdapter, + llmExtract, + type LLMServiceAdapter, + type LLMProvider, +} from './llm-service'; // === Types === @@ -36,6 +45,15 @@ export interface ConversationMessage { content: string; } +export interface ExtractionConfig { + useLLM: boolean; // Use LLM for semantic extraction (Phase 4) + llmProvider?: LLMProvider; // Preferred LLM provider + llmFallbackToRules: boolean; // Fall back to rules if LLM fails + minMessagesForExtraction: number; // Minimum messages before extraction + extractionCooldownMs: number; // Cooldown between extractions + minImportanceThreshold: number; // Only save items with importance >= this +} + // === Extraction Prompt === const EXTRACTION_PROMPT = `请从以下对话中提取值得长期记住的信息。 @@ -59,38 +77,80 @@ const EXTRACTION_PROMPT = `请从以下对话中提取值得长期记住的信 对话内容: `; +// === Default Config === + +export const DEFAULT_EXTRACTION_CONFIG: ExtractionConfig = { + useLLM: false, + llmFallbackToRules: true, + minMessagesForExtraction: 4, + extractionCooldownMs: 30_000, + minImportanceThreshold: 3, +}; + // === Memory Extractor === export class MemoryExtractor { - private minMessagesForExtraction = 4; - private extractionCooldownMs = 30_000; // 30 seconds between extractions + private config: ExtractionConfig; private lastExtractionTime = 0; + private llmAdapter: LLMServiceAdapter | null = null; + + constructor(config?: Partial) { + this.config = { ...DEFAULT_EXTRACTION_CONFIG, ...config }; + + // Initialize LLM adapter if configured + if (this.config.useLLM) { + try { + this.llmAdapter = getLLMAdapter(); + } catch (error) { + console.warn('[MemoryExtractor] Failed to initialize LLM adapter:', error); + } + } + } /** - * Extract memories from a conversation using rule-based heuristics. - * This is the Phase 1 approach — no LLM call needed. - * Phase 2 will add LLM-based extraction using EXTRACTION_PROMPT. + * Extract memories from a conversation. + * Uses LLM if configured, falls back to rule-based extraction. */ async extractFromConversation( messages: ConversationMessage[], agentId: string, - conversationId?: string + conversationId?: string, + options?: { forceLLM?: boolean } ): Promise { // Cooldown check - if (Date.now() - this.lastExtractionTime < this.extractionCooldownMs) { + if (Date.now() - this.lastExtractionTime < this.config.extractionCooldownMs) { return { items: [], saved: 0, skipped: 0, userProfileUpdated: false }; } // Minimum message threshold const chatMessages = messages.filter(m => m.role === 'user' || m.role === 'assistant'); - if (chatMessages.length < this.minMessagesForExtraction) { + if (chatMessages.length < this.config.minMessagesForExtraction) { return { items: [], saved: 0, skipped: 0, userProfileUpdated: false }; } this.lastExtractionTime = Date.now(); - // Phase 1: Rule-based extraction (pattern matching) - const extracted = this.ruleBasedExtraction(chatMessages); + // Try LLM extraction if enabled + let extracted: ExtractedItem[]; + if ((this.config.useLLM || options?.forceLLM) && this.llmAdapter?.isAvailable()) { + try { + console.log('[MemoryExtractor] Using LLM-powered semantic extraction'); + extracted = await this.llmBasedExtraction(chatMessages); + } catch (error) { + console.error('[MemoryExtractor] LLM extraction failed:', error); + if (!this.config.llmFallbackToRules) { + throw error; + } + console.log('[MemoryExtractor] Falling back to rule-based extraction'); + extracted = this.ruleBasedExtraction(chatMessages); + } + } else { + // Rule-based extraction + extracted = this.ruleBasedExtraction(chatMessages); + } + + // Filter by importance threshold + extracted = extracted.filter(item => item.importance >= this.config.minImportanceThreshold); // Save to memory const memoryManager = getMemoryManager(); @@ -135,6 +195,23 @@ export class MemoryExtractor { return { items: extracted, saved, skipped, userProfileUpdated }; } + /** + * LLM-powered semantic extraction. + * Uses LLM to understand context and score importance semantically. + */ + private async llmBasedExtraction(messages: ConversationMessage[]): Promise { + const conversationText = messages + .filter(m => m.role === 'user' || m.role === 'assistant') + .map(m => `[${m.role === 'user' ? '用户' : '助手'}]: ${m.content}`) + .join('\n\n'); + + // Use llmExtract helper from llm-service + const llmResponse = await llmExtract(conversationText, this.llmAdapter!); + + // Parse the JSON response + return this.parseExtractionResponse(llmResponse); + } + /** * Phase 1: Rule-based extraction using pattern matching. * Extracts common patterns from user messages. diff --git a/desktop/src/lib/reflection-engine.ts b/desktop/src/lib/reflection-engine.ts index 56e57b6..6b7d49f 100644 --- a/desktop/src/lib/reflection-engine.ts +++ b/desktop/src/lib/reflection-engine.ts @@ -15,6 +15,12 @@ import { getMemoryManager, type MemoryEntry } from './agent-memory'; import { getAgentIdentityManager, type IdentityChangeProposal } from './agent-identity'; +import { + getLLMAdapter, + llmReflect, + type LLMServiceAdapter, + type LLMProvider, +} from './llm-service'; // === Types === @@ -23,6 +29,9 @@ export interface ReflectionConfig { triggerAfterHours: number; // Reflect after N hours (default 24) allowSoulModification: boolean; // Can propose SOUL.md changes requireApproval: boolean; // Identity changes need user OK + useLLM: boolean; // Use LLM for deep reflection (Phase 4) + llmProvider?: LLMProvider; // Preferred LLM provider + llmFallbackToRules: boolean; // Fall back to rules if LLM fails } export interface PatternObservation { @@ -53,6 +62,8 @@ export const DEFAULT_REFLECTION_CONFIG: ReflectionConfig = { triggerAfterHours: 24, allowSoulModification: false, requireApproval: true, + useLLM: false, + llmFallbackToRules: true, }; // === Storage === @@ -72,11 +83,21 @@ export class ReflectionEngine { private config: ReflectionConfig; private state: ReflectionState; private history: ReflectionResult[] = []; + private llmAdapter: LLMServiceAdapter | null = null; constructor(config?: Partial) { this.config = { ...DEFAULT_REFLECTION_CONFIG, ...config }; this.state = this.loadState(); this.loadHistory(); + + // Initialize LLM adapter if configured + if (this.config.useLLM) { + try { + this.llmAdapter = getLLMAdapter(); + } catch (error) { + console.warn('[ReflectionEngine] Failed to initialize LLM adapter:', error); + } + } } // === Trigger Management === @@ -116,9 +137,205 @@ export class ReflectionEngine { /** * Execute a reflection cycle for the given agent. */ - async reflect(agentId: string): Promise { + async reflect(agentId: string, options?: { forceLLM?: boolean }): Promise { console.log(`[Reflection] Starting reflection for agent: ${agentId}`); + // Try LLM-powered reflection if enabled + if ((this.config.useLLM || options?.forceLLM) && this.llmAdapter?.isAvailable()) { + try { + console.log('[Reflection] Using LLM-powered deep reflection'); + return await this.llmReflectImpl(agentId); + } catch (error) { + console.error('[Reflection] LLM reflection failed:', error); + if (!this.config.llmFallbackToRules) { + throw error; + } + console.log('[Reflection] Falling back to rule-based analysis'); + } + } + + // Rule-based reflection (original implementation) + return this.ruleBasedReflect(agentId); + } + + /** + * LLM-powered deep reflection implementation. + * Uses semantic analysis for pattern detection and improvement suggestions. + */ + private async llmReflectImpl(agentId: string): Promise { + const memoryMgr = getMemoryManager(); + const identityMgr = getAgentIdentityManager(); + + // 1. Gather context for LLM analysis + const allMemories = await memoryMgr.getAll(agentId, { limit: 100 }); + const context = this.buildReflectionContext(agentId, allMemories); + + // 2. Call LLM for deep reflection + const llmResponse = await llmReflect(context, this.llmAdapter!); + + // 3. Parse LLM response + const { patterns, improvements } = this.parseLLMResponse(llmResponse); + + // 4. Propose identity changes if patterns warrant it + const identityProposals: IdentityChangeProposal[] = []; + if (this.config.allowSoulModification) { + const proposals = this.proposeIdentityChanges(agentId, patterns, identityMgr); + identityProposals.push(...proposals); + } + + // 5. Save reflection insights as memories + let newMemories = 0; + for (const pattern of patterns.filter(p => p.frequency >= 2)) { + await memoryMgr.save({ + agentId, + content: `[LLM反思] ${pattern.observation} (出现${pattern.frequency}次, ${pattern.sentiment === 'positive' ? '正面' : pattern.sentiment === 'negative' ? '负面' : '中性'})`, + type: 'lesson', + importance: pattern.sentiment === 'negative' ? 8 : 5, + source: 'llm-reflection', + tags: ['reflection', 'pattern', 'llm'], + }); + newMemories++; + } + + for (const improvement of improvements.filter(i => i.priority === 'high')) { + await memoryMgr.save({ + agentId, + content: `[LLM建议] [${improvement.area}] ${improvement.suggestion}`, + type: 'lesson', + importance: 7, + source: 'llm-reflection', + tags: ['reflection', 'improvement', 'llm'], + }); + newMemories++; + } + + // 6. Build result + const result: ReflectionResult = { + patterns, + improvements, + identityProposals, + newMemories, + timestamp: new Date().toISOString(), + }; + + // 7. Update state and history + this.state.conversationsSinceReflection = 0; + this.state.lastReflectionTime = result.timestamp; + this.state.lastReflectionAgentId = agentId; + this.saveState(); + + this.history.push(result); + if (this.history.length > 20) { + this.history = this.history.slice(-10); + } + this.saveHistory(); + + console.log( + `[Reflection] LLM complete: ${patterns.length} patterns, ${improvements.length} improvements, ` + + `${identityProposals.length} proposals, ${newMemories} memories saved` + ); + + return result; + } + + /** + * Build context string for LLM reflection. + */ + private buildReflectionContext(agentId: string, memories: MemoryEntry[]): string { + const memorySummary = memories.slice(0, 50).map(m => + `[${m.type}] ${m.content} (重要性: ${m.importance}, 访问: ${m.accessCount}次)` + ).join('\n'); + + const typeStats = new Map(); + for (const m of memories) { + typeStats.set(m.type, (typeStats.get(m.type) || 0) + 1); + } + + const recentHistory = this.history.slice(-3).map(h => + `上次反思(${h.timestamp}): ${h.patterns.length}个模式, ${h.improvements.length}个建议` + ).join('\n'); + + return ` +Agent ID: ${agentId} +记忆总数: ${memories.length} +记忆类型分布: ${[...typeStats.entries()].map(([k, v]) => `${k}:${v}`).join(', ')} + +最近记忆: +${memorySummary} + +历史反思: +${recentHistory || '无'} +`; + } + + /** + * Parse LLM response into structured reflection data. + */ + private parseLLMResponse(response: string): { + patterns: PatternObservation[]; + improvements: ImprovementSuggestion[]; + } { + const patterns: PatternObservation[] = []; + const improvements: ImprovementSuggestion[] = []; + + try { + // Try to extract JSON from response + const jsonMatch = response.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + + if (Array.isArray(parsed.patterns)) { + for (const p of parsed.patterns) { + patterns.push({ + observation: p.observation || p.observation || '未知模式', + frequency: p.frequency || 1, + sentiment: p.sentiment || 'neutral', + evidence: Array.isArray(p.evidence) ? p.evidence : [], + }); + } + } + + if (Array.isArray(parsed.improvements)) { + for (const i of parsed.improvements) { + improvements.push({ + area: i.area || '通用', + suggestion: i.suggestion || i.suggestion || '', + priority: i.priority || 'medium', + }); + } + } + } + } catch (error) { + console.warn('[Reflection] Failed to parse LLM response as JSON:', error); + + // Fallback: extract text patterns + if (response.includes('模式') || response.includes('pattern')) { + patterns.push({ + observation: 'LLM 分析完成,但未能解析结构化数据', + frequency: 1, + sentiment: 'neutral', + evidence: [response.slice(0, 200)], + }); + } + } + + // Ensure we have at least some output + if (patterns.length === 0) { + patterns.push({ + observation: 'LLM 反思完成,未检测到显著模式', + frequency: 1, + sentiment: 'neutral', + evidence: [], + }); + } + + return { patterns, improvements }; + } + + /** + * Rule-based reflection (original implementation). + */ + private async ruleBasedReflect(agentId: string): Promise { const memoryMgr = getMemoryManager(); const identityMgr = getAgentIdentityManager();