Ctrl K
ComunidadGuías…

rex/user_facts.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Per-user key-value fact store persisted to Memory/<user>_facts.json.

Provides a minimal store/recall API used by the Assistant to inject remembered
facts into the system prompt:

    store("james", "dog_name", "Max")
    recall("james", "dog_name")  # -> "Max"

Each user's facts live in a single JSON file under the Memory/ directory so
they survive across sessions without touching the profile/voice data layout.
"""

from __future__ import annotations

import json
import logging
from pathlib import Path

from rex.identity import validate_user_id

logger = logging.getLogger(__name__)

# Memory/ is at the repo root (same directory as rex/).
_MEMORY_ROOT = Path(__file__).parent.parent / "Memory"


def _facts_path(user: str, memory_root: Path | None = None) -> Path:
    """Return the facts file for a validated user ID.

    User IDs are also used by the profile/session identity layer. Reusing its
    validator prevents ambiguous filename sanitization where distinct IDs such
    as ``alice/bob`` and ``alice_bob`` could map to the same facts file.
    """
    safe = validate_user_id(user)
    root = memory_root or _MEMORY_ROOT
    root.mkdir(parents=True, exist_ok=True)
    return root / f"{safe}_facts.json"


def _load(path: Path) -> dict[str, str]:
    if not path.exists():
        return {}
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        if isinstance(data, dict):
            return {str(k): str(v) for k, v in data.items()}
    except (json.JSONDecodeError, OSError) as exc:
        logger.warning("Failed to load user facts from %s: %s", path, exc)
    return {}


def _save(path: Path, facts: dict[str, str]) -> None:
    try:
        path.write_text(json.dumps(facts, indent=2, ensure_ascii=False), encoding="utf-8")
    except OSError as exc:
        logger.error("Failed to save user facts to %s: %s", path, exc)


def store(user: str, key: str, value: str, *, memory_root: Path | None = None) -> None:
    """Persist a key-value fact for *user*.

    Args:
        user: The user identifier (e.g. "james", "default").
        key: Fact key (e.g. "dog_name").
        value: Fact value (e.g. "Max").
        memory_root: Override the Memory/ directory (used in tests).
    """
    path = _facts_path(user, memory_root)
    facts = _load(path)
    facts[key] = value
    _save(path, facts)
    logger.debug("Stored fact for %s: %s = %s", user, key, value)


def recall(user: str, key: str, *, memory_root: Path | None = None) -> str | None:
    """Recall a previously stored fact for *user*.

    Args:
        user: The user identifier.
        key: Fact key to look up.
        memory_root: Override the Memory/ directory (used in tests).

    Returns:
        The stored value, or ``None`` if not found.
    """
    path = _facts_path(user, memory_root)
    return _load(path).get(key)


def recall_all(user: str, *, memory_root: Path | None = None) -> dict[str, str]:
    """Return all stored facts for *user* as a dict."""
    path = _facts_path(user, memory_root)
    return _load(path)


def format_facts_for_prompt(user: str, *, memory_root: Path | None = None) -> str | None:
    """Return a compact context string with all stored facts, or ``None`` if empty.

    The string is injected into the system prompt so the LLM can reference
    remembered facts during conversation.

    Example output::

        [Remembered facts about james: dog_name=Max; city=Austin]
    """
    facts = recall_all(user, memory_root=memory_root)
    if not facts:
        return None
    pairs = "; ".join(f"{k}={v}" for k, v in facts.items())
    return f"[Remembered facts about {user}: {pairs}]"