mirror of
https://github.com/Pavelevich/claw-alexa.git
synced 2026-03-12 06:04:44 +01:00
246 lines
8.3 KiB
JavaScript
246 lines
8.3 KiB
JavaScript
/**
|
|
* Alexa Skill Lambda - OpenClaw/Grok Integration
|
|
* Connects Alexa to your personal AI gateway
|
|
*/
|
|
|
|
const https = require('https');
|
|
const http = require('http');
|
|
|
|
// Gateway configuration - set via environment variables or Account Linking
|
|
const DEFAULT_GATEWAY_URL = process.env.OPENCLAW_GATEWAY_URL || '';
|
|
const DEFAULT_GATEWAY_PASSWORD = process.env.OPENCLAW_GATEWAY_PASSWORD || '';
|
|
|
|
// Fallback to Grok API
|
|
const XAI_API_KEY = process.env.XAI_API_KEY || '';
|
|
|
|
// Call OpenClaw gateway using OpenAI-compatible endpoint
|
|
async function callGateway(message) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(DEFAULT_GATEWAY_URL);
|
|
|
|
// OpenClaw uses OpenAI-compatible chat completions endpoint
|
|
const postData = JSON.stringify({
|
|
model: 'openclaw',
|
|
messages: [
|
|
{
|
|
role: 'system',
|
|
content: 'You are Smart Claw, a helpful AI assistant connected via Alexa. Keep responses concise and suitable for voice output (under 150 words).'
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: message
|
|
}
|
|
],
|
|
stream: false
|
|
});
|
|
|
|
const options = {
|
|
hostname: url.hostname,
|
|
port: 443,
|
|
path: '/v1/chat/completions',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(postData),
|
|
'Authorization': `Bearer ${DEFAULT_GATEWAY_PASSWORD}`
|
|
},
|
|
timeout: 25000
|
|
};
|
|
|
|
console.log('Calling OpenClaw gateway:', DEFAULT_GATEWAY_URL + '/v1/chat/completions');
|
|
|
|
const req = https.request(options, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
console.log('Gateway response status:', res.statusCode);
|
|
console.log('Gateway response data:', data.substring(0, 500));
|
|
|
|
// If gateway returns HTML or error, fall back to Grok
|
|
if (res.statusCode !== 200 || data.includes('<!DOCTYPE') || data.includes('<html')) {
|
|
reject(new Error(`Gateway returned status ${res.statusCode}`));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const json = JSON.parse(data);
|
|
// OpenAI-compatible response format
|
|
if (json.choices && json.choices[0] && json.choices[0].message) {
|
|
resolve(json.choices[0].message.content);
|
|
} else if (json.error) {
|
|
reject(new Error(json.error.message || 'Gateway error'));
|
|
} else {
|
|
reject(new Error('Unexpected gateway response format'));
|
|
}
|
|
} catch (e) {
|
|
reject(new Error('Could not parse gateway response: ' + e.message));
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', (e) => {
|
|
console.error('Gateway request error:', e.message);
|
|
reject(e);
|
|
});
|
|
|
|
req.on('timeout', () => {
|
|
console.error('Gateway request timeout');
|
|
req.destroy();
|
|
reject(new Error('Gateway timeout'));
|
|
});
|
|
|
|
req.write(postData);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// Call Grok API directly (fallback)
|
|
async function callGrok(message) {
|
|
return new Promise((resolve, reject) => {
|
|
const postData = JSON.stringify({
|
|
model: 'grok-3-fast',
|
|
messages: [
|
|
{
|
|
role: 'system',
|
|
content: 'You are a helpful AI assistant called Smart Claw. Keep responses concise and suitable for voice output (under 150 words).'
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: message
|
|
}
|
|
],
|
|
max_tokens: 400
|
|
});
|
|
|
|
const options = {
|
|
hostname: 'api.x.ai',
|
|
port: 443,
|
|
path: '/v1/chat/completions',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(postData),
|
|
'Authorization': `Bearer ${XAI_API_KEY}`
|
|
}
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
const json = JSON.parse(data);
|
|
if (json.choices && json.choices[0] && json.choices[0].message) {
|
|
resolve(json.choices[0].message.content);
|
|
} else {
|
|
resolve('Sorry, I could not process that request.');
|
|
}
|
|
} catch (e) {
|
|
resolve('Sorry, I encountered an error.');
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
req.write(postData);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// Main handler
|
|
exports.handler = async (event) => {
|
|
console.log('Alexa Request:', JSON.stringify(event, null, 2));
|
|
|
|
const requestType = event.request.type;
|
|
|
|
// Launch request
|
|
if (requestType === 'LaunchRequest') {
|
|
return buildResponse(
|
|
'Welcome to Smart Claw, connected to your OpenClaw gateway. What would you like to know?',
|
|
false
|
|
);
|
|
}
|
|
|
|
// Intent request
|
|
if (requestType === 'IntentRequest') {
|
|
const intentName = event.request.intent.name;
|
|
|
|
if (intentName === 'AMAZON.HelpIntent') {
|
|
return buildResponse(
|
|
'I am connected to your OpenClaw AI gateway. You can ask me anything. Try saying: scan my network, or tell me a joke.',
|
|
false
|
|
);
|
|
}
|
|
|
|
if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
|
|
return buildResponse('Goodbye!', true);
|
|
}
|
|
|
|
if (intentName === 'AMAZON.FallbackIntent') {
|
|
return buildResponse('I did not catch that. Please try again.', false);
|
|
}
|
|
|
|
// Handle ChatIntent - main conversation
|
|
if (intentName === 'ChatIntent') {
|
|
const userMessage = event.request.intent.slots?.query?.value || 'Hello';
|
|
console.log('User message:', userMessage);
|
|
|
|
try {
|
|
let response;
|
|
|
|
// Try gateway first
|
|
try {
|
|
console.log('Attempting gateway call...');
|
|
response = await callGateway(userMessage);
|
|
console.log('Gateway success:', response?.substring(0, 100));
|
|
} catch (gatewayError) {
|
|
console.log('Gateway failed, using Grok:', gatewayError.message);
|
|
// Fallback to Grok
|
|
if (XAI_API_KEY) {
|
|
response = await callGrok(userMessage);
|
|
} else {
|
|
response = 'Sorry, I could not connect to the gateway. Please check if OpenClaw is running.';
|
|
}
|
|
}
|
|
|
|
// Clean and truncate response
|
|
let speechResponse = String(response).trim();
|
|
if (speechResponse.length > 6000) {
|
|
speechResponse = speechResponse.substring(0, 5900) + '... That is all for now.';
|
|
}
|
|
|
|
return buildResponse(speechResponse, false);
|
|
} catch (error) {
|
|
console.error('Handler error:', error);
|
|
return buildResponse('Sorry, I encountered an error. Please try again.', false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Session ended
|
|
if (requestType === 'SessionEndedRequest') {
|
|
return { version: '1.0', response: {} };
|
|
}
|
|
|
|
return buildResponse('I did not understand that. Please try again.', false);
|
|
};
|
|
|
|
function buildResponse(speechText, shouldEndSession) {
|
|
return {
|
|
version: '1.0',
|
|
response: {
|
|
outputSpeech: {
|
|
type: 'PlainText',
|
|
text: speechText
|
|
},
|
|
shouldEndSession: shouldEndSession,
|
|
reprompt: shouldEndSession ? undefined : {
|
|
outputSpeech: {
|
|
type: 'PlainText',
|
|
text: 'What else would you like to know?'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|