iobroker.assistant 0.1.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/LICENSE +21 -0
- package/README.md +176 -0
- package/admin/assistant.svg +19 -0
- package/admin/custom/.vite/manifest.json +169 -0
- package/admin/custom/assets/Components-DcnuVFM6.js +1 -0
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DogEjv3U.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-Dw5TkU1t.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-BEFjTs9S.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-C4r3udVz.js +1243 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DlOBHPL0.js +1494 -0
- package/admin/custom/assets/hostInit-DKMI-sLp.js +1 -0
- package/admin/custom/assets/index-B8wW0kQx.js +1 -0
- package/admin/custom/assets/lib-DpE8sc4h.js +3 -0
- package/admin/custom/assets/mf-entry-bootstrap-0-e7fa582d.js +36 -0
- package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +1 -0
- package/admin/custom/assets/virtualExposes-DOTO22UK.js +1 -0
- package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CQa59yVi.js +1 -0
- package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__ConfigCustomAssistant__customComponents_js-9g2Fj8tk.js +2 -0
- package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-BTUQTbYo.js +2 -0
- package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +1 -0
- package/admin/custom/customComponents.js +1 -0
- package/admin/custom/customComponents.ssr.js +1 -0
- package/admin/custom/i18n/de.json +58 -0
- package/admin/custom/i18n/en.json +58 -0
- package/admin/custom/i18n/es.json +34 -0
- package/admin/custom/i18n/fr.json +34 -0
- package/admin/custom/i18n/it.json +34 -0
- package/admin/custom/i18n/nl.json +34 -0
- package/admin/custom/i18n/pl.json +34 -0
- package/admin/custom/i18n/pt.json +34 -0
- package/admin/custom/i18n/ru.json +34 -0
- package/admin/custom/i18n/uk.json +34 -0
- package/admin/custom/i18n/zh-cn.json +34 -0
- package/admin/custom/index.html +32 -0
- package/admin/custom/mf-manifest.json +1 -0
- package/admin/custom/mf-stats.json +1 -0
- package/admin/i18n/de.json +130 -0
- package/admin/i18n/en.json +130 -0
- package/admin/i18n/es.json +106 -0
- package/admin/i18n/fr.json +106 -0
- package/admin/i18n/it.json +106 -0
- package/admin/i18n/nl.json +106 -0
- package/admin/i18n/pl.json +106 -0
- package/admin/i18n/pt.json +106 -0
- package/admin/i18n/ru.json +106 -0
- package/admin/i18n/uk.json +106 -0
- package/admin/i18n/zh-cn.json +106 -0
- package/admin/jsonConfig.json +743 -0
- package/admin/sounds/alarm.wav +0 -0
- package/admin/sounds/timer.wav +0 -0
- package/build/lib/alarms.js +281 -0
- package/build/lib/alarms.js.map +1 -0
- package/build/lib/context.js +52 -0
- package/build/lib/context.js.map +1 -0
- package/build/lib/credentials.js +121 -0
- package/build/lib/credentials.js.map +1 -0
- package/build/lib/llm.js +303 -0
- package/build/lib/llm.js.map +1 -0
- package/build/lib/localLlm.js +221 -0
- package/build/lib/localLlm.js.map +1 -0
- package/build/lib/memory.js +190 -0
- package/build/lib/memory.js.map +1 -0
- package/build/lib/nlu.js +929 -0
- package/build/lib/nlu.js.map +1 -0
- package/build/lib/timers.js +245 -0
- package/build/lib/timers.js.map +1 -0
- package/build/lib/tools.js +299 -0
- package/build/lib/tools.js.map +1 -0
- package/build/lib/voice/aws.js +102 -0
- package/build/lib/voice/aws.js.map +1 -0
- package/build/lib/voice/azure.js +144 -0
- package/build/lib/voice/azure.js.map +1 -0
- package/build/lib/voice/download.js +76 -0
- package/build/lib/voice/download.js.map +1 -0
- package/build/lib/voice/engines.js +89 -0
- package/build/lib/voice/engines.js.map +1 -0
- package/build/lib/voice/lang.js +33 -0
- package/build/lib/voice/lang.js.map +1 -0
- package/build/lib/voice/piper.js +191 -0
- package/build/lib/voice/piper.js.map +1 -0
- package/build/lib/voice/protocol.js +35 -0
- package/build/lib/voice/protocol.js.map +1 -0
- package/build/lib/voice/stt.js +116 -0
- package/build/lib/voice/stt.js.map +1 -0
- package/build/lib/voice/tts.js +36 -0
- package/build/lib/voice/tts.js.map +1 -0
- package/build/lib/voice/voiceServer.js +249 -0
- package/build/lib/voice/voiceServer.js.map +1 -0
- package/build/lib/voice/vosk.js +260 -0
- package/build/lib/voice/vosk.js.map +1 -0
- package/build/lib/voice/wyoming.js +320 -0
- package/build/lib/voice/wyoming.js.map +1 -0
- package/build/lib/weather.js +423 -0
- package/build/lib/weather.js.map +1 -0
- package/build/main.js +3152 -0
- package/build/main.js.map +1 -0
- package/io-package.json +528 -0
- package/package.json +68 -0
package/build/lib/llm.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LlmAgent = exports.PROVIDER_PRESETS = void 0;
|
|
7
|
+
exports.resolveProvider = resolveProvider;
|
|
8
|
+
const openai_1 = __importDefault(require("openai"));
|
|
9
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
10
|
+
/** Per-provider SDK, endpoint and default model. Gemini/DeepSeek expose OpenAI-compatible endpoints. */
|
|
11
|
+
exports.PROVIDER_PRESETS = {
|
|
12
|
+
openai: { sdk: 'openai', baseUrl: '', defaultModel: 'gpt-4o-mini' },
|
|
13
|
+
anthropic: { sdk: 'anthropic', baseUrl: '', defaultModel: 'claude-sonnet-4-6' },
|
|
14
|
+
gemini: {
|
|
15
|
+
sdk: 'openai',
|
|
16
|
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
17
|
+
defaultModel: 'gemini-2.0-flash',
|
|
18
|
+
},
|
|
19
|
+
deepseek: { sdk: 'openai', baseUrl: 'https://api.deepseek.com', defaultModel: 'deepseek-chat' },
|
|
20
|
+
custom: { sdk: 'openai', baseUrl: '', defaultModel: '' },
|
|
21
|
+
};
|
|
22
|
+
/** Resolve the effective SDK, base URL and default model for a provider (custom uses `configBaseUrl`). */
|
|
23
|
+
function resolveProvider(provider, configBaseUrl) {
|
|
24
|
+
const preset = exports.PROVIDER_PRESETS[provider || 'openai'] || exports.PROVIDER_PRESETS.openai;
|
|
25
|
+
return {
|
|
26
|
+
sdk: preset.sdk,
|
|
27
|
+
baseUrl: provider === 'custom' ? configBaseUrl || '' : preset.baseUrl,
|
|
28
|
+
defaultModel: preset.defaultModel,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Provider-agnostic LLM agent with tool-calling.
|
|
33
|
+
*
|
|
34
|
+
* `ask(question)` runs the model, executes any tool calls it requests, feeds the
|
|
35
|
+
* results back, and repeats until the model returns a final text answer.
|
|
36
|
+
*
|
|
37
|
+
* The request/response plumbing of the vendor SDKs is intentionally loosely typed
|
|
38
|
+
* (`any`) so the code stays robust across minor SDK type changes; the public API
|
|
39
|
+
* (options, `ask`, tools) is fully typed.
|
|
40
|
+
*/
|
|
41
|
+
class LlmAgent {
|
|
42
|
+
provider;
|
|
43
|
+
model;
|
|
44
|
+
systemPrompt;
|
|
45
|
+
maxTokens;
|
|
46
|
+
tools;
|
|
47
|
+
toolMap;
|
|
48
|
+
log;
|
|
49
|
+
maxRounds = 6; // safety cap against tool-call loops
|
|
50
|
+
openai;
|
|
51
|
+
anthropic;
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.provider = opts.provider === 'anthropic' ? 'anthropic' : 'openai';
|
|
54
|
+
this.model = opts.model;
|
|
55
|
+
this.systemPrompt = opts.systemPrompt || '';
|
|
56
|
+
this.maxTokens = opts.maxTokens || 1024;
|
|
57
|
+
this.tools = opts.tools || [];
|
|
58
|
+
this.log = opts.log;
|
|
59
|
+
this.toolMap = Object.fromEntries(this.tools.map(t => [t.name, t]));
|
|
60
|
+
if (this.provider === 'anthropic') {
|
|
61
|
+
this.anthropic = new sdk_1.default({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.openai = new openai_1.default({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `systemPrompt` overrides the configured one for this call (e.g. to inject a device list).
|
|
69
|
+
* `history` are prior turns (user/assistant) prepended so the model can resolve follow-ups.
|
|
70
|
+
*/
|
|
71
|
+
async ask(question, systemPrompt, history = []) {
|
|
72
|
+
this.log.debug(`llm ask [${this.provider}/${this.model}] (${history.length} history): ${question}`);
|
|
73
|
+
const started = Date.now();
|
|
74
|
+
const sys = systemPrompt ?? this.systemPrompt;
|
|
75
|
+
const answer = this.provider === 'anthropic'
|
|
76
|
+
? await this.askAnthropic(question, sys, history)
|
|
77
|
+
: await this.askOpenAI(question, sys, history);
|
|
78
|
+
this.log.debug(`llm answer (${Date.now() - started} ms): ${answer}`);
|
|
79
|
+
return answer;
|
|
80
|
+
}
|
|
81
|
+
/** Compact, truncated JSON for debug logging of LLM requests/responses. */
|
|
82
|
+
dump(label, payload) {
|
|
83
|
+
if (this.log.level !== 'silly' && this.log.level !== 'debug') {
|
|
84
|
+
return; // avoid stringifying large payloads when debug logging is off
|
|
85
|
+
}
|
|
86
|
+
const max = this.log.level === 'silly' ? 8000 : 1500;
|
|
87
|
+
let text;
|
|
88
|
+
try {
|
|
89
|
+
text = JSON.stringify(payload);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
text = String(payload);
|
|
93
|
+
}
|
|
94
|
+
this.log.debug(`${label}: ${text.length > max ? `${text.slice(0, max)}…[${text.length}]` : text}`);
|
|
95
|
+
}
|
|
96
|
+
/** Translate a short device/room name into `targetLanguage` (single completion, no tools). */
|
|
97
|
+
async translate(text, targetLanguage) {
|
|
98
|
+
const trimmed = (text || '').trim();
|
|
99
|
+
if (!trimmed) {
|
|
100
|
+
return '';
|
|
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}`;
|
|
104
|
+
if (this.provider === 'anthropic') {
|
|
105
|
+
const client = this.anthropic;
|
|
106
|
+
const resp = await client.messages.create({
|
|
107
|
+
model: this.model,
|
|
108
|
+
max_tokens: 64,
|
|
109
|
+
messages: [{ role: 'user', content: prompt }],
|
|
110
|
+
});
|
|
111
|
+
return resp.content
|
|
112
|
+
.filter((b) => b.type === 'text')
|
|
113
|
+
.map((b) => b.text)
|
|
114
|
+
.join('')
|
|
115
|
+
.trim();
|
|
116
|
+
}
|
|
117
|
+
const client = this.openai;
|
|
118
|
+
const resp = await client.chat.completions.create({
|
|
119
|
+
model: this.model,
|
|
120
|
+
max_tokens: 64,
|
|
121
|
+
messages: [{ role: 'user', content: prompt }],
|
|
122
|
+
});
|
|
123
|
+
return String(resp.choices[0].message.content || '').trim();
|
|
124
|
+
}
|
|
125
|
+
/** Lightweight validation of the provider credentials (used by the settings Test button). */
|
|
126
|
+
async testConnection() {
|
|
127
|
+
try {
|
|
128
|
+
if (this.provider === 'anthropic') {
|
|
129
|
+
const client = this.anthropic;
|
|
130
|
+
await client.messages.create({
|
|
131
|
+
model: this.model,
|
|
132
|
+
max_tokens: 1,
|
|
133
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const client = this.openai;
|
|
138
|
+
await client.models.list();
|
|
139
|
+
}
|
|
140
|
+
return { ok: true };
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
return { ok: false, error: e.message };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** List the provider's available model ids (for the settings autocomplete). */
|
|
147
|
+
async listModels() {
|
|
148
|
+
if (this.provider === 'anthropic') {
|
|
149
|
+
const client = this.anthropic;
|
|
150
|
+
const res = (await client.models.list());
|
|
151
|
+
return (res.data || []).map(m => m.id);
|
|
152
|
+
}
|
|
153
|
+
const client = this.openai;
|
|
154
|
+
const res = (await client.models.list());
|
|
155
|
+
// OpenAI lists many non-chat models (embeddings, tts, whisper, …) — filter to chat-capable-ish.
|
|
156
|
+
const EXCLUDE = /embedding|whisper|tts|audio|dall-e|moderation|image|realtime|transcribe|search|davinci|babbage|codex/i;
|
|
157
|
+
return (res.data || [])
|
|
158
|
+
.map(m => String(m.id))
|
|
159
|
+
.filter(id => !EXCLUDE.test(id))
|
|
160
|
+
.sort();
|
|
161
|
+
}
|
|
162
|
+
async runTool(name, args) {
|
|
163
|
+
const tool = this.toolMap[name];
|
|
164
|
+
if (!tool) {
|
|
165
|
+
return { error: `unknown tool: ${name}` };
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const result = await tool.run(args || {});
|
|
169
|
+
this.log.info(`tool ${name}(${JSON.stringify(args)}) → ${JSON.stringify(result).slice(0, 200)}`);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
this.log.warn(`tool ${name} failed: ${e.message}`);
|
|
174
|
+
return { error: e.message };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ── OpenAI (Chat Completions + function tools) ──────────────────────────
|
|
178
|
+
async askOpenAI(question, systemPrompt, history = []) {
|
|
179
|
+
const client = this.openai;
|
|
180
|
+
const tools = this.tools.map(t => ({
|
|
181
|
+
type: 'function',
|
|
182
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
183
|
+
}));
|
|
184
|
+
const messages = [];
|
|
185
|
+
if (systemPrompt) {
|
|
186
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
187
|
+
}
|
|
188
|
+
for (const t of history) {
|
|
189
|
+
messages.push({ role: t.role, content: t.content });
|
|
190
|
+
}
|
|
191
|
+
messages.push({ role: 'user', content: question });
|
|
192
|
+
for (let round = 0; round < this.maxRounds; round++) {
|
|
193
|
+
const request = {
|
|
194
|
+
model: this.model,
|
|
195
|
+
messages,
|
|
196
|
+
tools: tools.length ? tools : undefined,
|
|
197
|
+
max_tokens: this.maxTokens,
|
|
198
|
+
};
|
|
199
|
+
this.dump(`llm → openai round ${round}`, request);
|
|
200
|
+
const resp = await client.chat.completions.create(request);
|
|
201
|
+
const msg = resp.choices[0].message;
|
|
202
|
+
this.dump(`llm ← openai round ${round}`, { message: msg, usage: resp.usage });
|
|
203
|
+
messages.push(msg);
|
|
204
|
+
if (!msg.tool_calls || !msg.tool_calls.length) {
|
|
205
|
+
return String(msg.content || '').trim();
|
|
206
|
+
}
|
|
207
|
+
for (const tc of msg.tool_calls) {
|
|
208
|
+
let args = {};
|
|
209
|
+
try {
|
|
210
|
+
args = JSON.parse(tc.function.arguments || '{}');
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
/* leave empty */
|
|
214
|
+
}
|
|
215
|
+
const result = await this.runTool(tc.function.name, args);
|
|
216
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Keep a single cache breakpoint on the last content block of the last message (Anthropic allows
|
|
223
|
+
* ≤4 total; we also mark system + last tool). Strips prior message breakpoints so we never exceed it.
|
|
224
|
+
*/
|
|
225
|
+
markLastMessageForCache(messages) {
|
|
226
|
+
for (const m of messages) {
|
|
227
|
+
if (Array.isArray(m.content)) {
|
|
228
|
+
for (const b of m.content) {
|
|
229
|
+
if (b && typeof b === 'object') {
|
|
230
|
+
delete b.cache_control;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const last = messages[messages.length - 1];
|
|
236
|
+
if (!last) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (typeof last.content === 'string') {
|
|
240
|
+
last.content = [{ type: 'text', text: last.content, cache_control: { type: 'ephemeral' } }];
|
|
241
|
+
}
|
|
242
|
+
else if (Array.isArray(last.content) && last.content.length) {
|
|
243
|
+
const b = last.content[last.content.length - 1];
|
|
244
|
+
if (b && typeof b === 'object') {
|
|
245
|
+
b.cache_control = { type: 'ephemeral' };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// ── Anthropic (Messages API + tool_use) ─────────────────────────────────
|
|
250
|
+
async askAnthropic(question, systemPrompt, history = []) {
|
|
251
|
+
const client = this.anthropic;
|
|
252
|
+
const tools = this.tools.map(t => ({
|
|
253
|
+
name: t.name,
|
|
254
|
+
description: t.description,
|
|
255
|
+
input_schema: t.parameters,
|
|
256
|
+
}));
|
|
257
|
+
// Prompt caching: mark the last tool → the static prefix (system + all tools) is cached and
|
|
258
|
+
// read back on every later round/request (5-min TTL), cutting latency & cost on the big context.
|
|
259
|
+
if (tools.length) {
|
|
260
|
+
tools[tools.length - 1] = { ...tools[tools.length - 1], cache_control: { type: 'ephemeral' } };
|
|
261
|
+
}
|
|
262
|
+
const system = systemPrompt
|
|
263
|
+
? [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }]
|
|
264
|
+
: undefined;
|
|
265
|
+
const messages = [
|
|
266
|
+
...history.map(t => ({ role: t.role, content: t.content })),
|
|
267
|
+
{ role: 'user', content: question },
|
|
268
|
+
];
|
|
269
|
+
for (let round = 0; round < this.maxRounds; round++) {
|
|
270
|
+
// Cache the growing conversation prefix too (e.g. the large list_devices result): move a
|
|
271
|
+
// single cache breakpoint to the last content block of the last message each round.
|
|
272
|
+
this.markLastMessageForCache(messages);
|
|
273
|
+
const request = {
|
|
274
|
+
model: this.model,
|
|
275
|
+
max_tokens: this.maxTokens,
|
|
276
|
+
system,
|
|
277
|
+
tools: tools.length ? tools : undefined,
|
|
278
|
+
messages,
|
|
279
|
+
};
|
|
280
|
+
this.dump(`llm → anthropic round ${round}`, request);
|
|
281
|
+
const resp = await client.messages.create(request);
|
|
282
|
+
this.dump(`llm ← anthropic round ${round}`, { content: resp.content, usage: resp.usage });
|
|
283
|
+
const toolUses = resp.content.filter((b) => b.type === 'tool_use');
|
|
284
|
+
if (!toolUses.length) {
|
|
285
|
+
return resp.content
|
|
286
|
+
.filter((b) => b.type === 'text')
|
|
287
|
+
.map((b) => b.text)
|
|
288
|
+
.join('')
|
|
289
|
+
.trim();
|
|
290
|
+
}
|
|
291
|
+
messages.push({ role: 'assistant', content: resp.content });
|
|
292
|
+
const toolResults = [];
|
|
293
|
+
for (const tu of toolUses) {
|
|
294
|
+
const result = await this.runTool(tu.name, tu.input);
|
|
295
|
+
toolResults.push({ type: 'tool_result', tool_use_id: tu.id, content: JSON.stringify(result) });
|
|
296
|
+
}
|
|
297
|
+
messages.push({ role: 'user', content: toolResults });
|
|
298
|
+
}
|
|
299
|
+
return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
exports.LlmAgent = LlmAgent;
|
|
303
|
+
//# sourceMappingURL=llm.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,221 @@
|
|
|
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.LocalLlm = exports.DEFAULT_LOCAL_MODEL_URL = exports.HANDOFF = void 0;
|
|
37
|
+
exports.isLocalLlmInstalled = isLocalLlmInstalled;
|
|
38
|
+
exports.isModelDownloaded = isModelDownloaded;
|
|
39
|
+
exports.installLocalLlm = installLocalLlm;
|
|
40
|
+
exports.isHandoff = isHandoff;
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
const node_module_1 = require("node:module");
|
|
43
|
+
const node_url_1 = require("node:url");
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const fs = __importStar(require("node:fs"));
|
|
46
|
+
/**
|
|
47
|
+
* Tier-1a local LLM via `node-llama-cpp` — installed **on demand** (not a package.json dependency, so
|
|
48
|
+
* users who don't enable it never pay the large native download). Everything lives in the writable
|
|
49
|
+
* instance data dir (`<iobroker-data>/assistant.<n>/`), which survives adapter upgrades; on a fresh
|
|
50
|
+
* upgrade the module is simply re-installed on request.
|
|
51
|
+
*
|
|
52
|
+
* The model runs **tool-free**: it answers general questions and, when a request needs live ioBroker
|
|
53
|
+
* state or it is unsure, it emits the {@link HANDOFF} sentinel so the orchestrator escalates to the
|
|
54
|
+
* cloud LLM (which has the tools). Small models handle a "say HANDOFF" rule far more reliably than
|
|
55
|
+
* real tool-calling.
|
|
56
|
+
*/
|
|
57
|
+
/** Sentinel the local model returns to escalate a request to the cloud LLM. */
|
|
58
|
+
exports.HANDOFF = 'HANDOFF';
|
|
59
|
+
/** Default local model: small enough for arm64 (Raspi/NanoPi), tool-free chat, ~1 GB. */
|
|
60
|
+
exports.DEFAULT_LOCAL_MODEL_URL = 'https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf';
|
|
61
|
+
const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
62
|
+
/** Directory the on-demand npm install targets (`<dataDir>/node_modules/node-llama-cpp`). */
|
|
63
|
+
function moduleDir(dataDir) {
|
|
64
|
+
return path.join(dataDir, 'node_modules', 'node-llama-cpp');
|
|
65
|
+
}
|
|
66
|
+
/** Local file path a model URL is cached to. */
|
|
67
|
+
function modelPathFor(dataDir, url) {
|
|
68
|
+
const file = url.split('/').pop() || 'model.gguf';
|
|
69
|
+
return path.join(dataDir, 'models', file);
|
|
70
|
+
}
|
|
71
|
+
/** True once `node-llama-cpp` has been installed into the data dir. */
|
|
72
|
+
function isLocalLlmInstalled(dataDir) {
|
|
73
|
+
try {
|
|
74
|
+
return fs.existsSync(path.join(moduleDir(dataDir), 'package.json'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** True once the model file has been downloaded. */
|
|
81
|
+
function isModelDownloaded(dataDir, url) {
|
|
82
|
+
try {
|
|
83
|
+
return fs.existsSync(modelPathFor(dataDir, url));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Install `node-llama-cpp` (with its prebuilt native binary) into the data dir via a spawned `npm
|
|
91
|
+
* install`. Streams npm output to `onProgress`. Idempotent-ish: npm is a no-op if already present.
|
|
92
|
+
*/
|
|
93
|
+
function installLocalLlm(dataDir, onProgress) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
96
|
+
// A local package.json makes npm install into <dataDir>/node_modules (and not walk up).
|
|
97
|
+
const pkg = path.join(dataDir, 'package.json');
|
|
98
|
+
if (!fs.existsSync(pkg)) {
|
|
99
|
+
fs.writeFileSync(pkg, JSON.stringify({ name: 'assistant-local-llm', private: true, version: '1.0.0' }));
|
|
100
|
+
}
|
|
101
|
+
const child = (0, node_child_process_1.spawn)(NPM, ['install', 'node-llama-cpp@3', '--omit=dev', '--no-audit', '--no-fund'], {
|
|
102
|
+
cwd: dataDir,
|
|
103
|
+
shell: process.platform === 'win32', // npm.cmd needs a shell on Windows
|
|
104
|
+
});
|
|
105
|
+
child.stdout.on('data', d => onProgress(String(d).trim()));
|
|
106
|
+
child.stderr.on('data', d => onProgress(String(d).trim()));
|
|
107
|
+
child.on('error', reject);
|
|
108
|
+
child.on('close', code => (code === 0 ? resolve() : reject(new Error(`npm install exited with code ${code}`))));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A loaded local model. `load()` imports the on-demand module, downloads the GGUF if missing, and
|
|
113
|
+
* spins up a llama context. `ask()` returns the model's answer (or {@link HANDOFF}).
|
|
114
|
+
*/
|
|
115
|
+
class LocalLlm {
|
|
116
|
+
opts;
|
|
117
|
+
session = null;
|
|
118
|
+
model = null;
|
|
119
|
+
context = null;
|
|
120
|
+
constructor(opts) {
|
|
121
|
+
this.opts = opts;
|
|
122
|
+
}
|
|
123
|
+
/** Full system prompt = the strict HANDOFF rule (device work must escalate to the tool-enabled cloud LLM). */
|
|
124
|
+
systemPrompt() {
|
|
125
|
+
// The local model has NO tools and cannot touch the smart home. Anything device-related MUST hand off,
|
|
126
|
+
// including control commands — otherwise a small model tends to "helpfully" refuse instead of escalating.
|
|
127
|
+
return (`You are a smart-home assistant, but you have NO ability to read or control anything in the home. ` +
|
|
128
|
+
`If the user wants to control or query ANYTHING in the home — e.g. turn on/off, switch, dim, ` +
|
|
129
|
+
`open/close, start/stop, set, activate, or ask for the value/state/status of a light, socket, ` +
|
|
130
|
+
`sensor, device, room, or the home — OR if you are unsure — you MUST reply with EXACTLY "${exports.HANDOFF}" ` +
|
|
131
|
+
`and nothing else. Do NOT apologise, do NOT explain, do NOT say you are an AI. ` +
|
|
132
|
+
`Reply directly ONLY for general knowledge, small talk, or questions unrelated to the home.`);
|
|
133
|
+
}
|
|
134
|
+
async load(onProgress) {
|
|
135
|
+
const dir = moduleDir(this.opts.dataDir);
|
|
136
|
+
if (!fs.existsSync(path.join(dir, 'package.json'))) {
|
|
137
|
+
throw new Error('node-llama-cpp is not installed — run the install first');
|
|
138
|
+
}
|
|
139
|
+
// Resolve the package entry from the data dir and import it (v3 is ESM-only → dynamic import).
|
|
140
|
+
const req = (0, node_module_1.createRequire)(path.join(this.opts.dataDir, 'package.json'));
|
|
141
|
+
const entry = req.resolve('node-llama-cpp');
|
|
142
|
+
const mod = await import((0, node_url_1.pathToFileURL)(entry).href);
|
|
143
|
+
const { getLlama, LlamaChatSession, createModelDownloader } = mod;
|
|
144
|
+
const modelPath = modelPathFor(this.opts.dataDir, this.opts.modelUrl);
|
|
145
|
+
if (!fs.existsSync(modelPath)) {
|
|
146
|
+
this.opts.log.info(`Downloading local model → ${modelPath}`);
|
|
147
|
+
fs.mkdirSync(path.dirname(modelPath), { recursive: true });
|
|
148
|
+
const downloader = await createModelDownloader({
|
|
149
|
+
modelUri: this.opts.modelUrl,
|
|
150
|
+
dirPath: path.dirname(modelPath),
|
|
151
|
+
fileName: path.basename(modelPath),
|
|
152
|
+
onProgress: (p) => {
|
|
153
|
+
if (p.totalSize) {
|
|
154
|
+
onProgress?.(`download ${Math.round((100 * p.downloadedSize) / p.totalSize)}%`);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
await downloader.download();
|
|
159
|
+
}
|
|
160
|
+
const llama = await getLlama();
|
|
161
|
+
this.model = await llama.loadModel({ modelPath });
|
|
162
|
+
this.context = await this.model.createContext();
|
|
163
|
+
this.session = new LlamaChatSession({
|
|
164
|
+
contextSequence: this.context.getSequence(),
|
|
165
|
+
systemPrompt: this.systemPrompt(),
|
|
166
|
+
});
|
|
167
|
+
this.opts.log.info('Local model loaded.');
|
|
168
|
+
}
|
|
169
|
+
/** Ask the local model. Returns its answer, or {@link HANDOFF} to escalate to the cloud LLM. */
|
|
170
|
+
async ask(question) {
|
|
171
|
+
if (!this.session) {
|
|
172
|
+
return exports.HANDOFF;
|
|
173
|
+
}
|
|
174
|
+
// Reset per-question so prompts stay independent (best-effort across node-llama-cpp versions).
|
|
175
|
+
try {
|
|
176
|
+
this.session.setChatHistory?.([]);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
/* ignore */
|
|
180
|
+
}
|
|
181
|
+
const answer = await this.session.prompt(question, { maxTokens: this.opts.maxTokens || 512 });
|
|
182
|
+
return (answer || '').trim();
|
|
183
|
+
}
|
|
184
|
+
async dispose() {
|
|
185
|
+
try {
|
|
186
|
+
await this.context?.dispose?.();
|
|
187
|
+
await this.model?.dispose?.();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* ignore */
|
|
191
|
+
}
|
|
192
|
+
this.session = this.context = this.model = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.LocalLlm = LocalLlm;
|
|
196
|
+
/**
|
|
197
|
+
* Refusal patterns (de/en/ru) a small tool-free model emits when it *should* have handed off — e.g.
|
|
198
|
+
* "I'm an AI and can't control real devices". Treated as a hand-off so the tool-enabled cloud LLM runs.
|
|
199
|
+
*/
|
|
200
|
+
const REFUSAL = [
|
|
201
|
+
/\bhandoff\b/i,
|
|
202
|
+
// en
|
|
203
|
+
/\bi('?m| am)\s+an?\s+ai\b/i,
|
|
204
|
+
/\bi\s+(can('?t|not)|am\s+unable\s+to)\b.*\b(turn|switch|control|access|interact|device|light)\b/i,
|
|
205
|
+
// de
|
|
206
|
+
/\bich\s+bin\s+eine?\s+(ki|künstliche)\b/i,
|
|
207
|
+
/\bkann\s+.*\bnicht\b.*\b(schalten|steuern|einschalten|zugreifen|geräte?|licht)\b/i,
|
|
208
|
+
// ru (\w/\b are ASCII-only → use Cyrillic classes / plain substrings)
|
|
209
|
+
/искусственн[а-яё]*\s+интеллект/i,
|
|
210
|
+
/не\s+(могу|имею)/i,
|
|
211
|
+
];
|
|
212
|
+
/** True if the model's answer signals (or implies) a hand-off to the cloud LLM. */
|
|
213
|
+
function isHandoff(answer) {
|
|
214
|
+
const trimmed = answer.trim();
|
|
215
|
+
const up = trimmed.toUpperCase();
|
|
216
|
+
if (up === exports.HANDOFF || up.startsWith(`${exports.HANDOFF} `) || up === `"${exports.HANDOFF}"`) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return REFUSAL.some(re => re.test(trimmed));
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=localLlm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localLlm.js","sourceRoot":"","sources":["../../src/lib/localLlm.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport { pathToFileURL } from 'node:url';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\n\n/**\n * Tier-1a local LLM via `node-llama-cpp` — installed **on demand** (not a package.json dependency, so\n * users who don't enable it never pay the large native download). Everything lives in the writable\n * instance data dir (`<iobroker-data>/assistant.<n>/`), which survives adapter upgrades; on a fresh\n * upgrade the module is simply re-installed on request.\n *\n * The model runs **tool-free**: it answers general questions and, when a request needs live ioBroker\n * state or it is unsure, it emits the {@link HANDOFF} sentinel so the orchestrator escalates to the\n * cloud LLM (which has the tools). Small models handle a \"say HANDOFF\" rule far more reliably than\n * real tool-calling.\n */\n\n/** Sentinel the local model returns to escalate a request to the cloud LLM. */\nexport const HANDOFF = 'HANDOFF';\n\n/** Default local model: small enough for arm64 (Raspi/NanoPi), tool-free chat, ~1 GB. */\nexport const DEFAULT_LOCAL_MODEL_URL =\n 'https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf';\n\nconst NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';\n\ntype Logger = Pick<ioBroker.Logger, 'info' | 'warn' | 'debug' | 'error'>;\n\nexport interface LocalLlmOptions {\n /** Writable instance data dir (from `adapter.getAbsoluteInstanceDataDir()`). */\n dataDir: string;\n /** Direct URL of a GGUF model file. */\n modelUrl: string;\n systemPrompt?: string;\n maxTokens?: number;\n log: Logger;\n}\n\n/** Directory the on-demand npm install targets (`<dataDir>/node_modules/node-llama-cpp`). */\nfunction moduleDir(dataDir: string): string {\n return path.join(dataDir, 'node_modules', 'node-llama-cpp');\n}\n\n/** Local file path a model URL is cached to. */\nfunction modelPathFor(dataDir: string, url: string): string {\n const file = url.split('/').pop() || 'model.gguf';\n return path.join(dataDir, 'models', file);\n}\n\n/** True once `node-llama-cpp` has been installed into the data dir. */\nexport function isLocalLlmInstalled(dataDir: string): boolean {\n try {\n return fs.existsSync(path.join(moduleDir(dataDir), 'package.json'));\n } catch {\n return false;\n }\n}\n\n/** True once the model file has been downloaded. */\nexport function isModelDownloaded(dataDir: string, url: string): boolean {\n try {\n return fs.existsSync(modelPathFor(dataDir, url));\n } catch {\n return false;\n }\n}\n\n/**\n * Install `node-llama-cpp` (with its prebuilt native binary) into the data dir via a spawned `npm\n * install`. Streams npm output to `onProgress`. Idempotent-ish: npm is a no-op if already present.\n */\nexport function installLocalLlm(dataDir: string, onProgress: (line: string) => void): Promise<void> {\n return new Promise((resolve, reject) => {\n fs.mkdirSync(dataDir, { recursive: true });\n // A local package.json makes npm install into <dataDir>/node_modules (and not walk up).\n const pkg = path.join(dataDir, 'package.json');\n if (!fs.existsSync(pkg)) {\n fs.writeFileSync(pkg, JSON.stringify({ name: 'assistant-local-llm', private: true, version: '1.0.0' }));\n }\n const child = spawn(NPM, ['install', 'node-llama-cpp@3', '--omit=dev', '--no-audit', '--no-fund'], {\n cwd: dataDir,\n shell: process.platform === 'win32', // npm.cmd needs a shell on Windows\n });\n child.stdout.on('data', d => onProgress(String(d).trim()));\n child.stderr.on('data', d => onProgress(String(d).trim()));\n child.on('error', reject);\n child.on('close', code => (code === 0 ? resolve() : reject(new Error(`npm install exited with code ${code}`))));\n });\n}\n\n/**\n * A loaded local model. `load()` imports the on-demand module, downloads the GGUF if missing, and\n * spins up a llama context. `ask()` returns the model's answer (or {@link HANDOFF}).\n */\nexport class LocalLlm {\n private session: any = null;\n private model: any = null;\n private context: any = null;\n\n constructor(private readonly opts: LocalLlmOptions) {}\n\n /** Full system prompt = the strict HANDOFF rule (device work must escalate to the tool-enabled cloud LLM). */\n private systemPrompt(): string {\n // The local model has NO tools and cannot touch the smart home. Anything device-related MUST hand off,\n // including control commands — otherwise a small model tends to \"helpfully\" refuse instead of escalating.\n return (\n `You are a smart-home assistant, but you have NO ability to read or control anything in the home. ` +\n `If the user wants to control or query ANYTHING in the home — e.g. turn on/off, switch, dim, ` +\n `open/close, start/stop, set, activate, or ask for the value/state/status of a light, socket, ` +\n `sensor, device, room, or the home — OR if you are unsure — you MUST reply with EXACTLY \"${HANDOFF}\" ` +\n `and nothing else. Do NOT apologise, do NOT explain, do NOT say you are an AI. ` +\n `Reply directly ONLY for general knowledge, small talk, or questions unrelated to the home.`\n );\n }\n\n async load(onProgress?: (line: string) => void): Promise<void> {\n const dir = moduleDir(this.opts.dataDir);\n if (!fs.existsSync(path.join(dir, 'package.json'))) {\n throw new Error('node-llama-cpp is not installed — run the install first');\n }\n // Resolve the package entry from the data dir and import it (v3 is ESM-only → dynamic import).\n const req = createRequire(path.join(this.opts.dataDir, 'package.json'));\n const entry = req.resolve('node-llama-cpp');\n const mod: any = await import(pathToFileURL(entry).href);\n const { getLlama, LlamaChatSession, createModelDownloader } = mod;\n\n const modelPath = modelPathFor(this.opts.dataDir, this.opts.modelUrl);\n if (!fs.existsSync(modelPath)) {\n this.opts.log.info(`Downloading local model → ${modelPath}`);\n fs.mkdirSync(path.dirname(modelPath), { recursive: true });\n const downloader = await createModelDownloader({\n modelUri: this.opts.modelUrl,\n dirPath: path.dirname(modelPath),\n fileName: path.basename(modelPath),\n onProgress: (p: { downloadedSize: number; totalSize: number }) => {\n if (p.totalSize) {\n onProgress?.(`download ${Math.round((100 * p.downloadedSize) / p.totalSize)}%`);\n }\n },\n });\n await downloader.download();\n }\n\n const llama = await getLlama();\n this.model = await llama.loadModel({ modelPath });\n this.context = await this.model.createContext();\n this.session = new LlamaChatSession({\n contextSequence: this.context.getSequence(),\n systemPrompt: this.systemPrompt(),\n });\n this.opts.log.info('Local model loaded.');\n }\n\n /** Ask the local model. Returns its answer, or {@link HANDOFF} to escalate to the cloud LLM. */\n async ask(question: string): Promise<string> {\n if (!this.session) {\n return HANDOFF;\n }\n // Reset per-question so prompts stay independent (best-effort across node-llama-cpp versions).\n try {\n this.session.setChatHistory?.([]);\n } catch {\n /* ignore */\n }\n const answer: string = await this.session.prompt(question, { maxTokens: this.opts.maxTokens || 512 });\n return (answer || '').trim();\n }\n\n async dispose(): Promise<void> {\n try {\n await this.context?.dispose?.();\n await this.model?.dispose?.();\n } catch {\n /* ignore */\n }\n this.session = this.context = this.model = null;\n }\n}\n\n/**\n * Refusal patterns (de/en/ru) a small tool-free model emits when it *should* have handed off — e.g.\n * \"I'm an AI and can't control real devices\". Treated as a hand-off so the tool-enabled cloud LLM runs.\n */\nconst REFUSAL = [\n /\\bhandoff\\b/i,\n // en\n /\\bi('?m| am)\\s+an?\\s+ai\\b/i,\n /\\bi\\s+(can('?t|not)|am\\s+unable\\s+to)\\b.*\\b(turn|switch|control|access|interact|device|light)\\b/i,\n // de\n /\\bich\\s+bin\\s+eine?\\s+(ki|künstliche)\\b/i,\n /\\bkann\\s+.*\\bnicht\\b.*\\b(schalten|steuern|einschalten|zugreifen|geräte?|licht)\\b/i,\n // ru (\\w/\\b are ASCII-only → use Cyrillic classes / plain substrings)\n /искусственн[а-яё]*\\s+интеллект/i,\n /не\\s+(могу|имею)/i,\n];\n\n/** True if the model's answer signals (or implies) a hand-off to the cloud LLM. */\nexport function isHandoff(answer: string): boolean {\n const trimmed = answer.trim();\n const up = trimmed.toUpperCase();\n if (up === HANDOFF || up.startsWith(`${HANDOFF} `) || up === `\"${HANDOFF}\"`) {\n return true;\n }\n return REFUSAL.some(re => re.test(trimmed));\n}\n"]}
|