mud_server.translation.service ============================== .. py:module:: mud_server.translation.service .. autoapi-nested-parse:: OOC→IC translation service. ``OOCToICTranslationService`` is the single public entry-point for the translation layer. It orchestrates ``CharacterProfileBuilder``, ``OllamaRenderer``, and ``OutputValidator`` to produce in-character dialogue from a raw player message. Caller contract --------------- ``translate()`` always returns either: - A non-empty IC string on success. - ``None`` on any failure (missing profile, Ollama error, validation failure). The caller (``GameEngine.chat/yell/whisper``) treats ``None`` as a signal to use the original OOC message. This is the graceful-degradation guarantee: the layer never breaks the game. Profile summary injection -------------------------- Each world's active canonical prompt template uses a single ``{{profile_summary}}`` placeholder to embed the character's current axis state as a formatted block. Before ``_render_system_prompt`` substitutes placeholders, ``translate()`` calls :func:`_build_profile_summary` to produce this block and injects it into the profile dict under the key ``profile_summary``. Without this step, ``{{profile_summary}}`` would reach the LLM as a literal unresolved string and the model would have no character context. Ledger integration ------------------ Every ``translate()`` call — success or failure — emits a ``chat.translation`` event to the world's JSONL ledger via :func:`~mud_server.ledger.append_event`. Events record: - ``status``: ``"success"`` | ``"fallback.api_error"`` | ``"fallback.validation_failed"`` - ``character_name``: the character whose voice was translated - ``channel``: ``"say"`` | ``"yell"`` | ``"whisper"`` - ``ooc_input``: the raw OOC message from the player - ``ic_output``: the final IC text, or ``null`` on fallback - ``axis_snapshot``: ``{axis_name: {score, label}}`` for every axis present in the character's profile at translation time A ledger write failure is **never fatal** — the game interaction completes and only the audit record is lost. See ``TODO(ledger-hardening)`` comments in :func:`_emit_translation_event`. The event is **not** emitted when the character profile cannot be resolved (``profile is None``) — there is no character data to record. IPC hash and deterministic mode --------------------------------- ``translate()`` accepts an optional ``ipc_hash: str | None`` parameter. The axis engine (``core/engine.py``) computes this hash via ``AxisEngine.resolve_chat_interaction()`` and passes it here. When ``ipc_hash`` is provided and ``config.deterministic`` is ``True``: 1. ``ipc_hash[:16]`` is converted to an integer seed. 2. ``self._renderer.set_deterministic(seed_int)`` is called, clamping temperature to 0.0 and forwarding the seed to Ollama. 3. Identical game state + identical OOC input → identical IC output (subject to Ollama model determinism at seed=constant, temp=0.0). When ``ipc_hash`` is ``None`` (solo-room interactions, axis resolution disabled, or axis engine failure), deterministic mode is silently skipped and the renderer uses the configured temperature. Pre-axis-engine era ------------------- Events emitted before the axis engine was integrated carry ``meta: {"phase": "pre_axis_engine"}`` to distinguish them from post-integration events during ledger replay or analysis. Post-integration events with a real ``ipc_hash`` carry ``meta: {}``. Attributes ---------- .. autoapisummary:: mud_server.translation.service.logger Classes ------- .. autoapisummary:: mud_server.translation.service.LabTranslateResult mud_server.translation.service.OOCToICTranslationService Module Contents --------------- .. py:data:: logger .. py:class:: LabTranslateResult Result returned by ``OOCToICTranslationService.translate_with_axes``. Carries the full research context the Axis Descriptor Lab needs to display results: the IC text, outcome status, the profile_summary block as the server formatted it, and the fully-rendered system prompt that was actually sent to Ollama. .. attribute:: ic_text Validated IC dialogue on success, ``None`` on any fallback path. .. attribute:: status Outcome string — ``"success"``, ``"fallback.api_error"``, or ``"fallback.validation_failed"``. .. attribute:: profile_summary The ``{{profile_summary}}`` block as formatted by the server (canonical format). .. attribute:: rendered_prompt The fully-rendered system prompt sent to Ollama, with all placeholders resolved. .. attribute:: prompt_template Raw template text before per-character variable substitution. Consumers that need a stable hash identifying which prompt *file* was used should hash this field, not ``rendered_prompt``. .. py:attribute:: ic_text :type: str | None .. py:attribute:: status :type: str .. py:attribute:: profile_summary :type: str .. py:attribute:: rendered_prompt :type: str .. py:attribute:: prompt_template :type: str .. py:class:: OOCToICTranslationService(*, world_id, config, world_root) Orchestrates profile building, rendering, and validation. One instance is created per ``World`` when the world's ``translation_layer.enabled`` is ``True``. It is cached on the ``World`` object and reused for every chat call in that world. .. attribute:: _world_id World this service is scoped to. .. attribute:: _config Frozen translation config from ``world.json``. .. attribute:: _profile_builder Builds the character context dict. .. attribute:: _renderer Calls the Ollama API. .. attribute:: _validator Validates/cleans the raw LLM output. .. attribute:: _prompt_template System prompt template text, loaded once at init. Initialise the service. :param world_id: World this service is scoped to. Required. :param config: Frozen config from ``world.json``. :param world_root: Path to the world package directory. Retained for compatibility with world construction flow. :raises ValueError: If ``world_id`` is empty (same guard as ``CharacterProfileBuilder``). .. py:method:: translate(character_name, ooc_message, *, channel = 'say', ipc_hash = None) Translate an OOC message to in-character dialogue. Full pipeline: 1. Build character axis profile (DB lookup). 2. Inject ``channel`` and ``profile_summary`` into the profile dict so that ``{{channel}}`` and ``{{profile_summary}}`` placeholders in the active canonical prompt template resolve correctly. 3. Arm deterministic mode if ``ipc_hash`` is provided and ``config.deterministic`` is ``True``. 4. Render the system prompt from the profile + template. 5. Call Ollama via the renderer. 6. Validate the raw output. 7. Emit a ``chat.translation`` ledger event (success or fallback). On any failure at steps 1–6 the method returns ``None`` and the caller falls back to the original OOC message. Step 7 always executes on the success/fallback paths (steps 5–6 only) — it is skipped when the profile cannot be resolved (step 1 failure) because there is no character data to record. :param character_name: Name of the character speaking (must exist in ``self._world_id``; world-scoped lookup is used). :param ooc_message: The raw, unsanitised message from the player. :param channel: Chat channel context (``"say"``, ``"yell"``, ``"whisper"``). Injected into the profile dict as ``channel`` so that prompt templates can tailor tone by delivery mode. :param ipc_hash: Optional IPC hash produced by the axis engine for this interaction. When provided and ``config.deterministic=True``, deterministic mode is armed: temperature is clamped to 0.0 and a seed derived from the hash is forwarded to Ollama, ensuring identical game state + OOC input always produces identical IC output. When ``None`` (solo-room interaction, axis engine disabled, or engine failure), deterministic mode is skipped silently. :returns: IC dialogue string on success, ``None`` on any failure. .. py:property:: config :type: mud_server.translation.config.TranslationLayerConfig Return the world's frozen translation layer configuration. .. py:method:: translate_with_axes(axes, ooc_message, *, character_name = 'Lab Subject', channel = 'say', seed = None, temperature = 0.7, prompt_template_override = None) Translate an OOC message using raw axis values — no DB lookup. This is the entry point for the Axis Descriptor Lab. It accepts a caller-supplied axis dict instead of looking up a character in the database, then runs steps 2–6 of the standard ``translate()`` pipeline (profile injection, prompt rendering, Ollama call, validation). No ledger event is emitted — lab calls are research runs, not production game interactions. The server filters ``axes`` to its configured ``active_axes`` before building the profile, so the caller may supply all 11 known axes and the server will silently use only the ones its world is configured for. The response includes ``world_config`` so the lab can see exactly which axes were applied. A fresh ``OllamaRenderer`` is created for each call to avoid polluting the persistent game renderer's deterministic-mode state. :param axes: Dict of ``{axis_name: {"label": str, "score": float}}``. Keys not in ``active_axes`` are silently ignored. :param ooc_message: The raw OOC message to translate. :param character_name: Display name used in the ``profile_summary`` first line. Defaults to ``"Lab Subject"``. :param channel: Chat channel context (``"say"``, ``"yell"``, ``"whisper"``). :param seed: Integer seed for deterministic Ollama output. ``None`` means non-deterministic (random). :param temperature: Sampling temperature forwarded to Ollama. Ignored when ``seed`` is provided (clamped to 0.0 for determinism). :param prompt_template_override: Optional full prompt template text. When provided, used instead of ``self._prompt_template`` for this single call. The server's canonical file is never modified. :returns: :class:`LabTranslateResult` with the IC text, status, canonical profile_summary, and fully-rendered system prompt.