diff --git a/README.md b/README.md index 7b5e4061..9d18e6ce 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

License English - 简体中文 + Simplified Chinese GitHub Stars

@@ -20,66 +20,65 @@ A memory management toolkit for AI agents — Remember Me, Refine Me.

-> For legacy versions, see [0.2.x Documentation](docs/README_0_2_x.md) +> For the older version, please refer to the [0.2.x documentation](docs/README_0_2_x_ZH.md). --- -🧠 ReMe is a **memory management framework** built for **AI agents**, offering both **file-based** and **vector-based** -memory systems. +🧠 ReMe is a memory management framework designed for **AI agents**, providing both file-based and vector-based memory +systems. -It addresses two core problems of agent memory: **limited context windows** (early information gets truncated or lost -during long conversations) and **stateless sessions** (new conversations cannot inherit history and always start from -scratch). - -ReMe gives agents **real memory** — old conversations are automatically condensed, important information is persisted, -and the next conversation can recall it automatically. +It tackles two core problems of agent memory: **limited context window** (early information is truncated or lost in long +conversations) and **stateless sessions** (new sessions cannot inherit history and always start from scratch). +ReMe gives agents **real memory** — old conversations are automatically compacted, important information is persistently +stored, and relevant context is automatically recalled in future interactions. --- -## 📁 File-Based Memory System (ReMeLight) +## 📁 File-based memory system (ReMeLight) -> Memory as files, files as memory +> Memory as files, files as memory. -Treat **memory as files** — readable, editable, and portable. -[CoPaw](https://github.com/agentscope-ai/CoPaw) implements long-term memory and context management by inheriting +Treat **memory as files** — readable, editable, and copyable. +[CoPaw](https://github.com/agentscope-ai/CoPaw) integrates long-term memory and context management by inheriting from `ReMeLight`. -| Traditional Memory Systems | File-Based ReMe | -|----------------------------|--------------------| -| 🗄️ Database storage | 📝 Markdown files | -| 🔒 Opaque | 👀 Read anytime | -| ❌ Hard to modify | ✏️ Edit directly | -| 🚫 Hard to migrate | 📦 Copy to migrate | +| Traditional memory system | File-based ReMe | +|---------------------------|----------------------| +| 🗄️ Database storage | 📝 Markdown files | +| 🔒 Opaque | 👀 Always readable | +| ❌ Hard to modify | ✏️ Directly editable | +| 🚫 Hard to migrate | 📦 Copy to migrate | ``` working_dir/ -├── MEMORY.md # Long-term memory: user preferences, project config, etc. +├── MEMORY.md # Long-term memory: persistent info such as user preferences ├── memory/ -│ └── YYYY-MM-DD.md # Daily summary logs: written automatically after conversation ends -└── tool_result/ # Cache for oversized tool outputs (auto-managed, auto-cleaned when expired) +│ └── YYYY-MM-DD.md # Daily journal: automatically written after each conversation +└── tool_result/ # Cache for long tool outputs (auto-managed, expired entries auto-cleaned) └── .txt ``` -### Core Capabilities +### Core capabilities -[ReMeLight](reme/reme_light.py) is the core class of this memory system, providing complete memory management -capabilities for AI Agents: +[ReMeLight](reme/reme_light.py) is the core class of the file-based memory system. It provides full memory management +capabilities for AI agents: -| Method | Function | Key Components | -|------------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `start` | 🚀 Start memory system | Initialize file store, file watcher, Embedding cache; clean up expired tool result files | -| `close` | 📕 Close and clean up | Clean tool result files, stop file watcher, save Embedding cache | -| `compact_memory` | 📦 Compact history to summary | [Compactor](reme/memory/file_based/compactor.py) — ReActAgent generates structured context checkpoint | -| `summary_memory` | 📝 Write important memory to files | [Summarizer](reme/memory/file_based/summarizer.py) — ReActAgent + file tools (read / write / edit) | -| `compact_tool_result` | ✂️ Compact oversized tool output | [ToolResultCompactor](reme/memory/file_based/tool_result_compactor.py) — Truncate and save to `tool_result/`, keep file reference in message | -| `pre_reasoning_hook` | 🔄 Pre-reasoning hook | Auto compact tool results + generate summary + async trigger memory summarization task | -| `memory_search` | 🔍 Semantic memory search | [MemorySearch](reme/memory/tools/chunk/memory_search.py) — Vector + BM25 hybrid retrieval | -| `get_in_memory_memory` | 🗂️ Create in-memory instance | [ReMeInMemoryMemory](reme/memory/file_based/reme_in_memory_memory.py) — Token-aware memory management, supports compression summary and state serialization (static method) | +| Method | Function | Key components | +|------------------------|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `check_context` | 📊 Check context size | [ContextChecker](reme/memory/file_based/component/context_checker.py) — checks whether context exceeds thresholds and splits messages | +| `compact_memory` | 📦 Compact history into summary | [Compactor](reme/memory/file_based/component/compactor.py) — ReActAgent that generates structured context summaries | +| `summary_memory` | 📝 Persist important memory to files | [Summarizer](reme/memory/file_based/component/summarizer.py) — ReActAgent + file tools (`read` / `write` / `edit`) | +| `compact_tool_result` | ✂️ Compact long tool outputs | [ToolResultCompactor](reme/memory/file_based/component/tool_result_compactor.py) — truncates long tool outputs and stores them in `tool_result/` while keeping file references in messages | +| `memory_search` | 🔍 Semantic memory search | [MemorySearch](reme/memory/file_based/tools/memory_search.py) — hybrid retrieval with vectors + BM25 | +| `ReMeInMemoryMemory` | 🗂️ In-session memory class | [ReMeInMemoryMemory](reme/memory/file_based/reme_in_memory_memory.py) — token-aware memory management with summary compression and state serialization | +| `pre_reasoning_hook` | 🔄 Pre-reasoning hook | `compact_tool_result` + `check_context` + `compact_memory` + `summary_memory` (async) | +| `start` | 🚀 Start memory system | Initialize file storage, file watcher, and embedding cache; clean up expired tool result files | +| `close` | 📕 Shutdown and cleanup | Clean up tool result files, stop file watcher, and persist embedding cache | --- -### 🚀 Quick Start +### 🚀 Quick start #### Installation @@ -87,23 +86,22 @@ capabilities for AI Agents: pip install -e ".[light]" ``` -#### Environment Variables +#### Environment variables -`ReMeLight` environment variables configure Embedding and storage backend +`ReMeLight` uses environment variables to configure the embedding model and storage backends: -| Variable | Description | Example | -|----------------------|--------------------------------|-----------------------------------------------------| -| `LLM_API_KEY` | LLM API key | `sk-xxx` | -| `LLM_BASE_URL` | LLM base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` | -| `EMBEDDING_API_KEY` | Embedding API key (Optional) | `sk-xxx` | -| `EMBEDDING_BASE_URL` | Embedding base URL (Optional) | `https://dashscope.aliyuncs.com/compatible-mode/v1` | +| Variable | Description | Example | +|----------------------|-------------------------------|-----------------------------------------------------| +| `LLM_API_KEY` | LLM API key | `sk-xxx` | +| `LLM_BASE_URL` | LLM base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` | +| `EMBEDDING_API_KEY` | Embedding API key (optional) | `sk-xxx` | +| `EMBEDDING_BASE_URL` | Embedding base URL (optional) | `https://dashscope.aliyuncs.com/compatible-mode/v1` | -#### Python Usage +#### Python usage ```python import asyncio -from agentscope.message import Msg from reme.reme_light import ReMeLight @@ -116,24 +114,24 @@ async def main(): ) await reme.start() - messages = [...] # Conversation message list + messages = [...] # List of conversation messages - # 1. Compact oversized tool outputs (prevent tool results from overflowing context) + # 1. Compact long tool outputs (prevent tool results from blowing up context) messages = await reme.compact_tool_result(messages) - # 2. Compact history to structured summary (can pass previous summary for incremental update) + # 2. Compact conversation history into a structured summary summary = await reme.compact_memory( messages=messages, previous_summary="", max_input_length=128000, # Model context window (tokens) - compact_ratio=0.7, # Trigger compaction when reaching max_input_length * 0.7 - language="zh", # Summary language (zh / "") + compact_ratio=0.7, # Trigger compaction when exceeding max_input_length * 0.7 + language="zh", # Summary language (e.g., "zh" / "") ) - # 3. Submit async summary task in background (non-blocking, writes to memory/YYYY-MM-DD.md) + # 3. Submit summary task asynchronously (non-blocking, writes to memory/YYYY-MM-DD.md) reme.add_async_summary_task(messages=messages) - # 4. Pre-reasoning hook (auto compact tool results + generate summary) + # 4. Pre-reasoning hook (auto compact tool results + generate summaries) processed_messages, compressed_summary = await reme.pre_reasoning_hook( messages=messages, system_prompt="You are a helpful AI assistant.", @@ -145,22 +143,23 @@ async def main(): tool_result_compact_keep_n=3, ) - # 5. Semantic memory search (Vector + BM25 hybrid retrieval) + # 5. Semantic memory search (vector + BM25 hybrid retrieval) result = await reme.memory_search(query="Python version preference", max_results=5) - # 6. Get in-memory instance (static method, manages single conversation context) - memory = ReMeLight.get_in_memory_memory() + # 6. Create in-session memory instance (manages context for one conversation) + from reme.memory.file_based.reme_in_memory_memory import ReMeInMemoryMemory + memory = ReMeInMemoryMemory() for msg in messages: await memory.add(msg) token_stats = await memory.estimate_tokens(max_input_length=128000) print(f"Current context usage: {token_stats['context_usage_ratio']:.1f}%") - print(f"Message tokens: {token_stats['messages_tokens']}") + print(f"Message token count: {token_stats['messages_tokens']}") print(f"Estimated total tokens: {token_stats['estimated_tokens']}") - # 7. Wait for background tasks before closing + # 7. Wait for background summary tasks to complete before shutdown summary_result = await reme.await_summary_tasks() - # Close ReMeLight + # Shutdown ReMeLight await reme.close() @@ -168,173 +167,226 @@ if __name__ == "__main__": asyncio.run(main()) ``` -> 📂 Full example code: [test_reme_light.py](tests/light/test_reme_light.py) -> 📋 Example output: [test_reme_light.log](tests/light/test_reme_light_log.txt) (223,838 tokens → 1,105 tokens, 99.5% -> compression ratio) +> 📂 Full example: [test_reme_light.py](tests/light/test_reme_light.py) +> 📋 Sample run log: [test_reme_light_log.txt](tests/light/test_reme_light_log.txt) (223,838 tokens → 1,105 tokens, 99.5% +> compression) -### File-Based ReMeLight Memory System Architecture +### Architecture of the file-based ReMeLight memory system [CoPaw MemoryManager](https://github.com/agentscope-ai/CoPaw/blob/main/src/copaw/agents/memory/memory_manager.py) -inherits `ReMeLight` and integrates memory capabilities into the Agent reasoning flow: - -```mermaid -graph TB - CoPaw["CoPaw MemoryManager
(inherits ReMeLight)"] -->|pre_reasoning hook| Hook[MemoryCompactionHook] - CoPaw --> ReMeLight[ReMeLight] - Hook -->|exceeds threshold| ReMeLight - ReMeLight --> CompactMemory[compact_memory
History compaction] - ReMeLight --> SummaryMemory[summary_memory
Write memory to files] - ReMeLight --> CompactToolResult[compact_tool_result
Oversized tool output compaction] - ReMeLight --> MemSearch[memory_search
Semantic search] - ReMeLight --> InMemory[get_in_memory_memory
ReMeInMemoryMemory] - CompactMemory --> Compactor[Compactor
ReActAgent] - SummaryMemory --> Summarizer[Summarizer
ReActAgent + file tools] - CompactToolResult --> ToolResultCompactor[ToolResultCompactor
Truncate + save to file] - Summarizer --> FileIO[FileIO
read / write / edit] - FileIO --> MemoryFiles[memory/YYYY-MM-DD.md] - ToolResultCompactor --> ToolResultFiles[tool_result/*.txt] - MemoryFiles -.->|File change| FileWatcher[Async File Watcher] - FileWatcher -->|Update index| FileStore[Local DB] - MemSearch --> FileStore -``` - -### Context Compaction Mechanism - -#### Context Compaction - -[Compactor](reme/memory/file_based/compactor.py) uses ReActAgent to compact history into structured **context -checkpoints**: - -| Field | Description | -|-----------------------|-----------------------------------------------------| -| `## Goal` | 🎯 User's objectives (can be multiple) | -| `## Constraints` | ⚙️ Constraints and preferences mentioned by user | -| `## Progress` | 📈 Completed / in progress / blocked tasks | -| `## Key Decisions` | 🔑 Decisions made with brief reasons | -| `## Next Steps` | 🗺️ Next action plan (ordered list) | -| `## Critical Context` | 📌 File paths, function names, error messages, etc. | - -Supports **incremental updates**: when `previous_summary` is passed, automatically merges new conversation with old -summary, preserving historical progress. - -#### Tool Result Compaction - -[ToolResultCompactor](reme/memory/file_based/tool_result_compactor.py) solves context overflow caused by oversized tool -outputs (e.g., browser use): +inherits +`ReMeLight` and integrates its memory capabilities into the agent reasoning loop: ```mermaid graph LR - A[tool_result message] --> B{Content length > threshold?} - B -->|No| C[Keep as-is] - B -->|Yes| D[Truncate to threshold characters] - D --> E[Write full content to tool_result/uuid.txt] - E --> F[Append file reference path to message] -``` - -Expired files (exceeding `retention_days`) are automatically cleaned up during `start` / `close` / -`compact_tool_result`. - -### Memory Summary: ReAct + File Tools - -[Summarizer](reme/memory/file_based/summarizer.py) uses the **ReAct + file tools** pattern, letting AI autonomously -decide what to write and where: - -```mermaid -graph LR - A[Receive conversation] --> B{Think: What's worth recording?} - B --> C[Act: read memory/YYYY-MM-DD.md] - C --> D{Think: How to merge with existing content?} - D --> E[Act: edit to update file] - E --> F{Think: Anything missing?} - F -->|Yes| B - F -->|No| G[Done] -``` - -[FileIO](reme/memory/file_based/file_io.py) provides file operation tools: - -| Tool | Function | Use case | -|---------|--------------------------------|-----------------------------------------| -| `read` | Read file content (line range) | View existing memory, avoid duplicates | -| `write` | Overwrite file | Create new memory file or major rewrite | -| `edit` | Replace after exact match | Append or modify specific sections | - -### In-Memory Session Management - -[ReMeInMemoryMemory](reme/memory/file_based/reme_in_memory_memory.py) extends AgentScope's `InMemoryMemory`: - -| Feature | Description | -|----------------------------------|---------------------------------------------------------------------| -| `get_memory` | Filter messages by mark, auto-prepend compression summary | -| `estimate_tokens` | Precisely estimate current context token usage and ratio | -| `get_history_str` | Generate human-readable conversation summary (with token stats) | -| `state_dict` / `load_state_dict` | Support state serialization / deserialization (session persistence) | -| `mark_messages_compressed` | Mark messages as compressed state | -| `get_compressed_summary` | Get compressed summary content | - -### Memory Retrieval - -[MemorySearch](reme/memory/tools/chunk/memory_search.py) provides **vector + BM25 hybrid retrieval**: - -| Retrieval | Strength | Weakness | -|---------------------|-------------------------------------------------|----------------------------------------| -| **Vector semantic** | Captures similar meaning with different wording | Weaker on exact token match | -| **BM25 full-text** | Strong exact token match | No synonym or paraphrase understanding | - -**Fusion**: Both retrieval paths are weighted and summed (vector 0.7 + BM25 0.3), so both natural-language queries and -exact lookups get reliable results. - -```mermaid -graph LR - Q[Search query] --> V[Vector search × 0.7] -Q --> B[BM25 × 0.3] -V --> M[Dedupe + weighted merge] -B --> M -M --> R[Top-N results] + Agent[Agent] -->|Before each reasoning step| Hook[pre_reasoning_hook] + Hook --> TC[compact_tool_result
Compact tool outputs] + TC --> CC[check_context
Token counting] + CC -->|Exceeds limit| CM[compact_memory
Generate summary] + CC -->|Exceeds limit| SM[summary_memory
Async persistence] + SM -->|ReAct + FileIO| Files[memory/*.md] + Agent -->|Explicit call| Search[memory_search
Vector+BM25] + Agent -->|In-session| InMem[ReMeInMemoryMemory
Token-aware memory] + Files -.->|FileWatcher| Store[(FileStore
Vector+FTS index)] + Search --> Store ``` --- -## 🗃️ Vector-Based Memory System +#### 1. `check_context` — context checking -[ReMe Vector Based](reme/reme.py) is the core class for the vector-based memory system, supporting unified management of -three memory types: +[ContextChecker](reme/memory/file_based/component/context_checker.py) uses token counting to determine whether the +context exceeds thresholds and automatically splits messages into a "to compact" group and a "to keep" group. -| Memory Type | Purpose | Usage Context | -|------------------------------|-----------------------------------------------------|---------------| -| **Personal memory** | User preferences, habits | `user_name` | -| **Task / procedural memory** | Task execution experience, success/failure patterns | `task_name` | -| **Tool memory** | Tool usage experience, parameter tuning | `tool_name` | - -### Core Capabilities - -| Method | Function | Description | -|--------------------|---------------------|-----------------------------------------------------------| -| `summarize_memory` | 🧠 Summarize memory | Automatically extract and store memory from conversations | -| `retrieve_memory` | 🔍 Retrieve memory | Retrieve relevant memory by query | -| `add_memory` | ➕ Add memory | Manually add memory to vector store | -| `get_memory` | 📖 Get memory | Fetch a single memory by ID | -| `update_memory` | ✏️ Update memory | Update content or metadata of existing memory | -| `delete_memory` | 🗑️ Delete memory | Delete specified memory | -| `list_memory` | 📋 List memory | List memories with filtering and sorting | - -### Installation - -```bash -pip install -U reme-ai +```mermaid +graph LR + M[messages] --> H[AsMsgHandler
Token counting] + H --> C{total > threshold?} + C -->|No| K[Return all messages] + C -->|Yes| S[Keep from tail
reserve tokens] + S --> CP[messages_to_compact
Earlier messages] + S --> KP[messages_to_keep
Recent messages] + S --> V{is_valid
Tool calls aligned?} ``` -### Environment Variables +- **Core logic**: keep `reserve` tokens from the tail; mark the rest as messages to compact. +- **Integrity guarantee**: preserves complete user-assistant turns and tool_use/tool_result pairs without splitting + them. -API keys are set via environment variables; you can put them in a `.env` file in the project root: +--- -| Variable | Description | Example | -|----------------------|--------------------|-----------------------------------------------------| -| `LLM_API_KEY` | LLM API Key | `sk-xxx` | -| `LLM_BASE_URL` | LLM Base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` | -| `EMBEDDING_API_KEY` | Embedding API Key | `sk-xxx` | -| `EMBEDDING_BASE_URL` | Embedding Base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` | +#### 2. `compact_memory` — conversation compaction -### Python Usage +[Compactor](reme/memory/file_based/component/compactor.py) uses a ReActAgent to compact conversation history into a * +*structured context summary**. + +```mermaid +graph LR + M[messages] --> H[AsMsgHandler
format_msgs_to_str] + H --> A[ReActAgent
reme_compactor] + P[previous_summary] -->|Incremental update| A + A --> S[Structured summary
Goal/Progress/Decisions...] +``` + +**Summary structure** (context checkpoints): + +| Field | Description | +|-----------------------|------------------------------------------------------------------------| +| `## Goal` | User goals | +| `## Constraints` | Constraints and preferences | +| `## Progress` | Task progress | +| `## Key Decisions` | Key decisions | +| `## Next Steps` | Next step plans | +| `## Critical Context` | Critical data such as file paths, function names, error messages, etc. | + +- **Incremental updates**: when `previous_summary` is provided, new conversations are merged into the existing summary. + +--- + +#### 3. `summary_memory` — persistent memory + +[Summarizer](reme/memory/file_based/component/summarizer.py) uses a **ReAct + file tools** pattern so that the AI can +decide what to write and where to write it. + +```mermaid +graph LR + M[messages] --> A[ReActAgent
reme_summarizer] + A -->|read| R[Read memory/YYYY-MM-DD.md] + R --> T{Reason: how to merge?} + T -->|write| W[Overwrite] + T -->|edit| E[Edit in place] + W --> F[memory/YYYY-MM-DD.md] + E --> F +``` + +**File tools** ([FileIO](reme/memory/file_based/tools/file_io.py)): + +| Tool | Function | +|---------|-----------------------| +| `read` | Read file content | +| `write` | Overwrite file | +| `edit` | Find-and-replace edit | + +--- + +#### 4. `compact_tool_result` — tool result compaction + +[ToolResultCompactor](reme/memory/file_based/component/tool_result_compactor.py) addresses the problem of long tool +outputs bloating the context. + +```mermaid +graph LR + M[messages] --> L{Iterate tool_result
len > threshold?} + L -->|No| K[Keep as-is] + L -->|Yes| T[truncate_text
Truncate to threshold] + T --> S[Write full content
tool_result/uuid.txt] + S --> R[Append file path reference
to message] + R --> C[cleanup_expired_files
Delete expired files] +``` + +- **Auto cleanup**: expired files (older than `retention_days`) are deleted automatically during `start` / `close` / + `compact_tool_result`. + +--- + +#### 5. `memory_search` — memory retrieval + +[MemorySearch](reme/memory/file_based/tools/memory_search.py) provides **vector + BM25 hybrid retrieval**. + +```mermaid +graph LR + Q[query] --> E[Embedding
Vectorization] + E --> V[vector_search
Semantic similarity] + Q --> B[BM25
Keyword matching] + V -->|" weight: 0.7 "| M[Deduplicate + weighted merge] + B -->|" weight: 0.3 "| M + M --> F[min_score filter] + F --> R[Top-N results] +``` + +- **Fusion mechanism**: vector weight 0.7 + BM25 weight 0.3 — balancing semantic similarity and exact matches. + +--- + +#### 6. `ReMeInMemoryMemory` — in-session memory + +[ReMeInMemoryMemory](reme/memory/file_based/reme_in_memory_memory.py) extends AgentScope's `InMemoryMemory` to provide +token-aware memory management. + +```mermaid +graph LR + C[content] --> G[get_memory
exclude_mark=COMPRESSED] + G --> F[Filter out compressed messages] + F --> P{prepend_summary?} + P -->|Yes| S[Prepend previous summary] + S --> O[Output messages] + P -->|No| O +``` + +| Function | Description | +|----------------------------------|---------------------------------------------------| +| `get_memory` | Filter messages by mark and auto-append summary | +| `estimate_tokens` | Estimate token usage of the context | +| `state_dict` / `load_state_dict` | Serialize/deserialize state (session persistence) | + +--- + +#### 7. `pre_reasoning_hook` — pre-reasoning processing + +This is a unified entry point that wires all the above components together and automatically manages context before each +reasoning step. + +```mermaid +graph LR + M[messages] --> TC[compact_tool_result
Compact long tool outputs] + TC --> CC[check_context
Compute remaining space] + CC --> D{messages_to_compact
Non-empty?} + D -->|No| K[Return original messages + summary] + D -->|Yes| V{is_valid?} + V -->|No| K + V -->|Yes| CM[compact_memory
Sync summary generation] + V -->|Yes| SM[add_async_summary_task
Async persistence] + CM --> R[Return messages_to_keep + new summary] +``` + +**Execution flow**: + +1. `compact_tool_result` — compact long tool outputs. +2. `check_context` — check whether the context exceeds limits. +3. `compact_memory` — generate compact summary (sync). +4. `summary_memory` — persist memory (async in the background). + +--- + +## 🗃️ Vector-based memory system + +[ReMe Vector Based](reme/reme.py) is the core class for the vector-based memory system. It manages three types of +memories: + +| Memory type | Use case | +|-----------------------|-------------------------------------------------------------------| +| **Personal memory** | Records user preferences and habits | +| **Procedural memory** | Records task execution experience and patterns of success/failure | +| **Tool memory** | Records tool usage experience and parameter tuning | + +### Core capabilities + +| Method | Function | Description | +|--------------------|--------------|-------------------------------------------------------------| +| `summarize_memory` | 🧠 Summarize | Automatically extract and store memories from conversations | +| `retrieve_memory` | 🔍 Retrieve | Retrieve related memories based on a query | +| `add_memory` | ➕ Add | Manually add memories into the vector store | +| `get_memory` | 📖 Get | Get a single memory by ID | +| `update_memory` | ✏️ Update | Update existing memory content or metadata | +| `delete_memory` | 🗑️ Delete | Delete a specific memory | +| `list_memory` | 📋 List | List memories with filtering and sorting | + +### Installation and environment variables + +Installation and environment configuration are the same as [ReMeLight](#installation). +API keys are configured via environment variables and can be stored in a `.env` file at the project root. + +### Python usage ```python import asyncio @@ -363,34 +415,34 @@ async def main(): messages = [ {"role": "user", "content": "Help me write a Python script", "time_created": "2026-02-28 10:00:00"}, - {"role": "assistant", "content": "Sure, I'll help you write it", "time_created": "2026-02-28 10:00:05"}, + {"role": "assistant", "content": "Sure, I'll help you with that.", "time_created": "2026-02-28 10:00:05"}, ] - # 1. Summarize memory from conversation (auto-extract user preferences, task experience, etc.) + # 1. Summarize memories from conversation (automatically extract user preferences, task experience, etc.) result = await reme.summarize_memory( messages=messages, user_name="alice", # Personal memory - # task_name="code_writing", # Task memory + # task_name="code_writing", # Procedural memory ) - print(f"Summarize result: {result}") + print(f"Summary result: {result}") - # 2. Retrieve relevant memory + # 2. Retrieve related memories memories = await reme.retrieve_memory( query="Python programming", user_name="alice", # task_name="code_writing", ) - print(f"Retrieve result: {memories}") + print(f"Retrieved memories: {memories}") - # 3. Manually add memory + # 3. Manually add a memory memory_node = await reme.add_memory( - memory_content="User prefers concise code style", + memory_content="The user prefers concise code style.", user_name="alice", ) print(f"Added memory: {memory_node}") memory_id = memory_node.memory_id - # 4. Get single memory by ID + # 4. Get a single memory by ID fetched_memory = await reme.get_memory(memory_id=memory_id) print(f"Fetched memory: {fetched_memory}") @@ -398,11 +450,11 @@ async def main(): updated_memory = await reme.update_memory( memory_id=memory_id, user_name="alice", - memory_content="User prefers concise, well-commented code style", + memory_content="The user prefers concise code with comments.", ) print(f"Updated memory: {updated_memory}") - # 6. List all memories for user (with filtering and sorting) + # 6. List all memories for the user (supports filtering and sorting) all_memories = await reme.list_memory( user_name="alice", limit=10, @@ -411,11 +463,11 @@ async def main(): ) print(f"User memory list: {all_memories}") - # 7. Delete specified memory + # 7. Delete a specific memory await reme.delete_memory(memory_id=memory_id) print(f"Deleted memory: {memory_id}") - # 8. Delete all memories (use with caution) + # 8. Delete all memories (use with care) # await reme.delete_all() await reme.close() @@ -425,21 +477,21 @@ if __name__ == "__main__": asyncio.run(main()) ``` -### Technical Architecture +### Technical architecture ```mermaid -graph TB +graph LR User[User / Agent] --> ReMe[Vector Based ReMe] - ReMe --> Summarize[Memory Summarize] - ReMe --> Retrieve[Memory Retrieve] - ReMe --> CRUD[CRUD] + ReMe --> Summarize[Summarize memories] + ReMe --> Retrieve[Retrieve memories] + ReMe --> CRUD[CRUD operations] Summarize --> PersonalSum[PersonalSummarizer] Summarize --> ProceduralSum[ProceduralSummarizer] Summarize --> ToolSum[ToolSummarizer] Retrieve --> PersonalRet[PersonalRetriever] Retrieve --> ProceduralRet[ProceduralRetriever] Retrieve --> ToolRet[ToolRetriever] - PersonalSum --> VectorStore[Vector DB] + PersonalSum --> VectorStore[Vector database] ProceduralSum --> VectorStore ToolSum --> VectorStore PersonalRet --> VectorStore @@ -447,17 +499,53 @@ graph TB ToolRet --> VectorStore ``` -## ⭐ Community & Support +### Experimental results -- **Star & Watch**: Star helps more agent developers discover ReMe; Watch keeps you updated on new releases and - features. -- **Share your work**: In Issues or Discussions, share what ReMe unlocks for your agents — we're happy to highlight - great community examples. -- **Need a new feature?** Open a Feature Request; we'll iterate with the community. -- **Code contributions**: All forms of code contribution are welcome. See - the [Contribution Guide](docs/contribution.md). -- **Acknowledgments**: Thanks to OpenClaw, Mem0, MemU, CoPaw, and other open-source projects for inspiration and - support. +Coming soon... + +--- + +## 🧪 Procedural memory paper + +> Our procedural (task) memory paper is available on [arXiv](https://arxiv.org/abs/2512.10696). + +### 🌍 [Appworld benchmark](benchmark/appworld/quickstart.md) + +We evaluate ReMe on the Appworld environment using Qwen3-8B (non-thinking mode): + +| Method | Avg@4 | Pass@4 | +|----------|---------------------|---------------------| +| w/o ReMe | 0.1497 | 0.3285 | +| w/ ReMe | 0.1706 **(+2.09%)** | 0.3631 **(+3.46%)** | + +Pass@K measures the probability that at least one of K generated candidates successfully completes the task (score=1). +The current experiments use an internal AppWorld environment, which may differ slightly from the public version. + +For more details on how to reproduce the experiments, see [quickstart.md](benchmark/appworld/quickstart.md). + +### 🔧 [BFCL-V3 benchmark](benchmark/bfcl/quickstart.md) + +We evaluate ReMe on the BFCL-V3 multi-turn-base task (random split 50 train / 150 val) using Qwen3-8B (thinking mode): + +| Method | Avg@4 | Pass@4 | +|----------|---------------------|---------------------| +| w/o ReMe | 0.4033 | 0.5955 | +| w/ ReMe | 0.4450 **(+4.17%)** | 0.6577 **(+6.22%)** | + +For more details on how to reproduce the experiments, see [quickstart.md](benchmark/bfcl/quickstart.md). + + +## ⭐ Community & support + +- **Star & Watch**: Starring helps more agent developers discover ReMe; Watching keeps you up to date with new releases + and features. +- **Share your results**: Share how ReMe empowers your agents in Issues or Discussions — we are happy to showcase great + community use cases. +- **Need a new feature?** Open a feature request; we’ll evolve ReMe together with the community. +- **Code contributions**: All forms of contributions are welcome. Please see + the [contribution guide](docs/contribution.md). +- **Acknowledgements**: We thank excellent open-source projects such as OpenClaw, Mem0, MemU, and CoPaw for their + inspiration and support. --- @@ -476,10 +564,11 @@ graph TB ## ⚖️ License -This project is open source under the Apache License 2.0. See the [LICENSE](./LICENSE) file for details. +This project is open-sourced under the Apache License 2.0. See [LICENSE](./LICENSE) for details. --- -## 📈 Star History +## 📈 Star history [![Star History Chart](https://api.star-history.com/svg?repos=agentscope-ai/ReMe&type=Date)](https://www.star-history.com/#agentscope-ai/ReMe&Date) + diff --git a/README_ZH.md b/README_ZH.md index 3c379a2b..8723177c 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -38,7 +38,7 @@ ReMe 让智能体拥有**真正的记忆力**——旧对话自动浓缩,重 > 记忆即文件,文件即记忆 将**记忆视为文件**——可读、可编辑、可复制。 -[CoPaw](https://github.com/agentscope-ai/CoPaw)通过继承 `ReMeLight` 实现了长期记忆和上下文的管理。 +[CoPaw](https://github.com/agentscope-ai/CoPaw) 通过继承 `ReMeLight` 实现了长期记忆和上下文的管理。 | 传统记忆系统 | File Based ReMe | |-----------|-----------------| @@ -49,9 +49,9 @@ ReMe 让智能体拥有**真正的记忆力**——旧对话自动浓缩,重 ``` working_dir/ -├── MEMORY.md # 长期记忆:用户偏好、项目配置等持久信息 +├── MEMORY.md # 长期记忆:用户偏好等持久信息 ├── memory/ -│ └── YYYY-MM-DD.md # 每日摘要日志:对话结束后自动写入 +│ └── YYYY-MM-DD.md # 每日日记:对话结束后自动写入 └── tool_result/ # 超长工具输出缓存(自动管理,超期自动清理) └── .txt ``` @@ -60,17 +60,17 @@ working_dir/ [ReMeLight](reme/reme_light.py) 是该记忆系统的核心类,为 AI Agent 提供完整的记忆管理能力: -| 方法 | 功能 | 关键组件 | -|------------------------|--------------|----------------------------------------------------------------------------------------------------------| -| `start` | 🚀 启动记忆系统 | 初始化文件存储、文件监控、Embedding 缓存;清理过期工具结果文件 | -| `close` | 📕 关闭并清理 | 清理工具结果文件、停止文件监控、保存 Embedding 缓存 | -| `compact_memory` | 📦 压缩历史对话为摘要 | [Compactor](reme/memory/file_based/compactor.py) — ReActAgent 生成结构化上下文检查点 | -| `summary_memory` | 📝 将重要记忆写入文件 | [Summarizer](reme/memory/file_based/summarizer.py) — ReActAgent + 文件工具(read / write / edit) | -| `compact_tool_result` | ✂️ 压缩超长工具输出 | [ToolResultCompactor](reme/memory/file_based/tool_result_compactor.py) — 截断并转存到 `tool_result/`,消息中保留文件引用 | -| `pre_reasoning_hook` | 🔄 推理前预处理钩子 | 自动压缩工具结果 + 生成摘要 + 异步触发记忆总结任务 | -| `memory_search` | 🔍 语义搜索记忆 | [MemorySearch](reme/memory/tools/chunk/memory_search.py) — 向量 + BM25 混合检索 | -| `get_in_memory_memory` | 🗂️ 创建会话内存实例 | [ReMeInMemoryMemory](reme/memory/file_based/reme_in_memory_memory.py) — Token 感知的内存管理,支持压缩摘要和状态序列化(静态方法) | - +| 方法 | 功能 | 关键组件 | +|------------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------| +| `check_context` | 📊 检查上下文大小 | [ContextChecker](reme/memory/file_based/component/context_checker.py) — 检查上下文是否超出阈值并拆分Message | +| `compact_memory` | 📦 压缩历史对话为摘要 | [Compactor](reme/memory/file_based/component/compactor.py) — ReActAgent 生成结构化上下文摘要 | +| `summary_memory` | 📝 将重要记忆写入文件 | [Summarizer](reme/memory/file_based/component/summarizer.py) — ReActAgent + 文件工具(read / write / edit) | +| `compact_tool_result` | ✂️ 压缩超长工具输出 | [ToolResultCompactor](reme/memory/file_based/component/tool_result_compactor.py) — 截断超长的工具调用结果并转存到 `tool_result/`,消息中保留文件引用 | +| `memory_search` | 🔍 语义搜索记忆 | [MemorySearch](reme/memory/file_based/tools/memory_search.py) — 向量 + BM25 混合检索 | +| `ReMeInMemoryMemory` | 🗂️ 会话内存类 | [ReMeInMemoryMemory](reme/memory/file_based/reme_in_memory_memory.py) — Token 感知的内存管理,支持压缩摘要和状态序列化 | +| `pre_reasoning_hook` | 🔄 推理前预处理钩子 | compact_tool_result + check_context + compact_memory + summary_memory(async) | +| `start` | 🚀 启动记忆系统 | 初始化文件存储、文件监控、Embedding 缓存;清理过期工具结果文件 | +| `close` | 📕 关闭并清理 | 清理工具结果文件、停止文件监控、保存 Embedding 缓存 |· --- ### 🚀 快速开始 @@ -85,14 +85,14 @@ pip install -e ".[light]" `ReMeLight` 环境变量配置 Embedding 和存储后端 -| Variable | Description | Example | -|----------------------|--------------------|-----------------------------------------------------| -| `LLM_API_KEY` | LLM API key | `sk-xxx` | -| `LLM_BASE_URL` | LLM base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` | -| `EMBEDDING_API_KEY` | Embedding API key | `sk-xxx` | -| `EMBEDDING_BASE_URL` | Embedding base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` | +| Variable | Description | Example | +|----------------------|-------------------------|-----------------------------------------------------| +| `LLM_API_KEY` | LLM API key | `sk-xxx` | +| `LLM_BASE_URL` | LLM base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` | +| `EMBEDDING_API_KEY` | Embedding API key (可选) | `sk-xxx` | +| `EMBEDDING_BASE_URL` | Embedding base URL (可选) | `https://dashscope.aliyuncs.com/compatible-mode/v1` | -#### Python使用 +#### Python 使用 ```python import asyncio @@ -141,8 +141,9 @@ async def main(): # 5. 语义搜索记忆(向量 + BM25 混合检索) result = await reme.memory_search(query="Python 版本偏好", max_results=5) - # 6. 获取会话内存实例(静态方法,管理单次对话的上下文) - memory = ReMeLight.get_in_memory_memory() + # 6. 创建会话内存实例(管理单次对话的上下文) + from reme.memory.file_based.reme_in_memory_memory import ReMeInMemoryMemory + memory = ReMeInMemoryMemory() for msg in messages: await memory.add(msg) token_stats = await memory.estimate_tokens(max_input_length=128000) @@ -162,7 +163,7 @@ if __name__ == "__main__": ``` > 📂 完整示例代码:[test_reme_light.py](tests/light/test_reme_light.py) -> 📋 运行结果示例:[test_reme_light.log](tests/light/test_reme_light_log.txt)(223,838 tokens → 1,105 tokens,压缩率 99.5%) +> 📋 运行结果示例:[test_reme_light_log.txt](tests/light/test_reme_light_log.txt)(223,838 tokens → 1,105 tokens,压缩率99.5%) ### 基于文件的 ReMeLight 记忆系统架构 @@ -170,125 +171,188 @@ if __name__ == "__main__": `ReMeLight`,将记忆能力集成到 Agent 推理流程中: ```mermaid -graph TB - CoPaw["CoPaw MemoryManager
(继承 ReMeLight)"] -->|pre_reasoning hook| Hook[MemoryCompactionHook] - CoPaw --> ReMeLight[ReMeLight] - Hook -->|超出阈值| ReMeLight - ReMeLight --> CompactMemory[compact_memory
历史对话压缩] - ReMeLight --> SummaryMemory[summary_memory
记忆写入文件] - ReMeLight --> CompactToolResult[compact_tool_result
超长工具输出压缩] - ReMeLight --> MemSearch[memory_search
语义搜索] - ReMeLight --> InMemory[get_in_memory_memory
ReMeInMemoryMemory] - CompactMemory --> Compactor[Compactor
ReActAgent] - SummaryMemory --> Summarizer[Summarizer
ReActAgent + 文件工具] - CompactToolResult --> ToolResultCompactor[ToolResultCompactor
截断 + 转存文件] - Summarizer --> FileIO[FileIO
read / write / edit] - FileIO --> MemoryFiles[memory/YYYY-MM-DD.md] - ToolResultCompactor --> ToolResultFiles[tool_result/*.txt] - MemoryFiles -.->|文件变更| FileWatcher[异步文件监控] - FileWatcher -->|更新索引| FileStore[本地数据库] - MemSearch --> FileStore +graph LR + Agent[Agent] -->|每轮推理前| Hook[pre_reasoning_hook] + Hook --> TC[compact_tool_result
压缩工具输出] + TC --> CC[check_context
Token 计数] + CC -->|超限| CM[compact_memory
生成摘要] + CC -->|超限| SM[summary_memory
异步持久化] + SM -->|ReAct + FileIO| Files[memory/*.md] + Agent -->|主动调用| Search[memory_search
向量+BM25] + Agent -->|会话内存| InMem[ReMeInMemoryMemory
Token感知内存] + Files -.->|FileWatcher| Store[(FileStore
向量+FTS索引)] + Search --> Store ``` -### 上下文压缩机制 +--- -#### 上下文压缩 +#### 1. check_context — 上下文检查 -[Compactor](reme/memory/file_based/compactor.py) 使用 ReActAgent 将历史对话压缩为结构化的**上下文检查点**: - -| 字段 | 说明 | -|-----------------------|-----------------------| -| `## Goal` | 🎯 用户要完成的目标(可多项) | -| `## Constraints` | ⚙️ 用户提到的约束和偏好 | -| `## Progress` | 📈 已完成 / 进行中 / 阻塞的任务 | -| `## Key Decisions` | 🔑 做出的决策及简短理由 | -| `## Next Steps` | 🗺️ 下一步行动计划(有序列表) | -| `## Critical Context` | 📌 文件路径、函数名、错误信息等关键数据 | - -支持**增量更新**:传入 `previous_summary` 时,自动将新对话与旧摘要合并,保留历史进展。 - -#### 工具结果压缩 - -[ToolResultCompactor](reme/memory/file_based/tool_result_compactor.py) 解决工具输出过长(比如 browser use)导致上下文膨胀的问题: +[ContextChecker](reme/memory/file_based/component/context_checker.py) 基于 Token 计数判断上下文是否超限,自动拆分为「待压缩」和「保留」两组消息。 ```mermaid graph LR - A[tool_result 消息] --> B{内容长度 > threshold?} - B -->|否| C[保留原样] - B -->|是| D[截断到 threshold 字符] - D --> E[完整内容写入 tool_result/uuid.txt] - E --> F[消息中追加文件引用路径] + M[messages] --> H[AsMsgHandler
Token 计数] + H --> C{total > threshold?} + C -->|否| K[返回全部消息] + C -->|是| S[从尾部向前保留
reserve tokens] + S --> CP[messages_to_compact
早期消息] + S --> KP[messages_to_keep
近期消息] + S --> V{is_valid
工具调用对齐?} ``` -过期文件(超过 `retention_days`)在 `start` / `close` / `compact_tool_result` 时自动清理。 +- **核心逻辑**:从尾部向前保留 `reserve` tokens,超出部分标记为待压缩 +- **完整性保证**:不拆分 user-assistant 对话对,不拆分 tool_use/tool_result 配对 -### 记忆总结:ReAct + 文件工具 +--- -[Summarizer](reme/memory/file_based/summarizer.py) 采用 **ReAct + 文件工具** 模式,让 AI 自主决定写什么、写到哪: +#### 2. compact_memory — 对话压缩 + +[Compactor](reme/memory/file_based/component/compactor.py) 使用 ReActAgent 将历史对话压缩为**结构化上下文摘要**。 ```mermaid graph LR - A[接收对话] --> B{思考: 有什么值得记录?} - B --> C[行动: read memory/YYYY-MM-DD.md] - C --> D{思考: 如何与现有内容合并?} - D --> E[行动: edit 更新文件] - E --> F{思考: 还有遗漏吗?} - F -->|是| B - F -->|否| G[完成] + M[messages] --> H[AsMsgHandler
format_msgs_to_str] + H --> A[ReActAgent
reme_compactor] + P[previous_summary] -->|增量更新| A + A --> S[结构化摘要
Goal/Progress/Decisions...] ``` -[FileIO](reme/memory/file_based/file_io.py) 提供文件操作工具集: +**摘要结构**(上下文检查点): -| 工具 | 功能 | 使用场景 | -|---------|---------------|---------------| -| `read` | 读取文件内容(支持行范围) | 查看现有记忆,避免重复写入 | -| `write` | 覆盖写入文件 | 创建新记忆文件或大幅重构 | -| `edit` | 精确匹配后替换 | 追加新内容或修改特定段落 | +| 字段 | 说明 | +|-----------------------|--------------------| +| `## Goal` | 用户目标 | +| `## Constraints` | 约束和偏好 | +| `## Progress` | 任务进展 | +| `## Key Decisions` | 关键决策 | +| `## Next Steps` | 下一步计划 | +| `## Critical Context` | 文件路径、函数名、错误信息等关键数据 | -### 会话内存管理 +- **增量更新**:传入 `previous_summary` 时,自动将新对话与旧摘要合并 -[ReMeInMemoryMemory](reme/memory/file_based/reme_in_memory_memory.py) 扩展了 AgentScope 的 `InMemoryMemory`: +--- -| 功能 | 说明 | -|----------------------------------|---------------------------| -| `get_memory` | 按标记过滤消息,自动在头部追加压缩摘要 | -| `estimate_tokens` | 精确估算当前上下文 Token 用量及使用率 | -| `get_history_str` | 生成人类可读的对话历史摘要(含 Token 统计) | -| `state_dict` / `load_state_dict` | 支持状态序列化 / 反序列化(会话持久化) | -| `mark_messages_compressed` | 标记消息为已压缩状态 | -| `get_compressed_summary` | 获取已压缩的摘要内容 | +#### 3. summary_memory — 记忆持久化 -### 记忆检索 - -[MemorySearch](reme/memory/tools/chunk/memory_search.py) 提供**向量 + BM25 混合检索**能力: - -| 检索方式 | 优势 | 劣势 | -|-------------|-----------------|----------------| -| **向量语义** | 捕捉意义相近但措辞不同的内容 | 对精确 token 匹配较弱 | -| **BM25 全文** | 精确 token 命中效果极佳 | 无法理解同义词和改写 | - -**融合机制**:两路召回后按权重加权求和(向量 0.7 + BM25 0.3),自然语言与精确查找均可命中。 +[Summarizer](reme/memory/file_based/component/summarizer.py) 采用 **ReAct + 文件工具** 模式,让 AI 自主决定写什么、写到哪。 ```mermaid graph LR - Q[搜索查询] --> V[向量搜索 × 0.7] -Q --> B[BM25 × 0.3] -V --> M[去重 + 加权融合] -B --> M -M --> R[Top-N 结果] + M[messages] --> A[ReActAgent
reme_summarizer] + A -->|read| R[读取 memory/YYYY-MM-DD.md] + R --> T{思考: 如何合并?} + T -->|write| W[覆盖写入] + T -->|edit| E[精确替换] + W --> F[memory/YYYY-MM-DD.md] + E --> F ``` +**文件工具**([FileIO](reme/memory/file_based/tools/file_io.py)): + +| 工具 | 功能 | +|---------|---------| +| `read` | 读取文件内容 | +| `write` | 覆盖写入文件 | +| `edit` | 精确匹配后替换 | + +--- + +#### 4. compact_tool_result — 工具结果压缩 + +[ToolResultCompactor](reme/memory/file_based/component/tool_result_compactor.py) 解决工具输出过长导致上下文膨胀的问题。 + +```mermaid +graph LR + M[messages] --> L{遍历 tool_result
len > threshold?} + L -->|否| K[保留原样] + L -->|是| T[truncate_text
截断到 threshold] + T --> S[完整内容写入
tool_result/uuid.txt] + S --> R[消息追加文件路径引用] + R --> C[cleanup_expired_files
清理过期文件] +``` + +- **自动清理**:过期文件(超过 `retention_days`)在 `start`/`close`/`compact_tool_result` 时自动删除 + +--- + +#### 5. memory_search — 记忆检索 + +[MemorySearch](reme/memory/file_based/tools/memory_search.py) 提供**向量 + BM25 混合检索**能力。 + +```mermaid +graph LR + Q[query] --> E[Embedding
向量化] + E --> V[vector_search
语义相似] + Q --> B[BM25
关键词匹配] + V -->|" weight: 0.7 "| M[去重 + 加权融合] + B -->|" weight: 0.3 "| M + M --> F[min_score 过滤] + F --> R[Top-N 结果] +``` + +- **融合机制**:向量权重 0.7 + BM25 权重 0.3,兼顾语义相似和精确匹配 + +--- + +#### 6. ReMeInMemoryMemory — 会话内存 + +[ReMeInMemoryMemory](reme/memory/file_based/reme_in_memory_memory.py) 扩展 AgentScope 的 `InMemoryMemory`,提供 Token +感知的内存管理。 + +```mermaid +graph LR + C[content] --> G[get_memory
exclude_mark=COMPRESSED] + G --> F[排除已压缩消息] + F --> P{prepend_summary?} + P -->|是| S[头部插入 previous-summary] + S --> O[输出 messages] + P -->|否| O +``` + +| 功能 | 说明 | +|----------------------------------|-------------------| +| `get_memory` | 按标记过滤,自动追加压缩摘要 | +| `estimate_tokens` | 估算上下文 Token 用量 | +| `state_dict` / `load_state_dict` | 状态序列化/反序列化(会话持久化) | + +--- + +#### 7. pre_reasoning_hook — 推理前预处理 + +整合上述组件的统一入口,在每轮推理前自动管理上下文。 + +```mermaid +graph LR + M[messages] --> TC[compact_tool_result
压缩超长工具输出] + TC --> CC[check_context
计算剩余空间] + CC --> D{messages_to_compact
非空?} + D -->|否| K[返回原消息 + 原摘要] + D -->|是| V{is_valid?} + V -->|否| K + V -->|是| CM[compact_memory
同步生成摘要] + V -->|是| SM[add_async_summary_task
异步持久化] + CM --> R[返回 messages_to_keep + 新摘要] +``` + +**执行流程**: + +1. `compact_tool_result` — 压缩超长工具输出 +2. `check_context` — 检查上下文是否超限 +3. `compact_memory` — 生成压缩摘要(同步) +4. `summary_memory` — 持久化记忆(异步后台) + --- ## 🗃️ 基于向量库的记忆系统 [ReMe Vector Based](reme/reme.py) 是基于向量库的记忆系统核心类,支持三种记忆类型的统一管理: -| 记忆类型 | 用途 | 使用场景 | -|--------------|------------------|-------------| -| **个人记忆** | 记录用户偏好、习惯 | `user_name` | -| **任务/程序性记忆** | 记录任务执行经验、成功/失败模式 | `task_name` | -| **工具记忆** | 记录工具使用经验、参数优化 | `tool_name` | +| 记忆类型 | 用途 | +|--------------|------------------| +| **个人记忆** | 记录用户偏好、习惯 | +| **任务/程序性记忆** | 记录任务执行经验、成功/失败模式 | +| **工具记忆** | 记录工具使用经验、参数优化 | ### 核心能力 @@ -302,24 +366,11 @@ M --> R[Top-N 结果] | `delete_memory` | 🗑️ 删除记忆 | 删除指定记忆 | | `list_memory` | 📋 列出记忆 | 列出某类记忆,支持过滤和排序 | -### 安装 +### 安装与环境变量 -```bash -pip install -U reme-ai -``` +安装和环境变量配置与 [ReMeLight 一致](#安装),通过环境变量设置 API 密钥,可写在项目根目录的 `.env` 文件中。 -### 环境变量 - -API 密钥通过环境变量设置,可写在项目根目录的 `.env` 文件中: - -| 环境变量 | 说明 | 示例 | -|----------------------|--------------------------|-----------------------------------------------------| -| `LLM_API_KEY` | LLM 的 API Key | `sk-xxx` | -| `LLM_BASE_URL` | LLM 的 Base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` | -| `EMBEDDING_API_KEY` | Embedding 的 API Key(可选) | `sk-xxx` | -| `EMBEDDING_BASE_URL` | Embedding 的 Base URL(可选) | `https://dashscope.aliyuncs.com/compatible-mode/v1` | - -### Python使用 +### Python 使用 ```python import asyncio @@ -413,7 +464,7 @@ if __name__ == "__main__": ### 技术架构 ```mermaid -graph TB +graph LR User[用户 / Agent] --> ReMe[Vector Based ReMe] ReMe --> Summarize[记忆总结] ReMe --> Retrieve[记忆检索] @@ -432,6 +483,42 @@ graph TB ToolRet --> VectorStore ``` +### 实验效果 + +Coming soon... + +--- + +## 🧪 程序化记忆论文 + +> 我们的程序性(任务)记忆论文已在 [arXiv](https://arxiv.org/abs/2512.10696) 发布 + +### 🌍 [Appworld 实验](benchmark/appworld/quickstart.md) + +我们在 Appworld 环境上使用 Qwen3-8B(非思考模式)进行评测: + +| 方法 | Avg@4 | Pass@4 | +|---------|---------------------|---------------------| +| 无 ReMe | 0.1497 | 0.3285 | +| 使用 ReMe | 0.1706 **(+2.09%)** | 0.3631 **(+3.46%)** | + +Pass@K 衡量在生成 K 个候选中,至少一个成功完成任务(score=1)的概率。 +当前实验使用的是内部 AppWorld 环境,可能与对外版本存在轻微差异。 + +关于如何复现实验的更多细节,见 [quickstart.md](benchmark/appworld/quickstart.md) + +### 🔧 [BFCL-V3 实验](benchmark/bfcl/quickstart.md) + +我们在 BFCL-V3 multi-turn-base 任务(随机划分 50 train / 150 val)上,使用 Qwen3-8B(思考模式)进行评测: + +| 方法 | Avg@4 | Pass@4 | +|---------|---------------------|---------------------| +| 无 ReMe | 0.4033 | 0.5955 | +| 使用 ReMe | 0.4450 **(+4.17%)** | 0.6577 **(+6.22%)** | + +关于如何复现实验的更多细节,见 [quickstart.md](benchmark/bfcl/quickstart.md) + + ## ⭐ 社区与支持 - **Star 与 Watch**:Star 可让更多智能体开发者发现 ReMe;Watch 可助你第一时间获知新版本与特性。 diff --git a/docs/cookbook/appworld/quickstart.md b/benchmark/appworld/quickstart.md similarity index 100% rename from docs/cookbook/appworld/quickstart.md rename to benchmark/appworld/quickstart.md diff --git a/docs/cookbook/bfcl/quickstart.md b/benchmark/bfcl/quickstart.md similarity index 100% rename from docs/cookbook/bfcl/quickstart.md rename to benchmark/bfcl/quickstart.md diff --git a/benchmark/halumem/scripts.sh b/benchmark/halumem/cat_correct_scripts.sh similarity index 100% rename from benchmark/halumem/scripts.sh rename to benchmark/halumem/cat_correct_scripts.sh diff --git a/benchmark/halumem/eval_scripts.sh b/benchmark/halumem/eval_scripts.sh new file mode 100755 index 00000000..c8a06881 --- /dev/null +++ b/benchmark/halumem/eval_scripts.sh @@ -0,0 +1,5 @@ +clear && python benchmark/halumem/eval_reme.py \ + --data_path /Users/yuli/workspace/HaluMem/data/HaluMem-Medium.jsonl \ + --reme_model_name qwen3.5-plus \ + --batch_size 10000 \ + --algo_version default \ No newline at end of file diff --git a/docs/REME2_README.md b/docs/REME2_README.md deleted file mode 100644 index fd6174a6..00000000 --- a/docs/REME2_README.md +++ /dev/null @@ -1,13 +0,0 @@ - - -# TODO -- [] halumem bench开发 -- [] default版本开发,for cli版本体验 -- [] cli开发 -- [] locomo bench开发 -- [] task memory迁移 -- [] mcp开发 -- [] reme外层接口完善 -- [] reme2 readme完善 -- [] 看日志,看要这个default版本怎么优化。 -- [] 学习Clawdbot记忆系统 \ No newline at end of file diff --git a/docs/deprecated.txt b/docs/deprecated.txt deleted file mode 100644 index 06f9648f..00000000 --- a/docs/deprecated.txt +++ /dev/null @@ -1,13 +0,0 @@ -from loguru import logger - -用英文注释,完善module/class/function docstring,要一句话简洁,不要变更代码逻辑,符合pep和pylint规范,使用list而不是typing.List/Dict,不使用typing.Union - -看看代码有什么问题 -用英文注释,完善module/class/function docstring,要一句话简洁,代码要简洁,符合pep和pylint规范,使用list而不是typing.List,不使用typing.Union -C0114: Missing module docstring (missing-module-docstring) -C0115: Missing class docstring (missing-class-docstring) -C0116: Missing function or method docstring (missing-function-docstring) -done: { for f in ./*.py; do [[ "$f" != "./__init__.py" ]] && grep -v '^[[:space:]]*#' "$f"; done; } | pbcopy - -然后是一个完整的tests,但是不要用其他的包,只是test开头的函数或者类,要求from loguru import logger -写一个测试文件,不要使用pytest,普通的test,要求英文注释 \ No newline at end of file diff --git a/docs/future_work.md b/docs/future_work.md deleted file mode 100644 index 807abfed..00000000 --- a/docs/future_work.md +++ /dev/null @@ -1,18 +0,0 @@ -# Future Work - -- [ ] P0 ReMe documentation style migration: Recommend using the same doc and jupyter structure as Agentscope Runtime @jiaji -- [ ] P0 ReMe integration with agentscope Personal/Task/Tool @jinli -- [ ] P0 ReMe sample library examples [show case](https://github.com/agentscope-ai/agentscope-samples/tree/main/functionality/long_term_memory_mem0) @jinli -- [ ] P0 Decouple flowllm dependencies @jinli -- [ ] P0 ReMe support for import, improve code documentation @jinli -- [ ] P1 ReMe integration with asio tool_memory @jinli -- [ ] P2 ReMe integration with agentscope-Runtime tool_memory @jinli - -- [ ] P0 Task Memory Research Paper @zhoyin - -- [ ] P1 Context interface definition @jinli - -- [ ] P2 Database layer interface unification @jinli -- [ ] P2 Automatic Tool Exploration Mode @wangcan -- [ ] P2 Mem-Agent Exploration @weikang -- [ ] P2 Desktop Pet Personal Assistant diff --git a/docs/reme_v2_design.md b/docs/reme_v2_design.md deleted file mode 100644 index 6fa78140..00000000 --- a/docs/reme_v2_design.md +++ /dev/null @@ -1,735 +0,0 @@ -# ReMeV2 深度设计文档:渐进式 Agentic Memory 方案 - -## 一、 背景与现状分析 - -### 1.1 当前面临的挑战 - -* **外功修炼(接口易用性)**:现有的 `server-client` 模式对新手开发者不够友好,集成成本高,需要更直观、纯 Pythonic 的调用方式。 -* **内功修炼(架构深度)**:受 `skills` 和 `agentic memory` 启发,现有的存储检索较为机械。我们需要一种基于**渐进式检索(Progressive Retrieval)**与**渐进式总结(Progressive Summarization)**的智能体记忆方案。 - -### 1.2 核心目标 - -1. **极简开发体验**:开发者友好,全异步接口,支持本地直接运行与 CLI 体验。 -2. **认知架构升级**:引入 渐进式检索 & 渐进式总结 的 Agentic 模式,融合多种记忆,让记忆的存取具备“思考”过程。 -3. **生态融合**:原生支持 AgentScope、LangChain 等主流框架。 - ---- - -## 二、 竞品调研与启示 - -### 2.1 主流竞品深度对比 - -| 产品 | 设计哲学 | 核心优势 | 局限性 | -|-------------|----------|---------------------------------------------|-------------------| -| **mem0** | 智能便签本 | 原子事实提取,极高 Token 效率。 | 缺乏对复杂逻辑链条的支持。 | -| **Letta** | 带硬盘的 CPU | 模拟计算机三级存储(Core/Recall/Archival),Agent 自主控存。 | 状态机管理相对复杂。 | -| **MIRIX** | 认知架构图谱 | 实体-关系双引擎,支持记忆“进化”与“固化”。 | 侧重研究,落地集成门槛较高。 | -| **LangMem** | 用户档案系统 | 异步 Compaction(压缩),Schema 驱动,强一致性。 | 偏向 SaaS 应用,灵活性略逊。 | - -### 2.2 mem0 -- https://github.com/mem0ai/mem0 -- https://docs.mem0.ai/core-concepts/memory-operations/add -- https://docs.mem0.ai/core-concepts/memory-operations/search -- https://docs.mem0.ai/core-concepts/memory-operations/update -- https://docs.mem0.ai/core-concepts/memory-operations/delete - -#### 2.2.1 API Reference -| 接口名称 | 核心输入参数 (Inputs) | 核心输出 (Outputs) | 背后逻辑 (Internal Logic) | -| --- | --- | --- | --- | -| **Add** | `messages` (文本/对话), `user_id`, `metadata` | `id`, `event` (ADD/UPDATE), `data` | **提取与合并**:LLM 提取事实,自动去重并更新已有记忆,而非简单堆叠。 | -| **Search** | `query` (自然语言), `filters`, `limit` | `id`, `memory` (事实文本), `score`, `metadata` | **语义检索**:基于向量相似度查找最相关的“原子事实”,支持多维过滤。 | -| **Update** | `memory_id` (必填), `data` (新内容) | 操作状态 (Success/Fail) | **手动干预**:允许开发者对特定的事实进行精确修正。 | -| **Delete** | `memory_id` 或 `user_id` (清空) | 操作状态 (Success/Fail) | **遗忘机制**:物理删除或逻辑移除不再需要的信息。 | - -#### 2.2.2 Tech Strategy & Benefits -| 维度 | 技术方案 (Technical Solution) | 核心优势 (Key Advantages) | -| --- | --- | --- | -| **存储架构** | **混合存储**:向量数据库 (Vector) + 图数据库 (Graph) + 关系型元数据。 | **多维关联**:不仅能搜到相似内容,还能理解实体间的逻辑关系(如“父子”、“因果”)。 | -| **数据处理** | **原子化事实提取**:利用 LLM 将长篇对话压缩为简短的 Fact。 | **极高 Token 效率**:注入 Prompt 的内容更精炼,减少 90% 以上的冗余信息,大幅降本。 | -| **管理层级** | **多级联动**:User (长期) Agent (专业) Session (短期)。 | **个性化定制**:实现跨会话的“长效记忆”,AI 能记住用户一个月前说过的偏好。 | -| **冲突处理** | **自适应更新算法**:新信息进入时自动比对旧记忆。 | **数据一致性**:自动处理矛盾信息(如用户更换了住址),确保记忆库始终是“最新真理”。 | -| **兼容性** | **解耦设计**:支持多种 Embedding 模型与向量数据库后端。 | **快速集成**:几行代码即可为现有 LLM 应用增加记忆层,适配各种生产环境。 | - - ---- - -### 2.3 Letta -- https://github.com/letta-ai/letta -- https://docs.letta.com/guides/agents/archival-memory/ -- https://docs.letta.com/guides/agents/archival-search/ - -#### 2.3.1 存储架构层级 (Memory Tiering) - -Letta 将记忆分为三个物理/逻辑层,模拟计算机的存储架构: - -| 记忆层级 | 存储介质 | 访问方式 | 核心作用 | -| --- | --- | --- | --- | -| **Core Memory** | **上下文窗口 (Prompt)** | 直接读写 | **即时意识**:包含 `Persona`(AI 设定)和 `Human`(用户信息)。Agent 随时可见,响应最快。 | -| **Recall Memory** | **关系型数据库 (SQL)** | 分页检索 | **短期/历史回顾**:存储完整的对话流(Messages)。用于回答“你刚才说了什么”。 | -| **Archival Memory** | **向量数据库 (Vector)** | 语义搜索 | **长期知识库**:存储海量事实或文档。Agent 通过工具自主检索或存入。 | - -#### 2.3.2 核心操作接口 (API & Tool Reference) - -在 Letta 中,记忆的操作通常封装为 **Tools**,由 Agent 根据推理需求主动调用。 - -| 接口/工具名称 | 输入参数 (Inputs) | 核心输出 (Outputs) | 背后逻辑 (Internal Logic) | -| --- | --- | --- | --- | -| **`core_memory_update`** | `section`, `new_content` | 更新后的段落内容 | **原子替换**:直接修改 System Prompt 中的特定块(如:更新用户的职业或 AI 的性格偏好)。 | -| **`archival_memory_insert`** | `content` (字符串) | 写入状态/ID | **知识沉淀**:将当前对话中的重要信息或外部文件片段“持久化”到向量数据库。 | -| **`archival_memory_search`** | `query`, `page` | 匹配的文本块列表 | **主动 RAG**:Agent 意识到知识不足时,自主发起向量检索,并将结果拉入临时上下文。 | -| **`conversation_search`** | `query`, `start_date` | 历史消息记录 | **全文检索**:在 Recall Memory 中根据关键词或时间戳查找历史对话详情。 | -| **`send_message`** | `message`, `agent_id` | 响应流/状态更新 | **状态循环**:这是主入口,触发 Agent 的“思考-行动-观察”循环,自动处理内存同步。 | - -#### 2.3.3 技术策略与核心优势 (Tech Strategy & Benefits) - -| 维度 | 技术方案 (Technical Solution) | 核心优势 (Key Advantages) | -| --- | --- | --- | -| **状态持久化** | **Agent State Snapshot**:将 Agent 的所有内存、工具定义和历史记录打包存入数据库。 | **无限存续**:Agent 不再是无状态的 API 调用。重启服务器后,Agent 依然记得所有细节。 | -| **自主演进** | **Self-Editing Loop**:Agent 拥有修改自己 Core Memory 的权限(通过函数调用)。 | **认知闭环**:AI 能在交流中发现矛盾并自我更正,例如发现用户搬家后自动更新 `Human` 模块。 | -| **算力调度** | **OOC (Out-of-Context) 管理**:当对话过长,系统自动将旧消息从 Core 移入 Recall。 | **突破 Context 限制**:在 8k 窗口的模型上也能处理相当于 1M 窗口的逻辑量,且成本更低。 | -| **多代理协同** | **Letta Server 中控**:统一管理多个 Agent 的状态机与资源访问权限。 | **企业级扩展**:支持创建 Agent 团队,每个 Agent 拥有独立的记忆空间但可共享 Archival 库。 | -| **解耦灵活性** | **Provider Agnostic**:后端支持 Postgres/Chroma,前端支持 OpenAI/Anthropic/Local LLMs。 | **无缝迁移**:不绑定特定模型,开发者可以根据成本或能力随时更换底座。 | - -#### 2.3.4 与 mem0 的深度对比 - -* **设计哲学**: -* **mem0** 像是一个**“智能记事本”**,它在后台默默地帮你总结事实。 -* **Letta** 像是一个**“带硬盘的 CPU”**,它把记忆管理完全交给了 Agent 自己的逻辑推理。 - - -* **交互模式**: -* **mem0** 通常是外部干预(Add/Search)。 -* **Letta** 强调 **Agentic Control**(Agent 意识到需要搜索时才去搜索),这种模式更接近人类的思维过程。 - ---- - -### 2.4 MIRIX -- https://github.com/Mirix-AI/MIRIX -- https://docs.mirix.io/ - -#### 2.4.1 API Reference - -| 接口名称 | 核心输入参数 (Inputs) | 核心输出 (Outputs) | 背后逻辑 (Internal Logic) | -| --- | --- | --- | --- | -| **Add** | `content` (观察/对话), `agent_id`, `context_type` (如任务/闲聊) | `memory_id`, `graph_nodes`, `status` | **实体建模**:不只是提取事实,而是将信息拆解为实体(Entities)与关系(Relations),并挂载到智能体的知识图谱中。 | -| **Query** | `query` (意图), `scope` (全局/局部), `top_k` | `retrieved_memories`, `relation_paths`, `score` | **混合检索**:结合向量(Vector)的语义相关性和图(Graph)的拓扑连接性,寻找具有逻辑深度背景的记忆。 | -| **Evolve** | `target_memories` (可选), `agent_id` | `optimized_structure`, `merged_nodes` | **记忆固化/压缩**:模仿人类大脑的“睡眠”机制,自动合并碎片化记忆,将短期经验转化为长期的结构化知识。 | -| **Observe** | `interaction_stream`, `feedback` | `insights`, `priority_update` | **实时学习**:根据用户反馈或环境变化,动态调整记忆的权重(Importance)和置信度。 | - -#### 2.4.2 Tech Strategy & Benefits - -| 维度 | 技术方案 (Technical Solution) | 核心优势 (Key Advantages) | -| --- | --- | --- | -| **存储架构** | **语义-关系双引擎**:向量索引(Vector Index)+ 属性图(Property Graph)。 | **深度上下文**:不仅知道“是什么”,还能通过图路径推理出“为什么”,有效解决 LLM 幻觉问题。 | -| **记忆层级** | **三层架构**:感知记忆 (Perception) -> 语义记忆 (Semantic) -> 经验记忆 (Episodic)。 | **任务适应性**:不同任务自动匹配不同的记忆深度,短期任务关注细节,长期任务关注模式。 | -| **演化机制** | **自主固化 (Self-Consolidation)**:通过 LLM 定期对冗余、矛盾信息进行清洗和逻辑抽象。 | **永久生命力**:解决随时间推移记忆库膨胀导致的检索噪声,确保记忆库“越用越聪明”。 | -| **推理增强** | **基于记忆的 RAG+**:在检索到的事实基础上,额外提供关联的逻辑链条(Logic Chains)。 | **辅助决策**:为 Agent 提供决策支撑,使其在处理复杂流程时具备类似“长期经验值”的直觉。 | -| **多代理协同** | **内存共享协议**:支持 Agent 之间的记忆交换与知识同步。 | **群体智能**:多个 Agent 可以共享同一套底层知识体系,同时保留各自的私有工作记忆。 | - -#### 2.4.3 与 mem0 的主要区别 - -* **Mem0** 侧重于**个性化偏好存储**(Personalization),核心是记住“用户喜欢什么”。 -* **MIRIX** 侧重于**智能体认知架构**(Agent Cognition),核心是让 Agent 具备类似人类的“知识归纳”和“逻辑推理”记忆能力。 - ---- - -### 2.5 LangMem -- https://github.com/langchain-ai/langmem -- https://langchain-ai.github.io/langmem/ - -#### 2.5.1 API Reference - -| 接口名称 | 核心输入参数 (Inputs) | 核心输出 (Outputs) | 背后逻辑 (Internal Logic) | -| --- | --- | --- | --- | -| **Add Messages** | `thread_id`, `messages` (List), `user_id` | 操作确认 / 任务 ID | **流式注入**:将原始对话追加到指定的 Thread。LangMem 会自动关联用户上下文,准备进行后续的异步处理。 | -| **Query Memory** | `user_id`, `query` (语义描述), `namespace` | 结构化记忆对象 (JSON / Text) | **多维检索**:不仅支持向量相似度搜索,还能根据定义的 Schema 返回结构化的用户画像或知识状态。 | -| **Trigger Logic** | `thread_id`, `memory_type` | 更新后的 Memory State | **异步固化**:后台启动 LLM 任务,将长篇对话“压缩”并“提取”到长期存储中。支持自定义提取逻辑(如更新用户信息)。 | -| **Manage State** | `user_id`, `patch_data` (增量更新) | 成功/失败 状态 | **精确受控**:开发者可以直接修改持久化的状态(State),支持类似于 Git 的状态管理。 | - -#### 2.5.2 Tech Strategy & Benefits - -| 维度 | 技术方案 (Technical Solution) | 核心优势 (Key Advantages) | -| --- | --- | --- | -| **存储架构** | **Stateful Persistence**:基于关系型数据库 (Postgres) + 向量索引。 | **强一致性**:利用数据库事务确保记忆更新的可靠性,支持复杂的结构化查询与过滤。 | -| **数据处理** | **异步化 Compaction (压缩)**:在对话间隙通过后台 Worker 提取知识。 | **无感延迟**:核心对话流程不被记忆提取阻塞,通过定时或事件驱动完成“记忆固化”,优化用户体验。 | -| **管理层级** | **Thread -> User -> Organization**:三层级联记忆。 | **上下文隔离**:完美适配 SaaS 应用场景,既能记住单次对话(Thread),也能沉淀用户习惯(User)。 | -| **逻辑引擎** | **Schema-Driven (模式驱动)**:允许定义 JSON Schema 来规范记忆内容。 | **高度可预测**:输出不再是散乱的句子,而是结构化的字段,方便下游程序直接调用逻辑(如自动填充表单)。 | -| **集成生态** | **LangGraph 原生集成**:作为 Checkpointer 或存储节点直接接入。 | **生态协同**:如果你已经在用 LangChain,LangMem 可以无缝接管状态流转,无需重写底层存储逻辑。 | - -#### 2.5.3 与 mem0 的核心差异 - -* **mem0** 像是一个**“便签本”**:它擅长从每一句话里抠出零散的事实(如“我喜欢吃苹果”),然后把它们存成一条条语义片段。 -* **LangMem** 像是一个**“用户档案系统”**:它更擅长分析一整段对话,然后更新一个复杂的 JSON 档案(如更新用户的偏好模型、性格标签、历史任务状态)。 - ---- - -## 三、 ReMeV2 API 接口设计 - -### 3.1 Long-Term Memory (长期记忆) - -#### 3.1.1 Basic Usage (基础用法) - -The most straightforward way to use ReMe for long-term memory management. Supports basic summary and retrieval operations. - -```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 identifier - llm={"backend": "openai", "model": "qwen-plus", "temperature": 0.6}, - embedding={"backend": "openai", "model": "text-embedding-v4", "dimension": 1024}, - vector_store={"backend": "local_file"}, # supported: local_file, chromadb, qdrant, etc. -) - -# Summarize conversation into memory -result = await memory.summary( - messages=[ - {"role": "user", "content": "I'm travelling to SF"}, - {"role": "assistant", "content": "That's great to hear!"} - ], - user_id="Alice", - # memory_type="auto" # default: auto (auto, personal, procedural, tool) -) - -# Retrieve relevant memories -memories = await memory.retrieve( - query="what is your travel plan?", - limit=3, - user_id="Alice", - # memory_type="auto" # default: auto -) -memories_str = "\n".join(f"- {m['memory']}" for m in memories["results"]) -print(memories_str) -``` - -#### 3.1.2 CLI Chat Application (命令行聊天应用) - -A complete example demonstrating how to build a memory-enhanced chatbot with CLI interface. - -```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", - llm={"backend": "openai", "model": "qwen-plus", "temperature": 0.6}, - embedding={"backend": "openai", "model": "text-embedding-v4", "dimension": 1024}, - vector_store={"backend": "local_file"}, -) - -os.environ["OPENAI_API_KEY"] = "sk-..." -os.environ["OPENAI_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1" -openai_client = OpenAI() - -def chat_with_memories( - query: str, - history_messages: list[dict], - user_name: str = "", - start_summary_size: int = 2, - keep_size: int = 0 -) -> str: - # Retrieve relevant memories for the query - memories = memory.retrieve(query=query, user_id=user_name, limit=3) - - # Build system prompt with memories - system_prompt = ( - "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.\n" - ) - if memories: - memories_str = "\n".join(f"- {m['memory']}" for m in memories["results"]) - system_prompt += f"User Memories:\n{memories_str}\n" - - # Generate response - 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}) - - # Summarize history when it gets too long - if 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("Enter your 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)}") - - # Cleanup - memory.delete_all_memories(user_id=user_name) - print("All memories deleted") - -if __name__ == "__main__": - main() -``` - -#### 3.1.3 Advanced Usage (高级用法) - -For advanced users who want to customize retriever and summarizer behavior with Agentic mode. - -```python -import os -from reme_ai import ReMe -from reme_ai.retriever import AgenticRetriever -from reme_ai.summarizer import AgenticSummarizer -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", - llm={"backend": "openai", "model": "qwen-plus", "temperature": 0.6}, - embedding={"backend": "openai", "model": "text-embedding-v4", "dimension": 1024}, - vector_store={"backend": "local_file"}, - use_agentic_mode=True, -) - -# Customize retriever and summarizer with custom tools and prompts -memory.set_retriever( - AgenticRetriever(tools=[ATool(), BTool(), CTool()]), - system_prompt="Custom retrieval instructions..." -) -memory.set_summarizer( - AgenticSummarizer(tools=[ATool(), BTool(), CTool()]) -) - -# Use the customized memory system -result = memory.summary( - messages=[ - {"role": "user", "content": "I'm travelling to SF"}, - {"role": "assistant", "content": "That's great to hear!"} - ], - user_id="Alice", - memory_type="auto", # auto, personal, procedural, tool -) - -memories = memory.retrieve( - query="what is your travel plan?", - limit=3, - user_id="Alice", - memory_type="auto", -) -memories_str = "\n".join(f"- {m['memory']}" for m in memories["results"]) -print(memories_str) -``` - -### 3.2 Short-Term Memory (短期记忆) - -#### 3.2.1 Basic Usage (基础用法) - -Context offload/reload API for managing short-term conversational memory within a session. - -```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", - llm={"backend": "openai", "model": "qwen-plus", "temperature": 0.6}, - embedding={"backend": "openai", "model": "text-embedding-v4", "dimension": 1024}, - vector_store={"backend": "local_file"}, -) - -# Offload context when conversation gets too long -result = memory.offload_context( - messages=[ - {"role": "user", "content": "I'm travelling to SF"}, - {"role": "assistant", "content": "That's great to hear!"} - ], -) - -# Reload relevant context when needed -memories = memory.reload_context( - query="what is your travel plan?", - limit=3, -) -memories_str = "\n".join(f"- {m['memory']}" for m in memories["results"]) -print(memories_str) -``` - -### 3.3 Framework Integration (框架集成) - -#### 3.3.1 Integration with AgentScope - -Integration example for AgentScope ReActAgent with long-term memory support. - -```python -# TODO: Provide AgentScope integration example -``` - -#### 3.3.2 Integration with LangChain - -Integration example for LangChain agents with ReMe memory layer. - -```python -# TODO: Provide LangChain integration example -``` - -### 3.4 OpenAI Compatible Interface - -OpenAI-compatible API interface for seamless integration with existing OpenAI-based applications. - -```python -# TODO: Research and implement OpenAI-compatible interface -# - Support for threads and assistants API -# - Compatible with OpenAI SDK -# - Support for streaming responses -``` - - - ---- - -## 四、核心方案设计 - -### 4.1 设计概述 - -ReMeV2 采用简洁的架构设计,核心理念为:**ReMeV2 = Tool(s) + Agent(s)** - -- **Tool层**:提供原子化的记忆操作能力,包括增删改查、检索、元数据管理等基础操作 -- **Agent层**:基于Tool层构建的智能代理,负责复杂的记忆管理逻辑,如分类总结、渐进式检索等 -- **Runtime层**:内部调度机制,协调Tool和Agent的交互流程 - -### 4.2 Tool层设计 - -Tool层提供装饰器形式的记忆操作工具,每个工具类通过 `@tool` 装饰器注册,明确定义初始化参数和调用参数。 - -#### 4.2.1 基类:BaseMemoryToolOp - -**初始化参数:** -- `enable_multiple` (bool): Enable multi-item operation mode. Default: `True` -- `enable_thinking_params` (bool): Include thinking parameter in tool schema for model reasoning. Default: `False` -- `memory_metadata_dir` (str): Directory path for storing memory metadata. Default: `"./memory_metadata"` - -#### 4.2.2 Tool操作列表 - -以下是所有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, 必需) | - -### 4.3 Agent层设计 - -#### 4.3.1 基类:BaseMemoryAgentOp - -Agent层构建在Tool层之上,封装复杂的记忆管理逻辑。每个Agent通过组合多个Tool实现特定的记忆管理任务。 - -#### 4.3.2 Agent操作列表 - -以下是所有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) | - -### 4.4 Runtime层设计(内部实现) - -Runtime层负责协调Tool和Agent的调用流程,实现记忆的渐进式处理。 - -#### 4.4.1 渐进式总结流程(Summary) - -总结流程采用分层处理策略,首先保存历史对话,读取元信息,然后由主Agent协调多个专用Agent完成分类总结。 - -**流程结构:** - -```python -# Step 1: Save conversation history -AddHistoryMemoryOp() - -# Step 2: Load meta information (memory types and targets) -ReadMetaMemoryOp() - -# Step 3: Progressive summarization with delegation -ReMeSummaryAgentV1Op(tools=[ - # Add meta memory entries for new memory types/targets - AddMetaMemoryOp(list(memory_type, memory_target)), - - # Add general summary memory as fallback - AddSummaryMemoryOp(summary_memory), - - # Delegate to specialized summary agents - HandsOffOp(list(memory_type, memory_target), agents=[ - PersonalSummaryAgentV1Op, # Summarize personal memories - ProceduralSummaryAgentV1Op, # Summarize procedural memories - ToolSummaryAgentV1Op, # Summarize tool-related memories - IdentitySummaryAgentV1Op # Update identity memory - ]), -]) - -# Specialized agents and their available tools -PersonalSummaryAgentV1Op(tools=[AddMemoryOp, UpdateMemoryOp, DeleteMemoryOp, VectorRetrieveMemoryOp]) -ProceduralSummaryAgentV1Op(tools=[AddMemoryOp, UpdateMemoryOp, DeleteMemoryOp, VectorRetrieveMemoryOp]) -ToolSummaryAgentV1Op(tools=[AddMemoryOp, UpdateMemoryOp, VectorRetrieveMemoryOp]) -IdentitySummaryAgentV1Op(tools=[ReadIdentityMemoryOp, UpdateIdentityMemoryOp]) -``` - -#### 4.4.2 渐进式检索流程(Retrieve) - -检索流程采用三层检索策略,类似于技能系统的加载机制,逐层加载和过滤记忆。 - -**流程结构:** - -```python -# Progressive retrieval with three layers -ReMeRetrieveAgentV1Op(tools=[ - # Layer 0: Load meta memory (all available memory types and targets) - ReadMetaMemoryOp(), - # Output format example: - # - personal(jinli): Information about Jinli's personal life and preferences - # - personal(jiaji): Information about Jiaji's background and interests - # - personal(jinli&jiaji): Shared memories between Jinli and Jiaji - # - procedural(appworld): Procedural knowledge for AppWorld tasks - # - procedural(bfcl-v3): Procedural knowledge for BFCL-v3 benchmark - # - tool(tool_guidelines): Guidelines for tool usage - # - identity(self): Agent's self-identity information - - # Layer 1+2: Vector-based retrieval on structured memories - VectorRetrieveMemoryOp(list(memory_type, memory_target, query)), - - # Layer 3: Load full conversation history for specific memory - ReadHistoryMemoryOp(ref_memory_id), -]) -``` - -**与技能系统的类比:** - -```python -# Skill system hierarchy (for reference) -load_meta_skills # Load skill metadata -load_skills # Load skill implementations -load_reference_skills # Load detailed skill documentation -execute_shell # Execute actual commands -``` - -## 五、扩展设计与实验方向 - -### 5.1 Summary Memory机制 - -Summary Memory作为通用维度的记忆类型,提供兜底的原始对话索引能力。 - -**工作流程示例:** - -```txt -Step 1: Progressive summarization across sessions - session1: List[Message] -> session2: List[Message] -> session3: List[Message] -> ... -summary ✓ (always) ✓ (always) ✓ (always) -personal ✗ ✗ ✓ (when applicable) -procedural ✗ ✓ (when applicable) ✗ - -Step 2: Retrieval with fallback strategy -vector_retrieve_memory(query, memory_type="personal", memory_target="jinli") - -> Search in memory_type: ["personal", "summary"] # Fallback to summary if personal not found -``` - -**设计优势:** -1. Provides a universal dimension for memory extraction across all memory types -2. Ensures fallback indexing of original conversations when specific meta memory is not available -3. Maintains conversation context even when specialized memory extraction fails - -### 5.2 Thinking参数实验 - -探索不同的模型推理能力增强方案,受AgentScope和Claude启发。 - -#### 5.2.1 Thinking参数设计 - -```python -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. - """ -``` - -#### 5.2.2 实验对比方案 - -| 方案类型 | 说明 | 灵感来源 | -|-------------------------------|--------------------------------------------|----------------| -| Thinking Model | Native reasoning-capable models (e.g., o1) | OpenAI | -| Instruct Model | Standard instruction-following models | Baseline | -| Instruct Model + Thinking Params | Add thinking parameter to tool schema | AgentScope | -| Instruct Model + Thinking Tool | Dedicated thinking tool for explicit reasoning | Claude | - -### 5.3 多项操作模式实验 - -对比单次调用和批量调用的性能与准确性差异。 - -**两种模式对比:** - -| 模式 | Tool调用方式 | Model调用次数 | 优势 | 劣势 | -|--------------|----------------------------|---------------|------------------------------|--------------------------| -| 单项模式 | Single-item per call | Multiple | Fine-grained control | Higher latency, more tokens | -| 多项模式 | Batch multiple items | Single | Lower latency, fewer tokens | Potential batch errors | - -**实验目标:** -- Evaluate accuracy: single vs. batch operations -- Measure latency and token efficiency -- Identify optimal use cases for each mode - -### 5.4 多版本与扩展性 - -支持从基类继承实现自定义Agent,便于团队协作和功能迭代。 - -**扩展示例:** - -```python -# Version 2 implementations by different team members -PersonalSummaryAgentV2Op / PersonalRetrieveAgentV2Op # @weikang -ProceduralSummaryAgentV2Op / ProceduralRetrieveAgentV2Op # @zouyin - -# Inherit from BaseMemoryAgentOp -class PersonalSummaryAgentV2Op(BaseMemoryAgentOp): - """Enhanced personal memory summarization with improved algorithms""" - pass -``` - -### 5.5 文件系统集成(未来方向) - -探索将文件操作能力集成到记忆系统中,支持基于文件的记忆管理。 - -**挑战与考虑:** - -1. **操作适配性**:Current operations (retrieve/add/update/delete) need adaptation for file-based storage -2. **工具选择**:Consider file operation tools: `grep`, `glob`, `ls`, `read_file`, `write_file`, `edit_file` -3. **模型能力**:Base models have limited file operation capabilities; `qwen3-code` shows better performance - -**潜在架构:** - -```python -# File-based memory operations -FileMemoryOp(tools=[ - grep, # Search within files - glob, # File pattern matching - ls, # List directory contents - read_file, # Read file contents - write_file, # Write new memory files - edit_file, # Update existing memory files -]) -``` - -### 5.6 自我修改上下文 - -支持Agent动态修改自身的上下文状态,实现自适应记忆管理。 - -**实现方式:** - -1. **Summary Agent 主动修改**: - - `add_meta_memory` directly modifies agent context - - Updates available memory types and targets during execution - -2. **ReMy Agent 被动修改**: - - Retrieves `identity_memory` at each interaction - - Dynamically updates self-state based on retrieved identity - - Enables adaptive behavior based on accumulated identity knowledge - -## ReMe V2 开发路线图与实施计划 - -### 技术改造阶段 -1. **代码整合与兼容**:合并flowllm中reme必要的代码,保留现在server-client的依赖,兼容现在各个仓库的依赖代码 -2. **核心接口重构**:新的ReMe接口设计,支持summary,retrieve,context_offload, context_reload 4个核心接口 -3. **Agentic算法升级**:新的agentic算法方案开发 - -### 评估验证阶段 -4. **Benchmark测试** - - halumem - - locomo - - longmemevel - - personal-v2 ? - - appworld/bfcl-v3 - -### 发布推广阶段 -5. **技术报告**撰写与发布 -6. **生态更新**:更新各个仓库的依赖代码 - - agentscope - - agentscope-runtime - - evotraders - - alias(tool-memory) - - agentscope-java - - AgentEvolver - - cookbook: reme procedural memory paper - - tool-memory-upgrade(将要合并) - -**里程碑目标**:春节前完成小版本发布 - ---- - -## ReMe V2 核心竞争优势 - -### 1. 渐进式 Agentic Memory 架构【核心创新】 -融合了多种记忆的渐进式agentic方案,实现从短期到长期记忆的智能化演进 - -### 2. 全生命周期记忆管理 -同时支持长期记忆(Long-term Memory)和短期记忆(Working Memory),完整覆盖Agent认知周期 - -### 3. 模型 -提供开源小模型 - -### 4. 开发者友好生态 - 1. **简洁接口**:提供简洁的接口设计,全异步接口 - 2. **即开即用**:提供CLI工具,开箱即用的体验 - 3. **生态融合**:提供和AgentScope、LangChain无缝集成的方案 - 4. **高度可扩展**:支持Agentic算法的二次开发与定制 \ No newline at end of file diff --git a/docs/todo.md b/docs/todo.md deleted file mode 100644 index 5d73e04d..00000000 --- a/docs/todo.md +++ /dev/null @@ -1,3 +0,0 @@ -1. 如何更好的注册class -2. op的返回,使用return 还是 self.output -3. 如何把agent的东西放出来 \ No newline at end of file diff --git a/example.env b/example.env index 5f39a48d..d1a27415 100644 --- a/example.env +++ b/example.env @@ -2,6 +2,4 @@ LLM_API_KEY=sk-xxxx LLM_BASE_URL=https://xxxx/v1 #EMBEDDING_API_KEY=sk-xxxx #EMBEDDING_BASE_URL=https://xxxx/v1 -LLM_MODEL_NAME=qwen3.5-plus - #TAVILY_API_KEY=xxxx diff --git a/reme/memory/__init__.py b/reme/memory/__init__.py index ac825bc6..e209a3fe 100644 --- a/reme/memory/__init__.py +++ b/reme/memory/__init__.py @@ -1,11 +1,11 @@ """memory""" from . import file_based -from . import tools +from . import vector_tools from . import vector_based __all__ = [ "file_based", - "tools", + "vector_tools", "vector_based", ] diff --git a/reme/memory/file_based/__init__.py b/reme/memory/file_based/__init__.py index 2e01cc41..766f9b3e 100644 --- a/reme/memory/file_based/__init__.py +++ b/reme/memory/file_based/__init__.py @@ -9,18 +9,21 @@ Components: - Summarizer: Generates memory summaries using LLM - Compactor: Compacts memory content to reduce token usage - ToolResultCompactor: Truncates large tool results and saves full content to files + - ContextChecker: Checks context size and splits messages for compaction """ from .as_msg_handler import AsMsgHandler -from .reme_in_memory_memory import ReMeInMemoryMemory from .component.compactor import Compactor +from .component.context_checker import ContextChecker from .component.summarizer import Summarizer from .component.tool_result_compactor import ToolResultCompactor +from .reme_in_memory_memory import ReMeInMemoryMemory __all__ = [ "AsMsgHandler", "ReMeInMemoryMemory", "Summarizer", "Compactor", + "ContextChecker", "ToolResultCompactor", ] diff --git a/reme/memory/file_based/component/context_checker.py b/reme/memory/file_based/component/context_checker.py new file mode 100644 index 00000000..82bb381e --- /dev/null +++ b/reme/memory/file_based/component/context_checker.py @@ -0,0 +1,97 @@ +"""ContextChecker module for checking context size and splitting messages.""" + +from agentscope.message import Msg +from agentscope.token import HuggingFaceTokenCounter + +from ..as_msg_handler import AsMsgHandler +from ....core.op import BaseOp +from ....core.utils import get_std_logger + +logger = get_std_logger() + + +class ContextChecker(BaseOp): + """ + ContextChecker class for checking context size and splitting messages. + + This class analyzes conversation messages to determine if the context + exceeds the specified token threshold and splits messages into two groups: + those that should be compacted and those to keep in context. + + Attributes: + memory_compact_threshold (int): Token count threshold for triggering compaction. + memory_compact_reserve (int): Token count to reserve for recent messages. + msg_handler (AsMsgHandler): Handler for message processing and token counting. + """ + + def __init__( + self, + memory_compact_threshold: int, + memory_compact_reserve: int = 10000, + token_counter: HuggingFaceTokenCounter | None = None, + **kwargs, + ): + """ + Initialize the ContextChecker. + + Args: + memory_compact_threshold (int): Token count threshold for triggering + compaction. Messages exceeding this threshold will be split. + memory_compact_reserve (int): Token count to reserve for recent messages + to keep in context. Defaults to 10000 tokens. + token_counter (HuggingFaceTokenCounter | None): Token counter for + measuring content length. If None, a default counter will be used. + **kwargs: Additional keyword arguments passed to BaseOp. + """ + super().__init__(**kwargs) + self.memory_compact_threshold: int = memory_compact_threshold + self.memory_compact_reserve: int = memory_compact_reserve + assert self.memory_compact_threshold > self.memory_compact_reserve + + self.msg_handler = AsMsgHandler(token_counter=token_counter) + + async def execute(self) -> tuple[list[Msg], list[Msg], bool]: + """ + Execute context check and split messages. + + Retrieves messages from context and checks if they exceed the token + threshold. If so, splits them into messages to compact and messages + to keep. + + Context Parameters: + messages (list[Msg]): List of conversation messages to check. + Retrieved from self.context.get("messages", []). + + Returns: + tuple[list[Msg], list[Msg], bool]: A tuple containing: + - messages_to_compact (list[Msg]): Older messages that should + be compacted/summarized. + - messages_to_keep (list[Msg]): Recent messages to keep in context. + - is_valid (bool): True if the split is valid (tool calls aligned), + False if splitting would break conversation integrity. + + Note: + - Returns ([], messages, True) if no compaction is needed. + - Ensures conversation pairs (user-assistant) are not split. + - is_valid=False indicates tool_use and tool_result are misaligned. + """ + messages: list[Msg] = self.context.get("messages", []) + + if not messages: + logger.info("ContextChecker: No messages to check.") + return [], [], True + + messages_to_compact, messages_to_keep, is_valid = self.msg_handler.context_check( + messages=messages, + memory_compact_threshold=self.memory_compact_threshold, + memory_compact_reserve=self.memory_compact_reserve, + ) + + logger.info( + f"ContextChecker Result: " + f"to_compact={len(messages_to_compact)}, " + f"to_keep={len(messages_to_keep)}, " + f"is_valid={is_valid}", + ) + + return messages_to_compact, messages_to_keep, is_valid diff --git a/reme/memory/file_based/tools/__init__.py b/reme/memory/file_based/tools/__init__.py new file mode 100644 index 00000000..0fb0d814 --- /dev/null +++ b/reme/memory/file_based/tools/__init__.py @@ -0,0 +1,13 @@ +"""File-based memory tool implementations.""" + +from .file_io import FileIO +from .memory_get import MemoryGet +from .memory_search import MemorySearch +from .shell import Shell + +__all__ = [ + "FileIO", + "MemoryGet", + "MemorySearch", + "Shell", +] diff --git a/reme/memory/tools/file/file_io.py b/reme/memory/file_based/tools/file_io.py similarity index 77% rename from reme/memory/tools/file/file_io.py rename to reme/memory/file_based/tools/file_io.py index 161c3210..58e9a5f7 100644 --- a/reme/memory/tools/file/file_io.py +++ b/reme/memory/file_based/tools/file_io.py @@ -7,6 +7,8 @@ from typing import Optional from agentscope.message import TextBlock from agentscope.tool import ToolResponse +from .utils import DEFAULT_MAX_BYTES, read_file_safe, truncate_output + class FileIO: """File I/O operations with a configurable working directory.""" @@ -78,57 +80,64 @@ class FileIO: ) try: - with open(file_path, "r", encoding="utf-8") as f: - all_lines = f.readlines() + content = read_file_safe(file_path) + all_lines = content.split("\n") + total = len(all_lines) - range_requested = start_line is not None or end_line is not None + # Determine read range + s = max(1, start_line if start_line is not None else 1) + e = min(total, end_line if end_line is not None else total) - if range_requested: - total = len(all_lines) - s = max(1, start_line if start_line is not None else 1) - e = min(total, end_line if end_line is not None else total) - - if s > total: - return ToolResponse( - content=[ - TextBlock( - type="text", - text=(f"Error: start_line {s} exceeds file length " f"({total} lines) in {file_path}."), - ), - ], - ) - - if s > e: - return ToolResponse( - content=[ - TextBlock( - type="text", - text=(f"Error: start_line ({s}) is greater than " f"end_line ({e}) in {file_path}."), - ), - ], - ) - - selected = all_lines[s - 1 : e] - content = "".join(selected) - header = f"{file_path} (lines {s}-{e} of {total})\n" + if s > total: return ToolResponse( content=[ TextBlock( type="text", - text=header + content, + text=f"Error: start_line {s} exceeds file length ({total} lines).", ), ], ) + + if s > e: + return ToolResponse( + content=[ + TextBlock( + type="text", + text=f"Error: start_line ({s}) > end_line ({e}).", + ), + ], + ) + + # Extract selected lines + selected_content = "\n".join(all_lines[s - 1 : e]) + + # Apply smart truncation (keep head for file reading) + truncated, was_truncated, output_lines, reason = truncate_output(selected_content, keep="head") + + # Build response with truncation hints + if was_truncated: + end_display = s + output_lines - 1 + next_line = end_display + 1 + if reason == "lines": + hint = f"\n\n[Lines {s}-{end_display} of {total}. Use start_line={next_line} to continue.]" + else: + hint = ( + f"\n\n[Lines {s}-{end_display} of {total} ({DEFAULT_MAX_BYTES // 1024}KB limit). " + f"Use start_line={next_line} to continue.]" + ) + text = truncated + hint + elif e < total: + remaining = total - e + text = ( + f"{file_path} (lines {s}-{e} of {total})\n{truncated}\n\n[{remaining} more lines. " + f"Use start_line={e + 1} to continue.]" + ) else: - content = "".join(all_lines) - return ToolResponse( - content=[ - TextBlock( - type="text", - text=content, - ), - ], - ) + text = truncated + + return ToolResponse( + content=[TextBlock(type="text", text=text)], + ) except Exception as e: return ToolResponse( diff --git a/reme/memory/tools/chunk/memory_get.py b/reme/memory/file_based/tools/memory_get.py similarity index 100% rename from reme/memory/tools/chunk/memory_get.py rename to reme/memory/file_based/tools/memory_get.py diff --git a/reme/memory/tools/chunk/memory_search.py b/reme/memory/file_based/tools/memory_search.py similarity index 100% rename from reme/memory/tools/chunk/memory_search.py rename to reme/memory/file_based/tools/memory_search.py diff --git a/reme/memory/file_based/tools/shell.py b/reme/memory/file_based/tools/shell.py new file mode 100644 index 00000000..c2714b87 --- /dev/null +++ b/reme/memory/file_based/tools/shell.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +# flake8: noqa: E501 +# pylint: disable=line-too-long +"""The shell command tool.""" + +import asyncio +import locale +import subprocess +import sys +from pathlib import Path + +from agentscope.message import TextBlock +from agentscope.tool import ToolResponse + +from .utils import truncate_shell_output + + +def _execute_subprocess_sync( + cmd: str, + cwd: str, + timeout: int, +) -> tuple[int, str, str]: + """Execute subprocess synchronously in a thread. + + This function runs in a separate thread to avoid Windows asyncio + subprocess limitations. + + Args: + cmd (`str`): + The shell command to execute. + cwd (`str`): + The working directory for the command execution. + timeout (`int`): + The maximum time (in seconds) allowed for the command to run. + + Returns: + `tuple[int, str, str]`: + A tuple containing the return code, standard output, and + standard error of the executed command. If timeout occurs, the + return code will be -1 and stderr will contain timeout information. + """ + try: + result = subprocess.run( + cmd, + shell=True, + capture_output=True, + text=True, + cwd=cwd, + timeout=timeout, + encoding=locale.getpreferredencoding(False) or "utf-8", + errors="replace", + check=True, + ) + return ( + result.returncode, + result.stdout.strip("\n"), + result.stderr.strip("\n"), + ) + except subprocess.TimeoutExpired: + return ( + -1, + "", + f"Command execution exceeded the timeout of {timeout} seconds.", + ) + except Exception as e: + return -1, "", str(e) + + +class Shell: + """Shell command execution with a configurable working directory.""" + + def __init__(self, working_dir: str | Path): + """Initialize Shell with a working directory. + + Args: + working_dir (`str | Path`): + The working directory for command execution. + """ + self.working_dir = Path(working_dir) + + # pylint: disable=too-many-branches, too-many-statements + async def execute_shell_command( + self, + command: str, + timeout: int = 60, + ) -> ToolResponse: + """Execute given command and return the return code, standard output and + error within , and + tags. + + Args: + command (`str`): + The shell command to execute. + timeout (`int`, defaults to `60`): + The maximum time (in seconds) allowed for the command to run. + Default is 60 seconds. + + Returns: + `ToolResponse`: + The tool response containing the return code, standard output, and + standard error of the executed command. If timeout occurs, the + return code will be -1 and stderr will contain timeout information. + """ + + cmd = (command or "").strip() + + # Set working directory + working_dir = self.working_dir + + try: + if sys.platform == "win32": + # Windows: use thread pool to avoid asyncio subprocess limitations + returncode, stdout_str, stderr_str = await asyncio.to_thread( + _execute_subprocess_sync, + cmd, + str(working_dir), + timeout, + ) + else: + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + bufsize=0, + cwd=str(working_dir), + ) + + try: + # Apply timeout to communicate directly; wait()+communicate() + # can hang if descendants keep stdout/stderr pipes open. + stdout, stderr = await asyncio.wait_for( + proc.communicate(), + timeout=timeout, + ) + encoding = locale.getpreferredencoding(False) or "utf-8" + stdout_str = stdout.decode(encoding, errors="replace").strip( + "\n", + ) + stderr_str = stderr.decode(encoding, errors="replace").strip( + "\n", + ) + returncode = proc.returncode + + except asyncio.TimeoutError: + # Handle timeout + stderr_suffix = ( + f"⚠️ TimeoutError: The command execution exceeded " + f"the timeout of {timeout} seconds. " + f"Please consider increasing the timeout value if this command " + f"requires more time to complete." + ) + returncode = -1 + try: + proc.terminate() + # Wait a bit for graceful termination + try: + await asyncio.wait_for(proc.wait(), timeout=1) + except asyncio.TimeoutError: + # Force kill if graceful termination fails + proc.kill() + await proc.wait() + + # Avoid hanging forever while draining pipes after timeout. + try: + stdout, stderr = await asyncio.wait_for( + proc.communicate(), + timeout=1, + ) + except asyncio.TimeoutError: + stdout, stderr = b"", b"" + encoding = locale.getpreferredencoding(False) or "utf-8" + stdout_str = stdout.decode( + encoding, + errors="replace", + ).strip( + "\n", + ) + stderr_str = stderr.decode( + encoding, + errors="replace", + ).strip( + "\n", + ) + if stderr_str: + stderr_str += f"\n{stderr_suffix}" + else: + stderr_str = stderr_suffix + except ProcessLookupError: + stdout_str = "" + stderr_str = stderr_suffix + + # Apply output truncation + stdout_str = truncate_shell_output(stdout_str) + stderr_str = truncate_shell_output(stderr_str) + + # Format the response in a human-friendly way + if returncode == 0: + # Success case: just show the output + if stdout_str: + response_text = stdout_str + else: + response_text = "Command executed successfully (no output)." + else: + # Error case: show detailed information + response_parts = [f"Command failed with exit code {returncode}."] + if stdout_str: + response_parts.append(f"\n[stdout]\n{stdout_str}") + if stderr_str: + response_parts.append(f"\n[stderr]\n{stderr_str}") + response_text = "".join(response_parts) + + return ToolResponse( + content=[ + TextBlock( + type="text", + text=response_text, + ), + ], + ) + + except Exception as e: + return ToolResponse( + content=[ + TextBlock( + type="text", + text=f"Error: Shell command execution failed due to \n{e}", + ), + ], + ) diff --git a/reme/memory/file_based/tools/utils.py b/reme/memory/file_based/tools/utils.py new file mode 100644 index 00000000..17f58877 --- /dev/null +++ b/reme/memory/file_based/tools/utils.py @@ -0,0 +1,112 @@ +"""Shared utilities for file and shell tools.""" + +# Default truncation limits +DEFAULT_MAX_LINES = 1000 +DEFAULT_MAX_BYTES = 30 * 1024 # 30KB + + +def truncate_output( + text: str, + max_lines: int = DEFAULT_MAX_LINES, + max_bytes: int = DEFAULT_MAX_BYTES, + keep: str = "head", +) -> tuple[str, bool, int, str]: + """Smart truncation for large content. + + Args: + text: Text content to truncate. + max_lines: Maximum number of lines. + max_bytes: Maximum size in bytes. + keep: Which part to keep - "head" (first lines) or "tail" (last lines). + + Returns: + (truncated_content, was_truncated, output_line_count, truncate_reason) + """ + if not text: + return text, False, 0, "" + + lines = text.split("\n") + total_lines = len(lines) + + # No truncation needed + if total_lines <= max_lines and len(text.encode("utf-8")) <= max_bytes: + return text, False, total_lines, "" + + # Apply line limit + if total_lines > max_lines: + if keep == "tail": + lines = lines[-max_lines:] + else: + lines = lines[:max_lines] + reason = "lines" + else: + reason = "" + + # Apply byte limit + if len("\n".join(lines).encode("utf-8")) > max_bytes: + if keep == "tail": + while lines and len("\n".join(lines).encode("utf-8")) > max_bytes: + lines.pop(0) + else: + truncated = [] + current_bytes = 0 + for line in lines: + line_bytes = len(line.encode("utf-8")) + 1 + if current_bytes + line_bytes > max_bytes: + break + truncated.append(line) + current_bytes += line_bytes + lines = truncated + reason = "bytes" + + return "\n".join(lines), True, len(lines), reason + + +def truncate_shell_output(text: str) -> str: + """Truncate shell output to last N lines or M bytes, with truncation notice. + + Args: + text: The output text to truncate. + + Returns: + Truncated text with notice if truncated. + """ + if not text: + return text + + try: + total_lines = len(text.split("\n")) + truncated, was_truncated, output_lines, reason = truncate_output(text, keep="tail") + + if not was_truncated: + return text + + start_line = total_lines - output_lines + 1 + if reason == "lines": + notice = f"\n\n[Output truncated: showing lines {start_line}-{total_lines} of {total_lines} total]" + else: + notice = ( + f"\n\n[Output truncated: showing lines {start_line}-{total_lines} of {total_lines} " + f"({DEFAULT_MAX_BYTES // 1024}KB limit)]" + ) + + return truncated + notice + except Exception: + return text + + +def read_file_safe(file_path: str) -> str: + """Read file with Unicode error handling. + + Args: + file_path: Path to the file. + + Returns: + File content as string. + """ + try: + with open(file_path, "r", encoding="utf-8") as f: + return f.read() + except UnicodeDecodeError: + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + return f.read() diff --git a/reme/memory/tools/file/__init__.py b/reme/memory/tools/file/__init__.py deleted file mode 100644 index 8234e60d..00000000 --- a/reme/memory/tools/file/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""File-based memory tool implementations.""" - -from .file_io import FileIO - -__all__ = [ - "FileIO", -] diff --git a/reme/memory/tools/record/__init__.py b/reme/memory/tools/record/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/reme/memory/tools/__init__.py b/reme/memory/vector_tools/__init__.py similarity index 92% rename from reme/memory/tools/__init__.py rename to reme/memory/vector_tools/__init__.py index af9b851f..fd5e721f 100644 --- a/reme/memory/tools/__init__.py +++ b/reme/memory/vector_tools/__init__.py @@ -3,8 +3,6 @@ from .base_memory_tool import BaseMemoryTool # chunk tools -from .chunk.memory_get import MemoryGet -from .chunk.memory_search import MemorySearch from .delegate_task import DelegateTask # history tools @@ -36,9 +34,6 @@ __all__ = [ # base "BaseMemoryTool", "DelegateTask", - # chunk tools - "MemoryGet", - "MemorySearch", # history tools "AddHistory", "ReadHistory", diff --git a/reme/memory/tools/base_memory_tool.py b/reme/memory/vector_tools/base_memory_tool.py similarity index 100% rename from reme/memory/tools/base_memory_tool.py rename to reme/memory/vector_tools/base_memory_tool.py diff --git a/reme/memory/tools/delegate_task.py b/reme/memory/vector_tools/delegate_task.py similarity index 100% rename from reme/memory/tools/delegate_task.py rename to reme/memory/vector_tools/delegate_task.py diff --git a/cookbook/__init__.py b/reme/memory/vector_tools/history/__init__.py similarity index 100% rename from cookbook/__init__.py rename to reme/memory/vector_tools/history/__init__.py diff --git a/reme/memory/tools/history/add_history.py b/reme/memory/vector_tools/history/add_history.py similarity index 100% rename from reme/memory/tools/history/add_history.py rename to reme/memory/vector_tools/history/add_history.py diff --git a/reme/memory/tools/history/read_history.py b/reme/memory/vector_tools/history/read_history.py similarity index 100% rename from reme/memory/tools/history/read_history.py rename to reme/memory/vector_tools/history/read_history.py diff --git a/reme/memory/tools/history/read_history_v2.py b/reme/memory/vector_tools/history/read_history_v2.py similarity index 100% rename from reme/memory/tools/history/read_history_v2.py rename to reme/memory/vector_tools/history/read_history_v2.py diff --git a/cookbook/appworld/__init__.py b/reme/memory/vector_tools/profiles/__init__.py similarity index 100% rename from cookbook/appworld/__init__.py rename to reme/memory/vector_tools/profiles/__init__.py diff --git a/reme/memory/tools/profiles/add_draft_and_read_all_profiles.py b/reme/memory/vector_tools/profiles/add_draft_and_read_all_profiles.py similarity index 100% rename from reme/memory/tools/profiles/add_draft_and_read_all_profiles.py rename to reme/memory/vector_tools/profiles/add_draft_and_read_all_profiles.py diff --git a/reme/memory/tools/profiles/add_profile.py b/reme/memory/vector_tools/profiles/add_profile.py similarity index 100% rename from reme/memory/tools/profiles/add_profile.py rename to reme/memory/vector_tools/profiles/add_profile.py diff --git a/reme/memory/tools/profiles/delete_profile.py b/reme/memory/vector_tools/profiles/delete_profile.py similarity index 100% rename from reme/memory/tools/profiles/delete_profile.py rename to reme/memory/vector_tools/profiles/delete_profile.py diff --git a/reme/memory/tools/profiles/profile_handler.py b/reme/memory/vector_tools/profiles/profile_handler.py similarity index 100% rename from reme/memory/tools/profiles/profile_handler.py rename to reme/memory/vector_tools/profiles/profile_handler.py diff --git a/reme/memory/tools/profiles/read_all_profiles.py b/reme/memory/vector_tools/profiles/read_all_profiles.py similarity index 100% rename from reme/memory/tools/profiles/read_all_profiles.py rename to reme/memory/vector_tools/profiles/read_all_profiles.py diff --git a/reme/memory/tools/profiles/update_profile.py b/reme/memory/vector_tools/profiles/update_profile.py similarity index 100% rename from reme/memory/tools/profiles/update_profile.py rename to reme/memory/vector_tools/profiles/update_profile.py diff --git a/reme/memory/tools/profiles/update_profiles_v1.py b/reme/memory/vector_tools/profiles/update_profiles_v1.py similarity index 100% rename from reme/memory/tools/profiles/update_profiles_v1.py rename to reme/memory/vector_tools/profiles/update_profiles_v1.py diff --git a/cookbook/bfcl/__init__.py b/reme/memory/vector_tools/record/__init__.py similarity index 100% rename from cookbook/bfcl/__init__.py rename to reme/memory/vector_tools/record/__init__.py diff --git a/reme/memory/tools/record/add_and_retrieve_similar_memory.py b/reme/memory/vector_tools/record/add_and_retrieve_similar_memory.py similarity index 100% rename from reme/memory/tools/record/add_and_retrieve_similar_memory.py rename to reme/memory/vector_tools/record/add_and_retrieve_similar_memory.py diff --git a/reme/memory/tools/record/add_draft_and_retrieve_similar_memory.py b/reme/memory/vector_tools/record/add_draft_and_retrieve_similar_memory.py similarity index 100% rename from reme/memory/tools/record/add_draft_and_retrieve_similar_memory.py rename to reme/memory/vector_tools/record/add_draft_and_retrieve_similar_memory.py diff --git a/reme/memory/tools/record/add_memory.py b/reme/memory/vector_tools/record/add_memory.py similarity index 100% rename from reme/memory/tools/record/add_memory.py rename to reme/memory/vector_tools/record/add_memory.py diff --git a/reme/memory/tools/record/delete_memory.py b/reme/memory/vector_tools/record/delete_memory.py similarity index 100% rename from reme/memory/tools/record/delete_memory.py rename to reme/memory/vector_tools/record/delete_memory.py diff --git a/reme/memory/tools/record/memory_handler.py b/reme/memory/vector_tools/record/memory_handler.py similarity index 100% rename from reme/memory/tools/record/memory_handler.py rename to reme/memory/vector_tools/record/memory_handler.py diff --git a/reme/memory/tools/record/retrieve_memory.py b/reme/memory/vector_tools/record/retrieve_memory.py similarity index 100% rename from reme/memory/tools/record/retrieve_memory.py rename to reme/memory/vector_tools/record/retrieve_memory.py diff --git a/reme/memory/tools/record/retrieve_recent_memory.py b/reme/memory/vector_tools/record/retrieve_recent_memory.py similarity index 100% rename from reme/memory/tools/record/retrieve_recent_memory.py rename to reme/memory/vector_tools/record/retrieve_recent_memory.py diff --git a/reme/memory/tools/record/update_memory.py b/reme/memory/vector_tools/record/update_memory.py similarity index 100% rename from reme/memory/tools/record/update_memory.py rename to reme/memory/vector_tools/record/update_memory.py diff --git a/reme/memory/tools/record/update_memory_v1.py b/reme/memory/vector_tools/record/update_memory_v1.py similarity index 100% rename from reme/memory/tools/record/update_memory_v1.py rename to reme/memory/vector_tools/record/update_memory_v1.py diff --git a/reme/memory/tools/record/update_memory_v2.py b/reme/memory/vector_tools/record/update_memory_v2.py similarity index 100% rename from reme/memory/tools/record/update_memory_v2.py rename to reme/memory/vector_tools/record/update_memory_v2.py diff --git a/reme/reme.py b/reme/reme.py index 6c5525ee..685cdcdd 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -7,7 +7,7 @@ from .config import ReMeConfigParser from .core import Application from .core.enumeration import MemoryType, Role from .core.schema import Message, MemoryNode -from .memory.tools import ( +from .memory.vector_tools import ( AddDraftAndRetrieveSimilarMemory, AddHistory, AddMemory, @@ -17,8 +17,8 @@ from .memory.tools import ( RetrieveMemory, UpdateProfilesV1, ) -from .memory.tools.profiles.profile_handler import ProfileHandler -from .memory.tools.record.memory_handler import MemoryHandler +from .memory.vector_tools.profiles.profile_handler import ProfileHandler +from .memory.vector_tools.record.memory_handler import MemoryHandler from .memory.vector_based import ( BaseMemoryAgent, PersonalRetriever, @@ -185,7 +185,7 @@ class ReMe(Application): format_messages.append(message) if version == "default": - personal_summarizer_tools = [ + personal_summarizer_tools: list = [ AddDraftAndRetrieveSimilarMemory( enable_thinking_params=enable_thinking_params, enable_memory_target=False, diff --git a/reme/reme_light.py b/reme/reme_light.py index 5266ae76..922b846b 100644 --- a/reme/reme_light.py +++ b/reme/reme_light.py @@ -26,15 +26,45 @@ from agentscope.tool import Toolkit, ToolResponse from .config import ReMeConfigParser from .core import Application from .core.utils import get_hf_token_counter, get_std_logger -from .memory.file_based import Compactor, Summarizer, ToolResultCompactor, ReMeInMemoryMemory, AsMsgHandler -from .memory.tools import MemorySearch -from .memory.tools.file import FileIO +from .memory.file_based import ( + Compactor, + ContextChecker, + Summarizer, + ToolResultCompactor, + ReMeInMemoryMemory, + AsMsgHandler, +) +from .memory.file_based import MemorySearch +from .memory.file_based.tools import FileIO logger = get_std_logger() class ReMeLight(Application): - """ReMe Light Application Class""" + """ + ReMe Light Application Class. + + A lightweight memory-enabled application that provides semantic search, + memory compaction, summarization, and tool result management capabilities. + Built on top of the core Application framework with integrated vector store + and file-based memory management. + + This class is designed for applications requiring: + - Long conversation memory management with automatic compaction + - Semantic search over stored memories using hybrid vector/text search + - Background summarization of conversation history + - Automatic cleanup of expired tool results + + Attributes: + working_path (Path): Absolute path to the working directory. + memory_path (Path): Path to the memory storage directory. + tool_result_path (Path): Path to the tool result storage directory. + vector_weight (float): Weight for vector search in hybrid search (0-1). + candidate_multiplier (float): Multiplier for candidate retrieval count. + tool_result_threshold (int): Character threshold for tool result compaction. + retention_days (int): Number of days to retain tool result files. + summary_tasks (list[asyncio.Task]): List of active background summary tasks. + """ def __init__( self, @@ -51,6 +81,47 @@ class ReMeLight(Application): tool_result_threshold: int = 1000, retention_days: int = 7, ): + """ + Initialize the ReMeLight application. + + Sets up the working directory structure, configures API connections, + and initializes memory management components. + + Args: + working_dir (str): Base directory for all application data storage. + Defaults to ".reme". Will be created if it doesn't exist. + llm_api_key (str | None): API key for the language model service. + If None, will attempt to use environment variables. + llm_base_url (str | None): Base URL for the language model API endpoint. + If None, will use the default endpoint. + embedding_api_key (str | None): API key for the embedding model service. + If None, will attempt to use environment variables. + embedding_base_url (str | None): Base URL for the embedding API endpoint. + If None, will use the default endpoint. + default_as_llm_config (dict | None): Default configuration dictionary + for AgentScope language model. Overrides default settings. + default_embedding_model_config (dict | None): Default configuration + dictionary for the embedding model. + default_file_store_config (dict | None): Default configuration + dictionary for the file storage backend. + vector_weight (float): Weight assigned to vector similarity search + in hybrid search operations. Range [0.0, 1.0], default 0.7. + Higher values prioritize semantic similarity over keyword matching. + candidate_multiplier (float): Multiplier applied to max_results when + retrieving candidates for re-ranking. Default 3.0 means 3x more + candidates are retrieved than the final result count. + tool_result_threshold (int): Character count threshold for tool result + compaction. Results exceeding this length will be truncated and + saved to files. Default 1000 characters. + retention_days (int): Number of days to retain tool result files + before automatic cleanup. Default 7 days. + + Note: + The following directory structure will be created: + - {working_dir}/ - Root working directory + - {working_dir}/memory/ - Memory storage files + - {working_dir}/tool_result/ - Compacted tool result files + """ # Initialize working directory structure self.working_path = Path(working_dir).absolute() self.working_path.mkdir(parents=True, exist_ok=True) @@ -129,18 +200,64 @@ class ReMeLight(Application): return 0 async def start(self): - """Start the application lifecycle.""" + """ + Start the application lifecycle. + + Initializes all application components by calling the parent class start + method, then performs initial cleanup of expired tool result files. + + Returns: + The result from the parent Application.start() method. + + Note: + This method should be called before using any other application + functionality. It ensures all services are properly initialized. + """ result = await super().start() + # Perform initial cleanup of any expired tool result files self._cleanup_tool_results() return result async def close(self) -> bool: - """Close the application and perform cleanup.""" + """ + Close the application and perform cleanup. + + Performs final cleanup of expired tool result files and then shuts down + all application components by calling the parent class close method. + + Returns: + bool: True if the application was closed successfully, False otherwise. + + Note: + This method should be called when the application is no longer needed + to ensure proper resource cleanup and data persistence. + """ + # Final cleanup of expired tool result files before shutdown self._cleanup_tool_results() return await super().close() async def compact_tool_result(self, messages: list[Msg]) -> list[Msg]: - """Compact tool results by truncating large outputs and saving full content to files.""" + """ + Compact tool results by truncating large outputs and saving full content to files. + + This method processes a list of messages containing tool results and compacts + any that exceed the configured threshold. Large tool outputs are truncated + in the message while the full content is saved to separate files for later + retrieval if needed. + + Args: + messages (list[Msg]): List of messages potentially containing tool results + that may need compaction. + + Returns: + list[Msg]: The processed list of messages with large tool results compacted. + If an error occurs, returns the original unmodified messages. + + Note: + - Tool results shorter than tool_result_threshold are left unchanged + - Full content of truncated results is saved to tool_result_path + - Expired files are automatically cleaned up during this operation + """ try: # Create compactor with instance configuration compactor = ToolResultCompactor( @@ -162,6 +279,61 @@ class ReMeLight(Application): logger.exception(f"Error compacting tool results: {e}") return messages + async def check_context( + self, + messages: list[Msg], + memory_compact_threshold: int, + memory_compact_reserve: int = 10000, + token_counter: HuggingFaceTokenCounter | None = None, + ) -> tuple[list[Msg], list[Msg], bool]: + """ + Check context size and determine if compaction is needed. + + Analyzes the provided messages to determine if they exceed the configured + token threshold and splits them into two groups: messages that should be + compacted and messages to keep in context. + + Args: + messages (list[Msg]): List of messages to check for context overflow. + memory_compact_threshold (int): Token count threshold for triggering + compaction. Messages exceeding this threshold will be split. + memory_compact_reserve (int): Token count to reserve for recent messages + to keep in context. Defaults to 10000 tokens. + token_counter (HuggingFaceTokenCounter | None): Token counter for + measuring message length. If None, uses default HuggingFace counter. + + Returns: + tuple[list[Msg], list[Msg], bool]: A tuple containing: + - messages_to_compact (list[Msg]): Older messages that should + be compacted/summarized. + - messages_to_keep (list[Msg]): Recent messages to keep in context. + - is_valid (bool): True if the split is valid (tool calls aligned), + False if splitting would break conversation integrity. + + Note: + - Returns ([], messages, True) if no compaction is needed. + - Ensures conversation pairs (user-assistant) are not split. + - is_valid=False indicates tool_use and tool_result are misaligned. + """ + try: + if token_counter is None: + token_counter = get_hf_token_counter() + + checker = ContextChecker( + memory_compact_threshold=memory_compact_threshold, + memory_compact_reserve=memory_compact_reserve, + token_counter=token_counter, + ) + + return await checker.call( + messages=messages, + service_context=self.service_context, + ) + + except Exception as e: + logger.exception(f"Error checking context: {e}") + return [], messages, False + async def compact_memory( self, messages: list[Msg], @@ -173,7 +345,34 @@ class ReMeLight(Application): compact_ratio: float = 0.7, previous_summary: str = "", ) -> str: - """Compact a list of messages into a condensed summary.""" + """ + Compact a list of messages into a condensed summary. + + Uses the configured language model to generate a concise summary of the + provided messages. This is useful for reducing context window usage while + preserving important information from the conversation history. + + Args: + messages (list[Msg]): List of messages to be compacted into a summary. + as_llm (str | ChatModelBase): Language model identifier or instance + to use for summarization. Defaults to "default". + as_llm_formatter (str | FormatterBase): Formatter for the language model. + Defaults to "default". + token_counter (HuggingFaceTokenCounter | None): Token counter for + measuring message length. If None, uses default HuggingFace counter. + language (str): Language for the summary output. "zh" for Chinese, + any other value for English. Defaults to "zh". + max_input_length (float): Maximum input length in tokens for the model. + Defaults to 128K tokens. + compact_ratio (float): Ratio used to calculate compaction threshold. + Defaults to 0.7. + previous_summary (str): Previous summary to incorporate into the new + summary for continuity. Defaults to empty string. + + Returns: + str: The condensed summary of the messages, or an empty string if + an error occurred during compaction. + """ try: if token_counter is None: token_counter = get_hf_token_counter() @@ -208,7 +407,37 @@ class ReMeLight(Application): max_input_length: float = 128 * 1024, compact_ratio: float = 0.7, ) -> str: - """Generate a comprehensive summary of the given messages.""" + """ + Generate a comprehensive summary of the given messages. + + Creates a detailed summary of the conversation history and persists it + to the memory directory as structured files. Unlike compact_memory, this + method produces more detailed summaries suitable for long-term storage. + + Args: + messages (list[Msg]): List of messages to summarize. + as_llm (str | ChatModelBase): Language model identifier or instance + for summarization. Defaults to "default". + as_llm_formatter (str | FormatterBase): Formatter for the language model. + Defaults to "default". + token_counter (HuggingFaceTokenCounter | None): Token counter for + measuring message length. If None, uses default HuggingFace counter. + toolkit (Toolkit | None): Toolkit with file operations for persisting + summaries. If None, creates a default toolkit with read/write/edit. + language (str): Language for the summary output. "zh" for Chinese, + any other value for English. Defaults to "zh". + max_input_length (float): Maximum input length in tokens. + Defaults to 128K tokens. + compact_ratio (float): Ratio used to calculate compaction threshold. + Defaults to 0.7. + + Returns: + str: The generated summary text, or an empty string if an error occurred. + + Note: + This method may write summary files to the memory_path directory + using the provided or default toolkit. + """ try: if token_counter is None: token_counter = get_hf_token_counter() @@ -238,7 +467,30 @@ class ReMeLight(Application): return "" def add_async_summary_task(self, messages: list[Msg], **kwargs): - """Add an asynchronous summary task for the given messages.""" + """ + Add an asynchronous summary task for the given messages. + + Creates a background task to generate a summary of the provided messages + without blocking the main execution flow. Completed tasks are automatically + cleaned up from the task list. + + Args: + messages (list[Msg]): List of messages to be summarized asynchronously. + **kwargs: Additional keyword arguments passed to summary_memory(). + Supported arguments include: + - as_llm: Language model identifier or instance + - as_llm_formatter: Formatter for the language model + - token_counter: Token counter instance + - toolkit: Toolkit for file operations + - language: Output language ("zh" or other) + - max_input_length: Maximum input token length + - compact_ratio: Compaction threshold ratio + + Note: + - Completed/failed/cancelled tasks are cleaned up before adding new ones + - Task results and errors are logged automatically + - Use await_summary_tasks() to wait for all pending tasks to complete + """ remaining_tasks = [] for task in self.summary_tasks: if task.done(): @@ -274,7 +526,49 @@ class ReMeLight(Application): enable_tool_result_compact: bool = True, tool_result_compact_keep_n: int = 3, ) -> tuple[list[Msg], str]: - """Hook called before reasoning.""" + """ + Hook called before reasoning to manage memory and context. + + This method is designed to be called before each reasoning step to ensure + the conversation context fits within model limits. It performs tool result + compaction, checks context size, and triggers memory compaction if needed. + + Args: + messages (list[Msg]): Current conversation messages to be processed. + system_prompt (str): System prompt that will be included in the context. + Used to calculate available space. Defaults to empty string. + compressed_summary (str): Existing compressed summary from previous + compactions. Defaults to empty string. + as_llm (str | ChatModelBase): Language model for compaction operations. + Defaults to "default". + as_llm_formatter (str | FormatterBase): Formatter for the language model. + Defaults to "default". + token_counter (HuggingFaceTokenCounter | None): Token counter for + measuring content length. If None, uses default counter. + toolkit (Toolkit | None): Toolkit for file operations in summarization. + Defaults to None. + language (str): Language for generated summaries. Defaults to "zh". + max_input_length (float): Maximum context window size in tokens. + Defaults to 128K tokens. + compact_ratio (float): Ratio for calculating compaction threshold. + Defaults to 0.7. + memory_compact_reserve (int): Token count to reserve for new responses. + Defaults to 10000 tokens. + enable_tool_result_compact (bool): Whether to compact tool results. + Defaults to True. + tool_result_compact_keep_n (int): Number of recent messages to exclude + from tool result compaction. Defaults to 3. + + Returns: + tuple[list[Msg], str]: A tuple containing: + - list[Msg]: Messages to keep in context (may be reduced) + - str: Updated compressed summary incorporating compacted messages + + Note: + - Automatically triggers background summarization for compacted messages + - Tool results in recent messages (keep_n) are not compacted + - Returns original messages unchanged if no compaction is needed + """ if token_counter is None: token_counter = get_hf_token_counter() @@ -328,7 +622,25 @@ class ReMeLight(Application): return messages_to_keep, compressed_summary async def await_summary_tasks(self) -> str: - """Wait for all background summary tasks to complete and collect results.""" + """ + Wait for all background summary tasks to complete and collect results. + + Blocks until all pending summary tasks in the task list have completed, + cancelled, or failed. Collects status information from each task and + clears the task list after processing. + + Returns: + str: A concatenated string of status messages for all tasks, including: + - Completion confirmations with results + - Cancellation notices + - Error messages for failed tasks + + Note: + - This method will block if any tasks are still running + - All tasks are removed from summary_tasks after this call + - Task exceptions are logged but do not raise to the caller + - Use this before application shutdown to ensure all summaries complete + """ result = "" for task in self.summary_tasks: if task.done(): @@ -369,26 +681,19 @@ class ReMeLight(Application): async def memory_search(self, query: str, max_results: int = 5, min_score: float = 0.1) -> ToolResponse: """ - Perform semantic memory search using vector and full-text search. - - This method searches the memory store for content relevant to the given query - using a hybrid approach combining vector similarity search and full-text search. - Results are ranked by relevance and filtered by the minimum score threshold. + Mandatory recall step: semantically search MEMORY.md + memory/*.md + (and optional session transcripts) before answering questions about + prior work, decisions, dates, people, preferences, or todos; returns + top snippets with path + lines. Args: - query (str): The search query string. Must not be empty. - max_results (int): Maximum number of results to return (1-100, default: 5) - min_score (float): Minimum relevance score threshold (0.001-0.999, default: 0.1) + query (str): The semantic search query to find relevant memory snippets. + max_results (int): Maximum number of search results to return (optional), default 5. + min_score (float): Minimum similarity score threshold for results (optional), default 0.1. Returns: ToolResponse: A ToolResponse containing the search results as text, - or an error message if the query is empty - - Note: - - Vector search weight is controlled by self.vector_weight - - Candidate retrieval uses self.candidate_multiplier for broader search - - Parameters are validated and clamped to valid ranges - - Requires vector search to be enabled via embedding configuration + or an error message if the query is empty. """ # Validate query parameter if not query: @@ -452,7 +757,25 @@ class ReMeLight(Application): @staticmethod def get_in_memory_memory(token_counter: HuggingFaceTokenCounter | None = None): - """Create and return an in-memory memory instance.""" + """ + Create and return an in-memory memory instance. + + Factory method to create a ReMeInMemoryMemory instance configured with + the specified token counter. This memory instance stores data in RAM + without persistence, suitable for temporary or session-based storage. + + Args: + token_counter (HuggingFaceTokenCounter | None): Token counter for + measuring content length in the memory. If None, creates a + default HuggingFace token counter. + + Returns: + ReMeInMemoryMemory: A new in-memory memory instance ready for use. + + Example: + >>> memory = ReMeLight.get_in_memory_memory() + >>> # Use memory for temporary storage during a session + """ if token_counter is None: token_counter = get_hf_token_counter() diff --git a/cookbook/frozenlake/__init__.py b/test/cookbook/__init__.py similarity index 100% rename from cookbook/frozenlake/__init__.py rename to test/cookbook/__init__.py diff --git a/cookbook/simple_demo/__init__.py b/test/cookbook/appworld/__init__.py similarity index 100% rename from cookbook/simple_demo/__init__.py rename to test/cookbook/appworld/__init__.py diff --git a/cookbook/appworld/appworld_react_agent.py b/test/cookbook/appworld/appworld_react_agent.py similarity index 87% rename from cookbook/appworld/appworld_react_agent.py rename to test/cookbook/appworld/appworld_react_agent.py index ba194462..ca8ee2f3 100644 --- a/cookbook/appworld/appworld_react_agent.py +++ b/test/cookbook/appworld/appworld_react_agent.py @@ -96,10 +96,7 @@ class AppworldReactAgent: def prompt_messages(self, run_id, task_index, previous_memories: None, world: AppWorld): app_descriptions = json.dumps( - [ - {"name": k, "description": v} - for (k, v) in world.task.app_descriptions.items() - ], + [{"name": k, "description": v} for (k, v) in world.task.app_descriptions.items()], indent=1, ) dictionary = {"supervisor": world.task.supervisor, "app_descriptions": app_descriptions} @@ -112,7 +109,12 @@ class AppworldReactAgent: self.retrieved_memory_list[run_id][task_index] = response["metadata"]["memory_list"] task_memory = response["answer"] logger.info(f"loaded task_memory: {task_memory}") - query = "Task:\n" + query + "\n\nSome Related Experience to help you to complete the task:\n" + re.sub(r'(?i)\bMemory\s*(\d+)\s*[:]', r'Experience \1:', task_memory) + query = ( + "Task:\n" + + query + + "\n\nSome Related Experience to help you to complete the task:\n" + + re.sub(r"(?i)\bMemory\s*(\d+)\s*[:]", r"Experience \1:", task_memory) + ) else: formatted_memories = [] for i, memory in enumerate(previous_memories, 1): @@ -120,14 +122,18 @@ class AppworldReactAgent: memory_content = memory["content"] memory_text = f"Experience {i}:\n When to use: {condition}\n Content: {memory_content}\n" formatted_memories.append(memory_text) - query = "Task:\n" + query + "\n\nSome Related Experience to help you to complete the task:\n" + "\n".join(formatted_memories) + query = ( + "Task:\n" + + query + + "\n\nSome Related Experience to help you to complete the task:\n" + + "\n".join(formatted_memories) + ) messages = [ {"role": "system", "content": sys_prompt}, - {"role": "user", "content": query} + {"role": "user", "content": query}, ] self.history[run_id][task_index] = messages - @staticmethod def get_reward(world) -> float: tracker = world.evaluate() @@ -136,7 +142,9 @@ class AppworldReactAgent: return num_passes / (num_passes + num_failures) def extract_code_and_fix_content( - self, text: str, ignore_multiple_calls=True + self, + text: str, + ignore_multiple_calls=True, ) -> tuple[str, str]: full_code_regex = r"```python\n(.*?)```" partial_code_regex = r".*```python\n(.*)" @@ -154,7 +162,9 @@ class AppworldReactAgent: match_end = re_match.end() # check for partial code match at end (no terminating ```) following the last match partial_match = re.match( - partial_code_regex, original_text[match_end:], flags=re.DOTALL + partial_code_regex, + original_text[match_end:], + flags=re.DOTALL, ) if partial_match: output_code += partial_match.group(1).strip() @@ -180,7 +190,12 @@ class AppworldReactAgent: before_score = self.get_reward(world) for i in range(self.max_interactions): if i == 0: - self.prompt_messages(run_id=run_id, task_index=task_index, previous_memories=previous_memories, world=world) + self.prompt_messages( + run_id=run_id, + task_index=task_index, + previous_memories=previous_memories, + world=world, + ) code_msg = self.call_llm(self.history[run_id][task_index]) code, text = self.extract_code_and_fix_content(code_msg) self.history[run_id][task_index].append({"role": "assistant", "content": code}) @@ -189,7 +204,9 @@ class AppworldReactAgent: # if len(output) > self.max_response_size: # # logger.warning(f"output exceed max size={len(output)}") # output = output[: self.max_response_size] - self.history[run_id][task_index].append({"role": "user", "content": "Output:\n```\n" + output + "```\n\n"}) + self.history[run_id][task_index].append( + {"role": "user", "content": "Output:\n```\n" + output + "```\n\n"}, + ) if world.task_completed(): break @@ -199,7 +216,9 @@ class AppworldReactAgent: if self.use_memory: if self.use_memory_addition: - new_traj_list = [self.get_traj_from_task_history(task_id, self.history[run_id][task_index], after_score)] + new_traj_list = [ + self.get_traj_from_task_history(task_id, self.history[run_id][task_index], after_score), + ] previous_memories = self.add_memory(new_traj_list) if after_score != 1: self.delete_memory_by_ids([mem["memory_id"] for mem in previous_memories]) @@ -209,7 +228,7 @@ class AppworldReactAgent: self.update_memory_information(self.retrieved_memory_list[run_id][task_index], update_utility) counter += 1 - if self.use_memory_deletion: # and counter % self.delete_freq == 0: + if self.use_memory_deletion: # and counter % self.delete_freq == 0: self.delete_memory() t_result = { @@ -261,7 +280,7 @@ class AppworldReactAgent: return { "task_id": task_id, "messages": task_history, - "score": reward + "score": reward, } def add_memory(self, trajectories): @@ -290,8 +309,8 @@ class AppworldReactAgent: json={ "workspace_id": self.memory_workspace_id, "action": "delete_ids", - "memory_ids": memory_ids - } + "memory_ids": memory_ids, + }, ) response.raise_for_status() @@ -318,6 +337,7 @@ class AppworldReactAgent: ) response.raise_for_status() + def main(): dataset_name = "train" task_ids = load_task_ids(dataset_name) diff --git a/cookbook/appworld/prompt.py b/test/cookbook/appworld/prompt.py similarity index 100% rename from cookbook/appworld/prompt.py rename to test/cookbook/appworld/prompt.py diff --git a/cookbook/appworld/requirements.txt b/test/cookbook/appworld/requirements.txt similarity index 100% rename from cookbook/appworld/requirements.txt rename to test/cookbook/appworld/requirements.txt diff --git a/cookbook/appworld/run_appworld.py b/test/cookbook/appworld/run_appworld.py similarity index 98% rename from cookbook/appworld/run_appworld.py rename to test/cookbook/appworld/run_appworld.py index 3379a16a..d286c64a 100644 --- a/cookbook/appworld/run_appworld.py +++ b/test/cookbook/appworld/run_appworld.py @@ -90,7 +90,7 @@ def run_agent( utility_threshold: float = 0.5, workspace_id: str = "appworld_v1", api_url: str = "http://0.0.0.0:8002/", - batch_size: int = 4 + batch_size: int = 4, ): experiment_name = dataset_name + "_" + experiment_suffix path: Path = Path(f"./exp_result/{model_name}") @@ -125,7 +125,7 @@ def run_agent( future_list: list = [] for i, task_id in enumerate(batch_task_ids): actor = AppworldReactAgent.remote( - index=start_idx+i, + index=start_idx + i, model_name=model_name, task_ids=[task_id], experiment_name=experiment_name, @@ -193,9 +193,10 @@ def run_agent( result.append(task_results) dump_file() + def main(): max_workers = 8 - num_runs = 1 # Number of runs + num_runs = 1 # Number of runs batch_size = 8 # Number of concurrent tasks per batch num_trials = 2 @@ -206,7 +207,6 @@ def main(): workspace_id = "appworld" api_url = "http://0.0.0.0:8002/" - # Clean up workspace before starting logger.info("Deleting workspace...") delete_workspace(workspace_id=workspace_id, api_url=api_url) @@ -216,7 +216,6 @@ def main(): logger.info("Start load experiments to build task memories") load_memory(workspace_id=workspace_id, api_url=api_url) - for i in range(num_runs): run_agent( model_name=model_name, @@ -232,8 +231,9 @@ def main(): utility_threshold=0.5, workspace_id=workspace_id, api_url=api_url, - batch_size=batch_size + batch_size=batch_size, ) + if __name__ == "__main__": main() diff --git a/cookbook/appworld/run_exp_statistic.py b/test/cookbook/appworld/run_exp_statistic.py similarity index 100% rename from cookbook/appworld/run_exp_statistic.py rename to test/cookbook/appworld/run_exp_statistic.py diff --git a/cookbook/tool_memory/__init__.py b/test/cookbook/bfcl/__init__.py similarity index 100% rename from cookbook/tool_memory/__init__.py rename to test/cookbook/bfcl/__init__.py diff --git a/cookbook/bfcl/bfcl_agent.py b/test/cookbook/bfcl/bfcl_agent.py similarity index 98% rename from cookbook/bfcl/bfcl_agent.py rename to test/cookbook/bfcl/bfcl_agent.py index 64eb6e6b..2c779a6a 100644 --- a/cookbook/bfcl/bfcl_agent.py +++ b/test/cookbook/bfcl/bfcl_agent.py @@ -195,7 +195,7 @@ class BFCLAgent: # Extract memory list from response memory_list = result.get("metadata", {}).get("memory_list", []) - logger.info(f'add new memories: {memory_list}') + logger.info(f"add new memories: {memory_list}") return memory_list def delete_memory_by_ids(self, memory_ids): @@ -204,8 +204,8 @@ class BFCLAgent: json={ "workspace_id": self.memory_workspace_id, "action": "delete_ids", - "memory_ids": memory_ids - } + "memory_ids": memory_ids, + }, ) response.raise_for_status() @@ -647,7 +647,9 @@ class BFCLAgent: reward = self.get_reward(run_id, task_index) if self.use_memory: if self.use_memory_addition: # selectively add memories when succeed - new_traj_list = [self.get_traj_from_task_history(task_id, self.history[run_id][task_index], reward)] + new_traj_list = [ + self.get_traj_from_task_history(task_id, self.history[run_id][task_index], reward), + ] previous_memories = self.add_memory(new_traj_list) if reward != 1: self.delete_memory_by_ids([mem["memory_id"] for mem in previous_memories]) diff --git a/cookbook/bfcl/bfcl_utils.py b/test/cookbook/bfcl/bfcl_utils.py similarity index 100% rename from cookbook/bfcl/bfcl_utils.py rename to test/cookbook/bfcl/bfcl_utils.py diff --git a/cookbook/bfcl/init_exp_pool.py b/test/cookbook/bfcl/init_exp_pool.py similarity index 100% rename from cookbook/bfcl/init_exp_pool.py rename to test/cookbook/bfcl/init_exp_pool.py diff --git a/cookbook/bfcl/init_task_memory_pool.py b/test/cookbook/bfcl/init_task_memory_pool.py similarity index 100% rename from cookbook/bfcl/init_task_memory_pool.py rename to test/cookbook/bfcl/init_task_memory_pool.py diff --git a/cookbook/bfcl/local_file_to_library.py b/test/cookbook/bfcl/local_file_to_library.py similarity index 100% rename from cookbook/bfcl/local_file_to_library.py rename to test/cookbook/bfcl/local_file_to_library.py diff --git a/cookbook/bfcl/requirements.txt b/test/cookbook/bfcl/requirements.txt similarity index 100% rename from cookbook/bfcl/requirements.txt rename to test/cookbook/bfcl/requirements.txt diff --git a/cookbook/bfcl/run_bfcl.py b/test/cookbook/bfcl/run_bfcl.py similarity index 99% rename from cookbook/bfcl/run_bfcl.py rename to test/cookbook/bfcl/run_bfcl.py index fd8cc2db..177d91e1 100644 --- a/cookbook/bfcl/run_bfcl.py +++ b/test/cookbook/bfcl/run_bfcl.py @@ -91,7 +91,7 @@ def main(): num_runs = 1 num_trials = 2 - model_name="qwen3-8b" + model_name = "qwen3-8b" use_memory = False use_memory_addition = False use_memory_deletion = False diff --git a/cookbook/bfcl/run_exp_statistic.py b/test/cookbook/bfcl/run_exp_statistic.py similarity index 100% rename from cookbook/bfcl/run_exp_statistic.py rename to test/cookbook/bfcl/run_exp_statistic.py diff --git a/cookbook/bfcl/split_into_trainval.py b/test/cookbook/bfcl/split_into_trainval.py similarity index 100% rename from cookbook/bfcl/split_into_trainval.py rename to test/cookbook/bfcl/split_into_trainval.py diff --git a/reme/memory/tools/chunk/__init__.py b/test/cookbook/frozenlake/__init__.py similarity index 100% rename from reme/memory/tools/chunk/__init__.py rename to test/cookbook/frozenlake/__init__.py diff --git a/cookbook/frozenlake/frozenlake_prompts.yaml b/test/cookbook/frozenlake/frozenlake_prompts.yaml similarity index 100% rename from cookbook/frozenlake/frozenlake_prompts.yaml rename to test/cookbook/frozenlake/frozenlake_prompts.yaml diff --git a/cookbook/frozenlake/frozenlake_react_agent.py b/test/cookbook/frozenlake/frozenlake_react_agent.py similarity index 100% rename from cookbook/frozenlake/frozenlake_react_agent.py rename to test/cookbook/frozenlake/frozenlake_react_agent.py diff --git a/cookbook/frozenlake/map_manager.py b/test/cookbook/frozenlake/map_manager.py similarity index 100% rename from cookbook/frozenlake/map_manager.py rename to test/cookbook/frozenlake/map_manager.py diff --git a/cookbook/frozenlake/run_exp_statistic.py b/test/cookbook/frozenlake/run_exp_statistic.py similarity index 100% rename from cookbook/frozenlake/run_exp_statistic.py rename to test/cookbook/frozenlake/run_exp_statistic.py diff --git a/cookbook/frozenlake/run_frozenlake.py b/test/cookbook/frozenlake/run_frozenlake.py similarity index 100% rename from cookbook/frozenlake/run_frozenlake.py rename to test/cookbook/frozenlake/run_frozenlake.py diff --git a/reme/memory/tools/history/__init__.py b/test/cookbook/simple_demo/__init__.py similarity index 100% rename from reme/memory/tools/history/__init__.py rename to test/cookbook/simple_demo/__init__.py diff --git a/cookbook/simple_demo/import_usage_demo.py b/test/cookbook/simple_demo/import_usage_demo.py similarity index 100% rename from cookbook/simple_demo/import_usage_demo.py rename to test/cookbook/simple_demo/import_usage_demo.py diff --git a/cookbook/simple_demo/mcp_task_memory.jsonl b/test/cookbook/simple_demo/mcp_task_memory.jsonl similarity index 100% rename from cookbook/simple_demo/mcp_task_memory.jsonl rename to test/cookbook/simple_demo/mcp_task_memory.jsonl diff --git a/cookbook/simple_demo/personal_memory.jsonl b/test/cookbook/simple_demo/personal_memory.jsonl similarity index 100% rename from cookbook/simple_demo/personal_memory.jsonl rename to test/cookbook/simple_demo/personal_memory.jsonl diff --git a/cookbook/simple_demo/task_memory.jsonl b/test/cookbook/simple_demo/task_memory.jsonl similarity index 100% rename from cookbook/simple_demo/task_memory.jsonl rename to test/cookbook/simple_demo/task_memory.jsonl diff --git a/cookbook/simple_demo/task_messages.jsonl b/test/cookbook/simple_demo/task_messages.jsonl similarity index 100% rename from cookbook/simple_demo/task_messages.jsonl rename to test/cookbook/simple_demo/task_messages.jsonl diff --git a/cookbook/simple_demo/use_personal_memory_demo.py b/test/cookbook/simple_demo/use_personal_memory_demo.py similarity index 100% rename from cookbook/simple_demo/use_personal_memory_demo.py rename to test/cookbook/simple_demo/use_personal_memory_demo.py diff --git a/cookbook/simple_demo/use_task_memory_demo.py b/test/cookbook/simple_demo/use_task_memory_demo.py similarity index 100% rename from cookbook/simple_demo/use_task_memory_demo.py rename to test/cookbook/simple_demo/use_task_memory_demo.py diff --git a/cookbook/simple_demo/use_task_memory_mcp_demo.py b/test/cookbook/simple_demo/use_task_memory_mcp_demo.py similarity index 100% rename from cookbook/simple_demo/use_task_memory_mcp_demo.py rename to test/cookbook/simple_demo/use_task_memory_mcp_demo.py diff --git a/cookbook/simple_demo/use_tool_memory_demo.py b/test/cookbook/simple_demo/use_tool_memory_demo.py similarity index 100% rename from cookbook/simple_demo/use_tool_memory_demo.py rename to test/cookbook/simple_demo/use_tool_memory_demo.py diff --git a/reme/memory/tools/profiles/__init__.py b/test/cookbook/tool_memory/__init__.py similarity index 100% rename from reme/memory/tools/profiles/__init__.py rename to test/cookbook/tool_memory/__init__.py diff --git a/cookbook/tool_memory/query.json b/test/cookbook/tool_memory/query.json similarity index 100% rename from cookbook/tool_memory/query.json rename to test/cookbook/tool_memory/query.json diff --git a/cookbook/tool_memory/run_reme_tool_bench.py b/test/cookbook/tool_memory/run_reme_tool_bench.py similarity index 100% rename from cookbook/tool_memory/run_reme_tool_bench.py rename to test/cookbook/tool_memory/run_reme_tool_bench.py diff --git a/cookbook/working_memory/react_agent_with_working_memory.py b/test/cookbook/working_memory/react_agent_with_working_memory.py similarity index 70% rename from cookbook/working_memory/react_agent_with_working_memory.py rename to test/cookbook/working_memory/react_agent_with_working_memory.py index 52e3fdc6..27f2275e 100644 --- a/cookbook/working_memory/react_agent_with_working_memory.py +++ b/test/cookbook/working_memory/react_agent_with_working_memory.py @@ -28,9 +28,11 @@ class ReactAgent: rather than on complex agent logic. """ - def __init__(self, - model_name="", - max_steps: int = 50): + def __init__( + self, + model_name="", + max_steps: int = 50, + ): # You can replace this with your own LLM wrapper if needed. self.llm = OpenAICompatibleLLM(model_name=model_name) @@ -65,10 +67,16 @@ class ReactAgent: # Prepare all available tools from the MCP server. tool_dict: Dict[str, ToolCall] = {} - async with FastMcpClient("reme_mcp_server", { - "type": "sse", - "url": "http://0.0.0.0:8002/sse", - }) as mcp_client, HttpClient(base_url="http://localhost:8003") as http_client: + async with ( + FastMcpClient( + "reme_mcp_server", + { + "type": "sse", + "url": "http://0.0.0.0:8002/sse", + }, + ) as mcp_client, + HttpClient(base_url="http://localhost:8003") as http_client, + ): tool_calls = await mcp_client.list_tool_calls() for tool_call in tool_calls: @@ -87,25 +95,30 @@ class ReactAgent: # - compress long histories, # - offload detailed context into working memory storage, # - keep the recent message(s) for short-term reasoning. - result = await http_client.execute_flow("summary_working_memory", - messages=[x.simple_dump() for x in messages], - working_summary_mode="auto", - compact_ratio_threshold=0.75, - max_total_tokens=20000, - max_tool_message_tokens=2000, - group_token_threshold=None, - keep_recent_count=1, - store_dir="./test_working_memory") + result = await http_client.execute_flow( + "summary_working_memory", + messages=[x.simple_dump() for x in messages], + working_summary_mode="auto", + compact_ratio_threshold=0.75, + max_total_tokens=20000, + max_tool_message_tokens=2000, + group_token_threshold=None, + keep_recent_count=1, + store_dir="./test_working_memory", + ) # Convert the API result back into `Message` objects for the LLM. messages = [Message(**x) for x in result.answer] # Ask the LLM what to do next. # You can plug in your own tool-calling strategy here. - assistant_message: Message = await self.llm.achat(messages=messages, tools=[ - tool_dict["grep_working_memory"], - tool_dict["read_working_memory"], - ]) + assistant_message: Message = await self.llm.achat( + messages=messages, + tools=[ + tool_dict["grep_working_memory"], + tool_dict["read_working_memory"], + ], + ) messages.append(assistant_message) @@ -118,20 +131,25 @@ class ReactAgent: logger.exception(f"unknown tool_call.name={tool_call.name}") continue - logger.info(f"round{i + 1}.{j} submit tool_calls={tool_call.name} " - f"argument={tool_call.argument_dict}") + logger.info( + f"round{i + 1}.{j} submit tool_calls={tool_call.name} " f"argument={tool_call.argument_dict}", + ) # Execute the tool via MCP and parse the result. - result = await mcp_client.call_tool(tool_call.name, - arguments=tool_call.argument_dict, - parse_result=True) + result = await mcp_client.call_tool( + tool_call.name, + arguments=tool_call.argument_dict, + parse_result=True, + ) # Attach the tool result as a TOOL-role message so the LLM # can see and reason about it in the next step. - messages.append(Message( - role=Role.TOOL, - tool_call_id=tool_call.id, - content=result, - )) + messages.append( + Message( + role=Role.TOOL, + tool_call_id=tool_call.id, + content=result, + ), + ) return messages diff --git a/cookbook/working_memory/work_memory_demo.py b/test/cookbook/working_memory/work_memory_demo.py similarity index 99% rename from cookbook/working_memory/work_memory_demo.py rename to test/cookbook/working_memory/work_memory_demo.py index 612a637a..826cb0f2 100644 --- a/cookbook/working_memory/work_memory_demo.py +++ b/test/cookbook/working_memory/work_memory_demo.py @@ -108,7 +108,7 @@ async def main(): logger.info( f"origin_token_count: {origin_token_count} " f"after_token_count: {after_token_count} " - f"compress_ratio={after_token_count / origin_token_count:.2f}" + f"compress_ratio={after_token_count / origin_token_count:.2f}", ) diff --git a/test/cli/__init__.py b/test/test/cli/__init__.py similarity index 100% rename from test/cli/__init__.py rename to test/test/cli/__init__.py diff --git a/test/cli/fb_cli.py b/test/test/cli/fb_cli.py similarity index 100% rename from test/cli/fb_cli.py rename to test/test/cli/fb_cli.py diff --git a/test/cli/fb_cli.yaml b/test/test/cli/fb_cli.yaml similarity index 100% rename from test/cli/fb_cli.yaml rename to test/test/cli/fb_cli.yaml diff --git a/test/cli/fb_compactor.py b/test/test/cli/fb_compactor.py similarity index 100% rename from test/cli/fb_compactor.py rename to test/test/cli/fb_compactor.py diff --git a/test/cli/fb_compactor.yaml b/test/test/cli/fb_compactor.yaml similarity index 100% rename from test/cli/fb_compactor.yaml rename to test/test/cli/fb_compactor.yaml diff --git a/test/cli/fb_context_checker.py b/test/test/cli/fb_context_checker.py similarity index 100% rename from test/cli/fb_context_checker.py rename to test/test/cli/fb_context_checker.py diff --git a/test/cli/fb_summarizer.py b/test/test/cli/fb_summarizer.py similarity index 100% rename from test/cli/fb_summarizer.py rename to test/test/cli/fb_summarizer.py diff --git a/test/cli/fb_summarizer.yaml b/test/test/cli/fb_summarizer.yaml similarity index 100% rename from test/cli/fb_summarizer.yaml rename to test/test/cli/fb_summarizer.yaml diff --git a/test/reme_cli.py b/test/test/reme_cli.py similarity index 100% rename from test/reme_cli.py rename to test/test/reme_cli.py diff --git a/test/test_fs_compactor.py b/test/test/test_fs_compactor.py similarity index 100% rename from test/test_fs_compactor.py rename to test/test/test_fs_compactor.py diff --git a/test/test_fs_context_checker.py b/test/test/test_fs_context_checker.py similarity index 100% rename from test/test_fs_context_checker.py rename to test/test/test_fs_context_checker.py diff --git a/test/test_fs_file_watch_integration.py b/test/test/test_fs_file_watch_integration.py similarity index 100% rename from test/test_fs_file_watch_integration.py rename to test/test/test_fs_file_watch_integration.py diff --git a/test/test_fs_memory_get.py b/test/test/test_fs_memory_get.py similarity index 100% rename from test/test_fs_memory_get.py rename to test/test/test_fs_memory_get.py diff --git a/test/test_fs_memory_search.py b/test/test/test_fs_memory_search.py similarity index 100% rename from test/test_fs_memory_search.py rename to test/test/test_fs_memory_search.py diff --git a/test/test_fs_summary.py b/test/test/test_fs_summary.py similarity index 100% rename from test/test_fs_summary.py rename to test/test/test_fs_summary.py diff --git a/tests/light/test_summarizer.py b/tests/light/test_summarizer.py index a2f2d975..9718cd2a 100644 --- a/tests/light/test_summarizer.py +++ b/tests/light/test_summarizer.py @@ -15,7 +15,7 @@ from test_utils import ( ) from reme.core.utils import get_std_logger from reme.memory.file_based import Summarizer -from reme.memory.tools.file import FileIO +from reme.memory.file_based.tools import FileIO logger = get_std_logger() diff --git a/tests/light/test_tools.py b/tests/light/test_tools.py new file mode 100644 index 00000000..933d506d --- /dev/null +++ b/tests/light/test_tools.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +# pylint: disable=redefined-outer-name +"""Unit tests for Shell and FileIO tools.""" + +import asyncio +import os +import shutil +import tempfile + +import pytest + +from reme.memory.file_based.tools.shell import Shell +from reme.memory.file_based.tools.file_io import FileIO +from reme.memory.file_based.tools.utils import DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES + + +# ============ Shell Tests ============ + + +@pytest.fixture(scope="module") +def shell_env(): + """Create temporary directory and Shell instance.""" + test_dir = tempfile.mkdtemp(prefix="test_shell_") + shell = Shell(working_dir=test_dir) + yield {"dir": test_dir, "shell": shell} + shutil.rmtree(test_dir, ignore_errors=True) + + +def test_shell_echo_success(shell_env): + """Test successful echo command execution.""" + result = asyncio.run(shell_env["shell"].execute_shell_command("echo hello")) + assert result.content + text = result.content[0].get("text", "") + assert "hello" in text + + +def test_shell_pwd_in_working_dir(shell_env): + """Test command executes in correct working directory.""" + result = asyncio.run(shell_env["shell"].execute_shell_command("pwd")) + text = result.content[0].get("text", "") + assert shell_env["dir"] in text + + +def test_shell_command_failure(shell_env): + """Test failed command returns error information.""" + result = asyncio.run(shell_env["shell"].execute_shell_command("exit 1")) + text = result.content[0].get("text", "") + assert "failed" in text.lower() + assert "exit code" in text.lower() + + +def test_shell_command_with_stderr(shell_env): + """Test command with stderr output.""" + result = asyncio.run( + shell_env["shell"].execute_shell_command("echo error >&2 && exit 1"), + ) + text = result.content[0].get("text", "") + assert "error" in text + + +def test_shell_no_output(shell_env): + """Test successful command with no output.""" + result = asyncio.run(shell_env["shell"].execute_shell_command("true")) + text = result.content[0].get("text", "") + assert "successfully" in text.lower() + + +def test_shell_multiline_output(shell_env): + """Test command with multiline output.""" + result = asyncio.run( + shell_env["shell"].execute_shell_command("echo -e 'line1\nline2\nline3'"), + ) + text = result.content[0].get("text", "") + assert "line1" in text + assert "line2" in text + assert "line3" in text + + +def test_shell_truncated_output(shell_env): + """Test output truncation for large output.""" + lines_to_generate = DEFAULT_MAX_LINES + 500 + cmd = f"seq 1 {lines_to_generate}" + result = asyncio.run(shell_env["shell"].execute_shell_command(cmd)) + text = result.content[0].get("text", "") + + # Should contain truncation notice + assert "truncated" in text.lower() + # Should contain the last line (tail is kept) + assert str(lines_to_generate) in text + # Verify first numeric line is > 1 (truncated from head) + numeric_lines = [tl for tl in text.strip().split("\n") if tl.isdigit()] + if numeric_lines: + assert int(numeric_lines[0]) > 1 + + +def test_shell_timeout(shell_env): + """Test command timeout handling.""" + result = asyncio.run( + shell_env["shell"].execute_shell_command("sleep 10", timeout=1), + ) + text = result.content[0].get("text", "") + assert "timeout" in text.lower() + + +# ============ FileIO Read Tests ============ + + +@pytest.fixture(scope="module") +def fileio_env(): + """Create temporary directory with test files.""" + test_dir = tempfile.mkdtemp(prefix="test_fileio_") + file_io = FileIO(working_dir=test_dir) + + # Create simple test file + simple_file = os.path.join(test_dir, "simple.txt") + with open(simple_file, "w", encoding="utf-8") as f: + f.write("line1\nline2\nline3\nline4\nline5") + + # Create large file (exceeds DEFAULT_MAX_LINES) + large_file = os.path.join(test_dir, "large.txt") + with open(large_file, "w", encoding="utf-8") as f: + for i in range(1, DEFAULT_MAX_LINES + 500): + f.write(f"line {i}\n") + + # Create large bytes file (exceeds DEFAULT_MAX_BYTES) + large_bytes_file = os.path.join(test_dir, "large_bytes.txt") + with open(large_bytes_file, "w", encoding="utf-8") as f: + content = "x" * 100 + "\n" + lines_needed = (DEFAULT_MAX_BYTES // 101) + 100 + for _ in range(lines_needed): + f.write(content) + + yield { + "dir": test_dir, + "file_io": file_io, + "simple_file": simple_file, + "large_file": large_file, + "large_bytes_file": large_bytes_file, + } + shutil.rmtree(test_dir, ignore_errors=True) + + +def test_read_file_success(fileio_env): + """Test successful file reading.""" + result = asyncio.run(fileio_env["file_io"].read(fileio_env["simple_file"])) + text = result.content[0].get("text", "") + assert "line1" in text + assert "line5" in text + + +def test_read_file_relative_path(fileio_env): + """Test reading file with relative path.""" + result = asyncio.run(fileio_env["file_io"].read("simple.txt")) + text = result.content[0].get("text", "") + assert "line1" in text + + +def test_read_file_not_exists(fileio_env): + """Test reading non-existent file.""" + result = asyncio.run(fileio_env["file_io"].read("nonexistent.txt")) + text = result.content[0].get("text", "") + assert "Error" in text + assert "does not exist" in text + + +def test_read_file_with_line_range(fileio_env): + """Test reading specific line range.""" + result = asyncio.run( + fileio_env["file_io"].read(fileio_env["simple_file"], start_line=2, end_line=4), + ) + text = result.content[0].get("text", "") + assert "line2" in text + assert "line4" in text + assert "lines 2-4" in text.lower() + + +def test_read_file_start_line_exceeds(fileio_env): + """Test start_line exceeding file length.""" + result = asyncio.run( + fileio_env["file_io"].read(fileio_env["simple_file"], start_line=100), + ) + text = result.content[0].get("text", "") + assert "Error" in text + assert "exceeds" in text + + +def test_read_file_invalid_range(fileio_env): + """Test invalid line range (start > end).""" + result = asyncio.run( + fileio_env["file_io"].read(fileio_env["simple_file"], start_line=4, end_line=2), + ) + text = result.content[0].get("text", "") + assert "Error" in text + + +def test_read_file_truncated_by_lines(fileio_env): + """Test file truncation by line limit.""" + result = asyncio.run(fileio_env["file_io"].read(fileio_env["large_file"])) + text = result.content[0].get("text", "") + assert "line 1" in text # Head is kept + assert "continue" in text.lower() + + +def test_read_file_truncated_by_bytes(fileio_env): + """Test file truncation by byte limit.""" + result = asyncio.run(fileio_env["file_io"].read(fileio_env["large_bytes_file"])) + text = result.content[0].get("text", "") + assert "continue" in text.lower() or "KB" in text + + +def test_read_directory_error(fileio_env): + """Test reading a directory returns error.""" + result = asyncio.run(fileio_env["file_io"].read(fileio_env["dir"])) + text = result.content[0].get("text", "") + assert "Error" in text + assert "not a file" in text + + +# ============ FileIO Write Tests ============ + + +@pytest.fixture +def write_env(): + """Create temporary directory for write tests.""" + test_dir = tempfile.mkdtemp(prefix="test_fileio_write_") + file_io = FileIO(working_dir=test_dir) + yield {"dir": test_dir, "file_io": file_io} + shutil.rmtree(test_dir, ignore_errors=True) + + +def test_write_new_file(write_env): + """Test writing a new file.""" + file_path = os.path.join(write_env["dir"], "new_file.txt") + result = asyncio.run(write_env["file_io"].write(file_path, "test content")) + text = result.content[0].get("text", "") + assert "Wrote" in text + + with open(file_path, "r", encoding="utf-8") as f: + assert f.read() == "test content" + + +def test_write_overwrite_file(write_env): + """Test overwriting existing file.""" + file_path = os.path.join(write_env["dir"], "overwrite.txt") + with open(file_path, "w", encoding="utf-8") as f: + f.write("old content") + + result = asyncio.run(write_env["file_io"].write(file_path, "new content")) + text = result.content[0].get("text", "") + assert "Wrote" in text + + with open(file_path, "r", encoding="utf-8") as f: + assert f.read() == "new content" + + +def test_write_empty_path(write_env): + """Test writing with empty path.""" + result = asyncio.run(write_env["file_io"].write("", "content")) + text = result.content[0].get("text", "") + assert "Error" in text + + +def test_write_relative_path(write_env): + """Test writing file with relative path.""" + result = asyncio.run(write_env["file_io"].write("relative.txt", "relative content")) + text = result.content[0].get("text", "") + assert "Wrote" in text + + file_path = os.path.join(write_env["dir"], "relative.txt") + assert os.path.exists(file_path) + + +# ============ FileIO Edit Tests ============ + + +@pytest.fixture +def edit_env(): + """Create temporary directory with edit test file.""" + test_dir = tempfile.mkdtemp(prefix="test_fileio_edit_") + file_io = FileIO(working_dir=test_dir) + + edit_file = os.path.join(test_dir, "edit_test.txt") + with open(edit_file, "w", encoding="utf-8") as f: + f.write("Hello World\nThis is a test\nHello Again") + + yield {"dir": test_dir, "file_io": file_io, "edit_file": edit_file} + shutil.rmtree(test_dir, ignore_errors=True) + + +def test_edit_replace_text(edit_env): + """Test replacing text in file.""" + result = asyncio.run( + edit_env["file_io"].edit(edit_env["edit_file"], "Hello", "Hi"), + ) + text = result.content[0].get("text", "") + assert "Successfully" in text + + with open(edit_env["edit_file"], "r", encoding="utf-8") as f: + content = f.read() + assert "Hello" not in content + assert "Hi World" in content + assert "Hi Again" in content + + +def test_edit_text_not_found(edit_env): + """Test editing when text not found.""" + result = asyncio.run( + edit_env["file_io"].edit(edit_env["edit_file"], "NotExists", "Replacement"), + ) + text = result.content[0].get("text", "") + assert "Error" in text + assert "not found" in text + + +def test_edit_nonexistent_file(edit_env): + """Test editing non-existent file.""" + result = asyncio.run( + edit_env["file_io"].edit("nonexistent.txt", "old", "new"), + ) + text = result.content[0].get("text", "") + assert "Error" in text diff --git a/tests/vector/test_reme_vector.py b/tests/vector/test_reme_vector.py new file mode 100644 index 00000000..3829c841 --- /dev/null +++ b/tests/vector/test_reme_vector.py @@ -0,0 +1,89 @@ +"""测试 ReMe 的 vector 搜索功能""" + +import asyncio + +from reme import ReMe + + +async def main(): + """测试 ReMe 的 vector 搜索功能""" + # 初始化 ReMe + reme = ReMe( + working_dir=".reme", + default_llm_config={ + "backend": "openai", + "model_name": "qwen3.5-plus", + }, + default_embedding_model_config={ + "backend": "openai", + "model_name": "text-embedding-v4", + "dimensions": 1024, + }, + default_vector_store_config={ + "backend": "local", # 支持 local/chroma/qdrant/elasticsearch + }, + ) + await reme.start() + + messages = [ + {"role": "user", "content": "帮我写一个 Python 脚本", "time_created": "2026-02-28 10:00:00"}, + {"role": "assistant", "content": "好的,我来帮你写", "time_created": "2026-02-28 10:00:05"}, + ] + + # 1. 从对话中总结记忆(自动提取用户偏好、任务经验等) + result = await reme.summarize_memory( + messages=messages, + user_name="alice", # 个人记忆 + # task_name="code_writing", # 任务记忆 + ) + print(f"总结结果: {result}") + + # 2. 检索相关记忆 + memories = await reme.retrieve_memory( + query="Python 编程", + user_name="alice", + # task_name="code_writing", + ) + print(f"检索结果: {memories}") + + # 3. 手动添加记忆 + memory_node = await reme.add_memory( + memory_content="用户喜欢简洁的代码风格", + user_name="alice", + ) + print(f"添加的记忆: {memory_node}") + memory_id = memory_node.memory_id + + # 4. 通过 ID 获取单条记忆 + fetched_memory = await reme.get_memory(memory_id=memory_id) + print(f"获取的记忆: {fetched_memory}") + + # 5. 更新记忆内容 + updated_memory = await reme.update_memory( + memory_id=memory_id, + user_name="alice", + memory_content="用户喜欢简洁且带注释的代码风格", + ) + print(f"更新后的记忆: {updated_memory}") + + # 6. 列出用户的所有记忆(支持过滤和排序) + all_memories = await reme.list_memory( + user_name="alice", + limit=10, + sort_key="time_created", + reverse=True, + ) + print(f"用户记忆列表: {all_memories}") + + # 7. 删除指定记忆 + await reme.delete_memory(memory_id=memory_id) + print(f"已删除记忆: {memory_id}") + + # 8. 删除所有记忆(谨慎使用) + # await reme.delete_all() + + await reme.close() + + +if __name__ == "__main__": + asyncio.run(main())