pinecall-protocol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pinecall_protocol/__init__.py +41 -0
- pinecall_protocol/_base.py +13 -0
- pinecall_protocol/_version.py +4 -0
- pinecall_protocol/codec.py +52 -0
- pinecall_protocol/commands.py +208 -0
- pinecall_protocol/defs.py +407 -0
- pinecall_protocol/envelope.py +30 -0
- pinecall_protocol/events.py +471 -0
- pinecall_protocol/fixtures/__init__.py +7 -0
- pinecall_protocol/fixtures/call-log-golden.json +127 -0
- pinecall_protocol/fixtures/call-log-golden.state.json +39 -0
- pinecall_protocol/metrics.py +303 -0
- pinecall_protocol/py.typed +0 -0
- pinecall_protocol/registry.py +341 -0
- pinecall_protocol/rest.py +344 -0
- pinecall_protocol/room.py +83 -0
- pinecall_protocol/state.py +241 -0
- pinecall_protocol/verbs.py +63 -0
- pinecall_protocol-0.1.0.dist-info/METADATA +15 -0
- pinecall_protocol-0.1.0.dist-info/RECORD +22 -0
- pinecall_protocol-0.1.0.dist-info/WHEEL +4 -0
- pinecall_protocol-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Generated from schema/rest.json: the envelopes the read doors answer in."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from pinecall_protocol._base import WireModel
|
|
8
|
+
from pinecall_protocol.defs import (
|
|
9
|
+
Channel,
|
|
10
|
+
Contact,
|
|
11
|
+
Cost,
|
|
12
|
+
Direction,
|
|
13
|
+
EndReason,
|
|
14
|
+
Env,
|
|
15
|
+
KnowledgeFile,
|
|
16
|
+
PlatformTool,
|
|
17
|
+
)
|
|
18
|
+
from pinecall_protocol.envelope import Entry
|
|
19
|
+
from pinecall_protocol.state import CallStatus, State
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CallState(WireModel):
|
|
23
|
+
"""GET /v1/calls/{call}/state: the whole log folded, and the seq a stream resumes from."""
|
|
24
|
+
|
|
25
|
+
state: State
|
|
26
|
+
last_seq: int
|
|
27
|
+
live: bool
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LogPage(WireModel):
|
|
31
|
+
"""One page of a log: what this reader may see, whether the log is open, and where to resume."""
|
|
32
|
+
|
|
33
|
+
entries: list[Entry]
|
|
34
|
+
live: bool
|
|
35
|
+
next: int | None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SessionLine(WireModel):
|
|
39
|
+
"""One call as a list draws it: which call, how far the log got, and the state's own fields."""
|
|
40
|
+
|
|
41
|
+
call: str
|
|
42
|
+
agent: str
|
|
43
|
+
live: bool
|
|
44
|
+
last_seq: int
|
|
45
|
+
status: CallStatus
|
|
46
|
+
channel: Channel | None
|
|
47
|
+
direction: Direction | None
|
|
48
|
+
from_: str | None = Field(alias="from")
|
|
49
|
+
to: str | None
|
|
50
|
+
caller: Contact | None
|
|
51
|
+
started_at: float | None
|
|
52
|
+
ended_at: float | None
|
|
53
|
+
end_reason: EndReason | None
|
|
54
|
+
outcome: str | None
|
|
55
|
+
cost: Cost | None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SessionList(WireModel):
|
|
59
|
+
"""GET /v1/agents/{slug}/sessions: which calls that agent handled, newest first."""
|
|
60
|
+
|
|
61
|
+
calls: list[SessionLine]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# One corner of a world holding an agent: the member whose it is, named so a person can read it.
|
|
65
|
+
class LineHolder(WireModel):
|
|
66
|
+
"""One corner of a world holding an agent."""
|
|
67
|
+
|
|
68
|
+
holder: str | None
|
|
69
|
+
name: str | None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class HeldAgent(WireModel):
|
|
73
|
+
"""One agent, as somebody choosing which to open needs to see it: its name and its channels."""
|
|
74
|
+
|
|
75
|
+
slug: str
|
|
76
|
+
channels: list[Channel]
|
|
77
|
+
holder: LineHolder | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AgentList(WireModel):
|
|
81
|
+
"""GET /v1/agents: every agent this fleet is holding right now, as the front page lists them."""
|
|
82
|
+
|
|
83
|
+
agents: list[HeldAgent]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# GET /v1/agents/{slug}/line: whose terminal a call that RINGS at this agent's doors lands in. An
|
|
87
|
+
# org shares one sandbox number, so it rings in one place and which one is claimed.
|
|
88
|
+
class TheLine(WireModel):
|
|
89
|
+
"""GET /v1/agents/{slug}/line."""
|
|
90
|
+
|
|
91
|
+
agent: str
|
|
92
|
+
env: Env
|
|
93
|
+
held: bool
|
|
94
|
+
holding: LineHolder | None = None
|
|
95
|
+
yours: bool
|
|
96
|
+
waiting: list[LineHolder]
|
|
97
|
+
calling: list[str]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# The base is replaced, never merged.
|
|
101
|
+
class KnowledgePush(WireModel):
|
|
102
|
+
"""PUT /v1/knowledge/{base}, the body: the tenant's folder as of now, sent whole."""
|
|
103
|
+
|
|
104
|
+
files: list[KnowledgeFile]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# A chunk answers when its file and heading path start with `expects`, so naming a file alone
|
|
108
|
+
# accepts any chunk of it and naming a heading accepts that section.
|
|
109
|
+
class GoldenQuestion(WireModel):
|
|
110
|
+
"""One question of a base's golden: what somebody asks, and the chunk that should answer it."""
|
|
111
|
+
|
|
112
|
+
asks: str
|
|
113
|
+
expects: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# A golden is fixed and the index is the variable; a question is never softened so a change can
|
|
117
|
+
# pass.
|
|
118
|
+
class KnowledgeGolden(WireModel):
|
|
119
|
+
"""POST /v1/knowledge/{base}/eval, the body: the questions a base is held to."""
|
|
120
|
+
|
|
121
|
+
questions: list[GoldenQuestion]
|
|
122
|
+
k: int | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# One question whose expected chunk was not among the k returned, and what came back instead.
|
|
126
|
+
class GoldenMiss(WireModel):
|
|
127
|
+
"""GoldenMiss, as protocol/schema declares it."""
|
|
128
|
+
|
|
129
|
+
asks: str
|
|
130
|
+
expects: str
|
|
131
|
+
found: list[str]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Both figures are computed by code, with no model, so two runs of the same golden over the same
|
|
135
|
+
# base answer the same numbers.
|
|
136
|
+
class KnowledgeScore(WireModel):
|
|
137
|
+
"""POST /v1/knowledge/{base}/eval, the answer: how the index did on its own golden."""
|
|
138
|
+
|
|
139
|
+
base: str
|
|
140
|
+
model: str
|
|
141
|
+
questions: int
|
|
142
|
+
k: int
|
|
143
|
+
recall_at_k: float
|
|
144
|
+
ndcg_at_10: float
|
|
145
|
+
took_ms: float
|
|
146
|
+
misses: list[GoldenMiss]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# PUT /v1/knowledge/{base}, the answer: which base, how many chunks it became, and how long that
|
|
150
|
+
# took.
|
|
151
|
+
class KnowledgePushed(WireModel):
|
|
152
|
+
"""PUT /v1/knowledge/{base}, the answer."""
|
|
153
|
+
|
|
154
|
+
base: str
|
|
155
|
+
chunks: int
|
|
156
|
+
took_ms: float
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class KnowledgeBase(WireModel):
|
|
160
|
+
"""One knowledge base as the list draws it: its name, its size, and when it was last pushed."""
|
|
161
|
+
|
|
162
|
+
base: str
|
|
163
|
+
chunks: int
|
|
164
|
+
model: str
|
|
165
|
+
pushed_at: float
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class KnowledgeList(WireModel):
|
|
169
|
+
"""GET /v1/knowledge: every base this org has pushed."""
|
|
170
|
+
|
|
171
|
+
bases: list[KnowledgeBase]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# A fact is never deleted, only superseded, so the history keeps every version.
|
|
175
|
+
class ContactFact(WireModel):
|
|
176
|
+
"""One fact of a contact's history: a MemoryFact with the two dates that bound it."""
|
|
177
|
+
|
|
178
|
+
id: str | None = None
|
|
179
|
+
text: str
|
|
180
|
+
category: str | None = None
|
|
181
|
+
source: str | None = None
|
|
182
|
+
valid_from: float
|
|
183
|
+
invalidated_at: float | None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# GET /v1/contacts/{contact}/memory: everything memory ever kept about one contact, current facts
|
|
187
|
+
# first.
|
|
188
|
+
class ContactMemory(WireModel):
|
|
189
|
+
"""GET /v1/contacts/{contact}/memory."""
|
|
190
|
+
|
|
191
|
+
facts: list[ContactFact]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# DELETE /v1/contacts/{contact}/memory, the answer: how many facts the right to be forgotten erased.
|
|
195
|
+
class Forgotten(WireModel):
|
|
196
|
+
"""DELETE /v1/contacts/{contact}/memory, the answer."""
|
|
197
|
+
|
|
198
|
+
forgotten: int
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# One question of a memory golden: what memory holds about the contact who asks it, the words they
|
|
202
|
+
# just said, and the fact or facts that should come back. The facts are the question's own, so a
|
|
203
|
+
# golden needs no contact in any table.
|
|
204
|
+
class MemoryQuestion(WireModel):
|
|
205
|
+
"""One question of a memory golden."""
|
|
206
|
+
|
|
207
|
+
holds: list[str]
|
|
208
|
+
asks: str
|
|
209
|
+
expects: list[str]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Each question brings its own facts, which are written to a scratch contact of this org, asked, and
|
|
213
|
+
# deleted. A golden is fixed and the ranking is the variable; a question is never softened so a
|
|
214
|
+
# change can pass.
|
|
215
|
+
class MemoryGolden(WireModel):
|
|
216
|
+
"""POST /v1/contacts/memory/eval, the body: the questions memory is held to."""
|
|
217
|
+
|
|
218
|
+
questions: list[MemoryQuestion]
|
|
219
|
+
k: int | None = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# One question memory did not answer whole: what it wanted and did not get, and what came back
|
|
223
|
+
# instead. GoldenMiss is the knowledge base's and names the one chunk that should have won; a memory
|
|
224
|
+
# question may expect several facts and miss some of them.
|
|
225
|
+
class MemoryMiss(WireModel):
|
|
226
|
+
"""One question memory did not answer whole."""
|
|
227
|
+
|
|
228
|
+
asks: str
|
|
229
|
+
missing: list[str]
|
|
230
|
+
found: list[str]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# POST /v1/contacts/memory/eval, the answer: how memory ranked the facts its own golden asked for.
|
|
234
|
+
# Both figures are computed by code, with no model in the loop, so two runs of one golden answer the
|
|
235
|
+
# same numbers.
|
|
236
|
+
class MemoryScore(WireModel):
|
|
237
|
+
"""POST /v1/contacts/memory/eval, the answer."""
|
|
238
|
+
|
|
239
|
+
model: str
|
|
240
|
+
questions: int
|
|
241
|
+
k: int
|
|
242
|
+
recall_at_k: float
|
|
243
|
+
ndcg_at_10: float
|
|
244
|
+
took_ms: float
|
|
245
|
+
misses: list[MemoryMiss]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# What must come of one call's hang-up: each field is one of the four ways a hang-up costs a
|
|
249
|
+
# business. Nothing here compares one sentence to another — a category is the class's own word, a
|
|
250
|
+
# value is a literal the caller said out loud, and a supersession is an id — because two ways of
|
|
251
|
+
# writing one fact are one fact.
|
|
252
|
+
class ExtractionExpected(WireModel):
|
|
253
|
+
"""What must come of one call's hang-up."""
|
|
254
|
+
|
|
255
|
+
writes: list[str] = Field(default_factory=list[str])
|
|
256
|
+
never: list[str] = Field(default_factory=list[str])
|
|
257
|
+
never_says: list[str] = Field(default_factory=list[str])
|
|
258
|
+
invalidates: list[str] = Field(default_factory=list[str])
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# One call written down and what memory must make of it: the write side of the table, judged by
|
|
262
|
+
# code. The read side is MemoryGolden and neither answers for the other — a call may extract the
|
|
263
|
+
# perfect fact and never see it again, because six is what a turn is handed and the seventh is cut.
|
|
264
|
+
class ExtractionGolden(WireModel):
|
|
265
|
+
"""One call written down and what memory must make of it."""
|
|
266
|
+
|
|
267
|
+
name: str
|
|
268
|
+
said: list[tuple[str, str]]
|
|
269
|
+
holds: list[str] = Field(default_factory=list[str])
|
|
270
|
+
plants: list[str] = Field(default_factory=list[str])
|
|
271
|
+
channel: Channel = "phone"
|
|
272
|
+
expect: ExtractionExpected = Field(default_factory=ExtractionExpected)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# POST /v1/agents/{slug}/memory/extraction, the body: the goldens whole, as the tenant wrote them
|
|
276
|
+
# down. Each costs ONE model call — the very one a hang-up makes — run on the org's own model and
|
|
277
|
+
# keys against the class the caller is holding.
|
|
278
|
+
class ExtractionCases(WireModel):
|
|
279
|
+
"""POST /v1/agents/{slug}/memory/extraction, the body."""
|
|
280
|
+
|
|
281
|
+
cases: list[ExtractionGolden]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# One thing that did not hold about a case: which of the questions, and the evidence in a sentence a
|
|
285
|
+
# person can act on.
|
|
286
|
+
class ExtractionBroke(WireModel):
|
|
287
|
+
"""One thing that did not hold about a case."""
|
|
288
|
+
|
|
289
|
+
check: str
|
|
290
|
+
detail: str
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ExtractionJudged(WireModel):
|
|
294
|
+
"""One case, run: what memory would have kept, what admission refused, and what did not hold."""
|
|
295
|
+
|
|
296
|
+
name: str
|
|
297
|
+
held: bool
|
|
298
|
+
wrote: list[str] = Field(default_factory=list[str])
|
|
299
|
+
refused: list[str] = Field(default_factory=list[str])
|
|
300
|
+
broke: list[ExtractionBroke] = Field(default_factory=list[ExtractionBroke])
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# POST /v1/agents/{slug}/memory/extraction, the answer: which model answered, how many cases held,
|
|
304
|
+
# and every one of them. A verb prints this and a pipeline exits on it.
|
|
305
|
+
class ExtractionRun(WireModel):
|
|
306
|
+
"""POST /v1/agents/{slug}/memory/extraction, the answer."""
|
|
307
|
+
|
|
308
|
+
agent: str
|
|
309
|
+
model: str
|
|
310
|
+
cases: int
|
|
311
|
+
held: int
|
|
312
|
+
took_ms: float
|
|
313
|
+
results: list[ExtractionJudged]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# POST /v1/calls/{call}/lookup, the body: which platform tool to run for this turn, and what to run
|
|
317
|
+
# it with. Worker-only; the gateway runs it against its own stores and writes memory.ops or
|
|
318
|
+
# docs.sources on the call's log itself.
|
|
319
|
+
class LookupRequest(WireModel):
|
|
320
|
+
"""POST /v1/calls/{call}/lookup, the body."""
|
|
321
|
+
|
|
322
|
+
tool: PlatformTool
|
|
323
|
+
input: dict[str, Any]
|
|
324
|
+
speech_id: str | None = None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# POST /v1/calls/{call}/lookup, the answer: what the tool found, and how long finding it took. The
|
|
328
|
+
# output is JSON-encoded into a tool_result block, which is where everything from outside the
|
|
329
|
+
# conversation goes and the only place it goes.
|
|
330
|
+
class LookupResult(WireModel):
|
|
331
|
+
"""POST /v1/calls/{call}/lookup, the answer."""
|
|
332
|
+
|
|
333
|
+
output: dict[str, Any]
|
|
334
|
+
took_ms: float
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# POST /v1/calls/{call}/remember, the answer: what the call taught about the contact, counted.
|
|
338
|
+
# Worker-only; the body is empty, the gateway reads the turns off its own log and writes memory.ops
|
|
339
|
+
# itself.
|
|
340
|
+
class Remembered(WireModel):
|
|
341
|
+
"""POST /v1/calls/{call}/remember, the answer."""
|
|
342
|
+
|
|
343
|
+
ops: int
|
|
344
|
+
took_ms: float
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Generated from schema/room.json: the room's facts and the outside world's."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pinecall_protocol._base import WireModel
|
|
6
|
+
from pinecall_protocol.defs import Channel, EventSource, ParticipantKind, TrackKind, TrackSource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Phone and web calls have one; a text session has no room and never logs this.
|
|
10
|
+
class RoomOpened(WireModel):
|
|
11
|
+
"""The LiveKit room exists and the call lives in it."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
sid: str
|
|
15
|
+
channel: Channel
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Somebody joined the room: the caller over SIP or the widget, the agent, a supervisor, a listener,
|
|
19
|
+
# or a second SIP leg. Their attributes travel verbatim: the caller's number is a fact of the room,
|
|
20
|
+
# not a field we invent.
|
|
21
|
+
class ParticipantJoined(WireModel):
|
|
22
|
+
"""Somebody joined the room."""
|
|
23
|
+
|
|
24
|
+
identity: str
|
|
25
|
+
kind: ParticipantKind
|
|
26
|
+
name: str | None = None
|
|
27
|
+
attributes: dict[str, Any]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# When it is the caller, call.ended follows.
|
|
31
|
+
class ParticipantLeft(WireModel):
|
|
32
|
+
"""Somebody left the room."""
|
|
33
|
+
|
|
34
|
+
identity: str
|
|
35
|
+
reason: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Ephemeral: it is a light for the console, and the turns say who spoke.
|
|
39
|
+
class ParticipantSpeaking(WireModel):
|
|
40
|
+
"""The room's own voice activity for one participant flipped."""
|
|
41
|
+
|
|
42
|
+
identity: str
|
|
43
|
+
speaking: bool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TrackPublished(WireModel):
|
|
47
|
+
"""A participant put a track on the room: their microphone, their camera, a screen."""
|
|
48
|
+
|
|
49
|
+
identity: str
|
|
50
|
+
kind: TrackKind
|
|
51
|
+
source: TrackSource
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# A participant's track left the room: they stopped sharing, or a participant.mute took their audio
|
|
55
|
+
# away.
|
|
56
|
+
class TrackUnpublished(WireModel):
|
|
57
|
+
"""A participant's track left the room."""
|
|
58
|
+
|
|
59
|
+
identity: str
|
|
60
|
+
kind: TrackKind
|
|
61
|
+
source: TrackSource
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# A fact arrived from outside the conversation: the tenant's backend sent call.event, or a
|
|
65
|
+
# participant's browser sent pinecall.event. One event for both, told apart by source. It reached
|
|
66
|
+
# the log only because the agent declared the name in its events, from that source; anything else
|
|
67
|
+
# was refused before this.
|
|
68
|
+
class EventReceived(WireModel):
|
|
69
|
+
"""A fact arrived from outside the conversation."""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
data: dict[str, Any]
|
|
73
|
+
source: EventSource
|
|
74
|
+
identity: str | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Ephemeral, and the payload stays out of the log: the tenant chose what to send and to whom.
|
|
78
|
+
class RoomSent(WireModel):
|
|
79
|
+
"""The agent pushed a payload to a browser in the room, on the tenant's room.send."""
|
|
80
|
+
|
|
81
|
+
topic: str
|
|
82
|
+
to: str | None = None
|
|
83
|
+
bytes: int
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Generated from schema/state.json: what a log reduces to."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from pinecall_protocol._base import WireModel
|
|
8
|
+
from pinecall_protocol.defs import (
|
|
9
|
+
AgentState,
|
|
10
|
+
Channel,
|
|
11
|
+
Contact,
|
|
12
|
+
Cost,
|
|
13
|
+
Direction,
|
|
14
|
+
DocSource,
|
|
15
|
+
EndReason,
|
|
16
|
+
EventSource,
|
|
17
|
+
MemoryOp,
|
|
18
|
+
ParticipantKind,
|
|
19
|
+
Route,
|
|
20
|
+
Supervisor,
|
|
21
|
+
TransferMode,
|
|
22
|
+
UserState,
|
|
23
|
+
)
|
|
24
|
+
from pinecall_protocol.metrics import (
|
|
25
|
+
AgentTurnMetrics,
|
|
26
|
+
AvatarMetrics,
|
|
27
|
+
EOTInferenceMetrics,
|
|
28
|
+
EOUMetrics,
|
|
29
|
+
InterruptionMetrics,
|
|
30
|
+
LLMMetrics,
|
|
31
|
+
ModelUsage,
|
|
32
|
+
RealtimeModelMetrics,
|
|
33
|
+
STTMetrics,
|
|
34
|
+
TTSMetrics,
|
|
35
|
+
UserTurnMetrics,
|
|
36
|
+
VADMetrics,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Where the call is in its life. idle before any call.* entry, which is what an agent's own log
|
|
40
|
+
# looks like.
|
|
41
|
+
type CallStatus = Literal["idle", "ringing", "dialing", "active", "ended"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UserTurn(WireModel):
|
|
45
|
+
"""One finished turn of the caller, with what the session measured about it."""
|
|
46
|
+
|
|
47
|
+
role: Literal["user"] = "user"
|
|
48
|
+
speech_id: str
|
|
49
|
+
item_id: str | None = None
|
|
50
|
+
text: str
|
|
51
|
+
language: str | None = None
|
|
52
|
+
transcript_confidence: float | None = None
|
|
53
|
+
metrics: UserTurnMetrics
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AgentTurn(WireModel):
|
|
57
|
+
"""One finished reply of the agent, with what the session measured about it."""
|
|
58
|
+
|
|
59
|
+
role: Literal["agent"] = "agent"
|
|
60
|
+
speech_id: str
|
|
61
|
+
item_id: str | None = None
|
|
62
|
+
text: str
|
|
63
|
+
interrupted: bool
|
|
64
|
+
metrics: AgentTurnMetrics
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# One turn of either side, told apart by role.
|
|
68
|
+
type Turn = Annotated[UserTurn | AgentTurn, Field(discriminator="role")]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# The turns hold the join; this holds the measurements.
|
|
72
|
+
class CollectedMetrics(WireModel):
|
|
73
|
+
"""Every raw metric block of the call, by kind, in the order it arrived."""
|
|
74
|
+
|
|
75
|
+
llm: list[LLMMetrics]
|
|
76
|
+
stt: list[STTMetrics]
|
|
77
|
+
tts: list[TTSMetrics]
|
|
78
|
+
vad: list[VADMetrics]
|
|
79
|
+
eou: list[EOUMetrics]
|
|
80
|
+
eot: list[EOTInferenceMetrics]
|
|
81
|
+
interruption: list[InterruptionMetrics]
|
|
82
|
+
realtime: list[RealtimeModelMetrics]
|
|
83
|
+
avatar: list[AvatarMetrics]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ToolRun(WireModel):
|
|
87
|
+
"""One tool call and, once the app answered, its result."""
|
|
88
|
+
|
|
89
|
+
call_id: str
|
|
90
|
+
name: str
|
|
91
|
+
arguments: dict[str, Any]
|
|
92
|
+
speech_id: str | None = None
|
|
93
|
+
status: Literal["running", "done", "failed"]
|
|
94
|
+
output: Any = None
|
|
95
|
+
error: str | None = None
|
|
96
|
+
summary: str | None = None
|
|
97
|
+
duration_s: float | None = None
|
|
98
|
+
seq: int
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PromptBlockState(WireModel):
|
|
102
|
+
"""What is known about one block of the prompt without storing its text."""
|
|
103
|
+
|
|
104
|
+
hash: str
|
|
105
|
+
chars: int
|
|
106
|
+
seq: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Every block the app has written, by name, without its text. The history between the static and the
|
|
110
|
+
# dynamic blocks is the turns.
|
|
111
|
+
type PromptState = dict[str, PromptBlockState]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Confirm(WireModel):
|
|
115
|
+
"""One confirmation the platform asked for, and how it went."""
|
|
116
|
+
|
|
117
|
+
tool: str
|
|
118
|
+
call_id: str
|
|
119
|
+
audience: str
|
|
120
|
+
phrase: str
|
|
121
|
+
status: Literal["pending", "granted", "declined"]
|
|
122
|
+
said: str | None = None
|
|
123
|
+
reason: str | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Handoff(WireModel):
|
|
127
|
+
"""Whether a supervisor holds the line right now."""
|
|
128
|
+
|
|
129
|
+
active: bool
|
|
130
|
+
by: Supervisor | None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TransferState(WireModel):
|
|
134
|
+
"""The transfer in flight or the one that happened."""
|
|
135
|
+
|
|
136
|
+
to: str
|
|
137
|
+
mode: TransferMode
|
|
138
|
+
status: Literal["requested", "done", "failed"]
|
|
139
|
+
by: Literal["agent", "supervisor"]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class LiveTranscript(WireModel):
|
|
143
|
+
"""The words on screen right now: interim transcripts that a finished turn clears."""
|
|
144
|
+
|
|
145
|
+
user: str | None
|
|
146
|
+
agent: str | None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Gap(WireModel):
|
|
150
|
+
"""A stretch of seqs this reader never saw."""
|
|
151
|
+
|
|
152
|
+
from_seq: int
|
|
153
|
+
to_seq: int
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class LoggedError(WireModel):
|
|
157
|
+
"""An error entry, kept so the console can show what went wrong and when."""
|
|
158
|
+
|
|
159
|
+
seq: int
|
|
160
|
+
code: str
|
|
161
|
+
message: str
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Participant(WireModel):
|
|
165
|
+
"""One participant in the room right now, as the room reported them when they joined."""
|
|
166
|
+
|
|
167
|
+
identity: str
|
|
168
|
+
kind: ParticipantKind
|
|
169
|
+
name: str | None = None
|
|
170
|
+
joined_at: float
|
|
171
|
+
speaking: bool
|
|
172
|
+
attributes: dict[str, Any]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Room(WireModel):
|
|
176
|
+
"""The LiveKit room the call lives in, and who is in it right now."""
|
|
177
|
+
|
|
178
|
+
name: str
|
|
179
|
+
sid: str
|
|
180
|
+
participants: list[Participant]
|
|
181
|
+
caller: str | None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Its data is in the log at that seq.
|
|
185
|
+
class ReceivedEvent(WireModel):
|
|
186
|
+
"""One fact that reached the agent from outside the conversation, kept by name and origin."""
|
|
187
|
+
|
|
188
|
+
seq: int
|
|
189
|
+
name: str
|
|
190
|
+
source: EventSource
|
|
191
|
+
identity: str | None = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class CustomNote(WireModel):
|
|
195
|
+
"""A line the app wrote into the log with call.log."""
|
|
196
|
+
|
|
197
|
+
seq: int
|
|
198
|
+
name: str
|
|
199
|
+
data: dict[str, Any]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class State(WireModel):
|
|
203
|
+
"""The whole of what a log says, at the seq it was read to."""
|
|
204
|
+
|
|
205
|
+
seq: int
|
|
206
|
+
agent: str
|
|
207
|
+
call: str | None
|
|
208
|
+
status: CallStatus
|
|
209
|
+
channel: Channel | None
|
|
210
|
+
direction: Direction | None
|
|
211
|
+
from_: str | None = Field(alias="from")
|
|
212
|
+
to: str | None
|
|
213
|
+
caller: Contact | None
|
|
214
|
+
room: Room | None
|
|
215
|
+
started_at: float | None
|
|
216
|
+
ended_at: float | None
|
|
217
|
+
end_reason: EndReason | None
|
|
218
|
+
outcome: str | None
|
|
219
|
+
user_state: UserState | None
|
|
220
|
+
agent_state: AgentState | None
|
|
221
|
+
live: LiveTranscript
|
|
222
|
+
turns: list[Turn]
|
|
223
|
+
metrics: CollectedMetrics
|
|
224
|
+
tools: list[ToolRun]
|
|
225
|
+
app_state: dict[str, Any]
|
|
226
|
+
events: list[ReceivedEvent]
|
|
227
|
+
prompt: PromptState
|
|
228
|
+
tools_visible: list[str]
|
|
229
|
+
confirms: list[Confirm]
|
|
230
|
+
memory: list[MemoryOp]
|
|
231
|
+
sources: list[DocSource]
|
|
232
|
+
handoff: Handoff
|
|
233
|
+
held: bool
|
|
234
|
+
muted: bool
|
|
235
|
+
transfer: TransferState | None
|
|
236
|
+
usage: list[ModelUsage]
|
|
237
|
+
cost: Cost | None
|
|
238
|
+
routes: list[Route]
|
|
239
|
+
gaps: list[Gap]
|
|
240
|
+
errors: list[LoggedError]
|
|
241
|
+
custom: list[CustomNote]
|