Instructions to use Falln87/clerk-memory with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use Falln87/clerk-memory with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
generator: remove module-global session index; question targets restricted to ledger-supported predicates
Browse files- clerk/generator.py +24 -40
clerk/generator.py
CHANGED
|
@@ -10,11 +10,11 @@ Invariants the code maintains:
|
|
| 10 |
* Questions are only asked about predicates the ledger can actually
|
| 11 |
support: never-stated (unknown), currently-valid (stale/superseded),
|
| 12 |
or currently-tombstoned (negated/temporal). Active-but-EVICTED
|
| 13 |
-
predicates are never
|
| 14 |
-
|
| 15 |
-
* n_uses (the salience signal) is
|
| 16 |
-
|
| 17 |
-
|
| 18 |
* Everything is seeded: the same --seed yields byte-identical data.
|
| 19 |
|
| 20 |
Author: Justin Wolcott (fallnai-research.org)
|
|
@@ -33,14 +33,13 @@ from clerk.common import (
|
|
| 33 |
Slot,
|
| 34 |
apply_ops,
|
| 35 |
empty_ledger,
|
| 36 |
-
|
| 37 |
-
parse_ledger,
|
| 38 |
serialize_ledger,
|
| 39 |
)
|
| 40 |
|
| 41 |
# ---------------------------------------------------------------- predicates
|
| 42 |
# p: ledger key | h: human phrase | v: value pool
|
| 43 |
-
# add/upd/tmb: statement templates
|
| 44 |
|
| 45 |
PREDICATES: List[Dict[str, Any]] = [
|
| 46 |
{"p": "job", "h": "job", "v": ["barista", "chef", "nurse", "high school teacher", "librarian", "electrician", "accountant", "software engineer", "pharmacist", "bus driver"],
|
|
@@ -146,16 +145,16 @@ def render_session(
|
|
| 146 |
rng: random.Random,
|
| 147 |
persona: Dict[str, Any],
|
| 148 |
ledger_before: List[Slot],
|
|
|
|
| 149 |
n_add: int, n_upd: int, n_tmb: int, n_q: int,
|
| 150 |
) -> Tuple[List[Dict[str, str]], List[Dict[str, Any]], List[Dict[str, Any]]]:
|
| 151 |
"""Render one session. Returns (turns, write_events, qas).
|
| 152 |
|
| 153 |
-
Question targets are constrained
|
| 154 |
-
|
| 155 |
-
predicate
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
active in persona but EVICTED from ledger .. never asked
|
| 159 |
"""
|
| 160 |
turns: List[Dict[str, str]] = []
|
| 161 |
qas: List[Dict[str, Any]] = []
|
|
@@ -165,7 +164,7 @@ def render_session(
|
|
| 165 |
turns.append({"role": "assistant",
|
| 166 |
"content": rng.choice(ACKS) if assistant is None else assistant})
|
| 167 |
|
| 168 |
-
# ---- events (chosen against persona state at session start)
|
| 169 |
events: List[Dict[str, Any]] = []
|
| 170 |
|
| 171 |
for _ in range(n_add):
|
|
@@ -179,7 +178,7 @@ def render_session(
|
|
| 179 |
|
| 180 |
for _ in range(n_upd):
|
| 181 |
updatable = [f for f in persona["facts"]
|
| 182 |
-
if f["state"] == "active" and f["born_session"] <
|
| 183 |
if not updatable:
|
| 184 |
break
|
| 185 |
f = rng.choice(updatable)
|
|
@@ -199,20 +198,10 @@ def render_session(
|
|
| 199 |
|
| 200 |
rng.shuffle(events)
|
| 201 |
|
| 202 |
-
# ---- questions,
|
| 203 |
-
|
| 204 |
-
for s in ledger_before:
|
| 205 |
-
if s.occupied:
|
| 206 |
-
led_state[s.predicate] = (True, s.valid)
|
| 207 |
-
for f in persona["facts"]:
|
| 208 |
-
if f["pred"] not in led_state:
|
| 209 |
-
# active but never consolidated (session 0) or evicted
|
| 210 |
-
led_state[f["pred"]] = (True, f["state"] != "tombstoned")
|
| 211 |
-
|
| 212 |
askable = [p for p in PREDICATES
|
| 213 |
-
if p["p"]
|
| 214 |
-
or led_state[p["p"]][1] is True # stale/superseded
|
| 215 |
-
or not led_state[p["p"]][1]] # negated/temporal
|
| 216 |
rng.shuffle(askable)
|
| 217 |
for spec in askable[:n_q]:
|
| 218 |
f = find_fact(persona, spec["p"])
|
|
@@ -229,9 +218,12 @@ def render_session(
|
|
| 229 |
else:
|
| 230 |
qas.append({"q": q, "a": ANSWER_NEGATED, "type": "negated",
|
| 231 |
"pred": spec["p"], "value": None})
|
| 232 |
-
|
| 233 |
qas.append({"q": q, "a": f["value"], "type": "superseded",
|
| 234 |
"pred": spec["p"], "value": f["value"]})
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
# ---- render turns: statements (+ chatter), then the Q&A block
|
| 237 |
n_chatter = rng.randint(1, 3)
|
|
@@ -246,9 +238,6 @@ def render_session(
|
|
| 246 |
return turns, events, qas
|
| 247 |
|
| 248 |
|
| 249 |
-
_session_being_rendered: int = 0
|
| 250 |
-
|
| 251 |
-
|
| 252 |
def apply_event_to_persona(persona: Dict[str, Any], ev: Dict[str, Any],
|
| 253 |
session_idx: int) -> None:
|
| 254 |
if ev["kind"] == "ADD":
|
|
@@ -285,7 +274,7 @@ def gold_ops_for_events(
|
|
| 285 |
session_idx: int,
|
| 286 |
) -> Tuple[List[Dict[str, Any]], List[Slot]]:
|
| 287 |
"""Build the gold edit program for one session's events, in transcript
|
| 288 |
-
order,
|
| 289 |
|
| 290 |
Eviction-safe: if a predicate's slot was already evicted, an UPDATE
|
| 291 |
degrades to an ADD (the fact must be re-remembered) and a TOMBSTONE of
|
|
@@ -310,10 +299,8 @@ def gold_ops_for_events(
|
|
| 310 |
ops.append(op)
|
| 311 |
ledger_now, _ = apply_ops(ledger_now, [op], session_idx)
|
| 312 |
|
| 313 |
-
# budget pressure: evict lowest-salience occupied slots until it fits
|
| 314 |
occupied = [s for s in ledger_now if s.occupied]
|
| 315 |
for _ in range(max(0, len(occupied) - len(ledger_now))):
|
| 316 |
-
from clerk.common import salience
|
| 317 |
victim = min((s for s in ledger_now if s.occupied),
|
| 318 |
key=lambda s: salience(s, session_idx))
|
| 319 |
ops.append({"op": "EVICT", "i": victim.id})
|
|
@@ -331,9 +318,6 @@ def generate_persona_timeline(
|
|
| 331 |
sessions: List[Dict[str, Any]] = []
|
| 332 |
|
| 333 |
for s_idx in range(n_sessions):
|
| 334 |
-
global _session_being_rendered
|
| 335 |
-
_session_being_rendered = s_idx
|
| 336 |
-
|
| 337 |
if s_idx == 0:
|
| 338 |
# intro session: persona states the initial facts
|
| 339 |
events = [{"kind": "ADD", "pred": f["pred"], "value": f["value"],
|
|
@@ -347,7 +331,7 @@ def generate_persona_timeline(
|
|
| 347 |
qas: List[Dict[str, Any]] = []
|
| 348 |
else:
|
| 349 |
turns, events, qas = render_session(
|
| 350 |
-
rng, persona, ledger,
|
| 351 |
rng.randint(1, 2), rng.randint(0, 2), rng.randint(0, 1),
|
| 352 |
rng.randint(1, 3))
|
| 353 |
for ev in events:
|
|
|
|
| 10 |
* Questions are only asked about predicates the ledger can actually
|
| 11 |
support: never-stated (unknown), currently-valid (stale/superseded),
|
| 12 |
or currently-tombstoned (negated/temporal). Active-but-EVICTED
|
| 13 |
+
predicates are never asked, so supervision never demands recall of
|
| 14 |
+
what the memory no longer holds.
|
| 15 |
+
* n_uses (the salience signal) is incremented at the moment a question
|
| 16 |
+
is asked, so the eviction oracle at session t only sees evidence
|
| 17 |
+
available up to t.
|
| 18 |
* Everything is seeded: the same --seed yields byte-identical data.
|
| 19 |
|
| 20 |
Author: Justin Wolcott (fallnai-research.org)
|
|
|
|
| 33 |
Slot,
|
| 34 |
apply_ops,
|
| 35 |
empty_ledger,
|
| 36 |
+
salience,
|
|
|
|
| 37 |
serialize_ledger,
|
| 38 |
)
|
| 39 |
|
| 40 |
# ---------------------------------------------------------------- predicates
|
| 41 |
# p: ledger key | h: human phrase | v: value pool
|
| 42 |
+
# add/upd/tmb: statement templates ({v} = new value).
|
| 43 |
|
| 44 |
PREDICATES: List[Dict[str, Any]] = [
|
| 45 |
{"p": "job", "h": "job", "v": ["barista", "chef", "nurse", "high school teacher", "librarian", "electrician", "accountant", "software engineer", "pharmacist", "bus driver"],
|
|
|
|
| 145 |
rng: random.Random,
|
| 146 |
persona: Dict[str, Any],
|
| 147 |
ledger_before: List[Slot],
|
| 148 |
+
session_idx: int,
|
| 149 |
n_add: int, n_upd: int, n_tmb: int, n_q: int,
|
| 150 |
) -> Tuple[List[Dict[str, str]], List[Dict[str, Any]], List[Dict[str, Any]]]:
|
| 151 |
"""Render one session. Returns (turns, write_events, qas).
|
| 152 |
|
| 153 |
+
Question targets are constrained to what the ledger supports:
|
| 154 |
+
predicate never stated .................... unknown
|
| 155 |
+
predicate has a slot in ledger (v=1) ...... stale / superseded
|
| 156 |
+
predicate has a tombstoned slot (v=0) ..... negated / temporal
|
| 157 |
+
predicate active but EVICTED from ledger .. never asked
|
|
|
|
| 158 |
"""
|
| 159 |
turns: List[Dict[str, str]] = []
|
| 160 |
qas: List[Dict[str, Any]] = []
|
|
|
|
| 164 |
turns.append({"role": "assistant",
|
| 165 |
"content": rng.choice(ACKS) if assistant is None else assistant})
|
| 166 |
|
| 167 |
+
# ---- write events (chosen against persona state at session start)
|
| 168 |
events: List[Dict[str, Any]] = []
|
| 169 |
|
| 170 |
for _ in range(n_add):
|
|
|
|
| 178 |
|
| 179 |
for _ in range(n_upd):
|
| 180 |
updatable = [f for f in persona["facts"]
|
| 181 |
+
if f["state"] == "active" and f["born_session"] < session_idx]
|
| 182 |
if not updatable:
|
| 183 |
break
|
| 184 |
f = rng.choice(updatable)
|
|
|
|
| 198 |
|
| 199 |
rng.shuffle(events)
|
| 200 |
|
| 201 |
+
# ---- questions, answered from ledger_before (pre-consolidation state)
|
| 202 |
+
in_ledger = {s.predicate for s in ledger_before if s.occupied}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
askable = [p for p in PREDICATES
|
| 204 |
+
if find_fact(persona, p["p"]) is None or p["p"] in in_ledger]
|
|
|
|
|
|
|
| 205 |
rng.shuffle(askable)
|
| 206 |
for spec in askable[:n_q]:
|
| 207 |
f = find_fact(persona, spec["p"])
|
|
|
|
| 218 |
else:
|
| 219 |
qas.append({"q": q, "a": ANSWER_NEGATED, "type": "negated",
|
| 220 |
"pred": spec["p"], "value": None})
|
| 221 |
+
elif f["state"] == "updated":
|
| 222 |
qas.append({"q": q, "a": f["value"], "type": "superseded",
|
| 223 |
"pred": spec["p"], "value": f["value"]})
|
| 224 |
+
else:
|
| 225 |
+
qas.append({"q": q, "a": f["value"], "type": "stale",
|
| 226 |
+
"pred": spec["p"], "value": f["value"]})
|
| 227 |
|
| 228 |
# ---- render turns: statements (+ chatter), then the Q&A block
|
| 229 |
n_chatter = rng.randint(1, 3)
|
|
|
|
| 238 |
return turns, events, qas
|
| 239 |
|
| 240 |
|
|
|
|
|
|
|
|
|
|
| 241 |
def apply_event_to_persona(persona: Dict[str, Any], ev: Dict[str, Any],
|
| 242 |
session_idx: int) -> None:
|
| 243 |
if ev["kind"] == "ADD":
|
|
|
|
| 274 |
session_idx: int,
|
| 275 |
) -> Tuple[List[Dict[str, Any]], List[Slot]]:
|
| 276 |
"""Build the gold edit program for one session's events, in transcript
|
| 277 |
+
order, then apply budget pressure with the salience oracle.
|
| 278 |
|
| 279 |
Eviction-safe: if a predicate's slot was already evicted, an UPDATE
|
| 280 |
degrades to an ADD (the fact must be re-remembered) and a TOMBSTONE of
|
|
|
|
| 299 |
ops.append(op)
|
| 300 |
ledger_now, _ = apply_ops(ledger_now, [op], session_idx)
|
| 301 |
|
|
|
|
| 302 |
occupied = [s for s in ledger_now if s.occupied]
|
| 303 |
for _ in range(max(0, len(occupied) - len(ledger_now))):
|
|
|
|
| 304 |
victim = min((s for s in ledger_now if s.occupied),
|
| 305 |
key=lambda s: salience(s, session_idx))
|
| 306 |
ops.append({"op": "EVICT", "i": victim.id})
|
|
|
|
| 318 |
sessions: List[Dict[str, Any]] = []
|
| 319 |
|
| 320 |
for s_idx in range(n_sessions):
|
|
|
|
|
|
|
|
|
|
| 321 |
if s_idx == 0:
|
| 322 |
# intro session: persona states the initial facts
|
| 323 |
events = [{"kind": "ADD", "pred": f["pred"], "value": f["value"],
|
|
|
|
| 331 |
qas: List[Dict[str, Any]] = []
|
| 332 |
else:
|
| 333 |
turns, events, qas = render_session(
|
| 334 |
+
rng, persona, ledger, s_idx,
|
| 335 |
rng.randint(1, 2), rng.randint(0, 2), rng.randint(0, 1),
|
| 336 |
rng.randint(1, 3))
|
| 337 |
for ev in events:
|