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
pinecall/evals/case.py ADDED
@@ -0,0 +1,327 @@
1
+ """A finished call as a judge reads it: turns, tool calls, states, evidence, the caller's rule."""
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Mapping, Sequence
6
+ from dataclasses import dataclass, field
7
+ from typing import Literal
8
+
9
+ from livekit.agents.evals import Verdict
10
+ from livekit.agents.llm import ChatContext, ChatItem, ChatMessage, FunctionCall, FunctionCallOutput
11
+
12
+ from pinecall.domain.agent import AgentConfig, SideEffect, ToolSpec
13
+ from pinecall.domain.errors import DeclarationRefused
14
+ from pinecall.domain.names import Direction, Json, JsonObject
15
+ from pinecall.evals.spoken import A_SIMULATED_CALLER
16
+ from pinecall.session.tools import result_text
17
+ from pinecall.wire.events import (
18
+ AgentTurnEnded,
19
+ CallStarted,
20
+ CallSummary,
21
+ ConfirmDeclined,
22
+ ConfirmGranted,
23
+ ConfirmRequest,
24
+ DocsSources,
25
+ EventReceived,
26
+ MemoryOps,
27
+ StateChanged,
28
+ ToolCall,
29
+ UserTurnEnded,
30
+ event_of,
31
+ )
32
+ from pinecall.wire.frames import Entry
33
+ from pinecall.wire.parts import ScoreVerdict, ToolResult
34
+
35
+ # livekit's two speakers.
36
+ type Role = Literal["user", "assistant"]
37
+
38
+
39
+ # The entries the consent rule reads, in the order the log wrote them.
40
+ type GateKind = Literal["tool.call", "confirm.request", "confirm.granted", "confirm.declined"]
41
+
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ PUNCTUATION = ".,;:¿?¡!()\"'"
47
+
48
+
49
+ METRICS = "metrics."
50
+
51
+
52
+ # `skipped` is ours alone: it marks a judge that was never asked.
53
+ VERDICT_WORDS: dict[Verdict, ScoreVerdict] = {"pass": "held", "fail": "broken", "maybe": "deferred"}
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class Called:
58
+ """One tool call of a turn: its arguments, and the text the model read back; None: no answer."""
59
+
60
+ call_id: str
61
+ name: str
62
+ arguments: JsonObject
63
+ answer: str | None = None
64
+ failed: bool = False
65
+ description: str | None = None
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class Arrived:
70
+ """One fact that reached the call from outside, at the seq that places it between turns."""
71
+
72
+ seq: int
73
+ name: str
74
+ data: JsonObject
75
+ source: str
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Said:
80
+ """One turn: who, what, livekit's own metric names beside it, what it read and what it ran."""
81
+
82
+ role: Role
83
+ text: str
84
+ seq: int = 0
85
+ speech_id: str = ""
86
+ interrupted: bool = False
87
+ metrics: Mapping[str, Json] = field(default_factory=dict[str, Json])
88
+ # The metrics.* entries of the same speech, as written.
89
+ blocks: tuple[JsonObject, ...] = ()
90
+ retrieved: tuple[str, ...] = ()
91
+ calls: tuple[Called, ...] = ()
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class GateLine:
96
+ """A tool call or a confirmation, in log order; the side effect comes from the declaration."""
97
+
98
+ seq: int
99
+ kind: GateKind
100
+ call_id: str
101
+ tool: str
102
+ audience: str | None = None
103
+ side_effect: SideEffect | None = None
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class Case:
108
+ """A finished call: its turns, the gate trace, the facts that arrived, and its evidence."""
109
+
110
+ call: str = ""
111
+ agent: str = ""
112
+ turns: tuple[Said, ...] = ()
113
+ gate: tuple[GateLine, ...] = ()
114
+ arrived: tuple[Arrived, ...] = ()
115
+ # Every state the call was in, in order: the view is rendered from them, not stored.
116
+ states: tuple[JsonObject, ...] = ()
117
+ # Knowledge text and the facts memory recalled.
118
+ evidence: tuple[str, ...] = ()
119
+ summary: JsonObject | None = None
120
+ # (accepts_when, declines_when) off call.started; None when the caller wrote no rule.
121
+ persona_rule: tuple[str, str] | None = None
122
+ # A persona played the caller: named on call.started, or the spoken caller's leg.
123
+ simulated: bool = False
124
+ declared: AgentConfig | None = None
125
+ # Off call.started; None for a log that never started.
126
+ direction: Direction | None = None
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class _Filed:
131
+ """The entries a turn is built from, filed by the speech or the tool call they belong to."""
132
+
133
+ tools: Mapping[str, ToolSpec]
134
+ said: list[tuple[Entry, UserTurnEnded | AgentTurnEnded]] = field(
135
+ default_factory=list[tuple[Entry, UserTurnEnded | AgentTurnEnded]]
136
+ )
137
+ called: dict[str, list[ToolCall]] = field(default_factory=dict[str, list[ToolCall]])
138
+ results: dict[str, ToolResult] = field(default_factory=dict[str, ToolResult])
139
+ sources: dict[str, list[str]] = field(default_factory=dict[str, list[str]])
140
+ blocks: dict[str, list[JsonObject]] = field(default_factory=dict[str, list[JsonObject]])
141
+ gate: list[GateLine] = field(default_factory=list[GateLine])
142
+
143
+
144
+ AGENT: Role = "assistant"
145
+
146
+
147
+ CALLER: Role = "user"
148
+
149
+
150
+ # Tool calls and what retrieval showed go on the agent's turn alone, or evidence counts them twice.
151
+ def case_of(entries: Sequence[Entry], declared: AgentConfig | None) -> Case:
152
+ """The case of a finished call, read from its log."""
153
+ read = [(entry, data) for entry in entries if (data := _read(entry)) is not None]
154
+ filed = _Filed(tools={} if declared is None else dict(declared.tools_by_name))
155
+ for entry, data in read:
156
+ _file(filed, entry, data)
157
+ knowledge = () if declared is None or not declared.knowledge else (declared.knowledge,)
158
+ recalled = [
159
+ fact.text
160
+ for _, data in read
161
+ if isinstance(data, MemoryOps)
162
+ for op in data.ops
163
+ for fact in op.facts
164
+ ]
165
+ summary = next((data for _, data in reversed(read) if isinstance(data, CallSummary)), None)
166
+ started = next((data for _, data in read if isinstance(data, CallStarted)), None)
167
+ return Case(
168
+ call=next((entry.call for entry in entries if entry.call is not None), ""),
169
+ agent=entries[0].agent if entries else "",
170
+ turns=tuple(_turn(entry, data, filed) for entry, data in filed.said),
171
+ gate=tuple(filed.gate),
172
+ arrived=tuple(
173
+ Arrived(entry.seq, data.name, dict(data.data), data.source)
174
+ for entry, data in read
175
+ if isinstance(data, EventReceived)
176
+ ),
177
+ states=tuple(dict(data.state) for _, data in read if isinstance(data, StateChanged)),
178
+ evidence=(*knowledge, *recalled),
179
+ summary=None if summary is None else summary.written(),
180
+ persona_rule=None if started is None else _rule_on(started),
181
+ simulated=started is not None
182
+ and (started.persona is not None or started.from_ == A_SIMULATED_CALLER),
183
+ declared=declared,
184
+ direction=None if started is None else started.direction,
185
+ )
186
+
187
+
188
+ def gate_line(entry: Entry, data: ConfirmRequest | ConfirmGranted | ConfirmDeclined) -> GateLine:
189
+ """A confirmation as a line of the gate trace; the side effect is left to the declaration."""
190
+ kind: GateKind = (
191
+ "confirm.request"
192
+ if isinstance(data, ConfirmRequest)
193
+ else "confirm.granted"
194
+ if isinstance(data, ConfirmGranted)
195
+ else "confirm.declined"
196
+ )
197
+ return GateLine(entry.seq, kind, data.call_id, data.tool, data.audience)
198
+
199
+
200
+ # livekit's judges take a ChatContext alone.
201
+ def as_chat(case: Case) -> ChatContext:
202
+ """The turns as livekit's judges read them, every tool call and its answer included."""
203
+ return ChatContext(items=[item for turn in case.turns for item in _items_of(turn)])
204
+
205
+
206
+ def said_by(case: Case, role: Role) -> tuple[str, ...]:
207
+ """What one side said, turn by turn."""
208
+ return tuple(turn.text for turn in case.turns if turn.role == role)
209
+
210
+
211
+ def calls_of(case: Case) -> tuple[Called, ...]:
212
+ """Every tool call the agent made, in order."""
213
+ return tuple(called for turn in case.turns for called in turn.calls)
214
+
215
+
216
+ def words_of(text: str) -> set[str]:
217
+ """The text's words, case-folded, without the punctuation around them."""
218
+ return {word.strip(PUNCTUATION).casefold() for word in text.split()}
219
+
220
+
221
+ def verdict_word(verdict: Verdict) -> ScoreVerdict:
222
+ """A verdict of livekit's in the word the log writes it with."""
223
+ return VERDICT_WORDS[verdict]
224
+
225
+
226
+ # An entry of an older shape is left out of the case, not the whole case refused.
227
+ def _read(entry: Entry) -> object:
228
+ try:
229
+ return event_of(entry)
230
+ except DeclarationRefused:
231
+ logger.warning(
232
+ "call %s: seq %d is not a %s the case reads", entry.call, entry.seq, entry.type
233
+ )
234
+ return None
235
+
236
+
237
+ def _file(filed: _Filed, entry: Entry, data: object) -> None:
238
+ match data:
239
+ case UserTurnEnded() | AgentTurnEnded():
240
+ filed.said.append((entry, data))
241
+ case ToolCall():
242
+ filed.called.setdefault(data.speech_id or "", []).append(data)
243
+ spec = filed.tools.get(data.name)
244
+ effect = None if spec is None else spec.side_effect
245
+ filed.gate.append(
246
+ GateLine(entry.seq, "tool.call", data.call_id, data.name, None, effect)
247
+ )
248
+ case ToolResult():
249
+ filed.results[data.call_id] = data
250
+ case ConfirmRequest() | ConfirmGranted() | ConfirmDeclined():
251
+ filed.gate.append(gate_line(entry, data))
252
+ case DocsSources():
253
+ filed.sources.setdefault(data.speech_id or "", []).extend(_excerpts(data))
254
+ case _ if entry.type.startswith(METRICS):
255
+ filed.blocks.setdefault(str(entry.data.get("speech_id") or ""), []).append(entry.data)
256
+ case _:
257
+ return
258
+
259
+
260
+ def _turn(entry: Entry, data: UserTurnEnded | AgentTurnEnded, filed: _Filed) -> Said:
261
+ speech = data.speech_id
262
+ if isinstance(data, UserTurnEnded):
263
+ return Said(
264
+ role=CALLER,
265
+ text=data.text,
266
+ seq=entry.seq,
267
+ speech_id=speech,
268
+ metrics=data.metrics.written(),
269
+ blocks=tuple(filed.blocks.get(speech, ())),
270
+ )
271
+ return Said(
272
+ role=AGENT,
273
+ text=data.text,
274
+ seq=entry.seq,
275
+ speech_id=speech,
276
+ interrupted=data.interrupted,
277
+ metrics=data.metrics.written(),
278
+ blocks=tuple(filed.blocks.get(speech, ())),
279
+ retrieved=tuple(filed.sources.get(speech, ())),
280
+ calls=tuple(_called(tool, filed) for tool in filed.called.get(speech, ())),
281
+ )
282
+
283
+
284
+ def _called(tool: ToolCall, filed: _Filed) -> Called:
285
+ answered = filed.results.get(tool.call_id)
286
+ spec = filed.tools.get(tool.name)
287
+ return Called(
288
+ call_id=tool.call_id,
289
+ name=tool.name,
290
+ arguments=dict(tool.arguments),
291
+ answer=None if answered is None else result_text(answered),
292
+ failed=answered is not None and answered.error is not None,
293
+ description=None if spec is None else spec.description,
294
+ )
295
+
296
+
297
+ # Read off call.started, so a persona edited after the call does not change its judgment.
298
+ def _rule_on(started: CallStarted) -> tuple[str, str] | None:
299
+ accepts, declines = started.accepts_when or "", started.declines_when or ""
300
+ return (accepts, declines) if accepts or declines else None
301
+
302
+
303
+ def _excerpts(data: DocsSources) -> list[str]:
304
+ return [
305
+ "\n".join(part for part in (source.heading, source.excerpt) if part)
306
+ for source in data.sources
307
+ if source.excerpt or source.heading
308
+ ]
309
+
310
+
311
+ def _items_of(turn: Said) -> list[ChatItem]:
312
+ items: list[ChatItem] = [
313
+ ChatMessage(role=turn.role, content=[turn.text], interrupted=turn.interrupted)
314
+ ]
315
+ for called in turn.calls:
316
+ arguments = json.dumps(called.arguments, ensure_ascii=False)
317
+ items.append(FunctionCall(call_id=called.call_id, name=called.name, arguments=arguments))
318
+ if called.answer is not None:
319
+ items.append(
320
+ FunctionCallOutput(
321
+ call_id=called.call_id,
322
+ name=called.name,
323
+ output=called.answer,
324
+ is_error=called.failed,
325
+ )
326
+ )
327
+ return items