refactor(agent): 迁移AI会话引擎至pi-agent-core库
将原有基于Anthropic/OpenAI SDK的直播聊天代理重构为使用`@earendil-works/pi-agent-core`和`@earendil-works/pi-ai`库的统一API。 新增pi-bridge、pi-model、pi-persist、pi-tools四个模块,封装Agent路由、模型配置、消息持久化和工具适配逻辑。移除`chat.ts`中大量死代码,简化WebSocket处理流程。 BREAKING CHANGE: 移除`VideoAgent`类的`getAnthropicClient`、`getOpenAIClient`、`executeTool`等方法,外部调用需迁移至新pi-bridge API。`PROJECT_ROOT`路径计算方式变更,从`../../..`变为`../../`。
This commit is contained in:
117
web/server/agent/pi-bridge.ts
Normal file
117
web/server/agent/pi-bridge.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Agent } from '@earendil-works/pi-agent-core';
|
||||
import type { AgentEvent } from '@earendil-works/pi-agent-core';
|
||||
import { streamSimple } from '@earendil-works/pi-ai';
|
||||
import type { AssistantMessage } from '@earendil-works/pi-ai';
|
||||
import { WebSocket } from 'ws';
|
||||
import { createPiModel } from './pi-model';
|
||||
import { createPiTools } from './pi-tools';
|
||||
import { tools } from './tools/index';
|
||||
import { videoAgent } from './index';
|
||||
import { dbToPiMessages, saveUserMessage, saveAssistantMessage, saveToolResult, type DbMessage } from './pi-persist';
|
||||
import { getDb } from '../db';
|
||||
|
||||
interface RunContext {
|
||||
currentAssistantMsgId: string | null;
|
||||
}
|
||||
|
||||
export async function runAgentChat(ws: WebSocket, convId: string, userContent: string) {
|
||||
const userMsgId = saveUserMessage(convId, userContent);
|
||||
ws.send(JSON.stringify({ type: 'message', data: { id: userMsgId, role: 'user', content: userContent } }));
|
||||
|
||||
const msgCount = getDb().prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?').get(convId) as { count: number };
|
||||
if (msgCount.count <= 1) {
|
||||
const title = userContent.slice(0, 30) + (userContent.length > 30 ? '...' : '');
|
||||
getDb().prepare("UPDATE conversations SET title = ?, updated_at = datetime('now') WHERE id = ?").run(title, convId);
|
||||
}
|
||||
getDb().prepare("UPDATE conversations SET updated_at = datetime('now') WHERE id = ?").run(convId);
|
||||
|
||||
const history = getDb().prepare(
|
||||
'SELECT * FROM messages WHERE conversation_id = ? AND id != ? ORDER BY created_at'
|
||||
).all(convId, userMsgId) as DbMessage[];
|
||||
const piMessages = dbToPiMessages(history);
|
||||
|
||||
const { model, apiKey } = createPiModel();
|
||||
const piTools = createPiTools(tools);
|
||||
|
||||
const agent = new Agent({
|
||||
initialState: {
|
||||
systemPrompt: videoAgent.getSystemPrompt(),
|
||||
model,
|
||||
thinkingLevel: 'off',
|
||||
tools: piTools,
|
||||
messages: piMessages,
|
||||
},
|
||||
streamFn: streamSimple,
|
||||
getApiKey: () => apiKey,
|
||||
});
|
||||
|
||||
const ctx: RunContext = { currentAssistantMsgId: null };
|
||||
|
||||
agent.subscribe((event: AgentEvent) => {
|
||||
handleAgentEvent(ws, convId, event, ctx);
|
||||
});
|
||||
|
||||
ws.send(JSON.stringify({ type: 'status', data: { status: 'thinking' } }));
|
||||
|
||||
try {
|
||||
await agent.prompt(userContent);
|
||||
} catch (err) {
|
||||
const errMsg = (err as Error).message;
|
||||
console.error('[pi-bridge] Agent error:', errMsg);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'message',
|
||||
data: { id: '', role: 'assistant', content: `抱歉,出错了:${errMsg}` },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function handleAgentEvent(ws: WebSocket, convId: string, event: AgentEvent, ctx: RunContext) {
|
||||
switch (event.type) {
|
||||
case 'message_start': {
|
||||
if (event.message.role === 'assistant') {
|
||||
const id = crypto.randomUUID();
|
||||
ctx.currentAssistantMsgId = id;
|
||||
ws.send(JSON.stringify({ type: 'message_start', data: { id } }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'message_update': {
|
||||
const piEvent = event.assistantMessageEvent;
|
||||
const id = ctx.currentAssistantMsgId || '';
|
||||
|
||||
if (piEvent.type === 'text_delta') {
|
||||
ws.send(JSON.stringify({ type: 'text_delta', data: { id, text: piEvent.delta } }));
|
||||
} else if (piEvent.type === 'thinking_delta') {
|
||||
ws.send(JSON.stringify({ type: 'reasoning_delta', data: { id, text: piEvent.delta } }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'message_end': {
|
||||
if (event.message.role === 'assistant') {
|
||||
const id = ctx.currentAssistantMsgId || '';
|
||||
ws.send(JSON.stringify({ type: 'message_end', data: { id } }));
|
||||
saveAssistantMessage(convId, event.message as AssistantMessage);
|
||||
ctx.currentAssistantMsgId = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tool_execution_start': {
|
||||
ws.send(JSON.stringify({ type: 'tool_start', data: { tool: event.toolName, input: event.args } }));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tool_execution_end': {
|
||||
const resultText = event.result?.content?.map((c: any) => c.text || '').join('') || '';
|
||||
if (event.isError) {
|
||||
ws.send(JSON.stringify({ type: 'tool_error', data: { tool: event.toolName, error: resultText } }));
|
||||
} else {
|
||||
ws.send(JSON.stringify({ type: 'tool_result', data: { tool: event.toolName, result: resultText.slice(0, 1000) } }));
|
||||
}
|
||||
saveToolResult(convId, event.toolCallId, event.toolName, resultText, event.isError);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user