pinecall 0.0.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +167 -0
- data/LICENSE +202 -0
- data/README.md +140 -0
- data/lib/pinecall/agent/author.rb +31 -0
- data/lib/pinecall/agent/config.rb +124 -0
- data/lib/pinecall/agent/doc.rb +73 -0
- data/lib/pinecall/agent/knowledge.rb +18 -0
- data/lib/pinecall/agent/searching.rb +33 -0
- data/lib/pinecall/agent/spec.rb +133 -0
- data/lib/pinecall/agent/state.rb +185 -0
- data/lib/pinecall/agent/tools.rb +119 -0
- data/lib/pinecall/agent.rb +160 -0
- data/lib/pinecall/blocks.rb +152 -0
- data/lib/pinecall/bridge.rb +239 -0
- data/lib/pinecall/call_world/answers.rb +61 -0
- data/lib/pinecall/call_world/room.rb +25 -0
- data/lib/pinecall/call_world.rb +175 -0
- data/lib/pinecall/client/agent.rb +179 -0
- data/lib/pinecall/client/call.rb +140 -0
- data/lib/pinecall/client/connection.rb +190 -0
- data/lib/pinecall/client/endpoints.rb +30 -0
- data/lib/pinecall/client/listeners.rb +42 -0
- data/lib/pinecall/client/observe.rb +109 -0
- data/lib/pinecall/client/rest.rb +61 -0
- data/lib/pinecall/client.rb +212 -0
- data/lib/pinecall/errors.rb +53 -0
- data/lib/pinecall/panel.rb +138 -0
- data/lib/pinecall/reading.rb +37 -0
- data/lib/pinecall/rules.rb +38 -0
- data/lib/pinecall/serve/held.rb +40 -0
- data/lib/pinecall/serve/loading.rb +102 -0
- data/lib/pinecall/serve/viewing.rb +40 -0
- data/lib/pinecall/serve.rb +169 -0
- data/lib/pinecall/testing.rb +190 -0
- data/lib/pinecall/version.rb +6 -0
- data/lib/pinecall/view.rb +70 -0
- data/lib/pinecall/wire/codec.rb +75 -0
- data/lib/pinecall/wire/enums.rb +104 -0
- data/lib/pinecall/wire/errors.rb +12 -0
- data/lib/pinecall/wire/reduce.rb +249 -0
- data/lib/pinecall/wire/registry.rb +168 -0
- data/lib/pinecall/wire/shapes.rb +33 -0
- data/lib/pinecall/wire/shapes_call_events.rb +137 -0
- data/lib/pinecall/wire/shapes_commands.rb +114 -0
- data/lib/pinecall/wire/shapes_config.rb +70 -0
- data/lib/pinecall/wire/shapes_doors.rb +96 -0
- data/lib/pinecall/wire/shapes_events.rb +358 -0
- data/lib/pinecall/wire/shapes_metrics.rb +109 -0
- data/lib/pinecall/wire/shapes_parts.rb +340 -0
- data/lib/pinecall/wire/state.rb +56 -0
- data/lib/pinecall/wire/validate.rb +154 -0
- data/lib/pinecall/wire.rb +43 -0
- data/lib/pinecall.rb +45 -0
- data/sig/pinecall.rbs +453 -0
- metadata +119 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The shapes the events, commands and doors are made of.
|
|
4
|
+
|
|
5
|
+
module Pinecall
|
|
6
|
+
module Wire
|
|
7
|
+
module Shapes
|
|
8
|
+
PARTS = {
|
|
9
|
+
"Contact" => {
|
|
10
|
+
id: { kind: :str },
|
|
11
|
+
phone: { kind: :str },
|
|
12
|
+
name: { kind: :str },
|
|
13
|
+
email: { kind: :str },
|
|
14
|
+
external_id: { kind: :str }
|
|
15
|
+
}.freeze,
|
|
16
|
+
"Route" => {
|
|
17
|
+
channel: { kind: :ref, ref: "Channel", required: true },
|
|
18
|
+
number: { kind: :str, null: true, required: true },
|
|
19
|
+
label: { kind: :str }
|
|
20
|
+
}.freeze,
|
|
21
|
+
"Supervisor" => {
|
|
22
|
+
id: { kind: :str, required: true },
|
|
23
|
+
name: { kind: :str }
|
|
24
|
+
}.freeze,
|
|
25
|
+
"MemoryFact" => {
|
|
26
|
+
id: { kind: :str },
|
|
27
|
+
text: { kind: :str, required: true },
|
|
28
|
+
category: { kind: :str },
|
|
29
|
+
score: { kind: :float },
|
|
30
|
+
source: { kind: :str }
|
|
31
|
+
}.freeze,
|
|
32
|
+
"MemoryOp" => {
|
|
33
|
+
op: { kind: :enum, values: %w[recall remember forget], required: true },
|
|
34
|
+
contact: { kind: :str },
|
|
35
|
+
query: { kind: :str },
|
|
36
|
+
facts: { kind: :list, items: { kind: :ref, ref: "MemoryFact" }, required: true },
|
|
37
|
+
took_ms: { kind: :float, required: true }
|
|
38
|
+
}.freeze,
|
|
39
|
+
"DocSource" => {
|
|
40
|
+
id: { kind: :str, required: true },
|
|
41
|
+
base: { kind: :str },
|
|
42
|
+
path: { kind: :str, required: true },
|
|
43
|
+
heading: { kind: :str },
|
|
44
|
+
score: { kind: :float, required: true },
|
|
45
|
+
excerpt: { kind: :str }
|
|
46
|
+
}.freeze,
|
|
47
|
+
"CostRow" => {
|
|
48
|
+
provider: { kind: :str, required: true },
|
|
49
|
+
model: { kind: :str, required: true },
|
|
50
|
+
unit: { kind: :enum, values: %w[input_tokens cached_input_tokens cache_creation_tokens output_tokens characters audio_seconds requests session_seconds minutes], required: true },
|
|
51
|
+
quantity: { kind: :float, required: true },
|
|
52
|
+
unit_price_usd: { kind: :float, required: true },
|
|
53
|
+
usd: { kind: :float, required: true }
|
|
54
|
+
}.freeze,
|
|
55
|
+
"UnpricedRow" => {
|
|
56
|
+
provider: { kind: :str, required: true },
|
|
57
|
+
model: { kind: :str, required: true }
|
|
58
|
+
}.freeze,
|
|
59
|
+
"Cost" => {
|
|
60
|
+
usd: { kind: :float, required: true },
|
|
61
|
+
rows: { kind: :list, items: { kind: :ref, ref: "CostRow" }, required: true },
|
|
62
|
+
unpriced: { kind: :list, items: { kind: :ref, ref: "UnpricedRow" }, required: true }
|
|
63
|
+
}.freeze,
|
|
64
|
+
"Entry" => {
|
|
65
|
+
seq: { kind: :int, required: true },
|
|
66
|
+
ts: { kind: :float, required: true },
|
|
67
|
+
call: { kind: :str, null: true, required: true },
|
|
68
|
+
agent: { kind: :str, required: true },
|
|
69
|
+
type: { kind: :str, required: true },
|
|
70
|
+
ephemeral: { kind: :bool, required: true },
|
|
71
|
+
data: { kind: :json, required: true }
|
|
72
|
+
}.freeze,
|
|
73
|
+
"Command" => {
|
|
74
|
+
type: { kind: :str, required: true },
|
|
75
|
+
agent: { kind: :str, required: true },
|
|
76
|
+
call: { kind: :str, null: true, required: true },
|
|
77
|
+
id: { kind: :str },
|
|
78
|
+
data: { kind: :json, required: true }
|
|
79
|
+
}.freeze,
|
|
80
|
+
"Metadata" => {
|
|
81
|
+
model_name: { kind: :str, null: true },
|
|
82
|
+
model_provider: { kind: :str, null: true }
|
|
83
|
+
}.freeze,
|
|
84
|
+
"TurnMetadata" => {
|
|
85
|
+
model_name: { kind: :str },
|
|
86
|
+
model_provider: { kind: :str }
|
|
87
|
+
}.freeze,
|
|
88
|
+
"GoldenQuestion" => {
|
|
89
|
+
asks: { kind: :str, required: true },
|
|
90
|
+
expects: { kind: :str, required: true }
|
|
91
|
+
}.freeze,
|
|
92
|
+
"GoldenMiss" => {
|
|
93
|
+
asks: { kind: :str, required: true },
|
|
94
|
+
expects: { kind: :str, required: true },
|
|
95
|
+
found: { kind: :list, items: { kind: :str }, required: true }
|
|
96
|
+
}.freeze,
|
|
97
|
+
"KnowledgeBase" => {
|
|
98
|
+
base: { kind: :str, required: true },
|
|
99
|
+
chunks: { kind: :int, required: true },
|
|
100
|
+
model: { kind: :str, required: true },
|
|
101
|
+
pushed_at: { kind: :float, required: true }
|
|
102
|
+
}.freeze,
|
|
103
|
+
"ContactFact" => {
|
|
104
|
+
id: { kind: :str },
|
|
105
|
+
text: { kind: :str, required: true },
|
|
106
|
+
category: { kind: :str },
|
|
107
|
+
source: { kind: :str },
|
|
108
|
+
valid_from: { kind: :float, required: true },
|
|
109
|
+
invalidated_at: { kind: :float, null: true, required: true }
|
|
110
|
+
}.freeze,
|
|
111
|
+
"MemoryQuestion" => {
|
|
112
|
+
holds: { kind: :list, items: { kind: :str }, required: true },
|
|
113
|
+
asks: { kind: :str, required: true },
|
|
114
|
+
expects: { kind: :list, items: { kind: :str }, required: true }
|
|
115
|
+
}.freeze,
|
|
116
|
+
"MemoryMiss" => {
|
|
117
|
+
asks: { kind: :str, required: true },
|
|
118
|
+
missing: { kind: :list, items: { kind: :str }, required: true },
|
|
119
|
+
found: { kind: :list, items: { kind: :str }, required: true }
|
|
120
|
+
}.freeze,
|
|
121
|
+
"ExtractionExpected" => {
|
|
122
|
+
writes: { kind: :list, items: { kind: :str }, default: [] },
|
|
123
|
+
never: { kind: :list, items: { kind: :str }, default: [] },
|
|
124
|
+
never_says: { kind: :list, items: { kind: :str }, default: [] },
|
|
125
|
+
invalidates: { kind: :list, items: { kind: :str }, default: [] }
|
|
126
|
+
}.freeze,
|
|
127
|
+
"ExtractionGolden" => {
|
|
128
|
+
name: { kind: :str, required: true },
|
|
129
|
+
said: { kind: :list, items: { kind: :tuple, members: [{ kind: :str }, { kind: :str }] }, required: true },
|
|
130
|
+
holds: { kind: :list, items: { kind: :str }, default: [] },
|
|
131
|
+
plants: { kind: :list, items: { kind: :str }, default: [] },
|
|
132
|
+
channel: { kind: :ref, ref: "Channel", default: "phone" },
|
|
133
|
+
expect: { kind: :ref, ref: "ExtractionExpected", default: {} }
|
|
134
|
+
}.freeze,
|
|
135
|
+
"ExtractionBroke" => {
|
|
136
|
+
check: { kind: :str, required: true },
|
|
137
|
+
detail: { kind: :str, required: true }
|
|
138
|
+
}.freeze,
|
|
139
|
+
"ExtractionJudged" => {
|
|
140
|
+
name: { kind: :str, required: true },
|
|
141
|
+
held: { kind: :bool, required: true },
|
|
142
|
+
wrote: { kind: :list, items: { kind: :str }, default: [] },
|
|
143
|
+
refused: { kind: :list, items: { kind: :str }, default: [] },
|
|
144
|
+
broke: { kind: :list, items: { kind: :ref, ref: "ExtractionBroke" }, default: [] }
|
|
145
|
+
}.freeze,
|
|
146
|
+
"UserTurn" => {
|
|
147
|
+
role: { kind: :const, const: "user", required: true },
|
|
148
|
+
speech_id: { kind: :str, required: true },
|
|
149
|
+
item_id: { kind: :str },
|
|
150
|
+
text: { kind: :str, required: true },
|
|
151
|
+
language: { kind: :str },
|
|
152
|
+
transcript_confidence: { kind: :float },
|
|
153
|
+
metrics: { kind: :ref, ref: "UserTurnMetrics", required: true }
|
|
154
|
+
}.freeze,
|
|
155
|
+
"AgentTurn" => {
|
|
156
|
+
role: { kind: :const, const: "agent", required: true },
|
|
157
|
+
speech_id: { kind: :str, required: true },
|
|
158
|
+
item_id: { kind: :str },
|
|
159
|
+
text: { kind: :str, required: true },
|
|
160
|
+
interrupted: { kind: :bool, required: true },
|
|
161
|
+
metrics: { kind: :ref, ref: "AgentTurnMetrics", required: true }
|
|
162
|
+
}.freeze,
|
|
163
|
+
"ToolRun" => {
|
|
164
|
+
call_id: { kind: :str, required: true },
|
|
165
|
+
name: { kind: :str, required: true },
|
|
166
|
+
arguments: { kind: :json, required: true },
|
|
167
|
+
speech_id: { kind: :str },
|
|
168
|
+
status: { kind: :enum, values: %w[running done failed], required: true },
|
|
169
|
+
output: { kind: :any },
|
|
170
|
+
error: { kind: :str },
|
|
171
|
+
summary: { kind: :str },
|
|
172
|
+
duration_s: { kind: :float },
|
|
173
|
+
seq: { kind: :int, required: true }
|
|
174
|
+
}.freeze,
|
|
175
|
+
"PromptBlockState" => {
|
|
176
|
+
hash: { kind: :str, required: true },
|
|
177
|
+
chars: { kind: :int, required: true },
|
|
178
|
+
seq: { kind: :int, required: true }
|
|
179
|
+
}.freeze,
|
|
180
|
+
"Confirm" => {
|
|
181
|
+
tool: { kind: :str, required: true },
|
|
182
|
+
call_id: { kind: :str, required: true },
|
|
183
|
+
audience: { kind: :str, required: true },
|
|
184
|
+
phrase: { kind: :str, required: true },
|
|
185
|
+
status: { kind: :enum, values: %w[pending granted declined], required: true },
|
|
186
|
+
said: { kind: :str },
|
|
187
|
+
reason: { kind: :str }
|
|
188
|
+
}.freeze,
|
|
189
|
+
"Handoff" => {
|
|
190
|
+
active: { kind: :bool, required: true },
|
|
191
|
+
by: { kind: :ref, null: true, ref: "Supervisor", required: true }
|
|
192
|
+
}.freeze,
|
|
193
|
+
"TransferState" => {
|
|
194
|
+
to: { kind: :str, required: true },
|
|
195
|
+
mode: { kind: :ref, null: true, ref: "TransferMode" },
|
|
196
|
+
status: { kind: :enum, values: %w[requested done failed], required: true },
|
|
197
|
+
by: { kind: :enum, values: %w[agent supervisor], required: true }
|
|
198
|
+
}.freeze,
|
|
199
|
+
"AttentionState" => {
|
|
200
|
+
reason: { kind: :str, required: true },
|
|
201
|
+
wait_s: { kind: :float, required: true },
|
|
202
|
+
status: { kind: :enum, values: %w[open answered lapsed], required: true },
|
|
203
|
+
asked_at: { kind: :float, required: true },
|
|
204
|
+
by: { kind: :ref, null: true, ref: "Supervisor", required: true }
|
|
205
|
+
}.freeze,
|
|
206
|
+
"LiveTranscript" => {
|
|
207
|
+
user: { kind: :str, null: true, required: true },
|
|
208
|
+
agent: { kind: :str, null: true, required: true }
|
|
209
|
+
}.freeze,
|
|
210
|
+
"Gap" => {
|
|
211
|
+
from_seq: { kind: :int, required: true },
|
|
212
|
+
to_seq: { kind: :int, required: true }
|
|
213
|
+
}.freeze,
|
|
214
|
+
"LoggedError" => {
|
|
215
|
+
seq: { kind: :int, required: true },
|
|
216
|
+
code: { kind: :str, required: true },
|
|
217
|
+
message: { kind: :str, required: true }
|
|
218
|
+
}.freeze,
|
|
219
|
+
"Participant" => {
|
|
220
|
+
identity: { kind: :str, required: true },
|
|
221
|
+
kind: { kind: :ref, ref: "ParticipantKind", required: true },
|
|
222
|
+
name: { kind: :str },
|
|
223
|
+
joined_at: { kind: :float, required: true },
|
|
224
|
+
speaking: { kind: :bool, required: true },
|
|
225
|
+
attributes: { kind: :json, required: true }
|
|
226
|
+
}.freeze,
|
|
227
|
+
"Room" => {
|
|
228
|
+
name: { kind: :str, required: true },
|
|
229
|
+
sid: { kind: :str, required: true },
|
|
230
|
+
participants: { kind: :list, items: { kind: :ref, ref: "Participant" }, required: true },
|
|
231
|
+
caller: { kind: :str, null: true, required: true }
|
|
232
|
+
}.freeze,
|
|
233
|
+
"ReceivedEvent" => {
|
|
234
|
+
seq: { kind: :int, required: true },
|
|
235
|
+
name: { kind: :str, required: true },
|
|
236
|
+
source: { kind: :ref, ref: "EventSource", required: true },
|
|
237
|
+
identity: { kind: :str }
|
|
238
|
+
}.freeze,
|
|
239
|
+
"CustomNote" => {
|
|
240
|
+
seq: { kind: :int, required: true },
|
|
241
|
+
name: { kind: :str, required: true },
|
|
242
|
+
data: { kind: :json, required: true }
|
|
243
|
+
}.freeze,
|
|
244
|
+
"State" => {
|
|
245
|
+
seq: { kind: :int, required: true },
|
|
246
|
+
agent: { kind: :str, required: true },
|
|
247
|
+
call: { kind: :str, null: true, required: true },
|
|
248
|
+
status: { kind: :ref, ref: "CallStatus", required: true },
|
|
249
|
+
channel: { kind: :ref, null: true, ref: "Channel", required: true },
|
|
250
|
+
direction: { kind: :ref, null: true, ref: "Direction", required: true },
|
|
251
|
+
from: { kind: :str, null: true, required: true },
|
|
252
|
+
to: { kind: :str, null: true, required: true },
|
|
253
|
+
caller: { kind: :ref, null: true, ref: "Contact", required: true },
|
|
254
|
+
room: { kind: :ref, null: true, ref: "Room", required: true },
|
|
255
|
+
started_at: { kind: :float, null: true, required: true },
|
|
256
|
+
ended_at: { kind: :float, null: true, required: true },
|
|
257
|
+
end_reason: { kind: :ref, null: true, ref: "EndReason", required: true },
|
|
258
|
+
outcome: { kind: :str, null: true, required: true },
|
|
259
|
+
user_state: { kind: :ref, null: true, ref: "UserState", required: true },
|
|
260
|
+
agent_state: { kind: :ref, null: true, ref: "AgentState", required: true },
|
|
261
|
+
live: { kind: :ref, ref: "LiveTranscript", required: true },
|
|
262
|
+
turns: { kind: :list, items: { kind: :ref, ref: "Turn" }, required: true },
|
|
263
|
+
metrics: { kind: :ref, ref: "CollectedMetrics", required: true },
|
|
264
|
+
tools: { kind: :list, items: { kind: :ref, ref: "ToolRun" }, required: true },
|
|
265
|
+
app_state: { kind: :json, required: true },
|
|
266
|
+
events: { kind: :list, items: { kind: :ref, ref: "ReceivedEvent" }, required: true },
|
|
267
|
+
prompt: { kind: :ref, ref: "PromptState", required: true },
|
|
268
|
+
tools_visible: { kind: :list, items: { kind: :str }, required: true },
|
|
269
|
+
confirms: { kind: :list, items: { kind: :ref, ref: "Confirm" }, required: true },
|
|
270
|
+
memory: { kind: :list, items: { kind: :ref, ref: "MemoryOp" }, required: true },
|
|
271
|
+
sources: { kind: :list, items: { kind: :ref, ref: "DocSource" }, required: true },
|
|
272
|
+
handoff: { kind: :ref, ref: "Handoff", required: true },
|
|
273
|
+
held: { kind: :bool, required: true },
|
|
274
|
+
muted: { kind: :bool, required: true },
|
|
275
|
+
transfer: { kind: :ref, null: true, ref: "TransferState", required: true },
|
|
276
|
+
attention: { kind: :ref, null: true, ref: "AttentionState" },
|
|
277
|
+
usage: { kind: :list, items: { kind: :ref, ref: "ModelUsage" }, required: true },
|
|
278
|
+
cost: { kind: :ref, null: true, ref: "Cost", required: true },
|
|
279
|
+
routes: { kind: :list, items: { kind: :ref, ref: "Route" }, required: true },
|
|
280
|
+
gaps: { kind: :list, items: { kind: :ref, ref: "Gap" }, required: true },
|
|
281
|
+
errors: { kind: :list, items: { kind: :ref, ref: "LoggedError" }, required: true },
|
|
282
|
+
custom: { kind: :list, items: { kind: :ref, ref: "CustomNote" }, required: true }
|
|
283
|
+
}.freeze,
|
|
284
|
+
"SayVerb" => {
|
|
285
|
+
verb: { kind: :const, const: "say", required: true },
|
|
286
|
+
text: { kind: :str, required: true }
|
|
287
|
+
}.freeze,
|
|
288
|
+
"WhisperVerb" => {
|
|
289
|
+
verb: { kind: :const, const: "whisper", required: true },
|
|
290
|
+
text: { kind: :str, required: true }
|
|
291
|
+
}.freeze,
|
|
292
|
+
"TakeoverVerb" => {
|
|
293
|
+
verb: { kind: :const, const: "takeover", required: true }
|
|
294
|
+
}.freeze,
|
|
295
|
+
"ReleaseVerb" => {
|
|
296
|
+
verb: { kind: :const, const: "release", required: true }
|
|
297
|
+
}.freeze,
|
|
298
|
+
"TransferVerb" => {
|
|
299
|
+
verb: { kind: :const, const: "transfer", required: true },
|
|
300
|
+
to: { kind: :str, required: true },
|
|
301
|
+
mode: { kind: :ref, ref: "TransferMode" }
|
|
302
|
+
}.freeze,
|
|
303
|
+
"EndVerb" => {
|
|
304
|
+
verb: { kind: :const, const: "end", required: true },
|
|
305
|
+
reason: { kind: :str }
|
|
306
|
+
}.freeze,
|
|
307
|
+
"DevRefusal" => {
|
|
308
|
+
status: { kind: :int, required: true },
|
|
309
|
+
detail: { kind: :str, required: true }
|
|
310
|
+
}.freeze,
|
|
311
|
+
"JudgedBy" => {
|
|
312
|
+
provider: { kind: :str, null: true, required: true },
|
|
313
|
+
model: { kind: :str, null: true, required: true },
|
|
314
|
+
criteria: { kind: :str, required: true }
|
|
315
|
+
}.freeze,
|
|
316
|
+
"Judgment" => {
|
|
317
|
+
name: { kind: :str, required: true },
|
|
318
|
+
verdict: { kind: :ref, ref: "ScoreVerdict", required: true },
|
|
319
|
+
criteria: { kind: :str, required: true },
|
|
320
|
+
reason: { kind: :str, required: true },
|
|
321
|
+
evidence: { kind: :ref, ref: "JudgmentEvidence", required: true }
|
|
322
|
+
}.freeze,
|
|
323
|
+
"JudgmentEvidence" => {
|
|
324
|
+
seqs: { kind: :list, items: { kind: :int }, required: true },
|
|
325
|
+
said: { kind: :str }
|
|
326
|
+
}.freeze,
|
|
327
|
+
"StateCauseTool" => {
|
|
328
|
+
kind: { kind: :const, const: "tool", required: true },
|
|
329
|
+
tool: { kind: :str, required: true },
|
|
330
|
+
call_id: { kind: :str, required: true }
|
|
331
|
+
}.freeze,
|
|
332
|
+
"StateCauseEvent" => {
|
|
333
|
+
kind: { kind: :const, const: "event", required: true },
|
|
334
|
+
name: { kind: :str, required: true },
|
|
335
|
+
seq: { kind: :int, required: true }
|
|
336
|
+
}.freeze,
|
|
337
|
+
}.freeze
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The state an empty log starts from, and the parts a fold writes into it.
|
|
4
|
+
|
|
5
|
+
module Pinecall
|
|
6
|
+
module Wire
|
|
7
|
+
# The state a log reduces to.
|
|
8
|
+
module State
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# The state of an empty log. Identical in all three languages, so one log reduces the same.
|
|
12
|
+
def initial
|
|
13
|
+
{
|
|
14
|
+
seq: 0,
|
|
15
|
+
agent: "",
|
|
16
|
+
call: nil,
|
|
17
|
+
status: "idle",
|
|
18
|
+
channel: nil,
|
|
19
|
+
direction: nil,
|
|
20
|
+
from: nil,
|
|
21
|
+
to: nil,
|
|
22
|
+
caller: nil,
|
|
23
|
+
room: nil,
|
|
24
|
+
started_at: nil,
|
|
25
|
+
ended_at: nil,
|
|
26
|
+
end_reason: nil,
|
|
27
|
+
outcome: nil,
|
|
28
|
+
user_state: nil,
|
|
29
|
+
agent_state: nil,
|
|
30
|
+
live: { user: nil, agent: nil },
|
|
31
|
+
turns: [],
|
|
32
|
+
metrics: { llm: [], stt: [], tts: [], vad: [], eou: [], eot: [], interruption: [], realtime: [], avatar: [] },
|
|
33
|
+
tools: [],
|
|
34
|
+
app_state: {},
|
|
35
|
+
events: [],
|
|
36
|
+
prompt: {},
|
|
37
|
+
tools_visible: [],
|
|
38
|
+
confirms: [],
|
|
39
|
+
memory: [],
|
|
40
|
+
sources: [],
|
|
41
|
+
handoff: { active: false, by: nil },
|
|
42
|
+
held: false,
|
|
43
|
+
muted: false,
|
|
44
|
+
transfer: nil,
|
|
45
|
+
attention: nil,
|
|
46
|
+
usage: [],
|
|
47
|
+
cost: nil,
|
|
48
|
+
routes: [],
|
|
49
|
+
gaps: [],
|
|
50
|
+
errors: [],
|
|
51
|
+
custom: []
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Checks a value against a named shape of the table, field by field, refusing a key nobody declared.
|
|
4
|
+
|
|
5
|
+
module Pinecall
|
|
6
|
+
module Wire
|
|
7
|
+
# Validates a payload against the generated shape tables and names the offending field.
|
|
8
|
+
# The Ruby counterpart of zod and pydantic; a table walk avoids a schema-library dependency.
|
|
9
|
+
module Validate
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Validate a payload against the named shape and return it. Shapes are closed: an undeclared
|
|
13
|
+
# key (e.g. from a newer protocol) is refused rather than silently dropped.
|
|
14
|
+
def call!(name, value, where: name)
|
|
15
|
+
return union!(name, value, where) if Shapes::UNIONS.key?(name)
|
|
16
|
+
return map!(Shapes::MAPS.fetch(name), value, where) if Shapes::MAPS.key?(name)
|
|
17
|
+
|
|
18
|
+
shape = Shapes::SHAPES[name]
|
|
19
|
+
raise WireError, "#{where}: the protocol declares no shape called #{name}" if shape.nil?
|
|
20
|
+
raise WireError, "#{where}: expected the fields of #{name}, got #{named(value)}" unless value.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
unknown = value.keys - shape.keys
|
|
23
|
+
raise WireError, "#{where}: #{name} has no field called #{unknown.join(", ")}" unless unknown.empty?
|
|
24
|
+
|
|
25
|
+
shape.each { |field, spec| field_of!(shape: name, spec:, value:, field:, where:) }
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# One field. An optional one may be absent or null, as TypeScript's nullish reads it: the
|
|
30
|
+
# runtime writes null for a field it set to nothing.
|
|
31
|
+
def field_of!(shape:, spec:, value:, field:, where:)
|
|
32
|
+
return if !spec[:required] && value[field].nil?
|
|
33
|
+
raise WireError, "#{where}.#{field}: #{shape} requires it" unless value.key?(field)
|
|
34
|
+
|
|
35
|
+
field!(spec, value[field], "#{where}.#{field}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# One value against a field descriptor; one branch per kind the generator emits.
|
|
39
|
+
def field!(spec, value, where)
|
|
40
|
+
if value.nil?
|
|
41
|
+
return if spec[:null]
|
|
42
|
+
|
|
43
|
+
raise WireError, "#{where}: nothing is not one of the things it may be"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
case spec[:kind]
|
|
47
|
+
when :str then string!(spec, value, where)
|
|
48
|
+
when :int then expect(value, Integer, where, "a whole number")
|
|
49
|
+
when :float then expect(value, Numeric, where, "a number")
|
|
50
|
+
when :bool then expect_boolean(value, where)
|
|
51
|
+
when :json then expect(value, Hash, where, "an object")
|
|
52
|
+
when :any then value
|
|
53
|
+
when :list then list!(spec, value, where)
|
|
54
|
+
when :tuple then tuple!(spec, value, where)
|
|
55
|
+
when :map then map!(spec[:items], value, where)
|
|
56
|
+
when :enum then one_of!(spec[:values], value, where)
|
|
57
|
+
when :const then const!(spec[:const], value, where)
|
|
58
|
+
when :ref then ref!(spec[:ref], value, where)
|
|
59
|
+
else raise WireError, "#{where}: the generator emitted a kind nobody reads: #{spec[:kind]}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A union is told apart by the one field the schema's discriminator names.
|
|
64
|
+
def union!(name, value, where)
|
|
65
|
+
union = Shapes::UNIONS.fetch(name)
|
|
66
|
+
on = union[:on]
|
|
67
|
+
said = value.is_a?(Hash) ? value[on] : nil
|
|
68
|
+
member = union[:members].find { |one| Shapes::SHAPES.dig(one, on, :const) == said }
|
|
69
|
+
if member.nil?
|
|
70
|
+
allowed = union[:members].filter_map { |one| Shapes::SHAPES.dig(one, on, :const) }
|
|
71
|
+
raise WireError, "#{where}: #{on} #{said.inspect} is not one of #{allowed.join(", ")}"
|
|
72
|
+
end
|
|
73
|
+
call!(member, value, where:)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A reference points at another object, at a union, or at one of the closed lists.
|
|
77
|
+
def ref!(name, value, where)
|
|
78
|
+
allowed = Enums::ALL[name]
|
|
79
|
+
return one_of!(allowed, value, where) unless allowed.nil?
|
|
80
|
+
|
|
81
|
+
call!(name, value, where:)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def list!(spec, value, where)
|
|
85
|
+
expect(value, Array, where, "a list")
|
|
86
|
+
value.each_with_index { |item, at| field!(spec[:items], item, "#{where}[#{at}]") }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A tuple has a fixed length and one shape per position, e.g. a transcript line [who, what].
|
|
90
|
+
def tuple!(spec, value, where)
|
|
91
|
+
expect(value, Array, where, "a list")
|
|
92
|
+
places = spec[:members]
|
|
93
|
+
unless value.length == places.length
|
|
94
|
+
raise WireError, "#{where}: a list of #{places.length} things, not #{value.length}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
places.each_with_index { |place, at| field!(place, value[at], "#{where}[#{at}]") }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Map keys are app-chosen names; every value has the same shape.
|
|
101
|
+
def map!(spec, value, where)
|
|
102
|
+
expect(value, Hash, where, "an object")
|
|
103
|
+
value.each { |key, item| field!(spec, item, "#{where}.#{key}") }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# JSON Schema's ^ and $ anchor the whole string but Ruby's anchor a line, so a value with a
|
|
107
|
+
# newline is refused before matching.
|
|
108
|
+
def string!(spec, value, where)
|
|
109
|
+
expect(value, String, where, "a string")
|
|
110
|
+
pattern = spec[:pattern]
|
|
111
|
+
return value if pattern.nil? || (!value.include?("\n") && Regexp.new(pattern).match?(value))
|
|
112
|
+
|
|
113
|
+
raise WireError, "#{where}: #{value.inspect} does not match #{pattern}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def one_of!(values, value, where)
|
|
117
|
+
return value if values.include?(value)
|
|
118
|
+
|
|
119
|
+
raise WireError, "#{where}: #{value.inspect} is not one of #{values.join(", ")}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def const!(const, value, where)
|
|
123
|
+
return value if value == const
|
|
124
|
+
|
|
125
|
+
raise WireError, "#{where}: this field is always #{const.inspect}, never #{value.inspect}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def expect(value, type, where, called)
|
|
129
|
+
return value if value.is_a?(type)
|
|
130
|
+
|
|
131
|
+
raise WireError, "#{where}: expected #{called}, got #{named(value)}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def expect_boolean(value, where)
|
|
135
|
+
return value if value == true || value == false
|
|
136
|
+
|
|
137
|
+
raise WireError, "#{where}: expected true or false, got #{named(value)}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Describe a value's type in the words the error messages use.
|
|
141
|
+
def named(value)
|
|
142
|
+
case value
|
|
143
|
+
when nil then "nothing"
|
|
144
|
+
when String then "a string"
|
|
145
|
+
when Integer, Float then "a number"
|
|
146
|
+
when true, false then "true or false"
|
|
147
|
+
when Array then "a list"
|
|
148
|
+
when Hash then "an object"
|
|
149
|
+
else value.class.name
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The runtime's wire as this gem speaks it: the shapes, a validator, the codec and the reducer.
|
|
4
|
+
|
|
5
|
+
require "json"
|
|
6
|
+
require "set"
|
|
7
|
+
|
|
8
|
+
require_relative "wire/errors"
|
|
9
|
+
require_relative "wire/enums"
|
|
10
|
+
require_relative "wire/shapes"
|
|
11
|
+
require_relative "wire/registry"
|
|
12
|
+
require_relative "wire/validate"
|
|
13
|
+
require_relative "wire/codec"
|
|
14
|
+
require_relative "wire/state"
|
|
15
|
+
require_relative "wire/reduce"
|
|
16
|
+
|
|
17
|
+
module Pinecall
|
|
18
|
+
# The wire between gateway and app: events the gateway writes, commands the app sends.
|
|
19
|
+
module Wire
|
|
20
|
+
class << self
|
|
21
|
+
# One log line from parsed JSON.
|
|
22
|
+
def decode_entry(raw) = Codec.decode_entry(raw)
|
|
23
|
+
|
|
24
|
+
# The entry's data as the shape its type names.
|
|
25
|
+
def event_of(entry) = Codec.event_of(entry)
|
|
26
|
+
|
|
27
|
+
# One command frame, checked against the shape its type names.
|
|
28
|
+
def command(...) = Codec.command(...)
|
|
29
|
+
|
|
30
|
+
# A frame as JSON text.
|
|
31
|
+
def encode(frame) = Codec.encode(frame)
|
|
32
|
+
|
|
33
|
+
# Fold a log into the state it means.
|
|
34
|
+
def reduce(entries) = Reduce.reduce(entries)
|
|
35
|
+
|
|
36
|
+
# One entry folded into a state, for a reader following a log as it happens.
|
|
37
|
+
def apply(state, entry) = Reduce.apply(state, entry)
|
|
38
|
+
|
|
39
|
+
# The state of an empty log.
|
|
40
|
+
def initial_state = State.initial
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/pinecall.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pinecall's Ruby SDK: write an agent as a class and serve its calls over the gateway socket.
|
|
4
|
+
#
|
|
5
|
+
# `Pinecall::Agent` and `Pinecall.mount` are the framework; `Pinecall::Client` is the bare socket
|
|
6
|
+
# for applications that decide their own answers.
|
|
7
|
+
|
|
8
|
+
require "json"
|
|
9
|
+
require "pinecall/wire"
|
|
10
|
+
|
|
11
|
+
require_relative "pinecall/version"
|
|
12
|
+
require_relative "pinecall/errors"
|
|
13
|
+
require_relative "pinecall/reading"
|
|
14
|
+
require_relative "pinecall/view"
|
|
15
|
+
require_relative "pinecall/rules"
|
|
16
|
+
require_relative "pinecall/blocks"
|
|
17
|
+
require_relative "pinecall/panel"
|
|
18
|
+
require_relative "pinecall/agent"
|
|
19
|
+
require_relative "pinecall/call_world"
|
|
20
|
+
require_relative "pinecall/client"
|
|
21
|
+
require_relative "pinecall/bridge"
|
|
22
|
+
require_relative "pinecall/serve"
|
|
23
|
+
|
|
24
|
+
module Pinecall
|
|
25
|
+
class << self
|
|
26
|
+
# Mount an agent class on a client; each call gets its own instance. Nothing is sent until
|
|
27
|
+
# `connect`.
|
|
28
|
+
def mount(klass, client:, **options) = Bridge.mount(klass, client:, **options)
|
|
29
|
+
|
|
30
|
+
# Render the agent's prompt blocks in send order, offline.
|
|
31
|
+
def render(agent, **context) = Prompt.render(agent, **context)
|
|
32
|
+
|
|
33
|
+
# Render the prompt as one page, each block under its header.
|
|
34
|
+
def show_prompt(agent, **context) = Prompt.show(agent, **context)
|
|
35
|
+
|
|
36
|
+
# Serve an agent class from inside your own Ruby process: mounted on a client of its own (or
|
|
37
|
+
# the one given), connected, and held. `held.stop` drains then closes, for an `at_exit`.
|
|
38
|
+
def serve(klass, client: nil, url: nil, api_key: nil, env: nil, slug: nil, last: nil)
|
|
39
|
+
client ||= Client.new(url:, api_key:, env:)
|
|
40
|
+
mounted = mount(klass, client:, slug: slug || klass.slug, last:)
|
|
41
|
+
client.connect
|
|
42
|
+
Serve::Held.new(client, [mounted])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|