Spaces:
Sleeping
Sleeping
| """World chat: deterministic environment lookup + harmless improv + writeback. | |
| Tiered resolution (Section 8.4): | |
| 1. Authored salient detail -> deterministic lookup; the environment tier only | |
| *narrates* the retrieved fact (no invention). | |
| 2. Unauthored detail -> may improvise, but only if the query touches no | |
| evidential object/clue topic; otherwise defer ("nothing notable"). | |
| 3. Invariant: improvised details are never incriminating/exculpatory, and are | |
| written back to world_delta.json so re-asks stay consistent. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from ..llm.client import LLMClient | |
| from ..llm.prompts import PromptRegistry | |
| from ..models import EnvObject | |
| from ..worldio import World | |
| from .clues import ClueGraph | |
| class WorldAnswer: | |
| text: str | |
| discovered_clue: str = "" | |
| source: str = "" # authored | improv | deferred | delta | |
| object_id: str = "" | |
| class WorldDelta: | |
| """Persisted improvised harmless details (Section 9 / runtime state).""" | |
| def __init__(self, path: Path) -> None: | |
| self.path = path | |
| self.details: dict[str, str] = {} | |
| if path.exists(): | |
| self.details = json.loads(path.read_text("utf-8")).get("details", {}) | |
| def get(self, key: str) -> str | None: | |
| return self.details.get(key) | |
| def put(self, key: str, value: str) -> None: | |
| self.details[key] = value | |
| self.path.write_text(json.dumps({"details": self.details}, indent=2), "utf-8") | |
| class EnvironmentChat: | |
| def __init__( | |
| self, | |
| *, | |
| world: World, | |
| client: LLMClient, | |
| prompts: PromptRegistry, | |
| clue_graph: ClueGraph, | |
| delta: WorldDelta, | |
| ) -> None: | |
| self.world = world | |
| self.client = client | |
| self.prompts = prompts | |
| self.clue_graph = clue_graph | |
| self.delta = delta | |
| def _match_authored( | |
| self, query: str, location: str | None | |
| ) -> list[tuple[EnvObject, int]]: | |
| """Return (object, overlap_score) for objects the query plausibly hits, | |
| ranked best-first. Scoring by token overlap stops a generic shared word | |
| (e.g. "desk") from shadowing a more specific match.""" | |
| q = query.lower().replace("?", " ").replace(",", " ") | |
| tokens = {t.strip(".'\"") for t in q.split() if len(t) > 3} | |
| scored: list[tuple[EnvObject, int]] = [] | |
| for obj in self.world.environment: | |
| if location and obj.location.lower() != location.lower(): | |
| continue | |
| hay = f"{obj.id} {obj.description_true}".lower() | |
| score = sum(1 for t in tokens if t in hay) | |
| if score == 0 and location and not tokens: | |
| score = 1 # bare "look @location" surfaces something there | |
| if score: | |
| scored.append((obj, score)) | |
| scored.sort(key=lambda os: os[1], reverse=True) | |
| return scored | |
| def ask( | |
| self, *, query: str, location: str | None, discovered: set[str] | |
| ) -> WorldAnswer: | |
| # 1) authored salient detail -> deterministic lookup | |
| matches = self._match_authored(query, location) | |
| visible = [ | |
| (o, score) for (o, score) in matches | |
| if o.visible_by_default or self._unlocked(o, discovered) | |
| ] | |
| if visible: | |
| # Prefer a still-undiscovered piece of evidence over already-found or | |
| # non-evidential objects, then by match strength. | |
| def rank(item: tuple[EnvObject, int]) -> tuple[int, int]: | |
| o, score = item | |
| fresh = ( | |
| o.evidential and o.clue is not None | |
| and self.clue_graph.is_unlocked(o.clue, discovered) | |
| and o.clue not in discovered | |
| ) | |
| return (1 if fresh else 0, score) | |
| obj = max(visible, key=rank)[0] | |
| narration = self._narrate(query, obj.description_true) | |
| clue = "" | |
| if obj.evidential and obj.clue and self.clue_graph.is_unlocked( | |
| obj.clue, discovered | |
| ): | |
| clue = obj.clue | |
| return WorldAnswer( | |
| text=narration, discovered_clue=clue, source="authored", | |
| object_id=obj.id, | |
| ) | |
| # If a match exists but is gated/evidential and not yet unlocked: defer. | |
| if matches: | |
| return WorldAnswer( | |
| text="Nothing notable catches your eye there — at least not yet.", | |
| source="deferred", | |
| ) | |
| # 2) unauthored: does the query touch an evidential object/clue topic? | |
| if self._touches_evidential(query): | |
| return WorldAnswer( | |
| text="You look carefully, but find nothing notable.", | |
| source="deferred", | |
| ) | |
| # check delta cache for prior improv | |
| key = f"{location or 'scene'}::{query.strip().lower()}" | |
| cached = self.delta.get(key) | |
| if cached: | |
| return WorldAnswer(text=cached, source="delta") | |
| # 3) harmless improv + writeback | |
| narration = self._improvise(query, location) | |
| self.delta.put(key, narration) | |
| return WorldAnswer(text=narration, source="improv") | |
| def _unlocked(self, obj: EnvObject, discovered: set[str]) -> bool: | |
| if not obj.clue: | |
| return True | |
| return self.clue_graph.is_unlocked(obj.clue, discovered) | |
| def _touches_evidential(self, query: str) -> bool: | |
| q = query.lower() | |
| for obj in self.world.environment: | |
| if not obj.evidential: | |
| continue | |
| tokens = [t for t in obj.description_true.lower().split() if len(t) > 4] | |
| if any(t in q for t in tokens) or obj.id.lower() in q: | |
| return True | |
| for node in self.world.clues: | |
| tokens = [t for t in node.reveals.lower().split() if len(t) > 4] | |
| if any(t in q for t in tokens): | |
| return True | |
| return False | |
| def _narrate(self, query: str, fact: str) -> str: | |
| prompt = self.prompts.render( | |
| "environment/narrate.md.j2", query=query, fact=fact, improvise=False, | |
| ) | |
| return self.client.complete( | |
| tier="environment", task="env_narrate", user=prompt, | |
| ).text.strip() | |
| def _improvise(self, query: str, location: str | None) -> str: | |
| prompt = self.prompts.render( | |
| "environment/narrate.md.j2", | |
| query=query, fact="", improvise=True, location=location or "the scene", | |
| ) | |
| return self.client.complete( | |
| tier="environment", task="env_narrate", user=prompt, | |
| ).text.strip() | |