"""Prompt and formatting helpers for per-thread memory.""" from __future__ import annotations import json import re from typing import Any from langchain_core.messages import HumanMessage from deerflow.agents.memory.prompt import format_conversation_for_update, format_memory_for_injection THREAD_MEMORY_UPDATE_PROMPT = """You are a user profile memory system. Current per-thread memory: {existing_memory} Conversation: {conversation} Preferred memory language: {preferred_language} Return JSON only with this schema: {{ "user": {{ "workContext": {{"summary": string, "updatedAt": string}}, "personalContext": {{"summary": string, "updatedAt": string}}, "topOfMind": {{"summary": string, "updatedAt": string}} }}, "history": {{ "recentMonths": {{"summary": string, "updatedAt": string}}, "earlierContext": {{"summary": string, "updatedAt": string}}, "longTermBackground": {{"summary": string, "updatedAt": string}} }}, "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. - Write all human-readable text fields (`summary`, `content`, and similar prose) in the preferred memory language. - Return valid JSON only. """ def create_empty_thread_memory() -> dict[str, Any]: return { "user": { "workContext": {"summary": "", "updatedAt": ""}, "personalContext": {"summary": "", "updatedAt": ""}, "topOfMind": {"summary": "", "updatedAt": ""}, }, "history": { "recentMonths": {"summary": "", "updatedAt": ""}, "earlierContext": {"summary": "", "updatedAt": ""}, "longTermBackground": {"summary": "", "updatedAt": ""}, }, "facts": [], } def _extract_human_text(content: Any) -> str: if isinstance(content, str): return content.strip() if isinstance(content, list): chunks: list[str] = [] for item in content: if isinstance(item, str): stripped = item.strip() if stripped: chunks.append(stripped) elif isinstance(item, dict): text_val = item.get("text") if isinstance(text_val, str): stripped = text_val.strip() if stripped: chunks.append(stripped) return "\n".join(chunks).strip() return "" def _infer_preferred_memory_language(messages: list[Any]) -> str: user_texts: list[str] = [] for msg in messages: if isinstance(msg, HumanMessage): extracted = _extract_human_text(getattr(msg, "content", None)) if extracted: user_texts.append(extracted) if not user_texts: return "same as the user's latest message" # Prioritize the latest user message; fallback to a short recent window. recent_window = user_texts[-3:] language_sample = "\n".join(recent_window) # If user explicitly provides locale hints, prefer them. locale_match = re.search(r"\b([a-z]{2}-[A-Z]{2})\b", language_sample) if locale_match: return locale_match.group(1) # Script-based heuristic (dynamic, not hard-coded to two languages). script_patterns = { "zh-Hans": r"[\u4e00-\u9fff]", "ja-JP": r"[\u3040-\u30ff]", "ko-KR": r"[\uac00-\ud7af]", "ru-RU": r"[\u0400-\u04FF]", "ar": r"[\u0600-\u06FF]", "hi-IN": r"[\u0900-\u097F]", "th-TH": r"[\u0E00-\u0E7F]", "he-IL": r"[\u0590-\u05FF]", "el-GR": r"[\u0370-\u03FF]", } counts = {lang: len(re.findall(pattern, language_sample)) for lang, pattern in script_patterns.items()} best_lang, best_count = max(counts.items(), key=lambda item: item[1]) if best_count > 0: return best_lang # Latin-script fallback: ask model to keep same language as the user's latest message. return "same as the user's latest message" def format_thread_memory_for_injection(memory_data: dict[str, Any], max_tokens: int = 2000) -> str: return format_memory_for_injection(memory_data, max_tokens=max_tokens) 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), preferred_language=_infer_preferred_memory_language(messages), )