@littlebigbrain/create 0.2.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 CHANGED
@@ -20,6 +20,7 @@ npm test
20
20
  | `ontology-fit` | Proposes the ontology changes the texts of a class support, as review items |
21
21
  | `documents-map` | Maps the links of `documents-link` into the graph's own ontology, as fact proposals |
22
22
  | `review` | Writes the fact proposals at or above a gate as facts and gives the others to a person |
23
+ | `link` | Connects the mentions of one thing across the records of a graph into one thing, and proposes records of one thing as an identity |
23
24
 
24
25
  A template id is the id of its starter workflow, with dashes for dots:
25
26
  `documents-link` is the code of `documents.link`.
@@ -31,7 +32,8 @@ A template id is the id of its starter workflow, with dashes for dots:
31
32
  | `src/workflow.ts` | The workflow, its settings, the triggered workflow that sends it the instances (`trigger()`) and the ontology it writes (`ontology()`) |
32
33
  | `src/*.ts` | The template's helpers and `samples.ts`, which puts the samples on a graph |
33
34
  | `src/workflow.test.ts`, `src/testing.ts` | The workflow on its samples, with mock models and no server |
34
- | `src/worker.ts`, `src/setup.ts`, `src/try.ts` | `npm start`, `npm run setup` and `npm run try` |
35
+ | `src/worker.ts`, `src/setup.ts`, `src/try.ts` | `npm start`, `npm run setup` and `npm run try`; a template that has its own (`link`) keeps them |
36
+ | `src/grade-run.ts` | `npm run grade`, in a template that grades a run against a gold file (`link`) |
35
37
  | `samples/` | The samples and their answer key |
36
38
 
37
39
  `npm run try` creates a graph (`LBB_GRAPH`, default `try-<template>`), puts
package/lib/create.mjs CHANGED
@@ -21,6 +21,7 @@ export const TEMPLATES = [
21
21
  "review",
22
22
  "values-extract",
23
23
  "ontology-fit",
24
+ "link",
24
25
  ];
25
26
 
26
27
  const PACKAGE = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -119,15 +120,23 @@ export async function create({ template, dir, client, root = PACKAGE }) {
119
120
  const src = join(dir, "src");
120
121
  await mkdir(src, { recursive: true });
121
122
 
122
- // The template's code, its test and the test support, all under src/.
123
- for (const file of await readdir(from)) {
124
- if (!file.endsWith(".ts")) continue;
123
+ // The template's code, its test and the test support, all under src/, the
124
+ // template's folders (`sources/`) as they are.
125
+ const code = await templateCode(from);
126
+ for (const file of code) {
125
127
  const text = await readFile(join(from, file), "utf8");
126
- await writeFile(join(src, file), text.replaceAll("../_shared/testing.js", "./testing.js"));
128
+ const up = "../".repeat(file.split("/").length - 1);
129
+ await mkdir(dirname(join(src, file)), { recursive: true });
130
+ await writeFile(
131
+ join(src, file),
132
+ text.replaceAll(`../${up}_shared/testing.js`, `${up || "./"}testing.js`),
133
+ );
127
134
  }
128
135
  await cp(join(templates, "_shared", "testing.ts"), join(src, "testing.ts"));
129
136
  await cp(join(from, "samples"), join(dir, "samples"), { recursive: true });
137
+ // A template's own commands win: a workflow of several parts sets itself up.
130
138
  for (const file of ["worker.ts", "setup.ts", "try.ts"]) {
139
+ if (code.includes(file)) continue;
131
140
  const text = await readFile(join(root, "project", file), "utf8");
132
141
  await writeFile(join(src, file), text.replaceAll("__TEMPLATE__", template));
133
142
  }
@@ -144,6 +153,7 @@ export async function create({ template, dir, client, root = PACKAGE }) {
144
153
  try: "tsx src/try.ts",
145
154
  test: "tsx --test src/workflow.test.ts",
146
155
  typecheck: "tsc --noEmit",
156
+ ...(code.includes("grade-run.ts") ? { grade: "tsx src/grade-run.ts" } : {}),
147
157
  },
148
158
  dependencies: { "@littlebigbrain/client": client ?? `^${clientVersion(templates)}` },
149
159
  devDependencies: { "@types/node": "^22", tsx: "^4", typescript: "^5.6" },
@@ -209,6 +219,18 @@ ${projectReadme(readme).trim()}
209
219
  return listFiles(dir);
210
220
  }
211
221
 
222
+ /**
223
+ * The TypeScript files of a template folder, relative, in its folders too;
224
+ * the samples are copied on their own.
225
+ * @param {string} from
226
+ * @returns {Promise<string[]>}
227
+ */
228
+ async function templateCode(from) {
229
+ return (await listFiles(from)).filter(
230
+ (file) => file.endsWith(".ts") && !file.startsWith("samples/"),
231
+ );
232
+ }
233
+
212
234
  /** Every file under `dir`, relative and sorted. @param {string} dir @returns {Promise<string[]>} */
213
235
  async function listFiles(dir, prefix = "") {
214
236
  /** @type {string[]} */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlebigbrain/create",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Create a project from a little big brain starter workflow template: npm create @littlebigbrain",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -405,7 +405,14 @@ export async function runTurn<S>(
405
405
  definition: WorkflowDefinition<S, WorkflowTriggerMessage>,
406
406
  state: unknown,
407
407
  message: WorkflowTriggerMessage,
408
- options: { prefix?: string; saved?: Turn; crashAfter?: number } = {},
408
+ options: {
409
+ prefix?: string;
410
+ saved?: Turn;
411
+ crashAfter?: number;
412
+ /** The engine identity that runs the turn (default `starter.test`), and the turn's number. */
413
+ workflowId?: string;
414
+ turn?: number;
415
+ } = {},
409
416
  ): Promise<{ result: TurnResult<S>; turn: Turn }> {
410
417
  const prefix = options.prefix ?? mockId(JSON.stringify(message));
411
418
  const turn: Turn = { message, keys: [], outputs: [], bytes: 0, progress: [] };
@@ -413,9 +420,9 @@ export async function runTurn<S>(
413
420
  let ran = 0;
414
421
  const ctx = {
415
422
  signal: new AbortController().signal,
416
- workflowId: "starter.test",
423
+ workflowId: options.workflowId ?? "starter.test",
417
424
  messageId: "starter-test-1",
418
- turn: 1,
425
+ turn: options.turn ?? 1,
419
426
  async step<T>(
420
427
  key: string,
421
428
  effect: (s: {
@@ -480,17 +487,23 @@ export async function runTurn<S>(
480
487
  /**
481
488
  * Run a message and every continuation it leads to, from the empty state the
482
489
  * server creates the instance with. Returns the final state and the turns.
490
+ * With `workflowId` and `turn`, the turns run as that identity, numbered
491
+ * from `turn` on.
483
492
  */
484
493
  export async function runMessage<S>(
485
494
  definition: WorkflowDefinition<S, WorkflowTriggerMessage>,
486
495
  message: WorkflowTriggerMessage,
487
496
  state: unknown = {},
497
+ options: { workflowId?: string; turn?: number } = {},
488
498
  ) {
489
499
  const turns: Turn[] = [];
490
500
  let next: WorkflowTriggerMessage | undefined = message;
491
501
  while (next) {
492
502
  if (turns.length > 100) throw new Error("more than 100 turns");
493
- const ran: { result: TurnResult<S>; turn: Turn } = await runTurn(definition, state, next);
503
+ const ran: { result: TurnResult<S>; turn: Turn } = await runTurn(definition, state, next, {
504
+ ...(options.workflowId ? { workflowId: options.workflowId } : {}),
505
+ ...(options.turn !== undefined ? { turn: options.turn + turns.length } : {}),
506
+ });
494
507
  turns.push(ran.turn);
495
508
  state = ran.result.state;
496
509
  next = ran.result.continuation?.message;
@@ -0,0 +1,203 @@
1
+ # link
2
+
3
+ Connects the mentions of one thing across the records of a graph. A
4
+ contact, a lead, a deal and a note can each name one person in a different
5
+ way: `Mira Tannenholt-Ruiz`, `Mira Ruiz`, `M. Tannenholt`, `Mimi T.`. The
6
+ workflow groups those mentions into one `LinkedThing`. The links are
7
+ references across the graph, and later checks compare what each source says
8
+ about the thing.
9
+
10
+ This is phase L1 of the design
11
+ [general linking](../../../docs/architecture/general-linking.md): identities
12
+ on records. Documents (pages) come in phase L2.
13
+
14
+ ## What it writes
15
+
16
+ | Term | Kind | Holds |
17
+ | --- | --- | --- |
18
+ | `Mention` | class | `span`, `role` (`self` or `refers`), `kind`, `field`, `offset`, `length`, `block_key` (several), `window` (the sentence of a span of text); the links and constraints `linked_to`, `must_link` and `cannot_link` |
19
+ | `LinkedThing` | class | its name (the anchor's name), `kind`, `anchor_key`, `alias` (several: the other names its members use) |
20
+ | `MENTIONED_IN` | relation, mention to record | the record that holds the mention |
21
+ | `SAME_THING_AS` | relation, mention to mention | a decided or accepted pair, with its confidence and its evidence (the sentence and the record's name) |
22
+ | `REFERS_TO` | relation, mention to `LinkedThing` | the thing the mention is in |
23
+ | `HAS_RECORD` | relation, `LinkedThing` to record | the records whose names and keys are in the thing |
24
+ | `MERGED_INTO` | relation, `LinkedThing` to `LinkedThing` | where an old thing is now: a thing whose anchor changed, or that merged |
25
+ | `SAME_AS` | relation, record to record | two records of one thing: a person writes it when they accept an identity |
26
+
27
+ A record's own names and keys are `self` mentions. Each record anchors a
28
+ thing. A span in a text field that names something is a `refers` mention.
29
+
30
+ ## How it links
31
+
32
+ 1. **Mentions.** A `name` or `key:*` field gives one `self` mention per
33
+ value. A `text` field gives `refers` mentions: first the gazetteer pass,
34
+ which keeps each span whose name or key a mention of the graph has; then
35
+ the spans written like a name or a key (`M. Tannenholt`, an email).
36
+ 2. **Block keys.** Each mention gets normal forms of its name or key as
37
+ graph facts (`block_key`): the sorted name, the initials forms, the name
38
+ parts and their sound, an email, a web address, a code. A key that more
39
+ than 200 mentions hold is a stop key and finds nothing.
40
+ 3. **Candidates.** The mentions that share a key, and the things of the
41
+ records within two hops (the deal that a note is about). At most 30, the
42
+ ones with a strong key first.
43
+ 4. **Decision.** See the next section.
44
+ 5. **Things.** The things identity groups the decided pairs. A person's
45
+ decisions are constraints. Two records are never one thing until a person
46
+ accepts their identity.
47
+
48
+ The thing's IRI is a hash of the graph and its anchor's key. The anchor is a
49
+ record's name first, then any other mention. So the same inputs give the
50
+ same IRIs. A thing whose anchor changes keeps one statement, `MERGED_INTO`
51
+ the thing that holds its members now.
52
+
53
+ ## Model reads of record values
54
+
55
+ `modelsReadRecords` is off by default (general linking 12, decision 1).
56
+
57
+ - **Off.** No model reads a record value. A strong key (an email, a web
58
+ address, a phone number, a handle, a code) links at confidence 1. Every
59
+ other candidate goes to a person, grouped by thing.
60
+ - **On.** The decision model reads the names, keys and context fields of
61
+ the records. It answers a kind question and a names question per mention,
62
+ and a same-record question per pair of records. A link at or above `gate`
63
+ (0.8) is written; a link below it goes to a person. The reasoning model
64
+ reads only the text fields that opt in: `{ "role": "text", "extract": true }`
65
+ (decision 8).
66
+
67
+ Every model call has an effect id made from its request. The same question
68
+ gets its first answer from the effect cache, at no cost.
69
+
70
+ ## Review items
71
+
72
+ | Item | One per | Accept writes |
73
+ | --- | --- | --- |
74
+ | Link (`starter:<name>:link:…`) | Thing | The kept `SAME_THING_AS` rows. A row left out keeps its mention apart from the thing. |
75
+ | No link (`starter:<name>:nolink:…`) | Thing of the best candidate | The kept rows: the model chose no thing, and a person disagrees. |
76
+ | Identity (`starter:<name>:identity:…`) | Group of 2 to 10 records | `SAME_AS` between the records, and `SAME_THING_AS` between their names. |
77
+
78
+ A person always accepts an identity (decision 2). The `identityGate` (0.99)
79
+ only orders and groups the identity items (`mapping.identity.above_gate`).
80
+ Each item has the origin `starter:<name>` with the label `link`, so the
81
+ inbox labels and patterns of the producer `link` read it.
82
+
83
+ A person's decisions feed back. A kept row is a must-link. A row left out,
84
+ or a dismissed item, is a cannot-link. No model answer joins what a
85
+ cannot-link keeps apart.
86
+
87
+ ## Settings
88
+
89
+ | Setting | Default | Meaning |
90
+ | --- | --- | --- |
91
+ | `name` | `link` | The named workflows are `<name>-<class>` and `<name>-reviews`; the things identity is `<name>-things` |
92
+ | `fields` | the samples' CRM | The role of each field of each class (below) |
93
+ | `gate` | 0.8 | A link at or above it is written, with model reads on |
94
+ | `identityGate` | 0.99 | Orders and groups the identity items |
95
+ | `modelsReadRecords` | `false` | Whether models may read record values |
96
+ | `shards` | 4 | Source shards of each named workflow |
97
+ | `sourcesPerTurn` | 16 | Records per message |
98
+ | `textPerTurn` | 8,000 | Characters of text per turn; the rest continues in a new turn |
99
+
100
+ | Field role | Gives |
101
+ | --- | --- |
102
+ | `name` | One `self` mention per value |
103
+ | `name:given`, `name:family` | One `self` mention of the two joined (`first_name+last_name`) |
104
+ | `key:email`, `key:phone`, `key:web`, `key:handle`, `key:code` | One `self` mention per value, with a strong key |
105
+ | `text`, `{ "role": "text", "extract": true }` | `refers` mentions found in the text |
106
+ | `context` | No mention; read for the decisions |
107
+
108
+ A field named `name` (or `label`) is the record's display name, which the
109
+ published graph holds as `rdfs:label`; the workflow watches it as `label`.
110
+ Any other field is a property of the record.
111
+
112
+ ## Run it
113
+
114
+ ```sh
115
+ npm create @littlebigbrain -- --template link my-links
116
+ cd my-links && npm install
117
+ npm test # the workflow on the samples, with mock models
118
+ LBB_GRAPH=try-link npm run try # the samples on a graph of your stack
119
+ LBB_GRAPH=try-link npm start # serve it
120
+ LBB_GRAPH=try-link npm run grade -- --gold samples/gold.json
121
+ LBB_GRAPH=crm npm run setup && npm start # your graph
122
+ ```
123
+
124
+ `npm run setup` creates the classes and properties, one named workflow per
125
+ class of the settings, the named workflow that watches accepted items, and
126
+ the things identity. It is safe to repeat. The workflow is for a records
127
+ graph. Do not run it on a graph where `documents.link` runs: both write
128
+ `MENTIONED_IN` and `REFERS_TO`.
129
+
130
+ ## Grade a run
131
+
132
+ `npm run grade` (`src/grade-run.ts`) prints the metrics of general linking
133
+ 9.2: link precision at the gate and its 95% Wilson interval, pairwise
134
+ precision and recall over things, cluster purity and completeness
135
+ (B-cubed), the review share, and identity precision and recall.
136
+
137
+ ```sh
138
+ LBB_URL=... LBB_API_KEY=... LBB_GRAPH=crm npm run grade -- --gold .lbb-datasets/link-eval/gold.json --save run.json
139
+ npm run grade -- --gold .lbb-datasets/link-eval/gold.json --run run.json
140
+ ```
141
+
142
+ The gold file (`lbb.link.gold.v1`) lists the groups. Each group is one real
143
+ thing, with the mentions that name it:
144
+
145
+ ```json
146
+ {
147
+ "format": "lbb.link.gold.v1",
148
+ "complete": false,
149
+ "records": { "contact-1": "https://littlebigbrain.com/e/…" },
150
+ "groups": [
151
+ { "id": "mira", "kind": "person", "mentions": [
152
+ { "record": "contact-1", "field": "name", "span": "Mira Tannenholt-Ruiz" },
153
+ { "record": "https://littlebigbrain.com/e/…", "field": "body", "span": "Mimi T.", "occurrence": 0 }
154
+ ] }
155
+ ]
156
+ }
157
+ ```
158
+
159
+ A mention names its record (an IRI, or a name of `records`), its field
160
+ (`first_name+last_name` for a joined name), its span as the field writes it,
161
+ and which equal span of the field it is (`occurrence`, default 0). With
162
+ `complete: true`, a mention that the gold does not list is a thing of its
163
+ own. Without it, a link to such a mention counts apart as unjudged. Keep a
164
+ gold file of real records out of git, under `.lbb-datasets/`.
165
+
166
+ ## The samples
167
+
168
+ `samples/records.json` is an invented CRM: 7 organizations, 12 contacts, 5
169
+ leads, 7 deals and 11 notes. Every name, company and address is invented.
170
+ The names vary: a nickname (`Bob` for `Robert`), initials (`M. Tannenholt`,
171
+ `F. Okafor`), accents and transliterations (`Jürgen`, `Juergen`, `Jurgen`;
172
+ `Søren`, `Soren`; `Zoë`, `Zoe`), name order (`Wren, Tobias`), a typo
173
+ (`Ramaswamey`), a person known only by an email, a surname that two people
174
+ share (`Okafor`), two people of one name at two organizations (`Alex
175
+ Brandt`), and company domains. `samples/gold.json` holds the gold groups.
176
+
177
+ ## How the template differs from the design
178
+
179
+ - A named workflow takes one watch trigger in this release, so each class is
180
+ a named workflow of its own. All of them run the workflow type `link`.
181
+ - The candidates by the name index and by meaning wait for work tokens that
182
+ reach those routes (N3). The keys and the neighbours find the candidates.
183
+ - A source turn sends its decided pairs in messages of at most 24,000
184
+ bytes; a larger message goes in parts (`.1`, `.2`, …) until blobs (G11).
185
+ - The things identity is an engine identity that `npm run setup` creates.
186
+ It reads the review items it filed to learn a person's decisions.
187
+
188
+ ## Files
189
+
190
+ | File | Holds |
191
+ | --- | --- |
192
+ | `workflow.ts` | The workflow, the named workflows (`workflows()`), the ontology, the ports (`linkPorts()`) and `setup()` |
193
+ | `settings.ts` | The settings and the names of the parts, and the decided message |
194
+ | `names.ts` | Normal forms and block keys |
195
+ | `candidates.ts` | The candidate reads, the scoring and the bounds |
196
+ | `decide.ts` | The questions, the packing, the link rule and the rules without models |
197
+ | `things.ts` | The grouping with constraints, anchors, IRIs, merges and splits |
198
+ | `sources/records.ts` | Field roles, and the mentions of record fields |
199
+ | `source-turn.ts`, `things-turn.ts` | The turns of a source shard and of the things identity |
200
+ | `items.ts`, `reads.ts` | The review items, and the reads of the graph |
201
+ | `grade.ts`, `grade-run.ts` | The grading, and its command |
202
+ | `fixture.ts` | The test support: the graph in memory, mock models and ports, a reviewer that follows a gold file |
203
+ | `setup.ts`, `worker.ts`, `try.ts`, `samples.ts` | The commands |