Skip to content

Commit 5a61f3b

Browse files
committed
add: test script
1 parent 6a97fa2 commit 5a61f3b

File tree

4 files changed

+271
-67
lines changed

4 files changed

+271
-67
lines changed

README.md

Lines changed: 65 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ memU v0.3.0-Alpha has been released! This version initializes the memorize and r
4040
Starting from this release, memU will roll out multiple features in the short- to mid-term:
4141

4242
### Core capabilities iteration
43-
- [ ] **Multi-modal enhancements** – Support for images, audio, and video
43+
- [x] **Multi-modal enhancements** – Support for images, audio, and video
4444
- [ ] **Intention** – Higher-level decision-making and goal management
4545
- [ ] **Multi-client support** – Switch between OpenAI, Deepseek, Gemini, etc.
4646
- [ ] **Data persistence expansion** – Support for Postgres, S3, DynamoDB
@@ -88,76 +88,76 @@ Through this three-layer design, **MemU brings genuine memory into the agent lay
8888
A feedback-driven mechanism continuously adapts the memory structure according to real usage patterns.
8989
<img width="1280" height="312" alt="image" src="https://github.com/user-attachments/assets/e2c0ac0c-e5cb-44a9-b880-89be142e1ca5" />
9090

91-
## 🚀Get Started
91+
#### Quick Start
9292

93-
There are three ways to get started with MemU:
94-
95-
### ☁️ Cloud Version ([Online Platform](https://app.memu.so))
96-
97-
The fastest way to integrate your application with memU. Perfect for teams and individuals who want immediate access without setup complexity. We host the models, APIs, and cloud storage, ensuring your application gets the best quality AI memory.
98-
99-
- **Instant Access** - Start integrating AI memories in minutes
100-
- **Managed Infrastructure** - We handle scaling, updates, and maintenance for optimal memory quality
101-
- **Premium Support** - Subscribe and get priority assistance from our engineering team
102-
103-
### Step-by-step
104-
105-
**Step 1:** Create account
106-
107-
Create account on https://app.memu.so
108-
109-
Then, go to https://app.memu.so/api-key/ for generating api-keys.
110-
111-
**Step 2:** Add three lines to your code
112-
```python
113-
pip install memu-py
114-
115-
# Example usage
116-
from memu import MemuClient
93+
**Step 1: Install**
94+
```bash
95+
pip install -e .
11796
```
11897

119-
**Step 3:** Quick Start
98+
**Step 2: Run the example**
12099
```python
121-
# Initialize
122-
memu_client = MemuClient(
123-
base_url="https://api.memu.so",
124-
api_key=os.getenv("MEMU_API_KEY")
125-
)
126-
memu_client.memorize_conversation(
127-
conversation=conversation_text, # Recommend longer conversation (~8000 tokens), see https://memu.pro/blog/memu-best-practice for details
128-
user_id="user001",
129-
user_name="User",
130-
agent_id="assistant001",
131-
agent_name="Assistant"
132-
)
100+
from memu.app import MemoryUser
101+
import logging
102+
103+
async def test_memory_service():
104+
logging.basicConfig(
105+
level=logging.INFO,
106+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
107+
)
108+
logger = logging.getLogger("memu")
109+
logger.setLevel(logging.DEBUG)
110+
111+
# Initialize MemoryUser with your OpenAI API key
112+
service = MemoryUser(llm_config={"api_key": "your-openai-api-key"})
113+
114+
# Memorize a conversation
115+
memory = await service.memorize(
116+
resource_url="tests/data/example_conversation.json",
117+
modality="conversation"
118+
)
119+
120+
# Example conversation history for query rewriting
121+
conversation_history = [
122+
{"role": "user", "content": "Tell me about the user's preferences"},
123+
{"role": "assistant", "content": "I'd be happy to help. Let me search the memory."},
124+
{"role": "user", "content": "What are their habits?"}
125+
]
126+
127+
# Test 1: RAG-based Retrieval with conversation history
128+
print("\n[Test 1] RAG-based Retrieval with conversation history")
129+
retrieved_rag = await service.retrieve(
130+
query="What are their habits?",
131+
conversation_history=conversation_history,
132+
method="rag",
133+
top_k=5
134+
)
135+
print(f"Method: {retrieved_rag.get('method')}")
136+
print(f"Original query: {retrieved_rag.get('original_query')}")
137+
print(f"Rewritten query: {retrieved_rag.get('rewritten_query')}")
138+
print(f"Results: {len(retrieved_rag.get('categories', []))} categories, "
139+
f"{len(retrieved_rag.get('items', []))} items")
140+
141+
# Test 2: LLM-based Retrieval with conversation history
142+
print("\n[Test 2] LLM-based Retrieval with conversation history")
143+
retrieved_llm = await service.retrieve(
144+
query="What are their habits?",
145+
conversation_history=conversation_history,
146+
method="llm",
147+
top_k=5
148+
)
149+
print(f"Method: {retrieved_llm.get('method')}")
150+
print(f"Original query: {retrieved_llm.get('original_query')}")
151+
print(f"Rewritten query: {retrieved_llm.get('rewritten_query')}")
152+
print(f"Results: {len(retrieved_llm.get('categories', []))} categories, "
153+
f"{len(retrieved_llm.get('items', []))} items")
154+
155+
if __name__ == "__main__":
156+
import asyncio
157+
asyncio.run(test_memory_service())
133158
```
134-
Check [API reference](docs/API_REFERENCE.md) or [our blog](https://memu.pro/blog) for more details.
135-
136-
📖 **See [`example/client/memory.py`](example/client/memory.py) for complete integration details**
137-
138-
**That's it!** MemU remembers everything and helps your AI learn from past conversations.
139-
140-
141-
### 🏢 Enterprise Edition
142-
143-
For organizations requiring maximum security, customization, control and best quality:
144-
145-
- **Commercial License** - Full proprietary features, commercial usage rights, white-labeling options
146-
- **Custom Development** - SSO/RBAC integration, dedicated algorithm team for scenario-specific framework optimization
147-
- **Intelligence & Analytics** - User behavior analysis, real-time production monitoring, automated agent optimization
148-
- **Premium Support** - 24/7 dedicated support, custom SLAs, professional implementation services
149-
150-
📧 **Enterprise Inquiries:** [contact@nevamind.ai](mailto:contact@nevamind.ai)
151-
152-
153-
### 🏠 Self-Hosting (Community Edition)
154-
For users and developers who prefer local control, data privacy, or customization:
155-
156-
* **Data Privacy** - Keep sensitive data within your infrastructure
157-
* **Customization** - Modify and extend the platform to fit your needs
158-
* **Cost Control** - Avoid recurring cloud fees for large-scale deployments
159159

160-
See [self hosting README](README.self_host.md)
160+
See [self hosting README](README.self_host.md) for more details.
161161

162162
---
163163

src/memu/app/service.py

Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from memu.prompts.memory_type import PROMPTS as MEMORY_TYPE_PROMPTS
1818
from memu.prompts.preprocess import PROMPTS as PREPROCESS_PROMPTS
1919
from memu.prompts.retrieve.judger import PROMPT as RETRIEVE_JUDGER_PROMPT
20+
from memu.prompts.retrieve.query_rewriter import PROMPT as QUERY_REWRITER_PROMPT
2021
from memu.storage.local_fs import LocalFS
2122
from memu.utils.video import VideoFrameExtractor
2223
from memu.vector.index import cosine_topk
@@ -777,9 +778,56 @@ def _validate_config(
777778
return model_type()
778779
return model_type.model_validate(config)
779780

780-
async def retrieve(self, query: str, *, top_k: int = 5) -> dict[str, Any]:
781+
async def retrieve(
782+
self,
783+
query: str,
784+
*,
785+
conversation_history: list[dict[str, str]] | None = None,
786+
method: str = "rag",
787+
top_k: int = 5,
788+
) -> dict[str, Any]:
789+
"""
790+
Retrieve relevant memories based on the query.
791+
792+
Args:
793+
query: The search query
794+
conversation_history: Optional conversation history for query rewriting
795+
method: Retrieval method - "rag" (vector similarity) or "llm" (LLM-based ranking)
796+
top_k: Number of top results to return
797+
798+
Returns:
799+
Dictionary containing original_query, rewritten_query, method, and retrieved results
800+
"""
801+
# Rewrite query if conversation history is provided
802+
original_query = query
803+
rewritten_query = query
804+
805+
if conversation_history:
806+
rewritten_query = await self._rewrite_query_with_history(query, conversation_history)
807+
logger.debug(f"Original query: {original_query}")
808+
logger.debug(f"Rewritten query: {rewritten_query}")
809+
810+
response: dict[str, Any] = {
811+
"original_query": original_query,
812+
"rewritten_query": rewritten_query,
813+
"method": method,
814+
"resources": [],
815+
"items": [],
816+
"categories": [],
817+
}
818+
819+
if method == "rag":
820+
return await self._retrieve_rag(rewritten_query, response, top_k)
821+
elif method == "llm":
822+
return await self._retrieve_llm(rewritten_query, response, top_k)
823+
else:
824+
msg = f"Unknown retrieval method '{method}'. Use 'rag' or 'llm'."
825+
raise ValueError(msg)
826+
827+
async def _retrieve_rag(self, query: str, response: dict[str, Any], top_k: int) -> dict[str, Any]:
828+
"""RAG-based retrieval using vector similarity search"""
829+
# Use query for embedding
781830
qvec = (await self.openai.embed([query]))[0]
782-
response: dict[str, list[dict[str, Any]]] = {"resources": [], "items": [], "categories": []}
783831
content_sections: list[str] = []
784832

785833
cat_hits, summary_lookup = await self._rank_categories_by_summary(qvec, top_k)
@@ -806,6 +854,126 @@ async def retrieve(self, query: str, *, top_k: int = 5) -> dict[str, Any]:
806854

807855
return response
808856

857+
async def _retrieve_llm(self, query: str, response: dict[str, Any], top_k: int) -> dict[str, Any]:
858+
"""LLM-based retrieval using language model to rank and select memories"""
859+
# Get all available memories
860+
all_categories = list(self.store.categories.values())
861+
all_items = list(self.store.items.values())
862+
all_resources = list(self.store.resources.values())
863+
864+
# Use LLM to select and rank relevant memories
865+
if all_categories:
866+
selected_categories = await self._llm_rank_memories(query, all_categories, "categories", top_k)
867+
response["categories"] = selected_categories
868+
869+
if all_items:
870+
selected_items = await self._llm_rank_memories(query, all_items, "items", top_k)
871+
response["items"] = selected_items
872+
873+
if all_resources:
874+
selected_resources = await self._llm_rank_memories(query, all_resources, "resources", top_k)
875+
response["resources"] = selected_resources
876+
877+
return response
878+
879+
async def _llm_rank_memories(
880+
self, query: str, memories: list[Any], memory_type: str, top_k: int
881+
) -> list[dict[str, Any]]:
882+
"""Use LLM to rank and select relevant memories"""
883+
if not memories:
884+
return []
885+
886+
# Limit to top 20 to avoid token limits
887+
sample_size = min(len(memories), 20)
888+
memories_to_rank = memories[:sample_size]
889+
890+
# Format memories for LLM
891+
formatted_memories = []
892+
for idx, mem in enumerate(memories_to_rank):
893+
if memory_type == "categories":
894+
content = f"Category: {mem.name}\nSummary: {mem.summary or 'N/A'}"
895+
elif memory_type == "items":
896+
content = f"Item: {mem.summary}"
897+
else: # resources
898+
content = f"Resource: {mem.caption or mem.url}"
899+
formatted_memories.append(f"[{idx}] {content}")
900+
901+
memories_text = "\n\n".join(formatted_memories)
902+
903+
# Create prompt for LLM ranking
904+
prompt = f"""Given the query and a list of memories, select the top {top_k} most relevant memories.
905+
Return only the indices (numbers) of the selected memories, separated by commas.
906+
907+
Query: {query}
908+
909+
Memories:
910+
{memories_text}
911+
912+
Output format: 0,3,7,... (indices only, comma-separated)
913+
Selected indices:"""
914+
915+
response_text = await self.openai.summarize(prompt, system_prompt=None)
916+
917+
# Parse selected indices
918+
selected_indices = self._parse_llm_indices(response_text, len(memories_to_rank))
919+
920+
# Return selected memories
921+
result = []
922+
for idx in selected_indices[:top_k]:
923+
mem = memories_to_rank[idx]
924+
mem_dict = {
925+
"id": mem.id,
926+
"score": 1.0 - (selected_indices.index(idx) * 0.1), # Decreasing score
927+
}
928+
if memory_type == "categories":
929+
mem_dict.update({"name": mem.name, "summary": mem.summary})
930+
elif memory_type == "items":
931+
mem_dict.update({"summary": mem.summary, "memory_type": mem.memory_type})
932+
else:
933+
mem_dict.update({"url": mem.url, "caption": mem.caption})
934+
result.append(mem_dict)
935+
936+
return result
937+
938+
def _parse_llm_indices(self, response: str, max_idx: int) -> list[int]:
939+
"""Parse indices from LLM response"""
940+
# Extract numbers from response
941+
numbers = re.findall(r"\d+", response)
942+
indices = []
943+
for num_str in numbers:
944+
idx = int(num_str)
945+
if 0 <= idx < max_idx and idx not in indices:
946+
indices.append(idx)
947+
return indices
948+
949+
async def _rewrite_query_with_history(self, query: str, conversation_history: list[dict[str, str]]) -> str:
950+
"""Rewrite query using conversation history to resolve references"""
951+
# Format conversation history
952+
history_text = "\n".join([
953+
f"{msg.get('role', 'unknown')}: {msg.get('content', '')}" for msg in conversation_history
954+
])
955+
956+
# Create prompt for query rewriting
957+
prompt = QUERY_REWRITER_PROMPT.format(
958+
conversation_history=self._escape_prompt_value(history_text), query=self._escape_prompt_value(query)
959+
)
960+
961+
# Get rewritten query from LLM
962+
response = await self.openai.summarize(prompt, system_prompt=None)
963+
964+
# Parse the rewritten query from the response
965+
rewritten_query = self._parse_rewritten_query(response)
966+
return rewritten_query or query # Fall back to original if parsing fails
967+
968+
def _parse_rewritten_query(self, response: str) -> str | None:
969+
"""Parse rewritten query from LLM response"""
970+
# Try to extract content between <rewritten_query> tags
971+
match = re.search(r"<rewritten_query>\s*(.*?)\s*</rewritten_query>", response, re.DOTALL)
972+
if match:
973+
return match.group(1).strip()
974+
# If no tags found, return the response as is (fallback)
975+
return response.strip()
976+
809977
async def _rank_categories_by_summary(
810978
self, query_vec: list[float], top_k: int
811979
) -> tuple[list[tuple[str, float]], dict[str, str]]:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from memu.prompts.retrieve.judger import PROMPT as JUDGER_PROMPT
2+
from memu.prompts.retrieve.query_rewriter import PROMPT as QUERY_REWRITER_PROMPT
3+
4+
__all__ = ["JUDGER_PROMPT", "QUERY_REWRITER_PROMPT"]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
PROMPT = """Your task is to rewrite a user query by resolving references and ambiguities using the conversation history.
2+
3+
## Conversation History:
4+
{conversation_history}
5+
6+
## Current Query:
7+
{query}
8+
9+
## Task:
10+
Analyze the current query and the conversation history. If the query contains:
11+
- Pronouns (e.g., "they", "it", "their", "his", "her")
12+
- Referential expressions (e.g., "that", "those", "the same")
13+
- Implicit context (e.g., "what about...", "and also...")
14+
- Incomplete information that can be inferred from history
15+
16+
Then rewrite the query to be self-contained and explicit by:
17+
1. Replacing pronouns with specific entities mentioned in the conversation
18+
2. Adding necessary context from the conversation history
19+
3. Making implicit references explicit
20+
4. Ensuring the rewritten query can be understood without the conversation history
21+
22+
If the query is already self-contained and clear, return it as is.
23+
24+
## Output Format:
25+
<analysis>
26+
[Brief analysis of whether the query needs rewriting and why]
27+
</analysis>
28+
29+
<rewritten_query>
30+
[The rewritten query that is self-contained and explicit]
31+
</rewritten_query>
32+
"""

0 commit comments

Comments
 (0)