rememberstack 0.1.0__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.
- rememberstack/__init__.py +9 -0
- rememberstack/adapters/__init__.py +42 -0
- rememberstack/adapters/codex_writer.py +221 -0
- rememberstack/adapters/markitdown_converter.py +42 -0
- rememberstack/adapters/openrouter.py +136 -0
- rememberstack/adapters/selfhost/__init__.py +54 -0
- rememberstack/adapters/selfhost/forget.py +66 -0
- rememberstack/adapters/selfhost/git.py +374 -0
- rememberstack/adapters/selfhost/lance.py +328 -0
- rememberstack/adapters/selfhost/minio.py +279 -0
- rememberstack/adapters/selfhost/mounts.py +249 -0
- rememberstack/adapters/selfhost/object_store.py +130 -0
- rememberstack/adapters/selfhost/projection.py +80 -0
- rememberstack/adapters/selfhost/queue.py +137 -0
- rememberstack/adapters/selfhost/telemetry.py +45 -0
- rememberstack/adapters/selfhost/watcher.py +70 -0
- rememberstack/adapters/testing/__init__.py +15 -0
- rememberstack/adapters/testing/cost_meter.py +13 -0
- rememberstack/adapters/testing/model_provider.py +83 -0
- rememberstack/adapters/testing/queue.py +43 -0
- rememberstack/adapters/testing/telemetry.py +22 -0
- rememberstack/client.py +19 -0
- rememberstack/core/__init__.py +127 -0
- rememberstack/core/blockizer.py +189 -0
- rememberstack/core/chunker.py +216 -0
- rememberstack/core/consumption_skill.py +275 -0
- rememberstack/core/conversion.py +76 -0
- rememberstack/core/core_manifest.py +598 -0
- rememberstack/core/extension_packs.py +124 -0
- rememberstack/core/forget.py +17 -0
- rememberstack/core/knowledge_authored.py +276 -0
- rememberstack/core/knowledge_compile.py +215 -0
- rememberstack/core/knowledge_fact_sheet.py +210 -0
- rememberstack/core/knowledge_hashing.py +68 -0
- rememberstack/core/knowledge_planner.py +64 -0
- rememberstack/core/knowledge_writer.py +175 -0
- rememberstack/core/ranking.py +200 -0
- rememberstack/core/recipe_linter.py +149 -0
- rememberstack/core/section_snap.py +209 -0
- rememberstack/core/storage_routing.py +27 -0
- rememberstack/eval/__init__.py +53 -0
- rememberstack/eval/consumption.py +141 -0
- rememberstack/eval/contradiction.py +184 -0
- rememberstack/eval/harness.py +136 -0
- rememberstack/eval/lifecycle.py +400 -0
- rememberstack/eval/operational_scale.py +49 -0
- rememberstack/eval/resolution.py +255 -0
- rememberstack/eval/retrieval_spikes.py +50 -0
- rememberstack/eval/skeleton.py +231 -0
- rememberstack/llm/__init__.py +1 -0
- rememberstack/model/__init__.py +589 -0
- rememberstack/model/adjudication.py +100 -0
- rememberstack/model/auth.py +27 -0
- rememberstack/model/blocks.py +30 -0
- rememberstack/model/chunks.py +190 -0
- rememberstack/model/claims.py +162 -0
- rememberstack/model/client.py +98 -0
- rememberstack/model/clustering.py +54 -0
- rememberstack/model/component_version.py +124 -0
- rememberstack/model/consumption.py +88 -0
- rememberstack/model/conversion.py +31 -0
- rememberstack/model/deployment.py +53 -0
- rememberstack/model/documents.py +168 -0
- rememberstack/model/envelope.py +513 -0
- rememberstack/model/evaluation.py +72 -0
- rememberstack/model/forget.py +143 -0
- rememberstack/model/git.py +13 -0
- rememberstack/model/knowledge.py +840 -0
- rememberstack/model/knowledge_authored.py +325 -0
- rememberstack/model/knowledge_planner.py +431 -0
- rememberstack/model/lifecycle.py +42 -0
- rememberstack/model/model_provider.py +78 -0
- rememberstack/model/mounts.py +24 -0
- rememberstack/model/object_store.py +21 -0
- rememberstack/model/operational_scale.py +59 -0
- rememberstack/model/operations.py +153 -0
- rememberstack/model/processing.py +228 -0
- rememberstack/model/queue.py +73 -0
- rememberstack/model/recipes.py +83 -0
- rememberstack/model/relations.py +79 -0
- rememberstack/model/resolution.py +83 -0
- rememberstack/model/retrieval_spikes.py +62 -0
- rememberstack/model/sections.py +120 -0
- rememberstack/model/telemetry.py +30 -0
- rememberstack/ports/__init__.py +29 -0
- rememberstack/ports/auth.py +16 -0
- rememberstack/ports/connector.py +23 -0
- rememberstack/ports/cost_meter.py +17 -0
- rememberstack/ports/forget.py +20 -0
- rememberstack/ports/git.py +20 -0
- rememberstack/ports/model_provider.py +28 -0
- rememberstack/ports/mounts.py +16 -0
- rememberstack/ports/object_store.py +27 -0
- rememberstack/ports/p1_index.py +92 -0
- rememberstack/ports/purge.py +93 -0
- rememberstack/ports/queue.py +23 -0
- rememberstack/ports/telemetry.py +21 -0
- rememberstack/profiles/__init__.py +22 -0
- rememberstack/profiles/selfhost.py +324 -0
- rememberstack/profiles/selfhost_forget.py +158 -0
- rememberstack/profiles/selfhost_operations.py +95 -0
- rememberstack/py.typed +1 -0
- rememberstack/spine/__init__.py +93 -0
- rememberstack/spine/admission.py +26 -0
- rememberstack/spine/backfill.py +168 -0
- rememberstack/spine/catalog_contract.py +742 -0
- rememberstack/spine/chunk_catalog.py +237 -0
- rememberstack/spine/claim_catalog.py +298 -0
- rememberstack/spine/clustering.py +740 -0
- rememberstack/spine/component_versions.py +208 -0
- rememberstack/spine/consumption.py +81 -0
- rememberstack/spine/deployment_bootstrap.py +445 -0
- rememberstack/spine/document_catalog.py +621 -0
- rememberstack/spine/entity_registry.py +205 -0
- rememberstack/spine/extension_packs.py +220 -0
- rememberstack/spine/fact_catalog.py +571 -0
- rememberstack/spine/forget.py +1753 -0
- rememberstack/spine/knowledge.py +5467 -0
- rememberstack/spine/lifecycle.py +1071 -0
- rememberstack/spine/migrations/__init__.py +1 -0
- rememberstack/spine/migrations/_helpers.py +153 -0
- rememberstack/spine/migrations/env.py +58 -0
- rememberstack/spine/migrations/script.py.mako +27 -0
- rememberstack/spine/migrations/versions/__init__.py +1 -0
- rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
- rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
- rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
- rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
- rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
- rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
- rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
- rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
- rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
- rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
- rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
- rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
- rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
- rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
- rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
- rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
- rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
- rememberstack/spine/observation_adjudication.py +778 -0
- rememberstack/spine/operations.py +298 -0
- rememberstack/spine/projection.py +662 -0
- rememberstack/spine/recipes.py +276 -0
- rememberstack/spine/resolver.py +763 -0
- rememberstack/spine/review.py +650 -0
- rememberstack/spine/settings.py +22 -0
- rememberstack/spine/supersession.py +510 -0
- rememberstack/spine/sync.py +128 -0
- rememberstack/spine/work_ledger.py +816 -0
- rememberstack/surfaces/__init__.py +110 -0
- rememberstack/surfaces/cli.py +447 -0
- rememberstack/surfaces/consumption_skill.py +87 -0
- rememberstack/surfaces/graph_queries.py +698 -0
- rememberstack/surfaces/http_api.py +377 -0
- rememberstack/surfaces/mcp.py +67 -0
- rememberstack/surfaces/query_engine.py +1591 -0
- rememberstack/surfaces/recipe_executor.py +185 -0
- rememberstack/surfaces/recipe_surface.py +219 -0
- rememberstack/surfaces/remote_mcp.py +133 -0
- rememberstack/surfaces/sdk.py +324 -0
- rememberstack/workers/__init__.py +155 -0
- rememberstack/workers/base.py +312 -0
- rememberstack/workers/e0.py +577 -0
- rememberstack/workers/e1.py +425 -0
- rememberstack/workers/e2.py +525 -0
- rememberstack/workers/e3.py +434 -0
- rememberstack/workers/forget.py +299 -0
- rememberstack/workers/knowledge_authored.py +146 -0
- rememberstack/workers/knowledge_driver.py +735 -0
- rememberstack/workers/knowledge_fact_sheet.py +123 -0
- rememberstack/workers/knowledge_planner.py +325 -0
- rememberstack/workers/knowledge_writer.py +393 -0
- rememberstack/workers/operations.py +42 -0
- rememberstack/workers/p1.py +234 -0
- rememberstack/workers/p2.py +513 -0
- rememberstack/workers/p2_analytics.py +276 -0
- rememberstack/workers/p3.py +673 -0
- rememberstack/workers/reconcile.py +485 -0
- rememberstack/workers/sync.py +168 -0
- rememberstack-0.1.0.dist-info/METADATA +213 -0
- rememberstack-0.1.0.dist-info/RECORD +186 -0
- rememberstack-0.1.0.dist-info/WHEEL +4 -0
- rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
- rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
"""The full ER cascade (D17/D20): T0 exact → T1/T2 blocking → T3 → T4 → mint.
|
|
2
|
+
|
|
3
|
+
Block-loose / decide-tight: T1 (trigram) and T2 (Daitch-Mokotoff phonetic)
|
|
4
|
+
only GENERATE candidates; decisions are T0 (exact), T3 (embedding band), and
|
|
5
|
+
T4 (LLM adjudication, small → frontier escalation). A near-miss is escalated,
|
|
6
|
+
never auto-rejected. Every verdict lands append-only in
|
|
7
|
+
`resolution_decisions` with its tier, scores, and the resolver version whose
|
|
8
|
+
thresholds were in force. Registry-self-contained: no external authority
|
|
9
|
+
tier (D20).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Final
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from sqlalchemy import bindparam
|
|
17
|
+
from sqlalchemy import JSON
|
|
18
|
+
from sqlalchemy import text
|
|
19
|
+
from sqlalchemy.engine import Connection
|
|
20
|
+
from sqlalchemy.engine import Engine
|
|
21
|
+
|
|
22
|
+
from rememberstack.model import AdjudicationVerdict
|
|
23
|
+
from rememberstack.model import ClaimForNormalization
|
|
24
|
+
from rememberstack.model import EmbeddingRequest
|
|
25
|
+
from rememberstack.model import EntityRef
|
|
26
|
+
from rememberstack.model import ModelRequest
|
|
27
|
+
from rememberstack.model import P1EntityRow
|
|
28
|
+
from rememberstack.model import ResolutionCandidate
|
|
29
|
+
from rememberstack.model import ResolvedEntity
|
|
30
|
+
from rememberstack.model import ResolverConfig
|
|
31
|
+
from rememberstack.ports.cost_meter import CostMeterPort
|
|
32
|
+
from rememberstack.ports.model_provider import ModelProviderPort
|
|
33
|
+
from rememberstack.ports.p1_index import EntityIndexPort
|
|
34
|
+
from rememberstack.spine.entity_registry import normalized_lemma
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ResolverVersionConflictError(Exception):
|
|
38
|
+
"""A resolver version re-registered with a different definition (D22)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
RESOLVER_VERSION: Final = "resolver-2026.07a"
|
|
42
|
+
"""The cascade generation whose thresholds stamp every decision (D17/D22)."""
|
|
43
|
+
|
|
44
|
+
_T4_PROMPT: Final = """You adjudicate entity identity for a memory system.
|
|
45
|
+
Are these the same real-world entity? Answer strictly from the evidence given.
|
|
46
|
+
|
|
47
|
+
MENTION: {mention!r} (emitted type {mention_type})
|
|
48
|
+
CLAIM CONTEXT: {context}
|
|
49
|
+
|
|
50
|
+
CANDIDATE: {candidate!r} (registry type {candidate_type})
|
|
51
|
+
|
|
52
|
+
Same entity?"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CascadeResolver:
|
|
56
|
+
"""T0-T4 resolution over one deployment's registry, minting on no-match."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
engine: Engine,
|
|
62
|
+
entity_index: EntityIndexPort,
|
|
63
|
+
model_provider: ModelProviderPort,
|
|
64
|
+
config: ResolverConfig,
|
|
65
|
+
embedding_model: str,
|
|
66
|
+
small_model: str,
|
|
67
|
+
frontier_model: str,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Bind the cascade to the registry, the T3 index, and the T4 models.
|
|
70
|
+
|
|
71
|
+
Model seats follow the port-default principle (D70's pattern): the
|
|
72
|
+
adjudicator ladder is deployment configuration, measured per phase.
|
|
73
|
+
"""
|
|
74
|
+
self._engine = engine
|
|
75
|
+
self._entity_index = entity_index
|
|
76
|
+
self._model_provider = model_provider
|
|
77
|
+
self._config = config
|
|
78
|
+
self._embedding_model = embedding_model
|
|
79
|
+
self._small_model = small_model
|
|
80
|
+
self._frontier_model = frontier_model
|
|
81
|
+
self._registered = False
|
|
82
|
+
self._last_rejection: tuple[str, float, dict[str, object]] | None = None
|
|
83
|
+
|
|
84
|
+
def resolve(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
deployment_id: UUID,
|
|
88
|
+
reference: EntityRef,
|
|
89
|
+
claim: ClaimForNormalization,
|
|
90
|
+
meter: CostMeterPort | None = None,
|
|
91
|
+
call_key: str = "resolve",
|
|
92
|
+
) -> ResolvedEntity:
|
|
93
|
+
"""Run the cascade for one reference; mint when nothing matches.
|
|
94
|
+
|
|
95
|
+
Stops at the first confident decision. The mention and the
|
|
96
|
+
append-only verdict (tier, scores, config version) are written in the
|
|
97
|
+
same transaction as any mint. Cross-SURFACE duplicate mints (two
|
|
98
|
+
distinct variants racing on an empty registry) are deliberately not
|
|
99
|
+
serialized here — that is the clustering/merge machinery's job
|
|
100
|
+
(registries §6, WP-2.2); the lemma lock prevents the same-lemma race.
|
|
101
|
+
"""
|
|
102
|
+
self._ensure_registered(deployment_id=deployment_id)
|
|
103
|
+
lemma = normalized_lemma(surface=reference.name)
|
|
104
|
+
with self._engine.begin() as connection:
|
|
105
|
+
connection.execute(_LOCK_LEMMA, {"key": f"{deployment_id}:lemma:{lemma}"})
|
|
106
|
+
exact = (
|
|
107
|
+
connection.execute(
|
|
108
|
+
_T0_EXACT, {"deployment_id": deployment_id, "lemma": lemma}
|
|
109
|
+
)
|
|
110
|
+
.mappings()
|
|
111
|
+
.one_or_none()
|
|
112
|
+
)
|
|
113
|
+
if exact is not None:
|
|
114
|
+
return self._record(
|
|
115
|
+
connection=connection,
|
|
116
|
+
deployment_id=deployment_id,
|
|
117
|
+
reference=reference,
|
|
118
|
+
claim=claim,
|
|
119
|
+
lemma=lemma,
|
|
120
|
+
entity_id=exact["entity_id"],
|
|
121
|
+
entity_type=exact["type"],
|
|
122
|
+
method="T0",
|
|
123
|
+
confidence=1.0,
|
|
124
|
+
features={"lemma": lemma},
|
|
125
|
+
created=False,
|
|
126
|
+
)
|
|
127
|
+
candidates = self._blocked_candidates(
|
|
128
|
+
connection=connection, deployment_id=deployment_id, lemma=lemma
|
|
129
|
+
)
|
|
130
|
+
decision = self._decide(
|
|
131
|
+
deployment_id=deployment_id,
|
|
132
|
+
reference=reference,
|
|
133
|
+
claim=claim,
|
|
134
|
+
candidates=candidates,
|
|
135
|
+
meter=meter,
|
|
136
|
+
call_key=call_key,
|
|
137
|
+
)
|
|
138
|
+
if decision is not None:
|
|
139
|
+
candidate, method, confidence, features = decision
|
|
140
|
+
return self._record(
|
|
141
|
+
connection=connection,
|
|
142
|
+
deployment_id=deployment_id,
|
|
143
|
+
reference=reference,
|
|
144
|
+
claim=claim,
|
|
145
|
+
lemma=lemma,
|
|
146
|
+
entity_id=candidate.entity_id,
|
|
147
|
+
entity_type=candidate.type,
|
|
148
|
+
method=method,
|
|
149
|
+
confidence=confidence,
|
|
150
|
+
features=features,
|
|
151
|
+
created=False,
|
|
152
|
+
)
|
|
153
|
+
return self._mint(
|
|
154
|
+
connection=connection,
|
|
155
|
+
deployment_id=deployment_id,
|
|
156
|
+
reference=reference,
|
|
157
|
+
claim=claim,
|
|
158
|
+
lemma=lemma,
|
|
159
|
+
considered=candidates,
|
|
160
|
+
meter=meter,
|
|
161
|
+
call_key=call_key,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _ensure_registered(self, *, deployment_id: UUID) -> None:
|
|
165
|
+
"""Verify the in-force config IS the registered resolver version.
|
|
166
|
+
|
|
167
|
+
Registers on first use; a version whose stored definition differs
|
|
168
|
+
from this config is a hard error — thresholds are immutable per
|
|
169
|
+
version (D22): change the numbers, mint a new version string.
|
|
170
|
+
"""
|
|
171
|
+
if self._registered:
|
|
172
|
+
return
|
|
173
|
+
seed_resolver_version(
|
|
174
|
+
engine=self._engine, deployment_id=deployment_id, config=self._config
|
|
175
|
+
)
|
|
176
|
+
self._registered = True
|
|
177
|
+
|
|
178
|
+
def judge_pair(
|
|
179
|
+
self,
|
|
180
|
+
*,
|
|
181
|
+
surface_a: str,
|
|
182
|
+
surface_b: str,
|
|
183
|
+
entity_type: str,
|
|
184
|
+
context_a: str | None,
|
|
185
|
+
context_b: str | None,
|
|
186
|
+
) -> tuple[bool, str]:
|
|
187
|
+
"""The cascade's decision function over one golden pair (D22).
|
|
188
|
+
|
|
189
|
+
Registry-free: measures whether the tiers would identify the two
|
|
190
|
+
surfaces — lemma equality (T0), blocking reachability (T1/T2; a pair
|
|
191
|
+
blocking cannot reach is a no_match by the recall ceiling), the T3
|
|
192
|
+
band over the two surface embeddings, then T4 with both contexts.
|
|
193
|
+
Returns (match, deciding_tier).
|
|
194
|
+
"""
|
|
195
|
+
lemma_a = normalized_lemma(surface=surface_a)
|
|
196
|
+
lemma_b = normalized_lemma(surface=surface_b)
|
|
197
|
+
if lemma_a == lemma_b:
|
|
198
|
+
return True, "T0"
|
|
199
|
+
with self._engine.connect() as connection:
|
|
200
|
+
reachable = connection.execute(
|
|
201
|
+
_PAIR_REACHABLE,
|
|
202
|
+
{"a": lemma_a, "b": lemma_b, "floor": self._config.trigram_floor},
|
|
203
|
+
).scalar_one()
|
|
204
|
+
if not reachable:
|
|
205
|
+
return False, "blocking"
|
|
206
|
+
thresholds = self._config.thresholds_for(entity_type=entity_type)
|
|
207
|
+
vectors = self._model_provider.embed(
|
|
208
|
+
request=EmbeddingRequest(
|
|
209
|
+
model=self._embedding_model, texts=(surface_a, surface_b)
|
|
210
|
+
)
|
|
211
|
+
).vectors
|
|
212
|
+
score = _cosine(vectors[0], vectors[1])
|
|
213
|
+
if score >= thresholds.t3_accept:
|
|
214
|
+
return True, "T3"
|
|
215
|
+
if score <= thresholds.t3_reject:
|
|
216
|
+
return False, "T3"
|
|
217
|
+
prompt = _T4_PROMPT.format(
|
|
218
|
+
mention=surface_b,
|
|
219
|
+
mention_type=entity_type,
|
|
220
|
+
context=context_b or "(none)",
|
|
221
|
+
candidate=surface_a,
|
|
222
|
+
candidate_type=entity_type,
|
|
223
|
+
)
|
|
224
|
+
if context_a:
|
|
225
|
+
prompt += f"\nCANDIDATE CONTEXT: {context_a}"
|
|
226
|
+
verdict = self._model_provider.generate(
|
|
227
|
+
request=ModelRequest(model=self._small_model, prompt=prompt),
|
|
228
|
+
response_type=AdjudicationVerdict,
|
|
229
|
+
)
|
|
230
|
+
if verdict.output.confidence >= thresholds.t4_small_confidence_floor:
|
|
231
|
+
return verdict.output.match, "T4_small"
|
|
232
|
+
frontier = self._model_provider.generate(
|
|
233
|
+
request=ModelRequest(model=self._frontier_model, prompt=prompt),
|
|
234
|
+
response_type=AdjudicationVerdict,
|
|
235
|
+
)
|
|
236
|
+
return frontier.output.match, "T4_frontier"
|
|
237
|
+
|
|
238
|
+
def _blocked_candidates(
|
|
239
|
+
self, *, connection: Connection, deployment_id: UUID, lemma: str
|
|
240
|
+
) -> tuple[ResolutionCandidate, ...]:
|
|
241
|
+
"""T1 trigram + T2 phonetic candidate generation (never a decision)."""
|
|
242
|
+
rows = (
|
|
243
|
+
connection.execute(
|
|
244
|
+
_T1_T2_BLOCK,
|
|
245
|
+
{
|
|
246
|
+
"deployment_id": deployment_id,
|
|
247
|
+
"lemma": lemma,
|
|
248
|
+
"floor": self._config.trigram_floor,
|
|
249
|
+
"limit": self._config.blocking_limit,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
.mappings()
|
|
253
|
+
.all()
|
|
254
|
+
)
|
|
255
|
+
return tuple(
|
|
256
|
+
ResolutionCandidate(
|
|
257
|
+
entity_id=row["entity_id"],
|
|
258
|
+
canonical_name=row["canonical_name"],
|
|
259
|
+
type=row["type"],
|
|
260
|
+
blocking_tier=row["blocking_tier"],
|
|
261
|
+
trigram_score=row["trigram_score"],
|
|
262
|
+
)
|
|
263
|
+
for row in rows
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _decide(
|
|
267
|
+
self,
|
|
268
|
+
*,
|
|
269
|
+
deployment_id: UUID,
|
|
270
|
+
reference: EntityRef,
|
|
271
|
+
claim: ClaimForNormalization,
|
|
272
|
+
candidates: tuple[ResolutionCandidate, ...],
|
|
273
|
+
meter: CostMeterPort | None,
|
|
274
|
+
call_key: str,
|
|
275
|
+
) -> tuple[ResolutionCandidate, str, float, dict[str, object]] | None:
|
|
276
|
+
"""T3 embedding bands, then T4 adjudication for the ambiguous band."""
|
|
277
|
+
if not candidates:
|
|
278
|
+
return None
|
|
279
|
+
thresholds = self._config.thresholds_for(entity_type=reference.type)
|
|
280
|
+
scored = self._t3_scores(
|
|
281
|
+
deployment_id=deployment_id,
|
|
282
|
+
reference=reference,
|
|
283
|
+
candidates=candidates,
|
|
284
|
+
meter=meter,
|
|
285
|
+
call_key=f"{call_key}:t3",
|
|
286
|
+
)
|
|
287
|
+
ordered = sorted(
|
|
288
|
+
scored,
|
|
289
|
+
key=lambda item: item[1] if item[1] is not None else 0.0,
|
|
290
|
+
reverse=True,
|
|
291
|
+
)
|
|
292
|
+
adjudicated = 0
|
|
293
|
+
for candidate, score in ordered:
|
|
294
|
+
if score is not None and score >= thresholds.t3_accept:
|
|
295
|
+
return (
|
|
296
|
+
candidate,
|
|
297
|
+
"T3",
|
|
298
|
+
score,
|
|
299
|
+
{
|
|
300
|
+
"blocking_tier": candidate.blocking_tier,
|
|
301
|
+
"embedding_score": score,
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
if score is not None and score <= thresholds.t3_reject:
|
|
305
|
+
self._last_rejection = ("T3", score, {"embedding_score": score})
|
|
306
|
+
continue # confidently not THIS candidate; others get a look
|
|
307
|
+
# ambiguous band — or no stored profile vector, which must
|
|
308
|
+
# ESCALATE, never count as a confident non-match (Codex review):
|
|
309
|
+
if adjudicated >= self._config.t4_max_candidates:
|
|
310
|
+
break
|
|
311
|
+
adjudicated += 1
|
|
312
|
+
verdict, seat, model = self._t4(
|
|
313
|
+
reference=reference,
|
|
314
|
+
claim=claim,
|
|
315
|
+
candidate=candidate,
|
|
316
|
+
meter=meter,
|
|
317
|
+
call_key=f"{call_key}:t4:{candidate.entity_id}",
|
|
318
|
+
)
|
|
319
|
+
if verdict.match:
|
|
320
|
+
return (
|
|
321
|
+
candidate,
|
|
322
|
+
seat,
|
|
323
|
+
verdict.confidence,
|
|
324
|
+
{
|
|
325
|
+
"blocking_tier": candidate.blocking_tier,
|
|
326
|
+
"embedding_score": score,
|
|
327
|
+
"model": model,
|
|
328
|
+
"rationale": verdict.rationale,
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
self._last_rejection = (
|
|
332
|
+
seat,
|
|
333
|
+
verdict.confidence,
|
|
334
|
+
{"model": model, "rationale": verdict.rationale},
|
|
335
|
+
)
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def _t3_scores(
|
|
339
|
+
self,
|
|
340
|
+
*,
|
|
341
|
+
deployment_id: UUID,
|
|
342
|
+
reference: EntityRef,
|
|
343
|
+
candidates: tuple[ResolutionCandidate, ...],
|
|
344
|
+
meter: CostMeterPort | None,
|
|
345
|
+
call_key: str,
|
|
346
|
+
) -> tuple[tuple[ResolutionCandidate, float | None], ...]:
|
|
347
|
+
"""Cosine similarity against candidate profiles; None = no profile.
|
|
348
|
+
|
|
349
|
+
A missing/stale profile vector is AMBIGUITY (route to T4), never a
|
|
350
|
+
confident non-match (Codex review).
|
|
351
|
+
"""
|
|
352
|
+
query_vector = self._embed(
|
|
353
|
+
surface=reference.name, meter=meter, call_key=call_key
|
|
354
|
+
)
|
|
355
|
+
by_id = self._entity_index.entity_vectors(
|
|
356
|
+
deployment_id=str(deployment_id),
|
|
357
|
+
entity_ids=tuple(str(candidate.entity_id) for candidate in candidates),
|
|
358
|
+
)
|
|
359
|
+
return tuple(
|
|
360
|
+
(
|
|
361
|
+
candidate,
|
|
362
|
+
None
|
|
363
|
+
if by_id.get(str(candidate.entity_id)) is None
|
|
364
|
+
else _cosine(query_vector, by_id[str(candidate.entity_id)]),
|
|
365
|
+
)
|
|
366
|
+
for candidate in candidates
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def _t4(
|
|
370
|
+
self,
|
|
371
|
+
*,
|
|
372
|
+
reference: EntityRef,
|
|
373
|
+
claim: ClaimForNormalization,
|
|
374
|
+
candidate: ResolutionCandidate,
|
|
375
|
+
meter: CostMeterPort | None,
|
|
376
|
+
call_key: str,
|
|
377
|
+
) -> tuple[AdjudicationVerdict, str, str]:
|
|
378
|
+
"""T4 small-model adjudication, escalating to frontier below the floor."""
|
|
379
|
+
prompt = _T4_PROMPT.format(
|
|
380
|
+
mention=reference.name,
|
|
381
|
+
mention_type=reference.type,
|
|
382
|
+
context=claim.claim_text,
|
|
383
|
+
candidate=candidate.canonical_name,
|
|
384
|
+
candidate_type=candidate.type,
|
|
385
|
+
)
|
|
386
|
+
verdict_call = self._model_provider.generate(
|
|
387
|
+
request=ModelRequest(model=self._small_model, prompt=prompt),
|
|
388
|
+
response_type=AdjudicationVerdict,
|
|
389
|
+
)
|
|
390
|
+
if meter is not None:
|
|
391
|
+
meter.record(
|
|
392
|
+
call_key=f"{call_key}:small", tier="T4_small", usage=verdict_call.usage
|
|
393
|
+
)
|
|
394
|
+
verdict = verdict_call.output
|
|
395
|
+
thresholds = self._config.thresholds_for(entity_type=reference.type)
|
|
396
|
+
if verdict.confidence >= thresholds.t4_small_confidence_floor:
|
|
397
|
+
return verdict, "T4_small", self._small_model
|
|
398
|
+
frontier_call = self._model_provider.generate(
|
|
399
|
+
request=ModelRequest(model=self._frontier_model, prompt=prompt),
|
|
400
|
+
response_type=AdjudicationVerdict,
|
|
401
|
+
)
|
|
402
|
+
if meter is not None:
|
|
403
|
+
meter.record(
|
|
404
|
+
call_key=f"{call_key}:frontier",
|
|
405
|
+
tier="T4_frontier",
|
|
406
|
+
usage=frontier_call.usage,
|
|
407
|
+
)
|
|
408
|
+
return frontier_call.output, "T4_frontier", self._frontier_model
|
|
409
|
+
|
|
410
|
+
def _mint(
|
|
411
|
+
self,
|
|
412
|
+
*,
|
|
413
|
+
connection: Connection,
|
|
414
|
+
deployment_id: UUID,
|
|
415
|
+
reference: EntityRef,
|
|
416
|
+
claim: ClaimForNormalization,
|
|
417
|
+
lemma: str,
|
|
418
|
+
considered: tuple[ResolutionCandidate, ...],
|
|
419
|
+
meter: CostMeterPort | None,
|
|
420
|
+
call_key: str,
|
|
421
|
+
) -> ResolvedEntity:
|
|
422
|
+
"""Create the canonical entity + alias and index its T3 profile."""
|
|
423
|
+
entity_id = uuid4()
|
|
424
|
+
# the mint verdict records the tier that DECIDED novelty: T0 when
|
|
425
|
+
# nothing blocked, else the rejecting tier's method and confidence
|
|
426
|
+
# (Codex review — the audit trail keeps the actual path):
|
|
427
|
+
rejection = self._last_rejection if considered else None
|
|
428
|
+
self._last_rejection = None
|
|
429
|
+
method, confidence, extra = rejection or ("T0", 1.0, {})
|
|
430
|
+
connection.execute(
|
|
431
|
+
_INSERT_ENTITY,
|
|
432
|
+
{
|
|
433
|
+
"entity_id": entity_id,
|
|
434
|
+
"deployment_id": deployment_id,
|
|
435
|
+
"type": reference.type,
|
|
436
|
+
"canonical_name": reference.name,
|
|
437
|
+
"normalized_name": lemma,
|
|
438
|
+
},
|
|
439
|
+
)
|
|
440
|
+
connection.execute(
|
|
441
|
+
_INSERT_ALIAS,
|
|
442
|
+
{
|
|
443
|
+
"alias_id": uuid4(),
|
|
444
|
+
"deployment_id": deployment_id,
|
|
445
|
+
"entity_id": entity_id,
|
|
446
|
+
"alias_text": reference.name,
|
|
447
|
+
"lemma": lemma,
|
|
448
|
+
},
|
|
449
|
+
)
|
|
450
|
+
self._entity_index.upsert_entities(
|
|
451
|
+
rows=(
|
|
452
|
+
P1EntityRow(
|
|
453
|
+
entity_id=entity_id,
|
|
454
|
+
deployment_id=deployment_id,
|
|
455
|
+
type=reference.type,
|
|
456
|
+
canonical_name=reference.name,
|
|
457
|
+
vector=self._embed(
|
|
458
|
+
surface=reference.name, meter=meter, call_key=f"{call_key}:mint"
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
connection.execute(
|
|
464
|
+
_STAMP_PROFILE_REF, {"entity_id": entity_id, "ref": str(entity_id)}
|
|
465
|
+
)
|
|
466
|
+
return self._record(
|
|
467
|
+
connection=connection,
|
|
468
|
+
deployment_id=deployment_id,
|
|
469
|
+
reference=reference,
|
|
470
|
+
claim=claim,
|
|
471
|
+
lemma=lemma,
|
|
472
|
+
entity_id=entity_id,
|
|
473
|
+
entity_type=reference.type,
|
|
474
|
+
method=method,
|
|
475
|
+
confidence=confidence,
|
|
476
|
+
features={
|
|
477
|
+
"lemma": lemma,
|
|
478
|
+
"novelty": True,
|
|
479
|
+
"considered": [str(c.entity_id) for c in considered],
|
|
480
|
+
**extra,
|
|
481
|
+
},
|
|
482
|
+
created=True,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def _record(
|
|
486
|
+
self,
|
|
487
|
+
*,
|
|
488
|
+
connection: Connection,
|
|
489
|
+
deployment_id: UUID,
|
|
490
|
+
reference: EntityRef,
|
|
491
|
+
claim: ClaimForNormalization,
|
|
492
|
+
lemma: str,
|
|
493
|
+
entity_id: UUID,
|
|
494
|
+
entity_type: str,
|
|
495
|
+
method: str,
|
|
496
|
+
confidence: float,
|
|
497
|
+
features: dict[str, object],
|
|
498
|
+
created: bool,
|
|
499
|
+
) -> ResolvedEntity:
|
|
500
|
+
"""Write the mention + append-only verdict; return the resolution."""
|
|
501
|
+
mention_id = uuid4()
|
|
502
|
+
connection.execute(
|
|
503
|
+
_INSERT_MENTION,
|
|
504
|
+
{
|
|
505
|
+
"mention_id": mention_id,
|
|
506
|
+
"deployment_id": deployment_id,
|
|
507
|
+
"surface_form": reference.name,
|
|
508
|
+
"lemma": lemma,
|
|
509
|
+
"canonical_name_form": reference.name,
|
|
510
|
+
"emitted_type": reference.type,
|
|
511
|
+
"claim_id": claim.claim_id,
|
|
512
|
+
"chunk_id": claim.chunk_id,
|
|
513
|
+
"doc_id": claim.doc_id,
|
|
514
|
+
},
|
|
515
|
+
)
|
|
516
|
+
connection.execute(
|
|
517
|
+
_INSERT_DECISION,
|
|
518
|
+
{
|
|
519
|
+
"decision_id": uuid4(),
|
|
520
|
+
"deployment_id": deployment_id,
|
|
521
|
+
"mention_id": mention_id,
|
|
522
|
+
"entity_id": entity_id,
|
|
523
|
+
"method": method,
|
|
524
|
+
"confidence": confidence,
|
|
525
|
+
"is_new_entity": created,
|
|
526
|
+
"features": features,
|
|
527
|
+
"resolver_version": self._config.resolver_version,
|
|
528
|
+
},
|
|
529
|
+
)
|
|
530
|
+
connection.execute( # keep the blast-radius input warm (registries §6)
|
|
531
|
+
_BUMP_MENTION_COUNT,
|
|
532
|
+
{"deployment_id": deployment_id, "entity_id": entity_id},
|
|
533
|
+
)
|
|
534
|
+
return ResolvedEntity(
|
|
535
|
+
entity_id=entity_id, created=created, entity_type=entity_type
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def _embed(
|
|
539
|
+
self, *, surface: str, meter: CostMeterPort | None, call_key: str
|
|
540
|
+
) -> tuple[float, ...]:
|
|
541
|
+
"""One profile/query embedding through the configured port (D63)."""
|
|
542
|
+
response = self._model_provider.embed(
|
|
543
|
+
request=EmbeddingRequest(model=self._embedding_model, texts=(surface,))
|
|
544
|
+
)
|
|
545
|
+
if meter is not None:
|
|
546
|
+
meter.record(call_key=call_key, tier="T3", usage=response.usage)
|
|
547
|
+
return response.vectors[0]
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def seed_resolver_version(
|
|
551
|
+
*, engine: Engine, deployment_id: UUID, config: ResolverConfig
|
|
552
|
+
) -> None:
|
|
553
|
+
"""Register the cascade configuration once (immutable per version, D22).
|
|
554
|
+
|
|
555
|
+
Re-seeding an identical definition is a no-op; a DIFFERENT definition
|
|
556
|
+
under the same version string is a hard error — change the numbers, mint
|
|
557
|
+
a new version. Thresholds are starting points until curves exist.
|
|
558
|
+
"""
|
|
559
|
+
definition = _config_definition(config=config)
|
|
560
|
+
stored = _stored_config(
|
|
561
|
+
engine=engine,
|
|
562
|
+
deployment_id=deployment_id,
|
|
563
|
+
resolver_version=config.resolver_version,
|
|
564
|
+
)
|
|
565
|
+
if stored is not None:
|
|
566
|
+
if stored != definition:
|
|
567
|
+
raise ResolverVersionConflictError(
|
|
568
|
+
f"resolver version {config.resolver_version!r} already registered "
|
|
569
|
+
"with a different definition; mint a new version string"
|
|
570
|
+
)
|
|
571
|
+
return
|
|
572
|
+
with engine.begin() as connection:
|
|
573
|
+
connection.execute(
|
|
574
|
+
_SEED_RESOLVER_VERSION,
|
|
575
|
+
{
|
|
576
|
+
"deployment_id": deployment_id,
|
|
577
|
+
"resolver_version": config.resolver_version,
|
|
578
|
+
**definition,
|
|
579
|
+
},
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _stored_config(
|
|
584
|
+
*, engine: Engine, deployment_id: UUID, resolver_version: str
|
|
585
|
+
) -> dict[str, object] | None:
|
|
586
|
+
"""The registered definition for a version, or None if unregistered."""
|
|
587
|
+
with engine.connect() as connection:
|
|
588
|
+
row = (
|
|
589
|
+
connection.execute(
|
|
590
|
+
_SELECT_RESOLVER_VERSION,
|
|
591
|
+
{"deployment_id": deployment_id, "resolver_version": resolver_version},
|
|
592
|
+
)
|
|
593
|
+
.mappings()
|
|
594
|
+
.one_or_none()
|
|
595
|
+
)
|
|
596
|
+
if row is None:
|
|
597
|
+
return None
|
|
598
|
+
return {
|
|
599
|
+
"tier_config": row["tier_config"],
|
|
600
|
+
"thresholds_by_type": row["thresholds_by_type"],
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _config_definition(*, config: ResolverConfig) -> dict[str, object]:
|
|
605
|
+
"""The comparable stored form of one in-memory config."""
|
|
606
|
+
return {
|
|
607
|
+
"tier_config": {
|
|
608
|
+
"order": ["T0", "T1", "T2", "T3", "T4_small", "T4_frontier"],
|
|
609
|
+
"trigram_floor": config.trigram_floor,
|
|
610
|
+
"blocking_limit": config.blocking_limit,
|
|
611
|
+
"t4_max_candidates": config.t4_max_candidates,
|
|
612
|
+
},
|
|
613
|
+
"thresholds_by_type": {
|
|
614
|
+
"default": config.default_thresholds.model_dump(),
|
|
615
|
+
**{
|
|
616
|
+
entity_type: thresholds.model_dump()
|
|
617
|
+
for entity_type, thresholds in config.thresholds_by_type.items()
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _cosine(a: tuple[float, ...], b: tuple[float, ...] | None) -> float:
|
|
624
|
+
"""Cosine similarity; a candidate without a profile vector scores 0."""
|
|
625
|
+
if b is None or len(a) != len(b):
|
|
626
|
+
return 0.0
|
|
627
|
+
dot = sum(x * y for x, y in zip(a, b, strict=True))
|
|
628
|
+
norm_a = sum(x * x for x in a) ** 0.5
|
|
629
|
+
norm_b = sum(y * y for y in b) ** 0.5
|
|
630
|
+
if norm_a == 0.0 or norm_b == 0.0:
|
|
631
|
+
return 0.0
|
|
632
|
+
return dot / (norm_a * norm_b)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
_LOCK_LEMMA = text("SELECT pg_advisory_xact_lock(hashtextextended(:key, 0))")
|
|
636
|
+
|
|
637
|
+
_PAIR_REACHABLE = text(
|
|
638
|
+
"""
|
|
639
|
+
SELECT similarity(:a, :b) >= :floor
|
|
640
|
+
OR daitch_mokotoff(:a) && daitch_mokotoff(:b)
|
|
641
|
+
"""
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
_T0_EXACT = text(
|
|
645
|
+
"""
|
|
646
|
+
SELECT aliases.entity_id, entities.type FROM aliases
|
|
647
|
+
JOIN entities ON entities.deployment_id = aliases.deployment_id
|
|
648
|
+
AND entities.entity_id = aliases.entity_id
|
|
649
|
+
WHERE aliases.deployment_id = :deployment_id
|
|
650
|
+
AND aliases.normalized_lemma = :lemma
|
|
651
|
+
AND entities.status = 'active'
|
|
652
|
+
ORDER BY aliases.first_seen
|
|
653
|
+
LIMIT 1
|
|
654
|
+
"""
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
_T1_T2_BLOCK = text(
|
|
658
|
+
"""
|
|
659
|
+
WITH t1 AS (
|
|
660
|
+
SELECT DISTINCT ON (aliases.entity_id)
|
|
661
|
+
aliases.entity_id, similarity(aliases.normalized_lemma, :lemma) AS score
|
|
662
|
+
FROM aliases
|
|
663
|
+
WHERE aliases.deployment_id = :deployment_id
|
|
664
|
+
AND similarity(aliases.normalized_lemma, :lemma) >= :floor
|
|
665
|
+
ORDER BY aliases.entity_id, score DESC
|
|
666
|
+
),
|
|
667
|
+
t2 AS (
|
|
668
|
+
SELECT DISTINCT aliases.entity_id
|
|
669
|
+
FROM aliases
|
|
670
|
+
WHERE aliases.deployment_id = :deployment_id
|
|
671
|
+
AND daitch_mokotoff(aliases.normalized_lemma)
|
|
672
|
+
&& daitch_mokotoff(:lemma)
|
|
673
|
+
)
|
|
674
|
+
SELECT entities.entity_id, entities.canonical_name, entities.type,
|
|
675
|
+
coalesce(t1.score, 0.0) AS trigram_score,
|
|
676
|
+
CASE WHEN t1.entity_id IS NOT NULL THEN 'T1' ELSE 'T2' END
|
|
677
|
+
AS blocking_tier
|
|
678
|
+
FROM entities
|
|
679
|
+
LEFT JOIN t1 ON t1.entity_id = entities.entity_id
|
|
680
|
+
LEFT JOIN t2 ON t2.entity_id = entities.entity_id
|
|
681
|
+
WHERE entities.deployment_id = :deployment_id
|
|
682
|
+
AND entities.status = 'active'
|
|
683
|
+
AND (t1.entity_id IS NOT NULL OR t2.entity_id IS NOT NULL)
|
|
684
|
+
ORDER BY coalesce(t1.score, 0.0) DESC
|
|
685
|
+
LIMIT :limit
|
|
686
|
+
"""
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
_INSERT_ENTITY = text(
|
|
690
|
+
"""
|
|
691
|
+
INSERT INTO entities (
|
|
692
|
+
entity_id, deployment_id, type, canonical_name, normalized_name
|
|
693
|
+
) VALUES (
|
|
694
|
+
:entity_id, :deployment_id, :type, :canonical_name, :normalized_name
|
|
695
|
+
)
|
|
696
|
+
"""
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
_INSERT_ALIAS = text(
|
|
700
|
+
"""
|
|
701
|
+
INSERT INTO aliases (
|
|
702
|
+
alias_id, deployment_id, entity_id, alias_text, normalized_lemma, provenance
|
|
703
|
+
) VALUES (
|
|
704
|
+
:alias_id, :deployment_id, :entity_id, :alias_text, :lemma, 'llm_canonical'
|
|
705
|
+
)
|
|
706
|
+
"""
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
_STAMP_PROFILE_REF = text(
|
|
710
|
+
"UPDATE entities SET profile_embedding_ref = :ref WHERE entity_id = :entity_id"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
_INSERT_MENTION = text(
|
|
714
|
+
"""
|
|
715
|
+
INSERT INTO mentions (
|
|
716
|
+
mention_id, deployment_id, surface_form, normalized_lemma,
|
|
717
|
+
canonical_name_form, emitted_type, claim_id, chunk_id, doc_id
|
|
718
|
+
) VALUES (
|
|
719
|
+
:mention_id, :deployment_id, :surface_form, :lemma,
|
|
720
|
+
:canonical_name_form, :emitted_type, :claim_id, :chunk_id, :doc_id
|
|
721
|
+
)
|
|
722
|
+
"""
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
_INSERT_DECISION = text(
|
|
726
|
+
"""
|
|
727
|
+
INSERT INTO resolution_decisions (
|
|
728
|
+
decision_id, deployment_id, mention_id, entity_id, method,
|
|
729
|
+
confidence, is_new_entity, features, resolver_version
|
|
730
|
+
) VALUES (
|
|
731
|
+
:decision_id, :deployment_id, :mention_id, :entity_id, :method,
|
|
732
|
+
:confidence, :is_new_entity, :features, :resolver_version
|
|
733
|
+
)
|
|
734
|
+
"""
|
|
735
|
+
).bindparams(bindparam("features", type_=JSON))
|
|
736
|
+
|
|
737
|
+
_SEED_RESOLVER_VERSION = text(
|
|
738
|
+
"""
|
|
739
|
+
INSERT INTO resolver_versions (
|
|
740
|
+
deployment_id, resolver_version, tier_config, thresholds_by_type
|
|
741
|
+
) VALUES (
|
|
742
|
+
:deployment_id, :resolver_version, :tier_config, :thresholds_by_type
|
|
743
|
+
)
|
|
744
|
+
ON CONFLICT (deployment_id, resolver_version) DO NOTHING
|
|
745
|
+
"""
|
|
746
|
+
).bindparams(
|
|
747
|
+
bindparam("tier_config", type_=JSON), bindparam("thresholds_by_type", type_=JSON)
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
_SELECT_RESOLVER_VERSION = text(
|
|
751
|
+
"""
|
|
752
|
+
SELECT tier_config, thresholds_by_type FROM resolver_versions
|
|
753
|
+
WHERE deployment_id = :deployment_id
|
|
754
|
+
AND resolver_version = :resolver_version
|
|
755
|
+
"""
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
_BUMP_MENTION_COUNT = text(
|
|
759
|
+
"""
|
|
760
|
+
UPDATE entities SET mention_count = mention_count + 1, updated_at = now()
|
|
761
|
+
WHERE deployment_id = :deployment_id AND entity_id = :entity_id
|
|
762
|
+
"""
|
|
763
|
+
)
|