pinecall-runtime 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. pinecall/__init__.py +0 -0
  2. pinecall/channels/__init__.py +0 -0
  3. pinecall/channels/_chooser.py +45 -0
  4. pinecall/channels/offers.py +219 -0
  5. pinecall/channels/rooms.py +176 -0
  6. pinecall/channels/routes.py +275 -0
  7. pinecall/channels/telephony/__init__.py +0 -0
  8. pinecall/channels/telephony/_twilio.py +349 -0
  9. pinecall/channels/telephony/carrier.py +229 -0
  10. pinecall/channels/telephony/carrier_catalog.py +92 -0
  11. pinecall/channels/telephony/carriers.csv +41 -0
  12. pinecall/channels/telephony/dialing.py +419 -0
  13. pinecall/channels/telephony/firewall.py +58 -0
  14. pinecall/channels/telephony/hand_over.py +78 -0
  15. pinecall/channels/telephony/number_path.py +176 -0
  16. pinecall/channels/telephony/numbers.py +530 -0
  17. pinecall/channels/telephony/origination.py +77 -0
  18. pinecall/channels/telephony/sip.py +448 -0
  19. pinecall/channels/whatsapp.py +413 -0
  20. pinecall/cli/__init__.py +0 -0
  21. pinecall/cli/_archive.py +85 -0
  22. pinecall/cli/_drift.py +38 -0
  23. pinecall/cli/_facts.py +67 -0
  24. pinecall/cli/_load.py +433 -0
  25. pinecall/cli/_operator.py +626 -0
  26. pinecall/cli/_sessions.py +164 -0
  27. pinecall/cli/_traceback.py +95 -0
  28. pinecall/cli/_usage.py +34 -0
  29. pinecall/cli/main.py +521 -0
  30. pinecall/domain/__init__.py +0 -0
  31. pinecall/domain/agent.py +446 -0
  32. pinecall/domain/call.py +152 -0
  33. pinecall/domain/country_codes.py +32 -0
  34. pinecall/domain/errors.py +151 -0
  35. pinecall/domain/names.py +115 -0
  36. pinecall/domain/org.py +120 -0
  37. pinecall/domain/person.py +208 -0
  38. pinecall/domain/scope.py +21 -0
  39. pinecall/evals/__init__.py +0 -0
  40. pinecall/evals/_evidence.py +158 -0
  41. pinecall/evals/calibration.py +112 -0
  42. pinecall/evals/callers.py +160 -0
  43. pinecall/evals/case.py +327 -0
  44. pinecall/evals/checks.py +437 -0
  45. pinecall/evals/compliance.py +197 -0
  46. pinecall/evals/dataset.py +451 -0
  47. pinecall/evals/goldens.py +331 -0
  48. pinecall/evals/judges.py +525 -0
  49. pinecall/evals/runs.py +252 -0
  50. pinecall/evals/spoken.py +356 -0
  51. pinecall/fleet/__init__.py +0 -0
  52. pinecall/fleet/client.py +569 -0
  53. pinecall/fleet/demand.py +51 -0
  54. pinecall/fleet/heartbeat.py +194 -0
  55. pinecall/fleet/measures.py +157 -0
  56. pinecall/fleet/roster.py +232 -0
  57. pinecall/fleet/worlds.py +44 -0
  58. pinecall/gateway/__init__.py +0 -0
  59. pinecall/gateway/_call_setup.py +53 -0
  60. pinecall/gateway/_deps.py +556 -0
  61. pinecall/gateway/_gateway.py +75 -0
  62. pinecall/gateway/_served.py +396 -0
  63. pinecall/gateway/_sockets.py +444 -0
  64. pinecall/gateway/_streams.py +83 -0
  65. pinecall/gateway/_text_calls.py +182 -0
  66. pinecall/gateway/api/__init__.py +0 -0
  67. pinecall/gateway/api/accounts.py +324 -0
  68. pinecall/gateway/api/agents.py +150 -0
  69. pinecall/gateway/api/apps.py +478 -0
  70. pinecall/gateway/api/box.py +658 -0
  71. pinecall/gateway/api/calibration.py +58 -0
  72. pinecall/gateway/api/callbacks.py +105 -0
  73. pinecall/gateway/api/calls.py +655 -0
  74. pinecall/gateway/api/chat.py +223 -0
  75. pinecall/gateway/api/dataset.py +163 -0
  76. pinecall/gateway/api/desk.py +113 -0
  77. pinecall/gateway/api/entries.py +77 -0
  78. pinecall/gateway/api/evals.py +647 -0
  79. pinecall/gateway/api/fleet.py +73 -0
  80. pinecall/gateway/api/hosting.py +241 -0
  81. pinecall/gateway/api/judges.py +97 -0
  82. pinecall/gateway/api/keys.py +110 -0
  83. pinecall/gateway/api/line.py +142 -0
  84. pinecall/gateway/api/members.py +197 -0
  85. pinecall/gateway/api/metrics.py +226 -0
  86. pinecall/gateway/api/numbers.py +476 -0
  87. pinecall/gateway/api/ops.py +428 -0
  88. pinecall/gateway/api/org.py +283 -0
  89. pinecall/gateway/api/personas.py +157 -0
  90. pinecall/gateway/api/pipeline.py +206 -0
  91. pinecall/gateway/api/prompts.py +50 -0
  92. pinecall/gateway/api/providers.py +240 -0
  93. pinecall/gateway/api/recordings.py +89 -0
  94. pinecall/gateway/api/relay.py +169 -0
  95. pinecall/gateway/api/retrieval.py +403 -0
  96. pinecall/gateway/api/runner.py +109 -0
  97. pinecall/gateway/api/sessions.py +102 -0
  98. pinecall/gateway/api/settings.py +463 -0
  99. pinecall/gateway/api/signup.py +164 -0
  100. pinecall/gateway/api/sso_login.py +168 -0
  101. pinecall/gateway/api/threads.py +187 -0
  102. pinecall/gateway/api/usage.py +303 -0
  103. pinecall/gateway/api/visitors.py +222 -0
  104. pinecall/gateway/api/whatsapp.py +48 -0
  105. pinecall/gateway/api/widget.py +49 -0
  106. pinecall/gateway/app.py +504 -0
  107. pinecall/gateway/calls/__init__.py +0 -0
  108. pinecall/gateway/calls/binding.py +63 -0
  109. pinecall/gateway/calls/commands.py +18 -0
  110. pinecall/gateway/calls/inbox.py +100 -0
  111. pinecall/gateway/calls/known.py +23 -0
  112. pinecall/gateway/calls/owners.py +87 -0
  113. pinecall/gateway/calls/pump.py +33 -0
  114. pinecall/gateway/calls/serving.py +118 -0
  115. pinecall/gateway/calls/threads.py +367 -0
  116. pinecall/gateway/dispatching/__init__.py +0 -0
  117. pinecall/gateway/dispatching/arrivals.py +58 -0
  118. pinecall/gateway/dispatching/sweep.py +43 -0
  119. pinecall/gateway/ending/__init__.py +0 -0
  120. pinecall/gateway/ending/reaper.py +160 -0
  121. pinecall/gateway/ending/seal.py +312 -0
  122. pinecall/gateway/ending/stranded.py +117 -0
  123. pinecall/log/__init__.py +0 -0
  124. pinecall/log/_histogram.py +74 -0
  125. pinecall/log/_relay.py +259 -0
  126. pinecall/log/_writer.py +454 -0
  127. pinecall/log/days.py +154 -0
  128. pinecall/log/drift.py +550 -0
  129. pinecall/log/facts.py +358 -0
  130. pinecall/log/inbox.py +169 -0
  131. pinecall/log/lists.py +155 -0
  132. pinecall/log/logs.py +581 -0
  133. pinecall/log/openings.py +71 -0
  134. pinecall/log/private.py +135 -0
  135. pinecall/log/queries.py +229 -0
  136. pinecall/log/readers.py +207 -0
  137. pinecall/log/reduce.py +659 -0
  138. pinecall/log/refold.py +217 -0
  139. pinecall/log/store.py +370 -0
  140. pinecall/log/usage.py +242 -0
  141. pinecall/postgres/__init__.py +0 -0
  142. pinecall/postgres/migrate.py +202 -0
  143. pinecall/postgres/migrations/0001_schema.sql +591 -0
  144. pinecall/postgres/migrations/0002_worlds.sql +15 -0
  145. pinecall/postgres/migrations/0003_lent.sql +4 -0
  146. pinecall/postgres/migrations/0004_carriers.sql +23 -0
  147. pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
  148. pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
  149. pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
  150. pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
  151. pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
  152. pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
  153. pinecall/postgres/migrations/0011_erasure.sql +32 -0
  154. pinecall/postgres/migrations/0012_org_policy.sql +10 -0
  155. pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
  156. pinecall/postgres/migrations/0014_consents.sql +24 -0
  157. pinecall/postgres/migrations/0015_disclosure.sql +7 -0
  158. pinecall/postgres/migrations/0016_call_records.sql +21 -0
  159. pinecall/postgres/migrations/0017_reads.sql +17 -0
  160. pinecall/postgres/migrations/0018_hosting.sql +41 -0
  161. pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
  162. pinecall/postgres/migrations/0020_written_once.sql +7 -0
  163. pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
  164. pinecall/postgres/migrations/0022_sealed.sql +39 -0
  165. pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
  166. pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
  167. pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
  168. pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
  169. pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
  170. pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
  171. pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
  172. pinecall/postgres/migrations/0060_drift.sql +62 -0
  173. pinecall/postgres/migrations/0061_canaries.sql +24 -0
  174. pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
  175. pinecall/postgres/migrations/0063_prompts.sql +16 -0
  176. pinecall/postgres/migrations/0075_call_private.sql +14 -0
  177. pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
  178. pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
  179. pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
  180. pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
  181. pinecall/postgres/migrations/0080_call_openings.sql +20 -0
  182. pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
  183. pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
  184. pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
  185. pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
  186. pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
  187. pinecall/postgres/migrations/0086_route_origin.sql +17 -0
  188. pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
  189. pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
  190. pinecall/postgres/migrations/0089_offers.sql +21 -0
  191. pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
  192. pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
  193. pinecall/postgres/migrations/0092_key_parent.sql +6 -0
  194. pinecall/postgres/migrations/0093_code_tries.sql +10 -0
  195. pinecall/postgres/migrations/0094_route_approval.sql +12 -0
  196. pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
  197. pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
  198. pinecall/postgres/migrations/0097_case_status.sql +19 -0
  199. pinecall/postgres/pool.py +190 -0
  200. pinecall/process/__init__.py +0 -0
  201. pinecall/process/_objects.py +97 -0
  202. pinecall/process/box_settings.py +23 -0
  203. pinecall/process/connections.py +124 -0
  204. pinecall/process/metrics.py +127 -0
  205. pinecall/process/recordings.py +323 -0
  206. pinecall/process/resolver.py +41 -0
  207. pinecall/process/sealed_audio.py +173 -0
  208. pinecall/process/settings.py +534 -0
  209. pinecall/process/shared.py +158 -0
  210. pinecall/process/signal.py +416 -0
  211. pinecall/providers/__init__.py +0 -0
  212. pinecall/providers/build.py +570 -0
  213. pinecall/providers/catalog.py +266 -0
  214. pinecall/providers/credentials.py +230 -0
  215. pinecall/providers/declared.py +69 -0
  216. pinecall/providers/prices.py +240 -0
  217. pinecall/providers/tried.py +104 -0
  218. pinecall/providers/voices.py +174 -0
  219. pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
  220. pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
  221. pinecall/public/console/favicon.png +0 -0
  222. pinecall/public/console/index.html +20 -0
  223. pinecall/public/console/logo-mark.png +0 -0
  224. pinecall/public/console/pinecall-logo-dark.png +0 -0
  225. pinecall/public/console/pinecall-logo.png +0 -0
  226. pinecall/public/console/pinecall-mark.png +0 -0
  227. pinecall/public/console/sw.js +37 -0
  228. pinecall/public/widget/pinecall-widget.js +853 -0
  229. pinecall/py.typed +0 -0
  230. pinecall/retrieval/__init__.py +0 -0
  231. pinecall/retrieval/_search.py +186 -0
  232. pinecall/retrieval/_sections.py +70 -0
  233. pinecall/retrieval/embed.py +340 -0
  234. pinecall/retrieval/extraction.py +403 -0
  235. pinecall/retrieval/knowledge.py +659 -0
  236. pinecall/retrieval/lookups.py +192 -0
  237. pinecall/retrieval/memory.py +603 -0
  238. pinecall/runner/__init__.py +0 -0
  239. pinecall/runner/_kube.py +459 -0
  240. pinecall/runner/_plan.py +157 -0
  241. pinecall/runner/main.py +306 -0
  242. pinecall/session/__init__.py +0 -0
  243. pinecall/session/_agent.py +141 -0
  244. pinecall/session/_hearing.py +101 -0
  245. pinecall/session/_livekit.py +195 -0
  246. pinecall/session/_prompt.py +147 -0
  247. pinecall/session/_recording.py +237 -0
  248. pinecall/session/call.py +367 -0
  249. pinecall/session/clock.py +40 -0
  250. pinecall/session/hold.py +239 -0
  251. pinecall/session/room.py +626 -0
  252. pinecall/session/session.py +690 -0
  253. pinecall/session/text.py +144 -0
  254. pinecall/session/tools.py +483 -0
  255. pinecall/session/voice.py +118 -0
  256. pinecall/session/widget.py +201 -0
  257. pinecall/tenancy/__init__.py +0 -0
  258. pinecall/tenancy/admission.py +204 -0
  259. pinecall/tenancy/agents.py +164 -0
  260. pinecall/tenancy/canary.py +126 -0
  261. pinecall/tenancy/carrier_networks.py +156 -0
  262. pinecall/tenancy/carriers.py +265 -0
  263. pinecall/tenancy/codes.py +196 -0
  264. pinecall/tenancy/consents.py +167 -0
  265. pinecall/tenancy/dial_policy.py +405 -0
  266. pinecall/tenancy/disclosure.py +41 -0
  267. pinecall/tenancy/erasure.py +204 -0
  268. pinecall/tenancy/export.py +167 -0
  269. pinecall/tenancy/hosted_running.py +166 -0
  270. pinecall/tenancy/hosting.py +475 -0
  271. pinecall/tenancy/judges.py +126 -0
  272. pinecall/tenancy/keys.py +467 -0
  273. pinecall/tenancy/knocks.py +40 -0
  274. pinecall/tenancy/letters.py +378 -0
  275. pinecall/tenancy/mail.py +458 -0
  276. pinecall/tenancy/org_secrets.py +123 -0
  277. pinecall/tenancy/orgs.py +78 -0
  278. pinecall/tenancy/people.py +543 -0
  279. pinecall/tenancy/personas.py +145 -0
  280. pinecall/tenancy/policy.py +65 -0
  281. pinecall/tenancy/prompts.py +48 -0
  282. pinecall/tenancy/reads.py +73 -0
  283. pinecall/tenancy/recording_keys.py +41 -0
  284. pinecall/tenancy/remembered.py +148 -0
  285. pinecall/tenancy/retention.py +86 -0
  286. pinecall/tenancy/scopes.py +406 -0
  287. pinecall/tenancy/signin.py +622 -0
  288. pinecall/tenancy/spend.py +69 -0
  289. pinecall/tenancy/sso.py +431 -0
  290. pinecall/tenancy/sso_domains.py +132 -0
  291. pinecall/tenancy/throttle.py +88 -0
  292. pinecall/tenancy/tokens.py +400 -0
  293. pinecall/tenancy/traceback.py +70 -0
  294. pinecall/tenancy/usage.py +136 -0
  295. pinecall/tenancy/vault.py +204 -0
  296. pinecall/tenancy/words.py +144 -0
  297. pinecall/wire/__init__.py +0 -0
  298. pinecall/wire/commands.py +350 -0
  299. pinecall/wire/events.py +655 -0
  300. pinecall/wire/frames.py +65 -0
  301. pinecall/wire/metrics.py +287 -0
  302. pinecall/wire/parts.py +320 -0
  303. pinecall/wire/rest/__init__.py +0 -0
  304. pinecall/wire/rest/accounts.py +512 -0
  305. pinecall/wire/rest/agents.py +172 -0
  306. pinecall/wire/rest/calls.py +397 -0
  307. pinecall/wire/rest/evals.py +453 -0
  308. pinecall/wire/rest/fleet.py +69 -0
  309. pinecall/wire/rest/hosting.py +174 -0
  310. pinecall/wire/rest/numbers.py +280 -0
  311. pinecall/wire/rest/ops.py +342 -0
  312. pinecall/wire/rest/providers.py +83 -0
  313. pinecall/wire/rest/retrieval.py +301 -0
  314. pinecall/wire/rest/settings.py +184 -0
  315. pinecall/wire/rest/usage.py +245 -0
  316. pinecall/wire/room.py +61 -0
  317. pinecall/wire/scores.py +49 -0
  318. pinecall/wire/state.py +243 -0
  319. pinecall/worker/__init__.py +0 -0
  320. pinecall/worker/_job.py +655 -0
  321. pinecall/worker/_recorder.py +82 -0
  322. pinecall/worker/_traces.py +48 -0
  323. pinecall/worker/main.py +307 -0
  324. pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
  325. pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
  326. pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
  327. pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
  328. pinecall_runtime-0.1.6.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,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))