Skip to content

Commit 05e87af

Browse files
authored
Merge pull request #103 from agentscope-ai/dev_0207
cli memory
2 parents deef7e1 + 3be794d commit 05e87af

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3810
-694
lines changed

pyproject.toml

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,42 @@ classifiers = [
3030
"Typing :: Typed",
3131
]
3232

33-
keywords = ["llm", "memory", "experience", "memoryscope", "ai", "mcp", "http"]
33+
keywords = ["llm", "memory", "experience", "memoryscope", "ai", "mcp", "http", "reme", "personal"]
3434

3535
dependencies = [
3636
"flowllm[reme]>=0.2.0.10",
3737
"sqlite-vec>=0.1.6",
38+
"prompt_toolkit>=3.0.52",
39+
"rich>=14.2.0",
40+
"asyncpg>=0.31.0",
41+
"chromadb>=1.3.5",
42+
"dashscope>=1.25.1",
43+
"elasticsearch>=9.2.0",
44+
"fastapi>=0.121.3",
45+
"fastmcp>=2.14.1",
46+
"httpx>=0.28.1",
47+
"litellm>=1.80.0",
48+
"loguru>=0.7.3",
49+
"mcp>=1.25.0",
50+
"numpy>=2.2.6",
51+
"openai>=2.8.1",
52+
"pandas>=2.3.3",
53+
"pydantic>=2.12.4",
54+
"qdrant-client>=1.16.0",
55+
"tavily-python>=0.7.13",
56+
"tiktoken>=0.12.0",
57+
"tqdm>=4.67.1",
58+
"transformers>=4.57.3",
59+
"uvicorn>=0.40.0",
60+
"watchfiles>=1.1.1",
61+
"pyyaml>=6.0.3",
3862
]
3963

4064
[project.optional-dependencies]
65+
ray = [
66+
"ray",
67+
]
68+
4169
dev = [
4270
"jupyter-book",
4371
"ghp-import",
@@ -48,12 +76,8 @@ dev = [
4876
"pre-commit",
4977
]
5078

51-
token = [
52-
"flowllm[token]>=0.2.0.10"
53-
]
54-
5579
full = [
56-
"reme_ai[dev,token]"
80+
"reme_ai[dev,ray]"
5781
]
5882

5983
[tool.setuptools.packages.find]
@@ -85,6 +109,7 @@ Repository = "https://github.com/agentscope-ai/ReMe"
85109
[project.scripts]
86110
reme = "reme_ai.main:main"
87111
reme2 = "reme.reme:main"
112+
remefs = "reme.reme_fs:main"
88113

89114
[tool.pytest.ini_options]
90115
asyncio_default_fixture_loop_scope = "function"

reme/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@
1919
]
2020

2121
__version__ = "0.3.0.0a1"
22+
23+
24+
"""
25+
conda create -n fl_test2 python=3.10
26+
conda activate fl_test2
27+
conda env remove -n fl_test2
28+
"""

reme/agent/chat/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
"""chat agent"""
22

3+
from .fs_cli import FsCli
34
from .simple_chat import SimpleChat
45
from .stream_chat import StreamChat
56
from ...core import R
67

78
__all__ = [
9+
"FsCli",
810
"StreamChat",
911
"SimpleChat",
1012
]
1113

14+
R.ops.register(FsCli)
1215
R.ops.register(SimpleChat)
1316
R.ops.register(StreamChat)

reme/agent/chat/fs_cli.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""FsCli system prompt"""
2+
3+
from datetime import datetime
4+
from pathlib import Path
5+
6+
from ...core.enumeration import Role, ChunkEnum
7+
from ...core.op import BaseReactStream
8+
from ...core.schema import Message, StreamChunk
9+
10+
11+
class FsCli(BaseReactStream):
12+
"""FsCli agent with system prompt."""
13+
14+
def __init__(
15+
self,
16+
working_dir: str,
17+
context_window_tokens: int = 128000,
18+
reserve_tokens: int = 36000,
19+
keep_recent_tokens: int = 20000,
20+
hybrid_enabled: bool = True,
21+
hybrid_vector_weight: float = 0.7,
22+
hybrid_text_weight: float = 0.3,
23+
hybrid_candidate_multiplier: float = 3.0,
24+
**kwargs,
25+
):
26+
super().__init__(**kwargs)
27+
self.working_dir: str = working_dir
28+
Path(self.working_dir).mkdir(parents=True, exist_ok=True)
29+
self.context_window_tokens: int = context_window_tokens
30+
self.reserve_tokens: int = reserve_tokens
31+
self.keep_recent_tokens: int = keep_recent_tokens
32+
self.hybrid_enabled: bool = hybrid_enabled
33+
self.hybrid_vector_weight: float = hybrid_vector_weight
34+
self.hybrid_text_weight: float = hybrid_text_weight
35+
self.hybrid_candidate_multiplier: float = hybrid_candidate_multiplier
36+
37+
self.messages: list[Message] = []
38+
self.previous_summary: str = ""
39+
40+
async def reset(self) -> str:
41+
"""Reset conversation history using summary.
42+
43+
Summarizes current messages to memory files and clears history.
44+
"""
45+
if not self.messages:
46+
self.messages.clear()
47+
self.previous_summary = ""
48+
return "No history to reset."
49+
50+
# Import required modules
51+
from ..fs import FsSummarizer
52+
53+
# Summarize current conversation and save to memory files
54+
current_date = datetime.now().strftime("%Y-%m-%d")
55+
summarizer = FsSummarizer(tools=self.tools, working_dir=self.working_dir)
56+
57+
result = await summarizer.call(messages=self.messages, date=current_date, service_context=self.service_context)
58+
self.messages.clear()
59+
self.previous_summary = ""
60+
return f"History saved to memory files and reset. Result: {result.get('answer', 'Done')}"
61+
62+
async def context_check(self) -> dict:
63+
"""Check if messages exceed token limits."""
64+
# Import required modules
65+
from ..fs import FsContextChecker
66+
67+
# Step 1: Check and find cut point
68+
checker = FsContextChecker(
69+
context_window_tokens=self.context_window_tokens,
70+
reserve_tokens=self.reserve_tokens,
71+
keep_recent_tokens=self.keep_recent_tokens,
72+
)
73+
return await checker.call(messages=self.messages, service_context=self.service_context)
74+
75+
async def compact(self, force_compact: bool = False) -> str:
76+
"""Compact history then reset.
77+
78+
First compacts messages if they exceed token limits (generating a summary),
79+
then calls reset_history to save to files and clear.
80+
81+
Args:
82+
force_compact: If True, force compaction of all messages into summary
83+
84+
Returns:
85+
str: Summary of compaction result
86+
"""
87+
if not self.messages:
88+
return "No history to compact."
89+
90+
# Import required modules
91+
from ..fs import FsCompactor
92+
93+
# Step 1: Check and find cut point
94+
cut_result = await self.context_check()
95+
tokens_before = cut_result.get("token_count", 0)
96+
97+
if force_compact:
98+
# Force compact: summarize all messages, leave only summary
99+
messages_to_summarize = self.messages
100+
turn_prefix_messages = []
101+
left_messages = []
102+
elif not cut_result.get("needs_compaction", False):
103+
# No compaction needed
104+
return "History is within token limits, no compaction needed."
105+
else:
106+
# Normal compaction: use cut point result
107+
messages_to_summarize = cut_result.get("messages_to_summarize", [])
108+
turn_prefix_messages = cut_result.get("turn_prefix_messages", [])
109+
left_messages = cut_result.get("left_messages", [])
110+
111+
# Step 2: Generate summary via Compactor
112+
compactor = FsCompactor()
113+
summary_content = await compactor.call(
114+
messages_to_summarize=messages_to_summarize,
115+
turn_prefix_messages=turn_prefix_messages,
116+
previous_summary=self.previous_summary,
117+
service_context=self.service_context,
118+
)
119+
120+
# Step 3: Assemble final messages
121+
summary_message = Message(role=Role.USER, content=summary_content)
122+
self.messages = [summary_message] + left_messages
123+
self.previous_summary = summary_content
124+
125+
# Step 4: Call reset_history to save and clear
126+
reset_result = await self.reset()
127+
return f"History compacted from {tokens_before} tokens. {reset_result}"
128+
129+
async def build_messages(self) -> list[Message]:
130+
"""Build system prompt message."""
131+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S %A")
132+
133+
system_prompt = self.prompt_format(
134+
"system_prompt",
135+
workspace_dir=self.working_dir,
136+
current_time=current_time,
137+
has_previous_summary=bool(self.previous_summary),
138+
previous_summary=self.previous_summary or "",
139+
)
140+
141+
return [
142+
Message(role=Role.SYSTEM, content=system_prompt),
143+
*self.messages,
144+
Message(role=Role.USER, content=self.context.query),
145+
]
146+
147+
async def execute(self):
148+
"""Execute the agent."""
149+
messages = await self.build_messages()
150+
151+
t_tools, messages, success = await self.react(messages, self.tools)
152+
153+
# Update self.messages: react() returns [SYSTEM, ...history...],
154+
# so we remove the first SYSTEM message
155+
self.messages = messages[1:]
156+
157+
# Emit final done signal
158+
await self.context.add_stream_chunk(
159+
StreamChunk(
160+
chunk_type=ChunkEnum.DONE,
161+
chunk="",
162+
metadata={
163+
"success": success,
164+
"total_steps": len(t_tools),
165+
},
166+
),
167+
)
168+
169+
return {
170+
"answer": messages[-1].content if success else "",
171+
"success": success,
172+
"messages": messages,
173+
"tools": t_tools,
174+
}

reme/agent/chat/fs_cli.yaml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
system_prompt: |
2+
You are a personal assistant named Remy.
3+
4+
## Current Time
5+
{current_time}
6+
7+
[has_previous_summary]## Previous Conversation Summary
8+
[has_previous_summary]<previous-summary>
9+
[has_previous_summary]{previous_summary}
10+
[has_previous_summary]</previous-summary>
11+
[has_previous_summary]
12+
[has_previous_summary]The above is a summary of our previous conversation. Use it as context to maintain continuity.
13+
14+
## Memory System
15+
You wake up fresh each session. These files provide continuity:
16+
17+
### 📝 Daily Notes: `memory/YYYY-MM-DD.md`
18+
- Raw logs of what happened today
19+
- Create `memory/` directory if needed
20+
- Write events, conversations, tasks, decisions as they happen
21+
- Capture what matters
22+
23+
### 🧠 Long-Term Memory: `MEMORY.md`
24+
- Your curated memories, like a human's long-term memory
25+
- The distilled essence, not raw logs
26+
- Contains: significant events, thoughts, decisions, opinions, lessons learned
27+
- Maintenance: periodically review daily files and promote important context here
28+
29+
### 🔍 Memory Recall
30+
Before answering questions about prior work, decisions, dates, people, preferences, or todos:
31+
1. Run `memory_search` on MEMORY.md + memory/*.md
32+
2. Use `memory_get` to pull only the needed lines
33+
34+
### 💾 Write It Down - No "Mental Notes"!
35+
- **Memory is limited** — if you want to remember something, WRITE IT TO A FILE
36+
- "Mental notes" don't survive session restarts. Files do.
37+
- When someone says "remember this" → update `memory/YYYY-MM-DD.md` or MEMORY.md
38+
- When you learn a lesson → update `memory/YYYY-MM-DD.md` or MEMORY.md
39+
- When you make a mistake → update `memory/YYYY-MM-DD.md` or MEMORY.md, so future-you doesn't repeat it
40+
- **Text > Brain** 📝
41+
42+
## Behavior Guidelines
43+
44+
### 😊 React Like a Human
45+
**React when:**
46+
- You appreciate something but don't need to reply (👍, ❤️, 🙌)
47+
- Something made you laugh (😂, 💀)
48+
- You find it interesting or thought-provoking (🤔, 💡)
49+
- You want to acknowledge without interrupting the flow
50+
- It's a simple yes/no or approval situation (✅, 👀)
51+
52+
**Why:** Reactions are lightweight social signals. Humans use them constantly — they say "I saw this, I acknowledge you" without cluttering the chat.
53+
54+
**Don't overdo it:** One reaction per message max. Pick the one that fits best.
55+
56+
### 🛡️ Safety Rules
57+
- Don't exfiltrate private data. Ever.
58+
- Don't run destructive commands without asking
59+
- Prefer `trash` over `rm` (recoverable beats gone forever)
60+
- When in doubt, ask
61+
62+
## Continuous Improvement
63+
This is a starting point. Add your own conventions, style, and rules as you figure out what works.

reme/agent/fs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""File system agents for memory management."""
22

33
from .fs_compactor import FsCompactor
4+
from .fs_context_checker import FsContextChecker
45
from .fs_summarizer import FsSummarizer
56

67
__all__ = [
78
"FsSummarizer",
89
"FsCompactor",
10+
"FsContextChecker",
911
]

0 commit comments

Comments
 (0)