From b6f6b1e09e29f26a1c8db3dcbf8766ef4877e8b0 Mon Sep 17 00:00:00 2001 From: SMKRV Date: Mon, 18 Nov 2024 11:35:23 +0300 Subject: [PATCH] Reset to current local state --- custom_components/ha_text_ai/__init__.py | 195 +++++++++++++++-------- ha-text-ai.zip | Bin 0 -> 7717 bytes ha_text_ai.zip | Bin 7312 -> 0 bytes 3 files changed, 126 insertions(+), 69 deletions(-) create mode 100644 ha-text-ai.zip delete mode 100644 ha_text_ai.zip diff --git a/custom_components/ha_text_ai/__init__.py b/custom_components/ha_text_ai/__init__.py index 2e1bf5c..b5e2b70 100644 --- a/custom_components/ha_text_ai/__init__.py +++ b/custom_components/ha_text_ai/__init__.py @@ -8,6 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError from .const import ( DOMAIN, @@ -26,74 +27,96 @@ from .coordinator import HATextAICoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: """Set up the HA text AI component from configuration.yaml.""" hass.data.setdefault(DOMAIN, {}) - return True - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up HA text AI from a config entry.""" - coordinator = HATextAICoordinator( - hass, - api_key=entry.data[CONF_API_KEY], - endpoint=entry.data.get(CONF_API_ENDPOINT), - model=entry.data.get(CONF_MODEL), - temperature=entry.data.get(CONF_TEMPERATURE), - max_tokens=entry.data.get(CONF_MAX_TOKENS), - request_interval=entry.data.get(CONF_REQUEST_INTERVAL), - ) - - await coordinator.async_config_entry_first_refresh() - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def async_ask_question(call: ServiceCall) -> None: - """Handle the ask_question service call.""" - question = call.data["question"] - model = call.data.get("model", coordinator.model) - temperature = call.data.get("temperature", coordinator.temperature) - max_tokens = call.data.get("max_tokens", coordinator.max_tokens) - - # Temporarily update parameters if they were overridden - original_model = coordinator.model - original_temperature = coordinator.temperature - original_max_tokens = coordinator.max_tokens + """Handle the ask_question service call. + Args: + call: Service call containing question and optional parameters. + """ try: - coordinator.model = model - coordinator.temperature = temperature - coordinator.max_tokens = max_tokens - await coordinator.async_ask_question(question) - finally: - # Restore original parameters - coordinator.model = original_model - coordinator.temperature = original_temperature - coordinator.max_tokens = original_max_tokens + # Get the coordinator from the first config entry + if not hass.data[DOMAIN]: + raise HomeAssistantError("No AI Text integration configured") + + coordinator = next(iter(hass.data[DOMAIN].values())) + + question = call.data["question"] + model = call.data.get("model", coordinator.model) + temperature = call.data.get("temperature", coordinator.temperature) + max_tokens = call.data.get("max_tokens", coordinator.max_tokens) + + # Temporarily update parameters if they were overridden + original_model = coordinator.model + original_temperature = coordinator.temperature + original_max_tokens = coordinator.max_tokens + + try: + coordinator.model = model + coordinator.temperature = temperature + coordinator.max_tokens = max_tokens + await coordinator.async_ask_question(question) + finally: + # Restore original parameters + coordinator.model = original_model + coordinator.temperature = original_temperature + coordinator.max_tokens = original_max_tokens + except Exception as ex: + _LOGGER.error("Error asking question: %s", str(ex)) + raise HomeAssistantError(f"Failed to ask question: {str(ex)}") async def async_clear_history(call: ServiceCall) -> None: """Handle the clear_history service call.""" - coordinator._responses.clear() - await coordinator.async_refresh() + try: + if not hass.data[DOMAIN]: + raise HomeAssistantError("No AI Text integration configured") - async def async_get_history(call: ServiceCall) -> None: - """Handle the get_history service call.""" - limit = call.data.get("limit", 10) - history = list(coordinator._responses.items())[-limit:] - return { - "history": [ - {"question": q, "response": r} for q, r in history - ] - } + coordinator = next(iter(hass.data[DOMAIN].values())) + coordinator._responses.clear() + await coordinator.async_refresh() + except Exception as ex: + _LOGGER.error("Error clearing history: %s", str(ex)) + raise HomeAssistantError(f"Failed to clear history: {str(ex)}") + + async def async_get_history(call: ServiceCall) -> dict[str, list]: + """Handle the get_history service call. + + Returns: + Dictionary containing chat history. + """ + try: + if not hass.data[DOMAIN]: + raise HomeAssistantError("No AI Text integration configured") + + coordinator = next(iter(hass.data[DOMAIN].values())) + limit = call.data.get("limit", 10) + history = list(coordinator._responses.items())[-limit:] + return { + "history": [ + {"question": q, "response": r} for q, r in history + ] + } + except Exception as ex: + _LOGGER.error("Error getting history: %s", str(ex)) + raise HomeAssistantError(f"Failed to get history: {str(ex)}") async def async_set_system_prompt(call: ServiceCall) -> None: """Handle the set_system_prompt service call.""" - prompt = call.data["prompt"] - coordinator.system_prompt = prompt + try: + if not hass.data[DOMAIN]: + raise HomeAssistantError("No AI Text integration configured") - # Register all services + coordinator = next(iter(hass.data[DOMAIN].values())) + prompt = call.data["prompt"] + coordinator.system_prompt = prompt + except Exception as ex: + _LOGGER.error("Error setting system prompt: %s", str(ex)) + raise HomeAssistantError(f"Failed to set system prompt: {str(ex)}") + + # Register services hass.services.async_register( DOMAIN, SERVICE_ASK_QUESTION, @@ -101,8 +124,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: schema=vol.Schema({ vol.Required("question"): cv.string, vol.Optional("model"): cv.string, - vol.Optional("temperature"): vol.Coerce(float), - vol.Optional("max_tokens"): vol.Coerce(int), + vol.Optional("temperature"): vol.All( + vol.Coerce(float), vol.Range(min=0, max=2) + ), + vol.Optional("max_tokens"): vol.All( + vol.Coerce(int), vol.Range(min=1, max=4096) + ), }) ) @@ -118,7 +145,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SERVICE_GET_HISTORY, async_get_history, schema=vol.Schema({ - vol.Optional("limit"): vol.Coerce(int), + vol.Optional("limit", default=10): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), }) ) @@ -133,18 +162,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up HA text AI from a config entry.""" + try: + coordinator = HATextAICoordinator( + hass, + api_key=entry.data[CONF_API_KEY], + endpoint=entry.data.get(CONF_API_ENDPOINT), + model=entry.data.get(CONF_MODEL), + temperature=entry.data.get(CONF_TEMPERATURE), + max_tokens=entry.data.get(CONF_MAX_TOKENS), + request_interval=entry.data.get(CONF_REQUEST_INTERVAL), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + + return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + except Exception as ex: + _LOGGER.error("Error setting up entry: %s", str(ex)) + raise ConfigEntryNotReady from ex + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - # Unregister services - for service in [ - SERVICE_ASK_QUESTION, - SERVICE_CLEAR_HISTORY, - SERVICE_GET_HISTORY, - SERVICE_SET_SYSTEM_PROMPT - ]: - hass.services.async_remove(DOMAIN, service) + try: + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + # Only remove services if this is the last entry + if not hass.data[DOMAIN]: + for service in [ + SERVICE_ASK_QUESTION, + SERVICE_CLEAR_HISTORY, + SERVICE_GET_HISTORY, + SERVICE_SET_SYSTEM_PROMPT + ]: + hass.services.async_remove(DOMAIN, service) + + return unload_ok + except Exception as ex: + _LOGGER.error("Error unloading entry: %s", str(ex)) + return False diff --git a/ha-text-ai.zip b/ha-text-ai.zip new file mode 100644 index 0000000000000000000000000000000000000000..49ebdb904393a6cd48abdf06c4c1e29d431ee88b GIT binary patch literal 7717 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