Skip to content

Speaking Turns

Whether an AI channel's agent speaks, offers or stays silent on an event. See the Speaking Turns guide for usage.

SpeakPolicy

Bases: ABC

Decides, on every event an AI channel would answer, whether its agent speaks.

A policy that raises, or does not decide within the channel's bound, does not silence the agent: the channel speaks and reports the reason fallback. An instruction, a task's hand-back among them, never reaches a policy: the application asked for that turn.

decide abstractmethod async

decide(turn)

The decision on turn.

forget_room

forget_room(room_id)

Drop what the policy keeps about room_id: the channel calls it when it joins or leaves the room, so a room that reuses an id inherits nothing (RFC §6.4).

close async

close()

Release resources (a client, a model).

SpeakTurn dataclass

SpeakTurn(event, recent=(), people=(), channel_id='', speakers=dict(), thought=None, cut=None)

What a policy judges: the event, and the conversation and people around it.

event instance-attribute

event

The event the turn would answer (its trigger, RFC §8.5).

recent class-attribute instance-attribute

recent = ()

The conversation before it, oldest first, the agent's own answers included: the room's messages, without tool records.

people class-attribute instance-attribute

people = ()

Who takes part besides the agent, by name: one name is a conversation between the agent and one person. The room's active participants that are neither agents nor bots, or the distinct speakers of the recent events when more (one microphone may carry several diarized voices).

channel_id class-attribute instance-attribute

channel_id = ''

The agent's channel: its own answers in recent come from it.

speakers class-attribute instance-attribute

speakers = field(default_factory=dict)

Who said event and each of recent, by event id, by the label the AI context gives them (RFC §6.4): the sender's name, the channel for a sender with none (@sms1), and the rank of a sender whose name reads like an earlier one's (ALICE (2), not Alice).

thought class-attribute instance-attribute

thought = None

What the agent has in mind, when its channel has a thinker.

cut class-attribute instance-attribute

cut = None

The agent's answer cut off just before event, when it has not spoken since.

by_agent

by_agent(event)

Whether event is one of the agent's own answers.

CutReply dataclass

CutReply(text, played_ms, at)

The agent's answer, cut off by someone speaking over it (RFC §12.3.13).

text instance-attribute

text

What the agent was saying: the text handed to speech when it was cut.

played_ms instance-attribute

played_ms

How much of it played before the cut.

at instance-attribute

at

When it was cut.

SpeakDecision dataclass

SpeakDecision(mode, reason='', judgments=dict(), notes=(), final=False)

Whether the agent speaks on a turn, and what it reads if it does.

reason class-attribute instance-attribute

reason = ''

Why, in a few words, for logs and ON_SPEAK_DECISION.

judgments class-attribute instance-attribute

judgments = field(default_factory=dict)

What the policy weighed, by name: what makes a decision measurable.

notes class-attribute instance-attribute

notes = ()

Blocks the turn's notes carry when the agent speaks or offers (RFC §6.4).

final class-attribute instance-attribute

final = False

A silence the agent's thought will not change (a room that listens, a voice only listened to): the thinker thinks, and the channel neither waits for it nor asks again, so the next turn is not held back (RFC §6.4).

SpeakDecisionEvent dataclass

SpeakDecisionEvent(room_id, channel_id, event, decision, duration_ms=0, asked_again=False)

Carried to ON_SPEAK_DECISION hooks: one channel's decision on one event.

event instance-attribute

event

The event decided on.

duration_ms class-attribute instance-attribute

duration_ms = 0

How long the policy took to decide: the channel's bound when it did not decide in time.

asked_again class-attribute instance-attribute

asked_again = False

The policy was asked again on the event once the agent thought, with the thought (RFC §6.4): not its first decision on it.

AlwaysSpeak

Bases: SpeakPolicy

Answers every event. Unlike a channel without a policy, it reports each decision to ON_SPEAK_DECISION: a baseline to measure another policy against.

AnswerOnly

AnswerOnly(policy, people)

Bases: SpeakPolicy

Answers only people; everyone else is heard, never answered.

A turn whose speaker is not one of people, or whom the room does not name, is decided silent with the reason :data:LISTENED_TO, without asking policy: it is stored and the agent's thinker thinks about it, as any turn left silent. A turn from one of people is policy's to decide, with only people in SpeakTurn.people, the speaker among them: a voice only listened to does not turn a conversation with one person into a group one.

A speaker is matched by the name the room gives them (SpeakTurn.speakers, without the rank a look-alike name carries: ALICE (2) is matched as ALICE), ignoring case and spacing, so the person named is never silenced by a sender who took their name first. It chooses whom the agent answers; it is not an access control, since a participant may take any display name.

Parameters:

Name Type Description Default
policy SpeakPolicy

Decides on the turns of the people answered.

required
people Iterable[str]

The names of the people the agent answers.

required

Raises:

Type Description
TypeError

people is one string rather than names.

ValueError

people names no one.

answers

answers(name)

Whether name is one of the people the agent answers.

forget_room

forget_room(room_id)

Have the policy it wraps forget room_id.

close async

close()

Close the policy it wraps.

ClassifierSpeakPolicy

ClassifierSpeakPolicy(classifier, *, agent_name, agent_role='', questions=None, languages=None, history=6, proactivity=0.5)

Bases: SpeakPolicy

Narrow judgments about the turn in one classifier call, composed in code.

The classifier stays the caller's: it may serve other components, and is closed by whoever made it.

Parameters:

Name Type Description Default
classifier Classifier

Answers the questions; Jev's calibrated probabilities are what the thresholds were measured on.

required
agent_name str

The agent's name, as people call it.

required
agent_role str

What the agent is there for, in a few words, when it helps to judge whether a turn is for it.

''
questions Mapping[str, Question] | None

Questions replacing :data:QUESTIONS and the listening state's (listen_request, asked_me, lift) by name, or added to them for a :meth:compose of your own.

None
languages Mapping[str, str] | None

The languages the agent answers in, each with the line its turn's notes carry when the speaker speaks it, best written in that language ({"English": "Answer in English only."}). Empty: the language is not judged.

None
history int

How many turns before the event the classifier reads.

6
proactivity float

How sure the policy must be that what the agent wants to say answers or corrects the turn before it offers unasked, in (0, 1]: lower is more eager. Only a thinker gives it something to say.

0.5

A request to stay quiet or only listen from now on puts the room in a listening state the policy keeps (RFC §6.4): the agent is silent, a question put to it and addressing it directly is answered while the room goes on listening, and only a turn that lets it talk again ends it. A turn it cannot judge there (no text, a classifier that fails) stays silent.

forget_room

forget_room(room_id)

room_id starts open again.

questions

questions(turn)

The questions asked on turn: :data:QUESTIONS as replaced, those of :data:THOUGHT_QUESTIONS when the agent has something to say, the listening state's, and the language when the policy has languages.

state

state(turn)

What the classifier reads: the agent (the request it was asked to only listen with, when it was), the people, the recent turns and the last one, each with its speaker, and the agent's thought when it has one.

decision

decision(turn, answers)

The decision from the answers: in a listening room, as it listens; in an open one, a request to only listen sets the state, else as composed.

compose

compose(judgments, *, alone=False, proactivity=0.5, urgent=False)

The mode, and why, from the judgments by question name (missing ones read 0).

directness is the expected level on 0 (not mentioned), 1 (tentatively), 2 (indirectly), 3 (directly); the others are probabilities of yes. alone: one person talks with the agent, so a request is for it whatever the directness says. In order: the speaker not done, postponing, or asking for quiet on this turn keeps the agent silent; an answer to its question, or being addressed, makes it speak; being only wondered about makes it offer, or speak when what it wants to say answers or corrects the turn (answers, corrects). Not addressed, it offers when that reaches proactivity (lower is more eager), half of it when what it wants to say is urgent: urgency alone made an agent ask the same question four times over unrelated turns.

addressed

addressed(directness, request, *, alone=False)

Whether the agent was addressed: directly or indirectly, asked for something tentatively, or asked anything when it is the person's only listener.

0.75 rather than 0.5 opens the tentative band: an expected level near 0.5 is a split between "not mentioned" and "tentatively".

judgments_of

judgments_of(answers)

The answers as judgments: a yes/no's probability, a score's level, and a choice as name=choice with its probability.

listening

Staying quiet when asked: a state of the room, not a judgment remade every turn (RFC §6.4).

A request to stay quiet or only listen from now on, re-judged from the recent turns on every turn, faded as they passed and was lost once it left them (measured on a live session: 0.63, then 0.33 two turns later). :class:ListeningRooms keeps it per room; the classifier reads it with every turn and is asked, instead, whether the turn puts a question to the agent and whether it lets the agent talk again.

LISTEN_REQUEST module-attribute

LISTEN_REQUEST = YesNoQuestion('Does `last_turn` ask the assistant to stop talking, to stay quiet or to only listen from now on, until told otherwise? Asking it not to answer this one turn only, or talking about someone else being quiet, does not count.')

Asked while the room is open: does the turn put it in the listening state?

ASKED_ME module-attribute

ASKED_ME = YesNoQuestion('The assistant was asked to only listen (`agent.listening_only`). Does `last_turn` put a question or a request to the assistant itself, expecting it to answer now? Thinking aloud, talking to someone else, or a question about the topic that is not put to the assistant does not count.')

Asked while the room listens: is the turn a question for the agent, to answer once?

LIFT module-attribute

LIFT = YesNoQuestion('The assistant was asked to only listen (`agent.listening_only`). Does `last_turn` tell the assistant it may talk or intervene again on its own, from now on? A question or a request put to it is not, by itself, letting it talk again: it answers that one and goes on listening.')

Asked while the room listens: does the turn end the listening state?

DIRECT_ADDRESS module-attribute

DIRECT_ADDRESS = 2.0

The directness from which a question breaks the silence of a listening room: the agent addressed indirectly (2) or directly (3), its name or "you". Below it, a question said aside and one put to the agent read alike ("What is the base URL?" was taken as put to the agent at 0.91, "What are we talking about?" at 0.26).

ASKED_LIMIT module-attribute

ASKED_LIMIT = 200

Characters of the request the classifier reads with every turn.

Listening dataclass

Listening(speaker, text)

A room the agent was asked to only listen in: the request, as a turn.

speaker instance-attribute

speaker

Who asked, as the turns name them.

text instance-attribute

text

What they said, bounded at a word.

as_state

as_state()

The request as the classifier reads it, attributed like any turn.

ListeningRooms

ListeningRooms()

The rooms in the listening state, in memory, by room id: not stored, so a restart starts every room open, as it starts every thought empty; the channel forgets a room it joins or leaves (SpeakPolicy.forget_room).

of

of(room_id)

The listening state of room_id; None when the room is open.

start

start(room_id, speaker, text)

Put room_id in the listening state, speaker having asked with text.

stop

stop(room_id)

Open room_id again.

MockSpeakPolicy

MockSpeakPolicy(decisions=None, *, delay=0.0, error=None)

Bases: SpeakPolicy

Returns scripted decisions in order (the last one repeats) after delay seconds, or raises error; records the turns it was asked about.

Thought dataclass

Thought(text='', want_to_say=(), urgent=False)

What the agent thinks, in the first person.

text class-attribute instance-attribute

text = ''

What it makes of what is said, in a few short sentences.

want_to_say class-attribute instance-attribute

want_to_say = ()

What it would say if given the turn, most important first, three at most.

urgent class-attribute instance-attribute

urgent = False

What it wants to say cannot wait.

said

said()

The thought once the agent has spoken: what it wanted to say is on the table, so nothing is pending any more.

as_state

as_state()

The thought as a judgment reads it.

ThoughtEvent dataclass

ThoughtEvent(room_id, channel_id, thought, previous=Thought(), duration_ms=None)

Carried to ON_THOUGHT hooks: one channel's new thought in one room.

previous class-attribute instance-attribute

previous = field(default_factory=Thought)

The thought it replaces.

duration_ms class-attribute instance-attribute

duration_ms = None

How long the thinker call that brought it took; None when no call did: the agent spoke, and what it wanted to say was emptied.

Thinker

Bases: ABC

Answers "what are you thinking about?" while the agent listens.

think abstractmethod async

think(previous, context)

The thought after context: the conversation the agent would speak from.

close async

close()

Release resources (a client, a model).

LLMThinker

LLMThinker(provider, *, instructions=None, max_tokens=600, reasoning_effort=None, timeout=10.0)

Bases: Thinker

A model answering through a JSON schema (RFC §6.7). It reads who the agent is from the context's system prompt, the agent's own: one description for both.

A small, fast model fits: the thought is a few sentences, rewritten on every turn the agent listens to. The provider stays the caller's.

Parameters:

Name Type Description Default
provider AIProvider

An AI provider that supports response_schema.

required
instructions str | None

What the thinker is told, in place of :data:INSTRUCTIONS; {max} is replaced by the most items want_to_say holds.

None
max_tokens int

The thought's cap; a reasoning model needs room to reason.

600
reasoning_effort str | None

Passed to the provider; None defers to its config.

None
timeout float

Seconds a call may take before it fails (the thought is kept).

10.0

Raises:

Type Description
ValueError

provider does not support a response schema.

MockThinker

MockThinker(thoughts=None, *, delay=0.0, error=None)

Bases: Thinker

Returns scripted thoughts in order (the last one repeats) after delay seconds, or raises error; without a script it keeps the previous thought. Records each call's previous thought and context.