427 lines
17 KiB
Python
427 lines
17 KiB
Python
"""The AI Agent HA integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
from homeassistant.components.frontend import async_register_built_in_panel
|
|
from homeassistant.components.http import StaticPathConfig
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
from .agent import AiAgentHaAgent
|
|
from .const import DOMAIN
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# Config schema - this integration only supports config entries
|
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
|
|
|
# Define service schema to accept a custom prompt
|
|
SERVICE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("prompt"): cv.string,
|
|
}
|
|
)
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up the AI Agent HA component."""
|
|
return True
|
|
|
|
|
|
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Migrate old config entries to new version."""
|
|
_LOGGER.debug("Migrating config entry from version %s", entry.version)
|
|
|
|
if entry.version == 1:
|
|
# No migration needed for version 1
|
|
return True
|
|
|
|
# Future migrations would go here
|
|
# if entry.version < 2:
|
|
# # Migrate from version 1 to 2
|
|
# new_data = dict(entry.data)
|
|
# # Add migration logic here
|
|
# hass.config_entries.async_update_entry(entry, data=new_data, version=2)
|
|
|
|
_LOGGER.info("Migration to version %s successful", entry.version)
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up AI Agent HA from a config entry."""
|
|
try:
|
|
# Handle version compatibility
|
|
if not hasattr(entry, "version") or entry.version != 1:
|
|
_LOGGER.warning(
|
|
"Config entry has version %s, expected 1. Attempting compatibility mode.",
|
|
getattr(entry, "version", "unknown"),
|
|
)
|
|
|
|
# Convert ConfigEntry to dict and ensure all required keys exist
|
|
config_data = dict(entry.data)
|
|
|
|
# Ensure backward compatibility - check for required keys
|
|
if "ai_provider" not in config_data:
|
|
_LOGGER.error(
|
|
"Config entry missing required 'ai_provider' key. Entry data: %s",
|
|
config_data,
|
|
)
|
|
raise ConfigEntryNotReady("Config entry missing required 'ai_provider' key")
|
|
|
|
if DOMAIN not in hass.data:
|
|
hass.data[DOMAIN] = {"agents": {}, "configs": {}}
|
|
|
|
provider = config_data["ai_provider"]
|
|
|
|
# Validate provider
|
|
if provider not in [
|
|
"llama",
|
|
"openai",
|
|
"gemini",
|
|
"openrouter",
|
|
"anthropic",
|
|
"alter",
|
|
"zai",
|
|
"local_ollama",
|
|
"openai_compatible",
|
|
]:
|
|
_LOGGER.error("Unknown AI provider: %s", provider)
|
|
raise ConfigEntryNotReady(f"Unknown AI provider: {provider}")
|
|
|
|
# Store config for this provider
|
|
hass.data[DOMAIN]["configs"][provider] = config_data
|
|
|
|
# Create agent for this provider
|
|
_LOGGER.debug(
|
|
"Creating AI agent for provider %s with config: %s",
|
|
provider,
|
|
{
|
|
k: v
|
|
for k, v in config_data.items()
|
|
if k
|
|
not in [
|
|
"llama_token",
|
|
"openai_token",
|
|
"gemini_token",
|
|
"openrouter_token",
|
|
"anthropic_token",
|
|
"zai_token",
|
|
]
|
|
},
|
|
)
|
|
hass.data[DOMAIN]["agents"][provider] = AiAgentHaAgent(hass, config_data)
|
|
|
|
_LOGGER.info("Successfully set up AI Agent HA for provider: %s", provider)
|
|
|
|
except KeyError as err:
|
|
_LOGGER.error("Missing required configuration key: %s", err)
|
|
raise ConfigEntryNotReady(f"Missing required configuration key: {err}")
|
|
except Exception as err:
|
|
_LOGGER.exception("Unexpected error setting up AI Agent HA")
|
|
raise ConfigEntryNotReady(f"Error setting up AI Agent HA: {err}")
|
|
|
|
# Modify the query service handler to use the correct provider
|
|
async def async_handle_query(call):
|
|
"""Handle the query service call."""
|
|
try:
|
|
# Check if agents are available
|
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
|
_LOGGER.error(
|
|
"No AI agents available. Please configure the integration first."
|
|
)
|
|
result = {"error": "No AI agents configured"}
|
|
hass.bus.async_fire("ai_agent_ha_response", result)
|
|
return
|
|
|
|
provider = call.data.get("provider")
|
|
if provider not in hass.data[DOMAIN]["agents"]:
|
|
# Get the first available provider
|
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
|
if not available_providers:
|
|
_LOGGER.error("No AI agents available")
|
|
result = {"error": "No AI agents configured"}
|
|
hass.bus.async_fire("ai_agent_ha_response", result)
|
|
return
|
|
provider = available_providers[0]
|
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
|
|
|
agent = hass.data[DOMAIN]["agents"][provider]
|
|
result = await agent.process_query(
|
|
call.data.get("prompt", ""),
|
|
provider=provider,
|
|
debug=call.data.get("debug", False),
|
|
)
|
|
hass.bus.async_fire("ai_agent_ha_response", result)
|
|
except Exception as e:
|
|
_LOGGER.error(f"Error processing query: {e}")
|
|
result = {"error": str(e)}
|
|
hass.bus.async_fire("ai_agent_ha_response", result)
|
|
|
|
async def async_handle_create_automation(call):
|
|
"""Handle the create_automation service call."""
|
|
try:
|
|
# Check if agents are available
|
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
|
_LOGGER.error(
|
|
"No AI agents available. Please configure the integration first."
|
|
)
|
|
return {"error": "No AI agents configured"}
|
|
|
|
provider = call.data.get("provider")
|
|
if provider not in hass.data[DOMAIN]["agents"]:
|
|
# Get the first available provider
|
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
|
if not available_providers:
|
|
_LOGGER.error("No AI agents available")
|
|
return {"error": "No AI agents configured"}
|
|
provider = available_providers[0]
|
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
|
|
|
agent = hass.data[DOMAIN]["agents"][provider]
|
|
result = await agent.create_automation(call.data.get("automation", {}))
|
|
return result
|
|
except Exception as e:
|
|
_LOGGER.error(f"Error creating automation: {e}")
|
|
return {"error": str(e)}
|
|
|
|
async def async_handle_save_prompt_history(call):
|
|
"""Handle the save_prompt_history service call."""
|
|
try:
|
|
# Check if agents are available
|
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
|
_LOGGER.error(
|
|
"No AI agents available. Please configure the integration first."
|
|
)
|
|
return {"error": "No AI agents configured"}
|
|
|
|
provider = call.data.get("provider")
|
|
if provider not in hass.data[DOMAIN]["agents"]:
|
|
# Get the first available provider
|
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
|
if not available_providers:
|
|
_LOGGER.error("No AI agents available")
|
|
return {"error": "No AI agents configured"}
|
|
provider = available_providers[0]
|
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
|
|
|
agent = hass.data[DOMAIN]["agents"][provider]
|
|
user_id = call.context.user_id if call.context.user_id else "default"
|
|
result = await agent.save_user_prompt_history(
|
|
user_id, call.data.get("history", [])
|
|
)
|
|
return result
|
|
except Exception as e:
|
|
_LOGGER.error(f"Error saving prompt history: {e}")
|
|
return {"error": str(e)}
|
|
|
|
async def async_handle_load_prompt_history(call):
|
|
"""Handle the load_prompt_history service call."""
|
|
try:
|
|
# Check if agents are available
|
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
|
_LOGGER.error(
|
|
"No AI agents available. Please configure the integration first."
|
|
)
|
|
return {"error": "No AI agents configured"}
|
|
|
|
provider = call.data.get("provider")
|
|
if provider not in hass.data[DOMAIN]["agents"]:
|
|
# Get the first available provider
|
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
|
if not available_providers:
|
|
_LOGGER.error("No AI agents available")
|
|
return {"error": "No AI agents configured"}
|
|
provider = available_providers[0]
|
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
|
|
|
agent = hass.data[DOMAIN]["agents"][provider]
|
|
user_id = call.context.user_id if call.context.user_id else "default"
|
|
result = await agent.load_user_prompt_history(user_id)
|
|
_LOGGER.debug("Load prompt history result: %s", result)
|
|
return result
|
|
except Exception as e:
|
|
_LOGGER.error(f"Error loading prompt history: {e}")
|
|
return {"error": str(e)}
|
|
|
|
async def async_handle_create_dashboard(call):
|
|
"""Handle the create_dashboard service call."""
|
|
try:
|
|
# Check if agents are available
|
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
|
_LOGGER.error(
|
|
"No AI agents available. Please configure the integration first."
|
|
)
|
|
return {"error": "No AI agents configured"}
|
|
|
|
provider = call.data.get("provider")
|
|
if provider not in hass.data[DOMAIN]["agents"]:
|
|
# Get the first available provider
|
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
|
if not available_providers:
|
|
_LOGGER.error("No AI agents available")
|
|
return {"error": "No AI agents configured"}
|
|
provider = available_providers[0]
|
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
|
|
|
agent = hass.data[DOMAIN]["agents"][provider]
|
|
|
|
# Parse dashboard config if it's a string
|
|
dashboard_config = call.data.get("dashboard_config", {})
|
|
if isinstance(dashboard_config, str):
|
|
try:
|
|
import json
|
|
|
|
dashboard_config = json.loads(dashboard_config)
|
|
except json.JSONDecodeError as e:
|
|
_LOGGER.error(f"Invalid JSON in dashboard_config: {e}")
|
|
return {"error": f"Invalid JSON in dashboard_config: {e}"}
|
|
|
|
result = await agent.create_dashboard(dashboard_config)
|
|
return result
|
|
except Exception as e:
|
|
_LOGGER.error(f"Error creating dashboard: {e}")
|
|
return {"error": str(e)}
|
|
|
|
async def async_handle_update_dashboard(call):
|
|
"""Handle the update_dashboard service call."""
|
|
try:
|
|
# Check if agents are available
|
|
if DOMAIN not in hass.data or not hass.data[DOMAIN].get("agents"):
|
|
_LOGGER.error(
|
|
"No AI agents available. Please configure the integration first."
|
|
)
|
|
return {"error": "No AI agents configured"}
|
|
|
|
provider = call.data.get("provider")
|
|
if provider not in hass.data[DOMAIN]["agents"]:
|
|
# Get the first available provider
|
|
available_providers = list(hass.data[DOMAIN]["agents"].keys())
|
|
if not available_providers:
|
|
_LOGGER.error("No AI agents available")
|
|
return {"error": "No AI agents configured"}
|
|
provider = available_providers[0]
|
|
_LOGGER.debug(f"Using fallback provider: {provider}")
|
|
|
|
agent = hass.data[DOMAIN]["agents"][provider]
|
|
|
|
# Parse dashboard config if it's a string
|
|
dashboard_config = call.data.get("dashboard_config", {})
|
|
if isinstance(dashboard_config, str):
|
|
try:
|
|
import json
|
|
|
|
dashboard_config = json.loads(dashboard_config)
|
|
except json.JSONDecodeError as e:
|
|
_LOGGER.error(f"Invalid JSON in dashboard_config: {e}")
|
|
return {"error": f"Invalid JSON in dashboard_config: {e}"}
|
|
|
|
dashboard_url = call.data.get("dashboard_url", "")
|
|
if not dashboard_url:
|
|
return {"error": "Dashboard URL is required"}
|
|
|
|
result = await agent.update_dashboard(dashboard_url, dashboard_config)
|
|
return result
|
|
except Exception as e:
|
|
_LOGGER.error(f"Error updating dashboard: {e}")
|
|
return {"error": str(e)}
|
|
|
|
# Register services
|
|
hass.services.async_register(DOMAIN, "query", async_handle_query)
|
|
hass.services.async_register(
|
|
DOMAIN, "create_automation", async_handle_create_automation
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, "save_prompt_history", async_handle_save_prompt_history
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, "load_prompt_history", async_handle_load_prompt_history
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, "create_dashboard", async_handle_create_dashboard
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN, "update_dashboard", async_handle_update_dashboard
|
|
)
|
|
|
|
# Register static path for frontend
|
|
await hass.http.async_register_static_paths(
|
|
[
|
|
StaticPathConfig(
|
|
"/frontend/ai_agent_ha",
|
|
hass.config.path("custom_components/ai_agent_ha/frontend"),
|
|
False,
|
|
)
|
|
]
|
|
)
|
|
|
|
# Panel registration with proper error handling
|
|
panel_name = "ai_agent_ha"
|
|
try:
|
|
if await _panel_exists(hass, panel_name):
|
|
_LOGGER.debug("AI Agent HA panel already exists, skipping registration")
|
|
return True
|
|
|
|
_LOGGER.debug("Registering AI Agent HA panel")
|
|
async_register_built_in_panel(
|
|
hass,
|
|
component_name="custom",
|
|
sidebar_title="AI Agent HA",
|
|
sidebar_icon="mdi:robot",
|
|
frontend_url_path=panel_name,
|
|
require_admin=False,
|
|
config={
|
|
"_panel_custom": {
|
|
"name": "ai_agent_ha-panel",
|
|
"module_url": "/frontend/ai_agent_ha/ai_agent_ha-panel.js",
|
|
"embed_iframe": False,
|
|
}
|
|
},
|
|
)
|
|
_LOGGER.debug("AI Agent HA panel registered successfully")
|
|
except Exception as e:
|
|
_LOGGER.warning("Panel registration error: %s", str(e))
|
|
|
|
return True
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
if await _panel_exists(hass, "ai_agent_ha"):
|
|
try:
|
|
from homeassistant.components.frontend import async_remove_panel
|
|
|
|
async_remove_panel(hass, "ai_agent_ha")
|
|
_LOGGER.debug("AI Agent HA panel removed successfully")
|
|
except Exception as e:
|
|
_LOGGER.debug("Error removing panel: %s", str(e))
|
|
|
|
# Remove services
|
|
hass.services.async_remove(DOMAIN, "query")
|
|
hass.services.async_remove(DOMAIN, "create_automation")
|
|
hass.services.async_remove(DOMAIN, "save_prompt_history")
|
|
hass.services.async_remove(DOMAIN, "load_prompt_history")
|
|
hass.services.async_remove(DOMAIN, "create_dashboard")
|
|
hass.services.async_remove(DOMAIN, "update_dashboard")
|
|
# Remove data
|
|
if DOMAIN in hass.data:
|
|
hass.data.pop(DOMAIN)
|
|
|
|
return True
|
|
|
|
|
|
async def _panel_exists(hass: HomeAssistant, panel_name: str) -> bool:
|
|
"""Check if a panel already exists."""
|
|
try:
|
|
return hasattr(hass.data, "frontend_panels") and panel_name in hass.data.get(
|
|
"frontend_panels", {}
|
|
)
|
|
except Exception as e:
|
|
_LOGGER.debug("Error checking panel existence: %s", str(e))
|
|
return False
|