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.
- pinecall/__init__.py +0 -0
- pinecall/channels/__init__.py +0 -0
- pinecall/channels/_chooser.py +45 -0
- pinecall/channels/offers.py +219 -0
- pinecall/channels/rooms.py +176 -0
- pinecall/channels/routes.py +275 -0
- pinecall/channels/telephony/__init__.py +0 -0
- pinecall/channels/telephony/_twilio.py +349 -0
- pinecall/channels/telephony/carrier.py +229 -0
- pinecall/channels/telephony/carrier_catalog.py +92 -0
- pinecall/channels/telephony/carriers.csv +41 -0
- pinecall/channels/telephony/dialing.py +419 -0
- pinecall/channels/telephony/firewall.py +58 -0
- pinecall/channels/telephony/hand_over.py +78 -0
- pinecall/channels/telephony/number_path.py +176 -0
- pinecall/channels/telephony/numbers.py +530 -0
- pinecall/channels/telephony/origination.py +77 -0
- pinecall/channels/telephony/sip.py +448 -0
- pinecall/channels/whatsapp.py +413 -0
- pinecall/cli/__init__.py +0 -0
- pinecall/cli/_archive.py +85 -0
- pinecall/cli/_drift.py +38 -0
- pinecall/cli/_facts.py +67 -0
- pinecall/cli/_load.py +433 -0
- pinecall/cli/_operator.py +626 -0
- pinecall/cli/_sessions.py +164 -0
- pinecall/cli/_traceback.py +95 -0
- pinecall/cli/_usage.py +34 -0
- pinecall/cli/main.py +521 -0
- pinecall/domain/__init__.py +0 -0
- pinecall/domain/agent.py +446 -0
- pinecall/domain/call.py +152 -0
- pinecall/domain/country_codes.py +32 -0
- pinecall/domain/errors.py +151 -0
- pinecall/domain/names.py +115 -0
- pinecall/domain/org.py +120 -0
- pinecall/domain/person.py +208 -0
- pinecall/domain/scope.py +21 -0
- pinecall/evals/__init__.py +0 -0
- pinecall/evals/_evidence.py +158 -0
- pinecall/evals/calibration.py +112 -0
- pinecall/evals/callers.py +160 -0
- pinecall/evals/case.py +327 -0
- pinecall/evals/checks.py +437 -0
- pinecall/evals/compliance.py +197 -0
- pinecall/evals/dataset.py +451 -0
- pinecall/evals/goldens.py +331 -0
- pinecall/evals/judges.py +525 -0
- pinecall/evals/runs.py +252 -0
- pinecall/evals/spoken.py +356 -0
- pinecall/fleet/__init__.py +0 -0
- pinecall/fleet/client.py +569 -0
- pinecall/fleet/demand.py +51 -0
- pinecall/fleet/heartbeat.py +194 -0
- pinecall/fleet/measures.py +157 -0
- pinecall/fleet/roster.py +232 -0
- pinecall/fleet/worlds.py +44 -0
- pinecall/gateway/__init__.py +0 -0
- pinecall/gateway/_call_setup.py +53 -0
- pinecall/gateway/_deps.py +556 -0
- pinecall/gateway/_gateway.py +75 -0
- pinecall/gateway/_served.py +396 -0
- pinecall/gateway/_sockets.py +444 -0
- pinecall/gateway/_streams.py +83 -0
- pinecall/gateway/_text_calls.py +182 -0
- pinecall/gateway/api/__init__.py +0 -0
- pinecall/gateway/api/accounts.py +324 -0
- pinecall/gateway/api/agents.py +150 -0
- pinecall/gateway/api/apps.py +478 -0
- pinecall/gateway/api/box.py +658 -0
- pinecall/gateway/api/calibration.py +58 -0
- pinecall/gateway/api/callbacks.py +105 -0
- pinecall/gateway/api/calls.py +655 -0
- pinecall/gateway/api/chat.py +223 -0
- pinecall/gateway/api/dataset.py +163 -0
- pinecall/gateway/api/desk.py +113 -0
- pinecall/gateway/api/entries.py +77 -0
- pinecall/gateway/api/evals.py +647 -0
- pinecall/gateway/api/fleet.py +73 -0
- pinecall/gateway/api/hosting.py +241 -0
- pinecall/gateway/api/judges.py +97 -0
- pinecall/gateway/api/keys.py +110 -0
- pinecall/gateway/api/line.py +142 -0
- pinecall/gateway/api/members.py +197 -0
- pinecall/gateway/api/metrics.py +226 -0
- pinecall/gateway/api/numbers.py +476 -0
- pinecall/gateway/api/ops.py +428 -0
- pinecall/gateway/api/org.py +283 -0
- pinecall/gateway/api/personas.py +157 -0
- pinecall/gateway/api/pipeline.py +206 -0
- pinecall/gateway/api/prompts.py +50 -0
- pinecall/gateway/api/providers.py +240 -0
- pinecall/gateway/api/recordings.py +89 -0
- pinecall/gateway/api/relay.py +169 -0
- pinecall/gateway/api/retrieval.py +403 -0
- pinecall/gateway/api/runner.py +109 -0
- pinecall/gateway/api/sessions.py +102 -0
- pinecall/gateway/api/settings.py +463 -0
- pinecall/gateway/api/signup.py +164 -0
- pinecall/gateway/api/sso_login.py +168 -0
- pinecall/gateway/api/threads.py +187 -0
- pinecall/gateway/api/usage.py +303 -0
- pinecall/gateway/api/visitors.py +222 -0
- pinecall/gateway/api/whatsapp.py +48 -0
- pinecall/gateway/api/widget.py +49 -0
- pinecall/gateway/app.py +504 -0
- pinecall/gateway/calls/__init__.py +0 -0
- pinecall/gateway/calls/binding.py +63 -0
- pinecall/gateway/calls/commands.py +18 -0
- pinecall/gateway/calls/inbox.py +100 -0
- pinecall/gateway/calls/known.py +23 -0
- pinecall/gateway/calls/owners.py +87 -0
- pinecall/gateway/calls/pump.py +33 -0
- pinecall/gateway/calls/serving.py +118 -0
- pinecall/gateway/calls/threads.py +367 -0
- pinecall/gateway/dispatching/__init__.py +0 -0
- pinecall/gateway/dispatching/arrivals.py +58 -0
- pinecall/gateway/dispatching/sweep.py +43 -0
- pinecall/gateway/ending/__init__.py +0 -0
- pinecall/gateway/ending/reaper.py +160 -0
- pinecall/gateway/ending/seal.py +312 -0
- pinecall/gateway/ending/stranded.py +117 -0
- pinecall/log/__init__.py +0 -0
- pinecall/log/_histogram.py +74 -0
- pinecall/log/_relay.py +259 -0
- pinecall/log/_writer.py +454 -0
- pinecall/log/days.py +154 -0
- pinecall/log/drift.py +550 -0
- pinecall/log/facts.py +358 -0
- pinecall/log/inbox.py +169 -0
- pinecall/log/lists.py +155 -0
- pinecall/log/logs.py +581 -0
- pinecall/log/openings.py +71 -0
- pinecall/log/private.py +135 -0
- pinecall/log/queries.py +229 -0
- pinecall/log/readers.py +207 -0
- pinecall/log/reduce.py +659 -0
- pinecall/log/refold.py +217 -0
- pinecall/log/store.py +370 -0
- pinecall/log/usage.py +242 -0
- pinecall/postgres/__init__.py +0 -0
- pinecall/postgres/migrate.py +202 -0
- pinecall/postgres/migrations/0001_schema.sql +591 -0
- pinecall/postgres/migrations/0002_worlds.sql +15 -0
- pinecall/postgres/migrations/0003_lent.sql +4 -0
- pinecall/postgres/migrations/0004_carriers.sql +23 -0
- pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
- pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
- pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
- pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
- pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
- pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
- pinecall/postgres/migrations/0011_erasure.sql +32 -0
- pinecall/postgres/migrations/0012_org_policy.sql +10 -0
- pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
- pinecall/postgres/migrations/0014_consents.sql +24 -0
- pinecall/postgres/migrations/0015_disclosure.sql +7 -0
- pinecall/postgres/migrations/0016_call_records.sql +21 -0
- pinecall/postgres/migrations/0017_reads.sql +17 -0
- pinecall/postgres/migrations/0018_hosting.sql +41 -0
- pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
- pinecall/postgres/migrations/0020_written_once.sql +7 -0
- pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
- pinecall/postgres/migrations/0022_sealed.sql +39 -0
- pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
- pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
- pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
- pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
- pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
- pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
- pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
- pinecall/postgres/migrations/0060_drift.sql +62 -0
- pinecall/postgres/migrations/0061_canaries.sql +24 -0
- pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
- pinecall/postgres/migrations/0063_prompts.sql +16 -0
- pinecall/postgres/migrations/0075_call_private.sql +14 -0
- pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
- pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
- pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
- pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
- pinecall/postgres/migrations/0080_call_openings.sql +20 -0
- pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
- pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
- pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
- pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
- pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
- pinecall/postgres/migrations/0086_route_origin.sql +17 -0
- pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
- pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
- pinecall/postgres/migrations/0089_offers.sql +21 -0
- pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
- pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
- pinecall/postgres/migrations/0092_key_parent.sql +6 -0
- pinecall/postgres/migrations/0093_code_tries.sql +10 -0
- pinecall/postgres/migrations/0094_route_approval.sql +12 -0
- pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
- pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
- pinecall/postgres/migrations/0097_case_status.sql +19 -0
- pinecall/postgres/pool.py +190 -0
- pinecall/process/__init__.py +0 -0
- pinecall/process/_objects.py +97 -0
- pinecall/process/box_settings.py +23 -0
- pinecall/process/connections.py +124 -0
- pinecall/process/metrics.py +127 -0
- pinecall/process/recordings.py +323 -0
- pinecall/process/resolver.py +41 -0
- pinecall/process/sealed_audio.py +173 -0
- pinecall/process/settings.py +534 -0
- pinecall/process/shared.py +158 -0
- pinecall/process/signal.py +416 -0
- pinecall/providers/__init__.py +0 -0
- pinecall/providers/build.py +570 -0
- pinecall/providers/catalog.py +266 -0
- pinecall/providers/credentials.py +230 -0
- pinecall/providers/declared.py +69 -0
- pinecall/providers/prices.py +240 -0
- pinecall/providers/tried.py +104 -0
- pinecall/providers/voices.py +174 -0
- pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
- pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
- pinecall/public/console/favicon.png +0 -0
- pinecall/public/console/index.html +20 -0
- pinecall/public/console/logo-mark.png +0 -0
- pinecall/public/console/pinecall-logo-dark.png +0 -0
- pinecall/public/console/pinecall-logo.png +0 -0
- pinecall/public/console/pinecall-mark.png +0 -0
- pinecall/public/console/sw.js +37 -0
- pinecall/public/widget/pinecall-widget.js +853 -0
- pinecall/py.typed +0 -0
- pinecall/retrieval/__init__.py +0 -0
- pinecall/retrieval/_search.py +186 -0
- pinecall/retrieval/_sections.py +70 -0
- pinecall/retrieval/embed.py +340 -0
- pinecall/retrieval/extraction.py +403 -0
- pinecall/retrieval/knowledge.py +659 -0
- pinecall/retrieval/lookups.py +192 -0
- pinecall/retrieval/memory.py +603 -0
- pinecall/runner/__init__.py +0 -0
- pinecall/runner/_kube.py +459 -0
- pinecall/runner/_plan.py +157 -0
- pinecall/runner/main.py +306 -0
- pinecall/session/__init__.py +0 -0
- pinecall/session/_agent.py +141 -0
- pinecall/session/_hearing.py +101 -0
- pinecall/session/_livekit.py +195 -0
- pinecall/session/_prompt.py +147 -0
- pinecall/session/_recording.py +237 -0
- pinecall/session/call.py +367 -0
- pinecall/session/clock.py +40 -0
- pinecall/session/hold.py +239 -0
- pinecall/session/room.py +626 -0
- pinecall/session/session.py +690 -0
- pinecall/session/text.py +144 -0
- pinecall/session/tools.py +483 -0
- pinecall/session/voice.py +118 -0
- pinecall/session/widget.py +201 -0
- pinecall/tenancy/__init__.py +0 -0
- pinecall/tenancy/admission.py +204 -0
- pinecall/tenancy/agents.py +164 -0
- pinecall/tenancy/canary.py +126 -0
- pinecall/tenancy/carrier_networks.py +156 -0
- pinecall/tenancy/carriers.py +265 -0
- pinecall/tenancy/codes.py +196 -0
- pinecall/tenancy/consents.py +167 -0
- pinecall/tenancy/dial_policy.py +405 -0
- pinecall/tenancy/disclosure.py +41 -0
- pinecall/tenancy/erasure.py +204 -0
- pinecall/tenancy/export.py +167 -0
- pinecall/tenancy/hosted_running.py +166 -0
- pinecall/tenancy/hosting.py +475 -0
- pinecall/tenancy/judges.py +126 -0
- pinecall/tenancy/keys.py +467 -0
- pinecall/tenancy/knocks.py +40 -0
- pinecall/tenancy/letters.py +378 -0
- pinecall/tenancy/mail.py +458 -0
- pinecall/tenancy/org_secrets.py +123 -0
- pinecall/tenancy/orgs.py +78 -0
- pinecall/tenancy/people.py +543 -0
- pinecall/tenancy/personas.py +145 -0
- pinecall/tenancy/policy.py +65 -0
- pinecall/tenancy/prompts.py +48 -0
- pinecall/tenancy/reads.py +73 -0
- pinecall/tenancy/recording_keys.py +41 -0
- pinecall/tenancy/remembered.py +148 -0
- pinecall/tenancy/retention.py +86 -0
- pinecall/tenancy/scopes.py +406 -0
- pinecall/tenancy/signin.py +622 -0
- pinecall/tenancy/spend.py +69 -0
- pinecall/tenancy/sso.py +431 -0
- pinecall/tenancy/sso_domains.py +132 -0
- pinecall/tenancy/throttle.py +88 -0
- pinecall/tenancy/tokens.py +400 -0
- pinecall/tenancy/traceback.py +70 -0
- pinecall/tenancy/usage.py +136 -0
- pinecall/tenancy/vault.py +204 -0
- pinecall/tenancy/words.py +144 -0
- pinecall/wire/__init__.py +0 -0
- pinecall/wire/commands.py +350 -0
- pinecall/wire/events.py +655 -0
- pinecall/wire/frames.py +65 -0
- pinecall/wire/metrics.py +287 -0
- pinecall/wire/parts.py +320 -0
- pinecall/wire/rest/__init__.py +0 -0
- pinecall/wire/rest/accounts.py +512 -0
- pinecall/wire/rest/agents.py +172 -0
- pinecall/wire/rest/calls.py +397 -0
- pinecall/wire/rest/evals.py +453 -0
- pinecall/wire/rest/fleet.py +69 -0
- pinecall/wire/rest/hosting.py +174 -0
- pinecall/wire/rest/numbers.py +280 -0
- pinecall/wire/rest/ops.py +342 -0
- pinecall/wire/rest/providers.py +83 -0
- pinecall/wire/rest/retrieval.py +301 -0
- pinecall/wire/rest/settings.py +184 -0
- pinecall/wire/rest/usage.py +245 -0
- pinecall/wire/room.py +61 -0
- pinecall/wire/scores.py +49 -0
- pinecall/wire/state.py +243 -0
- pinecall/worker/__init__.py +0 -0
- pinecall/worker/_job.py +655 -0
- pinecall/worker/_recorder.py +82 -0
- pinecall/worker/_traces.py +48 -0
- pinecall/worker/main.py +307 -0
- pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
- pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
- pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
- pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|