@reconcrap/people-network-memory 0.1.0
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.
- package/README.md +476 -0
- package/docs/mcp_tools.md +138 -0
- package/harness_adapters/openclaw/mcp.managed.unix.template.json +25 -0
- package/harness_adapters/openclaw/mcp.managed.windows.template.json +26 -0
- package/harness_adapters/openclaw/mcp.template.json +14 -0
- package/harness_adapters/openclaw/ppl/SKILL.md +114 -0
- package/package.json +30 -0
- package/pyproject.toml +26 -0
- package/scripts/install_windows.ps1 +92 -0
- package/scripts/npm/people-memory.js +276 -0
- package/scripts/people_memory_bootstrap.py +247 -0
- package/scripts/run_graphiti_live_from_liepin.ps1 +87 -0
- package/scripts/run_tests_with_artifacts.ps1 +307 -0
- package/src/people_network_memory/__init__.py +6 -0
- package/src/people_network_memory/application/__init__.py +16 -0
- package/src/people_network_memory/application/normalization.py +1441 -0
- package/src/people_network_memory/application/services.py +921 -0
- package/src/people_network_memory/cli.py +1212 -0
- package/src/people_network_memory/config.py +268 -0
- package/src/people_network_memory/domain/__init__.py +55 -0
- package/src/people_network_memory/domain/identity.py +77 -0
- package/src/people_network_memory/domain/models.py +355 -0
- package/src/people_network_memory/fixtures/__init__.py +6 -0
- package/src/people_network_memory/fixtures/eval.py +398 -0
- package/src/people_network_memory/fixtures/extractor_eval.py +364 -0
- package/src/people_network_memory/fixtures/generator.py +290 -0
- package/src/people_network_memory/fixtures/report.py +252 -0
- package/src/people_network_memory/graphiti_adapter/__init__.py +9 -0
- package/src/people_network_memory/graphiti_adapter/episode_formatter.py +70 -0
- package/src/people_network_memory/graphiti_adapter/graphiti_store.py +655 -0
- package/src/people_network_memory/graphiti_adapter/indexer.py +194 -0
- package/src/people_network_memory/graphiti_adapter/ontology.py +68 -0
- package/src/people_network_memory/harness_adapters/__init__.py +2 -0
- package/src/people_network_memory/harness_adapters/openclaw/__init__.py +9 -0
- package/src/people_network_memory/harness_adapters/openclaw/installer.py +577 -0
- package/src/people_network_memory/harness_adapters/openclaw/integration_eval.py +508 -0
- package/src/people_network_memory/harness_adapters/openclaw/smoke.py +292 -0
- package/src/people_network_memory/infrastructure/__init__.py +2 -0
- package/src/people_network_memory/infrastructure/archive_backup.py +171 -0
- package/src/people_network_memory/infrastructure/diagnostics.py +171 -0
- package/src/people_network_memory/infrastructure/embeddings.py +155 -0
- package/src/people_network_memory/infrastructure/file_store.py +129 -0
- package/src/people_network_memory/infrastructure/graphiti_promotion.py +212 -0
- package/src/people_network_memory/infrastructure/id_generator.py +40 -0
- package/src/people_network_memory/infrastructure/in_memory_store.py +1008 -0
- package/src/people_network_memory/infrastructure/llm_extractor.py +476 -0
- package/src/people_network_memory/infrastructure/llm_identity_advisor.py +200 -0
- package/src/people_network_memory/infrastructure/llm_judge.py +162 -0
- package/src/people_network_memory/infrastructure/redaction.py +21 -0
- package/src/people_network_memory/infrastructure/release_check.py +186 -0
- package/src/people_network_memory/infrastructure/retrieval_intent.py +98 -0
- package/src/people_network_memory/infrastructure/semantic_index.py +262 -0
- package/src/people_network_memory/mcp_server/__init__.py +2 -0
- package/src/people_network_memory/mcp_server/contracts.py +85 -0
- package/src/people_network_memory/mcp_server/runtime.py +133 -0
- package/src/people_network_memory/mcp_server/tools.py +588 -0
- package/src/people_network_memory/ports/__init__.py +2 -0
- package/src/people_network_memory/ports/errors.py +25 -0
- package/src/people_network_memory/ports/interfaces.py +103 -0
- package/src/people_network_memory/projection/__init__.py +6 -0
- package/src/people_network_memory/projection/builders.py +46 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Pure domain models.
|
|
2
|
+
|
|
3
|
+
Rules for this module:
|
|
4
|
+
- no Graphiti imports,
|
|
5
|
+
- no MCP imports,
|
|
6
|
+
- no filesystem/env access,
|
|
7
|
+
- no live model calls.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import date, datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StrictModel(BaseModel):
|
|
20
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SensitivityLabel(str, Enum):
|
|
24
|
+
PUBLIC = "public"
|
|
25
|
+
PRIVATE = "private"
|
|
26
|
+
SENSITIVE = "sensitive"
|
|
27
|
+
SECONDHAND = "secondhand"
|
|
28
|
+
UNVERIFIED = "unverified"
|
|
29
|
+
DO_NOT_SURFACE_UNPROMPTED = "do_not_surface_unprompted"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Evidence(StrictModel):
|
|
33
|
+
evidence_id: str
|
|
34
|
+
source_text: str
|
|
35
|
+
recorded_at: datetime
|
|
36
|
+
speaker_person_id: str | None = None
|
|
37
|
+
speaker_label: str | None = None
|
|
38
|
+
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PersonRef(StrictModel):
|
|
42
|
+
label: str
|
|
43
|
+
person_id: str | None = None
|
|
44
|
+
aliases: list[str] = Field(default_factory=list)
|
|
45
|
+
email: str | None = None
|
|
46
|
+
phone: str | None = None
|
|
47
|
+
company_hint: str | None = None
|
|
48
|
+
source_span: str | None = None
|
|
49
|
+
|
|
50
|
+
@field_validator("label")
|
|
51
|
+
@classmethod
|
|
52
|
+
def label_must_not_be_empty(cls, value: str) -> str:
|
|
53
|
+
value = value.strip()
|
|
54
|
+
if not value:
|
|
55
|
+
raise ValueError("person reference label cannot be empty")
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ContactMethod(StrictModel):
|
|
60
|
+
kind: Literal["email", "phone", "linkedin", "wechat", "address", "other"]
|
|
61
|
+
value: str
|
|
62
|
+
label: str | None = None
|
|
63
|
+
sensitivity: list[SensitivityLabel] = Field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class WorkHistoryRecord(StrictModel):
|
|
67
|
+
organization: str
|
|
68
|
+
role: str | None = None
|
|
69
|
+
department: str | None = None
|
|
70
|
+
location: str | None = None
|
|
71
|
+
start_date: date | None = None
|
|
72
|
+
end_date: date | None = None
|
|
73
|
+
is_current: bool = False
|
|
74
|
+
evidence_id: str | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class EducationRecord(StrictModel):
|
|
78
|
+
school: str
|
|
79
|
+
degree: str | None = None
|
|
80
|
+
major: str | None = None
|
|
81
|
+
start_date: date | None = None
|
|
82
|
+
end_date: date | None = None
|
|
83
|
+
evidence_id: str | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ImportantDate(StrictModel):
|
|
87
|
+
kind: Literal["birthday", "anniversary", "graduation", "first_met", "custom"]
|
|
88
|
+
value: date
|
|
89
|
+
label: str | None = None
|
|
90
|
+
evidence_id: str | None = None
|
|
91
|
+
sensitivity: list[SensitivityLabel] = Field(default_factory=list)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DirectFact(StrictModel):
|
|
95
|
+
subject: PersonRef
|
|
96
|
+
predicate: str
|
|
97
|
+
value: str
|
|
98
|
+
effective_at: date | None = None
|
|
99
|
+
sensitivity: list[SensitivityLabel] = Field(default_factory=list)
|
|
100
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AttributedClaim(StrictModel):
|
|
104
|
+
speaker: PersonRef | None = None
|
|
105
|
+
subject: PersonRef | None = None
|
|
106
|
+
claim_text: str
|
|
107
|
+
claim_type: str | None = None
|
|
108
|
+
sensitivity: list[SensitivityLabel] = Field(
|
|
109
|
+
default_factory=lambda: [SensitivityLabel.SECONDHAND, SensitivityLabel.UNVERIFIED]
|
|
110
|
+
)
|
|
111
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
112
|
+
|
|
113
|
+
@field_validator("claim_text")
|
|
114
|
+
@classmethod
|
|
115
|
+
def claim_text_required(cls, value: str) -> str:
|
|
116
|
+
value = value.strip()
|
|
117
|
+
if not value:
|
|
118
|
+
raise ValueError("claim_text cannot be empty")
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class FollowUpTask(StrictModel):
|
|
123
|
+
description: str
|
|
124
|
+
due_at: date | None = None
|
|
125
|
+
related_people: list[PersonRef] = Field(default_factory=list)
|
|
126
|
+
status: Literal["open", "done", "dismissed"] = "open"
|
|
127
|
+
sensitivity: list[SensitivityLabel] = Field(default_factory=list)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class RelationshipAssertion(StrictModel):
|
|
131
|
+
source: PersonRef
|
|
132
|
+
target: PersonRef
|
|
133
|
+
relationship_type: str
|
|
134
|
+
directed: bool = False
|
|
135
|
+
sensitivity: list[SensitivityLabel] = Field(default_factory=list)
|
|
136
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Participant(StrictModel):
|
|
140
|
+
person: PersonRef
|
|
141
|
+
role: Literal["participant", "host", "organizer"] = "participant"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MentionedPerson(StrictModel):
|
|
145
|
+
person: PersonRef
|
|
146
|
+
mentioned_by: PersonRef | None = None
|
|
147
|
+
context: str | None = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class SocialInteraction(StrictModel):
|
|
151
|
+
source_text: str
|
|
152
|
+
occurred_at: datetime | None = None
|
|
153
|
+
interaction_type: Literal[
|
|
154
|
+
"meeting", "coffee", "dinner", "call", "message", "intro", "event", "other"
|
|
155
|
+
] = "meeting"
|
|
156
|
+
place: str | None = None
|
|
157
|
+
participants: list[Participant] = Field(default_factory=list)
|
|
158
|
+
mentioned_people: list[MentionedPerson] = Field(default_factory=list)
|
|
159
|
+
topics: list[str] = Field(default_factory=list)
|
|
160
|
+
direct_facts: list[DirectFact] = Field(default_factory=list)
|
|
161
|
+
attributed_claims: list[AttributedClaim] = Field(default_factory=list)
|
|
162
|
+
follow_ups: list[FollowUpTask] = Field(default_factory=list)
|
|
163
|
+
relationships: list[RelationshipAssertion] = Field(default_factory=list)
|
|
164
|
+
sensitivity: list[SensitivityLabel] = Field(default_factory=list)
|
|
165
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
166
|
+
|
|
167
|
+
@field_validator("source_text")
|
|
168
|
+
@classmethod
|
|
169
|
+
def source_text_required(cls, value: str) -> str:
|
|
170
|
+
value = value.strip()
|
|
171
|
+
if not value:
|
|
172
|
+
raise ValueError("source_text cannot be empty")
|
|
173
|
+
return value
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class PersonProfileUpdate(StrictModel):
|
|
177
|
+
person: PersonRef
|
|
178
|
+
source_text: str
|
|
179
|
+
work_history: list[WorkHistoryRecord] = Field(default_factory=list)
|
|
180
|
+
education: list[EducationRecord] = Field(default_factory=list)
|
|
181
|
+
interests: list[str] = Field(default_factory=list)
|
|
182
|
+
important_dates: list[ImportantDate] = Field(default_factory=list)
|
|
183
|
+
contacts: list[ContactMethod] = Field(default_factory=list)
|
|
184
|
+
preferences: list[str] = Field(default_factory=list)
|
|
185
|
+
direct_facts: list[DirectFact] = Field(default_factory=list)
|
|
186
|
+
sensitivity: list[SensitivityLabel] = Field(default_factory=list)
|
|
187
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class IdentityCandidate(StrictModel):
|
|
191
|
+
person_id: str
|
|
192
|
+
display_name: str
|
|
193
|
+
score: float = Field(ge=0.0, le=1.0)
|
|
194
|
+
evidence: list[str] = Field(default_factory=list)
|
|
195
|
+
exact_identifier_match: bool = False
|
|
196
|
+
exact_name_match: bool = False
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class IdentityAdvice(StrictModel):
|
|
200
|
+
recommendation: Literal[
|
|
201
|
+
"same_person",
|
|
202
|
+
"different_person",
|
|
203
|
+
"ambiguous_needs_review",
|
|
204
|
+
"unknown",
|
|
205
|
+
] = "unknown"
|
|
206
|
+
confidence: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
207
|
+
candidate_person_id: str | None = None
|
|
208
|
+
reasons: list[str] = Field(default_factory=list)
|
|
209
|
+
evidence: list[str] = Field(default_factory=list)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ReviewItem(StrictModel):
|
|
213
|
+
review_id: str
|
|
214
|
+
kind: Literal["identity", "relationship", "date", "sensitivity", "merge"]
|
|
215
|
+
message: str
|
|
216
|
+
candidates: list[IdentityCandidate] = Field(default_factory=list)
|
|
217
|
+
subject_person_id: str | None = None
|
|
218
|
+
source_text: str | None = None
|
|
219
|
+
created_at: datetime
|
|
220
|
+
status: Literal["open", "resolved", "dismissed"] = "open"
|
|
221
|
+
resolved_to_person_id: str | None = None
|
|
222
|
+
resolved_at: datetime | None = None
|
|
223
|
+
resolution_note: str | None = None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class OpportunityTimeHint(StrictModel):
|
|
227
|
+
text: str | None = None
|
|
228
|
+
normalized_date: date | None = None
|
|
229
|
+
needs_interpretation: bool = False
|
|
230
|
+
default_assumption: str | None = None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class HarnessContextRequest(StrictModel):
|
|
234
|
+
kind: Literal["search_harness_memory", "inspect_available_tools", "apply_user_preferences"]
|
|
235
|
+
query: str
|
|
236
|
+
purpose: str
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class PostCaptureOpportunity(StrictModel):
|
|
240
|
+
kind: Literal["open_loop", "context_enrichment", "identity_review", "sensitivity_review"]
|
|
241
|
+
priority: Literal["low", "medium", "high"] = "medium"
|
|
242
|
+
summary: str
|
|
243
|
+
user_value: str
|
|
244
|
+
recommended_default: Literal["execute_if_supported", "ask_first", "do_not_execute"]
|
|
245
|
+
possible_next_steps: list[str] = Field(default_factory=list)
|
|
246
|
+
risk_level: Literal["low", "medium", "high"] = "low"
|
|
247
|
+
reversibility: Literal["easy", "manual", "hard", "impossible"] = "easy"
|
|
248
|
+
external_visibility: Literal["private", "local", "visible_to_others"] = "private"
|
|
249
|
+
requires_confirmation: bool = False
|
|
250
|
+
undo_hint: str | None = None
|
|
251
|
+
time_hint: OpportunityTimeHint | None = None
|
|
252
|
+
related_people: list[PersonRef] = Field(default_factory=list)
|
|
253
|
+
harness_context_requests: list[HarnessContextRequest] = Field(default_factory=list)
|
|
254
|
+
evidence: list[Evidence] = Field(default_factory=list)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class CapturedPersonSummary(StrictModel):
|
|
258
|
+
person_id: str
|
|
259
|
+
label: str
|
|
260
|
+
action: Literal["created", "updated", "linked"]
|
|
261
|
+
roles: list[str] = Field(default_factory=list)
|
|
262
|
+
correction_hint: str | None = None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class CaptureSummary(StrictModel):
|
|
266
|
+
status: Literal["saved"] = "saved"
|
|
267
|
+
people: list[CapturedPersonSummary] = Field(default_factory=list)
|
|
268
|
+
details: list[str] = Field(default_factory=list)
|
|
269
|
+
follow_ups: list[str] = Field(default_factory=list)
|
|
270
|
+
reviews: list[str] = Field(default_factory=list)
|
|
271
|
+
correction_hint: str = (
|
|
272
|
+
"If anything is wrong, tell the assistant immediately, for example "
|
|
273
|
+
"'different person', 'same person', or 'remove that follow-up'."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class RecordInteractionResult(StrictModel):
|
|
278
|
+
interaction_id: str
|
|
279
|
+
saved: bool = True
|
|
280
|
+
created_people: list[str] = Field(default_factory=list)
|
|
281
|
+
updated_people: list[str] = Field(default_factory=list)
|
|
282
|
+
person_ref_map: dict[str, str] = Field(default_factory=dict)
|
|
283
|
+
needs_review: list[ReviewItem] = Field(default_factory=list)
|
|
284
|
+
evidence: list[Evidence] = Field(default_factory=list)
|
|
285
|
+
capture_summary: CaptureSummary | None = None
|
|
286
|
+
captured_follow_ups: list[FollowUpTask] = Field(default_factory=list)
|
|
287
|
+
post_capture_opportunities: list[PostCaptureOpportunity] = Field(default_factory=list)
|
|
288
|
+
harness_context_requests: list[HarnessContextRequest] = Field(default_factory=list)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class PersonMemoryRecord(StrictModel):
|
|
292
|
+
person_id: str
|
|
293
|
+
display_name: str
|
|
294
|
+
aliases: list[str] = Field(default_factory=list)
|
|
295
|
+
work_history: list[WorkHistoryRecord] = Field(default_factory=list)
|
|
296
|
+
education: list[EducationRecord] = Field(default_factory=list)
|
|
297
|
+
interests: list[str] = Field(default_factory=list)
|
|
298
|
+
important_dates: list[ImportantDate] = Field(default_factory=list)
|
|
299
|
+
contacts: list[ContactMethod] = Field(default_factory=list)
|
|
300
|
+
preferences: list[str] = Field(default_factory=list)
|
|
301
|
+
direct_facts: list[DirectFact] = Field(default_factory=list)
|
|
302
|
+
attributed_claims: list[AttributedClaim] = Field(default_factory=list)
|
|
303
|
+
relationships: list[RelationshipAssertion] = Field(default_factory=list)
|
|
304
|
+
interactions: list[SocialInteraction] = Field(default_factory=list)
|
|
305
|
+
follow_ups: list[FollowUpTask] = Field(default_factory=list)
|
|
306
|
+
evidence: list[Evidence] = Field(default_factory=list)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class PersonCard(StrictModel):
|
|
310
|
+
person_id: str
|
|
311
|
+
display_name: str
|
|
312
|
+
aliases: list[str] = Field(default_factory=list)
|
|
313
|
+
headline: str | None = None
|
|
314
|
+
summary: str | None = None
|
|
315
|
+
work_history: list[WorkHistoryRecord] = Field(default_factory=list)
|
|
316
|
+
education: list[EducationRecord] = Field(default_factory=list)
|
|
317
|
+
interests: list[str] = Field(default_factory=list)
|
|
318
|
+
important_dates: list[ImportantDate] = Field(default_factory=list)
|
|
319
|
+
contacts: list[ContactMethod] = Field(default_factory=list)
|
|
320
|
+
preferences: list[str] = Field(default_factory=list)
|
|
321
|
+
recent_interactions: list[str] = Field(default_factory=list)
|
|
322
|
+
attributed_claims: list[AttributedClaim] = Field(default_factory=list)
|
|
323
|
+
relationships: list[RelationshipAssertion] = Field(default_factory=list)
|
|
324
|
+
open_follow_ups: list[FollowUpTask] = Field(default_factory=list)
|
|
325
|
+
evidence: list[Evidence] = Field(default_factory=list)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class RetrievalItem(StrictModel):
|
|
329
|
+
item_id: str
|
|
330
|
+
kind: Literal["person", "interaction", "fact", "claim", "follow_up"]
|
|
331
|
+
title: str
|
|
332
|
+
matched_text: str
|
|
333
|
+
score: float = Field(ge=0.0)
|
|
334
|
+
why_matched: str
|
|
335
|
+
person_ids: list[str] = Field(default_factory=list)
|
|
336
|
+
sensitivity: list[SensitivityLabel] = Field(default_factory=list)
|
|
337
|
+
evidence: list[Evidence] = Field(default_factory=list)
|
|
338
|
+
is_secondhand: bool = False
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class RetrievalResponse(StrictModel):
|
|
342
|
+
query: str
|
|
343
|
+
mode: Literal["recall", "brief"] = "recall"
|
|
344
|
+
sensitivity_policy: Literal["personal", "strict", "task_aware"] = "personal"
|
|
345
|
+
output_context: Literal["private", "shareable"] = "private"
|
|
346
|
+
sensitive_results_included: bool = False
|
|
347
|
+
results: list[RetrievalItem] = Field(default_factory=list)
|
|
348
|
+
missing_info: list[str] = Field(default_factory=list)
|
|
349
|
+
needs_review: list[ReviewItem] = Field(default_factory=list)
|
|
350
|
+
ambiguous: bool = False
|
|
351
|
+
result_type: str | None = None
|
|
352
|
+
answer_policy: str | None = None
|
|
353
|
+
candidates: list[dict[str, Any]] = Field(default_factory=list)
|
|
354
|
+
final_answer_instruction: str | None = None
|
|
355
|
+
candidate_results: list[dict[str, Any]] = Field(default_factory=list)
|