Release v1.0.3

This commit is contained in:
SMKRV
2024-11-19 14:00:33 +03:00
parent 5d49b4a40b
commit d899144149
10 changed files with 283 additions and 67 deletions
+11 -7
View File
@@ -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
+34 -16
View File
@@ -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)),
})
+31
View File
@@ -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"
@@ -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."""
+1 -1
View File
@@ -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": []
}
+54 -10
View File
@@ -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()
+52 -15
View File
@@ -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
@@ -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"
}
}
}
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -4,6 +4,6 @@
"domains": ["sensor"],
"homeassistant": "2024.11.0",
"icon": "mdi:brain",
"version": "1.0.2",
"version": "1.0.3",
"documentation": "https://github.com/smkrv/ha-text-ai"
}