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,471 @@
|
|
|
1
|
+
"""Generated from schema/events/: one model per event."""
|
|
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
|
+
DevVerb,
|
|
14
|
+
Direction,
|
|
15
|
+
DocSource,
|
|
16
|
+
EndedBy,
|
|
17
|
+
EndReason,
|
|
18
|
+
Env,
|
|
19
|
+
MemoryOp,
|
|
20
|
+
Route,
|
|
21
|
+
ScoreVerdict,
|
|
22
|
+
Supervisor,
|
|
23
|
+
TransferMode,
|
|
24
|
+
UserState,
|
|
25
|
+
)
|
|
26
|
+
from pinecall_protocol.metrics import AgentTurnMetrics, ModelUsage, UserTurnMetrics
|
|
27
|
+
from pinecall_protocol.state import State
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Live calls keep their session; the next call starts with the new config.
|
|
31
|
+
class AgentConfigured(WireModel):
|
|
32
|
+
"""The gateway applied an agent.configure."""
|
|
33
|
+
|
|
34
|
+
changed: list[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentDetached(WireModel):
|
|
38
|
+
"""One socket stopped holding the agent."""
|
|
39
|
+
|
|
40
|
+
app: str
|
|
41
|
+
env: Env
|
|
42
|
+
left: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# The gateway accepted an agent.register: this socket now speaks for the agent and answers its
|
|
46
|
+
# routes. Many sockets may hold one agent at once — a new call takes the newest of them, unless the
|
|
47
|
+
# caller names one by its `app` id.
|
|
48
|
+
class AgentRegistered(WireModel):
|
|
49
|
+
"""The gateway accepted an agent.register."""
|
|
50
|
+
|
|
51
|
+
routes: list[Route]
|
|
52
|
+
app: str
|
|
53
|
+
sdk: str | None = None
|
|
54
|
+
env: Env | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AgentStateChanged(WireModel):
|
|
58
|
+
"""The agent's state changed, in the session's own words."""
|
|
59
|
+
|
|
60
|
+
state: AgentState
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Interim while final is false; the whole reply becomes turn.agent. Interim entries are ephemeral.
|
|
64
|
+
class AgentTranscript(WireModel):
|
|
65
|
+
"""Words from the agent as they are played, synced to the audio."""
|
|
66
|
+
|
|
67
|
+
speech_id: str
|
|
68
|
+
text: str
|
|
69
|
+
final: bool
|
|
70
|
+
start: float | None = None
|
|
71
|
+
end: float | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# The first entry of an outbound call's log.
|
|
75
|
+
class CallDialing(WireModel):
|
|
76
|
+
"""The platform is placing an outbound call and the far end has not answered yet."""
|
|
77
|
+
|
|
78
|
+
channel: Channel
|
|
79
|
+
from_: str = Field(alias="from")
|
|
80
|
+
to: str
|
|
81
|
+
run: str | None = None
|
|
82
|
+
caller: Contact | None
|
|
83
|
+
external_id: str | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Nothing about the conversation follows; call.summary still does.
|
|
87
|
+
class CallEnded(WireModel):
|
|
88
|
+
"""The call is over."""
|
|
89
|
+
|
|
90
|
+
reason: EndReason
|
|
91
|
+
ended_by: EndedBy
|
|
92
|
+
ended_at: float
|
|
93
|
+
duration_s: float
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Both are stated so a reader never has to remember the other.
|
|
97
|
+
class CallLine(WireModel):
|
|
98
|
+
"""The line's hold and mute flags after one of them changed."""
|
|
99
|
+
|
|
100
|
+
held: bool
|
|
101
|
+
muted: bool
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# The first entry of an inbound call's log.
|
|
105
|
+
class CallRinging(WireModel):
|
|
106
|
+
"""An inbound call is offered to this agent and has not been answered yet."""
|
|
107
|
+
|
|
108
|
+
channel: Channel
|
|
109
|
+
from_: str = Field(alias="from")
|
|
110
|
+
to: str
|
|
111
|
+
route: Route
|
|
112
|
+
run: str | None = None
|
|
113
|
+
caller: Contact | None
|
|
114
|
+
external_id: str | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# A reader who disagrees with a verdict opens the log at those lines.
|
|
118
|
+
class JudgmentEvidence(WireModel):
|
|
119
|
+
"""Where in the call's own log a judgment is about."""
|
|
120
|
+
|
|
121
|
+
seqs: list[int]
|
|
122
|
+
said: str | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# One judge's answer about one call: the question it was asked, the word it answered and the
|
|
126
|
+
# sentence that says why.
|
|
127
|
+
class Judgment(WireModel):
|
|
128
|
+
"""One judge's answer about one call."""
|
|
129
|
+
|
|
130
|
+
name: str
|
|
131
|
+
verdict: ScoreVerdict
|
|
132
|
+
criteria: str
|
|
133
|
+
reason: str
|
|
134
|
+
evidence: JudgmentEvidence
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Written after call.summary, and the entry the log seals on.
|
|
138
|
+
class CallScore(WireModel):
|
|
139
|
+
"""The last entry of a call: what the judges said about it at hang-up, one row per judge."""
|
|
140
|
+
|
|
141
|
+
passed: bool | None = None
|
|
142
|
+
not_judged: str | None = None
|
|
143
|
+
judges: list[Judgment]
|
|
144
|
+
panel: list[str] | None = None
|
|
145
|
+
judge_calls: int
|
|
146
|
+
judge_cost_eur: float | None = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Everything the agent says and hears comes after this.
|
|
150
|
+
class CallStarted(WireModel):
|
|
151
|
+
"""Media is up: the caller and the agent can hear each other, or the text session is open."""
|
|
152
|
+
|
|
153
|
+
channel: Channel
|
|
154
|
+
direction: Direction
|
|
155
|
+
from_: str = Field(alias="from")
|
|
156
|
+
to: str
|
|
157
|
+
run: str | None = None
|
|
158
|
+
caller: Contact | None
|
|
159
|
+
started_at: float
|
|
160
|
+
env: Env | None = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# Written after call.ended, once memory and pricing are done; call.score follows it and seals the
|
|
164
|
+
# log.
|
|
165
|
+
class CallSummary(WireModel):
|
|
166
|
+
"""What the call was about, how it went, what it consumed and what that cost."""
|
|
167
|
+
|
|
168
|
+
reason: EndReason
|
|
169
|
+
outcome: str
|
|
170
|
+
duration_s: float
|
|
171
|
+
turns: int
|
|
172
|
+
usage: list[ModelUsage]
|
|
173
|
+
cost: Cost
|
|
174
|
+
recording: str | None = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CallTransferred(WireModel):
|
|
178
|
+
"""A transfer asked for by the agent or a supervisor finished, one way or the other."""
|
|
179
|
+
|
|
180
|
+
to: str
|
|
181
|
+
mode: TransferMode
|
|
182
|
+
ok: bool
|
|
183
|
+
error: str | None = None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# Somebody asked to be called back because no seat was free: a phone caller the overflow agent
|
|
187
|
+
# answered, or a web visitor who left a number at the widget. Written into the agent's own log; the
|
|
188
|
+
# tenant's app reads it and places the call.
|
|
189
|
+
class CallbackRequested(WireModel):
|
|
190
|
+
"""Somebody asked to be called back because no seat was free."""
|
|
191
|
+
|
|
192
|
+
channel: Channel
|
|
193
|
+
number: str
|
|
194
|
+
via: Literal["overflow", "widget"]
|
|
195
|
+
call: str | None
|
|
196
|
+
contact: Contact | None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# The tool does not run; the model is told.
|
|
200
|
+
class ConfirmDeclined(WireModel):
|
|
201
|
+
"""The caller did not say yes, or the request lapsed."""
|
|
202
|
+
|
|
203
|
+
tool: str
|
|
204
|
+
call_id: str
|
|
205
|
+
audience: str
|
|
206
|
+
said: str | None = None
|
|
207
|
+
reason: Literal["no", "timeout", "changed", "cancelled"]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# The platform minted a one-shot token bound to the audience and the tool now runs. The token itself
|
|
211
|
+
# never enters the log.
|
|
212
|
+
class ConfirmGranted(WireModel):
|
|
213
|
+
"""The caller said yes."""
|
|
214
|
+
|
|
215
|
+
tool: str
|
|
216
|
+
call_id: str
|
|
217
|
+
audience: str
|
|
218
|
+
said: str
|
|
219
|
+
ttl_s: int
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# The agent reads the phrase; nothing runs until confirm.granted.
|
|
223
|
+
class ConfirmRequest(WireModel):
|
|
224
|
+
"""A tool with confirm set is about to run and the platform is asking the caller."""
|
|
225
|
+
|
|
226
|
+
tool: str
|
|
227
|
+
call_id: str
|
|
228
|
+
arguments: dict[str, Any]
|
|
229
|
+
audience: str
|
|
230
|
+
phrase: str
|
|
231
|
+
ttl_s: int
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Written into the agent's own log, which is the org's, before the door says no.
|
|
235
|
+
class CreditsExhausted(WireModel):
|
|
236
|
+
"""The gateway refused a call or a register because one of the org's quotas ran out."""
|
|
237
|
+
|
|
238
|
+
org: str
|
|
239
|
+
quota: Literal[
|
|
240
|
+
"minutes",
|
|
241
|
+
"messages",
|
|
242
|
+
"agents",
|
|
243
|
+
"concurrent_calls",
|
|
244
|
+
"memory_facts",
|
|
245
|
+
"knowledge_chunks",
|
|
246
|
+
"numbers",
|
|
247
|
+
"seats",
|
|
248
|
+
]
|
|
249
|
+
used: float
|
|
250
|
+
limit: int
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# The platform never reads it; the console shows it and evals may.
|
|
254
|
+
class Custom(WireModel):
|
|
255
|
+
"""A line the app wrote into the log with call.log."""
|
|
256
|
+
|
|
257
|
+
name: str
|
|
258
|
+
data: dict[str, Any]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class DevRequest(WireModel):
|
|
262
|
+
"""One ask of the process in the agent's directory, on a console's behalf."""
|
|
263
|
+
|
|
264
|
+
id: str
|
|
265
|
+
verb: DevVerb
|
|
266
|
+
data: dict[str, Any]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# An answer can be traced back to its chunks.
|
|
270
|
+
class DocsSources(WireModel):
|
|
271
|
+
"""What retrieval put in front of the model for this turn."""
|
|
272
|
+
|
|
273
|
+
query: str
|
|
274
|
+
sources: list[DocSource]
|
|
275
|
+
took_ms: float
|
|
276
|
+
speech_id: str | None = None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# Inside a call it says what failed; outside a call it says which command the gateway refused.
|
|
280
|
+
class ErrorEvent(WireModel):
|
|
281
|
+
"""Something went wrong."""
|
|
282
|
+
|
|
283
|
+
code: str
|
|
284
|
+
message: str
|
|
285
|
+
command: str | None = None
|
|
286
|
+
id: str | None = None
|
|
287
|
+
recoverable: bool
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# Written into the agent's own log, which is the org's, before the door says no — the caller was
|
|
291
|
+
# offered a call back instead of a room.
|
|
292
|
+
class FleetFull(WireModel):
|
|
293
|
+
"""The gateway refused to open a call because every worker of the fleet was full."""
|
|
294
|
+
|
|
295
|
+
channel: Channel
|
|
296
|
+
workers: int
|
|
297
|
+
active: int
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# Never stored; sent to the reader.
|
|
301
|
+
class LogCaughtUp(WireModel):
|
|
302
|
+
"""The replay is done: everything up to seq has been sent and what follows is live."""
|
|
303
|
+
|
|
304
|
+
seq: int
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# This reader missed a stretch: it reconnected too late for the store, or fell behind and the fanout
|
|
308
|
+
# dropped ephemeral entries. When the platform has a snapshot, it is here so the reader can catch up
|
|
309
|
+
# in one step. Never stored; sent to the reader.
|
|
310
|
+
class LogGap(WireModel):
|
|
311
|
+
"""This reader missed a stretch."""
|
|
312
|
+
|
|
313
|
+
from_seq: int
|
|
314
|
+
to_seq: int
|
|
315
|
+
snapshot: State | None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# What memory did for this turn or at hangup: a recall before the reply, a remember after the call,
|
|
319
|
+
# a forget on request.
|
|
320
|
+
class MemoryOps(WireModel):
|
|
321
|
+
"""What memory did for this turn or at hangup."""
|
|
322
|
+
|
|
323
|
+
ops: list[MemoryOp]
|
|
324
|
+
speech_id: str | None = None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# Ephemeral: it proves the socket is alive and says nothing else.
|
|
328
|
+
class Pong(WireModel):
|
|
329
|
+
"""The answer to ping."""
|
|
330
|
+
|
|
331
|
+
ts: float
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# The text stays out of the log; its hash and length let two states be compared.
|
|
335
|
+
class PromptChanged(WireModel):
|
|
336
|
+
"""A block of the prompt was rewritten."""
|
|
337
|
+
|
|
338
|
+
name: str
|
|
339
|
+
hash: str
|
|
340
|
+
chars: int
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class StateCauseTool(WireModel):
|
|
344
|
+
"""A tool call's result changed the state."""
|
|
345
|
+
|
|
346
|
+
kind: Literal["tool"] = "tool"
|
|
347
|
+
tool: str
|
|
348
|
+
call_id: str
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# A fact from outside changed the state: the app's handler for an event.received moved a field.
|
|
352
|
+
class StateCauseEvent(WireModel):
|
|
353
|
+
"""A fact from outside changed the state."""
|
|
354
|
+
|
|
355
|
+
kind: Literal["event"] = "event"
|
|
356
|
+
name: str
|
|
357
|
+
seq: int
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# What changed the state: a tool's result, or a fact from outside. Told apart by kind.
|
|
361
|
+
type StateCause = Annotated[StateCauseTool | StateCauseEvent, Field(discriminator="kind")]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# A tool's result or an outside fact caused it, and the cause says which; the whole state travels so
|
|
365
|
+
# a reader never needs the previous entry.
|
|
366
|
+
class StateChanged(WireModel):
|
|
367
|
+
"""The app's declared state changed."""
|
|
368
|
+
|
|
369
|
+
state: dict[str, Any]
|
|
370
|
+
changed: list[str]
|
|
371
|
+
cause: StateCause | None = None
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# call.ended follows with reason supervisor_ended.
|
|
375
|
+
class SupervisorEnded(WireModel):
|
|
376
|
+
"""A supervisor hung up the call."""
|
|
377
|
+
|
|
378
|
+
by: Supervisor
|
|
379
|
+
reason: str | None = None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class SupervisorReleased(WireModel):
|
|
383
|
+
"""The supervisor gave the line back; the agent resumes with the history intact."""
|
|
384
|
+
|
|
385
|
+
by: Supervisor
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class SupervisorSaid(WireModel):
|
|
389
|
+
"""A supervisor made the agent say this to the caller."""
|
|
390
|
+
|
|
391
|
+
by: Supervisor
|
|
392
|
+
text: str
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class SupervisorTookOver(WireModel):
|
|
396
|
+
"""A supervisor took the line; the agent is quiet until supervisor.released."""
|
|
397
|
+
|
|
398
|
+
by: Supervisor
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# call.transferred says how it went.
|
|
402
|
+
class SupervisorTransferred(WireModel):
|
|
403
|
+
"""A supervisor asked for a transfer."""
|
|
404
|
+
|
|
405
|
+
by: Supervisor
|
|
406
|
+
to: str
|
|
407
|
+
mode: TransferMode
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class SupervisorWhispered(WireModel):
|
|
411
|
+
"""A supervisor told the agent something the caller never heard."""
|
|
412
|
+
|
|
413
|
+
by: Supervisor
|
|
414
|
+
text: str
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# The platform sends this to the app and the app's method runs in the app's own process; tool.result
|
|
418
|
+
# closes it.
|
|
419
|
+
class ToolCall(WireModel):
|
|
420
|
+
"""The model called a tool."""
|
|
421
|
+
|
|
422
|
+
call_id: str
|
|
423
|
+
name: str
|
|
424
|
+
arguments: dict[str, Any]
|
|
425
|
+
speech_id: str | None = None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# The app's state moved and each tool's when was recomputed.
|
|
429
|
+
class ToolsChanged(WireModel):
|
|
430
|
+
"""The tools the model can see changed."""
|
|
431
|
+
|
|
432
|
+
visible: list[str]
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# With it, everything the session measured about the reply.
|
|
436
|
+
class AgentTurnEnded(WireModel):
|
|
437
|
+
"""The agent's reply is over and this is what was said."""
|
|
438
|
+
|
|
439
|
+
speech_id: str
|
|
440
|
+
item_id: str | None = None
|
|
441
|
+
text: str
|
|
442
|
+
interrupted: bool
|
|
443
|
+
metrics: AgentTurnMetrics
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# With it, everything the session measured about the turn.
|
|
447
|
+
class UserTurnEnded(WireModel):
|
|
448
|
+
"""The caller's turn is over and this is what they said."""
|
|
449
|
+
|
|
450
|
+
speech_id: str
|
|
451
|
+
item_id: str | None = None
|
|
452
|
+
text: str
|
|
453
|
+
language: str | None = None
|
|
454
|
+
transcript_confidence: float | None = None
|
|
455
|
+
metrics: UserTurnMetrics
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class UserStateChanged(WireModel):
|
|
459
|
+
"""The caller's state changed, in the session's own words."""
|
|
460
|
+
|
|
461
|
+
state: UserState
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Interim while final is false; the final one becomes turn.user. Interim entries are ephemeral.
|
|
465
|
+
class UserTranscript(WireModel):
|
|
466
|
+
"""Words from the caller as the recognizer hears them."""
|
|
467
|
+
|
|
468
|
+
text: str
|
|
469
|
+
final: bool
|
|
470
|
+
language: str | None = None
|
|
471
|
+
confidence: float | None = None
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""The golden call log and the state it reduces to: every consumer proves the same thing."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
GOLDEN_LOG = Path(str(files(__package__) / "call-log-golden.json"))
|
|
7
|
+
GOLDEN_STATE = Path(str(files(__package__) / "call-log-golden.state.json"))
|