@promptev/context-engine 0.0.3 → 0.0.4
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 +25 -1
- package/dist/cli.js +527 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +532 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +244 -3
- package/dist/index.d.ts +244 -3
- package/dist/index.js +528 -17
- package/dist/index.js.map +1 -1
- package/dist/{mcp-BKSmxayM.d.cts → mcp-uirRbluA.d.cts} +4 -0
- package/dist/{mcp-BKSmxayM.d.ts → mcp-uirRbluA.d.ts} +4 -0
- package/dist/mcp.cjs +50 -10
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +50 -10
- package/dist/mcp.js.map +1 -1
- package/dist/skills/context-engine/SKILL.md +29 -1
- package/package.json +1 -1
- package/src/skills/context-engine/SKILL.md +29 -1
package/dist/cli.js
CHANGED
|
@@ -6186,15 +6186,16 @@ function parseDocumentId(documentId) {
|
|
|
6186
6186
|
}
|
|
6187
6187
|
return raw;
|
|
6188
6188
|
}
|
|
6189
|
-
function scopeSql(sourceIds, principals, params) {
|
|
6189
|
+
function scopeSql(sourceIds, principals, params, alias = "") {
|
|
6190
|
+
const col = alias ? `${alias}.` : "";
|
|
6190
6191
|
const where = [];
|
|
6191
6192
|
if (sourceIds != null) {
|
|
6192
6193
|
params.push(sourceIds);
|
|
6193
|
-
where.push(
|
|
6194
|
+
where.push(`${col}source_id = ANY($${params.length}::text[])`);
|
|
6194
6195
|
}
|
|
6195
6196
|
if (principals != null) {
|
|
6196
6197
|
params.push(principals);
|
|
6197
|
-
where.push(`(acl IS NULL OR acl && $${params.length}::text[])`);
|
|
6198
|
+
where.push(`(${col}acl IS NULL OR ${col}acl && $${params.length}::text[])`);
|
|
6198
6199
|
}
|
|
6199
6200
|
return where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
6200
6201
|
}
|
|
@@ -6453,6 +6454,19 @@ async function listDocuments(opts) {
|
|
|
6453
6454
|
const principals = opts.principals ?? null;
|
|
6454
6455
|
const params = [];
|
|
6455
6456
|
let where = scopeSql(sourceIds, principals, params);
|
|
6457
|
+
if (opts.documentIds != null) {
|
|
6458
|
+
const parsedIds = [];
|
|
6459
|
+
for (const did of opts.documentIds) {
|
|
6460
|
+
try {
|
|
6461
|
+
parsedIds.push(parseDocumentId(did));
|
|
6462
|
+
} catch {
|
|
6463
|
+
console.warn("listDocuments: skipping invalid document id %s", did);
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6466
|
+
params.push(parsedIds);
|
|
6467
|
+
const clause = `id = ANY($${params.length}::uuid[])`;
|
|
6468
|
+
where = where ? `${where} AND ${clause}` : `WHERE ${clause}`;
|
|
6469
|
+
}
|
|
6456
6470
|
const cursorTime = parsedCursor?.time ?? parsedCursor?.created_at;
|
|
6457
6471
|
const cursorId = parsedCursor?.id;
|
|
6458
6472
|
if (cursorTime && cursorId && UUID_RE2.test(String(cursorId))) {
|
|
@@ -6512,6 +6526,350 @@ async function listDocuments(opts) {
|
|
|
6512
6526
|
}
|
|
6513
6527
|
return result;
|
|
6514
6528
|
}
|
|
6529
|
+
async function spreadsheetSchema(opts) {
|
|
6530
|
+
const ids = [];
|
|
6531
|
+
for (const did of opts.documentIds ?? []) {
|
|
6532
|
+
try {
|
|
6533
|
+
ids.push(parseDocumentId(did));
|
|
6534
|
+
} catch {
|
|
6535
|
+
console.warn("spreadsheetSchema: skipping invalid document id %s", did);
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
if (!ids.length) return [];
|
|
6539
|
+
const principals = opts.principals ?? null;
|
|
6540
|
+
const budget = opts.budget ?? MAX_SCHEMA_TEXT_CHARS;
|
|
6541
|
+
const params = [];
|
|
6542
|
+
let where = scopeSql(opts.sourceIds ?? null, principals, params);
|
|
6543
|
+
const mimeClause = TABULAR_MIME_PATTERNS.map((pattern) => {
|
|
6544
|
+
params.push(pattern);
|
|
6545
|
+
return `mime_type ILIKE $${params.length}`;
|
|
6546
|
+
}).join(" OR ");
|
|
6547
|
+
where = where ? `${where} AND (${mimeClause})` : `WHERE (${mimeClause})`;
|
|
6548
|
+
params.push(ids);
|
|
6549
|
+
where += ` AND id = ANY($${params.length}::uuid[])`;
|
|
6550
|
+
const sized = await opts.pool.query(
|
|
6551
|
+
`SELECT id, name, source_id, mime_type, COALESCE(LENGTH(text), 0) AS text_len
|
|
6552
|
+
FROM context_engine_documents
|
|
6553
|
+
${where}`,
|
|
6554
|
+
params
|
|
6555
|
+
);
|
|
6556
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6557
|
+
for (const row of sized.rows) {
|
|
6558
|
+
if (isTabularMime(row.mime_type)) byId.set(String(row.id), row);
|
|
6559
|
+
}
|
|
6560
|
+
const rows = ids.map((id) => byId.get(id)).filter((r) => r != null);
|
|
6561
|
+
const chosen = [];
|
|
6562
|
+
let spent = 0;
|
|
6563
|
+
for (const row of rows) {
|
|
6564
|
+
const len = Number(row.text_len ?? 0);
|
|
6565
|
+
if (chosen.length && spent + len > budget) break;
|
|
6566
|
+
chosen.push(String(row.id));
|
|
6567
|
+
spent += len;
|
|
6568
|
+
}
|
|
6569
|
+
const texts = /* @__PURE__ */ new Map();
|
|
6570
|
+
if (chosen.length) {
|
|
6571
|
+
const bodyParams = [];
|
|
6572
|
+
const bodyWhere = scopeSql(opts.sourceIds ?? null, principals, bodyParams);
|
|
6573
|
+
bodyParams.push(chosen);
|
|
6574
|
+
const clause = `id = ANY($${bodyParams.length}::uuid[])`;
|
|
6575
|
+
const { rows: bodies } = await opts.pool.query(
|
|
6576
|
+
`SELECT id, text FROM context_engine_documents
|
|
6577
|
+
${bodyWhere ? `${bodyWhere} AND ${clause}` : `WHERE ${clause}`}`,
|
|
6578
|
+
bodyParams
|
|
6579
|
+
);
|
|
6580
|
+
for (const row of bodies) texts.set(String(row.id), row.text ?? null);
|
|
6581
|
+
}
|
|
6582
|
+
return rows.map((row) => {
|
|
6583
|
+
const id = String(row.id);
|
|
6584
|
+
if (!texts.has(id)) {
|
|
6585
|
+
return {
|
|
6586
|
+
document_id: id,
|
|
6587
|
+
name: row.name,
|
|
6588
|
+
source_id: row.source_id,
|
|
6589
|
+
sheets: null,
|
|
6590
|
+
schema_unavailable: "not read: this page of spreadsheets is past the text budget \u2014 narrow with sourceIds, or ask for a smaller limit"
|
|
6591
|
+
};
|
|
6592
|
+
}
|
|
6593
|
+
return {
|
|
6594
|
+
document_id: id,
|
|
6595
|
+
name: row.name,
|
|
6596
|
+
source_id: row.source_id,
|
|
6597
|
+
sheets: redactValueRecursive(spreadsheetSchemaFromText(texts.get(id)), opts.redaction, {
|
|
6598
|
+
principals,
|
|
6599
|
+
secretKey: opts.secretKey ?? null,
|
|
6600
|
+
hooks: opts.hooks
|
|
6601
|
+
})
|
|
6602
|
+
};
|
|
6603
|
+
});
|
|
6604
|
+
}
|
|
6605
|
+
function remainder(structure) {
|
|
6606
|
+
let total = 0;
|
|
6607
|
+
for (const [key, value] of Object.entries(structure)) {
|
|
6608
|
+
if (key.startsWith("more_") && typeof value === "number") total += value;
|
|
6609
|
+
}
|
|
6610
|
+
const sheets = structure.sheets;
|
|
6611
|
+
if (Array.isArray(sheets)) {
|
|
6612
|
+
for (const sheet of sheets) {
|
|
6613
|
+
if (sheet && typeof sheet === "object" && typeof sheet.more_columns === "number") {
|
|
6614
|
+
total += sheet.more_columns;
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
return total;
|
|
6619
|
+
}
|
|
6620
|
+
function boundedSheets(sheets) {
|
|
6621
|
+
if (!sheets?.length) return sheets;
|
|
6622
|
+
return sheets.slice(0, MAX_STRUCTURE_ITEMS).map((sheet) => {
|
|
6623
|
+
const columns = sheet.columns ?? [];
|
|
6624
|
+
const trimmed = {
|
|
6625
|
+
...sheet,
|
|
6626
|
+
columns: columns.slice(0, MAX_STRUCTURE_ITEMS)
|
|
6627
|
+
};
|
|
6628
|
+
if (columns.length > MAX_STRUCTURE_ITEMS) {
|
|
6629
|
+
trimmed.more_columns = columns.length - MAX_STRUCTURE_ITEMS;
|
|
6630
|
+
}
|
|
6631
|
+
return trimmed;
|
|
6632
|
+
});
|
|
6633
|
+
}
|
|
6634
|
+
async function documentStructure(opts) {
|
|
6635
|
+
const ids = [];
|
|
6636
|
+
for (const did of opts.documentIds ?? []) {
|
|
6637
|
+
try {
|
|
6638
|
+
ids.push(parseDocumentId(did));
|
|
6639
|
+
} catch {
|
|
6640
|
+
console.warn("documentStructure: skipping invalid document id %s", did);
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6643
|
+
if (!ids.length) return {};
|
|
6644
|
+
const principals = opts.principals ?? null;
|
|
6645
|
+
const bounded = opts.bounded ?? true;
|
|
6646
|
+
const described2 = await spreadsheetSchema({
|
|
6647
|
+
pool: opts.pool,
|
|
6648
|
+
documentIds: ids,
|
|
6649
|
+
sourceIds: opts.sourceIds,
|
|
6650
|
+
principals,
|
|
6651
|
+
budget: opts.budget,
|
|
6652
|
+
redaction: opts.redaction,
|
|
6653
|
+
secretKey: opts.secretKey,
|
|
6654
|
+
hooks: opts.hooks
|
|
6655
|
+
});
|
|
6656
|
+
const tabular = new Map(described2.map((entry) => [entry.document_id, entry]));
|
|
6657
|
+
const params = [];
|
|
6658
|
+
let where = scopeSql(opts.sourceIds ?? null, principals, params, "d");
|
|
6659
|
+
params.push(ids);
|
|
6660
|
+
const clause = `c.document_id = ANY($${params.length}::uuid[])`;
|
|
6661
|
+
where = where ? `${where} AND ${clause}` : `WHERE ${clause}`;
|
|
6662
|
+
const { rows } = await opts.pool.query(
|
|
6663
|
+
`SELECT c.document_id AS document_id,
|
|
6664
|
+
c.meta_data->>'section_title' AS section,
|
|
6665
|
+
c.meta_data->>'top_level_key' AS key,
|
|
6666
|
+
MIN(c.idx) AS first_idx,
|
|
6667
|
+
COUNT(*) AS chunks,
|
|
6668
|
+
-- Only the page-marker pass writes the page number, and it writes an int
|
|
6669
|
+
-- \u2014 but a cast that meets anything else raises for the whole
|
|
6670
|
+
-- query, so the type is checked in SQL rather than assumed.
|
|
6671
|
+
MAX(CASE WHEN jsonb_typeof(c.meta_data->'page') = 'number'
|
|
6672
|
+
THEN (c.meta_data->>'page')::int END) AS page
|
|
6673
|
+
FROM context_engine_chunks c
|
|
6674
|
+
JOIN context_engine_documents d ON d.id = c.document_id
|
|
6675
|
+
${where}
|
|
6676
|
+
GROUP BY c.document_id, section, key`,
|
|
6677
|
+
params
|
|
6678
|
+
);
|
|
6679
|
+
const outline = /* @__PURE__ */ new Map();
|
|
6680
|
+
for (const row of rows) {
|
|
6681
|
+
const id = String(row.document_id);
|
|
6682
|
+
let found = outline.get(id);
|
|
6683
|
+
if (!found) {
|
|
6684
|
+
found = { chunks: 0, page: null, parts: [] };
|
|
6685
|
+
outline.set(id, found);
|
|
6686
|
+
}
|
|
6687
|
+
found.chunks += Number(row.chunks ?? 0);
|
|
6688
|
+
if (row.page != null) found.page = Math.max(found.page ?? 0, Number(row.page));
|
|
6689
|
+
found.parts.push([Number(row.first_idx ?? 0), row.section ?? null, row.key ?? null]);
|
|
6690
|
+
}
|
|
6691
|
+
const ordered = (parts, pick, cap) => {
|
|
6692
|
+
const seen = [];
|
|
6693
|
+
for (const part of [...parts].sort((a, b) => a[0] - b[0])) {
|
|
6694
|
+
const value = pick(part);
|
|
6695
|
+
if (value && !seen.includes(value)) seen.push(value);
|
|
6696
|
+
}
|
|
6697
|
+
return [cap === null ? seen : seen.slice(0, cap), seen.length];
|
|
6698
|
+
};
|
|
6699
|
+
const out = {};
|
|
6700
|
+
for (const id of ids) {
|
|
6701
|
+
const found = outline.get(id);
|
|
6702
|
+
const structure = { chunks: found ? found.chunks : 0 };
|
|
6703
|
+
const sheetDoc = tabular.get(id);
|
|
6704
|
+
if (sheetDoc) {
|
|
6705
|
+
const sheets = sheetDoc.sheets ?? null;
|
|
6706
|
+
structure.sheets = bounded ? boundedSheets(sheets) : sheets;
|
|
6707
|
+
if (bounded && sheets && sheets.length > MAX_STRUCTURE_ITEMS) {
|
|
6708
|
+
structure.more_sheets = sheets.length - MAX_STRUCTURE_ITEMS;
|
|
6709
|
+
}
|
|
6710
|
+
if (sheetDoc.schema_unavailable) structure.schema_unavailable = sheetDoc.schema_unavailable;
|
|
6711
|
+
out[id] = structure;
|
|
6712
|
+
continue;
|
|
6713
|
+
}
|
|
6714
|
+
if (found) {
|
|
6715
|
+
if (found.page != null) {
|
|
6716
|
+
structure.last_page = found.page;
|
|
6717
|
+
}
|
|
6718
|
+
const cap = bounded ? MAX_STRUCTURE_ITEMS : null;
|
|
6719
|
+
const [sections, totalSections] = ordered(found.parts, (p) => p[1], cap);
|
|
6720
|
+
if (sections.length) {
|
|
6721
|
+
structure.sections = sections;
|
|
6722
|
+
if (totalSections > sections.length) structure.more_sections = totalSections - sections.length;
|
|
6723
|
+
}
|
|
6724
|
+
const [keys, totalKeys] = ordered(found.parts, (p) => p[2], cap);
|
|
6725
|
+
if (keys.length) {
|
|
6726
|
+
structure.keys = keys;
|
|
6727
|
+
if (totalKeys > keys.length) structure.more_keys = totalKeys - keys.length;
|
|
6728
|
+
}
|
|
6729
|
+
}
|
|
6730
|
+
out[id] = structure;
|
|
6731
|
+
}
|
|
6732
|
+
for (const [id, structure] of Object.entries(out)) {
|
|
6733
|
+
const left = remainder(structure);
|
|
6734
|
+
if (!left) continue;
|
|
6735
|
+
if (left <= MAX_INVITED_ITEMS) {
|
|
6736
|
+
structure.next_action = { action: "discover", document_ids: [id] };
|
|
6737
|
+
} else if ("sheets" in structure) {
|
|
6738
|
+
structure.instead = "too many to list: a sheet this wide is one to COMPUTE over, not to read. Use action 'compute' and name the columns above, or search for a value to find the one row you want.";
|
|
6739
|
+
} else {
|
|
6740
|
+
structure.instead = "too many to list: read the document with get_chunks, or search it for the part you need.";
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
const redactOpts = { principals, secretKey: opts.secretKey ?? null, hooks: opts.hooks };
|
|
6744
|
+
return Object.fromEntries(
|
|
6745
|
+
Object.entries(out).map(([id, structure]) => [
|
|
6746
|
+
id,
|
|
6747
|
+
redactValueRecursive(structure, opts.redaction, redactOpts)
|
|
6748
|
+
])
|
|
6749
|
+
);
|
|
6750
|
+
}
|
|
6751
|
+
async function documentTypes(opts) {
|
|
6752
|
+
const principals = opts.principals ?? null;
|
|
6753
|
+
const params = [];
|
|
6754
|
+
let where = scopeSql(opts.sourceIds ?? null, principals, params);
|
|
6755
|
+
if (opts.documentIds != null) {
|
|
6756
|
+
const parsedIds = [];
|
|
6757
|
+
for (const did of opts.documentIds) {
|
|
6758
|
+
try {
|
|
6759
|
+
parsedIds.push(parseDocumentId(did));
|
|
6760
|
+
} catch {
|
|
6761
|
+
console.warn("documentTypes: skipping invalid document id %s", did);
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
6764
|
+
params.push(parsedIds);
|
|
6765
|
+
const clause = `id = ANY($${params.length}::uuid[])`;
|
|
6766
|
+
where = where ? `${where} AND ${clause}` : `WHERE ${clause}`;
|
|
6767
|
+
}
|
|
6768
|
+
params.push(MAX_CENSUS_ROWS);
|
|
6769
|
+
const { rows } = await opts.pool.query(
|
|
6770
|
+
`SELECT document_type AS doc_type,
|
|
6771
|
+
mime_type,
|
|
6772
|
+
COUNT(*) AS documents,
|
|
6773
|
+
COUNT(*) FILTER (
|
|
6774
|
+
WHERE structured_data IS NOT NULL AND structured_data::text <> '{}'
|
|
6775
|
+
) AS with_fields
|
|
6776
|
+
FROM context_engine_documents
|
|
6777
|
+
${where}
|
|
6778
|
+
GROUP BY document_type, mime_type
|
|
6779
|
+
ORDER BY COUNT(*) DESC
|
|
6780
|
+
LIMIT $${params.length}`,
|
|
6781
|
+
params
|
|
6782
|
+
);
|
|
6783
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
6784
|
+
for (const row of rows) {
|
|
6785
|
+
const kind = isTabularMime(row.mime_type) ? "spreadsheet" : "text";
|
|
6786
|
+
const type = row.doc_type ?? null;
|
|
6787
|
+
const bucketKey = `${kind}\0${type ?? ""}`;
|
|
6788
|
+
let bucket = buckets.get(bucketKey);
|
|
6789
|
+
if (!bucket) {
|
|
6790
|
+
bucket = { kind, type, documents: 0, with_fields: 0 };
|
|
6791
|
+
buckets.set(bucketKey, bucket);
|
|
6792
|
+
}
|
|
6793
|
+
bucket.documents += Number(row.documents ?? 0);
|
|
6794
|
+
bucket.with_fields += Number(row.with_fields ?? 0);
|
|
6795
|
+
}
|
|
6796
|
+
const census = [...buckets.values()].sort(
|
|
6797
|
+
(a, b) => b.documents - a.documents || a.kind.localeCompare(b.kind) || (a.type ?? "").localeCompare(b.type ?? "")
|
|
6798
|
+
).slice(0, opts.limit ?? MAX_DOCUMENT_TYPES);
|
|
6799
|
+
return redactValueRecursive(census, opts.redaction, {
|
|
6800
|
+
principals,
|
|
6801
|
+
secretKey: opts.secretKey ?? null,
|
|
6802
|
+
hooks: opts.hooks
|
|
6803
|
+
});
|
|
6804
|
+
}
|
|
6805
|
+
async function fieldSummary(opts) {
|
|
6806
|
+
const principals = opts.principals ?? null;
|
|
6807
|
+
const maxFieldsPerType = opts.maxFieldsPerType ?? MAX_FIELDS_PER_TYPE;
|
|
6808
|
+
const params = [];
|
|
6809
|
+
let where = scopeSql(opts.sourceIds ?? null, principals, params);
|
|
6810
|
+
if (opts.documentIds != null) {
|
|
6811
|
+
const parsedIds = [];
|
|
6812
|
+
for (const did of opts.documentIds) {
|
|
6813
|
+
try {
|
|
6814
|
+
parsedIds.push(parseDocumentId(did));
|
|
6815
|
+
} catch {
|
|
6816
|
+
console.warn("fieldSummary: skipping invalid document id %s", did);
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
params.push(parsedIds);
|
|
6820
|
+
const clause = `id = ANY($${params.length}::uuid[])`;
|
|
6821
|
+
where = where ? `${where} AND ${clause}` : `WHERE ${clause}`;
|
|
6822
|
+
}
|
|
6823
|
+
const typed = "jsonb_typeof(structured_data) = 'object'";
|
|
6824
|
+
where = where ? `${where} AND ${typed}` : `WHERE ${typed}`;
|
|
6825
|
+
params.push(MAX_FIELD_ROWS);
|
|
6826
|
+
const { rows } = await opts.pool.query(
|
|
6827
|
+
`SELECT d.document_type AS doc_type,
|
|
6828
|
+
d.mime_type AS mime_type,
|
|
6829
|
+
kv.key AS key,
|
|
6830
|
+
COUNT(*) AS documents,
|
|
6831
|
+
MAX(kv.value::text) AS sample
|
|
6832
|
+
FROM context_engine_documents d
|
|
6833
|
+
JOIN LATERAL jsonb_each(d.structured_data) AS kv(key, value) ON TRUE
|
|
6834
|
+
${where}
|
|
6835
|
+
GROUP BY d.document_type, d.mime_type, kv.key
|
|
6836
|
+
ORDER BY d.document_type, COUNT(*) DESC, kv.key
|
|
6837
|
+
LIMIT $${params.length}`,
|
|
6838
|
+
params
|
|
6839
|
+
);
|
|
6840
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6841
|
+
for (const row of rows) {
|
|
6842
|
+
const kind = isTabularMime(row.mime_type) ? "spreadsheet" : "text";
|
|
6843
|
+
const type = row.doc_type ?? null;
|
|
6844
|
+
const groupKey = `${kind}\0${type ?? ""}`;
|
|
6845
|
+
let group = groups.get(groupKey);
|
|
6846
|
+
if (!group) {
|
|
6847
|
+
group = { kind, type, fields: [] };
|
|
6848
|
+
groups.set(groupKey, group);
|
|
6849
|
+
}
|
|
6850
|
+
let sample = null;
|
|
6851
|
+
try {
|
|
6852
|
+
sample = row.sample == null ? null : JSON.parse(String(row.sample));
|
|
6853
|
+
} catch {
|
|
6854
|
+
sample = row.sample;
|
|
6855
|
+
}
|
|
6856
|
+
if (group.fields.length < maxFieldsPerType) {
|
|
6857
|
+
group.fields.push({
|
|
6858
|
+
field: String(row.key),
|
|
6859
|
+
type: inferDataType(String(row.key), sample),
|
|
6860
|
+
documents: Number(row.documents)
|
|
6861
|
+
});
|
|
6862
|
+
} else {
|
|
6863
|
+
group.more_fields ??= [];
|
|
6864
|
+
group.more_fields.push(String(row.key));
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
6867
|
+
return redactValueRecursive([...groups.values()], opts.redaction, {
|
|
6868
|
+
principals,
|
|
6869
|
+
secretKey: opts.secretKey ?? null,
|
|
6870
|
+
hooks: opts.hooks
|
|
6871
|
+
});
|
|
6872
|
+
}
|
|
6515
6873
|
function candidateFieldTokens(question) {
|
|
6516
6874
|
const candidates = [];
|
|
6517
6875
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6592,8 +6950,7 @@ async function requireDanfo() {
|
|
|
6592
6950
|
throw new ExtraMissingError("compute", "danfojs-node", "compute() dataframes");
|
|
6593
6951
|
}
|
|
6594
6952
|
}
|
|
6595
|
-
function
|
|
6596
|
-
if (!text) return {};
|
|
6953
|
+
function splitSheets(text) {
|
|
6597
6954
|
const sheets = {};
|
|
6598
6955
|
let current = "sheet1";
|
|
6599
6956
|
for (const line of text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n")) {
|
|
@@ -6610,6 +6967,34 @@ function parseSpreadsheetText(dfd, text) {
|
|
|
6610
6967
|
}
|
|
6611
6968
|
bucket.push(line);
|
|
6612
6969
|
}
|
|
6970
|
+
return sheets;
|
|
6971
|
+
}
|
|
6972
|
+
function spreadsheetSchemaFromText(text) {
|
|
6973
|
+
if (!text) return [];
|
|
6974
|
+
const out = [];
|
|
6975
|
+
for (const [name, lines] of Object.entries(splitSheets(text))) {
|
|
6976
|
+
const body = lines.join("\n").trim();
|
|
6977
|
+
if (!body) continue;
|
|
6978
|
+
let rows;
|
|
6979
|
+
try {
|
|
6980
|
+
rows = parseCsv(body);
|
|
6981
|
+
} catch (exc) {
|
|
6982
|
+
console.warn("discover: sheet '%s' not parseable as CSV: %s", name, exc);
|
|
6983
|
+
continue;
|
|
6984
|
+
}
|
|
6985
|
+
rows = rows.filter((row) => row.some((cell) => (cell ?? "").trim()));
|
|
6986
|
+
if (!rows.length) continue;
|
|
6987
|
+
out.push({
|
|
6988
|
+
name,
|
|
6989
|
+
columns: rows[0].map((cell) => String(cell).trim()),
|
|
6990
|
+
row_count: rows.length - 1
|
|
6991
|
+
});
|
|
6992
|
+
}
|
|
6993
|
+
return out;
|
|
6994
|
+
}
|
|
6995
|
+
function parseSpreadsheetText(dfd, text) {
|
|
6996
|
+
if (!text) return {};
|
|
6997
|
+
const sheets = splitSheets(text);
|
|
6613
6998
|
const out = {};
|
|
6614
6999
|
for (const [name, lines] of Object.entries(sheets)) {
|
|
6615
7000
|
const body = lines.join("\n").trim();
|
|
@@ -6822,7 +7207,7 @@ ${schemaLines.join("\n")}`;
|
|
|
6822
7207
|
hooks
|
|
6823
7208
|
});
|
|
6824
7209
|
}
|
|
6825
|
-
var MAX_LIST_LIMIT, MAX_COMPUTE_TEXT_CHARS, DEFAULT_COMPUTE_TIMEOUT, MAX_COMPUTE_DOCUMENTS, TABULAR_MIME_PATTERNS, UUID_RE2, WORD_RE, STOPWORDS, SHEET_MARKER_RE2, CODE_FENCE_RE, COMPUTE_SYSTEM_PROMPT, visible, MAX_CHUNKS_PER_READ, MAX_MAP_REDUCE_DOCS, DEFAULT_MAP_REDUCE_DOCS, MAX_MAP_REDUCE_CONCURRENCY;
|
|
7210
|
+
var MAX_LIST_LIMIT, MAX_COMPUTE_TEXT_CHARS, DEFAULT_COMPUTE_TIMEOUT, MAX_COMPUTE_DOCUMENTS, MAX_SCHEMA_TEXT_CHARS, MAX_FIELDS_PER_TYPE, MAX_FIELD_ROWS, MAX_STRUCTURE_ITEMS, MAX_INVITED_ITEMS, MAX_DOCUMENT_TYPES, MAX_CENSUS_ROWS, TABULAR_MIME_PATTERNS, UUID_RE2, WORD_RE, STOPWORDS, SHEET_MARKER_RE2, CODE_FENCE_RE, COMPUTE_SYSTEM_PROMPT, visible, MAX_CHUNKS_PER_READ, MAX_MAP_REDUCE_DOCS, DEFAULT_MAP_REDUCE_DOCS, MAX_MAP_REDUCE_CONCURRENCY;
|
|
6826
7211
|
var init_actions = __esm({
|
|
6827
7212
|
"src/actions.ts"() {
|
|
6828
7213
|
init_chunkers();
|
|
@@ -6838,6 +7223,13 @@ var init_actions = __esm({
|
|
|
6838
7223
|
MAX_COMPUTE_TEXT_CHARS = 2e6;
|
|
6839
7224
|
DEFAULT_COMPUTE_TIMEOUT = 30;
|
|
6840
7225
|
MAX_COMPUTE_DOCUMENTS = 50;
|
|
7226
|
+
MAX_SCHEMA_TEXT_CHARS = MAX_COMPUTE_TEXT_CHARS;
|
|
7227
|
+
MAX_FIELDS_PER_TYPE = 40;
|
|
7228
|
+
MAX_FIELD_ROWS = 2e3;
|
|
7229
|
+
MAX_STRUCTURE_ITEMS = 40;
|
|
7230
|
+
MAX_INVITED_ITEMS = 400;
|
|
7231
|
+
MAX_DOCUMENT_TYPES = 30;
|
|
7232
|
+
MAX_CENSUS_ROWS = 500;
|
|
6841
7233
|
TABULAR_MIME_PATTERNS = ["%csv%", "%sheet%", "%excel%", "%spreadsheetml%", "%tab-separated%"];
|
|
6842
7234
|
UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6843
7235
|
WORD_RE = /[^\W\d_]+/gu;
|
|
@@ -7440,11 +7832,16 @@ function parseCursor2(cursor) {
|
|
|
7440
7832
|
}
|
|
7441
7833
|
return parsed;
|
|
7442
7834
|
}
|
|
7443
|
-
async function listPage(engine, sourceIds, principals, action, limit, cursor, ceiling, redaction) {
|
|
7835
|
+
async function listPage(engine, sourceIds, principals, action, limit, cursor, documentIds, ceiling, redaction) {
|
|
7444
7836
|
const page = await engine.listDocuments({
|
|
7445
7837
|
// `!= null`, NOT truthiness: an EMPTY array means "nothing is in scope"
|
|
7446
7838
|
// and collapsing it to null would list the whole corpus.
|
|
7447
7839
|
sourceIds: sourceIds != null ? [...new Set(sourceIds)] : null,
|
|
7840
|
+
// Already intersected with the host's ceiling by `narrowToCeiling`, and
|
|
7841
|
+
// filtered in SQL rather than after the page is built — three named
|
|
7842
|
+
// documents sitting on page four must come back as themselves, not as an
|
|
7843
|
+
// empty page.
|
|
7844
|
+
documentIds: documentIds != null ? [...new Set(documentIds)] : null,
|
|
7448
7845
|
principals,
|
|
7449
7846
|
cursor,
|
|
7450
7847
|
limit: Math.max(1, Math.min(limit, MAX_LIST_LIMIT)),
|
|
@@ -7454,7 +7851,9 @@ async function listPage(engine, sourceIds, principals, action, limit, cursor, ce
|
|
|
7454
7851
|
id: String(raw.id),
|
|
7455
7852
|
name: raw.name,
|
|
7456
7853
|
source_id: raw.sourceId ?? raw.source_id,
|
|
7457
|
-
kind: documentKind(raw)
|
|
7854
|
+
kind: documentKind(raw),
|
|
7855
|
+
document_type: raw.documentType ?? raw.document_type ?? null,
|
|
7856
|
+
mode: raw.mode ?? null
|
|
7458
7857
|
}));
|
|
7459
7858
|
if (ceiling.documentIds != null) {
|
|
7460
7859
|
const allowed = new Set(ceiling.documentIds);
|
|
@@ -7507,30 +7906,62 @@ async function callKnowledgeTool(engine, args) {
|
|
|
7507
7906
|
action,
|
|
7508
7907
|
args.limit ?? 50,
|
|
7509
7908
|
parseCursor2(args.cursor),
|
|
7909
|
+
documentIds,
|
|
7510
7910
|
ceiling,
|
|
7511
7911
|
args.redaction
|
|
7512
7912
|
);
|
|
7513
7913
|
if (action === "list") return { success: true, ...page };
|
|
7514
|
-
const
|
|
7914
|
+
const structures = await engine.documentStructure({
|
|
7915
|
+
documentIds: page.documents.map((d) => d.id),
|
|
7916
|
+
sourceIds,
|
|
7917
|
+
principals,
|
|
7918
|
+
// The cap is for the call that did NOT name its documents. A caller
|
|
7919
|
+
// that asked about specific documents asked for all of them, and the
|
|
7920
|
+
// truncated payload tells it to make exactly this call — so answering
|
|
7921
|
+
// it truncated again would be a loop.
|
|
7922
|
+
bounded: !requestedDocumentIds?.length,
|
|
7923
|
+
redaction: args.redaction
|
|
7924
|
+
});
|
|
7925
|
+
for (const doc of page.documents) doc.structure = structures[doc.id] ?? {};
|
|
7926
|
+
const hasSpreadsheet = page.documents.some((d) => d.kind === "spreadsheet");
|
|
7927
|
+
const fieldsByType = await engine.fieldSummary({
|
|
7928
|
+
sourceIds,
|
|
7929
|
+
documentIds,
|
|
7930
|
+
principals,
|
|
7931
|
+
redaction: args.redaction
|
|
7932
|
+
});
|
|
7933
|
+
const census = await engine.documentTypes({
|
|
7934
|
+
sourceIds,
|
|
7935
|
+
documentIds,
|
|
7936
|
+
principals,
|
|
7937
|
+
redaction: args.redaction
|
|
7938
|
+
});
|
|
7515
7939
|
const forAFact = { action: "search", query: "<bare identifier or key words>" };
|
|
7516
7940
|
const nextAction = {};
|
|
7517
|
-
if (
|
|
7941
|
+
if (hasSpreadsheet && available.compute) {
|
|
7518
7942
|
nextAction["for a figure from a spreadsheet"] = {
|
|
7519
7943
|
action: "compute",
|
|
7520
|
-
query: "<what to compute, columns as named>"
|
|
7944
|
+
query: "<what to compute, columns as named above>"
|
|
7521
7945
|
};
|
|
7522
7946
|
}
|
|
7523
7947
|
nextAction["for a clause or a fact"] = forAFact;
|
|
7948
|
+
if (fieldsByType.length && available.query_meta) {
|
|
7949
|
+
nextAction["for documents by a field value"] = {
|
|
7950
|
+
action: "query_meta",
|
|
7951
|
+
query: "<a question naming a field from fields_by_type>"
|
|
7952
|
+
};
|
|
7953
|
+
}
|
|
7524
7954
|
if (available.get_neighbors) {
|
|
7525
7955
|
nextAction["for how things connect"] = {
|
|
7526
7956
|
action: "get_neighbors",
|
|
7527
7957
|
entity: "<a name that appears in the documents>"
|
|
7528
7958
|
};
|
|
7529
7959
|
}
|
|
7530
|
-
|
|
7960
|
+
const discovered = {
|
|
7531
7961
|
success: true,
|
|
7532
7962
|
...page,
|
|
7533
|
-
|
|
7963
|
+
document_types: census,
|
|
7964
|
+
fields_by_type: fieldsByType,
|
|
7534
7965
|
available_actions: Object.fromEntries(
|
|
7535
7966
|
Object.keys(ACTION_HELP).map((name) => [
|
|
7536
7967
|
name,
|
|
@@ -7539,6 +7970,7 @@ async function callKnowledgeTool(engine, args) {
|
|
|
7539
7970
|
),
|
|
7540
7971
|
next_action: nextAction
|
|
7541
7972
|
};
|
|
7973
|
+
return discovered;
|
|
7542
7974
|
}
|
|
7543
7975
|
if (action === "search") {
|
|
7544
7976
|
if (!text) return { success: false, error: "search needs a query" };
|
|
@@ -7768,7 +8200,7 @@ var init_knowledge_tool = __esm({
|
|
|
7768
8200
|
"get_neighbors",
|
|
7769
8201
|
"community_summary"
|
|
7770
8202
|
];
|
|
7771
|
-
KNOWLEDGE_TOOL_DESCRIPTION = "The knowledge base \u2014 the ingested documents and data files (PDF, Word, Excel, CSV and the rest) \u2014 as ONE tool with actions. Nothing is searched for you: use it before answering anything that should come from those documents, and do not use it for general knowledge. Call action 'discover' FIRST when you do not already know what is there: it lists the documents
|
|
8203
|
+
KNOWLEDGE_TOOL_DESCRIPTION = "The knowledge base \u2014 the ingested documents and data files (PDF, Word, Excel, CSV and the rest) \u2014 as ONE tool with actions. Nothing is searched for you: use it before answering anything that should come from those documents, and do not use it for general knowledge. Call action 'discover' FIRST when you do not already know what is there: it lists the documents and says what is INSIDE each one \u2014 a spreadsheet's sheet names, column headers and row counts; a document's section titles and last page; a JSON file's top-level keys \u2014 plus the extracted field names grouped by document type with each field's data type and how many documents carry it, a census of the whole corpus, and which actions this deployment can run. Use those names verbatim: they are what compute, query_meta and get_chunks match on, so one discover is enough and you never have to go looking for a column, a section or a field name. A long list is shortened there, with the rest reported as a count and, when it is worth the trip, the exact call that returns it in full \u2014 discover again with document_ids set to that one document, which answers whole. When it is not worth the trip the payload says so and says what to do instead: a sheet with hundreds of columns is one to compute over, never one to read back. Then match the action to the task. 'search' finds passages by meaning or keywords \u2014 for questions answered by reading text. Looking up an identifier (an ID, code, SKU or invoice number) is the exception: search the BARE identifier alone, e.g. '2525', never the whole question. Those are CONTENT identifiers \u2014 written inside a document \u2014 and they belong in a query; a document_id is a system id (a uuid) that only discover, list or a search hit can give you, so never search a uuid as text and never hand an invoice number to get_doc. 'get_doc' reads one whole document by id, 'get_docs' reads several at once, and 'get_chunks' walks one long document in order a piece at a time when you need more of it than an excerpt; 'list' browses the documents without searching. 'map_reduce' asks the SAME question of every document in scope and answers once per document \u2014 for 'which contracts mention X', where search would return a handful of passages and miss the rest. 'query_meta' filters and aggregates documents by their structured fields (dates, amounts, categories) \u2014 usually the right action for a question about spreadsheet data. 'compute' runs code over the spreadsheets for any figure DERIVED from them \u2014 a total, average, count, ranking, margin or comparison across rows \u2014 AND for finding the exact row matching one id or value. In a large table, search cannot reliably locate an individual row; compute can. When the question is about how things are CONNECTED rather than what a document says \u2014 who works with whom, what belongs to what, what a change touches \u2014 use the graph actions: 'get_neighbors' for what is one step from one thing, 'traverse' for everything within a few steps of it, 'find_related' for connections of a kind across the corpus, and 'community_summary' for the themes the corpus groups into. They are available only where a graph was built; discover says so. Searching with mode='graph' ranks passages by those same connections instead of by wording alone, which finds a passage that never repeats your words. Rules that decide answers: search returns EXCERPTS, and rows of a spreadsheet are not arithmetic \u2014 never add up, average or rank rows yourself from what search returned, and never say a figure is not available before computing over the sheet that holds it. A listing that says has_more has MORE: send its next_cursor back as 'cursor' for the next page, and never conclude a document is absent from a first page that was truncated. Search again with different words before saying a document is missing. A result may carry a next_action (or next_page): it is the call to make next, already filled in \u2014 follow it rather than guessing the next step. Cite document names.";
|
|
7772
8204
|
ACTION_PARAM_DESCRIPTION = "Match it to the task: reading questions -> search (a bare identifier for an ID or code); spreadsheet analysis, or the exact row for one id or value -> query_meta or compute; how things connect -> get_neighbors, traverse, find_related or community_summary; browse everything -> list; unsure what exists -> discover first.";
|
|
7773
8205
|
ACTION_HELP = {
|
|
7774
8206
|
search: "passages by meaning or keywords; an ID, code or number as the BARE identifier",
|
|
@@ -7791,7 +8223,7 @@ var init_knowledge_tool = __esm({
|
|
|
7791
8223
|
graph: "the graph actions are off: this deployment has no graph configured, so nothing has been linked up. Use search, get_doc or list instead."
|
|
7792
8224
|
};
|
|
7793
8225
|
GRAPH_ACTIONS = ["traverse", "find_related", "get_neighbors", "community_summary"];
|
|
7794
|
-
DOCUMENT_ID_ACTIONS = ["search", "compute", "get_docs", "map_reduce"];
|
|
8226
|
+
DOCUMENT_ID_ACTIONS = ["search", "compute", "get_docs", "map_reduce", "discover", "list"];
|
|
7795
8227
|
DEFAULT_GET_DOCS_CHARS = 2e5;
|
|
7796
8228
|
INPUT_PROPERTIES = {
|
|
7797
8229
|
action: { type: "string", enum: [...KNOWLEDGE_ACTIONS], description: ACTION_PARAM_DESCRIPTION },
|
|
@@ -7811,7 +8243,7 @@ var init_knowledge_tool = __esm({
|
|
|
7811
8243
|
document_ids: {
|
|
7812
8244
|
type: "array",
|
|
7813
8245
|
items: { type: "string" },
|
|
7814
|
-
description: "Narrows to these documents, using ids from discover, list or a search hit. It INTERSECTS with source_ids, so a document outside the sources you named returns nothing. (for search, compute)"
|
|
8246
|
+
description: "Narrows to these documents, using ids from discover, list or a search hit. It INTERSECTS with source_ids, so a document outside the sources you named returns nothing. On discover it also asks for those documents' structure IN FULL, past the shortening a whole-page discover applies. (for search, compute, get_docs, map_reduce, discover, list)"
|
|
7815
8247
|
},
|
|
7816
8248
|
entity: {
|
|
7817
8249
|
type: "string",
|
|
@@ -13820,6 +14252,7 @@ var init_engine = __esm({
|
|
|
13820
14252
|
pool,
|
|
13821
14253
|
sourceId: opts.sourceId,
|
|
13822
14254
|
sourceIds: opts.sourceIds,
|
|
14255
|
+
documentIds: opts.documentIds,
|
|
13823
14256
|
principals: resolvePrincipals(opts.principals, "listDocuments"),
|
|
13824
14257
|
cursor: opts.cursor,
|
|
13825
14258
|
limit: opts.limit,
|
|
@@ -13828,6 +14261,84 @@ var init_engine = __esm({
|
|
|
13828
14261
|
hooks: this.hooks
|
|
13829
14262
|
});
|
|
13830
14263
|
}
|
|
14264
|
+
/**
|
|
14265
|
+
* Sheet names, columns and row counts for the named spreadsheets.
|
|
14266
|
+
*
|
|
14267
|
+
* See `actions.spreadsheetSchema`. This is the half of `discover` that lets
|
|
14268
|
+
* a model write ONE `compute` call: sheet names here are the keys it will
|
|
14269
|
+
* index `dfs` by, and columns are the names it will use inside the code it
|
|
14270
|
+
* writes.
|
|
14271
|
+
*/
|
|
14272
|
+
async spreadsheetSchema(opts) {
|
|
14273
|
+
const pool = await this.ensurePool();
|
|
14274
|
+
return spreadsheetSchema({
|
|
14275
|
+
pool,
|
|
14276
|
+
documentIds: opts.documentIds,
|
|
14277
|
+
sourceIds: opts.sourceIds,
|
|
14278
|
+
principals: resolvePrincipals(opts.principals, "spreadsheetSchema"),
|
|
14279
|
+
redaction: opts.redaction ?? this.config.redaction,
|
|
14280
|
+
secretKey: this.config.secretKey,
|
|
14281
|
+
hooks: this.hooks
|
|
14282
|
+
});
|
|
14283
|
+
}
|
|
14284
|
+
/**
|
|
14285
|
+
* What is inside each of the named documents, whatever its type.
|
|
14286
|
+
*
|
|
14287
|
+
* See `actions.documentStructure`. Sheets and columns for a workbook,
|
|
14288
|
+
* sections and the last page for a document with headings, top-level keys
|
|
14289
|
+
* for JSON, and a chunk count for everything — so `discover` describes the
|
|
14290
|
+
* whole corpus rather than only the spreadsheets in it.
|
|
14291
|
+
*/
|
|
14292
|
+
async documentStructure(opts) {
|
|
14293
|
+
const pool = await this.ensurePool();
|
|
14294
|
+
return documentStructure({
|
|
14295
|
+
pool,
|
|
14296
|
+
documentIds: opts.documentIds,
|
|
14297
|
+
sourceIds: opts.sourceIds,
|
|
14298
|
+
bounded: opts.bounded,
|
|
14299
|
+
principals: resolvePrincipals(opts.principals, "documentStructure"),
|
|
14300
|
+
redaction: opts.redaction ?? this.config.redaction,
|
|
14301
|
+
secretKey: this.config.secretKey,
|
|
14302
|
+
hooks: this.hooks
|
|
14303
|
+
});
|
|
14304
|
+
}
|
|
14305
|
+
/**
|
|
14306
|
+
* The corpus census — `[{kind, type, documents, with_fields}]`.
|
|
14307
|
+
*
|
|
14308
|
+
* See `actions.documentTypes`. `kind` comes from the mime and is always
|
|
14309
|
+
* known; `type` is the LLM-written document type and exists only where
|
|
14310
|
+
* structured extraction was opted into.
|
|
14311
|
+
*/
|
|
14312
|
+
async documentTypes(opts = {}) {
|
|
14313
|
+
const pool = await this.ensurePool();
|
|
14314
|
+
return documentTypes({
|
|
14315
|
+
pool,
|
|
14316
|
+
sourceIds: opts.sourceIds,
|
|
14317
|
+
documentIds: opts.documentIds,
|
|
14318
|
+
principals: resolvePrincipals(opts.principals, "documentTypes"),
|
|
14319
|
+
redaction: opts.redaction ?? this.config.redaction,
|
|
14320
|
+
secretKey: this.config.secretKey,
|
|
14321
|
+
hooks: this.hooks
|
|
14322
|
+
});
|
|
14323
|
+
}
|
|
14324
|
+
/**
|
|
14325
|
+
* Extracted structured field names grouped by document kind and type.
|
|
14326
|
+
*
|
|
14327
|
+
* See `actions.fieldSummary`. One row per group, keyed by the same
|
|
14328
|
+
* `(kind, type)` pair `documentTypes` uses.
|
|
14329
|
+
*/
|
|
14330
|
+
async fieldSummary(opts = {}) {
|
|
14331
|
+
const pool = await this.ensurePool();
|
|
14332
|
+
return fieldSummary({
|
|
14333
|
+
pool,
|
|
14334
|
+
sourceIds: opts.sourceIds,
|
|
14335
|
+
documentIds: opts.documentIds,
|
|
14336
|
+
principals: resolvePrincipals(opts.principals, "fieldSummary"),
|
|
14337
|
+
redaction: opts.redaction ?? this.config.redaction,
|
|
14338
|
+
secretKey: this.config.secretKey,
|
|
14339
|
+
hooks: this.hooks
|
|
14340
|
+
});
|
|
14341
|
+
}
|
|
13831
14342
|
async queryStructured(question, opts = {}) {
|
|
13832
14343
|
const pool = await this.ensurePool();
|
|
13833
14344
|
return queryStructured(question, {
|