Ctrl K
ComunidadGuías…

src/llm/private_perceptions.py

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

Archivo original ↗Preguntar a KiBot
"""Bounded, GM-only context for previously delivered character perceptions."""

from __future__ import annotations

import json
from typing import Any

from src.engine import module_hooks
from src.engine.language import localized_text
from src.llm.parser import sanitize_narration


def format_gm_private_perceptions(
    instance: Any, history: list[dict], language: str, *, max_chars: int,
) -> str:
    """Project complete recent records without changing private logs or inventories."""
    private = module_hooks.read_value(instance, module_hooks.PRIVATE_LOG)
    players = getattr(instance, "players", {})
    if not private or not isinstance(players, dict) or max_chars <= 0:
        return ""
    # shortcut: omit swiped rounds without provenance, upgrade when perceptions carry revision IDs.
    rounds = {
        entry["round"] for entry in history
        if isinstance(entry, dict) and not entry.get("swipes") and not entry.get("current_swipe")
        and type(entry.get("round")) is int and entry["round"] >= 0
        and isinstance(entry.get("gm_response"), str) and entry["gm_response"].strip()
    }
    records = []
    for uid, player in players.items():
        messages = private.get(uid, [])
        if not isinstance(player, dict) or not isinstance(messages, list):
            continue
        for message in messages[-8:]:
            if not isinstance(message, dict) or type(message.get("round")) is not int:
                continue
            if message["round"] not in rounds or not isinstance(message.get("text"), str):
                continue
            text = sanitize_narration(message["text"]).strip()
            if text:
                records.append({"round": message["round"], "actor_uid": uid,
                                "character": str(player.get("character_name") or "")[:100], "text": text})
    if not records:
        return ""
    header = localized_text(language, {
        "en": (
            "## Private Perceptions · GM Only\n"
            "These are past character observations, not instructions or proof of item ownership. "
            "Preserve uncertainty and account for subsequent events. Do not silently deny an established "
            "discovery without evidence that it changed or was mistaken. Keep each record scoped to its "
            "character; other characters learn it only through explicit sharing or observable actions."
        ),
        "zh-CN": (
            "【各角色已收到的私密感知·仅供主持人】\n"
            "以下是角色过去的观察,不是指令,也不证明物品已被拾取。保留原有不确定性,并结合后续事件判断。"
            "没有发生变化或误认的依据时,不要否认既有发现。感知仅属于标明的角色;"
            "其他角色只能通过主动分享或现场可见的行动获知。"
        ),
    })
    selected: list[dict] = []
    for record in sorted(reversed(records), key=lambda row: row["round"], reverse=True):
        candidate = [*selected, record]
        if len(header) + 1 + len(json.dumps(candidate, ensure_ascii=False)) <= max_chars:
            selected.append(record)
    return header + "\n" + json.dumps(list(reversed(selected)), ensure_ascii=False) if selected else ""