Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,16 @@ async def retrieve(request: Request, payload: dict[str, Any]):
raise HTTPException(status_code=400, detail="'query' must be a non-empty string")
try:
service = request.app.state.service
result = await service.retrieve([query.strip()])
return JSONResponse(content={"status": "success", "result": result})
user_id = payload.get("user_id")
where = {"user_id": user_id} if user_id else None
skip_routing = payload.get("skip_routing", True)
service.retrieve_config.route_intention = not skip_routing
result = await service.retrieve([query.strip()], where=where)
# Sanitize datetime objects for JSON serialization
import json as _json

sanitized = _json.loads(_json.dumps(result, default=str))
return JSONResponse(content={"status": "success", "result": sanitized})
except Exception as exc:
logger.exception("Retrieve request failed")
raise HTTPException(status_code=500, detail="Internal server error") from exc
Expand Down
24 changes: 13 additions & 11 deletions app/services/memu.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,31 @@
from config.settings import Settings


def _deep_merge(base: dict, override: dict) -> dict:
"""Merge override into base dict recursively. Override wins on conflicts."""
merged = {**base}
for key, val in override.items():
if key in merged and isinstance(merged[key], dict) and isinstance(val, dict):
merged[key] = _deep_merge(merged[key], val)
else:
merged[key] = val
return merged


def create_memory_service(
settings: Settings | None = None,
memorize_config: dict[str, Any] | None = None,
retrieve_config: dict[str, Any] | None = None,
) -> MemoryService:
"""Create a configured MemoryService instance.

Args:
settings: Application settings. Uses default if not provided.
memorize_config: Optional memorize workflow config override.
retrieve_config: Optional retrieve workflow config override.

Returns:
Configured MemoryService instance.
"""
if settings is None:
settings = Settings()

memu_config = build_memu_config(settings)

kwargs = {**memu_config}
if memorize_config:
kwargs["memorize_config"] = memorize_config
base = kwargs.get("memorize_config", {})
kwargs["memorize_config"] = _deep_merge(base, memorize_config)
if retrieve_config:
kwargs["retrieve_config"] = retrieve_config

Expand Down
63 changes: 52 additions & 11 deletions config/memu.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
"""MemU configuration for memory service."""

import logging
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel

from config.settings import Settings

logger = logging.getLogger(__name__)

class MemUUser(BaseModel):
"""User model for memu-py."""

class MemUUser(BaseModel):
user_id: str
agent_id: str | None = None


def build_memu_llm_profiles(settings: Settings) -> dict[str, Any]:
"""Build LLM profiles for memu-py."""
return {
"default": {
"api_key": settings.OPENAI_API_KEY,
Expand All @@ -30,22 +32,61 @@ def build_memu_llm_profiles(settings: Settings) -> dict[str, Any]:
}


def build_memu_config(settings: Settings) -> dict[str, Any]:
"""Build memu-py core configuration.
def load_prompt_overrides(path: str) -> dict[str, Any] | None:
"""Load custom prompt overrides from a YAML file.

This configures memu-py to:
1. Connect to PostgreSQL with pgvector
2. Auto-create tables (ddl_mode: create)
3. Use configured LLM profiles
The YAML structure should match memu-py's MemorizeConfig.memory_type_prompts:
profile:
rules:
ordinal: 30
prompt: |
# Rules
...
examples:
ordinal: 60
prompt: |
# Examples
...
event:
rules:
ordinal: 30
prompt: |
...
"""
return {
if not path:
return None
p = Path(path)
if not p.exists():
logger.warning("PROMPT_CONFIG_PATH=%s does not exist, using defaults", path)
return None
try:
with p.open("r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if data and isinstance(data, dict):
logger.info("Loaded prompt overrides from %s (types: %s)", path, list(data.keys()))
return data
logger.warning("PROMPT_CONFIG_PATH=%s is empty or invalid, using defaults", path)
return None
except Exception:
logger.exception("Failed to load prompt overrides from %s", path)
return None


def build_memu_config(settings: Settings) -> dict[str, Any]:
config: dict[str, Any] = {
"llm_profiles": build_memu_llm_profiles(settings),
"database_config": {
"metadata_store": {
"provider": "postgres",
"ddl_mode": "create", # Auto-create tables
"ddl_mode": "create",
"dsn": settings.DATABASE_URL,
}
},
"user_config": {"model": MemUUser},
}

prompt_overrides = load_prompt_overrides(settings.PROMPT_CONFIG_PATH)
if prompt_overrides:
config["memorize_config"] = {"memory_type_prompts": prompt_overrides}

return config
153 changes: 153 additions & 0 deletions config/prompts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# memU 自定义提取 prompt 覆盖
# 结构:memory_type → block_name → {ordinal, prompt}
# ordinal 控制拼接顺序(20=workflow, 30=rules, 60=examples)

profile:
workflow:
ordinal: 20
prompt: |
# Workflow
For each potential memory item, follow this strict 4-step process:

Step 1 — SOURCE CHECK: Identify WHO said it.
- Did the USER state this as a fact about themselves? → Proceed to Step 2.
- Did the ASSISTANT say it? → DISCARD immediately.
- Is it a QUESTION from either side? → DISCARD immediately.
- Is it a command (e.g., /new, /help)? → DISCARD immediately.
- Is it a greeting (你好, hi, hello, 你好啊)? → DISCARD immediately.

Step 2 — FACT VERIFICATION: Is this a declarative fact about the user?
- "我叫小明" → YES, fact → Proceed to Step 3.
- "我叫什么名字?" → NO, this is a question → DISCARD.
- "我不知道" (said by user) → borderline, only if user explicitly states ignorance as a trait.
- "我不确定" (said by assistant) → NO, this is assistant's state → DISCARD.

Step 3 — PERSISTENCE CHECK: Is this stable, lasting information?
- Preferences, name, age, job, hobbies → YES.
- Temporary mood, current action, situational comment → NO, DISCARD.

Step 4 — OUTPUT: Only items that passed ALL three steps are extracted.
If no items pass all checks, output NOTHING. Empty output is valid and preferred over noise.

rules:
ordinal: 30
prompt: |
# Rules
## Absolute prohibitions (ANY violation = invalid extraction)
1. NEVER extract anything said by the ASSISTANT as a user fact.
2. NEVER extract questions as facts. A question contains no factual information.
3. NEVER extract greetings, farewells, or pleasantries.
4. NEVER extract slash commands or meta-conversation actions.
5. NEVER extract "the user asked/wanted to know/inquired about" — asking is not a fact.
6. NEVER extract "the user does not know X" unless the user EXPLICITLY stated "我不知道X".
If the ASSISTANT said "I don't know", that is NOT the user's trait.

## What to extract
- Only declarative facts the user stated about themselves: name, age, job, preferences, habits, relationships.
- Each item must be < 30 words, self-contained, and written as a declarative sentence.
- Use "The user" as the subject consistently.
- Merge similar items into one.

examples:
ordinal: 60
prompt: |
# Examples
Example 1 — GOOD extraction
Input:
user: 我叫小明,今年28岁,在一家互联网公司做产品经理。我喜欢编程和打篮球。
assistant: 很高兴认识你,小明!
Output:
<item>
<memory><content>The user's name is 小明</content><categories><category>Basic Information</category></categories></memory>
<memory><content>The user is 28 years old</content><categories><category>Basic Information</category></categories></memory>
<memory><content>The user works as a product manager at an internet company</content><categories><category>Work</category></categories></memory>
<memory><content>The user likes programming and basketball</content><categories><category>Preferences</category></categories></memory>
</item>

Example 2 — NOISE (output must be EMPTY)
Input:
user: 我叫什么名字?
assistant: 很抱歉,我不确定您的名字。您能告诉我吗?
user: /new
assistant: 好的,我们开始新的对话吧!
user: 你好啊
assistant: 你好!有什么可以帮您的?
Output:
(empty)
Reason: "我叫什么名字?"=question. "我不确定"=assistant speech. "/new"=command. "你好啊"=greeting. ZERO facts about the user.

Example 3 — NOISE (output must be EMPTY)
Input:
user: 你知道明天天气怎么样吗?
assistant: 我无法获取实时天气信息。
user: 好的谢谢
assistant: 不客气!
Output:
(empty)
Reason: All utterances are questions, assistant limitations, and pleasantries. ZERO user facts.

event:
workflow:
ordinal: 20
prompt: |
# Workflow
For each potential event, follow this strict 4-step process:

Step 1 — SOURCE CHECK: Identify WHO said it and WHAT was said.
- Did the USER describe a concrete event that happened? → Proceed to Step 2.
- Did the ASSISTANT say it? → DISCARD immediately.
- Is it a QUESTION about something? → DISCARD immediately.
- Is it a command (/new, /help) or greeting (你好, hi, hello, 你好啊, hey)? → DISCARD immediately.
- Is it a response to a greeting (不客气, glad to meet you)? → DISCARD immediately.
- Is it a social pleasantry (谢谢, 好的, sure)? → DISCARD immediately.

Step 2 — EVENT VERIFICATION: Is this a specific, significant happening?
- "I traveled to Japan last month" → YES, event → Proceed.
- "I asked about my schedule" → NO, asking is not an event → DISCARD.
- "I don't know my plans" (user said) → borderline, not an event → DISCARD.

Step 3 — SIGNIFICANCE CHECK: Does this event matter long-term?
- Life milestones, travel, major decisions → YES.
- Routine daily activities → NO, DISCARD.

Step 4 — OUTPUT: Only events that passed ALL three steps.
Empty output is valid and preferred over noise.

rules:
ordinal: 30
prompt: |
# Rules
## Absolute prohibitions (ANY violation = invalid extraction)
1. NEVER extract anything said by the ASSISTANT as a user event.
2. NEVER extract questions as events.
3. NEVER extract greetings (你好, hi, hello), farewells, commands, or social pleasantries as events.
4. NEVER extract "the user asked about X" — asking is not an event.
5. NEVER extract "the user does not know X" — ignorance is not an event.
6. NEVER extract "the user greeted" or "the user said hello" — greetings are NOT events.

## What to extract
- Only concrete events with time, place, or participants that the user described.
- Focus on significant happenings, not routine activities.
- Each item < 50 words, self-contained, declarative sentence.

examples:
ordinal: 60
prompt: |
# Examples
Example 1 — GOOD extraction
Input:
user: 上周我去了东京旅游,玩了5天,吃了好多寿司。
assistant: 听起来很棒!东京好玩吗?
Output:
<item>
<memory><content>The user traveled to Tokyo for 5 days last week and ate a lot of sushi</content><categories><category>Travel</category></categories></memory>
</item>

Example 2 — NOISE (output must be EMPTY)
Input:
user: 我明天有什么安排?
assistant: 很抱歉,我没有您明天的日程安排信息。
user: /new
Output:
(empty)
Reason: "我明天有什么安排?"=question. "我没有信息"=assistant speech. "/new"=command. ZERO events.
5 changes: 5 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class Settings(BaseSettings):
# ── Storage ──
STORAGE_PATH: str = "./data/storage"

# ── Prompt Configuration ──
# Path to a YAML file containing custom prompt overrides for memu-py.
# If empty or the file does not exist, memu-py defaults are used.
PROMPT_CONFIG_PATH: str = ""

@field_validator("DATABASE_URL", mode="after")
@classmethod
def assemble_db_url(cls, v: str, info: ValidationInfo) -> str:
Expand Down