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,164 @@
|
|
|
1
|
+
"""The log read back off Postgres: the newest calls, one call whole or folded, a call followed."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
|
|
9
|
+
from pinecall.domain.errors import NotFound
|
|
10
|
+
from pinecall.log import queries
|
|
11
|
+
from pinecall.log.logs import Logs
|
|
12
|
+
from pinecall.log.reduce import reduce
|
|
13
|
+
from pinecall.log.store import Store
|
|
14
|
+
from pinecall.postgres.pool import Pool, open_pool
|
|
15
|
+
from pinecall.process.settings import Settings
|
|
16
|
+
from pinecall.tenancy import reads
|
|
17
|
+
from pinecall.tenancy.reads import Read
|
|
18
|
+
from pinecall.wire.events import TERMINAL_EVENT, CallSummary
|
|
19
|
+
from pinecall.wire.frames import Entry
|
|
20
|
+
from pinecall.wire.rest.calls import ReadKind
|
|
21
|
+
|
|
22
|
+
NO_SUCH_CALL = "no call {call} was ever written here"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
NOT_RECORDED = "call {call} was not recorded: its call.summary carries no path"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
NO_LIVE_CALL = "no call is open right now"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sessions_group(group: argparse.ArgumentParser) -> None:
|
|
32
|
+
"""`sessions`: list, show, tail, recording."""
|
|
33
|
+
sessions = group.add_subparsers(required=True)
|
|
34
|
+
listing = sessions.add_parser("list", help="the newest calls")
|
|
35
|
+
listing.add_argument("--agent", default=None)
|
|
36
|
+
listing.add_argument("--limit", type=int, default=20)
|
|
37
|
+
listing.set_defaults(run=sessions_list)
|
|
38
|
+
show = sessions.add_parser("show", help="one call, entry by entry, or its state with --json")
|
|
39
|
+
show.add_argument("call")
|
|
40
|
+
show.add_argument("--json", dest="as_json", action="store_true")
|
|
41
|
+
show.set_defaults(run=sessions_show)
|
|
42
|
+
tail = sessions.add_parser("tail", help="a call as it happens; none named, the newest live")
|
|
43
|
+
tail.add_argument("call", nargs="?", default=None)
|
|
44
|
+
tail.set_defaults(run=sessions_tail)
|
|
45
|
+
recording = sessions.add_parser("recording", help="where the call's audio was written")
|
|
46
|
+
recording.add_argument("call")
|
|
47
|
+
recording.set_defaults(run=sessions_recording)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sessions_list(settings: Settings, args: argparse.Namespace) -> int:
|
|
51
|
+
"""The newest calls of the box, or of one agent: when, whose, how they ended."""
|
|
52
|
+
return asyncio.run(_listed(settings, args.agent, args.limit))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def sessions_show(settings: Settings, args: argparse.Namespace) -> int:
|
|
56
|
+
"""One call's log printed whole, or the state it folds to as JSON."""
|
|
57
|
+
if args.as_json:
|
|
58
|
+
return asyncio.run(_folded(settings, args.call))
|
|
59
|
+
return asyncio.run(_shown(settings, args.call))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def sessions_tail(settings: Settings, args: argparse.Namespace) -> int:
|
|
63
|
+
"""A call followed as it is written, until it seals."""
|
|
64
|
+
return asyncio.run(_tailed(settings, args.call))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sessions_recording(settings: Settings, args: argparse.Namespace) -> int:
|
|
68
|
+
"""The path the call's audio was written to; 1 when it kept none."""
|
|
69
|
+
return asyncio.run(_recording(settings, args.call))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def _listed(settings: Settings, agent: str | None, limit: int) -> int:
|
|
73
|
+
pool = await open_pool(settings.database_url)
|
|
74
|
+
try:
|
|
75
|
+
store = Store(pool)
|
|
76
|
+
calls = await store.newest_calls(limit, agent=agent)
|
|
77
|
+
facts = await queries.facts_of_calls(pool, calls)
|
|
78
|
+
finally:
|
|
79
|
+
await pool.close()
|
|
80
|
+
for call in calls:
|
|
81
|
+
row = facts.get(call)
|
|
82
|
+
if row is None:
|
|
83
|
+
_line_out(call)
|
|
84
|
+
continue
|
|
85
|
+
_line_out(
|
|
86
|
+
f"{call} {row.agent} {row.channel or '-':9} {_when(row.ended_at)}"
|
|
87
|
+
f" {row.end_reason or 'open':16} {row.outcome or ''}"
|
|
88
|
+
)
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def _shown(settings: Settings, call: str) -> int:
|
|
93
|
+
for entry in await _whole(settings, call):
|
|
94
|
+
_line_out(_line(entry))
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _folded(settings: Settings, call: str) -> int:
|
|
99
|
+
entries = await _whole(settings, call)
|
|
100
|
+
_line_out(json.dumps(reduce(entries).model_dump(mode="json", by_alias=True), indent=2))
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def _tailed(settings: Settings, call: str | None) -> int:
|
|
105
|
+
pool = await open_pool(settings.database_url)
|
|
106
|
+
try:
|
|
107
|
+
store = Store(pool)
|
|
108
|
+
named = call or await store.newest_live_call()
|
|
109
|
+
if named is None:
|
|
110
|
+
raise NotFound(NO_LIVE_CALL)
|
|
111
|
+
await _read_by_the_operator(pool, named, "log")
|
|
112
|
+
async for entry in Logs(store).reading(named).stream():
|
|
113
|
+
_line_out(_line(entry))
|
|
114
|
+
if entry.type == TERMINAL_EVENT:
|
|
115
|
+
return 0
|
|
116
|
+
finally:
|
|
117
|
+
await pool.close()
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def _recording(settings: Settings, call: str) -> int:
|
|
122
|
+
entries = await _whole(settings, call, "recording")
|
|
123
|
+
summary = next((entry for entry in reversed(entries) if entry.type == "call.summary"), None)
|
|
124
|
+
path = None if summary is None else CallSummary.model_validate(summary.data).recording
|
|
125
|
+
if path is None:
|
|
126
|
+
sys.stderr.write(f"{NOT_RECORDED.format(call=call)}\n")
|
|
127
|
+
return 1
|
|
128
|
+
_line_out(path)
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def _whole(settings: Settings, call: str, what: ReadKind = "log") -> list[Entry]:
|
|
133
|
+
pool = await open_pool(settings.database_url)
|
|
134
|
+
try:
|
|
135
|
+
entries = await Store(pool).whole(call)
|
|
136
|
+
if entries:
|
|
137
|
+
await _read_by_the_operator(pool, call, what)
|
|
138
|
+
finally:
|
|
139
|
+
await pool.close()
|
|
140
|
+
if not entries:
|
|
141
|
+
raise NotFound(NO_SUCH_CALL.format(call=call))
|
|
142
|
+
return entries
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# The operator reading an org's call off the box is a read the org's access log shows.
|
|
146
|
+
async def _read_by_the_operator(pool: Pool, call: str, what: ReadKind) -> None:
|
|
147
|
+
kept = await queries.scope_of_call(pool, call)
|
|
148
|
+
if kept is not None and kept.scope is not None:
|
|
149
|
+
await reads.record(pool, kept.scope, Read(call, what, reads.OPERATOR))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _line(entry: Entry) -> str:
|
|
153
|
+
data = json.dumps(entry.data, ensure_ascii=False)
|
|
154
|
+
return f"{entry.seq:>5} {_when(entry.ts)} {entry.type:24} {data}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _when(at: float | None) -> str:
|
|
158
|
+
if at is None:
|
|
159
|
+
return "-" * 19
|
|
160
|
+
return datetime.fromtimestamp(at, UTC).strftime("%Y-%m-%d %H:%M:%S")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _line_out(text: str) -> None:
|
|
164
|
+
sys.stdout.write(f"{text}\n")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""`traceback <number>`: every phone call with a number and every dial to it, for a carrier."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
|
|
8
|
+
from pinecall.domain.errors import DeclarationRefused
|
|
9
|
+
from pinecall.postgres.pool import open_pool
|
|
10
|
+
from pinecall.process.settings import Settings
|
|
11
|
+
from pinecall.tenancy import reads, traceback
|
|
12
|
+
from pinecall.wire.rest.ops import Traceback, TracebackCall, TracebackDial
|
|
13
|
+
|
|
14
|
+
NOT_A_DAY = "--since {since}: a day, like 2026-09-01"
|
|
15
|
+
|
|
16
|
+
NOTHING = "no call with {number} and no dial to it since {since}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def traceback_verb(verb: argparse.ArgumentParser) -> None:
|
|
20
|
+
"""`traceback <number> [--since YYYY-MM-DD]`."""
|
|
21
|
+
verb.add_argument("number")
|
|
22
|
+
verb.add_argument("--since", default=None, help="a day, YYYY-MM-DD; 24 months back unset")
|
|
23
|
+
verb.set_defaults(run=traced)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def traced(settings: Settings, args: argparse.Namespace) -> int:
|
|
27
|
+
"""Print the number's calls, kept or erased, and its dials, oldest first."""
|
|
28
|
+
since = day_of(args.since)
|
|
29
|
+
found = asyncio.run(_found(settings, args.number, since))
|
|
30
|
+
for line in lines_of(found):
|
|
31
|
+
sys.stdout.write(f"{line}\n")
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def lines_of(found: Traceback) -> list[str]:
|
|
36
|
+
"""The traceback as lines a person pastes into an answer to a carrier."""
|
|
37
|
+
if not found.calls and not found.dials:
|
|
38
|
+
return [NOTHING.format(number=found.number, since=_day(found.since))]
|
|
39
|
+
calls = [_call_line(call) for call in found.calls]
|
|
40
|
+
dials = [_dial_line(dial) for dial in found.dials]
|
|
41
|
+
return [
|
|
42
|
+
f"{found.number} since {_day(found.since)}",
|
|
43
|
+
f"calls ({len(calls)})",
|
|
44
|
+
*calls,
|
|
45
|
+
f"dials ({len(dials)})",
|
|
46
|
+
*dials,
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def day_of(day: str | None) -> float | None:
|
|
51
|
+
"""The start of a day given as YYYY-MM-DD, in UTC; None when none was given."""
|
|
52
|
+
if day is None:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
return datetime.strptime(day, "%Y-%m-%d").replace(tzinfo=UTC).timestamp()
|
|
56
|
+
except ValueError:
|
|
57
|
+
raise DeclarationRefused(NOT_A_DAY.format(since=day)) from None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _found(settings: Settings, number: str, since: float | None) -> Traceback:
|
|
61
|
+
pool = await open_pool(settings.database_url)
|
|
62
|
+
try:
|
|
63
|
+
found = await traceback.of_number(pool, number, since)
|
|
64
|
+
await traceback.read_by(pool, found, reads.OPERATOR)
|
|
65
|
+
return found
|
|
66
|
+
finally:
|
|
67
|
+
await pool.close()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _call_line(call: TracebackCall) -> str:
|
|
71
|
+
lasted = "-"
|
|
72
|
+
if call.started_at is not None and call.ended_at is not None:
|
|
73
|
+
lasted = f"{round(call.ended_at - call.started_at)}s"
|
|
74
|
+
kept = "erased, record kept" if call.erased else "kept"
|
|
75
|
+
return (
|
|
76
|
+
f" {_when(call.started_at)} {call.direction or '-':8} {call.from_number or '-'} -> "
|
|
77
|
+
f"{call.to_number or '-'} {lasted:>6} {call.end_reason or '-'} "
|
|
78
|
+
f"org {call.org or '-'} {call.env or '-'} {call.call} {kept}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _dial_line(dial: TracebackDial) -> str:
|
|
83
|
+
outcome = f"refused: {dial.refused}" if dial.refused else f"placed {dial.call or '-'}"
|
|
84
|
+
return (
|
|
85
|
+
f" {_when(dial.at)} shown {dial.shown or '-'} org {dial.org} {dial.env} "
|
|
86
|
+
f"agent {dial.agent} asked by {dial.asked_by} {outcome}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _when(at: float | None) -> str:
|
|
91
|
+
return "-" * 16 if at is None else datetime.fromtimestamp(at, UTC).strftime("%Y-%m-%d %H:%M")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _day(at: float) -> str:
|
|
95
|
+
return datetime.fromtimestamp(at, UTC).strftime("%Y-%m-%d")
|
pinecall/cli/_usage.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""`usage rebuild`: every org's usage totals folded again from the summaries in the log."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from pinecall.postgres.pool import open_pool
|
|
8
|
+
from pinecall.process.settings import Settings
|
|
9
|
+
from pinecall.tenancy import usage
|
|
10
|
+
|
|
11
|
+
REBUILT = "{calls} summaries refolded into {rows} rows of usage_totals\n"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def usage_group(group: argparse.ArgumentParser) -> None:
|
|
15
|
+
"""`usage rebuild`."""
|
|
16
|
+
verbs = group.add_subparsers(required=True)
|
|
17
|
+
rebuilt = verbs.add_parser("rebuild", help="fold the usage totals again from the log")
|
|
18
|
+
rebuilt.set_defaults(run=rebuild)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def rebuild(settings: Settings, _args: argparse.Namespace) -> int:
|
|
22
|
+
"""Refold every org's totals from the log and say how many summaries and rows."""
|
|
23
|
+
refolded = asyncio.run(_rebuilt(settings))
|
|
24
|
+
calls = sum(used.calls for used in refolded.values())
|
|
25
|
+
sys.stdout.write(REBUILT.format(calls=calls, rows=len(refolded)))
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def _rebuilt(settings: Settings) -> dict[usage.Month, usage.Usage]:
|
|
30
|
+
pool = await open_pool(settings.database_url)
|
|
31
|
+
try:
|
|
32
|
+
return await usage.rebuild(pool)
|
|
33
|
+
finally:
|
|
34
|
+
await pool.close()
|