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,407 @@
|
|
|
1
|
+
"""Generated from schema/defs.json: the shapes shared across the wire."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from pinecall_protocol._base import WireModel
|
|
8
|
+
|
|
9
|
+
# The door the public came through: a phone call over SIP, the browser widget over WebRTC, or
|
|
10
|
+
# WhatsApp text.
|
|
11
|
+
type Channel = Literal["phone", "web", "whatsapp"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Inbound: the public reached the agent. Outbound: the agent reached out (a dial).
|
|
15
|
+
type Direction = Literal["inbound", "outbound"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Which of the two worlds a key opens, and so which world an agent is held in and a call ran in. A
|
|
19
|
+
# key is issued into one; an agent registered on it and every call it takes carry that one; a door
|
|
20
|
+
# claimed in one is refused to a key of the other. `sandbox` is where things are written and
|
|
21
|
+
# `production` is what the public reaches — and whether a sandbox agent is one PERSON's copy or the
|
|
22
|
+
# team's shared one is not this field: it is whether the key that registered it names a person.
|
|
23
|
+
# Every key issued before the field existed is production.
|
|
24
|
+
type Env = Literal["production", "sandbox"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# What a console may ask of the process standing in the agent's directory, relayed by the gateway: a
|
|
28
|
+
# written call to the class mounted there (chat), a simulated caller from its personas, its goldens
|
|
29
|
+
# and a suite of them, its knowledge folder pushed or its golden asked, its memory goldens, a call
|
|
30
|
+
# promoted to a candidate file, the drift of the last two windows, and the reproductions a broken
|
|
31
|
+
# run left on that disk. Everything else a console needs is a door of the gateway.
|
|
32
|
+
type DevVerb = Literal[
|
|
33
|
+
"chat.roster",
|
|
34
|
+
"chat.start",
|
|
35
|
+
"chat.say",
|
|
36
|
+
"chat.end",
|
|
37
|
+
"simulate.roster",
|
|
38
|
+
"simulate.start",
|
|
39
|
+
"goldens.roster",
|
|
40
|
+
"goldens.run",
|
|
41
|
+
"knowledge.roster",
|
|
42
|
+
"knowledge.push",
|
|
43
|
+
"knowledge.eval",
|
|
44
|
+
"memory.roster",
|
|
45
|
+
"memory.eval",
|
|
46
|
+
"memory.extraction",
|
|
47
|
+
"promote.roster",
|
|
48
|
+
"promote.write",
|
|
49
|
+
"drift.read",
|
|
50
|
+
"reproductions.roster",
|
|
51
|
+
"reproductions.read",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Why the call is over. Who hung up, what failed before anybody could, drained: the platform took
|
|
56
|
+
# the worker down (a deploy, a stop) with the call still on it, or app_detached: the app holding the
|
|
57
|
+
# agent closed its socket mid-call, so nothing was rendering the prompt or answering a tool — both
|
|
58
|
+
# are nobody's fault and neither is an error.
|
|
59
|
+
type EndReason = Literal[
|
|
60
|
+
"caller_hung_up",
|
|
61
|
+
"agent_hung_up",
|
|
62
|
+
"supervisor_ended",
|
|
63
|
+
"transferred",
|
|
64
|
+
"no_answer",
|
|
65
|
+
"busy",
|
|
66
|
+
"dial_failed",
|
|
67
|
+
"timeout",
|
|
68
|
+
"drained",
|
|
69
|
+
"app_detached",
|
|
70
|
+
"error",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Whose action ended the call. platform covers timeouts, errors and a drained worker.
|
|
75
|
+
type EndedBy = Literal["caller", "agent", "supervisor", "platform"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# What one judge answered about a finished call. Held: the rule held. Broken: it did not, and the
|
|
79
|
+
# reason names the evidence. Deferred: the judge was asked and could not settle it. Skipped: nobody
|
|
80
|
+
# asked it — no model was reachable inside the call's judging budget.
|
|
81
|
+
type ScoreVerdict = Literal["held", "broken", "deferred", "skipped"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Cold: the caller is sent on and the agent leaves. Warm: the agent stays on the line until the
|
|
85
|
+
# other side answers, then leaves.
|
|
86
|
+
type TransferMode = Literal["cold", "warm"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Which region of the prompt a block lives in: static, before the history, cached by the provider;
|
|
90
|
+
# or dynamic, after the history, replaced every turn. The append-only history in between is never
|
|
91
|
+
# written by the app.
|
|
92
|
+
type PromptRegion = Literal["static", "dynamic"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# How the knowledge base reaches the model: retrieved, the platform runs search itself when the
|
|
96
|
+
# caller's turn ends; or tool, the model calls search when it decides to. Either way the chunks
|
|
97
|
+
# arrive as a tool result.
|
|
98
|
+
type DocsMode = Literal["retrieved", "tool"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# The two tools the platform runs on the app's behalf: recall reads the contact's facts out of
|
|
102
|
+
# memory, search reads chunks out of the knowledge base. The app declares memory and docs and writes
|
|
103
|
+
# neither method; the platform runs the lookup, and the answer reaches the model as a tool result
|
|
104
|
+
# rather than as part of the prompt.
|
|
105
|
+
type PlatformTool = Literal["recall", "search"]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# The default layout, when an agent declares none, is identity, knowledge and tools (static), then
|
|
109
|
+
# the history, then view (dynamic). A block's text is written per call with prompt.set.
|
|
110
|
+
class PromptBlockSpec(WireModel):
|
|
111
|
+
"""One named block of the prompt and the region it lives in."""
|
|
112
|
+
|
|
113
|
+
name: str = Field(pattern="^[a-z][a-z0-9_]*$")
|
|
114
|
+
region: PromptRegion
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# What the platform believes the person on the line is doing right now. The states are the session's
|
|
118
|
+
# own.
|
|
119
|
+
type UserState = Literal["listening", "speaking", "away"]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# What the agent is doing right now, in the session's own words: warming up, waiting, hearing the
|
|
123
|
+
# caller, generating, or playing audio.
|
|
124
|
+
type AgentState = Literal["initializing", "idle", "listening", "thinking", "speaking"]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Who a participant is to the call: the person the agent serves (over SIP or the widget), the agent
|
|
128
|
+
# itself, a supervisor who took a seat in the room, a listener who only hears, or a second SIP leg
|
|
129
|
+
# that room.invite brought in.
|
|
130
|
+
type ParticipantKind = Literal["caller", "agent", "supervisor", "listener", "sip"]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# What a track carries: a microphone's audio, a camera's video, or a screen share.
|
|
134
|
+
type TrackKind = Literal["audio", "video", "screen"]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Where a track comes from, as livekit's TrackSource names it, in lower case.
|
|
138
|
+
type TrackSource = Literal["microphone", "camera", "screen_share", "screen_share_audio", "unknown"]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Where an outside fact came from: the tenant's backend over the app socket (app), or a
|
|
142
|
+
# participant's browser over the DataChannel (participant).
|
|
143
|
+
type EventSource = Literal["app", "participant"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Who may see a field of the app's state: everyone in the call (public), the tenant's own readers
|
|
147
|
+
# (tenant, the default for a field never declared), or nobody without masking (pii).
|
|
148
|
+
type Visibility = Literal["public", "tenant", "pii"]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Which projection a sink applies before a state or an entry leaves the platform: public for a
|
|
152
|
+
# participant reading its own call, tenant for the tenant's readers. The contract is
|
|
153
|
+
# docs/protocol/projections.md; a client never applies one.
|
|
154
|
+
type Projection = Literal["public", "tenant"]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Everything is optional: a web visitor may be nobody yet.
|
|
158
|
+
class Contact(WireModel):
|
|
159
|
+
"""Who is on the line, as far as the platform knows."""
|
|
160
|
+
|
|
161
|
+
id: str | None = None
|
|
162
|
+
phone: str | None = None
|
|
163
|
+
name: str | None = None
|
|
164
|
+
email: str | None = None
|
|
165
|
+
external_id: str | None = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# A number is a route, never an agent.
|
|
169
|
+
class Route(WireModel):
|
|
170
|
+
"""One door to an agent: a channel and, for phone and WhatsApp, the number that answers."""
|
|
171
|
+
|
|
172
|
+
channel: Channel
|
|
173
|
+
number: str | None
|
|
174
|
+
label: str | None = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Supervisor(WireModel):
|
|
178
|
+
"""The human who sent a supervise verb, as the token that let them in names them."""
|
|
179
|
+
|
|
180
|
+
id: str
|
|
181
|
+
name: str | None = None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# What the app declares about one tool: the contract the model sees and the rules the platform
|
|
185
|
+
# enforces before running it.
|
|
186
|
+
class ToolSpec(WireModel):
|
|
187
|
+
"""What the app declares about one tool."""
|
|
188
|
+
|
|
189
|
+
name: str
|
|
190
|
+
description: str
|
|
191
|
+
parameters: dict[str, Any]
|
|
192
|
+
side_effect: Literal["read", "write", "irreversible"] = "read"
|
|
193
|
+
confirm: str | None = None
|
|
194
|
+
pii: list[str] | None = None
|
|
195
|
+
timeout_s: float | None = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Either an output or an error, never both.
|
|
199
|
+
class ToolResult(WireModel):
|
|
200
|
+
"""What came back from running a tool in the app's process."""
|
|
201
|
+
|
|
202
|
+
call_id: str
|
|
203
|
+
name: str
|
|
204
|
+
output: Any = None
|
|
205
|
+
error: str | None = None
|
|
206
|
+
summary: str | None = None
|
|
207
|
+
duration_s: float | None = None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# One thing remembered about a contact: a sentence, where it came from, and how well it matched when
|
|
211
|
+
# recalled.
|
|
212
|
+
class MemoryFact(WireModel):
|
|
213
|
+
"""One thing remembered about a contact."""
|
|
214
|
+
|
|
215
|
+
id: str | None = None
|
|
216
|
+
text: str
|
|
217
|
+
category: str | None = None
|
|
218
|
+
score: float | None = None
|
|
219
|
+
source: str | None = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# One operation against the contact's memory: a recall during the turn, a remember at hangup, or a
|
|
223
|
+
# forget on request.
|
|
224
|
+
class MemoryOp(WireModel):
|
|
225
|
+
"""One operation against the contact's memory."""
|
|
226
|
+
|
|
227
|
+
op: Literal["recall", "remember", "forget"]
|
|
228
|
+
contact: str | None = None
|
|
229
|
+
query: str | None = None
|
|
230
|
+
facts: list[MemoryFact]
|
|
231
|
+
took_ms: float
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class DocSource(WireModel):
|
|
235
|
+
"""One chunk of the knowledge base that retrieval put in front of the model for this turn."""
|
|
236
|
+
|
|
237
|
+
id: str
|
|
238
|
+
path: str
|
|
239
|
+
heading: str | None = None
|
|
240
|
+
score: float
|
|
241
|
+
excerpt: str | None = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class CostRate(WireModel):
|
|
245
|
+
"""The exchange rate the cost was computed with, stated so the number can be reproduced."""
|
|
246
|
+
|
|
247
|
+
currency: Literal["EUR"] = "EUR"
|
|
248
|
+
usd_to_eur: float
|
|
249
|
+
as_of: str
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class CostRow(WireModel):
|
|
253
|
+
"""One priced line: a model, what was counted, how much, and what it came to."""
|
|
254
|
+
|
|
255
|
+
provider: str
|
|
256
|
+
model: str
|
|
257
|
+
unit: Literal[
|
|
258
|
+
"input_tokens",
|
|
259
|
+
"cached_input_tokens",
|
|
260
|
+
"cache_creation_tokens",
|
|
261
|
+
"output_tokens",
|
|
262
|
+
"characters",
|
|
263
|
+
"audio_seconds",
|
|
264
|
+
"requests",
|
|
265
|
+
"session_seconds",
|
|
266
|
+
]
|
|
267
|
+
quantity: float
|
|
268
|
+
unit_price_usd: float
|
|
269
|
+
eur: float
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# It is listed, never priced at zero.
|
|
273
|
+
class UnpricedRow(WireModel):
|
|
274
|
+
"""A usage row the price table does not know."""
|
|
275
|
+
|
|
276
|
+
provider: str
|
|
277
|
+
model: str
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# The runtime never prices commercially; this is the provider's bill as best we know it.
|
|
281
|
+
class Cost(WireModel):
|
|
282
|
+
"""What the call cost in provider fees, informational, in euros."""
|
|
283
|
+
|
|
284
|
+
eur: float
|
|
285
|
+
rate: CostRate
|
|
286
|
+
rows: list[CostRow]
|
|
287
|
+
unpriced: list[UnpricedRow]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# Which voice speaks for the agent: the name it was asked for, or the id the provider knows it by.
|
|
291
|
+
class VoiceConfig(WireModel):
|
|
292
|
+
"""Which voice speaks for the agent."""
|
|
293
|
+
|
|
294
|
+
name: str | None = None
|
|
295
|
+
provider: str | None = None
|
|
296
|
+
model: str | None = None
|
|
297
|
+
voice_id: str | None = None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class ModelConfig(WireModel):
|
|
301
|
+
"""Which model does a job (the LLM, or the STT), and the one or two knobs worth turning."""
|
|
302
|
+
|
|
303
|
+
provider: str
|
|
304
|
+
model: str
|
|
305
|
+
temperature: float | None = None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TurnConfig(WireModel):
|
|
309
|
+
"""How the session decides that the caller has finished, and when the caller may interrupt."""
|
|
310
|
+
|
|
311
|
+
min_interruption_words: int | None = None
|
|
312
|
+
endpointing_ms: int | None = None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# How the voice says one word it would otherwise get wrong: a proper name, a brand, a street.
|
|
316
|
+
class Pronunciation(WireModel):
|
|
317
|
+
"""How the voice says one word it would otherwise get wrong."""
|
|
318
|
+
|
|
319
|
+
word: str
|
|
320
|
+
spoken: str
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# A field never declared is tenant.
|
|
324
|
+
class StateFieldSpec(WireModel):
|
|
325
|
+
"""What the app declares about one field of its state: who may see it."""
|
|
326
|
+
|
|
327
|
+
name: str
|
|
328
|
+
visibility: Visibility
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# An event nobody declared is refused before it touches the log.
|
|
332
|
+
class EventSpec(WireModel):
|
|
333
|
+
"""One outside event the agent accepts, and from whom."""
|
|
334
|
+
|
|
335
|
+
name: str
|
|
336
|
+
from_: list[EventSource] = Field(alias="from")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class KnowledgeFile(WireModel):
|
|
340
|
+
"""One file of knowledge, sent whole: its path as the tenant keeps it, and its text."""
|
|
341
|
+
|
|
342
|
+
path: str
|
|
343
|
+
text: str
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# It is named by the base it was pushed under, with PUT /v1/knowledge/{base}.
|
|
347
|
+
class DocsConfig(WireModel):
|
|
348
|
+
"""The knowledge base the agent answers from, and how its chunks reach the model."""
|
|
349
|
+
|
|
350
|
+
base: str
|
|
351
|
+
mode: DocsMode = "retrieved"
|
|
352
|
+
k: int = 8
|
|
353
|
+
min_score: float | None = None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Exactly one of the two, because there are only two ways to open one: `say` are the words
|
|
357
|
+
# themselves and `reply` is what the model is told before it finds its own. They are agent.say and
|
|
358
|
+
# agent.reply declared instead of called, so a class that opens every call the same way needs no
|
|
359
|
+
# onCall hook to do it, and an operator can turn the opening at the pipeline door without a deploy.
|
|
360
|
+
# Absent: nobody speaks until the caller does.
|
|
361
|
+
class GreetingConfig(WireModel):
|
|
362
|
+
"""How the agent opens a call, before the caller has said anything."""
|
|
363
|
+
|
|
364
|
+
say: str | None = None
|
|
365
|
+
reply: str | None = None
|
|
366
|
+
allow_interruptions: bool | None = None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# Declaring this is what puts livekit's own end_call tool in front of the model; a class that says
|
|
370
|
+
# nothing here cannot hang up, and the call ends when the caller does or when a supervisor says so.
|
|
371
|
+
# The tool is hidden while the agent is greeting, because a model that can hang up on its first turn
|
|
372
|
+
# eventually does.
|
|
373
|
+
class HangupConfig(WireModel):
|
|
374
|
+
"""Whether the model may end the call itself."""
|
|
375
|
+
|
|
376
|
+
when: str = ""
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# Both lists are in the tenant's own words.
|
|
380
|
+
class MemoryConfig(WireModel):
|
|
381
|
+
"""What memory keeps about a contact across calls, and what it must never keep."""
|
|
382
|
+
|
|
383
|
+
remember: list[str] = Field(default_factory=list[str])
|
|
384
|
+
forget: list[str] = Field(default_factory=list[str])
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# What an app declares about its agent: the voice, the models, the language, the greeting, the
|
|
388
|
+
# tools, and who may see and send what. Every field is optional so a configure can change one thing.
|
|
389
|
+
class AgentConfig(WireModel):
|
|
390
|
+
"""What an app declares about its agent."""
|
|
391
|
+
|
|
392
|
+
prompt: list[PromptBlockSpec] | None = None
|
|
393
|
+
language: str | None = None
|
|
394
|
+
greeting: GreetingConfig | None = None
|
|
395
|
+
voice: VoiceConfig | None = None
|
|
396
|
+
llm: ModelConfig | None = None
|
|
397
|
+
stt: ModelConfig | None = None
|
|
398
|
+
turn: TurnConfig | None = None
|
|
399
|
+
says: list[Pronunciation] | None = None
|
|
400
|
+
hears: list[str] | None = None
|
|
401
|
+
knowledge: KnowledgeFile | None = None
|
|
402
|
+
docs: DocsConfig | None = None
|
|
403
|
+
memory: MemoryConfig | None = None
|
|
404
|
+
hangup: HangupConfig | None = None
|
|
405
|
+
tools: list[ToolSpec] | None = None
|
|
406
|
+
state_fields: list[StateFieldSpec] | None = None
|
|
407
|
+
events: list[EventSpec] | None = None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Generated from schema/envelope.json: the log entry and the command frame."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pinecall_protocol._base import WireModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# seq is written before control returns, so two readers never disagree about order.
|
|
9
|
+
class Entry(WireModel):
|
|
10
|
+
"""One line of a call's log, or of an agent's log when call is null."""
|
|
11
|
+
|
|
12
|
+
seq: int
|
|
13
|
+
ts: float
|
|
14
|
+
call: str | None
|
|
15
|
+
agent: str
|
|
16
|
+
type: str
|
|
17
|
+
ephemeral: bool
|
|
18
|
+
data: dict[str, Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# The gateway answers with the events the command produces, or with an error naming the command's
|
|
22
|
+
# id.
|
|
23
|
+
class Command(WireModel):
|
|
24
|
+
"""One instruction from an app to the gateway over its WebSocket."""
|
|
25
|
+
|
|
26
|
+
type: str
|
|
27
|
+
agent: str
|
|
28
|
+
call: str | None
|
|
29
|
+
id: str | None = None
|
|
30
|
+
data: dict[str, Any]
|