tinywebui 0.1.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/LICENSE +661 -0
- package/README.md +586 -0
- package/bin/tinywebui.js +105 -0
- package/package.json +49 -0
- package/public/app.js +81 -0
- package/public/chat/attachments.js +304 -0
- package/public/chat/chat.js +282 -0
- package/public/chat/composer.js +289 -0
- package/public/chat/library.js +76 -0
- package/public/chat/outline.js +39 -0
- package/public/chat/queue.js +124 -0
- package/public/chat/sidebar.js +485 -0
- package/public/chat/stream.js +162 -0
- package/public/chat/tasks.js +36 -0
- package/public/chat/transcript.js +691 -0
- package/public/core/access.js +42 -0
- package/public/core/api.js +27 -0
- package/public/core/dom.js +13 -0
- package/public/core/id.js +7 -0
- package/public/core/md.js +131 -0
- package/public/core/state.js +31 -0
- package/public/favicon.svg +9 -0
- package/public/index.html +261 -0
- package/public/panels/admin.js +199 -0
- package/public/panels/automations.js +464 -0
- package/public/panels/panels.js +52 -0
- package/public/panels/settings.js +365 -0
- package/public/panels/usage.js +474 -0
- package/public/shared/attachment_note.js +23 -0
- package/public/shared/events.js +27 -0
- package/public/shared/features.js +21 -0
- package/public/shared/origins.js +8 -0
- package/public/shared/usage.js +59 -0
- package/public/styles.css +1455 -0
- package/public/themes/README.md +178 -0
- package/public/themes/cyber-grid/cyber-grid.css +59 -0
- package/public/themes/drivers/scene-kit.js +422 -0
- package/public/themes/fall-fairy/fall-fairy.css +221 -0
- package/public/themes/fall-fairy/fall-fairy.js +859 -0
- package/public/themes/manifest.json +6 -0
- package/public/themes/theme-loader.js +123 -0
- package/src/access/auth.js +228 -0
- package/src/access/auth_gate.js +67 -0
- package/src/access/policy.js +180 -0
- package/src/audit.js +7 -0
- package/src/automations/automation.js +136 -0
- package/src/automations/scheduler.js +147 -0
- package/src/chat/compact.js +242 -0
- package/src/chat/llm.js +448 -0
- package/src/chat/runs.js +180 -0
- package/src/chat/tool_executor.js +239 -0
- package/src/cli.js +478 -0
- package/src/config/approval.js +54 -0
- package/src/config/config.js +479 -0
- package/src/config/models.js +112 -0
- package/src/files/documents.js +91 -0
- package/src/files/images.js +62 -0
- package/src/harness/agent.js +115 -0
- package/src/harness/attribution.js +75 -0
- package/src/harness/checkpoint.js +96 -0
- package/src/harness/history.js +40 -0
- package/src/harness/prompt.js +93 -0
- package/src/harness/provider.js +430 -0
- package/src/http.js +95 -0
- package/src/log.js +24 -0
- package/src/mcp.js +405 -0
- package/src/retrieval/embedding.js +298 -0
- package/src/retrieval/retrieval.js +250 -0
- package/src/routes/admin.js +112 -0
- package/src/routes/auth.js +97 -0
- package/src/routes/automations.js +92 -0
- package/src/routes/chats.js +232 -0
- package/src/routes/documents.js +133 -0
- package/src/routes/library.js +51 -0
- package/src/routes/mcp.js +59 -0
- package/src/routes/settings.js +101 -0
- package/src/server.js +397 -0
- package/src/set-password.js +32 -0
- package/src/store/automations.js +119 -0
- package/src/store/chats.js +254 -0
- package/src/store/documents.js +256 -0
- package/src/store/embeddings.js +67 -0
- package/src/store/index.js +90 -0
- package/src/store/messages.js +93 -0
- package/src/store/schema.js +480 -0
- package/src/store/scope.js +43 -0
- package/src/store/search.js +56 -0
- package/src/store/turns.js +78 -0
- package/src/store/usage.js +327 -0
- package/src/store/users.js +102 -0
- package/src/store/wire.js +55 -0
- package/src/tools/ask_tool.js +93 -0
- package/src/tools/chat_search_tool.js +63 -0
- package/src/tools/context_tool.js +183 -0
- package/src/tools/document_tool.js +102 -0
- package/src/tools/task_tool.js +63 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Chunk vectors for every retrieval corpus (see retrieval/retrieval.js).
|
|
2
|
+
// One area of the Store; see index.js.
|
|
3
|
+
|
|
4
|
+
/** The retrieval corpora whose vectors live here (see retrieval/retrieval.js). */
|
|
5
|
+
export const CORPUS = Object.freeze({ DOCUMENTS: 'documents', CHATS: 'chats' });
|
|
6
|
+
|
|
7
|
+
export class EmbeddingStore {
|
|
8
|
+
constructor(db, deps = {}) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
Object.assign(this, deps);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Chunk vectors of the given units under one model: Map(unit id -> Float32Array[], one per chunk). */
|
|
14
|
+
vectors(corpus, modelKey, unitIds) {
|
|
15
|
+
const out = new Map();
|
|
16
|
+
if (!unitIds.length) return out;
|
|
17
|
+
const rows = this.db.prepare(`
|
|
18
|
+
SELECT unit_id, vec FROM embeddings
|
|
19
|
+
WHERE corpus = ? AND model_key = ? AND unit_id IN (SELECT value FROM json_each(?))
|
|
20
|
+
ORDER BY unit_id, chunk
|
|
21
|
+
`).all(corpus, modelKey, JSON.stringify(unitIds));
|
|
22
|
+
for (const r of rows) {
|
|
23
|
+
const buf = r.vec;
|
|
24
|
+
if (!out.has(r.unit_id)) out.set(r.unit_id, []);
|
|
25
|
+
out.get(r.unit_id).push(new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4).slice());
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What each of an owner's units was embedded from: Map(unit id -> digest). */
|
|
31
|
+
digests(corpus, ownerId, modelKey) {
|
|
32
|
+
const rows = this.db.prepare('SELECT unit_id, digest FROM embeddings WHERE corpus = ? AND owner_id = ? AND model_key = ? AND chunk = 0')
|
|
33
|
+
.all(corpus, ownerId, modelKey);
|
|
34
|
+
return new Map(rows.map((r) => [r.unit_id, r.digest]));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** rows: [{ unitId, digest, vecs: Float32Array[] (its chunks, in order) }], replacing what those units had. */
|
|
38
|
+
put(corpus, ownerId, modelKey, rows) {
|
|
39
|
+
const clear = this.db.prepare('DELETE FROM embeddings WHERE corpus = ? AND unit_id = ? AND model_key = ?');
|
|
40
|
+
const put = this.db.prepare('INSERT INTO embeddings (corpus, unit_id, owner_id, model_key, digest, chunk, vec) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
41
|
+
this.db.exec('BEGIN');
|
|
42
|
+
try {
|
|
43
|
+
for (const { unitId, digest, vecs } of rows) {
|
|
44
|
+
clear.run(corpus, unitId, modelKey);
|
|
45
|
+
vecs.forEach((vec, chunk) => put.run(corpus, unitId, ownerId, modelKey, digest, chunk, Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength)));
|
|
46
|
+
}
|
|
47
|
+
this.db.exec('COMMIT');
|
|
48
|
+
} catch (err) { this.db.exec('ROLLBACK'); throw err; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Drops an owner's vectors for units it no longer has. */
|
|
52
|
+
dropStale(corpus, ownerId, keepUnitIds) {
|
|
53
|
+
return this.db.prepare(`
|
|
54
|
+
DELETE FROM embeddings WHERE corpus = ? AND owner_id = ? AND unit_id NOT IN (SELECT value FROM json_each(?))
|
|
55
|
+
`).run(corpus, ownerId, JSON.stringify(keepUnitIds)).changes;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Drops vectors from models (or chunkings) no longer configured. */
|
|
59
|
+
prune(keepModelKey) {
|
|
60
|
+
return this.db.prepare('DELETE FROM embeddings WHERE model_key != ?').run(keepModelKey).changes;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Drops every vector an owner (a document, a chat) has in a corpus. */
|
|
64
|
+
deleteOwner(corpus, ownerId) {
|
|
65
|
+
this.db.prepare('DELETE FROM embeddings WHERE corpus = ? AND owner_id = ?').run(corpus, ownerId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { SCHEMA_VERSION, migrate as migrateSchema } from './schema.js';
|
|
5
|
+
import { ChatStore } from './chats.js';
|
|
6
|
+
import { MessageStore } from './messages.js';
|
|
7
|
+
import { AutomationStore } from './automations.js';
|
|
8
|
+
import { UsageStore } from './usage.js';
|
|
9
|
+
import { DocumentStore } from './documents.js';
|
|
10
|
+
import { EmbeddingStore } from './embeddings.js';
|
|
11
|
+
import { TurnStore } from './turns.js';
|
|
12
|
+
import { SearchStore } from './search.js';
|
|
13
|
+
import { UserStore } from './users.js';
|
|
14
|
+
|
|
15
|
+
export { SCHEMA_VERSION } from './schema.js';
|
|
16
|
+
export { ALL_USERS } from './scope.js';
|
|
17
|
+
export { PASSAGE_SIZE, PASSAGE_OVERLAP } from './documents.js';
|
|
18
|
+
export { toWire, toView } from './wire.js';
|
|
19
|
+
export { CORPUS } from './embeddings.js';
|
|
20
|
+
export { AUX_KIND } from './usage.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Conversation storage.
|
|
24
|
+
*
|
|
25
|
+
* The client used to hold the whole transcript and POST it back every turn,
|
|
26
|
+
* which meant every tool result -- including page-sized ones -- crossed the wire
|
|
27
|
+
* again on each message and eventually blew the localStorage quota. State lives
|
|
28
|
+
* here now, and the client sends only the new user message.
|
|
29
|
+
*
|
|
30
|
+
* Two things this file deliberately does NOT do: it never parses tool output,
|
|
31
|
+
* and it never special-cases a tool name. A tool result is an opaque blob from
|
|
32
|
+
* an arbitrary MCP server. Everything downstream (compaction, expansion) holds
|
|
33
|
+
* to the same rule.
|
|
34
|
+
*
|
|
35
|
+
* One connection, one object per area. Every call names its area --
|
|
36
|
+
* `store.chats.list()`, `store.documents.add()` -- and each area lives in the
|
|
37
|
+
* file of the same name next to this one. An area that needs another is
|
|
38
|
+
* handed it here, so every dependency between areas is listed in one place.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** Thrown when a database is behind and migrating was not allowed. */
|
|
42
|
+
export class MigrationRequiredError extends Error {}
|
|
43
|
+
|
|
44
|
+
export class Store {
|
|
45
|
+
/**
|
|
46
|
+
* `migrate: false` refuses to touch an existing database that is behind
|
|
47
|
+
* SCHEMA_VERSION instead of upgrading it on open; a brand-new file is
|
|
48
|
+
* always created at the current version.
|
|
49
|
+
*/
|
|
50
|
+
constructor(path, { migrate = true } = {}) {
|
|
51
|
+
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
|
|
52
|
+
this.db = new DatabaseSync(path);
|
|
53
|
+
const found = this.db.prepare('PRAGMA user_version').get().user_version;
|
|
54
|
+
const fresh = !this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='chats'").get();
|
|
55
|
+
if (!fresh && found > SCHEMA_VERSION) {
|
|
56
|
+
this.db.close();
|
|
57
|
+
throw new MigrationRequiredError(`database is at schema ${found}, newer than this TinyWebUI (${SCHEMA_VERSION}); upgrade TinyWebUI`);
|
|
58
|
+
}
|
|
59
|
+
if (!fresh && found < SCHEMA_VERSION && !migrate) {
|
|
60
|
+
this.db.close();
|
|
61
|
+
throw new MigrationRequiredError(`database is at schema ${found}, needs ${SCHEMA_VERSION}: run \`tinywebui migrate\``);
|
|
62
|
+
}
|
|
63
|
+
this.migratedFrom = fresh ? null : found;
|
|
64
|
+
migrateSchema(this.db, { fresh });
|
|
65
|
+
|
|
66
|
+
const { db } = this;
|
|
67
|
+
this.embeddings = new EmbeddingStore(db);
|
|
68
|
+
this.messages = new MessageStore(db);
|
|
69
|
+
this.automations = new AutomationStore(db);
|
|
70
|
+
this.documents = new DocumentStore(db, { embeddings: this.embeddings });
|
|
71
|
+
this.chats = new ChatStore(db, { messages: this.messages, automations: this.automations, documents: this.documents, embeddings: this.embeddings });
|
|
72
|
+
this.turns = new TurnStore(db, { chats: this.chats });
|
|
73
|
+
this.usage = new UsageStore(db);
|
|
74
|
+
this.search = new SearchStore(db);
|
|
75
|
+
this.users = new UserStore(db);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
schemaVersion() {
|
|
79
|
+
return this.db.prepare('PRAGMA user_version').get().user_version;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Cheap liveness probe of the database, for /readyz. */
|
|
83
|
+
ping() {
|
|
84
|
+
return this.db.prepare('SELECT 1 AS ok').get().ok === 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
close() {
|
|
88
|
+
try { this.db.close(); } catch { /* already gone */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// The transcript: messages in order, and the tool-result artifacts behind them.
|
|
4
|
+
// One area of the Store; see index.js.
|
|
5
|
+
|
|
6
|
+
export class MessageStore {
|
|
7
|
+
constructor(db, deps = {}) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
Object.assign(this, deps);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
nextSeq(chatId) {
|
|
13
|
+
const row = this.db.prepare('SELECT MAX(seq) AS m FROM messages WHERE chat_id = ?').get(chatId);
|
|
14
|
+
return (row?.m ?? -1) + 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Drops a chat's messages from `seq` on (a rewind). Returns how many went. */
|
|
18
|
+
deleteFrom(chatId, seq) {
|
|
19
|
+
return this.db.prepare('DELETE FROM messages WHERE chat_id = ? AND seq >= ?').run(chatId, seq).changes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Drops a chat's whole transcript and the artifacts behind it (the chat is going). */
|
|
23
|
+
deleteForChat(chatId) {
|
|
24
|
+
this.db.prepare('DELETE FROM messages WHERE chat_id = ?').run(chatId);
|
|
25
|
+
this.db.prepare('DELETE FROM artifacts WHERE chat_id = ?').run(chatId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Appends one message. Append-only by design: rows are never reordered. */
|
|
29
|
+
add(chatId, msg) {
|
|
30
|
+
const seq = this.nextSeq(chatId);
|
|
31
|
+
this.db.prepare(`
|
|
32
|
+
INSERT INTO messages
|
|
33
|
+
(chat_id, seq, role, content, tool_call_id, tool_calls_json,
|
|
34
|
+
reasoning, reasoning_details_json, artifact_id, stub_text, usage_json,
|
|
35
|
+
model, created_at, images_json, origin_json)
|
|
36
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
37
|
+
`).run(
|
|
38
|
+
chatId, seq, msg.role,
|
|
39
|
+
msg.content ?? null,
|
|
40
|
+
msg.tool_call_id ?? null,
|
|
41
|
+
msg.tool_calls ? JSON.stringify(msg.tool_calls) : null,
|
|
42
|
+
msg.reasoning ?? null,
|
|
43
|
+
msg.reasoning_details ? JSON.stringify(msg.reasoning_details) : null,
|
|
44
|
+
msg.artifact_id ?? null,
|
|
45
|
+
msg.stub_text ?? null,
|
|
46
|
+
msg.usage ? JSON.stringify(msg.usage) : null,
|
|
47
|
+
msg.model ?? null,
|
|
48
|
+
Date.now(),
|
|
49
|
+
msg.images ? JSON.stringify(msg.images) : null,
|
|
50
|
+
msg.origin ? JSON.stringify(msg.origin) : null
|
|
51
|
+
);
|
|
52
|
+
return seq;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
updateUsage(chatId, seq, usage) {
|
|
56
|
+
this.db.prepare('UPDATE messages SET usage_json = ? WHERE chat_id = ? AND seq = ?')
|
|
57
|
+
.run(JSON.stringify(usage), chatId, seq);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
list(chatId) {
|
|
61
|
+
return this.db.prepare(
|
|
62
|
+
'SELECT * FROM messages WHERE chat_id = ? ORDER BY seq ASC'
|
|
63
|
+
).all(chatId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Writes a stub onto an already-stored tool message. This is the ONE place
|
|
68
|
+
* history is rewritten, and it happens only at an epoch boundary. `content`
|
|
69
|
+
* is left untouched so the human transcript keeps the full output -- only
|
|
70
|
+
* what goes to the model shrinks.
|
|
71
|
+
*/
|
|
72
|
+
setStub(messageId, stubText) {
|
|
73
|
+
this.db.prepare('UPDATE messages SET stub_text = ? WHERE id = ?').run(stubText, messageId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Takes a message's images off the wire at an epoch; the transcript keeps them. */
|
|
77
|
+
dropImages(messageId) {
|
|
78
|
+
this.db.prepare('UPDATE messages SET images_dropped = 1 WHERE id = ?').run(messageId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
addArtifact(chatId, { toolName, args, content }) {
|
|
82
|
+
const id = randomBytes(12).toString('hex');
|
|
83
|
+
this.db.prepare(`
|
|
84
|
+
INSERT INTO artifacts (id, chat_id, tool_name, args_json, content, char_len, created_at)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
86
|
+
`).run(id, chatId, toolName, JSON.stringify(args ?? {}), content, content.length, Date.now());
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getArtifact(id, chatId) {
|
|
91
|
+
return this.db.prepare('SELECT * FROM artifacts WHERE id = ? AND chat_id = ?').get(id, chatId) || null;
|
|
92
|
+
}
|
|
93
|
+
}
|