node-red-contrib-knx-ultimate 6.3.21 → 6.3.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/nodes/knxUltimateAI.html +78 -10
- package/nodes/knxUltimateAI.js +209 -94
- package/nodes/locales/de/knxUltimateAI.html +5 -3
- package/nodes/locales/de/knxUltimateAI.json +13 -3
- package/nodes/locales/en/knxUltimateAI.html +5 -3
- package/nodes/locales/en/knxUltimateAI.json +13 -3
- package/nodes/locales/es/knxUltimateAI.html +5 -3
- package/nodes/locales/es/knxUltimateAI.json +13 -3
- package/nodes/locales/fr/knxUltimateAI.html +5 -3
- package/nodes/locales/fr/knxUltimateAI.json +13 -3
- package/nodes/locales/it/knxUltimateAI.html +5 -3
- package/nodes/locales/it/knxUltimateAI.json +13 -3
- package/nodes/locales/zh-CN/knxUltimateAI.html +5 -3
- package/nodes/locales/zh-CN/knxUltimateAI.json +13 -3
- package/package.json +1 -1
package/nodes/knxUltimateAI.js
CHANGED
|
@@ -78,6 +78,8 @@ const KNX_AI_CLOUD_LLM_TIMEOUT_MIN_MS = 120000
|
|
|
78
78
|
const KNX_AI_LOCAL_LLM_TIMEOUT_MIN_MS = 10 * 60 * 1000
|
|
79
79
|
const KNX_AI_MINIMAL_CONTEXT_MAX_TOKENS = 16 * 1024
|
|
80
80
|
const KNX_AI_COMPACT_CONTEXT_MAX_TOKENS = 64 * 1024
|
|
81
|
+
const KNX_AI_LMSTUDIO_PROMPT_CONTEXT_MAX_TOKENS = 16 * 1024
|
|
82
|
+
const KNX_AI_OLLAMA_CONTEXT_MAX_TOKENS = 16 * 1024
|
|
81
83
|
const KNX_AI_ROUTINE_FEEDBACK_TIMEOUT_MS = 4000
|
|
82
84
|
const KNX_AI_ADAPTER_HISTORY_RETENTION_DAYS = Math.max(1, KNX_AI_TRAFFIC_DEFAULTS.historyStoreRetentionDays)
|
|
83
85
|
|
|
@@ -91,13 +93,77 @@ const resolveKnxAiLlmTimeoutMs = ({ provider, configuredTimeoutMs } = {}) => {
|
|
|
91
93
|
|
|
92
94
|
const resolveKnxAiPromptContextMode = ({ provider, contextLength } = {}) => {
|
|
93
95
|
if (provider !== 'ollama' && provider !== 'lmstudio') return 'full'
|
|
94
|
-
const
|
|
96
|
+
const reportedTokens = Math.max(0, Number(contextLength) || 0)
|
|
97
|
+
// Local providers may advertise a 131K model capability even when that is a
|
|
98
|
+
// poor operational choice. Never let the advertised maximum promote KNX AI
|
|
99
|
+
// to the huge "full" prompt. The 16K semantic view retains every action type
|
|
100
|
+
// while selecting only question-relevant KNX/history/project context.
|
|
101
|
+
const localPromptCap = provider === 'lmstudio'
|
|
102
|
+
? KNX_AI_LMSTUDIO_PROMPT_CONTEXT_MAX_TOKENS
|
|
103
|
+
: KNX_AI_OLLAMA_CONTEXT_MAX_TOKENS
|
|
104
|
+
const tokens = Math.min(reportedTokens || localPromptCap, localPromptCap)
|
|
95
105
|
if (!tokens) return 'full'
|
|
96
106
|
if (tokens <= KNX_AI_MINIMAL_CONTEXT_MAX_TOKENS) return 'minimal'
|
|
97
107
|
if (tokens <= KNX_AI_COMPACT_CONTEXT_MAX_TOKENS) return 'compact'
|
|
98
108
|
return 'full'
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
const resolveKnxAiOperationalContextLimit = ({ provider, contextLength } = {}) => {
|
|
112
|
+
const normalizedProvider = String(provider || '').trim().toLowerCase()
|
|
113
|
+
const localPromptCap = normalizedProvider === 'lmstudio'
|
|
114
|
+
? KNX_AI_LMSTUDIO_PROMPT_CONTEXT_MAX_TOKENS
|
|
115
|
+
: normalizedProvider === 'ollama'
|
|
116
|
+
? KNX_AI_OLLAMA_CONTEXT_MAX_TOKENS
|
|
117
|
+
: 0
|
|
118
|
+
if (!localPromptCap) {
|
|
119
|
+
return {
|
|
120
|
+
provider: normalizedProvider,
|
|
121
|
+
tokens: 0,
|
|
122
|
+
mode: 'provider-managed'
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const activeContextLength = Math.max(0, Number(contextLength) || 0)
|
|
126
|
+
return {
|
|
127
|
+
provider: normalizedProvider,
|
|
128
|
+
tokens: Math.min(activeContextLength || localPromptCap, localPromptCap),
|
|
129
|
+
mode: 'fixed'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const measureKnxAiPromptContext = ({ body, provider, model } = {}) => {
|
|
134
|
+
const requestBody = body && typeof body === 'object' ? body : {}
|
|
135
|
+
const textParts = []
|
|
136
|
+
let imageCount = 0
|
|
137
|
+
const appendContent = (content) => {
|
|
138
|
+
if (typeof content === 'string') {
|
|
139
|
+
textParts.push(content)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
if (!Array.isArray(content)) return
|
|
143
|
+
content.forEach(part => {
|
|
144
|
+
if (!part || typeof part !== 'object') return
|
|
145
|
+
if (part.type === 'text' && typeof part.text === 'string') textParts.push(part.text)
|
|
146
|
+
if (part.type === 'image' || part.type === 'image_url') imageCount += 1
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
appendContent(requestBody.system)
|
|
150
|
+
;(Array.isArray(requestBody.messages) ? requestBody.messages : []).forEach(message => {
|
|
151
|
+
if (!message || typeof message !== 'object') return
|
|
152
|
+
appendContent(message.content)
|
|
153
|
+
if (Array.isArray(message.images)) imageCount += message.images.length
|
|
154
|
+
})
|
|
155
|
+
const promptText = textParts.join('\n')
|
|
156
|
+
const bytes = Buffer.byteLength(promptText, 'utf8')
|
|
157
|
+
return {
|
|
158
|
+
provider: String(provider || '').trim().toLowerCase(),
|
|
159
|
+
model: String(model || requestBody.model || '').trim(),
|
|
160
|
+
bytes,
|
|
161
|
+
characters: promptText.length,
|
|
162
|
+
estimatedInputTokens: bytes > 0 ? Math.max(1, Math.ceil(bytes / 4)) : 0,
|
|
163
|
+
imageCount
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
101
167
|
const selectKnxAiCatalogForPrompt = ({ catalog, question, mode = 'full' } = {}) => {
|
|
102
168
|
const source = Array.isArray(catalog) ? catalog : []
|
|
103
169
|
if (mode === 'full') return source.slice(0, 600)
|
|
@@ -356,7 +422,14 @@ const summarizeKnxAiChatContext = ({ node, nodeId, redUserDir } = {}) => {
|
|
|
356
422
|
}
|
|
357
423
|
|
|
358
424
|
return {
|
|
359
|
-
|
|
425
|
+
contextLimit: resolveKnxAiOperationalContextLimit({
|
|
426
|
+
provider: node && node.llmProvider,
|
|
427
|
+
contextLength: node && node.llmContextLength
|
|
428
|
+
}),
|
|
429
|
+
lastPromptUsage: node && node._lastChatPromptUsage
|
|
430
|
+
? Object.assign({}, node._lastChatPromptUsage)
|
|
431
|
+
: null,
|
|
432
|
+
sources: ['knxTraffic', 'adapterHistory', 'etsProject', 'memoryEducation', 'cameras', 'ttsUltimate'],
|
|
360
433
|
files: files.map(item => Object.assign({}, item, { exists: fs.existsSync(item.path) })),
|
|
361
434
|
telegramDirectories: [
|
|
362
435
|
{ id: 'archiveRoot', path: telegramArchiveRoot, exists: fs.existsSync(telegramArchiveRoot) },
|
|
@@ -1831,7 +1904,26 @@ const coerceKnxAiCommandPayload = (value, { dpt } = {}) => {
|
|
|
1831
1904
|
const resolveKnxAiOperationEvent = (candidate) => {
|
|
1832
1905
|
const item = candidate && typeof candidate === 'object' ? candidate : {}
|
|
1833
1906
|
const raw = String(item.event || item.operation || item.action || '').trim().toLowerCase()
|
|
1834
|
-
|
|
1907
|
+
const normalized = raw.replace(/[\s-]+/g, '_')
|
|
1908
|
+
const compact = normalized.replace(/[^a-z]/g, '')
|
|
1909
|
+
const readNames = new Set([
|
|
1910
|
+
'groupvalue_read', 'groupvalue_response', 'read', 'query', 'get',
|
|
1911
|
+
'get_state', 'read_state', 'read_status', 'request_status', 'status'
|
|
1912
|
+
])
|
|
1913
|
+
if (readNames.has(normalized) || ['groupvalueread', 'groupvalueresponse', 'getstate', 'readstate', 'readstatus', 'requeststatus'].includes(compact)) {
|
|
1914
|
+
return 'GroupValue_Read'
|
|
1915
|
+
}
|
|
1916
|
+
const writeNames = new Set(['groupvalue_write', 'write', 'set', 'set_state', 'command'])
|
|
1917
|
+
if (writeNames.has(normalized) || ['groupvaluewrite', 'setstate'].includes(compact)) return 'GroupValue_Write'
|
|
1918
|
+
|
|
1919
|
+
// Small local models sometimes omit the operation discriminator on a state
|
|
1920
|
+
// query even though they correctly return exact ETS destinations. An item
|
|
1921
|
+
// without a payload cannot be an actuator write, so treat it as a safe read.
|
|
1922
|
+
// Legacy write proposals that contain a payload remain writes and still pass
|
|
1923
|
+
// through command-role, DPT, payload and confirmation validation.
|
|
1924
|
+
const hasPayload = Object.prototype.hasOwnProperty.call(item, 'payload') || Object.prototype.hasOwnProperty.call(item, 'value')
|
|
1925
|
+
const payload = Object.prototype.hasOwnProperty.call(item, 'payload') ? item.payload : item.value
|
|
1926
|
+
if (!hasPayload || payload === null || payload === undefined) return 'GroupValue_Read'
|
|
1835
1927
|
return 'GroupValue_Write'
|
|
1836
1928
|
}
|
|
1837
1929
|
|
|
@@ -4113,12 +4205,11 @@ const findLmStudioModel = ({ catalog, model }) => {
|
|
|
4113
4205
|
}) || null
|
|
4114
4206
|
}
|
|
4115
4207
|
|
|
4116
|
-
const
|
|
4208
|
+
const resolveLmStudioModelContext = async ({
|
|
4117
4209
|
baseUrl,
|
|
4118
4210
|
apiKey,
|
|
4119
4211
|
model,
|
|
4120
|
-
get = getJson
|
|
4121
|
-
post = postJson
|
|
4212
|
+
get = getJson
|
|
4122
4213
|
} = {}) => {
|
|
4123
4214
|
const selectedModel = String(model || '').trim()
|
|
4124
4215
|
if (!selectedModel) throw new Error('No Bionic LM Studio model selected')
|
|
@@ -4132,74 +4223,40 @@ const ensureLmStudioModelMaxContext = async ({
|
|
|
4132
4223
|
model: selectedModel
|
|
4133
4224
|
})
|
|
4134
4225
|
if (!descriptor) throw new Error(`Bionic LM Studio model not found: ${selectedModel}`)
|
|
4135
|
-
const
|
|
4136
|
-
if (!
|
|
4226
|
+
const maxContextLength = Math.max(0, Number(descriptor.maxContextLength) || 0)
|
|
4227
|
+
if (!maxContextLength) {
|
|
4137
4228
|
throw new Error(`Bionic LM Studio did not report max_context_length for model "${descriptor.id}"`)
|
|
4138
4229
|
}
|
|
4139
|
-
|
|
4230
|
+
// A loaded instance reflects the context explicitly chosen in Bionic LM
|
|
4231
|
+
// Studio. Preserve it instead of treating max_context_length (a capability)
|
|
4232
|
+
// as the desired runtime configuration and silently reloading the model.
|
|
4233
|
+
const readyInstance = descriptor.loadedInstances.find(instance => {
|
|
4234
|
+
return instance.id === selectedModel && instance.contextLength > 0
|
|
4235
|
+
}) || descriptor.loadedInstances.find(instance => instance.contextLength > 0)
|
|
4140
4236
|
if (readyInstance) {
|
|
4141
4237
|
return {
|
|
4142
4238
|
model: descriptor.id,
|
|
4143
4239
|
displayName: descriptor.displayName,
|
|
4144
4240
|
instanceId: readyInstance.id,
|
|
4145
|
-
contextLength:
|
|
4146
|
-
maxContextLength
|
|
4241
|
+
contextLength: readyInstance.contextLength,
|
|
4242
|
+
maxContextLength,
|
|
4243
|
+
active: true,
|
|
4147
4244
|
changed: false
|
|
4148
4245
|
}
|
|
4149
4246
|
}
|
|
4150
4247
|
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
try {
|
|
4164
|
-
const loaded = await post({
|
|
4165
|
-
url: loadUrl,
|
|
4166
|
-
headers,
|
|
4167
|
-
body: {
|
|
4168
|
-
model: descriptor.id,
|
|
4169
|
-
context_length: targetContextLength,
|
|
4170
|
-
echo_load_config: true
|
|
4171
|
-
},
|
|
4172
|
-
timeoutMs: KNX_AI_LOCAL_LLM_TIMEOUT_MIN_MS
|
|
4173
|
-
})
|
|
4174
|
-
const appliedContextLength = Math.max(0, Number(loaded && loaded.load_config && loaded.load_config.context_length) || targetContextLength)
|
|
4175
|
-
return {
|
|
4176
|
-
model: descriptor.id,
|
|
4177
|
-
displayName: descriptor.displayName,
|
|
4178
|
-
instanceId: String(loaded && loaded.instance_id || descriptor.id),
|
|
4179
|
-
contextLength: appliedContextLength,
|
|
4180
|
-
maxContextLength: targetContextLength,
|
|
4181
|
-
changed: true
|
|
4182
|
-
}
|
|
4183
|
-
} catch (error) {
|
|
4184
|
-
const previousContextLength = descriptor.loadedInstances.reduce((max, instance) => Math.max(max, instance.contextLength), 0)
|
|
4185
|
-
if (previousContextLength > 0) {
|
|
4186
|
-
try {
|
|
4187
|
-
await post({
|
|
4188
|
-
url: loadUrl,
|
|
4189
|
-
headers,
|
|
4190
|
-
body: {
|
|
4191
|
-
model: descriptor.id,
|
|
4192
|
-
context_length: previousContextLength,
|
|
4193
|
-
echo_load_config: true
|
|
4194
|
-
},
|
|
4195
|
-
timeoutMs: KNX_AI_LOCAL_LLM_TIMEOUT_MIN_MS
|
|
4196
|
-
})
|
|
4197
|
-
} catch (restoreError) { /* best-effort restoration of the previous instance */ }
|
|
4198
|
-
}
|
|
4199
|
-
const detail = String(error && error.message ? error.message : error)
|
|
4200
|
-
const loadError = new Error(`Bionic LM Studio could not load "${descriptor.displayName}" with its maximum context (${targetContextLength} tokens). ${detail}`)
|
|
4201
|
-
if (error && error.status !== undefined) loadError.status = error.status
|
|
4202
|
-
throw loadError
|
|
4248
|
+
// Do not load an inactive model through the management API. Bionic LM
|
|
4249
|
+
// Studio's JIT loader must remain free to apply the user's saved per-model
|
|
4250
|
+
// defaults (including context length) when the first chat request arrives.
|
|
4251
|
+
// Until that happens, build a conservative prompt that fits within 16K.
|
|
4252
|
+
return {
|
|
4253
|
+
model: descriptor.id,
|
|
4254
|
+
displayName: descriptor.displayName,
|
|
4255
|
+
instanceId: '',
|
|
4256
|
+
contextLength: Math.min(maxContextLength, KNX_AI_MINIMAL_CONTEXT_MAX_TOKENS),
|
|
4257
|
+
maxContextLength,
|
|
4258
|
+
active: false,
|
|
4259
|
+
changed: false
|
|
4203
4260
|
}
|
|
4204
4261
|
}
|
|
4205
4262
|
|
|
@@ -4292,7 +4349,7 @@ const resolveOllamaModelMaxContext = async ({ baseUrl, model, post = postJson }
|
|
|
4292
4349
|
return {
|
|
4293
4350
|
model: selectedModel,
|
|
4294
4351
|
maxContextLength,
|
|
4295
|
-
contextLength: maxContextLength
|
|
4352
|
+
contextLength: Math.min(maxContextLength, KNX_AI_OLLAMA_CONTEXT_MAX_TOKENS)
|
|
4296
4353
|
}
|
|
4297
4354
|
}
|
|
4298
4355
|
|
|
@@ -5796,7 +5853,7 @@ module.exports = function (RED) {
|
|
|
5796
5853
|
if (!apiKey && deployedNode && deployedNode.credentials && deployedNode.credentials.llmApiKey) {
|
|
5797
5854
|
apiKey = sanitizeApiKey(deployedNode.credentials.llmApiKey)
|
|
5798
5855
|
}
|
|
5799
|
-
const result = await
|
|
5856
|
+
const result = await resolveLmStudioModelContext({
|
|
5800
5857
|
baseUrl,
|
|
5801
5858
|
apiKey,
|
|
5802
5859
|
model: body.model
|
|
@@ -7287,7 +7344,7 @@ module.exports = function (RED) {
|
|
|
7287
7344
|
}, 90)
|
|
7288
7345
|
}
|
|
7289
7346
|
|
|
7290
|
-
const buildLLMPrompt = ({ question, summary, compact = false, languageHint = '' } = {}) => {
|
|
7347
|
+
const buildLLMPrompt = ({ question, summary, compact = false, languageHint = '', includeDocs = true } = {}) => {
|
|
7291
7348
|
const promptMode = compact === 'minimal' ? 'minimal' : compact === true || compact === 'compact' ? 'compact' : 'full'
|
|
7292
7349
|
const compactMode = promptMode !== 'full'
|
|
7293
7350
|
const minimalMode = promptMode === 'minimal'
|
|
@@ -7366,7 +7423,7 @@ module.exports = function (RED) {
|
|
|
7366
7423
|
}
|
|
7367
7424
|
|
|
7368
7425
|
let docsContext = ''
|
|
7369
|
-
if (node.llmIncludeDocsSnippets) {
|
|
7426
|
+
if (includeDocs && node.llmIncludeDocsSnippets) {
|
|
7370
7427
|
const docsMaxCharsConfigured = Math.max(500, Math.min(5000, Number(node.llmDocsMaxChars) || 500))
|
|
7371
7428
|
const docsMaxChars = minimalMode
|
|
7372
7429
|
? Math.min(docsMaxCharsConfigured, 500)
|
|
@@ -9818,14 +9875,19 @@ module.exports = function (RED) {
|
|
|
9818
9875
|
if (node._lmStudioContextPromise && node._lmStudioContextPromise.key === key) {
|
|
9819
9876
|
return node._lmStudioContextPromise.promise
|
|
9820
9877
|
}
|
|
9821
|
-
const promise =
|
|
9878
|
+
const promise = resolveLmStudioModelContext({
|
|
9822
9879
|
baseUrl: node.llmBaseUrl,
|
|
9823
9880
|
apiKey: node.llmApiKey,
|
|
9824
9881
|
model: node.llmModel
|
|
9825
9882
|
}).then(result => {
|
|
9826
9883
|
node.llmContextLength = Math.max(0, Number(result && result.contextLength) || node.llmContextLength)
|
|
9827
|
-
|
|
9828
|
-
|
|
9884
|
+
if (result && result.active === true) {
|
|
9885
|
+
node._lmStudioContextReadyKey = `${node.llmBaseUrl}\u0000${node.llmModel}\u0000${node.llmContextLength}`
|
|
9886
|
+
node._lmStudioContextReadyResult = result
|
|
9887
|
+
} else {
|
|
9888
|
+
node._lmStudioContextReadyKey = ''
|
|
9889
|
+
node._lmStudioContextReadyResult = null
|
|
9890
|
+
}
|
|
9829
9891
|
return result
|
|
9830
9892
|
}).finally(() => {
|
|
9831
9893
|
if (node._lmStudioContextPromise && node._lmStudioContextPromise.key === key) {
|
|
@@ -9842,7 +9904,11 @@ module.exports = function (RED) {
|
|
|
9842
9904
|
const model = node.llmModel || 'llama3.1'
|
|
9843
9905
|
const key = `${url}\u0000${model}`
|
|
9844
9906
|
if (!force && node._ollamaContextReadyKey === key && node.llmContextLength > 0) {
|
|
9845
|
-
return {
|
|
9907
|
+
return {
|
|
9908
|
+
model,
|
|
9909
|
+
maxContextLength: Math.max(node.llmContextLength, Number(node._ollamaModelMaxContextLength) || 0),
|
|
9910
|
+
contextLength: node.llmContextLength
|
|
9911
|
+
}
|
|
9846
9912
|
}
|
|
9847
9913
|
const resolveContext = () => resolveOllamaModelMaxContext({ baseUrl: url, model })
|
|
9848
9914
|
let result
|
|
@@ -9853,7 +9919,8 @@ module.exports = function (RED) {
|
|
|
9853
9919
|
await ensureOllamaServerRunning({ baseUrl: url, autoStart: true, timeoutMs: 22000 })
|
|
9854
9920
|
result = await resolveContext()
|
|
9855
9921
|
}
|
|
9856
|
-
node.
|
|
9922
|
+
node._ollamaModelMaxContextLength = Math.max(0, Number(result && result.maxContextLength) || 0)
|
|
9923
|
+
node.llmContextLength = Math.max(0, Number(result && result.contextLength) || 0)
|
|
9857
9924
|
node._ollamaContextReadyKey = key
|
|
9858
9925
|
return result
|
|
9859
9926
|
}
|
|
@@ -9864,7 +9931,24 @@ module.exports = function (RED) {
|
|
|
9864
9931
|
return null
|
|
9865
9932
|
}
|
|
9866
9933
|
|
|
9867
|
-
const
|
|
9934
|
+
const recordChatPromptUsage = ({ body, provider, model } = {}) => {
|
|
9935
|
+
const sequence = Math.max(0, Number(node._chatPromptUsageSequence) || 0) + 1
|
|
9936
|
+
node._chatPromptUsageSequence = sequence
|
|
9937
|
+
node._lastChatPromptUsageSequence = sequence
|
|
9938
|
+
node._lastChatPromptUsage = Object.assign(
|
|
9939
|
+
{ at: new Date().toISOString(), exactInputTokens: 0 },
|
|
9940
|
+
measureKnxAiPromptContext({ body, provider, model })
|
|
9941
|
+
)
|
|
9942
|
+
return sequence
|
|
9943
|
+
}
|
|
9944
|
+
|
|
9945
|
+
const recordExactChatPromptTokens = ({ sequence, inputTokens } = {}) => {
|
|
9946
|
+
const tokens = Math.max(0, Number(inputTokens) || 0)
|
|
9947
|
+
if (!tokens || sequence !== node._lastChatPromptUsageSequence || !node._lastChatPromptUsage) return
|
|
9948
|
+
node._lastChatPromptUsage = Object.assign({}, node._lastChatPromptUsage, { exactInputTokens: Math.round(tokens) })
|
|
9949
|
+
}
|
|
9950
|
+
|
|
9951
|
+
const callLLMChat = async ({ systemPrompt, userContent, images = [], jsonSchema = null, maxTokensOverride = null, trackChatContextUsage = false }) => {
|
|
9868
9952
|
if (!node.llmEnabled) throw new Error('LLM is disabled in node config')
|
|
9869
9953
|
if (node.llmProvider === 'lmstudio' && !String(node.llmModel || '').trim()) {
|
|
9870
9954
|
throw new Error('No Bionic LM Studio model selected. Start the LM Studio API server, refresh the model list and select a model.')
|
|
@@ -9910,10 +9994,16 @@ module.exports = function (RED) {
|
|
|
9910
9994
|
)
|
|
9911
9995
|
}
|
|
9912
9996
|
let json
|
|
9997
|
+
let promptUsageSequence = 0
|
|
9913
9998
|
const requestOllamaChat = requestBody => postLocalLlmWithContextFallbacks({
|
|
9914
9999
|
body: requestBody,
|
|
9915
10000
|
enabled: true,
|
|
9916
|
-
request: compactBody =>
|
|
10001
|
+
request: compactBody => {
|
|
10002
|
+
if (trackChatContextUsage) {
|
|
10003
|
+
promptUsageSequence = recordChatPromptUsage({ body: compactBody, provider: 'ollama', model: compactBody.model })
|
|
10004
|
+
}
|
|
10005
|
+
return postJson({ url, body: compactBody, timeoutMs: effectiveTimeoutMs })
|
|
10006
|
+
}
|
|
9917
10007
|
})
|
|
9918
10008
|
try {
|
|
9919
10009
|
json = await requestOllamaChat(body)
|
|
@@ -9927,6 +10017,7 @@ module.exports = function (RED) {
|
|
|
9927
10017
|
throw decorateOllamaConnectionError({ error, url, action: 'chat with the model' })
|
|
9928
10018
|
}
|
|
9929
10019
|
}
|
|
10020
|
+
recordExactChatPromptTokens({ sequence: promptUsageSequence, inputTokens: json && json.prompt_eval_count })
|
|
9930
10021
|
const content = json && json.message && typeof json.message.content === 'string' ? json.message.content : safeStringify(json)
|
|
9931
10022
|
return { provider: 'ollama', model: body.model, content, finishReason: String(json && json.done_reason ? json.done_reason : '') }
|
|
9932
10023
|
}
|
|
@@ -9957,7 +10048,11 @@ module.exports = function (RED) {
|
|
|
9957
10048
|
}]
|
|
9958
10049
|
}
|
|
9959
10050
|
if (sys) body.system = sys
|
|
10051
|
+
const promptUsageSequence = trackChatContextUsage
|
|
10052
|
+
? recordChatPromptUsage({ body, provider: 'anthropic', model: body.model })
|
|
10053
|
+
: 0
|
|
9960
10054
|
const json = await postJson({ url, headers, body, timeoutMs: effectiveTimeoutMs })
|
|
10055
|
+
recordExactChatPromptTokens({ sequence: promptUsageSequence, inputTokens: json && json.usage && json.usage.input_tokens })
|
|
9961
10056
|
const content = extractAnthropicText(json)
|
|
9962
10057
|
const finishReason = String(json && json.stop_reason ? json.stop_reason : '')
|
|
9963
10058
|
return { provider: 'anthropic', model: body.model, content, finishReason }
|
|
@@ -10011,17 +10106,27 @@ module.exports = function (RED) {
|
|
|
10011
10106
|
? (localOutputTokenLimit > 0 ? { max_tokens: Math.min(resolvedMaxTokens, localOutputTokenLimit) } : {})
|
|
10012
10107
|
: { max_tokens: resolvedMaxTokens }
|
|
10013
10108
|
let json
|
|
10109
|
+
let promptUsageSequence = 0
|
|
10014
10110
|
try {
|
|
10015
10111
|
json = await postLocalLlmWithContextFallbacks({
|
|
10016
10112
|
body: Object.assign(tokenLimitBody, schemaBody),
|
|
10017
10113
|
enabled: node.llmProvider === 'lmstudio',
|
|
10018
|
-
request: requestBody =>
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10114
|
+
request: requestBody => {
|
|
10115
|
+
if (trackChatContextUsage) {
|
|
10116
|
+
promptUsageSequence = recordChatPromptUsage({
|
|
10117
|
+
body: requestBody,
|
|
10118
|
+
provider: node.llmProvider === 'lmstudio' ? 'lmstudio' : 'openai_compat',
|
|
10119
|
+
model: baseBody.model
|
|
10120
|
+
})
|
|
10121
|
+
}
|
|
10122
|
+
return postOpenAiCompatibleChatWithFallbacks({
|
|
10123
|
+
url,
|
|
10124
|
+
headers,
|
|
10125
|
+
body: requestBody,
|
|
10126
|
+
timeoutMs: effectiveTimeoutMs,
|
|
10127
|
+
model: baseBody.model
|
|
10128
|
+
})
|
|
10129
|
+
}
|
|
10025
10130
|
})
|
|
10026
10131
|
} catch (error) {
|
|
10027
10132
|
if (node.llmProvider === 'lmstudio' && isLikelyConnectionFailure(error)) {
|
|
@@ -10031,6 +10136,7 @@ module.exports = function (RED) {
|
|
|
10031
10136
|
}
|
|
10032
10137
|
throw error
|
|
10033
10138
|
}
|
|
10139
|
+
recordExactChatPromptTokens({ sequence: promptUsageSequence, inputTokens: json && json.usage && json.usage.prompt_tokens })
|
|
10034
10140
|
const content = extractOpenAICompatText(json) || buildOpenAICompatFallbackText(json)
|
|
10035
10141
|
const finishReason = String(json && json.choices && json.choices[0] && json.choices[0].finish_reason ? json.choices[0].finish_reason : '')
|
|
10036
10142
|
return { provider: node.llmProvider === 'lmstudio' ? 'lmstudio' : 'openai_compat', model: baseBody.model, content, finishReason }
|
|
@@ -10203,7 +10309,7 @@ module.exports = function (RED) {
|
|
|
10203
10309
|
}
|
|
10204
10310
|
}
|
|
10205
10311
|
|
|
10206
|
-
const callLLM = async ({ question, sessionId = 'default', languageHint = '' }) => {
|
|
10312
|
+
const callLLM = async ({ question, sessionId = 'default', languageHint = '', includeDocs = true }) => {
|
|
10207
10313
|
await ensureSelectedLocalModelContext({ autoStartOllama: true })
|
|
10208
10314
|
const contextMode = resolveKnxAiPromptContextMode({
|
|
10209
10315
|
provider: node.llmProvider,
|
|
@@ -10220,14 +10326,16 @@ module.exports = function (RED) {
|
|
|
10220
10326
|
question,
|
|
10221
10327
|
summary,
|
|
10222
10328
|
compact: contextMode === 'full' ? false : contextMode,
|
|
10223
|
-
languageHint
|
|
10329
|
+
languageHint,
|
|
10330
|
+
includeDocs
|
|
10224
10331
|
})
|
|
10225
10332
|
const userContent = chatContext ? `${chatContext}\n\n${prompt}` : prompt
|
|
10226
10333
|
const configuredMaxTokens = Math.max(10000, Number(node.llmMaxTokens) || 0)
|
|
10227
10334
|
let ret = await callLLMChat({
|
|
10228
10335
|
systemPrompt: node.llmSystemPrompt || '',
|
|
10229
10336
|
userContent,
|
|
10230
|
-
maxTokensOverride: configuredMaxTokens
|
|
10337
|
+
maxTokensOverride: configuredMaxTokens,
|
|
10338
|
+
trackChatContextUsage: includeDocs === false
|
|
10231
10339
|
})
|
|
10232
10340
|
const finishReason = String(ret && ret.finishReason ? ret.finishReason : '').trim().toLowerCase()
|
|
10233
10341
|
const lengthLimited = finishReason === 'length' || isOpenAICompatLengthFallbackText(ret && ret.content)
|
|
@@ -10238,14 +10346,15 @@ module.exports = function (RED) {
|
|
|
10238
10346
|
sessionId,
|
|
10239
10347
|
maxChars: retryMode === 'minimal' ? 600 : 3000
|
|
10240
10348
|
})
|
|
10241
|
-
const compactBasePrompt = buildLLMPrompt({ question, summary, compact: retryMode, languageHint })
|
|
10349
|
+
const compactBasePrompt = buildLLMPrompt({ question, summary, compact: retryMode, languageHint, includeDocs })
|
|
10242
10350
|
const compactPrompt = compactChatContext ? `${compactChatContext}\n\n${compactBasePrompt}` : compactBasePrompt
|
|
10243
10351
|
const retryMaxTokens = Math.min(16000, Math.max(10000, Math.round(configuredMaxTokens * 1.25)))
|
|
10244
10352
|
try {
|
|
10245
10353
|
ret = await callLLMChat({
|
|
10246
10354
|
systemPrompt: node.llmSystemPrompt || '',
|
|
10247
10355
|
userContent: compactPrompt,
|
|
10248
|
-
maxTokensOverride: retryMaxTokens
|
|
10356
|
+
maxTokensOverride: retryMaxTokens,
|
|
10357
|
+
trackChatContextUsage: includeDocs === false
|
|
10249
10358
|
})
|
|
10250
10359
|
} catch (retryError) {
|
|
10251
10360
|
// Keep the first provider answer if retry fails.
|
|
@@ -10317,14 +10426,14 @@ module.exports = function (RED) {
|
|
|
10317
10426
|
if (contextMode !== 'full') {
|
|
10318
10427
|
gaLines = takeFirstItemsByCharBudget(gaLines, contextMode === 'minimal' ? 5000 : 18000)
|
|
10319
10428
|
}
|
|
10320
|
-
//
|
|
10321
|
-
//
|
|
10322
|
-
// below, but starts from the same complete KNX analysis prompt.
|
|
10429
|
+
// Conversational channels keep the live KNX analysis context used by the web
|
|
10430
|
+
// Assistant, but deliberately omit packaged help/README/wiki/example snippets.
|
|
10323
10431
|
const analysisContext = buildLLMPrompt({
|
|
10324
10432
|
question,
|
|
10325
10433
|
summary,
|
|
10326
10434
|
compact: contextMode === 'full' ? false : contextMode,
|
|
10327
|
-
languageHint
|
|
10435
|
+
languageHint,
|
|
10436
|
+
includeDocs: false
|
|
10328
10437
|
})
|
|
10329
10438
|
const fullCameraCatalog = Array.from(node._cameraCatalog.values())
|
|
10330
10439
|
const cameraSearch = normalizeSearchText(question)
|
|
@@ -10366,7 +10475,8 @@ module.exports = function (RED) {
|
|
|
10366
10475
|
node.llmSystemPrompt || 'You are a KNX building automation assistant.',
|
|
10367
10476
|
'',
|
|
10368
10477
|
'KNX CHAT AND CONTROL CONTRACT:',
|
|
10369
|
-
'- Return only one JSON object with exactly this shape: {"reply":"text for the user","language":"it","routine":{"active":false,"name":"","phase":"none
|
|
10478
|
+
'- Return only one JSON object with exactly this top-level shape: {"reply":"text for the user","language":"it","routine":{"active":false,"name":"","phase":"none"},"commands":[],"cameraActions":[],"speechActions":[]}.',
|
|
10479
|
+
'- Begin with every action array empty. Add an item only when the current user request actually needs that action; never copy placeholder addresses, DPTs, cameras, events, or payloads from these instructions.',
|
|
10370
10480
|
'- Use the same language as the user for reply and reason.',
|
|
10371
10481
|
'- Set language to the ISO code matching the current user request: en, it, de, fr, es, or zh.',
|
|
10372
10482
|
'- For an explicit request to refresh, read, query, or retrieve a current KNX state, create GroupValue_Read operations for the exact relevant objects. Use payload null for reads.',
|
|
@@ -10506,7 +10616,8 @@ module.exports = function (RED) {
|
|
|
10506
10616
|
required: ['reply', 'language', 'routine', 'commands', 'cameraActions', 'speechActions']
|
|
10507
10617
|
}
|
|
10508
10618
|
},
|
|
10509
|
-
maxTokensOverride: configuredMaxTokens
|
|
10619
|
+
maxTokensOverride: configuredMaxTokens,
|
|
10620
|
+
trackChatContextUsage: true
|
|
10510
10621
|
})
|
|
10511
10622
|
|
|
10512
10623
|
let envelope
|
|
@@ -11988,7 +12099,7 @@ module.exports = function (RED) {
|
|
|
11988
12099
|
allowKnxCommands: node.llmAllowKnxCommands,
|
|
11989
12100
|
languageHint: requestLanguage
|
|
11990
12101
|
})
|
|
11991
|
-
: await callLLM({ question, sessionId, languageHint: requestLanguage })
|
|
12102
|
+
: await callLLM({ question, sessionId, languageHint: requestLanguage, includeDocs: false })
|
|
11992
12103
|
const initialRoutine = normalizeKnxAiRoutineDescriptor(ret && ret.routine)
|
|
11993
12104
|
const inspectionCommands = (Array.isArray(ret && ret.commands) ? ret.commands : [])
|
|
11994
12105
|
.filter(command => command && command.event === 'GroupValue_Read')
|
|
@@ -12562,7 +12673,9 @@ module.exports.__test = {
|
|
|
12562
12673
|
KNX_AI_COMPACT_CONTEXT_MAX_TOKENS,
|
|
12563
12674
|
KNX_AI_LOCAL_CONTEXT_RETRY_CHAR_BUDGETS,
|
|
12564
12675
|
KNX_AI_LOCAL_LLM_TIMEOUT_MIN_MS,
|
|
12676
|
+
KNX_AI_LMSTUDIO_PROMPT_CONTEXT_MAX_TOKENS,
|
|
12565
12677
|
KNX_AI_MINIMAL_CONTEXT_MAX_TOKENS,
|
|
12678
|
+
KNX_AI_OLLAMA_CONTEXT_MAX_TOKENS,
|
|
12566
12679
|
KNX_AI_ROUTINE_FEEDBACK_TIMEOUT_MS,
|
|
12567
12680
|
KNX_AI_THINKING_DELAY_MS,
|
|
12568
12681
|
KNX_AI_TRAFFIC_DEFAULTS,
|
|
@@ -12581,7 +12694,7 @@ module.exports.__test = {
|
|
|
12581
12694
|
detectKnxAiLanguageFromText,
|
|
12582
12695
|
deriveLmStudioNativeApiUrl,
|
|
12583
12696
|
dispatchKnxAiTtsUltimateAnnouncement,
|
|
12584
|
-
|
|
12697
|
+
resolveLmStudioModelContext,
|
|
12585
12698
|
executeKnxAiChatAdapter,
|
|
12586
12699
|
extractLlmHttpErrorDetail,
|
|
12587
12700
|
extractOllamaModelMaxContextLength,
|
|
@@ -12601,12 +12714,14 @@ module.exports.__test = {
|
|
|
12601
12714
|
normalizeKnxAiCommandCandidates,
|
|
12602
12715
|
normalizeKnxAiRoutineDescriptor,
|
|
12603
12716
|
normalizeLmStudioModelCatalog,
|
|
12717
|
+
measureKnxAiPromptContext,
|
|
12604
12718
|
parseQuestionTimeRange,
|
|
12605
12719
|
parseKnxAiConversationResponse,
|
|
12606
12720
|
postLocalLlmWithContextFallbacks,
|
|
12607
12721
|
postOpenAiCompatibleChatWithFallbacks,
|
|
12608
12722
|
resolveKnxAiLanguage,
|
|
12609
12723
|
resolveKnxAiLlmTimeoutMs,
|
|
12724
|
+
resolveKnxAiOperationalContextLimit,
|
|
12610
12725
|
resolveKnxAiPromptContextMode,
|
|
12611
12726
|
resolveKnxAiOperationEvent,
|
|
12612
12727
|
resolveKnxAiSessionId,
|
|
@@ -31,7 +31,7 @@ Jede Ask-/Chat-Sitzung speichert ihre letzten 8 Gesprächsschritte und bis zu 20
|
|
|
31
31
|
Bei DPT-1.xxx-Schreibvorgängen werden die sicheren KI-Entsprechungen `true`/`false`, `1`/`0` und `on`/`off` vor der lokalen Validierung und Ausgabe in echte Boolesche Werte normalisiert.
|
|
32
32
|
|
|
33
33
|
### Aktuelle KNX-Lesewerte
|
|
34
|
-
Wenn der Benutzer ausdrücklich einen aktuellen oder aktualisierten Zustand anfordert, kann die KI exakte Objekte aus dem importierten ETS-Katalog abfragen, einschließlich Status- und anderer schreibgeschützter Objekte. Ausgang 4 gibt `msg.destination`, `msg.dpt`, `msg.event = "GroupValue_Read"` und `msg.readstatus = true` aus. Der Node wartet bis zu 6 Sekunden auf jede `GroupValue_Response` oder ein aktuelles Write-Telegramm, gibt anschließend die dekodierten Werte an Ausgang 3 zurück und stellt Details in `msg.knxAi.readResults` bereit. Leseoperationen erfordern keine Bestätigung und werden niemals in Schreiboperationen umgewandelt.
|
|
34
|
+
Wenn der Benutzer ausdrücklich einen aktuellen oder aktualisierten Zustand anfordert, kann die KI exakte Objekte aus dem importierten ETS-Katalog abfragen, einschließlich Status- und anderer schreibgeschützter Objekte. Ausgang 4 gibt `msg.destination`, `msg.dpt`, `msg.event = "GroupValue_Read"` und `msg.readstatus = true` aus. Der Node wartet bis zu 6 Sekunden auf jede `GroupValue_Response` oder ein aktuelles Write-Telegramm, gibt anschließend die dekodierten Werte an Ausgang 3 zurück und stellt Details in `msg.knxAi.readResults` bereit. Leseoperationen erfordern keine Bestätigung und werden niemals in Schreiboperationen umgewandelt. Lässt ein kleines lokales Modell Vorgangstyp und Payload weg, werden exakte ETS-Objekte sicher als Leseoperationen normalisiert; ein Element mit Payload bleibt eine validierte Schreiboperation.
|
|
35
35
|
|
|
36
36
|
### Mehrstufige Gesprächsroutinen
|
|
37
37
|
Anfragen wie „Ich gehe“, „Gute Nacht“ oder „Kinomodus“ können ohne neue Editor-Option eine zustandsabhängige Routine koordinieren. Im ersten LLM-Durchlauf werden ausschließlich exakte ETS-Leseoperationen akzeptiert (maximal 20); KNX AI sendet sie und übergibt die aktuellen GA-/DPT-/Wert-Ergebnisse an einen zweiten isolierten Planungsdurchlauf. Dieser darf bis zu 12 validierte Schreiboperationen vorbereiten, aber keinen weiteren Lesezyklus starten. Bei aktivierter Bestätigung benötigt der gesamte Plan eine einzige lokalisierte Bestätigung; vorher werden weder Schreiboperationen noch angeforderte TTS-Ansagen ausgegeben. Nach der Bestätigung wird jede Schreiboperation erneut validiert, in Reihenfolge weitergegeben und bis zu 4 Sekunden auf eine passende unmittelbare Bus-Rückmeldung beobachtet. Die Abschlussmeldung unterscheidet beobachtete Rückmeldungen von Vorgängen ohne unmittelbare Rückmeldung, ohne daraus einen Gerätefehler abzuleiten. Details stehen in `msg.knxAi.routine`, `readResults`, `verifiedCount` und `unverifiedCount`.
|
|
@@ -59,7 +59,7 @@ Wenn das optionale Paket `node-red-contrib-tts-ultimate` installiert ist, ersche
|
|
|
59
59
|
Nur eine ausdrückliche Anfrage in der aktuellen Chat-Nachricht kann eine Ansage erzeugen. KNX AI sendet den exakten Text direkt als `msg.payload` mit `msg.topic = "knx_ai_announcement"` an den ausgewählten Node; eine Zwischenverkabelung im Flow ist nicht erforderlich. TTS Ultimate verwaltet anschließend den konfigurierten Sonos-Player, Stimme, Lautstärke, Hailing und Warteschlange. Persistenter Kontext, KI-Erziehung, Kamerainhalte und abgeleitete Ereignisse lösen niemals selbstständig Sprache aus.
|
|
60
60
|
|
|
61
61
|
### Übersicht des Chat-Kontexts
|
|
62
|
-
Der Node-Editor zeigt eine kompakte Karte mit den für den Chat verfügbaren Quellen: aktuellem KNX-Verkehr, ETS-Semantik und Node-RED-Projekt, Sitzungs- und Hausgedächtnis, KI-Erziehung
|
|
62
|
+
Der Node-Editor zeigt eine kompakte Karte mit den für den Chat verfügbaren Quellen: aktuellem KNX-Verkehr, ETS-Semantik und Node-RED-Projekt, Sitzungs- und Hausgedächtnis, KI-Erziehung und erkannten Kameras. Sie zeigt außerdem den maximalen operativen Kontext und die tatsächliche UTF-8-Größe des letzten Chat-Prompts; gemeldete Eingabe-Token des Anbieters werden exakt verwendet, andernfalls wird der Tokenwert als Schätzung gekennzeichnet. Außerdem werden `knxai-chat-context.md`, `knxai-home-memory.md` und `knxai-config-<node-id>.json` sowie das absolute Stammverzeichnis des KNX-Telegrammarchivs, das Node-spezifische Verzeichnis und das Tagesdateimuster `YYYY-MM-DD.jsonl` aufgeführt. Die Pfade werden zur Laufzeit aus dem tatsächlich verwendeten Datenverzeichnis des konfigurierten Gateways ermittelt.
|
|
63
63
|
|
|
64
64
|
## Durch KI-Erziehung gesteuerte proaktive Hausintelligenz und begrenztes Gedächtnis
|
|
65
65
|
Aus ETS-Hierarchie, Namen, Rollen und DPTs erstellt der Node ein deterministisches semantisches Modell. Es gibt keinen separaten Schalter und keine erweiterten proaktiven Einstellungen. Eine Benachrichtigung wird nur bewertet, wenn das LLM aktiv ist und die **KI-Erziehung** sie ausdrücklich verlangt. Ausschließlich die Erziehung bestimmt Bedingungen, Offenzeit, Ruhezeiten und Wiederholung. Ohne eine ausdrückliche Regel oder bei fehlgeschlagener LLM-Auswertung wird nichts gesendet.
|
|
@@ -114,7 +114,7 @@ Hier sind alle Felder aufgeführt, wie sie im KNX-AI-Editor sichtbar sind.
|
|
|
114
114
|
- **Vor dem Senden von KNX-Befehlen bestätigen lassen**: Standardmäßig aktiv. Zeigt zuerst die validierten Änderungen und sendet nichts, bis dieselbe Chat-Sitzung bestätigt. Wenn Befehle auf Bestätigung warten, fügt die Antwort immer die genauen Anweisungen zum Bestätigen oder Abbrechen in der Sprache der aktuellen Anfrage hinzu. Vor der Ausgabe werden die Befehle erneut validiert.
|
|
115
115
|
- **Adapter-Vorlage**: Standardmäßig ist **Kein Adapter** gewählt. Die Auswahl lädt das vordefinierte Paar aus Ein- und Ausgangszuordnung; beide bleiben im Editor verborgen.
|
|
116
116
|
- **KI-Erziehung**: Verbindliche, ausschließlich vom Benutzer verwaltete Hinweise, die die KI lesen, aber nie ändern darf. Nur hier werden proaktive Benachrichtigungen mit Bedingungen, Dauer, Ruhezeiten und Wiederholung angefordert.
|
|
117
|
-
-
|
|
117
|
+
- Mitgelieferte Auszüge aus Hilfe, README, Changelog, Wiki und Beispielen werden nicht in Prompts von Telegram, RedBot oder benutzerdefinierten CHAT-Adaptern aufgenommen. Sie bleiben nur dem Web-Assistenten für technische Fragen zum Paket verfügbar.
|
|
118
118
|
- Button **Refresh**: Fragt den Provider ab und lädt verfügbare Modelle. Währenddessen dreht sich das Symbol; ein erfolgreicher Abschluss bleibt absichtlich ohne Meldung.
|
|
119
119
|
|
|
120
120
|
### Ollama Schnellstart (lokal)
|
|
@@ -125,6 +125,7 @@ Hier sind alle Felder aufgeführt, wie sie im KNX-AI-Editor sichtbar sind.
|
|
|
125
125
|
- **2) Install it**: lädt und installiert das Modell lokal (z. B. `llama3.1`).
|
|
126
126
|
- Beim Refresh/Install versucht KNX AI zusätzlich, den Ollama-Server automatisch zu starten.
|
|
127
127
|
- Bei Installationsfehlern mit Verbindungsproblem prüfen, ob Ollama läuft (Desktop-App oder `ollama serve`).
|
|
128
|
+
- Der von `/api/show` gemeldete maximale Kontext dient nur zur Information. KNX AI sendet immer `num_ctx = 16384` (oder das kleinere Modellmaximum) und verwendet dieselbe relevanzbasierte semantische 16K-Ansicht. So wird keine übergroße KV-Cache-Zuweisung erzeugt, ohne Agentenfunktionen zu entfernen.
|
|
128
129
|
- Wenn Node-RED in Docker läuft, im Endpoint `host.docker.internal` statt `localhost` verwenden.
|
|
129
130
|
|
|
130
131
|
### Bionic LM Studio Schnellstart (lokal)
|
|
@@ -132,6 +133,7 @@ Hier sind alle Felder aufgeführt, wie sie im KNX-AI-Editor sichtbar sind.
|
|
|
132
133
|
- Den LM-Studio-API-Server auf der Seite **Developer** oder mit `lms server start` starten.
|
|
133
134
|
- Standard-Endpoint: `http://localhost:1234/v1/chat/completions`.
|
|
134
135
|
- Mit **Refresh** alle von `/v1/models` bereitgestellten Modelle laden; ist kein Modell konfiguriert, wird das erste ausgewählt.
|
|
136
|
+
- Ist ein Modell bereits geladen, behält KNX AI dessen aktive Kontextlänge bei. KNX AI lädt ein inaktives Bionic-Modell niemals über die Verwaltungs-API: Die erste Chat-Anfrage lässt Bionic das Modell per JIT mit den gespeicherten modellspezifischen Standardwerten laden. Unabhängig vom von Bionic gemeldeten Kontext begrenzt KNX AI den eigenen Prompt immer auf eine nach Relevanz ausgewählte semantische 16K-Ansicht; dadurch wird nicht der gesamte 131K-Datensatz gesendet, ohne Denk-, KNX-, Routinen-, Kamera- oder TTS-Funktionen zu entfernen.
|
|
135
137
|
- Der API-Schlüssel ist optional, sofern die Authentifizierung in den LM-Studio-Servereinstellungen nicht aktiviert ist. In Docker `localhost` durch `host.docker.internal` ersetzen.
|
|
136
138
|
|
|
137
139
|
## Sicherheitshinweis
|
|
@@ -62,10 +62,12 @@
|
|
|
62
62
|
},
|
|
63
63
|
"messages": {
|
|
64
64
|
"lmStudioContextAvailable": "Maximaler Modellkontext",
|
|
65
|
-
"lmStudioContextLoading": "
|
|
66
|
-
"
|
|
65
|
+
"lmStudioContextLoading": "Aktiver Modellkontext wird geprüft",
|
|
66
|
+
"lmStudioContextInactive": "Modell inaktiv; bei der ersten Anfrage werden die Bionic-Standardwerte verwendet",
|
|
67
|
+
"lmStudioContextConfigured": "Aktiver Modellkontext",
|
|
67
68
|
"lmStudioContextFailed": "Der Modellkontext konnte nicht konfiguriert werden",
|
|
68
69
|
"lmStudioContextCurrentlyLoaded": "derzeit geladen",
|
|
70
|
+
"localContextBudget": "KNX-AI-Kontextbudget",
|
|
69
71
|
"ollamaNotSupported": "Ollama local mode: API key not required. Default endpoint is http://localhost:11434/api/chat.",
|
|
70
72
|
"ollamaNoModels": "No local Ollama model found. Install one or pick one from the library.",
|
|
71
73
|
"installingOllamaModel": "Starting Ollama and installing model…",
|
|
@@ -86,6 +88,14 @@
|
|
|
86
88
|
"chatContextLoading": "Zusammenfassung des Chat-Kontexts wird geladen…",
|
|
87
89
|
"chatContextUnavailable": "Die Zusammenfassung des Chat-Kontexts ist vorübergehend nicht verfügbar.",
|
|
88
90
|
"chatContextIntro": "Der Chat erhält diese Quellen automatisch. Die folgenden Pfade werden von dieser Node-RED-Installation tatsächlich verwendet.",
|
|
91
|
+
"chatContextLimitLabel": "Maximaler operativer Kontext",
|
|
92
|
+
"chatContextProviderManaged": "vom ausgewählten Anbieter/Modell verwaltet",
|
|
93
|
+
"chatContextTokens": "Token",
|
|
94
|
+
"chatContextLastPromptLabel": "Tatsächliche Größe des letzten Chat-Prompts",
|
|
95
|
+
"chatContextLastPromptUnavailable": "bis zur ersten Chat-Anfrage nicht verfügbar",
|
|
96
|
+
"chatContextExactInputTokens": "vom Anbieter gemessene Eingabe-Token",
|
|
97
|
+
"chatContextEstimatedInputTokens": "geschätzte Eingabe-Token",
|
|
98
|
+
"chatContextImages": "Bilder",
|
|
89
99
|
"chatContextSourcesTitle": "Enthaltene Quellen",
|
|
90
100
|
"chatContextFilesTitle": "Dauerhafte Kontextdateien",
|
|
91
101
|
"chatContextDirectoriesTitle": "KNX-Telegrammarchiv",
|
|
@@ -93,7 +103,7 @@
|
|
|
93
103
|
"chatContextSourceAdapterHistory": "Dauerhaftes Tagesarchiv automatisch erkannter Adapterereignisse einschließlich Kameraerkennungen.",
|
|
94
104
|
"chatContextSourceEtsProject": "ETS-Semantik und vollständiges Inventar des Node-RED-Projekts.",
|
|
95
105
|
"chatContextSourceMemoryEducation": "Sitzungskontext, KI-Erziehung und begrenztes Hausgedächtnis.",
|
|
96
|
-
"
|
|
106
|
+
"chatContextSourceCameras": "Erkannte Kameras und ihre verfügbaren Funktionen.",
|
|
97
107
|
"chatContextSourceTtsUltimate": "Ausgewählter TTS-Ultimate-Node für Ansagen.",
|
|
98
108
|
"chatContextSourceBadge": "Quelle",
|
|
99
109
|
"chatContextFileChatContext": "Dauerhafte Gesprächsverläufe, Anweisungen und Regeln für Kamerabenachrichtigungen.",
|