mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-20 00:12:50 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
2f682b8d7c
16 changed files with 703 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
9
reme/core/as_token_counter/__init__.py
Normal file
9
reme/core/as_token_counter/__init__.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"] = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
303
reme/memory/file_based/components/cli.py
Normal file
303
reme/memory/file_based/components/cli.py
Normal file
|
|
@ -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)
|
||||
95
reme/memory/file_based/components/cli.yaml
Normal file
95
reme/memory/file_based/components/cli.yaml
Normal file
|
|
@ -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]<previous-summary>
|
||||
[has_previous_summary]{previous_summary}
|
||||
[has_previous_summary]</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]<previous-summary>
|
||||
[has_previous_summary]{previous_summary}
|
||||
[has_previous_summary]</previous-summary>
|
||||
[has_previous_summary]
|
||||
[has_previous_summary]以上是我们之前对话的摘要。使用它作为上下文以保持连续性。
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"<conversation>\n{history_formatted_str}\n</conversation>\n" + self.prompt_format(
|
||||
"user_message",
|
||||
|
|
|
|||
192
reme/reme_cli.py
Normal file
192
reme/reme_cli.py
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Reference in a new issue