"""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), )