/************************************************************
* Groq (Llama 3.3) – Alexa Fallback (SMART-HOME-FIX VERSION)
************************************************************/
const ns = {
MODEL_ID: 'llama-3.3-70b-versatile',
QUESTION_DP_ID: '0_userdata.0.Manuelle_Datenpunkte.KI.Question',
ANSWER_DP_ID: '0_userdata.0.Manuelle_Datenpunkte.KI.Answer',
APIKEY_DP_ID: '0_userdata.0.Manuelle_Datenpunkte.KI.ApiKey',
ALEXA_HISTORY_TRIGGER: 'alexa2.0.History.#trigger',
ALEXA_SUMMARY_DP_ID: 'alexa2.0.History.summary',
ALEXA_ANSWERTEXT_DP_ID: 'alexa2.0.History.answerText',
ALEXA_CREATIONTIME_DP_ID:'alexa2.0.History.creationTime',
ALEXA_SERIAL_DP_ID: 'alexa2.0.History.serialNumber',
DEFAULT_SERIAL: 'Hier die Serialnummer von der Alexa eingeben',
POLL_INTERVAL_MS: 15000,
MAX_AGE_MS: 120000
};
ns.https = require('https');
let lastProcessedQuestion = '';
let lastProcessedTime = 0;
/* ===================== Auto-Setup ===================== */
async function setupDataPoints() {
const statesToSetup = [
{ id: ns.QUESTION_DP_ID, name: 'KI Frage (Input)', def: '' },
{ id: ns.ANSWER_DP_ID, name: 'KI Antwort (Output)', def: '' },
{ id: ns.APIKEY_DP_ID, name: 'Groq API Key', def: '' }
];
for (const state of statesToSetup) {
const obj = await getObjectAsync(state.id);
if (!obj) {
await createStateAsync(state.id, state.def, false, {
name: state.name, type: 'string', role: 'text', read: true, write: true
});
console.log('✅ Datenpunkt erstellt: ' + state.id);
}
}
}
/* ===================== Sprachausgabe ===================== */
function speak(text, serial) {
let targetSerial = String(serial || '').trim();
if (!targetSerial || !getObject('alexa2.0.Echo-Devices.' + targetSerial)) {
console.log('⚠️ Seriennummer ungültig (' + targetSerial + '). Nutze Standard-Gerät.');
targetSerial = ns.DEFAULT_SERIAL;
}
const speakId = 'alexa2.0.Echo-Devices.' + targetSerial + '.Commands.speak';
console.log('🔊 AUSGABE ÜBER: ' + speakId);
setState(speakId, text, false);
}
/* ===================== Groq API Call ===================== */
function callGroq(inputText, callback) {
const apiKey = getState(ns.APIKEY_DP_ID).val;
if (!apiKey || String(apiKey).trim().length < 10) {
callback(null, 'Fehler: API-Key fehlt');
return;
}
const payload = JSON.stringify({
model: ns.MODEL_ID,
messages: [{ role: 'user', content: inputText }],
temperature: 0.5,
max_tokens: 150
});
const options = {
hostname: 'api.groq.com',
path: '/openai/v1/chat/completions',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
}
};
const req = ns.https.request(options, res => {
let data = '';
res.on('data', chunk => { data += chunk; });
res.on('end', () => {
try {
const json = JSON.parse(data);
if (json.error) { callback(null, 'Groq Fehler: ' + json.error.message); return; }
let answer = 'Keine Antwort';
if (json.choices && json.choices.length > 0) {
answer = json.choices[0].message.content.trim();
}
callback(answer, null);
} catch (e) {
callback(null, 'JSON Parse Error');
}
});
});
req.on('error', () => callback(null, 'HTTP Error'));
req.write(payload);
req.end();
}
/* ===================== Prompts ===================== */
function askKI(question, callback) {
const prompt =
'Du bist die Sprachausgabe eines Smart-Speakers. Der Nutzer kann NICHT antworten oder nachfragen – ' +
'deine Antwort wird einmalig vorgelesen und die Interaktion ist danach beendet. ' +
'Beantworte die folgende Frage deshalb IMMER mit einer konkreten Aussage in maximal 1-2 Sätzen (unter 40 Wörtern). ' +
'Stelle unter keinen Umständen eine Rückfrage und bitte nicht um weitere Details. ' +
'WICHTIGER KONTEXT: Wenn der Nutzer nach "io broker", "i. o. broker" oder "iobroker" fragt, meint er die bekannte Open-Source Smart-Home-Plattform ioBroker, NICHT einen Finanzmakler oder "in Ordnung". ' +
'Frage: ' + question;
callGroq(prompt, callback);
}
/* ===================== Klassifikation ===================== */
function istWeissNichtAntwort(answerText, callback) {
const prompt =
'Analysiere die Antwort eines Sprachassistenten. ' +
'Konnte der Assistent die Frage NICHT beantworten oder hat er unsinnigen Standard-Text ausgegeben (z.B. "weiß ich nicht", "kann ich nicht helfen", ' +
'"wie ich dir dabei helfen kann", "habe ich nicht verstanden", "Es konnte keine Zeit erkannt werden", "probiere es mit")? ' +
'Antworte AUSSCHLIESSLICH mit diesem JSON-Objekt: {"failed": true} oder {"failed": false}. ' +
'Kein Markdown, keine Erklärungen! ' +
'Antwort des Assistenten: "' + answerText + '"';
callGroq(prompt, (result, error) => {
if (error) {
console.log('🔴 Klassifikation API Fehler: ' + error);
callback(false);
return;
}
console.log('🟣 KI RAW ANTWORT: ' + result);
let failed = false;
try {
const match = String(result).match(/\{[\s\S]*\}/);
if (match) {
failed = JSON.parse(match[0]).failed === true;
}
} catch (e) {
console.log('🔴 JSON Parsing Fehler: ' + e);
}
callback(failed);
});
}
/* ===================== Smart-Home-Erkennung ===================== */
function istSmartHomeBefehl(question, answerText) {
const lowerQuestion = question.toLowerCase();
const lowerAnswer = String(answerText || '').toLowerCase();
// Smart-Home-Trigger-Wörter
const smartHomeKeywords = [
'schalte', 'schalt', 'mache', 'mach', 'stelle', 'stell', 'setze', 'setz',
'licht', 'lampe', 'lampen', 'leuchte', 'leuchten',
'ein', 'aus', 'an', 'hell', 'dunkel', 'dimme', 'dimmen',
'temperatur', 'heizung', 'thermostat', 'klima',
'steckdose', 'steckdosen', 'schalter',
'szene', 'szenen', 'modus',
'öffne', 'schließe', 'tor', 'tür', 'garage', 'rollo', 'rollladen', 'vorhang',
'starte', 'stoppe', 'pause', 'musik', 'radio', 'playlist',
'timer', 'wecker', 'erinnere', 'erinnerung'
];
// Prüfe, ob die Frage Smart-Home-Wörter enthält
const hatSmartHomeKeyword = smartHomeKeywords.some(keyword =>
lowerQuestion.includes(keyword)
);
// Prüfe, ob die Antwort eine Bestätigung ist (oder leer = Aktion ausgeführt)
const istBestaetigung =
!answerText || answerText.trim() === '' ||
lowerAnswer.includes('eingeschaltet') ||
lowerAnswer.includes('ausgeschaltet') ||
lowerAnswer.includes('ok') ||
lowerAnswer.includes('fertig') ||
lowerAnswer.includes('erledigt') ||
lowerAnswer.includes('wird') ||
lowerAnswer.includes('habe ich') ||
lowerAnswer.length < 10; // Sehr kurze Antworten sind oft Bestätigungen
const istSmartHome = hatSmartHomeKeyword && istBestaetigung;
if (istSmartHome) {
console.log('🏠 SMART-HOME-BEFEHL ERKANNT – kein KI-Fallback nötig.');
}
return istSmartHome;
}
/* ===================== Direkt-KI-Phrasen ===================== */
const KI_TRIGGER_PHRASES = [
'frag gpt', 'frage gpt', 'frag chatgpt', 'frage chatgpt',
'frag chat', 'frage chat', 'frag die ki', 'frage die ki',
'frag ki', 'frage ki', 'frag jarvis', 'frage jarvis',
'frag google', 'frage google', 'frag gemini', 'frage gemini',
'frag llama', 'frage llama'
];
/* ===================== History-Eintrag verarbeiten ===================== */
function processHistoryEntry(summary, answerText, serial) {
const now = Date.now();
if (summary === lastProcessedQuestion && (now - lastProcessedTime) < 10000) {
console.log('⏳ DEBOUNCE: Frage wurde gerade erst bearbeitet. Ignoriere doppelten Trigger.');
return;
}
const lower = summary.toLowerCase();
const sorted = [...KI_TRIGGER_PHRASES].sort((a, b) => b.length - a.length);
const treffer = sorted.find(p => lower.startsWith(p + ' ') || lower === p);
// 1) Direkte KI-Anfrage (z.B. "Alexa, frag Jarvis ...")
if (treffer) {
const question = summary.slice(treffer.length).trim().replace(/^,\s*/, '');
if (question.length >= 2) {
console.log('🎯 Direkte KI-Anfrage erkannt: ' + question);
lastProcessedQuestion = summary;
lastProcessedTime = now;
setState(ns.ANSWER_DP_ID, 'Bitte warten …', true);
askKI(question, (answer, err) => {
const text = err || answer;
console.log('🏆 KI ANTWORT: ' + text);
setState(ns.ANSWER_DP_ID, text, true);
speak(text, serial);
});
return;
}
}
// 2) WICHTIG: Ist es ein Smart-Home-Befehl? Dann KEIN Fallback!
if (istSmartHomeBefehl(summary, answerText)) {
lastProcessedQuestion = summary;
lastProcessedTime = now;
return; // Abbruch – Alexa hat die Aktion erfolgreich ausgeführt
}
// 3) Fallback: Alexa-Antwort war komplett leer (bei Wissensfragen)
if (!answerText || answerText.trim() === '') {
console.log('⚠️ Keine Alexa-Antwort in der History (leer). Leite sicherheitshalber an KI weiter.');
lastProcessedQuestion = summary;
lastProcessedTime = now;
setState(ns.ANSWER_DP_ID, 'Bitte warten …', true);
askKI(summary, (answer, err) => {
if (err) { setState(ns.ANSWER_DP_ID, err, true); return; }
console.log('🏆 KI ANTWORT (bei leerem Alexa-Text): ' + answer);
setState(ns.ANSWER_DP_ID, answer, true);
speak('Aber ich hab mal nachgefragt: ' + answer, serial);
});
return;
}
// 4) Normaler Fallback: Hat Alexa eine Ausrede gegeben?
istWeissNichtAntwort(answerText, failed => {
console.log('🟣 PRÜFUNG ERGEBNIS (Alexa versagt?): ' + failed);
if (!failed) { console.log('✅ Alexa wusste Rat – kein Fallback nötig.'); return; }
lastProcessedQuestion = summary;
lastProcessedTime = now;
console.log('🚨 ALEXA WUSSTE ES NICHT! Frage an KI: ' + summary);
setState(ns.ANSWER_DP_ID, 'Bitte warten …', true);
askKI(summary, (answer, err) => {
if (err) { setState(ns.ANSWER_DP_ID, err, true); return; }
console.log('🏆 KI ANTWORT: ' + answer);
setState(ns.ANSWER_DP_ID, answer, true);
speak('Aber ich hab mal nachgefragt: ' + answer, serial);
});
});
}
/* ===================== Init ===================== */
async function init() {
console.log('### 🚀 SCRIPT START (SMART-HOME-FIX VERSION) ###');
await setupDataPoints();
on({ id: ns.QUESTION_DP_ID, change: 'any' }, obj => {
const question = String(obj.state.val || '').trim();
if (!question || question.length < 2) return;
console.log('🟢 MANUELLE FRAGE: ' + question);
setState(ns.ANSWER_DP_ID, 'Bitte warten …', true);
askKI(question, (answer, err) => {
setState(ns.ANSWER_DP_ID, err || answer, true);
});
});
setState(ns.ALEXA_HISTORY_TRIGGER, true);
setInterval(() => {
setState(ns.ALEXA_HISTORY_TRIGGER, true);
}, ns.POLL_INTERVAL_MS);
console.log('🔄 History-Polling aktiv (alle ' + (ns.POLL_INTERVAL_MS / 1000) + ' Sekunden)');
on({ id: ns.ALEXA_CREATIONTIME_DP_ID, change: 'ne' }, obj => {
const creationTime = obj.state.val;
console.log('📥 Neuer History-Eintrag: ' + creationTime);
const t = new Date(creationTime).getTime();
if (!isNaN(t) && (Date.now() - t) > ns.MAX_AGE_MS) {
console.log('⚠️ Eintrag ist zu alt – wird ignoriert.');
return;
}
setTimeout(() => {
const summary = String(getState(ns.ALEXA_SUMMARY_DP_ID).val || '').trim();
const answerText = String(getState(ns.ALEXA_ANSWERTEXT_DP_ID).val || '').trim();
const serial = String(getState(ns.ALEXA_SERIAL_DP_ID).val || '').trim();
console.log('🟡 DEINE FRAGE: ' + summary);
console.log('🔵 ALEXAS ANTWORT: ' + answerText);
console.log('📱 FRAGENDES GERÄT: ' + serial);
if (!summary || summary.length < 2) return;
processHistoryEntry(summary, answerText, serial);
}, 700);
});
console.log('### 🟢 ALLES AKTIV – TESTE JETZT! ###');
}
init();