smart-home-engine 1.52.1 → 1.53.1
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/dist/web/assets/{index-ClJLf8-l.js → index-CBrAPut6.js} +130 -129
- package/dist/web/assets/index-x3Sn9IKo.css +1 -0
- package/dist/web/assets/{tsMode-B7KUL0-b.js → tsMode-CzP5eDeL.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/index.js +15 -0
- package/src/web/ai-api.js +28 -15
- package/src/web/ai-context.js +3 -2
- package/src/web/ai-sandbox-worker.js +84 -0
- package/src/web/ai-sandbox.js +63 -0
- package/src/web/ai-tools.js +608 -58
- package/src/web/prompts/compact.md +3 -3
- package/src/web/prompts/formats-notools.md +14 -0
- package/src/web/prompts/formats.md +2 -13
- package/src/web/prompts/tools.md +11 -4
- package/dist/web/assets/index-C5e-98I_.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-CBrAPut6.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
package/dist/web/index.html
CHANGED
|
@@ -175,10 +175,10 @@
|
|
|
175
175
|
}
|
|
176
176
|
})();
|
|
177
177
|
</script>
|
|
178
|
-
<script type="module" crossorigin src="/assets/index-
|
|
178
|
+
<script type="module" crossorigin src="/assets/index-CBrAPut6.js"></script>
|
|
179
179
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-DKJI_o9J.js">
|
|
180
180
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
181
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
181
|
+
<link rel="stylesheet" crossorigin href="/assets/index-x3Sn9IKo.css">
|
|
182
182
|
</head>
|
|
183
183
|
<body>
|
|
184
184
|
<div id="app"></div>
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -691,6 +691,9 @@ if (config.url) {
|
|
|
691
691
|
state.lc = state.val === oldState.val ? oldState.lc : ts;
|
|
692
692
|
store.setObject('mqtt::' + topic, state);
|
|
693
693
|
store.setObject('var::' + varName, state);
|
|
694
|
+
// dispatch now, as setVariable() does: the broker's echo of the status publish below finds the store
|
|
695
|
+
// already updated, so a { change: true } subscription would never see the change (B-22)
|
|
696
|
+
stateChange(topic, state, oldState, msg);
|
|
694
697
|
mqtt.publish(topic, JSON.stringify(state), { retain: true });
|
|
695
698
|
} else {
|
|
696
699
|
if (!state) {
|
|
@@ -701,6 +704,18 @@ if (config.url) {
|
|
|
701
704
|
state.ts = new Date().getTime();
|
|
702
705
|
}
|
|
703
706
|
oldState = store.getObject('mqtt::' + topic) || {};
|
|
707
|
+
// our own echo: she publishes <prefix>/status/<name> retained after a var/set or she.var.set(), and the
|
|
708
|
+
// broker delivers it back (we subscribe to #). The subscriptions were dispatched at set time; the
|
|
709
|
+
// echo carries the stored state verbatim (same ts and lc), so it is stored again but not dispatched.
|
|
710
|
+
if (
|
|
711
|
+
topicArr[0] === config.variablePrefix &&
|
|
712
|
+
topicArr[1] === 'status' &&
|
|
713
|
+
oldState.ts === state.ts &&
|
|
714
|
+
oldState.lc === state.lc &&
|
|
715
|
+
JSON.stringify(oldState.val) === JSON.stringify(state.val)
|
|
716
|
+
) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
704
719
|
// lc (last change): prefer a payload-provided value; otherwise set it
|
|
705
720
|
// on value change and carry the previous lc over when the value is
|
|
706
721
|
// unchanged. Previously the old lc was simply lost on repeated
|
package/src/web/ai-api.js
CHANGED
|
@@ -349,6 +349,23 @@ function answerText(content) {
|
|
|
349
349
|
* @returns {Promise<{ message: string, usage?: object }>}
|
|
350
350
|
*/
|
|
351
351
|
/** run a tool; a result of { text, event } sends the event to the chat and hands the text to the model */
|
|
352
|
+
/**
|
|
353
|
+
* The tool calls of one round, executed concurrently (roadmap I31): the model is invited to ask for every
|
|
354
|
+
* independent tool in the same turn, and a slow history query must not delay a topic lookup. The call events go
|
|
355
|
+
* out first, in the model's order; each result event goes out as its tool finishes; the contents come back in
|
|
356
|
+
* the model's order, so the tool_result blocks keep their ids.
|
|
357
|
+
*/
|
|
358
|
+
async function runToolCalls(calls, ctx, onEvent) {
|
|
359
|
+
for (const c of calls) onEvent?.({ type: 'tool_call', name: c.name, args: c.args });
|
|
360
|
+
return Promise.all(
|
|
361
|
+
calls.map(async (c) => {
|
|
362
|
+
const content = await runToolForChat(c.name, c.args, ctx, onEvent);
|
|
363
|
+
onEvent?.({ type: 'tool_result', name: c.name, content });
|
|
364
|
+
return content;
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
352
369
|
async function runToolForChat(name, args, ctx, onEvent) {
|
|
353
370
|
const out = await executeTool(name, args, ctx);
|
|
354
371
|
if (out && typeof out === 'object') {
|
|
@@ -420,14 +437,12 @@ async function resolveAndGetAnswer(ai, messages, toolContext, onEvent) {
|
|
|
420
437
|
if (isAnthropic) {
|
|
421
438
|
// the assistant turn goes back unchanged: the current models bind their thinking blocks to the turn
|
|
422
439
|
msgs = [...msgs, { role: 'assistant', content: result.assistantMsg }];
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
onEvent
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
toolResultBlocks.push({ type: 'tool_result', tool_use_id: tc.id, content });
|
|
430
|
-
}
|
|
440
|
+
const contents = await runToolCalls(
|
|
441
|
+
result.toolCalls.map((tc) => ({ name: tc.name, args: tc.input || {} })),
|
|
442
|
+
toolContext,
|
|
443
|
+
onEvent,
|
|
444
|
+
);
|
|
445
|
+
const toolResultBlocks = result.toolCalls.map((tc, i) => ({ type: 'tool_result', tool_use_id: tc.id, content: contents[i] }));
|
|
431
446
|
msgs = [...msgs, { role: 'user', content: toolResultBlocks }];
|
|
432
447
|
} else {
|
|
433
448
|
// Strip any draft content alongside tool_calls — if kept, the model reproduces
|
|
@@ -435,19 +450,17 @@ async function resolveAndGetAnswer(ai, messages, toolContext, onEvent) {
|
|
|
435
450
|
const assistantEntry = { ...result.assistantMsg, role: 'assistant' };
|
|
436
451
|
if (assistantEntry.content && assistantEntry.tool_calls?.length) assistantEntry.content = null;
|
|
437
452
|
msgs = [...msgs, assistantEntry];
|
|
438
|
-
|
|
439
|
-
const name = tc.function.name;
|
|
453
|
+
const calls = result.toolCalls.map((tc) => {
|
|
440
454
|
let args;
|
|
441
455
|
try {
|
|
442
456
|
args = JSON.parse(tc.function.arguments || '{}');
|
|
443
457
|
} catch {
|
|
444
458
|
args = {};
|
|
445
459
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
460
|
+
return { name: tc.function.name, args };
|
|
461
|
+
});
|
|
462
|
+
const contents = await runToolCalls(calls, toolContext, onEvent);
|
|
463
|
+
msgs = [...msgs, ...result.toolCalls.map((tc, i) => ({ role: 'tool', tool_call_id: tc.id, content: contents[i] }))];
|
|
451
464
|
}
|
|
452
465
|
}
|
|
453
466
|
|
package/src/web/ai-context.js
CHANGED
|
@@ -23,7 +23,8 @@ const ROLE = read('role.md');
|
|
|
23
23
|
const CONVENTIONS = read('conventions.md');
|
|
24
24
|
const API_REF = read('api-ref.md');
|
|
25
25
|
const API_REF_COMPACT = read('api-ref-compact.md');
|
|
26
|
-
const FORMATS = read('formats.md');
|
|
26
|
+
const FORMATS = read('formats.md'); // the hand-over through propose_script (I16, I41)
|
|
27
|
+
const FORMATS_NOTOOLS = read('formats-notools.md'); // the fenced block with the @new-file hint, for chats without tools
|
|
27
28
|
const TOOLS = read('tools.md');
|
|
28
29
|
const COMPACT_STEPS = read('compact.md');
|
|
29
30
|
const DB_VIEW_PROMPT = read('db-view.md');
|
|
@@ -98,7 +99,7 @@ function buildSystemPromptParts(requestCtx, currentScript, currentView, currentD
|
|
|
98
99
|
staticParts.push(ROLE, CONVENTIONS);
|
|
99
100
|
if (requestCtx.apiref !== false) staticParts.push(profile === 'compact' ? API_REF_COMPACT : API_REF);
|
|
100
101
|
if (toolsOffered) staticParts.push(TOOLS);
|
|
101
|
-
staticParts.push(FORMATS);
|
|
102
|
+
staticParts.push(toolsOffered ? FORMATS : FORMATS_NOTOOLS);
|
|
102
103
|
if (profile === 'compact') staticParts.push(COMPACT_STEPS);
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The worker side of the AI chat's run_analysis tool (roadmap I42, decision D-9). It runs the model's code as the
|
|
5
|
+
* body of an async function in a vm context that holds only `data`, `console` and the JavaScript built-ins —
|
|
6
|
+
* no require, no process, no network. Every `data.*` call is a message to the main thread, which answers from the
|
|
7
|
+
* daemon's loaders; the main thread kills the worker when the time is up.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const vm = require('node:vm');
|
|
11
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
12
|
+
|
|
13
|
+
const MAX_LOG_LINES = 50;
|
|
14
|
+
const logs = [];
|
|
15
|
+
let nextId = 1;
|
|
16
|
+
const pending = new Map();
|
|
17
|
+
|
|
18
|
+
function call(name, args) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const id = nextId++;
|
|
21
|
+
pending.set(id, { resolve, reject });
|
|
22
|
+
parentPort.postMessage({ type: 'call', id, name, args });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
parentPort.on('message', (m) => {
|
|
27
|
+
if (m && m.type === 'reply') {
|
|
28
|
+
const p = pending.get(m.id);
|
|
29
|
+
if (!p) return;
|
|
30
|
+
pending.delete(m.id);
|
|
31
|
+
if (m.ok) p.resolve(m.value);
|
|
32
|
+
else p.reject(new Error(m.error));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const fmt = (args) =>
|
|
37
|
+
args
|
|
38
|
+
.map((a) => {
|
|
39
|
+
if (typeof a === 'string') return a;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(a);
|
|
42
|
+
} catch {
|
|
43
|
+
return String(a);
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.join(' ');
|
|
47
|
+
const sandboxConsole = {
|
|
48
|
+
log: (...a) => logs.length < MAX_LOG_LINES && logs.push(fmt(a)),
|
|
49
|
+
info: (...a) => logs.length < MAX_LOG_LINES && logs.push(fmt(a)),
|
|
50
|
+
warn: (...a) => logs.length < MAX_LOG_LINES && logs.push('warn: ' + fmt(a)),
|
|
51
|
+
error: (...a) => logs.length < MAX_LOG_LINES && logs.push('error: ' + fmt(a)),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const data = Object.freeze({
|
|
55
|
+
topics: (filter = '') => call('topics', [filter]),
|
|
56
|
+
history: (topics, from, to) => call('history', [topics, from, to]),
|
|
57
|
+
messages: (topics, from, to, limit) => call('messages', [topics, from, to, limit]),
|
|
58
|
+
log: (filter = '', from, to, level, limit) => call('log', [filter, from, to, level, limit]),
|
|
59
|
+
script: (path) => call('script', [path]),
|
|
60
|
+
time: (str) => call('time', [str]),
|
|
61
|
+
from: workerData.from,
|
|
62
|
+
to: workerData.to,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const context = vm.createContext({ data, console: sandboxConsole });
|
|
66
|
+
|
|
67
|
+
(async () => {
|
|
68
|
+
let value;
|
|
69
|
+
try {
|
|
70
|
+
const script = new vm.Script('(async () => {\n' + String(workerData.code) + '\n})()', { filename: 'analysis.js' });
|
|
71
|
+
value = await script.runInContext(context, { timeout: workerData.timeoutMs });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
parentPort.postMessage({ type: 'error', message: e && e.message ? e.message : String(e), logs });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let json;
|
|
77
|
+
try {
|
|
78
|
+
json = JSON.stringify(value === undefined ? null : value);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
parentPort.postMessage({ type: 'error', message: 'the result is not JSON-serialisable: ' + e.message, logs });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
parentPort.postMessage({ type: 'done', json, logs });
|
|
84
|
+
})();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The main-thread side of the AI chat's run_analysis tool (roadmap I42, decision D-9): starts the worker with the
|
|
5
|
+
* model's code, answers its data calls from the loaders it is given, and terminates it when the time is up or the
|
|
6
|
+
* heap limit is hit. Nothing of the daemon is reachable from the worker except through the loaders.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { Worker } = require('node:worker_threads');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
13
|
+
const DEFAULT_HEAP_MB = 256;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ code: string, from?: string, to?: string }} job
|
|
17
|
+
* @param {Record<string, (...args: any[]) => any>} loaders topics, history, messages, log, script, time
|
|
18
|
+
* @param {{ timeoutMs?: number, heapMb?: number }} [opts]
|
|
19
|
+
* @returns {Promise<{ json?: string, error?: string, logs: string[], ms: number, calls: number }>}
|
|
20
|
+
*/
|
|
21
|
+
function runAnalysis(job, loaders, opts = {}) {
|
|
22
|
+
const timeoutMs = Math.max(1000, Number(opts.timeoutMs) || DEFAULT_TIMEOUT_MS);
|
|
23
|
+
const heapMb = Math.max(32, Number(opts.heapMb) || DEFAULT_HEAP_MB);
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
let calls = 0;
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
let settled = false;
|
|
28
|
+
const worker = new Worker(path.join(__dirname, 'ai-sandbox-worker.js'), {
|
|
29
|
+
workerData: { code: String(job.code || ''), from: job.from ?? null, to: job.to ?? null, timeoutMs },
|
|
30
|
+
resourceLimits: { maxOldGenerationSizeMb: heapMb, maxYoungGenerationSizeMb: 32 },
|
|
31
|
+
});
|
|
32
|
+
const finish = (result) => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
worker.terminate().catch(() => {});
|
|
37
|
+
resolve({ ...result, ms: Date.now() - started, calls });
|
|
38
|
+
};
|
|
39
|
+
const timer = setTimeout(() => finish({ error: `the analysis did not finish within ${timeoutMs / 1000} s and was stopped`, logs: [] }), timeoutMs);
|
|
40
|
+
worker.on('message', async (m) => {
|
|
41
|
+
if (!m || typeof m !== 'object') return;
|
|
42
|
+
if (m.type === 'call') {
|
|
43
|
+
calls++;
|
|
44
|
+
const fn = loaders[m.name];
|
|
45
|
+
try {
|
|
46
|
+
if (typeof fn !== 'function') throw new Error(`data.${m.name} is not available`);
|
|
47
|
+
const value = await fn(...(Array.isArray(m.args) ? m.args : []));
|
|
48
|
+
worker.postMessage({ type: 'reply', id: m.id, ok: true, value });
|
|
49
|
+
} catch (e) {
|
|
50
|
+
worker.postMessage({ type: 'reply', id: m.id, ok: false, error: e && e.message ? e.message : String(e) });
|
|
51
|
+
}
|
|
52
|
+
} else if (m.type === 'done') finish({ json: m.json, logs: m.logs || [] });
|
|
53
|
+
else if (m.type === 'error') finish({ error: m.message, logs: m.logs || [] });
|
|
54
|
+
});
|
|
55
|
+
worker.on('error', (e) => finish({ error: e && e.message ? e.message : String(e), logs: [] }));
|
|
56
|
+
worker.on('exit', (code) => {
|
|
57
|
+
if (!settled)
|
|
58
|
+
finish({ error: code === 0 ? 'the analysis ended without a result' : `the analysis was stopped (exit ${code}; the heap limit of ${heapMb} MB?)`, logs: [] });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { runAnalysis, DEFAULT_TIMEOUT_MS };
|