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; } } }