From e7d14691df2945e443735617fb09a69f3b93a8cd Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 26 Mar 2026 22:29:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=B0=E5=BF=86=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 8196 bytes app/agent/__init__.py | 6 +- app/agent/middleware/memory.py | 142 +++++++++++++++++++++++++-------- 3 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..570ba09aa14fe44263b60e154ff09e7f919af4a7 GIT binary patch literal 8196 zcmeHMy=xRf6o0d~mmH+IN`fF9>1=`li5L;r<3yu^LJFJUc^~;AiFf7;1dGMOLjM3U zO;Q|)2qKC^F?GbwLQ{zfqPA8+1ERnA$laUU-K12!nPc`X^WK}^Z+U0w3@F5JcFeenD(9wTRg_8)hbyuf=Qy{8P4&$+}$aQwl0k0pK&R4P)PuD7(3Eu0HBf(OZwoY&Kz zOJ5%D=oQ;9=eTz?=WF)D6lSzeb;@o+wy2lCmqC+Zn%dRyx{>g9M|P?!32 zL6;h4XB?|~f}>LU!gjl#V>fy~tlW5{u&qA00!cBP2X$SPJTiEuO0|EpB%0a~^(ii8nTb#wq4*>~%s?euG(^$iEmOgZMxZThByg&DKv!U0D)d%O2 z6l3wA?~;dac+A@z2Cs1+#~(g(WJPHFW-iB$^V_&;Ub-+B(^wlb__bkQ8dBqDevaRN zzKL)3!8s(w*gVuaO=E40fy+W03-9zQSTHZ=@VeOKU`4c^k@yG;-}?lBjx8xY@?LrV zKb8Fbzhm`r19u8^f&#)Xj1=-1sX1Fm#_XXbhk6B778w_6t5R^0A+lW^$6=d)7~(!> je1Wl{w(y|*l@9@aD50&r+unQK{;%|L`@dW#|7H0DHKi*~ literal 0 HcmV?d00001 diff --git a/app/agent/__init__.py b/app/agent/__init__.py index fb4f18f2..e1c66c68 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -143,10 +143,8 @@ class MoviePilotAgent: JobsMiddleware( sources=[str(settings.CONFIG_PATH / "agent" / "jobs")], ), - # 记忆管理 - MemoryMiddleware( - sources=[str(settings.CONFIG_PATH / "agent" / "MEMORY.md")] - ), + # 记忆管理(自动扫描 agent 目录下所有 .md 文件) + MemoryMiddleware(memory_dir=str(settings.CONFIG_PATH / "agent")), # 活动日志 ActivityLogMiddleware( activity_dir=str(settings.CONFIG_PATH / "agent" / "activity"), diff --git a/app/agent/middleware/memory.py b/app/agent/middleware/memory.py index 3827a31c..579319df 100644 --- a/app/agent/middleware/memory.py +++ b/app/agent/middleware/memory.py @@ -17,6 +17,12 @@ from langgraph.runtime import Runtime from app.agent.middleware.utils import append_to_system_message from app.log import logger +# 记忆文件最大限制为 5MB,防止单文件过大导致上下文溢出 +MAX_MEMORY_FILE_SIZE = 5 * 1024 * 1024 + +# 默认记忆文件名(用户主记忆) +DEFAULT_MEMORY_FILE = "MEMORY.md" + class MemoryState(AgentState): """`MemoryMiddleware` 的状态模型。 @@ -40,11 +46,21 @@ class MemoryStateUpdate(TypedDict): MEMORY_SYSTEM_PROMPT = """ +The following memory files were loaded from your memory directory: `{memory_dir}` +You can create, edit, or organize any `.md` files in this directory to manage your knowledge. + {agent_memory} - The above was loaded in from files in your filesystem. As you learn from your interactions with the user, you can save new knowledge by calling the `edit_file` or `write_file` tool. + The above was loaded from `.md` files in your memory directory (`{memory_dir}`). As you learn from your interactions with the user, you can save new knowledge by calling the `edit_file` or `write_file` tool on files in this directory. + + **Memory file organization:** + - All `.md` files in `{memory_dir}` are automatically loaded as memory. + - `MEMORY.md` is the default/primary memory file for general user preferences and profile. + - You may create additional `.md` files to organize knowledge by topic (e.g., `MEDIA_RULES.md`, `DOWNLOAD_PREFERENCES.md`, `SITE_CONFIGS.md`, etc.). + - Keep each file focused on a specific domain or topic for better organization. + - Subdirectories are NOT scanned — only `.md` files directly in `{memory_dir}`. **Learning from feedback:** - One of your MAIN PRIORITIES is to learn from your interactions with the user. These learnings can be implicit or explicit. This means that in the future, you will remember this important information. @@ -76,7 +92,7 @@ MEMORY_SYSTEM_PROMPT = """ - When the information is stale or irrelevant in future conversations - Never store API keys, access tokens, passwords, or any other credentials in any file, memory, or system prompt. - If the user asks where to put API keys or provides an API key, do NOT echo or save it. - - Do NOT record daily activities or task execution history in MEMORY.md - these are automatically tracked in the activity log system (see ). MEMORY.md is only for long-term knowledge, preferences, and patterns. + - Do NOT record daily activities or task execution history in memory files - these are automatically tracked in the activity log system (see ). Memory files are only for long-term knowledge, preferences, and patterns. **Examples:** Example 1 (remembering user information): @@ -103,13 +119,14 @@ MEMORY_SYSTEM_PROMPT = """ MEMORY_ONBOARDING_PROMPT = """ (No memory loaded — this is a brand new user with no saved preferences.) -Memory file path: {memory_file} +Memory directory: {memory_dir} +Default memory file: {memory_file} **IMPORTANT — First-time user detected!** - The user's memory file is currently empty. This means this is likely the user's first interaction, or their preferences have been reset. + The memory directory is currently empty. This means this is likely the user's first interaction, or their preferences have been reset. **Your MANDATORY first action in this conversation:** Before doing ANYTHING else (before answering questions, before calling tools, before performing any task), you MUST proactively greet the user warmly and ask them about their preferences so you can provide personalized service going forward. Specifically, ask about: @@ -126,6 +143,7 @@ Memory file path: {memory_file} 3. The `## User Profile` section MUST include the user's preferred name/nickname at the top 4. Only AFTER saving the preferences, proceed to help with whatever the user originally asked about (if anything) 5. From this point on, always address the user by their preferred name/nickname in conversations + 6. You may also create additional `.md` files in the memory directory (`{memory_dir}`) for different topics as needed. **If the user skips the preference questions** and directly asks you to do something: - Go ahead and help them with their request first @@ -137,7 +155,12 @@ Memory file path: {memory_file} - Your memory file is at: {memory_file}. You can save new knowledge by calling the `edit_file` or `write_file` tool. + Your memory directory is at: {memory_dir}. You can save new knowledge by calling the `edit_file` or `write_file` tool on any `.md` file in this directory. + + **Memory file organization:** + - `MEMORY.md` is the default/primary memory file for general user preferences and profile. + - You may create additional `.md` files to organize knowledge by topic. + - All `.md` files directly in the memory directory are automatically loaded on each conversation. **Learning from feedback:** - One of your MAIN PRIORITIES is to learn from your interactions with the user. These learnings can be implicit or explicit. This means that in the future, you will remember this important information. @@ -158,20 +181,19 @@ Memory file path: {memory_file} - One-time task requests - Simple questions, acknowledgments, or small talk - Never store API keys, access tokens, passwords, or credentials - - Do NOT record daily activities in MEMORY.md — those go to the activity log + - Do NOT record daily activities in memory files — those go to the activity log """ class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]): # noqa - """从 `AGENTS.md` 文件加载代理记忆的中间件。 + """从代理记忆目录加载所有 MD 文件作为记忆的中间件。 - 从配置的源加载记忆内容并注入到系统提示词中。 - - 支持对多个源进行合并。 + 自动扫描指定目录下的所有 `.md` 文件,加载其内容并注入到系统提示词中。 + 支持多文件记忆组织:用户可以创建多个 `.md` 文件来按主题组织知识。 参数: - sources: 包含指定路径和名称的 `MemorySource` 配置列表。 + memory_dir: 记忆文件目录路径。 """ state_schema = MemoryState @@ -179,19 +201,16 @@ class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]): # no def __init__( self, *, - sources: list[str], + memory_dir: str, ) -> None: """初始化记忆中间件。 参数: - sources: 要加载的记忆文件路径列表(例如,`["~/.deepagents/AGENTS.md", - "./.deepagents/AGENTS.md"]`)。 - - 显示名称自动从路径中派生。 - - 按顺序加载源。 + memory_dir: 记忆文件目录路径(例如,`"/config/agent"`)。 + 该目录下所有 `.md` 文件都会被自动加载为记忆。 """ - self.sources = sources + self.memory_dir = memory_dir + self.default_memory_file = str(AsyncPath(memory_dir) / DEFAULT_MEMORY_FILE) @staticmethod def _is_memory_empty(contents: dict[str, str]) -> bool: @@ -215,7 +234,7 @@ class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]): # no """格式化记忆,将位置和内容成对组合。 当记忆为空时,返回初始化引导提示词,引导智能体主动询问用户偏好。 - 当记忆非空时,返回标准记忆系统提示词。 + 当记忆非空时,返回标准记忆系统提示词,包含所有加载的文件内容。 参数: contents: 将源路径映射到内容的字典。 @@ -226,17 +245,53 @@ class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]): # no """ # 记忆为空时返回初始化引导提示词 if memory_empty or self._is_memory_empty(contents): - return MEMORY_ONBOARDING_PROMPT.format(memory_file=self.sources[0]) + return MEMORY_ONBOARDING_PROMPT.format( + memory_dir=self.memory_dir, + memory_file=self.default_memory_file, + ) - sections = [ - f"{path}\n{contents[path]}" for path in self.sources if contents.get(path) - ] + # 按文件名排序,确保 MEMORY.md 排在最前面 + sorted_paths = sorted( + [p for p in contents if contents[p].strip()], + key=lambda p: (0 if AsyncPath(p).name == DEFAULT_MEMORY_FILE else 1, p), + ) - if not sections: - return MEMORY_ONBOARDING_PROMPT.format(memory_file=self.sources[0]) + if not sorted_paths: + return MEMORY_ONBOARDING_PROMPT.format( + memory_dir=self.memory_dir, + memory_file=self.default_memory_file, + ) - memory_body = "\n\n".join(sections) - return MEMORY_SYSTEM_PROMPT.format(agent_memory=memory_body) + sections = [] + for path in sorted_paths: + file_name = AsyncPath(path).name + sections.append(f"### {file_name}\n**Path:** `{path}`\n\n{contents[path]}") + + memory_body = "\n\n---\n\n".join(sections) + return MEMORY_SYSTEM_PROMPT.format( + agent_memory=memory_body, + memory_dir=self.memory_dir, + ) + + async def _scan_memory_files(self) -> list[str]: + """扫描记忆目录下的所有 .md 文件。 + + 仅扫描目录下直接存在的 `.md` 文件(不递归子目录)。 + 文件大小超过限制的将被跳过。 + + 返回: + 发现的 .md 文件路径列表。 + """ + dir_path = AsyncPath(self.memory_dir) + if not await dir_path.exists(): + return [] + + md_files: list[str] = [] + async for entry in dir_path.iterdir(): + if await entry.is_file() and entry.name.lower().endswith(".md"): + md_files.append(str(entry)) + + return md_files async def abefore_agent( self, @@ -244,9 +299,9 @@ class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]): # no runtime: Runtime, # noqa config: RunnableConfig, ) -> MemoryStateUpdate | None: - """在代理执行前加载记忆内容。 + """在代理执行前扫描记忆目录并加载所有 .md 文件的内容。 - 从所有配置的源加载记忆并存储在状态中。 + 自动发现目录下所有 `.md` 文件并加载其内容到状态中。 如果状态中尚未存在则进行加载。 同时检测记忆文件是否为空,设置 memory_empty 标志位, 以便在系统提示词中触发初始化引导流程。 @@ -263,12 +318,35 @@ class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]): # no if "memory_contents" in state: return None + # 扫描目录下所有 .md 文件 + md_files = await self._scan_memory_files() + contents: Dict[str, str] = {} - for path in self.sources: + for path in md_files: file_path = AsyncPath(path) - if await file_path.exists(): + try: + # 检查文件大小 + stat = await file_path.stat() + if stat.st_size > MAX_MEMORY_FILE_SIZE: + logger.warning( + "Skipping memory file %s: too large (%d bytes, max %d)", + path, + stat.st_size, + MAX_MEMORY_FILE_SIZE, + ) + continue contents[path] = await file_path.read_text() logger.debug("Loaded memory from: %s", path) + except Exception as e: + logger.warning("Failed to read memory file %s: %s", path, e) + + if contents: + logger.info( + "Loaded %d memory file(s) from %s: %s", + len(contents), + self.memory_dir, + [AsyncPath(p).name for p in contents], + ) # 检测记忆是否为空(文件不存在、文件内容为空白) is_empty = self._is_memory_empty(contents)