pinecall-runtime 0.1.6__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.
Files changed (328) hide show
  1. pinecall/__init__.py +0 -0
  2. pinecall/channels/__init__.py +0 -0
  3. pinecall/channels/_chooser.py +45 -0
  4. pinecall/channels/offers.py +219 -0
  5. pinecall/channels/rooms.py +176 -0
  6. pinecall/channels/routes.py +275 -0
  7. pinecall/channels/telephony/__init__.py +0 -0
  8. pinecall/channels/telephony/_twilio.py +349 -0
  9. pinecall/channels/telephony/carrier.py +229 -0
  10. pinecall/channels/telephony/carrier_catalog.py +92 -0
  11. pinecall/channels/telephony/carriers.csv +41 -0
  12. pinecall/channels/telephony/dialing.py +419 -0
  13. pinecall/channels/telephony/firewall.py +58 -0
  14. pinecall/channels/telephony/hand_over.py +78 -0
  15. pinecall/channels/telephony/number_path.py +176 -0
  16. pinecall/channels/telephony/numbers.py +530 -0
  17. pinecall/channels/telephony/origination.py +77 -0
  18. pinecall/channels/telephony/sip.py +448 -0
  19. pinecall/channels/whatsapp.py +413 -0
  20. pinecall/cli/__init__.py +0 -0
  21. pinecall/cli/_archive.py +85 -0
  22. pinecall/cli/_drift.py +38 -0
  23. pinecall/cli/_facts.py +67 -0
  24. pinecall/cli/_load.py +433 -0
  25. pinecall/cli/_operator.py +626 -0
  26. pinecall/cli/_sessions.py +164 -0
  27. pinecall/cli/_traceback.py +95 -0
  28. pinecall/cli/_usage.py +34 -0
  29. pinecall/cli/main.py +521 -0
  30. pinecall/domain/__init__.py +0 -0
  31. pinecall/domain/agent.py +446 -0
  32. pinecall/domain/call.py +152 -0
  33. pinecall/domain/country_codes.py +32 -0
  34. pinecall/domain/errors.py +151 -0
  35. pinecall/domain/names.py +115 -0
  36. pinecall/domain/org.py +120 -0
  37. pinecall/domain/person.py +208 -0
  38. pinecall/domain/scope.py +21 -0
  39. pinecall/evals/__init__.py +0 -0
  40. pinecall/evals/_evidence.py +158 -0
  41. pinecall/evals/calibration.py +112 -0
  42. pinecall/evals/callers.py +160 -0
  43. pinecall/evals/case.py +327 -0
  44. pinecall/evals/checks.py +437 -0
  45. pinecall/evals/compliance.py +197 -0
  46. pinecall/evals/dataset.py +451 -0
  47. pinecall/evals/goldens.py +331 -0
  48. pinecall/evals/judges.py +525 -0
  49. pinecall/evals/runs.py +252 -0
  50. pinecall/evals/spoken.py +356 -0
  51. pinecall/fleet/__init__.py +0 -0
  52. pinecall/fleet/client.py +569 -0
  53. pinecall/fleet/demand.py +51 -0
  54. pinecall/fleet/heartbeat.py +194 -0
  55. pinecall/fleet/measures.py +157 -0
  56. pinecall/fleet/roster.py +232 -0
  57. pinecall/fleet/worlds.py +44 -0
  58. pinecall/gateway/__init__.py +0 -0
  59. pinecall/gateway/_call_setup.py +53 -0
  60. pinecall/gateway/_deps.py +556 -0
  61. pinecall/gateway/_gateway.py +75 -0
  62. pinecall/gateway/_served.py +396 -0
  63. pinecall/gateway/_sockets.py +444 -0
  64. pinecall/gateway/_streams.py +83 -0
  65. pinecall/gateway/_text_calls.py +182 -0
  66. pinecall/gateway/api/__init__.py +0 -0
  67. pinecall/gateway/api/accounts.py +324 -0
  68. pinecall/gateway/api/agents.py +150 -0
  69. pinecall/gateway/api/apps.py +478 -0
  70. pinecall/gateway/api/box.py +658 -0
  71. pinecall/gateway/api/calibration.py +58 -0
  72. pinecall/gateway/api/callbacks.py +105 -0
  73. pinecall/gateway/api/calls.py +655 -0
  74. pinecall/gateway/api/chat.py +223 -0
  75. pinecall/gateway/api/dataset.py +163 -0
  76. pinecall/gateway/api/desk.py +113 -0
  77. pinecall/gateway/api/entries.py +77 -0
  78. pinecall/gateway/api/evals.py +647 -0
  79. pinecall/gateway/api/fleet.py +73 -0
  80. pinecall/gateway/api/hosting.py +241 -0
  81. pinecall/gateway/api/judges.py +97 -0
  82. pinecall/gateway/api/keys.py +110 -0
  83. pinecall/gateway/api/line.py +142 -0
  84. pinecall/gateway/api/members.py +197 -0
  85. pinecall/gateway/api/metrics.py +226 -0
  86. pinecall/gateway/api/numbers.py +476 -0
  87. pinecall/gateway/api/ops.py +428 -0
  88. pinecall/gateway/api/org.py +283 -0
  89. pinecall/gateway/api/personas.py +157 -0
  90. pinecall/gateway/api/pipeline.py +206 -0
  91. pinecall/gateway/api/prompts.py +50 -0
  92. pinecall/gateway/api/providers.py +240 -0
  93. pinecall/gateway/api/recordings.py +89 -0
  94. pinecall/gateway/api/relay.py +169 -0
  95. pinecall/gateway/api/retrieval.py +403 -0
  96. pinecall/gateway/api/runner.py +109 -0
  97. pinecall/gateway/api/sessions.py +102 -0
  98. pinecall/gateway/api/settings.py +463 -0
  99. pinecall/gateway/api/signup.py +164 -0
  100. pinecall/gateway/api/sso_login.py +168 -0
  101. pinecall/gateway/api/threads.py +187 -0
  102. pinecall/gateway/api/usage.py +303 -0
  103. pinecall/gateway/api/visitors.py +222 -0
  104. pinecall/gateway/api/whatsapp.py +48 -0
  105. pinecall/gateway/api/widget.py +49 -0
  106. pinecall/gateway/app.py +504 -0
  107. pinecall/gateway/calls/__init__.py +0 -0
  108. pinecall/gateway/calls/binding.py +63 -0
  109. pinecall/gateway/calls/commands.py +18 -0
  110. pinecall/gateway/calls/inbox.py +100 -0
  111. pinecall/gateway/calls/known.py +23 -0
  112. pinecall/gateway/calls/owners.py +87 -0
  113. pinecall/gateway/calls/pump.py +33 -0
  114. pinecall/gateway/calls/serving.py +118 -0
  115. pinecall/gateway/calls/threads.py +367 -0
  116. pinecall/gateway/dispatching/__init__.py +0 -0
  117. pinecall/gateway/dispatching/arrivals.py +58 -0
  118. pinecall/gateway/dispatching/sweep.py +43 -0
  119. pinecall/gateway/ending/__init__.py +0 -0
  120. pinecall/gateway/ending/reaper.py +160 -0
  121. pinecall/gateway/ending/seal.py +312 -0
  122. pinecall/gateway/ending/stranded.py +117 -0
  123. pinecall/log/__init__.py +0 -0
  124. pinecall/log/_histogram.py +74 -0
  125. pinecall/log/_relay.py +259 -0
  126. pinecall/log/_writer.py +454 -0
  127. pinecall/log/days.py +154 -0
  128. pinecall/log/drift.py +550 -0
  129. pinecall/log/facts.py +358 -0
  130. pinecall/log/inbox.py +169 -0
  131. pinecall/log/lists.py +155 -0
  132. pinecall/log/logs.py +581 -0
  133. pinecall/log/openings.py +71 -0
  134. pinecall/log/private.py +135 -0
  135. pinecall/log/queries.py +229 -0
  136. pinecall/log/readers.py +207 -0
  137. pinecall/log/reduce.py +659 -0
  138. pinecall/log/refold.py +217 -0
  139. pinecall/log/store.py +370 -0
  140. pinecall/log/usage.py +242 -0
  141. pinecall/postgres/__init__.py +0 -0
  142. pinecall/postgres/migrate.py +202 -0
  143. pinecall/postgres/migrations/0001_schema.sql +591 -0
  144. pinecall/postgres/migrations/0002_worlds.sql +15 -0
  145. pinecall/postgres/migrations/0003_lent.sql +4 -0
  146. pinecall/postgres/migrations/0004_carriers.sql +23 -0
  147. pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
  148. pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
  149. pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
  150. pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
  151. pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
  152. pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
  153. pinecall/postgres/migrations/0011_erasure.sql +32 -0
  154. pinecall/postgres/migrations/0012_org_policy.sql +10 -0
  155. pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
  156. pinecall/postgres/migrations/0014_consents.sql +24 -0
  157. pinecall/postgres/migrations/0015_disclosure.sql +7 -0
  158. pinecall/postgres/migrations/0016_call_records.sql +21 -0
  159. pinecall/postgres/migrations/0017_reads.sql +17 -0
  160. pinecall/postgres/migrations/0018_hosting.sql +41 -0
  161. pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
  162. pinecall/postgres/migrations/0020_written_once.sql +7 -0
  163. pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
  164. pinecall/postgres/migrations/0022_sealed.sql +39 -0
  165. pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
  166. pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
  167. pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
  168. pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
  169. pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
  170. pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
  171. pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
  172. pinecall/postgres/migrations/0060_drift.sql +62 -0
  173. pinecall/postgres/migrations/0061_canaries.sql +24 -0
  174. pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
  175. pinecall/postgres/migrations/0063_prompts.sql +16 -0
  176. pinecall/postgres/migrations/0075_call_private.sql +14 -0
  177. pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
  178. pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
  179. pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
  180. pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
  181. pinecall/postgres/migrations/0080_call_openings.sql +20 -0
  182. pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
  183. pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
  184. pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
  185. pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
  186. pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
  187. pinecall/postgres/migrations/0086_route_origin.sql +17 -0
  188. pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
  189. pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
  190. pinecall/postgres/migrations/0089_offers.sql +21 -0
  191. pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
  192. pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
  193. pinecall/postgres/migrations/0092_key_parent.sql +6 -0
  194. pinecall/postgres/migrations/0093_code_tries.sql +10 -0
  195. pinecall/postgres/migrations/0094_route_approval.sql +12 -0
  196. pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
  197. pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
  198. pinecall/postgres/migrations/0097_case_status.sql +19 -0
  199. pinecall/postgres/pool.py +190 -0
  200. pinecall/process/__init__.py +0 -0
  201. pinecall/process/_objects.py +97 -0
  202. pinecall/process/box_settings.py +23 -0
  203. pinecall/process/connections.py +124 -0
  204. pinecall/process/metrics.py +127 -0
  205. pinecall/process/recordings.py +323 -0
  206. pinecall/process/resolver.py +41 -0
  207. pinecall/process/sealed_audio.py +173 -0
  208. pinecall/process/settings.py +534 -0
  209. pinecall/process/shared.py +158 -0
  210. pinecall/process/signal.py +416 -0
  211. pinecall/providers/__init__.py +0 -0
  212. pinecall/providers/build.py +570 -0
  213. pinecall/providers/catalog.py +266 -0
  214. pinecall/providers/credentials.py +230 -0
  215. pinecall/providers/declared.py +69 -0
  216. pinecall/providers/prices.py +240 -0
  217. pinecall/providers/tried.py +104 -0
  218. pinecall/providers/voices.py +174 -0
  219. pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
  220. pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
  221. pinecall/public/console/favicon.png +0 -0
  222. pinecall/public/console/index.html +20 -0
  223. pinecall/public/console/logo-mark.png +0 -0
  224. pinecall/public/console/pinecall-logo-dark.png +0 -0
  225. pinecall/public/console/pinecall-logo.png +0 -0
  226. pinecall/public/console/pinecall-mark.png +0 -0
  227. pinecall/public/console/sw.js +37 -0
  228. pinecall/public/widget/pinecall-widget.js +853 -0
  229. pinecall/py.typed +0 -0
  230. pinecall/retrieval/__init__.py +0 -0
  231. pinecall/retrieval/_search.py +186 -0
  232. pinecall/retrieval/_sections.py +70 -0
  233. pinecall/retrieval/embed.py +340 -0
  234. pinecall/retrieval/extraction.py +403 -0
  235. pinecall/retrieval/knowledge.py +659 -0
  236. pinecall/retrieval/lookups.py +192 -0
  237. pinecall/retrieval/memory.py +603 -0
  238. pinecall/runner/__init__.py +0 -0
  239. pinecall/runner/_kube.py +459 -0
  240. pinecall/runner/_plan.py +157 -0
  241. pinecall/runner/main.py +306 -0
  242. pinecall/session/__init__.py +0 -0
  243. pinecall/session/_agent.py +141 -0
  244. pinecall/session/_hearing.py +101 -0
  245. pinecall/session/_livekit.py +195 -0
  246. pinecall/session/_prompt.py +147 -0
  247. pinecall/session/_recording.py +237 -0
  248. pinecall/session/call.py +367 -0
  249. pinecall/session/clock.py +40 -0
  250. pinecall/session/hold.py +239 -0
  251. pinecall/session/room.py +626 -0
  252. pinecall/session/session.py +690 -0
  253. pinecall/session/text.py +144 -0
  254. pinecall/session/tools.py +483 -0
  255. pinecall/session/voice.py +118 -0
  256. pinecall/session/widget.py +201 -0
  257. pinecall/tenancy/__init__.py +0 -0
  258. pinecall/tenancy/admission.py +204 -0
  259. pinecall/tenancy/agents.py +164 -0
  260. pinecall/tenancy/canary.py +126 -0
  261. pinecall/tenancy/carrier_networks.py +156 -0
  262. pinecall/tenancy/carriers.py +265 -0
  263. pinecall/tenancy/codes.py +196 -0
  264. pinecall/tenancy/consents.py +167 -0
  265. pinecall/tenancy/dial_policy.py +405 -0
  266. pinecall/tenancy/disclosure.py +41 -0
  267. pinecall/tenancy/erasure.py +204 -0
  268. pinecall/tenancy/export.py +167 -0
  269. pinecall/tenancy/hosted_running.py +166 -0
  270. pinecall/tenancy/hosting.py +475 -0
  271. pinecall/tenancy/judges.py +126 -0
  272. pinecall/tenancy/keys.py +467 -0
  273. pinecall/tenancy/knocks.py +40 -0
  274. pinecall/tenancy/letters.py +378 -0
  275. pinecall/tenancy/mail.py +458 -0
  276. pinecall/tenancy/org_secrets.py +123 -0
  277. pinecall/tenancy/orgs.py +78 -0
  278. pinecall/tenancy/people.py +543 -0
  279. pinecall/tenancy/personas.py +145 -0
  280. pinecall/tenancy/policy.py +65 -0
  281. pinecall/tenancy/prompts.py +48 -0
  282. pinecall/tenancy/reads.py +73 -0
  283. pinecall/tenancy/recording_keys.py +41 -0
  284. pinecall/tenancy/remembered.py +148 -0
  285. pinecall/tenancy/retention.py +86 -0
  286. pinecall/tenancy/scopes.py +406 -0
  287. pinecall/tenancy/signin.py +622 -0
  288. pinecall/tenancy/spend.py +69 -0
  289. pinecall/tenancy/sso.py +431 -0
  290. pinecall/tenancy/sso_domains.py +132 -0
  291. pinecall/tenancy/throttle.py +88 -0
  292. pinecall/tenancy/tokens.py +400 -0
  293. pinecall/tenancy/traceback.py +70 -0
  294. pinecall/tenancy/usage.py +136 -0
  295. pinecall/tenancy/vault.py +204 -0
  296. pinecall/tenancy/words.py +144 -0
  297. pinecall/wire/__init__.py +0 -0
  298. pinecall/wire/commands.py +350 -0
  299. pinecall/wire/events.py +655 -0
  300. pinecall/wire/frames.py +65 -0
  301. pinecall/wire/metrics.py +287 -0
  302. pinecall/wire/parts.py +320 -0
  303. pinecall/wire/rest/__init__.py +0 -0
  304. pinecall/wire/rest/accounts.py +512 -0
  305. pinecall/wire/rest/agents.py +172 -0
  306. pinecall/wire/rest/calls.py +397 -0
  307. pinecall/wire/rest/evals.py +453 -0
  308. pinecall/wire/rest/fleet.py +69 -0
  309. pinecall/wire/rest/hosting.py +174 -0
  310. pinecall/wire/rest/numbers.py +280 -0
  311. pinecall/wire/rest/ops.py +342 -0
  312. pinecall/wire/rest/providers.py +83 -0
  313. pinecall/wire/rest/retrieval.py +301 -0
  314. pinecall/wire/rest/settings.py +184 -0
  315. pinecall/wire/rest/usage.py +245 -0
  316. pinecall/wire/room.py +61 -0
  317. pinecall/wire/scores.py +49 -0
  318. pinecall/wire/state.py +243 -0
  319. pinecall/worker/__init__.py +0 -0
  320. pinecall/worker/_job.py +655 -0
  321. pinecall/worker/_recorder.py +82 -0
  322. pinecall/worker/_traces.py +48 -0
  323. pinecall/worker/main.py +307 -0
  324. pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
  325. pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
  326. pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
  327. pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
  328. pinecall_runtime-0.1.6.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,151 @@
1
+ """The one error hierarchy: every error the runtime raises on purpose, with its HTTP status."""
2
+
3
+
4
+ class PinecallError(Exception):
5
+ """Base of every deliberate error; the message is the sentence a person reads."""
6
+
7
+ status = 500
8
+
9
+
10
+ class DeclarationRefused(PinecallError):
11
+ """A value broke a contract; the message states the rule."""
12
+
13
+ status = 400
14
+
15
+
16
+ class NotSignedIn(PinecallError):
17
+ """No key, a wrong one, or wrong credentials."""
18
+
19
+ status = 401
20
+
21
+
22
+ class NotAllowed(PinecallError):
23
+ """A key that opens something, but not this."""
24
+
25
+ status = 403
26
+
27
+
28
+ class NotFound(PinecallError):
29
+ """Nothing by that name in the caller's scope."""
30
+
31
+ status = 404
32
+
33
+
34
+ class Conflict(PinecallError):
35
+ """A valid request against stored state that contradicts it."""
36
+
37
+ status = 409
38
+
39
+
40
+ class QuotaExhausted(PinecallError):
41
+ """A limit of the org is reached: which one, what was used and the limit, when it is one."""
42
+
43
+ status = 429
44
+
45
+ def __init__(
46
+ self, sentence: str, *, quota: str | None = None, used: float = 0, limit: int = 0
47
+ ) -> None:
48
+ """The refusal, and the numbers the door writes into the agent's log."""
49
+ super().__init__(sentence)
50
+ self.quota = quota
51
+ self.used = used
52
+ self.limit = limit
53
+
54
+
55
+ class TooLarge(PinecallError):
56
+ """A body over what the door takes."""
57
+
58
+ status = 413
59
+
60
+
61
+ class TooManyRequests(PinecallError):
62
+ """The same name knocked too often in the last minute: a password, a code, a sign-up."""
63
+
64
+ status = 429
65
+
66
+
67
+ class Throttled(TooManyRequests):
68
+ """An org's family of doors took its minute's requests; the answer says when to come back."""
69
+
70
+ def __init__(self, sentence: str, *, retry_after_s: int) -> None:
71
+ """The refusal, and the whole seconds until the minute turns."""
72
+ super().__init__(sentence)
73
+ self.retry_after_s = retry_after_s
74
+
75
+
76
+ class UpstreamFailed(PinecallError):
77
+ """A vendor, a carrier or an identity provider did not answer as it should."""
78
+
79
+ status = 502
80
+
81
+
82
+ class GatewayRefused(UpstreamFailed):
83
+ """The worker's gateway did not answer, or answered with a refusal; `answered` is its status."""
84
+
85
+ def __init__(self, sentence: str, *, answered: int | None = None) -> None:
86
+ """The refusal, and the status the gateway answered with, None when it was unreachable."""
87
+ super().__init__(sentence)
88
+ self.answered = answered
89
+
90
+
91
+ class AppRefused(PinecallError):
92
+ """A tenant's app refused what a console asked of it; its own status is the answer's."""
93
+
94
+ def __init__(self, answered: int, sentence: str) -> None:
95
+ """The app's status and its sentence, passed on as they came."""
96
+ super().__init__(sentence)
97
+ self.status = answered
98
+
99
+
100
+ class NotAvailable(PinecallError):
101
+ """This box lacks what the request needs: a key, the vault, an embedder, a domain."""
102
+
103
+ status = 503
104
+
105
+
106
+ class EmbedderUnreachable(PinecallError):
107
+ """The box's embedder did not answer, or answered something that is not its vectors."""
108
+
109
+ status = 503
110
+
111
+ # The sentence reaches a tenant (a lookup's error on its call's log): it names the vendor and
112
+ # never where the box asked it, which is the operator's to read in the process's log.
113
+ def __init__(self, vendor: str, url: str, words: str) -> None:
114
+ """The embedder, where it was asked, and what it said or why it said nothing."""
115
+ super().__init__(f"{vendor} did not answer: {words}")
116
+ self.url = url
117
+ self.words = words
118
+
119
+
120
+ class WrongWidth(PinecallError):
121
+ """The embedder answered vectors of a width the vector columns do not hold."""
122
+
123
+ status = 503
124
+
125
+
126
+ # Two models' vectors can share a width and still not be comparable.
127
+ class WrongModel(PinecallError):
128
+ """A base's vectors were written by another model than the one this box embeds with."""
129
+
130
+ status = 503
131
+
132
+ def __init__(self, base: str, pushed: str, embeds: str) -> None:
133
+ """The base, the model that wrote it, and the model the box embeds with now."""
134
+ super().__init__(
135
+ f"base {base} was pushed with {pushed}; this box embeds with {embeds}: "
136
+ "push it again with `pinecall docs push`"
137
+ )
138
+
139
+
140
+ class SettingsRefused(PinecallError):
141
+ """The process cannot start with the environment it was given."""
142
+
143
+
144
+ class StoreUnreachable(PinecallError):
145
+ """The database cannot be reached or is not usable."""
146
+
147
+ status = 503
148
+
149
+
150
+ class MigrationsRefused(PinecallError):
151
+ """The database and the shipped migrations disagree."""
@@ -0,0 +1,115 @@
1
+ """The words every part of the runtime speaks: worlds, channels, slugs, numbers, key scopes."""
2
+
3
+ import re
4
+ from typing import Literal
5
+
6
+ from pinecall.domain.errors import DeclarationRefused
7
+
8
+ # The distribution on PyPI; the import stays `pinecall`. PyPI `pinecall` is the Python SDK.
9
+ DISTRIBUTION = "pinecall-runtime"
10
+
11
+ type Json = str | int | float | bool | list[Json] | dict[str, Json] | None
12
+
13
+
14
+ type JsonObject = dict[str, Json]
15
+
16
+
17
+ # A lone secret, or the constructor's own keyword arguments (Azure's key and region, Google's
18
+ # service account, LiveKit's key pair).
19
+ type Credentials = str | JsonObject
20
+
21
+
22
+ # A key belongs to one environment; its agents, numbers and calls are isolated to it. A shared
23
+ # "staging" is a sandbox agent registered by a machine key (no holder), not a third environment.
24
+ type Env = Literal["production", "sandbox"]
25
+
26
+
27
+ # phone: SIP into a LiveKit room; web: the widget over WebRTC; whatsapp: text.
28
+ type Channel = Literal["phone", "web", "whatsapp"]
29
+
30
+
31
+ # How the call is had: spoken (a room, a voice) or written (a text session); `web` is either.
32
+ type Medium = Literal["voice", "text"]
33
+
34
+
35
+ type Direction = Literal["inbound", "outbound"]
36
+
37
+
38
+ # How a number's row was written: bought by the box, imported from an account of the org, hooked
39
+ # by the org from its own carrier, or typed by the box's operator.
40
+ type RouteOrigin = Literal["bought", "imported", "hooked", "typed"]
41
+
42
+
43
+ # What one step of a call's way to an agent does now: works, waits on someone, or stops the call.
44
+ type StepState = Literal["ok", "waiting", "broken"]
45
+
46
+
47
+ # e.g. clinica-norte: an agent's slug, and an org's.
48
+ A_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
49
+
50
+
51
+ _E164 = re.compile(r"^\+[1-9]\d{1,14}$")
52
+
53
+
54
+ # Checked on every address before a letter is sent: whitespace would allow header injection
55
+ # (RFC 5322 §2.2).
56
+ AN_ADDRESS = re.compile(r"^[^\s@<>,;]+@[^\s@<>,;]+\.[^\s@<>,;]+$")
57
+
58
+
59
+ ENVS: tuple[Env, ...] = ("production", "sandbox")
60
+
61
+
62
+ PRODUCTION: Env = "production"
63
+
64
+
65
+ SANDBOX: Env = "sandbox"
66
+
67
+
68
+ CHANNELS: tuple[Channel, ...] = ("phone", "web", "whatsapp")
69
+
70
+
71
+ CHANNELS_WITH_A_NUMBER: tuple[Channel, ...] = ("phone", "whatsapp")
72
+
73
+
74
+ THE_WIDGET: Channel = "web"
75
+
76
+
77
+ def other_world(world: Env) -> Env:
78
+ """Return the world that is not this one."""
79
+ return SANDBOX if world == PRODUCTION else PRODUCTION
80
+
81
+
82
+ def parse_env(word: str) -> Env:
83
+ """Return the word as an Env, raising DeclarationRefused when it is neither world."""
84
+ if word not in ENVS:
85
+ raise DeclarationRefused(f"a key opens one of {sorted(ENVS)}, not {word!r}")
86
+ return word
87
+
88
+
89
+ def parse_channel(word: str) -> Channel:
90
+ """Return the word as a Channel, raising DeclarationRefused when it is none."""
91
+ if word not in CHANNELS:
92
+ raise DeclarationRefused(f"a call comes through one of {sorted(CHANNELS)}, not {word!r}")
93
+ return word
94
+
95
+
96
+ def parse_slug(word: str) -> str:
97
+ """Return the slug, raising DeclarationRefused unless it is lowercase words joined by dashes."""
98
+ if not A_SLUG.match(word):
99
+ raise DeclarationRefused(f"a slug is lowercase words joined by dashes, not {word!r}")
100
+ return word
101
+
102
+
103
+ def dialable(number: str) -> bool:
104
+ """Return whether the number is in E.164 form."""
105
+ return _E164.match(number) is not None
106
+
107
+
108
+ def parse_e164(number: str) -> str:
109
+ """Return the trimmed number, raising DeclarationRefused when it is not E.164."""
110
+ data = number.strip()
111
+ if not dialable(data):
112
+ raise DeclarationRefused(
113
+ f"a number is written in E.164 form, like +59829001199, not {data!r}"
114
+ )
115
+ return data
pinecall/domain/org.py ADDED
@@ -0,0 +1,120 @@
1
+ """An org and what it may do: its quotas, its admission, what the box lends it."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import Literal
6
+
7
+ from pinecall.domain.errors import DeclarationRefused
8
+ from pinecall.domain.names import parse_slug
9
+
10
+ # Names match the DB columns and the wire. The first four are flows (consumed or open now); the
11
+ # rest are stocks (rows kept), which lets a plan disable a feature with a zero; the budget is
12
+ # dollars a month, both worlds together.
13
+ type QuotaName = Literal[
14
+ "minutes",
15
+ "messages",
16
+ "agents",
17
+ "concurrent_calls",
18
+ "memory_facts",
19
+ "knowledge_chunks",
20
+ "numbers",
21
+ "seats",
22
+ "llm_tokens",
23
+ "hosted_apps",
24
+ "budget_usd",
25
+ ]
26
+
27
+
28
+ # Seeded by the schema; the first key is issued against it and CLI verbs fall back to it.
29
+ DEFAULT_ORG = "default"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Org:
34
+ """An organisation: immutable id, editable slug, and display name."""
35
+
36
+ id: str
37
+ slug: str
38
+ name: str
39
+
40
+ def __post_init__(self) -> None:
41
+ if not self.id:
42
+ raise DeclarationRefused("an org has an id")
43
+ parse_slug(self.slug)
44
+
45
+
46
+ # None is no limit (the self-hosted default). Zero is a real limit that refuses everything.
47
+ @dataclass(frozen=True)
48
+ class Quotas:
49
+ """An org's quota limits, plus its monthly budget and the box keys lent to it."""
50
+
51
+ minutes: int | None = None
52
+ messages: int | None = None
53
+ agents: int | None = None
54
+ concurrent_calls: int | None = None
55
+ memory_facts: int | None = None
56
+ knowledge_chunks: int | None = None
57
+ # Only numbers bought on the box's carrier account; imported numbers do not count.
58
+ numbers: int | None = None
59
+ # Invited and active members; disabled members hold no seat.
60
+ seats: int | None = None
61
+ llm_tokens: int | None = None
62
+ # Apps the box runs for the org from sources it uploaded.
63
+ hosted_apps: int | None = None
64
+ # Whole US dollars a calendar month, both worlds together: a new call is refused past it.
65
+ budget_usd: int | None = None
66
+ # Box vendor keys the org may use: None all, empty none, else `vendor` or `vendor/model`.
67
+ lends: frozenset[str] | None = None
68
+
69
+ def __post_init__(self) -> None:
70
+ for name, limit in self.limits.items():
71
+ if limit is None or limit >= 0:
72
+ continue
73
+ if name == "budget_usd":
74
+ raise DeclarationRefused(f"a budget is dollars, and cannot be {limit}")
75
+ raise DeclarationRefused(f"a quota is a count, and {name} cannot be {limit}")
76
+
77
+ @property
78
+ def limits(self) -> Mapping[QuotaName, int | None]:
79
+ """Return every limit by its name."""
80
+ return {
81
+ "minutes": self.minutes,
82
+ "messages": self.messages,
83
+ "agents": self.agents,
84
+ "concurrent_calls": self.concurrent_calls,
85
+ "memory_facts": self.memory_facts,
86
+ "knowledge_chunks": self.knowledge_chunks,
87
+ "numbers": self.numbers,
88
+ "seats": self.seats,
89
+ "llm_tokens": self.llm_tokens,
90
+ "hosted_apps": self.hosted_apps,
91
+ "budget_usd": self.budget_usd,
92
+ }
93
+
94
+ def reached(self, quota: QuotaName, used: float) -> int | None:
95
+ """Return the limit when `used` is at or past it, else None."""
96
+ limit = self.limits[quota]
97
+ return limit if limit is not None and used >= limit else None
98
+
99
+ def exceeded(self, quota: QuotaName, keeping: float) -> int | None:
100
+ """Return the limit when `keeping` would pass it, else None; for pushes sized up front."""
101
+ limit = self.limits[quota]
102
+ return limit if limit is not None and keeping > limit else None
103
+
104
+ def switched_off(self, quota: QuotaName) -> bool:
105
+ """Return whether the quota is zero: the feature is not in the plan."""
106
+ return self.limits[quota] == 0
107
+
108
+
109
+ QUOTAS: tuple[QuotaName, ...] = (
110
+ "minutes",
111
+ "messages",
112
+ "agents",
113
+ "concurrent_calls",
114
+ "memory_facts",
115
+ "knowledge_chunks",
116
+ "numbers",
117
+ "seats",
118
+ "llm_tokens",
119
+ "hosted_apps",
120
+ )
@@ -0,0 +1,208 @@
1
+ """People and their keys: members, roles, invitations, the scopes a key opens."""
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ from typing import Literal
7
+
8
+ from pinecall.domain.errors import DeclarationRefused
9
+ from pinecall.domain.names import AN_ADDRESS, PRODUCTION, Env
10
+
11
+ # Every gateway door belongs to exactly one scope; `fleet` is the box's worker, resolved per call
12
+ # instead of per key org, and only `keys issue --scope fleet` grants it. `runner` is the box's
13
+ # own too: the process that runs every org's hosted apps, handed their tokens and secrets.
14
+ type KeyScope = Literal[
15
+ "app",
16
+ "calls",
17
+ "talk",
18
+ "supervise",
19
+ "pipeline",
20
+ "knowledge",
21
+ "memory",
22
+ "evals",
23
+ "numbers",
24
+ "keys",
25
+ "providers",
26
+ "team",
27
+ "usage",
28
+ "words",
29
+ "fleet",
30
+ "runner",
31
+ ]
32
+
33
+
34
+ # A role is only a preset of key scopes. Doors check scopes, never roles, so changing a role
35
+ # affects newly issued keys only.
36
+ type Role = Literal["qa", "supervisor", "manager", "admin", "developer"]
37
+
38
+
39
+ # A room token's scope, which LiveKit carries on the seat as its `pinecall.scope` attribute, so
40
+ # the worker reads it without a lookup.
41
+ type RoomScope = Literal["talk", "chat", "observe", "supervise", "participate", "read"]
42
+
43
+
44
+ # disabled: cannot log in and keys are revoked; the row is kept because the log references it.
45
+ type MemberStatus = Literal["invited", "active", "disabled"]
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Member:
50
+ """A person in one org, with their role, agents and status."""
51
+
52
+ id: str
53
+ org: str
54
+ email: str
55
+ name: str
56
+ role: Role
57
+ # Empty means all of the org's agents.
58
+ agents: frozenset[str] = frozenset()
59
+ status: MemberStatus = "invited"
60
+ # Box operator: their key also opens /v1/ops/*. Granted only with the ops key.
61
+ operator: bool = False
62
+ # Granted by an admin; checked per request, so revoking it takes effect immediately.
63
+ production: bool = False
64
+ # Email ownership proven by a mailed link, an IdP, or an operator invitation. Required
65
+ # before joining another org without a link.
66
+ verified: bool = False
67
+
68
+ def __post_init__(self) -> None:
69
+ if not self.id or not self.org:
70
+ raise DeclarationRefused("a member names their id and the org they belong to")
71
+ if not AN_ADDRESS.match(self.email):
72
+ raise DeclarationRefused(f"an email has one @ and a domain, not {self.email!r}")
73
+ if not self.name.strip():
74
+ raise DeclarationRefused("a member has a name: it is what a seat says when they sit")
75
+
76
+ @property
77
+ def scopes(self) -> frozenset[KeyScope]:
78
+ """Return the scopes of keys minted for this member."""
79
+ return ROLE_SCOPES[self.role]
80
+
81
+ @property
82
+ def opens_production(self) -> bool:
83
+ """Return whether this member may act in production; admins always may."""
84
+ return self.role == "admin" or self.production
85
+
86
+
87
+ EVERY_SCOPE: tuple[KeyScope, ...] = (
88
+ "app",
89
+ "calls",
90
+ "talk",
91
+ "supervise",
92
+ "pipeline",
93
+ "knowledge",
94
+ "memory",
95
+ "evals",
96
+ "numbers",
97
+ "keys",
98
+ "providers",
99
+ "team",
100
+ "usage",
101
+ "words",
102
+ "fleet",
103
+ "runner",
104
+ )
105
+
106
+
107
+ THE_FLEET: KeyScope = "fleet"
108
+
109
+
110
+ THE_RUNNER: KeyScope = "runner"
111
+
112
+
113
+ HOLDING: KeyScope = "app"
114
+
115
+
116
+ THE_TEAM: KeyScope = "team"
117
+
118
+
119
+ THE_KEYS: KeyScope = "keys"
120
+
121
+
122
+ # Default scopes: every scope except the box's own two.
123
+ KEY_SCOPES: frozenset[KeyScope] = frozenset(EVERY_SCOPE) - {THE_FLEET, THE_RUNNER}
124
+
125
+
126
+ # Doors take the org only from this record, never from the request.
127
+ @dataclass(frozen=True)
128
+ class Key:
129
+ """A verified API key: its org, environment, scopes and holder."""
130
+
131
+ key_id: str
132
+ org: str
133
+ label: str | None = None
134
+ env: Env = PRODUCTION
135
+ scopes: frozenset[KeyScope] = KEY_SCOPES
136
+ # The person a key was minted for; None on a server's key. A person's one key opens both
137
+ # worlds, so its env is production's and the request names the world.
138
+ subject: str | None = None
139
+ name: str | None = None
140
+ # None never expires. Keys minted from a personal key never outlive it.
141
+ expires_at: datetime | None = None
142
+
143
+
144
+ ROLES: tuple[Role, ...] = ("qa", "supervisor", "manager", "admin", "developer")
145
+
146
+
147
+ STATUSES: tuple[MemberStatus, ...] = ("invited", "active", "disabled")
148
+
149
+
150
+ # Serving an agent, its calls, room tokens, knowledge pushes and evals; never the org's people,
151
+ # numbers or money.
152
+ SERVER_SCOPES: frozenset[KeyScope] = frozenset({"app", "calls", "talk", "knowledge", "evals"})
153
+
154
+
155
+ ROLE_SCOPES: Mapping[Role, frozenset[KeyScope]] = {
156
+ "qa": frozenset({"calls", "evals"}),
157
+ "supervisor": frozenset({"calls", "evals", "supervise", "talk", "memory", "words"}),
158
+ "manager": frozenset(
159
+ {
160
+ "calls",
161
+ "evals",
162
+ "supervise",
163
+ "talk",
164
+ "memory",
165
+ "numbers",
166
+ "keys",
167
+ "providers",
168
+ "usage",
169
+ "team",
170
+ "words",
171
+ }
172
+ ),
173
+ "admin": KEY_SCOPES,
174
+ "developer": frozenset(
175
+ {"app", "calls", "talk", "supervise", "pipeline", "knowledge", "memory", "evals", "words"}
176
+ ),
177
+ }
178
+
179
+
180
+ # The browser's scopes, limited to their own call: the guest log and the widget's channel admit
181
+ # them.
182
+ READS_ITS_OWN_CALL: frozenset[RoomScope] = frozenset({"talk", "chat", "read", "participate"})
183
+
184
+
185
+ def key_scopes(words: Iterable[str]) -> frozenset[KeyScope]:
186
+ """Return the words as scopes, raising DeclarationRefused on the first that is none."""
187
+ scopes: set[KeyScope] = set()
188
+ for word in sorted(set(words)):
189
+ if word not in EVERY_SCOPE:
190
+ raise DeclarationRefused(
191
+ f"{word!r} is not a key scope; the scopes are {sorted(EVERY_SCOPE)}"
192
+ )
193
+ scopes.add(word)
194
+ return frozenset(scopes)
195
+
196
+
197
+ def parse_role(word: str) -> Role:
198
+ """Return the word as a Role, raising DeclarationRefused when it is none."""
199
+ if word not in ROLES:
200
+ raise DeclarationRefused(f"a role is one of {sorted(ROLES)}, not {word!r}")
201
+ return word
202
+
203
+
204
+ def parse_status(word: str) -> MemberStatus:
205
+ """Return the word as a member's status, raising DeclarationRefused when it is none."""
206
+ if word not in STATUSES:
207
+ raise DeclarationRefused(f"a member's status is one of {sorted(STATUSES)}, not {word!r}")
208
+ return word
@@ -0,0 +1,21 @@
1
+ """A scope: an org, one of its two worlds, and the holder a request acts as."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pinecall.domain.names import PRODUCTION, Env
6
+
7
+ # Holder value for org-owned rows: an empty string, not NULL, because it is part of a primary
8
+ # key and NULL never matches.
9
+ THE_ORGS_OWN = ""
10
+
11
+ SCOPE_ATTRIBUTE = "pinecall.scope"
12
+
13
+
14
+ # What a request may see: an org, one of its two worlds, and in the sandbox one developer's own.
15
+ @dataclass(frozen=True)
16
+ class Scope:
17
+ """An org's scope: the org, the environment and the holder ("" for the org itself)."""
18
+
19
+ org: str
20
+ env: Env = PRODUCTION
21
+ holder: str = THE_ORGS_OWN
File without changes