From d89914414901343735031446c9550077a62fa9c7 Mon Sep 17 00:00:00 2001 From: SMKRV Date: Tue, 19 Nov 2024 14:00:33 +0300 Subject: [PATCH] Release v1.0.3 --- custom_components/ha_text_ai/__init__.py | 18 +-- custom_components/ha_text_ai/config_flow.py | 50 +++++--- custom_components/ha_text_ai/const.py | 31 +++++ custom_components/ha_text_ai/coordinator.py | 1 - custom_components/ha_text_ai/manifest.json | 2 +- custom_components/ha_text_ai/sensor.py | 64 ++++++++-- custom_components/ha_text_ai/services.yaml | 67 +++++++--- .../ha_text_ai/translations/en.json | 115 +++++++++++++++--- ha_text_ai.zip | Bin 9341 -> 11773 bytes hacs.json | 2 +- 10 files changed, 283 insertions(+), 67 deletions(-) diff --git a/custom_components/ha_text_ai/__init__.py b/custom_components/ha_text_ai/__init__.py index 5028bce..93406fe 100644 --- a/custom_components/ha_text_ai/__init__.py +++ b/custom_components/ha_text_ai/__init__.py @@ -23,10 +23,13 @@ from .const import ( CONF_API_ENDPOINT, CONF_REQUEST_INTERVAL, ) -from .coordinator import HATextAICoordinator _LOGGER = logging.getLogger(__name__) +def get_coordinator(): + """Get coordinator class with lazy import to avoid circular deps.""" + from .coordinator import HATextAICoordinator + return HATextAICoordinator CONFIG_SCHEMA = vol.Schema( { @@ -56,7 +59,6 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: coordinator = next(iter(hass.data[DOMAIN].values())) question = call.data["question"] - original_params = { "model": coordinator.model, "temperature": coordinator.temperature, @@ -64,7 +66,6 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: } try: - if "model" in call.data: coordinator.model = call.data["model"] if "temperature" in call.data: @@ -77,7 +78,6 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: _LOGGER.error("Error asking question: %s", str(ex)) raise HomeAssistantError(f"Failed to ask question: {str(ex)}") from ex finally: - coordinator.model = original_params["model"] coordinator.temperature = original_params["temperature"] coordinator.max_tokens = original_params["max_tokens"] @@ -118,7 +118,6 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: coordinator = next(iter(hass.data[DOMAIN].values())) coordinator.system_prompt = call.data["prompt"] - hass.services.async_register( DOMAIN, SERVICE_ASK_QUESTION, @@ -166,6 +165,7 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up HA text AI from a config entry.""" + HATextAICoordinator = get_coordinator() try: coordinator = HATextAICoordinator( hass, @@ -186,11 +186,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if entry.entry_id not in hass.data.get(DOMAIN, {}): + return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - + # Remove services only if this was the last instance if not hass.data[DOMAIN]: services = [ SERVICE_ASK_QUESTION, @@ -199,7 +202,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SERVICE_SET_SYSTEM_PROMPT ] for service in services: - if service in hass.services.async_services().get(DOMAIN, {}): + if DOMAIN in hass.services.async_services() and \ + service in hass.services.async_services()[DOMAIN]: hass.services.async_remove(DOMAIN, service) return unload_ok diff --git a/custom_components/ha_text_ai/config_flow.py b/custom_components/ha_text_ai/config_flow.py index 197497b..30d66c6 100644 --- a/custom_components/ha_text_ai/config_flow.py +++ b/custom_components/ha_text_ai/config_flow.py @@ -22,6 +22,9 @@ from .const import ( DEFAULT_REQUEST_INTERVAL, ) +import logging +_LOGGER = logging.getLogger(__name__) + STEP_USER_DATA_SCHEMA = vol.Schema({ vol.Required(CONF_API_KEY): str, vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str, @@ -54,35 +57,50 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: - + # Create OpenAI client client = openai.OpenAI( api_key=user_input[CONF_API_KEY], base_url=user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT) ) - await self.hass.async_add_executor_job( - client.models.list - ) + # Verify API connection and model availability + models = await self.hass.async_add_executor_job(client.models.list) + model_ids = [model.id for model in models.data] - await self.async_set_unique_id(user_input[CONF_API_KEY]) - self._abort_if_unique_id_configured() + if user_input[CONF_MODEL] not in model_ids: + _LOGGER.warning( + "Selected model %s not found in available models: %s", + user_input[CONF_MODEL], + ", ".join(model_ids) + ) + errors["base"] = "invalid_model" + else: + await self.async_set_unique_id(user_input[CONF_API_KEY]) + self._abort_if_unique_id_configured() - return self.async_create_entry( - title="HA text AI", - data=user_input - ) + return self.async_create_entry( + title="HA text AI", + data=user_input + ) - except openai.AuthenticationError: + except openai.AuthenticationError as err: + _LOGGER.error("Authentication failed: %s", str(err)) errors["base"] = "invalid_auth" - except openai.APIError: + except openai.APIError as err: + _LOGGER.error("API connection failed: %s", str(err)) errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error: %s", str(err)) errors["base"] = "unknown" return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors, + description_placeholders={ + "default_model": DEFAULT_MODEL, + "default_endpoint": DEFAULT_API_ENDPOINT, + } ) @staticmethod @@ -114,21 +132,21 @@ class OptionsFlowHandler(config_entries.OptionsFlow): default=self.config_entry.options.get( CONF_TEMPERATURE, DEFAULT_TEMPERATURE ), - description="Temperature for response generation (0-2)", + description={"suggested_value": DEFAULT_TEMPERATURE}, ): vol.All(vol.Coerce(float), vol.Range(min=0, max=2)), vol.Optional( CONF_MAX_TOKENS, default=self.config_entry.options.get( CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS ), - description="Maximum tokens in response (1-4096)", + description={"suggested_value": DEFAULT_MAX_TOKENS}, ): vol.All(vol.Coerce(int), vol.Range(min=1, max=4096)), vol.Optional( CONF_REQUEST_INTERVAL, default=self.config_entry.options.get( CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL ), - description="Minimum time between API requests (seconds)", + description={"suggested_value": DEFAULT_REQUEST_INTERVAL}, ): vol.All(vol.Coerce(float), vol.Range(min=0.1)), }) diff --git a/custom_components/ha_text_ai/const.py b/custom_components/ha_text_ai/const.py index 9830c15..4ee0ff7 100644 --- a/custom_components/ha_text_ai/const.py +++ b/custom_components/ha_text_ai/const.py @@ -43,11 +43,21 @@ SERVICE_SET_SYSTEM_PROMPT_DESCRIPTION: Final = "Set system prompt for AI model" ATTR_QUESTION: Final = "question" ATTR_RESPONSE: Final = "response" ATTR_LAST_UPDATED: Final = "last_updated" +ATTR_MODEL: Final = "model" +ATTR_TEMPERATURE: Final = "temperature" +ATTR_MAX_TOKENS: Final = "max_tokens" +ATTR_TOTAL_RESPONSES: Final = "total_responses" +ATTR_SYSTEM_PROMPT: Final = "system_prompt" +ATTR_RESPONSE_TIME: Final = "response_time" # Error messages ERROR_INVALID_API_KEY: Final = "invalid_api_key" ERROR_CANNOT_CONNECT: Final = "cannot_connect" ERROR_UNKNOWN: Final = "unknown_error" +ERROR_INVALID_MODEL: Final = "invalid_model" +ERROR_RATE_LIMIT: Final = "rate_limit_exceeded" +ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded" +ERROR_API_ERROR: Final = "api_error" # Configuration descriptions CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses" @@ -56,6 +66,27 @@ CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)" CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL" CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)" +# Entity attributes descriptions +ATTR_QUESTION_DESCRIPTION: Final = "Last question asked" +ATTR_RESPONSE_DESCRIPTION: Final = "Last response received" +ATTR_LAST_UPDATED_DESCRIPTION: Final = "Time of last update" +ATTR_MODEL_DESCRIPTION: Final = "Current AI model in use" +ATTR_TEMPERATURE_DESCRIPTION: Final = "Current temperature setting" +ATTR_MAX_TOKENS_DESCRIPTION: Final = "Current max tokens setting" +ATTR_TOTAL_RESPONSES_DESCRIPTION: Final = "Total number of responses" +ATTR_SYSTEM_PROMPT_DESCRIPTION: Final = "Current system prompt" +ATTR_RESPONSE_TIME_DESCRIPTION: Final = "Time taken for last response" + # Entity attributes ENTITY_NAME: Final = "HA Text AI" ENTITY_ICON: Final = "mdi:robot" + +# Translation keys +TRANSLATION_KEY_CONFIG: Final = "config" +TRANSLATION_KEY_OPTIONS: Final = "options" +TRANSLATION_KEY_ERROR: Final = "error" + +# State attributes +STATE_READY: Final = "ready" +STATE_PROCESSING: Final = "processing" +STATE_ERROR: Final = "error" diff --git a/custom_components/ha_text_ai/coordinator.py b/custom_components/ha_text_ai/coordinator.py index 9c360ec..a8f7614 100644 --- a/custom_components/ha_text_ai/coordinator.py +++ b/custom_components/ha_text_ai/coordinator.py @@ -3,7 +3,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN, PLATFORMS -from .coordinator import HATextAICoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up HA Text AI from a config entry.""" diff --git a/custom_components/ha_text_ai/manifest.json b/custom_components/ha_text_ai/manifest.json index 357e4d3..093e823 100644 --- a/custom_components/ha_text_ai/manifest.json +++ b/custom_components/ha_text_ai/manifest.json @@ -9,6 +9,6 @@ "issue_tracker": "https://github.com/smkrv/ha-text-ai/issues", "requirements": ["openai>=1.0.0"], "ssdp": [], - "version": "1.0.2", + "version": "1.0.3", "zeroconf": [] } diff --git a/custom_components/ha_text_ai/sensor.py b/custom_components/ha_text_ai/sensor.py index d1715f8..d7cf58f 100644 --- a/custom_components/ha_text_ai/sensor.py +++ b/custom_components/ha_text_ai/sensor.py @@ -13,8 +13,18 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util -from .const import DOMAIN, ATTR_QUESTION, ATTR_RESPONSE, ATTR_LAST_UPDATED +from .const import ( + DOMAIN, + ATTR_QUESTION, + ATTR_RESPONSE, + ATTR_LAST_UPDATED, + ATTR_MODEL, + ATTR_TEMPERATURE, + ATTR_MAX_TOKENS, + ATTR_TOTAL_RESPONSES, +) from .coordinator import HATextAICoordinator _LOGGER = logging.getLogger(__name__) @@ -46,42 +56,71 @@ class HATextAISensor(CoordinatorEntity, SensorEntity): self._config_entry = config_entry self._attr_unique_id = f"{config_entry.entry_id}" self._attr_name = "Last Response" + self._attr_suggested_display_precision = 0 @property def state(self) -> StateType: """Return the state of the sensor.""" - if not self.coordinator.data: + if not self.coordinator.data or not self.coordinator.last_update_success_time: return None + + # Convert to local time + if isinstance(self.coordinator.last_update_success_time, datetime): + return dt_util.as_local(self.coordinator.last_update_success_time) return self.coordinator.last_update_success_time @property def extra_state_attributes(self) -> Optional[Dict[str, Any]]: """Return entity specific state attributes.""" if not self.coordinator.data: - return None + return { + ATTR_TOTAL_RESPONSES: 0, + ATTR_MODEL: self.coordinator.model, + ATTR_TEMPERATURE: self.coordinator.temperature, + ATTR_MAX_TOKENS: self.coordinator.max_tokens, + } try: - history = list(self.coordinator.data.items()) if not history: - return None + return { + ATTR_TOTAL_RESPONSES: 0, + ATTR_MODEL: self.coordinator.model, + ATTR_TEMPERATURE: self.coordinator.temperature, + ATTR_MAX_TOKENS: self.coordinator.max_tokens, + } last_question, last_data = history[-1] - + # Handle different response formats if isinstance(last_data, dict): last_response = last_data.get("response", "") + last_updated = last_data.get("timestamp", self.coordinator.last_update_success_time) else: last_response = str(last_data) + last_updated = self.coordinator.last_update_success_time + + # Convert timestamp to local time if needed + if isinstance(last_updated, datetime): + last_updated = dt_util.as_local(last_updated) return { ATTR_QUESTION: last_question, ATTR_RESPONSE: last_response, - ATTR_LAST_UPDATED: self.coordinator.last_update_success_time, + ATTR_LAST_UPDATED: last_updated, + ATTR_TOTAL_RESPONSES: len(history), + ATTR_MODEL: self.coordinator.model, + ATTR_TEMPERATURE: self.coordinator.temperature, + ATTR_MAX_TOKENS: self.coordinator.max_tokens, + } + except Exception as err: + _LOGGER.error("Error getting attributes: %s", err, exc_info=True) + return { + ATTR_TOTAL_RESPONSES: 0, + ATTR_MODEL: self.coordinator.model, + ATTR_TEMPERATURE: self.coordinator.temperature, + ATTR_MAX_TOKENS: self.coordinator.max_tokens, } - except (IndexError, KeyError, AttributeError) as err: - _LOGGER.warning("Error getting attributes: %s", err) - return None @property def available(self) -> bool: @@ -92,3 +131,8 @@ class HATextAISensor(CoordinatorEntity, SensorEntity): def should_poll(self) -> bool: """No need to poll. Coordinator notifies entity of updates.""" return False + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/custom_components/ha_text_ai/services.yaml b/custom_components/ha_text_ai/services.yaml index 7a35454..b1d256b 100644 --- a/custom_components/ha_text_ai/services.yaml +++ b/custom_components/ha_text_ai/services.yaml @@ -1,35 +1,49 @@ ask_question: name: Ask Question - description: Send a question to the AI model and receive a detailed response + description: >- + Send a question to the AI model and receive a detailed response. + The response will be stored in the conversation history and can be retrieved later. fields: question: name: Question - description: Your question or prompt for the AI assistant + description: >- + Your question or prompt for the AI assistant. Be specific and clear for better results. + You can ask about home automation, technical advice, or general questions. required: true - example: "What automations would you recommend for a smart kitchen?" + example: | + What automations would you recommend for a smart kitchen? + Consider energy efficiency and convenience. selector: text: multiline: true + type: text model: name: Model - description: Select an AI model to use (optional, overrides default setting) + description: >- + Select an AI model to use (optional, overrides default setting). + Different models have different capabilities and token limits. required: false example: "gpt-3.5-turbo" default: "gpt-3.5-turbo" selector: select: options: - - label: "GPT-3.5 Turbo" + - label: "GPT-3.5 Turbo (Fast & Efficient)" value: "gpt-3.5-turbo" - - label: "GPT-4" + - label: "GPT-4 (Most Capable)" value: "gpt-4" - - label: "GPT-4 32K" + - label: "GPT-4 32K (Extended Context)" value: "gpt-4-32k" + mode: dropdown temperature: name: Temperature - description: "Controls response creativity (0-2): Lower values for focused responses, higher for creative ones" + description: >- + Controls response creativity (0-2): + 0.0-0.3: Focused, consistent responses + 0.4-0.7: Balanced responses + 0.8-2.0: More creative, varied responses required: false default: 0.7 selector: @@ -38,10 +52,16 @@ ask_question: max: 2.0 step: 0.1 mode: slider + unit_of_measurement: "" max_tokens: name: Max Tokens - description: Maximum length of the response + description: >- + Maximum length of the response. Higher values allow longer responses but use more API tokens. + Recommended ranges: + - Short responses: 256-512 + - Medium responses: 512-1024 + - Long responses: 1024-4096 required: false default: 1000 selector: @@ -53,16 +73,22 @@ ask_question: clear_history: name: Clear History - description: Delete all stored questions and responses + description: >- + Delete all stored questions and responses from the conversation history. + This action cannot be undone. fields: {} get_history: name: Get History - description: Retrieve recent conversation history + description: >- + Retrieve recent conversation history, including questions, responses, and timestamps. + Results are ordered from newest to oldest. fields: limit: name: Limit - description: Number of most recent conversations to return + description: >- + Number of most recent conversations to return. + Higher values return more history but may take longer to process. required: false default: 10 selector: @@ -74,13 +100,24 @@ get_history: set_system_prompt: name: Set System Prompt - description: Configure the AI's behavior by setting a system prompt + description: >- + Configure the AI's behavior by setting a system prompt. + This affects how the AI interprets and responds to all future questions. fields: prompt: name: System Prompt - description: Instructions that define how the AI should behave and respond + description: >- + Instructions that define how the AI should behave and respond. + Be specific about the desired expertise, tone, and format of responses. required: true - example: "You are a home automation expert assistant. Provide practical advice focused on smart home technology." + example: | + You are a home automation expert assistant. Focus on: + 1. Practical and efficient solutions + 2. Energy-saving recommendations + 3. Integration with popular smart home platforms + 4. Security and privacy considerations + Provide detailed but concise responses with clear steps when applicable. selector: text: multiline: true + type: text diff --git a/custom_components/ha_text_ai/translations/en.json b/custom_components/ha_text_ai/translations/en.json index 7245314..08661b9 100644 --- a/custom_components/ha_text_ai/translations/en.json +++ b/custom_components/ha_text_ai/translations/en.json @@ -3,14 +3,32 @@ "step": { "user": { "title": "Set up HA text AI", - "description": "Configure your OpenAI integration for smart home interactions", + "description": "Configure your OpenAI integration for smart home interactions. You'll need an OpenAI API key to proceed.", "data": { - "api_key": "OpenAI API Key", - "model": "AI Model", - "temperature": "Temperature", - "max_tokens": "Max Tokens", - "api_endpoint": "API Endpoint", - "request_interval": "Request Interval" + "api_key": { + "name": "OpenAI API Key", + "description": "Your OpenAI API key from platform.openai.com" + }, + "model": { + "name": "AI Model", + "description": "Select the AI model to use. GPT-3.5-Turbo is recommended for most uses." + }, + "temperature": { + "name": "Temperature", + "description": "Controls response creativity (0-2). Lower values for focused responses, higher for creative ones." + }, + "max_tokens": { + "name": "Max Tokens", + "description": "Maximum length of responses. Higher values allow longer responses but use more API tokens." + }, + "api_endpoint": { + "name": "API Endpoint", + "description": "OpenAI API endpoint URL. Leave default unless using a custom endpoint." + }, + "request_interval": { + "name": "Request Interval", + "description": "Minimum time between API requests in seconds. Increase if hitting rate limits." + } } } }, @@ -18,22 +36,36 @@ "invalid_auth": "Invalid API key. Please check your OpenAI API key and try again.", "cannot_connect": "Failed to connect to API. Please check your internet connection and API endpoint.", "unknown": "Unexpected error occurred. Please check the logs for more details.", - "already_exists": "This API key is already configured in another integration." + "already_exists": "This API key is already configured in another integration.", + "invalid_model": "Selected model is not available. Please choose a different model.", + "rate_limit": "API rate limit exceeded. Please try again later or increase the request interval.", + "context_length": "Input too long for selected model. Try reducing max tokens or using a model with larger context.", + "api_error": "OpenAI API error. Please check the logs for details." }, "abort": { "already_configured": "This OpenAI integration is already configured", - "auth_failed": "Authentication failed. Please verify your API key." + "auth_failed": "Authentication failed. Please verify your API key.", + "invalid_endpoint": "Invalid API endpoint URL provided" } }, "options": { "step": { "init": { "title": "HA text AI Options", - "description": "Adjust your OpenAI integration settings", + "description": "Adjust your OpenAI integration settings. Changes will apply to future requests.", "data": { - "temperature": "Temperature", - "max_tokens": "Max Tokens", - "request_interval": "Request Interval" + "temperature": { + "name": "Temperature", + "description": "Controls response creativity (0-2). Lower values for focused responses, higher for creative ones." + }, + "max_tokens": { + "name": "Max Tokens", + "description": "Maximum length of responses. Higher values allow longer responses but use more API tokens." + }, + "request_interval": { + "name": "Request Interval", + "description": "Minimum time between API requests in seconds. Increase if hitting rate limits." + } } } } @@ -44,16 +76,67 @@ "name": "Last Response", "state_attributes": { "last_updated": { - "name": "Last Updated" + "name": "Last Updated", + "description": "Time of the last AI response" }, "question": { - "name": "Last Question" + "name": "Last Question", + "description": "Most recent question asked" }, "response": { - "name": "AI Response" + "name": "AI Response", + "description": "Latest response from the AI" + }, + "model": { + "name": "Current Model", + "description": "AI model currently in use" + }, + "temperature": { + "name": "Temperature Setting", + "description": "Current temperature parameter" + }, + "max_tokens": { + "name": "Max Tokens Setting", + "description": "Current maximum tokens limit" + }, + "total_responses": { + "name": "Total Responses", + "description": "Number of responses since last reset" + }, + "system_prompt": { + "name": "System Prompt", + "description": "Current system instructions for the AI" + }, + "response_time": { + "name": "Response Time", + "description": "Time taken to generate last response" } } } } + }, + "services": { + "ask_question": { + "name": "Ask Question", + "description": "Send a question to the AI model", + "fields": { + "question": { + "name": "Question", + "description": "Your question for the AI" + } + } + }, + "clear_history": { + "name": "Clear History", + "description": "Clear conversation history" + }, + "get_history": { + "name": "Get History", + "description": "Retrieve conversation history" + }, + "set_system_prompt": { + "name": "Set System Prompt", + "description": "Set AI behavior instructions" + } } } diff --git a/ha_text_ai.zip b/ha_text_ai.zip index 9f0e90a487788436685cfb4bd9315ff46c6f8c92..d90a9116dffdae0ed60a70be4f7a076e06b2c2fe 100644 GIT binary patch literal 11773 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 S{FxKN01iO`fH#^y@BR;jA7^> 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