@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,262 @@
|
|
|
1
|
+
"""Local semantic sidecar for projection-cache recall.
|
|
2
|
+
|
|
3
|
+
The graph backend owns graph extraction and graph search. This sidecar keeps
|
|
4
|
+
small, evidence-backed interaction chunks searchable by embedding so vague
|
|
5
|
+
event-level queries do not depend only on Graphiti edge granularity.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
from people_network_memory.domain.models import (
|
|
19
|
+
Evidence,
|
|
20
|
+
RetrievalItem,
|
|
21
|
+
SensitivityLabel,
|
|
22
|
+
SocialInteraction,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
EmbedTexts = Callable[[list[str]], list[list[float]]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class SemanticIndexEntry:
|
|
31
|
+
entry_id: str
|
|
32
|
+
text: str
|
|
33
|
+
embedding: list[float]
|
|
34
|
+
source_text: str
|
|
35
|
+
recorded_at: datetime
|
|
36
|
+
person_ids: list[str]
|
|
37
|
+
sensitivity: list[SensitivityLabel]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SemanticProjectionIndex:
|
|
41
|
+
def __init__(self, path: Path) -> None:
|
|
42
|
+
self._path = path
|
|
43
|
+
self._entries: dict[str, SemanticIndexEntry] = {}
|
|
44
|
+
self._load()
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def path(self) -> Path:
|
|
48
|
+
return self._path
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def entries(self) -> list[SemanticIndexEntry]:
|
|
52
|
+
return list(self._entries.values())
|
|
53
|
+
|
|
54
|
+
def build_from_interactions(
|
|
55
|
+
self,
|
|
56
|
+
interactions: list[SocialInteraction],
|
|
57
|
+
*,
|
|
58
|
+
embed_texts: EmbedTexts,
|
|
59
|
+
batch_size: int = 16,
|
|
60
|
+
reset: bool = False,
|
|
61
|
+
) -> dict[str, object]:
|
|
62
|
+
if reset:
|
|
63
|
+
self._entries = {}
|
|
64
|
+
chunks = [
|
|
65
|
+
_interaction_chunk(interaction)
|
|
66
|
+
for interaction in interactions
|
|
67
|
+
if _interaction_chunk(interaction)["entry_id"] not in self._entries
|
|
68
|
+
]
|
|
69
|
+
embedded = 0
|
|
70
|
+
for start in range(0, len(chunks), batch_size):
|
|
71
|
+
batch = chunks[start : start + batch_size]
|
|
72
|
+
vectors = embed_texts([str(chunk["text"]) for chunk in batch])
|
|
73
|
+
if len(vectors) != len(batch):
|
|
74
|
+
raise ValueError("embedding provider returned the wrong number of vectors")
|
|
75
|
+
for chunk, vector in zip(batch, vectors):
|
|
76
|
+
labels = [
|
|
77
|
+
SensitivityLabel(label)
|
|
78
|
+
for label in chunk["sensitivity"]
|
|
79
|
+
if label in {item.value for item in SensitivityLabel}
|
|
80
|
+
]
|
|
81
|
+
self._entries[str(chunk["entry_id"])] = SemanticIndexEntry(
|
|
82
|
+
entry_id=str(chunk["entry_id"]),
|
|
83
|
+
text=str(chunk["text"]),
|
|
84
|
+
embedding=[float(value) for value in vector],
|
|
85
|
+
source_text=str(chunk["source_text"]),
|
|
86
|
+
recorded_at=chunk["recorded_at"], # type: ignore[arg-type]
|
|
87
|
+
person_ids=list(chunk["person_ids"]), # type: ignore[arg-type]
|
|
88
|
+
sensitivity=labels,
|
|
89
|
+
)
|
|
90
|
+
embedded += 1
|
|
91
|
+
self._flush()
|
|
92
|
+
self._flush()
|
|
93
|
+
return {
|
|
94
|
+
"ok": True,
|
|
95
|
+
"target_index": str(self._path),
|
|
96
|
+
"attempted": len(interactions),
|
|
97
|
+
"embedded": embedded,
|
|
98
|
+
"skipped_existing": len(interactions) - len(chunks),
|
|
99
|
+
"entries": len(self._entries),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def search(
|
|
103
|
+
self,
|
|
104
|
+
query: str,
|
|
105
|
+
*,
|
|
106
|
+
embed_texts: EmbedTexts,
|
|
107
|
+
limit: int,
|
|
108
|
+
include_sensitive: bool = False,
|
|
109
|
+
) -> list[RetrievalItem]:
|
|
110
|
+
if not self._entries:
|
|
111
|
+
return []
|
|
112
|
+
query_vectors = embed_texts([query])
|
|
113
|
+
if not query_vectors:
|
|
114
|
+
return []
|
|
115
|
+
query_vector = query_vectors[0]
|
|
116
|
+
scored: list[tuple[float, SemanticIndexEntry]] = []
|
|
117
|
+
for entry in self._entries.values():
|
|
118
|
+
if _blocked(entry.sensitivity, include_sensitive):
|
|
119
|
+
continue
|
|
120
|
+
score = _cosine(query_vector, entry.embedding)
|
|
121
|
+
if score <= 0:
|
|
122
|
+
continue
|
|
123
|
+
scored.append((score, entry))
|
|
124
|
+
scored.sort(key=lambda item: item[0], reverse=True)
|
|
125
|
+
return [
|
|
126
|
+
RetrievalItem(
|
|
127
|
+
item_id=f"semantic:{entry.entry_id}",
|
|
128
|
+
kind="interaction",
|
|
129
|
+
title="Semantic interaction match",
|
|
130
|
+
matched_text=entry.text,
|
|
131
|
+
score=max(score, 0.0) * 2.0,
|
|
132
|
+
why_matched="Matched by local semantic projection index.",
|
|
133
|
+
person_ids=entry.person_ids,
|
|
134
|
+
sensitivity=entry.sensitivity,
|
|
135
|
+
evidence=[
|
|
136
|
+
Evidence(
|
|
137
|
+
evidence_id=entry.entry_id,
|
|
138
|
+
source_text=entry.source_text,
|
|
139
|
+
recorded_at=entry.recorded_at,
|
|
140
|
+
)
|
|
141
|
+
],
|
|
142
|
+
is_secondhand=SensitivityLabel.SECONDHAND in entry.sensitivity,
|
|
143
|
+
)
|
|
144
|
+
for score, entry in scored[:limit]
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
def _load(self) -> None:
|
|
148
|
+
if not self._path.exists():
|
|
149
|
+
return
|
|
150
|
+
payload = json.loads(self._path.read_text(encoding="utf-8"))
|
|
151
|
+
for item in payload.get("entries", []):
|
|
152
|
+
labels = [
|
|
153
|
+
SensitivityLabel(label)
|
|
154
|
+
for label in item.get("sensitivity", [])
|
|
155
|
+
if label in {entry.value for entry in SensitivityLabel}
|
|
156
|
+
]
|
|
157
|
+
self._entries[item["entry_id"]] = SemanticIndexEntry(
|
|
158
|
+
entry_id=item["entry_id"],
|
|
159
|
+
text=item["text"],
|
|
160
|
+
embedding=[float(value) for value in item["embedding"]],
|
|
161
|
+
source_text=item["source_text"],
|
|
162
|
+
recorded_at=datetime.fromisoformat(item["recorded_at"]),
|
|
163
|
+
person_ids=list(item.get("person_ids", [])),
|
|
164
|
+
sensitivity=labels,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _flush(self) -> None:
|
|
168
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
payload = {
|
|
170
|
+
"schema_version": 1,
|
|
171
|
+
"entries": [
|
|
172
|
+
{
|
|
173
|
+
"entry_id": entry.entry_id,
|
|
174
|
+
"text": entry.text,
|
|
175
|
+
"embedding": entry.embedding,
|
|
176
|
+
"source_text": entry.source_text,
|
|
177
|
+
"recorded_at": entry.recorded_at.isoformat(),
|
|
178
|
+
"person_ids": entry.person_ids,
|
|
179
|
+
"sensitivity": [label.value for label in entry.sensitivity],
|
|
180
|
+
}
|
|
181
|
+
for entry in self._entries.values()
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
self._path.write_text(
|
|
185
|
+
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True),
|
|
186
|
+
encoding="utf-8",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _interaction_chunk(interaction: SocialInteraction) -> dict[str, object]:
|
|
191
|
+
participants = [participant.person.label for participant in interaction.participants]
|
|
192
|
+
mentioned = [
|
|
193
|
+
f"{mentioned.mentioned_by.label} mentioned {mentioned.person.label}"
|
|
194
|
+
if mentioned.mentioned_by
|
|
195
|
+
else mentioned.person.label
|
|
196
|
+
for mentioned in interaction.mentioned_people
|
|
197
|
+
]
|
|
198
|
+
direct_facts = [
|
|
199
|
+
f"{fact.subject.label} {fact.predicate} {fact.value}"
|
|
200
|
+
for fact in interaction.direct_facts
|
|
201
|
+
]
|
|
202
|
+
follow_ups = [follow_up.description for follow_up in interaction.follow_ups]
|
|
203
|
+
person_ids = sorted(
|
|
204
|
+
{
|
|
205
|
+
ref.person_id
|
|
206
|
+
for ref in [
|
|
207
|
+
*[participant.person for participant in interaction.participants],
|
|
208
|
+
*[mentioned.person for mentioned in interaction.mentioned_people],
|
|
209
|
+
]
|
|
210
|
+
if ref.person_id
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
sensitivity = set(interaction.sensitivity)
|
|
214
|
+
for claim in interaction.attributed_claims:
|
|
215
|
+
sensitivity.update(claim.sensitivity)
|
|
216
|
+
for fact in interaction.direct_facts:
|
|
217
|
+
sensitivity.update(fact.sensitivity)
|
|
218
|
+
parts = [
|
|
219
|
+
interaction.source_text,
|
|
220
|
+
f"Place: {interaction.place}" if interaction.place else "",
|
|
221
|
+
"Participants: " + ", ".join(participants) if participants else "",
|
|
222
|
+
"Mentioned: " + ", ".join(mentioned) if mentioned else "",
|
|
223
|
+
"Topics: " + ", ".join(interaction.topics) if interaction.topics else "",
|
|
224
|
+
"Direct facts: " + "; ".join(direct_facts) if direct_facts else "",
|
|
225
|
+
"Follow-ups: " + "; ".join(follow_ups) if follow_ups else "",
|
|
226
|
+
]
|
|
227
|
+
text = "\n".join(part for part in parts if part)
|
|
228
|
+
recorded_at = interaction.occurred_at or datetime.now(timezone.utc)
|
|
229
|
+
return {
|
|
230
|
+
"entry_id": hashlib.sha256(
|
|
231
|
+
interaction.model_dump_json().encode("utf-8")
|
|
232
|
+
).hexdigest(),
|
|
233
|
+
"text": text,
|
|
234
|
+
"source_text": interaction.source_text,
|
|
235
|
+
"recorded_at": recorded_at,
|
|
236
|
+
"person_ids": person_ids,
|
|
237
|
+
"sensitivity": [label.value for label in sorted(sensitivity, key=lambda item: item.value)],
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _cosine(left: list[float], right: list[float]) -> float:
|
|
242
|
+
if not left or not right or len(left) != len(right):
|
|
243
|
+
return 0.0
|
|
244
|
+
dot = sum(a * b for a, b in zip(left, right))
|
|
245
|
+
left_norm = math.sqrt(sum(value * value for value in left))
|
|
246
|
+
right_norm = math.sqrt(sum(value * value for value in right))
|
|
247
|
+
if left_norm == 0 or right_norm == 0:
|
|
248
|
+
return 0.0
|
|
249
|
+
return dot / (left_norm * right_norm)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _blocked(labels: list[SensitivityLabel], include_sensitive: bool) -> bool:
|
|
253
|
+
if include_sensitive:
|
|
254
|
+
return False
|
|
255
|
+
return any(
|
|
256
|
+
label
|
|
257
|
+
in {
|
|
258
|
+
SensitivityLabel.SENSITIVE,
|
|
259
|
+
SensitivityLabel.DO_NOT_SURFACE_UNPROMPTED,
|
|
260
|
+
}
|
|
261
|
+
for label in labels
|
|
262
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Public V1 MCP tool contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, model_validator
|
|
8
|
+
|
|
9
|
+
from people_network_memory.domain.models import (
|
|
10
|
+
PersonCard,
|
|
11
|
+
RecordInteractionResult,
|
|
12
|
+
RetrievalResponse,
|
|
13
|
+
SocialInteraction,
|
|
14
|
+
StrictModel,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CONTRACT_VERSION = "v1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RetrieveNetworkContextInput(StrictModel):
|
|
22
|
+
query: str = Field(min_length=1)
|
|
23
|
+
limit: int = Field(default=10, ge=1, le=50)
|
|
24
|
+
include_sensitive: bool | None = None
|
|
25
|
+
sensitivity_policy: Literal["personal", "strict", "task_aware"] | None = None
|
|
26
|
+
output_context: Literal["private", "shareable"] = "private"
|
|
27
|
+
mode: Literal["recall", "brief"] = "recall"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GetPersonInput(StrictModel):
|
|
31
|
+
person_id: str | None = Field(default=None, min_length=1)
|
|
32
|
+
name: str | None = Field(default=None, min_length=1)
|
|
33
|
+
|
|
34
|
+
@model_validator(mode="after")
|
|
35
|
+
def require_identifier(self) -> "GetPersonInput":
|
|
36
|
+
if not (self.person_id or self.name):
|
|
37
|
+
raise ValueError("get_person requires either person_id or name")
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PersonCardFoundResponse(PersonCard):
|
|
42
|
+
found: bool = True
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PersonCardMissingResponse(StrictModel):
|
|
46
|
+
person_id: str | None = None
|
|
47
|
+
name: str | None = None
|
|
48
|
+
found: bool = False
|
|
49
|
+
missing_info: list[str] = Field(default_factory=list)
|
|
50
|
+
candidates: list[dict[str, Any]] = Field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def public_tool_names() -> list[str]:
|
|
54
|
+
return ["record_interaction", "retrieve_network_context", "get_person"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def public_tool_contracts() -> list[dict[str, Any]]:
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
"name": "record_interaction",
|
|
61
|
+
"version": CONTRACT_VERSION,
|
|
62
|
+
"description": "Capture a social interaction or memory note.",
|
|
63
|
+
"input_schema": SocialInteraction.model_json_schema(),
|
|
64
|
+
"output_schema": RecordInteractionResult.model_json_schema(),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "retrieve_network_context",
|
|
68
|
+
"version": CONTRACT_VERSION,
|
|
69
|
+
"description": "Retrieve relevant social-memory context from a vague query.",
|
|
70
|
+
"input_schema": RetrieveNetworkContextInput.model_json_schema(),
|
|
71
|
+
"output_schema": RetrievalResponse.model_json_schema(),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "get_person",
|
|
75
|
+
"version": CONTRACT_VERSION,
|
|
76
|
+
"description": "Return one projected person card by stable person_id, or by name as a best-effort harness fallback.",
|
|
77
|
+
"input_schema": GetPersonInput.model_json_schema(),
|
|
78
|
+
"output_schema": {
|
|
79
|
+
"oneOf": [
|
|
80
|
+
PersonCardFoundResponse.model_json_schema(),
|
|
81
|
+
PersonCardMissingResponse.model_json_schema(),
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Runtime wiring for CLI and MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from people_network_memory.application.services import (
|
|
9
|
+
BuildPersonCardService,
|
|
10
|
+
RecordInteractionService,
|
|
11
|
+
RetrieveContextService,
|
|
12
|
+
ReviewWorkflowService,
|
|
13
|
+
)
|
|
14
|
+
from people_network_memory.config import PeopleMemoryConfig
|
|
15
|
+
from people_network_memory.graphiti_adapter.graphiti_store import GraphitiGraphStore
|
|
16
|
+
from people_network_memory.infrastructure.file_store import JsonPeopleStore
|
|
17
|
+
from people_network_memory.infrastructure.id_generator import SequentialIdGenerator
|
|
18
|
+
from people_network_memory.infrastructure.in_memory_store import InMemoryPeopleStore
|
|
19
|
+
from people_network_memory.mcp_server.tools import PeopleMemoryTools
|
|
20
|
+
from people_network_memory.projection.builders import DefaultPersonProjector
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class PeopleMemoryRuntime:
|
|
25
|
+
tools: PeopleMemoryTools
|
|
26
|
+
store: Any
|
|
27
|
+
record_service: RecordInteractionService
|
|
28
|
+
retrieve_service: RetrieveContextService
|
|
29
|
+
review_workflow: ReviewWorkflowService
|
|
30
|
+
|
|
31
|
+
def close(self) -> None:
|
|
32
|
+
close_store = getattr(self.store, "close", None)
|
|
33
|
+
if callable(close_store):
|
|
34
|
+
close_store()
|
|
35
|
+
|
|
36
|
+
def run_stdio(self) -> None:
|
|
37
|
+
from mcp.server.fastmcp import FastMCP
|
|
38
|
+
|
|
39
|
+
mcp = FastMCP("people-network-memory")
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
def record_interaction(payload: dict[str, Any]) -> dict[str, Any]:
|
|
43
|
+
"""Capture a messy social interaction note."""
|
|
44
|
+
|
|
45
|
+
return self.tools.record_interaction(payload)
|
|
46
|
+
|
|
47
|
+
@mcp.tool()
|
|
48
|
+
def retrieve_network_context(payload: dict[str, Any]) -> dict[str, Any]:
|
|
49
|
+
"""Retrieve relevant personal network context from a vague query."""
|
|
50
|
+
|
|
51
|
+
return self.tools.retrieve_network_context(payload)
|
|
52
|
+
|
|
53
|
+
@mcp.tool()
|
|
54
|
+
def get_person(payload: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
"""Return a projected person card."""
|
|
56
|
+
|
|
57
|
+
return self.tools.get_person(payload)
|
|
58
|
+
|
|
59
|
+
mcp.run()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_runtime(config: PeopleMemoryConfig) -> PeopleMemoryRuntime:
|
|
63
|
+
config.validate_runtime()
|
|
64
|
+
store = _build_store(config)
|
|
65
|
+
ids = getattr(store, "id_generator", SequentialIdGenerator())
|
|
66
|
+
projector = DefaultPersonProjector()
|
|
67
|
+
record_service = RecordInteractionService(
|
|
68
|
+
memory_store=store,
|
|
69
|
+
identity_index=store,
|
|
70
|
+
review_queue=store,
|
|
71
|
+
id_generator=ids,
|
|
72
|
+
interaction_extractor=_build_interaction_extractor(config),
|
|
73
|
+
identity_advisor=_build_identity_advisor(config),
|
|
74
|
+
)
|
|
75
|
+
retrieve_service = RetrieveContextService(
|
|
76
|
+
graph_search=store,
|
|
77
|
+
review_queue=store,
|
|
78
|
+
default_sensitivity_policy=config.sensitivity_policy,
|
|
79
|
+
retrieval_judge=_build_retrieval_judge(config),
|
|
80
|
+
)
|
|
81
|
+
card_service = BuildPersonCardService(memory_store=store, projector=projector)
|
|
82
|
+
review_workflow = ReviewWorkflowService(memory_store=store, review_queue=store)
|
|
83
|
+
return PeopleMemoryRuntime(
|
|
84
|
+
tools=PeopleMemoryTools(
|
|
85
|
+
record_interaction_service=record_service,
|
|
86
|
+
retrieve_context_service=retrieve_service,
|
|
87
|
+
build_person_card_service=card_service,
|
|
88
|
+
),
|
|
89
|
+
store=store,
|
|
90
|
+
record_service=record_service,
|
|
91
|
+
retrieve_service=retrieve_service,
|
|
92
|
+
review_workflow=review_workflow,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_store(config: PeopleMemoryConfig) -> InMemoryPeopleStore | JsonPeopleStore | GraphitiGraphStore:
|
|
97
|
+
if config.backend == "graphiti":
|
|
98
|
+
return GraphitiGraphStore.from_config(config)
|
|
99
|
+
if config.backend == "local_json":
|
|
100
|
+
return JsonPeopleStore.from_config(config)
|
|
101
|
+
return InMemoryPeopleStore()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _build_retrieval_judge(config: PeopleMemoryConfig) -> Any | None:
|
|
105
|
+
if config.retrieval_judge != "llm":
|
|
106
|
+
return None
|
|
107
|
+
from people_network_memory.infrastructure.llm_judge import OpenAICompatibleRetrievalJudge
|
|
108
|
+
|
|
109
|
+
return OpenAICompatibleRetrievalJudge.from_config(config)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _build_interaction_extractor(config: PeopleMemoryConfig) -> Any | None:
|
|
113
|
+
if config.ingestion_extractor != "llm":
|
|
114
|
+
return None
|
|
115
|
+
if not config.has_llm_settings():
|
|
116
|
+
return None
|
|
117
|
+
from people_network_memory.infrastructure.llm_extractor import (
|
|
118
|
+
OpenAICompatibleInteractionExtractor,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return OpenAICompatibleInteractionExtractor.from_config(config)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _build_identity_advisor(config: PeopleMemoryConfig) -> Any | None:
|
|
125
|
+
if config.identity_advisor != "llm":
|
|
126
|
+
return None
|
|
127
|
+
if not config.has_llm_settings():
|
|
128
|
+
return None
|
|
129
|
+
from people_network_memory.infrastructure.llm_identity_advisor import (
|
|
130
|
+
OpenAICompatibleIdentityAdvisor,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return OpenAICompatibleIdentityAdvisor.from_config(config)
|