pinecall-runtime 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pinecall/__init__.py +0 -0
- pinecall/channels/__init__.py +0 -0
- pinecall/channels/_chooser.py +45 -0
- pinecall/channels/offers.py +219 -0
- pinecall/channels/rooms.py +176 -0
- pinecall/channels/routes.py +275 -0
- pinecall/channels/telephony/__init__.py +0 -0
- pinecall/channels/telephony/_twilio.py +349 -0
- pinecall/channels/telephony/carrier.py +229 -0
- pinecall/channels/telephony/carrier_catalog.py +92 -0
- pinecall/channels/telephony/carriers.csv +41 -0
- pinecall/channels/telephony/dialing.py +419 -0
- pinecall/channels/telephony/firewall.py +58 -0
- pinecall/channels/telephony/hand_over.py +78 -0
- pinecall/channels/telephony/number_path.py +176 -0
- pinecall/channels/telephony/numbers.py +530 -0
- pinecall/channels/telephony/origination.py +77 -0
- pinecall/channels/telephony/sip.py +448 -0
- pinecall/channels/whatsapp.py +413 -0
- pinecall/cli/__init__.py +0 -0
- pinecall/cli/_archive.py +85 -0
- pinecall/cli/_drift.py +38 -0
- pinecall/cli/_facts.py +67 -0
- pinecall/cli/_load.py +433 -0
- pinecall/cli/_operator.py +626 -0
- pinecall/cli/_sessions.py +164 -0
- pinecall/cli/_traceback.py +95 -0
- pinecall/cli/_usage.py +34 -0
- pinecall/cli/main.py +521 -0
- pinecall/domain/__init__.py +0 -0
- pinecall/domain/agent.py +446 -0
- pinecall/domain/call.py +152 -0
- pinecall/domain/country_codes.py +32 -0
- pinecall/domain/errors.py +151 -0
- pinecall/domain/names.py +115 -0
- pinecall/domain/org.py +120 -0
- pinecall/domain/person.py +208 -0
- pinecall/domain/scope.py +21 -0
- pinecall/evals/__init__.py +0 -0
- pinecall/evals/_evidence.py +158 -0
- pinecall/evals/calibration.py +112 -0
- pinecall/evals/callers.py +160 -0
- pinecall/evals/case.py +327 -0
- pinecall/evals/checks.py +437 -0
- pinecall/evals/compliance.py +197 -0
- pinecall/evals/dataset.py +451 -0
- pinecall/evals/goldens.py +331 -0
- pinecall/evals/judges.py +525 -0
- pinecall/evals/runs.py +252 -0
- pinecall/evals/spoken.py +356 -0
- pinecall/fleet/__init__.py +0 -0
- pinecall/fleet/client.py +569 -0
- pinecall/fleet/demand.py +51 -0
- pinecall/fleet/heartbeat.py +194 -0
- pinecall/fleet/measures.py +157 -0
- pinecall/fleet/roster.py +232 -0
- pinecall/fleet/worlds.py +44 -0
- pinecall/gateway/__init__.py +0 -0
- pinecall/gateway/_call_setup.py +53 -0
- pinecall/gateway/_deps.py +556 -0
- pinecall/gateway/_gateway.py +75 -0
- pinecall/gateway/_served.py +396 -0
- pinecall/gateway/_sockets.py +444 -0
- pinecall/gateway/_streams.py +83 -0
- pinecall/gateway/_text_calls.py +182 -0
- pinecall/gateway/api/__init__.py +0 -0
- pinecall/gateway/api/accounts.py +324 -0
- pinecall/gateway/api/agents.py +150 -0
- pinecall/gateway/api/apps.py +478 -0
- pinecall/gateway/api/box.py +658 -0
- pinecall/gateway/api/calibration.py +58 -0
- pinecall/gateway/api/callbacks.py +105 -0
- pinecall/gateway/api/calls.py +655 -0
- pinecall/gateway/api/chat.py +223 -0
- pinecall/gateway/api/dataset.py +163 -0
- pinecall/gateway/api/desk.py +113 -0
- pinecall/gateway/api/entries.py +77 -0
- pinecall/gateway/api/evals.py +647 -0
- pinecall/gateway/api/fleet.py +73 -0
- pinecall/gateway/api/hosting.py +241 -0
- pinecall/gateway/api/judges.py +97 -0
- pinecall/gateway/api/keys.py +110 -0
- pinecall/gateway/api/line.py +142 -0
- pinecall/gateway/api/members.py +197 -0
- pinecall/gateway/api/metrics.py +226 -0
- pinecall/gateway/api/numbers.py +476 -0
- pinecall/gateway/api/ops.py +428 -0
- pinecall/gateway/api/org.py +283 -0
- pinecall/gateway/api/personas.py +157 -0
- pinecall/gateway/api/pipeline.py +206 -0
- pinecall/gateway/api/prompts.py +50 -0
- pinecall/gateway/api/providers.py +240 -0
- pinecall/gateway/api/recordings.py +89 -0
- pinecall/gateway/api/relay.py +169 -0
- pinecall/gateway/api/retrieval.py +403 -0
- pinecall/gateway/api/runner.py +109 -0
- pinecall/gateway/api/sessions.py +102 -0
- pinecall/gateway/api/settings.py +463 -0
- pinecall/gateway/api/signup.py +164 -0
- pinecall/gateway/api/sso_login.py +168 -0
- pinecall/gateway/api/threads.py +187 -0
- pinecall/gateway/api/usage.py +303 -0
- pinecall/gateway/api/visitors.py +222 -0
- pinecall/gateway/api/whatsapp.py +48 -0
- pinecall/gateway/api/widget.py +49 -0
- pinecall/gateway/app.py +504 -0
- pinecall/gateway/calls/__init__.py +0 -0
- pinecall/gateway/calls/binding.py +63 -0
- pinecall/gateway/calls/commands.py +18 -0
- pinecall/gateway/calls/inbox.py +100 -0
- pinecall/gateway/calls/known.py +23 -0
- pinecall/gateway/calls/owners.py +87 -0
- pinecall/gateway/calls/pump.py +33 -0
- pinecall/gateway/calls/serving.py +118 -0
- pinecall/gateway/calls/threads.py +367 -0
- pinecall/gateway/dispatching/__init__.py +0 -0
- pinecall/gateway/dispatching/arrivals.py +58 -0
- pinecall/gateway/dispatching/sweep.py +43 -0
- pinecall/gateway/ending/__init__.py +0 -0
- pinecall/gateway/ending/reaper.py +160 -0
- pinecall/gateway/ending/seal.py +312 -0
- pinecall/gateway/ending/stranded.py +117 -0
- pinecall/log/__init__.py +0 -0
- pinecall/log/_histogram.py +74 -0
- pinecall/log/_relay.py +259 -0
- pinecall/log/_writer.py +454 -0
- pinecall/log/days.py +154 -0
- pinecall/log/drift.py +550 -0
- pinecall/log/facts.py +358 -0
- pinecall/log/inbox.py +169 -0
- pinecall/log/lists.py +155 -0
- pinecall/log/logs.py +581 -0
- pinecall/log/openings.py +71 -0
- pinecall/log/private.py +135 -0
- pinecall/log/queries.py +229 -0
- pinecall/log/readers.py +207 -0
- pinecall/log/reduce.py +659 -0
- pinecall/log/refold.py +217 -0
- pinecall/log/store.py +370 -0
- pinecall/log/usage.py +242 -0
- pinecall/postgres/__init__.py +0 -0
- pinecall/postgres/migrate.py +202 -0
- pinecall/postgres/migrations/0001_schema.sql +591 -0
- pinecall/postgres/migrations/0002_worlds.sql +15 -0
- pinecall/postgres/migrations/0003_lent.sql +4 -0
- pinecall/postgres/migrations/0004_carriers.sql +23 -0
- pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
- pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
- pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
- pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
- pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
- pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
- pinecall/postgres/migrations/0011_erasure.sql +32 -0
- pinecall/postgres/migrations/0012_org_policy.sql +10 -0
- pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
- pinecall/postgres/migrations/0014_consents.sql +24 -0
- pinecall/postgres/migrations/0015_disclosure.sql +7 -0
- pinecall/postgres/migrations/0016_call_records.sql +21 -0
- pinecall/postgres/migrations/0017_reads.sql +17 -0
- pinecall/postgres/migrations/0018_hosting.sql +41 -0
- pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
- pinecall/postgres/migrations/0020_written_once.sql +7 -0
- pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
- pinecall/postgres/migrations/0022_sealed.sql +39 -0
- pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
- pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
- pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
- pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
- pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
- pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
- pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
- pinecall/postgres/migrations/0060_drift.sql +62 -0
- pinecall/postgres/migrations/0061_canaries.sql +24 -0
- pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
- pinecall/postgres/migrations/0063_prompts.sql +16 -0
- pinecall/postgres/migrations/0075_call_private.sql +14 -0
- pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
- pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
- pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
- pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
- pinecall/postgres/migrations/0080_call_openings.sql +20 -0
- pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
- pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
- pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
- pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
- pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
- pinecall/postgres/migrations/0086_route_origin.sql +17 -0
- pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
- pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
- pinecall/postgres/migrations/0089_offers.sql +21 -0
- pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
- pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
- pinecall/postgres/migrations/0092_key_parent.sql +6 -0
- pinecall/postgres/migrations/0093_code_tries.sql +10 -0
- pinecall/postgres/migrations/0094_route_approval.sql +12 -0
- pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
- pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
- pinecall/postgres/migrations/0097_case_status.sql +19 -0
- pinecall/postgres/pool.py +190 -0
- pinecall/process/__init__.py +0 -0
- pinecall/process/_objects.py +97 -0
- pinecall/process/box_settings.py +23 -0
- pinecall/process/connections.py +124 -0
- pinecall/process/metrics.py +127 -0
- pinecall/process/recordings.py +323 -0
- pinecall/process/resolver.py +41 -0
- pinecall/process/sealed_audio.py +173 -0
- pinecall/process/settings.py +534 -0
- pinecall/process/shared.py +158 -0
- pinecall/process/signal.py +416 -0
- pinecall/providers/__init__.py +0 -0
- pinecall/providers/build.py +570 -0
- pinecall/providers/catalog.py +266 -0
- pinecall/providers/credentials.py +230 -0
- pinecall/providers/declared.py +69 -0
- pinecall/providers/prices.py +240 -0
- pinecall/providers/tried.py +104 -0
- pinecall/providers/voices.py +174 -0
- pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
- pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
- pinecall/public/console/favicon.png +0 -0
- pinecall/public/console/index.html +20 -0
- pinecall/public/console/logo-mark.png +0 -0
- pinecall/public/console/pinecall-logo-dark.png +0 -0
- pinecall/public/console/pinecall-logo.png +0 -0
- pinecall/public/console/pinecall-mark.png +0 -0
- pinecall/public/console/sw.js +37 -0
- pinecall/public/widget/pinecall-widget.js +853 -0
- pinecall/py.typed +0 -0
- pinecall/retrieval/__init__.py +0 -0
- pinecall/retrieval/_search.py +186 -0
- pinecall/retrieval/_sections.py +70 -0
- pinecall/retrieval/embed.py +340 -0
- pinecall/retrieval/extraction.py +403 -0
- pinecall/retrieval/knowledge.py +659 -0
- pinecall/retrieval/lookups.py +192 -0
- pinecall/retrieval/memory.py +603 -0
- pinecall/runner/__init__.py +0 -0
- pinecall/runner/_kube.py +459 -0
- pinecall/runner/_plan.py +157 -0
- pinecall/runner/main.py +306 -0
- pinecall/session/__init__.py +0 -0
- pinecall/session/_agent.py +141 -0
- pinecall/session/_hearing.py +101 -0
- pinecall/session/_livekit.py +195 -0
- pinecall/session/_prompt.py +147 -0
- pinecall/session/_recording.py +237 -0
- pinecall/session/call.py +367 -0
- pinecall/session/clock.py +40 -0
- pinecall/session/hold.py +239 -0
- pinecall/session/room.py +626 -0
- pinecall/session/session.py +690 -0
- pinecall/session/text.py +144 -0
- pinecall/session/tools.py +483 -0
- pinecall/session/voice.py +118 -0
- pinecall/session/widget.py +201 -0
- pinecall/tenancy/__init__.py +0 -0
- pinecall/tenancy/admission.py +204 -0
- pinecall/tenancy/agents.py +164 -0
- pinecall/tenancy/canary.py +126 -0
- pinecall/tenancy/carrier_networks.py +156 -0
- pinecall/tenancy/carriers.py +265 -0
- pinecall/tenancy/codes.py +196 -0
- pinecall/tenancy/consents.py +167 -0
- pinecall/tenancy/dial_policy.py +405 -0
- pinecall/tenancy/disclosure.py +41 -0
- pinecall/tenancy/erasure.py +204 -0
- pinecall/tenancy/export.py +167 -0
- pinecall/tenancy/hosted_running.py +166 -0
- pinecall/tenancy/hosting.py +475 -0
- pinecall/tenancy/judges.py +126 -0
- pinecall/tenancy/keys.py +467 -0
- pinecall/tenancy/knocks.py +40 -0
- pinecall/tenancy/letters.py +378 -0
- pinecall/tenancy/mail.py +458 -0
- pinecall/tenancy/org_secrets.py +123 -0
- pinecall/tenancy/orgs.py +78 -0
- pinecall/tenancy/people.py +543 -0
- pinecall/tenancy/personas.py +145 -0
- pinecall/tenancy/policy.py +65 -0
- pinecall/tenancy/prompts.py +48 -0
- pinecall/tenancy/reads.py +73 -0
- pinecall/tenancy/recording_keys.py +41 -0
- pinecall/tenancy/remembered.py +148 -0
- pinecall/tenancy/retention.py +86 -0
- pinecall/tenancy/scopes.py +406 -0
- pinecall/tenancy/signin.py +622 -0
- pinecall/tenancy/spend.py +69 -0
- pinecall/tenancy/sso.py +431 -0
- pinecall/tenancy/sso_domains.py +132 -0
- pinecall/tenancy/throttle.py +88 -0
- pinecall/tenancy/tokens.py +400 -0
- pinecall/tenancy/traceback.py +70 -0
- pinecall/tenancy/usage.py +136 -0
- pinecall/tenancy/vault.py +204 -0
- pinecall/tenancy/words.py +144 -0
- pinecall/wire/__init__.py +0 -0
- pinecall/wire/commands.py +350 -0
- pinecall/wire/events.py +655 -0
- pinecall/wire/frames.py +65 -0
- pinecall/wire/metrics.py +287 -0
- pinecall/wire/parts.py +320 -0
- pinecall/wire/rest/__init__.py +0 -0
- pinecall/wire/rest/accounts.py +512 -0
- pinecall/wire/rest/agents.py +172 -0
- pinecall/wire/rest/calls.py +397 -0
- pinecall/wire/rest/evals.py +453 -0
- pinecall/wire/rest/fleet.py +69 -0
- pinecall/wire/rest/hosting.py +174 -0
- pinecall/wire/rest/numbers.py +280 -0
- pinecall/wire/rest/ops.py +342 -0
- pinecall/wire/rest/providers.py +83 -0
- pinecall/wire/rest/retrieval.py +301 -0
- pinecall/wire/rest/settings.py +184 -0
- pinecall/wire/rest/usage.py +245 -0
- pinecall/wire/room.py +61 -0
- pinecall/wire/scores.py +49 -0
- pinecall/wire/state.py +243 -0
- pinecall/worker/__init__.py +0 -0
- pinecall/worker/_job.py +655 -0
- pinecall/worker/_recorder.py +82 -0
- pinecall/worker/_traces.py +48 -0
- pinecall/worker/main.py +307 -0
- pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
- pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
- pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
- pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
- pinecall_runtime-0.1.6.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,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
|