memoryintelligence 1.0.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.
- memoryintelligence/__init__.py +153 -0
- memoryintelligence/edge_client.py +384 -0
- memoryintelligence/memory_client.py +688 -0
- memoryintelligence/mi_types.py +364 -0
- memoryintelligence-1.0.0.dist-info/METADATA +428 -0
- memoryintelligence-1.0.0.dist-info/RECORD +9 -0
- memoryintelligence-1.0.0.dist-info/WHEEL +5 -0
- memoryintelligence-1.0.0.dist-info/licenses/LICENSE +31 -0
- memoryintelligence-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory Intelligence SDK - Client
|
|
3
|
+
================================
|
|
4
|
+
|
|
5
|
+
The main client for interacting with Memory Intelligence.
|
|
6
|
+
Designed around the six core problem statements:
|
|
7
|
+
|
|
8
|
+
1. Privacy-Personalization Paradox → retention_policy="meaning_only"
|
|
9
|
+
2. Black Box Accountability → explain=True on all operations
|
|
10
|
+
3. Proprietary Data Paralysis → EdgeClient for on-prem
|
|
11
|
+
4. Context Cost Catastrophe → Compressed meaning retrieval
|
|
12
|
+
5. Knowledge Silo Epidemic → Scope isolation with cryptographic boundaries
|
|
13
|
+
6. Attribution Extinction → provenance_mode="authorship"
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from memoryintelligence import MemoryClient
|
|
17
|
+
|
|
18
|
+
mi = MemoryClient(api_key="mi_sk_...")
|
|
19
|
+
|
|
20
|
+
# Process content → meaning
|
|
21
|
+
umo = mi.process("Meeting notes from today", user_ulid="01ABC...")
|
|
22
|
+
|
|
23
|
+
# Search meaning
|
|
24
|
+
results = mi.search("What did we discuss?", user_ulid="01ABC...")
|
|
25
|
+
|
|
26
|
+
# Match for recommendations
|
|
27
|
+
score = mi.match(user_ulid, candidate_ulid, explain=True)
|
|
28
|
+
|
|
29
|
+
# Delete for GDPR
|
|
30
|
+
mi.delete(user_ulid="01ABC...")
|
|
31
|
+
|
|
32
|
+
Author: Memory Intelligence Team
|
|
33
|
+
Version: 1.0.0
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import httpx
|
|
37
|
+
import logging
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
from typing import Any, Dict, List, Optional, Union
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from .mi_types import (
|
|
43
|
+
# Enums
|
|
44
|
+
Scope,
|
|
45
|
+
RetentionPolicy,
|
|
46
|
+
PIIHandling,
|
|
47
|
+
ProvenanceMode,
|
|
48
|
+
ExplainLevel,
|
|
49
|
+
# Response types
|
|
50
|
+
MeaningObject,
|
|
51
|
+
SearchResponse,
|
|
52
|
+
SearchResult,
|
|
53
|
+
MatchResult,
|
|
54
|
+
DeleteResult,
|
|
55
|
+
VerifyResult,
|
|
56
|
+
Explanation,
|
|
57
|
+
ExplainHuman,
|
|
58
|
+
ExplainAudit,
|
|
59
|
+
Entity,
|
|
60
|
+
Topic,
|
|
61
|
+
SVOTriple,
|
|
62
|
+
Provenance,
|
|
63
|
+
PIIDetection,
|
|
64
|
+
# Config types
|
|
65
|
+
ProcessConfig,
|
|
66
|
+
SearchConfig,
|
|
67
|
+
MatchConfig,
|
|
68
|
+
# Errors
|
|
69
|
+
MIError,
|
|
70
|
+
AuthenticationError,
|
|
71
|
+
RateLimitError,
|
|
72
|
+
ScopeViolationError,
|
|
73
|
+
PIIViolationError,
|
|
74
|
+
GovernanceError,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
logger = logging.getLogger(__name__)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ============================================================================
|
|
81
|
+
# API Configuration
|
|
82
|
+
# ============================================================================
|
|
83
|
+
|
|
84
|
+
DEFAULT_API_URL = "https://api.memoryintelligence.dev"
|
|
85
|
+
DEFAULT_TIMEOUT = 30.0
|
|
86
|
+
SDK_VERSION = "1.0.0"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ============================================================================
|
|
90
|
+
# Main Client
|
|
91
|
+
# ============================================================================
|
|
92
|
+
|
|
93
|
+
class MemoryClient:
|
|
94
|
+
"""
|
|
95
|
+
Main client for Memory Intelligence SDK.
|
|
96
|
+
|
|
97
|
+
Provides six core operations aligned to developer pain points:
|
|
98
|
+
|
|
99
|
+
- process() → Convert raw content to meaning (Privacy-Personalization)
|
|
100
|
+
- search() → Find relevant memories (Context Cost)
|
|
101
|
+
- match() → Compare memories for relevance (Personalization)
|
|
102
|
+
- explain() → Get explanation for any UMO (Black Box)
|
|
103
|
+
- delete() → Remove all user data (Compliance)
|
|
104
|
+
- verify_provenance() → Verify authorship/timestamp (Attribution)
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
api_key: Your MI API key (starts with "mi_sk_")
|
|
108
|
+
org_ulid: Organization ULID (optional, derived from key)
|
|
109
|
+
base_url: API endpoint (default: production)
|
|
110
|
+
timeout: Request timeout in seconds
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
mi = MemoryClient(api_key="mi_sk_live_abc123...")
|
|
114
|
+
|
|
115
|
+
# Process with meaning-only retention (default)
|
|
116
|
+
umo = mi.process("Important insight", user_ulid="01ABC...")
|
|
117
|
+
|
|
118
|
+
# Search with explanation
|
|
119
|
+
results = mi.search("What do I know about X?", user_ulid="01ABC...", explain=True)
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
api_key: str,
|
|
125
|
+
org_ulid: Optional[str] = None,
|
|
126
|
+
base_url: str = DEFAULT_API_URL,
|
|
127
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
128
|
+
):
|
|
129
|
+
if not api_key or not api_key.startswith("mi_sk_"):
|
|
130
|
+
raise AuthenticationError(
|
|
131
|
+
"Invalid API key format. Keys should start with 'mi_sk_'"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.api_key = api_key
|
|
135
|
+
self.org_ulid = org_ulid
|
|
136
|
+
self.base_url = base_url.rstrip("/")
|
|
137
|
+
self.timeout = timeout
|
|
138
|
+
|
|
139
|
+
# HTTP client with auth headers
|
|
140
|
+
self._client = httpx.Client(
|
|
141
|
+
base_url=self.base_url,
|
|
142
|
+
timeout=self.timeout,
|
|
143
|
+
headers={
|
|
144
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
145
|
+
"X-MI-SDK-Version": SDK_VERSION,
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
logger.info(f"MemoryClient initialized (org={org_ulid})")
|
|
151
|
+
|
|
152
|
+
def __enter__(self):
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
156
|
+
self.close()
|
|
157
|
+
|
|
158
|
+
def close(self):
|
|
159
|
+
"""Close the HTTP client."""
|
|
160
|
+
self._client.close()
|
|
161
|
+
|
|
162
|
+
# ========================================================================
|
|
163
|
+
# CORE OPERATION 1: PROCESS
|
|
164
|
+
# ========================================================================
|
|
165
|
+
|
|
166
|
+
def process(
|
|
167
|
+
self,
|
|
168
|
+
content: str,
|
|
169
|
+
user_ulid: str,
|
|
170
|
+
*,
|
|
171
|
+
retention_policy: RetentionPolicy = RetentionPolicy.MEANING_ONLY,
|
|
172
|
+
pii_handling: PIIHandling = PIIHandling.EXTRACT_AND_REDACT,
|
|
173
|
+
provenance_mode: ProvenanceMode = ProvenanceMode.STANDARD,
|
|
174
|
+
scope: Scope = Scope.USER,
|
|
175
|
+
scope_id: Optional[str] = None,
|
|
176
|
+
source: str = "api",
|
|
177
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
178
|
+
) -> MeaningObject:
|
|
179
|
+
"""
|
|
180
|
+
Process raw content into a Meaning Object.
|
|
181
|
+
|
|
182
|
+
This is the core operation: content goes in, meaning comes out,
|
|
183
|
+
raw content is discarded (by default).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
content: Raw text content to process
|
|
187
|
+
user_ulid: Owner's ULID
|
|
188
|
+
retention_policy: What to retain (default: meaning_only)
|
|
189
|
+
pii_handling: How to handle PII (default: extract_and_redact)
|
|
190
|
+
provenance_mode: Provenance tracking level (default: standard)
|
|
191
|
+
scope: Governance scope (default: user)
|
|
192
|
+
scope_id: Scope identifier (e.g., client_ulid for Scope.CLIENT)
|
|
193
|
+
source: Source identifier (e.g., "slack", "email")
|
|
194
|
+
metadata: Additional context
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
MeaningObject with entities, topics, embedding, provenance
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
PIIViolationError: If PII detected and pii_handling=REJECT
|
|
201
|
+
ScopeViolationError: If scope_id required but not provided
|
|
202
|
+
AuthenticationError: Invalid API key
|
|
203
|
+
RateLimitError: Rate limit exceeded
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
# Basic processing (meaning-only, PII redacted)
|
|
207
|
+
umo = mi.process("Sarah said budget is approved", user_ulid="01ABC...")
|
|
208
|
+
|
|
209
|
+
# With client scope isolation
|
|
210
|
+
umo = mi.process(
|
|
211
|
+
meeting_notes,
|
|
212
|
+
user_ulid="01ABC...",
|
|
213
|
+
scope=Scope.CLIENT,
|
|
214
|
+
scope_id=client_ulid
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# With authorship provenance
|
|
218
|
+
umo = mi.process(
|
|
219
|
+
article_text,
|
|
220
|
+
user_ulid="01ABC...",
|
|
221
|
+
provenance_mode=ProvenanceMode.AUTHORSHIP
|
|
222
|
+
)
|
|
223
|
+
"""
|
|
224
|
+
# Validate scope_id requirement
|
|
225
|
+
if scope in (Scope.CLIENT, Scope.PROJECT, Scope.TEAM) and not scope_id:
|
|
226
|
+
raise ScopeViolationError(
|
|
227
|
+
f"scope_id required for {scope.value} scope"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
payload = {
|
|
231
|
+
"content": content,
|
|
232
|
+
"user_ulid": user_ulid,
|
|
233
|
+
"retention_policy": retention_policy.value,
|
|
234
|
+
"pii_handling": pii_handling.value,
|
|
235
|
+
"provenance_mode": provenance_mode.value,
|
|
236
|
+
"scope": scope.value,
|
|
237
|
+
"scope_id": scope_id,
|
|
238
|
+
"source": source,
|
|
239
|
+
"metadata": metadata or {},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if self.org_ulid:
|
|
243
|
+
payload["org_ulid"] = self.org_ulid
|
|
244
|
+
|
|
245
|
+
response = self._request("POST", "/v1/process", json=payload)
|
|
246
|
+
return self._parse_meaning_object(response)
|
|
247
|
+
|
|
248
|
+
# ========================================================================
|
|
249
|
+
# CORE OPERATION 2: SEARCH
|
|
250
|
+
# ========================================================================
|
|
251
|
+
|
|
252
|
+
def search(
|
|
253
|
+
self,
|
|
254
|
+
query: str,
|
|
255
|
+
user_ulid: str,
|
|
256
|
+
*,
|
|
257
|
+
scope: Scope = Scope.USER,
|
|
258
|
+
scope_id: Optional[str] = None,
|
|
259
|
+
explain: Union[bool, ExplainLevel] = False,
|
|
260
|
+
limit: int = 10,
|
|
261
|
+
offset: int = 0,
|
|
262
|
+
date_from: Optional[datetime] = None,
|
|
263
|
+
date_to: Optional[datetime] = None,
|
|
264
|
+
topics: Optional[List[str]] = None,
|
|
265
|
+
entities: Optional[List[str]] = None,
|
|
266
|
+
budget_tokens: Optional[int] = None,
|
|
267
|
+
) -> SearchResponse:
|
|
268
|
+
"""
|
|
269
|
+
Search for relevant meaning objects.
|
|
270
|
+
|
|
271
|
+
Multi-signal ranking: semantic + temporal + entity + graph.
|
|
272
|
+
Returns compressed meaning (95% smaller than raw).
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
query: Natural language search query
|
|
276
|
+
user_ulid: Searcher's ULID
|
|
277
|
+
scope: Search scope (default: user)
|
|
278
|
+
scope_id: Scope identifier for scoped searches
|
|
279
|
+
explain: Include explanation (default: False)
|
|
280
|
+
limit: Maximum results (default: 10)
|
|
281
|
+
offset: Pagination offset
|
|
282
|
+
date_from: Filter by date range start
|
|
283
|
+
date_to: Filter by date range end
|
|
284
|
+
topics: Filter by topics
|
|
285
|
+
entities: Filter by entities
|
|
286
|
+
budget_tokens: Maximum tokens in response (for cost control)
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
SearchResponse with ranked results and explanations
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
# Basic search
|
|
293
|
+
results = mi.search("budget discussions", user_ulid="01ABC...")
|
|
294
|
+
|
|
295
|
+
# Scoped search with explanation
|
|
296
|
+
results = mi.search(
|
|
297
|
+
"What are Acme's concerns?",
|
|
298
|
+
user_ulid="01ABC...",
|
|
299
|
+
scope=Scope.CLIENT,
|
|
300
|
+
scope_id=acme_ulid,
|
|
301
|
+
explain=True
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Token-budgeted search
|
|
305
|
+
results = mi.search(
|
|
306
|
+
"summarize last month",
|
|
307
|
+
user_ulid="01ABC...",
|
|
308
|
+
budget_tokens=500
|
|
309
|
+
)
|
|
310
|
+
"""
|
|
311
|
+
if scope in (Scope.CLIENT, Scope.PROJECT, Scope.TEAM) and not scope_id:
|
|
312
|
+
raise ScopeViolationError(
|
|
313
|
+
f"scope_id required for {scope.value} scope"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Normalize explain parameter
|
|
317
|
+
if isinstance(explain, bool):
|
|
318
|
+
explain_level = ExplainLevel.FULL if explain else ExplainLevel.NONE
|
|
319
|
+
else:
|
|
320
|
+
explain_level = explain
|
|
321
|
+
|
|
322
|
+
payload = {
|
|
323
|
+
"query": query,
|
|
324
|
+
"user_ulid": user_ulid,
|
|
325
|
+
"scope": scope.value,
|
|
326
|
+
"scope_id": scope_id,
|
|
327
|
+
"explain": explain_level.value,
|
|
328
|
+
"limit": limit,
|
|
329
|
+
"offset": offset,
|
|
330
|
+
"budget_tokens": budget_tokens,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if date_from:
|
|
334
|
+
payload["date_from"] = date_from.isoformat()
|
|
335
|
+
if date_to:
|
|
336
|
+
payload["date_to"] = date_to.isoformat()
|
|
337
|
+
if topics:
|
|
338
|
+
payload["topics"] = topics
|
|
339
|
+
if entities:
|
|
340
|
+
payload["entities"] = entities
|
|
341
|
+
if self.org_ulid:
|
|
342
|
+
payload["org_ulid"] = self.org_ulid
|
|
343
|
+
|
|
344
|
+
response = self._request("POST", "/v1/search", json=payload)
|
|
345
|
+
return self._parse_search_response(response, query, scope)
|
|
346
|
+
|
|
347
|
+
# ========================================================================
|
|
348
|
+
# CORE OPERATION 3: MATCH
|
|
349
|
+
# ========================================================================
|
|
350
|
+
|
|
351
|
+
def match(
|
|
352
|
+
self,
|
|
353
|
+
source_ulid: str,
|
|
354
|
+
candidate_ulid: str,
|
|
355
|
+
*,
|
|
356
|
+
explain: Union[bool, ExplainLevel] = False,
|
|
357
|
+
threshold: float = 0.7,
|
|
358
|
+
) -> MatchResult:
|
|
359
|
+
"""
|
|
360
|
+
Compare two meaning objects for relevance.
|
|
361
|
+
|
|
362
|
+
Used for recommendations: "Is this content relevant to this user?"
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
source_ulid: User or memory ULID (the "who")
|
|
366
|
+
candidate_ulid: Candidate memory ULID (the "what")
|
|
367
|
+
explain: Include explanation (default: False)
|
|
368
|
+
threshold: Match threshold (default: 0.7)
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
MatchResult with score and explanation
|
|
372
|
+
|
|
373
|
+
Example:
|
|
374
|
+
# Check if post is relevant to user
|
|
375
|
+
match = mi.match(user_ulid, post_ulid, explain=True)
|
|
376
|
+
|
|
377
|
+
if match.match:
|
|
378
|
+
print(f"Relevant! {match.explain.human.summary}")
|
|
379
|
+
"""
|
|
380
|
+
if isinstance(explain, bool):
|
|
381
|
+
explain_level = ExplainLevel.FULL if explain else ExplainLevel.NONE
|
|
382
|
+
else:
|
|
383
|
+
explain_level = explain
|
|
384
|
+
|
|
385
|
+
payload = {
|
|
386
|
+
"source_ulid": source_ulid,
|
|
387
|
+
"candidate_ulid": candidate_ulid,
|
|
388
|
+
"explain": explain_level.value,
|
|
389
|
+
"threshold": threshold,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if self.org_ulid:
|
|
393
|
+
payload["org_ulid"] = self.org_ulid
|
|
394
|
+
|
|
395
|
+
response = self._request("POST", "/v1/match", json=payload)
|
|
396
|
+
return self._parse_match_result(response, source_ulid, candidate_ulid)
|
|
397
|
+
|
|
398
|
+
# ========================================================================
|
|
399
|
+
# CORE OPERATION 4: EXPLAIN
|
|
400
|
+
# ========================================================================
|
|
401
|
+
|
|
402
|
+
def explain(
|
|
403
|
+
self,
|
|
404
|
+
umo_id: str,
|
|
405
|
+
*,
|
|
406
|
+
level: ExplainLevel = ExplainLevel.FULL,
|
|
407
|
+
) -> Explanation:
|
|
408
|
+
"""
|
|
409
|
+
Get detailed explanation for any meaning object.
|
|
410
|
+
|
|
411
|
+
Returns human-readable + machine-verifiable explanation.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
umo_id: ULID of the meaning object
|
|
415
|
+
level: Explanation level (default: full)
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Explanation with human and audit components
|
|
419
|
+
|
|
420
|
+
Example:
|
|
421
|
+
# Get explanation for a recommendation
|
|
422
|
+
explanation = mi.explain(umo_id)
|
|
423
|
+
print(explanation.human.summary)
|
|
424
|
+
print(explanation.audit.hash_chain)
|
|
425
|
+
"""
|
|
426
|
+
payload = {
|
|
427
|
+
"umo_id": umo_id,
|
|
428
|
+
"level": level.value,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if self.org_ulid:
|
|
432
|
+
payload["org_ulid"] = self.org_ulid
|
|
433
|
+
|
|
434
|
+
response = self._request("GET", f"/v1/explain/{umo_id}", params=payload)
|
|
435
|
+
return self._parse_explanation(response)
|
|
436
|
+
|
|
437
|
+
# ========================================================================
|
|
438
|
+
# CORE OPERATION 5: DELETE
|
|
439
|
+
# ========================================================================
|
|
440
|
+
|
|
441
|
+
def delete(
|
|
442
|
+
self,
|
|
443
|
+
user_ulid: str,
|
|
444
|
+
*,
|
|
445
|
+
scope: Scope = Scope.ALL,
|
|
446
|
+
scope_id: Optional[str] = None,
|
|
447
|
+
) -> DeleteResult:
|
|
448
|
+
"""
|
|
449
|
+
Delete all meaning for a user/scope.
|
|
450
|
+
|
|
451
|
+
GDPR compliance in one API call. Provenance proves deletion.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
user_ulid: User whose data to delete
|
|
455
|
+
scope: Scope to delete (default: ALL)
|
|
456
|
+
scope_id: Specific scope to delete (for partial deletion)
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
DeleteResult with count and audit proof
|
|
460
|
+
|
|
461
|
+
Example:
|
|
462
|
+
# Full GDPR deletion
|
|
463
|
+
result = mi.delete(user_ulid="01ABC...")
|
|
464
|
+
print(f"Deleted {result.deleted_count} memories")
|
|
465
|
+
|
|
466
|
+
# Delete only one client's data
|
|
467
|
+
result = mi.delete(
|
|
468
|
+
user_ulid="01ABC...",
|
|
469
|
+
scope=Scope.CLIENT,
|
|
470
|
+
scope_id=client_ulid
|
|
471
|
+
)
|
|
472
|
+
"""
|
|
473
|
+
payload = {
|
|
474
|
+
"user_ulid": user_ulid,
|
|
475
|
+
"scope": scope.value,
|
|
476
|
+
"scope_id": scope_id,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if self.org_ulid:
|
|
480
|
+
payload["org_ulid"] = self.org_ulid
|
|
481
|
+
|
|
482
|
+
response = self._request("DELETE", "/v1/delete", json=payload)
|
|
483
|
+
|
|
484
|
+
return DeleteResult(
|
|
485
|
+
deleted_count=response.get("deleted_count", 0),
|
|
486
|
+
user_ulid=user_ulid,
|
|
487
|
+
scope=scope,
|
|
488
|
+
scope_id=scope_id,
|
|
489
|
+
audit_proof=response.get("audit_proof", {}),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# ========================================================================
|
|
493
|
+
# CORE OPERATION 6: VERIFY PROVENANCE
|
|
494
|
+
# ========================================================================
|
|
495
|
+
|
|
496
|
+
def verify_provenance(
|
|
497
|
+
self,
|
|
498
|
+
content_hash: str,
|
|
499
|
+
) -> VerifyResult:
|
|
500
|
+
"""
|
|
501
|
+
Verify provenance of content.
|
|
502
|
+
|
|
503
|
+
Check who created content first, verify hash chain integrity.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
content_hash: Semantic hash of content to verify
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
VerifyResult with authorship and timestamp proof
|
|
510
|
+
|
|
511
|
+
Example:
|
|
512
|
+
# Verify authorship before using content
|
|
513
|
+
result = mi.verify_provenance(content_hash)
|
|
514
|
+
|
|
515
|
+
if result.valid:
|
|
516
|
+
print(f"Original author: {result.original_author_ulid}")
|
|
517
|
+
print(f"First published: {result.first_published}")
|
|
518
|
+
"""
|
|
519
|
+
response = self._request(
|
|
520
|
+
"GET",
|
|
521
|
+
f"/v1/provenance/verify/{content_hash}"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return VerifyResult(
|
|
525
|
+
valid=response.get("valid", False),
|
|
526
|
+
semantic_hash=response.get("semantic_hash", content_hash),
|
|
527
|
+
timestamp_anchor=datetime.fromisoformat(
|
|
528
|
+
response.get("timestamp_anchor", datetime.utcnow().isoformat())
|
|
529
|
+
),
|
|
530
|
+
original_author_ulid=response.get("original_author_ulid"),
|
|
531
|
+
first_published=datetime.fromisoformat(response["first_published"])
|
|
532
|
+
if response.get("first_published") else None,
|
|
533
|
+
hash_chain_valid=response.get("hash_chain_valid", True),
|
|
534
|
+
audit_proof=response.get("audit_proof", {}),
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# ========================================================================
|
|
538
|
+
# INTERNAL METHODS
|
|
539
|
+
# ========================================================================
|
|
540
|
+
|
|
541
|
+
def _request(
|
|
542
|
+
self,
|
|
543
|
+
method: str,
|
|
544
|
+
path: str,
|
|
545
|
+
**kwargs
|
|
546
|
+
) -> Dict[str, Any]:
|
|
547
|
+
"""Make authenticated request to MI API."""
|
|
548
|
+
try:
|
|
549
|
+
response = self._client.request(method, path, **kwargs)
|
|
550
|
+
|
|
551
|
+
if response.status_code == 401:
|
|
552
|
+
raise AuthenticationError("Invalid or expired API key")
|
|
553
|
+
elif response.status_code == 429:
|
|
554
|
+
error = RateLimitError("Rate limit exceeded")
|
|
555
|
+
error.retry_after = int(response.headers.get("Retry-After", 60))
|
|
556
|
+
raise error
|
|
557
|
+
elif response.status_code == 403:
|
|
558
|
+
raise ScopeViolationError(
|
|
559
|
+
response.json().get("detail", "Access denied")
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
response.raise_for_status()
|
|
563
|
+
return response.json()
|
|
564
|
+
|
|
565
|
+
except httpx.HTTPStatusError as e:
|
|
566
|
+
raise MIError(f"API error: {e}")
|
|
567
|
+
except httpx.RequestError as e:
|
|
568
|
+
raise MIError(f"Request failed: {e}")
|
|
569
|
+
|
|
570
|
+
def _parse_meaning_object(self, data: Dict[str, Any]) -> MeaningObject:
|
|
571
|
+
"""Parse API response into MeaningObject."""
|
|
572
|
+
return MeaningObject(
|
|
573
|
+
umo_id=data["umo_id"],
|
|
574
|
+
user_ulid=data["user_ulid"],
|
|
575
|
+
entities=[
|
|
576
|
+
Entity(
|
|
577
|
+
text=e["text"],
|
|
578
|
+
type=e["type"],
|
|
579
|
+
confidence=e.get("confidence", 1.0),
|
|
580
|
+
first_seen=datetime.fromisoformat(e["first_seen"]) if e.get("first_seen") else None,
|
|
581
|
+
resolved_ulid=e.get("resolved_ulid"),
|
|
582
|
+
)
|
|
583
|
+
for e in data.get("entities", [])
|
|
584
|
+
],
|
|
585
|
+
topics=[
|
|
586
|
+
Topic(
|
|
587
|
+
name=t["name"],
|
|
588
|
+
confidence=t.get("confidence", 1.0),
|
|
589
|
+
parent=t.get("parent"),
|
|
590
|
+
)
|
|
591
|
+
for t in data.get("topics", [])
|
|
592
|
+
],
|
|
593
|
+
svo_triples=[
|
|
594
|
+
SVOTriple(
|
|
595
|
+
subject=s["subject"],
|
|
596
|
+
verb=s["verb"],
|
|
597
|
+
object=s["object"],
|
|
598
|
+
confidence=s.get("confidence", 1.0),
|
|
599
|
+
)
|
|
600
|
+
for s in data.get("svo_triples", [])
|
|
601
|
+
],
|
|
602
|
+
key_phrases=data.get("key_phrases", []),
|
|
603
|
+
summary=data.get("summary"),
|
|
604
|
+
embedding=data.get("embedding"),
|
|
605
|
+
embedding_model=data.get("embedding_model", ""),
|
|
606
|
+
sentiment_label=data.get("sentiment_label"),
|
|
607
|
+
sentiment_score=data.get("sentiment_score", 0.0),
|
|
608
|
+
timestamp=datetime.fromisoformat(data["timestamp"]) if data.get("timestamp") else None,
|
|
609
|
+
ingested_at=datetime.fromisoformat(data.get("ingested_at", datetime.utcnow().isoformat())),
|
|
610
|
+
recency_score=data.get("recency_score", 1.0),
|
|
611
|
+
quality_score=data.get("quality_score", 0.0),
|
|
612
|
+
validation_status=data.get("validation_status", "pending"),
|
|
613
|
+
provenance=Provenance(
|
|
614
|
+
semantic_hash=data["provenance"]["semantic_hash"],
|
|
615
|
+
timestamp_anchor=datetime.fromisoformat(data["provenance"]["timestamp_anchor"]),
|
|
616
|
+
hash_chain=data["provenance"]["hash_chain"],
|
|
617
|
+
lineage=data["provenance"].get("lineage", []),
|
|
618
|
+
model_version=data["provenance"].get("model_version", ""),
|
|
619
|
+
) if data.get("provenance") else None,
|
|
620
|
+
pii=PIIDetection(
|
|
621
|
+
detected=data["pii"]["detected"],
|
|
622
|
+
types=data["pii"].get("types", []),
|
|
623
|
+
count=data["pii"].get("count", 0),
|
|
624
|
+
handling_applied=PIIHandling(data["pii"].get("handling_applied", "detect_only")),
|
|
625
|
+
) if data.get("pii") else None,
|
|
626
|
+
scope=Scope(data.get("scope", "user")),
|
|
627
|
+
scope_id=data.get("scope_id"),
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
def _parse_search_response(
|
|
631
|
+
self,
|
|
632
|
+
data: Dict[str, Any],
|
|
633
|
+
query: str,
|
|
634
|
+
scope: Scope
|
|
635
|
+
) -> SearchResponse:
|
|
636
|
+
"""Parse API response into SearchResponse."""
|
|
637
|
+
results = []
|
|
638
|
+
for r in data.get("results", []):
|
|
639
|
+
umo = self._parse_meaning_object(r["umo"])
|
|
640
|
+
explain = self._parse_explanation(r["explain"]) if r.get("explain") else None
|
|
641
|
+
results.append(SearchResult(
|
|
642
|
+
umo=umo,
|
|
643
|
+
score=r["score"],
|
|
644
|
+
explain=explain,
|
|
645
|
+
))
|
|
646
|
+
|
|
647
|
+
return SearchResponse(
|
|
648
|
+
results=results,
|
|
649
|
+
query=query,
|
|
650
|
+
scope=scope,
|
|
651
|
+
total_count=data.get("total_count", len(results)),
|
|
652
|
+
audit_proof=data.get("audit_proof"),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
def _parse_match_result(
|
|
656
|
+
self,
|
|
657
|
+
data: Dict[str, Any],
|
|
658
|
+
source_ulid: str,
|
|
659
|
+
candidate_ulid: str
|
|
660
|
+
) -> MatchResult:
|
|
661
|
+
"""Parse API response into MatchResult."""
|
|
662
|
+
return MatchResult(
|
|
663
|
+
score=data["score"],
|
|
664
|
+
match=data["match"],
|
|
665
|
+
source_ulid=source_ulid,
|
|
666
|
+
candidate_ulid=candidate_ulid,
|
|
667
|
+
explain=self._parse_explanation(data["explain"]) if data.get("explain") else None,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
def _parse_explanation(self, data: Dict[str, Any]) -> Explanation:
|
|
671
|
+
"""Parse explanation data."""
|
|
672
|
+
return Explanation(
|
|
673
|
+
human=ExplainHuman(
|
|
674
|
+
summary=data.get("human", {}).get("summary", ""),
|
|
675
|
+
key_reasons=data.get("human", {}).get("key_reasons", []),
|
|
676
|
+
what_changed=data.get("human", {}).get("what_changed"),
|
|
677
|
+
),
|
|
678
|
+
audit=ExplainAudit(
|
|
679
|
+
semantic_score=data.get("audit", {}).get("semantic_score", 0.0),
|
|
680
|
+
temporal_score=data.get("audit", {}).get("temporal_score", 0.0),
|
|
681
|
+
entity_score=data.get("audit", {}).get("entity_score", 0.0),
|
|
682
|
+
graph_score=data.get("audit", {}).get("graph_score", 0.0),
|
|
683
|
+
topic_match=data.get("audit", {}).get("topic_match", []),
|
|
684
|
+
model_version=data.get("audit", {}).get("model_version", ""),
|
|
685
|
+
hash_chain=data.get("audit", {}).get("hash_chain", ""),
|
|
686
|
+
reproducible=data.get("audit", {}).get("reproducible", True),
|
|
687
|
+
),
|
|
688
|
+
)
|