feat(l4): add LLM service adapter for Phase 2 engine upgrades
- Unified interface for OpenAI, Volcengine, Gateway, and Mock providers - Structured LLMMessage and LLMResponse types - Configurable via localStorage with API key security - Built-in prompt templates for reflection, compaction, extraction - Helper functions: llmReflect(), llmCompact(), llmExtract() This adapter enables the 3 engines to be upgraded from rule-based to LLM-powered in Phase 2.1-2.3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
500
desktop/src/lib/llm-service.ts
Normal file
500
desktop/src/lib/llm-service.ts
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
/**
|
||||||
|
* LLM Service Adapter - Unified LLM interface for L4 self-evolution engines
|
||||||
|
*
|
||||||
|
* Provides a unified interface for:
|
||||||
|
* - ReflectionEngine: Semantic analysis + deep reflection
|
||||||
|
* - ContextCompactor: High-quality summarization
|
||||||
|
* - MemoryExtractor: Semantic importance scoring
|
||||||
|
*
|
||||||
|
* Supports multiple backends:
|
||||||
|
* - OpenAI (GPT-4, GPT-3.5)
|
||||||
|
* - Volcengine (Doubao)
|
||||||
|
* - OpenFang Gateway (passthrough)
|
||||||
|
*
|
||||||
|
* Part of ZCLAW L4 Self-Evolution capability.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// === Types ===
|
||||||
|
|
||||||
|
export type LLMProvider = 'openai' | 'volcengine' | 'gateway' | 'mock';
|
||||||
|
|
||||||
|
export interface LLMConfig {
|
||||||
|
provider: LLMProvider;
|
||||||
|
model?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
apiBase?: string;
|
||||||
|
maxTokens?: number;
|
||||||
|
temperature?: number;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LLMMessage {
|
||||||
|
role: 'system' | 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LLMResponse {
|
||||||
|
content: string;
|
||||||
|
tokensUsed?: {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
};
|
||||||
|
model?: string;
|
||||||
|
latencyMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LLMServiceAdapter {
|
||||||
|
complete(messages: LLMMessage[], options?: Partial<LLMConfig>): Promise<LLMResponse>;
|
||||||
|
isAvailable(): boolean;
|
||||||
|
getProvider(): LLMProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Default Configs ===
|
||||||
|
|
||||||
|
const DEFAULT_CONFIGS: Record<LLMProvider, LLMConfig> = {
|
||||||
|
openai: {
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4o-mini',
|
||||||
|
apiBase: 'https://api.openai.com/v1',
|
||||||
|
maxTokens: 2000,
|
||||||
|
temperature: 0.7,
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
volcengine: {
|
||||||
|
provider: 'volcengine',
|
||||||
|
model: 'doubao-pro-32k',
|
||||||
|
apiBase: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||||
|
maxTokens: 2000,
|
||||||
|
temperature: 0.7,
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
provider: 'gateway',
|
||||||
|
apiBase: '/api/llm',
|
||||||
|
maxTokens: 2000,
|
||||||
|
temperature: 0.7,
|
||||||
|
timeout: 60000,
|
||||||
|
},
|
||||||
|
mock: {
|
||||||
|
provider: 'mock',
|
||||||
|
maxTokens: 100,
|
||||||
|
temperature: 0,
|
||||||
|
timeout: 100,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Storage ===
|
||||||
|
|
||||||
|
const LLM_CONFIG_KEY = 'zclaw-llm-config';
|
||||||
|
|
||||||
|
// === Mock Adapter (for testing) ===
|
||||||
|
|
||||||
|
class MockLLMAdapter implements LLMServiceAdapter {
|
||||||
|
private config: LLMConfig;
|
||||||
|
|
||||||
|
constructor(config: LLMConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async complete(messages: LLMMessage[]): Promise<LLMResponse> {
|
||||||
|
// Simulate latency
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
const content = lastMessage?.content || '';
|
||||||
|
|
||||||
|
// Generate mock response based on content type
|
||||||
|
let response = '[Mock LLM Response] ';
|
||||||
|
|
||||||
|
if (content.includes('reflect') || content.includes('反思')) {
|
||||||
|
response += JSON.stringify({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
observation: '用户经常询问代码优化相关问题',
|
||||||
|
frequency: 5,
|
||||||
|
sentiment: 'positive',
|
||||||
|
evidence: ['多次讨论性能优化', '关注代码质量'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
improvements: [
|
||||||
|
{
|
||||||
|
area: '代码解释',
|
||||||
|
suggestion: '可以提供更详细的代码注释',
|
||||||
|
priority: 'medium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
identityProposals: [],
|
||||||
|
});
|
||||||
|
} else if (content.includes('summarize') || content.includes('摘要')) {
|
||||||
|
response += '这是一个关于对话内容的摘要,包含了主要讨论的要点和结论。';
|
||||||
|
} else if (content.includes('importance') || content.includes('重要性')) {
|
||||||
|
response += JSON.stringify({
|
||||||
|
memories: [
|
||||||
|
{ content: '用户偏好简洁的回答', importance: 7, type: 'preference' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response += 'Processed: ' + content.slice(0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: response,
|
||||||
|
tokensUsed: { input: content.length / 4, output: response.length / 4 },
|
||||||
|
model: 'mock-model',
|
||||||
|
latencyMs: 50,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isAvailable(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProvider(): LLMProvider {
|
||||||
|
return 'mock';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === OpenAI Adapter ===
|
||||||
|
|
||||||
|
class OpenAILLMAdapter implements LLMServiceAdapter {
|
||||||
|
private config: LLMConfig;
|
||||||
|
|
||||||
|
constructor(config: LLMConfig) {
|
||||||
|
this.config = { ...DEFAULT_CONFIGS.openai, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
async complete(messages: LLMMessage[], options?: Partial<LLMConfig>): Promise<LLMResponse> {
|
||||||
|
const config = { ...this.config, ...options };
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
if (!config.apiKey) {
|
||||||
|
throw new Error('[OpenAI] API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${config.apiBase}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: config.model,
|
||||||
|
messages,
|
||||||
|
max_tokens: config.maxTokens,
|
||||||
|
temperature: config.temperature,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(config.timeout || 30000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`[OpenAI] API error: ${response.status} - ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const latencyMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: data.choices[0]?.message?.content || '',
|
||||||
|
tokensUsed: {
|
||||||
|
input: data.usage?.prompt_tokens || 0,
|
||||||
|
output: data.usage?.completion_tokens || 0,
|
||||||
|
},
|
||||||
|
model: data.model,
|
||||||
|
latencyMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isAvailable(): boolean {
|
||||||
|
return !!this.config.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProvider(): LLMProvider {
|
||||||
|
return 'openai';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Volcengine Adapter ===
|
||||||
|
|
||||||
|
class VolcengineLLMAdapter implements LLMServiceAdapter {
|
||||||
|
private config: LLMConfig;
|
||||||
|
|
||||||
|
constructor(config: LLMConfig) {
|
||||||
|
this.config = { ...DEFAULT_CONFIGS.volcengine, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
async complete(messages: LLMMessage[], options?: Partial<LLMConfig>): Promise<LLMResponse> {
|
||||||
|
const config = { ...this.config, ...options };
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
if (!config.apiKey) {
|
||||||
|
throw new Error('[Volcengine] API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${config.apiBase}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: config.model,
|
||||||
|
messages,
|
||||||
|
max_tokens: config.maxTokens,
|
||||||
|
temperature: config.temperature,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(config.timeout || 30000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`[Volcengine] API error: ${response.status} - ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const latencyMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: data.choices[0]?.message?.content || '',
|
||||||
|
tokensUsed: {
|
||||||
|
input: data.usage?.prompt_tokens || 0,
|
||||||
|
output: data.usage?.completion_tokens || 0,
|
||||||
|
},
|
||||||
|
model: data.model,
|
||||||
|
latencyMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isAvailable(): boolean {
|
||||||
|
return !!this.config.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProvider(): LLMProvider {
|
||||||
|
return 'volcengine';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Gateway Adapter (pass through to OpenFang) ===
|
||||||
|
|
||||||
|
class GatewayLLMAdapter implements LLMServiceAdapter {
|
||||||
|
private config: LLMConfig;
|
||||||
|
|
||||||
|
constructor(config: LLMConfig) {
|
||||||
|
this.config = { ...DEFAULT_CONFIGS.gateway, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
async complete(messages: LLMMessage[], options?: Partial<LLMConfig>): Promise<LLMResponse> {
|
||||||
|
const config = { ...this.config, ...options };
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const response = await fetch(`${config.apiBase}/complete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages,
|
||||||
|
max_tokens: config.maxTokens,
|
||||||
|
temperature: config.temperature,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(config.timeout || 60000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`[Gateway] API error: ${response.status} - ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const latencyMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: data.content || data.choices?.[0]?.message?.content || '',
|
||||||
|
tokensUsed: data.tokensUsed || { input: 0, output: 0 },
|
||||||
|
model: data.model,
|
||||||
|
latencyMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isAvailable(): boolean {
|
||||||
|
// Gateway is available if we're connected to OpenFang
|
||||||
|
return typeof window !== 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
getProvider(): LLMProvider {
|
||||||
|
return 'gateway';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Factory ===
|
||||||
|
|
||||||
|
let cachedAdapter: LLMServiceAdapter | null = null;
|
||||||
|
|
||||||
|
export function createLLMAdapter(config?: Partial<LLMConfig>): LLMServiceAdapter {
|
||||||
|
const savedConfig = loadConfig();
|
||||||
|
const finalConfig = { ...savedConfig, ...config };
|
||||||
|
|
||||||
|
switch (finalConfig.provider) {
|
||||||
|
case 'openai':
|
||||||
|
return new OpenAILLMAdapter(finalConfig);
|
||||||
|
case 'volcengine':
|
||||||
|
return new VolcengineLLMAdapter(finalConfig);
|
||||||
|
case 'gateway':
|
||||||
|
return new GatewayLLMAdapter(finalConfig);
|
||||||
|
case 'mock':
|
||||||
|
default:
|
||||||
|
return new MockLLMAdapter(finalConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLLMAdapter(): LLMServiceAdapter {
|
||||||
|
if (!cachedAdapter) {
|
||||||
|
cachedAdapter = createLLMAdapter();
|
||||||
|
}
|
||||||
|
return cachedAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetLLMAdapter(): void {
|
||||||
|
cachedAdapter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Config Management ===
|
||||||
|
|
||||||
|
export function loadConfig(): LLMConfig {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return DEFAULT_CONFIGS.mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(LLM_CONFIG_KEY);
|
||||||
|
if (saved) {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to mock for safety
|
||||||
|
return DEFAULT_CONFIGS.mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveConfig(config: LLMConfig): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Don't save API key to localStorage for security
|
||||||
|
const safeConfig = { ...config };
|
||||||
|
delete safeConfig.apiKey;
|
||||||
|
|
||||||
|
localStorage.setItem(LLM_CONFIG_KEY, JSON.stringify(safeConfig));
|
||||||
|
resetLLMAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Prompt Templates ===
|
||||||
|
|
||||||
|
export const LLM_PROMPTS = {
|
||||||
|
reflection: {
|
||||||
|
system: `你是一个 AI Agent 的自我反思引擎。分析最近的对话历史,识别行为模式,并生成改进建议。
|
||||||
|
|
||||||
|
输出 JSON 格式:
|
||||||
|
{
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"observation": "观察到的模式描述",
|
||||||
|
"frequency": 数字,
|
||||||
|
"sentiment": "positive/negative/neutral",
|
||||||
|
"evidence": ["证据1", "证据2"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"improvements": [
|
||||||
|
{
|
||||||
|
"area": "改进领域",
|
||||||
|
"suggestion": "具体建议",
|
||||||
|
"priority": "high/medium/low"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"identityProposals": []
|
||||||
|
}`,
|
||||||
|
user: (context: string) => `分析以下对话历史,进行自我反思:
|
||||||
|
|
||||||
|
${context}
|
||||||
|
|
||||||
|
请识别行为模式(积极和消极),并提供具体的改进建议。`,
|
||||||
|
},
|
||||||
|
|
||||||
|
compaction: {
|
||||||
|
system: `你是一个对话摘要专家。将长对话压缩为简洁的摘要,保留关键信息。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 保留所有重要决策和结论
|
||||||
|
2. 保留用户偏好和约束
|
||||||
|
3. 保留未完成的任务
|
||||||
|
4. 保持时间顺序
|
||||||
|
5. 摘要应能在后续对话中替代原始内容`,
|
||||||
|
user: (messages: string) => `请将以下对话压缩为简洁摘要,保留关键信息:
|
||||||
|
|
||||||
|
${messages}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
extraction: {
|
||||||
|
system: `你是一个记忆提取专家。从对话中提取值得长期记住的信息。
|
||||||
|
|
||||||
|
提取类型:
|
||||||
|
- fact: 用户告知的事实(如"我的公司叫XXX")
|
||||||
|
- preference: 用户的偏好(如"我喜欢简洁的回答")
|
||||||
|
- lesson: 本次对话的经验教训
|
||||||
|
- task: 未完成的任务或承诺
|
||||||
|
|
||||||
|
输出 JSON 数组:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": "记忆内容",
|
||||||
|
"type": "fact/preference/lesson/task",
|
||||||
|
"importance": 1-10,
|
||||||
|
"tags": ["标签1", "标签2"]
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
user: (conversation: string) => `从以下对话中提取值得长期记住的信息:
|
||||||
|
|
||||||
|
${conversation}
|
||||||
|
|
||||||
|
如果没有值得记忆的内容,返回空数组 []。`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
export async function llmReflect(context: string, adapter?: LLMServiceAdapter): Promise<string> {
|
||||||
|
const llm = adapter || getLLMAdapter();
|
||||||
|
|
||||||
|
const response = await llm.complete([
|
||||||
|
{ role: 'system', content: LLM_PROMPTS.reflection.system },
|
||||||
|
{ role: 'user', content: LLM_PROMPTS.reflection.user(context) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function llmCompact(messages: string, adapter?: LLMServiceAdapter): Promise<string> {
|
||||||
|
const llm = adapter || getLLMAdapter();
|
||||||
|
|
||||||
|
const response = await llm.complete([
|
||||||
|
{ role: 'system', content: LLM_PROMPTS.compaction.system },
|
||||||
|
{ role: 'user', content: LLM_PROMPTS.compaction.user(messages) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function llmExtract(
|
||||||
|
conversation: string,
|
||||||
|
adapter?: LLMServiceAdapter
|
||||||
|
): Promise<string> {
|
||||||
|
const llm = adapter || getLLMAdapter();
|
||||||
|
|
||||||
|
const response = await llm.complete([
|
||||||
|
{ role: 'system', content: LLM_PROMPTS.extraction.system },
|
||||||
|
{ role: 'user', content: LLM_PROMPTS.extraction.user(conversation) },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response.content;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user