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,570 @@
|
|
|
1
|
+
"""The vendors livekit ships once installed, and one of them built for a stage of a call."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import dataclasses
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
import pkgutil
|
|
10
|
+
import re
|
|
11
|
+
import types
|
|
12
|
+
import typing
|
|
13
|
+
from collections.abc import Callable, Mapping
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from functools import cache
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
from typing import Any, Literal, Never, override
|
|
18
|
+
|
|
19
|
+
from livekit.agents import llm, stt, tts
|
|
20
|
+
from livekit.agents.language import LanguageCode
|
|
21
|
+
from livekit.agents.llm import Tool, ToolChoice
|
|
22
|
+
from livekit.agents.types import (
|
|
23
|
+
DEFAULT_API_CONNECT_OPTIONS,
|
|
24
|
+
NOT_GIVEN,
|
|
25
|
+
APIConnectOptions,
|
|
26
|
+
NotGivenOr,
|
|
27
|
+
)
|
|
28
|
+
from livekit.agents.utils import is_given
|
|
29
|
+
from typing_extensions import TypeIs
|
|
30
|
+
|
|
31
|
+
from pinecall.domain.agent import Tuning, Turn
|
|
32
|
+
from pinecall.domain.errors import DeclarationRefused, NotAvailable
|
|
33
|
+
from pinecall.domain.names import Credentials, Json, JsonObject
|
|
34
|
+
from pinecall.wire.metrics import LLMModelUsage
|
|
35
|
+
|
|
36
|
+
type Modality = Literal["llm", "stt", "tts"]
|
|
37
|
+
|
|
38
|
+
# A call's stage as it is built: the plugin's object, or livekit's adapter over several.
|
|
39
|
+
type Thinking = llm.LLM[Never] | llm.FallbackAdapter
|
|
40
|
+
|
|
41
|
+
type Ears = stt.STT[Never] | stt.FallbackAdapter
|
|
42
|
+
|
|
43
|
+
type Speaking = tts.TTS[Never] | tts.FallbackAdapter
|
|
44
|
+
|
|
45
|
+
# The two local end-of-turn models, both read off the audio on the worker's CPU: livekit's own
|
|
46
|
+
# (its detector's small version) and Daily's Smart Turn v3 (smart-turn-livekit).
|
|
47
|
+
type TurnModel = Literal["v1-mini", "smart-turn-v3"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# LiveKit Inference: the same three classes, on the box's LiveKit key pair, models `vendor/model`.
|
|
54
|
+
INFERENCE = "livekit"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_INFERENCE_MODULE = "livekit.agents.inference"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_PLUGINS = "livekit.plugins"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# livekit's three bases are generic in the extra events a subclass may emit; a plugin adds none.
|
|
64
|
+
_AN_LLM: type[llm.LLM[Never]] = llm.LLM
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_AN_STT: type[stt.STT[Never]] = stt.STT
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_A_TTS: type[tts.TTS[Never]] = tts.TTS
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Plugins spell the same argument differently; the first one a constructor declares gets it.
|
|
74
|
+
# `voice_id` comes before `voice`: a plugin that declares both reads the id.
|
|
75
|
+
_THE_KEY = ("api_key", "speech_key", "secret")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_THE_VOICE = ("voice_id", "voice_uuid", "voice", "speaker", "voice_name")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_THE_LANGUAGE = ("language", "language_code")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_THE_HINTS = ("language_hint", "language_hints")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_NO_SUCH_CLASS = "{vendor} exports no {stage} named {name!r}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# What an org's credentials object may carry: the fields the vendor's constructors take, none of
|
|
91
|
+
# them an address, a file or a session. Where a vendor is reached is the operator's, in the
|
|
92
|
+
# providers row's tuning; a tenant's object that named one would aim the box's requests anywhere.
|
|
93
|
+
NOT_A_CREDENTIAL = (
|
|
94
|
+
"{vendor} credentials carry {field!r}: an address, a file or a session is the operator's to "
|
|
95
|
+
"set in the providers row, not a credential"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
NOT_TAKEN = "{vendor} takes no credential named {field!r}; its plugin takes {fields}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
NOT_A_VALUE = (
|
|
103
|
+
"{vendor} credentials: {field!r} is a string, a number or a boolean of at most 1024 characters"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
TOO_MANY_FIELDS = "{vendor} credentials carry {count} fields: eight at most"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_NOT_A_CREDENTIAL = frozenset(
|
|
111
|
+
{"url", "host", "endpoint", "session", "client", "transport", "connection", "model"}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
_NOT_A_CREDENTIAL_ENDS = ("_url", "_base", "_endpoint", "_host", "_file", "_path", "_session")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
_MOST_FIELDS = 8
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_LONGEST_VALUE = 1024
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
_UNTAKEN = (
|
|
125
|
+
"{vendor}'s {stage} takes no {knobs}, so a call would run without it: leave it out, or pick a "
|
|
126
|
+
"vendor that takes it"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# livekit's adapter wraps ears that do not stream with a VAD the session does not hold, and refuses
|
|
131
|
+
# voices of another channel count: such a fallback is left out and the call runs on the rest.
|
|
132
|
+
LEFT_OUT = "%s is left out of the stage's fallbacks: %s"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class Vendor:
|
|
137
|
+
"""A vendor this process can build: what it does, or why its plugin does not import."""
|
|
138
|
+
|
|
139
|
+
name: str
|
|
140
|
+
does: frozenset[Modality]
|
|
141
|
+
broken: str | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class Running:
|
|
146
|
+
"""One stage of a call as it runs: vendor, model, key, and what the plugin is told."""
|
|
147
|
+
|
|
148
|
+
vendor: str
|
|
149
|
+
credentials: Credentials
|
|
150
|
+
# None: the plugin's own default.
|
|
151
|
+
model: str | None = None
|
|
152
|
+
# The plugin's class, where the operator names one other than LLM, STT or TTS.
|
|
153
|
+
builds: str | None = None
|
|
154
|
+
# The operator's keyword arguments for this vendor and stage, as the plugin names them.
|
|
155
|
+
options: JsonObject = field(default_factory=dict[str, Json])
|
|
156
|
+
voice: str | None = None
|
|
157
|
+
language: str | None = None
|
|
158
|
+
# Languages the ears listen for, the call's first.
|
|
159
|
+
hints: tuple[str, ...] = ()
|
|
160
|
+
# The ears end the turn themselves, so the session stacks no detector on top.
|
|
161
|
+
ends_the_turn: bool = False
|
|
162
|
+
# Which local model reads the end of the turn off the audio, where the ears do not.
|
|
163
|
+
turn_model: TurnModel = "v1-mini"
|
|
164
|
+
# What every request of an llm stage carries beside the conversation, in the vendor's names.
|
|
165
|
+
request: JsonObject = field(default_factory=dict[str, Json])
|
|
166
|
+
# On the box's key rather than the org's own.
|
|
167
|
+
lent: bool = False
|
|
168
|
+
# Who takes over, in order, each on its own key: the stage is livekit's FallbackAdapter.
|
|
169
|
+
fallbacks: tuple["Running", ...] = ()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# A plugin's constructor takes no field for every request (a model's thinking, its effort), and
|
|
173
|
+
# livekit hands each request extra_kwargs: the row's ride every one, a caller's own over them.
|
|
174
|
+
class LLMWithRequest(llm.LLM[Never]):
|
|
175
|
+
"""A plugin's LLM whose every request also carries the operator's fields for its model."""
|
|
176
|
+
|
|
177
|
+
def __init__(self, built: llm.LLM[Never], request: JsonObject) -> None:
|
|
178
|
+
"""Wrap the built LLM, and pass on what it reports as its own."""
|
|
179
|
+
super().__init__()
|
|
180
|
+
self.built = built
|
|
181
|
+
self.request = request
|
|
182
|
+
self._label = built.label
|
|
183
|
+
built.on("metrics_collected", self._reported_metrics) # pyright: ignore[reportUnknownMemberType]
|
|
184
|
+
built.on("error", self._reported_error) # pyright: ignore[reportUnknownMemberType]
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
@override
|
|
188
|
+
def model(self) -> str:
|
|
189
|
+
"""The built LLM's model, so usage and metrics name it."""
|
|
190
|
+
return self.built.model
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
@override
|
|
194
|
+
def provider(self) -> str:
|
|
195
|
+
"""The built LLM's vendor, so usage and metrics name it."""
|
|
196
|
+
return self.built.provider
|
|
197
|
+
|
|
198
|
+
@override
|
|
199
|
+
def chat(
|
|
200
|
+
self,
|
|
201
|
+
*,
|
|
202
|
+
chat_ctx: llm.ChatContext,
|
|
203
|
+
tools: list[Tool] | None = None,
|
|
204
|
+
conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
|
|
205
|
+
parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
|
|
206
|
+
tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
|
|
207
|
+
extra_kwargs: NotGivenOr[dict[str, Any]] = NOT_GIVEN,
|
|
208
|
+
) -> llm.LLMStream:
|
|
209
|
+
"""The built LLM's request, with the row's fields and the caller's over them."""
|
|
210
|
+
given = extra_kwargs if is_given(extra_kwargs) else {}
|
|
211
|
+
return self.built.chat(
|
|
212
|
+
chat_ctx=chat_ctx,
|
|
213
|
+
tools=tools,
|
|
214
|
+
conn_options=conn_options,
|
|
215
|
+
parallel_tool_calls=parallel_tool_calls,
|
|
216
|
+
tool_choice=tool_choice,
|
|
217
|
+
extra_kwargs={**self.request, **given},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@override
|
|
221
|
+
def prewarm(self, *, loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
222
|
+
"""Open the built LLM's connection before the first request."""
|
|
223
|
+
self.built.prewarm(loop=loop)
|
|
224
|
+
|
|
225
|
+
@override
|
|
226
|
+
async def aclose(self) -> None:
|
|
227
|
+
"""Stop listening to the built LLM, and close it."""
|
|
228
|
+
self.built.off("metrics_collected", self._reported_metrics) # pyright: ignore[reportUnknownMemberType]
|
|
229
|
+
self.built.off("error", self._reported_error) # pyright: ignore[reportUnknownMemberType]
|
|
230
|
+
await self.built.aclose()
|
|
231
|
+
|
|
232
|
+
def _reported_metrics(self, *args: object) -> None:
|
|
233
|
+
self.emit("metrics_collected", *args)
|
|
234
|
+
|
|
235
|
+
def _reported_error(self, *args: object) -> None:
|
|
236
|
+
self.emit("error", *args)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
MODALITIES: tuple[Modality, ...] = ("llm", "stt", "tts")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
CLASS_OF: dict[Modality, str] = {"llm": "LLM", "stt": "STT", "tts": "TTS"}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# The org's own knobs and the names a plugin may take each under; one it takes under none is
|
|
246
|
+
# refused where the org sets it, not dropped on the next call. `stt_of` gives the endpointing to
|
|
247
|
+
# ears that call it `eot_timeout_ms`.
|
|
248
|
+
_KNOB_NAMES: dict[str, tuple[str, ...]] = {
|
|
249
|
+
"endpointing_ms": ("endpointing_ms", "eot_timeout_ms"),
|
|
250
|
+
"eot_threshold": ("eot_threshold",),
|
|
251
|
+
"eager_eot_threshold": ("eager_eot_threshold",),
|
|
252
|
+
"voice": _THE_VOICE,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# A component that failed is named in its error's label: `label='livekit.plugins.deepgram.stt.STT'`.
|
|
257
|
+
_LABELLED = re.compile(
|
|
258
|
+
rf"label='(?:{re.escape(_PLUGINS)}\.(?P<plugin>\w+)|(?P<inference>{re.escape(_INFERENCE_MODULE)}))\."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@cache
|
|
263
|
+
def installed() -> dict[str, Vendor]:
|
|
264
|
+
"""Every vendor of this process: each livekit plugin exporting an LLM, an STT or a TTS."""
|
|
265
|
+
found = {INFERENCE: _vendor(INFERENCE, _INFERENCE_MODULE)}
|
|
266
|
+
spec = importlib.util.find_spec(_PLUGINS)
|
|
267
|
+
locations = spec.submodule_search_locations if spec is not None else None
|
|
268
|
+
for module in pkgutil.iter_modules(locations or []):
|
|
269
|
+
vendor = _vendor(module.name, f"{_PLUGINS}.{module.name}")
|
|
270
|
+
if vendor.does or vendor.broken:
|
|
271
|
+
found[vendor.name] = vendor
|
|
272
|
+
return found
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def doing(vendor: str, modality: Modality) -> str:
|
|
276
|
+
"""The vendor's name, once it is installed and does that stage; DeclarationRefused if not."""
|
|
277
|
+
found = installed().get(vendor)
|
|
278
|
+
if found is None:
|
|
279
|
+
raise DeclarationRefused(
|
|
280
|
+
f"no vendor named {vendor!r} is installed here; GET /v1/providers lists every one"
|
|
281
|
+
)
|
|
282
|
+
if found.broken is None and modality not in found.does:
|
|
283
|
+
raise DeclarationRefused(
|
|
284
|
+
f"{vendor} has no {modality}: it does {', '.join(sorted(found.does))}"
|
|
285
|
+
)
|
|
286
|
+
return vendor
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# A plugin that did not import is tried again: what broke it (a package missing) may have been
|
|
290
|
+
# fixed since the process started.
|
|
291
|
+
def plugin(vendor: str) -> ModuleType:
|
|
292
|
+
"""The installed module of a vendor; refused when it is not installed or does not import."""
|
|
293
|
+
found = installed().get(vendor)
|
|
294
|
+
if found is None:
|
|
295
|
+
raise DeclarationRefused(
|
|
296
|
+
f"no vendor named {vendor!r} is installed; "
|
|
297
|
+
f'`pip install "livekit-agents[{vendor}]"` adds its plugin'
|
|
298
|
+
)
|
|
299
|
+
module = _INFERENCE_MODULE if vendor == INFERENCE else f"{_PLUGINS}.{vendor}"
|
|
300
|
+
if found.broken is not None:
|
|
301
|
+
found = _vendor(vendor, module)
|
|
302
|
+
installed()[vendor] = found
|
|
303
|
+
if found.broken is not None:
|
|
304
|
+
raise NotAvailable(f"the {vendor} plugin is installed and does not import: {found.broken}")
|
|
305
|
+
return importlib.import_module(module)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# What a plugin or a row hands back is typed as nothing; these say what it is, once.
|
|
309
|
+
def a_mapping(value: object) -> TypeIs[Mapping[str, object]]:
|
|
310
|
+
"""Whether the value is a mapping, read with its keys as names."""
|
|
311
|
+
return isinstance(value, Mapping)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def a_list(value: object) -> TypeIs[list[object]]:
|
|
315
|
+
"""Whether the value is a list."""
|
|
316
|
+
return isinstance(value, list)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def primary(language: str | None) -> str | None:
|
|
320
|
+
"""The base code of a language (`es` for `es-ES` or `spanish`); vendors refuse full tags."""
|
|
321
|
+
if language is None or not language.strip():
|
|
322
|
+
return None
|
|
323
|
+
return LanguageCode(language.strip()).language
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def llm_of(running: Running) -> llm.LLM[Never]:
|
|
327
|
+
"""The LLM a stage runs: the plugin's class called with the key, the model and the options."""
|
|
328
|
+
built = _built("llm", _AN_LLM, running, {})
|
|
329
|
+
return LLMWithRequest(built, running.request) if running.request else built
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# A model the runtime asks itself, outside a call's session: livekit meters none of it.
|
|
333
|
+
def completion_usage(
|
|
334
|
+
model: llm.LLM[Never], used: llm.CompletionUsage | None
|
|
335
|
+
) -> LLMModelUsage | None:
|
|
336
|
+
"""What one answer of a model cost in tokens, as the call's usage writes it."""
|
|
337
|
+
if used is None:
|
|
338
|
+
return None
|
|
339
|
+
# Frames drop unset fields, so the discriminator is set here or the SDKs reject the entry.
|
|
340
|
+
return LLMModelUsage(
|
|
341
|
+
type="llm_usage",
|
|
342
|
+
provider=model.provider,
|
|
343
|
+
model=model.model,
|
|
344
|
+
input_tokens=used.prompt_tokens,
|
|
345
|
+
input_cached_tokens=used.prompt_cached_tokens,
|
|
346
|
+
input_cache_creation_tokens=used.cache_creation_tokens,
|
|
347
|
+
output_tokens=used.completion_tokens,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def tts_of(running: Running) -> tts.TTS[Never]:
|
|
352
|
+
"""The voice a stage speaks with, its voice and language under the names its plugin uses."""
|
|
353
|
+
return _built("tts", _A_TTS, running, {})
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def stt_of(running: Running, turn: Turn | None) -> stt.STT[Never]:
|
|
357
|
+
"""The ears a stage hears with; the agent's turn knobs reach them where they take them."""
|
|
358
|
+
return _built("stt", _AN_STT, running, _turn_knobs(turn))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# A call's three stages: with no fallback, exactly the plugin's object; with some, livekit's
|
|
362
|
+
# FallbackAdapter over the stage and the vendors that take over, in order.
|
|
363
|
+
def thinking_of(running: Running) -> Thinking:
|
|
364
|
+
"""The LLM a call thinks with, over its fallbacks when the stage names any."""
|
|
365
|
+
primary = llm_of(running)
|
|
366
|
+
if not running.fallbacks:
|
|
367
|
+
return primary
|
|
368
|
+
return llm.FallbackAdapter([primary, *(llm_of(fallback) for fallback in running.fallbacks)])
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def ears_of(running: Running, turn: Turn | None) -> Ears:
|
|
372
|
+
"""The ears a call hears with, over its fallbacks when the stage names any that stream."""
|
|
373
|
+
primary = stt_of(running, turn)
|
|
374
|
+
if not primary.capabilities.streaming:
|
|
375
|
+
return primary
|
|
376
|
+
backups: list[stt.STT[Never]] = []
|
|
377
|
+
for fallback in running.fallbacks:
|
|
378
|
+
ears = stt_of(fallback, turn)
|
|
379
|
+
if ears.capabilities.streaming:
|
|
380
|
+
backups.append(ears)
|
|
381
|
+
else:
|
|
382
|
+
logger.warning(LEFT_OUT, ears.label, "it does not stream")
|
|
383
|
+
return stt.FallbackAdapter([primary, *backups]) if backups else primary
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# The adapter resamples to the highest rate among them, but mixes no channels.
|
|
387
|
+
def speaking_of(running: Running) -> Speaking:
|
|
388
|
+
"""The voice a call speaks with, over its fallbacks when the stage names any that fit."""
|
|
389
|
+
primary = tts_of(running)
|
|
390
|
+
backups: list[tts.TTS[Never]] = []
|
|
391
|
+
for fallback in running.fallbacks:
|
|
392
|
+
voice = tts_of(fallback)
|
|
393
|
+
if voice.num_channels == primary.num_channels:
|
|
394
|
+
backups.append(voice)
|
|
395
|
+
else:
|
|
396
|
+
logger.warning(LEFT_OUT, voice.label, "its channels differ")
|
|
397
|
+
return tts.FallbackAdapter([primary, *backups]) if backups else primary
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# Only what the org set: the key, the language and the hints are the runtime's, and a plugin
|
|
401
|
+
# that takes none of them is the operator's to configure in the providers row.
|
|
402
|
+
def refuse_untaken(ears: Running, voice: Running, tuning: Tuning) -> None:
|
|
403
|
+
"""Refuse the org's turn and voice knobs that its ears or its voice take under no name."""
|
|
404
|
+
turn = tuning.turn or Turn()
|
|
405
|
+
heard = {
|
|
406
|
+
"endpointing_ms": turn.endpointing_ms,
|
|
407
|
+
"eot_threshold": turn.eot_threshold,
|
|
408
|
+
"eager_eot_threshold": turn.eager_eot_threshold,
|
|
409
|
+
}
|
|
410
|
+
_refuse_untaken("stt", ears, [knob for knob, value in heard.items() if value is not None])
|
|
411
|
+
_refuse_untaken("tts", voice, ["voice"] if tuning.voice is not None else [])
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# Every field is one a constructor of the vendor takes, named for a secret and not for where it
|
|
415
|
+
# is sent, and a short scalar: nothing a plugin would open, dial or read a file by.
|
|
416
|
+
def check_credentials(vendor: str, credentials: Credentials) -> None:
|
|
417
|
+
"""Refuse a credentials object that names an address, a file, a session or a field untaken."""
|
|
418
|
+
if not isinstance(credentials, dict):
|
|
419
|
+
return
|
|
420
|
+
if len(credentials) > _MOST_FIELDS:
|
|
421
|
+
raise DeclarationRefused(TOO_MANY_FIELDS.format(vendor=vendor, count=len(credentials)))
|
|
422
|
+
taken = _every_parameter(vendor)
|
|
423
|
+
for name, value in credentials.items():
|
|
424
|
+
lowered = name.lower()
|
|
425
|
+
if lowered in _NOT_A_CREDENTIAL or lowered.endswith(_NOT_A_CREDENTIAL_ENDS):
|
|
426
|
+
raise DeclarationRefused(NOT_A_CREDENTIAL.format(vendor=vendor, field=name))
|
|
427
|
+
if name not in taken:
|
|
428
|
+
fields = ", ".join(sorted(taken)) or "nothing by name"
|
|
429
|
+
raise DeclarationRefused(NOT_TAKEN.format(vendor=vendor, field=name, fields=fields))
|
|
430
|
+
if not isinstance(value, str | int | float | bool) or (
|
|
431
|
+
isinstance(value, str) and (len(value) > _LONGEST_VALUE or "://" in value)
|
|
432
|
+
):
|
|
433
|
+
raise DeclarationRefused(NOT_A_VALUE.format(vendor=vendor, field=name))
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def vendor_named_in(text: str) -> str:
|
|
437
|
+
"""The vendor whose plugin a component's error names, "" when it names none."""
|
|
438
|
+
found = _LABELLED.search(text)
|
|
439
|
+
if found is None:
|
|
440
|
+
return ""
|
|
441
|
+
return INFERENCE if found["inference"] else found["plugin"]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _turn_knobs(turn: Turn | None) -> dict[str, object]:
|
|
445
|
+
knobs: dict[str, object] = {} if turn is None else dataclasses.asdict(turn)
|
|
446
|
+
given = {knob: value for knob, value in knobs.items() if value is not None}
|
|
447
|
+
# The agent's endpointing is the silence that closes a turn: the ears that take it call it so.
|
|
448
|
+
if turn is not None and turn.endpointing_ms is not None:
|
|
449
|
+
given["eot_timeout_ms"] = turn.endpointing_ms
|
|
450
|
+
return given
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _vendor(name: str, module: str) -> Vendor:
|
|
454
|
+
try:
|
|
455
|
+
imported = importlib.import_module(module)
|
|
456
|
+
except ImportError as broken:
|
|
457
|
+
logger.warning("the %s plugin does not import, so it builds nothing", name, exc_info=True)
|
|
458
|
+
return Vendor(name, frozenset(), broken=str(broken))
|
|
459
|
+
return Vendor(name, frozenset(m for m in MODALITIES if hasattr(imported, CLASS_OF[m])))
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _refuse_untaken(modality: Modality, running: Running, knobs: list[str]) -> None:
|
|
463
|
+
if not knobs:
|
|
464
|
+
return
|
|
465
|
+
accepts = _parameters(_class_of(modality, running))
|
|
466
|
+
untaken = [knob for knob in knobs if not any(name in accepts for name in _KNOB_NAMES[knob])]
|
|
467
|
+
if untaken:
|
|
468
|
+
raise DeclarationRefused(
|
|
469
|
+
_UNTAKEN.format(vendor=running.vendor, stage=modality, knobs=", ".join(untaken))
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _class_of(modality: Modality, running: Running) -> type:
|
|
474
|
+
name = running.builds or CLASS_OF[modality]
|
|
475
|
+
made: object = getattr(plugin(running.vendor), name, None)
|
|
476
|
+
if not isinstance(made, type):
|
|
477
|
+
raise DeclarationRefused(
|
|
478
|
+
_NO_SUCH_CLASS.format(vendor=running.vendor, stage=CLASS_OF[modality], name=name)
|
|
479
|
+
)
|
|
480
|
+
return made
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _built[T](
|
|
484
|
+
modality: Modality, base: type[T], running: Running, knobs: Mapping[str, object]
|
|
485
|
+
) -> T:
|
|
486
|
+
named = running.builds or CLASS_OF[modality]
|
|
487
|
+
made = _class_of(modality, running)
|
|
488
|
+
if not issubclass(made, base):
|
|
489
|
+
raise DeclarationRefused(
|
|
490
|
+
_NO_SUCH_CLASS.format(vendor=running.vendor, stage=CLASS_OF[modality], name=named)
|
|
491
|
+
)
|
|
492
|
+
constructor: Callable[..., T] = made
|
|
493
|
+
accepts = _parameters(made)
|
|
494
|
+
given: dict[str, object] = {**running.options}
|
|
495
|
+
given |= {knob: value for knob, value in knobs.items() if knob in accepts}
|
|
496
|
+
given |= _credentials(running.credentials, accepts)
|
|
497
|
+
given |= _first(accepts, _THE_VOICE, running.voice)
|
|
498
|
+
# A row that names the language wins: a vendor may want the full tag the call cuts to its base.
|
|
499
|
+
if not any(name in running.options for name in _THE_LANGUAGE):
|
|
500
|
+
given |= _first(accepts, _THE_LANGUAGE, primary(running.language))
|
|
501
|
+
given |= _first(accepts, _THE_HINTS, list(running.hints) or None)
|
|
502
|
+
if running.model is not None:
|
|
503
|
+
given["model"] = running.model
|
|
504
|
+
# A plugin raises what it likes when it refuses (SpitchError, a missing variable): each is
|
|
505
|
+
# the vendor refusing its stage, said in its own words.
|
|
506
|
+
try:
|
|
507
|
+
return constructor(**_shaped(made, given))
|
|
508
|
+
except Exception as refused:
|
|
509
|
+
raise DeclarationRefused(f"{running.vendor} refused its {named}: {refused}") from refused
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _every_parameter(vendor: str) -> frozenset[str]:
|
|
513
|
+
module = plugin(vendor)
|
|
514
|
+
classes = (getattr(module, name, None) for name in CLASS_OF.values())
|
|
515
|
+
taken = frozenset[str]()
|
|
516
|
+
for made in classes:
|
|
517
|
+
if isinstance(made, type):
|
|
518
|
+
taken |= _parameters(made)
|
|
519
|
+
return taken - {"self", "kwargs", "args"}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _parameters(made: type) -> frozenset[str]:
|
|
523
|
+
try:
|
|
524
|
+
return frozenset(inspect.signature(made).parameters)
|
|
525
|
+
except (TypeError, ValueError):
|
|
526
|
+
return frozenset()
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _credentials(credentials: Credentials, accepts: frozenset[str]) -> dict[str, object]:
|
|
530
|
+
if isinstance(credentials, dict):
|
|
531
|
+
return dict(credentials)
|
|
532
|
+
return _first(accepts, _THE_KEY, credentials)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _first(accepts: frozenset[str], names: tuple[str, ...], value: object) -> dict[str, object]:
|
|
536
|
+
chosen = next((name for name in names if name in accepts), None)
|
|
537
|
+
return {} if chosen is None or value is None else {chosen: value}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# A plugin that takes its options as one object (`params=STTOptions(...)`) is given that object,
|
|
541
|
+
# built from the row's mapping field by field, however deep.
|
|
542
|
+
def _shaped(made: type, given: Mapping[str, object]) -> dict[str, object]:
|
|
543
|
+
try:
|
|
544
|
+
declared = typing.get_type_hints(made.__init__)
|
|
545
|
+
except (NameError, TypeError):
|
|
546
|
+
return dict(given)
|
|
547
|
+
return {name: _as_declared(declared.get(name), value) for name, value in given.items()}
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _as_declared(declared: object, value: object) -> object:
|
|
551
|
+
shape = _dataclass_in(declared)
|
|
552
|
+
if shape is None or not a_mapping(value):
|
|
553
|
+
return value
|
|
554
|
+
typed = typing.get_type_hints(shape)
|
|
555
|
+
return shape(**{name: _as_declared(typed.get(name), given) for name, given in value.items()})
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _dataclass_in(declared: object) -> type | None:
|
|
559
|
+
if isinstance(declared, type) and dataclasses.is_dataclass(declared):
|
|
560
|
+
return declared
|
|
561
|
+
if isinstance(declared, types.UnionType):
|
|
562
|
+
return next(
|
|
563
|
+
(
|
|
564
|
+
declared_one
|
|
565
|
+
for declared_one in typing.get_args(declared)
|
|
566
|
+
if _dataclass_in(declared_one)
|
|
567
|
+
),
|
|
568
|
+
None,
|
|
569
|
+
)
|
|
570
|
+
return None
|