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,167 @@
|
|
|
1
|
+
"""An org's world as JSON Lines: calls, memories, settings, words, documents, consent, drift."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
|
|
7
|
+
from pinecall.domain.names import Env
|
|
8
|
+
from pinecall.postgres.pool import Pool, unbounded
|
|
9
|
+
|
|
10
|
+
# One page of calls at a time, each with its whole log, so an org of any size streams in bounded
|
|
11
|
+
# memory. Every read is unbounded: an org's list of anything has no size. Postgres writes the
|
|
12
|
+
# JSON: timestamps, jsonb and nulls come out as they are stored.
|
|
13
|
+
CALLS = """
|
|
14
|
+
SELECT jsonb_build_object(
|
|
15
|
+
'kind', 'call', 'call', head.call, 'agent', head.agent, 'holder', head.holder,
|
|
16
|
+
'started_at', head.started_at, 'sealed', head.sealed,
|
|
17
|
+
'facts', to_jsonb(facts) - 'call',
|
|
18
|
+
'entries', (
|
|
19
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object(
|
|
20
|
+
'seq', entry.seq, 'ts', entry.ts, 'type', entry.type, 'data', entry.data
|
|
21
|
+
) ORDER BY entry.seq), '[]'::jsonb)
|
|
22
|
+
FROM call_log entry WHERE entry.log = head.log
|
|
23
|
+
)
|
|
24
|
+
)::text AS line, coalesce(head.started_at, 0) AS at, head.call
|
|
25
|
+
FROM call_log_head head
|
|
26
|
+
LEFT JOIN call_facts facts ON facts.call = head.call
|
|
27
|
+
WHERE head.org = %(org)s AND head.env = %(env)s AND head.call IS NOT NULL
|
|
28
|
+
AND (coalesce(head.started_at, 0), head.call) > (%(at)s, %(call)s)
|
|
29
|
+
ORDER BY coalesce(head.started_at, 0), head.call
|
|
30
|
+
LIMIT %(page)s
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# The embedding is the box's to compute again; the words are the org's.
|
|
34
|
+
MEMORIES = """
|
|
35
|
+
SELECT jsonb_build_object(
|
|
36
|
+
'kind', 'memory', 'contact', contact, 'holder', holder, 'text', text, 'category', category,
|
|
37
|
+
'valid_from', valid_from, 'invalidated_at', invalidated_at, 'source_call', source_call
|
|
38
|
+
)::text AS line
|
|
39
|
+
FROM contact_memories WHERE org = %(org)s AND env = %(env)s
|
|
40
|
+
ORDER BY contact, valid_from
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
SETTINGS = """
|
|
44
|
+
SELECT jsonb_build_object(
|
|
45
|
+
'kind', 'agent_config', 'agent', agent, 'holder', holder, 'version', version,
|
|
46
|
+
'config', config, 'author', author, 'note', note, 'set_at', set_at
|
|
47
|
+
)::text AS line
|
|
48
|
+
FROM agent_config WHERE org = %(org)s AND env = %(env)s
|
|
49
|
+
ORDER BY agent, holder, version
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Every canary set and cleared, as the settings are: nothing is ever updated.
|
|
53
|
+
CANARIES = """
|
|
54
|
+
SELECT jsonb_build_object(
|
|
55
|
+
'kind', 'canary', 'agent', agent, 'holder', holder, 'version', version, 'share', share,
|
|
56
|
+
'author', author, 'note', note, 'set_at', set_at
|
|
57
|
+
)::text AS line
|
|
58
|
+
FROM agent_canaries WHERE org = %(org)s AND env = %(env)s
|
|
59
|
+
ORDER BY agent, holder, set_at, id
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# The dataset is the org's in both worlds: every export carries it whole, with the world its
|
|
63
|
+
# call ran in.
|
|
64
|
+
CASES = """
|
|
65
|
+
SELECT jsonb_build_object(
|
|
66
|
+
'kind', 'eval_case', 'id', id, 'agent', agent, 'name', name, 'golden', golden,
|
|
67
|
+
'source_call', source_call, 'source_env', source_env, 'held_out', held_out,
|
|
68
|
+
'author', author, 'created_at', created_at
|
|
69
|
+
)::text AS line
|
|
70
|
+
FROM eval_cases WHERE org = %(org)s
|
|
71
|
+
ORDER BY agent, name
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Each block of prompt the org's calls were told, once per text, the same in both worlds.
|
|
75
|
+
PROMPTS = """
|
|
76
|
+
SELECT jsonb_build_object(
|
|
77
|
+
'kind', 'prompt', 'hash', hash, 'text', text, 'first_used_at', first_used_at
|
|
78
|
+
)::text AS line
|
|
79
|
+
FROM prompts WHERE org = %(org)s
|
|
80
|
+
ORDER BY first_used_at, hash
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
WORDS = """
|
|
84
|
+
SELECT jsonb_build_object(
|
|
85
|
+
'kind', 'lexicon', 'agent', agent, 'holder', holder, 'version', version,
|
|
86
|
+
'said', said, 'heard', heard, 'author', author, 'note', note, 'set_at', set_at
|
|
87
|
+
)::text AS line
|
|
88
|
+
FROM lexicon WHERE org = %(org)s AND env = %(env)s
|
|
89
|
+
ORDER BY agent, holder, version
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
DOCUMENTS = """
|
|
93
|
+
SELECT jsonb_build_object(
|
|
94
|
+
'kind', 'knowledge_file', 'base', base, 'holder', holder, 'path', path, 'text', text,
|
|
95
|
+
'pushed_at', pushed_at
|
|
96
|
+
)::text AS line
|
|
97
|
+
FROM knowledge_files WHERE org = %(org)s AND env = %(env)s
|
|
98
|
+
ORDER BY base, path
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
CONSENTS = """
|
|
102
|
+
SELECT jsonb_build_object(
|
|
103
|
+
'kind', 'consent', 'number', number, 'consent', kind, 'source', source, 'text', text,
|
|
104
|
+
'evidence', evidence, 'given_by', given_by, 'call', call, 'given_at', given_at
|
|
105
|
+
)::text AS line
|
|
106
|
+
FROM contact_consents WHERE org = %(org)s AND env = %(env)s
|
|
107
|
+
ORDER BY number, given_at, id
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# What the seal counted of each day, by agent and version: numbers and the judges' names.
|
|
111
|
+
STAGE_DAYS = """
|
|
112
|
+
SELECT jsonb_build_object(
|
|
113
|
+
'kind', 'stage_day', 'holder', holder, 'agent', agent, 'day', day,
|
|
114
|
+
'config_version', config_version, 'stage', stage, 'vendor', vendor, 'model', model,
|
|
115
|
+
'turns', turns, 'buckets', buckets, 'confidence_sum', confidence_sum,
|
|
116
|
+
'confidence_turns', confidence_turns
|
|
117
|
+
)::text AS line
|
|
118
|
+
FROM stage_days WHERE org = %(org)s AND env = %(env)s
|
|
119
|
+
ORDER BY day, agent, config_version, stage, vendor, model
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
JUDGE_DAYS = """
|
|
123
|
+
SELECT jsonb_build_object(
|
|
124
|
+
'kind', 'judge_day', 'holder', holder, 'agent', agent, 'day', day,
|
|
125
|
+
'config_version', config_version, 'judge', judge, 'criteria', criteria, 'held', held,
|
|
126
|
+
'broken', broken
|
|
127
|
+
)::text AS line
|
|
128
|
+
FROM judge_days WHERE org = %(org)s AND env = %(env)s
|
|
129
|
+
ORDER BY day, agent, config_version, judge, criteria
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
A_PAGE_OF_CALLS = 100
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# What follows the calls, in this order.
|
|
136
|
+
AFTER_THE_CALLS = (
|
|
137
|
+
MEMORIES,
|
|
138
|
+
SETTINGS,
|
|
139
|
+
CANARIES,
|
|
140
|
+
WORDS,
|
|
141
|
+
DOCUMENTS,
|
|
142
|
+
CONSENTS,
|
|
143
|
+
CASES,
|
|
144
|
+
PROMPTS,
|
|
145
|
+
STAGE_DAYS,
|
|
146
|
+
JUDGE_DAYS,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def lines(pool: Pool, org: str, env: Env) -> AsyncIterator[str]:
|
|
151
|
+
"""Every line: the header, calls, memories, settings, words, documents, consent, drift."""
|
|
152
|
+
yield json.dumps({"kind": "export", "org": org, "env": env, "exported_at": time.time()})
|
|
153
|
+
at, call = -1.0, ""
|
|
154
|
+
while True:
|
|
155
|
+
async with unbounded(pool) as connection:
|
|
156
|
+
params = {"org": org, "env": env, "at": at, "call": call, "page": A_PAGE_OF_CALLS}
|
|
157
|
+
rows = await (await connection.execute(CALLS, params)).fetchall()
|
|
158
|
+
for row in rows:
|
|
159
|
+
yield str(row["line"])
|
|
160
|
+
if len(rows) < A_PAGE_OF_CALLS:
|
|
161
|
+
break
|
|
162
|
+
at, call = float(rows[-1]["at"]), str(rows[-1]["call"])
|
|
163
|
+
for query in AFTER_THE_CALLS:
|
|
164
|
+
async with unbounded(pool) as connection:
|
|
165
|
+
rows = await (await connection.execute(query, {"org": org, "env": env})).fetchall()
|
|
166
|
+
for row in rows:
|
|
167
|
+
yield str(row["line"])
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""A hosted app while it runs: stopped and started, the logs asked for, the time it served."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import UTC, date, datetime
|
|
6
|
+
from typing import LiteralString
|
|
7
|
+
|
|
8
|
+
from pinecall.domain.errors import NotFound
|
|
9
|
+
from pinecall.domain.names import Env
|
|
10
|
+
from pinecall.postgres.pool import Pool
|
|
11
|
+
from pinecall.tenancy.hosting import NOT_HOSTED, HostedApp
|
|
12
|
+
|
|
13
|
+
# A gap between two beats longer than this is a runner that was away: it counts this much.
|
|
14
|
+
LONGEST_BEAT_S = 30.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# What a person reads of a process's output: its last lines, and no more than this.
|
|
18
|
+
LONGEST_LOGS = 64 * 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
STOP = """
|
|
22
|
+
UPDATE hosted_apps SET stopped_at = now(), stopped_by = %(by)s
|
|
23
|
+
WHERE org = %(org)s AND env = %(env)s AND name = %(name)s RETURNING name
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
START = """
|
|
28
|
+
UPDATE hosted_apps SET stopped_at = NULL, stopped_by = ''
|
|
29
|
+
WHERE org = %(org)s AND env = %(env)s AND name = %(name)s RETURNING name
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
ASK_LOGS = """
|
|
34
|
+
UPDATE hosted_apps SET logs_asked_at = now()
|
|
35
|
+
WHERE org = %(org)s AND env = %(env)s AND name = %(name)s
|
|
36
|
+
RETURNING logs, logs_host, logs_at
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
KEEP_LOGS = """
|
|
41
|
+
UPDATE hosted_apps SET logs = %(lines)s, logs_host = %(host)s, logs_at = now()
|
|
42
|
+
WHERE org = %(org)s AND env = %(env)s AND name = %(name)s
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# One statement for a world's beat. Every app that was being counted, or serves now, is locked and
|
|
47
|
+
# read once: the time since its last count goes on today's row, capped, and it is counted from now
|
|
48
|
+
# if it serves and from nothing if it does not. Two runners beating at once wait on the lock and
|
|
49
|
+
# read the other's count, so no span is counted twice.
|
|
50
|
+
COUNT = """
|
|
51
|
+
WITH serving AS (
|
|
52
|
+
SELECT * FROM unnest(%(orgs)s::text[], %(names)s::text[]) AS app (org, name)
|
|
53
|
+
), previous AS (
|
|
54
|
+
SELECT app.org, app.name, app.metered_at,
|
|
55
|
+
EXISTS (SELECT 1 FROM serving WHERE org = app.org AND name = app.name) AS serves
|
|
56
|
+
FROM hosted_apps app
|
|
57
|
+
WHERE app.env = %(env)s AND (app.metered_at IS NOT NULL OR EXISTS (
|
|
58
|
+
SELECT 1 FROM serving WHERE org = app.org AND name = app.name))
|
|
59
|
+
FOR UPDATE OF app
|
|
60
|
+
), counted AS (
|
|
61
|
+
INSERT INTO hosted_usage (org, env, name, day, seconds)
|
|
62
|
+
SELECT org, %(env)s, name, %(day)s,
|
|
63
|
+
least(greatest(extract(epoch FROM %(now)s::timestamptz - metered_at), 0), %(longest)s)
|
|
64
|
+
FROM previous WHERE serves AND metered_at IS NOT NULL
|
|
65
|
+
ON CONFLICT (org, env, name, day)
|
|
66
|
+
DO UPDATE SET seconds = hosted_usage.seconds + excluded.seconds
|
|
67
|
+
)
|
|
68
|
+
UPDATE hosted_apps app
|
|
69
|
+
SET metered_at = CASE WHEN previous.serves THEN %(now)s::timestamptz END
|
|
70
|
+
FROM previous
|
|
71
|
+
WHERE app.env = %(env)s AND app.org = previous.org AND app.name = previous.name
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
USAGE = """
|
|
76
|
+
SELECT org, env, name, day, seconds FROM hosted_usage
|
|
77
|
+
WHERE day >= %(since)s AND day < %(until)s AND (%(org)s::text IS NULL OR org = %(org)s)
|
|
78
|
+
AND (%(env)s::text IS NULL OR env = %(env)s)
|
|
79
|
+
ORDER BY org, env, day, name
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class Logs:
|
|
85
|
+
"""The last lines of an app's process as the runner read them; empty before the first read."""
|
|
86
|
+
|
|
87
|
+
host: str | None
|
|
88
|
+
lines: str
|
|
89
|
+
at: datetime | None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class Served:
|
|
94
|
+
"""The time an app served on one UTC day, in seconds."""
|
|
95
|
+
|
|
96
|
+
org: str
|
|
97
|
+
env: str
|
|
98
|
+
name: str
|
|
99
|
+
day: date
|
|
100
|
+
seconds: float
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def stop_app(pool: Pool, app: HostedApp, *, by: str) -> None:
|
|
104
|
+
"""Stop running the app: the runner drains it, and its releases and token stay."""
|
|
105
|
+
await _changed(pool, STOP, {**app.columns, "by": by}, app)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def start_app(pool: Pool, app: HostedApp) -> None:
|
|
109
|
+
"""Run a stopped app again, its newest release."""
|
|
110
|
+
await _changed(pool, START, app.columns, app)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def ask_for_logs(pool: Pool, app: HostedApp) -> Logs:
|
|
114
|
+
"""What the runner last sent of the app's output; the runner is told to send it again."""
|
|
115
|
+
async with pool.connection() as connection:
|
|
116
|
+
row = await (await connection.execute(ASK_LOGS, app.columns)).fetchone()
|
|
117
|
+
if row is None:
|
|
118
|
+
raise NotFound(NOT_HOSTED.format(name=app.name, env=app.env))
|
|
119
|
+
return Logs(host=row["logs_host"], lines=row["logs"], at=row["logs_at"])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def keep_logs(pool: Pool, app: HostedApp, *, host: str, lines: str) -> None:
|
|
123
|
+
"""Keep what the runner read of the app's output, its last part when it is long."""
|
|
124
|
+
values = {**app.columns, "host": host, "lines": lines[-LONGEST_LOGS:]}
|
|
125
|
+
async with pool.connection() as connection:
|
|
126
|
+
await connection.execute(KEEP_LOGS, values)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def count_serving(
|
|
130
|
+
pool: Pool, env: Env, serving: Sequence[tuple[str, str]], now: datetime
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Count the time the world's serving apps, each an (org, name), served since the last count."""
|
|
133
|
+
values = {
|
|
134
|
+
"env": env,
|
|
135
|
+
"orgs": [org for org, _ in serving],
|
|
136
|
+
"names": [name for _, name in serving],
|
|
137
|
+
"now": now,
|
|
138
|
+
"day": now.astimezone(UTC).date(),
|
|
139
|
+
"longest": LONGEST_BEAT_S,
|
|
140
|
+
}
|
|
141
|
+
async with pool.connection() as connection, connection.transaction():
|
|
142
|
+
await connection.execute(COUNT, values)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def served(
|
|
146
|
+
pool: Pool, since: date, until: date, *, org: str | None = None, env: str | None = None
|
|
147
|
+
) -> list[Served]:
|
|
148
|
+
"""The time apps served per UTC day in [since, until), one org's or every org's."""
|
|
149
|
+
values = {"since": since, "until": until, "org": org, "env": env}
|
|
150
|
+
async with pool.connection() as connection:
|
|
151
|
+
rows = await (await connection.execute(USAGE, values)).fetchall()
|
|
152
|
+
return [
|
|
153
|
+
Served(
|
|
154
|
+
org=row["org"], env=row["env"], name=row["name"], day=row["day"], seconds=row["seconds"]
|
|
155
|
+
)
|
|
156
|
+
for row in rows
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def _changed(
|
|
161
|
+
pool: Pool, statement: LiteralString, values: dict[str, str], app: HostedApp
|
|
162
|
+
) -> None:
|
|
163
|
+
async with pool.connection() as connection:
|
|
164
|
+
row = await (await connection.execute(statement, values)).fetchone()
|
|
165
|
+
if row is None:
|
|
166
|
+
raise NotFound(NOT_HOSTED.format(name=app.name, env=app.env))
|