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,476 @@
|
|
|
1
|
+
"""The number doors: the org's carrier accounts, its numbers, dialling out, and a worker's leg."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Query
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from pinecall.channels import routes
|
|
10
|
+
from pinecall.channels.telephony import carrier_catalog, dialing, number_path, numbers, sip
|
|
11
|
+
from pinecall.channels.telephony.carrier import declared_networks, verify_account
|
|
12
|
+
from pinecall.channels.telephony.dialing import Placement
|
|
13
|
+
from pinecall.channels.telephony.numbers import NumberImport, NumberPurchase
|
|
14
|
+
from pinecall.domain.call import today_in
|
|
15
|
+
from pinecall.domain.errors import Conflict, NotAvailable, NotFound
|
|
16
|
+
from pinecall.domain.names import parse_e164
|
|
17
|
+
from pinecall.domain.scope import Scope
|
|
18
|
+
from pinecall.gateway._deps import (
|
|
19
|
+
Acting,
|
|
20
|
+
CallsKey,
|
|
21
|
+
GatewayDep,
|
|
22
|
+
NumbersKey,
|
|
23
|
+
ScopeDep,
|
|
24
|
+
TalkKey,
|
|
25
|
+
WorkerKey,
|
|
26
|
+
asked_by,
|
|
27
|
+
)
|
|
28
|
+
from pinecall.gateway._gateway import Gateway
|
|
29
|
+
from pinecall.tenancy import carrier_networks, carriers, consents, keys, tokens
|
|
30
|
+
from pinecall.tenancy.carrier_networks import NetworkAsk
|
|
31
|
+
from pinecall.tenancy.carriers import Carrier
|
|
32
|
+
from pinecall.tenancy.consents import Given
|
|
33
|
+
from pinecall.tenancy.dial_policy import Dial, guard_second_leg
|
|
34
|
+
from pinecall.wire.rest.numbers import (
|
|
35
|
+
AvailableNumbers,
|
|
36
|
+
BuyNumberRequest,
|
|
37
|
+
CarrierCatalog,
|
|
38
|
+
CarrierList,
|
|
39
|
+
CarrierRow,
|
|
40
|
+
CatalogCarrier,
|
|
41
|
+
ConsentHistory,
|
|
42
|
+
DialGuards,
|
|
43
|
+
DialRequest,
|
|
44
|
+
DialResponse,
|
|
45
|
+
DoNotCall,
|
|
46
|
+
DoNotCallImport,
|
|
47
|
+
DoNotCallImported,
|
|
48
|
+
ImportNumberRequest,
|
|
49
|
+
ImportNumberResponse,
|
|
50
|
+
LegTrunkResponse,
|
|
51
|
+
MoveNumberRequest,
|
|
52
|
+
NetworkRow,
|
|
53
|
+
NumberPath,
|
|
54
|
+
NumberRow,
|
|
55
|
+
OutboundStatus,
|
|
56
|
+
OwnedNumberRow,
|
|
57
|
+
PathStep,
|
|
58
|
+
ProvisionOutboundResponse,
|
|
59
|
+
RecordConsent,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
NO_SUCH_NUMBER = "the org answers at no {number} in the {env}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
router = APIRouter()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Who the trail names for an opt-out written at a door, with no form behind it.
|
|
69
|
+
ASKED_BY_HAND = "asked at the console or the API"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
NOBODY_HOLDING = (
|
|
73
|
+
"nobody holds agent {agent} in this corner: a phone would ring with no app to serve it"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# A person or a server acts in the org's own scope of its world: numbers are the org's.
|
|
78
|
+
AccountAsked = Annotated[str | None, Query()]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
DryRun = Annotated[bool, Query()]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ColdTransferQuery(BaseModel):
|
|
85
|
+
"""A worker asking whether the caller may be sent on: to whom, from which call."""
|
|
86
|
+
|
|
87
|
+
to: str
|
|
88
|
+
call: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class LegTrunkQuery(BaseModel):
|
|
92
|
+
"""A worker asking how to dial a leg of a call: to whom, from which of the org's numbers."""
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
95
|
+
|
|
96
|
+
to: str
|
|
97
|
+
call: str
|
|
98
|
+
shown: str | None = Field(None, alias="from")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.get("/v1/carrier")
|
|
102
|
+
async def get_carrier(
|
|
103
|
+
key: NumbersKey, gateway: GatewayDep, account: AccountAsked = None
|
|
104
|
+
) -> CarrierRow:
|
|
105
|
+
"""The org's account, named, never its secret; the only one unless one is asked for."""
|
|
106
|
+
carrier = await carriers.carrier_named(
|
|
107
|
+
gateway.connections.pool, gateway.connections.vault, key.org, account
|
|
108
|
+
)
|
|
109
|
+
asks = await carrier_networks.of_org(gateway.connections.pool, key.org)
|
|
110
|
+
return _carrier_row(carrier, asks)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@router.get("/v1/carriers")
|
|
114
|
+
async def list_carriers(key: NumbersKey, gateway: GatewayDep) -> CarrierList:
|
|
115
|
+
"""Every account of the org, oldest first."""
|
|
116
|
+
carriers_listed = await carriers.carriers_of(
|
|
117
|
+
gateway.connections.pool, gateway.connections.vault, key.org
|
|
118
|
+
)
|
|
119
|
+
asks = await carrier_networks.of_org(gateway.connections.pool, key.org)
|
|
120
|
+
return CarrierList(carriers=[_carrier_row(carrier, asks) for carrier in carriers_listed])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# What Add a number offers beside the org's own accounts: Twilio drives its own numbers; a
|
|
124
|
+
# guided carrier is SIP terms, a number pasted into its portal.
|
|
125
|
+
@router.get("/v1/carriers/catalog")
|
|
126
|
+
async def list_carrier_catalog(_key: NumbersKey, gateway: GatewayDep) -> CarrierCatalog:
|
|
127
|
+
"""The carriers this box admits, each automatic or guided, and whether the box sells numbers."""
|
|
128
|
+
connections = gateway.connections
|
|
129
|
+
admitted = await carrier_catalog.admitted(connections.pool)
|
|
130
|
+
try:
|
|
131
|
+
await carriers.box_twilio(connections.pool, connections.vault)
|
|
132
|
+
sells = True
|
|
133
|
+
except NotAvailable:
|
|
134
|
+
sells = False
|
|
135
|
+
return CarrierCatalog(
|
|
136
|
+
carriers=[
|
|
137
|
+
CatalogCarrier(
|
|
138
|
+
kind=carrier.kind,
|
|
139
|
+
name=carrier.name,
|
|
140
|
+
how="automatic" if carrier.control else "guided",
|
|
141
|
+
networks=list(carrier.networks),
|
|
142
|
+
)
|
|
143
|
+
for carrier in carrier_catalog.known().values()
|
|
144
|
+
if carrier.kind in admitted
|
|
145
|
+
],
|
|
146
|
+
sells=sells,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Bringing an account the org already holds replaces its secret; a Twilio pair is tried first.
|
|
151
|
+
@router.put("/v1/carrier")
|
|
152
|
+
async def put_carrier(body: carriers.Account, key: NumbersKey, gateway: GatewayDep) -> CarrierRow:
|
|
153
|
+
"""Keep an account of the org: a Twilio account, a SIP peer, a WhatsApp number at Meta."""
|
|
154
|
+
connections = gateway.connections
|
|
155
|
+
declared = declared_networks(Carrier(org=key.org, account=body))
|
|
156
|
+
for network in declared or ():
|
|
157
|
+
carrier_networks.checked(network)
|
|
158
|
+
await verify_account(connections.http, body)
|
|
159
|
+
carrier = await carriers.put_carrier(connections.pool, connections.vault, key.org, body)
|
|
160
|
+
if declared is not None:
|
|
161
|
+
await carrier_networks.ask(connections.pool, key.org, carrier.id, declared)
|
|
162
|
+
await sip.readmit(connections, key.org)
|
|
163
|
+
return _carrier_row(carrier, await carrier_networks.of_org(connections.pool, key.org))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@router.delete("/v1/carrier", status_code=204)
|
|
167
|
+
async def drop_carrier(key: NumbersKey, gateway: GatewayDep, account: AccountAsked = None) -> None:
|
|
168
|
+
"""Forget an account; its numbers stay routed until each is let go."""
|
|
169
|
+
await carriers.drop_carrier(
|
|
170
|
+
gateway.connections.pool, gateway.connections.vault, key.org, account
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@router.get("/v1/numbers")
|
|
175
|
+
async def list_numbers(key: NumbersKey, gateway: GatewayDep) -> list[NumberRow]:
|
|
176
|
+
"""The org's numbers in the key's world, and the agent each reaches."""
|
|
177
|
+
records = await routes.records_of(gateway.connections.pool, key.org, key.env)
|
|
178
|
+
running_agents = _running(gateway, Scope(org=key.org, env=key.env))
|
|
179
|
+
rings = await number_path.rings_of(
|
|
180
|
+
gateway.connections, records, lambda record: record.route.agent in running_agents
|
|
181
|
+
)
|
|
182
|
+
return [
|
|
183
|
+
NumberRow(
|
|
184
|
+
route=record.route,
|
|
185
|
+
origin=record.origin,
|
|
186
|
+
rings=ringing,
|
|
187
|
+
last_call_at=record.last_call_at,
|
|
188
|
+
via=record.via,
|
|
189
|
+
account=record.account,
|
|
190
|
+
)
|
|
191
|
+
for record, ringing in zip(records, rings, strict=True)
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.get("/v1/numbers/{number}/path")
|
|
196
|
+
async def number_path_of(number: str, key: NumbersKey, gateway: GatewayDep) -> NumberPath:
|
|
197
|
+
"""What a call to the number goes through now: its carrier, the fence, the world, the agent."""
|
|
198
|
+
record = await routes.record_of(gateway.connections.pool, key.org, parse_e164(number))
|
|
199
|
+
if record is None or record.route.env != key.env:
|
|
200
|
+
raise NotFound(NO_SUCH_NUMBER.format(number=number, env=key.env))
|
|
201
|
+
running_agents = _running(gateway, Scope(org=key.org, env=key.env))
|
|
202
|
+
found = await number_path.path_of(
|
|
203
|
+
gateway.connections, record, running=record.route.agent in running_agents
|
|
204
|
+
)
|
|
205
|
+
return NumberPath(
|
|
206
|
+
number=str(record.route.number),
|
|
207
|
+
steps=[
|
|
208
|
+
PathStep(step=step.step, state=step.state, says=step.says, fix=step.fix)
|
|
209
|
+
for step in found.steps
|
|
210
|
+
],
|
|
211
|
+
rings=found.rings,
|
|
212
|
+
last_call_at=record.last_call_at,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@router.get("/v1/numbers/available")
|
|
217
|
+
async def list_available_numbers(
|
|
218
|
+
key: NumbersKey, gateway: GatewayDep, account: AccountAsked = None
|
|
219
|
+
) -> AvailableNumbers:
|
|
220
|
+
"""What the org's Twilio accounts own, and which of it this world imported."""
|
|
221
|
+
kind, owned = await numbers.owned_numbers(gateway.connections, _org_scope(key), account)
|
|
222
|
+
return AvailableNumbers(
|
|
223
|
+
kind=kind,
|
|
224
|
+
numbers=[
|
|
225
|
+
OwnedNumberRow(
|
|
226
|
+
number=owned_number.number,
|
|
227
|
+
name=owned_number.name,
|
|
228
|
+
imported=owned_number.imported,
|
|
229
|
+
account=owned_number.account,
|
|
230
|
+
)
|
|
231
|
+
for owned_number in owned
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@router.post("/v1/numbers")
|
|
237
|
+
async def import_number(
|
|
238
|
+
body: ImportNumberRequest, key: NumbersKey, gateway: GatewayDep, *, dry_run: DryRun = False
|
|
239
|
+
) -> ImportNumberResponse:
|
|
240
|
+
"""Hook a number at its account, admit it on the SFU in the key's world, route it."""
|
|
241
|
+
keys.check_agent(key.bearer, body.agent)
|
|
242
|
+
wanted = NumberImport(
|
|
243
|
+
scope=_org_scope(key),
|
|
244
|
+
agent=body.agent,
|
|
245
|
+
number=body.number,
|
|
246
|
+
channel=body.channel,
|
|
247
|
+
account=body.account,
|
|
248
|
+
hooked=body.hooked,
|
|
249
|
+
networks=tuple(body.networks),
|
|
250
|
+
via=body.via,
|
|
251
|
+
move=body.move,
|
|
252
|
+
)
|
|
253
|
+
plan = (
|
|
254
|
+
await numbers.plan_import(gateway.connections, wanted)
|
|
255
|
+
if dry_run
|
|
256
|
+
else await numbers.import_number(gateway.connections, wanted)
|
|
257
|
+
)
|
|
258
|
+
return ImportNumberResponse(route=plan.route, steps=plan.steps, dry_run=plan.dry_run)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@router.post("/v1/numbers/buy")
|
|
262
|
+
async def buy_number(
|
|
263
|
+
body: BuyNumberRequest, key: NumbersKey, gateway: GatewayDep, *, dry_run: DryRun = False
|
|
264
|
+
) -> ImportNumberResponse:
|
|
265
|
+
"""Buy a number on the box's account and hook it, counted against the world's stock."""
|
|
266
|
+
keys.check_agent(key.bearer, body.agent)
|
|
267
|
+
wanted = NumberPurchase(
|
|
268
|
+
scope=_org_scope(key),
|
|
269
|
+
agent=body.agent,
|
|
270
|
+
country=body.country,
|
|
271
|
+
area_code=body.area_code,
|
|
272
|
+
channel=body.channel,
|
|
273
|
+
)
|
|
274
|
+
plan = (
|
|
275
|
+
await numbers.plan_buy(gateway.connections, wanted)
|
|
276
|
+
if dry_run
|
|
277
|
+
else await numbers.buy_number(gateway.connections, wanted)
|
|
278
|
+
)
|
|
279
|
+
return ImportNumberResponse(route=plan.route, steps=plan.steps, dry_run=plan.dry_run)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@router.delete("/v1/numbers/{number}", status_code=204)
|
|
283
|
+
async def release_number(number: str, key: NumbersKey, gateway: GatewayDep) -> None:
|
|
284
|
+
"""Let the number go: its route and its admission; the account keeps it."""
|
|
285
|
+
await numbers.release(gateway.connections, key.org, parse_e164(number), key.env)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@router.put("/v1/numbers/{number}/env")
|
|
289
|
+
async def move_number(
|
|
290
|
+
number: str, body: MoveNumberRequest, key: NumbersKey, gateway: GatewayDep
|
|
291
|
+
) -> NumberRow:
|
|
292
|
+
"""Move the number into the other world: its row and the two rules."""
|
|
293
|
+
moved = await numbers.move(gateway.connections, key.org, parse_e164(number), body.env)
|
|
294
|
+
return NumberRow(route=moved.route, origin=moved.origin)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@router.get("/v1/carrier/outbound")
|
|
298
|
+
async def get_outbound_status(
|
|
299
|
+
key: NumbersKey, gateway: GatewayDep, account: AccountAsked = None
|
|
300
|
+
) -> OutboundStatus:
|
|
301
|
+
"""Whether the org can place a call, one sentence per thing missing, and its guards."""
|
|
302
|
+
readiness = await dialing.outbound_readiness(gateway.connections, _org_scope(key), account)
|
|
303
|
+
return OutboundStatus(
|
|
304
|
+
ready=readiness.ready,
|
|
305
|
+
kind=readiness.kind,
|
|
306
|
+
from_numbers=readiness.from_numbers,
|
|
307
|
+
steps_missing=readiness.steps_missing,
|
|
308
|
+
guards=DialGuards.model_validate(readiness.guards.model_dump()),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@router.post("/v1/carrier/outbound", response_model_exclude_unset=True)
|
|
313
|
+
async def provision_outbound(
|
|
314
|
+
key: NumbersKey, gateway: GatewayDep, *, account: AccountAsked = None, dry_run: DryRun = False
|
|
315
|
+
) -> ProvisionOutboundResponse:
|
|
316
|
+
"""Make an account dialable: Twilio's termination and a credential; a peer needs nothing."""
|
|
317
|
+
done = (
|
|
318
|
+
await dialing.plan_outbound(gateway.connections, key.org, key.env, account)
|
|
319
|
+
if dry_run
|
|
320
|
+
else await dialing.provision_outbound(gateway.connections, key.org, key.env, account)
|
|
321
|
+
)
|
|
322
|
+
if done.dry_run:
|
|
323
|
+
return ProvisionOutboundResponse(steps=done.steps, dry_run=True, ready=False)
|
|
324
|
+
return ProvisionOutboundResponse(
|
|
325
|
+
steps=done.steps, dry_run=False, ready=done.ready, trunk=done.trunk, address=done.address
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@router.post("/v1/agents/{slug}/dial", status_code=202)
|
|
330
|
+
async def dial_out(
|
|
331
|
+
slug: str, body: DialRequest, key: TalkKey, where: ScopeDep, gateway: GatewayDep
|
|
332
|
+
) -> DialResponse:
|
|
333
|
+
"""Place a call as the agent, after its guards: the call it became, before anything rings."""
|
|
334
|
+
if gateway.sockets.serving(where, slug, None) is None:
|
|
335
|
+
raise Conflict(NOBODY_HOLDING.format(agent=slug))
|
|
336
|
+
who = asked_by(key)
|
|
337
|
+
placement = Placement(
|
|
338
|
+
scope=where,
|
|
339
|
+
agent=slug,
|
|
340
|
+
to=body.to,
|
|
341
|
+
shown=body.from_,
|
|
342
|
+
asked_by=who,
|
|
343
|
+
today=today_in(gateway.connections.settings.timezone),
|
|
344
|
+
at=datetime.now(UTC),
|
|
345
|
+
own_phone=gateway.sockets.phone_of(where.env, body.to) == who,
|
|
346
|
+
consent=body.consent,
|
|
347
|
+
)
|
|
348
|
+
placed = await dialing.place_call(
|
|
349
|
+
gateway.connections,
|
|
350
|
+
gateway.logs,
|
|
351
|
+
placement,
|
|
352
|
+
gateway.offering,
|
|
353
|
+
running=gateway.live.running(where.org, where.env),
|
|
354
|
+
)
|
|
355
|
+
return DialResponse.model_validate(
|
|
356
|
+
{
|
|
357
|
+
"call": placed.call,
|
|
358
|
+
"agent": slug,
|
|
359
|
+
"to": placed.to,
|
|
360
|
+
"from": placed.shown,
|
|
361
|
+
"env": where.env,
|
|
362
|
+
"log_token": tokens.log_token(gateway.signer, placed.call, body.log),
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# The worker's, for a leg into a live call and for the leg a dial placed.
|
|
368
|
+
@router.get("/v1/agents/{slug}/outbound-trunk")
|
|
369
|
+
async def get_leg_trunk(
|
|
370
|
+
slug: str,
|
|
371
|
+
key: WorkerKey,
|
|
372
|
+
where: ScopeDep,
|
|
373
|
+
gateway: GatewayDep,
|
|
374
|
+
query: Annotated[LegTrunkQuery, Query()],
|
|
375
|
+
) -> LegTrunkResponse:
|
|
376
|
+
"""The leg's trunk inline, after the shape and the pace; a dial's own first leg passes."""
|
|
377
|
+
dial = Dial(where, slug, query.to, query.shown, asked_by(key), query.call, datetime.now(UTC))
|
|
378
|
+
return LegTrunkResponse(trunk=await dialing.leg_trunk(gateway.connections, dial))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# The worker's, before it sends the caller on: a REFER dials nothing of the box's, but the carrier
|
|
382
|
+
# dials it on the org's bill, so it passes the same guards as a warm transfer's leg.
|
|
383
|
+
@router.post("/v1/agents/{slug}/cold-transfer", status_code=204)
|
|
384
|
+
async def judge_cold_transfer(
|
|
385
|
+
slug: str,
|
|
386
|
+
key: WorkerKey,
|
|
387
|
+
where: ScopeDep,
|
|
388
|
+
gateway: GatewayDep,
|
|
389
|
+
query: Annotated[ColdTransferQuery, Query()],
|
|
390
|
+
) -> None:
|
|
391
|
+
"""A cold transfer's leg, past the shape and the pace, its ledger row written."""
|
|
392
|
+
dial = Dial(where, slug, query.to, None, asked_by(key), query.call, datetime.now(UTC))
|
|
393
|
+
await guard_second_leg(gateway.connections.pool, dial)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ── consent and the do-not-call list ──
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@router.post("/v1/org/consents")
|
|
400
|
+
async def record_consent(
|
|
401
|
+
body: RecordConsent, key: TalkKey, where: ScopeDep, gateway: GatewayDep
|
|
402
|
+
) -> ConsentHistory:
|
|
403
|
+
"""Write one fact about a number, a consent or an opt-out; what stands for it after."""
|
|
404
|
+
given = Given(
|
|
405
|
+
kind=body.kind,
|
|
406
|
+
source=body.source,
|
|
407
|
+
given_by=asked_by(key),
|
|
408
|
+
text=body.text,
|
|
409
|
+
evidence=body.evidence,
|
|
410
|
+
)
|
|
411
|
+
pool = gateway.connections.pool
|
|
412
|
+
await consents.give(pool, where, body.number, given)
|
|
413
|
+
return await consents.history(pool, where, body.number)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@router.get("/v1/org/consents/{number}")
|
|
417
|
+
async def consent_of(
|
|
418
|
+
number: str, _key: CallsKey, where: ScopeDep, gateway: GatewayDep
|
|
419
|
+
) -> ConsentHistory:
|
|
420
|
+
"""What stands for the number in the key's world, and every fact about it, newest first."""
|
|
421
|
+
return await consents.history(gateway.connections.pool, where, number)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# An opt-out is a fact, not a deletion: the consent before it stays in the history.
|
|
425
|
+
@router.delete("/v1/org/consents/{number}")
|
|
426
|
+
async def opt_out(
|
|
427
|
+
number: str, key: TalkKey, where: ScopeDep, gateway: GatewayDep
|
|
428
|
+
) -> ConsentHistory:
|
|
429
|
+
"""Put the number on the do-not-call list: no call of the org's reaches it from now on."""
|
|
430
|
+
given = Given(kind="opt_out", source=ASKED_BY_HAND, given_by=asked_by(key))
|
|
431
|
+
pool = gateway.connections.pool
|
|
432
|
+
await consents.give(pool, where, number, given)
|
|
433
|
+
return await consents.history(pool, where, number)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@router.get("/v1/org/dnc")
|
|
437
|
+
async def do_not_call(
|
|
438
|
+
_key: CallsKey, where: ScopeDep, gateway: GatewayDep, after: str | None = None
|
|
439
|
+
) -> DoNotCall:
|
|
440
|
+
"""The world's do-not-call list, newest first, a page after the cursor."""
|
|
441
|
+
return await consents.do_not_call(gateway.connections.pool, where, after=after)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@router.post("/v1/org/dnc")
|
|
445
|
+
async def import_do_not_call(
|
|
446
|
+
body: DoNotCallImport, key: TalkKey, where: ScopeDep, gateway: GatewayDep
|
|
447
|
+
) -> DoNotCallImported:
|
|
448
|
+
"""Numbers the org's own list or its Registry scrub says not to call, onto the list at once."""
|
|
449
|
+
given = Given(kind="opt_out", source=body.source, given_by=asked_by(key))
|
|
450
|
+
added, refused = await consents.opt_out_many(
|
|
451
|
+
gateway.connections.pool, where, body.numbers, given
|
|
452
|
+
)
|
|
453
|
+
return DoNotCallImported(added=added, refused=refused)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _running(gateway: Gateway, scope: Scope) -> set[str]:
|
|
457
|
+
"""The agents a process holds in the org's world now, in any of its scopes."""
|
|
458
|
+
return {registration.slug for registration in gateway.sockets.holding(scope, every_corner=True)}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _carrier_row(carrier: Carrier, asks: list[NetworkAsk]) -> CarrierRow:
|
|
462
|
+
"""An account as the doors send it, a peer's networks with the operator's answer to each."""
|
|
463
|
+
return CarrierRow(
|
|
464
|
+
kind=carrier.account.kind,
|
|
465
|
+
account=carrier.id,
|
|
466
|
+
label=carrier.account.label,
|
|
467
|
+
networks=[
|
|
468
|
+
NetworkRow(network=ask.network, state=ask.state)
|
|
469
|
+
for ask in asks
|
|
470
|
+
if ask.source == carrier.id
|
|
471
|
+
],
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _org_scope(key: Acting) -> Scope:
|
|
476
|
+
return Scope(key.org, key.env)
|