From 675f6e394264794e7a74ce879c7c16541404be82 Mon Sep 17 00:00:00 2001 From: jango-blockchained Date: Sat, 1 Feb 2025 12:35:13 +0100 Subject: [PATCH] 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 --- README.md | 72 ++++++ openai_test.ts | 580 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 +- 3 files changed, 658 insertions(+), 1 deletion(-) create mode 100644 openai_test.ts diff --git a/README.md b/README.md index 95b181f..32f75d6 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ See [SSE_API.md](docs/SSE_API.md) for complete documentation of the SSE system. - [Add-on Management](#add-on-management) - [Package Management](#package-management) - [Automation Management](#automation-management) +- [OpenAI Integration](#openai-integration) + - [Standard Analysis](#1-standard-analysis) + - [Custom Prompt Analysis](#2-custom-prompt-analysis) - [Natural Language Integration](#natural-language-integration) - [Troubleshooting](#troubleshooting) - [Project Status](#project-status) @@ -667,6 +670,75 @@ async function executeAction() { } ``` +## OpenAI Integration + +The server includes powerful AI analysis capabilities powered by OpenAI's GPT-4 model. This feature provides intelligent analysis of your Home Assistant setup through two main modes: + +### 1. Standard Analysis + +Performs a comprehensive system analysis including: +- System Overview +- Performance Analysis +- Security Assessment +- Optimization Recommendations +- Maintenance Tasks + +```bash +# Run standard analysis +npm run test:openai +# Select option 1 when prompted +``` + +### 2. Custom Prompt Analysis + +Allows you to ask specific questions about your Home Assistant setup. The analysis can include: +- Device States +- Configuration Details +- Active Devices +- Device Attributes (brightness, temperature, etc.) + +```bash +# Run custom analysis +npm run test:openai +# Select option 2 when prompted +``` + +#### Available Variables +When using custom prompts, you can use these variables: +- `{device_count}`: Total number of devices +- `{device_types}`: List of device types +- `{device_states}`: Current states of devices +- `{device_examples}`: Example devices and their states + +#### Example Custom Prompts +``` +"Show me all active lights" +"Which devices in {device_types} need maintenance?" +"Analyze my {device_count} devices and suggest automations" +``` + +### Configuration + +To use the OpenAI integration, you need to set up your OpenAI API key in the `.env` file: +```env +OPENAI_API_KEY=your_openai_api_key_here +``` + +### Features +- 🔍 Intelligent device state analysis +- 📊 System health assessment +- 🤖 Smart automation suggestions +- 🔧 Maintenance recommendations +- 💡 Custom query support +- 🔄 Real-time device state information + +### Token Usage Optimization +The analysis tool includes smart token usage optimization: +- Automatic filtering of relevant devices based on query +- Fallback to summarized data for large systems +- Intelligent attribute selection based on device types +- Automatic retry with condensed information if token limit is reached + ## Development ```bash diff --git a/openai_test.ts b/openai_test.ts new file mode 100644 index 0000000..c5ee89f --- /dev/null +++ b/openai_test.ts @@ -0,0 +1,580 @@ +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; + 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; +} + +interface ListDevicesResponse { + success: boolean; + message?: string; + devices?: Record; +} + +/** + * Executes a tool on the MCP server + */ +async function executeMcpTool(toolName: string, parameters: Record = {}): Promise { + 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 { + const info: Record = {}; + + // 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 { + // 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, [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: + + + Brief overall state + Brief health assessment + + Key configuration insight + + + Key integration insight + + + Critical issue if any + + + + + Key performance tip + + + Key automation suggestion + + + + + Critical update if needed + + + Key maintenance task + + +`; + + 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 { + 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 { + 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, [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); +}); diff --git a/package.json b/package.json index 44aef83..e3e5a05 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.cjs", "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.cjs --coverage", "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.cjs --watch", + "test:openai": "tsx openai_test.ts", "lint": "eslint src --ext .ts", "lint:fix": "eslint src --ext .ts --fix", "prepare": "npm run build", @@ -21,6 +22,8 @@ "dependencies": { "@digital-alchemy/core": "^24.11.4", "@digital-alchemy/hass": "^24.11.4", + "@types/xmldom": "^0.1.34", + "@xmldom/xmldom": "^0.9.7", "ajv": "^8.12.0", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -41,6 +44,8 @@ "@types/uuid": "^9.0.8", "@types/ws": "^8.5.10", "jest": "^29.7.0", + "node-fetch": "^3.3.2", + "openai": "^4.82.0", "rimraf": "^5.0.5", "ts-jest": "^29.1.2", "tsx": "^4.7.0", @@ -48,4 +53,4 @@ }, "author": "Jango Blockchained", "license": "MIT" -} \ No newline at end of file +}