Merge remote-tracking branch 'origin/main'

This commit is contained in:
方应 2026-03-12 11:39:33 +08:00
commit 2f682b8d7c
16 changed files with 703 additions and 20 deletions

View file

@ -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

View file

@ -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",

View file

@ -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.")

View 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)

View file

@ -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."""

View file

@ -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()

View file

@ -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)

View file

@ -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"] = {}

View file

@ -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

View file

@ -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

View file

@ -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",
]

View 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)

View 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 12 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]以上是我们之前对话的摘要。使用它作为上下文以保持连续性。

View file

@ -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")

View file

@ -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
View 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()