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,406 @@
|
|
|
1
|
+
"""An agent's tuning and lexicon, versioned per scope, and what is current."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection, Sequence
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Literal, LiteralString
|
|
7
|
+
|
|
8
|
+
from psycopg import sql
|
|
9
|
+
from psycopg.rows import DictRow
|
|
10
|
+
from psycopg.types.json import Jsonb
|
|
11
|
+
from pydantic import TypeAdapter
|
|
12
|
+
|
|
13
|
+
from pinecall.domain.agent import Lexicon, Tuning, Version, Versions
|
|
14
|
+
from pinecall.domain.errors import Conflict
|
|
15
|
+
from pinecall.domain.names import PRODUCTION, Json, JsonObject
|
|
16
|
+
from pinecall.domain.scope import THE_ORGS_OWN, Scope
|
|
17
|
+
from pinecall.postgres.pool import Connection, Pool
|
|
18
|
+
from pinecall.tenancy.canary import bucket_of
|
|
19
|
+
|
|
20
|
+
type VersionedTable = Literal["agent_config", "lexicon"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MOVED = "this corner is at v{newest} now, not the version you read: read it again, then set again"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
HISTORY_LIMIT = 20
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
TUNING: TypeAdapter[Tuning] = TypeAdapter(Tuning)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
HISTORY = sql.SQL("""
|
|
33
|
+
SELECT holder, version, {value} AS value, author, note, set_at FROM {table}
|
|
34
|
+
WHERE org = %(org)s AND env = %(env)s AND holder = %(holder)s AND agent = %(agent)s
|
|
35
|
+
ORDER BY version DESC LIMIT %(limit)s
|
|
36
|
+
""")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# The version a call ran on is the holder's or, when it fell through, the org's own.
|
|
40
|
+
AT = sql.SQL("""
|
|
41
|
+
SELECT holder, version, {value} AS value, author, note, set_at FROM {table}
|
|
42
|
+
WHERE org = %(org)s AND env = %(env)s AND holder IN (%(holder)s, '') AND agent = %(agent)s
|
|
43
|
+
AND version = %(version)s
|
|
44
|
+
ORDER BY holder DESC LIMIT 1
|
|
45
|
+
""")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
NEWEST = sql.SQL("""
|
|
49
|
+
SELECT coalesce(max(version), 0) AS newest FROM {table}
|
|
50
|
+
WHERE org = %(org)s AND env = %(env)s AND holder = %(holder)s AND agent = %(agent)s
|
|
51
|
+
""")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# The next version, unless the scope moved since the writer read it: then no row.
|
|
55
|
+
PUT_TUNING = """
|
|
56
|
+
INSERT INTO agent_config (org, env, holder, agent, version, config, author, note)
|
|
57
|
+
SELECT %(org)s, %(env)s, %(holder)s, %(agent)s, coalesce(max(version), 0) + 1, %(value)s,
|
|
58
|
+
%(author)s, %(note)s
|
|
59
|
+
FROM agent_config
|
|
60
|
+
WHERE org = %(org)s AND env = %(env)s AND holder = %(holder)s AND agent = %(agent)s
|
|
61
|
+
HAVING %(if_version)s::integer IS NULL OR coalesce(max(version), 0) = %(if_version)s
|
|
62
|
+
ON CONFLICT (org, env, holder, agent, version) DO NOTHING
|
|
63
|
+
RETURNING version
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
PUT_LEXICON = """
|
|
68
|
+
INSERT INTO lexicon (org, env, holder, agent, version, said, heard, author, note)
|
|
69
|
+
SELECT %(org)s, %(env)s, %(holder)s, %(agent)s, coalesce(max(version), 0) + 1, %(said)s,
|
|
70
|
+
%(heard)s, %(author)s, %(note)s
|
|
71
|
+
FROM lexicon
|
|
72
|
+
WHERE org = %(org)s AND env = %(env)s AND holder = %(holder)s AND agent = %(agent)s
|
|
73
|
+
HAVING %(if_version)s::integer IS NULL OR coalesce(max(version), 0) = %(if_version)s
|
|
74
|
+
ON CONFLICT (org, env, holder, agent, version) DO NOTHING
|
|
75
|
+
RETURNING version
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Both chains in one statement, so the tuning and the lexicon a call is built on are read from
|
|
80
|
+
# the same moment: the holder's newest row, then the org's own, of each. Where a level stands on a
|
|
81
|
+
# canary (tenancy/canary.py), a call whose bucket is under its share runs the canary's version and
|
|
82
|
+
# every other call, or a read for no call, the newest version but that one. A version named (an
|
|
83
|
+
# eval run's candidate) is the scope's own level's, whatever canary stands there.
|
|
84
|
+
STANDING = """
|
|
85
|
+
SELECT * FROM (
|
|
86
|
+
SELECT DISTINCT ON (config.holder) 'tuning' AS kind, config.holder, config.version,
|
|
87
|
+
config.config AS value, config.author, config.note, config.set_at
|
|
88
|
+
FROM agent_config config
|
|
89
|
+
LEFT JOIN LATERAL (
|
|
90
|
+
SELECT canary.version, canary.share FROM agent_canaries canary
|
|
91
|
+
WHERE canary.org = config.org AND canary.env = config.env
|
|
92
|
+
AND canary.holder = config.holder AND canary.agent = config.agent
|
|
93
|
+
ORDER BY canary.set_at DESC, canary.id DESC
|
|
94
|
+
LIMIT 1
|
|
95
|
+
) standing ON true
|
|
96
|
+
WHERE config.org = %(org)s AND config.env = %(env)s AND config.holder IN (%(holder)s, '')
|
|
97
|
+
AND config.agent = %(agent)s
|
|
98
|
+
AND CASE WHEN %(version)s::integer IS NOT NULL AND config.holder = %(holder)s
|
|
99
|
+
THEN config.version = %(version)s
|
|
100
|
+
ELSE standing.version IS NULL
|
|
101
|
+
OR (config.version = standing.version)
|
|
102
|
+
= coalesce(%(bucket)s::integer < standing.share, false)
|
|
103
|
+
END
|
|
104
|
+
ORDER BY config.holder DESC, config.version DESC
|
|
105
|
+
) tuning
|
|
106
|
+
UNION ALL
|
|
107
|
+
SELECT * FROM (
|
|
108
|
+
SELECT DISTINCT ON (holder) 'lexicon', holder, version,
|
|
109
|
+
jsonb_build_object('said', said, 'heard', heard), author, note, set_at
|
|
110
|
+
FROM lexicon
|
|
111
|
+
WHERE org = %(org)s AND env = %(env)s AND holder IN (%(holder)s, '') AND agent = %(agent)s
|
|
112
|
+
ORDER BY holder DESC, version DESC
|
|
113
|
+
) lexicon
|
|
114
|
+
ORDER BY kind, holder DESC
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
EVERY_AGENT = """
|
|
119
|
+
SELECT DISTINCT ON (agent, holder) agent, holder, version, config AS value, author, note, set_at
|
|
120
|
+
FROM agent_config
|
|
121
|
+
WHERE org = %(org)s AND env = %(env)s AND holder IN (%(holder)s, '')
|
|
122
|
+
ORDER BY agent, holder DESC, version DESC
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# The versions a drift names: the ones its calls ran and the ones set in its window. The same
|
|
127
|
+
# number at both levels is the holder's, as the version a call ran on is read (AT).
|
|
128
|
+
NOTED = """
|
|
129
|
+
SELECT DISTINCT ON (version) holder, version, author, note, set_at FROM agent_config
|
|
130
|
+
WHERE org = %(org)s AND env = %(env)s AND holder IN (%(holder)s, '') AND agent = %(agent)s
|
|
131
|
+
AND (version = ANY(%(versions)s)
|
|
132
|
+
OR (set_at >= to_timestamp(%(since)s) AND set_at < to_timestamp(%(until)s)))
|
|
133
|
+
ORDER BY version, holder DESC
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class Noted:
|
|
139
|
+
"""A version of the agent's tuning without its value: whose, which, who wrote it, why, when."""
|
|
140
|
+
|
|
141
|
+
holder: str
|
|
142
|
+
version: int
|
|
143
|
+
author: str
|
|
144
|
+
note: str | None
|
|
145
|
+
set_at: datetime
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# The call picks between a canary's version and the rest by its id; neither is the rest.
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class Picked:
|
|
151
|
+
"""Which of a scope's versions a read is for: a call's, a version named, or the rest's."""
|
|
152
|
+
|
|
153
|
+
call: str | None = None
|
|
154
|
+
version: int | None = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class Written:
|
|
159
|
+
"""Who writes a version, why, and the version they read, when they want no other."""
|
|
160
|
+
|
|
161
|
+
author: str
|
|
162
|
+
note: str | None = None
|
|
163
|
+
if_version: int | None = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class SideBySide[T]:
|
|
168
|
+
"""What a settings screen shows side by side: yours, the team's and production's newest."""
|
|
169
|
+
|
|
170
|
+
yours: Version[T] | None
|
|
171
|
+
team: Version[T] | None
|
|
172
|
+
production: Version[T] | None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(frozen=True)
|
|
176
|
+
class Current:
|
|
177
|
+
"""The tuning and the lexicon a call in the scope is built on, and their versions."""
|
|
178
|
+
|
|
179
|
+
tuning: Tuning
|
|
180
|
+
lexicon: Lexicon
|
|
181
|
+
versions: Versions
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# What each table keeps as its value, read as one JSON object so both are read the same way.
|
|
185
|
+
VALUE: dict[VersionedTable, sql.Composable] = {
|
|
186
|
+
"agent_config": sql.SQL("config"),
|
|
187
|
+
"lexicon": sql.SQL("jsonb_build_object('said', said, 'heard', heard)"),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def tuning_history(
|
|
192
|
+
pool: Pool, scope: Scope, agent: str, *, limit: int = HISTORY_LIMIT
|
|
193
|
+
) -> list[Version[Tuning]]:
|
|
194
|
+
"""The scope's own versions of the agent's tuning, newest first."""
|
|
195
|
+
rows = await _history(pool, "agent_config", scope, agent, limit)
|
|
196
|
+
return [_tuning(row) for row in rows]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def lexicon_history(
|
|
200
|
+
pool: Pool, scope: Scope, agent: str, *, limit: int = HISTORY_LIMIT
|
|
201
|
+
) -> list[Version[Lexicon]]:
|
|
202
|
+
"""The scope's own versions of the agent's lexicon, newest first."""
|
|
203
|
+
return [_lexicon(row) for row in await _history(pool, "lexicon", scope, agent, limit)]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def tuning_at(pool: Pool, scope: Scope, agent: str, version: int) -> Version[Tuning] | None:
|
|
207
|
+
"""One version of the agent's tuning, the scope's own or the org's it fell through to."""
|
|
208
|
+
row = await _at(pool, "agent_config", _where(scope, agent), version)
|
|
209
|
+
return None if row is None else _tuning(row)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def lexicon_at(pool: Pool, scope: Scope, agent: str, version: int) -> Version[Lexicon] | None:
|
|
213
|
+
"""One version of the agent's lexicon, the scope's own or the org's it fell through to."""
|
|
214
|
+
row = await _at(pool, "lexicon", _where(scope, agent), version)
|
|
215
|
+
return None if row is None else _lexicon(row)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def put_tuning(pool: Pool, scope: Scope, agent: str, tuning: Tuning, written: Written) -> int:
|
|
219
|
+
"""Write the agent's next tuning in the scope; a scope moved since it was read refuses."""
|
|
220
|
+
values: dict[str, object] = {
|
|
221
|
+
**_where(scope, agent),
|
|
222
|
+
"value": Jsonb(_stored(tuning)),
|
|
223
|
+
"author": written.author,
|
|
224
|
+
"note": written.note,
|
|
225
|
+
"if_version": written.if_version,
|
|
226
|
+
}
|
|
227
|
+
return await _put(pool, "agent_config", PUT_TUNING, values)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def put_lexicon(
|
|
231
|
+
pool: Pool, scope: Scope, agent: str, lexicon: Lexicon, written: Written
|
|
232
|
+
) -> int:
|
|
233
|
+
"""Write the agent's next lexicon in the scope; a scope moved since it was read refuses."""
|
|
234
|
+
values: dict[str, object] = {
|
|
235
|
+
**_where(scope, agent),
|
|
236
|
+
"said": Jsonb(dict(lexicon.said)),
|
|
237
|
+
"heard": Jsonb(list(lexicon.heard)),
|
|
238
|
+
"author": written.author,
|
|
239
|
+
"note": written.note,
|
|
240
|
+
"if_version": written.if_version,
|
|
241
|
+
}
|
|
242
|
+
return await _put(pool, "lexicon", PUT_LEXICON, values)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def current(pool: Pool, scope: Scope, agent: str, picked: Picked | None = None) -> Current:
|
|
246
|
+
"""What a call in the scope is built on: each knob from the nearest scope that sets it."""
|
|
247
|
+
wanted = picked or Picked()
|
|
248
|
+
params = {
|
|
249
|
+
**_where(scope, agent),
|
|
250
|
+
"bucket": None if wanted.call is None else bucket_of(wanted.call),
|
|
251
|
+
"version": wanted.version,
|
|
252
|
+
}
|
|
253
|
+
async with pool.connection() as connection:
|
|
254
|
+
rows = await (await connection.execute(STANDING, params)).fetchall()
|
|
255
|
+
tuned = resolve([_tuning(row) for row in rows if row["kind"] == "tuning"])
|
|
256
|
+
words = next((_lexicon(row) for row in rows if row["kind"] == "lexicon"), None)
|
|
257
|
+
return Current(
|
|
258
|
+
tuning=Tuning() if tuned is None else tuned.value,
|
|
259
|
+
lexicon=Lexicon() if words is None else words.value,
|
|
260
|
+
versions=Versions(
|
|
261
|
+
config=None if tuned is None else tuned.version,
|
|
262
|
+
lexicon=None if words is None else words.version,
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def versions_noted(
|
|
268
|
+
pool: Pool, scope: Scope, agent: str, versions: Collection[int], between: tuple[float, float]
|
|
269
|
+
) -> list[Noted]:
|
|
270
|
+
"""The agent's versions named, and those set in [since, until), oldest first, without values."""
|
|
271
|
+
since, until = between
|
|
272
|
+
params = {**_where(scope, agent), "versions": list(versions), "since": since, "until": until}
|
|
273
|
+
async with pool.connection() as connection:
|
|
274
|
+
rows = await (await connection.execute(NOTED, params)).fetchall()
|
|
275
|
+
return [
|
|
276
|
+
Noted(row["holder"], row["version"], row["author"], row["note"], row["set_at"])
|
|
277
|
+
for row in rows
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
async def every_tuning(pool: Pool, scope: Scope) -> dict[str, Tuning]:
|
|
282
|
+
"""Every agent's tuning as it stands in the scope."""
|
|
283
|
+
async with pool.connection() as connection:
|
|
284
|
+
rows = await (await connection.execute(EVERY_AGENT, _where(scope, None))).fetchall()
|
|
285
|
+
chains: dict[str, list[Version[Tuning]]] = {}
|
|
286
|
+
for row in rows:
|
|
287
|
+
chains.setdefault(row["agent"], []).append(_tuning(row))
|
|
288
|
+
resolved = {agent: resolve(chain) for agent, chain in chains.items()}
|
|
289
|
+
return {agent: kept.value for agent, kept in resolved.items() if kept is not None}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def tuning_side_by_side(pool: Pool, scope: Scope, agent: str) -> SideBySide[Tuning]:
|
|
293
|
+
"""The agent's tuning as yours, the team's and production's, none falling through."""
|
|
294
|
+
return SideBySide(
|
|
295
|
+
yours=await _newest_tuning(pool, scope, agent) if scope.holder else None,
|
|
296
|
+
team=await _newest_tuning(pool, replace(scope, holder=THE_ORGS_OWN), agent),
|
|
297
|
+
production=await _newest_tuning(pool, Scope(scope.org, PRODUCTION), agent),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def lexicon_side_by_side(pool: Pool, scope: Scope, agent: str) -> SideBySide[Lexicon]:
|
|
302
|
+
"""The agent's lexicon as yours, the team's and production's, none falling through."""
|
|
303
|
+
return SideBySide(
|
|
304
|
+
yours=await _newest_lexicon(pool, scope, agent) if scope.holder else None,
|
|
305
|
+
team=await _newest_lexicon(pool, replace(scope, holder=THE_ORGS_OWN), agent),
|
|
306
|
+
production=await _newest_lexicon(pool, Scope(scope.org, PRODUCTION), agent),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# A knob is set when it is not None, so a falsy value is set and does not fall through:
|
|
311
|
+
# `bases: []` is "no bases", not "the team's". The version is the nearest row that set any.
|
|
312
|
+
def resolve(chain: Sequence[Version[Tuning]]) -> Version[Tuning] | None:
|
|
313
|
+
"""Merge a nearest-first chain knob by knob; None when no row sets any knob."""
|
|
314
|
+
supplying = [(row, stored) for row in chain if (stored := _stored(row.value))]
|
|
315
|
+
if not supplying:
|
|
316
|
+
return None
|
|
317
|
+
merged: JsonObject = {}
|
|
318
|
+
for _row, stored in reversed(supplying):
|
|
319
|
+
merged.update(stored)
|
|
320
|
+
nearest, _ = supplying[0]
|
|
321
|
+
return replace(nearest, value=TUNING.validate_python(merged))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def _newest_tuning(pool: Pool, scope: Scope, agent: str) -> Version[Tuning] | None:
|
|
325
|
+
rows = await _history(pool, "agent_config", scope, agent, 1)
|
|
326
|
+
return _tuning(rows[0]) if rows else None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
async def _newest_lexicon(pool: Pool, scope: Scope, agent: str) -> Version[Lexicon] | None:
|
|
330
|
+
rows = await _history(pool, "lexicon", scope, agent, 1)
|
|
331
|
+
return _lexicon(rows[0]) if rows else None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def _history(
|
|
335
|
+
pool: Pool, table: VersionedTable, scope: Scope, agent: str, limit: int
|
|
336
|
+
) -> list[DictRow]:
|
|
337
|
+
query = HISTORY.format(**_parts(table))
|
|
338
|
+
async with pool.connection() as connection:
|
|
339
|
+
values = {**_where(scope, agent), "limit": limit}
|
|
340
|
+
return await (await connection.execute(query, values)).fetchall()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
async def _at(
|
|
344
|
+
pool: Pool, table: VersionedTable, where: dict[str, object], version: int
|
|
345
|
+
) -> DictRow | None:
|
|
346
|
+
query = AT.format(**_parts(table))
|
|
347
|
+
async with pool.connection() as connection:
|
|
348
|
+
values = {**where, "version": version}
|
|
349
|
+
return await (await connection.execute(query, values)).fetchone()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
async def _put(
|
|
353
|
+
pool: Pool, table: VersionedTable, statement: LiteralString, values: dict[str, object]
|
|
354
|
+
) -> int:
|
|
355
|
+
# independent: a write the version refused, then the newest version read to say so
|
|
356
|
+
async with pool.connection() as connection:
|
|
357
|
+
row = await (await connection.execute(statement, values)).fetchone()
|
|
358
|
+
if row is not None:
|
|
359
|
+
return int(row["version"])
|
|
360
|
+
raise Conflict(MOVED.format(newest=await _newest(connection, table, values)))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
async def _newest(connection: Connection, table: VersionedTable, values: dict[str, object]) -> int:
|
|
364
|
+
row = await (await connection.execute(NEWEST.format(**_parts(table)), values)).fetchone()
|
|
365
|
+
return 0 if row is None else int(row["newest"])
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _parts(table: VersionedTable) -> dict[str, sql.Composable]:
|
|
369
|
+
return {"table": sql.Identifier(table), "value": VALUE[table]}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _where(scope: Scope, agent: str | None) -> dict[str, object]:
|
|
373
|
+
return {"org": scope.org, "env": scope.env, "holder": scope.holder, "agent": agent}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _stored(tuning: Tuning) -> JsonObject:
|
|
377
|
+
dumped: JsonObject = TUNING.dump_python(tuning, mode="json", exclude_none=True)
|
|
378
|
+
return dumped
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _tuning(row: DictRow) -> Version[Tuning]:
|
|
382
|
+
return _kept(row, TUNING.validate_python(row["value"]))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _lexicon(row: DictRow) -> Version[Lexicon]:
|
|
386
|
+
value: dict[str, Json] = row["value"]
|
|
387
|
+
answer = value.get("said") or {}
|
|
388
|
+
heard = value.get("heard") or []
|
|
389
|
+
lexicon = Lexicon(
|
|
390
|
+
said={str(word): str(spoken) for word, spoken in dict(answer).items()}
|
|
391
|
+
if isinstance(answer, dict)
|
|
392
|
+
else {},
|
|
393
|
+
heard=tuple(str(word) for word in heard) if isinstance(heard, list) else (),
|
|
394
|
+
)
|
|
395
|
+
return _kept(row, lexicon)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _kept[T](row: DictRow, value: T) -> Version[T]:
|
|
399
|
+
return Version(
|
|
400
|
+
holder=row["holder"],
|
|
401
|
+
version=row["version"],
|
|
402
|
+
author=row["author"],
|
|
403
|
+
note=row["note"],
|
|
404
|
+
set_at=row["set_at"],
|
|
405
|
+
value=value,
|
|
406
|
+
)
|