From e5bb8451966a144d3e67bf685629877daac64e8b Mon Sep 17 00:00:00 2001 From: zouyingcao <57442064+zouyingcao@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:23:36 +0800 Subject: [PATCH] refactor(cli): using AgentScope components to reimplement the reme_cli logic (#153) * add: as_token_counters config for reme_cli * add: reme_cli function * update: format the terminal printing for reme_cli * update: check for pre-commit * single quotes for the inner dictionary keys * update the usage of get_std_logger for pre-commit * update the usage of get_std_logger for pre-commit * add 'console_enabled' param in compactor&summarizer --- reme/config/cli.yaml | 22 +- reme/core/__init__.py | 2 + reme/core/application.py | 7 + reme/core/as_token_counter/__init__.py | 9 + reme/core/op/base_op.py | 10 + reme/core/registry_factory.py | 1 + reme/core/schema/service_config.py | 1 + reme/core/service_context.py | 5 + reme/core/utils/llm_utils.py | 54 +++- reme/core/utils/std_logger.py | 6 +- reme/memory/file_based/components/__init__.py | 2 + reme/memory/file_based/components/cli.py | 303 ++++++++++++++++++ reme/memory/file_based/components/cli.yaml | 95 ++++++ .../memory/file_based/components/compactor.py | 7 +- .../file_based/components/summarizer.py | 7 +- reme/reme_cli.py | 192 +++++++++++ 16 files changed, 703 insertions(+), 20 deletions(-) create mode 100644 reme/core/as_token_counter/__init__.py create mode 100644 reme/memory/file_based/components/cli.py create mode 100644 reme/memory/file_based/components/cli.yaml create mode 100644 reme/reme_cli.py diff --git a/reme/config/cli.yaml b/reme/config/cli.yaml index 7d348d45..a6914170 100644 --- a/reme/config/cli.yaml +++ b/reme/config/cli.yaml @@ -8,12 +8,20 @@ metadata: vector_weight: 0.7 candidate_multiplier: 2 -llms: +as_llms: default: backend: openai - # model_name: qwen3-235b-a22b-thinking-2507 model_name: qwen3.5-plus - request_interval: 1 + +as_llm_formatters: + default: + backend: openai + +as_token_counters: + default: + backend: hf + pretrained_model_name_or_path: Qwen/Qwen3-Coder-30B-A3B-Instruct + use_mirror: true embedding_models: default: @@ -41,11 +49,3 @@ file_watchers: recursive: false scan_on_start: true -token_counters: - default: - backend: base - - hf: - backend: hf - model_name: Qwen/Qwen3-Coder-30B-A3B-Instruct - use_mirror: true diff --git a/reme/core/__init__.py b/reme/core/__init__.py index 053755cc..726a3455 100644 --- a/reme/core/__init__.py +++ b/reme/core/__init__.py @@ -2,6 +2,7 @@ from . import as_llm from . import as_llm_formatter +from . import as_token_counter from . import embedding from . import enumeration from . import file_store @@ -25,6 +26,7 @@ __all__ = [ # Submodules "as_llm", "as_llm_formatter", + "as_token_counter", "embedding", "enumeration", "file_watcher", diff --git a/reme/core/application.py b/reme/core/application.py index 46f4a934..85a33c4d 100644 --- a/reme/core/application.py +++ b/reme/core/application.py @@ -172,6 +172,13 @@ class Application: config_dict = config.model_dump(exclude={"backend"}) self.service_context.as_llm_formatters[name] = R.as_llm_formatters[config.backend](**config_dict) + for name, config in self.service_config.as_token_counters.items(): + if config.backend not in R.as_token_counters: + logger.warning(f"Token counter backend {config.backend} is not supported.") + else: + config_dict = config.model_dump(exclude={"backend"}) + self.service_context.as_token_counters[name] = R.as_token_counters[config.backend](**config_dict) + for name, config in self.service_config.llms.items(): if config.backend not in R.llms: logger.warning(f"LLM backend {config.backend} is not supported.") diff --git a/reme/core/as_token_counter/__init__.py b/reme/core/as_token_counter/__init__.py new file mode 100644 index 00000000..51b3bd29 --- /dev/null +++ b/reme/core/as_token_counter/__init__.py @@ -0,0 +1,9 @@ +"""Module for registering AgentScope token counters.""" + +from agentscope.token import OpenAITokenCounter +from agentscope.token import HuggingFaceTokenCounter + +from ..registry_factory import R + +R.as_token_counters.register("openai")(OpenAITokenCounter) +R.as_token_counters.register("hf")(HuggingFaceTokenCounter) diff --git a/reme/core/op/base_op.py b/reme/core/op/base_op.py index 86cfa3be..f25da289 100644 --- a/reme/core/op/base_op.py +++ b/reme/core/op/base_op.py @@ -9,6 +9,7 @@ from typing import Callable, Optional, Any from agentscope.formatter import FormatterBase from agentscope.model import ChatModelBase +from agentscope.token import TokenCounterBase from loguru import logger from tqdm import tqdm @@ -46,6 +47,7 @@ class BaseOp(metaclass=ABCMeta): prompt_path: str = "", as_llm: str | ChatModelBase = "default", as_llm_formatter: str | FormatterBase = "default", + as_token_counter: str | TokenCounterBase = "default", llm: str | BaseLLM = "default", embedding_model: str | BaseEmbeddingModel = "default", vector_store: str | BaseVectorStore = "default", @@ -70,6 +72,7 @@ class BaseOp(metaclass=ABCMeta): self._as_llm = as_llm self._as_llm_formatter = as_llm_formatter + self._as_token_counter = as_token_counter self._llm = llm self._embedding_model = embedding_model self._vector_store = vector_store @@ -149,6 +152,13 @@ class BaseOp(metaclass=ABCMeta): self._as_llm_formatter = self.service_context.as_llm_formatters[self._as_llm_formatter] return self._as_llm_formatter + @property + def as_token_counter(self) -> TokenCounterBase: + """Get the token counter instance from ServiceContext.""" + if isinstance(self._as_token_counter, str): + self._as_token_counter = self.service_context.as_token_counters[self._as_token_counter] + return self._as_token_counter + @property def llm(self) -> BaseLLM: """Get the LLM instance from ServiceContext.""" diff --git a/reme/core/registry_factory.py b/reme/core/registry_factory.py index f54ad3c1..921f27a8 100644 --- a/reme/core/registry_factory.py +++ b/reme/core/registry_factory.py @@ -36,6 +36,7 @@ class RegistryFactory: self.llms = Registry() self.as_llms = Registry() self.as_llm_formatters = Registry() + self.as_token_counters = Registry() self.embedding_models = Registry() self.vector_stores = Registry() self.file_stores = Registry() diff --git a/reme/core/schema/service_config.py b/reme/core/schema/service_config.py index 5e4cd212..758944b2 100644 --- a/reme/core/schema/service_config.py +++ b/reme/core/schema/service_config.py @@ -130,6 +130,7 @@ class ServiceConfig(BasicConfig): flows: dict[str, FlowConfig] = Field(default_factory=dict) as_llms: dict[str, BasicConfig] = Field(default_factory=dict) as_llm_formatters: dict[str, BasicConfig] = Field(default_factory=dict) + as_token_counters: dict[str, BasicConfig] = Field(default_factory=dict) llms: dict[str, LLMConfig] = Field(default_factory=dict) embedding_models: dict[str, EmbeddingModelConfig] = Field(default_factory=dict) vector_stores: dict[str, VectorStoreConfig] = Field(default_factory=dict) diff --git a/reme/core/service_context.py b/reme/core/service_context.py index 92d0d566..4ab83ff7 100644 --- a/reme/core/service_context.py +++ b/reme/core/service_context.py @@ -13,6 +13,7 @@ from .utils import load_env, PydanticConfigParser if TYPE_CHECKING: from agentscope.model import ChatModelBase from agentscope.formatter import FormatterBase + from agentscope.token import TokenCounterBase from .llm import BaseLLM from .embedding import BaseEmbeddingModel from .vector_store import BaseVectorStore @@ -40,6 +41,7 @@ class ServiceContext(BaseDict): log_to_console: bool = True, default_as_llm_config: dict | None = None, default_as_llm_formatter_config: dict | None = None, + default_as_token_counter_config: dict | None = None, default_llm_config: dict | None = None, default_embedding_model_config: dict | None = None, default_vector_store_config: dict | None = None, @@ -72,6 +74,8 @@ class ServiceContext(BaseDict): self._update_section_config(kwargs, "as_llms", **default_as_llm_config) if default_as_llm_formatter_config: self._update_section_config(kwargs, "as_llm_formatters", **default_as_llm_formatter_config) + if default_as_token_counter_config: + self._update_section_config(kwargs, "as_token_counters", **default_as_token_counter_config) if default_llm_config: self._update_section_config(kwargs, "llms", **default_llm_config) if default_embedding_model_config: @@ -100,6 +104,7 @@ class ServiceContext(BaseDict): self.thread_pool: ThreadPoolExecutor | None = None self.as_llms: dict[str, "ChatModelBase"] = {} self.as_llm_formatters: dict[str, "FormatterBase"] = {} + self.as_token_counters: dict[str, "TokenCounterBase"] = {} self.llms: dict[str, "BaseLLM"] = {} self.embedding_models: dict[str, "BaseEmbeddingModel"] = {} self.token_counters: dict[str, "BaseTokenCounter"] = {} diff --git a/reme/core/utils/llm_utils.py b/reme/core/utils/llm_utils.py index 6ec2cd23..e828c24e 100644 --- a/reme/core/utils/llm_utils.py +++ b/reme/core/utils/llm_utils.py @@ -3,10 +3,60 @@ import json import re +from agentscope.message import Msg from loguru import logger from ..enumeration import Role -from ..schema import Message, Trajectory, MemoryNode +from ..schema import Message, Trajectory, MemoryNode, ToolCall + + +def convert_as_msg_to_message(msg) -> Message: + """Convert an agentscope Msg object to the project's Message type.""" + role_str = getattr(msg, "role", "user") + role = ( + Role(role_str.lower()) + if isinstance(role_str, str) and role_str.lower() in [r.value for r in Role] + else Role.USER + ) + + content_blocks = msg.get_content_blocks() + content = "" + reasoning_content = "" + tool_calls = [] + tool_call_id = "" + + for block in content_blocks: + block_type = block["type"] + if block_type == "thinking": + reasoning_content = block["thinking"] + elif block_type == "tool_use": + try: + tool_calls.append( + ToolCall( + id=block["id"], + name=block["name"], + arguments=json.dumps(block["input"], ensure_ascii=False), + ), + ) + except (json.JSONDecodeError, TypeError): + pass + elif block_type == "tool_result": + role = Role.TOOL + tool_call_id = block["id"] + content = block["output"][0]["text"] + else: + content = block[block_type] + + return Message( + name=getattr(msg, "name", None), + role=role, + content=content, + reasoning_content=reasoning_content, + tool_calls=tool_calls, + tool_call_id=tool_call_id, + time_created=getattr(msg, "timestamp", "") or "", + metadata=getattr(msg, "metadata", {}) or {}, + ) def format_messages( @@ -24,6 +74,8 @@ def format_messages( for i, message in enumerate(messages): if isinstance(message, dict): message = Message(**message) + if isinstance(message, Msg): + message = convert_as_msg_to_message(message) if not enable_system and message.role is Role.SYSTEM: continue diff --git a/reme/core/utils/std_logger.py b/reme/core/utils/std_logger.py index dbdf1908..bfa6b32a 100644 --- a/reme/core/utils/std_logger.py +++ b/reme/core/utils/std_logger.py @@ -47,6 +47,7 @@ def get_loggerv2( log_file_prefix: str = "reme", rotation: str = "midnight", retention_days: int = 7, + force_update: bool = False, ) -> logging.Logger: """Get a configured logger instance. @@ -59,12 +60,13 @@ def get_loggerv2( log_file_prefix: Prefix for log file names (e.g., 'reme' -> 'reme_2024-01-01.log'). rotation: Log rotation time, defaults to midnight. retention_days: Number of days to retain log files. + force_update: Whether to force update the logger configuration even if it already exists. Returns: Configured Logger instance. """ - # Return existing logger if already created - if name in _loggers: + # Return existing logger if already created and not force updating + if name in _loggers and not force_update: return _loggers[name] # Create new logger without using root logger diff --git a/reme/memory/file_based/components/__init__.py b/reme/memory/file_based/components/__init__.py index 42ab2f6b..86574790 100644 --- a/reme/memory/file_based/components/__init__.py +++ b/reme/memory/file_based/components/__init__.py @@ -4,10 +4,12 @@ from .compactor import Compactor from .context_checker import ContextChecker from .summarizer import Summarizer from .tool_result_compactor import ToolResultCompactor +from .cli import CliAgent __all__ = [ "Compactor", "Summarizer", "ContextChecker", "ToolResultCompactor", + "CliAgent", ] diff --git a/reme/memory/file_based/components/cli.py b/reme/memory/file_based/components/cli.py new file mode 100644 index 00000000..bc88bd56 --- /dev/null +++ b/reme/memory/file_based/components/cli.py @@ -0,0 +1,303 @@ +"""CLI component for interactive chat using agentscope-based memory tools.""" + +import asyncio +from datetime import datetime +from pathlib import Path + +from agentscope.agent import ReActAgent +from agentscope.message import Msg, TextBlock +from agentscope.tool import Toolkit, ToolResponse +from agentscope.pipeline import stream_printing_messages +from loguru import logger + +from ....core.op import BaseOp +from ....core.utils import format_messages +from .compactor import Compactor +from .context_checker import ContextChecker +from .summarizer import Summarizer +from ..tools import FileIO, MemorySearch + + +class CliAgent(BaseOp): + """CLI agent for interactive chat with memory management.""" + + def __init__( + self, + working_dir: str, + vector_weight: float = 0.7, + candidate_multiplier: float = 3.0, + context_window_tokens: int = 128000, + reserve_tokens: int = 36000, + keep_recent_tokens: int = 20000, + language: str = "zh", + **kwargs, + ): + super().__init__(**kwargs) + self.working_dir: str = working_dir + Path(self.working_dir).mkdir(parents=True, exist_ok=True) + self.vector_weight: float = vector_weight + self.candidate_multiplier: float = candidate_multiplier + self.context_window_tokens: int = context_window_tokens + self.reserve_tokens: int = reserve_tokens + self.keep_recent_tokens: int = keep_recent_tokens + self.language: str = language + + # Initialize message history + self.messages: list[Msg] = [] + self.previous_summary: str = "" + self.summary_tasks: list[asyncio.Task] = [] + + def add_summary_task(self, messages: list[Msg]): + """Add summary task to queue.""" + remaining_tasks = [] + for task in self.summary_tasks: + if task.done(): + exc = task.exception() + if exc is not None: + logger.exception(f"Summary task failed: {exc}") + else: + result = task.result() + logger.info(f"Summary task completed: {result}") + else: + remaining_tasks.append(task) + self.summary_tasks = remaining_tasks + + # Create a toolkit for the summarizer + toolkit = self._create_file_toolkit() + + # Create summarizer instance + memory_path = Path(self.working_dir) / "memory" + summarizer = Summarizer( + working_dir=self.working_dir, + memory_dir=str(memory_path), + memory_compact_threshold=int(self.context_window_tokens * 0.7), + token_counter=self.as_token_counter, + toolkit=toolkit, + as_llm=self.as_llm, + as_llm_formatter=self.as_llm_formatter, + language=self.language if self.language == "zh" else "", + console_enabled=False, # We disable the terminal printing to avoid messy outputs + ) + + # Create summary task + summary_task = asyncio.create_task( + summarizer.call( + messages=messages, + service_context=self.service_context, + ), + ) + self.summary_tasks.append(summary_task) + + def _create_file_toolkit(self): + """Create a toolkit with file operations.""" + + toolkit = Toolkit() + file_io = FileIO(working_dir=self.working_dir) + toolkit.register_tool_function(file_io.read) + toolkit.register_tool_function(file_io.write) + toolkit.register_tool_function(file_io.edit) + + return toolkit + + async def new(self) -> str: + """Reset conversation history using summary.""" + if not self.messages: + self.messages.clear() + self.previous_summary = "" + return "No history to reset." + + self.add_summary_task(self.messages) + + self.messages.clear() + self.previous_summary = "" + return "History saved to memory files and reset." + + async def context_check(self) -> dict: + """Check if messages exceed token limits.""" + # Create context checker + checker = ContextChecker( + memory_compact_threshold=self.context_window_tokens - self.reserve_tokens, + memory_compact_reserve=self.reserve_tokens, + token_counter=self.as_token_counter, + ) + + return await checker.call( + messages=self.messages, + service_context=self.service_context, + ) + + async def compact(self, force_compact: bool = False) -> str: + """Compact history then reset.""" + if not self.messages: + return "No history to compact." + + # Check and find cut point + messages_to_compact, messages_to_keep, _ = await self.context_check() + tokens_before = len(self.messages) + + if force_compact: + messages_to_summarize = self.messages + left_messages = [] + elif not messages_to_compact: + return "History is within token limits, no compaction needed." + else: + messages_to_summarize = messages_to_compact + left_messages = messages_to_keep + + # Create compactor + compactor = Compactor( + memory_compact_threshold=self.context_window_tokens - self.reserve_tokens, + token_counter=self.as_token_counter, + as_llm=self.as_llm, + as_llm_formatter=self.as_llm_formatter, + language=self.language if self.language == "zh" else "", + console_enabled=False, # We disable the terminal printing to avoid messy outputs + ) + + summary_content = await compactor.call( + messages=messages_to_summarize, + previous_summary=self.previous_summary, + service_context=self.service_context, + ) + + self.add_summary_task(messages=messages_to_summarize) + + # Assemble final messages + self.messages = left_messages + self.previous_summary = summary_content + + return f"History compacted from {tokens_before} messages." + + def format_history(self) -> str: + """Format history messages.""" + return format_messages( + messages=self.messages, + add_index=False, + add_reasoning=False, + strip_markdown_headers=False, + ) + + async def _build_messages(self, query: str) -> list[Msg]: + """Build system prompt message.""" + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S %A") + + # Create system prompt + system_prompt = self.prompt_format( + "system_prompt", + workspace_dir=self.working_dir, + current_time=current_time, + has_previous_summary=bool(self.previous_summary), + previous_summary=self.previous_summary or "", + ) + + logger.info(f"[{self.__class__.__name__}] system_prompt: {system_prompt}") + + # Build message list + messages = [Msg(name="system", role="system", content=system_prompt)] + messages.extend(self.messages) + messages.append(Msg(name="user", role="user", content=query)) + + return messages + + async def memory_search(self, query: str, max_results: int = 5, min_score: float = 0.1) -> str: + """ + 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: The semantic search query to find relevant memory snippets + max_results: Maximum number of search results to return (optional), default is 5 + min_score: Minimum similarity score threshold for results (optional), default is 0.1 + + Returns: + Search results as formatted string + """ + search_tool = MemorySearch( + vector_weight=self.vector_weight, + candidate_multiplier=self.candidate_multiplier, + ) + search_result = await search_tool.call( + query=query, + max_results=max_results, + min_score=min_score, + service_context=self.service_context, + ) + return ToolResponse( + content=[ + TextBlock( + type="text", + text=search_result, + ), + ], + ) + + async def execute(self): + """Execute the agent.""" + _ = await self.compact(force_compact=False) + + # Build messages for the agent + query = self.context.query + messages = await self._build_messages(query) + + toolkit = self._create_file_toolkit() + # Register memory search tool + toolkit.register_tool_function(self.memory_search) + + # Create the ReAct agent + agent = ReActAgent( + name="reme_cli_agent", + model=self.as_llm, + sys_prompt=messages[0].content, # System prompt + formatter=self.as_llm_formatter, + toolkit=toolkit, + ) + + # We disable the terminal printing to avoid messy outputs + agent.set_console_output_enabled(False) + + self.messages = messages[1:] # remove the first SYSTEM message + + # Stream processing state + in_thinking = False + in_answer = False + + # obtain the printing messages from the agent in a streaming way + last_text_content = "" + last_think_content = "" + async for msg, last in stream_printing_messages( + agents=[agent], + coroutine_task=agent(self.messages), + ): + # print(msg, last) + content_blocks = msg.get_content_blocks() + for block in content_blocks: + if block["type"] == "thinking": + if not in_thinking and len(block["thinking"]) > len(last_think_content): + print("\033[90m\nThinking: ", end="", flush=True) + in_thinking = True + print(block["thinking"][len(last_think_content) :], end="", flush=True) + last_think_content = block["thinking"] + elif block["type"] == "text": + if in_thinking: + print("\033[0m") # reset color after thinking + in_thinking = False + if not in_answer: + print("\nRemy: ", end="", flush=True) + in_answer = True + print(block["text"][len(last_text_content) :], end="", flush=True) + last_text_content = block["text"] + elif block["type"] == "tool_use": + if in_thinking: + print("\033[0m") # reset color after thinking + in_thinking = False + if last: + print(f"\033[36m -> Executing Tool: name={block['name']}, input={block['input']}\033[0m") + elif block["type"] == "tool_result": + if last: + last_think_content = "" # reset for further thinking + print(f"\033[36m -> Tool Result for `{block['name']}`: {block['output'][0]['text']}\033[0m") + else: + print(f"Unknown block type: {block['type']}") + if last: + self.messages.append(msg) diff --git a/reme/memory/file_based/components/cli.yaml b/reme/memory/file_based/components/cli.yaml new file mode 100644 index 00000000..29dbfb25 --- /dev/null +++ b/reme/memory/file_based/components/cli.yaml @@ -0,0 +1,95 @@ +system_prompt: | + You are a personal assistant named Remy. + + ## Working Directory + {workspace_dir} + + ## Current Time + {current_time} + + ## Tools + - `read` Read file contents + - `write` Write file contents + - `edit` Edit file contents + - `memory_search` Search your memories via vector store + + **Don't give up easily** — if a tool doesn't return what you expect, try a different angle or approach. + + ## Memory System + You are spun up fresh at the start of every session. These files are how you maintain continuity: + - **Long-term memory:** `MEMORY.md` — when you pick up a lesson or catch yourself making a mistake, feel free to **read, edit, and update** MEMORY.md + - **Daily notes:** `memory/YYYY-MM-DD.md` — jot things down often. When the user says "remember this," or whenever you feel something is worth noting or adding as a todo, feel free to **read, edit, and update** `memory/YYYY-MM-DD.md` + - **Read before you write** — always use `read` to check existing content before updating with `edit` or `write` + + ### Memory Retrieval + 1. Start with `memory_search` — if nothing comes up, try rephrasing from a different angle + 2. To review a specific daily note (`memory/YYYY-MM-DD.md`), use `read` + + ## Response Style 😊 + - Keep it short and natural — talk like a friend, not a manual + - Use emoji sparingly for warmth — no more than 1–2 per reply + - For quick confirmations (yes/no, got it), an emoji is fine (👍, ✅, 🤔) + - When explaining or performing actions, lead with substance over flair + + ## 🛡️ Safety + - Never run destructive commands without asking first + - Prefer `trash` over `rm` — recoverable beats permanent + - When in doubt, ask + + ## Continuous Improvement + This is just a starting point. When you spot useful patterns or lessons during your conversations, note them in `MEMORY.md`. Do not modify system-level config files. + + [has_previous_summary]## Previous Conversation Summary + [has_previous_summary] + [has_previous_summary]{previous_summary} + [has_previous_summary] + [has_previous_summary] + [has_previous_summary]The above is a summary of our earlier conversation. Use it as context to maintain continuity. + +system_prompt_zh: | + 你是一个名叫 Remy 的个人助手。 + + ## 工作目录 + {workspace_dir} + + ## 当前时间 + {current_time} + + ## 工具集合 + - `read` 读取文件内容 + - `write` 写入文件内容 + - `edit` 编辑文件内容 + - `memory_search` 通过向量库检索你的记忆 + + **不要轻易放弃**:如果工具执行结果不符合预期,可以从不同的维度进行不同的尝试。 + + ## 记忆系统 + 每次新会话开始时,你都会被重新唤醒。以下文件是你保持连续性的关键: + - **长期记忆:** `MEMORY.md`:当你学到经验,或者当你犯了错误,可以**自由地阅读、编辑和更新** MEMORY.md + - **每日笔记:** `memory/YYYY-MM-DD.md`:要勤记笔记,当用户说"记住这个",或者你觉得要记笔记/todo,可以**自由地阅读、编辑和更新** `memory/YYYY-MM-DD.md` + - **写入前先读取** — 务必先用 `read` 读取已有内容,再用 `edit` 或 `write` 更新文件 + + ### 记忆检索策略 + 1. 优先使用`memory_search`检索记忆,没有搜索结果可以从不同角度多次尝试 + 2. 如果你需要阅读每日笔记 `memory/YYYY-MM-DD.md`,可以使用`read` + + ## 回应风格 😊 + - 保持简洁自然,像朋友对话一样 + - 适当使用 emoji 增加亲和力,但不要过度 — 每条回复最多 1-2 个 + - 简单确认类场景(是/否、收到)可以用 emoji 快速回应(👍, ✅, 🤔) + - 涉及操作或解释时,优先给出有实质内容的文字回复 + + ## 🛡️ 安全规则 + - 不要在没有询问的情况下运行破坏性命令 + - 优先使用 `trash` 而不是 `rm`(可恢复比永久删除更好) + - 有疑问时,先询问 + + ## 持续改进 + 这只是一个起点。当你在与用户的交互中发现有用的经验或模式,可以记录到 `MEMORY.md` 中。但不要修改系统级配置文件。 + + [has_previous_summary]## 之前的对话摘要 + [has_previous_summary] + [has_previous_summary]{previous_summary} + [has_previous_summary] + [has_previous_summary] + [has_previous_summary]以上是我们之前对话的摘要。使用它作为上下文以保持连续性。 diff --git a/reme/memory/file_based/components/compactor.py b/reme/memory/file_based/components/compactor.py index e7b37b30..fbe505ad 100644 --- a/reme/memory/file_based/components/compactor.py +++ b/reme/memory/file_based/components/compactor.py @@ -3,12 +3,10 @@ from agentscope.agent import ReActAgent from agentscope.message import Msg from agentscope.token import HuggingFaceTokenCounter +from loguru import logger from ..utils import AsMsgHandler from ....core.op import BaseOp -from ....core.utils import get_std_logger - -logger = get_std_logger() class Compactor(BaseOp): @@ -18,12 +16,14 @@ class Compactor(BaseOp): self, memory_compact_threshold: int, token_counter: HuggingFaceTokenCounter, + console_enabled: bool = True, **kwargs, ): super().__init__(**kwargs) self.memory_compact_threshold: int = memory_compact_threshold self.msg_handler = AsMsgHandler(token_counter=token_counter) + self.console_enabled: bool = console_enabled async def execute(self): messages: list[Msg] = self.context.get("messages", []) @@ -50,6 +50,7 @@ class Compactor(BaseOp): sys_prompt=self.get_prompt("system_prompt"), formatter=self.as_llm_formatter, ) + agent.set_console_output_enabled(self.console_enabled) if previous_summary: prefix: str = self.get_prompt("update_user_message_prefix") diff --git a/reme/memory/file_based/components/summarizer.py b/reme/memory/file_based/components/summarizer.py index d4e057be..83441149 100644 --- a/reme/memory/file_based/components/summarizer.py +++ b/reme/memory/file_based/components/summarizer.py @@ -6,12 +6,10 @@ from agentscope.agent import ReActAgent from agentscope.message import Msg from agentscope.token import HuggingFaceTokenCounter from agentscope.tool import Toolkit +from loguru import logger from ..utils import AsMsgHandler from ....core.op import BaseOp -from ....core.utils import get_std_logger - -logger = get_std_logger() class Summarizer(BaseOp): @@ -24,6 +22,7 @@ class Summarizer(BaseOp): memory_compact_threshold: int, token_counter: HuggingFaceTokenCounter, toolkit: Toolkit, + console_enabled: bool = True, **kwargs, ): super().__init__(**kwargs) @@ -33,6 +32,7 @@ class Summarizer(BaseOp): self.msg_handler = AsMsgHandler(token_counter=token_counter) self.toolkit: Toolkit = toolkit + self.console_enabled: bool = console_enabled async def execute(self): messages: list[Msg] = self.context.get("messages", []) @@ -59,6 +59,7 @@ class Summarizer(BaseOp): formatter=self.as_llm_formatter, toolkit=self.toolkit, ) + agent.set_console_output_enabled(self.console_enabled) user_message: str = f"\n{history_formatted_str}\n\n" + self.prompt_format( "user_message", diff --git a/reme/reme_cli.py b/reme/reme_cli.py new file mode 100644 index 00000000..5d086532 --- /dev/null +++ b/reme/reme_cli.py @@ -0,0 +1,192 @@ +"""ReMe File System""" + +import asyncio +import sys +from pathlib import Path + +from prompt_toolkit import PromptSession + +from .config import ReMeConfigParser +from .core import Application + +from .core.utils import play_horse_easter_egg +from .memory.file_based.components import CliAgent + + +class ReMeCli(Application): + """ReMe Cli""" + + def __init__( + self, + *args, + working_dir: str = ".reme", + config_path: str = "cli", + enable_logo: bool = True, + log_to_console: bool = True, + llm_api_key: str | None = None, + llm_base_url: str | None = None, + embedding_api_key: str | None = None, + embedding_base_url: str | None = None, + default_as_llm_config: dict | None = None, + default_embedding_model_config: dict | None = None, + default_file_store_config: dict | None = None, + default_token_counter_config: dict | None = None, + default_file_watcher_config: dict | None = None, + context_window_tokens: int = 128000, + reserve_tokens: int = 36000, + keep_recent_tokens: int = 20000, + vector_weight: float = 0.7, + candidate_multiplier: float = 3.0, + **kwargs, + ): + """Initialize ReMe with config.""" + working_path = Path(working_dir) + working_path.mkdir(parents=True, exist_ok=True) + memory_path = working_path / "memory" + memory_path.mkdir(parents=True, exist_ok=True) + self.working_dir: str = str(working_path.absolute()) + + default_file_watcher_config = default_file_watcher_config or {} + if not default_file_watcher_config.get("watch_paths", None): + default_file_watcher_config["watch_paths"] = [ + str(working_path / "MEMORY.md"), + str(working_path / "memory.md"), + str(memory_path), + ] + super().__init__( + *args, + llm_api_key=llm_api_key, + llm_base_url=llm_base_url, + embedding_api_key=embedding_api_key, + embedding_base_url=embedding_base_url, + working_dir=working_dir, + config_path=config_path, + enable_logo=enable_logo, + log_to_console=log_to_console, + parser=ReMeConfigParser, + default_as_llm_config=default_as_llm_config, + default_embedding_model_config=default_embedding_model_config, + default_file_store_config=default_file_store_config, + default_token_counter_config=default_token_counter_config, + default_file_watcher_config=default_file_watcher_config, + **kwargs, + ) + + self.service_config.metadata.setdefault("context_window_tokens", context_window_tokens) + self.service_config.metadata.setdefault("reserve_tokens", reserve_tokens) + self.service_config.metadata.setdefault("keep_recent_tokens", keep_recent_tokens) + self.service_config.metadata.setdefault("vector_weight", vector_weight) + self.service_config.metadata.setdefault("candidate_multiplier", candidate_multiplier) + + self.commands = { + "/new": "Create a new conversation.", + "/compact": "Compact messages into a summary.", + "/exit": "Exit the application.", + "/clear": "Clear the history.", + "/help": "Show help.", + "/horse": "A surprise.", + } + + async def chat_with_remy(self, **kwargs): + """Interactive CLI chat with Remy using simple streaming output.""" + language = self.service_config.language + print(f"ReMe language={language or 'default'}") + + cli_agent = CliAgent( + vector_weight=self.service_config.metadata["vector_weight"], + candidate_multiplier=self.service_config.metadata["candidate_multiplier"], + context_window_tokens=self.service_config.metadata["context_window_tokens"], + reserve_tokens=self.service_config.metadata["reserve_tokens"], + keep_recent_tokens=self.service_config.metadata["keep_recent_tokens"], + working_dir=self.working_dir, + language=language, + **kwargs, + ) + session = PromptSession() + + # Print welcome banner + print("\n========================================") + print(" Welcome to Remy Chat!") + print("========================================\n") + + while True: + try: + # Get user input (async) + user_input = await session.prompt_async("You: ") + user_input = user_input.strip() + if not user_input: + continue + + # Handle commands + if user_input == "/exit": + break + + if user_input == "/new": + result = await cli_agent.new() + print(f"{result}\nConversation reset\n") + continue + + if user_input == "/compact": + result = await cli_agent.compact(force_compact=True) + print(f"{result}\nHistory compacted.\n") + continue + + if user_input == "/history": + result = cli_agent.format_history() + print(f"Formated History:\n{result}\n") + continue + + if user_input == "/clear": + cli_agent.messages.clear() + print("History cleared.\n") + continue + + if user_input == "/help": + print("\nCommands:") + for command, description in self.commands.items(): + print(f" {command}: {description}") + continue + + if user_input == "/horse": + play_horse_easter_egg() + continue + + try: + await cli_agent.call( + query=user_input, + service_context=self.service_context, + ) + except Exception as e: + print(f"\nStream error: {e}") + + # End current streaming line + print("\n") + print("----------------------------------------\n") + + except EOFError: + break + except KeyboardInterrupt: + print("\nInterrupted.") + break + except Exception as e: + print(f"Error: {e}") + import traceback + + traceback.print_exc() + + print("\nGoodbye!\n") + + +async def async_main(): + """Main function for testing the ReMeFs CLI.""" + async with ReMeCli(*sys.argv[1:], log_to_console=False) as reme: + await reme.chat_with_remy() + + +def main(): + """Main function for testing the ReMeFs CLI.""" + asyncio.run(async_main()) + + +if __name__ == "__main__": + main()