"""Prompt and formatting helpers for per-thread memory."""
from __future__ import annotations
import json
from typing import Any
from deerflow.agents.memory.prompt import _coerce_confidence, _count_tokens, format_conversation_for_update
THREAD_MEMORY_UPDATE_PROMPT = """You are a user profile memory system.
Current per-thread memory:
{existing_memory}
Conversation:
{conversation}
Return JSON only with this schema:
{{
"profile": {{
"name": string|null,
"role": string|null,
"expertise": string[],
"language": "zh-CN"|"en-US"|null,
"context": string|null
}},
"preferences": {{
"tone": "casual"|"formal"|"technical"|"friendly"|null,
"verbosity": "concise"|"detailed"|null,
"codeStyle": string|null,
"other": string|null
}},
"facts": [
{{
"content": string,
"category": "tech_stack"|"preference"|"personal"|"context"|"goal",
"confidence": number
}}
]
}}
Rules:
- Keep only stable and useful user profile facts.
- Do not store sensitive personal data (phone/email/address/password/token/id/bank).
- Deduplicate and keep high-confidence facts.
- Return valid JSON only.
"""
def create_empty_thread_memory() -> dict[str, Any]:
return {
"profile": {"name": None, "role": None, "expertise": [], "language": None, "context": None},
"preferences": {"tone": None, "verbosity": None, "codeStyle": None, "other": None},
"facts": [],
}
def format_thread_memory_for_injection(memory_data: dict[str, Any], max_tokens: int = 2000) -> str:
if not memory_data:
return ""
profile = memory_data.get("profile") or {}
preferences = memory_data.get("preferences") or {}
facts = memory_data.get("facts") or []
profile_lines: list[str] = []
for key, label in (("name", "Name"), ("role", "Role"), ("language", "Language"), ("context", "Context")):
value = profile.get(key)
if isinstance(value, str) and value.strip():
profile_lines.append(f"- {label}: {value.strip()}")
expertise = profile.get("expertise")
if isinstance(expertise, list):
cleaned = [str(item).strip() for item in expertise if str(item).strip()]
if cleaned:
profile_lines.append(f"- Expertise: {', '.join(cleaned)}")
pref_lines: list[str] = []
for key, label in (("tone", "Tone"), ("verbosity", "Verbosity"), ("codeStyle", "Code Style"), ("other", "Other")):
value = preferences.get(key)
if isinstance(value, str) and value.strip():
pref_lines.append(f"- {label}: {value.strip()}")
sections: list[str] = []
if profile_lines:
sections.append("Profile:\n" + "\n".join(profile_lines))
if pref_lines:
sections.append("Preferences:\n" + "\n".join(pref_lines))
# Facts are lowest priority: include by confidence/recency and trim by token budget.
ranked_facts = sorted(
(
f
for f in facts
if isinstance(f, dict) and isinstance(f.get("content"), str) and f.get("content", "").strip()
),
key=lambda f: (_coerce_confidence(f.get("confidence"), default=0.0), str(f.get("createdAt", ""))),
reverse=True,
)
base = "\n\n".join(sections)
running = _count_tokens(base) if base else 0
fact_lines: list[str] = []
if ranked_facts:
running += _count_tokens("\n\nFacts:\n" if base else "Facts:\n")
for fact in ranked_facts:
line = (
f"- [{str(fact.get('category', 'context')).strip() or 'context'} | "
f"{_coerce_confidence(fact.get('confidence'), default=0.0):.2f}] {fact.get('content').strip()}"
)
candidate = ("\n" + line) if fact_lines else line
cost = _count_tokens(candidate)
if running + cost > max_tokens:
break
fact_lines.append(line)
running += cost
if fact_lines:
sections.append("Facts:\n" + "\n".join(fact_lines))
return "\n\n".join(sections)
def build_thread_memory_prompt(existing_memory: dict[str, Any], messages: list[Any]) -> str:
return THREAD_MEMORY_UPDATE_PROMPT.format(
existing_memory=json.dumps(existing_memory, ensure_ascii=False, indent=2),
conversation=format_conversation_for_update(messages),
)