@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.
Files changed (61) hide show
  1. package/README.md +476 -0
  2. package/docs/mcp_tools.md +138 -0
  3. package/harness_adapters/openclaw/mcp.managed.unix.template.json +25 -0
  4. package/harness_adapters/openclaw/mcp.managed.windows.template.json +26 -0
  5. package/harness_adapters/openclaw/mcp.template.json +14 -0
  6. package/harness_adapters/openclaw/ppl/SKILL.md +114 -0
  7. package/package.json +30 -0
  8. package/pyproject.toml +26 -0
  9. package/scripts/install_windows.ps1 +92 -0
  10. package/scripts/npm/people-memory.js +276 -0
  11. package/scripts/people_memory_bootstrap.py +247 -0
  12. package/scripts/run_graphiti_live_from_liepin.ps1 +87 -0
  13. package/scripts/run_tests_with_artifacts.ps1 +307 -0
  14. package/src/people_network_memory/__init__.py +6 -0
  15. package/src/people_network_memory/application/__init__.py +16 -0
  16. package/src/people_network_memory/application/normalization.py +1441 -0
  17. package/src/people_network_memory/application/services.py +921 -0
  18. package/src/people_network_memory/cli.py +1212 -0
  19. package/src/people_network_memory/config.py +268 -0
  20. package/src/people_network_memory/domain/__init__.py +55 -0
  21. package/src/people_network_memory/domain/identity.py +77 -0
  22. package/src/people_network_memory/domain/models.py +355 -0
  23. package/src/people_network_memory/fixtures/__init__.py +6 -0
  24. package/src/people_network_memory/fixtures/eval.py +398 -0
  25. package/src/people_network_memory/fixtures/extractor_eval.py +364 -0
  26. package/src/people_network_memory/fixtures/generator.py +290 -0
  27. package/src/people_network_memory/fixtures/report.py +252 -0
  28. package/src/people_network_memory/graphiti_adapter/__init__.py +9 -0
  29. package/src/people_network_memory/graphiti_adapter/episode_formatter.py +70 -0
  30. package/src/people_network_memory/graphiti_adapter/graphiti_store.py +655 -0
  31. package/src/people_network_memory/graphiti_adapter/indexer.py +194 -0
  32. package/src/people_network_memory/graphiti_adapter/ontology.py +68 -0
  33. package/src/people_network_memory/harness_adapters/__init__.py +2 -0
  34. package/src/people_network_memory/harness_adapters/openclaw/__init__.py +9 -0
  35. package/src/people_network_memory/harness_adapters/openclaw/installer.py +577 -0
  36. package/src/people_network_memory/harness_adapters/openclaw/integration_eval.py +508 -0
  37. package/src/people_network_memory/harness_adapters/openclaw/smoke.py +292 -0
  38. package/src/people_network_memory/infrastructure/__init__.py +2 -0
  39. package/src/people_network_memory/infrastructure/archive_backup.py +171 -0
  40. package/src/people_network_memory/infrastructure/diagnostics.py +171 -0
  41. package/src/people_network_memory/infrastructure/embeddings.py +155 -0
  42. package/src/people_network_memory/infrastructure/file_store.py +129 -0
  43. package/src/people_network_memory/infrastructure/graphiti_promotion.py +212 -0
  44. package/src/people_network_memory/infrastructure/id_generator.py +40 -0
  45. package/src/people_network_memory/infrastructure/in_memory_store.py +1008 -0
  46. package/src/people_network_memory/infrastructure/llm_extractor.py +476 -0
  47. package/src/people_network_memory/infrastructure/llm_identity_advisor.py +200 -0
  48. package/src/people_network_memory/infrastructure/llm_judge.py +162 -0
  49. package/src/people_network_memory/infrastructure/redaction.py +21 -0
  50. package/src/people_network_memory/infrastructure/release_check.py +186 -0
  51. package/src/people_network_memory/infrastructure/retrieval_intent.py +98 -0
  52. package/src/people_network_memory/infrastructure/semantic_index.py +262 -0
  53. package/src/people_network_memory/mcp_server/__init__.py +2 -0
  54. package/src/people_network_memory/mcp_server/contracts.py +85 -0
  55. package/src/people_network_memory/mcp_server/runtime.py +133 -0
  56. package/src/people_network_memory/mcp_server/tools.py +588 -0
  57. package/src/people_network_memory/ports/__init__.py +2 -0
  58. package/src/people_network_memory/ports/errors.py +25 -0
  59. package/src/people_network_memory/ports/interfaces.py +103 -0
  60. package/src/people_network_memory/projection/__init__.py +6 -0
  61. 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)
@@ -0,0 +1,6 @@
1
+ """Deterministic mock data for testing and demos."""
2
+
3
+ from people_network_memory.fixtures.generator import MockDataset, generate_mock_dataset
4
+
5
+ __all__ = ["MockDataset", "generate_mock_dataset"]
6
+