@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
package/README.md
CHANGED
|
@@ -15,9 +15,12 @@ npm test
|
|
|
15
15
|
|
|
16
16
|
| Template | Does |
|
|
17
17
|
| --- | --- |
|
|
18
|
-
| `documents-link` |
|
|
18
|
+
| `documents-link` | Connects mentions of the same thing across the documents: each reference links to where that thing is defined or described |
|
|
19
19
|
| `values-extract` | Fills properties of a class from the text of its instances, each value with a verbatim quote |
|
|
20
20
|
| `ontology-fit` | Proposes the ontology changes the texts of a class support, as review items |
|
|
21
|
+
| `documents-map` | Maps the links of `documents-link` into the graph's own ontology, as fact proposals |
|
|
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 |
|
|
21
24
|
|
|
22
25
|
A template id is the id of its starter workflow, with dashes for dots:
|
|
23
26
|
`documents-link` is the code of `documents.link`.
|
|
@@ -29,7 +32,8 @@ A template id is the id of its starter workflow, with dashes for dots:
|
|
|
29
32
|
| `src/workflow.ts` | The workflow, its settings, the triggered workflow that sends it the instances (`trigger()`) and the ontology it writes (`ontology()`) |
|
|
30
33
|
| `src/*.ts` | The template's helpers and `samples.ts`, which puts the samples on a graph |
|
|
31
34
|
| `src/workflow.test.ts`, `src/testing.ts` | The workflow on its samples, with mock models and no server |
|
|
32
|
-
| `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`) |
|
|
33
37
|
| `samples/` | The samples and their answer key |
|
|
34
38
|
|
|
35
39
|
`npm run try` creates a graph (`LBB_GRAPH`, default `try-<template>`), puts
|
package/lib/create.mjs
CHANGED
|
@@ -15,7 +15,14 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
17
17
|
/** The templates a project can start from: the folder names under `sdk/templates`. */
|
|
18
|
-
export const TEMPLATES = [
|
|
18
|
+
export const TEMPLATES = [
|
|
19
|
+
"documents-link",
|
|
20
|
+
"documents-map",
|
|
21
|
+
"review",
|
|
22
|
+
"values-extract",
|
|
23
|
+
"ontology-fit",
|
|
24
|
+
"link",
|
|
25
|
+
];
|
|
19
26
|
|
|
20
27
|
const PACKAGE = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
28
|
|
|
@@ -113,15 +120,23 @@ export async function create({ template, dir, client, root = PACKAGE }) {
|
|
|
113
120
|
const src = join(dir, "src");
|
|
114
121
|
await mkdir(src, { recursive: true });
|
|
115
122
|
|
|
116
|
-
// The template's code, its test and the test support, all under src
|
|
117
|
-
|
|
118
|
-
|
|
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) {
|
|
119
127
|
const text = await readFile(join(from, file), "utf8");
|
|
120
|
-
|
|
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
|
+
);
|
|
121
134
|
}
|
|
122
135
|
await cp(join(templates, "_shared", "testing.ts"), join(src, "testing.ts"));
|
|
123
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.
|
|
124
138
|
for (const file of ["worker.ts", "setup.ts", "try.ts"]) {
|
|
139
|
+
if (code.includes(file)) continue;
|
|
125
140
|
const text = await readFile(join(root, "project", file), "utf8");
|
|
126
141
|
await writeFile(join(src, file), text.replaceAll("__TEMPLATE__", template));
|
|
127
142
|
}
|
|
@@ -138,6 +153,7 @@ export async function create({ template, dir, client, root = PACKAGE }) {
|
|
|
138
153
|
try: "tsx src/try.ts",
|
|
139
154
|
test: "tsx --test src/workflow.test.ts",
|
|
140
155
|
typecheck: "tsc --noEmit",
|
|
156
|
+
...(code.includes("grade-run.ts") ? { grade: "tsx src/grade-run.ts" } : {}),
|
|
141
157
|
},
|
|
142
158
|
dependencies: { "@littlebigbrain/client": client ?? `^${clientVersion(templates)}` },
|
|
143
159
|
devDependencies: { "@types/node": "^22", tsx: "^4", typescript: "^5.6" },
|
|
@@ -203,6 +219,18 @@ ${projectReadme(readme).trim()}
|
|
|
203
219
|
return listFiles(dir);
|
|
204
220
|
}
|
|
205
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
|
+
|
|
206
234
|
/** Every file under `dir`, relative and sorted. @param {string} dir @returns {Promise<string[]>} */
|
|
207
235
|
async function listFiles(dir, prefix = "") {
|
|
208
236
|
/** @type {string[]} */
|
package/package.json
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
type SparqlResults,
|
|
27
27
|
type WorkflowContext,
|
|
28
28
|
type WorkflowDefinition,
|
|
29
|
+
type WorkflowProgressCounts,
|
|
30
|
+
type WorkflowStepContext,
|
|
29
31
|
type WorkflowTriggerMessage,
|
|
30
32
|
} from "@littlebigbrain/client";
|
|
31
33
|
|
|
@@ -291,12 +293,13 @@ export class MockActivities implements LbbActivities {
|
|
|
291
293
|
writeOwned(
|
|
292
294
|
ctx: WorkflowContext,
|
|
293
295
|
stepKey: string,
|
|
294
|
-
write: OwnedWrite | (() => OwnedWrite | Promise<OwnedWrite>),
|
|
296
|
+
write: OwnedWrite | ((step: WorkflowStepContext) => OwnedWrite | Promise<OwnedWrite>),
|
|
295
297
|
) {
|
|
296
|
-
return ctx.step(stepKey, async (
|
|
298
|
+
return ctx.step(stepKey, async (step) => {
|
|
299
|
+
const { effectId } = step;
|
|
297
300
|
const replayed = this.written.has(effectId);
|
|
298
301
|
if (!replayed) {
|
|
299
|
-
this.graph.apply(typeof write === "function" ? await write() : write);
|
|
302
|
+
this.graph.apply(typeof write === "function" ? await write(step) : write);
|
|
300
303
|
this.written.add(effectId);
|
|
301
304
|
}
|
|
302
305
|
return {
|
|
@@ -345,12 +348,33 @@ function assertNever(what: string): never {
|
|
|
345
348
|
|
|
346
349
|
// ---- turns -------------------------------------------------------------------------
|
|
347
350
|
|
|
348
|
-
/** One
|
|
351
|
+
/** One progress report of a step (`s.progress`), as the worker would send it. */
|
|
352
|
+
export interface ProgressCall extends WorkflowProgressCounts {
|
|
353
|
+
step: string;
|
|
354
|
+
phase: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** One turn: its step keys, their saved outputs, and the progress its steps reported. */
|
|
349
358
|
export interface Turn {
|
|
350
359
|
message: WorkflowTriggerMessage;
|
|
351
360
|
keys: string[];
|
|
352
361
|
outputs: unknown[];
|
|
353
362
|
bytes: number;
|
|
363
|
+
progress: ProgressCall[];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const UNIT = /^[\p{L}\p{N} _-]{1,32}$/u;
|
|
367
|
+
const whole = (n: unknown) => Number.isSafeInteger(n) && (n as number) >= 0;
|
|
368
|
+
|
|
369
|
+
/** A progress report checked as the worker checks it: the progress convention. */
|
|
370
|
+
function progressCall(step: string, phase: string, counts: WorkflowProgressCounts): ProgressCall {
|
|
371
|
+
const text = phase.trim();
|
|
372
|
+
if (!text || [...text].length > 80) throw new Error(`step ${step}: progress phase "${phase}"`);
|
|
373
|
+
if (!whole(counts.done) || (counts.total !== undefined && !whole(counts.total)))
|
|
374
|
+
throw new Error(`step ${step}: progress counts ${JSON.stringify(counts)}`);
|
|
375
|
+
if (counts.unit !== undefined && !UNIT.test(counts.unit))
|
|
376
|
+
throw new Error(`step ${step}: progress unit "${counts.unit}"`);
|
|
377
|
+
return { step, phase: text, ...counts };
|
|
354
378
|
}
|
|
355
379
|
|
|
356
380
|
/** What a turn returns: its state, and a continuation when the work goes on in another turn. */
|
|
@@ -381,17 +405,24 @@ export async function runTurn<S>(
|
|
|
381
405
|
definition: WorkflowDefinition<S, WorkflowTriggerMessage>,
|
|
382
406
|
state: unknown,
|
|
383
407
|
message: WorkflowTriggerMessage,
|
|
384
|
-
options: {
|
|
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
|
+
} = {},
|
|
385
416
|
): Promise<{ result: TurnResult<S>; turn: Turn }> {
|
|
386
417
|
const prefix = options.prefix ?? mockId(JSON.stringify(message));
|
|
387
|
-
const turn: Turn = { message, keys: [], outputs: [], bytes: 0 };
|
|
418
|
+
const turn: Turn = { message, keys: [], outputs: [], bytes: 0, progress: [] };
|
|
388
419
|
let busy = false;
|
|
389
420
|
let ran = 0;
|
|
390
421
|
const ctx = {
|
|
391
422
|
signal: new AbortController().signal,
|
|
392
|
-
workflowId: "starter.test",
|
|
423
|
+
workflowId: options.workflowId ?? "starter.test",
|
|
393
424
|
messageId: "starter-test-1",
|
|
394
|
-
turn: 1,
|
|
425
|
+
turn: options.turn ?? 1,
|
|
395
426
|
async step<T>(
|
|
396
427
|
key: string,
|
|
397
428
|
effect: (s: {
|
|
@@ -399,6 +430,7 @@ export async function runTurn<S>(
|
|
|
399
430
|
signal: AbortSignal;
|
|
400
431
|
checkpoint: unknown;
|
|
401
432
|
heartbeat(): Promise<void>;
|
|
433
|
+
progress(phase: string, counts: WorkflowProgressCounts): void;
|
|
402
434
|
}) => Promise<T>,
|
|
403
435
|
) {
|
|
404
436
|
if (busy) throw new Error("steps run one at a time: await each step");
|
|
@@ -423,6 +455,7 @@ export async function runTurn<S>(
|
|
|
423
455
|
signal: new AbortController().signal,
|
|
424
456
|
checkpoint: null,
|
|
425
457
|
heartbeat: async () => {},
|
|
458
|
+
progress: (phase, counts) => turn.progress.push(progressCall(key, phase, counts)),
|
|
426
459
|
});
|
|
427
460
|
const json = JSON.stringify(output ?? null);
|
|
428
461
|
if (json.length > 64 * 1024)
|
|
@@ -454,17 +487,23 @@ export async function runTurn<S>(
|
|
|
454
487
|
/**
|
|
455
488
|
* Run a message and every continuation it leads to, from the empty state the
|
|
456
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.
|
|
457
492
|
*/
|
|
458
493
|
export async function runMessage<S>(
|
|
459
494
|
definition: WorkflowDefinition<S, WorkflowTriggerMessage>,
|
|
460
495
|
message: WorkflowTriggerMessage,
|
|
461
496
|
state: unknown = {},
|
|
497
|
+
options: { workflowId?: string; turn?: number } = {},
|
|
462
498
|
) {
|
|
463
499
|
const turns: Turn[] = [];
|
|
464
500
|
let next: WorkflowTriggerMessage | undefined = message;
|
|
465
501
|
while (next) {
|
|
466
502
|
if (turns.length > 100) throw new Error("more than 100 turns");
|
|
467
|
-
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
|
+
});
|
|
468
507
|
turns.push(ran.turn);
|
|
469
508
|
state = ran.result.state;
|
|
470
509
|
next = ran.result.continuation?.message;
|
|
@@ -472,6 +511,11 @@ export async function runMessage<S>(
|
|
|
472
511
|
return { state: state as S, turns };
|
|
473
512
|
}
|
|
474
513
|
|
|
514
|
+
/** The phases the steps of `turns` reported, each once, in the order they first came. */
|
|
515
|
+
export function phases(turns: Turn[]): string[] {
|
|
516
|
+
return [...new Set(turns.flatMap((t) => t.progress.map((p) => p.phase)))];
|
|
517
|
+
}
|
|
518
|
+
|
|
475
519
|
/** A trigger message of the triggered workflow `workflow`. */
|
|
476
520
|
export function triggerMessage(
|
|
477
521
|
workflow: string,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# documents-link
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Connects mentions of the same thing across the documents of a graph. It
|
|
4
|
+
finds the identifiers the documents define (the codes of a parts list, the
|
|
5
|
+
sections of a specification) and the spans that refer to them. Each
|
|
6
|
+
mention links to the entry of the same thing, in the same document or in
|
|
7
|
+
another one. A manual's `P114` gets a `REFERS_TO` link to `P 114` of the
|
|
8
|
+
parts list, with the line as evidence and the span's box on the page as
|
|
9
|
+
its region.
|
|
8
10
|
|
|
9
11
|
The LBB-run version is the starter workflow `documents.link@v1`
|
|
10
12
|
(`docs/reference/starter-workflows.md`, "Document workflows"). This
|
|
@@ -97,12 +99,12 @@ classes `DocEntry` and `DocMention` and the relations `DEFINED_IN`,
|
|
|
97
99
|
definition across the whole document; here it stops at the end of the
|
|
98
100
|
window.
|
|
99
101
|
- **Number tokens and page heads.** Candidates also come from the number
|
|
100
|
-
tokens of a span (`
|
|
102
|
+
tokens of a span (`Section 2.4` finds `Ref. 2.4`), and an own number
|
|
101
103
|
gets candidates by the meaning of its page head.
|
|
102
104
|
- **Roles as a setting.** `params.roles` of the triggered workflow, so a
|
|
103
105
|
request changes them without new code.
|
|
104
106
|
- **Same-item questions.** A span that is the number of the item its page
|
|
105
|
-
is about (a
|
|
107
|
+
is about (a product sheet's own number) is asked which entry describes the
|
|
106
108
|
same item, and links with `SAME_ITEM_AS`. Here `own_number` does not
|
|
107
109
|
link.
|
|
108
110
|
- **Request size estimate.** A decide request is packed by an estimate of
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Crash,
|
|
18
18
|
MockActivities,
|
|
19
19
|
mockId,
|
|
20
|
+
phases,
|
|
20
21
|
readJson,
|
|
21
22
|
runMessage,
|
|
22
23
|
runTurn,
|
|
@@ -362,8 +363,34 @@ test("a long document is read in windows over several turns", async () => {
|
|
|
362
363
|
assert.equal(lbb.graph.edgesOf("REFERS_TO").length, 225);
|
|
363
364
|
});
|
|
364
365
|
|
|
366
|
+
test("each step reports its phase as the LBB-run documents.link words it", async () => {
|
|
367
|
+
const { turns } = await run();
|
|
368
|
+
assert.deepEqual(phases(turns), [
|
|
369
|
+
"reading pages",
|
|
370
|
+
"writing facts",
|
|
371
|
+
"embedding the catalogue",
|
|
372
|
+
"linking mentions",
|
|
373
|
+
]);
|
|
374
|
+
const first = turns[0]!.progress[0]!;
|
|
375
|
+
assert.deepEqual(
|
|
376
|
+
[first.step, first.phase, first.done, first.unit],
|
|
377
|
+
["define-p1", "reading pages", 0, "pages"],
|
|
378
|
+
);
|
|
379
|
+
const linking = turns.at(-1)!.progress.filter((p) => p.phase === "linking mentions");
|
|
380
|
+
assert.deepEqual(
|
|
381
|
+
linking.map((p) => p.done),
|
|
382
|
+
linking.map((_, n) => n),
|
|
383
|
+
);
|
|
384
|
+
assert.ok(linking.every((p) => p.total === linking.length && p.unit === "pages"));
|
|
385
|
+
});
|
|
386
|
+
|
|
365
387
|
test("links below the gate go to review instead of the graph", async () => {
|
|
366
|
-
const { lbb, docs, state } = await run({ gate: 0.99 });
|
|
388
|
+
const { lbb, docs, state, turns } = await run({ gate: 0.99 });
|
|
389
|
+
const filing = turns.flatMap((t) => t.progress).filter((p) => p.phase === "filing review items");
|
|
390
|
+
assert.deepEqual(
|
|
391
|
+
filing.map((p) => [p.done, p.total, p.unit]),
|
|
392
|
+
filing.map((_, k) => [k, 13, "items"]),
|
|
393
|
+
);
|
|
367
394
|
const reviewed = [...lbb.reviews.values()];
|
|
368
395
|
const linked = reviewed.filter((r) => r.key!.includes(":link:"));
|
|
369
396
|
// One item per document, span and entry: S1.1 and S 2.1 are on both room sheets.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// documents-link:
|
|
2
|
-
// them, and link each mention to
|
|
1
|
+
// documents-link: connect mentions of the same thing across the documents of a graph. Find the
|
|
2
|
+
// identifiers the documents define and the spans that refer to them, and link each mention to the
|
|
3
|
+
// entry it names.
|
|
3
4
|
//
|
|
4
5
|
// A triggered workflow of the starter workflow `workflow` watches the class `Document` and its
|
|
5
6
|
// field `parse_status` (LBB's parse writes it), and sends the parsed documents here. The workflow
|
|
@@ -12,8 +13,8 @@
|
|
|
12
13
|
// candidates from the catalogue (every `DocEntry` of the graph) by characters and by meaning
|
|
13
14
|
// (`embed`), and the decision model (`decide`) answers what the span is and which entry it
|
|
14
15
|
// names. A mention is written as a `DocMention` `MENTIONED_IN` the document, with a
|
|
15
|
-
// `REFERS_TO` link to
|
|
16
|
-
// gate becomes a review item.
|
|
16
|
+
// `REFERS_TO` link to the entry it names when the link's confidence reaches the gate; a link
|
|
17
|
+
// below the gate becomes a review item.
|
|
17
18
|
//
|
|
18
19
|
// Each model call runs in a step and passes the step's effect id, so a turn that runs again after
|
|
19
20
|
// a crash pays for nothing twice.
|
|
@@ -254,6 +255,7 @@ async function define(
|
|
|
254
255
|
const found: link.Definition[] = [];
|
|
255
256
|
for (let page = w.from; page < w.to; page++) {
|
|
256
257
|
const kept = await ctx.step(`define-p${page + 1}`, async (s) => {
|
|
258
|
+
s.progress("reading pages", { done: page - w.from, total: w.to - w.from, unit: "pages" });
|
|
257
259
|
const p = await read.page(page);
|
|
258
260
|
const answer = await lbb.reason(link.definitionsRequest(p), {
|
|
259
261
|
effectId: s.effectId,
|
|
@@ -266,9 +268,10 @@ async function define(
|
|
|
266
268
|
// A commit names at most 1,000 entities: the entries go in parts of at most 900.
|
|
267
269
|
const count = new Set(found.map((d) => d.identifier)).size;
|
|
268
270
|
for (let n = 0; n * link.PART < count; n++)
|
|
269
|
-
await lbb.writeOwned(ctx, `entries-${n}`, async () =>
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
await lbb.writeOwned(ctx, `entries-${n}`, async (s) => {
|
|
272
|
+
s.progress("writing facts", { done: n * link.PART, total: count, unit: "items" });
|
|
273
|
+
return link.part(link.entriesWrite(doc, await read.pages(), found), n);
|
|
274
|
+
});
|
|
272
275
|
return { ...state, entries: state.entries + count };
|
|
273
276
|
}
|
|
274
277
|
|
|
@@ -287,6 +290,7 @@ async function linkMentions(
|
|
|
287
290
|
const mentions: link.Mention[][] = [];
|
|
288
291
|
for (const page of pages) {
|
|
289
292
|
const kept = await ctx.step(`mention-p${page + 1}`, async (s) => {
|
|
293
|
+
s.progress("reading pages", { done: page - w.from, total: w.to - w.from, unit: "pages" });
|
|
290
294
|
const p = await read.page(page);
|
|
291
295
|
const answer = await lbb.reason(link.mentionsRequest(p), {
|
|
292
296
|
effectId: s.effectId,
|
|
@@ -296,12 +300,16 @@ async function linkMentions(
|
|
|
296
300
|
});
|
|
297
301
|
mentions.push(kept);
|
|
298
302
|
}
|
|
299
|
-
const byMeaning = await ctx.step("meaning", (s) =>
|
|
303
|
+
const byMeaning = await ctx.step("meaning", (s) => {
|
|
304
|
+
s.progress("embedding the catalogue", { done: 0, unit: "items" });
|
|
305
|
+
return meaning(s.effectId, lbb, doc, read, mentions);
|
|
306
|
+
});
|
|
300
307
|
const resolved: link.Resolved[][] = [];
|
|
301
308
|
for (const [n, page] of pages.entries()) {
|
|
302
|
-
const answers = await ctx.step(`decide-p${page + 1}`, (s) =>
|
|
303
|
-
|
|
304
|
-
|
|
309
|
+
const answers = await ctx.step(`decide-p${page + 1}`, (s) => {
|
|
310
|
+
s.progress("linking mentions", { done: n, total: pages.length, unit: "pages" });
|
|
311
|
+
return decidePage(s.effectId, lbb, doc, read, page, mentions[n]!, byMeaning[n]!);
|
|
312
|
+
});
|
|
305
313
|
resolved.push(answers);
|
|
306
314
|
}
|
|
307
315
|
|
|
@@ -316,7 +324,7 @@ async function linkMentions(
|
|
|
316
324
|
);
|
|
317
325
|
const written = all.filter((x) => x.target !== null && x.confidence >= settings.gate);
|
|
318
326
|
const below = all.filter((x) => x.target !== null && x.confidence < settings.gate);
|
|
319
|
-
await ctx.step("review", async () => {
|
|
327
|
+
await ctx.step("review", async (s) => {
|
|
320
328
|
const catalogue = await read.catalogue();
|
|
321
329
|
// One item per span and entry: S1.1 on two pages is one item with two links.
|
|
322
330
|
const groups = new Map<string, typeof below>();
|
|
@@ -331,7 +339,8 @@ async function linkMentions(
|
|
|
331
339
|
group: group.slice(n * 100, (n + 1) * 100),
|
|
332
340
|
})),
|
|
333
341
|
);
|
|
334
|
-
for (const { n, group } of items) {
|
|
342
|
+
for (const [k, { n, group }] of items.entries()) {
|
|
343
|
+
s.progress("filing review items", { done: k, total: items.length, unit: "items" });
|
|
335
344
|
const entry = catalogue[group[0]!.target!]!;
|
|
336
345
|
const span = group[0]!.m.span;
|
|
337
346
|
const lowest = Math.min(...group.map((x) => x.confidence));
|
|
@@ -357,7 +366,8 @@ async function linkMentions(
|
|
|
357
366
|
return items.length;
|
|
358
367
|
});
|
|
359
368
|
for (let n = 0; n * link.PART < all.length; n++)
|
|
360
|
-
await lbb.writeOwned(ctx, `mentions-${n}`, async () => {
|
|
369
|
+
await lbb.writeOwned(ctx, `mentions-${n}`, async (s) => {
|
|
370
|
+
s.progress("writing facts", { done: n * link.PART, total: all.length, unit: "mentions" });
|
|
361
371
|
const [pages, catalogue] = [await read.pages(), await read.catalogue()];
|
|
362
372
|
const links = written.map((x) => ({ ...x, entry: catalogue[x.target!]! }));
|
|
363
373
|
return link.part(link.mentionsWrite(doc, pages, all, links), n);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# documents-map
|
|
2
|
+
|
|
3
|
+
Maps the links of `documents-link` into the graph's own ontology. A
|
|
4
|
+
manual's line `Replace the filter P114 every 500 hours` links the span
|
|
5
|
+
`P114` to the entry `P 114` of the parts list. This workflow asks which
|
|
6
|
+
instance each entry is and which statement the line makes, and proposes
|
|
7
|
+
the fact `procedure 4.2 USES_PART part P 114`. It writes each statement as a
|
|
8
|
+
`FactProposal`; a `review` workflow (LBB's `review`, or the
|
|
9
|
+
[`review`](../review/README.md) template) decides which become facts.
|
|
10
|
+
|
|
11
|
+
The LBB-run version is the starter workflow `documents.map@v1`
|
|
12
|
+
(`docs/reference/starter-workflows.md`, `documents.map`; design
|
|
13
|
+
`docs/architecture/document-output-mapping.md`). This template is the same
|
|
14
|
+
pipeline in TypeScript: the workflow in `workflow.ts`, its questions, rules
|
|
15
|
+
and proposals in `map.ts`.
|
|
16
|
+
|
|
17
|
+
## How it runs
|
|
18
|
+
|
|
19
|
+
The triggered workflow watches the class `DocMention` and its links
|
|
20
|
+
`REFERS_TO` and `SAME_ITEM_AS`. A new, changed or withdrawn link sends a
|
|
21
|
+
message. A message is a signal: each one maps every link of the graph
|
|
22
|
+
again, in one turn. The decision model's calls take an effect id made from
|
|
23
|
+
the content of their request, so a question asked again in a later message
|
|
24
|
+
gets its first answer from LBB's effect cache at no cost. Only new and
|
|
25
|
+
changed questions cost.
|
|
26
|
+
|
|
27
|
+
1. **Read the trace** (`readTrace`). The links with their confidence (on the
|
|
28
|
+
link's evidence statement node), the mentions of the documents that hold
|
|
29
|
+
them, and the entries, with SPARQL at the message's commit.
|
|
30
|
+
2. **The subject of each link** (`map.subjects`). The object is the entry
|
|
31
|
+
the span names. The subject is what the line belongs to, in the line's
|
|
32
|
+
own document. The first mention of a page heads the page when all of
|
|
33
|
+
these are true: the page has at least two mentions, the mention starts in
|
|
34
|
+
the top 30% of the page, no entry the document defines on the page is on
|
|
35
|
+
its row or above it, and it is written unlike most of the page's other
|
|
36
|
+
mentions (a product sheet's number above the codes it uses). Then its
|
|
37
|
+
link's entry, or else a page entry made from it (the span is the
|
|
38
|
+
identifier, the rest of its line the description), is the subject of
|
|
39
|
+
every other mention of the page, and of the next page when that has
|
|
40
|
+
neither a head nor an entry. Otherwise the subject is the last anchor at
|
|
41
|
+
or before the line (the document's own entries written like its first
|
|
42
|
+
one, such as the positions of a bill).
|
|
43
|
+
3. **Ground the entries.** Each end of a link gets candidates: the
|
|
44
|
+
instances whose `identifier`, `rdfs:label`, `skos:notation`,
|
|
45
|
+
`skos:prefLabel`, `schema:identifier` or `dcterms:identifier` is the
|
|
46
|
+
identifier as written, without spaces, or with one space after its
|
|
47
|
+
letters (at most 10, each with its class, label and four facts). The
|
|
48
|
+
decision model answers which instance the entry is: a candidate,
|
|
49
|
+
`new_instance` or `none`. A new instance gets a second question, its
|
|
50
|
+
class among at most 12 classes, or `no_class`. Its IRI is minted from the
|
|
51
|
+
document and the identifier, so the same entry always proposes the same
|
|
52
|
+
instance.
|
|
53
|
+
4. **Type the links.** For a link whose two ends are instances with a
|
|
54
|
+
class, the options are the relations whose domain and range fit the two
|
|
55
|
+
classes (superclasses included, both ways round, an inverse written the
|
|
56
|
+
right way round), at most 12, and `same_item`, `no_fact` and
|
|
57
|
+
`missing_property`. The state holds the page's rows (the parsed page, or
|
|
58
|
+
the lines of its mentions when the file has no parse), the two instances
|
|
59
|
+
and the part of the ontology the questions use. A declared mapping types
|
|
60
|
+
its links without a question.
|
|
61
|
+
5. **Propose.** One `FactProposal` per statement (its key is
|
|
62
|
+
`proposal:<hash>` of the statement and the proposer, `proposed_by` is
|
|
63
|
+
`workflow/<name>`): a typed link with the link's confidence times the
|
|
64
|
+
typing answer's probability; `same_item` as `owl:sameAs`; an entry
|
|
65
|
+
grounded to an instance as `DENOTES` from the entry (or from the mention
|
|
66
|
+
of a page head) to the instance; a new instance with a class as its
|
|
67
|
+
`rdf:type` (the grounding answer's probability times the class answer's),
|
|
68
|
+
its `rdfs:label`, its `identifier` and its `DENOTES`. `fact_evidence` holds
|
|
69
|
+
the line, the page, the box, the link and every option with its
|
|
70
|
+
probability. An entry whose class question chose `no_class` proposes
|
|
71
|
+
nothing (no type, label, identifier or `DENOTES`) until the ontology has a
|
|
72
|
+
class for it, and its links are not typed.
|
|
73
|
+
6. **Suggest.** The answers `no_class` and `missing_property` with a
|
|
74
|
+
probability of at least 0.5 become ontology suggestions, one per pattern
|
|
75
|
+
(see below).
|
|
76
|
+
7. **Write.** The proposals that changed since the graph's copy, owned with
|
|
77
|
+
`replace_owned` over the proposal's seven fields, in parts of 900. The
|
|
78
|
+
proposals of this workflow that no link states any more are retracted.
|
|
79
|
+
|
|
80
|
+
## Ontology suggestions
|
|
81
|
+
|
|
82
|
+
Each suggestion explains itself. Its rationale says what the decision model
|
|
83
|
+
found, from how many lines or entries, and quotes them (page, line, the two
|
|
84
|
+
ends and the probability). Its samples have the shape of a review item's
|
|
85
|
+
(`document`, `page` from 1, `line`, `box`, plus `line_id`, `span`, `subject`
|
|
86
|
+
and `object`, or `identifier` and `description` for an entry), with the
|
|
87
|
+
answer: `choice`, `answer` in words, `probability` and every option's
|
|
88
|
+
probability in `options`.
|
|
89
|
+
|
|
90
|
+
- **A relation** (`Add a relation from Room to SpecSection: SIEHE_KAP`). Its
|
|
91
|
+
name comes from the words next to the span on its lines: the two nearest
|
|
92
|
+
words before the span (else after it) that are made of letters, at least
|
|
93
|
+
three long, and not words of the two ends or of the object's document. The
|
|
94
|
+
phrase most lines share wins, in upper case ASCII. Only when the lines have
|
|
95
|
+
no such words is it `HAS_<CLASS>`. A taken name gets a number.
|
|
96
|
+
- **The overlap rule.** When an existing relation fits the two classes
|
|
97
|
+
(either way round), a line backs the suggestion only when its typing
|
|
98
|
+
question offered that relation and the answer chose `missing_property`
|
|
99
|
+
over it. The suggestion is then filed only with at least 2 such lines, or
|
|
100
|
+
one at 0.8 or more (`OVERLAP_LINES`, `OVERLAP_ALONE_AT` in `map.ts`), and
|
|
101
|
+
its rationale names the relation and the most probability the answers gave
|
|
102
|
+
it. Otherwise nothing is filed, and the lines are unmapped links.
|
|
103
|
+
- **A class** (`Add a class for 3.1 Bodenbeläge`, or `Add a class for 2
|
|
104
|
+
entries of baubeschreibung.pdf: 3.1 …, 3.2 …`). Its title and rationale
|
|
105
|
+
name its entries; the rationale lists the classes the answers weighed and
|
|
106
|
+
the entries of the same document, written the same way, that got a class
|
|
107
|
+
of the ontology. A person who finds those belong to the new class too
|
|
108
|
+
dismisses their types in the review.
|
|
109
|
+
|
|
110
|
+
Edit the name, then accept (the accept request's `change`, or **Edit, then
|
|
111
|
+
accept** in the console): the next message maps the links of the pattern
|
|
112
|
+
again.
|
|
113
|
+
|
|
114
|
+
**Unmapped links.** A link that makes no proposal and backs no filed
|
|
115
|
+
suggestion stays in the workflow's state: `unmapped` counts them and
|
|
116
|
+
`unmappedLinks` holds the first 50, by document, page and line, each with
|
|
117
|
+
its line, the two ends, its `outcome` (`no_subject`, `ungrounded`,
|
|
118
|
+
`same_instance`, `unclassed`, `unanswered`, `no_fact`, `lacks`), a `reason`
|
|
119
|
+
in words and the typing answer when it has one.
|
|
120
|
+
|
|
121
|
+
The questions go in requests of at most 32 questions and 60,000 characters:
|
|
122
|
+
grounding and classes per defining document, typing per page. A refused
|
|
123
|
+
request is asked in halves.
|
|
124
|
+
|
|
125
|
+
## Settings
|
|
126
|
+
|
|
127
|
+
`defaults` in `workflow.ts`:
|
|
128
|
+
|
|
129
|
+
| Setting | Default | Meaning |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| `mappings` | none | Declared mappings: `{ from: { document?, entry_class }, to: { entry_class }, property }`. A link from an instance of `from.entry_class` (in `document`) to one of `to.entry_class` (subclasses too) states `property`, with the link's confidence, and no model is asked |
|
|
132
|
+
|
|
133
|
+
`trigger()` names the triggered workflow `documents-map` and owns the class
|
|
134
|
+
`FactProposal` and its seven proposer fields. `ontology()` adds
|
|
135
|
+
`FactProposal` and its fields, `identifier`, and the relation `DENOTES`,
|
|
136
|
+
open on both sides (from an entry or a page's head mention to any
|
|
137
|
+
instance), as LBB's `documents.map` declares it. Do not run it beside an
|
|
138
|
+
LBB-run
|
|
139
|
+
`documents.map` on the same graph: they own the same class.
|
|
140
|
+
|
|
141
|
+
## What the LBB-run version adds
|
|
142
|
+
|
|
143
|
+
- **Links below the gate.** It reads the stored outputs of the graph's
|
|
144
|
+
`documents.link` workflows, so it maps the links held for review too, with
|
|
145
|
+
their confidence. The template reads the links in the graph.
|
|
146
|
+
- **The RDF schema.** It reads the RDFS and OWL schema of the published
|
|
147
|
+
generation beside the records ontology, and shows each term's
|
|
148
|
+
`rdfs:label` and `rdfs:comment` in the options. The template reads the
|
|
149
|
+
records ontology (`GET /v1/ontology`).
|
|
150
|
+
- **More candidates.** The name index of `POST /v1/query/ask` and search by
|
|
151
|
+
meaning over the graph's embeddings; class options by meaning. The
|
|
152
|
+
template finds candidates by identifier and orders classes by shared
|
|
153
|
+
words.
|
|
154
|
+
- **Descriptions.** An entry's description comes from the definitions
|
|
155
|
+
model. The graph holds the entry's context, which can run on to the next
|
|
156
|
+
rows; the template uses it.
|
|
157
|
+
- **Both graph kinds by name.** It proposes the records relation
|
|
158
|
+
`DENOTES` (`https://littlebigbrain.com/r/denotes`) on a records graph, and
|
|
159
|
+
`lbb:denotes` (`https://littlebigbrain.com/ns#denotes`) and the schema's
|
|
160
|
+
own property IRIs on an RDF-native graph. The template proposes the
|
|
161
|
+
records relations (`https://littlebigbrain.com/r/<name>`), `DENOTES`
|
|
162
|
+
included, on both graph kinds.
|
|
163
|
+
- **Scopes per document.** On an RDF-native graph it owns its proposals per
|
|
164
|
+
document (a proposal from several documents in the scope of the smallest
|
|
165
|
+
SHA-256), in one owned write per step. The template writes the proposals
|
|
166
|
+
with `replace_owned` on both graph kinds.
|
|
167
|
+
- **Any number of links.** It runs in time-limited steps over any number of
|
|
168
|
+
documents and keeps its typed links per document. The template maps at
|
|
169
|
+
most 1,000 linked mentions in one turn and fails the turn (no retry) when
|
|
170
|
+
a graph needs more steps.
|
|
171
|
+
- **Requests by tokens.** Requests are packed by an estimate of their
|
|
172
|
+
tokens (28,000), per page, four at once.
|
|
173
|
+
- **A new ontology version maps again**, and a new decision model asks
|
|
174
|
+
again (its version holds the model). The template maps on a message: call
|
|
175
|
+
`client.workflows.triggered.refresh` after an ontology change, and change
|
|
176
|
+
`m1` in the effect id to ask a new model.
|
|
177
|
+
- **Stale suggestions.** It supersedes the open suggestions a later run no
|
|
178
|
+
longer finds.
|
|
179
|
+
- **Status and budget.** Counters per outcome, the `map` model job on the
|
|
180
|
+
Models page, a monthly budget and a preview. The template's calls count
|
|
181
|
+
as `decide` activities.
|
|
182
|
+
|
|
183
|
+
## Samples and test
|
|
184
|
+
|
|
185
|
+
`samples/` holds the three invented PDFs of `documents-link`, the parsed
|
|
186
|
+
pages of the room sheets, `trace.json` (what a correct `documents-link` run
|
|
187
|
+
writes on them: 15 entries, 21 mentions, 15 links) and the answer key
|
|
188
|
+
`expected.json` ([`../_samples/README.md`](../_samples/README.md)).
|
|
189
|
+
`node sdk/templates/_samples/make-trace.mjs` writes `trace.json` and the
|
|
190
|
+
copies from the samples of `documents-link`.
|
|
191
|
+
|
|
192
|
+
`npm run try` uploads the three PDFs and waits until LBB's
|
|
193
|
+
`documents.parse` has read them, so the typing questions show the rows of
|
|
194
|
+
each page. Then it adds the ontology of the samples (`Room`, `Equipment`,
|
|
195
|
+
`SpecSection`, `HAS_EQUIPMENT`, `SPECIFIED_IN`) and commits the trace
|
|
196
|
+
itself. This is the simplest way to run the map on known links: a link run
|
|
197
|
+
first would need its own models and give links that vary from run to run.
|
|
198
|
+
On your graph, run `documents.link` (or the `documents-link` template)
|
|
199
|
+
first.
|
|
200
|
+
|
|
201
|
+
`workflow.test.ts` runs the workflow on the trace with mock models: the
|
|
202
|
+
grounding question picks a candidate whose label holds the identifier, else
|
|
203
|
+
a new instance; the class question picks `Room` for the room sheets,
|
|
204
|
+
`SpecSection` for a section and `Equipment` for the rest; the typing
|
|
205
|
+
question picks the relation whose range is the object's class. The run makes
|
|
206
|
+
the 15 facts of the answer key and the 60 statements of the 15 new
|
|
207
|
+
instances, and nothing for `M 7`, `S 1.3` or `3.3`. The test also checks the
|
|
208
|
+
confidences, a second message answered from the effect cache, a turn
|
|
209
|
+
stopped part way, an existing instance, the ontology suggestions (their
|
|
210
|
+
titles, rationales, samples, names from the lines, the overlap rule, and
|
|
211
|
+
the links of an entry without a class as unmapped), a withdrawn link, a
|
|
212
|
+
refused request split in halves, a declared mapping, and the subject rules.
|