@factbag/jev 0.0.0-alpha.5 → 0.0.0-alpha.6
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/CHANGELOG.md +21 -0
- package/README.md +51 -8
- package/dist/public/index.d.ts +25 -2
- package/dist/public/index.js +154 -48
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @factbag/jev
|
|
2
2
|
|
|
3
|
+
## 0.0.0-alpha.6
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6433c6b: Classify open-ended `String`, `Number`, and `Date` fields, and entity fields with no registered entities, over the values each passage offers. Pass `createChunkSymbolExtractor` to `classifier(...)` from `@factbag/jev` or `@factbag/clef` to build an extractor per chunk whose `query` answers candidate values for the open fields of the predicates it classifies. Both packages export the `ChunkSymbolExtractor`, `CreateChunkSymbolExtractor`, and `SymbolAnswer` types.
|
|
8
|
+
|
|
9
|
+
Extracted values are candidates only. An extracted entity is registered in `run.kb.entities` when asserted or denied evidence mentions it; entities the model does not admit evidence for are discarded.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [7225a51]
|
|
14
|
+
- Updated dependencies [7f26b45]
|
|
15
|
+
- Updated dependencies [b7c85ff]
|
|
16
|
+
- Updated dependencies [ed6ec84]
|
|
17
|
+
- Updated dependencies [5114b2f]
|
|
18
|
+
- Updated dependencies [15391f2]
|
|
19
|
+
- Updated dependencies [eab3679]
|
|
20
|
+
- Updated dependencies [ecbd8d9]
|
|
21
|
+
- Updated dependencies [e4b343e]
|
|
22
|
+
- factbag@0.0.0-alpha.6
|
|
23
|
+
|
|
3
24
|
## 0.0.0-alpha.5
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -102,14 +102,14 @@ field that cannot be classified. A nonempty metadata description is required.
|
|
|
102
102
|
|
|
103
103
|
Supported field domains:
|
|
104
104
|
|
|
105
|
-
| Schema field | Candidate values
|
|
106
|
-
| -------------------------- |
|
|
107
|
-
| `Boolean` | `false`, `true`
|
|
108
|
-
| `enumeration(...)` | Explicit string, number, or Date members
|
|
109
|
-
| Entity constructor |
|
|
110
|
-
| `String`, `Number`, `Date` |
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
| Schema field | Candidate values |
|
|
106
|
+
| -------------------------- | ---------------------------------------------------- |
|
|
107
|
+
| `Boolean` | `false`, `true` |
|
|
108
|
+
| `enumeration(...)` | Explicit string, number, or Date members |
|
|
109
|
+
| Entity constructor | Registered instances, plus each chunk's extracted |
|
|
110
|
+
| `String`, `Number`, `Date` | Each chunk's extracted values; unsupported otherwise |
|
|
111
|
+
|
|
112
|
+
Without `createChunkSymbolExtractor`, an entity domain must contain registered instances. Existing scalar facts do not
|
|
113
113
|
turn an open-ended schema into an enum. To model a fixed value, declare it explicitly
|
|
114
114
|
with `enumeration(value)` rather than asking the classifier to discover it.
|
|
115
115
|
|
|
@@ -132,6 +132,49 @@ proposition. Every candidate gets an independent `Choice(ASSERT, DENY, IRRELEVAN
|
|
|
132
132
|
question. Assert and deny permissions for the same predicate share one question.
|
|
133
133
|
No enum exclusivity or Boolean equivalence is assumed; those belong to the ruleset.
|
|
134
134
|
|
|
135
|
+
### Passage symbols
|
|
136
|
+
|
|
137
|
+
Pass `createChunkSymbolExtractor` to fill open-ended and entity fields from the text
|
|
138
|
+
being classified. The classifier builds one extractor per job over that job's chunk,
|
|
139
|
+
and asks it for the open fields of the predicates it classifies:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const run = classifier({
|
|
143
|
+
corpus,
|
|
144
|
+
input: access,
|
|
145
|
+
createChunkSymbolExtractor: (chunks) => {
|
|
146
|
+
const found = scan(chunks);
|
|
147
|
+
return {
|
|
148
|
+
query: (requests) =>
|
|
149
|
+
new Map(
|
|
150
|
+
[...requests].map(([predicate, fields]) => [
|
|
151
|
+
predicate,
|
|
152
|
+
Object.fromEntries(fields.map((name) => [name, found.for(predicate, name)])),
|
|
153
|
+
]),
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- Open fields are `String`, `Number`, `Date`, and entity fields. Boolean and
|
|
161
|
+
enumeration fields keep their declared domains.
|
|
162
|
+
- `query` receives a map from each predicate to its open field names, and answers with
|
|
163
|
+
a map from each predicate to the values for each field, possibly asynchronously. How
|
|
164
|
+
it finds them, and what it caches, is up to the extractor.
|
|
165
|
+
- The answer must cover exactly the requested predicates and fields, with values each
|
|
166
|
+
field can hold: strings, finite numbers, valid dates, or entities of the field's
|
|
167
|
+
type. Otherwise the job rejects before anything is classified.
|
|
168
|
+
- Scalar fields take exactly the values answered. Entity fields take them alongside
|
|
169
|
+
their registered instances. Extracted entities are only candidates: those that
|
|
170
|
+
asserted or denied evidence mentions are registered in `run.kb.entities`, and the
|
|
171
|
+
rest are discarded.
|
|
172
|
+
- A predicate with a field left empty is skipped for that chunk.
|
|
173
|
+
|
|
174
|
+
An entity's type and `id` are its identity, so the extractor decides which mentions
|
|
175
|
+
are the same entity by the ids it mints. `new Person("Alice")` from two chunks is one
|
|
176
|
+
entity.
|
|
177
|
+
|
|
135
178
|
## Jobs and execution
|
|
136
179
|
|
|
137
180
|
`run` is a single-cursor `IterableIterator`. Each yielded job classifies one chunk
|
package/dist/public/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GuardedKB, KnowledgeBase } from "factbag/kb";
|
|
2
|
-
import { Corpus, Metadata } from "factbag/corpus";
|
|
2
|
+
import { Chunk, Corpus, Metadata } from "factbag/corpus";
|
|
3
|
+
import { Entity, PredicateDeclaration, Scalar } from "factbag/dsl";
|
|
3
4
|
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
4
5
|
/** Default minimum probability for admitting signed evidence. @beta */
|
|
5
6
|
export declare const DEFAULT_MINIMUM_PROBABILITY = .4;
|
|
@@ -32,6 +33,25 @@ type Settings = {
|
|
|
32
33
|
};
|
|
33
34
|
/** Settings with every default applied. @beta */
|
|
34
35
|
type Normalized = Readonly<Required<Settings>>;
|
|
36
|
+
/**
|
|
37
|
+
* Candidate values for each requested field, by predicate and then by field name. Open
|
|
38
|
+
* fields are `String`, `Number`, `Date`, and entity fields. The same value may be offered
|
|
39
|
+
* to any number of fields.
|
|
40
|
+
* @beta
|
|
41
|
+
*/
|
|
42
|
+
type SymbolAnswer = ReadonlyMap<PredicateDeclaration, Readonly<Record<string, readonly (Scalar | Entity)[]>>>;
|
|
43
|
+
/**
|
|
44
|
+
* Candidate symbols found in a fixed set of chunks. How and when it reads them, and what
|
|
45
|
+
* it caches between queries, is up to the implementation. An entity's type and id are its
|
|
46
|
+
* identity, so the ids it mints decide which mentions are the same entity.
|
|
47
|
+
* @beta
|
|
48
|
+
*/
|
|
49
|
+
type ChunkSymbolExtractor = {
|
|
50
|
+
/** Candidate values for exactly the requested fields of each requested predicate. */
|
|
51
|
+
readonly query: (requests: ReadonlyMap<PredicateDeclaration, readonly string[]>) => SymbolAnswer | Promise<SymbolAnswer>;
|
|
52
|
+
};
|
|
53
|
+
/** Build an extractor over the chunks it will be queried about. @beta */
|
|
54
|
+
type CreateChunkSymbolExtractor = (chunks: readonly Chunk[]) => ChunkSymbolExtractor;
|
|
35
55
|
/** One opaque unit of work. Its effects are written to the run’s knowledge base. @beta */
|
|
36
56
|
type Job = {
|
|
37
57
|
readonly execute: () => Promise<void>;
|
|
@@ -57,12 +77,15 @@ export declare function createConfig(config?: Config): Normalized & Config;
|
|
|
57
77
|
* Validate available predicates eagerly and return a lazy iterator of executable jobs.
|
|
58
78
|
* Bare KBs expose all known predicates; explicit guards retain their permission policy.
|
|
59
79
|
* Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
|
|
80
|
+
* With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
|
|
81
|
+
* each chunk offers, so they need no finite domain up front.
|
|
60
82
|
* @beta
|
|
61
83
|
*/
|
|
62
84
|
export declare function classifier<M extends Metadata = Metadata>(input: {
|
|
63
85
|
corpus: Corpus<M>;
|
|
64
86
|
input: KnowledgeBase | GuardedKB;
|
|
65
87
|
config?: Config;
|
|
88
|
+
createChunkSymbolExtractor?: CreateChunkSymbolExtractor;
|
|
66
89
|
}): Run;
|
|
67
90
|
/**
|
|
68
91
|
* Settle contradictions in a knowledge base by reading each one's evidence together.
|
|
@@ -76,4 +99,4 @@ export declare function classifier<M extends Metadata = Metadata>(input: {
|
|
|
76
99
|
* @beta
|
|
77
100
|
*/
|
|
78
101
|
export declare function resolver<M extends Metadata = Metadata>(corpus: Corpus<M>, kb: KnowledgeBase, config?: Config): Run;
|
|
79
|
-
export type { Job, Normalized, Run, Settings };
|
|
102
|
+
export type { ChunkSymbolExtractor, CreateChunkSymbolExtractor, Job, Normalized, Run, Settings, SymbolAnswer };
|
package/dist/public/index.js
CHANGED
|
@@ -11,7 +11,9 @@ import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
|
11
11
|
* Query-only predicates are excluded.
|
|
12
12
|
*
|
|
13
13
|
* Called once when constructing a run, so unsupported predicates fail before reading
|
|
14
|
-
* the corpus or calling Jev.
|
|
14
|
+
* the corpus or calling Jev. Each field starts with the values known before any passage
|
|
15
|
+
* is read: open-ended fields, and entity fields with no registered entities, start empty.
|
|
16
|
+
* Candidate combinations are built later, after filtering.
|
|
15
17
|
*/
|
|
16
18
|
function classifiablePredicates(access) {
|
|
17
19
|
const actions = /* @__PURE__ */ new Map();
|
|
@@ -30,10 +32,12 @@ function classifiablePredicates(access) {
|
|
|
30
32
|
};
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
|
-
/** Validate a
|
|
35
|
+
/** Validate a field's type and collect the values known before reading any passage. */
|
|
34
36
|
function fieldValues({ predicate, name, kb }) {
|
|
35
37
|
const type = predicate.params[name];
|
|
36
|
-
if (type ===
|
|
38
|
+
if (type === void 0) throw new UnsupportedPredicateError(`Invalid schema for ${predicate.name}: field "${name}" has no type`, predicate, name);
|
|
39
|
+
/** Open-ended fields take values only from passages. */
|
|
40
|
+
if (type === String || type === Number || type === Date) return [];
|
|
37
41
|
if (type === Boolean) return [false, true];
|
|
38
42
|
if (typeof type === "object") {
|
|
39
43
|
const kind = type.values[0] instanceof Date ? "date" : typeof type.values[0];
|
|
@@ -41,14 +45,57 @@ function fieldValues({ predicate, name, kb }) {
|
|
|
41
45
|
if (type.values.length === 0 || kind !== "string" && kind !== "number" && kind !== "date" || type.values.some((value) => (value instanceof Date ? "date" : typeof value) !== kind) || keys.some((key) => typeof key === "number" && Number.isNaN(key)) || new Set(keys).size !== type.values.length) throw new UnsupportedPredicateError(`Invalid enum domain for ${predicate.name}.${name}`, predicate, name);
|
|
42
46
|
return [...type.values];
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
if (ids.length === 0) throw new UnsupportedPredicateError(`No registered entities for ${predicate.name}.${name}`, predicate, name);
|
|
46
|
-
return ids.map((id) => {
|
|
48
|
+
return kb.entities.ids(type).map((id) => {
|
|
47
49
|
const entity = kb.entities.get(type, id);
|
|
48
50
|
if (entity === void 0) throw new Error("A registered entity domain was modified");
|
|
49
51
|
return entity;
|
|
50
52
|
});
|
|
51
53
|
}
|
|
54
|
+
// ../../packages/system-one/src/utils.ts
|
|
55
|
+
/** Snapshot JSON data, including nested dates and entities identified by a string id. */
|
|
56
|
+
function encode(value) {
|
|
57
|
+
const entities = /* @__PURE__ */ new WeakMap();
|
|
58
|
+
const json = JSON.stringify(value, (_, item) => {
|
|
59
|
+
if (!isRecord(item) || typeof item["id"] !== "string") return item;
|
|
60
|
+
let entity = entities.get(item);
|
|
61
|
+
if (entity === void 0) {
|
|
62
|
+
entity = {
|
|
63
|
+
...item,
|
|
64
|
+
type: typeof item.constructor === "function" ? item.constructor.name : "Object"
|
|
65
|
+
};
|
|
66
|
+
entities.set(item, entity);
|
|
67
|
+
}
|
|
68
|
+
return entity;
|
|
69
|
+
});
|
|
70
|
+
if (typeof json !== "string") throw new TypeError("Cannot encode a value without a JSON representation");
|
|
71
|
+
return JSON.parse(json);
|
|
72
|
+
}
|
|
73
|
+
/** A compact diagnostic label, using entity ids and the predicate's declared field order. */
|
|
74
|
+
function formatAtom({ predicate, fields }) {
|
|
75
|
+
return `${predicate.name}(${predicate.paramNames.map((name) => format(fields[name])).join(", ")})`;
|
|
76
|
+
}
|
|
77
|
+
function format(value) {
|
|
78
|
+
if (value instanceof Date) return value.toISOString();
|
|
79
|
+
return isRecord(value) && "id" in value ? String(value["id"]) : String(value);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Fail fast when an invariant breaks: a bug in Factbag, never bad input from the caller,
|
|
83
|
+
* which gets a descriptive error of its own.
|
|
84
|
+
*/
|
|
85
|
+
function assert(condition, message) {
|
|
86
|
+
if (!condition) throw new Error(`Assertion failed: ${message}`);
|
|
87
|
+
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
90
|
+
}
|
|
91
|
+
/** An entity value: an object with a string id, other than a Date. */
|
|
92
|
+
function isEntity(value) {
|
|
93
|
+
return typeof value === "object" && value !== null && !(value instanceof Date) && "id" in value && typeof value.id === "string";
|
|
94
|
+
}
|
|
95
|
+
/** A finite probability, including both endpoints. */
|
|
96
|
+
function isProbability(value) {
|
|
97
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
98
|
+
}
|
|
52
99
|
// ../../packages/system-one/src/classifier/extract-evidence.ts
|
|
53
100
|
/**
|
|
54
101
|
* Assess the retained propositions against one passage and add admitted evidence to
|
|
@@ -68,6 +115,12 @@ async function extractEvidence({ chunk, predicates, symbols, config, kb }) {
|
|
|
68
115
|
})))).flat();
|
|
69
116
|
for (const { sign, atom, metadata } of evidence) if (sign === "+") kb.asserts(atom, metadata);
|
|
70
117
|
else kb.denies(atom, metadata);
|
|
118
|
+
/**
|
|
119
|
+
* Register the entities that written evidence mentions, so later readers of the KB can
|
|
120
|
+
* enumerate them. Extracted candidates that no evidence mentions stay unregistered.
|
|
121
|
+
* Identity is type and id, so an entity from several chunks is registered once.
|
|
122
|
+
*/
|
|
123
|
+
kb.entities.add(...evidence.flatMap(({ atom }) => Object.values(atom.fields)).filter((value) => isEntity(value)));
|
|
71
124
|
}
|
|
72
125
|
/**
|
|
73
126
|
* Enumerate one predicate's candidate facts, have the model judge each against the passage,
|
|
@@ -124,6 +177,59 @@ function enumerateCandidateFacts({ predicate, symbols }) {
|
|
|
124
177
|
fields: values
|
|
125
178
|
}));
|
|
126
179
|
}
|
|
180
|
+
// ../../packages/system-one/src/classifier/extract-symbols.ts
|
|
181
|
+
/**
|
|
182
|
+
* Widen each retained predicate's open fields with the values this chunk offers. This is
|
|
183
|
+
* the second pipeline step, after predicate filtering, so only fields of predicates the
|
|
184
|
+
* passage may bear on are queried. Boolean and enumeration fields keep their declared
|
|
185
|
+
* domains; entity fields keep their registered entities. Extracted entities are only
|
|
186
|
+
* candidates: nothing is registered here. Fields may still be empty afterwards.
|
|
187
|
+
*/
|
|
188
|
+
async function extractSymbols({ chunk, predicates, createExtractor }) {
|
|
189
|
+
const requests = new Map(predicates.map(({ declaration }) => [declaration, openFields(declaration)]).filter(([, fields]) => fields.length > 0));
|
|
190
|
+
/** Nothing to ask builds no extractor. */
|
|
191
|
+
if (createExtractor === void 0 || requests.size === 0) return predicates;
|
|
192
|
+
const answer = await createExtractor([chunk]).query(requests);
|
|
193
|
+
assertAnswers(requests, answer);
|
|
194
|
+
return predicates.map((predicate) => ({
|
|
195
|
+
...predicate,
|
|
196
|
+
roles: Object.fromEntries(Object.entries(predicate.roles).map(([name, domain]) => [name, unique([...domain, ...answer.get(predicate.declaration)?.[name] ?? []])]))
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
/** The fields whose values come from the passage: every field but Boolean and enumeration ones. */
|
|
200
|
+
function openFields(predicate) {
|
|
201
|
+
return predicate.paramNames.filter((name) => {
|
|
202
|
+
const type = predicate.params[name];
|
|
203
|
+
return typeof type === "function" && type !== Boolean;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The answer must cover exactly the requested predicates and fields, with values each
|
|
208
|
+
* field can hold, so an extractor bug fails the job before anything is asked.
|
|
209
|
+
*/
|
|
210
|
+
function assertAnswers(requests, answer) {
|
|
211
|
+
for (const predicate of answer.keys()) if (!requests.has(predicate)) throw new TypeError(`Extracted symbols answer unrequested predicate "${predicate.name}"`);
|
|
212
|
+
for (const [predicate, fields] of requests) {
|
|
213
|
+
const values = answer.get(predicate);
|
|
214
|
+
if (values === void 0) throw new TypeError(`Extracted symbols omit requested predicate "${predicate.name}"`);
|
|
215
|
+
if (Object.keys(values).length !== fields.length || !fields.every((name) => Object.hasOwn(values, name))) throw new TypeError(`Extracted symbols for "${predicate.name}" must answer exactly its fields: ${fields.join(", ")}`);
|
|
216
|
+
for (const name of fields) {
|
|
217
|
+
const type = predicate.params[name];
|
|
218
|
+
if (!(values[name] ?? []).every((value) => holds(type, value))) throw new TypeError(`Extracted symbols for ${predicate.name}.${name} must be valid values of its type`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Whether a field of this type can hold the value: a finite number, valid date, or own entity. */
|
|
223
|
+
function holds(type, value) {
|
|
224
|
+
if (type === String) return typeof value === "string";
|
|
225
|
+
if (type === Number) return typeof value === "number" && Number.isFinite(value);
|
|
226
|
+
if (type === Date) return value instanceof Date && !Number.isNaN(value.getTime());
|
|
227
|
+
return isEntity(value) && value.constructor === type;
|
|
228
|
+
}
|
|
229
|
+
/** Drop structural duplicates, keeping the first occurrence. */
|
|
230
|
+
function unique(values) {
|
|
231
|
+
return values.filter((value, index) => values.findIndex((other) => fieldEquals(value, other)) === index);
|
|
232
|
+
}
|
|
127
233
|
// ../../packages/system-one/src/classifier/filter-predicates.ts
|
|
128
234
|
/** Retain the predicates relevant to this passage. This is the first pipeline step. */
|
|
129
235
|
async function filterPredicates({ chunk, predicates, config }) {
|
|
@@ -162,11 +268,15 @@ function needsFiltering(predicates, config) {
|
|
|
162
268
|
* Validate available predicates eagerly and return a lazy iterator of executable jobs.
|
|
163
269
|
* Bare KBs expose all known predicates; explicit guards retain their permission policy.
|
|
164
270
|
* Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
|
|
271
|
+
* With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
|
|
272
|
+
* each chunk offers, so they need no finite domain up front. Without one, a predicate
|
|
273
|
+
* with a field that has no values fails here.
|
|
165
274
|
*/
|
|
166
|
-
function classifier$1({ corpus, input, config }) {
|
|
275
|
+
function classifier$1({ corpus, input, config, createChunkSymbolExtractor }) {
|
|
167
276
|
const baselineKB = (input instanceof GuardedKB ? input : guard(input).permit("*")).fork();
|
|
168
277
|
const outputKB = baselineKB.kb.fork();
|
|
169
278
|
const predicates = classifiablePredicates(baselineKB);
|
|
279
|
+
if (createChunkSymbolExtractor === void 0) assertAskable(predicates);
|
|
170
280
|
const chunker = chunkers.recursive({
|
|
171
281
|
size: 8e3,
|
|
172
282
|
overlap: 200
|
|
@@ -174,16 +284,21 @@ function classifier$1({ corpus, input, config }) {
|
|
|
174
284
|
const jobs = corpus.documents().values().filter(() => predicates.length > 0).flatMap((document) => corpus.chunk(document, chunker)).map((chunk) => ({ execute: async () => await processChunk({
|
|
175
285
|
chunk,
|
|
176
286
|
predicates,
|
|
287
|
+
createExtractor: createChunkSymbolExtractor,
|
|
177
288
|
config,
|
|
178
289
|
kb: outputKB
|
|
179
290
|
}) }));
|
|
180
291
|
return Object.assign(jobs, { kb: outputKB });
|
|
181
292
|
}
|
|
182
|
-
async function processChunk({ chunk, predicates: inputPredicates, config, kb }) {
|
|
183
|
-
const predicates = await
|
|
293
|
+
async function processChunk({ chunk, predicates: inputPredicates, createExtractor, config, kb }) {
|
|
294
|
+
const predicates = await extractSymbols({
|
|
184
295
|
chunk,
|
|
185
|
-
predicates:
|
|
186
|
-
|
|
296
|
+
predicates: await filterPredicates({
|
|
297
|
+
chunk,
|
|
298
|
+
predicates: inputPredicates,
|
|
299
|
+
config
|
|
300
|
+
}),
|
|
301
|
+
createExtractor
|
|
187
302
|
});
|
|
188
303
|
await extractEvidence({
|
|
189
304
|
chunk,
|
|
@@ -197,39 +312,25 @@ async function processChunk({ chunk, predicates: inputPredicates, config, kb })
|
|
|
197
312
|
kb
|
|
198
313
|
});
|
|
199
314
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
});
|
|
216
|
-
if (typeof json !== "string") throw new TypeError("Cannot encode a value without a JSON representation");
|
|
217
|
-
return JSON.parse(json);
|
|
218
|
-
}
|
|
219
|
-
/** A compact diagnostic label, using entity ids and the predicate's declared field order. */
|
|
220
|
-
function formatAtom({ predicate, fields }) {
|
|
221
|
-
return `${predicate.name}(${predicate.paramNames.map((name) => format(fields[name])).join(", ")})`;
|
|
222
|
-
}
|
|
223
|
-
function format(value) {
|
|
224
|
-
if (value instanceof Date) return value.toISOString();
|
|
225
|
-
return isRecord(value) && "id" in value ? String(value["id"]) : String(value);
|
|
226
|
-
}
|
|
227
|
-
function isRecord(value) {
|
|
228
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
315
|
+
/**
|
|
316
|
+
* Throw for the first predicate with a field that has no values. Without a symbol
|
|
317
|
+
* extractor, fields hold only the values known up front, so such a predicate could never
|
|
318
|
+
* be asked about.
|
|
319
|
+
*/
|
|
320
|
+
function assertAskable(predicates) {
|
|
321
|
+
for (const { declaration, roles } of predicates) {
|
|
322
|
+
const name = emptyField({
|
|
323
|
+
declaration,
|
|
324
|
+
roles
|
|
325
|
+
});
|
|
326
|
+
if (name === void 0) continue;
|
|
327
|
+
const type = declaration.params[name];
|
|
328
|
+
throw new UnsupportedPredicateError(type === String || type === Number || type === Date ? `Cannot classify predicate "${declaration.name}": field "${name}" has an unsupported open-ended domain. Use Boolean, enumeration(...), registered entities, or a symbol extractor.` : `No registered entities for ${declaration.name}.${name}`, declaration, name);
|
|
329
|
+
}
|
|
229
330
|
}
|
|
230
|
-
/**
|
|
231
|
-
function
|
|
232
|
-
return
|
|
331
|
+
/** The first field, in declared order, with no values. */
|
|
332
|
+
function emptyField({ declaration, roles }) {
|
|
333
|
+
return declaration.paramNames.find((name) => (roles[name] ?? []).length === 0);
|
|
233
334
|
}
|
|
234
335
|
// ../../packages/system-one/src/config.ts
|
|
235
336
|
/** Default minimum probability for admitting signed evidence. @beta */
|
|
@@ -412,7 +513,7 @@ function systemOneModel(endpoint) {
|
|
|
412
513
|
judgement: choice(response, OUTCOMES, id, name)
|
|
413
514
|
})
|
|
414
515
|
})),
|
|
415
|
-
adjudicate: async ({ atom, passages }) => await ask({
|
|
516
|
+
adjudicate: async ({ atom, passages }) => passages.length === 0 ? [] : await ask({
|
|
416
517
|
endpoint,
|
|
417
518
|
prefix: "passage",
|
|
418
519
|
items: passages,
|
|
@@ -434,10 +535,12 @@ async function inBatches(items, size, request) {
|
|
|
434
535
|
* Ask one question per item in a single request and turn each answer into a result.
|
|
435
536
|
* Question keys are positional, so they are minted and matched only here. Validation
|
|
436
537
|
* happens while building results, so one bad answer rejects the whole request.
|
|
437
|
-
*
|
|
538
|
+
* Every request holds at least one question: callers send nothing when there is nothing
|
|
539
|
+
* to ask, so an empty request is a bug. More than the provider allows sends nothing and
|
|
540
|
+
* throws.
|
|
438
541
|
*/
|
|
439
542
|
async function ask({ endpoint: { name, maxBatchSize, transport }, prefix, items, state, question, answer }) {
|
|
440
|
-
|
|
543
|
+
assert(items.length > 0, `Asked ${name} an empty ${prefix} request`);
|
|
441
544
|
if (items.length > maxBatchSize) throw new RangeError(`Cannot ask ${items.length} ${prefix} questions in one request: ${name} accepts at most ${maxBatchSize}`);
|
|
442
545
|
const keyed = items.map((item, index) => ({
|
|
443
546
|
id: `${prefix}_${index}`,
|
|
@@ -584,10 +687,11 @@ function systemOne(provider) {
|
|
|
584
687
|
pipelines.set(normalized, resolved);
|
|
585
688
|
return normalized;
|
|
586
689
|
},
|
|
587
|
-
classifier: ({ corpus, input, config }) => classifier$1({
|
|
690
|
+
classifier: ({ corpus, input, config, createChunkSymbolExtractor }) => classifier$1({
|
|
588
691
|
corpus,
|
|
589
692
|
input,
|
|
590
|
-
config: pipeline(config)
|
|
693
|
+
config: pipeline(config),
|
|
694
|
+
createChunkSymbolExtractor
|
|
591
695
|
}),
|
|
592
696
|
resolver: (corpus, kb, config) => resolver$1(corpus, kb, pipeline(config))
|
|
593
697
|
};
|
|
@@ -613,6 +717,8 @@ function createConfig(config) {
|
|
|
613
717
|
* Validate available predicates eagerly and return a lazy iterator of executable jobs.
|
|
614
718
|
* Bare KBs expose all known predicates; explicit guards retain their permission policy.
|
|
615
719
|
* Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
|
|
720
|
+
* With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
|
|
721
|
+
* each chunk offers, so they need no finite domain up front.
|
|
616
722
|
* @beta
|
|
617
723
|
*/
|
|
618
724
|
function classifier(input) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factbag/jev",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.6",
|
|
4
4
|
"description": "Lazy AI-assisted classification and evidence resolution for Factbag.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"classification",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"vitest": "5.0.5"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
|
-
"factbag": "0.0.0-alpha.
|
|
92
|
+
"factbag": "0.0.0-alpha.6",
|
|
93
93
|
"@typesafe-ai/sdk": "^0.7.0"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|