@wirecat/cli-core 0.431.0 → 0.432.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/CHANGELOG.md +6 -1
- package/dist/cli/messenger/conversation-answers.d.ts +1 -0
- package/dist/cli/messenger/conversations-command.js +11 -3
- package/dist/cli/messenger/message-answers.d.ts +1 -0
- package/dist/cli/messenger/message-writes.d.ts +2 -0
- package/dist/cli/messenger/messages-search-command.js +11 -1
- package/dist/cli/messenger/search-answers.js +13 -0
- package/dist/cli/steps.js +6 -0
- package/dist/entities/conversation-preparation.d.ts +10 -0
- package/dist/entities/conversation-preparation.js +2 -0
- package/dist/entities/conversation-results.d.ts +10 -0
- package/dist/entities/evidence.d.ts +8 -0
- package/dist/entities/index.d.ts +45 -0
- package/dist/entities/listener.d.ts +4 -0
- package/dist/entities/message-context.d.ts +3 -0
- package/dist/entities/message-transfer.d.ts +8 -0
- package/dist/entities/message-write.d.ts +4 -0
- package/dist/entities/message.d.ts +2 -0
- package/dist/entities/message.js +2 -0
- package/dist/entities/store-records.d.ts +5 -0
- package/dist/services/embeddings-maintenance.js +71 -2
- package/dist/services/embeddings-search.js +3 -1
- package/dist/services/embeddings.d.ts +7 -0
- package/dist/services/messages-archive-search.d.ts +2 -1
- package/dist/services/messages-archive-search.js +4 -4
- package/dist/services/messages-discovery.d.ts +24 -1
- package/dist/services/messages-discovery.js +61 -4
- package/dist/services/messages-meaning.d.ts +7 -0
- package/dist/services/messages-meaning.js +53 -0
- package/dist/services/messages-search.d.ts +5 -0
- package/dist/services/messages.d.ts +5 -0
- package/dist/services/messages.js +2 -1
- package/dist/services/search-catchup.js +1 -0
- package/dist/store/sqlite/message-chunks.d.ts +44 -0
- package/dist/store/sqlite/message-chunks.js +76 -0
- package/dist/store/sqlite/vectors.js +4 -1
- package/dist/store/vectors.d.ts +36 -0
- package/dist/store/vectors.js +54 -0
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { sql } from "./drizzle/core.js";
|
|
2
|
+
import { dot } from "./vectors.js";
|
|
3
|
+
/** Live messages with text of the account, by pk after `after`; the caller compares hashes. */
|
|
4
|
+
export const messagesToEmbed = ({ orm }, accountPk, { chatKey, model, after, limit }) => orm
|
|
5
|
+
.all(sql `SELECT m.id AS messagePk, m.text, m.sent_at AS sentAt, k.content_hash AS hash,
|
|
6
|
+
EXISTS (SELECT 1 FROM embeddings v WHERE v.model = ${model} AND v.content_hash = k.content_hash) AS embedded
|
|
7
|
+
FROM messages m
|
|
8
|
+
LEFT JOIN chunks k ON k.chunkable_type = 'message' AND k.chunkable_id = m.id AND k.position = 0
|
|
9
|
+
WHERE m.account_id = ${accountPk} AND m.deleted_at IS NULL AND trim(m.text) <> '' AND m.id > ${after}
|
|
10
|
+
${chatKey === undefined ? sql `` : sql `AND m.chat_id = ${chatKey}`}
|
|
11
|
+
ORDER BY m.id LIMIT ${limit}`)
|
|
12
|
+
.map((row) => ({ ...row, embedded: row.embedded === 1 }));
|
|
13
|
+
const dropUnshared = ({ orm }, hashes) => {
|
|
14
|
+
for (const hash of hashes)
|
|
15
|
+
orm.run(sql `DELETE FROM embeddings WHERE content_hash = ${hash}
|
|
16
|
+
AND NOT EXISTS (SELECT 1 FROM chunks WHERE content_hash = ${hash})`);
|
|
17
|
+
};
|
|
18
|
+
/** Makes each hash its message's chunk; the vectors are saved apart, by `saveVectors`. */
|
|
19
|
+
export const saveMessageChunks = (context, accountPk, chunks) => {
|
|
20
|
+
const { orm, now } = context;
|
|
21
|
+
const at = now();
|
|
22
|
+
const scope = orm.get(sql `SELECT scope FROM accounts WHERE id = ${accountPk}`)?.scope ?? null;
|
|
23
|
+
const old = new Set();
|
|
24
|
+
for (const { messagePk, hash, length, occurredAt } of chunks) {
|
|
25
|
+
const held = orm.get(sql `SELECT content_hash AS hash FROM chunks
|
|
26
|
+
WHERE chunkable_type = 'message' AND chunkable_id = ${messagePk} AND position = 0`);
|
|
27
|
+
if (held?.hash === hash)
|
|
28
|
+
continue;
|
|
29
|
+
if (held)
|
|
30
|
+
old.add(held.hash);
|
|
31
|
+
orm.run(sql `DELETE FROM chunks WHERE chunkable_type = 'message' AND chunkable_id = ${messagePk} AND position = 0`);
|
|
32
|
+
orm.run(sql `INSERT INTO chunks (chunkable_type, chunkable_id, position, start_offset, end_offset, content_hash, scope,
|
|
33
|
+
account_id, occurred_at, created_at, updated_at)
|
|
34
|
+
VALUES ('message', ${messagePk}, 0, 0, ${length}, ${hash}, ${scope}, ${accountPk}, ${occurredAt}, ${at}, ${at})`);
|
|
35
|
+
}
|
|
36
|
+
dropUnshared(context, old);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Drops the chunks of the account's messages that are gone, deleted or emptied, with the vectors nothing else
|
|
40
|
+
* uses: no trigger does it for messages, and a deleted message keeps no trace of its text.
|
|
41
|
+
*/
|
|
42
|
+
export const dropGoneMessageChunks = (context, accountPk) => {
|
|
43
|
+
const gone = sql `chunkable_type = 'message' AND account_id = ${accountPk} AND NOT EXISTS (SELECT 1 FROM messages m
|
|
44
|
+
WHERE m.id = chunkable_id AND m.deleted_at IS NULL AND trim(m.text) <> '')`;
|
|
45
|
+
const dropped = context.orm.all(sql `SELECT content_hash AS hash FROM chunks WHERE ${gone}`);
|
|
46
|
+
context.orm.run(sql `DELETE FROM chunks WHERE ${gone}`);
|
|
47
|
+
dropUnshared(context, dropped.map(({ hash }) => hash));
|
|
48
|
+
return dropped.length;
|
|
49
|
+
};
|
|
50
|
+
/** Whether any message of the account has a vector of `model`. */
|
|
51
|
+
export const hasMessageVectors = ({ orm }, accountPk, model) => orm.get(sql `SELECT 1 FROM chunks k JOIN embeddings v ON v.model = ${model} AND v.content_hash = k.content_hash
|
|
52
|
+
WHERE k.chunkable_type = 'message' AND k.account_id = ${accountPk} LIMIT 1`) !== undefined;
|
|
53
|
+
const SCAN_PAGE = 5_000;
|
|
54
|
+
/** The account's live messages nearest to `query` by their vector of `model`, best first: a scan of every vector. */
|
|
55
|
+
export const nearestMessages = ({ orm }, accountPk, { chatKey, model, since, before, limit, query, }) => {
|
|
56
|
+
const found = [];
|
|
57
|
+
let after = 0;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const rows = orm.all(sql `SELECT k.id, ch.external_id AS chatId, m.external_id AS messageId, v.vector
|
|
60
|
+
FROM chunks k CROSS JOIN messages m ON m.id = k.chunkable_id AND m.deleted_at IS NULL
|
|
61
|
+
JOIN chats ch ON ch.id = m.chat_id
|
|
62
|
+
JOIN embeddings v ON v.model = ${model} AND v.content_hash = k.content_hash
|
|
63
|
+
WHERE k.chunkable_type = 'message' AND k.position = 0 AND k.account_id = ${accountPk} AND k.id > ${after}
|
|
64
|
+
${chatKey === undefined ? sql `AND ch.searchable = 1` : sql `AND m.chat_id = ${chatKey}`}
|
|
65
|
+
${since === undefined ? sql `` : sql `AND m.sent_at >= ${since}`}
|
|
66
|
+
${before === undefined ? sql `` : sql `AND m.sent_at < ${before}`}
|
|
67
|
+
ORDER BY k.id LIMIT ${SCAN_PAGE}`);
|
|
68
|
+
for (const row of rows)
|
|
69
|
+
found.push({ chatId: row.chatId, messageId: row.messageId, score: dot(query, row.vector) });
|
|
70
|
+
const last = rows.at(-1);
|
|
71
|
+
if (!last || rows.length < SCAN_PAGE)
|
|
72
|
+
break;
|
|
73
|
+
after = last.id;
|
|
74
|
+
}
|
|
75
|
+
return found.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
76
|
+
};
|
|
@@ -144,7 +144,10 @@ export const clearVectors = ({ orm }, chatKey, model) => orm.all(sql `DELETE FRO
|
|
|
144
144
|
/** Rows read per step of a search, so a chat of any size is scanned in bounded memory. */
|
|
145
145
|
const SCAN_PAGE = 5_000;
|
|
146
146
|
export const dot = (query, blob) => {
|
|
147
|
-
|
|
147
|
+
// A view, not a copy: copying every blob cost more than the scoring itself (cli-testing vector-index).
|
|
148
|
+
const vector = blob.byteOffset % 4 === 0
|
|
149
|
+
? new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4)
|
|
150
|
+
: new Float32Array(blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength));
|
|
148
151
|
let sum = 0;
|
|
149
152
|
for (let index = 0; index < query.length; index++)
|
|
150
153
|
sum += query[index] * vector[index];
|
package/dist/store/vectors.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Id } from "../domain/models.js";
|
|
2
2
|
import { type MailStore } from "./sqlite/emails.js";
|
|
3
3
|
import * as imageChunks from "./sqlite/image-chunks.js";
|
|
4
|
+
import * as messageChunks from "./sqlite/message-chunks.js";
|
|
4
5
|
import type { StoreContext } from "./sqlite/open.js";
|
|
5
6
|
import type { ChunkToEmbed } from "./sqlite/vectors.js";
|
|
6
7
|
import type { AccountKey, ConversationHit, PictureInput, StoredReadiness } from "./types.js";
|
|
@@ -51,6 +52,41 @@ export interface AccountVectors {
|
|
|
51
52
|
messageId: Id;
|
|
52
53
|
score: number;
|
|
53
54
|
}[]>;
|
|
55
|
+
/** Live messages with text after pk `after`, with the chunk and vector each has; `chatId` narrows it. */
|
|
56
|
+
messagesToEmbed(key: AccountKey, options: {
|
|
57
|
+
chatId?: Id;
|
|
58
|
+
model: string;
|
|
59
|
+
after: number;
|
|
60
|
+
limit: number;
|
|
61
|
+
}): Promise<messageChunks.MessageToEmbed[]>;
|
|
62
|
+
/** Keeps each message's vector of `model` and makes its hash the message's chunk. */
|
|
63
|
+
saveMessageVectors(key: AccountKey, model: string, dims: number, messages: {
|
|
64
|
+
messagePk: number;
|
|
65
|
+
hash: string;
|
|
66
|
+
length: number;
|
|
67
|
+
occurredAt: number;
|
|
68
|
+
vector: Float32Array;
|
|
69
|
+
}[]): Promise<void>;
|
|
70
|
+
/** Drops the chunks of messages gone, deleted or emptied, and the vectors only they used; how many. */
|
|
71
|
+
dropGoneMessages(key: AccountKey): Promise<number>;
|
|
72
|
+
/** Whether any message of the account has a vector of `model`. */
|
|
73
|
+
hasMessageVectors(key: AccountKey, model: string): Promise<boolean>;
|
|
74
|
+
/**
|
|
75
|
+
* The messages nearest in meaning to `query` by their own vector of `model`, best first. The one place
|
|
76
|
+
* message vectors are read, so their storage can change behind it.
|
|
77
|
+
*/
|
|
78
|
+
nearestMessages(key: AccountKey, options: {
|
|
79
|
+
chatId?: Id;
|
|
80
|
+
model: string;
|
|
81
|
+
since?: string;
|
|
82
|
+
before?: string;
|
|
83
|
+
limit: number;
|
|
84
|
+
query: Float32Array;
|
|
85
|
+
}): Promise<{
|
|
86
|
+
chatId: Id;
|
|
87
|
+
messageId: Id;
|
|
88
|
+
score: number;
|
|
89
|
+
}[]>;
|
|
54
90
|
/** The current build's distinct chunks, and how many have a vector of `model`. */
|
|
55
91
|
vectorStatus(key: AccountKey, chatId: Id, model: string): Promise<{
|
|
56
92
|
chunks: number;
|
package/dist/store/vectors.js
CHANGED
|
@@ -2,6 +2,7 @@ import { keysOver } from "./keys.js";
|
|
|
2
2
|
import * as conversationQueries from "./sqlite/conversations.js";
|
|
3
3
|
import { threadConversation } from "./sqlite/emails.js";
|
|
4
4
|
import * as imageChunks from "./sqlite/image-chunks.js";
|
|
5
|
+
import * as messageChunks from "./sqlite/message-chunks.js";
|
|
5
6
|
import { write } from "./sqlite/transaction.js";
|
|
6
7
|
import * as vectors from "./sqlite/vectors.js";
|
|
7
8
|
export const accountVectorsOver = (context, mail) => {
|
|
@@ -93,6 +94,59 @@ export const accountVectorsOver = (context, mail) => {
|
|
|
93
94
|
position: imageChunks.PICTURE_POSITION,
|
|
94
95
|
});
|
|
95
96
|
},
|
|
97
|
+
messagesToEmbed: async (key, { chatId, model, after, limit }) => {
|
|
98
|
+
const accountPk = findAccountPk(key);
|
|
99
|
+
if (accountPk === undefined)
|
|
100
|
+
return [];
|
|
101
|
+
const chatKey = chatId === undefined ? undefined : chatKeyOf(key, chatId);
|
|
102
|
+
if (chatId !== undefined && chatKey === undefined)
|
|
103
|
+
return [];
|
|
104
|
+
return messageChunks.messagesToEmbed(context, accountPk, {
|
|
105
|
+
...(chatKey === undefined ? {} : { chatKey }),
|
|
106
|
+
model,
|
|
107
|
+
after,
|
|
108
|
+
limit,
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
saveMessageVectors: async (key, model, dims, messages) => {
|
|
112
|
+
const accountPk = findAccountPk(key);
|
|
113
|
+
if (accountPk === undefined)
|
|
114
|
+
return;
|
|
115
|
+
write(database, () => {
|
|
116
|
+
vectors.saveVectors(context, model, dims, messages.map(({ hash, vector }) => ({ hash, vector })));
|
|
117
|
+
messageChunks.saveMessageChunks(context, accountPk, messages);
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
dropGoneMessages: async (key) => {
|
|
121
|
+
const accountPk = findAccountPk(key);
|
|
122
|
+
if (accountPk === undefined)
|
|
123
|
+
return 0;
|
|
124
|
+
let dropped = 0;
|
|
125
|
+
write(database, () => {
|
|
126
|
+
dropped = messageChunks.dropGoneMessageChunks(context, accountPk);
|
|
127
|
+
});
|
|
128
|
+
return dropped;
|
|
129
|
+
},
|
|
130
|
+
hasMessageVectors: async (key, model) => {
|
|
131
|
+
const accountPk = findAccountPk(key);
|
|
132
|
+
return accountPk !== undefined && messageChunks.hasMessageVectors(context, accountPk, model);
|
|
133
|
+
},
|
|
134
|
+
nearestMessages: async (key, { chatId, model, since, before, limit, query }) => {
|
|
135
|
+
const accountPk = findAccountPk(key);
|
|
136
|
+
if (accountPk === undefined)
|
|
137
|
+
return [];
|
|
138
|
+
const chatKey = chatId === undefined ? undefined : chatKeyOf(key, chatId);
|
|
139
|
+
if (chatId !== undefined && chatKey === undefined)
|
|
140
|
+
return [];
|
|
141
|
+
return messageChunks.nearestMessages(context, accountPk, {
|
|
142
|
+
...(chatKey === undefined ? {} : { chatKey }),
|
|
143
|
+
...(since === undefined ? {} : { since: Date.parse(since) }),
|
|
144
|
+
...(before === undefined ? {} : { before: Date.parse(before) }),
|
|
145
|
+
model,
|
|
146
|
+
limit,
|
|
147
|
+
query,
|
|
148
|
+
});
|
|
149
|
+
},
|
|
96
150
|
vectorStatus: async (key, chatId, model) => {
|
|
97
151
|
const chatKey = chatKeyOf(key, chatId);
|
|
98
152
|
return chatKey === undefined ? { chunks: 0, embedded: 0 } : vectors.vectorStatus(context, chatKey, model);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.432.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|