207 lines
6.8 KiB
JavaScript
207 lines
6.8 KiB
JavaScript
import 'dotenv/config';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { createDeepAgent, FilesystemBackend, CompositeBackend } from "deepagents";
|
|
import { ChatOpenAI } from "@langchain/openai";
|
|
import { AIMessageChunk, ToolMessage } from "langchain";
|
|
import { HumanMessage } from "@langchain/core/messages";
|
|
import { ChatDeepSeek } from "@langchain/deepseek";
|
|
import config from '../../conf.json' with { type: 'json' };
|
|
import logger from '../../utils/logger.js';
|
|
|
|
// 密钥由 conf.json 统一提供;百度地图 skill 通过 env_get 读环境变量,此处桥接
|
|
process.env.TAVILY_API_KEY ??= config.agent.tavily?.apiKey;
|
|
process.env.BAIDU_MAP_AUTH_TOKEN ??= config.agent.baiduMap?.authToken;
|
|
import Prompts from "./prompts.js";
|
|
import {
|
|
getEnvTool, webFetchTool, webSearchTool, getCalendarInfoTool,
|
|
getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool, getLatLngTool,
|
|
httpGetTool, httpPostTool, createEscortRecordQueryTool
|
|
} from "./tools/index.js";
|
|
|
|
export default class EscortAgent {
|
|
// 历史消息字符数上限,超出时从头部丢弃完整轮次
|
|
static MAX_HISTORY_CHARS = config.agent.maxHistoryChars || 40000;
|
|
|
|
constructor() {
|
|
this.messages = [];
|
|
}
|
|
|
|
clearMessages() {
|
|
this.messages = [];
|
|
}
|
|
|
|
// 按字符数裁剪历史:只以 HumanMessage 为界从头部删除完整轮次,
|
|
// 避免切断 AI(tool_calls) -> Tool 的链式结构;永远保留最新一轮
|
|
_trimHistory() {
|
|
const size = (msgs) => msgs.reduce((sum, m) => sum + (m.text?.length ?? 0), 0);
|
|
if (size(this.messages) <= EscortAgent.MAX_HISTORY_CHARS) {
|
|
return;
|
|
}
|
|
|
|
const starts = [];
|
|
this.messages.forEach((m, i) => {
|
|
if (HumanMessage.isInstance(m)) starts.push(i);
|
|
});
|
|
if (starts.length < 2) {
|
|
return;
|
|
}
|
|
|
|
let cut = 0;
|
|
for (let i = 0; i < starts.length - 1; i++) {
|
|
cut = starts[i + 1];
|
|
if (size(this.messages.slice(cut)) <= EscortAgent.MAX_HISTORY_CHARS) {
|
|
break;
|
|
}
|
|
}
|
|
this.messages = this.messages.slice(cut);
|
|
logger.info(`History trimmed to ${this.messages.length} messages, ${size(this.messages)} chars`);
|
|
}
|
|
|
|
// msg: { ts: "2023-08-01 10:00:00", content: "你好" }
|
|
async streamChat(userInfo, msgs, callback) {
|
|
if (!msgs.length) {
|
|
return;
|
|
}
|
|
|
|
msgs.forEach(msg => {
|
|
if (msg.type === "clear") {
|
|
this.messages = [];
|
|
this.agent = null;
|
|
} else {
|
|
this.messages.push(new HumanMessage(`${msg.ts} - ${msg.content}`));
|
|
}
|
|
});
|
|
|
|
if (this.messages.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this._trimHistory();
|
|
|
|
// 在处理完 clear 消息后再生成 agent,确保清空后使用新实例
|
|
const agent = this._genAgent(userInfo);
|
|
const INTERESTING_NODES = new Set(["model_request", "tools"]);
|
|
for await (const [namespace, mode, data] of await agent.stream(
|
|
{ messages: this.messages },
|
|
{
|
|
recursion_limit: 50,
|
|
streamMode: ["updates", "messages", "custom"], subgraphs: true,
|
|
configurable: {
|
|
thread_id: msgs[0].userId || msgs[0].appId
|
|
}
|
|
})) {
|
|
const isSubagent = namespace.some(s => s.startsWith("tools:"));
|
|
const source = isSubagent ? "subagent" : "main";
|
|
if (mode === "updates") {
|
|
// Main agent updates (empty namespace)
|
|
if (namespace.length === 0) {
|
|
for (const [nodeName, data_] of Object.entries(data)) {
|
|
if (!INTERESTING_NODES.has(nodeName)) continue;
|
|
if (nodeName === "tools") {
|
|
// 工具结果必须并入历史,否则下一轮会出现 tool_calls 缺少对应 tool 结果的断链
|
|
for (const msg of data_.messages ?? []) {
|
|
if (msg.type === "tool") {
|
|
this.messages.push(msg);
|
|
logger.info(`Tool complete: ${msg.name}, Result: ${String(msg.content).slice(0, 200)}`);
|
|
}
|
|
}
|
|
} else if (nodeName === "model_request") {
|
|
this.messages.push(...data_.messages);
|
|
}
|
|
}
|
|
} else {
|
|
// Subagent updates (non-empty namespace)
|
|
for (const nodeName of Object.keys(data)) {
|
|
if (!INTERESTING_NODES.has(nodeName)) continue;
|
|
logger.info(`[${namespace[0]}] step: ${nodeName}`);
|
|
}
|
|
}
|
|
} else if (mode === "messages") {
|
|
const [message, metadata] = data;
|
|
if (message.tool_call_chunks?.length) {
|
|
continue;
|
|
}
|
|
if (metadata?.lcSource === "summarization" || metadata?.lc_source === "summarization") {
|
|
continue;
|
|
}
|
|
if (AIMessageChunk.isInstance(message)) {
|
|
if (message.text) {
|
|
callback(source, "ai", message.text, message.id);
|
|
}
|
|
if (message.additional_kwargs.reasoning_content) {
|
|
callback(source, "reasoning", message.additional_kwargs.reasoning_content, message.id);
|
|
}
|
|
}
|
|
if (ToolMessage.isInstance(message) && message.text) {
|
|
callback(source, "tool", message.text, message.id);
|
|
}
|
|
} else if (mode === "custom") {
|
|
logger.info("custom: ", data);
|
|
}
|
|
}
|
|
}
|
|
|
|
_genAgent(userInfo) {
|
|
const rootDir = process.cwd();
|
|
const memoryFile = userInfo
|
|
? path.join(rootDir, "data", userInfo._id, "memories", "user_memory.txt")
|
|
: null;
|
|
|
|
// 会话中记忆文件被更新后重建 agent,使 systemPrompt 中的用户记忆保持最新
|
|
if (this.agent && this._memoryMtime !== this._statMemory(memoryFile)) {
|
|
this.agent = null;
|
|
}
|
|
if (this.agent) {
|
|
return this.agent;
|
|
}
|
|
|
|
let backend = new FilesystemBackend({ rootDir });
|
|
|
|
if (userInfo) {
|
|
const userMemoryPath = path.join(rootDir, "data", userInfo._id, "memories");
|
|
backend = new CompositeBackend(
|
|
new FilesystemBackend({ rootDir }),
|
|
{
|
|
"/memories/": new FilesystemBackend({
|
|
rootDir: userMemoryPath,
|
|
virtualMode: true
|
|
})
|
|
},
|
|
)
|
|
}
|
|
|
|
this.flashModel = new ChatDeepSeek({
|
|
model: config.agent.deepseek.flashModel,
|
|
apiKey: config.agent.deepseek.apiKey,
|
|
temperature: 0.0
|
|
});
|
|
|
|
this._memoryMtime = this._statMemory(memoryFile);
|
|
|
|
this.agent = createDeepAgent({
|
|
name: "deep-agent",
|
|
model: this.flashModel,
|
|
systemPrompt: Prompts.buildSystemPrompt(userInfo),
|
|
memory: ["./agent/escort/AGENTS.md"],
|
|
backend,
|
|
tools: [getEnvTool, webFetchTool, webSearchTool, getLatLngTool, httpGetTool, httpPostTool,
|
|
getCalendarInfoTool, getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool,
|
|
createEscortRecordQueryTool(userInfo)],
|
|
skills: ["./agent/escort/skills/"],
|
|
});
|
|
|
|
return this.agent;
|
|
}
|
|
|
|
// 返回记忆文件 mtime,文件不存在时返回 null
|
|
_statMemory(memoryFile) {
|
|
if (!memoryFile) return null;
|
|
try {
|
|
return fs.statSync(memoryFile).mtimeMs;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
}
|