latticesql 1.2.6 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/cli.js +265 -3
- package/dist/index.cjs +269 -3
- package/dist/index.d.cts +205 -1
- package/dist/index.d.ts +205 -1
- package/dist/index.js +267 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ Every AI agent session starts cold — no memory of what happened yesterday, wha
|
|
|
18
18
|
2. **Watches** for DB changes and re-renders automatically
|
|
19
19
|
3. **Ingests** agent-written output back into the DB via the writeback pipeline
|
|
20
20
|
4. **Manages** state with full CRUD, natural-key operations, seeding, and soft-delete
|
|
21
|
+
5. **Optimizes** context with token budgets, relevance filtering, enrichment pipelines, and reward-scored memory
|
|
22
|
+
6. **Searches** semantically via bring-your-own embeddings and cosine similarity
|
|
21
23
|
|
|
22
24
|
Lattice has no opinions about your schema, your agents, or your file format. You define the tables. You control the rendering. Lattice runs the sync loop.
|
|
23
25
|
|
|
@@ -41,6 +43,9 @@ Lattice has no opinions about your schema, your agents, or your file format. You
|
|
|
41
43
|
- [Render, sync, watch, and reconcile](#render-sync-watch-and-reconcile)
|
|
42
44
|
- [Events](#events)
|
|
43
45
|
- [Raw DB access](#raw-db-access)
|
|
46
|
+
- [Context optimization](#context-optimization-v13)
|
|
47
|
+
- [Semantic search](#semantic-search-v13)
|
|
48
|
+
- [Writeback validation](#writeback-validation-v13)
|
|
44
49
|
- [Template rendering](#template-rendering)
|
|
45
50
|
- [Built-in templates](#built-in-templates)
|
|
46
51
|
- [Lifecycle hooks](#lifecycle-hooks)
|
|
@@ -1261,6 +1266,140 @@ const rows = db.db
|
|
|
1261
1266
|
|
|
1262
1267
|
---
|
|
1263
1268
|
|
|
1269
|
+
### Context optimization (v1.3+)
|
|
1270
|
+
|
|
1271
|
+
Lattice provides several options on `TableDefinition` to optimize what gets rendered into context files.
|
|
1272
|
+
|
|
1273
|
+
#### Token budget
|
|
1274
|
+
|
|
1275
|
+
Limit the token count of rendered output. When content exceeds the budget, rows are pruned by priority:
|
|
1276
|
+
|
|
1277
|
+
```typescript
|
|
1278
|
+
db.define('tickets', {
|
|
1279
|
+
columns: { id: 'TEXT PRIMARY KEY', title: 'TEXT', updated_at: 'TEXT' },
|
|
1280
|
+
render: (rows) => rows.map((r) => `- ${r.title}`).join('\n'),
|
|
1281
|
+
outputFile: 'TICKETS.md',
|
|
1282
|
+
tokenBudget: 4000, // max estimated tokens (~4 chars/token)
|
|
1283
|
+
prioritizeBy: 'updated_at', // keep most recent rows when pruning
|
|
1284
|
+
});
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
A truncation footer is appended: `[truncated: 47 of 123 rows rendered, ~3800 tokens]`
|
|
1288
|
+
|
|
1289
|
+
#### Relevance filtering
|
|
1290
|
+
|
|
1291
|
+
Dynamically filter rows based on a task context string:
|
|
1292
|
+
|
|
1293
|
+
```typescript
|
|
1294
|
+
db.define('knowledge', {
|
|
1295
|
+
columns: { id: 'TEXT PRIMARY KEY', topic: 'TEXT', body: 'TEXT' },
|
|
1296
|
+
render: (rows) => rows.map((r) => `## ${r.topic}\n${r.body}`).join('\n\n'),
|
|
1297
|
+
outputFile: 'KNOWLEDGE.md',
|
|
1298
|
+
relevanceFilter: (row, ctx) =>
|
|
1299
|
+
ctx ? String(row.body).toLowerCase().includes(ctx.toLowerCase()) : true,
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
// Set the current task context — only matching rows are rendered
|
|
1303
|
+
db.setTaskContext('deployment');
|
|
1304
|
+
await db.render('./context');
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
#### Enrichment pipeline
|
|
1308
|
+
|
|
1309
|
+
Transform rows between filtering and rendering — add computed fields, cluster, summarize:
|
|
1310
|
+
|
|
1311
|
+
```typescript
|
|
1312
|
+
db.define('incidents', {
|
|
1313
|
+
columns: { id: 'TEXT PRIMARY KEY', severity: 'TEXT', title: 'TEXT', created_at: 'TEXT' },
|
|
1314
|
+
render: (rows) => JSON.stringify(rows, null, 2),
|
|
1315
|
+
outputFile: 'incidents.json',
|
|
1316
|
+
enrich: [
|
|
1317
|
+
(rows) => rows.map((r) => ({
|
|
1318
|
+
...r,
|
|
1319
|
+
_age_hours: Math.round((Date.now() - new Date(r.created_at as string).getTime()) / 3600000),
|
|
1320
|
+
})),
|
|
1321
|
+
(rows) => rows.length > 100 ? [{ _summary: `${rows.length} incidents` }] : rows,
|
|
1322
|
+
],
|
|
1323
|
+
});
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
#### Reward-scored memory
|
|
1327
|
+
|
|
1328
|
+
Track which data is useful. High-reward rows are prioritized in rendering; low-scoring rows can be auto-pruned:
|
|
1329
|
+
|
|
1330
|
+
```typescript
|
|
1331
|
+
db.define('tips', {
|
|
1332
|
+
columns: { id: 'TEXT PRIMARY KEY', tip: 'TEXT', deleted_at: 'TEXT' },
|
|
1333
|
+
render: (rows) => rows.map((r) => `- ${r.tip}`).join('\n'),
|
|
1334
|
+
outputFile: 'TIPS.md',
|
|
1335
|
+
rewardTracking: true, // auto-adds _reward_total, _reward_count columns
|
|
1336
|
+
pruneBelow: 0.3, // soft-delete rows with reward < 0.3 (requires deleted_at column)
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
await db.init();
|
|
1340
|
+
const id = await db.insert('tips', { tip: 'Use batch inserts for bulk data' });
|
|
1341
|
+
|
|
1342
|
+
// After the agent confirms this tip was useful:
|
|
1343
|
+
await db.reward('tips', id, { relevance: 0.9, accuracy: 1.0 });
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
### Semantic search (v1.3+)
|
|
1347
|
+
|
|
1348
|
+
Enable embedding-based search on any table. Bring your own embedding function:
|
|
1349
|
+
|
|
1350
|
+
```typescript
|
|
1351
|
+
db.define('docs', {
|
|
1352
|
+
columns: { id: 'TEXT PRIMARY KEY', title: 'TEXT', body: 'TEXT' },
|
|
1353
|
+
render: (rows) => rows.map((r) => `## ${r.title}\n${r.body}`).join('\n\n---\n\n'),
|
|
1354
|
+
outputFile: 'DOCS.md',
|
|
1355
|
+
embeddings: {
|
|
1356
|
+
fields: ['title', 'body'],
|
|
1357
|
+
embed: async (text) => {
|
|
1358
|
+
const res = await openai.embeddings.create({ input: text, model: 'text-embedding-3-small' });
|
|
1359
|
+
return res.data[0].embedding;
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
await db.init();
|
|
1365
|
+
await db.insert('docs', { title: 'Deploy guide', body: 'How to deploy to production...' });
|
|
1366
|
+
|
|
1367
|
+
// Search by meaning, not keywords
|
|
1368
|
+
const results = await db.search('docs', 'ship to prod', { topK: 5, minScore: 0.7 });
|
|
1369
|
+
for (const { row, score } of results) {
|
|
1370
|
+
console.log(`${score.toFixed(2)} — ${row.title}`);
|
|
1371
|
+
}
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
Embeddings are stored in a companion SQLite table and cosine similarity is computed in JS — no external vector database required.
|
|
1375
|
+
|
|
1376
|
+
### Writeback validation (v1.3+)
|
|
1377
|
+
|
|
1378
|
+
Validate agent-written data before persisting. Reject low-quality or hallucinated writes:
|
|
1379
|
+
|
|
1380
|
+
```typescript
|
|
1381
|
+
db.defineWriteback({
|
|
1382
|
+
file: './agent-output/*.md',
|
|
1383
|
+
parse: (content, offset) => ({ entries: [content.slice(offset)], nextOffset: content.length }),
|
|
1384
|
+
persist: async (entry) => { /* save to DB */ },
|
|
1385
|
+
validate: async (entry) => {
|
|
1386
|
+
const text = entry as string;
|
|
1387
|
+
const hasRequiredFields = text.includes('## Title') && text.includes('## Body');
|
|
1388
|
+
return {
|
|
1389
|
+
pass: hasRequiredFields,
|
|
1390
|
+
score: hasRequiredFields ? 0.9 : 0.1,
|
|
1391
|
+
reason: hasRequiredFields ? undefined : 'Missing required sections',
|
|
1392
|
+
};
|
|
1393
|
+
},
|
|
1394
|
+
rejectBelow: 0.5,
|
|
1395
|
+
onReject: (entry, result) => {
|
|
1396
|
+
console.warn(`Rejected write: ${result.reason} (score: ${result.score})`);
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
---
|
|
1402
|
+
|
|
1264
1403
|
## Template rendering
|
|
1265
1404
|
|
|
1266
1405
|
### Built-in templates
|
package/dist/cli.js
CHANGED
|
@@ -646,6 +646,55 @@ var Sanitizer = class {
|
|
|
646
646
|
import { join as join5, basename, isAbsolute, resolve as resolve3 } from "path";
|
|
647
647
|
import { mkdirSync as mkdirSync3, existsSync as existsSync5, copyFileSync } from "fs";
|
|
648
648
|
|
|
649
|
+
// src/render/token-budget.ts
|
|
650
|
+
function estimateTokens(text) {
|
|
651
|
+
return Math.ceil(text.length / 4);
|
|
652
|
+
}
|
|
653
|
+
function applyTokenBudget(rows, renderFn, budget, prioritizeBy) {
|
|
654
|
+
const fullContent = renderFn(rows);
|
|
655
|
+
if (estimateTokens(fullContent) <= budget) return fullContent;
|
|
656
|
+
if (rows.length === 0) return fullContent;
|
|
657
|
+
const prioritized = [...rows];
|
|
658
|
+
if (typeof prioritizeBy === "function") {
|
|
659
|
+
prioritized.sort(prioritizeBy);
|
|
660
|
+
} else if (typeof prioritizeBy === "string") {
|
|
661
|
+
const col = prioritizeBy;
|
|
662
|
+
prioritized.sort((a, b) => {
|
|
663
|
+
const va = a[col];
|
|
664
|
+
const vb = b[col];
|
|
665
|
+
if (va == null && vb == null) return 0;
|
|
666
|
+
if (va == null) return 1;
|
|
667
|
+
if (vb == null) return -1;
|
|
668
|
+
if (va < vb) return 1;
|
|
669
|
+
if (va > vb) return -1;
|
|
670
|
+
return 0;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
let lo = 0;
|
|
674
|
+
let hi = prioritized.length;
|
|
675
|
+
let bestContent = "";
|
|
676
|
+
let bestCount = 0;
|
|
677
|
+
while (lo < hi) {
|
|
678
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
679
|
+
const content = renderFn(prioritized.slice(0, mid));
|
|
680
|
+
if (estimateTokens(content) <= budget) {
|
|
681
|
+
bestContent = content;
|
|
682
|
+
bestCount = mid;
|
|
683
|
+
lo = mid;
|
|
684
|
+
if (lo === hi) break;
|
|
685
|
+
} else {
|
|
686
|
+
hi = mid - 1;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (bestCount === 0) {
|
|
690
|
+
bestContent = renderFn([]);
|
|
691
|
+
}
|
|
692
|
+
const tokens = estimateTokens(bestContent);
|
|
693
|
+
return bestContent + `
|
|
694
|
+
|
|
695
|
+
[truncated: ${bestCount} of ${rows.length} rows rendered, ~${tokens} tokens]`;
|
|
696
|
+
}
|
|
697
|
+
|
|
649
698
|
// src/render/entity-query.ts
|
|
650
699
|
var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
|
|
651
700
|
function effectiveFilters(opts) {
|
|
@@ -1073,9 +1122,11 @@ function cleanupEntityContexts(outputDir, entityContexts, currentSlugsByTable, m
|
|
|
1073
1122
|
var RenderEngine = class {
|
|
1074
1123
|
_schema;
|
|
1075
1124
|
_adapter;
|
|
1076
|
-
|
|
1125
|
+
_getTaskContext;
|
|
1126
|
+
constructor(schema, adapter, getTaskContext) {
|
|
1077
1127
|
this._schema = schema;
|
|
1078
1128
|
this._adapter = adapter;
|
|
1129
|
+
this._getTaskContext = getTaskContext ?? (() => "");
|
|
1079
1130
|
}
|
|
1080
1131
|
async render(outputDir) {
|
|
1081
1132
|
const start = Date.now();
|
|
@@ -1083,8 +1134,38 @@ var RenderEngine = class {
|
|
|
1083
1134
|
const counters = { skipped: 0 };
|
|
1084
1135
|
for (const [name, def] of this._schema.getTables()) {
|
|
1085
1136
|
let rows = this._schema.queryTable(this._adapter, name);
|
|
1137
|
+
if (def.relevanceFilter) {
|
|
1138
|
+
const ctx = this._getTaskContext();
|
|
1139
|
+
rows = rows.filter((row) => def.relevanceFilter(row, ctx));
|
|
1140
|
+
}
|
|
1086
1141
|
if (def.filter) rows = def.filter(rows);
|
|
1087
|
-
|
|
1142
|
+
if (def.rewardTracking) {
|
|
1143
|
+
if (def.pruneBelow !== void 0) {
|
|
1144
|
+
const threshold = def.pruneBelow;
|
|
1145
|
+
const toPrune = rows.filter(
|
|
1146
|
+
(r) => r._reward_count > 0 && r._reward_total < threshold
|
|
1147
|
+
);
|
|
1148
|
+
if (toPrune.length > 0) {
|
|
1149
|
+
for (const r of toPrune) {
|
|
1150
|
+
const pkCol = this._schema.getPrimaryKey(name)[0] ?? "id";
|
|
1151
|
+
this._adapter.run(
|
|
1152
|
+
`UPDATE "${name}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`,
|
|
1153
|
+
[r[pkCol]]
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
rows = rows.filter(
|
|
1157
|
+
(r) => r._reward_count === 0 || r._reward_total >= threshold
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (!def.prioritizeBy) {
|
|
1162
|
+
rows.sort((a, b) => (b._reward_total ?? 0) - (a._reward_total ?? 0));
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (def.enrich) {
|
|
1166
|
+
for (const fn of def.enrich) rows = fn(rows);
|
|
1167
|
+
}
|
|
1168
|
+
const content = def.tokenBudget ? applyTokenBudget(rows, def.render, def.tokenBudget, def.prioritizeBy) : def.render(rows);
|
|
1088
1169
|
const filePath = join5(outputDir, def.outputFile);
|
|
1089
1170
|
if (atomicWrite(filePath, content)) {
|
|
1090
1171
|
filesWritten.push(filePath);
|
|
@@ -1488,6 +1569,14 @@ var WritebackPipeline = class {
|
|
|
1488
1569
|
if (store.isSeen(filePath, key)) continue;
|
|
1489
1570
|
store.markSeen(filePath, key);
|
|
1490
1571
|
}
|
|
1572
|
+
if (def.validate) {
|
|
1573
|
+
const result = await def.validate(entry);
|
|
1574
|
+
const threshold = def.rejectBelow ?? 0;
|
|
1575
|
+
if (!result.pass || result.score < threshold) {
|
|
1576
|
+
def.onReject?.(entry, result);
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1491
1580
|
await def.persist(entry, filePath);
|
|
1492
1581
|
processed++;
|
|
1493
1582
|
}
|
|
@@ -1666,6 +1755,73 @@ function resolveEncryptedColumns(encrypted, allColumns) {
|
|
|
1666
1755
|
return new Set(allColumns.filter((c) => !SKIP_COLUMNS.has(c)));
|
|
1667
1756
|
}
|
|
1668
1757
|
|
|
1758
|
+
// src/search/embeddings.ts
|
|
1759
|
+
var EMBEDDINGS_TABLE = "_lattice_embeddings";
|
|
1760
|
+
function ensureEmbeddingsTable(adapter) {
|
|
1761
|
+
adapter.run(`CREATE TABLE IF NOT EXISTS "${EMBEDDINGS_TABLE}" (
|
|
1762
|
+
"table_name" TEXT NOT NULL,
|
|
1763
|
+
"row_pk" TEXT NOT NULL,
|
|
1764
|
+
"embedding" TEXT NOT NULL,
|
|
1765
|
+
PRIMARY KEY ("table_name", "row_pk")
|
|
1766
|
+
)`);
|
|
1767
|
+
}
|
|
1768
|
+
async function storeEmbedding(adapter, table, pk, row, config) {
|
|
1769
|
+
const text = config.fields.map((f) => {
|
|
1770
|
+
const v = row[f];
|
|
1771
|
+
return v == null ? "" : String(v);
|
|
1772
|
+
}).filter((s) => s.length > 0).join(" ");
|
|
1773
|
+
if (text.length === 0) return;
|
|
1774
|
+
const vector = await config.embed(text);
|
|
1775
|
+
adapter.run(
|
|
1776
|
+
`INSERT OR REPLACE INTO "${EMBEDDINGS_TABLE}" ("table_name", "row_pk", "embedding") VALUES (?, ?, ?)`,
|
|
1777
|
+
[table, pk, JSON.stringify(vector)]
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
function removeEmbedding(adapter, table, pk) {
|
|
1781
|
+
adapter.run(
|
|
1782
|
+
`DELETE FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ? AND "row_pk" = ?`,
|
|
1783
|
+
[table, pk]
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
function cosineSimilarity(a, b) {
|
|
1787
|
+
const len = Math.min(a.length, b.length);
|
|
1788
|
+
let dot = 0;
|
|
1789
|
+
let magA = 0;
|
|
1790
|
+
let magB = 0;
|
|
1791
|
+
for (let i = 0; i < len; i++) {
|
|
1792
|
+
dot += a[i] * b[i];
|
|
1793
|
+
magA += a[i] * a[i];
|
|
1794
|
+
magB += b[i] * b[i];
|
|
1795
|
+
}
|
|
1796
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
1797
|
+
return denom === 0 ? 0 : dot / denom;
|
|
1798
|
+
}
|
|
1799
|
+
async function searchByEmbedding(adapter, table, queryText, config, topK, minScore, pkColumn = "id") {
|
|
1800
|
+
const queryVector = await config.embed(queryText);
|
|
1801
|
+
const stored = adapter.all(
|
|
1802
|
+
`SELECT "row_pk", "embedding" FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ?`,
|
|
1803
|
+
[table]
|
|
1804
|
+
);
|
|
1805
|
+
const scored = [];
|
|
1806
|
+
for (const entry of stored) {
|
|
1807
|
+
const vec = JSON.parse(entry.embedding);
|
|
1808
|
+
const score = cosineSimilarity(queryVector, vec);
|
|
1809
|
+
if (score >= minScore) {
|
|
1810
|
+
scored.push({ pk: entry.row_pk, score });
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1814
|
+
const topResults = scored.slice(0, topK);
|
|
1815
|
+
const results = [];
|
|
1816
|
+
for (const { pk, score } of topResults) {
|
|
1817
|
+
const row = adapter.get(`SELECT * FROM "${table}" WHERE "${pkColumn}" = ?`, [pk]);
|
|
1818
|
+
if (row) {
|
|
1819
|
+
results.push({ row, score });
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return results;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1669
1825
|
// src/lattice.ts
|
|
1670
1826
|
var Lattice = class {
|
|
1671
1827
|
_adapter;
|
|
@@ -1684,6 +1840,8 @@ var Lattice = class {
|
|
|
1684
1840
|
_encryptedTableColumns = /* @__PURE__ */ new Map();
|
|
1685
1841
|
/** Raw encryption key passphrase from constructor options. */
|
|
1686
1842
|
_encryptionKeyRaw;
|
|
1843
|
+
/** Current task context string for relevance filtering. */
|
|
1844
|
+
_taskContext = "";
|
|
1687
1845
|
_auditHandlers = [];
|
|
1688
1846
|
_renderHandlers = [];
|
|
1689
1847
|
_writebackHandlers = [];
|
|
@@ -1710,7 +1868,7 @@ var Lattice = class {
|
|
|
1710
1868
|
this._adapter = new SQLiteAdapter(dbPath, adapterOpts);
|
|
1711
1869
|
this._schema = new SchemaManager();
|
|
1712
1870
|
this._sanitizer = new Sanitizer(options.security);
|
|
1713
|
-
this._render = new RenderEngine(this._schema, this._adapter);
|
|
1871
|
+
this._render = new RenderEngine(this._schema, this._adapter, () => this._taskContext);
|
|
1714
1872
|
this._reverseSync = new ReverseSyncEngine(this._schema, this._adapter);
|
|
1715
1873
|
this._loop = new SyncLoop(this._render);
|
|
1716
1874
|
this._writeback = new WritebackPipeline();
|
|
@@ -1734,8 +1892,10 @@ var Lattice = class {
|
|
|
1734
1892
|
// -------------------------------------------------------------------------
|
|
1735
1893
|
define(table, def) {
|
|
1736
1894
|
this._assertNotInit("define");
|
|
1895
|
+
const columns = def.rewardTracking ? { ...def.columns, _reward_total: "REAL DEFAULT 0", _reward_count: "INTEGER DEFAULT 0" } : def.columns;
|
|
1737
1896
|
const compiledDef = {
|
|
1738
1897
|
...def,
|
|
1898
|
+
columns,
|
|
1739
1899
|
render: def.render ? compileRender(
|
|
1740
1900
|
def,
|
|
1741
1901
|
table,
|
|
@@ -1781,6 +1941,10 @@ var Lattice = class {
|
|
|
1781
1941
|
const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
|
|
1782
1942
|
this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
|
|
1783
1943
|
}
|
|
1944
|
+
const hasEmbeddings = [...this._schema.getTables().values()].some((d) => d.embeddings);
|
|
1945
|
+
if (hasEmbeddings) {
|
|
1946
|
+
ensureEmbeddingsTable(this._adapter);
|
|
1947
|
+
}
|
|
1784
1948
|
this._setupEncryption();
|
|
1785
1949
|
this._initialized = true;
|
|
1786
1950
|
return Promise.resolve();
|
|
@@ -1810,6 +1974,21 @@ var Lattice = class {
|
|
|
1810
1974
|
this._initialized = false;
|
|
1811
1975
|
}
|
|
1812
1976
|
// -------------------------------------------------------------------------
|
|
1977
|
+
// Task context (for relevance filtering)
|
|
1978
|
+
// -------------------------------------------------------------------------
|
|
1979
|
+
/**
|
|
1980
|
+
* Set the current task context string. Tables with a `relevanceFilter`
|
|
1981
|
+
* will use this value to filter rows before rendering.
|
|
1982
|
+
*/
|
|
1983
|
+
setTaskContext(context) {
|
|
1984
|
+
this._taskContext = context;
|
|
1985
|
+
return this;
|
|
1986
|
+
}
|
|
1987
|
+
/** Return the current task context string. */
|
|
1988
|
+
getTaskContext() {
|
|
1989
|
+
return this._taskContext;
|
|
1990
|
+
}
|
|
1991
|
+
// -------------------------------------------------------------------------
|
|
1813
1992
|
// Encryption helpers
|
|
1814
1993
|
// -------------------------------------------------------------------------
|
|
1815
1994
|
_setupEncryption() {
|
|
@@ -1884,6 +2063,7 @@ var Lattice = class {
|
|
|
1884
2063
|
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
1885
2064
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
1886
2065
|
this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
2066
|
+
this._syncEmbedding(table, "insert", rowWithPk, pkValue);
|
|
1887
2067
|
return Promise.resolve(pkValue);
|
|
1888
2068
|
}
|
|
1889
2069
|
/**
|
|
@@ -1951,6 +2131,11 @@ var Lattice = class {
|
|
|
1951
2131
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1952
2132
|
this._sanitizer.emitAudit(table, "update", auditId);
|
|
1953
2133
|
this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
2134
|
+
const def = this._schema.getTables().get(table);
|
|
2135
|
+
if (def?.embeddings) {
|
|
2136
|
+
const fullRow = this._adapter.get(`SELECT * FROM "${table}" WHERE ${clause}`, pkParams);
|
|
2137
|
+
if (fullRow) this._syncEmbedding(table, "update", fullRow, auditId);
|
|
2138
|
+
}
|
|
1954
2139
|
return Promise.resolve();
|
|
1955
2140
|
}
|
|
1956
2141
|
/**
|
|
@@ -1972,6 +2157,7 @@ var Lattice = class {
|
|
|
1972
2157
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1973
2158
|
this._sanitizer.emitAudit(table, "delete", auditId);
|
|
1974
2159
|
this._fireWriteHooks(table, "delete", { id: auditId }, auditId);
|
|
2160
|
+
this._syncEmbedding(table, "delete", {}, auditId);
|
|
1975
2161
|
return Promise.resolve();
|
|
1976
2162
|
}
|
|
1977
2163
|
get(table, id) {
|
|
@@ -2355,6 +2541,65 @@ var Lattice = class {
|
|
|
2355
2541
|
const ms = unit === "h" ? num * 36e5 : unit === "d" ? num * 864e5 : num * 6e4;
|
|
2356
2542
|
return new Date(Date.now() - ms).toISOString();
|
|
2357
2543
|
}
|
|
2544
|
+
// -------------------------------------------------------------------------
|
|
2545
|
+
// Reward tracking
|
|
2546
|
+
// -------------------------------------------------------------------------
|
|
2547
|
+
/**
|
|
2548
|
+
* Update reward scores for a row. The total reward is recalculated as
|
|
2549
|
+
* the running average across all reward calls. Requires `rewardTracking`
|
|
2550
|
+
* on the table definition.
|
|
2551
|
+
*/
|
|
2552
|
+
reward(table, id, scores) {
|
|
2553
|
+
const notInit = this._notInitError();
|
|
2554
|
+
if (notInit) return notInit;
|
|
2555
|
+
const def = this._schema.getTables().get(table);
|
|
2556
|
+
if (!def?.rewardTracking) {
|
|
2557
|
+
return Promise.reject(
|
|
2558
|
+
new Error(`Table "${table}" does not have rewardTracking enabled`)
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
const vals = Object.values(scores);
|
|
2562
|
+
if (vals.length === 0) return Promise.resolve();
|
|
2563
|
+
const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
2564
|
+
const { clause, params: pkParams } = this._pkWhere(table, id);
|
|
2565
|
+
this._adapter.run(
|
|
2566
|
+
`UPDATE "${table}" SET "_reward_total" = ("_reward_total" * "_reward_count" + ?) / ("_reward_count" + 1), "_reward_count" = "_reward_count" + 1 WHERE ${clause}`,
|
|
2567
|
+
[avg, ...pkParams]
|
|
2568
|
+
);
|
|
2569
|
+
return Promise.resolve();
|
|
2570
|
+
}
|
|
2571
|
+
// -------------------------------------------------------------------------
|
|
2572
|
+
// Semantic search
|
|
2573
|
+
// -------------------------------------------------------------------------
|
|
2574
|
+
/**
|
|
2575
|
+
* Search for rows by semantic similarity. Requires `embeddings` config
|
|
2576
|
+
* on the table definition.
|
|
2577
|
+
*
|
|
2578
|
+
* @param table - Table to search
|
|
2579
|
+
* @param query - Natural-language query text
|
|
2580
|
+
* @param opts - Search options (topK, minScore)
|
|
2581
|
+
* @returns Matching rows with similarity scores, sorted best-first.
|
|
2582
|
+
*/
|
|
2583
|
+
async search(table, query, opts = {}) {
|
|
2584
|
+
const notInit = this._notInitError();
|
|
2585
|
+
if (notInit) return notInit;
|
|
2586
|
+
const def = this._schema.getTables().get(table);
|
|
2587
|
+
if (!def?.embeddings) {
|
|
2588
|
+
return Promise.reject(
|
|
2589
|
+
new Error(`Table "${table}" does not have embeddings configured`)
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
const pkCol = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
2593
|
+
return searchByEmbedding(
|
|
2594
|
+
this._adapter,
|
|
2595
|
+
table,
|
|
2596
|
+
query,
|
|
2597
|
+
def.embeddings,
|
|
2598
|
+
opts.topK ?? 10,
|
|
2599
|
+
opts.minScore ?? 0,
|
|
2600
|
+
pkCol
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2358
2603
|
query(table, opts = {}) {
|
|
2359
2604
|
const notInit = this._notInitError();
|
|
2360
2605
|
if (notInit) return notInit;
|
|
@@ -2613,6 +2858,23 @@ var Lattice = class {
|
|
|
2613
2858
|
}
|
|
2614
2859
|
}
|
|
2615
2860
|
}
|
|
2861
|
+
/**
|
|
2862
|
+
* Update or remove the embedding for a row.
|
|
2863
|
+
* No-op if the table doesn't have `embeddings` configured.
|
|
2864
|
+
*/
|
|
2865
|
+
_syncEmbedding(table, op, row, pk) {
|
|
2866
|
+
const def = this._schema.getTables().get(table);
|
|
2867
|
+
if (!def?.embeddings) return;
|
|
2868
|
+
if (op === "delete") {
|
|
2869
|
+
removeEmbedding(this._adapter, table, pk);
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
storeEmbedding(this._adapter, table, pk, row, def.embeddings).catch((err) => {
|
|
2873
|
+
for (const h of this._errorHandlers) {
|
|
2874
|
+
h(err instanceof Error ? err : new Error(String(err)));
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2616
2878
|
_notInitError() {
|
|
2617
2879
|
if (!this._initialized) {
|
|
2618
2880
|
return Promise.reject(
|