persona.py 5.07 KB
"""
Persona module — user profile that gets injected into Hermes system prompt.
Loaded from ~/.hermes/persona.yaml, auto-updated from behavior data.
"""
from __future__ import annotations
import os
import json
import time
from pathlib import Path


def _load_hermes_env():
    termux_home = Path("/data/data/com.termux/files/home")
    home = termux_home if termux_home.exists() else Path(os.environ.get("HOME", str(termux_home)))
    env_file = home / ".hermes" / ".env"
    if not env_file.exists():
        return
    for raw in env_file.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, _, v = line.partition("=")
        k = k.strip(); v = v.strip().strip('"').strip("'")
        if k and not os.environ.get(k):
            os.environ[k] = v

_load_hermes_env()
_HERMES_HOME = Path(os.environ.get("HOME", "/data/data/com.termux/files/home")) / ".hermes"
PERSONA_PATH = os.environ.get(
    "PERSONA_PATH",
    str(_HERMES_HOME / "persona.yaml"),
)

DEFAULT_PERSONA = {
    "name": "用户",
    "language": "zh-CN",
    "style": "concise",
    "timezone": "Asia/Shanghai",
    "preferences": {
        "apps": ["WeChat", "Calendar"],
        "work_hours": "9:00-22:00",
        "response_style": "简洁直接",
    },
    "important_contacts": [],
    "current_goals": [],
    "last_updated": 0,
}


def _load() -> dict:
    try:
        p = Path(PERSONA_PATH)
        if not p.exists():
            return dict(DEFAULT_PERSONA)
        # Try YAML, fallback to JSON
        text = p.read_text(encoding="utf-8")
        try:
            import yaml
            return yaml.safe_load(text) or {}
        except ImportError:
            import json as _json
            return _json.loads(text)
    except Exception:
        return dict(DEFAULT_PERSONA)


def _save(persona: dict):
    p = Path(PERSONA_PATH)
    p.parent.mkdir(parents=True, exist_ok=True)
    persona["last_updated"] = int(time.time())
    try:
        import yaml
        p.write_text(yaml.dump(persona, allow_unicode=True, default_flow_style=False), encoding="utf-8")
    except ImportError:
        p.write_text(json.dumps(persona, ensure_ascii=False, indent=2), encoding="utf-8")


def get_persona() -> dict:
    return _load()


def update_persona(key: str, value) -> dict:
    persona = _load()
    # Support dot-notation keys like "preferences.apps"
    parts = key.split(".")
    target = persona
    for part in parts[:-1]:
        target = target.setdefault(part, {})
    target[parts[-1]] = value
    _save(persona)
    return persona


def generate_system_prompt_fragment() -> str:
    p = _load()
    name = p.get("name", "用户")
    style = p.get("style", "concise")
    prefs = p.get("preferences", {})
    goals = p.get("current_goals", [])
    contacts = p.get("important_contacts", [])
    tz = p.get("timezone", "Asia/Shanghai")

    lines = [
        f"## 用户信息",
        f"- 姓名:{name}",
        f"- 语言:{p.get('language', 'zh-CN')}",
        f"- 时区:{tz}",
        f"- 回复风格:{prefs.get('response_style', style)}",
    ]
    if prefs.get("work_hours"):
        lines.append(f"- 工作时间:{prefs['work_hours']}")
    if goals:
        lines.append(f"- 当前目标:" + "、".join(goals[:3]))
    if contacts:
        lines.append(f"- 重要联系人:" + "、".join(str(c) for c in contacts[:5]))
    return "\n".join(lines)


def infer_from_memory_graph() -> dict:
    """Auto-update persona from behavior data in Memory Graph."""
    try:
        import sys
        _base = str(Path(__file__).parent)
        if _base not in sys.path:
            sys.path.insert(0, _base)
        from memory_graph import get_stats
        import sqlite3
        db_path = os.environ.get("MEMORY_GRAPH_DB",
                                 str(_HERMES_HOME / "memory_graph.db"))
        conn = sqlite3.connect(db_path)
        conn.row_factory = sqlite3.Row

        # Top contacts by message frequency
        top_contacts = conn.execute(
            "SELECT p.nickname, p.wxid, COUNT(m.id) as cnt "
            "FROM message m JOIN person p ON m.person_id = p.id "
            "WHERE p.nickname != '' AND m.talker NOT LIKE '%@chatroom' "
            "GROUP BY m.person_id ORDER BY cnt DESC LIMIT 10"
        ).fetchall()
        conn.close()

        if top_contacts:
            contacts = [{"name": r["nickname"], "wxid": r["wxid"], "msg_count": r["cnt"]}
                        for r in top_contacts]
            persona = _load()
            persona["important_contacts"] = contacts
            persona["preferences"]["apps"] = ["WeChat"]
            _save(persona)
            return {"updated_contacts": len(contacts)}
    except Exception as e:
        return {"error": str(e)}
    return {}


if __name__ == "__main__":
    # Initialize persona
    p = get_persona()
    print("Current persona:", json.dumps(p, ensure_ascii=False, indent=2))
    print("\nSystem prompt fragment:")
    print(generate_system_prompt_fragment())
    print("\nInferring from memory graph...")
    result = infer_from_memory_graph()
    print("Inference result:", result)