Enhance OpenAI Test Script with Advanced Model Selection and Error Handling
- Added comprehensive model configuration and selection mechanism - Implemented dynamic model handling for OpenAI and DeepSeek providers - Enhanced error handling and retry logic for API interactions - Expanded system analysis with more detailed XML-based response parsing - Updated configuration loading with improved model selection and validation - Added colored logging and more informative console output - Integrated new test suite for API endpoints
This commit is contained in:
163
__tests__/api/index.test.ts
Normal file
163
__tests__/api/index.test.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||||
|
import express from 'express';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import type { Entity } from '../../src/types/hass';
|
||||||
|
import { TokenManager } from '../../src/security/index';
|
||||||
|
import { MCP_SCHEMA } from '../../src/mcp/schema';
|
||||||
|
|
||||||
|
// Load test environment variables
|
||||||
|
config({ path: resolve(process.cwd(), '.env.test') });
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('../../src/security/index', () => ({
|
||||||
|
TokenManager: {
|
||||||
|
validateToken: jest.fn().mockImplementation((token) => token === 'valid-test-token'),
|
||||||
|
},
|
||||||
|
rateLimiter: (req: any, res: any, next: any) => next(),
|
||||||
|
securityHeaders: (req: any, res: any, next: any) => next(),
|
||||||
|
validateRequest: (req: any, res: any, next: any) => next(),
|
||||||
|
sanitizeInput: (req: any, res: any, next: any) => next(),
|
||||||
|
errorHandler: (err: any, req: any, res: any, next: any) => {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create mock entity
|
||||||
|
const mockEntity: Entity = {
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
state: 'off',
|
||||||
|
attributes: {},
|
||||||
|
last_changed: new Date().toISOString(),
|
||||||
|
last_updated: new Date().toISOString(),
|
||||||
|
context: {
|
||||||
|
id: '123',
|
||||||
|
parent_id: null,
|
||||||
|
user_id: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock Home Assistant module
|
||||||
|
jest.mock('../../src/hass/index');
|
||||||
|
|
||||||
|
// Mock LiteMCP
|
||||||
|
jest.mock('litemcp', () => ({
|
||||||
|
LiteMCP: jest.fn().mockImplementation(() => ({
|
||||||
|
name: 'home-assistant',
|
||||||
|
version: '0.1.0',
|
||||||
|
tools: []
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create Express app for testing
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Add test routes that mimic our actual routes
|
||||||
|
app.get('/mcp', (_req, res) => {
|
||||||
|
res.json(MCP_SCHEMA);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/state', (req, res) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== 'valid-test-token') {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
res.json([mockEntity]);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/command', (req, res) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== 'valid-test-token') {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { command, entity_id } = req.body;
|
||||||
|
if (!command || !entity_id) {
|
||||||
|
return res.status(400).json({ error: 'Missing required parameters' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === 'invalid_command') {
|
||||||
|
return res.status(400).json({ error: 'Invalid command' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('API Endpoints', () => {
|
||||||
|
describe('GET /mcp', () => {
|
||||||
|
it('should return MCP schema without authentication', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/mcp')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toBeDefined();
|
||||||
|
expect(response.body).toHaveProperty('name');
|
||||||
|
expect(response.body).toHaveProperty('version');
|
||||||
|
expect(response.body).toHaveProperty('tools');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Protected Endpoints', () => {
|
||||||
|
describe('GET /state', () => {
|
||||||
|
it('should return 401 without authentication', async () => {
|
||||||
|
await request(app)
|
||||||
|
.get('/state')
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return state with valid token', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/state')
|
||||||
|
.set('Authorization', 'Bearer valid-test-token')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toBeDefined();
|
||||||
|
expect(Array.isArray(response.body)).toBeTruthy();
|
||||||
|
expect(response.body[0]).toHaveProperty('entity_id', 'light.living_room');
|
||||||
|
expect(response.body[0]).toHaveProperty('state', 'off');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /command', () => {
|
||||||
|
it('should return 401 without authentication', async () => {
|
||||||
|
await request(app)
|
||||||
|
.post('/command')
|
||||||
|
.send({
|
||||||
|
command: 'turn_on',
|
||||||
|
entity_id: 'light.living_room'
|
||||||
|
})
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process valid command with authentication', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.set('Authorization', 'Bearer valid-test-token')
|
||||||
|
.post('/command')
|
||||||
|
.send({
|
||||||
|
command: 'turn_on',
|
||||||
|
entity_id: 'light.living_room'
|
||||||
|
})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toBeDefined();
|
||||||
|
expect(response.body).toHaveProperty('success', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate command parameters', async () => {
|
||||||
|
await request(app)
|
||||||
|
.post('/command')
|
||||||
|
.set('Authorization', 'Bearer valid-test-token')
|
||||||
|
.send({
|
||||||
|
command: 'invalid_command',
|
||||||
|
entity_id: 'light.living_room'
|
||||||
|
})
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,7 +28,8 @@ module.exports = {
|
|||||||
'**/__tests__/helpers.test.ts',
|
'**/__tests__/helpers.test.ts',
|
||||||
'**/__tests__/schemas/devices.test.ts',
|
'**/__tests__/schemas/devices.test.ts',
|
||||||
'**/__tests__/context/index.test.ts',
|
'**/__tests__/context/index.test.ts',
|
||||||
'**/__tests__/hass/index.test.ts'
|
'**/__tests__/hass/index.test.ts',
|
||||||
|
'**/__tests__/api/index.test.ts'
|
||||||
],
|
],
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
|
|||||||
821
openai_test.ts
821
openai_test.ts
@@ -3,6 +3,9 @@ import OpenAI from "openai";
|
|||||||
import { DOMParser, Element, Document } from '@xmldom/xmldom';
|
import { DOMParser, Element, Document } from '@xmldom/xmldom';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import express from 'express';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -21,10 +24,6 @@ if (!hassToken) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const openai = new OpenAI({
|
|
||||||
apiKey: openaiApiKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
// MCP Server configuration
|
// MCP Server configuration
|
||||||
const MCP_SERVER = process.env.MCP_SERVER || 'http://localhost:3000';
|
const MCP_SERVER = process.env.MCP_SERVER || 'http://localhost:3000';
|
||||||
|
|
||||||
@@ -43,8 +42,8 @@ interface ToolsResponse {
|
|||||||
|
|
||||||
interface SystemAnalysis {
|
interface SystemAnalysis {
|
||||||
overview: {
|
overview: {
|
||||||
state: string;
|
state: string[];
|
||||||
health: string;
|
health: string[];
|
||||||
configurations: string[];
|
configurations: string[];
|
||||||
integrations: string[];
|
integrations: string[];
|
||||||
issues: string[];
|
issues: string[];
|
||||||
@@ -70,6 +69,22 @@ interface SystemAnalysis {
|
|||||||
cleanup_tasks: string[];
|
cleanup_tasks: string[];
|
||||||
regular_tasks: string[];
|
regular_tasks: string[];
|
||||||
};
|
};
|
||||||
|
entity_usage: {
|
||||||
|
most_active: string[];
|
||||||
|
rarely_used: string[];
|
||||||
|
potential_duplicates: string[];
|
||||||
|
};
|
||||||
|
automation_analysis: {
|
||||||
|
inefficient_automations: string[];
|
||||||
|
potential_improvements: string[];
|
||||||
|
suggested_blueprints: string[];
|
||||||
|
condition_optimizations: string[];
|
||||||
|
};
|
||||||
|
energy_management?: {
|
||||||
|
high_consumption: string[];
|
||||||
|
monitoring_suggestions: string[];
|
||||||
|
tariff_optimizations: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface McpSchema {
|
interface McpSchema {
|
||||||
@@ -93,36 +108,154 @@ interface ListDevicesResponse {
|
|||||||
devices?: Record<string, any[]>;
|
devices?: Record<string, any[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add model configuration interface
|
||||||
|
interface ModelConfig {
|
||||||
|
name: string;
|
||||||
|
maxTokens: number;
|
||||||
|
contextWindow: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update model listing to filter based on API key availability
|
||||||
|
const AVAILABLE_MODELS: ModelConfig[] = [
|
||||||
|
// OpenAI models always available
|
||||||
|
{ name: 'gpt-4o', maxTokens: 4096, contextWindow: 128000 },
|
||||||
|
{ name: 'gpt-4-turbo', maxTokens: 4096, contextWindow: 128000 },
|
||||||
|
{ name: 'gpt-4', maxTokens: 8192, contextWindow: 128000 },
|
||||||
|
{ name: 'gpt-3.5-turbo', maxTokens: 4096, contextWindow: 16385 },
|
||||||
|
{ name: 'gpt-3.5-turbo-16k', maxTokens: 16385, contextWindow: 16385 },
|
||||||
|
|
||||||
|
// Conditionally include DeepSeek models
|
||||||
|
...(process.env.DEEPSEEK_API_KEY ? [
|
||||||
|
{ name: 'deepseek-v3', maxTokens: 4096, contextWindow: 128000 },
|
||||||
|
{ name: 'deepseek-r1', maxTokens: 4096, contextWindow: 1000000 }
|
||||||
|
] : [])
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add configuration interface
|
||||||
|
interface AppConfig {
|
||||||
|
mcpServer: string;
|
||||||
|
openaiModel: string;
|
||||||
|
maxRetries: number;
|
||||||
|
analysisTimeout: number;
|
||||||
|
selectedModel: ModelConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add colored logging functions
|
||||||
|
const logger = {
|
||||||
|
info: (msg: string) => console.log(chalk.blue(`ℹ ${msg}`)),
|
||||||
|
success: (msg: string) => console.log(chalk.green(`✓ ${msg}`)),
|
||||||
|
warn: (msg: string) => console.log(chalk.yellow(`⚠ ${msg}`)),
|
||||||
|
error: (msg: string) => console.log(chalk.red(`✗ ${msg}`)),
|
||||||
|
debug: (msg: string) => process.env.DEBUG && console.log(chalk.gray(`› ${msg}`))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update default model selection in loadConfig
|
||||||
|
function loadConfig(): AppConfig {
|
||||||
|
// Use environment variable or default to gpt-4o
|
||||||
|
const defaultModelName = process.env.OPENAI_MODEL || 'gpt-4o';
|
||||||
|
let defaultModel = AVAILABLE_MODELS.find(m => m.name === defaultModelName);
|
||||||
|
|
||||||
|
// If the configured model isn't found, use gpt-4o without warning
|
||||||
|
if (!defaultModel) {
|
||||||
|
defaultModel = AVAILABLE_MODELS.find(m => m.name === 'gpt-4o') || AVAILABLE_MODELS[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mcpServer: process.env.MCP_SERVER || 'http://localhost:3000',
|
||||||
|
openaiModel: defaultModel.name, // Use the resolved model name
|
||||||
|
maxRetries: parseInt(process.env.MAX_RETRIES || '3'),
|
||||||
|
analysisTimeout: parseInt(process.env.ANALYSIS_TIMEOUT || '30000'),
|
||||||
|
selectedModel: defaultModel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenAIClient(): OpenAI {
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
return new OpenAI({
|
||||||
|
apiKey: config.selectedModel.name.startsWith('deepseek')
|
||||||
|
? process.env.DEEPSEEK_API_KEY
|
||||||
|
: openaiApiKey,
|
||||||
|
baseURL: config.selectedModel.name.startsWith('deepseek')
|
||||||
|
? 'https://api.deepseek.com/v1'
|
||||||
|
: 'https://api.openai.com/v1'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a tool on the MCP server
|
* Executes a tool on the MCP server
|
||||||
*/
|
*/
|
||||||
async function executeMcpTool(toolName: string, parameters: Record<string, any> = {}): Promise<any> {
|
async function executeMcpTool(toolName: string, parameters: Record<string, any> = {}): Promise<any> {
|
||||||
try {
|
const config = loadConfig();
|
||||||
const response = await fetch(`${MCP_SERVER}/mcp/execute`, {
|
let attempt = 0;
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${hassToken}`,
|
|
||||||
'Content-Type': "application/json",
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
tool: toolName,
|
|
||||||
parameters
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
while (attempt <= config.maxRetries) {
|
||||||
return await response.json();
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), config.analysisTimeout);
|
||||||
|
|
||||||
|
// Update endpoint URL to use the same base path as schema
|
||||||
|
const endpoint = `${config.mcpServer}/mcp/execute`;
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${hassToken}`,
|
||||||
|
'Content-Type': "application/json",
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ tool: toolName, parameters }),
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (!isMcpExecuteResponse(data)) {
|
||||||
|
throw new Error('Invalid MCP response structure');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
const retryAfter = response.headers.get('Retry-After') || '1';
|
||||||
|
await new Promise(resolve => setTimeout(resolve, parseInt(retryAfter) * 1000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
logger.error(`Endpoint not found: ${endpoint}`);
|
||||||
|
return { success: false, message: 'Endpoint not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 500) {
|
||||||
|
logger.warn(`Server error (${response.status}), retrying...`);
|
||||||
|
attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHttpError(response.status);
|
||||||
|
return { success: false, message: `HTTP error ${response.status}` };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
logger.warn(`Request timed out, retrying (${attempt + 1}/${config.maxRetries})...`);
|
||||||
|
attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.error(`Error executing tool ${toolName}: ${error.message}`);
|
||||||
|
return { success: false, message: error.message };
|
||||||
}
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return { success: false, message: 'Max retries exceeded' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add type guard for MCP responses
|
||||||
|
function isMcpExecuteResponse(obj: any): obj is McpExecuteResponse {
|
||||||
|
return typeof obj === 'object' &&
|
||||||
|
'success' in obj &&
|
||||||
|
(obj.success === true || typeof obj.message === 'string');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,10 +263,12 @@ async function executeMcpTool(toolName: string, parameters: Record<string, any>
|
|||||||
*/
|
*/
|
||||||
async function collectHomeAssistantInfo(): Promise<any> {
|
async function collectHomeAssistantInfo(): Promise<any> {
|
||||||
const info: Record<string, any> = {};
|
const info: Record<string, any> = {};
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
// First, get the MCP schema which contains available tools
|
// Update schema endpoint to be consistent
|
||||||
const schemaResponse = await fetch(`${MCP_SERVER}/mcp`, {
|
const schemaResponse = await fetch(`${config.mcpServer}/mcp`, {
|
||||||
headers: {
|
headers: {
|
||||||
|
'Authorization': `Bearer ${hassToken}`,
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -171,8 +306,8 @@ function formatAnalysis(analysis: SystemAnalysis): string {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
=== System Overview ===
|
=== System Overview ===
|
||||||
Current State: ${analysis.overview.state}
|
Current State: ${analysis.overview.state.join(', ')}
|
||||||
Health: ${analysis.overview.health}
|
Health: ${analysis.overview.health.join(', ')}
|
||||||
|
|
||||||
Notable Configurations:
|
Notable Configurations:
|
||||||
${formatSection(analysis.overview.configurations)}
|
${formatSection(analysis.overview.configurations)}
|
||||||
@@ -228,11 +363,49 @@ ${formatSection(analysis.maintenance.regular_tasks)}
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update compression function with filtering
|
||||||
|
function compressHaInfo(haInfo: any, focus?: string): string {
|
||||||
|
return JSON.stringify(haInfo, (key: string, value: any) => {
|
||||||
|
// Filter based on device type if focus exists
|
||||||
|
if (focus && key === 'devices') {
|
||||||
|
const focusedTypes = getRelevantDeviceTypes(focus);
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).filter(([domain]) =>
|
||||||
|
focusedTypes.includes(domain)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing compression logic
|
||||||
|
if (key === 'attributes') {
|
||||||
|
return Object.keys(value).length > 0 ? value : undefined;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}, 2); // Added space parameter of 2 for better readability
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add device type mapping
|
||||||
|
function getRelevantDeviceTypes(prompt: string): string[] {
|
||||||
|
const TYPE_MAP: Record<string, string[]> = {
|
||||||
|
light: ['light', 'switch', 'group'],
|
||||||
|
temperature: ['climate', 'sensor'],
|
||||||
|
security: ['binary_sensor', 'alarm_control_panel']
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.entries(TYPE_MAP)
|
||||||
|
.filter(([keyword]) => prompt.toLowerCase().includes(keyword))
|
||||||
|
.flatMap(([, types]) => types);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates analysis and recommendations using the OpenAI API based on the Home Assistant data
|
* Generates analysis and recommendations using the OpenAI API based on the Home Assistant data
|
||||||
*/
|
*/
|
||||||
async function generateAnalysis(haInfo: any): Promise<SystemAnalysis> {
|
async function generateAnalysis(haInfo: any): Promise<SystemAnalysis> {
|
||||||
// Prepare a summarized version of the data to reduce token count
|
const openai = getOpenAIClient();
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
const compressedInfo = compressHaInfo(haInfo);
|
||||||
|
|
||||||
const deviceTypes = haInfo.devices ? Object.keys(haInfo.devices) : [];
|
const deviceTypes = haInfo.devices ? Object.keys(haInfo.devices) : [];
|
||||||
const deviceStates = haInfo.devices ? Object.entries(haInfo.devices).reduce((acc: Record<string, number>, [domain, devices]) => {
|
const deviceStates = haInfo.devices ? Object.entries(haInfo.devices).reduce((acc: Record<string, number>, [domain, devices]) => {
|
||||||
acc[domain] = (devices as any[]).length;
|
acc[domain] = (devices as any[]).length;
|
||||||
@@ -256,48 +429,18 @@ async function generateAnalysis(haInfo: any): Promise<SystemAnalysis> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const prompt = `
|
const prompt = `
|
||||||
Analyze this Home Assistant device summary and provide a concise analysis in XML format.
|
Generate your response EXACTLY in this XML format without any additional text:
|
||||||
Focus on key insights and actionable recommendations.
|
|
||||||
|
|
||||||
Device Summary:
|
|
||||||
${JSON.stringify(summarizedInfo, null, 2)}
|
|
||||||
|
|
||||||
Provide your analysis in this XML format:
|
|
||||||
<analysis>
|
<analysis>
|
||||||
<overview>
|
<!-- Content -->
|
||||||
<state>Brief overall state</state>
|
</analysis>
|
||||||
<health>Brief health assessment</health>
|
|
||||||
<configurations>
|
HA System Snapshot: ${compressedInfo}
|
||||||
<item>Key configuration insight</item>
|
${JSON.stringify(summarizedInfo, null, 2)}
|
||||||
</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 {
|
try {
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-4",
|
model: config.selectedModel.name,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -305,45 +448,55 @@ Provide your analysis in this XML format:
|
|||||||
},
|
},
|
||||||
{ role: "user", content: prompt },
|
{ role: "user", content: prompt },
|
||||||
],
|
],
|
||||||
max_tokens: 500,
|
max_tokens: Math.min(config.selectedModel.maxTokens, 1000),
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = completion.choices[0].message?.content || "";
|
const result = completion.choices[0].message?.content || "";
|
||||||
|
|
||||||
// Parse XML response into structured data
|
// Clean the response by removing markdown code blocks
|
||||||
|
const cleanedResult = result.replace(/```xml/g, '').replace(/```/g, '').trim();
|
||||||
|
|
||||||
|
// Parse XML response with error handling
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const xmlDoc = parser.parseFromString(result, "text/xml");
|
let xmlDoc: Document;
|
||||||
|
try {
|
||||||
|
xmlDoc = parser.parseFromString(cleanedResult, "text/xml");
|
||||||
|
|
||||||
const getItems = (path: string): string[] => {
|
// Validate XML structure
|
||||||
const items = Array.from(xmlDoc.getElementsByTagName('item'))
|
if (xmlDoc.getElementsByTagName('analysis').length === 0) {
|
||||||
.filter(item => {
|
throw new Error('Missing root <analysis> element');
|
||||||
let parent = item.parentNode;
|
}
|
||||||
let pathParts = path.split('>').map(p => p.trim());
|
} catch (error) {
|
||||||
for (let i = pathParts.length - 1; i >= 0; i--) {
|
logger.error(`XML parsing failed: ${error.message}`);
|
||||||
if (!parent || parent.nodeName !== pathParts[i]) return false;
|
logger.debug(`Raw AI response:\n${cleanedResult}`);
|
||||||
parent = parent.parentNode;
|
throw new Error('Failed to parse analysis response');
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
// Update the getItems function with fallbacks
|
||||||
return items.map(item => (item as unknown as Element).textContent || "");
|
const getItems = (path: string): string[] => {
|
||||||
};
|
try {
|
||||||
|
return Array.from(xmlDoc.getElementsByTagName('item'))
|
||||||
const getText = (path: string): string => {
|
.filter(item => {
|
||||||
const pathParts = path.split('>').map(p => p.trim());
|
let parent = item.parentNode;
|
||||||
let currentElement: Document | Element = xmlDoc;
|
const pathParts = path.split('>').map(p => p.trim());
|
||||||
for (const part of pathParts) {
|
for (let i = pathParts.length - 1; i >= 0; i--) {
|
||||||
const elements = currentElement.getElementsByTagName(part);
|
if (!parent || parent.nodeName !== pathParts[i]) return false;
|
||||||
if (elements.length === 0) return "";
|
parent = parent.parentNode;
|
||||||
currentElement = elements[0] as Element;
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(item => (item as Element).textContent?.trim() || "");
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to parse ${path}: ${error.message}`);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return currentElement.textContent || "";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const analysis: SystemAnalysis = {
|
const analysis: SystemAnalysis = {
|
||||||
overview: {
|
overview: {
|
||||||
state: getText("analysis > overview > state"),
|
state: getItems("analysis > overview > state"),
|
||||||
health: getText("analysis > overview > health"),
|
health: getItems("analysis > overview > health"),
|
||||||
configurations: getItems("analysis > overview > configurations"),
|
configurations: getItems("analysis > overview > configurations"),
|
||||||
integrations: getItems("analysis > overview > integrations"),
|
integrations: getItems("analysis > overview > integrations"),
|
||||||
issues: getItems("analysis > overview > issues"),
|
issues: getItems("analysis > overview > issues"),
|
||||||
@@ -369,6 +522,22 @@ Provide your analysis in this XML format:
|
|||||||
cleanup_tasks: getItems("analysis > maintenance > cleanup_tasks"),
|
cleanup_tasks: getItems("analysis > maintenance > cleanup_tasks"),
|
||||||
regular_tasks: getItems("analysis > maintenance > regular_tasks"),
|
regular_tasks: getItems("analysis > maintenance > regular_tasks"),
|
||||||
},
|
},
|
||||||
|
entity_usage: {
|
||||||
|
most_active: getItems("analysis > entity_usage > most_active"),
|
||||||
|
rarely_used: getItems("analysis > entity_usage > rarely_used"),
|
||||||
|
potential_duplicates: getItems("analysis > entity_usage > potential_duplicates")
|
||||||
|
},
|
||||||
|
automation_analysis: {
|
||||||
|
inefficient_automations: getItems("analysis > automation_analysis > inefficient_automations"),
|
||||||
|
potential_improvements: getItems("analysis > automation_analysis > potential_improvements"),
|
||||||
|
suggested_blueprints: getItems("analysis > automation_analysis > suggested_blueprints"),
|
||||||
|
condition_optimizations: getItems("analysis > automation_analysis > condition_optimizations")
|
||||||
|
},
|
||||||
|
energy_management: {
|
||||||
|
high_consumption: getItems("analysis > energy_management > high_consumption"),
|
||||||
|
monitoring_suggestions: getItems("analysis > energy_management > monitoring_suggestions"),
|
||||||
|
tariff_optimizations: getItems("analysis > energy_management > tariff_optimizations")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return analysis;
|
return analysis;
|
||||||
@@ -392,187 +561,341 @@ async function getUserInput(question: string): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update chunk size calculation
|
||||||
|
const MAX_CHARACTERS = 8000; // ~2000 tokens (4 chars/token)
|
||||||
|
|
||||||
|
// Update model handling in retry
|
||||||
async function handleCustomPrompt(haInfo: any): Promise<void> {
|
async function handleCustomPrompt(haInfo: any): Promise<void> {
|
||||||
console.log("\nEnter your custom prompt. Available variables:");
|
try {
|
||||||
console.log("- {device_count}: Total number of devices");
|
// Add device metadata
|
||||||
console.log("- {device_types}: List of device types");
|
const deviceTypes = haInfo.devices ? Object.keys(haInfo.devices) : [];
|
||||||
console.log("- {device_states}: Current states of devices");
|
const deviceStates = haInfo.devices ? Object.entries(haInfo.devices).reduce((acc: Record<string, number>, [domain, devices]) => {
|
||||||
console.log("- {device_examples}: Example devices and their states");
|
acc[domain] = (devices as any[]).length;
|
||||||
console.log("\nExample: 'Analyze my {device_count} devices and suggest automations for {device_types}'");
|
return acc;
|
||||||
|
}, {}) : {};
|
||||||
|
const totalDevices = deviceTypes.reduce((sum, type) => sum + deviceStates[type], 0);
|
||||||
|
|
||||||
const customPrompt = await getUserInput("\nEnter your prompt: ");
|
const userPrompt = await getUserInput("Enter your custom prompt: ");
|
||||||
|
if (!userPrompt) {
|
||||||
// Prepare the data for variable replacement
|
console.log("No prompt provided. Exiting...");
|
||||||
const deviceTypes = haInfo.devices ? Object.keys(haInfo.devices) : [];
|
return;
|
||||||
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) => {
|
const openai = getOpenAIClient();
|
||||||
if (devices[type]) {
|
const config = loadConfig();
|
||||||
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({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-4",
|
model: config.selectedModel.name,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: `You are an expert Home Assistant analyst with direct access to the current state of a Home Assistant instance.
|
content: `You are a Home Assistant expert. Analyze the following Home Assistant information and respond to the user's prompt.
|
||||||
When analyzing device states:
|
Current system has ${totalDevices} devices across ${deviceTypes.length} types: ${JSON.stringify(deviceStates)}`
|
||||||
- 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 },
|
{ role: "user", content: userPrompt },
|
||||||
],
|
],
|
||||||
max_tokens: 1000,
|
max_tokens: config.selectedModel.maxTokens,
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("\nAnalysis Results:\n");
|
console.log("\nAnalysis Results:\n");
|
||||||
console.log(completion.choices[0].message?.content || "No response generated");
|
console.log(completion.choices[0].message?.content || "No response generated");
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during OpenAI API call:", error);
|
console.error("Error processing custom prompt:", 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] = {
|
// Retry with simplified prompt if there's an error
|
||||||
total: devices.length,
|
try {
|
||||||
active: activeDevices.length,
|
const retryPrompt = "Please provide a simpler analysis of the Home Assistant system.";
|
||||||
active_devices: activeDevices.map((d: any) => ({
|
const openai = getOpenAIClient();
|
||||||
entity_id: d.entity_id,
|
const config = loadConfig();
|
||||||
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 = `
|
const retryCompletion = await openai.chat.completions.create({
|
||||||
Analyzing Home Assistant devices:
|
model: config.selectedModel.name,
|
||||||
Total Devices: ${totalDevices}
|
messages: [
|
||||||
Device Types: ${deviceTypes.join(', ')}
|
{
|
||||||
|
role: "system",
|
||||||
|
content: "You are a Home Assistant expert. Provide a simple analysis of the system."
|
||||||
|
},
|
||||||
|
{ role: "user", content: retryPrompt },
|
||||||
|
],
|
||||||
|
max_tokens: config.selectedModel.maxTokens,
|
||||||
|
temperature: 0.3,
|
||||||
|
});
|
||||||
|
|
||||||
Relevant Device Summary:
|
console.log("\nAnalysis Results:\n");
|
||||||
${JSON.stringify(summarizedDevices, null, 2)}
|
console.log(retryCompletion.choices[0].message?.content || "No response generated");
|
||||||
|
} catch (retryError) {
|
||||||
User Query: ${customPrompt}
|
console.error("Error during retry:", retryError);
|
||||||
|
|
||||||
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() {
|
// Add new automation optimization function
|
||||||
console.log("Collecting Home Assistant information...");
|
async function analyzeAutomations(automations: any[]): Promise<string> {
|
||||||
const haInfo = await collectHomeAssistantInfo();
|
const openai = getOpenAIClient();
|
||||||
if (!Object.keys(haInfo).length) {
|
const config = loadConfig();
|
||||||
console.error("Failed to collect any Home Assistant information. Exiting.");
|
|
||||||
return;
|
// Compress automation data by only including essential fields
|
||||||
|
const compressedAutomations = automations.map(automation => ({
|
||||||
|
id: automation.entity_id,
|
||||||
|
name: automation.attributes?.friendly_name || automation.entity_id,
|
||||||
|
state: automation.state,
|
||||||
|
last_triggered: automation.attributes?.last_triggered,
|
||||||
|
mode: automation.attributes?.mode,
|
||||||
|
trigger_count: automation.attributes?.trigger?.length || 0,
|
||||||
|
action_count: automation.attributes?.action?.length || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
const prompt = `Analyze these Home Assistant automations and provide optimization suggestions in XML format:
|
||||||
|
${JSON.stringify(compressedAutomations, null, 2)}
|
||||||
|
|
||||||
|
Generate your response in this EXACT format:
|
||||||
|
<analysis>
|
||||||
|
<findings>
|
||||||
|
<item>Finding 1</item>
|
||||||
|
<item>Finding 2</item>
|
||||||
|
<!-- Add more findings as needed -->
|
||||||
|
</findings>
|
||||||
|
<recommendations>
|
||||||
|
<item>Recommendation 1</item>
|
||||||
|
<item>Recommendation 2</item>
|
||||||
|
<!-- Add more recommendations as needed -->
|
||||||
|
</recommendations>
|
||||||
|
<blueprints>
|
||||||
|
<item>Blueprint suggestion 1</item>
|
||||||
|
<item>Blueprint suggestion 2</item>
|
||||||
|
<!-- Add more blueprint suggestions as needed -->
|
||||||
|
</blueprints>
|
||||||
|
</analysis>
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
1. Identifying patterns and potential improvements
|
||||||
|
2. Suggesting energy-saving optimizations
|
||||||
|
3. Recommending error handling improvements
|
||||||
|
4. Suggesting relevant blueprints`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: config.selectedModel.name,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: "You are a Home Assistant automation expert. Analyze the provided automations and respond with specific, actionable suggestions in the required XML format."
|
||||||
|
},
|
||||||
|
{ role: "user", content: prompt }
|
||||||
|
],
|
||||||
|
temperature: 0.2,
|
||||||
|
max_tokens: Math.min(config.selectedModel.maxTokens, 4000)
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = completion.choices[0].message?.content || "";
|
||||||
|
|
||||||
|
// Ensure the response is valid XML
|
||||||
|
if (!response.trim().startsWith('<analysis>')) {
|
||||||
|
return `<?xml version="1.0"?>
|
||||||
|
<analysis>
|
||||||
|
<findings>
|
||||||
|
<item>Error: Could not analyze automations</item>
|
||||||
|
</findings>
|
||||||
|
<recommendations>
|
||||||
|
<item>Please try again with fewer automations</item>
|
||||||
|
</recommendations>
|
||||||
|
<blueprints>
|
||||||
|
<item>No blueprint suggestions available</item>
|
||||||
|
</blueprints>
|
||||||
|
</analysis>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Automation analysis failed: ${error.message}`);
|
||||||
|
return `<?xml version="1.0"?>
|
||||||
|
<analysis>
|
||||||
|
<findings>
|
||||||
|
<item>Error: ${error.message}</item>
|
||||||
|
</findings>
|
||||||
|
<recommendations>
|
||||||
|
<item>Please try again later</item>
|
||||||
|
</recommendations>
|
||||||
|
<blueprints>
|
||||||
|
<item>No blueprint suggestions available</item>
|
||||||
|
</blueprints>
|
||||||
|
</analysis>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update automation handling
|
||||||
|
async function handleAutomationOptimization(haInfo: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result = await executeMcpTool('automation', { action: 'list' });
|
||||||
|
if (!result?.success) {
|
||||||
|
logger.error(`Failed to retrieve automations: ${result?.message || 'Unknown error'}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const automations = result.automations || [];
|
||||||
|
if (automations.length === 0) {
|
||||||
|
logger.warn("No automations found in the system");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Analyzing ${automations.length} automations...`);
|
||||||
|
const optimizationXml = await analyzeAutomations(automations);
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(optimizationXml, "text/xml");
|
||||||
|
|
||||||
|
const formatSection = (title: string, items: string[]) =>
|
||||||
|
items.length > 0 ? `${chalk.bold(title)}:\n${items.map(i => ` • ${i}`).join('\n')}` : '';
|
||||||
|
|
||||||
|
console.log(chalk.bold.underline("\nAutomation Optimization Report"));
|
||||||
|
console.log(formatSection("Key Findings", getItems(xmlDoc, "analysis > item")));
|
||||||
|
console.log(formatSection("\nRecommendations", getItems(xmlDoc, "recommendations > item")));
|
||||||
|
console.log(formatSection("\nSuggested Blueprints", getItems(xmlDoc, "blueprints > item")));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Automation optimization failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update model selection prompt count dynamically
|
||||||
|
async function selectModel(): Promise<ModelConfig> {
|
||||||
|
console.log(chalk.bold.underline("\nAvailable Models:"));
|
||||||
|
AVAILABLE_MODELS.forEach((model, index) => {
|
||||||
|
console.log(
|
||||||
|
`${index + 1}. ${chalk.blue(model.name.padEnd(20))} ` +
|
||||||
|
`Context: ${chalk.yellow(model.contextWindow.toLocaleString().padStart(6))} tokens | ` +
|
||||||
|
`Max output: ${chalk.green(model.maxTokens.toLocaleString().padStart(5))} tokens`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxOption = AVAILABLE_MODELS.length;
|
||||||
|
const choice = await getUserInput(`\nSelect model (1-${maxOption}): `);
|
||||||
|
const selectedIndex = parseInt(choice) - 1;
|
||||||
|
|
||||||
|
if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= AVAILABLE_MODELS.length) {
|
||||||
|
console.log(chalk.yellow("Invalid selection, using default model"));
|
||||||
|
return AVAILABLE_MODELS[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode = await getUserInput(
|
const selectedModel = AVAILABLE_MODELS[selectedIndex];
|
||||||
"\nSelect mode:\n1. Standard Analysis\n2. Custom Prompt\nEnter choice (1 or 2): "
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mode === "2") {
|
// Validate API keys for specific providers
|
||||||
await handleCustomPrompt(haInfo);
|
if (selectedModel.name.startsWith('deepseek')) {
|
||||||
} else {
|
if (!process.env.DEEPSEEK_API_KEY) {
|
||||||
console.log("Generating standard analysis and recommendations...");
|
logger.error("DeepSeek models require DEEPSEEK_API_KEY in .env");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify DeepSeek connection
|
||||||
try {
|
try {
|
||||||
const analysis = await generateAnalysis(haInfo);
|
await getOpenAIClient().models.list();
|
||||||
const formattedAnalysis = formatAnalysis(analysis);
|
|
||||||
console.log("\nHome Assistant Analysis and Recommendations:\n");
|
|
||||||
console.log(formattedAnalysis);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error generating analysis:", error);
|
logger.error(`DeepSeek connection failed: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedModel.name.startsWith('gpt-4-o') && !process.env.OPENAI_API_KEY) {
|
||||||
|
logger.error("OpenAI models require OPENAI_API_KEY in .env");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced main function with progress indicators
|
||||||
|
async function main() {
|
||||||
|
let config = loadConfig();
|
||||||
|
|
||||||
|
// Model selection
|
||||||
|
config.selectedModel = await selectModel();
|
||||||
|
logger.info(`Selected model: ${chalk.blue(config.selectedModel.name)} ` +
|
||||||
|
`(Context: ${config.selectedModel.contextWindow.toLocaleString()} tokens, ` +
|
||||||
|
`Output: ${config.selectedModel.maxTokens.toLocaleString()} tokens)`);
|
||||||
|
|
||||||
|
logger.info(`Starting analysis with ${config.selectedModel.name} model...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info("Collecting Home Assistant information...");
|
||||||
|
const haInfo = await collectHomeAssistantInfo();
|
||||||
|
|
||||||
|
if (!Object.keys(haInfo).length) {
|
||||||
|
logger.error("Failed to collect Home Assistant information");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`Collected data from ${Object.keys(haInfo.devices).length} device types`);
|
||||||
|
|
||||||
|
const mode = await getUserInput(
|
||||||
|
"\nSelect mode:\n1. Standard Analysis\n2. Custom Prompt\n3. Automation Optimization\nEnter choice (1-3): "
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mode === "2") {
|
||||||
|
await handleCustomPrompt(haInfo);
|
||||||
|
} else if (mode === "3") {
|
||||||
|
await handleAutomationOptimization(haInfo);
|
||||||
|
} else {
|
||||||
|
logger.info("Generating standard analysis...");
|
||||||
|
const analysis = await generateAnalysis(haInfo);
|
||||||
|
const formattedAnalysis = formatAnalysis(analysis);
|
||||||
|
console.log("\n" + chalk.bold.underline("Home Assistant Analysis") + "\n");
|
||||||
|
console.log(formattedAnalysis);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Critical failure: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add HTTP error handler
|
||||||
|
function handleHttpError(status: number): void {
|
||||||
|
const errors: Record<number, string> = {
|
||||||
|
400: 'Invalid request parameters',
|
||||||
|
401: 'Authentication failed - check HASS_TOKEN',
|
||||||
|
403: 'Insufficient permissions',
|
||||||
|
404: 'Endpoint not found',
|
||||||
|
429: 'Too many requests'
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.error(errors[status] || `HTTP error ${status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add helper function for XML parsing
|
||||||
|
function getItems(xmlDoc: Document, path: string): string[] {
|
||||||
|
return Array.from(xmlDoc.getElementsByTagName('item'))
|
||||||
|
.filter(item => {
|
||||||
|
let parent = item.parentNode;
|
||||||
|
const pathParts = path.split('>').reverse();
|
||||||
|
for (const part of pathParts) {
|
||||||
|
if (!parent || parent.nodeName !== part.trim()) return false;
|
||||||
|
parent = parent.parentNode;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(item => (item as Element).textContent || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add environment check for processor type
|
||||||
|
if (process.env.PROCESSOR_TYPE === 'openai') {
|
||||||
|
// Initialize Express server only for OpenAI
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
// Keep existing OpenAI routes
|
||||||
|
app.post('/chat', async (req, res) => {
|
||||||
|
// ... existing OpenAI handler code ...
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`[OpenAI Server] Running on port ${port}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[Claude Mode] Using stdio communication');
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
|
|||||||
@@ -22,9 +22,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@digital-alchemy/core": "^24.11.4",
|
"@digital-alchemy/core": "^24.11.4",
|
||||||
"@digital-alchemy/hass": "^24.11.4",
|
"@digital-alchemy/hass": "^24.11.4",
|
||||||
|
"@types/chalk": "^0.4.31",
|
||||||
"@types/xmldom": "^0.1.34",
|
"@types/xmldom": "^0.1.34",
|
||||||
"@xmldom/xmldom": "^0.9.7",
|
"@xmldom/xmldom": "^0.9.7",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^7.1.5",
|
"express-rate-limit": "^7.1.5",
|
||||||
@@ -56,4 +58,4 @@
|
|||||||
"author": "Jango Blockchained",
|
"author": "Jango Blockchained",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user