@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,364 @@
|
|
|
1
|
+
"""Evaluation fixtures for the LLM ingestion extractor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from people_network_memory.domain.models import SocialInteraction
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExtractorLike(Protocol):
|
|
13
|
+
def extract(self, interaction: SocialInteraction) -> SocialInteraction:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ExtractorExpected:
|
|
19
|
+
participants: list[str] = field(default_factory=list)
|
|
20
|
+
mentioned_people: list[str] = field(default_factory=list)
|
|
21
|
+
aliases: dict[str, list[str]] = field(default_factory=dict)
|
|
22
|
+
follow_up_terms: list[list[str]] = field(default_factory=list)
|
|
23
|
+
forbidden_follow_up_terms: list[list[str]] = field(default_factory=list)
|
|
24
|
+
follow_up_due_dates: list[str] = field(default_factory=list)
|
|
25
|
+
attributed_claim_terms: list[list[str]] = field(default_factory=list)
|
|
26
|
+
direct_fact_terms: list[list[str]] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ExtractorEvalCase:
|
|
31
|
+
case_id: str
|
|
32
|
+
source_text: str
|
|
33
|
+
expected: ExtractorExpected
|
|
34
|
+
occurred_at: datetime | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
EXTRACTOR_EVAL_CASES: list[ExtractorEvalCase] = [
|
|
38
|
+
ExtractorEvalCase(
|
|
39
|
+
case_id="zh_promise_explicit",
|
|
40
|
+
source_text=(
|
|
41
|
+
"今天在Manner Coffee见了测试赵小北,她在北辰智能负责人形机器人"
|
|
42
|
+
"感知算法招聘,喜欢微信联系。我答应下周三上午10点给她发三位"
|
|
43
|
+
"感知算法候选人。"
|
|
44
|
+
),
|
|
45
|
+
occurred_at=datetime(2026, 5, 15, tzinfo=timezone.utc),
|
|
46
|
+
expected=ExtractorExpected(
|
|
47
|
+
participants=["测试赵小北"],
|
|
48
|
+
follow_up_terms=[["测试赵小北", "三位", "感知算法候选人"]],
|
|
49
|
+
follow_up_due_dates=["2026-05-20"],
|
|
50
|
+
direct_fact_terms=[["测试赵小北", "北辰智能"], ["测试赵小北", "微信"]],
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
ExtractorEvalCase(
|
|
54
|
+
case_id="zh_commitment_soft",
|
|
55
|
+
source_text=(
|
|
56
|
+
"在三顿半碰到测试林小雨,她在星河机器人做人形机器人算法招聘。"
|
|
57
|
+
"我回头把两个感知算法候选人发给她,最好下周前。"
|
|
58
|
+
),
|
|
59
|
+
occurred_at=datetime(2026, 5, 15, tzinfo=timezone.utc),
|
|
60
|
+
expected=ExtractorExpected(
|
|
61
|
+
participants=["测试林小雨"],
|
|
62
|
+
follow_up_terms=[["两个", "感知算法候选人"]],
|
|
63
|
+
direct_fact_terms=[["测试林小雨", "星河机器人"]],
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
ExtractorEvalCase(
|
|
67
|
+
case_id="zh_asked_me",
|
|
68
|
+
source_text=(
|
|
69
|
+
"今天和测试陈一诺聊了云栖机器人的控制算法招聘,"
|
|
70
|
+
"她让我明天下午3点前发两个控制算法候选人。"
|
|
71
|
+
),
|
|
72
|
+
occurred_at=datetime(2026, 5, 15, tzinfo=timezone.utc),
|
|
73
|
+
expected=ExtractorExpected(
|
|
74
|
+
participants=["测试陈一诺"],
|
|
75
|
+
follow_up_terms=[["两个", "控制算法候选人"]],
|
|
76
|
+
follow_up_due_dates=["2026-05-16"],
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
ExtractorEvalCase(
|
|
80
|
+
case_id="zh_alias",
|
|
81
|
+
source_text=(
|
|
82
|
+
"今天在潘家园见了测试王小明(阿明),聊了北京机器人项目。"
|
|
83
|
+
"他在摸金科技做AI硬件。"
|
|
84
|
+
),
|
|
85
|
+
expected=ExtractorExpected(
|
|
86
|
+
participants=["测试王小明"],
|
|
87
|
+
aliases={"测试王小明": ["阿明"]},
|
|
88
|
+
direct_fact_terms=[["测试王小明", "摸金科技"], ["测试王小明", "AI硬件"]],
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
ExtractorEvalCase(
|
|
92
|
+
case_id="zh_contact_preference_not_follow_up",
|
|
93
|
+
source_text=(
|
|
94
|
+
"今天又见了测试王小明(阿明),他现在也在关注具身智能芯片供应链,"
|
|
95
|
+
"之后最好用邮件联系。"
|
|
96
|
+
),
|
|
97
|
+
expected=ExtractorExpected(
|
|
98
|
+
participants=["测试王小明"],
|
|
99
|
+
aliases={"测试王小明": ["阿明"]},
|
|
100
|
+
forbidden_follow_up_terms=[["邮件联系"]],
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
ExtractorEvalCase(
|
|
104
|
+
case_id="zh_secondhand_claim",
|
|
105
|
+
source_text=(
|
|
106
|
+
"今天在% Arabica见了测试陈一诺,她说测试王凯可能要离开云栖机器人。"
|
|
107
|
+
"陈一诺自己在云栖机器人做控制算法招聘。"
|
|
108
|
+
),
|
|
109
|
+
expected=ExtractorExpected(
|
|
110
|
+
participants=["测试陈一诺"],
|
|
111
|
+
mentioned_people=["测试王凯"],
|
|
112
|
+
attributed_claim_terms=[["测试陈一诺", "测试王凯", "可能要离开"]],
|
|
113
|
+
direct_fact_terms=[["测试陈一诺", "云栖机器人"]],
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
ExtractorEvalCase(
|
|
117
|
+
case_id="en_need_to_follow_up",
|
|
118
|
+
source_text=(
|
|
119
|
+
"Met Alice Zhang at Blue Bottle. We discussed robotics hiring. "
|
|
120
|
+
"Need to send her two founder intros next week."
|
|
121
|
+
),
|
|
122
|
+
expected=ExtractorExpected(
|
|
123
|
+
participants=["Alice Zhang"],
|
|
124
|
+
follow_up_terms=[["Alice Zhang", "two founder intros"]],
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
ExtractorEvalCase(
|
|
128
|
+
case_id="en_asked_me",
|
|
129
|
+
source_text=(
|
|
130
|
+
"Had coffee with Clara Wu at % Arabica. Clara asked me to WhatsApp "
|
|
131
|
+
"her the AI hardware shortlist tomorrow."
|
|
132
|
+
),
|
|
133
|
+
occurred_at=datetime(2026, 5, 15, tzinfo=timezone.utc),
|
|
134
|
+
expected=ExtractorExpected(
|
|
135
|
+
participants=["Clara Wu"],
|
|
136
|
+
follow_up_terms=[["WhatsApp", "AI hardware shortlist"]],
|
|
137
|
+
follow_up_due_dates=["2026-05-16"],
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
ExtractorEvalCase(
|
|
141
|
+
case_id="en_mentioned_not_present",
|
|
142
|
+
source_text=(
|
|
143
|
+
"Met Alice Zhang at Seesaw. Alice mentioned Bob Li for founder intros, "
|
|
144
|
+
"but Bob was not there."
|
|
145
|
+
),
|
|
146
|
+
expected=ExtractorExpected(
|
|
147
|
+
participants=["Alice Zhang"],
|
|
148
|
+
mentioned_people=["Bob Li"],
|
|
149
|
+
attributed_claim_terms=[["Alice", "Bob Li", "founder intros"]],
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def evaluate_extractor(
|
|
156
|
+
extractor: ExtractorLike,
|
|
157
|
+
*,
|
|
158
|
+
cases: list[ExtractorEvalCase] | None = None,
|
|
159
|
+
max_cases: int | None = None,
|
|
160
|
+
) -> dict[str, object]:
|
|
161
|
+
selected = list(cases or EXTRACTOR_EVAL_CASES)
|
|
162
|
+
if max_cases is not None:
|
|
163
|
+
selected = selected[:max_cases]
|
|
164
|
+
case_payloads: list[dict[str, object]] = []
|
|
165
|
+
passed = 0
|
|
166
|
+
for case in selected:
|
|
167
|
+
interaction = SocialInteraction(
|
|
168
|
+
source_text=case.source_text,
|
|
169
|
+
occurred_at=case.occurred_at,
|
|
170
|
+
)
|
|
171
|
+
actual = extractor.extract(interaction)
|
|
172
|
+
checks = _case_checks(case.expected, actual)
|
|
173
|
+
ok = all(item["ok"] for item in checks)
|
|
174
|
+
passed += int(ok)
|
|
175
|
+
case_payloads.append(
|
|
176
|
+
{
|
|
177
|
+
"case_id": case.case_id,
|
|
178
|
+
"ok": ok,
|
|
179
|
+
"source_text": case.source_text,
|
|
180
|
+
"expected": _expected_payload(case.expected),
|
|
181
|
+
"actual": _actual_payload(actual),
|
|
182
|
+
"checks": checks,
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
checked = len(selected)
|
|
186
|
+
return {
|
|
187
|
+
"ok": passed == checked,
|
|
188
|
+
"checked": checked,
|
|
189
|
+
"passed": passed,
|
|
190
|
+
"failed": checked - passed,
|
|
191
|
+
"pass_rate": (passed / checked) if checked else 0.0,
|
|
192
|
+
"cases": case_payloads,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _case_checks(
|
|
197
|
+
expected: ExtractorExpected, actual: SocialInteraction
|
|
198
|
+
) -> list[dict[str, object]]:
|
|
199
|
+
checks: list[dict[str, object]] = []
|
|
200
|
+
participant_labels = [item.person.label for item in actual.participants]
|
|
201
|
+
mentioned_labels = [item.person.label for item in actual.mentioned_people]
|
|
202
|
+
follow_ups = [
|
|
203
|
+
" ".join(
|
|
204
|
+
[
|
|
205
|
+
item.description,
|
|
206
|
+
*[person.label for person in item.related_people],
|
|
207
|
+
]
|
|
208
|
+
)
|
|
209
|
+
for item in actual.follow_ups
|
|
210
|
+
]
|
|
211
|
+
due_dates = [item.due_at.isoformat() for item in actual.follow_ups if item.due_at]
|
|
212
|
+
claims = [
|
|
213
|
+
" ".join(
|
|
214
|
+
part
|
|
215
|
+
for part in [
|
|
216
|
+
item.speaker.label if item.speaker else "",
|
|
217
|
+
item.subject.label if item.subject else "",
|
|
218
|
+
item.claim_text,
|
|
219
|
+
]
|
|
220
|
+
if part
|
|
221
|
+
)
|
|
222
|
+
for item in actual.attributed_claims
|
|
223
|
+
]
|
|
224
|
+
mention_claim_equivalents = [
|
|
225
|
+
" ".join(
|
|
226
|
+
part
|
|
227
|
+
for part in [
|
|
228
|
+
item.mentioned_by.label if item.mentioned_by else "",
|
|
229
|
+
item.person.label,
|
|
230
|
+
item.context or "",
|
|
231
|
+
]
|
|
232
|
+
if part
|
|
233
|
+
)
|
|
234
|
+
for item in actual.mentioned_people
|
|
235
|
+
]
|
|
236
|
+
facts = [
|
|
237
|
+
" ".join([item.subject.label, item.predicate, item.value, *map(str, item.metadata.values())])
|
|
238
|
+
for item in actual.direct_facts
|
|
239
|
+
]
|
|
240
|
+
for label in expected.participants:
|
|
241
|
+
checks.append(
|
|
242
|
+
_check(
|
|
243
|
+
"participant",
|
|
244
|
+
label,
|
|
245
|
+
_contains_label(participant_labels, label),
|
|
246
|
+
participant_labels,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
for label in expected.mentioned_people:
|
|
250
|
+
checks.append(
|
|
251
|
+
_check(
|
|
252
|
+
"mentioned_person",
|
|
253
|
+
label,
|
|
254
|
+
_contains_label(mentioned_labels, label),
|
|
255
|
+
mentioned_labels,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
for label, aliases in expected.aliases.items():
|
|
259
|
+
actual_aliases = {
|
|
260
|
+
item.person.label: item.person.aliases for item in actual.participants
|
|
261
|
+
}
|
|
262
|
+
for alias in aliases:
|
|
263
|
+
checks.append(
|
|
264
|
+
_check(
|
|
265
|
+
"alias",
|
|
266
|
+
f"{label} -> {alias}",
|
|
267
|
+
alias in actual_aliases.get(label, []),
|
|
268
|
+
actual_aliases,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
for terms in expected.follow_up_terms:
|
|
272
|
+
checks.append(
|
|
273
|
+
_check(
|
|
274
|
+
"follow_up_terms",
|
|
275
|
+
terms,
|
|
276
|
+
_any_text_has_terms(follow_ups, terms),
|
|
277
|
+
follow_ups,
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
for terms in expected.forbidden_follow_up_terms:
|
|
281
|
+
checks.append(
|
|
282
|
+
_check(
|
|
283
|
+
"forbidden_follow_up_terms",
|
|
284
|
+
terms,
|
|
285
|
+
not _any_text_has_terms(follow_ups, terms),
|
|
286
|
+
follow_ups,
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
for due_date in expected.follow_up_due_dates:
|
|
290
|
+
checks.append(
|
|
291
|
+
_check("follow_up_due_at", due_date, due_date in due_dates, due_dates)
|
|
292
|
+
)
|
|
293
|
+
for terms in expected.attributed_claim_terms:
|
|
294
|
+
checks.append(
|
|
295
|
+
_check(
|
|
296
|
+
"attributed_claim_terms",
|
|
297
|
+
terms,
|
|
298
|
+
_any_text_has_terms(claims + mention_claim_equivalents, terms),
|
|
299
|
+
claims + mention_claim_equivalents,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
for terms in expected.direct_fact_terms:
|
|
303
|
+
checks.append(
|
|
304
|
+
_check(
|
|
305
|
+
"direct_fact_terms",
|
|
306
|
+
terms,
|
|
307
|
+
_any_text_has_terms(facts, terms),
|
|
308
|
+
facts,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
return checks
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _contains_label(labels: list[str], expected: str) -> bool:
|
|
315
|
+
expected_folded = expected.casefold()
|
|
316
|
+
return any(
|
|
317
|
+
expected_folded == label.casefold() or expected_folded in label.casefold()
|
|
318
|
+
for label in labels
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _any_text_has_terms(texts: list[str], terms: list[str]) -> bool:
|
|
323
|
+
return any(
|
|
324
|
+
all(term.casefold() in text.casefold() for term in terms)
|
|
325
|
+
for text in texts
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _check(kind: str, expected: object, ok: bool, actual: object) -> dict[str, object]:
|
|
330
|
+
return {"kind": kind, "expected": expected, "ok": ok, "actual": actual}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _expected_payload(expected: ExtractorExpected) -> dict[str, object]:
|
|
334
|
+
return {
|
|
335
|
+
"participants": expected.participants,
|
|
336
|
+
"mentioned_people": expected.mentioned_people,
|
|
337
|
+
"aliases": expected.aliases,
|
|
338
|
+
"follow_up_terms": expected.follow_up_terms,
|
|
339
|
+
"forbidden_follow_up_terms": expected.forbidden_follow_up_terms,
|
|
340
|
+
"follow_up_due_dates": expected.follow_up_due_dates,
|
|
341
|
+
"attributed_claim_terms": expected.attributed_claim_terms,
|
|
342
|
+
"direct_fact_terms": expected.direct_fact_terms,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _actual_payload(actual: SocialInteraction) -> dict[str, object]:
|
|
347
|
+
return {
|
|
348
|
+
"place": actual.place,
|
|
349
|
+
"participants": [
|
|
350
|
+
item.person.model_dump(mode="json") for item in actual.participants
|
|
351
|
+
],
|
|
352
|
+
"mentioned_people": [
|
|
353
|
+
item.model_dump(mode="json") for item in actual.mentioned_people
|
|
354
|
+
],
|
|
355
|
+
"topics": actual.topics,
|
|
356
|
+
"direct_facts": [item.model_dump(mode="json") for item in actual.direct_facts],
|
|
357
|
+
"attributed_claims": [
|
|
358
|
+
item.model_dump(mode="json") for item in actual.attributed_claims
|
|
359
|
+
],
|
|
360
|
+
"follow_ups": [item.model_dump(mode="json") for item in actual.follow_ups],
|
|
361
|
+
"relationships": [
|
|
362
|
+
item.model_dump(mode="json") for item in actual.relationships
|
|
363
|
+
],
|
|
364
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Deterministic social-network fixture generator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from random import Random
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
from people_network_memory.domain.models import (
|
|
12
|
+
AttributedClaim,
|
|
13
|
+
DirectFact,
|
|
14
|
+
FollowUpTask,
|
|
15
|
+
MentionedPerson,
|
|
16
|
+
Participant,
|
|
17
|
+
PersonRef,
|
|
18
|
+
RelationshipAssertion,
|
|
19
|
+
SensitivityLabel,
|
|
20
|
+
SocialInteraction,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FixturePerson(BaseModel):
|
|
25
|
+
model_config = ConfigDict(extra="forbid")
|
|
26
|
+
|
|
27
|
+
person_id: str
|
|
28
|
+
name: str
|
|
29
|
+
company: str
|
|
30
|
+
previous_companies: list[str]
|
|
31
|
+
schools: list[str]
|
|
32
|
+
interests: list[str]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EvalQuery(BaseModel):
|
|
36
|
+
model_config = ConfigDict(extra="forbid")
|
|
37
|
+
|
|
38
|
+
query: str
|
|
39
|
+
expected_people: list[str] = Field(default_factory=list)
|
|
40
|
+
expected_terms: list[str] = Field(default_factory=list)
|
|
41
|
+
category: Literal["mentioned", "vague", "bilingual", "follow_up", "profile"]
|
|
42
|
+
source_interaction_index: int | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MockDataset(BaseModel):
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
47
|
+
|
|
48
|
+
people: list[FixturePerson]
|
|
49
|
+
organizations: list[str]
|
|
50
|
+
schools: list[str]
|
|
51
|
+
places: list[str]
|
|
52
|
+
interactions: list[SocialInteraction]
|
|
53
|
+
eval_queries: list[EvalQuery]
|
|
54
|
+
|
|
55
|
+
def summary(self) -> dict[str, int]:
|
|
56
|
+
return {
|
|
57
|
+
"people": len(self.people),
|
|
58
|
+
"organizations": len(self.organizations),
|
|
59
|
+
"schools": len(self.schools),
|
|
60
|
+
"places": len(self.places),
|
|
61
|
+
"interactions": len(self.interactions),
|
|
62
|
+
"eval_queries": len(self.eval_queries),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
FIRST_NAMES = [
|
|
67
|
+
"Alice",
|
|
68
|
+
"Bob",
|
|
69
|
+
"Chen",
|
|
70
|
+
"Diana",
|
|
71
|
+
"Evan",
|
|
72
|
+
"Fiona",
|
|
73
|
+
"Grace",
|
|
74
|
+
"Henry",
|
|
75
|
+
"Iris",
|
|
76
|
+
"Jason",
|
|
77
|
+
"Kelly",
|
|
78
|
+
"Leo",
|
|
79
|
+
"Mina",
|
|
80
|
+
"Nora",
|
|
81
|
+
"Oscar",
|
|
82
|
+
"Priya",
|
|
83
|
+
"Qian",
|
|
84
|
+
"Rita",
|
|
85
|
+
"Sam",
|
|
86
|
+
"Tina",
|
|
87
|
+
]
|
|
88
|
+
LAST_NAMES = ["Zhang", "Li", "Wang", "Chen", "Liu", "Guo", "Tan", "Kim"]
|
|
89
|
+
ORGS = [
|
|
90
|
+
"Tencent Robotics",
|
|
91
|
+
"ByteDance AI Lab",
|
|
92
|
+
"Alibaba Cloud",
|
|
93
|
+
"Meituan",
|
|
94
|
+
"SenseTime",
|
|
95
|
+
"DJI",
|
|
96
|
+
"Ant Group",
|
|
97
|
+
"Xpeng Robotics",
|
|
98
|
+
"MiniMax",
|
|
99
|
+
"Moonshot AI",
|
|
100
|
+
]
|
|
101
|
+
SCHOOLS = ["Tsinghua", "Peking University", "Zhejiang University", "SJTU", "Fudan", "CMU"]
|
|
102
|
+
PLACES = [
|
|
103
|
+
"Blue Bottle Coffee",
|
|
104
|
+
"Xintiandi",
|
|
105
|
+
"Z Park",
|
|
106
|
+
"People Square",
|
|
107
|
+
"Sanlitun",
|
|
108
|
+
"West Lake",
|
|
109
|
+
"张江咖啡馆",
|
|
110
|
+
"深圳湾创业广场",
|
|
111
|
+
"The Bund",
|
|
112
|
+
"Nanshan Library",
|
|
113
|
+
]
|
|
114
|
+
INTERESTS = [
|
|
115
|
+
"robotics hiring",
|
|
116
|
+
"embodied AI",
|
|
117
|
+
"fundraising",
|
|
118
|
+
"jazz",
|
|
119
|
+
"trail running",
|
|
120
|
+
"Japanese food",
|
|
121
|
+
"robot learning",
|
|
122
|
+
"founder intros",
|
|
123
|
+
"product strategy",
|
|
124
|
+
"coffee",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def generate_mock_dataset(seed: int = 42) -> MockDataset:
|
|
129
|
+
rng = Random(seed)
|
|
130
|
+
people: list[FixturePerson] = []
|
|
131
|
+
for index in range(42):
|
|
132
|
+
first = FIRST_NAMES[index % len(FIRST_NAMES)]
|
|
133
|
+
last = LAST_NAMES[(index * 3) % len(LAST_NAMES)]
|
|
134
|
+
if index in {8, 19, 31}:
|
|
135
|
+
first, last = "Bob", "Li"
|
|
136
|
+
company = ORGS[index % len(ORGS)]
|
|
137
|
+
previous = [ORGS[(index + 2) % len(ORGS)], ORGS[(index + 5) % len(ORGS)]]
|
|
138
|
+
schools = [SCHOOLS[index % len(SCHOOLS)], SCHOOLS[(index + 2) % len(SCHOOLS)]]
|
|
139
|
+
interests = [INTERESTS[index % len(INTERESTS)], INTERESTS[(index + 3) % len(INTERESTS)]]
|
|
140
|
+
people.append(
|
|
141
|
+
FixturePerson(
|
|
142
|
+
person_id=f"fixture_person_{index:03d}",
|
|
143
|
+
name=f"{first} {last}",
|
|
144
|
+
company=company,
|
|
145
|
+
previous_companies=previous,
|
|
146
|
+
schools=schools,
|
|
147
|
+
interests=interests,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
interactions: list[SocialInteraction] = []
|
|
152
|
+
for index in range(90):
|
|
153
|
+
person = people[index % len(people)]
|
|
154
|
+
other = people[(index + 7) % len(people)]
|
|
155
|
+
place = PLACES[index % len(PLACES)]
|
|
156
|
+
topic = INTERESTS[index % len(INTERESTS)]
|
|
157
|
+
source_text = (
|
|
158
|
+
f"Met {person.name} at {place}. We discussed {topic}. "
|
|
159
|
+
f"{person.name} mentioned {other.name} may be useful for {INTERESTS[(index + 2) % len(INTERESTS)]}."
|
|
160
|
+
)
|
|
161
|
+
if index % 11 == 0:
|
|
162
|
+
source_text = (
|
|
163
|
+
f"在{place}见了{person.name},聊了{topic},"
|
|
164
|
+
f"{person.name}提到{other.name}正在关注{INTERESTS[(index + 2) % len(INTERESTS)]}。"
|
|
165
|
+
)
|
|
166
|
+
interaction = SocialInteraction(
|
|
167
|
+
source_text=source_text,
|
|
168
|
+
occurred_at=datetime(2026, 5, (index % 25) + 1, tzinfo=timezone.utc),
|
|
169
|
+
interaction_type="coffee" if "Coffee" in place or "咖啡" in place else "meeting",
|
|
170
|
+
place=place,
|
|
171
|
+
participants=[Participant(person=_ref(person))],
|
|
172
|
+
mentioned_people=[
|
|
173
|
+
MentionedPerson(
|
|
174
|
+
person=_ref(other),
|
|
175
|
+
mentioned_by=_ref(person),
|
|
176
|
+
context=INTERESTS[(index + 2) % len(INTERESTS)],
|
|
177
|
+
)
|
|
178
|
+
],
|
|
179
|
+
topics=[topic],
|
|
180
|
+
direct_facts=[
|
|
181
|
+
DirectFact(subject=_ref(person), predicate="interest", value=topic),
|
|
182
|
+
DirectFact(subject=_ref(person), predicate="works_at", value=person.company),
|
|
183
|
+
],
|
|
184
|
+
attributed_claims=[
|
|
185
|
+
AttributedClaim(
|
|
186
|
+
speaker=_ref(person),
|
|
187
|
+
subject=_ref(other),
|
|
188
|
+
claim_text=(
|
|
189
|
+
f"{person.name} said {other.name} may be useful for "
|
|
190
|
+
f"{INTERESTS[(index + 2) % len(INTERESTS)]}."
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
],
|
|
194
|
+
follow_ups=[
|
|
195
|
+
FollowUpTask(
|
|
196
|
+
description=f"Follow up with {person.name} about {topic}",
|
|
197
|
+
related_people=[_ref(person)],
|
|
198
|
+
)
|
|
199
|
+
]
|
|
200
|
+
if index % 5 == 0
|
|
201
|
+
else [],
|
|
202
|
+
relationships=[
|
|
203
|
+
RelationshipAssertion(
|
|
204
|
+
source=_ref(person),
|
|
205
|
+
target=_ref(other),
|
|
206
|
+
relationship_type="knows_person",
|
|
207
|
+
directed=False,
|
|
208
|
+
)
|
|
209
|
+
],
|
|
210
|
+
sensitivity=[SensitivityLabel.DO_NOT_SURFACE_UNPROMPTED]
|
|
211
|
+
if index % 17 == 0
|
|
212
|
+
else [],
|
|
213
|
+
metadata={"fixture_index": index, "random_check": rng.randint(1, 1000)},
|
|
214
|
+
)
|
|
215
|
+
interactions.append(interaction)
|
|
216
|
+
|
|
217
|
+
eval_queries: list[EvalQuery] = []
|
|
218
|
+
for index in range(10):
|
|
219
|
+
person = people[index]
|
|
220
|
+
other = people[(index + 7) % len(people)]
|
|
221
|
+
eval_queries.append(
|
|
222
|
+
EvalQuery(
|
|
223
|
+
query=f"who mentioned {other.name}",
|
|
224
|
+
expected_people=[person.name],
|
|
225
|
+
expected_terms=[other.name],
|
|
226
|
+
category="mentioned",
|
|
227
|
+
source_interaction_index=index,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
for index in range(10, 20):
|
|
231
|
+
person = people[index]
|
|
232
|
+
eval_queries.append(
|
|
233
|
+
EvalQuery(
|
|
234
|
+
query=f"person from {PLACES[index % len(PLACES)]} talking about {INTERESTS[index % len(INTERESTS)]}",
|
|
235
|
+
expected_people=[person.name],
|
|
236
|
+
expected_terms=[PLACES[index % len(PLACES)], INTERESTS[index % len(INTERESTS)]],
|
|
237
|
+
category="vague",
|
|
238
|
+
source_interaction_index=index,
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
for index in range(20, 25):
|
|
242
|
+
person = people[index]
|
|
243
|
+
eval_queries.append(
|
|
244
|
+
EvalQuery(
|
|
245
|
+
query=f"在{PLACES[index % len(PLACES)]}聊{INTERESTS[index % len(INTERESTS)]}的人",
|
|
246
|
+
expected_people=[person.name],
|
|
247
|
+
expected_terms=[INTERESTS[index % len(INTERESTS)]],
|
|
248
|
+
category="bilingual",
|
|
249
|
+
source_interaction_index=index,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
for index in range(25, 75, 5):
|
|
253
|
+
person = people[index % len(people)]
|
|
254
|
+
topic = INTERESTS[index % len(INTERESTS)]
|
|
255
|
+
eval_queries.append(
|
|
256
|
+
EvalQuery(
|
|
257
|
+
query=f"what did I promise {person.name}",
|
|
258
|
+
expected_people=[person.name],
|
|
259
|
+
expected_terms=[f"Follow up with {person.name} about {topic}"],
|
|
260
|
+
category="follow_up",
|
|
261
|
+
source_interaction_index=index,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
for index in range(35, 42):
|
|
265
|
+
person = people[index % len(people)]
|
|
266
|
+
eval_queries.append(
|
|
267
|
+
EvalQuery(
|
|
268
|
+
query=f"{person.company} {person.interests[0]}",
|
|
269
|
+
expected_people=[person.name],
|
|
270
|
+
expected_terms=[person.company, person.interests[0]],
|
|
271
|
+
category="profile",
|
|
272
|
+
source_interaction_index=index,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
return MockDataset(
|
|
276
|
+
people=people,
|
|
277
|
+
organizations=ORGS,
|
|
278
|
+
schools=SCHOOLS,
|
|
279
|
+
places=PLACES,
|
|
280
|
+
interactions=interactions,
|
|
281
|
+
eval_queries=eval_queries,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _ref(person: FixturePerson) -> PersonRef:
|
|
286
|
+
return PersonRef(
|
|
287
|
+
label=person.name,
|
|
288
|
+
person_id=person.person_id,
|
|
289
|
+
company_hint=person.company,
|
|
290
|
+
)
|