metaspn-entities 0.1.6__py3-none-any.whl → 0.1.7__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.
- metaspn_entities/__init__.py +2 -0
- metaspn_entities/attribution.py +88 -0
- metaspn_entities/resolver.py +29 -0
- metaspn_entities/sqlite_backend.py +6 -0
- {metaspn_entities-0.1.6.dist-info → metaspn_entities-0.1.7.dist-info}/METADATA +14 -1
- metaspn_entities-0.1.7.dist-info/RECORD +14 -0
- metaspn_entities-0.1.6.dist-info/RECORD +0 -13
- {metaspn_entities-0.1.6.dist-info → metaspn_entities-0.1.7.dist-info}/WHEEL +0 -0
- {metaspn_entities-0.1.6.dist-info → metaspn_entities-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {metaspn_entities-0.1.6.dist-info → metaspn_entities-0.1.7.dist-info}/top_level.txt +0 -0
metaspn_entities/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .adapter import SignalResolutionResult, resolve_normalized_social_signal
|
|
2
|
+
from .attribution import OutcomeAttribution
|
|
2
3
|
from .context import RecommendationContext, EntityContext, build_confidence_summary, build_recommendation_context
|
|
3
4
|
from .events import EmittedEvent
|
|
4
5
|
from .models import EntityResolution
|
|
@@ -8,6 +9,7 @@ from .sqlite_backend import SQLiteEntityStore
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"resolve_normalized_social_signal",
|
|
10
11
|
"SignalResolutionResult",
|
|
12
|
+
"OutcomeAttribution",
|
|
11
13
|
"EntityContext",
|
|
12
14
|
"RecommendationContext",
|
|
13
15
|
"build_confidence_summary",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
5
|
+
|
|
6
|
+
from .normalize import normalize_identifier
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class OutcomeAttribution:
|
|
11
|
+
entity_id: Optional[str]
|
|
12
|
+
confidence: float
|
|
13
|
+
matched_references: List[Dict[str, Any]] = field(default_factory=list)
|
|
14
|
+
strategy: str = "confidence-weighted-reference-v1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize_outcome_references(references: Mapping[str, Any] | Sequence[Mapping[str, Any]]) -> List[Tuple[str, str]]:
|
|
18
|
+
refs: List[Tuple[str, str]] = []
|
|
19
|
+
if isinstance(references, Mapping):
|
|
20
|
+
for raw_type in sorted(references):
|
|
21
|
+
value = references[raw_type]
|
|
22
|
+
if value is None:
|
|
23
|
+
continue
|
|
24
|
+
if isinstance(value, str) and value.strip():
|
|
25
|
+
refs.append((str(raw_type), value.strip()))
|
|
26
|
+
return refs
|
|
27
|
+
|
|
28
|
+
for item in references:
|
|
29
|
+
id_type = str(item.get("identifier_type") or item.get("type") or "").strip()
|
|
30
|
+
value = str(item.get("value") or "").strip()
|
|
31
|
+
if not id_type or not value:
|
|
32
|
+
continue
|
|
33
|
+
refs.append((id_type, value))
|
|
34
|
+
return refs
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def rank_entity_candidates(
|
|
38
|
+
references: Iterable[Tuple[str, str]],
|
|
39
|
+
resolve_reference: Any,
|
|
40
|
+
) -> OutcomeAttribution:
|
|
41
|
+
candidate_scores: Dict[str, float] = {}
|
|
42
|
+
candidate_hits: Dict[str, int] = {}
|
|
43
|
+
matched: List[Dict[str, Any]] = []
|
|
44
|
+
total_refs = 0
|
|
45
|
+
|
|
46
|
+
for identifier_type, value in references:
|
|
47
|
+
total_refs += 1
|
|
48
|
+
match = resolve_reference(identifier_type, value)
|
|
49
|
+
matched.append(
|
|
50
|
+
{
|
|
51
|
+
"identifier_type": identifier_type,
|
|
52
|
+
"value": value,
|
|
53
|
+
"normalized_value": match.get("normalized_value"),
|
|
54
|
+
"matched_entity_id": match.get("entity_id"),
|
|
55
|
+
"reference_confidence": float(match.get("confidence", 0.0)),
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
entity_id = match.get("entity_id")
|
|
59
|
+
confidence = float(match.get("confidence", 0.0))
|
|
60
|
+
if entity_id:
|
|
61
|
+
candidate_scores[entity_id] = candidate_scores.get(entity_id, 0.0) + confidence
|
|
62
|
+
candidate_hits[entity_id] = candidate_hits.get(entity_id, 0) + 1
|
|
63
|
+
|
|
64
|
+
if not candidate_scores:
|
|
65
|
+
return OutcomeAttribution(entity_id=None, confidence=0.0, matched_references=matched)
|
|
66
|
+
|
|
67
|
+
ranked = sorted(
|
|
68
|
+
candidate_scores.items(),
|
|
69
|
+
key=lambda kv: (
|
|
70
|
+
-kv[1],
|
|
71
|
+
-candidate_hits.get(kv[0], 0),
|
|
72
|
+
kv[0],
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
best_entity_id, best_score = ranked[0]
|
|
76
|
+
denom = max(1, total_refs)
|
|
77
|
+
normalized_confidence = min(1.0, round(best_score / float(denom), 6))
|
|
78
|
+
return OutcomeAttribution(
|
|
79
|
+
entity_id=best_entity_id,
|
|
80
|
+
confidence=normalized_confidence,
|
|
81
|
+
matched_references=matched,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def normalize_reference(identifier_type: str, value: str) -> Tuple[str, str]:
|
|
86
|
+
if identifier_type == "entity_id":
|
|
87
|
+
return identifier_type, value
|
|
88
|
+
return identifier_type, normalize_identifier(identifier_type, value)
|
metaspn_entities/resolver.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, List, Optional
|
|
4
4
|
|
|
5
|
+
from .attribution import OutcomeAttribution, normalize_outcome_references, normalize_reference, rank_entity_candidates
|
|
5
6
|
from .context import RecommendationContext, EntityContext, build_confidence_summary, build_recommendation_context
|
|
6
7
|
from .events import EmittedEvent, EventFactory
|
|
7
8
|
from .models import (
|
|
@@ -166,6 +167,34 @@ class EntityResolver:
|
|
|
166
167
|
identifiers = self.store.list_identifier_records_for_entity(canonical_id)
|
|
167
168
|
return build_recommendation_context(canonical_id, aliases, identifiers)
|
|
168
169
|
|
|
170
|
+
def attribute_outcome(self, references: Any) -> OutcomeAttribution:
|
|
171
|
+
refs = normalize_outcome_references(references)
|
|
172
|
+
|
|
173
|
+
def _resolve_ref(identifier_type: str, value: str) -> Dict[str, Any]:
|
|
174
|
+
raw_type, normalized = normalize_reference(identifier_type, value)
|
|
175
|
+
if raw_type == "entity_id":
|
|
176
|
+
entity = self.store.get_entity(normalized)
|
|
177
|
+
if not entity:
|
|
178
|
+
return {"entity_id": None, "confidence": 0.0, "normalized_value": normalized}
|
|
179
|
+
return {
|
|
180
|
+
"entity_id": self.store.canonical_entity_id(str(entity["entity_id"])),
|
|
181
|
+
"confidence": 0.99,
|
|
182
|
+
"normalized_value": normalized,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
alias = self.store.find_alias(raw_type, normalized)
|
|
186
|
+
if not alias:
|
|
187
|
+
return {"entity_id": None, "confidence": 0.0, "normalized_value": normalized}
|
|
188
|
+
|
|
189
|
+
canonical = self.store.canonical_entity_id(str(alias["entity_id"]))
|
|
190
|
+
identifier = self.store.get_identifier(raw_type, normalized)
|
|
191
|
+
alias_conf = float(alias["confidence"])
|
|
192
|
+
identifier_conf = float(identifier["confidence"]) if identifier else 0.0
|
|
193
|
+
confidence = round(max(alias_conf, identifier_conf), 6)
|
|
194
|
+
return {"entity_id": canonical, "confidence": confidence, "normalized_value": normalized}
|
|
195
|
+
|
|
196
|
+
return rank_entity_candidates(refs, _resolve_ref)
|
|
197
|
+
|
|
169
198
|
def export_snapshot(self, output_path: str) -> None:
|
|
170
199
|
self.store.export_snapshot(output_path)
|
|
171
200
|
|
|
@@ -103,6 +103,12 @@ class SQLiteEntityStore:
|
|
|
103
103
|
(identifier_type, normalized_value),
|
|
104
104
|
).fetchone()
|
|
105
105
|
|
|
106
|
+
def get_identifier(self, identifier_type: str, normalized_value: str) -> Optional[sqlite3.Row]:
|
|
107
|
+
return self.conn.execute(
|
|
108
|
+
"SELECT * FROM identifiers WHERE identifier_type = ? AND normalized_value = ?",
|
|
109
|
+
(identifier_type, normalized_value),
|
|
110
|
+
).fetchone()
|
|
111
|
+
|
|
106
112
|
def upsert_identifier(
|
|
107
113
|
self,
|
|
108
114
|
identifier_type: str,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metaspn-entities
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Canonical entity resolution, aliasing, and merges for MetaSPN systems
|
|
5
5
|
Author: MetaSPN Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -122,3 +122,16 @@ The recommendation context includes:
|
|
|
122
122
|
- preferred channel hint
|
|
123
123
|
- relationship stage hint (`cold` / `warm` / `engaged`)
|
|
124
124
|
- merge-safe continuity fields keyed to canonical entity IDs
|
|
125
|
+
|
|
126
|
+
## M3 Outcome Attribution API
|
|
127
|
+
|
|
128
|
+
Outcome evaluators can map attempt/outcome references back to canonical entity lineage:
|
|
129
|
+
|
|
130
|
+
- `resolver.attribute_outcome(references)`
|
|
131
|
+
|
|
132
|
+
Supported references include `entity_id`, `email`, `canonical_url`, handles, domains, and names.
|
|
133
|
+
|
|
134
|
+
Attribution guarantees:
|
|
135
|
+
- canonical merge redirects are resolved before returning `entity_id`
|
|
136
|
+
- output includes explicit confidence for downstream learning logic
|
|
137
|
+
- deterministic tie-breaks are applied by score, then hit count, then entity ID
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
metaspn_entities/__init__.py,sha256=LCrhnRQHweDRNX9BOj6FV34uzLRbT7whU6TleZlZiHU,709
|
|
2
|
+
metaspn_entities/adapter.py,sha256=eNB5kr1tinav85WPA4YCldRDJBgb6uYe3ZWCRVjdOms,4654
|
|
3
|
+
metaspn_entities/attribution.py,sha256=bCJGAW5XAzI9ZOWVTvw_bWfUVXbEAATFJ3N9W4_C2U8,3061
|
|
4
|
+
metaspn_entities/context.py,sha256=sUpW50Z99R0iQ5ryVR9uk7WqUaiKCxMyTQlvuV3DvVk,6054
|
|
5
|
+
metaspn_entities/events.py,sha256=Hkc3gy5_vRTSR0MKUvF24dTqNqOkG423_PTUe7csUfw,2066
|
|
6
|
+
metaspn_entities/models.py,sha256=b2EFsc1EIT9Ao_bKA2I52-5W_0fTwhsyO6VFRG8gZg8,1377
|
|
7
|
+
metaspn_entities/normalize.py,sha256=nPAHRfipgS6zHy2x70ZFd5HB1W4FKmeTF8Kd4TYz5tI,1125
|
|
8
|
+
metaspn_entities/resolver.py,sha256=bKnxInPNLefdgblrrl1oAtc2aiUlXqZz3bmSZWNX21E,9910
|
|
9
|
+
metaspn_entities/sqlite_backend.py,sha256=h7_dMNmd9-k9hxvJwZp_TM9_yNWyFbJric6TvH_BseU,12087
|
|
10
|
+
metaspn_entities-0.1.7.dist-info/licenses/LICENSE,sha256=tvVpto97dUnh1-KVYPs1rCr5dzyX8jUyNmT7F7ZPVAM,1077
|
|
11
|
+
metaspn_entities-0.1.7.dist-info/METADATA,sha256=MRfkGTOysqJ99tSf5gdSgirOPhTXDtExezGWlPDQ3WQ,4635
|
|
12
|
+
metaspn_entities-0.1.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
metaspn_entities-0.1.7.dist-info/top_level.txt,sha256=YP2V8Z1Statrs3YAI-tGvyC73vLjPHr9Vkal4yqXkhs,17
|
|
14
|
+
metaspn_entities-0.1.7.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
metaspn_entities/__init__.py,sha256=aija_xk40mk4JreYPiKKIpKwt6eQdUQRlrriwNkgZB0,639
|
|
2
|
-
metaspn_entities/adapter.py,sha256=eNB5kr1tinav85WPA4YCldRDJBgb6uYe3ZWCRVjdOms,4654
|
|
3
|
-
metaspn_entities/context.py,sha256=sUpW50Z99R0iQ5ryVR9uk7WqUaiKCxMyTQlvuV3DvVk,6054
|
|
4
|
-
metaspn_entities/events.py,sha256=Hkc3gy5_vRTSR0MKUvF24dTqNqOkG423_PTUe7csUfw,2066
|
|
5
|
-
metaspn_entities/models.py,sha256=b2EFsc1EIT9Ao_bKA2I52-5W_0fTwhsyO6VFRG8gZg8,1377
|
|
6
|
-
metaspn_entities/normalize.py,sha256=nPAHRfipgS6zHy2x70ZFd5HB1W4FKmeTF8Kd4TYz5tI,1125
|
|
7
|
-
metaspn_entities/resolver.py,sha256=lmjQj5W2ny1_FnzNB-_cywReL_5ScOOHohV_TcY4usM,8345
|
|
8
|
-
metaspn_entities/sqlite_backend.py,sha256=Ed0CGAfDGlzKuD-v3xkxumGf8My4WByMQRtrO9JHK84,11790
|
|
9
|
-
metaspn_entities-0.1.6.dist-info/licenses/LICENSE,sha256=tvVpto97dUnh1-KVYPs1rCr5dzyX8jUyNmT7F7ZPVAM,1077
|
|
10
|
-
metaspn_entities-0.1.6.dist-info/METADATA,sha256=Lp2AXFXLhnY46SlExYN4pnqora7_DH9wcwF5IAMuOD0,4130
|
|
11
|
-
metaspn_entities-0.1.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
-
metaspn_entities-0.1.6.dist-info/top_level.txt,sha256=YP2V8Z1Statrs3YAI-tGvyC73vLjPHr9Vkal4yqXkhs,17
|
|
13
|
-
metaspn_entities-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|