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
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
|