diff --git a/.gitignore b/.gitignore index e63b985..80dfd1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules/ /logs/ +/data/ diff --git a/agent/escort/AGENTS.md b/agent/escort/AGENTS.md new file mode 100644 index 0000000..dcbc5bd --- /dev/null +++ b/agent/escort/AGENTS.md @@ -0,0 +1,13 @@ +# 用户记忆管理 + +## 记忆目录 +`/memories/` 目录用于持久化存储用户信息,在每次会话开始时自动加载。 + +## 长期记忆 +当用户分享以下信息时,使用 `write_file` 将其保存到 `/memories/user_memory.txt`: +- 个人基本信息、生活习惯 +- 健康或医疗相关的任何信息(身体健康、看病、住院、手术、病情、用药、过敏、体质、病历、检查报告、长期健康目标等) + +## 维护规范 +- 文件内容使用 UTF-8 编码 +- 每次写入时,将新内容与已有记忆合并整理后再保存 \ No newline at end of file diff --git a/agent/escort/agent.js b/agent/escort/agent.js index e817240..927115d 100644 --- a/agent/escort/agent.js +++ b/agent/escort/agent.js @@ -1,14 +1,16 @@ import 'dotenv/config'; -import { createDeepAgent, FilesystemBackend } from "deepagents"; +import path from 'path'; +import { createDeepAgent, FilesystemBackend, CompositeBackend, StoreBackend } from "deepagents"; import { ChatOpenAI } from "@langchain/openai"; -import { AIMessageChunk, ToolMessage } from "langchain"; +import { AIMessageChunk, ToolMessage, summarizationMiddleware } from "langchain"; import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages"; import { ChatDeepSeek } from "@langchain/deepseek"; import Prompts from "./prompts.js"; -import { getEnvTool,webFetchTool, webSearchTool, getCalendarInfoTool, +import { + getEnvTool, webFetchTool, webSearchTool, getCalendarInfoTool, getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool, getLatLngTool, - httpGetTool, httpPostTool - } from "./tools/index.js"; + httpGetTool, httpPostTool, createEscortRecordQueryTool +} from "./tools/index.js"; export default class EscortAgent { constructor() { @@ -44,7 +46,7 @@ export default class EscortAgent { recursion_limit: 50, streamMode: ["updates", "messages", "custom"], subgraphs: true, configurable: { - thread_id: 'default-session' + thread_id: msgs[0].userId || msgs[0].appId } })) { const isSubagent = namespace.some(s => s.startsWith("tools:")); @@ -104,22 +106,46 @@ export default class EscortAgent { this.messages = []; const rootDir = process.cwd(); - const backend = new FilesystemBackend({ rootDir }); + const userMemoryPath = path.join(rootDir, "data", userInfo._id, "memories"); + console.log(userMemoryPath); + const backend = new CompositeBackend( + new FilesystemBackend({ rootDir }), + { + "/memories/": new FilesystemBackend({ + rootDir: userMemoryPath, + virtualMode: true + }) + }, + ) - this.model = new ChatDeepSeek({ + this.flashModel = new ChatDeepSeek({ model: 'deepseek-v4-flash', apiKey: 'sk-a58ccd82b7ba4ce3ac176a88c9381095', + temperature: 0.0 + }); + this.proModel = new ChatDeepSeek({ + model: 'deepseek-v4-pro', + apiKey: 'sk-a58ccd82b7ba4ce3ac176a88c9381095', temperature: 0.3 }); this.agent = createDeepAgent({ name: "deep-agent", - model: this.model, + model: this.flashModel, systemPrompt: Prompts.buildSystemPrompt(userInfo), + memory: ["./agent/escort/AGENTS.md"], backend, tools: [getEnvTool, webFetchTool, webSearchTool, getLatLngTool, httpGetTool, httpPostTool, - getCalendarInfoTool, getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool], - skills: ["./agent/escort/skills/"] + getCalendarInfoTool, getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool, + createEscortRecordQueryTool(userInfo)], + skills: ["./agent/escort/skills/"], + middleware: [ + summarizationMiddleware({ + model: this.flashModel, + trigger: { tokens: 5000 }, + keep: { messages: 10 }, + }), + ], }); return this.agent; diff --git a/agent/escort/prompts.js b/agent/escort/prompts.js index 884612d..3c8100a 100644 --- a/agent/escort/prompts.js +++ b/agent/escort/prompts.js @@ -1,10 +1,13 @@ import moment from "moment"; +import fs from "fs"; +import path from "path"; import services from "../../resource/services.js"; import agreement from "../../resource/agreement.js"; class Prompts { static buildSystemPrompt(userInfo) { let userInfo_str = "用户未登录,提示用户先登录,并在'我的'中完善个人信息"; + let usermem_str = ""; if (userInfo) { userInfo_str = JSON.stringify({ name: userInfo.profile.name, @@ -15,6 +18,14 @@ class Prompts { city: userInfo.location.city, address: userInfo.addresses || [], }); + + const rootDir = process.cwd(); + const userMemoryPath = path.join(rootDir, "data", userInfo._id, "memories", "user_memory.txt"); + try { + usermem_str = fs.readFileSync(userMemoryPath, 'utf8'); + } catch (err) { + console.log('读取用户记忆失败', err); + } } return ` @@ -38,6 +49,7 @@ class Prompts { ## 参考信息 用户信息:${userInfo_str}; +用户记忆:${usermem_str}; 服务项目:${JSON.stringify(services)}; 服务协议:${JSON.stringify(agreement)}; `; diff --git a/agent/escort/tools/db/escort_record_query.js b/agent/escort/tools/db/escort_record_query.js new file mode 100644 index 0000000..26ca95e --- /dev/null +++ b/agent/escort/tools/db/escort_record_query.js @@ -0,0 +1,74 @@ +import { tool } from "@langchain/core/tools"; +import z from "zod"; +import { DBModel } from "../../../../models/index.js"; + +function createEscortRecordQueryTool(userInfo) { + const userMobile = userInfo?.profile?.mobile; + + return tool( + async ({ patientName, status, page = 1, pageSize = 20 }) => { + try { + const filter = {}; + + if (patientName) { + filter["patient.name"] = { $regex: patientName, $options: "i" }; + } + + // 强制使用当前登录用户的手机号进行查询 + if (!userMobile) { + return { + success: false, + error: "用户未登录或手机号缺失,无法查询陪诊记录", + }; + } + filter["patient.mobile"] = userMobile; + + if (status) { + filter.status = status; + } + + const skip = (page - 1) * pageSize; + + const records = await DBModel.EscortRecord.find(filter) + .sort({ "schedule.date": -1 }) + .skip(skip) + .limit(pageSize) + .lean(); + + const total = await DBModel.EscortRecord.countDocuments(filter); + + return { + success: true, + data: records, + total, + page, + pageSize, + }; + } catch (error) { + return { + success: false, + error: error.message, + }; + } + }, + { + name: "escort_record_query", + description: + "Query escort records from database for the current user. Supports filtering by patient name and appointment status. Returns paginated results sorted by appointment date in descending order. The query is automatically scoped to the current logged-in user's mobile phone number.", + schema: z.object({ + patientName: z + .string() + .optional() + .describe("Patient name for fuzzy search"), + status: z + .enum(["pending", "confirmed", "in_progress", "completed", "cancelled"]) + .optional() + .describe( + "Appointment status: pending (待确认), confirmed (已确认), in_progress (进行中), completed (已完成), cancelled (已取消)" + ), + }), + } + ); +} + +export { createEscortRecordQueryTool }; diff --git a/agent/escort/tools/db/escort_record_set.js b/agent/escort/tools/db/escort_record_set.js new file mode 100644 index 0000000..e86fe08 --- /dev/null +++ b/agent/escort/tools/db/escort_record_set.js @@ -0,0 +1,116 @@ +import { tool } from "@langchain/core/tools"; +import z from "zod"; +import { DBModel } from "../../../../models/index.js"; + +const escortRecordSetTool = tool( + async ({ orderId, status, notes, payment }) => { + try { + if (!orderId) { + return { + success: false, + error: "Order ID (_id or orderNo) is required as the lookup key", + }; + } + + const query = { + $or: [ + { _id: orderId }, + { orderNo: orderId } + ] + }; + + const record = await DBModel.EscortRecord.findOne(query); + if (!record) { + return { + success: false, + error: `No escort record found for order ID: ${orderId}`, + }; + } + + const update = {}; + + if (status !== undefined) { + update.status = status; + } + + if (notes !== undefined) { + if (notes.patientNote !== undefined) { + update["notes.patientNote"] = notes.patientNote; + } + if (notes.escortNote !== undefined) { + update["notes.escortNote"] = notes.escortNote; + } + if (notes.medicalSummary !== undefined) { + update["notes.medicalSummary"] = notes.medicalSummary; + } + } + + if (payment !== undefined) { + if (payment.totalFee !== undefined) { + update["payment.totalFee"] = payment.totalFee; + } + if (payment.paidFee !== undefined) { + update["payment.paidFee"] = payment.paidFee; + } + if (payment.status !== undefined) { + update["payment.status"] = payment.status; + } + } + + update["meta.updatetime"] = Date.now(); + + const updatedRecord = await DBModel.EscortRecord.findByIdAndUpdate( + record._id, + { $set: update }, + { new: true } + ); + + return { + success: true, + data: updatedRecord, + }; + } catch (error) { + return { + success: false, + error: error.message, + }; + } + }, + { + name: "escort_record_set", + description: + "Update escort record fields by order ID (_id or orderNo). Supports updating status, notes (patientNote, escortNote, medicalSummary), and payment (totalFee, paidFee, status). Only provided fields will be updated.", + schema: z.object({ + orderId: z + .string() + .describe("Order ID (_id or orderNo) used as the lookup key"), + status: z + .enum(["pending", "confirmed", "in_progress", "completed", "cancelled"]) + .optional() + .describe( + "Appointment status: pending (待确认), confirmed (已确认), in_progress (进行中), completed (已完成), cancelled (已取消)" + ), + notes: z + .object({ + patientNote: z.string().optional().describe("Patient remarks / special requirements"), + escortNote: z.string().optional().describe("Escort service notes"), + medicalSummary: z.string().optional().describe("Medical visit summary"), + }) + .optional() + .describe("Notes information to update"), + payment: z + .object({ + totalFee: z.number().optional().describe("Total service fee (RMB)"), + paidFee: z.number().optional().describe("Amount already paid (RMB)"), + status: z + .enum(["unpaid", "partial", "paid", "refunded"]) + .optional() + .describe("Payment status: unpaid, partial, paid, refunded"), + }) + .optional() + .describe("Payment information to update"), + }), + } +); + +export { escortRecordSetTool }; diff --git a/agent/escort/tools/index.js b/agent/escort/tools/index.js index c862dde..628f32f 100644 --- a/agent/escort/tools/index.js +++ b/agent/escort/tools/index.js @@ -18,3 +18,6 @@ export { getLatLngTool } from './geo/latlon.js'; export { getEnvTool } from './system/envs.js'; +// db +export { createEscortRecordQueryTool } from './db/escort_record_query.js'; +export { escortRecordSetTool } from './db/escort_record_set.js';