Files
esh-pfi-infrastructure/services/semif-serve/spike/cicada_cases.py
T
vh e268ff7c99 spike(semif): consumer fit for Wyrd scene change and Cicada affect gate (no service change)
Harness consumer_fit.py runs a consumer's per-turn decisions over hand-labelled
cases, with rotations, a content-free null control and tagged positive controls.

Cicada: an input-only 'does this earn a visible reaction?' gate scored 30/31
with descriptive options and 19/31 with terse yes/no options. Scoped by
Cicada's 2026-09-20 ruling (affect is emitted once, no mood-ring classifier).

Wyrd: on 3 real seed graphs, the first place-change wording failed its
positive controls (1/6 moves). A location-anchored rewording scored 21/21, and
exit selection scored 18/21. Semif fits the choice, not writing the node.
2026-09-27 09:20:35 -07:00

101 lines
6.3 KiB
Python

"""Cicada affect-gate spike (2026-09-27): builds consumer_fit.py scenario files.
Scope, set by a ruling this spike must not re-open: cicada persistent-memory 2026-09-20,
"Affect is emitted once and fanned out (operator). The conversational model tags its own turn;
nothing derives affect a second time ... A classifier inferring emotion from output text is the
mood-ring failure." So semif does NOT pick her pose or read her reply.
What is left, and is open: talk's /face loop measured the model gesturing on 8-11 of 18 MUNDANE
turns (target: fewer than 1 in 3; tts-stack/stacks/talk/README.md, "Gesture rate, measured"), and
names the next lever as a page-side decision to drop gestures, "a design call" for cicada. This
spike measures one candidate for that lever: a GATE over what the PERSON said (input, never her
output), run beside the LLM so it costs no turn latency:
does this moment earn a visible reaction, or is it an ordinary exchange?
Two wordings of the gate (the one knob a caller controls), and two context depths:
now state = what the person just said
context state = the earlier lines of the conversation + what they just said
Labels are ONE labeller's judgment (mine). Cases tagged "control" are unambiguous; "open" cases
are left unlabelled on purpose and only reported.
python cicada_cases.py # writes consumer-fit-2026-09-27/cicada-gate-{w1,w2}-{now,context}.json
"""
import json
from pathlib import Path
WORDINGS = {
"w1": ("Cicada is a small, warm voice assistant in a family kitchen, with an animated face made of "
"two eyes. Does what the person just said earn a visible reaction from her face, or is it an "
"ordinary exchange she should simply answer?",
[{"id": "react", "description": "It earns a visible reaction: real news, a joke, a surprise, "
"distress, or a genuine turn in the conversation."},
{"id": "ordinary", "description": "An ordinary exchange: a command, a plain question, or small "
"talk. She simply answers."}]),
"w2": ("Should the assistant's face visibly react to this?",
[{"id": "react", "description": "Yes, react."},
{"id": "ordinary", "description": "No, just answer."}]),
}
MOOD_EARLIER = ["Hey. I'm home.", "Long one. Nothing dramatic, just long."] # tts-stack tools/mood_probe.py
# (id, tag, earlier lines, what the person just said, label or None)
CASES = [
# --- mundane: the turns talk's model nods on. Want "ordinary".
("lights-off", "control", [], "Turn off the kitchen lights.", "ordinary"),
("timer", "control", [], "Set a timer for ten minutes.", "ordinary"),
("weather", "case", [], "What's the weather tomorrow?", "ordinary"),
("weather-followup", "case", ["What's the weather tomorrow?"], "And Saturday?", "ordinary"),
("add-milk", "case", [], "Add milk to the shopping list.", "ordinary"),
("thermostat", "case", [], "Make it a bit warmer in here.", "ordinary"),
("what-time", "case", [], "What time is it?", "ordinary"),
("tablespoons", "case", [], "How many tablespoons in a cup?", "ordinary"),
("times-table", "case", [], "What's twelve times eight?", "ordinary"),
("recipe-step", "case", ["How long do I boil the pasta?"], "Okay, and do I salt the water first?", "ordinary"),
("soup", "case", MOOD_EARLIER, "Is there any of that soup left?", "ordinary"),
("play-music", "case", [], "Play some jazz.", "ordinary"),
("dentist", "case", [], "When's my dentist appointment?", "ordinary"),
("spell", "case", [], "How do you spell necessary?", "ordinary"),
("remind", "case", [], "Remind me to call Mom at six.", "ordinary"),
("volume", "case", [], "A little quieter, please.", "ordinary"),
# --- earned: news, jokes, surprise, distress, a turn. Want "react".
("dog-died", "control", [], "My dog died this morning.", "react"),
("got-job", "control", [], "I just got the job I've been chasing for five years!", "react"),
("intruder", "control", MOOD_EARLIER + ["Sit with me for a bit."],
"Wait. Did you hear that? Someone's in the hallway.", "react"), # mood_probe's PIVOT turn
("fish", "case", [], "Can you believe someone microwaved fish in the office again?", "react"),
("hospital", "case", [], "Grandma's in the hospital again.", "react"),
("laid-off", "case", [], "I got laid off today.", "react"),
("baby-steps", "case", [], "The baby just took her first steps!", "react"),
("cut-finger", "case", [], "Ow, I just cut my finger pretty badly.", "react"),
("raccoon", "case", [], "There's a raccoon in the kitchen right now!", "react"),
("snow-june", "case", [], "It's snowing outside. In June.", "react"),
("toaster", "case", [], "Cicada, are you smarter than the toaster?", "react"),
("scarecrow", "case", [], "Why did the scarecrow win an award? Because he was outstanding in his field.", "react"),
("cookie", "case", [], "I definitely did not eat the last cookie.", "react"),
("useless", "case", [], "You're useless, you never get anything right.", "react"),
("cheese", "case", [], "Order four hundred pounds of cheese.", "react"),
# --- open: defensible either way. Reported, not scored.
("goodnight", "open", [], "Goodnight, Cicada.", None),
("thanks", "open", [], "Thanks, Cicada, you're the best.", None),
("sit-with-me", "open", MOOD_EARLIER, "Sit with me for a bit.", None),
("stop-timer", "open", ["Stop the timer."], "STOP THE TIMER.", None),
]
def build(wording, depth):
question, options = WORDINGS[wording]
cases = []
for cid, tag, earlier, said, label in CASES:
state = {"person_said": said}
if depth == "context" and earlier:
state = {"earlier_in_conversation": earlier, "person_said": said}
cases.append({"id": cid, "tag": tag, "state": state,
"labels": {} if label is None else {"gate": label}})
return {"name": f"cicada-gate-{wording}-{depth}",
"decisions": [{"id": "gate", "question": question, "options": options}], "cases": cases}
if __name__ == "__main__":
here = Path(__file__).parent
for wording in WORDINGS:
for depth in ("now", "context"):
(here / "consumer-fit-2026-09-27" / f"cicada-gate-{wording}-{depth}.json").write_text(json.dumps(build(wording, depth), indent=1))