@littlebigbrain/create 0.5.0 → 0.6.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 +42 -6
- package/lib/create.mjs +228 -14
- package/package.json +2 -1
- package/project/python/values_extract/tests/fake_lbb.py +226 -0
- package/project/python/values_extract/tests/test_workflow.py +150 -0
- package/python/values_extract/README.md +52 -0
- package/python/values_extract/__init__.py +3 -0
- package/python/values_extract/__main__.py +97 -0
- package/python/values_extract/samples/expected.json +149 -0
- package/python/values_extract/samples/records.json +26 -0
- package/python/values_extract/samples.py +37 -0
- package/python/values_extract/workflow.py +1044 -0
- package/python/versions.json +3 -0
- package/templates/_shared/testing.ts +197 -2
- package/templates/ontology-fit/README.md +41 -32
- package/templates/ontology-fit/workflow.test.ts +56 -5
- package/templates/ontology-fit/workflow.ts +708 -207
- package/templates/review/README.md +56 -29
- package/templates/review/decision.ts +118 -37
- package/templates/review/workflow.test.ts +30 -10
- package/templates/review/workflow.ts +123 -76
- package/templates/values-extract/README.md +92 -23
- package/templates/values-extract/samples/expected.json +1 -0
- package/templates/values-extract/test-examples.ts +155 -0
- package/templates/values-extract/workflow.test.ts +532 -27
- package/templates/values-extract/workflow.ts +719 -179
- package/templates/versions.json +1 -1
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
// `fact_status: accepted`; a person can leave facts out of the accept, except the type of a new
|
|
19
19
|
// instance. The proposal gets its `fact_status` (`written`, `in_review`, `refused`) and
|
|
20
20
|
// `review_note`, owned by this workflow.
|
|
21
|
-
import { createHash } from "node:crypto";
|
|
22
21
|
import {
|
|
23
22
|
entityRef,
|
|
23
|
+
LbbError,
|
|
24
24
|
triggeredWorkflow,
|
|
25
25
|
type LbbActivities,
|
|
26
26
|
type Schemas,
|
|
@@ -69,15 +69,30 @@ export function ontology(settings: Settings = defaults): Schemas["OntologyEvolve
|
|
|
69
69
|
];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** What the workflow decided, over all its turns. */
|
|
72
|
+
/** What the workflow decided, over all its turns: the counters of LBB's review. */
|
|
73
73
|
export interface ReviewState {
|
|
74
74
|
proposals: number;
|
|
75
|
+
/** Written, or held by the graph already (`held_already`). */
|
|
75
76
|
written: number;
|
|
76
|
-
|
|
77
|
+
held_already: number;
|
|
77
78
|
in_review: number;
|
|
78
79
|
refused: number;
|
|
80
|
+
/** Proposals for a person that name an instance the graph does not have. */
|
|
81
|
+
new_instance: number;
|
|
82
|
+
/** Review items filed. */
|
|
83
|
+
items: number;
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
const ZERO: ReviewState = {
|
|
87
|
+
proposals: 0,
|
|
88
|
+
written: 0,
|
|
89
|
+
held_already: 0,
|
|
90
|
+
in_review: 0,
|
|
91
|
+
refused: 0,
|
|
92
|
+
new_instance: 0,
|
|
93
|
+
items: 0,
|
|
94
|
+
};
|
|
95
|
+
|
|
81
96
|
/** Proposals one turn decides: two steps each, and at most one review item each, in a turn of 64 steps. */
|
|
82
97
|
const TURN_PROPOSALS = 20;
|
|
83
98
|
const PLAIN_IRI = /^[^\s<>"{}|\\^`]+$/;
|
|
@@ -106,6 +121,10 @@ interface Outcome {
|
|
|
106
121
|
note?: string;
|
|
107
122
|
facts?: review.Facts;
|
|
108
123
|
holds?: boolean;
|
|
124
|
+
/** The confidence the decision gated on (`gateValue`). */
|
|
125
|
+
gate?: number;
|
|
126
|
+
/** For a person, and it names an instance the graph does not have. */
|
|
127
|
+
newInstance?: boolean;
|
|
109
128
|
/** For a person: the item's instance, its view, and what the item shows of the fact. */
|
|
110
129
|
item?: {
|
|
111
130
|
instance: string;
|
|
@@ -121,27 +140,46 @@ export function createWorkflow(lbb: LbbActivities, settings: Settings = defaults
|
|
|
121
140
|
return triggeredWorkflow<ReviewState>({
|
|
122
141
|
name: workflowType.workflow_type,
|
|
123
142
|
version: workflowType.version,
|
|
124
|
-
initialState:
|
|
143
|
+
initialState: ZERO,
|
|
125
144
|
async onInstances(ctx, message, state) {
|
|
126
145
|
const batch = message.instances.slice(0, TURN_PROPOSALS);
|
|
127
146
|
let cache: Promise<Read> | undefined;
|
|
128
147
|
const read = () => (cache ??= reader(lbb, batch, settings));
|
|
129
|
-
const next = { ...state };
|
|
148
|
+
const next = { ...ZERO, ...state };
|
|
130
149
|
const forPeople: { iri: string; outcome: Outcome }[] = [];
|
|
131
150
|
for (const [i, iri] of batch.entries()) {
|
|
132
|
-
|
|
151
|
+
let outcome = await ctx.step(`decide-${i}`, async (s) => {
|
|
133
152
|
s.progress("deciding proposals", { done: i, total: batch.length, unit: "proposals" });
|
|
134
153
|
return decideOne(lbb, await read(), iri, settings);
|
|
135
154
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
try {
|
|
156
|
+
await lbb.writeOwned(ctx, `write-${i}`, async (s) => {
|
|
157
|
+
s.progress("writing facts", { done: i, total: batch.length, unit: "proposals" });
|
|
158
|
+
return statusWrite(await read(), iri, outcome, settings);
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// The graph refused the statement (a value it cannot hold): the proposal says why.
|
|
162
|
+
if (!(error instanceof LbbError && error.status === 400) || outcome.status !== "written")
|
|
163
|
+
throw error;
|
|
164
|
+
outcome = {
|
|
165
|
+
status: "refused",
|
|
166
|
+
note: `The graph refused the statement: ${error.message}`,
|
|
167
|
+
};
|
|
168
|
+
const refused = outcome;
|
|
169
|
+
await lbb.writeOwned(ctx, `refuse-${i}`, async () =>
|
|
170
|
+
statusWrite(await read(), iri, refused, settings),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
140
173
|
if (outcome.status === "decided") continue;
|
|
141
174
|
next.proposals += 1;
|
|
142
|
-
if (outcome.status === "written")
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
if (outcome.status === "written") {
|
|
176
|
+
next.written += 1;
|
|
177
|
+
if (outcome.holds) next.held_already += 1;
|
|
178
|
+
} else next[outcome.status] += 1;
|
|
179
|
+
if (outcome.status === "in_review") {
|
|
180
|
+
if (outcome.newInstance) next.new_instance += 1;
|
|
181
|
+
forPeople.push({ iri, outcome });
|
|
182
|
+
}
|
|
145
183
|
}
|
|
146
184
|
// One review item per instance of this turn.
|
|
147
185
|
const groups = new Map<string, { iri: string; outcome: Outcome }[]>();
|
|
@@ -149,12 +187,14 @@ export function createWorkflow(lbb: LbbActivities, settings: Settings = defaults
|
|
|
149
187
|
const instance = one.outcome.item!.instance;
|
|
150
188
|
groups.set(instance, [...(groups.get(instance) ?? []), one]);
|
|
151
189
|
}
|
|
152
|
-
for (const [n, members] of [...groups.values()].entries())
|
|
190
|
+
for (const [n, members] of [...groups.values()].entries()) {
|
|
153
191
|
await ctx.step(`review-${n}`, async (s) => {
|
|
154
192
|
s.progress("filing review items", { done: n, total: groups.size, unit: "instances" });
|
|
155
193
|
await fileItem(lbb, await read(), members, message, settings);
|
|
156
194
|
return true;
|
|
157
195
|
});
|
|
196
|
+
next.items += 1;
|
|
197
|
+
}
|
|
158
198
|
// A proposal its proposer withdrew (message.removed) keeps the fact written for it: see the README.
|
|
159
199
|
const rest = message.instances.slice(TURN_PROPOSALS);
|
|
160
200
|
return rest.length
|
|
@@ -295,10 +335,11 @@ async function decideOne(
|
|
|
295
335
|
);
|
|
296
336
|
edgeHolds = rows.length > 0;
|
|
297
337
|
}
|
|
298
|
-
const
|
|
338
|
+
const gate = review.gateValue(proposal.confidence, proposal.evidence);
|
|
339
|
+
const decision = review.decide(terms, s.predicate, gate, settings, edgeHolds);
|
|
299
340
|
if (decision.status === "refused" || terms.kind === "refused")
|
|
300
341
|
return { status: "refused", note: decision.status === "refused" ? decision.why : "" };
|
|
301
|
-
const facts = review.factsOf(terms, iri,
|
|
342
|
+
const facts = review.factsOf(terms, iri, gate, proposal.evidence);
|
|
302
343
|
if (decision.status === "written")
|
|
303
344
|
return decision.holds
|
|
304
345
|
? { status: "written", holds: true }
|
|
@@ -313,6 +354,8 @@ async function decideOne(
|
|
|
313
354
|
status: "in_review",
|
|
314
355
|
note: decision.why,
|
|
315
356
|
facts,
|
|
357
|
+
gate,
|
|
358
|
+
newInstance: review.namesNewInstance(terms),
|
|
316
359
|
item: {
|
|
317
360
|
instance,
|
|
318
361
|
...(known.class || terms.kind === "type"
|
|
@@ -329,10 +372,13 @@ const proposalRef = (proposal: Proposal, settings: Settings) =>
|
|
|
329
372
|
entityRef(proposal.iri, settings.className, proposal.label);
|
|
330
373
|
|
|
331
374
|
/**
|
|
332
|
-
* File the review item of one instance
|
|
333
|
-
* `fact_status: accepted
|
|
334
|
-
*
|
|
335
|
-
*
|
|
375
|
+
* File the review item of one instance, as LBB's review files it: one status row per proposal
|
|
376
|
+
* (`fact_status: accepted`) in the order a person reads them (the type, the label, values,
|
|
377
|
+
* links, the trace), the type of a new instance and the trace required, one evidence sample per
|
|
378
|
+
* fact, and LBB's key, title, rationale and summary. As the design of the workflows on the
|
|
379
|
+
* fleet asks (G8), the item also holds each statement after the status rows, so its accept
|
|
380
|
+
* commits the facts: each sample names the rows and triplets of its fact (`rows`, `triplets`),
|
|
381
|
+
* and a person can leave facts out of the accept.
|
|
336
382
|
*/
|
|
337
383
|
async function fileItem(
|
|
338
384
|
lbb: LbbActivities,
|
|
@@ -341,97 +387,98 @@ async function fileItem(
|
|
|
341
387
|
message: WorkflowTriggerMessage,
|
|
342
388
|
settings: Settings,
|
|
343
389
|
) {
|
|
344
|
-
const
|
|
345
|
-
const instance = first.instance;
|
|
390
|
+
const instance = members[0]!.outcome.item!.instance;
|
|
346
391
|
const kind = (m: { outcome: Outcome }) => m.outcome.item!.shown.kind;
|
|
392
|
+
const text = (m: { outcome: Outcome }) => m.outcome.item!.shown.text;
|
|
347
393
|
const ordered = [...members].sort(
|
|
348
394
|
(a, b) =>
|
|
349
395
|
review.KIND_ORDER.indexOf(kind(a)) - review.KIND_ORDER.indexOf(kind(b)) ||
|
|
350
|
-
a
|
|
396
|
+
(text(a) < text(b) ? -1 : text(a) > text(b) ? 1 : 0) ||
|
|
397
|
+
(a.iri < b.iri ? -1 : a.iri > b.iri ? 1 : 0),
|
|
351
398
|
);
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
399
|
+
const isNew = members.some((m) => m.outcome.item!.isNew);
|
|
400
|
+
const creates = isNew && ordered.some((m) => kind(m) === "type");
|
|
401
|
+
const className = ordered.map((m) => m.outcome.item!.class).find(Boolean);
|
|
402
|
+
const label = ordered.map((m) => m.outcome.item!.label).find(Boolean);
|
|
403
|
+
// The status rows first, one per fact, as LBB's item holds them; the statements after them.
|
|
404
|
+
const rows: Schemas["EntityPropertiesInput"][] = ordered.map(({ iri }) => ({
|
|
405
|
+
...proposalRef(read.proposals.get(iri)!, settings),
|
|
406
|
+
properties: { fact_status: "accepted" },
|
|
407
|
+
}));
|
|
357
408
|
const triplets: Schemas["TripletInput"][] = [];
|
|
358
|
-
const rows: Schemas["EntityPropertiesInput"][] = [];
|
|
359
|
-
// The rows an accept must keep (`required`, from client 0.24; an older client sends it as is).
|
|
360
409
|
const required: { row: number; reason: string }[] = [];
|
|
410
|
+
const statementRequired: { row: number; reason: string }[] = [];
|
|
361
411
|
const samples: Record<string, unknown>[] = [];
|
|
362
|
-
const whys =
|
|
363
|
-
|
|
412
|
+
const whys: string[] = [];
|
|
413
|
+
const proposers = new Set<string>();
|
|
414
|
+
const parts: string[] = [];
|
|
415
|
+
let links = 0;
|
|
416
|
+
for (const [row, { iri, outcome }] of ordered.entries()) {
|
|
364
417
|
const proposal = read.proposals.get(iri)!;
|
|
365
418
|
const shown = outcome.item!.shown;
|
|
366
419
|
const needed = shown.kind === "source" || (creates && shown.kind === "type");
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (outcome.note) whys.add(outcome.note);
|
|
420
|
+
const reason =
|
|
421
|
+
shown.kind === "type"
|
|
422
|
+
? "a new instance needs its type"
|
|
423
|
+
: "the source of an instance goes with it";
|
|
424
|
+
const factRows = (outcome.facts?.entity_properties ?? []).map((r) => rows.push(r) - 1);
|
|
425
|
+
const factTriplets = (outcome.facts?.triplets ?? []).map((t) => triplets.push(t) - 1);
|
|
426
|
+
if (needed) {
|
|
427
|
+
required.push({ row, reason });
|
|
428
|
+
for (const at of factRows) statementRequired.push({ row: at, reason });
|
|
429
|
+
}
|
|
430
|
+
if (outcome.note && !whys.includes(outcome.note)) whys.push(outcome.note);
|
|
431
|
+
if (proposal.proposedBy) proposers.add(proposal.proposedBy);
|
|
432
|
+
if (shown.kind === "link") links += 1;
|
|
433
|
+
else if (shown.kind !== "source") parts.push(shown.text);
|
|
382
434
|
samples.push({
|
|
383
435
|
proposal: iri,
|
|
384
436
|
row,
|
|
385
|
-
...(writeRows.length ? { rows: writeRows } : {}),
|
|
386
|
-
...(writeTriplets.length ? { triplets: writeTriplets } : {}),
|
|
387
437
|
kind: shown.kind,
|
|
388
438
|
fact: shown.text,
|
|
389
|
-
confidence: Math.round(proposal.confidence * 1000) / 1000,
|
|
439
|
+
confidence: Math.round((outcome.gate ?? proposal.confidence) * 1000) / 1000,
|
|
390
440
|
...(review.place(proposal.evidence) ?? {}),
|
|
391
441
|
...(needed ? { required: true } : {}),
|
|
442
|
+
...(shown.waitsFor !== undefined ? { waits_for: shown.waitsFor } : {}),
|
|
443
|
+
...(factRows.length ? { rows: factRows } : {}),
|
|
444
|
+
...(factTriplets.length ? { triplets: factTriplets } : {}),
|
|
392
445
|
});
|
|
393
446
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
ordered
|
|
397
|
-
.map((m) => m.iri)
|
|
398
|
-
.sort()
|
|
399
|
-
.join("\n"),
|
|
400
|
-
)
|
|
401
|
-
.digest("hex")
|
|
402
|
-
.slice(0, 12);
|
|
447
|
+
if (links) parts.push(links === 1 ? "1 link" : `${links} links`);
|
|
448
|
+
const tail = instance.slice(instance.lastIndexOf("/") + 1);
|
|
403
449
|
const title = creates
|
|
404
|
-
? `New ${className} ${label}`
|
|
405
|
-
: `${className} ${label}: ${
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
);
|
|
450
|
+
? `New ${className ?? "Instance"} ${label ?? tail}`
|
|
451
|
+
: `${className ?? "Instance"} ${label ?? tail}: ${ordered.length === 1 ? "1 fact" : `${ordered.length} facts`} to check`;
|
|
452
|
+
const by = [...proposers].sort();
|
|
453
|
+
const rationale = `${whys.slice(0, 3).join(" ")}${by.length ? ` Proposed by ${by.join(", ")}.` : ""}`;
|
|
409
454
|
await lbb.review({
|
|
410
455
|
workflow: message.workflow,
|
|
411
|
-
key:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
500,
|
|
456
|
+
key: review.itemKey(
|
|
457
|
+
message.workflow,
|
|
458
|
+
instance,
|
|
459
|
+
ordered.map((m) => m.iri),
|
|
416
460
|
),
|
|
461
|
+
title: review.clip(title, 200),
|
|
462
|
+
rationale: review.clip(rationale.trim(), 500),
|
|
463
|
+
origin: { kind: "agent", id: `starter:${message.workflow}`, label: "review" },
|
|
417
464
|
facts: {
|
|
418
465
|
triplets,
|
|
419
466
|
entity_properties: rows,
|
|
420
|
-
required,
|
|
421
|
-
summary: review.clip(
|
|
467
|
+
required: [...required, ...statementRequired],
|
|
468
|
+
summary: review.clip(parts.join(" · "), 500),
|
|
422
469
|
} as Schemas["SuggestedFacts"],
|
|
423
470
|
mapping: {
|
|
424
471
|
review: {
|
|
425
472
|
instance,
|
|
426
|
-
class: className,
|
|
427
|
-
label,
|
|
473
|
+
class: className ?? null,
|
|
474
|
+
label: label ?? null,
|
|
428
475
|
new: creates,
|
|
429
|
-
facts:
|
|
430
|
-
shown:
|
|
476
|
+
facts: ordered.length,
|
|
477
|
+
shown: ordered.length,
|
|
431
478
|
truncated: false,
|
|
432
479
|
},
|
|
433
480
|
},
|
|
434
|
-
evidence: { records:
|
|
481
|
+
evidence: { records: ordered.length, samples },
|
|
435
482
|
});
|
|
436
483
|
}
|
|
437
484
|
|
|
@@ -11,31 +11,73 @@ The LBB-run version is the starter workflow `values.extract@v1`
|
|
|
11
11
|
|
|
12
12
|
## How it runs
|
|
13
13
|
|
|
14
|
+
The template follows the rules of LBB's version
|
|
15
|
+
(`lbb-graph-query/src/infer_llm.rs`). Its eval runs both on the same
|
|
16
|
+
recorded model answers and finds the same calls and outputs
|
|
17
|
+
([`../_evals/README.md`](../_evals/README.md)).
|
|
18
|
+
|
|
14
19
|
The triggered workflow watches the class and its text field. For each
|
|
15
20
|
instance of a trigger message, two steps:
|
|
16
21
|
|
|
17
|
-
1. **Values.** The
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
the
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
1. **Values.** The text is the field's values as LBB composes it
|
|
23
|
+
(`text: <values>`, each value's whitespace as single spaces). The
|
|
24
|
+
reasoning model reads it and returns `{values: [{property, value, unit,
|
|
25
|
+
quote}]}`; the schema allows only the settings' property names, in name
|
|
26
|
+
order, and a number property names its unit. A number comes as the text
|
|
27
|
+
states it, with its unit, and the model never converts it. The prompt is
|
|
28
|
+
the LBB version's: the values of the instance's own item in its base
|
|
29
|
+
version, not those of a part, an accessory, an alternative or a variant
|
|
30
|
+
under a condition; an approximate value is stated. Then the checks
|
|
31
|
+
without a model, in LBB's order (`check`): the property is one the
|
|
32
|
+
workflow fills (else `unknown_property`), the value is the first of its
|
|
33
|
+
property (else `duplicate`), it takes its property's type (else
|
|
34
|
+
`invalid_value`), and its quote is in the text with at least three words
|
|
35
|
+
or the value in it (else `quote_not_found`). A number of a property with
|
|
36
|
+
a unit must come in that unit, with the unit in its quote
|
|
37
|
+
(`unitProblem`); otherwise it goes to review at most, with LBB's reason.
|
|
25
38
|
2. **Support.** The decision model scores how well each quote supports its
|
|
26
39
|
value: `supported`, `weak` or `not_supported`. The support is the
|
|
27
40
|
probability of `supported`.
|
|
28
41
|
|
|
29
|
-
Then the gate: a value at or above `gate` is written
|
|
30
|
-
gate
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
values
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(`
|
|
42
|
+
Then the gate: a value at or above `gate` is `written`, one from 0.5 to the
|
|
43
|
+
gate goes to `review`, one below 0.5 or without an answer is
|
|
44
|
+
`weak_support` and is dropped. A value in review becomes a `value` review
|
|
45
|
+
item (`lbb.review({ kind: "value" })`) with the property's unit and the
|
|
46
|
+
quote, one per property of an instance, under LBB's key
|
|
47
|
+
`starter:<workflow>:value:<hash of instance and property>` (`valueItemKey`,
|
|
48
|
+
with `stableHashHex`). One owned write per turn replaces the values of the
|
|
49
|
+
turn's instances: a property the workflow does not write again loses its
|
|
50
|
+
value and its source. Each value's evidence is its quote, with the
|
|
51
|
+
instance's page box as its region when the instance has one (`page`, `x0`,
|
|
52
|
+
`y0`, `x1`, `y1`, as a `DocEntry` has).
|
|
53
|
+
|
|
54
|
+
Before it writes, the workflow reads the items (`lbb.reviewItems(keys)`): a
|
|
55
|
+
person's answer wins over the model, for the evidence it was given on
|
|
56
|
+
(`evidence_hash`: a hash of the value's quote and of the unit the text
|
|
57
|
+
states, `evidenceOf`). With the same evidence, the value in force, or the
|
|
58
|
+
value the person decided on, a value a person accepted, or corrected with
|
|
59
|
+
a number in the property's unit, is written as answered. New evidence that
|
|
60
|
+
reads another value asks the person again, and the answered value and its
|
|
61
|
+
source link stay (the instance is written on its own, without that
|
|
62
|
+
property) until a new answer is written. A value a person rejected is
|
|
63
|
+
not written again, nor is one the graph refused (`effects.commit_error`; the
|
|
64
|
+
item is open again for the person). An earlier version filed a value in
|
|
65
|
+
review as a suggestion, one per value (`legacyValueKey`): an accepted one is
|
|
66
|
+
written again, an open one stays the question, and a dismissed one is a
|
|
67
|
+
reject of its value. An instance that leaves the class keeps its values:
|
|
68
|
+
they go with its fields when it is retracted.
|
|
69
|
+
|
|
70
|
+
The write's request id is its step's effect id, which no other turn
|
|
71
|
+
repeats, so a value that changes back (A, then B, then A) is written again.
|
|
72
|
+
When a model call is refused, the instance goes back to the watch
|
|
73
|
+
(`lbb.requeue`, after 1, 4 and 16 minutes, at most 3 times) and keeps its
|
|
74
|
+
values; the turn completes and the shard goes on. A spent budget fails the
|
|
75
|
+
turn with the time to retry, and the worker waits without an attempt.
|
|
76
|
+
|
|
77
|
+
The state counts what LBB's version counts: `instances`,
|
|
78
|
+
`values_proposed`, one counter per outcome, `failed`, `commits` and
|
|
79
|
+
`removed`. A turn reads at most 24 instances; the rest of a message goes to
|
|
80
|
+
a new turn (`ctx.continue`).
|
|
39
81
|
|
|
40
82
|
## Settings
|
|
41
83
|
|
|
@@ -48,6 +90,7 @@ A turn reads at most 24 instances; the rest of a message goes to a new turn
|
|
|
48
90
|
| `properties` | `width_cm`, `depth_cm` (`f64`), `material` (`text`), `water_supply` (`keyword`) | The properties to fill, with their types (`text`, `keyword`, `i64`, `f64`, `bool`, `date_time`) |
|
|
49
91
|
| `context` | the samples' | One sentence about what the text is |
|
|
50
92
|
| `gate` | 0.8 | The support a value needs to be written |
|
|
93
|
+
| `units` | `width_cm`, `depth_cm`: `cm` | The unit of each number property; `ontology()` declares it on the property |
|
|
51
94
|
|
|
52
95
|
`trigger(settings)` names the triggered workflow `values-extract` and
|
|
53
96
|
owns the class `ValueSource`, the relation `HAS_VALUE_SOURCE` and the
|
|
@@ -57,14 +100,22 @@ relation.
|
|
|
57
100
|
## What the LBB-run version adds
|
|
58
101
|
|
|
59
102
|
- `params.where`: a SPARQL pattern that limits the instances it reads.
|
|
60
|
-
- Texts built from several watched fields,
|
|
61
|
-
|
|
62
|
-
- The instance's page box as the region of each value's evidence, for an
|
|
63
|
-
instance that has one (a `DocEntry`).
|
|
103
|
+
- Texts built from several watched fields, and the fields chosen for you
|
|
104
|
+
when the watch names none.
|
|
64
105
|
- A check at creation that each property is in the property registry with
|
|
65
106
|
a type it can write.
|
|
66
107
|
- Its own monthly budget, a preview with the values of up to 3 instances,
|
|
67
|
-
and counters
|
|
108
|
+
and its counters in the workflow's status.
|
|
109
|
+
|
|
110
|
+
It differs in three places. It commits once per instance, the template once
|
|
111
|
+
per turn, so its `commits` counter is larger. When the reasoning model gives
|
|
112
|
+
no usable answer, it counts the instance `failed` and clears its values; the
|
|
113
|
+
template sends the instance back to the watch and keeps them. And it
|
|
114
|
+
converts a number the text states in another unit of the same kind (`600 mm`
|
|
115
|
+
for a property in `cm`) to the unit the property declares, and refuses a
|
|
116
|
+
unit it does not know or of another kind; the template sends such a number
|
|
117
|
+
to review. So the question of such a number differs: LBB's item asks the
|
|
118
|
+
converted value, the template's the value as stated.
|
|
68
119
|
|
|
69
120
|
## Samples and test
|
|
70
121
|
|
|
@@ -79,3 +130,21 @@ or a conditional variant (`Bei Einsatz`) does not support its value. The run
|
|
|
79
130
|
writes the values of the answer key and none it leaves out. The test also
|
|
80
131
|
checks the review path, the checks of type and quote, an instance that
|
|
81
132
|
leaves the class, and a message larger than a turn.
|
|
133
|
+
|
|
134
|
+
## Test on examples
|
|
135
|
+
|
|
136
|
+
`npm run test:examples` runs the workflow on instances of your graph with
|
|
137
|
+
the real models, and writes nothing (`test-examples.ts`). The activities go
|
|
138
|
+
through `captureActivities`: the server checks each owned write with the
|
|
139
|
+
commit's dry run, and keeps it. Each review item is kept too. The script
|
|
140
|
+
prints, for each input, the values the workflow would write and the values
|
|
141
|
+
it would give a person to review.
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
LBB_URL=... LBB_API_KEY=... LBB_GRAPH=tender npm run test:examples # the first 6 instances
|
|
145
|
+
LBB_URL=... LBB_API_KEY=... LBB_GRAPH=tender npm run test:examples -- <iri> # chosen instances
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The model calls count in your environment's budget. For the LBB-run
|
|
149
|
+
version, test the workflow in the console (the workflow's Test tab) or with
|
|
150
|
+
`client.workflows.tests`.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Test the workflow on examples before it writes. `npm run test:examples`
|
|
2
|
+
// runs it with the real models on chosen instances of your graph, through
|
|
3
|
+
// `captureActivities`, which writes nothing: each owned write is checked
|
|
4
|
+
// with the commit's dry run and kept, and each review item is kept. Then it
|
|
5
|
+
// prints, per input, the values the workflow would write and the values it
|
|
6
|
+
// would give a person to review.
|
|
7
|
+
//
|
|
8
|
+
// LBB_URL=... LBB_API_KEY=... LBB_GRAPH=... npm run test:examples -- [<iri> …]
|
|
9
|
+
//
|
|
10
|
+
// Without IRIs it tests the first 6 instances of the class. The model calls
|
|
11
|
+
// count in your environment's budget.
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
LbbClient,
|
|
15
|
+
captureActivities,
|
|
16
|
+
entityIdFromIri,
|
|
17
|
+
lbbActivities,
|
|
18
|
+
type ActivityCaptureResult,
|
|
19
|
+
type LbbActivities,
|
|
20
|
+
type WorkflowTriggerMessage,
|
|
21
|
+
} from "@littlebigbrain/client";
|
|
22
|
+
import { runMessage } from "../_shared/testing.js";
|
|
23
|
+
import { createWorkflow, defaults, type Settings } from "./workflow.js";
|
|
24
|
+
|
|
25
|
+
/** A value the workflow would write. */
|
|
26
|
+
export interface ExampleValue {
|
|
27
|
+
property: string;
|
|
28
|
+
value: string;
|
|
29
|
+
quote?: string;
|
|
30
|
+
support?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the workflow would do with one input. */
|
|
34
|
+
export interface ExampleResult {
|
|
35
|
+
iri: string;
|
|
36
|
+
/** Values at or above the gate, which it writes. */
|
|
37
|
+
written: ExampleValue[];
|
|
38
|
+
/** Values it gives a person to review: each item's title. */
|
|
39
|
+
review: string[];
|
|
40
|
+
/** Why the server's dry run (or the local check) refused a write. */
|
|
41
|
+
errors: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The first `limit` instances of the class, and the commit they were read at. */
|
|
45
|
+
export async function firstInstances(
|
|
46
|
+
client: LbbClient,
|
|
47
|
+
settings: Settings = defaults,
|
|
48
|
+
limit = 6,
|
|
49
|
+
): Promise<{ iris: string[]; seq: number }> {
|
|
50
|
+
const read = await client.sparqlRows({
|
|
51
|
+
query: `SELECT ?s WHERE { ?s a <https://littlebigbrain.com/class/${encodeURIComponent(settings.className.toLowerCase())}> } LIMIT ${limit}`,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
iris: read.rows.map((row) => row.s!).filter(Boolean),
|
|
55
|
+
seq: read.snapshot?.served_at_seq ?? 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Per input, what the captured writes and review items say. */
|
|
60
|
+
export function resultsOf(iris: string[], captured: ActivityCaptureResult): ExampleResult[] {
|
|
61
|
+
return iris.map((iri) => {
|
|
62
|
+
const id = entityIdFromIri(iri);
|
|
63
|
+
const mine = (key: string | null | undefined) => key?.startsWith(`value:${id}:`) ?? false;
|
|
64
|
+
const written: ExampleValue[] = [];
|
|
65
|
+
const errors: string[] = [];
|
|
66
|
+
for (const write of captured.writes) {
|
|
67
|
+
const about = (write.body.triplets ?? []).filter(
|
|
68
|
+
(edge) => edge.relation === "HAS_VALUE_SOURCE" && mine(edge.target.key),
|
|
69
|
+
);
|
|
70
|
+
if (about.length) errors.push(...write.errors);
|
|
71
|
+
for (const edge of about) {
|
|
72
|
+
const source = (write.body.entity_properties ?? []).find(
|
|
73
|
+
(entity) => entity.type === "ValueSource" && entity.key === edge.target.key,
|
|
74
|
+
);
|
|
75
|
+
const properties = (source?.properties ?? {}) as Record<string, unknown>;
|
|
76
|
+
const quote = typeof edge.evidence === "object" ? edge.evidence?.text : undefined;
|
|
77
|
+
written.push({
|
|
78
|
+
property: String(properties.property ?? edge.target.key?.split(":")[2] ?? ""),
|
|
79
|
+
value: String(properties.value ?? ""),
|
|
80
|
+
...(quote ? { quote } : {}),
|
|
81
|
+
...(edge.confidence != null ? { support: edge.confidence } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const review = captured.reviews
|
|
86
|
+
.filter((item) => item.request.key?.includes(`:value:${id}:`))
|
|
87
|
+
.map((item) => item.request.title);
|
|
88
|
+
return { iri, written, review, errors };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Run the workflow on `iris` with `activities` captured
|
|
94
|
+
* (`captureActivities`): the models run, and nothing is written. `seq` is
|
|
95
|
+
* the commit the workflow reads the texts at.
|
|
96
|
+
*/
|
|
97
|
+
export async function testExamples(
|
|
98
|
+
activities: LbbActivities,
|
|
99
|
+
iris: string[],
|
|
100
|
+
options: { settings?: Settings; seq?: number; graph?: string } = {},
|
|
101
|
+
): Promise<ExampleResult[]> {
|
|
102
|
+
const settings = options.settings ?? defaults;
|
|
103
|
+
const capture = captureActivities(activities, { workflow: "values-extract" });
|
|
104
|
+
const message: WorkflowTriggerMessage = {
|
|
105
|
+
type: "instances",
|
|
106
|
+
workflow: "values-extract-test",
|
|
107
|
+
graph: options.graph ?? "main",
|
|
108
|
+
as_of_commit_seq: options.seq ?? 0,
|
|
109
|
+
reason: "refresh",
|
|
110
|
+
instances: iris,
|
|
111
|
+
removed: [],
|
|
112
|
+
};
|
|
113
|
+
await runMessage(createWorkflow(capture.activities, settings), message);
|
|
114
|
+
return resultsOf(iris, capture.result());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The results as lines of text. */
|
|
118
|
+
export function report(results: ExampleResult[]): string[] {
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
for (const result of results) {
|
|
121
|
+
lines.push(result.iri);
|
|
122
|
+
for (const value of result.written)
|
|
123
|
+
lines.push(
|
|
124
|
+
` write ${value.property} = ${value.value}${value.support != null ? ` (support ${value.support.toFixed(2)})` : ""}${value.quote ? ` "${value.quote}"` : ""}`,
|
|
125
|
+
);
|
|
126
|
+
for (const title of result.review) lines.push(` review ${title}`);
|
|
127
|
+
if (!result.written.length && !result.review.length) lines.push(" no value");
|
|
128
|
+
for (const error of result.errors) lines.push(` refused ${error}`);
|
|
129
|
+
}
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function main() {
|
|
134
|
+
const graph = process.env.LBB_GRAPH;
|
|
135
|
+
if (!graph) throw new Error("set LBB_GRAPH to the graph to test on");
|
|
136
|
+
const client = new LbbClient({
|
|
137
|
+
baseUrl: process.env.LBB_URL ?? "http://127.0.0.1:7400",
|
|
138
|
+
apiKey: process.env.LBB_API_KEY,
|
|
139
|
+
graph,
|
|
140
|
+
});
|
|
141
|
+
const named = process.argv.slice(2).filter((arg) => !arg.startsWith("-"));
|
|
142
|
+
const first = await firstInstances(client);
|
|
143
|
+
const iris = named.length ? named : first.iris;
|
|
144
|
+
if (!iris.length) throw new Error(`the graph ${graph} has no ${defaults.className} to test`);
|
|
145
|
+
const results = await testExamples(lbbActivities(client), iris, { seq: first.seq, graph });
|
|
146
|
+
for (const line of report(results)) console.log(line);
|
|
147
|
+
console.log("Nothing was written.");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
151
|
+
main().catch((error: unknown) => {
|
|
152
|
+
console.error(error instanceof Error ? error.message : error);
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
});
|
|
155
|
+
}
|