diff --git a/app/main.py b/app/main.py
index a129fb6..2b73719 100644
--- a/app/main.py
+++ b/app/main.py
@@ -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
diff --git a/app/services/memu.py b/app/services/memu.py
index e8df706..f622afe 100644
--- a/app/services/memu.py
+++ b/app/services/memu.py
@@ -8,21 +8,22 @@
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()
@@ -30,7 +31,8 @@ def create_memory_service(
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
diff --git a/config/memu.py b/config/memu.py
index fdf76da..e7602ba 100644
--- a/config/memu.py
+++ b/config/memu.py
@@ -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,
@@ -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
diff --git a/config/prompts.yaml b/config/prompts.yaml
new file mode 100644
index 0000000..f73be7b
--- /dev/null
+++ b/config/prompts.yaml
@@ -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:
+ -
+ The user's name is 小明Basic Information
+ The user is 28 years oldBasic Information
+ The user works as a product manager at an internet companyWork
+ The user likes programming and basketballPreferences
+
+
+ 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:
+ -
+ The user traveled to Tokyo for 5 days last week and ate a lot of sushiTravel
+
+
+ Example 2 — NOISE (output must be EMPTY)
+ Input:
+ user: 我明天有什么安排?
+ assistant: 很抱歉,我没有您明天的日程安排信息。
+ user: /new
+ Output:
+ (empty)
+ Reason: "我明天有什么安排?"=question. "我没有信息"=assistant speech. "/new"=command. ZERO events.
diff --git a/config/settings.py b/config/settings.py
index 834c536..9471962 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -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: