"""Profile derivation: distill standing facts about the user (semantic memory). This is consolidation step 2. It reads every session gist and map-reduces them into one profile document — who Brian is as a player and person — which is then injected into every prompt. This is what answers identity/abstract questions ("what kind of player am I", "what are my leaks") that raw recall handles badly, because those are patterns across many sessions, not facts in any single message. """ from __future__ import annotations from lyra import config, llm, logbus, memory from lyra.llm import Backend, Message BATCH_CHARS = 18000 _MAP_PROMPT = """From these session summaries, extract durable facts about Brian \ — things that are stably true, not one-off events. Cover, where present: poker \ games/formats/stakes he plays, his playing style and strengths, recurring leaks \ and tendencies, mental-game patterns (tilt triggers, scared money, fatigue), \ relevant personal context, and how he likes to be coached. Terse bullet points. \ Omit anything not supported by the summaries.""" _REDUCE_PROMPT = """Merge these fact lists into one deduplicated profile of Brian. \ Organize under these headings: Poker Style, Leaks & Tendencies, Mental Game, \ Personal Context, Working With Brian. Keep it tight — bullets, no fluff, no \ repetition. Resolve contradictions toward the more recent/frequent signal.""" def _batch_texts(texts: list[str], budget: int) -> list[str]: """Group texts into joined blocks under `budget` chars.""" blocks, buf, size = [], [], 0 for t in texts: if size + len(t) > budget and buf: blocks.append("\n\n".join(buf)) buf, size = [], 0 buf.append(t) size += len(t) if buf: blocks.append("\n\n".join(buf)) return blocks def _call(prompt: str, body: str, backend: Backend) -> str: messages: list[Message] = [ {"role": "system", "content": prompt}, {"role": "user", "content": body}, ] return llm.complete(messages, backend=backend) def rebuild_profile(backend: Backend | None = None) -> str | None: """Re-derive the profile from all current session gists and store it.""" backend = backend or config.load().summary_backend summaries = memory.list_summaries() if not summaries: return None # MAP: extract facts from batches of gists. blocks = _batch_texts([s.content for s in summaries], BATCH_CHARS) partials = [_call(_MAP_PROMPT, b, backend) for b in blocks] logbus.log("info", "profile map done", batches=len(partials), sessions=len(summaries)) # REDUCE: fold partials together until one remains. while len(partials) > 1: partials = [_call(_REDUCE_PROMPT, g, backend) for g in _batch_texts(partials, BATCH_CHARS)] profile = partials[0] memory.set_profile(profile, len(summaries)) logbus.log("info", "profile rebuilt", sessions=len(summaries), chars=len(profile)) return profile def main() -> int: profile = rebuild_profile() if profile is None: print("No summaries yet — run lyra-summarize first.") return 1 print(profile) return 0 if __name__ == "__main__": raise SystemExit(main())