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,222 @@
1
+ """A visitor's doors: a browser's room token, and the code a caller keys to tie a page."""
2
+
3
+ import time
4
+ from typing import Annotated
5
+
6
+ from fastapi import APIRouter, Query
7
+ from pydantic import BaseModel
8
+
9
+ from pinecall.channels import rooms, routes
10
+ from pinecall.channels.rooms import Dispatch
11
+ from pinecall.domain.call import new_call_id
12
+ from pinecall.domain.errors import (
13
+ Conflict,
14
+ DeclarationRefused,
15
+ NotAllowed,
16
+ NotAvailable,
17
+ NotFound,
18
+ )
19
+ from pinecall.domain.names import THE_WIDGET
20
+ from pinecall.domain.person import RoomScope
21
+ from pinecall.fleet import worlds
22
+ from pinecall.gateway import _deps
23
+ from pinecall.gateway._deps import GatewayDep, ReaderDep, TalkKey
24
+ from pinecall.gateway._gateway import Gateway
25
+ from pinecall.gateway._sockets import NO_AGENT
26
+ from pinecall.tenancy import keys, tokens
27
+ from pinecall.tenancy.keys import LONGEST_VISIT_TTL_S
28
+ from pinecall.tenancy.tokens import MINTED_FOR_A_VISIT, ONE_VISIT_TTL_S
29
+ from pinecall.wire.events import (
30
+ FleetFull,
31
+ )
32
+ from pinecall.wire.rest.calls import (
33
+ CodeStatus,
34
+ IssueCodeRequest,
35
+ IssueCodeResponse,
36
+ MintTokenRequest,
37
+ MintTokenResponse,
38
+ )
39
+
40
+ router = APIRouter()
41
+
42
+
43
+ NOT_A_VISIT = "scope is one of {scopes}: observe and supervise take the API key instead"
44
+
45
+
46
+ NO_AGENT_NAMED = "name the agent: `agent` in the body, or agentName as a livekit client sends it"
47
+
48
+
49
+ OURS_TO_SET = {
50
+ "room_name": "the room is the call id this door mints",
51
+ "participant_name": "a name is PII, and the log would carry it",
52
+ "participant_metadata": "it carries the contact id: send `contact`",
53
+ }
54
+
55
+
56
+ NOT_YOURS_TO_SET = "{field} is minted here and refused in the body: {why}"
57
+
58
+
59
+ OUR_ATTRIBUTES = "pinecall."
60
+
61
+
62
+ FLEET_FULL = (
63
+ "every seat of the fleet is taken: {active} calls on {workers} workers. Offer a call back, or "
64
+ "try again in a minute"
65
+ )
66
+
67
+
68
+ NO_PHONE = "agent {slug} answers at no phone number in {env}: import one first"
69
+
70
+
71
+ NOT_YOUR_CODE = "this token reads another code"
72
+
73
+
74
+ # 25 s: under the 30 s a proxy lets a request idle.
75
+ LONGEST_WAIT_S = 25.0
76
+
77
+
78
+ A_PAGE_MAY_ASK_AFTER_S = 60.0
79
+
80
+
81
+ class CodeQuery(BaseModel):
82
+ """Whether a page's ask waits for its code to be keyed."""
83
+
84
+ wait: bool = False
85
+
86
+
87
+ # 201, livekit's own: a client SDK's token source expects it. The ledger row is written first,
88
+ # so the dispatch spends the token once.
89
+ @router.post("/v1/tokens", status_code=201)
90
+ async def mint_room_token(
91
+ body: MintTokenRequest, key: TalkKey, gateway: GatewayDep
92
+ ) -> MintTokenResponse:
93
+ """A room token for one of the key's agents, the dispatch to its world's fleet inside it."""
94
+ _refuse_what_is_ours(body)
95
+ org_scope = keys.scope_of(key.bearer, key.env)
96
+ agent = body.agent or rooms.client_named_agent(body.room_config)
97
+ if agent is None:
98
+ raise DeclarationRefused(NO_AGENT_NAMED)
99
+ keys.check_agent(key.bearer, agent)
100
+ if gateway.sockets.serving(org_scope, agent, None) is None:
101
+ raise NotFound(NO_AGENT.format(slug=agent))
102
+ fleet = worlds.fleet_of(await worlds.fleets(gateway.connections.pool), org_scope.env)
103
+ await _room_for_one_more(gateway, fleet, agent)
104
+ await _deps.admit_call(gateway, org_scope, agent)
105
+ call = new_call_id()
106
+ ttl = min(body.ttl_s or ONE_VISIT_TTL_S, LONGEST_VISIT_TTL_S)
107
+ expires_at = time.time() + ttl
108
+ scope = _visit_scope(body.scope)
109
+ visitor = body.participant_identity or f"web_{new_call_id()[5:17]}"
110
+ carried = Dispatch(
111
+ agent=agent,
112
+ org=org_scope.org,
113
+ env=org_scope.env,
114
+ holder=org_scope.holder or None,
115
+ scope=scope,
116
+ caller=visitor,
117
+ contact=body.contact,
118
+ metadata=body.metadata,
119
+ )
120
+ token = tokens.room_token(
121
+ gateway.signer,
122
+ call,
123
+ scope,
124
+ tokens.Visitor(
125
+ expires_at=expires_at,
126
+ identity=visitor,
127
+ dispatch=rooms.room_dispatch(fleet, carried),
128
+ ),
129
+ )
130
+ await tokens.minted(
131
+ gateway.connections.pool,
132
+ tokens.MintedToken(call, org_scope.org, org_scope.env, agent, scope, expires_at),
133
+ )
134
+ return MintTokenResponse(
135
+ server_url=gateway.connections.settings.browser_livekit_url(org_scope.env),
136
+ participant_token=token,
137
+ call=call,
138
+ log_token=tokens.log_token(gateway.signer, call, body.log),
139
+ )
140
+
141
+
142
+ # The number is the key's own agent's in the key's world: a key issues codes for its agents.
143
+ @router.post("/v1/codes", status_code=201)
144
+ async def issue_code(
145
+ body: IssueCodeRequest, key: TalkKey, gateway: GatewayDep
146
+ ) -> IssueCodeResponse:
147
+ """Four digits for a caller to key, the number to call, and a token that asks after them."""
148
+ keys.check_agent(key.bearer, body.agent)
149
+ doors = await routes.of_org(gateway.connections.pool, key.org, key.env)
150
+ number = next(
151
+ (door.number for door in doors if door.agent == body.agent and door.channel == "phone"),
152
+ None,
153
+ )
154
+ if number is None:
155
+ raise Conflict(NO_PHONE.format(slug=body.agent, env=key.env))
156
+ issued = await gateway.codes.issue(key.env, body.agent, body.ttl_s, body.log)
157
+ # The token outlives the code a little, so the page is told it expired rather than refused.
158
+ asks_until = issued.expires_at + A_PAGE_MAY_ASK_AFTER_S
159
+ token = tokens.code_token(gateway.signer, issued.code, body.agent, key.env, asks_until)
160
+ return IssueCodeResponse(
161
+ code=issued.code, number=number, expires_at=issued.expires_at, code_token=token
162
+ )
163
+
164
+
165
+ # A page holds no key: the code token its page was handed is the only credential.
166
+ @router.get("/v1/codes/{code}")
167
+ async def code_status(
168
+ code: str, reading: ReaderDep, gateway: GatewayDep, query: Annotated[CodeQuery, Query()]
169
+ ) -> CodeStatus:
170
+ """How the code stands; with ?wait=1, held up to 25 s for a call to key it."""
171
+ visit = reading.visit
172
+ if visit is None or visit.code != code or visit.agent is None or visit.env is None:
173
+ raise NotAllowed(NOT_YOUR_CODE)
174
+ issued = await gateway.codes.status_of(visit.env, visit.agent, code)
175
+ if issued is None:
176
+ raise NotFound(_deps.NOBODY_ISSUED.format(code=code, agent=visit.agent))
177
+ left = issued.expires_at - time.time()
178
+ if query.wait and issued.claimed is None and left > 0:
179
+ issued = await gateway.codes.waited(issued, min(LONGEST_WAIT_S, left))
180
+ if issued.claimed is not None:
181
+ token = tokens.log_token(gateway.signer, issued.claimed, issued.log)
182
+ return CodeStatus(
183
+ code=code,
184
+ status="claimed",
185
+ expires_at=issued.expires_at,
186
+ call=issued.claimed,
187
+ log_token=token,
188
+ )
189
+ status = "expired" if time.time() >= issued.expires_at else "waiting"
190
+ return CodeStatus(
191
+ code=code, status=status, expires_at=issued.expires_at, call=None, log_token=None
192
+ )
193
+
194
+
195
+ # A gateway nobody's worker reported to refuses nobody.
196
+ async def _room_for_one_more(gateway: Gateway, fleet: str, agent: str) -> None:
197
+ totals = gateway.roster.totals(fleet, time.time())
198
+ if not totals.full:
199
+ return
200
+ full = FleetFull(channel=THE_WIDGET, workers=totals.workers, active=totals.active)
201
+ await gateway.logs.agent(agent).append("fleet.full", full.written())
202
+ raise NotAvailable(FLEET_FULL.format(active=totals.active, workers=totals.workers))
203
+
204
+
205
+ def _refuse_what_is_ours(body: MintTokenRequest) -> None:
206
+ ours = (
207
+ ("room_name", body.room_name),
208
+ ("participant_name", body.participant_name),
209
+ ("participant_metadata", body.participant_metadata),
210
+ )
211
+ for field, sent in ours:
212
+ if sent is not None:
213
+ raise DeclarationRefused(NOT_YOURS_TO_SET.format(field=field, why=OURS_TO_SET[field]))
214
+ if any(name.startswith(OUR_ATTRIBUTES) for name in body.participant_attributes):
215
+ raise DeclarationRefused(f"participant_attributes under {OUR_ATTRIBUTES} are minted here")
216
+
217
+
218
+ def _visit_scope(scope: str) -> RoomScope:
219
+ for visit in MINTED_FOR_A_VISIT:
220
+ if visit == scope:
221
+ return visit
222
+ raise DeclarationRefused(NOT_A_VISIT.format(scopes=sorted(MINTED_FOR_A_VISIT)))
@@ -0,0 +1,48 @@
1
+ """Meta's webhook: the handshake that subscribes it, and the messages it delivers."""
2
+
3
+ import logging
4
+ from typing import Annotated
5
+
6
+ from fastapi import APIRouter, Query, Request
7
+ from fastapi.responses import PlainTextResponse
8
+
9
+ from pinecall.channels import whatsapp
10
+ from pinecall.domain.errors import NotAllowed, NotAvailable
11
+ from pinecall.gateway._deps import GatewayDep, webhook_body
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ router = APIRouter()
16
+
17
+ UNSIGNED = "the body carries no signature of this box's Meta app"
18
+
19
+ STILL_READING = "{count} of these messages are being read by another delivery: send it again"
20
+
21
+
22
+ @router.get("/v1/whatsapp/webhook", response_class=PlainTextResponse)
23
+ async def verify_webhook(
24
+ gateway: GatewayDep,
25
+ mode: Annotated[str | None, Query(alias="hub.mode")] = None,
26
+ word: Annotated[str | None, Query(alias="hub.verify_token")] = None,
27
+ challenge: Annotated[str | None, Query(alias="hub.challenge")] = None,
28
+ ) -> str:
29
+ """Meta subscribing: its challenge echoed back when it says this box's word."""
30
+ app = await whatsapp.box_account(gateway.connections.pool, gateway.connections.vault)
31
+ return whatsapp.handshake(app, mode, word, challenge)
32
+
33
+
34
+ # Past the signature the answer is 200: Meta disables a webhook that keeps failing, so what goes
35
+ # wrong after it is a line in the log. The one exception is a message another delivery is still
36
+ # reading: 503 keeps it on Meta's side until that reading is done or has died.
37
+ @router.post("/v1/whatsapp/webhook")
38
+ async def receive_webhook(request: Request, gateway: GatewayDep) -> dict[str, int]:
39
+ """Every message of a signed body onto its contact's conversation."""
40
+ app = await whatsapp.box_account(gateway.connections.pool, gateway.connections.vault)
41
+ body = await webhook_body(request)
42
+ if not whatsapp.is_signed(app.app_secret, body, request.headers.get(whatsapp.SIGNATURE_HEADER)):
43
+ raise NotAllowed(UNSIGNED)
44
+ messages = whatsapp.messages_in(body)
45
+ unread = [inbound for inbound in messages if not await gateway.threads.received(inbound)]
46
+ if unread:
47
+ raise NotAvailable(STILL_READING.format(count=len(unread)))
48
+ return {"received": len(messages)}
@@ -0,0 +1,49 @@
1
+ """How the widget presents an agent: its title, tagline, greeting, accent, autostart and theme."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ from pinecall.domain.scope import Scope
6
+ from pinecall.gateway._deps import GatewayDep, PipelineKey, TalkKey
7
+ from pinecall.tenancy import agents
8
+ from pinecall.tenancy.agents import Look
9
+ from pinecall.wire.rest.agents import WidgetSettings
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ # `talk`: whoever may mint the widget's token may read how it looks.
15
+ @router.get("/v1/agents/{slug}/widget")
16
+ async def get_widget(slug: str, key: TalkKey, gateway: GatewayDep) -> WidgetSettings:
17
+ """How the agent's widget looks in the key's world; the widget's own defaults when unset."""
18
+ look = await agents.look_of(gateway.connections.pool, Scope(key.org, key.env), slug)
19
+ return _widget_row(look)
20
+
21
+
22
+ # `pipeline`: the same key that sets the agent's greeting and voice.
23
+ @router.put("/v1/agents/{slug}/widget")
24
+ async def put_widget(
25
+ slug: str, body: WidgetSettings, key: PipelineKey, gateway: GatewayDep
26
+ ) -> WidgetSettings:
27
+ """Replace how the widget looks, whole; a null field is the widget's own default."""
28
+ look = Look(
29
+ title=body.title,
30
+ tagline=body.tagline,
31
+ greeting=body.greeting,
32
+ accent=body.accent,
33
+ autostart=body.autostart,
34
+ theme=body.theme,
35
+ )
36
+ await agents.put_look(gateway.connections.pool, Scope(key.org, key.env), slug, look)
37
+ return _widget_row(look)
38
+
39
+
40
+ def _widget_row(look: Look) -> WidgetSettings:
41
+ """A look as the doors send it."""
42
+ return WidgetSettings(
43
+ title=look.title,
44
+ tagline=look.tagline,
45
+ greeting=look.greeting,
46
+ accent=look.accent,
47
+ autostart=look.autostart,
48
+ theme=look.theme,
49
+ )