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,396 @@
|
|
|
1
|
+
"""The calls this gateway serves: handed to an app socket, parked, told their commands, counted."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
|
+
|
|
8
|
+
from pydantic import TypeAdapter
|
|
9
|
+
|
|
10
|
+
from pinecall.domain.agent import AgentConfig
|
|
11
|
+
from pinecall.domain.call import CallContext, Opener
|
|
12
|
+
from pinecall.domain.errors import NotAvailable
|
|
13
|
+
from pinecall.domain.names import Env
|
|
14
|
+
from pinecall.domain.scope import Scope
|
|
15
|
+
from pinecall.gateway._sockets import Process, SocketId
|
|
16
|
+
from pinecall.gateway.calls.commands import COMMANDS_CHANNEL, SUPERVISOR_VERB
|
|
17
|
+
from pinecall.gateway.calls.inbox import (
|
|
18
|
+
ANSWER_CHANNEL,
|
|
19
|
+
Bound,
|
|
20
|
+
Elsewhere,
|
|
21
|
+
ForApp,
|
|
22
|
+
SocketRow,
|
|
23
|
+
answered_back,
|
|
24
|
+
told_app,
|
|
25
|
+
told_bound,
|
|
26
|
+
)
|
|
27
|
+
from pinecall.gateway.calls.owners import Owners
|
|
28
|
+
from pinecall.gateway.calls.pump import Send, pumped
|
|
29
|
+
from pinecall.log.logs import Log, Logs
|
|
30
|
+
from pinecall.process.connections import Connections
|
|
31
|
+
from pinecall.process.metrics import Counters
|
|
32
|
+
from pinecall.process.shared import Shared
|
|
33
|
+
from pinecall.process.signal import LocalSignal, Signal
|
|
34
|
+
from pinecall.retrieval.embed import Embedder
|
|
35
|
+
from pinecall.session.call import with_app_fields
|
|
36
|
+
from pinecall.session.session import Session
|
|
37
|
+
from pinecall.session.tools import ToolCalls
|
|
38
|
+
from pinecall.tenancy.prompts import Prompts
|
|
39
|
+
from pinecall.wire.commands import DevAnswer, SessionConfigure, SupervisorVerb, command_of
|
|
40
|
+
from pinecall.wire.frames import Command, Entry, WireModel
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Entries a taking-over socket is rebuilt from; the prompt is not, the log keeps its hash alone.
|
|
46
|
+
STARTED = "call.started"
|
|
47
|
+
|
|
48
|
+
# Where each gateway says how many calls each org runs on it, and how often.
|
|
49
|
+
RUNNING_CHANNEL = "running"
|
|
50
|
+
RUNNING_EVERY_S = 1.0
|
|
51
|
+
_RUNNING: TypeAdapter[dict[str, int]] = TypeAdapter(dict[str, int])
|
|
52
|
+
|
|
53
|
+
# How long a call another gateway opened stays served here with no door asking for it.
|
|
54
|
+
FIRST_SEEN_IDLE_S = 600.0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
CLAIMED = "call.claimed"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# A call belongs to its agent, not to a socket: `app` is None while it waits for one.
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Served:
|
|
63
|
+
"""A call this gateway serves: the socket it reaches, its log, its commands, its tools."""
|
|
64
|
+
|
|
65
|
+
agent: str
|
|
66
|
+
scope: Scope
|
|
67
|
+
app: SocketId | None
|
|
68
|
+
log: Log
|
|
69
|
+
# A None ends the worker's command stream.
|
|
70
|
+
commands: asyncio.Queue[Command | None]
|
|
71
|
+
context: CallContext
|
|
72
|
+
config: AgentConfig
|
|
73
|
+
tools: ToolCalls
|
|
74
|
+
# A written call runs here; a voice call runs in a worker.
|
|
75
|
+
session: Session | None = None
|
|
76
|
+
# Held while the call is sealed, so its seal runs once however many times it is asked for.
|
|
77
|
+
sealing: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
78
|
+
# False for a call another gateway opened, served here from what was kept when it opened:
|
|
79
|
+
# it counts on its opener's gateway, and no socket here takes it as parked.
|
|
80
|
+
opened_here: bool = True
|
|
81
|
+
# Who opened it: the worker doors of the fleet's call take the fleet's key alone, a written
|
|
82
|
+
# call's take nobody's. None for a call opened before the head said.
|
|
83
|
+
opened_by: Opener | None = None
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def call(self) -> str:
|
|
87
|
+
"""The call's id."""
|
|
88
|
+
return self.context.call
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Not durable on purpose: it says which sockets are open now; the log is the record.
|
|
92
|
+
class ServedCalls:
|
|
93
|
+
"""The open app sockets and the calls served to them."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, signal: Signal | None = None) -> None:
|
|
96
|
+
"""Nothing open; bindings to other gateways' sockets told on the signal."""
|
|
97
|
+
self.signal = signal or LocalSignal()
|
|
98
|
+
# Which written calls and threads run here, and on the other gateways; their sockets.
|
|
99
|
+
self.owners = Owners(self.signal)
|
|
100
|
+
self.elsewhere = Elsewhere(self.signal)
|
|
101
|
+
# Each org's calls at once in each world, as every gateway counts its own each second.
|
|
102
|
+
self.counted = Shared(self.signal, RUNNING_CHANNEL, _RUNNING, {}, lambda: None)
|
|
103
|
+
self.counted.every_s = RUNNING_EVERY_S
|
|
104
|
+
self.counted.gathered = self._running_here
|
|
105
|
+
# Each dev.request asked here, listening for its answer from the socket's gateway.
|
|
106
|
+
self.answers: dict[str, asyncio.Task[None]] = {}
|
|
107
|
+
self.sockets: dict[SocketId, Send] = {}
|
|
108
|
+
self.processes: dict[SocketId, Process] = {}
|
|
109
|
+
self.calls: dict[str, Served] = {}
|
|
110
|
+
# The calls first seen here, by when a door last asked for one: let go once idle.
|
|
111
|
+
self.seen: dict[str, float] = {}
|
|
112
|
+
self.pending_answers: dict[str, asyncio.Future[DevAnswer]] = {}
|
|
113
|
+
# Each bound call's pump to its socket, and each call's listener for its commands, by call:
|
|
114
|
+
# held here, since asyncio keeps weak references to tasks, and cancelled when it ends.
|
|
115
|
+
self.pumps: dict[str, asyncio.Task[None]] = {}
|
|
116
|
+
self.listening: dict[str, tuple[asyncio.Event, asyncio.Task[None]]] = {}
|
|
117
|
+
|
|
118
|
+
async def listen(self) -> None:
|
|
119
|
+
"""Hear the other gateways' owners and sockets, and say this one's."""
|
|
120
|
+
await self.owners.start()
|
|
121
|
+
await self.elsewhere.start()
|
|
122
|
+
await self.counted.start()
|
|
123
|
+
|
|
124
|
+
async def quiet(self) -> None:
|
|
125
|
+
"""Stop hearing and saying."""
|
|
126
|
+
await self.owners.close()
|
|
127
|
+
await self.elsewhere.close()
|
|
128
|
+
await self.counted.close()
|
|
129
|
+
|
|
130
|
+
def connect(self, process: Process, send: Send) -> None:
|
|
131
|
+
"""An app socket opened."""
|
|
132
|
+
self.sockets[process.app] = send
|
|
133
|
+
self.processes[process.app] = process
|
|
134
|
+
self._say_sockets()
|
|
135
|
+
|
|
136
|
+
def disconnect(self, app: SocketId) -> None:
|
|
137
|
+
"""An app socket closed; its calls go on until another socket takes them."""
|
|
138
|
+
self.sockets.pop(app, None)
|
|
139
|
+
self.processes.pop(app, None)
|
|
140
|
+
self._say_sockets()
|
|
141
|
+
|
|
142
|
+
def named(self, app: SocketId, host: str | None) -> None:
|
|
143
|
+
"""The machine the app said it runs on."""
|
|
144
|
+
process = self.processes.get(app)
|
|
145
|
+
if process is not None and host is not None:
|
|
146
|
+
self.processes[app] = replace(process, host=host)
|
|
147
|
+
self._say_sockets()
|
|
148
|
+
|
|
149
|
+
# The other gateways' sockets are stopped through their inbox, as any message to them is.
|
|
150
|
+
def processes_of(self, org: str, env: Env) -> list[Process]:
|
|
151
|
+
"""The org's sockets in the world on every gateway, oldest first."""
|
|
152
|
+
mine = [
|
|
153
|
+
value
|
|
154
|
+
for value in self.processes.values()
|
|
155
|
+
if value.scope.org == org and value.scope.env == env
|
|
156
|
+
]
|
|
157
|
+
theirs = [
|
|
158
|
+
Process(
|
|
159
|
+
row.app, row.scope, row.address, row.connected_at, self._stop(row.app), row.host
|
|
160
|
+
)
|
|
161
|
+
for row in self.elsewhere.rows_of(org, env)
|
|
162
|
+
]
|
|
163
|
+
return sorted([*mine, *theirs], key=lambda one: one.connected_at)
|
|
164
|
+
|
|
165
|
+
# dev.request is never stored: it goes straight down the socket, wherever it is held.
|
|
166
|
+
async def tell(self, app: SocketId, entry: Entry) -> bool:
|
|
167
|
+
"""Send an entry to one socket; False when no gateway holds it."""
|
|
168
|
+
send = self.sockets.get(app)
|
|
169
|
+
if send is None:
|
|
170
|
+
return await told_app(self.signal, app, ForApp(entry=entry))
|
|
171
|
+
await send(entry)
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
async def ask(self, params: str) -> asyncio.Future[DevAnswer]:
|
|
175
|
+
"""A future for the answer to a dev.request, answered here or by the socket's gateway."""
|
|
176
|
+
answer: asyncio.Future[DevAnswer] = asyncio.get_running_loop().create_future()
|
|
177
|
+
self.pending_answers[params] = answer
|
|
178
|
+
heard = asyncio.Event()
|
|
179
|
+
task = asyncio.create_task(self._answer_from_there(params, answer, heard))
|
|
180
|
+
self.answers[params] = task
|
|
181
|
+
|
|
182
|
+
# Answered, or given up on (the asker cancels it): the listener goes with it.
|
|
183
|
+
def over(_: asyncio.Future[DevAnswer]) -> None:
|
|
184
|
+
self.answers.pop(params, None)
|
|
185
|
+
task.cancel()
|
|
186
|
+
|
|
187
|
+
answer.add_done_callback(over)
|
|
188
|
+
await heard.wait()
|
|
189
|
+
return answer
|
|
190
|
+
|
|
191
|
+
async def dev_answered(self, answer: DevAnswer) -> bool:
|
|
192
|
+
"""Hand an app's dev.answer to the door waiting for it; False when nobody waits."""
|
|
193
|
+
waiting = self.pending_answers.pop(answer.id, None)
|
|
194
|
+
if waiting is None:
|
|
195
|
+
return await answered_back(self.signal, answer)
|
|
196
|
+
if waiting.done():
|
|
197
|
+
return False
|
|
198
|
+
waiting.set_result(answer)
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def serve(self, served: Served) -> None:
|
|
202
|
+
"""Serve the call to the socket its door chose; a call already served stays as it is."""
|
|
203
|
+
if served.call in self.calls:
|
|
204
|
+
return
|
|
205
|
+
self.calls[served.call] = served
|
|
206
|
+
self.pumped(served.call, after=0)
|
|
207
|
+
heard = asyncio.Event()
|
|
208
|
+
self.listening[served.call] = (
|
|
209
|
+
heard,
|
|
210
|
+
asyncio.create_task(self._commands(served.call, heard)),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
async def commands_heard(self, call: str) -> None:
|
|
214
|
+
"""Return once the call's commands are listened for here."""
|
|
215
|
+
listening = self.listening.get(call)
|
|
216
|
+
if listening is not None:
|
|
217
|
+
await listening[0].wait()
|
|
218
|
+
|
|
219
|
+
# Synchronous, so two sockets taking one parked call cannot both have it. The new socket's pump
|
|
220
|
+
# starts once call.attached is written (`attach`), from it.
|
|
221
|
+
def attach(self, call: str, app: SocketId | None) -> Served | None:
|
|
222
|
+
"""Move the call to this socket (None parks it); None when nothing moved."""
|
|
223
|
+
served = self.calls.get(call)
|
|
224
|
+
if served is None or served.app == app:
|
|
225
|
+
return None
|
|
226
|
+
self._stopped(call)
|
|
227
|
+
moved = replace(served, app=app)
|
|
228
|
+
self.calls[call] = moved
|
|
229
|
+
return moved
|
|
230
|
+
|
|
231
|
+
# What any gateway writes to the call reaches its socket here, in seq order, from `after`;
|
|
232
|
+
# `then` goes down right after the first entry (the tools still waiting, after call.attached).
|
|
233
|
+
def pumped(self, call: str, *, after: int, then: Sequence[Entry] = ()) -> None:
|
|
234
|
+
"""Start sending the call's entries to its socket, if the socket is open here."""
|
|
235
|
+
served = self.calls.get(call)
|
|
236
|
+
if served is None or served.app is None:
|
|
237
|
+
return
|
|
238
|
+
send = self.sockets.get(served.app)
|
|
239
|
+
if send is None:
|
|
240
|
+
told_bound(self.signal, served.app, Bound(call=call, after=after))
|
|
241
|
+
return
|
|
242
|
+
self._stopped(call)
|
|
243
|
+
self.pumps[call] = asyncio.ensure_future(pumped(served.log, after, send, then))
|
|
244
|
+
|
|
245
|
+
def opened_by(self, call: str, opener: Opener | None) -> None:
|
|
246
|
+
"""Say who opened the call, once it is served here."""
|
|
247
|
+
served = self.calls.get(call)
|
|
248
|
+
if served is not None:
|
|
249
|
+
self.calls[call] = replace(served, opened_by=opener)
|
|
250
|
+
|
|
251
|
+
def bound_to(self, app: SocketId) -> list[str]:
|
|
252
|
+
"""The calls this socket serves."""
|
|
253
|
+
return [call for call, served in self.calls.items() if served.app == app]
|
|
254
|
+
|
|
255
|
+
def parked(self, scope: Scope, agent: str) -> list[str]:
|
|
256
|
+
"""The calls of the agent in the scope that wait for a socket."""
|
|
257
|
+
return [
|
|
258
|
+
call
|
|
259
|
+
for call, served in self.calls.items()
|
|
260
|
+
if served.app is None
|
|
261
|
+
and served.opened_here
|
|
262
|
+
and served.agent == agent
|
|
263
|
+
and served.scope == scope
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
# The concurrent calls quota counts these, never head rows: a dead worker's row would count
|
|
267
|
+
# for ever. Per world, as every quota is: the sandbox's calls never close production.
|
|
268
|
+
# The concurrent-calls quota reads it: every gateway's count, the others' a second old at most,
|
|
269
|
+
# so two gateways admitting at once may go past the limit by what they opened that second.
|
|
270
|
+
def running(self, org: str, env: Env) -> int:
|
|
271
|
+
"""How many of the org's calls are open in the world, on every gateway of the box."""
|
|
272
|
+
named = f"{org}|{env}"
|
|
273
|
+
theirs = sum(heard.share.get(named, 0) for heard in self.counted.theirs.values())
|
|
274
|
+
return self._running_here().get(named, 0) + theirs
|
|
275
|
+
|
|
276
|
+
def in_use(self, call: str, now: float) -> None:
|
|
277
|
+
"""A door asked for a call first seen here: it stays while it is asked for."""
|
|
278
|
+
if call in self.seen:
|
|
279
|
+
self.seen[call] = now
|
|
280
|
+
|
|
281
|
+
# A call first seen here is a cache of what was kept: let go when idle, and read again later.
|
|
282
|
+
def idle(self, now: float) -> None:
|
|
283
|
+
"""Let go of the calls first seen here that no door asked for in FIRST_SEEN_IDLE_S."""
|
|
284
|
+
idle = [call for call, at in self.seen.items() if now - at > FIRST_SEEN_IDLE_S]
|
|
285
|
+
for call in idle:
|
|
286
|
+
if call not in self.pumps:
|
|
287
|
+
self.close(call)
|
|
288
|
+
|
|
289
|
+
def close(self, call: str) -> None:
|
|
290
|
+
"""Forget a finished call: its entries end, and its worker's command stream."""
|
|
291
|
+
self.seen.pop(call, None)
|
|
292
|
+
served = self.calls.pop(call, None)
|
|
293
|
+
if served is None:
|
|
294
|
+
return
|
|
295
|
+
self._stopped(call)
|
|
296
|
+
served.commands.put_nowait(None)
|
|
297
|
+
self.owners.running(call, here=False)
|
|
298
|
+
listening = self.listening.pop(call, None)
|
|
299
|
+
if listening is not None:
|
|
300
|
+
listening[1].cancel()
|
|
301
|
+
|
|
302
|
+
# A voice call's commands wait here for its worker's stream; a written call's session takes
|
|
303
|
+
# them at once. Heard whichever gateway the app or the desk sent them to.
|
|
304
|
+
async def _commands(self, call: str, heard: asyncio.Event) -> None:
|
|
305
|
+
try:
|
|
306
|
+
listening = await self.signal.subscribe(COMMANDS_CHANNEL.format(call=call))
|
|
307
|
+
except NotAvailable:
|
|
308
|
+
logger.warning("call %s: its commands are not heard here, the signal is down", call)
|
|
309
|
+
return
|
|
310
|
+
finally:
|
|
311
|
+
heard.set()
|
|
312
|
+
try:
|
|
313
|
+
async for data in listening:
|
|
314
|
+
await self._applied(call, Command.model_validate_json(data))
|
|
315
|
+
finally:
|
|
316
|
+
listening.close()
|
|
317
|
+
|
|
318
|
+
async def _applied(self, call: str, command: Command) -> None:
|
|
319
|
+
served = self.calls.get(call)
|
|
320
|
+
if served is None or served.agent != command.agent:
|
|
321
|
+
return
|
|
322
|
+
if served.session is None:
|
|
323
|
+
served.commands.put_nowait(command)
|
|
324
|
+
elif command.type == SUPERVISOR_VERB:
|
|
325
|
+
await served.session.supervise(SupervisorVerb.model_validate(command.data))
|
|
326
|
+
else:
|
|
327
|
+
model = command_of(command)
|
|
328
|
+
declared_for_the_call(served, model)
|
|
329
|
+
await served.session.apply(model)
|
|
330
|
+
|
|
331
|
+
def _running_here(self) -> dict[str, int]:
|
|
332
|
+
counts: dict[str, int] = {}
|
|
333
|
+
for served in self.calls.values():
|
|
334
|
+
if served.opened_here:
|
|
335
|
+
named = f"{served.scope.org}|{served.scope.env}"
|
|
336
|
+
counts[named] = counts.get(named, 0) + 1
|
|
337
|
+
return counts
|
|
338
|
+
|
|
339
|
+
def _say_sockets(self) -> None:
|
|
340
|
+
self.elsewhere.shared.put(
|
|
341
|
+
tuple(
|
|
342
|
+
SocketRow(item.app, item.scope, item.address, item.connected_at, item.host)
|
|
343
|
+
for item in self.processes.values()
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def _stop(self, app: SocketId) -> Callable[[str], Awaitable[None]]:
|
|
348
|
+
async def stop(why: str) -> None:
|
|
349
|
+
await told_app(self.signal, app, ForApp(stop=why))
|
|
350
|
+
|
|
351
|
+
return stop
|
|
352
|
+
|
|
353
|
+
async def _answer_from_there(
|
|
354
|
+
self, params: str, answer: asyncio.Future[DevAnswer], heard: asyncio.Event
|
|
355
|
+
) -> None:
|
|
356
|
+
try:
|
|
357
|
+
listening = await self.signal.subscribe(ANSWER_CHANNEL.format(id=params))
|
|
358
|
+
except NotAvailable:
|
|
359
|
+
return
|
|
360
|
+
finally:
|
|
361
|
+
heard.set()
|
|
362
|
+
try:
|
|
363
|
+
async for data in listening:
|
|
364
|
+
if not answer.done():
|
|
365
|
+
answer.set_result(DevAnswer.model_validate_json(data))
|
|
366
|
+
return
|
|
367
|
+
finally:
|
|
368
|
+
listening.close()
|
|
369
|
+
|
|
370
|
+
def _stopped(self, call: str) -> None:
|
|
371
|
+
pump = self.pumps.pop(call, None)
|
|
372
|
+
if pump is not None:
|
|
373
|
+
pump.cancel()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@dataclass(frozen=True)
|
|
377
|
+
class Serving:
|
|
378
|
+
"""What serves a call: the process's connections, the logs, and the calls live in it."""
|
|
379
|
+
|
|
380
|
+
connections: Connections
|
|
381
|
+
logs: Logs
|
|
382
|
+
live: ServedCalls
|
|
383
|
+
# None when the box embeds nothing: every lookup finds nothing and no hang-up remembers.
|
|
384
|
+
embedder: Embedder | None
|
|
385
|
+
# The blocks of prompt this process has kept, so a call's prompt is read back whole.
|
|
386
|
+
prompts: Prompts = field(default_factory=Prompts)
|
|
387
|
+
# What the process counts, for /metrics: the seal holds up an org's unusual spend there.
|
|
388
|
+
counters: Counters = field(default_factory=Counters)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def declared_for_the_call(served: Served, model: WireModel) -> None:
|
|
392
|
+
"""A written call's own declaration of what is private, as the app configured it."""
|
|
393
|
+
privacy = served.log.privacy
|
|
394
|
+
if isinstance(model, SessionConfigure) and model.config is not None and privacy is not None:
|
|
395
|
+
declared = with_app_fields(privacy.config, model.config)
|
|
396
|
+
served.log.privacy = replace(privacy, config=declared)
|