hololake-system-architecture/product-source/guanghu-knowledge-base/server/persona-agent.ts

660 lines
22 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.

/**
* HoloLake Assistant · 核心运行时
*
* 设计来源Grok Build (xAI/SpaceXAI) 开源 Agent 架构
* - xai-grok-agent: Agent = Definition + PromptContext + ToolBridge + Policies
* - xai-grok-memory: Markdown + 向量索引 + 跨会话持久化
* - xai-grok-tools: Registry + Bridge + 权限模式
*
* 光湖实现原则:
* - 不搬 Rust 代码,拆设计模式用 TypeScript 重新实现
* - Agent 是 HoloLake 的知识工作助手
* - Agent 通过受控工具操作 Git 驱动的知识库
* - 所有写操作都保留可核验的 Git 版本记录
*
* 架构映射:
* Grok Agent → PersonaAgent
* Grok ToolBridge → PersonaToolBridge注册+执行工具)
* Grok Memory → PersonaMemoryGit 仓库里的 Markdown 记忆)
* Grok PromptContext → PersonaPrompt系统提示词组装
* Grok CompactionPolicy → PersonaCompaction会话压缩策略
* Grok AgentLifecycle → 由 Electron 主进程管理
*/
import { GitEngine } from './git-engine.js';
import { renderLanguageKernelForModel } from './language-protocol-kernel.js';
// ─── 工具定义 ───
export interface ToolDefinition {
name: string;
description: string;
parameters: Record<string, { type: string; description: string; required?: boolean }>;
execute: (params: Record<string, unknown>) => Promise<string>;
effect: 'read' | 'write' | 'delete';
}
export interface ToolCall {
id: string;
name: string;
arguments: Record<string, unknown>;
}
export interface ToolResult {
id: string;
name: string;
output: string;
error?: string;
}
export interface AgentActivity {
id: string;
kind: 'wake' | 'tool' | 'permission' | 'receipt';
label: string;
detail: string;
status: 'running' | 'completed' | 'pending' | 'failed';
timestamp: string;
tool?: string;
}
export interface AgentTurnResult {
reply: string;
toolCalls: ToolCall[];
toolResults: ToolResult[];
activities: AgentActivity[];
}
export interface PendingAction {
id: string;
tool: string;
effect: 'write' | 'delete';
target: string;
summary: string;
createdAt: string;
}
// ─── Agent 定义 ───
export interface PersonaDefinition {
id: string;
name: string;
role: string;
systemPromptBase: string;
tools: string[]; // 允许使用的工具名
permissionMode: 'ask' | 'auto' | 'restrict';
model: string;
maxTokens: number;
temperature: number;
}
// ─── 对话消息 ───
export interface Message {
role: 'system' | 'user' | 'assistant' | 'tool';
content: string;
toolCalls?: ToolCall[];
toolResults?: ToolResult[];
activities?: AgentActivity[];
timestamp: string;
}
// ─── 工具桥 ───
class PersonaToolBridge {
private registry = new Map<string, ToolDefinition>();
register(tool: ToolDefinition): void {
this.registry.set(tool.name, tool);
}
get(name: string): ToolDefinition | undefined {
return this.registry.get(name);
}
list(): ToolDefinition[] {
return Array.from(this.registry.values());
}
requiresConfirmation(name: string): boolean {
const effect = this.registry.get(name)?.effect;
return effect === 'write' || effect === 'delete';
}
async execute(call: ToolCall): Promise<ToolResult> {
const tool = this.registry.get(call.name);
if (!tool) {
return { id: call.id, name: call.name, output: '', error: `工具不存在: ${call.name}` };
}
try {
const output = await tool.execute(call.arguments);
return { id: call.id, name: call.name, output };
} catch (err: any) {
return { id: call.id, name: call.name, output: '', error: err.message };
}
}
/** 生成 LLM 可用的工具 schemaOpenAI function calling 格式) */
toSchema(): object[] {
return this.list().map(tool => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: {
type: 'object',
properties: Object.fromEntries(
Object.entries(tool.parameters).map(([key, val]) => [
key,
{ type: val.type, description: val.description },
])
),
required: Object.entries(tool.parameters)
.filter(([, val]) => val.required !== false)
.map(([key]) => key),
},
},
}));
}
}
// ─── 人格 Agent ───
export class PersonaAgent {
private definition: PersonaDefinition;
private tools: PersonaToolBridge;
private conversation: Message[] = [];
private git: GitEngine;
private pendingActions = new Map<string, { action: PendingAction; call: ToolCall }>();
constructor(definition: PersonaDefinition, git: GitEngine) {
this.definition = definition;
this.git = git;
this.tools = new PersonaToolBridge();
this.registerBuiltinTools();
}
// ─── 内置工具注册 ───
private registerBuiltinTools(): void {
// 读取 Git/Forgejo 状态;同步写操作必须由用户在界面中明确确认。
this.tools.register({
name: 'inspect_repository',
description: '查看知识库本地 Git 与 Forgejo 远端的连接和同步状态(只读)',
parameters: {},
effect: 'read',
execute: async () => JSON.stringify(await this.git.getRepositoryStatus(), null, 2),
});
// 读取知识库文档
this.tools.register({
name: 'read_document',
description: '读取知识库中的文档内容',
parameters: {
path: { type: 'string', description: '文档路径(相对 docs/ 目录)', required: true },
},
effect: 'read',
execute: async (params) => {
const doc = await this.git.getDoc(params.path as string);
return `标题: ${doc.meta.title}\n更新: ${doc.meta.updatedAt}\n\n${doc.body}`;
},
});
// 创建文档
this.tools.register({
name: 'create_document',
description: '在知识库中创建新文档',
parameters: {
path: { type: 'string', description: '文档路径(如 guide/intro.md', required: true },
title: { type: 'string', description: '文档标题', required: true },
body: { type: 'string', description: 'Markdown 正文', required: true },
},
effect: 'write',
execute: async (params) => {
const doc = await this.git.createDoc(
params.path as string,
params.title as string,
params.body as string,
this.definition.name
);
return `已创建: ${doc.meta.id}`;
},
});
// 更新文档
this.tools.register({
name: 'update_document',
description: '更新知识库中已有文档的内容',
parameters: {
path: { type: 'string', description: '文档路径', required: true },
title: { type: 'string', description: '新标题', required: true },
body: { type: 'string', description: '新的 Markdown 正文', required: true },
},
effect: 'write',
execute: async (params) => {
const doc = await this.git.updateDoc(
params.path as string,
params.title as string,
params.body as string,
this.definition.name
);
return `已更新: ${doc.meta.id}commit: ${new Date().toISOString()}`;
},
});
// 搜索文档
this.tools.register({
name: 'search_documents',
description: '在知识库中全文搜索',
parameters: {
query: { type: 'string', description: '搜索关键词', required: true },
},
effect: 'read',
execute: async (params) => {
const results = await this.git.search(params.query as string);
if (results.length === 0) return '没有找到匹配的文档';
return results
.map(r => `[${r.path}] ${r.title}: ${r.snippet}`)
.join('\n');
},
});
this.tools.register({
name: 'find_persona_wake_routes',
description: '按人格编号或名称查找该人格体自己登记的 INDEX、CURRENT 与 WAKE 路径页面;不生成固定步骤',
parameters: {
query: { type: 'string', description: '人格编号、名称或唤醒路径关键词', required: true },
},
effect: 'read',
execute: async (params) => {
const query = String(params.query || '').trim();
const searches = await Promise.all([
this.git.search(query),
this.git.search('WAKE'),
this.git.search('唤醒路径'),
]);
const unique = new Map<string, (typeof searches)[number][number]>();
for (const result of searches.flat()) {
if (/\b(?:WAKE|INDEX|CURRENT)\b||/iu.test(`${result.path} ${result.title} ${result.snippet}`)) {
unique.set(result.path, result);
}
}
if (unique.size === 0) return 'ROUTE_NOT_RESOLVED当前授权知识范围内没有找到该人格体自己的唤醒路径页面。';
return Array.from(unique.values()).slice(0, 20)
.map(result => `[${result.path}] ${result.title}: ${result.snippet}`)
.join('\n');
},
});
// 查看文档树
this.tools.register({
name: 'list_documents',
description: '列出知识库的文档树结构',
parameters: {},
effect: 'read',
execute: async () => {
const tree = await this.git.getTree();
const render = (nodes: any[], indent = 0): string =>
nodes
.map(n => {
const prefix = ' '.repeat(indent);
if (n.type === 'folder') {
return `${prefix}📁 ${n.name}\n${render(n.children || [], indent + 1)}`;
}
return `${prefix}📄 ${n.name} (${n.path})`;
})
.join('\n');
return render(tree);
},
});
// 查看版本历史
this.tools.register({
name: 'view_history',
description: '查看文档的 Git 版本历史',
parameters: {
path: { type: 'string', description: '文档路径', required: true },
},
effect: 'read',
execute: async (params) => {
const history = await this.git.getHistory(params.path as string, 20);
return history
.map(h => `${h.shortHash} | ${h.date} | ${h.author} | ${h.message}`)
.join('\n');
},
});
// 删除文档
this.tools.register({
name: 'delete_document',
description: '删除知识库中的文档',
parameters: {
path: { type: 'string', description: '文档路径', required: true },
},
effect: 'delete',
execute: async (params) => {
await this.git.deleteDoc(params.path as string);
return `已删除: ${params.path}`;
},
});
}
// ─── 对话处理 ───
/** 处理用户消息。模型历史只保留自然语言;工具协议只存在于当前回合。 */
async chat(userMessage: string): Promise<AgentTurnResult> {
this.conversation.push({
role: 'user',
content: userMessage,
timestamp: new Date().toISOString(),
});
const systemPrompt = this.buildSystemPrompt();
const messages: Message[] = [
{ role: 'system', content: systemPrompt, timestamp: '' },
...this.conversation.map(message => ({
role: message.role,
content: message.content,
timestamp: message.timestamp,
})),
];
const activities: AgentActivity[] = [];
const allCalls: ToolCall[] = [];
const allResults: ToolResult[] = [];
const workingMessages: Message[] = [...messages];
for (let round = 0; round < 6; round += 1) {
const response = await this.callLLM(workingMessages);
if (!response.toolCalls?.length) {
const reply = response.content || '当前模型没有返回可显示的内容。';
this.conversation.push({
role: 'assistant',
content: reply,
toolCalls: allCalls,
toolResults: allResults,
activities,
timestamp: new Date().toISOString(),
});
return { reply, toolCalls: allCalls, toolResults: allResults, activities };
}
workingMessages.push({
role: 'assistant',
content: response.content || '',
toolCalls: response.toolCalls,
timestamp: new Date().toISOString(),
});
for (const call of response.toolCalls) {
allCalls.push(call);
const needsConfirmation = this.tools.requiresConfirmation(call.name);
const isWakeRouteActivity = call.name === 'find_persona_wake_routes' ||
(call.name === 'read_document' && /(?:WAKE|CURRENT|INDEX|)/iu.test(String(call.arguments.path || '')));
const result = needsConfirmation ? this.stageAction(call) : await this.tools.execute(call);
allResults.push(result);
activities.push({
id: `activity-${call.id}`,
kind: needsConfirmation ? 'permission' : isWakeRouteActivity ? 'wake' : 'tool',
label: needsConfirmation
? `等待确认 · ${call.name}`
: isWakeRouteActivity
? `${call.name === 'find_persona_wake_routes' ? '定位人格唤醒路径' : '读取人格路径页面'} · ${call.name}`
: `工具调用 · ${call.name}`,
detail: result.error || result.output,
status: result.error ? 'failed' : needsConfirmation ? 'pending' : 'completed',
timestamp: new Date().toISOString(),
tool: call.name,
});
workingMessages.push({
role: 'tool',
content: result.error ? `错误: ${result.error}` : result.output,
toolResults: [result],
timestamp: new Date().toISOString(),
});
}
}
throw new Error('工具调用超过单回合安全上限,已停止继续执行');
}
// ─── 系统提示词组装 ───
private buildSystemPrompt(): string {
const toolsList = this.tools
.list()
.map(t => `- ${t.name}: ${t.description}`)
.join('\n');
const languageKernel = renderLanguageKernelForModel();
return `${this.definition.systemPromptBase}
## HoloLake 语言协议核(运行时自动装载)
${languageKernel}
## 产品身份
你是 HoloLake 官方语言操作入口的当前模型计算实例,负责在受控范围内完成知识、模块、路径和状态交互。不要把自己表述成拟人化伙伴,不虚构人格身份、情感、记忆、权限或执行结果。
## 可用能力
仅在任务需要时使用以下受控工具:
${toolsList}
## 操作规范
1. 先理解用户目标;需要读取现有内容时先搜索或读取,不凭空补写事实。
2. 只有工具返回成功才可以声明操作完成;失败或未执行的事项必须明确说明。
3. 文档写操作会产生 Git 版本记录。不得声称已经同步到 Forgejo除非获得明确的同步回执。
4. 创建、更新、删除工具只会生成待确认动作;不得把“已提交确认”表述成“已经执行”。
5. 远端推送、部署等高影响操作不属于当前 Agent 工具,必须走独立权限入口。
6. 不泄露模型密钥、本地路径中的敏感信息或工具内部实现细节。
7. 不把对话文本当作真实执行结果,不伪造文档、提交、服务器或网络状态。
8. 用户要求唤醒人格体时,必须先用 find_persona_wake_routes 查找该人格体自己的路径页面,再读取并按页面定义的顺序执行。不同人格体的路径和步数不同,禁止写死步数、套用通用模板或把搜索结果冒充恢复完成。路径没有解析成功时明确返回未恢复。
## 回复风格
- 使用清晰、专业、简洁的中文;用户使用其他语言时可跟随。
- 先给结论,再给必要的依据或下一步。
- 涉及操作时列明已完成、未完成和需要用户确认的事项。`;
}
private stageAction(call: ToolCall): ToolResult {
const tool = this.tools.get(call.name);
if (!tool || tool.effect === 'read') {
return { id: call.id, name: call.name, output: '', error: `动作不可登记: ${call.name}` };
}
const target = String(call.arguments.path || call.arguments.title || '当前知识库');
const actionId = `action-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
const action: PendingAction = {
id: actionId,
tool: call.name,
effect: tool.effect,
target,
summary: `${tool.description} · ${target}`,
createdAt: new Date().toISOString(),
};
this.pendingActions.set(actionId, { action, call });
return {
id: call.id,
name: call.name,
output: `PENDING_CONFIRMATION ${actionId}: 已登记待确认动作,尚未执行。`,
};
}
getPendingActions(): PendingAction[] {
return Array.from(this.pendingActions.values()).map(item => item.action);
}
async confirmAction(actionId: string): Promise<ToolResult> {
const pending = this.pendingActions.get(actionId);
if (!pending) throw new Error('待确认动作不存在或已经处理');
this.pendingActions.delete(actionId);
const result = await this.tools.execute(pending.call);
this.conversation.push({
role: 'assistant',
content: result.error ? `确认后的操作执行失败:${result.error}` : `已确认并执行:${result.output}`,
toolResults: [result],
activities: [{
id: `receipt-${actionId}`,
kind: 'receipt',
label: result.error ? '执行回执 · 失败' : '执行回执 · 完成',
detail: result.error || result.output,
status: result.error ? 'failed' : 'completed',
timestamp: new Date().toISOString(),
tool: result.name,
}],
timestamp: new Date().toISOString(),
});
return result;
}
rejectAction(actionId: string): void {
if (!this.pendingActions.delete(actionId)) throw new Error('待确认动作不存在或已经处理');
}
// ─── LLM 调用(可插拔) ───
private async callLLM(messages: Message[]): Promise<{ content: string; toolCalls?: ToolCall[] }> {
const apiKey = process.env.OPENAI_API_KEY || process.env.HOLOLAKE_LLM_KEY || '';
const baseUrl = process.env.HOLOLAKE_LLM_BASE || 'https://api.openai.com/v1';
const model = process.env.HOLOLAKE_LLM_MODEL || this.definition.model || 'gpt-4o';
if (!apiKey) {
throw new Error('Agent 尚未配置模型。请先在运行环境中设置 HOLOLAKE_LLM_KEY当前不会伪装成已运行。');
}
try {
const res = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages: messages.map(m => {
if (m.role === 'assistant' && m.toolCalls?.length) {
return {
role: 'assistant',
content: m.content || null,
tool_calls: m.toolCalls.map(call => ({
id: call.id,
type: 'function',
function: { name: call.name, arguments: JSON.stringify(call.arguments) },
})),
};
}
if (m.role === 'tool' && m.toolResults?.[0]) {
return {
role: 'tool',
content: m.content,
tool_call_id: m.toolResults[0].id,
name: m.toolResults[0].name,
};
}
return { role: m.role, content: m.content };
}),
tools: this.tools.toSchema(),
temperature: this.definition.temperature,
max_tokens: this.definition.maxTokens,
}),
});
const data = await res.json() as any;
if (!res.ok) {
throw new Error(data?.error?.message || `模型服务返回 HTTP ${res.status}`);
}
const choice = data.choices?.[0]?.message;
if (!choice) {
return { content: '抱歉AI 模型未返回有效回复。' };
}
const toolCalls: ToolCall[] | undefined = choice.tool_calls?.map((tc: any) => ({
id: tc.id,
name: tc.function.name,
arguments: JSON.parse(tc.function.arguments || '{}'),
}));
return {
content: choice.content || '',
toolCalls,
};
} catch (err: any) {
throw new Error(`AI 调用失败: ${err.message}`);
}
}
// ─── 会话管理 ───
getConversation(): Message[] {
return [...this.conversation];
}
restoreConversation(messages: Message[]): void {
this.conversation = messages
.filter(message => message.role === 'user' || message.role === 'assistant')
.filter(message => typeof message.content === 'string')
.slice(-200)
.map(message => ({
...message,
toolCalls: Array.isArray(message.toolCalls) ? message.toolCalls : [],
toolResults: Array.isArray(message.toolResults) ? message.toolResults : [],
activities: Array.isArray(message.activities) ? message.activities : [],
}));
this.pendingActions.clear();
}
clearConversation(): void {
this.conversation = [];
this.pendingActions.clear();
}
getDefinition(): PersonaDefinition {
return {
...this.definition,
model: process.env.HOLOLAKE_LLM_MODEL || this.definition.model,
};
}
getRuntimeStatus(): { configured: boolean; operational: boolean; toolNames: string[] } {
const configured = Boolean(process.env.OPENAI_API_KEY || process.env.HOLOLAKE_LLM_KEY);
const operational = configured && process.env.HOLOLAKE_LLM_VERIFIED === '1';
return {
configured,
operational,
toolNames: this.tools.list().map(tool => tool.name),
};
}
}
// ─── 默认人格体工厂 ───
export function createDefaultPersona(git: GitEngine): PersonaAgent {
return new PersonaAgent(
{
id: 'hololake-language-entry-default',
name: 'HoloLake',
role: '语言操作入口 · 路由、知识、模块与受控执行',
systemPromptBase: '你是 HoloLake 官方语言操作入口的当前模型计算实例。语言协议、权限边界、路径校验和可核验回执高于模型自由发挥。',
tools: [
'inspect_repository',
'read_document',
'create_document',
'update_document',
'delete_document',
'search_documents',
'find_persona_wake_routes',
'list_documents',
'view_history',
],
permissionMode: 'ask',
model: process.env.HOLOLAKE_LLM_MODEL || 'gpt-4o',
maxTokens: 4096,
temperature: 0.7,
},
git
);
}