iobroker.assistant 0.1.6 → 1.0.2
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 +61 -4
- package/admin/custom/.vite/manifest.json +122 -77
- package/admin/custom/assets/Components-7BDDNrGP.js +1 -0
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DcAGnE2z.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-CzBMxGFr.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-1XsLCK2M.js +1 -0
- 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 +1217 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__198531368674860__loadShare__shared-3mzaJWDF.js +2 -0
- 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 +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 +1 -0
- package/admin/custom/assets/dist-DiU1y0R9.js +1 -0
- package/admin/custom/assets/helpers-Dot7vgQQ.js +1 -0
- package/admin/custom/assets/hostInit-MshDicd1.js +1 -0
- package/admin/custom/assets/iconLibraryData-B5fiyl1C.js +1 -0
- package/admin/custom/assets/index-P2Xu9kJm.js +1 -0
- package/admin/custom/assets/lib-DjwHKtRi.js +3 -0
- package/admin/custom/assets/{mf-entry-bootstrap-0-87f15505.js → mf-entry-bootstrap-0-7c1ddf47.js} +14 -2
- package/admin/custom/assets/pendingShares-CaJDCKLB.js +2 -0
- package/admin/custom/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
- package/admin/custom/assets/utils-towTPKqG.js +4 -0
- package/admin/custom/assets/vite-preload-helper-BbbETgeG.js +1 -0
- package/admin/custom/customComponents.js +2 -2
- package/admin/custom/i18n/de.json +64 -57
- package/admin/custom/i18n/en.json +8 -1
- package/admin/custom/i18n/es.json +64 -57
- package/admin/custom/i18n/fr.json +64 -57
- package/admin/custom/i18n/it.json +64 -57
- package/admin/custom/i18n/nl.json +64 -57
- package/admin/custom/i18n/pl.json +64 -57
- package/admin/custom/i18n/pt.json +64 -57
- package/admin/custom/i18n/ru.json +64 -57
- package/admin/custom/i18n/uk.json +64 -57
- package/admin/custom/i18n/zh-cn.json +64 -57
- 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 +59 -3
- package/admin/i18n/en.json +59 -3
- package/admin/i18n/es.json +59 -3
- package/admin/i18n/fr.json +59 -3
- package/admin/i18n/it.json +59 -3
- package/admin/i18n/nl.json +59 -3
- package/admin/i18n/pl.json +59 -3
- package/admin/i18n/pt.json +59 -3
- package/admin/i18n/ru.json +59 -3
- package/admin/i18n/uk.json +59 -3
- package/admin/i18n/zh-cn.json +59 -3
- package/admin/jsonConfig.json +589 -75
- package/build/lib/ask.js +130 -0
- package/build/lib/ask.js.map +1 -0
- package/build/lib/llm.js +135 -13
- package/build/lib/llm.js.map +1 -1
- package/build/lib/nlu.js +74 -1
- package/build/lib/nlu.js.map +1 -1
- package/build/lib/notifications.js +112 -0
- package/build/lib/notifications.js.map +1 -0
- package/build/lib/presence.js +188 -0
- package/build/lib/presence.js.map +1 -0
- package/build/lib/routines.js +115 -0
- package/build/lib/routines.js.map +1 -0
- package/build/lib/targets.js +96 -0
- package/build/lib/targets.js.map +1 -0
- package/build/lib/triggers.js +632 -0
- package/build/lib/triggers.js.map +1 -0
- package/build/lib/voice/aws.js +9 -1
- package/build/lib/voice/aws.js.map +1 -1
- package/build/lib/voice/azure.js +9 -2
- package/build/lib/voice/azure.js.map +1 -1
- package/build/lib/voice/engines.js +82 -0
- package/build/lib/voice/engines.js.map +1 -1
- package/build/lib/voice/esphome.js +732 -0
- package/build/lib/voice/esphome.js.map +1 -0
- package/build/lib/voice/esphomeEntities.js +303 -0
- package/build/lib/voice/esphomeEntities.js.map +1 -0
- package/build/lib/voice/esphomeProto.js +181 -0
- package/build/lib/voice/esphomeProto.js.map +1 -0
- package/build/lib/voice/fallback.js +85 -0
- package/build/lib/voice/fallback.js.map +1 -0
- package/build/lib/voice/mediaServer.js +151 -0
- package/build/lib/voice/mediaServer.js.map +1 -0
- package/build/lib/voice/protocol.js.map +1 -1
- package/build/lib/voice/tone.js +32 -0
- package/build/lib/voice/tone.js.map +1 -0
- package/build/lib/voice/tts.js.map +1 -1
- package/build/lib/voice/ttsCache.js +274 -0
- package/build/lib/voice/ttsCache.js.map +1 -0
- package/build/lib/voice/vad.js +173 -0
- package/build/lib/voice/vad.js.map +1 -0
- package/build/lib/voice/voiceServer.js +13 -0
- package/build/lib/voice/voiceServer.js.map +1 -1
- package/build/main.js +1721 -67
- package/build/main.js.map +1 -1
- package/io-package.json +241 -3
- package/package.json +13 -12
- package/admin/custom/assets/Components-xWdtqvTH.js +0 -1
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-dybYZgsu.js +0 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BnSUsPHa.js +0 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-DlSVT49A.js +0 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cfsgderv.js +0 -1243
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-ByJf4zv_.js +0 -1494
- package/admin/custom/assets/hostInit-CZawMH9B.js +0 -1
- package/admin/custom/assets/index-JB7g1jgx.js +0 -1
- package/admin/custom/assets/lib-bB-LHRhZ.js +0 -3
- package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +0 -1
- package/admin/custom/assets/virtualExposes-brKKZvi0.js +0 -1
- package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CRhwj12f.js +0 -1
- package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-C5XHUc1J.js +0 -2
- package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +0 -1
package/build/lib/ask.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pending questions — the other direction of a conversation: the adapter asks and waits for the answer.
|
|
4
|
+
*
|
|
5
|
+
* Everywhere else the user starts the exchange. Here a script, a timer or (later) a trigger asks
|
|
6
|
+
* something ("The fryer has been on for 5 hours. Shall I switch it off?") and the *next* utterance from
|
|
7
|
+
* that source is the answer: it must not go to the NLU or the LLM, which would treat a bare "yes" as a
|
|
8
|
+
* command and swallow it.
|
|
9
|
+
*
|
|
10
|
+
* A question is armed for one or more **sources** — the same key the answer pipeline sees: a satellite's
|
|
11
|
+
* device name, `'chat'`, `'telegram:<user>'`, … — or for {@link ANY_SOURCE} when it was broadcast and
|
|
12
|
+
* whoever hears it may answer. If nothing arrives within the timeout the promise resolves with `null`,
|
|
13
|
+
* so a question can never block the pipeline for good.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.PendingQuestions = exports.ASK_TIMEOUT_MS = exports.ANY_SOURCE = void 0;
|
|
17
|
+
/** Key for a question that any source may answer (a broadcast question). */
|
|
18
|
+
exports.ANY_SOURCE = '*';
|
|
19
|
+
/** Default time to wait for an answer before giving up. */
|
|
20
|
+
exports.ASK_TIMEOUT_MS = 60_000;
|
|
21
|
+
class PendingQuestions {
|
|
22
|
+
defaultTimeoutMs;
|
|
23
|
+
/** Every armed key points at its entry; several keys can share one (a question asked in two rooms). */
|
|
24
|
+
byKey = new Map();
|
|
25
|
+
constructor(defaultTimeoutMs = exports.ASK_TIMEOUT_MS) {
|
|
26
|
+
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Arm `question` for `keys` (empty → {@link ANY_SOURCE}) and resolve with the first answer from one of
|
|
30
|
+
* them, or with `null` on timeout. Arming a key that already holds a question cancels that older one:
|
|
31
|
+
* the newest question is the one the user just heard, so it has to be the one that wins.
|
|
32
|
+
*/
|
|
33
|
+
ask(keys, question, timeoutMs) {
|
|
34
|
+
const unique = [...new Set(keys.map(k => String(k ?? '').trim()).filter(Boolean))];
|
|
35
|
+
if (!unique.length) {
|
|
36
|
+
unique.push(exports.ANY_SOURCE);
|
|
37
|
+
}
|
|
38
|
+
for (const key of unique) {
|
|
39
|
+
this.cancel(key);
|
|
40
|
+
}
|
|
41
|
+
return new Promise(resolve => {
|
|
42
|
+
const entry = {
|
|
43
|
+
keys: unique,
|
|
44
|
+
question,
|
|
45
|
+
asked: Date.now(),
|
|
46
|
+
timer: null,
|
|
47
|
+
done: false,
|
|
48
|
+
settle: resolve,
|
|
49
|
+
};
|
|
50
|
+
// Deliberately not unref'd: the timeout is what releases the caller, so it has to fire. It is
|
|
51
|
+
// cleared on an answer, on cancel, and by cancelAll() on unload, so nothing is held for long.
|
|
52
|
+
entry.timer = setTimeout(() => this.finish(entry, null), timeoutMs ?? this.defaultTimeoutMs);
|
|
53
|
+
for (const key of unique) {
|
|
54
|
+
this.byKey.set(key, entry);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Consume an utterance as the answer to an open question. Returns true when it was taken — the caller
|
|
60
|
+
* must then stop processing it. Blank text is never an answer (a dropped utterance would otherwise
|
|
61
|
+
* silently satisfy the question).
|
|
62
|
+
*/
|
|
63
|
+
deliver(source, text) {
|
|
64
|
+
if (!String(text ?? '').trim()) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const entry = this.find(source);
|
|
68
|
+
if (!entry) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
this.finish(entry, text);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/** Whether an open question would take the next utterance from `source`. */
|
|
75
|
+
has(source) {
|
|
76
|
+
return !!this.find(source);
|
|
77
|
+
}
|
|
78
|
+
/** The open question `source` is expected to answer, if any (for logs). */
|
|
79
|
+
question(source) {
|
|
80
|
+
return this.find(source)?.question ?? '';
|
|
81
|
+
}
|
|
82
|
+
/** Give up on the question armed for `key` (resolves it with `null`). */
|
|
83
|
+
cancel(key) {
|
|
84
|
+
const entry = this.byKey.get(key);
|
|
85
|
+
if (!entry) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
this.finish(entry, null);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/** Give up on every open question — on unload, so no caller is left hanging. */
|
|
92
|
+
cancelAll() {
|
|
93
|
+
for (const entry of [...new Set(this.byKey.values())]) {
|
|
94
|
+
this.finish(entry, null);
|
|
95
|
+
}
|
|
96
|
+
this.byKey.clear();
|
|
97
|
+
}
|
|
98
|
+
/** All open questions (one entry per question, not per key). */
|
|
99
|
+
list() {
|
|
100
|
+
return [...new Set(this.byKey.values())].map(e => ({
|
|
101
|
+
question: e.question,
|
|
102
|
+
keys: [...e.keys],
|
|
103
|
+
asked: e.asked,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
/** The entry responsible for `source`: armed for it by name, else the broadcast one. */
|
|
107
|
+
find(source) {
|
|
108
|
+
const key = String(source ?? '').trim();
|
|
109
|
+
return (key ? this.byKey.get(key) : undefined) ?? this.byKey.get(exports.ANY_SOURCE);
|
|
110
|
+
}
|
|
111
|
+
finish(entry, answer) {
|
|
112
|
+
if (entry.done) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
entry.done = true;
|
|
116
|
+
if (entry.timer) {
|
|
117
|
+
clearTimeout(entry.timer);
|
|
118
|
+
entry.timer = null;
|
|
119
|
+
}
|
|
120
|
+
for (const key of entry.keys) {
|
|
121
|
+
// Only drop keys still pointing here — a newer question may already have taken one over.
|
|
122
|
+
if (this.byKey.get(key) === entry) {
|
|
123
|
+
this.byKey.delete(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
entry.settle(answer);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.PendingQuestions = PendingQuestions;
|
|
130
|
+
//# sourceMappingURL=ask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask.js","sourceRoot":"","sources":["../../src/lib/ask.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pending questions — the other direction of a conversation: the adapter asks and waits for the answer.\n *\n * Everywhere else the user starts the exchange. Here a script, a timer or (later) a trigger asks\n * something (\"The fryer has been on for 5 hours. Shall I switch it off?\") and the *next* utterance from\n * that source is the answer: it must not go to the NLU or the LLM, which would treat a bare \"yes\" as a\n * command and swallow it.\n *\n * A question is armed for one or more **sources** — the same key the answer pipeline sees: a satellite's\n * device name, `'chat'`, `'telegram:<user>'`, … — or for {@link ANY_SOURCE} when it was broadcast and\n * whoever hears it may answer. If nothing arrives within the timeout the promise resolves with `null`,\n * so a question can never block the pipeline for good.\n */\n\n/** Key for a question that any source may answer (a broadcast question). */\nexport const ANY_SOURCE = '*';\n\n/** Default time to wait for an answer before giving up. */\nexport const ASK_TIMEOUT_MS = 60_000;\n\ninterface Entry {\n keys: string[];\n question: string;\n asked: number;\n timer: NodeJS.Timeout | null;\n done: boolean;\n settle: (answer: string | null) => void;\n}\n\n/** One open question, for logging and diagnostics. */\nexport interface PendingInfo {\n question: string;\n /** Sources that may answer it (`'*'` = any). */\n keys: string[];\n /** When it was asked (ms epoch). */\n asked: number;\n}\n\nexport class PendingQuestions {\n /** Every armed key points at its entry; several keys can share one (a question asked in two rooms). */\n private readonly byKey = new Map<string, Entry>();\n\n constructor(private readonly defaultTimeoutMs = ASK_TIMEOUT_MS) {}\n\n /**\n * Arm `question` for `keys` (empty → {@link ANY_SOURCE}) and resolve with the first answer from one of\n * them, or with `null` on timeout. Arming a key that already holds a question cancels that older one:\n * the newest question is the one the user just heard, so it has to be the one that wins.\n */\n ask(keys: string[], question: string, timeoutMs?: number): Promise<string | null> {\n const unique = [...new Set(keys.map(k => String(k ?? '').trim()).filter(Boolean))];\n if (!unique.length) {\n unique.push(ANY_SOURCE);\n }\n for (const key of unique) {\n this.cancel(key);\n }\n return new Promise<string | null>(resolve => {\n const entry: Entry = {\n keys: unique,\n question,\n asked: Date.now(),\n timer: null,\n done: false,\n settle: resolve,\n };\n // Deliberately not unref'd: the timeout is what releases the caller, so it has to fire. It is\n // cleared on an answer, on cancel, and by cancelAll() on unload, so nothing is held for long.\n entry.timer = setTimeout(() => this.finish(entry, null), timeoutMs ?? this.defaultTimeoutMs);\n for (const key of unique) {\n this.byKey.set(key, entry);\n }\n });\n }\n\n /**\n * Consume an utterance as the answer to an open question. Returns true when it was taken — the caller\n * must then stop processing it. Blank text is never an answer (a dropped utterance would otherwise\n * silently satisfy the question).\n */\n deliver(source: string, text: string): boolean {\n if (!String(text ?? '').trim()) {\n return false;\n }\n const entry = this.find(source);\n if (!entry) {\n return false;\n }\n this.finish(entry, text);\n return true;\n }\n\n /** Whether an open question would take the next utterance from `source`. */\n has(source: string): boolean {\n return !!this.find(source);\n }\n\n /** The open question `source` is expected to answer, if any (for logs). */\n question(source: string): string {\n return this.find(source)?.question ?? '';\n }\n\n /** Give up on the question armed for `key` (resolves it with `null`). */\n cancel(key: string): boolean {\n const entry = this.byKey.get(key);\n if (!entry) {\n return false;\n }\n this.finish(entry, null);\n return true;\n }\n\n /** Give up on every open question — on unload, so no caller is left hanging. */\n cancelAll(): void {\n for (const entry of [...new Set(this.byKey.values())]) {\n this.finish(entry, null);\n }\n this.byKey.clear();\n }\n\n /** All open questions (one entry per question, not per key). */\n list(): PendingInfo[] {\n return [...new Set(this.byKey.values())].map(e => ({\n question: e.question,\n keys: [...e.keys],\n asked: e.asked,\n }));\n }\n\n /** The entry responsible for `source`: armed for it by name, else the broadcast one. */\n private find(source: string): Entry | undefined {\n const key = String(source ?? '').trim();\n return (key ? this.byKey.get(key) : undefined) ?? this.byKey.get(ANY_SOURCE);\n }\n\n private finish(entry: Entry, answer: string | null): void {\n if (entry.done) {\n return;\n }\n entry.done = true;\n if (entry.timer) {\n clearTimeout(entry.timer);\n entry.timer = null;\n }\n for (const key of entry.keys) {\n // Only drop keys still pointing here — a newer question may already have taken one over.\n if (this.byKey.get(key) === entry) {\n this.byKey.delete(key);\n }\n }\n entry.settle(answer);\n }\n}\n"]}
|
package/build/lib/llm.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.LlmAgent = exports.PROVIDER_PRESETS = void 0;
|
|
7
7
|
exports.resolveProvider = resolveProvider;
|
|
8
|
+
exports.effectiveApiKey = effectiveApiKey;
|
|
8
9
|
const openai_1 = __importDefault(require("openai"));
|
|
9
10
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
10
11
|
/** Per-provider SDK, endpoint and default model. Gemini/DeepSeek expose OpenAI-compatible endpoints. */
|
|
@@ -28,6 +29,14 @@ function resolveProvider(provider, configBaseUrl) {
|
|
|
28
29
|
defaultModel: preset.defaultModel,
|
|
29
30
|
};
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* API key to use for `provider`: a local OpenAI-compatible server (the `custom` provider) usually has no
|
|
34
|
+
* key at all, but the OpenAI SDK refuses to start without one — a placeholder goes into the
|
|
35
|
+
* `Authorization` header, which such servers ignore. Empty for every other provider without a key.
|
|
36
|
+
*/
|
|
37
|
+
function effectiveApiKey(provider, apiKey) {
|
|
38
|
+
return apiKey || (provider === 'custom' ? 'no-key' : '');
|
|
39
|
+
}
|
|
31
40
|
/**
|
|
32
41
|
* Provider-agnostic LLM agent with tool-calling.
|
|
33
42
|
*
|
|
@@ -40,7 +49,9 @@ function resolveProvider(provider, configBaseUrl) {
|
|
|
40
49
|
*/
|
|
41
50
|
class LlmAgent {
|
|
42
51
|
provider;
|
|
43
|
-
model;
|
|
52
|
+
/** Configured model; empty means "ask the server" (see {@link ensureModel}). */
|
|
53
|
+
configuredModel;
|
|
54
|
+
modelLookup = null;
|
|
44
55
|
systemPrompt;
|
|
45
56
|
maxTokens;
|
|
46
57
|
tools;
|
|
@@ -51,7 +62,7 @@ class LlmAgent {
|
|
|
51
62
|
anthropic;
|
|
52
63
|
constructor(opts) {
|
|
53
64
|
this.provider = opts.provider === 'anthropic' ? 'anthropic' : 'openai';
|
|
54
|
-
this.
|
|
65
|
+
this.configuredModel = (opts.model || '').trim();
|
|
55
66
|
this.systemPrompt = opts.systemPrompt || '';
|
|
56
67
|
this.maxTokens = opts.maxTokens || 1024;
|
|
57
68
|
this.tools = opts.tools || [];
|
|
@@ -64,11 +75,42 @@ class LlmAgent {
|
|
|
64
75
|
this.openai = new openai_1.default({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });
|
|
65
76
|
}
|
|
66
77
|
}
|
|
78
|
+
/** The model requests go to — empty until {@link ensureModel} has looked it up, if none was configured. */
|
|
79
|
+
get model() {
|
|
80
|
+
return this.configuredModel;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Make sure there is a model to send. A local OpenAI-compatible server (Ollama, LM Studio, llama.cpp
|
|
84
|
+
* server, …) is usually set up with the `custom` provider and no model typed in — and sending
|
|
85
|
+
* `"model": ""` gets a 400 from the server ("unknown provider for model" and the like) that says
|
|
86
|
+
* nothing about the real cause. So an empty model is looked up once from the server's `/models` list
|
|
87
|
+
* (with one model loaded, that is the one), and a server that lists nothing gets a clear error.
|
|
88
|
+
*/
|
|
89
|
+
async ensureModel() {
|
|
90
|
+
if (this.configuredModel) {
|
|
91
|
+
return this.configuredModel;
|
|
92
|
+
}
|
|
93
|
+
this.modelLookup ||= this.listModels()
|
|
94
|
+
.then(models => {
|
|
95
|
+
if (!models.length) {
|
|
96
|
+
throw new Error('no model configured and the server lists none');
|
|
97
|
+
}
|
|
98
|
+
this.configuredModel = models[0];
|
|
99
|
+
this.log.info(`No model configured — using "${models[0]}" from the server${models.length > 1 ? ` (available: ${models.join(', ')})` : ''}. Select a model in the adapter settings to choose another.`);
|
|
100
|
+
return models[0];
|
|
101
|
+
})
|
|
102
|
+
.catch(e => {
|
|
103
|
+
this.modelLookup = null; // let the next request try again (server may still be starting)
|
|
104
|
+
throw new Error(`No model configured — select one in the adapter settings (lookup failed: ${e.message})`);
|
|
105
|
+
});
|
|
106
|
+
return this.modelLookup;
|
|
107
|
+
}
|
|
67
108
|
/**
|
|
68
109
|
* `systemPrompt` overrides the configured one for this call (e.g. to inject a device list).
|
|
69
110
|
* `history` are prior turns (user/assistant) prepended so the model can resolve follow-ups.
|
|
70
111
|
*/
|
|
71
112
|
async ask(question, systemPrompt, history = []) {
|
|
113
|
+
await this.ensureModel();
|
|
72
114
|
this.log.debug(`llm ask [${this.provider}/${this.model}] (${history.length} history): ${question}`);
|
|
73
115
|
const started = Date.now();
|
|
74
116
|
const sys = systemPrompt ?? this.systemPrompt;
|
|
@@ -93,19 +135,17 @@ class LlmAgent {
|
|
|
93
135
|
}
|
|
94
136
|
this.log.debug(`${label}: ${text.length > max ? `${text.slice(0, max)}…[${text.length}]` : text}`);
|
|
95
137
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const prompt = `Translate the following smart-home device or room name into ${targetLanguage}. ` +
|
|
103
|
-
`Reply with ONLY the translation — no quotes, no punctuation, no explanation.\n\nName: ${trimmed}`;
|
|
138
|
+
/**
|
|
139
|
+
* One prompt in, one line out — no tools, no history, no system prompt. The small side jobs
|
|
140
|
+
* (translate/rephrase/classify) all run through here.
|
|
141
|
+
*/
|
|
142
|
+
async complete(prompt, maxTokens) {
|
|
143
|
+
await this.ensureModel();
|
|
104
144
|
if (this.provider === 'anthropic') {
|
|
105
145
|
const client = this.anthropic;
|
|
106
146
|
const resp = await client.messages.create({
|
|
107
147
|
model: this.model,
|
|
108
|
-
max_tokens:
|
|
148
|
+
max_tokens: maxTokens,
|
|
109
149
|
messages: [{ role: 'user', content: prompt }],
|
|
110
150
|
});
|
|
111
151
|
return resp.content
|
|
@@ -117,18 +157,100 @@ class LlmAgent {
|
|
|
117
157
|
const client = this.openai;
|
|
118
158
|
const resp = await client.chat.completions.create({
|
|
119
159
|
model: this.model,
|
|
120
|
-
max_tokens:
|
|
160
|
+
max_tokens: maxTokens,
|
|
121
161
|
messages: [{ role: 'user', content: prompt }],
|
|
122
162
|
});
|
|
123
163
|
return String(resp.choices[0].message.content || '').trim();
|
|
124
164
|
}
|
|
165
|
+
/** Translate a short device/room name into `targetLanguage` (single completion, no tools). */
|
|
166
|
+
async translate(text, targetLanguage) {
|
|
167
|
+
const trimmed = (text || '').trim();
|
|
168
|
+
if (!trimmed) {
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
return this.complete(`Translate the following smart-home device or room name into ${targetLanguage}. ` +
|
|
172
|
+
`Reply with ONLY the translation — no quotes, no punctuation, no explanation.\n\nName: ${trimmed}`, 64);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Reword a canned sentence (a trigger's announcement or question) so it sounds spoken rather than
|
|
176
|
+
* templated, keeping every concrete detail. `persona` is the adapter's system prompt, so the reworded
|
|
177
|
+
* line still sounds like the same assistant. Returns the input unchanged on any problem — a trigger
|
|
178
|
+
* must still fire when the model is unavailable.
|
|
179
|
+
*/
|
|
180
|
+
async rephrase(text, language, persona = '') {
|
|
181
|
+
const trimmed = (text || '').trim();
|
|
182
|
+
if (!trimmed) {
|
|
183
|
+
return '';
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const result = await this.complete(`${persona ? `${persona.trim()}\n\n` : ''}Reword the following sentence so it sounds natural and ` +
|
|
187
|
+
`spoken${language ? `, in ${language}` : ''}. Keep every concrete detail (names, numbers, ` +
|
|
188
|
+
`versions, times) and keep it one short sentence. If it is a question, keep it a question. ` +
|
|
189
|
+
`Reply with ONLY the sentence.\n\nSentence: ${trimmed}`, 200);
|
|
190
|
+
return result || trimmed;
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
this.log.warn(`rephrase failed, using the original text: ${e.message}`);
|
|
194
|
+
return trimmed;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Reword an ioBroker system notification into one spoken sentence, in the given `tone` (see
|
|
199
|
+
* `notifications.toneFor`). Separate from {@link rephrase} because of the hints below: they are
|
|
200
|
+
* specific to how ioBroker words its notifications, and each one comes from a real text that was
|
|
201
|
+
* read out wrongly — version numbers mistaken for dates, origin prefixes spelled out letter by
|
|
202
|
+
* letter, an empty `{}` announced as a problem. Returns the input unchanged on any failure.
|
|
203
|
+
*/
|
|
204
|
+
async rewordNotification(text, tone, language, persona = '') {
|
|
205
|
+
const trimmed = (text || '').trim();
|
|
206
|
+
if (!trimmed) {
|
|
207
|
+
return '';
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const result = await this.complete(`${persona ? `${persona.trim()}\n\n` : ''}Reword the following system notification into one ` +
|
|
211
|
+
`short, natural, spoken sentence${language ? `, in ${language}` : ''}. ` +
|
|
212
|
+
`Keep every concrete detail such as adapter names and version numbers. ` +
|
|
213
|
+
`Dates like M/D/YYYY or M/D/YYYY, H:MM:SS AM/PM are timestamps — say them as a date or ` +
|
|
214
|
+
`a time, never as a version number. Technical prefixes such as ` +
|
|
215
|
+
`"system.host.XYZ: adapter.0:" only say where it came from and need not be repeated. ` +
|
|
216
|
+
`Empty values such as "onedrive: {}" mean there is nothing wrong there — only mention ` +
|
|
217
|
+
`them if it matters. ${tone} Reply with ONLY the sentence.\n\nNotification: ${trimmed}`, 300);
|
|
218
|
+
return result || trimmed;
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
this.log.warn(`notification rewording failed, speaking the original: ${e.message}`);
|
|
222
|
+
return trimmed;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Does `answer` express `category` ("agreement", "Zustimmung", "refusal", …)? This is the classifier
|
|
227
|
+
* behind a trigger's response rules, where a spoken "ja, mach das" has to map onto one rule. Decided
|
|
228
|
+
* by the model because the wording is open-ended; a failure answers `false`, so an unclassifiable
|
|
229
|
+
* reply falls through to the trigger's fallback rule instead of acting on a guess.
|
|
230
|
+
*/
|
|
231
|
+
async classify(answer, category) {
|
|
232
|
+
const text = (answer || '').trim();
|
|
233
|
+
const want = (category || '').trim();
|
|
234
|
+
if (!text || !want) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const verdict = await this.complete(`Does the following reply express "${want}"? Consider the meaning, in any language, ` +
|
|
239
|
+
`including short or indirect phrasings. Answer with exactly YES or NO.\n\nReply: ${text}`, 8);
|
|
240
|
+
return /^\s*(yes|ja|да)\b/i.test(verdict);
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
this.log.warn(`classify failed ('${want}'): ${e.message}`);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
125
247
|
/** Lightweight validation of the provider credentials (used by the settings Test button). */
|
|
126
248
|
async testConnection() {
|
|
127
249
|
try {
|
|
128
250
|
if (this.provider === 'anthropic') {
|
|
129
251
|
const client = this.anthropic;
|
|
130
252
|
await client.messages.create({
|
|
131
|
-
model: this.
|
|
253
|
+
model: await this.ensureModel(),
|
|
132
254
|
max_tokens: 1,
|
|
133
255
|
messages: [{ role: 'user', content: 'ping' }],
|
|
134
256
|
});
|
package/build/lib/llm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/lib/llm.ts"],"names":[],"mappings":"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","sourcesContent":["import OpenAI from 'openai';\nimport Anthropic from '@anthropic-ai/sdk';\nimport type { Tool } from './tools';\n\n/** All selectable LLM providers. gemini/deepseek/custom all use the OpenAI-compatible SDK path. */\nexport type Provider = 'openai' | 'anthropic' | 'gemini' | 'deepseek' | 'custom';\n\ninterface ProviderPreset {\n /** Which vendor SDK to use. */\n sdk: 'openai' | 'anthropic';\n /** Fixed API base URL ('' = SDK default; for 'custom' the user's baseUrl is used instead). */\n baseUrl: string;\n /** Default model when none is configured. */\n defaultModel: string;\n}\n\n/** Per-provider SDK, endpoint and default model. Gemini/DeepSeek expose OpenAI-compatible endpoints. */\nexport const PROVIDER_PRESETS: Record<Provider, ProviderPreset> = {\n openai: { sdk: 'openai', baseUrl: '', defaultModel: 'gpt-4o-mini' },\n anthropic: { sdk: 'anthropic', baseUrl: '', defaultModel: 'claude-sonnet-4-6' },\n gemini: {\n sdk: 'openai',\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/',\n defaultModel: 'gemini-2.0-flash',\n },\n deepseek: { sdk: 'openai', baseUrl: 'https://api.deepseek.com', defaultModel: 'deepseek-chat' },\n custom: { sdk: 'openai', baseUrl: '', defaultModel: '' },\n};\n\n/** Resolve the effective SDK, base URL and default model for a provider (custom uses `configBaseUrl`). */\nexport function resolveProvider(\n provider: string | undefined,\n configBaseUrl?: string,\n): { sdk: 'openai' | 'anthropic'; baseUrl: string; defaultModel: string } {\n const preset = PROVIDER_PRESETS[(provider as Provider) || 'openai'] || PROVIDER_PRESETS.openai;\n return {\n sdk: preset.sdk,\n baseUrl: provider === 'custom' ? configBaseUrl || '' : preset.baseUrl,\n defaultModel: preset.defaultModel,\n };\n}\n\nexport interface LlmAgentOptions {\n provider: Provider;\n apiKey: string;\n model: string;\n baseUrl?: string;\n systemPrompt?: string;\n maxTokens?: number;\n tools: Tool[];\n log: ioBroker.Logger;\n}\n\n/**\n * Provider-agnostic LLM agent with tool-calling.\n *\n * `ask(question)` runs the model, executes any tool calls it requests, feeds the\n * results back, and repeats until the model returns a final text answer.\n *\n * The request/response plumbing of the vendor SDKs is intentionally loosely typed\n * (`any`) so the code stays robust across minor SDK type changes; the public API\n * (options, `ask`, tools) is fully typed.\n */\nexport class LlmAgent {\n public readonly provider: 'openai' | 'anthropic';\n public readonly model: string;\n\n private readonly systemPrompt: string;\n private readonly maxTokens: number;\n private readonly tools: Tool[];\n private readonly toolMap: Record<string, Tool>;\n private readonly log: ioBroker.Logger;\n private readonly maxRounds = 6; // safety cap against tool-call loops\n\n private readonly openai?: OpenAI;\n private readonly anthropic?: Anthropic;\n\n constructor(opts: LlmAgentOptions) {\n this.provider = opts.provider === 'anthropic' ? 'anthropic' : 'openai';\n this.model = opts.model;\n this.systemPrompt = opts.systemPrompt || '';\n this.maxTokens = opts.maxTokens || 1024;\n this.tools = opts.tools || [];\n this.log = opts.log;\n this.toolMap = Object.fromEntries(this.tools.map(t => [t.name, t]));\n\n if (this.provider === 'anthropic') {\n this.anthropic = new Anthropic({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });\n } else {\n this.openai = new OpenAI({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });\n }\n }\n\n /**\n * `systemPrompt` overrides the configured one for this call (e.g. to inject a device list).\n * `history` are prior turns (user/assistant) prepended so the model can resolve follow-ups.\n */\n public async ask(\n question: string,\n systemPrompt?: string,\n history: { role: 'user' | 'assistant'; content: string }[] = [],\n ): Promise<string> {\n this.log.debug(`llm ask [${this.provider}/${this.model}] (${history.length} history): ${question}`);\n const started = Date.now();\n const sys = systemPrompt ?? this.systemPrompt;\n const answer =\n this.provider === 'anthropic'\n ? await this.askAnthropic(question, sys, history)\n : await this.askOpenAI(question, sys, history);\n this.log.debug(`llm answer (${Date.now() - started} ms): ${answer}`);\n return answer;\n }\n\n /** Compact, truncated JSON for debug logging of LLM requests/responses. */\n private dump(label: string, payload: unknown): void {\n if (this.log.level !== 'silly' && this.log.level !== 'debug') {\n return; // avoid stringifying large payloads when debug logging is off\n }\n const max = this.log.level === 'silly' ? 8000 : 1500;\n let text: string;\n try {\n text = JSON.stringify(payload);\n } catch {\n text = String(payload);\n }\n this.log.debug(`${label}: ${text.length > max ? `${text.slice(0, max)}…[${text.length}]` : text}`);\n }\n\n /** Translate a short device/room name into `targetLanguage` (single completion, no tools). */\n public async translate(text: string, targetLanguage: string): Promise<string> {\n const trimmed = (text || '').trim();\n if (!trimmed) {\n return '';\n }\n const prompt =\n `Translate the following smart-home device or room name into ${targetLanguage}. ` +\n `Reply with ONLY the translation — no quotes, no punctuation, no explanation.\\n\\nName: ${trimmed}`;\n if (this.provider === 'anthropic') {\n const client = this.anthropic as Anthropic;\n const resp: any = await client.messages.create({\n model: this.model,\n max_tokens: 64,\n messages: [{ role: 'user', content: prompt }],\n });\n return resp.content\n .filter((b: any) => b.type === 'text')\n .map((b: any) => b.text)\n .join('')\n .trim();\n }\n const client = this.openai as OpenAI;\n const resp: any = await client.chat.completions.create({\n model: this.model,\n max_tokens: 64,\n messages: [{ role: 'user', content: prompt }],\n });\n return String(resp.choices[0].message.content || '').trim();\n }\n\n /** Lightweight validation of the provider credentials (used by the settings Test button). */\n public async testConnection(): Promise<{ ok: boolean; error?: string }> {\n try {\n if (this.provider === 'anthropic') {\n const client = this.anthropic as Anthropic;\n await client.messages.create({\n model: this.model,\n max_tokens: 1,\n messages: [{ role: 'user', content: 'ping' }],\n });\n } else {\n const client = this.openai as OpenAI;\n await client.models.list();\n }\n return { ok: true };\n } catch (e) {\n return { ok: false, error: (e as Error).message };\n }\n }\n\n /** List the provider's available model ids (for the settings autocomplete). */\n public async listModels(): Promise<string[]> {\n if (this.provider === 'anthropic') {\n const client = this.anthropic as Anthropic;\n const res = (await client.models.list()) as unknown as { data?: { id: string }[] };\n return (res.data || []).map(m => m.id);\n }\n const client = this.openai as OpenAI;\n const res = (await client.models.list()) as unknown as { data?: { id: string }[] };\n // OpenAI lists many non-chat models (embeddings, tts, whisper, …) — filter to chat-capable-ish.\n const EXCLUDE =\n /embedding|whisper|tts|audio|dall-e|moderation|image|realtime|transcribe|search|davinci|babbage|codex/i;\n return (res.data || [])\n .map(m => String(m.id))\n .filter(id => !EXCLUDE.test(id))\n .sort();\n }\n\n private async runTool(name: string, args: Record<string, unknown>): Promise<unknown> {\n const tool = this.toolMap[name];\n if (!tool) {\n return { error: `unknown tool: ${name}` };\n }\n try {\n const result = await tool.run(args || {});\n this.log.info(`tool ${name}(${JSON.stringify(args)}) → ${JSON.stringify(result).slice(0, 200)}`);\n return result;\n } catch (e) {\n this.log.warn(`tool ${name} failed: ${(e as Error).message}`);\n return { error: (e as Error).message };\n }\n }\n\n // ── OpenAI (Chat Completions + function tools) ──────────────────────────\n private async askOpenAI(\n question: string,\n systemPrompt: string,\n history: { role: 'user' | 'assistant'; content: string }[] = [],\n ): Promise<string> {\n const client = this.openai as OpenAI;\n const tools = this.tools.map(t => ({\n type: 'function' as const,\n function: { name: t.name, description: t.description, parameters: t.parameters },\n }));\n\n const messages: any[] = [];\n if (systemPrompt) {\n messages.push({ role: 'system', content: systemPrompt });\n }\n for (const t of history) {\n messages.push({ role: t.role, content: t.content });\n }\n messages.push({ role: 'user', content: question });\n\n for (let round = 0; round < this.maxRounds; round++) {\n const request = {\n model: this.model,\n messages,\n tools: tools.length ? tools : undefined,\n max_tokens: this.maxTokens,\n };\n this.dump(`llm → openai round ${round}`, request);\n const resp: any = await client.chat.completions.create(request);\n const msg = resp.choices[0].message;\n this.dump(`llm ← openai round ${round}`, { message: msg, usage: resp.usage });\n messages.push(msg);\n\n if (!msg.tool_calls || !msg.tool_calls.length) {\n return String(msg.content || '').trim();\n }\n\n for (const tc of msg.tool_calls) {\n let args: Record<string, unknown> = {};\n try {\n args = JSON.parse(tc.function.arguments || '{}');\n } catch {\n /* leave empty */\n }\n const result = await this.runTool(tc.function.name, args);\n messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) });\n }\n }\n return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';\n }\n\n /**\n * Keep a single cache breakpoint on the last content block of the last message (Anthropic allows\n * ≤4 total; we also mark system + last tool). Strips prior message breakpoints so we never exceed it.\n */\n private markLastMessageForCache(messages: any[]): void {\n for (const m of messages) {\n if (Array.isArray(m.content)) {\n for (const b of m.content) {\n if (b && typeof b === 'object') {\n delete b.cache_control;\n }\n }\n }\n }\n const last = messages[messages.length - 1];\n if (!last) {\n return;\n }\n if (typeof last.content === 'string') {\n last.content = [{ type: 'text', text: last.content, cache_control: { type: 'ephemeral' } }];\n } else if (Array.isArray(last.content) && last.content.length) {\n const b = last.content[last.content.length - 1];\n if (b && typeof b === 'object') {\n b.cache_control = { type: 'ephemeral' };\n }\n }\n }\n\n // ── Anthropic (Messages API + tool_use) ─────────────────────────────────\n private async askAnthropic(\n question: string,\n systemPrompt: string,\n history: { role: 'user' | 'assistant'; content: string }[] = [],\n ): Promise<string> {\n const client = this.anthropic as Anthropic;\n const tools: any[] = this.tools.map(t => ({\n name: t.name,\n description: t.description,\n input_schema: t.parameters as Anthropic.Tool.InputSchema,\n }));\n // Prompt caching: mark the last tool → the static prefix (system + all tools) is cached and\n // read back on every later round/request (5-min TTL), cutting latency & cost on the big context.\n if (tools.length) {\n tools[tools.length - 1] = { ...tools[tools.length - 1], cache_control: { type: 'ephemeral' } };\n }\n const system: any = systemPrompt\n ? [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }]\n : undefined;\n\n const messages: any[] = [\n ...history.map(t => ({ role: t.role, content: t.content })),\n { role: 'user', content: question },\n ];\n\n for (let round = 0; round < this.maxRounds; round++) {\n // Cache the growing conversation prefix too (e.g. the large list_devices result): move a\n // single cache breakpoint to the last content block of the last message each round.\n this.markLastMessageForCache(messages);\n const request = {\n model: this.model,\n max_tokens: this.maxTokens,\n system,\n tools: tools.length ? tools : undefined,\n messages,\n };\n this.dump(`llm → anthropic round ${round}`, request);\n const resp: any = await client.messages.create(request);\n this.dump(`llm ← anthropic round ${round}`, { content: resp.content, usage: resp.usage });\n\n const toolUses = resp.content.filter((b: any) => b.type === 'tool_use');\n if (!toolUses.length) {\n return resp.content\n .filter((b: any) => b.type === 'text')\n .map((b: any) => b.text)\n .join('')\n .trim();\n }\n\n messages.push({ role: 'assistant', content: resp.content });\n const toolResults = [];\n for (const tu of toolUses) {\n const result = await this.runTool(tu.name, tu.input);\n toolResults.push({ type: 'tool_result', tool_use_id: tu.id, content: JSON.stringify(result) });\n }\n messages.push({ role: 'user', content: toolResults });\n }\n return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/lib/llm.ts"],"names":[],"mappings":"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","sourcesContent":["import OpenAI from 'openai';\nimport Anthropic from '@anthropic-ai/sdk';\nimport type { Tool } from './tools';\n\n/** All selectable LLM providers. gemini/deepseek/custom all use the OpenAI-compatible SDK path. */\nexport type Provider = 'openai' | 'anthropic' | 'gemini' | 'deepseek' | 'custom';\n\ninterface ProviderPreset {\n /** Which vendor SDK to use. */\n sdk: 'openai' | 'anthropic';\n /** Fixed API base URL ('' = SDK default; for 'custom' the user's baseUrl is used instead). */\n baseUrl: string;\n /** Default model when none is configured. */\n defaultModel: string;\n}\n\n/** Per-provider SDK, endpoint and default model. Gemini/DeepSeek expose OpenAI-compatible endpoints. */\nexport const PROVIDER_PRESETS: Record<Provider, ProviderPreset> = {\n openai: { sdk: 'openai', baseUrl: '', defaultModel: 'gpt-4o-mini' },\n anthropic: { sdk: 'anthropic', baseUrl: '', defaultModel: 'claude-sonnet-4-6' },\n gemini: {\n sdk: 'openai',\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/',\n defaultModel: 'gemini-2.0-flash',\n },\n deepseek: { sdk: 'openai', baseUrl: 'https://api.deepseek.com', defaultModel: 'deepseek-chat' },\n custom: { sdk: 'openai', baseUrl: '', defaultModel: '' },\n};\n\n/** Resolve the effective SDK, base URL and default model for a provider (custom uses `configBaseUrl`). */\nexport function resolveProvider(\n provider: string | undefined,\n configBaseUrl?: string,\n): { sdk: 'openai' | 'anthropic'; baseUrl: string; defaultModel: string } {\n const preset = PROVIDER_PRESETS[(provider as Provider) || 'openai'] || PROVIDER_PRESETS.openai;\n return {\n sdk: preset.sdk,\n baseUrl: provider === 'custom' ? configBaseUrl || '' : preset.baseUrl,\n defaultModel: preset.defaultModel,\n };\n}\n\n/**\n * API key to use for `provider`: a local OpenAI-compatible server (the `custom` provider) usually has no\n * key at all, but the OpenAI SDK refuses to start without one — a placeholder goes into the\n * `Authorization` header, which such servers ignore. Empty for every other provider without a key.\n */\nexport function effectiveApiKey(provider: string | undefined, apiKey: string): string {\n return apiKey || (provider === 'custom' ? 'no-key' : '');\n}\n\nexport interface LlmAgentOptions {\n provider: Provider;\n apiKey: string;\n model: string;\n baseUrl?: string;\n systemPrompt?: string;\n maxTokens?: number;\n tools: Tool[];\n log: ioBroker.Logger;\n}\n\n/**\n * Provider-agnostic LLM agent with tool-calling.\n *\n * `ask(question)` runs the model, executes any tool calls it requests, feeds the\n * results back, and repeats until the model returns a final text answer.\n *\n * The request/response plumbing of the vendor SDKs is intentionally loosely typed\n * (`any`) so the code stays robust across minor SDK type changes; the public API\n * (options, `ask`, tools) is fully typed.\n */\nexport class LlmAgent {\n public readonly provider: 'openai' | 'anthropic';\n /** Configured model; empty means \"ask the server\" (see {@link ensureModel}). */\n private configuredModel: string;\n private modelLookup: Promise<string> | null = null;\n\n private readonly systemPrompt: string;\n private readonly maxTokens: number;\n private readonly tools: Tool[];\n private readonly toolMap: Record<string, Tool>;\n private readonly log: ioBroker.Logger;\n private readonly maxRounds = 6; // safety cap against tool-call loops\n\n private readonly openai?: OpenAI;\n private readonly anthropic?: Anthropic;\n\n constructor(opts: LlmAgentOptions) {\n this.provider = opts.provider === 'anthropic' ? 'anthropic' : 'openai';\n this.configuredModel = (opts.model || '').trim();\n this.systemPrompt = opts.systemPrompt || '';\n this.maxTokens = opts.maxTokens || 1024;\n this.tools = opts.tools || [];\n this.log = opts.log;\n this.toolMap = Object.fromEntries(this.tools.map(t => [t.name, t]));\n\n if (this.provider === 'anthropic') {\n this.anthropic = new Anthropic({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });\n } else {\n this.openai = new OpenAI({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });\n }\n }\n\n /** The model requests go to — empty until {@link ensureModel} has looked it up, if none was configured. */\n public get model(): string {\n return this.configuredModel;\n }\n\n /**\n * Make sure there is a model to send. A local OpenAI-compatible server (Ollama, LM Studio, llama.cpp\n * server, …) is usually set up with the `custom` provider and no model typed in — and sending\n * `\"model\": \"\"` gets a 400 from the server (\"unknown provider for model\" and the like) that says\n * nothing about the real cause. So an empty model is looked up once from the server's `/models` list\n * (with one model loaded, that is the one), and a server that lists nothing gets a clear error.\n */\n private async ensureModel(): Promise<string> {\n if (this.configuredModel) {\n return this.configuredModel;\n }\n this.modelLookup ||= this.listModels()\n .then(models => {\n if (!models.length) {\n throw new Error('no model configured and the server lists none');\n }\n this.configuredModel = models[0];\n this.log.info(\n `No model configured — using \"${models[0]}\" from the server${\n models.length > 1 ? ` (available: ${models.join(', ')})` : ''\n }. Select a model in the adapter settings to choose another.`,\n );\n return models[0];\n })\n .catch(e => {\n this.modelLookup = null; // let the next request try again (server may still be starting)\n throw new Error(\n `No model configured — select one in the adapter settings (lookup failed: ${(e as Error).message})`,\n );\n });\n return this.modelLookup;\n }\n\n /**\n * `systemPrompt` overrides the configured one for this call (e.g. to inject a device list).\n * `history` are prior turns (user/assistant) prepended so the model can resolve follow-ups.\n */\n public async ask(\n question: string,\n systemPrompt?: string,\n history: { role: 'user' | 'assistant'; content: string }[] = [],\n ): Promise<string> {\n await this.ensureModel();\n this.log.debug(`llm ask [${this.provider}/${this.model}] (${history.length} history): ${question}`);\n const started = Date.now();\n const sys = systemPrompt ?? this.systemPrompt;\n const answer =\n this.provider === 'anthropic'\n ? await this.askAnthropic(question, sys, history)\n : await this.askOpenAI(question, sys, history);\n this.log.debug(`llm answer (${Date.now() - started} ms): ${answer}`);\n return answer;\n }\n\n /** Compact, truncated JSON for debug logging of LLM requests/responses. */\n private dump(label: string, payload: unknown): void {\n if (this.log.level !== 'silly' && this.log.level !== 'debug') {\n return; // avoid stringifying large payloads when debug logging is off\n }\n const max = this.log.level === 'silly' ? 8000 : 1500;\n let text: string;\n try {\n text = JSON.stringify(payload);\n } catch {\n text = String(payload);\n }\n this.log.debug(`${label}: ${text.length > max ? `${text.slice(0, max)}…[${text.length}]` : text}`);\n }\n\n /**\n * One prompt in, one line out — no tools, no history, no system prompt. The small side jobs\n * (translate/rephrase/classify) all run through here.\n */\n private async complete(prompt: string, maxTokens: number): Promise<string> {\n await this.ensureModel();\n if (this.provider === 'anthropic') {\n const client = this.anthropic as Anthropic;\n const resp: any = await client.messages.create({\n model: this.model,\n max_tokens: maxTokens,\n messages: [{ role: 'user', content: prompt }],\n });\n return resp.content\n .filter((b: any) => b.type === 'text')\n .map((b: any) => b.text)\n .join('')\n .trim();\n }\n const client = this.openai as OpenAI;\n const resp: any = await client.chat.completions.create({\n model: this.model,\n max_tokens: maxTokens,\n messages: [{ role: 'user', content: prompt }],\n });\n return String(resp.choices[0].message.content || '').trim();\n }\n\n /** Translate a short device/room name into `targetLanguage` (single completion, no tools). */\n public async translate(text: string, targetLanguage: string): Promise<string> {\n const trimmed = (text || '').trim();\n if (!trimmed) {\n return '';\n }\n return this.complete(\n `Translate the following smart-home device or room name into ${targetLanguage}. ` +\n `Reply with ONLY the translation — no quotes, no punctuation, no explanation.\\n\\nName: ${trimmed}`,\n 64,\n );\n }\n\n /**\n * Reword a canned sentence (a trigger's announcement or question) so it sounds spoken rather than\n * templated, keeping every concrete detail. `persona` is the adapter's system prompt, so the reworded\n * line still sounds like the same assistant. Returns the input unchanged on any problem — a trigger\n * must still fire when the model is unavailable.\n */\n public async rephrase(text: string, language: string, persona = ''): Promise<string> {\n const trimmed = (text || '').trim();\n if (!trimmed) {\n return '';\n }\n try {\n const result = await this.complete(\n `${persona ? `${persona.trim()}\\n\\n` : ''}Reword the following sentence so it sounds natural and ` +\n `spoken${language ? `, in ${language}` : ''}. Keep every concrete detail (names, numbers, ` +\n `versions, times) and keep it one short sentence. If it is a question, keep it a question. ` +\n `Reply with ONLY the sentence.\\n\\nSentence: ${trimmed}`,\n 200,\n );\n return result || trimmed;\n } catch (e) {\n this.log.warn(`rephrase failed, using the original text: ${(e as Error).message}`);\n return trimmed;\n }\n }\n\n /**\n * Reword an ioBroker system notification into one spoken sentence, in the given `tone` (see\n * `notifications.toneFor`). Separate from {@link rephrase} because of the hints below: they are\n * specific to how ioBroker words its notifications, and each one comes from a real text that was\n * read out wrongly — version numbers mistaken for dates, origin prefixes spelled out letter by\n * letter, an empty `{}` announced as a problem. Returns the input unchanged on any failure.\n */\n public async rewordNotification(text: string, tone: string, language: string, persona = ''): Promise<string> {\n const trimmed = (text || '').trim();\n if (!trimmed) {\n return '';\n }\n try {\n const result = await this.complete(\n `${persona ? `${persona.trim()}\\n\\n` : ''}Reword the following system notification into one ` +\n `short, natural, spoken sentence${language ? `, in ${language}` : ''}. ` +\n `Keep every concrete detail such as adapter names and version numbers. ` +\n `Dates like M/D/YYYY or M/D/YYYY, H:MM:SS AM/PM are timestamps — say them as a date or ` +\n `a time, never as a version number. Technical prefixes such as ` +\n `\"system.host.XYZ: adapter.0:\" only say where it came from and need not be repeated. ` +\n `Empty values such as \"onedrive: {}\" mean there is nothing wrong there — only mention ` +\n `them if it matters. ${tone} Reply with ONLY the sentence.\\n\\nNotification: ${trimmed}`,\n 300,\n );\n return result || trimmed;\n } catch (e) {\n this.log.warn(`notification rewording failed, speaking the original: ${(e as Error).message}`);\n return trimmed;\n }\n }\n\n /**\n * Does `answer` express `category` (\"agreement\", \"Zustimmung\", \"refusal\", …)? This is the classifier\n * behind a trigger's response rules, where a spoken \"ja, mach das\" has to map onto one rule. Decided\n * by the model because the wording is open-ended; a failure answers `false`, so an unclassifiable\n * reply falls through to the trigger's fallback rule instead of acting on a guess.\n */\n public async classify(answer: string, category: string): Promise<boolean> {\n const text = (answer || '').trim();\n const want = (category || '').trim();\n if (!text || !want) {\n return false;\n }\n try {\n const verdict = await this.complete(\n `Does the following reply express \"${want}\"? Consider the meaning, in any language, ` +\n `including short or indirect phrasings. Answer with exactly YES or NO.\\n\\nReply: ${text}`,\n 8,\n );\n return /^\\s*(yes|ja|да)\\b/i.test(verdict);\n } catch (e) {\n this.log.warn(`classify failed ('${want}'): ${(e as Error).message}`);\n return false;\n }\n }\n\n /** Lightweight validation of the provider credentials (used by the settings Test button). */\n public async testConnection(): Promise<{ ok: boolean; error?: string }> {\n try {\n if (this.provider === 'anthropic') {\n const client = this.anthropic as Anthropic;\n await client.messages.create({\n model: await this.ensureModel(),\n max_tokens: 1,\n messages: [{ role: 'user', content: 'ping' }],\n });\n } else {\n const client = this.openai as OpenAI;\n await client.models.list();\n }\n return { ok: true };\n } catch (e) {\n return { ok: false, error: (e as Error).message };\n }\n }\n\n /** List the provider's available model ids (for the settings autocomplete). */\n public async listModels(): Promise<string[]> {\n if (this.provider === 'anthropic') {\n const client = this.anthropic as Anthropic;\n const res = (await client.models.list()) as unknown as { data?: { id: string }[] };\n return (res.data || []).map(m => m.id);\n }\n const client = this.openai as OpenAI;\n const res = (await client.models.list()) as unknown as { data?: { id: string }[] };\n // OpenAI lists many non-chat models (embeddings, tts, whisper, …) — filter to chat-capable-ish.\n const EXCLUDE =\n /embedding|whisper|tts|audio|dall-e|moderation|image|realtime|transcribe|search|davinci|babbage|codex/i;\n return (res.data || [])\n .map(m => String(m.id))\n .filter(id => !EXCLUDE.test(id))\n .sort();\n }\n\n private async runTool(name: string, args: Record<string, unknown>): Promise<unknown> {\n const tool = this.toolMap[name];\n if (!tool) {\n return { error: `unknown tool: ${name}` };\n }\n try {\n const result = await tool.run(args || {});\n this.log.info(`tool ${name}(${JSON.stringify(args)}) → ${JSON.stringify(result).slice(0, 200)}`);\n return result;\n } catch (e) {\n this.log.warn(`tool ${name} failed: ${(e as Error).message}`);\n return { error: (e as Error).message };\n }\n }\n\n // ── OpenAI (Chat Completions + function tools) ──────────────────────────\n private async askOpenAI(\n question: string,\n systemPrompt: string,\n history: { role: 'user' | 'assistant'; content: string }[] = [],\n ): Promise<string> {\n const client = this.openai as OpenAI;\n const tools = this.tools.map(t => ({\n type: 'function' as const,\n function: { name: t.name, description: t.description, parameters: t.parameters },\n }));\n\n const messages: any[] = [];\n if (systemPrompt) {\n messages.push({ role: 'system', content: systemPrompt });\n }\n for (const t of history) {\n messages.push({ role: t.role, content: t.content });\n }\n messages.push({ role: 'user', content: question });\n\n for (let round = 0; round < this.maxRounds; round++) {\n const request = {\n model: this.model,\n messages,\n tools: tools.length ? tools : undefined,\n max_tokens: this.maxTokens,\n };\n this.dump(`llm → openai round ${round}`, request);\n const resp: any = await client.chat.completions.create(request);\n const msg = resp.choices[0].message;\n this.dump(`llm ← openai round ${round}`, { message: msg, usage: resp.usage });\n messages.push(msg);\n\n if (!msg.tool_calls || !msg.tool_calls.length) {\n return String(msg.content || '').trim();\n }\n\n for (const tc of msg.tool_calls) {\n let args: Record<string, unknown> = {};\n try {\n args = JSON.parse(tc.function.arguments || '{}');\n } catch {\n /* leave empty */\n }\n const result = await this.runTool(tc.function.name, args);\n messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) });\n }\n }\n return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';\n }\n\n /**\n * Keep a single cache breakpoint on the last content block of the last message (Anthropic allows\n * ≤4 total; we also mark system + last tool). Strips prior message breakpoints so we never exceed it.\n */\n private markLastMessageForCache(messages: any[]): void {\n for (const m of messages) {\n if (Array.isArray(m.content)) {\n for (const b of m.content) {\n if (b && typeof b === 'object') {\n delete b.cache_control;\n }\n }\n }\n }\n const last = messages[messages.length - 1];\n if (!last) {\n return;\n }\n if (typeof last.content === 'string') {\n last.content = [{ type: 'text', text: last.content, cache_control: { type: 'ephemeral' } }];\n } else if (Array.isArray(last.content) && last.content.length) {\n const b = last.content[last.content.length - 1];\n if (b && typeof b === 'object') {\n b.cache_control = { type: 'ephemeral' };\n }\n }\n }\n\n // ── Anthropic (Messages API + tool_use) ─────────────────────────────────\n private async askAnthropic(\n question: string,\n systemPrompt: string,\n history: { role: 'user' | 'assistant'; content: string }[] = [],\n ): Promise<string> {\n const client = this.anthropic as Anthropic;\n const tools: any[] = this.tools.map(t => ({\n name: t.name,\n description: t.description,\n input_schema: t.parameters as Anthropic.Tool.InputSchema,\n }));\n // Prompt caching: mark the last tool → the static prefix (system + all tools) is cached and\n // read back on every later round/request (5-min TTL), cutting latency & cost on the big context.\n if (tools.length) {\n tools[tools.length - 1] = { ...tools[tools.length - 1], cache_control: { type: 'ephemeral' } };\n }\n const system: any = systemPrompt\n ? [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }]\n : undefined;\n\n const messages: any[] = [\n ...history.map(t => ({ role: t.role, content: t.content })),\n { role: 'user', content: question },\n ];\n\n for (let round = 0; round < this.maxRounds; round++) {\n // Cache the growing conversation prefix too (e.g. the large list_devices result): move a\n // single cache breakpoint to the last content block of the last message each round.\n this.markLastMessageForCache(messages);\n const request = {\n model: this.model,\n max_tokens: this.maxTokens,\n system,\n tools: tools.length ? tools : undefined,\n messages,\n };\n this.dump(`llm → anthropic round ${round}`, request);\n const resp: any = await client.messages.create(request);\n this.dump(`llm ← anthropic round ${round}`, { content: resp.content, usage: resp.usage });\n\n const toolUses = resp.content.filter((b: any) => b.type === 'tool_use');\n if (!toolUses.length) {\n return resp.content\n .filter((b: any) => b.type === 'text')\n .map((b: any) => b.text)\n .join('')\n .trim();\n }\n\n messages.push({ role: 'assistant', content: resp.content });\n const toolResults = [];\n for (const tu of toolUses) {\n const result = await this.runTool(tu.name, tu.input);\n toolResults.push({ type: 'tool_result', tool_use_id: tu.id, content: JSON.stringify(result) });\n }\n messages.push({ role: 'user', content: toolResults });\n }\n return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';\n }\n}\n"]}
|
package/build/lib/nlu.js
CHANGED
|
@@ -148,6 +148,46 @@ const COLOR_TYPES = new Set(['rgb', 'rgbSingle', 'rgbwSingle', 'hue', 'cie', 'ct
|
|
|
148
148
|
const WINDOW_TYPES = new Set(['window', 'windowTilt']);
|
|
149
149
|
/** Words (de/en/ru, stemmed) that name a window. */
|
|
150
150
|
const WINDOW_WORDS = ['fenster', 'window', 'windows', 'окно', 'окна', 'окон'];
|
|
151
|
+
/**
|
|
152
|
+
* Measurement words (de/en/ru, stemmed) → the `@iobroker/type-detector` device types that answer them.
|
|
153
|
+
* This is what makes "wie ist die Luft im Wohnzimmer" work without a device being named: the question
|
|
154
|
+
* is about a *kind* of value, not about one thing. Ported from the Python original's `category_words`
|
|
155
|
+
* (`C:\iot\Hannah`, `core/hannah/settings_manager.py`), mapped onto our detector types.
|
|
156
|
+
*/
|
|
157
|
+
const CATEGORY_WORDS = [
|
|
158
|
+
// Order matters, most specific first: `wordInText` is suffix-tolerant (it has to be, for German and
|
|
159
|
+
// Russian inflection), so the bare word "luft" also matches "Luftdruck" and "Luftfeuchtigkeit". The
|
|
160
|
+
// compound categories therefore have to be tested before the plain-"Luft" one, or they are unreachable.
|
|
161
|
+
{
|
|
162
|
+
words: ['luftfeucht', 'feuchtigkeit', 'feucht', 'humidity', 'humid', 'влажност'],
|
|
163
|
+
category: 'humidity',
|
|
164
|
+
types: ['humidity'],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
words: ['luftdruck', 'druck', 'pressure', 'давлени'],
|
|
168
|
+
category: 'pressure',
|
|
169
|
+
types: ['pressure'],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
words: ['helligkeit', 'hell', 'lux', 'beleuchtungsstaerke', 'brightness', 'illuminance', 'светло', 'яркост'],
|
|
173
|
+
category: 'illuminance',
|
|
174
|
+
types: ['illuminance'],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
words: ['luft', 'luftqualitaet', 'luftguete', 'raumluft', 'iaq', 'co2', 'voc', 'air', 'воздух'],
|
|
178
|
+
category: 'airQuality',
|
|
179
|
+
types: ['airQuality'],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
// Last: 'warm'/'temperatur' are the words people use most, so a more specific category above wins
|
|
183
|
+
// when both appear ("wie warm und wie feucht ist es" is answered about the humidity).
|
|
184
|
+
words: ['temperatur', 'warm', 'kalt', 'grad', 'temperature', 'cold', 'температур', 'тепло', 'холодно'],
|
|
185
|
+
category: 'temperature',
|
|
186
|
+
types: ['temperature', 'thermostat'],
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
/** Words that ask about every room at once ("wie warm ist es überall"). */
|
|
190
|
+
const EVERYWHERE_WORDS = ['ueberall', 'alle', 'allen', 'everywhere', 'all', 'везде', 'всюду'];
|
|
151
191
|
/** Words (de/en/ru, stemmed) that mean "open". */
|
|
152
192
|
const OPEN_WORDS = ['offen', 'geoeffnet', 'geoffnet', 'auf', 'open', 'открыт', 'открыта', 'открыты', 'открытые'];
|
|
153
193
|
/** Words (de/en/ru, stemmed) that make a query an on/off question ("is the light on?"). */
|
|
@@ -699,7 +739,8 @@ class Nlu {
|
|
|
699
739
|
const roomName = this.findRoom(p.joined);
|
|
700
740
|
const device = this.findDevice(p.joined, roomName);
|
|
701
741
|
if (!device) {
|
|
702
|
-
|
|
742
|
+
// No device named — but the question may still be about a kind of measurement.
|
|
743
|
+
return this.parseCategoryQuery(p, this.extractFeatures(p));
|
|
703
744
|
}
|
|
704
745
|
let features = this.extractFeatures(p);
|
|
705
746
|
if (inherited && this.namesOnly(p, device, roomName)) {
|
|
@@ -909,6 +950,38 @@ class Nlu {
|
|
|
909
950
|
* "Which windows are open?" (de/en/ru) → an aggregate query over all window devices. Matches when the
|
|
910
951
|
* text mentions a window word AND an "open" word; optionally restricted to a matched room.
|
|
911
952
|
*/
|
|
953
|
+
/**
|
|
954
|
+
* "How is the air in the living room?", "how warm is it?", "how bright is it everywhere?" — a
|
|
955
|
+
* question about a kind of measurement rather than about a named device. Returns null unless the
|
|
956
|
+
* house actually has devices of that kind, so the LLM still gets its chance.
|
|
957
|
+
*
|
|
958
|
+
* Only treated as a category query when no device was named: "wie warm ist die Heizung" is about
|
|
959
|
+
* that thermostat, and the device path answers it better.
|
|
960
|
+
*/
|
|
961
|
+
parseCategoryQuery(p, features) {
|
|
962
|
+
if (!features.isQuery) {
|
|
963
|
+
return null; // a statement, not a question — nothing to report
|
|
964
|
+
}
|
|
965
|
+
const match = CATEGORY_WORDS.find(c => c.words.some(w => wordInText(w, p.joined)));
|
|
966
|
+
if (!match) {
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
const roomName = this.findRoom(p.joined);
|
|
970
|
+
const everywhere = EVERYWHERE_WORDS.some(w => wordInText(w, p.joined));
|
|
971
|
+
const room = everywhere ? '' : roomName;
|
|
972
|
+
const devices = this.devices.filter(d => match.types.includes(d.type) && (!room || d.room === room));
|
|
973
|
+
if (!devices.length) {
|
|
974
|
+
return null; // nothing of that kind here (or not in that room) → let the LLM answer
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
action: 'categoryQuery',
|
|
978
|
+
category: match.category,
|
|
979
|
+
types: match.types,
|
|
980
|
+
devices,
|
|
981
|
+
room: room || undefined,
|
|
982
|
+
confidence: 0.8,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
912
985
|
parseWindowsOpen(text) {
|
|
913
986
|
if (!WINDOW_WORDS.some(w => wordInText(w, text)) || !OPEN_WORDS.some(w => wordInText(w, text))) {
|
|
914
987
|
return null;
|