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,266 @@
|
|
|
1
|
+
"""The box's providers configuration: one row in Postgres the operator edits from the console."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from psycopg.types.json import Jsonb
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from pinecall.domain.errors import Conflict, DeclarationRefused, NotAvailable
|
|
10
|
+
from pinecall.domain.names import Json, JsonObject
|
|
11
|
+
from pinecall.postgres.pool import Pool
|
|
12
|
+
from pinecall.process import box_settings
|
|
13
|
+
from pinecall.providers.build import MODALITIES, Modality, TurnModel, doing
|
|
14
|
+
|
|
15
|
+
ROW = "providers"
|
|
16
|
+
|
|
17
|
+
SEED = """
|
|
18
|
+
INSERT INTO box_settings (name, value) VALUES (%(name)s, %(value)s)
|
|
19
|
+
ON CONFLICT (name) DO NOTHING
|
|
20
|
+
RETURNING name
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# The width of every halfvec column that holds a vector.
|
|
24
|
+
VECTOR_WIDTH = 1024
|
|
25
|
+
|
|
26
|
+
UNSET = (
|
|
27
|
+
"this box has no providers configuration yet: the operator writes it from the console, or "
|
|
28
|
+
"`pinecall-runtime providers seed` writes the one it starts from"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
NO_JUDGE_FALLBACK = "the judge runs on one model: fallbacks are for the stages of a call"
|
|
33
|
+
|
|
34
|
+
NESTED = "{modality} fallback {vendor}: a fallback names no fallbacks of its own"
|
|
35
|
+
|
|
36
|
+
ASKED_OF_A_MODEL = "{key}: `request` is what a model is asked beside the conversation, so only llm"
|
|
37
|
+
|
|
38
|
+
TURN_ENDED_OTHERWISE = (
|
|
39
|
+
"stt fallback {vendor}: it {how} and {primary} does the other; the session reads the end "
|
|
40
|
+
"of the turn one way for the whole call, so every vendor of the stage must end it alike"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Stage(BaseModel):
|
|
45
|
+
"""A vendor and, when not the plugin's own, its model; a default names who takes over."""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(frozen=True)
|
|
48
|
+
|
|
49
|
+
vendor: str
|
|
50
|
+
model: str | None = None
|
|
51
|
+
# A default stage's vendors in the order they take over when the one before fails.
|
|
52
|
+
fallbacks: tuple["Stage", ...] = ()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Rate(BaseModel):
|
|
56
|
+
"""What a model costs in US dollars: per million tokens, per character, second or minute."""
|
|
57
|
+
|
|
58
|
+
model_config = ConfigDict(frozen=True)
|
|
59
|
+
|
|
60
|
+
input: float | None = None
|
|
61
|
+
output: float | None = None
|
|
62
|
+
# Omitted, a cache read costs what fresh input does.
|
|
63
|
+
cached_input: float | None = None
|
|
64
|
+
cache_creation: float | None = None
|
|
65
|
+
characters: float | None = None
|
|
66
|
+
audio_seconds: float | None = None
|
|
67
|
+
# A phone leg's minute, each one begun billed whole.
|
|
68
|
+
minutes: float | None = None
|
|
69
|
+
as_of: str = ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class StageOptions(BaseModel):
|
|
73
|
+
"""What one vendor is told for one stage: which class it builds and the kwargs it is given."""
|
|
74
|
+
|
|
75
|
+
model_config = ConfigDict(frozen=True)
|
|
76
|
+
|
|
77
|
+
# The plugin's class where it is not the one named LLM, STT or TTS (`STTv2`).
|
|
78
|
+
builds: str | None = None
|
|
79
|
+
options: JsonObject = Field(default_factory=dict[str, Json])
|
|
80
|
+
# The ears end the turn themselves, so the session stacks no detector on top.
|
|
81
|
+
ends_the_turn: bool = False
|
|
82
|
+
# Where they do not, which local model reads the end of the turn off the audio.
|
|
83
|
+
turn_model: TurnModel = "v1-mini"
|
|
84
|
+
# What every request of an llm stage carries beside the conversation, in the vendor's own
|
|
85
|
+
# field names (`{"thinking": {"type": "disabled"}}`): livekit's extra_kwargs, each request.
|
|
86
|
+
request: JsonObject = Field(default_factory=dict[str, Json])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class KeptVoice(BaseModel):
|
|
90
|
+
"""A voice the row lists for a vendor, as a picker shows it: its id and what to choose by."""
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(frozen=True)
|
|
93
|
+
|
|
94
|
+
id: str
|
|
95
|
+
name: str
|
|
96
|
+
gender: str = ""
|
|
97
|
+
# Where the accent is from: `ES`, `MX`; a language code does not tell them apart.
|
|
98
|
+
country: str = ""
|
|
99
|
+
accent: str = ""
|
|
100
|
+
description: str = ""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Judge(BaseModel):
|
|
104
|
+
"""The model that judges a finished call, on the box's key, and what one call may spend."""
|
|
105
|
+
|
|
106
|
+
model_config = ConfigDict(frozen=True)
|
|
107
|
+
|
|
108
|
+
llm: Stage
|
|
109
|
+
ceiling_usd: float
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Caller(BaseModel):
|
|
113
|
+
"""What one simulated call's caller may spend, its lines and its voice, before it hangs up."""
|
|
114
|
+
|
|
115
|
+
model_config = ConfigDict(frozen=True)
|
|
116
|
+
|
|
117
|
+
ceiling_usd: float = Field(gt=0)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Embedding(BaseModel):
|
|
121
|
+
"""The one embedder this box runs, on its own key; every base and fact is in its space."""
|
|
122
|
+
|
|
123
|
+
model_config = ConfigDict(frozen=True)
|
|
124
|
+
|
|
125
|
+
# The box's `credentials/<vendor>` row holds its key.
|
|
126
|
+
vendor: str
|
|
127
|
+
url: str
|
|
128
|
+
model: str
|
|
129
|
+
# Two wire formats: `embeddings` takes texts, `contextual` takes documents of chunks.
|
|
130
|
+
shape: Literal["embeddings", "contextual"]
|
|
131
|
+
dimensions: int = VECTOR_WIDTH
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Providers(BaseModel):
|
|
135
|
+
"""The operator's choices: defaults, what each vendor is told, what a model costs, the judge."""
|
|
136
|
+
|
|
137
|
+
model_config = ConfigDict(frozen=True)
|
|
138
|
+
|
|
139
|
+
# The stage an agent runs where it names no vendor.
|
|
140
|
+
defaults: dict[Modality, Stage]
|
|
141
|
+
# `tts/elevenlabs`: the model a vendor named alone runs, where the plugin's own is not.
|
|
142
|
+
models: dict[str, str] = Field(default_factory=dict[str, str])
|
|
143
|
+
# `cartesia/es`: the voice an agent that names none speaks with.
|
|
144
|
+
voices: dict[str, str] = Field(default_factory=dict[str, str])
|
|
145
|
+
# `cartesia/es`: the voices a picker offers, where the vendor's plugin lists none of its own.
|
|
146
|
+
listed: dict[str, tuple[KeptVoice, ...]] = Field(
|
|
147
|
+
default_factory=dict[str, tuple[KeptVoice, ...]]
|
|
148
|
+
)
|
|
149
|
+
# `stt/deepgram`: what that vendor is told for that stage; `llm/anthropic/claude-haiku-5-5`
|
|
150
|
+
# for the models whose id starts so, the longest such key replacing the vendor's whole.
|
|
151
|
+
tuning: dict[str, StageOptions] = Field(default_factory=dict[str, StageOptions])
|
|
152
|
+
# Languages the ears listen for beside the call's own.
|
|
153
|
+
hints: tuple[str, ...] = ()
|
|
154
|
+
# A model's price, by the longest prefix of its id: dated snapshots price by family.
|
|
155
|
+
rates: dict[str, Rate] = Field(default_factory=dict[str, Rate])
|
|
156
|
+
judge: Judge | None = None
|
|
157
|
+
# Absent: a simulated caller spends what its turns ask, as before the row named one.
|
|
158
|
+
caller: Caller | None = None
|
|
159
|
+
# `es`: the line a voice reads in the picker.
|
|
160
|
+
lines: dict[str, str] = Field(default_factory=dict[str, str])
|
|
161
|
+
embedding: Embedding | None = None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def tuning_of(
|
|
165
|
+
configured: Providers, modality: Modality, vendor: str, model: str | None
|
|
166
|
+
) -> StageOptions | None:
|
|
167
|
+
"""What a vendor is told for a stage: the longest key naming the model's id, else its own."""
|
|
168
|
+
stage = f"{modality}/{vendor}"
|
|
169
|
+
models = (
|
|
170
|
+
key.removeprefix(f"{stage}/") for key in configured.tuning if key.startswith(f"{stage}/")
|
|
171
|
+
)
|
|
172
|
+
named = None if model is None else longest_prefix(models, model)
|
|
173
|
+
return configured.tuning.get(stage if named is None else f"{stage}/{named}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def longest_prefix(keys: Iterable[str], name: str) -> str | None:
|
|
177
|
+
"""The longest of the keys the name starts with, so a dated snapshot reads as its family."""
|
|
178
|
+
listed = [key for key in keys if name.startswith(key)]
|
|
179
|
+
return max(listed, key=len) if listed else None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def judge_ceiling(configured: Providers) -> float | None:
|
|
183
|
+
"""What one call may spend on the box's judge, or None where the row names no judge."""
|
|
184
|
+
return None if configured.judge is None else configured.judge.ceiling_usd
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def providers(pool: Pool) -> Providers:
|
|
188
|
+
"""The box's configuration; NotAvailable until it was written."""
|
|
189
|
+
async with pool.connection() as connection:
|
|
190
|
+
value = await box_settings.read(connection, ROW)
|
|
191
|
+
if value is None:
|
|
192
|
+
raise NotAvailable(UNSET)
|
|
193
|
+
return Providers.model_validate(value)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def configure(pool: Pool, edited: Providers) -> None:
|
|
197
|
+
"""Write the configuration whole, as the console's box screen sends it."""
|
|
198
|
+
checked(edited)
|
|
199
|
+
async with pool.connection() as connection:
|
|
200
|
+
await box_settings.write(connection, ROW, edited.model_dump(mode="json"))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def seed(pool: Pool, seeded: Providers) -> None:
|
|
204
|
+
"""Write the configuration a box starts from; Conflict when one is already there."""
|
|
205
|
+
checked(seeded)
|
|
206
|
+
async with pool.connection() as connection:
|
|
207
|
+
written = await connection.execute(
|
|
208
|
+
SEED, {"name": ROW, "value": Jsonb(seeded.model_dump(mode="json"))}
|
|
209
|
+
)
|
|
210
|
+
if await written.fetchone() is None:
|
|
211
|
+
raise Conflict("this box is configured already: the console edits what is there")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# A row that names a vendor nobody installed, or for a stage it does not do, is refused where it
|
|
215
|
+
# is written, not on the first call that reads it.
|
|
216
|
+
def checked(written: Providers) -> Providers:
|
|
217
|
+
"""The configuration: each vendor installed and doing its stage, the embedder at 1024 wide."""
|
|
218
|
+
for modality, stage in written.defaults.items():
|
|
219
|
+
doing(stage.vendor, modality)
|
|
220
|
+
for fallback in stage.fallbacks:
|
|
221
|
+
_a_fallback(written, modality, stage, fallback)
|
|
222
|
+
for key in written.models:
|
|
223
|
+
_a_stage_key(key)
|
|
224
|
+
for key, options in written.tuning.items():
|
|
225
|
+
if _a_stage_key(key) != "llm" and options.request:
|
|
226
|
+
raise DeclarationRefused(ASKED_OF_A_MODEL.format(key=key))
|
|
227
|
+
for key in (*written.voices, *written.listed):
|
|
228
|
+
doing(key.partition("/")[0], "tts")
|
|
229
|
+
if written.judge is not None:
|
|
230
|
+
doing(written.judge.llm.vendor, "llm")
|
|
231
|
+
if written.judge.llm.fallbacks:
|
|
232
|
+
raise DeclarationRefused(NO_JUDGE_FALLBACK)
|
|
233
|
+
if written.embedding is not None and written.embedding.dimensions != VECTOR_WIDTH:
|
|
234
|
+
raise DeclarationRefused(
|
|
235
|
+
f"the embedder answers {written.embedding.dimensions}-wide vectors; every vector "
|
|
236
|
+
f"column is halfvec({VECTOR_WIDTH}), so it must be asked for {VECTOR_WIDTH}"
|
|
237
|
+
)
|
|
238
|
+
return written
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# `stt/deepgram`, or `llm/anthropic/claude-haiku-5-5` where the key names a model of the vendor.
|
|
242
|
+
def _a_stage_key(key: str) -> Modality:
|
|
243
|
+
modality, _, vendor = key.partition("/")
|
|
244
|
+
if modality not in MODALITIES:
|
|
245
|
+
raise DeclarationRefused(f"{key!r}: a stage is one of {', '.join(MODALITIES)}")
|
|
246
|
+
doing(vendor.partition("/")[0], modality)
|
|
247
|
+
return modality
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# The ears are chosen once for the whole session: a fallback that ends the turn itself where the
|
|
251
|
+
# primary does not (or the reverse) would leave the call with nobody ending the caller's turn.
|
|
252
|
+
def _a_fallback(written: Providers, modality: Modality, primary: Stage, fallback: Stage) -> None:
|
|
253
|
+
doing(fallback.vendor, modality)
|
|
254
|
+
if fallback.fallbacks:
|
|
255
|
+
raise DeclarationRefused(NESTED.format(modality=modality, vendor=fallback.vendor))
|
|
256
|
+
if modality != "stt" or _ends_the_turn(written, fallback) == _ends_the_turn(written, primary):
|
|
257
|
+
return
|
|
258
|
+
how = "ends the turn itself" if _ends_the_turn(written, fallback) else "leaves the turn open"
|
|
259
|
+
raise DeclarationRefused(
|
|
260
|
+
TURN_ENDED_OTHERWISE.format(vendor=fallback.vendor, how=how, primary=primary.vendor)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _ends_the_turn(written: Providers, stage: Stage) -> bool:
|
|
265
|
+
options = tuning_of(written, "stt", stage.vendor, stage.model)
|
|
266
|
+
return options is not None and options.ends_the_turn
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Whose key each stage of a call runs on: the org's own runs anything, the box's what it lends."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from pinecall.domain.agent import AgentConfig, Model, Voice
|
|
11
|
+
from pinecall.domain.errors import DeclarationRefused, NotAllowed, NotAvailable
|
|
12
|
+
from pinecall.domain.names import Credentials
|
|
13
|
+
from pinecall.providers.build import Modality, Running, Vendor, installed, primary
|
|
14
|
+
from pinecall.providers.catalog import Providers, tuning_of
|
|
15
|
+
from pinecall.providers.declared import SEPARATOR
|
|
16
|
+
|
|
17
|
+
# yours: the org brought its key. offered: the box holds one and lends it to this org. bring your
|
|
18
|
+
# own: installed, and nobody but the org can key it. broken: its plugin does not import.
|
|
19
|
+
type Availability = Literal["yours", "offered", "bring your own", "broken"]
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# What a dated snapshot adds to a model's name: `-20251001`, `-2025-08-07`, `@20240229`.
|
|
25
|
+
A_SNAPSHOT = re.compile(r"[-@](\d{8}|\d{4}-\d{2}-\d{2})")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Keyring:
|
|
30
|
+
"""What a call may run on: the org's own credentials, the box's, and what the box lends it."""
|
|
31
|
+
|
|
32
|
+
own: Mapping[str, Credentials] = field(default_factory=dict[str, Credentials])
|
|
33
|
+
box: Mapping[str, Credentials] = field(default_factory=dict[str, Credentials])
|
|
34
|
+
# quotas.lends: None lends everything the box holds, empty lends nothing.
|
|
35
|
+
lends: frozenset[str] | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Pipeline:
|
|
40
|
+
"""The three stages of a voice call, each on the key it runs on."""
|
|
41
|
+
|
|
42
|
+
llm: Running
|
|
43
|
+
stt: Running
|
|
44
|
+
tts: Running
|
|
45
|
+
|
|
46
|
+
# The gateway keeps these with the call's facts: their usage is the operator's to bill.
|
|
47
|
+
@property
|
|
48
|
+
def lent(self) -> list[str]:
|
|
49
|
+
"""The vendors the call may run on the box's key, in stage order, once each."""
|
|
50
|
+
stages = (self.llm, self.stt, self.tts)
|
|
51
|
+
each = (item for stage in stages for item in (stage, *stage.fallbacks))
|
|
52
|
+
return list(dict.fromkeys(item.vendor for item in each if item.lent))
|
|
53
|
+
|
|
54
|
+
# The row's order stands but for the vendors `failing` names (over their error line now),
|
|
55
|
+
# which go last, among themselves in the row's order too.
|
|
56
|
+
def demoting(self, failing: frozenset[str]) -> "Pipeline":
|
|
57
|
+
"""The same stages, each with the vendors failing now behind the ones that are not."""
|
|
58
|
+
return Pipeline(
|
|
59
|
+
llm=_behind(self.llm, failing),
|
|
60
|
+
stt=_behind(self.stt, failing),
|
|
61
|
+
tts=_behind(self.tts, failing),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Readiness:
|
|
67
|
+
"""One vendor as `GET /v1/providers` shows it to an org: what it does, and whose key runs it."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
does: tuple[Modality, ...]
|
|
71
|
+
availability: Availability
|
|
72
|
+
broken: str | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def readiness(installed: Mapping[str, Vendor], keys: Keyring) -> list[Readiness]:
|
|
76
|
+
"""Every installed vendor, sorted, and how this org may run it."""
|
|
77
|
+
return [
|
|
78
|
+
Readiness(
|
|
79
|
+
name=vendor.name,
|
|
80
|
+
does=tuple(sorted(vendor.does)),
|
|
81
|
+
availability=_availability_of(vendor, keys),
|
|
82
|
+
broken=vendor.broken,
|
|
83
|
+
)
|
|
84
|
+
for vendor in sorted(installed.values(), key=lambda one: one.name)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def pipeline(config: AgentConfig, configured: Providers, keys: Keyring) -> Pipeline:
|
|
89
|
+
"""The stages an agent runs: its vendors or the defaults, each with its key and options."""
|
|
90
|
+
language = primary(config.language)
|
|
91
|
+
llm = thinking(config, configured, keys)
|
|
92
|
+
stt = stage("stt", config.stt, configured, keys)
|
|
93
|
+
tts = stage("tts", config.voice, configured, keys)
|
|
94
|
+
heard = dict.fromkeys(item for item in (language, *configured.hints) if item)
|
|
95
|
+
voice = config.voice.voice_id if config.voice else None
|
|
96
|
+
ears = (
|
|
97
|
+
dataclasses.replace(item, language=language, hints=tuple(heard))
|
|
98
|
+
for item in (stt, *stt.fallbacks)
|
|
99
|
+
)
|
|
100
|
+
# The agent's voice is its vendor's; a vendor that takes over speaks the row's for the language.
|
|
101
|
+
voices = (
|
|
102
|
+
dataclasses.replace(
|
|
103
|
+
item,
|
|
104
|
+
language=language,
|
|
105
|
+
voice=(voice if item is tts else None)
|
|
106
|
+
or configured.voices.get(f"{item.vendor}{SEPARATOR}{language}"),
|
|
107
|
+
)
|
|
108
|
+
for item in (tts, *tts.fallbacks)
|
|
109
|
+
)
|
|
110
|
+
return Pipeline(llm=llm, stt=_over(*ears), tts=_over(*voices))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# A written call runs this stage alone: a call with no voice is not refused for want of one.
|
|
114
|
+
def thinking(config: AgentConfig, configured: Providers, keys: Keyring) -> Running:
|
|
115
|
+
"""The model an agent thinks with, on its key, at the temperature it declared."""
|
|
116
|
+
llm = stage("llm", config.llm, configured, keys)
|
|
117
|
+
if config.llm is None or config.llm.temperature is None:
|
|
118
|
+
return llm
|
|
119
|
+
return dataclasses.replace(llm, options={**llm.options, "temperature": config.llm.temperature})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def running(keys: Keyring, vendor: str, model: str | None) -> Running:
|
|
123
|
+
"""The vendor on the org's own key, else on a key the box lends; refused before the call."""
|
|
124
|
+
own = keys.own.get(vendor)
|
|
125
|
+
if own is not None:
|
|
126
|
+
return Running(vendor=vendor, credentials=own, model=model)
|
|
127
|
+
box = keys.box.get(vendor)
|
|
128
|
+
if box is None:
|
|
129
|
+
raise NotAvailable(f"{vendor} has no key: the org brought none and the box holds none")
|
|
130
|
+
if not lent(keys.lends, vendor, model):
|
|
131
|
+
raise NotAllowed(refusal(keys.lends or frozenset(), vendor, model))
|
|
132
|
+
return Running(vendor=vendor, credentials=box, model=model, lent=True)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# A model entry lends itself and its dated snapshots (`anthropic/claude-haiku-5-5` lends
|
|
136
|
+
# `claude-haiku-5-5-20261001`), never a sibling that only starts the same (`openai/gpt-5` lends no
|
|
137
|
+
# `gpt-5-pro`); a plugin's own default needs the whole vendor.
|
|
138
|
+
def lent(lends: frozenset[str] | None, vendor: str, model: str | None) -> bool:
|
|
139
|
+
"""Whether the box lends this org its key for this vendor and model."""
|
|
140
|
+
if lends is None or vendor in lends:
|
|
141
|
+
return True
|
|
142
|
+
if model is None:
|
|
143
|
+
return False
|
|
144
|
+
prefix = f"{vendor}{SEPARATOR}"
|
|
145
|
+
named = (entry.removeprefix(prefix) for entry in lends if entry.startswith(prefix))
|
|
146
|
+
return any(model == entry or _a_snapshot_of(entry, model) for entry in named)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def refusal(lends: frozenset[str], vendor: str, model: str | None) -> str:
|
|
150
|
+
"""The sentence a refused org reads: what it asked, what it may run, and the other way."""
|
|
151
|
+
params = vendor if model is None else f"{vendor}{SEPARATOR}{model}"
|
|
152
|
+
may = ", ".join(sorted(lends)) if lends else "nothing of the box's"
|
|
153
|
+
return f"{params} is not lent to this org: it may run on {may}, or on a key of its own"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_lending(entries: Iterable[str]) -> frozenset[str]:
|
|
157
|
+
"""The entries an operator lends, each a vendor installed here or `vendor/model`."""
|
|
158
|
+
parsed: set[str] = set()
|
|
159
|
+
for entry in entries:
|
|
160
|
+
named, slash, model = entry.strip().partition(SEPARATOR)
|
|
161
|
+
vendor = named.lower()
|
|
162
|
+
if vendor not in installed():
|
|
163
|
+
raise DeclarationRefused(f"{entry!r} lends no vendor installed here")
|
|
164
|
+
if slash and not model:
|
|
165
|
+
raise DeclarationRefused(f"{entry!r} names a vendor and no model after the slash")
|
|
166
|
+
parsed.add(f"{vendor}{SEPARATOR}{model}" if slash else vendor)
|
|
167
|
+
return frozenset(parsed)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# An agent that names its vendor runs it alone; one on the row's default runs the default's
|
|
171
|
+
# fallbacks behind it, each on its own key, in the row's order. A fallback this org cannot key is
|
|
172
|
+
# left out.
|
|
173
|
+
def stage(
|
|
174
|
+
modality: Modality, declared: Model | Voice | None, configured: Providers, keys: Keyring
|
|
175
|
+
) -> Running:
|
|
176
|
+
"""One stage on its key: the vendor declared or the default, with the operator's options."""
|
|
177
|
+
if declared is not None:
|
|
178
|
+
return _on_its_key(modality, declared.provider, declared.model or None, configured, keys)
|
|
179
|
+
default = configured.defaults[modality]
|
|
180
|
+
chosen = _on_its_key(modality, default.vendor, default.model, configured, keys)
|
|
181
|
+
backups: list[Running] = []
|
|
182
|
+
for fallback in default.fallbacks:
|
|
183
|
+
try:
|
|
184
|
+
backups.append(_on_its_key(modality, fallback.vendor, fallback.model, configured, keys))
|
|
185
|
+
except (NotAvailable, NotAllowed):
|
|
186
|
+
logger.info(
|
|
187
|
+
"%s fallback %s has no key this org may run it on", modality, fallback.vendor
|
|
188
|
+
)
|
|
189
|
+
return _over(chosen, *backups)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _a_snapshot_of(entry: str, model: str) -> bool:
|
|
193
|
+
return model.startswith(entry) and A_SNAPSHOT.fullmatch(model[len(entry) :]) is not None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _on_its_key(
|
|
197
|
+
modality: Modality, vendor: str, params: str | None, configured: Providers, keys: Keyring
|
|
198
|
+
) -> Running:
|
|
199
|
+
model = params or configured.models.get(f"{modality}{SEPARATOR}{vendor}")
|
|
200
|
+
chosen = running(keys, vendor, model)
|
|
201
|
+
options = tuning_of(configured, modality, vendor, model)
|
|
202
|
+
if options is None:
|
|
203
|
+
return chosen
|
|
204
|
+
return dataclasses.replace(
|
|
205
|
+
chosen,
|
|
206
|
+
builds=options.builds,
|
|
207
|
+
options=dict(options.options),
|
|
208
|
+
ends_the_turn=options.ends_the_turn,
|
|
209
|
+
turn_model=options.turn_model,
|
|
210
|
+
request=dict(options.request),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _over(primary: Running, *fallbacks: Running) -> Running:
|
|
215
|
+
return dataclasses.replace(primary, fallbacks=fallbacks)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _behind(chosen: Running, failing: frozenset[str]) -> Running:
|
|
219
|
+
chain = (dataclasses.replace(chosen, fallbacks=()), *chosen.fallbacks)
|
|
220
|
+
return _over(*sorted(chain, key=lambda item: item.vendor in failing))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _availability_of(vendor: Vendor, keys: Keyring) -> Availability:
|
|
224
|
+
if vendor.broken is not None:
|
|
225
|
+
return "broken"
|
|
226
|
+
if vendor.name in keys.own:
|
|
227
|
+
return "yours"
|
|
228
|
+
if vendor.name in keys.box and lent(keys.lends, vendor.name, None):
|
|
229
|
+
return "offered"
|
|
230
|
+
return "bring your own"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""What an agent runs as its org set it: the declaration, its tuning and its lexicon."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
from pinecall.domain.agent import AgentConfig, Lexicon, Model, Tuning, Voice
|
|
7
|
+
from pinecall.providers.build import Modality, doing, installed
|
|
8
|
+
from pinecall.providers.catalog import Stage
|
|
9
|
+
|
|
10
|
+
# Model ids carry slashes of their own (`openai/gpt-5` on LiveKit Inference): the first one ends
|
|
11
|
+
# the vendor.
|
|
12
|
+
SEPARATOR = "/"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_tuning(
|
|
16
|
+
declared: AgentConfig, tuning: Tuning, lexicon: Lexicon, *, defaults: Mapping[Modality, Stage]
|
|
17
|
+
) -> AgentConfig:
|
|
18
|
+
"""The declaration with the org's settings over it: what the world set is what runs."""
|
|
19
|
+
return dataclasses.replace(
|
|
20
|
+
declared,
|
|
21
|
+
greeting=tuning.greeting,
|
|
22
|
+
# An app on an SDK before 0.9.19 still declares one; the world's wins.
|
|
23
|
+
language=declared.language if tuning.language is None else tuning.language,
|
|
24
|
+
voice=_voice(declared, tuning, defaults),
|
|
25
|
+
stt=model_of(tuning.stt, "stt", in_use=_in_use(declared.stt, defaults["stt"])),
|
|
26
|
+
llm=model_of(tuning.llm, "llm", in_use=_in_use(declared.llm, defaults["llm"])),
|
|
27
|
+
hangup=tuning.hangup,
|
|
28
|
+
turn=tuning.turn,
|
|
29
|
+
memory=tuning.memory,
|
|
30
|
+
record=declared.record if tuning.record is None else tuning.record,
|
|
31
|
+
max_duration_s=(
|
|
32
|
+
declared.max_duration_s if tuning.max_duration_s is None else tuning.max_duration_s
|
|
33
|
+
),
|
|
34
|
+
llm_timeout_s=tuning.llm_timeout_s,
|
|
35
|
+
knowledge=tuning.knowledge,
|
|
36
|
+
bases=tuning.bases or (),
|
|
37
|
+
says=dict(lexicon.said),
|
|
38
|
+
hears=lexicon.heard,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def model_of(text: str | None, modality: Modality, *, in_use: str) -> Model | None:
|
|
43
|
+
"""`vendor/model`, a vendor alone, or a model on the vendor in use; None when none is named."""
|
|
44
|
+
if text is None:
|
|
45
|
+
return None
|
|
46
|
+
word = text.strip()
|
|
47
|
+
vendor, slash, model = word.partition(SEPARATOR)
|
|
48
|
+
if slash:
|
|
49
|
+
return Model(provider=doing(vendor.lower(), modality), model=model)
|
|
50
|
+
if word.lower() in installed():
|
|
51
|
+
return Model(provider=doing(word.lower(), modality), model="")
|
|
52
|
+
return Model(provider=doing(in_use, modality), model=word)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# `tts_model` names the model of the vendor `tts` names, or of the one in use.
|
|
56
|
+
def _voice(
|
|
57
|
+
declared: AgentConfig, tuning: Tuning, defaults: Mapping[Modality, Stage]
|
|
58
|
+
) -> Voice | None:
|
|
59
|
+
if tuning.tts is None and tuning.voice is None and tuning.tts_model is None:
|
|
60
|
+
return None
|
|
61
|
+
in_use = _in_use(declared.voice, defaults["tts"])
|
|
62
|
+
named = model_of(tuning.tts, "tts", in_use=in_use)
|
|
63
|
+
provider = in_use if named is None else named.provider
|
|
64
|
+
model = tuning.tts_model or (None if named is None else named.model or None)
|
|
65
|
+
return Voice(provider=provider, model=model, voice_id=tuning.voice)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _in_use(declared: Model | Voice | None, default: Stage) -> str:
|
|
69
|
+
return default.vendor if declared is None else declared.provider
|