Files
Hy-Memory/hy_memory/prompts/__init__.py
T
alvinfeiandClaude Opus 5.5 16da11f788 feat(sdk): prompts in 11 languages and an explicit language option
Move every LLM prompt that had a zh variant into one registry,
hy_memory/prompts/, with one module per language (en, zh-Hans, zh-Hant,
ja, es, de, pt, fr, ko, ru, id). Call sites look prompts up with
get_prompt(name, language), so a new language needs only a new module.
The en and zh-Hans strings are moved verbatim, a test pins their sha256,
and the old names (prompts_zh.*_ZH, extractor.EXTRACT_SYSTEM_PROMPT, ...)
still import.

Add an optional language: MemoryConfig.language (env MEMORY_LANGUAGE)
as the client default, plus a per-call language= on add / async_add,
add_extracted, finalize_session, evolve and sweep. It goes through
extract and basic profile, reconcile and search query, the coding
extractor and both coding reconcilers with the chain summary, the
episode and topic summaries, and System 2. Codes are validated, with
aliases (zh, zh-CN, zh-TW, zh-HK, pt-BR); an unknown code raises
ValueError. With an explicit language, memories are written in that
language. Without one, the zh/en auto-detection is unchanged. The
server accepts language on /add and /add_extracted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-01 21:29:59 +08:00

243 lines
7.5 KiB
Python

"""Prompt registry: every multilingual LLM prompt, keyed by language code.
One module per language (``en.py``, ``zh_hans.py``, ``ja.py`` ...) defines the
same set of names (``PROMPT_NAMES``). Call sites look prompts up with
``get_prompt(name, language)`` instead of importing a language module, so a
new language only needs a new module and an entry in ``_MODULES``.
Language selection (``resolve_language``):
* explicit ``language`` (per call, else ``MemoryConfig.language`` /
``MEMORY_LANGUAGE``) wins and is validated by ``normalize_language``;
* otherwise the existing zh/en auto-detection runs unchanged
(``utils.lang_detect.is_chinese`` → ``zh-Hans`` or ``en``). Auto-detection
never picks any of the other languages.
When the language is explicit, ``with_output_language`` appends
``OUTPUT_LANGUAGE_DIRECTIVE`` so memories are written in that language even
if the conversation uses another one. Auto-detected requests keep the
"same language as the input" rule and their prompts are byte-identical to
earlier releases.
"""
from __future__ import annotations
import importlib
from types import ModuleType
from typing import Any, Dict, Optional, Tuple
# Canonical codes, in documentation order.
SUPPORTED_LANGUAGES: Tuple[str, ...] = (
"en",
"zh-Hans",
"zh-Hant",
"ja",
"es",
"de",
"pt",
"fr",
"ko",
"ru",
"id",
)
# Accepted aliases (matched case-insensitively, "_" treated as "-").
LANGUAGE_ALIASES: Dict[str, str] = {
"zh": "zh-Hans",
"zh-cn": "zh-Hans",
"zh-sg": "zh-Hans",
"zh-hans-cn": "zh-Hans",
"zh-tw": "zh-Hant",
"zh-hk": "zh-Hant",
"zh-mo": "zh-Hant",
"zh-hant-tw": "zh-Hant",
"zh-hant-hk": "zh-Hant",
"pt-br": "pt",
"pt-pt": "pt",
"en-us": "en",
"en-gb": "en",
}
_MODULES: Dict[str, str] = {
"en": "en",
"zh-Hans": "zh_hans",
"zh-Hant": "zh_hant",
"ja": "ja",
"es": "es",
"de": "de",
"pt": "pt",
"fr": "fr",
"ko": "ko",
"ru": "ru",
"id": "id",
}
# Every language module must define all of these.
PROMPT_NAMES: Tuple[str, ...] = (
# chat extraction
"EXTRACT_SYSTEM_PROMPT",
"STRUCTURE_METADATA_PROMPT",
"EXTRACT_FEW_SHOT",
"EXTRACT_PROMPT",
"EXTRACT_PROMPT_NO_INTENTION",
"NO_INTENTION_SECTION_PATTERN",
"NO_INTENTION_MEMORY_SECTION",
"NO_INTENTION_REPLACEMENTS",
"INTENTION_DISABLED_NOTE",
"USER_NAME_LINE",
# chat reconcile
"SEARCH_QUERY_PROMPT",
"RECONCILE_PROMPT",
"RECONCILE_STRUCTURE_METADATA_PROMPT",
"RECONCILE_FEW_SHOT",
# working / coding memory
"CODING_EXTRACT_PROMPT",
"CODING_EXTRACT_PROMPT_CODING",
"CODING_CONVERSATION_BLOCK",
"CODING_EMPTY_BLOCK",
"CODING_RECONCILE_PROMPT",
"CODING_FACT_RECONCILE_PROMPT",
"CODING_CHAIN_SUMMARY_PROMPT",
# summaries
"EPISODE_SUMMARY_PROMPT",
"TOPIC_SUMMARY_PROMPT",
# System 2 evolve
"SYSTEM2_AGENT_PROMPT",
"SYSTEM2_OUTPUT_INSTRUCTIONS",
"SYSTEM2_SCHEMA_EVIDENCE_GATE",
"SYSTEM2_SINGLE_CALL_REPLACEMENTS",
"SYSTEM2_MATERIALS",
# explicit-language output rule
"OUTPUT_LANGUAGE_DIRECTIVE",
"LANGUAGE_NAME",
)
# Prompts rendered with str.format(); the value lists the expected fields.
# The rest are appended verbatim (their braces are literal JSON).
FORMAT_FIELDS: Dict[str, Tuple[str, ...]] = {
"EXTRACT_PROMPT": ("content", "current_date", "last_messages", "memory_at"),
"EXTRACT_PROMPT_NO_INTENTION": ("content", "current_date", "last_messages", "memory_at"),
"USER_NAME_LINE": ("name",),
"SEARCH_QUERY_PROMPT": ("new_memories",),
"RECONCILE_PROMPT": ("current_date", "existing_memories", "few_shot_section", "new_memories"),
"CODING_EXTRACT_PROMPT": ("branch_present", "conversation_block", "workspace_present"),
"CODING_EXTRACT_PROMPT_CODING": ("branch_present", "conversation_block", "workspace_present"),
"CODING_CONVERSATION_BLOCK": ("history", "new"),
"CODING_RECONCILE_PROMPT": ("candidates_block", "drafts_block"),
"CODING_FACT_RECONCILE_PROMPT": ("candidates_block", "drafts_block"),
"CODING_CHAIN_SUMMARY_PROMPT": ("chain_block",),
"EPISODE_SUMMARY_PROMPT": ("memories",),
"TOPIC_SUMMARY_PROMPT": ("memories", "topic_name"),
}
# Fields of each SYSTEM2_MATERIALS entry (all other entries have none).
SYSTEM2_MATERIALS_FIELDS: Dict[str, Tuple[str, ...]] = {
"title": (),
"stats": ("clustered", "clusters", "evidenced", "fresh", "graph_total", "recalled", "total", "unclustered"),
"graph_status_empty": (),
"clusters_header": (),
"label_holistic": ("n",),
"label_source_context": ("n",),
"label_topic": ("n",),
"raw_label": (),
"holistic_note": ("max_schemas",),
"companion_header": (),
"unclustered_header": (),
"unclustered_hint": (),
"forward_header": ("count",),
"graph_empty": (),
"graph_no_recall": ("graph_total",),
"reverse_header": (),
"tags_header": (),
"closing": (),
}
_loaded: Dict[str, ModuleType] = {}
def _supported_text() -> str:
aliases = ", ".join(f"{k}->{v}" for k, v in LANGUAGE_ALIASES.items())
return f"supported: {', '.join(SUPPORTED_LANGUAGES)} (aliases: {aliases})"
def normalize_language(language: Optional[str]) -> Optional[str]:
"""Return the canonical code for ``language``; None/"" stays None.
Raises ValueError for an unknown code, listing the supported codes.
"""
if language is None:
return None
if not isinstance(language, str):
raise ValueError(
f"language must be a string, got {type(language).__name__}; "
+ _supported_text()
)
raw = language.strip()
if not raw:
return None
key = raw.replace("_", "-").lower()
for code in SUPPORTED_LANGUAGES:
if code.lower() == key:
return code
if key in LANGUAGE_ALIASES:
return LANGUAGE_ALIASES[key]
raise ValueError(f"unsupported language {language!r}; " + _supported_text())
def detect_language_code(content: str = "", messages=None) -> str:
"""The legacy automatic choice: ``zh-Hans`` if Chinese, else ``en``."""
from ..utils.lang_detect import is_chinese
return "zh-Hans" if is_chinese(content, messages=messages) else "en"
def resolve_language(
language: Optional[str] = None,
content: str = "",
messages=None,
) -> str:
"""Explicit language if given (validated), else zh/en auto-detection."""
code = normalize_language(language)
if code is not None:
return code
return detect_language_code(content, messages=messages)
def _module(language: str) -> ModuleType:
code = normalize_language(language) or "en"
mod = _loaded.get(code)
if mod is None:
mod = importlib.import_module(f"{__name__}.{_MODULES[code]}")
_loaded[code] = mod
return mod
def get_prompt(name: str, language: Optional[str] = "en") -> Any:
"""Look up prompt ``name`` for ``language`` (canonical code or alias)."""
if name not in PROMPT_NAMES:
raise KeyError(f"unknown prompt {name!r}")
return getattr(_module(language or "en"), name)
def with_output_language(
prompt: str, language: Optional[str], explicit: bool,
) -> str:
"""Append the output-language rule when the language was set explicitly."""
if not explicit or not language:
return prompt
return prompt + get_prompt("OUTPUT_LANGUAGE_DIRECTIVE", language)
__all__ = [
"SUPPORTED_LANGUAGES",
"LANGUAGE_ALIASES",
"PROMPT_NAMES",
"FORMAT_FIELDS",
"SYSTEM2_MATERIALS_FIELDS",
"normalize_language",
"detect_language_code",
"resolve_language",
"get_prompt",
"with_output_language",
]