@littlebigbrain/create 0.1.0 → 0.3.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 +6 -2
- package/lib/create.mjs +33 -5
- package/package.json +1 -1
- package/templates/_shared/testing.ts +53 -9
- package/templates/documents-link/README.md +9 -7
- package/templates/documents-link/workflow.test.ts +28 -1
- package/templates/documents-link/workflow.ts +24 -14
- package/templates/documents-map/README.md +212 -0
- package/templates/documents-map/map.ts +1135 -0
- package/templates/documents-map/samples/ausstattungsliste.pdf +76 -0
- package/templates/documents-map/samples/baubeschreibung.pdf +54 -0
- package/templates/documents-map/samples/expected.json +50 -0
- package/templates/documents-map/samples/pages/raumblaetter.json +1580 -0
- package/templates/documents-map/samples/raumblaetter.pdf +109 -0
- package/templates/documents-map/samples/trace.json +613 -0
- package/templates/documents-map/samples.ts +182 -0
- package/templates/documents-map/workflow.test.ts +770 -0
- package/templates/documents-map/workflow.ts +1199 -0
- package/templates/link/README.md +203 -0
- package/templates/link/candidates.ts +432 -0
- package/templates/link/decide.test.ts +368 -0
- package/templates/link/decide.ts +340 -0
- package/templates/link/fixture.ts +727 -0
- package/templates/link/grade-run.ts +53 -0
- package/templates/link/grade.test.ts +113 -0
- package/templates/link/grade.ts +335 -0
- package/templates/link/items.ts +333 -0
- package/templates/link/names.test.ts +260 -0
- package/templates/link/names.ts +590 -0
- package/templates/link/reads.ts +302 -0
- package/templates/link/samples/gold.json +108 -0
- package/templates/link/samples/records.json +48 -0
- package/templates/link/samples.ts +64 -0
- package/templates/link/settings.ts +162 -0
- package/templates/link/setup.ts +18 -0
- package/templates/link/source-turn.ts +894 -0
- package/templates/link/sources/records.ts +429 -0
- package/templates/link/things-turn.ts +984 -0
- package/templates/link/things.test.ts +204 -0
- package/templates/link/things.ts +426 -0
- package/templates/link/try.ts +28 -0
- package/templates/link/worker.ts +24 -0
- package/templates/link/workflow.test.ts +484 -0
- package/templates/link/workflow.ts +202 -0
- package/templates/ontology-fit/workflow.test.ts +7 -1
- package/templates/ontology-fit/workflow.ts +5 -2
- package/templates/review/README.md +158 -0
- package/templates/review/decision.ts +483 -0
- package/templates/review/samples/expected.json +37 -0
- package/templates/review/samples/proposals.json +35 -0
- package/templates/review/samples.ts +154 -0
- package/templates/review/workflow.test.ts +438 -0
- package/templates/review/workflow.ts +459 -0
- package/templates/values-extract/workflow.test.ts +24 -1
- package/templates/values-extract/workflow.ts +11 -3
- package/templates/versions.json +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// The reads of the link workflow: records, the mentions it wrote, and the
|
|
2
|
+
// things they belong to. Each query starts with a `# link: <name>` line, so
|
|
3
|
+
// the call log and a test's mock can tell them apart.
|
|
4
|
+
import {
|
|
5
|
+
CLASS_IRI,
|
|
6
|
+
P,
|
|
7
|
+
PROPERTY_IRI,
|
|
8
|
+
RDFS_LABEL,
|
|
9
|
+
RELATION_IRI,
|
|
10
|
+
iriSegment,
|
|
11
|
+
iris,
|
|
12
|
+
propertyIri,
|
|
13
|
+
} from "./candidates.js";
|
|
14
|
+
import type { SourceRecord } from "./sources/records.js";
|
|
15
|
+
|
|
16
|
+
/** The properties of a mention that the workflow writes, by name. */
|
|
17
|
+
export const MENTION_FIELDS = [
|
|
18
|
+
"span",
|
|
19
|
+
"role",
|
|
20
|
+
"kind",
|
|
21
|
+
"field",
|
|
22
|
+
"offset",
|
|
23
|
+
"length",
|
|
24
|
+
"block_key",
|
|
25
|
+
"window",
|
|
26
|
+
];
|
|
27
|
+
/** The properties of a mention that the things identity writes: its links and a person's constraints. */
|
|
28
|
+
export const LINK_FIELDS = ["linked_to", "must_link", "cannot_link"];
|
|
29
|
+
/** The properties of a thing. */
|
|
30
|
+
export const THING_FIELDS = ["anchor_key", "kind", "alias"];
|
|
31
|
+
|
|
32
|
+
/** Records one read names, at most. */
|
|
33
|
+
export const PER_READ = 400;
|
|
34
|
+
|
|
35
|
+
/** A field named `name` or `label` is the record's display name: `rdfs:label`. */
|
|
36
|
+
export const isDisplayName = (field: string) => field === "name" || field === "label";
|
|
37
|
+
/** The predicate of a field: `rdfs:label` for the display name, else the property. */
|
|
38
|
+
export const fieldIri = (field: string) => (isDisplayName(field) ? RDFS_LABEL : propertyIri(field));
|
|
39
|
+
|
|
40
|
+
/** The values of the fields of records, and their names. */
|
|
41
|
+
export function sourcesQuery(records: string[], fields: string[]): string {
|
|
42
|
+
const predicates = [...new Set([RDFS_LABEL, ...fields.map(fieldIri)])]
|
|
43
|
+
.map((iri) => `<${iri}>`)
|
|
44
|
+
.join(", ");
|
|
45
|
+
return `# link: sources\n${P}SELECT ?s ?p ?v WHERE {
|
|
46
|
+
VALUES ?s { ${iris(records)} }
|
|
47
|
+
?s ?p ?v FILTER(?p IN (${predicates}))
|
|
48
|
+
}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The records of a read: each field's values in the order the rows give them. */
|
|
52
|
+
export function parseSources(
|
|
53
|
+
rows: Record<string, string>[],
|
|
54
|
+
records: string[],
|
|
55
|
+
cls: string,
|
|
56
|
+
fields: string[],
|
|
57
|
+
): Map<string, SourceRecord> {
|
|
58
|
+
const out = new Map<string, SourceRecord>();
|
|
59
|
+
const byIri = new Map(fields.map((f) => [fieldIri(f), f]));
|
|
60
|
+
for (const iri of records) out.set(iri, { iri, class: cls, name: "", values: {} });
|
|
61
|
+
const found = new Set<string>();
|
|
62
|
+
for (const r of rows) {
|
|
63
|
+
const record = out.get(r.s ?? "");
|
|
64
|
+
if (!record || r.v === undefined) continue;
|
|
65
|
+
found.add(record.iri);
|
|
66
|
+
if (r.p === RDFS_LABEL) record.name ||= r.v;
|
|
67
|
+
const field = byIri.get(r.p ?? "");
|
|
68
|
+
if (!field) continue;
|
|
69
|
+
const values = (record.values[field] ??= []);
|
|
70
|
+
if (!values.includes(r.v)) values.push(r.v);
|
|
71
|
+
}
|
|
72
|
+
for (const iri of records) if (!found.has(iri)) out.delete(iri);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The mentions the workflow wrote for records, with the thing each is in and its cannot-links. */
|
|
77
|
+
export function sourceMentionsQuery(records: string[]): string {
|
|
78
|
+
return `# link: source mentions\n${P}SELECT ?s ?m ?thing ?c WHERE {
|
|
79
|
+
VALUES ?s { ${iris(records)} }
|
|
80
|
+
?m r:mentioned_in ?s .
|
|
81
|
+
OPTIONAL { ?m r:refers_to ?thing } OPTIONAL { ?m p:cannot_link ?c }
|
|
82
|
+
} LIMIT 5000`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Mentions of the workflow whose source is gone: deleting a record retracts the edges to it. */
|
|
86
|
+
export function orphanMentionsQuery(): string {
|
|
87
|
+
return `# link: orphan mentions\n${P}SELECT ?m ?thing WHERE {
|
|
88
|
+
?m a <${CLASS_IRI}mention> .
|
|
89
|
+
FILTER NOT EXISTS { ?m r:mentioned_in ?any }
|
|
90
|
+
OPTIONAL { ?m r:refers_to ?thing }
|
|
91
|
+
} LIMIT 900`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface WrittenMention {
|
|
95
|
+
mention: string;
|
|
96
|
+
thing: string | null;
|
|
97
|
+
cannot: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseSourceMentions(rows: Record<string, string>[]): Map<string, WrittenMention[]> {
|
|
101
|
+
const out = new Map<string, Map<string, WrittenMention>>();
|
|
102
|
+
for (const r of rows) {
|
|
103
|
+
if (!r.s || !r.m) continue;
|
|
104
|
+
const byMention = out.get(r.s) ?? out.set(r.s, new Map()).get(r.s)!;
|
|
105
|
+
const m = byMention.get(r.m) ?? { mention: r.m, thing: null, cannot: [] };
|
|
106
|
+
if (r.thing) m.thing = r.thing;
|
|
107
|
+
if (r.c && !m.cannot.includes(r.c)) m.cannot.push(r.c);
|
|
108
|
+
byMention.set(r.m, m);
|
|
109
|
+
}
|
|
110
|
+
return new Map([...out].map(([s, ms]) => [s, [...ms.values()]]));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Rows of one read of the members of things, at most: a larger thing is read in part (the supernode bound). */
|
|
114
|
+
export const MEMBER_ROWS = 5_000;
|
|
115
|
+
|
|
116
|
+
/** The members of things. */
|
|
117
|
+
export function thingMembersQuery(things: string[]): string {
|
|
118
|
+
return `# link: thing members\n${P}SELECT ?t ?x WHERE {
|
|
119
|
+
VALUES ?t { ${iris(things)} }
|
|
120
|
+
?x r:refers_to ?t .
|
|
121
|
+
} LIMIT ${MEMBER_ROWS}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function parseThingMembers(rows: Record<string, string>[]): Map<string, Set<string>> {
|
|
125
|
+
const members = new Map<string, Set<string>>();
|
|
126
|
+
for (const r of rows) {
|
|
127
|
+
if (!r.t || !r.x) continue;
|
|
128
|
+
(members.get(r.t) ?? members.set(r.t, new Set()).get(r.t)!).add(r.x);
|
|
129
|
+
}
|
|
130
|
+
return members;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The facts of mentions: their properties, their source, their thing and their links with the confidence of each. */
|
|
134
|
+
export function mentionFactsQuery(mentions: string[]): string {
|
|
135
|
+
const props = ["role", "span", "kind", "field", "window", "block_key", ...LINK_FIELDS]
|
|
136
|
+
.map((f) => `p:${f}`)
|
|
137
|
+
.join(", ");
|
|
138
|
+
return `# link: mention facts\n${P}PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
|
139
|
+
PREFIX lbb: <https://littlebigbrain.com/ns#>
|
|
140
|
+
SELECT ?m ?p ?v ?c WHERE {
|
|
141
|
+
VALUES ?m { ${iris(mentions)} }
|
|
142
|
+
{ ?m ?p ?v FILTER(?p IN (${props}, r:mentioned_in, r:refers_to)) }
|
|
143
|
+
UNION {
|
|
144
|
+
?m r:same_thing_as ?v BIND(r:same_thing_as AS ?p)
|
|
145
|
+
OPTIONAL { ?st rdf:subject ?m ; rdf:predicate r:same_thing_as ; rdf:object ?v ; lbb:confidence ?c }
|
|
146
|
+
}
|
|
147
|
+
}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A mention as the graph holds it. */
|
|
151
|
+
export interface MentionFacts {
|
|
152
|
+
iri: string;
|
|
153
|
+
role: "self" | "refers" | "defines";
|
|
154
|
+
span: string;
|
|
155
|
+
kind: string;
|
|
156
|
+
field: string;
|
|
157
|
+
window: string | null;
|
|
158
|
+
source: string;
|
|
159
|
+
thing: string | null;
|
|
160
|
+
keys: string[];
|
|
161
|
+
linkedTo: string[];
|
|
162
|
+
must: string[];
|
|
163
|
+
cannot: string[];
|
|
164
|
+
/** Its `SAME_THING_AS` targets, with the confidence of each. */
|
|
165
|
+
same: { target: string; confidence: number | null }[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function parseMentionFacts(rows: Record<string, string>[]): Map<string, MentionFacts> {
|
|
169
|
+
const out = new Map<string, MentionFacts>();
|
|
170
|
+
const get = (iri: string) => {
|
|
171
|
+
let m = out.get(iri);
|
|
172
|
+
if (!m) {
|
|
173
|
+
m = {
|
|
174
|
+
iri,
|
|
175
|
+
role: "refers",
|
|
176
|
+
span: "",
|
|
177
|
+
kind: "",
|
|
178
|
+
field: "",
|
|
179
|
+
window: null,
|
|
180
|
+
source: "",
|
|
181
|
+
thing: null,
|
|
182
|
+
keys: [],
|
|
183
|
+
linkedTo: [],
|
|
184
|
+
must: [],
|
|
185
|
+
cannot: [],
|
|
186
|
+
same: [],
|
|
187
|
+
};
|
|
188
|
+
out.set(iri, m);
|
|
189
|
+
}
|
|
190
|
+
return m;
|
|
191
|
+
};
|
|
192
|
+
const add = (list: string[], v: string) => {
|
|
193
|
+
if (!list.includes(v)) list.push(v);
|
|
194
|
+
};
|
|
195
|
+
for (const r of rows) {
|
|
196
|
+
if (!r.m || !r.p || r.v === undefined) continue;
|
|
197
|
+
const m = get(r.m);
|
|
198
|
+
const p = r.p.startsWith(PROPERTY_IRI)
|
|
199
|
+
? `p:${decodeURIComponent(r.p.slice(PROPERTY_IRI.length))}`
|
|
200
|
+
: `r:${decodeURIComponent(r.p.slice(RELATION_IRI.length))}`;
|
|
201
|
+
if (p === "p:role") m.role = r.v === "self" || r.v === "defines" ? r.v : "refers";
|
|
202
|
+
else if (p === "p:span") m.span = r.v;
|
|
203
|
+
else if (p === "p:kind") m.kind = r.v;
|
|
204
|
+
else if (p === "p:field") m.field = r.v;
|
|
205
|
+
else if (p === "p:window") m.window = r.v;
|
|
206
|
+
else if (p === "p:block_key") add(m.keys, r.v);
|
|
207
|
+
else if (p === "p:linked_to") add(m.linkedTo, r.v);
|
|
208
|
+
else if (p === "p:must_link") add(m.must, r.v);
|
|
209
|
+
else if (p === "p:cannot_link") add(m.cannot, r.v);
|
|
210
|
+
else if (p === "r:mentioned_in") m.source = r.v;
|
|
211
|
+
else if (p === "r:refers_to") m.thing = r.v;
|
|
212
|
+
else if (p === "r:same_thing_as" && !m.same.some((s) => s.target === r.v))
|
|
213
|
+
m.same.push({ target: r.v, confidence: r.c !== undefined ? Number(r.c) : null });
|
|
214
|
+
}
|
|
215
|
+
// A mention without a span or a source is gone, or no mention of this workflow.
|
|
216
|
+
for (const [iri, m] of out) if (!m.span || !m.source) out.delete(iri);
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The names and classes of records. */
|
|
221
|
+
export function recordsQuery(records: string[]): string {
|
|
222
|
+
return `# link: records\n${P}SELECT ?s ?name ?class WHERE {
|
|
223
|
+
VALUES ?s { ${iris(records)} }
|
|
224
|
+
OPTIONAL { ?s rdfs:label ?name } OPTIONAL { ?s a ?class }
|
|
225
|
+
}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface RecordFacts {
|
|
229
|
+
name: string;
|
|
230
|
+
/** The class as the settings name it, else the local name of its IRI. */
|
|
231
|
+
class: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function parseRecords(
|
|
235
|
+
rows: Record<string, string>[],
|
|
236
|
+
classes: string[],
|
|
237
|
+
): Map<string, RecordFacts> {
|
|
238
|
+
const bySegment = new Map(classes.map((c) => [iriSegment(c), c]));
|
|
239
|
+
const out = new Map<string, RecordFacts>();
|
|
240
|
+
for (const r of rows) {
|
|
241
|
+
if (!r.s) continue;
|
|
242
|
+
const entry = out.get(r.s) ?? { name: "", class: "" };
|
|
243
|
+
if (r.name && !entry.name) entry.name = r.name;
|
|
244
|
+
if (r.class?.startsWith(CLASS_IRI)) {
|
|
245
|
+
const segment = r.class.slice(CLASS_IRI.length);
|
|
246
|
+
const named = bySegment.get(segment);
|
|
247
|
+
if (named || !entry.class) entry.class = named ?? decodeURIComponent(segment);
|
|
248
|
+
}
|
|
249
|
+
out.set(r.s, entry);
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The anchor keys and labels of things, and where a merged thing points. */
|
|
255
|
+
export function thingFactsQuery(things: string[]): string {
|
|
256
|
+
return `# link: thing facts\n${P}SELECT ?t ?p ?v WHERE {
|
|
257
|
+
VALUES ?t { ${iris(things)} }
|
|
258
|
+
?t ?p ?v FILTER(?p IN (p:anchor_key, rdfs:label, r:merged_into))
|
|
259
|
+
}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function parseThingFacts(rows: Record<string, string>[]) {
|
|
263
|
+
const out = new Map<string, { anchorKey: string; label: string; mergedInto: string | null }>();
|
|
264
|
+
for (const r of rows) {
|
|
265
|
+
if (!r.t || r.v === undefined) continue;
|
|
266
|
+
const entry = out.get(r.t) ?? { anchorKey: "", label: "", mergedInto: null };
|
|
267
|
+
if (r.p === propertyIri("anchor_key")) entry.anchorKey = r.v;
|
|
268
|
+
else if (r.p === RDFS_LABEL) entry.label = r.v;
|
|
269
|
+
else entry.mergedInto = r.v;
|
|
270
|
+
out.set(r.t, entry);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The accepted identities (`SAME_AS`) of records, both ways. */
|
|
276
|
+
export function sameAsQuery(records: string[]): string {
|
|
277
|
+
return `# link: same as\n${P}SELECT ?a ?b WHERE {
|
|
278
|
+
VALUES ?a { ${iris(records)} }
|
|
279
|
+
{ ?a r:same_as ?b } UNION { ?b r:same_as ?a }
|
|
280
|
+
}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function parseSameAs(rows: Record<string, string>[]): [string, string][] {
|
|
284
|
+
const seen = new Set<string>();
|
|
285
|
+
const out: [string, string][] = [];
|
|
286
|
+
for (const r of rows) {
|
|
287
|
+
if (!r.a || !r.b || r.a === r.b) continue;
|
|
288
|
+
const pair = [r.a, r.b].sort() as [string, string];
|
|
289
|
+
const key = pair.join("\u0000");
|
|
290
|
+
if (!seen.has(key)) {
|
|
291
|
+
seen.add(key);
|
|
292
|
+
out.push(pair);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** `list` in parts of at most `size`. */
|
|
299
|
+
export const parts = <T>(list: T[], size = PER_READ): T[][] =>
|
|
300
|
+
Array.from({ length: Math.ceil(list.length / size) }, (_, n) =>
|
|
301
|
+
list.slice(n * size, (n + 1) * size),
|
|
302
|
+
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "lbb.link.gold.v1",
|
|
3
|
+
"about": "The gold groups of the invented CRM of samples/records.json. Each group is one real thing: the mentions that name it, by record, field and span. Every name, company and address is invented.",
|
|
4
|
+
"complete": true,
|
|
5
|
+
"groups": [
|
|
6
|
+
{ "id": "mira", "kind": "person", "mentions": [
|
|
7
|
+
{ "record": "person-mira", "field": "name", "span": "Mira Tannenholt-Ruiz" },
|
|
8
|
+
{ "record": "person-mira", "field": "email", "span": "mira.tannenholt@northbay.example" },
|
|
9
|
+
{ "record": "lead-mira", "field": "first_name+last_name", "span": "Mira Ruiz" },
|
|
10
|
+
{ "record": "lead-mira", "field": "email", "span": "mira.tannenholt@northbay.example" },
|
|
11
|
+
{ "record": "deal-northbay", "field": "name", "span": "M. Tannenholt" },
|
|
12
|
+
{ "record": "note-1", "field": "body", "span": "Mimi T." }
|
|
13
|
+
] },
|
|
14
|
+
{ "id": "robert", "kind": "person", "mentions": [
|
|
15
|
+
{ "record": "person-robert", "field": "name", "span": "Robert Haldane" },
|
|
16
|
+
{ "record": "person-robert", "field": "email", "span": "r.haldane@quillfield.example" },
|
|
17
|
+
{ "record": "note-2", "field": "body", "span": "Bob Haldane" },
|
|
18
|
+
{ "record": "note-11", "field": "body", "span": "Robert Haldane" }
|
|
19
|
+
] },
|
|
20
|
+
{ "id": "soren", "kind": "person", "mentions": [
|
|
21
|
+
{ "record": "person-soren", "field": "name", "span": "Søren Vestergaard" },
|
|
22
|
+
{ "record": "person-soren", "field": "email", "span": "soren@brightmoor.example" },
|
|
23
|
+
{ "record": "lead-soren", "field": "first_name+last_name", "span": "Soren Vestergaard" },
|
|
24
|
+
{ "record": "lead-soren", "field": "email", "span": "s.vestergaard@brightmoor.example" },
|
|
25
|
+
{ "record": "note-3", "field": "body", "span": "Vestergaard, S." }
|
|
26
|
+
] },
|
|
27
|
+
{ "id": "dana", "kind": "person", "mentions": [
|
|
28
|
+
{ "record": "person-dana", "field": "name", "span": "Dana Okafor" },
|
|
29
|
+
{ "record": "person-dana", "field": "email", "span": "dana.okafor@northbay.example" },
|
|
30
|
+
{ "record": "note-5", "field": "body", "span": "D. Okafor" }
|
|
31
|
+
] },
|
|
32
|
+
{ "id": "felix", "kind": "person", "mentions": [
|
|
33
|
+
{ "record": "person-felix", "field": "name", "span": "Felix Okafor" },
|
|
34
|
+
{ "record": "person-felix", "field": "email", "span": "felix.okafor@ridgeway.example" },
|
|
35
|
+
{ "record": "note-4", "field": "body", "span": "F. Okafor" }
|
|
36
|
+
] },
|
|
37
|
+
{ "id": "jurgen", "kind": "person", "mentions": [
|
|
38
|
+
{ "record": "person-jurgen", "field": "name", "span": "Jürgen Kessler" },
|
|
39
|
+
{ "record": "person-jurgen", "field": "email", "span": "juergen.kessler@harbourline.example" },
|
|
40
|
+
{ "record": "deal-harbourline", "field": "name", "span": "Jurgen Kessler" },
|
|
41
|
+
{ "record": "note-6", "field": "body", "span": "Juergen Kessler" }
|
|
42
|
+
] },
|
|
43
|
+
{ "id": "zoe", "kind": "person", "mentions": [
|
|
44
|
+
{ "record": "person-zoe", "field": "name", "span": "Zoë Lindqvist" },
|
|
45
|
+
{ "record": "person-zoe", "field": "email", "span": "zoe@lumen-ash.example" },
|
|
46
|
+
{ "record": "lead-zoe", "field": "first_name+last_name", "span": "Zoe Lindqvist" }
|
|
47
|
+
] },
|
|
48
|
+
{ "id": "kit", "kind": "person", "mentions": [
|
|
49
|
+
{ "record": "person-kit", "field": "name", "span": "Kit Marsh" },
|
|
50
|
+
{ "record": "person-kit", "field": "email", "span": "k.marsh@fernway.example" },
|
|
51
|
+
{ "record": "lead-kit", "field": "email", "span": "k.marsh@fernway.example" },
|
|
52
|
+
{ "record": "note-7", "field": "body", "span": "k.marsh@fernway.example" }
|
|
53
|
+
] },
|
|
54
|
+
{ "id": "tobias", "kind": "person", "mentions": [
|
|
55
|
+
{ "record": "person-tobias", "field": "name", "span": "Tobias Wren" },
|
|
56
|
+
{ "record": "person-tobias", "field": "email", "span": "tobias.wren@quillfield.example" },
|
|
57
|
+
{ "record": "deal-quillfield", "field": "name", "span": "Wren, Tobias" },
|
|
58
|
+
{ "record": "note-11", "field": "body", "span": "Tobias Wren" }
|
|
59
|
+
] },
|
|
60
|
+
{ "id": "priya", "kind": "person", "mentions": [
|
|
61
|
+
{ "record": "person-priya", "field": "name", "span": "Priya Ramaswamy" },
|
|
62
|
+
{ "record": "person-priya", "field": "email", "span": "priya.r@brightmoor.example" },
|
|
63
|
+
{ "record": "note-8", "field": "body", "span": "Priya Ramaswamey" }
|
|
64
|
+
] },
|
|
65
|
+
{ "id": "alex-northbay", "kind": "person", "mentions": [
|
|
66
|
+
{ "record": "person-alex-northbay", "field": "name", "span": "Alex Brandt" },
|
|
67
|
+
{ "record": "person-alex-northbay", "field": "email", "span": "alex.brandt@northbay.example" }
|
|
68
|
+
] },
|
|
69
|
+
{ "id": "alex-ridgeway", "kind": "person", "mentions": [
|
|
70
|
+
{ "record": "person-alex-ridgeway", "field": "name", "span": "Alex Brandt" },
|
|
71
|
+
{ "record": "person-alex-ridgeway", "field": "email", "span": "a.brandt@ridgeway.example" },
|
|
72
|
+
{ "record": "note-10", "field": "body", "span": "Alex Brandt" }
|
|
73
|
+
] },
|
|
74
|
+
{ "id": "casey", "kind": "person", "mentions": [
|
|
75
|
+
{ "record": "lead-casey", "field": "first_name+last_name", "span": "Casey Moreau" },
|
|
76
|
+
{ "record": "lead-casey", "field": "email", "span": "casey.moreau@gmail.com" }
|
|
77
|
+
] },
|
|
78
|
+
{ "id": "northbay", "kind": "organization", "mentions": [
|
|
79
|
+
{ "record": "org-northbay", "field": "name", "span": "Northbay Water GmbH" },
|
|
80
|
+
{ "record": "org-northbay", "field": "website", "span": "https://www.northbay.example/" },
|
|
81
|
+
{ "record": "note-9", "field": "body", "span": "Northbay Water" }
|
|
82
|
+
] },
|
|
83
|
+
{ "id": "quillfield", "kind": "organization", "mentions": [
|
|
84
|
+
{ "record": "org-quillfield", "field": "name", "span": "Quillfield Labs" },
|
|
85
|
+
{ "record": "org-quillfield", "field": "website", "span": "https://quillfield.example" }
|
|
86
|
+
] },
|
|
87
|
+
{ "id": "brightmoor", "kind": "organization", "mentions": [
|
|
88
|
+
{ "record": "org-brightmoor", "field": "name", "span": "Brightmoor Foods" },
|
|
89
|
+
{ "record": "org-brightmoor", "field": "website", "span": "brightmoor.example" }
|
|
90
|
+
] },
|
|
91
|
+
{ "id": "ridgeway", "kind": "organization", "mentions": [
|
|
92
|
+
{ "record": "org-ridgeway", "field": "name", "span": "Ridgeway Transit" },
|
|
93
|
+
{ "record": "org-ridgeway", "field": "website", "span": "https://ridgeway.example/" }
|
|
94
|
+
] },
|
|
95
|
+
{ "id": "harbourline", "kind": "organization", "mentions": [
|
|
96
|
+
{ "record": "org-harbourline", "field": "name", "span": "Harbourline Marine" },
|
|
97
|
+
{ "record": "org-harbourline", "field": "website", "span": "https://www.harbourline.example" }
|
|
98
|
+
] },
|
|
99
|
+
{ "id": "lumen", "kind": "organization", "mentions": [
|
|
100
|
+
{ "record": "org-lumen", "field": "name", "span": "Lumen & Ash" },
|
|
101
|
+
{ "record": "org-lumen", "field": "website", "span": "https://lumen-ash.example" }
|
|
102
|
+
] },
|
|
103
|
+
{ "id": "fernway", "kind": "organization", "mentions": [
|
|
104
|
+
{ "record": "org-fernway", "field": "name", "span": "Fernway Studio" },
|
|
105
|
+
{ "record": "org-fernway", "field": "website", "span": "https://fernway.example" }
|
|
106
|
+
] }
|
|
107
|
+
]
|
|
108
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "id": "org-northbay", "class": "Organization", "name": "Northbay Water GmbH", "fields": { "website": "https://www.northbay.example/" } },
|
|
3
|
+
{ "id": "org-quillfield", "class": "Organization", "name": "Quillfield Labs", "fields": { "website": "https://quillfield.example" } },
|
|
4
|
+
{ "id": "org-brightmoor", "class": "Organization", "name": "Brightmoor Foods", "fields": { "website": "brightmoor.example" } },
|
|
5
|
+
{ "id": "org-ridgeway", "class": "Organization", "name": "Ridgeway Transit", "fields": { "website": "https://ridgeway.example/" } },
|
|
6
|
+
{ "id": "org-harbourline", "class": "Organization", "name": "Harbourline Marine", "fields": { "website": "https://www.harbourline.example" } },
|
|
7
|
+
{ "id": "org-lumen", "class": "Organization", "name": "Lumen & Ash", "fields": { "website": "https://lumen-ash.example" } },
|
|
8
|
+
{ "id": "org-fernway", "class": "Organization", "name": "Fernway Studio", "fields": { "website": "https://fernway.example" } },
|
|
9
|
+
|
|
10
|
+
{ "id": "person-mira", "class": "Person", "name": "Mira Tannenholt-Ruiz", "fields": { "email": "mira.tannenholt@northbay.example", "job_title": "Procurement Lead" }, "relations": { "WORKS_AT": ["org-northbay"] } },
|
|
11
|
+
{ "id": "person-robert", "class": "Person", "name": "Robert Haldane", "fields": { "email": "r.haldane@quillfield.example", "job_title": "Lab Manager" }, "relations": { "WORKS_AT": ["org-quillfield"] } },
|
|
12
|
+
{ "id": "person-soren", "class": "Person", "name": "Søren Vestergaard", "fields": { "email": "soren@brightmoor.example", "job_title": "Head of Quality" }, "relations": { "WORKS_AT": ["org-brightmoor"] } },
|
|
13
|
+
{ "id": "person-dana", "class": "Person", "name": "Dana Okafor", "fields": { "email": "dana.okafor@northbay.example", "job_title": "Site Engineer" }, "relations": { "WORKS_AT": ["org-northbay"] } },
|
|
14
|
+
{ "id": "person-felix", "class": "Person", "name": "Felix Okafor", "fields": { "email": "felix.okafor@ridgeway.example", "job_title": "Fleet Manager" }, "relations": { "WORKS_AT": ["org-ridgeway"] } },
|
|
15
|
+
{ "id": "person-jurgen", "class": "Person", "name": "Jürgen Kessler", "fields": { "email": "juergen.kessler@harbourline.example", "job_title": "Technical Director" }, "relations": { "WORKS_AT": ["org-harbourline"] } },
|
|
16
|
+
{ "id": "person-zoe", "class": "Person", "name": "Zoë Lindqvist", "fields": { "email": "zoe@lumen-ash.example", "job_title": "Founder" }, "relations": { "WORKS_AT": ["org-lumen"] } },
|
|
17
|
+
{ "id": "person-kit", "class": "Person", "name": "Kit Marsh", "fields": { "email": "k.marsh@fernway.example", "job_title": "Studio Lead" }, "relations": { "WORKS_AT": ["org-fernway"] } },
|
|
18
|
+
{ "id": "person-tobias", "class": "Person", "name": "Tobias Wren", "fields": { "email": "tobias.wren@quillfield.example", "job_title": "Buyer" }, "relations": { "WORKS_AT": ["org-quillfield"] } },
|
|
19
|
+
{ "id": "person-priya", "class": "Person", "name": "Priya Ramaswamy", "fields": { "email": "priya.r@brightmoor.example", "job_title": "Operations Manager" }, "relations": { "WORKS_AT": ["org-brightmoor"] } },
|
|
20
|
+
{ "id": "person-alex-northbay", "class": "Person", "name": "Alex Brandt", "fields": { "email": "alex.brandt@northbay.example", "job_title": "Planner" }, "relations": { "WORKS_AT": ["org-northbay"] } },
|
|
21
|
+
{ "id": "person-alex-ridgeway", "class": "Person", "name": "Alex Brandt", "fields": { "email": "a.brandt@ridgeway.example", "job_title": "Depot Lead" }, "relations": { "WORKS_AT": ["org-ridgeway"] } },
|
|
22
|
+
|
|
23
|
+
{ "id": "lead-mira", "class": "Lead", "name": "Mira Ruiz", "fields": { "first_name": "Mira", "last_name": "Ruiz", "email": "mira.tannenholt@northbay.example", "company_name": "Northbay Water" } },
|
|
24
|
+
{ "id": "lead-soren", "class": "Lead", "name": "Soren Vestergaard", "fields": { "first_name": "Soren", "last_name": "Vestergaard", "email": "s.vestergaard@brightmoor.example", "company_name": "Brightmoor Foods" } },
|
|
25
|
+
{ "id": "lead-zoe", "class": "Lead", "name": "Zoe Lindqvist", "fields": { "first_name": "Zoe", "last_name": "Lindqvist", "company_name": "Lumen & Ash" } },
|
|
26
|
+
{ "id": "lead-kit", "class": "Lead", "name": "k.marsh@fernway.example", "fields": { "email": "k.marsh@fernway.example" } },
|
|
27
|
+
{ "id": "lead-casey", "class": "Lead", "name": "Casey Moreau", "fields": { "first_name": "Casey", "last_name": "Moreau", "email": "casey.moreau@gmail.com", "company_name": "Moreau Design" } },
|
|
28
|
+
|
|
29
|
+
{ "id": "deal-northbay", "class": "Deal", "name": "Northbay PX-200 retrofit (M. Tannenholt)", "relations": { "INVOLVES": ["person-mira"], "FOR_ORGANIZATION": ["org-northbay"] } },
|
|
30
|
+
{ "id": "deal-ridgeway", "class": "Deal", "name": "Ridgeway depot pumps", "relations": { "INVOLVES": ["person-felix", "person-alex-ridgeway"], "FOR_ORGANIZATION": ["org-ridgeway"] } },
|
|
31
|
+
{ "id": "deal-quillfield", "class": "Deal", "name": "Wren, Tobias - annual renewal", "relations": { "INVOLVES": ["person-tobias"], "FOR_ORGANIZATION": ["org-quillfield"] } },
|
|
32
|
+
{ "id": "deal-brightmoor", "class": "Deal", "name": "Brightmoor cold-room filters", "relations": { "INVOLVES": ["person-soren"], "FOR_ORGANIZATION": ["org-brightmoor"] } },
|
|
33
|
+
{ "id": "deal-harbourline", "class": "Deal", "name": "Harbourline service plan (Jurgen Kessler)", "relations": { "INVOLVES": ["person-jurgen"], "FOR_ORGANIZATION": ["org-harbourline"] } },
|
|
34
|
+
{ "id": "deal-audit", "class": "Deal", "name": "Brightmoor line audit", "relations": { "INVOLVES": ["person-priya"], "FOR_ORGANIZATION": ["org-brightmoor"] } },
|
|
35
|
+
{ "id": "deal-survey", "class": "Deal", "name": "Northbay site survey", "relations": { "INVOLVES": ["person-dana"], "FOR_ORGANIZATION": ["org-northbay"] } },
|
|
36
|
+
|
|
37
|
+
{ "id": "note-1", "class": "Note", "name": "Note 1", "fields": { "body": "Call with Mimi T.: she wants four filters before March." }, "relations": { "ABOUT": ["deal-northbay"] } },
|
|
38
|
+
{ "id": "note-2", "class": "Note", "name": "Note 2", "fields": { "body": "Bob Haldane asked for the new price list." }, "relations": { "ABOUT": ["person-robert"] } },
|
|
39
|
+
{ "id": "note-3", "class": "Note", "name": "Note 3", "fields": { "body": "Vestergaard, S. confirmed the trial for May." }, "relations": { "ABOUT": ["deal-brightmoor"] } },
|
|
40
|
+
{ "id": "note-4", "class": "Note", "name": "Note 4", "fields": { "body": "F. Okafor wants the invoice split in two." }, "relations": { "ABOUT": ["deal-ridgeway"] } },
|
|
41
|
+
{ "id": "note-5", "class": "Note", "name": "Note 5", "fields": { "body": "D. Okafor sent the site drawings." }, "relations": { "ABOUT": ["deal-survey"] } },
|
|
42
|
+
{ "id": "note-6", "class": "Note", "name": "Note 6", "fields": { "body": "Juergen Kessler called back about the service plan." }, "relations": { "ABOUT": ["deal-harbourline"] } },
|
|
43
|
+
{ "id": "note-7", "class": "Note", "name": "Note 7", "fields": { "body": "Forwarded the quote to k.marsh@fernway.example." }, "relations": { "ABOUT": ["lead-kit"] } },
|
|
44
|
+
{ "id": "note-8", "class": "Note", "name": "Note 8", "fields": { "body": "Priya Ramaswamey will join the call on Friday." }, "relations": { "ABOUT": ["deal-audit"] } },
|
|
45
|
+
{ "id": "note-9", "class": "Note", "name": "Note 9", "fields": { "body": "Northbay Water confirmed the delivery address." }, "relations": { "ABOUT": ["deal-northbay"] } },
|
|
46
|
+
{ "id": "note-10", "class": "Note", "name": "Note 10", "fields": { "body": "Alex Brandt from Ridgeway asked about lead times." }, "relations": { "ABOUT": ["deal-ridgeway"] } },
|
|
47
|
+
{ "id": "note-11", "class": "Note", "name": "Note 11", "fields": { "body": "Tobias Wren signed the renewal; Robert Haldane is in copy." }, "relations": { "ABOUT": ["deal-quillfield"] } }
|
|
48
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The samples of link, for `npm run try`: an invented CRM (samples/records.json)
|
|
2
|
+
// of organizations, contacts, leads, deals and notes, committed with the
|
|
3
|
+
// classes, fields and relations it needs. Every name, company and address
|
|
4
|
+
// is invented.
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { LbbClient, Schemas } from "@littlebigbrain/client";
|
|
8
|
+
|
|
9
|
+
interface SampleRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
class: string;
|
|
12
|
+
name: string;
|
|
13
|
+
fields?: Record<string, string>;
|
|
14
|
+
relations?: Record<string, string[]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The ontology of the samples: their classes, fields and relations. */
|
|
18
|
+
export function sampleOntology(records: SampleRecord[]): Schemas["OntologyEvolveOp"][] {
|
|
19
|
+
const classes = [...new Set(records.map((r) => r.class))];
|
|
20
|
+
const fields = [...new Set(records.flatMap((r) => Object.keys(r.fields ?? {})))];
|
|
21
|
+
const relations = new Map<string, { domain: Set<string>; range: Set<string> }>();
|
|
22
|
+
const classOf = new Map(records.map((r) => [r.id, r.class]));
|
|
23
|
+
for (const r of records)
|
|
24
|
+
for (const [relation, targets] of Object.entries(r.relations ?? {})) {
|
|
25
|
+
const entry = relations.get(relation) ?? { domain: new Set(), range: new Set() };
|
|
26
|
+
entry.domain.add(r.class);
|
|
27
|
+
for (const t of targets) entry.range.add(classOf.get(t)!);
|
|
28
|
+
relations.set(relation, entry);
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
...classes.map((name) => ({ op: "add_entity_type" as const, name })),
|
|
32
|
+
...fields.map((name) => ({
|
|
33
|
+
op: "add_property" as const,
|
|
34
|
+
name,
|
|
35
|
+
value_type: name === "body" ? "text" : "keyword",
|
|
36
|
+
})),
|
|
37
|
+
...[...relations].map(([name, { domain, range }]) => ({
|
|
38
|
+
op: "add_relation" as const,
|
|
39
|
+
name,
|
|
40
|
+
domain: [...domain],
|
|
41
|
+
range: [...range],
|
|
42
|
+
})),
|
|
43
|
+
] as Schemas["OntologyEvolveOp"][];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Add the classes of the samples, then commit the records and their relations to the client's graph. */
|
|
47
|
+
export async function loadSamples(client: LbbClient, dir: string): Promise<void> {
|
|
48
|
+
const records = JSON.parse(await readFile(join(dir, "records.json"), "utf8")) as SampleRecord[];
|
|
49
|
+
await client.ontology.evolve({ ops: sampleOntology(records) });
|
|
50
|
+
const ref = (r: SampleRecord) => ({ type: r.class, name: r.name, key: r.id });
|
|
51
|
+
const byId = new Map(records.map((r) => [r.id, r]));
|
|
52
|
+
await client.commit(
|
|
53
|
+
{
|
|
54
|
+
entity_properties: records.map((r) => ({ ...ref(r), properties: { ...(r.fields ?? {}) } })),
|
|
55
|
+
triplets: records.flatMap((r) =>
|
|
56
|
+
Object.entries(r.relations ?? {}).flatMap(([relation, targets]) =>
|
|
57
|
+
targets.map((t) => ({ source: ref(r), relation, target: ref(byId.get(t)!) })),
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
{ idempotencyKey: "link-samples" },
|
|
62
|
+
);
|
|
63
|
+
console.log(`Committed ${records.length} records`);
|
|
64
|
+
}
|