diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..9685f577 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,38 @@ +name: Pre-commit + +on: [ push, pull_request ] + +jobs: + run: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: True + matrix: + os: [ ubuntu-latest ] + env: + OS: ${{ matrix.os }} + PYTHON: '3.10' + steps: + - uses: actions/checkout@master + - name: Setup Python + uses: actions/setup-python@master + with: + python-version: '3.10' + - name: Update setuptools + run: | + pip install -U setuptools wheel + - name: Install + run: | + pip install -q -e .[dev] + - name: Install pre-commit + run: | + pre-commit install + - name: Pre-commit starts + run: | + pre-commit run --all-files > pre-commit.log 2>&1 || true + cat pre-commit.log + if grep -q Failed pre-commit.log; then + echo -e "\e[41m [**FAIL**] Please install pre-commit and format your code first. \e[0m" + exit 1 + fi + echo -e "\e[46m ********************************Passed******************************** \e[0m" \ No newline at end of file diff --git a/docs/mem0_learn.md b/docs/mem0_learn.md new file mode 100644 index 00000000..f70c1620 --- /dev/null +++ b/docs/mem0_learn.md @@ -0,0 +1,117 @@ +# ReMeV2的设计文档 + +## 竞品调研(开源框架+商业化Memory+论文) +### mem0 + +开源方案 vs 闭源方案 +1. from mem0.client.main import AsyncMemoryClient, MemoryClient +2. from mem0.memory.main import AsyncMemory, Memory +https://docs.mem0.ai/platform/platform-vs-oss + +问题:之前对比的是闭源方案? + +memory分类【短期记忆与长期记忆】: +https://docs.mem0.ai/core-concepts/memory-types +短期记忆使当前的对话保持连贯性。它包括: +- Conversation history – recent turns in order so the agent remembers what was just said. +- Working memory – temporary state such as tool outputs or intermediate calculations. +- Attention context – the immediate focus of the assistant, similar to what a person holds in mind mid-sentence. +长期记忆能够将知识在不同阶段间保存下来。它记录了: +- Factual memory – user preferences, account details, and domain facts. +- Episodic memory – summaries of past interactions or completed tasks. +- Semantic memory – relationships between concepts so agents can reason about them later. +``` ? +class MemoryType(Enum): + SEMANTIC = "semantic_memory" -> personal + EPISODIC = "episodic_memory" -> summary + PROCEDURAL = "procedural_memory" -> procedural +``` +完全没有SEMANTIC和EPISODIC的引用,只有PROCEDURAL + +提供了哪些接口: +https://docs.mem0.ai/core-concepts/memory-operations/add +1. add 对应 summary workflow +2. search 对应 retrieve workflow +3. update 原子操作 +4. delete 原子操作 +接口层面:开源版本和闭源版本是对齐的 + +MCP接口, +1. add_memory Save text or conversation history for a user/agent +2. search_memories Semantic search across existing memories with filters +3. get_memories List memories with structured filters and pagination +4. get_memory Retrieve one memory by its memory_id +5. update_memory Overwrite a memory’s text after confirming the ID +6. delete_memory Delete a single memory by memory_id +7. delete_all_memories Bulk delete all memories in scope +8. delete_entities Delete a user/agent/app/run entity and its memories +9. list_entities Enumerate users/agents/apps/runs stored in Mem0 +前4个对齐的是闭源版本的python接口,add增加了text + + +user_id概念 +user_id="alice" <==> memory_target +agent_id/session_id可选 +多个user_id # 带讨论,库隔离 + +### letta(MemGPT) @weikang +TODO + + + +## 我们相比竞品的优势 +1. 算法层面:workflow -> agentic & 渐进式方案 +2. 模型层面:mem-agent-rl +3. 支持用户友好的接口,只用用户二开 + +``` 同步方案 +from reme_ai import xxx_summarizer, xxx_retriever # v2的agent +from reme_ai import xxx_tool # v2的agent +from reme_ai import xxx_op # v1的op +from reme_ai import OpenAILLM, OpenAIEMBEDDING +from reme_ai import VectorStore + +def main(): + reme = ReMe( + llm={}, + embedding={}, + vector_store={}, + retriever=Retriever(ops=Opa(top_k=5) >> OpB()] + retriever=Retriever(tools=[ToolA(), ToolB()], + summarizer=Summarizer(ops=Opa(top_k=5) >> OpB()], + summarizer=Summarizer(tools=[ToolA(), ToolB()], + ) + + # 服务模式 + reme.serve() + + # 直接调用 + result = reme.retrieve( + "food preferences", + filters={"user_id": "alice"}, top_k=10) + + result = reme.summary( + user_id="alice", + messages=[{"role": "user", "content": "Help me create a project plan"}]) + + reme.close() + +if __name__ == "__main__": + main() +``` +Reme提供接口 +1. 顶层:提供 summary 和 retrieve 的python使用 & http接口 +2. 底层db:http接口 / mcp接口(和http保持一致) + + +### 引用的库希望不是外部一个小库 +合并flowllm部分逻辑回来 + +### vector_store +1. 复用之前的,有不断开发的需求 +2. 复用mem0,没有异步接口 +3. 使用langchain和llama-index + 1. 需要额外封装 + 2. 之前碰到es同步接口底层是es异步封装,外部要异步很复杂 + +增加下载量 diff --git a/docs/reme_v2_design.md b/docs/reme_v2_design.md index 18d14657..516b8634 100644 --- a/docs/reme_v2_design.md +++ b/docs/reme_v2_design.md @@ -1,378 +1,216 @@ -# ReMeV2 - agent memory kit design - -ReMeV2 是一个面向智能体(Agent)的分层记忆系统,用于从连续的对话与交互中,逐步沉淀出稳定、可检索、可更新的长期记忆。 -系统强调从**原始轨迹 → 事件索引 → 高维抽象记忆**的分层存储,并通过**检索器(Retriever)**与**总结器(Summarizer)**实现动态更新。 - ---- - -## 设计目标 - -本设计旨在构建一个可嵌入任意智能体框架(如对话 Agent、任务 Agent 等)的记忆子系统,使其具备以下能力: - -- **支持多天、多会话的长期记忆积累**,不丢失原始细节。 -- **在推理时按需检索**与当前问题高度相关的记忆片段。 -- **在后台持续做分层总结与压缩**,提炼出抽象的“世界观 / 自我认知 / 用户画像 / 任务知识 / 工具经验”。 -- **支持记忆的增删改**,能够对过时或冲突信息进行更新,保持长期记忆的时效性与一致性。 -- **支持渐进式检索**,从高层抽象记忆逐步回溯到中层事件索引与底层原始轨迹,按需展开细节、控制上下文长度与推理成本。 -- **支持渐进式总结**,从最新对话开始按块增量总结并更新索引与主题记忆,而非每次对全量历史做重复总结。 - ---- - -## 架构概览 - -系统整体可以分为两个维度来看: - -- **静态维度**:记忆如何被分层存储(底层消息块 → 中层索引 → 顶层主题记忆)。 -- **动态维度**:记忆如何被检索与更新(上行 Summarizer、下行 Retriever)。 - -核心组件包括: - -- **底层记忆层(Raw Dialogue Store)**:多天对话轨迹切分成 `messages block`。 -- **中层索引层(Index & Summary Store)**:为每个 `messages block` 生成 `block_id: time + summary`。 -- **顶层主题记忆层(High-level Memories)**:自我认知、任务记忆、工具记忆、用户画像等高维抽象。 -- **Meta Agent(记忆中枢)**:持有高层记忆视图与索引,负责路由检索与更新请求。 -- **Sub Agents(记忆子智能体)**:面向具体主题(如某个用户、某类任务、某组工具)的记忆管理单元。 -- **Retriever(检索器)**:从高层到低层,渐进式查找相关记忆。 -- **Summarizer(总结器)**:从低层到高层,渐进式抽象和更新长期记忆。 - ---- - -## 静态记忆三层架构 - -### 垂直层级划分 - -系统将记忆分为自下而上的三个层级,抽象程度逐级升高: - -- **底层:记忆碎片(历史对话 / 轨迹)** - - 存放原始的、未经压缩的对话与交互记录。 - - **规则**:不去重、不解冲突,只做追加与切片。 - -- **中层:记忆索引(事件总结)** - - 对原始消息块做首轮总结,形成“时间 + 简要摘要”的事件索引。 - - 连接底层细节与顶层抽象。 - - **规则**:允许存在部分冗余与矛盾,主要目标是加速检索。 - -- **顶层:主题记忆(高维抽象知识)** - - 聚合并抽象出关于“自我、任务、工具、用户”等长期知识。 - - **规则**:需要去重(Deduplication)与解冲突(Resolve Conflict),追求紧凑、一致、可解释。 - -### 底层:原始对话与轨迹(Raw Dialogue) - -- 按时间顺序堆叠的对话与行为轨迹: - - `Day1 Messages → Day2 Messages → ... → DayN Messages` -- 通过时间窗口、业务逻辑、固定长度(可重叠)等策略切分为多个 `messages block`: - - `Day1 Messages Block1` - - `Day1 Messages Block2` - - `DayN Messages BlockN` -- 特性: - - 不丢失任何历史细节。 - - 支持后续回溯、审计与再训练。 - -### 中层:记忆索引与事件总结(Index & Summary) - -- 对每个 `messages block` 生成一个结构化索引: - - 格式:`block_id: time + summary` -- 示例: - - `block_id1: 2025-11-11 + 用户提到他非常喜欢吃西瓜。` - - `block_id2: 2025-11-12 + 用户表达自己很孤独。` -- 作用: - - 作为底层原始消息与顶层抽象记忆之间的“桥梁”与“目录”。 - - 为时间感知检索、语义检索提供最小粒度的事件单元。 - -### 顶层:主题记忆(High-Dimensional Abstraction) - -顶层记忆由多个“记忆气泡”(Memory Bubble)构成,每个气泡代表一类主题知识,并通过关联的 `block_id` 与中层索引相连。主要包括: - -- **自我认知记忆(Self-Cognition Memory)** - - 示例: - - “我叫 Remy” - - “我每天需要上班” - - “我是一名面向长期陪伴的智能助手” - - 位置:处于记忆拓扑中心,与其他记忆类别广泛连接。 - -- **金融任务记忆(Financial Task Memory)** - - 示例: - - “做财报分析前需要调研企业的经营数据” - - “估值时可以优先考虑 forward PE” - - 作用:支撑在特定领域(如金融)的任务推理能力。 - -- **工具记忆(Tool Memory)** - - 示例: - - “`bing_search` 很不稳定,经常失败” - - “`tongyi_search` 的结果质量较好,常需要配合 `web_extract` 使用” - - 作用:让智能体在工具选择、调用顺序和错误预期上具有“经验”。 - -- **用户画像记忆(Per-User Memory,如用户 A / 用户 B)** - - 对用户 A 的记忆示例: - - “A 很喜欢吃西瓜” - - “A 非常有礼貌” - - “A 计划下周出行” - - 与中层事件关联: - - 顶层的“喜欢吃西瓜”记忆与 `block_id1` 建立逻辑连接。 - - 每个用户独立维护一套长期画像,避免混淆。 - ---- - -## 动态流程一:ReMeV2 Retriever(检索流程) - -检索流程是一个**自上而下**的渐进式过程:从高维抽象出发,逐步定位到具体事件甚至原始对话块,为当前问答或决策提供证据。 - -```mermaid -flowchart TD - op0[op: 当前查询 / 上下文输入] - op1[op: Meta Agent 读取自我认知与主题索引] - op2[op: Meta Agent 基于 topic/intent 选择记忆子空间] - op3[op: 调度对应 Sub Agent 实例] - op4[op: 在顶层/中层记忆空间检索相关 Memories / 索引条目] - op5[op: 根据需要回溯到底层 messages block(retrieve_block)] - op6[op: 汇总得到的 messages block 列表,构造检索上下文] - op7[op: 将上下文交给上层 Agent 做回答 / 决策] - - op0 --> op1 --> op2 --> op3 --> op4 --> op5 --> op6 --> op7 +# ReMeV2 design +一些问题: +1. 只focus在python import的方案? +2. 旧的workflow & agentic 方案都按照这个接口? +3. 是否需要异步?单独构建,不在本期迭代中 + +保留user_id的设计 + +## Long Term Memory Basic Usage + +```python +import os +from reme_ai import ReMe + +# os.environ["OPENAI_API_KEY"] = "sk-..." +# os.environ["OPENAI_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" + +os.environ["REME_LLM_API_KEY"] = "sk-..." +os.environ["REME_LLM_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" +os.environ["REME_EMBEDDING_API_KEY"] = "sk-..." +os.environ["REME_EMBEDDING_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" + +memory = ReMe( + memory_space="remy", # workspace + llm={"backend": "openai", "model": "qwen-plus", "temperature": 0.6, }, + embedding={"backend": "openai", "model": "text-embedding-v4", "dimension": 1024}, + vector_store={"backend": "local_file"}, # 支持的其他vector_store包括xxxx +) + +# memory.update/delete/list/ <=> + +result = await memory.summary( + messages=[ + {"role": "user", "content": "I'm travelling to SF"}, + {"role": "assistant", "content": "That's great to hear!"} + ], + user_id="Alice", # user_id + # memory_type="auto" # 默认是auto + # **kwargs 所有参数都放到这里 +) + +memories = await memory.retrieve( + query="what is your travel plan?", # or messages + limit=3, + user_id="Alice", + # memory_type="auto" # 默认是auto +) +memories_str = "\n".join(f"- {m['memory']}" for m in memories["results"]) +print(memories_str) ``` -### Meta Agent:记忆入口与路由 - -- 持有: - - **常驻的自我认知记忆**(总是加载在工作内存中)。 - - 各类主题记忆的**名称 + 描述索引**,例如: - - `1. 金融任务记忆` - - `2. 工具记忆` - - `3. 用户A记忆` - - `4. 用户B记忆` -- 职责: - - 根据当前查询的 **topic / intent**,选择合适的记忆子空间。 - - 按需创建或调度具体的 `Sub_Agent` 来完成深入检索。 - -### Sub Agent:按主题检索 - -- **预定义子智能体**(如:工具记忆 Agent、某个用户画像 Agent 等): - - 支持接口: - - `retrieve_by_query(query)`:基于语义相似度的检索。 - - `time_aware_retrieve(time)`:基于时间的窗口检索(适合用户时间线类记忆)。 -- **自定义子智能体**: - - 可以实现更复杂的检索逻辑(例如结合时间衰减、多源融合等)。 - -### 渐进式检索与回溯 - -- 子智能体首先在顶层 / 中层记忆空间中找到语义相关的 **Memories / 索引条目**: - - 每条记忆通常携带 `block_id` 与简要 summary。 -- 若需要更细节的证据,系统可以进一步调用: - - `retrieve_block(block_id)`:回溯到底层 `messages block`。 - - `time_match_memories(time_range)`:根据时间窗口获取原始对话块。 -- 最终输出: - - 一组与当前问题高度相关的 `messages block List`,作为上下文,供 Agent 生成回答或做决策。 - ---- - -## 动态流程二:ReMeV2 Summarizer(总结流程) - -总结流程是一个**自下而上**的学习与更新过程:将新发生的对话转化为结构化事件,再进一步纳入长期记忆。 - -```mermaid -flowchart TD - op0[op: 收集 DayN 的原始对话与行为轨迹] - op1[op: 按时间/业务逻辑/固定长度切片为 messages block] - op2[op: 对每个 block 做摘要,生成 block_id: time + summary] - op3[op: 将 block summary 流交给 Meta Agent] - op4[op: Meta Agent 判断主题并路由/创建对应 Sub Agent] - op5[op: Sub Agent 加载当前主题的长期记忆与新 block] - op6[op: 通过 recall_similar_memory 检索历史相似记忆] - op7[op: 对比新旧信息,决定 delete/new/update 记忆操作] - op8[op: 写回更新后的高层主题记忆,维护 block_id 链接] - op9[op: 将最新记忆视图回流给 Meta Agent,用于后续检索] - - op0 --> op1 --> op2 --> op3 --> op4 --> op5 --> op6 --> op7 --> op8 --> op9 +## Long Term Memory Cli Chat / APP体验(都保留) +```python +import os + +from reme_ai import ReMe +from openai import OpenAI + +os.environ["REME_LLM_API_KEY"] = "sk-..." +os.environ["REME_LLM_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" +os.environ["REME_EMBEDDING_API_KEY"] = "sk-..." +os.environ["REME_EMBEDDING_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" + +memory = ReMe( + memory_space="remy", # workspace + llm={"backend": "openai", "model": "qwen-plus", "temperature": 0.6, }, + embedding={"backend": "openai", "model": "text-embedding-v4", "dimension": 1024}, + vector_store={"backend": "local_file"}, # 支持的其他vector_store包括xxxx +) + +openai_client = OpenAI() + +os.environ["OPENAI_API_KEY"] = "sk-..." +os.environ["OPENAI_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" + +def chat_with_memories(query: str, history_messages: list[dict], user_name: str = "", start_summary_size: int = 2, keep_size: int = 0) -> str: + memories = memory.retrieve(query=query, user_id=user_name, limit=3) + system_prompt = f"You are a helpful AI named `Remy`. Use the user memories to answer the question. If you don't know the answer, just say you don't know. Don't try to make up an answer. Answer the question based on query and memories.\n" + if memories: + memories_str = "\n".join(f"- {m['memory']}" for m in memories["results"]) + system_prompt += f"User Memories:\n{memories_str}\n" + + system_message = {"role": "system", "content": system_prompt} + history_messages.append({"role": "user", "content": query}) + response = openai_client.chat.completions.create(model="qwen-plus", messages=[system_message] + history_messages) + history_messages.append({"role": "assistant", "content": response.choices[0].message.content}) + + if history_messages and len(history_messages) >= start_summary_size: + memory.summary(history_messages[:-keep_size], user_id=user_name) + print("current memories: " + memory.list_memories(user_id=user_name)) + history_messages = history_messages[-keep_size:] + + return history_messages[-1]["content"] + +def main(): + user_name = input("user_name: ").strip() + print("Chat with Remy (type 'exit' to quit)") + + messages = [] + while True: + user_input = input(f"{user_name}: ").strip() + if user_input.lower() == 'exit': + print("Goodbye!") + break + + print(f"Remy: {chat_with_memories(user_input, messages, user_name)}") + + memory.delete_all_memories(user_id=user_name) + print("All memories deleted") + +if __name__ == "__main__": + main() ``` -### 输入与切片 - -- 输入源: - - `DayN Messages / Trajectory`(第 N 天的完整对话与行为轨迹)。 -- 切片策略: - - 按时间窗口切片(例如每 5 分钟一个 block)。 - - 按业务逻辑边界切片(如一个任务完成即成一个 block)。 - - 按固定长度(可带重叠)切片,兼顾局部上下文。 -- 结果: - - `messages block1, block2, ... blockN` - -### 初步总结:生成索引条目 - -- 对每个 `messages block` 进行摘要,形成: - - `block_id: time + summary` -- 示例: - - `id: 20251111_01 - 用户询问“什么是 AI”并表达对技术的好奇。` -- 这一阶段的目标: - - 为中层索引层提供结构化事件单元,尚不过度抽象。 - -### Meta Agent 路由与子智能体更新 - -- Meta Agent 接收新的 `block summary` 流: - - 根据内容判断其归属主题(例如用户相关、任务相关、工具体验相关等)。 - - 调用 `create_sub_agent(topic)` 或路由到已有的 `Sub_Agent`。 -- 子智能体内的处理步骤: - - **Step 1:加载上下文记忆** - - 加载与当前主题相关的长期记忆(如“对用户 A 的记忆”)与当前 `messages block`。 - - **Step 2:回忆(Recall)** - - 通过 `recall_similar_memory(query)` 拉取历史相似记忆,用于对比新旧信息。 - - **Step 3:更新(Update)** - - 基于新旧信息的对比,决定对顶层记忆的操作: - - `delete_memory(mem_id)`:删除已过时或被否定的记忆。 - - `new_memory(mem_content)`:新增一条新的长期记忆。 - - `update_memory(mem_id, mem_content)`:对既有记忆做内容更新或细化。 - -### 输出与闭环 - -- Summarizer 输出一批新的或更新后的 **高层记忆项(New Memories)**,并附带 `block_id` 链接。 -- 这些记忆将: - - 被 Meta Agent 纳入其主题索引中。 - - 为后续 Retriever 流程提供可检索的抽象知识。 -- 至此,形成从“原始对话 → 索引总结 → 高维记忆 → 检索使用 → 再次总结更新”的完整闭环。 - ---- - -## 总结 - -- **分层存储**:底层保留原始细节,中层作为事件索引,顶层沉淀抽象认知;中层通过 `block_id` 将两端连通。 -- **按需调用**:检索时从高层主题记忆出发,由 Meta Agent 路由到 Sub Agent,再逐级回溯到底层对话。 -- **动态增删改**:新对话先被切片与总结,再与旧记忆对比,通过新增、更新、删除操作保持长期记忆“准确 + 精简 + 有时序感”。 -- **渐进式检索与总结**:在检索方向自上而下逐级展开,在学习方向自下而上逐级抽象,实现“用多少展开多少、学多少沉淀多少”的流式记忆管理。 - - -``` python code -base_memory中增加 -# block_id: str = Field(default=..., description="block_id") -block_ids: List[str] = Field(default=..., description="block_id") - - -from typing import List, Tuple - -from flowllm.core.schema import Message -from pydantic import BaseModel, Field - -class MessageModel(Message): - """ - 被存储到传统的db中的数据结构,一行一条message - def retrieve_message(key_word=None, time_start=None, limit: int=50): - 支持关键字检索和时间检索 - content like %{key_word}% - 暂时不支持向量检索,向量只做抽取的memory的 - """ - name: str = Field(default="", description="The name of the character who actually outputs this message") - message_id: str = Field(default=..., description="message_id, uuid,每一条message不重样") - block_id: str = Field(default=..., description="block_id,message分块的id,用户链接summary") - - -from reme_ai.schema.memory import BaseMemory - -""" -基本和原来一致,增加block compress的memory - -工具记忆放到传统db中 -BlockCompress放到向量库中 -Personal + Task + Self-Cognition 放到向量库中 -""" - - - - -class BlockCompressMemory(BaseMemory): - start_time: str = Field(default=..., description="block的开始时间") - end_time: str = Field(default=..., description="block的结束时间") - -class SelfCognitionMemory(BaseMemory): - target_name: str = Field(default="自我认知") - memory_type: str = Field(default="self") - -class ToolMemory(BaseMemory): - target_name: str = Field(default="", description="工具名称") - memory_type: str = Field(default="tool") - -class PersonalMemory(BaseMemory): - target_name: str = Field(default="", description="这是记录谁的记忆?比如TOM、Jerry?") - memory_type: str = Field(default="personal") - -class TaskMemory(BaseMemory): - target_name: str = Field(default="", description="这是什么任务的记忆、比如bfcl、appworld、金融?") - memory_type: str = Field(default="task") - - - -# Handoffs allow meta agent to delegate tasks to another agent. -""" -summarizer -""" - -def compress_message_block(message_block: List[Message]) -> BlockCompressMemory: - """ - 压缩message_block信息到BlockCompressMemory - """ - -def handoff_to_summary_agent(agent_name: str) -> str: - """ - 如果agent_name是存在的,那直接使用现成的,否则可以创建新的agent(默认是不开启的) - """ - -def retrieve_memory_by_query(query: str, limit_size: int = 50, threshold = 0.5) -> List[BaseMemory]: - """ - 如果之前使用过handoff_to_summary_agent - 在自我认知 - """ - -def datetime_tool(**kwargs) - """ - 时间工具 - Returns: - - """ - -def delete_memory(memory_id: str): - ... - -# 做实验 -def update_memory(memory_id: str, content: str, time_stamp: str): - ... +## Long Term Memory Advance Usage +```python +import os + +from reme_ai import ReMe +from reme_ai.retriever import FlowRetriever, AgenticRetriever +from reme_ai.summarizer import FlowSummarizer, AgenticSummarizer +from reme_ai.ops import AOp, BOP, COP +from reme_ai.tools import ATool, BTool, CTool + +os.environ["REME_LLM_API_KEY"] = "sk-..." +os.environ["REME_LLM_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" +os.environ["REME_EMBEDDING_API_KEY"] = "sk-..." +os.environ["REME_EMBEDDING_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" + +memory = ReMe( + memory_space="remy", # workspace + llm={"backend": "openai", "model": "qwen-plus", "temperature": 0.6, }, + embedding={"backend": "openai", "model": "text-embedding-v4", "dimension": 1024}, + vector_store={"backend": "local_file"}, # 支持的其他vector_store包括xxxx + use_agentic_mode=True, +) + +# 只暴露agentic方式进行构建,或者就不暴露接口 +memory.set_retriever(AgenticRetriever(tools=[ATool(), BTool(), CTool]), system_prompt="xxxx") # 后面应该会把Op去掉 +memory.set_summarizer(AgenticSummarizer(tools=[ATool(), BTool(), CTool()])) + +# memory.set_retriever(Pipeline(ops=[Router1Op(), Router2Op()])) +# memory.set_retriever(Router1Op() >> Router2Op()) +# memory.set_summarizer(FlowSummarizer(AOp(top_k=5) >> BOP() >> COP())) + +result = memory.summary( + desc="user(Alice) & AI的对话", + messages=[ + {"role": "user", "content": "I'm travelling to SF"}, + {"role": "assistant", "content": "That's great to hear!"} + ], + memory_type="auto", # auto, personal, procedural, tool + **kwargs # top_k 表格有哪一些参数是可以修改的,包括memory_type +) + +memories = memory.retrieve( + query="what is your travel plan?", + limit=3, + memory_type="auto", # auto, personal, procedural, tool + **kwargs, +) +memories_str = "\n".join(f"- {m['memory']}" for m in memories["results"]) +print(memories_str) +``` -def new_memory(content: str, time_stamp: str) -> BaseMemory: - ... +## Short Term Memory Basic Usage +```python +import os +from reme_ai import ReMe + +os.environ["REME_LLM_API_KEY"] = "sk-..." +os.environ["REME_LLM_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" +os.environ["REME_EMBEDDING_API_KEY"] = "sk-..." +os.environ["REME_EMBEDDING_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" + +memory = ReMe( + memory_space="remy", # workspace + llm={"backend": "openai", "model": "qwen-plus", "temperature": 0.6, }, + embedding={"backend": "openai", "model": "text-embedding-v4", "dimension": 1024}, + vector_store={"backend": "local_file"}, # 支持的其他vector_store包括xxxx +) + +result = memory.offload_context( + messages=[ + {"role": "user", "content": "I'm travelling to SF"}, + {"role": "assistant", "content": "That's great to hear!"} + ], + **kwargs +) + +# agentic +memories = memory.reload_context( + query="what is your travel plan?", + limit=3, + **kwargs +) +memories_str = "\n".join(f"- {m['memory']}" for m in memories["results"]) +print(memories_str) +``` -""" -summary reviser / Reward Model -""" +## Short Term Memory with ReactAgent +集成agentscope langchain -def revise_memory(messages: List[Message], - origin_memory: List[BaseMemory], - output_memory: List[BaseMemory]) -> Tuple["", bool]: - """ - reasoning and result - 是否所有信息都加入了? - 当前结果是不是有冲突和重复。 - 如果有问题,就重新react或者重试 - """ +## Long Term Memory with ReactAgent +集成agentscope langchain -""" -retriever -""" +装饰器直接用 -def retrieve_memory_by_query(query: str, limit_size: int = 50, threshold = 0.5) -> List[BaseMemory]: - """ - 和上面不一致的在于,这里可以搜索到compress的记忆 - """ +1. retrieve / reload 是否合并? -def retrieve_message(key_word=None, time_offset=None, time_limit=None): - """ - 支持关键字检索和时间检索 - content like %{key_word}% - 暂时不支持向量检索,向量只做抽取的memory的 - """ +1. todo 对比5家,出参,入参,竞品对比的逻辑 -def retrieve_block_id(block_id): - """ - 支持关键字检索和时间检索 - content like %{key_word}% - 暂时不支持向量检索,向量只做抽取的memory的 - """ -``` \ No newline at end of file +2. session调研新的接口,带session id的设计 \ No newline at end of file diff --git a/docs/reme_v2_draft.md b/docs/reme_v2_draft.md new file mode 100644 index 00000000..28513904 --- /dev/null +++ b/docs/reme_v2_draft.md @@ -0,0 +1,525 @@ +# ReMeV2-重构的设计思路和代码细节 + +## 背景 + +### Claude Skills的启发 +ReMeV2的设计受到Claude Skills的渐进式披露(Progressive Disclosure)架构启发。Claude Skills通过三层架构实现高效的上下文管理: + +1. **元数据层(Metadata Layer)**:在启动时加载技能的名称和描述,让Agent了解可用的技能及其用途 +2. **指令层(Instructions Layer)**:当技能被触发时,加载SKILL.md文件的主体部分,包含具体的工作流程、最佳实践和指导 +3. **资源层(Resources Layer)**:按需加载附加资源,如代码示例、参考资料等 + +这种渐进式披露确保在任何给定时间,只有相关内容占据上下文窗口,避免重复提供相同指导,从而优化性能。 + +参考:https://docs.claude.com/en/docs/agents-and-tools/agent-skills/quickstart + +### ReMeV2的设计理念 +借鉴Claude Skills的思路,ReMeV2设计了基于**渐进式检索&总结**的记忆管理方案: + +**渐进式检索(Progressive Retrieval)**: +- Layer 1: 加载元记忆(Meta Memory)- 记忆类型、目标和描述概览 +- Layer 2: 检索具体记忆(Retrieve Memory)- 根据查询获取相关记忆内容 +- Layer 3: 读取历史细节(Read History)- 按需加载完整的历史交互记录 + +**渐进式总结(Progressive Summarization)**: +- 通过专门的Summary Agents(personal/procedural/tool/identity)对不同类型的记忆进行分层总结 +- 使用Meta-Summarizer协调各个Summary Agent,实现记忆的增量更新和压缩 +- Summary Memory作为新的维度压缩Message,优化上下文使用 + +这种设计既保证了记忆检索的效率,又确保了记忆总结的准确性和可维护性。 + +## Memory +@MemoryType 三层架构 + +### Memory schema 设计 +@MemoryNode的 + +## 代码设计 +相对workflow更加简洁,激进? +ReMeV2 = tool(s) + agent(s) + +### tool +@tool +列出所有的类初始化参数,tool_call参数 + +#### 基类:BaseMemoryToolOp + +**初始化参数:** +- `enable_multiple` (bool): 是否启用多项操作模式。默认:`True` +- `enable_thinking_params` (bool): 是否在schema中包含thinking参数。默认:`False` +- `memory_metadata_dir` (str): 存储记忆元数据的目录路径。默认:`"./memory_metadata"` + +#### Tool操作列表 + +| Tool类 | 继承自 | 初始化参数(除基类外) | Tool Call参数(单项模式) | Tool Call参数(多项模式) | +|----------------------------|------------------|------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| **AddMemoryOp** | BaseMemoryToolOp | `add_when_to_use` (bool, 默认: False)
`add_metadata` (bool, 默认: True) | `when_to_use` (str, 可选)
`memory_content` (str, 必需)
`metadata` (dict, 可选) | `memories` (array, 必需):
- `when_to_use` (str, 可选)
- `memory_content` (str, 必需)
- `metadata` (dict, 可选) | +| **UpdateMemoryOp** | BaseMemoryToolOp | 无 | `memory_id` (str, 必需)
`memory_content` (str, 必需)
`metadata` (dict, 可选) | `memories` (array, 必需):
- `memory_id` (str, 必需)
- `memory_content` (str, 必需)
- `metadata` (dict, 可选) | +| **DeleteMemoryOp** | BaseMemoryToolOp | 无 | `memory_id` (str, 必需) | `memory_ids` (array[str], 必需) | +| **VectorRetrieveMemoryOp** | BaseMemoryToolOp | `enable_summary_memory` (bool, 默认: False)
`add_memory_type_target` (bool, 默认: False)
`top_k` (int, 默认: 20) | `query` (str, 必需)
`memory_type` (str, 可选, 枚举: [identity, personal, procedural])
`memory_target` (str, 可选) | `query_items` (array, 必需):
- `query` (str, 必需)
- `memory_type` (str, 可选)
- `memory_target` (str, 可选) | +| **AddMetaMemoryOp** | BaseMemoryToolOp | 无 | `memory_type` (str, 必需, 枚举: [personal, procedural])
`memory_target` (str, 必需) | `meta_memories` (array, 必需):
- `memory_type` (str, 必需)
- `memory_target` (str, 必需) | +| **ReadMetaMemoryOp** | BaseMemoryToolOp | `enable_tool_memory` (bool, 默认: False)
`enable_identity_memory` (bool, 默认: False) | 无(无输入schema) | N/A (enable_multiple=False) | +| **AddHistoryMemoryOp** | BaseMemoryToolOp | 无 | `messages` (array[object], 必需) | N/A (enable_multiple=False) | +| **ReadHistoryMemoryOp** | BaseMemoryToolOp | 无 | `memory_id` (str, 必需) | `memory_ids` (array[str], 必需) | +| **AddSummaryMemoryOp** | AddMemoryOp | 无(继承自AddMemoryOp) | `summary_memory` (str, 必需)
`metadata` (dict, 可选) | N/A (enable_multiple=False) | +| **ReadIdentityMemoryOp** | BaseMemoryToolOp | 无 | 无(无输入schema) | N/A (enable_multiple=False) | +| **UpdateIdentityMemoryOp** | BaseMemoryToolOp | 无 | `identity_memory` (str, 必需) | N/A (enable_multiple=False) | +| **ThinkToolOp** | BaseAsyncToolOp | `add_output_reflection` (bool, 默认: False) | `reflection` (str, 必需) | N/A | +| **HandsOffOp** | BaseMemoryToolOp | 无 | `memory_type` (str, 必需, 枚举: [identity, personal, procedural, tool])
`memory_target` (str, 必需) | `memory_tasks` (array, 必需):
- `memory_type` (str, 必需)
- `memory_target` (str, 必需) | + +**注意事项:** +1. 所有BaseMemoryToolOp子类自动继承 `enable_multiple`、`enable_thinking_params` 和 `memory_metadata_dir` 参数 +2. 当 `enable_thinking_params=True` 时,会自动在tool call schema中添加 `thinking` 参数 +3. 部分工具在 `__init__` 方法中强制设置 `enable_multiple=False`(AddHistoryMemoryOp、AddSummaryMemoryOp、ReadIdentityMemoryOp、UpdateIdentityMemoryOp、ReadMetaMemoryOp) +4. 通过 `self.context` 访问的上下文参数:`workspace_id`、`memory_type`、`memory_target`、`ref_memory_id`、`author` +5. VectorRetrieveMemoryOp:当 `add_memory_type_target=False` 时,memory_type和memory_target从context中获取,而非tool call参数 + +### agent +@agent/v1 +列出所有的类初始化参数,tool_call参数 + +#### 基类:BaseMemoryAgentOp + +**初始化参数:** +- `max_steps` (int):ReAct循环的最大推理-执行步数。默认值:`20` +- `tool_call_interval` (float):工具调用之间的间隔时间(秒)。默认值:`0` +- `add_think_tool` (bool):是否为指令模型添加思考工具。默认值:`False` + +**类属性:** +- `memory_type` (MemoryType | None):Agent的记忆类型。默认值:`None` + +**上下文参数(通过 `self.context` 访问):** +- `workspace_id` (str, required):工作空间标识符 +- `query` (str, optional):查询文本输入 +- `messages` (array[object], optional):消息输入(query的替代方式) +- `memory_target` (str, optional):记忆操作的目标 +- `ref_memory_id` (str, optional):参考记忆ID +- `author` (str):作者/模型名称(从LLM配置获取) + +#### Agent操作 + +| Agent类 | 继承自 | 初始化参数(基类外) | Tool Call参数 | 可用工具 | +|--------------------------------|-------------------|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| **PersonalSummaryAgentV1Op** | BaseMemoryAgentOp | None | `workspace_id` (str, required)
`memory_target` (str, required)
`query` (str, optional)
`messages` (array, optional)
`ref_memory_id` (str, required) | add_memory
update_memory
delete_memory
vector_retrieve_memory | +| **ProceduralSummaryAgentV1Op** | BaseMemoryAgentOp | None | `workspace_id` (str, required)
`memory_target` (str, required)
`query` (str, optional)
`messages` (array, optional)
`ref_memory_id` (str, required) | add_memory
update_memory
delete_memory
vector_retrieve_memory | +| **ToolSummaryAgentV1Op** | BaseMemoryAgentOp | None | `workspace_id` (str, required)
`memory_target` (str, required)
`query` (str, optional)
`messages` (array, optional)
`ref_memory_id` (str, required) | add_memory
update_memory
vector_retrieve_memory | +| **IdentitySummaryAgentV1Op** | BaseMemoryAgentOp | None | `workspace_id` (str, required)
`query` (str, optional)
`messages` (array, optional) | read_identity_memory
update_identity_memory | +| **ReMeSummaryAgentV1Op** | BaseMemoryAgentOp | `enable_tool_memory` (bool, 默认: True)
`enable_identity_memory` (bool, 默认: True) | `workspace_id` (str, required)
`query` (str, optional)
`messages` (array, optional) | add_meta_memory
add_summary_memory
hands_off
(内部调用: add_history_memory, read_identity_memory, read_meta_memory) | +| **ReMeRetrieveAgentV1Op** | BaseMemoryAgentOp | `enable_tool_memory` (bool, 默认: True) | `workspace_id` (str, required)
`query` (str, optional)
`messages` (array, optional) | vector_retrieve_memory
read_history_memory
(内部调用: read_meta_memory) | +| **ReMyAgentV1Op** | BaseMemoryAgentOp | `enable_tool_memory` (bool, 默认: True)
`enable_identity_memory` (bool, 默认: True) | `workspace_id` (str, required)
`query` (str, optional)
`messages` (array, optional) | vector_retrieve_memory
read_history_memory
(内部调用: read_identity_memory, read_meta_memory) | + +**说明:** +1. 所有Agent都从 `BaseMemoryAgentOp` 继承 `max_steps`、`tool_call_interval` 和 `add_think_tool` 参数 +2. Agent实现ReAct模式:推理步骤(带工具的LLM调用)→ 执行步骤(执行工具调用)→ 重复直到完成或达到max_steps +3. `PersonalSummaryAgentV1Op` 的 `memory_type = MemoryType.PERSONAL` +4. `ProceduralSummaryAgentV1Op` 的 `memory_type = MemoryType.PROCEDURAL` +5. `ToolSummaryAgentV1Op` 的 `memory_type = MemoryType.TOOL` +6. `IdentitySummaryAgentV1Op` 的 `memory_type = MemoryType.IDENTITY` +7. Summary agents(Personal/Procedural/Tool/Identity)通常由 `ReMeSummaryAgentV1Op` 通过 `hands_off` 工具调用 +8. `ReMeSummaryAgentV1Op` 在每个推理步骤中动态更新系统提示中的meta_memory_info +9. 工具可用性由每个Agent初始化时注册的 `ops` 字典决定 + +## Runtime设计(内部设计) + +### summary 渐进式总结 +AddHistoryMemoryOp() +ReadMetaMemoryOp() + +ReMeSummaryAgentV1Op << [ + AddMetaMemoryOp(list(memory_type, memory_target)), + AddSummaryMemoryOp(summary_memory), + HandsOffOp(list(memory_type, memory_target)) << [ + PersonalSummaryAgentV1Op, + ProceduralSummaryAgentV1Op, + ToolSummaryAgentV1Op, + IdentitySummaryAgentV1Op + ], +] +PersonalSummaryAgentV1Op << [AddMemoryOp, UpdateMemoryOp, DeleteMemoryOp, VectorRetrieveMemoryOp] +ProceduralSummaryAgentV1Op << [AddMemoryOp, UpdateMemoryOp, DeleteMemoryOp, VectorRetrieveMemoryOp] +ToolSummaryAgentV1Op << [AddMemoryOp, UpdateMemoryOp, VectorRetrieveMemoryOp] +IdentitySummaryAgentV1Op << [ReadIdentityMemoryOp, UpdateIdentityMemoryOp] + +### retrieve: 渐进式检索【这里和skills检索很像】 +``` skills +load_meta_skills +load_skills +load_reference_skills +execute_shell +``` + +ReMeRetrieveAgentV1Op << [ + ReadMetaMemoryOp(), + ``` prompt + 格式:"- (): " + personal jinli xxxxx + personal jiaji xxxxx + personal jinli&jiaji xxxxx + procedural appworld xxxxx + procedural bfcl-v3 xxxxx + tool tool_guidelines xxxxx + identity self xxxxx + ``` + VectorRetrieveMemoryOp(list(memory_type, memory_target, query)), layer1+layer2 + ReadHistoryMemoryOp(ref_memory_id), layer3 +] + +## 额外的设计 + +### summary memory的作用 +```txt +step1: summary jinli's dialog + session1: List[Message] -> session2: List[Message] -> session3: List[Message] -> ... +summary 1 1 1 +personal 0 0 1 +procedural 0 1 0 + +step2: retrieve +vector_retrieve_memory(query, memory_type="personal", memory_target="jinli") -> memory_type in ["personal", "summary"] +``` +1. 相较于其他维度的memory,提供了一种通用维度的memory抽取逻辑 +2. 确保如果不符合其他的meta memory的情况下,有一个兜底的原始对话的索引。 + +### 带thinking参数的实验 +Inspired by Agentscope +``` +async def record_to_memory( + self, + thinking: str, + content: list[str], + **kwargs: Any, +) -> ToolResponse: + """Use this function to record important information that you may + need later. The target content should be specific and concise, e.g. + who, when, where, do what, why, how, etc. + + Args: + thinking (`str`): + Your thinking and reasoning about what to record + content (`list[str]`): + The content to remember, which is a list of strings. + """ +``` + +对比 +- thinking model +- instruct model +- instruct model with thinking_params @Inspired by Agentscope +- instruct model with thinking_tool @Inspired by claude + +### multi模式实验 +- tool是单次调用,模型多次调用工具 +- tool是多次调用,模型只需要调用一次工具 + +### 多版本-扩展性 +从BaseMemoryAgentOp继承: +personal_summary_agent_v2/personal_retrieve_agent_v2 @weikang +procedural_summary_agent_v2/procedural_retrieve_agent_v2 @zouyin + +### Hook模式/Agent +``` python +@memory_wrapper +async def call_agent(messages: List[Message]) -> List[Message]: + await agent.achat(messages) + history_messages: List[Message] = agent.history_messages + return history_messages +``` +or +``` +ReMyAgentV1Op << [ReadMetaMemoryOp, VectorRetrieveMemoryOp, ReadHistoryMemoryOp] ++ async call summary_service +``` + +### 项目组织 +新的方案:experimental or v2 or core +老的方案:v1 or 废弃 +- 文档&README如何组织这几部分 +- CookBook + - reme-agent + - 使用mem-agent和agentscope、langchain结合 + - mini-reme +v1->v2->v3? +personal -> 多种记忆 -> 融合多种记忆+agentic+渐进式 -> 文件系统? + -> MemoryModel + + +### 文件系统-涉及后续的代码再次重写 +难点:retrieve/add/update/delete 需要变化吗? +使用File工具? +grep/grob/ls/read_file/write_file/edit_file? +基模操作这些的能力稍差,qwen3-code的能力相对可以。 + +### short-term-memory +TODO, 单独设计 + +### 自我修改上下文 +summary_agent add_meta_memory直接修改上下文 +remy_agent 可以每次通过retrieve_identity_memory修改自己的状态 + +### 论文 +1. mem0 +2. agentscope +3. datajuicer + +### 模型 +xxx + +### hagging face建设 +1. 金融? +2. bfcl/appworld + + +### 看下竞品的文档 +看文档而不是看代码 +直接让云鹏用 + + +### 对外接口,对齐mem0 +https://docs.mem0.ai/core-concepts/memory-operations/add + +- import: + - basic: 去掉with,直接reme.memory.summary/retrieve,大幅降低developer的使用成本 + +- http[reme-http-server]: + - 只有summary_memory & retrieve_memory两个接口 + - vector-db相关接口:包括workspace & memory的更加详细的操作接口(v1只提供了dump,load) + +- mcp[reme-mcp-server]: + - 提供retrieve的[ReadMetaMemoryOp, VectorRetrieveMemoryOp, ReadHistoryMemoryOp] + + +#### +是否要提供一个方便用户二开的接口。 +reme-mini 学习文档 + + +1. add不是原子操作,https://docs.mem0.ai/core-concepts/memory-operations/add +2. search/update/delete算原子操作 +3. user_id是物理隔离+用户是谁两个概念;目前物理隔离是workspace_id, 用户是谁是memory_target(名字可换) +4. mem0支持了很多的vector_store,但是只有同步接口 + + +是否需要重载运算符 + + + +一起调研竞品: +1. 开源框架 +2. 商业化的Memory +3. 论文 + +我们相比竞品的优势: +1. agentic 渐进式 +2. 支持用户开发: + 用户使用接口: + 3. 顶层接口:summary / retrieve <=> add / search + 4. 底层接口(MCP):add/delete/update/retrieve/ + 用户二开接口: + xxx +3. mem-agent 模型 + + + + + +6 llm = {provider: "openai", model_name: "gpt-4-turbo", temperature: 0.7} + +import asyncio +from reme_ai import ReMeApp + + +async def main(): + + reme = ReMe( + # 这里之有golbal的配置 + llm = {provider: "openai", model_name: "gpt-4-turbo", temperature: 0.7}, + "embedding_model.default.model_name=text-embedding-v4", + "vector_store.default.backend=memory" + retrieve=Retriever(ops=Opa() >> OpB(),], # 修改只在自己的op内修改 + retrieve=Retriever(tools=[ToolA(), ToolB()], + summarizer=Summarizer(tools=[ToolA(), ToolB()] + ) + + # summary的参数优先基高于全局优先基 + result = await reme.summary( + workspace_id="task_workspace", + trajectories=[ + { + "messages": [ + {"role": "user", "content": "Help me create a project plan"} + ], + "score": 1.0 + } + ] + ) + + + reme.close() + +if __name__ == "__main__": + asyncio.run(main()) + + +# 通过python代码启动。reme.serve() + + + +class BasePipeline(object): + pass + + +class GraphPipeline(BasePipeline): + pass + + +class AgenticPipeline(BasePipeline): + pass + + +retrieve_pipeline = Pipeline( + ops=[ + BuildQueryOp(), + RecallVectorStoreOp(), + RerankMemoryOp(enable_llm_rerank=True, enable_score_filter=False, top_k=5), + RewriteMemoryOp(enable_llm_rewrite=True) + ], + tools=[BuildQueryOp(), RecallVectorStoreOp()], +) + + +1. 现有的op参数变更 +2. 新建自己的op +3. search的top_k 》初始化的优先级 +4. react_agent.tools = [xxx, xxx, xxx] + +""" +## summary memory +load_meta_skills() +summary_context(thinking, summary) +hand_off_agent(thinking, type, target) + +add_memory() +delete_memory() +update_memory() +retrieve_memory() + +update_tool_memory() +update_identity_memory(memory_content) + +## retrieve memory +load_meta_memory() << read_identity_memory() +retrieve_personal_memory(think, target, query) +retrieve_procedural_memory(think, target, query) +retrieve_tool_memory(think, target, tool_name) +read_history() + +# summary agent +meta-summarizer +identity_summary_agent +personal_summary_agent +procedural_summary_agent +tool_summary_agent + +# retrieve agent +meta-retrieve +llm_agent + + +# summary working memory +compact +compress +auto + +# reload working memory +grep_content(query, -5, 5, 0, 10) + + +## retrieve skills +load_meta_skills() +load_skills() +load_reference_skills() +execute_shell(skill_name, parameters) + + + +meta-summarizer << [ + summary_context(thinking, summary) + hand_off_agent(thinking, type, target) +] + +hand_off_agent << [ + identity_summary_agent << [update_identity_memory] + personal_summary_agent << [add_memory, delete_memory, update_memory, retrieve_memory] + procedural_summary_agent << [add_memory, delete_memory, update_memory, retrieve_memory] + tool_summary_agent << [update_tool_memory] +] + +meta-retrieve << [ + retrieve_personal_memory(think, target, query) + retrieve_procedural_memory(think, target, query) + retrieve_tool_memory(think, target, tool_name) + retrieve_history(think, id) +] + +llm_agent << [ + read_identity_memory() 主动读取 + ... +] + + + +1. memory: layers +2. op: agent + tools +2. retrieve: skills渐进式agentic + retriever << [ + load_meta_memory(), + ``` + 格式:"- (): " + personal jinli xxxxx + personal jiaji xxxxx + personal jinli&jiaji xxxxx + procedural appworld xxxxx + procedural bfcl-v3 xxxxx + tool tool_guidelines xxxxx + identity self xxxxx + ``` + RetrieveMemory(list(memory_type, memory_target, query))), layer1+layer2 + ReadHistory(ref_memory_id), layer3 + ] + + +personal ref_memory_id -> history + + +3. summary: + messages -> add_history_memory -> db (layer1 memory) + meta-summarizer << [ + load_meta_memory(), + + add_meta_memory(list(memory_type, memory_target)), + + add_summary_memory(summary_memory), -> layer2 memory + + hands_off_agent(list(memory_type, memory_target)) , + ] + personal_summary_agent << [add_memory, update_memory, delete_memory, VectorRetrieveMemoryOp(only layer1)] + procedural_summary_agent << [add_memory, update_memory, delete_memory, VectorRetrieveMemoryOp(only layer1)] # todo + tool_summary_agent << [add_memory, update_memory, VectorRetrieveMemoryOp(only layer1)] + identity_summary_agent << [read_identity_memory, update_identity_memory] + +5. thinking实验 + +6. multi模式实验 + +7. dialog_agent / hooking + + +kit/system +what is reme +key diff +develop +file system + + +experimental / v1 / v2 ? +""" \ No newline at end of file diff --git a/reme_ai/__init__.py b/reme_ai/__init__.py index bd3a7687..aaeb865e 100644 --- a/reme_ai/__init__.py +++ b/reme_ai/__init__.py @@ -28,6 +28,7 @@ "summary", "utils", "vector_store", + "ReMeApp", ] __version__ = "0.2.0.4" diff --git a/reme_ai/agent/react/agentic_retrieve_op.py b/reme_ai/agent/react/agentic_retrieve_op.py index aab28e1a..f0178887 100644 --- a/reme_ai/agent/react/agentic_retrieve_op.py +++ b/reme_ai/agent/react/agentic_retrieve_op.py @@ -203,7 +203,7 @@ async def async_execute(self): # Extract context management parameters from input, excluding messages # These will be passed to the context management pipeline - context_kwargs = self.input_dict.copy() + context_kwargs = self.context.copy() context_kwargs.pop("messages", None) # Main ReAct loop: iterate up to max_steps times diff --git a/reme_ai/agent/tools/llm_mock_search_op.py b/reme_ai/agent/tools/llm_mock_search_op.py index 91313e25..7af4bdc2 100644 --- a/reme_ai/agent/tools/llm_mock_search_op.py +++ b/reme_ai/agent/tools/llm_mock_search_op.py @@ -209,7 +209,7 @@ async def async_execute(self): This method classifies the query, applies the appropriate configuration, simulates delays, and generates search results based on success and relevance rates. """ - query: str = self.input_dict["query"] + query: str = self.context["query"] logger.info(f"LLMMockSearchOp processing query: {query}") # Step 1: Classify the query diff --git a/reme_ai/agent/tools/use_mock_search_op.py b/reme_ai/agent/tools/use_mock_search_op.py index 64098dc3..07f10f9a 100644 --- a/reme_ai/agent/tools/use_mock_search_op.py +++ b/reme_ai/agent/tools/use_mock_search_op.py @@ -89,7 +89,7 @@ async def async_execute(self): This method selects an appropriate tool, executes it, measures performance, and creates a ToolCallResult with metrics. """ - query: str = self.input_dict["query"] + query: str = self.context["query"] logger.info(f"query={query}") tool_ops = [ diff --git a/reme_ai/config/default.yaml b/reme_ai/config/default.yaml index a55e60df..a38ec7dc 100644 --- a/reme_ai/config/default.yaml +++ b/reme_ai/config/default.yaml @@ -254,14 +254,14 @@ llm: default: backend: openai_compatible model_name: qwen3-30b-a3b-instruct-2507 - params: - temperature: 0.6 token_count: # Optional backend: base qwen3_coder_plus: backend: openai_compatible model_name: qwen3-coder-plus + params: + temperature: 0.6 token_count: # Optional model_name: Qwen/Qwen3-Coder-480B-A35B-Instruct backend: hf @@ -367,7 +367,7 @@ embedding_model: vector_store: default: - backend: memory + backend: local embedding_model: default # params: # hosts: "http://localhost:9200" diff --git a/reme_ai/core/context/__init__.py b/reme_ai/core/context/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme_ai/core/context/base_context.py b/reme_ai/core/context/base_context.py new file mode 100644 index 00000000..e1d8f562 --- /dev/null +++ b/reme_ai/core/context/base_context.py @@ -0,0 +1,26 @@ +class BaseContext(dict): + """A dict subclass that supports attribute-style access and pickling.""" + + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") + + def __setattr__(self, name, value): + self[name] = value + + def __delattr__(self, name): + try: + del self[name] + except KeyError: + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") + + def __getstate__(self): + return dict(self) + + def __setstate__(self, state): + self.update(state) + + def __reduce__(self): + return self.__class__, (), self.__getstate__() diff --git a/reme_ai/core/context/prompt_handler.py b/reme_ai/core/context/prompt_handler.py new file mode 100644 index 00000000..184bcf6c --- /dev/null +++ b/reme_ai/core/context/prompt_handler.py @@ -0,0 +1,223 @@ +"""Prompt handler for loading and formatting prompt templates. + +This module provides a PromptHandler class that manages prompt templates +with support for YAML file loading, language-specific prompts, and +conditional formatting. +""" + +from pathlib import Path +from typing import Any + +import yaml +from loguru import logger + +from .base_context import BaseContext +from .service_context import C + + +class PromptHandler(BaseContext): + """Handler for loading, storing, and formatting prompt templates. + + This class extends BaseContext to provide prompt template management + with support for: + - Loading prompts from YAML files + - Language-specific prompt retrieval + - Conditional line filtering with boolean flags + - Variable substitution in prompts + + Attributes: + language: Language suffix for prompt retrieval (e.g., "zh", "en"). + + Example: + >>> handler = PromptHandler(language="zh") + >>> handler.load_prompt_by_file("prompts.yaml") + >>> prompt = handler.prompt_format("greeting", name="Alice", show_emoji=True) + """ + + def __init__(self, language: str = "", **kwargs): + """Initialize PromptHandler with language setting. + + Args: + language: Language suffix for prompt keys. If empty, uses + the global language setting from ServiceContext. + **kwargs: Additional context data to store. + """ + super().__init__(**kwargs) + self.language: str = language or C.language + + def load_prompt_by_file( + self, prompt_file_path: Path | str | None = None, strict: bool = False + ) -> "PromptHandler": + """Load prompts from a YAML file. + + Args: + prompt_file_path: Path to the YAML file containing prompts. + If None, returns self without loading. + strict: If True, raise FileNotFoundError when file doesn't exist. + If False, log warning and return self. + + Returns: + Self for method chaining. + + Raises: + FileNotFoundError: If strict=True and file doesn't exist. + """ + if prompt_file_path is None: + return self + + if isinstance(prompt_file_path, str): + prompt_file_path = Path(prompt_file_path) + + if not prompt_file_path.exists(): + if strict: + raise FileNotFoundError(f"Prompt file not found: {prompt_file_path}") + logger.warning(f"Prompt file not found: {prompt_file_path}") + return self + + with prompt_file_path.open(encoding="utf-8") as f: + prompt_dict = yaml.load(f, yaml.FullLoader) + self.load_prompt_dict(prompt_dict) + return self + + def load_prompt_dict(self, prompt_dict: dict | None = None) -> "PromptHandler": + """Load prompts from a dictionary. + + Args: + prompt_dict: Dictionary mapping prompt names to prompt strings. + Non-string values are ignored. + + Returns: + Self for method chaining. + """ + if not prompt_dict: + return self + + for key, value in prompt_dict.items(): + if isinstance(value, str): + if key in self: + self[key] = value + logger.warning(f"prompt_dict key={key} overwrite!") + else: + self[key] = value + logger.debug(f"add prompt_dict key={key}") + return self + + def _resolve_prompt_key(self, prompt_name: str) -> str: + """Resolve the actual prompt key with language suffix. + + Args: + prompt_name: Base prompt name. + + Returns: + Resolved key with language suffix if applicable. + """ + key = prompt_name + if self.language and not key.endswith(self.language.strip()): + key += "_" + self.language.strip() + return key + + def has_prompt(self, prompt_name: str) -> bool: + """Check if a prompt exists. + + Args: + prompt_name: Name of the prompt to check. + + Returns: + True if the prompt exists, False otherwise. + """ + key = self._resolve_prompt_key(prompt_name) + return key in self + + def get_prompt(self, prompt_name: str, default: Any = None) -> str: + """Get a prompt by name. + + Args: + prompt_name: Name of the prompt. Language suffix is automatically + appended if language is set. + default: Default value to return if prompt not found. + If None and prompt not found, raises KeyError. + + Returns: + The prompt string. + + Raises: + KeyError: If prompt not found and no default provided. + """ + key = self._resolve_prompt_key(prompt_name) + + if key not in self: + if default is not None: + return default + raise KeyError(f"Prompt not found: {key}") + return self[key] + + @staticmethod + def _filter_conditional_lines(prompt: str, flag_kwargs: dict[str, bool]) -> str: + """Filter prompt lines based on boolean flags. + + Lines starting with [flag_name] are conditionally included based on + the corresponding boolean value in flag_kwargs. + + Args: + prompt: The prompt string to filter. + flag_kwargs: Dictionary of flag names to boolean values. + + Returns: + Filtered prompt with conditional lines processed. + """ + split_prompt = [] + for line in prompt.strip().split("\n"): + hit = False + hit_flag = True + for key, flag in flag_kwargs.items(): + if not line.startswith(f"[{key}]"): + continue + + hit = True + hit_flag = flag + line = line.removeprefix(f"[{key}]") + break + + if not hit: + split_prompt.append(line) + elif hit_flag: + split_prompt.append(line) + + return "\n".join(split_prompt) + + def prompt_format(self, prompt_name: str, **kwargs) -> str: + """Get and format a prompt with variable substitution. + + Supports two types of keyword arguments: + - Boolean flags: Filter lines starting with [flag_name] based on the flag value. + - Other values: Substituted into the prompt using str.format(). + + Args: + prompt_name: Name of the prompt to format. + **kwargs: Variables for substitution. Boolean values are used for + conditional line filtering. + + Returns: + The formatted prompt string. + + Example: + Given prompt "greeting": + Hello {name}! + [formal]It's a pleasure to meet you. + [casual]What's up? + + >>> handler.prompt_format("greeting", name="Alice", formal=True, casual=False) + "Hello Alice!\\nIt's a pleasure to meet you." + """ + prompt = self.get_prompt(prompt_name) + + flag_kwargs = {k: v for k, v in kwargs.items() if isinstance(v, bool)} + other_kwargs = {k: v for k, v in kwargs.items() if not isinstance(v, bool)} + + if flag_kwargs: + prompt = self._filter_conditional_lines(prompt, flag_kwargs) + + if other_kwargs: + prompt = prompt.format(**other_kwargs) + + return prompt diff --git a/reme_ai/core/context/registry.py b/reme_ai/core/context/registry.py new file mode 100644 index 00000000..e23f80f5 --- /dev/null +++ b/reme_ai/core/context/registry.py @@ -0,0 +1,12 @@ +from .base_context import BaseContext + + +class Registry(BaseContext): + def register(self, name: str = "", add_cls: bool = True): + def decorator(cls): + if add_cls: + key = name or cls.__name__ + self._data[key] = cls + return cls + + return decorator diff --git a/reme_ai/core/context/runtime_context.py b/reme_ai/core/context/runtime_context.py new file mode 100644 index 00000000..8bb47408 --- /dev/null +++ b/reme_ai/core/context/runtime_context.py @@ -0,0 +1,92 @@ +import asyncio +import uuid +from typing import Optional + +from .base_context import BaseContext +from ..enumeration import ChunkEnum +from ..schema import FlowResponse +from ..schema import FlowStreamChunk + + +class RuntimeContext(BaseContext): + """Context for managing flow execution state and streaming. + + This class manages the state of a single flow execution, including: + - Flow identification + - Response handling + - Stream queue for asynchronous streaming + + Attributes: + flow_id: Unique identifier for the flow instance. + response: FlowResponse object for storing flow results. + stream_queue: Asynchronous queue for streaming chunks. + """ + + def __init__( + self, + flow_id: str = uuid.uuid4().hex, + response: Optional[FlowResponse] = None, + stream_queue: Optional[asyncio.Queue] = None, + **kwargs, + ): + """Initialize FlowContext with flow ID and optional components. + + Args: + flow_id: Unique identifier for the flow instance. + Defaults to a random UUID hex string. + response: FlowResponse object. If None, a new FlowResponse + will be created. + stream_queue: Asynchronous queue for streaming chunks. + **kwargs: Additional context data to store. + """ + super().__init__(**kwargs) + + self.flow_id: str = flow_id + self.response: Optional[FlowResponse] = response if response is not None else FlowResponse() + self.stream_queue: Optional[asyncio.Queue] = stream_queue + + async def add_stream_string_and_type(self, chunk: str, chunk_type: ChunkEnum): + """Add a stream chunk with string content and type to the stream queue. + + Args: + chunk: The string content to stream. + chunk_type: The type of the chunk. + + Returns: + Self for method chaining. + """ + stream_chunk = FlowStreamChunk(flow_id=self.flow_id, chunk_type=chunk_type, chunk=chunk) + await self.stream_queue.put(stream_chunk) + return self + + async def add_stream_chunk(self, stream_chunk: FlowStreamChunk): + """Add a stream chunk to the stream queue. + + Args: + stream_chunk: The stream chunk to add. + + Returns: + Self for method chaining. + """ + stream_chunk.flow_id = self.flow_id + await self.stream_queue.put(stream_chunk) + return self + + async def add_stream_done(self): + """Add a done signal to the stream queue. + + Returns: + Self for method chaining. + """ + done_chunk = FlowStreamChunk(flow_id=self.flow_id, chunk_type=ChunkEnum.DONE, chunk="", done=True) + await self.stream_queue.put(done_chunk) + return self + + def add_response_error(self, e: Exception): + """Add an error to the flow response. + + Args: + e: The exception to record as an error. + """ + self.response.success = False + self.response.answer = str(e.args) diff --git a/reme_ai/core/context/service_context.py b/reme_ai/core/context/service_context.py new file mode 100644 index 00000000..b0f7a6d9 --- /dev/null +++ b/reme_ai/core/context/service_context.py @@ -0,0 +1,290 @@ +"""Service context for managing global application state. + +This module provides a singleton service context that manages application-wide +configuration, registries, vector stores, and other shared resources. +""" + +import uuid +from concurrent.futures import ThreadPoolExecutor +from typing import Dict + +from .base_context import BaseContext +from .registry import Registry +from ..enumeration import RegistryEnum +from ..schema import ServiceConfig +from ..utils import singleton + + +@singleton +class ServiceContext(BaseContext): + """Singleton service context for global application state management. + + This class manages application-wide configuration including: + - Service configuration and identification + - Registry for models, operations, flows, and services + - Vector stores + - Thread pool executor + - External MCP tool calls + - Flow instances + + Attributes: + service_id: Unique identifier for the service instance. + service_config: Service configuration object. + language: Language setting for the service. + thread_pool: Thread pool executor for concurrent operations. + vector_store_dict: Dictionary of vector store instances. + external_mcp_tool_call_dict: Dictionary of external MCP tool calls. + registry_dict: Dictionary of registries by type. + flow_dict: Dictionary of flow instances. + """ + + def __init__(self, service_id: str | None = None, **kwargs): + """Initialize ServiceContext with service ID. + + Args: + service_id: Unique identifier for the service instance. + Defaults to a random UUID hex string. + **kwargs: Additional context data to store. + """ + super().__init__(**kwargs) + self.service_id: str = service_id or uuid.uuid4().hex + + self.service_config: ServiceConfig | None = None + self.language: str = "" + self.thread_pool: ThreadPoolExecutor | None = None + self.vector_store_dict: dict = {} + self.external_mcp_tool_call_dict: dict = {} + self.registry_dict: Dict[RegistryEnum, Registry] = {v: Registry() for v in RegistryEnum.__members__.values()} + self.flow_dict: dict = {} + + def register(self, name: str, register_type: RegistryEnum): + """Register a model class by name and type. + + Args: + name: Name to register the class under. + register_type: Type of registry (e.g., LLM, EMBEDDING_MODEL). + + Returns: + Decorator function for class registration. + """ + return self.registry_dict[register_type].register(name=name) + + def register_embedding_model(self, name: str = ""): + """Register an embedding model class. + + Args: + name: Name to register the class under. + + Returns: + Decorator function for class registration. + """ + return self.register(name=name, register_type=RegistryEnum.EMBEDDING_MODEL) + + def register_llm(self, name: str = ""): + """Register an LLM class. + + Args: + name: Name to register the class under. + + Returns: + Decorator function for class registration. + """ + return self.register(name=name, register_type=RegistryEnum.LLM) + + def register_vector_store(self, name: str = ""): + """Register a vector store class. + + Args: + name: Name to register the class under. + + Returns: + Decorator function for class registration. + """ + return self.register(name=name, register_type=RegistryEnum.VECTOR_STORE) + + def register_op(self, name: str = ""): + """Register an operation class. + + Args: + name: Name to register the class under. + + Returns: + Decorator function for class registration. + """ + return self.register(name=name, register_type=RegistryEnum.OP) + + def register_flow(self, name: str = ""): + """Register a flow class. + + Args: + name: Name to register the class under. + + Returns: + Decorator function for class registration. + """ + return self.register(name=name, register_type=RegistryEnum.FLOW) + + def register_service(self, name: str = ""): + """Register a service class. + + Args: + name: Name to register the class under. + + Returns: + Decorator function for class registration. + """ + return self.register(name=name, register_type=RegistryEnum.SERVICE) + + def register_token_counter(self, name: str = ""): + """Register a token counter class. + + Args: + name: Name to register the class under. + + Returns: + Decorator function for class registration. + """ + return self.register(name=name, register_type=RegistryEnum.TOKEN_COUNTER) + + def get_model_class(self, name: str, register_type: RegistryEnum): + """Get a registered model class by name and type. + + Args: + name: Name of the registered class. + register_type: Type of registry to search. + + Returns: + The registered class. + + Raises: + AssertionError: If the name is not found in the registry. + """ + assert name in self.registry_dict[register_type], ( + f"name={name} not found in registry_dict.{register_type.value}! " + f"supported names={self.registry_dict[register_type].keys()}" + ) + + return self.registry_dict[register_type][name] + + def get_embedding_model_class(self, name: str): + """Get a registered embedding model class. + + Args: + name: Name of the registered class. + + Returns: + The registered embedding model class. + + Raises: + AssertionError: If the name is not found in the registry. + """ + return self.get_model_class(name, RegistryEnum.EMBEDDING_MODEL) + + def get_llm_class(self, name: str): + """Get a registered LLM class. + + Args: + name: Name of the registered class. + + Returns: + The registered LLM class. + + Raises: + AssertionError: If the name is not found in the registry. + """ + return self.get_model_class(name, RegistryEnum.LLM) + + def get_vector_store_class(self, name: str): + """Get a registered vector store class. + + Args: + name: Name of the registered class. + + Returns: + The registered vector store class. + + Raises: + AssertionError: If the name is not found in the registry. + """ + return self.get_model_class(name, RegistryEnum.VECTOR_STORE) + + def get_op_class(self, name: str): + """Get a registered operation class. + + Args: + name: Name of the registered class. + + Returns: + The registered operation class. + + Raises: + AssertionError: If the name is not found in the registry. + """ + return self.get_model_class(name, RegistryEnum.OP) + + def get_flow_class(self, name: str): + """Get a registered flow class. + + Args: + name: Name of the registered class. + + Returns: + The registered flow class. + + Raises: + AssertionError: If the name is not found in the registry. + """ + return self.get_model_class(name, RegistryEnum.FLOW) + + def get_service_class(self, name: str): + """Get a registered service class. + + Args: + name: Name of the registered class. + + Returns: + The registered service class. + + Raises: + AssertionError: If the name is not found in the registry. + """ + return self.get_model_class(name, RegistryEnum.SERVICE) + + def get_token_counter_class(self, name: str): + """Get a registered token counter class. + + Args: + name: Name of the registered class. + + Returns: + The registered token counter class. + + Raises: + AssertionError: If the name is not found in the registry. + """ + return self.get_model_class(name, RegistryEnum.TOKEN_COUNTER) + + def get_vector_store(self, name: str = "default"): + """Get a vector store instance by name. + + Args: + name: Name of the vector store instance. Defaults to "default". + + Returns: + The vector store instance. + """ + return self.vector_store_dict[name] + + def get_flow(self, name: str = "default"): + """Get a flow instance by name. + + Args: + name: Name of the flow instance. Defaults to "default". + + Returns: + The flow instance. + """ + return self.flow_dict[name] + + +C = ServiceContext() diff --git a/reme_ai/core/enumeration/__init__.py b/reme_ai/core/enumeration/__init__.py new file mode 100644 index 00000000..f7650406 --- /dev/null +++ b/reme_ai/core/enumeration/__init__.py @@ -0,0 +1,13 @@ +"""Core enumeration module.""" + +from .chunk_enum import ChunkEnum +from .http_enum import HttpEnum +from .registry_enum import RegistryEnum +from .role import Role + +__all__ = [ + "ChunkEnum", + "HttpEnum", + "RegistryEnum", + "Role", +] diff --git a/reme_ai/core/enumeration/chunk_enum.py b/reme_ai/core/enumeration/chunk_enum.py new file mode 100644 index 00000000..103d10fd --- /dev/null +++ b/reme_ai/core/enumeration/chunk_enum.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class ChunkEnum(str, Enum): + THINK = "think" + + ANSWER = "answer" + + TOOL = "tool" + + USAGE = "usage" + + ERROR = "error" + + DONE = "done" diff --git a/reme_ai/core/enumeration/http_enum.py b/reme_ai/core/enumeration/http_enum.py new file mode 100644 index 00000000..4ae71d50 --- /dev/null +++ b/reme_ai/core/enumeration/http_enum.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class HttpEnum(str, Enum): + GET = "get" + + POST = "post" + + HEAD = "head" + + PUT = "put" + + DELETE = "delete" diff --git a/reme_ai/core/enumeration/registry_enum.py b/reme_ai/core/enumeration/registry_enum.py new file mode 100644 index 00000000..9bdcfbcc --- /dev/null +++ b/reme_ai/core/enumeration/registry_enum.py @@ -0,0 +1,17 @@ +from enum import Enum + + +class RegistryEnum(str, Enum): + LLM = "llm" + + EMBEDDING_MODEL = "embedding_model" + + VECTOR_STORE = "vector_store" + + OP = "op" + + FLOW = "flow" + + SERVICE = "service" + + TOKEN_COUNTER = "token_counter" diff --git a/reme_ai/core/enumeration/role.py b/reme_ai/core/enumeration/role.py new file mode 100644 index 00000000..54bd0b16 --- /dev/null +++ b/reme_ai/core/enumeration/role.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class Role(str, Enum): + SYSTEM = "system" + + USER = "user" + + ASSISTANT = "assistant" + + TOOL = "tool" diff --git a/reme_ai/core/schema/__init__.py b/reme_ai/core/schema/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme_ai/core/schema/message.py b/reme_ai/core/schema/message.py new file mode 100644 index 00000000..3d8a7596 --- /dev/null +++ b/reme_ai/core/schema/message.py @@ -0,0 +1,143 @@ +import datetime +from typing import List + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from .tool_call import ToolCall +from ..enumeration import Role + + +class ContentBlock(BaseModel): + """ + examples: + { + "type": "image_url", + "image_url": { + "url": "https://img.alicdn.com/imgextra/i1/O1CN01gDEY8M1W114Hi3XcN_!!6000000002727-0-tps-1024-406.jpg" + }, + } + { + "type": "video", + "video": [ + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/xzsgiz/football1.jpg", + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/tdescd/football2.jpg", + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/zefdja/football3.jpg", + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/aedbqh/football4.jpg", + ], + } + { + "type": "text", + "text": "How do you solve this problem?" + } + """ + + model_config = ConfigDict(extra="allow") + + type: str = Field(default="") + content: str | dict | list = Field(default="") + + @model_validator(mode="before") + @classmethod + def init_block(cls, data: dict): + """Initialize content block by extracting content based on type field.""" + result = data.copy() + content_type = data.get("type", "") + if content_type and content_type in data: + result["content"] = data[content_type] + return result + + def simple_dump(self) -> dict: + """Convert ContentBlock to a simple dictionary format.""" + result = { + "type": self.type, + self.type: self.content, + **self.model_extra, + } + + return result + + +class Message(BaseModel): + """Represents a message in a conversation with role, content, and optional tool calls.""" + + name: str | None = Field(default=None) + role: Role = Field(default=Role.USER) + content: str | List[ContentBlock] = Field(default="") + reasoning_content: str = Field(default="") + tool_calls: List[ToolCall] = Field(default_factory=list) + tool_call_id: str = Field(default="") + time_created: str = Field(default_factory=lambda: datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + metadata: dict = Field(default_factory=dict) + + def dump_content(self) -> str | list[dict]: + """Dump message content to string or list of dicts.""" + if isinstance(self.content, str): + return self.content + elif isinstance(self.content, list): + return [x.simple_dump() for x in self.content] + else: + raise ValueError(f"Invalid content type: {type(self.content)}") + + def simple_dump(self, add_reasoning: bool = True) -> dict: + """Convert Message to a simple dictionary format for API serialization.""" + result: dict = {"role": self.role.value, "content": self.dump_content()} + + if add_reasoning: + result["reasoning_content"] = self.reasoning_content + + if self.tool_calls: + result["tool_calls"] = [x.simple_output_dump() for x in self.tool_calls] + + if self.tool_call_id: + result["tool_call_id"] = self.tool_call_id + + return result + + def format_message( + self, + i: int | None = None, + add_time_created: bool = False, + use_name_first: bool = False, + add_reasoning_content: bool = True, + add_tool_calls: bool = True + ) -> str: + content = "" + if i is not None: + content += f"round{i} " + + if add_time_created: + content += f"[{self.time_created}] " + + if use_name_first: + content += f"{self.name or self.role.value}:\n" + else: + content += f"{self.role.value}:\n" + + if add_reasoning_content and self.reasoning_content: + content += self.reasoning_content + "\n" + + if self.content: + if isinstance(self.content, str): + content += self.content + "\n" + else: + # Handle List[ContentBlock] case + for block in self.content: + if block.type == "text": + content += str(block.content) + "\n" + else: + content += f"[{block.type}]\n" + + if add_tool_calls and self.tool_calls: + for tool_call in self.tool_calls: + content += f" - tool_call={tool_call.name} params={tool_call.arguments}\n" + + return content.strip() + + +class Trajectory(BaseModel): + """Represents a conversation trajectory with messages and optional scoring.""" + + task_id: str = Field(default="") + messages: List[Message] = Field(default_factory=list) + score: float = Field(default=0.0) + metadata: dict = Field(default_factory=dict) diff --git a/reme_ai/core/schema/request.py b/reme_ai/core/schema/request.py new file mode 100644 index 00000000..17214621 --- /dev/null +++ b/reme_ai/core/schema/request.py @@ -0,0 +1,14 @@ +from typing import List + +from pydantic import Field, BaseModel, ConfigDict + +from .message import Message + + +class Request(BaseModel): + model_config = ConfigDict(extra="allow") + + workspace_id: str = Field(default="") + query: str = Field(default="") + messages: List[Message] = Field(default_factory=list) + metadata: dict = Field(default_factory=dict) diff --git a/reme_ai/core/schema/response.py b/reme_ai/core/schema/response.py new file mode 100644 index 00000000..f9367023 --- /dev/null +++ b/reme_ai/core/schema/response.py @@ -0,0 +1,8 @@ +from pydantic import Field, BaseModel + + +class Response(BaseModel): + + answer: str | dict | list = Field(default="") + success: bool = Field(default=True) + metadata: dict = Field(default_factory=dict) diff --git a/reme_ai/core/schema/service_config.py b/reme_ai/core/schema/service_config.py new file mode 100644 index 00000000..91355476 --- /dev/null +++ b/reme_ai/core/schema/service_config.py @@ -0,0 +1,77 @@ +from typing import Dict, List + +from pydantic import BaseModel, Field + +from .tool_call import ToolCall + + +class MCPConfig(BaseModel): + transport: str = Field(default="") + host: str = Field(default="0.0.0.0") + port: int = Field(default=8001) + + +class HttpConfig(BaseModel): + host: str = Field(default="0.0.0.0") + port: int = Field(default=8001) + timeout_keep_alive: int = Field(default=3600) + limit_concurrency: int = Field(default=1000) + + +class CmdConfig(BaseModel): + flow: str = Field(default="") + params: dict = Field(default_factory=dict) + + +class FlowConfig(ToolCall): + flow_content: str = Field(default="") + stream: bool = Field(default=False) + enable_cache: bool = Field(default=False, description="Enable non-stream response caching") + cache_path: str = Field(default="cache/{flow_name}", description="Cache path template; supports {flow_name}") + cache_expire_hours: float = Field(default=0.1, description="Cache TTL (hours)") + + +class LLMTokenCountConfig(BaseModel): + backend: str = Field(default="base") + model_name: str = Field(default="") + params: dict = Field(default_factory=dict) + + +class LLMConfig(BaseModel): + backend: str = Field(default="") + model_name: str = Field(default="") + token_count: LLMTokenCountConfig = Field(default_factory=LLMTokenCountConfig) + params: dict = Field(default_factory=dict) + + +class EmbeddingModelConfig(BaseModel): + backend: str = Field(default="") + model_name: str = Field(default="") + params: dict = Field(default_factory=dict) + + +class VectorStoreConfig(BaseModel): + backend: str = Field(default="") + embedding_model: str = Field(default="") + params: dict = Field(default_factory=dict) + + +class ServiceConfig(BaseModel): + backend: str = Field(default="") + enable_logo: bool = Field(default=True) + language: str = Field(default="") + thread_pool_max_workers: int = Field(default=16) + ray_max_workers: int = Field(default=-1) + init_logger: bool = Field(default=True) + disabled_flows: List[str] = Field(default_factory=list) + enabled_flows: List[str] = Field(default_factory=list) + + cmd: CmdConfig = Field(default_factory=CmdConfig) + mcp: MCPConfig = Field(default_factory=MCPConfig) + external_mcp: Dict[str, dict] = Field(default_factory=dict, description="External MCP Server config") + http: HttpConfig = Field(default_factory=HttpConfig) + flow: Dict[str, FlowConfig] = Field(default_factory=dict) + llm: Dict[str, LLMConfig] = Field(default_factory=dict) + embedding_model: Dict[str, EmbeddingModelConfig] = Field(default_factory=dict) + vector_store: Dict[str, VectorStoreConfig] = Field(default_factory=dict) + metadata: dict = Field(default_factory=dict) diff --git a/reme_ai/core/schema/stream_chunk.py b/reme_ai/core/schema/stream_chunk.py new file mode 100644 index 00000000..df5d7d79 --- /dev/null +++ b/reme_ai/core/schema/stream_chunk.py @@ -0,0 +1,11 @@ +from pydantic import Field, BaseModel + +from ..enumeration import ChunkEnum + + +class FlowStreamChunk(BaseModel): + flow_id: str = Field(default="") + chunk_type: ChunkEnum = Field(default=ChunkEnum.ANSWER) + chunk: str | dict | list = Field(default="") + done: bool = Field(default=False) + metadata: dict = Field(default_factory=dict) diff --git a/reme_ai/core/schema/tool_call.py b/reme_ai/core/schema/tool_call.py new file mode 100644 index 00000000..f0bd7da3 --- /dev/null +++ b/reme_ai/core/schema/tool_call.py @@ -0,0 +1,203 @@ +import json +from typing import Dict, List, Literal + +from mcp.types import Tool +from pydantic import BaseModel, Field, model_validator, ConfigDict + +TOOL_ATTR_TYPE = Literal["string", "array", "integer", "number", "boolean", "object"] + + +class ToolAttr(BaseModel): + """Attributes for tool parameters.""" + + type: TOOL_ATTR_TYPE = Field(default="string", description="tool attribute type") + description: str = Field(default="", description="tool attribute description") + required: bool = Field(default=True, description="tool attribute required") + enum: List[str] | None = Field(default=None, description="tool attribute enum") + items: dict = Field(default_factory=dict, description="tool attribute items") + + model_config = ConfigDict(extra="allow") + + def simple_input_dump(self, version: str = "default") -> dict: + """Convert ToolAttr to input format dictionary for API requests.""" + if version == "default": + result: dict = { + "type": self.type, + "description": self.description, + } + + if self.enum: + result["enum"] = self.enum + + if self.items: + result["items"] = self.items + + return result + + else: + raise NotImplementedError(f"version {version} not supported") + + +class ToolCall(BaseModel): + """ + input: + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "It is very useful when you want to check the weather of a specified city.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "Cities or counties, such as Beijing, Hangzhou, Yuhang District, etc.", + } + }, + "required": ["location"] + } + } + } + output: + { + "index": 0, + "id": "call_6596dafa2a6a46f7a217da", + "function": { + "arguments": "{\"location\": \"Beijing\"}", + "name": "get_current_weather" + }, + "type": "function", + } + """ + + index: int = Field(default=0) + id: str = Field(default="") + type: str = Field(default="function") + name: str = Field(default="") + + arguments: str = Field(default="", description="tool execution arguments") + + description: str = Field(default="") + input_schema: Dict[str, ToolAttr] = Field(default_factory=dict) + output_schema: Dict[str, ToolAttr] = Field(default_factory=dict) + + @model_validator(mode="before") + @classmethod + def init_tool_call(cls, data: dict): + """Initialize ToolCall from raw data dictionary, extracting function info.""" + tool_type = data.get("type", "") + tool_type_dict = data.get(tool_type, {}) + + # Create a new dict to avoid modifying the original data + result = data.copy() + + if "name" in tool_type_dict: + result["name"] = tool_type_dict["name"] + + if "arguments" in tool_type_dict: + result["arguments"] = tool_type_dict["arguments"] + + if "description" in tool_type_dict: + result["description"] = tool_type_dict["description"] + + if "parameters" in tool_type_dict: + parameters = tool_type_dict["parameters"] + properties: dict = parameters.get("properties", {}) + required: list = parameters.get("required", []) + result["input_schema"] = {} + for name, param_attrs in properties.items(): + tool_attr = ToolAttr(**param_attrs) + tool_attr.required = name in required + result["input_schema"][name] = tool_attr + + return result + + @property + def argument_dict(self) -> dict: + """Parse and return arguments as a dictionary.""" + return json.loads(self.arguments) + + def check_argument(self) -> bool: + """Check if arguments can be parsed as valid JSON.""" + try: + _ = self.argument_dict + return True + except Exception: + return False + + @staticmethod + def _build_schema_dict(schema: Dict[str, ToolAttr]) -> dict: + required_list = [] + properties = {} + for name, tool_attr in schema.items(): + if tool_attr.required: + required_list.append(name) + properties[name] = tool_attr.simple_input_dump() + + return { + "type": "object", + "properties": properties, + "required": required_list, + } + + def simple_input_dump(self, version: str = "default") -> dict: + """Convert ToolCall to input format dictionary for API requests.""" + if version == "default": + return { + "type": self.type, + self.type: { + "name": self.name, + "description": self.description, + "parameters": self._build_schema_dict(self.input_schema), + }, + } + + else: + raise NotImplementedError(f"version {version} not supported") + + def simple_output_dump(self, version: str = "default") -> dict: + """Convert ToolCall to output format dictionary for API responses.""" + if version == "default": + return { + "index": self.index, + "id": self.id, + self.type: { + "arguments": self.arguments, + "name": self.name, + }, + "type": self.type, + } + else: + raise NotImplementedError(f"version {version} not supported") + + @classmethod + def from_mcp_tool(cls, tool: Tool) -> "ToolCall": + """Create a ToolCall instance from an MCP Tool object.""" + input_schema = {} + properties = tool.inputSchema.get("properties", {}) + required = tool.inputSchema.get("required", []) + for name, attr_dict in properties.items(): + tool_attr = ToolAttr(**attr_dict) + if name in required: + tool_attr.required = True + input_schema[name] = tool_attr + + return cls( + name=tool.name, + description=tool.description or "", + input_schema=input_schema, + ) + + def to_mcp_tool(self) -> Tool: + """Convert this ToolCall to an MCP Tool object.""" + kwargs = { + "name": self.name, + "description": self.description, + "inputSchema": self._build_schema_dict(self.input_schema), + } + + # Build outputSchema from output_schema if present + if self.output_schema: + kwargs["outputSchema"] = self._build_schema_dict(self.output_schema) + + return Tool(**kwargs) diff --git a/reme_ai/core/schema/vector_node.py b/reme_ai/core/schema/vector_node.py new file mode 100644 index 00000000..480aefbe --- /dev/null +++ b/reme_ai/core/schema/vector_node.py @@ -0,0 +1,16 @@ +"""Vector node schema for representing nodes with embeddings.""" + +from typing import List +from uuid import uuid4 + +from pydantic import BaseModel, Field + + +class VectorNode(BaseModel): + """Represents a node with content, vector embeddings, and metadata.""" + + unique_id: str = Field(default_factory=lambda: uuid4().hex) + workspace_id: str = Field(default="") + content: str = Field(default="") + vector: List[float] | None = Field(default=None) + metadata: dict = Field(default_factory=dict) diff --git a/reme_ai/retrieve/working/grep_op.py b/reme_ai/retrieve/working/grep_op.py index eaa30642..741938df 100644 --- a/reme_ai/retrieve/working/grep_op.py +++ b/reme_ai/retrieve/working/grep_op.py @@ -56,9 +56,9 @@ def build_tool_call(self) -> ToolCall: async def async_execute(self): """Execute the grep search operation.""" - pattern: str = self.input_dict.get("pattern", "").strip() - file_path: str | None | Path = self.input_dict.get("file_path", "") - limit: int = int(self.input_dict.get("limit", 50)) + pattern: str = self.context.get("pattern", "").strip() + file_path: str | None | Path = self.context.get("file_path", "") + limit: int = int(self.context.get("limit", 50)) assert pattern, "The 'pattern' parameter cannot be empty." assert file_path, "The 'file_path' parameter is required." @@ -88,7 +88,7 @@ async def async_execute(self): async def async_default_execute(self, e: Exception = None, **_kwargs): """Fill outputs with a default failure message when execution fails.""" - pattern: str = self.input_dict.get("pattern", "").strip() + pattern: str = self.context.get("pattern", "").strip() error_msg = f'Failed to search for pattern "{pattern}"' if e: error_msg += f": {str(e)}" diff --git a/reme_ai/retrieve/working/read_file_op.py b/reme_ai/retrieve/working/read_file_op.py index 0d2a8f93..522f121d 100644 --- a/reme_ai/retrieve/working/read_file_op.py +++ b/reme_ai/retrieve/working/read_file_op.py @@ -55,9 +55,9 @@ def build_tool_call(self) -> ToolCall: async def async_execute(self): """Execute the read file operation.""" - file_path: str = self.input_dict.get("file_path", "").strip() - offset: Optional[int] = int(self.input_dict.get("offset")) - limit: Optional[int] = int(self.input_dict.get("limit")) + file_path: str = self.context.get("file_path", "").strip() + offset: Optional[int] = int(self.context.get("offset")) + limit: Optional[int] = int(self.context.get("limit")) # Validate and resolve file path assert file_path, "The 'file_path' parameter cannot be empty." @@ -84,7 +84,7 @@ async def async_execute(self): async def async_default_execute(self, e: Exception = None, **_kwargs): """Fill outputs with a default failure message when execution fails.""" - file_path: str = self.input_dict.get("file_path", "").strip() + file_path: str = self.context.get("file_path", "").strip() error_msg = f'Failed to read file "{file_path}"' if e: error_msg += f": {str(e)}" diff --git a/reme_ai/retrieve/working/write_file_op.py b/reme_ai/retrieve/working/write_file_op.py index 20097c79..93b9b7d4 100644 --- a/reme_ai/retrieve/working/write_file_op.py +++ b/reme_ai/retrieve/working/write_file_op.py @@ -49,8 +49,8 @@ def build_tool_call(self) -> ToolCall: async def async_execute(self): """Execute the write file operation.""" - file_path: str = self.input_dict.get("file_path", "").strip() - content: str = self.input_dict.get("content", "") + file_path: str = self.context.get("file_path", "").strip() + content: str = self.context.get("content", "") # Validate file_path if not file_path: @@ -82,7 +82,7 @@ async def async_execute(self): async def async_default_execute(self, e: Exception = None, **_kwargs): """Fill outputs with a default failure message when execution fails.""" - file_path: str = self.input_dict.get("file_path", "").strip() + file_path: str = self.context.get("file_path", "").strip() error_msg = f'Failed to write file "{file_path}"' if e: error_msg += f": {str(e)}" diff --git a/reme_ai/v2/__init__.py b/reme_ai/v2/__init__.py new file mode 100644 index 00000000..bcf782a8 --- /dev/null +++ b/reme_ai/v2/__init__.py @@ -0,0 +1,2 @@ +from flowllm.core.op import BaseAsyncToolOp, BaseAsyncOp +from flowllm.core.context import C diff --git a/reme_ai/v2/agent/__init__.py b/reme_ai/v2/agent/__init__.py new file mode 100644 index 00000000..667803f3 --- /dev/null +++ b/reme_ai/v2/agent/__init__.py @@ -0,0 +1,7 @@ +from . import v1 +from .base_memory_agent_op import BaseMemoryAgentOp + +__all__ = [ + "BaseMemoryAgentOp", + "v1", +] \ No newline at end of file diff --git a/reme_ai/v2/agent/base_memory_agent_op.py b/reme_ai/v2/agent/base_memory_agent_op.py new file mode 100644 index 00000000..af921e74 --- /dev/null +++ b/reme_ai/v2/agent/base_memory_agent_op.py @@ -0,0 +1,201 @@ +import asyncio +from abc import ABC +from typing import List, Dict + +from loguru import logger + +from .. import BaseAsyncToolOp +from ..enumeration import Role, MemoryType +from ..schema import Message, ToolCall +from ..tool import ThinkToolOp + + +class BaseMemoryAgentOp(BaseAsyncToolOp, ABC): + memory_type: MemoryType | None = None + + def __init__( + self, + max_steps: int = 20, + tool_call_interval: float = 0, + add_think_tool: bool = False, # only for instruct model + **kwargs, + ): + super().__init__(**kwargs) + self.max_steps: int = max_steps + self.tool_call_interval: float = tool_call_interval + self.add_think_tool: bool = add_think_tool + + def build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "base memory agent", + "input_schema": { + "workspace_id": { + "type": "string", + "description": "workspace id", + "required": True, + }, + "query": { + "type": "string", + "description": "query text", + "required": False, + }, + "messages": { + "type": "array", + "description": "messages", + "required": False, + "items": {"type": "object"}, + }, + }, + }, + ) + + async def build_tool_op_dict(self) -> Dict[str, BaseAsyncToolOp]: + tool_op_dict: Dict[str, BaseAsyncToolOp] = { + op.tool_call.name: op for op in self.ops.values() if isinstance(op, BaseAsyncToolOp) + } + for op in tool_op_dict.values(): + op.language = self.language + + if self.add_think_tool: + think_op = ThinkToolOp(language=self.language) + tool_op_dict["think_tool"] = think_op + + return tool_op_dict + + def get_messages(self) -> List[Message]: + if self.context.get("query"): + messages = [ + Message( + role=Role.USER, + content=self.context.query, + ) + ] + + elif self.context.get("messages"): + messages = [Message(**msg) if isinstance(msg, dict) else msg for msg in self.context.messages] + + else: + raise ValueError("input must have either `query` or `messages`") + + return messages + + async def build_messages(self) -> List[Message]: + return self.get_messages() + + async def _reasoning_step( + self, + messages: List[Message], + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + ) -> tuple[Message, bool]: + assistant_message: Message = await self.llm.achat( + messages=messages, + tools=[op.tool_call for op in tool_op_dict.values()], + ) + messages.append(assistant_message) + logger.info(f"step{step + 1}.assistant={assistant_message.model_dump_json()}") + should_act = bool(assistant_message.tool_calls) + return assistant_message, should_act + + async def _acting_step( + self, + assistant_message: Message, + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + **kwargs, + ) -> List[Message]: + if not assistant_message.tool_calls: + return [] + + op_list: List[BaseAsyncToolOp] = [] + has_think_tool_flag: bool = False + tool_result_messages: List[Message] = [] + think_op = tool_op_dict.get("think_tool", None) + + for j, tool_call in enumerate(assistant_message.tool_calls): + if think_op is not None and tool_call.name == think_op.tool_call.name: + has_think_tool_flag = True + + if tool_call.name not in tool_op_dict: + logger.exception(f"unknown tool_call.name={tool_call.name}") + continue + + logger.info(f"step{step + 1}.{j} submit tool_calls={tool_call.name} argument={tool_call.argument_dict}") + op_copy: BaseAsyncToolOp = tool_op_dict[tool_call.name].copy() + op_copy.tool_call.id = tool_call.id + op_list.append(op_copy) + self.submit_async_task(op_copy.async_call, + **kwargs, + **tool_call.argument_dict) + if self.tool_call_interval > 0: + await asyncio.sleep(self.tool_call_interval) + + await self.join_async_task() + + for j, op in enumerate(op_list): + tool_result = str(op.output) + tool_message = Message( + role=Role.TOOL, + content=tool_result, + tool_call_id=op.tool_call.id, + ) + tool_result_messages.append(tool_message) + logger.info(f"step{step + 1}.{j} join tool_result={tool_result[:200]}...\n\n") + + if self.add_think_tool: + if not has_think_tool_flag: + tool_op_dict["think_tool"] = think_op + else: + tool_op_dict.pop("think_tool", None) + + return tool_result_messages + + async def react(self, messages: List[Message], tool_op_dict: Dict[str, BaseAsyncToolOp]): + success: bool = False + for step in range(self.max_steps): + assistant_message, should_act = await self._reasoning_step(messages, tool_op_dict, step) + + if not should_act: + success = True + break + + tool_result_messages = await self._acting_step(assistant_message, tool_op_dict, step) + messages.extend(tool_result_messages) + + return messages, success + + async def async_execute(self): + tool_op_dict = await self.build_tool_op_dict() + + messages = await self.build_messages() + for i, message in enumerate(messages): + logger.info(f"step0.{i} {message.role} {message.name or ''} {message.simple_dump()}") + + messages, success = await self.react(messages, tool_op_dict) + if messages: + if success: + self.set_output(messages[-1].content) + else: + self.set_output(f"react is not complete with content:\n{messages[-1].content}") + else: + self.set_output("empty messages") + self.context.response.metadata["messages"] = messages + self.context.response.metadata["success"] = success + + @property + def memory_target(self) -> str: + return self.context.get("memory_target", "") + + @property + def ref_memory_id(self) -> str: + return self.context.get("ref_memory_id", "") + + @property + def workspace_id(self) -> str: + return self.context.get("workspace_id", "default") + + @property + def author(self) -> str: + _ = self.llm + return self.llm_config.model_name diff --git a/reme_ai/v2/agent/v1/__init__.py b/reme_ai/v2/agent/v1/__init__.py new file mode 100644 index 00000000..c7761beb --- /dev/null +++ b/reme_ai/v2/agent/v1/__init__.py @@ -0,0 +1,17 @@ +from .identity_summary_agent_v1_op import IdentitySummaryAgentV1Op +from .personal_summary_agent_v1_op import PersonalSummaryAgentV1Op +from .procedural_summary_agent_v1_op import ProceduralSummaryAgentV1Op +from .reme_retrieve_agent_v1_op import ReMeRetrieveAgentV1Op +from .reme_summary_agent_v1_op import ReMeSummaryAgentV1Op +from .remy_agent_v1_op import ReMyAgentV1Op +from .tool_summary_agent_v1_op import ToolSummaryAgentV1Op + +__all__ = [ + "IdentitySummaryAgentV1Op", + "PersonalSummaryAgentV1Op", + "ProceduralSummaryAgentV1Op", + "ReMeRetrieveAgentV1Op", + "ReMeSummaryAgentV1Op", + "ReMyAgentV1Op", + "ToolSummaryAgentV1Op", +] diff --git a/reme_ai/v2/agent/v1/identity_summarizer_v1_op.py b/reme_ai/v2/agent/v1/identity_summarizer_v1_op.py new file mode 100644 index 00000000..f8702748 --- /dev/null +++ b/reme_ai/v2/agent/v1/identity_summarizer_v1_op.py @@ -0,0 +1,101 @@ +"""Identity summary agent for extracting and storing agent self-cognition memories. + +This module provides the IdentitySummaryAgentV1Op class for analyzing conversation context +to extract and update identity memories (self-cognition, personality, current state). +""" + +from typing import List, Dict + +from flowllm.core.op import BaseAsyncToolOp + +from ..base_memory_agent_op import BaseMemoryAgentOp +from ... import C +from ... import utils +from ...enumeration import Role, MemoryType +from ...schema import Message, ToolCall + + +@C.register_op() +class IdentitySummaryAgentV1Op(BaseMemoryAgentOp): + """Agent for extracting and storing identity memories. + + This agent analyzes conversation context to extract identity-related information + such as self-cognition, personality traits, and current state, then updates + the identity memory using file-based storage. + """ + + memory_type: MemoryType = MemoryType.IDENTITY + + def build_tool_call(self) -> ToolCall: + """Build the tool call schema for identity summary agent. + + Returns: + ToolCall: Tool call configuration with workspace_id/query/messages input schema. + """ + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "input_schema": { + "workspace_id": { + "type": "string", + "description": "workspace_id", + "required": True, + }, + "query": { + "type": "string", + "description": "query", + "required": False, + }, + "messages": { + "type": "array", + "description": "messages", + "required": False, + "items": {"type": "object"}, + }, + }, + }, + ) + + async def build_messages(self) -> List[Message]: + """Build the initial messages for the identity summary agent. + + Constructs messages by: + 1. Combining query and messages into context + 2. Building system prompt with context and current time + 3. Adding user message to trigger analysis + + Returns: + List[Message]: Complete message list with system prompt and user message. + """ + now_time: str = utils.get_now_time() + context: str = utils.format_messages(self.get_messages()) + + messages = [ + Message( + role=Role.SYSTEM, + content=self.prompt_format( + prompt_name="system_prompt", + now_time=now_time, + context=context, + memory_type=self.memory_type.value, + ), + ), + Message( + role=Role.USER, + content=self.get_prompt("user_message"), + ), + ] + return messages + + async def _acting_step( + self, + assistant_message: Message, + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + **kwargs, + ) -> List[Message]: + return await super()._acting_step(assistant_message, + tool_op_dict, + step, + workspace_id=self.workspace_id, + author=self.author) \ No newline at end of file diff --git a/reme_ai/v2/agent/v1/identity_summary_agent_v1_prompt.yaml b/reme_ai/v2/agent/v1/identity_summary_agent_v1_prompt.yaml new file mode 100644 index 00000000..a0361633 --- /dev/null +++ b/reme_ai/v2/agent/v1/identity_summary_agent_v1_prompt.yaml @@ -0,0 +1,57 @@ +tool: | + Update agent self-cognition based on conversation context. + First read existing self-cognition using `read_identity_memory`, then analyze the context to determine if updates are needed, and use `update_identity_memory` to update when necessary. + +tool_zh: | + 根据对话上下文更新代理的自我认知。 + 首先使用 `read_identity_memory` 读取现有的自我认知,然后分析上下文判断是否需要更新,必要时使用 `update_identity_memory` 进行更新。 + +system_prompt: | + You are a specialized memory agent in the domain of self-awareness. Your task is to update the main agent's self-perception based on the provided context. + + ## Context: + {context} + + ## Current Time: + {now_time} + + ## Your Responsibilities: + + 1. **Read the main agent's current self-perception**: Retrieve it using `read_identity_memory`. + + 2. **Analyze the context** to determine whether an update to self-perception is needed: + - Extract any self-perception–related information from the dialogue (e.g., self-awareness, personality traits, current state, etc.). + - If no relevant self-perception information is found, output `` and halt further processing. + + 3. **Update if necessary**: Use `update_identity_memory` to perform the update: + - Compare the extracted information with the existing self-perception. + - If an update is required (due to new information, corrections, or additions), invoke `update_identity_memory`. + - If no update is needed, output ``. + +user_message: | + Please analyze the context and update the main agent's self-perception if necessary. + +system_prompt_zh: | + 你是一名在自我认知领域的专业记忆Agent。你的任务是根据上下文更新主Agent的自我认知。 + + ## 上下文: + {context} + + ## 当前时间: + {now_time} + + ## 你的任务: + 1. **读取现有主agent的自我认知**:使用 `read_identity_memory` 获取。 + + 2. **分析上下文**判断是否需要更新自我认知: + - 从对话中提取自我认知相关信息(如自我意识、个性特征、当前状态等)。 + - 如果未找到相关自我认知信息,输出 `` 并停止。 + + 3. **必要时更新**:使用 `update_identity_memory` 进行更新: + - 将提取的信息与现有自我认知进行对比。 + - 如需更新(新信息、修正或补充),使用 `update_identity_memory` 更新。 + - 如无需更新,输出 ``。 + +user_message_zh: | + 请分析上下文,必要时更新主Agent的自我认知。 + diff --git a/reme_ai/v2/agent/v1/personal_summary_agent_v1_op.py b/reme_ai/v2/agent/v1/personal_summary_agent_v1_op.py new file mode 100644 index 00000000..f38457e5 --- /dev/null +++ b/reme_ai/v2/agent/v1/personal_summary_agent_v1_op.py @@ -0,0 +1,87 @@ +from typing import List, Dict + +from flowllm.core.op import BaseAsyncToolOp + +from ..base_memory_agent_op import BaseMemoryAgentOp +from ... import C +from ... import utils +from ...enumeration import Role, MemoryType +from ...schema import Message, ToolCall + + +@C.register_op() +class PersonalSummaryAgentV1Op(BaseMemoryAgentOp): + memory_type: MemoryType = MemoryType.PERSONAL + + def build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "input_schema": { + "workspace_id": { + "type": "string", + "description": "workspace_id", + "required": True, + }, + "memory_target": { + "type": "string", + "description": "memory_target", + "required": True, + }, + "query": { + "type": "string", + "description": "query", + "required": False, + }, + "messages": { + "type": "array", + "description": "messages", + "required": False, + "items": {"type": "object"}, + }, + "ref_memory_id": { + "type": "string", + "description": "ref_memory_id", + "required": True, + }, + }, + }, + ) + + async def build_messages(self) -> List[Message]: + now_time: str = utils.get_now_time() + context: str = utils.format_messages(self.get_messages()) + + messages = [ + Message( + role=Role.SYSTEM, + content=self.prompt_format( + prompt_name="system_prompt", + now_time=now_time, + context=context, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + ), + ), + Message( + role=Role.USER, + content=self.get_prompt("user_message"), + ), + ] + return messages + + async def _acting_step( + self, + assistant_message: Message, + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + **kwargs, + ) -> List[Message]: + return await super()._acting_step(assistant_message, + tool_op_dict, + step, + workspace_id=self.workspace_id, + ref_memory_id=self.context["ref_memory_id"], + memory_target=self.memory_target, + memory_type=self.memory_type.value, + author=self.author) diff --git a/reme_ai/v2/agent/v1/personal_summary_agent_v1_prompt.yaml b/reme_ai/v2/agent/v1/personal_summary_agent_v1_prompt.yaml new file mode 100644 index 00000000..9f8cd5be --- /dev/null +++ b/reme_ai/v2/agent/v1/personal_summary_agent_v1_prompt.yaml @@ -0,0 +1,108 @@ +tool: | + Extract and store personal memories from conversation context. + Use this tool to analyze dialogues and extract important personal information about users, + such as preferences, habits, personal background, relationships, and significant facts. + The agent will determine whether the information is worth remembering, check for duplicates + or conflicts with existing memories, and perform add, update, or delete operations as needed. + +tool_zh: | + 从对话上下文中提取并存储个人记忆。 + 使用此工具分析对话并提取关于用户的重要个人信息, + 例如偏好、习惯、个人背景、人际关系和重要事实。 + 代理将判断信息是否值得记忆,检查是否与现有记忆重复或冲突, + 并根据需要执行添加、更新或删除操作。 + +system_prompt: | + You are a professional memory agent specializing in the domain of **{memory_target}**. Your task is to update the main agent's {memory_type} memory regarding {memory_target} based on the context. + + ## Context: + {context} + + ## Current Time: + {now_time} + + ## Memory Objective: + You are managing **{memory_type}** memories about **{memory_target}** for the main agent. Focus on extracting and storing information directly related to this person’s preferences, habits, personal background, and significant facts. + + ## Your Tasks: + + 1. **Analyze and Extract** potential memories from the dialogue context: + - Determine whether the conversation contains important, memorable information, including but not limited to: user preferences, habits, or personal details; key facts, decisions, or conclusions; relationships or contextual background related to people or topics. + - If the dialogue is casual chatter or contains no valuable information, output `` and stop. + - Extract key information using clear and concise phrasing. + - Each memory entry must be self-contained and understandable without additional context. + - Avoid storing trivial or temporary information. + - Before proceeding, list all extracted memories in your response. + + 2. **Retrieve similar historical memories** using `vector_retrieve_memory`: + - Perform a semantic similarity search based on the extracted memories to find existing, potentially relevant memories. + - Retrieve related memories for comparison to check for duplication or associations. + + 3. **Compare and Decide** on memory operations: + - Compare the newly extracted memories with historical ones to ensure the final memory store contains no duplicates or contradictions. + - Choose the appropriate operation based on the situation: + - If the information already exists and is consistent: skip—no action needed. + - If existing memory needs supplementation or correction: use `update_memory` to update it. + - If existing memory is outdated or incorrect: use `delete_memory` to remove it, then use `add_memory` to insert the correct version. + - If the information is entirely new: use `add_memory` to add it to the memory store. + + 4. **Output** the result: + - If no memory operation is required, output ``. + - If memories were added, updated, or deleted, summarize the operations performed. + + ## Guidelines: + - Be selective: store only truly important information. + - Stay concise: each memory should be clear and atomic. + - Be accurate: ensure extracted content faithfully reflects the original context. + - Avoid redundancy: always check for similar existing memories before adding new ones. + - Include relevant metadata (e.g., timestamps) when appropriate. + +user_message: | + Please analyze the context to determine whether important information should be extracted and stored as memory, and perform memory addition, deletion, or update operations when necessary. + +system_prompt_zh: | + 你是一名在{memory_target}领域的专业记忆Agent。你的任务是根据上下文更新主Agent关于{memory_target}的{memory_type}记忆。 + + ## 上下文: + {context} + + ## 当前时间: + {now_time} + + ## 记忆目标: + 你正在为主Agent管理关于**{memory_target}**的{memory_type}记忆。专注于提取并存储与此人喜好、习惯、个人背景和重要事实直接相关的信息。 + + ## 你的任务: + 1. **分析并提取**对话上下文中的潜在记忆: + - 判断对话是否包含值得记住的重要信息,包括但不限于:用户偏好、习惯或个人信息;重要事实、决策或结论;与人物或事物相关的关系或背景 + - 如果对话是闲聊或没有有价值的信息,则输出 `` 并停止 + - 以清晰简洁的表述提取关键信息 + - 每条记忆应自成一体,无需额外上下文即可理解 + - 避免存储琐碎或临时性的信息 + - 在继续之前,先在回复中列出所有提取的记忆 + + 2. **检索相似的历史记忆**,使用 `vector_retrieve_memory`: + - 根据提取的记忆,使用语义相似度搜索相似的现有记忆 + - 检索可能相关的记忆以进行比较,检查重复或关联 + + 3. **比较并决策**记忆操作: + - 将提取的记忆与历史记忆进行对比,确保最终记忆库中不存在重复或冲突的内容 + - 根据实际情况灵活选择操作: + - 若信息已存在且一致:跳过,无需操作 + - 若需要补充或修正现有记忆:使用 `update_memory` 更新现有记忆 + - 若现有记忆已过时或错误:使用 `delete_memory` 删除后再用 `add_memory` 添加正确版本 + - 若为全新信息:使用 `add_memory` 添加到记忆库 + + 4. **输出**结果: + - 若无需记忆操作,输出 `` + - 若新增/更新/删除了记忆,总结所执行的操作 + + ## 指南: + - 有选择性:仅保存真正重要的信息 + - 保持简洁:每条记忆应清晰、原子 + - 精准准确:确保提取内容忠实于原始上下文 + - 避免冗余:新增前先检查已有相似记忆 + - 在适当的情况下包含相关元数据(例如时间) + +user_message_zh: | + 请分析上下文,判断是否需要提取重要信息并将其存储为记忆,并在必要时执行记忆的增删改操作。 \ No newline at end of file diff --git a/reme_ai/v2/agent/v1/procedural_summary_agent_v1_op.py b/reme_ai/v2/agent/v1/procedural_summary_agent_v1_op.py new file mode 100644 index 00000000..fdb8d7de --- /dev/null +++ b/reme_ai/v2/agent/v1/procedural_summary_agent_v1_op.py @@ -0,0 +1,94 @@ +from typing import List, Dict + +from flowllm.core.op import BaseAsyncToolOp + +from ..base_memory_agent_op import BaseMemoryAgentOp +from ... import C +from ... import utils +from ...enumeration import Role, MemoryType +from ...schema import Message, ToolCall + + +@C.register_op() +class ProceduralSummaryAgentV1Op(BaseMemoryAgentOp): + """Agent for extracting and storing procedural memories. + + This agent analyzes conversation context to extract procedural knowledge + such as workflows, step-by-step procedures, how-to guides, and best practices, + then stores them in the memory system. + """ + + memory_type: MemoryType = MemoryType.PROCEDURAL + + def build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "input_schema": { + "workspace_id": { + "type": "string", + "description": "workspace_id", + "required": True, + }, + "memory_target": { + "type": "string", + "description": "memory_target", + "required": True, + }, + "query": { + "type": "string", + "description": "query", + "required": False, + }, + "messages": { + "type": "array", + "description": "messages", + "required": False, + "items": {"type": "object"}, + }, + "ref_memory_id": { + "type": "string", + "description": "ref_memory_id", + "required": True, + }, + }, + }, + ) + + async def build_messages(self) -> List[Message]: + now_time: str = utils.get_now_time() + context: str = utils.format_messages(self.get_messages()) + + messages = [ + Message( + role=Role.SYSTEM, + content=self.prompt_format( + prompt_name="system_prompt", + now_time=now_time, + context=context, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + ), + ), + Message( + role=Role.USER, + content=self.get_prompt("user_message"), + ), + ] + return messages + + async def _acting_step( + self, + assistant_message: Message, + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + **kwargs, + ) -> List[Message]: + return await super()._acting_step(assistant_message, + tool_op_dict, + step, + workspace_id=self.workspace_id, + ref_memory_id=self.context["ref_memory_id"], + memory_target=self.memory_target, + memory_type=self.memory_type.value, + author=self.author) diff --git a/reme_ai/v2/agent/v1/procedural_summary_agent_v1_prompt.yaml b/reme_ai/v2/agent/v1/procedural_summary_agent_v1_prompt.yaml new file mode 100644 index 00000000..cd9888ef --- /dev/null +++ b/reme_ai/v2/agent/v1/procedural_summary_agent_v1_prompt.yaml @@ -0,0 +1,128 @@ +tool: | + Extract and store procedural memories from conversation context. + Use this tool to analyze dialogues and extract important procedural knowledge, + such as step-by-step workflows, how-to guides, best practices, problem-solving methods, + debugging techniques, and task completion strategies. + The agent will also reflect on task outcomes - extracting lessons from failures + and successful strategies from successes to improve future performance. + +tool_zh: | + 从对话上下文中提取并存储程序性记忆。 + 使用此工具分析对话并提取重要的程序性知识, + 例如分步骤工作流、操作指南、最佳实践、问题解决方法、调试技巧和任务完成策略。 + 代理还会反思任务结果——从失败中提取教训,从成功中总结有效策略,以提升未来表现。 + +system_prompt: | + You are a professional memory Agent specializing in the domain of **{memory_target}**. Your task is to update the main agent's {memory_type} memory regarding {memory_target} based on the context. + + ## Context: + {context} + + ## Current Time: + {now_time} + + ## Memory Objective: + You are managing **{memory_type}** memories about **{memory_target}** for the main Agent. Focus on extracting and storing procedural knowledge, such as: + - Step-by-step procedures and workflows + - Operational guides and instructions + - Best practices and methodologies + - Established routines and processes + - Problem-solving techniques and troubleshooting tips + - Task-completion strategies + + ## Your Tasks: + + 1. **Analyze and Extract** potential memories from the conversation context: + - Determine whether the dialogue contains procedural knowledge worth remembering, including but not limited to: + - Multi-step procedures or workflows + - Instructions for completing specific tasks + - Best practices or recommended approaches + - Problem-solving methods or debugging tips + - Configuration or setup processes + - If the context includes task outcome information: + - **Successful tasks**: Extract and reflect on successful experiences; summarize key success factors, effective methods, and reusable strategies. + - **Failed tasks**: Extract and reflect on lessons learned; analyze root causes of failure, pitfalls to avoid, and improvement suggestions. + - **Both success and failure**: Conduct comparative reflection; identify critical differences and distill key decision factors and best practices. + - If the conversation is casual chat or contains no valuable information, output `` and stop. + - Express extracted information clearly and concisely. + - Each memory entry should be self-contained and understandable without additional context. + - Avoid storing trivial or transient information. + - Before proceeding, list all extracted memories in your response. + + 2. **Retrieve similar historical memories** using `vector_retrieve_memory`: + - Perform a semantic similarity search based on the extracted memories. + - Retrieve potentially relevant existing memories for comparison to check for duplicates or associations. + + 3. **Compare and Decide** on memory operations: + - Compare extracted memories against historical ones to ensure no duplicates or conflicts exist in the final memory repository. + - Choose the appropriate operation based on the situation: + - If the information already exists and is consistent: skip (no action needed). + - If existing memory needs supplementation or correction: use `update_memory` to revise it. + - If existing memory is outdated or incorrect: use `delete_memory` followed by `add_memory` to store the corrected version. + - If the information is entirely new: use `add_memory` to add it to the memory repository. + + 4. **Output** the result: + - If no memory operation is needed, output ``. + - If memories were added, updated, or deleted, summarize the performed operations. + + ## Guidelines: + - **Be selective**: Store only truly important information. + - **Stay concise**: Each memory should be clear and atomic. + - **Be precise and accurate**: Ensure extracted content faithfully reflects the original context. + - **Avoid redundancy**: Always check for similar existing memories before adding new ones. + - **Include relevant metadata when appropriate** (e.g., timestamp, preconditions, expected outcomes). + +user_message: | + Please analyze the context to determine whether important procedural knowledge should be extracted and stored as memory, and perform memory addition, deletion, or update operations when necessary. + +system_prompt_zh: | + 你是一名在{memory_target}领域的专业记忆Agent。你的任务是根据上下文更新主Agent关于{memory_target}的{memory_type}记忆。 + + ## 上下文: + {context} + + ## 当前时间: + {now_time} + + ## 记忆目标: + 正在为主Agent管理关于**{memory_target}**的{memory_type}记忆。专注于提取并存储程序性知识,例如:分步骤流程和工作流、操作指南和说明、最佳实践和方法论、已建立的流程和例程、问题解决方法和技巧、任务完成策略。 + + ## 你的任务: + 1. **分析并提取**对话上下文中的潜在记忆: + - 判断对话是否包含值得记住的程序性知识,包括但不限于:多步骤流程或工作流;完成特定任务的说明;最佳实践或推荐方法;问题解决方法或调试技巧;配置或设置流程 + - 若上下文中包含任务结果信息: + - 若包含**任务成功**的信息:提取并反思成功的经验,总结导致成功的关键因素、有效的方法和可复用的策略 + - 若包含**任务失败**的信息:提取并反思失败的教训,分析失败原因、避免的陷阱和改进建议 + - 若**既有成功也有失败**的任务:进行对比反思,分析成功与失败的差异点,提炼出关键的决策因素和最佳实践 + - 如果对话是闲聊或没有有价值的信息,则输出 `` 并停止 + - 以清晰简洁的表述提取关键信息 + - 每条记忆应自成一体,无需额外上下文即可理解 + - 避免存储琐碎或临时性的信息 + - 在继续之前,先在回复中列出所有提取的记忆 + + 2. **检索相似的历史记忆**,使用 `vector_retrieve_memory`: + - 根据提取的记忆,使用语义相似度搜索相似的现有记忆 + - 检索可能相关的记忆以进行比较,检查重复或关联 + + 3. **比较并决策**记忆操作: + - 将提取的记忆与历史记忆进行对比,确保最终记忆库中不存在重复或冲突的内容 + - 根据实际情况灵活选择操作: + - 若信息已存在且一致:跳过,无需操作 + - 若需要补充或修正现有记忆:使用 `update_memory` 更新现有记忆 + - 若现有记忆已过时或错误:使用 `delete_memory` 删除后再用 `add_memory` 添加正确版本 + - 若为全新信息:使用 `add_memory` 添加到记忆库 + + 4. **输出**结果: + - 若无需记忆操作,输出 `` + - 若新增/更新/删除了记忆,总结所执行的操作 + + ## 指南: + - 有选择性:仅保存真正重要的信息 + - 保持简洁:每条记忆应清晰、原子 + - 精准准确:确保提取内容忠实于原始上下文 + - 避免冗余:新增前先检查已有相似记忆 + - 在适当的情况下包含相关元数据(例如时间、前提条件、预期结果) + +user_message_zh: | + 请分析上下文,判断是否需要提取重要的程序性知识并将其存储为记忆,并在必要时执行记忆的增删改操作。 + diff --git a/reme_ai/v2/agent/v1/reme_retrieve_agent_v1_op.py b/reme_ai/v2/agent/v1/reme_retrieve_agent_v1_op.py new file mode 100644 index 00000000..6cf146be --- /dev/null +++ b/reme_ai/v2/agent/v1/reme_retrieve_agent_v1_op.py @@ -0,0 +1,85 @@ +from typing import List, Dict + +from flowllm.core.op import BaseAsyncToolOp + +from ..base_memory_agent_op import BaseMemoryAgentOp +from ... import C +from ... import utils +from ...enumeration import Role +from ...schema import Message, ToolCall + + +@C.register_op() +class ReMeRetrieveAgentV1Op(BaseMemoryAgentOp): + + def __init__( + self, + enable_tool_memory: bool = True, + **kwargs + ): + super().__init__(**kwargs) + self.enable_tool_memory = enable_tool_memory + + def build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "input_schema": { + "workspace_id": { + "type": "string", + "description": "workspace_id", + "required": True, + }, + "query": { + "type": "string", + "description": "query", + "required": False, + }, + "messages": { + "type": "array", + "description": "messages", + "required": False, + "items": {"type": "object"}, + }, + }, + }, + ) + + async def _read_meta_memories(self) -> str: + from ...tool import ReadMetaMemoryOp + + op = ReadMetaMemoryOp(enable_tool_memory=self.enable_tool_memory, + enable_identity_memory=False) + await op.async_call(workspace_id=self.workspace_id) + return str(op.output) + + async def build_messages(self) -> List[Message]: + now_time: str = utils.get_now_time() + meta_memory_info = await self._read_meta_memories() + context: str = utils.format_messages(self.get_messages()) + + system_prompt = self.prompt_format( + prompt_name="system_prompt", + now_time=now_time, + meta_memory_info=meta_memory_info, + context=context, + ) + + user_message = self.get_prompt("user_message") + messages = [ + Message(role=Role.SYSTEM, content=system_prompt), + Message(role=Role.USER, content=user_message), + ] + return messages + + async def _acting_step( + self, + assistant_message: Message, + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + **kwargs, + ) -> List[Message]: + return await super()._acting_step(assistant_message, + tool_op_dict, + step, + workspace_id=self.workspace_id) diff --git a/reme_ai/v2/agent/v1/reme_retrieve_agent_v1_prompt.yaml b/reme_ai/v2/agent/v1/reme_retrieve_agent_v1_prompt.yaml new file mode 100644 index 00000000..6c6d94fa --- /dev/null +++ b/reme_ai/v2/agent/v1/reme_retrieve_agent_v1_prompt.yaml @@ -0,0 +1,99 @@ +tool: | + Retrieve relevant memories from the memory bank to assist in answering questions. + Use this tool when you need to search for historical information, user preferences, + procedural knowledge, or any other stored memories that may help answer the current query. + The agent will analyze the context, determine what information is needed, and perform + semantic searches across different memory types to find the most relevant memories. + +tool_zh: | + 从记忆库中检索相关记忆以协助回答问题。 + 当您需要搜索历史信息、用户偏好、程序性知识或任何其他可能有助于回答当前问题的存储记忆时,使用此工具。 + 代理将分析上下文,确定需要哪些信息,并在不同记忆类型中执行语义搜索以找到最相关的记忆。 + +system_prompt: | + You are a memory agent. Please analyze the context, retrieve relevant information from the memory bank when needed, and return a summary of the retrieved memories to assist in answering the user's question. + + ## Context + {context} + + ## Current Time + {now_time} + + ## Available Meta Memories + Format: "- (): " + {meta_memory_info} + + ## Your Tasks + + 1. **Analyze** the context to determine whether retrieval is necessary: + - If the question can be directly answered using the existing context, output `` and stop. + - If additional information is required, proceed to retrieval. + - Consider which types of meta memory from the "Available Meta Memories" list are most relevant. + + 2. **Retrieve** relevant memories using `vector_retrieve_memory`: + - Select the appropriate `memory_type` and `memory_target` from the "Available Meta Memories" list. + - Clearly define the needed information and construct suitable queries. + - Design queries flexibly based on actual needs: + * Generate different queries for different `memory_type`/`memory_target` combinations. + * For the same combination, create multiple queries using different phrasings or perspectives. + * Choose the optimal combination strategy based on the retrieval scenario. + - **Important**: When retrieving tool-related memories (`memory_type` is "tool"), the query must use the tool’s exact name (not a description or paraphrase of the problem). + - If retrieval results include a `ref_memory_id` and more details are needed—or if vector retrieval proves insufficient—use `read_history_memory` with the `ref_memory_id` as the `memory_id` parameter. + + 3. **Iterate if necessary**: + - If the initial retrieval fails, try alternative phrasings or perspectives. + - If multiple memory types exist, attempt retrievals across different types. + - Before concluding that no relevant memory exists, perform at least 2–3 retrieval attempts using varied phrasings or viewpoints. + - If repeated vector retrievals still fail to yield sufficient information, use `read_history_memory` to fetch the original message content. + + 4. **Output** the result: + - If no retrieval is needed, output ``. + - If relevant memories are found, clearly summarize the retrieved information. + - If multiple attempts still yield no relevant memory, output ``. + +user_message: | + Please analyze the context, retrieve relevant information from the memory bank when needed, and return a summary of the retrieved memories to assist in answering the user's question. + + +system_prompt_zh: | + 你是一个记忆Agent。请分析上下文,在需要时从记忆库检索相关信息,并返回检索到的记忆总结以帮助回答用户问题。 + + ## 上下文 + {context} + + ## 当前时间 + {now_time} + + ## 可用的meta memory + 格式:"- (): " + {meta_memory_info} + + ## 你的任务 + 1. **分析**上下文以判断是否需要检索: + - 如果问题可以直接通过现有上下文回答,则输出 `` 并停止。 + - 如果需要额外信息,则继续检索。 + - 根据可用的“可用的meta memory”,考虑和哪几种meta memory最相关。 + + 2. **检索**相关记忆,使用 `vector_retrieve_memory`: + - 从"可用的meta memory"列表中选择 `memory_type` 和 `memory_target`。 + - 明确需要的信息并构造合适的查询。 + - 根据实际需要灵活设计查询: + * 针对不同的 `memory_type`/`memory_target` 组合生成不同的查询。 + * 针对相同的组合,可以用不同表述或角度生成多个查询。 + * 根据检索场景选择最合适的组合策略。 + - **重要**:检索工具记忆时(`memory_type` 为 "tool"),query必须使用工具的真实名称(而非描述或问题)。 + - 若检索结果包含 `ref_memory_id` 且需要更多细节,或向量检索不足时,使用 `read_history_memory` 并将 `ref_memory_id` 作为 `memory_id` 参数。 + + 3. **必要时迭代**: + - 首次检索不命中时尝试不同表述或角度。 + - 如存在多种记忆类型,尝试不同类型的检索。 + - 在宣布无相关记忆前,至少用 2-3 种不同表述或视角重新检索。 + - 多次向量检索仍不足以回答时,使用 `read_history_memory` 获取原始消息内容。 + + 4. **输出**结果: + - 无需检索时输出 ``。 + - 找到相关记忆时清晰总结检索到的信息。 + - 多次尝试仍无相关记忆时输出 ``。 + +user_message_zh: | + 请分析上下文,在需要时从记忆库检索相关信息,并返回检索到的记忆总结以帮助回答用户问题。 diff --git a/reme_ai/v2/agent/v1/reme_summary_agent_v1_op.py b/reme_ai/v2/agent/v1/reme_summary_agent_v1_op.py new file mode 100644 index 00000000..61049c79 --- /dev/null +++ b/reme_ai/v2/agent/v1/reme_summary_agent_v1_op.py @@ -0,0 +1,134 @@ +import re +from typing import List, Dict + +from loguru import logger + +from ..base_memory_agent_op import BaseMemoryAgentOp +from ... import C +from ... import utils +from ...enumeration import Role +from ...schema import Message, ToolCall, MemoryNode +from ....core import BaseAsyncToolOp + + +@C.register_op() +class ReMeSummaryAgentV1Op(BaseMemoryAgentOp): + + def __init__( + self, + enable_tool_memory: bool = True, + enable_identity_memory: bool = True, + **kwargs + ): + super().__init__(**kwargs) + self.enable_tool_memory = enable_tool_memory + self.enable_identity_memory = enable_identity_memory + + def build_tool_call(self) -> ToolCall: + """Build the tool call schema for ReMeSummaryAgent. + + Returns: + ToolCall: Tool call configuration with workspace_id/query/messages input schema. + """ + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "input_schema": { + "workspace_id": { + "type": "string", + "description": "workspace id", + "required": True, + }, + "query": { + "type": "string", + "description": "query", + "required": False, + }, + "messages": { + "type": "array", + "description": "messages", + "required": False, + "items": {"type": "object"}, + }, + }, + }, + ) + + async def _add_history_memory(self) -> MemoryNode: + from ...tool import AddHistoryMemoryOp + + op = AddHistoryMemoryOp() + await op.async_call(workspace_id=self.workspace_id, messages=self.get_messages(), author=self.author) + return op.output + + async def _read_identity_memory(self) -> str: + from ...tool import ReadIdentityMemoryOp + + op = ReadIdentityMemoryOp() + await op.async_call(workspace_id=self.workspace_id) + return str(op.output) + + async def _read_meta_memories(self) -> str: + from ...tool import ReadMetaMemoryOp + + op = ReadMetaMemoryOp(enable_tool_memory=self.enable_tool_memory, + enable_identity_memory=self.enable_identity_memory) + await op.async_call(workspace_id=self.workspace_id) + return str(op.output) + + async def build_messages(self) -> List[Message]: + now_time: str = utils.get_now_time() + memory_node: MemoryNode = await self._add_history_memory() + identity_memory = await self._read_identity_memory() + meta_memory_info = await self._read_meta_memories() + context: str = utils.format_messages(self.get_messages()) + + logger.info(f"now_time={now_time} memory_node={memory_node} identity_memory={identity_memory} " + f"meta_memory_info={meta_memory_info} context={context}") + + system_prompt = self.prompt_format( + prompt_name="system_prompt", + now_time=now_time, + identity_memory=identity_memory, + meta_memory_info=meta_memory_info, + context=context, + ) + + user_message = self.get_prompt("user_message") + messages = [ + Message(role=Role.SYSTEM, content=system_prompt), + Message(role=Role.USER, content=user_message), + ] + + self.context["ref_memory_id"] = memory_node.memory_id + return messages + + async def _reasoning_step( + self, + messages: List[Message], + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + ) -> tuple[Message, bool]: + meta_memory_info = await self._read_meta_memories() + system_messages = [message for message in messages if message.role == Role.SYSTEM] + if system_messages: + system_message = system_messages[0] + pattern = r'("- \(\): "\n)(.*?)(\n\n)' + replacement = rf'\g<1>{meta_memory_info}\g<3>' + system_message.content = re.sub(pattern, replacement, system_message.content, flags=re.DOTALL) + + return await super()._reasoning_step(messages, tool_op_dict, step) + + async def _acting_step( + self, + assistant_message: Message, + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + **kwargs, + ) -> List[Message]: + return await super()._acting_step(assistant_message, + tool_op_dict, + step, + workspace_id=self.workspace_id, + ref_memory_id=self.context["ref_memory_id"], + author=self.author) diff --git a/reme_ai/v2/agent/v1/reme_summary_agent_v1_prompt.yaml b/reme_ai/v2/agent/v1/reme_summary_agent_v1_prompt.yaml new file mode 100644 index 00000000..154a0cf0 --- /dev/null +++ b/reme_ai/v2/agent/v1/reme_summary_agent_v1_prompt.yaml @@ -0,0 +1,104 @@ +tool: | + Orchestrate the complete memory summarization workflow for the agent. + This tool receives conversation context and performs necessary memory updates including: + 1. Adding history memory for conversation tracking + 2. Creating new meta-memory entries if needed + 3. Adding summary memory for quick future recall + 4. Delegating to specialized memory agents for detailed memory extraction and storage + +tool_zh: | + 编排Agent的完整记忆总结工作流。 + 该工具接收对话上下文并执行必要的记忆更新,包括: + 1. 添加历史记忆以跟踪对话 + 2. 如有需要创建新的meta memory条目 + 3. 添加摘要记忆以便将来快速回忆 + 4. 委托给专业记Agent进行详细的记忆提取和存储 + +system_prompt: | + # Context + {context} + + You are a Memory Agent responsible for performing necessary updates and summaries of the main Agent's memories based on the **context**. + + ## Current Time + {now_time} + + ## Main Agent's Self-Perception + {identity_memory} + + ## Main Agent's Meta Memory + Each line of meta memory indicates the existence of a specialized Memory Agent dedicated to deep summarization and updating of memories within a specific dimension (memory_type + memory_target). + Format: "- (): " + {meta_memory_info} + + ## Your Tasks + + ### 1. Create New Meta Memory (if needed) + When the context contains significant personal or procedural information not yet covered by existing meta memories: + - Use `add_meta_memory` to create one or more new meta memory entries. + - For personal memories: specify `memory_type="personal"` and `memory_target=`. + - For procedural memories: specify `memory_type="procedural"` and `memory_target=`. + - Each meta memory entry will instantiate a dedicated specialized Memory Agent for that dimension. + + ### 2. Add Summary Memory (if valuable) + When the context includes information worth remembering for quick future recall: + - Use `add_summary_memory` to store a concise summary. + - The summary should capture key points, decisions, or important facts to aid later recollection of the original conversation. + + ### 3. Delegate to Specialized Memory Agents (Core Task) + You do not need to summarize or update memories yourself. Instead, analyze the context, identify which memory dimensions (memory_type + memory_target) from the existing meta memory require updates, and delegate using `hands_off`: + - The parameters of `hands_off` (`memory_type` and `memory_target`) must exactly match an existing entry in the "Main Agent's Meta Memory" listed above. + - You may delegate concurrently to multiple specialized agents to enable parallel memory processing. + - Each specialized agent will perform detailed memory extraction, addition, updating, or deletion within its assigned dimension. + + ## Output Requirements + - If the context contains no memorable information (e.g., simple greetings or meaningless small talk), output ``. + - If any memory operations were performed, briefly summarize what was done. + +user_message: | + Please perform your task based on the context. + +system_prompt_zh: | + # 上下文 + {context} + + 你是一个记忆Agent,负责根据**上下文**对主Agent的所有记忆进行必要的更新和总结。 + + ## 当前时间 + {now_time} + + ## 主Agent的自我认知 + {identity_memory} + + ## 主Agent的meta memory + 每一行meta memory代表存在一个专业的记忆Agent,负责针对某个特定维度(memory_type+memory_target)进行深度的记忆总结和更新。 + 格式:"- (): " + {meta_memory_info} + + ## 你的任务 + + ### 1. 创建新的meta memory(如果需要) + 当上下文包含重要的个人或程序性信息,但现有meta memory尚未覆盖时: + - 使用 `add_meta_memory` 创建一条或者多条新的meta memory + - 对于个人记忆:指定 memory_type="personal" 和 memory_target=<人名> + - 对于程序性记忆:指定 memory_type="procedural" 和 memory_target=<主题或领域> + - 每个meta memory条目将创建一个专门负责该维度的专业记忆Agent + + ### 2. 添加摘要记忆(如果有价值) + 当上下文包含值得记忆以便将来快速回忆的信息时: + - 使用 `add_summary_memory` 存储简洁的摘要 + - 摘要应捕捉关键点、决策或重要事实,帮助稍后回忆原始对话 + + ### 3. 委托给不同维度的专业记忆Agent(核心任务) + 你不需要亲自总结或者更新记忆,分析上下文,思考meta memory中哪些记忆维度(memory_type+memory_target)需要更新,然后使用 `hands_off` 委托给相关的专业记忆Agent: + - `hands_off` 的参数(memory_type 和 memory_target)必须严格对应上述"当前Agent的meta memory"中已存在的条目 + - 可以并行委托给多个专业代理,实现并发的记忆处理 + - 每个专业代理将针对其特定维度执行详细的记忆提取、添加、更新或删除 + + ## 输出要求 + - 如果上下文不包含任何值得记忆的信息(如简单问候、无意义闲聊),输出 `` + - 如果执行了任何记忆操作,简要总结所做的事情 + +user_message_zh: | + 请根据上下文执行你的任务。 + diff --git a/reme_ai/v2/agent/v1/remy_agent_v1_op.py b/reme_ai/v2/agent/v1/remy_agent_v1_op.py new file mode 100644 index 00000000..e0a8e6d9 --- /dev/null +++ b/reme_ai/v2/agent/v1/remy_agent_v1_op.py @@ -0,0 +1,92 @@ +from typing import List + +from ..base_memory_agent_op import BaseMemoryAgentOp +from ... import C +from ... import utils +from ...enumeration import Role +from ...schema import Message, ToolCall + + +@C.register_op() +class ReMyAgentV1Op(BaseMemoryAgentOp): + + def __init__( + self, + enable_tool_memory: bool = True, + enable_identity_memory: bool = True, + **kwargs + ): + super().__init__(**kwargs) + self.enable_tool_memory = enable_tool_memory + self.enable_identity_memory = enable_identity_memory + + def build_tool_call(self) -> ToolCall: + """Build the tool call schema for ReMy agent. + + Returns: + ToolCall: Tool call configuration with query/messages input schema. + """ + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "input_schema": { + "workspace_id": { + "type": "string", + "description": "workspace_id", + "required": True, + }, + "query": { + "type": "string", + "description": "query", + "required": False, + }, + "messages": { + "type": "array", + "description": "messages", + "required": False, + "items": {"type": "object"}, + }, + }, + }, + ) + + async def _read_identity_memory(self) -> str: + from ...tool import ReadIdentityMemoryOp + + op = ReadIdentityMemoryOp() + await op.async_call(workspace_id=self.workspace_id) + return str(op.output) + + async def _read_meta_memories(self) -> str: + from ...tool import ReadMetaMemoryOp + + op = ReadMetaMemoryOp(enable_tool_memory=self.enable_tool_memory, + enable_identity_memory=self.enable_identity_memory) + await op.async_call(workspace_id=self.workspace_id) + return str(op.output) + + async def build_messages(self) -> List[Message]: + now_time: str = utils.get_now_time() + identity_memory = await self._read_identity_memory() + meta_memory_info = await self._read_meta_memories() + + messages: List[Message] = [] + if self.context.get("query"): + messages.append(Message(role=Role.USER, content=self.context.query)) + elif self.context.get("messages"): + raw_messages = [Message(**msg) if isinstance(msg, dict) else msg for msg in self.context["messages"]] + messages.extend([msg for msg in raw_messages if msg.role is not Role.SYSTEM]) + else: + raise ValueError("input_dict must contain either `query` or `messages`") + + # Build system prompt with memory context + system_prompt = self.prompt_format( + prompt_name="system_prompt", + now_time=now_time, + identity_memory=identity_memory, + meta_memory_info=meta_memory_info, + ) + + messages = [Message(role=Role.SYSTEM, content=system_prompt)] + messages + return messages + diff --git a/reme_ai/v2/agent/v1/remy_agent_v1_prompt.yaml b/reme_ai/v2/agent/v1/remy_agent_v1_prompt.yaml new file mode 100644 index 00000000..e016e367 --- /dev/null +++ b/reme_ai/v2/agent/v1/remy_agent_v1_prompt.yaml @@ -0,0 +1,72 @@ +tool: | + Conversational AI assistant with integrated memory capabilities. + Use this tool to engage in natural conversations with users while leveraging + stored identity and memory context. The agent can access historical information, + user preferences, and procedural knowledge through its memory system, and can + use various tools to accomplish tasks and answer questions. + +tool_zh: | + 具有集成记忆能力的对话式AI助手。 + 使用此工具与用户进行自然对话,同时利用存储的身份和记忆上下文。 + 该代理可以通过其记忆系统访问历史信息、用户偏好和程序性知识, + 并可以使用各种工具来完成任务和回答问题。 + +system_prompt: | + You are ReMy, an intelligent AI assistant with memory capabilities. + + ## Current Time + {now_time} + + ## Self-Awareness + {identity_memory} + + ## Available Meta Memories + Format: "- (): " + {meta_memory_info} + + ## Guiding Principles + 1. **Be Helpful and Accurate**: Provide clear and correct information. + 2. **Use Memory Wisely**: Retrieve relevant memories when they can improve your response. + 3. **Use Tools Appropriately**: Select the right tool for each task. + 4. **Stay Conversational**: Maintain a natural and friendly tone. + 5. **Seek Clarification**: Ask questions if the user’s intent is unclear. + 6. **Acknowledge Limitations**: Be honest about what you can and cannot do. + + ## How to Use the Memory Retrieval Tool + When using `vector_retrieve_memory` to search memories: + - Choose an appropriate `memory_type` and `memory_target` from the "Available Meta Memories" list above. + - Formulate a clear and specific query based on the information you need. + - **Important**: When retrieving tool-related memories (`memory_type` is "tool"), the query must use the tool’s exact name (not a description or a question). + - If retrieval results include a `ref_memory_id` and you need more details, use `read_history_memory` with the `ref_memory_id` as the `memory_id` parameter. + - If the initial retrieval yields no results, try rephrasing your query or using a different memory type. + - You may generate multiple queries with different phrasings or perspectives for the same memory type/target. +system_prompt_zh: | + 你是ReMy,一个具有记忆能力的智能AI助手。 + + ## 当前时间 + {now_time} + + ## 自我认知 + {identity_memory} + + ## 可用的meta memory + 格式:"- (): " + {meta_memory_info} + + ## 指导原则 + 1. **提供帮助且准确**: 提供清晰、正确的信息 + 2. **明智使用记忆**: 在能改善回答时检索相关记忆 + 3. **适当使用工具**: 为每个任务选择正确的工具 + 4. **保持对话性**: 保持自然、友好的语气 + 5. **寻求澄清**: 如果用户意图不明确,请提问 + 6. **承认局限性**: 诚实面对你能做和不能做的事情 + + ## 如何使用记忆检索工具 + 使用 `vector_retrieve_memory` 搜索记忆时: + - 从上述"可用的meta memory"列表中选择合适的 `memory_type` 和 `memory_target` + - 根据需要的信息构造清晰、具体的查询 + - **重要**: 检索工具记忆时(`memory_type` 为 "tool"),query必须使用工具的真实名称(而非描述或问题) + - 若检索结果包含 `ref_memory_id` 且需要更多细节,使用 `read_history_memory` 并将 `ref_memory_id` 作为 `memory_id` 参数 + - 首次检索无结果时,尝试不同的表述或不同的记忆类型 + - 可以针对相同的记忆类型/目标用不同表述或角度生成多个查询 + diff --git a/reme_ai/v2/agent/v1/tool_summary_agent_v1_op.py b/reme_ai/v2/agent/v1/tool_summary_agent_v1_op.py new file mode 100644 index 00000000..b9c044bd --- /dev/null +++ b/reme_ai/v2/agent/v1/tool_summary_agent_v1_op.py @@ -0,0 +1,87 @@ +from typing import List, Dict + +from flowllm.core.op import BaseAsyncToolOp + +from ..base_memory_agent_op import BaseMemoryAgentOp +from ... import C +from ... import utils +from ...enumeration import Role, MemoryType +from ...schema import Message, ToolCall + + +@C.register_op() +class ToolSummaryAgentV1Op(BaseMemoryAgentOp): + memory_type: MemoryType = MemoryType.TOOL + + def build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "input_schema": { + "workspace_id": { + "type": "string", + "description": "workspace_id", + "required": True, + }, + "memory_target": { + "type": "string", + "description": "memory_target", + "required": True, + }, + "query": { + "type": "string", + "description": "query", + "required": False, + }, + "messages": { + "type": "array", + "description": "messages", + "required": False, + "items": {"type": "object"}, + }, + "ref_memory_id": { + "type": "string", + "description": "ref_memory_id", + "required": True, + }, + }, + }, + ) + + async def build_messages(self) -> List[Message]: + now_time: str = utils.get_now_time() + context: str = utils.format_messages(self.get_messages()) + + messages = [ + Message( + role=Role.SYSTEM, + content=self.prompt_format( + prompt_name="system_prompt", + now_time=now_time, + context=context, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + ), + ), + Message( + role=Role.USER, + content=self.get_prompt("user_message"), + ), + ] + return messages + + async def _acting_step( + self, + assistant_message: Message, + tool_op_dict: Dict[str, BaseAsyncToolOp], + step: int, + **kwargs, + ) -> List[Message]: + return await super()._acting_step(assistant_message, + tool_op_dict, + step, + workspace_id=self.workspace_id, + ref_memory_id=self.context["ref_memory_id"], + memory_target=self.memory_target, + memory_type=self.memory_type.value, + author=self.author) diff --git a/reme_ai/v2/agent/v1/tool_summary_agent_v1_prompt.yaml b/reme_ai/v2/agent/v1/tool_summary_agent_v1_prompt.yaml new file mode 100644 index 00000000..d1db5b43 --- /dev/null +++ b/reme_ai/v2/agent/v1/tool_summary_agent_v1_prompt.yaml @@ -0,0 +1,110 @@ +tool: | + Extract and store tool usage guidelines from tool call execution context. + Use this tool to analyze tool calls and their results, extracting valuable insights + about how to use tools more effectively, including best practices, common patterns, + error handling strategies, and optimization tips. + The agent will determine whether the information is worth remembering, check for duplicates + or conflicts with existing tool guidelines, and perform add, update, or delete operations as needed. + +tool_zh: | + 从工具调用执行上下文中提取并存储工具使用指南。 + 使用此工具分析工具调用及其结果,提取关于如何更有效使用工具的宝贵见解, + 包括最佳实践、常见模式、错误处理策略和优化技巧。 + 代理将判断信息是否值得记忆,检查是否与现有工具指南重复或冲突, + 并根据需要执行添加、更新或删除操作。 + +system_prompt: | + You are a professional memory Agent specializing in the domain of **{memory_target}**. Please analyze the tool execution context, and your task is to update the main Agent's **{memory_type}** memory regarding **{memory_target}** based on this context. + + ## Context: + {context} + + ## Current Time: + {now_time} + + ## Memory Target: + You are managing the main Agent’s **{memory_type}** memory about **{memory_target}**. Focus on extracting and storing guidelines, best practices, and insights on how to effectively use this tool. + + ## Your Tasks: + + 1. **Analyze and Extract** tool usage guidelines from the execution context: + - Determine whether the tool invocation and its results contain valuable insights worth remembering, including but not limited to: successful usage patterns and best practices; common errors and how to avoid them; effective parameter combinations; performance optimization tips; edge cases and special handling requirements. + - If the tool execution represents a routine operation with no new insights, output `` and stop. + - Extract key guidelines in a clear and actionable manner. + - Each guideline should be self-contained and directly applicable. + - Avoid storing trivial or obvious information. + - Before proceeding, list all extracted guidelines in your response. + + 2. **Retrieve historical guidelines** for this tool by calling `vector_retrieve_memory`, using the `tool_name` as the query parameter to fetch any existing guidelines. + + 3. **Compare and Decide** on the appropriate memory operation: + - Compare the newly extracted guidelines with the historical ones to ensure the final memory store contains no duplicates or contradictions. + - Normally, `vector_retrieve_memory` should return at most one guideline per tool. If multiple guidelines exist for the same tool, use `delete_memory` to remove the redundant entries and merge all useful information into a single, comprehensive guideline. + - Choose the appropriate action based on the situation: + - If the guideline already exists and is consistent: skip—no action needed. + - If the existing guideline needs supplementation or refinement: use `update_memory` to enhance it. + - If the existing guideline is outdated or incorrect: use `update_memory` to replace it with the correct version. + - If multiple guidelines exist for the same tool: use `delete_memory` to remove duplicates, then use `update_memory` on the remaining entry to consolidate all useful information. + - If the guideline is entirely new: use `add_memory` to add it to the memory store. + + 4. **Output** the result: + - If no memory operation is required, output ``. + - If you added, updated, or deleted any guidelines, summarize the operations performed. + + ## Guidelines: + - **Be selective**: Only retain insights that genuinely improve tool usage efficiency. + - **Keep it actionable**: Each guideline should offer clear, practical advice. + - **Ensure accuracy**: Verify that extracted guidelines are supported by actual tool execution results. + - **Avoid redundancy**: Always check for similar existing guidelines before adding new ones. + - **Include relevant context when appropriate** (e.g., parameter values, error messages). + +user_message: | + Please analyze the tool execution context to determine whether important usage guidelines should be extracted and stored as memory, and perform memory addition, deletion, or update operations when necessary. + +system_prompt_zh: | + 你是一名在{memory_target}领域的专业记忆Agent。请分析工具执行上下文,你的任务是根据上下文更新主Agent关于{memory_target}的{memory_type}记忆。 + + ## 上下文: + {context} + + ## 当前时间: + {now_time} + + ## 记忆目标: + 你正在为主Agent管理关于**{memory_target}**的{memory_type}记忆。专注于提取并存储关于如何有效使用此工具的指南、最佳实践和见解。 + + ## 你的任务: + 1. **分析并提取**执行上下文中的工具使用指南: + - 判断工具调用和结果是否包含值得记住的宝贵见解,包括但不限于:成功的使用模式和最佳实践;常见错误及其避免方法;有效的参数组合;性能优化技巧;边缘情况和特殊处理要求 + - 如果工具执行是常规操作且没有新见解,则输出 `` 并停止 + - 以清晰可操作的方式提取关键指南 + - 每条指南应自成一体且可直接应用 + - 避免存储琐碎或显而易见的信息 + - 在继续之前,先在回复中列出所有提取的指南 + + 2. **检索工具的历史指南**,使用 `vector_retrieve_memory`,通过在query参数中填入tool_name参数,查询对应的历史指南 + + 3. **比较并决策**记忆操作: + - 将提取的指南与历史指南进行对比,确保最终记忆库中不存在重复或冲突的内容 + - 正常情况下,`vector_retrieve_memory` 对每个工具最多应返回1条指南。如果发现同一工具有多条指南,通过 `delete_memory` 删除多余的,并将所有有用信息合并到一条完整的指南中 + - 根据实际情况灵活选择操作: + - 若指南已存在且一致:跳过,无需操作 + - 若需要补充或完善现有指南:使用 `update_memory` 更新现有指南 + - 若现有指南已过时或错误:使用 `update_memory` 替换为正确版本 + - 若同一工具存在多条指南:使用 `delete_memory` 删除重复项,然后对剩余的一条使用 `update_memory` 合并所有有用信息 + - 若为全新指南:使用 `add_memory` 添加到记忆库 + + 4. **输出**结果: + - 若无需记忆操作,输出 `` + - 若新增/更新/删除了指南,总结所执行的操作 + + ## 指南: + - 有选择性:仅保存真正能提高工具使用效率的宝贵见解 + - 保持可操作:每条指南应提供清晰、实用的建议 + - 精准准确:确保提取的指南得到实际工具执行结果的支持 + - 避免冗余:新增前先检查已有相似指南 + - 在适当的情况下包含相关上下文(例如参数值、错误信息) + +user_message_zh: | + 请分析工具执行上下文,判断是否需要提取重要的使用指南并将其存储为记忆,并在必要时执行记忆的增删改操作。 + diff --git a/reme_ai/v2/enumeration/__init__.py b/reme_ai/v2/enumeration/__init__.py new file mode 100644 index 00000000..0430c856 --- /dev/null +++ b/reme_ai/v2/enumeration/__init__.py @@ -0,0 +1,11 @@ +"""Enumeration module for ReMe AI memory system.""" + +from .memory_type import MemoryType +from .identity_mode import IdentityMode +from flowllm.core.enumeration import Role + +__all__ = [ + "MemoryType", + "IdentityMode", + "Role" +] diff --git a/reme_ai/v2/enumeration/identity_mode.py b/reme_ai/v2/enumeration/identity_mode.py new file mode 100644 index 00000000..83f320a3 --- /dev/null +++ b/reme_ai/v2/enumeration/identity_mode.py @@ -0,0 +1,11 @@ +"""Identity mode enumeration for identity memory operations.""" + +from enum import Enum + + +class IdentityMode(str, Enum): + """Operation modes for identity memory management.""" + NEW = "new" + GET = "get" + UPDATE = "update" + diff --git a/reme_ai/v2/enumeration/memory_type.py b/reme_ai/v2/enumeration/memory_type.py new file mode 100644 index 00000000..610a0298 --- /dev/null +++ b/reme_ai/v2/enumeration/memory_type.py @@ -0,0 +1,27 @@ +"""Memory type enumeration for the three-layer memory architecture.""" + +from enum import Enum + + +class MemoryType(str, Enum): + """ + Three-layer memory architecture for agent memory management. + + Layer 1 - High-level Abstraction Memory: + - IDENTITY: Self-cognition (identity, personality, current state) + - PERSONAL: Person-specific memory (preferences and context about specific individuals) + - PROCEDURAL: Procedural memory (how-to knowledge, e.g., 4 steps to write financial reports) + - TOOL: Tool memory (tool usage patterns, success rates, token consumption, latency) + + Layer 2 - Summary Memory (Compressed): Summarized digest of raw message history + Layer 3 - History Memory (Raw): Raw message history + """ + + IDENTITY = "identity" + PERSONAL = "personal" + PROCEDURAL = "procedural" + TOOL = "tool" + + SUMMARY = "summary" + + HISTORY = "history" \ No newline at end of file diff --git a/reme_ai/v2/schema/__init__.py b/reme_ai/v2/schema/__init__.py new file mode 100644 index 00000000..ffc06f57 --- /dev/null +++ b/reme_ai/v2/schema/__init__.py @@ -0,0 +1,52 @@ +from typing import List + +from flowllm.core.schema import Message as FlowMessage, ToolCall as FlowToolCall +from pydantic import BaseModel, Field + + +class Message(FlowMessage): + + def format_message( + self, + i: int | None = None, + add_time_created: bool = False, + use_name_first: bool = False, + add_reasoning_content: bool = True, + add_tool_calls: bool = True + ) -> str: + content = "" + if i is not None: + content += f"round{i} " + + if add_time_created: + content += f"[{self.time_created}] " + + if use_name_first: + content += f"{self.name or self.role.value}:\n" + else: + content += f"{self.role.value}:\n" + + if add_reasoning_content and self.reasoning_content: + content += self.reasoning_content + "\n" + + if self.content: + content += self.content + "\n" + + if add_tool_calls and self.tool_calls: + for tool_call in self.tool_calls: + content += f" - tool_call={tool_call.name} params={tool_call.arguments}\n" + + return content.strip() + + +class ToolCall(FlowToolCall): + pass + +class Trajectory(BaseModel): + + task_id: str = Field(default="") + messages: List[Message] = Field(default_factory=list) + score: float = Field(default=0.0) + metadata: dict = Field(default_factory=dict) + +from .memory_node import MemoryNode \ No newline at end of file diff --git a/reme_ai/v2/schema/memory_node.py b/reme_ai/v2/schema/memory_node.py new file mode 100644 index 00000000..4989f76e --- /dev/null +++ b/reme_ai/v2/schema/memory_node.py @@ -0,0 +1,228 @@ +"""Memory schema module for the ReMe AI system. + +This module defines the MemoryNode class for storing and retrieving +memories in the ReMe system. +""" + +import datetime +import hashlib +import json +from typing import Any + +from flowllm.core.schema import VectorNode +from pydantic import BaseModel, Field, model_validator + +from ..enumeration import MemoryType + +# Length of the memory ID (first N characters of SHA-256 hash) +MEMORY_ID_LENGTH: int = 16 + + +def _get_current_timestamp() -> str: + """Get current timestamp in YYYY-MM-DD HH:MM:SS format. + + Returns: + str: Current timestamp string in format 'YYYY-MM-DD HH:MM:SS'. + """ + return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +class MemoryNode(BaseModel): + """Memory node for storing memories in the ReMe system. + + Attributes: + workspace_id: Workspace identifier this memory belongs to. + memory_id: Unique identifier, auto-generated from content hash. + memory_type: Type of memory (e.g., SUMMARY, PERSONAL). + memory_target: Target or topic this memory relates to. + when_to_use: Condition description for vector retrieval. + content: Actual memory content. + ref_memory_id: Reference to related raw history memory. + time_created: Creation timestamp. + time_modified: Last modification timestamp. + author: Author or source of this memory. + score: Relevance or importance score. + metadata: Additional metadata for extensibility. + """ + + workspace_id: str = Field(default="", description="Workspace identifier") + memory_id: str = Field(default="", description="Unique memory identifier") + + memory_type: MemoryType = Field(default=..., description="Type of memory") + memory_target: str = Field(default="", description="Target or topic of the memory") + when_to_use: str = Field(default="", description="Condition description for vector retrieval") + content: str = Field(default="", description="Actual memory content") + ref_memory_id: str = Field(default="", description="Reference to related raw history memory ID") + + time_created: str = Field(default_factory=_get_current_timestamp, description="Creation timestamp") + time_modified: str = Field(default_factory=_get_current_timestamp, description="Last modification timestamp") + author: str = Field(default="", description="Author or source of the memory") + score: float = Field(default=0, description="Relevance or importance score") + + metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata") + + def _update_modified_time(self) -> "MemoryNode": + """Update time_modified to current timestamp. + + Returns: + Self: Returns self for method chaining. + """ + self.time_modified = _get_current_timestamp() + return self + + def _update_memory_id(self) -> "MemoryNode": + """Generate memory_id from SHA-256 hash of content. + + Takes the first MEMORY_ID_LENGTH characters of the hash. + + Returns: + Self: Returns self for method chaining. + """ + if not self.content: + return self + + hash_obj = hashlib.sha256(self.content.encode('utf-8')) + hex_dig = hash_obj.hexdigest() + self.memory_id = hex_dig[:MEMORY_ID_LENGTH] + return self + + @model_validator(mode='after') + def _update_after_init(self) -> "MemoryNode": + """Post-initialization validator. + + Auto-generates memory_id from content if not provided. + + Returns: + Self: Returns self for method chaining. + """ + if not self.memory_id: + self._update_memory_id() + return self + + def __setattr__(self, name: str, value): + """Auto-update timestamps and memory_id when content or when_to_use changes. + + Args: + name: Attribute name being set. + value: New value for the attribute. + """ + should_update: bool = name in ("when_to_use", "content") and getattr(self, name, None) != value + super().__setattr__(name, value) + if should_update: + self._update_modified_time() + if name == "content": + self._update_memory_id() + + def to_vector_node(self) -> VectorNode: + """Convert to VectorNode for vector storage. + + When when_to_use is set, use it as vector content and store content in metadata. + When when_to_use is empty, use content as vector content directly. + + Returns: + VectorNode: Vector node representation of this memory. + """ + # Build base metadata (shared fields) + metadata: dict[str, Any] = { + "memory_type": self.memory_type.value, + "memory_target": self.memory_target, + "ref_memory_id": self.ref_memory_id, + "time_created": self.time_created, + "time_modified": self.time_modified, + "author": self.author, + "score": self.score, + **self.metadata, + } + + if self.when_to_use: + # Use when_to_use for vector embedding, store content in metadata + vector_content = self.when_to_use + metadata["content"] = self.content + else: + # Use content directly for vector embedding + vector_content = self.content + + return VectorNode( + unique_id=self.memory_id, + workspace_id=self.workspace_id, + content=vector_content, + metadata=metadata, + ) + + def format_memory(self) -> str: + """Format memory as human-readable string. + + Returns: + str: Formatted string with when_to_use, content, and ref_memory_id. + """ + parts: list[str] = [ + f"memory_id={self.memory_id}" + f"modified_time={self.time_modified}" + ] + + if self.when_to_use: + parts.append(self.when_to_use) + + if self.content: + parts.append(self.content) + + if self.metadata: + parts.append(f"metadata={json.dumps(self.metadata, ensure_ascii=False)}") + + if self.ref_memory_id: + parts.append(f"history_memory.ref_memory_id={self.ref_memory_id}") + + return " ".join(parts) + + @classmethod + def from_vector_node(cls, node: VectorNode) -> "MemoryNode": + """Reconstruct MemoryNode from VectorNode. + + Reverses the to_vector_node conversion: + - If metadata contains 'content': node.content -> when_to_use, metadata['content'] -> content + - Otherwise: node.content -> content, when_to_use remains empty + + Args: + node: VectorNode containing memory data. + + Returns: + Self: Reconstructed MemoryNode instance. + + Raises: + ValueError: If memory_type in metadata is invalid. + """ + metadata = node.metadata.copy() + memory_type_str = metadata.pop("memory_type", None) + + try: + memory_type: MemoryType = MemoryType(memory_type_str) + except ValueError as e: + raise ValueError( + f"Invalid memory_type '{memory_type_str}' in VectorNode metadata. " + f"Valid types are: {[t.value for t in MemoryType]}" + ) from e + + # Restore when_to_use and content based on metadata structure + if "content" in metadata: + # Original had when_to_use set + when_to_use = node.content + content = metadata.pop("content", "") + else: + # Original had empty when_to_use + when_to_use = "" + content = node.content + + return cls( + workspace_id=node.workspace_id, + memory_id=node.unique_id, + memory_type=memory_type, + memory_target=metadata.pop("memory_target", ""), + when_to_use=when_to_use, + content=content, + ref_memory_id=metadata.pop("ref_memory_id", ""), + time_created=metadata.pop("time_created", ""), + time_modified=metadata.pop("time_modified", ""), + author=metadata.pop("author", ""), + score=metadata.pop("score", 0), + metadata=metadata, + ) diff --git a/reme_ai/v2/tool/__init__.py b/reme_ai/v2/tool/__init__.py new file mode 100644 index 00000000..f31a6d00 --- /dev/null +++ b/reme_ai/v2/tool/__init__.py @@ -0,0 +1,36 @@ +"""Tool operations for memory management. + +This module provides various tool operations for managing memories, +including adding, reading, updating, deleting, and retrieving memories. +""" + +from .add_history_memory_op import AddHistoryMemoryOp +from .add_memory_op import AddMemoryOp +from .add_meta_memory_op import AddMetaMemoryOp +from .add_summary_memory_op import AddSummaryMemoryOp +from .base_memory_tool_op import BaseMemoryToolOp +from .delete_memory_op import DeleteMemoryOp +from .read_history_memory_op import ReadHistoryMemoryOp +from .read_identity_memory_op import ReadIdentityMemoryOp +from .read_meta_memory_op import ReadMetaMemoryOp +from .think_tool_op import ThinkToolOp +from .update_identity_memory_op import UpdateIdentityMemoryOp +from .update_memory_op import UpdateMemoryOp +from .vector_retrieve_memory_op import VectorRetrieveMemoryOp + +__all__ = [ + "BaseMemoryToolOp", + "AddMemoryOp", + "AddHistoryMemoryOp", + "AddMetaMemoryOp", + "AddSummaryMemoryOp", + "DeleteMemoryOp", + "ReadHistoryMemoryOp", + "ReadIdentityMemoryOp", + "ReadMetaMemoryOp", + "ThinkToolOp", + "UpdateIdentityMemoryOp", + "UpdateMemoryOp", + "VectorRetrieveMemoryOp", +] + diff --git a/reme_ai/v2/tool/add_history_memory_op.py b/reme_ai/v2/tool/add_history_memory_op.py new file mode 100644 index 00000000..85cd2fc1 --- /dev/null +++ b/reme_ai/v2/tool/add_history_memory_op.py @@ -0,0 +1,63 @@ +"""Add history memory operation for inserting history memory into the vector store. + +This module provides the AddHistoryMemoryOp class for adding history memory +from message lists to the vector store. +""" + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C +from .. import utils +from ..enumeration import MemoryType +from ..schema import MemoryNode + + +@C.register_op() +class AddHistoryMemoryOp(BaseMemoryToolOp): + """Operation for adding history memory to the vector store. + + This operation takes a list of messages, formats them using Message.format_message, + and stores them as history memory in the vector store. + """ + + def __init__(self, **kwargs): + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + + def build_input_schema(self) -> dict: + """Build input schema for history memory addition. + + Returns: + dict: Input schema for adding history memory from messages. + """ + return { + "messages": { + "type": "array", + "description": "messages", + "required": True, + "items": {"type": "object"} + } + } + + async def async_execute(self): + """Execute the add history memory operation. + + Takes messages from input, formats them using Message.format_message, + creates a MemoryNode with memory_type=HISTORY, and stores it in the vector store. + + Returns the created memory. + """ + workspace_id: str = self.workspace_id + messages: list = self.context.get("messages", []) + assert messages, "No messages provided" + memory_node = MemoryNode( + workspace_id=workspace_id, + memory_type=MemoryType.HISTORY, + content=utils.format_messages(messages), + author=self.author, + ) + vector_node = memory_node.to_vector_node() + + await self.vector_store.async_delete(node_ids=[vector_node.unique_id], workspace_id=workspace_id) + await self.vector_store.async_insert(nodes=[vector_node], workspace_id=workspace_id) + + self.set_output(memory_node) diff --git a/reme_ai/v2/tool/add_history_memory_prompt.yaml b/reme_ai/v2/tool/add_history_memory_prompt.yaml new file mode 100644 index 00000000..be514227 --- /dev/null +++ b/reme_ai/v2/tool/add_history_memory_prompt.yaml @@ -0,0 +1,5 @@ +tool: | + Add history memory to the vector store from a list of messages. + Use this tool to store conversation history as memory for future retrieval. + The messages will be formatted and stored as a single history memory entry. + diff --git a/reme_ai/v2/tool/add_memory_op.py b/reme_ai/v2/tool/add_memory_op.py new file mode 100644 index 00000000..41a87435 --- /dev/null +++ b/reme_ai/v2/tool/add_memory_op.py @@ -0,0 +1,170 @@ +"""Add memory operation for inserting memories into the vector store. + +This module provides the AddMemoryOp class for adding memories +to the vector store with support for both single and multiple memory modes. +""" + +from typing import Any, Dict, List + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C +from ..schema import MemoryNode + + +@C.register_op() +class AddMemoryOp(BaseMemoryToolOp): + """Operation for adding memories to the vector store. + + This operation supports both single and multiple memory addition modes, + controlled by the `enable_multiple` parameter inherited from BaseMemoryToolOp. + """ + + def __init__(self, + add_when_to_use: bool = False, + add_metadata: bool = True, + **kwargs): + super().__init__(**kwargs) + self.add_when_to_use: bool = add_when_to_use + self.add_metadata: bool = add_metadata + + def build_input_schema(self) -> dict: + """Build input schema for single memory addition mode. + + Returns: + dict: Input schema for adding a single memory. + """ + schema = {} + if self.add_when_to_use: + schema["when_to_use"] = { + "type": "string", + "description": self.get_prompt("when_to_use"), + "required": True, + } + schema["memory_content"] = { + "type": "string", + "description": self.get_prompt("memory_content"), + "required": True, + } + if self.add_metadata: + schema["metadata"] = { + "type": "object", + "description": self.get_prompt("metadata"), + "required": False, + } + return schema + + def build_multiple_input_schema(self) -> dict: + """Build input schema for multiple memory addition mode. + + Returns: + dict: Input schema for adding multiple memories. + """ + item_properties = {} + required_fields = ["memory_content"] + if self.add_when_to_use: + item_properties["when_to_use"] = { + "type": "string", + "description": self.get_prompt("when_to_use"), + } + required_fields.insert(0, "when_to_use") + item_properties["memory_content"] = { + "type": "string", + "description": self.get_prompt("memory_content"), + } + if self.add_metadata: + item_properties["metadata"] = { + "type": "object", + "description": self.get_prompt("metadata"), + } + return { + "memories": { + "type": "array", + "description": self.get_prompt("memories"), + "required": True, + "items": { + "type": "object", + "properties": item_properties, + "required": required_fields + }, + } + } + + def _build_memory_node( + self, + memory_content: str, + when_to_use: str, + metadata: Dict[str, Any], + workspace_id: str, + ) -> MemoryNode: + """Build a MemoryNode from memory_content, when_to_use and metadata. + + Args: + memory_content: The memory content. + when_to_use: Condition description for when to retrieve this memory. + metadata: Additional metadata for the memory. + workspace_id: The workspace ID. + + Returns: + MemoryNode: The constructed memory node. + """ + return MemoryNode( + workspace_id=workspace_id, + memory_type=self.memory_type, + memory_target=self.memory_target, + when_to_use=when_to_use, + content=memory_content, + ref_memory_id=self.ref_memory_id, + author=self.author, + metadata=metadata, + ) + + async def async_execute(self): + """Execute the add memory operation. + + Adds one or more memories to the vector store. The operation handles both + single memory (query + metadata) and multiple memories (list) inputs. + For each memory, it first deletes any existing memory with the same ID, + then inserts the new memory. + + Raises: + ValueError: If no valid memories are provided. + """ + workspace_id: str = self.workspace_id + + # Build memory nodes based on mode + memory_nodes: List[MemoryNode] = [] + if self.enable_multiple: + memories: List[Dict[str, Any]] = self.context.get("memories", []) + for mem in memories: + memory_content = mem.get("memory_content", "") + if not memory_content: + continue + + when_to_use = mem.get("when_to_use", "") + metadata = mem.get("metadata", {}) + memory_nodes.append(self._build_memory_node(memory_content, when_to_use, metadata, workspace_id)) + + else: + memory_content = self.context.get("memory_content", "") + if memory_content: + when_to_use = self.context.get("when_to_use", "") + metadata = self.context.get("metadata", {}) + memory_nodes.append(self._build_memory_node(memory_content, when_to_use, metadata, workspace_id)) + + if not memory_nodes: + self.set_output("No valid memories provided for addition.") + return + + vector_nodes = [node.to_vector_node() for node in memory_nodes] + node_ids: List[str] = [node.unique_id for node in vector_nodes] + + await self.vector_store.async_delete(node_ids=node_ids, workspace_id=workspace_id) + await self.vector_store.async_insert(nodes=vector_nodes, workspace_id=workspace_id) + + # Format output message + if len(memory_nodes) == 1: + output_msg = f"Successfully added memory (id={memory_nodes[0].memory_id}) to workspace={workspace_id}." + else: + output_msg = f"Successfully added {len(memory_nodes)} memories to workspace={workspace_id}." + + self.set_output(output_msg) diff --git a/reme_ai/v2/tool/add_memory_prompt.yaml b/reme_ai/v2/tool/add_memory_prompt.yaml new file mode 100644 index 00000000..70b4a8a6 --- /dev/null +++ b/reme_ai/v2/tool/add_memory_prompt.yaml @@ -0,0 +1,27 @@ +tool: | + Add a memory to the vector store for future retrieval. + Use this tool to store important information that should be remembered, such as "I am very happy" (meta), "John prefers dark mode" (personal), "To deploy, run build then push" (procedural), or "search_tool works best with short queries" (tool). + +tool_multiple: | + Add multiple memories to the vector store for future retrieval. + Use this tool to store multiple pieces of important information that should be remembered, such as "I am very happy" (meta), "John prefers dark mode" (personal), "To deploy, run build then push" (procedural), or "search_tool works best with short queries" (tool). + +when_to_use: | + Optional condition description for when to retrieve this memory. + This will be used for vector embedding to improve retrieval accuracy. + For example: "when user asks about authentication", "when deploying to production", "when using search_tool". + +memory_content: | + The content of the memory to store. + Should be a clear, concise statement that captures the information to remember. + +metadata: | + Optional metadata for the memory, including time or other important information. Can include: + - time: The timestamp or date associated with the memory. + - Any other custom key-value pairs relevant to the memory. + +memories: | + A list of memory objects to store. Each object should contain: + - when_to_use (optional): Condition description for when to retrieve this memory. + - memory_content (required): The content of the memory to store. + - metadata (optional): Additional metadata for the memory. diff --git a/reme_ai/v2/tool/add_meta_memory_op.py b/reme_ai/v2/tool/add_meta_memory_op.py new file mode 100644 index 00000000..da71f019 --- /dev/null +++ b/reme_ai/v2/tool/add_meta_memory_op.py @@ -0,0 +1,116 @@ +"""Add meta memory operation for adding memory metadata. + +This module provides the AddMetaMemoryOp class for adding memory metadata +(memory_type and memory_target) using file-based storage with CacheHandler. +""" + +import json +from typing import List + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C +from ..enumeration import MemoryType + + +@C.register_op() +class AddMetaMemoryOp(BaseMemoryToolOp): + """Operation for adding memory metadata using file-based storage.""" + + def build_input_schema(self) -> dict: + """Build input schema for single meta memory addition. + + Returns: + dict: Input schema for adding a single memory metadata entry. + """ + return { + "memory_type": { + "type": "string", + "description": self.get_prompt("memory_type"), + "enum": [MemoryType.PERSONAL.value, MemoryType.PROCEDURAL.value], + "required": True, + }, + "memory_target": { + "type": "string", + "description": self.get_prompt("memory_target"), + "required": True, + }, + } + + def build_multiple_input_schema(self) -> dict: + """Build input schema for multiple meta memory addition. + + Returns: + dict: Input schema for adding multiple memory metadata entries. + """ + return { + "meta_memories": { + "type": "array", + "description": self.get_prompt("meta_memories"), + "required": True, + "items": { + "type": "object", + "properties": { + "memory_type": { + "type": "string", + "description": self.get_prompt("memory_type"), + "enum": [MemoryType.PERSONAL.value, MemoryType.PROCEDURAL.value], + }, + "memory_target": { + "type": "string", + "description": self.get_prompt("memory_target"), + }, + }, + "required": ["memory_type", "memory_target"], + }, + }, + } + + def _load_meta_memories(self) -> List[dict]: + result = self.metadata_handler.load("meta_memories") + return result if result is not None else [] + + def _save_meta_memories(self, memories: List[dict]) -> bool: + return self.metadata_handler.save("meta_memories", memories) + + async def async_execute(self): + """Execute the add meta memory operation. + + Adds memory metadata to file storage. Supports both single and multiple modes. + Duplicates (same memory_type and memory_target) are skipped. + """ + existing_memories: List[dict] = self._load_meta_memories() + existing_set = {(m["memory_type"], m["memory_target"]) for m in existing_memories} + + # Build new memories to add based on mode + new_memories: List[dict] = [] + if self.enable_multiple: + meta_memories: List[dict] = self.context.get("meta_memories", []) + for mem in meta_memories: + memory_type = mem.get("memory_type", "") + memory_target = mem.get("memory_target", "") + if memory_type and (memory_type, memory_target) not in existing_set: + new_memories.append({ + "memory_type": memory_type, + "memory_target": memory_target, + }) + existing_set.add((memory_type, memory_target)) + else: + memory_type = self.context.get("memory_type", "") + memory_target = self.context.get("memory_target", "") + if memory_type and (memory_type, memory_target) not in existing_set: + new_memories.append({ + "memory_type": memory_type, + "memory_target": memory_target, + }) + + if not new_memories: + self.set_output("No new meta memories to add (all entries already exist or invalid).") + return + + # Merge and save + all_memories = existing_memories + new_memories + self._save_meta_memories(all_memories) + + # Format output + added_str = json.dumps(new_memories, ensure_ascii=False) + self.set_output(f"Successfully added {len(new_memories)} meta memory entries: {added_str}") diff --git a/reme_ai/v2/tool/add_meta_memory_prompt.yaml b/reme_ai/v2/tool/add_meta_memory_prompt.yaml new file mode 100644 index 00000000..69c9b91a --- /dev/null +++ b/reme_ai/v2/tool/add_meta_memory_prompt.yaml @@ -0,0 +1,20 @@ +tool: | + Add a single memory metadata entry for the agent. + Use this tool to register a new memory type and target. + +tool_multiple: | + Add multiple memory metadata entries for the agent. + Use this tool to register multiple memory types and targets at once. + +meta_memories: | + A list of memory metadata entries to add. Each entry contains memory_type and memory_target. + +memory_type: | + The type of memory. Valid values are: personal, procedural. + - personal: Person-specific memory (preferences and context about specific individuals) + - procedural: Procedural memory (how-to knowledge) + +memory_target: | + The target of the memory. For example, in personal memory, this would be the person's name. + For identity memory, this can be empty or a specific aspect of identity. + diff --git a/reme_ai/v2/tool/add_summary_memory_op.py b/reme_ai/v2/tool/add_summary_memory_op.py new file mode 100644 index 00000000..747f01ea --- /dev/null +++ b/reme_ai/v2/tool/add_summary_memory_op.py @@ -0,0 +1,57 @@ +"""Add summary memory operation for inserting summarized memories into the vector store. + +This module provides the AddSummaryMemoryOp class for adding summary memories +to the vector store. The LLM summarizes the context and calls this tool with +the summarized content. +""" + +from .add_memory_op import AddMemoryOp +from .. import C + + +@C.register_op() +class AddSummaryMemoryOp(AddMemoryOp): + """Operation for adding summary memories to the vector store. + + This operation only supports single memory addition mode (enable_multiple=False). + The external LLM summarizes the provided context and calls this tool with + the summary_memory parameter to store the summarized content. + """ + + def __init__(self, **kwargs): + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + + def build_input_schema(self) -> dict: + """Build input schema for summary memory addition. + + Returns: + dict: Input schema for adding a summary memory. + """ + schema = { + "summary_memory": { + "type": "string", + "description": self.get_prompt("summary_memory"), + "required": True, + }, + } + if self.add_metadata: + schema["metadata"] = { + "type": "object", + "description": self.get_prompt("metadata"), + "required": False, + } + return schema + + async def async_execute(self): + """Execute the add summary memory operation. + + Adds a summary memory to the vector store. The external LLM should + summarize the context before calling this tool with the summary_memory parameter. + """ + # Map summary_memory to memory_content for parent class + summary_memory = self.context.get("summary_memory", "") + self.context["memory_content"] = summary_memory + + # Call parent's async_execute + await super().async_execute() diff --git a/reme_ai/v2/tool/add_summary_memory_prompt.yaml b/reme_ai/v2/tool/add_summary_memory_prompt.yaml new file mode 100644 index 00000000..b08fa5fc --- /dev/null +++ b/reme_ai/v2/tool/add_summary_memory_prompt.yaml @@ -0,0 +1,14 @@ +tool: | + Add a summary memory to the vector store for future retrieval. + Use this tool to store a summarized version of the provided context. + The LLM should first summarize the context, then call this tool with the summarized content. + +summary_memory: | + The summarized content to store as memory. + Should be a clear, concise summary that captures the key information from the context. + +metadata: | + Optional metadata for the memory, including time or other important information. Can include: + - time: The timestamp or date associated with the memory. + - Any other custom key-value pairs relevant to the memory. + diff --git a/reme_ai/v2/tool/base_memory_tool_op.py b/reme_ai/v2/tool/base_memory_tool_op.py new file mode 100644 index 00000000..c6718c90 --- /dev/null +++ b/reme_ai/v2/tool/base_memory_tool_op.py @@ -0,0 +1,120 @@ +from abc import ABC +from pathlib import Path + +from flowllm.core.schema import ToolCall +from flowllm.core.storage import CacheHandler + +from .. import C, BaseAsyncToolOp +from ..enumeration import MemoryType + + +@C.register_op() +class BaseMemoryToolOp(BaseAsyncToolOp, ABC): + """ + Base class for memory tool operations. + + This abstract base class provides common functionality for memory-related tools, + including configuration handling, workspace identification, and tool call construction. + """ + + def __init__(self, + enable_multiple: bool = True, + enable_thinking_params: bool = False, + memory_metadata_dir: str = "./memory_metadata", + **kwargs): + """ + Initialize the BaseMemoryToolOp. + + Args: + enable_multiple (bool): Whether to enable multiple item operations. Defaults to True. + enable_thinking_params (bool): Whether to include thinking parameters in the schema. Defaults to False. + **kwargs: Additional keyword arguments passed to the parent class. + """ + super().__init__(**kwargs) + self.enable_multiple: bool = enable_multiple + self.enable_thinking_params: bool = enable_thinking_params + self.memory_metadata_dir: Path = Path(memory_metadata_dir) + self._metadata_handler: CacheHandler | None = None + + def build_input_schema(self) -> dict: + """ + Build the input schema for single item operations. + + This method should be overridden by subclasses to define + the specific input schema for single item operations. + + Returns: + dict: The input schema definition. + """ + return {} + + def build_multiple_input_schema(self) -> dict: + """ + Build the input schema for multiple item operations. + + This method should be overridden by subclasses to define + the specific input schema for multiple item operations. + + Returns: + dict: The input schema definition for multiple items. + """ + return {} + + def build_tool_call(self) -> ToolCall: + """ + Build and return a ToolCall object with appropriate schema and description. + + Constructs a ToolCall with either single or multiple item input schema + based on the enable_multiple flag, and optionally adds thinking parameters. + + Returns: + ToolCall: Configured ToolCall object. + """ + if self.enable_multiple: + input_schema = self.build_multiple_input_schema() + else: + input_schema = self.build_input_schema() + + if self.enable_thinking_params and "thinking" not in input_schema: + input_schema = { + "thinking": { + "type": "string", + "description": "Your thinking and reasoning about how to fill in the parameters", + "required": True, + }, + **input_schema, + } + + tool_name: str = "tool" + ("_multiple" if self.enable_multiple else "") + return ToolCall( + **{ + "description": self.get_prompt(tool_name), + "input_schema": input_schema, + } + ) + + @property + def metadata_handler(self): + if self._metadata_handler is None: + self._metadata_handler = CacheHandler(self.memory_metadata_dir / self.workspace_id) + return self._metadata_handler + + @property + def memory_type(self) -> MemoryType: + return MemoryType(self.context.get("memory_type")) + + @property + def memory_target(self) -> str: + return self.context.get("memory_target", "") + + @property + def ref_memory_id(self) -> str: + return self.context.get("ref_memory_id", "") + + @property + def author(self) -> str: + return self.context.get("author", "") + + @property + def workspace_id(self) -> str: + return self.context.get("workspace_id", "default") \ No newline at end of file diff --git a/reme_ai/v2/tool/delete_memory_op.py b/reme_ai/v2/tool/delete_memory_op.py new file mode 100644 index 00000000..59cd9c74 --- /dev/null +++ b/reme_ai/v2/tool/delete_memory_op.py @@ -0,0 +1,83 @@ +"""Delete memory operation for removing memories from the vector store. + +This module provides the DeleteMemoryOp class for deleting memories +by their IDs from the vector store. +""" + +from typing import List + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C + + +@C.register_op() +class DeleteMemoryOp(BaseMemoryToolOp): + """Operation for deleting memories from the vector store. + + This operation supports both single and multiple memory deletion modes, + controlled by the `enable_multiple` parameter inherited from BaseMemoryToolOp. + """ + + def build_input_schema(self) -> dict: + """Build input schema for single memory deletion mode. + + Returns: + dict: Input schema for deleting a single memory by ID. + """ + return { + "memory_id": { + "type": "string", + "description": self.get_prompt("memory_id"), + "required": True, + } + } + + def build_multiple_input_schema(self) -> dict: + """Build input schema for multiple memory deletion mode. + + Returns: + dict: Input schema for deleting multiple memories by IDs. + """ + return { + "memory_ids": { + "type": "array", + "description": self.get_prompt("memory_ids"), + "required": True, + "items": {"type": "string"}, + } + } + + async def async_execute(self): + """Execute the delete memory operation. + + Deletes one or more memories from the vector store based on their IDs. + The operation handles both single ID (string) and multiple IDs (list) inputs. + + Raises: + ValueError: If no valid memory IDs are provided. + """ + workspace_id: str = self.workspace_id + + # Get memory_ids based on mode + if self.enable_multiple: + memory_ids: List[str] = self.context.get("memory_ids", []) + else: + memory_ids: List[str] = [self.context.get("memory_id", "")] + + # Filter out empty strings + memory_ids = [memory_id for memory_id in memory_ids if memory_id] + + if not memory_ids: + self.set_output("No valid memory IDs provided for deletion.") + return + + # Perform deletion + await self.vector_store.async_delete(node_ids=memory_ids, workspace_id=workspace_id) + + # Format output message + if len(memory_ids) == 1: + output_msg = f"Successfully deleted memory (id={memory_ids[0]}) from workspace={workspace_id}." + else: + output_msg = f"Successfully deleted {len(memory_ids)} memories from workspace={workspace_id}." + + self.set_output(output_msg) diff --git a/reme_ai/v2/tool/delete_memory_prompt.yaml b/reme_ai/v2/tool/delete_memory_prompt.yaml new file mode 100644 index 00000000..078d09c4 --- /dev/null +++ b/reme_ai/v2/tool/delete_memory_prompt.yaml @@ -0,0 +1,17 @@ +tool: | + Delete a memory from the vector store using its unique ID. + Use this tool when the user explicitly requests to remove or forget information, + or when a memory is identified as outdated, incorrect, or no longer relevant. + Memory ID can be obtained from previous memory retrieval results. + +tool_multiple: | + Delete multiple memories from the vector store using their unique IDs. + Use this tool when the user explicitly requests to remove or forget multiple pieces of information, + or when memories are identified as outdated, incorrect, or no longer relevant. + Memory IDs can be obtained from previous memory retrieval results. + +memory_id: | + The unique identifier (memory_id) of the memory to delete. + +memory_ids: | + A list of unique identifiers (memory_ids) of the memories to delete. diff --git a/reme_ai/v2/tool/hands_off_op.py b/reme_ai/v2/tool/hands_off_op.py new file mode 100644 index 00000000..7415829e --- /dev/null +++ b/reme_ai/v2/tool/hands_off_op.py @@ -0,0 +1,183 @@ +"""Hands-off operation for distributing memory summarization tasks to appropriate agents. + +This module provides the HandsOffOp class for distributing memory summarization tasks +to the appropriate memory agents (identity, personal, procedural, tool) based on memory_type, +and executing them in parallel. +""" + +import json +from typing import List, Dict + +from loguru import logger + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C +from ..agent import BaseMemoryAgentOp +from ..enumeration import MemoryType + + +@C.register_op() +class HandsOffOp(BaseMemoryToolOp): + """Operation for distributing memory summarization tasks to appropriate agents. + + This tool receives memory_type and memory_target parameters, then distributes + the summarization tasks to the corresponding memory agents and executes them in parallel. + """ + + def build_input_schema(self) -> dict: + """Build input schema for single memory task distribution. + + Returns: + dict: Input schema for distributing a single memory task. + """ + return { + "memory_type": { + "type": "string", + "description": "memory_type", + "enum": [ + MemoryType.IDENTITY.value, + MemoryType.PERSONAL.value, + MemoryType.PROCEDURAL.value, + MemoryType.TOOL.value, + ], + "required": True, + }, + "memory_target": { + "type": "string", + "description": "memory_target", + "required": True, + }, + } + + def build_multiple_input_schema(self) -> dict: + """Build input schema for multiple memory task distribution. + + Returns: + dict: Input schema for distributing multiple memory tasks. + """ + return { + "memory_tasks": { + "type": "array", + "description": self.get_prompt("memory_tasks"), + "required": True, + "items": { + "type": "object", + "properties": { + "memory_type": { + "type": "string", + "description": "memory_type", + "enum": [ + MemoryType.IDENTITY.value, + MemoryType.PERSONAL.value, + MemoryType.PROCEDURAL.value, + MemoryType.TOOL.value, + ], + }, + "memory_target": { + "type": "string", + "description": "memory_target", + }, + }, + "required": ["memory_type", "memory_target"], + }, + }, + } + + async def _build_agent_op_dict(self) -> Dict[MemoryType, BaseMemoryAgentOp]: + """Build a dictionary mapping memory types to their corresponding agent operations. + + Returns: + Dict[MemoryType, BaseMemoryAgentOp]: Dictionary mapping memory types to agent ops. + """ + + agent_op_dict: Dict[MemoryType, BaseMemoryAgentOp] = {} + for op in self.ops.values(): + if isinstance(op, BaseMemoryAgentOp) and op.memory_type is not None: + agent_op_dict[op.memory_type] = op + + return agent_op_dict + + async def async_execute(self): + """Execute the hands-off operation. + + Distributes memory summarization tasks to appropriate agents based on memory_type + and executes them in parallel. Supports both single and multiple modes. + """ + # Build agent operation dictionary + agent_op_dict = await self._build_agent_op_dict() + + # Collect tasks to execute + tasks: List[Dict] = [] + if self.enable_multiple: + memory_tasks: List[dict] = self.context.get("memory_tasks", []) + for task in memory_tasks: + memory_type_str = task.get("memory_type", "") + memory_target = task.get("memory_target", "") + if memory_type_str: + tasks.append({ + "memory_type": MemoryType(memory_type_str), + "memory_target": memory_target, + }) + else: + memory_type_str = self.context.get("memory_type", "") + memory_target = self.context.get("memory_target", "") + if memory_type_str: + tasks.append({ + "memory_type": MemoryType(memory_type_str), + "memory_target": memory_target, + }) + + if not tasks: + self.set_output("No valid memory tasks to execute.") + return + + # Submit tasks to corresponding agents in parallel + op_list = [] + for i, task in enumerate(tasks): + memory_type: MemoryType = task["memory_type"] + memory_target: str = task["memory_target"] + + if memory_type not in agent_op_dict: + logger.warning(f"No agent found for memory_type={memory_type.value}") + continue + + # Copy the agent op and prepare for execution + op_copy = agent_op_dict[memory_type].copy() + op_list.append({ + "op": op_copy, + "memory_type": memory_type, + "memory_target": memory_target, + }) + + # Submit async task + logger.info(f"Task {i}: Submitting {memory_type.value} agent for target={memory_target}") + self.submit_async_task( + op_copy.async_call, + workspace_id=self.context.get("workspace_id", "default"), + memory_target=memory_target, + query=self.context.get("query", ""), + messages=self.context.get("messages", []), + ref_memory_id=self.context.get("ref_memory_id", ""), + ) + + # Wait for all tasks to complete + await self.join_async_task() + + # Collect results + results = [] + for i, op_info in enumerate(op_list): + op = op_info["op"] + memory_type = op_info["memory_type"] + memory_target = op_info["memory_target"] + + result_str = str(op.output) + results.append({ + "memory_type": memory_type.value, + "memory_target": memory_target, + "result": result_str[:200] + ("..." if len(result_str) > 200 else ""), + }) + logger.info(f"Task {i}: Completed {memory_type.value} agent for target={memory_target}") + + # Format output + results_str = json.dumps(results, ensure_ascii=False, indent=2) + self.set_output(f"Successfully executed {len(results)} memory summarization tasks:\n{results_str}") diff --git a/reme_ai/v2/tool/hands_off_prompt.yaml b/reme_ai/v2/tool/hands_off_prompt.yaml new file mode 100644 index 00000000..aaa8ad96 --- /dev/null +++ b/reme_ai/v2/tool/hands_off_prompt.yaml @@ -0,0 +1,14 @@ +tool: | + Distribute a single memory summarization task to the appropriate sub agent. + Based on the meta memory present in the context, fill in the corresponding memory_type and memory_target. + Use this tool to hand off a memory task to the corresponding memory agent. + +tool_multiple: | + Distribute multiple memory summarization tasks to the appropriate sub agents. + Based on the meta memory present in the context, fill in the corresponding memory_type and memory_target for each task. + Use this tool to hand off multiple memory tasks to their corresponding memory agents in parallel. + +memory_tasks: | + A list of memory tasks to distribute. Each task contains memory_type and memory_target. + + diff --git a/reme_ai/v2/tool/read_history_memory_op.py b/reme_ai/v2/tool/read_history_memory_op.py new file mode 100644 index 00000000..56f62648 --- /dev/null +++ b/reme_ai/v2/tool/read_history_memory_op.py @@ -0,0 +1,101 @@ +"""Read history memory operation for retrieving history memory by their IDs from the vector store. + +This module provides the ReadHistoryMemoryOp class for reading history memory +by their unique IDs from the vector store. +""" + +from typing import List + +from flowllm.core.schema import VectorNode + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C +from ..enumeration import MemoryType +from ..schema import MemoryNode + + +@C.register_op() +class ReadHistoryMemoryOp(BaseMemoryToolOp): + """Operation for reading history memory from the vector store by their IDs. + + This operation supports both single and multiple ID modes, + controlled by the `enable_multiple` parameter inherited from BaseMemoryToolOp. + """ + + def build_input_schema(self) -> dict: + """Build input schema for single history memory reading mode. + + Returns: + dict: Input schema for reading a single history memory by ID. + """ + return { + "memory_id": { + "type": "string", + "description": self.get_prompt("memory_id"), + "required": True, + } + } + + def build_multiple_input_schema(self) -> dict: + """Build input schema for multiple history memory reading mode. + + Returns: + dict: Input schema for reading multiple history memory by IDs. + """ + return { + "memory_ids": { + "type": "array", + "description": self.get_prompt("memory_ids"), + "required": True, + "items": {"type": "string"}, + } + } + + async def async_execute(self): + """Execute the read history memory operation. + + Reads one or more history memory from the vector store based on their IDs. + The operation handles both single ID (string) and multiple IDs (list) inputs. + + Returns the memory_value for each found history memory. + + Raises: + ValueError: If no valid history memory IDs are provided. + """ + workspace_id: str = self.workspace_id + + # Get memory_ids based on mode + if self.enable_multiple: + memory_ids: List[str] = self.context.get("memory_ids", []) + else: + memory_ids: List[str] = [self.context.get("memory_id", "")] + + # Filter out empty strings + memory_ids = [memory_id for memory_id in memory_ids if memory_id] + + if not memory_ids: + self.set_output("No valid history memory IDs provided for reading.") + return + + # Perform search by IDs using filter + nodes: List[VectorNode] = await self.vector_store.async_search( + query="", + workspace_id=workspace_id, + top_k=len(memory_ids), + filter_dict={"unique_id": memory_ids} + ) + + if not nodes: + self.set_output(f"No history memory found with the provided IDs in workspace={workspace_id}.") + return + + # Convert nodes to memories + memories: List[MemoryNode] = [MemoryNode.from_vector_node(n) for n in nodes] + + # Format output with memory_value + output_lines = [] + for memory in memories: + assert memory.memory_type is MemoryType.HISTORY + output_lines.append(f"{memory.memory_id}:\n{memory.memory_value}") + + self.set_output("\n".join(output_lines)) diff --git a/reme_ai/v2/tool/read_history_memory_prompt.yaml b/reme_ai/v2/tool/read_history_memory_prompt.yaml new file mode 100644 index 00000000..8bf6b915 --- /dev/null +++ b/reme_ai/v2/tool/read_history_memory_prompt.yaml @@ -0,0 +1,15 @@ +tool: | + Read a history memory from the vector store using its unique ID. + Use this tool to retrieve the full content of a specific history memory + when you already know its ID from previous retrieval results. + +tool_multiple: | + Read multiple history memory from the vector store using their unique IDs. + Use this tool to retrieve the full content of specific history memory + when you already know their IDs from previous retrieval results. + +memory_id: | + The unique identifier (memory_id) of the history memory to read. + +memory_ids: | + A list of unique identifiers (memory_ids) of the history memory to read. diff --git a/reme_ai/v2/tool/read_identity_memory_op.py b/reme_ai/v2/tool/read_identity_memory_op.py new file mode 100644 index 00000000..fae27d9d --- /dev/null +++ b/reme_ai/v2/tool/read_identity_memory_op.py @@ -0,0 +1,41 @@ +"""Read identity memory operation for retrieving agent self-cognition memories. + +This module provides the ReadIdentityMemoryOp class for reading identity memories +using file-based storage with CacheHandler. +""" + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C + + +@C.register_op() +class ReadIdentityMemoryOp(BaseMemoryToolOp): + """Operation for reading identity memories using file-based storage.""" + + def __init__(self, **kwargs): + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + + def _load_identity_memory(self, workspace_id: str) -> str: + """Load identity memory from file. + + Args: + workspace_id: The workspace ID. + + Returns: + str: The identity memory content, or empty string if not found. + """ + metadata_handler = self.get_metadata_handler(workspace_id) + result = self.load_metadata_value(metadata_handler, "identity_memory") + return result if result is not None else "" + + async def async_execute(self): + """Execute the read identity memory operation. + + Reads identity memory from file storage. + """ + identity_memory = self._load_identity_memory(self.workspace_id) + if identity_memory: + self.set_output(f"Identity memory:\n{identity_memory}") + else: + self.set_output(f"No identity memory found in workspace={self.workspace_id}.") diff --git a/reme_ai/v2/tool/read_identity_memory_prompt.yaml b/reme_ai/v2/tool/read_identity_memory_prompt.yaml new file mode 100644 index 00000000..78fcd2d7 --- /dev/null +++ b/reme_ai/v2/tool/read_identity_memory_prompt.yaml @@ -0,0 +1,4 @@ +tool: | + Read the identity memory for the agent. + Use this tool to retrieve self-cognition information, such as identity, personality, or current state of the agent. + diff --git a/reme_ai/v2/tool/read_meta_memory_op.py b/reme_ai/v2/tool/read_meta_memory_op.py new file mode 100644 index 00000000..99e9da09 --- /dev/null +++ b/reme_ai/v2/tool/read_meta_memory_op.py @@ -0,0 +1,88 @@ +"""Read meta memory operation for retrieving memory metadata. + +This module provides the ReadMetaMemoryOp class for reading memory metadata +(memory_type and memory_target) from file storage. +""" + +from typing import List, Dict + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C +from ..enumeration.memory_type import MemoryType + + +@C.register_op() +class ReadMetaMemoryOp(BaseMemoryToolOp): + """Operation for reading memory metadata from file storage.""" + + def __init__( + self, + enable_tool_memory: bool = False, + enable_identity_memory: bool = False, + **kwargs + ): + """Initialize the ReadMetaMemoryOp. + + Args: + enable_tool_memory: Whether to include TOOL type meta memory. Defaults to True. + enable_identity_memory: Whether to include IDENTITY type meta memory. Defaults to False. + **kwargs: Additional keyword arguments passed to parent class. + """ + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + self.enable_tool_memory = enable_tool_memory + self.enable_identity_memory = enable_identity_memory + + def _load_meta_memories(self, workspace_id: str) -> List[Dict[str, str]]: + metadata_handler = self.get_metadata_handler(workspace_id) + result = self.load_metadata_value(metadata_handler, "meta_memories") + all_memories = result if result is not None else [] + + filtered_memories = [] + for m in all_memories: + memory_type = MemoryType(m.get("memory_type")) + assert memory_type in (MemoryType.PERSONAL, MemoryType.PROCEDURAL) + filtered_memories.append(m) + + if self.enable_tool_memory: + filtered_memories.append({"memory_type": MemoryType.TOOL.value, "memory_target": "tool_guidelines"}) + + if self.enable_identity_memory: + filtered_memories.append({"memory_type": MemoryType.IDENTITY.value, "memory_target": "self"}) + + return filtered_memories + + def _format_memory_metadata(self, memories: List[Dict[str, str]]) -> str: + """Format memory metadata into a readable string. + + Args: + memories: List of memory metadata entries. + + Returns: + str: Formatted memory metadata string. + """ + if not memories: + return "" + + lines = [] + for memory in memories: + memory_type = memory["memory_type"] + memory_target = memory["memory_target"] + description = self.get_prompt(f"type_{memory_type}") + lines.append(f"- {memory_type}({memory_target}): {description}") + + return "\n".join(lines) + + async def async_execute(self): + """Execute the read meta memory operation. + + Reads memory metadata from file storage and formats output. + """ + workspace_id: str = self.workspace_id + memories = self._load_meta_memories(workspace_id) + + if memories: + formatted = self._format_memory_metadata(memories) + self.set_output(formatted) + else: + self.set_output(f"No memory metadata found in workspace={workspace_id}.") diff --git a/reme_ai/v2/tool/read_meta_memory_prompt.yaml b/reme_ai/v2/tool/read_meta_memory_prompt.yaml new file mode 100644 index 00000000..8b0358a8 --- /dev/null +++ b/reme_ai/v2/tool/read_meta_memory_prompt.yaml @@ -0,0 +1,16 @@ +tool: | + Read the memory metadata for the agent. + Use this tool to retrieve memory type and target information from file storage. + +type_identity: | + Self-cognition memory storing agent's identity, personality, and current state. + +type_personal: | + Person-specific memory storing preferences and context about specific individuals. + +type_procedural: | + Procedural memory storing how-to knowledge and step-by-step processes. + +type_tool: | + Tool memory storing tool usage patterns, success rates, token consumption, and latency. + diff --git a/reme_ai/v2/tool/think_tool_op.py b/reme_ai/v2/tool/think_tool_op.py new file mode 100644 index 00000000..aacca76d --- /dev/null +++ b/reme_ai/v2/tool/think_tool_op.py @@ -0,0 +1,31 @@ +from .. import C, BaseAsyncToolOp +from ..schema import ToolCall + + +@C.register_op() +class ThinkToolOp(BaseAsyncToolOp): + """Utility operation that prompts the model for explicit reflection text.""" + + def __init__(self, add_output_reflection: bool = False, **kwargs): + super().__init__(**kwargs) + self.add_output_reflection: bool = add_output_reflection + + def build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "input_schema": { + "reflection": { + "type": "string", + "description": self.get_prompt("reflection"), + "required": True, + }, + }, + }, + ) + + async def async_execute(self): + if self.add_output_reflection: + self.set_output(self.context["reflection"]) + else: + self.set_output(self.get_prompt("reflection_output")) diff --git a/reme_ai/v2/tool/think_tool_prompt.yaml b/reme_ai/v2/tool/think_tool_prompt.yaml new file mode 100644 index 00000000..75903557 --- /dev/null +++ b/reme_ai/v2/tool/think_tool_prompt.yaml @@ -0,0 +1,32 @@ +tool: | + Before calling any external tool or when rethinking and planning is needed, you must invoke this tool for brief reflection. + The output must cover: + 1. Whether the current context is enough to answer the user directly, plus reasoning. + 2. If not, what information or validation is missing. + 3. A strategy to close the gap: which tool to call next, why, and key parameters or query terms. + Keep the reasoning tightly scoped to the current turn, avoid unrelated background, + and do not execute tools from here—only produce clear, actionable thoughts. + +reflection: | + 1) Can I answer now? Why? + 2) What is missing? + 3) Which tool + params next? + +reflection_output: | + Reflection has been recorded. + +tool_zh: | + 每次准备调用任何外部工具之前或者需要重新思考规划,都必须先调用本工具进行简短思考。 + 输出需覆盖以下要点: + 1. 评估当前上下文是否足以直接回答用户问题,并解释理由。 + 2. 若不能回答,明确缺失的信息或验证步骤。 + 3. 针对缺口设计下一步策略:列出计划使用的工具、调用目的、关键参数或查询关键词。 + 思考要紧扣当前轮对话内容,避免复述无关背景,不要直接执行工具,只输出清晰推理。 + +reflection_zh: | + 1) 能直接回答吗?为什么? + 2) 缺什么信息? + 3) 下一步用哪个工具+参数? + +reflection_output_zh: | + 已经记录反思 \ No newline at end of file diff --git a/reme_ai/v2/tool/update_identity_memory_op.py b/reme_ai/v2/tool/update_identity_memory_op.py new file mode 100644 index 00000000..b6658164 --- /dev/null +++ b/reme_ai/v2/tool/update_identity_memory_op.py @@ -0,0 +1,61 @@ +"""Update identity memory operation for saving agent self-cognition memories. + +This module provides the UpdateIdentityMemoryOp class for updating identity memories +using file-based storage with CacheHandler. +""" + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C + + +@C.register_op() +class UpdateIdentityMemoryOp(BaseMemoryToolOp): + """Operation for updating identity memories using file-based storage.""" + + def __init__(self, **kwargs): + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + + def build_input_schema(self) -> dict: + """Build input schema for identity memory update. + + Returns: + dict: Input schema for updating an identity memory. + """ + return { + "identity_memory": { + "type": "string", + "description": self.get_prompt("identity_memory"), + "required": True, + }, + } + + def _save_identity_memory(self, identity_memory: str, workspace_id: str) -> bool: + """Save identity memory to file. + + Args: + identity_memory: The memory content for identity. + workspace_id: The workspace ID. + + Returns: + bool: Whether the save was successful. + """ + metadata_handler = self.get_metadata_handler(workspace_id) + return self.save_metadata_value(metadata_handler, "identity_memory", identity_memory) + + async def async_execute(self): + """Execute the update identity memory operation. + + Updates identity memory to file storage. + """ + workspace_id: str = self.workspace_id + + identity_memory = self.context.get("identity_memory", "") + + if not identity_memory: + self.set_output("No valid identity memory provided for update.") + return + + self._save_identity_memory(identity_memory, workspace_id) + self.set_output(f"Successfully updated identity memory in workspace={workspace_id}.") + diff --git a/reme_ai/v2/tool/update_identity_memory_prompt.yaml b/reme_ai/v2/tool/update_identity_memory_prompt.yaml new file mode 100644 index 00000000..1839300e --- /dev/null +++ b/reme_ai/v2/tool/update_identity_memory_prompt.yaml @@ -0,0 +1,8 @@ +tool: | + Update the identity memory for the agent. + Use this tool to store self-cognition information, such as identity, personality, or current state of the agent. + +identity_memory: | + The identity memory content to store. + Should be a clear, concise statement that captures the agent's self-cognition, personality traits, or current state. + diff --git a/reme_ai/v2/tool/update_memory_op.py b/reme_ai/v2/tool/update_memory_op.py new file mode 100644 index 00000000..f89920ff --- /dev/null +++ b/reme_ai/v2/tool/update_memory_op.py @@ -0,0 +1,168 @@ +"""Update memory operation for updating memories in the vector store. + +This module provides the UpdateMemoryOp class for updating memories +by deleting the old memory and inserting a new one with updated content. +""" + +from typing import Any, Dict, List + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C +from ..schema import MemoryNode + + +@C.register_op() +class UpdateMemoryOp(BaseMemoryToolOp): + """Operation for updating memories in the vector store. + + This operation supports both single and multiple memory update modes, + controlled by the `enable_multiple` parameter inherited from BaseMemoryToolOp. + Update is performed by deleting the old memory and inserting a new one. + """ + + def build_input_schema(self) -> dict: + """Build input schema for single memory update mode. + + Returns: + dict: Input schema for updating a single memory. + """ + return { + "memory_id": { + "type": "string", + "description": self.get_prompt("memory_id"), + "required": True, + }, + "memory_content": { + "type": "string", + "description": self.get_prompt("memory_content"), + "required": True, + }, + "metadata": { + "type": "object", + "description": self.get_prompt("metadata"), + "required": False, + } + } + + def build_multiple_input_schema(self) -> dict: + """Build input schema for multiple memory update mode. + + Returns: + dict: Input schema for updating multiple memories. + """ + return { + "memories": { + "type": "array", + "description": self.get_prompt("memories"), + "required": True, + "items": { + "type": "object", + "properties": { + "memory_id": { + "type": "string", + "description": self.get_prompt("memory_id"), + }, + "memory_content": { + "type": "string", + "description": self.get_prompt("memory_content"), + }, + "metadata": { + "type": "object", + "description": self.get_prompt("metadata"), + } + }, + "required": ["memory_id", "memory_content"] + }, + } + } + + def _build_memory_node( + self, + memory_content: str, + metadata: Dict[str, Any], + workspace_id: str, + ) -> MemoryNode: + """Build a MemoryNode from memory_content and metadata. + + Args: + memory_content: The memory content. + metadata: Additional metadata for the memory. + workspace_id: The workspace ID. + + Returns: + MemoryNode: The constructed memory node. + """ + return MemoryNode( + workspace_id=workspace_id, + memory_type=self.memory_type, + memory_target=self.memory_target, + when_to_use="", + content=memory_content, + ref_memory_id=self.ref_memory_id, + author=self.author, + metadata=metadata, + ) + + async def async_execute(self): + """Execute the update memory operation. + + Updates one or more memories in the vector store. The operation handles both + single memory and multiple memories inputs. For each update, it first deletes + the old memory by memory_id, then inserts the new memory with updated content. + + Raises: + ValueError: If no valid memories are provided. + """ + workspace_id: str = self.workspace_id + + # Collect old memory_ids to delete and new memory nodes to insert + old_memory_ids: List[str] = [] + new_memory_nodes: List[MemoryNode] = [] + + if self.enable_multiple: + memories: List[Dict[str, Any]] = self.context.get("memories", []) + for mem in memories: + memory_id = mem.get("memory_id", "") + memory_content = mem.get("memory_content", "") + if not memory_id or not memory_content: + continue + metadata = mem.get("metadata", {}) or {} + old_memory_ids.append(memory_id) + new_memory_nodes.append(self._build_memory_node(memory_content, metadata, workspace_id)) + else: + memory_id = self.context.get("memory_id", "") + memory_content = self.context.get("memory_content", "") + if memory_id and memory_content: + metadata = self.context.get("metadata", {}) or {} + old_memory_ids.append(memory_id) + new_memory_nodes.append(self._build_memory_node(memory_content, metadata, workspace_id)) + + if not old_memory_ids or not new_memory_nodes: + self.set_output("No valid memories provided for update.") + return + + # Delete old memories and any existing memories with the same new IDs (upsert behavior) + new_memory_ids: List[str] = [node.memory_id for node in new_memory_nodes] + all_ids_to_delete: List[str] = list(set(old_memory_ids + new_memory_ids)) + await self.vector_store.async_delete(node_ids=all_ids_to_delete, workspace_id=workspace_id) + + # Convert to VectorNodes and insert + vector_nodes = [node.to_vector_node() for node in new_memory_nodes] + await self.vector_store.async_insert(nodes=vector_nodes, workspace_id=workspace_id) + + # Format output message + if len(new_memory_nodes) == 1: + output_msg = ( + f"Successfully updated memory: deleted old (id={old_memory_ids[0]}), " + f"added new (id={new_memory_nodes[0].memory_id}) in workspace={workspace_id}." + ) + else: + old_ids_str = ", ".join(old_memory_ids) + new_ids_str = ", ".join([node.memory_id for node in new_memory_nodes]) + output_msg = ( + f"Successfully updated {len(new_memory_nodes)} memories: " + f"deleted old (ids={old_ids_str}), added new (ids={new_ids_str}) in workspace={workspace_id}." + ) + + self.set_output(output_msg) + diff --git a/reme_ai/v2/tool/update_memory_prompt.yaml b/reme_ai/v2/tool/update_memory_prompt.yaml new file mode 100644 index 00000000..197daf69 --- /dev/null +++ b/reme_ai/v2/tool/update_memory_prompt.yaml @@ -0,0 +1,30 @@ +tool: | + Update a memory in the vector store by replacing the old memory with new content. + Use this tool when the user wants to modify or correct existing information, + or when a memory needs to be updated with new details while keeping its relevance. + Memory ID can be obtained from previous memory retrieval results. + +tool_multiple: | + Update multiple memories in the vector store by replacing old memories with new content. + Use this tool when the user wants to modify or correct multiple pieces of existing information, + or when memories need to be updated with new details while keeping their relevance. + Memory IDs can be obtained from previous memory retrieval results. + +memory_id: | + The unique identifier (memory_id) of the old memory to be replaced. + +memory_content: | + The new content of the memory to store. + Should be a clear, concise statement that captures the updated information to remember. + +metadata: | + Optional metadata for the new memory, including time or other important information. Can include: + - time: The timestamp or date associated with the memory. + - Any other custom key-value pairs relevant to the memory. + +memories: | + A list of memory update objects. Each object should contain: + - memory_id (required): The unique identifier of the old memory to be replaced. + - memory_content (required): The new content of the memory to store. + - metadata (optional): Additional metadata for the new memory. + diff --git a/reme_ai/v2/tool/vector_retrieve_memory_op.py b/reme_ai/v2/tool/vector_retrieve_memory_op.py new file mode 100644 index 00000000..ae1b91bf --- /dev/null +++ b/reme_ai/v2/tool/vector_retrieve_memory_op.py @@ -0,0 +1,223 @@ +"""Vector retrieve memory operation for searching memories from the memory store. + +This module provides the VectorRetrieveMemoryOp class for retrieving memories +by query texts using vector similarity search. +""" + +from typing import Dict, List + +from flowllm.core.schema import VectorNode + +from .base_memory_tool_op import BaseMemoryToolOp +from .. import C +from ..enumeration import MemoryType +from ..schema import MemoryNode + + +@C.register_op() +class VectorRetrieveMemoryOp(BaseMemoryToolOp): + """Operation for retrieving memories from the memory store. + + This operation supports both single and multiple query modes, + controlled by the `enable_multiple` parameter inherited from BaseMemoryToolOp. + + When `add_memory_type_target` is False, memory_type and memory_target are not + included in the tool call schema, and will be retrieved from context instead. + """ + + def __init__( + self, + enable_summary_memory: bool = False, + add_memory_type_target: bool = False, + top_k: int = 20, + **kwargs + ): + super().__init__(**kwargs) + self.enable_summary_memory: bool = enable_summary_memory + self.add_memory_type_target: bool = add_memory_type_target + self.top_k: int = C.service_config.metadata.get("top_k", top_k) + + def build_input_schema(self) -> dict: + """Build input schema for single query mode. + + Returns: + dict: Input schema for retrieving memories. When add_memory_type_target is True, + includes memory_type, memory_target, and query. Otherwise only query. + """ + schema = { + "query": { + "type": "string", + "description": self.get_prompt("query"), + "required": True, + }, + } + + if self.add_memory_type_target: + memory_type_target_schema = { + "memory_type": { + "type": "string", + "description": self.get_prompt("memory_type"), + "enum": [ + MemoryType.IDENTITY.value, + MemoryType.PERSONAL.value, + MemoryType.PROCEDURAL.value, + ], + "required": True, + }, + "memory_target": { + "type": "string", + "description": self.get_prompt("memory_target"), + "required": True, + }, + } + schema = {**memory_type_target_schema, **schema} + + return schema + + def build_multiple_input_schema(self) -> dict: + """Build input schema for multiple query mode. + + Returns: + dict: Input schema for retrieving memories with list of query items. + When add_memory_type_target is True, each item includes memory_type, + memory_target, and query. Otherwise only query. + """ + # Build item properties by combining schemas + item_properties = {} + item_required = [] + + if self.add_memory_type_target: + # Add memory_type and memory_target schemas + item_properties["memory_type"] = { + "type": "string", + "description": self.get_prompt("memory_type"), + "enum": [ + MemoryType.IDENTITY.value, + MemoryType.PERSONAL.value, + MemoryType.PROCEDURAL.value, + ], + } + item_properties["memory_target"] = { + "type": "string", + "description": self.get_prompt("memory_target"), + } + item_required.extend(["memory_type", "memory_target"]) + + # Add query schema + item_properties["query"] = { + "type": "string", + "description": self.get_prompt("query"), + } + item_required.append("query") + + return { + "query_items": { + "type": "array", + "description": self.get_prompt("query_items"), + "required": True, + "items": { + "type": "object", + "properties": item_properties, + "required": item_required, + }, + } + } + + async def retrieve_by_query( + self, query: str, memory_type: str, memory_target: str, workspace_id: str + ) -> List[MemoryNode]: + """Retrieve memories by query using vector similarity search. + + Args: + query: The query string for similarity search. + memory_type: The type of memory to search for. + memory_target: The target of memory to search for. + workspace_id: The workspace ID to search in. + + Returns: + List[MemoryNode]: List of matching memories. + """ + # Build memory type list, including summary if enabled + memory_type_list = [MemoryType(memory_type)] + if self.enable_summary_memory: + memory_type_list.append(MemoryType.SUMMARY) + + nodes: List[VectorNode] = await self.vector_store.async_search( + query=query, + workspace_id=workspace_id, + top_k=self.top_k, + filter_dict={ + "metadata.memory_type": memory_type_list, + "metadata.memory_target": [memory_target], + }, + ) + memory_nodes: List[MemoryNode] = [MemoryNode.from_vector_node(n) for n in nodes] + filtered_memory_nodes = [] + for memory_node in memory_nodes: + # For TOOL type memories, only keep those where when_to_use matches the query (tool name) + if memory_node.memory_type == MemoryType.TOOL and memory_node.when_to_use != query: + continue + filtered_memory_nodes.append(memory_node) + + return filtered_memory_nodes + + + + async def async_execute(self): + """Execute the retrieve memory operation. + + Retrieves memories from the memory store based on query texts. + The operation handles both single query and multiple queries inputs. + + When add_memory_type_target is False, memory_type and memory_target + are retrieved from self.context instead of from each query item. + """ + workspace_id: str = self.workspace_id + + # Get default memory_type and memory_target from input_dict + default_memory_type: str = self.context.get("memory_type", "") + default_memory_target: str = self.context.get("memory_target", "") + + # Get query items based on mode + if self.enable_multiple: + query_items: List[dict] = self.context.get("query_items", []) + else: + query_items: List[dict] = [ + { + "memory_type": default_memory_type, + "memory_target": default_memory_target, + "query": self.context.get("query", ""), + } + ] + + # Filter out items with empty query + query_items = [item for item in query_items if item.get("query")] + + if not query_items: + self.set_output("No valid query texts provided for retrieval.") + return + + # Perform retrieval for all query items + memories: List[MemoryNode] = [] + for item in query_items: + # Use item's memory_type/memory_target if available, + # otherwise fall back to default values from input_dict + memory_type = item.get("memory_type") or default_memory_type + memory_target = item.get("memory_target") or default_memory_target + + retrieved = await self.retrieve_by_query( + query=item["query"], + memory_type=memory_type, + memory_target=memory_target, + workspace_id=workspace_id, + ) + memories.extend(retrieved) + + # Deduplicate and format output + memories = self._deduplicate_memories(memories) + + if not memories: + output = f"No memories found in workspace={workspace_id}." + else: + output = "\n".join([m.format_memory() for m in memories]) + self.set_output(output) diff --git a/reme_ai/v2/tool/vector_retrieve_memory_prompt.yaml b/reme_ai/v2/tool/vector_retrieve_memory_prompt.yaml new file mode 100644 index 00000000..91e759eb --- /dev/null +++ b/reme_ai/v2/tool/vector_retrieve_memory_prompt.yaml @@ -0,0 +1,23 @@ +tool: | + Retrieve a memory from the memory store using vector similarity search. + Use this tool to find relevant memories based on semantic similarity to the query. + The search returns the most relevant memories ranked by similarity score. + +tool_multiple: | + Retrieve memories from the memory store using vector similarity search. + Use this tool to find relevant memories based on semantic similarity to the queries. + The search returns the most relevant memories ranked by similarity score. + +memory_type: | + The type of memory to search for. Must be one of: "identity", "personal", "procedural", "tool". + +memory_target: | + The target of the memory. For example, in personal memory, this would be the person's name. + +query: | + The query text for vector similarity search. + Use descriptive queries that capture the semantic meaning of what you're looking for. + +query_items: | + A list of query items for vector similarity search. + Each item should contain memory_type, memory_target, and query fields. diff --git a/reme_ai/v2/utils/__init__.py b/reme_ai/v2/utils/__init__.py new file mode 100644 index 00000000..5ea525ea --- /dev/null +++ b/reme_ai/v2/utils/__init__.py @@ -0,0 +1,7 @@ +from .common_utils import get_now_time, format_messages, deduplicate_memories + +__all__ = [ + "get_now_time", + "format_messages", + "deduplicate_memories", +] diff --git a/reme_ai/v2/utils/common_utils.py b/reme_ai/v2/utils/common_utils.py new file mode 100644 index 00000000..7032b9cb --- /dev/null +++ b/reme_ai/v2/utils/common_utils.py @@ -0,0 +1,29 @@ +import datetime +from typing import List, Dict + +from ..enumeration import Role +from ..schema import Message, MemoryNode + + +def get_now_time() -> str: + return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def format_messages(messages: List[dict | Message]) -> str: + messages = [Message(**x) if isinstance(x, dict) else x for x in messages] + messages = [x for x in messages if x.role is not Role.SYSTEM] + messages_context = "\n".join([x.format_message( + add_time_created=True, + use_name_first=True, + add_reasoning_content=True, + add_tool_calls=True, + ) for x in messages]) + return messages_context + + +def deduplicate_memories(memories: List[MemoryNode]) -> List[MemoryNode]: + seen_memories: Dict[str, MemoryNode] = {} + for memory in memories: + if memory.memory_id not in seen_memories: + seen_memories[memory.memory_id] = memory + return list(seen_memories.values()) diff --git a/test_op/test_reme_v2.py b/test_op/test_reme_v2.py new file mode 100644 index 00000000..a973652a --- /dev/null +++ b/test_op/test_reme_v2.py @@ -0,0 +1,72 @@ +import asyncio + +from reme_ai import ReMeApp +from reme_ai.core.agent.v1 import ReMeSummaryAgentV1Op +from reme_ai.core.tool import AddMetaMemoryOp, AddSummaryMemoryOp +from reme_ai.core.tool.hands_off_op import HandsOffOp + + +async def test_summary_and_retrieve(): + """Test summary agent first, then retrieve agent.""" + + + messages = [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "My name is Alice and I love programming in Python." + }, + { + "role": "assistant", + "content": "Nice to meet you, Alice! Python is a great programming language." + }, + { + "role": "user", + "content": "I also enjoy machine learning and deep learning." + }, + { + "role": "assistant", + "content": "That's wonderful! Machine learning and deep learning are fascinating fields." + }, + ] + + async with ReMeApp( + "llm.default.model_name=qwen3-max", + "vector_store.default.backend=local", + ) as _: + # app.service_config.llm["default"].model_name = "qwen3-max" + # app.service_config.vector_store["default"].backend = "local" + + # Step 1: Summary agent to summarize the conversation + summary_agent = ReMeSummaryAgentV1Op() << [ + AddMetaMemoryOp(), + AddSummaryMemoryOp(), + HandsOffOp(), + ] + await summary_agent.async_call( + messages=messages, + workspace_id="test_workspace", + ) + print(f"Summary result: {summary_agent.output}") + + # # Step 2: Retrieve agent to retrieve relevant memories + # retrieve_agent = ReMeRetrieveAgentV1Op() << [ + # RetrieveMemoryOp(enable_summary_memory=True, top_k=10), + # ReadHistoryMemoryOp(), + # ] + # await retrieve_agent.async_call( + # query="What does Alice like?", + # workspace_id="test_workspace", + # memory_target="Alice", + # ) + # print(f"Retrieve result: {retrieve_agent.output}") + + +if __name__ == "__main__": + asyncio.run(test_summary_and_retrieve()) + + + diff --git a/tests/test_base_context.py b/tests/test_base_context.py new file mode 100644 index 00000000..487ac84a --- /dev/null +++ b/tests/test_base_context.py @@ -0,0 +1,67 @@ +import pickle +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from reme_ai.core.context.base_context import BaseContext + + +def test_attribute_access(): + c = BaseContext() + c.xxx = 123 + assert c.xxx == 123 + assert c["xxx"] == 123 + + +def test_dict_access(): + c = BaseContext() + c["yyy"] = 456 + assert c.yyy == 456 + assert c["yyy"] == 456 + + +def test_delete_attribute(): + c = BaseContext() + c.zzz = 789 + del c.zzz + assert "zzz" not in c + + +def test_attribute_error(): + c = BaseContext() + try: + _ = c.nonexistent + assert False, "Should raise AttributeError" + except AttributeError as e: + assert "nonexistent" in str(e) + + +def test_pickling(): + c = BaseContext() + c.foo = "bar" + c.num = 42 + + pickled = pickle.dumps(c) + restored = pickle.loads(pickled) + + assert restored.foo == "bar" + assert restored.num == 42 + assert isinstance(restored, BaseContext) + + +def test_init_with_data(): + c = BaseContext({"a": 1, "b": 2}) + assert c.a == 1 + assert c.b == 2 + + +if __name__ == "__main__": + test_attribute_access() + test_dict_access() + test_delete_attribute() + test_attribute_error() + test_pickling() + test_init_with_data() + print("All tests passed!") +