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,27 @@
|
|
|
1
|
+
-- A lexicon is one agent's. Every row the org wrote is copied to each agent the org has in that
|
|
2
|
+
-- world, keeping its version so a call's lexicon_version still reads it; then the org's rows go.
|
|
3
|
+
-- The agents an org has: those with settings in the world, and those whose own log it claimed
|
|
4
|
+
-- (an agent's log carries no world, so it counts in both).
|
|
5
|
+
|
|
6
|
+
ALTER TABLE lexicon ADD COLUMN agent text;
|
|
7
|
+
ALTER TABLE lexicon DROP CONSTRAINT lexicon_pkey;
|
|
8
|
+
|
|
9
|
+
WITH org_agents AS (
|
|
10
|
+
SELECT DISTINCT org, env, agent FROM agent_config
|
|
11
|
+
UNION
|
|
12
|
+
SELECT head.org, world.env, head.agent
|
|
13
|
+
FROM call_log_head head
|
|
14
|
+
CROSS JOIN (VALUES ('production'), ('sandbox')) AS world (env)
|
|
15
|
+
WHERE head.call IS NULL AND head.org IS NOT NULL AND head.agent IS NOT NULL
|
|
16
|
+
)
|
|
17
|
+
INSERT INTO lexicon (org, env, holder, agent, version, said, heard, author, note, set_at)
|
|
18
|
+
SELECT lexicon.org, lexicon.env, lexicon.holder, org_agents.agent, lexicon.version, lexicon.said,
|
|
19
|
+
lexicon.heard, lexicon.author, lexicon.note, lexicon.set_at
|
|
20
|
+
FROM lexicon
|
|
21
|
+
JOIN org_agents ON org_agents.org = lexicon.org AND org_agents.env = lexicon.env
|
|
22
|
+
WHERE lexicon.agent IS NULL;
|
|
23
|
+
|
|
24
|
+
DELETE FROM lexicon WHERE agent IS NULL;
|
|
25
|
+
|
|
26
|
+
ALTER TABLE lexicon ALTER COLUMN agent SET NOT NULL;
|
|
27
|
+
ALTER TABLE lexicon ADD CONSTRAINT lexicon_pkey PRIMARY KEY (org, env, holder, agent, version);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- A persona is one agent's: one copy per agent it named, or, named for every agent, per agent the
|
|
2
|
+
-- org tuned in either world or whose own log it claimed. An org with no agent loses the row.
|
|
3
|
+
|
|
4
|
+
ALTER TABLE agent_personas ADD COLUMN agent text;
|
|
5
|
+
ALTER TABLE agent_personas DROP CONSTRAINT agent_personas_pkey;
|
|
6
|
+
|
|
7
|
+
WITH agents_of_the_org AS (
|
|
8
|
+
SELECT DISTINCT org, agent FROM agent_config
|
|
9
|
+
UNION
|
|
10
|
+
SELECT DISTINCT org, agent FROM call_log_head
|
|
11
|
+
WHERE call IS NULL AND org IS NOT NULL AND agent IS NOT NULL
|
|
12
|
+
),
|
|
13
|
+
fanned AS (
|
|
14
|
+
SELECT persona.*, named.agent AS its_agent
|
|
15
|
+
FROM agent_personas persona, unnest(persona.agents) AS named(agent)
|
|
16
|
+
WHERE persona.agent IS NULL AND persona.agents <> '{}'
|
|
17
|
+
UNION ALL
|
|
18
|
+
SELECT persona.*, owned.agent AS its_agent
|
|
19
|
+
FROM agent_personas persona JOIN agents_of_the_org owned ON owned.org = persona.org
|
|
20
|
+
WHERE persona.agent IS NULL AND persona.agents = '{}'
|
|
21
|
+
)
|
|
22
|
+
INSERT INTO agent_personas (org, agent, name, about, goal, style, facts, state, author, set_at,
|
|
23
|
+
llm, tts, voice, accepts_when, declines_when, agents)
|
|
24
|
+
SELECT org, its_agent, name, about, goal, style, facts, state, author, set_at,
|
|
25
|
+
llm, tts, voice, accepts_when, declines_when, '{}'
|
|
26
|
+
FROM fanned;
|
|
27
|
+
|
|
28
|
+
DELETE FROM agent_personas WHERE agent IS NULL;
|
|
29
|
+
ALTER TABLE agent_personas ALTER COLUMN agent SET NOT NULL;
|
|
30
|
+
ALTER TABLE agent_personas ADD CONSTRAINT agent_personas_pkey PRIMARY KEY (org, agent, name);
|
|
31
|
+
ALTER TABLE agent_personas DROP COLUMN agents;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- An agent's own judges: a question the org wrote about the agent's job, asked at hang-up beside
|
|
2
|
+
-- the runtime's panel. One list per agent for both worlds, as its personas are.
|
|
3
|
+
CREATE TABLE agent_judges (
|
|
4
|
+
org text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
5
|
+
agent text NOT NULL,
|
|
6
|
+
name text NOT NULL,
|
|
7
|
+
question text NOT NULL,
|
|
8
|
+
runs_on text NOT NULL DEFAULT 'every-call' CHECK (runs_on IN ('every-call', 'simulations')),
|
|
9
|
+
author text NOT NULL DEFAULT '',
|
|
10
|
+
set_at timestamptz NOT NULL DEFAULT now(),
|
|
11
|
+
PRIMARY KEY (org, agent, name)
|
|
12
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- Erasure: the one path that may delete from call_log, and the trail of what it deleted.
|
|
2
|
+
-- The trigger still refuses every UPDATE, and every DELETE a door of the runtime could send; only
|
|
3
|
+
-- a transaction that set pinecall.erasing (tenancy/erasure.py, and nothing else) gets through.
|
|
4
|
+
|
|
5
|
+
CREATE OR REPLACE FUNCTION call_log_refuses_the_statement() RETURNS trigger
|
|
6
|
+
LANGUAGE plpgsql
|
|
7
|
+
AS $$
|
|
8
|
+
begin
|
|
9
|
+
if tg_op = 'DELETE' and current_setting('pinecall.erasing', true) = 'on' then
|
|
10
|
+
return null;
|
|
11
|
+
end if;
|
|
12
|
+
raise exception 'call_log is append-only: % is refused', tg_op
|
|
13
|
+
using errcode = 'restrict_violation';
|
|
14
|
+
end;
|
|
15
|
+
$$;
|
|
16
|
+
|
|
17
|
+
-- No foreign key to orgs: the row that says an org was erased outlives the org.
|
|
18
|
+
CREATE TABLE erasures (
|
|
19
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
20
|
+
org text NOT NULL,
|
|
21
|
+
env text,
|
|
22
|
+
what text NOT NULL CHECK (what IN ('call', 'contact', 'org')),
|
|
23
|
+
subject text NOT NULL,
|
|
24
|
+
asked_by text NOT NULL,
|
|
25
|
+
calls integer NOT NULL,
|
|
26
|
+
entries bigint NOT NULL,
|
|
27
|
+
memories integer NOT NULL,
|
|
28
|
+
recordings integer NOT NULL,
|
|
29
|
+
at timestamptz NOT NULL DEFAULT now()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE INDEX erasures_by_org ON erasures (org, at DESC);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- An org's own compliance settings, one row. Retention first: how many days a sealed call is kept
|
|
2
|
+
-- before the nightly run erases it (tenancy/retention.py). No row, or no days, is kept until the
|
|
3
|
+
-- org erases it: what a self-hosted box wants unless its operator says otherwise.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE org_policy (
|
|
6
|
+
org text PRIMARY KEY REFERENCES orgs(id) ON DELETE CASCADE,
|
|
7
|
+
retention_days integer CHECK (retention_days > 0),
|
|
8
|
+
set_by text NOT NULL,
|
|
9
|
+
set_at timestamptz NOT NULL DEFAULT now()
|
|
10
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- The org's outbound calling rules, beside its retention: the hours of the called number's day a
|
|
2
|
+
-- call may ring, and how many times one number may be rung in a day. Unset is the platform's
|
|
3
|
+
-- default by destination (tenancy/dial_policy.py): the US floor for a +1 number, nothing elsewhere.
|
|
4
|
+
|
|
5
|
+
ALTER TABLE org_policy
|
|
6
|
+
ADD COLUMN calling_from smallint CHECK (calling_from BETWEEN 0 AND 23),
|
|
7
|
+
ADD COLUMN calling_until smallint CHECK (calling_until BETWEEN 1 AND 24),
|
|
8
|
+
ADD COLUMN per_number_day integer CHECK (per_number_day > 0),
|
|
9
|
+
ADD CONSTRAINT org_policy_calling_hours_whole CHECK (
|
|
10
|
+
(calling_from IS NULL AND calling_until IS NULL)
|
|
11
|
+
OR (calling_from IS NOT NULL AND calling_until IS NOT NULL AND calling_from < calling_until)
|
|
12
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Consent and the org's do-not-call list, per world. A row is a fact about a number, never
|
|
2
|
+
-- changed: a consent given, or an opt-out. What stands is the newest row, so a person who opted out
|
|
3
|
+
-- and later consented again is called again, and the history of both is kept.
|
|
4
|
+
-- An erasure of the contact leaves these rows: an opt-out has to outlive the person's data, or the
|
|
5
|
+
-- next list the org imports calls them again.
|
|
6
|
+
|
|
7
|
+
CREATE TABLE contact_consents (
|
|
8
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
9
|
+
org text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
10
|
+
env text NOT NULL CHECK (env IN ('production', 'sandbox')),
|
|
11
|
+
number text NOT NULL,
|
|
12
|
+
kind text NOT NULL CHECK (kind IN ('express', 'written', 'opt_out')),
|
|
13
|
+
source text NOT NULL,
|
|
14
|
+
text text,
|
|
15
|
+
evidence text,
|
|
16
|
+
given_by text NOT NULL,
|
|
17
|
+
call text,
|
|
18
|
+
given_at timestamptz NOT NULL DEFAULT now()
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX contact_consents_by_number ON contact_consents (org, env, number, given_at DESC, id DESC);
|
|
22
|
+
|
|
23
|
+
-- Consent on file for a call to any country, not only a +1 number (tenancy/dial_policy.py).
|
|
24
|
+
ALTER TABLE org_policy ADD COLUMN consent_everywhere boolean NOT NULL DEFAULT false;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- What a call says before its greeting. `disclosure` is the org's own opening sentence for an
|
|
2
|
+
-- outbound call: NULL is the platform's ("an automated assistant calling on behalf of <org>"), ''
|
|
3
|
+
-- is none. `recording_notice` says "this call may be recorded" on every recorded spoken call.
|
|
4
|
+
|
|
5
|
+
ALTER TABLE org_policy
|
|
6
|
+
ADD COLUMN disclosure text,
|
|
7
|
+
ADD COLUMN recording_notice boolean NOT NULL DEFAULT true;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- A phone call's detail record, kept when the call is erased: the numbers, the times and how it
|
|
2
|
+
-- ended — no name, no words, no outcome. A carrier's traceback asks who placed a call, when and
|
|
3
|
+
-- from which number, and an erasure must not take the answer. The nightly retention run forgets a
|
|
4
|
+
-- record 24 months after its call started. No foreign key: a record outlives its org, as the
|
|
5
|
+
-- erasure trail does.
|
|
6
|
+
|
|
7
|
+
CREATE TABLE call_records (
|
|
8
|
+
call text PRIMARY KEY,
|
|
9
|
+
org text,
|
|
10
|
+
env text CHECK (env IN ('production', 'sandbox')),
|
|
11
|
+
direction text,
|
|
12
|
+
from_number text,
|
|
13
|
+
to_number text,
|
|
14
|
+
started_at double precision,
|
|
15
|
+
ended_at double precision,
|
|
16
|
+
end_reason text,
|
|
17
|
+
erased_at timestamptz NOT NULL DEFAULT now()
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE INDEX call_records_from ON call_records (from_number, started_at);
|
|
21
|
+
CREATE INDEX call_records_to ON call_records (to_number, started_at);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Who read what: a person reading a call's log or recording, the operator reading one off the box
|
|
2
|
+
-- or looking a number up for a traceback. The access log a breach notification is written from.
|
|
3
|
+
-- A row names the call or the number and never what it said; one row per reader, subject and kind
|
|
4
|
+
-- an hour, so a page that reads a call twenty times says it once.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE reads (
|
|
7
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
8
|
+
org text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
9
|
+
env text CHECK (env IN ('production', 'sandbox')),
|
|
10
|
+
subject text NOT NULL,
|
|
11
|
+
what text NOT NULL CHECK (what IN ('log', 'recording', 'traceback')),
|
|
12
|
+
reader text NOT NULL,
|
|
13
|
+
at timestamptz NOT NULL DEFAULT now()
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX reads_by_org ON reads (org, at DESC);
|
|
17
|
+
CREATE INDEX reads_by_subject ON reads (org, subject, at DESC);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- What the box hosts for an org: an app it runs from sources the org uploaded. One row per app
|
|
2
|
+
-- and world, with the server's token minted for its process, sealed; every upload is a release,
|
|
3
|
+
-- numbered from 1 and never edited. The secrets are what its process is started with.
|
|
4
|
+
CREATE TABLE hosted_apps (
|
|
5
|
+
org text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
6
|
+
env text NOT NULL CHECK (env IN ('production', 'sandbox')),
|
|
7
|
+
name text NOT NULL,
|
|
8
|
+
key_fingerprint text NOT NULL,
|
|
9
|
+
sealed_key text NOT NULL,
|
|
10
|
+
created_by text NOT NULL DEFAULT '',
|
|
11
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
12
|
+
PRIMARY KEY (org, env, name)
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE hosted_releases (
|
|
16
|
+
org text NOT NULL,
|
|
17
|
+
env text NOT NULL,
|
|
18
|
+
name text NOT NULL,
|
|
19
|
+
release integer NOT NULL CHECK (release >= 1),
|
|
20
|
+
source bytea NOT NULL,
|
|
21
|
+
sha256 text NOT NULL,
|
|
22
|
+
bytes integer NOT NULL,
|
|
23
|
+
author text NOT NULL DEFAULT '',
|
|
24
|
+
note text NOT NULL DEFAULT '',
|
|
25
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
PRIMARY KEY (org, env, name, release),
|
|
27
|
+
FOREIGN KEY (org, env, name) REFERENCES hosted_apps (org, env, name) ON DELETE CASCADE
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE org_secrets (
|
|
31
|
+
org text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
32
|
+
env text NOT NULL CHECK (env IN ('production', 'sandbox')),
|
|
33
|
+
name text NOT NULL,
|
|
34
|
+
sealed text NOT NULL,
|
|
35
|
+
set_by text NOT NULL DEFAULT '',
|
|
36
|
+
set_at timestamptz NOT NULL DEFAULT now(),
|
|
37
|
+
PRIMARY KEY (org, env, name)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
-- How many apps the box hosts for the org in a world; NULL is no limit, 0 hosts none.
|
|
41
|
+
ALTER TABLE quotas ADD COLUMN hosted_apps integer;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- What the runner last said of a hosted app: the release it serves now and under which host
|
|
2
|
+
-- name, and the host name that failed to build or start, with why. A host name is one release
|
|
3
|
+
-- under one set of the org's secrets, so a failure is never tried again until either changes.
|
|
4
|
+
ALTER TABLE hosted_apps
|
|
5
|
+
ADD COLUMN live_release integer,
|
|
6
|
+
ADD COLUMN live_host text,
|
|
7
|
+
ADD COLUMN failed_host text,
|
|
8
|
+
ADD COLUMN failed_why text NOT NULL DEFAULT '',
|
|
9
|
+
ADD COLUMN reported_by text NOT NULL DEFAULT '',
|
|
10
|
+
ADD COLUMN reported_at timestamptz;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- A worker writes its call's entries in batches and retries a batch whose answer it lost. The head
|
|
2
|
+
-- row counts what it took from that writer, so a retry is recognised and answered with the seqs it
|
|
3
|
+
-- was given instead of being written again under new ones. The gateway's own writes move neither.
|
|
4
|
+
|
|
5
|
+
ALTER TABLE call_log_head
|
|
6
|
+
ADD COLUMN written bigint NOT NULL DEFAULT 0,
|
|
7
|
+
ADD COLUMN written_seq bigint;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- What happens to a hosted app while it runs. Stopped: the runner is no longer told to run it, and
|
|
2
|
+
-- its releases and token stay. Its logs: the last lines the runner read, when a person asked for
|
|
3
|
+
-- them. Metered: when its time serving was last counted, and the time itself, per app and UTC day,
|
|
4
|
+
-- which outlives the app because it is what the org is billed for.
|
|
5
|
+
ALTER TABLE hosted_apps
|
|
6
|
+
ADD COLUMN stopped_at timestamptz,
|
|
7
|
+
ADD COLUMN stopped_by text NOT NULL DEFAULT '',
|
|
8
|
+
ADD COLUMN logs_asked_at timestamptz,
|
|
9
|
+
ADD COLUMN logs text NOT NULL DEFAULT '',
|
|
10
|
+
ADD COLUMN logs_host text,
|
|
11
|
+
ADD COLUMN logs_at timestamptz,
|
|
12
|
+
ADD COLUMN metered_at timestamptz;
|
|
13
|
+
|
|
14
|
+
CREATE TABLE hosted_usage (
|
|
15
|
+
org text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
16
|
+
env text NOT NULL CHECK (env IN ('production', 'sandbox')),
|
|
17
|
+
name text NOT NULL,
|
|
18
|
+
day date NOT NULL,
|
|
19
|
+
seconds double precision NOT NULL DEFAULT 0 CHECK (seconds >= 0),
|
|
20
|
+
PRIMARY KEY (org, env, name, day)
|
|
21
|
+
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- A sealed log is the database's word, not only the application's: call_log refuses an entry on a
|
|
2
|
+
-- sealed log but its verdict (call.score, which a judge writes again after the seal), and
|
|
3
|
+
-- call_log_head refuses opening a sealed log again. Erasure deletes, and neither trigger fires on a
|
|
4
|
+
-- delete. Each CREATE TRIGGER takes a SHARE ROW EXCLUSIVE lock on its table, which waits for the
|
|
5
|
+
-- writes in flight and holds new ones for the instant it takes; the runner gives up after 1 s.
|
|
6
|
+
|
|
7
|
+
CREATE FUNCTION call_log_refuses_a_sealed_log() RETURNS trigger
|
|
8
|
+
LANGUAGE plpgsql
|
|
9
|
+
AS $$
|
|
10
|
+
begin
|
|
11
|
+
if exists (
|
|
12
|
+
select 1 from written join call_log_head head on head.log = written.log
|
|
13
|
+
where head.sealed and written.type <> 'call.score'
|
|
14
|
+
) then
|
|
15
|
+
raise exception 'call_log: a sealed log takes no entry but its call.score'
|
|
16
|
+
using errcode = 'restrict_violation';
|
|
17
|
+
end if;
|
|
18
|
+
return null;
|
|
19
|
+
end;
|
|
20
|
+
$$;
|
|
21
|
+
|
|
22
|
+
-- Once per statement, over every row it wrote, never once per row.
|
|
23
|
+
CREATE TRIGGER call_log_refuses_a_sealed_log AFTER INSERT ON call_log
|
|
24
|
+
REFERENCING NEW TABLE AS written
|
|
25
|
+
FOR EACH STATEMENT EXECUTE FUNCTION call_log_refuses_a_sealed_log();
|
|
26
|
+
|
|
27
|
+
CREATE FUNCTION call_log_head_stays_sealed() RETURNS trigger
|
|
28
|
+
LANGUAGE plpgsql
|
|
29
|
+
AS $$
|
|
30
|
+
begin
|
|
31
|
+
raise exception 'call_log_head: log % is sealed and is never opened again', old.log
|
|
32
|
+
using errcode = 'restrict_violation';
|
|
33
|
+
end;
|
|
34
|
+
$$;
|
|
35
|
+
|
|
36
|
+
-- The WHEN is read without calling the function: an update that leaves the seal alone costs nothing.
|
|
37
|
+
CREATE TRIGGER call_log_head_stays_sealed BEFORE UPDATE ON call_log_head
|
|
38
|
+
FOR EACH ROW WHEN (old.sealed AND NOT new.sealed)
|
|
39
|
+
EXECUTE FUNCTION call_log_head_stays_sealed();
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
-- What each org used in a world, a row per calendar month (UTC) of its calls' summaries: admission
|
|
2
|
+
-- reads the org's rows instead of every summary it ever wrote. The database keeps them, whichever
|
|
3
|
+
-- release writes: a summary adds its usage in the transaction that writes it, an erasure that
|
|
4
|
+
-- deletes it takes it away (while its head still names the org), and a log whose org or world
|
|
5
|
+
-- changes carries its summaries' usage over. The fold mirrors `_used_by_a_call` in
|
|
6
|
+
-- pinecall/log/reduce.py; `pinecall-runtime usage rebuild` refolds the table from the log in
|
|
7
|
+
-- Python, and the suite holds the two to each other.
|
|
8
|
+
--
|
|
9
|
+
-- Locks and time: the CREATE TRIGGERs take SHARE ROW EXCLUSIVE on call_log and call_log_head,
|
|
10
|
+
-- held to this transaction's commit, so every append waits while it runs and none is written
|
|
11
|
+
-- between the backfill's read and the trigger that counts the next one (the runner gives up
|
|
12
|
+
-- waiting for the locks after 1 s). The backfill reads every summary once through
|
|
13
|
+
-- call_log_metered (type, position) and folds it: with N summaries, a few microseconds each
|
|
14
|
+
-- (production today holds a few thousand: well under a second of appends held). An append that
|
|
15
|
+
-- waits past its statement timeout is answered 503, and a worker retries it.
|
|
16
|
+
|
|
17
|
+
CREATE TABLE usage_totals (
|
|
18
|
+
org text NOT NULL,
|
|
19
|
+
env text NOT NULL,
|
|
20
|
+
period date NOT NULL,
|
|
21
|
+
calls bigint NOT NULL DEFAULT 0,
|
|
22
|
+
minutes double precision NOT NULL DEFAULT 0,
|
|
23
|
+
messages bigint NOT NULL DEFAULT 0,
|
|
24
|
+
input_tokens bigint NOT NULL DEFAULT 0,
|
|
25
|
+
output_tokens bigint NOT NULL DEFAULT 0,
|
|
26
|
+
characters bigint NOT NULL DEFAULT 0,
|
|
27
|
+
cost_usd double precision NOT NULL DEFAULT 0,
|
|
28
|
+
PRIMARY KEY (org, env, period)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- A number the summary holds, or 0 where it holds none: a wrong shape never refuses an append.
|
|
32
|
+
CREATE FUNCTION usage_number(value jsonb) RETURNS numeric
|
|
33
|
+
LANGUAGE sql IMMUTABLE
|
|
34
|
+
AS $$ SELECT CASE WHEN jsonb_typeof(value) = 'number' THEN value::numeric ELSE 0 END $$;
|
|
35
|
+
|
|
36
|
+
CREATE FUNCTION usage_rows(summary jsonb, kind text) RETURNS SETOF jsonb
|
|
37
|
+
LANGUAGE sql IMMUTABLE
|
|
38
|
+
AS $$
|
|
39
|
+
SELECT row FROM jsonb_array_elements(
|
|
40
|
+
CASE WHEN jsonb_typeof(summary -> 'usage') = 'array' THEN summary -> 'usage'
|
|
41
|
+
ELSE '[]'::jsonb END) AS row
|
|
42
|
+
WHERE row ->> 'type' = kind
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
-- One summary's usage, as `_used_by_a_call` counts it.
|
|
46
|
+
CREATE FUNCTION summary_usage(
|
|
47
|
+
summary jsonb,
|
|
48
|
+
OUT minutes double precision, OUT messages bigint, OUT input_tokens bigint,
|
|
49
|
+
OUT output_tokens bigint, OUT characters bigint, OUT cost_usd double precision
|
|
50
|
+
)
|
|
51
|
+
LANGUAGE sql IMMUTABLE
|
|
52
|
+
AS $$
|
|
53
|
+
SELECT usage_number(summary -> 'duration_s')::float8 / 60,
|
|
54
|
+
usage_number(summary -> 'turns')::bigint,
|
|
55
|
+
coalesce((SELECT sum(usage_number(row -> 'input_tokens'))
|
|
56
|
+
FROM usage_rows(summary, 'llm_usage') row), 0)::bigint,
|
|
57
|
+
coalesce((SELECT sum(usage_number(row -> 'output_tokens'))
|
|
58
|
+
FROM usage_rows(summary, 'llm_usage') row), 0)::bigint,
|
|
59
|
+
coalesce((SELECT sum(usage_number(row -> 'characters_count'))
|
|
60
|
+
FROM usage_rows(summary, 'tts_usage') row), 0)::bigint,
|
|
61
|
+
usage_number(summary -> 'cost' -> 'usd')::float8
|
|
62
|
+
$$;
|
|
63
|
+
|
|
64
|
+
CREATE FUNCTION usage_period(ts double precision) RETURNS date
|
|
65
|
+
LANGUAGE sql IMMUTABLE
|
|
66
|
+
AS $$ SELECT date_trunc('month', to_timestamp(ts) AT TIME ZONE 'UTC')::date $$;
|
|
67
|
+
|
|
68
|
+
-- Added to, or taken from with sign -1, by the summaries of logs as their heads name them now.
|
|
69
|
+
CREATE FUNCTION usage_counted(summaries jsonb, sign integer) RETURNS void
|
|
70
|
+
LANGUAGE sql
|
|
71
|
+
AS $$
|
|
72
|
+
INSERT INTO usage_totals AS total
|
|
73
|
+
(org, env, period, calls, minutes, messages, input_tokens, output_tokens, characters,
|
|
74
|
+
cost_usd)
|
|
75
|
+
SELECT one ->> 'org', one ->> 'env', usage_period((one ->> 'ts')::float8), sign * count(*),
|
|
76
|
+
sign * sum(used.minutes), sign * sum(used.messages), sign * sum(used.input_tokens),
|
|
77
|
+
sign * sum(used.output_tokens), sign * sum(used.characters), sign * sum(used.cost_usd)
|
|
78
|
+
FROM jsonb_array_elements(summaries) AS one
|
|
79
|
+
CROSS JOIN LATERAL summary_usage(one -> 'data') AS used
|
|
80
|
+
WHERE one ->> 'org' IS NOT NULL AND one ->> 'env' IS NOT NULL
|
|
81
|
+
GROUP BY 1, 2, 3
|
|
82
|
+
ORDER BY 1, 2, 3
|
|
83
|
+
ON CONFLICT (org, env, period) DO UPDATE SET
|
|
84
|
+
calls = total.calls + excluded.calls,
|
|
85
|
+
minutes = total.minutes + excluded.minutes,
|
|
86
|
+
messages = total.messages + excluded.messages,
|
|
87
|
+
input_tokens = total.input_tokens + excluded.input_tokens,
|
|
88
|
+
output_tokens = total.output_tokens + excluded.output_tokens,
|
|
89
|
+
characters = total.characters + excluded.characters,
|
|
90
|
+
cost_usd = total.cost_usd + excluded.cost_usd
|
|
91
|
+
$$;
|
|
92
|
+
|
|
93
|
+
-- Most statements write no summary: they leave after one look at their rows.
|
|
94
|
+
CREATE FUNCTION usage_of_summaries_written() RETURNS trigger
|
|
95
|
+
LANGUAGE plpgsql
|
|
96
|
+
AS $$
|
|
97
|
+
begin
|
|
98
|
+
if not exists (select 1 from summaries where type = 'call.summary') then
|
|
99
|
+
return null;
|
|
100
|
+
end if;
|
|
101
|
+
perform usage_counted(coalesce(jsonb_agg(jsonb_build_object(
|
|
102
|
+
'org', head.org, 'env', head.env, 'ts', entry.ts, 'data', entry.data)), '[]'), 1)
|
|
103
|
+
from summaries entry join call_log_head head on head.log = entry.log
|
|
104
|
+
where entry.type = 'call.summary';
|
|
105
|
+
return null;
|
|
106
|
+
end;
|
|
107
|
+
$$;
|
|
108
|
+
|
|
109
|
+
CREATE FUNCTION usage_of_summaries_erased() RETURNS trigger
|
|
110
|
+
LANGUAGE plpgsql
|
|
111
|
+
AS $$
|
|
112
|
+
begin
|
|
113
|
+
perform usage_counted(coalesce(jsonb_agg(jsonb_build_object(
|
|
114
|
+
'org', head.org, 'env', head.env, 'ts', entry.ts, 'data', entry.data)), '[]'), -1)
|
|
115
|
+
from summaries entry join call_log_head head on head.log = entry.log
|
|
116
|
+
where entry.type = 'call.summary';
|
|
117
|
+
return null;
|
|
118
|
+
end;
|
|
119
|
+
$$;
|
|
120
|
+
|
|
121
|
+
CREATE FUNCTION usage_of_a_log_moved() RETURNS trigger
|
|
122
|
+
LANGUAGE plpgsql
|
|
123
|
+
AS $$
|
|
124
|
+
begin
|
|
125
|
+
perform usage_counted(coalesce(jsonb_agg(jsonb_build_object(
|
|
126
|
+
'org', old.org, 'env', old.env, 'ts', entry.ts, 'data', entry.data)), '[]'), -1),
|
|
127
|
+
usage_counted(coalesce(jsonb_agg(jsonb_build_object(
|
|
128
|
+
'org', new.org, 'env', new.env, 'ts', entry.ts, 'data', entry.data)), '[]'), 1)
|
|
129
|
+
from call_log entry
|
|
130
|
+
where entry.log = new.log and entry.type = 'call.summary';
|
|
131
|
+
return null;
|
|
132
|
+
end;
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
CREATE TRIGGER usage_of_summaries_written AFTER INSERT ON call_log
|
|
136
|
+
REFERENCING NEW TABLE AS summaries
|
|
137
|
+
FOR EACH STATEMENT EXECUTE FUNCTION usage_of_summaries_written();
|
|
138
|
+
|
|
139
|
+
-- Erasure deletes a log's entries in a statement of their own, before its head.
|
|
140
|
+
CREATE TRIGGER usage_of_summaries_erased AFTER DELETE ON call_log
|
|
141
|
+
REFERENCING OLD TABLE AS summaries
|
|
142
|
+
FOR EACH STATEMENT EXECUTE FUNCTION usage_of_summaries_erased();
|
|
143
|
+
|
|
144
|
+
CREATE TRIGGER usage_of_a_log_moved AFTER UPDATE OF org, env ON call_log_head
|
|
145
|
+
FOR EACH ROW WHEN (old.org IS DISTINCT FROM new.org OR old.env IS DISTINCT FROM new.env)
|
|
146
|
+
EXECUTE FUNCTION usage_of_a_log_moved();
|
|
147
|
+
|
|
148
|
+
-- The backfill: every summary of a log its head names an org and a world for.
|
|
149
|
+
INSERT INTO usage_totals
|
|
150
|
+
(org, env, period, calls, minutes, messages, input_tokens, output_tokens, characters, cost_usd)
|
|
151
|
+
SELECT head.org, head.env, usage_period(entry.ts), count(*), sum(used.minutes),
|
|
152
|
+
sum(used.messages), sum(used.input_tokens), sum(used.output_tokens), sum(used.characters),
|
|
153
|
+
sum(used.cost_usd)
|
|
154
|
+
FROM call_log entry
|
|
155
|
+
JOIN call_log_head head ON head.log = entry.log
|
|
156
|
+
CROSS JOIN LATERAL summary_usage(entry.data) AS used
|
|
157
|
+
WHERE entry.type = 'call.summary' AND head.org IS NOT NULL AND head.env IS NOT NULL
|
|
158
|
+
GROUP BY 1, 2, 3;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- The first of four that make call_log a table partitioned by day (0024 to 0027). A partitioned
|
|
2
|
+
-- table's key holds the partition key, so the log's key becomes (log, seq, ts), built here beside
|
|
3
|
+
-- the old one. Not CONCURRENTLY: that waits for every transaction open in the database, the
|
|
4
|
+
-- migration runner's own lock holder among them. CREATE INDEX takes SHARE on call_log: reads go on,
|
|
5
|
+
-- appends wait for one pass over the table and a sort of its keys (with N rows, about a second per
|
|
6
|
+
-- two million on the box's disk; production today holds far fewer). The swap (0027) makes it the
|
|
7
|
+
-- key of the table's first partition.
|
|
8
|
+
CREATE UNIQUE INDEX call_log_by_seq_and_day ON call_log (log, seq, ts);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- The bound between the log as it is and its days: every entry written until the swap is before
|
|
2
|
+
-- the start of the day after tomorrow (UTC), which a CHECK says, NOT VALID: adding it takes an
|
|
3
|
+
-- ACCESS EXCLUSIVE lock for the instant the catalog changes and reads no row. 0026 validates it
|
|
4
|
+
-- without holding writes; 0027 reads the bound back, so the old table becomes the partition of
|
|
5
|
+
-- everything before it and is never scanned under a lock that stops appends.
|
|
6
|
+
DO $$
|
|
7
|
+
DECLARE
|
|
8
|
+
bound double precision := extract(epoch FROM date_trunc('day', now() AT TIME ZONE 'UTC')
|
|
9
|
+
+ interval '2 days');
|
|
10
|
+
BEGIN
|
|
11
|
+
EXECUTE format(
|
|
12
|
+
'ALTER TABLE call_log ADD CONSTRAINT call_log_before_the_days CHECK (ts < %s) NOT VALID',
|
|
13
|
+
bound);
|
|
14
|
+
END
|
|
15
|
+
$$;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- The bound of 0025 proved on every row: VALIDATE takes SHARE UPDATE EXCLUSIVE, so appends go on
|
|
2
|
+
-- while it reads the table once (with N rows, the time of one sequential pass). A row past the
|
|
3
|
+
-- bound would fail it here, before anything is swapped.
|
|
4
|
+
ALTER TABLE call_log VALIDATE CONSTRAINT call_log_before_the_days;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
-- call_log becomes a table partitioned by range on ts, a partition per UTC day: retention drops a
|
|
2
|
+
-- day instead of deleting rows, and the rows written today sit in a table and indexes the size of
|
|
3
|
+
-- a day. The table as it was becomes the partition of everything before the bound 0025 set (the
|
|
4
|
+
-- start of the day after it ran), keeping its rows, its key (0024's) and its index; the parent
|
|
5
|
+
-- takes the name, the columns, the CHECK, the identity (numbering on from the last position
|
|
6
|
+
-- given) and the triggers, so a release before this one reads and writes call_log as it did. A
|
|
7
|
+
-- default partition catches a row no day holds, so a missing day never refuses an append;
|
|
8
|
+
-- `pinecall-runtime retention run` makes the days ahead each night and doctor names a row in the
|
|
9
|
+
-- default.
|
|
10
|
+
--
|
|
11
|
+
-- Locks and time: LOCK TABLE takes ACCESS EXCLUSIVE on call_log, held to the commit, so appends
|
|
12
|
+
-- and reads of the log wait; everything under it changes the catalog and reads no row (the
|
|
13
|
+
-- partition's bound is proved by 0026's constraint, its key and index exist), so with N rows it
|
|
14
|
+
-- holds them for tens of milliseconds, measured at 2 million rows. The runner gives up after 1 s
|
|
15
|
+
-- of waiting for the lock, before anything changed.
|
|
16
|
+
|
|
17
|
+
LOCK TABLE call_log IN ACCESS EXCLUSIVE MODE;
|
|
18
|
+
|
|
19
|
+
DROP TRIGGER call_log_refuses_delete ON call_log;
|
|
20
|
+
DROP TRIGGER call_log_refuses_update ON call_log;
|
|
21
|
+
DROP TRIGGER call_log_refuses_a_sealed_log ON call_log;
|
|
22
|
+
DROP TRIGGER usage_of_summaries_written ON call_log;
|
|
23
|
+
DROP TRIGGER usage_of_summaries_erased ON call_log;
|
|
24
|
+
|
|
25
|
+
-- The key becomes 0024's index, which holds the partition key; the rows are not read.
|
|
26
|
+
ALTER TABLE call_log DROP CONSTRAINT call_log_one_row_per_seq;
|
|
27
|
+
ALTER TABLE call_log ADD CONSTRAINT call_log_before_one_row_per_seq
|
|
28
|
+
PRIMARY KEY USING INDEX call_log_by_seq_and_day;
|
|
29
|
+
|
|
30
|
+
-- The last position given, kept for this transaction alone: the parent numbers on from it.
|
|
31
|
+
SELECT set_config(
|
|
32
|
+
'pinecall.last_position',
|
|
33
|
+
coalesce(pg_sequence_last_value('call_log_position_seq'::regclass), 0)::text,
|
|
34
|
+
true);
|
|
35
|
+
|
|
36
|
+
-- A partition may not number its rows itself: the parent does, from where the table left off.
|
|
37
|
+
ALTER TABLE call_log ALTER COLUMN "position" DROP IDENTITY;
|
|
38
|
+
ALTER TABLE call_log RENAME TO call_log_before_the_days;
|
|
39
|
+
|
|
40
|
+
CREATE TABLE call_log (
|
|
41
|
+
call text,
|
|
42
|
+
seq bigint NOT NULL,
|
|
43
|
+
ts double precision NOT NULL,
|
|
44
|
+
agent text NOT NULL,
|
|
45
|
+
type text NOT NULL,
|
|
46
|
+
ephemeral boolean NOT NULL,
|
|
47
|
+
data jsonb NOT NULL,
|
|
48
|
+
log text GENERATED ALWAYS AS (COALESCE(call, ('@'::text || agent))) STORED NOT NULL,
|
|
49
|
+
"position" bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
|
|
50
|
+
CONSTRAINT call_log_call_never_opens_with_at CHECK (((call IS NULL) OR ("left"(call, 1) <> '@'::text)))
|
|
51
|
+
) PARTITION BY RANGE (ts);
|
|
52
|
+
|
|
53
|
+
-- The bound is 0025's, read back from its constraint; the days from it on are made here, a week.
|
|
54
|
+
DO $$
|
|
55
|
+
DECLARE
|
|
56
|
+
bound double precision := (
|
|
57
|
+
SELECT (regexp_match(pg_get_constraintdef(oid), '\(ts < \(?''?(-?[0-9.e+]+)'))[1]::float8
|
|
58
|
+
FROM pg_constraint
|
|
59
|
+
WHERE conname = 'call_log_before_the_days'
|
|
60
|
+
AND conrelid = 'call_log_before_the_days'::regclass);
|
|
61
|
+
day double precision;
|
|
62
|
+
BEGIN
|
|
63
|
+
EXECUTE format(
|
|
64
|
+
'ALTER TABLE call_log ATTACH PARTITION call_log_before_the_days '
|
|
65
|
+
'FOR VALUES FROM (MINVALUE) TO (%s)', bound);
|
|
66
|
+
EXECUTE format(
|
|
67
|
+
'ALTER TABLE call_log ALTER COLUMN "position" RESTART WITH %s',
|
|
68
|
+
current_setting('pinecall.last_position')::bigint + 1);
|
|
69
|
+
FOR n IN 0..6 LOOP
|
|
70
|
+
day := bound + n * 86400;
|
|
71
|
+
EXECUTE format(
|
|
72
|
+
'CREATE TABLE %I PARTITION OF call_log FOR VALUES FROM (%s) TO (%s)',
|
|
73
|
+
'call_log_' || to_char(to_timestamp(day) AT TIME ZONE 'UTC', 'YYYYMMDD'),
|
|
74
|
+
day, day + 86400);
|
|
75
|
+
END LOOP;
|
|
76
|
+
END
|
|
77
|
+
$$;
|
|
78
|
+
|
|
79
|
+
CREATE TABLE call_log_default PARTITION OF call_log DEFAULT;
|
|
80
|
+
|
|
81
|
+
ALTER TABLE call_log ADD CONSTRAINT call_log_one_row_per_seq_and_day PRIMARY KEY (log, seq, ts);
|
|
82
|
+
CREATE INDEX call_log_metered_by_day ON call_log (type, "position");
|
|
83
|
+
|
|
84
|
+
CREATE TRIGGER call_log_refuses_delete BEFORE DELETE ON call_log
|
|
85
|
+
FOR EACH STATEMENT EXECUTE FUNCTION call_log_refuses_the_statement();
|
|
86
|
+
CREATE TRIGGER call_log_refuses_update BEFORE UPDATE ON call_log
|
|
87
|
+
FOR EACH STATEMENT EXECUTE FUNCTION call_log_refuses_the_statement();
|
|
88
|
+
CREATE TRIGGER call_log_refuses_a_sealed_log AFTER INSERT ON call_log
|
|
89
|
+
REFERENCING NEW TABLE AS written
|
|
90
|
+
FOR EACH STATEMENT EXECUTE FUNCTION call_log_refuses_a_sealed_log();
|
|
91
|
+
CREATE TRIGGER usage_of_summaries_written AFTER INSERT ON call_log
|
|
92
|
+
REFERENCING NEW TABLE AS summaries
|
|
93
|
+
FOR EACH STATEMENT EXECUTE FUNCTION usage_of_summaries_written();
|
|
94
|
+
CREATE TRIGGER usage_of_summaries_erased AFTER DELETE ON call_log
|
|
95
|
+
REFERENCING OLD TABLE AS summaries
|
|
96
|
+
FOR EACH STATEMENT EXECUTE FUNCTION usage_of_summaries_erased();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- A WhatsApp message is read once: Meta delivers a webhook again when it thinks it unanswered.
|
|
2
|
+
-- The gateway claims a message by inserting its row before reading it (channels/whatsapp.py), so
|
|
3
|
+
-- of two deliveries at once, in one process or two, only one reads it; `read_at` is set once it is
|
|
4
|
+
-- queued or kept, and a claim still unread past its lease is taken by the next delivery. Keyed by
|
|
5
|
+
-- org, so one org's message id never shadows another's. The nightly retention run forgets the rows
|
|
6
|
+
-- past Meta's 7 days of retries, by `claimed_at`.
|
|
7
|
+
-- A new table: CREATE TABLE locks nothing that exists but `orgs`, in SHARE ROW EXCLUSIVE for the
|
|
8
|
+
-- foreign key, for the instant of the statement: reads of orgs go on, a write to it waits that long.
|
|
9
|
+
CREATE TABLE whatsapp_seen (
|
|
10
|
+
org text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
11
|
+
message_id text NOT NULL,
|
|
12
|
+
claimed_at timestamptz NOT NULL,
|
|
13
|
+
read_at timestamptz,
|
|
14
|
+
PRIMARY KEY (org, message_id)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX whatsapp_seen_claimed_at ON whatsapp_seen (claimed_at);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Every read is on the record: a server's key reading a call, a seat that listens to or
|
|
2
|
+
-- supervises a live call, an export of the org's world, and a read of what memory keeps of a
|
|
3
|
+
-- contact join the log, beside a person's and the operator's reads of a call, a recording and a
|
|
4
|
+
-- number.
|
|
5
|
+
--
|
|
6
|
+
-- Lock: ACCESS EXCLUSIVE on `reads` for two catalog changes and no scan, milliseconds under the
|
|
7
|
+
-- runner's lock_timeout. NOT VALID skips checking the rows already there, which the narrower
|
|
8
|
+
-- check admitted; every row written from now on is checked. A VALIDATE would hold the same lock
|
|
9
|
+
-- over a scan of the table, and every read of a call waits on its row here.
|
|
10
|
+
|
|
11
|
+
ALTER TABLE reads
|
|
12
|
+
DROP CONSTRAINT reads_what_check,
|
|
13
|
+
ADD CONSTRAINT reads_what_check
|
|
14
|
+
CHECK (what IN ('log', 'recording', 'traceback', 'listen', 'supervise', 'export', 'memory'))
|
|
15
|
+
NOT VALID;
|