Questions and events
How choice, noul and score questions become typed events, and how thresholds decide when they fire.
A monitor's questions is an object of questions. Jev answers all of them about each item, in one request. Jev Events re-exports TypeSafe's three question builders, so you don't need a second import.
| Builder | Answers | Events it creates |
|---|---|---|
choice(instructions, labels) | Which one label fits, with a probability for every label | "<id>:<label>" for each label |
noul(instructions, criteria?) | The probability that the answer is yes | "<id>" |
score(instructions, levels) | A position on your ordered scale, from 0 to levels − 1 | "<id>" |
import { choice, monitor, noul, score } from "jev-events";
import { twitch } from "@jev-events/twitch";
const chat = monitor({
source: twitch.chat(),
questions: {
// choice: exactly one label wins.
// Events: "kind:question", "kind:hype" and "kind:other".
kind: choice("What is this chat message mainly doing?", {
question: "Asks the streamer a genuine question",
hype: "Cheers, celebrates or reacts with excitement",
other: null,
}),
// noul: the probability that the answer is yes. Event: "spoiler"
spoiler: noul("Does this message reveal the ending of Elden Ring?"),
// score: a position on your own scale, from 0 to 3 here. Event: "toxicity"
toxicity: score("How toxic is this message toward other people?", [
"Friendly or neutral",
"Rude, but not aimed at anyone",
"Insulting or mean toward someone",
"Harassment, slurs or threats",
]),
},
});
// Every answer rides along, such as the probability of each kind label.
chat.on("kind:question", (e) => console.log(e.answers.kind.probabilities));
chat.on("spoiler", { min: 0.8 }, (e) => console.log(e.trigger.probability));
chat.on("toxicity", { atLeast: 2 }, (e) => console.log(e.trigger.score));
// @ts-expect-error there is no "kind:spoiler": typos are compile errors
chat.on("kind:spoiler", () => {});Event names and payloads are inferred from the questions. e.answers.kind.probabilities is typed with your labels, and handling an event that doesn't exist is a compile error, as the last line above shows.
When an event fires
Pass a policy between the event name and the handler:
| Question | Default | Policy |
|---|---|---|
| choice | Fires for the label Jev chose | { min: 0.8 } fires when that label's probability reaches 0.8, even if it wasn't the top choice |
| noul | Fires at p ≥ 0.5 | { min: 0.9 } |
| score | Fires at the middle of the scale | { atLeast: 2 } |
Pick thresholds by what a mistake costs. A wrong highlight is cheap, so fire early. A wrong timeout is public, so demand more certainty and send the uncertain middle to a person.
The review band
Add review to any policy. When the answer lands between review and min (or atLeast), the handler doesn't run. The monitor emits a review event instead, so a person can decide:
chat.on("hateful", { min: 0.9, review: 0.6 }, twitch.timeout({ seconds: 600 }));
chat.on("review", (e) => modQueue.add(e)); // 0.6 ≤ p < 0.9: ask a humanSpecial events
Besides outcome events, every monitor emits these:
| Event | When | Payload |
|---|---|---|
judged | Every item Jev answered | item, answers, connection, monitor, latencyMs, usage, cached, dryRun, protected |
review | An answer fell in a review band | The same, plus trigger and the handler that didn't run |
action | A native action ran, would have run, or was skipped | action, description, status, reason, and the event that triggered it |
dropped | An item was skipped before judging | item, connection, reason: filtered, duplicate, stale, overflow, budget or stopped |
error | Something failed; the monitor keeps going | error, phase: source, judge, handler or action, and the connection it happened for |
Outcome events carry connection too: the account the item came from, so one handler can serve every user. It's undefined for streams that aren't read as anyone, such as a public chat.
An error with fatal: true ended one connection's run, which starts again after a delay. One with needsSignIn: true means the platform signed the user out: the connection is marked needs-sign-in and isn't read again until they reconnect. See managing connections.
Writing good questions
These follow TypeSafe's own guidance, which is worth reading in full.
- Ask one narrow judgment per question. "Is this hateful?" and "Is this spam?" are two questions, not one choice between them, when a message can be both.
- Describe every outcome. Labels and criteria are shown to Jev.
"Tells the streamer how to play, without being asked"beats"backseat". - Leave a way out. Give a choice an
otherlabel, so Jev isn't forced into a wrong one. - Put the meaning in the question. Question ids like
kindare for your code and aren't sent to Jev. - Keep math and dates in code. Compute "starts in 20 minutes" or "5th message this minute" yourself and pass it as a fact.
- Ask independent questions together. They are answered in parallel in the same request and can't see each other's answers.
Or start from a recipe, which already does all of this.