iobroker.assistant 1.0.2 → 1.0.3
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 +3 -0
- package/admin/custom/.vite/manifest.json +91 -87
- package/admin/custom/assets/Components-C6qcpaRC.js +1 -0
- package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DcAGnE2z.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-A4gakomR.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-CzBMxGFr.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-1XsLCK2M.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-CwqmfMvn.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-CMv8bcyA.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BMuLYe6f.js} +80 -97
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare__shared-3mzaJWDF.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare__shared-BxjZmnpc.js} +2 -2
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-TM4l0OYm.js +122 -0
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-B_v57Qfr.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js} +1 -1
- package/admin/custom/assets/{dist-DiU1y0R9.js → dist-B3E7IYuQ.js} +1 -1
- package/admin/custom/assets/helpers-DE61bb74.js +1 -0
- package/admin/custom/assets/{hostInit-MshDicd1.js → hostInit-4roC4hEj.js} +1 -1
- package/admin/custom/assets/{lib-DjwHKtRi.js → lib-CchzgTUs.js} +1 -1
- package/admin/custom/assets/{mf-entry-bootstrap-0-7c1ddf47.js → mf-entry-bootstrap-0-4806bbe4.js} +2 -2
- package/admin/custom/assets/{pendingShares-CaJDCKLB.js → pendingShares-DLlv8yFY.js} +2 -2
- package/admin/custom/assets/prod-ChPGnyiA.js +15 -0
- package/admin/custom/assets/rolldown-runtime-COiEfhsA.js +1 -0
- package/admin/custom/assets/utils-Bi7ATSfV.js +4 -0
- package/admin/custom/customComponents.js +2 -2
- package/admin/custom/i18n/de.json +25 -0
- package/admin/custom/i18n/en.json +25 -0
- package/admin/custom/i18n/es.json +25 -0
- package/admin/custom/i18n/fr.json +25 -0
- package/admin/custom/i18n/it.json +25 -0
- package/admin/custom/i18n/nl.json +25 -0
- package/admin/custom/i18n/pl.json +25 -0
- package/admin/custom/i18n/pt.json +25 -0
- package/admin/custom/i18n/ru.json +25 -0
- package/admin/custom/i18n/uk.json +25 -0
- package/admin/custom/i18n/zh-cn.json +25 -0
- package/admin/custom/index.html +2 -2
- package/admin/custom/mf-manifest.json +1 -1
- package/admin/custom/mf-stats.json +1 -1
- package/admin/i18n/de.json +3 -1
- package/admin/i18n/en.json +3 -1
- package/admin/i18n/es.json +3 -1
- package/admin/i18n/fr.json +3 -1
- package/admin/i18n/it.json +3 -1
- package/admin/i18n/nl.json +3 -1
- package/admin/i18n/pl.json +3 -1
- package/admin/i18n/pt.json +3 -1
- package/admin/i18n/ru.json +3 -1
- package/admin/i18n/uk.json +3 -1
- package/admin/i18n/zh-cn.json +3 -1
- package/admin/jsonConfig.json +13 -0
- package/build/lib/journal.js +273 -0
- package/build/lib/journal.js.map +1 -0
- package/build/lib/voice/esphome.js +9 -1
- package/build/lib/voice/esphome.js.map +1 -1
- package/build/lib/voice/voiceServer.js +3 -1
- package/build/lib/voice/voiceServer.js.map +1 -1
- package/build/lib/voice/wyoming.js +3 -1
- package/build/lib/voice/wyoming.js.map +1 -1
- package/build/main.js +146 -12
- package/build/main.js.map +1 -1
- package/io-package.json +36 -1
- package/package.json +7 -7
- package/admin/custom/assets/Components-7BDDNrGP.js +0 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-DS8oaQQt.js +0 -122
- package/admin/custom/assets/helpers-Dot7vgQQ.js +0 -1
- package/admin/custom/assets/rolldown-runtime-Dd_uD5pT.js +0 -1
- package/admin/custom/assets/utils-towTPKqG.js +0 -4
package/admin/i18n/pl.json
CHANGED
|
@@ -182,5 +182,7 @@
|
|
|
182
182
|
"e.g. westeurope": "np. zachodnia Europa",
|
|
183
183
|
"None (local only)": "Brak (tylko lokalnie)",
|
|
184
184
|
"Without an LLM provider only the local tiers answer: the rule-based commands, routines, timers and alarms, and the local model if enabled. Anything else gets a short \"I can't do that\" reply.": "Bez dostawcy LLM odpowiadają tylko poziomy lokalne: polecenia oparte na regułach, rutyny, minutniki i budziki oraz model lokalny, jeśli jest włączony. Na wszystko inne pada krótka odpowiedź „Nie potrafię tego”.",
|
|
185
|
-
"Chat": "Czat"
|
|
185
|
+
"Chat": "Czat",
|
|
186
|
+
"Chat history (hours)": "Chat history (hours)",
|
|
187
|
+
"How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing.": "How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing."
|
|
186
188
|
}
|
package/admin/i18n/pt.json
CHANGED
|
@@ -182,5 +182,7 @@
|
|
|
182
182
|
"e.g. westeurope": "por exemplo, Europa Ocidental",
|
|
183
183
|
"None (local only)": "Nenhum (apenas local)",
|
|
184
184
|
"Without an LLM provider only the local tiers answer: the rule-based commands, routines, timers and alarms, and the local model if enabled. Anything else gets a short \"I can't do that\" reply.": "Sem fornecedor de LLM, apenas os níveis locais respondem: os comandos baseados em regras, as rotinas, os temporizadores e alarmes e o modelo local, se ativado. Tudo o resto recebe uma breve resposta «Não consigo fazer isso».",
|
|
185
|
-
"Chat": "Chat"
|
|
185
|
+
"Chat": "Chat",
|
|
186
|
+
"Chat history (hours)": "Chat history (hours)",
|
|
187
|
+
"How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing.": "How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing."
|
|
186
188
|
}
|
package/admin/i18n/ru.json
CHANGED
|
@@ -182,5 +182,7 @@
|
|
|
182
182
|
"e.g. westeurope": "например, Западная Европа",
|
|
183
183
|
"None (local only)": "Нет (только локально)",
|
|
184
184
|
"Without an LLM provider only the local tiers answer: the rule-based commands, routines, timers and alarms, and the local model if enabled. Anything else gets a short \"I can't do that\" reply.": "Без LLM-провайдера отвечают только локальные уровни: команды на основе правил, рутины, таймеры и будильники, а также локальная модель, если она включена. На всё остальное будет короткий ответ «Этого я не умею».",
|
|
185
|
-
"Chat": "Чат"
|
|
185
|
+
"Chat": "Чат",
|
|
186
|
+
"Chat history (hours)": "История чата (часы)",
|
|
187
|
+
"How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing.": "Как долго вкладка «Чат» показывает прошлые запросы из всех источников (сателлиты, Telegram, скрипты) с wake word, уровнем обработки, инструментами и ID состояний. 0 = ничего не записывать."
|
|
186
188
|
}
|
package/admin/i18n/uk.json
CHANGED
|
@@ -182,5 +182,7 @@
|
|
|
182
182
|
"e.g. westeurope": "наприклад, Західна Європа",
|
|
183
183
|
"None (local only)": "Немає (лише локально)",
|
|
184
184
|
"Without an LLM provider only the local tiers answer: the rule-based commands, routines, timers and alarms, and the local model if enabled. Anything else gets a short \"I can't do that\" reply.": "Без LLM-провайдера відповідають лише локальні рівні: команди на основі правил, рутини, таймери й будильники, а також локальна модель, якщо її ввімкнено. На все інше буде коротка відповідь «Цього я не вмію».",
|
|
185
|
-
"Chat": "Чат"
|
|
185
|
+
"Chat": "Чат",
|
|
186
|
+
"Chat history (hours)": "Chat history (hours)",
|
|
187
|
+
"How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing.": "How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing."
|
|
186
188
|
}
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -182,5 +182,7 @@
|
|
|
182
182
|
"e.g. westeurope": "例如:西欧",
|
|
183
183
|
"None (local only)": "无(仅本地)",
|
|
184
184
|
"Without an LLM provider only the local tiers answer: the rule-based commands, routines, timers and alarms, and the local model if enabled. Anything else gets a short \"I can't do that\" reply.": "没有 LLM 提供商时,只有本地层级会作答:基于规则的命令、例程、定时器和闹钟,以及已启用的本地模型。其他请求只会得到一句简短的“我做不到”。",
|
|
185
|
-
"Chat": "聊天"
|
|
185
|
+
"Chat": "聊天",
|
|
186
|
+
"Chat history (hours)": "Chat history (hours)",
|
|
187
|
+
"How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing.": "How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing."
|
|
186
188
|
}
|
package/admin/jsonConfig.json
CHANGED
|
@@ -218,6 +218,19 @@
|
|
|
218
218
|
"lg": 4,
|
|
219
219
|
"xl": 4
|
|
220
220
|
},
|
|
221
|
+
"journalHours": {
|
|
222
|
+
"type": "number",
|
|
223
|
+
"label": "Chat history (hours)",
|
|
224
|
+
"help": "How long the Chat tab shows past requests from all sources (satellites, Telegram, scripts) with wake word, answering tier, tools and state ids. 0 = record nothing.",
|
|
225
|
+
"min": 0,
|
|
226
|
+
"max": 168,
|
|
227
|
+
"default": 24,
|
|
228
|
+
"xs": 12,
|
|
229
|
+
"sm": 6,
|
|
230
|
+
"md": 4,
|
|
231
|
+
"lg": 4,
|
|
232
|
+
"xl": 4
|
|
233
|
+
},
|
|
221
234
|
"weatherInstance": {
|
|
222
235
|
"newLine": true,
|
|
223
236
|
"type": "selectSendTo",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Journal = void 0;
|
|
37
|
+
exports.transportForSource = transportForSource;
|
|
38
|
+
/**
|
|
39
|
+
* Conversation journal: one entry per request the assistant answered — where it came from (chat, script,
|
|
40
|
+
* Telegram, which satellite, which wake word), which tier answered it (routine, rule-based NLU, local
|
|
41
|
+
* model, cloud model), which tools the model called and which state ids were read or written along the
|
|
42
|
+
* way. The admin chat shows it live, so one can check what a satellite heard and what was done about it.
|
|
43
|
+
*
|
|
44
|
+
* Tools and state accesses happen deep inside the pipeline (NLU executor, LLM tool loop, routine
|
|
45
|
+
* executor). Instead of threading an entry object through all of them, the entry rides along in an
|
|
46
|
+
* {@link AsyncLocalStorage}: {@link Journal.run} opens it, and {@link Journal.note}/{@link Journal.tool}/
|
|
47
|
+
* {@link Journal.stateAccess} write to whatever entry the current call chain belongs to — and do nothing
|
|
48
|
+
* outside of one (a trigger writing a state is not a conversation).
|
|
49
|
+
*/
|
|
50
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
51
|
+
const node_fs_1 = require("node:fs");
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
/** Hard cap independent of the retention — a script asking every few seconds must not fill the disk. */
|
|
54
|
+
const MAX_ENTRIES = 3000;
|
|
55
|
+
const SAVE_DELAY_MS = 5000;
|
|
56
|
+
const RESULT_CHARS = 300;
|
|
57
|
+
/** Derive the transport from a request source when the caller did not say. */
|
|
58
|
+
function transportForSource(source) {
|
|
59
|
+
if (source === 'chat') {
|
|
60
|
+
return 'chat';
|
|
61
|
+
}
|
|
62
|
+
if (source === '') {
|
|
63
|
+
return 'state';
|
|
64
|
+
}
|
|
65
|
+
if (source === 'wyoming') {
|
|
66
|
+
return 'wyoming';
|
|
67
|
+
}
|
|
68
|
+
if (source.startsWith('telegram:')) {
|
|
69
|
+
return 'telegram';
|
|
70
|
+
}
|
|
71
|
+
return 'script';
|
|
72
|
+
}
|
|
73
|
+
class Journal {
|
|
74
|
+
opts;
|
|
75
|
+
entries = [];
|
|
76
|
+
store = new node_async_hooks_1.AsyncLocalStorage();
|
|
77
|
+
saveTimer = null;
|
|
78
|
+
counter = 0;
|
|
79
|
+
constructor(opts) {
|
|
80
|
+
this.opts = opts;
|
|
81
|
+
}
|
|
82
|
+
get enabled() {
|
|
83
|
+
return this.opts.hours > 0;
|
|
84
|
+
}
|
|
85
|
+
/** Load the entries of the last run (silently starts empty when there are none). */
|
|
86
|
+
async load() {
|
|
87
|
+
if (!this.enabled || !this.opts.file) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const raw = JSON.parse(await node_fs_1.promises.readFile(this.opts.file, 'utf8'));
|
|
92
|
+
if (Array.isArray(raw)) {
|
|
93
|
+
// An entry still open at shutdown never got its answer.
|
|
94
|
+
this.entries = raw
|
|
95
|
+
.filter(e => e && typeof e.id === 'string' && typeof e.ts === 'number')
|
|
96
|
+
.map(e => (e.done ? e : { ...e, done: true, error: e.error || 'interrupted' }));
|
|
97
|
+
this.prune();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
if (e.code !== 'ENOENT') {
|
|
102
|
+
this.opts.log.warn(`Journal could not be read: ${e.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Entries newer than `since` (ms), oldest first. */
|
|
107
|
+
list(since = 0) {
|
|
108
|
+
this.prune();
|
|
109
|
+
return this.entries.filter(e => e.ts > since);
|
|
110
|
+
}
|
|
111
|
+
clear() {
|
|
112
|
+
this.entries = [];
|
|
113
|
+
this.scheduleSave();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Run `fn` as one journalled request. The entry is published once when the request starts (so the
|
|
117
|
+
* chat shows the question while the answer is still being worked out) and once when it is finished.
|
|
118
|
+
*/
|
|
119
|
+
async run(question, source, origin, fn) {
|
|
120
|
+
if (!this.enabled) {
|
|
121
|
+
return fn();
|
|
122
|
+
}
|
|
123
|
+
const entry = {
|
|
124
|
+
id: `${Date.now().toString(36)}-${(this.counter++).toString(36)}`,
|
|
125
|
+
ts: Date.now(),
|
|
126
|
+
done: false,
|
|
127
|
+
source,
|
|
128
|
+
transport: origin.transport || transportForSource(source),
|
|
129
|
+
question,
|
|
130
|
+
tools: [],
|
|
131
|
+
read: [],
|
|
132
|
+
written: [],
|
|
133
|
+
};
|
|
134
|
+
for (const key of ['satellite', 'room', 'wakeWord', 'sttMs']) {
|
|
135
|
+
if (origin[key] !== undefined && origin[key] !== '') {
|
|
136
|
+
entry[key] = origin[key];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// A satellite that re-opened its mic reports no wake word — say so instead of "unknown".
|
|
140
|
+
if (origin.wakeWord === '') {
|
|
141
|
+
entry.wakeWord = '';
|
|
142
|
+
}
|
|
143
|
+
this.entries.push(entry);
|
|
144
|
+
this.publish(entry);
|
|
145
|
+
try {
|
|
146
|
+
const answer = await this.store.run(entry, fn);
|
|
147
|
+
entry.answer = answer;
|
|
148
|
+
return answer;
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
entry.error = e.message;
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
entry.done = true;
|
|
156
|
+
entry.ms = Date.now() - entry.ts;
|
|
157
|
+
this.publish(entry);
|
|
158
|
+
this.prune();
|
|
159
|
+
this.scheduleSave();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Fill in fields of the current request's entry (tier, detail, followUp …). */
|
|
163
|
+
note(fields) {
|
|
164
|
+
const entry = this.store.getStore();
|
|
165
|
+
if (entry) {
|
|
166
|
+
Object.assign(entry, fields);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Record a tool call the model made for the current request. */
|
|
170
|
+
tool(name, args, ms, result, error) {
|
|
171
|
+
const entry = this.store.getStore();
|
|
172
|
+
if (!entry) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const item = { name, args: args || {}, ms };
|
|
176
|
+
if (error) {
|
|
177
|
+
item.error = error;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
let text;
|
|
181
|
+
try {
|
|
182
|
+
text = typeof result === 'string' ? result : JSON.stringify(result);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
text = String(result);
|
|
186
|
+
}
|
|
187
|
+
item.result = text.length > RESULT_CHARS ? `${text.slice(0, RESULT_CHARS)}…` : text;
|
|
188
|
+
}
|
|
189
|
+
entry.tools.push(item);
|
|
190
|
+
// Live view: show each tool call as it happens, a cloud round can take seconds.
|
|
191
|
+
this.publish(entry);
|
|
192
|
+
}
|
|
193
|
+
/** Record which states a call reads or writes (MCP `get_states`/`set_state`/`set_states`, routines). */
|
|
194
|
+
stateAccess(tool, args) {
|
|
195
|
+
const entry = this.store.getStore();
|
|
196
|
+
if (!entry || !args) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (tool === 'get_states' && Array.isArray(args.ids)) {
|
|
200
|
+
for (const id of args.ids) {
|
|
201
|
+
if (typeof id === 'string' && !entry.read.includes(id)) {
|
|
202
|
+
entry.read.push(id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (tool === 'get_state' && typeof args.id === 'string') {
|
|
207
|
+
if (!entry.read.includes(args.id)) {
|
|
208
|
+
entry.read.push(args.id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (tool === 'set_state' && typeof args.id === 'string') {
|
|
212
|
+
entry.written.push({ id: args.id, value: args.value });
|
|
213
|
+
}
|
|
214
|
+
else if (tool === 'set_states' && Array.isArray(args.states)) {
|
|
215
|
+
for (const s of args.states) {
|
|
216
|
+
if (typeof s?.id === 'string') {
|
|
217
|
+
entry.written.push({ id: s.id, value: s.value });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Write pending changes now (on unload). */
|
|
223
|
+
async flush() {
|
|
224
|
+
if (this.saveTimer) {
|
|
225
|
+
clearTimeout(this.saveTimer);
|
|
226
|
+
this.saveTimer = null;
|
|
227
|
+
}
|
|
228
|
+
await this.save();
|
|
229
|
+
}
|
|
230
|
+
publish(entry) {
|
|
231
|
+
try {
|
|
232
|
+
this.opts.onChange?.(entry);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// a listener must never break the answer
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
prune() {
|
|
239
|
+
const cutoff = Date.now() - this.opts.hours * 3600_000;
|
|
240
|
+
let start = 0;
|
|
241
|
+
while (start < this.entries.length && this.entries[start].ts < cutoff) {
|
|
242
|
+
start++;
|
|
243
|
+
}
|
|
244
|
+
start = Math.max(start, this.entries.length - MAX_ENTRIES);
|
|
245
|
+
if (start > 0) {
|
|
246
|
+
this.entries = this.entries.slice(start);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
scheduleSave() {
|
|
250
|
+
if (!this.opts.file || this.saveTimer) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.saveTimer = setTimeout(() => {
|
|
254
|
+
this.saveTimer = null;
|
|
255
|
+
void this.save();
|
|
256
|
+
}, SAVE_DELAY_MS);
|
|
257
|
+
this.saveTimer.unref?.();
|
|
258
|
+
}
|
|
259
|
+
async save() {
|
|
260
|
+
if (!this.opts.file) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
await node_fs_1.promises.mkdir(path.dirname(this.opts.file), { recursive: true });
|
|
265
|
+
await node_fs_1.promises.writeFile(this.opts.file, JSON.stringify(this.entries.filter(e => e.done)));
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
this.opts.log.warn(`Journal could not be saved: ${e.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
exports.Journal = Journal;
|
|
273
|
+
//# sourceMappingURL=journal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/lib/journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Conversation journal: one entry per request the assistant answered — where it came from (chat, script,\n * Telegram, which satellite, which wake word), which tier answered it (routine, rule-based NLU, local\n * model, cloud model), which tools the model called and which state ids were read or written along the\n * way. The admin chat shows it live, so one can check what a satellite heard and what was done about it.\n *\n * Tools and state accesses happen deep inside the pipeline (NLU executor, LLM tool loop, routine\n * executor). Instead of threading an entry object through all of them, the entry rides along in an\n * {@link AsyncLocalStorage}: {@link Journal.run} opens it, and {@link Journal.note}/{@link Journal.tool}/\n * {@link Journal.stateAccess} write to whatever entry the current call chain belongs to — and do nothing\n * outside of one (a trigger writing a state is not a conversation).\n */\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\n\n/** How a request reached us. */\nexport type JournalTransport = 'chat' | 'script' | 'state' | 'telegram' | 'esphome' | 'udp' | 'native' | 'wyoming';\n\n/** Which stage of the pipeline produced the answer. */\nexport type JournalTier =\n | 'stop' // stop word silenced a ringing timer/alarm\n | 'askReply' // the utterance answered an open `askUser` question\n | 'routine'\n | 'nlu'\n | 'localLlm'\n | 'cloud'\n | 'none'; // nothing local understood it and there is no cloud provider\n\nexport interface JournalTool {\n name: string;\n args: Record<string, unknown>;\n ms: number;\n /** Result, JSON, shortened. */\n result?: string;\n error?: string;\n}\n\nexport interface JournalEntry {\n id: string;\n /** Start of the answer, ms. */\n ts: number;\n /** False while the answer is still being produced. */\n done: boolean;\n /** Time to produce the answer (without STT/TTS). */\n ms?: number;\n source: string;\n transport: JournalTransport;\n satellite?: string;\n room?: string;\n /** Wake word the satellite reported; '' = the mic was re-opened for a follow-up, absent = unknown. */\n wakeWord?: string;\n /** Speech recognition time, for voice requests. */\n sttMs?: number;\n question: string;\n answer?: string;\n error?: string;\n tier?: JournalTier;\n /** Model name, routine name or NLU actions — whatever explains the tier. */\n detail?: string;\n tools: JournalTool[];\n /** State ids read to produce the answer. */\n read: string[];\n /** States written to produce the answer. */\n written: { id: string; value: unknown }[];\n /** The reply asks back, so a satellite re-opened its mic. */\n followUp?: boolean;\n}\n\n/** What the caller knows about a request before the pipeline starts. */\nexport interface JournalOrigin {\n transport?: JournalTransport;\n satellite?: string;\n room?: string;\n wakeWord?: string;\n sttMs?: number;\n}\n\nexport interface JournalOptions {\n /** Retention in hours; 0 disables the journal entirely. */\n hours: number;\n /** File the journal is kept in across restarts ('' = memory only). */\n file: string;\n /** Called with every new or updated entry (drives the `journal.last` state the chat listens to). */\n onChange?: (entry: JournalEntry) => void;\n log: ioBroker.Logger;\n}\n\n/** Hard cap independent of the retention — a script asking every few seconds must not fill the disk. */\nconst MAX_ENTRIES = 3000;\nconst SAVE_DELAY_MS = 5000;\nconst RESULT_CHARS = 300;\n\n/** Derive the transport from a request source when the caller did not say. */\nexport function transportForSource(source: string): JournalTransport {\n if (source === 'chat') {\n return 'chat';\n }\n if (source === '') {\n return 'state';\n }\n if (source === 'wyoming') {\n return 'wyoming';\n }\n if (source.startsWith('telegram:')) {\n return 'telegram';\n }\n return 'script';\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly store = new AsyncLocalStorage<JournalEntry>();\n private saveTimer: NodeJS.Timeout | null = null;\n private counter = 0;\n\n constructor(private readonly opts: JournalOptions) {}\n\n get enabled(): boolean {\n return this.opts.hours > 0;\n }\n\n /** Load the entries of the last run (silently starts empty when there are none). */\n async load(): Promise<void> {\n if (!this.enabled || !this.opts.file) {\n return;\n }\n try {\n const raw = JSON.parse(await fs.readFile(this.opts.file, 'utf8')) as unknown;\n if (Array.isArray(raw)) {\n // An entry still open at shutdown never got its answer.\n this.entries = (raw as JournalEntry[])\n .filter(e => e && typeof e.id === 'string' && typeof e.ts === 'number')\n .map(e => (e.done ? e : { ...e, done: true, error: e.error || 'interrupted' }));\n this.prune();\n }\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n this.opts.log.warn(`Journal could not be read: ${(e as Error).message}`);\n }\n }\n }\n\n /** Entries newer than `since` (ms), oldest first. */\n list(since = 0): JournalEntry[] {\n this.prune();\n return this.entries.filter(e => e.ts > since);\n }\n\n clear(): void {\n this.entries = [];\n this.scheduleSave();\n }\n\n /**\n * Run `fn` as one journalled request. The entry is published once when the request starts (so the\n * chat shows the question while the answer is still being worked out) and once when it is finished.\n */\n async run(question: string, source: string, origin: JournalOrigin, fn: () => Promise<string>): Promise<string> {\n if (!this.enabled) {\n return fn();\n }\n const entry: JournalEntry = {\n id: `${Date.now().toString(36)}-${(this.counter++).toString(36)}`,\n ts: Date.now(),\n done: false,\n source,\n transport: origin.transport || transportForSource(source),\n question,\n tools: [],\n read: [],\n written: [],\n };\n for (const key of ['satellite', 'room', 'wakeWord', 'sttMs'] as const) {\n if (origin[key] !== undefined && origin[key] !== '') {\n (entry as unknown as Record<string, unknown>)[key] = origin[key];\n }\n }\n // A satellite that re-opened its mic reports no wake word — say so instead of \"unknown\".\n if (origin.wakeWord === '') {\n entry.wakeWord = '';\n }\n this.entries.push(entry);\n this.publish(entry);\n try {\n const answer = await this.store.run(entry, fn);\n entry.answer = answer;\n return answer;\n } catch (e) {\n entry.error = (e as Error).message;\n throw e;\n } finally {\n entry.done = true;\n entry.ms = Date.now() - entry.ts;\n this.publish(entry);\n this.prune();\n this.scheduleSave();\n }\n }\n\n /** Fill in fields of the current request's entry (tier, detail, followUp …). */\n note(fields: Partial<Pick<JournalEntry, 'tier' | 'detail' | 'followUp'>>): void {\n const entry = this.store.getStore();\n if (entry) {\n Object.assign(entry, fields);\n }\n }\n\n /** Record a tool call the model made for the current request. */\n tool(name: string, args: Record<string, unknown>, ms: number, result: unknown, error?: string): void {\n const entry = this.store.getStore();\n if (!entry) {\n return;\n }\n const item: JournalTool = { name, args: args || {}, ms };\n if (error) {\n item.error = error;\n } else {\n let text: string;\n try {\n text = typeof result === 'string' ? result : JSON.stringify(result);\n } catch {\n text = String(result);\n }\n item.result = text.length > RESULT_CHARS ? `${text.slice(0, RESULT_CHARS)}…` : text;\n }\n entry.tools.push(item);\n // Live view: show each tool call as it happens, a cloud round can take seconds.\n this.publish(entry);\n }\n\n /** Record which states a call reads or writes (MCP `get_states`/`set_state`/`set_states`, routines). */\n stateAccess(tool: string, args: Record<string, unknown> | undefined): void {\n const entry = this.store.getStore();\n if (!entry || !args) {\n return;\n }\n if (tool === 'get_states' && Array.isArray(args.ids)) {\n for (const id of args.ids) {\n if (typeof id === 'string' && !entry.read.includes(id)) {\n entry.read.push(id);\n }\n }\n } else if (tool === 'get_state' && typeof args.id === 'string') {\n if (!entry.read.includes(args.id)) {\n entry.read.push(args.id);\n }\n } else if (tool === 'set_state' && typeof args.id === 'string') {\n entry.written.push({ id: args.id, value: args.value });\n } else if (tool === 'set_states' && Array.isArray(args.states)) {\n for (const s of args.states as { id?: unknown; value?: unknown }[]) {\n if (typeof s?.id === 'string') {\n entry.written.push({ id: s.id, value: s.value });\n }\n }\n }\n }\n\n /** Write pending changes now (on unload). */\n async flush(): Promise<void> {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n await this.save();\n }\n\n private publish(entry: JournalEntry): void {\n try {\n this.opts.onChange?.(entry);\n } catch {\n // a listener must never break the answer\n }\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.opts.hours * 3600_000;\n let start = 0;\n while (start < this.entries.length && this.entries[start].ts < cutoff) {\n start++;\n }\n start = Math.max(start, this.entries.length - MAX_ENTRIES);\n if (start > 0) {\n this.entries = this.entries.slice(start);\n }\n }\n\n private scheduleSave(): void {\n if (!this.opts.file || this.saveTimer) {\n return;\n }\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.save();\n }, SAVE_DELAY_MS);\n this.saveTimer.unref?.();\n }\n\n private async save(): Promise<void> {\n if (!this.opts.file) {\n return;\n }\n try {\n await fs.mkdir(path.dirname(this.opts.file), { recursive: true });\n await fs.writeFile(this.opts.file, JSON.stringify(this.entries.filter(e => e.done)));\n } catch (e) {\n this.opts.log.warn(`Journal could not be saved: ${(e as Error).message}`);\n }\n }\n}\n"]}
|
|
@@ -542,6 +542,7 @@ class EsphomeConnection {
|
|
|
542
542
|
adaptive: true,
|
|
543
543
|
}),
|
|
544
544
|
conversationId: request.getConversationId(),
|
|
545
|
+
wakeWord: phrase || '',
|
|
545
546
|
timer,
|
|
546
547
|
closed: false,
|
|
547
548
|
};
|
|
@@ -595,7 +596,9 @@ class EsphomeConnection {
|
|
|
595
596
|
this.status('processing');
|
|
596
597
|
try {
|
|
597
598
|
const hints = this.opts.getHints ? await this.opts.getHints() : undefined;
|
|
599
|
+
const sttStart = Date.now();
|
|
598
600
|
const text = (await this.opts.stt.transcribe(pcm, MIC_SAMPLE_RATE, this.opts.language, hints)).trim();
|
|
601
|
+
const sttMs = Date.now() - sttStart;
|
|
599
602
|
this.event(EVENT.STT_END, { text });
|
|
600
603
|
if (!text) {
|
|
601
604
|
this.event(EVENT.RUN_END);
|
|
@@ -603,7 +606,12 @@ class EsphomeConnection {
|
|
|
603
606
|
return;
|
|
604
607
|
}
|
|
605
608
|
this.event(EVENT.INTENT_START);
|
|
606
|
-
const answer = (await this.opts.answer(text, {
|
|
609
|
+
const answer = (await this.opts.answer(text, {
|
|
610
|
+
device: this.deviceName,
|
|
611
|
+
room: this.room,
|
|
612
|
+
wakeWord: capture.wakeWord,
|
|
613
|
+
sttMs,
|
|
614
|
+
})).trim();
|
|
607
615
|
const followUp = !!answer && this.opts.expectsFollowUp?.(answer) === true;
|
|
608
616
|
this.event(EVENT.INTENT_END, followUp ? { continue_conversation: '1' } : undefined);
|
|
609
617
|
if (!answer) {
|