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,331 @@
1
+ """A golden played on a written call, and the judges its expectations set."""
2
+
3
+ import asyncio
4
+ from collections.abc import Callable, Sequence
5
+ from dataclasses import dataclass
6
+
7
+ from pinecall.domain.errors import PinecallError
8
+ from pinecall.domain.names import JsonObject
9
+ from pinecall.evals.case import AGENT, CALLER, PUNCTUATION, Case, calls_of, said_by
10
+ from pinecall.evals.judges import CaseJudge, consent_judge, failing, grounded_judge, passing
11
+ from pinecall.log.logs import Subscription
12
+ from pinecall.session import text
13
+ from pinecall.session.call import Lookup
14
+ from pinecall.session.session import Session
15
+ from pinecall.wire.commands import CallEvent
16
+ from pinecall.wire.parts import EndedBy, EndReason, PlatformTool
17
+ from pinecall.wire.rest.evals import EventStep, Golden, Register
18
+
19
+ # The wire has no "the app is done reacting": the log going quiet is the sign. Long enough for a
20
+ # render and its re-render to be one burst, short enough to cost little per turn.
21
+ QUIET_S = 0.15
22
+
23
+
24
+ # An app that never answers cannot hold a run up longer than this per step.
25
+ AT_MOST_S = 2.0
26
+
27
+
28
+ # The source of a golden's facts, so the log never passes them off as real memory.
29
+ A_GOLDEN = "golden"
30
+
31
+
32
+ APP_DETACHED: tuple[EndReason, EndedBy] = ("app_detached", "platform")
33
+
34
+
35
+ HUNG_UP: tuple[EndReason, EndedBy] = ("caller_hung_up", "caller")
36
+
37
+
38
+ # A golden the call refused (an event nobody declared) ends the call it opened.
39
+ BROKE: tuple[EndReason, EndedBy] = ("error", "platform")
40
+
41
+
42
+ HEARD = "Every line this golden puts in the caller's mouth reached the agent."
43
+
44
+
45
+ TOOLS = "Every tool this golden names was called in the conversation."
46
+
47
+
48
+ NOT_TOOLS = "The conversation called none of the tools this golden forbids."
49
+
50
+
51
+ SAYS = "The agent said every phrase this golden names."
52
+
53
+
54
+ SAYS_ANY = "The agent said at least one of the phrases this golden accepts."
55
+
56
+
57
+ SILENCE = "The agent said none of the phrases this golden names."
58
+
59
+
60
+ ANSWERED = "The agent answered every fact that arrived mid-call."
61
+
62
+
63
+ STAYED_QUIET = "The agent carried on without answering the facts that arrived mid-call."
64
+
65
+
66
+ # A check that could not look must not pass.
67
+ NO_EVENT = "this golden expects a reply to an event, and no event.received reached the call"
68
+
69
+
70
+ REGISTER = "The agent addressed the caller as {register} in every one of its turns."
71
+
72
+
73
+ ON_THE_PANEL = "The hang-up judge {name} held of this call."
74
+
75
+
76
+ # A check that could not look must not pass: a judge the panel does not hold for this call (a
77
+ # persona's, which needs a simulated caller; a name the org never wrote) breaks the golden.
78
+ NOT_ON_THE_PANEL = (
79
+ "this golden asks {name}, and the panel this call meets has no such judge: it holds {panel}"
80
+ )
81
+
82
+
83
+ # Unmistakable tú; `té` (tea) carries its accent, so `te` never matches it.
84
+ TUTEO: frozenset[str] = frozenset(
85
+ {"tú", "ti", "te", "contigo", "tu", "tus", "tuyo", "tuya", "tuyos", "tuyas"}
86
+ )
87
+
88
+
89
+ # `le`, `les`, `su` and `sus` are left out: they are as often the third person.
90
+ USTEO: frozenset[str] = frozenset({"usted", "ustedes", "consigo", "suyo", "suya", "suyos", "suyas"})
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class Played:
95
+ """A golden played to its end, or to where the app let go of the agent."""
96
+
97
+ held: bool
98
+ # The requests the model was sent, one per request, as they went.
99
+ requests: tuple[JsonObject, ...]
100
+
101
+
102
+ def golden_judges(golden: Golden, case: Case, panel: Sequence[CaseJudge] = ()) -> list[CaseJudge]:
103
+ """Consent, one judge per expectation set, then the hang-up judges it names, from `panel`."""
104
+ expect = golden.expect
105
+ judges = [consent_judge(case)]
106
+ # Without it an unheard caller would pass every "never did X".
107
+ if golden.input:
108
+ judges.append(_heard(case, len(golden.input)))
109
+ if expect.tools:
110
+ judges.append(_tools(case, expect.tools))
111
+ if expect.not_tools:
112
+ judges.append(_not_tools(case, expect.not_tools))
113
+ if expect.not_said:
114
+ judges.append(_silence(case, expect.not_said))
115
+ if expect.says:
116
+ judges.append(_says(case, expect.says))
117
+ if expect.says_any:
118
+ judges.append(_says_any(case, expect.says_any))
119
+ if expect.grounded:
120
+ judges.append(grounded_judge(case))
121
+ if expect.addressed_as is not None:
122
+ judges.append(_register_judge(case, expect.addressed_as))
123
+ if expect.replies is not None:
124
+ judges.append(_replies(case, replies=expect.replies))
125
+ present = {judge.name for judge in judges}
126
+ judges.extend(_on_the_panel(panel, name) for name in expect.judges if name not in present)
127
+ return judges
128
+
129
+
130
+ def events_after(golden: Golden, turn: int) -> tuple[EventStep, ...]:
131
+ """The facts injected after this many caller lines, in the order the golden lists them."""
132
+ return tuple(event for event in golden.events if event.after_turn == turn)
133
+
134
+
135
+ # recall reads the golden's facts and nothing else; search is the real index, since that is what
136
+ # the golden is asking about.
137
+ def golden_lookup(facts: Sequence[str], search: Lookup) -> Lookup:
138
+ """The lookup a golden's call runs: recall answered by its facts, search by the index."""
139
+
140
+ async def lookup(tool: PlatformTool, arguments: JsonObject, speech: str | None) -> JsonObject:
141
+ if tool != "recall":
142
+ return await search(tool, arguments, speech)
143
+ return {"facts": [{"text": fact, "source": A_GOLDEN} for fact in facts]}
144
+
145
+ return lookup
146
+
147
+
148
+ # The call opens with no greeting (a run starts mid-conversation); the facts go through the
149
+ # app's own `call.event`, so an event the agent never declared is refused as on a real call.
150
+ async def drive(
151
+ session: Session, golden: Golden, heard: Subscription, *, is_held: Callable[[], bool]
152
+ ) -> Played:
153
+ """Start the call, seed its state, say each line and inject each fact, then hang up."""
154
+ await session.start()
155
+ try:
156
+ kept_on = await _played(session, golden, heard, is_held=is_held)
157
+ except PinecallError:
158
+ await text.end(session, *BROKE)
159
+ raise
160
+ await text.end(session, *(HUNG_UP if kept_on else APP_DETACHED))
161
+ return Played(held=kept_on, requests=tuple(session.call.requests))
162
+
163
+
164
+ async def settled(heard: Subscription, *, quiet_s: float = QUIET_S) -> None:
165
+ """Return once nothing reached the log for `quiet_s`, and always within AT_MOST_S."""
166
+ loop = asyncio.get_running_loop()
167
+ deadline = loop.time() + AT_MOST_S
168
+ while (left := deadline - loop.time()) > 0:
169
+ try:
170
+ await asyncio.wait_for(anext(heard), min(quiet_s, left))
171
+ except (TimeoutError, StopAsyncIteration):
172
+ return
173
+
174
+
175
+ async def _played(
176
+ session: Session, golden: Golden, heard: Subscription, *, is_held: Callable[[], bool]
177
+ ) -> bool:
178
+ await settled(heard)
179
+ for turn, line in enumerate([*golden.input, None]):
180
+ for event in events_after(golden, turn):
181
+ await session.apply(CallEvent(name=event.name, data=dict(event.data)))
182
+ await settled(heard)
183
+ if not is_held():
184
+ return False
185
+ if line is None:
186
+ break
187
+ await text.hears(session, line)
188
+ await settled(heard)
189
+ return True
190
+
191
+
192
+ def _on_the_panel(panel: Sequence[CaseJudge], name: str) -> CaseJudge:
193
+ """The panel's judge of that name, or one that breaks saying the panel has none."""
194
+ found = next((judge for judge in panel if judge.name == name), None)
195
+ if found is not None:
196
+ return found
197
+ names = ", ".join(judge.name for judge in panel) or "no judge at all"
198
+ ruling = failing(NOT_ON_THE_PANEL.format(name=name, panel=names))
199
+ return CaseJudge(name, ON_THE_PANEL.format(name=name), ruling)
200
+
201
+
202
+ def _register_judge(case: Case, expected: Register) -> CaseJudge:
203
+ """Whether the agent never used the other register's words."""
204
+ other = USTEO if expected == "tu" else TUTEO
205
+ turns = said_by(case, AGENT)
206
+ slips = [
207
+ (number, word) for number, turn in enumerate(turns, 1) for word in _marked(turn, other)
208
+ ]
209
+ if slips:
210
+ spoken = "; ".join(f"{word!r} in agent turn {number}" for number, word in slips)
211
+ ruling = failing(f"the agent was asked for {expected} and said {spoken}")
212
+ else:
213
+ ruling = passing(
214
+ f"no word of the other register in {len(turns)} agent turn(s), asked for {expected}"
215
+ )
216
+ return CaseJudge("register", REGISTER.format(register=expected), ruling)
217
+
218
+
219
+ def _heard(case: Case, lines: int) -> CaseJudge:
220
+ heard = len(said_by(case, CALLER))
221
+ if heard < lines:
222
+ word = "line" if lines == 1 else "lines"
223
+ ruling = failing(
224
+ f"the golden says {lines} {word} and the agent heard {heard}: "
225
+ "whatever else this call did, it was not this golden"
226
+ )
227
+ else:
228
+ ruling = passing(f"the agent heard all {lines} of the caller's lines")
229
+ return CaseJudge("heard", HEARD, ruling)
230
+
231
+
232
+ # The order the tools ran in is not judged.
233
+ def _tools(case: Case, names: Sequence[str]) -> CaseJudge:
234
+ ran = {called.name for called in calls_of(case)}
235
+ missing = [name for name in names if name not in ran]
236
+ if missing:
237
+ what_ran = ", ".join(sorted(ran)) or "no tool at all"
238
+ ruling = failing(f"the golden expects {', '.join(missing)}, and this call ran {what_ran}")
239
+ else:
240
+ ruling = passing(f"every expected tool ran: {', '.join(names)}")
241
+ return CaseJudge("tools", TOOLS, ruling)
242
+
243
+
244
+ def _not_tools(case: Case, names: Sequence[str]) -> CaseJudge:
245
+ forbidden = frozenset(names)
246
+ slips = [
247
+ f"{line.tool} at seq {line.seq}"
248
+ for line in case.gate
249
+ if line.kind == "tool.call" and line.tool in forbidden
250
+ ]
251
+ if slips:
252
+ ruling = failing(
253
+ f"the golden forbids {', '.join(names)}, and this call ran {'; '.join(slips)}"
254
+ )
255
+ else:
256
+ ruling = passing(f"none of the {len(names)} forbidden tool(s) ran")
257
+ return CaseJudge("not_tools", NOT_TOOLS, ruling)
258
+
259
+
260
+ def _says(case: Case, phrases: Sequence[str]) -> CaseJudge:
261
+ turns = [turn.casefold() for turn in said_by(case, AGENT)]
262
+ missing = [phrase for phrase in phrases if not any(phrase.casefold() in turn for turn in turns)]
263
+ if missing:
264
+ ruling = failing(f"the agent never said {', '.join(repr(phrase) for phrase in missing)}")
265
+ else:
266
+ ruling = passing(f"the agent said all {len(phrases)} expected phrase(s)")
267
+ return CaseJudge("says", SAYS, ruling)
268
+
269
+
270
+ def _says_any(case: Case, phrases: Sequence[str]) -> CaseJudge:
271
+ turns = [turn.casefold() for turn in said_by(case, AGENT)]
272
+ found = [phrase for phrase in phrases if any(phrase.casefold() in turn for turn in turns)]
273
+ if found:
274
+ ruling = passing(f"the agent said {found[0]!r}, one of the {len(phrases)} accepted")
275
+ else:
276
+ accepted = ", ".join(repr(phrase) for phrase in phrases)
277
+ ruling = failing(f"the agent said none of {accepted}")
278
+ return CaseJudge("says_any", SAYS_ANY, ruling)
279
+
280
+
281
+ def _silence(case: Case, phrases: Sequence[str]) -> CaseJudge:
282
+ turns = [turn.casefold() for turn in said_by(case, AGENT)]
283
+ slips = [
284
+ f"{phrase!r} in agent turn {number}"
285
+ for phrase in phrases
286
+ for number, turn in enumerate(turns, 1)
287
+ if phrase.casefold() in turn
288
+ ]
289
+ if slips:
290
+ ruling = failing(f"the golden forbids these and the agent said {'; '.join(slips)}")
291
+ else:
292
+ ruling = passing(f"none of the {len(phrases)} forbidden phrase(s) was said")
293
+ return CaseJudge("silence", SILENCE, ruling)
294
+
295
+
296
+ # Whether the agent took the fact up, not when: the timing is the app's.
297
+ def _replies(case: Case, *, replies: bool) -> CaseJudge:
298
+ criteria = ANSWERED if replies else STAYED_QUIET
299
+ if not case.arrived:
300
+ return CaseJudge("replies", criteria, failing(NO_EVENT))
301
+ findings: list[str] = []
302
+ for fact in case.arrived:
303
+ after = next(
304
+ (turn for turn in case.turns if turn.role == AGENT and turn.seq > fact.seq), None
305
+ )
306
+ carried = [str(value) for value in fact.data.values() if str(value)]
307
+ named = after is not None and (
308
+ not carried or any(value.casefold() in after.text.casefold() for value in carried)
309
+ )
310
+ if replies and after is None:
311
+ findings.append(f"{fact.name} at seq {fact.seq} was followed by no turn of the agent's")
312
+ elif replies and not named:
313
+ findings.append(f"the agent's turn after {fact.name} names nothing the event carried")
314
+ elif not replies and named:
315
+ findings.append(f"the agent took {fact.name} up, and this golden expects it quiet")
316
+ if findings:
317
+ return CaseJudge("replies", criteria, failing("; ".join(findings)))
318
+ kept = "taken up" if replies else "left alone"
319
+ return CaseJudge(
320
+ "replies", criteria, passing(f"all {len(case.arrived)} fact(s) that arrived were {kept}")
321
+ )
322
+
323
+
324
+ # Whole words only: `tu` is not `tutor`.
325
+ def _marked(turn: str, markers: frozenset[str]) -> list[str]:
326
+ found: list[str] = []
327
+ for word in turn.split():
328
+ bare = word.strip(PUNCTUATION).casefold()
329
+ if bare in markers and bare not in found:
330
+ found.append(bare)
331
+ return found