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,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
|
+
)
|