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,603 @@
|
|
|
1
|
+
"""Contact memory: the facts table, recalled per turn, held and forgotten, and its goldens."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import unicodedata
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Literal
|
|
11
|
+
from uuid import UUID, uuid4
|
|
12
|
+
|
|
13
|
+
from psycopg import sql
|
|
14
|
+
from psycopg.rows import DictRow
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
from pinecall.domain.errors import DeclarationRefused
|
|
18
|
+
from pinecall.domain.names import Env
|
|
19
|
+
from pinecall.domain.scope import Scope
|
|
20
|
+
from pinecall.postgres.pool import Connection, Pool
|
|
21
|
+
from pinecall.retrieval._search import (
|
|
22
|
+
CANDIDATES_PER_BRANCH,
|
|
23
|
+
Evidence,
|
|
24
|
+
Figures,
|
|
25
|
+
Hit,
|
|
26
|
+
SearchedTable,
|
|
27
|
+
evidence_of,
|
|
28
|
+
figures,
|
|
29
|
+
hybrid,
|
|
30
|
+
relative_to_the_best,
|
|
31
|
+
top_cosine,
|
|
32
|
+
)
|
|
33
|
+
from pinecall.retrieval.embed import Embedder, halfvec
|
|
34
|
+
from pinecall.wire.rest.retrieval import (
|
|
35
|
+
MemoryGolden,
|
|
36
|
+
MemoryMiss,
|
|
37
|
+
MemoryQuestion,
|
|
38
|
+
MemoryScore,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
DEFAULT_FACTS_PER_TURN = 6
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# A fact learned a half-life ago weighs half of one learned now.
|
|
48
|
+
HALF_LIFE_DAYS = 90
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
A_DAY_S = 86_400
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
TEXT_INDEX = "contact_memories_text_bm25"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
COLUMNS = (
|
|
58
|
+
"id",
|
|
59
|
+
"contact",
|
|
60
|
+
"category",
|
|
61
|
+
"text",
|
|
62
|
+
"valid_from",
|
|
63
|
+
"invalidated_at",
|
|
64
|
+
"supersedes",
|
|
65
|
+
"source_call",
|
|
66
|
+
"model",
|
|
67
|
+
"confidence",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# There is no delete: a fact is only ever superseded or ended, and the history keeps it.
|
|
72
|
+
type OpName = Literal["add", "update", "invalidate"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
BAD_CURSOR = "{after!r} is not a cursor: pass the `next` a page answered, or nothing"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
HELD_NOW = sql.SQL(
|
|
79
|
+
"org = %(org)s AND env = %(env)s AND holder = %(holder)s AND contact = %(contact)s"
|
|
80
|
+
" AND invalidated_at IS NULL"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
HELD_THEN = sql.SQL(
|
|
85
|
+
"org = %(org)s AND env = %(env)s AND holder = %(holder)s AND contact = %(contact)s"
|
|
86
|
+
" AND valid_from <= %(as_of)s AND (invalidated_at IS NULL OR invalidated_at > %(as_of)s)"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Vectors of two models share a width and still mean nothing to each other.
|
|
91
|
+
SAME_SPACE = sql.SQL("model = %(model)s")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
ENDED = """
|
|
95
|
+
UPDATE contact_memories SET invalidated_at = %(at)s
|
|
96
|
+
WHERE id = %(of)s::uuid AND org = %(org)s AND env = %(env)s AND holder = %(holder)s
|
|
97
|
+
AND invalidated_at IS NULL
|
|
98
|
+
RETURNING id
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# The count comes back as a row, not read off the command tag.
|
|
103
|
+
FORGOTTEN = """
|
|
104
|
+
WITH gone AS (
|
|
105
|
+
DELETE FROM contact_memories
|
|
106
|
+
WHERE org = %(org)s AND env = %(env)s AND holder = %(holder)s AND contact = %(contact)s
|
|
107
|
+
RETURNING id
|
|
108
|
+
)
|
|
109
|
+
SELECT count(*) AS forgotten FROM gone
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# What the quota counts: this world's current facts, every holder's; history is not a fact held.
|
|
114
|
+
KEPT = """
|
|
115
|
+
SELECT count(*) AS kept FROM contact_memories
|
|
116
|
+
WHERE org = %(org)s AND env = %(env)s AND invalidated_at IS NULL
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# A batch at a time: each one written leaves the model's, so the next read starts past it.
|
|
121
|
+
STALE = """
|
|
122
|
+
SELECT id, text FROM contact_memories WHERE model <> %(model)s ORDER BY created_at, id
|
|
123
|
+
LIMIT %(batch)s
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
REEMBEDDED = """
|
|
128
|
+
UPDATE contact_memories SET embedding = %(embedding)s::halfvec, model = %(model)s
|
|
129
|
+
WHERE id = %(id)s AND model <> %(model)s
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class Fact:
|
|
135
|
+
"""One row of a contact's memory: what it says, when it held, and what wrote it."""
|
|
136
|
+
|
|
137
|
+
id: str
|
|
138
|
+
contact: str
|
|
139
|
+
category: str | None
|
|
140
|
+
text: str
|
|
141
|
+
valid_from: datetime
|
|
142
|
+
invalidated_at: datetime | None = None
|
|
143
|
+
supersedes: str | None = None
|
|
144
|
+
# The call that taught it; None for a golden's.
|
|
145
|
+
taught_by: str | None = None
|
|
146
|
+
model: str = ""
|
|
147
|
+
confidence: float = 1.0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class RecalledFact:
|
|
152
|
+
"""A fact recalled, and its score relative to the best of the recall."""
|
|
153
|
+
|
|
154
|
+
fact: Fact
|
|
155
|
+
score: float
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class Recalled:
|
|
160
|
+
"""What a recall found, best first, and how strongly the best vector says it answers."""
|
|
161
|
+
|
|
162
|
+
facts: list[RecalledFact]
|
|
163
|
+
top_cosine: float
|
|
164
|
+
evidence: Evidence
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class Recall:
|
|
169
|
+
"""A recall of one contact's facts: the query, the moment, how many, and what held when."""
|
|
170
|
+
|
|
171
|
+
contact: str
|
|
172
|
+
query: str
|
|
173
|
+
at: datetime
|
|
174
|
+
k: int = DEFAULT_FACTS_PER_TURN
|
|
175
|
+
# Reads the facts that held at that moment rather than the current ones.
|
|
176
|
+
as_of: datetime | None = None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True)
|
|
180
|
+
class Taught:
|
|
181
|
+
"""When a contact's memory was taught, and on which call: what every written fact carries."""
|
|
182
|
+
|
|
183
|
+
contact: str
|
|
184
|
+
call: str | None
|
|
185
|
+
at: datetime
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True)
|
|
189
|
+
class Held:
|
|
190
|
+
"""Sentences written as a contact's facts with no model, all at one moment."""
|
|
191
|
+
|
|
192
|
+
contact: str
|
|
193
|
+
sentences: tuple[str, ...]
|
|
194
|
+
at: datetime
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True)
|
|
198
|
+
class Op:
|
|
199
|
+
"""One change the extraction model asked of a contact's memory."""
|
|
200
|
+
|
|
201
|
+
op: OpName
|
|
202
|
+
text: str = ""
|
|
203
|
+
category: str | None = None
|
|
204
|
+
of: str | None = None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class Paging(BaseModel):
|
|
208
|
+
"""A page of current facts: after which cursor, the words to look for, and how many."""
|
|
209
|
+
|
|
210
|
+
model_config = ConfigDict(frozen=True)
|
|
211
|
+
|
|
212
|
+
after: str | None = None
|
|
213
|
+
q: str | None = Field(default=None, max_length=200)
|
|
214
|
+
limit: int = Field(default=50, ge=1, le=200)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass(frozen=True)
|
|
218
|
+
class FactsPage:
|
|
219
|
+
"""A page of facts, newest first, the cursor of the next, and the agent that taught each."""
|
|
220
|
+
|
|
221
|
+
facts: list[Fact]
|
|
222
|
+
next: str | None
|
|
223
|
+
agents: Mapping[str, str] = field(default_factory=dict[str, str])
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True)
|
|
227
|
+
class Answered:
|
|
228
|
+
"""A golden's question, and the texts of the facts recalled for it, best first."""
|
|
229
|
+
|
|
230
|
+
question: MemoryQuestion
|
|
231
|
+
found: tuple[str, ...]
|
|
232
|
+
|
|
233
|
+
# Only the first fact that says it counts: memory repeating itself answered once.
|
|
234
|
+
@property
|
|
235
|
+
def ranks(self) -> tuple[int | None, ...]:
|
|
236
|
+
"""The rank of each expected fact, from 1, or None when it was not recalled."""
|
|
237
|
+
return tuple(
|
|
238
|
+
next((at for at, text in enumerate(self.found, 1) if says(text, expected)), None)
|
|
239
|
+
for expected in self.question.expects
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def missing(self) -> tuple[str, ...]:
|
|
244
|
+
"""The expected facts that were not recalled."""
|
|
245
|
+
return tuple(
|
|
246
|
+
expected
|
|
247
|
+
for expected, rank in zip(self.question.expects, self.ranks, strict=True)
|
|
248
|
+
if rank is None
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(frozen=True)
|
|
253
|
+
class Score:
|
|
254
|
+
"""A memory golden's figures over every question, and the questions it missed."""
|
|
255
|
+
|
|
256
|
+
questions: int
|
|
257
|
+
k: int
|
|
258
|
+
figures: Figures
|
|
259
|
+
misses: tuple[Answered, ...]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
OP_NAMES: tuple[OpName, ...] = ("add", "update", "invalidate")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
WRITES: frozenset[OpName] = frozenset({"add", "update"})
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
NAMES_A_FACT: frozenset[OpName] = frozenset({"update", "invalidate"})
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
_LISTED = sql.SQL(", ").join(sql.Identifier(column) for column in COLUMNS)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
CURRENT = sql.SQL(
|
|
275
|
+
"SELECT {columns} FROM contact_memories WHERE org = %(org)s AND env = %(env)s"
|
|
276
|
+
" AND holder = %(holder)s AND contact = %(contact)s AND invalidated_at IS NULL"
|
|
277
|
+
" ORDER BY valid_from, id"
|
|
278
|
+
).format(columns=_LISTED)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
HISTORY = sql.SQL(
|
|
282
|
+
"SELECT {columns} FROM contact_memories WHERE org = %(org)s AND env = %(env)s"
|
|
283
|
+
" AND holder = %(holder)s AND contact = %(contact)s"
|
|
284
|
+
" ORDER BY (invalidated_at IS NULL) DESC, valid_from DESC, id"
|
|
285
|
+
).format(columns=_LISTED)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
ADDED = sql.SQL(
|
|
289
|
+
"INSERT INTO contact_memories"
|
|
290
|
+
" (org, env, holder, contact, text, category, embedding, valid_from, source_call, model)"
|
|
291
|
+
" VALUES (%(org)s, %(env)s, %(holder)s, %(contact)s, %(text)s, %(category)s,"
|
|
292
|
+
" %(embedding)s::halfvec, %(at)s, %(call)s, %(model)s)"
|
|
293
|
+
" RETURNING {columns}"
|
|
294
|
+
).format(columns=_LISTED)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# The old row ends and its replacement is written in one statement; nothing is written when the
|
|
298
|
+
# old row had already ended.
|
|
299
|
+
UPDATED = sql.SQL(
|
|
300
|
+
"WITH ended AS ("
|
|
301
|
+
" UPDATE contact_memories SET invalidated_at = %(at)s"
|
|
302
|
+
" WHERE id = %(of)s::uuid AND org = %(org)s AND env = %(env)s AND holder = %(holder)s"
|
|
303
|
+
" AND contact = %(contact)s AND invalidated_at IS NULL RETURNING id)"
|
|
304
|
+
" INSERT INTO contact_memories"
|
|
305
|
+
" (org, env, holder, contact, text, category, embedding, valid_from, source_call, model,"
|
|
306
|
+
" supersedes)"
|
|
307
|
+
" SELECT %(org)s, %(env)s, %(holder)s, %(contact)s, %(text)s, %(category)s,"
|
|
308
|
+
" %(embedding)s::halfvec, %(at)s, %(call)s, %(model)s, ended.id FROM ended"
|
|
309
|
+
" RETURNING {columns}"
|
|
310
|
+
).format(columns=_LISTED)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# A fact is an agent's through the call that taught it: a golden's fact has no call, and no agent.
|
|
314
|
+
TAUGHT_BY = sql.SQL(
|
|
315
|
+
"SELECT {columns}, head.agent AS taught_by_agent FROM contact_memories AS memory"
|
|
316
|
+
" JOIN call_log_head AS head ON head.log = memory.source_call"
|
|
317
|
+
" WHERE memory.org = %(org)s AND memory.env = %(env)s AND memory.holder = %(holder)s"
|
|
318
|
+
" AND (%(agent)s::text IS NULL OR head.agent = %(agent)s)"
|
|
319
|
+
" AND memory.invalidated_at IS NULL"
|
|
320
|
+
" AND (%(q)s::text IS NULL OR memory.text ILIKE %(q)s OR memory.contact ILIKE %(q)s"
|
|
321
|
+
" OR memory.category ILIKE %(q)s)"
|
|
322
|
+
" AND (%(after_at)s::timestamptz IS NULL"
|
|
323
|
+
" OR (memory.valid_from, memory.id) < (%(after_at)s::timestamptz, %(after_id)s::uuid))"
|
|
324
|
+
" ORDER BY memory.valid_from DESC, memory.id DESC"
|
|
325
|
+
" LIMIT %(limit)s"
|
|
326
|
+
).format(columns=sql.SQL(", ").join(sql.Identifier("memory", column) for column in COLUMNS))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# Memory has no fallback to the org's own facts: a corner recalls its own or nothing.
|
|
330
|
+
async def recall(pool: Pool, embedder: Embedder, scope: Scope, request: Recall) -> Recalled:
|
|
331
|
+
"""The contact's best facts for the query: both branches fused, then recency and confidence."""
|
|
332
|
+
(vector,) = await embedder.embed([request.query])
|
|
333
|
+
model = embedder.embedding.model
|
|
334
|
+
table = SearchedTable(
|
|
335
|
+
name="contact_memories",
|
|
336
|
+
text_index=TEXT_INDEX,
|
|
337
|
+
scope=HELD_NOW if request.as_of is None else HELD_THEN,
|
|
338
|
+
params={**_where(scope, request.contact), "as_of": request.as_of, "model": model},
|
|
339
|
+
columns=COLUMNS,
|
|
340
|
+
same_space=SAME_SPACE,
|
|
341
|
+
)
|
|
342
|
+
async with pool.connection() as connection:
|
|
343
|
+
hits = await hybrid(
|
|
344
|
+
connection, table, vector=vector, words=request.query, room=CANDIDATES_PER_BRANCH
|
|
345
|
+
)
|
|
346
|
+
# A cosine against another model's vector measures nothing.
|
|
347
|
+
best = top_cosine([hit for hit in hits if hit.row["model"] == model])
|
|
348
|
+
return Recalled(
|
|
349
|
+
facts=ranked(hits, now=request.as_of or request.at, k=request.k),
|
|
350
|
+
top_cosine=best,
|
|
351
|
+
evidence=evidence_of(best),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def ranked(hits: Sequence[Hit], *, now: datetime, k: int) -> list[RecalledFact]:
|
|
356
|
+
"""The k best hits once each is weighed by recency and confidence, relative to the best."""
|
|
357
|
+
weighed = [
|
|
358
|
+
dataclasses.replace(
|
|
359
|
+
hit,
|
|
360
|
+
fused=hit.fused * recency(hit.row["valid_from"], now) * hit.row["confidence"],
|
|
361
|
+
)
|
|
362
|
+
for hit in hits
|
|
363
|
+
]
|
|
364
|
+
return [
|
|
365
|
+
RecalledFact(fact=_fact_of(hit.row), score=hit.fused)
|
|
366
|
+
for hit in relative_to_the_best(weighed)[:k]
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def recency(learned: datetime, now: datetime) -> float:
|
|
371
|
+
"""1.0 for a fact learned now, halving every half-life; a date after now counts as now."""
|
|
372
|
+
days = max((now - learned).total_seconds(), 0.0) / A_DAY_S
|
|
373
|
+
return 0.5 ** (days / HALF_LIFE_DAYS)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def current(pool: Pool, scope: Scope, contact: str) -> list[Fact]:
|
|
377
|
+
"""The contact's facts that hold now, oldest first."""
|
|
378
|
+
return await _facts(pool, CURRENT, _where(scope, contact))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
async def history(pool: Pool, scope: Scope, contact: str) -> list[Fact]:
|
|
382
|
+
"""Every fact ever kept of the contact in the corner, current first, newest first."""
|
|
383
|
+
return await _facts(pool, HISTORY, _where(scope, contact))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
async def taught_by(pool: Pool, scope: Scope, agent: str | None, *, page: Paging) -> FactsPage:
|
|
387
|
+
"""A page of the current facts the agent's calls taught, newest first; None for every agent."""
|
|
388
|
+
after = _cursor(page.after)
|
|
389
|
+
params: dict[str, object] = {
|
|
390
|
+
"org": scope.org,
|
|
391
|
+
"env": scope.env,
|
|
392
|
+
"holder": scope.holder,
|
|
393
|
+
"agent": agent,
|
|
394
|
+
"q": f"%{_like_escaped(page.q)}%" if page.q else None,
|
|
395
|
+
"after_at": None if after is None else after[0],
|
|
396
|
+
"after_id": None if after is None else after[1],
|
|
397
|
+
"limit": page.limit + 1,
|
|
398
|
+
}
|
|
399
|
+
async with pool.connection() as connection:
|
|
400
|
+
rows = await (await connection.execute(TAUGHT_BY, params)).fetchall()
|
|
401
|
+
shown = rows[: page.limit]
|
|
402
|
+
facts = [_fact_of(row) for row in shown]
|
|
403
|
+
more = _cursor_of(facts[-1]) if len(rows) > page.limit else None
|
|
404
|
+
agents = {fact.id: str(row["taught_by_agent"]) for fact, row in zip(facts, shown, strict=True)}
|
|
405
|
+
return FactsPage(facts=facts, next=more, agents=agents)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
async def kept(pool: Pool, org: str, env: Env) -> int:
|
|
409
|
+
"""How many current facts the org keeps in the world, across every holder and contact."""
|
|
410
|
+
async with pool.connection() as connection:
|
|
411
|
+
row = await (await connection.execute(KEPT, {"org": org, "env": env})).fetchone()
|
|
412
|
+
return 0 if row is None else int(row["kept"])
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
async def hold(pool: Pool, embedder: Embedder, scope: Scope, request: Held) -> list[Fact]:
|
|
416
|
+
"""The sentences written as the contact's facts, embedded, with no model asked."""
|
|
417
|
+
vectors = await embedder.embed(list(request.sentences)) if request.sentences else []
|
|
418
|
+
base = {**_where(scope, request.contact), "at": request.at, "call": None, "category": None}
|
|
419
|
+
written: list[Fact] = []
|
|
420
|
+
async with pool.connection() as connection, connection.transaction():
|
|
421
|
+
for text, vector in zip(request.sentences, vectors, strict=True):
|
|
422
|
+
fact = await _added(connection, embedder, {**base, "text": text, "of": None}, vector)
|
|
423
|
+
if fact is not None:
|
|
424
|
+
written.append(fact)
|
|
425
|
+
return written
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# Every row, superseded ones included: an erasure that left the history would erase nothing.
|
|
429
|
+
async def forget(pool: Pool, scope: Scope, contact: str) -> int:
|
|
430
|
+
"""Delete every fact of the contact in the corner; how many went, zero included."""
|
|
431
|
+
async with pool.connection() as connection:
|
|
432
|
+
row = await (await connection.execute(FORGOTTEN, _where(scope, contact))).fetchone()
|
|
433
|
+
return 0 if row is None else int(row["forgotten"])
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
async def invalidated(pool: Pool, scope: Scope, fact_id: str, *, at: datetime) -> bool:
|
|
437
|
+
"""End one current fact of the corner at `at`; False when there is none by that id."""
|
|
438
|
+
params = {"org": scope.org, "env": scope.env, "holder": scope.holder, "of": fact_id}
|
|
439
|
+
async with pool.connection() as connection:
|
|
440
|
+
ended = await (await connection.execute(ENDED, {**params, "at": at})).fetchone()
|
|
441
|
+
return ended is not None
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# One transaction per batch: a failure leaves the batches before it written, and a second run
|
|
445
|
+
# skips them.
|
|
446
|
+
async def reembed(pool: Pool, embedder: Embedder, *, batch: int = 64) -> int:
|
|
447
|
+
"""Re-embed every fact another model wrote, in every org and world; how many there were."""
|
|
448
|
+
model = embedder.embedding.model
|
|
449
|
+
done = 0
|
|
450
|
+
while True:
|
|
451
|
+
async with pool.connection() as connection:
|
|
452
|
+
params = {"model": model, "batch": batch}
|
|
453
|
+
rows = await (await connection.execute(STALE, params)).fetchall()
|
|
454
|
+
if not rows:
|
|
455
|
+
return done
|
|
456
|
+
vectors = await embedder.embed([str(row["text"]) for row in rows])
|
|
457
|
+
written = [
|
|
458
|
+
{"id": row["id"], "embedding": halfvec(vector), "model": model}
|
|
459
|
+
for row, vector in zip(rows, vectors, strict=True)
|
|
460
|
+
]
|
|
461
|
+
async with (
|
|
462
|
+
pool.connection() as connection,
|
|
463
|
+
connection.transaction(),
|
|
464
|
+
connection.cursor() as cursor,
|
|
465
|
+
):
|
|
466
|
+
await cursor.executemany(REEMBEDDED, written)
|
|
467
|
+
done += len(rows)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
async def ask_golden(
|
|
471
|
+
pool: Pool, embedder: Embedder, scope: Scope, golden: MemoryGolden, *, at: datetime
|
|
472
|
+
) -> MemoryScore:
|
|
473
|
+
"""Each question's facts written to a scratch contact, recalled, scored, and forgotten."""
|
|
474
|
+
k = golden.k or DEFAULT_FACTS_PER_TURN
|
|
475
|
+
scratch = f"golden-{uuid4().hex}"
|
|
476
|
+
started = time.perf_counter()
|
|
477
|
+
answered: list[Answered] = []
|
|
478
|
+
try:
|
|
479
|
+
for question in golden.questions:
|
|
480
|
+
await hold(pool, embedder, scope, Held(scratch, tuple(question.holds), at))
|
|
481
|
+
recalled = await recall(
|
|
482
|
+
pool, embedder, scope, Recall(scratch, question.asks, at=at, k=k)
|
|
483
|
+
)
|
|
484
|
+
await forget(pool, scope, scratch)
|
|
485
|
+
found = tuple(item.fact.text for item in recalled.facts)
|
|
486
|
+
answered.append(Answered(question=question, found=found))
|
|
487
|
+
finally:
|
|
488
|
+
await forget(pool, scope, scratch)
|
|
489
|
+
score = score_memory(answered, k)
|
|
490
|
+
return MemoryScore(
|
|
491
|
+
model=embedder.embedding.model,
|
|
492
|
+
questions=score.questions,
|
|
493
|
+
k=score.k,
|
|
494
|
+
recall_at_k=score.figures.recall_at_k,
|
|
495
|
+
ndcg_at_10=score.figures.ndcg_at_10,
|
|
496
|
+
took_ms=(time.perf_counter() - started) * 1000,
|
|
497
|
+
misses=[
|
|
498
|
+
MemoryMiss(asks=item.question.asks, missing=list(item.missing), found=list(item.found))
|
|
499
|
+
for item in score.misses
|
|
500
|
+
],
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def score_memory(answered: Sequence[Answered], k: int) -> Score:
|
|
505
|
+
"""recall@k and nDCG@10 over every question; a question missing any expected fact is a miss."""
|
|
506
|
+
return Score(
|
|
507
|
+
questions=len(answered),
|
|
508
|
+
k=k,
|
|
509
|
+
figures=figures([item.ranks for item in answered]),
|
|
510
|
+
misses=tuple(item for item in answered if None in item.ranks),
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# Facts are written by a model, so the golden is held to containment, one direction: a fact that
|
|
515
|
+
# says less than expected is a miss.
|
|
516
|
+
def says(found: str, expected: str) -> bool:
|
|
517
|
+
"""Whether a recalled fact contains the expected text, accents, case and spacing aside."""
|
|
518
|
+
return _unaccented(expected) in _unaccented(found)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# Embedded before the transaction opens: an update never ends a fact without its replacement.
|
|
522
|
+
async def apply_ops(
|
|
523
|
+
pool: Pool, embedder: Embedder, scope: Scope, taught: Taught, ops: Sequence[Op]
|
|
524
|
+
) -> list[Fact]:
|
|
525
|
+
"""Write the ops as one: adds and updates embedded first; an update ends the row it replaces."""
|
|
526
|
+
writing = [op for op in ops if op.op in WRITES]
|
|
527
|
+
vectors = await embedder.embed([op.text for op in writing]) if writing else []
|
|
528
|
+
vector_of = dict(zip(writing, vectors, strict=True))
|
|
529
|
+
base = {**_where(scope, taught.contact), "at": taught.at, "call": taught.call}
|
|
530
|
+
written: list[Fact] = []
|
|
531
|
+
async with pool.connection() as connection, connection.transaction():
|
|
532
|
+
for op in ops:
|
|
533
|
+
if op.op == "invalidate":
|
|
534
|
+
await connection.execute(ENDED, {**base, "of": op.of})
|
|
535
|
+
continue
|
|
536
|
+
row = {**base, "text": op.text, "category": op.category, "of": op.of}
|
|
537
|
+
fact = await _added(connection, embedder, row, vector_of[op])
|
|
538
|
+
if fact is not None:
|
|
539
|
+
written.append(fact)
|
|
540
|
+
return written
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def folded(text: str) -> str:
|
|
544
|
+
"""The text case-folded with its spaces squeezed: how two sentences are compared."""
|
|
545
|
+
return " ".join(text.casefold().split())
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _where(scope: Scope, contact: str) -> dict[str, object]:
|
|
549
|
+
return {"org": scope.org, "env": scope.env, "holder": scope.holder, "contact": contact}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
async def _facts(pool: Pool, query: sql.Composed, params: Mapping[str, object]) -> list[Fact]:
|
|
553
|
+
async with pool.connection() as connection:
|
|
554
|
+
rows = await (await connection.execute(query, params)).fetchall()
|
|
555
|
+
return [_fact_of(row) for row in rows]
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _fact_of(row: DictRow) -> Fact:
|
|
559
|
+
supersedes = row["supersedes"]
|
|
560
|
+
return Fact(
|
|
561
|
+
id=str(row["id"]),
|
|
562
|
+
contact=row["contact"],
|
|
563
|
+
category=row["category"],
|
|
564
|
+
text=row["text"],
|
|
565
|
+
valid_from=row["valid_from"],
|
|
566
|
+
invalidated_at=row["invalidated_at"],
|
|
567
|
+
supersedes=None if supersedes is None else str(supersedes),
|
|
568
|
+
taught_by=row["source_call"],
|
|
569
|
+
model=row["model"],
|
|
570
|
+
confidence=row["confidence"],
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
async def _added(
|
|
575
|
+
connection: Connection, embedder: Embedder, params: Mapping[str, object], vector: list[float]
|
|
576
|
+
) -> Fact | None:
|
|
577
|
+
written = {**params, "embedding": halfvec(vector), "model": embedder.embedding.model}
|
|
578
|
+
query = UPDATED if params.get("of") is not None else ADDED
|
|
579
|
+
row = await (await connection.execute(query, written)).fetchone()
|
|
580
|
+
return None if row is None else _fact_of(row)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _cursor_of(fact: Fact) -> str:
|
|
584
|
+
return f"{fact.valid_from.isoformat()}|{fact.id}"
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _cursor(after: str | None) -> tuple[datetime, UUID] | None:
|
|
588
|
+
if not after:
|
|
589
|
+
return None
|
|
590
|
+
moment, _, named = after.partition("|")
|
|
591
|
+
try:
|
|
592
|
+
return datetime.fromisoformat(moment), UUID(named)
|
|
593
|
+
except ValueError as unread:
|
|
594
|
+
raise DeclarationRefused(BAD_CURSOR.format(after=after)) from unread
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _like_escaped(words: str) -> str:
|
|
598
|
+
return words.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _unaccented(text: str) -> str:
|
|
602
|
+
letters = unicodedata.normalize("NFD", text)
|
|
603
|
+
return folded("".join(item for item in letters if not unicodedata.combining(item)))
|
|
File without changes
|