iobroker.assistant 1.0.3 → 1.2.0

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.
Files changed (73) hide show
  1. package/README.md +65 -0
  2. package/admin/custom/.vite/manifest.json +62 -62
  3. package/admin/custom/assets/Components-BSewCBfS.js +2 -0
  4. package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-A4gakomR.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-C5NPM0Of.js} +1 -1
  5. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-CwqmfMvn.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-IaosaymG.js} +1 -1
  6. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BMuLYe6f.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cdyjt5KT.js} +39 -39
  7. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare__shared-BxjZmnpc.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared-Dsw5zWS8.js} +1 -1
  8. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-SgSagTOv.js +122 -0
  9. package/admin/custom/assets/{hostInit-4roC4hEj.js → hostInit-jJe8bIfV.js} +1 -1
  10. package/admin/custom/assets/{lib-CchzgTUs.js → lib-CL8mXvI6.js} +1 -1
  11. package/admin/custom/assets/{mf-entry-bootstrap-0-4806bbe4.js → mf-entry-bootstrap-0-ab227aa6.js} +2 -2
  12. package/admin/custom/assets/{pendingShares-DLlv8yFY.js → pendingShares-wrGtfliQ.js} +1 -1
  13. package/admin/custom/customComponents.js +2 -2
  14. package/admin/custom/i18n/de.json +40 -0
  15. package/admin/custom/i18n/en.json +40 -0
  16. package/admin/custom/i18n/es.json +40 -0
  17. package/admin/custom/i18n/fr.json +40 -0
  18. package/admin/custom/i18n/it.json +40 -0
  19. package/admin/custom/i18n/nl.json +40 -0
  20. package/admin/custom/i18n/pl.json +40 -0
  21. package/admin/custom/i18n/pt.json +40 -0
  22. package/admin/custom/i18n/ru.json +40 -0
  23. package/admin/custom/i18n/uk.json +40 -0
  24. package/admin/custom/i18n/zh-cn.json +40 -0
  25. package/admin/custom/index.html +2 -2
  26. package/admin/custom/mf-manifest.json +1 -1
  27. package/admin/custom/mf-stats.json +1 -1
  28. package/admin/i18n/de.json +13 -3
  29. package/admin/i18n/en.json +13 -3
  30. package/admin/i18n/es.json +13 -3
  31. package/admin/i18n/fr.json +13 -3
  32. package/admin/i18n/it.json +13 -3
  33. package/admin/i18n/nl.json +13 -3
  34. package/admin/i18n/pl.json +13 -3
  35. package/admin/i18n/pt.json +13 -3
  36. package/admin/i18n/ru.json +13 -3
  37. package/admin/i18n/uk.json +13 -3
  38. package/admin/i18n/zh-cn.json +13 -3
  39. package/admin/jsonConfig.json +61 -2
  40. package/build/lib/journal.js +298 -2
  41. package/build/lib/journal.js.map +1 -1
  42. package/build/lib/llm.js +105 -4
  43. package/build/lib/llm.js.map +1 -1
  44. package/build/lib/localLlm.js +47 -10
  45. package/build/lib/localLlm.js.map +1 -1
  46. package/build/lib/nlu.js +124 -5
  47. package/build/lib/nlu.js.map +1 -1
  48. package/build/lib/tools.js +3 -0
  49. package/build/lib/tools.js.map +1 -1
  50. package/build/lib/voice/esphome.js +81 -7
  51. package/build/lib/voice/esphome.js.map +1 -1
  52. package/build/lib/voice/esphomeProto.js.map +1 -1
  53. package/build/lib/voice/mediaServer.js +29 -1
  54. package/build/lib/voice/mediaServer.js.map +1 -1
  55. package/build/lib/voice/protocol.js.map +1 -1
  56. package/build/lib/voice/voiceServer.js +81 -7
  57. package/build/lib/voice/voiceServer.js.map +1 -1
  58. package/build/lib/voice/wakeWords.js +284 -0
  59. package/build/lib/voice/wakeWords.js.map +1 -0
  60. package/build/lib/voice/wyoming.js +12 -4
  61. package/build/lib/voice/wyoming.js.map +1 -1
  62. package/build/lib/weather.js +160 -0
  63. package/build/lib/weather.js.map +1 -1
  64. package/build/lib/youtube.js +625 -0
  65. package/build/lib/youtube.js.map +1 -0
  66. package/build/main.js +1152 -76
  67. package/build/main.js.map +1 -1
  68. package/io-package.json +30 -14
  69. package/package.json +6 -6
  70. package/admin/custom/assets/Components-C6qcpaRC.js +0 -1
  71. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-TM4l0OYm.js +0 -122
  72. /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js} +0 -0
  73. /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js} +0 -0
package/build/lib/llm.js CHANGED
@@ -8,6 +8,7 @@ exports.resolveProvider = resolveProvider;
8
8
  exports.effectiveApiKey = effectiveApiKey;
9
9
  const openai_1 = __importDefault(require("openai"));
10
10
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
11
+ const journal_1 = require("./journal");
11
12
  /** Per-provider SDK, endpoint and default model. Gemini/DeepSeek expose OpenAI-compatible endpoints. */
12
13
  exports.PROVIDER_PRESETS = {
13
14
  openai: { sdk: 'openai', baseUrl: '', defaultModel: 'gpt-4o-mini' },
@@ -37,6 +38,29 @@ function resolveProvider(provider, configBaseUrl) {
37
38
  function effectiveApiKey(provider, apiKey) {
38
39
  return apiKey || (provider === 'custom' ? 'no-key' : '');
39
40
  }
41
+ /** Normalised token counts of an OpenAI-compatible or Anthropic `usage` block. */
42
+ function tokensOf(usage) {
43
+ if (!usage || typeof usage !== 'object') {
44
+ return undefined;
45
+ }
46
+ if (usage.input_tokens !== undefined) {
47
+ return {
48
+ input: Number(usage.input_tokens) || 0,
49
+ output: Number(usage.output_tokens) || 0,
50
+ cacheRead: Number(usage.cache_read_input_tokens) || undefined,
51
+ cacheWrite: Number(usage.cache_creation_input_tokens) || undefined,
52
+ };
53
+ }
54
+ return {
55
+ input: Number(usage.prompt_tokens) || 0,
56
+ output: Number(usage.completion_tokens) || 0,
57
+ cacheRead: Number(usage.prompt_tokens_details?.cached_tokens) || undefined,
58
+ };
59
+ }
60
+ /** "Round 2 → get_states, set_state" or "Round 3 → answer" — the title of a round in the trace. */
61
+ function roundTitle(round, toolNames) {
62
+ return `Round ${round + 1} → ${toolNames.length ? toolNames.join(', ') : 'answer'}`;
63
+ }
40
64
  /**
41
65
  * Provider-agnostic LLM agent with tool-calling.
42
66
  *
@@ -58,10 +82,17 @@ class LlmAgent {
58
82
  toolMap;
59
83
  log;
60
84
  maxRounds = 6; // safety cap against tool-call loops
85
+ /** Configured provider (gemini, deepseek, custom … — {@link provider} is only the SDK). */
86
+ providerName;
87
+ baseUrl;
88
+ trace;
61
89
  openai;
62
90
  anthropic;
63
91
  constructor(opts) {
64
92
  this.provider = opts.provider === 'anthropic' ? 'anthropic' : 'openai';
93
+ this.providerName = opts.provider || this.provider;
94
+ this.baseUrl = opts.baseUrl || '';
95
+ this.trace = opts.trace;
65
96
  this.configuredModel = (opts.model || '').trim();
66
97
  this.systemPrompt = opts.systemPrompt || '';
67
98
  this.maxTokens = opts.maxTokens || 1024;
@@ -114,6 +145,20 @@ class LlmAgent {
114
145
  this.log.debug(`llm ask [${this.provider}/${this.model}] (${history.length} history): ${question}`);
115
146
  const started = Date.now();
116
147
  const sys = systemPrompt ?? this.systemPrompt;
148
+ this.trace?.({
149
+ stage: 'cloud',
150
+ title: `${this.providerName} · ${this.model}: ${this.tools.length} tools, ${history.length} earlier turn(s)`,
151
+ data: {
152
+ provider: this.providerName,
153
+ sdk: this.provider,
154
+ model: this.model,
155
+ baseUrl: this.baseUrl || undefined,
156
+ maxTokens: this.maxTokens,
157
+ maxRounds: this.maxRounds,
158
+ system: sys,
159
+ tools: this.tools.map(t => t.name),
160
+ },
161
+ });
117
162
  const answer = this.provider === 'anthropic'
118
163
  ? await this.askAnthropic(question, sys, history)
119
164
  : await this.askOpenAI(question, sys, history);
@@ -281,6 +326,30 @@ class LlmAgent {
281
326
  .filter(id => !EXCLUDE.test(id))
282
327
  .sort();
283
328
  }
329
+ /**
330
+ * One model request of the tool loop, timed. A failure goes into the trace together with what was sent
331
+ * (status, API error body, stack) before it is rethrown.
332
+ */
333
+ async timedRound(round, sent, call) {
334
+ const start = Date.now();
335
+ try {
336
+ return { resp: await call(), ms: Date.now() - start };
337
+ }
338
+ catch (e) {
339
+ this.trace?.({
340
+ stage: 'cloud',
341
+ level: 'error',
342
+ title: `Round ${round + 1} failed: ${e.message}`,
343
+ ms: Date.now() - start,
344
+ data: { sent, error: (0, journal_1.errorInfo)(e) },
345
+ });
346
+ throw e;
347
+ }
348
+ }
349
+ tooManyRounds() {
350
+ this.trace?.({ stage: 'cloud', level: 'warn', title: `Gave up after ${this.maxRounds} rounds of tool calls` });
351
+ return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';
352
+ }
284
353
  async runTool(name, args) {
285
354
  const tool = this.toolMap[name];
286
355
  if (!tool) {
@@ -311,6 +380,8 @@ class LlmAgent {
311
380
  messages.push({ role: t.role, content: t.content });
312
381
  }
313
382
  messages.push({ role: 'user', content: question });
383
+ // Messages the trace has shown already: the system prompt is part of the opening step.
384
+ let shown = systemPrompt ? 1 : 0;
314
385
  for (let round = 0; round < this.maxRounds; round++) {
315
386
  const request = {
316
387
  model: this.model,
@@ -319,10 +390,24 @@ class LlmAgent {
319
390
  max_tokens: this.maxTokens,
320
391
  };
321
392
  this.dump(`llm → openai round ${round}`, request);
322
- const resp = await client.chat.completions.create(request);
393
+ const sent = messages.slice(shown);
394
+ const { resp, ms } = await this.timedRound(round, sent, () => client.chat.completions.create(request));
323
395
  const msg = resp.choices[0].message;
324
396
  this.dump(`llm ← openai round ${round}`, { message: msg, usage: resp.usage });
325
397
  messages.push(msg);
398
+ shown = messages.length;
399
+ this.trace?.({
400
+ stage: 'cloud',
401
+ title: roundTitle(round, (msg.tool_calls || []).map((tc) => String(tc.function?.name))),
402
+ ms,
403
+ tokens: tokensOf(resp.usage),
404
+ data: {
405
+ sent,
406
+ response: { finish_reason: resp.choices[0].finish_reason, message: msg },
407
+ model: resp.model,
408
+ usage: resp.usage,
409
+ },
410
+ });
326
411
  if (!msg.tool_calls || !msg.tool_calls.length) {
327
412
  return String(msg.content || '').trim();
328
413
  }
@@ -338,7 +423,7 @@ class LlmAgent {
338
423
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) });
339
424
  }
340
425
  }
341
- return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';
426
+ return this.tooManyRounds();
342
427
  }
343
428
  /**
344
429
  * Keep a single cache breakpoint on the last content block of the last message (Anthropic allows
@@ -388,6 +473,8 @@ class LlmAgent {
388
473
  ...history.map(t => ({ role: t.role, content: t.content })),
389
474
  { role: 'user', content: question },
390
475
  ];
476
+ // Messages the trace has shown already (the system prompt is part of the opening step).
477
+ let shown = 0;
391
478
  for (let round = 0; round < this.maxRounds; round++) {
392
479
  // Cache the growing conversation prefix too (e.g. the large list_devices result): move a
393
480
  // single cache breakpoint to the last content block of the last message each round.
@@ -400,9 +487,22 @@ class LlmAgent {
400
487
  messages,
401
488
  };
402
489
  this.dump(`llm → anthropic round ${round}`, request);
403
- const resp = await client.messages.create(request);
490
+ const sent = messages.slice(shown);
491
+ const { resp, ms } = await this.timedRound(round, sent, () => client.messages.create(request));
404
492
  this.dump(`llm ← anthropic round ${round}`, { content: resp.content, usage: resp.usage });
405
493
  const toolUses = resp.content.filter((b) => b.type === 'tool_use');
494
+ this.trace?.({
495
+ stage: 'cloud',
496
+ title: roundTitle(round, toolUses.map((b) => String(b.name))),
497
+ ms,
498
+ tokens: tokensOf(resp.usage),
499
+ data: {
500
+ sent,
501
+ response: { stop_reason: resp.stop_reason, content: resp.content },
502
+ model: resp.model,
503
+ usage: resp.usage,
504
+ },
505
+ });
406
506
  if (!toolUses.length) {
407
507
  return resp.content
408
508
  .filter((b) => b.type === 'text')
@@ -411,6 +511,7 @@ class LlmAgent {
411
511
  .trim();
412
512
  }
413
513
  messages.push({ role: 'assistant', content: resp.content });
514
+ shown = messages.length;
414
515
  const toolResults = [];
415
516
  for (const tu of toolUses) {
416
517
  const result = await this.runTool(tu.name, tu.input);
@@ -418,7 +519,7 @@ class LlmAgent {
418
519
  }
419
520
  messages.push({ role: 'user', content: toolResults });
420
521
  }
421
- return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';
522
+ return this.tooManyRounds();
422
523
  }
423
524
  }
424
525
  exports.LlmAgent = LlmAgent;
@@ -1 +1 @@
1
- {"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/lib/llm.ts"],"names":[],"mappings":";;;;;;AA8BA,0CAUC;AAOD,0CAEC;AAjDD,oDAA4B;AAC5B,4DAA0C;AAe1C,wGAAwG;AAC3F,QAAA,gBAAgB,GAAqC;IAC9D,MAAM,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,aAAa,EAAE;IACnE,SAAS,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE;IAC/E,MAAM,EAAE;QACJ,GAAG,EAAE,QAAQ;QACb,OAAO,EAAE,0DAA0D;QACnE,YAAY,EAAE,kBAAkB;KACnC;IACD,QAAQ,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,0BAA0B,EAAE,YAAY,EAAE,eAAe,EAAE;IAC/F,MAAM,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE;CAC3D,CAAC;AAEF,0GAA0G;AAC1G,SAAgB,eAAe,CAC3B,QAA4B,EAC5B,aAAsB;IAEtB,MAAM,MAAM,GAAG,wBAAgB,CAAE,QAAqB,IAAI,QAAQ,CAAC,IAAI,wBAAgB,CAAC,MAAM,CAAC;IAC/F,OAAO;QACH,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,OAAO,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO;QACrE,YAAY,EAAE,MAAM,CAAC,YAAY;KACpC,CAAC;AACN,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAAC,QAA4B,EAAE,MAAc;IACxE,OAAO,MAAM,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC7D,CAAC;AAaD;;;;;;;;;GASG;AACH,MAAa,QAAQ;IACD,QAAQ,CAAyB;IACjD,gFAAgF;IACxE,eAAe,CAAS;IACxB,WAAW,GAA2B,IAAI,CAAC;IAElC,YAAY,CAAS;IACrB,SAAS,CAAS;IAClB,KAAK,CAAS;IACd,OAAO,CAAuB;IAC9B,GAAG,CAAkB;IACrB,SAAS,GAAG,CAAC,CAAC,CAAC,qCAAqC;IAEpD,MAAM,CAAU;IAChB,SAAS,CAAa;IAEvC,YAAY,IAAqB;QAC7B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC;QACvE,IAAI,CAAC,eAAe,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACjD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;QAC5C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC;QACxC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAEpE,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YAChC,IAAI,CAAC,SAAS,GAAG,IAAI,aAAS,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;QAChG,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;QAC1F,CAAC;IACL,CAAC;IAED,2GAA2G;IAC3G,IAAW,KAAK;QACZ,OAAO,IAAI,CAAC,eAAe,CAAC;IAChC,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,WAAW;QACrB,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,eAAe,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,UAAU,EAAE;aACjC,IAAI,CAAC,MAAM,CAAC,EAAE;YACX,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACrE,CAAC;YACD,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,IAAI,CACT,gCAAgC,MAAM,CAAC,CAAC,CAAC,oBACrC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAC/D,6DAA6D,CAChE,CAAC;YACF,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,CAAC,EAAE;YACP,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,gEAAgE;YACzF,MAAM,IAAI,KAAK,CACX,4EAA6E,CAAW,CAAC,OAAO,GAAG,CACtG,CAAC;QACN,CAAC,CAAC,CAAC;QACP,OAAO,IAAI,CAAC,WAAW,CAAC;IAC5B,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,GAAG,CACZ,QAAgB,EAChB,YAAqB,EACrB,UAA6D,EAAE;QAE/D,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACzB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK,MAAM,OAAO,CAAC,MAAM,cAAc,QAAQ,EAAE,CAAC,CAAC;QACpG,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC;QAC9C,MAAM,MAAM,GACR,IAAI,CAAC,QAAQ,KAAK,WAAW;YACzB,CAAC,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC;YACjD,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QACvD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,SAAS,MAAM,EAAE,CAAC,CAAC;QACrE,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,2EAA2E;IACnE,IAAI,CAAC,KAAa,EAAE,OAAgB;QACxC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YAC3D,OAAO,CAAC,8DAA8D;QAC1E,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACD,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACL,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACvG,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,QAAQ,CAAC,MAAc,EAAE,SAAiB;QACpD,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAsB,CAAC;YAC3C,MAAM,IAAI,GAAQ,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC3C,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,UAAU,EAAE,SAAS;gBACrB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;aAChD,CAAC,CAAC;YACH,OAAO,IAAI,CAAC,OAAO;iBACd,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;iBACrC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;iBACvB,IAAI,CAAC,EAAE,CAAC;iBACR,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;QACrC,MAAM,IAAI,GAAQ,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC;YACnD,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,SAAS;YACrB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;SAChD,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChE,CAAC;IAED,8FAA8F;IACvF,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,cAAsB;QACvD,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAChB,+DAA+D,cAAc,IAAI;YAC7E,yFAAyF,OAAO,EAAE,EACtG,EAAE,CACL,CAAC;IACN,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,QAAQ,CAAC,IAAY,EAAE,QAAgB,EAAE,OAAO,GAAG,EAAE;QAC9D,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC9B,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,yDAAyD;gBAC9F,SAAS,QAAQ,CAAC,CAAC,CAAC,QAAQ,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,gDAAgD;gBAC3F,4FAA4F;gBAC5F,8CAA8C,OAAO,EAAE,EAC3D,GAAG,CACN,CAAC;YACF,OAAO,MAAM,IAAI,OAAO,CAAC;QAC7B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,6CAA8C,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACnF,OAAO,OAAO,CAAC;QACnB,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,kBAAkB,CAAC,IAAY,EAAE,IAAY,EAAE,QAAgB,EAAE,OAAO,GAAG,EAAE;QACtF,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC9B,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,oDAAoD;gBACzF,kCAAkC,QAAQ,CAAC,CAAC,CAAC,QAAQ,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI;gBACxE,wEAAwE;gBACxE,wFAAwF;gBACxF,gEAAgE;gBAChE,sFAAsF;gBACtF,uFAAuF;gBACvF,uBAAuB,IAAI,mDAAmD,OAAO,EAAE,EAC3F,GAAG,CACN,CAAC;YACF,OAAO,MAAM,IAAI,OAAO,CAAC;QAC7B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,yDAA0D,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YAC/F,OAAO,OAAO,CAAC;QACnB,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,QAAQ,CAAC,MAAc,EAAE,QAAgB;QAClD,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC/B,qCAAqC,IAAI,4CAA4C;gBACjF,mFAAmF,IAAI,EAAE,EAC7F,CAAC,CACJ,CAAC;YACF,OAAO,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,qBAAqB,IAAI,OAAQ,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACtE,OAAO,KAAK,CAAC;QACjB,CAAC;IACL,CAAC;IAED,6FAA6F;IACtF,KAAK,CAAC,cAAc;QACvB,IAAI,CAAC;YACD,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAsB,CAAC;gBAC3C,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;oBACzB,KAAK,EAAE,MAAM,IAAI,CAAC,WAAW,EAAE;oBAC/B,UAAU,EAAE,CAAC;oBACb,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;iBAChD,CAAC,CAAC;YACP,CAAC;iBAAM,CAAC;gBACJ,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;gBACrC,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC/B,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;QACtD,CAAC;IACL,CAAC;IAED,+EAA+E;IACxE,KAAK,CAAC,UAAU;QACnB,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAsB,CAAC;YAC3C,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAA2C,CAAC;YACnF,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;QACrC,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAA2C,CAAC;QACnF,gGAAgG;QAChG,MAAM,OAAO,GACT,uGAAuG,CAAC;QAC5G,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;aAClB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aACtB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;aAC/B,IAAI,EAAE,CAAC;IAChB,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAA6B;QAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,OAAO,EAAE,KAAK,EAAE,iBAAiB,IAAI,EAAE,EAAE,CAAC;QAC9C,CAAC;QACD,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;YAC1C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YACjG,OAAO,MAAM,CAAC;QAClB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,YAAa,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YAC9D,OAAO,EAAE,KAAK,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;QAC3C,CAAC;IACL,CAAC;IAED,2EAA2E;IACnE,KAAK,CAAC,SAAS,CACnB,QAAgB,EAChB,YAAoB,EACpB,UAA6D,EAAE;QAE/D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC/B,IAAI,EAAE,UAAmB;YACzB,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE;SACnF,CAAC,CAAC,CAAC;QAEJ,MAAM,QAAQ,GAAU,EAAE,CAAC;QAC3B,IAAI,YAAY,EAAE,CAAC;YACf,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEnD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC;YAClD,MAAM,OAAO,GAAG;gBACZ,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ;gBACR,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;gBACvC,UAAU,EAAE,IAAI,CAAC,SAAS;aAC7B,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,sBAAsB,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC;YAClD,MAAM,IAAI,GAAQ,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAChE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YACpC,IAAI,CAAC,IAAI,CAAC,sBAAsB,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;YAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAEnB,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;gBAC5C,OAAO,MAAM,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5C,CAAC;YAED,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;gBAC9B,IAAI,IAAI,GAA4B,EAAE,CAAC;gBACvC,IAAI,CAAC;oBACD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;gBACrD,CAAC;gBAAC,MAAM,CAAC;oBACL,iBAAiB;gBACrB,CAAC;gBACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBAC1D,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC1F,CAAC;QACL,CAAC;QACD,OAAO,oEAAoE,CAAC;IAChF,CAAC;IAED;;;OAGG;IACK,uBAAuB,CAAC,QAAe;QAC3C,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACvB,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;oBACxB,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;wBAC7B,OAAO,CAAC,CAAC,aAAa,CAAC;oBAC3B,CAAC;gBACL,CAAC;YACL,CAAC;QACL,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,OAAO;QACX,CAAC;QACD,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;QAChG,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC5D,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAChD,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;gBAC7B,CAAC,CAAC,aAAa,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;YAC5C,CAAC;QACL,CAAC;IACL,CAAC;IAED,2EAA2E;IACnE,KAAK,CAAC,YAAY,CACtB,QAAgB,EAChB,YAAoB,EACpB,UAA6D,EAAE;QAE/D,MAAM,MAAM,GAAG,IAAI,CAAC,SAAsB,CAAC;QAC3C,MAAM,KAAK,GAAU,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACtC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,YAAY,EAAE,CAAC,CAAC,UAAwC;SAC3D,CAAC,CAAC,CAAC;QACJ,4FAA4F;QAC5F,iGAAiG;QACjG,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YACf,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;QACnG,CAAC;QACD,MAAM,MAAM,GAAQ,YAAY;YAC5B,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;YAC9E,CAAC,CAAC,SAAS,CAAC;QAEhB,MAAM,QAAQ,GAAU;YACpB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3D,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE;SACtC,CAAC;QAEF,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC;YAClD,yFAAyF;YACzF,oFAAoF;YACpF,IAAI,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,OAAO,GAAG;gBACZ,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,UAAU,EAAE,IAAI,CAAC,SAAS;gBAC1B,MAAM;gBACN,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;gBACvC,QAAQ;aACX,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,yBAAyB,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC;YACrD,MAAM,IAAI,GAAQ,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACxD,IAAI,CAAC,IAAI,CAAC,yBAAyB,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;YAE1F,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;YACxE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACnB,OAAO,IAAI,CAAC,OAAO;qBACd,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;qBACrC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;qBACvB,IAAI,CAAC,EAAE,CAAC;qBACR,IAAI,EAAE,CAAC;YAChB,CAAC;YAED,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5D,MAAM,WAAW,GAAG,EAAE,CAAC;YACvB,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;gBACxB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;gBACrD,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACnG,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,oEAAoE,CAAC;IAChF,CAAC;CACJ;AAtaD,4BAsaC","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"]}
1
+ {"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/lib/llm.ts"],"names":[],"mappings":";;;;;;AA+BA,0CAUC;AAOD,0CAEC;AAlDD,oDAA4B;AAC5B,4DAA0C;AAE1C,uCAAwE;AAcxE,wGAAwG;AAC3F,QAAA,gBAAgB,GAAqC;IAC9D,MAAM,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,aAAa,EAAE;IACnE,SAAS,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE;IAC/E,MAAM,EAAE;QACJ,GAAG,EAAE,QAAQ;QACb,OAAO,EAAE,0DAA0D;QACnE,YAAY,EAAE,kBAAkB;KACnC;IACD,QAAQ,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,0BAA0B,EAAE,YAAY,EAAE,eAAe,EAAE;IAC/F,MAAM,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE;CAC3D,CAAC;AAEF,0GAA0G;AAC1G,SAAgB,eAAe,CAC3B,QAA4B,EAC5B,aAAsB;IAEtB,MAAM,MAAM,GAAG,wBAAgB,CAAE,QAAqB,IAAI,QAAQ,CAAC,IAAI,wBAAgB,CAAC,MAAM,CAAC;IAC/F,OAAO;QACH,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,OAAO,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO;QACrE,YAAY,EAAE,MAAM,CAAC,YAAY;KACpC,CAAC;AACN,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAAC,QAA4B,EAAE,MAAc;IACxE,OAAO,MAAM,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC7D,CAAC;AAeD,kFAAkF;AAClF,SAAS,QAAQ,CAAC,KAAU;IACxB,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO;YACH,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC;YACtC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC;YACxC,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,IAAI,SAAS;YAC7D,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,2BAA2B,CAAC,IAAI,SAAS;SACrE,CAAC;IACN,CAAC;IACD,OAAO;QACH,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC;QACvC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC;QAC5C,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,qBAAqB,EAAE,aAAa,CAAC,IAAI,SAAS;KAC7E,CAAC;AACN,CAAC;AAED,mGAAmG;AACnG,SAAS,UAAU,CAAC,KAAa,EAAE,SAAmB;IAClD,OAAO,SAAS,KAAK,GAAG,CAAC,MAAM,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;AACxF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAa,QAAQ;IACD,QAAQ,CAAyB;IACjD,gFAAgF;IACxE,eAAe,CAAS;IACxB,WAAW,GAA2B,IAAI,CAAC;IAElC,YAAY,CAAS;IACrB,SAAS,CAAS;IAClB,KAAK,CAAS;IACd,OAAO,CAAuB;IAC9B,GAAG,CAAkB;IACrB,SAAS,GAAG,CAAC,CAAC,CAAC,qCAAqC;IACrE,2FAA2F;IAC1E,YAAY,CAAS;IACrB,OAAO,CAAS;IAChB,KAAK,CAAW;IAEhB,MAAM,CAAU;IAChB,SAAS,CAAa;IAEvC,YAAY,IAAqB;QAC7B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC;QACvE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC;QACnD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;QAClC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,eAAe,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACjD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;QAC5C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC;QACxC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAEpE,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YAChC,IAAI,CAAC,SAAS,GAAG,IAAI,aAAS,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;QAChG,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;QAC1F,CAAC;IACL,CAAC;IAED,2GAA2G;IAC3G,IAAW,KAAK;QACZ,OAAO,IAAI,CAAC,eAAe,CAAC;IAChC,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,WAAW;QACrB,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,eAAe,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,UAAU,EAAE;aACjC,IAAI,CAAC,MAAM,CAAC,EAAE;YACX,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACrE,CAAC;YACD,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,IAAI,CACT,gCAAgC,MAAM,CAAC,CAAC,CAAC,oBACrC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAC/D,6DAA6D,CAChE,CAAC;YACF,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,CAAC,EAAE;YACP,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,gEAAgE;YACzF,MAAM,IAAI,KAAK,CACX,4EAA6E,CAAW,CAAC,OAAO,GAAG,CACtG,CAAC;QACN,CAAC,CAAC,CAAC;QACP,OAAO,IAAI,CAAC,WAAW,CAAC;IAC5B,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,GAAG,CACZ,QAAgB,EAChB,YAAqB,EACrB,UAA6D,EAAE;QAE/D,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACzB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK,MAAM,OAAO,CAAC,MAAM,cAAc,QAAQ,EAAE,CAAC,CAAC;QACpG,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC;QAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,KAAK,EAAE,OAAO;YACd,KAAK,EAAE,GAAG,IAAI,CAAC,YAAY,MAAM,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,WAAW,OAAO,CAAC,MAAM,kBAAkB;YAC5G,IAAI,EAAE;gBACF,QAAQ,EAAE,IAAI,CAAC,YAAY;gBAC3B,GAAG,EAAE,IAAI,CAAC,QAAQ;gBAClB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,SAAS;gBAClC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,GAAG;gBACX,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aACrC;SACJ,CAAC,CAAC;QACH,MAAM,MAAM,GACR,IAAI,CAAC,QAAQ,KAAK,WAAW;YACzB,CAAC,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC;YACjD,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QACvD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,SAAS,MAAM,EAAE,CAAC,CAAC;QACrE,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,2EAA2E;IACnE,IAAI,CAAC,KAAa,EAAE,OAAgB;QACxC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YAC3D,OAAO,CAAC,8DAA8D;QAC1E,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACD,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACL,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACvG,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,QAAQ,CAAC,MAAc,EAAE,SAAiB;QACpD,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAsB,CAAC;YAC3C,MAAM,IAAI,GAAQ,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC3C,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,UAAU,EAAE,SAAS;gBACrB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;aAChD,CAAC,CAAC;YACH,OAAO,IAAI,CAAC,OAAO;iBACd,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;iBACrC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;iBACvB,IAAI,CAAC,EAAE,CAAC;iBACR,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;QACrC,MAAM,IAAI,GAAQ,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC;YACnD,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,SAAS;YACrB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;SAChD,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChE,CAAC;IAED,8FAA8F;IACvF,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,cAAsB;QACvD,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAChB,+DAA+D,cAAc,IAAI;YAC7E,yFAAyF,OAAO,EAAE,EACtG,EAAE,CACL,CAAC;IACN,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,QAAQ,CAAC,IAAY,EAAE,QAAgB,EAAE,OAAO,GAAG,EAAE;QAC9D,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC9B,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,yDAAyD;gBAC9F,SAAS,QAAQ,CAAC,CAAC,CAAC,QAAQ,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,gDAAgD;gBAC3F,4FAA4F;gBAC5F,8CAA8C,OAAO,EAAE,EAC3D,GAAG,CACN,CAAC;YACF,OAAO,MAAM,IAAI,OAAO,CAAC;QAC7B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,6CAA8C,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACnF,OAAO,OAAO,CAAC;QACnB,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,kBAAkB,CAAC,IAAY,EAAE,IAAY,EAAE,QAAgB,EAAE,OAAO,GAAG,EAAE;QACtF,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC9B,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,oDAAoD;gBACzF,kCAAkC,QAAQ,CAAC,CAAC,CAAC,QAAQ,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI;gBACxE,wEAAwE;gBACxE,wFAAwF;gBACxF,gEAAgE;gBAChE,sFAAsF;gBACtF,uFAAuF;gBACvF,uBAAuB,IAAI,mDAAmD,OAAO,EAAE,EAC3F,GAAG,CACN,CAAC;YACF,OAAO,MAAM,IAAI,OAAO,CAAC;QAC7B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,yDAA0D,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YAC/F,OAAO,OAAO,CAAC;QACnB,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,QAAQ,CAAC,MAAc,EAAE,QAAgB;QAClD,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC/B,qCAAqC,IAAI,4CAA4C;gBACjF,mFAAmF,IAAI,EAAE,EAC7F,CAAC,CACJ,CAAC;YACF,OAAO,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,qBAAqB,IAAI,OAAQ,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACtE,OAAO,KAAK,CAAC;QACjB,CAAC;IACL,CAAC;IAED,6FAA6F;IACtF,KAAK,CAAC,cAAc;QACvB,IAAI,CAAC;YACD,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAsB,CAAC;gBAC3C,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;oBACzB,KAAK,EAAE,MAAM,IAAI,CAAC,WAAW,EAAE;oBAC/B,UAAU,EAAE,CAAC;oBACb,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;iBAChD,CAAC,CAAC;YACP,CAAC;iBAAM,CAAC;gBACJ,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;gBACrC,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC/B,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;QACtD,CAAC;IACL,CAAC;IAED,+EAA+E;IACxE,KAAK,CAAC,UAAU;QACnB,IAAI,IAAI,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAsB,CAAC;YAC3C,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAA2C,CAAC;YACnF,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;QACrC,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAA2C,CAAC;QACnF,gGAAgG;QAChG,MAAM,OAAO,GACT,uGAAuG,CAAC;QAC5G,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;aAClB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aACtB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;aAC/B,IAAI,EAAE,CAAC;IAChB,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,UAAU,CACpB,KAAa,EACb,IAAe,EACf,IAAwB;QAExB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,IAAI,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;QAC1D,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,KAAK,EAAE,CAAC;gBACT,KAAK,EAAE,OAAO;gBACd,KAAK,EAAE,OAAO;gBACd,KAAK,EAAE,SAAS,KAAK,GAAG,CAAC,YAAa,CAAW,CAAC,OAAO,EAAE;gBAC3D,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;gBACtB,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAA,mBAAS,EAAC,CAAC,CAAC,EAAE;aACtC,CAAC,CAAC;YACH,MAAM,CAAC,CAAC;QACZ,CAAC;IACL,CAAC;IAEO,aAAa;QACjB,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,IAAI,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAAC;QAC/G,OAAO,oEAAoE,CAAC;IAChF,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAA6B;QAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,OAAO,EAAE,KAAK,EAAE,iBAAiB,IAAI,EAAE,EAAE,CAAC;QAC9C,CAAC;QACD,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;YAC1C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YACjG,OAAO,MAAM,CAAC;QAClB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,YAAa,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YAC9D,OAAO,EAAE,KAAK,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;QAC3C,CAAC;IACL,CAAC;IAED,2EAA2E;IACnE,KAAK,CAAC,SAAS,CACnB,QAAgB,EAChB,YAAoB,EACpB,UAA6D,EAAE;QAE/D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC/B,IAAI,EAAE,UAAmB;YACzB,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE;SACnF,CAAC,CAAC,CAAC;QAEJ,MAAM,QAAQ,GAAU,EAAE,CAAC;QAC3B,IAAI,YAAY,EAAE,CAAC;YACf,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QACnD,uFAAuF;QACvF,IAAI,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAEjC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC;YAClD,MAAM,OAAO,GAAG;gBACZ,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ;gBACR,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;gBACvC,UAAU,EAAE,IAAI,CAAC,SAAS;aAC7B,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,sBAAsB,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC;YAClD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACnC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YACvG,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YACpC,IAAI,CAAC,IAAI,CAAC,sBAAsB,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;YAC9E,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnB,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC;YACxB,IAAI,CAAC,KAAK,EAAE,CAAC;gBACT,KAAK,EAAE,OAAO;gBACd,KAAK,EAAE,UAAU,CACb,KAAK,EACL,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAO,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CACrE;gBACD,EAAE;gBACF,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;gBAC5B,IAAI,EAAE;oBACF,IAAI;oBACJ,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,GAAG,EAAE;oBACxE,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;iBACpB;aACJ,CAAC,CAAC;YAEH,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;gBAC5C,OAAO,MAAM,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5C,CAAC;YAED,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;gBAC9B,IAAI,IAAI,GAA4B,EAAE,CAAC;gBACvC,IAAI,CAAC;oBACD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;gBACrD,CAAC;gBAAC,MAAM,CAAC;oBACL,iBAAiB;gBACrB,CAAC;gBACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBAC1D,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC1F,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAChC,CAAC;IAED;;;OAGG;IACK,uBAAuB,CAAC,QAAe;QAC3C,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACvB,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;oBACxB,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;wBAC7B,OAAO,CAAC,CAAC,aAAa,CAAC;oBAC3B,CAAC;gBACL,CAAC;YACL,CAAC;QACL,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,OAAO;QACX,CAAC;QACD,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;QAChG,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC5D,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAChD,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;gBAC7B,CAAC,CAAC,aAAa,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;YAC5C,CAAC;QACL,CAAC;IACL,CAAC;IAED,2EAA2E;IACnE,KAAK,CAAC,YAAY,CACtB,QAAgB,EAChB,YAAoB,EACpB,UAA6D,EAAE;QAE/D,MAAM,MAAM,GAAG,IAAI,CAAC,SAAsB,CAAC;QAC3C,MAAM,KAAK,GAAU,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACtC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,YAAY,EAAE,CAAC,CAAC,UAAwC;SAC3D,CAAC,CAAC,CAAC;QACJ,4FAA4F;QAC5F,iGAAiG;QACjG,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YACf,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;QACnG,CAAC;QACD,MAAM,MAAM,GAAQ,YAAY;YAC5B,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;YAC9E,CAAC,CAAC,SAAS,CAAC;QAEhB,MAAM,QAAQ,GAAU;YACpB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3D,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE;SACtC,CAAC;QACF,wFAAwF;QACxF,IAAI,KAAK,GAAG,CAAC,CAAC;QAEd,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC;YAClD,yFAAyF;YACzF,oFAAoF;YACpF,IAAI,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,OAAO,GAAG;gBACZ,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,UAAU,EAAE,IAAI,CAAC,SAAS;gBAC1B,MAAM;gBACN,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;gBACvC,QAAQ;aACX,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,yBAAyB,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC;YACrD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACnC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YAC/F,IAAI,CAAC,IAAI,CAAC,yBAAyB,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;YAE1F,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;YACxE,IAAI,CAAC,KAAK,EAAE,CAAC;gBACT,KAAK,EAAE,OAAO;gBACd,KAAK,EAAE,UAAU,CACb,KAAK,EACL,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAC3C;gBACD,EAAE;gBACF,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;gBAC5B,IAAI,EAAE;oBACF,IAAI;oBACJ,QAAQ,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;oBAClE,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;iBACpB;aACJ,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACnB,OAAO,IAAI,CAAC,OAAO;qBACd,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;qBACrC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;qBACvB,IAAI,CAAC,EAAE,CAAC;qBACR,IAAI,EAAE,CAAC;YAChB,CAAC;YAED,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5D,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC;YACxB,MAAM,WAAW,GAAG,EAAE,CAAC;YACvB,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;gBACxB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;gBACrD,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACnG,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAChC,CAAC;CACJ;AA9fD,4BA8fC","sourcesContent":["import OpenAI from 'openai';\nimport Anthropic from '@anthropic-ai/sdk';\nimport type { Tool } from './tools';\nimport { errorInfo, type JournalTokens, type TraceFn } from './journal';\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 /** Records the prompt and every round of {@link LlmAgent.ask} (the request journal's debug view). */\n trace?: TraceFn;\n}\n\n/** Normalised token counts of an OpenAI-compatible or Anthropic `usage` block. */\nfunction tokensOf(usage: any): JournalTokens | undefined {\n if (!usage || typeof usage !== 'object') {\n return undefined;\n }\n if (usage.input_tokens !== undefined) {\n return {\n input: Number(usage.input_tokens) || 0,\n output: Number(usage.output_tokens) || 0,\n cacheRead: Number(usage.cache_read_input_tokens) || undefined,\n cacheWrite: Number(usage.cache_creation_input_tokens) || undefined,\n };\n }\n return {\n input: Number(usage.prompt_tokens) || 0,\n output: Number(usage.completion_tokens) || 0,\n cacheRead: Number(usage.prompt_tokens_details?.cached_tokens) || undefined,\n };\n}\n\n/** \"Round 2 → get_states, set_state\" or \"Round 3 → answer\" — the title of a round in the trace. */\nfunction roundTitle(round: number, toolNames: string[]): string {\n return `Round ${round + 1} → ${toolNames.length ? toolNames.join(', ') : 'answer'}`;\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 /** Configured provider (gemini, deepseek, custom … — {@link provider} is only the SDK). */\n private readonly providerName: string;\n private readonly baseUrl: string;\n private readonly trace?: TraceFn;\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.providerName = opts.provider || this.provider;\n this.baseUrl = opts.baseUrl || '';\n this.trace = opts.trace;\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 this.trace?.({\n stage: 'cloud',\n title: `${this.providerName} · ${this.model}: ${this.tools.length} tools, ${history.length} earlier turn(s)`,\n data: {\n provider: this.providerName,\n sdk: this.provider,\n model: this.model,\n baseUrl: this.baseUrl || undefined,\n maxTokens: this.maxTokens,\n maxRounds: this.maxRounds,\n system: sys,\n tools: this.tools.map(t => t.name),\n },\n });\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 /**\n * One model request of the tool loop, timed. A failure goes into the trace together with what was sent\n * (status, API error body, stack) before it is rethrown.\n */\n private async timedRound(\n round: number,\n sent: unknown[],\n call: () => Promise<any>,\n ): Promise<{ resp: any; ms: number }> {\n const start = Date.now();\n try {\n return { resp: await call(), ms: Date.now() - start };\n } catch (e) {\n this.trace?.({\n stage: 'cloud',\n level: 'error',\n title: `Round ${round + 1} failed: ${(e as Error).message}`,\n ms: Date.now() - start,\n data: { sent, error: errorInfo(e) },\n });\n throw e;\n }\n }\n\n private tooManyRounds(): string {\n this.trace?.({ stage: 'cloud', level: 'warn', title: `Gave up after ${this.maxRounds} rounds of tool calls` });\n return 'Ich konnte die Anfrage nicht abschließen (zu viele Tool-Schritte).';\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 // Messages the trace has shown already: the system prompt is part of the opening step.\n let shown = systemPrompt ? 1 : 0;\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 sent = messages.slice(shown);\n const { resp, ms } = await this.timedRound(round, sent, () => 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 shown = messages.length;\n this.trace?.({\n stage: 'cloud',\n title: roundTitle(\n round,\n (msg.tool_calls || []).map((tc: any) => String(tc.function?.name)),\n ),\n ms,\n tokens: tokensOf(resp.usage),\n data: {\n sent,\n response: { finish_reason: resp.choices[0].finish_reason, message: msg },\n model: resp.model,\n usage: resp.usage,\n },\n });\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 this.tooManyRounds();\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 // Messages the trace has shown already (the system prompt is part of the opening step).\n let shown = 0;\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 sent = messages.slice(shown);\n const { resp, ms } = await this.timedRound(round, sent, () => 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 this.trace?.({\n stage: 'cloud',\n title: roundTitle(\n round,\n toolUses.map((b: any) => String(b.name)),\n ),\n ms,\n tokens: tokensOf(resp.usage),\n data: {\n sent,\n response: { stop_reason: resp.stop_reason, content: resp.content },\n model: resp.model,\n usage: resp.usage,\n },\n });\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 shown = messages.length;\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 this.tooManyRounds();\n }\n}\n"]}
@@ -40,6 +40,7 @@ exports.installLocalLlm = installLocalLlm;
40
40
  exports.isHandoff = isHandoff;
41
41
  const node_child_process_1 = require("node:child_process");
42
42
  const node_module_1 = require("node:module");
43
+ const os = __importStar(require("node:os"));
43
44
  const node_url_1 = require("node:url");
44
45
  const path = __importStar(require("node:path"));
45
46
  const fs = __importStar(require("node:fs"));
@@ -117,9 +118,15 @@ class LocalLlm {
117
118
  session = null;
118
119
  model = null;
119
120
  context = null;
121
+ /** A prompt is running — the session takes one at a time. */
122
+ busy = false;
120
123
  constructor(opts) {
121
124
  this.opts = opts;
122
125
  }
126
+ /** A prompt is running, so {@link ask} would hand off at once. */
127
+ get running() {
128
+ return this.busy;
129
+ }
123
130
  /** Full system prompt = the strict HANDOFF rule (device work must escalate to the tool-enabled cloud LLM). */
124
131
  systemPrompt() {
125
132
  // The local model has NO tools and cannot touch the smart home. Anything device-related MUST hand off,
@@ -159,27 +166,57 @@ class LocalLlm {
159
166
  }
160
167
  const llama = await getLlama();
161
168
  this.model = await llama.loadModel({ modelPath });
162
- this.context = await this.model.createContext();
169
+ // Leave one core to js-controller and the other adapters: llama.cpp takes all of them otherwise, and on
170
+ // a Raspberry Pi the whole host — the voice server included — stalls for the length of an answer.
171
+ const cores = typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length;
172
+ this.context = await this.model.createContext({ threads: Math.max(1, cores - 1) });
163
173
  this.session = new LlamaChatSession({
164
174
  contextSequence: this.context.getSequence(),
165
175
  systemPrompt: this.systemPrompt(),
166
176
  });
167
177
  this.opts.log.info('Local model loaded.');
168
178
  }
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) {
179
+ /**
180
+ * Ask the local model. Returns its answer, or {@link HANDOFF} to escalate to the cloud LLM — also when
181
+ * another question is still being answered (waiting for it would only add its run time to this one).
182
+ * With `timeoutMs` the generation is aborted after that long and the call throws.
183
+ */
184
+ async ask(question, timeoutMs = 0) {
185
+ if (!this.session || this.busy) {
172
186
  return exports.HANDOFF;
173
187
  }
174
- // Reset per-question so prompts stay independent (best-effort across node-llama-cpp versions).
188
+ this.busy = true;
189
+ const abort = new AbortController();
190
+ const timer = timeoutMs > 0 ? setTimeout(() => abort.abort(), timeoutMs) : null;
175
191
  try {
176
- this.session.setChatHistory?.([]);
192
+ // Reset per-question so prompts stay independent (best-effort across node-llama-cpp versions).
193
+ try {
194
+ this.session.setChatHistory?.([]);
195
+ }
196
+ catch {
197
+ /* ignore */
198
+ }
199
+ const answer = await this.session.prompt(question, {
200
+ maxTokens: this.opts.maxTokens || 256,
201
+ signal: abort.signal,
202
+ });
203
+ if (abort.signal.aborted) {
204
+ throw new Error('aborted');
205
+ }
206
+ return (answer || '').trim();
177
207
  }
178
- catch {
179
- /* ignore */
208
+ catch (e) {
209
+ if (abort.signal.aborted) {
210
+ throw new Error(`timed out after ${Math.round(timeoutMs / 1000)} s`);
211
+ }
212
+ throw e;
213
+ }
214
+ finally {
215
+ if (timer) {
216
+ clearTimeout(timer);
217
+ }
218
+ this.busy = false;
180
219
  }
181
- const answer = await this.session.prompt(question, { maxTokens: this.opts.maxTokens || 512 });
182
- return (answer || '').trim();
183
220
  }
184
221
  async dispose() {
185
222
  try {
@@ -1 +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"]}
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 * as os from 'node:os';\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 /** A prompt is running — the session takes one at a time. */\n private busy = false;\n\n constructor(private readonly opts: LocalLlmOptions) {}\n\n /** A prompt is running, so {@link ask} would hand off at once. */\n get running(): boolean {\n return this.busy;\n }\n\n /** Full system prompt = the strict HANDOFF rule (device work must escalate to the tool-enabled cloud LLM). */\n 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 // Leave one core to js-controller and the other adapters: llama.cpp takes all of them otherwise, and on\n // a Raspberry Pi the whole host — the voice server included — stalls for the length of an answer.\n const cores = typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length;\n this.context = await this.model.createContext({ threads: Math.max(1, cores - 1) });\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 /**\n * Ask the local model. Returns its answer, or {@link HANDOFF} to escalate to the cloud LLM — also when\n * another question is still being answered (waiting for it would only add its run time to this one).\n * With `timeoutMs` the generation is aborted after that long and the call throws.\n */\n async ask(question: string, timeoutMs = 0): Promise<string> {\n if (!this.session || this.busy) {\n return HANDOFF;\n }\n this.busy = true;\n const abort = new AbortController();\n const timer = timeoutMs > 0 ? setTimeout(() => abort.abort(), timeoutMs) : null;\n try {\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, {\n maxTokens: this.opts.maxTokens || 256,\n signal: abort.signal,\n });\n if (abort.signal.aborted) {\n throw new Error('aborted');\n }\n return (answer || '').trim();\n } catch (e) {\n if (abort.signal.aborted) {\n throw new Error(`timed out after ${Math.round(timeoutMs / 1000)} s`);\n }\n throw e;\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n this.busy = false;\n }\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"]}