discussion_helper.session module

A conversation that outlives the call that started it.

Module summary

Discussion is a coroutine: you await it and it hands back a finished conversation. That is the right shape for a command line and the wrong one for everything else. A web page, an agent or a “stop talking” button all need the same three things while the conversation is still happening — to see it, to interrupt it, and to end it — and none of them can hold the await.

So a Session is a running discussion plus the handle on it: a background task, a floor you can read, an event stream you can follow, and an interrupt that goes through the same barge-in path the microphone uses. That last point is what keeps this from being a second implementation of the loop. A button that stopped the audio some other way would stop it in some other way — draining instead of discarding, or leaving the floor with the agent — and the difference would only show in a room.

Why there is a registry

One process, several conversations: a server with two callers, a test that runs three at once, an agent that starts one and comes back later. The registry is what makes “come back later” possible, and it is deliberately in-memory — sessions do not survive a restart, because neither does the microphone they were listening to.

Author

Warith HARCHAOUI — https://linkedin.com/in/warith-harchaoui

class discussion_helper.session.Event(kind, at_s, data=<factory>)[source]

Bases: object

Something worth telling a watcher about, as it happens.

Deliberately coarse. A watcher wants to know that a turn landed, that the floor moved, that the agent was cut off — not every frame of audio. The audio has its own channel; this one is for what a transcript would show.

Parameters:
  • kind (str) – "floor", "utterance", "timing" or "ended".

  • at_s (float) – Seconds since the session started.

  • data (dict) – The payload, already JSON-shaped: a watcher is usually a socket.

as_dict()[source]

The event as it goes on the wire.

Return type:

dict[str, Any]

at_s: float
data: dict[str, Any]
kind: str
class discussion_helper.session.Session(listener, responder, voice, policy=None)[source]

Bases: object

A discussion running in the background, with a handle on it.

Parameters:
  • listener (Listener) – The three parts, as Discussion takes them.

  • responder (Responder) – The three parts, as Discussion takes them.

  • voice (Voice) – The three parts, as Discussion takes them.

  • policy (Policy) – What to do about interruptions and length.

Notes

Subscribers get their own queue rather than sharing one. Two watchers reading from one queue each take half the events and neither knows it — the kind of bug that looks like a flaky network.

async events()[source]

Follow the conversation as it happens.

Yields until the session ends. A subscriber that arrives late sees what happens next, not what it missed — the transcript is in state, and duplicating it into the stream would make a reconnecting client replay the whole conversation.

Return type:

AsyncIterator[Event]

async interrupt()[source]

Stop the agent now, as a person talking over it would.

Goes through Voice.stop() — the same path the microphone’s barge-in takes — rather than cancelling the task. A button that stopped the audio some other way would stop it in some other way: draining instead of discarding, or leaving the floor with the agent. The difference would only ever show in a room.

Returns:

Whether there was anything to interrupt. False is not a failure: it means the agent was not speaking, which is the common case for a button somebody pressed a moment too late.

Return type:

bool

start()[source]

Begin the conversation in the background and return immediately.

Must be called from the loop the conversation will run on — the tasks are scheduled here. Calling it from another thread binds them to a loop nobody is running, and the conversation simply never happens: no error, no turns, a session that reports running and does nothing.

Return type:

Session

property started: bool

Whether the conversation has begun.

property state: dict[str, Any]

Everything a watcher needs to catch up, in one object.

async stop()[source]

End the conversation and release everything it holds.

Return type:

Conversation

async wait()[source]

Wait for the conversation to end on its own.

Return type:

Conversation

class discussion_helper.session.Sessions[source]

Bases: object

Every conversation this process is holding.

In-memory on purpose: a session is a microphone and an audio device as much as it is a transcript, and neither survives a restart. Persisting the text alone would hand a caller a session id that resolves to something that cannot speak.

async aclose()[source]

Stop every session. Called when the server shuts down.

A microphone left open outlives the process that opened it on some platforms, which is the kind of thing a user discovers from a recording indicator that will not go away.

Return type:

None

add(session)[source]

Register a session and return it.

Parameters:

session (Session)

Return type:

Session

get(session_id)[source]

The session with that id, or None.

Parameters:

session_id (str)

Return type:

Session | None

ids()[source]

Every live session id.

Return type:

list[str]

async remove(session_id)[source]

Stop a session and forget it.

Parameters:

session_id (str)

Return type:

Conversation | None

discussion_helper.session.utterance_of(speaker, text, at_s=0.0)[source]

Build an utterance from the wire, for callers that push text in.

speaker is the two-value Literal, not str: this helper exists to accept values off the wire, and a wire value of "bot" would otherwise build an Utterance that claims to be one of two things and is neither.

Parameters:
Return type:

Utterance