Files
homeassistant-mcp/openai_test.ts
jango-blockchained 675f6e3942 Add OpenAI-powered Home Assistant Analysis Tool
- Implemented comprehensive AI-driven system analysis using OpenAI's GPT-4
- Created interactive CLI tool for Home Assistant device and system insights
- Added support for standard and custom prompt-based analysis
- Integrated MCP server data collection with intelligent AI processing
- Updated package.json with new OpenAI and XML dependencies
- Enhanced README with detailed OpenAI integration documentation
2025-02-01 12:35:13 +01:00

581 lines
20 KiB
TypeScript

import fetch from "node-fetch";
import OpenAI from "openai";
import { DOMParser, Element, Document } from '@xmldom/xmldom';
import dotenv from 'dotenv';
import readline from 'readline';
// Load environment variables
dotenv.config();
// Retrieve API keys from environment variables
const openaiApiKey = process.env.OPENAI_API_KEY;
const hassToken = process.env.HASS_TOKEN;
if (!openaiApiKey) {
console.error("Please set the OPENAI_API_KEY environment variable.");
process.exit(1);
}
if (!hassToken) {
console.error("Please set the HASS_TOKEN environment variable.");
process.exit(1);
}
const openai = new OpenAI({
apiKey: openaiApiKey,
});
// MCP Server configuration
const MCP_SERVER = process.env.MCP_SERVER || 'http://localhost:3000';
interface McpTool {
name: string;
description: string;
parameters: {
properties: Record<string, any>;
required: string[];
};
}
interface ToolsResponse {
tools: McpTool[];
}
interface SystemAnalysis {
overview: {
state: string;
health: string;
configurations: string[];
integrations: string[];
issues: string[];
};
performance: {
resource_usage: string[];
response_times: string[];
optimization_areas: string[];
};
security: {
current_measures: string[];
vulnerabilities: string[];
recommendations: string[];
};
optimization: {
performance_suggestions: string[];
config_optimizations: string[];
integration_improvements: string[];
automation_opportunities: string[];
};
maintenance: {
required_updates: string[];
cleanup_tasks: string[];
regular_tasks: string[];
};
}
interface McpSchema {
tools: McpTool[];
prompts: any[];
resources: {
name: string;
url: string;
}[];
}
interface McpExecuteResponse {
success: boolean;
message?: string;
devices?: Record<string, any[]>;
}
interface ListDevicesResponse {
success: boolean;
message?: string;
devices?: Record<string, any[]>;
}
/**
* Executes a tool on the MCP server
*/
async function executeMcpTool(toolName: string, parameters: Record<string, any> = {}): Promise<any> {
try {
const response = await fetch(`${MCP_SERVER}/mcp/execute`, {
method: "POST",
headers: {
'Authorization': `Bearer ${hassToken}`,
'Content-Type': "application/json",
'Accept': 'application/json'
},
body: JSON.stringify({
tool: toolName,
parameters
})
});
if (response.ok) {
return await response.json();
}
console.warn(`Failed to execute tool ${toolName}: ${response.status}`);
if (response.status === 401) {
console.error("Authentication failed. Please check your HASS_TOKEN.");
}
return null;
} catch (error) {
console.warn(`Error executing tool ${toolName}:`, error);
return null;
}
}
/**
* Collects comprehensive information about the Home Assistant instance using MCP tools
*/
async function collectHomeAssistantInfo(): Promise<any> {
const info: Record<string, any> = {};
// First, get the MCP schema which contains available tools
const schemaResponse = await fetch(`${MCP_SERVER}/mcp`, {
headers: {
'Accept': 'application/json'
}
});
if (!schemaResponse.ok) {
console.error(`Failed to fetch MCP schema: ${schemaResponse.status}`);
return info;
}
const schema = await schemaResponse.json() as McpSchema;
console.log("Available tools:", schema.tools.map(t => t.name));
// Execute list_devices to get basic device information
console.log("Fetching device information...");
try {
const deviceInfo = await executeMcpTool('list_devices');
if (deviceInfo && deviceInfo.success && deviceInfo.devices) {
info.devices = deviceInfo.devices;
} else {
console.warn(`Failed to list devices: ${deviceInfo?.message || 'Unknown error'}`);
}
} catch (error) {
console.warn("Error fetching devices:", error);
}
return info;
}
/**
* Formats the analysis into a nice looking console output
*/
function formatAnalysis(analysis: SystemAnalysis): string {
const formatSection = (items: string[]): string =>
items.map(item => `${item}`).join('\n');
return `
=== System Overview ===
Current State: ${analysis.overview.state}
Health: ${analysis.overview.health}
Notable Configurations:
${formatSection(analysis.overview.configurations)}
Active Integrations:
${formatSection(analysis.overview.integrations)}
Identified Issues:
${formatSection(analysis.overview.issues)}
=== Performance Analysis ===
Resource Usage:
${formatSection(analysis.performance.resource_usage)}
Response Times:
${formatSection(analysis.performance.response_times)}
Areas Needing Optimization:
${formatSection(analysis.performance.optimization_areas)}
=== Security Assessment ===
Current Security Measures:
${formatSection(analysis.security.current_measures)}
Potential Vulnerabilities:
${formatSection(analysis.security.vulnerabilities)}
Security Recommendations:
${formatSection(analysis.security.recommendations)}
=== Optimization Recommendations ===
Performance Improvements:
${formatSection(analysis.optimization.performance_suggestions)}
Configuration Optimizations:
${formatSection(analysis.optimization.config_optimizations)}
Integration Improvements:
${formatSection(analysis.optimization.integration_improvements)}
Automation Opportunities:
${formatSection(analysis.optimization.automation_opportunities)}
=== Maintenance Tasks ===
Required Updates:
${formatSection(analysis.maintenance.required_updates)}
Cleanup Tasks:
${formatSection(analysis.maintenance.cleanup_tasks)}
Regular Maintenance:
${formatSection(analysis.maintenance.regular_tasks)}
`;
}
/**
* Generates analysis and recommendations using the OpenAI API based on the Home Assistant data
*/
async function generateAnalysis(haInfo: any): Promise<SystemAnalysis> {
// Prepare a summarized version of the data to reduce token count
const deviceTypes = haInfo.devices ? Object.keys(haInfo.devices) : [];
const deviceStates = haInfo.devices ? Object.entries(haInfo.devices).reduce((acc: Record<string, number>, [domain, devices]) => {
acc[domain] = (devices as any[]).length;
return acc;
}, {}) : {};
const summarizedInfo = {
device_summary: {
total_count: deviceTypes.reduce((sum, type) => sum + deviceStates[type], 0),
types: deviceTypes,
by_type: deviceStates,
examples: deviceTypes.slice(0, 3).flatMap(type =>
(haInfo.devices[type] as any[]).slice(0, 1).map(device => ({
type,
entity_id: device.entity_id,
state: device.state,
attributes: device.attributes
}))
)
}
};
const prompt = `
Analyze this Home Assistant device summary and provide a concise analysis in XML format.
Focus on key insights and actionable recommendations.
Device Summary:
${JSON.stringify(summarizedInfo, null, 2)}
Provide your analysis in this XML format:
<analysis>
<overview>
<state>Brief overall state</state>
<health>Brief health assessment</health>
<configurations>
<item>Key configuration insight</item>
</configurations>
<integrations>
<item>Key integration insight</item>
</integrations>
<issues>
<item>Critical issue if any</item>
</issues>
</overview>
<optimization>
<performance_suggestions>
<item>Key performance tip</item>
</performance_suggestions>
<automation_opportunities>
<item>Key automation suggestion</item>
</automation_opportunities>
</optimization>
<maintenance>
<required_updates>
<item>Critical update if needed</item>
</required_updates>
<regular_tasks>
<item>Key maintenance task</item>
</regular_tasks>
</maintenance>
</analysis>`;
try {
const completion = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{
role: "system",
content: "You are an expert Home Assistant analyst. Provide very concise, actionable insights in the specified XML format."
},
{ role: "user", content: prompt },
],
max_tokens: 500,
temperature: 0.7,
});
const result = completion.choices[0].message?.content || "";
// Parse XML response into structured data
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(result, "text/xml");
const getItems = (path: string): string[] => {
const items = Array.from(xmlDoc.getElementsByTagName('item'))
.filter(item => {
let parent = item.parentNode;
let pathParts = path.split('>').map(p => p.trim());
for (let i = pathParts.length - 1; i >= 0; i--) {
if (!parent || parent.nodeName !== pathParts[i]) return false;
parent = parent.parentNode;
}
return true;
});
return items.map(item => (item as unknown as Element).textContent || "");
};
const getText = (path: string): string => {
const pathParts = path.split('>').map(p => p.trim());
let currentElement: Document | Element = xmlDoc;
for (const part of pathParts) {
const elements = currentElement.getElementsByTagName(part);
if (elements.length === 0) return "";
currentElement = elements[0] as Element;
}
return currentElement.textContent || "";
};
const analysis: SystemAnalysis = {
overview: {
state: getText("analysis > overview > state"),
health: getText("analysis > overview > health"),
configurations: getItems("analysis > overview > configurations"),
integrations: getItems("analysis > overview > integrations"),
issues: getItems("analysis > overview > issues"),
},
performance: {
resource_usage: getItems("analysis > performance > resource_usage"),
response_times: getItems("analysis > performance > response_times"),
optimization_areas: getItems("analysis > performance > optimization_areas"),
},
security: {
current_measures: getItems("analysis > security > current_measures"),
vulnerabilities: getItems("analysis > security > vulnerabilities"),
recommendations: getItems("analysis > security > recommendations"),
},
optimization: {
performance_suggestions: getItems("analysis > optimization > performance_suggestions"),
config_optimizations: getItems("analysis > optimization > config_optimizations"),
integration_improvements: getItems("analysis > optimization > integration_improvements"),
automation_opportunities: getItems("analysis > optimization > automation_opportunities"),
},
maintenance: {
required_updates: getItems("analysis > maintenance > required_updates"),
cleanup_tasks: getItems("analysis > maintenance > cleanup_tasks"),
regular_tasks: getItems("analysis > maintenance > regular_tasks"),
},
};
return analysis;
} catch (error) {
console.error("Error during OpenAI API call:", error);
throw new Error("Failed to generate analysis");
}
}
async function getUserInput(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer);
});
});
}
async function handleCustomPrompt(haInfo: any): Promise<void> {
console.log("\nEnter your custom prompt. Available variables:");
console.log("- {device_count}: Total number of devices");
console.log("- {device_types}: List of device types");
console.log("- {device_states}: Current states of devices");
console.log("- {device_examples}: Example devices and their states");
console.log("\nExample: 'Analyze my {device_count} devices and suggest automations for {device_types}'");
const customPrompt = await getUserInput("\nEnter your prompt: ");
// Prepare the data for variable replacement
const deviceTypes = haInfo.devices ? Object.keys(haInfo.devices) : [];
const deviceStates = haInfo.devices ? Object.entries(haInfo.devices).reduce((acc: Record<string, number>, [domain, devices]) => {
acc[domain] = (devices as any[]).length;
return acc;
}, {}) : {};
const totalDevices = deviceTypes.reduce((sum, type) => sum + deviceStates[type], 0);
// Function to filter relevant devices based on the prompt
const getRelevantDevices = (prompt: string, devices: any) => {
const relevantTypes = deviceTypes.filter(type =>
prompt.toLowerCase().includes(type.toLowerCase()) ||
type === 'light' && prompt.toLowerCase().includes('lights') ||
type === 'switch' && prompt.toLowerCase().includes('switches')
);
if (relevantTypes.length === 0) {
// If no specific types mentioned, return a summary of all types
return Object.entries(devices).reduce((acc: any, [domain, deviceList]) => {
acc[domain] = {
count: (deviceList as any[]).length,
example: (deviceList as any[])[0]
};
return acc;
}, {});
}
return relevantTypes.reduce((acc: any, type) => {
if (devices[type]) {
acc[type] = devices[type];
}
return acc;
}, {});
};
const relevantDevices = getRelevantDevices(customPrompt, haInfo.devices);
// Replace variables in the prompt
let formattedPrompt = `
Here is the current state of your Home Assistant devices:
Total Devices: ${totalDevices}
Device Types: ${deviceTypes.join(', ')}
Relevant Device Information:
${JSON.stringify(relevantDevices, null, 2)}
User Query: ${customPrompt}
Please analyze this information and provide a detailed response focusing specifically on what was asked.
If the query is about specific device types, please filter and show only relevant information.
Include specific entity IDs and states in your response when applicable.
`;
try {
const completion = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{
role: "system",
content: `You are an expert Home Assistant analyst with direct access to the current state of a Home Assistant instance.
When analyzing device states:
- Always mention specific entity IDs when discussing devices
- Include current state values and relevant attributes
- If discussing lights, mention brightness levels if available
- For climate devices, include temperature and mode information
- For switches and other binary devices, clearly state if they are on/off
- Group related devices together in your analysis
- Provide specific, actionable insights based on the current states`
},
{ role: "user", content: formattedPrompt },
],
max_tokens: 1000,
temperature: 0.3,
});
console.log("\nAnalysis Results:\n");
console.log(completion.choices[0].message?.content || "No response generated");
} catch (error) {
console.error("Error during OpenAI API call:", error);
if (error instanceof Error && error.message.includes('maximum context length')) {
console.log("\nTrying with more concise data...");
// Retry with even more summarized data
const summarizedDevices = Object.entries(relevantDevices).reduce((acc: any, [type, devices]) => {
if (Array.isArray(devices)) {
const activeDevices = devices.filter((d: any) =>
d.state === 'on' ||
d.state === 'home' ||
(typeof d.state === 'number' && d.state > 0)
);
acc[type] = {
total: devices.length,
active: activeDevices.length,
active_devices: activeDevices.map((d: any) => ({
entity_id: d.entity_id,
state: d.state,
name: d.attributes?.friendly_name || d.entity_id,
...(d.attributes?.brightness && { brightness: Math.round((d.attributes.brightness / 255) * 100) + '%' }),
...(d.attributes?.temperature && { temperature: d.attributes.temperature }),
...(d.attributes?.hvac_mode && { mode: d.attributes.hvac_mode })
}))
};
}
return acc;
}, {});
const retryPrompt = `
Analyzing Home Assistant devices:
Total Devices: ${totalDevices}
Device Types: ${deviceTypes.join(', ')}
Relevant Device Summary:
${JSON.stringify(summarizedDevices, null, 2)}
User Query: ${customPrompt}
Please provide a detailed analysis focusing on active devices.
Include specific device names, states, and any relevant attributes (brightness, temperature, etc.).
Group similar devices together in your response.
`;
try {
const retryCompletion = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{
role: "system",
content: "You are an expert Home Assistant analyst. Provide concise, focused answers about device states and configurations."
},
{ role: "user", content: retryPrompt },
],
max_tokens: 1000,
temperature: 0.3,
});
console.log("\nAnalysis Results:\n");
console.log(retryCompletion.choices[0].message?.content || "No response generated");
} catch (retryError) {
console.error("Error during retry:", retryError);
}
}
}
}
async function main() {
console.log("Collecting Home Assistant information...");
const haInfo = await collectHomeAssistantInfo();
if (!Object.keys(haInfo).length) {
console.error("Failed to collect any Home Assistant information. Exiting.");
return;
}
const mode = await getUserInput(
"\nSelect mode:\n1. Standard Analysis\n2. Custom Prompt\nEnter choice (1 or 2): "
);
if (mode === "2") {
await handleCustomPrompt(haInfo);
} else {
console.log("Generating standard analysis and recommendations...");
try {
const analysis = await generateAnalysis(haInfo);
const formattedAnalysis = formatAnalysis(analysis);
console.log("\nHome Assistant Analysis and Recommendations:\n");
console.log(formattedAnalysis);
} catch (error) {
console.error("Error generating analysis:", error);
}
}
}
main().catch((error) => {
console.error("Unexpected error:", error);
});