@littlebigbrain/create 0.1.0 → 0.2.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 +3 -1
- package/lib/create.mjs +7 -1
- package/package.json +1 -1
- package/templates/_shared/testing.ts +36 -5
- 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/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,11 @@ 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 |
|
|
21
23
|
|
|
22
24
|
A template id is the id of its starter workflow, with dashes for dots:
|
|
23
25
|
`documents-link` is the code of `documents.link`.
|
package/lib/create.mjs
CHANGED
|
@@ -15,7 +15,13 @@ 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
|
+
];
|
|
19
25
|
|
|
20
26
|
const PACKAGE = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
27
|
|
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. */
|
|
@@ -384,7 +408,7 @@ export async function runTurn<S>(
|
|
|
384
408
|
options: { prefix?: string; saved?: Turn; crashAfter?: number } = {},
|
|
385
409
|
): Promise<{ result: TurnResult<S>; turn: Turn }> {
|
|
386
410
|
const prefix = options.prefix ?? mockId(JSON.stringify(message));
|
|
387
|
-
const turn: Turn = { message, keys: [], outputs: [], bytes: 0 };
|
|
411
|
+
const turn: Turn = { message, keys: [], outputs: [], bytes: 0, progress: [] };
|
|
388
412
|
let busy = false;
|
|
389
413
|
let ran = 0;
|
|
390
414
|
const ctx = {
|
|
@@ -399,6 +423,7 @@ export async function runTurn<S>(
|
|
|
399
423
|
signal: AbortSignal;
|
|
400
424
|
checkpoint: unknown;
|
|
401
425
|
heartbeat(): Promise<void>;
|
|
426
|
+
progress(phase: string, counts: WorkflowProgressCounts): void;
|
|
402
427
|
}) => Promise<T>,
|
|
403
428
|
) {
|
|
404
429
|
if (busy) throw new Error("steps run one at a time: await each step");
|
|
@@ -423,6 +448,7 @@ export async function runTurn<S>(
|
|
|
423
448
|
signal: new AbortController().signal,
|
|
424
449
|
checkpoint: null,
|
|
425
450
|
heartbeat: async () => {},
|
|
451
|
+
progress: (phase, counts) => turn.progress.push(progressCall(key, phase, counts)),
|
|
426
452
|
});
|
|
427
453
|
const json = JSON.stringify(output ?? null);
|
|
428
454
|
if (json.length > 64 * 1024)
|
|
@@ -472,6 +498,11 @@ export async function runMessage<S>(
|
|
|
472
498
|
return { state: state as S, turns };
|
|
473
499
|
}
|
|
474
500
|
|
|
501
|
+
/** The phases the steps of `turns` reported, each once, in the order they first came. */
|
|
502
|
+
export function phases(turns: Turn[]): string[] {
|
|
503
|
+
return [...new Set(turns.flatMap((t) => t.progress.map((p) => p.phase)))];
|
|
504
|
+
}
|
|
505
|
+
|
|
475
506
|
/** A trigger message of the triggered workflow `workflow`. */
|
|
476
507
|
export function triggerMessage(
|
|
477
508
|
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.
|