tinywebui 0.1.3 → 0.1.5
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 +20 -13
- package/bin/tinywebui.js +69 -63
- package/package.json +25 -8
- package/public/app.js +40 -14
- package/public/chat/composer.js +1 -1
- package/public/chat/sidebar.js +8 -5
- package/public/index.html +88 -49
- package/public/panels/admin.js +40 -1
- package/public/panels/model-editor.js +336 -0
- package/public/panels/panels.js +37 -12
- package/public/panels/settings-home.js +125 -0
- package/public/panels/settings.js +124 -36
- package/public/shared/attachment_note.js +7 -1
- package/public/shared/events.js +1 -0
- package/public/shared/features.js +1 -0
- package/public/shared/origins.js +1 -0
- package/public/shared/usage.js +16 -1
- package/public/styles.css +83 -20
- package/src/access/auth.js +155 -155
- package/src/access/auth_gate.js +39 -38
- package/src/access/email.js +17 -0
- package/src/access/policy.js +123 -138
- package/src/audit.js +1 -1
- package/src/automations/automation.js +135 -123
- package/src/automations/scheduler.js +138 -127
- package/src/chat/compact.js +149 -156
- package/src/chat/llm.js +380 -382
- package/src/chat/runs.js +162 -151
- package/src/chat/tool_executor.js +229 -218
- package/src/cli.js +535 -433
- package/src/config/approval.js +35 -29
- package/src/config/config.js +546 -451
- package/src/config/data_dir.js +24 -0
- package/src/config/models.js +191 -115
- package/src/config/refs.js +32 -0
- package/src/config/secrets.js +153 -0
- package/src/files/documents.js +57 -68
- package/src/files/images.js +30 -31
- package/src/harness/agent.js +60 -60
- package/src/harness/attribution.js +66 -58
- package/src/harness/checkpoint.js +67 -62
- package/src/harness/history.js +28 -23
- package/src/harness/prompt.js +40 -57
- package/src/harness/provider.js +284 -283
- package/src/harness/types.js +1 -0
- package/src/http.js +98 -66
- package/src/log.js +11 -12
- package/src/mcp.js +398 -374
- package/src/retrieval/embedding.js +228 -240
- package/src/retrieval/retrieval.js +182 -173
- package/src/routes/admin.js +178 -138
- package/src/routes/auth.js +184 -100
- package/src/routes/automations.js +99 -82
- package/src/routes/chats.js +240 -218
- package/src/routes/documents.js +130 -120
- package/src/routes/library.js +43 -46
- package/src/routes/mcp.js +49 -49
- package/src/routes/settings.js +127 -122
- package/src/routes/types.js +1 -0
- package/src/server.js +428 -392
- package/src/store/automations.js +106 -106
- package/src/store/chats.js +217 -242
- package/src/store/documents.js +181 -185
- package/src/store/embeddings.js +52 -54
- package/src/store/index.js +95 -49
- package/src/store/messages.js +55 -80
- package/src/store/schema.js +128 -107
- package/src/store/scope.js +18 -17
- package/src/store/search.js +25 -27
- package/src/store/turns.js +53 -53
- package/src/store/usage.js +343 -269
- package/src/store/users.js +162 -87
- package/src/store/wire.js +50 -41
- package/src/tools/ask_tool.js +68 -68
- package/src/tools/chat_search_tool.js +38 -44
- package/src/tools/context_tool.js +131 -138
- package/src/tools/document_tool.js +73 -78
- package/src/tools/mcp_tool.js +274 -232
- package/src/tools/task_tool.js +53 -50
- package/src/tools/types.js +1 -0
- package/public/panels/connectors.js +0 -184
- package/src/set-password.js +0 -43
package/src/chat/runs.js
CHANGED
|
@@ -4,11 +4,8 @@ import { effectiveConfig } from '../config/models.js';
|
|
|
4
4
|
import { logger } from '../log.js';
|
|
5
5
|
import { EVENT } from '../../public/shared/events.js';
|
|
6
6
|
import { CORPUS } from '../store/index.js';
|
|
7
|
-
|
|
8
7
|
const log = logger('retrieval');
|
|
9
|
-
|
|
10
8
|
const RETAIN_MS = 5 * 60 * 1000;
|
|
11
|
-
|
|
12
9
|
/**
|
|
13
10
|
* Turns in flight, by chat id.
|
|
14
11
|
*
|
|
@@ -27,159 +24,173 @@ const RETAIN_MS = 5 * 60 * 1000;
|
|
|
27
24
|
* `onRunIdle(chatId)`, called once a chat has nothing running.
|
|
28
25
|
*/
|
|
29
26
|
export function createRuns(app) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return Boolean(run && !run.done);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** Tells everyone watching a chat's run what is queued now. */
|
|
40
|
-
const announceQueue = (chatId) => runs.get(chatId)?.emit({ type: EVENT.QUEUE, items: store.chats.listQueued(chatId) });
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* `lead` is a queued follow-up that starts this run: stored after baseCount
|
|
44
|
-
* and sent as an event, so a live view and a reload each show it once.
|
|
45
|
-
*/
|
|
46
|
-
function start({ chat, tools, onFinish, historyFromSeq = null, unattended = false, model = null, lead = null, instructions = null }) {
|
|
47
|
-
const run = {
|
|
48
|
-
events: [],
|
|
49
|
-
subs: new Set(),
|
|
50
|
-
ac: new AbortController(),
|
|
51
|
-
done: false,
|
|
52
|
-
// Where the store stood when the turn began, including the user message
|
|
53
|
-
// that started it. A client reopening mid-turn replays up to here and
|
|
54
|
-
// plays the events over the top.
|
|
55
|
-
baseCount: store.messages.list(chat.id).length,
|
|
56
|
-
// Tool calls waiting on the user, by call id -> { name, resolve }.
|
|
57
|
-
approvals: new Map(),
|
|
58
|
-
// The ask_user question waiting on the user: { id, settle }, or null.
|
|
59
|
-
question: null,
|
|
60
|
-
unattended
|
|
27
|
+
const { store } = app;
|
|
28
|
+
const runs = new Map();
|
|
29
|
+
const get = (chatId) => runs.get(chatId);
|
|
30
|
+
const isRunning = (chatId) => {
|
|
31
|
+
const run = runs.get(chatId);
|
|
32
|
+
return Boolean(run && !run.done);
|
|
61
33
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// instead of waiting forever on a prompt nobody will answer now.
|
|
65
|
-
run.ac.signal.addEventListener('abort', () => {
|
|
66
|
-
for (const { resolve } of run.approvals.values()) resolve('deny');
|
|
67
|
-
run.approvals.clear();
|
|
68
|
-
// Stop ends the run; the question is closed so no late answer revives it.
|
|
69
|
-
run.question?.settle('cancelled');
|
|
70
|
-
}, { once: true });
|
|
71
|
-
// Unattended runs get no asker: the loop refuses gated calls itself.
|
|
72
|
-
const approve = unattended ? null : ({ id, name }) =>
|
|
73
|
-
new Promise((resolve) => run.approvals.set(id, { name, resolve }));
|
|
74
|
-
|
|
34
|
+
/** Tells everyone watching a chat's run what is queued now. */
|
|
35
|
+
const announceQueue = (chatId) => runs.get(chatId)?.emit({ type: EVENT.QUEUE, items: store.chats.listQueued(chatId) });
|
|
75
36
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* settled it; the timer is read from the live config at ask time.
|
|
37
|
+
* `lead` is a queued follow-up that starts this run: stored after baseCount
|
|
38
|
+
* and sent as an event, so a live view and a reload each show it once.
|
|
79
39
|
*/
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
resolve(status === 'answered' ? { answered: true, answer } : { answered: false, reason: status });
|
|
97
|
-
return true;
|
|
40
|
+
function start({ chat, tools, onFinish, historyFromSeq = null, unattended = false, model = null, lead = null, instructions = null }) {
|
|
41
|
+
const run = {
|
|
42
|
+
events: [],
|
|
43
|
+
subs: new Set(),
|
|
44
|
+
ac: new AbortController(),
|
|
45
|
+
done: false,
|
|
46
|
+
// Where the store stood when the turn began, including the user message
|
|
47
|
+
// that started it. A client reopening mid-turn replays up to here and
|
|
48
|
+
// plays the events over the top.
|
|
49
|
+
baseCount: store.messages.list(chat.id).length,
|
|
50
|
+
// Tool calls waiting on the user, by call id -> { name, resolve }.
|
|
51
|
+
approvals: new Map(),
|
|
52
|
+
// The ask_user question waiting on the user: { id, settle }, or null.
|
|
53
|
+
question: null,
|
|
54
|
+
unattended,
|
|
55
|
+
emit
|
|
98
56
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
57
|
+
runs.set(chat.id, run);
|
|
58
|
+
// A stop settles every open question as "no", so the loop can unwind
|
|
59
|
+
// instead of waiting forever on a prompt nobody will answer now.
|
|
60
|
+
run.ac.signal.addEventListener('abort', () => {
|
|
61
|
+
for (const { resolve } of run.approvals.values())
|
|
62
|
+
resolve('deny');
|
|
63
|
+
run.approvals.clear();
|
|
64
|
+
// Stop ends the run; the question is closed so no late answer revives it.
|
|
65
|
+
run.question?.settle('cancelled');
|
|
66
|
+
}, { once: true });
|
|
67
|
+
// Unattended runs get no asker: the loop refuses gated calls itself.
|
|
68
|
+
const approve = unattended ? null : ({ id, name }) => new Promise((resolve) => run.approvals.set(id, { name, resolve }));
|
|
69
|
+
/**
|
|
70
|
+
* Puts one ask_user question to the user and waits: for an answer, a
|
|
71
|
+
* skip, the timeout, or a stop. Persisted first, so the row decides who
|
|
72
|
+
* settled it; the timer is read from the live config at ask time.
|
|
73
|
+
*/
|
|
74
|
+
const askUser = unattended ? null : ({ question, choices, allowFreeText }) => {
|
|
75
|
+
if (run.ac.signal.aborted)
|
|
76
|
+
return Promise.resolve({ answered: false, reason: 'cancelled' });
|
|
77
|
+
// Calls are sequential, so a second one can only arrive after the first
|
|
78
|
+
// settled; this is the guard should that ever change.
|
|
79
|
+
if (run.question)
|
|
80
|
+
return Promise.resolve({ answered: false, reason: 'busy' });
|
|
81
|
+
const id = randomUUID();
|
|
82
|
+
const seconds = Math.max(0, Number(app.cfg.askUserTimeoutSeconds) || 0);
|
|
83
|
+
const deadline = seconds ? Date.now() + seconds * 1000 : null;
|
|
84
|
+
store.chats.addQuestion(chat.id, { id, question, choices, allowFreeText, deadline });
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
let timer;
|
|
87
|
+
const settle = (status, answer = null) => {
|
|
88
|
+
if (!store.chats.settleQuestion(id, status, answer))
|
|
89
|
+
return false;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (run.question?.id === id)
|
|
92
|
+
run.question = null;
|
|
93
|
+
emit({ type: EVENT.QUESTION_DONE, id, status, answer });
|
|
94
|
+
resolve(status === 'answered' ? { answered: true, answer: answer } : { answered: false, reason: status });
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
run.question = { id, choices, allowFreeText, settle };
|
|
98
|
+
if (deadline)
|
|
99
|
+
timer = setTimeout(() => settle('timeout'), seconds * 1000);
|
|
100
|
+
emit({ type: EVENT.QUESTION, id, question, choices, allowFreeText, deadline, timeoutSeconds: seconds });
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
function emit(event) {
|
|
104
|
+
run.events.push(event);
|
|
105
|
+
for (const sub of run.subs) {
|
|
106
|
+
try {
|
|
107
|
+
sub.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
run.subs.delete(sub);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
emit({ type: EVENT.CHAT, id: chat.id, title: chat.title });
|
|
115
|
+
if (lead != null) {
|
|
116
|
+
store.messages.add(chat.id, { role: 'user', content: lead });
|
|
117
|
+
emit({ type: EVENT.USER, content: lead });
|
|
118
|
+
}
|
|
119
|
+
emit({ type: EVENT.QUEUE, items: store.chats.listQueued(chat.id) });
|
|
120
|
+
// Steering is interactive input: an unattended run never takes any, so
|
|
121
|
+
// it cannot swallow what someone typed into a chat an automation shares.
|
|
122
|
+
const takeInput = unattended ? null : () => {
|
|
123
|
+
const items = store.chats.takeQueued(chat.id, { kind: 'steer' });
|
|
124
|
+
if (items.length)
|
|
125
|
+
emit({ type: EVENT.QUEUE, items: store.chats.listQueued(chat.id) });
|
|
126
|
+
return items.map((i) => i.content);
|
|
127
|
+
};
|
|
128
|
+
// Not awaited: the run outlives the request that started it.
|
|
129
|
+
void (async () => {
|
|
130
|
+
try {
|
|
131
|
+
const base = effectiveConfig(app.cfg, model);
|
|
132
|
+
const cfg = instructions ? { ...base, systemPrompt: `${base.systemPrompt}
|
|
132
133
|
|
|
133
134
|
The user's own instructions:
|
|
134
135
|
${instructions}` } : base;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
run.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
136
|
+
await runChat({ cfg, chatId: chat.id, store, tools, hub: app.hub, emit, signal: run.ac.signal, historyFromSeq, unattended, approve, takeInput, askUser });
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
emit({ type: EVENT.ERROR, error: run.ac.signal.aborted ? 'Stopped.' : err.message });
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
store.chats.touch(chat.id);
|
|
143
|
+
run.done = true;
|
|
144
|
+
app.retrieval.ingest(CORPUS.CHATS, chat.id).catch((err) => log.error(`embedding chat ${chat.id} failed: ${err.message}`));
|
|
145
|
+
// The run would go idle here, so the oldest queued item starts the
|
|
146
|
+
// next one -- in this same synchronous block, so nothing queued before
|
|
147
|
+
// `done` flipped can be missed. A stop or failure delivers nothing:
|
|
148
|
+
// the queue stays put and the client hands it back to the composer.
|
|
149
|
+
const ok = !run.events.some((event) => event.type === EVENT.ERROR) && !run.ac.signal.aborted;
|
|
150
|
+
const next = ok && !unattended ? store.chats.takeQueued(chat.id, { first: true })[0] : null;
|
|
151
|
+
if (next) {
|
|
152
|
+
emit({ type: EVENT.NEXT_RUN });
|
|
153
|
+
start({ chat, tools, model, lead: next.content });
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
onFinish?.({
|
|
157
|
+
ok,
|
|
158
|
+
error: run.events.find((event) => event.type === EVENT.ERROR)?.error || (run.ac.signal.aborted ? 'Stopped.' : null),
|
|
159
|
+
result: store.messages.list(chat.id).slice(run.baseCount).filter((m) => m.role === 'assistant').at(-1)?.content || ''
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch { /* a run observer cannot disrupt chat teardown */ }
|
|
163
|
+
for (const sub of run.subs) {
|
|
164
|
+
try {
|
|
165
|
+
sub.end();
|
|
166
|
+
}
|
|
167
|
+
catch { /* already gone */ }
|
|
168
|
+
}
|
|
169
|
+
run.subs.clear();
|
|
170
|
+
// Held briefly so a client reconnecting a second later still gets the
|
|
171
|
+
// tail of the turn rather than a 404.
|
|
172
|
+
setTimeout(() => { if (runs.get(chat.id) === run)
|
|
173
|
+
runs.delete(chat.id); }, RETAIN_MS).unref();
|
|
174
|
+
queueMicrotask(() => app.onRunIdle(chat.id));
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
return run;
|
|
178
|
+
}
|
|
179
|
+
/** Points one response at a run: the backlog from `from`, then the live rest. */
|
|
180
|
+
function attach(run, res, from) {
|
|
181
|
+
res.writeHead(200, {
|
|
182
|
+
'content-type': 'text/event-stream',
|
|
183
|
+
'cache-control': 'no-cache',
|
|
184
|
+
connection: 'keep-alive'
|
|
185
|
+
});
|
|
186
|
+
for (const ev of run.events.slice(from))
|
|
187
|
+
res.write(`data: ${JSON.stringify(ev)}\n\n`);
|
|
188
|
+
if (run.done)
|
|
189
|
+
return res.end();
|
|
190
|
+
run.subs.add(res);
|
|
191
|
+
// A viewer leaving is just a viewer leaving. The run keeps going.
|
|
192
|
+
res.on('close', () => run.subs.delete(res));
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
return { get, isRunning, start, attach, announceQueue };
|
|
185
196
|
}
|