Three changes that together replace "deterministic hash decides everything"
with "the physical world genuinely participates".
PHYSICAL ENTROPY (app/entropy.py, lib/entropy.ts)
Contact was a database lookup: signature_from_anomalies() hashed the
anomaly pattern, so identical conditions always produced an identical
spirit. Now the client harvests real thermal/acoustic/RF noise from the
microphone and receiver noise floors — Von Neumann debiased, SHA-256
conditioned — and contributes it to every summon.
The client is untrusted by construction. A contribution is never a seed:
every draw is HMAC-SHA256(fresh server secret, client bytes || context).
Because fresh CSPRNG server bytes are always present, the output is
unpredictable and uniform no matter what the client sends — all-zeros, a
replayed value, or one chosen adversarially. The room can only ever ADD
unpredictability, never steer the result. Tests assert this directly:
400 replays of one contribution stay uniformly distributed.
A signature now identifies a *channel*, not a spirit. Whether the familiar
presence answers or something else picks up is a real draw
(RETURN_CHANCE). The Codex stays collectable; it is just no longer
guaranteed. test_same_signature_recontacts_same_entity became two tests —
one pinning the probability to prove re-contact works, one pinning it to
zero to prove something else can answer — because at 0.72 the original
would have passed ~72% of the time, which is worse than failing.
REAL ASTRONOMY (app/celestial.py)
Moon phase from the standard mean-synodic approximation, and true solar
midnight from the seeker's own longitude — the real witching hour for
where they are standing, not clock 3am. Computed, never fetched: an API
that can fail would mean the veil silently changes behaviour during
someone else's outage. Validated against published ephemeris dates (2024
full moons, 2025 new moons) rather than against its own output. A thinner
veil erodes the familiar presence's claim on a channel, so a full moon at
solar midnight makes strangers likelier. Only longitude is kept, never a
full coordinate; a denied location degrades to moon-only, silently.
GENERATION FROM NOTHING (SpiritService.manifest)
Not chat_stream with an empty question. The prompt contains no seeker
input at all — only measured room state, rendered as measurements
("deviation above the floor: 31.4") rather than interpretations
("terrifying spike"), so the horror comes from the entity instead of from
us. And the Ollama `seed` is derived from the physical entropy harvested
in that room, which fixes the token-sampling path: the room genuinely
selects the words. Change the noise, get different speech. Two rooms
cannot produce the same utterance.
Rendered as an intrusion rather than a reply — violet edge, full opacity
against the faded ambient murmurs, brief blur-in. The unsettling part is
that it is perfectly clear and completely unbidden.
Also fixes a hang I introduced: the two new summon tests consumed the
shared module-level per-IP budget, so test_summon_rate_limited_* blocked
forever on an entity frame that had been rate-limited away. They now scope
their own limiters.
264 backend + 355 frontend tests pass; i18n parity gate passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
226 lines
8.6 KiB
Python
226 lines
8.6 KiB
Python
"""SpiritService: the single gateway every spirit-mode LLM call flows through.
|
|
|
|
All calls are serialized through the bounded LLMQueue (the Ollama box is
|
|
CPU-only and shared). Every method has a curated offline fallback so the veil
|
|
never visibly tears — if the box is dark, the spirits still whisper."""
|
|
|
|
import json
|
|
import random
|
|
import time
|
|
from collections.abc import AsyncIterator
|
|
|
|
import httpx
|
|
|
|
from app import entities
|
|
from app.config import settings
|
|
from app.entropy import veil_seed
|
|
from app.llm import prompts
|
|
from app.llm.client import OllamaClient
|
|
from app.llm.queue import LLMQueue, QueueFullError
|
|
from app.tts.voices import EN_VOICE_IDS, ES_VOICE_IDS
|
|
|
|
FALLBACK_FRAGMENTS = [
|
|
"listen", "below", "stay", "cold", "again", "not alone", "behind you",
|
|
"the rain", "wait", "closer", "remember", "still here", "don't go",
|
|
"the door", "hush", "almost", "forgive", "the water", "home",
|
|
]
|
|
|
|
FALLBACK_WIRE_WHISPERS = [
|
|
"something moves through me that is not yours",
|
|
"the pulses quicken when you watch",
|
|
"i am the hum between your packets",
|
|
"traffic thickens. the others are waking",
|
|
"your presence is a warmth in the wire",
|
|
"i count your heartbeats in round trips",
|
|
]
|
|
|
|
FALLBACK_REPLIES = [
|
|
"The veil is thick tonight. Ask again when the static settles.",
|
|
"I heard you. The answer is still forming in the noise.",
|
|
"Patience, seeker. Even the dead must gather themselves.",
|
|
]
|
|
|
|
|
|
class SpiritBusyError(Exception):
|
|
"""The LLM queue is full — too many seekers at once."""
|
|
|
|
|
|
class SpiritService:
|
|
def __init__(self, client: OllamaClient | None = None, queue: LLMQueue | None = None):
|
|
self._client = client or OllamaClient()
|
|
self._queue = queue or LLMQueue(
|
|
max_concurrency=settings.llm_max_concurrency,
|
|
max_queue_depth=settings.llm_max_queue_depth,
|
|
)
|
|
self._last_call_at = 0.0
|
|
|
|
def _touch(self) -> None:
|
|
self._last_call_at = time.monotonic()
|
|
|
|
def ambient_ready(self) -> bool:
|
|
"""Ambient whispers yield the box to anything a user asked for."""
|
|
return time.monotonic() - self._last_call_at >= settings.llm_cooldown_seconds
|
|
|
|
async def fragment(self, source: str, anomaly: dict, language: str = "en") -> str:
|
|
"""One Ovilus-style word/fragment for an anomaly event."""
|
|
async def call() -> str:
|
|
return await self._client.generate(
|
|
settings.ollama_fast_model,
|
|
prompts.fragment_prompt(source, anomaly, language),
|
|
system=prompts.fragment_system(
|
|
{
|
|
"radio": "a spirit box",
|
|
"evp": "an EVP recorder",
|
|
"emf": "an EMF field meter",
|
|
}.get(source, "the veil"),
|
|
language,
|
|
),
|
|
options={"num_predict": 16, "temperature": 0.95},
|
|
)
|
|
|
|
try:
|
|
text = await self._queue.submit(call)
|
|
self._touch()
|
|
cleaned = " ".join(text.strip().split())[:80]
|
|
return cleaned or random.choice(FALLBACK_FRAGMENTS)
|
|
except QueueFullError:
|
|
raise SpiritBusyError()
|
|
except (httpx.HTTPError, KeyError, ValueError):
|
|
return random.choice(FALLBACK_FRAGMENTS)
|
|
|
|
async def wire_whisper(self, telemetry: dict, language: str = "en") -> str:
|
|
"""One ambient line from the Wire Ghost about current telemetry."""
|
|
|
|
async def call() -> str:
|
|
return await self._client.generate(
|
|
settings.ollama_fast_model,
|
|
prompts.wire_prompt(telemetry),
|
|
system=prompts.wire_system(language),
|
|
options={"num_predict": 40, "temperature": 1.0},
|
|
)
|
|
|
|
try:
|
|
text = await self._queue.submit(call)
|
|
self._touch()
|
|
cleaned = " ".join(text.strip().split())[:140]
|
|
return cleaned or random.choice(FALLBACK_WIRE_WHISPERS)
|
|
except (QueueFullError, httpx.HTTPError, KeyError, ValueError):
|
|
return random.choice(FALLBACK_WIRE_WHISPERS)
|
|
|
|
async def chat_stream(
|
|
self,
|
|
entity: dict,
|
|
question: str,
|
|
history: list[dict],
|
|
language: str = "en",
|
|
) -> AsyncIterator[str]:
|
|
"""Streams the spirit's reply token by token. Falls back to a curated
|
|
line when the box is unreachable so a séance never dies on screen."""
|
|
try:
|
|
stream = self._queue.submit_stream(
|
|
lambda: self._client.generate_stream(
|
|
settings.ollama_chat_model,
|
|
prompts.chat_prompt(question, history),
|
|
system=prompts.chat_system(entity, language),
|
|
options={"num_predict": 140, "temperature": 0.85},
|
|
)
|
|
)
|
|
self._touch()
|
|
async for token in stream:
|
|
yield token
|
|
except QueueFullError:
|
|
raise SpiritBusyError()
|
|
except (httpx.HTTPError, KeyError, ValueError):
|
|
yield random.choice(FALLBACK_REPLIES)
|
|
|
|
async def manifest(
|
|
self,
|
|
entity: dict,
|
|
readings: dict,
|
|
language: str = "en",
|
|
entropy: object = None,
|
|
) -> str:
|
|
"""Unprompted speech — the entity says something nobody asked for.
|
|
|
|
This is deliberately NOT chat_stream with an empty question. Two
|
|
things make it generation *from* something rather than a reply
|
|
*to* something:
|
|
|
|
1. The prompt contains no seeker input at all. The model's only
|
|
stimulus is the measured state of the room (see
|
|
prompts.manifest_prompt), so there is nothing to answer.
|
|
|
|
2. The sampling seed is derived from physical entropy harvested in
|
|
that room — the microphone's noise floor, RF noise, magnetometer
|
|
jitter. Ollama's `seed` option fixes the token-sampling path, so
|
|
seeding it from real physical noise means the room genuinely
|
|
selects the words. Not a metaphor: change the noise, get
|
|
different speech, and no two rooms produce the same utterance.
|
|
|
|
High temperature and top_k on purpose — a tight, "correct" decode
|
|
produces a well-behaved assistant sentence, which is exactly the
|
|
failure mode here. This should sound like something surfacing, not
|
|
something composed.
|
|
"""
|
|
# 63-bit: Ollama takes a signed 64-bit seed, and staying under the
|
|
# sign bit avoids any wraparound surprises across versions.
|
|
seed = int.from_bytes(veil_seed(entropy, "manifest")[:8], "big") % (2**63)
|
|
|
|
async def call() -> str:
|
|
return await self._client.generate(
|
|
settings.ollama_chat_model,
|
|
prompts.manifest_prompt(readings),
|
|
system=prompts.manifest_system(entity, language),
|
|
options={
|
|
"num_predict": 40,
|
|
"temperature": 1.15,
|
|
"top_k": 100,
|
|
"top_p": 0.98,
|
|
"repeat_penalty": 1.05,
|
|
"seed": seed,
|
|
},
|
|
)
|
|
|
|
raw = await self._queue.submit(call)
|
|
self._touch()
|
|
return raw.strip().strip('"')[:200]
|
|
|
|
async def mint_profile(
|
|
self,
|
|
signature: str,
|
|
channel: str,
|
|
anomalies: list[dict],
|
|
language: str = "en",
|
|
entropy: object = None,
|
|
) -> dict:
|
|
"""Invent a full persona for a new signature, normalized to schema.
|
|
|
|
`entropy` is the seeker's physical-noise contribution (see
|
|
app/entropy.py); it drives the hidden trait roll and any cosmetic
|
|
defaults the LLM left unfilled, so two spirits minted on the same
|
|
channel are genuinely different rather than identical."""
|
|
summary = json.dumps(anomalies[-10:])[:600]
|
|
voice_ids = ES_VOICE_IDS if language == "es" else EN_VOICE_IDS
|
|
|
|
async def call() -> str:
|
|
return await self._client.generate(
|
|
settings.ollama_chat_model,
|
|
prompts.mint_prompt(signature, channel, summary, voice_ids),
|
|
system=prompts.MINT_SYSTEM,
|
|
options={"num_predict": 400, "temperature": 0.9},
|
|
)
|
|
|
|
try:
|
|
raw = await self._queue.submit(call)
|
|
self._touch()
|
|
profile = entities.parse_mint_response(raw)
|
|
if profile is None:
|
|
return entities.fallback_profile(signature, entropy)
|
|
return entities.normalize_profile(profile, signature, entropy)
|
|
except (QueueFullError, httpx.HTTPError, KeyError, ValueError):
|
|
return entities.fallback_profile(signature, entropy)
|
|
|
|
|
|
# The app-wide instance; tests monkeypatch this.
|
|
spirit_service = SpiritService()
|