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.
101 lines
6.3 KiB
Python
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))
|