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,451 @@
1
+ """The org's dataset: real calls kept as goldens, born at hang-up, decided, and played again."""
2
+
3
+ import datetime as dt
4
+ import re
5
+ import unicodedata
6
+ from collections.abc import Sequence
7
+ from dataclasses import dataclass, field
8
+ from uuid import uuid4
9
+
10
+ from psycopg.errors import UniqueViolation
11
+ from psycopg.types.json import Jsonb
12
+
13
+ from pinecall.domain.errors import Conflict, DeclarationRefused, NotFound
14
+ from pinecall.domain.names import Env
15
+ from pinecall.evals.spoken import A_SIMULATED_CALLER
16
+ from pinecall.postgres.pool import Pool
17
+ from pinecall.wire.events import (
18
+ CallStarted,
19
+ EventReceived,
20
+ MemoryOps,
21
+ StateChanged,
22
+ ToolCall,
23
+ UserTurnEnded,
24
+ event_of,
25
+ )
26
+ from pinecall.wire.frames import Entry
27
+ from pinecall.wire.rest.evals import BrokeOn, CaseDecision, EvalCase, EventStep, Expect, Golden
28
+ from pinecall.wire.scores import CallScore, Judgment
29
+
30
+ A_CASE = "case_"
31
+
32
+
33
+ # The inbox is for reading: past this many cases waiting on one agent, a broken call is judged as
34
+ # ever and not kept, until a person decides some.
35
+ PENDING_AT_MOST = 50
36
+
37
+
38
+ # Born at hang-up, a case is the platform's, not a person's.
39
+ THE_PANEL = "the hang-up panel"
40
+
41
+
42
+ # Settled into fields of code; every other judge is asked again by name in `expect.judges`.
43
+ BY_CODE = frozenset({"consent", "grounded"})
44
+
45
+
46
+ # Its rule is the simulated caller's, and a case plays written lines: nobody holds it.
47
+ UNPLAYABLE = frozenset({"persona"})
48
+
49
+
50
+ # The words of the first caller line a born case is named by.
51
+ NAMED_BY_WORDS = 4
52
+
53
+
54
+ TAKEN = "agent {agent} has a case named {name} already: forget it first, or promote under another"
55
+
56
+
57
+ NOTHING_SAID = "call {call} has no line of the caller's to play again"
58
+
59
+
60
+ NO_SUCH_CASE = "no case {id} in this key's org"
61
+
62
+
63
+ UNKNOWN_CASES = "agent {agent} has no case named {names}"
64
+
65
+
66
+ WRONG_ONLY_WHEN_DISMISSED = (
67
+ "judge_was_wrong says a judge broke where it should have held, so it dismisses the case: "
68
+ "send it with status dismissed"
69
+ )
70
+
71
+
72
+ NOT_WHAT_BROKE = "case {name} did not break on {judge}: it broke on {broke}"
73
+
74
+
75
+ # The note is kept on the judge's calibration label: with no judge it would be dropped unsaid.
76
+ A_NOTE_ALONE = (
77
+ "note is kept on the calibration label of the judge that was wrong: send judge_was_wrong"
78
+ )
79
+
80
+
81
+ PUT = """
82
+ INSERT INTO eval_cases (id, org, agent, name, golden, source_call, source_env, held_out, author,
83
+ status, broke, source_version)
84
+ VALUES (%(id)s, %(org)s, %(agent)s, %(name)s, %(golden)s, %(source_call)s, %(source_env)s,
85
+ %(held_out)s, %(author)s, %(status)s, %(broke)s, %(source_version)s)
86
+ RETURNING id, agent, name, golden, source_call, source_env, held_out, author, created_at,
87
+ status, broke, source_version, kept_in_repo, decided_by
88
+ """
89
+
90
+
91
+ # One statement, so two seals at once never both pass the count: the second finds the first.
92
+ BORN = """
93
+ INSERT INTO eval_cases (id, org, agent, name, golden, source_call, source_env, held_out, author,
94
+ status, broke, source_version)
95
+ SELECT %(id)s, %(org)s, %(agent)s, %(name)s, %(golden)s, %(source_call)s, %(source_env)s, false,
96
+ %(author)s, 'pending', %(broke)s, %(source_version)s
97
+ WHERE (SELECT count(*) FROM eval_cases
98
+ WHERE org = %(org)s AND agent = %(agent)s AND status = 'pending') < %(at_most)s
99
+ AND NOT EXISTS (SELECT 1 FROM eval_cases WHERE org = %(org)s AND source_call = %(source_call)s)
100
+ ON CONFLICT (org, agent, name) DO NOTHING
101
+ RETURNING id, agent, name, golden, source_call, source_env, held_out, author, created_at,
102
+ status, broke, source_version, kept_in_repo, decided_by
103
+ """
104
+
105
+
106
+ LISTED = """
107
+ SELECT id, agent, name, golden, source_call, source_env, held_out, author, created_at,
108
+ status, broke, source_version, kept_in_repo, decided_by
109
+ FROM eval_cases
110
+ WHERE org = %(org)s
111
+ AND (%(agent)s::text IS NULL OR agent = %(agent)s)
112
+ AND (%(status)s::text IS NULL OR status = %(status)s)
113
+ ORDER BY agent, status = 'pending' DESC, created_at DESC, name
114
+ """
115
+
116
+
117
+ WAITING = """
118
+ SELECT count(*) AS pending FROM eval_cases
119
+ WHERE org = %(org)s AND (%(agent)s::text IS NULL OR agent = %(agent)s) AND status = 'pending'
120
+ """
121
+
122
+
123
+ # A nightly run reads what a person approved and the repository does not hold; a case named is
124
+ # read whatever its status, so a pending one can be reproduced before it is decided.
125
+ PICKED = """
126
+ SELECT id, agent, name, golden, source_call, source_env, held_out, author, created_at,
127
+ status, broke, source_version, kept_in_repo, decided_by
128
+ FROM eval_cases
129
+ WHERE org = %(org)s AND agent = %(agent)s
130
+ AND (name = ANY(%(names)s)
131
+ OR (%(every)s AND status = 'approved' AND NOT held_out AND NOT kept_in_repo))
132
+ ORDER BY name
133
+ """
134
+
135
+
136
+ FOUND = """
137
+ SELECT id, agent, name, golden, source_call, source_env, held_out, author, created_at,
138
+ status, broke, source_version, kept_in_repo, decided_by
139
+ FROM eval_cases
140
+ WHERE id = %(id)s AND org = %(org)s
141
+ """
142
+
143
+
144
+ DECIDED = """
145
+ UPDATE eval_cases
146
+ SET status = coalesce(%(status)s, status),
147
+ held_out = coalesce(%(held_out)s, held_out),
148
+ kept_in_repo = coalesce(%(kept_in_repo)s, kept_in_repo),
149
+ decided_by = CASE WHEN %(status)s::text IS NULL THEN decided_by ELSE %(by)s END,
150
+ decided_at = CASE WHEN %(status)s::text IS NULL THEN decided_at ELSE now() END
151
+ WHERE id = %(id)s AND org = %(org)s
152
+ RETURNING id, agent, name, golden, source_call, source_env, held_out, author, created_at,
153
+ status, broke, source_version, kept_in_repo, decided_by
154
+ """
155
+
156
+
157
+ FORGET = "DELETE FROM eval_cases WHERE id = %(id)s AND org = %(org)s RETURNING id"
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class Promoted:
162
+ """A finished call to keep as a case: whose, from which world, named how, by whom."""
163
+
164
+ org: str
165
+ env: Env
166
+ name: str
167
+ author: str
168
+ held_out: bool = False
169
+ status: str = "approved"
170
+ broke: Sequence[BrokeOn] = field(default_factory=tuple[BrokeOn, ...])
171
+ source_version: int | None = None
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class Born:
176
+ """Where a call that broke was answered: the org, the world, the agent, the settings version."""
177
+
178
+ org: str
179
+ env: Env
180
+ agent: str
181
+ version: int | None
182
+
183
+
184
+ def golden_of(entries: Sequence[Entry], name: str, expect: Expect) -> Golden:
185
+ """The call's caller lines, the state it opened in, the app's facts and what it recalled."""
186
+ lines: list[str] = []
187
+ state: dict[str, object] = {}
188
+ events: list[EventStep] = []
189
+ recalled: list[str] = []
190
+ for entry in entries:
191
+ if entry.type not in {"turn.user", "state.changed", "event.received", "memory.ops"}:
192
+ continue
193
+ match event_of(entry):
194
+ case UserTurnEnded() as heard if heard.text.strip():
195
+ lines.append(heard.text)
196
+ case StateChanged() as changed if not lines and not state:
197
+ state = dict(changed.state)
198
+ case EventReceived() as arrived if arrived.source == "app":
199
+ events.append(
200
+ EventStep(after_turn=len(lines), name=arrived.name, data=arrived.data)
201
+ )
202
+ case MemoryOps() as memory:
203
+ facts = (fact.text for op in memory.ops if op.op == "recall" for fact in op.facts)
204
+ recalled.extend(fact for fact in facts if fact not in recalled)
205
+ case _:
206
+ continue
207
+ call = next((entry.call for entry in entries if entry.call is not None), "")
208
+ if not lines:
209
+ raise Conflict(NOTHING_SAID.format(call=call))
210
+ started = entries[0].ts if entries else 0.0
211
+ # A field the call gave nothing for is left out, as a golden written by hand leaves it out.
212
+ given = {"state": state, "memory": recalled, "events": [event.written() for event in events]}
213
+ return Golden.model_validate(
214
+ {
215
+ "name": name,
216
+ "input": lines,
217
+ "today": dt.datetime.fromtimestamp(started, dt.UTC).date(),
218
+ "expect": expect.written(),
219
+ "promoted_from": call,
220
+ **{field: value for field, value in given.items() if value},
221
+ }
222
+ )
223
+
224
+
225
+ # `state.changed` holds the whole state, so the last one at or before the cut is where it opens.
226
+ def cut_at(entries: Sequence[Entry], seq: int) -> list[Entry]:
227
+ """The call from after a seq on, opening in the state it was in at that seq."""
228
+ if seq == 0:
229
+ return list(entries)
230
+ states = [entry for entry in entries if entry.type == "state.changed" and entry.seq <= seq]
231
+ return [*states[-1:], *(entry for entry in entries if entry.seq > seq)]
232
+
233
+
234
+ # A verdict knows what broke, never what was right: the expect says "not like this again", and a
235
+ # person writes what the agent should have done.
236
+ def expect_of(judgments: Sequence[Judgment], entries: Sequence[Entry]) -> Expect:
237
+ """What a call's broken verdicts say must not happen again, as a golden's expect."""
238
+ broken = [judgment for judgment in judgments if judgment.verdict == "broken"]
239
+ not_tools: list[str] = []
240
+ for judgment in broken:
241
+ if judgment.name == "consent":
242
+ seqs = frozenset(judgment.evidence.seqs)
243
+ ran = (entry for entry in entries if entry.type == "tool.call" and entry.seq in seqs)
244
+ named = (event_of(entry) for entry in ran)
245
+ not_tools.extend(
246
+ call.name
247
+ for call in named
248
+ if isinstance(call, ToolCall) and call.name not in not_tools
249
+ )
250
+ by_name = [
251
+ judgment.name
252
+ for judgment in broken
253
+ if judgment.name not in BY_CODE and judgment.name not in UNPLAYABLE
254
+ ]
255
+ # Only what broke is set, so the golden a person reads or pulls says nothing it does not mean.
256
+ fields: dict[str, object] = {}
257
+ if not_tools:
258
+ fields["not_tools"] = not_tools
259
+ if any(judgment.name == "grounded" for judgment in broken):
260
+ fields["grounded"] = True
261
+ if by_name:
262
+ fields["judges"] = list(dict.fromkeys(by_name))
263
+ return Expect.model_validate(fields)
264
+
265
+
266
+ def broke_of(score: CallScore) -> list[BrokeOn]:
267
+ """The judges that broke on a call, each with its reason."""
268
+ return [
269
+ BrokeOn(judge=judgment.name, reason=judgment.reason)
270
+ for judgment in score.judges
271
+ if judgment.verdict == "broken"
272
+ ]
273
+
274
+
275
+ # A call a run opened is never judged at hang-up, so it never gets here; a simulated caller is
276
+ # meant to break things, and a call nobody spoke in has nothing to play again.
277
+ async def kept_at_hangup(
278
+ pool: Pool, entries: Sequence[Entry], score: CallScore, born: Born
279
+ ) -> None:
280
+ """Keep a call one of its judges broke on as a pending case, unless the inbox is full."""
281
+ broke = broke_of(score)
282
+ if not broke or _simulated(entries) or not _caller_spoke(entries):
283
+ return
284
+ call = next((entry.call for entry in entries if entry.call is not None), "")
285
+ name = _name_of(broke[0].judge, entries, call)
286
+ golden = golden_of(entries, name, expect_of(score.judges, entries))
287
+ params = {
288
+ "id": f"{A_CASE}{uuid4().hex[:12]}",
289
+ "org": born.org,
290
+ "agent": born.agent,
291
+ "name": name,
292
+ "golden": Jsonb(golden.written()),
293
+ "source_call": call,
294
+ "source_env": born.env,
295
+ "author": THE_PANEL,
296
+ "broke": Jsonb([judgment.written() for judgment in broke]),
297
+ "source_version": born.version,
298
+ "at_most": PENDING_AT_MOST,
299
+ }
300
+ async with pool.connection() as connection:
301
+ await connection.execute(BORN, params)
302
+
303
+
304
+ async def promoted(pool: Pool, golden: Golden, agent: str, promoting: Promoted) -> EvalCase:
305
+ """Keep the golden as a case of the org's dataset; a name the agent has already is refused."""
306
+ named = golden.model_copy(update={"name": promoting.name})
307
+ params = {
308
+ "id": f"{A_CASE}{uuid4().hex[:12]}",
309
+ "org": promoting.org,
310
+ "agent": agent,
311
+ "name": promoting.name,
312
+ "golden": Jsonb(named.written()),
313
+ "source_call": named.promoted_from,
314
+ "source_env": promoting.env,
315
+ "held_out": promoting.held_out,
316
+ "author": promoting.author,
317
+ "status": promoting.status,
318
+ "broke": Jsonb([judgment.written() for judgment in promoting.broke]),
319
+ "source_version": promoting.source_version,
320
+ }
321
+ try:
322
+ async with pool.connection() as connection:
323
+ row = await (await connection.execute(PUT, params)).fetchone()
324
+ except UniqueViolation as taken:
325
+ raise Conflict(TAKEN.format(agent=agent, name=promoting.name)) from taken
326
+ if row is None:
327
+ raise NotFound(NO_SUCH_CASE.format(id=params["id"]))
328
+ return _case_of(row)
329
+
330
+
331
+ async def listed(
332
+ pool: Pool, org: str, agent: str | None, status: str | None = None
333
+ ) -> list[EvalCase]:
334
+ """The org's cases, one agent's or every one, one status or all, the pending ones first."""
335
+ params = {"org": org, "agent": agent, "status": status}
336
+ async with pool.connection() as connection:
337
+ rows = await (await connection.execute(LISTED, params)).fetchall()
338
+ return [_case_of(row) for row in rows]
339
+
340
+
341
+ async def waiting(pool: Pool, org: str, agent: str | None) -> int:
342
+ """How many cases wait for a person, one agent's or the org's."""
343
+ async with pool.connection() as connection:
344
+ row = await (await connection.execute(WAITING, {"org": org, "agent": agent})).fetchone()
345
+ return 0 if row is None else int(row["pending"])
346
+
347
+
348
+ async def picked(
349
+ pool: Pool, org: str, agent: str, names: Sequence[str], *, every: bool
350
+ ) -> list[Golden]:
351
+ """The goldens a run plays: the cases named, and with every, the approved ones not held out."""
352
+ params = {"org": org, "agent": agent, "names": list(names), "every": every}
353
+ async with pool.connection() as connection:
354
+ rows = await (await connection.execute(PICKED, params)).fetchall()
355
+ found = [_case_of(row) for row in rows]
356
+ missing = sorted(set(names) - {case.name for case in found})
357
+ if missing:
358
+ raise NotFound(UNKNOWN_CASES.format(agent=agent, names=", ".join(missing)))
359
+ return [case.golden for case in found]
360
+
361
+
362
+ async def found(pool: Pool, org: str, case: str) -> EvalCase:
363
+ """One of the org's cases; another org's, or nobody's, is a 404."""
364
+ async with pool.connection() as connection:
365
+ row = await (await connection.execute(FOUND, {"id": case, "org": org})).fetchone()
366
+ if row is None:
367
+ raise NotFound(NO_SUCH_CASE.format(id=case))
368
+ return _case_of(row)
369
+
370
+
371
+ def check_decision(case: EvalCase, decision: CaseDecision) -> None:
372
+ """Refuse a judge called wrong on a case not dismissed or not broken on, and a note alone."""
373
+ wrong = decision.judge_was_wrong
374
+ if wrong is None:
375
+ if decision.note is not None:
376
+ raise DeclarationRefused(A_NOTE_ALONE)
377
+ return
378
+ if decision.status != "dismissed":
379
+ raise DeclarationRefused(WRONG_ONLY_WHEN_DISMISSED)
380
+ broke = [judgment.judge for judgment in case.broke]
381
+ if wrong not in broke:
382
+ named = ", ".join(broke) or "no judge (a person kept it)"
383
+ raise DeclarationRefused(NOT_WHAT_BROKE.format(name=case.name, judge=wrong, broke=named))
384
+
385
+
386
+ async def decided(pool: Pool, org: str, case: str, decision: CaseDecision, by: str) -> EvalCase:
387
+ """Write what a person decided of one case: its status, held out, kept in the repository."""
388
+ params = {
389
+ "id": case,
390
+ "org": org,
391
+ "status": decision.status,
392
+ "held_out": decision.held_out,
393
+ "kept_in_repo": decision.kept_in_repo,
394
+ "by": by,
395
+ }
396
+ async with pool.connection() as connection:
397
+ row = await (await connection.execute(DECIDED, params)).fetchone()
398
+ if row is None:
399
+ raise NotFound(NO_SUCH_CASE.format(id=case))
400
+ return _case_of(row)
401
+
402
+
403
+ async def forgotten(pool: Pool, org: str, case: str) -> None:
404
+ """Forget one of the org's cases; another org's, or nobody's, is a 404."""
405
+ async with pool.connection() as connection:
406
+ row = await (await connection.execute(FORGET, {"id": case, "org": org})).fetchone()
407
+ if row is None:
408
+ raise NotFound(NO_SUCH_CASE.format(id=case))
409
+
410
+
411
+ def _name_of(judge: str, entries: Sequence[Entry], call: str) -> str:
412
+ """The first judge that broke, the caller's first words, and the call's last six characters."""
413
+ first = next(
414
+ (
415
+ heard.text
416
+ for entry in entries
417
+ if entry.type == "turn.user"
418
+ and isinstance(heard := event_of(entry), UserTurnEnded)
419
+ and heard.text.strip()
420
+ ),
421
+ "",
422
+ )
423
+ bare = unicodedata.normalize("NFKD", first).encode("ascii", "ignore").decode().casefold()
424
+ words = re.findall(r"[a-z0-9]+", bare)[:NAMED_BY_WORDS]
425
+ return "-".join([judge, *words, call[-6:].casefold()])
426
+
427
+
428
+ def _simulated(entries: Sequence[Entry]) -> bool:
429
+ for entry in entries:
430
+ if entry.type == "call.started" and isinstance(started := event_of(entry), CallStarted):
431
+ return started.persona is not None or started.from_ == A_SIMULATED_CALLER
432
+ return False
433
+
434
+
435
+ def _caller_spoke(entries: Sequence[Entry]) -> bool:
436
+ return any(
437
+ entry.type == "turn.user"
438
+ and isinstance(heard := event_of(entry), UserTurnEnded)
439
+ and bool(heard.text.strip())
440
+ for entry in entries
441
+ )
442
+
443
+
444
+ def _case_of(row: dict[str, object]) -> EvalCase:
445
+ created = row["created_at"]
446
+ return EvalCase.model_validate(
447
+ {
448
+ **row,
449
+ "created_at": created.timestamp() if isinstance(created, dt.datetime) else created,
450
+ }
451
+ )