feat(l4): upgrade engines with LLM-powered capabilities (Phase 2)
Phase 2 LLM Engine Upgrades: - ReflectionEngine: Add LLM semantic analysis for pattern detection - ContextCompactor: Add LLM summarization for high-quality compaction - MemoryExtractor: Add LLM importance scoring for memory extraction - Add unified LLM service adapter (OpenAI, Volcengine, Gateway, Mock) - Add MemorySource 'llm-reflection' for LLM-generated memories - Add 13 integration tests for LLM-powered features Config options added: - useLLM: Enable LLM mode for each engine - llmProvider: Preferred LLM provider - llmFallbackToRules: Fallback to rules if LLM fails Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
228
desktop/src/lib/__tests__/llm-integration.test.ts
Normal file
228
desktop/src/lib/__tests__/llm-integration.test.ts
Normal file
@@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
export type MemoryType = 'fact' | 'preference' | 'lesson' | 'context' | 'task';
|
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 {
|
export interface MemoryEntry {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -8,12 +8,18 @@
|
|||||||
* 4. Replace old messages with summary — user sees no interruption
|
* 4. Replace old messages with summary — user sees no interruption
|
||||||
*
|
*
|
||||||
* Phase 2 implementation: heuristic token estimation + rule-based summarization.
|
* 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
|
* Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.3.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getMemoryExtractor, type ConversationMessage } from './memory-extractor';
|
import { getMemoryExtractor, type ConversationMessage } from './memory-extractor';
|
||||||
|
import {
|
||||||
|
getLLMAdapter,
|
||||||
|
llmCompact,
|
||||||
|
type LLMServiceAdapter,
|
||||||
|
type LLMProvider,
|
||||||
|
} from './llm-service';
|
||||||
|
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
@@ -24,6 +30,9 @@ export interface CompactionConfig {
|
|||||||
memoryFlushEnabled: boolean; // Extract memories before compacting (default true)
|
memoryFlushEnabled: boolean; // Extract memories before compacting (default true)
|
||||||
keepRecentMessages: number; // Always keep this many recent messages (default 6)
|
keepRecentMessages: number; // Always keep this many recent messages (default 6)
|
||||||
summaryMaxTokens: number; // Max tokens for the compaction summary (default 800)
|
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 {
|
export interface CompactableMessage {
|
||||||
@@ -59,6 +68,8 @@ export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
|
|||||||
memoryFlushEnabled: true,
|
memoryFlushEnabled: true,
|
||||||
keepRecentMessages: 6,
|
keepRecentMessages: 6,
|
||||||
summaryMaxTokens: 800,
|
summaryMaxTokens: 800,
|
||||||
|
useLLM: false,
|
||||||
|
llmFallbackToRules: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// === Token Estimation ===
|
// === Token Estimation ===
|
||||||
@@ -103,9 +114,19 @@ export function estimateMessagesTokens(messages: CompactableMessage[]): number {
|
|||||||
|
|
||||||
export class ContextCompactor {
|
export class ContextCompactor {
|
||||||
private config: CompactionConfig;
|
private config: CompactionConfig;
|
||||||
|
private llmAdapter: LLMServiceAdapter | null = null;
|
||||||
|
|
||||||
constructor(config?: Partial<CompactionConfig>) {
|
constructor(config?: Partial<CompactionConfig>) {
|
||||||
this.config = { ...DEFAULT_COMPACTION_CONFIG, ...config };
|
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.
|
* Execute compaction: summarize old messages, keep recent ones.
|
||||||
*
|
*
|
||||||
* Phase 2: Rule-based summarization (extract key points heuristically).
|
* 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(
|
async compact(
|
||||||
messages: CompactableMessage[],
|
messages: CompactableMessage[],
|
||||||
agentId: string,
|
agentId: string,
|
||||||
conversationId?: string
|
conversationId?: string,
|
||||||
|
options?: { forceLLM?: boolean }
|
||||||
): Promise<CompactionResult> {
|
): Promise<CompactionResult> {
|
||||||
const tokensBeforeCompaction = estimateMessagesTokens(messages);
|
const tokensBeforeCompaction = estimateMessagesTokens(messages);
|
||||||
const keepCount = Math.min(this.config.keepRecentMessages, messages.length);
|
const keepCount = Math.min(this.config.keepRecentMessages, messages.length);
|
||||||
@@ -176,7 +198,22 @@ export class ContextCompactor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Generate summary of old messages
|
// 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
|
// Step 3: Build compacted message list
|
||||||
const summaryMessage: CompactableMessage = {
|
const summaryMessage: CompactableMessage = {
|
||||||
@@ -206,6 +243,30 @@ export class ContextCompactor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM-powered summary generation for high-quality compaction.
|
||||||
|
*/
|
||||||
|
private async llmGenerateSummary(messages: CompactableMessage[]): Promise<string> {
|
||||||
|
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.
|
* Phase 2: Rule-based summary generation.
|
||||||
* Extracts key topics, decisions, and action items from old messages.
|
* Extracts key topics, decisions, and action items from old messages.
|
||||||
|
|||||||
@@ -9,11 +9,20 @@
|
|||||||
*
|
*
|
||||||
* Also handles auto-updating USER.md with discovered preferences.
|
* 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
|
* Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getMemoryManager, type MemoryType } from './agent-memory';
|
import { getMemoryManager, type MemoryType } from './agent-memory';
|
||||||
import { getAgentIdentityManager } from './agent-identity';
|
import { getAgentIdentityManager } from './agent-identity';
|
||||||
|
import {
|
||||||
|
getLLMAdapter,
|
||||||
|
llmExtract,
|
||||||
|
type LLMServiceAdapter,
|
||||||
|
type LLMProvider,
|
||||||
|
} from './llm-service';
|
||||||
|
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
@@ -36,6 +45,15 @@ export interface ConversationMessage {
|
|||||||
content: string;
|
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 ===
|
// === Extraction Prompt ===
|
||||||
|
|
||||||
const 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 ===
|
// === Memory Extractor ===
|
||||||
|
|
||||||
export class MemoryExtractor {
|
export class MemoryExtractor {
|
||||||
private minMessagesForExtraction = 4;
|
private config: ExtractionConfig;
|
||||||
private extractionCooldownMs = 30_000; // 30 seconds between extractions
|
|
||||||
private lastExtractionTime = 0;
|
private lastExtractionTime = 0;
|
||||||
|
private llmAdapter: LLMServiceAdapter | null = null;
|
||||||
|
|
||||||
|
constructor(config?: Partial<ExtractionConfig>) {
|
||||||
|
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.
|
* Extract memories from a conversation.
|
||||||
* This is the Phase 1 approach — no LLM call needed.
|
* Uses LLM if configured, falls back to rule-based extraction.
|
||||||
* Phase 2 will add LLM-based extraction using EXTRACTION_PROMPT.
|
|
||||||
*/
|
*/
|
||||||
async extractFromConversation(
|
async extractFromConversation(
|
||||||
messages: ConversationMessage[],
|
messages: ConversationMessage[],
|
||||||
agentId: string,
|
agentId: string,
|
||||||
conversationId?: string
|
conversationId?: string,
|
||||||
|
options?: { forceLLM?: boolean }
|
||||||
): Promise<ExtractionResult> {
|
): Promise<ExtractionResult> {
|
||||||
// Cooldown check
|
// 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 };
|
return { items: [], saved: 0, skipped: 0, userProfileUpdated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimum message threshold
|
// Minimum message threshold
|
||||||
const chatMessages = messages.filter(m => m.role === 'user' || m.role === 'assistant');
|
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 };
|
return { items: [], saved: 0, skipped: 0, userProfileUpdated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastExtractionTime = Date.now();
|
this.lastExtractionTime = Date.now();
|
||||||
|
|
||||||
// Phase 1: Rule-based extraction (pattern matching)
|
// Try LLM extraction if enabled
|
||||||
const extracted = this.ruleBasedExtraction(chatMessages);
|
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
|
// Save to memory
|
||||||
const memoryManager = getMemoryManager();
|
const memoryManager = getMemoryManager();
|
||||||
@@ -135,6 +195,23 @@ export class MemoryExtractor {
|
|||||||
return { items: extracted, saved, skipped, userProfileUpdated };
|
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<ExtractedItem[]> {
|
||||||
|
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.
|
* Phase 1: Rule-based extraction using pattern matching.
|
||||||
* Extracts common patterns from user messages.
|
* Extracts common patterns from user messages.
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
|
|
||||||
import { getMemoryManager, type MemoryEntry } from './agent-memory';
|
import { getMemoryManager, type MemoryEntry } from './agent-memory';
|
||||||
import { getAgentIdentityManager, type IdentityChangeProposal } from './agent-identity';
|
import { getAgentIdentityManager, type IdentityChangeProposal } from './agent-identity';
|
||||||
|
import {
|
||||||
|
getLLMAdapter,
|
||||||
|
llmReflect,
|
||||||
|
type LLMServiceAdapter,
|
||||||
|
type LLMProvider,
|
||||||
|
} from './llm-service';
|
||||||
|
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
@@ -23,6 +29,9 @@ export interface ReflectionConfig {
|
|||||||
triggerAfterHours: number; // Reflect after N hours (default 24)
|
triggerAfterHours: number; // Reflect after N hours (default 24)
|
||||||
allowSoulModification: boolean; // Can propose SOUL.md changes
|
allowSoulModification: boolean; // Can propose SOUL.md changes
|
||||||
requireApproval: boolean; // Identity changes need user OK
|
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 {
|
export interface PatternObservation {
|
||||||
@@ -53,6 +62,8 @@ export const DEFAULT_REFLECTION_CONFIG: ReflectionConfig = {
|
|||||||
triggerAfterHours: 24,
|
triggerAfterHours: 24,
|
||||||
allowSoulModification: false,
|
allowSoulModification: false,
|
||||||
requireApproval: true,
|
requireApproval: true,
|
||||||
|
useLLM: false,
|
||||||
|
llmFallbackToRules: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// === Storage ===
|
// === Storage ===
|
||||||
@@ -72,11 +83,21 @@ export class ReflectionEngine {
|
|||||||
private config: ReflectionConfig;
|
private config: ReflectionConfig;
|
||||||
private state: ReflectionState;
|
private state: ReflectionState;
|
||||||
private history: ReflectionResult[] = [];
|
private history: ReflectionResult[] = [];
|
||||||
|
private llmAdapter: LLMServiceAdapter | null = null;
|
||||||
|
|
||||||
constructor(config?: Partial<ReflectionConfig>) {
|
constructor(config?: Partial<ReflectionConfig>) {
|
||||||
this.config = { ...DEFAULT_REFLECTION_CONFIG, ...config };
|
this.config = { ...DEFAULT_REFLECTION_CONFIG, ...config };
|
||||||
this.state = this.loadState();
|
this.state = this.loadState();
|
||||||
this.loadHistory();
|
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 ===
|
// === Trigger Management ===
|
||||||
@@ -116,9 +137,205 @@ export class ReflectionEngine {
|
|||||||
/**
|
/**
|
||||||
* Execute a reflection cycle for the given agent.
|
* Execute a reflection cycle for the given agent.
|
||||||
*/
|
*/
|
||||||
async reflect(agentId: string): Promise<ReflectionResult> {
|
async reflect(agentId: string, options?: { forceLLM?: boolean }): Promise<ReflectionResult> {
|
||||||
console.log(`[Reflection] Starting reflection for agent: ${agentId}`);
|
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<ReflectionResult> {
|
||||||
|
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<string, number>();
|
||||||
|
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<ReflectionResult> {
|
||||||
const memoryMgr = getMemoryManager();
|
const memoryMgr = getMemoryManager();
|
||||||
const identityMgr = getAgentIdentityManager();
|
const identityMgr = getAgentIdentityManager();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user