125 lines
5.0 KiB
Python
125 lines
5.0 KiB
Python
"""Who wants which digest — read from `identity` at the start of every run.
|
|
|
|
`identity` (Phase 6) is this project's source of truth for per-person household
|
|
facts: it already owns chore exemptions, reminder styles and arrival-notification
|
|
settings for exactly the same reason it owns this one. Nothing about a person's
|
|
preferences is stored here; this module is a read of `GET /digest-preferences`
|
|
and nothing else.
|
|
|
|
WHAT THE PREFERENCE ACTUALLY CONTROLS
|
|
-------------------------------------
|
|
Two different things, and they are worth keeping apart:
|
|
|
|
1. **What gets generated.** run.py generates the UNION of the sections the
|
|
household asked for. A section nobody has ticked is never sent to the LLM at
|
|
all — no synthesis call, no counter-run call, and it never reaches output/.
|
|
That is the sense in which this is a per-person "should this be generated for
|
|
me" toggle rather than a display setting bolted on at the end.
|
|
2. **What each surface shows.** Each person's own subset is written into the
|
|
digest alongside the documents, and the renderer filters to it. That half is
|
|
a **display filter, not an access control**: digest-web serves the whole
|
|
output volume read-only to anything on the LAN, so anyone who can open the
|
|
canvas can open the JSON behind it. Ticking a box off keeps a section off
|
|
someone's screen and out of their narration; it does not make it secret from
|
|
them. Say so plainly rather than implying a boundary this stack doesn't have.
|
|
|
|
FAILING SAFE MEANS GENERATING MORE, NOT LESS
|
|
--------------------------------------------
|
|
Every failure here — identity disabled, unreachable, a bad token, a malformed
|
|
response — returns None, which run.py treats as "no preferences known" and falls
|
|
back to generating every section, exactly as this component did before the
|
|
setting existed. The alternative reading of a failed lookup ("nobody asked for
|
|
anything, generate nothing") would let one unreachable container silently cost
|
|
the household its whole digest, which is far worse than one run that shows a
|
|
section someone had opted out of.
|
|
|
|
The same reasoning applies one level down: a household that runs `identity` but
|
|
has not registered anybody yet is treated as "no preferences known" too, because
|
|
an empty person table is the state of a fresh install, not a decision. Everyone
|
|
opting out of everything IS a decision, and it is honoured (loudly).
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
|
|
import requests
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
DEFAULT_TIMEOUT = 10.0
|
|
|
|
|
|
def fetch(known_sections):
|
|
"""`{"people": [...], "wanted": [...]}` as identity reports it, or None.
|
|
|
|
`known_sections` is this component's own section list — anything identity names
|
|
that this version of digest-engine has no prompt for is dropped here rather than
|
|
handed to a synthesis pass that would fail on the missing file.
|
|
"""
|
|
base_url = os.environ.get("IDENTITY_URL", "").strip().rstrip("/")
|
|
token = os.environ.get("IDENTITY_TOKEN", "").strip()
|
|
if not base_url:
|
|
LOG.info("IDENTITY_URL is not set, generating every digest section")
|
|
return None
|
|
|
|
try:
|
|
response = requests.get(
|
|
f"{base_url}/digest-preferences",
|
|
headers={"Authorization": f"Bearer {token}"} if token else {},
|
|
timeout=float(os.environ.get("IDENTITY_TIMEOUT", DEFAULT_TIMEOUT)),
|
|
)
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("response was not a JSON object")
|
|
except Exception:
|
|
LOG.warning(
|
|
"could not read digest preferences from %s, generating every section",
|
|
base_url,
|
|
exc_info=True,
|
|
)
|
|
return None
|
|
|
|
people = []
|
|
for entry in payload.get("people") or []:
|
|
if not isinstance(entry, dict) or not entry.get("name"):
|
|
continue
|
|
people.append(
|
|
{
|
|
"id": entry.get("id"),
|
|
"name": str(entry["name"]),
|
|
"nickname": entry.get("nickname"),
|
|
"digest_sections": [
|
|
section
|
|
for section in known_sections
|
|
if section in (entry.get("digest_sections") or [])
|
|
],
|
|
}
|
|
)
|
|
|
|
if not people:
|
|
LOG.info("identity knows no people yet, generating every digest section")
|
|
return None
|
|
|
|
wanted = [
|
|
section
|
|
for section in known_sections
|
|
if any(section in person["digest_sections"] for person in people)
|
|
]
|
|
LOG.info(
|
|
"digest preferences: %d person(s), generating %s",
|
|
len(people),
|
|
", ".join(wanted) or "nothing — everybody has opted out of every section",
|
|
)
|
|
return {"people": people, "wanted": wanted}
|
|
|
|
|
|
def sections_to_generate(prefs, known_sections):
|
|
"""The sections this run should actually synthesise. Everything, when nothing is
|
|
known about who wants what (see the module docstring on why that is the safe
|
|
direction); otherwise exactly the union the household asked for.
|
|
"""
|
|
if prefs is None:
|
|
return list(known_sections)
|
|
return list(prefs["wanted"])
|